okstra 0.136.0 → 0.138.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/docs/architecture/storage-model.md +1 -1
- package/docs/architecture.md +3 -2
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/project-structure-overview.md +32 -5
- package/package.json +2 -3
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/antigravity-worker.md +1 -1
- package/runtime/agents/workers/codex-worker.md +1 -1
- package/runtime/bin/lib/okstra-ctl/cmd-rerun.sh +1 -1
- package/runtime/bin/okstra-spawn-followups.py +4 -9
- package/runtime/prompts/lead/adapters/claude-code.md +1 -0
- package/runtime/prompts/lead/adapters/codex.md +1 -0
- package/runtime/prompts/lead/adapters/external.md +1 -0
- package/runtime/prompts/lead/plan-body-verification.md +2 -2
- package/runtime/prompts/lead/report-writer.md +11 -10
- package/runtime/prompts/lead/team-contract.md +4 -2
- package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
- package/runtime/prompts/profiles/_implementation-executor.md +2 -2
- package/runtime/prompts/profiles/_implementation-self-check.md +1 -1
- package/runtime/python/okstra_ctl/codex_dispatch.py +199 -681
- package/runtime/python/okstra_ctl/consumers.py +7 -5
- package/runtime/python/okstra_ctl/context_cost.py +4 -4
- package/runtime/python/okstra_ctl/dispatch_core.py +146 -287
- package/runtime/python/okstra_ctl/dispatch_state.py +231 -0
- package/runtime/python/okstra_ctl/error_report.py +2 -7
- package/runtime/python/okstra_ctl/implementation_outcome.py +12 -23
- package/runtime/python/okstra_ctl/implementation_stage.py +7 -2
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +1053 -0
- package/runtime/python/okstra_ctl/path_hints.py +3 -3
- package/runtime/python/okstra_ctl/paths.py +17 -2
- package/runtime/python/okstra_ctl/recap.py +5 -12
- package/runtime/python/okstra_ctl/render.py +18 -19
- package/runtime/python/okstra_ctl/report_finalize.py +8 -4
- package/runtime/python/okstra_ctl/report_language.py +83 -0
- package/runtime/python/okstra_ctl/run.py +262 -328
- package/runtime/python/okstra_ctl/set_work_status.py +2 -1
- package/runtime/python/okstra_ctl/stage_targets.py +330 -101
- package/runtime/python/okstra_ctl/time_report.py +4 -9
- package/runtime/python/okstra_ctl/wizard.py +47 -49
- package/runtime/python/okstra_ctl/work_categories.py +2 -2
- package/runtime/python/okstra_ctl/worker_prompt_body.py +82 -0
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +73 -13
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +7 -0
- package/runtime/python/okstra_ctl/worktree.py +37 -29
- package/runtime/python/okstra_project/__init__.py +4 -0
- package/runtime/python/okstra_project/dirs.py +7 -0
- package/runtime/python/okstra_project/state.py +78 -8
- package/runtime/validators/lib/fixtures.sh +2 -0
- package/runtime/validators/validate-run.py +3 -21
- package/src/commands/inspect/stage-map.mjs +12 -19
|
@@ -22,92 +22,53 @@ from .final_report_paths import (
|
|
|
22
22
|
final_report_data_path as _final_report_data_path,
|
|
23
23
|
final_report_markdown_path as _final_report_markdown_path,
|
|
24
24
|
)
|
|
25
|
+
from .initial_prompt_materialization import (
|
|
26
|
+
InitialPromptMaterializationError,
|
|
27
|
+
InitialPromptMaterializationRequest,
|
|
28
|
+
InitialPromptWorkerRequest,
|
|
29
|
+
PromptDeliveryMode,
|
|
30
|
+
materialize_initial_prompts,
|
|
31
|
+
)
|
|
25
32
|
from .lead_events import LeadEvent, append_lead_event
|
|
33
|
+
from .dispatch_state import (
|
|
34
|
+
BACKEND_CLI_WRAPPER,
|
|
35
|
+
BACKEND_MIXED,
|
|
36
|
+
BACKEND_TMUX_PANE,
|
|
37
|
+
DispatchError,
|
|
38
|
+
WorkerJob,
|
|
39
|
+
dispatch_mode as _dispatch_mode,
|
|
40
|
+
load_json_object as _load_json_object,
|
|
41
|
+
missing_completion_paths as _missing_completion_paths,
|
|
42
|
+
require_string as _require_string,
|
|
43
|
+
resolve_project_path as _resolve_project_path,
|
|
44
|
+
resolve_required_path as _resolve_required_path,
|
|
45
|
+
set_dispatch_mode as _set_dispatch_mode,
|
|
46
|
+
set_worker_status as _set_worker_status,
|
|
47
|
+
string_list as _string_list,
|
|
48
|
+
string_value as _string_value,
|
|
49
|
+
utc_now as _utc_now,
|
|
50
|
+
worker_state as _worker_state,
|
|
51
|
+
worktree_path as _worktree_path,
|
|
52
|
+
write_json as _write_json,
|
|
53
|
+
)
|
|
26
54
|
from .path_hints import hydrate_active_run_context
|
|
27
55
|
from .report_finalize import (
|
|
28
56
|
STEP_VALIDATE_RUN,
|
|
29
57
|
FinalizeContext,
|
|
30
58
|
FinalizeError,
|
|
31
|
-
project_relative_path as _project_relative_path,
|
|
32
|
-
require_string as _require_string,
|
|
33
59
|
resolve_optional_path as _resolve_optional_path,
|
|
34
|
-
resolve_project_path as _resolve_project_path,
|
|
35
60
|
run_finalize,
|
|
36
61
|
run_seq as _run_seq,
|
|
37
|
-
string_value as _string_value,
|
|
38
62
|
)
|
|
39
63
|
from .wrapper_status import status_path_for_prompt
|
|
40
|
-
from .
|
|
41
|
-
from .worker_prompt_contract import PromptRecord, validate_initial_prompt_records
|
|
42
|
-
from .worker_prompt_body import (
|
|
43
|
-
analysis_input_lines as _analysis_input_lines,
|
|
44
|
-
analysis_prompt_body as _analysis_prompt_body,
|
|
45
|
-
existing_input_lines as _existing_input_lines,
|
|
46
|
-
instruction_path as _instruction_path,
|
|
47
|
-
mcp_pointer_line as _mcp_pointer_line,
|
|
48
|
-
)
|
|
49
|
-
from .worker_prompt_policy import (
|
|
50
|
-
PromptPlan,
|
|
51
|
-
resolve_prompt_plan_for_manifest,
|
|
52
|
-
)
|
|
64
|
+
from .worker_prompt_body import REPORT_WRITER_WORKER_ID
|
|
53
65
|
|
|
54
66
|
|
|
55
67
|
SUPPORTED_CLI_WORKERS = {
|
|
56
68
|
"codex": "okstra-codex-exec.sh",
|
|
57
69
|
"antigravity": "okstra-antigravity-exec.sh",
|
|
58
70
|
}
|
|
59
|
-
BACKEND_CLI_WRAPPER = "cli-wrapper"
|
|
60
|
-
BACKEND_MIXED = "mixed"
|
|
61
71
|
MAX_WORKER_ATTEMPTS = 2
|
|
62
|
-
REPORT_WRITER_WORKER_ID = "report-writer"
|
|
63
|
-
REPORT_LANGUAGE_VALUES = {"en", "ko", "auto"}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
class DispatchError(Exception):
|
|
67
|
-
"""Raised when a Codex dispatch request cannot be executed."""
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
@dataclass(frozen=True)
|
|
71
|
-
class WorkerDispatch:
|
|
72
|
-
worker_id: str
|
|
73
|
-
backend: str
|
|
74
|
-
project_root: Path
|
|
75
|
-
model_execution_value: str
|
|
76
|
-
wrapper_path: Path
|
|
77
|
-
prompt_path: Path
|
|
78
|
-
result_path: Path
|
|
79
|
-
worker_result_path: Path
|
|
80
|
-
completion_paths: tuple[Path, ...]
|
|
81
|
-
worktree_path: str
|
|
82
|
-
role: str
|
|
83
|
-
idle_timeout_seconds: int
|
|
84
|
-
|
|
85
|
-
@property
|
|
86
|
-
def command(self) -> list[str]:
|
|
87
|
-
return [
|
|
88
|
-
str(self.wrapper_path),
|
|
89
|
-
str(self.project_root),
|
|
90
|
-
self.model_execution_value,
|
|
91
|
-
str(self.prompt_path),
|
|
92
|
-
self.worktree_path,
|
|
93
|
-
self.role,
|
|
94
|
-
str(self.idle_timeout_seconds),
|
|
95
|
-
]
|
|
96
|
-
|
|
97
|
-
def to_payload(self) -> dict[str, Any]:
|
|
98
|
-
return {
|
|
99
|
-
"workerId": self.worker_id,
|
|
100
|
-
"backend": self.backend,
|
|
101
|
-
"modelExecutionValue": self.model_execution_value,
|
|
102
|
-
"wrapperPath": str(self.wrapper_path),
|
|
103
|
-
"promptPath": str(self.prompt_path),
|
|
104
|
-
"resultPath": str(self.result_path),
|
|
105
|
-
"workerResultPath": str(self.worker_result_path),
|
|
106
|
-
"completionPaths": [str(path) for path in self.completion_paths],
|
|
107
|
-
"worktreePath": self.worktree_path,
|
|
108
|
-
"role": self.role,
|
|
109
|
-
"command": self.command,
|
|
110
|
-
}
|
|
111
72
|
|
|
112
73
|
|
|
113
74
|
@dataclass(frozen=True)
|
|
@@ -118,7 +79,7 @@ class DispatchPlan:
|
|
|
118
79
|
team_state_path: Path
|
|
119
80
|
lead_events_path: Path
|
|
120
81
|
manifest: Mapping[str, Any]
|
|
121
|
-
workers: tuple[
|
|
82
|
+
workers: tuple[WorkerJob, ...]
|
|
122
83
|
|
|
123
84
|
def to_payload(self, *, dry_run: bool) -> dict[str, Any]:
|
|
124
85
|
return {
|
|
@@ -137,6 +98,18 @@ class DispatchPlan:
|
|
|
137
98
|
}
|
|
138
99
|
|
|
139
100
|
|
|
101
|
+
@dataclass(frozen=True)
|
|
102
|
+
class _DispatchInputs:
|
|
103
|
+
project_root: Path
|
|
104
|
+
manifest_path: Path
|
|
105
|
+
manifest: Mapping[str, Any]
|
|
106
|
+
selected_workers: tuple[str, ...]
|
|
107
|
+
team_state_path: Path
|
|
108
|
+
team_state: Mapping[str, Any]
|
|
109
|
+
active_context: Mapping[str, Any]
|
|
110
|
+
lead_events_path: Path
|
|
111
|
+
|
|
112
|
+
|
|
140
113
|
def build_dispatch_plan(
|
|
141
114
|
*,
|
|
142
115
|
project_root: Path,
|
|
@@ -148,19 +121,62 @@ def build_dispatch_plan(
|
|
|
148
121
|
enable_codex_report_writer: bool = False,
|
|
149
122
|
report_writer_codex_model: str = "",
|
|
150
123
|
) -> DispatchPlan:
|
|
124
|
+
inputs = _load_dispatch_inputs(
|
|
125
|
+
project_root,
|
|
126
|
+
run_manifest_path,
|
|
127
|
+
requested_workers,
|
|
128
|
+
enable_codex_report_writer=enable_codex_report_writer,
|
|
129
|
+
)
|
|
130
|
+
worker_requests, prompt_paths = _materialize_selected_worker_prompts(
|
|
131
|
+
project_root=inputs.project_root,
|
|
132
|
+
manifest_path=inputs.manifest_path,
|
|
133
|
+
workspace_root=workspace_root,
|
|
134
|
+
team_state=inputs.team_state,
|
|
135
|
+
selected_workers=inputs.selected_workers,
|
|
136
|
+
report_writer_codex_model=report_writer_codex_model,
|
|
137
|
+
)
|
|
138
|
+
workers = _build_selected_worker_dispatches(
|
|
139
|
+
project_root=inputs.project_root,
|
|
140
|
+
manifest=inputs.manifest,
|
|
141
|
+
team_state=inputs.team_state,
|
|
142
|
+
active_context=inputs.active_context,
|
|
143
|
+
workspace_root=workspace_root,
|
|
144
|
+
okstra_bin=okstra_bin,
|
|
145
|
+
idle_timeout_seconds=idle_timeout_seconds,
|
|
146
|
+
selected_workers=inputs.selected_workers,
|
|
147
|
+
worker_requests=worker_requests,
|
|
148
|
+
prompt_paths=prompt_paths,
|
|
149
|
+
)
|
|
150
|
+
return DispatchPlan(
|
|
151
|
+
project_root=inputs.project_root,
|
|
152
|
+
workspace_root=workspace_root,
|
|
153
|
+
manifest_path=inputs.manifest_path,
|
|
154
|
+
team_state_path=inputs.team_state_path,
|
|
155
|
+
lead_events_path=inputs.lead_events_path,
|
|
156
|
+
manifest=inputs.manifest,
|
|
157
|
+
workers=workers,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _load_dispatch_inputs(
|
|
162
|
+
project_root: Path,
|
|
163
|
+
run_manifest_path: Path,
|
|
164
|
+
requested_workers: Sequence[str],
|
|
165
|
+
*,
|
|
166
|
+
enable_codex_report_writer: bool,
|
|
167
|
+
) -> _DispatchInputs:
|
|
151
168
|
project_root = project_root.resolve()
|
|
152
169
|
manifest_path = _resolve_project_path(project_root, str(run_manifest_path))
|
|
153
170
|
manifest = _load_json_object(manifest_path, "run manifest")
|
|
154
171
|
_validate_codex_manifest(manifest, manifest_path)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
team_state_path = _resolve_required_path(
|
|
162
|
-
project_root, manifest, "teamStatePath", "team-state"
|
|
172
|
+
selected_workers = tuple(
|
|
173
|
+
_select_workers(
|
|
174
|
+
manifest,
|
|
175
|
+
requested_workers,
|
|
176
|
+
enable_codex_report_writer=enable_codex_report_writer,
|
|
177
|
+
)
|
|
163
178
|
)
|
|
179
|
+
team_state_path = _resolve_required_path(project_root, manifest, "teamStatePath")
|
|
164
180
|
team_state = _load_json_object(team_state_path, "team-state")
|
|
165
181
|
_mark_skipped_workers(
|
|
166
182
|
team_state_path,
|
|
@@ -175,19 +191,67 @@ def build_dispatch_plan(
|
|
|
175
191
|
active_context = _load_optional_json_object(
|
|
176
192
|
_resolve_optional_path(project_root, manifest.get("activeRunContextPath"))
|
|
177
193
|
)
|
|
178
|
-
|
|
179
|
-
project_root, manifest, "leadEventsPath", "lead events"
|
|
180
|
-
)
|
|
181
|
-
_materialize_missing_worker_prompts(
|
|
194
|
+
return _DispatchInputs(
|
|
182
195
|
project_root=project_root,
|
|
196
|
+
manifest_path=manifest_path,
|
|
183
197
|
manifest=manifest,
|
|
198
|
+
selected_workers=selected_workers,
|
|
199
|
+
team_state_path=team_state_path,
|
|
184
200
|
team_state=team_state,
|
|
185
201
|
active_context=active_context,
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
202
|
+
lead_events_path=_resolve_required_path(project_root, manifest, "leadEventsPath"),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _materialize_selected_worker_prompts(
|
|
207
|
+
*,
|
|
208
|
+
project_root: Path,
|
|
209
|
+
manifest_path: Path,
|
|
210
|
+
workspace_root: Path,
|
|
211
|
+
team_state: Mapping[str, Any],
|
|
212
|
+
selected_workers: Sequence[str],
|
|
213
|
+
report_writer_codex_model: str,
|
|
214
|
+
) -> tuple[tuple[InitialPromptWorkerRequest, ...], tuple[Path, ...]]:
|
|
215
|
+
worker_requests = tuple(
|
|
216
|
+
InitialPromptWorkerRequest(
|
|
217
|
+
worker_id=worker_id,
|
|
218
|
+
model=_requested_worker_model(
|
|
219
|
+
worker_id,
|
|
220
|
+
team_state,
|
|
221
|
+
report_writer_codex_model,
|
|
222
|
+
),
|
|
223
|
+
)
|
|
224
|
+
for worker_id in selected_workers
|
|
189
225
|
)
|
|
190
|
-
|
|
226
|
+
try:
|
|
227
|
+
prompt_paths = materialize_initial_prompts(
|
|
228
|
+
InitialPromptMaterializationRequest(
|
|
229
|
+
project_root=project_root,
|
|
230
|
+
run_manifest_path=manifest_path,
|
|
231
|
+
runtime_root=workspace_root,
|
|
232
|
+
delivery_mode=PromptDeliveryMode.EAGER_INCLUDE,
|
|
233
|
+
workers=worker_requests,
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
except InitialPromptMaterializationError as exc:
|
|
237
|
+
raise DispatchError(f"{exc.reason}: {exc}") from exc
|
|
238
|
+
return worker_requests, prompt_paths
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _build_selected_worker_dispatches(
|
|
242
|
+
*,
|
|
243
|
+
project_root: Path,
|
|
244
|
+
manifest: Mapping[str, Any],
|
|
245
|
+
team_state: Mapping[str, Any],
|
|
246
|
+
active_context: Mapping[str, Any],
|
|
247
|
+
workspace_root: Path,
|
|
248
|
+
okstra_bin: Path | None,
|
|
249
|
+
idle_timeout_seconds: int,
|
|
250
|
+
selected_workers: Sequence[str],
|
|
251
|
+
worker_requests: Sequence[InitialPromptWorkerRequest],
|
|
252
|
+
prompt_paths: Sequence[Path],
|
|
253
|
+
) -> tuple[WorkerJob, ...]:
|
|
254
|
+
return tuple(
|
|
191
255
|
_build_worker_dispatch(
|
|
192
256
|
worker_id=worker_id,
|
|
193
257
|
project_root=project_root,
|
|
@@ -197,36 +261,18 @@ def build_dispatch_plan(
|
|
|
197
261
|
workspace_root=workspace_root,
|
|
198
262
|
okstra_bin=okstra_bin,
|
|
199
263
|
idle_timeout_seconds=idle_timeout_seconds,
|
|
200
|
-
|
|
264
|
+
model_execution_value=worker_request.model,
|
|
265
|
+
prompt_path=prompt_path,
|
|
266
|
+
)
|
|
267
|
+
for worker_id, worker_request, prompt_path in zip(
|
|
268
|
+
selected_workers,
|
|
269
|
+
worker_requests,
|
|
270
|
+
prompt_paths,
|
|
271
|
+
strict=True,
|
|
201
272
|
)
|
|
202
|
-
for worker_id in selected_workers
|
|
203
|
-
)
|
|
204
|
-
_validate_initial_prompts(manifest, workers)
|
|
205
|
-
return DispatchPlan(
|
|
206
|
-
project_root=project_root,
|
|
207
|
-
workspace_root=workspace_root,
|
|
208
|
-
manifest_path=manifest_path,
|
|
209
|
-
team_state_path=team_state_path,
|
|
210
|
-
lead_events_path=lead_events_path,
|
|
211
|
-
manifest=manifest,
|
|
212
|
-
workers=workers,
|
|
213
273
|
)
|
|
214
274
|
|
|
215
275
|
|
|
216
|
-
def _validate_initial_prompts(
|
|
217
|
-
manifest: Mapping[str, Any],
|
|
218
|
-
workers: Sequence[WorkerDispatch],
|
|
219
|
-
) -> None:
|
|
220
|
-
records = [
|
|
221
|
-
PromptRecord(worker.worker_id, "initial", worker.prompt_path)
|
|
222
|
-
for worker in workers
|
|
223
|
-
]
|
|
224
|
-
errors = validate_initial_prompt_records(manifest=manifest, records=records)
|
|
225
|
-
if errors:
|
|
226
|
-
task_type = _require_string(manifest, "taskType")
|
|
227
|
-
raise DispatchError(f"{task_type} prompt contract: " + "; ".join(errors))
|
|
228
|
-
|
|
229
|
-
|
|
230
276
|
def dispatch_plan(plan: DispatchPlan) -> int:
|
|
231
277
|
_set_dispatch_mode(plan.team_state_path, _dispatch_mode(plan.workers))
|
|
232
278
|
for worker in plan.workers:
|
|
@@ -244,7 +290,7 @@ def dispatch_plan(plan: DispatchPlan) -> int:
|
|
|
244
290
|
return 0
|
|
245
291
|
|
|
246
292
|
|
|
247
|
-
def _dispatch_worker_with_retry(plan: DispatchPlan, worker:
|
|
293
|
+
def _dispatch_worker_with_retry(plan: DispatchPlan, worker: WorkerJob) -> int:
|
|
248
294
|
for attempt in range(1, MAX_WORKER_ATTEMPTS + 1):
|
|
249
295
|
_record_worker_attempt(plan, worker, attempt, "running")
|
|
250
296
|
attempt_details = _attempt_event_details(worker, attempt)
|
|
@@ -305,7 +351,7 @@ def _dispatch_worker_with_retry(plan: DispatchPlan, worker: WorkerDispatch) -> i
|
|
|
305
351
|
return 1
|
|
306
352
|
def _record_worker_attempt(
|
|
307
353
|
plan: DispatchPlan,
|
|
308
|
-
worker:
|
|
354
|
+
worker: WorkerJob,
|
|
309
355
|
attempt: int,
|
|
310
356
|
status: str,
|
|
311
357
|
reason: str = "",
|
|
@@ -327,7 +373,7 @@ def _record_worker_attempt(
|
|
|
327
373
|
|
|
328
374
|
|
|
329
375
|
def _matches_worker_attempt(
|
|
330
|
-
record: object, worker:
|
|
376
|
+
record: object, worker: WorkerJob, attempt: int
|
|
331
377
|
) -> bool:
|
|
332
378
|
return (
|
|
333
379
|
isinstance(record, dict)
|
|
@@ -338,7 +384,7 @@ def _matches_worker_attempt(
|
|
|
338
384
|
|
|
339
385
|
|
|
340
386
|
def _worker_dispatch_record(
|
|
341
|
-
worker:
|
|
387
|
+
worker: WorkerJob, attempt: int, status: str, reason: str
|
|
342
388
|
) -> dict[str, Any]:
|
|
343
389
|
provider = "codex" if worker.worker_id == REPORT_WRITER_WORKER_ID else worker.worker_id
|
|
344
390
|
return {
|
|
@@ -403,147 +449,6 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
403
449
|
return parser
|
|
404
450
|
|
|
405
451
|
|
|
406
|
-
def _materialize_missing_worker_prompts(
|
|
407
|
-
*,
|
|
408
|
-
project_root: Path,
|
|
409
|
-
manifest: Mapping[str, Any],
|
|
410
|
-
team_state: Mapping[str, Any],
|
|
411
|
-
active_context: Mapping[str, Any],
|
|
412
|
-
selected_workers: Sequence[str],
|
|
413
|
-
workspace_root: Path,
|
|
414
|
-
report_writer_codex_model: str,
|
|
415
|
-
) -> None:
|
|
416
|
-
for worker_id in selected_workers:
|
|
417
|
-
prompt_path = _resolve_project_path(
|
|
418
|
-
project_root, _worker_prompt_path(manifest, worker_id)
|
|
419
|
-
)
|
|
420
|
-
if prompt_path.is_file():
|
|
421
|
-
continue
|
|
422
|
-
prompt = _render_missing_worker_prompt(
|
|
423
|
-
project_root=project_root,
|
|
424
|
-
manifest=manifest,
|
|
425
|
-
team_state=team_state,
|
|
426
|
-
active_context=active_context,
|
|
427
|
-
worker_id=worker_id,
|
|
428
|
-
workspace_root=workspace_root,
|
|
429
|
-
report_writer_codex_model=report_writer_codex_model,
|
|
430
|
-
)
|
|
431
|
-
prompt_path.parent.mkdir(parents=True, exist_ok=True)
|
|
432
|
-
prompt_path.write_text(prompt, encoding="utf-8")
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
def _render_missing_worker_prompt(
|
|
436
|
-
*,
|
|
437
|
-
project_root: Path,
|
|
438
|
-
manifest: Mapping[str, Any],
|
|
439
|
-
team_state: Mapping[str, Any],
|
|
440
|
-
active_context: Mapping[str, Any],
|
|
441
|
-
worker_id: str,
|
|
442
|
-
workspace_root: Path,
|
|
443
|
-
report_writer_codex_model: str,
|
|
444
|
-
) -> str:
|
|
445
|
-
if worker_id == REPORT_WRITER_WORKER_ID:
|
|
446
|
-
return _render_missing_report_writer_prompt(
|
|
447
|
-
project_root=project_root,
|
|
448
|
-
manifest=manifest,
|
|
449
|
-
team_state=team_state,
|
|
450
|
-
active_context=active_context,
|
|
451
|
-
report_writer_codex_model=report_writer_codex_model,
|
|
452
|
-
)
|
|
453
|
-
return _render_missing_analysis_worker_prompt(
|
|
454
|
-
project_root=project_root,
|
|
455
|
-
manifest=manifest,
|
|
456
|
-
team_state=team_state,
|
|
457
|
-
active_context=active_context,
|
|
458
|
-
worker_id=worker_id,
|
|
459
|
-
workspace_root=workspace_root,
|
|
460
|
-
)
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
def _render_missing_analysis_worker_prompt(
|
|
464
|
-
*,
|
|
465
|
-
project_root: Path,
|
|
466
|
-
manifest: Mapping[str, Any],
|
|
467
|
-
team_state: Mapping[str, Any],
|
|
468
|
-
active_context: Mapping[str, Any],
|
|
469
|
-
worker_id: str,
|
|
470
|
-
workspace_root: Path,
|
|
471
|
-
) -> str:
|
|
472
|
-
worker_state = _worker_state(team_state, worker_id)
|
|
473
|
-
result_rel = _require_string(worker_state, "resultPath")
|
|
474
|
-
prompt_rel = _worker_prompt_path(manifest, worker_id)
|
|
475
|
-
model = _require_string(worker_state, "modelExecutionValue")
|
|
476
|
-
role = _analysis_pane_role(manifest, active_context, worker_id)
|
|
477
|
-
plan = _initial_prompt_plan(manifest, worker_id)
|
|
478
|
-
lines = _base_prompt_headers(
|
|
479
|
-
project_root=project_root,
|
|
480
|
-
manifest=manifest,
|
|
481
|
-
active_context=active_context,
|
|
482
|
-
worker_id=worker_id,
|
|
483
|
-
prompt_rel=prompt_rel,
|
|
484
|
-
result_rel=result_rel,
|
|
485
|
-
)
|
|
486
|
-
lines.extend(_worktree_headers(manifest, active_context, role))
|
|
487
|
-
lines.extend(
|
|
488
|
-
_analysis_prompt_body(
|
|
489
|
-
manifest,
|
|
490
|
-
active_context,
|
|
491
|
-
worker_id,
|
|
492
|
-
model,
|
|
493
|
-
role,
|
|
494
|
-
plan,
|
|
495
|
-
)
|
|
496
|
-
)
|
|
497
|
-
executor_tail = _implementation_executor_tail(
|
|
498
|
-
manifest,
|
|
499
|
-
active_context,
|
|
500
|
-
project_root,
|
|
501
|
-
workspace_root,
|
|
502
|
-
role,
|
|
503
|
-
)
|
|
504
|
-
if executor_tail:
|
|
505
|
-
lines.extend(["", executor_tail.rstrip()])
|
|
506
|
-
return "\n".join(lines).rstrip() + "\n"
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
def _render_missing_report_writer_prompt(
|
|
510
|
-
*,
|
|
511
|
-
project_root: Path,
|
|
512
|
-
manifest: Mapping[str, Any],
|
|
513
|
-
team_state: Mapping[str, Any],
|
|
514
|
-
active_context: Mapping[str, Any],
|
|
515
|
-
report_writer_codex_model: str,
|
|
516
|
-
) -> str:
|
|
517
|
-
worker_state = _worker_state(team_state, REPORT_WRITER_WORKER_ID)
|
|
518
|
-
markdown_path = _resolve_required_path(
|
|
519
|
-
project_root, manifest, "expectedReportPath", "report"
|
|
520
|
-
)
|
|
521
|
-
data_json_path = _final_report_data_path(markdown_path)
|
|
522
|
-
result_rel = _project_relative_path(project_root, data_json_path)
|
|
523
|
-
worker_result_rel = _require_string(worker_state, "resultPath")
|
|
524
|
-
model = (report_writer_codex_model or "").strip()
|
|
525
|
-
if not model:
|
|
526
|
-
raise DispatchError(
|
|
527
|
-
"Codex report-writer dispatch requires --report-writer-codex-model"
|
|
528
|
-
)
|
|
529
|
-
lines = _base_prompt_headers(
|
|
530
|
-
project_root=project_root,
|
|
531
|
-
manifest=manifest,
|
|
532
|
-
active_context=active_context,
|
|
533
|
-
worker_id=REPORT_WRITER_WORKER_ID,
|
|
534
|
-
prompt_rel=_worker_prompt_path(manifest, REPORT_WRITER_WORKER_ID),
|
|
535
|
-
result_rel=result_rel,
|
|
536
|
-
audit_source_rel=worker_result_rel,
|
|
537
|
-
)
|
|
538
|
-
lines.extend([
|
|
539
|
-
f"**Worker Result Path:** {worker_result_rel}",
|
|
540
|
-
f"**Model:** Report writer worker, {model}",
|
|
541
|
-
f"**Report Language:** {_resolve_report_language(project_root, manifest, active_context)}",
|
|
542
|
-
])
|
|
543
|
-
lines.extend(_report_writer_prompt_body(manifest, active_context, team_state))
|
|
544
|
-
return "\n".join(lines).rstrip() + "\n"
|
|
545
|
-
|
|
546
|
-
|
|
547
452
|
def _build_worker_dispatch(
|
|
548
453
|
*,
|
|
549
454
|
worker_id: str,
|
|
@@ -554,8 +459,9 @@ def _build_worker_dispatch(
|
|
|
554
459
|
workspace_root: Path,
|
|
555
460
|
okstra_bin: Path | None,
|
|
556
461
|
idle_timeout_seconds: int,
|
|
557
|
-
|
|
558
|
-
|
|
462
|
+
model_execution_value: str,
|
|
463
|
+
prompt_path: Path,
|
|
464
|
+
) -> WorkerJob:
|
|
559
465
|
if worker_id == REPORT_WRITER_WORKER_ID:
|
|
560
466
|
return _build_report_writer_dispatch(
|
|
561
467
|
project_root=project_root,
|
|
@@ -564,20 +470,19 @@ def _build_worker_dispatch(
|
|
|
564
470
|
workspace_root=workspace_root,
|
|
565
471
|
okstra_bin=okstra_bin,
|
|
566
472
|
idle_timeout_seconds=idle_timeout_seconds,
|
|
567
|
-
|
|
473
|
+
model_execution_value=model_execution_value,
|
|
474
|
+
prompt_path=prompt_path,
|
|
568
475
|
)
|
|
569
476
|
worker_state = _worker_state(team_state, worker_id)
|
|
570
|
-
prompt_rel = _worker_prompt_path(manifest, worker_id)
|
|
571
|
-
prompt_path = _resolve_project_path(project_root, prompt_rel)
|
|
572
477
|
result_path = _resolve_project_path(
|
|
573
478
|
project_root, _require_string(worker_state, "resultPath")
|
|
574
479
|
)
|
|
575
|
-
model_execution_value = _require_string(worker_state, "modelExecutionValue")
|
|
576
480
|
wrapper = _resolve_wrapper(worker_id, workspace_root, okstra_bin)
|
|
577
481
|
worktree_path = _worktree_path(manifest, active_context)
|
|
578
482
|
role = _pane_role(prompt_path)
|
|
579
|
-
return
|
|
483
|
+
return WorkerJob(
|
|
580
484
|
worker_id=worker_id,
|
|
485
|
+
provider=worker_id,
|
|
581
486
|
backend=BACKEND_CLI_WRAPPER,
|
|
582
487
|
project_root=project_root,
|
|
583
488
|
model_execution_value=model_execution_value,
|
|
@@ -589,6 +494,7 @@ def _build_worker_dispatch(
|
|
|
589
494
|
worktree_path=worktree_path,
|
|
590
495
|
role=role,
|
|
591
496
|
idle_timeout_seconds=idle_timeout_seconds,
|
|
497
|
+
dispatch_kind="initial",
|
|
592
498
|
)
|
|
593
499
|
|
|
594
500
|
|
|
@@ -600,27 +506,21 @@ def _build_report_writer_dispatch(
|
|
|
600
506
|
workspace_root: Path,
|
|
601
507
|
okstra_bin: Path | None,
|
|
602
508
|
idle_timeout_seconds: int,
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
if not model_execution_value:
|
|
607
|
-
raise DispatchError(
|
|
608
|
-
"Codex report-writer dispatch requires --report-writer-codex-model"
|
|
609
|
-
)
|
|
509
|
+
model_execution_value: str,
|
|
510
|
+
prompt_path: Path,
|
|
511
|
+
) -> WorkerJob:
|
|
610
512
|
worker_state = _worker_state(team_state, REPORT_WRITER_WORKER_ID)
|
|
611
|
-
prompt_path = _resolve_project_path(
|
|
612
|
-
project_root, _worker_prompt_path(manifest, REPORT_WRITER_WORKER_ID)
|
|
613
|
-
)
|
|
614
513
|
data_json_path = _final_report_data_path(
|
|
615
|
-
_resolve_required_path(project_root, manifest, "expectedReportPath"
|
|
514
|
+
_resolve_required_path(project_root, manifest, "expectedReportPath")
|
|
616
515
|
)
|
|
617
516
|
markdown_path = _final_report_markdown_path(data_json_path)
|
|
618
517
|
worker_result_path = _resolve_project_path(
|
|
619
518
|
project_root, _require_string(worker_state, "resultPath")
|
|
620
519
|
)
|
|
621
|
-
|
|
622
|
-
return WorkerDispatch(
|
|
520
|
+
return WorkerJob(
|
|
623
521
|
worker_id=REPORT_WRITER_WORKER_ID,
|
|
522
|
+
# codex 경로의 report-writer 는 claude 가 아니라 codex wrapper 로 돈다.
|
|
523
|
+
provider="codex",
|
|
624
524
|
backend=BACKEND_CLI_WRAPPER,
|
|
625
525
|
project_root=project_root,
|
|
626
526
|
model_execution_value=model_execution_value,
|
|
@@ -636,287 +536,33 @@ def _build_report_writer_dispatch(
|
|
|
636
536
|
worktree_path="",
|
|
637
537
|
role=REPORT_WRITER_WORKER_ID,
|
|
638
538
|
idle_timeout_seconds=idle_timeout_seconds,
|
|
539
|
+
dispatch_kind="initial",
|
|
639
540
|
)
|
|
640
541
|
|
|
641
542
|
|
|
642
|
-
def
|
|
643
|
-
manifest
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
return [
|
|
648
|
-
"",
|
|
649
|
-
"# Report Writer Worker Dispatch",
|
|
650
|
-
"",
|
|
651
|
-
"## Role",
|
|
652
|
-
(
|
|
653
|
-
"You are the Report writer worker. Author the final-report data.json "
|
|
654
|
-
"and the worker-results audit file."
|
|
655
|
-
),
|
|
656
|
-
"",
|
|
657
|
-
"## Task",
|
|
658
|
-
f"- Task key: `{_require_string(manifest, 'taskKey')}`",
|
|
659
|
-
f"- Task type: `{_require_string(manifest, 'taskType')}`",
|
|
660
|
-
"",
|
|
661
|
-
"## Inputs",
|
|
662
|
-
*_report_writer_input_lines(manifest, active_context, team_state),
|
|
663
|
-
"",
|
|
664
|
-
_mcp_pointer_line(),
|
|
665
|
-
"",
|
|
666
|
-
"## Output Contract",
|
|
667
|
-
"You are the author of TWO files:",
|
|
668
|
-
"- The final-report data.json at Result Path.",
|
|
669
|
-
"- The worker-results audit file at Audit sidecar path.",
|
|
670
|
-
(
|
|
671
|
-
'After writing the data.json, invoke "okstra render-final-report '
|
|
672
|
-
'<Result Path>" so the markdown sibling is rendered before you return.'
|
|
673
|
-
),
|
|
674
|
-
"Do not return the report inline.",
|
|
675
|
-
"Copy Report Language verbatim into data.json.meta.reportLanguage.",
|
|
676
|
-
]
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
def _base_prompt_headers(
|
|
680
|
-
*,
|
|
681
|
-
project_root: Path,
|
|
682
|
-
manifest: Mapping[str, Any],
|
|
683
|
-
active_context: Mapping[str, Any],
|
|
684
|
-
worker_id: str,
|
|
685
|
-
prompt_rel: str,
|
|
686
|
-
result_rel: str,
|
|
687
|
-
audit_source_rel: str | None = None,
|
|
688
|
-
) -> list[str]:
|
|
689
|
-
try:
|
|
690
|
-
return worker_prompt_headers(
|
|
691
|
-
project_root=project_root,
|
|
692
|
-
prompt_rel=prompt_rel,
|
|
693
|
-
result_rel=result_rel,
|
|
694
|
-
audit_source_rel=audit_source_rel,
|
|
695
|
-
worker_id=worker_id,
|
|
696
|
-
dispatch_kind="initial",
|
|
697
|
-
manifest=manifest,
|
|
698
|
-
active_context=active_context,
|
|
699
|
-
)
|
|
700
|
-
except WorkerPromptHeaderError as exc:
|
|
701
|
-
raise DispatchError(str(exc)) from exc
|
|
543
|
+
def _validate_codex_manifest(manifest: Mapping[str, Any], path: Path) -> None:
|
|
544
|
+
if manifest.get("leadRuntime") != "codex":
|
|
545
|
+
raise DispatchError(f"run manifest is not a Codex lead run: {path}")
|
|
546
|
+
if not isinstance(manifest.get("leadEventsPath"), str):
|
|
547
|
+
raise DispatchError(f"run manifest has no leadEventsPath: {path}")
|
|
702
548
|
|
|
703
549
|
|
|
704
|
-
def
|
|
705
|
-
manifest: Mapping[str, Any],
|
|
550
|
+
def _requested_worker_model(
|
|
706
551
|
worker_id: str,
|
|
707
|
-
) -> PromptPlan:
|
|
708
|
-
try:
|
|
709
|
-
return resolve_prompt_plan_for_manifest(
|
|
710
|
-
manifest=manifest,
|
|
711
|
-
worker_id=worker_id,
|
|
712
|
-
dispatch_kind="initial",
|
|
713
|
-
)
|
|
714
|
-
except ValueError as exc:
|
|
715
|
-
raise DispatchError(str(exc)) from exc
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
def _report_writer_input_lines(
|
|
719
|
-
manifest: Mapping[str, Any],
|
|
720
|
-
active_context: Mapping[str, Any],
|
|
721
552
|
team_state: Mapping[str, Any],
|
|
722
|
-
|
|
723
|
-
inputs = [
|
|
724
|
-
("Task brief", _instruction_path(manifest, active_context, "taskBriefPath")),
|
|
725
|
-
("Analysis profile", _instruction_path(manifest, active_context, "analysisProfilePath")),
|
|
726
|
-
("Analysis material", _instruction_path(manifest, active_context, "analysisMaterialPath")),
|
|
727
|
-
("Reference expectations", _instruction_path(manifest, active_context, "referenceExpectationsPath")),
|
|
728
|
-
("Clarification response", _instruction_path(manifest, active_context, "clarificationResponsePath")),
|
|
729
|
-
("Final report template", _instruction_path(manifest, active_context, "finalReportTemplatePath")),
|
|
730
|
-
("Final report schema", _instruction_path(manifest, active_context, "finalReportSchemaPath")),
|
|
731
|
-
("Worker results directory", _string_value(manifest.get("workerResultsDirectoryPath"))),
|
|
732
|
-
]
|
|
733
|
-
lines = _existing_input_lines(inputs)
|
|
734
|
-
lines.extend(_analysis_worker_result_lines(team_state))
|
|
735
|
-
return lines or ["- No report-writer inputs were recorded in active-run-context."]
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
def _analysis_worker_result_lines(team_state: Mapping[str, Any]) -> list[str]:
|
|
739
|
-
lines = []
|
|
740
|
-
workers = team_state.get("workers")
|
|
741
|
-
if not isinstance(workers, list):
|
|
742
|
-
return lines
|
|
743
|
-
for worker in workers:
|
|
744
|
-
if not isinstance(worker, Mapping):
|
|
745
|
-
continue
|
|
746
|
-
worker_id = worker.get("workerId")
|
|
747
|
-
if worker_id == REPORT_WRITER_WORKER_ID:
|
|
748
|
-
continue
|
|
749
|
-
result_path = _string_value(worker.get("resultPath"))
|
|
750
|
-
if result_path:
|
|
751
|
-
lines.append(f"- Analysis result ({worker_id}): `{result_path}`")
|
|
752
|
-
return lines
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
def _analysis_pane_role(
|
|
756
|
-
manifest: Mapping[str, Any],
|
|
757
|
-
active_context: Mapping[str, Any],
|
|
758
|
-
worker_id: str,
|
|
759
|
-
) -> str:
|
|
760
|
-
if _require_string(manifest, "taskType") != "implementation":
|
|
761
|
-
return "worker"
|
|
762
|
-
if _executor_provider(manifest) != worker_id:
|
|
763
|
-
return "worker"
|
|
764
|
-
worktree = _worktree_path(manifest, active_context)
|
|
765
|
-
if not worktree:
|
|
766
|
-
raise DispatchError("implementation executor prompt generation requires a worktree path")
|
|
767
|
-
return "executor"
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
def _executor_provider(manifest: Mapping[str, Any]) -> str:
|
|
771
|
-
team_contract = manifest.get("teamContract")
|
|
772
|
-
if not isinstance(team_contract, Mapping):
|
|
773
|
-
return ""
|
|
774
|
-
executor = team_contract.get("executor")
|
|
775
|
-
if not isinstance(executor, Mapping):
|
|
776
|
-
return ""
|
|
777
|
-
return _string_value(executor.get("provider"))
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
def _worktree_headers(
|
|
781
|
-
manifest: Mapping[str, Any],
|
|
782
|
-
active_context: Mapping[str, Any],
|
|
783
|
-
role: str,
|
|
784
|
-
) -> list[str]:
|
|
785
|
-
task_type = _require_string(manifest, "taskType")
|
|
786
|
-
worktree = _worktree_path(manifest, active_context)
|
|
787
|
-
if task_type != "implementation" or not worktree:
|
|
788
|
-
return []
|
|
789
|
-
headers = [f"**Worktree:** {worktree}"]
|
|
790
|
-
if role == "executor":
|
|
791
|
-
headers.append(f"cwd for every mutating command: {worktree}")
|
|
792
|
-
return headers
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
def _implementation_executor_tail(
|
|
796
|
-
manifest: Mapping[str, Any],
|
|
797
|
-
active_context: Mapping[str, Any],
|
|
798
|
-
project_root: Path,
|
|
799
|
-
workspace_root: Path,
|
|
800
|
-
role: str,
|
|
553
|
+
report_writer_codex_model: str,
|
|
801
554
|
) -> str:
|
|
802
|
-
if
|
|
803
|
-
return
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
analysis_profile = _instruction_path(
|
|
807
|
-
manifest,
|
|
808
|
-
active_context,
|
|
809
|
-
"analysisProfilePath",
|
|
810
|
-
)
|
|
811
|
-
if not analysis_profile:
|
|
812
|
-
raise DispatchError("implementation executor profile path is missing")
|
|
813
|
-
executor_profile = (
|
|
814
|
-
_resolve_project_path(project_root, analysis_profile).parent
|
|
815
|
-
/ "implementation-executor.md"
|
|
816
|
-
)
|
|
817
|
-
if not executor_profile.is_file():
|
|
818
|
-
raise DispatchError(
|
|
819
|
-
f"resolved implementation executor profile not found: {executor_profile}"
|
|
555
|
+
if worker_id != REPORT_WRITER_WORKER_ID:
|
|
556
|
+
return _require_string(
|
|
557
|
+
_worker_state(team_state, worker_id),
|
|
558
|
+
"modelExecutionValue",
|
|
820
559
|
)
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
diff_review_path = profiles / "_implementation-diff-review.md"
|
|
828
|
-
if diff_review_path.is_file():
|
|
829
|
-
parts.append(diff_review_path.read_text(encoding="utf-8"))
|
|
830
|
-
selfcheck_path = profiles / "_implementation-self-check.md"
|
|
831
|
-
if selfcheck_path.is_file():
|
|
832
|
-
parts.append(selfcheck_path.read_text(encoding="utf-8"))
|
|
833
|
-
parts.extend(_clarification_response_section(manifest, active_context, project_root))
|
|
834
|
-
return "\n\n".join(parts)
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
def _clarification_response_section(
|
|
838
|
-
manifest: Mapping[str, Any],
|
|
839
|
-
active_context: Mapping[str, Any],
|
|
840
|
-
project_root: Path,
|
|
841
|
-
) -> list[str]:
|
|
842
|
-
"""The user's answers, inlined for a CLI executor.
|
|
843
|
-
|
|
844
|
-
A CLI worker runs in its own process and receives only the prompt text, so
|
|
845
|
-
a path reference never reaches it. Passing the clarification response as a
|
|
846
|
-
path meant the executor implemented the pre-answer plan while the report
|
|
847
|
-
described the answers as carried in — the user answered and nothing
|
|
848
|
-
downstream acted on it.
|
|
849
|
-
"""
|
|
850
|
-
relative = _instruction_path(manifest, active_context, "clarificationResponsePath")
|
|
851
|
-
if not relative:
|
|
852
|
-
return []
|
|
853
|
-
path = _resolve_project_path(project_root, relative)
|
|
854
|
-
if not path.is_file():
|
|
855
|
-
return []
|
|
856
|
-
body = path.read_text(encoding="utf-8").strip()
|
|
857
|
-
if not body:
|
|
858
|
-
return []
|
|
859
|
-
return [
|
|
860
|
-
"# Clarification answers carried in (authoritative)\n\n"
|
|
861
|
-
"The user answered these before this run. Where an answer conflicts "
|
|
862
|
-
"with the approved plan text, the answer wins — implement the answer "
|
|
863
|
-
f"and say so in your result.\n\nSource: `{relative}`\n\n{body}"
|
|
864
|
-
]
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
def _resolve_report_language(
|
|
868
|
-
project_root: Path,
|
|
869
|
-
manifest: Mapping[str, Any],
|
|
870
|
-
active_context: Mapping[str, Any],
|
|
871
|
-
) -> str:
|
|
872
|
-
for value in _report_language_candidates(project_root):
|
|
873
|
-
if value == "auto":
|
|
874
|
-
return _infer_report_language(project_root, manifest, active_context)
|
|
875
|
-
if value:
|
|
876
|
-
return value
|
|
877
|
-
return _infer_report_language(project_root, manifest, active_context)
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
def _report_language_candidates(project_root: Path) -> list[str]:
|
|
881
|
-
okstra_home = Path(os.environ.get("OKSTRA_HOME") or Path.home() / ".okstra")
|
|
882
|
-
return [
|
|
883
|
-
_read_report_language(project_root / ".okstra" / "project.json", "project"),
|
|
884
|
-
_read_report_language(okstra_home / "config.json", "global"),
|
|
885
|
-
]
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
def _read_report_language(path: Path, label: str) -> str:
|
|
889
|
-
if not path.is_file():
|
|
890
|
-
return ""
|
|
891
|
-
value = _load_json_object(path, f"{label} config").get("reportLanguage")
|
|
892
|
-
if value in (None, ""):
|
|
893
|
-
return ""
|
|
894
|
-
if isinstance(value, str) and value in REPORT_LANGUAGE_VALUES:
|
|
895
|
-
return value
|
|
896
|
-
raise DispatchError(f"{label} config reportLanguage must be en, ko, or auto: {path}")
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
def _infer_report_language(
|
|
900
|
-
project_root: Path,
|
|
901
|
-
manifest: Mapping[str, Any],
|
|
902
|
-
active_context: Mapping[str, Any],
|
|
903
|
-
) -> str:
|
|
904
|
-
task_brief = _instruction_path(manifest, active_context, "taskBriefPath")
|
|
905
|
-
if not task_brief:
|
|
906
|
-
return "en"
|
|
907
|
-
path = _resolve_project_path(project_root, task_brief)
|
|
908
|
-
if not path.is_file():
|
|
909
|
-
return "en"
|
|
910
|
-
text = path.read_text(encoding="utf-8", errors="replace")[:16000]
|
|
911
|
-
hangul_count = sum(1 for char in text if "\uac00" <= char <= "\ud7a3")
|
|
912
|
-
return "ko" if hangul_count >= 10 else "en"
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
def _validate_codex_manifest(manifest: Mapping[str, Any], path: Path) -> None:
|
|
916
|
-
if manifest.get("leadRuntime") != "codex":
|
|
917
|
-
raise DispatchError(f"run manifest is not a Codex lead run: {path}")
|
|
918
|
-
if not isinstance(manifest.get("leadEventsPath"), str):
|
|
919
|
-
raise DispatchError(f"run manifest has no leadEventsPath: {path}")
|
|
560
|
+
model = report_writer_codex_model.strip()
|
|
561
|
+
if model:
|
|
562
|
+
return model
|
|
563
|
+
raise DispatchError(
|
|
564
|
+
"Codex report-writer dispatch requires --report-writer-codex-model"
|
|
565
|
+
)
|
|
920
566
|
|
|
921
567
|
|
|
922
568
|
def _select_workers(
|
|
@@ -1066,7 +712,7 @@ def _resolve_wrapper(
|
|
|
1066
712
|
|
|
1067
713
|
def _run_worker(
|
|
1068
714
|
plan: DispatchPlan,
|
|
1069
|
-
worker:
|
|
715
|
+
worker: WorkerJob,
|
|
1070
716
|
) -> subprocess.CompletedProcess[str]:
|
|
1071
717
|
if worker.backend == BACKEND_CLI_WRAPPER:
|
|
1072
718
|
return _run_cli_wrapper_worker(plan, worker)
|
|
@@ -1075,7 +721,7 @@ def _run_worker(
|
|
|
1075
721
|
|
|
1076
722
|
def _run_cli_wrapper_worker(
|
|
1077
723
|
plan: DispatchPlan,
|
|
1078
|
-
worker:
|
|
724
|
+
worker: WorkerJob,
|
|
1079
725
|
) -> subprocess.CompletedProcess[str]:
|
|
1080
726
|
env = {
|
|
1081
727
|
**os.environ,
|
|
@@ -1093,7 +739,7 @@ def _run_cli_wrapper_worker(
|
|
|
1093
739
|
|
|
1094
740
|
def _post_process_report_writer_result(
|
|
1095
741
|
plan: DispatchPlan,
|
|
1096
|
-
worker:
|
|
742
|
+
worker: WorkerJob,
|
|
1097
743
|
) -> dict[str, Any]:
|
|
1098
744
|
if worker.worker_id != REPORT_WRITER_WORKER_ID:
|
|
1099
745
|
return {"ok": True, "reason": "", "steps": []}
|
|
@@ -1137,14 +783,7 @@ def _append_event(
|
|
|
1137
783
|
)
|
|
1138
784
|
|
|
1139
785
|
|
|
1140
|
-
def
|
|
1141
|
-
backends = {worker.backend for worker in workers}
|
|
1142
|
-
if len(backends) == 1:
|
|
1143
|
-
return next(iter(backends))
|
|
1144
|
-
return BACKEND_MIXED
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
def _event_details(worker: WorkerDispatch) -> dict[str, Any]:
|
|
786
|
+
def _event_details(worker: WorkerJob) -> dict[str, Any]:
|
|
1148
787
|
return {
|
|
1149
788
|
"dispatchMode": worker.backend,
|
|
1150
789
|
"workerId": worker.worker_id,
|
|
@@ -1156,7 +795,7 @@ def _event_details(worker: WorkerDispatch) -> dict[str, Any]:
|
|
|
1156
795
|
}
|
|
1157
796
|
|
|
1158
797
|
|
|
1159
|
-
def _attempt_event_details(worker:
|
|
798
|
+
def _attempt_event_details(worker: WorkerJob, attempt: int) -> dict[str, Any]:
|
|
1160
799
|
return {
|
|
1161
800
|
**_event_details(worker),
|
|
1162
801
|
"attempt": attempt,
|
|
@@ -1176,69 +815,6 @@ def _should_retry_missing_result(
|
|
|
1176
815
|
)
|
|
1177
816
|
|
|
1178
817
|
|
|
1179
|
-
def _missing_completion_paths(worker: WorkerDispatch) -> list[Path]:
|
|
1180
|
-
return [path for path in worker.completion_paths if not path.is_file()]
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
def _set_worker_status(
|
|
1184
|
-
team_state_path: Path,
|
|
1185
|
-
worker_id: str,
|
|
1186
|
-
status: str,
|
|
1187
|
-
reason: str,
|
|
1188
|
-
*,
|
|
1189
|
-
model_execution_value: str = "",
|
|
1190
|
-
) -> None:
|
|
1191
|
-
payload = _load_json_object(team_state_path, "team-state")
|
|
1192
|
-
workers = payload.get("workers")
|
|
1193
|
-
if not isinstance(workers, list):
|
|
1194
|
-
raise DispatchError(f"team-state workers must be an array: {team_state_path}")
|
|
1195
|
-
for worker in workers:
|
|
1196
|
-
if isinstance(worker, dict) and worker.get("workerId") == worker_id:
|
|
1197
|
-
worker["status"] = status
|
|
1198
|
-
worker["reason"] = reason
|
|
1199
|
-
if model_execution_value:
|
|
1200
|
-
worker["model"] = model_execution_value
|
|
1201
|
-
worker["modelExecutionValue"] = model_execution_value
|
|
1202
|
-
_write_json(team_state_path, payload)
|
|
1203
|
-
return
|
|
1204
|
-
raise DispatchError(f"team-state has no workerId={worker_id}: {team_state_path}")
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
def _set_dispatch_mode(team_state_path: Path, dispatch_mode: str) -> None:
|
|
1208
|
-
payload = _load_json_object(team_state_path, "team-state")
|
|
1209
|
-
payload["dispatchMode"] = dispatch_mode
|
|
1210
|
-
_write_json(team_state_path, payload)
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
def _worker_state(team_state: Mapping[str, Any], worker_id: str) -> Mapping[str, Any]:
|
|
1214
|
-
workers = team_state.get("workers")
|
|
1215
|
-
if not isinstance(workers, list):
|
|
1216
|
-
raise DispatchError("team-state workers must be an array")
|
|
1217
|
-
for worker in workers:
|
|
1218
|
-
if isinstance(worker, Mapping) and worker.get("workerId") == worker_id:
|
|
1219
|
-
return worker
|
|
1220
|
-
raise DispatchError(f"team-state has no workerId={worker_id}")
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
def _worker_prompt_path(manifest: Mapping[str, Any], worker_id: str) -> str:
|
|
1224
|
-
paths = manifest.get("workerPromptPathByWorkerId")
|
|
1225
|
-
if not isinstance(paths, Mapping):
|
|
1226
|
-
raise DispatchError("run manifest has no workerPromptPathByWorkerId object")
|
|
1227
|
-
return _require_string(paths, worker_id)
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
def _worktree_path(
|
|
1231
|
-
manifest: Mapping[str, Any],
|
|
1232
|
-
active_context: Mapping[str, Any],
|
|
1233
|
-
) -> str:
|
|
1234
|
-
if manifest.get("taskType") not in {"implementation", "final-verification"}:
|
|
1235
|
-
return ""
|
|
1236
|
-
worktree = active_context.get("executorWorktree")
|
|
1237
|
-
if not isinstance(worktree, Mapping):
|
|
1238
|
-
return ""
|
|
1239
|
-
return str(worktree.get("path") or "")
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
818
|
def _pane_role(prompt_path: Path) -> str:
|
|
1243
819
|
if not prompt_path.is_file():
|
|
1244
820
|
raise DispatchError(f"worker prompt not found: {prompt_path}")
|
|
@@ -1250,39 +826,12 @@ def _pane_role(prompt_path: Path) -> str:
|
|
|
1250
826
|
return "worker"
|
|
1251
827
|
|
|
1252
828
|
|
|
1253
|
-
def _load_json_object(path: Path, label: str) -> dict[str, Any]:
|
|
1254
|
-
if not path.is_file():
|
|
1255
|
-
raise DispatchError(f"{label} not found: {path}")
|
|
1256
|
-
try:
|
|
1257
|
-
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
1258
|
-
except json.JSONDecodeError as exc:
|
|
1259
|
-
raise DispatchError(f"{label} is invalid JSON: {path}: {exc}") from exc
|
|
1260
|
-
if not isinstance(payload, dict):
|
|
1261
|
-
raise DispatchError(f"{label} must be a JSON object: {path}")
|
|
1262
|
-
return payload
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
829
|
def _load_optional_json_object(path: Path | None) -> dict[str, Any]:
|
|
1266
830
|
if path is None or not path.is_file():
|
|
1267
831
|
return {}
|
|
1268
832
|
return hydrate_active_run_context(_load_json_object(path, "active-run-context"))
|
|
1269
833
|
|
|
1270
834
|
|
|
1271
|
-
def _resolve_required_path(
|
|
1272
|
-
project_root: Path,
|
|
1273
|
-
manifest: Mapping[str, Any],
|
|
1274
|
-
key: str,
|
|
1275
|
-
label: str,
|
|
1276
|
-
) -> Path:
|
|
1277
|
-
return _resolve_project_path(project_root, _require_string(manifest, key))
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
def _string_list(value: Any) -> list[str]:
|
|
1281
|
-
if not isinstance(value, list):
|
|
1282
|
-
return []
|
|
1283
|
-
return [str(item).strip() for item in value if str(item).strip()]
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
835
|
def _failure_reason(exit_code: int, missing_paths: Sequence[Path]) -> str:
|
|
1287
836
|
if exit_code != 0:
|
|
1288
837
|
return f"wrapper exited with code {exit_code}"
|
|
@@ -1292,23 +841,6 @@ def _failure_reason(exit_code: int, missing_paths: Sequence[Path]) -> str:
|
|
|
1292
841
|
return "worker dispatch failed"
|
|
1293
842
|
|
|
1294
843
|
|
|
1295
|
-
def _replace_report_writer_model_line(
|
|
1296
|
-
prompt_path: Path,
|
|
1297
|
-
model_execution_value: str,
|
|
1298
|
-
) -> None:
|
|
1299
|
-
if not prompt_path.is_file():
|
|
1300
|
-
raise DispatchError(f"report-writer prompt not found: {prompt_path}")
|
|
1301
|
-
lines = prompt_path.read_text(encoding="utf-8").splitlines()
|
|
1302
|
-
marker = "**Model:** Report writer worker,"
|
|
1303
|
-
replacement = f"{marker} {model_execution_value}"
|
|
1304
|
-
updated = [
|
|
1305
|
-
replacement if line.startswith(marker) else line
|
|
1306
|
-
for line in lines
|
|
1307
|
-
]
|
|
1308
|
-
if updated != lines:
|
|
1309
|
-
prompt_path.write_text("\n".join(updated) + "\n", encoding="utf-8")
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
844
|
def _parse_workers(raw: str) -> list[str]:
|
|
1313
845
|
return [item.strip() for item in raw.split(",") if item.strip()]
|
|
1314
846
|
|
|
@@ -1323,20 +855,6 @@ def _parse_idle_timeout(raw: str) -> int:
|
|
|
1323
855
|
return value
|
|
1324
856
|
|
|
1325
857
|
|
|
1326
|
-
def _utc_now() -> str:
|
|
1327
|
-
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
def _write_json(path: Path, payload: Mapping[str, Any]) -> None:
|
|
1331
|
-
# Write via temp file + os.replace so a crash mid-write cannot leave a
|
|
1332
|
-
# truncated team-state.json that every later _load_json_object rejects;
|
|
1333
|
-
# os.replace is atomic on POSIX, matching run_context._atomic_write_json.
|
|
1334
|
-
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
1335
|
-
tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
|
|
1336
|
-
encoding="utf-8")
|
|
1337
|
-
os.replace(tmp, path)
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
858
|
def _print_json(payload: Mapping[str, Any]) -> None:
|
|
1341
859
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
1342
860
|
|