claude-dev-env 2.8.0 → 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 (53) hide show
  1. package/CLAUDE.md +7 -1
  2. package/agents/clean-coder.md +9 -19
  3. package/agents/test_agent_frontmatter.py +26 -0
  4. package/docs/CODE_RULES.md +4 -2
  5. package/docs/references/CLAUDE.md +2 -2
  6. package/docs/references/advisor-tool.md +44 -6
  7. package/docs/references/team-advisor-skill.md +14 -8
  8. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  9. package/output-styles/CLAUDE.md +17 -0
  10. package/output-styles/caveman-agent.md +37 -0
  11. package/package.json +2 -1
  12. package/rules/code-standards.md +33 -7
  13. package/rules/eli11-replies.md +1 -1
  14. package/scripts/CLAUDE.md +2 -2
  15. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
  16. package/scripts/dev_env_scripts_constants/grok_run_ledger_constants.py +50 -0
  17. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +104 -0
  18. package/scripts/grok_patch_artifacts.py +123 -0
  19. package/scripts/grok_run_ledger.py +318 -0
  20. package/scripts/spawn_grok_batch.py +553 -9
  21. package/scripts/test_grok_patch_artifacts.py +82 -0
  22. package/scripts/test_grok_run_ledger.py +116 -0
  23. package/scripts/test_spawn_grok_batch.py +295 -0
  24. package/skills/CLAUDE.md +4 -2
  25. package/skills/_shared/CLAUDE.md +37 -4
  26. package/skills/_shared/advisor/CLAUDE.md +9 -0
  27. package/skills/_shared/advisor/advisor-protocol.md +5 -0
  28. package/skills/_shared/advisor/scripts/README.md +9 -0
  29. package/skills/_shared/end-of-run-gotcha-recommendations.md +156 -0
  30. package/skills/_shared/pr-loop/CLAUDE.md +18 -1
  31. package/skills/_shared/pr-loop/audit-contract.md +5 -0
  32. package/skills/_shared/pr-loop/audit-reply-template.md +5 -0
  33. package/skills/_shared/pr-loop/code-rules-gate.md +5 -0
  34. package/skills/_shared/pr-loop/fix-protocol.md +5 -0
  35. package/skills/_shared/pr-loop/gh-payloads.md +5 -0
  36. package/skills/_shared/pr-loop/post-audit-thread-contract.md +5 -0
  37. package/skills/_shared/pr-loop/precatch-rubric.md +5 -0
  38. package/skills/_shared/pr-loop/scripts/CLAUDE.md +8 -1
  39. package/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +29 -0
  40. package/skills/_shared/pr-loop/state-schema.md +5 -0
  41. package/skills/_shared/pr-loop/worker-spawn.md +5 -0
  42. package/skills/e-code-review/SKILL.md +6 -1
  43. package/skills/e-code-review/reference/runner-selection.md +40 -0
  44. package/skills/e-code-review/scripts/e_code_review_scripts_constants/__init__.py +1 -0
  45. package/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +55 -0
  46. package/skills/e-code-review/scripts/grok_code_review.py +221 -0
  47. package/skills/e-code-review/scripts/test_grok_code_review.py +212 -0
  48. package/skills/grok-spawn/SKILL.md +5 -0
  49. package/skills/orchestrator/SKILL.md +5 -0
  50. package/skills/task-build/reference/tool-routing.md +3 -0
  51. package/skills/team-advisor/SKILL.md +23 -44
  52. package/system-prompts/software-engineer.xml +6 -3
  53. 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,320 @@ 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
+ 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
+
95
401
  def _require_known_worker_keys(all_worker_fields: dict[str, object]) -> None:
