okstra 0.173.0 → 0.175.1

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 (402) 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/dist/commands/execute/team.mjs +56 -0
  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/dist/commands/execute/worktree-status.mjs +115 -0
  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/dist/commands/report/finalize.mjs +64 -0
  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 +17 -3
  232. package/docs/architecture.md +12 -22
  233. package/docs/cli.md +29 -8
  234. package/docs/container.md +1 -1
  235. package/docs/contributor-change-matrix.md +3 -3
  236. package/docs/project-structure-overview.md +29 -20
  237. package/docs/task-process/README.md +3 -1
  238. package/docs/task-process/implementation-planning.md +2 -2
  239. package/docs/task-process/implementation.md +1 -1
  240. package/package.json +11 -5
  241. package/runtime/BUILD.json +2 -2
  242. package/runtime/agents/workers/claude-worker.md +3 -2
  243. package/runtime/agents/workers/report-writer-worker.md +3 -1
  244. package/runtime/agents/workers/translator-worker.md +3 -1
  245. package/runtime/bin/lib/okstra/cli.sh +12 -0
  246. package/runtime/bin/lib/okstra/globals.sh +4 -1
  247. package/runtime/bin/lib/okstra/usage.sh +4 -4
  248. package/runtime/bin/okstra-error-log.py +27 -0
  249. package/runtime/bin/okstra-provider-exec.py +245 -16
  250. package/runtime/bin/okstra-trace-cleanup.sh +58 -129
  251. package/runtime/bin/okstra.sh +10 -0
  252. package/runtime/prompts/lead/adapters/cmux.md +2 -0
  253. package/runtime/prompts/lead/convergence.md +27 -18
  254. package/runtime/prompts/lead/okstra-lead-contract.md +4 -2
  255. package/runtime/prompts/lead/plan-body-verification.md +13 -6
  256. package/runtime/prompts/lead/report-writer.md +6 -6
  257. package/runtime/prompts/lead/team-contract.md +1 -1
  258. package/runtime/prompts/profiles/_common-contract.md +2 -2
  259. package/runtime/prompts/profiles/_implementation-executor.md +2 -0
  260. package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
  261. package/runtime/prompts/profiles/change-impact-analysis.md +16 -0
  262. package/runtime/prompts/profiles/error-analysis.md +21 -1
  263. package/runtime/prompts/profiles/feature-analysis.md +16 -0
  264. package/runtime/prompts/profiles/final-verification.md +16 -0
  265. package/runtime/prompts/profiles/implementation-option-selection.md +16 -0
  266. package/runtime/prompts/profiles/implementation-planning.md +32 -9
  267. package/runtime/prompts/profiles/implementation.md +17 -1
  268. package/runtime/prompts/profiles/improvement-discovery.md +16 -0
  269. package/runtime/prompts/profiles/project-analysis.md +16 -0
  270. package/runtime/prompts/profiles/release-handoff.md +5 -1
  271. package/runtime/prompts/profiles/requirements-discovery.md +20 -0
  272. package/runtime/prompts/wizard/prompts.ko.json +18 -0
  273. package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +1 -6
  274. package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +8 -0
  275. package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +8 -0
  276. package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
  277. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +2 -2
  278. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +25 -5
  279. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
  280. package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +4 -1
  281. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +4 -4
  282. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
  283. package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +2 -2
  284. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +79 -11
  285. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +100 -25
  286. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +59 -14
  287. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +55 -9
  288. package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +51 -12
  289. package/runtime/python/okstra_ctl/agent_activity.py +5 -1
  290. package/runtime/python/okstra_ctl/agent_invocation.py +608 -14
  291. package/runtime/python/okstra_ctl/agent_prompt_cli.py +249 -50
  292. package/runtime/python/okstra_ctl/application/resolve_assignment.py +53 -12
  293. package/runtime/python/okstra_ctl/assignment_environment.py +102 -0
  294. package/runtime/python/okstra_ctl/assignment_resolver.py +790 -0
  295. package/runtime/python/okstra_ctl/attempt_evidence.py +342 -0
  296. package/runtime/python/okstra_ctl/conformance.py +68 -0
  297. package/runtime/python/okstra_ctl/convergence.py +409 -3
  298. package/runtime/python/okstra_ctl/convergence_engine.py +248 -15
  299. package/runtime/python/okstra_ctl/convergence_store.py +170 -1
  300. package/runtime/python/okstra_ctl/dispatch_core.py +1181 -102
  301. package/runtime/python/okstra_ctl/dispatch_state.py +835 -22
  302. package/runtime/python/okstra_ctl/doctor.py +16 -2
  303. package/runtime/python/okstra_ctl/domain/host.py +32 -1
  304. package/runtime/python/okstra_ctl/domain/provider.py +212 -6
  305. package/runtime/python/okstra_ctl/domain/role.py +125 -0
  306. package/runtime/python/okstra_ctl/domain/wizard/interaction.py +13 -0
  307. package/runtime/python/okstra_ctl/domain/worker_exec.py +39 -1
  308. package/runtime/python/okstra_ctl/domain/worker_role.py +13 -3
  309. package/runtime/python/okstra_ctl/domain/worker_stream.py +24 -0
  310. package/runtime/python/okstra_ctl/error_log_write.py +9 -0
  311. package/runtime/python/okstra_ctl/error_report.py +21 -1
  312. package/runtime/python/okstra_ctl/execution_identity.py +529 -0
  313. package/runtime/python/okstra_ctl/execution_manifest.py +1281 -0
  314. package/runtime/python/okstra_ctl/execution_mutation_audit.py +619 -0
  315. package/runtime/python/okstra_ctl/final_report_schema.py +5 -4
  316. package/runtime/python/okstra_ctl/index.py +6 -2
  317. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +43 -3
  318. package/runtime/python/okstra_ctl/legacy_model_selection.py +490 -0
  319. package/runtime/python/okstra_ctl/model_cli.py +278 -0
  320. package/runtime/python/okstra_ctl/model_defaults.py +46 -0
  321. package/runtime/python/okstra_ctl/model_discovery.py +26 -7
  322. package/runtime/python/okstra_ctl/model_pool.py +501 -0
  323. package/runtime/python/okstra_ctl/models.py +47 -27
  324. package/runtime/python/okstra_ctl/mutation_recovery.py +293 -0
  325. package/runtime/python/okstra_ctl/pane_reclaim.py +13 -22
  326. package/runtime/python/okstra_ctl/pane_title.py +151 -0
  327. package/runtime/python/okstra_ctl/ports/host_model.py +48 -17
  328. package/runtime/python/okstra_ctl/ports/worker_dispatch.py +4 -0
  329. package/runtime/python/okstra_ctl/registry/provider_registry.py +8 -4
  330. package/runtime/python/okstra_ctl/render.py +78 -10
  331. package/runtime/python/okstra_ctl/render_final_report.py +34 -19
  332. package/runtime/python/okstra_ctl/report_contract.py +39 -1
  333. package/runtime/python/okstra_ctl/report_finalize.py +84 -9
  334. package/runtime/python/okstra_ctl/report_html/render.py +1 -0
  335. package/runtime/python/okstra_ctl/role_requirements.py +217 -0
  336. package/runtime/python/okstra_ctl/run.py +1250 -260
  337. package/runtime/python/okstra_ctl/run_index_row.py +7 -1
  338. package/runtime/python/okstra_ctl/schema_excerpt.py +1 -1
  339. package/runtime/python/okstra_ctl/scope_provenance.py +1 -1
  340. package/runtime/python/okstra_ctl/session.py +69 -12
  341. package/runtime/python/okstra_ctl/team.py +51 -25
  342. package/runtime/python/okstra_ctl/time_report.py +38 -4
  343. package/runtime/python/okstra_ctl/tmux.py +19 -149
  344. package/runtime/python/okstra_ctl/usage_identity.py +86 -0
  345. package/runtime/python/okstra_ctl/usage_report.py +15 -3
  346. package/runtime/python/okstra_ctl/wizard.py +1253 -62
  347. package/runtime/python/okstra_ctl/worker_audit_ledger.py +68 -65
  348. package/runtime/python/okstra_ctl/worker_request.py +9 -1
  349. package/runtime/python/okstra_ctl/worker_runner.py +130 -5
  350. package/runtime/python/okstra_ctl/workers.py +7 -5
  351. package/runtime/python/okstra_ctl/worktree.py +69 -3
  352. package/runtime/python/okstra_ctl/write_policy.py +571 -0
  353. package/runtime/python/okstra_token_usage/cli.py +1 -1
  354. package/runtime/python/okstra_token_usage/collect.py +66 -6
  355. package/runtime/schemas/convergence-groups-v2.0.schema.json +128 -0
  356. package/runtime/schemas/execution-manifest-v2.schema.json +340 -0
  357. package/runtime/schemas/final-report-v2.0.schema.json +25 -0
  358. package/runtime/skills/okstra-run/SKILL.md +9 -9
  359. package/runtime/skills/okstra-setup/references/project-config.md +11 -0
  360. package/runtime/templates/report-writer-prompt-preamble.md +2 -0
  361. package/runtime/templates/reports/final-report-v2.template.md +7 -1
  362. package/runtime/templates/reports/html/base.template.html +21 -0
  363. package/runtime/templates/reports/html/i18n/en.json +4 -0
  364. package/runtime/templates/reports/html/i18n/ko.json +4 -0
  365. package/runtime/templates/reports/settings.template.json +0 -24
  366. package/runtime/templates/worker-prompt-preamble.md +6 -2
  367. package/runtime/validators/lib/fixtures.sh +98 -33
  368. package/runtime/validators/validate-implementation-plan-stages.py +63 -3
  369. package/runtime/validators/validate-run.py +443 -508
  370. package/runtime/validators/validate_session_conformance.py +26 -8
  371. package/runtime/bin/okstra-subagent-reclaim.sh +0 -26
  372. package/runtime/schemas/final-report-v1.0.schema.json +0 -6366
  373. package/runtime/templates/reports/final-report.template.md +0 -1258
  374. package/src/cli-registry.mjs +0 -532
  375. package/src/commands/execute/codex-dispatch.mjs +0 -10
  376. package/src/commands/execute/plan-items.mjs +0 -9
  377. package/src/commands/execute/plan-validate.mjs +0 -68
  378. package/src/commands/execute/run.mjs +0 -208
  379. package/src/commands/execute/team.mjs +0 -63
  380. package/src/commands/execute/wizard.mjs +0 -167
  381. package/src/commands/inspect/stage-map.mjs +0 -98
  382. package/src/commands/inspect/task-list.mjs +0 -97
  383. package/src/commands/inspect/task-show.mjs +0 -100
  384. package/src/commands/lifecycle/check-project.mjs +0 -187
  385. package/src/commands/lifecycle/config.mjs +0 -385
  386. package/src/commands/lifecycle/doctor.mjs +0 -308
  387. package/src/commands/lifecycle/install.mjs +0 -1172
  388. package/src/commands/lifecycle/paths.mjs +0 -104
  389. package/src/commands/lifecycle/preflight.mjs +0 -125
  390. package/src/commands/lifecycle/setup.mjs +0 -333
  391. package/src/commands/lifecycle/uninstall.mjs +0 -279
  392. package/src/commands/memory/memory.mjs +0 -524
  393. package/src/commands/pr/pr.mjs +0 -261
  394. package/src/commands/report/finalize.mjs +0 -64
  395. package/src/lib/host-registry-client.mjs +0 -176
  396. package/src/lib/paths.mjs +0 -86
  397. package/src/lib/proc.mjs +0 -31
  398. package/src/lib/python-helper.mjs +0 -142
  399. package/src/lib/runtime-manifest.mjs +0 -70
  400. package/src/lib/skill-catalog.mjs +0 -53
  401. package/src/lib/version.mjs +0 -20
  402. /package/{src → dist}/commands/pr/default.md +0 -0
