okstra 0.167.0 → 0.169.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/README.md +6 -5
- package/docs/architecture/storage-model.md +57 -1
- package/docs/architecture.md +70 -2
- package/docs/cli.md +8 -4
- package/docs/for-ai/skills/okstra-code-review.md +3 -2
- package/docs/for-ai/skills/okstra-schedule-gen.md +3 -1
- package/docs/pr-template-usage.md +10 -6
- package/docs/project-structure-overview.md +14 -11
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +6 -5
- package/runtime/agents/workers/report-writer-worker.md +9 -4
- package/runtime/agents/workers/translator-worker.md +6 -4
- package/runtime/prompts/coding-preflight/architectures/hexagonal.md +27 -1
- package/runtime/prompts/coding-preflight/clean-code.md +13 -0
- package/runtime/prompts/duties/acceptance-critic.md +24 -0
- package/runtime/prompts/duties/acceptance-verifier.md +24 -0
- package/runtime/prompts/duties/analysis-worker.md +24 -0
- package/runtime/prompts/duties/code-reviewer.md +24 -0
- package/runtime/prompts/duties/common.md +35 -0
- package/runtime/prompts/duties/implementation-executor.md +24 -0
- package/runtime/prompts/duties/implementation-verifier.md +24 -0
- package/runtime/prompts/duties/lead.md +24 -0
- package/runtime/prompts/duties/report-writer.md +24 -0
- package/runtime/prompts/duties/reverification-worker.md +24 -0
- package/runtime/prompts/duties/schedule-verifier.md +24 -0
- package/runtime/prompts/duties/scope-critic.md +24 -0
- package/runtime/prompts/duties/translator.md +24 -0
- package/runtime/prompts/lead/convergence.md +51 -7
- package/runtime/prompts/lead/okstra-lead-contract.md +10 -20
- package/runtime/prompts/lead/plan-body-verification.md +16 -1
- package/runtime/prompts/lead/report-writer.md +20 -5
- package/runtime/prompts/lead/team-contract.md +13 -13
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +3 -1
- package/runtime/prompts/profiles/_implementation-executor.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -1
- package/runtime/prompts/profiles/implementation.md +4 -2
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +6 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +3 -2
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +8 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +33 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +13 -12
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +6 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +3 -2
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +2 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +3 -3
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +6 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +2 -1
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +6 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +2 -1
- package/runtime/python/okstra_ctl/agent_invocation.py +1502 -0
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +788 -0
- package/runtime/python/okstra_ctl/codex_dispatch.py +2 -107
- package/runtime/python/okstra_ctl/context_cost.py +46 -5
- package/runtime/python/okstra_ctl/dispatch_core.py +312 -37
- package/runtime/python/okstra_ctl/dispatch_state.py +461 -36
- package/runtime/python/okstra_ctl/doctor.py +150 -16
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +87 -9
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +214 -23
- package/runtime/python/okstra_ctl/path_hints.py +26 -0
- package/runtime/python/okstra_ctl/paths.py +20 -0
- package/runtime/python/okstra_ctl/ports/__init__.py +8 -0
- package/runtime/python/okstra_ctl/ports/host.py +3 -0
- package/runtime/python/okstra_ctl/ports/host_model.py +60 -0
- package/runtime/python/okstra_ctl/pr_template.py +3 -6
- package/runtime/python/okstra_ctl/registry/host_registry.py +5 -0
- package/runtime/python/okstra_ctl/render.py +217 -12
- package/runtime/python/okstra_ctl/report_finalize.py +44 -0
- package/runtime/python/okstra_ctl/run.py +368 -51
- package/runtime/python/okstra_ctl/session.py +16 -12
- package/runtime/python/okstra_ctl/team.py +11 -11
- package/runtime/python/okstra_ctl/worker_dispatch.py +104 -0
- package/runtime/python/okstra_ctl/worker_prompt_body.py +5 -38
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +32 -2
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +38 -1
- package/runtime/skills/okstra-code-review/SKILL.md +22 -3
- package/runtime/skills/okstra-run/SKILL.md +16 -1
- package/runtime/skills/okstra-schedule-gen/SKILL.md +15 -1
- package/runtime/templates/implementation-worker-preamble.md +0 -10
- package/runtime/templates/report-writer-prompt-preamble.md +0 -9
- package/runtime/templates/reports/settings.template.json +0 -11
- package/runtime/templates/worker-prompt-preamble.md +0 -10
- package/runtime/validators/lib/fixtures.sh +93 -0
- package/runtime/validators/lib/validate-assets.sh +0 -8
- package/runtime/validators/validate-run.py +182 -0
- package/src/cli-registry.mjs +14 -0
- package/src/commands/execute/agent-prompt.mjs +25 -0
- package/src/commands/execute/codex-dispatch.mjs +6 -63
- package/src/commands/execute/worker-dispatch.mjs +76 -0
- package/src/commands/lifecycle/doctor.mjs +18 -3
- package/src/commands/lifecycle/install.mjs +33 -15
- package/src/commands/lifecycle/uninstall.mjs +4 -3
- package/src/lib/install-assets.mjs +9 -0
- package/runtime/agents/workers/antigravity-worker.md +0 -259
- package/runtime/agents/workers/codex-worker.md +0 -259
- package/runtime/agents/workers/grok-worker.md +0 -259
- package/runtime/agents/workers/kimi-worker.md +0 -259
- package/runtime/prompts/coding-preflight/scripts/preedit-check.sh +0 -79
- package/runtime/templates/operating-standard.md +0 -22
- package/src/lib/worker-agent-render.mjs +0 -50
- /package/runtime/templates/{prd → pr}/pr-body.template.md +0 -0
|
@@ -16,19 +16,22 @@ from .dispatch_state import (
|
|
|
16
16
|
BACKEND_CMUX_PANE,
|
|
17
17
|
BACKEND_MIXED,
|
|
18
18
|
BACKEND_TMUX_PANE,
|
|
19
|
+
append_worker_dispatch as _append_worker_dispatch,
|
|
19
20
|
DispatchError,
|
|
20
21
|
WorkerJob,
|
|
21
22
|
dispatch_mode as _dispatch_mode,
|
|
22
|
-
dispatch_record_matches,
|
|
23
23
|
LIVENESS_AUDIT_HEARTBEAT,
|
|
24
24
|
LIVENESS_WRAPPER_STATUS,
|
|
25
25
|
load_json_object as _load_json_object,
|
|
26
|
+
link_agent_dispatch_result as _link_agent_dispatch_result,
|
|
26
27
|
missing_completion_paths as _missing_completion_paths,
|
|
28
|
+
mutate_team_state as _mutate_team_state,
|
|
27
29
|
require_string as _require_string,
|
|
28
30
|
resolve_project_path as _resolve_project_path,
|
|
29
31
|
resolve_required_path as _resolve_required_path,
|
|
30
32
|
record_dispatch_facts as _record_dispatch_facts,
|
|
31
33
|
transition_worker_status as _transition_worker_status,
|
|
34
|
+
update_worker_dispatch_status as _update_worker_dispatch_status,
|
|
32
35
|
string_list as _string_list,
|
|
33
36
|
string_value as _string_value,
|
|
34
37
|
TEARDOWN_BEFORE_TERMINAL_REASON,
|
|
@@ -37,7 +40,6 @@ from .dispatch_state import (
|
|
|
37
40
|
worker_jobs_from_file as _worker_jobs_from_file,
|
|
38
41
|
worker_state as _worker_state,
|
|
39
42
|
worktree_path as _worktree_path,
|
|
40
|
-
write_json as _write_json,
|
|
41
43
|
)
|
|
42
44
|
from .final_report_paths import (
|
|
43
45
|
final_report_data_path as _final_report_data_path,
|
|
@@ -74,6 +76,7 @@ class WorkerHandle:
|
|
|
74
76
|
completed_process: subprocess.CompletedProcess[str] | None
|
|
75
77
|
status_sidecar_path: Path | None
|
|
76
78
|
degraded_from: str
|
|
79
|
+
running_process: subprocess.Popen[str] | None = None
|
|
77
80
|
|
|
78
81
|
|
|
79
82
|
@dataclass(frozen=True)
|
|
@@ -174,7 +177,6 @@ def build_dispatch_plan(
|
|
|
174
177
|
)
|
|
175
178
|
if jobs_file:
|
|
176
179
|
jobs = _jobs_from_file(project_root, workspace_root, jobs_file, options)
|
|
177
|
-
_validate_dispatch_prompts(manifest, active_context, jobs)
|
|
178
180
|
else:
|
|
179
181
|
jobs = _jobs_from_roster(
|
|
180
182
|
project_root,
|
|
@@ -186,6 +188,7 @@ def build_dispatch_plan(
|
|
|
186
188
|
requested_workers,
|
|
187
189
|
options,
|
|
188
190
|
)
|
|
191
|
+
_validate_dispatch_prompts(manifest, active_context, jobs)
|
|
189
192
|
return DispatchPlan(
|
|
190
193
|
project_root=project_root,
|
|
191
194
|
workspace_root=workspace_root.resolve(),
|
|
@@ -199,6 +202,7 @@ def build_dispatch_plan(
|
|
|
199
202
|
|
|
200
203
|
|
|
201
204
|
def dispatch_plan(plan: DispatchPlan, *, wait: bool = True) -> int:
|
|
205
|
+
_validate_report_writer_isolation(plan.jobs)
|
|
202
206
|
if wait:
|
|
203
207
|
pane_backends = sorted(
|
|
204
208
|
{
|
|
@@ -225,6 +229,161 @@ def dispatch_plan(plan: DispatchPlan, *, wait: bool = True) -> int:
|
|
|
225
229
|
return 0
|
|
226
230
|
|
|
227
231
|
|
|
232
|
+
def dispatch_cli_wrapper_plan(plan: DispatchPlan) -> int:
|
|
233
|
+
"""Start one dependency-free CLI batch before collecting any worker."""
|
|
234
|
+
if any(job.backend != BACKEND_CLI_WRAPPER for job in plan.jobs):
|
|
235
|
+
raise DispatchError("concurrent CLI dispatch requires cli-wrapper jobs only")
|
|
236
|
+
_validate_report_writer_isolation(plan.jobs)
|
|
237
|
+
_record_dispatch_facts(plan.team_state_path, _dispatch_mode(plan.jobs))
|
|
238
|
+
final_codes = _dispatch_cli_wrapper_batch(plan, plan.jobs)
|
|
239
|
+
return next((final_codes[job.worker_id] for job in plan.jobs
|
|
240
|
+
if final_codes.get(job.worker_id, 0) != 0), 0)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _validate_report_writer_isolation(jobs: Sequence[WorkerJob]) -> None:
|
|
244
|
+
if (
|
|
245
|
+
any(job.worker_id == REPORT_WRITER_WORKER_ID for job in jobs)
|
|
246
|
+
and len(jobs) > 1
|
|
247
|
+
):
|
|
248
|
+
raise DispatchError(
|
|
249
|
+
"report-writer must run in a separate Phase 6 dispatch after "
|
|
250
|
+
"analysis and convergence"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _dispatch_cli_wrapper_batch(
|
|
255
|
+
plan: DispatchPlan,
|
|
256
|
+
jobs: Sequence[WorkerJob],
|
|
257
|
+
) -> dict[str, int]:
|
|
258
|
+
"""Run one concurrent batch through all retries before the next dependency."""
|
|
259
|
+
pending = [(job, 1) for job in jobs]
|
|
260
|
+
final_codes: dict[str, int] = {}
|
|
261
|
+
while pending:
|
|
262
|
+
active: list[tuple[WorkerHandle, int]] = []
|
|
263
|
+
starting_job: WorkerJob | None = None
|
|
264
|
+
starting_attempt = 1
|
|
265
|
+
try:
|
|
266
|
+
for job, attempt in pending:
|
|
267
|
+
starting_job = job
|
|
268
|
+
starting_attempt = attempt
|
|
269
|
+
active.append((_spawn_cli_job_nonblocking(plan, job, attempt), attempt))
|
|
270
|
+
except (OSError, DispatchError) as exc:
|
|
271
|
+
if starting_job is not None:
|
|
272
|
+
_abort_cli_batch_start(
|
|
273
|
+
plan,
|
|
274
|
+
active,
|
|
275
|
+
starting_job,
|
|
276
|
+
starting_attempt,
|
|
277
|
+
pending,
|
|
278
|
+
exc,
|
|
279
|
+
)
|
|
280
|
+
raise
|
|
281
|
+
outcomes: list[tuple[WorkerHandle, int, WorkerOutcome]] = []
|
|
282
|
+
for handle, attempt in active:
|
|
283
|
+
process = handle.running_process
|
|
284
|
+
if process is None:
|
|
285
|
+
raise DispatchError("concurrent CLI worker process is missing")
|
|
286
|
+
returncode = process.wait()
|
|
287
|
+
completed = subprocess.CompletedProcess(handle.job.command, returncode)
|
|
288
|
+
settled = replace(handle, completed_process=completed, running_process=None)
|
|
289
|
+
outcomes.append((settled, attempt, _outcome_from_completed(settled)))
|
|
290
|
+
|
|
291
|
+
next_round: list[tuple[WorkerJob, int]] = []
|
|
292
|
+
for handle, attempt, outcome in outcomes:
|
|
293
|
+
job = handle.job
|
|
294
|
+
_finish_attempt(plan, job, attempt, outcome)
|
|
295
|
+
if _worker_terminal_status(plan.team_state_path, job.worker_id) == "completed":
|
|
296
|
+
final_codes[job.worker_id] = 0
|
|
297
|
+
continue
|
|
298
|
+
if _should_retry(outcome, attempt):
|
|
299
|
+
_append_event(
|
|
300
|
+
plan,
|
|
301
|
+
"worker-retry-scheduled",
|
|
302
|
+
_retry_details(job, attempt, outcome),
|
|
303
|
+
)
|
|
304
|
+
next_round.append((job, attempt + 1))
|
|
305
|
+
continue
|
|
306
|
+
final_codes[job.worker_id] = outcome.returncode or 1
|
|
307
|
+
pending = next_round
|
|
308
|
+
return final_codes
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _abort_cli_batch_start(
|
|
312
|
+
plan: DispatchPlan,
|
|
313
|
+
active: Sequence[tuple[WorkerHandle, int]],
|
|
314
|
+
failed_job: WorkerJob,
|
|
315
|
+
failed_attempt: int,
|
|
316
|
+
pending: Sequence[tuple[WorkerJob, int]],
|
|
317
|
+
error: Exception,
|
|
318
|
+
) -> None:
|
|
319
|
+
reason = f"concurrent CLI batch launch failed: {error}"
|
|
320
|
+
for handle, _attempt in active:
|
|
321
|
+
process = handle.running_process
|
|
322
|
+
if process is not None and process.poll() is None:
|
|
323
|
+
process.terminate()
|
|
324
|
+
for handle, attempt in active:
|
|
325
|
+
process = handle.running_process
|
|
326
|
+
if process is not None:
|
|
327
|
+
process.wait()
|
|
328
|
+
_transition_worker_status(
|
|
329
|
+
plan.team_state_path, handle.job.worker_id, "error", reason
|
|
330
|
+
)
|
|
331
|
+
_update_dispatch_status(
|
|
332
|
+
plan.team_state_path, handle.job, attempt, "error", reason
|
|
333
|
+
)
|
|
334
|
+
_transition_worker_status(
|
|
335
|
+
plan.team_state_path, failed_job.worker_id, "error", reason
|
|
336
|
+
)
|
|
337
|
+
if not _update_dispatch_status(
|
|
338
|
+
plan.team_state_path,
|
|
339
|
+
failed_job,
|
|
340
|
+
failed_attempt,
|
|
341
|
+
"error",
|
|
342
|
+
reason,
|
|
343
|
+
):
|
|
344
|
+
failed_handle = WorkerHandle(
|
|
345
|
+
failed_job,
|
|
346
|
+
"",
|
|
347
|
+
None,
|
|
348
|
+
status_path_for_prompt(failed_job.prompt_path),
|
|
349
|
+
"",
|
|
350
|
+
)
|
|
351
|
+
_record_dispatch(
|
|
352
|
+
plan.team_state_path,
|
|
353
|
+
failed_handle,
|
|
354
|
+
failed_attempt,
|
|
355
|
+
"error",
|
|
356
|
+
reason,
|
|
357
|
+
)
|
|
358
|
+
closed = {
|
|
359
|
+
(handle.job.worker_id, attempt) for handle, attempt in active
|
|
360
|
+
}
|
|
361
|
+
closed.add((failed_job.worker_id, failed_attempt))
|
|
362
|
+
for job, attempt in pending:
|
|
363
|
+
if (job.worker_id, attempt) in closed:
|
|
364
|
+
continue
|
|
365
|
+
_transition_worker_status(
|
|
366
|
+
plan.team_state_path, job.worker_id, "error", reason
|
|
367
|
+
)
|
|
368
|
+
if not _update_dispatch_status(
|
|
369
|
+
plan.team_state_path, job, attempt, "error", reason
|
|
370
|
+
):
|
|
371
|
+
handle = WorkerHandle(
|
|
372
|
+
job,
|
|
373
|
+
"",
|
|
374
|
+
None,
|
|
375
|
+
status_path_for_prompt(job.prompt_path),
|
|
376
|
+
"",
|
|
377
|
+
)
|
|
378
|
+
_record_dispatch(
|
|
379
|
+
plan.team_state_path,
|
|
380
|
+
handle,
|
|
381
|
+
attempt,
|
|
382
|
+
"error",
|
|
383
|
+
reason,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
|
|
228
387
|
def await_dispatches(
|
|
229
388
|
plan: DispatchPlan,
|
|
230
389
|
*,
|
|
@@ -312,7 +471,15 @@ def _select_workers(
|
|
|
312
471
|
options,
|
|
313
472
|
)
|
|
314
473
|
supported = set(options.supported_worker_wrappers)
|
|
315
|
-
selected =
|
|
474
|
+
selected = (
|
|
475
|
+
list(requested_workers)
|
|
476
|
+
if requested_workers
|
|
477
|
+
else [
|
|
478
|
+
worker
|
|
479
|
+
for worker in recommended
|
|
480
|
+
if worker in supported and worker != REPORT_WRITER_WORKER_ID
|
|
481
|
+
]
|
|
482
|
+
)
|
|
316
483
|
unknown = [worker for worker in selected if worker not in recommended]
|
|
317
484
|
if unknown:
|
|
318
485
|
raise DispatchError("requested worker(s) are not in this run roster: " + ", ".join(unknown))
|
|
@@ -339,7 +506,11 @@ def _select_cli_wrapper_assignments(
|
|
|
339
506
|
if _is_cli_wrapper_assignment(team_state, worker_id, options)
|
|
340
507
|
}
|
|
341
508
|
if not requested_workers:
|
|
342
|
-
selected = [
|
|
509
|
+
selected = [
|
|
510
|
+
worker
|
|
511
|
+
for worker in recommended
|
|
512
|
+
if worker in dispatchable and worker != REPORT_WRITER_WORKER_ID
|
|
513
|
+
]
|
|
343
514
|
if selected:
|
|
344
515
|
return selected
|
|
345
516
|
raise DispatchError("run roster has no cli-wrapper assignments for codex-dispatch")
|
|
@@ -400,6 +571,8 @@ def _job_from_roster_worker(
|
|
|
400
571
|
options: _BuildOptions,
|
|
401
572
|
) -> WorkerJob:
|
|
402
573
|
result_path = _resolve_project_path(project_root, fact.result_path)
|
|
574
|
+
metadata = _agent_metadata(prompt_path)
|
|
575
|
+
invocation = _invocation_job_fields(metadata, prompt_path)
|
|
403
576
|
return WorkerJob(
|
|
404
577
|
worker_id=fact.worker_id,
|
|
405
578
|
provider=fact.provider,
|
|
@@ -419,9 +592,47 @@ def _job_from_roster_worker(
|
|
|
419
592
|
role=fact.role,
|
|
420
593
|
idle_timeout_seconds=options.idle_timeout_seconds,
|
|
421
594
|
dispatch_kind=options.dispatch_kind,
|
|
595
|
+
**invocation,
|
|
422
596
|
)
|
|
423
597
|
|
|
424
598
|
|
|
599
|
+
def _agent_metadata(prompt_path: Path) -> Mapping[str, Any] | None:
|
|
600
|
+
metadata_path = prompt_path.with_name(prompt_path.name + ".meta.json")
|
|
601
|
+
if not metadata_path.is_file():
|
|
602
|
+
return None
|
|
603
|
+
return _load_json_object(metadata_path, "agent invocation metadata")
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _invocation_job_fields(
|
|
607
|
+
metadata: Mapping[str, Any] | None,
|
|
608
|
+
prompt_path: Path,
|
|
609
|
+
) -> dict[str, Any]:
|
|
610
|
+
if metadata is None:
|
|
611
|
+
return {}
|
|
612
|
+
digests = metadata.get("digests")
|
|
613
|
+
assignment = metadata.get("modelAssignment")
|
|
614
|
+
if not isinstance(digests, Mapping) or not isinstance(assignment, Mapping):
|
|
615
|
+
raise DispatchError("agent invocation metadata is incomplete")
|
|
616
|
+
host_model_value = assignment.get("hostModelValue")
|
|
617
|
+
if host_model_value is not None and not isinstance(host_model_value, str):
|
|
618
|
+
raise DispatchError("agent invocation hostModelValue is invalid")
|
|
619
|
+
return {
|
|
620
|
+
"invocation_id": _require_string(metadata, "invocationId"),
|
|
621
|
+
"audience": _require_string(metadata, "audience"),
|
|
622
|
+
"assignment_ref": _require_string(metadata, "assignmentRef"),
|
|
623
|
+
"prompt_metadata_path": prompt_path.with_name(
|
|
624
|
+
prompt_path.name + ".meta.json"
|
|
625
|
+
),
|
|
626
|
+
"catalog_digest": _require_string(digests, "catalogDigest"),
|
|
627
|
+
"assignment_digest": _require_string(digests, "assignmentDigest"),
|
|
628
|
+
"duty_digest": _require_string(digests, "dutyDigest"),
|
|
629
|
+
"instruction_digest": _require_string(digests, "instructionDigest"),
|
|
630
|
+
"prompt_digest": _require_string(digests, "promptDigest"),
|
|
631
|
+
"host_model_value": host_model_value,
|
|
632
|
+
"enforcement_mode": "core-pre-dispatch",
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
|
|
425
636
|
def _materialize_roster_prompts(
|
|
426
637
|
project_root: Path,
|
|
427
638
|
workspace_root: Path,
|
|
@@ -510,6 +721,29 @@ def _spawn_job(plan: DispatchPlan, job: WorkerJob, attempt: int) -> WorkerHandle
|
|
|
510
721
|
return handle
|
|
511
722
|
|
|
512
723
|
|
|
724
|
+
def _spawn_cli_job_nonblocking(
|
|
725
|
+
plan: DispatchPlan, job: WorkerJob, attempt: int,
|
|
726
|
+
) -> WorkerHandle:
|
|
727
|
+
_transition_worker_status(
|
|
728
|
+
plan.team_state_path, job.worker_id, "in-progress", "",
|
|
729
|
+
model_execution_value=job.model_execution_value,
|
|
730
|
+
)
|
|
731
|
+
process: subprocess.Popen[str] | None = None
|
|
732
|
+
try:
|
|
733
|
+
process = subprocess.Popen(job.command, cwd=plan.project_root, text=True)
|
|
734
|
+
handle = WorkerHandle(
|
|
735
|
+
job, "", None, status_path_for_prompt(job.prompt_path), "", process
|
|
736
|
+
)
|
|
737
|
+
_record_dispatch(plan.team_state_path, handle, attempt, "running", "")
|
|
738
|
+
_append_event(plan, "worker-dispatched", _attempt_details(job, attempt, handle))
|
|
739
|
+
return handle
|
|
740
|
+
except (OSError, DispatchError):
|
|
741
|
+
if process is not None and process.poll() is None:
|
|
742
|
+
process.terminate()
|
|
743
|
+
process.wait()
|
|
744
|
+
raise
|
|
745
|
+
|
|
746
|
+
|
|
513
747
|
def _dispatch_job_with_retry(plan: DispatchPlan, job: WorkerJob) -> int:
|
|
514
748
|
for attempt in range(1, MAX_WORKER_ATTEMPTS + 1):
|
|
515
749
|
handle = _spawn_job(plan, job, attempt)
|
|
@@ -700,6 +934,13 @@ def _retry_from_record(
|
|
|
700
934
|
|
|
701
935
|
def _finish_attempt(plan: DispatchPlan, job: WorkerJob, attempt: int, outcome: WorkerOutcome) -> None:
|
|
702
936
|
if outcome.returncode == 0 and not outcome.missing_completion_paths and not outcome.timeout:
|
|
937
|
+
if job.invocation_id:
|
|
938
|
+
_link_agent_dispatch_result(
|
|
939
|
+
project_root=plan.project_root,
|
|
940
|
+
run_manifest_path=plan.manifest_path,
|
|
941
|
+
dispatch_id=f"{job.invocation_id}:attempt-{attempt}",
|
|
942
|
+
result_path=job.worker_result_path,
|
|
943
|
+
)
|
|
703
944
|
post_process = _post_process_report_writer_result(plan, job)
|
|
704
945
|
if not post_process["ok"]:
|
|
705
946
|
reason = _require_string(post_process, "reason")
|
|
@@ -779,18 +1020,23 @@ def _worker_terminal_status(team_state_path: Path, worker_id: str) -> str:
|
|
|
779
1020
|
def _record_dispatch(
|
|
780
1021
|
team_state_path: Path, handle: WorkerHandle, attempt: int, status: str, reason: str
|
|
781
1022
|
) -> None:
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
1023
|
+
_append_worker_dispatch(
|
|
1024
|
+
team_state_path,
|
|
1025
|
+
_dispatch_record(
|
|
1026
|
+
handle.job,
|
|
1027
|
+
attempt,
|
|
1028
|
+
status,
|
|
1029
|
+
handle.pane_id,
|
|
1030
|
+
handle.degraded_from,
|
|
1031
|
+
reason,
|
|
1032
|
+
),
|
|
1033
|
+
)
|
|
788
1034
|
|
|
789
1035
|
|
|
790
1036
|
def _dispatch_record(
|
|
791
1037
|
job: WorkerJob, attempt: int, status: str, pane_id: str, degraded_from: str, reason: str = ""
|
|
792
1038
|
) -> dict[str, Any]:
|
|
793
|
-
|
|
1039
|
+
record = {
|
|
794
1040
|
"workerId": job.worker_id,
|
|
795
1041
|
"role": job.role,
|
|
796
1042
|
"kind": job.dispatch_kind,
|
|
@@ -814,6 +1060,18 @@ def _dispatch_record(
|
|
|
814
1060
|
"completionPaths": [str(path) for path in job.completion_paths],
|
|
815
1061
|
"worktreePath": job.worktree_path,
|
|
816
1062
|
}
|
|
1063
|
+
if job.invocation_id:
|
|
1064
|
+
record.update({
|
|
1065
|
+
"dispatchId": f"{job.invocation_id}:attempt-{attempt}",
|
|
1066
|
+
"invocationId": job.invocation_id,
|
|
1067
|
+
"audience": job.audience,
|
|
1068
|
+
"assignmentRef": job.assignment_ref,
|
|
1069
|
+
"promptMetadataPath": str(job.prompt_metadata_path),
|
|
1070
|
+
**job.digests,
|
|
1071
|
+
"hostModelValue": job.host_model_value,
|
|
1072
|
+
"enforcementMode": job.enforcement_mode,
|
|
1073
|
+
})
|
|
1074
|
+
return record
|
|
817
1075
|
|
|
818
1076
|
|
|
819
1077
|
def _liveness_mode(backend: str) -> str:
|
|
@@ -824,22 +1082,14 @@ def _liveness_mode(backend: str) -> str:
|
|
|
824
1082
|
|
|
825
1083
|
def _update_dispatch_status(
|
|
826
1084
|
team_state_path: Path, job: WorkerJob, attempt: int, status: str, reason: str
|
|
827
|
-
) ->
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
return
|
|
836
|
-
# A lead may re-send a round's prompt without advancing `attempt`, which
|
|
837
|
-
# appends a second row the prompt cannot tell from the first. The newest is
|
|
838
|
-
# the one still running, so an outcome written to the older row would leave
|
|
839
|
-
# the live one running for good — the shape that wedged an await loop.
|
|
840
|
-
matches[-1]["status"] = status
|
|
841
|
-
matches[-1]["reason"] = reason
|
|
842
|
-
_write_json(team_state_path, payload)
|
|
1085
|
+
) -> bool:
|
|
1086
|
+
return _update_worker_dispatch_status(
|
|
1087
|
+
team_state_path,
|
|
1088
|
+
prompt_path=job.prompt_path,
|
|
1089
|
+
attempt=attempt,
|
|
1090
|
+
status=status,
|
|
1091
|
+
reason=reason,
|
|
1092
|
+
)
|
|
843
1093
|
|
|
844
1094
|
|
|
845
1095
|
def _teardown_marked_dispatches(team_state_path: Path) -> list[Mapping[str, Any]]:
|
|
@@ -884,15 +1134,23 @@ def _mark_roster_skips(
|
|
|
884
1134
|
if not reasons:
|
|
885
1135
|
return
|
|
886
1136
|
team_state_path = _resolve_required_path(project_root, manifest, "teamStatePath")
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1137
|
+
|
|
1138
|
+
def mark_pristine_skips(current_team_state: dict[str, Any]) -> bool:
|
|
1139
|
+
changed = False
|
|
1140
|
+
for worker_id, reason in reasons.items():
|
|
1141
|
+
worker = _worker_state(current_team_state, worker_id)
|
|
1142
|
+
if (
|
|
1143
|
+
_string_value(worker.get("status")) != "not-run"
|
|
1144
|
+
or _string_value(worker.get("reason"))
|
|
1145
|
+
):
|
|
1146
|
+
continue
|
|
1147
|
+
worker["reason"] = reason
|
|
1148
|
+
worker.pop("startedAt", None)
|
|
1149
|
+
worker.pop("endedAt", None)
|
|
1150
|
+
changed = True
|
|
1151
|
+
return changed
|
|
1152
|
+
|
|
1153
|
+
_mutate_team_state(team_state_path, mark_pristine_skips)
|
|
896
1154
|
|
|
897
1155
|
|
|
898
1156
|
def _skip_reasons(
|
|
@@ -1068,6 +1326,23 @@ def _job_from_record(project_root: Path, record: Mapping[str, Any]) -> WorkerJob
|
|
|
1068
1326
|
role=_require_string(record, "role"),
|
|
1069
1327
|
idle_timeout_seconds=600,
|
|
1070
1328
|
dispatch_kind=_require_string(record, "kind"),
|
|
1329
|
+
invocation_id=_string_value(record.get("invocationId")),
|
|
1330
|
+
audience=_string_value(record.get("audience")),
|
|
1331
|
+
assignment_ref=_string_value(record.get("assignmentRef")),
|
|
1332
|
+
prompt_metadata_path=Path(
|
|
1333
|
+
_string_value(record.get("promptMetadataPath"))
|
|
1334
|
+
),
|
|
1335
|
+
catalog_digest=_string_value(record.get("catalogDigest")),
|
|
1336
|
+
assignment_digest=_string_value(record.get("assignmentDigest")),
|
|
1337
|
+
duty_digest=_string_value(record.get("dutyDigest")),
|
|
1338
|
+
instruction_digest=_string_value(record.get("instructionDigest")),
|
|
1339
|
+
prompt_digest=_string_value(record.get("promptDigest")),
|
|
1340
|
+
host_model_value=(
|
|
1341
|
+
record.get("hostModelValue")
|
|
1342
|
+
if isinstance(record.get("hostModelValue"), str)
|
|
1343
|
+
else None
|
|
1344
|
+
),
|
|
1345
|
+
enforcement_mode=_string_value(record.get("enforcementMode")),
|
|
1071
1346
|
)
|
|
1072
1347
|
|
|
1073
1348
|
|