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
@@ -7,8 +7,10 @@ does not depend on a parser the lead re-writes each time.
7
7
  from __future__ import annotations
8
8
 
9
9
  import argparse
10
+ import copy
10
11
  import importlib.util
11
12
  import json
13
+ import re
12
14
  from collections.abc import Mapping, Sequence
13
15
  from datetime import datetime, timezone
14
16
  from pathlib import Path
@@ -21,6 +23,7 @@ from .convergence import (
21
23
  validated_run_authority,
22
24
  )
23
25
  from .convergence_store import write_json_atomic
26
+ from .incremental_carry import sync_prepared_dispatch_queue
24
27
  from .json_boundary import JsonBoundaryError, load_owned_object
25
28
  from .report_finalize import task_manifest_path
26
29
  from .plan_derivations import extract_tokens, find_derivations
@@ -30,13 +33,17 @@ from .plan_items import (
30
33
  advisory_plan_body_gating,
31
34
  content_hash,
32
35
  correction_prompt_text,
36
+ critic_is_rostered,
33
37
  critic_tie_prompt_text,
34
38
  dispatch_item_ids,
35
39
  extract_plan_items,
36
40
  next_dispatch,
37
41
  planning_stage_ledger,
38
42
  reverify_item_ids,
43
+ reverify_prompt_text,
39
44
  tie_vote_item_ids,
45
+ voting_analyser_keys,
46
+ with_response_format,
40
47
  )
41
48
  from .paths import RunRef
42
49
  from .stage_ledger import build_stage_ledger
@@ -151,7 +158,9 @@ def _parser() -> argparse.ArgumentParser:
151
158
  prepare.add_argument(
152
159
  "--state", type=Path,
153
160
  help="previous plan-body state; when it carries verifiedContentHash "
154
- "rows the dispatch queue shrinks to the self-fix reverify set",
161
+ "rows the dispatch queue shrinks to the self-fix reverify set, and "
162
+ "each queued item's recorded votes and selfFixNote are carried "
163
+ "into the prompt as its `**Prior round dissent**` block",
155
164
  )