96
402
  """Reject a worker entry carrying a key the launcher does not accept.
97
403
 
@@ -137,6 +443,20 @@ class WorkerSpec:
137
443
  agent_name: str | None = None
138
444
 
139
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
+
140
460
  @dataclass(frozen=True)
141
461
  class BatchSpec:
142
462
  """Full batch specification for one fleet launch."""
@@ -144,6 +464,7 @@ class BatchSpec:
144
464
  role: str
145
465
  should_ping: bool
146
466
  all_workers: tuple[WorkerSpec, ...]
467
+ advisor: AdvisorSpec | None = None
147
468
 
148
469
 
149
470
  @dataclass(frozen=True)
@@ -170,6 +491,9 @@ class WorkerReport:
170
491
  leader_socket: str
171
492
  prompt_path: str
172
493
  debug_path: str
494
+ advisor_session_id: str | None = None
495
+ advisor_completion_signal: str | None = None
496
+ advisor_launcher: str | None = None
173
497
 
174
498
 
175
499
  @dataclass(frozen=True)
@@ -408,10 +732,12 @@ def load_batch_spec(specification_path: Path) -> BatchSpec:
408
732
  if not isinstance(each_entry, dict):
409
733
  raise ValueError("each worker must be an object")
410
734
  all_parsed_workers.append(_parse_worker_entry(each_entry))
735
+ advisor_spec = _parse_advisor_spec(parsed_payload.get(BATCH_SPEC_ADVISOR_KEY))
411
736
  return BatchSpec(
412
737
  role=role,
413
738
  should_ping=should_ping,
414
739
  all_workers=tuple(all_parsed_workers),
740
+ advisor=advisor_spec,
415
741
  )
416
742
 
417
743
 
@@ -459,11 +785,20 @@ def _write_assembled_prompt(
459
785
  *,
460
786
  worker_spec: WorkerSpec,
461
787
  prompt_path: Path,
788
+ advisor_session_id: str | None = None,
789
+ advisor_spec: AdvisorSpec | None = None,
462
790
  ) -> None:
463
791
  prompt_text = assemble_worker_prompt(
464
792
  all_prompt_part_paths=worker_spec.all_prompt_part_paths,
465
793
  tool_profile=worker_spec.tool_profile,
466
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}"
467
802
  prompt_path.write_text(prompt_text, encoding=UTF8_ENCODING)
468
803
 
469
804
 
@@ -497,6 +832,9 @@ def _worker_report(
497
832
  classification: str,
498
833
  is_ok: bool,
499
834
  report_text: str,
835
+ advisor_session_id: str | None = None,
836
+ advisor_completion_signal: str | None = None,
837
+ advisor_launcher: str | None = None,
500
838
  ) -> WorkerReport:
501
839
  with suppress(OSError):
502
840
  _write_report_file(scratch_paths.report_path, report_text)
@@ -511,6 +849,9 @@ def _worker_report(
511
849
  leader_socket=str(scratch_paths.leader_socket_path),
512
850
  prompt_path=str(scratch_paths.prompt_path),
513
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,
514
855
  )
515
856
 
516
857
 
@@ -546,28 +887,218 @@ def _error_report_for_exception(
546
887
  )
547
888
 
548
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
+
549
1068
  def _launch_one_worker(
550
1069
  *,
551
1070
  worker_spec: WorkerSpec,
552
1071
  worker_index: int,
553
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,
554
1076
  ) -> WorkerReport:
555
1077
  batch_sleep(worker_index * WORKER_STAGGER_SECONDS)
556
1078
  scratch_paths = _mint_worker_scratch_paths(run_state_directory)
1079
+ advisor_session_id: str | None = None
1080
+ advisor_launcher: str | None = None
557
1081
  try:
558
- _write_assembled_prompt(
559
- worker_spec=worker_spec,
560
- prompt_path=scratch_paths.prompt_path,
561
- )
562
- outcome = _invoke_worker(
1082
+ (
1083
+ advisor_session_id,
1084
+ advisor_launcher,
1085
+ early_block,
1086
+ ) = _maybe_bind_advisor_for_worker(
563
1087
  worker_spec=worker_spec,
564
1088
  scratch_paths=scratch_paths,
565
- 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,
566
1092
  )
567
- return _build_worker_report(
1093
+ if early_block is not None:
1094
+ return early_block
1095
+ return _run_worker_body(
568
1096
  worker_spec=worker_spec,
569
- outcome=outcome,
570
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,
571
1102
  )
572
1103
  except (
573
1104
  OSError,
@@ -576,14 +1107,19 @@ def _launch_one_worker(
576
1107
  TypeError,
577
1108
  AttributeError,
578
1109
  LookupError,
1110
+ AdvisorFailureError,
579
1111
  ) as raised_exception:
580
- return _error_report_for_exception(
1112
+ return _map_launch_exception(
581
1113
  worker_spec=worker_spec,
582
1114
  scratch_paths=scratch_paths,
583
1115
  raised_exception=raised_exception,
1116
+ advisor_spec=advisor_spec,
1117
+ advisor_session_id=advisor_session_id,
1118
+ advisor_launcher=advisor_launcher,
584
1119
  )
585
1120
 
586
1121
 
1122
+
587
1123
  def run_grok_batch(
588
1124
  *,
589
1125
  batch_spec: BatchSpec,
@@ -617,6 +1153,8 @@ def run_grok_batch(
617
1153
  preflight_reason=None,
618
1154
  all_worker_reports=(),
619
1155
  )
1156
+ all_used_session_ids: set[str] = set()
1157
+ session_id_lock = threading.Lock()
620
1158
  with ThreadPoolExecutor(max_workers=worker_count) as executor:
621
1159
  all_futures = [
622
1160
  executor.submit(
@@ -624,6 +1162,9 @@ def run_grok_batch(
624
1162
  worker_spec=each_worker,
625
1163
  worker_index=each_index,
626
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,
627
1168
  )
628
1169
  for each_index, each_worker in enumerate(batch_spec.all_workers)
629
1170
  ]
@@ -658,6 +1199,9 @@ def batch_summary_as_dict(batch_summary: BatchSummary) -> dict[str, object]:
658
1199
  SUMMARY_LEADER_SOCKET_KEY: each_report.leader_socket,
659
1200
  SUMMARY_PROMPT_FILE_KEY: each_report.prompt_path,
660
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,
661
1205
  }
662
1206
  for each_report in batch_summary.all_worker_reports
663
1207
  ]