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
@@ -6,10 +6,18 @@ import hashlib
6
6
  import json
7
7
  from typing import Any, Mapping
8
8
 
9
+ from .execution_identity import ExecutionManifest
10
+
9
11
 
10
12
  WORKING_SCHEMA_VERSION = "1.0"
13
+ V2_WORKING_SCHEMA_VERSION = "2.0"
14
+ EXECUTION_IDENTITY_VERSION = 2
11
15
  FINAL_SCHEMA_VERSION = "1.3"
12
16
  _AUDIENCES = {"analysis", "report-writer"}
17
+ _AUDIENCE_SOURCE_ROLES = {
18
+ "analysis": frozenset({"analyser", "designer", "planner", "verifier"}),
19
+ "report-writer": frozenset({"report-writer"}),
20
+ }
13
21
  _VERIFICATION_MODES = {"lightweight", "full-reanalysis"}
14
22
  _CLASSIFICATION_COUNT_KEYS = {
15
23
  "full-consensus": "fullConsensus",
@@ -52,19 +60,27 @@ def grouped_input_digest(grouped_input: Mapping[str, Any]) -> str:
52
60
  return hashlib.sha256(encoded).hexdigest()
53
61
 
54
62
 
55
- def seed_working_state(grouped_input: Mapping[str, Any]) -> dict[str, Any]:
63
+ def seed_working_state(
64
+ grouped_input: Mapping[str, Any],
65
+ *,
66
+ execution_manifest: ExecutionManifest | None = None,
67
+ ) -> dict[str, Any]:
56
68
  """Validate grouped findings, classify Round 0, and create the queue."""
57
69
  source = _object(grouped_input, "grouped input")
58
- if source.get("schemaVersion") != WORKING_SCHEMA_VERSION:
59
- raise ConvergenceContractError("grouped input schemaVersion must be 1.0")
70
+ identity_version = _grouped_execution_identity_version(source)
60
71
  task_key = _required_string(source, "taskKey", "grouped input")
61
72
  config = _parse_config(source.get("config"))
62
- workers = _parse_workers(source.get("workers"))
73
+ workers = _parse_workers(source.get("workers"), identity_version)
74
+ _validate_worker_execution_identity(
75
+ workers,
76
+ execution_manifest,
77
+ identity_version,
78
+ )
63
79
  analysis_workers = [
64
80
  worker["workerId"] for worker in workers if worker["audience"] == "analysis"
65
81
  ]
66
82
  state = {
67
- "schemaVersion": WORKING_SCHEMA_VERSION,
83
+ "schemaVersion": source["schemaVersion"],
68
84
  "taskKey": task_key,
69
85
  "groupsDigest": grouped_input_digest(source),
70
86
  "config": config,
@@ -74,6 +90,15 @@ def seed_working_state(grouped_input: Mapping[str, Any]) -> dict[str, Any]:
74
90
  "roundHistory": [],
75
91
  "stopReason": None,
76
92
  }
93
+ if identity_version == EXECUTION_IDENTITY_VERSION:
94
+ state.update({
95
+ "executionIdentityVersion": EXECUTION_IDENTITY_VERSION,
96
+ "runManifestPath": _required_string(
97
+ source,
98
+ "runManifestPath",
99
+ "grouped input",
100
+ ),
101
+ })
77
102
  if not config["enabled"] or len(analysis_workers) < 2:
78
103
  state["stopReason"] = "auto-disabled"
79
104
  return state
@@ -141,16 +166,30 @@ def plan_next_round(state: Mapping[str, Any]) -> dict[str, Any]:
141
166
  if not isinstance(worker, Mapping) or worker.get("audience") != "analysis":
142
167
  continue
143
168
  worker_id = _required_string(worker, "workerId", "working state worker")
169
+ worker_participant_ref = _worker_participant_ref(worker)
144
170
  eligible = [
145
171
  finding_id
146
172
  for finding_id in queue
147
- if findings[finding_id].get("originWorker") != worker_id
173
+ if _worker_is_independent_from_finding(
174
+ worker_id,
175
+ worker_participant_ref,
176
+ findings[finding_id],
177
+ workers,
178
+ )
148
179
  ]
149
180
  if eligible:
150
- dispatches.append({"worker": worker_id, "findingIds": eligible})
181
+ dispatches.append({
182
+ **_worker_execution_refs(worker),
183
+ "worker": worker_id,
184
+ "findingIds": eligible,
185
+ })
151
186
  else:
152
187
  skipped.append(
153
- {"worker": worker_id, "reason": "no items to verify"}
188
+ {
189
+ **_worker_execution_refs(worker),
190
+ "worker": worker_id,
191
+ "reason": "no items to verify",
192
+ }
154
193
  )
155
194
  return {
156
195
  "schemaVersion": WORKING_SCHEMA_VERSION,
@@ -162,6 +201,45 @@ def plan_next_round(state: Mapping[str, Any]) -> dict[str, Any]:
162
201
  }
163
202
 
164
203
 
204
+ def _worker_participant_ref(worker: Mapping[str, Any]) -> str | None:
205
+ value = worker.get("participantRef")
206
+ return value if isinstance(value, str) and value else None
207
+
208
+
209
+ def _worker_is_independent_from_finding(
210
+ worker_id: str,
211
+ participant_ref: str | None,
212
+ finding: Mapping[str, Any],
213
+ workers: list[Any],
214
+ ) -> bool:
215
+ origin_worker = finding.get("originWorker")
216
+ if participant_ref is None:
217
+ return origin_worker != worker_id
218
+ origin = next(
219
+ (
220
+ row
221
+ for row in workers
222
+ if isinstance(row, Mapping) and row.get("workerId") == origin_worker
223
+ ),
224
+ None,
225
+ )
226
+ origin_participant_ref = (
227
+ _worker_participant_ref(origin) if origin is not None else None
228
+ )
229
+ return origin_participant_ref != participant_ref
230
+
231
+
232
+ def _worker_execution_refs(worker: Mapping[str, Any]) -> dict[str, str]:
233
+ participant_ref = _worker_participant_ref(worker)
234
+ source_ref = worker.get("sourceRoleExecutionRef")
235
+ if participant_ref is None or not isinstance(source_ref, str) or not source_ref:
236
+ return {}
237
+ return {
238
+ "participantRef": participant_ref,
239
+ "sourceRoleExecutionRef": source_ref,
240
+ }
241
+
242
+
165
243
  def classify_collaborative_round(
166
244
  votes: Mapping[str, Mapping[str, Any]],
167
245
  ) -> str | None:
@@ -304,7 +382,10 @@ def apply_round_results(
304
382
  }
305
383
  for row in expected_plan["dispatches"]
306
384
  ]
307
- skipped = deepcopy(expected_plan["skippedWorkers"])
385
+ skipped = [
386
+ {"worker": row["worker"], "reason": row["reason"]}
387
+ for row in expected_plan["skippedWorkers"]
388
+ ]
308
389
  for row in dispatch_history:
309
390
  if row["status"] != "completed":
310
391
  skipped.append(
@@ -457,13 +538,40 @@ def finalize_working_state(state: Mapping[str, Any]) -> dict[str, Any]:
457
538
  return final
458
539
 
459
540
 
541
+ def _working_execution_identity_version(
542
+ state: Mapping[str, Any],
543
+ errors: list[str],
544
+ ) -> int | None:
545
+ schema_version = state.get("schemaVersion")
546
+ identity_version = state.get("executionIdentityVersion")
547
+ run_manifest_path = state.get("runManifestPath")
548
+ if schema_version == WORKING_SCHEMA_VERSION:
549
+ if (
550
+ "executionIdentityVersion" in state
551
+ or "runManifestPath" in state
552
+ ):
553
+ errors.append(
554
+ "working schemaVersion 1.0 v1 state forbids v2 identity fields"
555
+ )
556
+ return 1
557
+ if schema_version == V2_WORKING_SCHEMA_VERSION:
558
+ if identity_version != EXECUTION_IDENTITY_VERSION:
559
+ errors.append(
560
+ "working execution identity version must be 2 for schemaVersion 2.0"
561
+ )
562
+ if not _nonempty_string(run_manifest_path):
563
+ errors.append("working runManifestPath must be a non-empty string")
564
+ return EXECUTION_IDENTITY_VERSION
565
+ errors.append(f"unsupported working schemaVersion: {schema_version!r}")
566
+ return None
567
+
568
+
460
569
  def validate_working_state(state: Mapping[str, Any]) -> list[str]:
461
570
  """Return structural and transition errors for a working-state object."""
462
571
  errors: list[str] = []
463
572
  if not isinstance(state, Mapping):
464
573
  return ["working state must be an object"]
465
- if state.get("schemaVersion") != WORKING_SCHEMA_VERSION:
466
- errors.append("working schemaVersion must be 1.0")
574
+ identity_version = _working_execution_identity_version(state, errors)
467
575
  if not _nonempty_string(state.get("taskKey")):
468
576
  errors.append("taskKey must be a non-empty string")
469
577
  digest = state.get("groupsDigest")
@@ -497,6 +605,24 @@ def validate_working_state(state: Mapping[str, Any]) -> list[str]:
497
605
  "use 'analysis'; only the report author uses 'report-writer'. "
498
606
  f"Allowed: {sorted(_AUDIENCES)}."
499
607
  )
