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
@@ -45,8 +45,12 @@ from okstra_ctl.models import (
45
45
  PROVIDER_MAPPINGS,
46
46
  UnknownModelError,
47
47
  picker_options,
48
+ provider_ids,
49
+ provider_supports_role,
48
50
  resolve_model_metadata,
49
51
  )
52
+ from okstra_ctl.lead_runtime import lead_runtime_info
53
+ from okstra_ctl.runner_resolution import native_provider_for_host
50
54
  from okstra_ctl.clarification_items import (
51
55
  scan_approval_gate,
52
56
  user_response_sidecars,
@@ -135,10 +139,12 @@ EXECUTORS = ["claude", "codex", "antigravity"]
135
139
 
136
140
  # 역할 한 줄 설명 — defaults_or_custom 안내에서 각 역할이 무슨 일을 하는지 보여준다.
137
141
  ROLE_BLURBS = {
138
- "lead": "Claude lead · 워커 dispatch·교차검증 수렴·최종 합성",
142
+ "lead": "Okstra lead · 워커 dispatch·교차검증 수렴·최종 합성",
139
143
  "claude": "독립 분석 워커 (Claude)",
140
144
  "codex": "독립 분석 워커 (Codex)",
141
145
  "antigravity": "독립 분석 워커 (Antigravity)",
146
+ "grok": "독립 분석 워커 (Grok)",
147
+ "kimi": "독립 분석 워커 (Kimi)",
142
148
  "report-writer": "최종 리포트 작성 (분석은 하지 않음)",
143
149
  }
144
150
 
@@ -165,6 +171,8 @@ HOST_PICK_OPTION_CAP = 4
165
171
  CLAUDE_MODEL_OPTIONS = ["default", *picker_options("claude")]
166
172
  CODEX_MODEL_OPTIONS = ["default", *picker_options("codex")]
167
173
  ANTIGRAVITY_MODEL_OPTIONS = ["default", *picker_options("antigravity")]
174
+ GROK_MODEL_OPTIONS = ["default", *picker_options("grok")]
175
+ KIMI_MODEL_OPTIONS = ["default", *picker_options("kimi")]
168
176
 
169
177
  # special pick value: start a brand-new task
170
178
  TASK_PICK_NEW_TOKEN = "__new__"
@@ -315,6 +323,8 @@ S_EXECUTOR_MODEL = "executor_model"
315
323
  S_CLAUDE_MODEL = "claude_model"
316
324
  S_CODEX_MODEL = "codex_model"
317
325
  S_ANTIGRAVITY_MODEL = "antigravity_model"
326
+ S_GROK_MODEL = "grok_model"
327
+ S_KIMI_MODEL = "kimi_model"
318
328
  S_REPORT_WRITER_MODEL = "report_writer_model"
319
329
  S_DIRECTIVE_PICK = "directive_pick"
320
330
  S_DIRECTIVE = "directive"
@@ -341,7 +351,8 @@ GROUP_MAX_TABS = 4 # AskUserQuestion 의 질문(탭) 수 한도
341
351
  # *_TEXT 후속 / workers_override / pr_template_scope 는 의존성 때문에 개별 유지.
342
352
  PROMPT_GROUPS: dict[str, tuple[str, ...]] = {
343
353
  GROUP_MODELS: (S_LEAD_MODEL, S_EXECUTOR_MODEL, S_CLAUDE_MODEL,
344
- S_CODEX_MODEL, S_ANTIGRAVITY_MODEL, S_REPORT_WRITER_MODEL),
354
+ S_CODEX_MODEL, S_ANTIGRAVITY_MODEL, S_GROK_MODEL,
355
+ S_KIMI_MODEL, S_REPORT_WRITER_MODEL),
345
356
  GROUP_OPTIONS: (S_DIRECTIVE_PICK, S_RELATED_TASKS_PICK,
346
357
  S_CLARIFICATION_PICK, S_PR_TEMPLATE_PICK),
347
358
  }
@@ -362,6 +373,7 @@ class WizardState:
362
373
  workspace_root: str = ""
363
374
  project_root: str = ""
364
375
  project_id: str = ""
376
+ host_runtime: str = "claude-code"
365
377
 
366
378
  # task identity
367
379
  is_new_task: Optional[bool] = None
@@ -432,10 +444,14 @@ class WizardState:
432
444
  # customize
433
445
  use_defaults: Optional[bool] = None
434
446
  workers_override: str = ""
447
+ lead_provider: str = ""
435
448
  lead_model: str = ""
436
449
  claude_model: str = ""
437
450
  codex_model: str = ""
438
451
  antigravity_model: str = ""
452
+ grok_model: str = ""
453
+ kimi_model: str = ""
454
+ report_writer_provider: str = ""
439
455
  report_writer_model: str = ""
