okstra 0.186.7 → 0.187.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (541) hide show
  1. package/README.md +1 -1
  2. package/dist/cli-registry.d.mts +88 -1
  3. package/dist/cli-registry.mjs +68 -111
  4. package/dist/cli-registry.mjs.map +1 -1
  5. package/dist/commands/execute/render-bundle.mjs +0 -1
  6. package/dist/commands/execute/render-bundle.mjs.map +1 -1
  7. package/dist/commands/execute/run.mjs +8 -3
  8. package/dist/commands/execute/run.mjs.map +1 -1
  9. package/dist/commands/lifecycle/check-project.mjs +1 -14
  10. package/dist/commands/lifecycle/check-project.mjs.map +1 -1
  11. package/dist/commands/lifecycle/config.mjs +38 -40
  12. package/dist/commands/lifecycle/config.mjs.map +1 -1
  13. package/dist/commands/lifecycle/doctor.d.mts +22 -7
  14. package/dist/commands/lifecycle/doctor.mjs +77 -49
  15. package/dist/commands/lifecycle/doctor.mjs.map +1 -1
  16. package/dist/commands/lifecycle/install.d.mts +12 -10
  17. package/dist/commands/lifecycle/install.mjs +104 -39
  18. package/dist/commands/lifecycle/install.mjs.map +1 -1
  19. package/dist/commands/lifecycle/paths.mjs +8 -3
  20. package/dist/commands/lifecycle/paths.mjs.map +1 -1
  21. package/dist/commands/lifecycle/preflight.mjs +2 -1
  22. package/dist/commands/lifecycle/preflight.mjs.map +1 -1
  23. package/dist/commands/lifecycle/setup.mjs +22 -36
  24. package/dist/commands/lifecycle/setup.mjs.map +1 -1
  25. package/dist/commands/lifecycle/uninstall.d.mts +4 -2
  26. package/dist/commands/lifecycle/uninstall.mjs +59 -15
  27. package/dist/commands/lifecycle/uninstall.mjs.map +1 -1
  28. package/dist/commands/memory/memory.mjs +7 -1
  29. package/dist/commands/memory/memory.mjs.map +1 -1
  30. package/dist/lib/helper-scripts.d.mts +1 -1
  31. package/dist/lib/helper-scripts.mjs +10 -18
  32. package/dist/lib/helper-scripts.mjs.map +1 -1
  33. package/dist/lib/host-config.d.mts +72 -0
  34. package/dist/lib/host-config.mjs +404 -0
  35. package/dist/lib/host-config.mjs.map +1 -0
  36. package/dist/lib/host-registry-client.d.mts +2 -2
  37. package/dist/lib/host-registry-client.mjs +0 -3
  38. package/dist/lib/host-registry-client.mjs.map +1 -1
  39. package/dist/lib/install-assets.d.mts +1 -0
  40. package/dist/lib/install-assets.mjs +4 -0
  41. package/dist/lib/install-assets.mjs.map +1 -1
  42. package/dist/lib/proc.d.mts +2 -0
  43. package/dist/lib/proc.mjs +12 -0
  44. package/dist/lib/proc.mjs.map +1 -1
  45. package/dist/lib/python-command.d.mts +2 -0
  46. package/dist/lib/python-command.mjs +52 -0
  47. package/dist/lib/python-command.mjs.map +1 -0
  48. package/dist/lib/python-helper.d.mts +2 -5
  49. package/dist/lib/python-helper.mjs +3 -52
  50. package/dist/lib/python-helper.mjs.map +1 -1
  51. package/dist/lib/runtime-payload.d.mts +23 -0
  52. package/dist/lib/runtime-payload.mjs +57 -0
  53. package/dist/lib/runtime-payload.mjs.map +1 -0
  54. package/dist/lib/types.d.mts +24 -13
  55. package/docs/architecture/storage-model.md +21 -5
  56. package/docs/architecture.md +37 -36
  57. package/docs/cli.md +55 -76
  58. package/docs/coding-rules.md +295 -0
  59. package/docs/container.md +10 -36
  60. package/docs/contributor-change-matrix.md +1 -1
  61. package/docs/for-ai/skills/okstra-code-review.md +0 -1
  62. package/docs/for-ai/skills/okstra-container-build.md +8 -41
  63. package/docs/for-ai/skills/okstra-inspect.md +4 -4
  64. package/docs/for-ai/skills/okstra-manager.md +2 -2
  65. package/docs/for-ai/skills/okstra-rollup.md +0 -1
  66. package/docs/for-ai/skills/okstra-run.md +3 -3
  67. package/docs/for-ai/skills/okstra-user-response.md +0 -1
  68. package/docs/performance-improvement-plan-v2.md +1 -1
  69. package/docs/project-structure-overview.md +62 -62
  70. package/docs/task-process/README.md +3 -3
  71. package/docs/task-process/common-flow.md +1 -1
  72. package/docs/task-process/error-analysis.md +3 -3
  73. package/docs/task-process/implementation-planning.md +3 -1
  74. package/docs/task-process/release-handoff.md +1 -1
  75. package/package.json +3 -2
  76. package/runtime/BUILD.json +2 -2
  77. package/runtime/agents/workers/claude-worker.md +6 -11
  78. package/runtime/agents/workers/report-writer-worker.md +1 -1
  79. package/runtime/bin/lib/okstra/cli.sh +4 -0
  80. package/runtime/bin/lib/okstra/globals.sh +2 -0
  81. package/runtime/bin/lib/okstra/interactive.sh +26 -173
  82. package/runtime/bin/lib/okstra/project-resolver.sh +23 -61
  83. package/runtime/bin/lib/okstra/usage.sh +3 -3
  84. package/runtime/bin/okstra-compact-reminder.sh +1 -5
  85. package/runtime/bin/okstra-error-log.py +19 -2
  86. package/runtime/bin/okstra-import-check.py +26 -0
  87. package/runtime/bin/okstra-inject-report-index.py +5 -4
  88. package/runtime/bin/okstra-provider-exec.py +22 -17
  89. package/runtime/bin/okstra-render-final-report.py +16 -6
  90. package/runtime/bin/okstra-render-report-views.py +25 -18
  91. package/runtime/bin/okstra-report-translate.py +30 -18
  92. package/runtime/bin/okstra-spawn-followups.py +4 -0
  93. package/runtime/bin/okstra-token-usage.py +3 -0
  94. package/runtime/bin/okstra.sh +4 -2
  95. package/runtime/bin/okstra_bootstrap.py +58 -0
  96. package/runtime/prompts/coding-preflight/overview.md +1 -1
  97. package/runtime/prompts/duties/planning-worker.md +1 -1
  98. package/runtime/prompts/launch.template.md +35 -9
  99. package/runtime/prompts/lead/convergence.md +96 -87
  100. package/runtime/prompts/lead/okstra-lead-contract.md +63 -12
  101. package/runtime/prompts/lead/plan-body-verification.md +65 -46
  102. package/runtime/prompts/lead/report-writer.md +34 -4
  103. package/runtime/prompts/lead/team-contract.md +4 -1
  104. package/runtime/prompts/profiles/_clarification-recommendation.md +2 -1
  105. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
  106. package/runtime/prompts/profiles/_common-contract.md +3 -3
  107. package/runtime/prompts/profiles/_coverage-critic.md +1 -1
  108. package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
  109. package/runtime/prompts/profiles/_implementation-executor.md +3 -1
  110. package/runtime/prompts/profiles/_implementation-verifier.md +16 -2
  111. package/runtime/prompts/profiles/error-analysis.md +4 -3
  112. package/runtime/prompts/profiles/final-verification.md +2 -2
  113. package/runtime/prompts/profiles/implementation-planning.md +22 -17
  114. package/runtime/prompts/profiles/implementation.md +3 -2
  115. package/runtime/prompts/profiles/requirements-discovery.md +2 -1
  116. package/runtime/prompts/wizard/prompts.ko.json +29 -8
  117. package/runtime/python/okstra_ctl/__init__.py +6 -27
  118. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +4 -5
  119. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +21 -2
  120. package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +3 -12
  121. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +40 -22
  122. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +14 -21
  123. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +37 -20
  124. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +86 -14
  125. package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +12 -12
  126. package/runtime/python/okstra_ctl/adapters/runtime/cmux.py +4 -7
  127. package/runtime/python/okstra_ctl/agent/__init__.py +1 -0
  128. package/runtime/python/okstra_ctl/{agent_activity.py → agent/activity.py} +12 -1
  129. package/runtime/python/okstra_ctl/{agent_invocation.py → agent/invocation.py} +34 -5
  130. package/runtime/python/okstra_ctl/agent/prompt_cli/__init__.py +19 -0
  131. package/runtime/python/okstra_ctl/agent/prompt_cli/__main__.py +11 -0
  132. package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +215 -0
  133. package/runtime/python/okstra_ctl/agent/prompt_cli/dynamic_verifier.py +155 -0
  134. package/runtime/python/okstra_ctl/agent/prompt_cli/emit.py +66 -0
  135. package/runtime/python/okstra_ctl/agent/prompt_cli/inputs.py +138 -0
  136. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +408 -0
  137. package/runtime/python/okstra_ctl/agent/prompt_cli/results.py +197 -0
  138. package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +107 -0
  139. package/runtime/python/okstra_ctl/analysis_packet.py +47 -4
  140. package/runtime/python/okstra_ctl/approval_decisions.py +260 -8
  141. package/runtime/python/okstra_ctl/assignment_resolver.py +0 -12
  142. package/runtime/python/okstra_ctl/attempt_evidence.py +12 -24
  143. package/runtime/python/okstra_ctl/blocking_checks.py +251 -0
  144. package/runtime/python/okstra_ctl/brief_frontmatter.py +0 -7
  145. package/runtime/python/okstra_ctl/clarification_items/__init__.py +102 -0
  146. package/runtime/python/okstra_ctl/clarification_items/carry.py +224 -0
  147. package/runtime/python/okstra_ctl/clarification_items/dispositions.py +135 -0
  148. package/runtime/python/okstra_ctl/clarification_items/parsing.py +253 -0
  149. package/runtime/python/okstra_ctl/clarification_items/rows.py +115 -0
  150. package/runtime/python/okstra_ctl/clarification_items/scan.py +215 -0
  151. package/runtime/python/okstra_ctl/clarification_items/sidecars.py +211 -0
  152. package/runtime/python/okstra_ctl/cmux.py +38 -31
  153. package/runtime/python/okstra_ctl/code_review_target.py +163 -1
  154. package/runtime/python/okstra_ctl/conformance.py +23 -0
  155. package/runtime/python/okstra_ctl/container.py +38 -421
  156. package/runtime/python/okstra_ctl/context_cost.py +16 -2
  157. package/runtime/python/okstra_ctl/contract_graph_cli.py +7 -0
  158. package/runtime/python/okstra_ctl/convergence.py +101 -15
  159. package/runtime/python/okstra_ctl/convergence_critic_prompt.py +345 -0
  160. package/runtime/python/okstra_ctl/convergence_engine.py +382 -70
  161. package/runtime/python/okstra_ctl/convergence_store.py +5 -29
  162. package/runtime/python/okstra_ctl/design_prep.py +108 -12
  163. package/runtime/python/okstra_ctl/design_snapshot.py +11 -1
  164. package/runtime/python/okstra_ctl/design_surfaces.py +29 -2
  165. package/runtime/python/okstra_ctl/dispatch_core.py +102 -25
  166. package/runtime/python/okstra_ctl/dispatch_state.py +268 -39
  167. package/runtime/python/okstra_ctl/doctor_cli.py +48 -0
  168. package/runtime/python/okstra_ctl/domain/worker_exec.py +10 -5
  169. package/runtime/python/okstra_ctl/domain/worker_presentation.py +21 -1
  170. package/runtime/python/okstra_ctl/domain/worker_stream.py +52 -21
  171. package/runtime/python/okstra_ctl/domain/write_policy.py +219 -0
  172. package/runtime/python/okstra_ctl/entrypoints/hosts.py +9 -2
  173. package/runtime/python/okstra_ctl/error_log_core.py +1 -1
  174. package/runtime/python/okstra_ctl/error_report.py +14 -0
  175. package/runtime/python/okstra_ctl/error_zip.py +18 -2
  176. package/runtime/python/okstra_ctl/execution_identity.py +100 -7
  177. package/runtime/python/okstra_ctl/execution_manifest.py +67 -20
  178. package/runtime/python/okstra_ctl/execution_mutation_audit.py +90 -8
  179. package/runtime/python/okstra_ctl/final_report_paths.py +17 -0
  180. package/runtime/python/okstra_ctl/final_report_schema.py +90 -16
  181. package/runtime/python/okstra_ctl/git_reconcile.py +22 -2
  182. package/runtime/python/okstra_ctl/handoff.py +24 -1
  183. package/runtime/python/okstra_ctl/ids.py +6 -16
  184. package/runtime/python/okstra_ctl/implementation_direction.py +142 -33
  185. package/runtime/python/okstra_ctl/implementation_options.py +25 -11
  186. package/runtime/python/okstra_ctl/implementation_outcome.py +5 -2
  187. package/runtime/python/okstra_ctl/improvement_lenses.py +0 -14
  188. package/runtime/python/okstra_ctl/incremental_carry.py +69 -8
  189. package/runtime/python/okstra_ctl/incremental_scope.py +173 -12
  190. package/runtime/python/okstra_ctl/index.py +2 -2
  191. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +48 -8
  192. package/runtime/python/okstra_ctl/interactive_cli.py +223 -0
  193. package/runtime/python/okstra_ctl/json_boundary.py +10 -0
  194. package/runtime/python/okstra_ctl/listing.py +0 -91
  195. package/runtime/python/okstra_ctl/locks.py +5 -19
  196. package/runtime/python/okstra_ctl/log_report.py +14 -0
  197. package/runtime/python/okstra_ctl/manager_cli.py +22 -3
  198. package/runtime/python/okstra_ctl/manager_launch.py +4 -8
  199. package/runtime/python/okstra_ctl/material.py +2 -2
  200. package/runtime/python/okstra_ctl/migrate.py +23 -1
  201. package/runtime/python/okstra_ctl/model_cli.py +19 -5
  202. package/runtime/python/okstra_ctl/model_discovery.py +46 -47
  203. package/runtime/python/okstra_ctl/model_io/__init__.py +1 -0
  204. package/runtime/python/okstra_ctl/model_io/lines.py +163 -0
  205. package/runtime/python/okstra_ctl/model_io/references.py +370 -0
  206. package/runtime/python/okstra_ctl/model_io/renderers.py +498 -0
  207. package/runtime/python/okstra_ctl/model_io_cli.py +30 -945
  208. package/runtime/python/okstra_ctl/model_pool.py +10 -6
  209. package/runtime/python/okstra_ctl/models.py +8 -8
  210. package/runtime/python/okstra_ctl/mutation_recovery.py +5 -63
  211. package/runtime/python/okstra_ctl/next_phase.py +121 -69
  212. package/runtime/python/okstra_ctl/pane_reclaim.py +23 -0
  213. package/runtime/python/okstra_ctl/pane_title.py +11 -8
  214. package/runtime/python/okstra_ctl/path_hints.py +36 -11
  215. package/runtime/python/okstra_ctl/paths.py +76 -8
  216. package/runtime/python/okstra_ctl/plan_items.py +132 -16
  217. package/runtime/python/okstra_ctl/plan_items_cli.py +486 -36
  218. package/runtime/python/okstra_ctl/plan_run_root.py +1 -1
  219. package/runtime/python/okstra_ctl/plan_validate_cli.py +51 -0
  220. package/runtime/python/okstra_ctl/plan_verify_cli.py +80 -0
  221. package/runtime/python/okstra_ctl/prepare_error.py +20 -0
  222. package/runtime/python/okstra_ctl/prior_planning.py +157 -0
  223. package/runtime/python/okstra_ctl/profile_show.py +18 -1
  224. package/runtime/python/okstra_ctl/project_setup_cli.py +140 -0
  225. package/runtime/python/okstra_ctl/recap.py +22 -1
  226. package/runtime/python/okstra_ctl/reconcile.py +84 -0
  227. package/runtime/python/okstra_ctl/registry/host_registry.py +18 -1
  228. package/runtime/python/okstra_ctl/render.py +98 -32
  229. package/runtime/python/okstra_ctl/render_final_report.py +3 -7
  230. package/runtime/python/okstra_ctl/report_assembly.py +297 -7
  231. package/runtime/python/okstra_ctl/report_contract.py +30 -7
  232. package/runtime/python/okstra_ctl/report_finalize.py +206 -15
  233. package/runtime/python/okstra_ctl/report_html/common.py +2 -2
  234. package/runtime/python/okstra_ctl/report_html/render.py +8 -12
  235. package/runtime/python/okstra_ctl/report_language.py +8 -4
  236. package/runtime/python/okstra_ctl/report_narrative.py +90 -3
  237. package/runtime/python/okstra_ctl/report_projections.py +8 -2
  238. package/runtime/python/okstra_ctl/report_synthesis_packet.py +258 -1
  239. package/runtime/python/okstra_ctl/report_translation.py +2 -35
  240. package/runtime/python/okstra_ctl/report_views.py +4 -22
  241. package/runtime/python/okstra_ctl/resolve_task_key.py +13 -0
  242. package/runtime/python/okstra_ctl/rollup.py +13 -0
  243. package/runtime/python/okstra_ctl/run.py +639 -138
  244. package/runtime/python/okstra_ctl/run_audit.py +13 -0
  245. package/runtime/python/okstra_ctl/run_index_row.py +0 -12
  246. package/runtime/python/okstra_ctl/seeding.py +21 -11
  247. package/runtime/python/okstra_ctl/sequence.py +1 -1
  248. package/runtime/python/okstra_ctl/session.py +64 -14
  249. package/runtime/python/okstra_ctl/set_work_status.py +18 -0
  250. package/runtime/python/okstra_ctl/stage_integrate.py +15 -1
  251. package/runtime/python/okstra_ctl/stage_ledger.py +1 -1
  252. package/runtime/python/okstra_ctl/stage_map.py +57 -11
  253. package/runtime/python/okstra_ctl/stage_map_cli.py +83 -0
  254. package/runtime/python/okstra_ctl/stage_map_view.py +65 -0
  255. package/runtime/python/okstra_ctl/stage_targets.py +3 -12
  256. package/runtime/python/okstra_ctl/task_list_cli.py +138 -0
  257. package/runtime/python/okstra_ctl/task_show_cli.py +66 -0
  258. package/runtime/python/okstra_ctl/task_target.py +4 -16
  259. package/runtime/python/okstra_ctl/team.py +49 -2
  260. package/runtime/python/okstra_ctl/time_report.py +17 -2
  261. package/runtime/python/okstra_ctl/usage_report.py +11 -0
  262. package/runtime/python/okstra_ctl/user_response.py +55 -218
  263. package/runtime/python/okstra_ctl/user_response_values.py +242 -0
  264. package/runtime/python/okstra_ctl/validation_contract.py +3 -0
  265. package/runtime/python/okstra_ctl/verification_target.py +68 -0
  266. package/runtime/python/okstra_ctl/wizard.py +457 -92
  267. package/runtime/python/okstra_ctl/worker_artifacts.py +75 -16
  268. package/runtime/python/okstra_ctl/worker_audit_check.py +22 -0
  269. package/runtime/python/okstra_ctl/worker_dispatch.py +21 -0
  270. package/runtime/python/okstra_ctl/worker_liveness.py +33 -0
  271. package/runtime/python/okstra_ctl/worker_prompt_body.py +15 -2
  272. package/runtime/python/okstra_ctl/worker_prompt_contract.py +98 -7
  273. package/runtime/python/okstra_ctl/worker_prompt_headers.py +24 -2
  274. package/runtime/python/okstra_ctl/worker_prompt_policy.py +18 -3
  275. package/runtime/python/okstra_ctl/worker_request.py +2 -1
  276. package/runtime/python/okstra_ctl/worker_runner.py +16 -9
  277. package/runtime/python/okstra_ctl/worker_state.py +16 -1
  278. package/runtime/python/okstra_ctl/workflow.py +18 -1
  279. package/runtime/python/okstra_ctl/worktree/__init__.py +92 -0
  280. package/runtime/python/okstra_ctl/worktree/cleanliness.py +89 -0
  281. package/runtime/python/okstra_ctl/worktree/decisions.py +127 -0
  282. package/runtime/python/okstra_ctl/worktree/git_ops.py +165 -0
  283. package/runtime/python/okstra_ctl/worktree/linking.py +249 -0
  284. package/runtime/python/okstra_ctl/worktree/naming.py +91 -0
  285. package/runtime/python/okstra_ctl/worktree/provision.py +385 -0
  286. package/runtime/python/okstra_ctl/worktree/sync_config.py +181 -0
  287. package/runtime/python/okstra_ctl/worktree_cli.py +75 -0
  288. package/runtime/python/okstra_ctl/worktree_lookup_cli.py +39 -0
  289. package/runtime/python/okstra_ctl/worktree_registry.py +7 -0
  290. package/runtime/python/okstra_ctl/worktree_status_cli.py +53 -0
  291. package/runtime/python/okstra_ctl/write_policy.py +90 -213
  292. package/runtime/python/okstra_project/__init__.py +0 -4
  293. package/runtime/python/okstra_project/dirs.py +2 -2
  294. package/runtime/python/okstra_project/phase_pointer.py +84 -0
  295. package/runtime/python/okstra_project/slug.py +24 -0
  296. package/runtime/python/okstra_project/state.py +22 -230
  297. package/runtime/python/okstra_token_usage/claude.py +9 -1
  298. package/runtime/python/okstra_token_usage/cli.py +3 -1
  299. package/runtime/python/okstra_token_usage/collect.py +71 -10
  300. package/runtime/python/okstra_token_usage/cursor.py +2 -0
  301. package/runtime/python/okstra_token_usage/grok.py +1 -5
  302. package/runtime/python/okstra_token_usage/paths.py +26 -0
  303. package/runtime/python/okstra_token_usage/pricing.py +19 -7
  304. package/runtime/schemas/convergence-critic-results-v1.0.schema.json +5 -0
  305. package/runtime/schemas/execution-manifest-v2.schema.json +10 -10
  306. package/runtime/schemas/final-report-v2.0.schema.json +9 -26
  307. package/runtime/schemas/final-report-v3.0.schema.json +173 -41
  308. package/runtime/schemas/report-narrative-v3.0.schema.json +3 -2
  309. package/runtime/skills/okstra-brief-gen/SKILL.md +35 -2
  310. package/runtime/skills/okstra-container-build/SKILL.md +16 -47
  311. package/runtime/skills/okstra-inspect/facets/history.md +1 -1
  312. package/runtime/skills/okstra-inspect/facets/status.md +7 -6
  313. package/runtime/skills/okstra-pr-gen/SKILL.md +1 -1
  314. package/runtime/skills/okstra-run/SKILL.md +7 -5
  315. package/runtime/templates/report-writer-prompt-preamble.md +1 -1
  316. package/runtime/templates/reports/brief.template.md +6 -2
  317. package/runtime/templates/reports/error-analysis-input.template.md +2 -0
  318. package/runtime/templates/reports/final-verification-input.template.md +2 -0
  319. package/runtime/templates/reports/implementation-input.template.md +7 -1
  320. package/runtime/templates/reports/implementation-planning-input.template.md +4 -0
  321. package/runtime/templates/reports/quick-input.template.md +2 -0
  322. package/runtime/templates/reports/release-handoff-input.template.md +6 -1
  323. package/runtime/templates/reports/report.js +20 -3
  324. package/runtime/templates/reports/schedule.template.md +2 -0
  325. package/runtime/templates/reports/settings.template.json +0 -10
  326. package/runtime/templates/reports/task-brief.template.md +3 -0
  327. package/runtime/templates/reports/user-response.template.md +7 -3
  328. package/runtime/validators/checks/fixtures-01.py +57 -0
  329. package/runtime/validators/checks/fixtures-02.py +565 -0
  330. package/runtime/validators/checks/runners-01.py +108 -0
  331. package/runtime/validators/checks/validate-assets-01.py +60 -0
  332. package/runtime/validators/checks/validate-prompt-metadata-01.py +261 -0
  333. package/runtime/validators/checks/validate-tasks-01.py +46 -0
  334. package/runtime/validators/checks/validate-tasks-02.py +85 -0
  335. package/runtime/validators/checks/validate-tasks-03.py +61 -0
  336. package/runtime/validators/checks/validate-tasks-04.py +118 -0
  337. package/runtime/validators/forbidden_actions.py +73 -3
  338. package/runtime/validators/lib/common.sh +5 -0
  339. package/runtime/validators/lib/fixtures.sh +7 -591
  340. package/runtime/validators/lib/paths.sh +13 -0
  341. package/runtime/validators/lib/runners.sh +6 -104
  342. package/runtime/validators/lib/summary.sh +1 -1
  343. package/runtime/validators/lib/validate-assets.sh +6 -56
  344. package/runtime/validators/lib/validate-prompt-metadata.sh +6 -257
  345. package/runtime/validators/lib/validate-tasks.sh +9 -294
  346. package/runtime/validators/validate-implementation-plan-stages.py +4 -4
  347. package/runtime/validators/validate-run.py +1369 -2654
  348. package/runtime/validators/validate-workflow.sh +56 -16
  349. package/runtime/validators/validate_improvement_report.py +2 -1
  350. package/runtime/validators/validate_session_conformance.py +295 -49
  351. package/dist/commands/execute/agent-prompt.d.mts +0 -1
  352. package/dist/commands/execute/agent-prompt.mjs +0 -24
  353. package/dist/commands/execute/agent-prompt.mjs.map +0 -1
  354. package/dist/commands/execute/codex-dispatch.d.mts +0 -3
  355. package/dist/commands/execute/codex-dispatch.mjs +0 -6
  356. package/dist/commands/execute/codex-dispatch.mjs.map +0 -1
  357. package/dist/commands/execute/codex-run.d.mts +0 -3
  358. package/dist/commands/execute/codex-run.mjs +0 -62
  359. package/dist/commands/execute/codex-run.mjs.map +0 -1
  360. package/dist/commands/execute/convergence.d.mts +0 -1
  361. package/dist/commands/execute/convergence.mjs +0 -38
  362. package/dist/commands/execute/convergence.mjs.map +0 -1
  363. package/dist/commands/execute/error-log.d.mts +0 -1
  364. package/dist/commands/execute/error-log.mjs +0 -18
  365. package/dist/commands/execute/error-log.mjs.map +0 -1
  366. package/dist/commands/execute/git-reconcile.d.mts +0 -1
  367. package/dist/commands/execute/git-reconcile.mjs +0 -30
  368. package/dist/commands/execute/git-reconcile.mjs.map +0 -1
  369. package/dist/commands/execute/handoff.d.mts +0 -1
  370. package/dist/commands/execute/handoff.mjs +0 -31
  371. package/dist/commands/execute/handoff.mjs.map +0 -1
  372. package/dist/commands/execute/incremental-carry.d.mts +0 -1
  373. package/dist/commands/execute/incremental-carry.mjs +0 -20
  374. package/dist/commands/execute/incremental-carry.mjs.map +0 -1
  375. package/dist/commands/execute/incremental-scope.d.mts +0 -1
  376. package/dist/commands/execute/incremental-scope.mjs +0 -29
  377. package/dist/commands/execute/incremental-scope.mjs.map +0 -1
  378. package/dist/commands/execute/integrate-stages.d.mts +0 -1
  379. package/dist/commands/execute/integrate-stages.mjs +0 -24
  380. package/dist/commands/execute/integrate-stages.mjs.map +0 -1
  381. package/dist/commands/execute/pane-title.d.mts +0 -1
  382. package/dist/commands/execute/pane-title.mjs +0 -20
  383. package/dist/commands/execute/pane-title.mjs.map +0 -1
  384. package/dist/commands/execute/plan-items.d.mts +0 -1
  385. package/dist/commands/execute/plan-items.mjs +0 -9
  386. package/dist/commands/execute/plan-items.mjs.map +0 -1
  387. package/dist/commands/execute/plan-validate.d.mts +0 -1
  388. package/dist/commands/execute/plan-validate.mjs +0 -68
  389. package/dist/commands/execute/plan-validate.mjs.map +0 -1
  390. package/dist/commands/execute/plan-verify.d.mts +0 -1
  391. package/dist/commands/execute/plan-verify.mjs +0 -43
  392. package/dist/commands/execute/plan-verify.mjs.map +0 -1
  393. package/dist/commands/execute/spawn-followups.d.mts +0 -1
  394. package/dist/commands/execute/spawn-followups.mjs +0 -22
  395. package/dist/commands/execute/spawn-followups.mjs.map +0 -1
  396. package/dist/commands/execute/team.d.mts +0 -3
  397. package/dist/commands/execute/team.mjs +0 -66
  398. package/dist/commands/execute/team.mjs.map +0 -1
  399. package/dist/commands/execute/token-usage.d.mts +0 -1
  400. package/dist/commands/execute/token-usage.mjs +0 -19
  401. package/dist/commands/execute/token-usage.mjs.map +0 -1
  402. package/dist/commands/execute/worker-audit-check.d.mts +0 -1
  403. package/dist/commands/execute/worker-audit-check.mjs +0 -34
  404. package/dist/commands/execute/worker-audit-check.mjs.map +0 -1
  405. package/dist/commands/execute/worker-dispatch.d.mts +0 -7
  406. package/dist/commands/execute/worker-dispatch.mjs +0 -64
  407. package/dist/commands/execute/worker-dispatch.mjs.map +0 -1
  408. package/dist/commands/execute/worker-state.d.mts +0 -1
  409. package/dist/commands/execute/worker-state.mjs +0 -28
  410. package/dist/commands/execute/worker-state.mjs.map +0 -1
  411. package/dist/commands/execute/worktree-lookup.d.mts +0 -1
  412. package/dist/commands/execute/worktree-lookup.mjs +0 -92
  413. package/dist/commands/execute/worktree-lookup.mjs.map +0 -1
  414. package/dist/commands/execute/worktree-status.d.mts +0 -1
  415. package/dist/commands/execute/worktree-status.mjs +0 -121
  416. package/dist/commands/execute/worktree-status.mjs.map +0 -1
  417. package/dist/commands/inspect/code-review.d.mts +0 -1
  418. package/dist/commands/inspect/code-review.mjs +0 -32
  419. package/dist/commands/inspect/code-review.mjs.map +0 -1
  420. package/dist/commands/inspect/container.d.mts +0 -1
  421. package/dist/commands/inspect/container.mjs +0 -25
  422. package/dist/commands/inspect/container.mjs.map +0 -1
  423. package/dist/commands/inspect/context-cost.d.mts +0 -1
  424. package/dist/commands/inspect/context-cost.mjs +0 -25
  425. package/dist/commands/inspect/context-cost.mjs.map +0 -1
  426. package/dist/commands/inspect/design-prep.d.mts +0 -1
  427. package/dist/commands/inspect/design-prep.mjs +0 -22
  428. package/dist/commands/inspect/design-prep.mjs.map +0 -1
  429. package/dist/commands/inspect/error-report.d.mts +0 -1
  430. package/dist/commands/inspect/error-report.mjs +0 -25
  431. package/dist/commands/inspect/error-report.mjs.map +0 -1
  432. package/dist/commands/inspect/error-zip.d.mts +0 -1
  433. package/dist/commands/inspect/error-zip.mjs +0 -24
  434. package/dist/commands/inspect/error-zip.mjs.map +0 -1
  435. package/dist/commands/inspect/log-report.d.mts +0 -1
  436. package/dist/commands/inspect/log-report.mjs +0 -26
  437. package/dist/commands/inspect/log-report.mjs.map +0 -1
  438. package/dist/commands/inspect/model-io.d.mts +0 -1
  439. package/dist/commands/inspect/model-io.mjs +0 -25
  440. package/dist/commands/inspect/model-io.mjs.map +0 -1
  441. package/dist/commands/inspect/profile-show.d.mts +0 -1
  442. package/dist/commands/inspect/profile-show.mjs +0 -28
  443. package/dist/commands/inspect/profile-show.mjs.map +0 -1
  444. package/dist/commands/inspect/recap.d.mts +0 -1
  445. package/dist/commands/inspect/recap.mjs +0 -30
  446. package/dist/commands/inspect/recap.mjs.map +0 -1
  447. package/dist/commands/inspect/resolve-task-key.d.mts +0 -1
  448. package/dist/commands/inspect/resolve-task-key.mjs +0 -24
  449. package/dist/commands/inspect/resolve-task-key.mjs.map +0 -1
  450. package/dist/commands/inspect/rollup.d.mts +0 -1
  451. package/dist/commands/inspect/rollup.mjs +0 -25
  452. package/dist/commands/inspect/rollup.mjs.map +0 -1
  453. package/dist/commands/inspect/run-audit.d.mts +0 -1
  454. package/dist/commands/inspect/run-audit.mjs +0 -25
  455. package/dist/commands/inspect/run-audit.mjs.map +0 -1
  456. package/dist/commands/inspect/set-work-status.d.mts +0 -1
  457. package/dist/commands/inspect/set-work-status.mjs +0 -29
  458. package/dist/commands/inspect/set-work-status.mjs.map +0 -1
  459. package/dist/commands/inspect/stage-map.d.mts +0 -1
  460. package/dist/commands/inspect/stage-map.mjs +0 -131
  461. package/dist/commands/inspect/stage-map.mjs.map +0 -1
  462. package/dist/commands/inspect/task-list.d.mts +0 -1
  463. package/dist/commands/inspect/task-list.mjs +0 -149
  464. package/dist/commands/inspect/task-list.mjs.map +0 -1
  465. package/dist/commands/inspect/task-show.d.mts +0 -1
  466. package/dist/commands/inspect/task-show.mjs +0 -108
  467. package/dist/commands/inspect/task-show.mjs.map +0 -1
  468. package/dist/commands/inspect/time-report.d.mts +0 -1
  469. package/dist/commands/inspect/time-report.mjs +0 -24
  470. package/dist/commands/inspect/time-report.mjs.map +0 -1
  471. package/dist/commands/inspect/usage-report.d.mts +0 -1
  472. package/dist/commands/inspect/usage-report.mjs +0 -23
  473. package/dist/commands/inspect/usage-report.mjs.map +0 -1
  474. package/dist/commands/inspect/user-response.d.mts +0 -1
  475. package/dist/commands/inspect/user-response.mjs +0 -35
  476. package/dist/commands/inspect/user-response.mjs.map +0 -1
  477. package/dist/commands/inspect/worker-liveness.d.mts +0 -1
  478. package/dist/commands/inspect/worker-liveness.mjs +0 -45
  479. package/dist/commands/inspect/worker-liveness.mjs.map +0 -1
  480. package/dist/commands/lifecycle/contract-check.d.mts +0 -1
  481. package/dist/commands/lifecycle/contract-check.mjs +0 -18
  482. package/dist/commands/lifecycle/contract-check.mjs.map +0 -1
  483. package/dist/commands/lifecycle/migrate.d.mts +0 -1
  484. package/dist/commands/lifecycle/migrate.mjs +0 -30
  485. package/dist/commands/lifecycle/migrate.mjs.map +0 -1
  486. package/dist/commands/lifecycle/model.d.mts +0 -1
  487. package/dist/commands/lifecycle/model.mjs +0 -22
  488. package/dist/commands/lifecycle/model.mjs.map +0 -1
  489. package/dist/commands/manager.d.mts +0 -3
  490. package/dist/commands/manager.mjs +0 -50
  491. package/dist/commands/manager.mjs.map +0 -1
  492. package/dist/commands/report/agent-activity.d.mts +0 -1
  493. package/dist/commands/report/agent-activity.mjs +0 -20
  494. package/dist/commands/report/agent-activity.mjs.map +0 -1
  495. package/dist/commands/report/approval-decision.d.mts +0 -1
  496. package/dist/commands/report/approval-decision.mjs +0 -21
  497. package/dist/commands/report/approval-decision.mjs.map +0 -1
  498. package/dist/commands/report/design-snapshot.d.mts +0 -1
  499. package/dist/commands/report/design-snapshot.mjs +0 -19
  500. package/dist/commands/report/design-snapshot.mjs.map +0 -1
  501. package/dist/commands/report/finalize.d.mts +0 -4
  502. package/dist/commands/report/finalize.mjs +0 -64
  503. package/dist/commands/report/finalize.mjs.map +0 -1
  504. package/dist/commands/report/inject-report-index.d.mts +0 -1
  505. package/dist/commands/report/inject-report-index.mjs +0 -21
  506. package/dist/commands/report/inject-report-index.mjs.map +0 -1
  507. package/dist/commands/report/render-final-report.d.mts +0 -1
  508. package/dist/commands/report/render-final-report.mjs +0 -23
  509. package/dist/commands/report/render-final-report.mjs.map +0 -1
  510. package/dist/commands/report/render-views.d.mts +0 -1
  511. package/dist/commands/report/render-views.mjs +0 -27
  512. package/dist/commands/report/render-views.mjs.map +0 -1
  513. package/dist/commands/report/translate.d.mts +0 -1
  514. package/dist/commands/report/translate.mjs +0 -33
  515. package/dist/commands/report/translate.mjs.map +0 -1
  516. package/runtime/bin/lib/okstra/tmux-pane.sh +0 -40
  517. package/runtime/bin/lib/okstra-ctl/cmd-batch.sh +0 -59
  518. package/runtime/bin/lib/okstra-ctl/cmd-list.sh +0 -35
  519. package/runtime/bin/lib/okstra-ctl/cmd-open.sh +0 -36
  520. package/runtime/bin/lib/okstra-ctl/cmd-projects.sh +0 -26
  521. package/runtime/bin/lib/okstra-ctl/cmd-reconcile.sh +0 -29
  522. package/runtime/bin/lib/okstra-ctl/cmd-reindex.sh +0 -38
  523. package/runtime/bin/lib/okstra-ctl/cmd-rerun.sh +0 -345
  524. package/runtime/bin/lib/okstra-ctl/cmd-show.sh +0 -27
  525. package/runtime/bin/lib/okstra-ctl/cmd-tail.sh +0 -92
  526. package/runtime/bin/lib/okstra-ctl/main.sh +0 -41
  527. package/runtime/bin/lib/okstra-ctl/prepare.sh +0 -31
  528. package/runtime/bin/lib/okstra-ctl/usage.sh +0 -23
  529. package/runtime/bin/okstra-central.sh +0 -152
  530. package/runtime/bin/okstra-incremental-carry.py +0 -10
  531. package/runtime/bin/okstra-incremental-scope.py +0 -10
  532. package/runtime/bin/okstra-team-reconcile.sh +0 -36
  533. package/runtime/python/okstra_ctl/agent_prompt_cli.py +0 -1156
  534. package/runtime/python/okstra_ctl/batch.py +0 -60
  535. package/runtime/python/okstra_ctl/clarification_items.py +0 -1050
  536. package/runtime/python/okstra_ctl/container_registry.py +0 -72
  537. package/runtime/python/okstra_ctl/improvement_assignment.py +0 -61
  538. package/runtime/python/okstra_ctl/resolver.py +0 -54
  539. package/runtime/python/okstra_ctl/team_reconcile.py +0 -275
  540. package/runtime/python/okstra_ctl/tmux.py +0 -134
  541. package/runtime/python/okstra_ctl/worktree.py +0 -1099
