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,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.
@@ -0,0 +1,63 @@
1
+ """Bundled Kimi 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="kimi",
22
+ aliases=("kimi",),
23
+ native_provider_id="kimi",
24
+ required_executables=("kimi",),
25
+ launch_mode="lead",
26
+ install_targets=frozenset({"agents"}),
27
+ agent_id="kimi",
28
+ agent_label="Kimi CLI",
29
+ role="Kimi 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
+ "Kimi 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": "kimi", "factory": "adapter.py:create_adapter", "nativeProviderId": "kimi", "requiredExecutables": ["kimi"], "relayContract": "relay.md"}
@@ -0,0 +1,90 @@
1
+ # Kimi Lead Runtime Adapter
2
+
3
+ ## Scope
4
+
5
+ This adapter maps neutral Okstra lead operations to the Kimi CLI. Read it only when the rendered launch prompt selects `leadRuntime=kimi`.
6
+
7
+ ## Capability declaration
8
+
9
+ | Field | Value |
10
+ |---|---|
11
+ | `runtime` | `kimi` |
12
+ | `leadRoleLabel` | `Kimi 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": "kimi",
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 Kimi 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 Kimi 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 `kimi` 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 Kimi session ID when one exists; otherwise resume from Okstra run artifacts.
@@ -0,0 +1,183 @@
1
+ """Bundled Antigravity provider catalog."""
2
+ from typing import Any, Mapping
3
+
4
+ from okstra_ctl.domain.provider import LeadLaunchSpec, ModelSpec, ProviderSpec
5
+ from okstra_ctl.domain.worker_exec import (
6
+ STREAM_JSON,
7
+ ExecCommand,
8
+ PolicySupport,
9
+ WorkerExecRequest,
10
+ )
11
+ from okstra_ctl.domain.worker_stream import (
12
+ Result,
13
+ StreamEvent,
14
+ Text,
15
+ ToolCall,
16
+ ToolResult,
17
+ )
18
+
19
+
20
+ # `agy` needs a tier suffix at dispatch time; model_discovery owns that host
21
+ # specific normalization so catalog display values never reach the CLI.
22
+ ANTIGRAVITY = {
23
+ "gemini-3.1-pro": ModelSpec("Gemini 3.1 Pro", "gemini-3.1-pro", in_picker=True),
24
+ "gemini 3.1 pro": ModelSpec("Gemini 3.1 Pro", "gemini-3.1-pro"),
25
+ "gemini-3.6-flash": ModelSpec(
26
+ "Gemini 3.6 Flash", "gemini-3.6-flash", in_picker=True
27
+ ),
28
+ "gemini 3.6 flash": ModelSpec("Gemini 3.6 Flash", "gemini-3.6-flash"),
29
+ "gemini-3.5-flash": ModelSpec(
30
+ "Gemini 3.5 Flash", "gemini-3.5-flash", in_picker=True
31
+ ),
32
+ "gemini 3.5 flash": ModelSpec("Gemini 3.5 Flash", "gemini-3.5-flash"),
33
+ }
34
+
35
+
36
+ # The wall-clock cap on one print run, carried over from the wrapper. It is
37
+ # deliberately not derived from the idle budget: an analyser legitimately works
38
+ # for far longer than it goes quiet, so folding the idle budget in here would
39
+ # kill a healthy worker mid-run. Idle is the runner's watchdog to enforce.
40
+ _PRINT_TIMEOUT = "7200s"
41
+
42
+ _STEP_UPDATE = "step_update"
43
+ _RESULT = "result"
44
+ _CALL_STARTED = "ACTIVE"
45
+ _CALL_FINISHED = "DONE"
46
+
47
+
48
+ def step_update_events(event: Mapping[str, Any]) -> tuple[StreamEvent, ...]:
49
+ """Normalise this CLI's stream-json, which shares no key with the other four.
50
+
51
+ Measured 2026-08-11 against agy 1.1.11. Events are keyed on `event` rather
52
+ than `type`, and progress arrives as one `step_update` per step transition:
53
+
54
+ - `step_type: agent_response` carries the worker's prose in `text_delta`
55
+ (absent on the turns that only planned a tool call).
56
+ - `step_type: tool` arrives twice for one call — `ACTIVE` with the
57
+ arguments, then `DONE` with the tool's output.
58
+ - the closing `result` carries `response`, and an `error` string when
59
+ `status` is anything but SUCCESS. A failed run's `response` is empty, so
60
+ without the error text such a run would leave nothing behind at all.
61
+ """
62
+ kind = event.get("event")
63
+ if kind == _RESULT:
64
+ return _result_events(event.get(_RESULT))
65
+ if kind != _STEP_UPDATE:
66
+ return ()
67
+ step = event.get(_STEP_UPDATE)
68
+ if not isinstance(step, Mapping):
69
+ return ()
70
+ if step.get("step_type") == "agent_response":
71
+ text = step.get("text_delta")
72
+ return (Text(body=text),) if isinstance(text, str) and text.strip() else ()
73
+ if step.get("step_type") == "tool":
74
+ return _tool_events(step)
75
+ return ()
76
+
77
+
78
+ def _tool_events(step: Mapping[str, Any]) -> tuple[StreamEvent, ...]:
79
+ info = step.get("tool_info")
80
+ info = info if isinstance(info, Mapping) else {}
81
+ name = str(step.get("tool_name") or info.get("name") or "tool")
82
+ if step.get("state") == _CALL_STARTED:
83
+ return (ToolCall(name=name, detail=_argument(info.get("parameters"))),)
84
+ if step.get("state") != _CALL_FINISHED:
85
+ return ()
86
+ output = str(info.get("output", ""))
87
+ # No outcome field: a `run_command` whose command exited non-zero was
88
+ # measured closing as `DONE` with the failure text in `output` and nothing
89
+ # else to distinguish it, so the outcome is left unreported rather than
90
+ # rendered as success.
91
+ return (ToolResult(body=output, size_bytes=len(output.encode("utf-8"))),)
92
+
93
+
94
+ def _argument(parameters: Any) -> str:
95
+ """The one argument worth a row, without guessing at the parameter names.
96
+
97
+ Parameter keys belong to each tool, not to this CLI — `view_file` names its
98
+ path `AbsolutePath` while `run_command` names its command `CommandLine` —
99
+ so an allowlist of key names would cover only the tools that happened to be
100
+ observed. The first string argument is the subject of every tool measured.
101
+ """
102
+ if not isinstance(parameters, Mapping):
103
+ return ""
104
+ return next(
105
+ (str(value) for value in parameters.values() if isinstance(value, str) and value),
106
+ "",
107
+ )
108
+
109
+
110
+ def _result_events(result: Any) -> tuple[StreamEvent, ...]:
111
+ if not isinstance(result, Mapping):
112
+ return ()
113
+ error = result.get("error")
114
+ failure = (Text(body=error),) if isinstance(error, str) and error.strip() else ()
115
+ response = result.get("response")
116
+ if not isinstance(response, str):
117
+ return failure
118
+ return (*failure, Result(text=response))
119
+
120
+
121
+ class AntigravityExecution:
122
+ """agy CLI invocation.
123
+
124
+ `--add-dir` defines the workspace rather than widening a default one, so the
125
+ project root is included instead of skipped.
126
+
127
+ The prompt is an argument, not stdin: this CLI does not read stdin.
128
+
129
+ `--dangerously-skip-permissions` is required rather than convenient. Headless
130
+ `--print` has nobody to answer a permission request, so without it every
131
+ command tool is auto-denied and the run still reports SUCCESS with an empty
132
+ response.
133
+
134
+ No flag bounds what this worker may write. `--sandbox` was measured on
135
+ 2026-08-10 (agy 1.1.11) and did not block writes outside the `--add-dir`
136
+ workspace through either the shell tool or the file tool — identically to a
137
+ control run without it. It is therefore not passed, and `policy_support`
138
+ reports the gap rather than claiming a boundary that was not observed.
139
+ """
140
+
141
+ def build_command(self, request: WorkerExecRequest) -> ExecCommand:
142
+ argv = ["agy", "--print", request.prompt_text, "--model", request.model]
143
+ for directory in request.policy.write_scope:
144
+ argv += ["--add-dir", str(directory)]
145
+ argv += ["--output-format", "stream-json"]
146
+ argv += ["--print-timeout", _PRINT_TIMEOUT]
147
+ if request.policy.auto_approve:
148
+ argv.append("--dangerously-skip-permissions")
149
+ return ExecCommand(
150
+ argv=tuple(argv),
151
+ stdin_text=None,
152
+ stream_format=STREAM_JSON,
153
+ normalise=step_update_events,
154
+ cwd=request.project_root,
155
+ )
156
+
157
+ def policy_support(self) -> PolicySupport:
158
+ return PolicySupport(
159
+ can_auto_approve=True,
160
+ can_bound_write_scope=False,
161
+ note=(
162
+ "measured 2026-08-10 (agy 1.1.11): --sandbox did not block writes "
163
+ "outside the --add-dir workspace, so no flag bounds this provider"
164
+ ),
165
+ )
166
+
167
+
168
+ def create_provider() -> ProviderSpec:
169
+ return ProviderSpec(
170
+ provider="antigravity",
171
+ display_label="Antigravity",
172
+ models=ANTIGRAVITY,
173
+ default_models={
174
+ role: "gemini-3.1-pro"
175
+ for role in ("lead", "analyser", "critic", "executor", "verifier")
176
+ },
177
+ wrapper="okstra-antigravity-exec.sh",
178
+ supported_roles=frozenset({"lead", "analyser", "critic", "executor", "verifier"}),
179
+ lead_launch=LeadLaunchSpec(
180
+ executable="agy", model_flag="--model", prompt_flag="--prompt-interactive"
181
+ ),
182
+ exec_strategy=AntigravityExecution(),
183
+ )
@@ -0,0 +1 @@
1
+ {"schemaVersion": 1, "id": "antigravity", "factory": "adapter.py:create_provider"}
@@ -0,0 +1,110 @@
1
+ """Bundled Claude provider catalog."""
2
+ from okstra_ctl.domain.provider import LeadLaunchSpec, ModelSpec, ProviderSpec
3
+ from okstra_ctl.domain.worker_exec import (
4
+ STREAM_JSON,
5
+ ExecCommand,
6
+ PolicySupport,
7
+ WorkerExecRequest,
8
+ )
9
+ from okstra_ctl.domain.worker_stream import content_block_events
10
+
11
+
12
+ CLAUDE = {
13
+ "fable": ModelSpec("fable", "fable", in_picker=True),
14
+ "fable-5": ModelSpec("fable-5", "claude-fable-5"),
15
+ "claude-fable-5": ModelSpec("fable-5", "claude-fable-5"),
16
+ "opus": ModelSpec("opus", "opus", in_picker=True),
17
+ "opus-5": ModelSpec("opus-5", "claude-opus-5"),
18
+ "claude-opus-5": ModelSpec("opus-5", "claude-opus-5"),
19
+ "opus-4-8": ModelSpec("opus-4-8", "claude-opus-4-8"),
20
+ "claude-opus-4-8": ModelSpec("opus-4-8", "claude-opus-4-8"),
21
+ "opus-4-7": ModelSpec("opus-4-7", "claude-opus-4-7"),
22
+ "claude-opus-4-7": ModelSpec("opus-4-7", "claude-opus-4-7"),
23
+ "opus-4-6": ModelSpec("opus-4-6", "claude-opus-4-6"),
24
+ "claude-opus-4-6": ModelSpec("opus-4-6", "claude-opus-4-6"),
25
+ "sonnet": ModelSpec("sonnet", "sonnet", in_picker=True),
26
+ "sonnet-5": ModelSpec("sonnet-5", "claude-sonnet-5", in_picker=True),
27
+ "claude-sonnet-5": ModelSpec("sonnet-5", "claude-sonnet-5"),
28
+ "sonnet-4-6": ModelSpec("sonnet-4-6", "claude-sonnet-4-6"),
29
+ "claude-sonnet-4-6": ModelSpec("sonnet-4-6", "claude-sonnet-4-6"),
30
+ "haiku": ModelSpec("haiku", "haiku", in_picker=True),
31
+ "haiku-4-5": ModelSpec("haiku-4-5", "claude-haiku-4-5"),
32
+ "claude-haiku-4-5": ModelSpec("haiku-4-5", "claude-haiku-4-5"),
33
+ "claude-haiku-4-5-20251001": ModelSpec(
34
+ "haiku-4-5", "claude-haiku-4-5-20251001"
35
+ ),
36
+ }
37
+
38
+
39
+ # Opening the approval gate for a non-interactive run. Without it this CLI
40
+ # auto-denies any tool call outside the seeded allowlist and the worker silently
41
+ # routes around the denial, which reads as a thinner analysis rather than as a
42
+ # failure. The value is taken from the CLI's own help text; it has not been
43
+ # observed end to end, which is why policy_support claims no write boundary.
44
+ _APPROVAL_ARGS = ("--permission-mode", "bypassPermissions")
45
+
46
+
47
+ class ClaudeExecution:
48
+ """claude CLI invocation.
49
+
50
+ Working directory is the project root even when a stage worktree exists:
51
+ this CLI attaches the worktree with `--add-dir` and runs from the root,
52
+ unlike grok/kimi which run inside the worktree.
53
+ """
54
+
55
+ def build_command(self, request: WorkerExecRequest) -> ExecCommand:
56
+ argv = ["claude", "-p", "--model", request.model]
57
+ for directory in request.policy.write_scope:
58
+ if directory != request.project_root:
59
+ argv += ["--add-dir", str(directory)]
60
+ if request.policy.auto_approve:
61
+ argv += list(_APPROVAL_ARGS)
62
+ # `--verbose` is mandatory, not cosmetic: Claude Code rejects `--print`
63
+ # combined with `--output-format=stream-json` without it and exits 1 in
64
+ # under a second.
65
+ argv += ["--output-format=stream-json", "--verbose"]
66
+ return ExecCommand(
67
+ argv=tuple(argv),
68
+ stdin_text=request.prompt_text,
69
+ stream_format=STREAM_JSON,
70
+ normalise=content_block_events,
71
+ cwd=request.project_root,
72
+ )
73
+
74
+ def policy_support(self) -> PolicySupport:
75
+ return PolicySupport(
76
+ can_auto_approve=True,
77
+ can_bound_write_scope=False,
78
+ note=(
79
+ "the approval flag is taken from help text and has not been "
80
+ "observed end to end; no flag is known to bound this provider's "
81
+ "writes, so none is claimed"
82
+ ),
83
+ )
84
+
85
+
86
+ def create_provider() -> ProviderSpec:
87
+ return ProviderSpec(
88
+ provider="claude",
89
+ display_label="Claude",
90
+ models=CLAUDE,
91
+ default_models={
92
+ "lead": "opus",
93
+ "analyser": "opus",
94
+ "critic": "opus",
95
+ "executor": "opus",
96
+ "verifier": "opus",
97
+ "report-writer": "sonnet",
98
+ },
99
+ wrapper="okstra-claude-exec.sh",
100
+ supported_roles=frozenset(
101
+ {"lead", "analyser", "critic", "executor", "verifier", "report-writer"}
102
+ ),
103
+ lead_launch=LeadLaunchSpec(
104
+ executable="claude",
105
+ model_flag="--model",
106
+ start_session_id_flag="--session-id",
107
+ resume_session_id_flag="--resume",
108
+ ),
109
+ exec_strategy=ClaudeExecution(),
110
+ )
@@ -0,0 +1 @@
1
+ {"schemaVersion": 1, "id": "claude", "factory": "adapter.py:create_provider"}
@@ -0,0 +1,84 @@
1
+ """Bundled Codex provider catalog."""
2
+ from okstra_ctl.domain.provider import LeadLaunchSpec, ModelSpec, ProviderSpec
3
+ from okstra_ctl.domain.worker_exec import (
4
+ TEXT,
5
+ ExecCommand,
6
+ PolicySupport,
7
+ WorkerExecRequest,
8
+ )
9
+
10
+
11
+ CODEX = {
12
+ # ChatGPT-account hosts serve this model without per-token billing.
13
+ "gpt-5.6-sol": ModelSpec("gpt-5.6-sol", "gpt-5.6-sol", None, in_picker=True),
14
+ "gpt-5.6": ModelSpec("gpt-5.6", "gpt-5.6", (5.00, 0.50, 30.0), in_picker=True),
15
+ "gpt-5.5": ModelSpec("gpt-5.5", "gpt-5.5", (5.00, 0.50, 30.0), in_picker=True),
16
+ "gpt-5.4": ModelSpec("gpt-5.4", "gpt-5.4", (2.50, 0.25, 15.00), in_picker=True),
17
+ "gpt-5.4-mini": ModelSpec(
18
+ "gpt-5.4-mini", "gpt-5.4-mini", (0.75, 0.075, 4.50), in_picker=True
19
+ ),
20
+ "gpt-5.3-codex": ModelSpec("gpt-5.4", "gpt-5.4"),
21
+ "gpt-5.2": ModelSpec("gpt-5.2", "gpt-5.2", (1.75, 0.175, 14.0)),
22
+ "codex-auto-review": ModelSpec("codex-auto-review", "codex-auto-review"),
23
+ }
24
+
25
+
26
+ class CodexExecution:
27
+ """codex CLI invocation.
28
+
29
+ The sandbox stays `workspace-write` even when the approval gate is opened:
30
+ removing the gate is not the same as removing the boundary. A non-interactive
31
+ codex run under the default `on-request` policy blocks on the first approval
32
+ it wants, and with no TTY to answer it ends the turn with exit 0 and no
33
+ output — a success that produced nothing.
34
+
35
+ Working directory is the project root even when a stage worktree exists:
36
+ this CLI attaches the worktree with `--add-dir` and runs from the root,
37
+ unlike grok/kimi which run inside the worktree.
38
+ """
39
+
40
+ def build_command(self, request: WorkerExecRequest) -> ExecCommand:
41
+ argv = ["codex", "exec", "-C", str(request.project_root)]
42
+ for directory in request.policy.write_scope:
43
+ if directory != request.project_root:
44
+ argv += ["--add-dir", str(directory)]
45
+ argv += ["--model", request.model, "--sandbox", "workspace-write"]
46
+ if request.policy.auto_approve:
47
+ argv += ["-c", "approval_policy=never"]
48
+ argv.append("-")
49
+ return ExecCommand(
50
+ argv=tuple(argv),
51
+ stdin_text=request.prompt_text,
52
+ stream_format=TEXT,
53
+ cwd=request.project_root,
54
+ )
55
+
56
+ def policy_support(self) -> PolicySupport:
57
+ return PolicySupport(can_auto_approve=True, can_bound_write_scope=True)
58
+
59
+
60
+ def create_provider() -> ProviderSpec:
61
+ return ProviderSpec(
62
+ provider="codex",
63
+ display_label="Codex",
64
+ models=CODEX,
65
+ default_models={
66
+ role: "gpt-5.6-sol"
67
+ for role in ("lead", "analyser", "critic", "executor", "verifier", "report-writer")
68
+ },
69
+ wrapper="okstra-codex-exec.sh",
70
+ supported_roles=frozenset(
71
+ {"lead", "analyser", "critic", "executor", "verifier", "report-writer"}
72
+ ),
73
+ lead_launch=LeadLaunchSpec(
74
+ executable="codex",
75
+ model_flag="-m",
76
+ # A sandboxed lead cannot reach cmux or write its worker CLI config.
77
+ sandbox_waiver=("-s", "danger-full-access"),
78
+ sandbox_waiver_note=(
79
+ "codex will start without its filesystem and network sandbox, "
80
+ "which okstra needs so the lead can reach cmux and start worker CLIs."
81
+ ),
82
+ ),
83
+ exec_strategy=CodexExecution(),
84
+ )
@@ -0,0 +1 @@
1
+ {"schemaVersion": 1, "id": "codex", "factory": "adapter.py:create_provider"}