okstra 0.146.1 → 0.148.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.
Files changed (137) hide show
  1. package/README.md +23 -9
  2. package/docs/architecture/storage-model.md +39 -65
  3. package/docs/architecture.md +68 -60
  4. package/docs/cli.md +40 -23
  5. package/docs/for-ai/skills/okstra-run.md +13 -34
  6. package/docs/performance-improvement-plan-v2.md +2 -2
  7. package/docs/pr-template-usage.md +1 -1
  8. package/docs/project-structure-overview.md +26 -22
  9. package/docs/task-process/README.md +4 -4
  10. package/docs/task-process/common-flow.md +12 -12
  11. package/docs/task-process/final-verification.md +2 -2
  12. package/docs/task-process/implementation.md +1 -1
  13. package/docs/task-process/release-handoff.md +1 -1
  14. package/package.json +2 -2
  15. package/runtime/BUILD.json +2 -2
  16. package/runtime/agents/workers/antigravity-worker.md +2 -2
  17. package/runtime/agents/workers/claude-worker.md +1 -1
  18. package/runtime/agents/workers/codex-worker.md +2 -2
  19. package/runtime/agents/workers/grok-worker.md +256 -0
  20. package/runtime/agents/workers/kimi-worker.md +256 -0
  21. package/runtime/agents/workers/report-writer-worker.md +12 -12
  22. package/runtime/bin/lib/okstra/cli.sh +13 -1
  23. package/runtime/bin/lib/okstra/globals.sh +3 -0
  24. package/runtime/bin/lib/okstra/usage.sh +17 -12
  25. package/runtime/bin/okstra-grok-exec.sh +5 -0
  26. package/runtime/bin/okstra-kimi-exec.sh +5 -0
  27. package/runtime/bin/okstra-provider-exec.py +235 -0
  28. package/runtime/bin/okstra-render-final-report.py +4 -4
  29. package/runtime/bin/okstra-render-report-views.py +100 -12
  30. package/runtime/bin/okstra.sh +3 -0
  31. package/runtime/prompts/lead/adapters/antigravity.md +48 -0
  32. package/runtime/prompts/lead/adapters/claude-code.md +13 -11
  33. package/runtime/prompts/lead/adapters/codex.md +7 -7
  34. package/runtime/prompts/lead/okstra-lead-contract.md +5 -5
  35. package/runtime/prompts/lead/report-writer.md +16 -12
  36. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
  37. package/runtime/prompts/profiles/_common-contract.md +16 -10
  38. package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
  39. package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
  40. package/runtime/prompts/profiles/_implementation-executor.md +12 -12
  41. package/runtime/prompts/profiles/_implementation-self-check.md +4 -4
  42. package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
  43. package/runtime/prompts/profiles/change-impact-analysis.md +2 -0
  44. package/runtime/prompts/profiles/error-analysis.md +2 -0
  45. package/runtime/prompts/profiles/feature-analysis.md +2 -0
  46. package/runtime/prompts/profiles/final-verification.md +3 -1
  47. package/runtime/prompts/profiles/forbidden-actions.json +4 -4
  48. package/runtime/prompts/profiles/implementation-planning.md +3 -1
  49. package/runtime/prompts/profiles/implementation.md +2 -2
  50. package/runtime/prompts/profiles/improvement-discovery.md +6 -2
  51. package/runtime/prompts/profiles/project-analysis.md +2 -0
  52. package/runtime/prompts/profiles/release-handoff.md +7 -7
  53. package/runtime/prompts/profiles/requirements-discovery.md +2 -0
  54. package/runtime/prompts/wizard/prompts.ko.json +9 -1
  55. package/runtime/python/okstra_ctl/codex_dispatch.py +68 -87
  56. package/runtime/python/okstra_ctl/dispatch_core.py +4 -22
  57. package/runtime/python/okstra_ctl/final_report_schema.py +37 -12
  58. package/runtime/python/okstra_ctl/lead_events.py +1 -1
  59. package/runtime/python/okstra_ctl/lead_runtime.py +13 -2
  60. package/runtime/python/okstra_ctl/models.py +156 -8
  61. package/runtime/python/okstra_ctl/path_hints.py +9 -25
  62. package/runtime/python/okstra_ctl/paths.py +1 -1
  63. package/runtime/python/okstra_ctl/render.py +172 -74
  64. package/runtime/python/okstra_ctl/render_final_report.py +136 -28
  65. package/runtime/python/okstra_ctl/report_contract.py +124 -0
  66. package/runtime/python/okstra_ctl/report_finalize.py +1 -1
  67. package/runtime/python/okstra_ctl/report_html/__init__.py +10 -0
  68. package/runtime/python/okstra_ctl/report_html/common.py +86 -0
  69. package/runtime/python/okstra_ctl/report_html/filters.py +104 -0
  70. package/runtime/python/okstra_ctl/report_html/models.py +59 -0
  71. package/runtime/python/okstra_ctl/report_html/render.py +76 -0
  72. package/runtime/python/okstra_ctl/report_html/router.py +40 -0
  73. package/runtime/python/okstra_ctl/report_html/view_models/__init__.py +1 -0
  74. package/runtime/python/okstra_ctl/report_html/view_models/change_impact_analysis.py +39 -0
  75. package/runtime/python/okstra_ctl/report_html/view_models/error_analysis.py +49 -0
  76. package/runtime/python/okstra_ctl/report_html/view_models/feature_analysis.py +39 -0
  77. package/runtime/python/okstra_ctl/report_html/view_models/final_verification.py +47 -0
  78. package/runtime/python/okstra_ctl/report_html/view_models/implementation.py +47 -0
  79. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +103 -0
  80. package/runtime/python/okstra_ctl/report_html/view_models/improvement_discovery.py +43 -0
  81. package/runtime/python/okstra_ctl/report_html/view_models/project_analysis.py +54 -0
  82. package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +54 -0
  83. package/runtime/python/okstra_ctl/report_html/view_models/requirements_discovery.py +55 -0
  84. package/runtime/python/okstra_ctl/report_html/visualizations.py +139 -0
  85. package/runtime/python/okstra_ctl/report_view_artifacts.py +4 -1
  86. package/runtime/python/okstra_ctl/report_views.py +15 -43
  87. package/runtime/python/okstra_ctl/run.py +276 -51
  88. package/runtime/python/okstra_ctl/runner_resolution.py +103 -0
  89. package/runtime/python/okstra_ctl/schema_excerpt.py +7 -17
  90. package/runtime/python/okstra_ctl/team.py +2 -7
  91. package/runtime/python/okstra_ctl/wizard.py +194 -21
  92. package/runtime/python/okstra_ctl/worker_artifacts.py +46 -0
  93. package/runtime/python/okstra_ctl/workers.py +3 -1
  94. package/runtime/python/okstra_ctl/workflow.py +4 -2
  95. package/runtime/python/okstra_token_usage/__init__.py +1 -0
  96. package/runtime/python/okstra_token_usage/collect.py +32 -23
  97. package/runtime/python/okstra_token_usage/pricing.py +35 -3
  98. package/runtime/schemas/final-report-v2.0.schema.json +3923 -0
  99. package/runtime/skills/okstra-run/SKILL.md +31 -42
  100. package/runtime/templates/prd/pr-body.template.md +1 -1
  101. package/runtime/templates/reports/final-report-v2.template.md +66 -0
  102. package/runtime/templates/reports/html/assets/base.css +41 -0
  103. package/runtime/templates/reports/html/assets/base.js +5 -0
  104. package/runtime/templates/reports/html/base.template.html +79 -0
  105. package/runtime/templates/reports/html/macros/forms.html +47 -0
  106. package/runtime/templates/reports/html/macros/layout.html +19 -0
  107. package/runtime/templates/reports/html/macros/visualizations.html +27 -0
  108. package/runtime/templates/reports/html/tasks/change-impact-analysis.template.html +40 -0
  109. package/runtime/templates/reports/html/tasks/error-analysis.template.html +40 -0
  110. package/runtime/templates/reports/html/tasks/feature-analysis.template.html +40 -0
  111. package/runtime/templates/reports/html/tasks/final-verification.template.html +39 -0
  112. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +47 -0
  113. package/runtime/templates/reports/html/tasks/implementation.template.html +40 -0
  114. package/runtime/templates/reports/html/tasks/improvement-discovery.template.html +29 -0
  115. package/runtime/templates/reports/html/tasks/project-analysis.template.html +57 -0
  116. package/runtime/templates/reports/html/tasks/release-handoff.template.html +36 -0
  117. package/runtime/templates/reports/html/tasks/requirements-discovery.template.html +37 -0
  118. package/runtime/templates/reports/report.js +21 -4
  119. package/runtime/templates/reports/settings.template.json +4 -0
  120. package/runtime/templates/reports/task-brief.template.md +7 -7
  121. package/runtime/validators/validate-report-views.py +86 -4
  122. package/runtime/validators/validate-run.py +73 -15
  123. package/runtime/validators/validate_improvement_report.py +55 -0
  124. package/runtime/validators/validate_session_conformance.py +2 -1
  125. package/src/cli-registry.mjs +4 -4
  126. package/src/commands/execute/codex-dispatch.mjs +7 -10
  127. package/src/commands/execute/render-bundle.mjs +3 -3
  128. package/src/commands/execute/run.mjs +17 -52
  129. package/src/commands/execute/wizard.mjs +4 -1
  130. package/src/commands/lifecycle/doctor.mjs +6 -3
  131. package/src/commands/lifecycle/install.mjs +49 -21
  132. package/src/commands/report/finalize.mjs +2 -3
  133. package/src/commands/report/render-final-report.mjs +4 -2
  134. package/src/commands/report/render-views.mjs +8 -8
  135. package/src/lib/runtime-manifest.mjs +1 -1
  136. package/src/lib/runtime-resolver.mjs +2 -2
  137. package/src/lib/worker-agent-render.mjs +50 -0
