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
@@ -56,10 +56,27 @@ from .material import (
56
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
+ )
63
80
  from .report_contract import CURRENT_REPORT_SCHEMA_VERSION
64
81
  from .path_resolve import relative_to_project_root, resolve_user_file
65
82
  from .render import (
@@ -329,10 +346,13 @@ class PrepareInputs:
329
346
  evidence_inputs_raw: str = ""
330
347
  directive: str = ""
331
348
  workers_override: str = ""
349
+ lead_provider: str = ""
332
350
  lead_model: str = ""
333
351
  claude_model: str = ""
334
352
  codex_model: str = ""
335
353
  antigravity_model: str = ""
354
+ worker_models_raw: str = ""
355
+ report_writer_provider: str = ""
336
356
  report_writer_model: str = ""
337
357
  lead_runtime: str = "claude-code"
338
358
  lead_runtime_request: str = ""
@@ -743,7 +763,7 @@ def _brief_sha256(path: Path) -> str:
743
763
 
744
764
 
745
765
  def _record_artifact_runtime_render_only_event(inp: PrepareInputs, ctx: dict) -> None:
746
- 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:
747
767
  return
748
768
  append_lead_event(
749
769
  Path(ctx["LEAD_EVENTS_PATH"]),
@@ -781,10 +801,16 @@ def _canonical_argv(inp: PrepareInputs, ctx: dict) -> list[str]:
781
801
  ("--implementation-option", inp.implementation_option),
782
802
  ("--clarification-response", inp.clarification_response_path),
783
803
  ("--workers", workers),
804
+ ("--lead-provider", inp.lead_provider or ctx.get("LEAD_PROVIDER", "")),
784
805
  ("--lead-model", inp.lead_model or ctx.get("LEAD_MODEL", "")),
785
806
  ("--claude-model", inp.claude_model or ctx.get("CLAUDE_WORKER_MODEL", "")),
786
807
  ("--codex-model", inp.codex_model or ctx.get("CODEX_WORKER_MODEL", "")),
787
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
+ ),
788
814
  ("--report-writer-model", inp.report_writer_model or ctx.get("REPORT_WRITER_MODEL", "")),
789
815
  ("--lead-runtime", inp.lead_runtime if inp.lead_runtime != "claude-code" else ""),
790
816
  ("--executor", inp.executor or ctx.get("EXECUTOR_PROVIDER", "")),
@@ -1321,6 +1347,28 @@ def _resolve_pr_template(inp: PrepareInputs) -> tuple[str, str]:
1321
1347
  return str(resolved_tpl.path), resolved_tpl.source
1322
1348
 
1323
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
+
1324
1372
  @dataclass
1325
1373
  class _ModelBindings:
1326
1374
  """이 run 의 lead/worker 모델 + critic + executor 바인딩.
@@ -1336,6 +1384,7 @@ class _ModelBindings:
1336
1384
  ge: ModelAssignment
1337
1385
  rw: ModelAssignment
1338
1386
  critic_choice: str
1387
+ critic_model_execution: str
1339
1388
  executor_provider: str
1340
1389
  executor_display_name: str
1341
1390
  executor_worker_agent: str
@@ -1343,45 +1392,125 @@ class _ModelBindings:
1343
1392
  codex_worker_execution: str
1344
1393
  antigravity_worker_execution: str
1345
1394
  executor_execution: str
1395
+ lead_assignment: RoleAssignment
1396
+ worker_assignments: tuple[RoleAssignment, ...]
1346
1397
 
1347
1398
 
1348
1399
  def recommended_role_models() -> dict[str, str]:
1349
1400
  """역할 → 추천 모델 display 값 (env override 반영). prepare 의 모델 해소와
1350
1401
  wizard 의 안내 표기가 공유하는 단일 기준점."""
1351
1402
  lead_default = _default("OKSTRA_DEFAULT_LEAD_MODEL", default_model("lead"))
1352
- return {
1403
+ recommendations = {
1353
1404
  "lead": lead_default,
1354
1405
  "claude": _default("OKSTRA_DEFAULT_CLAUDE_MODEL", default_model("claude")),
1355
1406
  "codex": _default("OKSTRA_DEFAULT_CODEX_MODEL", default_model("codex")),
1356
1407
  "antigravity": _default("OKSTRA_DEFAULT_ANTIGRAVITY_MODEL", default_model("antigravity")),
1357
1408
  "report-writer": _default("OKSTRA_DEFAULT_REPORT_WRITER_MODEL", default_model("report-writer")),
1358
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
1359
1456
 
1360
1457
 
1361
1458
  def _resolve_worker_models(inp: PrepareInputs) -> dict:
1362
- """lead/claude/codex/antigravity/report-writer 모델을 alias (display, execution) 해소."""
1459
+ """Resolve provider models while preserving legacy fixed-provider projections."""
1363
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
+ )
1364
1495
  return {
1365
- "lead": resolve_model_metadata(
1366
- provider="claude", raw_value=inp.lead_model,
1367
- default_display=rec["lead"], default_execution=rec["lead"],
1368
- ),
1369
- "cw": resolve_model_metadata(
1370
- provider="claude", raw_value=inp.claude_model,
1371
- default_display=rec["claude"], default_execution=rec["claude"],
1372
- ),
1373
- "co": resolve_model_metadata(
1374
- provider="codex", raw_value=inp.codex_model,
1375
- default_display=rec["codex"], default_execution=rec["codex"],
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 "",
1376
1501
  ),
1377
- "ge": resolve_model_metadata(
1378
- provider="antigravity", raw_value=inp.antigravity_model,
1379
- default_display=rec["antigravity"], default_execution=rec["antigravity"],
1380
- ),
1381
- "rw": resolve_model_metadata(
1382
- provider="claude", raw_value=inp.report_writer_model,
1383
- 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 "",
1384
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
+ },
1385
1514
  }
1386
1515
 
1387
1516
 
@@ -1389,15 +1518,29 @@ def _resolve_model_bindings(inp: PrepareInputs, workers: list[str]) -> _ModelBin
1389
1518
  """worker 모델 + critic 선택 + executor 바인딩을 한 묶음으로 해소·검증한다."""
1390
1519
  m = _resolve_worker_models(inp)
1391
1520
  critic_choice = (inp.critic or "").strip().lower()
1392
- if critic_choice not in ("", "off", "claude", "codex", "antigravity"):
1521
+ allowed_critics = ["", "off", *provider_ids("critic")]
1522
+ if critic_choice not in allowed_critics:
1393
1523
  raise PrepareError(
1394
- 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",
1395
1536
  )
1396
1537
  executor_default = _default("OKSTRA_DEFAULT_EXECUTOR", "claude")
1397
1538
  executor_provider = (inp.executor or executor_default).strip().lower()
1398
- if executor_provider not in ("claude", "codex", "antigravity"):
1539
+ allowed_executors = provider_ids("executor")
1540
+ if executor_provider not in allowed_executors:
1399
1541
  raise PrepareError(
1400
- 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})"
1401
1544
  )
1402
1545
  if inp.task_type == "implementation" and executor_provider not in workers:
1403
1546
  raise PrepareError(
@@ -1405,15 +1548,21 @@ def _resolve_model_bindings(inp: PrepareInputs, workers: list[str]) -> _ModelBin
1405
1548
  f"--workers, but resolved roster is {workers!r}. "
1406
1549
  f"Add it explicitly, e.g. --workers {','.join(sorted(set(workers + [executor_provider])))}."
1407
1550
  )
1408
- provider_to_meta = {
1409
- "claude": ("Claude executor", "claude-worker", m["cw"]),
1410
- "codex": ("Codex executor", "codex-worker", m["co"]),
1411
- "antigravity": ("Antigravity executor", "antigravity-worker", m["ge"]),
1412
- }
1413
- 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)
1414
1562
  return _ModelBindings(
1415
1563
  lead=m["lead"], cw=m["cw"], co=m["co"], ge=m["ge"], rw=m["rw"],
1416
1564
  critic_choice=critic_choice,
1565
+ critic_model_execution=critic_model_execution,
1417
1566
  executor_provider=executor_provider,
1418
1567
  executor_display_name=display_name,
1419
1568
  executor_worker_agent=worker_agent,
@@ -1424,9 +1573,42 @@ def _resolve_model_bindings(inp: PrepareInputs, workers: list[str]) -> _ModelBin
1424
1573
  m["ge"], "antigravity", "worker", workers),
1425
1574
  executor_execution=_normalized_execution(
1426
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,
1427
1584
  )
1428
1585
 
1429
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
+
1430
1612
  def _normalized_execution(meta: ModelAssignment, provider: str, role: str) -> str:
1431
1613
  """Route a role's catalog execution value through the CLI-identity normalizer
1432
1614
  so the manifest carries the dispatch-correct spelling. SSOT for the mapping
@@ -1760,7 +1942,7 @@ def _render_lead_prompt_and_snapshot(
1760
1942
  """compute/default 토큰을 ctx 에 주입한 뒤 final-report 템플릿 사본과 lead 실행
1761
1943
  프롬프트를 렌더하고, 프롬프트 스냅샷을 기록한 후 prompt_text 를 돌려준다."""
1762
1944
  # inject populates ctx with compute + default tokens consumed by the lead
1763
- # prompt render below (claude-execution-prompt.md). The final-report
1945
+ # prompt render below (lead-execution-prompt.md). The final-report
1764
1946
  # template render is effectively a copy (Jinja2 `{{ var }}` syntax does
1765
1947
  # not match `_TOKEN_RE`); routed through render_template_with_ctx for SOT
1766
1948
  # consistency.
@@ -1793,10 +1975,11 @@ def _render_lead_prompt_and_snapshot(
1793
1975
  )
1794
1976
  except Exception: # noqa: BLE001 — advisory artifact; never fail prep over it
1795
1977
  pass
1796
- render_template_with_ctx(
1797
- str(prompt_template), str(instruction_set / "claude-execution-prompt.md"), ctx,
1798
- )
1799
- 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")
1800
1983
  Path(ctx["RUN_PROMPT_SNAPSHOT_FILE"]).parent.mkdir(parents=True, exist_ok=True)
1801
1984
  Path(ctx["RUN_PROMPT_SNAPSHOT_FILE"]).write_text(prompt_text, encoding="utf-8")
1802
1985
  return prompt_text
@@ -1825,10 +2008,22 @@ def _persist_run_inputs(
1825
2008
  "taskBriefAbsolutePath": str(inp.brief_path),
1826
2009
  "directive": inp.directive,
1827
2010
  "workers": selected_reviewers,
2011
+ "leadProvider": models.lead_assignment.provider,
1828
2012
  "leadModel": models.lead.display,
1829
2013
  "claudeModel": models.cw.display,
1830
2014
  "codexModel": models.co.display,
1831
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
+ ),
1832
2027
  "reportWriterModel": models.rw.display,
1833
2028
  "executor": models.executor_provider,
1834
2029
  "relatedTasks": inp.related_tasks_raw,
@@ -1925,10 +2120,10 @@ def _finalize_status_and_render_manifests(
1925
2120
  """최종 task/run status 를 확정하고 workflow state 를 재계산한 뒤 team-state,
1926
2121
  task-manifest, task-index, run-manifest, timeline, discovery 산출물을 렌더한다."""
1927
2122
  if inp.render_only:
1928
- ctx["CURRENT_TASK_STATUS"] = "instruction-set-generated"
2123
+ ctx["CURRENT_TASK_STATUS"] = "ready-for-lead"
1929
2124
  ctx["CURRENT_RUN_STATUS"] = "prepared"
1930
2125
  else:
1931
- ctx["CURRENT_TASK_STATUS"] = "claude-session-started"
2126
+ ctx["CURRENT_TASK_STATUS"] = "lead-session-started"
1932
2127
  ctx["CURRENT_RUN_STATUS"] = "in-progress"
1933
2128
  ctx["LATEST_REPORT_PATH"] = ctx["FINAL_REPORT_PATH"]
1934
2129
  ctx["LATEST_REPORT_RELATIVE_PATH"] = ctx["FINAL_REPORT_RELATIVE_PATH"]
@@ -2060,7 +2255,7 @@ def _initial_workflow_ctx(
2060
2255
  constants; `_finalize_status_and_render_manifests` recomputes the block once
2061
2256
  the real status is settled.
2062
2257
  """
2063
- task_status, run_status = "ready-for-claude", "not-run"
2258
+ task_status, run_status = "ready-for-lead", "not-run"
2064
2259
  return {
2065
2260
  "CURRENT_TASK_STATUS": task_status,
2066
2261
  "CURRENT_RUN_STATUS": run_status,
@@ -2091,7 +2286,15 @@ def _model_ctx(models: "_ModelBindings") -> dict[str, str]:
2091
2286
  """Render tokens for every model binding this run resolved."""
2092
2287
  return {
2093
2288
  "LEAD_MODEL": models.lead.display,
2094
- "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
+ ),
2095
2298
  "CLAUDE_WORKER_MODEL": models.cw.display,
2096
2299
  "CLAUDE_WORKER_MODEL_EXECUTION_VALUE": models.cw.execution,
2097
2300
  "CODEX_WORKER_MODEL": models.co.display,
@@ -2100,12 +2303,20 @@ def _model_ctx(models: "_ModelBindings") -> dict[str, str]:
2100
2303
  "ANTIGRAVITY_WORKER_MODEL_EXECUTION_VALUE": models.antigravity_worker_execution,
2101
2304
  "REPORT_WRITER_MODEL": models.rw.display,
2102
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
+ ),
2103
2313
  "EXECUTOR_PROVIDER": models.executor_provider,
2104
2314
  "EXECUTOR_DISPLAY_NAME": models.executor_display_name,
2105
2315
  "EXECUTOR_WORKER_AGENT": models.executor_worker_agent,
2106
2316
  "EXECUTOR_MODEL_DISPLAY": models.executor_model_meta.display,
2107
2317
  "EXECUTOR_MODEL_EXECUTION_VALUE": models.executor_execution,
2108
2318
  "CRITIC_CHOICE": models.critic_choice,
2319
+ "CRITIC_MODEL_EXECUTION_VALUE": models.critic_model_execution,
2109
2320
  }
2110
2321
 
2111
2322
 
@@ -2351,6 +2562,7 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
2351
2562
  ctx.update({
2352
2563
  "ANALYSIS_PROFILE": inp.task_type,
2353
2564
  "RECOMMENDED_ANALYSERS": selected_reviewers,
2565
+ "HOST_RUNTIME": lead_runtime,
2354
2566
  "LEAD_RUNTIME": lead_runtime,
2355
2567
  "LEAD_RUNTIME_REQUEST": lead_runtime_request,
2356
2568
  "RUNTIME_RESOLUTION_JSON": runtime_resolution_json or "{}",
@@ -2459,10 +2671,13 @@ def main(argv: list[str]) -> int:
2459
2671
  ),
2460
2672
  )
2461
2673
  p.add_argument("--workers", default="", dest="workers_override")
2674
+ p.add_argument("--lead-provider", default="")
2462
2675
  p.add_argument("--lead-model", default="")
2463
2676
  p.add_argument("--claude-model", default="")
2464
2677
  p.add_argument("--codex-model", default="")
2465
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="")
2466
2681
  p.add_argument("--report-writer-model", default="")
2467
2682
  p.add_argument(
2468
2683
  "--lead-runtime",
@@ -2471,8 +2686,9 @@ def main(argv: list[str]) -> int:
2471
2686
  dest="lead_runtime",
2472
2687
  help=(
2473
2688
  "Lead runtime adapter. Default: claude-code. `codex` renders "
2474
- "Codex CLI artifact bundles; `external` renders neutral bootstrap "
2475
- "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."
2476
2692
  ),
2477
2693
  )
2478
2694
  p.add_argument(
@@ -2628,10 +2844,13 @@ def main(argv: list[str]) -> int:
2628
2844
  evidence_inputs_raw=args.evidence_inputs_raw,
2629
2845
  directive=args.directive,
2630
2846
  workers_override=args.workers_override,
2847
+ lead_provider=args.lead_provider,
2631
2848
  lead_model=args.lead_model,
2632
2849
  claude_model=args.claude_model,
2633
2850
  codex_model=args.codex_model,
2634
2851
  antigravity_model=args.antigravity_model,
2852
+ worker_models_raw=args.worker_models_raw,
2853
+ report_writer_provider=args.report_writer_provider,
2635
2854
  report_writer_model=args.report_writer_model,
2636
2855
  lead_runtime=args.lead_runtime,
2637
2856
  lead_runtime_request=args.lead_runtime_request,
@@ -2691,7 +2910,7 @@ def main(argv: list[str]) -> int:
2691
2910
  "claudeSessionId": ctx["CLAUDE_SESSION_ID"],
2692
2911
  "leadModelExecutionValue": ctx["LEAD_MODEL_EXECUTION_VALUE"],
2693
2912
  "projectRoot": ctx["PROJECT_ROOT"],
2694
- "promptFile": str(Path(ctx["INSTRUCTION_SET_PATH"]) / "claude-execution-prompt.md"),
2913
+ "promptFile": str(Path(ctx["INSTRUCTION_SET_PATH"]) / "lead-execution-prompt.md"),
2695
2914
  }
2696
2915
  print(f"__OKSTRA_LAUNCH__ {json.dumps(machine)}")
2697
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}
@@ -16,13 +16,9 @@ from .dispatch_core import (
16
16
  build_dispatch_plan,
17
17
  dispatch_plan,
18
18
  )
19
+ from .runner_resolution import worker_wrapper_registry
19
20
 
20
- _SUPPORTED_WRAPPERS = {
21
- "claude": "okstra-claude-exec.sh",
22
- "codex": "okstra-codex-exec.sh",
23
- "antigravity": "okstra-antigravity-exec.sh",
24
- "report-writer": "okstra-claude-exec.sh",
25
- }
21
+ _SUPPORTED_WRAPPERS = worker_wrapper_registry()
26
22
  _TERMINAL_STATUSES = {"completed", "timeout", "error", "not-run"}
27
23
 
28
24
 
@@ -98,7 +94,6 @@ def _dispatch(args) -> int:
98
94
  okstra_bin=Path(args.okstra_bin),
99
95
  requested_workers=requested,
100
96
  idle_timeout_seconds=args.idle_timeout_seconds,
101
- enable_report_writer="report-writer" in requested,
102
97
  required_lead_runtime="external",
103
98
  default_backend=BACKEND_TMUX_PANE,
104
99
  supported_worker_wrappers=_SUPPORTED_WRAPPERS,