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.
Files changed (112) hide show
  1. package/README.md +7 -5
  2. package/docs/architecture.md +12 -7
  3. package/docs/cli.md +8 -5
  4. package/docs/project-structure-overview.md +7 -5
  5. package/package.json +1 -1
  6. package/runtime/BUILD.json +2 -2
  7. package/runtime/agents/workers/claude-worker.md +1 -0
  8. package/runtime/bin/lib/okstra/cli.sh +5 -0
  9. package/runtime/bin/lib/okstra/globals.sh +2 -0
  10. package/runtime/bin/lib/okstra/usage.sh +5 -5
  11. package/runtime/bin/okstra.sh +87 -91
  12. package/runtime/prompts/lead/adapters/cmux.md +1 -1
  13. package/runtime/prompts/lead/okstra-lead-contract.md +1 -0
  14. package/runtime/prompts/profiles/implementation-planning.md +1 -1
  15. package/runtime/python/okstra_ctl/adapters/accounting/__init__.py +11 -0
  16. package/runtime/python/okstra_ctl/adapters/accounting/claude_jsonl.py +17 -0
  17. package/runtime/python/okstra_ctl/adapters/accounting/cli_artifact.py +17 -0
  18. package/runtime/python/okstra_ctl/adapters/accounting/unavailable.py +19 -0
  19. package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +92 -0
  20. package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +54 -0
  21. package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +68 -0
  22. package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
  23. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +60 -0
  24. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/manifest.json +1 -0
  25. package/runtime/{prompts/lead/adapters/antigravity.md → python/okstra_ctl/adapters/hosts/antigravity/relay.md} +52 -0
  26. package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +292 -0
  27. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +120 -0
  28. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -0
  29. package/runtime/{prompts/lead/adapters/claude-code.md → python/okstra_ctl/adapters/hosts/claude-code/relay.md} +111 -0
  30. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +60 -0
  31. package/runtime/python/okstra_ctl/adapters/hosts/codex/manifest.json +1 -0
  32. package/runtime/{prompts/lead/adapters/codex.md → python/okstra_ctl/adapters/hosts/codex/relay.md} +52 -0
  33. package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +72 -0
  34. package/runtime/python/okstra_ctl/adapters/hosts/external/manifest.json +1 -0
  35. package/runtime/{prompts/lead/adapters/external.md → python/okstra_ctl/adapters/hosts/external/relay.md} +52 -0
  36. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +63 -0
  37. package/runtime/python/okstra_ctl/adapters/hosts/grok/manifest.json +1 -0
  38. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +90 -0
  39. package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +63 -0
  40. package/runtime/python/okstra_ctl/adapters/hosts/kimi/manifest.json +1 -0
  41. package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +90 -0
  42. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +35 -0
  43. package/runtime/python/okstra_ctl/adapters/providers/antigravity/manifest.json +1 -0
  44. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +55 -0
  45. package/runtime/python/okstra_ctl/adapters/providers/claude/manifest.json +1 -0
  46. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +43 -0
  47. package/runtime/python/okstra_ctl/adapters/providers/codex/manifest.json +1 -0
  48. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +32 -0
  49. package/runtime/python/okstra_ctl/adapters/providers/grok/manifest.json +1 -0
  50. package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +38 -0
  51. package/runtime/python/okstra_ctl/adapters/providers/kimi/manifest.json +1 -0
  52. package/runtime/python/okstra_ctl/application/__init__.py +1 -0
  53. package/runtime/python/okstra_ctl/application/advance_wizard.py +25 -0
  54. package/runtime/python/okstra_ctl/application/collect_usage.py +15 -0
  55. package/runtime/python/okstra_ctl/application/dispatch_assignments.py +15 -0
  56. package/runtime/python/okstra_ctl/application/resolve_assignment.py +93 -0
  57. package/runtime/python/okstra_ctl/application/resume_run.py +21 -0
  58. package/runtime/python/okstra_ctl/application/start_run.py +21 -0
  59. package/runtime/python/okstra_ctl/codex_dispatch.py +49 -826
  60. package/runtime/python/okstra_ctl/dispatch_core.py +240 -29
  61. package/runtime/python/okstra_ctl/dispatch_state.py +17 -0
  62. package/runtime/python/okstra_ctl/domain/__init__.py +34 -0
  63. package/runtime/python/okstra_ctl/domain/host.py +100 -0
  64. package/runtime/python/okstra_ctl/domain/provider.py +66 -0
  65. package/runtime/python/okstra_ctl/domain/wizard/__init__.py +19 -0
  66. package/runtime/python/okstra_ctl/domain/wizard/interaction.py +140 -0
  67. package/runtime/python/okstra_ctl/entrypoints/__init__.py +1 -0
  68. package/runtime/python/okstra_ctl/entrypoints/hosts.py +334 -0
  69. package/runtime/python/okstra_ctl/models.py +54 -269
  70. package/runtime/python/okstra_ctl/ports/__init__.py +15 -0
  71. package/runtime/python/okstra_ctl/ports/host.py +32 -0
  72. package/runtime/python/okstra_ctl/ports/interaction.py +15 -0
  73. package/runtime/python/okstra_ctl/ports/lead_session.py +25 -0
  74. package/runtime/python/okstra_ctl/ports/usage_accounting.py +23 -0
  75. package/runtime/python/okstra_ctl/ports/worker_dispatch.py +32 -0
  76. package/runtime/python/okstra_ctl/registry/__init__.py +13 -0
  77. package/runtime/python/okstra_ctl/registry/factory_loader.py +32 -0
  78. package/runtime/python/okstra_ctl/registry/host_discovery.py +124 -0
  79. package/runtime/python/okstra_ctl/registry/host_registry.py +365 -0
  80. package/runtime/python/okstra_ctl/registry/provider_registry.py +149 -0
  81. package/runtime/python/okstra_ctl/render.py +145 -47
  82. package/runtime/python/okstra_ctl/run.py +80 -58
  83. package/runtime/python/okstra_ctl/session.py +1 -1
  84. package/runtime/python/okstra_ctl/team.py +44 -32
  85. package/runtime/python/okstra_ctl/wizard.py +162 -64
  86. package/runtime/python/okstra_ctl/worker_audit_ledger.py +29 -4
  87. package/runtime/python/okstra_token_usage/collect.py +34 -6
  88. package/runtime/schemas/final-report-v1.0.schema.json +3989 -1085
  89. package/runtime/schemas/final-report-v2.0.schema.json +5622 -1451
  90. package/runtime/skills/okstra-run/SKILL.md +73 -28
  91. package/runtime/templates/implementation-worker-preamble.md +1 -1
  92. package/runtime/templates/reports/final-report.template.md +3 -3
  93. package/runtime/templates/reports/html/i18n/en.json +3 -1
  94. package/runtime/templates/reports/html/i18n/ko.json +3 -1
  95. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +1 -1
  96. package/runtime/templates/worker-prompt-preamble.md +1 -1
  97. package/runtime/validators/validate-implementation-plan-stages.py +17 -22
  98. package/runtime/validators/validate-report-views.py +0 -39
  99. package/runtime/validators/validate-run.py +8 -7
  100. package/runtime/validators/validate_session_conformance.py +7 -2
  101. package/src/commands/execute/render-bundle.mjs +4 -4
  102. package/src/commands/execute/run.mjs +8 -25
  103. package/src/commands/execute/wizard.mjs +33 -13
  104. package/src/commands/lifecycle/doctor.mjs +10 -10
  105. package/src/commands/lifecycle/install.mjs +53 -30
  106. package/src/commands/lifecycle/preflight.mjs +14 -4
  107. package/src/lib/host-registry-client.mjs +176 -0
  108. package/src/lib/runtime-manifest.mjs +6 -8
  109. package/runtime/python/okstra_ctl/lead_runtime.py +0 -115
  110. package/runtime/python/okstra_ctl/runner_resolution.py +0 -103
  111. package/src/lib/runtime-readiness.mjs +0 -90
  112. package/src/lib/runtime-resolver.mjs +0 -123
@@ -0,0 +1,120 @@
1
+ """Bundled Claude Code host strategy."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import shutil
7
+ from collections.abc import Callable
8
+ from pathlib import Path
9
+
10
+ from okstra_ctl.adapters.accounting import ClaudeJsonlUsageAccountingPort
11
+ from okstra_ctl.adapters.hosts.capability_adapter import (
12
+ INTERACTION_FUNCTIONS,
13
+ PENDING_HOST_PORT,
14
+ CapabilityHostAdapter,
15
+ numbered_interaction_port,
16
+ )
17
+ from okstra_ctl.domain.host import HostClaim, HostDescriptor, HostResolutionContext
18
+ from okstra_ctl.registry.provider_registry import ProviderRegistry
19
+
20
+
21
+ DESCRIPTOR = HostDescriptor(
22
+ id="claude-code",
23
+ aliases=("claude",),
24
+ native_provider_id="claude",
25
+ required_executables=("claude",),
26
+ launch_mode="lead",
27
+ install_targets=frozenset({"agents", "claude"}),
28
+ agent_id="claude",
29
+ agent_label="Claude Code",
30
+ role="Claude lead",
31
+ dispatch_mode="team",
32
+ session_accounting="claude-jsonl",
33
+ has_claude_session=True,
34
+ relay_contract=str(Path(__file__).with_name("relay.md").resolve()),
35
+ )
36
+
37
+
38
+ def _detect_claude_skill_handoff(
39
+ context: HostResolutionContext,
40
+ ) -> HostClaim | None:
41
+ if "claude-skill-handoff" not in context.signals:
42
+ return None
43
+ return HostClaim(
44
+ DESCRIPTOR.id,
45
+ 100,
46
+ "Claude Code skill handoff selected runtime 'claude-code'.",
47
+ )
48
+
49
+
50
+ def _workspace_trust_checks(context) -> tuple[dict[str, object], ...]:
51
+ project_root = Path(
52
+ os.path.abspath(
53
+ os.environ.get("OKSTRA_PROBE_PROJECT_ROOT", str(Path.cwd()))
54
+ )
55
+ )
56
+ home_dir = Path(os.environ.get("OKSTRA_PROBE_HOME_DIR", str(Path.home())))
57
+ if not (project_root / ".claude" / "settings.local.json").exists():
58
+ return ({"id": "workspace-trust", "status": "not-applicable"},)
59
+ try:
60
+ state = json.loads((home_dir / ".claude.json").read_text(encoding="utf-8"))
61
+ projects = state.get("projects") if isinstance(state, dict) else None
62
+ if not isinstance(projects, dict):
63
+ return _workspace_trust_unavailable()
64
+ project_key = str(project_root)
65
+ if project_key not in projects:
66
+ return _workspace_trust_required()
67
+ project_state = projects[project_key]
68
+ if not isinstance(project_state, dict):
69
+ return _workspace_trust_unavailable()
70
+ accepted = project_state.get("hasTrustDialogAccepted")
71
+ if accepted is True:
72
+ return ({"id": "workspace-trust", "status": "accepted"},)
73
+ if accepted is False or accepted is None:
74
+ return _workspace_trust_required()
75
+ return _workspace_trust_unavailable()
76
+ except (OSError, json.JSONDecodeError):
77
+ return _workspace_trust_unavailable()
78
+
79
+
80
+ def _workspace_trust_required() -> tuple[dict[str, object], ...]:
81
+ return ({
82
+ "id": "workspace-trust",
83
+ "status": "required",
84
+ "action": "restart-and-trust",
85
+ },)
86
+
87
+
88
+ def _workspace_trust_unavailable() -> tuple[dict[str, object], ...]:
89
+ return ({
90
+ "id": "workspace-trust",
91
+ "status": "unavailable",
92
+ "action": "open-project-and-trust",
93
+ },)
94
+
95
+
96
+ def create_adapter(
97
+ *,
98
+ executable_finder: Callable[[str], str | None] = shutil.which,
99
+ interaction_port=None,
100
+ lead_session_port=PENDING_HOST_PORT,
101
+ worker_dispatch_port=PENDING_HOST_PORT,
102
+ usage_accounting_port=ClaudeJsonlUsageAccountingPort(),
103
+ provider_registry: ProviderRegistry | None = None,
104
+ ) -> CapabilityHostAdapter:
105
+ return CapabilityHostAdapter(
106
+ DESCRIPTOR,
107
+ executable_finder=executable_finder,
108
+ interaction_port=(
109
+ interaction_port
110
+ if interaction_port is not None
111
+ else numbered_interaction_port()
112
+ ),
113
+ lead_session_port=lead_session_port,
114
+ worker_dispatch_port=worker_dispatch_port,
115
+ usage_accounting_port=usage_accounting_port,
116
+ supported_functions=INTERACTION_FUNCTIONS,
117
+ detector=_detect_claude_skill_handoff,
118
+ provider_registry=provider_registry,
119
+ readiness_probe=_workspace_trust_checks,
120
+ )
@@ -0,0 +1 @@
1
+ {"schemaVersion": 1, "id": "claude-code", "factory": "adapter.py:create_adapter", "nativeProviderId": "claude", "requiredExecutables": ["claude"], "relayContract": "relay.md"}
@@ -18,6 +18,117 @@ This adapter maps the neutral Okstra lead operations to Claude Code host primiti
18
18
  | `teardownMode` | `teammate-shutdown` |
19
19
  | `leadEventSource` | `claude-jsonl+artifact` |
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. `semanticFunctions` is an allowlist: the skill may declare a function only when both this list and the live harness expose it.
24
+
25
+ ```json
26
+ {
27
+ "schemaVersion": 1,
28
+ "runtime": "claude-code",
29
+ "semanticFunctions": [
30
+ "plain_text_input",
31
+ "native_single_select",
32
+ "native_multi_select",
33
+ "native_question_group"
34
+ ],
35
+ "interactions": {
36
+ "native-single": {
37
+ "function": "AskUserQuestion",
38
+ "input": {
39
+ "callCount": 1,
40
+ "questions": "one",
41
+ "question": "label-with-progress",
42
+ "header": "Q1",
43
+ "options": "all-in-original-order-as-label-description",
44
+ "multiSelect": false
45
+ },
46
+ "response": {
47
+ "envelope": "answers",
48
+ "key": "rendered-question-text",
49
+ "selection": "selected-label",
50
+ "submit": "matching-option-value"
51
+ }
52
+ },
53
+ "native-multi": {
54
+ "function": "AskUserQuestion",
55
+ "input": {
56
+ "callCount": 1,
57
+ "questions": "one",
58
+ "question": "label-with-progress",
59
+ "header": "Q1",
60
+ "options": "all-in-original-order-as-label-description",
61
+ "multiSelect": true
62
+ },
63
+ "response": {
64
+ "envelope": "answers",
65
+ "key": "rendered-question-text",
66
+ "selection": "selected-labels-joined-comma-space",
67
+ "submit": "csv-matching-option-values-in-option-order"
68
+ }
69
+ },
70
+ "native-group": {
71
+ "function": "AskUserQuestion",
72
+ "input": {
73
+ "callCount": 1,
74
+ "questions": "all-in-original-order",
75
+ "question": "label-with-progress",
76
+ "header": "Q<one-based-question-position>",
77
+ "options": "all-in-original-order-as-label-description",
78
+ "multiSelect": "questions[].multi"
79
+ },
80
+ "response": {
81
+ "envelope": "answers",
82
+ "key": "rendered-question-text",
83
+ "selection": "selected-label-or-labels-joined-comma-space",
84
+ "multiValue": "csv-matching-option-values-in-option-order",
85
+ "submit": "compact-step-json-values"
86
+ }
87
+ },
88
+ "numbered-single": {
89
+ "function": "host-text",
90
+ "input": {
91
+ "questions": "one",
92
+ "question": "label-with-progress",
93
+ "options": "all-in-original-order-as-numbered-markdown-label-and-description"
94
+ },
95
+ "response": { "source": "next-message", "submit": "raw" }
96
+ },
97
+ "numbered-multi": {
98
+ "function": "host-text",
99
+ "input": {
100
+ "questions": "one",
101
+ "question": "label-with-progress",
102
+ "options": "all-in-original-order-as-numbered-markdown-label-and-description"
103
+ },
104
+ "response": { "source": "next-message", "submit": "raw" }
105
+ },
106
+ "sequential-group": {
107
+ "function": "host-text",
108
+ "input": {
109
+ "questions": "all-one-at-a-time-in-original-order",
110
+ "question": "label-with-progress",
111
+ "options": "all-in-original-order-as-numbered-markdown-label-and-description"
112
+ },
113
+ "response": {
114
+ "source": "next-message-by-question-position",
115
+ "submit": "compact-step-json-raw"
116
+ }
117
+ },
118
+ "plain-text": {
119
+ "function": "host-text",
120
+ "input": { "questions": "one", "question": "label-with-progress" },
121
+ "response": { "source": "next-message", "submit": "raw" }
122
+ }
123
+ }
124
+ }
125
+ ```
126
+
127
+ For `AskUserQuestion`, map each wizard option to the tool's `{label, description}` input without removing, shortening, or reordering entries. Look up each answer by the exact rendered `question` string sent to the tool, including the progress suffix; the `answers` keys are question text, not positions or headers. A single-select answer is the selected label. A multi-select answer is one string whose selected labels are joined with `, `. Match those labels back to the complete original option list, emit their `value` fields in original option order, and join the values with `,` for the wizard. Native plans are emitted only for one to four questions whose option labels are unique and whose complete option lists each contain two to four entries; other prompts use the text mapping so no option is dropped. For a group, key the compact JSON object by the corresponding `questions[].step`; a multi-select question stores its ordered value CSV as one string, not a JSON array. Never use the display header as an answer key. The `Q1`…`Q4` headers only satisfy the host tool's short-header input and carry no domain meaning.
128
+
129
+ For a `host-text` mapping, render each numbered item as its option label followed by its description verbatim; preserve every item and its order. The next user message is the raw answer. Do not translate numbers, CSV members, labels, or values before `okstra wizard step`. A sequential group wraps each raw reply in one compact JSON object keyed by `questions[].step`; the wizard owns normalization.
130
+
131
+
21
132
  ## Semantic operation mapping
22
133
 
23
134
  | Operation | Mapping |
@@ -0,0 +1,60 @@
1
+ """Bundled Codex 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="codex",
22
+ aliases=("codex",),
23
+ native_provider_id="codex",
24
+ required_executables=("codex",),
25
+ launch_mode="lead",
26
+ install_targets=frozenset({"agents"}),
27
+ agent_id="codex",
28
+ agent_label="Codex CLI",
29
+ role="Codex 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": "codex", "factory": "adapter.py:create_adapter", "nativeProviderId": "codex", "requiredExecutables": ["codex"], "relayContract": "relay.md"}
@@ -18,6 +18,58 @@ This adapter maps the neutral Okstra lead operations to the Codex artifact-first
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": "codex",
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,72 @@
1
+ """Bundled external 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 UnavailableUsageAccountingPort
9
+ from okstra_ctl.adapters.hosts.capability_adapter import (
10
+ INTERACTION_FUNCTIONS,
11
+ PENDING_HOST_PORT,
12
+ CapabilityHostAdapter,
13
+ numbered_interaction_port,
14
+ )
15
+ from okstra_ctl.domain.host import HostClaim, HostDescriptor, HostResolutionContext
16
+ from okstra_ctl.registry.provider_registry import ProviderRegistry
17
+
18
+
19
+ DESCRIPTOR = HostDescriptor(
20
+ id="external",
21
+ aliases=("external",),
22
+ native_provider_id="",
23
+ required_executables=(),
24
+ launch_mode="team",
25
+ install_targets=frozenset({"agents"}),
26
+ agent_id="external",
27
+ agent_label="External harness",
28
+ role="Okstra lead",
29
+ dispatch_mode="render-only",
30
+ session_accounting="artifact-only",
31
+ has_claude_session=False,
32
+ relay_contract=str(Path(__file__).with_name("relay.md").resolve()),
33
+ )
34
+
35
+
36
+ def _detect_tmux(context: HostResolutionContext) -> HostClaim | None:
37
+ if "tmux" not in context.signals:
38
+ return None
39
+ return HostClaim(
40
+ DESCRIPTOR.id,
41
+ 10,
42
+ "No Claude Code host signal; tmux is available.",
43
+ )
44
+
45
+
46
+ def create_adapter(
47
+ *,
48
+ executable_finder: Callable[[str], str | None] = shutil.which,
49
+ interaction_port=None,
50
+ lead_session_port=PENDING_HOST_PORT,
51
+ worker_dispatch_port=PENDING_HOST_PORT,
52
+ usage_accounting_port=UnavailableUsageAccountingPort(
53
+ "External host usage accounting is unavailable because no session "
54
+ "transcript or CLI usage artifact contract is registered."
55
+ ),
56
+ provider_registry: ProviderRegistry | None = None,
57
+ ) -> CapabilityHostAdapter:
58
+ return CapabilityHostAdapter(
59
+ DESCRIPTOR,
60
+ executable_finder=executable_finder,
61
+ interaction_port=(
62
+ interaction_port
63
+ if interaction_port is not None
64
+ else numbered_interaction_port()
65
+ ),
66
+ lead_session_port=lead_session_port,
67
+ worker_dispatch_port=worker_dispatch_port,
68
+ usage_accounting_port=usage_accounting_port,
69
+ supported_functions=INTERACTION_FUNCTIONS,
70
+ detector=_detect_tmux,
71
+ provider_registry=provider_registry,
72
+ )
@@ -0,0 +1 @@
1
+ {"schemaVersion": 1, "id": "external", "factory": "adapter.py:create_adapter", "nativeProviderId": "", "requiredExecutables": [], "relayContract": "relay.md"}
@@ -18,6 +18,58 @@ This adapter maps the neutral Okstra lead operations to a generic host using Oks
18
18
  | `teardownMode` | `pane-teardown` |
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": "external",
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,63 @@
1
+ """Bundled Grok 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 UnavailableUsageAccountingPort
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="grok",
22
+ aliases=("grok",),
23
+ native_provider_id="grok",
24
+ required_executables=("grok",),
25
+ launch_mode="lead",
26
+ install_targets=frozenset({"agents"}),
27
+ agent_id="grok",
28
+ agent_label="Grok CLI",
29
+ role="Grok 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=UnavailableUsageAccountingPort(
44
+ "Grok host usage accounting is unavailable because no session "
45
+ "transcript or CLI usage artifact contract is registered."
46
+ ),
47
+ provider_registry: ProviderRegistry | None = None,
48
+ ) -> CapabilityHostAdapter:
49
+ return CapabilityHostAdapter(
50
+ DESCRIPTOR,
51
+ executable_finder=executable_finder,
52
+ interaction_port=(
53
+ interaction_port
54
+ if interaction_port is not None
55
+ else numbered_interaction_port()
56
+ ),
57
+ lead_session_port=lead_session_port,
58
+ worker_dispatch_port=worker_dispatch_port,
59
+ usage_accounting_port=usage_accounting_port,
60
+ supported_functions=INTERACTION_FUNCTIONS,
61
+ detector=no_automatic_claim,
62
+ provider_registry=provider_registry,
63
+ )
@@ -0,0 +1 @@
1
+ {"schemaVersion": 1, "id": "grok", "factory": "adapter.py:create_adapter", "nativeProviderId": "grok", "requiredExecutables": ["grok"], "relayContract": "relay.md"}
@@ -0,0 +1,90 @@
1
+ # Grok Lead Runtime Adapter
2
+
3
+ ## Scope
4
+
5
+ This adapter maps neutral Okstra lead operations to the Grok CLI. Read it only when the rendered launch prompt selects `leadRuntime=grok`.
6
+
7
+ ## Capability declaration
8
+
9
+ | Field | Value |
10
+ |---|---|
11
+ | `runtime` | `grok` |
12
+ | `leadRoleLabel` | `Grok lead` |
13
+ | `userPromptMode` | `host-text` |
14
+ | `workerDispatchBackend` | `mixed` |
15
+ | `initialPromptDeliveryMode` | `eager-include` |
16
+ | `sessionAccounting` | `artifact-only` |
17
+ | `resumeMode` | `native-session-id` |
18
+ | `teardownMode` | `process-cleanup` |
19
+ | `leadEventSource` | `lead-events-jsonl` |
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": "grok",
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. Submit the next user message unchanged. For `sequential-group`, collect one raw reply per question in order and build one compact JSON object keyed by the corresponding `questions[].step`.
71
+
72
+ ## Semantic operation mapping
73
+
74
+ | Operation | Mapping |
75
+ |---|---|
76
+ | `read_artifacts` | Read the manifest-provided paths through the current Grok host file interface. |
77
+ | `write_artifact` | Write only core-authorized `.okstra/` artifacts and preserve their schemas. |
78
+ | `prompt_user` | Ask through the current host text interface and wait for an explicit answer. |
79
+ | `dispatch_worker` | Follow each persisted assignment's `runner` and use the common host dispatch boundary. |
80
+ | `await_workers` | Await through the selected common dispatch backend, then verify terminal state and Result Paths. |
81
+ | `redispatch_worker` | Start a fresh attempt from the persisted assignment and record the supplied dispatch kind. |
82
+ | `shutdown_workers` | Clean up only host or process resources owned by this run. |
83
+ | `record_lead_event` | Append the required structured event to `leadEventsPath` and emit the matching progress line. |
84
+ | `collect_usage` | Return explicit unavailable lead usage until Grok registers a session transcript or CLI usage artifact contract. |
85
+
86
+ ## Completion, cleanup, and resume
87
+
88
+ - Do not infer the current host from an installed `grok` executable. The runtime must come from an explicit request or current-session declaration.
89
+ - Keep persisted provider, model, runner, and dispatch-kind assignments unchanged.
90
+ - Resume with the persisted Grok session ID when one exists; otherwise resume from Okstra run artifacts.