okstra 0.174.0 → 0.175.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (374) 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 +4 -0
  232. package/docs/architecture.md +7 -1
  233. package/docs/cli.md +26 -6
  234. package/docs/contributor-change-matrix.md +2 -2
  235. package/docs/project-structure-overview.md +16 -7
  236. package/docs/task-process/README.md +2 -0
  237. package/docs/task-process/implementation-planning.md +1 -1
  238. package/docs/task-process/implementation.md +1 -1
  239. package/package.json +11 -5
  240. package/runtime/BUILD.json +2 -2
  241. package/runtime/agents/workers/claude-worker.md +2 -1
  242. package/runtime/agents/workers/report-writer-worker.md +3 -1
  243. package/runtime/agents/workers/translator-worker.md +3 -1
  244. package/runtime/bin/lib/okstra/cli.sh +12 -0
  245. package/runtime/bin/lib/okstra/globals.sh +3 -0
  246. package/runtime/bin/lib/okstra/usage.sh +4 -4
  247. package/runtime/bin/okstra-error-log.py +27 -0
  248. package/runtime/bin/okstra-provider-exec.py +217 -5
  249. package/runtime/bin/okstra.sh +10 -0
  250. package/runtime/prompts/lead/convergence.md +27 -18
  251. package/runtime/prompts/lead/okstra-lead-contract.md +3 -1
  252. package/runtime/prompts/lead/plan-body-verification.md +10 -3
  253. package/runtime/prompts/lead/team-contract.md +1 -1
  254. package/runtime/prompts/profiles/change-impact-analysis.md +16 -0
  255. package/runtime/prompts/profiles/error-analysis.md +20 -0
  256. package/runtime/prompts/profiles/feature-analysis.md +16 -0
  257. package/runtime/prompts/profiles/final-verification.md +16 -0
  258. package/runtime/prompts/profiles/implementation-option-selection.md +16 -0
  259. package/runtime/prompts/profiles/implementation-planning.md +20 -0
  260. package/runtime/prompts/profiles/implementation.md +15 -0
  261. package/runtime/prompts/profiles/improvement-discovery.md +16 -0
  262. package/runtime/prompts/profiles/project-analysis.md +16 -0
  263. package/runtime/prompts/profiles/release-handoff.md +4 -0
  264. package/runtime/prompts/profiles/requirements-discovery.md +20 -0
  265. package/runtime/prompts/wizard/prompts.ko.json +18 -0
  266. package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +8 -0
  267. package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +8 -0
  268. package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
  269. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +2 -2
  270. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +25 -5
  271. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
  272. package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +4 -1
  273. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
  274. package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +2 -2
  275. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +79 -11
  276. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +95 -25
  277. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +59 -14
  278. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +55 -9
  279. package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +51 -12
  280. package/runtime/python/okstra_ctl/agent_activity.py +5 -1
  281. package/runtime/python/okstra_ctl/agent_invocation.py +608 -14
  282. package/runtime/python/okstra_ctl/agent_prompt_cli.py +249 -50
  283. package/runtime/python/okstra_ctl/application/resolve_assignment.py +53 -12
  284. package/runtime/python/okstra_ctl/assignment_environment.py +102 -0
  285. package/runtime/python/okstra_ctl/assignment_resolver.py +790 -0
  286. package/runtime/python/okstra_ctl/attempt_evidence.py +342 -0
  287. package/runtime/python/okstra_ctl/convergence.py +409 -3
  288. package/runtime/python/okstra_ctl/convergence_engine.py +248 -15
  289. package/runtime/python/okstra_ctl/convergence_store.py +170 -1
  290. package/runtime/python/okstra_ctl/dispatch_core.py +1092 -63
  291. package/runtime/python/okstra_ctl/dispatch_state.py +696 -11
  292. package/runtime/python/okstra_ctl/doctor.py +14 -0
  293. package/runtime/python/okstra_ctl/domain/host.py +32 -1
  294. package/runtime/python/okstra_ctl/domain/provider.py +212 -6
  295. package/runtime/python/okstra_ctl/domain/role.py +125 -0
  296. package/runtime/python/okstra_ctl/domain/wizard/interaction.py +13 -0
  297. package/runtime/python/okstra_ctl/domain/worker_exec.py +34 -1
  298. package/runtime/python/okstra_ctl/domain/worker_role.py +13 -3
  299. package/runtime/python/okstra_ctl/domain/worker_stream.py +24 -0
  300. package/runtime/python/okstra_ctl/error_log_write.py +9 -0
  301. package/runtime/python/okstra_ctl/error_report.py +21 -1
  302. package/runtime/python/okstra_ctl/execution_identity.py +529 -0
  303. package/runtime/python/okstra_ctl/execution_manifest.py +1281 -0
  304. package/runtime/python/okstra_ctl/execution_mutation_audit.py +619 -0
  305. package/runtime/python/okstra_ctl/index.py +6 -2
  306. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +43 -3
  307. package/runtime/python/okstra_ctl/legacy_model_selection.py +490 -0
  308. package/runtime/python/okstra_ctl/model_cli.py +278 -0
  309. package/runtime/python/okstra_ctl/model_defaults.py +46 -0
  310. package/runtime/python/okstra_ctl/model_discovery.py +26 -7
  311. package/runtime/python/okstra_ctl/model_pool.py +501 -0
  312. package/runtime/python/okstra_ctl/models.py +47 -27
  313. package/runtime/python/okstra_ctl/mutation_recovery.py +293 -0
  314. package/runtime/python/okstra_ctl/pane_title.py +151 -0
  315. package/runtime/python/okstra_ctl/ports/host_model.py +48 -17
  316. package/runtime/python/okstra_ctl/ports/worker_dispatch.py +4 -0
  317. package/runtime/python/okstra_ctl/registry/provider_registry.py +8 -4
  318. package/runtime/python/okstra_ctl/render.py +78 -10
  319. package/runtime/python/okstra_ctl/render_final_report.py +19 -0
  320. package/runtime/python/okstra_ctl/report_contract.py +39 -0
  321. package/runtime/python/okstra_ctl/report_finalize.py +16 -0
  322. package/runtime/python/okstra_ctl/report_html/render.py +1 -0
  323. package/runtime/python/okstra_ctl/role_requirements.py +217 -0
  324. package/runtime/python/okstra_ctl/run.py +1207 -258
  325. package/runtime/python/okstra_ctl/run_index_row.py +7 -1
  326. package/runtime/python/okstra_ctl/time_report.py +38 -4
  327. package/runtime/python/okstra_ctl/usage_identity.py +86 -0
  328. package/runtime/python/okstra_ctl/usage_report.py +15 -3
  329. package/runtime/python/okstra_ctl/wizard.py +1253 -62
  330. package/runtime/python/okstra_ctl/worker_audit_ledger.py +68 -65
  331. package/runtime/python/okstra_ctl/worker_request.py +7 -1
  332. package/runtime/python/okstra_ctl/worker_runner.py +130 -5
  333. package/runtime/python/okstra_ctl/workers.py +7 -5
  334. package/runtime/python/okstra_ctl/write_policy.py +571 -0
  335. package/runtime/schemas/convergence-groups-v2.0.schema.json +128 -0
  336. package/runtime/schemas/execution-manifest-v2.schema.json +340 -0
  337. package/runtime/schemas/final-report-v2.0.schema.json +25 -0
  338. package/runtime/skills/okstra-run/SKILL.md +9 -9
  339. package/runtime/templates/report-writer-prompt-preamble.md +2 -0
  340. package/runtime/templates/reports/final-report-v2.template.md +7 -1
  341. package/runtime/templates/reports/html/base.template.html +21 -0
  342. package/runtime/templates/reports/html/i18n/en.json +4 -0
  343. package/runtime/templates/reports/html/i18n/ko.json +4 -0
  344. package/runtime/templates/worker-prompt-preamble.md +6 -2
  345. package/runtime/validators/lib/fixtures.sh +49 -16
  346. package/runtime/validators/validate-run.py +429 -35
  347. package/runtime/validators/validate_session_conformance.py +25 -7
  348. package/src/cli-registry.mjs +0 -539
  349. package/src/commands/execute/codex-dispatch.mjs +0 -10
  350. package/src/commands/execute/plan-items.mjs +0 -9
  351. package/src/commands/execute/plan-validate.mjs +0 -68
  352. package/src/commands/execute/run.mjs +0 -208
  353. package/src/commands/execute/wizard.mjs +0 -167
  354. package/src/commands/inspect/stage-map.mjs +0 -98
  355. package/src/commands/inspect/task-list.mjs +0 -97
  356. package/src/commands/inspect/task-show.mjs +0 -100
  357. package/src/commands/lifecycle/check-project.mjs +0 -187
  358. package/src/commands/lifecycle/config.mjs +0 -385
  359. package/src/commands/lifecycle/doctor.mjs +0 -308
  360. package/src/commands/lifecycle/install.mjs +0 -1170
  361. package/src/commands/lifecycle/paths.mjs +0 -104
  362. package/src/commands/lifecycle/preflight.mjs +0 -125
  363. package/src/commands/lifecycle/setup.mjs +0 -333
  364. package/src/commands/lifecycle/uninstall.mjs +0 -279
  365. package/src/commands/memory/memory.mjs +0 -524
  366. package/src/commands/pr/pr.mjs +0 -261
  367. package/src/lib/host-registry-client.mjs +0 -176
  368. package/src/lib/paths.mjs +0 -86
  369. package/src/lib/proc.mjs +0 -31
  370. package/src/lib/python-helper.mjs +0 -142
  371. package/src/lib/runtime-manifest.mjs +0 -70
  372. package/src/lib/skill-catalog.mjs +0 -53
  373. package/src/lib/version.mjs +0 -20
  374. /package/{src → dist}/commands/pr/default.md +0 -0
@@ -84,6 +84,16 @@ def _write_json(path: Path, payload: dict) -> None:
84
84
  _write_text(path, json.dumps(payload, indent=2, ensure_ascii=False) + "\n")
85
85
 
86
86
 
87
+ def _execution_identity(ctx: Mapping[str, object]) -> dict:
88
+ raw = ctx.get("EXECUTION_IDENTITY_JSON")
89
+ if not isinstance(raw, str) or not raw.strip():
90
+ return {}
91
+ payload = json.loads(raw)
92
+ if not isinstance(payload, dict):
93
+ raise TokenRenderError("EXECUTION_IDENTITY_JSON must contain an object")
94
+ return payload
95
+
96
+
87
97
  def _lead_runtime(ctx: dict) -> str:
88
98
  return ctx.get("LEAD_RUNTIME", "") or "claude-code"
89
99
 
@@ -377,10 +387,14 @@ def _agent_contract(ctx: dict) -> dict:
377
387
  return parsed
378
388
 
379
389
 
380
- def _executor_contract(ctx: dict) -> dict:
390
+ def _executor_contract(ctx: dict) -> dict | None:
381
391
  runner = ctx.get("EXECUTOR_RUNNER", "")
382
392
  dispatch_mode = ctx.get("EXECUTOR_DISPATCH_MODE", "")
