okstra 0.163.2 → 0.164.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 +7 -5
- package/docs/architecture.md +12 -7
- package/docs/cli.md +8 -5
- package/docs/project-structure-overview.md +7 -5
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +1 -0
- package/runtime/bin/lib/okstra/cli.sh +5 -0
- package/runtime/bin/lib/okstra/globals.sh +2 -0
- package/runtime/bin/lib/okstra/usage.sh +5 -5
- package/runtime/bin/okstra.sh +87 -91
- package/runtime/prompts/lead/adapters/cmux.md +1 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +1 -0
- package/runtime/prompts/profiles/implementation-planning.md +1 -1
- package/runtime/python/okstra_ctl/adapters/accounting/__init__.py +11 -0
- package/runtime/python/okstra_ctl/adapters/accounting/claude_jsonl.py +17 -0
- package/runtime/python/okstra_ctl/adapters/accounting/cli_artifact.py +17 -0
- package/runtime/python/okstra_ctl/adapters/accounting/unavailable.py +19 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +92 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +54 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +68 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +60 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/antigravity.md → python/okstra_ctl/adapters/hosts/antigravity/relay.md} +52 -0
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +292 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +120 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/claude-code.md → python/okstra_ctl/adapters/hosts/claude-code/relay.md} +111 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +60 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/codex.md → python/okstra_ctl/adapters/hosts/codex/relay.md} +52 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +72 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/external.md → python/okstra_ctl/adapters/hosts/external/relay.md} +52 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +63 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +90 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +63 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +90 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +35 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +55 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +43 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +32 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +38 -0
- package/runtime/python/okstra_ctl/adapters/providers/kimi/manifest.json +1 -0
- package/runtime/python/okstra_ctl/application/__init__.py +1 -0
- package/runtime/python/okstra_ctl/application/advance_wizard.py +25 -0
- package/runtime/python/okstra_ctl/application/collect_usage.py +15 -0
- package/runtime/python/okstra_ctl/application/dispatch_assignments.py +15 -0
- package/runtime/python/okstra_ctl/application/resolve_assignment.py +93 -0
- package/runtime/python/okstra_ctl/application/resume_run.py +21 -0
- package/runtime/python/okstra_ctl/application/start_run.py +21 -0
- package/runtime/python/okstra_ctl/codex_dispatch.py +49 -826
- package/runtime/python/okstra_ctl/dispatch_core.py +240 -29
- package/runtime/python/okstra_ctl/dispatch_state.py +17 -0
- package/runtime/python/okstra_ctl/domain/__init__.py +34 -0
- package/runtime/python/okstra_ctl/domain/host.py +100 -0
- package/runtime/python/okstra_ctl/domain/provider.py +66 -0
- package/runtime/python/okstra_ctl/domain/wizard/__init__.py +19 -0
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +140 -0
- package/runtime/python/okstra_ctl/entrypoints/__init__.py +1 -0
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +334 -0
- package/runtime/python/okstra_ctl/models.py +54 -269
- package/runtime/python/okstra_ctl/ports/__init__.py +15 -0
- package/runtime/python/okstra_ctl/ports/host.py +32 -0
- package/runtime/python/okstra_ctl/ports/interaction.py +15 -0
- package/runtime/python/okstra_ctl/ports/lead_session.py +25 -0
- package/runtime/python/okstra_ctl/ports/usage_accounting.py +23 -0
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +32 -0
- package/runtime/python/okstra_ctl/registry/__init__.py +13 -0
- package/runtime/python/okstra_ctl/registry/factory_loader.py +32 -0
- package/runtime/python/okstra_ctl/registry/host_discovery.py +124 -0
- package/runtime/python/okstra_ctl/registry/host_registry.py +365 -0
- package/runtime/python/okstra_ctl/registry/provider_registry.py +149 -0
- package/runtime/python/okstra_ctl/render.py +145 -47
- package/runtime/python/okstra_ctl/run.py +80 -58
- package/runtime/python/okstra_ctl/session.py +1 -1
- package/runtime/python/okstra_ctl/team.py +44 -32
- package/runtime/python/okstra_ctl/wizard.py +162 -64
- package/runtime/python/okstra_ctl/worker_audit_ledger.py +29 -4
- package/runtime/python/okstra_token_usage/collect.py +34 -6
- package/runtime/schemas/final-report-v1.0.schema.json +3989 -1085
- package/runtime/schemas/final-report-v2.0.schema.json +5622 -1451
- package/runtime/skills/okstra-run/SKILL.md +73 -28
- package/runtime/templates/implementation-worker-preamble.md +1 -1
- package/runtime/templates/reports/final-report.template.md +3 -3
- package/runtime/templates/reports/html/i18n/en.json +3 -1
- package/runtime/templates/reports/html/i18n/ko.json +3 -1
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +1 -1
- package/runtime/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/validate-implementation-plan-stages.py +17 -22
- package/runtime/validators/validate-report-views.py +0 -39
- package/runtime/validators/validate-run.py +8 -7
- package/runtime/validators/validate_session_conformance.py +7 -2
- package/src/commands/execute/render-bundle.mjs +4 -4
- package/src/commands/execute/run.mjs +8 -25
- package/src/commands/execute/wizard.mjs +33 -13
- package/src/commands/lifecycle/doctor.mjs +10 -10
- package/src/commands/lifecycle/install.mjs +53 -30
- package/src/commands/lifecycle/preflight.mjs +14 -4
- package/src/lib/host-registry-client.mjs +176 -0
- package/src/lib/runtime-manifest.mjs +6 -8
- package/runtime/python/okstra_ctl/lead_runtime.py +0 -115
- package/runtime/python/okstra_ctl/runner_resolution.py +0 -103
- package/src/lib/runtime-readiness.mjs +0 -90
- package/src/lib/runtime-resolver.mjs +0 -123
|
@@ -1,477 +1,81 @@
|
|
|
1
1
|
"""CLI-wrapper dispatcher for a Codex-hosted Okstra lead.
|
|
2
2
|
|
|
3
3
|
The prepared assignment's ``runner`` field is authoritative. Native Codex
|
|
4
|
-
assignments stay in the host session; this
|
|
5
|
-
|
|
4
|
+
assignments stay in the host session; this entrypoint executes only the backend
|
|
5
|
+
plan returned by the worker-dispatch application use case.
|
|
6
6
|
"""
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import argparse
|
|
10
10
|
import json
|
|
11
|
-
import subprocess
|
|
12
11
|
import sys
|
|
13
|
-
from dataclasses import dataclass
|
|
14
|
-
from datetime import datetime, timezone
|
|
15
12
|
from pathlib import Path
|
|
16
13
|
from typing import Any, Mapping, Sequence
|
|
17
14
|
|
|
18
|
-
from .
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
from .
|
|
23
|
-
InitialPromptMaterializationError,
|
|
24
|
-
InitialPromptMaterializationRequest,
|
|
25
|
-
InitialPromptWorkerRequest,
|
|
26
|
-
PromptDeliveryMode,
|
|
27
|
-
materialize_initial_prompts,
|
|
28
|
-
)
|
|
29
|
-
from .lead_events import LeadEvent, append_lead_event
|
|
30
|
-
from .dispatch_state import (
|
|
31
|
-
BACKEND_CLI_WRAPPER,
|
|
32
|
-
BACKEND_MIXED,
|
|
33
|
-
BACKEND_TMUX_PANE,
|
|
34
|
-
DispatchError,
|
|
35
|
-
WorkerJob,
|
|
36
|
-
dispatch_mode as _dispatch_mode,
|
|
37
|
-
load_json_object as _load_json_object,
|
|
38
|
-
missing_completion_paths as _missing_completion_paths,
|
|
39
|
-
LIVENESS_WRAPPER_STATUS,
|
|
40
|
-
require_string as _require_string,
|
|
41
|
-
resolve_project_path as _resolve_project_path,
|
|
42
|
-
resolve_required_path as _resolve_required_path,
|
|
43
|
-
set_dispatch_mode as _set_dispatch_mode,
|
|
44
|
-
string_list as _string_list,
|
|
45
|
-
string_value as _string_value,
|
|
46
|
-
transition_worker_status as _transition_worker_status,
|
|
47
|
-
utc_now as _utc_now,
|
|
48
|
-
validate_dispatch_prompts as _validate_dispatch_prompts,
|
|
49
|
-
worker_jobs_from_file as _worker_jobs_from_file,
|
|
50
|
-
worker_state as _worker_state,
|
|
51
|
-
worktree_path as _worktree_path,
|
|
52
|
-
write_json as _write_json,
|
|
53
|
-
)
|
|
54
|
-
from .path_hints import hydrate_active_run_context
|
|
55
|
-
from .report_finalize import (
|
|
56
|
-
STEP_VALIDATE_RUN,
|
|
57
|
-
FinalizeContext,
|
|
58
|
-
FinalizeError,
|
|
59
|
-
resolve_optional_path as _resolve_optional_path,
|
|
60
|
-
run_finalize,
|
|
61
|
-
run_seq as _run_seq,
|
|
62
|
-
)
|
|
63
|
-
from .wrapper_status import status_path_for_prompt
|
|
64
|
-
from .worker_prompt_body import REPORT_WRITER_WORKER_ID
|
|
15
|
+
from . import dispatch_core
|
|
16
|
+
from .adapters.dispatch import with_worker_dispatch
|
|
17
|
+
from .adapters.dispatch.cli_wrapper import CliWrapperDispatchPort
|
|
18
|
+
from .application.dispatch_assignments import dispatch_assignments
|
|
19
|
+
from .dispatch_state import DispatchError
|
|
65
20
|
from .models import provider_wrappers
|
|
21
|
+
from .ports.worker_dispatch import WorkerDispatchRequest
|
|
22
|
+
from .registry.host_registry import default_host_registry
|
|
66
23
|
|
|
67
24
|
|
|
68
25
|
SUPPORTED_CLI_WORKERS = provider_wrappers("analyser")
|
|
69
|
-
MAX_WORKER_ATTEMPTS = 2
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
@dataclass(frozen=True)
|
|
73
|
-
class DispatchPlan:
|
|
74
|
-
project_root: Path
|
|
75
|
-
workspace_root: Path
|
|
76
|
-
manifest_path: Path
|
|
77
|
-
team_state_path: Path
|
|
78
|
-
lead_events_path: Path
|
|
79
|
-
manifest: Mapping[str, Any]
|
|
80
|
-
workers: tuple[WorkerJob, ...]
|
|
81
|
-
|
|
82
|
-
def to_payload(self, *, dry_run: bool) -> dict[str, Any]:
|
|
83
|
-
return {
|
|
84
|
-
"ok": True,
|
|
85
|
-
"dryRun": dry_run,
|
|
86
|
-
"dispatchMode": _dispatch_mode(self.workers),
|
|
87
|
-
"workerBackends": {
|
|
88
|
-
worker.worker_id: worker.backend
|
|
89
|
-
for worker in self.workers
|
|
90
|
-
},
|
|
91
|
-
"runManifestPath": str(self.manifest_path),
|
|
92
|
-
"teamStatePath": str(self.team_state_path),
|
|
93
|
-
"leadEventsPath": str(self.lead_events_path),
|
|
94
|
-
"workspaceRoot": str(self.workspace_root),
|
|
95
|
-
"workers": [worker.to_payload() for worker in self.workers],
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
@dataclass(frozen=True)
|
|
100
|
-
class _DispatchInputs:
|
|
101
|
-
project_root: Path
|
|
102
|
-
manifest_path: Path
|
|
103
|
-
manifest: Mapping[str, Any]
|
|
104
|
-
selected_workers: tuple[str, ...]
|
|
105
|
-
team_state_path: Path
|
|
106
|
-
team_state: Mapping[str, Any]
|
|
107
|
-
active_context: Mapping[str, Any]
|
|
108
|
-
lead_events_path: Path
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def build_dispatch_plan(
|
|
112
|
-
*,
|
|
113
|
-
project_root: Path,
|
|
114
|
-
run_manifest_path: Path,
|
|
115
|
-
workspace_root: Path,
|
|
116
|
-
okstra_bin: Path | None = None,
|
|
117
|
-
requested_workers: Sequence[str] = (),
|
|
118
|
-
idle_timeout_seconds: int = 600,
|
|
119
|
-
dispatch_kind: str = "initial",
|
|
120
|
-
jobs_file: Path | None = None,
|
|
121
|
-
) -> DispatchPlan:
|
|
122
|
-
inputs = _load_dispatch_inputs(
|
|
123
|
-
project_root,
|
|
124
|
-
run_manifest_path,
|
|
125
|
-
requested_workers,
|
|
126
|
-
select_roster=jobs_file is None,
|
|
127
|
-
)
|
|
128
|
-
if jobs_file is not None:
|
|
129
|
-
workers = _codex_jobs_from_file(
|
|
130
|
-
inputs,
|
|
131
|
-
jobs_file,
|
|
132
|
-
workspace_root=workspace_root,
|
|
133
|
-
okstra_bin=okstra_bin,
|
|
134
|
-
idle_timeout_seconds=idle_timeout_seconds,
|
|
135
|
-
dispatch_kind=dispatch_kind,
|
|
136
|
-
)
|
|
137
|
-
else:
|
|
138
|
-
worker_requests, prompt_paths = _materialize_selected_worker_prompts(
|
|
139
|
-
project_root=inputs.project_root,
|
|
140
|
-
manifest_path=inputs.manifest_path,
|
|
141
|
-
workspace_root=workspace_root,
|
|
142
|
-
team_state=inputs.team_state,
|
|
143
|
-
selected_workers=inputs.selected_workers,
|
|
144
|
-
)
|
|
145
|
-
workers = _build_selected_worker_dispatches(
|
|
146
|
-
project_root=inputs.project_root,
|
|
147
|
-
manifest=inputs.manifest,
|
|
148
|
-
team_state=inputs.team_state,
|
|
149
|
-
active_context=inputs.active_context,
|
|
150
|
-
workspace_root=workspace_root,
|
|
151
|
-
okstra_bin=okstra_bin,
|
|
152
|
-
idle_timeout_seconds=idle_timeout_seconds,
|
|
153
|
-
selected_workers=inputs.selected_workers,
|
|
154
|
-
worker_requests=worker_requests,
|
|
155
|
-
prompt_paths=prompt_paths,
|
|
156
|
-
)
|
|
157
|
-
return DispatchPlan(
|
|
158
|
-
project_root=inputs.project_root,
|
|
159
|
-
workspace_root=workspace_root,
|
|
160
|
-
manifest_path=inputs.manifest_path,
|
|
161
|
-
team_state_path=inputs.team_state_path,
|
|
162
|
-
lead_events_path=inputs.lead_events_path,
|
|
163
|
-
manifest=inputs.manifest,
|
|
164
|
-
workers=workers,
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
def _codex_jobs_from_file(
|
|
169
|
-
inputs: _DispatchInputs,
|
|
170
|
-
jobs_file: Path,
|
|
171
|
-
*,
|
|
172
|
-
workspace_root: Path,
|
|
173
|
-
okstra_bin: Path | None,
|
|
174
|
-
idle_timeout_seconds: int,
|
|
175
|
-
dispatch_kind: str,
|
|
176
|
-
) -> list[WorkerJob]:
|
|
177
|
-
workers = _worker_jobs_from_file(
|
|
178
|
-
inputs.project_root,
|
|
179
|
-
jobs_file,
|
|
180
|
-
backend=BACKEND_CLI_WRAPPER,
|
|
181
|
-
idle_timeout_seconds=idle_timeout_seconds,
|
|
182
|
-
default_dispatch_kind=dispatch_kind,
|
|
183
|
-
resolve_wrapper=lambda provider: _resolve_wrapper(
|
|
184
|
-
provider, workspace_root, okstra_bin
|
|
185
|
-
),
|
|
186
|
-
default_provider=lambda worker_id: (
|
|
187
|
-
"codex" if worker_id == REPORT_WRITER_WORKER_ID else worker_id
|
|
188
|
-
),
|
|
189
|
-
)
|
|
190
|
-
_validate_dispatch_prompts(inputs.manifest, inputs.active_context, workers)
|
|
191
|
-
return workers
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
def _load_dispatch_inputs(
|
|
195
|
-
project_root: Path,
|
|
196
|
-
run_manifest_path: Path,
|
|
197
|
-
requested_workers: Sequence[str],
|
|
198
|
-
*,
|
|
199
|
-
select_roster: bool = True,
|
|
200
|
-
) -> _DispatchInputs:
|
|
201
|
-
project_root = project_root.resolve()
|
|
202
|
-
manifest_path = _resolve_project_path(project_root, str(run_manifest_path))
|
|
203
|
-
manifest = _load_json_object(manifest_path, "run manifest")
|
|
204
|
-
_validate_codex_manifest(manifest, manifest_path)
|
|
205
|
-
team_state_path = _resolve_required_path(project_root, manifest, "teamStatePath")
|
|
206
|
-
team_state = _load_json_object(team_state_path, "team-state")
|
|
207
|
-
selected_workers = (
|
|
208
|
-
tuple(
|
|
209
|
-
_select_workers(
|
|
210
|
-
manifest,
|
|
211
|
-
team_state,
|
|
212
|
-
requested_workers,
|
|
213
|
-
)
|
|
214
|
-
)
|
|
215
|
-
if select_roster
|
|
216
|
-
else ()
|
|
217
|
-
)
|
|
218
|
-
if select_roster:
|
|
219
|
-
_mark_skipped_workers(
|
|
220
|
-
team_state_path,
|
|
221
|
-
team_state,
|
|
222
|
-
_skipped_worker_reasons(
|
|
223
|
-
manifest,
|
|
224
|
-
team_state,
|
|
225
|
-
selected_workers,
|
|
226
|
-
requested_workers,
|
|
227
|
-
),
|
|
228
|
-
)
|
|
229
|
-
active_context = _load_optional_json_object(
|
|
230
|
-
_resolve_optional_path(project_root, manifest.get("activeRunContextPath"))
|
|
231
|
-
)
|
|
232
|
-
return _DispatchInputs(
|
|
233
|
-
project_root=project_root,
|
|
234
|
-
manifest_path=manifest_path,
|
|
235
|
-
manifest=manifest,
|
|
236
|
-
selected_workers=selected_workers,
|
|
237
|
-
team_state_path=team_state_path,
|
|
238
|
-
team_state=team_state,
|
|
239
|
-
active_context=active_context,
|
|
240
|
-
lead_events_path=_resolve_required_path(project_root, manifest, "leadEventsPath"),
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
def _materialize_selected_worker_prompts(
|
|
245
|
-
*,
|
|
246
|
-
project_root: Path,
|
|
247
|
-
manifest_path: Path,
|
|
248
|
-
workspace_root: Path,
|
|
249
|
-
team_state: Mapping[str, Any],
|
|
250
|
-
selected_workers: Sequence[str],
|
|
251
|
-
) -> tuple[tuple[InitialPromptWorkerRequest, ...], tuple[Path, ...]]:
|
|
252
|
-
worker_requests = tuple(
|
|
253
|
-
InitialPromptWorkerRequest(
|
|
254
|
-
worker_id=worker_id,
|
|
255
|
-
model=_requested_worker_model(worker_id, team_state),
|
|
256
|
-
)
|
|
257
|
-
for worker_id in selected_workers
|
|
258
|
-
)
|
|
259
|
-
try:
|
|
260
|
-
prompt_paths = materialize_initial_prompts(
|
|
261
|
-
InitialPromptMaterializationRequest(
|
|
262
|
-
project_root=project_root,
|
|
263
|
-
run_manifest_path=manifest_path,
|
|
264
|
-
runtime_root=workspace_root,
|
|
265
|
-
delivery_mode=PromptDeliveryMode.EAGER_INCLUDE,
|
|
266
|
-
workers=worker_requests,
|
|
267
|
-
)
|
|
268
|
-
)
|
|
269
|
-
except InitialPromptMaterializationError as exc:
|
|
270
|
-
raise DispatchError(f"{exc.reason}: {exc}") from exc
|
|
271
|
-
return worker_requests, prompt_paths
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
def _build_selected_worker_dispatches(
|
|
275
|
-
*,
|
|
276
|
-
project_root: Path,
|
|
277
|
-
manifest: Mapping[str, Any],
|
|
278
|
-
team_state: Mapping[str, Any],
|
|
279
|
-
active_context: Mapping[str, Any],
|
|
280
|
-
workspace_root: Path,
|
|
281
|
-
okstra_bin: Path | None,
|
|
282
|
-
idle_timeout_seconds: int,
|
|
283
|
-
selected_workers: Sequence[str],
|
|
284
|
-
worker_requests: Sequence[InitialPromptWorkerRequest],
|
|
285
|
-
prompt_paths: Mapping[str, Path],
|
|
286
|
-
) -> tuple[WorkerJob, ...]:
|
|
287
|
-
return tuple(
|
|
288
|
-
_build_worker_dispatch(
|
|
289
|
-
worker_id=worker_id,
|
|
290
|
-
project_root=project_root,
|
|
291
|
-
manifest=manifest,
|
|
292
|
-
team_state=team_state,
|
|
293
|
-
active_context=active_context,
|
|
294
|
-
workspace_root=workspace_root,
|
|
295
|
-
okstra_bin=okstra_bin,
|
|
296
|
-
idle_timeout_seconds=idle_timeout_seconds,
|
|
297
|
-
model_execution_value=worker_request.model,
|
|
298
|
-
prompt_path=prompt_path,
|
|
299
|
-
)
|
|
300
|
-
for worker_id, worker_request, prompt_path in (
|
|
301
|
-
(worker_id, worker_request, prompt_paths[worker_id])
|
|
302
|
-
for worker_id, worker_request in zip(
|
|
303
|
-
selected_workers, worker_requests, strict=True
|
|
304
|
-
)
|
|
305
|
-
)
|
|
306
|
-
)
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
def dispatch_plan(plan: DispatchPlan) -> int:
|
|
310
|
-
_set_dispatch_mode(plan.team_state_path, _dispatch_mode(plan.workers))
|
|
311
|
-
for worker in plan.workers:
|
|
312
|
-
completed = _dispatch_worker_with_retry(plan, worker)
|
|
313
|
-
if completed == 0:
|
|
314
|
-
continue
|
|
315
|
-
return completed
|
|
316
|
-
return 0
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
def _dispatch_worker_with_retry(plan: DispatchPlan, worker: WorkerJob) -> int:
|
|
320
|
-
for attempt in range(1, MAX_WORKER_ATTEMPTS + 1):
|
|
321
|
-
_transition_worker_status(
|
|
322
|
-
plan.team_state_path,
|
|
323
|
-
worker.worker_id,
|
|
324
|
-
"in-progress",
|
|
325
|
-
"",
|
|
326
|
-
model_execution_value=worker.model_execution_value,
|
|
327
|
-
)
|
|
328
|
-
_record_worker_attempt(plan, worker, attempt, "running")
|
|
329
|
-
attempt_details = _attempt_event_details(worker, attempt)
|
|
330
|
-
_append_event(plan, "worker-dispatched", attempt_details)
|
|
331
|
-
result = _run_worker(plan, worker)
|
|
332
|
-
missing_paths = _missing_completion_paths(worker)
|
|
333
|
-
if result.returncode == 0 and not missing_paths:
|
|
334
|
-
post_process = _post_process_report_writer_result(plan, worker)
|
|
335
|
-
if not post_process["ok"]:
|
|
336
|
-
reason = _require_string(post_process, "reason")
|
|
337
|
-
_transition_worker_status(
|
|
338
|
-
plan.team_state_path, worker.worker_id, "error", reason
|
|
339
|
-
)
|
|
340
|
-
_record_worker_attempt(plan, worker, attempt, "error", reason)
|
|
341
|
-
details = {
|
|
342
|
-
**attempt_details,
|
|
343
|
-
"exitCode": 1,
|
|
344
|
-
"missingCompletionPaths": [],
|
|
345
|
-
"reason": reason,
|
|
346
|
-
"postProcessing": post_process["steps"],
|
|
347
|
-
}
|
|
348
|
-
_append_event(plan, "worker-failed", details)
|
|
349
|
-
return 1
|
|
350
|
-
_transition_worker_status(
|
|
351
|
-
plan.team_state_path, worker.worker_id, "completed", ""
|
|
352
|
-
)
|
|
353
|
-
_record_worker_attempt(plan, worker, attempt, "completed")
|
|
354
|
-
details = {
|
|
355
|
-
**attempt_details,
|
|
356
|
-
"missingCompletionPaths": [],
|
|
357
|
-
"postProcessing": post_process["steps"],
|
|
358
|
-
}
|
|
359
|
-
_append_event(plan, "worker-result-collected", details)
|
|
360
|
-
return 0
|
|
361
|
-
if _should_retry_missing_result(result, missing_paths, attempt):
|
|
362
|
-
retry_details = {
|
|
363
|
-
**attempt_details,
|
|
364
|
-
"missingCompletionPaths": [str(path) for path in missing_paths],
|
|
365
|
-
"reason": "required worker artifact was not produced",
|
|
366
|
-
}
|
|
367
|
-
_record_worker_attempt(
|
|
368
|
-
plan,
|
|
369
|
-
worker,
|
|
370
|
-
attempt,
|
|
371
|
-
"error",
|
|
372
|
-
"required worker artifact was not produced",
|
|
373
|
-
)
|
|
374
|
-
_append_event(plan, "worker-retry-scheduled", retry_details)
|
|
375
|
-
continue
|
|
376
|
-
|
|
377
|
-
reason = _failure_reason(result.returncode, missing_paths)
|
|
378
|
-
_transition_worker_status(
|
|
379
|
-
plan.team_state_path, worker.worker_id, "error", reason
|
|
380
|
-
)
|
|
381
|
-
_record_worker_attempt(plan, worker, attempt, "error", reason)
|
|
382
|
-
details = {
|
|
383
|
-
**attempt_details,
|
|
384
|
-
"exitCode": result.returncode,
|
|
385
|
-
"missingCompletionPaths": [str(path) for path in missing_paths],
|
|
386
|
-
"reason": reason,
|
|
387
|
-
}
|
|
388
|
-
_append_event(plan, "worker-failed", details)
|
|
389
|
-
return result.returncode if result.returncode else 1
|
|
390
|
-
return 1
|
|
391
|
-
def _record_worker_attempt(
|
|
392
|
-
plan: DispatchPlan,
|
|
393
|
-
worker: WorkerJob,
|
|
394
|
-
attempt: int,
|
|
395
|
-
status: str,
|
|
396
|
-
reason: str = "",
|
|
397
|
-
) -> None:
|
|
398
|
-
payload = _load_json_object(plan.team_state_path, "team-state")
|
|
399
|
-
dispatches = payload.setdefault("workerDispatches", [])
|
|
400
|
-
if not isinstance(dispatches, list):
|
|
401
|
-
raise DispatchError(
|
|
402
|
-
f"team-state workerDispatches must be an array: {plan.team_state_path}"
|
|
403
|
-
)
|
|
404
|
-
for record in dispatches:
|
|
405
|
-
if _matches_worker_attempt(record, worker, attempt):
|
|
406
|
-
record["status"] = status
|
|
407
|
-
record["reason"] = reason
|
|
408
|
-
_write_json(plan.team_state_path, payload)
|
|
409
|
-
return
|
|
410
|
-
dispatches.append(_worker_dispatch_record(worker, attempt, status, reason))
|
|
411
|
-
_write_json(plan.team_state_path, payload)
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
def _matches_worker_attempt(
|
|
415
|
-
record: object, worker: WorkerJob, attempt: int
|
|
416
|
-
) -> bool:
|
|
417
|
-
return (
|
|
418
|
-
isinstance(record, dict)
|
|
419
|
-
and record.get("workerId") == worker.worker_id
|
|
420
|
-
and record.get("kind") == worker.dispatch_kind
|
|
421
|
-
and record.get("attempt") == attempt
|
|
422
|
-
)
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
def _worker_dispatch_record(
|
|
426
|
-
worker: WorkerJob, attempt: int, status: str, reason: str
|
|
427
|
-
) -> dict[str, Any]:
|
|
428
|
-
return {
|
|
429
|
-
"workerId": worker.worker_id,
|
|
430
|
-
"role": worker.role,
|
|
431
|
-
"kind": worker.dispatch_kind,
|
|
432
|
-
"attempt": attempt,
|
|
433
|
-
"backendType": worker.backend,
|
|
434
|
-
"provider": worker.provider,
|
|
435
|
-
"status": status,
|
|
436
|
-
"paneId": "",
|
|
437
|
-
"promptPath": str(worker.prompt_path),
|
|
438
|
-
"resultPath": str(worker.result_path),
|
|
439
|
-
"workerResultPath": str(worker.worker_result_path),
|
|
440
|
-
"livenessMode": LIVENESS_WRAPPER_STATUS,
|
|
441
|
-
"statusSidecarPath": str(status_path_for_prompt(worker.prompt_path)),
|
|
442
|
-
"degradedFrom": "",
|
|
443
|
-
"reason": reason,
|
|
444
|
-
}
|
|
445
26
|
|
|
446
27
|
|
|
447
28
|
def main(argv: Sequence[str] | None = None) -> int:
|
|
448
|
-
|
|
449
|
-
args = parser.parse_args(argv)
|
|
29
|
+
args = _parser().parse_args(argv)
|
|
450
30
|
try:
|
|
451
31
|
if args.jobs_file and args.workers:
|
|
452
32
|
raise DispatchError("--jobs-file and --workers cannot be combined")
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
33
|
+
request = _dispatch_request(args)
|
|
34
|
+
port = CliWrapperDispatchPort(
|
|
35
|
+
supported_worker_wrappers=SUPPORTED_CLI_WORKERS,
|
|
36
|
+
unsupported_worker_label="Codex lead",
|
|
37
|
+
required_lead_runtime="codex",
|
|
38
|
+
cli_wrapper_assignments_only=True,
|
|
39
|
+
default_provider_by_worker_id={"report-writer": "codex"},
|
|
40
|
+
)
|
|
41
|
+
host_adapter = with_worker_dispatch(
|
|
42
|
+
default_host_registry().resolve("codex"),
|
|
43
|
+
port,
|
|
44
|
+
)
|
|
45
|
+
plan = dispatch_assignments(request, host_adapter.worker_dispatch())
|
|
46
|
+
backend_plan = plan.backend_plan
|
|
47
|
+
if backend_plan is None:
|
|
48
|
+
raise DispatchError("CLI wrapper dispatch did not produce a backend plan")
|
|
464
49
|
if args.dry_run:
|
|
465
|
-
_print_json(
|
|
50
|
+
_print_json(_backend_payload(backend_plan, dry_run=True))
|
|
466
51
|
return 0
|
|
467
|
-
code = dispatch_plan(
|
|
468
|
-
_print_json({**
|
|
52
|
+
code = dispatch_core.dispatch_plan(backend_plan)
|
|
53
|
+
_print_json({**_backend_payload(backend_plan, dry_run=False), "exitCode": code})
|
|
469
54
|
return code
|
|
470
|
-
except
|
|
55
|
+
except DispatchError as exc:
|
|
471
56
|
print(f"error: {exc}", file=sys.stderr)
|
|
472
57
|
return 2
|
|
473
58
|
|
|
474
59
|
|
|
60
|
+
def _dispatch_request(args: argparse.Namespace) -> WorkerDispatchRequest:
|
|
61
|
+
return WorkerDispatchRequest(
|
|
62
|
+
project_root=Path(args.project_root),
|
|
63
|
+
run_manifest=Path(args.run_manifest),
|
|
64
|
+
workspace_root=Path(args.workspace_root),
|
|
65
|
+
okstra_bin=Path(args.okstra_bin) if args.okstra_bin else None,
|
|
66
|
+
requested_workers=tuple(_parse_workers(args.workers)),
|
|
67
|
+
idle_timeout_seconds=_parse_idle_timeout(args.idle_timeout_seconds),
|
|
68
|
+
dispatch_kind=args.dispatch_kind,
|
|
69
|
+
jobs_file=Path(args.jobs_file) if args.jobs_file else None,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _backend_payload(backend_plan, *, dry_run: bool) -> dict[str, Any]:
|
|
74
|
+
payload = backend_plan.to_payload(dry_run=dry_run)
|
|
75
|
+
payload.pop("workerDispatches", None)
|
|
76
|
+
return payload
|
|
77
|
+
|
|
78
|
+
|
|
475
79
|
def _parser() -> argparse.ArgumentParser:
|
|
476
80
|
parser = argparse.ArgumentParser(
|
|
477
81
|
prog="okstra codex-dispatch",
|
|
@@ -489,387 +93,6 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
489
93
|
return parser
|
|
490
94
|
|
|
491
95
|
|
|
492
|
-
def _build_worker_dispatch(
|
|
493
|
-
*,
|
|
494
|
-
worker_id: str,
|
|
495
|
-
project_root: Path,
|
|
496
|
-
manifest: Mapping[str, Any],
|
|
497
|
-
team_state: Mapping[str, Any],
|
|
498
|
-
active_context: Mapping[str, Any],
|
|
499
|
-
workspace_root: Path,
|
|
500
|
-
okstra_bin: Path | None,
|
|
501
|
-
idle_timeout_seconds: int,
|
|
502
|
-
model_execution_value: str,
|
|
503
|
-
prompt_path: Path,
|
|
504
|
-
) -> WorkerJob:
|
|
505
|
-
worker_state = _worker_state(team_state, worker_id)
|
|
506
|
-
provider = _assigned_provider(worker_state, worker_id)
|
|
507
|
-
if worker_id == REPORT_WRITER_WORKER_ID:
|
|
508
|
-
return _build_report_writer_dispatch(
|
|
509
|
-
project_root=project_root,
|
|
510
|
-
manifest=manifest,
|
|
511
|
-
team_state=team_state,
|
|
512
|
-
workspace_root=workspace_root,
|
|
513
|
-
okstra_bin=okstra_bin,
|
|
514
|
-
idle_timeout_seconds=idle_timeout_seconds,
|
|
515
|
-
model_execution_value=model_execution_value,
|
|
516
|
-
prompt_path=prompt_path,
|
|
517
|
-
provider=provider,
|
|
518
|
-
)
|
|
519
|
-
result_path = _resolve_project_path(
|
|
520
|
-
project_root, _require_string(worker_state, "resultPath")
|
|
521
|
-
)
|
|
522
|
-
wrapper = _resolve_wrapper(provider, workspace_root, okstra_bin)
|
|
523
|
-
worktree_path = _worktree_path(manifest, active_context)
|
|
524
|
-
role = _pane_role(prompt_path)
|
|
525
|
-
return WorkerJob(
|
|
526
|
-
worker_id=worker_id,
|
|
527
|
-
provider=provider,
|
|
528
|
-
backend=BACKEND_CLI_WRAPPER,
|
|
529
|
-
project_root=project_root,
|
|
530
|
-
model_execution_value=model_execution_value,
|
|
531
|
-
wrapper_path=wrapper,
|
|
532
|
-
prompt_path=prompt_path,
|
|
533
|
-
result_path=result_path,
|
|
534
|
-
worker_result_path=result_path,
|
|
535
|
-
completion_paths=(result_path,),
|
|
536
|
-
worktree_path=worktree_path,
|
|
537
|
-
role=role,
|
|
538
|
-
idle_timeout_seconds=idle_timeout_seconds,
|
|
539
|
-
dispatch_kind="initial",
|
|
540
|
-
)
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
def _build_report_writer_dispatch(
|
|
544
|
-
*,
|
|
545
|
-
project_root: Path,
|
|
546
|
-
manifest: Mapping[str, Any],
|
|
547
|
-
team_state: Mapping[str, Any],
|
|
548
|
-
workspace_root: Path,
|
|
549
|
-
okstra_bin: Path | None,
|
|
550
|
-
idle_timeout_seconds: int,
|
|
551
|
-
model_execution_value: str,
|
|
552
|
-
prompt_path: Path,
|
|
553
|
-
provider: str,
|
|
554
|
-
) -> WorkerJob:
|
|
555
|
-
worker_state = _worker_state(team_state, REPORT_WRITER_WORKER_ID)
|
|
556
|
-
data_json_path = _final_report_data_path(
|
|
557
|
-
_resolve_required_path(project_root, manifest, "expectedReportPath")
|
|
558
|
-
)
|
|
559
|
-
markdown_path = _final_report_markdown_path(data_json_path)
|
|
560
|
-
worker_result_path = _resolve_project_path(
|
|
561
|
-
project_root, _require_string(worker_state, "resultPath")
|
|
562
|
-
)
|
|
563
|
-
return WorkerJob(
|
|
564
|
-
worker_id=REPORT_WRITER_WORKER_ID,
|
|
565
|
-
provider=provider,
|
|
566
|
-
backend=BACKEND_CLI_WRAPPER,
|
|
567
|
-
project_root=project_root,
|
|
568
|
-
model_execution_value=model_execution_value,
|
|
569
|
-
wrapper_path=_resolve_wrapper(provider, workspace_root, okstra_bin),
|
|
570
|
-
prompt_path=prompt_path,
|
|
571
|
-
result_path=data_json_path,
|
|
572
|
-
worker_result_path=worker_result_path,
|
|
573
|
-
completion_paths=(
|
|
574
|
-
data_json_path,
|
|
575
|
-
markdown_path,
|
|
576
|
-
worker_result_path,
|
|
577
|
-
),
|
|
578
|
-
worktree_path="",
|
|
579
|
-
role=REPORT_WRITER_WORKER_ID,
|
|
580
|
-
idle_timeout_seconds=idle_timeout_seconds,
|
|
581
|
-
dispatch_kind="initial",
|
|
582
|
-
)
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
def _validate_codex_manifest(manifest: Mapping[str, Any], path: Path) -> None:
|
|
586
|
-
if manifest.get("leadRuntime") != "codex":
|
|
587
|
-
raise DispatchError(f"run manifest is not a Codex lead run: {path}")
|
|
588
|
-
if not isinstance(manifest.get("leadEventsPath"), str):
|
|
589
|
-
raise DispatchError(f"run manifest has no leadEventsPath: {path}")
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
def _requested_worker_model(
|
|
593
|
-
worker_id: str,
|
|
594
|
-
team_state: Mapping[str, Any],
|
|
595
|
-
) -> str:
|
|
596
|
-
state = _worker_state(team_state, worker_id)
|
|
597
|
-
return _require_string(state, "modelExecutionValue")
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
def _select_workers(
|
|
601
|
-
manifest: Mapping[str, Any],
|
|
602
|
-
team_state: Mapping[str, Any],
|
|
603
|
-
requested_workers: Sequence[str],
|
|
604
|
-
) -> list[str]:
|
|
605
|
-
recommended = _string_list(manifest.get("recommendedWorkers"))
|
|
606
|
-
if not recommended:
|
|
607
|
-
raise DispatchError("run manifest has no recommendedWorkers entries")
|
|
608
|
-
dispatchable = _dispatchable_workers(recommended, team_state)
|
|
609
|
-
if not requested_workers:
|
|
610
|
-
selected = [worker for worker in recommended if worker in dispatchable]
|
|
611
|
-
if selected:
|
|
612
|
-
return selected
|
|
613
|
-
raise DispatchError(
|
|
614
|
-
"run roster has no cli-wrapper assignments for codex-dispatch"
|
|
615
|
-
)
|
|
616
|
-
|
|
617
|
-
selected = list(requested_workers)
|
|
618
|
-
unknown = [worker for worker in selected if worker not in recommended]
|
|
619
|
-
if unknown:
|
|
620
|
-
raise DispatchError(
|
|
621
|
-
"requested worker(s) are not in this run roster: " + ", ".join(unknown)
|
|
622
|
-
)
|
|
623
|
-
native = [
|
|
624
|
-
worker for worker in selected
|
|
625
|
-
if _assigned_runner(_worker_state(team_state, worker)) == "native-session"
|
|
626
|
-
]
|
|
627
|
-
if native:
|
|
628
|
-
raise DispatchError(
|
|
629
|
-
"requested worker(s) use runner=native-session and must be dispatched "
|
|
630
|
-
"by the current Codex host: " + ", ".join(native)
|
|
631
|
-
)
|
|
632
|
-
unsupported = [worker for worker in selected if worker not in dispatchable]
|
|
633
|
-
if unsupported:
|
|
634
|
-
supported = ", ".join(sorted(dispatchable))
|
|
635
|
-
raise DispatchError(
|
|
636
|
-
"unsupported Codex lead worker(s): "
|
|
637
|
-
+ ", ".join(unsupported)
|
|
638
|
-
+ f" (CLI dispatcher supports: {supported})"
|
|
639
|
-
)
|
|
640
|
-
return selected
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
def _assigned_provider(worker_state: Mapping[str, Any], worker_id: str) -> str:
|
|
644
|
-
provider = _string_value(worker_state.get("provider"))
|
|
645
|
-
if provider:
|
|
646
|
-
return provider
|
|
647
|
-
return "codex" if worker_id == REPORT_WRITER_WORKER_ID else worker_id
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
def _assigned_runner(worker_state: Mapping[str, Any]) -> str:
|
|
651
|
-
return _string_value(worker_state.get("runner")) or BACKEND_CLI_WRAPPER
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
def _dispatchable_workers(
|
|
655
|
-
recommended: Sequence[str], team_state: Mapping[str, Any],
|
|
656
|
-
) -> set[str]:
|
|
657
|
-
dispatchable = set()
|
|
658
|
-
for worker_id in recommended:
|
|
659
|
-
state = _worker_state(team_state, worker_id)
|
|
660
|
-
provider = _assigned_provider(state, worker_id)
|
|
661
|
-
if (
|
|
662
|
-
_assigned_runner(state) == BACKEND_CLI_WRAPPER
|
|
663
|
-
and provider in SUPPORTED_CLI_WORKERS
|
|
664
|
-
):
|
|
665
|
-
dispatchable.add(worker_id)
|
|
666
|
-
return dispatchable
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
def _skipped_worker_reasons(
|
|
670
|
-
manifest: Mapping[str, Any],
|
|
671
|
-
team_state: Mapping[str, Any],
|
|
672
|
-
selected_workers: Sequence[str],
|
|
673
|
-
requested_workers: Sequence[str],
|
|
674
|
-
) -> dict[str, str]:
|
|
675
|
-
recommended = _string_list(manifest.get("recommendedWorkers"))
|
|
676
|
-
selected = set(selected_workers)
|
|
677
|
-
if requested_workers:
|
|
678
|
-
return {
|
|
679
|
-
worker_id: "skipped by Codex dispatch: worker was not requested in this invocation"
|
|
680
|
-
for worker_id in recommended
|
|
681
|
-
if worker_id not in selected
|
|
682
|
-
and _assigned_runner(_worker_state(team_state, worker_id))
|
|
683
|
-
== BACKEND_CLI_WRAPPER
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
supported = _dispatchable_workers(recommended, team_state)
|
|
687
|
-
reasons = {}
|
|
688
|
-
for worker_id in recommended:
|
|
689
|
-
if worker_id in selected:
|
|
690
|
-
continue
|
|
691
|
-
state = _worker_state(team_state, worker_id)
|
|
692
|
-
if _assigned_runner(state) == "native-session":
|
|
693
|
-
continue
|
|
694
|
-
if worker_id not in supported:
|
|
695
|
-
reasons[worker_id] = (
|
|
696
|
-
"skipped by Codex dispatch default: assigned provider has no "
|
|
697
|
-
"registered CLI wrapper"
|
|
698
|
-
)
|
|
699
|
-
else:
|
|
700
|
-
reasons[worker_id] = "skipped by Codex dispatch default"
|
|
701
|
-
return reasons
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
def _mark_skipped_workers(
|
|
705
|
-
team_state_path: Path,
|
|
706
|
-
team_state: Mapping[str, Any],
|
|
707
|
-
reasons_by_worker_id: Mapping[str, str],
|
|
708
|
-
) -> None:
|
|
709
|
-
if not reasons_by_worker_id:
|
|
710
|
-
return
|
|
711
|
-
workers = team_state.get("workers")
|
|
712
|
-
if not isinstance(workers, list):
|
|
713
|
-
raise DispatchError(f"team-state workers must be an array: {team_state_path}")
|
|
714
|
-
for worker in workers:
|
|
715
|
-
if not isinstance(worker, dict):
|
|
716
|
-
continue
|
|
717
|
-
worker_id = _string_value(worker.get("workerId"))
|
|
718
|
-
reason = reasons_by_worker_id.get(worker_id)
|
|
719
|
-
if not reason:
|
|
720
|
-
continue
|
|
721
|
-
status = _string_value(worker.get("status")) or "not-run"
|
|
722
|
-
current_reason = _string_value(worker.get("reason"))
|
|
723
|
-
if status == "not-run" and not current_reason:
|
|
724
|
-
_transition_worker_status(
|
|
725
|
-
team_state_path, worker_id, "not-run", reason
|
|
726
|
-
)
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
def _resolve_wrapper(
|
|
730
|
-
worker_id: str,
|
|
731
|
-
workspace_root: Path,
|
|
732
|
-
okstra_bin: Path | None,
|
|
733
|
-
) -> Path:
|
|
734
|
-
script = SUPPORTED_CLI_WORKERS.get(worker_id)
|
|
735
|
-
if script is None:
|
|
736
|
-
raise DispatchError(f"unsupported worker provider: {worker_id}")
|
|
737
|
-
candidates = []
|
|
738
|
-
if okstra_bin is not None:
|
|
739
|
-
candidates.append(okstra_bin / script)
|
|
740
|
-
candidates.extend([
|
|
741
|
-
workspace_root / "bin" / script,
|
|
742
|
-
workspace_root / "scripts" / script,
|
|
743
|
-
])
|
|
744
|
-
for candidate in candidates:
|
|
745
|
-
if candidate.is_file():
|
|
746
|
-
return candidate.resolve()
|
|
747
|
-
searched = ", ".join(str(candidate) for candidate in candidates)
|
|
748
|
-
raise DispatchError(f"{script} not found (searched: {searched})")
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
def _run_worker(
|
|
752
|
-
plan: DispatchPlan,
|
|
753
|
-
worker: WorkerJob,
|
|
754
|
-
) -> subprocess.CompletedProcess[str]:
|
|
755
|
-
if worker.backend == BACKEND_CLI_WRAPPER:
|
|
756
|
-
return _run_cli_wrapper_worker(plan, worker)
|
|
757
|
-
raise DispatchError(f"unsupported Codex worker backend: {worker.backend}")
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
def _run_cli_wrapper_worker(
|
|
761
|
-
plan: DispatchPlan,
|
|
762
|
-
worker: WorkerJob,
|
|
763
|
-
) -> subprocess.CompletedProcess[str]:
|
|
764
|
-
return subprocess.run(worker.command, cwd=plan.project_root, text=True)
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
def _post_process_report_writer_result(
|
|
768
|
-
plan: DispatchPlan,
|
|
769
|
-
worker: WorkerJob,
|
|
770
|
-
) -> dict[str, Any]:
|
|
771
|
-
if worker.worker_id != REPORT_WRITER_WORKER_ID:
|
|
772
|
-
return {"ok": True, "reason": "", "steps": []}
|
|
773
|
-
try:
|
|
774
|
-
ctx = FinalizeContext.from_manifest(
|
|
775
|
-
project_root=plan.project_root,
|
|
776
|
-
workspace_root=plan.workspace_root,
|
|
777
|
-
manifest_path=plan.manifest_path,
|
|
778
|
-
team_state_path=plan.team_state_path,
|
|
779
|
-
data_path=worker.result_path,
|
|
780
|
-
manifest=plan.manifest,
|
|
781
|
-
)
|
|
782
|
-
except FinalizeError as exc:
|
|
783
|
-
return {"ok": False, "reason": str(exc), "steps": []}
|
|
784
|
-
|
|
785
|
-
def settle_before(step_name: str) -> None:
|
|
786
|
-
# validate-run reads team-state; the writer is done by the time its
|
|
787
|
-
# artifacts pass the earlier steps.
|
|
788
|
-
if step_name == STEP_VALIDATE_RUN:
|
|
789
|
-
_transition_worker_status(
|
|
790
|
-
plan.team_state_path, worker.worker_id, "completed", ""
|
|
791
|
-
)
|
|
792
|
-
|
|
793
|
-
return run_finalize(ctx, before_step=settle_before)
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
def _append_event(
|
|
797
|
-
plan: DispatchPlan,
|
|
798
|
-
event_type: str,
|
|
799
|
-
details: Mapping[str, Any],
|
|
800
|
-
) -> None:
|
|
801
|
-
append_lead_event(
|
|
802
|
-
plan.lead_events_path,
|
|
803
|
-
LeadEvent(
|
|
804
|
-
event_type=event_type,
|
|
805
|
-
lead_runtime="codex",
|
|
806
|
-
task_key=_require_string(plan.manifest, "taskKey"),
|
|
807
|
-
task_type=_require_string(plan.manifest, "taskType"),
|
|
808
|
-
run_seq=_run_seq(plan.manifest),
|
|
809
|
-
timestamp=_utc_now(),
|
|
810
|
-
details=details,
|
|
811
|
-
),
|
|
812
|
-
)
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
def _event_details(worker: WorkerJob) -> dict[str, Any]:
|
|
816
|
-
return {
|
|
817
|
-
"dispatchMode": worker.backend,
|
|
818
|
-
"workerId": worker.worker_id,
|
|
819
|
-
"promptPath": str(worker.prompt_path),
|
|
820
|
-
"resultPath": str(worker.result_path),
|
|
821
|
-
"workerResultPath": str(worker.worker_result_path),
|
|
822
|
-
"wrapperPath": str(worker.wrapper_path),
|
|
823
|
-
"role": worker.role,
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
def _attempt_event_details(worker: WorkerJob, attempt: int) -> dict[str, Any]:
|
|
828
|
-
return {
|
|
829
|
-
**_event_details(worker),
|
|
830
|
-
"attempt": attempt,
|
|
831
|
-
"maxAttempts": MAX_WORKER_ATTEMPTS,
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
def _should_retry_missing_result(
|
|
836
|
-
result: subprocess.CompletedProcess[str],
|
|
837
|
-
missing_paths: Sequence[Path],
|
|
838
|
-
attempt: int,
|
|
839
|
-
) -> bool:
|
|
840
|
-
return (
|
|
841
|
-
result.returncode == 0
|
|
842
|
-
and bool(missing_paths)
|
|
843
|
-
and attempt < MAX_WORKER_ATTEMPTS
|
|
844
|
-
)
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
def _pane_role(prompt_path: Path) -> str:
|
|
848
|
-
if not prompt_path.is_file():
|
|
849
|
-
raise DispatchError(f"worker prompt not found: {prompt_path}")
|
|
850
|
-
marker = "**Pane role:**"
|
|
851
|
-
for line in prompt_path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
852
|
-
if line.startswith(marker):
|
|
853
|
-
role = line[len(marker):].strip()
|
|
854
|
-
return role or "worker"
|
|
855
|
-
return "worker"
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
def _load_optional_json_object(path: Path | None) -> dict[str, Any]:
|
|
859
|
-
if path is None or not path.is_file():
|
|
860
|
-
return {}
|
|
861
|
-
return hydrate_active_run_context(_load_json_object(path, "active-run-context"))
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
def _failure_reason(exit_code: int, missing_paths: Sequence[Path]) -> str:
|
|
865
|
-
if exit_code != 0:
|
|
866
|
-
return f"wrapper exited with code {exit_code}"
|
|
867
|
-
if missing_paths:
|
|
868
|
-
missing = ", ".join(str(path) for path in missing_paths)
|
|
869
|
-
return f"required worker artifact was not produced: {missing}"
|
|
870
|
-
return "worker dispatch failed"
|
|
871
|
-
|
|
872
|
-
|
|
873
96
|
def _parse_workers(raw: str) -> list[str]:
|
|
874
97
|
return [item.strip() for item in raw.split(",") if item.strip()]
|
|
875
98
|
|