@@ -55,6 +55,21 @@ Read-side inspection (`/okstra-inspect`) and scheduling (`/okstra-schedule-gen`)
55
55
  - If the brief is incomplete, continue with explicit uncertainty markers rather than fabricating confidence.
56
56
  - Required roles must not be replaced by unnamed generic parallel workers. Before the final verdict, every selected worker must have either a saved result file or an explicit terminal status with reason. Any attempted worker with status `completed`, `timeout`, or `error` must also have a saved worker prompt history file at its assigned run-level prompt path.
57
57
 
58
+ ## Worker instruction quality gate (BLOCKING)
59
+
60
+ This gate applies before every initial dispatch, retry, re-verification, critic, and correction, regardless of worker role or transport. The lead performs it on the complete proposed instruction before persisting or sending that instruction.
61
+
62
+ 1. Read the current authoritative artifacts that define the assignment. Do not rely on memory, a prior round's summary, or an error message when the referenced schema, manifest, state, source file, or worker result is available.
63
+ 2. Resolve every lead-owned parameter before writing the instruction. State the objective, exact input paths and scope, requested action, forbidden actions, output paths and format, and completion conditions. When a field uses a constrained vocabulary, state the field path, the exact literal value to write, and the allowed literals from the authoritative contract.
64
+ 3. Write directions that the worker can execute without guessing the lead's intent. The worker must not infer a missing path, target, enum, threshold, affected item, or success condition. Indirect phrases such as `use the schema value`, `fix the invalid fields`, `handle the remaining issues`, and `verify as appropriate` are not instructions.
65
+ 4. Validate the complete proposed instruction against the applicable prompt contract and every authoritative artifact it cites. Check that paths resolve, named fields and items exist, literal values are allowed, requested actions stay inside the worker's role and lifecycle boundary, and the stated completion conditions can be checked from the named output. A fragment-only check does not pass this gate.
66
+ 5. If any lead-owned value remains undetermined, stop before dispatch and resolve it from the authoritative source or ask the user when it is user-owned. Do not delegate an accidental ambiguity as a worker choice. A worker may compare alternatives only when alternative evaluation is the explicit assignment.
67
+ 6. Preserve independent judgement. Exact instructions define the task and its constraints; they do not prescribe the finding, vote, verdict, or recommendation an analysis or verification worker must reach.
68
+
69
+ For a corrective dispatch, name every known defect in one instruction with its current value and exact replacement or required outcome. Do not spend one worker round per defect that was already visible before dispatch.
70
+
71
+ **Enforcement:** `tests/contract/test_agent_spawn_surface_census.py` requires this gate to remain in the lead contract and requires every worker-dispatch support contract to point back to it. Semantic correctness remains a lead-side pre-dispatch check because no generic validator can decide whether free-form task instructions accurately represent the cited project evidence.
72
+
58
73
  ## Lifecycle Phase Boundaries (BLOCKING)
