okstra 0.180.0 → 0.183.2

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 (223) hide show
  1. package/dist/cli-registry.mjs +16 -2
  2. package/dist/cli-registry.mjs.map +1 -1
  3. package/dist/commands/execute/render-bundle.d.mts +4 -2
  4. package/dist/commands/execute/render-bundle.mjs +46 -5
  5. package/dist/commands/execute/render-bundle.mjs.map +1 -1
  6. package/dist/commands/execute/run.mjs +11 -3
  7. package/dist/commands/execute/run.mjs.map +1 -1
  8. package/dist/commands/inspect/model-io.d.mts +1 -0
  9. package/dist/commands/inspect/model-io.mjs +25 -0
  10. package/dist/commands/inspect/model-io.mjs.map +1 -0
  11. package/dist/commands/inspect/stage-map.mjs +29 -8
  12. package/dist/commands/inspect/stage-map.mjs.map +1 -1
  13. package/dist/commands/inspect/task-list.mjs +52 -6
  14. package/dist/commands/inspect/task-list.mjs.map +1 -1
  15. package/dist/commands/inspect/user-response.mjs +14 -4
  16. package/dist/commands/inspect/user-response.mjs.map +1 -1
  17. package/dist/commands/lifecycle/check-project.d.mts +1 -0
  18. package/dist/commands/lifecycle/check-project.mjs +69 -50
  19. package/dist/commands/lifecycle/check-project.mjs.map +1 -1
  20. package/dist/commands/lifecycle/contract-check.d.mts +1 -0
  21. package/dist/commands/lifecycle/contract-check.mjs +18 -0
  22. package/dist/commands/lifecycle/contract-check.mjs.map +1 -0
  23. package/dist/commands/lifecycle/preflight.mjs +154 -51
  24. package/dist/commands/lifecycle/preflight.mjs.map +1 -1
  25. package/dist/commands/pr/pr.d.mts +1 -0
  26. package/dist/commands/pr/pr.mjs +19 -1
  27. package/dist/commands/pr/pr.mjs.map +1 -1
  28. package/dist/commands/report/agent-activity.mjs +2 -2
  29. package/dist/commands/report/translate.mjs +3 -0
  30. package/dist/commands/report/translate.mjs.map +1 -1
  31. package/dist/lib/host-registry-client.mjs +13 -9
  32. package/dist/lib/host-registry-client.mjs.map +1 -1
  33. package/docs/architecture.md +11 -0
  34. package/docs/cli.md +26 -13
  35. package/docs/container.md +6 -4
  36. package/docs/contributor-change-matrix.md +1 -1
  37. package/docs/for-ai/README.md +2 -2
  38. package/docs/for-ai/skills/okstra-brief-gen.md +5 -3
  39. package/docs/for-ai/skills/okstra-code-review.md +4 -4
  40. package/docs/for-ai/skills/okstra-container-build.md +20 -17
  41. package/docs/for-ai/skills/okstra-inspect.md +20 -23
  42. package/docs/for-ai/skills/okstra-manager.md +19 -18
  43. package/docs/for-ai/skills/okstra-memory.md +2 -2
  44. package/docs/for-ai/skills/okstra-pr-gen.md +3 -3
  45. package/docs/for-ai/skills/okstra-rollup.md +14 -13
  46. package/docs/for-ai/skills/okstra-run.md +7 -3
  47. package/docs/for-ai/skills/okstra-schedule-gen.md +15 -18
  48. package/docs/for-ai/skills/okstra-setup.md +7 -7
  49. package/docs/for-ai/skills/okstra-usage.md +5 -4
  50. package/docs/for-ai/skills/okstra-user-response.md +50 -32
  51. package/docs/project-structure-overview.md +29 -26
  52. package/docs/task-process/implementation.md +7 -4
  53. package/package.json +1 -1
  54. package/runtime/BUILD.json +2 -2
  55. package/runtime/agents/workers/claude-worker.md +4 -4
  56. package/runtime/agents/workers/report-writer-worker.md +3 -3
  57. package/runtime/agents/workers/translator-worker.md +5 -13
  58. package/runtime/bin/okstra-error-log.py +51 -11
  59. package/runtime/bin/okstra-report-translate.py +210 -23
  60. package/runtime/prompts/host-orchestration/implementation.md +1 -1
  61. package/runtime/prompts/launch.template.md +3 -7
  62. package/runtime/prompts/lead/context-loader.md +41 -141
  63. package/runtime/prompts/lead/convergence.md +8 -6
  64. package/runtime/prompts/lead/okstra-lead-contract.md +25 -35
  65. package/runtime/prompts/lead/plan-body-verification.md +187 -14
  66. package/runtime/prompts/lead/report-writer.md +20 -1
  67. package/runtime/prompts/lead/team-contract.md +8 -53
  68. package/runtime/prompts/profiles/_coding-conventions-preflight.md +3 -2
  69. package/runtime/prompts/profiles/_common-contract.md +1 -1
  70. package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
  71. package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
  72. package/runtime/prompts/profiles/final-verification.md +1 -1
  73. package/runtime/prompts/profiles/implementation-planning.md +8 -2
  74. package/runtime/prompts/profiles/release-handoff.md +0 -1
  75. package/runtime/prompts/wizard/prompts.ko.json +0 -1
  76. package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +69 -17
  77. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +13 -4
  78. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +6 -1
  79. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
  80. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +50 -5
  81. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
  82. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +66 -5
  83. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +70 -2
  84. package/runtime/python/okstra_ctl/agent_activity.py +118 -35
  85. package/runtime/python/okstra_ctl/agent_invocation.py +19 -6
  86. package/runtime/python/okstra_ctl/agent_prompt_cli.py +65 -18
  87. package/runtime/python/okstra_ctl/analysis_inputs.py +5 -4
  88. package/runtime/python/okstra_ctl/analysis_packet.py +81 -1
  89. package/runtime/python/okstra_ctl/approval_decisions.py +3 -2
  90. package/runtime/python/okstra_ctl/attempt_evidence.py +2 -2
  91. package/runtime/python/okstra_ctl/backfill.py +13 -10
  92. package/runtime/python/okstra_ctl/batch.py +2 -4
  93. package/runtime/python/okstra_ctl/build_tools.py +6 -3
  94. package/runtime/python/okstra_ctl/claim_reproduction.py +101 -0
  95. package/runtime/python/okstra_ctl/clarification_items.py +27 -13
  96. package/runtime/python/okstra_ctl/cmux.py +130 -52
  97. package/runtime/python/okstra_ctl/code_review_target.py +34 -8
  98. package/runtime/python/okstra_ctl/consumers.py +5 -4
  99. package/runtime/python/okstra_ctl/container.py +103 -8
  100. package/runtime/python/okstra_ctl/context_cost.py +2 -1
  101. package/runtime/python/okstra_ctl/contract_graph.py +497 -0
  102. package/runtime/python/okstra_ctl/contract_graph_cli.py +62 -0
  103. package/runtime/python/okstra_ctl/convergence.py +338 -17
  104. package/runtime/python/okstra_ctl/convergence_engine.py +10 -18
  105. package/runtime/python/okstra_ctl/convergence_provenance.py +58 -8
  106. package/runtime/python/okstra_ctl/convergence_store.py +55 -34
  107. package/runtime/python/okstra_ctl/design_prep.py +7 -4
  108. package/runtime/python/okstra_ctl/dispatch_core.py +35 -65
  109. package/runtime/python/okstra_ctl/dispatch_state.py +134 -59
  110. package/runtime/python/okstra_ctl/doctor.py +6 -3
  111. package/runtime/python/okstra_ctl/domain/worker_presentation.py +70 -9
  112. package/runtime/python/okstra_ctl/entrypoints/hosts.py +16 -30
  113. package/runtime/python/okstra_ctl/error_log_write.py +35 -30
  114. package/runtime/python/okstra_ctl/error_report.py +26 -1
  115. package/runtime/python/okstra_ctl/error_zip.py +27 -5
  116. package/runtime/python/okstra_ctl/execution_identity.py +3 -2
  117. package/runtime/python/okstra_ctl/execution_manifest.py +7 -4
  118. package/runtime/python/okstra_ctl/final_report_schema.py +2 -2
  119. package/runtime/python/okstra_ctl/fix_cycles.py +2 -2
  120. package/runtime/python/okstra_ctl/fixed_text.py +39 -0
  121. package/runtime/python/okstra_ctl/git_reconcile.py +41 -9
  122. package/runtime/python/okstra_ctl/handoff.py +5 -4
  123. package/runtime/python/okstra_ctl/i18n.py +4 -2
  124. package/runtime/python/okstra_ctl/implementation_direction.py +22 -14
  125. package/runtime/python/okstra_ctl/implementation_outcome.py +4 -7
  126. package/runtime/python/okstra_ctl/incremental_carry.py +2 -1
  127. package/runtime/python/okstra_ctl/incremental_scope.py +8 -3
  128. package/runtime/python/okstra_ctl/index.py +8 -11
  129. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +79 -7
  130. package/runtime/python/okstra_ctl/invocation.py +3 -6
  131. package/runtime/python/okstra_ctl/json_boundary.py +366 -0
  132. package/runtime/python/okstra_ctl/json_registry.py +10 -12
  133. package/runtime/python/okstra_ctl/jsonl.py +19 -2
  134. package/runtime/python/okstra_ctl/lead_events.py +33 -1
  135. package/runtime/python/okstra_ctl/listing.py +3 -3
  136. package/runtime/python/okstra_ctl/log_report.py +24 -2
  137. package/runtime/python/okstra_ctl/manager_cli.py +92 -7
  138. package/runtime/python/okstra_ctl/manager_store.py +12 -10
  139. package/runtime/python/okstra_ctl/material.py +5 -1
  140. package/runtime/python/okstra_ctl/migrate.py +29 -25
  141. package/runtime/python/okstra_ctl/model_cli.py +3 -15
  142. package/runtime/python/okstra_ctl/model_io_cli.py +1051 -0
  143. package/runtime/python/okstra_ctl/mutation_probe.py +13 -4
  144. package/runtime/python/okstra_ctl/pane_reclaim.py +3 -2
  145. package/runtime/python/okstra_ctl/paths.py +9 -0
  146. package/runtime/python/okstra_ctl/plan_items.py +115 -4
  147. package/runtime/python/okstra_ctl/plan_items_cli.py +513 -18
  148. package/runtime/python/okstra_ctl/pr_template.py +3 -2
  149. package/runtime/python/okstra_ctl/project_meta.py +5 -7
  150. package/runtime/python/okstra_ctl/recap.py +5 -4
  151. package/runtime/python/okstra_ctl/reconcile.py +21 -27
  152. package/runtime/python/okstra_ctl/registry/host_discovery.py +3 -2
  153. package/runtime/python/okstra_ctl/registry/provider_registry.py +3 -2
  154. package/runtime/python/okstra_ctl/render.py +26 -15
  155. package/runtime/python/okstra_ctl/render_final_report.py +3 -2
  156. package/runtime/python/okstra_ctl/report_assembly.py +172 -17
  157. package/runtime/python/okstra_ctl/report_finalize.py +7 -10
  158. package/runtime/python/okstra_ctl/report_html/render.py +3 -2
  159. package/runtime/python/okstra_ctl/report_language.py +3 -2
  160. package/runtime/python/okstra_ctl/report_markdown.py +13 -1
  161. package/runtime/python/okstra_ctl/report_narrative.py +40 -8
  162. package/runtime/python/okstra_ctl/report_synthesis_packet.py +518 -0
  163. package/runtime/python/okstra_ctl/report_views.py +3 -2
  164. package/runtime/python/okstra_ctl/rollup.py +65 -4
  165. package/runtime/python/okstra_ctl/run.py +159 -56
  166. package/runtime/python/okstra_ctl/run_audit.py +3 -2
  167. package/runtime/python/okstra_ctl/run_context.py +6 -9
  168. package/runtime/python/okstra_ctl/run_index_row.py +2 -8
  169. package/runtime/python/okstra_ctl/schedule_semantics.py +5 -2
  170. package/runtime/python/okstra_ctl/schema_excerpt.py +4 -2
  171. package/runtime/python/okstra_ctl/session_transcript.py +27 -1
  172. package/runtime/python/okstra_ctl/set_work_status.py +64 -38
  173. package/runtime/python/okstra_ctl/stage_fix_carry.py +4 -2
  174. package/runtime/python/okstra_ctl/stage_map.py +26 -6
  175. package/runtime/python/okstra_ctl/stage_targets.py +3 -4
  176. package/runtime/python/okstra_ctl/team.py +2 -1
  177. package/runtime/python/okstra_ctl/team_reconcile.py +11 -2
  178. package/runtime/python/okstra_ctl/time_report.py +51 -4
  179. package/runtime/python/okstra_ctl/usage_identity.py +2 -1
  180. package/runtime/python/okstra_ctl/usage_report.py +58 -4
  181. package/runtime/python/okstra_ctl/user_response.py +1431 -66
  182. package/runtime/python/okstra_ctl/wizard.py +11 -44
  183. package/runtime/python/okstra_ctl/work_categories.py +3 -2
  184. package/runtime/python/okstra_ctl/worker_prompt_body.py +18 -7
  185. package/runtime/python/okstra_ctl/worker_prompt_contract.py +3 -2
  186. package/runtime/python/okstra_ctl/worker_runner.py +14 -12
  187. package/runtime/python/okstra_ctl/workflow.py +2 -1
  188. package/runtime/python/okstra_ctl/worktree.py +3 -2
  189. package/runtime/python/okstra_ctl/wrapper_status.py +4 -2
  190. package/runtime/python/okstra_ctl/write_policy.py +4 -2
  191. package/runtime/python/okstra_token_usage/antigravity.py +39 -12
  192. package/runtime/python/okstra_token_usage/collect.py +90 -38
  193. package/runtime/python/okstra_token_usage/grok.py +127 -0
  194. package/runtime/schemas/final-report-v2.0.schema.json +9 -0
  195. package/runtime/schemas/final-report-v3.0.schema.json +9 -0
  196. package/runtime/schemas/report-synthesis-packet-v1.0.schema.json +140 -0
  197. package/runtime/skills/okstra-brief-gen/SKILL.md +9 -7
  198. package/runtime/skills/okstra-code-review/SKILL.md +21 -11
  199. package/runtime/skills/okstra-container-build/SKILL.md +18 -18
  200. package/runtime/skills/okstra-inspect/SKILL.md +12 -11
  201. package/runtime/skills/okstra-inspect/facets/error-zip.md +8 -8
  202. package/runtime/skills/okstra-inspect/facets/errors.md +2 -2
  203. package/runtime/skills/okstra-inspect/facets/history.md +9 -14
  204. package/runtime/skills/okstra-inspect/facets/logs.md +2 -2
  205. package/runtime/skills/okstra-inspect/facets/recap.md +5 -5
  206. package/runtime/skills/okstra-inspect/facets/report.md +6 -10
  207. package/runtime/skills/okstra-inspect/facets/status.md +9 -8
  208. package/runtime/skills/okstra-inspect/facets/time.md +3 -3
  209. package/runtime/skills/okstra-manager/SKILL.md +16 -14
  210. package/runtime/skills/okstra-memory/SKILL.md +3 -3
  211. package/runtime/skills/okstra-pr-gen/SKILL.md +5 -4
  212. package/runtime/skills/okstra-rollup/SKILL.md +6 -16
  213. package/runtime/skills/okstra-run/SKILL.md +7 -7
  214. package/runtime/skills/okstra-schedule-gen/SKILL.md +21 -17
  215. package/runtime/skills/okstra-setup/SKILL.md +21 -13
  216. package/runtime/skills/okstra-setup/references/project-config.md +2 -2
  217. package/runtime/skills/okstra-usage/SKILL.md +10 -10
  218. package/runtime/skills/okstra-user-response/SKILL.md +78 -107
  219. package/runtime/templates/report-writer-prompt-preamble.md +17 -1
  220. package/runtime/templates/reports/schedule.template.md +4 -4
  221. package/runtime/templates/worker-error-contract.md +17 -29
  222. package/runtime/validators/validate-run.py +379 -63
  223. package/runtime/validators/validate_session_conformance.py +43 -7
