okstra 0.179.2 → 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 (258) hide show
  1. package/README.md +1 -1
  2. package/dist/cli-registry.mjs +30 -2
  3. package/dist/cli-registry.mjs.map +1 -1
  4. package/dist/commands/execute/incremental-carry.mjs +9 -8
  5. package/dist/commands/execute/incremental-carry.mjs.map +1 -1
  6. package/dist/commands/execute/plan-verify.mjs +3 -1
  7. package/dist/commands/execute/plan-verify.mjs.map +1 -1
  8. package/dist/commands/execute/render-bundle.d.mts +4 -2
  9. package/dist/commands/execute/render-bundle.mjs +46 -5
  10. package/dist/commands/execute/render-bundle.mjs.map +1 -1
  11. package/dist/commands/execute/run.mjs +11 -3
  12. package/dist/commands/execute/run.mjs.map +1 -1
  13. package/dist/commands/inspect/model-io.d.mts +1 -0
  14. package/dist/commands/inspect/model-io.mjs +25 -0
  15. package/dist/commands/inspect/model-io.mjs.map +1 -0
  16. package/dist/commands/inspect/stage-map.mjs +29 -8
  17. package/dist/commands/inspect/stage-map.mjs.map +1 -1
  18. package/dist/commands/inspect/task-list.mjs +52 -6
  19. package/dist/commands/inspect/task-list.mjs.map +1 -1
  20. package/dist/commands/inspect/user-response.mjs +14 -4
  21. package/dist/commands/inspect/user-response.mjs.map +1 -1
  22. package/dist/commands/lifecycle/check-project.d.mts +1 -0
  23. package/dist/commands/lifecycle/check-project.mjs +69 -50
  24. package/dist/commands/lifecycle/check-project.mjs.map +1 -1
  25. package/dist/commands/lifecycle/contract-check.d.mts +1 -0
  26. package/dist/commands/lifecycle/contract-check.mjs +18 -0
  27. package/dist/commands/lifecycle/contract-check.mjs.map +1 -0
  28. package/dist/commands/lifecycle/preflight.mjs +154 -51
  29. package/dist/commands/lifecycle/preflight.mjs.map +1 -1
  30. package/dist/commands/pr/pr.d.mts +1 -0
  31. package/dist/commands/pr/pr.mjs +19 -1
  32. package/dist/commands/pr/pr.mjs.map +1 -1
  33. package/dist/commands/report/agent-activity.mjs +2 -2
  34. package/dist/commands/report/approval-decision.d.mts +1 -0
  35. package/dist/commands/report/approval-decision.mjs +21 -0
  36. package/dist/commands/report/approval-decision.mjs.map +1 -0
  37. package/dist/commands/report/design-snapshot.d.mts +1 -0
  38. package/dist/commands/report/design-snapshot.mjs +19 -0
  39. package/dist/commands/report/design-snapshot.mjs.map +1 -0
  40. package/dist/commands/report/translate.mjs +3 -0
  41. package/dist/commands/report/translate.mjs.map +1 -1
  42. package/dist/lib/host-registry-client.mjs +13 -9
  43. package/dist/lib/host-registry-client.mjs.map +1 -1
  44. package/docs/architecture/storage-model.md +1 -1
  45. package/docs/architecture.md +21 -10
  46. package/docs/cli.md +36 -20
  47. package/docs/container.md +6 -4
  48. package/docs/contributor-change-matrix.md +1 -1
  49. package/docs/for-ai/README.md +2 -2
  50. package/docs/for-ai/skills/okstra-brief-gen.md +5 -3
  51. package/docs/for-ai/skills/okstra-code-review.md +4 -4
  52. package/docs/for-ai/skills/okstra-container-build.md +20 -17
  53. package/docs/for-ai/skills/okstra-inspect.md +20 -23
  54. package/docs/for-ai/skills/okstra-manager.md +19 -18
  55. package/docs/for-ai/skills/okstra-memory.md +2 -2
  56. package/docs/for-ai/skills/okstra-pr-gen.md +3 -3
  57. package/docs/for-ai/skills/okstra-rollup.md +14 -13
  58. package/docs/for-ai/skills/okstra-run.md +7 -3
  59. package/docs/for-ai/skills/okstra-schedule-gen.md +15 -18
  60. package/docs/for-ai/skills/okstra-setup.md +7 -7
  61. package/docs/for-ai/skills/okstra-usage.md +5 -4
  62. package/docs/for-ai/skills/okstra-user-response.md +50 -32
  63. package/docs/project-structure-overview.md +43 -31
  64. package/docs/task-process/implementation-planning.md +2 -2
  65. package/docs/task-process/implementation.md +7 -4
  66. package/package.json +1 -1
  67. package/runtime/BUILD.json +2 -2
  68. package/runtime/agents/workers/claude-worker.md +4 -4
  69. package/runtime/agents/workers/report-writer-worker.md +15 -164
  70. package/runtime/agents/workers/translator-worker.md +5 -13
  71. package/runtime/bin/okstra-error-log.py +51 -11
  72. package/runtime/bin/okstra-report-translate.py +210 -23
  73. package/runtime/prompts/host-orchestration/implementation.md +1 -1
  74. package/runtime/prompts/launch.template.md +9 -12
  75. package/runtime/prompts/lead/adapters/cmux.md +1 -1
  76. package/runtime/prompts/lead/context-loader.md +41 -141
  77. package/runtime/prompts/lead/convergence.md +10 -8
  78. package/runtime/prompts/lead/okstra-lead-contract.md +40 -49
  79. package/runtime/prompts/lead/plan-body-verification.md +217 -23
  80. package/runtime/prompts/lead/report-writer.md +81 -421
  81. package/runtime/prompts/lead/team-contract.md +9 -54
  82. package/runtime/prompts/profiles/_clarification-recommendation.md +5 -4
  83. package/runtime/prompts/profiles/_coding-conventions-preflight.md +3 -2
  84. package/runtime/prompts/profiles/_common-contract.md +4 -4
  85. package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
  86. package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
  87. package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
  88. package/runtime/prompts/profiles/change-impact-analysis.md +1 -1
  89. package/runtime/prompts/profiles/error-analysis.md +1 -1
  90. package/runtime/prompts/profiles/feature-analysis.md +1 -1
  91. package/runtime/prompts/profiles/final-verification.md +1 -1
  92. package/runtime/prompts/profiles/implementation-planning.md +20 -12
  93. package/runtime/prompts/profiles/improvement-discovery.md +1 -1
  94. package/runtime/prompts/profiles/project-analysis.md +1 -1
  95. package/runtime/prompts/profiles/release-handoff.md +0 -1
  96. package/runtime/prompts/profiles/requirements-discovery.md +1 -1
  97. package/runtime/prompts/wizard/prompts.ko.json +0 -1
  98. package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +69 -17
  99. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +13 -4
  100. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +8 -2
  101. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
  102. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +50 -5
  103. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +1 -1
  104. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
  105. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +66 -5
  106. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +70 -2
  107. package/runtime/python/okstra_ctl/agent_activity.py +141 -38
  108. package/runtime/python/okstra_ctl/agent_invocation.py +19 -6
  109. package/runtime/python/okstra_ctl/agent_prompt_cli.py +70 -23
  110. package/runtime/python/okstra_ctl/analysis_inputs.py +5 -4
  111. package/runtime/python/okstra_ctl/analysis_packet.py +124 -3
  112. package/runtime/python/okstra_ctl/approval_decisions.py +328 -0
  113. package/runtime/python/okstra_ctl/attempt_evidence.py +2 -2
  114. package/runtime/python/okstra_ctl/backfill.py +13 -10
  115. package/runtime/python/okstra_ctl/batch.py +2 -4
  116. package/runtime/python/okstra_ctl/build_tools.py +6 -3
  117. package/runtime/python/okstra_ctl/claim_reproduction.py +101 -0
  118. package/runtime/python/okstra_ctl/clarification_items.py +27 -13
  119. package/runtime/python/okstra_ctl/cmux.py +130 -52
  120. package/runtime/python/okstra_ctl/code_review_target.py +34 -8
  121. package/runtime/python/okstra_ctl/consumers.py +5 -4
  122. package/runtime/python/okstra_ctl/container.py +103 -8
  123. package/runtime/python/okstra_ctl/context_cost.py +2 -1
  124. package/runtime/python/okstra_ctl/contract_graph.py +497 -0
  125. package/runtime/python/okstra_ctl/contract_graph_cli.py +62 -0
  126. package/runtime/python/okstra_ctl/convergence.py +338 -17
  127. package/runtime/python/okstra_ctl/convergence_engine.py +10 -18
  128. package/runtime/python/okstra_ctl/convergence_provenance.py +58 -8
  129. package/runtime/python/okstra_ctl/convergence_store.py +55 -34
  130. package/runtime/python/okstra_ctl/design_prep.py +7 -4
  131. package/runtime/python/okstra_ctl/design_snapshot.py +134 -0
  132. package/runtime/python/okstra_ctl/dispatch_core.py +97 -69
  133. package/runtime/python/okstra_ctl/dispatch_state.py +163 -63
  134. package/runtime/python/okstra_ctl/doctor.py +6 -3
  135. package/runtime/python/okstra_ctl/domain/worker_presentation.py +70 -9
  136. package/runtime/python/okstra_ctl/entrypoints/hosts.py +16 -30
  137. package/runtime/python/okstra_ctl/error_log_write.py +35 -30
  138. package/runtime/python/okstra_ctl/error_report.py +26 -1
  139. package/runtime/python/okstra_ctl/error_zip.py +27 -5
  140. package/runtime/python/okstra_ctl/execution_identity.py +3 -2
  141. package/runtime/python/okstra_ctl/execution_manifest.py +7 -4
  142. package/runtime/python/okstra_ctl/execution_mutation_audit.py +6 -2
  143. package/runtime/python/okstra_ctl/final_report_schema.py +26 -17
  144. package/runtime/python/okstra_ctl/fix_cycles.py +2 -2
  145. package/runtime/python/okstra_ctl/fixed_text.py +39 -0
  146. package/runtime/python/okstra_ctl/git_reconcile.py +41 -9
  147. package/runtime/python/okstra_ctl/handoff.py +5 -4
  148. package/runtime/python/okstra_ctl/i18n.py +4 -2
  149. package/runtime/python/okstra_ctl/implementation_direction.py +22 -14
  150. package/runtime/python/okstra_ctl/implementation_outcome.py +4 -7
  151. package/runtime/python/okstra_ctl/incremental_carry.py +130 -17
  152. package/runtime/python/okstra_ctl/incremental_scope.py +12 -4
  153. package/runtime/python/okstra_ctl/index.py +8 -11
  154. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +79 -7
  155. package/runtime/python/okstra_ctl/invocation.py +3 -6
  156. package/runtime/python/okstra_ctl/json_boundary.py +366 -0
  157. package/runtime/python/okstra_ctl/json_registry.py +10 -12
  158. package/runtime/python/okstra_ctl/jsonl.py +19 -2
  159. package/runtime/python/okstra_ctl/lead_events.py +33 -1
  160. package/runtime/python/okstra_ctl/listing.py +3 -3
  161. package/runtime/python/okstra_ctl/log_report.py +24 -2
  162. package/runtime/python/okstra_ctl/manager_cli.py +92 -7
  163. package/runtime/python/okstra_ctl/manager_store.py +12 -10
  164. package/runtime/python/okstra_ctl/material.py +5 -1
  165. package/runtime/python/okstra_ctl/migrate.py +29 -25
  166. package/runtime/python/okstra_ctl/model_cli.py +3 -15
  167. package/runtime/python/okstra_ctl/model_io_cli.py +1051 -0
  168. package/runtime/python/okstra_ctl/mutation_probe.py +13 -4
  169. package/runtime/python/okstra_ctl/pane_reclaim.py +3 -2
  170. package/runtime/python/okstra_ctl/path_hints.py +12 -0
  171. package/runtime/python/okstra_ctl/paths.py +21 -0
  172. package/runtime/python/okstra_ctl/plan_items.py +115 -4
  173. package/runtime/python/okstra_ctl/plan_items_cli.py +626 -34
  174. package/runtime/python/okstra_ctl/ports/worker_dispatch.py +2 -1
  175. package/runtime/python/okstra_ctl/pr_template.py +3 -2
  176. package/runtime/python/okstra_ctl/project_meta.py +5 -7
  177. package/runtime/python/okstra_ctl/recap.py +5 -4
  178. package/runtime/python/okstra_ctl/reconcile.py +21 -27
  179. package/runtime/python/okstra_ctl/registry/host_discovery.py +3 -2
  180. package/runtime/python/okstra_ctl/registry/provider_registry.py +3 -2
  181. package/runtime/python/okstra_ctl/render.py +74 -16
  182. package/runtime/python/okstra_ctl/render_final_report.py +10 -8
  183. package/runtime/python/okstra_ctl/report_assembly.py +509 -0
  184. package/runtime/python/okstra_ctl/report_contract.py +2 -1
  185. package/runtime/python/okstra_ctl/report_finalize.py +67 -32
  186. package/runtime/python/okstra_ctl/report_html/render.py +3 -2
  187. package/runtime/python/okstra_ctl/report_inputs.py +72 -0
  188. package/runtime/python/okstra_ctl/report_language.py +3 -2
  189. package/runtime/python/okstra_ctl/report_markdown.py +81 -8
  190. package/runtime/python/okstra_ctl/report_narrative.py +351 -0
  191. package/runtime/python/okstra_ctl/report_projections.py +265 -0
  192. package/runtime/python/okstra_ctl/report_synthesis_packet.py +518 -0
  193. package/runtime/python/okstra_ctl/report_views.py +3 -2
  194. package/runtime/python/okstra_ctl/rollup.py +65 -4
  195. package/runtime/python/okstra_ctl/run.py +184 -65
  196. package/runtime/python/okstra_ctl/run_audit.py +3 -2
  197. package/runtime/python/okstra_ctl/run_context.py +6 -9
  198. package/runtime/python/okstra_ctl/run_index_row.py +2 -8
  199. package/runtime/python/okstra_ctl/schedule_semantics.py +5 -2
  200. package/runtime/python/okstra_ctl/schema_excerpt.py +15 -8
  201. package/runtime/python/okstra_ctl/session_transcript.py +27 -1
  202. package/runtime/python/okstra_ctl/set_work_status.py +64 -38
  203. package/runtime/python/okstra_ctl/stage_fix_carry.py +8 -6
  204. package/runtime/python/okstra_ctl/stage_ledger.py +132 -18
  205. package/runtime/python/okstra_ctl/stage_map.py +96 -28
  206. package/runtime/python/okstra_ctl/stage_targets.py +3 -4
  207. package/runtime/python/okstra_ctl/team.py +3 -2
  208. package/runtime/python/okstra_ctl/team_reconcile.py +11 -2
  209. package/runtime/python/okstra_ctl/time_report.py +51 -4
  210. package/runtime/python/okstra_ctl/usage_identity.py +2 -1
  211. package/runtime/python/okstra_ctl/usage_report.py +58 -4
  212. package/runtime/python/okstra_ctl/user_response.py +1431 -66
  213. package/runtime/python/okstra_ctl/wizard.py +11 -44
  214. package/runtime/python/okstra_ctl/work_categories.py +3 -2
  215. package/runtime/python/okstra_ctl/worker_dispatch.py +5 -2
  216. package/runtime/python/okstra_ctl/worker_prompt_body.py +49 -3
  217. package/runtime/python/okstra_ctl/worker_prompt_contract.py +3 -2
  218. package/runtime/python/okstra_ctl/worker_prompt_policy.py +31 -3
  219. package/runtime/python/okstra_ctl/worker_runner.py +14 -12
  220. package/runtime/python/okstra_ctl/workflow.py +2 -1
  221. package/runtime/python/okstra_ctl/worktree.py +3 -2
  222. package/runtime/python/okstra_ctl/wrapper_status.py +4 -2
  223. package/runtime/python/okstra_ctl/write_policy.py +4 -2
  224. package/runtime/python/okstra_token_usage/antigravity.py +39 -12
  225. package/runtime/python/okstra_token_usage/collect.py +90 -38
  226. package/runtime/python/okstra_token_usage/grok.py +127 -0
  227. package/runtime/schemas/final-report-v2.0.schema.json +9 -0
  228. package/runtime/schemas/final-report-v3.0.schema.json +10219 -0
  229. package/runtime/schemas/report-narrative-v3.0.schema.json +30 -0
  230. package/runtime/schemas/report-synthesis-packet-v1.0.schema.json +140 -0
  231. package/runtime/skills/okstra-brief-gen/SKILL.md +9 -7
  232. package/runtime/skills/okstra-code-review/SKILL.md +21 -11
  233. package/runtime/skills/okstra-container-build/SKILL.md +18 -18
  234. package/runtime/skills/okstra-inspect/SKILL.md +12 -11
  235. package/runtime/skills/okstra-inspect/facets/error-zip.md +8 -8
  236. package/runtime/skills/okstra-inspect/facets/errors.md +2 -2
  237. package/runtime/skills/okstra-inspect/facets/history.md +9 -14
  238. package/runtime/skills/okstra-inspect/facets/logs.md +2 -2
  239. package/runtime/skills/okstra-inspect/facets/recap.md +5 -5
  240. package/runtime/skills/okstra-inspect/facets/report.md +6 -10
  241. package/runtime/skills/okstra-inspect/facets/status.md +9 -8
  242. package/runtime/skills/okstra-inspect/facets/time.md +3 -3
  243. package/runtime/skills/okstra-manager/SKILL.md +16 -14
  244. package/runtime/skills/okstra-memory/SKILL.md +3 -3
  245. package/runtime/skills/okstra-pr-gen/SKILL.md +5 -4
  246. package/runtime/skills/okstra-rollup/SKILL.md +6 -16
  247. package/runtime/skills/okstra-run/SKILL.md +7 -7
  248. package/runtime/skills/okstra-schedule-gen/SKILL.md +21 -17
  249. package/runtime/skills/okstra-setup/SKILL.md +21 -13
  250. package/runtime/skills/okstra-setup/references/project-config.md +2 -2
  251. package/runtime/skills/okstra-usage/SKILL.md +10 -10
  252. package/runtime/skills/okstra-user-response/SKILL.md +78 -107
  253. package/runtime/templates/report-writer-prompt-preamble.md +29 -19
  254. package/runtime/templates/reports/html/macros/forms.html +6 -4
  255. package/runtime/templates/reports/schedule.template.md +4 -4
  256. package/runtime/templates/worker-error-contract.md +17 -29
  257. package/runtime/validators/validate-run.py +636 -72
  258. package/runtime/validators/validate_session_conformance.py +43 -7
