okstra 0.137.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.
@@ -29,7 +29,6 @@ from .dispatch_state import (
29
29
  string_value as _string_value,
30
30
  utc_now as _utc_now,
31
31
  validate_initial_prompts as _validate_initial_prompts,
32
- worker_prompt_path as _worker_prompt_path,
33
32
  worker_state as _worker_state,
34
33
  worktree_path as _worktree_path,
35
34
  write_json as _write_json,
@@ -39,13 +38,15 @@ from .final_report_paths import (
39
38
  final_report_markdown_path as _final_report_markdown_path,
40
39
  )
41
40
  from .lead_events import LeadEvent, append_lead_event
41
+ from .initial_prompt_materialization import (
42
+ InitialPromptMaterializationError,
43
+ InitialPromptMaterializationRequest,
44
+ InitialPromptWorkerRequest,
45
+ PromptDeliveryMode,
46
+ materialize_initial_prompts,
47
+ )
42
48
  from .path_hints import hydrate_active_run_context
43
- from .worker_prompt_headers import WorkerPromptHeaderError, worker_prompt_headers
44
- from .report_language import resolve_report_language
45
- from .worker_prompt_body import analysis_prompt_body, report_writer_prompt_body
46
49
  from .worker_prompt_body import REPORT_WRITER_WORKER_ID
47
- from .worker_prompt_contract import PromptRecord, validate_initial_prompt_records
48
- from .worker_prompt_policy import resolve_prompt_plan_for_manifest
49
50
  from .worker_artifact_paths import audit_sidecar_rel
50
51
  from .wrapper_status import read_wrapper_status, status_path_for_prompt
51
52
 
@@ -116,6 +117,15 @@ class _BuildOptions:
116
117
  dispatch_kind: str
117
118
 
118
119
 
120
+ @dataclass(frozen=True)
121
+ class _RosterWorkerFacts:
122
+ worker_id: str
123
+ model: str
124
+ provider: str
125
+ result_path: str
126
+ role: str
127
+
128
+
119
129
  def build_dispatch_plan(
120
130
  *,
121
131
  project_root: Path,
@@ -148,18 +158,22 @@ def build_dispatch_plan(
148
158
  unsupported_worker_label=unsupported_worker_label,
149
159
  dispatch_kind=dispatch_kind,
150
160
  )
151
- jobs = _jobs_from_file(project_root, workspace_root, jobs_file, options) if jobs_file else _jobs_from_roster(
152
- project_root,
153
- workspace_root,
154
- manifest,
155
- team_state,
156
- active_context,
157
- requested_workers,
158
- enable_report_writer,
159
- report_writer_model,
160
- options,
161
- )
162
- _validate_initial_prompts(manifest, jobs)
161
+ if jobs_file:
162
+ jobs = _jobs_from_file(project_root, workspace_root, jobs_file, options)
163
+ _validate_initial_prompts(manifest, jobs)
164
+ else:
165
+ jobs = _jobs_from_roster(
166
+ project_root,
167
+ workspace_root,
168
+ manifest_path,
169
+ manifest,
170
+ team_state,
171
+ active_context,
172
+ requested_workers,
173
+ enable_report_writer,
174
+ report_writer_model,
175
+ options,
176
+ )
163
177
  return DispatchPlan(
164
178
  project_root=project_root,
165
179
  workspace_root=workspace_root.resolve(),
@@ -220,6 +234,7 @@ def await_dispatches(
220
234
  def _jobs_from_roster(
221
235
  project_root: Path,
222
236
  workspace_root: Path,
237
+ manifest_path: Path,
223
238
  manifest: Mapping[str, Any],
224
239
  team_state: Mapping[str, Any],
225
240
  active_context: Mapping[str, Any],
@@ -230,12 +245,27 @@ def _jobs_from_roster(
230
245
  ) -> list[WorkerJob]:
231
246
  selected = _select_workers(manifest, requested_workers, enable_report_writer, options)
232
247
  _mark_roster_skips(project_root, manifest, team_state, selected, requested_workers, options)
248
+ worker_facts, prompt_paths = _materialize_roster_prompts(
249
+ project_root,
250
+ workspace_root,
251
+ manifest_path,
252
+ manifest,
253
+ team_state,
254
+ selected,
255
+ report_writer_model,
256
+ options,
257
+ )
233
258
  return [
234
259
  _job_from_roster_worker(
235
- project_root, workspace_root, manifest, team_state, active_context,
236
- worker_id, report_writer_model, options,
260
+ project_root,
261
+ workspace_root,
262
+ manifest,
263
+ active_context,
264
+ fact,
265
+ prompt_path,
266
+ options,
237
267
  )
238
- for worker_id in selected
268
+ for fact, prompt_path in zip(worker_facts, prompt_paths, strict=True)
239
269
  ]
240
270
 
241
271
 
@@ -270,50 +300,87 @@ def _job_from_roster_worker(
270
300
  project_root: Path,
271
301
  workspace_root: Path,
272
302
  manifest: Mapping[str, Any],
273
- team_state: Mapping[str, Any],
274
303
  active_context: Mapping[str, Any],
275
- worker_id: str,
276
- report_writer_model: str,
304
+ fact: _RosterWorkerFacts,
305
+ prompt_path: Path,
277
306
  options: _BuildOptions,
278
307
  ) -> WorkerJob:
279
- state = _worker_state(team_state, worker_id)
280
- provider = _provider_for_worker(worker_id)
281
- model = _model_for_worker(worker_id, state, report_writer_model)
282
- prompt_rel = _worker_prompt_path(manifest, worker_id)
283
- result_rel = _require_string(state, "resultPath")
284
- prompt_path = _resolve_project_path(project_root, prompt_rel)
285
- result_path = _resolve_project_path(project_root, result_rel)
286
- _materialize_prompt_if_missing(
287
- prompt_path=prompt_path,
288
- prompt_rel=prompt_rel,
289
- worker_id=worker_id,
290
- result_rel=result_rel,
291
- project_root=project_root,
292
- manifest=manifest,
293
- team_state=team_state,
294
- active_context=active_context,
295
- dispatch_kind=options.dispatch_kind,
296
- model=model,
297
- role=_string_value(state.get("role")) or "worker",
298
- )
308
+ result_path = _resolve_project_path(project_root, fact.result_path)
299
309
  return WorkerJob(
300
- worker_id=worker_id,
301
- provider=provider,
310
+ worker_id=fact.worker_id,
311
+ provider=fact.provider,
302
312
  backend=options.default_backend,
303
313
  project_root=project_root,
304
- model_execution_value=model,
305
- wrapper_path=_resolve_wrapper(provider, workspace_root, options),
314
+ model_execution_value=fact.model,
315
+ wrapper_path=_resolve_wrapper(fact.provider, workspace_root, options),
306
316
  prompt_path=prompt_path,
307
- result_path=_result_path_for_worker(worker_id, result_path, manifest, project_root),
317
+ result_path=_result_path_for_worker(
318
+ fact.worker_id, result_path, manifest, project_root
319
+ ),
308
320
  worker_result_path=result_path,
309
- completion_paths=_completion_paths(worker_id, result_path, manifest, project_root),
321
+ completion_paths=_completion_paths(
322
+ fact.worker_id, result_path, manifest, project_root
323
+ ),
310
324
  worktree_path=_worktree_path(manifest, active_context),
311
- role=_string_value(state.get("role")) or "worker",
325
+ role=fact.role,
312
326
  idle_timeout_seconds=options.idle_timeout_seconds,
313
327
  dispatch_kind=options.dispatch_kind,
314
328
  )
315
329
 
316
330
 
331
+ def _materialize_roster_prompts(
332
+ project_root: Path,
333
+ workspace_root: Path,
334
+ manifest_path: Path,
335
+ manifest: Mapping[str, Any],
336
+ team_state: Mapping[str, Any],
337
+ selected: Sequence[str],
338
+ report_writer_model: str,
339
+ options: _BuildOptions,
340
+ ) -> tuple[tuple[_RosterWorkerFacts, ...], tuple[Path, ...]]:
341
+ worker_facts = tuple(
342
+ _roster_worker_facts(team_state, worker_id, report_writer_model)
343
+ for worker_id in selected
344
+ )
345
+ try:
346
+ prompt_paths = materialize_initial_prompts(
347
+ InitialPromptMaterializationRequest(
348
+ project_root=project_root,
349
+ run_manifest_path=manifest_path,
350
+ runtime_root=workspace_root,
351
+ delivery_mode=_delivery_mode(options.default_backend),
352
+ workers=tuple(
353
+ InitialPromptWorkerRequest(fact.worker_id, fact.model)
354
+ for fact in worker_facts
355
+ ),
356
+ )
357
+ )
358
+ except InitialPromptMaterializationError as exc:
359
+ raise DispatchError(f"{exc.reason}: {exc}") from exc
360
+ return worker_facts, prompt_paths
361
+
362
+
363
+ def _roster_worker_facts(
364
+ team_state: Mapping[str, Any],
365
+ worker_id: str,
366
+ report_writer_model: str,
367
+ ) -> _RosterWorkerFacts:
368
+ state = _worker_state(team_state, worker_id)
369
+ return _RosterWorkerFacts(
370
+ worker_id=worker_id,
371
+ model=_model_for_worker(worker_id, state, report_writer_model),
372
+ provider=_provider_for_worker(worker_id),
373
+ result_path=_require_string(state, "resultPath"),
374
+ role=_string_value(state.get("role")) or "worker",
375
+ )
376
+
377
+
378
+ def _delivery_mode(default_backend: str) -> PromptDeliveryMode:
379
+ if default_backend == BACKEND_TMUX_PANE:
380
+ return PromptDeliveryMode.LAZY_PATH_REFERENCE
381
+ return PromptDeliveryMode.EAGER_INCLUDE
382
+
383
+
317
384
  def _jobs_from_file(
318
385
  project_root: Path,
319
386
  workspace_root: Path,
@@ -728,91 +795,6 @@ def _resolve_wrapper(provider: str, workspace_root: Path, options: _BuildOptions
728
795
  raise DispatchError(f"{script} not found (searched: {', '.join(str(c) for c in candidates)})")
729
796
 
730
797
 
731
- def _materialize_prompt_if_missing(
732
- *,
733
- prompt_path: Path,
734
- prompt_rel: str,
735
- worker_id: str,
736
- result_rel: str,
737
- project_root: Path,
738
- manifest: Mapping[str, Any],
739
- team_state: Mapping[str, Any],
740
- active_context: Mapping[str, Any],
741
- dispatch_kind: str,
742
- model: str,
743
- role: str,
744
- ) -> None:
745
- if prompt_path.is_file():
746
- return
747
- prompt_path.parent.mkdir(parents=True, exist_ok=True)
748
- audit_source_rel = ""
749
- if worker_id == REPORT_WRITER_WORKER_ID:
750
- # The report-writer's Result Path is the report data.json; its own worker
751
- # result stays the markdown the audit sidecar is derived from.
752
- audit_source_rel = result_rel
753
- result_rel = str(_final_report_data_path(
754
- Path(_require_string(manifest, "expectedReportPath"))
755
- ))
756
- try:
757
- headers = worker_prompt_headers(
758
- project_root=project_root,
759
- prompt_rel=prompt_rel,
760
- result_rel=result_rel,
761
- audit_source_rel=audit_source_rel or None,
762
- worker_id=worker_id,
763
- dispatch_kind=dispatch_kind,
764
- manifest=manifest,
765
- active_context=active_context,
766
- )
767
- except WorkerPromptHeaderError as exc:
768
- raise DispatchError(str(exc)) from exc
769
- try:
770
- plan = resolve_prompt_plan_for_manifest(
771
- manifest=manifest,
772
- worker_id=worker_id,
773
- dispatch_kind=dispatch_kind,
774
- )
775
- except ValueError as exc:
776
- raise DispatchError(str(exc)) from exc
777
- lines = list(headers)
778
- if plan.audience == "report-writer":
779
- lines.extend(
780
- report_writer_prompt_body(
781
- manifest,
782
- active_context,
783
- team_state,
784
- model,
785
- resolve_report_language(project_root, manifest, active_context),
786
- )
787
- )
788
- elif plan.audience in {
789
- "analysis",
790
- "implementation-executor",
791
- "implementation-verifier",
792
- }:
793
- if _string_value(manifest.get("taskType")) == "implementation":
794
- worktree = _worktree_path(manifest, active_context)
795
- if not worktree:
796
- raise DispatchError(
797
- "implementation prompt generation requires a worktree path"
798
- )
799
- lines.append(f"**Worktree:** {worktree}")
800
- try:
801
- lines.extend(
802
- analysis_prompt_body(
803
- manifest,
804
- active_context,
805
- worker_id,
806
- model,
807
- role,
808
- plan,
809
- )
810
- )
811
- except ValueError as exc:
812
- raise DispatchError(str(exc)) from exc
813
- prompt_path.write_text("\n".join([*lines, ""]), encoding="utf-8")
814
-
815
-
816
798
  def _provider_for_worker(worker_id: str) -> str:
817
799
  if worker_id == REPORT_WRITER_WORKER_ID:
818
800
  return "claude"
@@ -870,5 +852,3 @@ def _run_seq(manifest: Mapping[str, Any]) -> str:
870
852
  if isinstance(seqs, Mapping):
871
853
  return _string_value(seqs.get("manifests")) or "001"
872
854
  return "001"
873
-
874
-