@@ -10,7 +10,7 @@ This adapter maps the neutral Okstra lead operations to the Codex artifact-first
10
10
  |---|---|
11
11
  | `runtime` | `codex` |
12
12
  | `leadRoleLabel` | `Codex lead` |
13
- | `userPromptMode` | `host-text` |
13
+ | `userPromptMode` | `native-question` |
14
14
  | `workerDispatchBackend` | `mixed` |
15
15
  | `initialPromptDeliveryMode` | `eager-include` |
16
16
  | `sessionAccounting` | `artifact-only` |
@@ -20,14 +20,57 @@ This adapter maps the neutral Okstra lead operations to the Codex artifact-first
20
20
 
21
21
  ## Wizard interaction relay
22
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.
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. Codex `request_user_input` is single-select only (2–3 mutually exclusive choices, 1–3 questions); it has no native multi-select.
24
24
 
25
25
  ```json
26
26
  {
27
27
  "schemaVersion": 1,
28
28
  "runtime": "codex",
29
- "semanticFunctions": ["plain_text_input"],
29
+ "semanticFunctions": [
30
+ "plain_text_input",
31
+ "native_single_select",
32
+ "native_question_group"
33
+ ],
34
+ "nativeLimits": {
35
+ "minOptions": 2,
36
+ "maxOptions": 3,
37
+ "maxQuestions": 3
38
+ },
30
39
  "interactions": {
40
+ "native-single": {
41
+ "function": "request_user_input",
42
+ "input": {
43
+ "callCount": 1,
44
+ "questions": "one",
45
+ "id": "prompt.step",
46
+ "header": "Q1",
47
+ "question": "label-with-progress",
48
+ "options": "all-in-original-order-as-label-description"
49
+ },
50
+ "response": {
51
+ "envelope": "answers",
52
+ "key": "question-id",
53
+ "selection": "answers-array",
54
+ "submit": "matching-option-value"
55
+ }
56
+ },
57
+ "native-group": {
58
+ "function": "request_user_input",
59
+ "input": {
60
+ "callCount": 1,
61
+ "questions": "all-in-original-order",
62
+ "id": "questions[].step",
63
+ "header": "Q<one-based-question-position>",
64
+ "question": "label-with-progress",
65
+ "options": "all-in-original-order-as-label-description"
66
+ },
67
+ "response": {
68
+ "envelope": "answers",
69
+ "key": "question-id",
70
+ "selection": "answers-array",
71
+ "submit": "compact-step-json-values"
72
+ }
73
+ },
31
74
  "numbered-single": {
32
75
  "function": "host-text",
33
76
  "input": {
@@ -67,7 +110,9 @@ Read this contract when `okstra preflight` returns this file as `runtimeReadines
67
110
  }
