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
@@ -53,13 +53,31 @@ from .material import (
53
53
  related_tasks_inline,
54
54
  resolve_related_tasks,
55
55
  )
56
- from .final_report_schema import load_schema
56
+ from .final_report_schema import load_schema_version
57
57
  from .final_report_paths import final_report_data_path as _final_report_data_path
58
58
  from .lead_events import LeadEvent, append_lead_event
59
- from .lead_runtime import ALLOWED_LEAD_RUNTIMES
59
+ from .lead_runtime import (
60
+ ALLOWED_LEAD_RUNTIMES,
61
+ ARTIFACT_ONLY_LEAD_RUNTIMES,
62
+ lead_runtime_info,
63
+ )
60
64
  from .model_discovery import normalize_execution_for_dispatch
61
- from .models import ModelAssignment, default_model, resolve_model_metadata
65
+ from .models import (
66
+ ModelAssignment,
67
+ default_model,
68
+ provider_default_model,
69
+ provider_ids,
70
+ provider_spec,
71
+ provider_supports_role,
72
+ resolve_model_metadata,
73
+ )
62
74
  from .schema_excerpt import build_schema_excerpt
75
+ from .runner_resolution import (
76
+ RunnerResolutionError,
77
+ native_provider_for_host,
78
+ resolve_runner,
79
+ )
80
+ from .report_contract import CURRENT_REPORT_SCHEMA_VERSION
63
81
  from .path_resolve import relative_to_project_root, resolve_user_file
