claude-dev-env 2.8.0 → 2.10.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 (323) hide show
  1. package/CLAUDE.md +8 -2
  2. package/_shared/pr-loop/CLAUDE.md +1 -1
  3. package/_shared/pr-loop/audit-contract.md +17 -6
  4. package/_shared/pr-loop/audit-reply-template.md +4 -4
  5. package/_shared/pr-loop/code-rules-gate.md +3 -5
  6. package/_shared/pr-loop/fix-protocol.md +2 -3
  7. package/_shared/pr-loop/gh-payloads.md +1 -1
  8. package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
  9. package/_shared/pr-loop/scripts/README.md +1 -1
  10. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +16 -1
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +11 -5
  13. package/_shared/pr-loop/scripts/preflight.py +9 -4
  14. package/_shared/pr-loop/scripts/reviews_disabled.py +50 -22
  15. package/_shared/pr-loop/scripts/tests/conftest.py +20 -0
  16. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +6 -6
  17. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +50 -6
  18. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +1 -1
  19. package/_shared/pr-loop/state-schema.md +5 -14
  20. package/agents/CLAUDE.md +2 -2
  21. package/agents/clean-coder.md +62 -562
  22. package/agents/code-quality-agent.md +10 -2
  23. package/agents/code-verifier.md +1 -1
  24. package/agents/test_agent_frontmatter.py +58 -40
  25. package/audit-rubrics/CLAUDE.md +2 -1
  26. package/audit-rubrics/audit-categories.json +704 -0
  27. package/audit-rubrics/prompts/category-i-concurrency.md +1 -1
  28. package/bin/CLAUDE.md +16 -5
  29. package/bin/ever-shipped-skills.mjs +2 -0
  30. package/bin/install-plan.mjs +402 -0
  31. package/bin/install-transaction.mjs +455 -0
  32. package/bin/install.mjs +593 -147
  33. package/bin/install.plan.test.mjs +194 -0
  34. package/bin/install.profile-root.test.mjs +154 -0
  35. package/bin/install.profiles.test.mjs +253 -0
  36. package/bin/install.settings-defaults.test.mjs +200 -0
  37. package/bin/install.transaction.test.mjs +400 -0
  38. package/bin/install.uninstall-transaction.test.mjs +418 -0
  39. package/bin/merge_managed_permissions.mjs +130 -0
  40. package/bin/resolve-install-root.mjs +181 -0
  41. package/bin/select-install-targets.mjs +401 -0
  42. package/commands/CLAUDE.md +0 -2
  43. package/docs/CODE_RULES.md +4 -2
  44. package/docs/references/CLAUDE.md +3 -2
  45. package/docs/references/advisor-tool.md +44 -6
  46. package/docs/references/prose-style-enforcement.md +25 -0
  47. package/docs/references/team-advisor-skill.md +14 -8
  48. package/hooks/blocking/CLAUDE.md +6 -6
  49. package/hooks/blocking/_path_setup.py +9 -5
  50. package/hooks/blocking/code_rules_docstrings.py +124 -30
  51. package/hooks/blocking/code_rules_enforcer.py +161 -16
  52. package/hooks/blocking/code_rules_shared.py +40 -23
  53. package/hooks/blocking/config/CLAUDE.md +3 -5
  54. package/hooks/blocking/config/prose_style_enforcement_constants.py +38 -0
  55. package/hooks/blocking/config/test_prose_style_enforcement_constants.py +45 -0
  56. package/hooks/blocking/eli11_reply_enforcer.py +70 -113
  57. package/hooks/blocking/hedging_language_blocker.py +103 -20
  58. package/hooks/blocking/hook_prose_detector_consistency.py +6 -0
  59. package/hooks/blocking/intent_only_ending_blocker.py +6 -0
  60. package/hooks/blocking/plain_language_blocker.py +139 -20
  61. package/hooks/blocking/pre_tool_use_dispatcher.py +102 -20
  62. package/hooks/blocking/state_description_blocker.py +7 -1
  63. package/hooks/blocking/tdd_enforcer.py +8 -0
  64. package/hooks/blocking/test__path_setup.py +28 -0
  65. package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +99 -0
  66. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +232 -10
  67. package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -1
  68. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +41 -0
  69. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +98 -0
  70. package/hooks/blocking/test_eli11_reply_enforcer.py +98 -165
  71. package/hooks/blocking/test_hedging_language_blocker.py +120 -1
  72. package/hooks/blocking/test_hook_prose_detector_consistency.py +28 -8
  73. package/hooks/blocking/test_intent_only_ending_blocker.py +27 -2
  74. package/hooks/blocking/test_package_inventory_stale_blocker.py +11 -4
  75. package/hooks/blocking/test_plain_language_blocker.py +129 -19
  76. package/hooks/blocking/test_plain_language_blocker_allowlist.py +70 -26
  77. package/hooks/blocking/test_pre_tool_use_dispatcher.py +99 -26
  78. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +87 -50
  79. package/hooks/blocking/test_state_description_blocker.py +45 -2
  80. package/hooks/blocking/test_stop_dispatcher.py +11 -7
  81. package/hooks/blocking/test_volatile_path_in_post_blocker.py +12 -12
  82. package/hooks/blocking/volatile_path_in_post_blocker.py +2 -2
  83. package/hooks/hooks.json +15 -0
  84. package/hooks/hooks_constants/CLAUDE.md +14 -3
  85. package/hooks/hooks_constants/ask_user_question_shape.py +281 -0
  86. package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -1
  87. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  88. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +5 -12
  89. package/hooks/hooks_constants/hedging_uncertainty_constants.py +42 -0
  90. package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +23 -0
  91. package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +23 -0
  92. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +4 -1
  93. package/hooks/hooks_constants/plain_language_blocker_constants.py +4 -1
  94. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -0
  95. package/hooks/hooks_constants/project_paths_reader.py +31 -4
  96. package/hooks/hooks_constants/prose_matcher_precision_constants.py +40 -0
  97. package/hooks/hooks_constants/pytest_invocation.py +354 -0
  98. package/hooks/hooks_constants/session_start_injector.py +163 -0
  99. package/hooks/hooks_constants/session_start_injector_constants.py +46 -0
  100. package/hooks/hooks_constants/shell_command_pipeline.py +397 -0
  101. package/hooks/hooks_constants/shell_command_segments.py +5 -0
  102. package/hooks/hooks_constants/test_ask_user_question_shape.py +167 -0
  103. package/hooks/hooks_constants/test_project_paths_reader.py +29 -0
  104. package/hooks/hooks_constants/test_prose_metrics_parity.py +8 -0
  105. package/hooks/hooks_constants/test_pytest_invocation.py +130 -0
  106. package/hooks/hooks_constants/test_session_start_injector.py +168 -0
  107. package/hooks/hooks_constants/test_shell_command_pipeline.py +135 -0
  108. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +1 -1
  109. package/hooks/hooks_constants/working_style_prompt_constants.py +30 -0
  110. package/hooks/observability/CLAUDE.md +2 -0
  111. package/hooks/observability/prose_matcher_advisory.py +237 -0
  112. package/hooks/observability/test_prose_matcher_advisory.py +143 -0
  113. package/hooks/session/CLAUDE.md +9 -1
  114. package/hooks/session/_path_setup.py +13 -0
  115. package/hooks/session/issue_tracker_session_starter.py +135 -0
  116. package/hooks/session/orchestrator_auto_starter.py +100 -0
  117. package/hooks/session/test__path_setup.py +28 -0
  118. package/hooks/session/test_issue_tracker_session_starter.py +104 -0
  119. package/hooks/session/test_orchestrator_auto_starter.py +99 -0
  120. package/hooks/session/test_working_style_prompt.py +47 -0
  121. package/hooks/session/untracked_repo_detector.py +1 -24
  122. package/hooks/session/working_style_prompt.py +36 -0
  123. package/hooks/validators/_path_setup.py +19 -0
  124. package/hooks/validators/run_all_validators.py +8 -13
  125. package/installable-surfaces.manifest.json +21 -0
  126. package/output-styles/CLAUDE.md +15 -0
  127. package/package.json +5 -2
  128. package/rules/CLAUDE.md +1 -0
  129. package/rules/code-standards.md +33 -7
  130. package/rules/durable-post-artifacts.md +2 -2
  131. package/rules/eli11-replies.md +7 -2
  132. package/rules/hedging-claims.md +4 -2
  133. package/rules/long-horizon-autonomy.md +3 -1
  134. package/rules/opus5-communication-contract.md +45 -0
  135. package/rules/plain-language.md +2 -2
  136. package/rules/research-mode.md +1 -1
  137. package/scripts/CLAUDE.md +13 -2
  138. package/scripts/Sync-RepoMain.ps1 +215 -0
  139. package/scripts/active_capability_references.py +218 -0
  140. package/scripts/ci/windows-installer-lifecycle.ps1 +78 -0
  141. package/scripts/claude_chain_runner.py +394 -6
  142. package/scripts/claude_chain_usage.py +1 -1
  143. package/scripts/codex_compat_materializer.py +105 -85
  144. package/scripts/dev_env_scripts_constants/CLAUDE.md +3 -1
  145. package/scripts/dev_env_scripts_constants/active_capability_constants.py +46 -0
  146. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +74 -0
  147. package/scripts/dev_env_scripts_constants/grok_run_ledger_constants.py +50 -0
  148. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +104 -0
  149. package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
  150. package/scripts/grok_patch_artifacts.py +123 -0
  151. package/scripts/grok_run_ledger.py +318 -0
  152. package/scripts/profile-isolation-launchers/config/mcp-bundles.json +25 -0
  153. package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +60 -0
  154. package/scripts/profile-isolation-launchers/config/profiles.manifest.json +54 -0
  155. package/scripts/profile-isolation-launchers/config/shared-allowlist.json +64 -0
  156. package/scripts/profile-isolation-launchers/launcher-runtime.mjs +180 -0
  157. package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +288 -0
  158. package/scripts/profile-isolation-launchers/mcp-bundles.mjs +275 -0
  159. package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +221 -0
  160. package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +108 -0
  161. package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +147 -0
  162. package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +102 -0
  163. package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +210 -0
  164. package/scripts/profile-isolation-launchers/version-compatibility.mjs +299 -0
  165. package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +127 -0
  166. package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +51 -0
  167. package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +77 -0
  168. package/scripts/spawn_grok_batch.py +556 -9
  169. package/scripts/test_active_capability_references.py +108 -0
  170. package/scripts/test_claude_chain_runner.py +414 -82
  171. package/scripts/test_claude_chain_usage.py +12 -12
  172. package/scripts/test_grok_patch_artifacts.py +82 -0
  173. package/scripts/test_grok_run_ledger.py +116 -0
  174. package/scripts/test_resolve_worker_spawn.py +2 -2
  175. package/scripts/test_spawn_grok_batch.py +295 -0
  176. package/scripts/test_verify_installable_package.py +208 -0
  177. package/scripts/tests/test_codex_compat_materializer.py +33 -0
  178. package/scripts/verify_installable_package.py +612 -0
  179. package/settings.json +10 -0
  180. package/skills/CLAUDE.md +6 -2
  181. package/skills/_shared/CLAUDE.md +37 -4
  182. package/skills/_shared/advisor/CLAUDE.md +9 -0
  183. package/skills/_shared/advisor/advisor-protocol.md +5 -0
  184. package/skills/_shared/advisor/scripts/README.md +9 -0
  185. package/skills/_shared/end-of-run-gotcha-recommendations.md +156 -0
  186. package/skills/_shared/pr-loop/CLAUDE.md +18 -1
  187. package/skills/_shared/pr-loop/audit-contract.md +5 -0
  188. package/skills/_shared/pr-loop/audit-reply-template.md +5 -0
  189. package/skills/_shared/pr-loop/code-rules-gate.md +5 -0
  190. package/skills/_shared/pr-loop/fix-protocol.md +5 -0
  191. package/skills/_shared/pr-loop/gh-payloads.md +5 -0
  192. package/skills/_shared/pr-loop/post-audit-thread-contract.md +5 -0
  193. package/skills/_shared/pr-loop/precatch-rubric.md +5 -0
  194. package/skills/_shared/pr-loop/scripts/CLAUDE.md +9 -1
  195. package/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +29 -0
  196. package/skills/_shared/pr-loop/scripts/audit_category_schema.py +355 -0
  197. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
  198. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +32 -0
  199. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +7 -19
  200. package/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +94 -0
  201. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +21 -0
  202. package/skills/_shared/pr-loop/state-schema.md +5 -0
  203. package/skills/_shared/pr-loop/worker-spawn.md +5 -0
  204. package/skills/autoconverge/reference/convergence.md +2 -1
  205. package/skills/autoconverge/reference/stop-conditions.md +5 -3
  206. package/skills/beat-sheet/SKILL.md +54 -0
  207. package/skills/beat-sheet/reference/visual-beats.md +29 -0
  208. package/skills/bugteam/CONSTRAINTS.md +4 -4
  209. package/skills/bugteam/EXAMPLES.md +1 -1
  210. package/skills/bugteam/reference/README.md +1 -1
  211. package/skills/e-code-review/SKILL.md +29 -3
  212. package/skills/e-code-review/reference/effort-evaluation.md +35 -0
  213. package/skills/e-code-review/reference/medium.md +15 -4
  214. package/skills/e-code-review/reference/runner-selection.md +40 -0
  215. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +41 -0
  216. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +40 -0
  217. package/skills/e-code-review/scripts/e_code_review_scripts_constants/__init__.py +1 -0
  218. package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
  219. package/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +55 -0
  220. package/skills/e-code-review/scripts/effort_defaults_evidence.json +186 -0
  221. package/skills/e-code-review/scripts/effort_evaluation.py +362 -0
  222. package/skills/e-code-review/scripts/finding_pipeline.py +140 -0
  223. package/skills/e-code-review/scripts/fixtures/demanding.json +26 -0
  224. package/skills/e-code-review/scripts/fixtures/easy.json +14 -0
  225. package/skills/e-code-review/scripts/fixtures/medium.json +20 -0
  226. package/skills/e-code-review/scripts/grok_code_review.py +230 -0
  227. package/skills/e-code-review/scripts/test_effort_evaluation.py +180 -0
  228. package/skills/e-code-review/scripts/test_finding_pipeline.py +197 -0
  229. package/skills/e-code-review/scripts/test_grok_code_review.py +289 -0
  230. package/skills/grok-spawn/SKILL.md +5 -0
  231. package/skills/orchestrator/SKILL.md +5 -0
  232. package/skills/plan-to-pr/scripts/create_packet.py +4 -4
  233. package/skills/plan-to-pr/scripts/load_skill_constants.py +41 -0
  234. package/skills/plan-to-pr/scripts/validate_packet.py +4 -4
  235. package/skills/plan-to-pr/scripts/validate_protocol.py +4 -1
  236. package/skills/plan-to-pr/scripts/validate_run.py +4 -1
  237. package/skills/pr-converge/scripts/check_convergence.py +21 -19
  238. package/skills/pr-converge/scripts/check_convergence_availability.py +50 -7
  239. package/skills/pr-converge/scripts/conftest.py +35 -0
  240. package/skills/pr-converge/scripts/test_check_convergence_availability.py +65 -0
  241. package/skills/pr-converge/scripts/test_check_convergence_codex.py +11 -1
  242. package/skills/pr-converge/scripts/test_check_convergence_contract.py +9 -2
  243. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  244. package/skills/rebase/SKILL.md +15 -3
  245. package/skills/reviewer-gates/SKILL.md +2 -2
  246. package/skills/show/SKILL.md +51 -0
  247. package/skills/show/references/accessibility.md +7 -0
  248. package/skills/show/references/art.md +3 -0
  249. package/skills/show/references/charts.md +3 -0
  250. package/skills/show/references/core-design.md +14 -0
  251. package/skills/show/references/erds.md +3 -0
  252. package/skills/show/references/flowcharts.md +3 -0
  253. package/skills/show/references/host-and-html.md +3 -0
  254. package/skills/show/references/illustrative-diagrams.md +10 -0
  255. package/skills/show/references/interaction.md +3 -0
  256. package/skills/show/references/mockups.md +3 -0
  257. package/skills/show/references/quality-gates.md +7 -0
  258. package/skills/show/references/structural-diagrams.md +3 -0
  259. package/skills/show/references/subject-inventory.md +21 -0
  260. package/skills/show/references/svg-contract.md +22 -0
  261. package/skills/show/routing.yaml +30 -0
  262. package/skills/show/samples/pr1262-v2.svg +222 -0
  263. package/skills/show/scripts/README.md +6 -0
  264. package/skills/show/scripts/validate-artifact.py +91 -0
  265. package/skills/show/scripts/validate-package.py +18 -0
  266. package/skills/show/templates/html-widget.html +4 -0
  267. package/skills/show/templates/svg-base.svg +19 -0
  268. package/skills/show/tests/fixtures/css-var.svg +6 -0
  269. package/skills/show/tests/fixtures/dead-ref.svg +7 -0
  270. package/skills/show/tests/fixtures/filled-glyph.svg +8 -0
  271. package/skills/show/tests/fixtures/inherited-fill.svg +18 -0
  272. package/skills/show/tests/fixtures/invalid.svg +1 -0
  273. package/skills/show/tests/fixtures/large-canvas.svg +21 -0
  274. package/skills/show/tests/fixtures/unfilled-connector.svg +15 -0
  275. package/skills/show/tests/fixtures/valid.html +1 -0
  276. package/skills/show/tests/test_validate-artifact.py +74 -0
  277. package/skills/show/tests/test_validators.py +59 -0
  278. package/skills/show/workflows/create-visual.md +13 -0
  279. package/skills/show/workflows/review-visual.md +20 -0
  280. package/skills/split-pr/SKILL.md +85 -0
  281. package/skills/split-pr/reference/path-layers.md +16 -0
  282. package/skills/split-pr/reference/proposal-format.md +15 -0
  283. package/skills/split-pr/reference/split-further-loop.md +10 -0
  284. package/skills/split-pr/reference/splitting-principles.md +26 -0
  285. package/skills/split-pr/scripts/analyze_pr.py +279 -0
  286. package/skills/split-pr/scripts/categorize_files.py +106 -0
  287. package/skills/split-pr/scripts/config/__init__.py +1 -0
  288. package/skills/split-pr/scripts/config/dependency_constants.py +14 -0
  289. package/skills/split-pr/scripts/config/git_operations_constants.py +36 -0
  290. package/skills/split-pr/scripts/config/packing_constants.py +61 -0
  291. package/skills/split-pr/scripts/config/plan_constants.py +49 -0
  292. package/skills/split-pr/scripts/config/split_pr_constants.py +110 -0
  293. package/skills/split-pr/scripts/execute_split_slices.py +82 -0
  294. package/skills/split-pr/scripts/pack_files_into_slices.py +212 -0
  295. package/skills/split-pr/scripts/split_pr_dependency_graph.py +70 -0
  296. package/skills/split-pr/scripts/split_pr_git_operations.py +184 -0
  297. package/skills/split-pr/scripts/split_pr_layer_order.py +58 -0
  298. package/skills/split-pr/scripts/split_pr_paginate.py +119 -0
  299. package/skills/split-pr/scripts/split_pr_process_runner.py +52 -0
  300. package/skills/split-pr/scripts/split_pr_script_types.py +126 -0
  301. package/skills/split-pr/scripts/split_pr_title.py +41 -0
  302. package/skills/split-pr/scripts/test_analyze_pr.py +228 -0
  303. package/skills/split-pr/scripts/test_categorize_files.py +55 -0
  304. package/skills/split-pr/scripts/test_categorize_files_packing.py +59 -0
  305. package/skills/split-pr/scripts/test_execute_split_slices.py +99 -0
  306. package/skills/split-pr/scripts/test_split_pr_dependency_graph.py +47 -0
  307. package/skills/split-pr/scripts/test_split_pr_git_operations.py +125 -0
  308. package/skills/split-pr/scripts/test_split_pr_layer_order.py +36 -0
  309. package/skills/split-pr/scripts/test_split_pr_paginate.py +65 -0
  310. package/skills/split-pr/scripts/test_split_pr_script_types.py +73 -0
  311. package/skills/split-pr/scripts/test_split_pr_title.py +28 -0
  312. package/skills/split-pr/scripts/test_verify_dependency_graph.py +46 -0
  313. package/skills/split-pr/scripts/test_verify_plan.py +56 -0
  314. package/skills/split-pr/scripts/test_verify_plan_contract.py +50 -0
  315. package/skills/split-pr/scripts/test_verify_plan_path_normalization.py +45 -0
  316. package/skills/split-pr/scripts/verify_dependency_graph.py +111 -0
  317. package/skills/split-pr/scripts/verify_plan.py +139 -0
  318. package/skills/task-build/reference/tool-routing.md +3 -0
  319. package/skills/team-advisor/SKILL.md +23 -44
  320. package/system-prompts/software-engineer.xml +17 -5
  321. package/commands/initialize.md +0 -90
  322. package/commands/stubcheck.md +0 -88
  323. package/skills/test_markdown_link_integrity.py +0 -107
@@ -84,7 +84,7 @@ def test_remaining_percent_is_full_scale_minus_utilization(
84
84
  tmp_path,
85
85
  [
86
86
  _entry("claude", credentials_path=PLACEHOLDER_CREDENTIALS_PRIMARY),
87
- _entry("claude-ev", credentials_path=PLACEHOLDER_CREDENTIALS_SECONDARY),
87
+ _entry("claude-profile-c", credentials_path=PLACEHOLDER_CREDENTIALS_SECONDARY),
88
88
  ],
89
89
  )
90
90
  monkeypatch.setattr(
@@ -100,7 +100,7 @@ def test_remaining_percent_is_full_scale_minus_utilization(
100
100
  all_reports = usage.report_chain_weekly_usage(config_path=config_file)
101
101
  assert [each_report.command for each_report in all_reports] == [
102
102
  "claude",
103
- "claude-ev",
103
+ "claude-profile-c",
104
104
  ]
105
105
  assert all_reports[0].weekly_remaining_percent == pytest.approx(
106
106
  FULL_WEEKLY_PERCENT - 42.0
@@ -143,7 +143,7 @@ def test_probe_failure_yields_null_remaining_and_error_string(
143
143
  tmp_path,
144
144
  [
145
145
  _entry("claude", credentials_path=PLACEHOLDER_CREDENTIALS_PRIMARY),
146
- _entry("claude-ev", credentials_path=PLACEHOLDER_CREDENTIALS_SECONDARY),
146
+ _entry("claude-profile-c", credentials_path=PLACEHOLDER_CREDENTIALS_SECONDARY),
147
147
  ],
148
148
  )
149
149
  monkeypatch.setattr(
@@ -217,7 +217,7 @@ def test_entry_credentials_path_is_passed_to_probe(
217
217
  ) -> None:
218
218
  config_file = _write_chain_config(
219
219
  tmp_path,
220
- [_entry("claude-ev", credentials_path=PLACEHOLDER_CREDENTIALS_TERTIARY)],
220
+ [_entry("claude-profile-c", credentials_path=PLACEHOLDER_CREDENTIALS_TERTIARY)],
221
221
  )
222
222
  probed_paths: list[Path] = []
223
223
 
@@ -236,7 +236,7 @@ def test_entry_credentials_path_expands_user_home(
236
236
  tilde_credentials_path = "~/.claude-accounts/secondary/.credentials.json"
237
237
  config_file = _write_chain_config(
238
238
  tmp_path,
239
- [_entry("claude-ev", credentials_path=tilde_credentials_path)],
239
+ [_entry("claude-profile-c", credentials_path=tilde_credentials_path)],
240
240
  )
241
241
  probed_paths: list[Path] = []
242
242
 
@@ -254,7 +254,7 @@ def test_load_chain_carries_optional_credentials_path(tmp_path: Path) -> None:
254
254
  tmp_path,
255
255
  [
256
256
  _entry("claude"),
257
- _entry("claude-ev", credentials_path=PLACEHOLDER_CREDENTIALS_SECONDARY),
257
+ _entry("claude-profile-c", credentials_path=PLACEHOLDER_CREDENTIALS_SECONDARY),
258
258
  ],
259
259
  )
260
260
  all_entries = chain_runner.load_chain(config_file)
@@ -286,7 +286,7 @@ def test_reports_to_json_payload_matches_cli_contract() -> None:
286
286
  all_reports = [
287
287
  usage.AccountUsageReport(command="claude", weekly_remaining_percent=58.0),
288
288
  usage.AccountUsageReport(
289
- command="claude-ev",
289
+ command="claude-profile-c",
290
290
  weekly_remaining_percent=None,
291
291
  error="probe failed",
292
292
  ),
@@ -299,7 +299,7 @@ def test_reports_to_json_payload_matches_cli_contract() -> None:
299
299
  JSON_WEEKLY_REMAINING_PERCENT_KEY: 58.0,
300
300
  },
301
301
  {
302
- JSON_COMMAND_KEY: "claude-ev",
302
+ JSON_COMMAND_KEY: "claude-profile-c",
303
303
  JSON_WEEKLY_REMAINING_PERCENT_KEY: None,
304
304
  JSON_ERROR_KEY: "probe failed",
305
305
  },
@@ -314,7 +314,7 @@ def test_cli_writes_json_accounts_report(
314
314
  tmp_path,
315
315
  [
316
316
  _entry("claude", credentials_path=PLACEHOLDER_CREDENTIALS_PRIMARY),
317
- _entry("claude-ev", credentials_path=PLACEHOLDER_CREDENTIALS_SECONDARY),
317
+ _entry("claude-profile-c", credentials_path=PLACEHOLDER_CREDENTIALS_SECONDARY),
318
318
  ],
319
319
  )
320
320
  monkeypatch.setattr(
@@ -341,7 +341,7 @@ def test_cli_writes_json_accounts_report(
341
341
  JSON_WEEKLY_REMAINING_PERCENT_KEY: FULL_WEEKLY_PERCENT - 42.0,
342
342
  },
343
343
  {
344
- JSON_COMMAND_KEY: "claude-ev",
344
+ JSON_COMMAND_KEY: "claude-profile-c",
345
345
  JSON_WEEKLY_REMAINING_PERCENT_KEY: None,
346
346
  JSON_ERROR_KEY: "no token",
347
347
  },
@@ -428,7 +428,7 @@ def test_probe_ignores_ingress_when_credential_token_missing(
428
428
  ) -> None:
429
429
  config_file = _write_chain_config(
430
430
  tmp_path,
431
- [_entry("claude-ev", credentials_path=PLACEHOLDER_CREDENTIALS_SECONDARY)],
431
+ [_entry("claude-profile-c", credentials_path=PLACEHOLDER_CREDENTIALS_SECONDARY)],
432
432
  )
433
433
 
434
434
  class _FakeResolver:
@@ -462,7 +462,7 @@ def test_load_failure_yields_per_account_error_and_cli_exits_zero(
462
462
  tmp_path,
463
463
  [
464
464
  _entry("claude", credentials_path=PLACEHOLDER_CREDENTIALS_PRIMARY),
465
- _entry("claude-ev", credentials_path=PLACEHOLDER_CREDENTIALS_SECONDARY),
465
+ _entry("claude-profile-c", credentials_path=PLACEHOLDER_CREDENTIALS_SECONDARY),
466
466
  ],
467
467
  )
468
468
 
@@ -0,0 +1,82 @@
1
+ """Behavioral tests for patch artifact extraction and manifests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ _SCRIPTS_DIR = Path(__file__).resolve().parent
11
+ if str(_SCRIPTS_DIR) not in sys.path:
12
+ sys.path.insert(0, str(_SCRIPTS_DIR))
13
+
14
+ from grok_patch_artifacts import ( # noqa: E402
15
+ compute_sha256_hex,
16
+ extract_worktree_diff,
17
+ write_patch_manifest,
18
+ )
19
+
20
+
21
+ def _init_repo(path: Path) -> str:
22
+ subprocess.check_call(["git", "init"], cwd=path, stdout=subprocess.DEVNULL)
23
+ subprocess.check_call(
24
+ ["git", "config", "user.email", "test@example.com"], cwd=path, stdout=subprocess.DEVNULL
25
+ )
26
+ subprocess.check_call(
27
+ ["git", "config", "user.name", "test"], cwd=path, stdout=subprocess.DEVNULL
28
+ )
29
+ sample = path / "sample.txt"
30
+ sample.write_text("one\n", encoding="utf-8")
31
+ subprocess.check_call(["git", "add", "sample.txt"], cwd=path, stdout=subprocess.DEVNULL)
32
+ subprocess.check_call(
33
+ ["git", "commit", "-m", "init"], cwd=path, stdout=subprocess.DEVNULL
34
+ )
35
+ base = subprocess.check_output(
36
+ ["git", "rev-parse", "HEAD"], cwd=path, text=True
37
+ ).strip()
38
+ sample.write_text("one\ntwo\n", encoding="utf-8")
39
+ return base
40
+
41
+
42
+ def test_write_patch_manifest_binds_hash_paths_and_report(tmp_path: Path) -> None:
43
+ worktree = tmp_path / "wt"
44
+ worktree.mkdir()
45
+ base_sha = _init_repo(worktree)
46
+ run_dir = tmp_path / "run"
47
+ report_text = '{"status":"ok"}'
48
+ manifest = write_patch_manifest(
49
+ run_state_directory=run_dir,
50
+ task_id="O-04",
51
+ base_sha=base_sha,
52
+ worktree_path=worktree,
53
+ worker_report_text=report_text,
54
+ )
55
+ assert manifest["task_id"] == "O-04"
56
+ assert manifest["base_sha"] == base_sha
57
+ assert "sample.txt" in manifest["changed_paths"]
58
+ patch_path = Path(str(manifest["patch_path"]))
59
+ assert patch_path.is_file()
60
+ assert manifest["content_sha256"] == compute_sha256_hex(patch_path.read_bytes())
61
+ assert manifest["worker_report_sha256"] == compute_sha256_hex(
62
+ report_text.encode("utf-8")
63
+ )
64
+ on_disk = json.loads((run_dir / "patch-manifest.json").read_text(encoding="utf-8"))
65
+ assert on_disk["content_sha256"] == manifest["content_sha256"]
66
+
67
+
68
+ def test_compute_sha256_hex_is_stable() -> None:
69
+ digest = compute_sha256_hex(b"abc")
70
+ assert digest == compute_sha256_hex(b"abc")
71
+ assert digest != compute_sha256_hex(b"abd")
72
+
73
+
74
+ def test_extract_worktree_diff_returns_paths(tmp_path: Path) -> None:
75
+ worktree = tmp_path / "wt"
76
+ worktree.mkdir()
77
+ base_sha = _init_repo(worktree)
78
+ diff_text, all_changed_paths = extract_worktree_diff(
79
+ worktree_path=worktree, base_sha=base_sha
80
+ )
81
+ assert "sample.txt" in all_changed_paths
82
+ assert "two" in diff_text or "sample" in diff_text
@@ -0,0 +1,116 @@
1
+ """Behavioral tests for the host-neutral Grok run ledger."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ _SCRIPTS_DIR = Path(__file__).resolve().parent
12
+ if str(_SCRIPTS_DIR) not in sys.path:
13
+ sys.path.insert(0, str(_SCRIPTS_DIR))
14
+
15
+ from dev_env_scripts_constants.grok_run_ledger_constants import ( # noqa: E402
16
+ TASK_STATUS_ADVISOR_BLOCKED,
17
+ TASK_STATUS_COMPLETED,
18
+ TASK_STATUS_PENDING,
19
+ TASK_STATUS_PENDING_REVIEW,
20
+ )
21
+ from grok_run_ledger import GrokRunLedger, is_legal_status # noqa: E402
22
+
23
+
24
+ def test_register_task_persists_atomically(tmp_path: Path) -> None:
25
+ ledger = GrokRunLedger(tmp_path)
26
+ ledger.register_task(task_id="O-04", all_dependencies=())
27
+ reloaded = GrokRunLedger(tmp_path)
28
+ record = reloaded.get_task("O-04")
29
+ assert record.status == TASK_STATUS_PENDING
30
+ assert is_legal_status(record.status)
31
+ payload = json.loads((tmp_path / "grok-run-ledger.json").read_text(encoding="utf-8"))
32
+ assert payload["tasks"][0]["task_id"] == "O-04"
33
+
34
+
35
+ def test_dependencies_block_dispatch(tmp_path: Path) -> None:
36
+ ledger = GrokRunLedger(tmp_path)
37
+ ledger.register_task(task_id="dep", all_dependencies=())
38
+ ledger.register_task(task_id="child", all_dependencies=("dep",))
39
+ assert ledger.can_dispatch("child") is False
40
+ with pytest.raises(ValueError, match="dependencies"):
41
+ ledger.mark_in_progress(
42
+ task_id="child",
43
+ owner_id="w1",
44
+ advisor_session_id="s1",
45
+ base_sha="aaa",
46
+ )
47
+ ledger.mark_in_progress(
48
+ task_id="dep", owner_id="w0", advisor_session_id="s0", base_sha="aaa"
49
+ )
50
+ ledger.mark_completed(
51
+ task_id="dep",
52
+ reviewed_head="bbb",
53
+ all_changed_paths=(),
54
+ advisor_verdict="ENDORSE",
55
+ all_acceptance_mapping={},
56
+ all_test_evidence=["ok"],
57
+ )
58
+ assert ledger.can_dispatch("child") is True
59
+
60
+
61
+ def test_one_live_owner_and_unique_advisor_session(tmp_path: Path) -> None:
62
+ ledger = GrokRunLedger(tmp_path)
63
+ ledger.register_task(task_id="a", all_dependencies=())
64
+ ledger.register_task(task_id="b", all_dependencies=())
65
+ ledger.mark_in_progress(
66
+ task_id="a", owner_id="owner", advisor_session_id="sess-a", base_sha="1"
67
+ )
68
+ with pytest.raises(ValueError, match="owner already live"):
69
+ ledger.mark_in_progress(
70
+ task_id="b", owner_id="owner", advisor_session_id="sess-b", base_sha="1"
71
+ )
72
+ with pytest.raises(ValueError, match="advisor session already bound"):
73
+ ledger.mark_in_progress(
74
+ task_id="b", owner_id="other", advisor_session_id="sess-a", base_sha="1"
75
+ )
76
+
77
+
78
+ def test_snapshot_drift_moves_to_pending_review(tmp_path: Path) -> None:
79
+ ledger = GrokRunLedger(tmp_path)
80
+ ledger.register_task(task_id="t", all_dependencies=())
81
+ ledger.mark_in_progress(
82
+ task_id="t", owner_id="w", advisor_session_id="s", base_sha="base"
83
+ )
84
+ record = ledger.invalidate_on_snapshot_drift(task_id="t", current_sha="drifted")
85
+ assert record.status == TASK_STATUS_PENDING_REVIEW
86
+ assert record.owner_id is None
87
+
88
+
89
+ def test_advisor_blocked_terminal(tmp_path: Path) -> None:
90
+ ledger = GrokRunLedger(tmp_path)
91
+ ledger.register_task(task_id="t", all_dependencies=())
92
+ ledger.mark_in_progress(
93
+ task_id="t", owner_id="w", advisor_session_id="s", base_sha="base"
94
+ )
95
+ record = ledger.mark_advisor_blocked(task_id="t", reason="bind failed")
96
+ assert record.status == TASK_STATUS_ADVISOR_BLOCKED
97
+ assert "bind failed" in record.test_evidence[0]
98
+
99
+
100
+ def test_completed_records_acceptance_and_head(tmp_path: Path) -> None:
101
+ ledger = GrokRunLedger(tmp_path)
102
+ ledger.register_task(task_id="t", all_dependencies=())
103
+ ledger.mark_in_progress(
104
+ task_id="t", owner_id="w", advisor_session_id="s", base_sha="base"
105
+ )
106
+ record = ledger.mark_completed(
107
+ task_id="t",
108
+ reviewed_head="head",
109
+ all_changed_paths=("a.py",),
110
+ advisor_verdict="ENDORSE",
111
+ all_acceptance_mapping={"criterion": "evidence"},
112
+ all_test_evidence=["pytest -q"],
113
+ )
114
+ assert record.status == TASK_STATUS_COMPLETED
115
+ assert record.reviewed_head == "head"
116
+ assert record.changed_paths == ("a.py",)
@@ -1141,7 +1141,7 @@ def test_usage_limit_fallover_delivers_full_prompt_to_each_binary(
1141
1141
  "load_chain",
1142
1142
  lambda _config_path: [
1143
1143
  chain_runner.ChainEntry(command="claude", extra_args=()),
1144
- chain_runner.ChainEntry(command="claude-ev", extra_args=()),
1144
+ chain_runner.ChainEntry(command="claude-profile-c", extra_args=()),
1145
1145
  ],
1146
1146
  )
1147
1147
  prompt_text_by_command: dict[str, str] = {}
@@ -1179,6 +1179,6 @@ def test_usage_limit_fallover_delivers_full_prompt_to_each_binary(
1179
1179
  )
1180
1180
 
1181
1181
  assert prompt_text_by_command["claude"] == FIXTURE_PROMPT_TEXT
1182
- assert prompt_text_by_command["claude-ev"] == FIXTURE_PROMPT_TEXT
1182
+ assert prompt_text_by_command["claude-profile-c"] == FIXTURE_PROMPT_TEXT
1183
1183
  assert outcome.tier_used == TIER_CLAUDE_HEADLESS
1184
1184
  assert outcome.is_ok is True
@@ -1319,3 +1319,298 @@ def test_load_batch_spec_rejects_empty_agent_name(tmp_path: Path) -> None:
1319
1319
 
1320
1320
  with pytest.raises(ValueError, match=WORKER_SPEC_AGENT_NAME_KEY):
1321
1321
  batch.load_batch_spec(specification_path)
1322
+
1323
+ # --- O-02 worker advisor contract ---
1324
+
1325
+
1326
+ def test_extract_advisor_signal_accepts_four_tokens_only() -> None:
1327
+ assert batch.extract_advisor_signal("ENDORSE\nok") == "ENDORSE"
1328
+ assert batch.extract_advisor_signal("CORRECTION fix path") == "CORRECTION"
1329
+ assert batch.extract_advisor_signal("PLAN later") == "PLAN"
1330
+ assert batch.extract_advisor_signal("STOP") == "STOP"
1331
+ assert batch.extract_advisor_signal("hello ENDORSE") is None
1332
+ assert batch.extract_advisor_signal("") is None
1333
+
1334
+
1335
+ def test_load_batch_spec_parses_advisor_block(tmp_path: Path) -> None:
1336
+ header_part, body_part = _write_prompt_parts(tmp_path)
1337
+ payload = _worker_payload(
1338
+ role_name="lens",
1339
+ all_prompt_parts=[str(header_part), str(body_part)],
1340
+ working_directory=tmp_path,
1341
+ tool_profile=TOOL_PROFILE_READONLY,
1342
+ )
1343
+ specification_path = tmp_path / "batch-spec.json"
1344
+ specification_path.write_text(
1345
+ json.dumps(
1346
+ {
1347
+ "role": DEFAULT_ROLE,
1348
+ "should_ping": False,
1349
+ "workers": [payload],
1350
+ "advisor": {
1351
+ "launcher": "fixture-advisor-launcher",
1352
+ "model": "opus",
1353
+ "effort": "high",
1354
+ },
1355
+ }
1356
+ ),
1357
+ encoding=UTF8_ENCODING,
1358
+ )
1359
+ loaded = batch.load_batch_spec(specification_path)
1360
+ assert loaded.advisor is not None
1361
+ assert loaded.advisor.launcher == "fixture-advisor-launcher"
1362
+ assert loaded.advisor.model == "opus"
1363
+ assert loaded.advisor.effort == "high"
1364
+
1365
+
1366
+ def test_unique_advisor_sessions_and_completion_verdict(
1367
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1368
+ ) -> None:
1369
+ header_a, body_a = _write_prompt_parts(tmp_path, role_marker="alpha")
1370
+ header_b, body_b = _write_prompt_parts(tmp_path, role_marker="beta")
1371
+ workers = [
1372
+ _worker_payload(
1373
+ role_name="alpha",
1374
+ all_prompt_parts=[str(header_a), str(body_a)],
1375
+ working_directory=tmp_path,
1376
+ tool_profile=TOOL_PROFILE_BUILD,
1377
+ ),
1378
+ _worker_payload(
1379
+ role_name="beta",
1380
+ all_prompt_parts=[str(header_b), str(body_b)],
1381
+ working_directory=tmp_path,
1382
+ tool_profile=TOOL_PROFILE_BUILD,
1383
+ ),
1384
+ ]
1385
+ specification_path = tmp_path / "batch-spec.json"
1386
+ specification_path.write_text(
1387
+ json.dumps(
1388
+ {
1389
+ "role": DEFAULT_ROLE,
1390
+ "should_ping": False,
1391
+ "workers": workers,
1392
+ "advisor": {
1393
+ "launcher": "fixture-advisor-launcher",
1394
+ "model": "opus",
1395
+ "effort": "high",
1396
+ },
1397
+ }
1398
+ ),
1399
+ encoding=UTF8_ENCODING,
1400
+ )
1401
+ bind_count = {"n": 0}
1402
+ sessions_issued: list[str] = []
1403
+
1404
+ def fake_advisor(
1405
+ *,
1406
+ launcher: str,
1407
+ model: str,
1408
+ effort: str,
1409
+ prompt_text: str,
1410
+ session_id: str | None = None,
1411
+ ) -> tuple[str | None, str, int]:
1412
+ assert launcher == "fixture-advisor-launcher"
1413
+ assert model == "opus"
1414
+ assert effort == "high"
1415
+ if session_id is None:
1416
+ bind_count["n"] += 1
1417
+ session = f"session-{bind_count['n']}"
1418
+ sessions_issued.append(session)
1419
+ return session, "ENDORSE\npre-dispatch ok", 0
1420
+ return session_id, "ENDORSE\npost-report ok", 0
1421
+
1422
+ monkeypatch.setattr(batch, "batch_invoke_advisor", fake_advisor)
1423
+ monkeypatch.setattr(
1424
+ batch,
1425
+ "batch_preflight",
1426
+ lambda **kwargs: PreflightOutcome(is_usable=True, reason=None),
1427
+ )
1428
+ monkeypatch.setattr(batch, "batch_sleep", lambda seconds: None)
1429
+ recorder = _RunnerRecorder(
1430
+ {
1431
+ "alpha": _ok_outcome(),
1432
+ "beta": _ok_outcome(),
1433
+ }
1434
+ )
1435
+ monkeypatch.setattr(batch, "batch_headless_runner", recorder)
1436
+ loaded = batch.load_batch_spec(specification_path)
1437
+ summary = batch.run_grok_batch(
1438
+ batch_spec=loaded, run_state_directory=tmp_path / "run"
1439
+ )
1440
+ assert summary.is_preflight_usable
1441
+ assert len(summary.all_worker_reports) == 2
1442
+ all_session_ids = {
1443
+ each.advisor_session_id for each in summary.all_worker_reports
1444
+ }
1445
+ assert all_session_ids == {"session-1", "session-2"}
1446
+ assert all(
1447
+ each.advisor_completion_signal == "ENDORSE"
1448
+ for each in summary.all_worker_reports
1449
+ )
1450
+ assert all(
1451
+ each.classification != "advisor_blocked"
1452
+ for each in summary.all_worker_reports
1453
+ )
1454
+ for each_report in summary.all_worker_reports:
1455
+ prompt_text = Path(each_report.prompt_path).read_text(encoding=UTF8_ENCODING)
1456
+ assert each_report.advisor_session_id in prompt_text
1457
+
1458
+
1459
+ def test_advisor_failure_classifies_advisor_blocked(
1460
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1461
+ ) -> None:
1462
+ header_part, body_part = _write_prompt_parts(tmp_path, role_marker="solo")
1463
+ payload = _worker_payload(
1464
+ role_name="solo",
1465
+ all_prompt_parts=[str(header_part), str(body_part)],
1466
+ working_directory=tmp_path,
1467
+ tool_profile=TOOL_PROFILE_BUILD,
1468
+ )
1469
+ specification_path = tmp_path / "batch-spec.json"
1470
+ specification_path.write_text(
1471
+ json.dumps(
1472
+ {
1473
+ "role": DEFAULT_ROLE,
1474
+ "should_ping": False,
1475
+ "workers": [payload],
1476
+ "advisor": {"launcher": "fixture-advisor-launcher"},
1477
+ }
1478
+ ),
1479
+ encoding=UTF8_ENCODING,
1480
+ )
1481
+
1482
+ def failing_advisor(**kwargs: object) -> tuple[str | None, str, int]:
1483
+ return None, "", 1
1484
+
1485
+ monkeypatch.setattr(batch, "batch_invoke_advisor", failing_advisor)
1486
+ monkeypatch.setattr(
1487
+ batch,
1488
+ "batch_preflight",
1489
+ lambda **kwargs: PreflightOutcome(is_usable=True, reason=None),
1490
+ )
1491
+ monkeypatch.setattr(batch, "batch_sleep", lambda seconds: None)
1492
+ monkeypatch.setattr(
1493
+ batch,
1494
+ "batch_headless_runner",
1495
+ _RunnerRecorder({"solo": _ok_outcome()}),
1496
+ )
1497
+ loaded = batch.load_batch_spec(specification_path)
1498
+ summary = batch.run_grok_batch(
1499
+ batch_spec=loaded, run_state_directory=tmp_path / "run"
1500
+ )
1501
+ assert len(summary.all_worker_reports) == 1
1502
+ report = summary.all_worker_reports[0]
1503
+ assert report.classification == "advisor_blocked"
1504
+ assert report.is_ok is False
1505
+
1506
+ def test_bind_unique_worker_advisor_rejects_placeholder(
1507
+ monkeypatch: pytest.MonkeyPatch,
1508
+ ) -> None:
1509
+ def boom(**kwargs: object) -> tuple[str | None, str, int]:
1510
+ raise AssertionError("should not call launcher for placeholder")
1511
+
1512
+ monkeypatch.setattr(batch, "batch_invoke_advisor", boom)
1513
+ with pytest.raises(ValueError, match="placeholder"):
1514
+ batch.bind_unique_worker_advisor(
1515
+ advisor_spec=batch.AdvisorSpec(launcher=batch.DEFAULT_ADVISOR_LAUNCHER_PLACEHOLDER),
1516
+ role_name="lens",
1517
+ all_used_session_ids=set(),
1518
+ )
1519
+
1520
+
1521
+ def test_bind_unique_worker_advisor_returns_session(
1522
+ monkeypatch: pytest.MonkeyPatch,
1523
+ ) -> None:
1524
+ def fake(**kwargs: object) -> tuple[str | None, str, int]:
1525
+ return "sess-unique-1", "ENDORSE\nok", 0
1526
+
1527
+ monkeypatch.setattr(batch, "batch_invoke_advisor", fake)
1528
+ session_id, signal = batch.bind_unique_worker_advisor(
1529
+ advisor_spec=batch.AdvisorSpec(launcher="fixture-advisor-launcher"),
1530
+ role_name="lens",
1531
+ all_used_session_ids=set(),
1532
+ )
1533
+ assert session_id == "sess-unique-1"
1534
+ assert signal == "ENDORSE"
1535
+
1536
+
1537
+ def test_obtain_advisor_completion_verdict_endorses(
1538
+ monkeypatch: pytest.MonkeyPatch,
1539
+ ) -> None:
1540
+ def fake(**kwargs: object) -> tuple[str | None, str, int]:
1541
+ return kwargs.get("session_id"), "ENDORSE\nok", 0
1542
+
1543
+ monkeypatch.setattr(batch, "batch_invoke_advisor", fake)
1544
+ signal = batch.obtain_advisor_completion_verdict(
1545
+ advisor_spec=batch.AdvisorSpec(launcher="fixture-advisor-launcher"),
1546
+ role_name="lens",
1547
+ session_id="sess-1",
1548
+ report_text="done",
1549
+ )
1550
+ assert signal == "ENDORSE"
1551
+
1552
+
1553
+ def test_invoke_advisor_launcher_builds_command(
1554
+ monkeypatch: pytest.MonkeyPatch,
1555
+ ) -> None:
1556
+ captured: dict[str, object] = {}
1557
+
1558
+ class _Completed:
1559
+ returncode = 0
1560
+ stdout = '{"session_id":"s1","result":"ENDORSE\\nok"}'
1561
+ stderr = ""
1562
+
1563
+ def fake_run(args, **kwargs): # type: ignore[no-untyped-def] # subprocess.run stub for argv capture
1564
+ captured["args"] = list(args)
1565
+ captured["input"] = kwargs.get("input")
1566
+ return _Completed()
1567
+
1568
+ monkeypatch.setattr(batch.subprocess, "run", fake_run)
1569
+ session_id, body, code = batch.invoke_advisor_launcher(
1570
+ launcher="fixture-advisor-launcher",
1571
+ model="opus",
1572
+ effort="high",
1573
+ prompt_text="hello",
1574
+ )
1575
+ assert code == 0
1576
+ assert session_id == "s1"
1577
+ assert "ENDORSE" in body
1578
+ assert captured["args"][0] == "fixture-advisor-launcher"
1579
+ assert "--model" in captured["args"]
1580
+
1581
+
1582
+ def test_invoke_advisor_launcher_missing_binary_raises_advisor_failure() -> None:
1583
+ try:
1584
+ batch.invoke_advisor_launcher(
1585
+ launcher="__no_such_advisor_launcher_xyz__",
1586
+ model="opus",
1587
+ effort="high",
1588
+ prompt_text="ping",
1589
+ )
1590
+ raise AssertionError("expected AdvisorFailureError")
1591
+ except batch.AdvisorFailureError as raised:
1592
+ assert "not found" in str(raised).lower() or "launcher" in str(raised).lower()
1593
+
1594
+
1595
+ def test_invoke_advisor_launcher_passes_timeout(
1596
+ monkeypatch: pytest.MonkeyPatch,
1597
+ ) -> None:
1598
+ captured: dict[str, object] = {}
1599
+
1600
+ class _Completed:
1601
+ returncode = 0
1602
+ stdout = '{"session_id":"s-timeout","result":"ENDORSE\\nok"}'
1603
+ stderr = ""
1604
+
1605
+ def fake_run(args, **kwargs): # type: ignore[no-untyped-def] # subprocess.run stub
1606
+ captured["timeout"] = kwargs.get("timeout")
1607
+ return _Completed()
1608
+
1609
+ monkeypatch.setattr(batch.subprocess, "run", fake_run)
1610
+ batch.invoke_advisor_launcher(
1611
+ launcher="fixture-advisor-launcher",
1612
+ model="opus",
1613
+ effort="high",
1614
+ prompt_text="hello",
1615
+ )
1616
+ assert captured["timeout"] == batch.MAXIMUM_ADVISOR_TIMEOUT_SECONDS