68
111
  ```
69
112
 
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.
113
+ For `request_user_input`, send one to three questions. Each question carries `id` (`prompt.step` or `questions[].step`), a header of at most 12 characters (`Q1`…`Q3`), the rendered question text including the progress suffix, and every wizard option as `{label, description}` in original order the tool has no option `value` field. Do not include an `Other` option; the client adds a free-form row. Native plans are emitted only when the prompt fits `nativeLimits` (unique labels, two or three options, one to three questions) and no grouped question is multi-select. Other prompts use the text mapping so no option is dropped. Look up each answer by question `id`. The selected strings are in `answers[id].answers`. Match those strings to option labels, emit their `value` fields in original option order, and join with `,` when more than one is present. If the typed Other text matches no label, submit it unchanged.
114
+
115
+ 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 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
116
 
72
117
 
73
118
  ## Semantic operation mapping
@@ -76,7 +121,7 @@ Render every numbered item as its option label followed by its description verba
76
121
  |---|---|
77
122
  | `read_artifacts` | Read the manifest-provided paths through the current host's file interface. |
78
123
  | `write_artifact` | Write only core-authorized `.okstra/` artifacts and preserve their schemas. |
79
- | `prompt_user` | Ask through the host text/question interface and stop at approval gates until an explicit answer arrives. |
124
+ | `prompt_user` | Use `request_user_input` for approvals and clarifications that fit two or three options; otherwise ask through host text and stop until an explicit answer arrives. |
80
125
  | `dispatch_worker` | Verify each materialized invocation first. Dispatch `runner=native-session` with the current Codex host's primitive, the returned `promptPath`, and `hostModelValue`. Pass `runner=cli-wrapper` assignments to `okstra worker-dispatch --project-root <root> --run-manifest <path> --workers <ids>`; use `--dry-run` first when required. **Not in a cmux run:** when `terminalBackend` is `cmux-pane`, the cmux adapter overrides this row. |
81
126
  | `await_workers` | Await native host workers through the host primitive and CLI workers through synchronous dispatch, then verify team-state terminal records and Result Paths for both. |
82
127
  | `redispatch_worker` | Materialize and verify a fresh invocation, then start a fresh native worker or `okstra worker-dispatch` attempt according to the persisted runner. |
@@ -11,7 +11,7 @@ from okstra_ctl.adapters.hosts.capability_adapter import (
11
11
  PENDING_HOST_PORT,
12
12
  CapabilityHostAdapter,
13
13
  no_automatic_claim,
14
- numbered_interaction_port,
14
+ relay_interaction_port,
15
15
  )
16
16
  from okstra_ctl.domain.host import HostDescriptor
17
17
  from okstra_ctl.ports.host_model import NativeExecutionValueHostModelBindingPort
@@ -54,7 +54,7 @@ def create_adapter(
54
54
  interaction_port=(
55
55
  interaction_port
56
56
  if interaction_port is not None
57
- else numbered_interaction_port()
57
+ else relay_interaction_port(DESCRIPTOR.relay_contract)
58
58
  ),
59
59
  lead_session_port=lead_session_port,
60
60
  worker_dispatch_port=worker_dispatch_port,
@@ -10,7 +10,7 @@ This adapter maps neutral Okstra lead operations to the Grok CLI. Read it only w
10
10
  |---|---|
11
11
  | `runtime` | `grok` |
12
12
  | `leadRoleLabel` | `Grok lead` |
13
- | `userPromptMode` | `host-text` |
13
+ | `userPromptMode` | `native-question` |
14
14
  | `workerDispatchBackend` | `mixed` |
15
15
  | `initialPromptDeliveryMode` | `eager-include` |
16
16
  | `sessionAccounting` | `artifact-only` |
@@ -20,14 +20,73 @@ This adapter maps neutral Okstra lead operations to the Grok CLI. Read it only w
20
20
 
21
21
  ## Wizard interaction relay
22
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.
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
24
 
25
25
  ```json