59
74
 
60
75
  A single okstra run executes **exactly one** lifecycle phase. The phase is given by `task-manifest.json.workflow.currentPhase` (or, equivalently, the run's `Task Type`). The lead and every worker MUST stay inside that phase's boundary for the duration of the run. Crossing the boundary is a contract violation, not a productivity gain.
@@ -74,7 +89,7 @@ Phase-transition checklist (lead, end of run):
74
89
 
75
90
  1. Confirm the current phase's required outputs are complete and recorded in the final report.
76
91
  2. Set `workflow.phaseStates.<currentPhase>.state = "completed"` in `task-manifest.json` (validator does this when the run passes; verify the value).
77
- 3. Record **Phase Routing** in the final report — it is the source the Next-Phase Pointer is projected from, and Phase 7 validation recomputes the pointer from it. Update `workflow.lastCompletedPhase`. The Next-Phase Pointer (`workflow.nextRecommendedPhase`) itself is written per the Artifact Persistence Checklist in [report-writer](./report-writer.md), which owns its shape and its rules.
92
+ 3. Record **Phase Routing** in the final report — the routing field your task type owns is the only input the Next-Phase Pointer is built from, including the `rationale` the next run's closeout quotes. Update `workflow.lastCompletedPhase`. The pointer (`workflow.nextRecommendedPhase`) is not a field anyone writes: Phase 7 projects it from that routing field, and [report-writer](./report-writer.md) owns which field feeds it and what the projection does with it.
78
93
  4. **Do NOT start the next phase inside the current run.** A new okstra invocation with the new `--task-type` is the only legal way to advance.
79
94
 
80
95
  User-utterance interpretation rule:
@@ -109,6 +124,8 @@ Required checkpoints:
109
124
  - `PROGRESS: phase-4-dispatch worker=<role> model=<model>` — once per worker, immediately before `dispatch_worker`. `<role>` is the **roster** role, exactly as team-state's `workers[].role` records it (`Claude worker`, `Codex worker`) — the checkpoint is matched against that entry, so a phase-specific functional label (`Claude verifier`, `Codex executor`) names no roster worker and fails the check. Only `claude-worker`-style hyphenation of the same roster role is also accepted.
110
125
  - `PROGRESS: phase-5-poll pending=<n> done=<m>` — emitted on each wakeup while the pending set is non-empty.
111
126
  - `PROGRESS: phase-5-collect worker=<role> status=<terminal-status>` — once per worker, immediately after the result file is verified. `<role>` is the roster role, same rule as `phase-4-dispatch` above.
127
+ - `PROGRESS: phase-5-stage stage=<N> title=<title> steps=<count>` — `implementation` only, immediately before the Executor's `phase-4-dispatch` line, after parsing the approved plan's Stage Map and this run's `**Stage for this implementation run:**` anchor. `<title>` is the stage's Stage Map title and `<count>` is its `stepwiseExecution` row count, both read from the approved plan — this is the line that tells the user WHICH plan stage this run executes. The numbering keeps the line sorted where the work happens — the stage runs in Phase 5.
128
+ - `PROGRESS: phase-5-stage-complete stage=<N> steps=<done>/<count>` — `implementation` only, immediately after the Executor result is verified and its `### Stage Carry Evidence` block is parsed, before the verifier dispatch. `<done>` counts the block's `stepResults[]` rows whose `status` is `done` — read from the emitted block, never recomputed from git. Omitted when the Executor ends without carry evidence (`FAIL` or a non-result); the user then learns the outcome from `phase-5-collect status=<terminal-status>`.
112
129
  - `PROGRESS: phase-5.5-convergence round=<N> queue=<count>` — at the start of each convergence round (Phase 5.5).
113
130
  - `PROGRESS: phase-5.6-critic provider=<provider> gaps=<n>` — after the critic result is collected (Phase 5.6, opt-in; the critic dispatch itself fires concurrently with the first 5.5 reverify round). Omitted when `convergence.critic.enabled == false`.
114
131
  - `PROGRESS: phase-batch-cleanup panes=<n>` — immediately after cleaning up the previous batch's panes, at each batch boundary (① just before the first `phase-5.5-convergence` round ② just before the `phase-6-synthesis` report-writer dispatch). `<n>` is the number of panes closed at that boundary — the panes of dispatches this run recorded and that have since finished — read from the `okstra team reclaim --dry-run` pass taken immediately before the closing pass, never estimated. A pane the harness opened for its own teammate carries no recorded id, so it is not counted and not closed. Expose only the counts and NEVER expose a raw `paneId` or worker handle. Just before the first batch (analysis-worker dispatch) there is nothing to clean up, so it is a no-op and the marker is omitted.
@@ -123,7 +140,21 @@ Do NOT replace them with prose ("Now I'm starting Phase 2..."), do NOT skip a ch
123
140
 
124
141
  `okstra-run` surfaces these lines to the user directly; other launch paths persist them in the selected adapter's declared conformance evidence/event source for post-hoc retrieval.
125
142
 
126
- **Enforcement:** the Phase 7 validator (`validators/validate-run.py` → `validate_session_conformance.py`) reads the selected adapter's declared conformance evidence/event source within the run window and fails the run as `contract-violated` when a required checkpoint is missing — including the per-worker `phase-4-dispatch` / `phase-5-collect` lines (which must name each worker's role) and the `phase-batch-cleanup` lines that MUST precede the first `phase-5.5-convergence` round and the `phase-6-synthesis` report-writer dispatch. When the plan-body state file records two or more rounds, `_check_plan_verify_cleanup_checkpoints` additionally requires a `phase-5.5.9-plan-verify` line per round and a `phase-batch-cleanup` between consecutive rounds. For activity-contract-v1 planning, `_check_activity_contract` validates the structured worker pairs, verification and self-fix counts, user-decision references, and `A-NNN` ordering. `phase-7-teardown` and `complete` fire after validation and are not checked.
143
+ **Enforcement:** the Phase 7 validator (`validators/validate-run.py` → `validate_session_conformance.py`) reads the selected adapter's declared conformance evidence/event source within the run window and fails the run as `contract-violated` when a required checkpoint is missing — including the per-worker `phase-4-dispatch` / `phase-5-collect` lines (which must name each worker's role) and the `phase-batch-cleanup` lines that MUST precede the first `phase-5.5-convergence` round and the `phase-6-synthesis` report-writer dispatch. When the plan-body state file records two or more rounds, `_check_plan_verify_cleanup_checkpoints` additionally requires a `phase-5.5.9-plan-verify` line per round and a `phase-batch-cleanup` between consecutive rounds. For `implementation`, `_check_progress_checkpoints` additionally requires the `phase-5-stage` announcement once any worker is dispatched, and the `phase-5-stage-complete` line once an implementer worker completed. For activity-contract-v1 planning, `_check_activity_contract` validates the structured worker pairs, verification and self-fix counts, user-decision references, and `A-NNN` ordering. `phase-7-teardown` and `complete` fire after validation and are not checked.
144
+
145
+ ## Asking the user (BLOCKING)
146
+
147
+ Every question this run puts to the user obeys the same three rules, whatever the subject — an approval blocker, a re-verification scope, a routing fork, a cleanup choice, a wizard step you are relaying. A question is work you are handing back to the user, so arrive with the work you can do yourself already done.
148
+
149
+ 1. **Investigate before asking.** Read the artifacts the answer turns on — the cited plan items, the `path:line`s, the prior report's Stage Map, the sidecar the user already wrote. Two things come out of that: the question may already be settled, in which case do not ask; or you now know the concrete candidates, which is what the options are made of. Say in the question what you read and what you could not read; never invent the missing fact.
150
+
151
+ 2. **Offer two or three concrete options, the recommendation first.** Each option names its outcome — "if you pick this, then …" — in the user's language, not the field name it maps to. The first option is the one you recommend, and one clause says why you recommend it. When the run genuinely has no basis to prefer one, say that in the question body rather than dressing an arbitrary option as a recommendation. The last option is always the free-input escape ("answer directly"), never the first: a list that opens on free input has handed the whole question back.
152
+
153
+ 3. **Never hand over a blank you could have filled.** Anything you worked out while investigating belongs in the options. Stating a finding in chat and then offering nothing but free input is the failure this rule exists for. Measured: a re-verification-scope prompt whose preamble named the stage the answers touched (`Stage 1`) still offered `직접 입력 — 다시 볼 stage 번호를 지정` as its first and only actionable choice, and the user had to retype what the run had just told them.
154
+
155
+ When you are relaying a wizard step, [okstra-run](../../skills/okstra-run/SKILL.md) still owns the option set: relay every `options[]` entry unchanged, in order. This rule adds to it rather than overriding it — put your investigation in the question body and name which of the wizard's own options you recommend and why. Do not drop, merge, reorder, or invent a wizard option to satisfy this section.
156
+
157
+ **Enforced:** `scripts/okstra_ctl/wizard.py` `Prompt.__post_init__` refuses any wizard step whose free-input option is not last (only an abort option may follow it), so a question cannot open on "answer directly". The investigate-first half is a contract on the lead, checked by the same evidence source as the rest of this section — see the `PROGRESS: user-confirm` checkpoint below for the one question kind that leaves a record.
127
158
 
