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
@@ -29,7 +29,7 @@ The start screen assigns a **model ref** (`provider/model`) to each **canonical
29
29
 
30
30
  **Dispatch does not invent a missing model assignment.** Launch-time empty slots are filled by the model default chain before the run starts. At dispatch the model for every role comes from `resultContract.requiredWorkerRoles[*].modelExecutionValue` in `task-manifest.json` (and lead model metadata). There is no per-role hard-coded fallback — see "Model Assignment Rules" below.
31
31
 
32
- **Dispatch-prompt invariant.** Lead's dispatch prompt body for every rostered analysis worker MUST be byte-identical except for the role label and any wrapper-specific path headers (e.g. `**Worktree:**`, `**Errors sidecar path:**`). The role label is the ONLY identity form the normalizer erases, and it erases exactly the label `worker_prompt_body.analysis_worker_label` renders for the run's own worker ids — so a provider outside that function's display map (`grok`, `kimi`, an installed adapter) is covered as it comes. Naming the worker any other way (a model name, a host name, a provider's product name) survives normalization and fails the equality group before publication. **Enforced:** `okstra_ctl.worker_prompt_contract.normalise_analysis_prompt`, pinned by `tests/contract/test_analysis_prompt_identity_normalization.py`. Lead MUST NOT bias the brief by inserting per-worker emphasis sentences ("you focus on X") into the body. Bias-by-prompt reproduces the historical failure mode where Claude commented only on assumptions, Codex only on code paths, and Antigravity only on requirements — leaving convergence with nothing to converge on.
32
+ **Dispatch-prompt invariant.** Lead's dispatch prompt body for every rostered analysis worker MUST be byte-identical except for the role label and wrapper-specific path headers (for example `**Worktree:**`). The role label is the ONLY identity form the normalizer erases, and it erases exactly the label `worker_prompt_body.analysis_worker_label` renders for the run's own worker ids — so a provider outside that function's display map (`grok`, `kimi`, an installed adapter) is covered as it comes. Naming the worker any other way (a model name, a host name, a provider's product name) survives normalization and fails the equality group before publication. **Enforced:** `okstra_ctl.worker_prompt_contract.normalise_analysis_prompt`, pinned by `tests/contract/test_analysis_prompt_identity_normalization.py`. Lead MUST NOT bias the brief by inserting per-worker emphasis sentences ("you focus on X") into the body. Bias-by-prompt reproduces the historical failure mode where Claude commented only on assumptions, Codex only on code paths, and Antigravity only on requirements — leaving convergence with nothing to converge on.
33
33
 
34
34
  Disjoint initial scopes are invalid triangulation. Every selected analysis worker owns the same common verification requirements; provider diversity supplies independent observations, not separate coverage slices. Do not shard the common scope by worker, provider, or model. Worker-specific depth belongs only in the non-voting Specialization Lens after the shared analysis is complete.
35
35
 