64
82
  from .render import (
65
83
  apply_lead_prompt_defaults,
@@ -264,11 +282,14 @@ def _set_data_json_approved_true_if_present(path: Path) -> bool:
264
282
  try:
265
283
  from .render_final_report import (
266
284
  FinalReportRenderError,
267
- find_default_template,
285
+ find_default_template_for_data,
268
286
  render,
269
287
  )
270
288
 
271
- rendered = render(data, template_path=find_default_template())
289
+ rendered = render(
290
+ data,
291
+ template_path=find_default_template_for_data(data),
292
+ )
272
293
  except FinalReportRenderError as exc:
273
294
  raise PrepareError(
274
295
  f"--approve could not re-render approved plan from data.json: {exc}"
@@ -325,10 +346,13 @@ class PrepareInputs:
325
346
  evidence_inputs_raw: str = ""
326
347
  directive: str = ""
327
348
  workers_override: str = ""
349
+ lead_provider: str = ""
328
350
  lead_model: str = ""
329
351
  claude_model: str = ""
330
352
  codex_model: str = ""
331
353
  antigravity_model: str = ""
354
+ worker_models_raw: str = ""
355
+ report_writer_provider: str = ""
332
356
  report_writer_model: str = ""
333
357
  lead_runtime: str = "claude-code"
334
358
  lead_runtime_request: str = ""
@@ -739,7 +763,7 @@ def _brief_sha256(path: Path) -> str:
739
763
 
740
764
 
741
765
  def _record_artifact_runtime_render_only_event(inp: PrepareInputs, ctx: dict) -> None:
742
- if inp.lead_runtime not in ("codex", "external") or not inp.render_only:
766
+ if inp.lead_runtime not in ARTIFACT_ONLY_LEAD_RUNTIMES or not inp.render_only:
743
767
  return
744
768
  append_lead_event(
745
769
  Path(ctx["LEAD_EVENTS_PATH"]),
@@ -777,10 +801,16 @@ def _canonical_argv(inp: PrepareInputs, ctx: dict) -> list[str]:
777
801
  ("--implementation-option", inp.implementation_option),
778
802
  ("--clarification-response", inp.clarification_response_path),
779
803
  ("--workers", workers),
804
+ ("--lead-provider", inp.lead_provider or ctx.get("LEAD_PROVIDER", "")),
780
805
  ("--lead-model", inp.lead_model or ctx.get("LEAD_MODEL", "")),
781
806
  ("--claude-model", inp.claude_model or ctx.get("CLAUDE_WORKER_MODEL", "")),
782
807
  ("--codex-model", inp.codex_model or ctx.get("CODEX_WORKER_MODEL", "")),
783
808
  ("--antigravity-model", inp.antigravity_model or ctx.get("ANTIGRAVITY_WORKER_MODEL", "")),
809
+ ("--worker-model", inp.worker_models_raw),
810
+ (
811
+ "--report-writer-provider",
812
+ inp.report_writer_provider or ctx.get("REPORT_WRITER_PROVIDER", ""),
813
+ ),
784
814
  ("--report-writer-model", inp.report_writer_model or ctx.get("REPORT_WRITER_MODEL", "")),
785
815
  ("--lead-runtime", inp.lead_runtime if inp.lead_runtime != "claude-code" else ""),
786
816
  ("--executor", inp.executor or ctx.get("EXECUTOR_PROVIDER", "")),
@@ -892,7 +922,7 @@ def _resolve_runtime_assets(workspace_root: Path, inp: PrepareInputs) -> _Resolv
892
922
  workspace_root / "templates" / "project-docs" / "task-index.template.md"
893
923
  )
894
924
  final_report_template = (
895
- workspace_root / "templates" / "reports" / "final-report.template.md"
925
+ workspace_root / "templates" / "reports" / "final-report-v2.template.md"
896
926
  )
897
927
  lead_contract = workspace_root / "prompts" / "lead" / "okstra-lead-contract.md"
898
928
  run_validator = workspace_root / "validators" / "validate-run.py"
@@ -1317,6 +1347,28 @@ def _resolve_pr_template(inp: PrepareInputs) -> tuple[str, str]:
1317
1347
  return str(resolved_tpl.path), resolved_tpl.source
1318
1348
 
1319
1349
 
1350
+ @dataclass
1351
+ class RoleAssignment:
1352
+ role: str
1353
+ provider: str
1354
+ model_display: str
1355
+ model_execution_value: str
1356
+ runner: str
1357
+ worker_id: str = ""
1358
+
1359
+ def to_payload(self) -> dict[str, str]:
1360
+ payload = {
1361
+ "role": self.role,
1362
+ "provider": self.provider,
1363
+ "model": self.model_display,
1364
+ "modelExecutionValue": self.model_execution_value,
1365
+ "runner": self.runner,
1366
+ }
1367
+ if self.worker_id:
1368
+ payload["workerId"] = self.worker_id
1369
+ return payload
1370
+
1371
+
1320
1372
  @dataclass
1321
1373
  class _ModelBindings:
1322
1374
  """이 run 의 lead/worker 모델 + critic + executor 바인딩.
@@ -1332,6 +1384,7 @@ class _ModelBindings:
1332
1384
  ge: ModelAssignment
1333
1385
  rw: ModelAssignment
1334
1386
  critic_choice: str
1387
+ critic_model_execution: str
1335
1388
  executor_provider: str
1336
1389
  executor_display_name: str
1337
1390
  executor_worker_agent: str
@@ -1339,45 +1392,125 @@ class _ModelBindings:
1339
1392
  codex_worker_execution: str
1340
1393
  antigravity_worker_execution: str
1341
1394
  executor_execution: str
1395
+ lead_assignment: RoleAssignment
1396
+ worker_assignments: tuple[RoleAssignment, ...]
1342
1397
 
1343
1398
 
1344
1399
  def recommended_role_models() -> dict[str, str]:
1345
1400
  """역할 → 추천 모델 display 값 (env override 반영). prepare 의 모델 해소와
1346
1401
  wizard 의 안내 표기가 공유하는 단일 기준점."""
1347
1402
  lead_default = _default("OKSTRA_DEFAULT_LEAD_MODEL", default_model("lead"))
1348
- return {
1403
+ recommendations = {
1349
1404
  "lead": lead_default,
1350
1405
  "claude": _default("OKSTRA_DEFAULT_CLAUDE_MODEL", default_model("claude")),
1351
1406
  "codex": _default("OKSTRA_DEFAULT_CODEX_MODEL", default_model("codex")),
1352
1407
  "antigravity": _default("OKSTRA_DEFAULT_ANTIGRAVITY_MODEL", default_model("antigravity")),
1353
1408
  "report-writer": _default("OKSTRA_DEFAULT_REPORT_WRITER_MODEL", default_model("report-writer")),
1354
1409
  }
1410
+ for provider in provider_ids("analyser"):
1411
+ recommendations.setdefault(
1412
+ provider, provider_default_model(provider, "analyser")
1413
+ )
1414
+ return recommendations
1415
+
1416
+
1417
+ def _parse_worker_model_overrides(raw_value: str) -> dict[str, str]:
1418
+ overrides: dict[str, str] = {}
1419
+ for item in (raw_value or "").split(","):
1420
+ item = item.strip()
1421
+ if not item:
1422
+ continue
1423
+ provider, separator, model = item.partition("=")
1424
+ provider = provider.strip().lower()
1425
+ model = model.strip()
1426
+ if not separator or not provider or not model:
1427
+ raise PrepareError(
1428
+ "--worker-model must use provider=model entries separated by commas"
1429
+ )
1430
+ if provider in overrides:
1431
+ raise PrepareError(f"duplicate --worker-model provider: {provider}")
1432
+ provider_spec(provider)
1433
+ overrides[provider] = model
1434
+ return overrides
1435
+
1436
+
1437
+ def _model_for_role(
1438
+ provider: str, raw_value: str, role: str, default_override: str = "",
1439
+ ) -> ModelAssignment:
1440
+ default_value = default_override or provider_default_model(provider, role)
1441
+ if not default_value:
1442
+ raise PrepareError(f"provider {provider!r} does not define a default for role {role!r}")
1443
+ return resolve_model_metadata(
1444
+ provider=provider,
1445
+ raw_value=raw_value,
1446
+ default_display=default_value,
1447
+ default_execution=default_value,
1448
+ )
1449
+
1450
+
1451
+ def _assignment_runner(lead_runtime: str, provider: str, role: str) -> str:
1452
+ try:
1453
+ return resolve_runner(lead_runtime, provider, role).runner
1454
+ except RunnerResolutionError as exc:
1455
+ raise PrepareError(str(exc)) from exc
1355
1456
 
1356
1457
 
1357
1458
  def _resolve_worker_models(inp: PrepareInputs) -> dict:
1358
- """lead/claude/codex/antigravity/report-writer 모델을 alias → (display, execution) 로 해소."""
1459
+ """Resolve provider models while preserving legacy fixed-provider projections."""
1359
1460
  rec = recommended_role_models()
1461
+ overrides = _parse_worker_model_overrides(inp.worker_models_raw)
1462
+ legacy = {
1463
+ "claude": inp.claude_model,
1464
+ "codex": inp.codex_model,
1465
+ "antigravity": inp.antigravity_model,
1466
+ }
1467
+ provider_models = {}
1468
+ for provider in provider_ids("analyser"):
1469
+ default_override = rec.get(provider, "")
1470
+ provider_models[provider] = _model_for_role(
1471
+ provider, overrides.get(provider, legacy.get(provider, "")),
1472
+ "analyser", default_override,
1473
+ )
1474
+ native_lead_provider = native_provider_for_host(inp.lead_runtime)
1475
+ requested_lead_provider = inp.lead_provider.strip().lower()
1476
+ if (
1477
+ native_lead_provider
1478
+ and requested_lead_provider
1479
+ and requested_lead_provider != native_lead_provider
1480
+ ):
1481
+ host_label = lead_runtime_info(inp.lead_runtime).agent_label
1482
+ raise PrepareError(
1483
+ f"{host_label.removesuffix(' CLI')} host requires provider "
1484
+ f"{native_lead_provider!r} for the in-session lead; provider "
1485
+ f"{requested_lead_provider!r} must be selected as a worker instead"
1486
+ )
1487
+ lead_provider = native_lead_provider or requested_lead_provider or "claude"
1488
+ report_writer_provider = inp.report_writer_provider.strip().lower() or "claude"
1489
+ if not provider_supports_role(lead_provider, "lead"):
1490
+ raise PrepareError(f"provider {lead_provider!r} does not support the lead role")
1491
+ if not provider_supports_role(report_writer_provider, "report-writer"):
1492
+ raise PrepareError(
1493
+ f"provider {report_writer_provider!r} does not support the report-writer role"
1494
+ )
1360
1495
  return {
1361
- "lead": resolve_model_metadata(
1362
- provider="claude", raw_value=inp.lead_model,
1363
- default_display=rec["lead"], default_execution=rec["lead"],
1364
- ),
1365
- "cw": resolve_model_metadata(
1366
- provider="claude", raw_value=inp.claude_model,
1367
- default_display=rec["claude"], default_execution=rec["claude"],
1496
+ "lead_provider": lead_provider,
1497
+ "report_writer_provider": report_writer_provider,
1498
+ "lead": _model_for_role(
1499
+ lead_provider, inp.lead_model, "lead",
1500
+ rec["lead"] if lead_provider == "claude" else "",
1368
1501
  ),
1369
- "co": resolve_model_metadata(
1370
- provider="codex", raw_value=inp.codex_model,
1371
- default_display=rec["codex"], default_execution=rec["codex"],
1372
- ),
1373
- "ge": resolve_model_metadata(
1374
- provider="antigravity", raw_value=inp.antigravity_model,
1375
- default_display=rec["antigravity"], default_execution=rec["antigravity"],
1376
- ),
1377
- "rw": resolve_model_metadata(
1378
- provider="claude", raw_value=inp.report_writer_model,
1379
- default_display=rec["report-writer"], default_execution=rec["report-writer"],
1502
+ "cw": provider_models["claude"],
1503
+ "co": provider_models["codex"],
1504
+ "ge": provider_models["antigravity"],
1505
+ "rw": _model_for_role(
1506
+ report_writer_provider, inp.report_writer_model, "report-writer",
1507
+ rec["report-writer"] if report_writer_provider == "claude" else "",
1380
1508
  ),
1509
+ "providers": provider_models,
1510
+ "worker_model_inputs": {
1511
+ provider: overrides.get(provider, legacy.get(provider, ""))
1512
+ for provider in provider_ids("analyser")
1513
+ },
1381
1514
  }
1382
1515
 
1383
1516
 
@@ -1385,15 +1518,29 @@ def _resolve_model_bindings(inp: PrepareInputs, workers: list[str]) -> _ModelBin
1385
1518
  """worker 모델 + critic 선택 + executor 바인딩을 한 묶음으로 해소·검증한다."""
1386
1519
  m = _resolve_worker_models(inp)
1387
1520
  critic_choice = (inp.critic or "").strip().lower()
1388
- if critic_choice not in ("", "off", "claude", "codex", "antigravity"):
1521
+ allowed_critics = ["", "off", *provider_ids("critic")]
1522
+ if critic_choice not in allowed_critics:
1389
1523
  raise PrepareError(
1390
- f"--critic must be one of: off, claude, codex, antigravity (got: {critic_choice!r})"
1524
+ f"--critic must be one of: {', '.join(allowed_critics[1:])} "
1525
+ f"(got: {critic_choice!r})"
1526
+ )
1527
+ critic_model_execution = ""
1528
+ if critic_choice in provider_ids("critic"):
1529
+ critic_meta = _model_for_role(
1530
+ critic_choice,
1531
+ m["worker_model_inputs"].get(critic_choice, ""),
1532
+ "critic",
1533
+ )
1534
+ critic_model_execution = _normalized_execution(
1535
+ critic_meta, provider=critic_choice, role="critic",
1391
1536
  )
1392
1537
  executor_default = _default("OKSTRA_DEFAULT_EXECUTOR", "claude")
1393
1538
  executor_provider = (inp.executor or executor_default).strip().lower()
1394
- if executor_provider not in ("claude", "codex", "antigravity"):
1539
+ allowed_executors = provider_ids("executor")
1540
+ if executor_provider not in allowed_executors:
1395
1541
  raise PrepareError(
1396
- f"--executor must be one of: claude, codex, antigravity (got: {executor_provider!r})"
1542
+ f"--executor must be one of: {', '.join(allowed_executors)} "
1543
+ f"(got: {executor_provider!r})"
1397
1544
  )
1398
1545
  if inp.task_type == "implementation" and executor_provider not in workers:
1399
1546
  raise PrepareError(
@@ -1401,15 +1548,21 @@ def _resolve_model_bindings(inp: PrepareInputs, workers: list[str]) -> _ModelBin
1401
1548
  f"--workers, but resolved roster is {workers!r}. "
1402
1549
  f"Add it explicitly, e.g. --workers {','.join(sorted(set(workers + [executor_provider])))}."
1403
1550
  )
1404
- provider_to_meta = {
1405
- "claude": ("Claude executor", "claude-worker", m["cw"]),
1406
- "codex": ("Codex executor", "codex-worker", m["co"]),
1407
- "antigravity": ("Antigravity executor", "antigravity-worker", m["ge"]),
1408
- }
1409
- display_name, worker_agent, model_meta = provider_to_meta[executor_provider]
1551
+ model_meta = m["providers"][executor_provider]
1552
+ display_name = f"{provider_spec(executor_provider).display_label} executor"
1553
+ worker_agent = f"{executor_provider}-worker"
1554
+ lead_provider = m["lead_provider"]
1555
+ lead_execution = m["lead"].execution
1556
+ lead_runner = _assignment_runner(inp.lead_runtime, lead_provider, "lead")
1557
+ if lead_runner == "cli-wrapper":
1558
+ lead_execution = _normalized_execution(
1559
+ m["lead"], provider=lead_provider, role="lead",
1560
+ )
1561
+ worker_assignments = _build_worker_assignments(inp, workers, m)
1410
1562
  return _ModelBindings(
1411
1563
  lead=m["lead"], cw=m["cw"], co=m["co"], ge=m["ge"], rw=m["rw"],
1412
1564
  critic_choice=critic_choice,
1565
+ critic_model_execution=critic_model_execution,
1413
1566
  executor_provider=executor_provider,
1414
1567
  executor_display_name=display_name,
1415
1568
  executor_worker_agent=worker_agent,
@@ -1420,9 +1573,42 @@ def _resolve_model_bindings(inp: PrepareInputs, workers: list[str]) -> _ModelBin
1420
1573
  m["ge"], "antigravity", "worker", workers),
1421
1574
  executor_execution=_normalized_execution(
1422
1575
  model_meta, provider=executor_provider, role="executor"),
1576
+ lead_assignment=RoleAssignment(
1577
+ role="lead",
1578
+ provider=lead_provider,
1579
+ model_display=m["lead"].display,
1580
+ model_execution_value=lead_execution,
1581
+ runner=lead_runner,
1582
+ ),
1583
+ worker_assignments=worker_assignments,
1423
1584
  )
1424
1585
 
1425
1586
 
1587
+ def _build_worker_assignments(
1588
+ inp: PrepareInputs, workers: list[str], models: dict,
1589
+ ) -> tuple[RoleAssignment, ...]:
1590
+ assignments: list[RoleAssignment] = []
1591
+ for worker_id in workers:
1592
+ is_report_writer = worker_id == "report-writer"
1593
+ provider = (
1594
+ models["report_writer_provider"] if is_report_writer else worker_id
1595
+ )
1596
+ role = "report-writer" if is_report_writer else "analyser"
1597
+ meta = models["rw"] if is_report_writer else models["providers"][provider]
1598
+ execution = _worker_execution_value(
1599
+ meta, provider, "worker", [provider],
1600
+ )
1601
+ assignments.append(RoleAssignment(
1602
+ role=role,
1603
+ provider=provider,
1604
+ model_display=meta.display,
1605
+ model_execution_value=execution,
1606
+ runner=_assignment_runner(inp.lead_runtime, provider, role),
1607
+ worker_id=worker_id,
1608
+ ))
1609
+ return tuple(assignments)
1610
+
1611
+
1426
1612
  def _normalized_execution(meta: ModelAssignment, provider: str, role: str) -> str:
1427
1613
  """Route a role's catalog execution value through the CLI-identity normalizer
1428
1614
  so the manifest carries the dispatch-correct spelling. SSOT for the mapping
@@ -1756,7 +1942,7 @@ def _render_lead_prompt_and_snapshot(
1756
1942
  """compute/default 토큰을 ctx 에 주입한 뒤 final-report 템플릿 사본과 lead 실행
1757
1943
  프롬프트를 렌더하고, 프롬프트 스냅샷을 기록한 후 prompt_text 를 돌려준다."""
1758
1944
  # inject populates ctx with compute + default tokens consumed by the lead
1759
- # prompt render below (claude-execution-prompt.md). The final-report
1945
+ # prompt render below (lead-execution-prompt.md). The final-report
1760
1946
  # template render is effectively a copy (Jinja2 `{{ var }}` syntax does
1761
1947
  # not match `_TOKEN_RE`); routed through render_template_with_ctx for SOT
1762
1948
  # consistency.
@@ -1766,7 +1952,7 @@ def _render_lead_prompt_and_snapshot(
1766
1952
  str(final_report_template), ctx["FINAL_REPORT_TEMPLATE_PATH"], ctx,
1767
1953
  )
1768
1954
  # Per-task-type schema excerpt for the report-writer worker. The full
1769
- # schema validates the data.json post-hoc (load_schema); the worker only
1955
+ # schema validates the data.json post-hoc; the worker only
1770
1956
  # needs the common structure + this run's task-type block, so we write a
1771
1957
  # scoped excerpt into the instruction-set rather than make the worker read
1772
1958
  # the whole 44 KB / all-task-types schema (whose repo `schemas/...` path is
@@ -1779,7 +1965,9 @@ def _render_lead_prompt_and_snapshot(
1779
1965
  # validation runs against the full schema regardless.
1780
1966
  try:
1781
1967
  _excerpt = build_schema_excerpt(
1782
- load_schema(), inp.task_type, installed_version()
1968
+ load_schema_version(CURRENT_REPORT_SCHEMA_VERSION),
1969
+ inp.task_type,
1970
+ installed_version(),
1783
1971
  )
1784
1972
  Path(ctx["FINAL_REPORT_SCHEMA_PATH"]).write_text(
1785
1973
  json.dumps(_excerpt, indent=2, ensure_ascii=False) + "\n",
@@ -1787,10 +1975,11 @@ def _render_lead_prompt_and_snapshot(
1787
1975
  )
1788
1976
  except Exception: # noqa: BLE001 — advisory artifact; never fail prep over it
1789
1977
  pass
1790
- render_template_with_ctx(
1791
- str(prompt_template), str(instruction_set / "claude-execution-prompt.md"), ctx,
1792
- )
1793
- prompt_text = (instruction_set / "claude-execution-prompt.md").read_text(encoding="utf-8")
1978
+ lead_prompt_path = instruction_set / "lead-execution-prompt.md"
1979
+ legacy_claude_prompt_path = instruction_set / "claude-execution-prompt.md"
1980
+ render_template_with_ctx(str(prompt_template), str(lead_prompt_path), ctx)
1981
+ prompt_text = lead_prompt_path.read_text(encoding="utf-8")
1982
+ legacy_claude_prompt_path.write_text(prompt_text, encoding="utf-8")
1794
1983
  Path(ctx["RUN_PROMPT_SNAPSHOT_FILE"]).parent.mkdir(parents=True, exist_ok=True)
1795
1984
  Path(ctx["RUN_PROMPT_SNAPSHOT_FILE"]).write_text(prompt_text, encoding="utf-8")
1796
1985
  return prompt_text
@@ -1819,10 +2008,22 @@ def _persist_run_inputs(
1819
2008
  "taskBriefAbsolutePath": str(inp.brief_path),
1820
2009
  "directive": inp.directive,
1821
2010
  "workers": selected_reviewers,
2011
+ "leadProvider": models.lead_assignment.provider,
1822
2012
  "leadModel": models.lead.display,
1823
2013
  "claudeModel": models.cw.display,
1824
2014
  "codexModel": models.co.display,
1825
2015
  "antigravityModel": models.ge.display,
2016
+ "workerAssignments": [
2017
+ assignment.to_payload() for assignment in models.worker_assignments
2018
+ ],
2019
+ "reportWriterProvider": next(
2020
+ (
2021
+ assignment.provider
2022
+ for assignment in models.worker_assignments
2023
+ if assignment.worker_id == "report-writer"
2024
+ ),
2025
+ inp.report_writer_provider or "claude",
2026
+ ),
1826
2027
  "reportWriterModel": models.rw.display,
1827
2028
  "executor": models.executor_provider,
1828
2029
  "relatedTasks": inp.related_tasks_raw,
@@ -1919,10 +2120,10 @@ def _finalize_status_and_render_manifests(
1919
2120
  """최종 task/run status 를 확정하고 workflow state 를 재계산한 뒤 team-state,
1920
2121
  task-manifest, task-index, run-manifest, timeline, discovery 산출물을 렌더한다."""
1921
2122
  if inp.render_only:
1922
- ctx["CURRENT_TASK_STATUS"] = "instruction-set-generated"
2123
+ ctx["CURRENT_TASK_STATUS"] = "ready-for-lead"
1923
2124
  ctx["CURRENT_RUN_STATUS"] = "prepared"
1924
2125
  else:
1925
- ctx["CURRENT_TASK_STATUS"] = "claude-session-started"
2126
+ ctx["CURRENT_TASK_STATUS"] = "lead-session-started"
1926
2127
  ctx["CURRENT_RUN_STATUS"] = "in-progress"
1927
2128
  ctx["LATEST_REPORT_PATH"] = ctx["FINAL_REPORT_PATH"]
1928
2129
  ctx["LATEST_REPORT_RELATIVE_PATH"] = ctx["FINAL_REPORT_RELATIVE_PATH"]
@@ -2054,7 +2255,7 @@ def _initial_workflow_ctx(
2054
2255
  constants; `_finalize_status_and_render_manifests` recomputes the block once
2055
2256
  the real status is settled.
2056
2257
  """
2057
- task_status, run_status = "ready-for-claude", "not-run"
2258
+ task_status, run_status = "ready-for-lead", "not-run"
2058
2259
  return {
2059
2260
  "CURRENT_TASK_STATUS": task_status,
2060
2261
  "CURRENT_RUN_STATUS": run_status,
@@ -2085,7 +2286,15 @@ def _model_ctx(models: "_ModelBindings") -> dict[str, str]:
2085
2286
  """Render tokens for every model binding this run resolved."""
2086
2287
  return {
2087
2288
  "LEAD_MODEL": models.lead.display,
2088
- "LEAD_MODEL_EXECUTION_VALUE": models.lead.execution,
2289
+ "LEAD_MODEL_EXECUTION_VALUE": models.lead_assignment.model_execution_value,
2290
+ "LEAD_PROVIDER": models.lead_assignment.provider,
2291
+ "LEAD_ASSIGNMENT_JSON": json.dumps(
2292
+ models.lead_assignment.to_payload(), ensure_ascii=False,
2293
+ ),
2294
+ "WORKER_ASSIGNMENTS_JSON": json.dumps(
2295
+ [assignment.to_payload() for assignment in models.worker_assignments],
2296
+ ensure_ascii=False,
2297
+ ),
2089
2298
  "CLAUDE_WORKER_MODEL": models.cw.display,
2090
2299
  "CLAUDE_WORKER_MODEL_EXECUTION_VALUE": models.cw.execution,
2091
2300
  "CODEX_WORKER_MODEL": models.co.display,
@@ -2094,12 +2303,20 @@ def _model_ctx(models: "_ModelBindings") -> dict[str, str]:
2094
2303
  "ANTIGRAVITY_WORKER_MODEL_EXECUTION_VALUE": models.antigravity_worker_execution,
2095
2304
  "REPORT_WRITER_MODEL": models.rw.display,
2096
2305
  "REPORT_WRITER_MODEL_EXECUTION_VALUE": models.rw.execution,
2306
+ "REPORT_WRITER_PROVIDER": next(
2307
+ (
2308
+ assignment.provider for assignment in models.worker_assignments
2309
+ if assignment.worker_id == "report-writer"
2310
+ ),
2311
+ "",
2312
+ ),
2097
2313
  "EXECUTOR_PROVIDER": models.executor_provider,
2098
2314
  "EXECUTOR_DISPLAY_NAME": models.executor_display_name,
2099
2315
  "EXECUTOR_WORKER_AGENT": models.executor_worker_agent,
2100
2316
  "EXECUTOR_MODEL_DISPLAY": models.executor_model_meta.display,
2101
2317
  "EXECUTOR_MODEL_EXECUTION_VALUE": models.executor_execution,
2102
2318
  "CRITIC_CHOICE": models.critic_choice,
2319
+ "CRITIC_MODEL_EXECUTION_VALUE": models.critic_model_execution,
2103
2320
  }
2104
2321
 
2105
2322
 
@@ -2345,6 +2562,7 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
2345
2562
  ctx.update({
2346
2563
  "ANALYSIS_PROFILE": inp.task_type,
2347
2564
  "RECOMMENDED_ANALYSERS": selected_reviewers,
2565
+ "HOST_RUNTIME": lead_runtime,
2348
2566
  "LEAD_RUNTIME": lead_runtime,
2349
2567
  "LEAD_RUNTIME_REQUEST": lead_runtime_request,
2350
2568
  "RUNTIME_RESOLUTION_JSON": runtime_resolution_json or "{}",
@@ -2453,10 +2671,13 @@ def main(argv: list[str]) -> int:
2453
2671
  ),
2454
2672
  )
2455
2673
  p.add_argument("--workers", default="", dest="workers_override")
2674
+ p.add_argument("--lead-provider", default="")
2456
2675
  p.add_argument("--lead-model", default="")
2457
2676
  p.add_argument("--claude-model", default="")
2458
2677
  p.add_argument("--codex-model", default="")
2459
2678
  p.add_argument("--antigravity-model", default="")
2679
+ p.add_argument("--worker-model", default="", dest="worker_models_raw")
2680
+ p.add_argument("--report-writer-provider", default="")
2460
2681
  p.add_argument("--report-writer-model", default="")
2461
2682
  p.add_argument(
2462
2683
  "--lead-runtime",
@@ -2465,8 +2686,9 @@ def main(argv: list[str]) -> int:
2465
2686
  dest="lead_runtime",
2466
2687
  help=(
2467
2688
  "Lead runtime adapter. Default: claude-code. `codex` renders "
2468
- "Codex CLI artifact bundles; `external` renders neutral bootstrap "
2469
- "bundles for manifest inspection until a dispatch backend is enabled."
2689
+ "Codex CLI artifact bundles; `antigravity` renders Antigravity CLI "
2690
+ "artifact bundles; `external` renders neutral bootstrap bundles for "
2691
+ "manifest inspection until a dispatch backend is enabled."
2470
2692
  ),
2471
2693
  )
2472
2694
  p.add_argument(
@@ -2622,10 +2844,13 @@ def main(argv: list[str]) -> int:
2622
2844
  evidence_inputs_raw=args.evidence_inputs_raw,
2623
2845
  directive=args.directive,
2624
2846
  workers_override=args.workers_override,
2847
+ lead_provider=args.lead_provider,
2625
2848
  lead_model=args.lead_model,
2626
2849
  claude_model=args.claude_model,
2627
2850
  codex_model=args.codex_model,
2628
2851
  antigravity_model=args.antigravity_model,
2852
+ worker_models_raw=args.worker_models_raw,
2853
+ report_writer_provider=args.report_writer_provider,
2629
2854
  report_writer_model=args.report_writer_model,
2630
2855
  lead_runtime=args.lead_runtime,
2631
2856
  lead_runtime_request=args.lead_runtime_request,
@@ -2685,7 +2910,7 @@ def main(argv: list[str]) -> int:
2685
2910
  "claudeSessionId": ctx["CLAUDE_SESSION_ID"],
2686
2911
  "leadModelExecutionValue": ctx["LEAD_MODEL_EXECUTION_VALUE"],
2687
2912
  "projectRoot": ctx["PROJECT_ROOT"],
2688
- "promptFile": str(Path(ctx["INSTRUCTION_SET_PATH"]) / "claude-execution-prompt.md"),
2913
+ "promptFile": str(Path(ctx["INSTRUCTION_SET_PATH"]) / "lead-execution-prompt.md"),
2689
2914
  }
2690
2915
  print(f"__OKSTRA_LAUNCH__ {json.dumps(machine)}")
2691
2916
  return 0
@@ -0,0 +1,103 @@
1
+ """Resolve native versus CLI execution from host and provider capabilities."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+
6
+ from .models import (
7
+ UnknownProviderError,
8
+ provider_spec,
9
+ provider_supports_role,
10
+ provider_wrappers,
11
+ )
12
+
13
+ NATIVE_SESSION = "native-session"
14
+ CLI_WRAPPER = "cli-wrapper"
15
+ RUNNER_VALUES = frozenset({NATIVE_SESSION, CLI_WRAPPER})
16
+
17
+ _HOST_NATIVE_PROVIDERS = {
18
+ "claude-code": frozenset({"claude"}),
19
+ "codex": frozenset({"codex"}),
20
+ "antigravity": frozenset({"antigravity"}),
21
+ "external": frozenset(),
22
+ }
23
+
24
+
25
+ def native_provider_for_host(host_runtime: str) -> str:
26
+ """Return the single provider owned by an in-session host, if any."""
27
+ providers = _HOST_NATIVE_PROVIDERS.get(host_runtime)
28
+ if providers is None:
29
+ allowed = ", ".join(_HOST_NATIVE_PROVIDERS)
30
+ raise RunnerResolutionError(
31
+ f"unknown host runtime {host_runtime!r}. Allowed values: {allowed}"
32
+ )
33
+ return next(iter(providers), "")
34
+
35
+
36
+ class RunnerResolutionError(ValueError):
37
+ """Raised when an assignment cannot run on the selected host."""
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class RunnerResolution:
42
+ runner: str
43
+ wrapper: str
44
+ reason: str
45
+
46
+
47
+ def resolve_runner(
48
+ host_runtime: str,
49
+ provider: str,
50
+ role: str,
51
+ requested_runner: str = "",
52
+ ) -> RunnerResolution:
53
+ """Resolve an assignment without inferring the host from installed CLIs."""
54
+ native_providers = _HOST_NATIVE_PROVIDERS.get(host_runtime)
55
+ if native_providers is None:
56
+ allowed = ", ".join(_HOST_NATIVE_PROVIDERS)
57
+ raise RunnerResolutionError(
58
+ f"unknown host runtime {host_runtime!r}. Allowed values: {allowed}"
59
+ )
60
+ try:
61
+ spec = provider_spec(provider)
62
+ except UnknownProviderError as exc:
63
+ raise RunnerResolutionError(str(exc)) from exc
64
+ if not provider_supports_role(provider, role):
65
+ raise RunnerResolutionError(
66
+ f"provider {provider!r} does not support role {role!r}"
67
+ )
68
+ requested = (requested_runner or "").strip()
69
+ if requested and requested not in RUNNER_VALUES:
70
+ allowed = ", ".join(sorted(RUNNER_VALUES))
71
+ raise RunnerResolutionError(
72
+ f"unknown runner {requested!r}. Allowed values: {allowed}"
73
+ )
74
+ if requested == NATIVE_SESSION and provider not in native_providers:
75
+ raise RunnerResolutionError(
76
+ f"host runtime {host_runtime!r} does not provide native {provider!r} execution"
77
+ )
78
+ if requested == NATIVE_SESSION or (not requested and provider in native_providers):
79
+ return RunnerResolution(
80
+ runner=NATIVE_SESSION,
81
+ wrapper="",
82
+ reason=f"{host_runtime} provides native {provider} execution",
83
+ )
84
+ if not spec.wrapper:
85
+ raise RunnerResolutionError(
86
+ f"provider {provider!r} has no CLI wrapper for role {role!r}"
87
+ )
88
+ return RunnerResolution(
89
+ runner=CLI_WRAPPER,
90
+ wrapper=spec.wrapper,
91
+ reason=f"{host_runtime} uses the registered {provider} CLI wrapper",
92
+ )
93
+
94
+
95
+ def worker_wrapper_registry(report_writer_provider: str = "claude") -> dict[str, str]:
96
+ """Worker-id wrapper map for dispatch front ends."""
97
+ wrappers = provider_wrappers("analyser")
98
+ report_writer = provider_spec(report_writer_provider)
99
+ if not provider_supports_role(report_writer_provider, "report-writer"):
100
+ raise RunnerResolutionError(
101
+ f"provider {report_writer_provider!r} does not support report-writer"
102
+ )
103
+ return {**wrappers, "report-writer": report_writer.wrapper}