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
@@ -15,9 +15,11 @@ Import ``run_grok_batch`` for the summary object, or run the module as a CLI::
15
15
  from __future__ import annotations
16
16
 
17
17
  import argparse
18
+ import subprocess
18
19
  import json
19
20
  import sys
20
21
  import time
22
+ import threading
21
23
  import uuid
22
24
  from concurrent.futures import ThreadPoolExecutor
23
25
  from contextlib import suppress
@@ -82,16 +84,323 @@ from dev_env_scripts_constants.grok_worker_constants import (
82
84
  WORKER_SPEC_ROLE_NAME_KEY,
83
85
  WORKER_SPEC_TIMEOUT_KEY,
84
86
  WORKER_SPEC_TOOL_PROFILE_KEY,
87
+ BATCH_SPEC_ADVISOR_KEY,
88
+ ADVISOR_SPEC_LAUNCHER_KEY,
89
+ ADVISOR_SPEC_MODEL_KEY,
90
+ ADVISOR_SPEC_EFFORT_KEY,
91
+ DEFAULT_ADVISOR_LAUNCHER_PLACEHOLDER,
92
+ DEFAULT_ADVISOR_MODEL,
93
+ DEFAULT_ADVISOR_EFFORT,
94
+ MAXIMUM_WORKER_ADVISOR_CORRECTIONS,
95
+ MAXIMUM_ADVISOR_TIMEOUT_SECONDS,
96
+ ADVISOR_SIGNAL_ENDORSE,
97
+ ADVISOR_SIGNAL_CORRECTION,
98
+ ADVISOR_SIGNAL_PLAN,
99
+ ADVISOR_SIGNAL_STOP,
100
+ ALL_KNOWN_ADVISOR_SIGNALS,
101
+ CLASSIFICATION_ADVISOR_BLOCKED,
102
+ PENDING_BIND_SENTINEL,
103
+ ADVISOR_PROMPT_HEADER_TEMPLATE,
104
+ SUMMARY_ADVISOR_SESSION_ID_KEY,
105
+ SUMMARY_ADVISOR_SIGNAL_KEY,
106
+ SUMMARY_ADVISOR_LAUNCHER_KEY,
107
+ ADVISOR_CLI_PRINT_FLAG,
108
+ ADVISOR_CLI_MODEL_FLAG,
109
+ ADVISOR_CLI_EFFORT_FLAG,
110
+ ADVISOR_CLI_OUTPUT_FORMAT_FLAG,
111
+ ADVISOR_CLI_OUTPUT_FORMAT_JSON,
112
+ ADVISOR_CLI_RESUME_FLAG,
113
+ ADVISOR_BIND_PROMPT_TEMPLATE,
114
+ ADVISOR_VERDICT_PROMPT_TEMPLATE,
85
115
  )
86
116
  from dev_env_scripts_constants.timing import WORKER_STAGGER_SECONDS
87
117
  from grok_headless_runner import GrokRunnerOutcome, run_headless_worker
88
118
  from grok_worker_preflight import PreflightOutcome, run_preflight
89
119
 
120
+ class AdvisorFailureError(ValueError):
121
+ """Raised when an advisor bind, resume, or launcher call fails closed."""
122
+
123
+
90
124
  batch_sleep = time.sleep
91
125
  batch_headless_runner = run_headless_worker
92
126
  batch_preflight = run_preflight
93
127
 
94
128
 