128
159
  ## User confirmation before an approval blocker (BLOCKING)
129
160
 
@@ -139,7 +170,7 @@ The sequence is fixed:
139
170
  4. On an answer — record the raw text in the row's `userInput`, set `status: answered` and `userConfirmation: asked-and-answered`, and apply the selected disposition in this run.
140
171
  5. Only when asking fails does the row stay open: `asked-awaiting` when the user has not answered, `deferred-no-interactive-session` when this run has no user to ask.
141
172
 
142
- For report contract v3 `implementation-planning`, record active approval decisions only through `okstra approval-decision`; report assembly derives each report row's status, resolution, and backtraces from that lead-owned ledger plus the activity ledger. Classify a user-owned selection as `user-decision`, a surviving non-correctness majority disagreement as `noncritical-dissent`, and a cited path/symbol mismatch, `P-Req-*` coverage mismatch, or independent Requirement Coverage blocker as `correctness-critical`. `select` is limited to `user-decision`. `accept-risk` is available to all three classifications: it ends the gate, keeps the DISAGREE votes and the clarification row as evidence, and later stages read that record. `request-revision` / `reject` are available to all three and still withhold the next phase. Contract v2 remains read-only compatible; do not create a new v2 report. **Enforced:** `scripts/okstra_ctl/approval_decisions.py` rejects invalid option/disposition combinations, and `validators/validate-run.py` `_validate_v3_approval_context` recomputes report backtraces.
173
+ For report contract v3 `implementation-planning`, record active approval decisions only through `okstra approval-decision`; report assembly derives each report row's status, resolution, and backtraces from that lead-owned ledger plus the activity ledger. Classify a user-owned selection as `user-decision`, a surviving non-correctness majority disagreement as `noncritical-dissent`, and a cited path/symbol mismatch, `P-Req-*` coverage mismatch, or independent Requirement Coverage blocker as `correctness-critical`. `select` is limited to `user-decision`. `accept-risk` is available to all three classifications: it ends the gate, keeps the DISAGREE votes and the clarification row as evidence, and later stages read that record. `request-revision` / `reject` are available to all three and still withhold the next phase. Contract v2 remains read-only compatible; do not create a new v2 report. **Enforced:** `scripts/okstra_ctl/approval_decisions.py` rejects an invalid option/disposition combination at the moment the decision is opened or resolved, so a classification that does not admit the disposition never reaches the ledger. The backtraces are not re-derived afterwards — they are produced by assembly (next paragraph), which is why the lead has no hand-written path to them. `validators/validate-run.py` `_validate_v3_approval_context` checks the one claim assembly cannot see: an `approved` frontmatter published against a row that still blocks progress.
143
174
 
144
175
  The approval state transitions are fixed:
145
176
 
@@ -151,7 +182,7 @@ Do not move `answered` back to `open` because a check failed. The user's choice
151
182
 
152
183
  `open` blocks until the user judges. `answered` with `select` / `accept-risk` / `answer`, `resolved`, and `obsolete` do not block approval or the next phase. `request-revision` and `reject` still withhold the next phase until this report's `supersessionLedger` records that the answer was incorporated (`superseded` or `no-dependent-statement`). `accept-risk` does not require re-verification `AGREE`. The worker votes stay on the plan item so a later stage can still see the dissent. **Enforced:** `scripts/okstra_ctl/clarification_items.py` `row_blocks_progress`, `validators/validate-run.py` `_user_accepted_plan_item_ids`.
153
184
 
154
- When a terminal row preserves a pre-correction dissent classification, keep superseded votes in `state/plan-body-verification-implementation-planning-<seq>.json`. Activities that implement or check the decision record the exact `C-NNN` in `clarificationRefs` and the affected `P-*` identifiers in `planItemIds`. Report assembly verifies that every resolution `checkRefs` value names an existing activity and derives each plan item's `clarificationRefs`; the lead never copies those references into `approvalContext`. A corrected coverage-only blocker keeps its `C-NNN` in the non-blocking Requirement Coverage row's `decisionRefs`. An `obsolete` row is invalid while its disagreement or coverage blocker remains active in the current plan. **Enforced:** `scripts/okstra_ctl/report_assembly.py` `_clarification_row` / `_attach_plan_backlinks` and `validators/validate-run.py` `_validate_v3_approval_context`.
185
+ When a terminal row preserves a pre-correction dissent classification, keep superseded votes in `state/plan-body-verification-implementation-planning-<seq>.json`. Activities that implement or check the decision record the exact `C-NNN` in `clarificationRefs` and the affected `P-*` identifiers in `planItemIds`. Report assembly verifies that every resolution `checkRefs` value names an existing activity and derives each plan item's `clarificationRefs`; the lead never copies those references into `approvalContext`. A corrected coverage-only blocker keeps its `C-NNN` in the non-blocking Requirement Coverage row's `decisionRefs`. An `obsolete` row is invalid while its disagreement or coverage blocker remains active in the current plan. **Enforced at assembly, not after it:** `scripts/okstra_ctl/report_assembly.py` `_clarification_row` refuses a resolution whose `checkRefs` is empty, names an activity that does not exist, or names one whose `clarificationRefs` omits this `C-NNN`; `_attach_plan_backlinks` derives every plan item's `clarificationRefs` from the activity ledger, so a hand-copied reference has nowhere to enter. Nothing recomputes these links afterwards — assembly failing is the whole check.
155
186
 
156
187
  **Predicting the blocker is not the same as raising it.** A lead that says "this will likely become an approval blocker; I will ask at that point" has already reached the moment — ask then, in that message. One run announced exactly that, never asked, wrote the row anyway, and then spent its entire self-fix budget on a gate no round could clear, because the user had already answered the question before the run started.
157
188
 
@@ -188,11 +219,11 @@ For `--task-type implementation` runs, the task bundle additionally pins one of
188
219
 
189
220
  Lead MUST dispatch Edit/Write-bearing work only through that executor binding: use the host primitive with `hostModelValue` for `runner=native-session`. For `runner=cli-wrapper`, use `okstra team dispatch` when `terminalBackend` is `cmux-pane`, or `okstra worker-dispatch` with `modelExecutionValue` otherwise. The other providers in the roster still run as read-only verifiers in the same run; the executor's own provider does not, because its worker ID materializes as the executor on every dispatch — so the diff is reviewed context-isolated by the remaining verifiers. Session isolation is the primary self-review safeguard — a verifier reusing the executor's model variant is acceptable in a distinct session. A different model variant (e.g. executor=opus / Claude verifier=sonnet) is recommended but not mandatory.
190
221
 
191
- Executor is chosen at run-prep time via `--executor <claude|codex|antigravity>` (or `OKSTRA_DEFAULT_EXECUTOR`, fallback `claude`); the model used by the executor is taken from the corresponding worker model flag (`--claude-model` / `--codex-model` / `--antigravity-model`). For CLI-backed executors, the underlying file mutation happens inside the executor CLI's own auto-edit mode (e.g. `codex exec --sandbox workspace-write`), not through the lead runtime's `write_artifact` operation.
222
+ Executor is chosen at run-prep time via `--executor <claude|codex|antigravity>` (or `OKSTRA_DEFAULT_EXECUTOR`, fallback `claude`); the model used by the executor is taken from the corresponding worker model flag (`--claude-model` / `--codex-model` / `--antigravity-model`). For CLI-backed executors, the underlying file mutation happens inside the executor CLI's own auto-edit mode (e.g. `codex exec --sandbox danger-full-access`), not through the lead runtime's `write_artifact` operation.
192
223
 
193
224
  #### Task worktree (BLOCKING for every task-type)
194
225
 
195
- `okstra-ctl` provisions dedicated `git worktree`s at run-prep time. Lead, the Executor, and every verifier MUST treat the provisioned worktree as the canonical working directory regardless of task-type.
226
+ Okstra provisions dedicated `git worktree`s at run-prep time. Lead, the Executor, and every verifier MUST treat the provisioned worktree as the canonical working directory regardless of task-type.
196
227
 
197
228
  - **Task-key worktree (non-`implementation` phases):** `requirements-discovery`, `error-analysis`, `implementation-option-selection`, and `implementation-planning` share one worktree per task-key so phase N inherits the working-tree state phase N-1 left behind. Location: `~/.okstra/worktrees/<project-id>/<task-group-segment>/<task-id-segment>/` (override `OKSTRA_HOME` only for tests). All segments are sanitised — `/`, `:`, and other special chars collapse to `-`.
198
229
  - **Stage worktree (`implementation`):** stage-isolated — one run = one stage, each in its own worktree at `.../<task-id-segment>/stage-<N>/` on its own branch. Single-stage `final-verification` (`--stage <N>`) reuses that stage worktree read-only; whole-task `final-verification` operates on the task-key worktree.
@@ -220,6 +251,13 @@ After context-loader completes, read **only the compact intake files below** in
220
251
  2. `instruction-set/analysis-profile.md` — needed to pick the right `Required workers:` block and phase rules
221
252
  3. `instruction-set/analysis-packet.md` — primary compact input for analysis worker dispatch
222
253
 
254
+ **Doctrine lazy reads (BLOCKING — read at the round, not at Phase 1):**
255
+
256
+ - [convergence](./convergence.md) — read before the first `PROGRESS: phase-5.5-convergence` line of any run that runs a convergence round.
257
+ - [plan-body-verification](./plan-body-verification.md) — read before the first `PROGRESS: phase-5.5.9-plan-verify` line.
258
+
259
+ Both stay out of the Phase 1 baseline for the token reason above, and neither is optional at its round: together they carry more than half of this contract family's MUST clauses, so a round dispatched without the read is a round run from memory. **Enforced:** `validators/validate_session_conformance.py` `_ENTRY_GUARD_READS` requires each read — a `Read` call or a shell command naming the file — inside this run's window and before that checkpoint. The requirement is conditioned on the checkpoint actually appearing, so a run that holds no such round is never asked for it.
260
+
223
261
  **Lazy reading discipline (do NOT read at Phase 1):**
224
262
 
225
263
  - `task-index.md` — only when the user explicitly asks for a human summary or when history disambiguation is required.
@@ -276,12 +314,14 @@ For `improvement-discovery`, Lead records `## Primary Pass Assignments` in the P
276
314
 
277
315
  ### Phase 4 / Phase 5 — Dispatch, await, and error-log recording
278
316
 
279
- For each selected worker assignment, persist the exact prompt history, emit the per-worker Phase 4 checkpoint, and call `dispatch_worker(assignment, prompt)` through the selected adapter. Then call `await_workers(handles)`. A dispatch acknowledgement or process/pane creation is never completion: verify the terminal status, Result Path, and worker-results audit path required by `team-contract` before emitting the Phase 5 collection checkpoint.
317
+ For each selected worker assignment, persist the exact prompt history, emit the per-worker Phase 4 checkpoint, and call `dispatch_worker(assignment, promptPath)` through the selected adapter — the dispatch hands the worker the persisted prompt's path, never a re-inlined copy of its body. Then call `await_workers(handles)`. A dispatch acknowledgement or process/pane creation is never completion: verify the terminal status, Result Path, and worker-results audit path required by `team-contract` before emitting the Phase 5 collection checkpoint.
280
318
 
281
319
  Retries and convergence re-verification always call `redispatch_worker` to create a fresh one-shot session. Never reuse a worker conversation or switch adapters/providers to hide a failed assignment.
282
320
 
283
321
  ### Errors log path wiring (BLOCKING)
284
322
 