26
26
  {
27
27
  "schemaVersion": 1,
28
28
  "runtime": "grok",
29
- "semanticFunctions": ["plain_text_input"],
29
+ "semanticFunctions": [
30
+ "plain_text_input",
31
+ "native_single_select",
32
+ "native_multi_select",
33
+ "native_question_group"
34
+ ],
35
+ "nativeLimits": {
36
+ "minOptions": 2,
37
+ "maxOptions": 15,
38
+ "maxQuestions": 8
39
+ },
30
40
  "interactions": {
41
+ "native-single": {
42
+ "function": "ask_user_question",
43
+ "input": {
44
+ "callCount": 1,
45
+ "questions": "one",
46
+ "question": "label-with-progress",
47
+ "options": "all-in-original-order-as-label-description",
48
+ "multiSelect": false
49
+ },
50
+ "response": {
51
+ "envelope": "answers",
52
+ "key": "rendered-question-text",
53
+ "selection": "selected-label",
54
+ "submit": "matching-option-value"
55
+ }
56
+ },
57
+ "native-multi": {
58
+ "function": "ask_user_question",
59
+ "input": {
60
+ "callCount": 1,
61
+ "questions": "one",
62
+ "question": "label-with-progress",
63
+ "options": "all-in-original-order-as-label-description",
64
+ "multiSelect": true
65
+ },
66
+ "response": {
67
+ "envelope": "answers",
68
+ "key": "rendered-question-text",
69
+ "selection": "selected-labels-joined-comma-space",
70
+ "submit": "csv-matching-option-values-in-option-order"
71
+ }
72
+ },
73
+ "native-group": {
74
+ "function": "ask_user_question",
75
+ "input": {
76
+ "callCount": 1,
77
+ "questions": "all-in-original-order",
78
+ "question": "label-with-progress",
79
+ "options": "all-in-original-order-as-label-description",
80
+ "multiSelect": "questions[].multi"
81
+ },
82
+ "response": {
83
+ "envelope": "answers",
84
+ "key": "rendered-question-text",
85
+ "selection": "selected-label-or-labels-joined-comma-space",
86
+ "multiValue": "csv-matching-option-values-in-option-order",
87
+ "submit": "compact-step-json-values"
88
+ }
89
+ },
31
90
  "numbered-single": {
32
91
  "function": "host-text",
33
92
  "input": {
@@ -67,7 +126,9 @@ Read this contract when `okstra preflight` returns this file as `runtimeReadines
67
126
  }
68
127
  ```
69
128
 
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`.
129
+ For `ask_user_question`, map each wizard option to the tool's `{label, description}` input without removing, shortening, or reordering entries. Do not send `preview`. Look up each answer by the exact rendered `question` string sent to the tool, including the progress suffix. 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 when the prompt fits `nativeLimits` (unique option labels, two to fifteen options, one to eight questions); other prompts use the text mapping so no option is dropped. The host adds a free-text row on every question. Do not drop a wizard option to make room for it. If the user types in that row and the text matches no option label, submit the typed text unchanged. 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.
130
+
131
+ 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.
71
132
 
72
133
  ## Semantic operation mapping
73
134
 
@@ -75,7 +136,7 @@ Render every numbered item as its option label followed by its description verba
75
136
  |---|---|
76
137
  | `read_artifacts` | Read the manifest-provided paths through the current Grok host file interface. |
77
138
  | `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. |
139
+ | `prompt_user` | Use `ask_user_question` for approvals and clarifications; do not infer an answer from silence. |
79
140
  | `dispatch_worker` | Verify the materialized invocation. Use the host primitive with `promptPath` and `hostModelValue` for `native-session`; use deterministic `okstra worker-dispatch` with `modelExecutionValue` for `cli-wrapper`. **Not in a cmux run:** the cmux adapter overrides this row. |
80
141
  | `await_workers` | Await through the selected common dispatch backend, then verify terminal state and Result Paths. |
81
142
  | `redispatch_worker` | Start a fresh attempt from the persisted assignment and record the supplied dispatch kind. |
@@ -16,7 +16,8 @@ from okstra_ctl.domain.worker_exec import (
16
16
  PolicySupport,
17
17
  WorkerExecRequest,
18
18
  )
19
- from okstra_ctl.domain.worker_presentation import MergedText
19
+ from okstra_ctl.domain.worker_presentation import JsonEvents
20
+ from okstra_ctl.domain.worker_stream import StreamEvent, Text, ToolCall, ToolResult
20
21
 
21
22
 
22
23
  GROK = {
@@ -108,6 +109,65 @@ def observe_served_model(event: Mapping[str, Any]) -> str | None:
108
109
  return names[0]
109
110
  return None
110
111
 
112
+
113
+ def stream_events(event: Mapping[str, Any]) -> tuple[StreamEvent, ...]:
114
+ """grok `--output-format streaming-json` 한 줄을 공통 이벤트로 옮긴다.
115
+
116
+ 이 CLI 의 `-p` 기본 `plain` 은 종료 시에만 stdout 에 쓴다. 파이프에 붙은
117
+ 워커는 그 동안 pane 과 로그가 0바이트다(실측 2026-08-22). 같은 호출에
118
+ `streaming-json` 을 주면 ACP 세션 업데이트가 NDJSON 으로 흐른다. 키는
119
+ `type=text|tool_call|tool_call_update|end` 이고 Claude 의
120
+ `message.content` 가 아니다 — 그 스키마로 읽으면 이벤트는 전부 버려진다.
121
+ """
122
+ kind = event.get("type")
123
+ if kind == "text":
124
+ data = event.get("data")
125
+ # 공백만 있는 토큰도 살린다. 버리면 붙인 문장에서 단어가 붙는다.
126
+ return (Text(body=data),) if isinstance(data, str) and data else ()
127
+ if kind == "tool_call":
128
+ name = str(event.get("toolName") or event.get("title") or "tool")
129
+ return (ToolCall(name=name, detail=_call_detail(event)),)
130
+ if kind == "tool_call_update":
131
+ return _completed_tool(event)
132
+ if kind == "error":
133
+ message = event.get("message")
134
+ return (Text(body=message),) if isinstance(message, str) and message.strip() else ()
135
+ return ()
136
+
137
+
138
+ def _call_detail(event: Mapping[str, Any]) -> str:
139
+ payload = event.get("rawInput")
140
+ if isinstance(payload, Mapping):
141
+ for key in ("command", "file_path", "path", "pattern", "query"):
142
+ value = payload.get(key)
143
+ if value:
144
+ return str(value)
145
+ return next(
146
+ (str(value) for value in payload.values() if isinstance(value, str) and value),
147
+ "",
148
+ )
149
+ title = event.get("title")
150
+ return str(title) if isinstance(title, str) else ""
151
+
152
+
153
+ def _completed_tool(event: Mapping[str, Any]) -> tuple[StreamEvent, ...]:
154
+ if event.get("status") != "completed":
155
+ return ()
156
+ body = _output_body(event.get("rawOutput"))
157
+ return (ToolResult(body=body, size_bytes=len(body.encode("utf-8"))),)
158
+
159
+
160
+ def _output_body(output: Any) -> str:
161
+ if isinstance(output, str):
162
+ return output
163
+ if isinstance(output, Mapping):
164
+ text = next(
165
+ (str(value) for value in output.values() if isinstance(value, str) and value),
166
+ "",
167
+ )
168
+ return text or json.dumps(output, ensure_ascii=False)
169
+ return "" if output is None else str(output)
170
+
111
171
  GROK_LEAD_LAUNCH = LeadLaunchSpec(
112
172
  executable="grok",
113
173
  model_flag="--model",
@@ -125,6 +185,8 @@ class GrokExecution:
125
185
 
126
186
  def build_command(self, request: WorkerExecRequest) -> ExecCommand:
127
187
  cwd = request.worktree_path or request.project_root
188
+ # `-p` 기본 `plain` 은 종료 시에만 stdout 에 쓴다. 러너는 파이프라
189
+ # TUI 도 열리지 않고, `--no-alt-screen` 도 그 침묵을 바꾸지 않는다.
128
190
  return ExecCommand(
129
191
  argv=(
130
192
  "grok",
@@ -134,10 +196,16 @@ class GrokExecution:
134
196
  request.model,
135
197
  "--cwd",
136
198
  str(cwd),
199
+ "--output-format",
200
+ "streaming-json",
137
201
  ),
138
202
  stdin_text=None,
139
203
  cwd=cwd,
140
- presentation=MergedText(served_model_at_exit=served_model_from_session),
204
+ presentation=JsonEvents(
205
+ normalise=stream_events,
206
+ observe=observe_served_model,
207
+ join_adjacent_text=True,
208
+ ),
141
209
  )
142
210
 
143
211
  def policy_support(self) -> PolicySupport:
@@ -3,10 +3,8 @@ from __future__ import annotations
3
3
 
4
4
  import argparse
5
5
  import json
6
- import os
7
6
  import re
8
7
  import sys
9
- import tempfile
10
8
  from collections.abc import Mapping, Sequence
11
9
  from datetime import datetime, timezone
12
10
  from pathlib import Path
@@ -22,6 +20,11 @@ from okstra_ctl.worker_audit_ledger import (
22
20
  parse_evidence_commands,
23
21
  read_evidence_commands,
24
22
  )
23
+ from okstra_ctl.json_boundary import (
24
+ JsonBoundaryError,
25
+ load_owned_object,
26
+ mutate_owned_object_atomic,
27
+ )
25
28
 
26
29
 
27
30
  ACTIVITY_KINDS = frozenset({
@@ -59,12 +62,9 @@ class ActivityProjectionError(ValueError):
59
62
 
60
63
  def _read_json_object(path: Path) -> dict[str, Any]:
61
64
  try:
62
- payload = json.loads(path.read_text(encoding="utf-8"))
63
- except (OSError, json.JSONDecodeError) as exc:
64
- raise ActivityProjectionError(f"cannot read JSON object {path}: {exc}") from exc
65
- if not isinstance(payload, dict):
66
- raise ActivityProjectionError(f"JSON value must be an object: {path}")
67
- return payload
65
+ return load_owned_object(path, artifact="agent activity artifact")
66
+ except JsonBoundaryError as exc:
67
+ raise ActivityProjectionError(str(exc)) from exc
68
68
 
69
69
 
70
70
  def _resolve_project_path(project_root: Path, value: str | Path) -> Path:
@@ -88,6 +88,50 @@ def _manifest_run_seq(manifest: Mapping[str, Any]) -> str:
88
88
  return _require_string(sequences, "manifests")
89
89
 
90
90
 
91
+ def _validate_activity_ownership(
92
+ project_root: Path,
93
+ manifest: Mapping[str, Any],
94
+ details: Mapping[str, Any],
95
+ ) -> None:
96
+ executions = manifest.get("roleExecutions")
97
+ assignments = manifest.get("workerAssignments")
98
+ if isinstance(assignments, list):
99
+ assigned = {
100
+ row["workerId"] for row in assignments
101
+ if isinstance(row, Mapping) and isinstance(row.get("workerId"), str)
102
+ }
103
+ elif isinstance(executions, list):
104
+ assigned = {
105
+ f"{row['provider']}-worker"
106
+ for row in executions
107
+ if isinstance(row, Mapping) and isinstance(row.get("provider"), str)
108
+ }
109
+ else:
110
+ assigned = set()
111
+ if isinstance(assignments, list) or isinstance(executions, list):
112
+ assigned.update({"okstra-lead", f"{manifest.get('leadRuntime', '')}-lead"})
113
+ agent = _require_string(details, "agent")
114
+ if agent not in assigned:
115
+ raise ActivityProjectionError(
116
+ f"agent `{agent}` is not assigned to this run"
117
+ )
118
+ state_value = manifest.get("planBodyVerificationPath")
119
+ if not isinstance(state_value, str) or not details.get("planItemIds"):
120
+ return
121
+ state = _read_json_object(_resolve_project_path(project_root, state_value))
122
+ verification = state.get("planBodyVerification")
123
+ items = verification.get("planItems") if isinstance(verification, Mapping) else []
124
+ known = {
125
+ row.get("id") for row in items
126
+ if isinstance(row, Mapping) and isinstance(row.get("id"), str)
127
+ }
128
+ unknown = sorted(set(details["planItemIds"]) - known)
129
+ if unknown:
130
+ raise ActivityProjectionError(
131
+ "plan item(s) not in the current plan state: " + ", ".join(unknown)
132
+ )
133
+
134
+
91
135
  def _utc_now() -> str:
92
136
  return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
93
137
 
@@ -139,27 +183,6 @@ def _validate_activity_order(rows: Sequence[Mapping[str, Any]]) -> None:
139
183
  )
140
184
 
141
185
 
142
- def _atomic_write_json(path: Path, payload: Mapping[str, Any]) -> None:
143
- path.parent.mkdir(parents=True, exist_ok=True)
144
- temporary_path: Path | None = None
145
- try:
146
- with tempfile.NamedTemporaryFile(
147
- mode="w",
148
- encoding="utf-8",
149
- dir=path.parent,
150
- prefix=f".{path.name}.",
151
- suffix=".tmp",
152
- delete=False,
153
- ) as handle:
154
- temporary_path = Path(handle.name)
155
- json.dump(payload, handle, ensure_ascii=False, indent=2)
156
- handle.write("\n")
157
- os.replace(temporary_path, path)
158
- finally:
159
- if temporary_path is not None and temporary_path.exists():
160
- temporary_path.unlink()
161
-
162
-
163
186
  def record_activity(
164
187
  project_root: Path,
165
188
  run_manifest_path: Path,
@@ -172,6 +195,7 @@ def record_activity(
172
195
  raise ActivityProjectionError(
173
196
  "run manifest does not declare activityContractVersion 1"
174
197
  )
198
+ _validate_activity_ownership(project_root, manifest, details)
175
199
  event = LeadEvent(
176
200
  event_type="activity",
177
201
  lead_runtime=_require_string(manifest, "leadRuntime"),
@@ -184,7 +208,10 @@ def record_activity(
184
208
  events_path = _resolve_project_path(
185
209
  project_root, _require_string(manifest, "leadEventsPath")
186
210
  )
187
- return append_activity_event(events_path, event)
211
+ try:
212
+ return append_activity_event(events_path, event)
213
+ except ValueError as exc:
214
+ raise ActivityProjectionError(str(exc)) from exc
188
215
 
189
216
 
190
217
  def agent_activity_rows(
@@ -224,9 +251,14 @@ def project_agent_activity(
224
251
  manifest = _read_json_object(run_manifest_path)
225
252
  if manifest.get("activityContractVersion") != 1:
226
253
  return ()
227
- data = _read_json_object(data_path)
228
- data["agentActivity"] = list(rows)
229
- _atomic_write_json(data_path, data)
254
+ try:
255
+ mutate_owned_object_atomic(
256
+ data_path,
257
+ lambda data: {**data, "agentActivity": list(rows)},
258
+ artifact="final report record",
259
+ )
260
+ except JsonBoundaryError as exc:
261
+ raise ActivityProjectionError(str(exc)) from exc
230
262
  return rows
231
263
 
232
264
 
@@ -242,6 +274,9 @@ def _parse_command_records(raw_records: Sequence[str]) -> tuple[EvidenceCommand,
242
274
 
243
275
  def _activity_commands(args: argparse.Namespace) -> tuple[EvidenceCommand, ...]:
244
276
  commands = list(_parse_command_records(args.command_record))
277
+ typed = _typed_command(args)
278
+ if typed is not None:
279
+ commands.append(typed)
245
280
  if args.audit_sidecar is not None:
246
281
  audit_commands, failures = read_evidence_commands(args.audit_sidecar)
247
282
  if failures:
@@ -250,6 +285,45 @@ def _activity_commands(args: argparse.Namespace) -> tuple[EvidenceCommand, ...]:
250
285
  return tuple(commands)
251
286
 
252
287
 
288
+ def _typed_command(args: argparse.Namespace) -> EvidenceCommand | None:
289
+ values = (
290
+ args.evidence_command,
291
+ args.command_cwd,
292
+ args.command_exit_code,
293
+ args.command_output_file,
294
+ )
295
+ if not any(value is not None for value in values):
296
+ return None
297
+ if any(value is None for value in values):
298
+ raise ActivityProjectionError(
299
+ "--command requires --command-cwd, --command-exit-code, and "
300
+ "--command-output-file"
301
+ )
302
+ try:
303
+ output_summary = args.command_output_file.read_text(encoding="utf-8")
304
+ except (OSError, UnicodeError) as exc:
305
+ raise ActivityProjectionError(
306
+ f"cannot read command output {args.command_output_file}: {exc}"
307
+ ) from exc
308
+ return EvidenceCommand(
309
+ command=args.evidence_command,
310
+ cwd=args.command_cwd,
311
+ exit_code=args.command_exit_code,
312
+ output_summary=output_summary,
313
+ )
314
+
315
+
316
+ def _activity_summary(args: argparse.Namespace) -> str:
317
+ if args.summary is not None:
318
+ return args.summary
319
+ try:
320
+ return args.summary_file.read_text(encoding="utf-8").strip()
321
+ except (OSError, UnicodeError) as exc:
322
+ raise ActivityProjectionError(
323
+ f"cannot read activity summary {args.summary_file}: {exc}"
324
+ ) from exc
325
+
326
+
253
327
  def _conversation_activity_line(details: Mapping[str, Any]) -> str:
254
328
  plan_items = ",".join(details["planItemIds"]) or "<none>"
255
329
  result_path = str(details["resultPath"] or "<none>")
@@ -266,13 +340,15 @@ def _append(args: argparse.Namespace) -> int:
266
340
  details = {
267
341
  "kind": args.kind,
268
342
  "agent": args.agent,
269
- "summary": args.summary,
343
+ "summary": _activity_summary(args),
270
344
  "planItemIds": args.plan_item_id,
271
345
  "resultPath": args.result_path,
272
346
  "commands": [command.to_record() for command in commands],
273
347
  "evidenceRefs": args.evidence_ref,
274
348
  "outcome": args.outcome,
275
349
  }
350
+ if args.request_ref is not None:
351
+ details["activityRequestRef"] = args.request_ref
276
352
  event = record_activity(args.project_root, args.run_manifest, details)
277
353
  payload = dict(event.details)
278
354
  payload["ok"] = True
@@ -301,15 +377,22 @@ def _parser() -> argparse.ArgumentParser:
301
377
  "--kind", choices=sorted(ACTIVITY_KINDS), required=True
302
378
  )
303
379
  append_parser.add_argument("--agent", required=True)
304
- append_parser.add_argument("--summary", required=True)
380
+ summary = append_parser.add_mutually_exclusive_group(required=True)
381
+ summary.add_argument("--summary")
382
+ summary.add_argument("--summary-file", type=Path)
305
383
  append_parser.add_argument(
306
384
  "--outcome", choices=sorted(ACTIVITY_OUTCOMES), required=True
307
385
  )
308
386
  append_parser.add_argument("--plan-item-id", action="append", default=[])
309
387
  append_parser.add_argument("--evidence-ref", action="append", default=[])
310
388
  append_parser.add_argument("--command-record", action="append", default=[])
389
+ append_parser.add_argument("--command", dest="evidence_command")
390
+ append_parser.add_argument("--command-cwd")
391
+ append_parser.add_argument("--command-exit-code", type=int)
392
+ append_parser.add_argument("--command-output-file", type=Path)
311
393
  append_parser.add_argument("--result-path", default="")
312
394
  append_parser.add_argument("--audit-sidecar", type=Path)
395
+ append_parser.add_argument("--request-ref")
313
396
  project_parser = subparsers.add_parser("project")
314
397
  project_parser.add_argument("--project-root", type=Path, required=True)
315
398
  project_parser.add_argument("--run-manifest", type=Path, required=True)
@@ -15,6 +15,12 @@ import tempfile
15
15
  from typing import Iterator, Literal, Mapping, get_args
16
16
 
17
17
  from .domain.role import RoleCatalogError, role_for_duty
18
+ from .json_boundary import (
19
+ JsonBoundaryError,
20
+ external_invocation_json_source,
21
+ load_external_json,
22
+ load_owned_object,
23
+ )
18
24
 
19
25
 
20
26
  AgentAudience = Literal[
@@ -935,8 +941,15 @@ def _verified_result_envelope(
935
941
  ) -> tuple[bytes, str]:
936
942
  try:
937
943
  body = result_path.read_bytes()
938
- envelope = json.loads(body.decode("utf-8"))
939
- except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
944
+ envelope = load_external_json(
945
+ external_invocation_json_source(
946
+ result_path,
947
+ project_root=project_root,
948
+ purpose=purpose,
949
+ ),
950
+ artifact="standalone worker result",
951
+ )
952
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError, JsonBoundaryError) as exc:
940
953
  raise AgentInvocationError(
941
954
  f"standalone result envelope is missing or invalid: {result_path}"
942
955
  ) from exc
@@ -1494,8 +1507,8 @@ def _metadata_difference(
1494
1507
  so they are reported by name too.
1495
1508
  """
1496
1509
  try:
1497
- existing = json.loads(metadata.read_text(encoding="utf-8"))
1498
- except (OSError, json.JSONDecodeError) as exc:
1510
+ existing = load_owned_object(metadata, artifact="agent invocation metadata")
1511
+ except JsonBoundaryError as exc:
1499
1512
  return f"existing metadata is unreadable: {exc}"
1500
1513
  composed = materialized.metadata
1501
1514
  if not isinstance(existing, dict):
@@ -2240,8 +2253,8 @@ def _load_json_object(path: Path, label: str) -> dict[str, object]:
2240
2253
 
2241
2254
  def _read_json_if_object(path: Path) -> dict[str, object] | None:
2242
2255
  try:
2243
- value = json.loads(path.read_text(encoding="utf-8"))
2244
- except (OSError, json.JSONDecodeError):
2256
+ value = load_owned_object(path, artifact="agent invocation metadata")
2257
+ except JsonBoundaryError:
2245
2258
  return None
2246
2259
  return value if isinstance(value, dict) else None
2247
2260