@@ -54,7 +54,7 @@ Only workers selected from `recommendedWorkers` in `task-manifest.json` and `res
54
54
 
55
55
  0. **Adapter-owned dispatch (BLOCKING).** Every worker start, await, retry, and shutdown goes through the selected runtime adapter. Core state records the outcome but never guesses a host primitive.
56
56
  1. The lead is responsible for orchestration, convergence supervision, and final-report review/approval. It never overrides worker analysis and never bypasses a rostered Report writer worker.
57
- 2. `Report writer worker` is NOT an analysis worker. It is excluded from Phase 4/5 (initial analysis) and Phase 5.5 (convergence re-verification). It is spawned only in Phase 6 and is the **author** of the report record at `runs/<task-type>/reports/final-report-<task-type>-<seq>.data.json`.
57
+ 2. `Report writer worker` is NOT an analysis worker. It is excluded from Phase 4/5 (initial analysis) and Phase 5.5 (convergence re-verification). It is spawned only in Phase 6 and authors only the narrative input consumed by report assembly.
58
58
  3. When `Report writer worker` is in the roster, Lead MUST dispatch it in Phase 6 as a separate invocation after convergence. Omit it from Phase 4/5 analysis selection and pass `--workers report-writer` for a CLI-backed Phase 6 call. Contract v3 has no lead-authored fallback: an attempted dispatch ending in `error` / `timeout` / `not-run` is retried or leaves the run blocked. **Enforced:** `dispatch_core._validate_report_writer_isolation()` rejects every mixed analysis/report plan before process creation, and the default roster selectors exclude `report-writer`; report-writer write paths exclude the final record.
59
59
  4. The assigned model for each role is maintained based on `resultContract.requiredWorkerRoles` in task-manifest.json and the lead model metadata.
60
60
  5. Required roles must not be replaced by unnamed generic parallel workers.
@@ -83,7 +83,7 @@ The Phase 7 run validator enforces the same cross-task rule against the persiste
83
83
 
84
84
  When a worker reads any project-relative path from the prompt, it MUST resolve it against `Project Root` (e.g. `<Project Root>/<Result Path>`) — never use bare relative paths that depend on cwd.
85
85
 
86
- Inject only the packet-scoped one-line pointer into every analysis worker's prompt and into the report-writer prompt when it is dispatched in Phase 6: `**MCP servers:** follow the analysis packet's "Available MCP Servers" section. If the section is absent or says none, treat MCP as unavailable for this run; never infer tools from host configuration.` Codex/Antigravity workers run external CLIs whose MCP availability is governed by their own CLI configs; they record `MCP not available in this CLI` when their CLI does not expose a server the packet names.
86
+ Inject only the packet-scoped one-line pointer into every analysis worker's prompt: `**MCP servers:** follow the analysis packet's "Available MCP Servers" section. If the section is absent or says none, treat MCP as unavailable for this run; never infer tools from host configuration.` Codex/Antigravity workers run external CLIs whose MCP availability is governed by their own CLI configs; they record `MCP not available in this CLI` when their CLI does not expose a server the packet names.
87
87
 
88
88
  Persist the exact worker prompt before dispatch per Operating Rule 6; never use `/tmp/*prompt*.txt` as the canonical artifact path.
89
89
 
@@ -96,7 +96,7 @@ The lead does not inline reading or error blocks. It resolves `PromptPlan.audien
96
96
  What the lead MUST still do per dispatch:
97
97
  - Inject the input file enumeration into the dispatch prompt body via an `## Inputs` section (or any heading the recipient agent expects), listing the actual project-relative primary inputs derived from the run's `instruction-set/`. For `final-verification` analysis workers, list only `analysis-packet.md` as the primary input; source files are reached on demand through that packet. Other phases may list source/fallback paths when useful. The preamble describes the rules; the lead provides the specific paths for THIS run.
98
98
  - Inject `**Evidence ledger:** required-v1` into every initial non-report-writer prompt. The selected audience preamble owns the audit-row syntax; `validators/validate-run.py` `validate_worker_results_audit()` enforces that each backticked `path:line` result citation has a matching evidence-read row. Report-writer and reverify prompts do not carry this marker.
99
- - Inject `**Worker Error Contract Path:**` plus the absolute `**Errors log path:**` and `**Errors sidecar path:**` headers — workers cannot synthesize these paths.
99
+ - Inject `**Worker Error Contract Path:**` plus the absolute `**Errors log path:**` header — workers cannot synthesize this path.
100
100
  - Omit the preamble pointer for reverify dispatches (Phase 5.5 lightweight mode) — see [convergence](./convergence.md) "Reverify prompt: required-reading suppression".
101
101
 
102
102
  Audience-scoped file enumeration (performance optimization — mandatory):
@@ -196,57 +196,13 @@ Lead-facing duties that stay here:
196
196
  - Lead and report-writer MUST preserve worker ticket tagging and `worker:item` source pairs when carrying findings into the final report.
197
197
  - Lead's result-missing redispatch check parses the frontmatter and sections 1–5 presence per the preamble contract (see "Lead Redispatch Policy on Result-Missing").
198
198
 
199
- ### Optional errors sidecar (worker-reported)
199
+ ### Worker error-log command
200
200
 
201
- A worker MAY produce an errors sidecar file at:
201
+ Workers record a tool failure through the typed `okstra error-log append-observed` command in `templates/worker-error-contract.md`. They do not create a JSON sidecar or pass JSON text. The command receives the assigned worker identity, task key, phase, model, and observed scalar fields; the Python writer owns JSONL serialization and validation.
202
202
 
203
- ```
204
- runs/<task-type>/worker-results/<role-slug>-errors-<task-type>-<seq>.json
205
- ```
206
-
207
- This sidecar collects tool failures observed inside the worker's session
208
- (non-zero Bash exits, MCP errors, tool exceptions). It is optional — its
209
- absence does not invalidate a worker result.
210
-
211
- Schema:
212
-
213
- ```json
214
- {
215
- "schemaVersion": 1,
216
- "errors": [
217
- {
218
- "ts": "<ISO 8601 string>",
219
- "phase": "<okstra phase 1..7>",
220
- "errorType": "tool-failure",
221
- "command": "<failed command or tool signature>",
222
- "commandKind": "bash | mcp | tool:Read | tool:Edit | ...",
223
- "exitCode": <int|null>,
224
- "durationMs": <int|null>,
225
- "message": "<one-line human summary>",
226
- "stderrExcerpt": "<first ~2KB of stderr or null>",
227
- "context": { "<freeform>": "..." }
228
- }
229
- ]
230
- }
231
- ```
232
-
233
- Workers MUST omit `source` / `recordedAt` / `agent` / `agentRole` / `model` /
234
- `taskKey`. The lead fills those in when dumping the sidecar to the
235
- run-level errors log (`runs/<task-type>/logs/errors-<task-type>-<seq>.jsonl`)
236
- via `okstra error-log append-from-worker`.
237
-
238
- Workers MUST use only `errorType: "tool-failure"` in the **sidecar file**.
239
-
240
- **Path delivery contract (BLOCKING).** Workers do NOT synthesize the
241
- run-level errors log path or their sidecar path from the
242
- `runs/<task-type>/...` template syntax. Both absolute paths are delivered
243
- by Lead via the `**Errors log path:**` / `**Errors sidecar path:**` anchor
244
- headers; Lead obtains the values from the launch prompt's `## Run Logs (error-log
245
- wiring)` section (resolved by the okstra runtime via `paths.py`). If Lead
246
- omits either header, the worker MUST return `<WORKER>_ERRORS_PATH_MISSING`
247
- without proceeding.
203
+ **Path delivery contract (BLOCKING).** Workers do NOT synthesize the run-level errors log path from `runs/<task-type>/...` template syntax. Lead delivers the absolute `**Errors log path:**` anchor from the launch prompt's `## Run Logs (error-log wiring)` section. If Lead omits the header, the worker MUST return `<WORKER>_ERRORS_PATH_MISSING` without proceeding.
248
204
 
249
- - `cli-failure` events are recorded by `worker-dispatch` directly to the run-level error log via `okstra error-log append-observed --error-type cli-failure ...` — NOT via the sidecar. The sidecar is a worker tool-failure channel only.
205
+ - `cli-failure` events are recorded by `worker-dispatch` directly to the run-level error log via `okstra error-log append-observed --error-type cli-failure ...`.
250
206
  - **Provider-process invocation arity.** Every `okstra-<provider>-exec.sh` entrypoint takes the same three required positional arguments plus three optional ones: `<project-root> <model-execution-value> <prompt-path> [worktree-path] [role] [idle-timeout-seconds]`, optionally followed by `--presentation live|quiet`. `worker-dispatch` alone constructs this invocation from the verified `WorkerJob`; leads and host adapters do not assemble it. The fourth argument is mandatory for implementation, the fifth names the functional role, and the sixth controls the shared idle budget (1500s for executor/verifier, 600s otherwise). `live` is reserved for a pane backend; deterministic dispatch uses `quiet`.
251
207
  - **Background dispatch + polling contract (CLI processes).** `worker-dispatch` starts the selected provider CLI through the adapter's asynchronous execution mapping and awaits the same handle until it reports terminal completion, capped at 30 minutes (1800s) of wall-clock elapsed time. The adapter's await operation is the wait primitive; do not add a standalone sleep or build shorter-sleep loops to bypass a host constraint. This rule applies in **every phase**. Recording responsibilities:
252
208
  - Successful completion: return the provider process's accumulated stdout from the terminal await result. No log entry.
@@ -255,7 +211,6 @@ without proceeding.
255
211
  - The selected adapter owns runtime-session accounting for the full wrapper window; core retains only the observed start/end event boundaries.
256
212
  - **No external timeout around `worker-dispatch`.** The deterministic dispatch owns BOTH timeout mechanisms: (1) the process polling cap (30min + optional 5min mtime grace), and (2) the shared runner's stream-idle watchdog. If the CLI produces nothing for `<idle-timeout-seconds>`, the runner terminates the process group and marks the status sidecar timed out. Lead MUST NOT layer an earlier host timeout around it.
257
213
  - `contract-violation` events (C) are recorded by Lead via `okstra error-log append-observed --error-type contract-violation ...` after inspecting worker outputs.
258
- - Lead's responsibility regarding the sidecar is to dump it to the run-level error log via `okstra error-log append-from-worker` after each worker terminates; Lead does not write into the sidecar.
259
214
 
260
215
  ## Convergence Phase Rules
261
216
 
@@ -16,7 +16,8 @@ prompt through `prepare_agent_invocation()` before `worker-dispatch`.
16
16
  Load the applicable coding conventions for every language the diff will touch, then state in ONE line which conventions apply (e.g. `Applying TS + hexagonal overlay; domain at src/domains/*/domain/`). Lint/test green is necessary but NOT sufficient — self-mocked tests, interaction-only assertions, and untruthful names all pass a green pipeline; this gate is what keeps them out of the diff.
17
17
 
18
18
  - **Resource selection — read the routed pack, never inline it here.** Use this worker prompt's `**Coding preflight pack:**` anchor header as the absolute path to the installed routed pack. Detect each touched file's language and framework from its extension or project manifest (`package.json`, `Cargo.toml`, `pyproject.toml`, `pom.xml`, `build.gradle*`, `prisma/schema.prisma`), then read that pack's resources via the Read tool by absolute path. Always read `overview.md` (the router) + `clean-code.md`, then select per the router's three ordered stages — Stage 1 language → `languages/<lang>.md`, Stage 2 framework → `frameworks/<fw>.md` (e.g. `frameworks/node-server.md` for server-side Node), Stage 3 architecture → `architectures/<arch>.md` (e.g. `architectures/hexagonal.md` for ports-and-adapters / NestJS-hex). Each stage is a list of rules; include EVERY matching resource (a change set can touch multiple languages/frameworks/architectures) — do not stop at the first match. These files are runtime resources, not Skill-tool skills, so always read them by path.
19
- - **Declared architecture style an authoritative Stage 3 input, and it binds.** Before selecting resources, read `<PROJECT_ROOT>/.okstra/project.json` and take `architecture.style`. A declared `hexagonal` selects `architectures/hexagonal.md` even when none of Stage 3's layout signals matched, so the declaration — not the directory shape — decides. A declared `layered` has no pack resource; its invariant applies from this line: dependencies run one direction only — an upper layer may import a lower one, never the reverse — and a variation point is extracted onto a layer boundary. A declared style makes this overlay binding rather than advisory, and which rule binds follows the style: under `hexagonal` the overlay's otherwise-advisory concrete-adapter item is blocking, so a service dependency you add or modify goes through a port instead of a concrete implementation and that placement violation is fixed before the write rather than recorded as a note; under `layered` what binds is the direction invariant just stated — your own judgement over the import list of every file the diff touches, plus extracting a variation point onto a layer boundary — while the concrete-adapter item stays advisory, since `layered` has no ports to route it through. An absent field, a `none` style, or an unreadable `project.json` changes nothing Stage 3 stays detection-driven and its overlay stays advisory, leaving the language-agnostic principles below as the only always-binding layer. The verifier re-grades the same diff under the same declaration (`_implementation-verifier.md` → Static design & test-quality review), so a placement violation missed here returns as a verdict `FAIL`.
20
- - **Project review rule packs:** a pack applies when either source names it the task brief's `Source Material` / `Reporter Confirmations` cites its exact `SKILL.md` path, or `<PROJECT_ROOT>/.okstra/project.json` lists it under `reviewRulePacks` (absolute paths; a project's standing standard, so it applies to every run whether or not the brief mentions it). The two sources are a union. Read only those files and the `references/*.md` files they directly name; a declared path that will not open is recorded as `project-review-rules: declared <path> unreadable`, never silently dropped. Do not search parent directories or host skill catalogs. Apply those rules during implementation as a prevention pass, not a PR-comment generation workflow: do not dispatch reviewer subagents from the executor. For Fonts Ninja-style PR review packs, the executor must avoid newly introduced duplicate helper stacks, tautological tests that merely re-call the delegated helper, self-mocking, domain rules in adapters/ports, domain objects outside `domain/`, dead APIs, weak public names, and functions that fail the plain-English read.
19
+ - **Project policy projection:** before selecting resources, run `okstra model-io project-context --project-root <PROJECT_ROOT> --task-ref <task-ref>`. Consume its `Architecture style`, `Project Review Rule Packs`, and `Project QA Commands` sections; do not open Okstra-owned JSON storage.
20
+ - **Declared architecture style an authoritative Stage 3 input, and it binds.** A projected `hexagonal` selects `architectures/hexagonal.md` even when none of Stage 3's layout signals matched, so the declaration — not the directory shape — decides. A projected `layered` has no pack resource; its invariant applies from this line: dependencies run one direction only — an upper layer may import a lower one, never the reverse and a variation point is extracted onto a layer boundary. A declared style makes this overlay binding rather than advisory, and which rule binds follows the style: under `hexagonal` the overlay's otherwise-advisory concrete-adapter item is blocking, so a service dependency you add or modify goes through a port instead of a concrete implementation and that placement violation is fixed before the write rather than recorded as a note; under `layered` what binds is the direction invariant just stated your own judgement over the import list of every file the diff touches, plus extracting a variation point onto a layer boundary — while the concrete-adapter item stays advisory, since `layered` has no ports to route it through. An absent or `none` projected style leaves Stage 3 detection-driven and its overlay advisory. The verifier re-grades the same diff under the same declaration (`_implementation-verifier.md` Static design & test-quality review), so a placement violation missed here returns as a verdict `FAIL`.
21
+ - **Project review rule packs:** a pack applies when either source names it — the task brief's `Source Material` / `Reporter Confirmations` cites its exact `SKILL.md` path, or the project-context projection lists it as a standing standard. The two sources are a union. Read only those files and the `references/*.md` files they directly name; a declared path that will not open is recorded as `project-review-rules: declared <path> unreadable`, never silently dropped. Do not search parent directories or host skill catalogs. Apply those rules during implementation as a prevention pass, not a PR-comment generation workflow: do not dispatch reviewer subagents from the executor. For Fonts Ninja-style PR review packs, the executor must avoid newly introduced duplicate helper stacks, tautological tests that merely re-call the delegated helper, self-mocking, domain rules in adapters/ports, domain objects outside `domain/`, dead APIs, weak public names, and functions that fail the plain-English read.
21
22
  - **Language-agnostic principles that ALWAYS bind (the TDD loop MUST satisfy them):** (1) no self-mocking of the SUT — stub/spy only injected collaborators, never the subject's own methods; (2) behavioral assertions on outcomes (return value, state, persisted rows, events, boundary calls) — never `toHaveBeenCalled*` on an internal helper as the only/primary assertion; (3) truthful names — a `get*` / `find*` that writes/inserts, or a name encoding the caller's use-case (`*ForInit`) or hiding a domain rule (`findValid*`), is a defect; (4) single-purpose functions ≤50 effective lines, plain-English readability. Self-mocking (1) — Enforced by `validators/detect_self_mock.py` (static); absent `qa/self-mock-*.json` sidecar BLOCKS at `validate-run.py`.
22
23
  - **Graceful degradation (codex / antigravity executor runtimes, or any runtime where the resolved coding-preflight pack files are absent or unreadable):** do NOT skip the gate — apply the agnostic principles above plus the project's own `CLAUDE.md` / `CONTRIBUTING` / formatter+lint config, and record `coding-conventions: resource-unavailable → applied <project rules + agnostic principles>` in the final report. Never claim a resource read that did not happen.
@@ -64,7 +64,7 @@ profile document.
64
64
  - **Enforced:** `validators/validate-run.py` `_validate_open_approval_blocker_provenance` requires `origin` and `userConfirmation` on every open approval blocker and rejects a lead-authored one raised with nobody to ask; `validators/validate_session_conformance.py` `_check_user_confirm_checkpoints` requires the matching `PROGRESS: user-confirm <C-NNN>` line for every row the report claims the user was asked about.
65
65
  - This contract is the single authority on brief consumption. Phase-specific addenda may *tighten* these rules but may not relax them.
66
66
  - Clarification request policy (shared — applies whenever a profile uses `## 1. Clarification Items`):
67
- - Schema-v2 final reports author `clarificationItems[]` in data.json; task-specific HTML renders the question and response controls directly from those IDs, and the full reading copy renders the same array as one headed section per row. The remaining table-layout rules describe schema-v1 compatibility and analysis-worker result tables only.
67
+ - Schema-v2 report narratives supply clarification rows; report assembly projects them into the final record, and task-specific HTML renders the question and response controls from the assembled IDs. The full reading copy renders the same rows as one headed section per row. The remaining table-layout rules describe schema-v1 compatibility and analysis-worker result tables only.
68
68
  - **Every row that is still `open` and carries `Blocks=approval` records two more fields.** Withholding approval is the most expensive thing a report does to a run, and until these fields existed a blocker could not be told apart from a question nobody had put to the user.
69
69
  - `origin` — who raised it. `worker-finding` (an analyser or verifier reached it on its own evidence), `material-gap` (neither the brief nor the codebase answers it), or `lead-directed` (the lead's own judgment, **including anything the lead instructed a worker to raise**). A lead that seeds its conclusion into a worker prompt and then reports the worker's agreement as an independent finding has mislabelled the row; that shape is what let one run block on a question its own lead had authored.
70
70
  - `userConfirmation` — what happened before the row was written. `asked-and-answered`, `asked-awaiting` (asked, no answer yet), or `deferred-no-interactive-session` (this run had no user to ask). Record an answer in `userInput` and move `status` to `answered`.
@@ -28,7 +28,7 @@ Do not scan holistically and stop when it "looks fine". Work the matrix exhausti
28
28
  1. **List every changed file:** `git diff --name-only <stage-base>..HEAD`. That list is your worklist — cover it to the end; no sampling.
29
29
  2. **Classify each file and select its rule set** from the conventions the preflight already loaded (do NOT re-derive the rules here — read them from the routed pack):
30
30
  - any source file → `clean-code.md`: truthful + standalone names, one identifier one meaning per file (a key parameter must not take the name a sibling signature gives the entity), plain-English summary test, single-purpose ≤50-line functions, DRY (incl. scattered domain literals + documented forks), YAGNI, no magic numbers, shallow nesting, comments explain why.
31
- - The YAGNI cell also counts callers, not just their existence: an abstraction layer this diff *introduces* — helper module, strategy / factory / builder, indirection or wrapper layer, interface or abstract base — with exactly **one** caller after the change is an inline candidate under `overview.md` core principle 2 ("name the second caller now, or inline"). Collapse it into that call site unless one of four exits applies and you record which: the approved plan declares it as a test seam or variation-point extraction, the project declares `architecture.style = hexagonal` and it is a port at the domain boundary, it breaks a real import cycle, or its single caller is a published package's public API. A function extracted only to get a body under the 50-line cap is not this finding.
31
+ - The YAGNI cell also counts callers, not just their existence: an abstraction layer this diff *introduces* — helper module, strategy / factory / builder, indirection or wrapper layer, interface or abstract base — with exactly **one** caller after the change is an inline candidate under `overview.md` core principle 2 ("name the second caller now, or inline"). Collapse it into that call site unless one of four exits applies and you record which: the approved plan declares it as a test seam or variation-point extraction, the project-context preflight projection declares hexagonal architecture and it is a port at the domain boundary, it breaks a real import cycle, or its single caller is a published package's public API. A function extracted only to get a body under the 50-line cap is not this finding.
32
32
  - The YAGNI cell is a grep, not an impression: for every identifier this diff adds — and every one whose last caller it removes — search the whole repository and name the caller you found. A declaration-only, test-only, or commented-out hit is not a caller. Zero callers means delete it here (or fold an added parameter back into its single call site), unless the approved plan reserves it for a named later stage or something outside project code calls it (framework entrypoint, implemented interface method, migration hook, published-package API) — in which case record that justification in the audit note. Fixing it now is cheaper than the verifier's Static design gate, where the same finding is blocking and fails the stage (`_implementation-verifier.md` "Caller-less identifier").
33
33
  - a file that wraps a third-party / library call → `clean-code.md` "Wrapping a third-party call": the wrapper adds behaviour the library does not already provide (read the installed library source before keeping a recovery branch — a `catch` repeating the library's own retry recovers nothing), no comment names a condition the call site does not establish, and a rethrow keeps the original error as `cause`.
34
34
  - a file that decides, mutates, or persists state → `clean-code.md` "Mutation and state boundaries": decide on the direct identifier rather than a status/flag proxy, capture before-state in one snapshot ahead of the mutating boundary, update only this work's owned fields on an existing row, re-read state before calling a zero-affected-rows write success or failure, put priority-between-inputs in a named domain function, and keep error messages to what was actually observed. Also check that no state union/enum was re-declared beside an authoritative one the domain or a dependency exports.
@@ -20,7 +20,7 @@ Every verifier acts as a QA gate, not just a diff reviewer. Trusting the executo
20
20
  Verifier obtains the QA command set from exactly two declared sources, in order — there is **no fallback to guessing tools from manifest files**.
21
21
 
22
22
  1. **Tier 1 — plan validation set (task-specific):** every command listed under the approved plan's `validation` block (pre / mid / post). The plan is the file at this prompt's `**Approved plan:**` anchor, scoped to the stage its `**Stage for this implementation run:**` anchor names; both are generated headers, so a missing one is `contract-violated`, never a value to infer.
23
- 2. **Tier 2 — project baseline (`project.json.qaCommands`):** the project's standing QA baseline declared in `<PROJECT_ROOT>/.okstra/project.json` under the `qaCommands` key. Schema (each category is an array of `{ "label", "cmd", "language"? }` objects):
23
+ 2. **Tier 2 — project baseline:** the project's standing QA baseline from the `Project QA Commands` section emitted by `okstra model-io project-context --project-root <PROJECT_ROOT> --task-ref <task-ref>`.
24
24
  ```json
25
25
  {
26
26
  "qaCommands": {
@@ -152,8 +152,8 @@ Re-running commands proves the diff *builds and passes*; it does NOT prove the d
152
152
 
153
153
  - **Scope (no silent sampling).** Enumerate every changed source/test file via `git diff --name-only <base>...HEAD` and review each one. Skipping a changed file silently is a `contract-violated` outcome. If a file's language has no reference and is not covered by the agnostic checks below, record `design-review skipped: <file> (language=<x> no reference)` — never pass it silently.
154
154
  - **Load the same conventions the executor used via the routed pack.** Use this worker prompt's `**Coding preflight pack:**` anchor header as the absolute path to the installed routed pack. Read `overview.md` first, then `clean-code.md`, then apply the router's three ordered stages: language, framework, architecture. In each stage, iterate every rule, treat a rule as matched when any listed condition is true, and accumulate every matching resource — including `frameworks/node-server.md` for server-side Node work and `architectures/hexagonal.md` for ports-and-adapters / NestJS-hex layouts. Degrade to the agnostic checks below when the resolved pack is unreadable, and record either `coding-conventions: resources=<...>` or `coding-conventions: resource-unavailable → applied <project rules + agnostic principles>`. The verifier does NOT inline language rules — it loads the same situation-specific resources as the executor preflight.
155
- - **Load the project's review rule packs.** A pack applies when either source names it — the task brief's `Source Material` / `Reporter Confirmations` cites its exact `SKILL.md` path, or `<PROJECT_ROOT>/.okstra/project.json` (the same file Tier 2's `qaCommands` comes from) lists it under `reviewRulePacks`. The two sources are a union, and a `reviewRulePacks` entry is the project's standing standard: it applies to this run whether or not the brief mentions it. Read only those files and the `references/*.md` files they directly name. Do not search parent directories or host skill catalogs. Apply the rules as an overlay on this static review, but do NOT dispatch extra reviewer agents unless the task explicitly configured them. Record `project-review-rules: <paths read>`, `project-review-rules: declared <path> unreadable`, or `project-review-rules: none declared or cited` in the worker result — an unreadable declared pack is a recorded gap, not a skip.
156
- - **Declared architecture style promotes the placement overlay from advisory to binding.** Read `<PROJECT_ROOT>/.okstra/project.json` — the same file Tier 2's `qaCommands` comes from take `architecture.style`, and record `architecture-style: <hexagonal|layered|none>` in the worker result next to the `coding-conventions:` line. A declared `hexagonal` counts the overlay as loaded even when none of the router's Stage 3 layout signals matched, so the **Hexagonal** blocking check below applies in full, and the concrete-adapter injection listed under Advisory findings is promoted to a blocking finding → verdict `FAIL`, not a `should-fix`. A declared `layered` has no pack resource; its binding invariant is direction — an upper layer may import a lower one, never the reverse — so a changed file whose import list reaches back up a layer, or around a layer boundary, is a blocking placement violation cited `path:line` from that import list. The `layered` half is worker judgement: no machine check reads layer names, so a missed reverse dependency is a missed finding, not a validator failure. A `none` style, an absent field, or an unreadable `project.json` leaves this section exactly as it is today — Stage 3 stays detection-driven and the placement items stay advisory. **Enforced:** `scripts/okstra_project/resolver.py` `resolve_architecture` reads this same field for the planning-side rule in `validators/validate-run.py` `_validate_variation_point_analysis`, and `_validate_verifier_fail_blocks_verdict` (cited under the DB gate below) keeps the resulting `FAIL` from being dropped during synthesis.
155
+ - **Load the project's review rule packs.** Run the project-context projection above and union its `Project Review Rule Packs` entries with exact `SKILL.md` paths cited by the task brief's `Source Material` / `Reporter Confirmations`. Read only those files and the `references/*.md` files they directly name. Do not search parent directories or host skill catalogs. Apply the rules as an overlay on this static review, but do NOT dispatch extra reviewer agents unless the task explicitly configured them. Record `project-review-rules: <paths read>`, `project-review-rules: declared <path> unreadable`, or `project-review-rules: none declared or cited` in the worker result — an unreadable declared pack is a recorded gap, not a skip.
156
+ - **Declared architecture style promotes the placement overlay from advisory to binding.** Take `Architecture style` from that projection and record `architecture-style: <hexagonal|layered|none>` in the worker result next to the `coding-conventions:` line. A declared `hexagonal` counts the overlay as loaded even when none of the router's Stage 3 layout signals matched, so the **Hexagonal** blocking check below applies in full, and the concrete-adapter injection listed under Advisory findings is promoted to a blocking finding → verdict `FAIL`, not a `should-fix`. A declared `layered` has no pack resource; its binding invariant is direction — an upper layer may import a lower one, never the reverse — so a changed file whose import list reaches back up a layer, or around a layer boundary, is a blocking placement violation cited `path:line` from that import list. The `layered` half is worker judgement: no machine check reads layer names, so a missed reverse dependency is a missed finding, not a validator failure. A `none` or absent projected style leaves Stage 3 detection-driven and the placement items advisory. **Enforced:** `scripts/okstra_project/resolver.py` `resolve_architecture` reads the same stored field for the planning-side rule in `validators/validate-run.py` `_validate_variation_point_analysis`, and `_validate_verifier_fail_blocks_verdict` keeps the resulting `FAIL` from being dropped during synthesis.
157
157
  - **Blocking checks (any hit → verdict `FAIL`, cited `path:line` + rule name, recommended fix recorded — the verifier does NOT apply it):**
158
158
  - **New duplication / DRY:** two or more newly added or meaningfully modified blocks implement the same helper stack, transform, or domain rule. Literal copy-paste is always blocking; semantically equivalent transforms across services are blocking unless the approved plan explicitly justified keeping them separate. Recommend the shared module location.
159
159
  - **Self-mocking:** a test for `Foo` stubs/spies a method on the `Foo` instance under test (`jest.spyOn(sut, ...)`, `spyOn(FooService.prototype, ...)` in `foo.*.spec.*`, `vi.mocked(sut)` + stub). Mocking injected collaborators is fine.
@@ -55,7 +55,7 @@ roles:
55
55
  - Worker verification procedure:
56
56
  - **Target confirmation:** analyse the injected target and nothing else. Read `verification-target.md` for the stage/report mapping and the complete diff stat. Prepare fixed that target and `validators/validate-run.py` `_validate_verification_target_match` re-checks the report against its digest, so the procedure to follow here is simply: if the worktree you can see does not match the injected target, record a `tool-failure` — never reselect a target.
57
57
  - **Evidence:** attach file:line, exact command + exit code, log excerpt, or MCP SELECT evidence to every finding. Mark a requirement as covered only when the cited artifact demonstrates it.
58
- - **Tier 1 and Tier 2 read-only validation:** Tier 1 is the originating brief/approved plan `validation` set; Tier 2 is `<PROJECT_ROOT>/.okstra/project.json` `qaCommands`. Do not auto-detect commands from package manifests. A missing tier is `qa-command not configured: <category>`. Before execution, reject commands containing source/lockfile mutation tokens such as `--fix`, `--write`, ` -w`, ` -u`, `--snapshot-update`, `INSTA_UPDATE=<not-no>`, `cargo update`, or `npm install` without `ci`; record the exact denied token. Tier 2 is already screened prepare refuses to start the run when `project.json` declares such a token (`okstra_ctl.run.validate_project_qa_commands`), so this check is the one that catches a Tier 1 command the brief or plan named.
58
+ - **Tier 1 and Tier 2 read-only validation:** Tier 1 is the originating brief/approved plan `validation` set. Tier 2 is the `Project QA Commands` section from `okstra model-io project-context --project-root <PROJECT_ROOT> --task-ref <task-ref>`. Do not auto-detect commands from package manifests. A missing tier is `qa-command not configured: <category>`. Before execution, reject commands containing source/lockfile mutation tokens such as `--fix`, `--write`, ` -w`, ` -u`, `--snapshot-update`, `INSTA_UPDATE=<not-no>`, `cargo update`, or `npm install` without `ci`; record the exact denied token. Tier 2 is already screened by prepare, so this check catches a Tier 1 command the brief or plan named.
59
59
  - **External QA outcome policy:** continue to attempt every in-scope Tier 3
60
60
  command. For an entry requiring `db`, `http`, or `external`, record non-PASS
61
61
  as a Tier 3 `advisory` command, add a user-owned Residual Risk and exact
@@ -132,6 +132,7 @@ roles:
132
132
  - `stageMap[].dependsOn` must form a DAG (no self-dependency, no unknown stage, no cycle), each row's `stepCount` must equal its stage's actual `stepwiseExecution` row count, and two `(none)`-dependency stages must not name the same file in their `exitContract` — they run as concurrent implementation runs in separate worktrees. **Enforced (S8/S4/S9):** same function.
133
133
  - Legacy candidate-comparison-only deliverable:
134
134
  - The report writer records the plan body in report narrative Markdown under `implementationPlanning`. Report assembly adds machine-owned `designPreparation`, `designSurfaceCoverage`, and `planBodyVerification`, then validates the completed `data.json` against `schemas/final-report-v3.0.schema.json`.
135
+ - **Narrative field names under `Implementation Planning` (the only ones accepted):** `Coverage Summary`, `Cross Project Dependencies`, `Decision Drafts`, `Dependency Migration Risk`, `Direction Invalidation`, `Direction Realization`, `Incremental Decision`, `Option Candidates`, `Outcome`, `Planning Contract`, `Recommended Option`, `Requirement Coverage`, `Rollback Strategy`, `Routing`, `Selected Direction Ref`, `Skipped ADR Candidates`, `Stage Map`, `Stages`, `Stepwise Execution`, `Supersession Ledger`, `Tradeoff Matrix`, `User Narrative`, `Validation Checklist`, `Variation Point Analysis`. The `## 5.5.<i>` headings, the `step | action | ...` tables, and the `Slice value:` / `Test case (...):` lines described below are the shape of the **rendered report a reader sees**, not narrative syntax — the narrative carries the same content as `- **Field**` / `- Item <N>` / `> value` rows under these names. `Design Preparation` and `Plan Body Verification` belong to report assembly and are rejected here.
135
136
  - Legacy candidate-comparison requires at least two implementation options. **Each option must include**:
136
137
  - **File Structure**: an explicit list of files to create / modify / delete with each file's responsibility (one-line each). Use the form `Create: path — responsibility` / `Modify: path:line-range — change summary` / `Delete: path — reason`. Write every `path` in full and `<PROJECT_ROOT>`-relative — never ellipsis-abbreviated (`…` / `...` / a trailing `/…`); an abbreviated path does not resolve and is rejected by plan-body verification as a kind-b path mismatch.
137
138
  - **Two-tier change description.** Each `fileStructure` row carries `summary` **and** optional `details`, and they are not interchangeable:
@@ -210,13 +211,18 @@ roles:
210
211
  - **Why enumeration is the scale gate.** The number of stages a plan carries is not bounded by any threshold — a genuinely large requirement may need many, and okstra does not guess a ratio. What IS bounded is how cheaply a plan can *claim* coverage of them: one `Stages 1-64` cell used to satisfy the reverse check for the whole map while the planner confirmed nothing, so scale grew for free. Enumeration prices it — every stage you claim costs you the act of naming it and asking whether this requirement is really satisfied there. A plan that cannot bring itself to type the numbers is telling you the stages are not all needed. The typing is the confirmation, so do not batch it mechanically: a row listing `Stages 1, 2, 3, ..., 12` you did not check one by one is the same rubber stamp with more characters.
211
212
  - Because that reader only sees prose, it still cannot tell a citation from a mention: `Stage 1 (superseded by Stage 2)` counts Stage 1 as cited. Cite the stages a requirement is actually satisfied by, not stages merely mentioned.
212
213
  - **Requirement Coverage (mandatory, §5.5.8):** selected-direction plans preserve the original requirement IDs and link each row to `stageRefs`, `stepRefs`, `validationRefs`, and `fileRefs`; exact forward and reverse coverage is enforced by `validate_selected_direction_plan`. Legacy candidate-comparison plans retain one `R-NNN` row per concrete requirement and the existing Option Candidate plus Stage/Step `coveredBy` semantics. The exact `P-Req-*` queue comes from `scripts/okstra_ctl/plan_items.py` in both branches.
214
+ - **Every coverage and validation row states the stages it belongs to.** Put the stage numbers in `stageRefs` on each `### Requirement Coverage` row and each `### Validation Checklist` row — the same integers the row's `Covered by` prose or its check already names. `coveredBy` stays as it is; this is the machine-readable form of the same fact.
215
+
216
+ Prose is not a substitute. Reading a stage out of `Covered by` needs a regular expression over a sentence, and a gate cannot be scored on that. Without the field, every requirement and validation row counts against whatever stage is being started, including rows that belong only to stages already finished or not yet begun — measured on one run, that is 9 of 13 blockers, 6 of them on frozen stages no amount of planning can close.
217
+
218
+ Omit `stageRefs` only for a row that genuinely spans no stage. An omitted field is read as "applies to every stage", which is the safe reading and the current behaviour, so an older plan without it is unaffected.
213
219
  - **Legacy compatibility details:** assign `R-001`, `R-002`, ... in source order. `Source` uses the existing `brief:` / `derived:` / `contract:` grammar. A `covered` row names the specific Option Candidate and Stage/Step. A gap, blocked clarification, or unaccepted deviation keeps the gate non-passing. `validators/validate-run.py` retains `_validate_requirement_coverage_covered_by`, `_validate_requirement_deviations`, `_validate_gate_blocked_by`, and `_independent_coverage_blockers` enforcement for this branch.
214
220
  - **Review-rule compliance plan:** when a project-local review rule pack is found, the chosen realization MUST include the design implication of those rules in its File Structure / interfaces / blast-radius notes. For any helper or data transform used by more than one changed service, the plan must either place it in a shared module or explicitly justify why duplication is intentional. For any test step, the plan must state the observable behavior being asserted, not the internal collaborator call being pinned. For any exported/public method added or renamed, the step must carry the intended noun/side-effect semantics so implementation names can be reviewed before code is written.
215
221
  - the report record MUST include `frontmatter.approved: false` (report-writer always emits the unflipped value). The user authorises the next `implementation` run with `--approve` or the in-session wizard. Do NOT recreate any `User Approval Request` body block — the validator fails reports that contain one (see `validators/validate-run.py` deprecated patterns).
216
222
  - Selected-direction plans omit `implementation-option:` because `selectedDirectionRef` already fixes the direction; the legacy-only selector rule is owned by the legacy deliverable section above.
217
223
  - **the frontmatter `approved: false` line is rendered unconditionally; if the plan-body verification gate (§5.5.9) returns `blocked-by-disagreement` or `aborted-non-result`, the writer MUST keep `approved: false` and the validator refuses any report that ships with `approved: true` under such a gate result.**
218
224
  - every ambiguity flagged during pre-planning that the user must resolve before approval registered as a `Blocks=approval` row in the `## 1. Clarification Items` table (the unified table is the single home for these — the "no separate `Open Questions` block" rule is in the shared `_common-contract.md` clarification policy)
219
- - **Exact plan-item queue (BLOCKING).** Run `okstra plan-items extract --narrative <report-narrative.md> --output <state>/plan-items-....json`, place the persisted `items[]` verbatim in the verifier prompt, then run `okstra plan-items validate --narrative <report-narrative.md> --items <state>/plan-items-....json`. Do not freely summarise, select, omit, reorder, or renumber the queue. Prompt headings use the compact `subject` and include the lossless `payload`. For every item, ask:
225
+ - **Exact plan-item queue (BLOCKING).** Run `okstra plan-items prepare --narrative <report-narrative.md> --run-manifest <run-manifest>`, place `okstra plan-items prompt --run-manifest <run-manifest>` output verbatim in the verifier prompt, then run `okstra plan-items validate-prepared --narrative <report-narrative.md> --run-manifest <run-manifest>`. Python resolves the one convergence-owned state path from that run identity. Do not freely summarise, select, omit, reorder, or renumber the queue. Prompt headings use the compact subject and include the lossless payload. For every item, ask:
220
226
 
221
227
  ```text
222
228
  What concrete false-positive input, failure ordering, or omitted dependency
@@ -250,7 +256,7 @@ roles:
250
256
  3. **Internal consistency** — the chosen realization's file list, interfaces, stages, and validation must agree on paths, names, and signatures. A symbol called `clearLayers()` in one field and `clearFullLayers()` in the steps is a bug.
251
257
  4. **Ambiguity check** — any requirement that could be read two ways must be made explicit or moved to the `## 1. Clarification Items` table as a `Blocks=approval` row.
252
258
  5. **Scope check** — if the plan now spans multiple independent subsystems, split it into separate planning runs rather than shipping an oversized plan. Then walk the plan in the expansion direction: for every stage, name the Requirement Coverage row that demanded it, and for every requirement row, read its `Source` cell as a skeptic — does the cited brief heading actually exist, and does a `derived:` rationale state a real technical consequence rather than a preference? Move anything that fails to a `Blocks=approval` clarification row.
253
- 6. **Review-rule preflight check** — when a project review rule pack applies (cited by the brief, or declared in `project.json` `reviewRulePacks` see the preflight rule above), map each relevant rule to the chosen realization. Reject the draft if it knowingly creates a violation that the later PR reviewer would flag, unless the plan records a specific rationale and follow-up. In particular, scan for repeated helper stacks across planned files, tests that assert delegation to the same calculator/helper they exercise, public names that hide side effects, domain rules placed in repositories/adapters, and APIs made dead by this change.
259
+ 6. **Review-rule preflight check** — when a project review rule pack applies (cited by the brief, or listed by `okstra model-io project-context --project-root <PROJECT_ROOT> --task-ref <task-ref>`), map each relevant rule to the chosen realization. Reject the draft if it knowingly creates a violation that the later PR reviewer would flag, unless the plan records a specific rationale and follow-up. In particular, scan for repeated helper stacks across planned files, tests that assert delegation to the same calculator/helper they exercise, public names that hide side effects, domain rules placed in repositories/adapters, and APIs made dead by this change.
254
260
  7. **Plan-body verification reconciliation (BLOCKING for implementation-planning).** For every §5.5.9 `planItems[]` entry whose verdicts make it `majority-disagree`, set that item's `clarificationId` to a `C-<N>` row that MUST exist in `## 1. Clarification Items` with `Kind` chosen per the standard policy and `Blocks=approval`. **Enforced:** `validators/validate-run.py` `_validate_plan_body_clarification_matching` recomputes each item's class and fails when a majority-disagree item has no `clarificationId`, or its `clarificationId` is dangling / points at a non-`approval` row. For `partial-consensus` and `dissent-isolated` plan-items, the dissenting opinion lives in §5.5.9 `Dissent log` and is NOT promoted to §5.
255
261
  8. **Stage Map self-check** — for every stage, count the effective rows of its `Stepwise Execution Order` table by hand; reject the draft if any stage exceeds 8. Confirm each stage declares a non-empty `Slice value:` and `Acceptance:` line, the three `Test case (success|boundary|failure):` lines (or carries a `TDD exemption:` line), and that its first step `action` starts with `RED:` with a later `GREEN:` — this is what validator S10 enforces, including S10d on the test-case lines. Read each stage's three test-case lines as a reviewer: reject any that restates the happy path in all three slots, leaves `boundary` blank, or writes `N/A` where a real edge input exists. Walk the `depends-on` graph and confirm it is a DAG (no cycle, no self-reference). For each `depends-on` link, confirm it encodes a real data/contract dependency — do NOT add links to serialise unrelated work, and do NOT split a stage merely to create more parallel stages. **Parallel-safety:** for every pair of `depends-on (none)` stages, confirm their `Stage Exit Contract` predicted file sets are disjoint; if they share a file, merge them or add a `depends-on` link (validator S9 rejects overlap). **Project-boundary:** confirm no stage mixes edits from two projects (different repo/`PROJECT_ROOT` or different top-level deployable module); if any stage does, split it per project. For multi-project plans, confirm each stage's `title` carries its `[<project>]` tag and the `Cross-project parallelism:` line under the table records the parallel-vs-sequenced determination (with the forcing dependency) for every project pair; for cross-repo work, confirm it is split into separate per-repo runs (required — one run structurally cannot touch another repo) rather than crammed into one task's stages.
256
262
  9. **Cross-project dependency check** — confirm you have not missed a dependency on another repo / another top-level deployable module / a published package. If `dependencyMigrationRisk` has a `kind: cross-project` row, confirm a matching `direction: upstream-precondition` `XP-NNN` row exists in `crossProjectDependencies`, and re-read as a reviewer whether its `requiredWork` is the concrete work the other side must actually build rather than an abstract phrase ("other side's work done") — validator S only checks existence, so concreteness is the self-review's responsibility. Confirm cross-repo work is split into a separate run + XP row instead of being crammed into one task's stages, and that the cross-project substance is not duplicated in `§3 Recommended Next Steps` but lives only in `§5.4 Cross-Project Dependencies`.
@@ -16,7 +16,6 @@ roles: []
16
16
  - The lead authors the final-report file directly (no `Report writer worker` dispatch). The report still conforms to the standard `templates/reports/final-report-v2.template.md` structure, including the `## 5.6 Release Handoff Deliverables` section.
17
17
  - The shared anti-escalation rule from the common contract still applies: do not start any other lifecycle phase from inside this run.
18
18
  - The shared "authority & permissions assumption" rule from the common contract still applies: assume the user holds every permission needed; do not block on hypothetical approvals.
19
- - The shared "MCP read-only" rule still applies if the brief lists MCP servers, though most release-handoff runs do not use MCP.
20
19
  - Pre-handoff entry gate (mandatory — refuse to start if any item fails):
21
20
  - the run's input document (`release-handoff-input.md`, generated by prepare in place of a task brief — briefs belong to entry phases only) carries a `## Source Verification Report` section with `Mode`, `Stages`, and one table row per cited `final-verification` final-report. The run context exposes the same selection as `HANDOFF_MODE` (`whole-task` | `stage-group`) and `HANDOFF_STAGES` (csv, empty for whole-task).
22
21
  - **whole-task mode** (`HANDOFF_MODE=whole-task`): the lead opens the cited report and confirms its `verificationScope` is `whole-task` and its verdict is release-ready — `Verdict Token` exactly `accepted`, or exactly `conditional-accept` with every **Conditional Acceptance Condition** row declaring `blocksReleaseHandoff: false`. The rule lives in `okstra_ctl.release_gate.release_handoff_allowed`; the lead reads the report against it and does not invent a third case.
@@ -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
 
@@ -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,