129
+ def extract_advisor_signal(advisor_text: str) -> str | None:
130
+ """Return the first allowed opening signal token from advisor text.
131
+
132
+ ::
133
+
134
+ extract_advisor_signal("ENDORSE\\nlooks good")
135
+ ok: "ENDORSE"
136
+ extract_advisor_signal("hello ENDORSE")
137
+ flag: None
138
+
139
+ Args:
140
+ advisor_text: Raw advisor reply body.
141
+
142
+ Returns:
143
+ One of the four known signals, or ``None`` when the first token is
144
+ missing or outside the set.
145
+ """
146
+ stripped = (advisor_text or "").strip()
147
+ if not stripped:
148
+ return None
149
+ first_line = stripped.splitlines()[0].strip()
150
+ first_token = first_line.split()[0] if first_line else ""
151
+ if first_token in ALL_KNOWN_ADVISOR_SIGNALS:
152
+ return first_token
153
+ return None
154
+
155
+
156
+ def _parse_advisor_spec(raw_advisor: object) -> AdvisorSpec | None:
157
+ if raw_advisor is None:
158
+ return None
159
+ if not isinstance(raw_advisor, dict):
160
+ raise ValueError("batch advisor must be an object when present")
161
+ launcher = raw_advisor.get(
162
+ ADVISOR_SPEC_LAUNCHER_KEY, DEFAULT_ADVISOR_LAUNCHER_PLACEHOLDER
163
+ )
164
+ model = raw_advisor.get(ADVISOR_SPEC_MODEL_KEY, DEFAULT_ADVISOR_MODEL)
165
+ effort = raw_advisor.get(ADVISOR_SPEC_EFFORT_KEY, DEFAULT_ADVISOR_EFFORT)
166
+ if not isinstance(launcher, str) or not launcher:
167
+ raise ValueError("advisor.launcher must be a non-empty string")
168
+ if launcher == PENDING_BIND_SENTINEL:
169
+ raise ValueError(f"advisor.launcher must not be {PENDING_BIND_SENTINEL}")
170
+ if not isinstance(model, str) or not model:
171
+ raise ValueError("advisor.model must be a non-empty string")
172
+ if not isinstance(effort, str) or not effort:
173
+ raise ValueError("advisor.effort must be a non-empty string")
174
+ return AdvisorSpec(launcher=launcher, model=model, effort=effort)
175
+
176
+
177
+ def _advisor_body_text_from_stdout(stdout_text: str) -> str:
178
+ stripped = (stdout_text or "").strip()
179
+ if not stripped:
180
+ return ""
181
+ try:
182
+ parsed = json.loads(stripped)
183
+ except json.JSONDecodeError:
184
+ return stripped
185
+ if isinstance(parsed, dict):
186
+ advisor_body_field = parsed.get("result")
187
+ if isinstance(advisor_body_field, str):
188
+ return advisor_body_field
189
+ return stripped
190
+ if isinstance(parsed, list):
191
+ for each_record in reversed(parsed):
192
+ if (
193
+ isinstance(each_record, dict)
194
+ and each_record.get("type") == "result"
195
+ and isinstance(each_record.get("result"), str)
196
+ ):
197
+ return each_record["result"]
198
+ return stripped
199
+ return stripped
200
+
201
+
202
+ def _session_id_from_advisor_stdout(stdout_text: str) -> str | None:
203
+ stripped = (stdout_text or "").strip()
204
+ if not stripped:
205
+ return None
206
+ try:
207
+ parsed = json.loads(stripped)
208
+ except json.JSONDecodeError:
209
+ return None
210
+ candidates: list[object] = []
211
+ if isinstance(parsed, dict):
212
+ candidates.append(parsed)
213
+ elif isinstance(parsed, list):
214
+ candidates.extend(each for each in parsed if isinstance(each, dict))
215
+ for each_record in candidates:
216
+ session_id = each_record.get("session_id")
217
+ if isinstance(session_id, str) and session_id and session_id != PENDING_BIND_SENTINEL:
218
+ return session_id
219
+ return None
220
+
221
+
222
+ def invoke_advisor_launcher(
223
+ *,
224
+ launcher: str,
225
+ model: str,
226
+ effort: str,
227
+ prompt_text: str,
228
+ session_id: str | None = None,
229
+ ) -> tuple[str | None, str, int]:
230
+ """Run one advisor launcher call via direct subprocess.
231
+
232
+ Args:
233
+ launcher: Spec-supplied executable name (runtime only).
234
+ model: Advisor model name.
235
+ effort: Advisor effort level.
236
+ prompt_text: Prompt body piped on stdin.
237
+ session_id: When set, pass ``--resume`` for a post-report consult.
238
+
239
+ Returns:
240
+ ``(session_id_or_none, advisor_body_text, returncode)``.
241
+
242
+ Raises:
243
+ AdvisorFailureError: When the launcher is missing or times out.
244
+ """
245
+ all_command_arguments = [
246
+ launcher,
247
+ ADVISOR_CLI_PRINT_FLAG,
248
+ ADVISOR_CLI_MODEL_FLAG,
249
+ model,
250
+ ADVISOR_CLI_EFFORT_FLAG,
251
+ effort,
252
+ ADVISOR_CLI_OUTPUT_FORMAT_FLAG,
253
+ ADVISOR_CLI_OUTPUT_FORMAT_JSON,
254
+ ]
255
+ if session_id is not None:
256
+ all_command_arguments.extend([ADVISOR_CLI_RESUME_FLAG, session_id])
257
+ try:
258
+ completion = subprocess.run(
259
+ all_command_arguments,
260
+ input=prompt_text,
261
+ capture_output=True,
262
+ text=True,
263
+ encoding=UTF8_ENCODING,
264
+ check=False,
265
+ timeout=MAXIMUM_ADVISOR_TIMEOUT_SECONDS,
266
+ )
267
+ except FileNotFoundError as missing_launcher:
268
+ raise AdvisorFailureError(
269
+ f"advisor launcher not found: {launcher}"
270
+ ) from missing_launcher
271
+ except subprocess.TimeoutExpired as timed_out:
272
+ raise AdvisorFailureError(
273
+ f"advisor launcher timed out after {MAXIMUM_ADVISOR_TIMEOUT_SECONDS}s"
274
+ ) from timed_out
275
+ stdout_text = completion.stdout or ""
276
+ resolved_session = _session_id_from_advisor_stdout(stdout_text)
277
+ if session_id is not None and resolved_session is None:
278
+ resolved_session = session_id
279
+ advisor_body_text = _advisor_body_text_from_stdout(stdout_text)
280
+ return resolved_session, advisor_body_text, completion.returncode
281
+
282
+
283
+ batch_invoke_advisor = invoke_advisor_launcher
284
+
285
+
286
+ def bind_unique_worker_advisor(
287
+ *,
288
+ advisor_spec: AdvisorSpec,
289
+ role_name: str,
290
+ all_used_session_ids: set[str],
291
+ ) -> tuple[str, str]:
292
+ """Bind one unique advisor session for a worker; refuse duplicates and sentinels.
293
+
294
+ ::
295
+
296
+ bind_unique_worker_advisor(...)
297
+ ok: returns (session_id, ENDORSE)
298
+ flag: duplicate session id across workers raises ValueError
299
+
300
+ Args:
301
+ advisor_spec: Lead-supplied launcher/model/effort.
302
+ role_name: Worker role used in the bind prompt.
303
+ all_used_session_ids: Sessions already issued in this batch.
304
+
305
+ Returns:
306
+ ``(session_id, opening_signal)``.
307
+
308
+ Raises:
309
+ ValueError: On bind failure, sentinel, missing signal, or duplicate id.
310
+ """
311
+ if advisor_spec.launcher == DEFAULT_ADVISOR_LAUNCHER_PLACEHOLDER:
312
+ raise ValueError(
313
+ "advisor.launcher still holds the placeholder; supply a real launcher in the batch spec"
314
+ )
315
+ if advisor_spec.launcher == PENDING_BIND_SENTINEL:
316
+ raise ValueError(f"advisor.launcher is {PENDING_BIND_SENTINEL}")
317
+ prompt_text = ADVISOR_BIND_PROMPT_TEMPLATE.format(role_name=role_name)
318
+ session_id, advisor_body_text, returncode = batch_invoke_advisor(
319
+ launcher=advisor_spec.launcher,
320
+ model=advisor_spec.model,
321
+ effort=advisor_spec.effort,
322
+ prompt_text=prompt_text,
323
+ )
324
+ if returncode != 0 or not session_id or session_id == PENDING_BIND_SENTINEL:
325
+ raise AdvisorFailureError(
326
+ f"advisor bind failed for role {role_name}: returncode={returncode}"
327
+ )
328
+ if session_id in all_used_session_ids:
329
+ raise AdvisorFailureError(
330
+ f"duplicate advisor session id {session_id} for role {role_name}"
331
+ )
332
+ signal = extract_advisor_signal(advisor_body_text)
333
+ if signal is None:
334
+ raise ValueError(f"malformed advisor signal for role {role_name}")
335
+ if signal == ADVISOR_SIGNAL_STOP:
336
+ raise ValueError(f"advisor STOP for role {role_name}")
337
+ if signal != ADVISOR_SIGNAL_ENDORSE:
338
+ raise ValueError(
339
+ f"advisor pre-dispatch signal {signal} for role {role_name}; require ENDORSE"
340
+ )
341
+ all_used_session_ids.add(session_id)
342
+ return session_id, signal
343
+
344
+
345
+ def obtain_advisor_completion_verdict(
346
+ *,
347
+ advisor_spec: AdvisorSpec,
348
+ role_name: str,
349
+ session_id: str,
350
+ report_text: str,
351
+ ) -> str:
352
+ """Resume the same advisor session until ENDORSE or the correction cap.
353
+
354
+ Args:
355
+ advisor_spec: Lead-supplied launcher/model/effort.
356
+ role_name: Worker role name.
357
+ session_id: Session from pre-dispatch bind.
358
+ report_text: Worker report body under review.
359
+
360
+ Returns:
361
+ The final accepted signal (``ENDORSE``).
362
+
363
+ Raises:
364
+ ValueError: On STOP, malformed signal, or correction cap exceeded.
365
+ """
366
+ if session_id == PENDING_BIND_SENTINEL:
367
+ raise ValueError(f"advisor session_id is {PENDING_BIND_SENTINEL}")
368
+ correction_count = 0
369
+ while True:
370
+ prompt_text = ADVISOR_VERDICT_PROMPT_TEMPLATE.format(
371
+ role_name=role_name,
372
+ session_id=session_id,
373
+ report_text=report_text,
374
+ )
375
+ _, advisor_body_text, returncode = batch_invoke_advisor(
376
+ launcher=advisor_spec.launcher,
377
+ model=advisor_spec.model,
378
+ effort=advisor_spec.effort,
379
+ prompt_text=prompt_text,
380
+ session_id=session_id,
381
+ )
382
+ if returncode != 0:
383
+ raise ValueError(
384
+ f"advisor resume failed for role {role_name}: returncode={returncode}"
385
+ )
386
+ signal = extract_advisor_signal(advisor_body_text)
387
+ if signal is None:
388
+ raise ValueError(f"malformed advisor signal for role {role_name}")
389
+ if signal == ADVISOR_SIGNAL_ENDORSE:
390
+ return signal
391
+ if signal == ADVISOR_SIGNAL_STOP:
392
+ raise ValueError(f"advisor STOP for role {role_name}")
393
+ if signal in (ADVISOR_SIGNAL_CORRECTION, ADVISOR_SIGNAL_PLAN):
394
+ correction_count += 1
395
+ if correction_count > MAXIMUM_WORKER_ADVISOR_CORRECTIONS:
396
+ raise ValueError(
397
+ f"advisor correction cap exceeded for role {role_name}"
398
+ )
399
+ continue
400
+ raise ValueError(f"unexpected advisor signal {signal} for role {role_name}")
401
+
402
+
403
+
95
404
  def _require_known_worker_keys(all_worker_fields: dict[str, object]) -> None:
