claude-dev-env 2.7.1 → 2.9.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 (127) hide show
  1. package/CLAUDE.md +7 -1
  2. package/_shared/CLAUDE.md +1 -0
  3. package/_shared/advisor/advisor-protocol.md +19 -9
  4. package/_shared/pr-loop/audit-contract.md +4 -4
  5. package/_shared/pr-loop/precatch-rubric.md +2 -2
  6. package/_shared/process-tree/CLAUDE.md +41 -0
  7. package/_shared/process-tree/scripts/config/process_tree_scripts_constants/__init__.py +1 -0
  8. package/_shared/process-tree/scripts/config/process_tree_scripts_constants/process_tree_kill_constants.py +27 -0
  9. package/_shared/process-tree/scripts/process_tree_kill.py +141 -0
  10. package/_shared/process-tree/scripts/pyproject.toml +16 -0
  11. package/_shared/process-tree/scripts/test_process_tree_kill.py +278 -0
  12. package/agents/clean-coder.md +9 -19
  13. package/agents/code-quality-agent.md +6 -5
  14. package/agents/deep-research.md +7 -24
  15. package/agents/docs-agent.md +1 -27
  16. package/agents/issue-tracker.md +1 -7
  17. package/agents/skill-writer-agent.md +1 -2
  18. package/agents/test_agent_frontmatter.py +335 -12
  19. package/docs/CODE_RULES.md +4 -2
  20. package/docs/references/CLAUDE.md +2 -2
  21. package/docs/references/advisor-tool.md +44 -6
  22. package/docs/references/team-advisor-skill.md +14 -8
  23. package/hooks/blocking/CLAUDE.md +2 -0
  24. package/hooks/blocking/fable_spawn_gate.py +187 -0
  25. package/hooks/blocking/piped_pytest_blocker.py +1223 -0
  26. package/hooks/blocking/plain_language_blocker.py +287 -15
  27. package/hooks/blocking/test_fable_spawn_gate.py +374 -0
  28. package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
  29. package/hooks/blocking/test_plain_language_blocker.py +277 -2
  30. package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
  31. package/hooks/git-hooks/CLAUDE.md +2 -2
  32. package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
  33. package/hooks/git-hooks/pre_push.py +343 -54
  34. package/hooks/git-hooks/test_pre_push.py +852 -6
  35. package/hooks/hooks.json +9 -19
  36. package/hooks/hooks_constants/CLAUDE.md +2 -0
  37. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
  38. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  39. package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
  40. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
  41. package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
  42. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
  43. package/hooks/hooks_constants/shell_command_segments.py +1 -1
  44. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
  45. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
  46. package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
  47. package/output-styles/CLAUDE.md +17 -0
  48. package/output-styles/caveman-agent.md +37 -0
  49. package/package.json +2 -1
  50. package/rules/CLAUDE.md +1 -0
  51. package/rules/ask-user-question-required.md +26 -0
  52. package/rules/claims-as-quotes.md +65 -0
  53. package/rules/code-standards.md +33 -7
  54. package/rules/eli11-replies.md +1 -1
  55. package/scripts/CLAUDE.md +3 -3
  56. package/scripts/_code_review_test_support.py +6 -0
  57. package/scripts/check.ps1 +18 -5
  58. package/scripts/claude_chain_runner.py +203 -31
  59. package/scripts/codec_forwarding_test_support.py +2 -0
  60. package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
  61. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
  62. package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
  63. package/scripts/dev_env_scripts_constants/grok_run_ledger_constants.py +50 -0
  64. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +132 -24
  65. package/scripts/grok_headless_runner.py +16 -83
  66. package/scripts/grok_patch_artifacts.py +123 -0
  67. package/scripts/grok_run_ledger.py +318 -0
  68. package/scripts/spawn_grok_batch.py +591 -10
  69. package/scripts/test_claude_chain_runner.py +358 -0
  70. package/scripts/test_grok_headless_runner.py +18 -63
  71. package/scripts/test_grok_patch_artifacts.py +82 -0
  72. package/scripts/test_grok_run_ledger.py +116 -0
  73. package/scripts/test_invoke_code_review.py +298 -0
  74. package/scripts/test_resolve_worker_spawn.py +6 -0
  75. package/scripts/test_spawn_grok_batch.py +396 -0
  76. package/scripts/tests/CLAUDE.md +1 -0
  77. package/scripts/tests/test_grok_worker_constants.py +59 -0
  78. package/skills/CLAUDE.md +4 -2
  79. package/skills/_shared/CLAUDE.md +37 -4
  80. package/skills/_shared/advisor/CLAUDE.md +9 -0
  81. package/skills/_shared/advisor/advisor-protocol.md +5 -0
  82. package/skills/_shared/advisor/scripts/README.md +9 -0
  83. package/skills/_shared/end-of-run-gotcha-recommendations.md +156 -0
  84. package/skills/_shared/pr-loop/CLAUDE.md +18 -1
  85. package/skills/_shared/pr-loop/audit-contract.md +5 -0
  86. package/skills/_shared/pr-loop/audit-reply-template.md +5 -0
  87. package/skills/_shared/pr-loop/code-rules-gate.md +5 -0
  88. package/skills/_shared/pr-loop/fix-protocol.md +5 -0
  89. package/skills/_shared/pr-loop/gh-payloads.md +5 -0
  90. package/skills/_shared/pr-loop/post-audit-thread-contract.md +5 -0
  91. package/skills/_shared/pr-loop/precatch-rubric.md +5 -0
  92. package/skills/_shared/pr-loop/scripts/CLAUDE.md +8 -1
  93. package/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +29 -0
  94. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
  95. package/skills/_shared/pr-loop/state-schema.md +5 -0
  96. package/skills/_shared/pr-loop/worker-spawn.md +5 -0
  97. package/skills/autoconverge/workflow/converge.contract.test.mjs +105 -2
  98. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +34 -1
  99. package/skills/autoconverge/workflow/converge.mjs +66 -33
  100. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
  101. package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
  102. package/skills/codex-review/scripts/run_codex_review.py +16 -64
  103. package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
  104. package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
  105. package/skills/e-code-review/SKILL.md +15 -9
  106. package/skills/e-code-review/reference/fix.md +29 -7
  107. package/skills/e-code-review/reference/loop.md +230 -14
  108. package/skills/e-code-review/reference/low.md +33 -15
  109. package/skills/e-code-review/reference/medium.md +55 -21
  110. package/skills/e-code-review/reference/runner-selection.md +40 -0
  111. package/skills/e-code-review/reference/xhigh.md +30 -12
  112. package/skills/e-code-review/scripts/e_code_review_scripts_constants/__init__.py +1 -0
  113. package/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +55 -0
  114. package/skills/e-code-review/scripts/grok_code_review.py +221 -0
  115. package/skills/e-code-review/scripts/test_grok_code_review.py +212 -0
  116. package/skills/fresh-branch/CLAUDE.md +5 -5
  117. package/skills/fresh-branch/SKILL.md +14 -6
  118. package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
  119. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
  120. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
  121. package/skills/grok-spawn/SKILL.md +10 -0
  122. package/skills/orchestrator/SKILL.md +9 -1
  123. package/skills/orchestrator-refresh/SKILL.md +5 -1
  124. package/skills/task-build/reference/tool-routing.md +3 -0
  125. package/skills/team-advisor/SKILL.md +23 -41
  126. package/system-prompts/software-engineer.xml +6 -3
  127. 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
@@ -26,6 +28,7 @@ from pathlib import Path
26
28
 
27
29
  from dev_env_scripts_constants.grok_worker_constants import (
28
30
  ALL_KNOWN_TOOL_PROFILES,
31
+ ALL_KNOWN_WORKER_SPEC_KEYS,
29
32
  BATCH_LAUNCH_ERROR_STDERR_PREFIX,
30
33
  BATCH_SPEC_ROLE_KEY,
31
34
  BATCH_SPEC_SHOULD_PING_KEY,
@@ -70,25 +73,363 @@ from dev_env_scripts_constants.grok_worker_constants import (
70
73
  SUMMARY_WORKERS_KEY,
71
74
  TOOL_PROFILE_BUILD,
72
75
  TOOL_PROFILE_READONLY,
76
+ UNKNOWN_WORKER_KEY_ERROR_TEMPLATE,
73
77
  UTF8_ENCODING,
74
78
  WORKER_EXCEPTION_RETURN_CODE,
75
79
  WORKER_SPEC_AGENT_NAME_KEY,
76
80
  WORKER_SPEC_CWD_KEY,
77
81
  WORKER_SPEC_IS_REPO_ONLY_KEY,
82
+ WORKER_SPEC_KEY_JOIN_SEPARATOR,
78
83
  WORKER_SPEC_PROMPT_PARTS_KEY,
79
84
  WORKER_SPEC_ROLE_NAME_KEY,
80
85
  WORKER_SPEC_TIMEOUT_KEY,
81
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,
82
115
  )
83
116
  from dev_env_scripts_constants.timing import WORKER_STAGGER_SECONDS
84
117
  from grok_headless_runner import GrokRunnerOutcome, run_headless_worker
85
118
  from grok_worker_preflight import PreflightOutcome, run_preflight
86
119
 
120
+ class AdvisorFailureError(ValueError):
121
+ """Raised when an advisor bind, resume, or launcher call fails closed."""
122
+
123
+
87
124
  batch_sleep = time.sleep
88
125
  batch_headless_runner = run_headless_worker
89
126
  batch_preflight = run_preflight
90
127
 
91
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
+ all_command_arguments = [
243
+ launcher,
244
+ ADVISOR_CLI_PRINT_FLAG,
245
+ ADVISOR_CLI_MODEL_FLAG,
246
+ model,
247
+ ADVISOR_CLI_EFFORT_FLAG,
248
+ effort,
249
+ ADVISOR_CLI_OUTPUT_FORMAT_FLAG,
250
+ ADVISOR_CLI_OUTPUT_FORMAT_JSON,
251
+ ]
252
+ if session_id is not None:
253
+ all_command_arguments.extend([ADVISOR_CLI_RESUME_FLAG, session_id])
254
+ try:
255
+ completion = subprocess.run(
256
+ all_command_arguments,
257
+ input=prompt_text,
258
+ capture_output=True,
259
+ text=True,
260
+ encoding=UTF8_ENCODING,
261
+ check=False,
262
+ timeout=MAXIMUM_ADVISOR_TIMEOUT_SECONDS,
263
+ )
264
+ except FileNotFoundError as missing_launcher:
265
+ raise AdvisorFailureError(
266
+ f"advisor launcher not found: {launcher}"
267
+ ) from missing_launcher
268
+ except subprocess.TimeoutExpired as timed_out:
269
+ raise AdvisorFailureError(
270
+ f"advisor launcher timed out after {MAXIMUM_ADVISOR_TIMEOUT_SECONDS}s"
271
+ ) from timed_out
272
+ stdout_text = completion.stdout or ""
273
+ resolved_session = _session_id_from_advisor_stdout(stdout_text)
274
+ if session_id is not None and resolved_session is None:
275
+ resolved_session = session_id
276
+ advisor_body_text = _advisor_body_text_from_stdout(stdout_text)
277
+ return resolved_session, advisor_body_text, completion.returncode
278
+
279
+
280
+ batch_invoke_advisor = invoke_advisor_launcher
281
+
282
+
283
+ def bind_unique_worker_advisor(
284
+ *,
285
+ advisor_spec: AdvisorSpec,
286
+ role_name: str,
287
+ all_used_session_ids: set[str],
288
+ ) -> tuple[str, str]:
289
+ """Bind one unique advisor session for a worker; refuse duplicates and sentinels.
290
+
291
+ ::
292
+
293
+ bind_unique_worker_advisor(...)
294
+ ok: returns (session_id, ENDORSE)
295
+ flag: duplicate session id across workers raises ValueError
296
+
297
+ Args:
298
+ advisor_spec: Lead-supplied launcher/model/effort.
299
+ role_name: Worker role used in the bind prompt.
300
+ all_used_session_ids: Sessions already issued in this batch.
301
+
302
+ Returns:
303
+ ``(session_id, opening_signal)``.
304
+
305
+ Raises:
306
+ ValueError: On bind failure, sentinel, missing signal, or duplicate id.
307
+ """
308
+ if advisor_spec.launcher == DEFAULT_ADVISOR_LAUNCHER_PLACEHOLDER:
309
+ raise ValueError(
310
+ "advisor.launcher still holds the placeholder; supply a real launcher in the batch spec"
311
+ )
312
+ if advisor_spec.launcher == PENDING_BIND_SENTINEL:
313
+ raise ValueError(f"advisor.launcher is {PENDING_BIND_SENTINEL}")
314
+ prompt_text = ADVISOR_BIND_PROMPT_TEMPLATE.format(role_name=role_name)
315
+ session_id, advisor_body_text, returncode = batch_invoke_advisor(
316
+ launcher=advisor_spec.launcher,
317
+ model=advisor_spec.model,
318
+ effort=advisor_spec.effort,
319
+ prompt_text=prompt_text,
320
+ )
321
+ if returncode != 0 or not session_id or session_id == PENDING_BIND_SENTINEL:
322
+ raise AdvisorFailureError(
323
+ f"advisor bind failed for role {role_name}: returncode={returncode}"
324
+ )
325
+ if session_id in all_used_session_ids:
326
+ raise AdvisorFailureError(
327
+ f"duplicate advisor session id {session_id} for role {role_name}"
328
+ )
329
+ signal = extract_advisor_signal(advisor_body_text)
330
+ if signal is None:
331
+ raise ValueError(f"malformed advisor signal for role {role_name}")
332
+ if signal == ADVISOR_SIGNAL_STOP:
333
+ raise ValueError(f"advisor STOP for role {role_name}")
334
+ if signal != ADVISOR_SIGNAL_ENDORSE:
335
+ raise ValueError(
336
+ f"advisor pre-dispatch signal {signal} for role {role_name}; require ENDORSE"
337
+ )
338
+ all_used_session_ids.add(session_id)
339
+ return session_id, signal
340
+
341
+
342
+ def obtain_advisor_completion_verdict(
343
+ *,
344
+ advisor_spec: AdvisorSpec,
345
+ role_name: str,
346
+ session_id: str,
347
+ report_text: str,
348
+ ) -> str:
349
+ """Resume the same advisor session until ENDORSE or the correction cap.
350
+
351
+ Args:
352
+ advisor_spec: Lead-supplied launcher/model/effort.
353
+ role_name: Worker role name.
354
+ session_id: Session from pre-dispatch bind.
355
+ report_text: Worker report body under review.
356
+
357
+ Returns:
358
+ The final accepted signal (``ENDORSE``).
359
+
360
+ Raises:
361
+ ValueError: On STOP, malformed signal, or correction cap exceeded.
362
+ """
363
+ if session_id == PENDING_BIND_SENTINEL:
364
+ raise ValueError(f"advisor session_id is {PENDING_BIND_SENTINEL}")
365
+ correction_count = 0
366
+ while True:
367
+ prompt_text = ADVISOR_VERDICT_PROMPT_TEMPLATE.format(
368
+ role_name=role_name,
369
+ session_id=session_id,
370
+ report_text=report_text,
371
+ )
372
+ _, advisor_body_text, returncode = batch_invoke_advisor(
373
+ launcher=advisor_spec.launcher,
374
+ model=advisor_spec.model,
375
+ effort=advisor_spec.effort,
376
+ prompt_text=prompt_text,
377
+ session_id=session_id,
378
+ )
379
+ if returncode != 0:
380
+ raise ValueError(
381
+ f"advisor resume failed for role {role_name}: returncode={returncode}"
382
+ )
383
+ signal = extract_advisor_signal(advisor_body_text)
384
+ if signal is None:
385
+ raise ValueError(f"malformed advisor signal for role {role_name}")
386
+ if signal == ADVISOR_SIGNAL_ENDORSE:
387
+ return signal
388
+ if signal == ADVISOR_SIGNAL_STOP:
389
+ raise ValueError(f"advisor STOP for role {role_name}")
390
+ if signal in (ADVISOR_SIGNAL_CORRECTION, ADVISOR_SIGNAL_PLAN):
391
+ correction_count += 1
392
+ if correction_count > MAXIMUM_WORKER_ADVISOR_CORRECTIONS:
393
+ raise ValueError(
394
+ f"advisor correction cap exceeded for role {role_name}"
395
+ )
396
+ continue
397
+ raise ValueError(f"unexpected advisor signal {signal} for role {role_name}")
398
+
399
+
400
+
401
+ def _require_known_worker_keys(all_worker_fields: dict[str, object]) -> None:
402
+ """Reject a worker entry carrying a key the launcher does not accept.
403
+
404
+ ::
405
+
406
+ {"role_name": "lens", ..., "timeout_second": <seconds>}
407
+ flag: unknown worker key(s): timeout_second; accepted keys: agent_name, ...
408
+
409
+ A dropped key reads as a setting that took effect. Naming it here makes
410
+ that a startup error rather than a silent gap.
411
+
412
+ Args:
413
+ all_worker_fields: One raw worker entry straight from the JSON spec.
414
+
415
+ Raises:
416
+ ValueError: When the entry carries any key outside the accepted set.
417
+ """
418
+ all_unknown_keys = set(all_worker_fields) - ALL_KNOWN_WORKER_SPEC_KEYS
419
+ if not all_unknown_keys:
420
+ return
421
+ joined_unknown = WORKER_SPEC_KEY_JOIN_SEPARATOR.join(sorted(all_unknown_keys))
422
+ joined_accepted = WORKER_SPEC_KEY_JOIN_SEPARATOR.join(
423
+ sorted(ALL_KNOWN_WORKER_SPEC_KEYS)
424
+ )
425
+ raise ValueError(
426
+ UNKNOWN_WORKER_KEY_ERROR_TEMPLATE.format(
427
+ unknown_keys=joined_unknown,
428
+ accepted_keys=joined_accepted,
429
+ )
430
+ )
431
+
432
+
92
433
  @dataclass(frozen=True)
93
434
  class WorkerSpec:
94
435
  """One worker entry from a batch specification."""
@@ -102,6 +443,20 @@ class WorkerSpec:
102
443
  agent_name: str | None = None
103
444
 
104
445
 
446
+ @dataclass(frozen=True)
447
+ class AdvisorSpec:
448
+ """Lead-supplied worker advisor binding configuration for one batch.
449
+
450
+ The launcher name is runtime-only: the committed default is the placeholder
451
+ ``DEFAULT_ADVISOR_LAUNCHER_PLACEHOLDER``. The lead replaces it in the batch
452
+ specification; real account-scoped launcher names never land as constants.
453
+ """
454
+
455
+ launcher: str
456
+ model: str = DEFAULT_ADVISOR_MODEL
457
+ effort: str = DEFAULT_ADVISOR_EFFORT
458
+
459
+
105
460
  @dataclass(frozen=True)
106
461
  class BatchSpec:
107
462
  """Full batch specification for one fleet launch."""
@@ -109,6 +464,7 @@ class BatchSpec:
109
464
  role: str
110
465
  should_ping: bool
111
466
  all_workers: tuple[WorkerSpec, ...]
467
+ advisor: AdvisorSpec | None = None
112
468
 
113
469
 
114
470
  @dataclass(frozen=True)
@@ -135,6 +491,9 @@ class WorkerReport:
135
491
  leader_socket: str
136
492
  prompt_path: str
137
493
  debug_path: str
494
+ advisor_session_id: str | None = None
495
+ advisor_completion_signal: str | None = None
496
+ advisor_launcher: str | None = None
138
497
 
139
498
 
140
499
  @dataclass(frozen=True)
@@ -290,6 +649,7 @@ def _require_timeout_within_bounds(raw_field: object) -> int:
290
649
 
291
650
 
292
651
  def _parse_worker_entry(all_worker_fields: dict[str, object]) -> WorkerSpec:
652
+ _require_known_worker_keys(all_worker_fields)
293
653
  role_name = _require_string(
294
654
  _require_worker_field(all_worker_fields, WORKER_SPEC_ROLE_NAME_KEY),
295
655
  WORKER_SPEC_ROLE_NAME_KEY,
@@ -348,7 +708,8 @@ def load_batch_spec(specification_path: Path) -> BatchSpec:
348
708
  The validated batch specification.
349
709
 
350
710
  Raises:
351
- ValueError: When the JSON shape is invalid or a required field is wrong.
711
+ ValueError: When the JSON shape is invalid, a required field is wrong,
712
+ or a worker entry carries a key outside the accepted set.
352
713
  OSError: When the specification file cannot be read.
353
714
  json.JSONDecodeError: When the file is not valid JSON.
354
715
  """
@@ -371,10 +732,12 @@ def load_batch_spec(specification_path: Path) -> BatchSpec:
371
732
  if not isinstance(each_entry, dict):
372
733
  raise ValueError("each worker must be an object")
373
734
  all_parsed_workers.append(_parse_worker_entry(each_entry))
735
+ advisor_spec = _parse_advisor_spec(parsed_payload.get(BATCH_SPEC_ADVISOR_KEY))
374
736
  return BatchSpec(
375
737
  role=role,
376
738
  should_ping=should_ping,
377
739
  all_workers=tuple(all_parsed_workers),
740
+ advisor=advisor_spec,
378
741
  )
379
742
 
380
743
 
@@ -422,11 +785,20 @@ def _write_assembled_prompt(
422
785
  *,
423
786
  worker_spec: WorkerSpec,
424
787
  prompt_path: Path,
788
+ advisor_session_id: str | None = None,
789
+ advisor_spec: AdvisorSpec | None = None,
425
790
  ) -> None:
426
791
  prompt_text = assemble_worker_prompt(
427
792
  all_prompt_part_paths=worker_spec.all_prompt_part_paths,
428
793
  tool_profile=worker_spec.tool_profile,
429
794
  )
795
+ if advisor_session_id is not None and advisor_spec is not None:
796
+ header = ADVISOR_PROMPT_HEADER_TEMPLATE.format(
797
+ session_id=advisor_session_id,
798
+ model=advisor_spec.model,
799
+ effort=advisor_spec.effort,
800
+ )
801
+ prompt_text = f"{header}{prompt_text}"
430
802
  prompt_path.write_text(prompt_text, encoding=UTF8_ENCODING)
431
803
 
432
804
 
@@ -460,6 +832,9 @@ def _worker_report(
460
832
  classification: str,
461
833
  is_ok: bool,
462
834
  report_text: str,
835
+ advisor_session_id: str | None = None,
836
+ advisor_completion_signal: str | None = None,
837
+ advisor_launcher: str | None = None,
463
838
  ) -> WorkerReport:
464
839
  with suppress(OSError):
465
840
  _write_report_file(scratch_paths.report_path, report_text)
@@ -474,6 +849,9 @@ def _worker_report(
474
849
  leader_socket=str(scratch_paths.leader_socket_path),
475
850
  prompt_path=str(scratch_paths.prompt_path),
476
851
  debug_path=str(scratch_paths.debug_path),
852
+ advisor_session_id=advisor_session_id,
853
+ advisor_completion_signal=advisor_completion_signal,
854
+ advisor_launcher=advisor_launcher,
477
855
  )
478
856
 
479
857
 
@@ -509,28 +887,218 @@ def _error_report_for_exception(
509
887
  )
510
888
 
511
889
 
890
+
891
+ def _bind_worker_advisor_session(
892
+ *,
893
+ advisor_spec: AdvisorSpec,
894
+ role_name: str,
895
+ all_used_session_ids: set[str],
896
+ session_id_lock: object | None,
897
+ ) -> tuple[str, str]:
898
+ if session_id_lock is not None:
899
+ with session_id_lock: # type: ignore[attr-defined] # Lock protocol from threading.Lock
900
+ return bind_unique_worker_advisor(
901
+ advisor_spec=advisor_spec,
902
+ role_name=role_name,
903
+ all_used_session_ids=all_used_session_ids,
904
+ )
905
+ return bind_unique_worker_advisor(
906
+ advisor_spec=advisor_spec,
907
+ role_name=role_name,
908
+ all_used_session_ids=all_used_session_ids,
909
+ )
910
+
911
+
912
+ def _advisor_blocked_report(
913
+ *,
914
+ worker_spec: WorkerSpec,
915
+ scratch_paths: WorkerScratchPaths,
916
+ report_text: str,
917
+ advisor_session_id: str | None = None,
918
+ advisor_launcher: str | None = None,
919
+ ) -> WorkerReport:
920
+ return _worker_report(
921
+ worker_spec=worker_spec,
922
+ scratch_paths=scratch_paths,
923
+ returncode=WORKER_EXCEPTION_RETURN_CODE,
924
+ classification=CLASSIFICATION_ADVISOR_BLOCKED,
925
+ is_ok=False,
926
+ report_text=report_text,
927
+ advisor_session_id=advisor_session_id,
928
+ advisor_completion_signal=None,
929
+ advisor_launcher=advisor_launcher,
930
+ )
931
+
932
+
933
+
934
+ def _attach_advisor_completion_if_configured(
935
+ *,
936
+ worker_spec: WorkerSpec,
937
+ scratch_paths: WorkerScratchPaths,
938
+ worker_report: WorkerReport,
939
+ advisor_spec: AdvisorSpec | None,
940
+ advisor_session_id: str | None,
941
+ advisor_launcher: str | None,
942
+ ) -> WorkerReport:
943
+ if advisor_spec is None or advisor_session_id is None:
944
+ return worker_report
945
+ try:
946
+ completion_signal = obtain_advisor_completion_verdict(
947
+ advisor_spec=advisor_spec,
948
+ role_name=worker_spec.role_name,
949
+ session_id=advisor_session_id,
950
+ report_text=worker_report.report_text,
951
+ )
952
+ except ValueError as advisor_error:
953
+ return _advisor_blocked_report(
954
+ worker_spec=worker_spec,
955
+ scratch_paths=scratch_paths,
956
+ report_text=str(advisor_error),
957
+ advisor_session_id=advisor_session_id,
958
+ advisor_launcher=advisor_launcher,
959
+ )
960
+ return _worker_report(
961
+ worker_spec=worker_spec,
962
+ scratch_paths=scratch_paths,
963
+ returncode=worker_report.returncode,
964
+ classification=worker_report.classification,
965
+ is_ok=worker_report.is_ok,
966
+ report_text=worker_report.report_text,
967
+ advisor_session_id=advisor_session_id,
968
+ advisor_completion_signal=completion_signal,
969
+ advisor_launcher=advisor_launcher,
970
+ )
971
+
972
+
973
+ def _maybe_bind_advisor_for_worker(
974
+ *,
975
+ worker_spec: WorkerSpec,
976
+ scratch_paths: WorkerScratchPaths,
977
+ advisor_spec: AdvisorSpec | None,
978
+ all_used_session_ids: set[str] | None,
979
+ session_id_lock: object | None,
980
+ ) -> tuple[str | None, str | None, WorkerReport | None]:
981
+ if advisor_spec is None:
982
+ return None, None, None
983
+ used_ids = all_used_session_ids if all_used_session_ids is not None else set()
984
+ advisor_session_id, pre_signal = _bind_worker_advisor_session(
985
+ advisor_spec=advisor_spec,
986
+ role_name=worker_spec.role_name,
987
+ all_used_session_ids=used_ids,
988
+ session_id_lock=session_id_lock,
989
+ )
990
+ if (
991
+ advisor_session_id == PENDING_BIND_SENTINEL
992
+ or pre_signal == PENDING_BIND_SENTINEL
993
+ ):
994
+ blocked = _advisor_blocked_report(
995
+ worker_spec=worker_spec,
996
+ scratch_paths=scratch_paths,
997
+ report_text=f"{PENDING_BIND_SENTINEL} sentinel before launch",
998
+ advisor_session_id=advisor_session_id,
999
+ advisor_launcher=advisor_spec.launcher,
1000
+ )
1001
+ return advisor_session_id, advisor_spec.launcher, blocked
1002
+ return advisor_session_id, advisor_spec.launcher, None
1003
+
1004
+
1005
+ def _map_launch_exception(
1006
+ *,
1007
+ worker_spec: WorkerSpec,
1008
+ scratch_paths: WorkerScratchPaths,
1009
+ raised_exception: BaseException,
1010
+ advisor_spec: AdvisorSpec | None,
1011
+ advisor_session_id: str | None,
1012
+ advisor_launcher: str | None,
1013
+ ) -> WorkerReport:
1014
+ if advisor_spec is not None and (
1015
+ isinstance(raised_exception, AdvisorFailureError)
1016
+ or PENDING_BIND_SENTINEL in str(raised_exception)
1017
+ or "advisor" in str(raised_exception).lower()
1018
+ ):
1019
+ return _advisor_blocked_report(
1020
+ worker_spec=worker_spec,
1021
+ scratch_paths=scratch_paths,
1022
+ report_text=f"{type(raised_exception).__name__}: {raised_exception}",
1023
+ advisor_session_id=advisor_session_id,
1024
+ advisor_launcher=advisor_launcher,
1025
+ )
1026
+ return _error_report_for_exception(
1027
+ worker_spec=worker_spec,
1028
+ scratch_paths=scratch_paths,
1029
+ raised_exception=raised_exception,
1030
+ )
1031
+
1032
+
1033
+ def _run_worker_body(
1034
+ *,
1035
+ worker_spec: WorkerSpec,
1036
+ scratch_paths: WorkerScratchPaths,
1037
+ run_state_directory: Path,
1038
+ advisor_spec: AdvisorSpec | None,
1039
+ advisor_session_id: str | None,
1040
+ advisor_launcher: str | None,
1041
+ ) -> WorkerReport:
1042
+ _write_assembled_prompt(
1043
+ worker_spec=worker_spec,
1044
+ prompt_path=scratch_paths.prompt_path,
1045
+ advisor_session_id=advisor_session_id,
1046
+ advisor_spec=advisor_spec,
1047
+ )
1048
+ outcome = _invoke_worker(
1049
+ worker_spec=worker_spec,
1050
+ scratch_paths=scratch_paths,
1051
+ run_state_directory=run_state_directory,
1052
+ )
1053
+ worker_report = _build_worker_report(
1054
+ worker_spec=worker_spec,
1055
+ outcome=outcome,
1056
+ scratch_paths=scratch_paths,
1057
+ )
1058
+ return _attach_advisor_completion_if_configured(
1059
+ worker_spec=worker_spec,
1060
+ scratch_paths=scratch_paths,
1061
+ worker_report=worker_report,
1062
+ advisor_spec=advisor_spec,
1063
+ advisor_session_id=advisor_session_id,
1064
+ advisor_launcher=advisor_launcher,
1065
+ )
1066
+
1067
+
512
1068
  def _launch_one_worker(
513
1069
  *,
514
1070
  worker_spec: WorkerSpec,
515
1071
  worker_index: int,
516
1072
  run_state_directory: Path,
1073
+ advisor_spec: AdvisorSpec | None = None,
1074
+ all_used_session_ids: set[str] | None = None,
1075
+ session_id_lock: object | None = None,
517
1076
  ) -> WorkerReport:
518
1077
  batch_sleep(worker_index * WORKER_STAGGER_SECONDS)
519
1078
  scratch_paths = _mint_worker_scratch_paths(run_state_directory)
1079
+ advisor_session_id: str | None = None
1080
+ advisor_launcher: str | None = None
520
1081
  try:
521
- _write_assembled_prompt(
522
- worker_spec=worker_spec,
523
- prompt_path=scratch_paths.prompt_path,
524
- )
525
- outcome = _invoke_worker(
1082
+ (
1083
+ advisor_session_id,
1084
+ advisor_launcher,
1085
+ early_block,
1086
+ ) = _maybe_bind_advisor_for_worker(
526
1087
  worker_spec=worker_spec,
527
1088
  scratch_paths=scratch_paths,
528
- run_state_directory=run_state_directory,
1089
+ advisor_spec=advisor_spec,
1090
+ all_used_session_ids=all_used_session_ids,
1091
+ session_id_lock=session_id_lock,
529
1092
  )
530
- return _build_worker_report(
1093
+ if early_block is not None:
1094
+ return early_block
1095
+ return _run_worker_body(
531
1096
  worker_spec=worker_spec,
532
- outcome=outcome,
533
1097
  scratch_paths=scratch_paths,
1098
+ run_state_directory=run_state_directory,
1099
+ advisor_spec=advisor_spec,
1100
+ advisor_session_id=advisor_session_id,
1101
+ advisor_launcher=advisor_launcher,
534
1102
  )
535
1103
  except (
536
1104
  OSError,
@@ -539,14 +1107,19 @@ def _launch_one_worker(
539
1107
  TypeError,
540
1108
  AttributeError,
541
1109
  LookupError,
1110
+ AdvisorFailureError,
542
1111
  ) as raised_exception:
543
- return _error_report_for_exception(
1112
+ return _map_launch_exception(
544
1113
  worker_spec=worker_spec,
545
1114
  scratch_paths=scratch_paths,
546
1115
  raised_exception=raised_exception,
1116
+ advisor_spec=advisor_spec,
1117
+ advisor_session_id=advisor_session_id,
1118
+ advisor_launcher=advisor_launcher,
547
1119
  )
548
1120
 
549
1121
 
1122
+
550
1123
  def run_grok_batch(
551
1124
  *,
552
1125
  batch_spec: BatchSpec,
@@ -580,6 +1153,8 @@ def run_grok_batch(
580
1153
  preflight_reason=None,
581
1154
  all_worker_reports=(),
582
1155
  )
1156
+ all_used_session_ids: set[str] = set()
1157
+ session_id_lock = threading.Lock()
583
1158
  with ThreadPoolExecutor(max_workers=worker_count) as executor:
584
1159
  all_futures = [
585
1160
  executor.submit(
@@ -587,6 +1162,9 @@ def run_grok_batch(
587
1162
  worker_spec=each_worker,
588
1163
  worker_index=each_index,
589
1164
  run_state_directory=run_state_directory,
1165
+ advisor_spec=batch_spec.advisor,
1166
+ all_used_session_ids=all_used_session_ids,
1167
+ session_id_lock=session_id_lock,
590
1168
  )
591
1169
  for each_index, each_worker in enumerate(batch_spec.all_workers)
592
1170
  ]
@@ -621,6 +1199,9 @@ def batch_summary_as_dict(batch_summary: BatchSummary) -> dict[str, object]:
621
1199
  SUMMARY_LEADER_SOCKET_KEY: each_report.leader_socket,
622
1200
  SUMMARY_PROMPT_FILE_KEY: each_report.prompt_path,
623
1201
  SUMMARY_DEBUG_FILE_KEY: each_report.debug_path,
1202
+ SUMMARY_ADVISOR_SESSION_ID_KEY: each_report.advisor_session_id,
1203
+ SUMMARY_ADVISOR_SIGNAL_KEY: each_report.advisor_completion_signal,
1204
+ SUMMARY_ADVISOR_LAUNCHER_KEY: each_report.advisor_launcher,
624
1205
  }
625
1206
  for each_report in batch_summary.all_worker_reports
626
1207
  ]