323
+ **Enforced:** `**Errors log path:**` is emitted by `okstra_ctl.worker_prompt_headers` and required of every worker-facing prompt — `worker_prompt_policy` prepends `ERRORS_PATH_HEADERS` for any audience other than `lead-only`, and `worker_prompt_contract` fails a prompt missing a required header.
324
+
285
325
  The launch prompt's `## Run Logs (error-log wiring)` section gives Lead the resolved absolute path for the run-level errors log. When Lead constructs each worker's dispatch prompt body, Lead MUST inject this header line verbatim:
286
326
 
287
327
  - `**Errors log path:** <absolute run-level errors log path from launch prompt>`
@@ -322,6 +362,8 @@ The deterministic dispatcher records this through its selected adapter — Lead
322
362
 
323
363
  ## Phase 5.5: Convergence loop
324
364
 
365
+ **Enforced:** the round budget belongs to the engine, not to a post-hoc re-count — `scripts/okstra_ctl/convergence_engine.py` `_parse_config` refuses a state whose `config.effectiveMaxRounds` exceeds `config.maxRounds`, and `plan_next_round` returns `action: "finalize"` with reason `max-rounds-reached` as soon as the executed rounds reach the effective budget, so there is no path that runs a round past it. `okstra convergence validate` owns queue membership and classification, so a hand-edited state fails there; and `_check_verification_errors_are_logged` in `validators/validate_session_conformance.py` reports a `verification-error` worker with no `contract-violation` entry in the run error log. That last one is an advisory — by the time the validator runs the session that would have written the entry has ended.
366
+
325
367
  **REQUIRED RESOURCE:** Read [convergence](./convergence.md) for iterative cross-verification.
326
368
 
327
369
  Convergence is enabled by default. Configure via task-manifest.json:
@@ -399,7 +441,7 @@ For a new `implementation-planning` run, the fixed order is initial verification
399
441
  4. After `okstra plan-verify` succeeds, run `okstra plan-items complete-round --state <plan-body-verification.json> --run-manifest <current-run-manifest.json> --round <N>`. Python reads the current worker assignments, atomically appends the convergence-owned history, and updates its nested final projection. This state is the only plan-verification input report assembly reads.
400
442
  5. Record every *in-scope execution* `majority-disagree` decision through `okstra approval-decision`; record its plan and clarification links only on activities. Do not promote `observed` / `deferred` / `record` items, and do not append `clarificationItems[]` directly.
401
443
  6. Run report assembly after the final plan-body state, approval ledger, design snapshot, activity ledger, and team state are complete. Assembly writes `implementationPlanning.planBodyVerification` and derived clarification rows while publishing `data.json` once.
402
- 7. Publish the report record `frontmatter.approved` field as `false`. There is no in-body `- [ ] Approved` marker line — approval lives only in the record (see [plan-body-verification](./plan-body-verification.md) §"Round protocol" step 9). The user may set it to `true` (via `--approve` or the in-session wizard) only when the gate is `passed` or `passed-with-dissent`. **Enforced:** `validators/validate-run.py` `validate_phase_boundary` fails a report shipping `approved: true` under `blocked-by-disagreement` / `aborted-non-result`, and run-prep (`scripts/okstra_ctl/run.py` `_validate_approved_plan`) fail-closes the same case. Manually flipping a blocked gate to passing is a contract violation.
444
+ 7. Publish the report record `frontmatter.approved` field as `false`. There is no in-body `- [ ] Approved` marker line — approval lives only in the record (see [plan-body-verification](./plan-body-verification.md) §"Round protocol" step 9). The user may set it to `true` (via `--approve` or the in-session wizard) when the gate is `passed` / `passed-with-dissent`, or under `blocked-by-disagreement` once every remaining `Blocks=approval` row is user-proceeded (`accept-risk` / `select` / `answer`); `aborted-non-result` still withholds approval. **Enforced:** run-prep (`scripts/okstra_ctl/run.py` `_validate_approved_plan` / `_blocking_gate_survives_user_decision`) fail-closes an `approved: true` plan whose blocking gate carries no user disposition, and `validators/validate-run.py` `_validate_plan_body_gate_recompute` fails a gate value upgraded past what the recorded votes support. Manually flipping a blocked gate to passing is a contract violation.
403
445
 
404
446
  If `convergence.planBodyVerification.enabled == false` (set by `--no-plan-verification` or by `okstra config set plan-verification off`), the entire sub-step is skipped and the top-of-report Approval marker is rendered unconditionally (legacy behaviour). This opt-out is intended for fast iteration only and is not recommended for handoff-ready plans.
405
447
 
@@ -416,12 +458,21 @@ Keep the assigned worker prompt history paths stable in `team-state`, `run-manif
416
458
 
417
459
  After persistence, reply briefly in the resolved Report Language. **Lead this reply with the run's task identity** — state `<task-group>/<task-id>` (or the full `taskKey`) first. Then: completion status, the task-qualified human report path, the report record path, validator result, any remaining blocker. **Close with the user's next action** — one command they can run now. A prohibition (`do not start implementation`) is not a next action. A status dump is not a close. This closeout is also in the launch prompt (`prompts/launch.template.md` "User closeout (BLOCKING)") so it is not lazy-read.
418
460
 
461
+ **Read the pointer from the `report-finalize` result — do not re-derive it.** Its top-level `nextRecommendedPhase` object (`phase`, `status`, `rationale`) is what the rows below branch on, and the same values are repeated on stderr as `next phase status:` / `next phase:` / `next phase rationale:`. You author your task type's routing field; Phase 7 projects the pointer from it, and for `implementation-planning` there is no routing field to re-derive from at all. A `nextRecommendedPhaseError` in that result means the pointer could not be read — say so in one line and close on the `validate-run` row. The same result also carries `nextCommand` — `{command, note}`, the table below already applied to this run. When `command` is non-empty it is the close; when it is empty the `note` says what to do with the `rationale` instead.
462
+
419
463
  Pick the next action from this table; the first matching row wins:
420
464
 
421
- - Open `blocks: approval` rows → `/okstra-user-response` (name the `C-NNN` ids). Do not start implementation until those answers exist. An `accept-risk` / `select` / `answer` already recorded is not an open blocker.
422
- - `workflow.awaitingApproval` is true → `/okstra-run` → `implementation` (asks `approve_plan_confirm`) or `--approve`. Do not propose another planning run.
423
- - Phase 7 `validate-run` failed → one line naming the blocking cause, then `/okstra-run` to re-run this phase with the recorded sidecar, or `/okstra-inspect recap` if the resume flags are unknown.
424
- - Pointer `status: ready` → `/okstra-run` for that `phase`.
465
+ - Open `blocks: approval` rows, or a `blocked-by-disagreement` gate whose approval rows carry no progress disposition (`okstra_ctl.run._blocking_gate_survives_user_decision`) → `/okstra-user-response` (name the `C-NNN` ids). Do not start implementation until those answers exist. An `accept-risk` / `select` / `answer` already recorded is not an open blocker. A report published with those rows open is not a failed run — the answers are the missing input, and a planning re-run without them reproduces the same block.
466
+
467
+ After `implementation-planning`, stop on the rows above. No open approval blocker → `/okstra-run` → `implementation` (asks `approve_plan_confirm`) or `--approve`. Do not propose another planning run. Do not say `/okstra-inspect`. A blocking `validate-run` failure is named in one line, then this same command; a `validate-run` advisory is named in one line and nothing else — it did not fail the run.
468
+
469
+ For every other task type:
470
+
471
+ - Pointer `status: ready` → `/okstra-run` for that `phase`. Quote the pointer's `rationale` — that sentence is this run's report saying why that phase comes next, and it is the analysis the user asked for.
472
+ - Pointer `status: terminal` → the lifecycle ends here. Say the task is finished and quote the pointer's `rationale`. Do not propose a run and do not send the user to `/okstra-inspect`: the decision is already made, so there is nothing to inspect. If this run registered follow-up tasks, name them and the command that starts one.
473
+ - Pointer `status: blocked` → the pointer's `rationale` names what is in the way and what to run. Quote it and issue that command — `/okstra-user-response` for the `C-NNN` ids it lists, or `/okstra-run` for the phase it names.
474
+ - Pointer `status: pending` with a `rationale` → the comparison finished and the user's own choice is what comes next. Quote the `rationale` and issue the command it names. Do not re-run the phase that just completed, and do not send the user to `/okstra-inspect`: a finished phase has nothing to inspect, and re-running it discards the result the user is being asked to choose from.
475
+ - Phase 7 `validate-run` failed → one line naming the blocking cause, then `/okstra-run` to re-run this phase with the recorded sidecar. Only a failure matching the blocking allowlist (`okstra_ctl.blocking_checks`) reaches this row; every other finding was demoted to an advisory, printed as `validate-run: advisory — <finding>`, and the run passed. Name those advisories in one line and take the command from the matching pointer row above — an advisory is not a reason to re-run.
425
476
  - Otherwise → `/okstra-inspect status` for this task.
426
477
 
427
478
  When the host native picker is available and two of those rows could apply, ask with that picker (recommended first). Do not end the turn after the status dump.
@@ -34,6 +34,8 @@ The finding queue (Phase 5.5, [convergence](./convergence.md)) and the plan-item
34
34
  - The two rounds write to **different state files**: `runs/<task-type>/state/convergence-<task-type>-<seq>.json` (findings, see [convergence](./convergence.md) §"Convergence State Artifact") vs. `runs/<task-type>/state/plan-body-verification-<task-type>-<seq>.json` (plan items, see §"`plan-body-verification.json` schema").
35
35
  - Aggregation logic (verdict counting, classification) MUST NOT carry votes from one queue into the other.
36
36
 
37
+ **Enforced:** `_validate_queue_mutual_exclusion` in `validators/validate-run.py` fails a `P-*` id in the convergence state's `findings[]` or `dispatches[].findingIds`, an `F-*`/`C-*` id in the plan-body state's `planItems[]`, and a `### <id>` heading from the wrong namespace in either round's prompts.
38
+
37
39
  Mixing the two queues — for example, parsing a Phase 6 draft's Stepwise Execution Order step as if it were an `F-*` finding — is a contract violation. Future Claude reading this contract: if you find yourself tempted to "just reuse the finding queue for plan items, they're similar enough", stop. They are not similar enough; the verdict semantics differ (see §"Plan-body verdict semantics" below).
38
40
 
39
41
  ## Configuration
@@ -45,7 +47,7 @@ Plan-body verification is configured under `convergence.planBodyVerification` in
45
47
  | `enabled` | `true` | If `false`, the round is skipped and the approval gate is not blocked by this round (legacy behaviour). |
46
48
  | `maxRounds` | `1` | Upper bound. Plan-body verification is consistency / completeness checking, not fact checking — additional rounds rarely help. Range 1–3. |
47
49
  | `selfFixMaxRounds` | `1` | One report-writer rewrite at most. The initial verification is round 1; targeted re-verification is round 2 after that rewrite. |
48
- | `gating` | `true` | If `true` (default), `majority-disagree` blocks approval. If `false`, the round is advisory-only and never blocks approval. Prepare emits `true` because the plan does not exist yet. After the report-writer draft, `okstra plan-items prepare` (and `seed`) flip it to `false` when `designPreparation.mode` is `no-design-inputs` and the Stage Map has exactly one row. That path keeps extraction and one verification round and does not run the self-fix loop or a sweep batch. Two-or-more stages, a PREP item, or non-empty `designPreparation.items` keep `gating=true`. `--no-plan-verification` is the separate manual opt-out (`enabled=false`). **Enforced:** `okstra_ctl.plan_items.advisory_plan_body_gating`, `validators/validate-run.py` `_validate_advisory_plan_body_gating`. |
50
+ | `gating` | `true` | If `true` (default), `majority-disagree` blocks approval. If `false`, the round is advisory-only and never blocks approval. Prepare emits `true` because the plan does not exist yet. After the report-writer draft, `okstra plan-items prepare` (and `seed`) flip it to `false` when `designPreparation.mode` is `no-design-inputs` and the Stage Map has exactly one row. That path keeps extraction and one verification round and does not run the self-fix loop or a sweep batch. Two-or-more stages, a PREP item, or non-empty `designPreparation.items` keep `gating=true`. `--no-plan-verification` is the separate manual opt-out (`enabled=false`). **Enforced:** `okstra_ctl.plan_items.advisory_plan_body_gating`, `validators/validate-run.py` `_validate_advisory_plan_body_gating`. "never blocks approval" is enforced by the `gating is False` early return in `_recompute_plan_body_gate`, `_gate_blocking_causes`, `_validate_plan_body_clarification_matching`, and `_validate_self_fix_before_clarification` — an advisory round neither demands a `blocks=approval` row nor an exhausted self-fix budget. |
49
51
 
50
52
  Default values are emitted into the manifest by `scripts/okstra_ctl/render.py` (`_build_convergence_block`). The ctx knob `OKSTRA_PLAN_VERIFICATION=false` flips `planBodyVerification.enabled` to false. `gating=false` is not that opt-out: extraction and one round still run.
51
53
 
@@ -99,7 +101,7 @@ For legacy candidate-comparison plans, `4.5.2 Trade-off Matrix` and `4.5.3 Recom
99
101
 
100
102
  Each plan item inherits the `[TICKETID: ...]` tag of its source section (per the standard ticket-tagging contract).
101
103
 
102
- For every detector-produced `(stage, kind)`, extract exactly one plan item named `P-Prep-S<stage>-<kind>`. The V1 detector in `scripts/okstra_ctl/design_surfaces.py` owns the set: extraction consumes its output and never reruns a free-form requirements or keyword analysis. The worker receives the stage trigger evidence and its single `designSurfaceCoverage` row, plus only the referenced `designPreparation` PREP items needed to judge that row. `okstra_ctl.plan_items.expected_plan_item_ids()` and `validators/validate-run.py` enforce the exact ID set, while `_validate_detector_coverage` enforces exactly-once coverage/evidence.
104
+ For every detector-produced `(stage, kind)`, extract exactly one plan item named `P-Prep-S<stage>-<kind>`. The V1 detector in `scripts/okstra_ctl/design_surfaces.py` owns the set: extraction consumes its output and never reruns a free-form requirements or keyword analysis. The worker receives the stage trigger evidence and its single `designSurfaceCoverage` row, plus only the referenced `designPreparation` PREP items needed to judge that row. `okstra_ctl.plan_items.expected_plan_item_ids()` and `validators/validate-run.py` enforce the exact ID set. The coverage rows themselves are detector-owned: `scripts/okstra_ctl/design_snapshot.py` `build_design_snapshot` writes one row per detected `(stage, kind)` with its trigger evidence, and `scripts/okstra_ctl/report_projections.py` `project_design` reruns the detector against the plan and raises `ReportProjectionError` unless the snapshot's rows and evidence match it exactly — so a missing, duplicated, or evidence-drifted row stops the report at projection, before the plan is judged.
103
105
 
104
106
  When extracting each item, lead also captures a **`subject`** — a plain one-line label (≤12 words) describing *what that item is* in the reader's terms, e.g. `P-Opt-1` → "Option A: split upload v2 into a new module", `P-Step-1.1` → "Stage 1 Step 2: regression-check with `npm run test:v2`". This is a label-capture, not new analysis. The `subject` is what §5.5.9 renders as the per-item heading so the reader knows *what* each AGREE/DISAGREE is about without cross-referencing §4.5; a bare `P-*` ID with no subject is a contract violation. **Enforced:** `validators/validate-run.py` `_validate_plan_item_subject_substance` fails a subject that is a placeholder — under 3 chars, equal to the item id, or shaped like a bare `P-*` id.
105
107
 
@@ -166,9 +168,12 @@ that produced the gate value. Do not hand-write it — a hand-written register
166
168
  drifts from the verdicts it claims to summarise, and nothing else reads it, so
167
169
  the drift stays invisible.
168
170
 
169
- **Enforced:** `validators/validate-run.py` `_validate_set_aside_register` fails
170
- a scored round whose declared register does not match the one recomputed from
171
- the verdicts.
171
+ **Enforced by the writer, not by a re-count.** `okstra plan-items complete-round`
172
+ (`scripts/okstra_ctl/plan_items_cli.py:1454`) is the only code in the repo that
173
+ writes `planBodyVerification.setAside`, and it emits it from the same round
174
+ summary that produced the gate value. Running the command is what makes the
175
+ register correct. Nothing re-derives it afterwards, so a hand-written register
176
+ is published unchecked.
172
177
 
173
178
  ## Two blocks — execution and record (BLOCKING)
174
179
 
@@ -279,7 +284,7 @@ Worker non-result handling (`timeout`, `error`, no result file, wrapper `cli-fai
279
284
  ## Planning-time environment gap