440
456
  directive: str = ""
441
457
  directive_pending_text: bool = False
@@ -3141,7 +3157,8 @@ _REUSE_FILLED_STEPS = (
3141
3157
  S_PROJECT_EVIDENCE_PICK, S_PROJECT_EVIDENCE,
3142
3158
  S_ANALYSIS_TARGET_PICK, S_ANALYSIS_TARGET,
3143
3159
  S_DEFAULTS_OR_CUSTOM, S_WORKERS_OVERRIDE, S_LEAD_MODEL, S_EXECUTOR_MODEL,
3144
- S_CLAUDE_MODEL, S_CODEX_MODEL, S_ANTIGRAVITY_MODEL, S_REPORT_WRITER_MODEL,
3160
+ S_CLAUDE_MODEL, S_CODEX_MODEL, S_ANTIGRAVITY_MODEL, S_GROK_MODEL,
3161
+ S_KIMI_MODEL, S_REPORT_WRITER_MODEL,
3145
3162
  S_DIRECTIVE_PICK, S_DIRECTIVE, S_RELATED_TASKS_PICK, S_RELATED_TASKS,
3146
3163
  S_CLARIFICATION_PICK, S_CLARIFICATION)
3147
3164
 
@@ -3205,6 +3222,26 @@ def _safe_model(provider: str, raw: object) -> str:
3205
3222
  return ""
3206
3223
 
3207
3224
 
3225
+ def _safe_provider(raw: object, role: str) -> str:
3226
+ provider = raw.strip().lower() if isinstance(raw, str) else ""
3227
+ if not provider:
3228
+ return ""
3229
+ return provider if provider_supports_role(provider, role) else ""
3230
+
3231
+
3232
+ def _assignment_model(inputs: dict, worker_id: str, provider: str) -> str:
3233
+ assignments = inputs.get("workerAssignments")
3234
+ if not isinstance(assignments, list):
3235
+ return ""
3236
+ for assignment in assignments:
3237
+ if not isinstance(assignment, dict):
3238
+ continue
3239
+ if assignment.get("workerId") != worker_id:
3240
+ continue
3241
+ return _safe_model(provider, assignment.get("model"))
3242
+ return ""
3243
+
3244
+
3208
3245
  def _build_reuse_previous(state: WizardState) -> Prompt:
3209
3246
  t = _p(state.workspace_root, "reuse_previous")
3210
3247
  return Prompt(
@@ -3229,12 +3266,27 @@ def _submit_reuse_previous(state: WizardState, value: str) -> Optional[str]:
3229
3266
  state.workers_override = (
3230
3267
  ",".join(w for w in workers if isinstance(w, str))
3231
3268
  if isinstance(workers, list) else "")
3232
- state.lead_model = _safe_model("claude", inputs.get("leadModel"))
3269
+ state.lead_provider = _safe_provider(inputs.get("leadProvider"), "lead")
3270
+ lead_provider = state.lead_provider or "claude"
3271
+ native_provider = native_provider_for_host(state.host_runtime)
3272
+ if native_provider and lead_provider != native_provider:
3273
+ raise WizardError(
3274
+ f"previous run used provider {lead_provider!r}, but "
3275
+ f"{state.host_runtime} requires provider {native_provider!r}; "
3276
+ "choose step-by-step re-entry"
3277
+ )
3278
+ state.lead_model = _safe_model(lead_provider, inputs.get("leadModel"))
3233
3279
  state.claude_model = _safe_model("claude", inputs.get("claudeModel"))
3234
3280
  state.codex_model = _safe_model("codex", inputs.get("codexModel"))
3235
3281
  state.antigravity_model = _safe_model("antigravity", inputs.get("antigravityModel"))
3282
+ state.grok_model = _assignment_model(inputs, "grok", "grok")
3283
+ state.kimi_model = _assignment_model(inputs, "kimi", "kimi")
3284
+ state.report_writer_provider = _safe_provider(
3285
+ inputs.get("reportWriterProvider"), "report-writer",
3286
+ )
3287
+ report_writer_provider = state.report_writer_provider or "claude"
3236
3288
  state.report_writer_model = _safe_model(
3237
- "claude", inputs.get("reportWriterModel"))
3289
+ report_writer_provider, inputs.get("reportWriterModel"))
3238
3290
  directive = inputs.get("directive")
3239
3291
  state.directive = directive if isinstance(directive, str) else ""
3240
3292
  related = inputs.get("relatedTasks")
@@ -3285,7 +3337,7 @@ def _role_model_lines(state: WizardState) -> str:
3285
3337
  ex = state.executor or "claude"
3286
3338
  lines.append(line(f"executor={ex}", "코드 구현 실행자", rec.get(ex, "auto")))