383
393
  host_model_value = ctx.get("EXECUTOR_HOST_MODEL_VALUE") or None
394
+ if not runner and not ctx.get("EXECUTOR_PROVIDER"):
395
+ if ctx.get("TASK_TYPE") == "implementation":
396
+ raise ValueError("implementation executor assignment is required")
397
+ return None
384
398
  expected_mode = {
385
399
  "native-session": "host-native",
386
400
  "cli-wrapper": "worker-dispatch",
@@ -1326,8 +1340,14 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
1326
1340
  latest_team_state_relative = pointers["latestTeamStatePath"]
1327
1341
  latest_resume_command_relative = pointers["latestResumeCommandPath"]
1328
1342
  convergence_block = _build_convergence_block(ctx)
1343
+ execution_identity = _execution_identity(ctx)
1344
+ existing_v2 = (
1345
+ existing.get("schemaVersion") == "2.0"
1346
+ and existing.get("executionIdentityVersion") == 2
1347
+ )
1348
+ writes_v2 = execution_identity.get("executionIdentityVersion") == 2
1329
1349
  payload = {
1330
- "schemaVersion": "1.0",
1350
+ "schemaVersion": "2.0" if existing_v2 or writes_v2 else "1.0",
1331
1351
  "projectId": ctx.get("PROJECT_ID", ""),
1332
1352
  "taskGroup": ctx.get("TASK_GROUP", ""),
1333
1353
  "taskId": ctx.get("TASK_ID", ""),
@@ -1487,6 +1507,12 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
1487
1507
  "sessionId": ctx.get("CLAUDE_SESSION_ID", ""),
1488
1508
  "resumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
1489
1509
  }
1510
+ if existing_v2 or writes_v2:
1511
+ existing_index = existing.get("roleExecutionIndex")
1512
+ payload["executionIdentityVersion"] = 2
1513
+ payload["roleExecutionIndex"] = (
1514
+ list(existing_index) if isinstance(existing_index, list) else []
1515
+ )
1490
1516
  _write_json(path, payload)
1491
1517
 
1492
1518
 
@@ -1608,8 +1634,9 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
1608
1634
  for s in str(ctx.get("CONCURRENT_RUN_STAGES", "") or "").split(",")
1609
1635
  if s.strip().isdigit()
1610
1636
  ]