280
285
 
281
286
  `implementation-planning` runs in the task-key worktree, and that worktree carries **no installed
282
- build/test dependencies** — `scripts/okstra_ctl/worktree.py` `DEFAULT_WORKTREE_SYNC_DIRS` mirrors
287
+ build/test dependencies** — `scripts/okstra_ctl/worktree/sync_config.py` `DEFAULT_WORKTREE_SYNC_DIRS` mirrors
283
288
  only okstra-owned directories, and installing dependencies here would violate the profile's
284
289
  "no build/migration/deploy execution" boundary. So a plan's `npm test` / `yarn workspace … build` /
285
290
  `pytest` command **cannot succeed at plan time by construction**, no matter how correctly the plan
@@ -322,6 +327,8 @@ Plan-body verification stays **lightweight** even under this posture — the `ve
322
327
 
323
328
  ## Round protocol (single round at default `maxRounds=1`)
324
329
 
330
+ Every plan-body verification and self-fix instruction passes [okstra-lead-contract](./okstra-lead-contract.md) "Worker instruction quality gate" before dispatch. The lead names the exact plan-item IDs, current defects or dissent, authoritative paths, requested changes or verdict fields, allowed literals, and completion conditions for that round.
331
+
325
332
  Before each verifier call, write one task-instructions file under the current
326
333
  run's `state/` directory and run `okstra agent-prompt materialize` with
327
334
  `--audience reverification-worker`,
@@ -346,14 +353,16 @@ CLI-wrapper calls follow the planned execution surface after
346
353
  consume only `modelExecutionValue`. A missing or invalid invocation contract blocks the
347
354
  round before any host or provider process starts.
348
355
 
349
- 1. Lead runs `okstra plan-items prepare --narrative <report-writer-narrative.md> --run-manifest <run-manifest>`, places the fixed output of `okstra plan-items prompt --run-manifest <run-manifest>` verbatim in every verifier prompt, then runs `okstra plan-items validate-prepared --narrative <report-writer-narrative.md> --run-manifest <run-manifest>`. After a self-fix rewrite, pass `--state <plan-body-verification.json>` on prepare and validate-prepared so the dispatch queue is the changed items plus their stage closure, not the full extract. Python resolves the one convergence-owned state path from that run identity. Dispatch only after that exact-match validation succeeds. The prompt is the dispatch queue: `observed` / `deferred` stages are omitted; plan-wide items (`P-Dir-1`, `P-Var-*`, `P-Dep-*`, `P-Opt-*`) stay. Unscoped `P-Val-*` / `P-Req-*` / `P-Rb-*` stay on the first round, then only when their own `contentHash` changed — a neighbour rewrite must not resweep them. **Enforced:** `okstra_ctl.plan_items.dispatch_item_ids` / `reverify_item_ids`. After `okstra incremental-carry`, the carry copies unchanged checklist verdicts, rewrites `dispatchQueue`, and updates the sibling `plan-items-*.json` so the next `okstra plan-items prompt` does not re-score a carried row.
356
+ 1. Lead runs `okstra plan-items prepare --narrative <report-writer-narrative.md> --run-manifest <run-manifest>`, places the fixed output of `okstra plan-items prompt --run-manifest <run-manifest>` verbatim in every verifier prompt, then runs `okstra plan-items validate-prepared --narrative <report-writer-narrative.md> --run-manifest <run-manifest>`. After a self-fix rewrite, pass `--state <plan-body-verification.json>` on prepare and validate-prepared so the dispatch queue is the changed items plus their stage closure, not the full extract, and so each re-dispatched item carries the previous round's dissent (§"Re-verification rounds (round 2+)"). Python resolves the one convergence-owned state path from that run identity. Dispatch only after that exact-match validation succeeds. The prompt is the dispatch queue: `observed` / `deferred` stages are omitted; plan-wide items (`P-Dir-1`, `P-Var-*`, `P-Dep-*`, `P-Opt-*`) stay. Unscoped `P-Val-*` / `P-Req-*` / `P-Rb-*` stay on the first round, then only when their own `contentHash` changed — a neighbour rewrite must not resweep them. **Enforced:** `okstra_ctl.plan_items.dispatch_item_ids` / `reverify_item_ids`. After `okstra incremental-carry`, the carry copies unchanged checklist verdicts, rewrites `dispatchQueue`, and updates the sibling `plan-items-*.json` so the next `okstra plan-items prompt` does not re-score a carried row.
350
357
 
351
358
  **Then seed the landing table (BLOCKING):** `okstra plan-items seed --narrative <report-writer-narrative.md> --state <plan-body-verification.json> --run-manifest <run-manifest.json>`. `apply-verdicts` in step 8 refuses a verdict whose item has no `planBodyVerification.planItems[]` row. The report writer never owns that state, so the deterministic seed is the only creator of its rows. The seed is idempotent by id and never touches existing verdicts, so it is safe to re-run between rounds and after a self-fix re-extraction. It does refresh `contentHash` from the current extract. Skipping it makes step 8 fail with `plan-body state has no row for [...]`.
352
359
 
360
+ **On a re-run, add `--prior-state <the previous run's plan-body-verification-<task-type>-<seq>.json>`.** A newly seeded item whose `contentHash` equals that run's `verifiedContentHash` for the same id inherits its verdicts, is tagged `carriedForwardFromSeq`, and drops out of round 1's `dispatchQueue` — in the state and in the sibling `plan-items-*.json` the next `okstra plan-items prompt` reads. Without it a re-run re-judges every line the previous run already settled, and the same dissent re-opens under a new `P-*` number. A matching id alone never carries: `P-*` ids are positional, so a plan that gained one line hands the old id to a different sentence. The flag requires `--state` and refuses a prior state file that sits under another task's root — a plan-body state carries no task identity of its own, so its path is the only identity there is. **Enforced:** `okstra_ctl.plan_items_cli._carry_prior_run_verdicts`.
361
+
353
362
  **`--run-manifest` is what scopes the gate to the stage you are starting.** Seed uses it to overlay disk `done` / `active` onto `planBodyVerification.stageLedger`. The current plan's depends-on fills `ready` / `blocked` when no prior plan exists, so a first run does not treat every stage as in-scope. **Enforced:** `okstra_ctl.plan_items.planning_stage_ledger`.
354
363
  2. For each analyser worker in the roster (`claude`, `codex`, and `antigravity` if opted in), lead constructs a reverify prompt using the template in §"Plan-body reverify prompt" below.
355
- 3. Dispatch uses the same wrapper infrastructure as finding convergence, so the `--role-slug` is the same canonical `<role>-worker` that convergence uses — not a round-specific slug. Result file path: `runs/<task-type>/worker-results/<role>-worker-plan-verify-r<N>-implementation-planning-<seq>.md` (e.g. `codex-worker-plan-verify-r1-implementation-planning-003.md`). **`<seq>` is the report's sequence** — the one in this run's `final-report-<task-type>-<seq>` filename, NOT the `workerResults` sequence the initial analysis results carry. The two are equal in most runs and diverge in some (`reports: 004` alongside `workerResults: 005` is a real case), and provenance globs on the report's. Picking the other one makes `_validate_plan_body_verdict_provenance` report that no result file exists while the file is sitting in the directory. The `-worker-` token is load-bearing twice over: §"Plan-body reverify prompt" requires the same anchor headers as convergence, whose `**Audit sidecar path:**` is derived by `okstra_ctl.worker_artifact_paths.audit_sidecar_rel()` inserting `-audit-` after that token — a slug without it makes the header underivable and the helper raises. Record each `planItems[].verdicts[].worker` as the same `<role>-worker` string, because provenance compares it to this filename's prefix. **Enforced:** `tests/contract/test_reverify_dispatch_anchors.py` derives the sidecar from the documented name and re-extracts the prefix the provenance resolver uses.
356
- **Verdict provenance (BLOCKING).** Every verdict recorded in `planItems[].verdicts[]` MUST trace back to a dispatch that actually returned a result file at the path above. The whole gate — classification, self-fix eligibility, promotion, `gateBlockedBy` — is computed from these votes, so an unbacked vote lets the round be skipped while the gate still reads `passed`. **Enforced:** `validators/validate-run.py` `_validate_plan_body_verdict_provenance` fails any `verdicts[].worker` with no matching `<worker>-plan-verify-r<N>-<task-type>-<seq>.md` result file. Recording a `verification-error` for a dispatch that produced no result is the correct way to represent a failed worker — inventing an `AGREE` is a contract violation.
364
+ 3. Dispatch uses the same wrapper infrastructure as finding convergence, so the `--role-slug` is the same canonical `<role>-worker` that convergence uses — not a round-specific slug. Result file path: `runs/<task-type>/worker-results/<role>-worker-plan-verify-r<N>-implementation-planning-<seq>.md` (e.g. `codex-worker-plan-verify-r1-implementation-planning-003.md`). **`<seq>` is the report's sequence** — the one in this run's `final-report-<task-type>-<seq>` filename, NOT the `workerResults` sequence the initial analysis results carry. The two are equal in most runs and diverge in some (`reports: 004` alongside `workerResults: 005` is a real case). Provenance no longer globs by either seq: it resolves the expected filenames from this run's team-state `workerDispatches[]` rows — the paths the dispatches actually recorded — and falls back to the seq glob only when no team-state is readable, saying so in its finding. The `-worker-` token is load-bearing twice over: §"Plan-body reverify prompt" requires the same anchor headers as convergence, whose `**Audit sidecar path:**` is derived by `okstra_ctl.worker_artifact_paths.audit_sidecar_rel()` inserting `-audit-` after that token — a slug without it makes the header underivable and the helper raises. Record each `planItems[].verdicts[].worker` as the same `<role>-worker` string, because provenance compares it to this filename's prefix. **Enforced:** `tests/contract/test_reverify_dispatch_anchors.py` derives the sidecar from the documented name and re-extracts the prefix the provenance resolver uses.
365
+ **Verdict provenance.** Every verdict recorded in `planItems[].verdicts[]` MUST trace back to a dispatch that actually returned a result file. The whole gate — classification, self-fix eligibility, promotion, `gateBlockedBy` — is computed from these votes, so an unbacked vote lets the round be skipped while the gate still reads `passed`. **Enforced (advisory):** `validators/validate-run.py` `_validate_plan_body_verdict_provenance` reports any `verdicts[].worker` with no recorded reverify dispatch whose result file exists; the finding is not in the blocking allowlist, so it surfaces as an advisory rather than failing the run — record it, never dismiss it. Recording a `verification-error` for a dispatch that produced no result is the correct way to represent a failed worker — inventing an `AGREE` is a contract violation, and renaming a result file to make provenance match destroys the link the check reads.
357
366
 
358
367
  4. After all dispatches return, lead aggregates verdicts per `P-*` item across workers and classifies each:
359
368
 
@@ -364,7 +373,7 @@ round before any host or provider process starts.
364
373
  - `dissent-isolated` — only one worker `DISAGREE`s, others `AGREE`. On a blocking kind (`b` / `c` / `e`, and kind `a` on `P-Var-*`) this is scored `majority-disagree` and **blocks approval**. Advisory-only `DISAGREE(d)` and `P-Rb-*` stay recorded dissent and do not block. (Distinct from finding-convergence `worker-unique`, which means the *opposite*: only one worker AGREEs.)
365
374
  - `majority-disagree` — a *majority* of analysers `DISAGREE` (majority needs ≥2 participating non-error votes; rollback-ordering `DISAGREE(d)` votes are advisory and excluded from the tally), OR any blocking-kind dissent with ≥2 participating votes (a minority `DISAGREE` is not outvoted), OR any single-vote-blocking kind fires: one reproduced `DISAGREE(a)` on any item other than a `P-Var-*` one, or one reproduced `DISAGREE(f)` on a `P-Req-*` item (see §"Single-vote-blocking kinds"). This classification **blocks approval**.
366
375
  - `needs-reverify` — one of two shapes the round could not settle.
367
- - **An even split on a blocking kind.** The majority test is strict, so a panel splitting evenly (1-AGREE / 1-DISAGREE, 2-2, …) reaches neither `full-consensus` nor `majority-disagree`. Until this shape existed it folded into `has-dissent` and the gate passed. Do **not** re-run the original two. Dispatch `critic-worker` immediately on those items only (`okstra plan-items prepare --tie-vote`, then `okstra plan-items prompt`). The prompt carries the analyser split and no other plan items. Record the critic vote as `verdicts[].worker = critic-worker` with `okstra plan-items apply-verdicts --append`. Critic `AGREE` / `SUPPLEMENT` settles the split to `has-dissent`. Critic `DISAGREE` on a blocking kind is `majority-disagree`. **Enforced:** `validators/validate-run.py` `_classify_plan_item_gate` / `_validate_tie_received_extra_vote` / `_validate_unresolved_tie_was_reverified`, `okstra_ctl.plan_items.next_dispatch` kind `critic-tie`.
376
+ - **An even split on a blocking kind.** The majority test is strict, so a panel splitting evenly (1-AGREE / 1-DISAGREE, 2-2, …) reaches neither `full-consensus` nor `majority-disagree`. Until this shape existed it folded into `has-dissent` and the gate passed. Do **not** re-run the original two. Dispatch `critic-worker` immediately on those items only (`okstra plan-items prepare --tie-vote`, then `okstra plan-items prompt`). The prompt carries the analyser split and no other plan items. Read the answer with `okstra plan-items collect-verdicts --items <the `--tie-vote` plan-items artifact> --result critic-worker=<path> --output <envelope>` — `--items` takes that artifact, whose `dispatchQueue` is the tie items, so pointing the next step at the raw result is refused against this round's full queue. Record the critic vote as `verdicts[].worker = critic-worker` with `okstra plan-items apply-verdicts --append`. Critic `AGREE` / `SUPPLEMENT` settles the split to `has-dissent`. Critic `DISAGREE` on a blocking kind is `majority-disagree`. **Enforced:** `validators/validate-run.py` `_validate_unresolved_tie_was_reverified` fails an in-scope item that carries an even split on a blocking kind and has neither a `critic-worker` vote nor a `blocks: approval` clarification row, `_classify_plan_item_gate` scores the tie shape and fails a settled classification the votes do not support, and `okstra_ctl.plan_items.next_dispatch` returns kind `critic-tie` for exactly these items, so the tie round is the queue the CLI hands you rather than one you assemble. **With no critic on the roster the split is a user decision, not another round.** `okstra plan-items next-dispatch --run-manifest <current-run-manifest.json>` answers `user-decision` (not `critic-tie`) when the run's `invocationAssignments` carries no `critic/*` entry, and its `itemIds` are the tie items. For each of them do what step 8 does for a surviving `majority-disagree`: `okstra approval-decision open` with `approvalContext.classification` set to `noncritical-dissent`, plus the matching `## 1. Clarification Items` row at `Blocks=approval`. Every tie that reaches this shape is non-correctness by construction — `_is_correctness_critical` is false for kinds `b`/`c`/`e` and for a `P-Var-*` item, and the single-vote-blocking kinds (`a`, and `f` on `P-Req-*`) never reach the tie branch at all. Dispatch no further verification for those items. The `Blocks=approval` row is what withholds approval until the user disposes, exactly as for any other approval row; the gate itself still folds `needs-reverify` into `passed-with-dissent`, so the round closes on the gate it actually scored. A tie left with neither a critic vote nor a decision row surfaces as recorded dissent plus an `advisories[]` entry, not a round-blocking failure. **Enforced:** `okstra_ctl.plan_items.critic_is_rostered` reads the roster and `next_dispatch` returns the kind; `validators/validate-run.py` `_validate_unresolved_tie_was_reverified` reads a `blocks: approval` row linked to the item as the settlement and emits its advisory only when neither settlement is recorded.
368
377
  - **A lone dissent nobody cross-verified** — a single-vote-blocking kind fired but the item has **fewer than 2 participating non-error votes**, i.e. the lone dissent was never cross-verified because its peer returned `verification-error`. A single-vote-blocking kind means "one *confirmed* DISAGREE is enough"; an unconfirmed one is not, and on a `P-Var-*` item none fires at all — its kind `a` never blocks on one vote and takes a majority like `b` / `e`. This does **not** block approval — blocking on it would make a worker failure produce a stricter gate than a healthy roster, the same paradox the ≥2-vote majority rule already rules out. The item is re-dispatched in the next round (step 7); if it survives the round budget it is promoted per step 8 with a Statement that says verification never completed. **Enforced:** `validators/validate-run.py` `_classify_plan_item_gate` returns `needs-reverify` for this shape and `_recompute_plan_body_gate` folds it into `passed-with-dissent`.