@@ -43,14 +43,16 @@ from okstra_project.dirs import tasks_root as _okstra_tasks_root # noqa: E402
43
43
  from okstra_project.resolver import resolve_architecture # noqa: E402
44
44
 
45
45
  from okstra_ctl.conformance import ( # noqa: E402
46
- CAPABILITY_WHITELIST,
47
46
  detect_surfaces,
48
47
  evaluate_conformance,
49
48
  manifest_required_surfaces,
49
+ normalize_conformance_script as _normalize_conformance_script,
50
+ parse_conformance_tests as _parse_conformance_tests,
50
51
  qa_result_from_dict,
51
52
  validate_conformance_manifest,
52
53
  )
53
54
  from okstra_ctl.paths import RunRef # noqa: E402
55
+ from okstra_ctl.report_contract import CURRENT_REPORT_SCHEMA_VERSION # noqa: E402
54
56
  from okstra_ctl.domain.host import HostNotRegistered # noqa: E402
55
57
  from okstra_ctl.registry.host_registry import default_host_registry # noqa: E402
56
58
  from okstra_ctl.build_tools import ( # noqa: E402
@@ -120,15 +122,21 @@ from okstra_ctl.worker_prompt_contract import ( # noqa: E402
120
122
  PromptRecord,
121
123
  validate_initial_prompt_records,
122
124
  )
125
+ from okstra_ctl.domain.role import RoleCatalogError, role_for_duty # noqa: E402
123
126
  from okstra_ctl.agent_invocation import ( # noqa: E402
124
127
  AgentInvocationError,
125
128
  agent_model_assignment_from_payload,
129
+ invocation_execution_identity_from_manifest,
126
130
  verify_agent_invocation,
127
131
  )
132
+ from okstra_ctl.execution_identity import ExecutionManifestError # noqa: E402
133
+ from okstra_ctl.execution_manifest import ( # noqa: E402
134
+ validate_execution_manifest_payload,
135
+ )
128
136
  from okstra_ctl.lead_events import LeadEventParseError, read_lead_events # noqa: E402
129
137
  from okstra_ctl.worker_audit_ledger import ( # noqa: E402
130
138
  READING_CONFIRMATION_HEADING_RE,
131
- check_worker_results_audit,
139
+ worker_results_audit_findings,
132
140
  )
133
141
  from validate_analysis_report import validate_analysis_report # noqa: E402
134
142
  from okstra_ctl.convergence_engine import ( # noqa: E402
@@ -141,7 +149,7 @@ from okstra_ctl.convergence_provenance import ( # noqa: E402
141
149
 
142
150
  TERMINAL_STATUSES = {"completed", "timeout", "error", "not-run"}
143
151
  ATTEMPTED_STATUSES = {"completed", "timeout", "error"}
144
- WORKER_DISPATCH_MODES = {"cli-wrapper", "mixed", "tmux-pane"}
152
+ WORKER_DISPATCH_MODES = {"cli-wrapper", "mixed"}
145
153
  _AGENT_DISPATCH_DIGEST_KEYS = (
146
154
  "catalogDigest",
147
155
  "assignmentDigest",
@@ -149,6 +157,30 @@ _AGENT_DISPATCH_DIGEST_KEYS = (
149
157
  "instructionDigest",
150
158
  "promptDigest",
151
159
  )
160
+ _V2_AGENT_DISPATCH_IDENTITY_KEYS = (
161
+ "participantRef",
162
+ "roleExecutionRef",
163
+ "executionLabel",
164
+ "dutyId",
165
+ "invocationRef",
166
+ )
167
+
168
+
169
+ def _validate_execution_identity_v2(
170
+ run_manifest: Mapping[str, Any],
171
+ failures: list[str],
172
+ ) -> None:
173
+ version = run_manifest.get("schemaVersion")
174
+ identity_version = run_manifest.get("executionIdentityVersion")
175
+ if version in {1, "1", "1.0"} and identity_version is None:
176
+ return
177
+ if version != "2.0" or identity_version != 2:
178
+ failures.append("execution identity v2: mixed execution identity version")
179
+ return
180
+ try:
181
+ validate_execution_manifest_payload(run_manifest)
182
+ except ExecutionManifestError as exc:
183
+ failures.append(f"execution identity v2: {exc}")
152
184
 
153
185
 
154
186
  def _validate_agent_dispatch_contract(
@@ -167,6 +199,28 @@ def _validate_agent_dispatch_contract(
167
199
  if not isinstance(assignments, Mapping):
168
200
  failures.append("agent invocation metadata is missing: invocationAssignments")
169
201
  return
202
+ manifest_version = run_manifest.get("schemaVersion")
203
+ execution_identity_version = run_manifest.get("executionIdentityVersion")
204
+ if manifest_version == "2.0" and execution_identity_version == 2:
205
+ uses_v2_identity = True
206
+ elif (
207
+ manifest_version in {None, 1, "1", "1.0"}
208
+ and execution_identity_version is None
209
+ ):
210
+ uses_v2_identity = False
211
+ else:
212
+ failures.append("agent dispatch contract mixes v1 and v2 execution identity")
213
+ return
214
+ canonical_invocations = {
215
+ row.get("invocationRef"): row
216
+ for row in run_manifest.get("invocations") or []
217
+ if isinstance(row, Mapping)
218
+ } if uses_v2_identity else {}
219
+ canonical_attempts = {
220
+ (row.get("invocationRef"), row.get("attempt"))
221
+ for row in run_manifest.get("attempts") or []
222
+ if isinstance(row, Mapping)
223
+ } if uses_v2_identity else set()
170
224
  worker_dispatches = [
171
225
  row for row in (team_state.get("workerDispatches") or [])
172
226
  if isinstance(row, Mapping) and row.get("invocationId")
@@ -177,13 +231,15 @@ def _validate_agent_dispatch_contract(
177
231
  ]
178
232
  dispatches = [*worker_dispatches, *agent_dispatches]
179
233
  ids: dict[str, Mapping[str, Any]] = {}
234
+ seen_dispatch_ids: set[str] = set()
180
235
  for row in dispatches:
181
236
  dispatch_id = str(row.get("dispatchId") or "").strip()
182
237
  missing = [
183
238
  key for key in (
184
- "dispatchId", "workerId", "audience", "invocationId",
239
+ "dispatchId", "audience", "invocationId",
185
240
  "assignmentRef", "promptMetadataPath", "modelExecutionValue",
186
241
  "enforcementMode", *_AGENT_DISPATCH_DIGEST_KEYS,
242
+ *(_V2_AGENT_DISPATCH_IDENTITY_KEYS if uses_v2_identity else ("workerId",)),
187
243
  )
188
244
  if not str(row.get(key) or "").strip()
189
245
  ]
@@ -193,10 +249,43 @@ def _validate_agent_dispatch_contract(
193
249
  + ", ".join(missing)
194
250
  )
195
251
  continue
196
- if dispatch_id in ids:
252
+ if uses_v2_identity:
253
+ if "workerId" in row or row.get("executionIdentityVersion") != 2:
254
+ failures.append(
255
+ f"agent dispatch {dispatch_id} mixes v1 and v2 execution identity"
256
+ )
257
+ continue
258
+ attempt = row.get("attempt")
259
+ if (
260
+ not isinstance(attempt, int)
261
+ or isinstance(attempt, bool)
262
+ or attempt < 1
263
+ ):
264
+ failures.append(
265
+ f"agent dispatch {dispatch_id}: v2 attempt must be positive"
266
+ )
267
+ continue
268
+ if dispatch_id != f"{row['invocationId']}:attempt-{attempt}":
269
+ failures.append(
270
+ f"agent dispatch {dispatch_id}: dispatchId does not match v2 attempt"
271
+ )
272
+ continue
273
+ elif any(
274
+ key in row
275
+ for key in (
276
+ "executionIdentityVersion",
277
+ "attempt",
278
+ *_V2_AGENT_DISPATCH_IDENTITY_KEYS,
279
+ )
280
+ ):
281
+ failures.append(
282
+ f"agent dispatch {dispatch_id} mixes v1 and v2 execution identity"
283
+ )
284
+ continue
285
+ if dispatch_id in seen_dispatch_ids:
197
286
  failures.append(f"agent dispatch ID is duplicated: {dispatch_id}")
198
287
  continue
199
- ids[dispatch_id] = row
288
+ seen_dispatch_ids.add(dispatch_id)
200
289
  assignment_ref = str(row["assignmentRef"])
201
290
  try:
202
291
  assignment = agent_model_assignment_from_payload(
@@ -205,6 +294,118 @@ def _validate_agent_dispatch_contract(
205
294
  except AgentInvocationError as exc:
206
295
  failures.append(f"agent dispatch {dispatch_id}: {exc}")
207
296
  continue
297
+ execution_identity = None
298
+ if uses_v2_identity:
299
+ try:
300
+ execution_identity = invocation_execution_identity_from_manifest(
301
+ run_manifest,
302
+ assignment=assignment,
303
+ assignment_ref=assignment_ref,
304
+ duty_id=str(row["dutyId"]),
305
+ )
306
+ except AgentInvocationError as exc:
307
+ failures.append(f"agent dispatch {dispatch_id}: {exc}")
308
+ continue
309
+ if (
310
+ execution_identity is None
311
+ or row.get("participantRef") != execution_identity.participant_ref
312
+ or row.get("roleExecutionRef") != execution_identity.role_execution_ref
313
+ ):
314
+ failures.append(
315
+ f"agent dispatch {dispatch_id}: v2 identity does not match "
316
+ "a canonical role execution"
317
+ )
318
+ continue
319
+ if row.get("executionLabel") != execution_identity.execution_label:
320
+ failures.append(
321
+ f"agent dispatch {dispatch_id}: executionLabel does not match "
322
+ "canonical role execution"
323
+ )
324
+ continue
325
+ dispatch_kind = row.get("dispatchKind") or row.get("kind")
326
+ if not isinstance(dispatch_kind, str) or not dispatch_kind:
327
+ failures.append(
328
+ f"agent dispatch {dispatch_id}: dispatchKind is missing"
329
+ )
330
+ continue
331
+ invocation = canonical_invocations.get(row.get("invocationRef"))
332
+ if not isinstance(invocation, Mapping) or any((
333
+ invocation.get("participantRef") != row.get("participantRef"),
334
+ invocation.get("roleExecutionRef") != row.get("roleExecutionRef"),
335
+ invocation.get("dutyId") != row.get("dutyId"),
336
+ invocation.get("dispatchKind") != dispatch_kind,
337
+ invocation.get("inputDigest") != row.get("promptDigest"),
338
+ )):
339
+ failures.append(
340
+ f"agent dispatch {dispatch_id}: does not match canonical invocation"
341
+ )
342
+ continue
343
+ if row.get("writePolicyDigest") != invocation.get("writePolicyDigest"):
344
+ failures.append(
345
+ f"agent dispatch {dispatch_id}: writePolicyDigest does not "
346
+ "match canonical invocation"
347
+ )
348
+ continue
349
+ if row.get("writeEnforcement") != invocation.get("writeEnforcement"):
350
+ failures.append(
351
+ f"agent dispatch {dispatch_id}: writeEnforcement does not "
352
+ "match canonical invocation"
353
+ )
354
+ continue
355
+ mutation_mode = invocation.get("writeEnforcement", {}).get(
356
+ "mutationAudit"
357
+ ) if isinstance(invocation.get("writeEnforcement"), Mapping) else None
358
+ snapshot_value = row.get("mutationAuditSnapshotPath")
359
+ if mutation_mode == "batch" and not (
360
+ isinstance(snapshot_value, str) and snapshot_value.strip()
361
+ ):
362
+ failures.append(
363
+ f"agent dispatch {dispatch_id}: mutation audit snapshot is missing"
364
+ )
365
+ continue
366
+ if mutation_mode == "none" and snapshot_value not in (None, ""):
367
+ failures.append(
368
+ f"agent dispatch {dispatch_id}: exact-path dispatch has a "
369
+ "mutation audit snapshot"
370
+ )
371
+ continue
372
+ if (row.get("invocationRef"), attempt) not in canonical_attempts:
373
+ failures.append(
374
+ f"agent dispatch {dispatch_id}: has no canonical invocation attempt"
375
+ )
376
+ continue
377
+ status_path_raw = row.get("statusSidecarPath")
378
+ if isinstance(status_path_raw, str) and status_path_raw.strip():
379
+ status_path = _resolve_prompt_record_path(
380
+ project_root, status_path_raw
381
+ )
382
+ try:
383
+ status = json.loads(status_path.read_text(encoding="utf-8"))
384
+ except (OSError, json.JSONDecodeError):
385
+ failures.append(
386
+ f"agent dispatch {dispatch_id}: wrapper status is missing or invalid"
387
+ )
388
+ continue
389
+ status_identity = {
390
+ key: status.get(key)
391
+ for key in (*_V2_AGENT_DISPATCH_IDENTITY_KEYS, "attempt")
392
+ } if isinstance(status, Mapping) else {}
393
+ dispatch_identity = {
394
+ key: row.get(key)
395
+ for key in (*_V2_AGENT_DISPATCH_IDENTITY_KEYS, "attempt")
396
+ }
397
+ if (
398
+ not isinstance(status, Mapping)
399
+ or status.get("schemaVersion") != "2.0"
400
+ or status.get("executionIdentityVersion") != 2
401
+ or status_identity != dispatch_identity
402
+ ):
403
+ failures.append(
404
+ f"agent dispatch {dispatch_id}: wrapper status identity "
405
+ "does not match dispatch"
406
+ )
407
+ continue
408
+ ids[dispatch_id] = row
208
409
  metadata_path = _resolve_prompt_record_path(
209
410
  project_root, str(row["promptMetadataPath"])
210
411
  )
@@ -214,9 +415,20 @@ def _validate_agent_dispatch_contract(
214
415
  expected_run_manifest_path=run_manifest_path,
215
416
  expected_assignment=assignment,
216
417
  expected_invocation_id=str(row["invocationId"]),
217
- expected_worker_id=str(row["workerId"]),
418
+ expected_worker_id=(
419
+ None if uses_v2_identity else str(row["workerId"])
420
+ ),
218
421
  expected_assignment_ref=assignment_ref,
219
422
  expected_audience=str(row["audience"]),
423
+ expected_participant_ref=(
424
+ str(row["participantRef"]) if uses_v2_identity else None
425
+ ),
426
+ expected_role_execution_ref=(
427
+ str(row["roleExecutionRef"]) if uses_v2_identity else None
428
+ ),
429
+ expected_invocation_ref=(
430
+ str(row["invocationRef"]) if uses_v2_identity else None
431
+ ),
220
432
  )
221
433
  failures.extend(
222
434
  f"agent dispatch {dispatch_id}: {error}" for error in errors
@@ -256,10 +468,23 @@ def _validate_agent_dispatch_contract(
256
468
  else "host-native prompt delivery cannot be marked verified"
257
469
  )
258
470
 
259
- lead_ids = {
260
- dispatch_id for dispatch_id, row in ids.items()
261
- if row.get("audience") == "lead" and row.get("workerId") == "lead"
262
- }
471
+ if uses_v2_identity:
472
+ role_executions = {
473
+ row.get("roleExecutionRef"): row
474
+ for row in run_manifest.get("roleExecutions") or []
475
+ if isinstance(row, Mapping)
476
+ }
477
+ lead_ids = {
478
+ dispatch_id for dispatch_id, row in ids.items()
479
+ if row.get("audience") == "lead"
480
+ and isinstance(role_executions.get(row.get("roleExecutionRef")), Mapping)
481
+ and role_executions[row["roleExecutionRef"]].get("role") == "leader"
482
+ }
483
+ else:
484
+ lead_ids = {
485
+ dispatch_id for dispatch_id, row in ids.items()
486
+ if row.get("audience") == "lead" and row.get("workerId") == "lead"
487
+ }
263
488
  if not lead_ids:
264
489
  failures.append("accepted lead result has no lead dispatch record")
265
490
 
@@ -275,6 +500,22 @@ def _validate_agent_dispatch_contract(
275
500
  if dispatch_id not in ids or not result_path:
276
501
  failures.append("agent result link has no matching dispatch record")
277
502
  continue
503
+ if uses_v2_identity:
504
+ dispatch = ids[dispatch_id]
505
+ identity_keys = (*_V2_AGENT_DISPATCH_IDENTITY_KEYS, "attempt")
506
+ link_identity = {key: link.get(key) for key in identity_keys}
507
+ dispatch_identity = {key: dispatch.get(key) for key in identity_keys}
508
+ if "workerId" in link or link_identity != dispatch_identity:
509
+ failures.append(
510
+ f"agent result link identity does not match dispatch: {dispatch_id}"
511
+ )
512
+ continue
513
+ elif any(
514
+ key in link
515
+ for key in (*_V2_AGENT_DISPATCH_IDENTITY_KEYS, "attempt")
516
+ ):
517
+ failures.append("agent result link mixes v1 and v2 execution identity")
518
+ continue
278
519
  dispatch_link_counts[dispatch_id] = dispatch_link_counts.get(dispatch_id, 0) + 1
279
520
  previous = paths.setdefault(result_path, dispatch_id)
280
521
  if previous != dispatch_id:
@@ -294,7 +535,11 @@ def _validate_agent_dispatch_contract(
294
535
  worker_result = str(worker.get("resultPath") or "").strip()
295
536
  worker_dispatch_ids = {
296
537
  dispatch_id for dispatch_id, row in ids.items()
297
- if row.get("workerId") == worker_id
538
+ if (
539
+ row.get("assignmentRef") == f"initial/{worker_id}"
540
+ if uses_v2_identity
541
+ else row.get("workerId") == worker_id
542
+ )
298
543
  }
299
544
  if worker_id and not worker_dispatch_ids:
300
545
  failures.append(
@@ -588,13 +833,16 @@ def update_validation_metadata(
588
833
  validation_status: str,
589
834
  failures: list[str],
590
835
  report_data: Mapping[str, Any] | None = None,
836
+ advisories: list[str] | None = None,
591
837
  ) -> None:
592
838
  checked_at = utc_now()
839
+ advisories = list(advisories or [])
593
840
 
594
841
  team_state.setdefault("validator", {})
595
842
  team_state["validator"]["status"] = validation_status
596
843
  team_state["validator"]["lastValidatedAt"] = checked_at
597
844
  team_state["validator"]["failures"] = failures
845
+ team_state["validator"]["advisories"] = advisories
598
846
 
599
847
  run_manifest.setdefault("validation", {})
600
848
  run_manifest["validation"]["required"] = True
@@ -602,6 +850,8 @@ def update_validation_metadata(
602
850
  run_manifest["validation"]["lastCheckedAt"] = checked_at
603
851
  run_manifest["validation"]["passed"] = validation_status == "passed"
604
852
  run_manifest["validation"]["failures"] = failures
853
+ # Reported, not blocking — a passing run can carry these.
854
+ run_manifest["validation"]["advisories"] = advisories
605
855
  run_manifest["status"] = (
606
856
  "completed" if validation_status == "passed" else "contract-violated"
607
857
  )
@@ -707,35 +957,127 @@ def _persisted_prompt_records(
707
957
  project_root: Path,
708
958
  selected_ids: set[str],
709
959
  team_state: dict,
710
- ) -> list[PromptRecord]:
711
- dispatches = team_state.get("workerDispatches")
712
- source = dispatches if isinstance(dispatches, list) and dispatches else (
713
- team_state.get("workers") or []
960
+ run_manifest: Mapping[str, Any],
961
+ ) -> tuple[list[PromptRecord], list[str]]:
962
+ uses_v2_identity = (
963
+ run_manifest.get("schemaVersion") == "2.0"
964
+ and run_manifest.get("executionIdentityVersion") == 2
714
965
  )
966
+ dispatches = team_state.get("workerDispatches")
967
+ if uses_v2_identity:
968
+ source = [
969
+ *(dispatches if isinstance(dispatches, list) else []),
970
+ *(
971
+ team_state.get("agentDispatches")
972
+ if isinstance(team_state.get("agentDispatches"), list)
973
+ else []
974
+ ),
975
+ ]
976
+ else:
977
+ source = dispatches if isinstance(dispatches, list) and dispatches else (
978
+ team_state.get("workers") or []
979
+ )
715
980
  records: list[PromptRecord] = []
716
981
  seen: set[tuple[str, str, str]] = set()
982
+ errors: list[str] = []
983
+ role_executions = {
984
+ str(row.get("roleExecutionRef") or ""): row
985
+ for row in run_manifest.get("roleExecutions") or []
986
+ if isinstance(row, Mapping)
987
+ } if uses_v2_identity else {}
717
988
  for worker in source:
718
989
  if not isinstance(worker, dict):
719
990
  continue
720
- worker_id = str(worker.get("workerId") or "").strip()
991
+ assignment_ref = str(worker.get("assignmentRef") or "").strip()
992
+ worker_id = (
993
+ assignment_ref.removeprefix("initial/")
994
+ if uses_v2_identity and assignment_ref.startswith("initial/")
995
+ else str(worker.get("workerId") or "").strip()
996
+ )
721
997
  prompt_value = str(worker.get("promptPath") or "").strip()
722
- if worker_id not in selected_ids or not prompt_value:
998
+ if worker_id not in selected_ids:
723
999
  continue
724
- dispatch_kind = str(worker.get("kind") or "").strip()
1000
+ if not prompt_value:
1001
+ errors.append(f"{worker_id}: persisted initial prompt path is missing")
1002
+ continue
1003
+ dispatch_kind = str(
1004
+ worker.get("kind") or worker.get("dispatchKind") or ""
1005
+ ).strip()
725
1006
  if not dispatch_kind:
726
1007
  dispatch_kind = _fallback_dispatch_kind(prompt_value)
1008
+ duty_id = ""
1009
+ if uses_v2_identity:
1010
+ execution = role_executions.get(
1011
+ str(worker.get("roleExecutionRef") or "").strip()
1012
+ )
1013
+ participant_ref = str(worker.get("participantRef") or "").strip()
1014
+ duty_id = str(worker.get("dutyId") or "").strip()
1015
+ audience = str(worker.get("audience") or "").strip()
1016
+ assignments = run_manifest.get("invocationAssignments")
1017
+ assignment = (
1018
+ assignments.get(assignment_ref)
1019
+ if isinstance(assignments, Mapping)
1020
+ else None
1021
+ )
1022
+ if not isinstance(execution, Mapping):
1023
+ errors.append(
1024
+ f"{worker_id}: persisted prompt has unknown roleExecutionRef"
1025
+ )
1026
+ continue
1027
+ try:
1028
+ expected_role = role_for_duty(duty_id)
1029
+ except RoleCatalogError as exc:
1030
+ errors.append(f"{worker_id}: {exc}")
1031
+ continue
1032
+ if (
1033
+ execution.get("participantRef") != participant_ref
1034
+ or execution.get("role") != expected_role
1035
+ or execution.get("executionLabel") != worker.get("executionLabel")
1036
+ or not isinstance(assignment, Mapping)
1037
+ or execution.get("provider") != assignment.get("provider")
1038
+ or not isinstance(execution.get("binding"), Mapping)
1039
+ or execution["binding"].get("resolvedExecutionValue")
1040
+ != assignment.get("modelExecutionValue")
1041
+ ):
1042
+ errors.append(
1043
+ f"{worker_id}: persisted prompt identity does not match role execution"
1044
+ )
1045
+ continue
1046
+ if audience != duty_id:
1047
+ errors.append(
1048
+ f"{worker_id}: persisted prompt audience does not match duty"
1049
+ )
1050
+ continue
727
1051
  identity = (worker_id, dispatch_kind, prompt_value)
728
1052
  if identity in seen:
729
1053
  continue
730
1054
  seen.add(identity)
1055
+ metadata_value = str(worker.get("promptMetadataPath") or "").strip()
731
1056
  records.append(
732
1057
  PromptRecord(
733
1058
  worker_id=worker_id,
734
1059
  dispatch_kind=dispatch_kind,
735
1060
  path=_resolve_prompt_record_path(project_root, prompt_value),
1061
+ expected_model=(
1062
+ str(worker.get("modelExecutionValue") or "").strip() or None
1063
+ if uses_v2_identity else None
1064
+ ),
1065
+ metadata_path=(
1066
+ _resolve_prompt_record_path(project_root, metadata_value)
1067
+ if uses_v2_identity and metadata_value else None
1068
+ ),
1069
+ expected_duty_audience=(duty_id if uses_v2_identity else None),
736
1070
  )
737
1071
  )
738
- return records
1072
+ if uses_v2_identity:
1073
+ recorded_ids = {record.worker_id for record in records}
1074
+ missing_ids = sorted(selected_ids - recorded_ids)
1075
+ if missing_ids:
1076
+ errors.append(
1077
+ "no persisted initial prompt records for selected role executions: "
1078
+ + ", ".join(missing_ids)
1079
+ )
1080
+ return records, errors
739
1081
 
740
1082
 
741
1083
  def _markdown_section(text: str, heading: str) -> str:
@@ -861,15 +1203,16 @@ def _validate_initial_analysis_prompts(
861
1203
  team_state = data.get("teamState") or {}
862
1204
  manifest = dict(run_manifest)
863
1205
  manifest["taskType"] = task_type
864
- records = _persisted_prompt_records(
1206
+ records, collection_errors = _persisted_prompt_records(
865
1207
  project_root=project_root,
866
1208
  selected_ids=selected_ids,
867
1209
  team_state=team_state,
1210
+ run_manifest=run_manifest,
868
1211
  )
869
- errors = validate_initial_prompt_records(
1212
+ errors = [*collection_errors, *validate_initial_prompt_records(
870
1213
  manifest=manifest,
871
1214
  records=records,
872
- )
1215
+ )]
873
1216
  if task_type == "improvement-discovery":
874
1217
  errors.extend(
875
1218
  _validate_improvement_primary_assignments(
@@ -1682,35 +2025,6 @@ def _approved_plan_stage_entry(
1682
2025
  }
1683
2026
 
1684
2027
 
1685
- _CONFORMANCE_TESTS_RE = re.compile(
1686
- r"^(?P<script>\S+)\s+\(requires=\[(?P<requires>[^\]]+)\]\)$"
1687
- )
1688
-
1689
-
1690
- def _normalize_conformance_script(script: str) -> str:
1691
- prefix = "<task_root>/"
1692
- return script[len(prefix):] if script.startswith(prefix) else script
1693
-
1694
-
1695
- def _parse_conformance_tests(value: object) -> tuple[str, frozenset[str]] | None:
1696
- """Parse `<script> (requires=[cap,...])` from a stage declaration."""
1697
- if not isinstance(value, str):
1698
- return None
1699
- match = _CONFORMANCE_TESTS_RE.fullmatch(value.strip())
1700
- if match is None:
1701
- return None
1702
- script = _normalize_conformance_script(match.group("script"))
1703
- capabilities = [part.strip() for part in match.group("requires").split(",")]
1704
- if (
1705
- not script
1706
- or any(not capability for capability in capabilities)
1707
- or len(set(capabilities)) != len(capabilities)
1708
- or any(capability not in CAPABILITY_WHITELIST for capability in capabilities)
1709
- ):
1710
- return None
1711
- return script, frozenset(capabilities)
1712
-
1713
-
1714
2028
  def _approved_plan_conformance_manifest(
1715
2029
  approved_plan_path: Path,
1716
2030
  task_root: Path,
@@ -2355,7 +2669,7 @@ def _validate_selfmock(report_path: Path, failures: list[str]) -> None:
2355
2669
  read the flat `self-mock.json`.
2356
2670
 
2357
2671
  Only the implementation template renders the §5.7.3 diff summary
2358
- (`templates/reports/final-report.template.md:589`); a final-verification report
2672
+ (`templates/reports/final-report-v2.template.md:589`); a final-verification report
2359
2673
  quotes the diff as a blockquote instead, so this gate is vacuous there by
2360
2674
  design — self-mock is enforced at the implementation stage, and
2361
2675
  final-verification is a read-only re-verify that adds no test files.
@@ -2665,22 +2979,30 @@ def _report_run_seq(report_path: Path) -> str | None:
2665
2979
 
2666
2980
 
2667
2981
  def validate_worker_results_audit(
2668
- report_path: Path, task_type: str, failures: list[str]
2982
+ report_path: Path,
2983
+ task_type: str,
2984
+ failures: list[str],
2985
+ advisories: list[str] | None = None,
2669
2986
  ) -> None:
2670
2987
  """Enforce the worker audit sidecar contract at Phase 7.
2671
2988
 
2672
2989
  The rules themselves live in `okstra_ctl.worker_audit_ledger` so that
2673
2990
  `okstra worker-audit-check` can apply the identical checks mid-run, while
2674
- the worker session is still alive and can fix its own citations. All this
2675
- wrapper adds is the Phase 7 anchor: the run directory and this run's seq,
2676
- both read off the report path.
2991
+ the worker session is still alive and can fix its own citations. This
2992
+ wrapper adds the Phase 7 anchor — the run directory and this run's seq, both
2993
+ read off the report path — and routes the citation-ledger findings to
2994
+ *advisories*, which do not fail the run. Mid-run the worker can still fix a
2995
+ citation; at Phase 7 the only remedy left is to void a finished run over a
2996
+ spelling, so the finding is reported instead.
2677
2997
  """
2678
- failures.extend(check_worker_results_audit(
2998
+ blocking, ledger = worker_results_audit_findings(
2679
2999
  # `report_path` is `runs/<task-type>/reports/final-report-...md`.
2680
3000
  report_path.parent.parent,
2681
3001
  task_type,
2682
3002
  _report_run_seq(report_path),
2683
- ))
3003
+ )
3004
+ failures.extend(blocking)
3005
+ (advisories if advisories is not None else failures).extend(ledger)
2684
3006
 
2685
3007
 
2686
3008
  def validate_team_state_usage(team_state: dict, failures: list[str]) -> None:
@@ -2717,65 +3039,6 @@ def validate_team_state_usage(team_state: dict, failures: list[str]) -> None:
2717
3039
  )
2718
3040
 
2719
3041
 
2720
- PLANNING_REQUIRED_SECTIONS = (
2721
- "Option Candidates",
2722
- "Trade-off",
2723
- "Recommended Option",
2724
- "Stage Map",
2725
- "Stepwise Execution Order",
2726
- "Dependency",
2727
- "Validation Checklist",
2728
- "Rollback",
2729
- "Requirement Coverage",
2730
- "Plan Body Verification",
2731
- "Cross-Project Dependencies",
2732
- "Decision Drafts",
2733
- )
2734
-
2735
- # §5.7 implementation deliverables — substring scan against report body.
2736
- IMPLEMENTATION_REQUIRED_SECTIONS = (
2737
- "Approved Plan Reference",
2738
- "Commit List",
2739
- "Diff Summary",
2740
- "Out-of-plan Edits",
2741
- "Stage Sidecar Evidence",
2742
- "Validation Evidence",
2743
- "Verifier Results",
2744
- "Rollback Verification",
2745
- "Manual User Test (Draft)",
2746
- "Routing Recommendation",
2747
- )
2748
-
2749
- # §5.8 final-verification deliverables — substring scan against report body.
2750
- FINAL_VERIFICATION_REQUIRED_SECTIONS = (
2751
- "Source Implementation Report",
2752
- "Acceptance Blockers",
2753
- "Residual Risk",
2754
- "Validation Evidence",
2755
- "Read-only Command Log",
2756
- "Conditional Acceptance Conditions",
2757
- "Manual User Test Results",
2758
- "Routing Recommendation",
2759
- "Could-Not-Verify Roll-up",
2760
- )
2761
-
2762
- # Allowed Verdict Token vocabulary, by task-type. `release-handoff` is
2763
- # author-tagged but reads its entry gate from final-verification's report
2764
- # and renders `not-applicable` itself.
2765
- FINAL_VERIFICATION_VERDICT_TOKENS = (
2766
- "accepted",
2767
- "conditional-accept",
2768
- "blocked",
2769
- )
2770
-
2771
- # `## 7. Final Verdict` Verdict Token cell — captures the value between
2772
- # backticks on the `Verdict Token` row. Tolerant to extra column whitespace
2773
- # and to leading bold/italic markers in the label cell.
2774
- _FINAL_VERDICT_TOKEN_RE = re.compile(
2775
- r"^\|[ \t]*\*{0,2}Verdict Token\*{0,2}[ \t]*\|[ \t]*`(?P<value>[^`\n]*)`",
2776
- re.MULTILINE,
2777
- )
2778
-
2779
3042
  # Verdict Card Verdict Token row (top-of-report at-a-glance). Same shape
2780
3043
  # as `_FINAL_VERDICT_TOKEN_RE` but matched against the first occurrence in
2781
3044
  # the Verdict Card block, scoped to the body between `## Verdict Card`
@@ -2786,23 +3049,6 @@ _VERDICT_CARD_BLOCK_RE = re.compile(
2786
3049
  re.DOTALL | re.MULTILINE,
2787
3050
  )
2788
3051
 
2789
- # `## 7. Final Verdict` block scope — used to scope the Verdict Token
2790
- # regex so that we don't accidentally match a Verdict Token row that
2791
- # lives in the Verdict Card or anywhere else.
2792
- _FINAL_VERDICT_BLOCK_RE = re.compile(
2793
- r"^##[ \t]+7\.[ \t]+Final Verdict" + _HEADING_TAIL
2794
- + r"\n(?P<body>.*?)(?=^##[ \t]|\Z)",
2795
- re.DOTALL | re.MULTILINE,
2796
- )
2797
-
2798
- # `## 5.6 Release Handoff Deliverables` and `## 5.6.6 Merge Conflict
2799
- # Probe` are required when task-type == release-handoff. The probe sub-
2800
- # section was retro-added to the template; old runs that predate it ship
2801
- # without it, but new runs must include it.
2802
- _MERGE_CONFLICT_PROBE_HEADING_RE = re.compile(
2803
- r"^###[ \t]+5\.6\.6[ \t]+Merge Conflict Probe\b", re.MULTILINE
2804
- )
2805
-
2806
3052
  PLAN_VERIFY_GATE_VALUES = (
2807
3053
  "passed",
2808
3054
  "passed-with-dissent",
@@ -2810,207 +3056,10 @@ PLAN_VERIFY_GATE_VALUES = (
2810
3056
  "aborted-non-result",
2811
3057
  )
2812
3058
 
2813
- # `Gate result:` line in §5.5.9 of the final report — captures the value
2814
- # token that follows. Tolerates arbitrary markdown formatting between the
2815
- # label and the value (backticks for inline code, double-asterisks for
2816
- # bold, colons, hyphens, whitespace). The captured value is then
2817
- # validated against `PLAN_VERIFY_GATE_VALUES` below so typo'd or unknown
2818
- # values surface as their own failure rather than silently no-matching.
2819
- _GATE_RESULT_RE = re.compile(
2820
- r"Gate result[^A-Za-z\n]+(?P<value>[a-z][a-z\-]+)",
2821
- re.IGNORECASE,
2822
- )
2823
-
2824
- # §5.5.9 Plan Body Verification section body — scopes the gate-result search
2825
- # so an earlier prose mention of "Gate result" elsewhere in the report cannot
2826
- # hijack the authoritative value. Matched from the `Plan Body Verification`
2827
- # heading to the next heading (or end-of-file).
2828
- _PLAN_BODY_VERIFICATION_BLOCK_RE = re.compile(
2829
- r"^#{2,}[ \t][^\n]*\bPlan Body Verification\b[^\n]*\n"
2830
- r"(?P<body>.*?)(?=^#{2,}[ \t]|\Z)",
2831
- re.DOTALL | re.MULTILINE,
2832
- )
2833
-
2834
- # Frontmatter approval flag — `approved: true|false` line inside the
2835
- # leading `---` YAML block. Mirrors `APPROVED_FRONTMATTER_PATTERN` in
2836
- # scripts/okstra_ctl/run.py.
2837
- _APPROVED_FRONTMATTER_RE = re.compile(
2838
- r"^approved:[ \t]+(true|false)[ \t]*$",
2839
- re.IGNORECASE | re.MULTILINE,
2840
- )
2841
3059
  # Tolerate a leading UTF-8 BOM and/or blank lines before the opening `---`
2842
3060
  # so a hand-edited or differently-rendered report does not silently bypass
2843
3061
  # the approved-frontmatter gate (the `.match` would otherwise return None).
2844
3062
  _FRONTMATTER_BLOCK_RE = re.compile(r"\A\ufeff?\s*---\n(.*?)\n---\n", re.DOTALL)
2845
- _REQUIREMENT_COVERAGE_HEADING_RE = re.compile(
2846
- r"^###[ \t]+(?:5\.5\.8[ \t]+)?Requirement Coverage\b",
2847
- re.IGNORECASE | re.MULTILINE,
2848
- )
2849
- _NEXT_THIRD_LEVEL_HEADING_RE = re.compile(r"^###[ \t]+", re.MULTILINE)
2850
-
2851
-
2852
- def _extract_final_verdict_token(content: str) -> str | None:
2853
- """Return the `Verdict Token` cell value from the `## 7. Final Verdict`
2854
- block, or None when the row is absent. Scoped to §7 so the Verdict
2855
- Card row (which has the same shape) does not shadow the authoritative
2856
- value.
2857
- """
2858
- block = _FINAL_VERDICT_BLOCK_RE.search(content)
2859
- if block is None:
2860
- return None
2861
- match = _FINAL_VERDICT_TOKEN_RE.search(block.group("body"))
2862
- if match is None:
2863
- return None
2864
- return match.group("value")
2865
-
2866
-
2867
- # 렌더러는 ID 정의 셀(`<a id="r-001"></a>R-001`)에도 스크롤 앵커를 넣는다.
2868
- # 셀 정규화 때 그 빈 앵커를 벗겨야 ID 컬럼이 bare 토큰으로 읽힌다
2869
- # (clarification_items._CELL_ANCHOR_RE 와 동형).
2870
- _CELL_ANCHOR_RE = re.compile(r'<a id="[^"]*"></a>')
2871
-
2872
-
2873
- def _split_markdown_row(line: str) -> list[str]:
2874
- return [
2875
- _CELL_ANCHOR_RE.sub("", cell).strip().strip("`").strip()
2876
- for cell in _split_pipe_row(line)
2877
- ]
2878
-
2879
-
2880
- def _append_requirement_coverage_failures(
2881
- content: str,
2882
- gate_value: str,
2883
- failures: list[str],
2884
- ) -> None:
2885
- """Validate implementation-planning §5.5.8 requirement coverage.
2886
-
2887
- The table is intentionally lightweight: it cannot prove semantic truth, but
2888
- it makes requirement-to-plan mapping explicit and blocks publishable plans
2889
- that admit uncovered requirements.
2890
- """
2891
- heading = _REQUIREMENT_COVERAGE_HEADING_RE.search(content)
2892
- if heading is None:
2893
- failures.append(
2894
- "implementation-planning report is missing `Requirement Coverage` "
2895
- "section — every task-brief requirement must map to option/stage/step."
2896
- )
2897
- return
2898
-
2899
- rest = content[heading.end():]
2900
- next_heading = _NEXT_THIRD_LEVEL_HEADING_RE.search(rest)
2901
- section = rest[: next_heading.start()] if next_heading else rest
2902
- lines = section.splitlines()
2903
-
2904
- header_idx = -1
2905
- headers: list[str] = []
2906
- for idx, line in enumerate(lines):
2907
- if not line.lstrip().startswith("|"):
2908
- continue
2909
- cells = [c.lower() for c in _split_markdown_row(line)]
2910
- if "id" in cells and "requirement" in cells and "status" in cells:
2911
- header_idx = idx
2912
- headers = cells
2913
- break
2914
- if header_idx < 0:
2915
- failures.append(
2916
- "implementation-planning Requirement Coverage section has no table "
2917
- "with `ID`, `Requirement`, and `Status` columns."
2918
- )
2919
- return
2920
-
2921
- id_col = headers.index("id")
2922
- status_col = headers.index("status")
2923
- rows: list[tuple[str, str]] = []
2924
- body_started = False
2925
- for line in lines[header_idx + 1:]:
2926
- if not line.lstrip().startswith("|"):
2927
- if body_started:
2928
- break
2929
- continue
2930
- if _is_markdown_separator(line):
2931
- body_started = True
2932
- continue
2933
- if not body_started:
2934
- continue
2935
- cells = _split_markdown_row(line)
2936
- if max(id_col, status_col) >= len(cells):
2937
- failures.append(
2938
- "implementation-planning Requirement Coverage table has a "
2939
- f"malformed row: `{line.strip()}`"
2940
- )
2941
- continue
2942
- rows.append((cells[id_col], cells[status_col].lower()))
2943
-
2944
- if not rows:
2945
- failures.append(
2946
- "implementation-planning Requirement Coverage table has no data rows."
2947
- )
2948
- return
2949
-
2950
- for row_id, status in rows:
2951
- # `status` is already lower-cased at parse time, so the blocker form
2952
- # arrives as `blocked c-001`; match lower-case here even though the
2953
- # canonical authored form (and the remedy message) is `blocked C-NNN`.
2954
- if not re.fullmatch(
2955
- r"covered|gap|blocked c-\d{3,}|"
2956
- r"documented-deviation — refs: (?:c-\d{3,}|d-\d{4,})"
2957
- r"(?:, (?:c-\d{3,}|d-\d{4,}))*; approval: "
2958
- r"(?:accepted|blocked c-\d{3,})",
2959
- status,
2960
- ):
2961
- failures.append(
2962
- "implementation-planning Requirement Coverage row "
2963
- f"`{row_id}` has invalid Status `{status}`; expected "
2964
- "`covered`, `gap`, `blocked C-NNN`, or a rendered "
2965
- "`documented-deviation` disposition."
2966
- )
2967
-
2968
- if gate_value in ("passed", "passed-with-dissent"):
2969
- uncovered = [
2970
- f"{row_id} ({status})"
2971
- for row_id, status in rows
2972
- if status != "covered"
2973
- and not status.endswith("; approval: accepted")
2974
- ]
2975
- if uncovered:
2976
- failures.append(
2977
- "implementation-planning Gate result is publishable but "
2978
- "Requirement Coverage has uncovered row(s): "
2979
- + ", ".join(uncovered)
2980
- )
2981
- def _extract_verdict_card_token(content: str) -> str | None:
2982
- """Return the `Verdict Token` cell from the Verdict Card block."""
2983
- block = _VERDICT_CARD_BLOCK_RE.search(content)
2984
- if block is None:
2985
- return None
2986
- match = _FINAL_VERDICT_TOKEN_RE.search(block.group("body"))
2987
- if match is None:
2988
- return None
2989
- return match.group("value")
2990
-
2991
-
2992
- def _validate_verdict_card_consistency(content: str, failures: list[str]) -> None:
2993
- """Verdict Card is a non-authoritative index of §7. If both blocks
2994
- carry a Verdict Token row, the values MUST byte-match (modulo case
2995
- and surrounding whitespace) — divergence is a contract violation per
2996
- `report-writer` SKILL.md "Authoring Contract".
2997
- """
2998
- card_value = _extract_verdict_card_token(content)
2999
- final_value = _extract_final_verdict_token(content)
3000
- if card_value is None or final_value is None:
3001
- # Missing-Card and missing-§7 are surfaced by other checks; this
3002
- # function only enforces the consistency contract between the two.
3003
- return
3004
- if card_value.strip().lower() != final_value.strip().lower():
3005
- failures.append(
3006
- "Verdict Card `Verdict Token` value "
3007
- f"`{card_value}` does not match `## 7. Final Verdict` value "
3008
- f"`{final_value}` — the Card is a non-authoritative index and "
3009
- "MUST byte-match §7. Either fix the Card or update §7; do not "
3010
- "ship divergent values."
3011
- )
3012
-
3013
-
3014
3063
  def _validate_verdict_card_fields(data: dict, failures: list[str]) -> None:
3015
3064
  """`verdictCard.direction` must byte-match its authoritative home in §7.
3016
3065
 
@@ -3337,9 +3386,8 @@ def validate_final_report_data(
3337
3386
  The data.json is the source-of-truth that the renderer reads to
3338
3387
  produce the markdown. If schema validation passes here, the rendered
3339
3388
  markdown is guaranteed to contain every section / row the contract
3340
- requires (the template loops over the data). The downstream
3341
- substring checks in ``validate_phase_boundary`` are kept as a safety
3342
- net but are expected to be redundant.
3389
+ requires (the template loops over the data), so the schema is the
3390
+ only place the deliverable contract is enforced.
3343
3391
 
3344
3392
  Missing data.json is reported as a single failure rather than a
3345
3393
  cascade of substring failures — that points the writer at the right
@@ -3381,6 +3429,13 @@ def validate_final_report_data(
3381
3429
  return data
3382
3430
 
3383
3431
  manifest = run_manifest or {}
3432
+ if data.get("executionIdentityVersion") == 2 or data.get("executionRoles"):
3433
+ try:
3434
+ from okstra_ctl.report_contract import assert_execution_roles_match_manifest
3435
+
3436
+ assert_execution_roles_match_manifest(data, manifest)
3437
+ except ValueError as exc:
3438
+ failures.append(str(exc))
3384
3439
  _validate_approval_context(data, manifest, failures, report_path)
3385
3440
  _validate_activity_contract_plan_limits(data, manifest, failures)
3386
3441
 
@@ -3674,7 +3729,17 @@ _ADVISORY_ONLY_KINDS = {"d"}
3674
3729
  # Stop reasons that justify promoting a still-broken planner-fixable item to the
3675
3730
  # user: the self-fix budget ran out, or a round produced no net resolution so
3676
3731
  # further rounds would repeat themselves.
3677
- _SELF_FIX_EXHAUSTED_REASONS = frozenset({"max-rounds-reached", "no-progress"})
3732
+ #
3733
+ # `cause-group-recurrence` is the legacy spelling of that same exhaustion
3734
+ # (plan-body-verification.md "Loop termination"). A pre-activity-contract report
3735
+ # carrying it is a report whose loop stopped because the cause kept recurring —
3736
+ # refusing it here left such a run with no exit at all: the loop may not run
3737
+ # again, and the surviving item may not be promoted either.
3738
+ # `_validate_activity_contract_plan_limits` still forbids a NEW v1 run from
3739
+ # emitting it, so this admits the legacy shape without reopening it.
3740
+ _SELF_FIX_EXHAUSTED_REASONS = frozenset(
3741
+ {"max-rounds-reached", "no-progress", "cause-group-recurrence"}
3742
+ )
3678
3743
 
3679
3744
 
3680
3745
  def _is_variation_point_item(item: dict) -> bool:
@@ -5059,10 +5124,11 @@ def _items_resolved_in_round(plan_items: object, round_number: int) -> set[str]:
5059
5124
  def _detect_self_fix_recurrence(pbv: dict) -> list[str]:
5060
5125
  """Rounds that re-target ground the previous round already worked, unresolved.
5061
5126
 
5062
- `no-progress` counts resolutions, which a round can claim while its
5063
- correction introduces the next round's defect — the observed shape was two
5064
- rounds spent on one identical seven-item set. Repeating the previous
5065
- round's *unresolved* remainder is the earlier signal.
5127
+ `no-progress` is judged at the round's end from what it resolved. Repeating
5128
+ the previous round's *unresolved* remainder is the same conclusion reached
5129
+ one dispatch earlier — the observed shape was two rounds spent on one
5130
+ identical seven-item set. This names that shape so the loop can exit on it
5131
+ rather than paying for the round that proves it.
5066
5132
 
5067
5133
  Advisory only. Narrowing onto what the last round genuinely left open is
5068
5134
  legitimate progress, and the rule separating that from re-digging the same
@@ -5091,8 +5157,8 @@ def _detect_self_fix_recurrence(pbv: dict) -> list[str]:
5091
5157
  f"self-fix round {round_number} re-targets only items round "
5092
5158
  f"{round_number - 1} left unresolved ({', '.join(sorted(current))}) "
5093
5159
  "— the previous round's correction did not move this cause. "
5094
- "Consider exiting with `cause-group-recurrence` instead of "
5095
- "spending the remaining budget on the same ground."
5160
+ "Consider exiting with `no-progress` instead of spending the "
5161
+ "remaining budget on the same ground."
5096
5162
  )
5097
5163
  return warnings
5098
5164
 
@@ -7394,7 +7460,7 @@ def _validate_unverified_critic_gaps_recorded(data: dict, failures: list[str]) -
7394
7460
 
7395
7461
 
7396
7462
  # Allowed `fixability` values, mirroring the schema enum
7397
- # (schemas/final-report-v1.0.schema.json planItems[].verdicts[].fixability).
7463
+ # (schemas/final-report-v2.0.schema.json planItems[].verdicts[].fixability).
7398
7464
  _FIXABILITY_VALUES = frozenset({"planner-fixable", "needs-user-input"})
7399
7465
 
7400
7466
 
@@ -8015,14 +8081,13 @@ def _load_stage_validator():
8015
8081
  def _append_stage_data_failures(data: Mapping[str, Any], failures: list[str]) -> None:
8016
8082
  """Run the stage relationship checks that schema v2 cannot express.
8017
8083
 
8018
- `_append_stage_structure_failures` scans rendered Markdown and sits after
8019
- the v2 early return in `validate_phase_boundary`, so for a v2 report the
8020
- depends-on DAG, parallel-stage file safety, RED→GREEN ordering, and the
8021
- TDD-exemption vocabulary had nothing enforcing them. The same validator
8022
- owns both modes so the rule vocabulary stays defined once.
8084
+ The depends-on DAG, parallel-stage file safety, RED→GREEN ordering, and
8085
+ the TDD-exemption vocabulary are relationships between stages, which a
8086
+ JSON Schema cannot state. They are enforced here, against the data.json,
8087
+ by the same validator that owns the rule vocabulary.
8023
8088
  """
8024
- if (data or {}).get("schemaVersion") != "2.0":
8025
- return # v1 reports are covered by the Markdown scan.
8089
+ if (data or {}).get("schemaVersion") != CURRENT_REPORT_SCHEMA_VERSION:
8090
+ return # Schema validation already rejected an unknown version.
8026
8091
  planning = (data or {}).get("implementationPlanning")
8027
8092
  if not isinstance(planning, Mapping):
8028
8093
  return # Schema validation already reported the missing block.
@@ -8037,157 +8102,6 @@ def _append_stage_data_failures(data: Mapping[str, Any], failures: list[str]) ->
8037
8102
  )
8038
8103
 
8039
8104
 
8040
- def _append_stage_structure_failures(content: str, failures: list[str]) -> None:
8041
- """Enforce the Stage Map structural contract at the implementation-planning
8042
- boundary. Without this, a plan missing `## 5.5 Stage Map` passes the
8043
- planning gate, gets approved, and only fails later at the `implementation`
8044
- entry (validators/validate-implementation-plan-stages.py via
8045
- prepare_task_bundle). Running the same validator here moves the failure to
8046
- produce-time."""
8047
- mod = _load_stage_validator()
8048
- if mod is None: # pragma: no cover — repo/runtime always ship the file
8049
- failures.append(f"cannot load Stage Map validator at {_STAGE_VALIDATOR_PATH}")
8050
- return
8051
- for e in mod.collect_validation_errors(content):
8052
- failures.append(
8053
- f"implementation-planning Stage Map structure invalid "
8054
- f"[{e.code} stage={e.stage}]: {e.message}"
8055
- )
8056
-
8057
-
8058
- def validate_phase_boundary(
8059
- task_type: str,
8060
- report_path: Path,
8061
- failures: list[str],
8062
- *,
8063
- report_data: Mapping[str, Any] | None = None,
8064
- ) -> None:
8065
- """Phase-specific contract checks.
8066
-
8067
- For `implementation-planning` runs, the final report must contain the
8068
- required deliverable sections; absence indicates a planning run that
8069
- skipped its core outputs (or an implementation run that ran under the
8070
- wrong task type).
8071
-
8072
- Additionally enforces the Plan Body Verification gate (§5.5.9):
8073
- - gate ∈ {passed, passed-with-dissent} → top-level Approval checkbox
8074
- MUST be present.
8075
- - gate ∈ {blocked-by-disagreement, aborted-non-result} → checkbox
8076
- MUST be absent (lead converted findings into Clarification rows
8077
- instead of opening the gate).
8078
- """
8079
- if (report_data or {}).get("schemaVersion") == "2.0":
8080
- return
8081
- if not report_path.exists():
8082
- return
8083
- content = report_path.read_text()
8084
-
8085
- # Verdict Card vs §7. Final Verdict Verdict Token consistency. The Card
8086
- # is a non-authoritative index; divergence is a contract violation.
8087
- _validate_verdict_card_consistency(content, failures)
8088
-
8089
- if task_type == "implementation":
8090
- for needle in IMPLEMENTATION_REQUIRED_SECTIONS:
8091
- if needle not in content:
8092
- failures.append(
8093
- "implementation report is missing required §5.7 "
8094
- f"deliverable section: `{needle}`"
8095
- )
8096
-
8097
- if task_type == "final-verification":
8098
- for needle in FINAL_VERIFICATION_REQUIRED_SECTIONS:
8099
- if needle not in content:
8100
- failures.append(
8101
- "final-verification report is missing required §5.8 "
8102
- f"deliverable section: `{needle}`"
8103
- )
8104
- token_value = _extract_final_verdict_token(content)
8105
- if token_value is None:
8106
- failures.append(
8107
- "final-verification report `## 7. Final Verdict` table is "
8108
- "missing the `Verdict Token` row — required by the release-"
8109
- "handoff entry gate."
8110
- )
8111
- elif token_value.strip().lower() not in FINAL_VERIFICATION_VERDICT_TOKENS:
8112
- failures.append(
8113
- "final-verification report `Verdict Token` value "
8114
- f"`{token_value}` is not one of "
8115
- f"{', '.join(FINAL_VERIFICATION_VERDICT_TOKENS)}."
8116
- )
8117
-
8118
- if task_type == "release-handoff":
8119
- if _MERGE_CONFLICT_PROBE_HEADING_RE.search(content) is None:
8120
- failures.append(
8121
- "release-handoff report is missing `### 5.6.6 Merge Conflict "
8122
- "Probe` sub-section — required by the release-handoff profile "
8123
- "(self-review 6, merge-conflict probe audit). When the run is "
8124
- "`local checkout` / `skip`, record the single line `- Not run "
8125
- "(user picked local checkout or skip).` under the heading."
8126
- )
8127
-
8128
- if task_type != "implementation-planning":
8129
- return
8130
- for needle in PLANNING_REQUIRED_SECTIONS:
8131
- if needle not in content:
8132
- failures.append(
8133
- "implementation-planning report is missing required section: "
8134
- f"`{needle}`"
8135
- )
8136
-
8137
- # Scope the gate-result search to the §5.5.9 Plan Body Verification block
8138
- # so an earlier prose mention of "Gate result" cannot hijack the value.
8139
- pbv_block = _PLAN_BODY_VERIFICATION_BLOCK_RE.search(content)
8140
- gate_match = (
8141
- _GATE_RESULT_RE.search(pbv_block.group("body")) if pbv_block else None
8142
- )
8143
- if gate_match is None:
8144
- # The `Plan Body Verification` heading check above already covers
8145
- # the wholly-missing case; a heading present without a `Gate result`
8146
- # line is its own contract violation.
8147
- if "Plan Body Verification" in content:
8148
- failures.append(
8149
- "implementation-planning report has `Plan Body Verification` "
8150
- "section but no `Gate result:` line — required by §5.5.9."
8151
- )
8152
- return
8153
- gate_value = gate_match.group("value").strip().lower()
8154
- if gate_value not in PLAN_VERIFY_GATE_VALUES:
8155
- failures.append(
8156
- "implementation-planning report `Gate result` value "
8157
- f"`{gate_value}` is not one of "
8158
- f"{', '.join(PLAN_VERIFY_GATE_VALUES)}."
8159
- )
8160
- return
8161
- fm_block = _FRONTMATTER_BLOCK_RE.match(content)
8162
- fm_match = (
8163
- _APPROVED_FRONTMATTER_RE.search(fm_block.group(1)) if fm_block else None
8164
- )
8165
- if gate_value in ("passed", "passed-with-dissent") and fm_match is None:
8166
- failures.append(
8167
- "implementation-planning report Gate result is "
8168
- f"`{gate_value}` but the frontmatter `approved:` field is missing — "
8169
- "render `approved: false` so the user (or `--approve`) can flip it."
8170
- )
8171
- if (
8172
- gate_value in ("blocked-by-disagreement", "aborted-non-result")
8173
- and fm_match is not None
8174
- and fm_match.group(1).lower() == "true"
8175
- ):
8176
- failures.append(
8177
- "implementation-planning report Gate result is "
8178
- f"`{gate_value}` but the frontmatter has `approved: true` — gate "
8179
- "must NOT publish a pre-approved plan when verification did not pass."
8180
- )
8181
-
8182
- _append_requirement_coverage_failures(content, gate_value, failures)
8183
-
8184
- # Only a publishable plan (gate passed) can be flipped to `approved: true`
8185
- # and reach the `implementation` entry, so the Stage Map structure is
8186
- # enforced only here — a blocked/aborted plan may legitimately be incomplete.
8187
- if gate_value in ("passed", "passed-with-dissent"):
8188
- _append_stage_structure_failures(content, failures)
8189
-
8190
-
8191
8105
  def _brief_path_from_manifest(task_manifest: dict, project_root: Path) -> Path:
8192
8106
  """This run's brief, or a non-existent path when the manifest names none.
8193
8107
 
@@ -8351,11 +8265,16 @@ def _validate_session_conformance(
8351
8265
  task_type: str,
8352
8266
  claude_projects_dir: str | None,
8353
8267
  failures: list[str],
8268
+ advisories: list[str] | None = None,
8354
8269
  ) -> None:
8355
8270
  """prompts/lead/okstra-lead-contract.md의 PROGRESS / activity / heartbeat /
8356
8271
  implementation entry guard 사후 검사를 위임하고 실패를
8357
8272
  ``session-conformance: `` 접두로 folding 한다. 설계:
8358
8273
  docs/superpowers/specs/2026-06-10-blocking-contract-posthoc-conformance-design.md
8274
+
8275
+ PROGRESS 서술 라인 누락은 ``result.advisories`` 로 돌아오며 run 을
8276
+ 실패시키지 않는다 — 그 라인을 쓸 세션은 이 검사가 도는 시점에 이미 끝나 있어
8277
+ 남은 구제책이 phase 폐기뿐이다.
8359
8278
  """
8360
8279
  _validators_dir = Path(__file__).resolve().parent
8361
8280
  if str(_validators_dir) not in sys.path:
@@ -8377,6 +8296,9 @@ def _validate_session_conformance(
8377
8296
  claude_projects_dir=Path(claude_projects_dir) if claude_projects_dir else None,
8378
8297
  )
8379
8298
  failures.extend(f"session-conformance: {err}" for err in result.errors)
8299
+ (advisories if advisories is not None else failures).extend(
8300
+ f"session-conformance: {note}" for note in result.advisories
8301
+ )
8380
8302
 
8381
8303
 
8382
8304
  def _validate_forbidden_actions(
@@ -9130,8 +9052,21 @@ def main() -> int:
9130
9052
  print(f"validate-run: Phase 7 auto-recovery skipped — {msg}", file=sys.stderr)
9131
9053
 
9132
9054
  failures: list[str] = []
9055
+ # Findings that are reported but do not fail the run.
9056
+ #
9057
+ # The line is whether a remedy still exists at Phase 7. A defect in the
9058
+ # report is fixable — re-author it and re-render. A defect in what a
9059
+ # *finished session* wrote down is not: the worker that would restate its
9060
+ # citation and the lead pane that would emit its PROGRESS line have both
9061
+ # ended, so the only remaining move is to void the phase. That blocks the
9062
+ # next phase without establishing that any conclusion in the report is
9063
+ # wrong. Those findings are surfaced and carried in the run manifest
9064
+ # instead — currently the worker citation ledger and the PROGRESS narration
9065
+ # lines.
9066
+ advisories: list[str] = []
9133
9067
  if autofix_state == "accuracy-failed":
9134
9068
  failures.extend(autofix_messages)
9069
+ _validate_execution_identity_v2(run_manifest, failures)
9135
9070
  contract = extract_contract(run_manifest, task_manifest, failures)
9136
9071
  concurrent_run_authorized = bool(
9137
9072
  (run_manifest.get("concurrentRun") or {}).get("detected")
@@ -9190,19 +9125,13 @@ def main() -> int:
9190
9125
  )
9191
9126
  validate_team_state_usage(team_state, failures)
9192
9127
 
9193
- validate_phase_boundary(
9194
- task_type,
9195
- report_path,
9196
- failures,
9197
- report_data=validation_data,
9198
- )
9199
9128
  _validate_phase_boundary_error_log(
9200
9129
  report_path.parent.parent,
9201
9130
  task_type,
9202
9131
  failures,
9203
9132
  )
9204
9133
  if task_type:
9205
- validate_worker_results_audit(report_path, task_type, failures)
9134
+ validate_worker_results_audit(report_path, task_type, failures, advisories)
9206
9135
  _validate_session_conformance(
9207
9136
  team_state,
9208
9137
  team_state_path,
@@ -9212,6 +9141,7 @@ def main() -> int:
9212
9141
  task_type,
9213
9142
  args.claude_projects_dir,
9214
9143
  failures,
9144
+ advisories,
9215
9145
  )
9216
9146
  _validate_forbidden_actions(
9217
9147
  team_state,
@@ -9309,6 +9239,7 @@ def main() -> int:
9309
9239
  validation_status,
9310
9240
  failures,
9311
9241
  report_data=validation_data,
9242
+ advisories=advisories,
9312
9243
  )
9313
9244
 
9314
9245
  write_json(team_state_path, team_state)
@@ -9335,10 +9266,14 @@ def main() -> int:
9335
9266
  + "\n"
9336
9267
  )
9337
9268
 
9269
+ for note in advisories:
9270
+ print(f"validate-run: advisory — {note}", file=sys.stderr)
9271
+
9338
9272
  result = {
9339
9273
  "validationStatus": validation_status,
9340
9274
  "finalRunStatus": run_manifest.get("status"),
9341
9275
  "failures": failures,
9276
+ "advisories": advisories,
9342
9277
  "teamStatePath": str(team_state_path),
9343
9278
  "reportPath": str(report_path),
9344
9279
  }