3287
3339
  else:
3288
- for w in ("claude", "codex", "antigravity"):
3340
+ for w in provider_ids("analyser"):
3289
3341
  if w in roster:
3290
3342
  lines.append(line(w, ROLE_BLURBS[w], rec[w]))
3291
3343
  if impl or "report-writer" in roster:
@@ -3371,15 +3423,65 @@ def _model_pick(step: str, label: str, options: list[str], echo: str) -> Prompt:
3371
3423
  options=opts, echo_template=echo)
3372
3424
 
3373
3425
 
3426
+ def _qualified_role_model_options(role: str) -> list[str]:
3427
+ options = list(CLAUDE_MODEL_OPTIONS)
3428
+ for provider in provider_ids(role):
3429
+ if provider == "claude":
3430
+ continue
3431
+ options.extend(
3432
+ f"{provider}:{model}" for model in picker_options(provider)
3433
+ )
3434
+ return options
3435
+
3436
+
3437
+ def _submit_qualified_role_model(
3438
+ state: WizardState, value: str, role: str, model_field: str, provider_field: str,
3439
+ ) -> str:
3440
+ raw = (value or "").strip()
3441
+ if ":" not in raw:
3442
+ setattr(state, provider_field, "")
3443
+ setattr(state, model_field, _validate_model("claude", raw))
3444
+ return getattr(state, model_field) or "default"
3445
+ provider, model = (part.strip().lower() for part in raw.split(":", 1))
3446
+ if not provider_supports_role(provider, role):
3447
+ raise WizardError(f"provider {provider!r} does not support the {role} role")
3448
+ setattr(state, provider_field, provider)
3449
+ setattr(state, model_field, _validate_model(provider, model))
3450
+ return raw
3451
+
3452
+
3374
3453
  def _build_lead_model(state: WizardState) -> Prompt:
3375
3454
  t = _p(state.workspace_root, "lead_model")
3376
- return _model_pick(S_LEAD_MODEL, t["label"],
3377
- CLAUDE_MODEL_OPTIONS, t["echo_template"])
3455
+ provider = native_provider_for_host(state.host_runtime)
3456
+ options = (
3457
+ ["default", *picker_options(provider)]
3458
+ if provider else _qualified_role_model_options("lead")
3459
+ )
3460
+ return _model_pick(S_LEAD_MODEL, t["label"], options, t["echo_template"])
3378
3461
 
3379
3462
 
3380
3463
  def _submit_lead_model(state: WizardState, value: str) -> Optional[str]:
3381
- state.lead_model = _validate_model("claude", value)
3382
- return f"lead-model: {state.lead_model or 'default'}"
3464
+ native_provider = native_provider_for_host(state.host_runtime)
3465
+ if native_provider:
3466
+ raw = (value or "").strip()
3467
+ if ":" in raw:
3468
+ selected_provider, raw = (
3469
+ part.strip().lower() for part in raw.split(":", 1)
3470
+ )
3471
+ if selected_provider != native_provider:
3472
+ host_label = lead_runtime_info(state.host_runtime).agent_label
3473
+ raise WizardError(
3474
+ f"{host_label.removesuffix(' CLI')} host requires provider "
3475
+ f"{native_provider!r} "
3476
+ "for the in-session lead"
3477
+ )
3478
+ state.lead_provider = native_provider
3479
+ state.lead_model = _validate_model(native_provider, raw)
3480
+ return f"lead-model: {native_provider}:{state.lead_model or 'default'}"
3481
+ selected = _submit_qualified_role_model(
3482
+ state, value, "lead", "lead_model", "lead_provider",
3483
+ )
3484
+ return f"lead-model: {selected}"
3383
3485
 
3384
3486
 
3385
3487
  def _build_executor_model(state: WizardState) -> Prompt:
@@ -3431,17 +3533,44 @@ def _submit_antigravity_model(state: WizardState, value: str) -> Optional[str]:
3431
3533
  return f"antigravity-model: {state.antigravity_model or 'default'}"
3432
3534
 
3433
3535
 
3536
+ def _build_grok_model(state: WizardState) -> Prompt:
3537
+ t = _p(state.workspace_root, "grok_model")
3538
+ return _model_pick(
3539
+ S_GROK_MODEL, t["label"], GROK_MODEL_OPTIONS, t["echo_template"],
3540
+ )
3541
+
3542
+
3543
+ def _submit_grok_model(state: WizardState, value: str) -> Optional[str]:
3544
+ state.grok_model = _validate_model("grok", value)
3545
+ return f"grok-model: {state.grok_model or 'default'}"
3546
+
3547
+
3548
+ def _build_kimi_model(state: WizardState) -> Prompt:
3549
+ t = _p(state.workspace_root, "kimi_model")
3550
+ return _model_pick(
3551
+ S_KIMI_MODEL, t["label"], KIMI_MODEL_OPTIONS, t["echo_template"],
3552
+ )
3553
+
3554
+
3555
+ def _submit_kimi_model(state: WizardState, value: str) -> Optional[str]:
3556
+ state.kimi_model = _validate_model("kimi", value)
3557
+ return f"kimi-model: {state.kimi_model or 'default'}"
3558
+
3559
+
3434
3560
  def _build_report_writer_model(state: WizardState) -> Prompt:
