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
@@ -14,6 +14,8 @@ import re
14
14
  import tempfile
15
15
  from typing import Iterator, Literal, Mapping, get_args
16
16
 
17
+ from .domain.role import RoleCatalogError, role_for_duty
18
+
17
19
 
18
20
  AgentAudience = Literal[
19
21
  "lead",
@@ -79,6 +81,15 @@ _TOP_LEVEL_KEYS = {
79
81
  "prompt",
80
82
  "digests",
81
83
  }
84
+ _V2_TOP_LEVEL_KEYS = (_TOP_LEVEL_KEYS - {"workerId"}) | {
85
+ "executionIdentityVersion",
86
+ "invocationRef",
87
+ "participantRef",
88
+ "roleExecutionRef",
89
+ "executionLabel",
90
+ "dutyId",
91
+ "attempt",
92
+ }
82
93
  _NESTED_KEYS = {
83
94
  "contractSource": {"mode", "runManifestPath", "dutyRootPath"},
84
95
  "modelAssignment": {
@@ -130,6 +141,24 @@ class AgentModelAssignment:
130
141
  host_model_value: str | None
131
142
 
132
143
 
144
+ @dataclass(frozen=True)
145
+ class InvocationExecutionIdentity:
146
+ participant_ref: str
147
+ role_execution_ref: str
148
+ execution_label: str
149
+ duty_id: str
150
+
151
+
152
+ @dataclass(frozen=True)
153
+ class InvocationMetadataIdentity:
154
+ invocation_ref: str
155
+ participant_ref: str
156
+ role_execution_ref: str
157
+ execution_label: str
158
+ duty_id: str
159
+ attempt: int
160
+
161
+
133
162
  @dataclass(frozen=True)
134
163
  class AgentInstructionSource:
135
164
  kind: Literal["project", "runtime"]
@@ -158,6 +187,11 @@ class AgentInvocationRequest:
158
187
  prompt_path: Path
159
188
  metadata_path: Path
160
189
  dispatch_kind: str
190
+ participant_ref: str | None
191
+ role_execution_ref: str | None
192
+ duty_id: str | None
193
+ invocation_ref: str | None
194
+ attempt: int
161
195
  # Republish a prompt this invocation id already wrote, but only while no
162
196
  # dispatch has used it. See `_publish_or_reuse`.
163
197
  replace_undispatched: bool = False
@@ -178,6 +212,10 @@ class PreparedAgentInvocation:
178
212
  duty_digest: str
179
213
  instruction_digest: str
180
214
  prompt_digest: str
215
+ participant_ref: str | None = None
216
+ role_execution_ref: str | None = None
217
+ invocation_ref: str | None = None
218
+ attempt: int = 1
181
219
 
182
220
 
183
221
  @dataclass(frozen=True)
@@ -189,6 +227,15 @@ class MaterializedStandaloneResult:
189
227
  result_envelope_digest: str
190
228
 
191
229
 
230
+ @dataclass(frozen=True)
231
+ class RetryAgentInvocation:
232
+ invocation_id: str
233
+ invocation_ref: str
234
+ prompt_path: Path
235
+ metadata_path: Path
236
+ attempt: int
237
+
238
+
192
239
  @dataclass(frozen=True)
193
240
  class VerifiedStandaloneResult:
194
241
  purpose: str
@@ -228,6 +275,201 @@ def agent_model_assignment_from_payload(payload: object) -> AgentModelAssignment
228
275
  return assignment
229
276
 
230
277
 
278
+ def invocation_execution_identity_from_manifest(
279
+ manifest: Mapping[str, object],
280
+ *,
281
+ assignment: AgentModelAssignment,
282
+ assignment_ref: str,
283
+ duty_id: str,
284
+ role_execution_ref: str | None = None,
285
+ ) -> InvocationExecutionIdentity | None:
286
+ """Resolve v2 refs from Task 8 without duplicating them in model assignments."""
287
+ schema = manifest.get("schemaVersion")
288
+ identity_version = manifest.get("executionIdentityVersion")
289
+ if schema in (None, 1, "1", "1.0") and identity_version is None:
290
+ return None
291
+ if schema != "2.0" or identity_version != 2:
292
+ raise AgentInvocationError("run manifest mixes v1 and v2 execution identity")
293
+ try:
294
+ role = role_for_duty(duty_id)
295
+ except RoleCatalogError as exc:
296
+ raise AgentInvocationError(str(exc)) from exc
297
+ role_executions = manifest.get("roleExecutions")
298
+ candidates = [
299
+ row for row in role_executions
300
+ if isinstance(row, Mapping)
301
+ and row.get("role") == role
302
+ and row.get("provider") == assignment.provider
303
+ and _role_execution_matches_assignment(row, assignment)
304
+ ] if isinstance(role_executions, list) else []
305
+ if role_execution_ref is None:
306
+ selected = _select_role_execution_candidate(
307
+ manifest,
308
+ candidates,
309
+ assignment_ref=assignment_ref,
310
+ role=role,
311
+ assignment=assignment,
312
+ )
313
+ else:
314
+ selected = next(
315
+ (
316
+ row
317
+ for row in candidates
318
+ if row.get("roleExecutionRef") == role_execution_ref
319
+ ),
320
+ None,
321
+ )
322
+ if selected is None:
323
+ raise AgentInvocationError(
324
+ "v2 execution identity does not match a canonical role execution"
325
+ )
326
+ return InvocationExecutionIdentity(
327
+ participant_ref=_required_metadata_string(selected, "participantRef"),
328
+ role_execution_ref=_required_metadata_string(selected, "roleExecutionRef"),
329
+ execution_label=_required_metadata_string(selected, "executionLabel"),
330
+ duty_id=duty_id,
331
+ )
332
+
333
+
334
+ def v2_role_assignment_authority_errors(
335
+ manifest: Mapping[str, object],
336
+ *,
337
+ role_execution_ref: str,
338
+ participant_ref: str,
339
+ assignment: AgentModelAssignment,
340
+ ) -> list[str]:
341
+ """Compare one v2 role and participant with the provider-neutral assignment."""
342
+ role_executions = manifest.get("roleExecutions")
343
+ execution = next((
344
+ row for row in role_executions
345
+ if isinstance(row, Mapping)
346
+ and row.get("roleExecutionRef") == role_execution_ref
347
+ ), None) if isinstance(role_executions, list) else None
348
+ participants = manifest.get("participantAssignments")
349
+ participant = next((
350
+ row for row in participants
351
+ if isinstance(row, Mapping)
352
+ and row.get("participantRef") == participant_ref
353
+ ), None) if isinstance(participants, list) else None
354
+ if not isinstance(execution, Mapping) or not isinstance(participant, Mapping):
355
+ return ["v2 assignment does not reference canonical execution identity"]
356
+ errors: list[str] = []
357
+ if participant.get("hostModelValue") != assignment.host_model_value:
358
+ errors.append(
359
+ "participant hostModelValue does not match invocation metadata"
360
+ )
361
+ if (
362
+ participant.get("hostRuntime") != assignment.host_runtime
363
+ or (
364
+ manifest.get("leadRuntime") is not None
365
+ and participant.get("hostRuntime") != manifest.get("leadRuntime")
366
+ )
367
+ ):
368
+ errors.append(
369
+ "participant hostRuntime does not match invocation metadata"
370
+ )
371
+ binding = execution.get("binding")
372
+ binding_matches = (
373
+ isinstance(binding, Mapping)
374
+ and binding.get("runner") == assignment.runner
375
+ and binding.get("resolvedExecutionValue")
376
+ == assignment.model_execution_value
377
+ and binding.get("hostModelValue") == assignment.host_model_value
378
+ and binding.get("hostModelValue") == participant.get("hostModelValue")
379
+ and binding.get("workerWriteCapability")
380
+ == participant.get("workerWriteCapability")
381
+ and participant.get("runner") == binding.get("runner")
382
+ ) or (
383
+ binding is None
384
+ and assignment.model_execution_value == "unknown"
385
+ and participant.get("runner") == assignment.runner
386
+ )
387
+ if (
388
+ execution.get("participantRef") != participant_ref
389
+ or execution.get("provider") != assignment.provider
390
+ or execution.get("provider") != participant.get("provider")
391
+ or execution.get("modelRef") != participant.get("modelRef")
392
+ or execution.get("modelId") != assignment.model
393
+ or execution.get("modelId") != participant.get("modelId")
394
+ or not binding_matches
395
+ ):
396
+ errors.append("v2 assignment does not match role execution authority")
397
+ return errors
398
+
399
+
400
+ def _role_execution_matches_assignment(
401
+ row: Mapping[str, object], assignment: AgentModelAssignment,
402
+ ) -> bool:
403
+ binding = row.get("binding")
404
+ if isinstance(binding, Mapping):
405
+ return binding.get("resolvedExecutionValue") == assignment.model_execution_value
406
+ return assignment.model_execution_value == "unknown"
407
+
408
+
409
+ def _select_role_execution_candidate(
410
+ manifest: Mapping[str, object],
411
+ candidates: list[Mapping[str, object]],
412
+ *,
413
+ assignment_ref: str,
414
+ role: str,
415
+ assignment: AgentModelAssignment,
416
+ ) -> Mapping[str, object] | None:
417
+ if len(candidates) == 1:
418
+ return candidates[0]
419
+ if not assignment_ref.startswith("initial/"):
420
+ return None
421
+ worker_id = assignment_ref.partition("/")[2]
422
+ workers = manifest.get("workerAssignments")
423
+ matching_workers = [
424
+ row for row in workers
425
+ if isinstance(row, Mapping)
426
+ and row.get("role") == role
427
+ and row.get("provider") == assignment.provider
428
+ and row.get("modelExecutionValue") == assignment.model_execution_value
429
+ ] if isinstance(workers, list) else []
430
+ worker_index = next((
431
+ index for index, row in enumerate(matching_workers)
432
+ if row.get("workerId") == worker_id
433
+ ), None)
434
+ ordered = sorted(candidates, key=lambda row: int(row.get("ordinal", 0)))
435
+ if worker_index is None or worker_index >= len(ordered):
436
+ return None
437
+ return ordered[worker_index]
438
+
439
+
440
+ def _required_metadata_string(
441
+ payload: Mapping[str, object], key: str,
442
+ ) -> str:
443
+ value = payload.get(key)
444
+ if not isinstance(value, str) or not value:
445
+ raise AgentInvocationError(f"v2 execution identity field is invalid: {key}")
446
+ return value
447
+
448
+
449
+ def invocation_metadata_identity(
450
+ payload: Mapping[str, object],
451
+ ) -> InvocationMetadataIdentity | None:
452
+ """Read one exact metadata version without guessing from partial fields."""
453
+ schema = payload.get("schemaVersion")
454
+ identity_version = payload.get("executionIdentityVersion")
455
+ if schema == 1 and identity_version is None:
456
+ if not _metadata_schema_is_exact(payload):
457
+ raise AgentInvocationError("agent invocation metadata schema is invalid")
458
+ return None
459
+ if schema != "2.0" or identity_version != 2:
460
+ raise AgentInvocationError("agent invocation metadata mixes v1 and v2 identity")
461
+ if not _metadata_schema_is_exact(payload):
462
+ raise AgentInvocationError("v2 execution identity metadata is invalid")
463
+ return InvocationMetadataIdentity(
464
+ invocation_ref=payload["invocationRef"],
465
+ participant_ref=payload["participantRef"],
466
+ role_execution_ref=payload["roleExecutionRef"],
467
+ execution_label=payload["executionLabel"],
468
+ duty_id=payload["dutyId"],
469
+ attempt=payload["attempt"],
470
+ )
471
+
472
+
231
473
  @dataclass(frozen=True)
232
474
  class _MaterializedInvocation:
233
475
  prepared: PreparedAgentInvocation
@@ -307,18 +549,132 @@ def prepare_agent_invocation(
307
549
  expected_worker_id=request.worker_id,
308
550
  expected_assignment_ref=request.assignment_ref,
309
551
  expected_audience=request.audience,
552
+ expected_participant_ref=request.participant_ref,
553
+ expected_role_execution_ref=request.role_execution_ref,
554
+ expected_invocation_ref=request.invocation_ref,
310
555
  )
311
556
  if errors:
312
557
  raise AgentInvocationError("; ".join(errors))
313
558
  return materialized.prepared
314
559
 
315
560
 
561
+ def materialize_retry_invocation(
562
+ metadata_path: Path,
563
+ *,
564
+ project_root: Path,
565
+ attempt: int,
566
+ ) -> RetryAgentInvocation:
567
+ """Publish an attempt-specific prompt/metadata pair for one v2 retry."""
568
+ metadata = _load_json_object(metadata_path, "agent invocation metadata")
569
+ identity = invocation_metadata_identity(metadata)
570
+ if identity is None:
571
+ raise AgentInvocationError("v1 invocation metadata cannot be retried as v2")
572
+ if attempt != identity.attempt + 1:
573
+ raise AgentInvocationError(
574
+ f"retry attempt must be {identity.attempt + 1}"
575
+ )
576
+ source = metadata.get("contractSource")
577
+ prompt_spec = metadata.get("prompt")
578
+ if not isinstance(source, dict) or not isinstance(prompt_spec, dict):
579
+ raise AgentInvocationError("retry invocation metadata is incomplete")
580
+ manifest_path = _project_path(
581
+ project_root, source.get("runManifestPath"), must_exist=True
582
+ )
583
+ prompt_path = _project_path(
584
+ project_root, prompt_spec.get("path"), must_exist=True
585
+ )
586
+ retry_prompt_path = prompt_path.with_name(
587
+ f"{prompt_path.stem}-attempt-{attempt}{prompt_path.suffix}"
588
+ )
589
+ retry_metadata_path = retry_prompt_path.with_name(
590
+ retry_prompt_path.name + ".meta.json"
591
+ )
592
+ retry_invocation_id = f"{metadata['invocationId']}-attempt-{attempt}"
593
+ retry_metadata = dict(metadata)
594
+ retry_metadata["invocationId"] = retry_invocation_id
595
+ retry_metadata["attempt"] = attempt
596
+ retry_metadata["prompt"] = {
597
+ "path": _project_relative(
598
+ retry_prompt_path, project_root, must_exist=False
599
+ )
600
+ }
601
+ manifest = _load_json_object(manifest_path, "run manifest")
602
+ contract = manifest.get("agentContract")
603
+ if not isinstance(contract, dict):
604
+ raise AgentInvocationError("run manifest has no agent contract")
605
+ reservation_root = _project_path(
606
+ project_root,
607
+ contract.get("invocationReservationRootPath"),
608
+ must_exist=True,
609
+ )
610
+ reservation = {
611
+ "schemaVersion": "2.0",
612
+ "executionIdentityVersion": 2,
613
+ "invocationId": retry_invocation_id,
614
+ "assignmentRef": metadata["assignmentRef"],
615
+ "audience": metadata["audience"],
616
+ "dispatchKind": metadata["dispatchKind"],
617
+ "promptPath": retry_metadata["prompt"]["path"],
618
+ "metadataPath": _project_relative(
619
+ retry_metadata_path, project_root, must_exist=False
620
+ ),
621
+ "invocationRef": identity.invocation_ref,
622
+ "participantRef": identity.participant_ref,
623
+ "roleExecutionRef": identity.role_execution_ref,
624
+ "dutyId": identity.duty_id,
625
+ "attempt": attempt,
626
+ }
627
+ with _exclusive_lock(reservation_root / "publish.lock"):
628
+ _publish_or_reuse_exact(
629
+ reservation_root / f"{retry_invocation_id}.json",
630
+ _pretty_json(reservation),
631
+ )
632
+ with _exclusive_lock(_prompt_lock_path(retry_prompt_path)):
633
+ _publish_or_reuse_exact(retry_prompt_path, prompt_path.read_bytes())
634
+ _publish_or_reuse_exact(
635
+ retry_metadata_path, _pretty_json(retry_metadata)
636
+ )
637
+ errors = verify_agent_invocation(
638
+ retry_metadata_path,
639
+ project_root=project_root,
640
+ expected_run_manifest_path=manifest_path,
641
+ expected_assignment=agent_model_assignment_from_payload(
642
+ metadata["modelAssignment"]
643
+ ),
644
+ expected_invocation_id=retry_invocation_id,
645
+ expected_assignment_ref=str(metadata["assignmentRef"]),
646
+ expected_audience=str(metadata["audience"]),
647
+ expected_participant_ref=identity.participant_ref,
648
+ expected_role_execution_ref=identity.role_execution_ref,
649
+ expected_invocation_ref=identity.invocation_ref,
650
+ )
651
+ if errors:
652
+ raise AgentInvocationError("; ".join(errors))
653
+ return RetryAgentInvocation(
654
+ invocation_id=retry_invocation_id,
655
+ invocation_ref=identity.invocation_ref,
656
+ prompt_path=retry_prompt_path,
657
+ metadata_path=retry_metadata_path,
658
+ attempt=attempt,
659
+ )
660
+
661
+
316
662
  def compose_agent_prompt(request: AgentInvocationRequest) -> str:
317
663
  """Render and validate a prompt candidate without publishing artifacts."""
318
664
  _validate_request(request)
319
665
  return _materialize(request).prompt_bytes.decode("utf-8")
320
666
 
321
667
 
668
+ def compose_unbound_run_prompt(request: AgentInvocationRequest) -> bytes:
669
+ """Compose v2 run prompt bytes before a dynamic role identity is reserved."""
670
+ _validate_common_request(request)
671
+ _validate_unbound_v2_run_request(request)
672
+ common = load_common_duty_contract(request.duty_root)
673
+ catalog = load_duty_catalog(request.duty_root)
674
+ duty = catalog[request.audience]
675
+ return _render_prompt(request, common, duty).encode("utf-8")
676
+
677
+
322
678
  def verify_agent_invocation(
323
679
  metadata_path: Path,
324
680
  *,
@@ -329,6 +685,9 @@ def verify_agent_invocation(
329
685
  expected_worker_id: str | None = None,
330
686
  expected_assignment_ref: str | None = None,
331
687
  expected_audience: AgentAudience | None = None,
688
+ expected_participant_ref: str | None = None,
689
+ expected_role_execution_ref: str | None = None,
690
+ expected_invocation_ref: str | None = None,
332
691
  ) -> list[str]:
333
692
  """Return every deterministic violation found in a published invocation."""
334
693
  metadata = _read_metadata(metadata_path)
@@ -340,6 +699,9 @@ def verify_agent_invocation(
340
699
  expected_worker_id=expected_worker_id,
341
700
  expected_assignment_ref=expected_assignment_ref,
342
701
  expected_audience=expected_audience,
702
+ expected_participant_ref=expected_participant_ref,
703
+ expected_role_execution_ref=expected_role_execution_ref,
704
+ expected_invocation_ref=expected_invocation_ref,
343
705
  )
344
706
  errors.extend(_verify_prompt_file(metadata, project_root))
345
707
  errors.extend(_verify_assignment(metadata, expected_assignment))
@@ -618,6 +980,8 @@ def _materialize(request: AgentInvocationRequest) -> _MaterializedInvocation:
618
980
  common = load_common_duty_contract(request.duty_root)
619
981
  catalog = load_duty_catalog(request.duty_root)
620
982
  duty = catalog[request.audience]
983
+ if _request_identity_version(request) == 2 and request.duty_id != duty.id:
984
+ raise AgentInvocationError("v2 execution identity duty does not match audience")
621
985
  prompt_bytes = _render_prompt(request, common, duty).encode("utf-8")
622
986
  digests = _invocation_digests(request, duty, prompt_bytes)
623
987
  metadata = _metadata_payload(request, duty, digests)
@@ -635,6 +999,10 @@ def _materialize(request: AgentInvocationRequest) -> _MaterializedInvocation:
635
999
  duty_digest=digests["dutyDigest"],
636
1000
  instruction_digest=digests["instructionDigest"],
637
1001
  prompt_digest=digests["promptDigest"],
1002
+ participant_ref=request.participant_ref,
1003
+ role_execution_ref=request.role_execution_ref,
1004
+ invocation_ref=request.invocation_ref,
1005
+ attempt=request.attempt,
638
1006
  )
639
1007
  reservation_root, reservation = _reservation_spec(request)
640
1008
  return _MaterializedInvocation(
@@ -648,6 +1016,14 @@ def _materialize(request: AgentInvocationRequest) -> _MaterializedInvocation:
648
1016
 
649
1017
 
650
1018
  def _validate_request(request: AgentInvocationRequest) -> None:
1019
+ _validate_common_request(request)
1020
+ if request.run_manifest_path is None:
1021
+ _validate_standalone_identity(request)
1022
+ else:
1023
+ _validate_run_identity(request)
1024
+
1025
+
1026
+ def _validate_common_request(request: AgentInvocationRequest) -> None:
651
1027
  if request.audience not in _SUPPORTED_AUDIENCES:
652
1028
  raise AgentInvocationError(f"unknown duty audience: {request.audience}")
653
1029
  adjacent = request.prompt_path.with_name(request.prompt_path.name + ".meta.json")
@@ -658,10 +1034,35 @@ def _validate_request(request: AgentInvocationRequest) -> None:
658
1034
  _project_relative(request.duty_root, request.project_root, must_exist=True)
659
1035
  _project_relative(request.prompt_path, request.project_root, must_exist=False)
660
1036
  _project_relative(request.metadata_path, request.project_root, must_exist=False)
661
- if request.run_manifest_path is None:
662
- _validate_standalone_identity(request)
663
- else:
664
- _validate_run_identity(request)
1037
+
1038
+
1039
+ def _validate_unbound_v2_run_request(request: AgentInvocationRequest) -> None:
1040
+ if request.run_manifest_path is None or request.purpose is not None:
1041
+ raise AgentInvocationError("unbound prompt requires a run invocation")
1042
+ if _request_identity_version(request) != 2:
1043
+ raise AgentInvocationError("unbound prompt requires v2 execution identity")
1044
+ if not request.assignment_ref:
1045
+ raise AgentInvocationError("run assignment reference is required")
1046
+ if any((
1047
+ request.participant_ref,
1048
+ request.role_execution_ref,
1049
+ request.duty_id,
1050
+ request.invocation_ref,
1051
+ )):
1052
+ raise AgentInvocationError("unbound prompt cannot carry execution identity")
1053
+ if not request.worker_id or not _SLUG_RE.fullmatch(request.worker_id):
1054
+ raise AgentInvocationError("run worker ID must be a non-empty slug")
1055
+ suffix = request.assignment_ref.partition("/")[2]
1056
+ if suffix and request.assignment_ref.split("/", 1)[0] in {"initial", "reverify"}:
1057
+ if suffix != request.worker_id:
1058
+ raise AgentInvocationError(
1059
+ "run worker ID does not match assignment reference"
1060
+ )
1061
+ manifest = _load_json_object(request.run_manifest_path, "run manifest")
1062
+ contract = manifest.get("agentContract")
1063
+ if not isinstance(contract, Mapping):
1064
+ raise AgentInvocationError("run manifest has no agent contract")
1065
+ _validate_manifest_contract(request, manifest, contract)
665
1066
 
666
1067
 
667
1068
  def _validate_assignment(assignment: AgentModelAssignment) -> None:
@@ -697,6 +1098,13 @@ def _validate_standalone_identity(request: AgentInvocationRequest) -> None:
697
1098
  raise AgentInvocationError("standalone invocation ID must be a slug")
698
1099
  if request.worker_id is not None or request.assignment_ref is not None:
699
1100
  raise AgentInvocationError("standalone invocation cannot bind a run worker")
1101
+ if any((
1102
+ request.participant_ref,
1103
+ request.role_execution_ref,
1104
+ request.duty_id,
1105
+ request.invocation_ref,
1106
+ )):
1107
+ raise AgentInvocationError("standalone invocation cannot bind v2 execution identity")
700
1108
  root = (
701
1109
  request.project_root
702
1110
  / ".okstra"
@@ -712,8 +1120,6 @@ def _validate_standalone_identity(request: AgentInvocationRequest) -> None:
712
1120
  def _validate_run_identity(request: AgentInvocationRequest) -> None:
713
1121
  if request.purpose is not None:
714
1122
  raise AgentInvocationError("run invocation cannot define standalone purpose")
715
- if not request.worker_id or not _SLUG_RE.fullmatch(request.worker_id):
716
- raise AgentInvocationError("run worker ID must be a non-empty slug")
717
1123
  if not request.assignment_ref:
718
1124
  raise AgentInvocationError("run assignment reference is required")
719
1125
  _project_relative(
@@ -721,12 +1127,74 @@ def _validate_run_identity(request: AgentInvocationRequest) -> None:
721
1127
  request.project_root,
722
1128
  must_exist=True,
723
1129
  )
1130
+ if _request_identity_version(request) == 2:
1131
+ _validate_v2_run_identity(request)
1132
+ return
1133
+ if any((
1134
+ request.participant_ref,
1135
+ request.role_execution_ref,
1136
+ request.duty_id,
1137
+ request.invocation_ref,
1138
+ )):
1139
+ raise AgentInvocationError("v1 run invocation cannot bind v2 execution identity")
1140
+ if not request.worker_id or not _SLUG_RE.fullmatch(request.worker_id):
1141
+ raise AgentInvocationError("run worker ID must be a non-empty slug")
724
1142
  suffix = request.assignment_ref.partition("/")[2]
725
1143
  if suffix and request.assignment_ref.split("/", 1)[0] in {"initial", "reverify"}:
726
1144
  if suffix != request.worker_id:
727
1145
  raise AgentInvocationError("run worker ID does not match assignment reference")
728
1146
 
729
1147
 
1148
+ def _request_identity_version(request: AgentInvocationRequest) -> int:
1149
+ if request.run_manifest_path is None:
1150
+ return 1
1151
+ manifest = _load_json_object(request.run_manifest_path, "run manifest")
1152
+ schema = manifest.get("schemaVersion")
1153
+ identity = manifest.get("executionIdentityVersion")
1154
+ if schema == "2.0" and identity == 2:
1155
+ return 2
1156
+ if schema == "2.0" or identity is not None:
1157
+ raise AgentInvocationError("run manifest mixes v1 and v2 execution identity")
1158
+ if schema not in (None, 1, "1", "1.0"):
1159
+ raise AgentInvocationError("run manifest execution identity version is unsupported")
1160
+ return 1
1161
+
1162
+
1163
+ def _validate_v2_run_identity(request: AgentInvocationRequest) -> None:
1164
+ fields = (
1165
+ request.participant_ref,
1166
+ request.role_execution_ref,
1167
+ request.duty_id,
1168
+ request.invocation_ref,
1169
+ )
1170
+ if any(not isinstance(value, str) or not value.strip() for value in fields):
1171
+ raise AgentInvocationError("v2 execution identity references are required")
1172
+ if request.invocation_ref != request.invocation_id:
1173
+ raise AgentInvocationError("v2 execution identity invocation reference is invalid")
1174
+ if (
1175
+ not isinstance(request.attempt, int)
1176
+ or isinstance(request.attempt, bool)
1177
+ or request.attempt < 1
1178
+ ):
1179
+ raise AgentInvocationError("v2 execution identity attempt must be positive")
1180
+ manifest = _load_json_object(request.run_manifest_path, "run manifest")
1181
+ identity = invocation_execution_identity_from_manifest(
1182
+ manifest,
1183
+ assignment=request.assignment,
1184
+ assignment_ref=request.assignment_ref,
1185
+ duty_id=request.duty_id,
1186
+ role_execution_ref=request.role_execution_ref,
1187
+ )
1188
+ if (
1189
+ identity is None
1190
+ or identity.participant_ref != request.participant_ref
1191
+ or identity.role_execution_ref != request.role_execution_ref
1192
+ ):
1193
+ raise AgentInvocationError(
1194
+ "v2 execution identity does not match a canonical role execution"
1195
+ )
1196
+
1197
+
730
1198
  def _render_prompt(
731
1199
  request: AgentInvocationRequest,
732
1200
  common: DutyContract,
@@ -799,7 +1267,7 @@ def _metadata_payload(
799
1267
  if request.run_manifest_path is not None
800
1268
  else None
801
1269
  )
802
- return {
1270
+ payload: dict[str, object] = {
803
1271
  "schemaVersion": 1,
804
1272
  "invocationId": request.invocation_id,
805
1273
  "audience": request.audience,
@@ -832,6 +1300,31 @@ def _metadata_payload(
832
1300
  },
833
1301
  "digests": dict(digests),
834
1302
  }
1303
+ if _request_identity_version(request) == 2:
1304
+ manifest = _load_json_object(request.run_manifest_path, "run manifest")
1305
+ identity = invocation_execution_identity_from_manifest(
1306
+ manifest,
1307
+ assignment=request.assignment,
1308
+ assignment_ref=request.assignment_ref,
1309
+ duty_id=request.duty_id,
1310
+ role_execution_ref=request.role_execution_ref,
1311
+ )
1312
+ if identity is None:
1313
+ raise AgentInvocationError(
1314
+ "v2 execution identity does not match a canonical role execution"
1315
+ )
1316
+ payload.update({
1317
+ "schemaVersion": "2.0",
1318
+ "executionIdentityVersion": 2,
1319
+ "invocationRef": request.invocation_ref,
1320
+ "participantRef": request.participant_ref,
1321
+ "roleExecutionRef": request.role_execution_ref,
1322
+ "executionLabel": identity.execution_label,
1323
+ "dutyId": request.duty_id,
1324
+ "attempt": request.attempt,
1325
+ })
1326
+ payload.pop("workerId")
1327
+ return payload
835
1328
 
836
1329
 
837
1330
  def _reservation_spec(
@@ -850,7 +1343,7 @@ def _reservation_spec(
850
1343
  reservation_value,
851
1344
  must_exist=False,
852
1345
  )
853
- reservation = {
1346
+ reservation: dict[str, object] = {
854
1347
  "schemaVersion": 1,
855
1348
  "invocationId": request.invocation_id,
856
1349
  "workerId": request.worker_id,
@@ -868,6 +1361,17 @@ def _reservation_spec(
868
1361
  must_exist=False,
869
1362
  ),
870
1363
  }
1364
+ if _request_identity_version(request) == 2:
1365
+ reservation.update({
1366
+ "schemaVersion": "2.0",
1367
+ "executionIdentityVersion": 2,
1368
+ "invocationRef": request.invocation_ref,
1369
+ "participantRef": request.participant_ref,
1370
+ "roleExecutionRef": request.role_execution_ref,
1371
+ "dutyId": request.duty_id,
1372
+ "attempt": request.attempt,
1373
+ })
1374
+ reservation.pop("workerId")
871
1375
  return reservation_root, reservation
872
1376
 
873
1377
 
@@ -894,7 +1398,9 @@ def _validate_manifest_contract(
894
1398
  if isinstance(assignments, dict)
895
1399
  else None
896
1400
  )
897
- if manifest_assignment != _assignment_payload(request.assignment):
1401
+ if _model_assignment_projection(manifest_assignment) != _assignment_payload(
1402
+ request.assignment
1403
+ ):
898
1404
  raise AgentInvocationError("model assignment does not match run manifest")
899
1405
  _validate_authorized_prompt_path(request, contract)
900
1406
 
@@ -1114,6 +1620,14 @@ def _prompt_lock_path(prompt_path: Path) -> Path:
1114
1620
  return prompt_path.with_name(prompt_path.name + ".publish.lock")
1115
1621
 
1116
1622
 
1623
+ def _publish_or_reuse_exact(path: Path, body: bytes) -> None:
1624
+ if path.exists():
1625
+ if path.read_bytes() != body:
1626
+ raise _existing_conflict(f"published path differs: {path}")
1627
+ return
1628
+ _publish_exclusive(path, body)
1629
+
1630
+
1117
1631
  def _publish_exclusive(path: Path, body: bytes, *, replace: bool = False) -> None:
1118
1632
  path.parent.mkdir(parents=True, exist_ok=True)
1119
1633
  temp_path: Path | None = None
@@ -1150,13 +1664,20 @@ def _verify_expected_identity(
1150
1664
  expected_worker_id: str | None,
1151
1665
  expected_assignment_ref: str | None,
1152
1666
  expected_audience: AgentAudience | None,
1667
+ expected_participant_ref: str | None,
1668
+ expected_role_execution_ref: str | None,
1669
+ expected_invocation_ref: str | None,
1153
1670
  ) -> list[str]:
1154
1671
  expected = {
1155
1672
  "invocationId": expected_invocation_id,
1156
- "workerId": expected_worker_id,
1157
1673
  "assignmentRef": expected_assignment_ref,
1158
1674
  "audience": expected_audience,
1675
+ "participantRef": expected_participant_ref,
1676
+ "roleExecutionRef": expected_role_execution_ref,
1677
+ "invocationRef": expected_invocation_ref,
1159
1678
  }
1679
+ if metadata.get("schemaVersion") == 1:
1680
+ expected["workerId"] = expected_worker_id
1160
1681
  errors = []
1161
1682
  for key, value in expected.items():
1162
1683
  if value is not None and metadata.get(key) != value:
@@ -1371,6 +1892,8 @@ def _verify_run_manifest_contract(
1371
1892
  if not isinstance(allowed, list) or metadata["audience"] not in allowed:
1372
1893
  errors.append("audience is not allowed by run manifest")
1373
1894
  errors.extend(_verify_manifest_assignment(metadata, manifest))
1895
+ if metadata.get("executionIdentityVersion") == 2:
1896
+ errors.extend(_verify_manifest_execution_identity(metadata, manifest))
1374
1897
  return errors
1375
1898
 
1376
1899
 
@@ -1382,11 +1905,39 @@ def _verify_manifest_assignment(
1382
1905
  reference = metadata["assignmentRef"]
1383
1906
  if not isinstance(assignments, dict) or reference not in assignments:
1384
1907
  return ["assignment reference is missing from run manifest"]
1385
- if assignments[reference] != metadata["modelAssignment"]:
1908
+ if _model_assignment_projection(assignments[reference]) != metadata["modelAssignment"]:
1386
1909
  return ["model assignment does not match run manifest"]
1387
1910
  return []
1388
1911
 
1389
1912
 
1913
+ def _verify_manifest_execution_identity(
1914
+ metadata: Mapping[str, object],
1915
+ manifest: Mapping[str, object],
1916
+ ) -> list[str]:
1917
+ if metadata["dutyContract"]["id"] != metadata["dutyId"]:
1918
+ return ["v2 duty identity does not match duty contract"]
1919
+ try:
1920
+ identity = invocation_execution_identity_from_manifest(
1921
+ manifest,
1922
+ assignment=agent_model_assignment_from_payload(
1923
+ metadata["modelAssignment"]
1924
+ ),
1925
+ assignment_ref=str(metadata["assignmentRef"]),
1926
+ duty_id=str(metadata["dutyId"]),
1927
+ role_execution_ref=str(metadata["roleExecutionRef"]),
1928
+ )
1929
+ except AgentInvocationError as exc:
1930
+ return [str(exc)]
1931
+ if (
1932
+ identity is None
1933
+ or identity.participant_ref != metadata["participantRef"]
1934
+ or identity.role_execution_ref != metadata["roleExecutionRef"]
1935
+ or identity.execution_label != metadata["executionLabel"]
1936
+ ):
1937
+ return ["v2 identity does not match a canonical role execution"]
1938
+ return []
1939
+
1940
+
1390
1941
  def _verify_duty_snapshot(
1391
1942
  metadata: Mapping[str, object],
1392
1943
  project_root: Path,
@@ -1473,23 +2024,57 @@ def _verify_reservation(
1473
2024
  return ["invocation reservation is missing or invalid"]
1474
2025
  prompt = metadata["prompt"]
1475
2026
  assert isinstance(prompt, dict)
1476
- expected = {
2027
+ expected: dict[str, object] = {
1477
2028
  "schemaVersion": 1,
1478
2029
  "invocationId": metadata["invocationId"],
1479
- "workerId": metadata["workerId"],
1480
2030
  "assignmentRef": metadata["assignmentRef"],
1481
2031
  "audience": metadata["audience"],
1482
2032
  "dispatchKind": metadata["dispatchKind"],
1483
2033
  "promptPath": prompt["path"],
1484
2034
  "metadataPath": f"{prompt['path']}.meta.json",
1485
2035
  }
2036
+ if metadata.get("executionIdentityVersion") == 2:
2037
+ expected.update({
2038
+ "schemaVersion": "2.0",
2039
+ "executionIdentityVersion": 2,
2040
+ "invocationRef": metadata["invocationRef"],
2041
+ "participantRef": metadata["participantRef"],
2042
+ "roleExecutionRef": metadata["roleExecutionRef"],
2043
+ "dutyId": metadata["dutyId"],
2044
+ "attempt": metadata["attempt"],
2045
+ })
2046
+ else:
2047
+ expected["workerId"] = metadata["workerId"]
1486
2048
  if actual != expected:
1487
2049
  return ["invocation reservation does not match invocation metadata"]
1488
2050
  return []
1489
2051
 
1490
2052
 
1491
2053
  def _metadata_schema_is_exact(metadata: Mapping[str, object]) -> bool:
1492
- if set(metadata) != _TOP_LEVEL_KEYS or metadata.get("schemaVersion") != 1:
2054
+ schema = metadata.get("schemaVersion")
2055
+ if schema == 1:
2056
+ if set(metadata) != _TOP_LEVEL_KEYS:
2057
+ return False
2058
+ elif schema == "2.0":
2059
+ if (
2060
+ set(metadata) != _V2_TOP_LEVEL_KEYS
2061
+ or metadata.get("executionIdentityVersion") != 2
2062
+ or not all(
2063
+ isinstance(metadata.get(key), str) and metadata[key]
2064
+ for key in (
2065
+ "invocationRef",
2066
+ "participantRef",
2067
+ "roleExecutionRef",
2068
+ "executionLabel",
2069
+ "dutyId",
2070
+ )
2071
+ )
2072
+ or not isinstance(metadata.get("attempt"), int)
2073
+ or isinstance(metadata.get("attempt"), bool)
2074
+ or metadata["attempt"] < 1
2075
+ ):
2076
+ return False
2077
+ else:
1493
2078
  return False
1494
2079
  for key, expected_keys in _NESTED_KEYS.items():
1495
2080
  value = metadata.get(key)
@@ -1509,6 +2094,15 @@ def _metadata_schema_is_exact(metadata: Mapping[str, object]) -> bool:
1509
2094
  }
1510
2095
 
1511
2096
 
2097
+ def _model_assignment_projection(payload: object) -> dict[str, object] | None:
2098
+ if not isinstance(payload, Mapping):
2099
+ return None
2100
+ keys = _NESTED_KEYS["modelAssignment"]
2101
+ if not keys.issubset(payload):
2102
+ return None
2103
+ return {key: payload[key] for key in keys}
2104
+
2105
+
1512
2106
  def _digest_paths(value: object, prefix: str = "") -> set[str]:
1513
2107
  if not isinstance(value, dict):
1514
2108
  return set()