1637
+ execution_identity = _execution_identity(ctx)
1611
1638
  payload = {
1612
- "schemaVersion": "1.0",
1639
+ "schemaVersion": execution_identity.get("schemaVersion", "1.0"),
1613
1640
  "okstraVersion": ctx.get("OKSTRA_VERSION", ""),
1614
1641
  "projectId": ctx.get("PROJECT_ID", ""),
1615
1642
  "taskGroup": ctx.get("TASK_GROUP", ""),
@@ -1783,6 +1810,7 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
1783
1810
  )
1784
1811
  if _lead_runtime(ctx) == "claude-code":
1785
1812
  payload["claudeSessionId"] = ctx.get("CLAUDE_SESSION_ID", "")
1813
+ payload.update(execution_identity)
1786
1814
  _guard_invocation_manifest_rewrite(existing_run_manifest, payload)
1787
1815
  _write_json(Path(run_manifest_path), payload)
1788
1816
 
@@ -1795,6 +1823,11 @@ _MODEL_ASSIGNMENT_KEYS = {
1795
1823
  "hostRuntime",
1796
1824
  "hostModelValue",
1797
1825
  }
1826
+ _V2_INVOCATION_IDENTITY_KEYS = {
1827
+ "participantRef",
1828
+ "roleExecutionRef",
1829
+ "dutyId",
1830
+ }
1798
1831
  _AGENT_CONTRACT_KEYS = {
1799
1832
  "schemaVersion",
1800
1833
  "dutyRootPath",
@@ -1860,11 +1893,8 @@ def _validate_invocation_manifest(payload: Mapping[str, object]) -> None:
1860
1893
  or not assignments
1861
1894
  ):
1862
1895
  raise ValueError("incomplete invocation manifest")
1863
- if any(
1864
- not isinstance(item, Mapping) or set(item) != _MODEL_ASSIGNMENT_KEYS
1865
- for item in assignments.values()
1866
- ):
1867
- raise ValueError("incomplete invocation manifest assignments")
1896
+ _invocation_manifest_identity_version(payload)
1897
+ _validate_invocation_assignment_shapes(assignments)
1868
1898
  _validate_compatibility_projections(payload, assignments)
