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