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.
- package/CLAUDE.md +7 -1
- package/agents/clean-coder.md +9 -19
- package/agents/test_agent_frontmatter.py +26 -0
- package/docs/CODE_RULES.md +4 -2
- package/docs/references/CLAUDE.md +2 -2
- package/docs/references/advisor-tool.md +44 -6
- package/docs/references/team-advisor-skill.md +14 -8
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/output-styles/CLAUDE.md +17 -0
- package/output-styles/caveman-agent.md +37 -0
- package/package.json +2 -1
- package/rules/code-standards.md +33 -7
- package/rules/eli11-replies.md +1 -1
- package/scripts/CLAUDE.md +2 -2
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
- package/scripts/dev_env_scripts_constants/grok_run_ledger_constants.py +50 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +104 -0
- package/scripts/grok_patch_artifacts.py +123 -0
- package/scripts/grok_run_ledger.py +318 -0
- package/scripts/spawn_grok_batch.py +553 -9
- package/scripts/test_grok_patch_artifacts.py +82 -0
- package/scripts/test_grok_run_ledger.py +116 -0
- package/scripts/test_spawn_grok_batch.py +295 -0
- package/skills/CLAUDE.md +4 -2
- package/skills/_shared/CLAUDE.md +37 -4
- package/skills/_shared/advisor/CLAUDE.md +9 -0
- package/skills/_shared/advisor/advisor-protocol.md +5 -0
- package/skills/_shared/advisor/scripts/README.md +9 -0
- package/skills/_shared/end-of-run-gotcha-recommendations.md +156 -0
- package/skills/_shared/pr-loop/CLAUDE.md +18 -1
- package/skills/_shared/pr-loop/audit-contract.md +5 -0
- package/skills/_shared/pr-loop/audit-reply-template.md +5 -0
- package/skills/_shared/pr-loop/code-rules-gate.md +5 -0
- package/skills/_shared/pr-loop/fix-protocol.md +5 -0
- package/skills/_shared/pr-loop/gh-payloads.md +5 -0
- package/skills/_shared/pr-loop/post-audit-thread-contract.md +5 -0
- package/skills/_shared/pr-loop/precatch-rubric.md +5 -0
- package/skills/_shared/pr-loop/scripts/CLAUDE.md +8 -1
- package/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +29 -0
- package/skills/_shared/pr-loop/state-schema.md +5 -0
- package/skills/_shared/pr-loop/worker-spawn.md +5 -0
- package/skills/e-code-review/SKILL.md +6 -1
- package/skills/e-code-review/reference/runner-selection.md +40 -0
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/__init__.py +1 -0
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +55 -0
- package/skills/e-code-review/scripts/grok_code_review.py +221 -0
- package/skills/e-code-review/scripts/test_grok_code_review.py +212 -0
- package/skills/grok-spawn/SKILL.md +5 -0
- package/skills/orchestrator/SKILL.md +5 -0
- package/skills/task-build/reference/tool-routing.md +3 -0
- package/skills/team-advisor/SKILL.md +23 -44
- package/system-prompts/software-engineer.xml +6 -3
- 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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
|
|
1089
|
+
advisor_spec=advisor_spec,
|
|
1090
|
+
all_used_session_ids=all_used_session_ids,
|
|
1091
|
+
session_id_lock=session_id_lock,
|
|
566
1092
|
)
|
|
567
|
-
|
|
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
|
|
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
|
]
|