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
@@ -73,8 +73,8 @@ from okstra_ctl.incremental_scope import ( # noqa: E402
73
73
  coverage_row_blocked_on,
74
74
  stages_for_clarification,
75
75
  )
76
+ from okstra_ctl import next_phase # noqa: E402
76
77
  from okstra_ctl.workflow import ( # noqa: E402
77
- DEFAULT_NEXT_PHASE,
78
78
  ERROR_ANALYSIS_ROUTING_DIRECTIONS,
79
79
  PHASE_SEQUENCE,
80
80
  REQUIREMENTS_DISCOVERY_ROUTING_TARGETS,
@@ -122,15 +122,21 @@ from okstra_ctl.worker_prompt_contract import ( # noqa: E402
122
122
  PromptRecord,
123
123
  validate_initial_prompt_records,
124
124
  )
125
+ from okstra_ctl.domain.role import RoleCatalogError, role_for_duty # noqa: E402
125
126
  from okstra_ctl.agent_invocation import ( # noqa: E402
126
127
  AgentInvocationError,
127
128
  agent_model_assignment_from_payload,
129
+ invocation_execution_identity_from_manifest,
128
130
  verify_agent_invocation,
129
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
+ )
130
136
  from okstra_ctl.lead_events import LeadEventParseError, read_lead_events # noqa: E402
131
137
  from okstra_ctl.worker_audit_ledger import ( # noqa: E402
132
138
  READING_CONFIRMATION_HEADING_RE,
133
- check_worker_results_audit,
139
+ worker_results_audit_findings,
134
140
  )
135
141
  from validate_analysis_report import validate_analysis_report # noqa: E402
136
142
  from okstra_ctl.convergence_engine import ( # noqa: E402
@@ -151,6 +157,30 @@ _AGENT_DISPATCH_DIGEST_KEYS = (
151
157
  "instructionDigest",
152
158
  "promptDigest",
153
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}")
154
184
 
155
185
 
156
186
  def _validate_agent_dispatch_contract(
@@ -169,6 +199,28 @@ def _validate_agent_dispatch_contract(
169
199
  if not isinstance(assignments, Mapping):
170
200
  failures.append("agent invocation metadata is missing: invocationAssignments")
171
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()
172
224
  worker_dispatches = [
173
225
  row for row in (team_state.get("workerDispatches") or [])
174
226
  if isinstance(row, Mapping) and row.get("invocationId")
@@ -179,13 +231,15 @@ def _validate_agent_dispatch_contract(
179
231
  ]
180
232
  dispatches = [*worker_dispatches, *agent_dispatches]
181
233
  ids: dict[str, Mapping[str, Any]] = {}
234
+ seen_dispatch_ids: set[str] = set()
182
235
  for row in dispatches:
183
236
  dispatch_id = str(row.get("dispatchId") or "").strip()
184
237
  missing = [
185
238
  key for key in (
186
- "dispatchId", "workerId", "audience", "invocationId",
239
+ "dispatchId", "audience", "invocationId",
187
240
  "assignmentRef", "promptMetadataPath", "modelExecutionValue",
188
241
  "enforcementMode", *_AGENT_DISPATCH_DIGEST_KEYS,
242
+ *(_V2_AGENT_DISPATCH_IDENTITY_KEYS if uses_v2_identity else ("workerId",)),
189
243
  )
190
244
  if not str(row.get(key) or "").strip()
191
245
  ]
@@ -195,10 +249,43 @@ def _validate_agent_dispatch_contract(
195
249
  + ", ".join(missing)
196
250
  )
197
251
  continue
198
- 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:
199
286
  failures.append(f"agent dispatch ID is duplicated: {dispatch_id}")
200
287
  continue
201
- ids[dispatch_id] = row
288
+ seen_dispatch_ids.add(dispatch_id)
202
289
  assignment_ref = str(row["assignmentRef"])
203
290
  try:
204
291
  assignment = agent_model_assignment_from_payload(
@@ -207,6 +294,118 @@ def _validate_agent_dispatch_contract(
207
294
  except AgentInvocationError as exc:
208
295
  failures.append(f"agent dispatch {dispatch_id}: {exc}")
209
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
210
409
  metadata_path = _resolve_prompt_record_path(
211
410
  project_root, str(row["promptMetadataPath"])
212
411
  )
@@ -216,9 +415,20 @@ def _validate_agent_dispatch_contract(
216
415
  expected_run_manifest_path=run_manifest_path,
217
416
  expected_assignment=assignment,
218
417
  expected_invocation_id=str(row["invocationId"]),
219
- expected_worker_id=str(row["workerId"]),
418
+ expected_worker_id=(
419
+ None if uses_v2_identity else str(row["workerId"])
420
+ ),
220
421
  expected_assignment_ref=assignment_ref,
221
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
+ ),
222
432
  )
223
433
  failures.extend(
224
434
  f"agent dispatch {dispatch_id}: {error}" for error in errors
@@ -258,10 +468,23 @@ def _validate_agent_dispatch_contract(
258
468
  else "host-native prompt delivery cannot be marked verified"
259
469
  )
260
470
 
261
- lead_ids = {
262
- dispatch_id for dispatch_id, row in ids.items()
263
- if row.get("audience") == "lead" and row.get("workerId") == "lead"
264
- }
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
+ }
265
488
  if not lead_ids:
266
489
  failures.append("accepted lead result has no lead dispatch record")
267
490
 
@@ -277,6 +500,22 @@ def _validate_agent_dispatch_contract(
277
500
  if dispatch_id not in ids or not result_path:
278
501
  failures.append("agent result link has no matching dispatch record")
279
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
280
519
  dispatch_link_counts[dispatch_id] = dispatch_link_counts.get(dispatch_id, 0) + 1
281
520
  previous = paths.setdefault(result_path, dispatch_id)
282
521
  if previous != dispatch_id:
@@ -296,7 +535,11 @@ def _validate_agent_dispatch_contract(
296
535
  worker_result = str(worker.get("resultPath") or "").strip()
297
536
  worker_dispatch_ids = {
298
537
  dispatch_id for dispatch_id, row in ids.items()
299
- 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
+ )
300
543
  }
301
544
  if worker_id and not worker_dispatch_ids:
302
545
  failures.append(
@@ -384,68 +627,6 @@ def write_json(path: Path, payload: dict) -> None:
384
627
  path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n")
385
628
 
386
629
 
387
- def default_next_phase(task_type: str) -> str:
388
- return DEFAULT_NEXT_PHASE.get(task_type, "unknown")
389
-
390
-
391
- def advance_next_phase(
392
- current_phase: str,
393
- phase_sequence: list,
394
- ) -> str:
395
- """Compute the next recommended phase after `current_phase` completes.
396
-
397
- Walk `phase_sequence` and return the entry following `current_phase`.
398
- Falls back to the static `default_next_phase` mapping when the current
399
- phase is the terminal entry or is absent from the sequence (e.g. custom
400
- task types that aren't part of the standard 5-phase lifecycle).
401
- """
402
- if isinstance(phase_sequence, list) and current_phase in phase_sequence:
403
- idx = phase_sequence.index(current_phase)
404
- if idx + 1 < len(phase_sequence):
405
- return phase_sequence[idx + 1]
406
- return "done-or-follow-up"
407
- return default_next_phase(current_phase)
408
-
409
-
410
- def _error_analysis_next_phase(data: Mapping[str, Any]) -> str | None:
411
- if not isinstance(data, Mapping):
412
- return None
413
- header = data.get("header")
414
- if not isinstance(header, Mapping) or header.get("taskType") != "error-analysis":
415
- return None
416
- error_analysis = data.get("errorAnalysis")
417
- if not isinstance(error_analysis, Mapping):
418
- return None
419
- routing = error_analysis.get("routing")
420
- if not isinstance(routing, Mapping):
421
- return None
422
- target = routing.get("nextTaskType")
423
- if target in ERROR_ANALYSIS_ROUTING_DIRECTIONS:
424
- return str(target)
425
- return None
426
-
427
-
428
- def _requirements_discovery_next_phase(data: Mapping[str, Any]) -> str | None:
429
- if not isinstance(data, Mapping):
430
- return None
431
- header = data.get("header")
432
- if (
433
- not isinstance(header, Mapping)
434
- or header.get("taskType") != "requirements-discovery"
435
- ):
436
- return None
437
- requirements = data.get("requirementsDiscovery")
438
- if not isinstance(requirements, Mapping):
439
- return None
440
- routing = requirements.get("routing")
441
- if not isinstance(routing, Mapping):
442
- return None
443
- target = routing.get("nextTaskType")
444
- if target in REQUIREMENTS_DISCOVERY_ROUTING_TARGETS:
445
- return str(target)
446
- return None
447
-
448
-
449
630
  def update_workflow_metadata(
450
631
  run_manifest: dict,
451
632
  task_manifest: dict,
@@ -472,36 +653,49 @@ def update_workflow_metadata(
472
653
  for phase in phase_sequence:
473
654
  phase_states.setdefault(phase, "not-started")
474
655
 
656
+ # 포인터는 리드가 리포트에 저작하고, 이 검증기는 그것을 계산하지 않는다.
657
+ # 여기서 하는 일은 리포트의 Phase 라우팅 투영과 대조하는 것뿐이다.
658
+ authored = next_phase.promote(workflow.get("nextRecommendedPhase"))
659
+ workflow.pop("nextRecommendedPhaseCorrection", None)
660
+
475
661
  if validation_status == "passed":
476
662
  current_phase_state = "completed"
477
663
  if current_phase:
478
664
  phase_states[current_phase] = current_phase_state
479
665
  last_completed_phase = current_phase or workflow.get("lastCompletedPhase", "")
480
- # Validation just passed → actively advance to the next phase in
481
- # the sequence rather than preserving a stale value that may equal
482
- # current_phase (which would cause the lifecycle pointer to stall).
483
- if current_phase == "requirements-discovery":
484
- report_next_phase = _requirements_discovery_next_phase(report_data or {})
485
- elif current_phase == "error-analysis":
486
- report_next_phase = _error_analysis_next_phase(report_data or {})
666
+ # 어긋나면 투영값으로 정정하되 실행은 죽이지 않는다 — 분석이 통과한 run 을
667
+ # 장부 필드 하나 때문에 버리지 않는다 (ADR-0004).
668
+ projected = next_phase.project(report_data or {})
669
+ if (
670
+ authored["phase"] == projected["phase"]
671
+ and authored["status"] == projected["status"]
672
+ ):
673
+ next_recommended_phase = authored
487
674
  else:
488
- report_next_phase = None
489
- next_recommended_phase = report_next_phase or advance_next_phase(
490
- current_phase, phase_sequence
491
- )
675
+ # 리드의 근거는 리드가 고른 phase 를 설명하는 문장이다. phase 를 정정한
676
+ # 뒤에도 들고 오면 포인터가 자기 목적지를 설명하지 않는 문장을 달게 된다.
677
+ # 원문은 아래 correction["authored"] 에 그대로 보존된다.
678
+ next_recommended_phase = next_phase.make(
679
+ phase=projected["phase"],
680
+ status=projected["status"],
681
+ rationale=(
682
+ "리포트 라우팅에서 투영됨. 리드가 쓴 값과 근거는 "
683
+ "nextRecommendedPhaseCorrection.authored 에 있다."
684
+ ),
685
+ )
686
+ # applied 는 사본이다. 살아 있는 포인터와 같은 dict 를 가리키면
687
+ # 나중에 포인터를 제자리 변형하는 호출부가 감사 기록까지 바꾼다.
688
+ workflow["nextRecommendedPhaseCorrection"] = {
689
+ "authored": authored,
690
+ "applied": dict(next_recommended_phase),
691
+ }
492
692
  else:
493
693
  current_phase_state = "blocked"
494
694
  if current_phase:
495
695
  phase_states[current_phase] = current_phase_state
496
696
  last_completed_phase = workflow.get("lastCompletedPhase", "")
497
- next_recommended_phase = current_phase or workflow.get(
498
- "nextRecommendedPhase", ""
499
- )
500
-
501
- routing_status = workflow.get("routingStatus")
502
- if not isinstance(routing_status, str) or not routing_status.strip():
503
- routing_status = (
504
- "pending" if current_phase == "requirements-discovery" else "not-applicable"
697
+ next_recommended_phase = next_phase.make(
698
+ status=next_phase.STATUS_BLOCKED, rationale=authored["rationale"]
505
699
  )
506
700
 
507
701
  awaiting_approval = workflow.get("awaitingApproval")
@@ -549,6 +743,9 @@ def update_workflow_metadata(
549
743
  }
550
744
  )
551
745
 
746
+ # routingStatus 는 포인터 status 로 흡수됐다. update() 는 키를 더할 뿐이라
747
+ # 이전 run 이 남긴 값은 명시적으로 지워야 사라진다.
748
+ workflow.pop("routingStatus", None)
552
749
  workflow.update(
553
750
  {
554
751
  "phaseSequence": phase_sequence,
@@ -558,7 +755,6 @@ def update_workflow_metadata(
558
755
  "lastCompletedPhase": last_completed_phase,
559
756
  "nextRecommendedPhase": next_recommended_phase,
560
757
  "awaitingApproval": awaiting_approval,
561
- "routingStatus": routing_status,
562
758
  "lastSafeCheckpoint": last_safe_checkpoint,
563
759
  }
564
760
  )
@@ -567,6 +763,10 @@ def update_workflow_metadata(
567
763
  workflow_snapshot = run_manifest.get("workflowSnapshot", {})
568
764
  if not isinstance(workflow_snapshot, dict):
569
765
  workflow_snapshot = {}
766
+ # task manifest 의 workflow 는 다음 run 이 덮어쓰므로 정정 기록이 남지 않는다.
767
+ # run 단위 감사 기록은 이 스냅샷이 유일한 보관처다.
768
+ workflow_snapshot.pop("routingStatus", None)
769
+ workflow_snapshot.pop("nextRecommendedPhaseCorrection", None)
570
770
  workflow_snapshot.update(
571
771
  {
572
772
  "phaseSequence": workflow["phaseSequence"],
@@ -574,12 +774,20 @@ def update_workflow_metadata(
574
774
  "currentPhaseState": workflow["currentPhaseState"],
575
775
  "phaseStates": workflow["phaseStates"],
576
776
  "lastCompletedPhase": workflow["lastCompletedPhase"],
577
- "nextRecommendedPhase": workflow["nextRecommendedPhase"],
777
+ # 포인터는 이제 문자열이 아니라 dict 다. 두 매니페스트가 같은 객체를
778
+ # 공유하면 한쪽을 제자리 변형할 때 다른 쪽이 조용히 따라 바뀐다.
779
+ "nextRecommendedPhase": dict(workflow["nextRecommendedPhase"]),
578
780
  "awaitingApproval": workflow["awaitingApproval"],
579
- "routingStatus": workflow["routingStatus"],
580
781
  "lastSafeCheckpoint": workflow["lastSafeCheckpoint"],
581
782
  }
582
783
  )
784
+ if "nextRecommendedPhaseCorrection" in workflow:
785
+ # 두 매니페스트는 별개 파일로 나간다 — dict 를 공유시키지 않는다.
786
+ # 겉 dict 만 복사하면 authored·applied 는 여전히 같은 객체다.
787
+ workflow_snapshot["nextRecommendedPhaseCorrection"] = {
788
+ key: dict(value)
789
+ for key, value in workflow["nextRecommendedPhaseCorrection"].items()
790
+ }
583
791
  run_manifest["workflowSnapshot"] = workflow_snapshot
584
792
 
585
793
 
@@ -590,13 +798,16 @@ def update_validation_metadata(
590
798
  validation_status: str,
591
799
  failures: list[str],
592
800
  report_data: Mapping[str, Any] | None = None,
801
+ advisories: list[str] | None = None,
593
802
  ) -> None:
594
803
  checked_at = utc_now()
804
+ advisories = list(advisories or [])
595
805
 
596
806
  team_state.setdefault("validator", {})
597
807
  team_state["validator"]["status"] = validation_status
598
808
  team_state["validator"]["lastValidatedAt"] = checked_at
599
809
  team_state["validator"]["failures"] = failures
810
+ team_state["validator"]["advisories"] = advisories
600
811
 
601
812
  run_manifest.setdefault("validation", {})
602
813
  run_manifest["validation"]["required"] = True
@@ -604,6 +815,8 @@ def update_validation_metadata(
604
815
  run_manifest["validation"]["lastCheckedAt"] = checked_at
605
816
  run_manifest["validation"]["passed"] = validation_status == "passed"
606
817
  run_manifest["validation"]["failures"] = failures
818
+ # Reported, not blocking — a passing run can carry these.
819
+ run_manifest["validation"]["advisories"] = advisories
607
820
  run_manifest["status"] = (
608
821
  "completed" if validation_status == "passed" else "contract-violated"
609
822
  )
@@ -709,35 +922,127 @@ def _persisted_prompt_records(
709
922
  project_root: Path,
710
923
  selected_ids: set[str],
711
924
  team_state: dict,
712
- ) -> list[PromptRecord]:
713
- dispatches = team_state.get("workerDispatches")
714
- source = dispatches if isinstance(dispatches, list) and dispatches else (
715
- team_state.get("workers") or []
925
+ run_manifest: Mapping[str, Any],
926
+ ) -> tuple[list[PromptRecord], list[str]]:
927
+ uses_v2_identity = (
928
+ run_manifest.get("schemaVersion") == "2.0"
929
+ and run_manifest.get("executionIdentityVersion") == 2
716
930
  )
931
+ dispatches = team_state.get("workerDispatches")
932
+ if uses_v2_identity:
933
+ source = [
934
+ *(dispatches if isinstance(dispatches, list) else []),
935
+ *(
936
+ team_state.get("agentDispatches")
937
+ if isinstance(team_state.get("agentDispatches"), list)
938
+ else []
939
+ ),
940
+ ]
941
+ else:
942
+ source = dispatches if isinstance(dispatches, list) and dispatches else (
943
+ team_state.get("workers") or []
944
+ )
717
945
  records: list[PromptRecord] = []
718
946
  seen: set[tuple[str, str, str]] = set()
947
+ errors: list[str] = []
948
+ role_executions = {
949
+ str(row.get("roleExecutionRef") or ""): row
950
+ for row in run_manifest.get("roleExecutions") or []
951
+ if isinstance(row, Mapping)
952
+ } if uses_v2_identity else {}
719
953
  for worker in source:
720
954
  if not isinstance(worker, dict):
721
955
  continue
722
- worker_id = str(worker.get("workerId") or "").strip()
956
+ assignment_ref = str(worker.get("assignmentRef") or "").strip()
957
+ worker_id = (
958
+ assignment_ref.removeprefix("initial/")
959
+ if uses_v2_identity and assignment_ref.startswith("initial/")
960
+ else str(worker.get("workerId") or "").strip()
961
+ )
723
962
  prompt_value = str(worker.get("promptPath") or "").strip()
724
- if worker_id not in selected_ids or not prompt_value:
963
+ if worker_id not in selected_ids:
725
964
  continue
726
- dispatch_kind = str(worker.get("kind") or "").strip()
965
+ if not prompt_value:
966
+ errors.append(f"{worker_id}: persisted initial prompt path is missing")
967
+ continue
968
+ dispatch_kind = str(
969
+ worker.get("kind") or worker.get("dispatchKind") or ""
970
+ ).strip()
727
971
  if not dispatch_kind:
728
972
  dispatch_kind = _fallback_dispatch_kind(prompt_value)
973
+ duty_id = ""
974
+ if uses_v2_identity:
975
+ execution = role_executions.get(
976
+ str(worker.get("roleExecutionRef") or "").strip()
977
+ )
978
+ participant_ref = str(worker.get("participantRef") or "").strip()
979
+ duty_id = str(worker.get("dutyId") or "").strip()
980
+ audience = str(worker.get("audience") or "").strip()
981
+ assignments = run_manifest.get("invocationAssignments")
982
+ assignment = (
983
+ assignments.get(assignment_ref)
984
+ if isinstance(assignments, Mapping)
985
+ else None
986
+ )
987
+ if not isinstance(execution, Mapping):
988
+ errors.append(
989
+ f"{worker_id}: persisted prompt has unknown roleExecutionRef"
990
+ )
991
+ continue
992
+ try:
993
+ expected_role = role_for_duty(duty_id)
994
+ except RoleCatalogError as exc:
995
+ errors.append(f"{worker_id}: {exc}")
996
+ continue
997
+ if (
998
+ execution.get("participantRef") != participant_ref
999
+ or execution.get("role") != expected_role
1000
+ or execution.get("executionLabel") != worker.get("executionLabel")
1001
+ or not isinstance(assignment, Mapping)
1002
+ or execution.get("provider") != assignment.get("provider")
1003
+ or not isinstance(execution.get("binding"), Mapping)
1004
+ or execution["binding"].get("resolvedExecutionValue")
1005
+ != assignment.get("modelExecutionValue")
1006
+ ):
1007
+ errors.append(
1008
+ f"{worker_id}: persisted prompt identity does not match role execution"
1009
+ )
1010
+ continue
1011
+ if audience != duty_id:
1012
+ errors.append(
1013
+ f"{worker_id}: persisted prompt audience does not match duty"
1014
+ )
1015
+ continue
729
1016
  identity = (worker_id, dispatch_kind, prompt_value)
730
1017
  if identity in seen:
731
1018
  continue
732
1019
  seen.add(identity)
1020
+ metadata_value = str(worker.get("promptMetadataPath") or "").strip()
733
1021
  records.append(
734
1022
  PromptRecord(
735
1023
  worker_id=worker_id,
736
1024
  dispatch_kind=dispatch_kind,
737
1025
  path=_resolve_prompt_record_path(project_root, prompt_value),
1026
+ expected_model=(
1027
+ str(worker.get("modelExecutionValue") or "").strip() or None
1028
+ if uses_v2_identity else None
1029
+ ),
1030
+ metadata_path=(
1031
+ _resolve_prompt_record_path(project_root, metadata_value)
1032
+ if uses_v2_identity and metadata_value else None
1033
+ ),
1034
+ expected_duty_audience=(duty_id if uses_v2_identity else None),
738
1035
  )
739
1036
  )
740
- return records
1037
+ if uses_v2_identity:
1038
+ recorded_ids = {record.worker_id for record in records}
1039
+ missing_ids = sorted(selected_ids - recorded_ids)
1040
+ if missing_ids:
1041
+ errors.append(
1042
+ "no persisted initial prompt records for selected role executions: "
1043
+ + ", ".join(missing_ids)
1044
+ )
1045
+ return records, errors
741
1046
 
742
1047
 
743
1048
  def _markdown_section(text: str, heading: str) -> str:
@@ -863,15 +1168,16 @@ def _validate_initial_analysis_prompts(
863
1168
  team_state = data.get("teamState") or {}
864
1169
  manifest = dict(run_manifest)
865
1170
  manifest["taskType"] = task_type
866
- records = _persisted_prompt_records(
1171
+ records, collection_errors = _persisted_prompt_records(
867
1172
  project_root=project_root,
868
1173
  selected_ids=selected_ids,
869
1174
  team_state=team_state,
1175
+ run_manifest=run_manifest,
870
1176
  )
871
- errors = validate_initial_prompt_records(
1177
+ errors = [*collection_errors, *validate_initial_prompt_records(
872
1178
  manifest=manifest,
873
1179
  records=records,
874
- )
1180
+ )]
875
1181
  if task_type == "improvement-discovery":
876
1182
  errors.extend(
877
1183
  _validate_improvement_primary_assignments(
@@ -2638,22 +2944,30 @@ def _report_run_seq(report_path: Path) -> str | None:
2638
2944
 
2639
2945
 
2640
2946
  def validate_worker_results_audit(
2641
- report_path: Path, task_type: str, failures: list[str]
2947
+ report_path: Path,
2948
+ task_type: str,
2949
+ failures: list[str],
2950
+ advisories: list[str] | None = None,
2642
2951
  ) -> None:
2643
2952
  """Enforce the worker audit sidecar contract at Phase 7.
2644
2953
 
2645
2954
  The rules themselves live in `okstra_ctl.worker_audit_ledger` so that
2646
2955
  `okstra worker-audit-check` can apply the identical checks mid-run, while
2647
- the worker session is still alive and can fix its own citations. All this
2648
- wrapper adds is the Phase 7 anchor: the run directory and this run's seq,
2649
- both read off the report path.
2956
+ the worker session is still alive and can fix its own citations. This
2957
+ wrapper adds the Phase 7 anchor — the run directory and this run's seq, both
2958
+ read off the report path — and routes the citation-ledger findings to
2959
+ *advisories*, which do not fail the run. Mid-run the worker can still fix a
2960
+ citation; at Phase 7 the only remedy left is to void a finished run over a
2961
+ spelling, so the finding is reported instead.
2650
2962
  """
2651
- failures.extend(check_worker_results_audit(
2963
+ blocking, ledger = worker_results_audit_findings(
2652
2964
  # `report_path` is `runs/<task-type>/reports/final-report-...md`.
2653
2965
  report_path.parent.parent,
2654
2966
  task_type,
2655
2967
  _report_run_seq(report_path),
2656
- ))
2968
+ )
2969
+ failures.extend(blocking)
2970
+ (advisories if advisories is not None else failures).extend(ledger)
2657
2971
 
2658
2972
 
2659
2973
  def validate_team_state_usage(team_state: dict, failures: list[str]) -> None:
@@ -3080,6 +3394,13 @@ def validate_final_report_data(
3080
3394
  return data
3081
3395
 
3082
3396
  manifest = run_manifest or {}
3397
+ if data.get("executionIdentityVersion") == 2 or data.get("executionRoles"):
3398
+ try:
3399
+ from okstra_ctl.report_contract import assert_execution_roles_match_manifest
3400
+
3401
+ assert_execution_roles_match_manifest(data, manifest)
3402
+ except ValueError as exc:
3403
+ failures.append(str(exc))
3083
3404
  _validate_approval_context(data, manifest, failures, report_path)
3084
3405
  _validate_activity_contract_plan_limits(data, manifest, failures)
3085
3406
 
@@ -3373,7 +3694,17 @@ _ADVISORY_ONLY_KINDS = {"d"}
3373
3694
  # Stop reasons that justify promoting a still-broken planner-fixable item to the
3374
3695
  # user: the self-fix budget ran out, or a round produced no net resolution so
3375
3696
  # further rounds would repeat themselves.
3376
- _SELF_FIX_EXHAUSTED_REASONS = frozenset({"max-rounds-reached", "no-progress"})
3697
+ #
3698
+ # `cause-group-recurrence` is the legacy spelling of that same exhaustion
3699
+ # (plan-body-verification.md "Loop termination"). A pre-activity-contract report
3700
+ # carrying it is a report whose loop stopped because the cause kept recurring —
3701
+ # refusing it here left such a run with no exit at all: the loop may not run
3702
+ # again, and the surviving item may not be promoted either.
3703
+ # `_validate_activity_contract_plan_limits` still forbids a NEW v1 run from
3704
+ # emitting it, so this admits the legacy shape without reopening it.
3705
+ _SELF_FIX_EXHAUSTED_REASONS = frozenset(
3706
+ {"max-rounds-reached", "no-progress", "cause-group-recurrence"}
3707
+ )
3377
3708
 
3378
3709
 
3379
3710
  def _is_variation_point_item(item: dict) -> bool:
@@ -4758,10 +5089,11 @@ def _items_resolved_in_round(plan_items: object, round_number: int) -> set[str]:
4758
5089
  def _detect_self_fix_recurrence(pbv: dict) -> list[str]:
4759
5090
  """Rounds that re-target ground the previous round already worked, unresolved.
4760
5091
 
4761
- `no-progress` counts resolutions, which a round can claim while its
4762
- correction introduces the next round's defect — the observed shape was two
4763
- rounds spent on one identical seven-item set. Repeating the previous
4764
- round's *unresolved* remainder is the earlier signal.
5092
+ `no-progress` is judged at the round's end from what it resolved. Repeating
5093
+ the previous round's *unresolved* remainder is the same conclusion reached
5094
+ one dispatch earlier — the observed shape was two rounds spent on one
5095
+ identical seven-item set. This names that shape so the loop can exit on it
5096
+ rather than paying for the round that proves it.
4765
5097
 
4766
5098
  Advisory only. Narrowing onto what the last round genuinely left open is
4767
5099
  legitimate progress, and the rule separating that from re-digging the same
@@ -4790,8 +5122,8 @@ def _detect_self_fix_recurrence(pbv: dict) -> list[str]:
4790
5122
  f"self-fix round {round_number} re-targets only items round "
4791
5123
  f"{round_number - 1} left unresolved ({', '.join(sorted(current))}) "
4792
5124
  "— the previous round's correction did not move this cause. "
4793
- "Consider exiting with `cause-group-recurrence` instead of "
4794
- "spending the remaining budget on the same ground."
5125
+ "Consider exiting with `no-progress` instead of spending the "
5126
+ "remaining budget on the same ground."
4795
5127
  )
4796
5128
  return warnings
4797
5129
 
@@ -7582,13 +7914,6 @@ def _validate_stage_has_requirement(data: dict, failures: list[str]) -> None:
7582
7914
  )
7583
7915
 
7584
7916
 
7585
- _FINAL_VERIFICATION_ROUTING_TOKEN_RE = re.compile(
7586
- r"(?<![A-Za-z-])(?:release-handoff\(stage-group\)|release-handoff|done|"
7587
- r"implementation|error-analysis|implementation-option-selection|"
7588
- r"implementation-planning)(?![A-Za-z-])"
7589
- )
7590
-
7591
-
7592
7917
  def _validate_final_verification_consistency(data: dict, failures: list[str]) -> None:
7593
7918
  """Enforce verdict ↔ blocker/condition/routing consistency on the
7594
7919
  final-verification data.json (SSOT). The schema guarantees field SHAPE;
@@ -7604,15 +7929,15 @@ def _validate_final_verification_consistency(data: dict, failures: list[str]) ->
7604
7929
  blockers = fv.get("acceptanceBlockers") or []
7605
7930
  conditions = verdict.get("conditionalAcceptanceConditions") or []
7606
7931
  routing_value = fv.get("routingRecommendation")
7607
- routing = routing_value if isinstance(routing_value, str) else ""
7608
- routing_tokens = _FINAL_VERIFICATION_ROUTING_TOKEN_RE.findall(routing)
7609
- routing_token = routing_tokens[0] if len(routing_tokens) == 1 else None
7610
-
7611
- if routing_token is None:
7932
+ routing_token = ""
7933
+ if isinstance(routing_value, dict):
7934
+ routing_token = str(routing_value.get("target") or "")
7935
+ if not routing_token:
7612
7936
  failures.append(
7613
- "final-verification: routingRecommendation must contain exactly one "
7614
- "supported routing token."
7937
+ "final-verification: routingRecommendation.target must name exactly one "
7938
+ "supported routing target."
7615
7939
  )
7940
+ routing_token = None
7616
7941
 
7617
7942
  if token == "accepted" and blockers:
7618
7943
  failures.append(
@@ -7898,11 +8223,16 @@ def _validate_session_conformance(
7898
8223
  task_type: str,
7899
8224
  claude_projects_dir: str | None,
7900
8225
  failures: list[str],
8226
+ advisories: list[str] | None = None,
7901
8227
  ) -> None:
7902
8228
  """prompts/lead/okstra-lead-contract.md의 PROGRESS / activity / heartbeat /
7903
8229
  implementation entry guard 사후 검사를 위임하고 실패를
7904
8230
  ``session-conformance: `` 접두로 folding 한다. 설계:
7905
8231
  docs/superpowers/specs/2026-06-10-blocking-contract-posthoc-conformance-design.md
8232
+
8233
+ PROGRESS 서술 라인 누락은 ``result.advisories`` 로 돌아오며 run 을
8234
+ 실패시키지 않는다 — 그 라인을 쓸 세션은 이 검사가 도는 시점에 이미 끝나 있어
8235
+ 남은 구제책이 phase 폐기뿐이다.
7906
8236
  """
7907
8237
  _validators_dir = Path(__file__).resolve().parent
7908
8238
  if str(_validators_dir) not in sys.path:
@@ -7924,6 +8254,9 @@ def _validate_session_conformance(
7924
8254
  claude_projects_dir=Path(claude_projects_dir) if claude_projects_dir else None,
7925
8255
  )
7926
8256
  failures.extend(f"session-conformance: {err}" for err in result.errors)
8257
+ (advisories if advisories is not None else failures).extend(
8258
+ f"session-conformance: {note}" for note in result.advisories
8259
+ )
7927
8260
 
7928
8261
 
7929
8262
  def _validate_forbidden_actions(
@@ -8677,8 +9010,21 @@ def main() -> int:
8677
9010
  print(f"validate-run: Phase 7 auto-recovery skipped — {msg}", file=sys.stderr)
8678
9011
 
8679
9012
  failures: list[str] = []
9013
+ # Findings that are reported but do not fail the run.
9014
+ #
9015
+ # The line is whether a remedy still exists at Phase 7. A defect in the
9016
+ # report is fixable — re-author it and re-render. A defect in what a
9017
+ # *finished session* wrote down is not: the worker that would restate its
9018
+ # citation and the lead pane that would emit its PROGRESS line have both
9019
+ # ended, so the only remaining move is to void the phase. That blocks the
9020
+ # next phase without establishing that any conclusion in the report is
9021
+ # wrong. Those findings are surfaced and carried in the run manifest
9022
+ # instead — currently the worker citation ledger and the PROGRESS narration
9023
+ # lines.
9024
+ advisories: list[str] = []
8680
9025
  if autofix_state == "accuracy-failed":
8681
9026
  failures.extend(autofix_messages)
9027
+ _validate_execution_identity_v2(run_manifest, failures)
8682
9028
  contract = extract_contract(run_manifest, task_manifest, failures)
8683
9029
  concurrent_run_authorized = bool(
8684
9030
  (run_manifest.get("concurrentRun") or {}).get("detected")
@@ -8743,7 +9089,7 @@ def main() -> int:
8743
9089
  failures,
8744
9090
  )
8745
9091
  if task_type:
8746
- validate_worker_results_audit(report_path, task_type, failures)
9092
+ validate_worker_results_audit(report_path, task_type, failures, advisories)
8747
9093
  _validate_session_conformance(
8748
9094
  team_state,
8749
9095
  team_state_path,
@@ -8753,6 +9099,7 @@ def main() -> int:
8753
9099
  task_type,
8754
9100
  args.claude_projects_dir,
8755
9101
  failures,
9102
+ advisories,
8756
9103
  )
8757
9104
  _validate_forbidden_actions(
8758
9105
  team_state,
@@ -8850,6 +9197,7 @@ def main() -> int:
8850
9197
  validation_status,
8851
9198
  failures,
8852
9199
  report_data=validation_data,
9200
+ advisories=advisories,
8853
9201
  )
8854
9202
 
8855
9203
  write_json(team_state_path, team_state)
@@ -8876,10 +9224,14 @@ def main() -> int:
8876
9224
  + "\n"
8877
9225
  )
8878
9226
 
9227
+ for note in advisories:
9228
+ print(f"validate-run: advisory — {note}", file=sys.stderr)
9229
+
8879
9230
  result = {
8880
9231
  "validationStatus": validation_status,
8881
9232
  "finalRunStatus": run_manifest.get("status"),
8882
9233
  "failures": failures,
9234
+ "advisories": advisories,
8883
9235
  "teamStatePath": str(team_state_path),
8884
9236
  "reportPath": str(report_path),
8885
9237
  }