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
@@ -32,8 +32,18 @@ from . import cmux
32
32
  from .agent_invocation import (
33
33
  AgentInvocationError,
34
34
  agent_model_assignment_from_payload,
35
+ invocation_metadata_identity,
36
+ v2_role_assignment_authority_errors,
35
37
  verify_agent_invocation,
36
38
  )
39
+ from .domain.role import RoleCatalogError, role_for_duty
40
+ from .execution_identity import Attempt, Invocation
41
+ from .execution_manifest import (
42
+ finish_attempt_mutation,
43
+ read_execution_manifest,
44
+ record_invocation_attempt,
45
+ )
46
+ from .execution_mutation_audit import ExecutionMutationAudit, MutationSnapshot
37
47
  from .final_report_paths import (
38
48
  final_report_data_path,
39
49
  final_report_markdown_path,
@@ -45,6 +55,15 @@ from .worker_prompt_contract import (
45
55
  validate_reverify_prompt,
46
56
  )
47
57
  from .worker_runner import LIVE, QUIET
58
+ from .worker_request import verifier_extra_dirs
59
+ from .worker_artifact_paths import audit_sidecar_rel
60
+ from .wrapper_status import status_path_for_prompt
61
+ from .write_policy import (
62
+ build_invocation_write_contract,
63
+ planned_paths_from_run_manifest,
64
+ write_policy_from_payload,
65
+ )
66
+ from .convergence_store import write_json_atomic
48
67
 
49
68
  BACKEND_CLI_WRAPPER = "cli-wrapper"
50
69
  BACKEND_CMUX_PANE = "cmux-pane"
@@ -104,6 +123,10 @@ class DispatchError(Exception):
104
123
  """Raised when a worker dispatch request cannot be executed."""
105
124
 
106
125
 
126
+ class IncompletePromptWriteArtifacts(DispatchError):
127
+ """Raised when a worker prompt omits the standard artifact headers."""
128
+
129
+
107
130
  @dataclass(frozen=True)
108
131
  class WorkerJob:
109
132
  worker_id: str
@@ -137,6 +160,12 @@ class WorkerJob:
137
160
  # process and leaves no `agentName` behind, so without this its usage cannot
138
161
  # be attributed at all.
139
162
  session_id: str = ""
163
+ participant_ref: str = ""
164
+ role_execution_ref: str = ""
165
+ execution_label: str = ""
166
+ duty_id: str = ""
167
+ invocation_ref: str = ""
168
+ attempt: int = 0
140
169
 
141
170
  @property
142
171
  def command(self) -> list[str]:
@@ -152,6 +181,11 @@ class WorkerJob:
152
181
  "--presentation",
153
182
  self._presentation(),
154
183
  ]
184
+ if self.has_execution_identity:
185
+ argv.extend([
186
+ "--invocation-metadata",
187
+ str(self.prompt_metadata_path),
188
+ ])
155
189
  if self.session_id:
156
190
  argv += ["--session-id", self.session_id]
157
191
  return argv
@@ -164,7 +198,6 @@ class WorkerJob:
164
198
 
165
199
  def to_payload(self) -> dict[str, Any]:
166
200
  payload = {
167
- "workerId": self.worker_id,
168
201
  "provider": self.provider,
169
202
  "backend": self.backend,
170
203
  "modelExecutionValue": self.model_execution_value,
@@ -178,6 +211,19 @@ class WorkerJob:
178
211
  "dispatchKind": self.dispatch_kind,
179
212
  "command": self.command,
180
213
  }
214
+ if self.has_execution_identity:
215
+ payload.update({
216
+ "schemaVersion": "2.0",
217
+ "executionIdentityVersion": 2,
218
+ "participantRef": self.participant_ref,
219
+ "roleExecutionRef": self.role_execution_ref,
220
+ "executionLabel": self.execution_label,
221
+ "dutyId": self.duty_id,
222
+ "invocationRef": self.invocation_ref,
223
+ "attempt": self.attempt,
224
+ })
225
+ else:
226
+ payload.update({"schemaVersion": 1, "workerId": self.worker_id})
181
227
  if self.invocation_id:
182
228
  payload.update({
183
229
  "invocationId": self.invocation_id,
@@ -190,6 +236,22 @@ class WorkerJob:
190
236
  })
191
237
  return payload
192
238
 
239
+ @property
240
+ def has_execution_identity(self) -> bool:
241
+ values = (
242
+ self.participant_ref,
243
+ self.role_execution_ref,
244
+ self.execution_label,
245
+ self.duty_id,
246
+ self.invocation_ref,
247
+ )
248
+ present = tuple(bool(value) for value in values)
249
+ if any(present) and not all(present):
250
+ raise DispatchError("worker job has partial v2 execution identity")
251
+ if all(present) and self.attempt < 1:
252
+ raise DispatchError("worker job v2 attempt must be positive")
253
+ return all(present)
254
+
193
255
  @property