369
378
  - `contested` only meaningful when `maxRounds > 1`; at default `maxRounds=1`, fold any unresolved item into `partial-consensus`.
370
379
  5. Gate result resolution:
@@ -381,9 +390,11 @@ round before any host or provider process starts.
381
390
 
382
391
  and records what it returns: `gate.recomputed` is the round's gate value, `gate.blockedBy` its `gateBlockedBy` causes, `gate.blockingItems` the items that block. The same call runs every plan-body check a round can be judged on alone — provenance, fixability, subject substance, self-fix grouping, round recording, clarification matching, state-file rounds — and exits 2 with `failures[]` when the round is not contract-clean. **A round is not complete while that exit code is non-zero.**
383
392
 
393
+ **`failures[]` carries only the blocking findings; the rest arrive as `advisories[]`.** What blocks is the allowlist in `scripts/okstra_ctl/blocking_checks.py` — the same set the finished run's validation blocks on. Every other finding is printed on stderr as `validate-run: advisory — …`, returned under `advisories[]`, and does not change the exit code. Record those in the round's notes; do not re-run the round for them and do not edit state to make them disappear. Without this split a check the completed run would let pass — an unsettled analyser tie, a `participatingAnalysers` count recomputed across rounds, a `subject` drift with no repair command — held the round open with no in-band way to close it. **Enforced:** `validators/validate-run.py` `_partition_round_failures` partitions both round-boundary entry points through `okstra_ctl.blocking_checks.partition`.
394
+
384
395
  Reading the rules in this section and tallying the votes in an ad-hoc script is a contract violation, not a shortcut. The classification carries five special cases that a re-derivation drops one at a time — single-vote-blocking `a`/`f`, advisory-only `d`, the `P-Var-*` majority gate, the `P-Rb-*` exemption, and the `needs-reverify` shape — and a hand-written tally re-derives them from scratch every round, so the loop's later rounds score differently from its first. One implementation, called once per round, is what keeps round 5 scored the same way as round 1.
385
396
 
386
- **Record the cause, not just the outcome.** The gate value names the outcome; `planBodyVerification.gateBlockedBy` (array) names every input that blocked it — `majority-disagree`, `coverage-gap`, `non-result`. Two independent inputs can block: a `majority-disagree` plan item, and a Requirement Coverage `gap` / `blocked C-NNN` row (`prompts/profiles/implementation-planning.md` §"Requirement Coverage"). A coverage-only block still renders as `blocked-by-disagreement` because that is the only blocking non-abort value, so **without `gateBlockedBy` the report asserts a worker disagreement that never happened** and the reader hunts for a dissent that does not exist. Leave the array empty for a passing gate. **Enforced:** `validators/validate-run.py` `_validate_gate_blocked_by` cross-checks the declared causes against the recorded verdicts and coverage rows, and fails a passing gate that has a blocking coverage row — the coverage rule was prose-only before.
397
+ **Record the cause, not just the outcome.** The gate value names the outcome; `planBodyVerification.gateBlockedBy` (array) names every input that blocked it — `majority-disagree`, `coverage-gap`, `non-result`. Two independent inputs can block: a `majority-disagree` plan item, and a Requirement Coverage `gap` / `blocked C-NNN` row (`prompts/profiles/implementation-planning.md` §"Requirement Coverage"). A coverage-only block still renders as `blocked-by-disagreement` because that is the only blocking non-abort value, so **without `gateBlockedBy` the report asserts a worker disagreement that never happened** and the reader hunts for a dissent that does not exist. Leave the array empty for a passing gate. **Enforced:** `validators/validate-run.py` `_validate_gate_blocked_by` fails a passing gate that has a blocking coverage row — the coverage rule was prose-only before. The declared array itself is not compared against a recomputed one: `okstra plan-verify` returns `gate.blockedBy` from the same computation that produced the gate value, so recording what it returns is what makes the array right.
387
398
 
388
399
  **A coverage row citing this run's own `C-NNN` is not an independent blocker.** When a coverage row's `blocked C-NNN` points at a clarification that step 8 below promoted from a `majority-disagree` item in *this same run*, that blocker is already counted once as the plan item. Counting it again as a coverage gap makes the run block on a clarification it just authored, and the row carries into the next run as a fresh blocker — the Requirement Coverage ↔ Clarification cycle. Such rows are excluded from `coverage-gap`. **Enforced:** `validators/validate-run.py` `_independent_coverage_blockers`.
389
400
  6. `okstra plan-items complete-round --run-manifest <current-run-manifest.json>` derives `planBodyVerification.participatingAnalysers` from the current assigned roster and persisted votes, then atomically records the completed round. The gate arithmetic is unchanged, but a shrunken roster changes what the round can settle: with two participating analysers a 1-AGREE / 1-DISAGREE split is a tie, so it reaches neither consensus nor `majority-disagree` and the item has to go back for a round (see `needs-reverify` above). **Enforced:** `validators/validate-run.py` `_validate_participating_analysers` recomputes `voting` from the recorded verdicts and fails a declared figure the table denies. `validators/validate-run.py` `_detect_uniform_verifier` remains advisory; do not copy its JSON output into state.
@@ -395,7 +406,8 @@ round before any host or provider process starts.
395
406
  | `none` | Do not add a worker batch. A round whose only failures are missing-dependency command runs — `UNVERIFIABLE` on a declared `npm` / `pytest` / equivalent, §"Planning-time environment gap" — is this shape. |
396
407
  | `worker-correction` | Re-dispatch **only** those workers. Peers are not re-run. The queue does not become a new round. Place the output of `okstra plan-items correction-prompt --worker <id> --run-manifest … --state …` first in that worker's prompt — the environment-exception paragraph is first. A byte-identical re-dispatch reproduces the same failure; a corrected one recovered 37 substantive verdicts from a worker whose first attempt answered `UNVERIFIABLE` to all 80 items. |
397
408
  | `queue-reverify` | An unsettled tie on a blocking kind. Legacy kind; current scoring emits `critic-tie` instead. |
398
- | `critic-tie` | An unsettled analyser tie. Dispatch `critic-worker` on those `itemIds` only. The critic's verdict settles the split. Do not re-run the original two. |
409
+ | `critic-tie` | An unsettled analyser tie on a run that rostered a critic. Dispatch `critic-worker` on those `itemIds` only. The critic's verdict settles the split. Do not re-run the original two. |
410
+ | `user-decision` | The same unsettled tie on a run with **no** critic rostered. There is no in-band vote left, so open no worker batch: for each `itemId` run `okstra approval-decision open` with classification `noncritical-dissent` and write the matching `## 1. Clarification Items` row at `Blocks=approval`, exactly as step 8 does for a surviving `majority-disagree`. Do not dispatch further verification for those items and do not re-run the original two. |
399
411
 
400
412
  A referenced **path** that does not exist is still `DISAGREE(b)` / a fact probe, never environment-unverifiable. **Enforced:** `okstra_ctl.plan_items.next_dispatch` / `correction_prompt_text`.
401
413
 
@@ -414,26 +426,28 @@ round before any host or provider process starts.
414
426
  - Lead re-runs plan-body verification, then records each worker Markdown result through `okstra plan-items apply-verdicts --state <plan-body-verification.json> --result <worker>=<result.md> --round <N>`. Score the result with `okstra plan-verify --narrative <report-writer-narrative.md> --state <plan-body-verification.json>`, then call `okstra plan-items complete-round --state <plan-body-verification.json> --run-manifest <current-run-manifest.json> --round <N>`. These commands fail on an assigned item the worker left unanswered, on a verdict for an item outside the queue, and on a duplicate worker result.
415
427
  - For a self-fix, record the correction through the typed convergence command rather than writing `selfFixNote` or `selfFixGroups` JSON. A resolved item does not create a clarification.
416
428
  - **Each round is a worker batch.** Before dispatching round N ≥ 2, reclaim the previous round's completed verifiers exactly as at any other batch boundary ([okstra-lead-contract](./okstra-lead-contract.md) "Run-scoped worker-resource lifecycle") and emit `PROGRESS: phase-batch-cleanup panes=<n>`, then announce the round with `PROGRESS: phase-5.5.9-plan-verify round=<N> items=<count>`. Saying a round will "reuse" the previous verifiers and then dispatching under fresh names leaves every prior round holding its panes — five rounds of that is what exhausts the pane budget and blocks the next dispatch. **Enforced:** `validators/validate_session_conformance.py` `_check_plan_verify_cleanup_checkpoints` requires both lines once the state file records two or more rounds.
417
- - **Round completion.** A round is complete only after `okstra plan-verify` exits 0 and `okstra plan-items complete-round` succeeds. A round left with a non-zero exit carries its defect into the next round's inputs. Report assembly and rendering occur only after the convergence state is terminal. **Enforced:** `validators/validate-run.py` `_validate_plan_body_state_rounds` requires one stored round per round number and a corresponding item vote.
418
- - **Loop termination.** The convergence command owns the round count and stop reason; the lead does not write either JSON field. A user-directed correction does not consume the automatic self-fix limit, and a verification failure after that correction does not restart the automatic loop:
429
+ - **Round completion.** A round is complete only after `okstra plan-verify` exits 0 and `okstra plan-items complete-round` succeeds. A round left with a non-zero exit carries its defect into the next round's inputs. Exit 0 with a non-empty `advisories[]` is a complete round: those findings are recorded, not round-blocking (step 5, §"`failures[]` carries only the blocking findings"). Report assembly and rendering occur only after the convergence state is terminal. **Enforced:** `validators/validate-run.py` `_validate_plan_body_state_rounds` requires one stored round per round number and a corresponding item vote.
430
+ - **Loop termination.** The convergence command owns the round count and stop reason; the lead never edits either field in the state file. Both are recorded by `okstra plan-items complete-round`, and its two self-fix flags are not interchangeable. `--self-fix-group` is a claim that this round rewrote something, so it sets `selfFixGroups` and `selfFixRoundsApplied` and **requires** `--self-fix-stop-reason` — there is no default, because the value that used to fill in (`all-resolved`) is the one value step 8 reads as "nothing was left unresolved", which then forbids promoting the items the round did leave unresolved. `--self-fix-stop-reason` on its own is how a round that rewrote nothing records that the loop stops there; it writes `selfFixStopReason` and touches neither `selfFixGroups` nor `selfFixRoundsApplied`, because `validators/validate-run.py` `_validate_self_fix_grouping` requires the highest `selfFixGroups[].round` to equal `selfFixRoundsApplied` — a round number raised without a group has no value that passes. A user-directed correction does not consume the automatic self-fix limit, and a verification failure after that correction does not restart the automatic loop:
419
431
  - `all-resolved` — no planner-fixable `majority-disagree` item remains. Exit.
420
432
  - `no-progress` — the round resolved **zero** planner-fixable items relative to the previous round. Exit even with budget left: the same rewrite would repeat. Newly *introduced* defects count against progress, so a rewrite that trades one defect for another stops the loop rather than churning. A round that re-targets only what the previous round left unresolved is this same conclusion reached one dispatch earlier — exit on it under this reason rather than paying for the round that proves it. **Enforced (advisory):** `validators/validate-run.py` `_detect_self_fix_recurrence` warns on that shape and names this stop reason.
421
- - `max-rounds-reached` — `selfFixRoundsApplied == selfFixMaxRounds`. Exit.
422
- - `cause-group-recurrence` — legacy read-only value for reports produced before activity contract v1. A new activity-contract-v1 run MUST NOT emit it because there is no second automatic self-fix round in which a cause group can recur. **Enforced:** `validators/validate-run.py` `_validate_activity_contract_plan_limits`. A legacy report that already carries it is still an *exhausted* loop, so step 8 may promote its surviving items: `_SELF_FIX_EXHAUSTED_REASONS` admits this value alongside `no-progress` / `max-rounds-reached`. Refusing it there left such a report with no exit at all — the loop may not run again, and the item may not be promoted either.
433
+ - `max-rounds-reached` — the rewrites are spent: the number of distinct rounds in `selfFixGroups` equals `selfFixMaxRounds`. Exit. Do not read this as `selfFixRoundsApplied == selfFixMaxRounds`. `selfFixRoundsApplied` is the **round number** the last self-fix was recorded on, not a count of rewrites (`validators/validate_session_conformance.py` `_self_fix_event_expectation` states this and counts the group rounds instead), and round 1 is the initial verification, so at the default `selfFixMaxRounds=1` the one rewrite lands on round 2 and the field reads `2` — the equality is never true and the exit it names is unreachable. The arithmetic that holds is `selfFixRoundsApplied - 1 == selfFixMaxRounds`. The same field also sets the batch ceiling: `_allowed_automatic_rounds` allows `2 + selfFixRoundsApplied` automatic plan-body batches, so the value is a round number on both sides.
434
+ - `cause-group-recurrence` — legacy read-only value for reports produced before activity contract v1. A new activity-contract-v1 run cannot emit it because there is no second automatic self-fix round in which a cause group can recur. **Enforced at the only writer:** `okstra plan-items complete-round --self-fix-stop-reason` accepts `all-resolved` / `no-progress` / `max-rounds-reached` and nothing else (`scripts/okstra_ctl/plan_items_cli.py:261`), and it is what sets `selfFixStopReason`, so the value has no way into a new report. A legacy report that already carries it is still an *exhausted* loop, so step 8 may promote its surviving items: `_SELF_FIX_EXHAUSTED_REASONS` admits this value alongside `no-progress` / `max-rounds-reached`. Refusing it there left such a report with no exit at all — the loop may not run again, and the item may not be promoted either.
423
435
  - `not-attempted` — the loop never ran because no item qualified.
424
436
  The `no-progress` and `max-rounds-reached` exits are what make the loop terminate; `selfFixMaxRounds` alone is the backstop.