3435
3561
  t = _p(state.workspace_root, "report_writer_model")
3436
3562
  return _model_pick(S_REPORT_WRITER_MODEL,
3437
3563
  t["label"],
3438
- CLAUDE_MODEL_OPTIONS,
3564
+ _qualified_role_model_options("report-writer"),
3439
3565
  t["echo_template"])
3440
3566
 
3441
3567
 
3442
3568
  def _submit_report_writer_model(state: WizardState, value: str) -> Optional[str]:
3443
- state.report_writer_model = _validate_model("claude", value)
3444
- return f"report-writer-model: {state.report_writer_model or 'default'}"
3569
+ selected = _submit_qualified_role_model(
3570
+ state, value, "report-writer", "report_writer_model",
3571
+ "report_writer_provider",
3572
+ )
3573
+ return f"report-writer-model: {selected}"
3445
3574
 
3446
3575
 
3447
3576
  def _build_directive(state: WizardState) -> Prompt:
@@ -3941,7 +4070,7 @@ STEPS: list[Step] = [
3941
4070
  applies=lambda s: (s.use_defaults is False
3942
4071
  and S_LEAD_MODEL not in s.answered),
3943
4072
  build=_build_lead_model, submit=_submit_lead_model,
3944
- owns=("lead_model",)),
4073
+ owns=("lead_provider", "lead_model")),
3945
4074
  Step(S_EXECUTOR_MODEL,
3946
4075
  applies=lambda s: (s.use_defaults is False
3947
4076
  and s.task_type == "implementation"
@@ -3969,13 +4098,27 @@ STEPS: list[Step] = [
3969
4098
  and S_ANTIGRAVITY_MODEL not in s.answered),
3970
4099
  build=_build_antigravity_model, submit=_submit_antigravity_model,
3971
4100
  owns=("antigravity_model",)),
4101
+ Step(S_GROK_MODEL,
4102
+ applies=lambda s: (s.use_defaults is False
4103
+ and s.task_type != "implementation"
4104
+ and "grok" in _resolved_roster(s)
4105
+ and S_GROK_MODEL not in s.answered),
4106
+ build=_build_grok_model, submit=_submit_grok_model,
4107
+ owns=("grok_model",)),
4108
+ Step(S_KIMI_MODEL,
4109
+ applies=lambda s: (s.use_defaults is False
4110
+ and s.task_type != "implementation"
4111
+ and "kimi" in _resolved_roster(s)
4112
+ and S_KIMI_MODEL not in s.answered),
4113
+ build=_build_kimi_model, submit=_submit_kimi_model,
4114
+ owns=("kimi_model",)),
3972
4115
  Step(S_REPORT_WRITER_MODEL,
3973
4116
  applies=lambda s: (s.use_defaults is False
3974
4117
  and (s.task_type == "implementation"
3975
4118
  or "report-writer" in _resolved_roster(s))
3976
4119
  and S_REPORT_WRITER_MODEL not in s.answered),
3977
4120
  build=_build_report_writer_model, submit=_submit_report_writer_model,
3978
- owns=("report_writer_model",)),
4121
+ owns=("report_writer_provider", "report_writer_model")),
3979
4122
  # directive(이번 run 의 추가 지시)는 기본값/커스터마이즈와 무관하게 항상
3980
4123
  # 묻는다 — 매 run 마다 줄 수 있는 입력이므로 'Use defaults' 분기 뒤에 숨기지
3981
4124
  # 않는다. (use_defaults is not None: defaults_or_custom 답 이후에만 등장)