194
256
  def digests(self) -> dict[str, str]:
195
257
  return {
@@ -438,9 +500,22 @@ def record_verified_agent_dispatch(
438
500
  assignment = agent_model_assignment_from_payload(assignments.get(assignment_ref))
439
501
  except AgentInvocationError as exc:
440
502
  raise DispatchError(str(exc)) from exc
441
- worker_id = require_string(metadata, "workerId")
503
+ try:
504
+ execution_identity = invocation_metadata_identity(metadata)
505
+ except AgentInvocationError as exc:
506
+ raise DispatchError(str(exc)) from exc
507
+ worker_id = (
508
+ require_string(metadata, "workerId")
509
+ if execution_identity is None
510
+ else None
511
+ )
442
512
  audience = require_string(metadata, "audience")
443
513
  invocation_id = require_string(metadata, "invocationId")
514
+ if (
515
+ execution_identity is not None
516
+ and execution_identity.invocation_ref != invocation_id
517
+ ):
518
+ raise DispatchError("v2 invocation reference does not match invocation ID")
444
519
  errors = verify_agent_invocation(
445
520
  metadata_path,
446
521
  project_root=project_root,
@@ -450,6 +525,15 @@ def record_verified_agent_dispatch(
450
525
  expected_worker_id=worker_id,
451
526
  expected_assignment_ref=assignment_ref,
452
527
  expected_audience=audience,
528
+ expected_participant_ref=(
529
+ execution_identity.participant_ref if execution_identity else None
530
+ ),
531
+ expected_role_execution_ref=(
532
+ execution_identity.role_execution_ref if execution_identity else None
533
+ ),
534
+ expected_invocation_ref=(
535
+ execution_identity.invocation_ref if execution_identity else None
536
+ ),
453
537
  )
454
538
  if errors:
455
539
  raise DispatchError("agent invocation verification failed: " + "; ".join(errors))
@@ -478,12 +562,15 @@ def record_verified_agent_dispatch(
478
562
  if not isinstance(prompt, Mapping):
479
563
  raise DispatchError("agent invocation prompt is missing")
480
564
  prompt_path = require_string(prompt, "path")
481
- # One verified record per invocation: the conflict guard below refuses a
482
- # second write under this id, so this path never numbers a retry.
483
- dispatch_id = build_dispatch_id(invocation_id, 1)
565
+ # Identity-bearing records carry the stored attempt. A path without
566
+ # identity still records the first attempt; the conflict guard below
567
+ # refuses a second write under that id.
568
+ dispatch_id = build_dispatch_id(
569
+ invocation_id,
570
+ execution_identity.attempt if execution_identity else 1,
571
+ )
484
572
  record = {
485
573
  "dispatchId": dispatch_id,
486
- "workerId": worker_id,
487
574
  "audience": audience,
488
575
  "invocationId": invocation_id,
489
576
  "assignmentRef": assignment_ref,
@@ -496,7 +583,99 @@ def record_verified_agent_dispatch(
496
583
  "promptDeliveryVerified": False,
497
584
  "status": "dispatched",
498
585
  }
586
+ if execution_identity is None:
587
+ record["workerId"] = worker_id
588
+ else:
589
+ record.update({
590
+ "schemaVersion": "2.0",
591
+ "executionIdentityVersion": 2,
592
+ "invocationRef": execution_identity.invocation_ref,
593
+ "participantRef": execution_identity.participant_ref,
594
+ "roleExecutionRef": execution_identity.role_execution_ref,
595
+ "executionLabel": execution_identity.execution_label,
596
+ "dutyId": execution_identity.duty_id,
597
+ "attempt": execution_identity.attempt,
598
+ "dispatchKind": require_string(metadata, "dispatchKind"),
599
+ })
499
600
  team_state_path = resolve_required_path(project_root, manifest, "teamStatePath")
601
+ write_policy = None
602
+ write_enforcement = None
603
+ mutation_snapshot_path = metadata_path.with_suffix(
604
+ metadata_path.suffix + ".mutation-audit.json"
605
+ )
606
+ if execution_identity is not None:
607
+ write_policy, write_enforcement = _agent_write_contract(
608
+ project_root,
609
+ run_manifest_path,
610
+ manifest,
611
+ execution_identity.role_execution_ref,
612
+ resolve_project_path(project_root, prompt_path),
613
+ assignment_ref,
614
+ )
615
+ record.update({
616
+ "writePolicyDigest": write_policy.digest,
617
+ "writeEnforcement": write_enforcement.to_payload(),
618
+ "mutationAuditSnapshotPath": (
619
+ _relative_project_path(project_root, mutation_snapshot_path)
620
+ if write_enforcement.mutation_audit == "batch"
621
+ else ""
622
+ ),
623
+ })
624
+ existing_state = load_json_object(team_state_path, "team-state")
625
+ existing = [
626
+ item
627
+ for item in existing_state.get("agentDispatches") or []
628
+ if isinstance(item, Mapping) and item.get("dispatchId") == dispatch_id
629
+ ]
630
+ if existing:
631
+ if len(existing) != 1 or dict(existing[0]) != record:
632
+ raise DispatchError(f"agent dispatch ID conflicts: {dispatch_id}")
633
+ return record
634
+ if execution_identity is not None:
635
+ dispatch_kind = require_string(metadata, "dispatchKind")
636
+ prompt_digest = require_string(digests, "promptDigest")
637
+ if write_policy is None or write_enforcement is None:
638
+ raise DispatchError("agent write contract was not prepared")
639
+ record_invocation_attempt(
640
+ run_manifest_path,
641
+ Invocation(
642
+ invocation_ref=execution_identity.invocation_ref,
643
+ participant_ref=execution_identity.participant_ref,
644
+ role_execution_ref=execution_identity.role_execution_ref,
645
+ source_invocation_ref=None,
646
+ recovery_ref=None,
647
+ duty_id=execution_identity.duty_id,
648
+ dispatch_kind=dispatch_kind,
649
+ round=_execution_dispatch_round(dispatch_kind),
650
+ input_digest=prompt_digest,
651
+ write_policy=write_policy.to_payload(),
652
+ write_policy_digest=write_policy.digest,
653
+ write_enforcement=write_enforcement.to_payload(),
654
+ ),
655
+ Attempt(
656
+ invocation_ref=execution_identity.invocation_ref,
657
+ attempt=execution_identity.attempt,
658
+ started_at=utc_now(),
659
+ finished_at=None,
660
+ status="started",
661
+ result_path=None,
662
+ error_path=None,
663
+ change_summary={},
664
+ evidence_seal_ref=None,
665
+ ),
666
+ task_key=require_string(manifest, "taskKey"),
667
+ )
668
+ if write_enforcement.mutation_audit == "batch":
669
+ snapshot = ExecutionMutationAudit().snapshot(
670
+ (write_policy,),
671
+ orchestrator_paths=(
672
+ run_manifest_path,
673
+ team_state_path,
674
+ Path(f"{team_state_path}.lock"),
675
+ mutation_snapshot_path,
676
+ ),
677
+ )
678
+ write_json_atomic(mutation_snapshot_path, snapshot.to_payload())
500
679
  with _team_state_lock(team_state_path):
501
680
  team_state = load_json_object(team_state_path, "team-state")
502
681
  dispatches = team_state.setdefault("agentDispatches", [])
@@ -515,13 +694,191 @@ def record_verified_agent_dispatch(
515
694
  return record
516
695
 
517
696
 
697
+ def _agent_write_contract(
698
+ project_root: Path,
699
+ manifest_path: Path,
700
+ authority: Mapping[str, Any],
701
+ role_execution_ref: str,
702
+ prompt_path: Path,
703
+ assignment_ref: str,
704
+ ):
705
+ execution_manifest = read_execution_manifest(manifest_path)
706
+ execution = next(
707
+ (
708
+ row for row in execution_manifest.role_executions
709
+ if row.role_execution_ref == role_execution_ref
710
+ ),
711
+ None,
712
+ )
713
+ capability = (
714
+ execution.binding.worker_write_capability
715
+ if execution is not None and execution.binding is not None
716
+ else None
717
+ )
718
+ if execution is None:
719
+ raise DispatchError("agent write policy has no role execution")
720
+ if execution.role == "leader":
721
+ artifacts = _leader_artifact_paths(project_root, authority)
722
+ worktree = None
723
+ maximum_precision = "none"
724
+ elif capability is not None:
725
+ try:
726
+ artifacts, worktree = _prompt_write_paths(project_root, prompt_path)
727
+ except IncompletePromptWriteArtifacts:
728
+ artifacts, worktree = _manifest_worker_write_paths(
729
+ project_root, authority, assignment_ref, prompt_path
730
+ )
731
+ maximum_precision = capability.max_boundary_precision
732
+ else:
733
+ raise DispatchError("agent write policy has no runner capability")
734
+ try:
735
+ return build_invocation_write_contract(
736
+ role=execution.role,
737
+ project_root=project_root,
738
+ worktree=worktree,
739
+ artifact_paths=artifacts,
740
+ maximum_precision=maximum_precision,
741
+ planned_paths=(
742
+ planned_paths_from_run_manifest(project_root, authority)
743
+ if execution.role == "implementer"
744
+ else ()
745
+ ),
746
+ auxiliary_roots=verifier_extra_dirs(execution.role),
747
+ validated_auxiliary_roots=verifier_extra_dirs(execution.role),
748
+ )
749
+ except (OSError, ValueError) as exc:
750
+ raise DispatchError(f"agent write policy is invalid: {exc}") from exc
751
+
752
+
753
+ def _leader_artifact_paths(
754
+ project_root: Path, authority: Mapping[str, Any]
755
+ ) -> tuple[Path, ...]:
756
+ value = authority.get("taskRootPath") or authority.get("runDirectoryPath")
757
+ if not isinstance(value, str) or not value.strip():
758
+ raise DispatchError("leader write policy has no artifact root")
759
+ artifact_home = project_root / ".okstra"
760
+ if not artifact_home.is_dir():
761
+ raise DispatchError("leader write policy artifact home does not exist")
762
+ return (artifact_home,)
763
+
764
+
765
+ def _manifest_worker_write_paths(
766
+ project_root: Path,
767
+ authority: Mapping[str, Any],
768
+ assignment_ref: str,
769
+ prompt_path: Path,
770
+ ) -> tuple[tuple[Path, ...], Path | None]:
771
+ prefix = "initial/"
772
+ if not assignment_ref.startswith(prefix):
773
+ raise DispatchError("agent prompt write artifact headers are incomplete")
774
+ worker_id = assignment_ref.removeprefix(prefix)
775
+ team_state_path = resolve_required_path(
776
+ project_root, authority, "teamStatePath"
777
+ )
778
+ team_state = load_json_object(team_state_path, "team-state")
779
+ matches = [
780
+ row for row in team_state.get("workers") or []
781
+ if isinstance(row, Mapping) and row.get("workerId") == worker_id
782
+ ]
783
+ if len(matches) != 1:
784
+ raise DispatchError("agent prompt has no unique worker artifact authority")
785
+ result = resolve_project_path(
786
+ project_root, require_string(matches[0], "resultPath")
787
+ )
788
+ paths = {
789
+ *dispatch_completion_paths(worker_id, result, authority, project_root),
790
+ result,
791
+ Path(audit_sidecar_rel(str(result))),
792
+ status_path_for_prompt(prompt_path),
793
+ prompt_path.with_suffix(prompt_path.suffix + ".log"),
794
+ }
795
+ active_value = authority.get("activeRunContextPath")
796
+ active = (
797
+ load_json_object(
798
+ resolve_project_path(project_root, active_value),
799
+ "active run context",
800
+ )
801
+ if isinstance(active_value, str) and active_value
802
+ else {}
803
+ )
804
+ error_logs = active.get("errorLogs")
805
+ sidecars = (
806
+ error_logs.get("sidecarsByWorkerId")
807
+ if isinstance(error_logs, Mapping)
808
+ else None
809
+ )
810
+ error_value = sidecars.get(worker_id) if isinstance(sidecars, Mapping) else None
811
+ if isinstance(error_value, str) and error_value:
812
+ paths.add(resolve_project_path(project_root, error_value))
813
+ worktree_value = worktree_path(authority, active)
814
+ worktree = Path(worktree_value) if worktree_value else None
815
+ return tuple(sorted(paths, key=str)), worktree
816
+
817
+
818
+ def _prompt_write_paths(
819
+ project_root: Path, prompt_path: Path
820
+ ) -> tuple[tuple[Path, ...], Path | None]:
821
+ try:
822
+ lines = prompt_path.read_text(encoding="utf-8").splitlines()
823
+ except (OSError, UnicodeError) as exc:
824
+ raise DispatchError("agent prompt cannot be read for write policy") from exc
825
+ values: dict[str, str] = {}
826
+ labels = {
827
+ "Result Path",
828
+ "Worker Result Path",
829
+ "Audit sidecar path",
830
+ "Errors sidecar path",
831
+ "Worktree",
832
+ }
833
+ for line in lines:
834
+ for label in labels:
835
+ prefix = f"**{label}:** "
836
+ if line.startswith(prefix):
837
+ values[label] = line.removeprefix(prefix).strip()
838
+ required = {"Result Path", "Audit sidecar path", "Errors sidecar path"}
839
+ if not required <= values.keys():
840
+ raise IncompletePromptWriteArtifacts(
841
+ "agent prompt write artifact headers are incomplete"
842
+ )
843
+ artifact_values = [values[label] for label in sorted(required)]
844
+ if "Worker Result Path" in values:
845
+ artifact_values.append(values["Worker Result Path"])
846
+ artifacts = {
847
+ _project_or_absolute(project_root, value) for value in artifact_values
848
+ }
849
+ artifacts.add(status_path_for_prompt(prompt_path))
850
+ artifacts.add(prompt_path.with_suffix(prompt_path.suffix + ".log"))
851
+ worktree_value = values.get("Worktree")
852
+ worktree = (
853
+ _project_or_absolute(project_root, worktree_value)
854
+ if worktree_value
855
+ else None
856
+ )
857
+ return tuple(sorted(artifacts, key=str)), worktree
858
+
859
+
860
+ def _project_or_absolute(project_root: Path, value: str) -> Path:
861
+ path = Path(value)
862
+ return path if path.is_absolute() else project_root / path
863
+
864
+
865
+ def _execution_dispatch_round(dispatch_kind: str) -> int:
866
+ prefix = "reverify-r"
867
+ if not dispatch_kind.startswith(prefix):
868
+ return 1
869
+ suffix = dispatch_kind[len(prefix):]
870
+ if not suffix.isdigit() or int(suffix) < 1:
871
+ raise DispatchError(f"invalid reverify dispatch kind: {dispatch_kind}")
872
+ return int(suffix)
873
+
874
+
518
875
  def link_agent_dispatch_result(
519
876
  *,
520
877
  project_root: Path,
521
878
  run_manifest_path: Path,
522
879
  dispatch_id: str,
523
880
  result_path: Path,
524
- ) -> dict[str, str]:
881
+ ) -> dict[str, Any]:
525
882
  """Link an existing result artifact to exactly one verified dispatch."""
526
883
  project_root = project_root.resolve()
527
884
  manifest_path = resolve_project_path(
@@ -538,7 +895,20 @@ def link_agent_dispatch_result(
538
895
  raise DispatchError("agent result link must stay inside the current run")
539
896
  result_relative = _relative_project_path(project_root, resolved_result)
540
897
  team_state_path = resolve_required_path(project_root, manifest, "teamStatePath")
541
- link = {"dispatchId": dispatch_id, "resultPath": result_relative}
898
+ preview = load_json_object(team_state_path, "team-state")
899
+ native_dispatches = [
900
+ item for item in preview.get("agentDispatches") or []
901
+ if isinstance(item, Mapping) and item.get("dispatchId") == dispatch_id
902
+ ]
903
+ if len(native_dispatches) == 1:
904
+ _finish_native_agent_attempt(
905
+ project_root=project_root,
906
+ manifest_path=manifest_path,
907
+ manifest=manifest,
908
+ dispatch=native_dispatches[0],
909
+ result_path=resolved_result,
910
+ result_relative=result_relative,
911
+ )
542
912
  with _team_state_lock(team_state_path):
543
913
  team_state = load_json_object(team_state_path, "team-state")
544
914
  dispatches = [
@@ -554,6 +924,21 @@ def link_agent_dispatch_result(
554
924
  raise DispatchError(
555
925
  f"agent result link requires exactly one dispatch record: {dispatch_id}"
556
926
  )
927
+ dispatch = dispatches[0]
928
+ link: dict[str, Any] = {
929
+ "dispatchId": dispatch_id,
930
+ "resultPath": result_relative,
931
+ }
932
+ identity = worker_execution_identity(dispatch)
933
+ if identity is not None:
934
+ link.update({
935
+ "participantRef": identity["participant_ref"],
936
+ "roleExecutionRef": identity["role_execution_ref"],
937
+ "executionLabel": identity["execution_label"],
938
+ "dutyId": identity["duty_id"],
939
+ "invocationRef": identity["invocation_ref"],
940
+ "attempt": identity["attempt"],
941
+ })
557
942
  links = team_state.setdefault("agentResultLinks", [])
558
943
  if not isinstance(links, list):
559
944
  raise DispatchError("team-state agentResultLinks must be an array")
@@ -591,6 +976,137 @@ def link_agent_dispatch_result(
591
976
  return link
592
977
 
593
978
 
979
+ def _finish_native_agent_attempt(
980
+ *,
981
+ project_root: Path,
982
+ manifest_path: Path,
983
+ manifest: Mapping[str, Any],
984
+ dispatch: Mapping[str, Any],
985
+ result_path: Path,
986
+ result_relative: str,
987
+ ) -> None:
988
+ identity = worker_execution_identity(dispatch)
989
+ if identity is None:
990
+ return
991
+ execution = read_execution_manifest(manifest_path)
992
+ invocation = next(
993
+ (
994
+ row for row in execution.invocations
995
+ if row.invocation_ref == identity["invocation_ref"]
996
+ ),
997
+ None,
998
+ )
999
+ if invocation is None:
1000
+ raise DispatchError("agent result has no canonical invocation policy")
1001
+ if dispatch.get("writePolicyDigest") != invocation.write_policy_digest:
1002
+ raise DispatchError("agent dispatch write policy digest changed")
1003
+ if dispatch.get("writeEnforcement") != invocation.write_enforcement:
1004
+ raise DispatchError("agent dispatch write enforcement changed")
1005
+ policy = write_policy_from_payload(invocation.write_policy)
1006
+ snapshot_value = string_value(dispatch.get("mutationAuditSnapshotPath"))
1007
+ if snapshot_value:
1008
+ snapshot_payload = load_json_object(
1009
+ resolve_project_path(project_root, snapshot_value),
1010
+ "mutation audit snapshot",
1011
+ )
1012
+ try:
1013
+ snapshot = MutationSnapshot.from_payload(snapshot_payload)
1014
+ except (KeyError, TypeError, ValueError) as exc:
1015
+ raise DispatchError("mutation audit snapshot is invalid") from exc
1016
+ result = ExecutionMutationAudit().compare(
1017
+ snapshot,
1018
+ (policy,),
1019
+ out_of_plan_edits=_native_out_of_plan_paths(result_path),
1020
+ attempt_succeeded=True,
1021
+ result_present=True,
1022
+ )
1023
+ summary = result.change_summary()
1024
+ status = result.status
1025
+ else:
1026
+ status = "ok"
1027
+ summary = {
1028
+ "mutationStatus": "ok",
1029
+ "changedPaths": [],
1030
+ "sourceChanged": False,
1031
+ "gitChanged": False,
1032
+ "attribution": "call",
1033
+ "retryAllowed": False,
1034
+ "violations": [],
1035
+ "beforeDigest": "",
1036
+ "afterDigest": "",
1037
+ }
1038
+ _close_native_attempt(
1039
+ manifest_path,
1040
+ manifest,
1041
+ identity,
1042
+ status,
1043
+ result_relative,
1044
+ summary,
1045
+ )
1046
+ if status != "ok":
1047
+ violations = summary.get("violations")
1048
+ detail = "; ".join(violations) if isinstance(violations, list) else status
1049
+ raise DispatchError(f"agent mutation audit failed: {detail or status}")
1050
+
1051
+
1052
+ def _close_native_attempt(
1053
+ manifest_path: Path,
1054
+ manifest: Mapping[str, Any],
1055
+ identity: Mapping[str, Any],
1056
+ status: str,
1057
+ result_path: str,
1058
+ summary: Mapping[str, Any],
1059
+ ) -> None:
1060
+ execution = read_execution_manifest(manifest_path)
1061
+ existing = next(
1062
+ (
1063
+ row for row in execution.attempts
1064
+ if row.invocation_ref == identity["invocation_ref"]
1065
+ and row.attempt == identity["attempt"]
1066
+ ),
1067
+ None,
1068
+ )
1069
+ if existing is not None and existing.finished_at is not None:
1070
+ if (
1071
+ existing.status == status
1072
+ and existing.result_path == result_path
1073
+ and dict(existing.change_summary) == dict(summary)
1074
+ ):
1075
+ return
1076
+ raise DispatchError("agent attempt terminal mutation result drift")
1077
+ finish_attempt_mutation(
1078
+ manifest_path,
1079
+ invocation_ref=identity["invocation_ref"],
1080
+ attempt=identity["attempt"],
1081
+ finished_at=utc_now(),
1082
+ status=status,
1083
+ result_path=result_path,
1084
+ error_path=None,
1085
+ change_summary=summary,
1086
+ task_key=require_string(manifest, "taskKey"),
1087
+ )
1088
+
1089
+
1090
+ def _native_out_of_plan_paths(result_path: Path) -> tuple[str, ...]:
1091
+ if result_path.suffix != ".json":
1092
+ return ()
1093
+ try:
1094
+ payload = json.loads(result_path.read_text(encoding="utf-8"))
1095
+ except (OSError, UnicodeError, json.JSONDecodeError):
1096
+ return ()
1097
+ implementation = payload.get("implementation") if isinstance(payload, Mapping) else None
1098
+ rows = implementation.get("outOfPlanEdits") if isinstance(implementation, Mapping) else None
1099
+ if not isinstance(rows, list):
1100
+ return ()
1101
+ return tuple(
1102
+ str(row["file"])
1103
+ for row in rows
1104
+ if isinstance(row, Mapping)
1105
+ and isinstance(row.get("file"), str)
1106
+ and row["file"]
1107
+ )
1108
+
1109
+
594
1110
  def reject_agent_dispatch_result(
595
1111
  *,
596
1112
  project_root: Path,
@@ -1045,6 +1561,121 @@ def _validate_agent_invocations(
1045
1561
  raise DispatchError("agent invocation contract: " + "; ".join(errors))
1046
1562
 
1047
1563
 
1564
+ def _v2_job_authority_errors(
1565
+ job: WorkerJob,
1566
+ metadata: Mapping[str, Any],
1567
+ manifest: Mapping[str, Any],
1568
+ ) -> list[str]:
1569
+ try:
1570
+ identity = invocation_metadata_identity(metadata)
1571
+ except AgentInvocationError as exc:
1572
+ return [str(exc)]
1573
+ if identity is None:
1574
+ return ["jobs file v2 identity has v1 invocation metadata"]
1575
+ errors: list[str] = []
1576
+ if job.invocation_ref != identity.invocation_ref:
1577
+ errors.append("jobs file invocationRef does not match invocation metadata")
1578
+ if (
1579
+ job.participant_ref != identity.participant_ref
1580
+ or job.role_execution_ref != identity.role_execution_ref
1581
+ or job.execution_label != identity.execution_label
1582
+ or job.duty_id != identity.duty_id
1583
+ or job.attempt != identity.attempt
1584
+ ):
1585
+ errors.append("jobs file v2 identity does not match invocation metadata")
1586
+ prompt = metadata.get("prompt")
1587
+ metadata_prompt_value = (
1588
+ string_value(prompt.get("path")) if isinstance(prompt, Mapping) else ""
1589
+ )
1590
+ metadata_prompt_path = resolve_project_path(
1591
+ job.project_root, metadata_prompt_value
1592
+ )
1593
+ if (
1594
+ not metadata_prompt_value
1595
+ or job.prompt_path.resolve() != metadata_prompt_path.resolve()
1596
+ ):
1597
+ errors.append("jobs file promptPath does not match invocation metadata")
1598
+ role_executions = manifest.get("roleExecutions")
1599
+ execution = next((
1600
+ row for row in role_executions
1601
+ if isinstance(row, Mapping)
1602
+ and row.get("roleExecutionRef") == identity.role_execution_ref
1603
+ ), None) if isinstance(role_executions, list) else None
1604
+ participants = manifest.get("participantAssignments")
1605
+ participant = next((
1606
+ row for row in participants
1607
+ if isinstance(row, Mapping)
1608
+ and row.get("participantRef") == identity.participant_ref
1609
+ ), None) if isinstance(participants, list) else None
1610
+ assignment = metadata.get("modelAssignment")
1611
+ try:
1612
+ duty_role = role_for_duty(job.duty_id)
1613
+ except RoleCatalogError:
1614
+ duty_role = ""
1615
+ if isinstance(assignment, Mapping):
1616
+ try:
1617
+ parsed_assignment = agent_model_assignment_from_payload(assignment)
1618
+ except AgentInvocationError as exc:
1619
+ errors.append(str(exc))
1620
+ else:
1621
+ errors.extend(
1622
+ "jobs file " + error
1623
+ for error in v2_role_assignment_authority_errors(
1624
+ manifest,
1625
+ role_execution_ref=identity.role_execution_ref,
1626
+ participant_ref=identity.participant_ref,
1627
+ assignment=parsed_assignment,
1628
+ )
1629
+ )
1630
+ if (
1631
+ not isinstance(execution, Mapping)
1632
+ or not isinstance(participant, Mapping)
1633
+ or not isinstance(assignment, Mapping)
1634
+ or execution.get("participantRef") != job.participant_ref
1635
+ or execution.get("executionLabel") != job.execution_label
1636
+ or execution.get("role") != job.role
1637
+ or execution.get("role") != duty_role
1638
+ or execution.get("provider") != job.provider
1639
+ or assignment.get("modelExecutionValue") != job.model_execution_value
1640
+ or metadata.get("dispatchKind") != job.dispatch_kind
1641
+ ):
1642
+ errors.append("jobs file v2 identity does not match role execution authority")
1643
+ return errors
1644
+
1645
+
1646
+ def _validate_v2_jobs_file_authority(
1647
+ manifest: Mapping[str, Any], jobs: Sequence[WorkerJob]
1648
+ ) -> None:
1649
+ if (
1650
+ manifest.get("schemaVersion") != "2.0"
1651
+ or manifest.get("executionIdentityVersion") != 2
1652
+ ):
1653
+ return
1654
+ _validate_agent_invocations(manifest, jobs)
1655
+ errors: list[str] = []
1656
+ for job in jobs:
1657
+ metadata = load_json_object(job.prompt_metadata_path, "agent metadata")
1658
+ errors.extend(
1659
+ f"{job.worker_id}: {error}"
1660
+ for error in _v2_job_authority_errors(job, metadata, manifest)
1661
+ )
1662
+ if errors:
1663
+ raise DispatchError("jobs file v2 authority: " + "; ".join(errors))
1664
+ role_executions = {
1665
+ string_value(row.get("roleExecutionRef")): row
1666
+ for row in manifest.get("roleExecutions") or []
1667
+ if isinstance(row, Mapping)
1668
+ }
1669
+ if any(
1670
+ isinstance(role_executions.get(job.role_execution_ref), Mapping)
1671
+ and role_executions[job.role_execution_ref].get("role") == "translator"
1672
+ for job in jobs
1673
+ ):
1674
+ raise DispatchError(
1675
+ "jobs file translator must use canonical reservation dispatch"
1676
+ )
1677
+
1678
+
1048
1679
  def worker_jobs_from_file(
1049
1680
  project_root: Path,
1050
1681
  jobs_file: Path,
@@ -1064,7 +1695,7 @@ def worker_jobs_from_file(
1064
1695
  workers = payload.get("workers")
1065
1696
  if not isinstance(workers, list):
1066
1697
  raise DispatchError("jobs file workers must be an array")
1067
- return [
1698
+ jobs = [
1068
1699
  _worker_job_from_file(
1069
1700
  project_root,
1070
1701
  item,
@@ -1078,6 +1709,8 @@ def worker_jobs_from_file(
1078
1709
  for item in workers
1079
1710
  if isinstance(item, Mapping)
1080
1711
  ]
1712
+ _validate_v2_jobs_file_authority(manifest, jobs)
1713
+ return jobs
1081
1714
 
1082
1715
 
1083
1716
  def _worker_job_from_file(
@@ -1091,8 +1724,17 @@ def _worker_job_from_file(
1091
1724
  resolve_wrapper: Callable[[str], Path],
1092
1725
  default_provider: Callable[[str], str],
1093
1726
  ) -> WorkerJob:
1094
- worker_id = require_string(item, "workerId")
1095
- provider = string_value(item.get("provider")) or default_provider(worker_id)
1727
+ identity = worker_execution_identity(item)
1728
+ worker_id = (
1729
+ require_string(item, "workerId")
1730
+ if identity is None
1731
+ else v2_worker_state_key(item)
1732
+ )
1733
+ provider = (
1734
+ string_value(item.get("provider")) or default_provider(worker_id)
1735
+ if identity is None
1736
+ else require_string(item, "provider")
1737
+ )
1096
1738
  prompt_path = resolve_project_path(
1097
1739
  project_root, require_string(item, "promptPath")
1098
1740
  )
@@ -1148,9 +1790,52 @@ def _worker_job_from_file(
1148
1790
  prompt_digest=string_value(digest_values.get("promptDigest")),
1149
1791
  host_model_value=host_model_value,
1150
1792
  enforcement_mode=string_value(item.get("enforcementMode")),
1793
+ **(identity or {}),
1151
1794
  )
1152
1795
 
1153
1796
 
1797
+ def worker_execution_identity(
1798
+ item: Mapping[str, Any],
1799
+ ) -> dict[str, Any] | None:
1800
+ identity_keys = {
1801
+ "executionIdentityVersion",
1802
+ "participantRef",
1803
+ "roleExecutionRef",
1804
+ "executionLabel",
1805
+ "dutyId",
1806
+ "invocationRef",
1807
+ }
1808
+ schema = item.get("schemaVersion")
1809
+ has_v2_fields = bool(identity_keys.intersection(item))
1810
+ if schema in (None, 1, "1", "1.0"):
1811
+ if has_v2_fields:
1812
+ raise DispatchError("jobs file worker mixes v1 and v2 identity")
1813
+ return None
1814
+ if schema != "2.0" or item.get("executionIdentityVersion") != 2:
1815
+ raise DispatchError("jobs file worker has unsupported identity version")
1816
+ if "workerId" in item:
1817
+ raise DispatchError("jobs file worker mixes v1 and v2 identity")
1818
+ attempt = item.get("attempt")
1819
+ if not isinstance(attempt, int) or isinstance(attempt, bool) or attempt < 1:
1820
+ raise DispatchError("jobs file v2 attempt must be positive")
1821
+ return {
1822
+ "participant_ref": require_string(item, "participantRef"),
1823
+ "role_execution_ref": require_string(item, "roleExecutionRef"),
1824
+ "execution_label": require_string(item, "executionLabel"),
1825
+ "duty_id": require_string(item, "dutyId"),
1826
+ "invocation_ref": require_string(item, "invocationRef"),
1827
+ "attempt": attempt,
1828
+ }
1829
+
1830
+
1831
+ def v2_worker_state_key(item: Mapping[str, Any]) -> str:
1832
+ assignment_ref = require_string(item, "assignmentRef")
1833
+ worker_id = assignment_ref.rsplit("/", 1)[-1]
1834
+ if not worker_id:
1835
+ raise DispatchError("jobs file v2 assignmentRef has no worker projection")
1836
+ return worker_id
1837
+
1838
+
1154
1839
  def utc_now() -> str:
1155
1840
  return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
1156
1841