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,92 @@
|
|
|
1
|
+
"""Host worker-dispatch strategies and host-port decoration."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
|
|
6
|
+
from ... import dispatch_core
|
|
7
|
+
from ...domain.host import HostDescriptor
|
|
8
|
+
from ...registry.provider_registry import ProviderRegistry
|
|
9
|
+
from ...ports.worker_dispatch import WorkerDispatchPort
|
|
10
|
+
from .cli_wrapper import CliWrapperDispatchPort
|
|
11
|
+
from .native_team import NativeTeamDispatchPort
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def provider_worker_wrappers(
|
|
15
|
+
registry: ProviderRegistry,
|
|
16
|
+
report_writer_provider: str = "claude",
|
|
17
|
+
) -> Mapping[str, str]:
|
|
18
|
+
wrappers = {
|
|
19
|
+
provider_id: registry.resolve(provider_id).wrapper
|
|
20
|
+
for provider_id in registry.ids("analyser")
|
|
21
|
+
if registry.resolve(provider_id).wrapper
|
|
22
|
+
}
|
|
23
|
+
report_writer = registry.resolve(report_writer_provider)
|
|
24
|
+
if "report-writer" not in report_writer.supported_roles:
|
|
25
|
+
raise dispatch_core.DispatchError(
|
|
26
|
+
f"provider {report_writer_provider!r} does not support report-writer"
|
|
27
|
+
)
|
|
28
|
+
return {**wrappers, "report-writer": report_writer.wrapper}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def default_worker_dispatch_port(
|
|
32
|
+
descriptor: HostDescriptor,
|
|
33
|
+
provider_registry: ProviderRegistry | None = None,
|
|
34
|
+
) -> WorkerDispatchPort:
|
|
35
|
+
if descriptor.dispatch_mode == "team":
|
|
36
|
+
return NativeTeamDispatchPort()
|
|
37
|
+
wrappers = (
|
|
38
|
+
provider_worker_wrappers(provider_registry)
|
|
39
|
+
if provider_registry is not None
|
|
40
|
+
else None
|
|
41
|
+
)
|
|
42
|
+
default_backend = (
|
|
43
|
+
dispatch_core.BACKEND_TMUX_PANE
|
|
44
|
+
if descriptor.launch_mode == "team"
|
|
45
|
+
else dispatch_core.BACKEND_CLI_WRAPPER
|
|
46
|
+
)
|
|
47
|
+
return CliWrapperDispatchPort(
|
|
48
|
+
supported_worker_wrappers=wrappers,
|
|
49
|
+
unsupported_worker_label=f"{descriptor.id} lead",
|
|
50
|
+
required_lead_runtime=descriptor.id,
|
|
51
|
+
default_backend=default_backend,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class WorkerDispatchHostAdapter:
|
|
56
|
+
def __init__(self, host_adapter, worker_dispatch_port: WorkerDispatchPort) -> None:
|
|
57
|
+
self._host_adapter = host_adapter
|
|
58
|
+
self._worker_dispatch_port = worker_dispatch_port
|
|
59
|
+
self.descriptor = host_adapter.descriptor
|
|
60
|
+
|
|
61
|
+
def detect(self, context):
|
|
62
|
+
return self._host_adapter.detect(context)
|
|
63
|
+
|
|
64
|
+
def probe(self, context):
|
|
65
|
+
return self._host_adapter.probe(context)
|
|
66
|
+
|
|
67
|
+
def interaction(self):
|
|
68
|
+
return self._host_adapter.interaction()
|
|
69
|
+
|
|
70
|
+
def lead_session(self):
|
|
71
|
+
return self._host_adapter.lead_session()
|
|
72
|
+
|
|
73
|
+
def worker_dispatch(self) -> WorkerDispatchPort:
|
|
74
|
+
return self._worker_dispatch_port
|
|
75
|
+
|
|
76
|
+
def usage_accounting(self):
|
|
77
|
+
return self._host_adapter.usage_accounting()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def with_worker_dispatch(
|
|
81
|
+
host_adapter,
|
|
82
|
+
worker_dispatch_port: WorkerDispatchPort,
|
|
83
|
+
) -> WorkerDispatchHostAdapter:
|
|
84
|
+
return WorkerDispatchHostAdapter(host_adapter, worker_dispatch_port)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
__all__ = (
|
|
88
|
+
"WorkerDispatchHostAdapter",
|
|
89
|
+
"default_worker_dispatch_port",
|
|
90
|
+
"provider_worker_wrappers",
|
|
91
|
+
"with_worker_dispatch",
|
|
92
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""CLI-wrapper dispatch planning backed by the shared dispatch core."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
|
|
6
|
+
from ... import dispatch_core
|
|
7
|
+
from ...ports.worker_dispatch import WorkerDispatchPlan, WorkerDispatchRequest
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CliWrapperDispatchPort:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
*,
|
|
14
|
+
supported_worker_wrappers: Mapping[str, str] | None = None,
|
|
15
|
+
unsupported_worker_label: str = "host",
|
|
16
|
+
required_lead_runtime: str | None = None,
|
|
17
|
+
default_backend: str = dispatch_core.BACKEND_CLI_WRAPPER,
|
|
18
|
+
cli_wrapper_assignments_only: bool = False,
|
|
19
|
+
default_provider_by_worker_id: Mapping[str, str] | None = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
self._supported_worker_wrappers = supported_worker_wrappers or {}
|
|
22
|
+
self._unsupported_worker_label = unsupported_worker_label
|
|
23
|
+
self._required_lead_runtime = required_lead_runtime
|
|
24
|
+
self._default_backend = default_backend
|
|
25
|
+
self._cli_wrapper_assignments_only = cli_wrapper_assignments_only
|
|
26
|
+
self._default_provider_by_worker_id = dict(
|
|
27
|
+
default_provider_by_worker_id or {}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def build_plan(self, request: WorkerDispatchRequest) -> WorkerDispatchPlan:
|
|
31
|
+
backend_plan = None
|
|
32
|
+
if request.workspace_root is not None:
|
|
33
|
+
backend_plan = dispatch_core.build_dispatch_plan(
|
|
34
|
+
project_root=request.project_root,
|
|
35
|
+
run_manifest_path=request.run_manifest,
|
|
36
|
+
workspace_root=request.workspace_root,
|
|
37
|
+
okstra_bin=request.okstra_bin,
|
|
38
|
+
requested_workers=request.requested_workers,
|
|
39
|
+
idle_timeout_seconds=request.idle_timeout_seconds,
|
|
40
|
+
required_lead_runtime=self._required_lead_runtime,
|
|
41
|
+
default_backend=self._default_backend,
|
|
42
|
+
supported_worker_wrappers=self._supported_worker_wrappers,
|
|
43
|
+
unsupported_worker_label=self._unsupported_worker_label,
|
|
44
|
+
dispatch_kind=request.dispatch_kind,
|
|
45
|
+
jobs_file=request.jobs_file,
|
|
46
|
+
cli_wrapper_assignments_only=self._cli_wrapper_assignments_only,
|
|
47
|
+
default_provider_by_worker_id=self._default_provider_by_worker_id,
|
|
48
|
+
)
|
|
49
|
+
return WorkerDispatchPlan(
|
|
50
|
+
mode="cli-wrapper",
|
|
51
|
+
request=request,
|
|
52
|
+
backend_plan=backend_plan,
|
|
53
|
+
dispatch_mode="render-only",
|
|
54
|
+
)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""cmux pane dispatch planning and lead-adapter metadata override."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
|
|
6
|
+
from ... import dispatch_core
|
|
7
|
+
from ...ports.worker_dispatch import (
|
|
8
|
+
WorkerDispatchPlan,
|
|
9
|
+
WorkerDispatchPort,
|
|
10
|
+
WorkerDispatchRequest,
|
|
11
|
+
)
|
|
12
|
+
from .cli_wrapper import CliWrapperDispatchPort
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
CMUX_ADAPTER_NAME = "cmux"
|
|
16
|
+
CMUX_RELAY_CONTRACT = "lead/adapters/cmux.md"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CmuxDispatchPort:
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
*,
|
|
23
|
+
supported_worker_wrappers: Mapping[str, str] | None = None,
|
|
24
|
+
unsupported_worker_label: str = "host",
|
|
25
|
+
required_lead_runtime: str | None = None,
|
|
26
|
+
cli_wrapper_assignments_only: bool = False,
|
|
27
|
+
default_provider_by_worker_id: Mapping[str, str] | None = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
self._backend_port = CliWrapperDispatchPort(
|
|
30
|
+
supported_worker_wrappers=supported_worker_wrappers,
|
|
31
|
+
unsupported_worker_label=unsupported_worker_label,
|
|
32
|
+
required_lead_runtime=required_lead_runtime,
|
|
33
|
+
default_backend=dispatch_core.BACKEND_CMUX_PANE,
|
|
34
|
+
cli_wrapper_assignments_only=cli_wrapper_assignments_only,
|
|
35
|
+
default_provider_by_worker_id=default_provider_by_worker_id,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def build_plan(self, request: WorkerDispatchRequest) -> WorkerDispatchPlan:
|
|
39
|
+
backend_plan = self._backend_port.build_plan(request).backend_plan
|
|
40
|
+
return WorkerDispatchPlan(
|
|
41
|
+
mode="cmux-pane",
|
|
42
|
+
request=request,
|
|
43
|
+
backend_plan=backend_plan,
|
|
44
|
+
adapter_name=CMUX_ADAPTER_NAME,
|
|
45
|
+
dispatch_mode="team",
|
|
46
|
+
relay_contract=CMUX_RELAY_CONTRACT,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def dispatch_port_for_terminal_backend(
|
|
51
|
+
terminal_backend: str,
|
|
52
|
+
fallback_port: WorkerDispatchPort,
|
|
53
|
+
*,
|
|
54
|
+
supported_worker_wrappers: Mapping[str, str] | None = None,
|
|
55
|
+
unsupported_worker_label: str = "host",
|
|
56
|
+
required_lead_runtime: str | None = None,
|
|
57
|
+
cli_wrapper_assignments_only: bool = False,
|
|
58
|
+
default_provider_by_worker_id: Mapping[str, str] | None = None,
|
|
59
|
+
) -> WorkerDispatchPort:
|
|
60
|
+
if terminal_backend != dispatch_core.BACKEND_CMUX_PANE:
|
|
61
|
+
return fallback_port
|
|
62
|
+
return CmuxDispatchPort(
|
|
63
|
+
supported_worker_wrappers=supported_worker_wrappers,
|
|
64
|
+
unsupported_worker_label=unsupported_worker_label,
|
|
65
|
+
required_lead_runtime=required_lead_runtime,
|
|
66
|
+
cli_wrapper_assignments_only=cli_wrapper_assignments_only,
|
|
67
|
+
default_provider_by_worker_id=default_provider_by_worker_id,
|
|
68
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Native host-team dispatch planning."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from ...ports.worker_dispatch import WorkerDispatchPlan, WorkerDispatchRequest
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class NativeTeamDispatchPort:
|
|
8
|
+
def build_plan(self, request: WorkerDispatchRequest) -> WorkerDispatchPlan:
|
|
9
|
+
return WorkerDispatchPlan(
|
|
10
|
+
mode="native-team",
|
|
11
|
+
request=request,
|
|
12
|
+
dispatch_mode="team",
|
|
13
|
+
)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Bundled Antigravity host strategy."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import shutil
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from okstra_ctl.adapters.accounting import CliArtifactUsageAccountingPort
|
|
9
|
+
from okstra_ctl.adapters.hosts.capability_adapter import (
|
|
10
|
+
INTERACTION_FUNCTIONS,
|
|
11
|
+
PENDING_HOST_PORT,
|
|
12
|
+
CapabilityHostAdapter,
|
|
13
|
+
no_automatic_claim,
|
|
14
|
+
numbered_interaction_port,
|
|
15
|
+
)
|
|
16
|
+
from okstra_ctl.domain.host import HostDescriptor
|
|
17
|
+
from okstra_ctl.registry.provider_registry import ProviderRegistry
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
DESCRIPTOR = HostDescriptor(
|
|
21
|
+
id="antigravity",
|
|
22
|
+
aliases=("avg", "agy"),
|
|
23
|
+
native_provider_id="antigravity",
|
|
24
|
+
required_executables=("agy",),
|
|
25
|
+
launch_mode="lead",
|
|
26
|
+
install_targets=frozenset({"agents"}),
|
|
27
|
+
agent_id="antigravity",
|
|
28
|
+
agent_label="Antigravity CLI",
|
|
29
|
+
role="Antigravity lead",
|
|
30
|
+
dispatch_mode="render-only",
|
|
31
|
+
session_accounting="artifact-only",
|
|
32
|
+
has_claude_session=False,
|
|
33
|
+
relay_contract=str(Path(__file__).with_name("relay.md").resolve()),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def create_adapter(
|
|
38
|
+
*,
|
|
39
|
+
executable_finder: Callable[[str], str | None] = shutil.which,
|
|
40
|
+
interaction_port=None,
|
|
41
|
+
lead_session_port=PENDING_HOST_PORT,
|
|
42
|
+
worker_dispatch_port=PENDING_HOST_PORT,
|
|
43
|
+
usage_accounting_port=CliArtifactUsageAccountingPort(),
|
|
44
|
+
provider_registry: ProviderRegistry | None = None,
|
|
45
|
+
) -> CapabilityHostAdapter:
|
|
46
|
+
return CapabilityHostAdapter(
|
|
47
|
+
DESCRIPTOR,
|
|
48
|
+
executable_finder=executable_finder,
|
|
49
|
+
interaction_port=(
|
|
50
|
+
interaction_port
|
|
51
|
+
if interaction_port is not None
|
|
52
|
+
else numbered_interaction_port()
|
|
53
|
+
),
|
|
54
|
+
lead_session_port=lead_session_port,
|
|
55
|
+
worker_dispatch_port=worker_dispatch_port,
|
|
56
|
+
usage_accounting_port=usage_accounting_port,
|
|
57
|
+
supported_functions=INTERACTION_FUNCTIONS,
|
|
58
|
+
detector=no_automatic_claim,
|
|
59
|
+
provider_registry=provider_registry,
|
|
60
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"schemaVersion": 1, "id": "antigravity", "factory": "adapter.py:create_adapter", "nativeProviderId": "antigravity", "requiredExecutables": ["agy"], "relayContract": "relay.md"}
|
|
@@ -18,6 +18,58 @@ This adapter maps the neutral Okstra lead operations to the current Antigravity
|
|
|
18
18
|
| `teardownMode` | `process-cleanup` |
|
|
19
19
|
| `leadEventSource` | `lead-events-jsonl` |
|
|
20
20
|
|
|
21
|
+
## Wizard interaction relay
|
|
22
|
+
|
|
23
|
+
Read this contract when `okstra preflight` returns this file as `runtimeReadiness.relayContract`. The JSON is the complete interaction mapping for this host. This relay exposes only text input; do not invent a native picker function.
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"schemaVersion": 1,
|
|
28
|
+
"runtime": "antigravity",
|
|
29
|
+
"semanticFunctions": ["plain_text_input"],
|
|
30
|
+
"interactions": {
|
|
31
|
+
"numbered-single": {
|
|
32
|
+
"function": "host-text",
|
|
33
|
+
"input": {
|
|
34
|
+
"questions": "one",
|
|
35
|
+
"question": "label-with-progress",
|
|
36
|
+
"options": "all-in-original-order-as-numbered-markdown-label-and-description"
|
|
37
|
+
},
|
|
38
|
+
"response": { "source": "next-message", "submit": "raw" }
|
|
39
|
+
},
|
|
40
|
+
"numbered-multi": {
|
|
41
|
+
"function": "host-text",
|
|
42
|
+
"input": {
|
|
43
|
+
"questions": "one",
|
|
44
|
+
"question": "label-with-progress",
|
|
45
|
+
"options": "all-in-original-order-as-numbered-markdown-label-and-description"
|
|
46
|
+
},
|
|
47
|
+
"response": { "source": "next-message", "submit": "raw" }
|
|
48
|
+
},
|
|
49
|
+
"sequential-group": {
|
|
50
|
+
"function": "host-text",
|
|
51
|
+
"input": {
|
|
52
|
+
"questions": "all-one-at-a-time-in-original-order",
|
|
53
|
+
"question": "label-with-progress",
|
|
54
|
+
"options": "all-in-original-order-as-numbered-markdown-label-and-description"
|
|
55
|
+
},
|
|
56
|
+
"response": {
|
|
57
|
+
"source": "next-message-by-question-position",
|
|
58
|
+
"submit": "compact-step-json-raw"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"plain-text": {
|
|
62
|
+
"function": "host-text",
|
|
63
|
+
"input": { "questions": "one", "question": "label-with-progress" },
|
|
64
|
+
"response": { "source": "next-message", "submit": "raw" }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Render every numbered item as its option label followed by its description verbatim, preserving every item and its original order. The next user message is the raw answer: do not translate a number such as `1`, a CSV reply such as `1, 3`, an option label, or an option value before `okstra wizard step`. For `sequential-group`, collect one raw reply per question in order and build one compact JSON object keyed by the corresponding `questions[].step`; the wizard owns all normalization.
|
|
71
|
+
|
|
72
|
+
|
|
21
73
|
## Semantic operation mapping
|
|
22
74
|
|
|
23
75
|
| Operation | Mapping |
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""Capability-based strategy shared by bundled host adapters."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Callable, Mapping
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from okstra_ctl.adapters.dispatch import default_worker_dispatch_port
|
|
8
|
+
from okstra_ctl.domain.host import (
|
|
9
|
+
HostClaim,
|
|
10
|
+
HostDescriptor,
|
|
11
|
+
HostReadiness,
|
|
12
|
+
HostResolutionContext,
|
|
13
|
+
HostSessionContext,
|
|
14
|
+
LeadResumeRequest,
|
|
15
|
+
LeadSessionPlan,
|
|
16
|
+
LeadStartRequest,
|
|
17
|
+
ProviderUnavailable,
|
|
18
|
+
)
|
|
19
|
+
from okstra_ctl.domain.provider import LeadLaunchSpec
|
|
20
|
+
from okstra_ctl.domain.wizard.interaction import (
|
|
21
|
+
AnswerProtocol,
|
|
22
|
+
InteractionPlan,
|
|
23
|
+
WizardPrompt,
|
|
24
|
+
)
|
|
25
|
+
from okstra_ctl.ports import (
|
|
26
|
+
InteractionPort,
|
|
27
|
+
LeadSessionPort,
|
|
28
|
+
UsageAccountingPort,
|
|
29
|
+
WorkerDispatchPort,
|
|
30
|
+
)
|
|
31
|
+
from okstra_ctl.registry.provider_registry import (
|
|
32
|
+
ProviderRegistry,
|
|
33
|
+
default_provider_registry,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class HostPorts:
|
|
39
|
+
interaction: InteractionPort
|
|
40
|
+
lead_session: LeadSessionPort
|
|
41
|
+
worker_dispatch: WorkerDispatchPort
|
|
42
|
+
usage_accounting: UsageAccountingPort
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class PendingHostPort:
|
|
46
|
+
"""Placeholder replaced as each concrete port migrates behind the adapter."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
PENDING_HOST_PORT = PendingHostPort()
|
|
50
|
+
PLAIN_TEXT_FUNCTIONS = frozenset({"plain_text_input"})
|
|
51
|
+
INTERACTION_FUNCTIONS = PLAIN_TEXT_FUNCTIONS | frozenset({
|
|
52
|
+
"plain_text_input",
|
|
53
|
+
"native_single_select",
|
|
54
|
+
"native_multi_select",
|
|
55
|
+
"native_question_group",
|
|
56
|
+
})
|
|
57
|
+
_NATIVE_QUESTION_LIMIT = 4
|
|
58
|
+
_NATIVE_OPTION_RANGE = range(2, 5)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _native_options_fit(prompt: WizardPrompt) -> bool:
|
|
62
|
+
labels = tuple(option.label for option in prompt.options)
|
|
63
|
+
return (
|
|
64
|
+
len(labels) in _NATIVE_OPTION_RANGE
|
|
65
|
+
and len(set(labels)) == len(labels)
|
|
66
|
+
and (not prompt.multi or all(", " not in label for label in labels))
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _native_group_fits(prompt: WizardPrompt) -> bool:
|
|
71
|
+
return (
|
|
72
|
+
1 <= len(prompt.questions) <= _NATIVE_QUESTION_LIMIT
|
|
73
|
+
and all(_native_options_fit(question) for question in prompt.questions)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class CapabilityInteractionPort:
|
|
78
|
+
def plan(
|
|
79
|
+
self,
|
|
80
|
+
prompt: WizardPrompt,
|
|
81
|
+
context: HostSessionContext,
|
|
82
|
+
) -> InteractionPlan:
|
|
83
|
+
functions = context.available_functions & INTERACTION_FUNCTIONS
|
|
84
|
+
if prompt.kind == "pick_group":
|
|
85
|
+
if (
|
|
86
|
+
"native_question_group" in functions
|
|
87
|
+
and _native_group_fits(prompt)
|
|
88
|
+
):
|
|
89
|
+
return InteractionPlan("native-group", AnswerProtocol("group-json"))
|
|
90
|
+
return InteractionPlan(
|
|
91
|
+
"sequential-group",
|
|
92
|
+
AnswerProtocol("numbered-group-json"),
|
|
93
|
+
)
|
|
94
|
+
if prompt.kind != "pick":
|
|
95
|
+
return InteractionPlan("plain-text", AnswerProtocol("exact-value"))
|
|
96
|
+
native_options_fit = _native_options_fit(prompt)
|
|
97
|
+
if (
|
|
98
|
+
prompt.multi
|
|
99
|
+
and "native_multi_select" in functions
|
|
100
|
+
and native_options_fit
|
|
101
|
+
):
|
|
102
|
+
return InteractionPlan(
|
|
103
|
+
"native-multi",
|
|
104
|
+
AnswerProtocol("exact-value", multi=True),
|
|
105
|
+
)
|
|
106
|
+
if (
|
|
107
|
+
not prompt.multi
|
|
108
|
+
and "native_single_select" in functions
|
|
109
|
+
and native_options_fit
|
|
110
|
+
):
|
|
111
|
+
return InteractionPlan("native-single", AnswerProtocol("exact-value"))
|
|
112
|
+
kind = "numbered-multi" if prompt.multi else "numbered-single"
|
|
113
|
+
return InteractionPlan(kind, AnswerProtocol("numbered", multi=prompt.multi))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def numbered_interaction_port() -> CapabilityInteractionPort:
|
|
117
|
+
return CapabilityInteractionPort()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ProviderLeadSessionPort:
|
|
121
|
+
def __init__(
|
|
122
|
+
self,
|
|
123
|
+
*,
|
|
124
|
+
host_id: str,
|
|
125
|
+
native_provider_id: str,
|
|
126
|
+
provider_registry: ProviderRegistry,
|
|
127
|
+
) -> None:
|
|
128
|
+
self._host_id = host_id
|
|
129
|
+
self._native_provider_id = native_provider_id
|
|
130
|
+
self._provider_registry = provider_registry
|
|
131
|
+
|
|
132
|
+
def build_start(
|
|
133
|
+
self,
|
|
134
|
+
request: LeadStartRequest,
|
|
135
|
+
context: HostSessionContext,
|
|
136
|
+
) -> LeadSessionPlan:
|
|
137
|
+
if context.entry_mode == "current-session":
|
|
138
|
+
return self._current_session_plan()
|
|
139
|
+
launch = self._lead_launch()
|
|
140
|
+
argv = (launch.executable, *launch.sandbox_waiver, *request.argv)
|
|
141
|
+
return self._spawn_plan(argv, launch.sandbox_waiver_note)
|
|
142
|
+
|
|
143
|
+
def build_resume(
|
|
144
|
+
self,
|
|
145
|
+
request: LeadResumeRequest,
|
|
146
|
+
context: HostSessionContext,
|
|
147
|
+
) -> LeadSessionPlan:
|
|
148
|
+
if context.entry_mode == "current-session":
|
|
149
|
+
return self._current_session_plan()
|
|
150
|
+
launch = self._lead_launch()
|
|
151
|
+
session_args = (
|
|
152
|
+
(launch.resume_session_id_flag, request.session_id)
|
|
153
|
+
if launch.resume_session_id_flag and request.session_id
|
|
154
|
+
else ()
|
|
155
|
+
)
|
|
156
|
+
argv = (
|
|
157
|
+
launch.executable,
|
|
158
|
+
*launch.sandbox_waiver,
|
|
159
|
+
*session_args,
|
|
160
|
+
*request.argv,
|
|
161
|
+
)
|
|
162
|
+
return self._spawn_plan(argv, launch.sandbox_waiver_note)
|
|
163
|
+
|
|
164
|
+
def _lead_launch(self) -> LeadLaunchSpec:
|
|
165
|
+
if not self._native_provider_id:
|
|
166
|
+
raise ProviderUnavailable(
|
|
167
|
+
f"host {self._host_id!r} has no native lead provider"
|
|
168
|
+
)
|
|
169
|
+
provider = self._provider_registry.resolve(self._native_provider_id)
|
|
170
|
+
if provider.lead_launch is None:
|
|
171
|
+
raise ProviderUnavailable(
|
|
172
|
+
f"provider {self._native_provider_id!r} cannot lead"
|
|
173
|
+
)
|
|
174
|
+
return provider.lead_launch
|
|
175
|
+
|
|
176
|
+
def _current_session_plan(self) -> LeadSessionPlan:
|
|
177
|
+
return LeadSessionPlan("current-session", (), {}, "")
|
|
178
|
+
|
|
179
|
+
def _spawn_plan(
|
|
180
|
+
self, argv: tuple[str, ...], sandbox_note: str
|
|
181
|
+
) -> LeadSessionPlan:
|
|
182
|
+
return LeadSessionPlan(
|
|
183
|
+
"spawn-process",
|
|
184
|
+
argv,
|
|
185
|
+
{"OKSTRA_RUNTIME_HOST": self._host_id},
|
|
186
|
+
sandbox_note,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class CapabilityHostAdapter:
|
|
191
|
+
def __init__(
|
|
192
|
+
self,
|
|
193
|
+
descriptor: HostDescriptor,
|
|
194
|
+
*,
|
|
195
|
+
executable_finder: Callable[[str], str | None],
|
|
196
|
+
interaction_port: InteractionPort,
|
|
197
|
+
lead_session_port: LeadSessionPort,
|
|
198
|
+
worker_dispatch_port: WorkerDispatchPort,
|
|
199
|
+
usage_accounting_port: UsageAccountingPort,
|
|
200
|
+
supported_functions: frozenset[str],
|
|
201
|
+
detector: Callable[[HostResolutionContext], HostClaim | None],
|
|
202
|
+
provider_registry: ProviderRegistry | None = None,
|
|
203
|
+
readiness_probe: Callable[
|
|
204
|
+
[HostSessionContext], tuple[Mapping[str, object], ...]
|
|
205
|
+
] | None = None,
|
|
206
|
+
) -> None:
|
|
207
|
+
self.descriptor = descriptor
|
|
208
|
+
self._executable_finder = executable_finder
|
|
209
|
+
resolved_provider_registry = provider_registry or default_provider_registry()
|
|
210
|
+
resolved_lead_session = lead_session_port
|
|
211
|
+
if lead_session_port is PENDING_HOST_PORT:
|
|
212
|
+
resolved_lead_session = ProviderLeadSessionPort(
|
|
213
|
+
host_id=descriptor.id,
|
|
214
|
+
native_provider_id=descriptor.native_provider_id,
|
|
215
|
+
provider_registry=resolved_provider_registry,
|
|
216
|
+
)
|
|
217
|
+
resolved_worker_dispatch = worker_dispatch_port
|
|
218
|
+
if worker_dispatch_port is PENDING_HOST_PORT:
|
|
219
|
+
resolved_worker_dispatch = default_worker_dispatch_port(
|
|
220
|
+
descriptor,
|
|
221
|
+
resolved_provider_registry,
|
|
222
|
+
)
|
|
223
|
+
self._ports = HostPorts(
|
|
224
|
+
interaction_port,
|
|
225
|
+
resolved_lead_session,
|
|
226
|
+
resolved_worker_dispatch,
|
|
227
|
+
usage_accounting_port,
|
|
228
|
+
)
|
|
229
|
+
self._supported_functions = supported_functions
|
|
230
|
+
self._detector = detector
|
|
231
|
+
self._readiness_probe = readiness_probe or (lambda context: ())
|
|
232
|
+
|
|
233
|
+
def detect(self, context: HostResolutionContext) -> HostClaim | None:
|
|
234
|
+
return self._detector(context)
|
|
235
|
+
|
|
236
|
+
def probe(self, context: HostSessionContext) -> HostReadiness:
|
|
237
|
+
effective_functions = self._effective_functions(context)
|
|
238
|
+
if context.host_id != self.descriptor.id:
|
|
239
|
+
reason = (
|
|
240
|
+
f"host mismatch: expected {self.descriptor.id!r}, "
|
|
241
|
+
f"received {context.host_id!r}"
|
|
242
|
+
)
|
|
243
|
+
return HostReadiness(False, effective_functions, (), (), reason)
|
|
244
|
+
if context.entry_mode == "current-session":
|
|
245
|
+
return self._readiness(context, effective_functions)
|
|
246
|
+
missing = tuple(
|
|
247
|
+
executable
|
|
248
|
+
for executable in self.descriptor.required_executables
|
|
249
|
+
if self._executable_finder(executable) is None
|
|
250
|
+
)
|
|
251
|
+
if missing:
|
|
252
|
+
reason = f"missing required executables: {', '.join(missing)}"
|
|
253
|
+
return HostReadiness(False, effective_functions, missing, (), reason)
|
|
254
|
+
return self._readiness(context, effective_functions)
|
|
255
|
+
|
|
256
|
+
def interaction(self) -> InteractionPort:
|
|
257
|
+
return self._ports.interaction
|
|
258
|
+
|
|
259
|
+
def lead_session(self) -> LeadSessionPort:
|
|
260
|
+
return self._ports.lead_session
|
|
261
|
+
|
|
262
|
+
def worker_dispatch(self) -> WorkerDispatchPort:
|
|
263
|
+
return self._ports.worker_dispatch
|
|
264
|
+
|
|
265
|
+
def usage_accounting(self) -> UsageAccountingPort:
|
|
266
|
+
return self._ports.usage_accounting
|
|
267
|
+
|
|
268
|
+
def _effective_functions(
|
|
269
|
+
self, context: HostSessionContext
|
|
270
|
+
) -> frozenset[str]:
|
|
271
|
+
return self._supported_functions & context.available_functions
|
|
272
|
+
|
|
273
|
+
def _readiness(
|
|
274
|
+
self,
|
|
275
|
+
context: HostSessionContext,
|
|
276
|
+
effective_functions: frozenset[str],
|
|
277
|
+
) -> HostReadiness:
|
|
278
|
+
checks = self._readiness_probe(context)
|
|
279
|
+
blockers = tuple(
|
|
280
|
+
check for check in checks
|
|
281
|
+
if check.get("status") in {"required", "unavailable"}
|
|
282
|
+
)
|
|
283
|
+
if blockers:
|
|
284
|
+
return HostReadiness(
|
|
285
|
+
False, effective_functions, (), checks, "host not ready"
|
|
286
|
+
)
|
|
287
|
+
return HostReadiness(True, effective_functions, (), checks, "ready")
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def no_automatic_claim(context: HostResolutionContext) -> HostClaim | None:
|
|
291
|
+
"""Require an explicit host instead of inferring one from installed CLIs."""
|
|
292
|
+
return None
|