@@ -4151,8 +4294,9 @@ _FIELD_DEFAULTS: dict[str, Any] = {
4151
4294
  "executor": "", "critic": "", "critic_pending_text": False,
4152
4295
  "reuse_previous": None,
4153
4296
  "use_defaults": None, "workers_override": "",
4154
- "lead_model": "", "claude_model": "", "codex_model": "",
4155
- "antigravity_model": "", "report_writer_model": "", "directive": "",
4297
+ "lead_provider": "", "lead_model": "", "claude_model": "", "codex_model": "",
4298
+ "antigravity_model": "", "grok_model": "", "kimi_model": "",
4299
+ "report_writer_provider": "", "report_writer_model": "", "directive": "",
4156
4300
  "directive_pending_text": False,
4157
4301
  "related_tasks_raw": "", "related_tasks_pending_text": False,
4158
4302
  "clarification_response_path": "", "clarification_pending_text": False,
@@ -4176,13 +4320,15 @@ def _reset_field(state: WizardState, fname: str) -> None:
4176
4320
  # ---- Public API ---------------------------------------------------------
4177
4321
 
4178
4322
  def init_state(
4179
- *, workspace_root: str, project_root: str, project_id: str
4323
+ *, workspace_root: str, project_root: str, project_id: str,
4324
+ host_runtime: str = "claude-code",
4180
4325
  ) -> WizardState:
4181
4326
  """Bootstrap a new wizard state."""
4182
4327
  return WizardState(
4183
4328
  workspace_root=workspace_root,
4184
4329
  project_root=project_root,
4185
4330
  project_id=project_id,
4331
+ host_runtime=host_runtime,
4186
4332
  )
4187
4333
 
4188
4334
 
@@ -4402,6 +4548,14 @@ def render_args(state: WizardState) -> dict[str, str]:
4402
4548
  for path in (state.feature_evidence_path, state.project_evidence_path)
4403
4549
  if path
4404
4550
  ]
4551
+ worker_models = ",".join(
4552
+ f"{provider}={model}"
4553
+ for provider, model in (
4554
+ ("grok", state.grok_model),
4555
+ ("kimi", state.kimi_model),
4556
+ )
4557
+ if model
4558
+ )
4405
4559
  return {
4406
4560
  "project-root": state.project_root,
4407
4561
  "project-id": state.project_id,
@@ -4420,10 +4574,13 @@ def render_args(state: WizardState) -> dict[str, str]:
4420
4574
  "base-ref": base_ref,
4421
4575
  "workers": workers,
4422
4576
  "directive": state.directive,
4577
+ "lead-provider": state.lead_provider,
4423
4578
  "lead-model": state.lead_model,
4424
4579
  "claude-model": state.claude_model,
4425
4580
  "codex-model": state.codex_model,
4426
4581
  "antigravity-model": state.antigravity_model,
4582
+ "worker-model": worker_models,
4583
+ "report-writer-provider": state.report_writer_provider,
4427
4584
  "report-writer-model": state.report_writer_model,
4428
4585
  "related-tasks": state.related_tasks_raw,
4429
4586
  "clarification-response": state.clarification_response_path,
@@ -4480,15 +4637,25 @@ def confirmation_block(state: WizardState) -> str:
4480
4637
  else:
4481
4638
  roster = state.workers_override or ",".join(state.profile_workers) or "(profile default)"
4482
4639
  lines.append(f" workers : {roster}")
4483
- lines.append(f" lead-model : {state.lead_model or 'default'}")
4640
+ lines.append(
4641
+ f" lead : {state.lead_provider or 'claude'} / "
4642
+ f"{state.lead_model or 'default'}"
4643
+ )
4484
4644
  if state.claude_model:
4485
4645
  lines.append(f" claude-model : {state.claude_model}")
4486
4646
  if state.codex_model:
4487
4647
  lines.append(f" codex-model : {state.codex_model}")
4488
4648
  if state.antigravity_model:
4489
4649
  lines.append(f" antigravity-model : {state.antigravity_model}")
4490
- if state.report_writer_model:
4491
- lines.append(f" report-writer : {state.report_writer_model}")
4650
+ if state.grok_model:
4651
+ lines.append(f" grok-model : {state.grok_model}")
4652
+ if state.kimi_model:
4653
+ lines.append(f" kimi-model : {state.kimi_model}")
4654
+ if state.report_writer_model or state.report_writer_provider:
4655
+ lines.append(
4656
+ f" report-writer : {state.report_writer_provider or 'claude'} / "
4657
+ f"{state.report_writer_model or 'default'}"
4658
+ )
4492
4659
  lines.append(f" directive : {state.directive or '(none)'}")
4493
4660
  if state.related_tasks_raw:
4494
4661
  lines.append(f" related-tasks : {state.related_tasks_raw}")
@@ -4587,6 +4754,11 @@ def _cli(argv: list[str]) -> int:
4587
4754
  p_init.add_argument("--workspace-root", required=True)
4588
4755
  p_init.add_argument("--project-root", required=True)
4589
4756
  p_init.add_argument("--project-id", required=True)
4757
+ p_init.add_argument(
4758
+ "--host-runtime",
4759
+ default="claude-code",
4760
+ choices=("claude-code", "codex", "antigravity", "external"),
4761
+ )
4590
4762
  p_init.add_argument("--critic", default="")
4591
4763
 
4592
4764
  p_step = sub.add_parser("step")
@@ -4615,6 +4787,7 @@ def _cli(argv: list[str]) -> int:
4615
4787
  workspace_root=args.workspace_root,
4616
4788
  project_root=args.project_root,
4617
4789
  project_id=args.project_id,
4790
+ host_runtime=args.host_runtime,
4618
4791
  )
4619
4792
  if args.critic:
4620
4793
  state.critic = args.critic
@@ -0,0 +1,46 @@
1
+ """Provider-registry-derived worker artifact paths."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+ from typing import Any, Mapping
6
+
7
+ from .models import provider_ids
8
+
9
+
10
+ def artifact_worker_ids() -> list[str]:
11
+ """All analysis worker ids plus the non-analysis report writer slot."""
12
+ return [*provider_ids("analyser"), "report-writer"]
13
+
14
+
15
+ def artifacts_from_context(
16
+ ctx: Mapping[str, Any], *, relative: bool
17
+ ) -> dict[str, dict[str, str]]:
18
+ """Derive prompt/result/error paths without provider-specific branches."""
19
+ prompt_root = _context_root(ctx, "RUN_PROMPTS", relative)
20
+ result_root = _context_root(ctx, "WORKER_RESULTS", relative)
21
+ task_type = str(ctx.get("TASK_TYPE_SEGMENT", ""))
22
+ prompt_suffix = f"-{task_type}-{ctx.get('RUN_PROMPTS_SEQ', '')}"
23
+ result_suffix = f"-{task_type}-{ctx.get('WORKER_RESULTS_SEQ', '')}"
24
+ return {
25
+ worker_id: {
26
+ "promptPath": str(prompt_root / f"{worker_id}-worker-prompt{prompt_suffix}.md"),
27
+ "resultPath": str(result_root / f"{worker_id}-worker{result_suffix}.md"),
28
+ "errorsSidecarPath": str(
29
+ result_root / f"{worker_id}-worker-errors{result_suffix}.json"
30
+ ),
31
+ }
32
+ for worker_id in artifact_worker_ids()
33
+ }
34
+
35
+
36
+ def _context_root(ctx: Mapping[str, Any], name: str, relative: bool) -> Path:
37
+ if name == "WORKER_RESULTS":
38
+ key = "WORKER_RESULTS_RELATIVE_PATH" if relative else "WORKER_RESULTS_PATH"
39
+ else:
40
+ suffix = "RELATIVE_PATH" if relative else "DIR"
41
+ key = f"{name}_{suffix}"
42
+ value = str(ctx.get(key, ""))
43
+ if value or relative:
44
+ return Path(value)
45
+ relative_value = str(ctx.get(f"{name}_RELATIVE_PATH", ""))
46
+ return Path(str(ctx.get("PROJECT_ROOT", ""))) / relative_value
@@ -8,7 +8,9 @@ from __future__ import annotations
8
8
 
9
9
  from pathlib import Path
10
10
 
11
- ALLOWED_WORKERS = ["claude", "codex", "antigravity", "report-writer"]
11
+ from .models import provider_ids
12
+
13
+ ALLOWED_WORKERS = provider_ids("analyser") + ["report-writer"]
12
14
  DEFAULT_WORKERS = ["claude", "codex", "report-writer"]
13
15
  PROFILE_BULLET_HEADERS = {
14
16
  "- Workers:",
@@ -108,7 +108,7 @@ PHASE_RULES: dict[str, dict[str, str]] = {
108
108
  " - `Out-of-plan edits` block listing every file touched outside the approved plan with rationale (empty block preferred)\n"
109
109
  ' - validation evidence: actual stdout/stderr and exit code for every pre / mid / post command from the plan (no paraphrased "tests pass")\n'
110
110
  " - TDD evidence for TDD-applicable steps: failing-test output before implementation commit and passing-test output after, with framing SHAs\n"
111
- " - per-verifier sections for every verifier in the resolved roster (`Claude verifier`, `Codex verifier`, plus `Antigravity verifier` when opted in) with independent verdict (PASS / CONCERNS / FAIL) and cited diff snippets; dissent is preserved by `Claude lead`\n"
111
+ " - per-verifier sections for every verifier in the resolved roster, with an independent verdict (PASS / CONCERNS / FAIL) and cited diff snippets; dissent is preserved by the Okstra lead\n"
112
112
  " - rollback verification (advisory, never blocks — record the revert path for a human; `result` is ok / not-applicable / advisory — human-run)\n"
113
113
  " - routing recommendation for `final-verification` (ready / needs new error-analysis or planning loop)"
114
114
  ),
@@ -125,7 +125,7 @@ PHASE_RULES: dict[str, dict[str, str]] = {
125
125
  " - entering this phase only when the cited final-verification report's `Verdict Token` is exactly `accepted`\n"
126
126
  " - asking the user (via `AskUserQuestion` / interactive prompt) which delivery action to take: `local checkout`, `push + PR`, or `skip` (end the run)\n"
127
127
  " - asking the user to pick a PR base branch from `staging` | `preprod` | `prod` | `main` | `dev` | a user-supplied branch name\n"
128
- " - drafting PR title and PR body **inline as the Claude lead** (no drafter worker, no `Agent` dispatch); the lead reviews its own draft with the user before any mutating git / gh command runs\n"
128
+ " - drafting PR title and PR body **inline as the Okstra lead** (no drafter worker and no delegated dispatch); the lead reviews its own draft with the user before any mutating git / gh command runs\n"
129
129
  " - read-only git inspection: `git status`, `git diff`, `git log`, `git rev-parse`\n"
130
130
  " - pushing the current feature branch to its origin remote via `git push -u origin <current-branch>` (the feature branch only — NEVER the base branch)\n"
131
131
  " - creating a pull request via `gh pr create --base <chosen-base> --head <current-branch>`; if a PR with the same head already exists, surface its URL and skip creation\n"
@@ -190,6 +190,8 @@ def compute_workflow_state(
190
190
  else:
191
191
  if current_task_status in (
192
192
  "instruction-set-generated",
193
+ "ready-for-lead",
194
+ # Compatibility with task manifests prepared before provider-neutral statuses.
193
195
  "ready-for-claude",
194
196
  "claude-session-started",
195
197
  ):
@@ -21,6 +21,7 @@ from .pricing import (
21
21
  claude_cost_usd,
22
22
  codex_cost_usd,
23
23
  antigravity_cost_usd,
24
+ provider_cost_usd,
24
25
  )
25
26
  from .report import (
26
27
  SubstituteRefusedError,
@@ -20,7 +20,9 @@ from .antigravity import (
20
20
  find_antigravity_sessions,
21
21
  )
22
22
  from .paths import claude_project_dir, utc_now
23
- from .pricing import codex_cost_usd, antigravity_cost_usd
23
+ from .pricing import antigravity_cost_usd, provider_cost_usd
24
+ from okstra_ctl.models import provider_wrappers
25
+ from okstra_ctl.lead_runtime import ARTIFACT_ONLY_LEAD_RUNTIMES
24
26
  from okstra_ctl.wrapper_status import read_wrapper_status, status_path_for_prompt
25
27
 
26
28
 
@@ -446,14 +448,16 @@ def _codex_worker_windows(project_root: Path, state: dict) -> dict[str, list[tup
446
448
  return windows
447
449
 
448
450
 
449
- def _codex_runtime_provider(worker: dict) -> str:
451
+ def _cli_assignment_provider(worker: dict) -> str:
450
452
  worker_id = str(worker.get("workerId") or "").strip()
451
- agent = str(worker.get("agent") or "").strip()
452
- if worker_id == "report-writer":
453
- return "codex"
454
- if agent in {"codex", "antigravity"}:
455
- return agent
456
- return ""
453
+ explicit_provider = str(worker.get("provider") or "").strip()
454
+ provider = explicit_provider or str(worker.get("agent") or "").strip()
455
+ runner = str(worker.get("runner") or "").strip()
456
+ if runner and runner != "cli-wrapper":
457
+ return ""
458
+ if not explicit_provider and worker_id == "report-writer":
459
+ provider = "codex"
460
+ return provider if provider in provider_wrappers() else ""
457
461
 
458
462
 
459
463
  def _cli_sessions_for_windows(
@@ -482,8 +486,10 @@ def _cli_session_totals(provider: str, session_paths: list[Path]) -> list[dict]:
482
486
  for session_path in session_paths:
483
487
  if provider == "codex":
484
488
  total = codex_session_total(session_path)
485
- else:
489
+ elif provider == "antigravity":
486
490
  total = antigravity_session_total(session_path)
491
+ else:
492
+ continue
487
493
  if total.get("available"):
488
494
  totals.append(total)
489
495
  return totals
@@ -496,8 +502,9 @@ def _cli_usage_block(provider: str, totals: dict, session_paths: list[Path]) ->
496
502
  if totals.get("model"):
497
503
  block["model"] = totals["model"]
498
504
  block["cliModel"] = totals["model"]
499
- if provider == "codex":
500
- cost = codex_cost_usd(
505
+ if provider in {"codex", "grok", "kimi"}:
506
+ cost = provider_cost_usd(
507
+ provider,
501
508
  totals.get("model"),
502
509
  totals.get("inputTokens", 0) or 0,
503
510
  totals.get("cachedInputTokens", 0) or 0,
@@ -627,7 +634,7 @@ def _attach_cli_usage(
627
634
  ) -> None:
628
635
  """Layer aggregated CLI token/cost onto a Claude-side worker ``block``.
629
636
 
630
- A codex/antigravity worker re-dispatched within one aggregated wrapper
637
+ A CLI-wrapper worker re-dispatched within one aggregated wrapper
631
638
  window produces several rollout jsonls; pricing only the latest (the old
632
639
  ``find_codex_session`` -> ``sessions[-1]`` behavior) dropped the earlier
633
640
  attempts' tokens. We sum every in-window session here so the redispatched
@@ -653,18 +660,19 @@ def _attach_cli_usage(
653
660
  block[key] = cli[key]
654
661
 
655
662
 
656
- def _collect_codex_runtime_usage(state: dict, project_root: Path) -> dict:
663
+ def _collect_cli_runtime_usage(state: dict, project_root: Path) -> dict:
657
664
  windows_by_worker = _codex_worker_windows(project_root, state)
658
665
  state["leadUsage"] = na_block(
659
- "Codex lead token accounting is not available yet; worker CLI usage is collected from provider logs."
666
+ "Host lead token accounting is unavailable; CLI worker usage is collected only from attributable provider logs."
660
667
  )
661
668
  for worker in state.get("workers", []):
662
669
  if not isinstance(worker, dict):
663
670
  continue
664
- provider = _codex_runtime_provider(worker)
671
+ provider = _cli_assignment_provider(worker)
665
672
  if not provider:
666
673
  worker["usage"] = na_block(
667
- f"worker provider is not CLI-backed in Codex runtime: {worker.get('agent') or worker.get('workerId')}"
674
+ "worker is host-native or has no registered CLI provider: "
675
+ f"{worker.get('provider') or worker.get('agent') or worker.get('workerId')}"
668
676
  )
669
677
  continue
670
678
  worker_id = str(worker.get("workerId") or "").strip()
@@ -745,7 +753,7 @@ def _populate_usage_summary(
745
753
  "definitions": {
746
754
  "totalTokens": "Sum of input + output + cache_creation + cache_read tokens (raw processed volume; matches Anthropic API breakdown). Cache reads are 95%+ in long sessions.",
747
755
  "billableEquivalentTokens": "Tokens normalized to base-input-price units (cache_creation_5m x1.25, cache_creation_1h x2.0, cache_read x0.1, output x5). 5m vs 1h is split from usage.cache_creation when the API breakdown is present; otherwise all cache_creation falls into 5m.",
748
- "estimatedCostUsd": "USD cost using public list pricing for the model recorded in the session. cliWorkers covers Codex/Antigravity CLI calls billed under those providers.",
756
+ "estimatedCostUsd": "USD cost using public list pricing for the model recorded in the session. cliWorkers covers attributable registered-provider CLI calls.",
749
757
  },
750
758
  }
751
759
 
@@ -761,8 +769,8 @@ def collect(team_state_path: Path, project_root: Path | None = None, *,
761
769
  task_key = state.get("taskKey", "")
762
770
  team_name = resolve_team_name(state)
763
771
  lead_sid = (state.get("lead") or {}).get("sessionId")
764
- if state.get("leadRuntime") == "codex":
765
- return _collect_codex_runtime_usage(state, cwd)
772
+ if state.get("leadRuntime") in ARTIFACT_ONLY_LEAD_RUNTIMES:
773
+ return _collect_cli_runtime_usage(state, cwd)
766
774
 
767
775
  # 1) Claude sessions (lead + claude-side workers). Cache totals at scan
768
776
  # time so we don't re-read the jsonl when a worker matches multiple
@@ -878,9 +886,10 @@ def collect(team_state_path: Path, project_root: Path | None = None, *,
878
886
  block["additionalSessionIds"] = [sid for sid, _, _ in matched[1:]]
879
887
  block["matchedAgentNames"] = sorted({t.get("agentName") for _, _, t in matched if t.get("agentName")})
880
888
 
881
- # For codex/antigravity workers, sum every CLI session that fell inside
882
- # the aggregated wrapper window (re-dispatches leave one rollout each).
883
- if agent in ("codex", "antigravity"):
889
+ # For CLI-wrapper workers, retain execution evidence and attach any
890
+ # attributable provider transcript usage without treating absence as zero.
891
+ provider = _cli_assignment_provider(worker)
892
+ if provider:
884
893
  windows, status_path, used_fallback = _worker_cli_windows(
885
894
  cwd,
886
895
  worker,
@@ -888,7 +897,7 @@ def collect(team_state_path: Path, project_root: Path | None = None, *,
888
897
  )
889
898
  _attach_cli_usage(
890
899
  block,
891
- agent,
900
+ provider,
892
901
  cwd,
893
902
  windows,
894
903
  status_path,