425
- - a `majority-disagree` item with a majority of its deciding `DISAGREE` votes at `needs-user-input` is NOT a self-fix target — after correctness-critical precedence, it goes straight to the next step as `user-decision` rather than generic `noncritical-dissent`. **Enforced:** `validators/validate-run.py` `_expected_approval_classification`.
426
- 8. For every **in-scope execution** `majority-disagree` item **that remains after the self-fix loop** (items not resolved by self-fix, or with a `needs-user-input` majority from the start), lead adds a row to `## 1. Clarification Items` with:
437
+ - a `majority-disagree` item with a majority of its deciding `DISAGREE` votes at `needs-user-input` is NOT a self-fix target — after correctness-critical precedence, it goes straight to the next step as `user-decision` rather than generic `noncritical-dissent`. **Enforced (promotion path):** `validators/validate-run.py` `_validate_self_fix_before_clarification` demands an exhausted self-fix budget only of `planner-fixable` majorities, so a `needs-user-input` majority is promotable with no self-fix round, and `_validate_plan_body_clarification_matching` fails it when it reaches no `blocks: approval` row. The classification *value* is authoring guidance per step 8: `scripts/okstra_ctl/approval_decisions.py` checks it against the classification enum and its allowed dispositions only — no validator recomputes it from the votes' `fixability`.
438
+ 8. For every **in-scope execution** `majority-disagree` item **that remains after the self-fix loop** (items not resolved by self-fix, or with a `needs-user-input` majority from the start), and for every in-scope tie item `next-dispatch` returned under `user-decision` (§"even split" — a criticless roster has no vote left to settle it), lead adds a row to `## 1. Clarification Items` with:
427
439
  - do **not** promote an `observed` / `deferred` / `record` item. Those belong in `setAside`. A `Blocks=approval` C row for a frozen or unreached stage is how the clarification list grew while the next stage was already executable. **Enforced:** `validators/validate-run.py` `_validate_plan_body_clarification_matching` uses `_plan_item_gate_class` (after stage scope), not the raw vote class.
428
440
  - new `C-<N>` ID (numbering continues from any existing rows)
429
441
  - `Statement` summarising the disagreement and the worker breakage `<kind>`
430
442
  - `Kind` chosen per the standard policy (usually `decision` for option-level conflicts, `data-point` for path/symbol mismatches)
431
443
  - `Blocks=approval`
444
+ - `origin=worker-finding` — the verifiers reached the defect on their own evidence (`_common-contract.md` §"Clarification request policy"). Do not invent a phase-named value: `origin` is a schema enum and `okstra approval-decision open` rejects anything outside it (`scripts/okstra_ctl/approval_decisions.py` `_reject_off_enum`); one report was published with `origin: "plan-body-verification"` and every later `okstra user-response` read of it failed schema validation. `lead-directed` is also wrong here — it marks the lead's own judgment and cannot be deferred without an interactive session.
445
+ - `userConfirmation` recording what actually happened before the row was written: `asked-and-answered`, `asked-awaiting`, or `deferred-no-interactive-session`.
432
446
  - the report record's `planItems[].clarificationRefs[]` reaching that `C-<N>`. Under contract v3 the report record carries the **plural** field and the v3.0 schema forbids `clarificationId` on a plan item; report assembly derives the refs from the activity ledger's `clarificationRefs[]` + `planItemIds[]`, so record the decision through `okstra approval-decision` rather than writing the link by hand. The lead-owned state file keeps the singular `clarificationId`. `validators/validate-run.py` `_validate_plan_body_clarification_matching` recomputes each item's class and fails when a majority-disagree item reaches no clarification, or reaches one that is missing or not `blocks: approval`.
433
- - set `approvalContext.classification` to `user-decision` for a majority `needs-user-input` item, `correctness-critical` for `DISAGREE(a)`, `DISAGREE(f)` on `P-Req-*`, or an independent Requirement Coverage blocker, and `noncritical-dissent` for another surviving majority disagreement.
447
+ - set `approvalContext.classification` to `user-decision` for a majority `needs-user-input` item, `correctness-critical` for `DISAGREE(a)`, `DISAGREE(f)` on `P-Req-*`, or an independent Requirement Coverage blocker, and `noncritical-dissent` for another surviving majority disagreement or an unsettled tie promoted under `user-decision`.
434
448
  - record the decision through `okstra approval-decision open`. Each option carries `disposition`, exactly one `reach`, and optional `scopeEffects`. The activity ledger carries affected `planItemIds` and `clarificationRefs`; the approval row never copies those backtrace IDs. `select` is allowed only for `user-decision`. `accept-risk` is allowed for every classification, including `correctness-critical`: it ends the gate and keeps the DISAGREE votes as evidence. `request-revision` / `reject` withhold the next phase. **Enforced:** `scripts/okstra_ctl/approval_decisions.py` and report assembly.
435
449
  - **Self-fix exhaustion is not risk acceptance.** A remaining item stays blocking until the user selects `accept-risk`, `select`, or `answer`. Record the user's non-empty original text. Do not require a second verification round to honour `accept-risk`.
436
- - **Correctness-critical `accept-risk` does not rewrite the votes.** The linked items keep their DISAGREE (or remaining dissent) so a later stage can still see them. `request-revision` is the path that corrects the plan and re-verifies. **Enforced:** `validators/validate-run.py` `_user_accepted_plan_item_ids` / `_validate_correctness_resolution`.
450
+ - **Correctness-critical `accept-risk` does not rewrite the votes.** The linked items keep their DISAGREE (or remaining dissent) so a later stage can still see them. `request-revision` is the path that corrects the plan and re-verifies. **Enforced by keeping the two ledgers apart:** `okstra approval-decision resolve` (`scripts/okstra_ctl/approval_decisions.py` `resolve_decision`) appends the disposition to the approval ledger and touches no verdict; the votes live in the plan-body state file, which only `okstra plan-items apply-verdicts` writes. An `accept-risk` that appears to have cleared a DISAGREE means the state file was edited by hand, not that the disposition did it.
437
451
  - When a correctness-critical `planner-fixable` item is promoted, its `Statement` MUST state "planner self-fix attempted but unresolved" and name the stop reason. `validators/validate-run.py` `_validate_self_fix_before_clarification` fails when a planner-fixable majority item is promoted while the budget is not exhausted — it requires `selfFixRoundsApplied >= 1` **and** `selfFixStopReason` in `{no-progress, max-rounds-reached}`, so neither `all-resolved` nor `not-attempted` can excuse a promotion.
438
452
  - Approval state transitions are fixed:
439
453
  - `open → answered` when the raw user response is recorded
@@ -571,6 +585,8 @@ The per-round structures mirror the finding-convergence state artifact ([converg
571
585
 
572
586
  ## Plan-body reverify prompt
573
587
 
588
+ **Enforced:** plan-body reverify prompts go through the same dispatch gate as finding convergence — `okstra_ctl.worker_prompt_contract.validate_reverify_prompt`, called from `dispatch_state.py`, which checks the `**Model:**` header against the dispatch model, the phase-boundary block's position, and the required output-contract block.
589
+
574
590
  Required prompt anchor headers are identical to finding convergence (see [convergence](./convergence.md) §"Required reverify-prompt anchor headers"). The prompt body changes from F-* listing to P-* listing:
575
591
 
576
592
  The [convergence](./convergence.md) §"Required reverify output contract"
@@ -670,9 +686,14 @@ alternative stated in `coveredBy`, each `decisionRefs` target, and the
670
686
  name an open `Blocks=approval` clarification in this report).
671
687
 
672
688
  For each `P-Prep-S<stage>-<kind>` item, use only the supplied detector trigger
673
- evidence, its single `designSurfaceCoverage` row, and referenced PREP items.
674
- Apply the disposition-specific AGREE/DISAGREE rules in this contract; do not
675
- rerun a free-form requirements analysis. Every DISAGREE includes Fixability.
689
+ evidence (a v2 historical queue may additionally carry the persisted
690
+ `designSurfaceCoverage` row and referenced PREP items). `designPreparation` /
691
+ `designSurfaceCoverage` are report-assembly-owned and produced **after** this
692
+ verification, so the narrative under review carries neither — their absence is
693
+ the contract, not a defect, and demanding them of the writer is asking for a
694
+ contract violation the self-fix loop cannot resolve. Apply the
695
+ disposition-specific AGREE/DISAGREE rules in this contract; do not rerun a
696
+ free-form requirements analysis. Every DISAGREE includes Fixability.
676
697
 
677
698
  ## Plan items to verify
678
699
 
@@ -692,15 +713,22 @@ rerun a free-form requirements analysis. Every DISAGREE includes Fixability.
692
713
  ...
693
714
 
694
715
  ### P-Prep-S2-external-interface [TICKETID: <id>]: <one-line summary>
695
- **From section**: Stage 2 designSurfaceCoverage + 5.5.10 Implementation Design Preparation
716
+ **From section**: Stage 2 + 5.5.10 Implementation Design Preparation
696
717
  **Trigger evidence**: <detector-produced evidence rows>
697
- **Coverage row**: <the single designSurfaceCoverage row>
698
- **Referenced PREP items**: <verbatim referenced items, or none>
718
+ (the queue renders the item's `stage` / `kind` / `evidence` payload plus its
719
+ ownership-boundary note; a persisted `designSurfaceCoverage` row appears only
720
+ in a v2 historical queue)
699
721
  **Payload**:
700
722
  > <lossless persisted payload>
701
723
 
702
724
  ## Response format
703
725
 
726
+ This block already arrives with the queue: `okstra plan-items prompt` output
727
+ ends with it (`okstra_ctl.plan_items.PLAN_VERIFY_RESPONSE_FORMAT`), so placing
728
+ the queue verbatim delivers it. Do not restate, edit, or strip it — a
729
+ hand-rendered variant is how one round lost all 64 verdicts to heading-depth
730
+ and bold-colon drift.
731
+
704
732
  Head each block with `### <plan item id>` at exactly three hashes. The heading
705
733
  depth is not style: the collector parses `^### ` and nothing else, so a block
706
734
  written at any other depth is not an unparsed block — it is a verdict that was
@@ -733,34 +761,25 @@ When `config.adversarial == true`, the lead prepends the adversarial framing fro
733
761
 
734
762
  The "Reverify prompt: required-reading suppression" rule in [convergence](./convergence.md) (lightweight mode does NOT inject a `[Required reading]` clause) applies here as well.
735
763
 
736
- ## Re-verification rounds (round 2+) — carry the dissent forward (BLOCKING)
737
-
738
- The template above is the round-1 prompt. A round 2+ prompt exists to settle the *previous* round's dissent, so it MUST carry that dissent forward. Re-rendering the round-1 template alone is a contract violation: with no record of what was objected to, the dissenting worker restates its verdict unchanged and its peers re-judge from nothing, so the loop spends its whole `selfFixMaxRounds` budget re-deriving the same split. Telling the worker that a prior verdict "carries no weight" is the same defect stated as an instruction — prior dissent is evidence about the plan, and discarding it is what makes the round repeat.
739
-
740
- Changes from the round-1 template:
764
+ ## Re-verification rounds (round 2+) — carry the dissent forward
741
765
 
742
- - The heading reads `(round <N>)`.
743
- - Every re-dispatched item carries a `**Prior round dissent**` block, built from the state file's `planItems[].rounds[].votes` and the item's `selfFixNote`:
766
+ The template above is the round-1 prompt. A round 2+ prompt exists to settle the *previous* round's dissent, so it has to carry that dissent forward. Re-rendering the round-1 template alone wastes the round: with no record of what was objected to, the dissenting worker restates its verdict unchanged and its peers re-judge from nothing, so the loop spends its whole `selfFixMaxRounds` budget re-deriving the same split. Telling the worker that a prior verdict "carries no weight" is the same defect stated as an instruction — prior dissent is evidence about the plan, and discarding it is what makes the round repeat.
744
767
 
745
- ```text
746
- **Prior round dissent** (round <N-1>):
747
- - <worker>: DISAGREE(<kind>) — <that worker's verbatim explanation>
748
- - <worker>: SUPPLEMENT — <verbatim explanation>
749
- **What the planner changed**: <the item's selfFixNote, or "no correction — this item's
750
- text shifted under a neighbouring rewrite">
751
- ```
768
+ **The whole block is machine-generated. The lead writes none of it.** Pass `--state <plan-body-verification.json>` to `okstra plan-items prepare` — the same flag step 1 already requires after a self-fix rewrite — and the CLI does the rest. `prepare` reads each queued item's recorded votes and its `selfFixNote` out of the state file and writes them into the prepared envelope as `priorRounds`; `okstra plan-items prompt` renders them; the lead places that output verbatim, exactly as at round 1. The carry is derived from the state file, so the same state and the same plan produce the same bytes.
752
769
 
753
- An item re-dispatched as `needs-reverify` (its peer returned a non-result, so nothing was objected to) carries the block with `- none — the peer vote was a non-result` in place of the verdict lines.
770
+ What the generated round 2+ prompt has that round 1 does not:
754
771
 
755
- - The `## Instructions` block gains this paragraph, which supersedes any "judge the corrected text on its own merits" framing:
772
+ - A preamble ahead of the queue telling the verifier to read the prior dissent and the planner's correction before judging the CURRENT text, to state what it re-opened if it still disagrees, and to AGREE if the correction resolved its objection. Source: `okstra_ctl.plan_items.REVERIFY_PREAMBLE`.
773
+ - One `**Prior round dissent**` block per re-dispatched item, between that item's `Subject` and its payload — the objection comes before the current text because that is the reading order the preamble asks for. Each recorded vote of the last round it holds renders as one line: the worker, its verdict with the breakage kind, its stated basis (`explanation`, or `note` when the worker left the explanation empty), and its `fixability` when it recorded one. The block closes with `- What the planner changed:` carrying the item's `selfFixNote`, or `no correction — this item's text shifted under a neighbouring rewrite` when the rewrite did not target this item.
774
+ - The preamble's instruction to answer with one line per item, directly under `**Verdict**`:
756
775
 
757
- > Each item below was objected to in the previous round, or its text shifted under a neighbouring correction. Read the prior dissent and the planner's correction before judging the CURRENT text. If you raised the prior objection and still DISAGREE, you MUST state what you re-opened after the rewrite and why the correction is insufficient — restating your previous explanation without engaging the correction is not a valid verdict. If the correction resolves your objection, say so and AGREE.
776
+ `**Prior dissent**: resolved | unresolved | none — <which prior objection, and what the current text does about it>`
758
777
 
759
- - The response format gains one line per item, directly under `**Verdict**`:
778
+ An item with no recorded vote carrying a round number gets no block, and an envelope with nothing to carry keeps the round-1 shape — `priorRounds` is absent and no preamble is prepended, so a first round is unaffected by this section.
760
779
 
761
- `**Prior dissent**: resolved | unresolved | none — <which prior objection, and what the current text does about it>`
780
+ **Enforced:** `okstra plan-items validate-prepared --state <same state>` re-derives the carry and exits 2 when the prepared envelope's `priorRounds` does not match, alongside the `items` / `dispatchQueue` comparison it already made. A prepared queue that dropped the dissent cannot pass the step-1 validation the dispatch is gated on.
762
781
 
763
- **Enforced — on the answer, not on the question.** `validators/validate-run.py` `_validate_reverify_result_addresses_prior_dissent` fails a run whose round 2+ plan-verify **worker results** lack the `**Prior dissent**` line described directly above. Nothing reads the prompt file, so the `**Prior round dissent**` block is a guideline: it is how you make that line answerable, and a worker cannot write it without one. Note the two spellings are different anchors for different artifacts — `**Prior round dissent**` is the block you put in the prompt, `**Prior dissent**` is the line the worker puts in its result, and only the latter is checked (`scripts/okstra_ctl/verdict_blocks.py` parses it as a response field). A prompt that carries the block while the response format omits the line fails this check with the block present.
782
+ The two spellings are different anchors for different artifacts: `**Prior round dissent**` is the block `prompt` puts in the prompt, `**Prior dissent**` is the line the worker puts in its result. `scripts/okstra_ctl/verdict_blocks.py` parses the result line into the verdict block when it is present and leaves it empty when it is not, so an omitted answer line is still silent — the prompt is what is now guaranteed, not the response.
764
783
 
765
784
  ## Worker non-result handling in plan-body round (BLOCKING)
766
785