96
405
  """Reject a worker entry carrying a key the launcher does not accept.
97
406
 
@@ -137,6 +446,20 @@ class WorkerSpec:
137
446
  agent_name: str | None = None
138
447
 
139
448
 
449
+ @dataclass(frozen=True)
450
+ class AdvisorSpec:
451
+ """Lead-supplied worker advisor binding configuration for one batch.
452
+
453
+ The launcher name is runtime-only: the committed default is the placeholder
454
+ ``DEFAULT_ADVISOR_LAUNCHER_PLACEHOLDER``. The lead replaces it in the batch
455
+ specification; real account-scoped launcher names never land as constants.
456
+ """
457
+
458
+ launcher: str
459
+ model: str = DEFAULT_ADVISOR_MODEL
460
+ effort: str = DEFAULT_ADVISOR_EFFORT
461
+
462
+
140
463
  @dataclass(frozen=True)
141
464
  class BatchSpec:
142
465
  """Full batch specification for one fleet launch."""
@@ -144,6 +467,7 @@ class BatchSpec:
144
467
  role: str
145
468
  should_ping: bool
146
469
  all_workers: tuple[WorkerSpec, ...]
470
+ advisor: AdvisorSpec | None = None
147
471
 
148
472
 
149
473
  @dataclass(frozen=True)
@@ -170,6 +494,9 @@ class WorkerReport:
170
494
  leader_socket: str
171
495
  prompt_path: str
172
496
  debug_path: str
497
+ advisor_session_id: str | None = None
498
+ advisor_completion_signal: str | None = None
499
+ advisor_launcher: str | None = None
173
500
 
174
501
 
175
502
  @dataclass(frozen=True)
@@ -408,10 +735,12 @@ def load_batch_spec(specification_path: Path) -> BatchSpec:
408
735
  if not isinstance(each_entry, dict):
409
736
  raise ValueError("each worker must be an object")
410
737
  all_parsed_workers.append(_parse_worker_entry(each_entry))
738
+ advisor_spec = _parse_advisor_spec(parsed_payload.get(BATCH_SPEC_ADVISOR_KEY))
411
739
  return BatchSpec(
412
740
  role=role,
413
741
  should_ping=should_ping,
414
742
  all_workers=tuple(all_parsed_workers),
743
+ advisor=advisor_spec,
415
744
  )
416
745
 
417
746
 
@@ -459,11 +788,20 @@ def _write_assembled_prompt(
459
788
  *,
460
789
  worker_spec: WorkerSpec,
461
790
  prompt_path: Path,
791
+ advisor_session_id: str | None = None,
792
+ advisor_spec: AdvisorSpec | None = None,
462
793
  ) -> None:
463
794
  prompt_text = assemble_worker_prompt(
464
795
  all_prompt_part_paths=worker_spec.all_prompt_part_paths,
465
796
  tool_profile=worker_spec.tool_profile,
466
797
  )
798
+ if advisor_session_id is not None and advisor_spec is not None:
799
+ header = ADVISOR_PROMPT_HEADER_TEMPLATE.format(
800
+ session_id=advisor_session_id,
801
+ model=advisor_spec.model,
802
+ effort=advisor_spec.effort,
803
+ )
804
+ prompt_text = f"{header}{prompt_text}"
467
805
  prompt_path.write_text(prompt_text, encoding=UTF8_ENCODING)
468
806
 
469
807
 
@@ -497,6 +835,9 @@ def _worker_report(
497
835
  classification: str,
498
836
  is_ok: bool,
499
837
  report_text: str,
838
+ advisor_session_id: str | None = None,
839
+ advisor_completion_signal: str | None = None,
840
+ advisor_launcher: str | None = None,
500
841
  ) -> WorkerReport:
501
842
  with suppress(OSError):
502
843
  _write_report_file(scratch_paths.report_path, report_text)
@@ -511,6 +852,9 @@ def _worker_report(
511
852
  leader_socket=str(scratch_paths.leader_socket_path),
512
853
  prompt_path=str(scratch_paths.prompt_path),
513
854
  debug_path=str(scratch_paths.debug_path),
855
+ advisor_session_id=advisor_session_id,
856
+ advisor_completion_signal=advisor_completion_signal,
857
+ advisor_launcher=advisor_launcher,
514
858
  )
515
859
 
516
860
 
@@ -546,28 +890,218 @@ def _error_report_for_exception(
546
890
  )
547
891
 
548
892
 
893
+
894
+ def _bind_worker_advisor_session(
895
+ *,
896
+ advisor_spec: AdvisorSpec,
897
+ role_name: str,
898
+ all_used_session_ids: set[str],
899
+ session_id_lock: object | None,
900
+ ) -> tuple[str, str]:
901
+ if session_id_lock is not None:
902
+ with session_id_lock: # type: ignore[attr-defined] # Lock protocol from threading.Lock
903
+ return bind_unique_worker_advisor(
904
+ advisor_spec=advisor_spec,
905
+ role_name=role_name,
906
+ all_used_session_ids=all_used_session_ids,
907
+ )
908
+ return bind_unique_worker_advisor(
909
+ advisor_spec=advisor_spec,
910
+ role_name=role_name,
911
+ all_used_session_ids=all_used_session_ids,
912
+ )
913
+
914
+
915
+ def _advisor_blocked_report(
916
+ *,
917
+ worker_spec: WorkerSpec,
918
+ scratch_paths: WorkerScratchPaths,
919
+ report_text: str,
920
+ advisor_session_id: str | None = None,
921
+ advisor_launcher: str | None = None,
922
+ ) -> WorkerReport:
923
+ return _worker_report(
924
+ worker_spec=worker_spec,
925
+ scratch_paths=scratch_paths,
926
+ returncode=WORKER_EXCEPTION_RETURN_CODE,
927
+ classification=CLASSIFICATION_ADVISOR_BLOCKED,
928
+ is_ok=False,
929
+ report_text=report_text,
930
+ advisor_session_id=advisor_session_id,
931
+ advisor_completion_signal=None,
932
+ advisor_launcher=advisor_launcher,
933
+ )
934
+
935
+
936
+
937
+ def _attach_advisor_completion_if_configured(
938
+ *,
939
+ worker_spec: WorkerSpec,
940
+ scratch_paths: WorkerScratchPaths,
941
+ worker_report: WorkerReport,
942
+ advisor_spec: AdvisorSpec | None,
943
+ advisor_session_id: str | None,
944
+ advisor_launcher: str | None,
945
+ ) -> WorkerReport:
946
+ if advisor_spec is None or advisor_session_id is None:
947
+ return worker_report
948
+ try:
949
+ completion_signal = obtain_advisor_completion_verdict(
950
+ advisor_spec=advisor_spec,
951
+ role_name=worker_spec.role_name,
952
+ session_id=advisor_session_id,
953
+ report_text=worker_report.report_text,
954
+ )
955
+ except ValueError as advisor_error:
956
+ return _advisor_blocked_report(
957
+ worker_spec=worker_spec,
958
+ scratch_paths=scratch_paths,
959
+ report_text=str(advisor_error),
960
+ advisor_session_id=advisor_session_id,
961
+ advisor_launcher=advisor_launcher,
962
+ )
963
+ return _worker_report(
964
+ worker_spec=worker_spec,
965
+ scratch_paths=scratch_paths,
966
+ returncode=worker_report.returncode,
967
+ classification=worker_report.classification,
968
+ is_ok=worker_report.is_ok,
969
+ report_text=worker_report.report_text,
970
+ advisor_session_id=advisor_session_id,
971
+ advisor_completion_signal=completion_signal,
972
+ advisor_launcher=advisor_launcher,
973
+ )
974
+
975
+
976
+ def _maybe_bind_advisor_for_worker(
977
+ *,
978
+ worker_spec: WorkerSpec,
979
+ scratch_paths: WorkerScratchPaths,
980
+ advisor_spec: AdvisorSpec | None,
981
+ all_used_session_ids: set[str] | None,
982
+ session_id_lock: object | None,
983
+ ) -> tuple[str | None, str | None, WorkerReport | None]:
984
+ if advisor_spec is None:
985
+ return None, None, None
986
+ used_ids = all_used_session_ids if all_used_session_ids is not None else set()
987
+ advisor_session_id, pre_signal = _bind_worker_advisor_session(
988
+ advisor_spec=advisor_spec,
989
+ role_name=worker_spec.role_name,
990
+ all_used_session_ids=used_ids,
991
+ session_id_lock=session_id_lock,
992
+ )
993
+ if (
994
+ advisor_session_id == PENDING_BIND_SENTINEL
995
+ or pre_signal == PENDING_BIND_SENTINEL
996
+ ):
997
+ blocked = _advisor_blocked_report(
998
+ worker_spec=worker_spec,
999
+ scratch_paths=scratch_paths,
1000
+ report_text=f"{PENDING_BIND_SENTINEL} sentinel before launch",
1001
+ advisor_session_id=advisor_session_id,
1002
+ advisor_launcher=advisor_spec.launcher,
1003
+ )
1004
+ return advisor_session_id, advisor_spec.launcher, blocked
1005
+ return advisor_session_id, advisor_spec.launcher, None
1006
+
1007
+
1008
+ def _map_launch_exception(
1009
+ *,
1010
+ worker_spec: WorkerSpec,
1011
+ scratch_paths: WorkerScratchPaths,
1012
+ raised_exception: BaseException,
1013
+ advisor_spec: AdvisorSpec | None,
1014
+ advisor_session_id: str | None,
1015
+ advisor_launcher: str | None,
1016
+ ) -> WorkerReport:
1017
+ if advisor_spec is not None and (
1018
+ isinstance(raised_exception, AdvisorFailureError)
1019
+ or PENDING_BIND_SENTINEL in str(raised_exception)
1020
+ or "advisor" in str(raised_exception).lower()
1021
+ ):
1022
+ return _advisor_blocked_report(
1023
+ worker_spec=worker_spec,
1024
+ scratch_paths=scratch_paths,
1025
+ report_text=f"{type(raised_exception).__name__}: {raised_exception}",
1026
+ advisor_session_id=advisor_session_id,
1027
+ advisor_launcher=advisor_launcher,
1028
+ )
1029
+ return _error_report_for_exception(
1030
+ worker_spec=worker_spec,
1031
+ scratch_paths=scratch_paths,
1032
+ raised_exception=raised_exception,
1033
+ )
1034
+
1035
+
1036
+ def _run_worker_body(
1037
+ *,
1038
+ worker_spec: WorkerSpec,
1039
+ scratch_paths: WorkerScratchPaths,
1040
+ run_state_directory: Path,
1041
+ advisor_spec: AdvisorSpec | None,
1042
+ advisor_session_id: str | None,
1043
+ advisor_launcher: str | None,
1044
+ ) -> WorkerReport:
1045
+ _write_assembled_prompt(
1046
+ worker_spec=worker_spec,
1047
+ prompt_path=scratch_paths.prompt_path,
1048
+ advisor_session_id=advisor_session_id,
1049
+ advisor_spec=advisor_spec,
1050
+ )
1051
+ outcome = _invoke_worker(
1052
+ worker_spec=worker_spec,
1053
+ scratch_paths=scratch_paths,
1054
+ run_state_directory=run_state_directory,
1055
+ )
1056
+ worker_report = _build_worker_report(
1057
+ worker_spec=worker_spec,
1058
+ outcome=outcome,
1059
+ scratch_paths=scratch_paths,
1060
+ )
1061
+ return _attach_advisor_completion_if_configured(
1062
+ worker_spec=worker_spec,
1063
+ scratch_paths=scratch_paths,
1064
+ worker_report=worker_report,
1065
+ advisor_spec=advisor_spec,
1066
+ advisor_session_id=advisor_session_id,
1067
+ advisor_launcher=advisor_launcher,
1068
+ )
1069
+
1070
+
549
1071
  def _launch_one_worker(
550
1072
  *,
551
1073
  worker_spec: WorkerSpec,
552
1074
  worker_index: int,
553
1075
  run_state_directory: Path,
1076
+ advisor_spec: AdvisorSpec | None = None,
1077
+ all_used_session_ids: set[str] | None = None,
1078
+ session_id_lock: object | None = None,
554
1079
  ) -> WorkerReport:
555
1080
  batch_sleep(worker_index * WORKER_STAGGER_SECONDS)
556
1081
  scratch_paths = _mint_worker_scratch_paths(run_state_directory)
1082
+ advisor_session_id: str | None = None
1083
+ advisor_launcher: str | None = None
557
1084
  try:
558
- _write_assembled_prompt(
559
- worker_spec=worker_spec,
560
- prompt_path=scratch_paths.prompt_path,
561
- )
562
- outcome = _invoke_worker(
1085
+ (
1086
+ advisor_session_id,
1087
+ advisor_launcher,
1088
+ early_block,
1089
+ ) = _maybe_bind_advisor_for_worker(
563
1090
  worker_spec=worker_spec,
564
1091
  scratch_paths=scratch_paths,
565
- run_state_directory=run_state_directory,
1092
+ advisor_spec=advisor_spec,
1093
+ all_used_session_ids=all_used_session_ids,
1094
+ session_id_lock=session_id_lock,
566
1095
  )
567
- return _build_worker_report(
1096
+ if early_block is not None:
1097
+ return early_block
1098
+ return _run_worker_body(
568
1099
  worker_spec=worker_spec,
569
- outcome=outcome,
570
1100
  scratch_paths=scratch_paths,
1101
+ run_state_directory=run_state_directory,
1102
+ advisor_spec=advisor_spec,
1103
+ advisor_session_id=advisor_session_id,
1104
+ advisor_launcher=advisor_launcher,
571
1105
  )
572
1106
  except (
573
1107
  OSError,
@@ -576,14 +1110,19 @@ def _launch_one_worker(
576
1110
  TypeError,
577
1111
  AttributeError,
578
1112
  LookupError,
1113
+ AdvisorFailureError,
579
1114
  ) as raised_exception:
580
- return _error_report_for_exception(
1115
+ return _map_launch_exception(
581
1116
  worker_spec=worker_spec,
582
1117
  scratch_paths=scratch_paths,
583
1118
  raised_exception=raised_exception,
1119
+ advisor_spec=advisor_spec,
1120
+ advisor_session_id=advisor_session_id,
1121
+ advisor_launcher=advisor_launcher,
584
1122
  )
585
1123
 
586
1124
 
1125
+
587
1126
  def run_grok_batch(
588
1127
  *,
589
1128
  batch_spec: BatchSpec,
@@ -617,6 +1156,8 @@ def run_grok_batch(
617
1156
  preflight_reason=None,
618
1157
  all_worker_reports=(),
619
1158
  )
1159
+ all_used_session_ids: set[str] = set()
1160
+ session_id_lock = threading.Lock()
620
1161
  with ThreadPoolExecutor(max_workers=worker_count) as executor:
621
1162
  all_futures = [
622
1163
  executor.submit(
@@ -624,6 +1165,9 @@ def run_grok_batch(
624
1165
  worker_spec=each_worker,
625
1166
  worker_index=each_index,
626
1167
  run_state_directory=run_state_directory,
1168
+ advisor_spec=batch_spec.advisor,
1169
+ all_used_session_ids=all_used_session_ids,
1170
+ session_id_lock=session_id_lock,
627
1171
  )
628
1172
  for each_index, each_worker in enumerate(batch_spec.all_workers)
629
1173
  ]
@@ -658,6 +1202,9 @@ def batch_summary_as_dict(batch_summary: BatchSummary) -> dict[str, object]:
658
1202
  SUMMARY_LEADER_SOCKET_KEY: each_report.leader_socket,
659
1203
  SUMMARY_PROMPT_FILE_KEY: each_report.prompt_path,
660
1204
  SUMMARY_DEBUG_FILE_KEY: each_report.debug_path,
1205
+ SUMMARY_ADVISOR_SESSION_ID_KEY: each_report.advisor_session_id,
1206
+ SUMMARY_ADVISOR_SIGNAL_KEY: each_report.advisor_completion_signal,
1207
+ SUMMARY_ADVISOR_LAUNCHER_KEY: each_report.advisor_launcher,
661
1208
  }
662
1209
  for each_report in batch_summary.all_worker_reports
663
1210
  ]