1869
1899
  for key in (
1870
1900
  item
@@ -1901,13 +1931,17 @@ def _validate_compatibility_projections(
1901
1931
  workers = payload.get("workerAssignments")
1902
1932
  if not isinstance(lead, Mapping) or not isinstance(workers, list):
1903
1933
  raise ValueError("incomplete invocation manifest compatibility projection")
1904
- if _model_projection(lead) != assignments.get("lead"):
1934
+ if _model_projection(lead) != _model_projection_from_assignment(
1935
+ assignments.get("lead")
1936
+ ):
1905
1937
  raise ValueError("immutable invocation manifest lead projection drift")
1906
1938
  for worker in workers:
1907
1939
  if not isinstance(worker, Mapping):
1908
1940
  raise ValueError("incomplete invocation manifest worker projection")
1909
1941
  reference = f"initial/{worker.get('workerId', '')}"
1910
- if _model_projection(worker) != assignments.get(reference):
1942
+ if _model_projection(worker) != _model_projection_from_assignment(
1943
+ assignments.get(reference)
1944
+ ):
1911
1945
  raise ValueError("immutable invocation manifest worker projection drift")
1912
1946
 
1913
1947
 
@@ -1915,6 +1949,40 @@ def _model_projection(value: Mapping[str, object]) -> dict[str, object]:
1915
1949
  return {key: value.get(key) for key in _MODEL_ASSIGNMENT_KEYS}
1916
1950
 
1917
1951
 
1952
+ def _model_projection_from_assignment(value: object) -> dict[str, object] | None:
1953
+ if not isinstance(value, Mapping) or not _MODEL_ASSIGNMENT_KEYS.issubset(value):
1954
+ return None
1955
+ return _model_projection(value)
1956
+
1957
+
1958
+ def _invocation_manifest_identity_version(payload: Mapping[str, object]) -> int:
1959
+ schema = payload.get("schemaVersion")
1960
+ identity = payload.get("executionIdentityVersion")
1961
+ if schema == "2.0" and identity == 2:
1962
+ return 2
1963
+ if schema == "2.0" or identity is not None:
1964
+ raise ValueError("invocation manifest mixes v1 and v2 execution identity")
1965
+ if schema not in (None, 1, "1", "1.0"):
1966
+ raise ValueError("unsupported invocation manifest execution identity version")
1967
+ return 1
1968
+
1969
+
1970
+ def _validate_invocation_assignment_shapes(
1971
+ assignments: Mapping[str, object],
1972
+ ) -> None:
1973
+ allowed = {frozenset(_MODEL_ASSIGNMENT_KEYS)}
1974
+ for item in assignments.values():
1975
+ if not isinstance(item, Mapping):
1976
+ raise ValueError("incomplete invocation manifest assignments")
1977
+ keys = set(item)
1978
+ if keys & _V2_INVOCATION_IDENTITY_KEYS:
1979
+ raise ValueError(
1980
+ "invocation assignment duplicates v2 execution identity"
1981
+ )
1982
+ if frozenset(keys) not in allowed:
1983
+ raise ValueError("incomplete invocation manifest assignments")
1984
+
1985
+
1918
1986
  def render_timeline(timeline_path: str, ctx: dict) -> None:
1919
1987
  task_manifest_path = Path(ctx.get("TASK_MANIFEST_PATH", ""))
1920
1988
  task_manifest = {}
@@ -566,6 +566,8 @@ def _ai_markdown_context(data: dict, schema: dict | None) -> dict:
566
566
  context["md_rest"] = sections.rest
567
567
  context["md_has"] = sections.has
568
568
  context["md_claim"] = sections.mark_rendered
569
+ context["executionRolesTable"] = render_execution_roles_markdown(data)
570
+ context["executionIdentityVersion"] = data.get("executionIdentityVersion")
569
571
  return context
570
572
 
571
573
 
@@ -623,6 +625,23 @@ _OPTIONAL_ANALYSIS_DEFAULTS = (
623
625
  )
624
626
 
625
627
 
628
+ def render_execution_roles_markdown(data: dict) -> str:
629
+ roles = data.get("executionRoles") or []
630
+ if data.get("executionIdentityVersion") != 2:
631
+ return "legacy execution roles"
632
+ lines = ["| Role | Label | Status |", "|---|---|---|"]
633
+ for row in roles:
634
+ lines.append(
635
+ f"| {row.get('role', '')} | {row.get('executionLabel', '')} | "
636
+ f"{row.get('status', '')} |"
637
+ )
638
+ return "\n".join(lines)
639
+
640
+
641
+ def render_execution_roles_html(data: dict) -> str:
642
+ return render_execution_roles_markdown(data)
643
+
644
+
626
645
  def _with_optional_defaults(data: dict) -> dict:
627
646
  """Render context with schema-optional fields filled in.
628
647
 
@@ -170,6 +170,45 @@ def html_template_for(task_type: str) -> str:
170
170
  raise ValueError(f"no task-specific html template for: {task_type}") from exc
171
171
 
172
172
 
173
+ def execution_roles_from_manifest(manifest: dict) -> list[dict]:
174
+ """Copy the exact RoleExecution set. Do not invent rows."""
175
+ if int(manifest.get("executionIdentityVersion") or 0) != 2:
176
+ return []
177
+ roles = []
178
+ for row in manifest.get("roleExecutions") or []:
179
+ if not isinstance(row, dict):
180
+ continue
181
+ roles.append({
182
+ "roleExecutionRef": row.get("roleExecutionRef"),
183
+ "role": row.get("role"),
184
+ "provider": row.get("provider"),
185
+ "modelRef": row.get("modelRef"),
186
+ "ordinal": row.get("ordinal"),
187
+ "executionLabel": row.get("executionLabel"),
188
+ "status": row.get("status"),
189
+ })
190
+ return roles
191
+
192
+
193
+ def apply_execution_roles(data: dict, manifest: dict) -> dict:
194
+ """Copy the manifest RoleExecution set onto a final-report payload."""
195
+ updated = dict(data)
196
+ if int(manifest.get("executionIdentityVersion") or 0) != 2:
197
+ return updated
198
+ updated["executionIdentityVersion"] = 2
199
+ updated["executionRoles"] = execution_roles_from_manifest(manifest)
200
+ return updated
201
+
202
+
203
+ def assert_execution_roles_match_manifest(report: dict, manifest: dict) -> None:
204
+ report_refs = {row.get("roleExecutionRef") for row in report.get("executionRoles") or []}
205
+ manifest_refs = {
206
+ row.get("roleExecutionRef") for row in manifest.get("roleExecutions") or []
207
+ }
208
+ if report_refs != manifest_refs:
209
+ raise ValueError("report executionRoles must equal manifest roleExecutions")
210
+
211
+
173
212
  def markdown_template_for(task_type: str) -> str:
174
213
  try:
175
214
  return TASK_TYPE_MARKDOWN_TEMPLATE[task_type]
@@ -38,6 +38,7 @@ from pathlib import Path
38
38
  from typing import Any, Callable, Mapping, Sequence
39
39
 
40
40
  from .agent_activity import ActivityProjectionError, project_agent_activity
41
+ from .report_contract import apply_execution_roles
41
42
  from .dispatch_state import DispatchError, link_agent_dispatch_result
42
43
  from .final_report_paths import final_report_data_path, final_report_markdown_path
43
44
  from .paths import task_dir, task_manifest_file
@@ -388,6 +389,7 @@ def run_finalize(
388
389
  steps: list[dict[str, Any]] = []
389
390
  deferred = ""
390
391
  try:
392
+ write_execution_roles(ctx)
391
393
  commands = build_commands(ctx)
392
394
  except FinalizeError as exc:
393
395
  return {"ok": False, "reason": str(exc), "steps": steps}
@@ -465,6 +467,20 @@ def run_finalize(
465
467
  return {"ok": True, "reason": "", "steps": steps}
466
468
 
467
469
 
470
+ def write_execution_roles(ctx: FinalizeContext) -> None:
471
+ """Write the exact manifest role set onto the final-report data.json."""
472
+ if not ctx.data_path.is_file() or not ctx.manifest_path.is_file():
473
+ return
474
+ manifest = _load_manifest(ctx.manifest_path)
475
+ data = _load_manifest(ctx.data_path)
476
+ updated = apply_execution_roles(dict(data), dict(manifest))
477
+ if updated == data:
478
+ return
479
+ tmp = ctx.data_path.with_suffix(ctx.data_path.suffix + ".tmp")
480
+ tmp.write_text(json.dumps(updated, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
481
+ tmp.replace(ctx.data_path)
482
+
483
+
468
484
  def _link_lead_result_for_validation(ctx: FinalizeContext) -> None:
469
485
  """Link the lead-authored synthesis artifact before Phase 7 validation."""
470
486
  manifest = _load_manifest(ctx.manifest_path)
@@ -155,6 +155,7 @@ def render_v2_html_view(
155
155
  # Every task type ends with the same run-cost section, so it is bound
156
156
  # here rather than in ten view models that would each rebuild it.
157
157
  "runUsage": run_usage(data),
158
+ "executionRoles": data.get("executionRoles") or [],
158
159
  "css": (root / "html/assets/base.css").read_text(encoding="utf-8"),
159
160
  "js": response_js + "\n" + base_js,
160
161
  }
@@ -0,0 +1,217 @@
1
+ """Parse canonical role requirements declared in phase profiles."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ from .domain.role import RoleCatalogError, normalize_role, role_for_duty
8
+
9
+
10
+ class RoleProfileError(ValueError):
11
+ """Raised when a profile's structured role declaration is invalid."""
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class RoleRequirement:
16
+ role: str
17
+ count: int
18
+ duty: str
19
+ optional_count: int = 0
20
+ min_distinct_providers: int | None = None
21
+ dynamic: bool = False
22
+
23
+ def signature(self) -> tuple[str, int, int, str, int | None, bool]:
24
+ return (
25
+ self.role,
26
+ self.count,
27
+ self.optional_count,
28
+ self.duty,
29
+ self.min_distinct_providers,
30
+ self.dynamic,
31
+ )
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class RoleProfile:
36
+ roles: tuple[RoleRequirement, ...]
37
+
38
+ def role_signature(self) -> tuple[tuple[str, int, int, str, int | None, bool], ...]:
39
+ return tuple(requirement.signature() for requirement in self.roles)
40
+
41
+
42
+ _REQUIRED_FIELDS = frozenset({"role", "count", "duty"})
43
+ _ALLOWED_FIELDS = _REQUIRED_FIELDS | {
44
+ "optionalCount",
45
+ "minDistinctProviders",
46
+ "dynamic",
47
+ }
48
+
49
+
50
+ def load_role_profile(path: Path) -> RoleProfile:
51
+ """Load and validate the one fenced YAML ``roles:`` declaration in a profile."""
52
+ profile_path = Path(path)
53
+ try:
54
+ text = profile_path.read_text(encoding="utf-8")
55
+ except OSError as exc:
56
+ raise RoleProfileError(f"cannot read role profile: {profile_path}") from exc
57
+ raw_roles, errors = _parse_roles_block(text)
58
+ requirements = tuple(
59
+ _validate_requirement(raw, index, errors)
60
+ for index, raw in enumerate(raw_roles, 1)
61
+ )
62
+ _validate_static_role_duplicates(requirements, errors)
63
+ if errors:
64
+ raise RoleProfileError("invalid role profile:\n- " + "\n- ".join(errors))
65
+ return RoleProfile(requirements)
66
+
67
+
68
+ def _parse_roles_block(text: str) -> tuple[list[dict[str, str]], list[str]]:
69
+ blocks = _yaml_blocks_with_roles(text)
70
+ if len(blocks) != 1:
71
+ return [], ["profile must declare exactly one fenced YAML roles block"]
72
+ lines = blocks[0]
73
+ if lines == ["roles: []"]:
74
+ return [], []
75
+ if not lines or lines[0] != "roles:":
76
+ return [], ["roles block must start with roles:"]
77
+ roles: list[dict[str, str]] = []
78
+ errors: list[str] = []
79
+ current: dict[str, str] | None = None
80
+ for line_number, line in enumerate(lines[1:], 2):
81
+ if not line.strip():
82
+ continue
83
+ if line.startswith(" - "):
84
+ if current is not None:
85
+ roles.append(current)
86
+ current = {}
87
+ _add_yaml_field(current, line[4:], line_number, errors)
88
+ continue
89
+ if line.startswith(" ") and current is not None:
90
+ _add_yaml_field(current, line[4:], line_number, errors)
91
+ continue
92
+ errors.append(f"roles block line {line_number} is not a role field")
93
+ if current is not None:
94
+ roles.append(current)
95
+ elif not errors:
96
+ errors.append("roles block must contain a role or use roles: []")
97
+ return roles, errors
98
+
99
+
100
+ def _yaml_blocks_with_roles(text: str) -> list[list[str]]:
101
+ blocks: list[list[str]] = []
102
+ current: list[str] | None = None
103
+ for line in text.splitlines():
104
+ if line.strip() == "```yaml":
105
+ current = []
106
+ continue
107
+ if line.strip() == "```" and current is not None:
108
+ if any(item.strip().startswith("roles:") for item in current):
109
+ blocks.append(current)
110
+ current = None
111
+ continue
112
+ if current is not None:
113
+ current.append(line)
114
+ return blocks
115
+
116
+
117
+ def _add_yaml_field(
118
+ fields: dict[str, str], line: str, line_number: int, errors: list[str],
119
+ ) -> None:
120
+ if ":" not in line:
121
+ errors.append(f"roles block line {line_number} must contain key: value")
122
+ return
123
+ key, value = (part.strip() for part in line.split(":", 1))
124
+ if not key or not value:
125
+ errors.append(f"roles block line {line_number} must contain key: value")
126
+ return
127
+ if key in fields:
128
+ errors.append(f"roles block line {line_number} repeats {key}")
129
+ return
130
+ fields[key] = value
131
+
132
+
133
+ def _validate_requirement(
134
+ raw: dict[str, str], index: int, errors: list[str],
135
+ ) -> RoleRequirement:
136
+ prefix = f"role requirement {index}"
137
+ for field in sorted(_REQUIRED_FIELDS - raw.keys()):
138
+ errors.append(f"{prefix} is missing {field}")
139
+ for field in sorted(raw.keys() - _ALLOWED_FIELDS):
140
+ errors.append(f"{prefix} has unknown field {field}")
141
+
142
+ role = _validate_role(raw.get("role", ""), prefix, errors)
143
+ count = _validate_non_negative_integer(raw.get("count", ""), "count", prefix, errors)
144
+ duty = _validate_duty(raw.get("duty", ""), role, prefix, errors)
145
+ optional_count = _validate_non_negative_integer(
146
+ raw.get("optionalCount", "0"), "optionalCount", prefix, errors,
147
+ )
148
+ diversity = _validate_diversity(raw.get("minDistinctProviders"), prefix, errors)
149
+ dynamic = _validate_dynamic(raw.get("dynamic", "false"), prefix, errors)
150
+ if diversity is not None and diversity > count + optional_count:
151
+ errors.append(f"{prefix} minDistinctProviders must not exceed count + optionalCount")
152
+ if dynamic and (count != 0 or optional_count != 0):
153
+ errors.append(f"{prefix} dynamic roles must have count and optionalCount of zero")
154
+ if dynamic and diversity is not None:
155
+ errors.append(f"{prefix} dynamic roles must not declare minDistinctProviders")
156
+ return RoleRequirement(role, count, duty, optional_count, diversity, dynamic)
157
+
158
+
159
+ def _validate_role(raw: str, prefix: str, errors: list[str]) -> str:
160
+ try:
161
+ return normalize_role(raw)
162
+ except RoleCatalogError as exc:
163
+ errors.append(f"{prefix} {exc}")
164
+ return raw
165
+
166
+
167
+ def _validate_duty(raw: str, role: str, prefix: str, errors: list[str]) -> str:
168
+ try:
169
+ duty_role = role_for_duty(raw)
170
+ except RoleCatalogError as exc:
171
+ errors.append(f"{prefix} {exc}")
172
+ return raw
173
+ if duty_role != role:
174
+ errors.append(f"{prefix} duty {raw} belongs to role {duty_role}, not {role}")
175
+ return raw
176
+
177
+
178
+ def _validate_non_negative_integer(raw: str, field: str, prefix: str, errors: list[str]) -> int:
179
+ try:
180
+ value = int(raw)
181
+ except ValueError:
182
+ errors.append(f"{prefix} {field} must be a non-negative integer")
183
+ return 0
184
+ if value < 0 or str(value) != raw:
185
+ errors.append(f"{prefix} {field} must be a non-negative integer")
186
+ return 0
187
+ return value
188
+
189
+
190
+ def _validate_diversity(raw: str | None, prefix: str, errors: list[str]) -> int | None:
191
+ if raw is None:
192
+ return None
193
+ value = _validate_non_negative_integer(raw, "minDistinctProviders", prefix, errors)
194
+ if value == 0:
195
+ errors.append(f"{prefix} minDistinctProviders must be at least one")
196
+ return value
197
+
198
+
199
+ def _validate_dynamic(raw: str, prefix: str, errors: list[str]) -> bool:
200
+ if raw == "true":
201
+ return True
202
+ if raw == "false":
203
+ return False
204
+ errors.append(f"{prefix} dynamic must be true or false")
205
+ return False
206
+
207
+
208
+ def _validate_static_role_duplicates(
209
+ requirements: tuple[RoleRequirement, ...], errors: list[str],
210
+ ) -> None:
211
+ static_roles: set[str] = set()
212
+ for requirement in requirements:
213
+ if requirement.dynamic:
214
+ continue
215
+ if requirement.role in static_roles:
216
+ errors.append(f"duplicate static role: {requirement.role}")
217
+ static_roles.add(requirement.role)