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.
Files changed (167) hide show
  1. package/README.md +8 -6
  2. package/docs/architecture.md +24 -15
  3. package/docs/cli.md +15 -8
  4. package/docs/for-ai/README.md +2 -2
  5. package/docs/for-ai/skills/okstra-inspect.md +2 -2
  6. package/docs/for-ai/skills/okstra-user-response.md +2 -2
  7. package/docs/project-structure-overview.md +22 -14
  8. package/package.json +1 -1
  9. package/runtime/BUILD.json +2 -2
  10. package/runtime/agents/workers/antigravity-worker.md +9 -7
  11. package/runtime/agents/workers/claude-worker.md +1 -0
  12. package/runtime/agents/workers/codex-worker.md +9 -7
  13. package/runtime/agents/workers/grok-worker.md +6 -4
  14. package/runtime/agents/workers/kimi-worker.md +6 -4
  15. package/runtime/bin/lib/okstra/cli.sh +5 -0
  16. package/runtime/bin/lib/okstra/globals.sh +2 -0
  17. package/runtime/bin/lib/okstra/usage.sh +5 -5
  18. package/runtime/bin/okstra-antigravity-exec.sh +1 -340
  19. package/runtime/bin/okstra-claude-exec.sh +1 -178
  20. package/runtime/bin/okstra-codex-exec.sh +1 -467
  21. package/runtime/bin/okstra-provider-exec.py +165 -190
  22. package/runtime/bin/okstra-trace-cleanup.sh +14 -7
  23. package/runtime/bin/okstra-wrapper-status.py +26 -19
  24. package/runtime/bin/okstra.sh +87 -91
  25. package/runtime/prompts/lead/adapters/cmux.md +2 -2
  26. package/runtime/prompts/lead/convergence.md +36 -8
  27. package/runtime/prompts/lead/okstra-lead-contract.md +24 -1
  28. package/runtime/prompts/lead/plan-body-verification.md +9 -1
  29. package/runtime/prompts/lead/report-writer.md +1 -0
  30. package/runtime/prompts/lead/team-contract.md +3 -3
  31. package/runtime/prompts/profiles/_common-contract.md +9 -1
  32. package/runtime/prompts/profiles/_coverage-critic.md +1 -1
  33. package/runtime/prompts/profiles/_implementation-diff-review.md +3 -1
  34. package/runtime/prompts/profiles/_implementation-self-check.md +1 -1
  35. package/runtime/prompts/profiles/_implementation-verifier.md +3 -1
  36. package/runtime/prompts/profiles/implementation-planning.md +6 -4
  37. package/runtime/python/okstra_ctl/adapters/accounting/__init__.py +11 -0
  38. package/runtime/python/okstra_ctl/adapters/accounting/claude_jsonl.py +17 -0
  39. package/runtime/python/okstra_ctl/adapters/accounting/cli_artifact.py +17 -0
  40. package/runtime/python/okstra_ctl/adapters/accounting/unavailable.py +19 -0
  41. package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +92 -0
  42. package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +54 -0
  43. package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +68 -0
  44. package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
  45. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +60 -0
  46. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/manifest.json +1 -0
  47. package/runtime/{prompts/lead/adapters/antigravity.md → python/okstra_ctl/adapters/hosts/antigravity/relay.md} +52 -0
  48. package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +292 -0
  49. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +120 -0
  50. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -0
  51. package/runtime/{prompts/lead/adapters/claude-code.md → python/okstra_ctl/adapters/hosts/claude-code/relay.md} +112 -1
  52. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +60 -0
  53. package/runtime/python/okstra_ctl/adapters/hosts/codex/manifest.json +1 -0
  54. package/runtime/{prompts/lead/adapters/codex.md → python/okstra_ctl/adapters/hosts/codex/relay.md} +52 -0
  55. package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +72 -0
  56. package/runtime/python/okstra_ctl/adapters/hosts/external/manifest.json +1 -0
  57. package/runtime/{prompts/lead/adapters/external.md → python/okstra_ctl/adapters/hosts/external/relay.md} +53 -1
  58. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +63 -0
  59. package/runtime/python/okstra_ctl/adapters/hosts/grok/manifest.json +1 -0
  60. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +90 -0
  61. package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +63 -0
  62. package/runtime/python/okstra_ctl/adapters/hosts/kimi/manifest.json +1 -0
  63. package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +90 -0
  64. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +183 -0
  65. package/runtime/python/okstra_ctl/adapters/providers/antigravity/manifest.json +1 -0
  66. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +110 -0
  67. package/runtime/python/okstra_ctl/adapters/providers/claude/manifest.json +1 -0
  68. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +84 -0
  69. package/runtime/python/okstra_ctl/adapters/providers/codex/manifest.json +1 -0
  70. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +76 -0
  71. package/runtime/python/okstra_ctl/adapters/providers/grok/manifest.json +1 -0
  72. package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +80 -0
  73. package/runtime/python/okstra_ctl/adapters/providers/kimi/manifest.json +1 -0
  74. package/runtime/python/okstra_ctl/application/__init__.py +1 -0
  75. package/runtime/python/okstra_ctl/application/advance_wizard.py +25 -0
  76. package/runtime/python/okstra_ctl/application/collect_usage.py +15 -0
  77. package/runtime/python/okstra_ctl/application/dispatch_assignments.py +15 -0
  78. package/runtime/python/okstra_ctl/application/resolve_assignment.py +93 -0
  79. package/runtime/python/okstra_ctl/application/resume_run.py +21 -0
  80. package/runtime/python/okstra_ctl/application/start_run.py +21 -0
  81. package/runtime/python/okstra_ctl/codex_dispatch.py +49 -826
  82. package/runtime/python/okstra_ctl/dispatch_core.py +244 -29
  83. package/runtime/python/okstra_ctl/dispatch_state.py +27 -0
  84. package/runtime/python/okstra_ctl/domain/__init__.py +34 -0
  85. package/runtime/python/okstra_ctl/domain/host.py +100 -0
  86. package/runtime/python/okstra_ctl/domain/provider.py +70 -0
  87. package/runtime/python/okstra_ctl/domain/wizard/__init__.py +19 -0
  88. package/runtime/python/okstra_ctl/domain/wizard/interaction.py +140 -0
  89. package/runtime/python/okstra_ctl/domain/worker_exec.py +102 -0
  90. package/runtime/python/okstra_ctl/domain/worker_role.py +34 -0
  91. package/runtime/python/okstra_ctl/domain/worker_stream.py +261 -0
  92. package/runtime/python/okstra_ctl/entrypoints/__init__.py +1 -0
  93. package/runtime/python/okstra_ctl/entrypoints/hosts.py +334 -0
  94. package/runtime/python/okstra_ctl/incremental_scope.py +16 -4
  95. package/runtime/python/okstra_ctl/models.py +54 -269
  96. package/runtime/python/okstra_ctl/ports/__init__.py +15 -0
  97. package/runtime/python/okstra_ctl/ports/host.py +32 -0
  98. package/runtime/python/okstra_ctl/ports/interaction.py +15 -0
  99. package/runtime/python/okstra_ctl/ports/lead_session.py +25 -0
  100. package/runtime/python/okstra_ctl/ports/usage_accounting.py +23 -0
  101. package/runtime/python/okstra_ctl/ports/worker_dispatch.py +32 -0
  102. package/runtime/python/okstra_ctl/registry/__init__.py +13 -0
  103. package/runtime/python/okstra_ctl/registry/factory_loader.py +32 -0
  104. package/runtime/python/okstra_ctl/registry/host_discovery.py +124 -0
  105. package/runtime/python/okstra_ctl/registry/host_registry.py +365 -0
  106. package/runtime/python/okstra_ctl/registry/provider_registry.py +149 -0
  107. package/runtime/python/okstra_ctl/render.py +145 -47
  108. package/runtime/python/okstra_ctl/report_html/common.py +71 -25
  109. package/runtime/python/okstra_ctl/report_html/models.py +5 -0
  110. package/runtime/python/okstra_ctl/report_html/render.py +1 -1
  111. package/runtime/python/okstra_ctl/report_html/run_usage.py +19 -0
  112. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +14 -0
  113. package/runtime/python/okstra_ctl/report_views.py +44 -16
  114. package/runtime/python/okstra_ctl/run.py +80 -58
  115. package/runtime/python/okstra_ctl/session.py +1 -1
  116. package/runtime/python/okstra_ctl/stage_citations.py +52 -15
  117. package/runtime/python/okstra_ctl/team.py +44 -32
  118. package/runtime/python/okstra_ctl/user_response.py +45 -29
  119. package/runtime/python/okstra_ctl/wizard.py +175 -73
  120. package/runtime/python/okstra_ctl/worker_audit_ledger.py +29 -4
  121. package/runtime/python/okstra_ctl/worker_prompt_policy.py +10 -3
  122. package/runtime/python/okstra_ctl/worker_request.py +140 -0
  123. package/runtime/python/okstra_ctl/worker_runner.py +622 -0
  124. package/runtime/python/okstra_token_usage/collect.py +42 -7
  125. package/runtime/python/okstra_token_usage/report.py +42 -0
  126. package/runtime/python/okstra_token_usage/task_totals.py +88 -0
  127. package/runtime/schemas/final-report-v1.0.schema.json +4040 -1066
  128. package/runtime/schemas/final-report-v2.0.schema.json +5673 -1412
  129. package/runtime/skills/okstra-inspect/SKILL.md +1 -2
  130. package/runtime/skills/okstra-inspect/facets/logs.md +5 -5
  131. package/runtime/skills/okstra-inspect/facets/run-audit.md +3 -3
  132. package/runtime/skills/okstra-run/SKILL.md +74 -29
  133. package/runtime/skills/okstra-user-response/SKILL.md +15 -5
  134. package/runtime/templates/implementation-worker-preamble.md +1 -1
  135. package/runtime/templates/report-writer-prompt-preamble.md +1 -0
  136. package/runtime/templates/reports/final-report.template.md +3 -3
  137. package/runtime/templates/reports/html/assets/base.css +8 -4
  138. package/runtime/templates/reports/html/base.template.html +12 -6
  139. package/runtime/templates/reports/html/i18n/en.json +32 -7
  140. package/runtime/templates/reports/html/i18n/ko.json +32 -7
  141. package/runtime/templates/reports/html/macros/forms.html +9 -3
  142. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +14 -19
  143. package/runtime/templates/reports/report.js +59 -26
  144. package/runtime/templates/reports/user-response.template.md +12 -8
  145. package/runtime/templates/worker-prompt-preamble.md +1 -1
  146. package/runtime/validators/validate-implementation-plan-stages.py +17 -22
  147. package/runtime/validators/validate-report-views.py +0 -39
  148. package/runtime/validators/validate-run.py +96 -14
  149. package/runtime/validators/validate_session_conformance.py +69 -3
  150. package/src/cli-registry.mjs +0 -7
  151. package/src/commands/execute/render-bundle.mjs +4 -4
  152. package/src/commands/execute/run.mjs +8 -25
  153. package/src/commands/execute/wizard.mjs +33 -13
  154. package/src/commands/lifecycle/doctor.mjs +10 -10
  155. package/src/commands/lifecycle/install.mjs +53 -30
  156. package/src/commands/lifecycle/preflight.mjs +14 -4
  157. package/src/lib/host-registry-client.mjs +176 -0
  158. package/src/lib/runtime-manifest.mjs +6 -8
  159. package/runtime/bin/okstra-wrapper-agy-stream.py +0 -61
  160. package/runtime/python/okstra_ctl/error_issue.py +0 -640
  161. package/runtime/python/okstra_ctl/issue_signals.py +0 -186
  162. package/runtime/python/okstra_ctl/lead_runtime.py +0 -115
  163. package/runtime/python/okstra_ctl/runner_resolution.py +0 -103
  164. package/runtime/skills/okstra-inspect/facets/error-issue.md +0 -77
  165. package/src/commands/inspect/error-issue.mjs +0 -27
  166. package/src/lib/runtime-readiness.mjs +0 -90
  167. 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