@@ -4,7 +4,7 @@
4
4
  roles:
5
5
  - role: analyser
6
6
  min: 2
7
- recommended: 2
7
+ recommended: 3
8
8
  max: 5
9
9
  duty: discovery-worker
10
10
  - role: critic
@@ -623,7 +623,6 @@
623
623
  "static_role": " static-role : {role}#{ordinal} / {model}",
624
624
  "dynamic_role": " dynamic-role : {role} / reuse selected participant model",
625
625
  "workers_implementation_default": " workers : (프로필 기본 — executor + verifier 2 + report-writer)",
626
- "critic_shares_analyser_provider": " ⚠ critic provider 가 분석 워커 `{worker}` 와 같습니다 — gap 검증에서 그 워커는 투표할 수 없어 유효 투표자가 {remaining}명으로 줍니다",
627
626
  "base_ref_stage_isolated": " base-ref : (stage 격리 — 의존 stage 기준으로 run 준비 시점에 자동 해소)",
628
627
  "base_ref_reuse_task_dir": " base-ref : (기존 `{task_key}` 디렉터리 재사용 — 최초 base 유지)",
629
628
  "worktree_new": " worktree : 새 브랜치 `{branch}` (base-ref `{base_ref}`) → `{path}`",
@@ -1,8 +1,10 @@
1
1
  """Capability-based strategy shared by bundled host adapters."""