@@ -1,10 +1,8 @@
1
- """Codex lead CLI-worker dispatcher.
1
+ """CLI-wrapper dispatcher for a Codex-hosted Okstra lead.
2
2
 
3
- This module is intentionally narrower than the Claude Code team dispatcher. It
4
- reads an already-prepared Codex run manifest and dispatches only workers that
5
- have a CLI wrapper path. `report-writer` is supported only through explicit
6
- Codex opt-in because the prepared manifest still records a Claude-side model by
7
- default.
3
+ The prepared assignment's ``runner`` field is authoritative. Native Codex
4
+ assignments stay in the host session; this module runs only ``cli-wrapper``
5
+ assignments through their registered provider wrappers.
8
6
  """
9
7
  from __future__ import annotations
10
8
 
@@ -64,12 +62,10 @@ from .report_finalize import (
64
62
  )
65
63
  from .wrapper_status import status_path_for_prompt
66
64
  from .worker_prompt_body import REPORT_WRITER_WORKER_ID
65
+ from .models import provider_wrappers
67
66
 
68
67
 
69
- SUPPORTED_CLI_WORKERS = {
70
- "codex": "okstra-codex-exec.sh",
71
- "antigravity": "okstra-antigravity-exec.sh",
72
- }
68
+ SUPPORTED_CLI_WORKERS = provider_wrappers("analyser")
73
69
  MAX_WORKER_ATTEMPTS = 2
74
70
 
75
71
 
@@ -120,8 +116,6 @@ def build_dispatch_plan(
120
116
  okstra_bin: Path | None = None,
121
117
  requested_workers: Sequence[str] = (),
122
118
  idle_timeout_seconds: int = 600,
123
- enable_codex_report_writer: bool = False,
124
- report_writer_codex_model: str = "",
125
119
  dispatch_kind: str = "initial",
126
120
  jobs_file: Path | None = None,
127
121
  ) -> DispatchPlan:
@@ -129,7 +123,6 @@ def build_dispatch_plan(
129
123
  project_root,
130
124
  run_manifest_path,
131
125
  requested_workers,
132
- enable_codex_report_writer=enable_codex_report_writer,
133
126
  select_roster=jobs_file is None,
134
127
  )
135
128
  if jobs_file is not None:
@@ -148,7 +141,6 @@ def build_dispatch_plan(
148
141
  workspace_root=workspace_root,
149
142
  team_state=inputs.team_state,
150
143
  selected_workers=inputs.selected_workers,
151
- report_writer_codex_model=report_writer_codex_model,
152
144
  )
153
145
  workers = _build_selected_worker_dispatches(
154
146
  project_root=inputs.project_root,
@@ -204,35 +196,34 @@ def _load_dispatch_inputs(
204
196
  run_manifest_path: Path,
205
197
  requested_workers: Sequence[str],
206
198
  *,
207
- enable_codex_report_writer: bool,
208
199
  select_roster: bool = True,
209
200
  ) -> _DispatchInputs:
210
201
  project_root = project_root.resolve()
211
202
  manifest_path = _resolve_project_path(project_root, str(run_manifest_path))
212
203
  manifest = _load_json_object(manifest_path, "run manifest")
213
204
  _validate_codex_manifest(manifest, manifest_path)
205
+ team_state_path = _resolve_required_path(project_root, manifest, "teamStatePath")
206
+ team_state = _load_json_object(team_state_path, "team-state")
214
207
  selected_workers = (
215
208
  tuple(
216
209
  _select_workers(
217
210
  manifest,
211
+ team_state,
218
212
  requested_workers,
219
- enable_codex_report_writer=enable_codex_report_writer,
220
213
  )
221
214
  )
222
215
  if select_roster
223
216
  else ()
224
217
  )
225
- team_state_path = _resolve_required_path(project_root, manifest, "teamStatePath")
226
- team_state = _load_json_object(team_state_path, "team-state")
227
218
  if select_roster:
228
219
  _mark_skipped_workers(
229
220
  team_state_path,
230
221
  team_state,
231
222
  _skipped_worker_reasons(
232
223
  manifest,
224
+ team_state,
233
225
  selected_workers,
234
226
  requested_workers,
235
- enable_codex_report_writer=enable_codex_report_writer,
236
227
  ),
237
228
  )
238
229
  active_context = _load_optional_json_object(
@@ -257,16 +248,11 @@ def _materialize_selected_worker_prompts(
257
248
  workspace_root: Path,
258
249
  team_state: Mapping[str, Any],
259
250
  selected_workers: Sequence[str],
260
- report_writer_codex_model: str,
261
251
  ) -> tuple[tuple[InitialPromptWorkerRequest, ...], tuple[Path, ...]]:
262
252
  worker_requests = tuple(
263
253
  InitialPromptWorkerRequest(
264
254
  worker_id=worker_id,
265
- model=_requested_worker_model(
266
- worker_id,
267
- team_state,
268
- report_writer_codex_model,
269
- ),
255
+ model=_requested_worker_model(worker_id, team_state),
270
256
  )
271
257
  for worker_id in selected_workers
272
258
  )
@@ -472,8 +458,6 @@ def main(argv: Sequence[str] | None = None) -> int:
472
458
  okstra_bin=Path(args.okstra_bin) if args.okstra_bin else None,
473
459
  requested_workers=_parse_workers(args.workers),
474
460
  idle_timeout_seconds=idle_timeout_seconds,
475
- enable_codex_report_writer=args.enable_codex_report_writer,
476
- report_writer_codex_model=args.report_writer_codex_model,
477
461
  dispatch_kind=args.dispatch_kind,
478
462
  jobs_file=Path(args.jobs_file) if args.jobs_file else None,
479
463
  )
@@ -500,8 +484,6 @@ def _parser() -> argparse.ArgumentParser:
500
484
  parser.add_argument("--workers", default="")
501
485
  parser.add_argument("--dry-run", action="store_true")
502
486
  parser.add_argument("--idle-timeout-seconds", default="600")
503
- parser.add_argument("--enable-codex-report-writer", action="store_true")
504
- parser.add_argument("--report-writer-codex-model", default="")
505
487
  parser.add_argument("--dispatch-kind", default="initial")
506
488
  parser.add_argument("--jobs-file", default="")
507
489
  return parser
@@ -520,6 +502,8 @@ def _build_worker_dispatch(
520
502
  model_execution_value: str,
521
503
  prompt_path: Path,
522
504
  ) -> WorkerJob:
505
+ worker_state = _worker_state(team_state, worker_id)
506
+ provider = _assigned_provider(worker_state, worker_id)
523
507
  if worker_id == REPORT_WRITER_WORKER_ID:
524
508
  return _build_report_writer_dispatch(
525
509
  project_root=project_root,
@@ -530,17 +514,17 @@ def _build_worker_dispatch(
530
514
  idle_timeout_seconds=idle_timeout_seconds,
531
515
  model_execution_value=model_execution_value,
532
516
  prompt_path=prompt_path,
517
+ provider=provider,
533
518
  )
534
- worker_state = _worker_state(team_state, worker_id)
535
519
  result_path = _resolve_project_path(
536
520
  project_root, _require_string(worker_state, "resultPath")
537
521
  )
538
- wrapper = _resolve_wrapper(worker_id, workspace_root, okstra_bin)
522
+ wrapper = _resolve_wrapper(provider, workspace_root, okstra_bin)
539
523
  worktree_path = _worktree_path(manifest, active_context)
540
524
  role = _pane_role(prompt_path)
541
525
  return WorkerJob(
542
526
  worker_id=worker_id,
543
- provider=worker_id,
527
+ provider=provider,
544
528
  backend=BACKEND_CLI_WRAPPER,
545
529
  project_root=project_root,
546
530
  model_execution_value=model_execution_value,
@@ -566,6 +550,7 @@ def _build_report_writer_dispatch(
566
550
  idle_timeout_seconds: int,
567
551
  model_execution_value: str,
568
552
  prompt_path: Path,
553
+ provider: str,
569
554
  ) -> WorkerJob:
570
555
  worker_state = _worker_state(team_state, REPORT_WRITER_WORKER_ID)
571
556
  data_json_path = _final_report_data_path(
@@ -577,12 +562,11 @@ def _build_report_writer_dispatch(
577
562
  )
578
563
  return WorkerJob(
579
564
  worker_id=REPORT_WRITER_WORKER_ID,
580
- # codex 경로의 report-writer 는 claude 가 아니라 codex wrapper 로 돈다.
581
- provider="codex",
565
+ provider=provider,
582
566
  backend=BACKEND_CLI_WRAPPER,
583
567
  project_root=project_root,
584
568
  model_execution_value=model_execution_value,
585
- wrapper_path=_resolve_wrapper("codex", workspace_root, okstra_bin),
569
+ wrapper_path=_resolve_wrapper(provider, workspace_root, okstra_bin),
586
570
  prompt_path=prompt_path,
587
571
  result_path=data_json_path,
588
572
  worker_result_path=worker_result_path,
@@ -608,46 +592,26 @@ def _validate_codex_manifest(manifest: Mapping[str, Any], path: Path) -> None:
608
592
  def _requested_worker_model(
609
593
  worker_id: str,
610
594
  team_state: Mapping[str, Any],
611
- report_writer_codex_model: str,
612
595
  ) -> str:
613
- if worker_id != REPORT_WRITER_WORKER_ID:
614
- return _require_string(
615
- _worker_state(team_state, worker_id),
616
- "modelExecutionValue",
617
- )
618
- model = report_writer_codex_model.strip()
619
- if model:
620
- return model
621
- raise DispatchError(
622
- "Codex report-writer dispatch requires --report-writer-codex-model"
623
- )
596
+ state = _worker_state(team_state, worker_id)
597
+ return _require_string(state, "modelExecutionValue")
624
598
 
625
599
 
626
600
  def _select_workers(
627
601
  manifest: Mapping[str, Any],
602
+ team_state: Mapping[str, Any],
628
603
  requested_workers: Sequence[str],
629
- *,
630
- enable_codex_report_writer: bool,
631
604
  ) -> list[str]:
632
605
  recommended = _string_list(manifest.get("recommendedWorkers"))
633
606
  if not recommended:
634
607
  raise DispatchError("run manifest has no recommendedWorkers entries")
635
- supported_workers = _supported_codex_dispatch_workers(
636
- enable_codex_report_writer=enable_codex_report_writer
637
- )
608
+ dispatchable = _dispatchable_workers(recommended, team_state)
638
609
  if not requested_workers:
639
- selected = [worker for worker in recommended if worker in supported_workers]
610
+ selected = [worker for worker in recommended if worker in dispatchable]
640
611
  if selected:
641
612
  return selected
642
- if REPORT_WRITER_WORKER_ID in recommended and not enable_codex_report_writer:
643
- raise DispatchError(
644
- "report-writer requires --enable-codex-report-writer because it "
645
- "runs through the Codex wrapper instead of Claude Agent"
646
- )
647
- supported = ", ".join(sorted(supported_workers))
648
613
  raise DispatchError(
649
- "run roster has no Codex-dispatch supported worker(s) "
650
- f"(CLI dispatcher supports: {supported})"
614
+ "run roster has no cli-wrapper assignments for codex-dispatch"
651
615
  )
652
616
 
653
617
  selected = list(requested_workers)
@@ -656,17 +620,18 @@ def _select_workers(
656
620
  raise DispatchError(
657
621
  "requested worker(s) are not in this run roster: " + ", ".join(unknown)
658
622
  )
659
- if REPORT_WRITER_WORKER_ID in selected and not enable_codex_report_writer:
660
- raise DispatchError(
661
- "report-writer requires --enable-codex-report-writer because it "
662
- "runs through the Codex wrapper instead of Claude Agent"
663
- )
664
- unsupported = [
623
+ native = [
665
624
  worker for worker in selected
666
- if worker not in supported_workers
625
+ if _assigned_runner(_worker_state(team_state, worker)) == "native-session"
667
626
  ]
627
+ if native:
628
+ raise DispatchError(
629
+ "requested worker(s) use runner=native-session and must be dispatched "
630
+ "by the current Codex host: " + ", ".join(native)
631
+ )
632
+ unsupported = [worker for worker in selected if worker not in dispatchable]
668
633
  if unsupported:
669
- supported = ", ".join(sorted(supported_workers))
634
+ supported = ", ".join(sorted(dispatchable))
670
635
  raise DispatchError(
671
636
  "unsupported Codex lead worker(s): "
672
637
  + ", ".join(unsupported)
@@ -675,19 +640,37 @@ def _select_workers(
675
640
  return selected
676
641
 
677
642
 
678
- def _supported_codex_dispatch_workers(*, enable_codex_report_writer: bool) -> set[str]:
679
- supported = set(SUPPORTED_CLI_WORKERS)
680
- if enable_codex_report_writer:
681
- supported.add(REPORT_WRITER_WORKER_ID)
682
- return supported
643
+ def _assigned_provider(worker_state: Mapping[str, Any], worker_id: str) -> str:
644
+ provider = _string_value(worker_state.get("provider"))
645
+ if provider:
646
+ return provider
647
+ return "codex" if worker_id == REPORT_WRITER_WORKER_ID else worker_id
648
+
649
+
650
+ def _assigned_runner(worker_state: Mapping[str, Any]) -> str:
651
+ return _string_value(worker_state.get("runner")) or BACKEND_CLI_WRAPPER
652
+
653
+
654
+ def _dispatchable_workers(
655
+ recommended: Sequence[str], team_state: Mapping[str, Any],
656
+ ) -> set[str]:
657
+ dispatchable = set()
658
+ for worker_id in recommended:
659
+ state = _worker_state(team_state, worker_id)
660
+ provider = _assigned_provider(state, worker_id)
661
+ if (
662
+ _assigned_runner(state) == BACKEND_CLI_WRAPPER
663
+ and provider in SUPPORTED_CLI_WORKERS
664
+ ):
665
+ dispatchable.add(worker_id)
666
+ return dispatchable
683
667
 
684
668
 
685
669
  def _skipped_worker_reasons(
686
670
  manifest: Mapping[str, Any],
671
+ team_state: Mapping[str, Any],
687
672
  selected_workers: Sequence[str],
688
673
  requested_workers: Sequence[str],
689
- *,
690
- enable_codex_report_writer: bool,
691
674
  ) -> dict[str, str]:
692
675
  recommended = _string_list(manifest.get("recommendedWorkers"))
693
676
  selected = set(selected_workers)
@@ -696,24 +679,22 @@ def _skipped_worker_reasons(
696
679
  worker_id: "skipped by Codex dispatch: worker was not requested in this invocation"
697
680
  for worker_id in recommended
698
681
  if worker_id not in selected
682
+ and _assigned_runner(_worker_state(team_state, worker_id))
683
+ == BACKEND_CLI_WRAPPER
699
684
  }
700
685
 
701
- supported = _supported_codex_dispatch_workers(
702
- enable_codex_report_writer=enable_codex_report_writer
703
- )
686
+ supported = _dispatchable_workers(recommended, team_state)
704
687
  reasons = {}
705
688
  for worker_id in recommended:
706
689
  if worker_id in selected:
707
690
  continue
708
- if worker_id == REPORT_WRITER_WORKER_ID and not enable_codex_report_writer:
709
- reasons[worker_id] = (
710
- "skipped by Codex dispatch default: report-writer requires "
711
- "--enable-codex-report-writer and --report-writer-codex-model"
712
- )
713
- elif worker_id not in supported:
691
+ state = _worker_state(team_state, worker_id)
692
+ if _assigned_runner(state) == "native-session":
693
+ continue
694
+ if worker_id not in supported:
714
695
  reasons[worker_id] = (
715
- "skipped by Codex dispatch default: worker is not supported by "
716
- "the Codex CLI dispatcher"
696
+ "skipped by Codex dispatch default: assigned provider has no "
697
+ "registered CLI wrapper"
717
698
  )
718
699
  else:
719
700
  reasons[worker_id] = "skipped by Codex dispatch default"
@@ -135,8 +135,6 @@ def build_dispatch_plan(
135
135
  okstra_bin: Path | None = None,
136
136
  requested_workers: Sequence[str] = (),
137
137
  idle_timeout_seconds: int = 600,
138
- enable_report_writer: bool = False,
139
- report_writer_model: str = "",
140
138
  required_lead_runtime: str | None = None,
141
139
  default_backend: str = BACKEND_CLI_WRAPPER,
142
140
  supported_worker_wrappers: Mapping[str, str],
@@ -171,8 +169,6 @@ def build_dispatch_plan(
171
169
  team_state,
172
170
  active_context,
173
171
  requested_workers,
174
- enable_report_writer,
175
- report_writer_model,
176
172
  options,
177
173
  )
178
174
  return DispatchPlan(
@@ -240,11 +236,9 @@ def _jobs_from_roster(
240
236
  team_state: Mapping[str, Any],
241
237
  active_context: Mapping[str, Any],
242
238
  requested_workers: Sequence[str],
243
- enable_report_writer: bool,
244
- report_writer_model: str,
245
239
  options: _BuildOptions,
246
240
  ) -> list[WorkerJob]:
247
- selected = _select_workers(manifest, requested_workers, enable_report_writer, options)
241
+ selected = _select_workers(manifest, requested_workers, options)
248
242
  _mark_roster_skips(project_root, manifest, team_state, selected, requested_workers, options)
249
243
  worker_facts, prompt_paths = _materialize_roster_prompts(
250
244
  project_root,
@@ -253,7 +247,6 @@ def _jobs_from_roster(
253
247
  manifest,
254
248
  team_state,
255
249
  selected,
256
- report_writer_model,
257
250
  options,
258
251
  )
259
252
  return [
@@ -273,15 +266,12 @@ def _jobs_from_roster(
273
266
  def _select_workers(
274
267
  manifest: Mapping[str, Any],
275
268
  requested_workers: Sequence[str],
276
- enable_report_writer: bool,
277
269
  options: _BuildOptions,
278
270
  ) -> list[str]:
279
271
  recommended = _string_list(manifest.get("recommendedWorkers"))
280
272
  if not recommended:
281
273
  raise DispatchError("run manifest has no recommendedWorkers entries")
282
274
  supported = set(options.supported_worker_wrappers)
283
- if not enable_report_writer and not requested_workers:
284
- supported.discard(REPORT_WRITER_WORKER_ID)
285
275
  selected = list(requested_workers) if requested_workers else [w for w in recommended if w in supported]
286
276
  unknown = [worker for worker in selected if worker not in recommended]
287
277
  if unknown:
@@ -336,11 +326,10 @@ def _materialize_roster_prompts(
336
326
  manifest: Mapping[str, Any],
337
327
  team_state: Mapping[str, Any],
338
328
  selected: Sequence[str],
339
- report_writer_model: str,
340
329
  options: _BuildOptions,
341
330
  ) -> tuple[tuple[_RosterWorkerFacts, ...], tuple[Path, ...]]:
342
331
  worker_facts = tuple(
343
- _roster_worker_facts(team_state, worker_id, report_writer_model)
332
+ _roster_worker_facts(team_state, worker_id)
344
333
  for worker_id in selected
345
334
  )
346
335
  try:
@@ -364,13 +353,12 @@ def _materialize_roster_prompts(
364
353
  def _roster_worker_facts(
365
354
  team_state: Mapping[str, Any],
366
355
  worker_id: str,
367
- report_writer_model: str,
368
356
  ) -> _RosterWorkerFacts:
369
357
  state = _worker_state(team_state, worker_id)
370
358
  return _RosterWorkerFacts(
371
359
  worker_id=worker_id,
372
- model=_model_for_worker(worker_id, state, report_writer_model),
373
- provider=_provider_for_worker(worker_id),
360
+ model=_require_string(state, "modelExecutionValue"),
361
+ provider=_string_value(state.get("provider")) or _provider_for_worker(worker_id),
374
362
  result_path=_require_string(state, "resultPath"),
375
363
  role=_string_value(state.get("role")) or "worker",
376
364
  )
@@ -771,12 +759,6 @@ def _provider_for_worker(worker_id: str) -> str:
771
759
  return worker_id
772
760
 
773
761
 
774
- def _model_for_worker(worker_id: str, state: Mapping[str, Any], report_writer_model: str) -> str:
775
- if worker_id == REPORT_WRITER_WORKER_ID and report_writer_model.strip():
776
- return report_writer_model.strip()
777
- return _require_string(state, "modelExecutionValue")
778
-
779
-
780
762
  def _result_path_for_worker(worker_id: str, result_path: Path, manifest: Mapping[str, Any], project_root: Path) -> Path:
781
763
  if worker_id != REPORT_WRITER_WORKER_ID:
782
764
  return result_path
@@ -259,20 +259,45 @@ def validate(data: Any, schema: dict) -> list[str]:
259
259
  return v.errors
260
260
 
261
261
 
262
+ SCHEMA_FILENAMES = {
263
+ "1.0": "final-report-v1.0.schema.json",
264
+ "2.0": "final-report-v2.0.schema.json",
265
+ }
266
+
267
+
268
+ def _locate_and_load_schema(filename: str, start: Path | None = None) -> dict:
269
+ here = (start or Path(__file__)).resolve()
270
+ if here.is_file():
271
+ here = here.parent
272
+ for parent in [here, *here.parents]:
273
+ candidate = parent / "schemas" / filename
274
+ if candidate.is_file():
275
+ return json.loads(candidate.read_text(encoding="utf-8"))
276
+ raise SchemaError(f"could not locate schemas/{filename}")
277
+
278
+
279
+ def load_schema_version(version: str, start: Path | None = None) -> dict:
280
+ """Load the final-report schema identified by its data version."""
281
+ try:
282
+ filename = SCHEMA_FILENAMES[version]
283
+ except KeyError as exc:
284
+ raise SchemaError(f"unsupported final-report schemaVersion: {version}") from exc
285
+ return _locate_and_load_schema(filename, start=start)
286
+
287
+
288
+ def load_schema_for_data(data: dict, start: Path | None = None) -> dict:
289
+ """Select a schema from the explicit ``schemaVersion`` in *data*."""
290
+ version = data.get("schemaVersion")
291
+ if not isinstance(version, str) or not version:
292
+ raise SchemaError("final-report data has no schemaVersion")
293
+ return load_schema_version(version, start=start)
294
+
295
+
262
296
  def load_schema(schema_path: Path | None = None) -> dict:
263
297
  """Load the final-report schema. If ``schema_path`` is None, locate
