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
@@ -13,6 +13,7 @@ from okstra_project.dirs import (
13
13
  )
14
14
 
15
15
  from .paths import runs_dir_of, task_manifest_file, task_timeline_file
16
+ from .worker_artifacts import artifacts_from_context
16
17
 
17
18
  RUN_CONTEXT_KIND = "run-context"
18
19
  RUN_CONTEXT_SCHEMA_VERSION = "2.0"
@@ -162,7 +163,7 @@ def _compact_active_workers(payload: Mapping[str, Any]) -> list[dict[str, Any]]:
162
163
  def _compact_active_worker(worker: Mapping[str, Any]) -> dict[str, Any]:
163
164
  keep = (
164
165
  "workerId", "role", "agent", "agentLabel", "model",
165
- "modelExecutionValue", "attemptRequired",
166
+ "provider", "runner", "modelExecutionValue", "attemptRequired",
166
167
  )
167
168
  return {key: worker.get(key, "") for key in keep}
168
169
 
@@ -242,19 +243,19 @@ def _hydrate_active_worker(
242
243
  ) -> dict[str, Any]:
243
244
  worker_id = str(worker.get("workerId", ""))
244
245
  hydrated = dict(worker)
245
- hydrated["promptPath"] = ctx.get(_worker_path_key(worker_id, "prompt"), "")
246
- hydrated["resultPath"] = ctx.get(_worker_path_key(worker_id, "result"), "")
246
+ paths = artifacts_from_context(ctx, relative=True).get(worker_id, {})
247
+ hydrated["promptPath"] = paths.get("promptPath", "")
248
+ hydrated["resultPath"] = paths.get("resultPath", "")
247
249
  return hydrated
248
250
 
249
251
 
250
252
  def _hydrate_active_error_logs(ctx: Mapping[str, str]) -> dict[str, Any]:
253
+ artifacts = artifacts_from_context(ctx, relative=True)
251
254
  return {
252
255
  "runErrorsLogPath": ctx.get("RUN_ERRORS_LOG_RELATIVE_PATH", ""),
253
256
  "sidecarsByWorkerId": {
254
- "claude": ctx.get("CLAUDE_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", ""),
255
- "codex": ctx.get("CODEX_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", ""),
256
- "antigravity": ctx.get("ANTIGRAVITY_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", ""),
257
- "report-writer": ctx.get("REPORT_WRITER_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", ""),
257
+ worker_id: paths["errorsSidecarPath"]
258
+ for worker_id, paths in artifacts.items()
258
259
  },
259
260
  }
260
261
 
@@ -268,23 +269,6 @@ def _hydrate_active_source_artifacts(ctx: Mapping[str, str]) -> dict[str, str]:
268
269
  }
269
270
 
270
271
 
271
- def _worker_path_key(worker_id: str, kind: str) -> str:
272
- by_worker = {
273
- "claude": ("CLAUDE_WORKER_PROMPT_RELATIVE_PATH", "CLAUDE_WORKER_RESULT_RELATIVE_PATH"),
274
- "codex": ("CODEX_WORKER_PROMPT_RELATIVE_PATH", "CODEX_WORKER_RESULT_RELATIVE_PATH"),
275
- "antigravity": (
276
- "ANTIGRAVITY_WORKER_PROMPT_RELATIVE_PATH",
277
- "ANTIGRAVITY_WORKER_RESULT_RELATIVE_PATH",
278
- ),
279
- "report-writer": (
280
- "REPORT_WRITER_WORKER_PROMPT_RELATIVE_PATH",
281
- "REPORT_WRITER_WORKER_RESULT_RELATIVE_PATH",
282
- ),
283
- }
284
- index = 0 if kind == "prompt" else 1
285
- return by_worker.get(worker_id, ("", ""))[index]
286
-
287
-
288
272
  def _compact_run_hints(ctx: Mapping[str, Any]) -> dict[str, Any]:
289
273
  return {
290
274
  "runRoot": ctx.get("RUN_DIR_RELATIVE_PATH", ""),
@@ -421,7 +405,7 @@ def _run_files(
421
405
  "run_manifest_file": run_manifests / f"run-manifest{suffixes['manifests']}.json",
422
406
  "run_context_file": run_manifests / f"run-context-{task_type_segment}-{sequences['manifests']}.json",
423
407
  "run_inputs_file": run_manifests / f"run-inputs-{task_type_segment}-{sequences['manifests']}.json",
424
- "run_prompt_snapshot": run_prompts / f"claude-execution-prompt{suffixes['prompts']}.md",
408
+ "run_prompt_snapshot": run_prompts / f"lead-execution-prompt{suffixes['prompts']}.md",
425
409
  "claude_worker_prompt": run_prompts / f"claude-worker-prompt{suffixes['prompts']}.md",
426
410
  "codex_worker_prompt": run_prompts / f"codex-worker-prompt{suffixes['prompts']}.md",
427
411
  "antigravity_worker_prompt": run_prompts / f"antigravity-worker-prompt{suffixes['prompts']}.md",
@@ -485,7 +485,7 @@ def compute_run_paths(
485
485
  run_file_suffix = suffixes["reports"]
486
486
 
487
487
  run_manifest_file = run_manifests / f"run-manifest{suffixes['manifests']}.json"
488
- run_prompt_snapshot = run_prompts / f"claude-execution-prompt{suffixes['prompts']}.md"
488
+ run_prompt_snapshot = run_prompts / f"lead-execution-prompt{suffixes['prompts']}.md"
489
489
  claude_worker_prompt = run_prompts / f"claude-worker-prompt{suffixes['prompts']}.md"
490
490
  codex_worker_prompt = run_prompts / f"codex-worker-prompt{suffixes['prompts']}.md"
491
491
  antigravity_worker_prompt = run_prompts / f"antigravity-worker-prompt{suffixes['prompts']}.md"
@@ -32,8 +32,11 @@ from . import fix_cycles
32
32
  from .analysis_inputs import ANALYSIS_TASK_TYPES
33
33
  from .paths import okstra_home
34
34
  from .lead_runtime import lead_runtime_info
35
+ from .models import UnknownProviderError, provider_ids, provider_spec
36
+ from .runner_resolution import native_provider_for_host
35
37
  from .path_hints import compact_active_run_context, hydrate_run_context
36
38
  from .paths import task_timeline_file
39
+ from .worker_artifacts import artifacts_from_context
37
40
  from .workflow import DEFAULT_NEXT_PHASE, PHASE_SEQUENCE
38
41
 
39
42
 
@@ -250,51 +253,106 @@ def _resolve_workers(ctx: dict) -> list[str]:
250
253
  ]
251
254
 
252
255
 
253
- def _worker_catalog(ctx: dict) -> dict:
256
+ def _assignment_runner_for_ctx(ctx: dict, provider: str) -> str:
257
+ native_provider = native_provider_for_host(_lead_runtime(ctx))
258
+ return "native-session" if provider == native_provider else "cli-wrapper"
259
+
260
+
261
+ def _lead_assignment(ctx: dict) -> dict:
262
+ raw = ctx.get("LEAD_ASSIGNMENT_JSON", "")
263
+ if raw:
264
+ try:
265
+ parsed = json.loads(raw)
266
+ if isinstance(parsed, dict):
267
+ return parsed
268
+ except json.JSONDecodeError:
269
+ pass
270
+ provider = (
271
+ ctx.get("LEAD_PROVIDER", "")
272
+ or native_provider_for_host(_lead_runtime(ctx))
273
+ or "claude"
274
+ )
254
275
  return {
255
- "claude": {
256
- "workerId": "claude",
257
- "role": "Claude worker",
258
- "agent": "claude",
259
- "agentLabel": "Claude Code",
260
- "model": ctx.get("CLAUDE_WORKER_MODEL", ""),
261
- "modelExecutionValue": ctx.get("CLAUDE_WORKER_MODEL_EXECUTION_VALUE", ""),
262
- "resultPath": ctx.get("CLAUDE_WORKER_RESULT_RELATIVE_PATH", ""),
263
- "promptPath": ctx.get("CLAUDE_WORKER_PROMPT_RELATIVE_PATH", ""),
264
- },
265
- "codex": {
266
- "workerId": "codex",
267
- "role": "Codex worker",
268
- "agent": "codex",
269
- "agentLabel": "Codex",
270
- "model": ctx.get("CODEX_WORKER_MODEL", ""),
271
- "modelExecutionValue": ctx.get("CODEX_WORKER_MODEL_EXECUTION_VALUE", ""),
272
- "resultPath": ctx.get("CODEX_WORKER_RESULT_RELATIVE_PATH", ""),
273
- "promptPath": ctx.get("CODEX_WORKER_PROMPT_RELATIVE_PATH", ""),
274
- },
275
- "antigravity": {
276
- "workerId": "antigravity",
277
- "role": "Antigravity worker",
278
- "agent": "antigravity",
279
- "agentLabel": "Antigravity",
280
- "model": ctx.get("ANTIGRAVITY_WORKER_MODEL", ""),
281
- "modelExecutionValue": ctx.get("ANTIGRAVITY_WORKER_MODEL_EXECUTION_VALUE", ""),
282
- "resultPath": ctx.get("ANTIGRAVITY_WORKER_RESULT_RELATIVE_PATH", ""),
283
- "promptPath": ctx.get("ANTIGRAVITY_WORKER_PROMPT_RELATIVE_PATH", ""),
284
- },
285
- "report-writer": {
286
- "workerId": "report-writer",
287
- "role": "Report writer worker",
288
- "agent": "claude",
289
- "agentLabel": "Claude Code",
290
- "model": ctx.get("REPORT_WRITER_MODEL", ""),
291
- "modelExecutionValue": ctx.get("REPORT_WRITER_MODEL_EXECUTION_VALUE", ""),
292
- "resultPath": ctx.get("REPORT_WRITER_WORKER_RESULT_RELATIVE_PATH", ""),
293
- "promptPath": ctx.get("REPORT_WRITER_WORKER_PROMPT_RELATIVE_PATH", ""),
294
- },
276
+ "role": "lead",
277
+ "provider": provider,
278
+ "model": ctx.get("LEAD_MODEL", ""),
279
+ "modelExecutionValue": ctx.get("LEAD_MODEL_EXECUTION_VALUE", ""),
280
+ "runner": _assignment_runner_for_ctx(ctx, provider),
295
281
  }
296
282
 
297
283
 
284
+ def _worker_assignments(ctx: dict) -> list[dict]:
285
+ raw = ctx.get("WORKER_ASSIGNMENTS_JSON", "")
286
+ if raw:
287
+ try:
288
+ parsed = json.loads(raw)
289
+ if isinstance(parsed, list) and all(isinstance(row, dict) for row in parsed):
290
+ return parsed
291
+ except json.JSONDecodeError:
292
+ pass
293
+ catalog = _worker_catalog(ctx, include_assignments=False)
294
+ return [
295
+ {
296
+ "workerId": worker_id,
297
+ "role": "report-writer" if worker_id == "report-writer" else "analyser",
298
+ "provider": catalog[worker_id]["agent"],
299
+ "model": catalog[worker_id]["model"],
300
+ "modelExecutionValue": catalog[worker_id]["modelExecutionValue"],
301
+ "runner": _assignment_runner_for_ctx(ctx, catalog[worker_id]["agent"]),
302
+ }
303
+ for worker_id in _resolve_workers(ctx)
304
+ ]
305
+
306
+
307
+ def _worker_catalog(ctx: dict, *, include_assignments: bool = True) -> dict:
308
+ assignments = {} if not include_assignments else {
309
+ row.get("workerId"): row for row in _worker_assignments(ctx)
310
+ if row.get("workerId")
311
+ }
312
+ catalog = {}
313
+ for worker_id, paths in artifacts_from_context(ctx, relative=True).items():
314
+ assignment = assignments.get(worker_id, {})
315
+ provider = assignment.get("provider") or _fallback_worker_provider(ctx, worker_id)
316
+ provider_info = provider_spec(provider)
317
+ model, execution = _legacy_worker_model(ctx, worker_id)
318
+ catalog[worker_id] = {
319
+ "workerId": worker_id,
320
+ "role": (
321
+ "Report writer worker" if worker_id == "report-writer"
322
+ else f"{provider_spec(worker_id).display_label} worker"
323
+ ),
324
+ "agent": provider,
325
+ "agentLabel": provider_info.display_label,
326
+ "provider": provider,
327
+ "runner": assignment.get(
328
+ "runner", _assignment_runner_for_ctx(ctx, provider),
329
+ ),
330
+ "model": assignment.get("model", model),
331
+ "modelExecutionValue": assignment.get("modelExecutionValue", execution),
332
+ **paths,
333
+ }
334
+ return catalog
335
+
336
+
337
+ def _fallback_worker_provider(ctx: dict, worker_id: str) -> str:
338
+ if worker_id == "report-writer":
339
+ return ctx.get("REPORT_WRITER_PROVIDER", "") or "claude"
340
+ return worker_id
341
+
342
+
343
+ def _legacy_worker_model(ctx: dict, worker_id: str) -> tuple[str, str]:
344
+ token = "REPORT_WRITER" if worker_id == "report-writer" else (
345
+ worker_id.upper().replace("-", "_")
346
+ )
347
+ return (
348
+ ctx.get(f"{token}_WORKER_MODEL", ctx.get(f"{token}_MODEL", "")),
349
+ ctx.get(
350
+ f"{token}_WORKER_MODEL_EXECUTION_VALUE",
351
+ ctx.get(f"{token}_MODEL_EXECUTION_VALUE", ""),
352
+ ),
353
+ )
354
+
355
+
298
356
  def _active_workers(ctx: dict) -> list[dict]:
299
357
  catalog = _worker_catalog(ctx)
300
358
  workers = []
@@ -305,6 +363,8 @@ def _active_workers(ctx: dict) -> list[dict]:
305
363
  "role": item["role"],
306
364
  "agent": item["agent"],
307
365
  "agentLabel": item["agentLabel"],
366
+ "provider": item["provider"],
367
+ "runner": item["runner"],
308
368
  "model": item["model"],
309
369
  "modelExecutionValue": item["modelExecutionValue"],
310
370
  "promptPath": item["promptPath"],
@@ -378,13 +438,12 @@ def _active_instruction_set(ctx: dict) -> dict:
378
438
 
379
439
 
380
440
  def _active_error_logs(ctx: dict) -> dict:
441
+ artifacts = artifacts_from_context(ctx, relative=True)
381
442
  return {
382
443
  "runErrorsLogPath": ctx.get("RUN_ERRORS_LOG_RELATIVE_PATH", ""),
383
444
  "sidecarsByWorkerId": {
384
- "claude": ctx.get("CLAUDE_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", ""),
385
- "codex": ctx.get("CODEX_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", ""),
386
- "antigravity": ctx.get("ANTIGRAVITY_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", ""),
387
- "report-writer": ctx.get("REPORT_WRITER_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", ""),
445
+ worker_id: paths["errorsSidecarPath"]
446
+ for worker_id, paths in artifacts.items()
388
447
  },
389
448
  }
390
449
 
@@ -496,6 +555,8 @@ def render_team_state(team_state_path: str, ctx: dict) -> None:
496
555
  "workerId": m["workerId"],
497
556
  "role": m["role"],
498
557
  "agent": m["agent"],
558
+ "provider": m["provider"],
559
+ "runner": m["runner"],
499
560
  "model": m["model"],
500
561
  "modelExecutionValue": m["modelExecutionValue"],
501
562
  "status": "not-run",
@@ -508,7 +569,10 @@ def render_team_state(team_state_path: str, ctx: dict) -> None:
508
569
  "schemaVersion": "1.0",
509
570
  "taskKey": ctx.get("TASK_KEY", ""),
510
571
  "taskType": ctx.get("TASK_TYPE", ""),
572
+ "hostRuntime": ctx.get("HOST_RUNTIME", "") or _lead_runtime(ctx),
511
573
  "leadRuntime": _lead_runtime(ctx),
574
+ "leadAssignment": _lead_assignment(ctx),
575
+ "workerAssignments": _worker_assignments(ctx),
512
576
  "leadAdapter": _lead_adapter(ctx),
513
577
  "leadEventsPath": ctx.get("LEAD_EVENTS_RELATIVE_PATH", ""),
514
578
  "runDirectoryPath": ctx.get("RUN_DIR_RELATIVE_PATH", ""),
@@ -516,6 +580,8 @@ def render_team_state(team_state_path: str, ctx: dict) -> None:
516
580
  "lead": {
517
581
  "role": _lead_role(ctx),
518
582
  "agent": _lead_agent(ctx),
583
+ "provider": _lead_assignment(ctx).get("provider", ""),
584
+ "runner": _lead_assignment(ctx).get("runner", ""),
519
585
  "model": ctx.get("LEAD_MODEL", ""),
520
586
  "modelExecutionValue": ctx.get("LEAD_MODEL_EXECUTION_VALUE", ""),
521
587
  "status": ctx.get("CURRENT_RUN_STATUS", ""),
@@ -810,6 +876,7 @@ def migrate_legacy_run_artifacts(ctx: dict) -> None:
810
876
  legacy_targets = [
811
877
  ("run-manifest-", ".json", Path(ctx["RUN_MANIFESTS_DIR"])),
812
878
  ("team-state-", ".json", Path(ctx["RUN_STATE_DIR"])),
879
+ ("lead-execution-prompt-", ".md", Path(ctx["RUN_PROMPTS_DIR"])),
813
880
  ("claude-execution-prompt-", ".md", Path(ctx["RUN_PROMPTS_DIR"])),
814
881
  ("final-report-", ".md", Path(ctx["RUN_REPORTS_DIR"])),
815
882
  ("final-", ".status", Path(ctx["RUN_STATUS_DIR"])),
@@ -866,6 +933,8 @@ def _required_worker_roles(ctx: dict, reviewers: list[str]) -> list[dict]:
866
933
  "workerId": catalog[item]["workerId"],
867
934
  "role": catalog[item]["role"],
868
935
  "agent": catalog[item]["agent"],
936
+ "provider": catalog[item]["provider"],
937
+ "runner": catalog[item]["runner"],
869
938
  "model": catalog[item]["model"],
870
939
  "modelExecutionValue": catalog[item]["modelExecutionValue"],
871
940
  "resultPath": catalog[item]["resultPath"],
@@ -1093,7 +1162,10 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
1093
1162
  "taskIdPathSegment": ctx.get("TASK_ID_SEGMENT", ""),
1094
1163
  "projectRoot": ctx.get("PROJECT_ROOT", ""),
1095
1164
  "taskType": ctx.get("TASK_TYPE", ""),
1165
+ "hostRuntime": ctx.get("HOST_RUNTIME", "") or _lead_runtime(ctx),
1096
1166
  "leadRuntime": _lead_runtime(ctx),
1167
+ "leadAssignment": _lead_assignment(ctx),
1168
+ "workerAssignments": _worker_assignments(ctx),
1097
1169
  "leadAdapter": _lead_adapter(ctx),
1098
1170
  "workCategory": work_category,
1099
1171
  # user-managed status (set-work-status CLI) — carried across re-renders
@@ -1151,6 +1223,8 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
1151
1223
  "referenceExpectationsPath": ctx.get(
1152
1224
  "REFERENCE_EXPECTATIONS_RELATIVE_PATH", ""
1153
1225
  ),
1226
+ "leadExecutionPromptPath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "")
1227
+ + "/lead-execution-prompt.md",
1154
1228
  "claudeExecutionPromptPath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "")
1155
1229
  + "/claude-execution-prompt.md",
1156
1230
  "leadPromptSnapshotPath": ctx.get("RUN_PROMPT_SNAPSHOT_RELATIVE_PATH", ""),
@@ -1174,7 +1248,7 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
1174
1248
  "leadExecutionMode": "synthesis-only",
1175
1249
  "finalSynthesisOwner": lead_role,
1176
1250
  "artifactFirst": True,
1177
- "resultCollectionMode": "claude-managed",
1251
+ "resultCollectionMode": "lead-managed",
1178
1252
  "finalReportFormat": "markdown",
1179
1253
  "finalReportFilename": ctx.get("FINAL_REPORT_FILENAME", ""),
1180
1254
  "finalStatusFilename": ctx.get("FINAL_STATUS_FILENAME", ""),
@@ -1182,7 +1256,7 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
1182
1256
  "requiredWorkerAttempts": reviewers,
1183
1257
  "requiredWorkerRoles": required_worker_roles,
1184
1258
  "requiredAgentStatusEntries": required_agent_status_entries,
1185
- "requireDistinctLeadFromClaudeWorker": True,
1259
+ "requireDistinctLeadFromWorkerSession": True,
1186
1260
  "requireAllRequiredWorkerAttempts": True,
1187
1261
  "requireAntigravityWorkerAttempt": "antigravity" in reviewers,
1188
1262
  "requireCollectedWorkerStatusesBeforeFinalVerdict": True,
@@ -1227,14 +1301,20 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
1227
1301
  "teamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
1228
1302
  "validatorScriptPath": ctx.get("RUN_VALIDATOR_RELATIVE_PATH", ""),
1229
1303
  },
1230
- "claudeSession": {
1304
+ "leadSession": {
1231
1305
  "sessionId": ctx.get("CLAUDE_SESSION_ID", ""),
1232
1306
  "resumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
1307
+ "accounting": _lead_info(ctx).session_accounting,
1233
1308
  },
1234
1309
  "convergence": convergence_block,
1235
1310
  "createdAt": existing.get("createdAt") or ctx.get("RUN_TIMESTAMP_ISO", ""),
1236
1311
  "updatedAt": ctx.get("RUN_TIMESTAMP_ISO", ""),
1237
1312
  }
1313
+ if _lead_runtime(ctx) == "claude-code":
1314
+ payload["claudeSession"] = {
1315
+ "sessionId": ctx.get("CLAUDE_SESSION_ID", ""),
1316
+ "resumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
1317
+ }
1238
1318
  _write_json(path, payload)
1239
1319
 
1240
1320
 
@@ -1255,7 +1335,7 @@ def _build_convergence_block(ctx: dict) -> dict:
1255
1335
  ctx knobs honoured:
1256
1336
  - `OKSTRA_PLAN_VERIFICATION`: "true" | "false" | "" (empty → default True).
1257
1337
  Wired from CLI `--no-plan-verification` (sets "false").
1258
- - `CRITIC_CHOICE`: "" | "off" | "claude" | "codex" | "antigravity" — critic
1338
+ - `CRITIC_CHOICE`: "" | "off" | a provider with the critic capability — critic
1259
1339
  backing provider (enabled only for requirements-discovery / error-analysis /
1260
1340
  implementation-planning / final-verification); model taken from that
1261
1341
  provider's execution value.
@@ -1277,16 +1357,24 @@ def _build_convergence_block(ctx: dict) -> dict:
1277
1357
  # Independent of `adversarial_phases` above (they answer different questions and
1278
1358
  # may diverge): the coverage critic is opt-in for the finding-producing phases.
1279
1359
  critic_phases = {"requirements-discovery", "error-analysis", "implementation-planning", "final-verification"}
1280
- critic_exec_key = {
1281
- "claude": "CLAUDE_WORKER_MODEL_EXECUTION_VALUE",
1282
- "codex": "CODEX_WORKER_MODEL_EXECUTION_VALUE",
1283
- "antigravity": "ANTIGRAVITY_WORKER_MODEL_EXECUTION_VALUE",
1284
- }
1285
- critic_enabled = critic_choice in critic_exec_key and task_type in critic_phases
1360
+ critic_enabled = critic_choice in provider_ids("critic") and task_type in critic_phases
1361
+ assignment = next(
1362
+ (
1363
+ row for row in _worker_assignments(ctx)
1364
+ if row.get("provider") == critic_choice and row.get("role") != "report-writer"
1365
+ ),
1366
+ {},
1367
+ )
1368
+ _, legacy_execution = _legacy_worker_model(ctx, critic_choice)
1286
1369
  critic_block = {
1287
1370
  "enabled": critic_enabled,
1288
1371
  "provider": critic_choice if critic_enabled else None,
1289
- "modelExecutionValue": (ctx.get(critic_exec_key[critic_choice]) or None) if critic_enabled else None,
1372
+ "modelExecutionValue": (
1373
+ ctx.get("CRITIC_MODEL_EXECUTION_VALUE")
1374
+ or assignment.get("modelExecutionValue")
1375
+ or legacy_execution
1376
+ or None
1377
+ ) if critic_enabled else None,
1290
1378
  }
1291
1379
  return {
1292
1380
  "enabled": True,
@@ -1353,9 +1441,12 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
1353
1441
  "taskId": ctx.get("TASK_ID", ""),
1354
1442
  "taskKey": ctx.get("TASK_KEY", ""),
1355
1443
  "taskType": ctx.get("TASK_TYPE", ""),
1444
+ "hostRuntime": ctx.get("HOST_RUNTIME", "") or _lead_runtime(ctx),
1356
1445
  "leadRuntime": _lead_runtime(ctx),
1357
1446
  "leadRuntimeRequest": ctx.get("LEAD_RUNTIME_REQUEST", "") or _lead_runtime(ctx),
1358
1447
  "runtimeResolution": _runtime_resolution(ctx),
1448
+ "leadAssignment": _lead_assignment(ctx),
1449
+ "workerAssignments": _worker_assignments(ctx),
1359
1450
  "leadAdapter": _lead_adapter(ctx),
1360
1451
  "workCategory": task_manifest.get(
1361
1452
  "workCategory", ctx.get("WORKFLOW_WORK_CATEGORY", "unknown")
@@ -1405,7 +1496,7 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
1405
1496
  "workerResultsDirectoryPath": ctx.get("WORKER_RESULTS_RELATIVE_PATH", ""),
1406
1497
  "reportTemplatePath": ctx.get("FINAL_REPORT_TEMPLATE_RELATIVE_PATH", ""),
1407
1498
  "validatorScriptPath": ctx.get("RUN_VALIDATOR_RELATIVE_PATH", ""),
1408
- "claudeSessionId": ctx.get("CLAUDE_SESSION_ID", ""),
1499
+ "leadSessionId": ctx.get("CLAUDE_SESSION_ID", ""),
1409
1500
  "resumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
1410
1501
  "concurrentRun": {
1411
1502
  "detected": bool(concurrent_run_stages),
@@ -1446,7 +1537,7 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
1446
1537
  "requiredWorkerRoles": required_worker_roles,
1447
1538
  "requiredAgentStatusEntries": [lead_role]
1448
1539
  + [catalog[item]["role"] for item in reviewers],
1449
- "requireDistinctLeadFromClaudeWorker": True,
1540
+ "requireDistinctLeadFromWorkerSession": True,
1450
1541
  "requireAllRequiredWorkerAttempts": True,
1451
1542
  "requireAntigravityWorkerAttempt": "antigravity" in reviewers,
1452
1543
  "requireCollectedWorkerStatusesBeforeFinalVerdict": True,
@@ -1485,6 +1576,8 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
1485
1576
  if ctx.get("TASK_TYPE") == "implementation-planning"
1486
1577
  else []
1487
1578
  )
1579
+ if _lead_runtime(ctx) == "claude-code":
1580
+ payload["claudeSessionId"] = ctx.get("CLAUDE_SESSION_ID", "")
1488
1581
  _write_json(Path(run_manifest_path), payload)
1489
1582
 
1490
1583
 
@@ -1497,12 +1590,7 @@ def render_timeline(timeline_path: str, ctx: dict) -> None:
1497
1590
  except Exception:
1498
1591
  task_manifest = {}
1499
1592
  reviewers = _resolve_workers(ctx)
1500
- worker_prompt_paths = {
1501
- "claude": ctx.get("CLAUDE_WORKER_PROMPT_RELATIVE_PATH", ""),
1502
- "codex": ctx.get("CODEX_WORKER_PROMPT_RELATIVE_PATH", ""),
1503
- "antigravity": ctx.get("ANTIGRAVITY_WORKER_PROMPT_RELATIVE_PATH", ""),
1504
- "report-writer": ctx.get("REPORT_WRITER_WORKER_PROMPT_RELATIVE_PATH", ""),
1505
- }
1593
+ catalog = _worker_catalog(ctx)
1506
1594
  path = Path(timeline_path)
1507
1595
  existing = {}
1508
1596
  if path.exists():
@@ -1548,7 +1636,7 @@ def render_timeline(timeline_path: str, ctx: dict) -> None:
1548
1636
  "promptSnapshotPath": ctx.get("RUN_PROMPT_SNAPSHOT_RELATIVE_PATH", ""),
1549
1637
  "workerPromptDirectoryPath": ctx.get("RUN_PROMPTS_RELATIVE_PATH", ""),
1550
1638
  "workerPromptPathByWorkerId": {
1551
- item: worker_prompt_paths[item] for item in reviewers
1639
+ item: catalog[item]["promptPath"] for item in reviewers
1552
1640
  },
1553
1641
  "reportPath": ctx.get("LATEST_REPORT_RELATIVE_PATH")
1554
1642
  or ctx.get("FINAL_REPORT_RELATIVE_PATH", ""),
@@ -1654,6 +1742,24 @@ def render_task_index(template_path: str, output_path: str, ctx: dict) -> None:
1654
1742
  or ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", "")
1655
1743
  or "--"
1656
1744
  )
1745
+ model_assignment_lines = [
1746
+ f"- `{lead_role}`: `{rc.get('leadModel', ctx.get('LEAD_MODEL', ''))}`"
1747
+ ]
1748
+ for assignment in task_manifest.get("workerAssignments", []):
1749
+ if not isinstance(assignment, dict):
1750
+ continue
1751
+ worker_id = assignment.get("workerId", "")
1752
+ provider = assignment.get("provider", "")
1753
+ if worker_id == "report-writer":
1754
+ label = "Report writer worker"
1755
+ else:
1756
+ try:
1757
+ label = f"{provider_spec(provider).display_label} worker"
1758
+ except (UnknownProviderError, TypeError):
1759
+ label = f"{worker_id} worker"
1760
+ model_assignment_lines.append(
1761
+ f"- `{label}`: `{assignment.get('model', '')}`"
1762
+ )
1657
1763
  mapping = {
1658
1764
  "{{TASK_KEY}}": task_manifest.get("taskKey", ctx.get("TASK_KEY", "")),
1659
1765
  "{{TASK_TYPE}}": task_manifest.get("taskType", ctx.get("TASK_TYPE", "")),
@@ -1686,15 +1792,7 @@ def render_task_index(template_path: str, output_path: str, ctx: dict) -> None:
1686
1792
  "status", ctx.get("VALIDATION_STATUS", "not-run")
1687
1793
  ),
1688
1794
  "{{CLAUDE_RESUME_COMMAND_RELATIVE_PATH}}": latest_resume_command,
1689
- "{{MODEL_ASSIGNMENT_LINES}}": "\n".join(
1690
- [
1691
- f"- `{lead_role}`: `{rc.get('leadModel', ctx.get('LEAD_MODEL', ''))}`",
1692
- f"- `Claude worker`: `{ctx.get('CLAUDE_WORKER_MODEL', '')}`",
1693
- f"- `Codex worker`: `{ctx.get('CODEX_WORKER_MODEL', '')}`",
1694
- f"- `Antigravity worker`: `{ctx.get('ANTIGRAVITY_WORKER_MODEL', '')}`",
1695
- f"- `Report writer worker`: `{ctx.get('REPORT_WRITER_MODEL', '')}`",
1696
- ]
1697
- ),
1795
+ "{{MODEL_ASSIGNMENT_LINES}}": "\n".join(model_assignment_lines),
1698
1796
  "{{TASK_MANIFEST_RELATIVE_PATH}}": task_manifest.get(
1699
1797
  "taskManifestPath", ctx.get("TASK_MANIFEST_RELATIVE_PATH", "")
1700
1798
  ),
@@ -10,19 +10,55 @@ def audit_context(data: dict) -> dict[str, object]:
10
10
  }
11
11
 
12
12
 
13
+ _LEDGER_TEXT_KEYS = ("statement", "evidence", "hypothesis", "question")
14
+ _LEDGER_SOURCE_KEYS = ("source", "confidence", "reason")
15
+
16
+
17
+ def _first_filled(row: dict, keys: tuple[str, ...]) -> str:
18
+ for key in keys:
19
+ value = row.get(key)
20
+ if value:
21
+ return str(value)
22
+ return ""
23
+
24
+
25
+ def _ledger_row(row: dict) -> dict[str, object]:
26
+ """Flatten one evidence row into the single shape the ledger renders.
27
+
28
+ Every row type that reaches the ledger is ``additionalProperties: false``
29
+ and names its content and source differently, so no row carries another
30
+ row type's keys:
31
+
32
+ - ``evidence.primary`` — ``evidence`` / ``source``
33
+ - ``evidence.secondary`` — ``hypothesis`` / ``confidence``
34
+ - ``analysisCommon.confirmedFacts`` — ``statement`` / ``currentCodeEvidence``
35
+ - ``analysisCommon.inferences`` — ``statement`` / ``confidence``
36
+ - ``analysisCommon.unknowns`` — ``question`` / ``reason``
37
+
38
+ Only this function knows which block a row came from, so the key hunt
39
+ belongs here rather than in the template.
40
+ """
41
+ return {
42
+ "id": row.get("id", ""),
43
+ "text": _first_filled(row, _LEDGER_TEXT_KEYS),
44
+ "codeEvidence": row.get("currentCodeEvidence") or [],
45
+ "source": _first_filled(row, _LEDGER_SOURCE_KEYS),
46
+ }
47
+
48
+
13
49
  def evidence_index(data: dict) -> dict[str, object]:
14
50
  rows: dict[str, object] = {}
15
51
  for section in ("primary", "secondary"):
16
52
  for row in data.get("evidence", {}).get(section, []):
17
53
  row_id = row.get("id")
18
54
  if row_id:
19
- rows[row_id] = row
55
+ rows[row_id] = _ledger_row(row)
20
56
  analysis = data.get("analysisCommon", {})
21
57
  for collection in ("confirmedFacts", "inferences", "unknowns"):
22
58
  for row in analysis.get(collection, []):
23
59
  row_id = row.get("id")
24
60
  if row_id:
25
- rows[row_id] = row
61
+ rows[row_id] = _ledger_row(row)
26
62
  return rows
27
63
 
28
64