156
165
  prepare.add_argument(
157
166
  "--tie-vote", action="store_true",
@@ -164,7 +173,8 @@ def _parser() -> argparse.ArgumentParser:
164
173
  validate_prepared.add_argument("--run-manifest", type=Path, required=True)
165
174
  validate_prepared.add_argument(
166
175
  "--state", type=Path,
167
- help="same previous state prepare used to shrink the dispatch queue",
176
+ help="same previous state prepare used to shrink the dispatch queue "
177
+ "and to build the prior-dissent carry; both are re-derived here",
168
178
  )
169
179
  validate_prepared.add_argument(
170
180
  "--tie-vote", action="store_true",
@@ -177,7 +187,14 @@ def _parser() -> argparse.ArgumentParser:
177
187
  collect.add_argument("--result", action="append", default=[], required=True,
178
188
  metavar="<worker-id>=<path>",
179
189
  help="one worker's plan-verify result file (repeatable)")
180
- collect.add_argument("--items", type=Path, required=True)
190
+ collect.add_argument(
191
+ "--items", type=Path, required=True,
192
+ metavar="<plan-items artifact>",
193
+ help="the plan-items artifact this round dispatched (not a list of "
194
+ "ids). Its `dispatchQueue` is the id set the results must answer, "
195
+ "so a tie round takes the `--tie-vote` artifact, not the full "
196
+ "round's one",
197
+ )
181
198
  collect.add_argument("--output", type=Path, required=True)
182
199
  derivations = commands.add_parser(
183
200
  "derivations",
@@ -199,6 +216,15 @@ def _parser() -> argparse.ArgumentParser:
199
216
  help="record the stage ledger so the gate can scope itself to the "
200
217
  "stage about to start; without it the gate does not narrow",
201
218
  )
219
+ seed.add_argument(
220
+ "--prior-state", type=Path,
221
+ help="the previous run's plan-body-verification-<task-type>-<seq>.json. "
222
+ "A newly seeded item whose contentHash still equals that run's "
223
+ "verifiedContentHash inherits its verdicts and is tagged "
224
+ "carriedForwardFromSeq, so round 1 does not re-judge text nobody "
225
+ "changed. Requires --state, refuses a state file belonging to "
226
+ "another task, and never carries on a matching id alone",
227
+ )
202
228
  apply_verdicts = commands.add_parser(
203
229
  "apply-verdicts",
204
230
  help="overwrite planBodyVerification.planItems[].verdicts in owned state",
@@ -239,8 +265,18 @@ def _parser() -> argparse.ArgumentParser:
239
265
  complete.add_argument("--run-manifest", type=Path, required=True)
240
266
  complete.add_argument("--round", type=int, required=True, dest="round_number")
241
267
  complete.add_argument("--self-fix-note", action="append", default=[], metavar="<item-id>=<markdown-file>")
242
- complete.add_argument("--self-fix-group", action="append", default=[], metavar="<cause-file>=<item-id>[,<item-id>...]")
243
- complete.add_argument("--self-fix-stop-reason", choices=("all-resolved", "no-progress", "max-rounds-reached"))
268
+ complete.add_argument(
269
+ "--self-fix-group", action="append", default=[],
270
+ metavar="<cause-file>=<item-id>[,<item-id>...]",
271
+ help="this round's cause groups; requires --self-fix-stop-reason",
272
+ )
273
+ complete.add_argument(
274
+ "--self-fix-stop-reason",
275
+ choices=("all-resolved", "no-progress", "max-rounds-reached"),
276
+ help="why the self-fix loop stops. Required with --self-fix-group, and "
277
+ "valid on its own to record a stop the round did not rewrite for; "
278
+ "on its own it leaves selfFixRoundsApplied and selfFixGroups alone",
279
+ )
244
280
  _add_dispatch_commands(commands)
245
281
  return parser
246
282
 
@@ -354,6 +390,8 @@ def _prepare(args: argparse.Namespace) -> dict[str, Any]:
354
390
  envelope["tieSplits"] = _tie_splits_from_state(
355
391
  getattr(args, "state", None), envelope["dispatchQueue"],
356
392
  )
393
+ else:
394
+ envelope.update(_reverify_carry(args, envelope["dispatchQueue"]))
357
395
  gating = not advisory_plan_body_gating(_planning(source), envelope["items"])
358
396
  _sync_task_manifest_gating(args.run_manifest, gating)
359
397
  write_json_atomic(output, envelope)
@@ -506,6 +544,70 @@ def _render_payload(item_id: str, payload: object) -> list[str]:
506
544
  return rows
507
545
 
508
546
 
547
+ # 검증 시점의 서사에는 조립 소유 필드가 없다는 계약을 검증자에게 직접 알린다.
548
+ # run 003 실측: critic 이 P-Prep 항목에 `designSurfaceCoverage` 처분을 요구하며
549
+ # DISAGREE(b) — writer 가 그 필드를 쓰면 계약 위반이라 self-fix 로도 풀 수 없는
550
+ # 차단이었다.
551
+ _PREP_OWNERSHIP_NOTE = (
552
+ "\nOwnership boundary: `designPreparation` / `designSurfaceCoverage` rows "
553
+ "are produced by report assembly after this verification, so the narrative "
554
+ "under review carries neither — their absence is the contract, not a "
555
+ "defect. Judge the trigger evidence above; do not DISAGREE because the "
556
+ "writer did not produce those fields.\n"
557
+ )
558
+
559
+
560
+ def _step_coordinate(payload: object, field: str) -> int | None:
561
+ if not isinstance(payload, Mapping):
562
+ return None
563
+ value = payload.get(field)
564
+ if isinstance(value, int) and not isinstance(value, bool) and value >= 1:
565
+ return value
566
+ return None
567
+
568
+
569
+ def _render_prior_steps(item: Mapping[str, Any], all_items: object) -> str:
570
+ """같은 stage 의 선행 step 요약. run 003 실측: step 1.1 이 만드는 스크립트를
571
+ 1.2 검증자가 '없다'고 판정했다 — 검증 단위는 step 인데 payload 가 자기 행뿐이라
572
+ 계획이 앞 step 에서 만드는 것을 볼 수 없었다. dispatchQueue 가 좁혀져도 선행
573
+ 맥락은 봉투의 전체 items 에서 온다."""
574
+ stage = _step_coordinate(item.get("payload"), "stage")
575
+ step = _step_coordinate(item.get("payload"), "step")
576
+ if stage is None or step is None or step < 2:
577
+ return ""
578
+ priors: list[tuple[int, str]] = []
579
+ for other in all_items if isinstance(all_items, list) else []:
580
+ if not isinstance(other, Mapping):
581
+ continue
582
+ other_step = _step_coordinate(other.get("payload"), "step")
583
+ if (
584
+ other_step is None
585
+ or other_step >= step
586
+ or _step_coordinate(other.get("payload"), "stage") != stage
587
+ ):
588
+ continue
589
+ payload = other.get("payload")
590
+ action = str(payload.get("action") or other.get("subject") or "").strip()
591
+ files = payload.get("files")
592
+ if isinstance(files, str) and files.strip():
593
+ files = [files]
594
+ suffix = (
595
+ " — files: " + ", ".join(f"`{scalar(entry)}`" for entry in files)
596
+ if isinstance(files, list) and files
597
+ else ""
598
+ )
599
+ priors.append((other_step, f"- step {other_step}: {action}{suffix}\n"))
600
+ if not priors:
601
+ return ""
602
+ priors.sort()
603
+ return (
604
+ "\nPrior steps in this stage (already applied when this step runs):\n"
605
+ + "".join(text for _, text in priors)
606
+ + "Judge this step against the state after them — a script, file, or "
607
+ "path a prior step creates is not missing.\n"
608
+ )
609
+
610
+
509
611
  def _render_analyser_split(verdicts: object) -> str:
510
612
  """동수 항목에 이미 찍힌 분석자 표를 critic 프롬프트에 붙인다."""
511
613
  if not isinstance(verdicts, list):
@@ -544,11 +646,147 @@ def _tie_splits_from_state(
544
646
  return splits
545
647
 
546
648
 
649
+ _PRIOR_DISSENT_PROMPT_ANCHOR = "**Prior round dissent**"
650
+ _NO_SELF_FIX_NOTE = (
651
+ "no correction — this item's text shifted under a neighbouring rewrite"
652
+ )
653
+
654
+
655
+ def _prior_vote(row: Mapping[str, Any]) -> dict[str, Any]:
656
+ """직전 라운드의 한 표를 프롬프트가 렌더할 만큼만 옮긴다.
657
+
658
+ `basis` 는 워커가 적은 근거다. `explanation` 이 계약상 필수 줄이고 `note` 는
659
+ 반증 후보라, 설명이 비어 있을 때만 후자로 내려간다. 둘 다 없으면 근거 없는
660
+ 표이므로 빈 문자열을 남긴다 — 없는 근거를 지어내지 않는다.
661
+ """
662
+ vote: dict[str, Any] = {
663
+ "worker": str(row.get("worker") or ""),
664
+ "verdict": str(row.get("verdict") or ""),
665
+ }
666
+ kind = str(row.get("breakageKind") or "").strip()
667
+ if kind:
668
+ vote["breakageKind"] = kind
669
+ fixability = str(row.get("fixability") or "").strip()
670
+ if fixability:
671
+ vote["fixability"] = fixability
672
+ basis = (
673
+ str(row.get("explanation") or "").strip()
674
+ or str(row.get("note") or "").strip()
675
+ )
676
+ if basis:
677
+ vote["basis"] = basis
678
+ return vote
679
+
680
+
681
+ def _prior_rounds_from_state(
682
+ state_path: Path | None, queue: Sequence[str],
683
+ ) -> dict[str, dict[str, Any]]:
684
+ """큐에 오른 항목별로 직전 라운드의 표와 자가수정 노트를 모은다.
685
+
686
+ `planBodyVerification.planItems[].verdicts[]` 는 라운드마다 덮어써지므로 지금
687
+ 거기 남아 있는 것이 직전 라운드의 판정이다. 자가수정 노트는 감사용
688
+ `planItems[]` 행의 `selfFixNote` 에 본문 그대로 들어 있다.
689
+
690
+ 라운드 번호가 없는 표는 어느 라운드의 것인지 알 수 없어 싣지 않는다 —
691
+ `apply-verdicts --round` 가 필수라 계약을 지킨 행에는 항상 번호가 있다.
692
+ 큐 순서를 그대로 따라 담아 같은 입력이 같은 바이트를 내도록 한다.
693
+ """
694
+ if state_path is None or not state_path.is_file():
695
+ return {}
696
+ state = _load_json_object(state_path)
697
+ audit = state.get("planItems")
698
+ notes = {
699
+ str(row["id"]): row["selfFixNote"].strip()
700
+ for row in (audit if isinstance(audit, list) else [])
701
+ if isinstance(row, Mapping)
702
+ and isinstance(row.get("id"), str)
703
+ and isinstance(row.get("selfFixNote"), str)
704
+ and row["selfFixNote"].strip()
705
+ }
706
+ voted = {
707
+ str(row["id"]): row
708
+ for row in _state_plan_body_items(state, state_path)
709
+ if isinstance(row, Mapping) and isinstance(row.get("id"), str)
710
+ }
711
+ prior: dict[str, dict[str, Any]] = {}
712
+ for item_id in queue:
713
+ row = voted.get(item_id)
714
+ if row is None:
715
+ continue
716
+ verdicts = [
717
+ vote for vote in (row.get("verdicts") or [])
718
+ if isinstance(vote, Mapping) and isinstance(vote.get("round"), int)
719
+ ]
720
+ if not verdicts:
721
+ continue
722
+ last = max(int(vote["round"]) for vote in verdicts)
723
+ entry: dict[str, Any] = {
724
+ "round": last,
725
+ "votes": [
726
+ _prior_vote(vote) for vote in verdicts if int(vote["round"]) == last
727
+ ],
728
+ }
729
+ if item_id in notes:
730
+ entry["selfFixNote"] = notes[item_id]
731
+ prior[item_id] = entry
732
+ return prior
733
+
734
+
735
+ def _reverify_carry(
736
+ args: argparse.Namespace, queue: Sequence[str],
737
+ ) -> dict[str, Any]:
738
+ """라운드 2+ 봉투에 실을 직전 라운드 맥락. 실을 것이 없으면 빈 dict.
739
+
740
+ 라운드 1 은 `--state` 없이 준비되고, `--state` 가 있어도 판정이 하나도
741
+ 기록돼 있지 않으면 옮길 반대 의견이 없다. 두 경우 모두 봉투는 라운드 1 의
742
+ 모양 그대로다.
743
+ """
744
+ prior = _prior_rounds_from_state(getattr(args, "state", None), queue)
745
+ if not prior:
746
+ return {}
747
+ return {"dispatchKind": "reverify", "priorRounds": prior}
748
+
749
+
750
+ def _render_prior_round(entry: object) -> str:
751
+ """직전 라운드의 반대 의견을 이 항목의 블록으로 렌더한다.
752
+
753
+ 앵커는 `**Prior round dissent**` 이고, 워커가 답에 적는 줄의 앵커
754
+ (`**Prior dissent**`, `verdict_blocks.py` 가 읽는다) 와 다른 문자열이다.
755
+ 두 표기는 서로 다른 산출물의 서로 다른 앵커다.
756
+ """
757
+ if not isinstance(entry, Mapping):
758
+ return ""
759
+ votes = entry.get("votes")
760
+ if not isinstance(votes, list) or not votes:
761
+ return ""
762
+ rows = [
763
+ f"{_PRIOR_DISSENT_PROMPT_ANCHOR} (round {scalar(entry.get('round'))}):\n"
764
+ ]
765
+ for vote in votes:
766
+ if not isinstance(vote, Mapping):
767
+ continue
768
+ kind = str(vote.get("breakageKind") or "")
769
+ token = str(vote.get("verdict") or "")
770
+ if kind:
771
+ token = f"{token}({kind})"
772
+ rows.append(
773
+ f"- `{scalar(vote.get('worker'))}`: `{scalar(token)}` — "
774
+ f"`{scalar(vote.get('basis'))}`\n"
775
+ )
776
+ if vote.get("fixability"):
777
+ rows.append(" " + line("Fixability", vote.get("fixability")))
778
+ rows.append(
779
+ line("What the planner changed", entry.get("selfFixNote") or _NO_SELF_FIX_NOTE)
780
+ )
781
+ return "".join(rows)
782
+
783
+
547
784
  def _prompt(args: argparse.Namespace) -> str:
548
785
  envelope = _load_json_object(
549
786
  _prepared_items_path(args.run_manifest, require_regular=True)
550
787
  )
551
- items = envelope.get("items") if isinstance(envelope.get("items"), list) else []
788
+ all_items = envelope.get("items") if isinstance(envelope.get("items"), list) else []
789
+ items = all_items
552
790
  queue = envelope.get("dispatchQueue")
553
791
  if isinstance(queue, list):
554
792
  allowed = {item_id for item_id in queue if isinstance(item_id, str)}
@@ -556,6 +794,10 @@ def _prompt(args: argparse.Namespace) -> str:
556
794
  item for item in items
557
795
  if isinstance(item, Mapping) and item.get("id") in allowed
558
796
  ]
797
+ prior_rounds = (
798
+ envelope.get("priorRounds")
799
+ if isinstance(envelope.get("priorRounds"), Mapping) else {}
800
+ )
559
801
  rows = ["# Plan verification queue\n", line("Item count", len(items))]
560
802
  for index, item in enumerate(items, 1):
561
803
  if not isinstance(item, Mapping):
@@ -563,7 +805,13 @@ def _prompt(args: argparse.Namespace) -> str:
563
805
  item_id = str(item.get("id") or "")
564
806
  rows.extend((f"\n## Plan item {index}\n", line("Item ID", item_id),
565
807
  line("Subject", item.get("subject"))))
808
+ # 직전 반대 의견이 현재 본문보다 앞이다 — 계약이 요구하는 판단 순서가
809
+ # "반대 의견과 정정을 읽고 나서 지금 본문을 본다" 이기 때문이다.
810
+ rows.append(_render_prior_round(prior_rounds.get(item_id)))
566
811
  rows.extend(_render_payload(item_id, item.get("payload")))
812
+ rows.append(_render_prior_steps(item, all_items))
813
+ if item_id.startswith("P-Prep-"):
814
+ rows.append(_PREP_OWNERSHIP_NOTE)
567
815
  split = _render_analyser_split(
568
816
  (envelope.get("tieSplits") or {}).get(item_id) if isinstance(
569
817
  envelope.get("tieSplits"), Mapping
@@ -571,9 +819,11 @@ def _prompt(args: argparse.Namespace) -> str:
571
819
  )
572
820
  if split:
573
821
  rows.append(split)
574
- body = "".join(rows)
822
+ body = with_response_format("".join(rows))
575
823
  if envelope.get("dispatchKind") == "critic-tie":
576
824
  return critic_tie_prompt_text(body)
825
+ if envelope.get("dispatchKind") == "reverify":
826
+ return reverify_prompt_text(body)
577
827
  return body
578
828
 
579
829
 
@@ -594,6 +844,17 @@ def _validate_prepared(args: argparse.Namespace) -> dict[str, Any]:
594
844
  )
595
845
  if actual.get("dispatchQueue") != expected_queue:
596
846
  raise PlanItemContractError("prepared dispatch queue does not match")
847
+ if not getattr(args, "tie_vote", False):
848
+ # 라운드 2+ 의 반대 의견 반입은 여기서만 검사된다. 봉투에서 빠지면
849
+ # 프롬프트에도 빠지고, 반대한 워커는 자기 판정을 그대로 다시 적는다.
850
+ expected_carry = _reverify_carry(args, expected_queue)
851
+ if actual.get("priorRounds") != expected_carry.get("priorRounds"):
852
+ raise PlanItemContractError(
853
+ "prepared plan items do not carry the previous round's dissent "
854
+ "— re-run `okstra plan-items prepare` with the same --state so "
855
+ "each re-dispatched item carries its `**Prior round dissent**` "
856
+ "block"
857
+ )
597
858
  return {"ok": True, "operation": "validate-prepared", "path": str(path)}
598
859
 
599
860
 
@@ -834,6 +1095,104 @@ def _stage_ledger_snapshot(run_manifest: Path | None) -> dict[str, str] | None:
834
1095
  return snapshot or None
835
1096
 
836
1097
 
1098
+ # `plan-body-verification-<task-type-segment>-<seq>.json`. seq 는 파일 이름에만
1099
+ # 있다 — 상태 본문은 자기 run 의 순번을 담지 않는다.
1100
+ _PRIOR_STATE_NAME_RE = re.compile(
1101
+ r"^plan-body-verification-.+-(?P<seq>\d{3,})\.json$"
1102
+ )
1103
+
1104
+
1105
+ class _PriorRunCarry:
1106
+ """직전 run 에서 실제로 이월된 것. 이월을 요청하지 않았으면 만들지 않는다."""
1107
+
1108
+ def __init__(self, prev_seq: str, count: int) -> None:
1109
+ self.prev_seq = prev_seq
1110
+ self.count = count
1111
+
1112
+
1113
+ def _state_task_root(path: Path, *, option: str) -> Path:
1114
+ """``<task_root>/runs/<task-type>/state/<file>`` 의 task_root.
1115
+
1116
+ plan-body 상태 파일은 자기 안에 task 신원을 담지 않는다(`_new_v3_state` 는
1117
+ `schemaVersion` 과 `owner` 만 쓴다). 남은 신원은 경로뿐이므로, 이월 양쪽이
1118
+ 같은 task 의 기록인지는 여기서만 판정할 수 있다. 정규 run 경로가 아니면
1119
+ 판정할 근거가 없다는 뜻이고, 그때는 이월을 거절한다 — 판정 못 하는 신원을
1120
+ 통과시키면 남의 task 판정이 이 run 의 게이트를 결정한다.
1121
+ """
1122
+ try:
1123
+ return RunRef.from_run_dir(path.parent.parent).task_root
1124
+ except ValueError as exc:
1125
+ raise PlanItemContractError(
1126
+ f"{option} must be a plan-body state under "
1127
+ f"<task-root>/runs/<task-type>/state/: {path}"
1128
+ ) from exc
1129
+
1130
+
1131
+ def _carry_prior_run_verdicts(
1132
+ args: argparse.Namespace,
1133
+ added: list[dict[str, Any]],
1134
+ hashes: Mapping[str, str],
1135
+ ) -> _PriorRunCarry | None:
1136
+ """직전 run 이 같은 본문에 내린 판정을 새로 시드된 행에 옮긴다.
1137
+
1138
+ 실패한 계획 run 을 다시 돌면 판정이 하나도 넘어오지 않았다. 이월은 답변된
1139
+ clarification 이 있는 경로에만 있고(`incremental_scope.decide_scope` 는
1140
+ 답변된 `C-NNN` 이 없으면 `full` 을 되돌려 `incremental_carry` 를 도달 불가로
1141
+ 만든다), 그래서 아무도 고치지 않은 문장이 매 재실행마다 다시 채점됐다.
1142
+
1143
+ 행 모양은 `incremental_carry._stamp_carried` 와 같다 — `verdicts` 를 그대로
1144
+ 옮기고, `carriedForwardFromSeq` 에 직전 seq 를 찍고, `verifiedContentHash` 를
1145
+ 이번 추출 해시로 맞춘다. 그 해시가 시드의 `dispatchQueue` 계산에 그대로
1146
+ 들어가므로(`_queue_for` → `reverify_item_ids`), 이월된 행은 라운드 1 큐에서
1147
+ 빠진다. 큐를 따로 줄이지 않는 이유가 이것이다: incremental 경로가
1148
+ `incremental_carry._recompute_dispatch_queue` 에서 쓰는 것과 같은 장치다.
1149
+
1150
+ id 만 같은 행은 절대 이월하지 않는다. `P-*` id 는 위치에서 나오므로 계획이
1151
+ 한 줄만 밀려도 같은 번호가 다른 문장을 가리킨다. 해시가 같아야 그 문장이
1152
+ 같은 문장이다.
1153
+ """
1154
+ prior = getattr(args, "prior_state", None)
1155
+ if prior is None:
1156
+ return None
1157
+ if args.state is None:
1158
+ raise PlanItemContractError("--prior-state requires --state")
1159
+ matched = _PRIOR_STATE_NAME_RE.match(prior.name)
1160
+ if matched is None:
1161
+ raise PlanItemContractError(
1162
+ "--prior-state must name a plan-body-verification-<task-type>-<seq>"
1163
+ f".json file; the seq is what a carried row is tagged with: {prior}"
1164
+ )
1165
+ if _state_task_root(prior, option="--prior-state") != _state_task_root(
1166
+ args.state, option="--state"
1167
+ ):
1168
+ raise PlanItemContractError(
1169
+ f"--prior-state belongs to another task than --state: {prior}"
1170
+ )
1171
+ prev_seq = matched.group("seq")
1172
+ prior_rows = {
1173
+ str(row["id"]): row
1174
+ for row in _state_plan_body_items(_load_json_object(prior), prior)
1175
+ if isinstance(row, Mapping) and isinstance(row.get("id"), str)
1176
+ }
1177
+ count = 0
1178
+ for row in added:
1179
+ item_id = row.get("id")
1180
+ previous = prior_rows.get(item_id)
1181
+ if previous is None:
1182
+ continue
1183
+ verdicts = previous.get("verdicts")
1184
+ if not isinstance(verdicts, list) or not verdicts:
1185
+ continue
1186
+ current_hash = hashes.get(item_id)
1187
+ if not current_hash or previous.get("verifiedContentHash") != current_hash:
1188
+ continue
1189
+ row["verdicts"] = copy.deepcopy(verdicts)
1190
+ row["carriedForwardFromSeq"] = prev_seq
1191
+ row["verifiedContentHash"] = current_hash
1192
+ count += 1
1193
+ return _PriorRunCarry(prev_seq, count)
1194
+
1195
+
837
1196
  def _seed(args: argparse.Namespace) -> dict[str, Any]:
838
1197
  """Create the `planBodyVerification.planItems[]` rows a round lands in.
839
1198
 
@@ -847,6 +1206,10 @@ def _seed(args: argparse.Namespace) -> dict[str, Any]:
847
1206
  Idempotent by id. An existing row keeps everything it carries — verdicts
848
1207
  already applied, `carriedForwardFromSeq`, `selfFixNote` — because a re-seed
849
1208
  between rounds must not erase the round before it.
1209
+
1210
+ `--prior-state` extends that to the previous *run*: a newly seeded item
1211
+ whose text the previous run already judged inherits its verdicts instead of
1212
+ entering round 1 again (`_carry_prior_run_verdicts`).
850
1213
  """
851
1214
  source = _plan_source(args)
852
1215
  extracted = _envelope(source)["items"]
@@ -887,6 +1250,7 @@ def _seed(args: argparse.Namespace) -> dict[str, Any]:
887
1250
  for item in extracted
888
1251
  if item["id"] not in known
889
1252
  ]
1253
+ carried = _carry_prior_run_verdicts(args, added, hashes)
890
1254
  recorded.extend(added)
891
1255
  for row in recorded:
892
1256
  item_id = row.get("id") if isinstance(row, Mapping) else None
@@ -932,13 +1296,22 @@ def _seed(args: argparse.Namespace) -> dict[str, Any]:
932
1296
  _planning(source), extracted,
933
1297
  )
934
1298
  write_json_atomic(target, data)
935
- return {
1299
+ result = {
936
1300
  "ok": True,
937
1301
  "operation": "seed",
938
1302
  "path": str(target),
939
1303
  "seeded": len(added),
940
1304
  "existing": len(known),
941
1305
  }
1306
+ if carried is not None:
1307
+ result["carried"] = carried.count
1308
+ result["carriedForwardFromSeq"] = carried.prev_seq
1309
+ if carried.count:
1310
+ # 시드는 prepare **뒤에** 돈다(리드 계약 step 1). 이월이 상태의
1311
+ # 큐만 줄이면 `plan-items prompt` 가 읽는 형제 파일은 여전히 전량
1312
+ # 이라, 워커는 직전 run 이 이미 판정한 행을 다시 받는다.
1313
+ sync_prepared_dispatch_queue(target, data)
1314
+ return result
942
1315
 
943
1316
 
944
1317
  def _judged_row(row: Mapping[str, Any], project_root: Path | None) -> dict[str, Any]:
@@ -1061,7 +1434,14 @@ def _append_item_verdicts(
1061
1434
 
1062
1435
 
1063
1436
  def _gate_module() -> Any:
1064
- path = Path(__file__).parents[2] / "validators" / "validate-run.py"
1437
+ # 부모 개수를 세면 체크아웃에서만 맞는다 — 설치본은 패키지가
1438
+ # `~/.okstra/lib/python/` 이라 validators 가 `~/.okstra/lib/` 아래다.
1439
+ from .paths import RUN_VALIDATOR_RELATIVE, find_asset_root
1440
+
1441
+ root = find_asset_root(RUN_VALIDATOR_RELATIVE)
1442
+ if root is None:
1443
+ raise PlanItemContractError("cannot locate plan-body gate authority")
1444
+ path = root.joinpath(*RUN_VALIDATOR_RELATIVE)
1065
1445
  spec = importlib.util.spec_from_file_location("okstra_plan_gate", path)
1066
1446
  if spec is None or spec.loader is None:
1067
1447
  raise PlanItemContractError("cannot load plan-body gate authority")
@@ -1088,12 +1468,7 @@ def _round_snapshot(item: Mapping[str, Any], round_number: int) -> dict[str, Any
1088
1468
 
1089
1469
 
1090
1470
  def _plan_gate_summary(verification: Mapping[str, Any]) -> dict[str, Any]:
1091
- path = Path(__file__).parents[2] / "validators" / "validate-run.py"
1092
- spec = importlib.util.spec_from_file_location("okstra_plan_gate", path)
1093
- if spec is None or spec.loader is None:
1094
- raise PlanItemContractError("cannot load plan-body gate authority")
1095
- module = importlib.util.module_from_spec(spec)
1096
- spec.loader.exec_module(module)
1471
+ module = _gate_module()
1097
1472
  summary = module.plan_body_gate_summary({
1098
1473
  "implementationPlanning": {"planBodyVerification": dict(verification)}
1099
1474
  })
@@ -1105,8 +1480,13 @@ def _plan_gate_summary(verification: Mapping[str, Any]) -> dict[str, Any]:
1105
1480
  def _record_self_fixes(args: argparse.Namespace, audit: list[object], verification: dict[str, Any]) -> None:
1106
1481
  notes: dict[str, str] = {}
1107
1482
  known = {item.get("id") for item in audit if isinstance(item, Mapping)}
1108
- if args.self_fix_stop_reason and not args.self_fix_group:
1109
- raise PlanItemContractError("--self-fix-stop-reason requires --self-fix-group")
1483
+ if args.self_fix_group and not args.self_fix_stop_reason:
1484
+ raise PlanItemContractError(
1485
+ "--self-fix-group requires --self-fix-stop-reason — a self-fix "
1486
+ "round has to say why the loop stops, and a defaulted "
1487
+ "`all-resolved` is the one value that later forbids promoting the "
1488
+ "items it left unresolved"
1489
+ )
1110
1490
  for raw in args.self_fix_note:
1111
1491
  item_id, separator, filename = raw.partition("=")
1112
1492
  if not separator or not item_id or not filename:
@@ -1137,7 +1517,12 @@ def _record_self_fixes(args: argparse.Namespace, audit: list[object], verificati
1137
1517
  if groups:
1138
1518
  verification["selfFixGroups"] = groups
1139
1519
  verification["selfFixRoundsApplied"] = args.round_number
1140
- verification["selfFixStopReason"] = args.self_fix_stop_reason or "all-resolved"
1520
+ if args.self_fix_stop_reason:
1521
+ # 원인 그룹 없이 중단 사유만 기록하는 라운드는 `selfFixRoundsApplied` 를
1522
+ # 건드리지 않는다. `validate-run.py` `_validate_self_fix_grouping` 이
1523
+ # `max(selfFixGroups[].round) == selfFixRoundsApplied` 를 요구하므로,
1524
+ # 그룹 없이 올린 숫자는 통과할 수 있는 값이 없는 상태를 만든다.
1525
+ verification["selfFixStopReason"] = args.self_fix_stop_reason
1141
1526
 
1142
1527
 
1143
1528
  def _round_inputs(args: argparse.Namespace) -> tuple[dict[str, Any], list[dict[str, Any]], list[object], list[object]]:
@@ -1177,14 +1562,22 @@ def _record_audit_round(audit: list[object], snapshots: Mapping[str, dict[str, A
1177
1562
  item["rounds"].append(snapshots[str(item["id"])])
1178
1563
 
1179
1564
 
1180
- def _participant_counts(args: argparse.Namespace, snapshots: Mapping[str, dict[str, Any]]) -> tuple[set[str], int, int]:
1565
+ def _participant_counts(
1566
+ manifest: Mapping[str, Any],
1567
+ snapshots: Mapping[str, dict[str, Any]],
1568
+ items: Sequence[Mapping[str, Any]],
1569
+ ) -> tuple[set[str], int, int]:
1570
+ """이번 라운드의 투표자, run 전체의 분석자 수, 로스터 크기.
1571
+
1572
+ `workers` 는 uniformVerifiers 를 뽑기 위한 이번 라운드 큐의 투표자다.
1573
+ `voting` 은 게이트 산술의 분모이므로 라운드 큐가 아니라 run 전체를 세고,
1574
+ 검증기와 같은 함수(`voting_analyser_keys`)를 쓴다 — 종전에는 이쪽만 라운드
1575
+ 큐를 세서, critic 이 동수만 가른 라운드에서 기록값 0 과 검증기 재계산값이
1576
+ 갈렸다.
1577
+ """
1181
1578
  workers = {worker for row in snapshots.values() for worker in row["votes"]}
1182
- voting = {
1183
- worker for row in snapshots.values()
1184
- for worker, verdict in row["votes"].items()
1185
- if verdict != "verification-error"
1186
- }
1187
- assignments = _load_json_object(args.run_manifest).get("workerAssignments")
1579
+ voting = voting_analyser_keys(items)
1580
+ assignments = manifest.get("workerAssignments")
1188
1581
  if not isinstance(assignments, list):
1189
1582
  raise PlanItemContractError("run manifest workerAssignments must be an array")
1190
1583
  rostered = sum(1 for row in assignments if isinstance(row, Mapping)
@@ -1192,6 +1585,34 @@ def _participant_counts(args: argparse.Namespace, snapshots: Mapping[str, dict[s
1192
1585
  return workers, len(voting), rostered
1193
1586
 
1194
1587
 
1588
+ def _reject_round_gap(history: list[Any], round_number: int) -> None:
1589
+ """앞 라운드의 결과가 기록되기 전에 다음 라운드를 닫지 못하게 한다.
1590
+
1591
+ `roundCount` 는 이 인자의 최대값이라 건너뛴 라운드까지 세지만, 이력에는
1592
+ 행이 남지 않는다. 그 불일치는 phase 끝에서 활동 건수 검증이 잡아내는데
1593
+ (`recorded=3` vs `rounds=4`), 그때는 라운드를 다시 닫을 방법이 없어 실행
1594
+ 전체가 막힌다. 빠진 라운드의 판정도 그대로 사라진다 — 다음 라운드는 그것을
1595
+ settle 하러 존재하므로 근거 없는 표결이 된다.
1596
+
1597
+ 실측(2026-08-27, `fontsninja-v3-site` `dev-10341`): 이력이 1·2·4 이고
1598
+ `roundCount` 가 4 였다.
1599
+ """
1600
+ recorded = {
1601
+ row.get("round")
1602
+ for row in history
1603
+ if isinstance(row, Mapping) and isinstance(row.get("round"), int)
1604
+ }
1605
+ missing = [
1606
+ number for number in range(1, round_number) if number not in recorded
1607
+ ]
1608
+ if missing:
1609
+ raise PlanItemContractError(
1610
+ f"round {round_number} cannot be completed while round(s) "
1611
+ f"{', '.join(str(number) for number in missing)} have no history "
1612
+ "entry — complete them in order so each round's verdict is recorded"
1613
+ )
1614
+
1615
+
1195
1616
  def _complete_round(args: argparse.Namespace) -> dict[str, Any]:
1196
1617
  if args.round_number < 1:
1197
1618
  raise PlanItemContractError("--round must be 1 or greater")
@@ -1202,20 +1623,31 @@ def _complete_round(args: argparse.Namespace) -> dict[str, Any]:
1202
1623
  raise PlanItemContractError(
1203
1624
  "advisory plan-body gating allows one verification round"
1204
1625
  )
1205
- if args.self_fix_group or args.self_fix_note:
1626
+ if args.self_fix_group or args.self_fix_note or args.self_fix_stop_reason:
1206
1627
  raise PlanItemContractError(
1207
1628
  "advisory plan-body gating forbids the self-fix loop"
1208
1629
  )
1630
+ _reject_round_gap(history, args.round_number)
1209
1631
  snapshots, summary = _round_snapshots(current, verification, args.round_number)
1210
1632
  _record_audit_round(audit, snapshots, args.round_number)
1211
1633
  gate = str(summary["recomputed"])
1212
- workers, voting, rostered = _participant_counts(args, snapshots)
1213
- uniform = [
1214
- {"worker": worker, "verdict": values[0], "itemCount": len(values)}
1215
- for worker in workers
1216
- for values in [[row["votes"][worker] for row in snapshots.values()]]
1217
- if len(set(values)) == 1
1218
- ]
1634
+ manifest = _load_json_object(args.run_manifest)
1635
+ workers, voting, rostered = _participant_counts(manifest, snapshots, current)
1636
+ # critic-tie 는 큐 일부에만 표를 남긴다. 합집합 워커를 모든 행에
1637
+ # 강제하면 표 없는 행에서 KeyError 가 난다. 모든 행에 같은 판정이
1638
+ # 있을 때만 uniform 이다.
1639
+ uniform = []
1640
+ for worker in workers:
1641
+ values = [
1642
+ row["votes"][worker]
1643
+ for row in snapshots.values()
1644
+ if worker in row["votes"]
1645
+ ]
1646
+ if len(values) != len(snapshots) or len(set(values)) != 1:
1647
+ continue
1648
+ uniform.append(
1649
+ {"worker": worker, "verdict": values[0], "itemCount": len(values)}
1650
+ )
1219
1651
  verification.update({"roundCount": max(int(verification.get("roundCount", 0)), args.round_number),
1220
1652
  "gateResult": gate, "gateBlockedBy": summary["blockedBy"],
1221
1653
  # 무엇이 막았는지는 `gateBlockedBy` 가, 무엇이 막지 않았고
@@ -1235,7 +1667,9 @@ def _complete_round(args: argparse.Namespace) -> dict[str, Any]:
1235
1667
  "operation": "complete-round",
1236
1668
  "path": str(args.state),
1237
1669
  "gateResult": gate,
1238
- "nextDispatch": next_dispatch(current, payloads).as_dict(),
1670
+ "nextDispatch": next_dispatch(
1671
+ current, payloads, critic_rostered=critic_is_rostered(manifest),
1672
+ ).as_dict(),
1239
1673
  }
1240
1674
 
1241
1675
 
@@ -1258,10 +1692,26 @@ def _prepared_payloads(run_manifest: Path | None) -> dict[str, dict[str, Any]]:
1258
1692
  }
1259
1693
 
1260
1694
 
1695
+ def _rostered_critic(run_manifest: Path | None) -> bool:
1696
+ """매니페스트 없이 부르면 종전대로 critic 이 있다고 본다.
1697
+
1698
+ 없다고 단정하면 critic 을 둔 run 이 가를 수 있는 동수까지 사용자에게
1699
+ 넘긴다 — 그쪽이 더 나쁜 오답이다. 리드 계약은 `next-dispatch` 를
1700
+ `--run-manifest` 와 함께 부르도록 못박고 있다
1701
+ (`prompts/lead/plan-body-verification.md` §"Round protocol" step 6).
1702
+ """
1703
+ if run_manifest is None:
1704
+ return True
1705
+ return critic_is_rostered(_load_json_object(run_manifest))
1706
+
1707
+
1261
1708
  def _state_next_dispatch(args: argparse.Namespace) -> NextDispatch:
1262
1709
  _data, current, _audit, _history = _round_inputs(args)
1263
- payloads = _prepared_payloads(getattr(args, "run_manifest", None))
1264
- return next_dispatch(current, payloads)
1710
+ run_manifest = getattr(args, "run_manifest", None)
1711
+ payloads = _prepared_payloads(run_manifest)
1712
+ return next_dispatch(
1713
+ current, payloads, critic_rostered=_rostered_critic(run_manifest),
1714
+ )
1265
1715
 
1266
1716
 
1267
1717
  def _next_dispatch(args: argparse.Namespace) -> dict[str, Any]: