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.
@@ -0,0 +1,1053 @@
1
+ """Render, validate, and immutably publish initial worker prompts."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import tempfile
7
+ from dataclasses import dataclass
8
+ from enum import Enum
9
+ from pathlib import Path
10
+ from typing import Any, Callable, Literal, Mapping, Sequence
11
+
12
+ from .final_report_paths import final_report_data_path
13
+ from .path_hints import hydrate_active_run_context
14
+ from .report_language import resolve_report_language
15
+ from .worker_prompt_body import (
16
+ REPORT_WRITER_WORKER_ID,
17
+ analysis_prompt_body,
18
+ instruction_path,
19
+ report_writer_prompt_body,
20
+ )
21
+ from .worker_prompt_contract import PromptRecord, validate_initial_prompt_records
22
+ from .worker_prompt_headers import worker_prompt_headers
23
+ from .worker_prompt_policy import PromptPlan, resolve_prompt_plan_for_manifest
24
+
25
+
26
+ class PromptDeliveryMode(str, Enum):
27
+ EAGER_INCLUDE = "eager-include"
28
+ LAZY_PATH_REFERENCE = "lazy-path-reference"
29
+
30
+
31
+ MaterializationReason = Literal[
32
+ "existing_prompt_invalid",
33
+ "existing_prompt_conflict",
34
+ "required_input_missing",
35
+ "render_failed",
36
+ "validation_failed",
37
+ "publication_failed",
38
+ ]
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class InitialPromptWorkerRequest:
43
+ worker_id: str
44
+ model: str
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class InitialPromptMaterializationRequest:
49
+ project_root: Path
50
+ run_manifest_path: Path
51
+ runtime_root: Path
52
+ delivery_mode: PromptDeliveryMode
53
+ workers: tuple[InitialPromptWorkerRequest, ...]
54
+
55
+
56
+ class InitialPromptMaterializationError(RuntimeError):
57
+ reason: MaterializationReason
58
+
59
+ def __init__(self, reason: MaterializationReason, message: str) -> None:
60
+ super().__init__(message)
61
+ self.reason = reason
62
+
63
+
64
+ MaterializeInitialPrompts = Callable[
65
+ [InitialPromptMaterializationRequest],
66
+ tuple[Path, ...],
67
+ ]
68
+
69
+ _MATERIALIZABLE_AUDIENCES = frozenset({
70
+ "analysis",
71
+ "implementation-executor",
72
+ "implementation-verifier",
73
+ "report-writer",
74
+ })
75
+ _CLARIFICATION_INPUT_PREFIX = "- Clarification response:"
76
+ _CLARIFICATION_AUTHORITY_HEADING = (
77
+ "# Clarification answers carried in (authoritative)"
78
+ )
79
+ _CLARIFICATION_AUTHORITY_HEADING_PREFIX = (
80
+ "# Clarification answers carried in"
81
+ )
82
+ _CLARIFICATION_AUTHORITY_INTRO = (
83
+ "The user answered these before this run. Where an answer conflicts "
84
+ "with the approved plan text, the answer wins — implement the answer "
85
+ "and say so in your result."
86
+ )
87
+ _CLARIFICATION_SOURCE_PREFIX = "Source:"
88
+ _REQUIRED_PROMPT_RESOURCES_HEADING = "## Required prompt resources"
89
+
90
+
91
+ @dataclass(frozen=True)
92
+ class _MaterializationContext:
93
+ request: InitialPromptMaterializationRequest
94
+ project_root: Path
95
+ manifest: Mapping[str, Any]
96
+ team_state: Mapping[str, Any]
97
+ active_context: Mapping[str, Any]
98
+
99
+
100
+ @dataclass(frozen=True)
101
+ class _PromptItem:
102
+ worker: InitialPromptWorkerRequest
103
+ final_path: Path
104
+ plan: PromptPlan
105
+ existed: bool
106
+ clarification_input: tuple[str, Path] | None = None
107
+ resources: tuple["_PromptResource", ...] = ()
108
+ temp_path: Path | None = None
109
+
110
+
111
+ @dataclass(frozen=True)
112
+ class _PromptResource:
113
+ path: Path
114
+ text: str
115
+
116
+
117
+ @dataclass(frozen=True)
118
+ class _PublishedPrompt:
119
+ worker: InitialPromptWorkerRequest
120
+ path: Path
121
+ plan: PromptPlan
122
+ existed: bool
123
+
124
+
125
+ def materialize_initial_prompts(
126
+ request: InitialPromptMaterializationRequest,
127
+ ) -> tuple[Path, ...]:
128
+ """Materialize selected initial prompts in request order."""
129
+ try:
130
+ return _materialize_initial_prompts(request)
131
+ except InitialPromptMaterializationError:
132
+ raise
133
+ except Exception as exc:
134
+ raise InitialPromptMaterializationError(
135
+ "render_failed",
136
+ f"unexpected initial prompt materialization failure: {exc}",
137
+ ) from exc
138
+
139
+
140
+ def _materialize_initial_prompts(
141
+ request: InitialPromptMaterializationRequest,
142
+ ) -> tuple[Path, ...]:
143
+ context = _load_materialization_context(request)
144
+ resolved_items = [
145
+ _resolve_prompt_item(context, worker)
146
+ for worker in request.workers
147
+ ]
148
+ items: list[_PromptItem] = []
149
+ completed = False
150
+ try:
151
+ for item in resolved_items:
152
+ items.append(_prepare_prompt_item(context, item))
153
+ _validate_prepublication_set(context, items)
154
+ published = tuple(
155
+ _publish_or_reuse(context, item)
156
+ for item in items
157
+ )
158
+ _validate_published_set(context, published)
159
+ completed = True
160
+ return tuple(prompt.path for prompt in published)
161
+ finally:
162
+ cleanup_error = _remove_temp_files(items)
163
+ if completed and cleanup_error is not None:
164
+ raise InitialPromptMaterializationError(
165
+ "publication_failed",
166
+ f"cannot remove prompt staging file: {cleanup_error}",
167
+ ) from cleanup_error
168
+
169
+
170
+ def _load_materialization_context(
171
+ request: InitialPromptMaterializationRequest,
172
+ ) -> _MaterializationContext:
173
+ project_root = request.project_root.resolve()
174
+ if not isinstance(request.delivery_mode, PromptDeliveryMode):
175
+ raise InitialPromptMaterializationError(
176
+ "required_input_missing",
177
+ "delivery_mode must be a PromptDeliveryMode",
178
+ )
179
+ manifest_path = _resolve_input_path(project_root, request.run_manifest_path)
180
+ manifest = _load_json_object(manifest_path, "run manifest")
181
+ team_state = _load_referenced_json(project_root, manifest, "teamStatePath")
182
+ active_context = hydrate_active_run_context(
183
+ _load_referenced_json(
184
+ project_root,
185
+ manifest,
186
+ "activeRunContextPath",
187
+ )
188
+ )
189
+ return _MaterializationContext(
190
+ request=request,
191
+ project_root=project_root,
192
+ manifest=manifest,
193
+ team_state=team_state,
194
+ active_context=active_context,
195
+ )
196
+
197
+
198
+ def _resolve_prompt_item(
199
+ context: _MaterializationContext,
200
+ worker: InitialPromptWorkerRequest,
201
+ ) -> _PromptItem:
202
+ _validate_worker_request(worker)
203
+ plan = _prompt_plan(context.manifest, worker.worker_id)
204
+ if plan.audience not in _MATERIALIZABLE_AUDIENCES:
205
+ raise InitialPromptMaterializationError(
206
+ "required_input_missing",
207
+ f"prompt audience is not materializable: {plan.audience}",
208
+ )
209
+ clarification_input = _resolve_clarification_input(context, plan)
210
+ final_path = _worker_prompt_path(context, worker.worker_id)
211
+ existed = final_path.exists()
212
+ return _PromptItem(
213
+ worker=worker,
214
+ final_path=final_path,
215
+ plan=plan,
216
+ existed=existed,
217
+ clarification_input=clarification_input,
218
+ )
219
+
220
+
221
+ def _prepare_prompt_item(
222
+ context: _MaterializationContext,
223
+ item: _PromptItem,
224
+ ) -> _PromptItem:
225
+ resources = _resolve_prompt_resources(
226
+ context,
227
+ item.plan,
228
+ existed=item.existed,
229
+ )
230
+ prepared_item = _with_prompt_resources(item, resources)
231
+ if item.existed:
232
+ _validate_existing_prompt(context, prepared_item)
233
+ return prepared_item
234
+ temp_path: Path | None = None
235
+ try:
236
+ item.final_path.parent.mkdir(parents=True, exist_ok=True)
237
+ text = _render_prompt(context, prepared_item)
238
+ with tempfile.NamedTemporaryFile(
239
+ mode="w",
240
+ encoding="utf-8",
241
+ dir=item.final_path.parent,
242
+ prefix=f".{item.final_path.name}.",
243
+ suffix=".tmp",
244
+ delete=False,
245
+ ) as handle:
246
+ temp_path = Path(handle.name)
247
+ handle.write(text)
248
+ except InitialPromptMaterializationError:
249
+ raise
250
+ except Exception as exc:
251
+ if temp_path is not None:
252
+ try:
253
+ temp_path.unlink(missing_ok=True)
254
+ except OSError:
255
+ pass
256
+ raise InitialPromptMaterializationError(
257
+ "render_failed",
258
+ f"cannot render initial prompt for {item.worker.worker_id}: {exc}",
259
+ ) from exc
260
+ if temp_path is None:
261
+ raise InitialPromptMaterializationError(
262
+ "render_failed",
263
+ f"no staging file was created for {item.worker.worker_id}",
264
+ )
265
+ return _PromptItem(
266
+ worker=item.worker,
267
+ final_path=item.final_path,
268
+ plan=item.plan,
269
+ existed=False,
270
+ clarification_input=item.clarification_input,
271
+ resources=resources,
272
+ temp_path=temp_path,
273
+ )
274
+
275
+
276
+ def _with_prompt_resources(
277
+ item: _PromptItem,
278
+ resources: tuple[_PromptResource, ...],
279
+ ) -> _PromptItem:
280
+ return _PromptItem(
281
+ worker=item.worker,
282
+ final_path=item.final_path,
283
+ plan=item.plan,
284
+ existed=item.existed,
285
+ clarification_input=item.clarification_input,
286
+ resources=resources,
287
+ temp_path=item.temp_path,
288
+ )
289
+
290
+
291
+ def _render_prompt(
292
+ context: _MaterializationContext,
293
+ item: _PromptItem,
294
+ ) -> str:
295
+ worker = item.worker
296
+ state = _worker_state(context.team_state, worker.worker_id)
297
+ prompt_rel = _project_relative(context.project_root, item.final_path)
298
+ result_rel, audit_source_rel = _result_paths(context, state, worker.worker_id)
299
+ try:
300
+ lines = worker_prompt_headers(
301
+ project_root=context.project_root,
302
+ prompt_rel=prompt_rel,
303
+ result_rel=result_rel,
304
+ audit_source_rel=audit_source_rel,
305
+ worker_id=worker.worker_id,
306
+ dispatch_kind="initial",
307
+ manifest=context.manifest,
308
+ active_context=context.active_context,
309
+ )
310
+ lines.append(
311
+ f"**Prompt Delivery Mode:** {context.request.delivery_mode.value}"
312
+ )
313
+ lines.extend(_worktree_lines(context, item.plan))
314
+ lines.extend(_prompt_body(context, state, worker, item.plan))
315
+ lines.extend(_resource_lines(context, item))
316
+ except InitialPromptMaterializationError:
317
+ raise
318
+ except Exception as exc:
319
+ raise InitialPromptMaterializationError(
320
+ "render_failed",
321
+ f"cannot compose initial prompt for {worker.worker_id}: {exc}",
322
+ ) from exc
323
+ return "\n".join(lines).rstrip() + "\n"
324
+
325
+
326
+ def _prompt_body(
327
+ context: _MaterializationContext,
328
+ state: Mapping[str, Any],
329
+ worker: InitialPromptWorkerRequest,
330
+ plan: PromptPlan,
331
+ ) -> list[str]:
332
+ if plan.audience == "report-writer":
333
+ return report_writer_prompt_body(
334
+ context.manifest,
335
+ context.active_context,
336
+ context.team_state,
337
+ worker.model,
338
+ resolve_report_language(
339
+ context.project_root,
340
+ context.manifest,
341
+ context.active_context,
342
+ ),
343
+ )
344
+ return analysis_prompt_body(
345
+ context.manifest,
346
+ context.active_context,
347
+ worker.worker_id,
348
+ worker.model,
349
+ _worker_role(plan),
350
+ plan,
351
+ )
352
+
353
+
354
+ def _resource_lines(
355
+ context: _MaterializationContext,
356
+ item: _PromptItem,
357
+ ) -> list[str]:
358
+ if not item.resources:
359
+ return []
360
+ if context.request.delivery_mode is PromptDeliveryMode.LAZY_PATH_REFERENCE:
361
+ return [
362
+ "",
363
+ _REQUIRED_PROMPT_RESOURCES_HEADING,
364
+ "",
365
+ *(f"- `{resource.path}`" for resource in item.resources),
366
+ ]
367
+ bodies = [resource.text.rstrip() for resource in item.resources]
368
+ clarification = (
369
+ _eager_executor_clarification(item.clarification_input)
370
+ if item.plan.audience == "implementation-executor"
371
+ else ""
372
+ )
373
+ if clarification:
374
+ bodies.append(clarification)
375
+ return ["", "\n\n".join(bodies)]
376
+
377
+
378
+ def _resolve_clarification_input(
379
+ context: _MaterializationContext,
380
+ plan: PromptPlan,
381
+ ) -> tuple[str, Path] | None:
382
+ relative = instruction_path(
383
+ context.manifest,
384
+ context.active_context,
385
+ "clarificationResponsePath",
386
+ )
387
+ if not relative:
388
+ return None
389
+ path = _resolve_owned_artifact_path(
390
+ context.project_root,
391
+ Path(relative),
392
+ "clarification response path",
393
+ )
394
+ return relative, path
395
+
396
+
397
+ def _eager_executor_clarification(
398
+ clarification_input: tuple[str, Path] | None,
399
+ ) -> str:
400
+ if clarification_input is None:
401
+ return ""
402
+ relative, _ = clarification_input
403
+ body = _clarification_body(clarification_input)
404
+ if not body:
405
+ return ""
406
+ return (
407
+ f"{_CLARIFICATION_AUTHORITY_HEADING}\n\n"
408
+ f"{_CLARIFICATION_AUTHORITY_INTRO}\n\n"
409
+ f"Source: `{relative}`\n\n{body}"
410
+ )
411
+
412
+
413
+ def _clarification_body(
414
+ clarification_input: tuple[str, Path] | None,
415
+ ) -> str:
416
+ if clarification_input is None:
417
+ return ""
418
+ _, path = clarification_input
419
+ if not path.is_file():
420
+ return ""
421
+ return _read_required_text(path, "required clarification response").strip()
422
+
423
+
424
+ def _read_required_nonempty_text(path: Path, label: str) -> str:
425
+ text = _read_required_text(path, label)
426
+ if text.strip():
427
+ return text
428
+ cause = ValueError(f"{label} is empty: {path}")
429
+ raise InitialPromptMaterializationError(
430
+ "required_input_missing",
431
+ str(cause),
432
+ ) from cause
433
+
434
+
435
+ def _read_required_text(path: Path, label: str) -> str:
436
+ try:
437
+ return path.read_text(encoding="utf-8")
438
+ except (OSError, UnicodeError) as exc:
439
+ raise InitialPromptMaterializationError(
440
+ "required_input_missing",
441
+ f"{label} cannot be read: {path}",
442
+ ) from exc
443
+
444
+
445
+ def _resolve_prompt_resources(
446
+ context: _MaterializationContext,
447
+ plan: PromptPlan,
448
+ *,
449
+ existed: bool,
450
+ ) -> tuple[_PromptResource, ...]:
451
+ paths = _required_resource_path_candidates(context, plan)
452
+ if existed and context.request.delivery_mode is PromptDeliveryMode.EAGER_INCLUDE:
453
+ return ()
454
+ return tuple(
455
+ _PromptResource(
456
+ path=path.resolve(),
457
+ text=_read_required_nonempty_text(path, "required prompt resource"),
458
+ )
459
+ for path in paths
460
+ )
461
+
462
+
463
+ def _required_resource_path_candidates(
464
+ context: _MaterializationContext,
465
+ plan: PromptPlan,
466
+ ) -> tuple[Path, ...]:
467
+ profiles = context.request.runtime_root.resolve() / "prompts" / "profiles"
468
+ if plan.audience == "implementation-executor":
469
+ paths = (
470
+ _executor_profile_path(context),
471
+ profiles / "_coding-conventions-preflight.md",
472
+ profiles / "_implementation-diff-review.md",
473
+ profiles / "_implementation-self-check.md",
474
+ )
475
+ elif plan.audience == "implementation-verifier":
476
+ paths = (
477
+ profiles / "_implementation-verifier.md",
478
+ profiles / "_implementation-self-check.md",
479
+ )
480
+ else:
481
+ return ()
482
+ return paths
483
+
484
+
485
+ def _executor_profile_path(context: _MaterializationContext) -> Path:
486
+ instruction_set = context.active_context.get("instructionSet")
487
+ value = ""
488
+ if isinstance(instruction_set, Mapping):
489
+ value = _string_value(instruction_set.get("analysisProfilePath"))
490
+ value = value or _string_value(context.manifest.get("analysisProfilePath"))
491
+ if not value:
492
+ raise InitialPromptMaterializationError(
493
+ "required_input_missing",
494
+ "implementation executor profile path is missing",
495
+ )
496
+ return _resolve_owned_artifact_path(
497
+ context.project_root,
498
+ _resolve_input_path(context.project_root, Path(value)).parent
499
+ / "implementation-executor.md",
500
+ "implementation executor profile path",
501
+ )
502
+
503
+
504
+ def _validate_prepublication_set(
505
+ context: _MaterializationContext,
506
+ items: Sequence[_PromptItem],
507
+ ) -> None:
508
+ records = [_item_record(context, item) for item in items]
509
+ _validate_existing_record_subset(
510
+ context.manifest,
511
+ [
512
+ record
513
+ for item, record in zip(items, records, strict=True)
514
+ if item.existed
515
+ ],
516
+ "prepublication",
517
+ )
518
+ for item, record in zip(items, records, strict=True):
519
+ if item.existed:
520
+ continue
521
+ errors = validate_initial_prompt_records(
522
+ manifest=context.manifest,
523
+ records=[record],
524
+ )
525
+ if errors:
526
+ raise InitialPromptMaterializationError(
527
+ "validation_failed",
528
+ "rendered initial prompt contract: " + "; ".join(errors),
529
+ )
530
+ errors = validate_initial_prompt_records(
531
+ manifest=context.manifest,
532
+ records=records,
533
+ )
534
+ if errors:
535
+ raise InitialPromptMaterializationError(
536
+ "validation_failed",
537
+ "prepublication initial prompt contract: " + "; ".join(errors),
538
+ )
539
+
540
+
541
+ def _publish_or_reuse(
542
+ context: _MaterializationContext,
543
+ item: _PromptItem,
544
+ ) -> _PublishedPrompt:
545
+ if item.existed:
546
+ return _published_prompt(item, existed=True)
547
+ temp_path = _required_temp_path(item)
548
+ try:
549
+ os.link(temp_path, item.final_path)
550
+ except FileExistsError:
551
+ _validate_existing_prompt(context, item)
552
+ return _published_prompt(item, existed=True)
553
+ except OSError as exc:
554
+ raise InitialPromptMaterializationError(
555
+ "publication_failed",
556
+ f"cannot publish initial prompt {item.final_path}: {exc}",
557
+ ) from exc
558
+ return _published_prompt(item, existed=False)
559
+
560
+
561
+ def _validate_existing_prompt(
562
+ context: _MaterializationContext,
563
+ item: _PromptItem,
564
+ ) -> None:
565
+ text = _require_readable_prompt(
566
+ item.final_path,
567
+ item.worker.worker_id,
568
+ "existing_prompt_invalid",
569
+ )
570
+ _validate_existing_clarification_metadata(context, item, text)
571
+ _validate_existing_resource_metadata(context, item, text)
572
+ basic_record = PromptRecord(
573
+ item.worker.worker_id,
574
+ "initial",
575
+ item.final_path,
576
+ )
577
+ basic_errors = validate_initial_prompt_records(
578
+ manifest=context.manifest,
579
+ records=[basic_record],
580
+ )
581
+ if basic_errors:
582
+ raise InitialPromptMaterializationError(
583
+ "existing_prompt_invalid",
584
+ "existing initial prompt contract: " + "; ".join(basic_errors),
585
+ )
586
+ expected_record = PromptRecord(
587
+ item.worker.worker_id,
588
+ "initial",
589
+ item.final_path,
590
+ expected_model=item.worker.model,
591
+ expected_delivery_mode=context.request.delivery_mode.value,
592
+ )
593
+ expected_errors = validate_initial_prompt_records(
594
+ manifest=context.manifest,
595
+ records=[expected_record],
596
+ )
597
+ if expected_errors:
598
+ reason = _existing_metadata_reason(expected_errors)
599
+ raise InitialPromptMaterializationError(
600
+ reason,
601
+ "existing initial prompt metadata: " + "; ".join(expected_errors),
602
+ )
603
+
604
+
605
+ def _validate_existing_clarification_metadata(
606
+ context: _MaterializationContext,
607
+ item: _PromptItem,
608
+ text: str,
609
+ ) -> None:
610
+ expected = (
611
+ item.clarification_input[0]
612
+ if item.clarification_input is not None
613
+ else None
614
+ )
615
+ enumerated = (
616
+ item.plan.audience in _MATERIALIZABLE_AUDIENCES
617
+ and not item.plan.packet_only
618
+ )
619
+ input_values = _backticked_line_values(
620
+ _markdown_section(text, "## Inputs"),
621
+ _CLARIFICATION_INPUT_PREFIX,
622
+ )
623
+ expected_inputs = [expected] if expected is not None and enumerated else []
624
+ if input_values != expected_inputs:
625
+ _raise_existing_clarification_invalid(
626
+ item,
627
+ "Clarification response",
628
+ )
629
+ _validate_eager_executor_clarification_authority(
630
+ context,
631
+ item,
632
+ text,
633
+ expected,
634
+ )
635
+
636
+
637
+ def _validate_eager_executor_clarification_authority(
638
+ context: _MaterializationContext,
639
+ item: _PromptItem,
640
+ text: str,
641
+ expected: str | None,
642
+ ) -> None:
643
+ authority_expected = (
644
+ item.plan.audience == "implementation-executor"
645
+ and context.request.delivery_mode is PromptDeliveryMode.EAGER_INCLUDE
646
+ and bool(_clarification_body(item.clarification_input))
647
+ )
648
+ lines = text.splitlines()
649
+ heading_indexes = [
650
+ index
651
+ for index, line in enumerate(lines)
652
+ if line.startswith(_CLARIFICATION_AUTHORITY_HEADING_PREFIX)
653
+ ]
654
+ if not authority_expected and not heading_indexes:
655
+ return
656
+ if expected is None or len(heading_indexes) != 1:
657
+ _raise_existing_clarification_invalid(item, "clarification authority")
658
+ expected_lines = [
659
+ _CLARIFICATION_AUTHORITY_HEADING,
660
+ "",
661
+ _CLARIFICATION_AUTHORITY_INTRO,
662
+ "",
663
+ f"{_CLARIFICATION_SOURCE_PREFIX} `{expected}`",
664
+ "",
665
+ ]
666
+ heading_index = heading_indexes[0]
667
+ authority_lines = lines[
668
+ heading_index:heading_index + len(expected_lines)
669
+ ]
670
+ if not authority_expected or authority_lines != expected_lines:
671
+ _raise_existing_clarification_invalid(item, "clarification authority")
672
+
673
+
674
+ def _validate_existing_resource_metadata(
675
+ context: _MaterializationContext,
676
+ item: _PromptItem,
677
+ text: str,
678
+ ) -> None:
679
+ if (
680
+ context.request.delivery_mode is not PromptDeliveryMode.LAZY_PATH_REFERENCE
681
+ or not item.resources
682
+ ):
683
+ return
684
+ expected_section = "\n".join([
685
+ "",
686
+ *(f"- `{resource.path}`" for resource in item.resources),
687
+ ])
688
+ actual_section = _markdown_section(
689
+ text,
690
+ _REQUIRED_PROMPT_RESOURCES_HEADING,
691
+ )
692
+ if (
693
+ text.count(_REQUIRED_PROMPT_RESOURCES_HEADING) != 1
694
+ or actual_section != expected_section
695
+ ):
696
+ raise InitialPromptMaterializationError(
697
+ "existing_prompt_invalid",
698
+ "existing required prompt resources do not match canonical "
699
+ f"paths for {item.worker.worker_id}",
700
+ )
701
+
702
+
703
+ def _backticked_line_values(text: str, prefix: str) -> list[str]:
704
+ values = []
705
+ for line in text.splitlines():
706
+ if not line.startswith(prefix):
707
+ continue
708
+ suffix = line[len(prefix):].strip()
709
+ if len(suffix) >= 2 and suffix.startswith("`") and suffix.endswith("`"):
710
+ values.append(suffix[1:-1])
711
+ else:
712
+ values.append("")
713
+ return values
714
+
715
+
716
+ def _markdown_section(text: str, heading: str) -> str:
717
+ lines = text.splitlines()
718
+ try:
719
+ start = lines.index(heading) + 1
720
+ except ValueError:
721
+ return ""
722
+ section = []
723
+ for line in lines[start:]:
724
+ if line.startswith("## "):
725
+ break
726
+ section.append(line)
727
+ return "\n".join(section)
728
+
729
+
730
+ def _raise_existing_clarification_invalid(
731
+ item: _PromptItem,
732
+ field: str,
733
+ ) -> None:
734
+ raise InitialPromptMaterializationError(
735
+ "existing_prompt_invalid",
736
+ f"existing {field} metadata does not match active context "
737
+ f"for {item.worker.worker_id}",
738
+ )
739
+
740
+
741
+ def _validate_published_set(
742
+ context: _MaterializationContext,
743
+ prompts: Sequence[_PublishedPrompt],
744
+ ) -> None:
745
+ records = [_published_record(context, prompt) for prompt in prompts]
746
+ for prompt, record in zip(prompts, records, strict=True):
747
+ reason: MaterializationReason = (
748
+ "existing_prompt_invalid"
749
+ if prompt.existed
750
+ else "validation_failed"
751
+ )
752
+ _require_readable_prompt(prompt.path, prompt.worker.worker_id, reason)
753
+ _validate_existing_record_subset(
754
+ context.manifest,
755
+ [
756
+ record
757
+ for prompt, record in zip(prompts, records, strict=True)
758
+ if prompt.existed
759
+ ],
760
+ "published",
761
+ )
762
+ for prompt, record in zip(prompts, records, strict=True):
763
+ if prompt.existed:
764
+ continue
765
+ errors = validate_initial_prompt_records(
766
+ manifest=context.manifest,
767
+ records=[record],
768
+ )
769
+ if errors:
770
+ raise InitialPromptMaterializationError(
771
+ "validation_failed",
772
+ "published initial prompt contract: " + "; ".join(errors),
773
+ )
774
+ errors = validate_initial_prompt_records(
775
+ manifest=context.manifest,
776
+ records=records,
777
+ )
778
+ if errors:
779
+ raise InitialPromptMaterializationError(
780
+ "validation_failed",
781
+ "published initial prompt contract: " + "; ".join(errors),
782
+ )
783
+
784
+
785
+ def _item_record(
786
+ context: _MaterializationContext,
787
+ item: _PromptItem,
788
+ ) -> PromptRecord:
789
+ path = item.final_path if item.existed else _required_temp_path(item)
790
+ return PromptRecord(
791
+ item.worker.worker_id,
792
+ "initial",
793
+ path,
794
+ expected_model=item.worker.model,
795
+ expected_delivery_mode=context.request.delivery_mode.value,
796
+ )
797
+
798
+
799
+ def _published_record(
800
+ context: _MaterializationContext,
801
+ prompt: _PublishedPrompt,
802
+ ) -> PromptRecord:
803
+ return PromptRecord(
804
+ prompt.worker.worker_id,
805
+ "initial",
806
+ prompt.path,
807
+ expected_model=prompt.worker.model,
808
+ expected_delivery_mode=context.request.delivery_mode.value,
809
+ )
810
+
811
+
812
+ def _worker_prompt_path(
813
+ context: _MaterializationContext,
814
+ worker_id: str,
815
+ ) -> Path:
816
+ paths = context.manifest.get("workerPromptPathByWorkerId")
817
+ if not isinstance(paths, Mapping):
818
+ raise InitialPromptMaterializationError(
819
+ "required_input_missing",
820
+ "run manifest has no workerPromptPathByWorkerId object",
821
+ )
822
+ value = _required_string(paths, worker_id)
823
+ return _resolve_owned_artifact_path(
824
+ context.project_root,
825
+ Path(value),
826
+ "initial prompt path",
827
+ )
828
+
829
+
830
+ def _resolve_owned_artifact_path(
831
+ project_root: Path,
832
+ path: Path,
833
+ label: str,
834
+ ) -> Path:
835
+ resolved = _resolve_input_path(project_root, path).resolve()
836
+ artifact_root = (project_root / ".okstra").resolve()
837
+ if resolved == artifact_root or artifact_root not in resolved.parents:
838
+ raise InitialPromptMaterializationError(
839
+ "required_input_missing",
840
+ f"{label} is outside .okstra: {resolved}",
841
+ )
842
+ return resolved
843
+
844
+
845
+ def _result_paths(
846
+ context: _MaterializationContext,
847
+ state: Mapping[str, Any],
848
+ worker_id: str,
849
+ ) -> tuple[str, str | None]:
850
+ worker_result = _required_string(state, "resultPath")
851
+ if worker_id != REPORT_WRITER_WORKER_ID:
852
+ return worker_result, None
853
+ expected_report = _required_string(context.manifest, "expectedReportPath")
854
+ return str(final_report_data_path(Path(expected_report))), worker_result
855
+
856
+
857
+ def _worktree_lines(
858
+ context: _MaterializationContext,
859
+ plan: PromptPlan,
860
+ ) -> list[str]:
861
+ if plan.audience not in {
862
+ "implementation-executor",
863
+ "implementation-verifier",
864
+ }:
865
+ return []
866
+ executor_worktree = context.active_context.get("executorWorktree")
867
+ value = (
868
+ _string_value(executor_worktree.get("path"))
869
+ if isinstance(executor_worktree, Mapping)
870
+ else ""
871
+ )
872
+ if not value:
873
+ raise InitialPromptMaterializationError(
874
+ "required_input_missing",
875
+ "implementation prompt generation requires a worktree path",
876
+ )
877
+ lines = [f"**Worktree:** {value}"]
878
+ if plan.audience == "implementation-executor":
879
+ lines.append(f"cwd for every mutating command: {value}")
880
+ return lines
881
+
882
+
883
+ def _prompt_plan(
884
+ manifest: Mapping[str, Any],
885
+ worker_id: str,
886
+ ) -> PromptPlan:
887
+ try:
888
+ return resolve_prompt_plan_for_manifest(
889
+ manifest=manifest,
890
+ worker_id=worker_id,
891
+ dispatch_kind="initial",
892
+ )
893
+ except ValueError as exc:
894
+ raise InitialPromptMaterializationError(
895
+ "required_input_missing",
896
+ str(exc),
897
+ ) from exc
898
+
899
+
900
+ def _worker_state(
901
+ team_state: Mapping[str, Any],
902
+ worker_id: str,
903
+ ) -> Mapping[str, Any]:
904
+ workers = team_state.get("workers")
905
+ if isinstance(workers, list):
906
+ for worker in workers:
907
+ if isinstance(worker, Mapping) and worker.get("workerId") == worker_id:
908
+ return worker
909
+ raise InitialPromptMaterializationError(
910
+ "required_input_missing",
911
+ f"team-state has no workerId={worker_id}",
912
+ )
913
+
914
+
915
+ def _load_referenced_json(
916
+ project_root: Path,
917
+ payload: Mapping[str, Any],
918
+ key: str,
919
+ ) -> Mapping[str, Any]:
920
+ value = _required_string(payload, key)
921
+ return _load_json_object(
922
+ _resolve_input_path(project_root, Path(value)),
923
+ key,
924
+ )
925
+
926
+
927
+ def _load_json_object(path: Path, label: str) -> Mapping[str, Any]:
928
+ try:
929
+ payload = json.loads(path.read_text(encoding="utf-8"))
930
+ except (OSError, UnicodeError, json.JSONDecodeError) as exc:
931
+ raise InitialPromptMaterializationError(
932
+ "required_input_missing",
933
+ f"cannot load {label}: {path}: {exc}",
934
+ ) from exc
935
+ if not isinstance(payload, dict):
936
+ raise InitialPromptMaterializationError(
937
+ "required_input_missing",
938
+ f"{label} must be a JSON object: {path}",
939
+ )
940
+ return payload
941
+
942
+
943
+ def _required_string(payload: Mapping[str, Any], key: str) -> str:
944
+ value = _string_value(payload.get(key))
945
+ if not value:
946
+ raise InitialPromptMaterializationError(
947
+ "required_input_missing",
948
+ f"missing required string field: {key}",
949
+ )
950
+ return value
951
+
952
+
953
+ def _validate_worker_request(worker: InitialPromptWorkerRequest) -> None:
954
+ if worker.worker_id.strip() and worker.model.strip():
955
+ return
956
+ raise InitialPromptMaterializationError(
957
+ "required_input_missing",
958
+ "worker_id and model are required",
959
+ )
960
+
961
+
962
+ def _worker_role(plan: PromptPlan) -> str:
963
+ if plan.audience == "implementation-executor":
964
+ return "executor"
965
+ if plan.audience == "implementation-verifier":
966
+ return "verifier"
967
+ return "worker"
968
+
969
+
970
+ def _existing_metadata_reason(
971
+ errors: Sequence[str],
972
+ ) -> MaterializationReason:
973
+ if any("does not match requested" in error for error in errors):
974
+ return "existing_prompt_conflict"
975
+ return "existing_prompt_invalid"
976
+
977
+
978
+ def _validate_existing_record_subset(
979
+ manifest: Mapping[str, Any],
980
+ records: Sequence[PromptRecord],
981
+ stage: str,
982
+ ) -> None:
983
+ errors = validate_initial_prompt_records(
984
+ manifest=manifest,
985
+ records=records,
986
+ )
987
+ if errors:
988
+ raise InitialPromptMaterializationError(
989
+ "existing_prompt_invalid",
990
+ f"{stage} existing initial prompt contract: " + "; ".join(errors),
991
+ )
992
+
993
+
994
+ def _published_prompt(
995
+ item: _PromptItem,
996
+ *,
997
+ existed: bool,
998
+ ) -> _PublishedPrompt:
999
+ return _PublishedPrompt(
1000
+ worker=item.worker,
1001
+ path=item.final_path,
1002
+ plan=item.plan,
1003
+ existed=existed,
1004
+ )
1005
+
1006
+
1007
+ def _require_readable_prompt(
1008
+ path: Path,
1009
+ worker_id: str,
1010
+ reason: MaterializationReason,
1011
+ ) -> str:
1012
+ try:
1013
+ return path.read_text(encoding="utf-8")
1014
+ except (OSError, UnicodeError) as exc:
1015
+ raise InitialPromptMaterializationError(
1016
+ reason,
1017
+ f"cannot read prompt for {worker_id}: {path}: {exc}",
1018
+ ) from exc
1019
+
1020
+
1021
+ def _required_temp_path(item: _PromptItem) -> Path:
1022
+ if item.temp_path is not None:
1023
+ return item.temp_path
1024
+ raise InitialPromptMaterializationError(
1025
+ "render_failed",
1026
+ f"no staging file exists for {item.worker.worker_id}",
1027
+ )
1028
+
1029
+
1030
+ def _project_relative(project_root: Path, path: Path) -> str:
1031
+ return path.relative_to(project_root).as_posix()
1032
+
1033
+
1034
+ def _resolve_input_path(project_root: Path, path: Path) -> Path:
1035
+ return path if path.is_absolute() else project_root / path
1036
+
1037
+
1038
+ def _remove_temp_files(
1039
+ items: Sequence[_PromptItem],
1040
+ ) -> OSError | None:
1041
+ first_error = None
1042
+ for item in items:
1043
+ if item.temp_path is None:
1044
+ continue
1045
+ try:
1046
+ item.temp_path.unlink(missing_ok=True)
1047
+ except OSError as exc:
1048
+ first_error = first_error or exc
1049
+ return first_error
1050
+
1051
+
1052
+ def _string_value(value: Any) -> str:
1053
+ return value.strip() if isinstance(value, str) else ""