2
2
  from __future__ import annotations
3
3
 
4
+ import json
4
5
  from collections.abc import Callable, Mapping
5
6
  from dataclasses import dataclass
7
+ from pathlib import Path
6
8
 
7
9
  from okstra_ctl.adapters.dispatch import default_worker_dispatch_port
8
10
  from okstra_ctl.domain.host import (
@@ -57,27 +59,52 @@ INTERACTION_FUNCTIONS = PLAIN_TEXT_FUNCTIONS | frozenset({
57
59
  "native_multi_select",
58
60
  "native_question_group",
59
61
  })
60
- _NATIVE_QUESTION_LIMIT = 4
61
- _NATIVE_OPTION_RANGE = range(2, 5)
62
+ _RELAY_HEADING = "## Wizard interaction relay"
62
63
 
63
64
 
64
- def _native_options_fit(prompt: WizardPrompt) -> bool:
65
- labels = tuple(option.label for option in prompt.options)
66
- return (
67
- len(labels) in _NATIVE_OPTION_RANGE
68
- and len(set(labels)) == len(labels)
69
- and (not prompt.multi or all(", " not in label for label in labels))
70
- )
71
-
72
-
73
- def _native_group_fits(prompt: WizardPrompt) -> bool:
74
- return (
75
- 1 <= len(prompt.questions) <= _NATIVE_QUESTION_LIMIT
76
- and all(_native_options_fit(question) for question in prompt.questions)
65
+ @dataclass(frozen=True)
66
+ class NativePickerLimits:
67
+ """How many questions and options a host's native picker can show.
68
+
69
+ Counts that miss the range keep every option and fall back to numbered
70
+ text. The defaults are Claude Code's AskUserQuestion shape; a host that
71
+ can show more must declare that in its relay `nativeLimits`.
72
+ """
73
+
74
+ min_options: int = 2
75
+ max_options: int = 4
76
+ max_questions: int = 4
77
+
78
+ def option_count_fits(self, count: int) -> bool:
79
+ return self.min_options <= count <= self.max_options
80
+
81
+
82
+ def wizard_relay_contract(path: Path) -> dict[str, object]:
83
+ """The JSON object under `## Wizard interaction relay` in a host relay."""
84
+ body = path.read_text(encoding="utf-8")
85
+ section = body.split(_RELAY_HEADING, 1)[1]
86
+ encoded = section.split("```json\n", 1)[1].split("\n```", 1)[0]
87
+ payload = json.loads(encoded)
88
+ if not isinstance(payload, dict):
89
+ raise ValueError(f"wizard relay in {path} is not a JSON object")
90
+ return payload
91
+
92
+
93
+ def native_limits_from_relay(contract: Mapping[str, object]) -> NativePickerLimits:
94
+ raw = contract.get("nativeLimits")
95
+ if not isinstance(raw, Mapping):
96
+ return NativePickerLimits()
97
+ return NativePickerLimits(
98
+ min_options=int(raw.get("minOptions", 2)),
99
+ max_options=int(raw.get("maxOptions", 4)),
100
+ max_questions=int(raw.get("maxQuestions", 4)),
77
101
  )
78
102
 
79
103
 
80
104
  class CapabilityInteractionPort:
105
+ def __init__(self, *, limits: NativePickerLimits | None = None) -> None:
106
+ self._limits = limits or NativePickerLimits()
107
+
81
108
  def plan(
82
109
  self,
83
110
  prompt: WizardPrompt,
@@ -87,7 +114,7 @@ class CapabilityInteractionPort:
87
114
  if prompt.kind == "pick_group":
88
115
  if (
89
116
  "native_question_group" in functions
90
- and _native_group_fits(prompt)
117
+ and self._native_group_fits(prompt, functions)
91
118
  ):
92
119
  return InteractionPlan("native-group", AnswerProtocol("group-json"))
93
120
  return InteractionPlan(
@@ -96,7 +123,7 @@ class CapabilityInteractionPort:
96
123
  )
97
124
  if prompt.kind != "pick":
98
125
  return InteractionPlan("plain-text", AnswerProtocol("exact-value"))
99
- native_options_fit = _native_options_fit(prompt)
126
+ native_options_fit = self._native_options_fit(prompt)
100
127
  if (
101
128
  prompt.multi
102
129
  and "native_multi_select" in functions
@@ -115,11 +142,36 @@ class CapabilityInteractionPort:
115
142
  kind = "numbered-multi" if prompt.multi else "numbered-single"
116
143
  return InteractionPlan(kind, AnswerProtocol("numbered", multi=prompt.multi))
117
144
 
145
+ def _native_options_fit(self, prompt: WizardPrompt) -> bool:
146
+ labels = tuple(option.label for option in prompt.options)
147
+ return (
148
+ self._limits.option_count_fits(len(labels))
149
+ and len(set(labels)) == len(labels)
150
+ and (not prompt.multi or all(", " not in label for label in labels))
151
+ )
152
+
153
+ def _native_group_fits(
154
+ self, prompt: WizardPrompt, functions: frozenset[str]
155
+ ) -> bool:
156
+ if any(question.multi for question in prompt.questions) and (
157
+ "native_multi_select" not in functions
158
+ ):
159
+ return False
160
+ return (
161
+ 1 <= len(prompt.questions) <= self._limits.max_questions
162
+ and all(self._native_options_fit(question) for question in prompt.questions)
163
+ )
164
+
118
165
 
119
166
  def numbered_interaction_port() -> CapabilityInteractionPort:
120
167
  return CapabilityInteractionPort()
121
168
 
122
169
 
170
+ def relay_interaction_port(relay_path: str | Path) -> CapabilityInteractionPort:
171
+ contract = wizard_relay_contract(Path(relay_path))
172
+ return CapabilityInteractionPort(limits=native_limits_from_relay(contract))
173
+
174
+
123
175
  class ProviderLeadSessionPort:
124
176
  def __init__(
125
177
  self,
@@ -12,7 +12,7 @@ from okstra_ctl.adapters.hosts.capability_adapter import (
12
12
  INTERACTION_FUNCTIONS,
13
13
  PENDING_HOST_PORT,
14
14
  CapabilityHostAdapter,
15
- numbered_interaction_port,
15
+ relay_interaction_port,
16
16
  )
17
17
  from okstra_ctl.domain.host import HostClaim, HostDescriptor, HostResolutionContext
18
18
  from okstra_ctl.domain.provider import HostModelBinding
@@ -21,6 +21,11 @@ from okstra_ctl.ports.host_model import (
21
21
  HostModelBindingRequest,
22
22
  )
23
23
  from okstra_ctl.registry.provider_registry import ProviderRegistry
24
+ from okstra_ctl.json_boundary import (
25
+ JsonBoundaryError,
26
+ external_claude_global_json_source,
27
+ load_external_json,
28
+ )
24
29
 
25
30
 
26
31
  DESCRIPTOR = HostDescriptor(
@@ -108,7 +113,11 @@ def _workspace_trust_checks(context) -> tuple[dict[str, object], ...]:
108
113
  if not (project_root / ".claude" / "settings.local.json").exists():
109
114
  return ({"id": "workspace-trust", "status": "not-applicable"},)
110
115
  try:
111
- state = json.loads((home_dir / ".claude.json").read_text(encoding="utf-8"))
116
+ # 외부 입력: Claude Code가 소유하는 사용자 workspace trust 상태다.
117
+ state = load_external_json(
118
+ external_claude_global_json_source(home_dir / ".claude.json", home_dir),
119
+ artifact="Claude Code configuration",
120
+ )
112
121
  projects = state.get("projects") if isinstance(state, dict) else None
113
122
  if not isinstance(projects, dict):
114
123
  return _workspace_trust_unavailable()
@@ -124,7 +133,7 @@ def _workspace_trust_checks(context) -> tuple[dict[str, object], ...]:
124
133
  if accepted is False or accepted is None:
125
134
  return _workspace_trust_required()
126
135
  return _workspace_trust_unavailable()
127
- except (OSError, json.JSONDecodeError):
136
+ except JsonBoundaryError:
128
137
  return _workspace_trust_unavailable()
129
138
 
130
139
 
@@ -160,7 +169,7 @@ def create_adapter(
160
169
  interaction_port=(
161
170
  interaction_port
162
171
  if interaction_port is not None
163
- else numbered_interaction_port()
172
+ else relay_interaction_port(DESCRIPTOR.relay_contract)
164
173
  ),
165
174
  lead_session_port=lead_session_port,
166
175
  worker_dispatch_port=worker_dispatch_port,
@@ -32,6 +32,11 @@ Read this contract when `okstra preflight` returns this file as `runtimeReadines
32
32
  "native_multi_select",
33
33
  "native_question_group"
34
34
  ],
35
+ "nativeLimits": {
36
+ "minOptions": 2,
37
+ "maxOptions": 4,
38
+ "maxQuestions": 4
39
+ },
35
40
  "interactions": {
36
41
  "native-single": {
37
42
  "function": "AskUserQuestion",
@@ -124,7 +129,7 @@ Read this contract when `okstra preflight` returns this file as `runtimeReadines
124
129
  }
125
130
  ```
126
131
 
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.
132
+ 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 when the prompt fits `nativeLimits` (unique option labels, two to four options, one to four questions); 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
133
 
129
134
  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
135
 
@@ -158,7 +163,8 @@ For a `host-text` mapping, render each numbered item as its option label followe
158
163
  - CLI-wrapper assignments never enter the Agent layer. `okstra worker-dispatch` validates the metadata and passes `modelExecutionValue` to the registered provider script.
159
164
  - Missing or unsupported family-token mapping is a pre-dispatch contract failure. Never inherit the lead model, choose a nearby alias, or switch provider silently.
160
165
  - Every analysis dispatch sets `name: "<workerId>-worker"`; convergence retries append `-reverify-r<N>`, implementation uses the functional `-executor` / `-verifier` suffix, and report writing uses `report-writer`. These values are retained as `agentName` in session JSONL for usage attribution.
161
- - Every CLI-worker prompt includes `**Pane role:** <functional-role>` so the entrypoint's optional fifth argument carries the dispatched role. That argument selects the dispatch's idle budget `executor` and `verifier` run silent build+test suites and get 1500s, every other role 600s and is recorded in the run's status sidecar. Omitting it defaults to `worker`, i.e. the short budget, which reaps a healthy build mid-suite.
166
+ - A CLI worker's role reaches the entrypoint from the invocation's duty, NOT from the prompt text. `WorkerJob.wrapper_role` resolves `role_for_duty(dutyId)` and passes the canonical role in the role positional; the entrypoint checks it against the same derivation in the invocation metadata and refuses a mismatch. Do not add, edit, or rely on a `**Pane role:**` line to change what a dispatch runs as the prompt body carries that header for the initial analysis audiences only, and it selects nothing.
167
+ - The role decides the dispatch's idle budget, and `worker-dispatch` leaves the budget positional EMPTY so it can: an empty slot makes the entrypoint read the role's own budget (`domain/worker_role.role_spec` — `implementer` and `verifier` run silent build+test suites and get 1500s, every other role 600s). Passing an explicit `--idle-timeout-seconds` overrides that for the whole dispatch, so pass one only for a run-specific reason; filling the slot with a blanket default is what made the role budgets unreachable and reaped healthy workers mid-suite.
162
168
  - The host may supply transport metadata for native calls, but acceptance records only the verified invocation specification link. Record `enforcementMode=host-native-spec-link-gate`, `promptPath`, and `metadataPath`; do not claim the host-delivered bytes were observed.
163
169
  - A retry keeps the same Agent `name`. When logging a twice-failed CLI-wrapper attempt, reference both attempts' `bash_ids` and prompt-history paths.
164
170
  - An internally detected contract violation without a specific worker uses `--agent "claude-lead"` in the error-log event.
@@ -12,7 +12,7 @@ from okstra_ctl.adapters.hosts.capability_adapter import (
12
12
  PENDING_HOST_PORT,
13
13
  CapabilityHostAdapter,
14
14
  no_automatic_claim,
15
- numbered_interaction_port,
15
+ relay_interaction_port,
16
16
  )
17
17
  from okstra_ctl.domain.host import HostDescriptor
18
18
  from okstra_ctl.ports.host_model import NativeExecutionValueHostModelBindingPort
@@ -73,7 +73,7 @@ def create_adapter(
73
73
  interaction_port=(
74
74
  interaction_port
75
75
  if interaction_port is not None
76
- else numbered_interaction_port()
76
+ else relay_interaction_port(DESCRIPTOR.relay_contract)
77
77
  ),
78
78
  lead_session_port=lead_session_port,
79
79
  worker_dispatch_port=worker_dispatch_port,
@@ -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. |
@@ -91,7 +91,7 @@ Render every numbered item as its option label followed by its description verba
91
91
  - Worker completion is valid only from `workerDispatches[]`, terminal status sidecars, and required Result Paths. Pane creation alone is not completion.
92
92
  - Reverify uses a fresh jobs file at `runs/<task-type>/state/reverify-jobs-r<N>-<task-type>-<seq>.json`, sets `dispatchKind: "reverify-r<N>"`, and dispatches with `okstra team dispatch --project-root <root> --run-manifest <path> --dispatch-kind reverify-r<N> --jobs-file <jobs-file>`.
93
93
  - Report-writer uses a fresh one-job jobs file with `dispatchKind: "report-writer"` and the same schema, then dispatches through `okstra team dispatch --project-root <root> --run-manifest <path> --jobs-file <jobs-file>`.
94
- - Every reverify or report-writer jobs file carries `workerId`, `provider`, `role`, `modelExecutionValue`, `promptPath`, `promptMetadataPath`, `invocationId`, `assignmentRef`, `audience`, the five prompt digests, `resultPath`, `workerResultPath`, and `completionPaths`. `worker-dispatch` verifies these fields before launching the provider process. For reverify, set `role` to `worker-reverify-r<N>`. The report-writer completion paths include both data.json and the worker-results audit file.
94
+ - Every reverify or report-writer jobs file carries `workerId`, `provider`, `role`, `modelExecutionValue`, `promptPath`, `promptMetadataPath`, `invocationId`, `assignmentRef`, `audience`, the five prompt digests, `resultPath`, `workerResultPath`, and `completionPaths`. `worker-dispatch` verifies these fields before launching the provider process. For reverify, set `role` to `worker-reverify-r<N>`. The report-writer completion paths include its narrative Markdown, worker-result pointer, and audit sidecar; Phase 7 later assembles `data.json`.
95
95
  - After either dispatch, run `okstra team await --project-root <root> --run-manifest <path>` before evaluating terminal status or completion paths.
96
96
 
97
97
  ## Completion, cleanup, and resume
@@ -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: