okstra 0.147.0 → 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 (116) hide show
  1. package/README.md +21 -7
  2. package/docs/architecture/storage-model.md +34 -61
  3. package/docs/architecture.md +51 -49
  4. package/docs/cli.md +38 -21
  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 +10 -8
  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 +2 -2
  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.sh +3 -0
  29. package/runtime/prompts/lead/adapters/antigravity.md +48 -0
  30. package/runtime/prompts/lead/adapters/claude-code.md +13 -11
  31. package/runtime/prompts/lead/adapters/codex.md +7 -7
  32. package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
  33. package/runtime/prompts/lead/report-writer.md +1 -1
  34. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
  35. package/runtime/prompts/profiles/_common-contract.md +4 -4
  36. package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
  37. package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
  38. package/runtime/prompts/profiles/_implementation-executor.md +12 -12
  39. package/runtime/prompts/profiles/_implementation-self-check.md +4 -4
  40. package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
  41. package/runtime/prompts/profiles/change-impact-analysis.md +2 -0
  42. package/runtime/prompts/profiles/error-analysis.md +2 -0
  43. package/runtime/prompts/profiles/feature-analysis.md +2 -0
  44. package/runtime/prompts/profiles/final-verification.md +3 -1
  45. package/runtime/prompts/profiles/forbidden-actions.json +4 -4
  46. package/runtime/prompts/profiles/implementation-planning.md +3 -1
  47. package/runtime/prompts/profiles/implementation.md +2 -2
  48. package/runtime/prompts/profiles/improvement-discovery.md +3 -1
  49. package/runtime/prompts/profiles/project-analysis.md +2 -0
  50. package/runtime/prompts/profiles/release-handoff.md +7 -7
  51. package/runtime/prompts/profiles/requirements-discovery.md +2 -0
  52. package/runtime/prompts/wizard/prompts.ko.json +9 -1
  53. package/runtime/python/okstra_ctl/codex_dispatch.py +68 -87
  54. package/runtime/python/okstra_ctl/dispatch_core.py +4 -22
  55. package/runtime/python/okstra_ctl/lead_events.py +1 -1
  56. package/runtime/python/okstra_ctl/lead_runtime.py +13 -2
  57. package/runtime/python/okstra_ctl/models.py +156 -8
  58. package/runtime/python/okstra_ctl/path_hints.py +9 -25
  59. package/runtime/python/okstra_ctl/paths.py +1 -1
  60. package/runtime/python/okstra_ctl/render.py +172 -74
  61. package/runtime/python/okstra_ctl/report_html/common.py +38 -2
  62. package/runtime/python/okstra_ctl/report_html/filters.py +104 -0
  63. package/runtime/python/okstra_ctl/report_html/render.py +7 -0
  64. package/runtime/python/okstra_ctl/report_html/view_models/change_impact_analysis.py +2 -1
  65. package/runtime/python/okstra_ctl/report_html/view_models/error_analysis.py +2 -1
  66. package/runtime/python/okstra_ctl/report_html/view_models/feature_analysis.py +2 -1
  67. package/runtime/python/okstra_ctl/report_html/view_models/final_verification.py +2 -1
  68. package/runtime/python/okstra_ctl/report_html/view_models/implementation.py +2 -1
  69. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +2 -1
  70. package/runtime/python/okstra_ctl/report_html/view_models/improvement_discovery.py +2 -1
  71. package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +2 -1
  72. package/runtime/python/okstra_ctl/report_html/view_models/requirements_discovery.py +2 -1
  73. package/runtime/python/okstra_ctl/report_html/visualizations.py +32 -6
  74. package/runtime/python/okstra_ctl/run.py +264 -45
  75. package/runtime/python/okstra_ctl/runner_resolution.py +103 -0
  76. package/runtime/python/okstra_ctl/team.py +2 -7
  77. package/runtime/python/okstra_ctl/wizard.py +194 -21
  78. package/runtime/python/okstra_ctl/worker_artifacts.py +46 -0
  79. package/runtime/python/okstra_ctl/workers.py +3 -1
  80. package/runtime/python/okstra_ctl/workflow.py +4 -2
  81. package/runtime/python/okstra_token_usage/__init__.py +1 -0
  82. package/runtime/python/okstra_token_usage/collect.py +32 -23
  83. package/runtime/python/okstra_token_usage/pricing.py +35 -3
  84. package/runtime/schemas/final-report-v2.0.schema.json +2 -2
  85. package/runtime/skills/okstra-run/SKILL.md +31 -42
  86. package/runtime/templates/prd/pr-body.template.md +1 -1
  87. package/runtime/templates/reports/html/assets/base.css +6 -3
  88. package/runtime/templates/reports/html/base.template.html +22 -8
  89. package/runtime/templates/reports/html/macros/forms.html +2 -2
  90. package/runtime/templates/reports/html/macros/layout.html +5 -5
  91. package/runtime/templates/reports/html/macros/visualizations.html +11 -1
  92. package/runtime/templates/reports/html/tasks/change-impact-analysis.template.html +11 -11
  93. package/runtime/templates/reports/html/tasks/error-analysis.template.html +12 -12
  94. package/runtime/templates/reports/html/tasks/feature-analysis.template.html +9 -9
  95. package/runtime/templates/reports/html/tasks/final-verification.template.html +7 -7
  96. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +12 -12
  97. package/runtime/templates/reports/html/tasks/implementation.template.html +7 -7
  98. package/runtime/templates/reports/html/tasks/improvement-discovery.template.html +6 -6
  99. package/runtime/templates/reports/html/tasks/project-analysis.template.html +11 -11
  100. package/runtime/templates/reports/html/tasks/release-handoff.template.html +5 -5
  101. package/runtime/templates/reports/html/tasks/requirements-discovery.template.html +8 -8
  102. package/runtime/templates/reports/report.js +21 -4
  103. package/runtime/templates/reports/settings.template.json +4 -0
  104. package/runtime/templates/reports/task-brief.template.md +7 -7
  105. package/runtime/validators/validate-run.py +11 -6
  106. package/runtime/validators/validate_session_conformance.py +2 -1
  107. package/src/cli-registry.mjs +4 -4
  108. package/src/commands/execute/codex-dispatch.mjs +7 -10
  109. package/src/commands/execute/render-bundle.mjs +3 -3
  110. package/src/commands/execute/run.mjs +17 -52
  111. package/src/commands/execute/wizard.mjs +4 -1
  112. package/src/commands/lifecycle/doctor.mjs +6 -3
  113. package/src/commands/lifecycle/install.mjs +31 -8
  114. package/src/lib/runtime-manifest.mjs +1 -1
  115. package/src/lib/runtime-resolver.mjs +2 -2
  116. 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
@@ -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",
@@ -6,14 +6,14 @@ role default = one ROLE_DEFAULTS line here.
6
6
  from __future__ import annotations
7
7
 
8
8
  from dataclasses import dataclass
9
- from typing import Optional
9
+ from typing import Mapping, Optional
10
10
 
11
11
 
12
12
  @dataclass(frozen=True)
13
13
  class ModelSpec:
14
14
  display: str
15
15
  execution: str
16
- pricing: Optional[tuple] = None # codex only: (input, cached_input, output) USD/1M
16
+ pricing: Optional[tuple[float, float, float]] = None
17
17
  in_picker: bool = False # exposed in the wizard 3-option picker
18
18
 
19
19
 
@@ -31,6 +31,8 @@ CLAUDE = {
31
31
  "opus-4-6": ModelSpec("opus-4-6", "claude-opus-4-6"),
32
32
  "claude-opus-4-6": ModelSpec("opus-4-6", "claude-opus-4-6"),
33
33
  "sonnet": ModelSpec("sonnet", "sonnet", in_picker=True),
34
+ "sonnet-5": ModelSpec("sonnet-5", "claude-sonnet-5", in_picker=True),
35
+ "claude-sonnet-5": ModelSpec("sonnet-5", "claude-sonnet-5"),
34
36
  "sonnet-4-6": ModelSpec("sonnet-4-6", "claude-sonnet-4-6"),
35
37
  "claude-sonnet-4-6": ModelSpec("sonnet-4-6", "claude-sonnet-4-6"),
36
38
  "haiku": ModelSpec("haiku", "haiku", in_picker=True),
@@ -47,6 +49,8 @@ CLAUDE = {
47
49
  ANTIGRAVITY = {
48
50
  "gemini-3.1-pro": ModelSpec("Gemini 3.1 Pro", "gemini-3.1-pro", in_picker=True),
49
51
  "gemini 3.1 pro": ModelSpec("Gemini 3.1 Pro", "gemini-3.1-pro"),
52
+ "gemini-3.6-flash": ModelSpec("Gemini 3.6 Flash", "gemini-3.6-flash", in_picker=True),
53
+ "gemini 3.6 flash": ModelSpec("Gemini 3.6 Flash", "gemini-3.6-flash"),
50
54
  "gemini-3.5-flash": ModelSpec("Gemini 3.5 Flash", "gemini-3.5-flash", in_picker=True),
51
55
  "gemini 3.5 flash": ModelSpec("Gemini 3.5 Flash", "gemini-3.5-flash"),
52
56
  }
@@ -61,12 +65,111 @@ CODEX = {
61
65
  "gpt-5.2": ModelSpec("gpt-5.2", "gpt-5.2", (1.75, 0.175, 14.0)),
62
66
  "codex-auto-review": ModelSpec("codex-auto-review", "codex-auto-review"),
63
67
  }
64
- PROVIDER_MAPPINGS = {"claude": CLAUDE, "antigravity": ANTIGRAVITY, "codex": CODEX}
68
+ GROK = {
69
+ "grok-build-0.1": ModelSpec(
70
+ "grok-build-0.1", "grok-build-0.1", (1.00, 0.20, 2.00), in_picker=True,
71
+ ),
72
+ "grok-4.5": ModelSpec(
73
+ "grok-4.5", "grok-4.5", (2.00, 0.30, 6.00), in_picker=True,
74
+ ),
75
+ }
76
+ KIMI = {
77
+ "kimi-k2.7-code": ModelSpec(
78
+ "Kimi K2.7 Code", "kimi-k2.7-code", (0.95, 0.19, 4.00), in_picker=True,
79
+ ),
80
+ "kimi k2.7 code": ModelSpec("Kimi K2.7 Code", "kimi-k2.7-code"),
81
+ "kimi-for-coding": ModelSpec(
82
+ "Kimi K2.7 Code", "kimi-for-coding", in_picker=True,
83
+ ),
84
+ "kimi-k3": ModelSpec(
85
+ "Kimi K3", "kimi-k3", (3.00, 0.30, 15.00), in_picker=True,
86
+ ),
87
+ "kimi k3": ModelSpec("Kimi K3", "kimi-k3"),
88
+ "k3": ModelSpec("Kimi K3", "k3", in_picker=True),
89
+ "k3-256k": ModelSpec("Kimi K3 256K", "k3-256k", in_picker=True),
90
+ "kimi k3 256k": ModelSpec("Kimi K3 256K", "k3-256k"),
91
+ }
92
+
93
+ ANALYSIS_ROLES = frozenset({"analyser", "critic"})
94
+ IMPLEMENTATION_ROLES = frozenset({"executor", "verifier"})
95
+
96
+
97
+ @dataclass(frozen=True)
98
+ class ProviderSpec:
99
+ provider: str
100
+ display_label: str
101
+ models: Mapping[str, ModelSpec]
102
+ default_models: Mapping[str, str]
103
+ wrapper: str
104
+ supported_roles: frozenset[str]
65
105
 
66
- # ROLE-keyed (not provider): 1:1 with run.py recommended_role_models. "lead" uses
67
- # the claude provider; "report-writer" resolves via the claude provider too.
68
- # report-writer defaults below the lead tier on purpose: it does not vote in
69
- # convergence, and its wall-clock is dominated by sheer report generation.
106
+
107
+ PROVIDERS = {
108
+ "claude": ProviderSpec(
109
+ provider="claude",
110
+ display_label="Claude",
111
+ models=CLAUDE,
112
+ default_models={
113
+ "lead": "opus",
114
+ "analyser": "opus",
115
+ "critic": "opus",
116
+ "executor": "opus",
117
+ "verifier": "opus",
118
+ "report-writer": "sonnet",
119
+ },
120
+ wrapper="okstra-claude-exec.sh",
121
+ supported_roles=frozenset(
122
+ {"lead", "analyser", "critic", "executor", "verifier", "report-writer"}
123
+ ),
124
+ ),
125
+ "antigravity": ProviderSpec(
126
+ provider="antigravity",
127
+ display_label="Antigravity",
128
+ models=ANTIGRAVITY,
129
+ default_models={role: "gemini-3.1-pro" for role in (
130
+ "lead", "analyser", "critic", "executor", "verifier",
131
+ )},
132
+ wrapper="okstra-antigravity-exec.sh",
133
+ supported_roles=frozenset(
134
+ {"lead", "analyser", "critic", "executor", "verifier"}
135
+ ),
136
+ ),
137
+ "codex": ProviderSpec(
138
+ provider="codex",
139
+ display_label="Codex",
140
+ models=CODEX,
141
+ default_models={role: "gpt-5.6-sol" for role in (
142
+ "lead", "analyser", "critic", "executor", "verifier", "report-writer",
143
+ )},
144
+ wrapper="okstra-codex-exec.sh",
145
+ supported_roles=frozenset(
146
+ {"lead", "analyser", "critic", "executor", "verifier", "report-writer"}
147
+ ),
148
+ ),
149
+ "grok": ProviderSpec(
150
+ provider="grok",
151
+ display_label="Grok",
152
+ models=GROK,
153
+ default_models={"analyser": "grok-build-0.1", "critic": "grok-4.5"},
154
+ wrapper="okstra-grok-exec.sh",
155
+ supported_roles=ANALYSIS_ROLES,
156
+ ),
157
+ "kimi": ProviderSpec(
158
+ provider="kimi",
159
+ display_label="Kimi",
160
+ models=KIMI,
161
+ default_models={"analyser": "kimi-k2.7-code", "critic": "kimi-k3"},
162
+ wrapper="okstra-kimi-exec.sh",
163
+ supported_roles=ANALYSIS_ROLES,
164
+ ),
165
+ }
166
+
167
+ PROVIDER_MAPPINGS = {
168
+ provider: spec.models for provider, spec in PROVIDERS.items()
169
+ }
170
+
171
+ # Legacy role-keyed compatibility defaults. Canonical assignments resolve a
172
+ # provider first and then use ProviderSpec.default_models for that role.
70
173
  ROLE_DEFAULTS = {
71
174
  "lead": "opus", "claude": "opus", "codex": "gpt-5.6-sol",
72
175
  "antigravity": "gemini-3.1-pro", "report-writer": "sonnet",
@@ -83,6 +186,51 @@ class UnknownModelError(ValueError):
83
186
  """raw_value 가 provider mapping 에 등록되어 있지 않을 때 발생."""
84
187
 
85
188
 
189
+ class UnknownProviderError(ValueError):
190
+ """Raised when a requested provider is absent from the registry."""
191
+
192
+
193
+ def provider_spec(provider: str) -> ProviderSpec:
194
+ """Return one registered provider or fail before assignment/dispatch."""
195
+ normalized = (provider or "").strip().lower()
196
+ try:
197
+ return PROVIDERS[normalized]
198
+ except KeyError as exc:
199
+ allowed = ", ".join(PROVIDERS)
200
+ raise UnknownProviderError(
201
+ f"unknown provider {provider!r}. Allowed values: {allowed}"
202
+ ) from exc
203
+
204
+
205
+ def provider_ids(role: str | None = None) -> list[str]:
206
+ """Registered provider ids, optionally filtered by functional role."""
207
+ if not role:
208
+ return list(PROVIDERS)
209
+ return [
210
+ provider for provider, spec in PROVIDERS.items()
211
+ if role in spec.supported_roles
212
+ ]
213
+
214
+
215
+ def provider_supports_role(provider: str, role: str) -> bool:
216
+ """Whether the provider has passed the registry gate for a role."""
217
+ return role in provider_spec(provider).supported_roles
218
+
219
+
220
+ def provider_wrappers(role: str | None = None) -> dict[str, str]:
221
+ """Provider-to-wrapper mapping derived from capability entries."""
222
+ return {
223
+ provider: PROVIDERS[provider].wrapper
224
+ for provider in provider_ids(role)
225
+ if PROVIDERS[provider].wrapper
226
+ }
227
+
228
+
229
+ def provider_default_model(provider: str, role: str) -> Optional[str]:
230
+ """Resolve a provider's explicit default for one functional role."""
231
+ return provider_spec(provider).default_models.get(role)
232
+
233
+
86
234
  def resolve_model_metadata(
87
235
  *, provider: str, raw_value: str, default_display: str, default_execution: str,
88
236
  ) -> ModelAssignment:
@@ -120,6 +268,6 @@ def default_model(role: str) -> Optional[str]:
120
268
 
121
269
 
122
270
  def catalog_pricing(provider: str) -> dict:
123
- """Selectable-model pricing for a provider (codex only carries pricing)."""
271
+ """Selectable-model pricing as (input, cached input, output) USD/1M."""
124
272
  mapping = PROVIDER_MAPPINGS.get(provider, {})
125
273
  return {alias: spec.pricing for alias, spec in mapping.items() if spec.pricing is not None}