264
298
  ``schemas/final-report-v1.0.schema.json`` relative to this file's
265
299
  repo root.
266
300
  """
267
- if schema_path is None:
268
- here = Path(__file__).resolve()
269
- for parent in [here, *here.parents]:
270
- candidate = parent / "schemas" / "final-report-v1.0.schema.json"
271
- if candidate.is_file():
272
- schema_path = candidate
273
- break
274
- if schema_path is None:
275
- raise SchemaError(
276
- "could not locate schemas/final-report-v1.0.schema.json"
277
- )
278
- return json.loads(Path(schema_path).read_text(encoding="utf-8"))
301
+ if schema_path is not None:
302
+ return json.loads(Path(schema_path).read_text(encoding="utf-8"))
303
+ return load_schema_version("1.0")
@@ -1,4 +1,4 @@
1
- """Structured JSONL events emitted by non-Claude lead runtimes."""
1
+ """Structured JSONL events emitted by artifact-accounted lead runtimes."""
2
2
  from __future__ import annotations
3
3
 
4
4
  import json
@@ -24,8 +24,8 @@ class LeadRuntimeInfo:
24
24
  }
25
25
 
26
26
 
27
- ALLOWED_LEAD_RUNTIMES = ("claude-code", "codex", "external")
28
- ARTIFACT_ONLY_LEAD_RUNTIMES = frozenset({"codex", "external"})
27
+ ALLOWED_LEAD_RUNTIMES = ("claude-code", "codex", "antigravity", "external")
28
+ ARTIFACT_ONLY_LEAD_RUNTIMES = frozenset({"codex", "antigravity", "external"})
29
29
 
30
30
  _LEAD_RUNTIMES = {
31
31
  "claude-code": LeadRuntimeInfo(
@@ -50,6 +50,17 @@ _LEAD_RUNTIMES = {
50
50
  has_claude_session=False,
51
51
  adapter_contract_relative_path="lead/adapters/codex.md",
52
52
  ),
53
+ "antigravity": LeadRuntimeInfo(
54
+ runtime="antigravity",
55
+ agent="antigravity",
56
+ agent_label="Antigravity CLI",
57
+ role="Antigravity lead",
58
+ adapter_name="antigravity",
59
+ adapter_dispatch_mode="render-only",
60
+ session_accounting="artifact-only",
61
+ has_claude_session=False,
62
+ adapter_contract_relative_path="lead/adapters/antigravity.md",
63
+ ),
53
64
  "external": LeadRuntimeInfo(
54
65
  runtime="external",
55
66
  agent="external",