okstra 0.163.2 → 0.165.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 +8 -6
- package/docs/architecture.md +24 -15
- package/docs/cli.md +15 -8
- package/docs/for-ai/README.md +2 -2
- package/docs/for-ai/skills/okstra-inspect.md +2 -2
- package/docs/for-ai/skills/okstra-user-response.md +2 -2
- package/docs/project-structure-overview.md +22 -14
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/antigravity-worker.md +9 -7
- package/runtime/agents/workers/claude-worker.md +1 -0
- package/runtime/agents/workers/codex-worker.md +9 -7
- package/runtime/agents/workers/grok-worker.md +6 -4
- package/runtime/agents/workers/kimi-worker.md +6 -4
- package/runtime/bin/lib/okstra/cli.sh +5 -0
- package/runtime/bin/lib/okstra/globals.sh +2 -0
- package/runtime/bin/lib/okstra/usage.sh +5 -5
- package/runtime/bin/okstra-antigravity-exec.sh +1 -340
- package/runtime/bin/okstra-claude-exec.sh +1 -178
- package/runtime/bin/okstra-codex-exec.sh +1 -467
- package/runtime/bin/okstra-provider-exec.py +165 -190
- package/runtime/bin/okstra-trace-cleanup.sh +14 -7
- package/runtime/bin/okstra-wrapper-status.py +26 -19
- package/runtime/bin/okstra.sh +87 -91
- package/runtime/prompts/lead/adapters/cmux.md +2 -2
- package/runtime/prompts/lead/convergence.md +36 -8
- package/runtime/prompts/lead/okstra-lead-contract.md +24 -1
- package/runtime/prompts/lead/plan-body-verification.md +9 -1
- package/runtime/prompts/lead/report-writer.md +1 -0
- package/runtime/prompts/lead/team-contract.md +3 -3
- package/runtime/prompts/profiles/_common-contract.md +9 -1
- package/runtime/prompts/profiles/_coverage-critic.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +3 -1
- package/runtime/prompts/profiles/_implementation-self-check.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -1
- package/runtime/prompts/profiles/implementation-planning.md +6 -4
- package/runtime/python/okstra_ctl/adapters/accounting/__init__.py +11 -0
- package/runtime/python/okstra_ctl/adapters/accounting/claude_jsonl.py +17 -0
- package/runtime/python/okstra_ctl/adapters/accounting/cli_artifact.py +17 -0
- package/runtime/python/okstra_ctl/adapters/accounting/unavailable.py +19 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +92 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +54 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +68 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +60 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/antigravity.md → python/okstra_ctl/adapters/hosts/antigravity/relay.md} +52 -0
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +292 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +120 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/claude-code.md → python/okstra_ctl/adapters/hosts/claude-code/relay.md} +112 -1
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +60 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/codex.md → python/okstra_ctl/adapters/hosts/codex/relay.md} +52 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +72 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/external.md → python/okstra_ctl/adapters/hosts/external/relay.md} +53 -1
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +63 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +90 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +63 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +90 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +183 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +110 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +84 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +76 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +80 -0
- package/runtime/python/okstra_ctl/adapters/providers/kimi/manifest.json +1 -0
- package/runtime/python/okstra_ctl/application/__init__.py +1 -0
- package/runtime/python/okstra_ctl/application/advance_wizard.py +25 -0
- package/runtime/python/okstra_ctl/application/collect_usage.py +15 -0
- package/runtime/python/okstra_ctl/application/dispatch_assignments.py +15 -0
- package/runtime/python/okstra_ctl/application/resolve_assignment.py +93 -0
- package/runtime/python/okstra_ctl/application/resume_run.py +21 -0
- package/runtime/python/okstra_ctl/application/start_run.py +21 -0
- package/runtime/python/okstra_ctl/codex_dispatch.py +49 -826
- package/runtime/python/okstra_ctl/dispatch_core.py +244 -29
- package/runtime/python/okstra_ctl/dispatch_state.py +27 -0
- package/runtime/python/okstra_ctl/domain/__init__.py +34 -0
- package/runtime/python/okstra_ctl/domain/host.py +100 -0
- package/runtime/python/okstra_ctl/domain/provider.py +70 -0
- package/runtime/python/okstra_ctl/domain/wizard/__init__.py +19 -0
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +140 -0
- package/runtime/python/okstra_ctl/domain/worker_exec.py +102 -0
- package/runtime/python/okstra_ctl/domain/worker_role.py +34 -0
- package/runtime/python/okstra_ctl/domain/worker_stream.py +261 -0
- package/runtime/python/okstra_ctl/entrypoints/__init__.py +1 -0
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +334 -0
- package/runtime/python/okstra_ctl/incremental_scope.py +16 -4
- package/runtime/python/okstra_ctl/models.py +54 -269
- package/runtime/python/okstra_ctl/ports/__init__.py +15 -0
- package/runtime/python/okstra_ctl/ports/host.py +32 -0
- package/runtime/python/okstra_ctl/ports/interaction.py +15 -0
- package/runtime/python/okstra_ctl/ports/lead_session.py +25 -0
- package/runtime/python/okstra_ctl/ports/usage_accounting.py +23 -0
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +32 -0
- package/runtime/python/okstra_ctl/registry/__init__.py +13 -0
- package/runtime/python/okstra_ctl/registry/factory_loader.py +32 -0
- package/runtime/python/okstra_ctl/registry/host_discovery.py +124 -0
- package/runtime/python/okstra_ctl/registry/host_registry.py +365 -0
- package/runtime/python/okstra_ctl/registry/provider_registry.py +149 -0
- package/runtime/python/okstra_ctl/render.py +145 -47
- package/runtime/python/okstra_ctl/report_html/common.py +71 -25
- package/runtime/python/okstra_ctl/report_html/models.py +5 -0
- package/runtime/python/okstra_ctl/report_html/render.py +1 -1
- package/runtime/python/okstra_ctl/report_html/run_usage.py +19 -0
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +14 -0
- package/runtime/python/okstra_ctl/report_views.py +44 -16
- package/runtime/python/okstra_ctl/run.py +80 -58
- package/runtime/python/okstra_ctl/session.py +1 -1
- package/runtime/python/okstra_ctl/stage_citations.py +52 -15
- package/runtime/python/okstra_ctl/team.py +44 -32
- package/runtime/python/okstra_ctl/user_response.py +45 -29
- package/runtime/python/okstra_ctl/wizard.py +175 -73
- package/runtime/python/okstra_ctl/worker_audit_ledger.py +29 -4
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +10 -3
- package/runtime/python/okstra_ctl/worker_request.py +140 -0
- package/runtime/python/okstra_ctl/worker_runner.py +622 -0
- package/runtime/python/okstra_token_usage/collect.py +42 -7
- package/runtime/python/okstra_token_usage/report.py +42 -0
- package/runtime/python/okstra_token_usage/task_totals.py +88 -0
- package/runtime/schemas/final-report-v1.0.schema.json +4040 -1066
- package/runtime/schemas/final-report-v2.0.schema.json +5673 -1412
- package/runtime/skills/okstra-inspect/SKILL.md +1 -2
- package/runtime/skills/okstra-inspect/facets/logs.md +5 -5
- package/runtime/skills/okstra-inspect/facets/run-audit.md +3 -3
- package/runtime/skills/okstra-run/SKILL.md +74 -29
- package/runtime/skills/okstra-user-response/SKILL.md +15 -5
- package/runtime/templates/implementation-worker-preamble.md +1 -1
- package/runtime/templates/report-writer-prompt-preamble.md +1 -0
- package/runtime/templates/reports/final-report.template.md +3 -3
- package/runtime/templates/reports/html/assets/base.css +8 -4
- package/runtime/templates/reports/html/base.template.html +12 -6
- package/runtime/templates/reports/html/i18n/en.json +32 -7
- package/runtime/templates/reports/html/i18n/ko.json +32 -7
- package/runtime/templates/reports/html/macros/forms.html +9 -3
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +14 -19
- package/runtime/templates/reports/report.js +59 -26
- package/runtime/templates/reports/user-response.template.md +12 -8
- package/runtime/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/validate-implementation-plan-stages.py +17 -22
- package/runtime/validators/validate-report-views.py +0 -39
- package/runtime/validators/validate-run.py +96 -14
- package/runtime/validators/validate_session_conformance.py +69 -3
- package/src/cli-registry.mjs +0 -7
- package/src/commands/execute/render-bundle.mjs +4 -4
- package/src/commands/execute/run.mjs +8 -25
- package/src/commands/execute/wizard.mjs +33 -13
- package/src/commands/lifecycle/doctor.mjs +10 -10
- package/src/commands/lifecycle/install.mjs +53 -30
- package/src/commands/lifecycle/preflight.mjs +14 -4
- package/src/lib/host-registry-client.mjs +176 -0
- package/src/lib/runtime-manifest.mjs +6 -8
- package/runtime/bin/okstra-wrapper-agy-stream.py +0 -61
- package/runtime/python/okstra_ctl/error_issue.py +0 -640
- package/runtime/python/okstra_ctl/issue_signals.py +0 -186
- package/runtime/python/okstra_ctl/lead_runtime.py +0 -115
- package/runtime/python/okstra_ctl/runner_resolution.py +0 -103
- package/runtime/skills/okstra-inspect/facets/error-issue.md +0 -77
- package/src/commands/inspect/error-issue.mjs +0 -27
- package/src/lib/runtime-readiness.mjs +0 -90
- package/src/lib/runtime-resolver.mjs +0 -123
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Bundled Grok provider catalog."""
|
|
2
|
+
from okstra_ctl.domain.provider import LeadLaunchSpec, ModelSpec, ProviderSpec
|
|
3
|
+
from okstra_ctl.domain.worker_exec import (
|
|
4
|
+
STREAM_JSON,
|
|
5
|
+
ExecCommand,
|
|
6
|
+
PolicySupport,
|
|
7
|
+
WorkerExecRequest,
|
|
8
|
+
)
|
|
9
|
+
from okstra_ctl.domain.worker_stream import content_block_events
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
GROK = {
|
|
13
|
+
"grok-build-0.1": ModelSpec(
|
|
14
|
+
"grok-build-0.1", "grok-build-0.1", (1.00, 0.20, 2.00), in_picker=True
|
|
15
|
+
),
|
|
16
|
+
"grok-4.5": ModelSpec("grok-4.5", "grok-4.5", (2.00, 0.30, 6.00), in_picker=True),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
GROK_LEAD_LAUNCH = LeadLaunchSpec(
|
|
20
|
+
executable="grok",
|
|
21
|
+
model_flag="--model",
|
|
22
|
+
resume_session_id_flag="--resume",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class GrokExecution:
|
|
27
|
+
"""grok CLI invocation.
|
|
28
|
+
|
|
29
|
+
Runs inside the stage tree when there is one. That is where this provider's
|
|
30
|
+
production path already puts it, and it is why the working directory is part
|
|
31
|
+
of the returned command rather than something the runner picks.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def build_command(self, request: WorkerExecRequest) -> ExecCommand:
|
|
35
|
+
cwd = request.worktree_path or request.project_root
|
|
36
|
+
return ExecCommand(
|
|
37
|
+
argv=(
|
|
38
|
+
"grok",
|
|
39
|
+
"-p",
|
|
40
|
+
request.prompt_text,
|
|
41
|
+
"-m",
|
|
42
|
+
request.model,
|
|
43
|
+
"--output-format",
|
|
44
|
+
"streaming-json",
|
|
45
|
+
"--cwd",
|
|
46
|
+
str(cwd),
|
|
47
|
+
),
|
|
48
|
+
stdin_text=None,
|
|
49
|
+
stream_format=STREAM_JSON,
|
|
50
|
+
normalise=content_block_events,
|
|
51
|
+
cwd=cwd,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def policy_support(self) -> PolicySupport:
|
|
55
|
+
return PolicySupport(
|
|
56
|
+
can_auto_approve=False,
|
|
57
|
+
can_bound_write_scope=False,
|
|
58
|
+
note="this CLI exposes no approval or sandbox flag",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def create_provider() -> ProviderSpec:
|
|
63
|
+
return ProviderSpec(
|
|
64
|
+
provider="grok",
|
|
65
|
+
display_label="Grok",
|
|
66
|
+
models=GROK,
|
|
67
|
+
default_models={
|
|
68
|
+
"lead": "grok-build-0.1",
|
|
69
|
+
"analyser": "grok-build-0.1",
|
|
70
|
+
"critic": "grok-4.5",
|
|
71
|
+
},
|
|
72
|
+
wrapper="okstra-grok-exec.sh",
|
|
73
|
+
supported_roles=frozenset({"lead", "analyser", "critic"}),
|
|
74
|
+
lead_launch=GROK_LEAD_LAUNCH,
|
|
75
|
+
exec_strategy=GrokExecution(),
|
|
76
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"schemaVersion": 1, "id": "grok", "factory": "adapter.py:create_provider"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Bundled Kimi provider catalog."""
|
|
2
|
+
from okstra_ctl.domain.provider import LeadLaunchSpec, ModelSpec, ProviderSpec
|
|
3
|
+
from okstra_ctl.domain.worker_exec import (
|
|
4
|
+
STREAM_JSON,
|
|
5
|
+
ExecCommand,
|
|
6
|
+
PolicySupport,
|
|
7
|
+
WorkerExecRequest,
|
|
8
|
+
)
|
|
9
|
+
from okstra_ctl.domain.worker_stream import content_block_events
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
KIMI = {
|
|
13
|
+
"kimi-k2.7-code": ModelSpec(
|
|
14
|
+
"Kimi K2.7 Code", "kimi-k2.7-code", (0.95, 0.19, 4.00), in_picker=True
|
|
15
|
+
),
|
|
16
|
+
"kimi k2.7 code": ModelSpec("Kimi K2.7 Code", "kimi-k2.7-code"),
|
|
17
|
+
"kimi-for-coding": ModelSpec("Kimi K2.7 Code", "kimi-for-coding", in_picker=True),
|
|
18
|
+
"kimi-k3": ModelSpec("Kimi K3", "kimi-k3", (3.00, 0.30, 15.00), in_picker=True),
|
|
19
|
+
"kimi k3": ModelSpec("Kimi K3", "kimi-k3"),
|
|
20
|
+
"k3": ModelSpec("Kimi K3", "k3", in_picker=True),
|
|
21
|
+
"k3-256k": ModelSpec("Kimi K3 256K", "k3-256k", in_picker=True),
|
|
22
|
+
"kimi k3 256k": ModelSpec("Kimi K3 256K", "k3-256k"),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
KIMI_LEAD_LAUNCH = LeadLaunchSpec(
|
|
26
|
+
executable="kimi",
|
|
27
|
+
model_flag="--model",
|
|
28
|
+
resume_session_id_flag="--session",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class KimiExecution:
|
|
33
|
+
"""kimi CLI invocation.
|
|
34
|
+
|
|
35
|
+
No flag names the working directory, so the process's own is the only
|
|
36
|
+
control. Returning it in the command keeps that fact inside the contract
|
|
37
|
+
instead of leaving the runner to infer it.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def build_command(self, request: WorkerExecRequest) -> ExecCommand:
|
|
41
|
+
cwd = request.worktree_path or request.project_root
|
|
42
|
+
return ExecCommand(
|
|
43
|
+
argv=(
|
|
44
|
+
"kimi",
|
|
45
|
+
"-p",
|
|
46
|
+
request.prompt_text,
|
|
47
|
+
"-m",
|
|
48
|
+
request.model,
|
|
49
|
+
"--output-format",
|
|
50
|
+
"stream-json",
|
|
51
|
+
),
|
|
52
|
+
stdin_text=None,
|
|
53
|
+
stream_format=STREAM_JSON,
|
|
54
|
+
normalise=content_block_events,
|
|
55
|
+
cwd=cwd,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def policy_support(self) -> PolicySupport:
|
|
59
|
+
return PolicySupport(
|
|
60
|
+
can_auto_approve=False,
|
|
61
|
+
can_bound_write_scope=False,
|
|
62
|
+
note="this CLI exposes no approval or sandbox flag",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def create_provider() -> ProviderSpec:
|
|
67
|
+
return ProviderSpec(
|
|
68
|
+
provider="kimi",
|
|
69
|
+
display_label="Kimi",
|
|
70
|
+
models=KIMI,
|
|
71
|
+
default_models={
|
|
72
|
+
"lead": "kimi-k2.7-code",
|
|
73
|
+
"analyser": "kimi-k2.7-code",
|
|
74
|
+
"critic": "kimi-k3",
|
|
75
|
+
},
|
|
76
|
+
wrapper="okstra-kimi-exec.sh",
|
|
77
|
+
supported_roles=frozenset({"lead", "analyser", "critic"}),
|
|
78
|
+
lead_launch=KIMI_LEAD_LAUNCH,
|
|
79
|
+
exec_strategy=KimiExecution(),
|
|
80
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"schemaVersion": 1, "id": "kimi", "factory": "adapter.py:create_provider"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Host-neutral application services."""
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Plan a wizard prompt through the selected host interaction boundary."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from okstra_ctl.domain.host import HostSessionContext
|
|
7
|
+
from okstra_ctl.domain.wizard.interaction import InteractionPlan, WizardPrompt
|
|
8
|
+
from okstra_ctl.ports import InteractionPort
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class PlannedWizardPrompt:
|
|
13
|
+
prompt: WizardPrompt
|
|
14
|
+
interaction: InteractionPlan
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def plan_prompt(
|
|
18
|
+
prompt: WizardPrompt,
|
|
19
|
+
context: HostSessionContext,
|
|
20
|
+
interaction_port: InteractionPort,
|
|
21
|
+
) -> PlannedWizardPrompt:
|
|
22
|
+
return PlannedWizardPrompt(
|
|
23
|
+
prompt=prompt,
|
|
24
|
+
interaction=interaction_port.plan(prompt, context),
|
|
25
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Collect a usage snapshot through the selected host boundary."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from okstra_ctl.ports.usage_accounting import (
|
|
5
|
+
UsageAccountingPort,
|
|
6
|
+
UsageRequest,
|
|
7
|
+
UsageSnapshot,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def collect_usage(
|
|
12
|
+
request: UsageRequest,
|
|
13
|
+
port: UsageAccountingPort,
|
|
14
|
+
) -> UsageSnapshot:
|
|
15
|
+
return port.collect(request)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Route one normalized worker-dispatch request through its host port."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from ..ports.worker_dispatch import (
|
|
5
|
+
WorkerDispatchPlan,
|
|
6
|
+
WorkerDispatchPort,
|
|
7
|
+
WorkerDispatchRequest,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def dispatch_assignments(
|
|
12
|
+
request: WorkerDispatchRequest,
|
|
13
|
+
port: WorkerDispatchPort,
|
|
14
|
+
) -> WorkerDispatchPlan:
|
|
15
|
+
return port.build_plan(request)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Resolve provider assignments from host and provider capabilities."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from okstra_ctl.domain.host import (
|
|
5
|
+
HostCapabilityMismatch,
|
|
6
|
+
HostDescriptor,
|
|
7
|
+
ProviderUnavailable,
|
|
8
|
+
)
|
|
9
|
+
from okstra_ctl.domain.provider import (
|
|
10
|
+
LeadProviderAssignment,
|
|
11
|
+
ProviderSpec,
|
|
12
|
+
RunnerResolution,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
NATIVE_SESSION = "native-session"
|
|
17
|
+
CLI_WRAPPER = "cli-wrapper"
|
|
18
|
+
RUNNER_VALUES = frozenset({NATIVE_SESSION, CLI_WRAPPER})
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def resolve_assignment_runner(
|
|
22
|
+
*,
|
|
23
|
+
host: HostDescriptor,
|
|
24
|
+
provider: ProviderSpec,
|
|
25
|
+
role: str,
|
|
26
|
+
requested_runner: str = "",
|
|
27
|
+
) -> RunnerResolution:
|
|
28
|
+
if role not in provider.supported_roles:
|
|
29
|
+
raise ProviderUnavailable(
|
|
30
|
+
f"provider {provider.provider!r} does not support role {role!r}"
|
|
31
|
+
)
|
|
32
|
+
requested = (requested_runner or "").strip()
|
|
33
|
+
if requested and requested not in RUNNER_VALUES:
|
|
34
|
+
allowed = ", ".join(sorted(RUNNER_VALUES))
|
|
35
|
+
raise HostCapabilityMismatch(
|
|
36
|
+
f"unknown runner {requested!r}. Allowed values: {allowed}"
|
|
37
|
+
)
|
|
38
|
+
is_native = provider.provider == host.native_provider_id
|
|
39
|
+
if requested == NATIVE_SESSION and not is_native:
|
|
40
|
+
raise HostCapabilityMismatch(
|
|
41
|
+
f"host {host.id!r} does not provide native "
|
|
42
|
+
f"{provider.provider!r} execution"
|
|
43
|
+
)
|
|
44
|
+
if requested == NATIVE_SESSION or (not requested and is_native):
|
|
45
|
+
return RunnerResolution(
|
|
46
|
+
NATIVE_SESSION,
|
|
47
|
+
"",
|
|
48
|
+
f"{host.id} provides native {provider.provider} execution",
|
|
49
|
+
)
|
|
50
|
+
if not provider.wrapper:
|
|
51
|
+
raise ProviderUnavailable(
|
|
52
|
+
f"provider {provider.provider!r} has no CLI wrapper for role {role!r}"
|
|
53
|
+
)
|
|
54
|
+
return RunnerResolution(
|
|
55
|
+
CLI_WRAPPER,
|
|
56
|
+
provider.wrapper,
|
|
57
|
+
f"{host.id} uses the registered {provider.provider} CLI wrapper",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def resolve_lead_provider(
|
|
62
|
+
*,
|
|
63
|
+
host_id: str,
|
|
64
|
+
requested_provider: str,
|
|
65
|
+
host_registry,
|
|
66
|
+
provider_registry,
|
|
67
|
+
) -> LeadProviderAssignment:
|
|
68
|
+
host = host_registry.resolve(host_id).descriptor
|
|
69
|
+
requested = (requested_provider or "").strip().lower()
|
|
70
|
+
if not requested and not host.native_provider_id:
|
|
71
|
+
raise HostCapabilityMismatch(
|
|
72
|
+
f"host {host.id!r} has no native lead provider; select an explicit "
|
|
73
|
+
"provider with lead capability"
|
|
74
|
+
)
|
|
75
|
+
if host.native_provider_id and requested not in {"", host.native_provider_id}:
|
|
76
|
+
raise HostCapabilityMismatch(
|
|
77
|
+
f"{host.agent_label.removesuffix(' CLI')} host requires provider "
|
|
78
|
+
f"{host.native_provider_id!r} for the in-session lead; provider "
|
|
79
|
+
f"{requested!r} must be selected as a worker instead"
|
|
80
|
+
)
|
|
81
|
+
provider_id = requested or host.native_provider_id
|
|
82
|
+
provider = provider_registry.resolve(provider_id)
|
|
83
|
+
resolution = resolve_assignment_runner(
|
|
84
|
+
host=host,
|
|
85
|
+
provider=provider,
|
|
86
|
+
role="lead",
|
|
87
|
+
)
|
|
88
|
+
return LeadProviderAssignment(
|
|
89
|
+
provider=provider.provider,
|
|
90
|
+
runner=resolution.runner,
|
|
91
|
+
wrapper=resolution.wrapper,
|
|
92
|
+
reason=resolution.reason,
|
|
93
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Prepare a lead resume plan through a ready host adapter."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from okstra_ctl.domain.host import (
|
|
5
|
+
HostSessionContext,
|
|
6
|
+
HostUnavailable,
|
|
7
|
+
LeadResumeRequest,
|
|
8
|
+
LeadSessionPlan,
|
|
9
|
+
)
|
|
10
|
+
from okstra_ctl.ports import HostAdapter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def resume_run(
|
|
14
|
+
request: LeadResumeRequest,
|
|
15
|
+
context: HostSessionContext,
|
|
16
|
+
adapter: HostAdapter,
|
|
17
|
+
) -> LeadSessionPlan:
|
|
18
|
+
readiness = adapter.probe(context)
|
|
19
|
+
if not readiness.ok:
|
|
20
|
+
raise HostUnavailable(readiness.reason)
|
|
21
|
+
return adapter.lead_session().build_resume(request, context)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Prepare a lead start plan through a ready host adapter."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from okstra_ctl.domain.host import (
|
|
5
|
+
HostSessionContext,
|
|
6
|
+
HostUnavailable,
|
|
7
|
+
LeadSessionPlan,
|
|
8
|
+
LeadStartRequest,
|
|
9
|
+
)
|
|
10
|
+
from okstra_ctl.ports import HostAdapter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def start_run(
|
|
14
|
+
request: LeadStartRequest,
|
|
15
|
+
context: HostSessionContext,
|
|
16
|
+
adapter: HostAdapter,
|
|
17
|
+
) -> LeadSessionPlan:
|
|
18
|
+
readiness = adapter.probe(context)
|
|
19
|
+
if not readiness.ok:
|
|
20
|
+
raise HostUnavailable(readiness.reason)
|
|
21
|
+
return adapter.lead_session().build_start(request, context)
|