okstra 0.174.0 → 0.176.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 (419) hide show
  1. package/README.md +18 -9
  2. package/bin/okstra +8 -4
  3. package/dist/cli-registry.d.mts +16 -0
  4. package/dist/cli-registry.mjs +560 -0
  5. package/dist/cli-registry.mjs.map +1 -0
  6. package/dist/commands/execute/agent-prompt.d.mts +1 -0
  7. package/{src → dist}/commands/execute/agent-prompt.mjs +10 -11
  8. package/dist/commands/execute/agent-prompt.mjs.map +1 -0
  9. package/dist/commands/execute/codex-dispatch.d.mts +3 -0
  10. package/dist/commands/execute/codex-dispatch.mjs +6 -0
  11. package/dist/commands/execute/codex-dispatch.mjs.map +1 -0
  12. package/dist/commands/execute/codex-run.d.mts +3 -0
  13. package/{src → dist}/commands/execute/codex-run.mjs +30 -37
  14. package/dist/commands/execute/codex-run.mjs.map +1 -0
  15. package/dist/commands/execute/convergence.d.mts +1 -0
  16. package/{src → dist}/commands/execute/convergence.mjs +15 -16
  17. package/dist/commands/execute/convergence.mjs.map +1 -0
  18. package/dist/commands/execute/error-log.d.mts +1 -0
  19. package/{src → dist}/commands/execute/error-log.mjs +2 -3
  20. package/dist/commands/execute/error-log.mjs.map +1 -0
  21. package/dist/commands/execute/git-reconcile.d.mts +1 -0
  22. package/{src → dist}/commands/execute/git-reconcile.mjs +10 -11
  23. package/dist/commands/execute/git-reconcile.mjs.map +1 -0
  24. package/dist/commands/execute/handoff.d.mts +1 -0
  25. package/{src → dist}/commands/execute/handoff.mjs +10 -11
  26. package/dist/commands/execute/handoff.mjs.map +1 -0
  27. package/dist/commands/execute/incremental-carry.d.mts +1 -0
  28. package/{src → dist}/commands/execute/incremental-carry.mjs +2 -3
  29. package/dist/commands/execute/incremental-carry.mjs.map +1 -0
  30. package/dist/commands/execute/incremental-scope.d.mts +1 -0
  31. package/{src → dist}/commands/execute/incremental-scope.mjs +2 -3
  32. package/dist/commands/execute/incremental-scope.mjs.map +1 -0
  33. package/dist/commands/execute/integrate-stages.d.mts +1 -0
  34. package/{src → dist}/commands/execute/integrate-stages.mjs +10 -11
  35. package/dist/commands/execute/integrate-stages.mjs.map +1 -0
  36. package/dist/commands/execute/pane-title.d.mts +1 -0
  37. package/dist/commands/execute/pane-title.mjs +20 -0
  38. package/dist/commands/execute/pane-title.mjs.map +1 -0
  39. package/dist/commands/execute/plan-items.d.mts +1 -0
  40. package/dist/commands/execute/plan-items.mjs +9 -0
  41. package/dist/commands/execute/plan-items.mjs.map +1 -0
  42. package/dist/commands/execute/plan-validate.d.mts +1 -0
  43. package/dist/commands/execute/plan-validate.mjs +68 -0
  44. package/dist/commands/execute/plan-validate.mjs.map +1 -0
  45. package/dist/commands/execute/plan-verify.d.mts +1 -0
  46. package/{src → dist}/commands/execute/plan-verify.mjs +13 -16
  47. package/dist/commands/execute/plan-verify.mjs.map +1 -0
  48. package/dist/commands/execute/render-bundle.d.mts +3 -0
  49. package/{src → dist}/commands/execute/render-bundle.mjs +65 -61
  50. package/dist/commands/execute/render-bundle.mjs.map +1 -0
  51. package/dist/commands/execute/run.d.mts +24 -0
  52. package/dist/commands/execute/run.mjs +196 -0
  53. package/dist/commands/execute/run.mjs.map +1 -0
  54. package/dist/commands/execute/spawn-followups.d.mts +1 -0
  55. package/{src → dist}/commands/execute/spawn-followups.mjs +6 -7
  56. package/dist/commands/execute/spawn-followups.mjs.map +1 -0
  57. package/dist/commands/execute/team.d.mts +3 -0
  58. package/{src → dist}/commands/execute/team.mjs +29 -36
  59. package/dist/commands/execute/team.mjs.map +1 -0
  60. package/dist/commands/execute/token-usage.d.mts +1 -0
  61. package/{src → dist}/commands/execute/token-usage.mjs +2 -3
  62. package/dist/commands/execute/token-usage.mjs.map +1 -0
  63. package/dist/commands/execute/wizard.d.mts +8 -0
  64. package/dist/commands/execute/wizard.mjs +191 -0
  65. package/dist/commands/execute/wizard.mjs.map +1 -0
  66. package/dist/commands/execute/worker-audit-check.d.mts +1 -0
  67. package/{src → dist}/commands/execute/worker-audit-check.mjs +11 -12
  68. package/dist/commands/execute/worker-audit-check.mjs.map +1 -0
  69. package/dist/commands/execute/worker-dispatch.d.mts +7 -0
  70. package/{src → dist}/commands/execute/worker-dispatch.mjs +34 -46
  71. package/dist/commands/execute/worker-dispatch.mjs.map +1 -0
  72. package/dist/commands/execute/worker-state.d.mts +1 -0
  73. package/{src → dist}/commands/execute/worker-state.mjs +15 -16
  74. package/dist/commands/execute/worker-state.mjs.map +1 -0
  75. package/dist/commands/execute/worktree-lookup.d.mts +1 -0
  76. package/{src → dist}/commands/execute/worktree-lookup.mjs +38 -37
  77. package/dist/commands/execute/worktree-lookup.mjs.map +1 -0
  78. package/dist/commands/execute/worktree-status.d.mts +1 -0
  79. package/{src → dist}/commands/execute/worktree-status.mjs +52 -46
  80. package/dist/commands/execute/worktree-status.mjs.map +1 -0
  81. package/dist/commands/inspect/code-review.d.mts +1 -0
  82. package/{src → dist}/commands/inspect/code-review.mjs +16 -19
  83. package/dist/commands/inspect/code-review.mjs.map +1 -0
  84. package/dist/commands/inspect/container.d.mts +1 -0
  85. package/{src → dist}/commands/inspect/container.mjs +11 -13
  86. package/dist/commands/inspect/container.mjs.map +1 -0
  87. package/dist/commands/inspect/context-cost.d.mts +1 -0
  88. package/{src → dist}/commands/inspect/context-cost.mjs +11 -13
  89. package/dist/commands/inspect/context-cost.mjs.map +1 -0
  90. package/dist/commands/inspect/design-prep.d.mts +1 -0
  91. package/{src → dist}/commands/inspect/design-prep.mjs +10 -11
  92. package/dist/commands/inspect/design-prep.mjs.map +1 -0
  93. package/dist/commands/inspect/error-report.d.mts +1 -0
  94. package/{src → dist}/commands/inspect/error-report.mjs +11 -13
  95. package/dist/commands/inspect/error-report.mjs.map +1 -0
  96. package/dist/commands/inspect/error-zip.d.mts +1 -0
  97. package/{src → dist}/commands/inspect/error-zip.mjs +11 -12
  98. package/dist/commands/inspect/error-zip.mjs.map +1 -0
  99. package/dist/commands/inspect/log-report.d.mts +1 -0
  100. package/{src → dist}/commands/inspect/log-report.mjs +11 -13
  101. package/dist/commands/inspect/log-report.mjs.map +1 -0
  102. package/dist/commands/inspect/profile-show.d.mts +1 -0
  103. package/{src → dist}/commands/inspect/profile-show.mjs +10 -11
  104. package/dist/commands/inspect/profile-show.mjs.map +1 -0
  105. package/dist/commands/inspect/recap.d.mts +1 -0
  106. package/{src → dist}/commands/inspect/recap.mjs +11 -13
  107. package/dist/commands/inspect/recap.mjs.map +1 -0
  108. package/dist/commands/inspect/resolve-task-key.d.mts +1 -0
  109. package/{src → dist}/commands/inspect/resolve-task-key.mjs +11 -13
  110. package/dist/commands/inspect/resolve-task-key.mjs.map +1 -0
  111. package/dist/commands/inspect/rollup.d.mts +1 -0
  112. package/{src → dist}/commands/inspect/rollup.mjs +11 -13
  113. package/dist/commands/inspect/rollup.mjs.map +1 -0
  114. package/dist/commands/inspect/run-audit.d.mts +1 -0
  115. package/{src → dist}/commands/inspect/run-audit.mjs +11 -12
  116. package/dist/commands/inspect/run-audit.mjs.map +1 -0
  117. package/dist/commands/inspect/set-work-status.d.mts +1 -0
  118. package/{src → dist}/commands/inspect/set-work-status.mjs +11 -13
  119. package/dist/commands/inspect/set-work-status.mjs.map +1 -0
  120. package/dist/commands/inspect/stage-map.d.mts +1 -0
  121. package/dist/commands/inspect/stage-map.mjs +110 -0
  122. package/dist/commands/inspect/stage-map.mjs.map +1 -0
  123. package/dist/commands/inspect/task-list.d.mts +1 -0
  124. package/dist/commands/inspect/task-list.mjs +103 -0
  125. package/dist/commands/inspect/task-list.mjs.map +1 -0
  126. package/dist/commands/inspect/task-show.d.mts +1 -0
  127. package/dist/commands/inspect/task-show.mjs +108 -0
  128. package/dist/commands/inspect/task-show.mjs.map +1 -0
  129. package/dist/commands/inspect/time-report.d.mts +1 -0
  130. package/{src → dist}/commands/inspect/time-report.mjs +11 -13
  131. package/dist/commands/inspect/time-report.mjs.map +1 -0
  132. package/dist/commands/inspect/usage-report.d.mts +1 -0
  133. package/{src → dist}/commands/inspect/usage-report.mjs +11 -13
  134. package/dist/commands/inspect/usage-report.mjs.map +1 -0
  135. package/dist/commands/inspect/user-response.d.mts +1 -0
  136. package/{src → dist}/commands/inspect/user-response.mjs +10 -11
  137. package/dist/commands/inspect/user-response.mjs.map +1 -0
  138. package/dist/commands/inspect/worker-liveness.d.mts +1 -0
  139. package/{src → dist}/commands/inspect/worker-liveness.mjs +11 -13
  140. package/dist/commands/inspect/worker-liveness.mjs.map +1 -0
  141. package/dist/commands/lifecycle/check-project.d.mts +21 -0
  142. package/dist/commands/lifecycle/check-project.mjs +175 -0
  143. package/dist/commands/lifecycle/check-project.mjs.map +1 -0
  144. package/dist/commands/lifecycle/config.d.mts +1 -0
  145. package/dist/commands/lifecycle/config.mjs +369 -0
  146. package/dist/commands/lifecycle/config.mjs.map +1 -0
  147. package/dist/commands/lifecycle/doctor.d.mts +10 -0
  148. package/dist/commands/lifecycle/doctor.mjs +317 -0
  149. package/dist/commands/lifecycle/doctor.mjs.map +1 -0
  150. package/dist/commands/lifecycle/install.d.mts +17 -0
  151. package/dist/commands/lifecycle/install.mjs +1077 -0
  152. package/dist/commands/lifecycle/install.mjs.map +1 -0
  153. package/dist/commands/lifecycle/migrate.d.mts +1 -0
  154. package/{src → dist}/commands/lifecycle/migrate.mjs +10 -11
  155. package/dist/commands/lifecycle/migrate.mjs.map +1 -0
  156. package/dist/commands/lifecycle/model.d.mts +1 -0
  157. package/dist/commands/lifecycle/model.mjs +22 -0
  158. package/dist/commands/lifecycle/model.mjs.map +1 -0
  159. package/dist/commands/lifecycle/paths.d.mts +1 -0
  160. package/dist/commands/lifecycle/paths.mjs +102 -0
  161. package/dist/commands/lifecycle/paths.mjs.map +1 -0
  162. package/dist/commands/lifecycle/preflight.d.mts +1 -0
  163. package/dist/commands/lifecycle/preflight.mjs +124 -0
  164. package/dist/commands/lifecycle/preflight.mjs.map +1 -0
  165. package/dist/commands/lifecycle/setup.d.mts +1 -0
  166. package/dist/commands/lifecycle/setup.mjs +297 -0
  167. package/dist/commands/lifecycle/setup.mjs.map +1 -0
  168. package/dist/commands/lifecycle/uninstall.d.mts +4 -0
  169. package/dist/commands/lifecycle/uninstall.mjs +262 -0
  170. package/dist/commands/lifecycle/uninstall.mjs.map +1 -0
  171. package/dist/commands/manager.d.mts +3 -0
  172. package/{src → dist}/commands/manager.mjs +25 -32
  173. package/dist/commands/manager.mjs.map +1 -0
  174. package/dist/commands/memory/memory.d.mts +1 -0
  175. package/dist/commands/memory/memory.mjs +569 -0
  176. package/dist/commands/memory/memory.mjs.map +1 -0
  177. package/dist/commands/pr/pr.d.mts +33 -0
  178. package/dist/commands/pr/pr.mjs +296 -0
  179. package/dist/commands/pr/pr.mjs.map +1 -0
  180. package/dist/commands/report/agent-activity.d.mts +1 -0
  181. package/{src → dist}/commands/report/agent-activity.mjs +11 -12
  182. package/dist/commands/report/agent-activity.mjs.map +1 -0
  183. package/dist/commands/report/finalize.d.mts +4 -0
  184. package/{src → dist}/commands/report/finalize.mjs +25 -32
  185. package/dist/commands/report/finalize.mjs.map +1 -0
  186. package/dist/commands/report/inject-report-index.d.mts +1 -0
  187. package/{src → dist}/commands/report/inject-report-index.mjs +6 -7
  188. package/dist/commands/report/inject-report-index.mjs.map +1 -0
  189. package/dist/commands/report/render-final-report.d.mts +1 -0
  190. package/{src → dist}/commands/report/render-final-report.mjs +6 -7
  191. package/dist/commands/report/render-final-report.mjs.map +1 -0
  192. package/dist/commands/report/render-views.d.mts +1 -0
  193. package/{src → dist}/commands/report/render-views.mjs +6 -7
  194. package/dist/commands/report/render-views.mjs.map +1 -0
  195. package/dist/commands/report/translate.d.mts +1 -0
  196. package/{src → dist}/commands/report/translate.mjs +6 -7
  197. package/dist/commands/report/translate.mjs.map +1 -0
  198. package/dist/lib/helper-scripts.d.mts +3 -0
  199. package/{src → dist}/lib/helper-scripts.mjs +11 -14
  200. package/dist/lib/helper-scripts.mjs.map +1 -0
  201. package/dist/lib/host-registry-client.d.mts +25 -0
  202. package/dist/lib/host-registry-client.mjs +152 -0
  203. package/dist/lib/host-registry-client.mjs.map +1 -0
  204. package/dist/lib/install-assets.d.mts +8 -0
  205. package/{src → dist}/lib/install-assets.mjs +17 -20
  206. package/dist/lib/install-assets.mjs.map +1 -0
  207. package/dist/lib/okstra-dirs.d.mts +4 -0
  208. package/{src → dist}/lib/okstra-dirs.mjs +3 -6
  209. package/dist/lib/okstra-dirs.mjs.map +1 -0
  210. package/dist/lib/paths.d.mts +4 -0
  211. package/dist/lib/paths.mjs +78 -0
  212. package/dist/lib/paths.mjs.map +1 -0
  213. package/dist/lib/proc.d.mts +3 -0
  214. package/dist/lib/proc.mjs +31 -0
  215. package/dist/lib/proc.mjs.map +1 -0
  216. package/dist/lib/python-helper.d.mts +8 -0
  217. package/dist/lib/python-helper.mjs +123 -0
  218. package/dist/lib/python-helper.mjs.map +1 -0
  219. package/dist/lib/runtime-manifest.d.mts +7 -0
  220. package/dist/lib/runtime-manifest.mjs +73 -0
  221. package/dist/lib/runtime-manifest.mjs.map +1 -0
  222. package/dist/lib/skill-catalog.d.mts +4 -0
  223. package/dist/lib/skill-catalog.mjs +50 -0
  224. package/dist/lib/skill-catalog.mjs.map +1 -0
  225. package/dist/lib/types.d.mts +129 -0
  226. package/dist/lib/types.mjs +2 -0
  227. package/dist/lib/types.mjs.map +1 -0
  228. package/dist/lib/version.d.mts +2 -0
  229. package/dist/lib/version.mjs +27 -0
  230. package/dist/lib/version.mjs.map +1 -0
  231. package/docs/architecture/storage-model.md +6 -2
  232. package/docs/architecture.md +29 -20
  233. package/docs/cli.md +40 -16
  234. package/docs/contributor-change-matrix.md +2 -2
  235. package/docs/for-ai/skills/okstra-inspect.md +2 -3
  236. package/docs/for-ai/skills/okstra-rollup.md +1 -0
  237. package/docs/project-structure-overview.md +72 -62
  238. package/docs/task-process/README.md +12 -8
  239. package/docs/task-process/common-flow.md +13 -16
  240. package/docs/task-process/error-analysis.md +9 -10
  241. package/docs/task-process/final-verification.md +7 -7
  242. package/docs/task-process/implementation-planning.md +9 -9
  243. package/docs/task-process/implementation.md +6 -6
  244. package/docs/task-process/release-handoff.md +8 -7
  245. package/docs/task-process/requirements-discovery.md +8 -8
  246. package/package.json +11 -5
  247. package/runtime/BUILD.json +2 -2
  248. package/runtime/agents/workers/claude-worker.md +2 -1
  249. package/runtime/agents/workers/report-writer-worker.md +3 -1
  250. package/runtime/agents/workers/translator-worker.md +3 -1
  251. package/runtime/bin/lib/okstra/cli.sh +12 -0
  252. package/runtime/bin/lib/okstra/globals.sh +3 -0
  253. package/runtime/bin/lib/okstra/interactive.sh +12 -6
  254. package/runtime/bin/lib/okstra/usage.sh +7 -6
  255. package/runtime/bin/okstra-error-log.py +27 -0
  256. package/runtime/bin/okstra-provider-exec.py +217 -5
  257. package/runtime/bin/okstra-spawn-followups.py +4 -2
  258. package/runtime/bin/okstra.sh +10 -0
  259. package/runtime/prompts/launch.template.md +2 -2
  260. package/runtime/prompts/lead/context-loader.md +1 -2
  261. package/runtime/prompts/lead/convergence.md +27 -18
  262. package/runtime/prompts/lead/okstra-lead-contract.md +6 -5
  263. package/runtime/prompts/lead/plan-body-verification.md +10 -3
  264. package/runtime/prompts/lead/report-writer.md +15 -1
  265. package/runtime/prompts/lead/team-contract.md +17 -13
  266. package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
  267. package/runtime/prompts/profiles/_implementation-executor.md +0 -3
  268. package/runtime/prompts/profiles/_implementation-verifier.md +3 -7
  269. package/runtime/prompts/profiles/change-impact-analysis.md +20 -0
  270. package/runtime/prompts/profiles/error-analysis.md +27 -1
  271. package/runtime/prompts/profiles/feature-analysis.md +20 -0
  272. package/runtime/prompts/profiles/final-verification.md +20 -1
  273. package/runtime/prompts/profiles/implementation-option-selection.md +20 -0
  274. package/runtime/prompts/profiles/implementation-planning.md +26 -0
  275. package/runtime/prompts/profiles/implementation.md +19 -0
  276. package/runtime/prompts/profiles/improvement-discovery.md +21 -1
  277. package/runtime/prompts/profiles/project-analysis.md +20 -0
  278. package/runtime/prompts/profiles/release-handoff.md +4 -0
  279. package/runtime/prompts/profiles/requirements-discovery.md +27 -1
  280. package/runtime/prompts/wizard/prompts.ko.json +35 -0
  281. package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +8 -0
  282. package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +8 -0
  283. package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
  284. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +2 -2
  285. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +25 -5
  286. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
  287. package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +4 -1
  288. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
  289. package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +2 -2
  290. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +79 -11
  291. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +95 -25
  292. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +59 -14
  293. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +55 -9
  294. package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +51 -12
  295. package/runtime/python/okstra_ctl/adapters/runtime/__init__.py +1 -0
  296. package/runtime/python/okstra_ctl/adapters/runtime/assembly.py +27 -0
  297. package/runtime/python/okstra_ctl/adapters/runtime/cli_wrapper.py +49 -0
  298. package/runtime/python/okstra_ctl/adapters/runtime/cmux.py +74 -0
  299. package/runtime/python/okstra_ctl/agent_activity.py +5 -1
  300. package/runtime/python/okstra_ctl/agent_invocation.py +608 -14
  301. package/runtime/python/okstra_ctl/agent_prompt_cli.py +249 -50
  302. package/runtime/python/okstra_ctl/application/open_worker.py +29 -0
  303. package/runtime/python/okstra_ctl/application/resolve_assignment.py +53 -12
  304. package/runtime/python/okstra_ctl/assignment_environment.py +102 -0
  305. package/runtime/python/okstra_ctl/assignment_resolver.py +790 -0
  306. package/runtime/python/okstra_ctl/attempt_evidence.py +342 -0
  307. package/runtime/python/okstra_ctl/convergence.py +409 -3
  308. package/runtime/python/okstra_ctl/convergence_engine.py +248 -15
  309. package/runtime/python/okstra_ctl/convergence_store.py +170 -1
  310. package/runtime/python/okstra_ctl/dispatch_core.py +1204 -155
  311. package/runtime/python/okstra_ctl/dispatch_state.py +696 -11
  312. package/runtime/python/okstra_ctl/doctor.py +14 -0
  313. package/runtime/python/okstra_ctl/domain/host.py +34 -1
  314. package/runtime/python/okstra_ctl/domain/provider.py +212 -6
  315. package/runtime/python/okstra_ctl/domain/role.py +125 -0
  316. package/runtime/python/okstra_ctl/domain/wizard/interaction.py +18 -0
  317. package/runtime/python/okstra_ctl/domain/worker_exec.py +34 -1
  318. package/runtime/python/okstra_ctl/domain/worker_role.py +13 -3
  319. package/runtime/python/okstra_ctl/domain/worker_runtime.py +44 -0
  320. package/runtime/python/okstra_ctl/domain/worker_stream.py +24 -0
  321. package/runtime/python/okstra_ctl/error_log_write.py +9 -0
  322. package/runtime/python/okstra_ctl/error_report.py +21 -1
  323. package/runtime/python/okstra_ctl/execution_identity.py +529 -0
  324. package/runtime/python/okstra_ctl/execution_manifest.py +1281 -0
  325. package/runtime/python/okstra_ctl/execution_mutation_audit.py +619 -0
  326. package/runtime/python/okstra_ctl/implementation_outcome.py +21 -5
  327. package/runtime/python/okstra_ctl/index.py +6 -2
  328. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +43 -3
  329. package/runtime/python/okstra_ctl/legacy_model_selection.py +578 -0
  330. package/runtime/python/okstra_ctl/manager_sync.py +4 -1
  331. package/runtime/python/okstra_ctl/model_cli.py +278 -0
  332. package/runtime/python/okstra_ctl/model_defaults.py +46 -0
  333. package/runtime/python/okstra_ctl/model_discovery.py +26 -7
  334. package/runtime/python/okstra_ctl/model_pool.py +501 -0
  335. package/runtime/python/okstra_ctl/models.py +47 -27
  336. package/runtime/python/okstra_ctl/mutation_recovery.py +293 -0
  337. package/runtime/python/okstra_ctl/next_phase.py +236 -0
  338. package/runtime/python/okstra_ctl/pane_title.py +151 -0
  339. package/runtime/python/okstra_ctl/ports/host_model.py +48 -17
  340. package/runtime/python/okstra_ctl/ports/worker_dispatch.py +4 -0
  341. package/runtime/python/okstra_ctl/ports/worker_runtime.py +26 -0
  342. package/runtime/python/okstra_ctl/recap.py +4 -1
  343. package/runtime/python/okstra_ctl/registry/provider_registry.py +8 -4
  344. package/runtime/python/okstra_ctl/render.py +124 -57
  345. package/runtime/python/okstra_ctl/render_final_report.py +19 -0
  346. package/runtime/python/okstra_ctl/report_contract.py +39 -0
  347. package/runtime/python/okstra_ctl/report_finalize.py +16 -0
  348. package/runtime/python/okstra_ctl/report_html/render.py +1 -0
  349. package/runtime/python/okstra_ctl/role_requirements.py +210 -0
  350. package/runtime/python/okstra_ctl/rollup.py +4 -1
  351. package/runtime/python/okstra_ctl/run.py +1210 -258
  352. package/runtime/python/okstra_ctl/run_index_row.py +7 -1
  353. package/runtime/python/okstra_ctl/stage_fix_carry.py +17 -1
  354. package/runtime/python/okstra_ctl/team.py +14 -18
  355. package/runtime/python/okstra_ctl/time_report.py +38 -4
  356. package/runtime/python/okstra_ctl/usage_identity.py +86 -0
  357. package/runtime/python/okstra_ctl/usage_report.py +15 -3
  358. package/runtime/python/okstra_ctl/wizard.py +1449 -66
  359. package/runtime/python/okstra_ctl/worker_audit_ledger.py +68 -65
  360. package/runtime/python/okstra_ctl/worker_prompt_body.py +18 -2
  361. package/runtime/python/okstra_ctl/worker_prompt_contract.py +40 -9
  362. package/runtime/python/okstra_ctl/worker_prompt_headers.py +16 -1
  363. package/runtime/python/okstra_ctl/worker_request.py +7 -1
  364. package/runtime/python/okstra_ctl/worker_runner.py +130 -5
  365. package/runtime/python/okstra_ctl/workers.py +7 -5
  366. package/runtime/python/okstra_ctl/workflow.py +18 -32
  367. package/runtime/python/okstra_ctl/worktree.py +3 -3
  368. package/runtime/python/okstra_ctl/worktree_registry.py +5 -4
  369. package/runtime/python/okstra_ctl/write_policy.py +571 -0
  370. package/runtime/python/okstra_project/state.py +54 -6
  371. package/runtime/schemas/convergence-groups-v2.0.schema.json +128 -0
  372. package/runtime/schemas/execution-manifest-v2.schema.json +340 -0
  373. package/runtime/schemas/final-report-v2.0.schema.json +68 -5
  374. package/runtime/skills/okstra-inspect/facets/recap.md +2 -0
  375. package/runtime/skills/okstra-inspect/facets/report.md +1 -1
  376. package/runtime/skills/okstra-inspect/facets/status.md +15 -13
  377. package/runtime/skills/okstra-rollup/SKILL.md +1 -0
  378. package/runtime/skills/okstra-run/SKILL.md +14 -14
  379. package/runtime/templates/implementation-worker-preamble.md +3 -18
  380. package/runtime/templates/project-docs/task-index.template.md +0 -1
  381. package/runtime/templates/report-writer-prompt-preamble.md +2 -0
  382. package/runtime/templates/reports/final-report-v2.template.md +7 -1
  383. package/runtime/templates/reports/html/base.template.html +21 -0
  384. package/runtime/templates/reports/html/i18n/en.json +4 -0
  385. package/runtime/templates/reports/html/i18n/ko.json +4 -0
  386. package/runtime/templates/reports/html/macros/forms.html +5 -4
  387. package/runtime/templates/reports/html/tasks/final-verification.template.html +1 -1
  388. package/runtime/templates/reports/html/tasks/implementation.template.html +1 -1
  389. package/runtime/templates/worker-prompt-preamble.md +6 -35
  390. package/runtime/validators/lib/fixtures.sh +49 -16
  391. package/runtime/validators/validate-run.py +486 -134
  392. package/runtime/validators/validate_session_conformance.py +25 -7
  393. package/src/cli-registry.mjs +0 -539
  394. package/src/commands/execute/codex-dispatch.mjs +0 -10
  395. package/src/commands/execute/plan-items.mjs +0 -9
  396. package/src/commands/execute/plan-validate.mjs +0 -68
  397. package/src/commands/execute/run.mjs +0 -208
  398. package/src/commands/execute/wizard.mjs +0 -167
  399. package/src/commands/inspect/stage-map.mjs +0 -98
  400. package/src/commands/inspect/task-list.mjs +0 -97
  401. package/src/commands/inspect/task-show.mjs +0 -100
  402. package/src/commands/lifecycle/check-project.mjs +0 -187
  403. package/src/commands/lifecycle/config.mjs +0 -385
  404. package/src/commands/lifecycle/doctor.mjs +0 -308
  405. package/src/commands/lifecycle/install.mjs +0 -1170
  406. package/src/commands/lifecycle/paths.mjs +0 -104
  407. package/src/commands/lifecycle/preflight.mjs +0 -125
  408. package/src/commands/lifecycle/setup.mjs +0 -333
  409. package/src/commands/lifecycle/uninstall.mjs +0 -279
  410. package/src/commands/memory/memory.mjs +0 -524
  411. package/src/commands/pr/pr.mjs +0 -261
  412. package/src/lib/host-registry-client.mjs +0 -176
  413. package/src/lib/paths.mjs +0 -86
  414. package/src/lib/proc.mjs +0 -31
  415. package/src/lib/python-helper.mjs +0 -142
  416. package/src/lib/runtime-manifest.mjs +0 -70
  417. package/src/lib/skill-catalog.mjs +0 -53
  418. package/src/lib/version.mjs +0 -20
  419. /package/{src → dist}/commands/pr/default.md +0 -0
@@ -8,12 +8,13 @@ write scope in the order the CLIs are told it, and the role's idle budget.
8
8
  """
9
9
  from __future__ import annotations
10
10
 
11
+ import json
11
12
  import os
12
13
  import shutil
13
14
  import sys
14
15
  from dataclasses import dataclass
15
16
  from pathlib import Path
16
- from typing import Any, Mapping
17
+ from typing import Any, Callable, Mapping
17
18
 
18
19
  _HERE = Path(__file__).resolve().parent
19
20
  # ``okstra_ctl`` sits beside this file in the repo (``scripts/``) but under
@@ -26,7 +27,13 @@ sys.path.insert(0, str(_HERE))
26
27
  if _HOME_LIB.is_dir() and str(_HOME_LIB) not in sys.path:
27
28
  sys.path.append(str(_HOME_LIB))
28
29
 
29
- from okstra_ctl.domain.provider import ProviderSpec, UnknownProviderError # noqa: E402
30
+ from okstra_ctl.domain.provider import ( # noqa: E402
31
+ ProviderSpec,
32
+ ServedModelAttestation,
33
+ ServedModelNormalizer,
34
+ UnknownProviderError,
35
+ )
36
+ from okstra_ctl.domain.role import normalize_role, role_for_duty # noqa: E402
30
37
  from okstra_ctl.domain.worker_exec import ( # noqa: E402
31
38
  ExecutionStrategy,
32
39
  WorkerExecRequest,
@@ -36,15 +43,32 @@ from okstra_ctl.registry.provider_registry import ( # noqa: E402
36
43
  )
37
44
  from okstra_ctl.worker_request import build_request, idle_timeout # noqa: E402
38
45
  from okstra_ctl.worker_runner import LIVE, QUIET, run_worker # noqa: E402
46
+ from okstra_ctl.dispatch_core import verify_served_model # noqa: E402
47
+ from okstra_ctl.dispatch_state import DispatchError # noqa: E402
48
+ from okstra_ctl.execution_manifest import read_execution_manifest # noqa: E402
49
+ from okstra_ctl.write_policy import ( # noqa: E402
50
+ WriteEnforcement,
51
+ WritePolicy,
52
+ write_enforcement_from_payload,
53
+ write_policy_from_payload,
54
+ )
55
+ from okstra_ctl.model_pool import ModelPool # noqa: E402
56
+ from okstra_ctl.agent_invocation import ( # noqa: E402
57
+ AgentInvocationError,
58
+ agent_model_assignment_from_payload,
59
+ invocation_metadata_identity,
60
+ )
39
61
 
40
62
  _USAGE = (
41
63
  "usage: okstra-provider-exec.py <provider> <project-root> "
42
64
  "<model-execution-value> <prompt-path> [worktree-path] [role] "
43
- "[idle-timeout-seconds] [--presentation live|quiet] [--session-id <uuid>]"
65
+ "[idle-timeout-seconds] [--presentation live|quiet] [--session-id <uuid>] "
66
+ "[--invocation-metadata path]"
44
67
  )
45
68
 
46
69
  _PRESENTATION_FLAG = "--presentation"
47
70
  _SESSION_ID_FLAG = "--session-id"
71
+ _INVOCATION_METADATA_FLAG = "--invocation-metadata"
48
72
  _PRESENTATIONS = (LIVE, QUIET)
49
73
 
50
74
 
@@ -62,6 +86,16 @@ class Invocation:
62
86
  log_path: Path
63
87
  status_path: Path
64
88
  status_extra: Mapping[str, Any]
89
+ served_model_normalizer: ServedModelNormalizer | None
90
+ served_model_verifier: Callable[[ServedModelAttestation], str] | None
91
+
92
+
93
+ @dataclass(frozen=True)
94
+ class _V2InvocationContext:
95
+ status_extra: Mapping[str, Any]
96
+ verify_attestation: Callable[[ServedModelAttestation], str]
97
+ write_policy: WritePolicy
98
+ write_enforcement: WriteEnforcement
65
99
 
66
100
 
67
101
  def parse_invocation(argv: list[str]) -> Invocation:
@@ -70,6 +104,7 @@ def parse_invocation(argv: list[str]) -> Invocation:
70
104
  # Empty unless the dispatcher issued one. Without it the CLI picks its own
71
105
  # id and nothing downstream can map that session back to this worker.
72
106
  positional, session_id = _take_flag(positional, _SESSION_ID_FLAG, "")
107
+ positional, metadata_raw = _take_flag(positional, _INVOCATION_METADATA_FLAG, "")
73
108
  if not 4 <= len(positional) <= 7:
74
109
  raise PreflightError(64, _USAGE)
75
110
  provider_id, project_root_raw, model, prompt_raw = positional[:4]
@@ -77,7 +112,6 @@ def parse_invocation(argv: list[str]) -> Invocation:
77
112
  role = positional[5] if len(positional) >= 6 and positional[5] else "worker"
78
113
  timeout_raw = positional[6] if len(positional) >= 7 and positional[6] else ""
79
114
 
80
- spec = _provider_spec(provider_id)
81
115
  project_root = _existing_dir(project_root_raw, 65, "project-root")
82
116
  if not model:
83
117
  raise PreflightError(66, "model-execution-value is empty")
@@ -86,6 +120,32 @@ def parse_invocation(argv: list[str]) -> Invocation:
86
120
  worktree = (
87
121
  _existing_dir(worktree_raw, 68, "worktree-path") if worktree_raw else None
88
122
  )
123
+ spec = _provider_spec(provider_id)
124
+ status_extra: dict[str, Any] = {"wrapper": spec.wrapper, "role": role}
125
+ served_model_normalizer: ServedModelNormalizer | None = None
126
+ served_model_verifier: Callable[[ServedModelAttestation], str] | None = None
127
+ write_policy: WritePolicy | None = None
128
+ write_enforcement: WriteEnforcement | None = None
129
+ if metadata_raw:
130
+ context = _v2_status_extra(
131
+ metadata_path=_existing_file(
132
+ metadata_raw, 64, "invocation-metadata"
133
+ ),
134
+ project_root=project_root,
135
+ prompt_path=prompt_path,
136
+ provider_id=provider_id,
137
+ model=model,
138
+ role=role,
139
+ )
140
+ status_extra.update(context.status_extra)
141
+ served_model_normalizer = spec.served_model_normalizer
142
+ served_model_verifier = context.verify_attestation
143
+ write_policy = context.write_policy
144
+ write_enforcement = context.write_enforcement
145
+ if not spec.supports_role(role):
146
+ raise PreflightError(
147
+ 64, f"provider {provider_id!r} does not support role {role!r}"
148
+ )
89
149
 
90
150
  request = build_request(
91
151
  prompt_text=prompt_path.read_text(encoding="utf-8"),
@@ -95,6 +155,8 @@ def parse_invocation(argv: list[str]) -> Invocation:
95
155
  role=role,
96
156
  idle_timeout_seconds=idle_timeout_seconds,
97
157
  session_id=session_id,
158
+ write_policy=write_policy,
159
+ write_enforcement=write_enforcement,
98
160
  )
99
161
  strategy = spec.exec_strategy
100
162
  _check_command(strategy, request)
@@ -104,7 +166,9 @@ def parse_invocation(argv: list[str]) -> Invocation:
104
166
  presentation=presentation,
105
167
  log_path=_log_path(prompt_path),
106
168
  status_path=Path(f"{prompt_path}.status.json"),
107
- status_extra={"wrapper": spec.wrapper, "role": role},
169
+ status_extra=status_extra,
170
+ served_model_normalizer=served_model_normalizer,
171
+ served_model_verifier=served_model_verifier,
108
172
  )
109
173
 
110
174
 
@@ -149,6 +213,152 @@ def _take_presentation(argv: list[str]) -> tuple[list[str], str]:
149
213
  return positional, presentation
150
214
 
151
215
 
216
+ def _v2_status_extra(
217
+ *,
218
+ metadata_path: Path,
219
+ project_root: Path,
220
+ prompt_path: Path,
221
+ provider_id: str,
222
+ model: str,
223
+ role: str,
224
+ ) -> _V2InvocationContext:
225
+ try:
226
+ metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
227
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
228
+ raise PreflightError(64, "invocation metadata is invalid") from exc
229
+ if not isinstance(metadata, dict):
230
+ raise PreflightError(64, "invocation metadata must be an object")
231
+ try:
232
+ identity = invocation_metadata_identity(metadata)
233
+ assignment = agent_model_assignment_from_payload(
234
+ metadata.get("modelAssignment")
235
+ )
236
+ except AgentInvocationError as exc:
237
+ raise PreflightError(64, str(exc)) from exc
238
+ if identity is None:
239
+ raise PreflightError(64, "new worker dispatch requires v2 invocation metadata")
240
+ if (
241
+ assignment.provider != provider_id
242
+ or assignment.model_execution_value != model
243
+ ):
244
+ raise PreflightError(64, "invocation metadata model assignment does not match wrapper")
245
+ try:
246
+ expected_role = role_for_duty(identity.duty_id)
247
+ actual_role = normalize_role(role)
248
+ except ValueError as exc:
249
+ raise PreflightError(64, f"invocation metadata role is invalid: {exc}") from exc
250
+ if expected_role != actual_role:
251
+ raise PreflightError(64, "invocation metadata duty does not match role")
252
+ prompt = metadata.get("prompt")
253
+ recorded_path = prompt.get("path") if isinstance(prompt, Mapping) else None
254
+ if not isinstance(recorded_path, str):
255
+ raise PreflightError(64, "invocation metadata prompt path is invalid")
256
+ recorded = Path(recorded_path)
257
+ if not recorded.is_absolute():
258
+ recorded = project_root / recorded
259
+ if recorded.resolve(strict=False) != prompt_path.resolve(strict=True):
260
+ raise PreflightError(64, "invocation metadata prompt does not match wrapper")
261
+ role_execution, invocation, pool = _execution_authority(
262
+ metadata=metadata,
263
+ project_root=project_root,
264
+ role_execution_ref=identity.role_execution_ref,
265
+ invocation_ref=identity.invocation_ref,
266
+ )
267
+ binding = role_execution.binding
268
+ if (
269
+ role_execution.participant_ref != identity.participant_ref
270
+ or role_execution.role != expected_role
271
+ or role_execution.provider != assignment.provider
272
+ or binding is None
273
+ or binding.runner != assignment.runner
274
+ or binding.resolved_execution_value != assignment.model_execution_value
275
+ ):
276
+ raise PreflightError(
277
+ 64, "invocation metadata role execution binding does not match run manifest"
278
+ )
279
+ if role_execution.execution_label != identity.execution_label:
280
+ raise PreflightError(
281
+ 64, "invocation metadata execution label does not match run manifest"
282
+ )
283
+
284
+ def verify_attestation(attestation: ServedModelAttestation) -> str:
285
+ try:
286
+ verify_served_model(role_execution, attestation, pool=pool)
287
+ except DispatchError as exc:
288
+ return str(exc)
289
+ return ""
290
+
291
+ try:
292
+ write_policy = write_policy_from_payload(invocation.write_policy)
293
+ write_enforcement = write_enforcement_from_payload(
294
+ invocation.write_enforcement
295
+ )
296
+ except ValueError as exc:
297
+ raise PreflightError(64, f"invocation write contract is invalid: {exc}") from exc
298
+
299
+ return _V2InvocationContext({
300
+ "schemaVersion": "2.0",
301
+ "executionIdentityVersion": 2,
302
+ "participantRef": identity.participant_ref,
303
+ "roleExecutionRef": identity.role_execution_ref,
304
+ "executionLabel": identity.execution_label,
305
+ "dutyId": identity.duty_id,
306
+ "invocationRef": identity.invocation_ref,
307
+ "attempt": identity.attempt,
308
+ "servedModelAttestation": {
309
+ "observedModel": None,
310
+ "normalizedModelRef": None,
311
+ "level": "unknown",
312
+ "source": "unavailable",
313
+ },
314
+ "writePolicyDigest": invocation.write_policy_digest,
315
+ "writeEnforcement": write_enforcement.to_payload(),
316
+ }, verify_attestation, write_policy, write_enforcement)
317
+
318
+
319
+ def _execution_authority(
320
+ *,
321
+ metadata: Mapping[str, Any],
322
+ project_root: Path,
323
+ role_execution_ref: str,
324
+ invocation_ref: str,
325
+ ):
326
+ source = metadata.get("contractSource")
327
+ manifest_raw = source.get("runManifestPath") if isinstance(source, Mapping) else None
328
+ if not isinstance(manifest_raw, str) or not manifest_raw:
329
+ raise PreflightError(64, "invocation metadata run manifest is invalid")
330
+ manifest_path = Path(manifest_raw)
331
+ if not manifest_path.is_absolute():
332
+ manifest_path = project_root / manifest_path
333
+ try:
334
+ manifest_path = manifest_path.resolve(strict=True)
335
+ manifest_path.relative_to(project_root.resolve(strict=True))
336
+ manifest = read_execution_manifest(manifest_path)
337
+ except (OSError, ValueError) as exc:
338
+ raise PreflightError(64, "invocation metadata run manifest is invalid") from exc
339
+ role_execution = next((
340
+ row for row in manifest.role_executions
341
+ if row.role_execution_ref == role_execution_ref
342
+ ), None)
343
+ if role_execution is None:
344
+ raise PreflightError(
345
+ 64, "invocation metadata role execution does not match run manifest"
346
+ )
347
+ invocation = next((
348
+ row for row in manifest.invocations
349
+ if row.invocation_ref == invocation_ref
350
+ ), None)
351
+ if invocation is None or invocation.role_execution_ref != role_execution_ref:
352
+ raise PreflightError(
353
+ 64, "invocation metadata invocation does not match run manifest"
354
+ )
355
+ return (
356
+ role_execution,
357
+ invocation,
358
+ ModelPool.from_registry(default_provider_registry()),
359
+ )
360
+
361
+
152
362
  def _provider_spec(provider_id: str) -> ProviderSpec:
153
363
  try:
154
364
  spec = default_provider_registry().resolve(provider_id)
@@ -214,6 +424,8 @@ def main(argv: list[str]) -> int:
214
424
  log_path=invocation.log_path,
215
425
  status_path=invocation.status_path,
216
426
  status_extra=invocation.status_extra,
427
+ served_model_normalizer=invocation.served_model_normalizer,
428
+ served_model_verifier=invocation.served_model_verifier,
217
429
  )
218
430
  except PreflightError as exc:
219
431
  print(f"okstra-provider-exec: {exc}", file=sys.stderr)
@@ -50,6 +50,7 @@ from pathlib import Path
50
50
 
51
51
  sys.path.insert(0, str(Path(__file__).resolve().parent))
52
52
 
53
+ from okstra_ctl import next_phase # noqa: E402
53
54
  from okstra_ctl.final_report_paths import final_report_markdown_path # noqa: E402
54
55
  from okstra_ctl.paths import task_manifest_file # noqa: E402
55
56
  from okstra_ctl.workflow import PHASE_SEQUENCE # noqa: E402
@@ -199,10 +200,11 @@ def _spawn_one(
199
200
  "workflow": {
200
201
  "currentPhase": suggested,
201
202
  "currentPhaseState": "not-started",
202
- "nextRecommendedPhase": suggested,
203
+ "nextRecommendedPhase": next_phase.make(
204
+ suggested, next_phase.STATUS_READY, "follow-up 으로 생성됨"
205
+ ),
203
206
  "phaseStates": {},
204
207
  "awaitingApproval": False,
205
- "routingStatus": "follow-up-spawned",
206
208
  },
207
209
  }
208
210
  _write_manifest(task_manifest_file(task_root), manifest_payload)
@@ -222,6 +222,16 @@ PY_ARGS=(
222
222
  [[ -n "${DIRECTIVE-}" ]] && PY_ARGS+=(--directive "$DIRECTIVE")
223
223
  [[ -n "${FIX_CYCLE-}" ]] && PY_ARGS+=(--fix-cycle "$FIX_CYCLE")
224
224
  [[ -n "${WORKERS_OVERRIDE-}" ]] && PY_ARGS+=(--workers "$WORKERS_OVERRIDE")
225
+ for role_count in "${ROLE_COUNTS[@]-}"; do
226
+ [[ -z "$role_count" ]] && continue
227
+ PY_ARGS+=(--role-count "$role_count")
228
+ done
229
+ for role_model in "${ROLE_MODELS[@]-}"; do
230
+ [[ -z "$role_model" ]] && continue
231
+ PY_ARGS+=(--role-model "$role_model")
232
+ done
233
+ [[ -n "${HOST_SESSION_CONTEXT_JSON-}" ]] && \
234
+ PY_ARGS+=(--host-session-context-json "$HOST_SESSION_CONTEXT_JSON")
225
235
  [[ -n "${LEAD_PROVIDER_OVERRIDE-}" ]] && PY_ARGS+=(--lead-provider "$LEAD_PROVIDER_OVERRIDE")
226
236
  [[ -n "${LEAD_MODEL_OVERRIDE-}" ]] && PY_ARGS+=(--lead-model "$LEAD_MODEL_OVERRIDE")
227
237
  [[ -n "${CLAUDE_MODEL_OVERRIDE-}" ]] && PY_ARGS+=(--claude-model "$CLAUDE_MODEL_OVERRIDE")
@@ -18,11 +18,11 @@ For a new `implementation-planning` run, the plan-body sequence is initial verif
18
18
  {{PHASE_ALLOWED_OUTPUTS}}
19
19
  - Forbidden actions in this phase:
20
20
  {{PHASE_FORBIDDEN_ACTIONS}}
21
- - This run executes `{{WORKFLOW_CURRENT_PHASE}}` only. Do not start `{{WORKFLOW_NEXT_RECOMMENDED_PHASE}}` or any later phase inside this run, even if the user says "proceed to the next step" or similar.
21
+ - This run executes `{{WORKFLOW_CURRENT_PHASE}}` only. Do not start any later phase inside this run, even if the user says "proceed to the next step" or similar. Which phase comes next is not decided yet — this run's final report decides it.
22
22
  {{STAGE_BATCH_DIRECTIVE}}
23
23
  {{VERIFICATION_TARGET}}
24
24
  {{STAGE_INTEGRATION}}
25
- - Phase advancement requires a new okstra invocation launched with `--task-type {{WORKFLOW_NEXT_RECOMMENDED_PHASE}}` after this run's final report is written and approved. The lead must not write source code, run builds/migrations/deployments, or otherwise produce artifacts of a different phase from inside this run.
25
+ - Phase advancement requires a new okstra invocation, launched with an explicit `--task-type` after this run's final report is written and approved. The target of that run comes from the pointer this run's report authors into `workflow.nextRecommendedPhase`, and only when that pointer's `status` says it can be started. The lead must not write source code, run builds/migrations/deployments, or otherwise produce artifacts of a different phase from inside this run.
26
26
  - See `Lifecycle Phase Boundaries` in the lifecycle core contract (`{{OKSTRA_LEAD_CONTRACT_PATH}}`) for the canonical rules and the phase-transition checklist.
27
27
 
28
28
  {{TEAM_CREATION_GATE}}
@@ -46,9 +46,8 @@
46
46
  | `workflow.currentPhaseState` | Current lifecycle phase state |
47
47
  | `workflow.phaseStates` | Phase-by-phase lifecycle state map |
48
48
  | `workflow.lastCompletedPhase` | Last completed lifecycle phase |
49
- | `workflow.nextRecommendedPhase` | Next recommended lifecycle phase |
49
+ | `workflow.nextRecommendedPhase` | Next-Phase Pointer — an object carrying the target phase, whether it can be started, and why. A projection of this report's Phase Routing; not authored here. Written per the Artifact Persistence Checklist in [report-writer](./report-writer.md); do not re-derive its rules here. |
50
50
  | `workflow.awaitingApproval` | Approval wait marker |
51
- | `workflow.routingStatus` | Routing decision status |
52
51
  | `workflow.lastSafeCheckpoint` | Safe resume checkpoint metadata |
53
52
  | `instructionSetPath` | Path to the `instruction-set/` **directory** containing `analysis-packet.md`, `analysis-profile.md`, `analysis-material.md`, `reference-expectations.md`, `task-brief.md`, `final-report-template.md`, and — only for task types whose host orchestration carries gates — `host-orchestration-rules.md` (see Step 4). Not a single-file path. |
54
53
  | `referenceExpectationsPath` | config/deployment expectation artifact path |
@@ -84,7 +84,7 @@ Read the worker result files generated in Phase 4/5 and extract individual findi
84
84
  - Same semantics but disjoint ticket sets → separate groups (do NOT over-merge across tickets).
85
85
  - Only one worker confirms a finding → one single-source group.
86
86
  4. When grouping is ambiguous, prefer splitting over merging (avoid over-merging). Semantic matching, ticket-set equality, and evidence interpretation remain lead judgments; the engine does not perform fuzzy matching or decide whether evidence is credible.
87
- 5. Write `runs/<task-type>/state/convergence-groups-<task-type>-<seq>.json`. Each group carries its `ticketIds`, `originWorker`, `originEvidence`, `discoveredBy`, and every `<worker>:<item-id>` source in `sourceItems`. For analysis sidetracks where ticket tagging is not required, `ticketIds: []` is the canonical value; never synthesize `"unknown"` or another placeholder. `scripts/okstra_ctl/convergence_engine.py` and `schemas/convergence-groups-v1.0.schema.json` enforce the required array field and reject non-string or blank entries while allowing the empty array. When a live command or external read produced reproducible evidence, also include `evidenceArtifacts[]` with its `.okstra/` path, SHA-256 digest, command, and environment. The field is optional because historical or inaccessible evidence may not have a captured artifact. The lead and verifier MUST NOT infer live or external evidence from wording or keyword matching; they use the finding's explicit claim, provenance, and supplied artifacts. Include the resolved worker roster in order with functional `audience` values; do not derive scope from provider or model identity. The `audience` enum is a convergence role, not a phase label: every finding-producing worker uses `analysis` — an `implementation` run's verifiers included — and only the report author uses `report-writer`. There is no `implementation-verifier` audience here; map the verifier roster to `analysis`.
87
+ 5. Write `runs/<task-type>/state/convergence-groups-<task-type>-<seq>.json`. Each group carries its `ticketIds`, `originWorker`, `originEvidence`, `discoveredBy`, and every `<worker>:<item-id>` source in `sourceItems`. For analysis sidetracks where ticket tagging is not required, `ticketIds: []` is the canonical value; never synthesize `"unknown"` or another placeholder. `scripts/okstra_ctl/convergence_engine.py` and the version-selected convergence-groups schema enforce the required array field and reject non-string or blank entries while allowing the empty array. When a live command or external read produced reproducible evidence, also include `evidenceArtifacts[]` with its `.okstra/` path, SHA-256 digest, command, and environment. The field is optional because historical or inaccessible evidence may not have a captured artifact. The lead and verifier MUST NOT infer live or external evidence from wording or keyword matching; they use the finding's explicit claim, provenance, and supplied artifacts. Include the resolved worker roster in order with functional `audience` values; do not derive scope from provider or model identity. In a v2 run, set top-level `schemaVersion: "2.0"`, `executionIdentityVersion: 2`, and `runManifestPath` to the current run manifest's exact canonical project-relative path. Write the groups artifact under that same resolved run directory's `state/` directory. Every v2 worker row also carries the paired `participantRef` and `sourceRoleExecutionRef` from that run manifest's canonical role state. Set `sourceRoleExecutionRef` to the selected source `RoleExecution` row's `roleExecutionRef`, not that row's `sourceRoleExecutionRef` field; a static source row has null in the latter field. Copy `participantRef` from that same selected row. Never derive those references from the worker name, provider, model, or execution label. A legacy v1 document keeps `schemaVersion: "1.0"` and omits `executionIdentityVersion`, `runManifestPath`, and both worker reference fields. The `audience` enum is a convergence role, not a phase label: every finding-producing worker uses `analysis` and selects an `analyser`, `designer`, `planner`, or `verifier` source role. Only the report author uses `report-writer`, paired with a `report-writer` source role. There is no `implementation-verifier` audience here; map an implementation verifier to `analysis`.
88
88
  6. Do not write a queue or classification in this grouped-input artifact. `okstra convergence seed` classifies Round 0 by mode:
89
89
  - Collaborative mode: multi-source groups become `full-consensus` immediately; only single-source groups enter the working queue.
90
90
  - Adversarial mode: every finding enters the working queue regardless of source count. Semantic grouping merges provenance only; it does not decide a finding is reliable.
@@ -106,8 +106,8 @@ convergence-<task-type>-<seq>.json
106
106
 
107
107
  Follow this protocol exactly:
108
108
 
109
- 0. Two schemas describe what the reducer reads: `schemas/convergence-groups-v1.0.schema.json` feeds step 1's `seed --groups`, and `schemas/convergence-round-results-v1.0.schema.json` feeds step 4's `apply-round --results`. `schemas/convergence-critic-results-v1.0.schema.json` is a third shape but **not** a reducer input — it describes the critic worker's own result document. Step 6's `apply-critic-gaps --results` takes the coverage batch you assemble from those candidates plus each analyser's vote (`{schemaVersion, taskKey, mode, provider, modelExecutionValue, dispatches[], gaps[]}`, spelled out in §"Coverage critic pass" §"State"); feeding the critic document straight in is rejected, by design. `okstra convergence example --kind <groups|round-results|critic-results>` prints a deterministic valid instance of each and writes only JSON to stdout.
110
- 1. Run `okstra convergence seed --groups <groups> --work-state <work> --final-state <final> --migration-dir <state/migrations>`. A `reuse-final` action means validate the existing final and continue to Phase 6. `create-work`, `resume-work`, and `restart-round0` continue with planning.
109
+ 0. Version-selected schemas describe what the reducer reads: `schemas/convergence-groups-v1.0.schema.json` accepts only legacy groups, `schemas/convergence-groups-v2.0.schema.json` accepts only explicit v2 execution identity, and `schemas/convergence-round-results-v1.0.schema.json` feeds step 4's `apply-round --results`. `schemas/convergence-critic-results-v1.0.schema.json` is a fourth shape but **not** a reducer input — it describes the critic worker's own result document. Step 6's `apply-critic-gaps --results` takes the coverage batch you assemble from those candidates plus each analyser's vote (`{schemaVersion, taskKey, mode, provider, modelExecutionValue, dispatches[], gaps[]}`, spelled out in §"Coverage critic pass" §"State"); feeding the critic document straight in is rejected, by design. `okstra convergence example --kind <groups|round-results|critic-results>` prints a deterministic valid v1 instance of each and writes only JSON to stdout.
110
+ 1. Run `okstra convergence seed --groups <groups> --run-manifest <current-run-manifest> --work-state <work> --final-state <final> --migration-dir <state/migrations>` for a v2 worker roster. `--run-manifest` must be the exact current manifest named by the groups document's `runManifestPath`; a previous run from the same task is not interchangeable. Omit the flag for a legacy v1 roster. A `reuse-final` action means validate the existing final and continue to Phase 6. `create-work`, `resume-work`, and `restart-round0` continue with planning.
111
111
  2. Run `okstra convergence plan-round --work-state <work> --plan <round-plan>`. This is read-only with respect to the working state.
112
112
  3. When the plan action is `dispatch`, create exactly one reverify prompt for each `dispatches[]` row and dispatch it through the selected runtime adapter. Its findings are exactly that row's `findingIds`.
113
113
  4. Convert parsed verdicts and every terminal dispatch outcome into `convergence-round-<N>-results-<task-type>-<seq>.json`; then run `okstra convergence apply-round --work-state <work> --plan <round-plan> --results <round-results>`.
@@ -228,21 +228,30 @@ Design intent: one `counter-evidence` refute denies a claim consensus (it cannot
228
228
 
229
229
  For every finding reverify row and critic-gap verification row, first write a
230
230
  call-specific task-instructions file under the current run's `state/`
231
- directory. Then run `okstra agent-prompt materialize` with `--audience
232
- reverification-worker`, `--assignment-ref reverify/<workerId>`, the exact
233
- `--worker-id`, `--dispatch-kind reverify-r<N>`, and the authorized
234
- prompt/result/audit paths. The returned `promptPath` is the only body that may
235
- be dispatched; do not append role prose or reconstruct model headers after
236
- materialization.
237
-
238
- If the dispatch gate then rejects that prompt, fix the task-instructions file
239
- and re-run the same `materialize` call with `--replace-undispatched` — keep the
240
- `--invocation-id`. A published prompt is otherwise immutable, so without that
241
- flag the retry fails as `existing_invocation_conflict`; do NOT delete the
242
- reservation under `prompts/.agent-invocations` and do NOT mint a second
243
- invocation id to get around it, because both detach the audit chain from the
244
- call it describes. The flag is checked: once any dispatch row names this
245
- invocation, the prompt is history and the replacement is refused.
231
+ directory. For a v2 run, take `participantRef` and
232
+ `sourceRoleExecutionRef` from the canonical round-plan dispatch row or the
233
+ matching canonical working-state worker row. That stored reference is the
234
+ selected source `RoleExecution` row's `roleExecutionRef`, not that row's
235
+ `sourceRoleExecutionRef` field. Then run `okstra agent-prompt
236
+ materialize` with `--audience
237
+ reverification-worker`, `--assignment-ref reverify/<workerId>`,
238
+ `--source-role-execution-ref
239
+ <sourceRoleExecutionRef>`, the exact `--worker-id`, `--dispatch-kind
240
+ reverify-r<N>`, and the authorized prompt/result/audit paths. Do not derive the
241
+ source role execution from `workerId`, provider, model, or execution-label
242
+ text. A legacy v1 run omits `--source-role-execution-ref`; the materializer
243
+ keeps its legacy read-only identity resolution for that schema version. The
244
+ returned `promptPath` is the only body that may be dispatched; do not append
245
+ role prose or reconstruct model headers after materialization.
246
+
247
+ If the dispatch gate then rejects that prompt, preserve its prompt, metadata,
248
+ reservation, and append-only Invocation bytes. A v2 reverify correction uses a
249
+ fresh `--invocation-id` and fresh prompt/metadata path; the materializer rejects
250
+ `--replace-undispatched` for this dynamic identity path. A legacy v1 correction
251
+ may fix the task-instructions file and re-run the same `materialize` call with
252
+ `--replace-undispatched` while keeping the `--invocation-id`. Once any dispatch
253
+ row names the invocation, replacement is rejected even for v1 and the prompt is
254
+ history. Do not delete prompt, metadata, or reservation files by hand.
246
255
 
247
256
  Run `okstra agent-prompt verify --run-manifest <path> --metadata
248
257
  <metadataPath> --json` immediately before dispatch. A failed verification is a
@@ -44,9 +44,10 @@ Read-side inspection (`/okstra-inspect`) and scheduling (`/okstra-schedule-gen`)
44
44
 
45
45
  ## Core operating contract
46
46
 
47
- - The `lead` owns orchestration, convergence supervision, and final-report review/approval. It does not author the final-report file when `Report writer worker` is in the roster.
48
- - Standard worker roles remain `Claude worker`, `Codex worker`, `Antigravity worker`, and `Report writer worker`; these are provider/functional-role identities, not lead-runtime primitives.
49
- - `Report writer worker`, when in the roster, is the **author** of the final-report file. Lead reviews the draft and may request a revision via a follow-up dispatch, but MUST NOT write the report itself as a "shortcut". The only legal lead-authored fallback is when a Report writer worker dispatch was actually attempted and recorded a terminal status of `error`/`timeout`/`not-run` with an explicit reason in team-state — see [report-writer](./report-writer.md) "Lead-authored fallback".
47
+ - The `leader` owns orchestration, convergence supervision, and final-report review/approval. It does not author the final-report file when `Report writer worker` is in the roster. `lead` is a compatibility alias for `leader` and must not be written on new artifacts.
48
+ - Dispatch consumes stored role executions, not provider-named worker IDs. Canonical roles are `leader`, `analyser`, `critic`, `designer`, `planner`, `implementer`, `verifier`, `report-writer`, and `translator`. `executor` is a compatibility alias for `implementer`.
49
+ - Pane titles and operational rows use the stored `executionLabel`. Do not rebuild that label from a provider name or model string.
50
+ - `report-writer`, when in the roster, is the **author** of the final-report file. Lead reviews the draft and may request a revision via a follow-up dispatch, but MUST NOT write the report itself as a "shortcut". The only legal lead-authored fallback is when a Report writer worker dispatch was actually attempted and recorded a terminal status of `error`/`timeout`/`not-run` with an explicit reason in team-state — see [report-writer](./report-writer.md) "Lead-authored fallback".
50
51
  - "Session resume", "team is no longer alive", and similar are NOT valid reasons to skip Report writer worker dispatch — see [report-writer](./report-writer.md) "Resume-safe dispatch".
51
52
  - A shell command the lead runs must not be able to ask a question. The lead's shell is the user's own, where `cp`, `mv`, and `rm` are commonly aliased to their `-i` form; the confirmation that alias raises has nobody to answer it, so the call hangs until it is killed — observed as a `cp` over an existing state file stalling a whole self-fix round. Invoke these as `command cp` / `command mv` / `command rm`, which skips alias expansion and leaves the tool's own behaviour untouched. `-f` is not a substitute: it changes what the tool does on failure (`rm -f` reports success on a path that never existed).
52
53
  - If the brief is incomplete, continue with explicit uncertainty markers rather than fabricating confidence.
@@ -71,14 +72,14 @@ Phase-transition checklist (lead, end of run):
71
72
 
72
73
  1. Confirm the current phase's required outputs are complete and recorded in the final report.
73
74
  2. Set `workflow.phaseStates.<currentPhase>.state = "completed"` in `task-manifest.json` (validator does this when the run passes; verify the value).
74
- 3. Update `workflow.lastCompletedPhase` and `workflow.nextRecommendedPhase`.
75
+ 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.
75
76
  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.
76
77
 
77
78
  User-utterance interpretation rule:
78
79
 
79
80
  - "proceed to the next step" / "move on to the next step" / equivalent phrases are scoped to **the current phase only**. Interpret them as "produce the remaining outputs of the current phase," never as "start the next lifecycle phase."
80
81
  - If the current phase's outputs are already complete and the user clearly wants to advance, reply with the phase-transition checklist above and the exact next-run command. Wait for explicit user confirmation before any action that belongs to the next phase.
81
- - If `nextRecommendedPhase` is `implementation-planning`, the next run produces a **plan**, not code. The next run after that is `implementation`.
82
+ - If the Next-Phase Pointer target (`nextRecommendedPhase.phase`) is `implementation-planning`, the next run produces a **plan**, not code. The next run after that is `implementation`.
82
83
 
83
84
  ## Progress reporting (BLOCKING)
84
85
 
@@ -202,7 +202,14 @@ Before each verifier call, write one task-instructions file under the current
202
202
  run's `state/` directory and run `okstra agent-prompt materialize` with
203
203
  `--audience reverification-worker`,
204
204
  `--assignment-ref reverify/<workerId>`, the exact `--worker-id`, and
205
- `--dispatch-kind reverify-r<N>`. Run `okstra agent-prompt verify` against the
205
+ `--dispatch-kind reverify-r<N>`. For a v2 run, select the verifier's canonical
206
+ source `RoleExecution` row from the run manifest's static role state. Use its
207
+ `participantRef`, and set `sourceRoleExecutionRef` to the selected source
208
+ `RoleExecution` row's `roleExecutionRef`, not that row's
209
+ `sourceRoleExecutionRef` field. Pass `--source-role-execution-ref
210
+ <sourceRoleExecutionRef>`. Do not derive that reference from `workerId`,
211
+ provider, model, or execution-label text. A legacy v1 run omits this flag. Run
212
+ `okstra agent-prompt verify` against the
206
213
  returned `metadataPath` before dispatch and use the returned `promptPath`
207
214
  without modification. Native-session calls use only `hostModelValue`; before
208
215
  the host primitive, run `okstra agent-prompt record-dispatch` with the project
@@ -268,9 +275,9 @@ round before any host or provider process starts.
268
275
  - **Round completion.** A round is complete only after the renderer has run on the corrected data.json, lead has appended the round to the state file per step 6, lead has reconciled instructed groups against applied corrections — every `itemIds` entry either carries a `selfFixNote` or is still recorded as broken — **`okstra plan-verify --report <report>` exits 0** (step 5), and lead has then set that round's immutable `completedAt`. A round left with a non-zero exit carries its defect into the next round's inputs, which is how a mis-scored gate survives a whole self-fix budget. A round that was instructed but never rendered has not happened, and counting it inflates the budget that gates promotion. The state-file append is not optional bookkeeping: the next re-verification overwrites data.json's `planItems[].verdicts`, so a round that never reached `roundHistory[]` leaves no record anywhere of what it blocked on — which is the whole reason this file exists. **Enforced:** `validators/validate-run.py` `_validate_plan_body_state_rounds` requires one `roundHistory[]` entry per round `1..roundCount`, each carrying its own `gateResult` and cited by at least one item's `rounds[]`, and requires the file's `selfFixRoundsApplied` to match the report's. For a resolved correctness-critical user response, `_validate_target_round_causality` additionally requires the cited round's `completedAt` to be after every linked canonical `user-decision-required` event and no later than every linked canonical `user-decision-evaluated` event.
269
276
  - **Loop termination.** Lead — not the report-writer worker — records the round count in `planBodyVerification.selfFixRoundsApplied` at each round's end, and why the loop stopped in `planBodyVerification.selfFixStopReason`. The count must equal the highest `round` in `selfFixGroups[]`, so it is derivable from recorded work rather than self-reported. 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:
270
277
  - `all-resolved` — no planner-fixable `majority-disagree` item remains. Exit.
271
- - `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.
278
+ - `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.
272
279
  - `max-rounds-reached` — `selfFixRoundsApplied == selfFixMaxRounds`. Exit.
273
- - `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`.
280
+ - `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.
274
281
  - `not-attempted` — the loop never ran because no item qualified.
275
282
  The `no-progress` and `max-rounds-reached` exits are what make the loop terminate; `selfFixMaxRounds` alone is the backstop.
276
283
  - 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`.
@@ -357,6 +357,8 @@ Every field MUST anchor its claim with at least one evidence reference — a `pa
357
357
  2. **Evidence and Detailed Analysis** — primary evidence rows (file path, line, snippet); secondary evidence / alternate interpretations. If `reference-expectations.md` lists explicit expected values, record match/gap per row.
358
358
  - **Error-analysis diagnosis and routing.** When `header.taskType` is `error-analysis`, populate the required `errorAnalysis` object. Copy `errorAnalysis.symptomVerbatim` byte-for-byte from the symptom stated in the brief's `Source Material`; do not paraphrase it. Every `causeCandidates[]` row includes the full `supportingEvidence`, `falsifyingEvidenceChecked`, `confidence`, and `disproveWith` fields. When a candidate is a step in a propagation chain rather than a competing explanation — the analysis calls it a downstream step, a second stage, or a consequence of another candidate — set its `downstreamOf` to the ids of the candidates immediately upstream of it; leave the field absent for a candidate that stands on its own. Every id listed MUST be another candidate in the same report, no row may name itself, and the links MUST NOT form a cycle; `validators/validate-run.py::_validate_cause_chain` rejects all three. This is the only place the chain is machine-readable — prose calling a candidate "the second step of the chain" while `downstreamOf` is absent leaves the report's figure claiming the candidates are alternatives. Route `errorAnalysis.routing.nextTaskType=implementation-option-selection` with `direction=begin-option-selection`, or route `errorAnalysis.routing.nextTaskType=error-analysis` with `direction=continue-investigation`; no other pairing is valid. `verdictCard.nextStep`, `finalVerdict.nextStep`, the first `recommendedNextSteps` action and command, and the unique `followUpTasks` row whose `origin` is `phase-continuation` MUST all point to the same `errorAnalysis.routing.nextTaskType` target. The schema enforces only the presence of a `phase-continuation` row; `validators/validate-run.py::_validate_error_analysis_consistency` enforces exact target agreement and uniqueness.
359
359
  - **Implementation-option-selection comparison.** When `header.taskType` is `implementation-option-selection`, populate `implementationOptionSelection` from the converged direction-selection findings. Preserve every merged or rejected raw candidate in `candidateAudit`, and put at most three selectable candidates in `rankedOptions`. Each displayed candidate carries its requirement coverage, scope commitments, criterion scores, feasibility votes, safety blockers, unresolved feasibility facts, planning invariants, and exact coverage summary. In each displayed candidate, `expectedChangeAreas` names direction-level change surfaces, never exact file paths or an exact file list. `expectedVerification` names direction-level verification signals, never a stage list or executable test commands. `schemas/final-report-v2.0.schema.json` enforces the displayed-summary constants and the three-option cap; semantic recalculation belongs to `validators/validate-run.py`.
360
+ - **Routing.** `implementationOptionSelection.routing` is a required **string enum** — not an object — with exactly three values: `implementation-planning`, `pending-direction-selection`, `blocked`. It is the only field in this report that records where the task goes next, and Phase 7 projects `workflow.nextRecommendedPhase` from it (`scripts/okstra_ctl/next_phase.py`): `implementation-planning` becomes a `ready` pointer naming that phase, while `pending-direction-selection` and `blocked` become `pending` and `blocked` pointers carrying no phase. Only the first proposes a next run.
361
+ - **The value is determined by this run's mode and candidate set, not chosen freely.** `preselected-validation` mode routes to `implementation-planning` — the direction was already selected and this run only validated it. `candidate-comparison` mode that displays any candidate routes to `pending-direction-selection` — the user still owes the direction pick, so a comparison never routes straight to planning. `blocked` is legal only when no valid candidate exists at all, and is required in that case. **Enforced:** `scripts/okstra_ctl/implementation_options.py::validate_implementation_option_selection` rejects all three mismatches (`validated preselected direction must route to implementation-planning`, `candidate-comparison with options must await direction selection`, `routing must be blocked only when no valid options exist` / `routing may be blocked only when no valid options exist`).
360
362
  - **Implementation-planning direction branch.** When `implementationPlanning.planningContract == "selected-direction"`, read `selectedDirectionRef` and the snapshot before authoring. Materialize the snapshot into `directionRealization`, stages, validation, rollback, and bidirectional original-requirement links. Author exactly one `P-Dir-1`; its payload is the complete `directionRealization`. Its verification covers the core mechanism, architecture boundaries, planning invariants, and any hidden direction change against `selectedDirectionRef`. Do not author Option Candidates, candidate scores, a Recommended Option, or user candidate-selection fields. When current evidence requires changing the direction, author `outcome: "direction-invalidated"` and omit the execution plan. Legacy candidate-comparison reruns retain `P-Opt-*`, Option Candidates, trade-off, and Recommended Option semantics.
361
363
 
362
364
  ```json
@@ -370,6 +372,12 @@ Every field MUST anchor its claim with at least one evidence reference — a `pa
370
372
  ```
371
373
 
372
374
  - **Implementation-option-selection is non-terminal.** Its `followUpTasks` includes a `phase-continuation` row with `autoSpawn: "no"` and `priority: "P0"`; the schema's non-terminal conditional enforces row presence.
375
+ - **Implementation and final-verification routing.** Both phases record where the task goes next in a `routingRecommendation` **object** with exactly two fields: `target` is one enum value, `rationale` is the sentence that justifies it. Neither field takes free-form routing prose, and a target named only in the prose does not count — Phase 7 projects `workflow.nextRecommendedPhase` from `target` alone (`scripts/okstra_ctl/next_phase.py`), so the value you write there is the route the task actually takes.
376
+ - `implementation.routingRecommendation.target` is one of `final-verification`, `error-analysis`, `implementation-planning`, `implementation`. Pick `final-verification` when this stage's plan items landed and validation passed; `error-analysis` when a failure's cause is not understood; `implementation-planning` when the approved plan itself no longer fits the evidence; `implementation` when work remains inside this stage (the next run is a fix run).
377
+ - `finalVerification.routingRecommendation.target` is one of `release-handoff`, `release-handoff(stage-group)`, `error-analysis`, `implementation-option-selection`, `implementation-planning`, `implementation`, `done`. Both `release-handoff` values require the `accepted` verdict, and plain `release-handoff` additionally requires `verificationScope` `whole-task` — a `single-stage` accepted run routes to `release-handoff(stage-group)` instead. `done` ends the lifecycle here. `error-analysis` / `implementation-option-selection` / `implementation-planning` follow the cause-vs-direction-vs-plan split of the verdict token table above.
378
+ - `rationale` is one or two sentences on why that target and nothing else, citing the blocker ids or evidence rows behind the choice. It is the only free-form half of the field; the digest sections still point here for the full reasoning.
379
+ - `releaseHandoff.routingRecommendation` is unchanged — it stays a single prose field, because `release-handoff` is terminal and nothing projects a next phase from it.
380
+ - **Enforced:** `schemas/final-report-v2.0.schema.json` rejects a `target` outside the enum, a missing `rationale`, and any string value in either field; `validators/validate-run.py::_validate_final_verification_consistency` rejects a final-verification report whose `routingRecommendation.target` is absent, and rejects the verdict↔routing and scope↔routing combinations named above.
373
381
  3. **Recommended Next Steps** — prioritized actions. After Phase 7's follow-up spawner runs, append a row per newly created task-key (see "Phase 6 → Phase 7 execution sequence" above). **Approval-gate consistency:** when §1 carries any `Blocks: approval` row with `Status` ∈ {open, answered}, the Verdict Card `Next Step` and the first recommended step MUST point to the clarification rerun (`resume-clarification` of the SAME task-type) — never to "flip frontmatter `approved: true` → jump straight to `implementation`". Run-prep enforces this gate (`run.py _validate_approved_plan` fail-closes on those rows and on a blocking data.json `gateResult`), so a direct-implementation next-step is an instruction the reader cannot actually follow. **Cross-project pointer rule:** for cross-project dependencies (another repo / a different top-level deployment module / a published package), `crossProjectDependencies` (§5.4 Cross-Project Dependencies) is authoritative — do NOT duplicate that substance (prerequisite work / verification signals / handoff) into `recommendedNextSteps`; put only a one-line pointer to that section (no double-recording).
374
382
  4. **Follow-up Tasks** — auto-spawn-eligible table. Each row drives `okstra-spawn-followups.py`; see template §4 for the row schema.
375
383
  5. **Missing Information and Risks** — uncertain / "I don't know" items. `implementation-planning` adds §5.5 (see heading contract below); `release-handoff` adds §5.6.
@@ -417,7 +425,13 @@ Persistence steps that must be performed in Phase 7:
417
425
  - [ ] 4. **Update task-manifest.json**: Reflect task-level status and workflow lifecycle metadata
418
426
  - Update `workCategory` if the run produced a confident classification
419
427
  - Update `workflow.currentPhase`, `workflow.currentPhaseState`, `workflow.lastCompletedPhase`, and `workflow.phaseStates`
420
- - Update `workflow.nextRecommendedPhase`, `workflow.awaitingApproval`, and `workflow.routingStatus`
428
+ - Write `workflow.nextRecommendedPhase` as an object with exactly three string fields — `phase`, `status`, `rationale`. **This checklist item is the canonical statement of that field**; the lead contract, the context loader and every okstra skill point here instead of restating it, so a change to the rule belongs in this bullet.
429
+ - `status` is one of `ready` (the named phase can be started now), `pending` (this run did not settle where the task goes next), `blocked` (something outside this run must change before any phase can start), or `terminal` (the lifecycle ends here; there is no next phase).
430
+ - `phase` carries a lifecycle phase name only when `status` is `ready`; under the other three, write the empty string. This is an authoring rule for the value **you** write, not a constraint the struct enforces or a shape you can rely on when reading. `prepare` lowers a `ready` pointer to `pending` and keeps its `phase` (`scripts/okstra_ctl/render.py::_derive_next_recommended_phase`), so every in-flight task's manifest holds a non-`ready` pointer that still names a phase. Never infer launchability from a non-empty `phase` — read `status`, which is the one field that answers it.
431
+ - `rationale` is one sentence saying why. It is the only free-form field, and it is the only part of the pointer that survives a correction.
432
+ - **`phase` and `status` MUST agree with this report's own routing field.** Which field that is depends on the task-type: `requirementsDiscovery.routing.nextTaskType`, `errorAnalysis.routing.nextTaskType`, `implementationOptionSelection.routing`, `implementationPlanning.outcome`, `implementation.routingRecommendation.target`, or `finalVerification.routingRecommendation.target` (see "Implementation and final-verification routing" above for those two enums). Two task-type groups have no routing field that Phase 7 projects from: `release-handoff` (its `routingRecommendation` is prose that nothing reads) is always `terminal`, and the analysis sidetracks — `improvement-discovery`, `project-analysis`, `feature-analysis`, `change-impact-analysis` — are always `pending`, whatever a per-candidate recommendation inside the report says. One routing value is not a phase name: when `finalVerification.routingRecommendation.target` is `release-handoff(stage-group)`, write `phase` as `release-handoff`. The parenthesised part names the handoff's scope, not a different phase, and Phase 7 projects it that way — writing the parenthesised form here records a correction against a report that was right.
433
+ - **Enforced:** Phase 7 validation recomputes `phase` and `status` from that routing field (`scripts/okstra_ctl/next_phase.py::project`). On a passing run whose authored pair disagrees with the recomputed pair, `validators/validate-run.py` overwrites both with the recomputed values, replaces your `rationale` with a pointer sentence, and preserves what you wrote under `workflow.nextRecommendedPhaseCorrection.authored`. A run whose validation fails ends with the pointer `blocked`, keeping your `rationale`. So the routing field is what actually moves the task — a pointer authored against the report body changes nothing but the audit trail.
434
+ - Update `workflow.awaitingApproval`
421
435
  - Update `workflow.lastSafeCheckpoint` to the best resume point for the current task
422
436
  - [ ] 5. **Update task-index.md**: Refresh human-readable summary
423
437
  - [ ] 6. **Generate final status file**: `runs/<task-type>/status/final-<task-type>-<seq>.status` (if necessary)