okstra 0.163.2 → 0.164.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/docs/architecture.md +12 -7
- package/docs/cli.md +8 -5
- package/docs/project-structure-overview.md +7 -5
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +1 -0
- package/runtime/bin/lib/okstra/cli.sh +5 -0
- package/runtime/bin/lib/okstra/globals.sh +2 -0
- package/runtime/bin/lib/okstra/usage.sh +5 -5
- package/runtime/bin/okstra.sh +87 -91
- package/runtime/prompts/lead/adapters/cmux.md +1 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +1 -0
- package/runtime/prompts/profiles/implementation-planning.md +1 -1
- package/runtime/python/okstra_ctl/adapters/accounting/__init__.py +11 -0
- package/runtime/python/okstra_ctl/adapters/accounting/claude_jsonl.py +17 -0
- package/runtime/python/okstra_ctl/adapters/accounting/cli_artifact.py +17 -0
- package/runtime/python/okstra_ctl/adapters/accounting/unavailable.py +19 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +92 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +54 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +68 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +60 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/antigravity.md → python/okstra_ctl/adapters/hosts/antigravity/relay.md} +52 -0
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +292 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +120 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/claude-code.md → python/okstra_ctl/adapters/hosts/claude-code/relay.md} +111 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +60 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/codex.md → python/okstra_ctl/adapters/hosts/codex/relay.md} +52 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +72 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/external.md → python/okstra_ctl/adapters/hosts/external/relay.md} +52 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +63 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +90 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +63 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +90 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +35 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +55 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +43 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +32 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +38 -0
- package/runtime/python/okstra_ctl/adapters/providers/kimi/manifest.json +1 -0
- package/runtime/python/okstra_ctl/application/__init__.py +1 -0
- package/runtime/python/okstra_ctl/application/advance_wizard.py +25 -0
- package/runtime/python/okstra_ctl/application/collect_usage.py +15 -0
- package/runtime/python/okstra_ctl/application/dispatch_assignments.py +15 -0
- package/runtime/python/okstra_ctl/application/resolve_assignment.py +93 -0
- package/runtime/python/okstra_ctl/application/resume_run.py +21 -0
- package/runtime/python/okstra_ctl/application/start_run.py +21 -0
- package/runtime/python/okstra_ctl/codex_dispatch.py +49 -826
- package/runtime/python/okstra_ctl/dispatch_core.py +240 -29
- package/runtime/python/okstra_ctl/dispatch_state.py +17 -0
- package/runtime/python/okstra_ctl/domain/__init__.py +34 -0
- package/runtime/python/okstra_ctl/domain/host.py +100 -0
- package/runtime/python/okstra_ctl/domain/provider.py +66 -0
- package/runtime/python/okstra_ctl/domain/wizard/__init__.py +19 -0
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +140 -0
- package/runtime/python/okstra_ctl/entrypoints/__init__.py +1 -0
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +334 -0
- package/runtime/python/okstra_ctl/models.py +54 -269
- package/runtime/python/okstra_ctl/ports/__init__.py +15 -0
- package/runtime/python/okstra_ctl/ports/host.py +32 -0
- package/runtime/python/okstra_ctl/ports/interaction.py +15 -0
- package/runtime/python/okstra_ctl/ports/lead_session.py +25 -0
- package/runtime/python/okstra_ctl/ports/usage_accounting.py +23 -0
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +32 -0
- package/runtime/python/okstra_ctl/registry/__init__.py +13 -0
- package/runtime/python/okstra_ctl/registry/factory_loader.py +32 -0
- package/runtime/python/okstra_ctl/registry/host_discovery.py +124 -0
- package/runtime/python/okstra_ctl/registry/host_registry.py +365 -0
- package/runtime/python/okstra_ctl/registry/provider_registry.py +149 -0
- package/runtime/python/okstra_ctl/render.py +145 -47
- package/runtime/python/okstra_ctl/run.py +80 -58
- package/runtime/python/okstra_ctl/session.py +1 -1
- package/runtime/python/okstra_ctl/team.py +44 -32
- package/runtime/python/okstra_ctl/wizard.py +162 -64
- package/runtime/python/okstra_ctl/worker_audit_ledger.py +29 -4
- package/runtime/python/okstra_token_usage/collect.py +34 -6
- package/runtime/schemas/final-report-v1.0.schema.json +3989 -1085
- package/runtime/schemas/final-report-v2.0.schema.json +5622 -1451
- package/runtime/skills/okstra-run/SKILL.md +73 -28
- package/runtime/templates/implementation-worker-preamble.md +1 -1
- package/runtime/templates/reports/final-report.template.md +3 -3
- package/runtime/templates/reports/html/i18n/en.json +3 -1
- package/runtime/templates/reports/html/i18n/ko.json +3 -1
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +1 -1
- package/runtime/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/validate-implementation-plan-stages.py +17 -22
- package/runtime/validators/validate-report-views.py +0 -39
- package/runtime/validators/validate-run.py +8 -7
- package/runtime/validators/validate_session_conformance.py +7 -2
- package/src/commands/execute/render-bundle.mjs +4 -4
- package/src/commands/execute/run.mjs +8 -25
- package/src/commands/execute/wizard.mjs +33 -13
- package/src/commands/lifecycle/doctor.mjs +10 -10
- package/src/commands/lifecycle/install.mjs +53 -30
- package/src/commands/lifecycle/preflight.mjs +14 -4
- package/src/lib/host-registry-client.mjs +176 -0
- package/src/lib/runtime-manifest.mjs +6 -8
- package/runtime/python/okstra_ctl/lead_runtime.py +0 -115
- package/runtime/python/okstra_ctl/runner_resolution.py +0 -103
- package/src/lib/runtime-readiness.mjs +0 -90
- package/src/lib/runtime-resolver.mjs +0 -123
|
@@ -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"}
|
package/runtime/{prompts/lead/adapters/codex.md → python/okstra_ctl/adapters/hosts/codex/relay.md}
RENAMED
|
@@ -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.
|