608
+ participant_ref = worker.get("participantRef")
609
+ source_ref = worker.get("sourceRoleExecutionRef")
610
+ has_execution_fields = (
611
+ "participantRef" in worker
612
+ or "sourceRoleExecutionRef" in worker
613
+ )
614
+ if identity_version == 1 and has_execution_fields:
615
+ errors.append(
616
+ f"workers[{index}] v1 row forbids execution identity references"
617
+ )
618
+ elif identity_version == EXECUTION_IDENTITY_VERSION and (
619
+ not _nonempty_string(participant_ref)
620
+ or not _nonempty_string(source_ref)
621
+ ):
622
+ errors.append(
623
+ f"workers[{index}] must carry participantRef and "
624
+ "sourceRoleExecutionRef together"
625
+ )
500
626
  findings = state.get("findings")
501
627
  if not isinstance(findings, list):
502
628
  errors.append("findings must be an array")
@@ -1788,8 +1914,10 @@ def _parse_vote(
1788
1914
  def _validate_plannable_state(
1789
1915
  state: Mapping[str, Any],
1790
1916
  ) -> tuple[list[str], dict[str, Mapping[str, Any]], list[Mapping[str, Any]]]:
1791
- if state.get("schemaVersion") != WORKING_SCHEMA_VERSION:
1792
- raise ConvergenceContractError("working state schemaVersion must be 1.0")
1917
+ version_errors: list[str] = []
1918
+ _working_execution_identity_version(state, version_errors)
1919
+ if version_errors:
1920
+ raise ConvergenceContractError("; ".join(version_errors))
1793
1921
  queue_value = state.get("queueFindingIds")
1794
1922
  if not isinstance(queue_value, list) or not all(
1795
1923
  isinstance(item, str) and item for item in queue_value
@@ -1833,6 +1961,41 @@ def _validate_plannable_state(
1833
1961
  return queue, findings, history
1834
1962
 
1835
1963
 
1964
+ def _grouped_execution_identity_version(source: Mapping[str, Any]) -> int:
1965
+ schema_version = source.get("schemaVersion")
1966
+ identity_version = source.get("executionIdentityVersion")
1967
+ workers = source.get("workers")
1968
+ worker_rows = workers if isinstance(workers, list) else []
1969
+ has_worker_identity = any(
1970
+ isinstance(worker, Mapping)
1971
+ and (
1972
+ "participantRef" in worker
1973
+ or "sourceRoleExecutionRef" in worker
1974
+ )
1975
+ for worker in worker_rows
1976
+ )
1977
+ if schema_version == WORKING_SCHEMA_VERSION:
1978
+ if (
1979
+ "executionIdentityVersion" in source
1980
+ or "runManifestPath" in source
1981
+ or has_worker_identity
1982
+ ):
1983
+ raise ConvergenceContractError(
1984
+ "v1 convergence groups forbid v2 execution identity fields"
1985
+ )
1986
+ return 1
1987
+ if schema_version == V2_WORKING_SCHEMA_VERSION:
1988
+ if identity_version != EXECUTION_IDENTITY_VERSION:
1989
+ raise ConvergenceContractError(
1990
+ "v2 convergence groups require executionIdentityVersion 2"
1991
+ )
1992
+ _required_string(source, "runManifestPath", "grouped input")
1993
+ return EXECUTION_IDENTITY_VERSION
1994
+ raise ConvergenceContractError(
1995
+ f"unsupported grouped input schemaVersion: {schema_version!r}"
1996
+ )
1997
+
1998
+
1836
1999
  def _parse_config(value: Any) -> dict[str, Any]:
1837
2000
  config = _object(value, "config")
1838
2001
  enabled = config.get("enabled")
@@ -1865,7 +2028,7 @@ def _parse_config(value: Any) -> dict[str, Any]:
1865
2028
  }
1866
2029
 
1867
2030
 
1868
- def _parse_workers(value: Any) -> list[dict[str, str]]:
2031
+ def _parse_workers(value: Any, identity_version: int) -> list[dict[str, str]]:
1869
2032
  if not isinstance(value, list):
1870
2033
  raise ConvergenceContractError("workers must be an array")
1871
2034
  workers: list[dict[str, str]] = []
@@ -1885,10 +2048,80 @@ def _parse_workers(value: Any) -> list[dict[str, str]]:
1885
2048
  if worker_id in seen:
1886
2049
  raise ConvergenceContractError(f"duplicate workerId: {worker_id}")
1887
2050
  seen.add(worker_id)
1888
- workers.append({"workerId": worker_id, "audience": audience})
2051
+ parsed = {"workerId": worker_id, "audience": audience}
2052
+ participant_ref = worker.get("participantRef")
2053
+ source_ref = worker.get("sourceRoleExecutionRef")
2054
+ has_execution_refs = (
2055
+ "participantRef" in worker
2056
+ or "sourceRoleExecutionRef" in worker
2057
+ )
2058
+ if identity_version == 1 and has_execution_refs:
2059
+ raise ConvergenceContractError(
2060
+ "v1 convergence workers forbid execution identity references"
2061
+ )
2062
+ if identity_version == EXECUTION_IDENTITY_VERSION and (
2063
+ not _nonempty_string(participant_ref)
2064
+ or not _nonempty_string(source_ref)
2065
+ ):
2066
+ raise ConvergenceContractError(
2067
+ "v2 convergence workers require execution identity references: "
2068
+ f"workers[{index}] must carry participantRef and "
2069
+ "sourceRoleExecutionRef"
2070
+ )
2071
+ if identity_version == EXECUTION_IDENTITY_VERSION:
2072
+ parsed.update({
2073
+ "participantRef": str(participant_ref),
2074
+ "sourceRoleExecutionRef": str(source_ref),
2075
+ })
2076
+ workers.append(parsed)
1889
2077
  return workers
1890
2078
 
1891
2079
 
2080
+ def _validate_worker_execution_identity(
2081
+ workers: list[dict[str, str]],
2082
+ execution_manifest: ExecutionManifest | None,
2083
+ identity_version: int,
2084
+ ) -> None:
2085
+ if identity_version == 1:
2086
+ if execution_manifest is not None:
2087
+ raise ConvergenceContractError(
2088
+ "v1 convergence forbids execution manifest authority; "
2089
+ "omit --run-manifest"
2090
+ )
2091
+ return
2092
+ if execution_manifest is None:
2093
+ raise ConvergenceContractError(
2094
+ "v2 convergence requires execution manifest authority; "
2095
+ "pass --run-manifest"
2096
+ )
2097
+ if execution_manifest.legacy:
2098
+ raise ConvergenceContractError(
2099
+ "v2 convergence workers require v2 execution manifest"
2100
+ )
2101
+ roles = {
2102
+ role.role_execution_ref: role
2103
+ for role in execution_manifest.role_executions
2104
+ }
2105
+ for worker in workers:
2106
+ source_ref = worker["sourceRoleExecutionRef"]
2107
+ source = roles.get(source_ref)
2108
+ if source is None:
2109
+ raise ConvergenceContractError(
2110
+ f"unknown sourceRoleExecutionRef: {source_ref}"
2111
+ )
2112
+ if source.participant_ref != worker["participantRef"]:
2113
+ raise ConvergenceContractError(
2114
+ f"worker {worker['workerId']} participantRef does not match "
2115
+ f"sourceRoleExecutionRef: {source_ref}"
2116
+ )
2117
+ allowed_roles = _AUDIENCE_SOURCE_ROLES[worker["audience"]]
2118
+ if source.role not in allowed_roles:
2119
+ raise ConvergenceContractError(
2120
+ f"{worker['audience']} audience source role is not allowed: "
2121
+ f"{source.role}"
2122
+ )
2123
+
2124
+
1892
2125
  def _parse_groups(
1893
2126
  value: Any,
1894
2127
  analysis_workers: list[str],
@@ -2,13 +2,28 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import json
5
- import hashlib
6
5
  import os
7
6
  from pathlib import Path
8
7
  import tempfile
9
8
  from typing import Any, Mapping
10
9
 
11
10
  from .convergence_engine import ConvergenceContractError
11
+ from .domain.role import RoleCatalogError, role_for_duty
12
+ from .execution_identity import ExecutionManifestError, Invocation, RoleExecution
13
+ from .execution_manifest import (
14
+ read_execution_manifest,
15
+ reserve_derived_role_invocation,
16
+ )
17
+ from .worker_request import verifier_extra_dirs
18
+ from .write_policy import build_invocation_write_contract
19
+
20
+
21
+ DYNAMIC_VERIFIER_SOURCE_ROLES = frozenset({
22
+ "analyser",
23
+ "designer",
24
+ "planner",
25
+ "verifier",
26
+ })
12
27
 
13
28
 
14
29
  def load_json_object(path: Path) -> dict[str, Any]:
@@ -62,6 +77,160 @@ def write_final_state_atomic(
62
77
  write_json_atomic(path, payload)
63
78
 
64
79
 
80
+ def reserve_dynamic_verifier(
81
+ manifest_path: Path,
82
+ source_role_execution_ref: str,
83
+ duty_id: str,
84
+ round_number: int,
85
+ task_key: str,
86
+ *,
87
+ input_digest: str,
88
+ invocation_ref: str | None = None,
89
+ artifact_paths: tuple[Path, ...],
90
+ ) -> tuple[RoleExecution, Invocation]:
91
+ """Reserve one provider-neutral verifier identity for a logical round."""
92
+ if round_number < 1:
93
+ raise ExecutionManifestError("dynamic verifier round must be positive")
94
+ manifest_path = Path(manifest_path).resolve()
95
+ manifest = read_execution_manifest(manifest_path)
96
+ if manifest.legacy:
97
+ raise ExecutionManifestError("cannot reserve a dynamic verifier in v1 data")
98
+ _validate_dynamic_verifier_request(
99
+ manifest.role_executions,
100
+ source_role_execution_ref,
101
+ duty_id,
102
+ )
103
+ authority = load_json_object(manifest_path)
104
+ if authority.get("taskKey") not in (None, task_key):
105
+ raise ExecutionManifestError("dynamic verifier taskKey does not match manifest")
106
+ project_root = _manifest_project_root(authority, manifest_path)
107
+ task_manifest_path = _manifest_authority_path(
108
+ authority,
109
+ "taskManifestPath",
110
+ project_root,
111
+ )
112
+ run_ref = _manifest_run_ref(authority, manifest_path, project_root)
113
+
114
+ def invocation_factory(verifier: RoleExecution) -> Invocation:
115
+ selected_ref = invocation_ref or (
116
+ f"{verifier.role_execution_ref}-{duty_id}-r{round_number:03d}"
117
+ )
118
+ capability = (
119
+ verifier.binding.worker_write_capability
120
+ if verifier.binding is not None
121
+ else None
122
+ )
123
+ if capability is None:
124
+ raise ExecutionManifestError(
125
+ "dynamic verifier has no runner write capability"
126
+ )
127
+ try:
128
+ policy, enforcement = build_invocation_write_contract(
129
+ role="verifier",
130
+ project_root=project_root,
131
+ worktree=None,
132
+ artifact_paths=artifact_paths,
133
+ maximum_precision=capability.max_boundary_precision,
134
+ auxiliary_roots=verifier_extra_dirs("verifier"),
135
+ validated_auxiliary_roots=verifier_extra_dirs("verifier"),
136
+ )
137
+ except (OSError, ValueError) as exc:
138
+ raise ExecutionManifestError(
139
+ f"dynamic verifier write policy is invalid: {exc}"
140
+ ) from exc
141
+ return Invocation(
142
+ invocation_ref=selected_ref,
143
+ participant_ref=verifier.participant_ref,
144
+ role_execution_ref=verifier.role_execution_ref,
145
+ source_invocation_ref=None,
146
+ recovery_ref=None,
147
+ duty_id=duty_id,
148
+ dispatch_kind=f"reverify-r{round_number}",
149
+ round=round_number,
150
+ input_digest=input_digest,
151
+ write_policy=policy.to_payload(),
152
+ write_policy_digest=policy.digest,
153
+ write_enforcement=enforcement.to_payload(),
154
+ )
155
+
156
+ return reserve_derived_role_invocation(
157
+ manifest_path,
158
+ source_role_execution_ref=source_role_execution_ref,
159
+ role="verifier",
160
+ invocation_factory=invocation_factory,
161
+ task_key=task_key,
162
+ task_manifest_path=task_manifest_path,
163
+ run_ref=run_ref,
164
+ )
165
+
166
+
167
+ def _validate_dynamic_verifier_request(
168
+ roles: tuple[RoleExecution, ...],
169
+ source_role_execution_ref: str,
170
+ duty_id: str,
171
+ ) -> None:
172
+ try:
173
+ duty_role = role_for_duty(duty_id)
174
+ except RoleCatalogError as exc:
175
+ raise ExecutionManifestError(f"unknown verifier duty: {duty_id}") from exc
176
+ if duty_role != "verifier":
177
+ raise ExecutionManifestError(f"dynamic verifier duty is not verifier: {duty_id}")
178
+ source = next(
179
+ (
180
+ role
181
+ for role in roles
182
+ if role.role_execution_ref == source_role_execution_ref
183
+ ),
184
+ None,
185
+ )
186
+ if source is not None and source.role not in DYNAMIC_VERIFIER_SOURCE_ROLES:
187
+ raise ExecutionManifestError(
188
+ f"dynamic verifier source role is not eligible: {source.role}"
189
+ )
190
+
191
+
192
+ def _manifest_project_root(
193
+ manifest: Mapping[str, Any],
194
+ manifest_path: Path,
195
+ ) -> Path:
196
+ value = manifest.get("projectRoot")
197
+ if not isinstance(value, str) or not value.strip():
198
+ raise ExecutionManifestError("run manifest has no projectRoot")
199
+ project_root = Path(value).resolve()
200
+ if not manifest_path.is_relative_to(project_root):
201
+ raise ExecutionManifestError("run manifest resolves outside projectRoot")
202
+ return project_root
203
+
204
+
205
+ def _manifest_authority_path(
206
+ manifest: Mapping[str, Any],
207
+ key: str,
208
+ project_root: Path,
209
+ ) -> Path:
210
+ value = manifest.get(key)
211
+ if not isinstance(value, str) or not value.strip():
212
+ raise ExecutionManifestError(f"run manifest has no {key}")
213
+ path = (project_root / value).resolve()
214
+ if not path.is_relative_to(project_root):
215
+ raise ExecutionManifestError(f"run manifest {key} escapes projectRoot")
216
+ return path
217
+
218
+
219
+ def _manifest_run_ref(
220
+ manifest: Mapping[str, Any],
221
+ manifest_path: Path,
222
+ project_root: Path,
223
+ ) -> str:
224
+ value = manifest.get("runManifestPath")
225
+ if not isinstance(value, str) or not value.strip():
226
+ raise ExecutionManifestError("run manifest has no runManifestPath")
227
+ if (project_root / value).resolve() != manifest_path:
228
+ raise ExecutionManifestError(
229
+ "runManifestPath does not identify the execution manifest"
230
+ )
231
+ return value
232
+
233
+
65
234
  def _validate_final_replacement(
66
235
  path: Path,
67
236
  migration: Mapping[str, Any] | None,