okstra 0.163.2 → 0.164.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 +7 -5
- package/docs/architecture.md +12 -7
- package/docs/cli.md +8 -5
- package/docs/project-structure-overview.md +7 -5
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +1 -0
- 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.sh +87 -91
- package/runtime/prompts/lead/adapters/cmux.md +1 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +1 -0
- package/runtime/prompts/profiles/implementation-planning.md +1 -1
- 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} +111 -0
- 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} +52 -0
- 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 +35 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +55 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +43 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +32 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +38 -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 +240 -29
- package/runtime/python/okstra_ctl/dispatch_state.py +17 -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 +66 -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/entrypoints/__init__.py +1 -0
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +334 -0
- 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/run.py +80 -58
- package/runtime/python/okstra_ctl/session.py +1 -1
- package/runtime/python/okstra_ctl/team.py +44 -32
- package/runtime/python/okstra_ctl/wizard.py +162 -64
- package/runtime/python/okstra_ctl/worker_audit_ledger.py +29 -4
- package/runtime/python/okstra_token_usage/collect.py +34 -6
- package/runtime/schemas/final-report-v1.0.schema.json +3989 -1085
- package/runtime/schemas/final-report-v2.0.schema.json +5622 -1451
- package/runtime/skills/okstra-run/SKILL.md +73 -28
- package/runtime/templates/implementation-worker-preamble.md +1 -1
- package/runtime/templates/reports/final-report.template.md +3 -3
- package/runtime/templates/reports/html/i18n/en.json +3 -1
- package/runtime/templates/reports/html/i18n/ko.json +3 -1
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +1 -1
- 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 +8 -7
- package/runtime/validators/validate_session_conformance.py +7 -2
- 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/python/okstra_ctl/lead_runtime.py +0 -115
- package/runtime/python/okstra_ctl/runner_resolution.py +0 -103
- package/src/lib/runtime-readiness.mjs +0 -90
- package/src/lib/runtime-resolver.mjs +0 -123
|
@@ -1,220 +1,33 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Compatibility exports for the provider model catalog.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Provider value objects live in ``domain`` and catalog entries are discovered
|
|
4
|
+
from provider adapters. This module preserves the established public helpers.
|
|
5
5
|
"""
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
-
from
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"claude-opus-4-8": ModelSpec("opus-4-8", "claude-opus-4-8"),
|
|
29
|
-
"opus-4-7": ModelSpec("opus-4-7", "claude-opus-4-7"),
|
|
30
|
-
"claude-opus-4-7": ModelSpec("opus-4-7", "claude-opus-4-7"),
|
|
31
|
-
"opus-4-6": ModelSpec("opus-4-6", "claude-opus-4-6"),
|
|
32
|
-
"claude-opus-4-6": ModelSpec("opus-4-6", "claude-opus-4-6"),
|
|
33
|
-
"sonnet": ModelSpec("sonnet", "sonnet", in_picker=True),
|
|
34
|
-
"sonnet-5": ModelSpec("sonnet-5", "claude-sonnet-5", in_picker=True),
|
|
35
|
-
"claude-sonnet-5": ModelSpec("sonnet-5", "claude-sonnet-5"),
|
|
36
|
-
"sonnet-4-6": ModelSpec("sonnet-4-6", "claude-sonnet-4-6"),
|
|
37
|
-
"claude-sonnet-4-6": ModelSpec("sonnet-4-6", "claude-sonnet-4-6"),
|
|
38
|
-
"haiku": ModelSpec("haiku", "haiku", in_picker=True),
|
|
39
|
-
"haiku-4-5": ModelSpec("haiku-4-5", "claude-haiku-4-5"),
|
|
40
|
-
"claude-haiku-4-5": ModelSpec("haiku-4-5", "claude-haiku-4-5"),
|
|
41
|
-
"claude-haiku-4-5-20251001": ModelSpec("haiku-4-5", "claude-haiku-4-5-20251001"),
|
|
42
|
-
}
|
|
43
|
-
# `agy` rejects a bare execution slug — it demands a tier, spelled as a suffix
|
|
44
|
-
# on the slug itself ("gemini-3.1-pro-high"), and its list is host/version-
|
|
45
|
-
# specific. model_discovery.normalize_execution_for_dispatch appends the role's
|
|
46
|
-
# effort to ModelSpec.execution at dispatch time. `display` is okstra-facing
|
|
47
|
-
# only and never reaches agy. agy also serves Claude/GPT-OSS models on some
|
|
48
|
-
# installs; those carry their tier inline and take no suffix.
|
|
49
|
-
ANTIGRAVITY = {
|
|
50
|
-
"gemini-3.1-pro": ModelSpec("Gemini 3.1 Pro", "gemini-3.1-pro", in_picker=True),
|
|
51
|
-
"gemini 3.1 pro": ModelSpec("Gemini 3.1 Pro", "gemini-3.1-pro"),
|
|
52
|
-
"gemini-3.6-flash": ModelSpec("Gemini 3.6 Flash", "gemini-3.6-flash", in_picker=True),
|
|
53
|
-
"gemini 3.6 flash": ModelSpec("Gemini 3.6 Flash", "gemini-3.6-flash"),
|
|
54
|
-
"gemini-3.5-flash": ModelSpec("Gemini 3.5 Flash", "gemini-3.5-flash", in_picker=True),
|
|
55
|
-
"gemini 3.5 flash": ModelSpec("Gemini 3.5 Flash", "gemini-3.5-flash"),
|
|
56
|
-
}
|
|
57
|
-
CODEX = {
|
|
58
|
-
# ChatGPT-account host (agy/codex) serves 5.6 without per-token billing → pricing=None.
|
|
59
|
-
"gpt-5.6-sol": ModelSpec("gpt-5.6-sol", "gpt-5.6-sol", None, in_picker=True),
|
|
60
|
-
"gpt-5.6": ModelSpec("gpt-5.6", "gpt-5.6", (5.00, 0.50, 30.0), in_picker=True),
|
|
61
|
-
"gpt-5.5": ModelSpec("gpt-5.5", "gpt-5.5", (5.00, 0.50, 30.0), in_picker=True),
|
|
62
|
-
"gpt-5.4": ModelSpec("gpt-5.4", "gpt-5.4", (2.50, 0.25, 15.0), in_picker=True),
|
|
63
|
-
"gpt-5.4-mini": ModelSpec("gpt-5.4-mini", "gpt-5.4-mini", (0.75, 0.075, 4.50), in_picker=True),
|
|
64
|
-
"gpt-5.3-codex": ModelSpec("gpt-5.4", "gpt-5.4"),
|
|
65
|
-
"gpt-5.2": ModelSpec("gpt-5.2", "gpt-5.2", (1.75, 0.175, 14.0)),
|
|
66
|
-
"codex-auto-review": ModelSpec("codex-auto-review", "codex-auto-review"),
|
|
67
|
-
}
|
|
68
|
-
GROK = {
|
|
69
|
-
"grok-build-0.1": ModelSpec(
|
|
70
|
-
"grok-build-0.1", "grok-build-0.1", (1.00, 0.20, 2.00), in_picker=True,
|
|
71
|
-
),
|
|
72
|
-
"grok-4.5": ModelSpec(
|
|
73
|
-
"grok-4.5", "grok-4.5", (2.00, 0.30, 6.00), in_picker=True,
|
|
74
|
-
),
|
|
75
|
-
}
|
|
76
|
-
KIMI = {
|
|
77
|
-
"kimi-k2.7-code": ModelSpec(
|
|
78
|
-
"Kimi K2.7 Code", "kimi-k2.7-code", (0.95, 0.19, 4.00), in_picker=True,
|
|
79
|
-
),
|
|
80
|
-
"kimi k2.7 code": ModelSpec("Kimi K2.7 Code", "kimi-k2.7-code"),
|
|
81
|
-
"kimi-for-coding": ModelSpec(
|
|
82
|
-
"Kimi K2.7 Code", "kimi-for-coding", in_picker=True,
|
|
83
|
-
),
|
|
84
|
-
"kimi-k3": ModelSpec(
|
|
85
|
-
"Kimi K3", "kimi-k3", (3.00, 0.30, 15.00), in_picker=True,
|
|
86
|
-
),
|
|
87
|
-
"kimi k3": ModelSpec("Kimi K3", "kimi-k3"),
|
|
88
|
-
"k3": ModelSpec("Kimi K3", "k3", in_picker=True),
|
|
89
|
-
"k3-256k": ModelSpec("Kimi K3 256K", "k3-256k", in_picker=True),
|
|
90
|
-
"kimi k3 256k": ModelSpec("Kimi K3 256K", "k3-256k"),
|
|
91
|
-
}
|
|
92
|
-
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from .domain.provider import (
|
|
11
|
+
LeadLaunchSpec,
|
|
12
|
+
ModelAssignment,
|
|
13
|
+
ModelSpec,
|
|
14
|
+
ProviderSpec,
|
|
15
|
+
UnknownModelError,
|
|
16
|
+
UnknownProviderError,
|
|
17
|
+
)
|
|
18
|
+
from .registry.provider_registry import default_provider_registry
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
PROVIDERS = default_provider_registry().providers
|
|
22
|
+
CLAUDE = PROVIDERS["claude"].models
|
|
23
|
+
ANTIGRAVITY = PROVIDERS["antigravity"].models
|
|
24
|
+
CODEX = PROVIDERS["codex"].models
|
|
25
|
+
GROK = PROVIDERS["grok"].models
|
|
26
|
+
KIMI = PROVIDERS["kimi"].models
|
|
27
|
+
PROVIDER_MAPPINGS = {provider: spec.models for provider, spec in PROVIDERS.items()}
|
|
93
28
|
ANALYSIS_ROLES = frozenset({"analyser", "critic"})
|
|
94
29
|
IMPLEMENTATION_ROLES = frozenset({"executor", "verifier"})
|
|
95
30
|
|
|
96
|
-
|
|
97
|
-
@dataclass(frozen=True)
|
|
98
|
-
class LeadLaunchSpec:
|
|
99
|
-
"""How to start this provider's CLI as an interactive lead session.
|
|
100
|
-
|
|
101
|
-
The worker wrappers cannot serve here: they run one non-interactive turn and
|
|
102
|
-
exit, while a lead owns the session the user talks to. Empty `prompt_flag`
|
|
103
|
-
means the CLI takes the prompt positionally; empty `session_id_flag` means it
|
|
104
|
-
has no resumable session id and resumes from artifacts instead.
|
|
105
|
-
"""
|
|
106
|
-
|
|
107
|
-
executable: str
|
|
108
|
-
model_flag: str
|
|
109
|
-
prompt_flag: str = ""
|
|
110
|
-
session_id_flag: str = ""
|
|
111
|
-
# Flags that lift the CLI's own sandbox, and what the user is agreeing to by
|
|
112
|
-
# accepting them. Empty when the CLI does not sandbox itself by default.
|
|
113
|
-
sandbox_waiver: tuple[str, ...] = ()
|
|
114
|
-
sandbox_waiver_note: str = ""
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
@dataclass(frozen=True)
|
|
118
|
-
class ProviderSpec:
|
|
119
|
-
provider: str
|
|
120
|
-
display_label: str
|
|
121
|
-
models: Mapping[str, ModelSpec]
|
|
122
|
-
default_models: Mapping[str, str]
|
|
123
|
-
wrapper: str
|
|
124
|
-
supported_roles: frozenset[str]
|
|
125
|
-
lead_launch: Optional[LeadLaunchSpec] = None
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
PROVIDERS = {
|
|
129
|
-
"claude": ProviderSpec(
|
|
130
|
-
provider="claude",
|
|
131
|
-
display_label="Claude",
|
|
132
|
-
models=CLAUDE,
|
|
133
|
-
default_models={
|
|
134
|
-
"lead": "opus",
|
|
135
|
-
"analyser": "opus",
|
|
136
|
-
"critic": "opus",
|
|
137
|
-
"executor": "opus",
|
|
138
|
-
"verifier": "opus",
|
|
139
|
-
"report-writer": "sonnet",
|
|
140
|
-
},
|
|
141
|
-
wrapper="okstra-claude-exec.sh",
|
|
142
|
-
supported_roles=frozenset(
|
|
143
|
-
{"lead", "analyser", "critic", "executor", "verifier", "report-writer"}
|
|
144
|
-
),
|
|
145
|
-
lead_launch=LeadLaunchSpec(
|
|
146
|
-
executable="claude",
|
|
147
|
-
model_flag="--model",
|
|
148
|
-
session_id_flag="--session-id",
|
|
149
|
-
),
|
|
150
|
-
),
|
|
151
|
-
"antigravity": ProviderSpec(
|
|
152
|
-
provider="antigravity",
|
|
153
|
-
display_label="Antigravity",
|
|
154
|
-
models=ANTIGRAVITY,
|
|
155
|
-
default_models={role: "gemini-3.1-pro" for role in (
|
|
156
|
-
"lead", "analyser", "critic", "executor", "verifier",
|
|
157
|
-
)},
|
|
158
|
-
wrapper="okstra-antigravity-exec.sh",
|
|
159
|
-
supported_roles=frozenset(
|
|
160
|
-
{"lead", "analyser", "critic", "executor", "verifier"}
|
|
161
|
-
),
|
|
162
|
-
lead_launch=LeadLaunchSpec(
|
|
163
|
-
executable="agy",
|
|
164
|
-
model_flag="--model",
|
|
165
|
-
# Without this the prompt runs once and the session ends; okstra
|
|
166
|
-
# needs the lead to stay and drive the remaining phases.
|
|
167
|
-
prompt_flag="--prompt-interactive",
|
|
168
|
-
),
|
|
169
|
-
),
|
|
170
|
-
"codex": ProviderSpec(
|
|
171
|
-
provider="codex",
|
|
172
|
-
display_label="Codex",
|
|
173
|
-
models=CODEX,
|
|
174
|
-
default_models={role: "gpt-5.6-sol" for role in (
|
|
175
|
-
"lead", "analyser", "critic", "executor", "verifier", "report-writer",
|
|
176
|
-
)},
|
|
177
|
-
wrapper="okstra-codex-exec.sh",
|
|
178
|
-
supported_roles=frozenset(
|
|
179
|
-
{"lead", "analyser", "critic", "executor", "verifier", "report-writer"}
|
|
180
|
-
),
|
|
181
|
-
lead_launch=LeadLaunchSpec(
|
|
182
|
-
executable="codex",
|
|
183
|
-
model_flag="-m",
|
|
184
|
-
# A codex session sandboxes itself, and everything it spawns inherits
|
|
185
|
-
# that — okstra's dispatch and every worker CLI with it. Measured:
|
|
186
|
-
# the cmux socket returns EPERM and each worker dies unable to write
|
|
187
|
-
# its own config. A sandboxed lead cannot run okstra at all.
|
|
188
|
-
sandbox_waiver=("-s", "danger-full-access"),
|
|
189
|
-
sandbox_waiver_note=(
|
|
190
|
-
"codex will start without its filesystem and network sandbox, "
|
|
191
|
-
"which okstra needs so the lead can reach cmux and start worker "
|
|
192
|
-
"CLIs."
|
|
193
|
-
),
|
|
194
|
-
),
|
|
195
|
-
),
|
|
196
|
-
"grok": ProviderSpec(
|
|
197
|
-
provider="grok",
|
|
198
|
-
display_label="Grok",
|
|
199
|
-
models=GROK,
|
|
200
|
-
default_models={"analyser": "grok-build-0.1", "critic": "grok-4.5"},
|
|
201
|
-
wrapper="okstra-grok-exec.sh",
|
|
202
|
-
supported_roles=ANALYSIS_ROLES,
|
|
203
|
-
),
|
|
204
|
-
"kimi": ProviderSpec(
|
|
205
|
-
provider="kimi",
|
|
206
|
-
display_label="Kimi",
|
|
207
|
-
models=KIMI,
|
|
208
|
-
default_models={"analyser": "kimi-k2.7-code", "critic": "kimi-k3"},
|
|
209
|
-
wrapper="okstra-kimi-exec.sh",
|
|
210
|
-
supported_roles=ANALYSIS_ROLES,
|
|
211
|
-
),
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
PROVIDER_MAPPINGS = {
|
|
215
|
-
provider: spec.models for provider, spec in PROVIDERS.items()
|
|
216
|
-
}
|
|
217
|
-
|
|
218
31
|
# Legacy role-keyed compatibility defaults. Canonical assignments resolve a
|
|
219
32
|
# provider first and then use ProviderSpec.default_models for that role.
|
|
220
33
|
ROLE_DEFAULTS = {
|
|
@@ -223,20 +36,6 @@ ROLE_DEFAULTS = {
|
|
|
223
36
|
}
|
|
224
37
|
|
|
225
38
|
|
|
226
|
-
@dataclass
|
|
227
|
-
class ModelAssignment:
|
|
228
|
-
display: str
|
|
229
|
-
execution: str
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
class UnknownModelError(ValueError):
|
|
233
|
-
"""raw_value 가 provider mapping 에 등록되어 있지 않을 때 발생."""
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
class UnknownProviderError(ValueError):
|
|
237
|
-
"""Raised when a requested provider is absent from the registry."""
|
|
238
|
-
|
|
239
|
-
|
|
240
39
|
def lead_launch_spec(provider: str) -> LeadLaunchSpec:
|
|
241
40
|
"""The launch spec for a provider that can lead, or an error naming why not."""
|
|
242
41
|
spec = provider_spec(provider)
|
|
@@ -248,13 +47,7 @@ def lead_launch_spec(provider: str) -> LeadLaunchSpec:
|
|
|
248
47
|
|
|
249
48
|
|
|
250
49
|
def bare_lead_launch_argv(provider: str) -> list[str]:
|
|
251
|
-
"""Start this provider
|
|
252
|
-
|
|
253
|
-
`okstra run <lead>` opens the host; the okstra-run skill inside it collects
|
|
254
|
-
task inputs, exactly as the Claude Code path already works. So there is no
|
|
255
|
-
prompt and no model to pin here — only the flags that make the session
|
|
256
|
-
usable to okstra at all.
|
|
257
|
-
"""
|
|
50
|
+
"""Start this provider CLI with only the flags required by Okstra."""
|
|
258
51
|
launch = lead_launch_spec(provider)
|
|
259
52
|
return [launch.executable, *launch.sandbox_waiver]
|
|
260
53
|
|
|
@@ -267,22 +60,15 @@ def lead_launch_argv(
|
|
|
267
60
|
session_id: str = "",
|
|
268
61
|
waive_sandbox: bool = True,
|
|
269
62
|
) -> list[str]:
|
|
270
|
-
"""
|
|
271
|
-
|
|
272
|
-
The sandbox waiver is applied by default because a sandboxed lead cannot run
|
|
273
|
-
okstra at all — it reaches neither cmux nor the worker CLIs' own config. The
|
|
274
|
-
caller still owns telling the user and collecting their answer; passing
|
|
275
|
-
False produces the un-waived argv so a declined confirmation can show what
|
|
276
|
-
would otherwise have run.
|
|
277
|
-
"""
|
|
63
|
+
"""Return the argv that starts one provider CLI as the lead for a run."""
|
|
278
64
|
launch = lead_launch_spec(provider)
|
|
279
65
|
argv = [launch.executable]
|
|
280
66
|
if waive_sandbox:
|
|
281
67
|
argv.extend(launch.sandbox_waiver)
|
|
282
68
|
if model:
|
|
283
69
|
argv.extend([launch.model_flag, model])
|
|
284
|
-
if session_id and launch.
|
|
285
|
-
argv.extend([launch.
|
|
70
|
+
if session_id and launch.start_session_id_flag:
|
|
71
|
+
argv.extend([launch.start_session_id_flag, session_id])
|
|
286
72
|
if launch.prompt_flag:
|
|
287
73
|
argv.append(launch.prompt_flag)
|
|
288
74
|
argv.append(prompt)
|
|
@@ -290,12 +76,12 @@ def lead_launch_argv(
|
|
|
290
76
|
|
|
291
77
|
|
|
292
78
|
def provider_spec(provider: str) -> ProviderSpec:
|
|
293
|
-
"""Return one registered provider or fail before assignment
|
|
79
|
+
"""Return one registered provider or fail before assignment or dispatch."""
|
|
294
80
|
normalized = (provider or "").strip().lower()
|
|
295
81
|
try:
|
|
296
|
-
return
|
|
297
|
-
except
|
|
298
|
-
allowed = ", ".join(
|
|
82
|
+
return default_provider_registry().resolve(normalized)
|
|
83
|
+
except UnknownProviderError as exc:
|
|
84
|
+
allowed = ", ".join(default_provider_registry().ids())
|
|
299
85
|
raise UnknownProviderError(
|
|
300
86
|
f"unknown provider {provider!r}. Allowed values: {allowed}"
|
|
301
87
|
) from exc
|
|
@@ -303,12 +89,8 @@ def provider_spec(provider: str) -> ProviderSpec:
|
|
|
303
89
|
|
|
304
90
|
def provider_ids(role: str | None = None) -> list[str]:
|
|
305
91
|
"""Registered provider ids, optionally filtered by functional role."""
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
return [
|
|
309
|
-
provider for provider, spec in PROVIDERS.items()
|
|
310
|
-
if role in spec.supported_roles
|
|
311
|
-
]
|
|
92
|
+
registry = default_provider_registry()
|
|
93
|
+
return list(registry.ids(role if role else None))
|
|
312
94
|
|
|
313
95
|
|
|
314
96
|
def provider_supports_role(provider: str, role: str) -> bool:
|
|
@@ -318,29 +100,30 @@ def provider_supports_role(provider: str, role: str) -> bool:
|
|
|
318
100
|
|
|
319
101
|
def provider_wrappers(role: str | None = None) -> dict[str, str]:
|
|
320
102
|
"""Provider-to-wrapper mapping derived from capability entries."""
|
|
103
|
+
registry = default_provider_registry()
|
|
321
104
|
return {
|
|
322
|
-
provider:
|
|
323
|
-
for provider in
|
|
324
|
-
if
|
|
105
|
+
provider: registry.resolve(provider).wrapper
|
|
106
|
+
for provider in registry.ids(role if role else None)
|
|
107
|
+
if registry.resolve(provider).wrapper
|
|
325
108
|
}
|
|
326
109
|
|
|
327
110
|
|
|
328
111
|
def provider_default_model(provider: str, role: str) -> Optional[str]:
|
|
329
|
-
"""Resolve a provider
|
|
112
|
+
"""Resolve a provider explicit default for one functional role."""
|
|
330
113
|
return provider_spec(provider).default_models.get(role)
|
|
331
114
|
|
|
332
115
|
|
|
333
116
|
def resolve_model_metadata(
|
|
334
117
|
*, provider: str, raw_value: str, default_display: str, default_execution: str,
|
|
335
118
|
) -> ModelAssignment:
|
|
336
|
-
"""
|
|
337
|
-
ModelSpec instead of tuples."""
|
|
119
|
+
"""Resolve a model alias to its display and execution values."""
|
|
338
120
|
raw_value = (raw_value or "").strip()
|
|
339
|
-
mapping =
|
|
340
|
-
|
|
121
|
+
mapping = default_provider_registry().providers.get(provider)
|
|
122
|
+
models = mapping.models if mapping else None
|
|
123
|
+
if raw_value and models is not None:
|
|
341
124
|
normalized = raw_value.lower()
|
|
342
|
-
if normalized not in
|
|
343
|
-
allowed = ", ".join(sorted(
|
|
125
|
+
if normalized not in models:
|
|
126
|
+
allowed = ", ".join(sorted(models))
|
|
344
127
|
raise UnknownModelError(
|
|
345
128
|
f"{provider} model {raw_value!r} is not a supported alias. "
|
|
346
129
|
f"Allowed values: {allowed}"
|
|
@@ -349,24 +132,26 @@ def resolve_model_metadata(
|
|
|
349
132
|
display = value
|
|
350
133
|
execution = raw_value or default_execution
|
|
351
134
|
normalized = value.strip().lower()
|
|
352
|
-
if
|
|
353
|
-
spec =
|
|
135
|
+
if models and normalized in models:
|
|
136
|
+
spec = models[normalized]
|
|
354
137
|
display, execution = spec.display, spec.execution
|
|
355
138
|
return ModelAssignment(display=display, execution=execution)
|
|
356
139
|
|
|
357
140
|
|
|
358
141
|
def picker_options(provider: str) -> list[str]:
|
|
359
142
|
"""Picker-exposed aliases for a provider, in catalog insertion order."""
|
|
360
|
-
|
|
361
|
-
|
|
143
|
+
spec = default_provider_registry().providers.get(provider)
|
|
144
|
+
models = spec.models if spec else {}
|
|
145
|
+
return [alias for alias, model in models.items() if model.in_picker]
|
|
362
146
|
|
|
363
147
|
|
|
364
148
|
def default_model(role: str) -> Optional[str]:
|
|
365
|
-
"""Role
|
|
149
|
+
"""Role-to-default model alias; environment overrides belong to the caller."""
|
|
366
150
|
return ROLE_DEFAULTS.get(role)
|
|
367
151
|
|
|
368
152
|
|
|
369
153
|
def catalog_pricing(provider: str) -> dict:
|
|
370
|
-
"""Selectable-model pricing as
|
|
371
|
-
|
|
372
|
-
|
|
154
|
+
"""Selectable-model pricing as input, cached-input, output USD per million."""
|
|
155
|
+
spec = default_provider_registry().providers.get(provider)
|
|
156
|
+
models = spec.models if spec else {}
|
|
157
|
+
return {alias: model.pricing for alias, model in models.items() if model.pricing is not None}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Host-facing ports consumed by application use cases."""
|
|
2
|
+
|
|
3
|
+
from .host import HostAdapter
|
|
4
|
+
from .interaction import InteractionPort
|
|
5
|
+
from .lead_session import LeadSessionPort
|
|
6
|
+
from .usage_accounting import UsageAccountingPort
|
|
7
|
+
from .worker_dispatch import WorkerDispatchPort
|
|
8
|
+
|
|
9
|
+
__all__ = (
|
|
10
|
+
"HostAdapter",
|
|
11
|
+
"InteractionPort",
|
|
12
|
+
"LeadSessionPort",
|
|
13
|
+
"UsageAccountingPort",
|
|
14
|
+
"WorkerDispatchPort",
|
|
15
|
+
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Composite interface implemented by each host strategy."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
from ..domain.host import (
|
|
7
|
+
HostClaim,
|
|
8
|
+
HostDescriptor,
|
|
9
|
+
HostReadiness,
|
|
10
|
+
HostResolutionContext,
|
|
11
|
+
HostSessionContext,
|
|
12
|
+
)
|
|
13
|
+
from .interaction import InteractionPort
|
|
14
|
+
from .lead_session import LeadSessionPort
|
|
15
|
+
from .usage_accounting import UsageAccountingPort
|
|
16
|
+
from .worker_dispatch import WorkerDispatchPort
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HostAdapter(Protocol):
|
|
20
|
+
descriptor: HostDescriptor
|
|
21
|
+
|
|
22
|
+
def detect(self, context: HostResolutionContext) -> HostClaim | None: ...
|
|
23
|
+
|
|
24
|
+
def probe(self, context: HostSessionContext) -> HostReadiness: ...
|
|
25
|
+
|
|
26
|
+
def interaction(self) -> InteractionPort: ...
|
|
27
|
+
|
|
28
|
+
def lead_session(self) -> LeadSessionPort: ...
|
|
29
|
+
|
|
30
|
+
def worker_dispatch(self) -> WorkerDispatchPort: ...
|
|
31
|
+
|
|
32
|
+
def usage_accounting(self) -> UsageAccountingPort: ...
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Interaction boundary exposed by a host adapter."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
from ..domain.host import HostSessionContext
|
|
7
|
+
from ..domain.wizard.interaction import InteractionPlan, WizardPrompt
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InteractionPort(Protocol):
|
|
11
|
+
def plan(
|
|
12
|
+
self,
|
|
13
|
+
prompt: WizardPrompt,
|
|
14
|
+
context: HostSessionContext,
|
|
15
|
+
) -> InteractionPlan: ...
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Lead-session boundary exposed by a host adapter."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
from ..domain.host import (
|
|
7
|
+
HostSessionContext,
|
|
8
|
+
LeadResumeRequest,
|
|
9
|
+
LeadSessionPlan,
|
|
10
|
+
LeadStartRequest,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LeadSessionPort(Protocol):
|
|
15
|
+
def build_start(
|
|
16
|
+
self,
|
|
17
|
+
request: LeadStartRequest,
|
|
18
|
+
context: HostSessionContext,
|
|
19
|
+
) -> LeadSessionPlan: ...
|
|
20
|
+
|
|
21
|
+
def build_resume(
|
|
22
|
+
self,
|
|
23
|
+
request: LeadResumeRequest,
|
|
24
|
+
context: HostSessionContext,
|
|
25
|
+
) -> LeadSessionPlan: ...
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Usage-accounting boundary exposed by a host adapter."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class UsageRequest:
|
|
11
|
+
team_state_path: Path
|
|
12
|
+
project_root: Path
|
|
13
|
+
incremental: bool = True
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class UsageSnapshot:
|
|
18
|
+
source: str
|
|
19
|
+
payload: dict
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UsageAccountingPort(Protocol):
|
|
23
|
+
def collect(self, request: UsageRequest) -> UsageSnapshot: ...
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Worker-dispatch boundary exposed by a host adapter."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Literal, Protocol
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class WorkerDispatchRequest:
|
|
11
|
+
project_root: Path
|
|
12
|
+
run_manifest: Path
|
|
13
|
+
workspace_root: Path | None = None
|
|
14
|
+
okstra_bin: Path | None = None
|
|
15
|
+
requested_workers: tuple[str, ...] = ()
|
|
16
|
+
idle_timeout_seconds: int = 600
|
|
17
|
+
dispatch_kind: str = "initial"
|
|
18
|
+
jobs_file: Path | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class WorkerDispatchPlan:
|
|
23
|
+
mode: Literal["native-team", "cli-wrapper", "cmux-pane"]
|
|
24
|
+
request: WorkerDispatchRequest
|
|
25
|
+
backend_plan: object | None = None
|
|
26
|
+
adapter_name: str | None = None
|
|
27
|
+
dispatch_mode: str | None = None
|
|
28
|
+
relay_contract: str | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class WorkerDispatchPort(Protocol):
|
|
32
|
+
def build_plan(self, request: WorkerDispatchRequest) -> WorkerDispatchPlan: ...
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Registries that discover and resolve installed Okstra adapters."""
|
|
2
|
+
|
|
3
|
+
from .provider_registry import (
|
|
4
|
+
ProviderAdapterContractError,
|
|
5
|
+
ProviderRegistry,
|
|
6
|
+
default_provider_registry,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = (
|
|
10
|
+
"ProviderAdapterContractError",
|
|
11
|
+
"ProviderRegistry",
|
|
12
|
+
"default_provider_registry",
|
|
13
|
+
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Safe loader for a provider adapter factory declared in a manifest."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import importlib.util
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FactoryLoadError(ValueError):
|
|
11
|
+
"""Raised when an adapter factory reference cannot be safely loaded."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_relative_factory(manifest_path: Path, factory_ref: str) -> Callable[..., object]:
|
|
15
|
+
"""Load a callable factory without allowing its file to leave the adapter."""
|
|
16
|
+
file_name, separator, symbol = factory_ref.partition(":")
|
|
17
|
+
if not separator or not file_name or not symbol:
|
|
18
|
+
raise FactoryLoadError(f"invalid factory reference: {factory_ref!r}")
|
|
19
|
+
adapter_root = manifest_path.parent.resolve()
|
|
20
|
+
module_path = (adapter_root / file_name).resolve()
|
|
21
|
+
if adapter_root not in module_path.parents or not module_path.is_file():
|
|
22
|
+
raise FactoryLoadError(f"factory escapes adapter root: {factory_ref!r}")
|
|
23
|
+
module_name = f"okstra_adapter_{hashlib.sha256(str(module_path).encode()).hexdigest()}"
|
|
24
|
+
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
|
25
|
+
if spec is None or spec.loader is None:
|
|
26
|
+
raise FactoryLoadError(f"cannot load factory module: {module_path}")
|
|
27
|
+
module = importlib.util.module_from_spec(spec)
|
|
28
|
+
spec.loader.exec_module(module)
|
|
29
|
+
factory = getattr(module, symbol, None)
|
|
30
|
+
if not callable(factory):
|
|
31
|
+
raise FactoryLoadError(f"factory symbol is not callable: {factory_ref!r}")
|
|
32
|
+
return factory
|