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
@@ -0,0 +1,790 @@
1
+ """Deterministic role-instance model assignment and host binding."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from types import MappingProxyType
6
+ from typing import Literal, Mapping, Sequence
7
+
8
+ from .application.resolve_assignment import (
9
+ resolve_assignment_runner,
10
+ resolve_lead_provider_id,
11
+ resolve_lead_provider_from_specs,
12
+ )
13
+ from .domain.host import HostDescriptor, HostSessionContext
14
+ from .domain.provider import (
15
+ ExecutionNormalization,
16
+ HostModelBinding,
17
+ LeadProviderAssignment,
18
+ )
19
+ from .domain.role import normalize_role
20
+ from .domain.worker_exec import WorkerWriteCapability
21
+ from .model_defaults import (
22
+ ModelDefaultError,
23
+ ModelDefaultScopes,
24
+ default_candidates,
25
+ default_scope,
26
+ )
27
+ from .model_pool import ModelPool, ResolvedModel
28
+ from .ports.host_model import (
29
+ HostModelBindingError,
30
+ HostModelBindingPort,
31
+ HostModelBindingRequest,
32
+ )
33
+ from .role_requirements import RoleProfile, RoleRequirement
34
+
35
+
36
+ TerminalBackend = Literal[
37
+ "cli-wrapper",
38
+ "cmux-pane",
39
+ "mixed",
40
+ ]
41
+ _TERMINAL_BACKENDS = frozenset({
42
+ "cli-wrapper",
43
+ "cmux-pane",
44
+ "mixed",
45
+ })
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class AssignmentEnvironment:
50
+ """Immutable host and dispatch facts consumed by assignment resolution."""
51
+
52
+ host_descriptor: HostDescriptor
53
+ host_model_port: HostModelBindingPort
54
+ native_worker_write_capability: WorkerWriteCapability
55
+ terminal_backend: TerminalBackend
56
+ execution_resolutions: Mapping[
57
+ tuple[str, str],
58
+ ExecutionNormalization,
59
+ ]
60
+
61
+ def __post_init__(self) -> None:
62
+ if self.terminal_backend not in _TERMINAL_BACKENDS:
63
+ allowed = ", ".join(sorted(_TERMINAL_BACKENDS))
64
+ raise ValueError(
65
+ f"unknown terminal backend {self.terminal_backend!r}. "
66
+ f"Allowed values: {allowed}"
67
+ )
68
+ resolutions: dict[tuple[str, str], ExecutionNormalization] = {}
69
+ for key, resolution in self.execution_resolutions.items():
70
+ if (
71
+ not isinstance(key, tuple)
72
+ or len(key) != 2
73
+ or not all(isinstance(value, str) and value for value in key)
74
+ or not isinstance(resolution, ExecutionNormalization)
75
+ ):
76
+ raise ValueError(
77
+ "execution resolutions require (model_ref, role) keys and "
78
+ "ExecutionNormalization values"
79
+ )
80
+ resolutions[key] = resolution
81
+ object.__setattr__(
82
+ self,
83
+ "execution_resolutions",
84
+ MappingProxyType(resolutions),
85
+ )
86
+
87
+
88
+ @dataclass(frozen=True)
89
+ class AssignmentContext:
90
+ pool: ModelPool
91
+ environment: AssignmentEnvironment
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class RoleCountSelection:
96
+ role: str
97
+ count: int
98
+
99
+
100
+ @dataclass(frozen=True)
101
+ class RoleModelSelection:
102
+ role: str
103
+ model_refs: tuple[str, ...]
104
+
105
+
106
+ @dataclass(frozen=True)
107
+ class RoleInstance:
108
+ role: str
109
+ duty_id: str
110
+ ordinal: int
111
+
112
+
113
+ @dataclass(frozen=True)
114
+ class CandidateAssessment:
115
+ role: str
116
+ ordinal: int
117
+ model_ref: str
118
+ scope: str
119
+ available: bool
120
+ reason: str = ""
121
+
122
+
123
+ @dataclass(frozen=True)
124
+ class UnsatisfiedConstraint:
125
+ role: str
126
+ constraint: str
127
+ required: int
128
+ available: int
129
+
130
+
131
+ @dataclass(frozen=True)
132
+ class ResolvedAssignment:
133
+ instance: RoleInstance
134
+ model_ref: str | None
135
+ provider_id: str
136
+ model_id: str
137
+ display_name: str
138
+ host_runtime: str
139
+ entry_mode: Literal["current-session", "new-session"]
140
+ binding: HostModelBinding | None
141
+
142
+ @property
143
+ def role(self) -> str:
144
+ return self.instance.role
145
+
146
+ @property
147
+ def duty_id(self) -> str:
148
+ return self.instance.duty_id
149
+
150
+ @property
151
+ def ordinal(self) -> int:
152
+ return self.instance.ordinal
153
+
154
+ @property
155
+ def display(self) -> str:
156
+ return self.display_name
157
+
158
+ @property
159
+ def execution(self) -> str:
160
+ return self.binding.resolved_execution_value if self.binding else ""
161
+
162
+ @property
163
+ def execution_label(self) -> str:
164
+ return f"{self.role}-{self.provider_id}-{self.model_id}-{self.ordinal}"
165
+
166
+
167
+ @dataclass(frozen=True)
168
+ class AssignmentPlan:
169
+ assignments: tuple[ResolvedAssignment, ...]
170
+ candidates: tuple[CandidateAssessment, ...] = ()
171
+
172
+ def by_role(self, role: str) -> tuple[ResolvedAssignment, ...]:
173
+ return tuple(row for row in self.assignments if row.role == role)
174
+
175
+
176
+ class AssignmentResolutionError(ValueError):
177
+ """Raised with all evaluated candidates and failed constraints."""
178
+
179
+ def __init__(
180
+ self,
181
+ message: str,
182
+ *,
183
+ candidates: Sequence[CandidateAssessment] = (),
184
+ unsatisfied_constraints: Sequence[UnsatisfiedConstraint] = (),
185
+ ) -> None:
186
+ self.candidates = tuple(candidates)
187
+ self.unsatisfied_constraints = tuple(unsatisfied_constraints)
188
+ super().__init__(message)
189
+
190
+
191
+ def _validate_environment(
192
+ host: HostSessionContext,
193
+ environment: AssignmentEnvironment,
194
+ ) -> None:
195
+ if environment.host_descriptor.id != host.host_id:
196
+ raise AssignmentResolutionError(
197
+ "assignment environment host does not match the session context: "
198
+ f"{environment.host_descriptor.id!r} != {host.host_id!r}"
199
+ )
200
+
201
+
202
+ def resolve_assignments(
203
+ *,
204
+ profile: RoleProfile,
205
+ role_counts: Mapping[str, int],
206
+ role_models: Mapping[str, tuple[str, ...]],
207
+ scopes: ModelDefaultScopes,
208
+ pool: ModelPool,
209
+ host: HostSessionContext,
210
+ environment: AssignmentEnvironment,
211
+ ) -> AssignmentPlan:
212
+ """Resolve the first complete assignment in deterministic DFS order."""
213
+ _validate_environment(host, environment)
214
+ requirements = _static_requirements(profile)
215
+ instances = _role_instances(requirements, role_counts)
216
+ _validate_model_selections(instances, role_models)
217
+ current_leader = _current_leader_assignment(
218
+ instances,
219
+ role_models,
220
+ pool,
221
+ host,
222
+ environment,
223
+ )
224
+ options, assessments = _assignment_options(
225
+ instances,
226
+ role_models,
227
+ scopes,
228
+ pool,
229
+ host,
230
+ profile,
231
+ requirements,
232
+ current_leader,
233
+ environment,
234
+ )
235
+ resolved = _first_complete_assignment(options, requirements)
236
+ if resolved is None:
237
+ constraints = _unmet_constraints(options, requirements)
238
+ raise AssignmentResolutionError(
239
+ "no complete role assignment satisfies the profile constraints",
240
+ candidates=assessments,
241
+ unsatisfied_constraints=constraints,
242
+ )
243
+ return AssignmentPlan(tuple(resolved), tuple(assessments))
244
+
245
+
246
+ def resolve_model_assignment(
247
+ *,
248
+ instance: RoleInstance,
249
+ model_ref: str,
250
+ pool: ModelPool,
251
+ host: HostSessionContext,
252
+ environment: AssignmentEnvironment,
253
+ entry_mode: Literal["current-session", "new-session"] = "new-session",
254
+ ) -> ResolvedAssignment:
255
+ """Resolve one canonical model through the selected host execution path."""
256
+ _validate_environment(host, environment)
257
+ model = pool.resolve(model_ref)
258
+ availability = pool.availability(
259
+ model_ref,
260
+ instance.role,
261
+ host.host_id,
262
+ entry_mode,
263
+ )
264
+ if not availability.available:
265
+ raise AssignmentResolutionError(
266
+ f"model {model_ref!r} is unavailable: {availability.reason}"
267
+ )
268
+ binding = _first_supported_binding(
269
+ instance,
270
+ model,
271
+ pool,
272
+ environment,
273
+ )
274
+ return _resolved_assignment(instance, model, host, entry_mode, binding)
275
+
276
+
277
+ def resolve_dispatch_assignment(
278
+ *,
279
+ context: AssignmentContext,
280
+ host_runtime: str,
281
+ role: str,
282
+ duty_id: str,
283
+ provider: str,
284
+ model: str = "",
285
+ ) -> ResolvedAssignment:
286
+ """Resolve one legacy provider/model input through the shared core."""
287
+ canonical_role = normalize_role(role)
288
+ pool = context.pool
289
+ selected = resolve_model_selection(pool, provider, canonical_role, model)
290
+ host = HostSessionContext(
291
+ host_id=host_runtime,
292
+ entry_mode="spawn-process",
293
+ available_functions=frozenset(),
294
+ interaction_surface="application-dispatch",
295
+ )
296
+ return resolve_model_assignment(
297
+ instance=RoleInstance(canonical_role, duty_id, 1),
298
+ model_ref=str(selected.model_ref),
299
+ pool=pool,
300
+ host=host,
301
+ environment=context.environment,
302
+ )
303
+
304
+
305
+ def resolve_context_lead_provider(
306
+ *,
307
+ context: AssignmentContext,
308
+ requested_provider: str,
309
+ ) -> LeadProviderAssignment:
310
+ """Resolve a lead provider from the same immutable assignment facts."""
311
+ host = context.environment.host_descriptor
312
+ provider_id = resolve_lead_provider_id(
313
+ host=host,
314
+ requested_provider=requested_provider,
315
+ )
316
+ provider = context.pool.resolve_provider(provider_id)
317
+ return resolve_lead_provider_from_specs(
318
+ host=host,
319
+ requested_provider=requested_provider,
320
+ provider=provider,
321
+ )
322
+
323
+
324
+ def resolve_model_selection(
325
+ pool: ModelPool,
326
+ provider: str,
327
+ role: str,
328
+ raw_model: str,
329
+ ) -> ResolvedModel:
330
+ """Resolve one explicit or provider-default catalog model without binding."""
331
+ if raw_model:
332
+ return pool.resolve_alias(provider, raw_model)
333
+ selected = pool.default_candidate(provider, role)
334
+ if selected is None:
335
+ raise AssignmentResolutionError(
336
+ f"provider {provider!r} has no default model for role {role!r}"
337
+ )
338
+ return selected
339
+
340
+
341
+ def _static_requirements(profile: RoleProfile) -> tuple[RoleRequirement, ...]:
342
+ leader = RoleRequirement("leader", 1, 1, 1, "lead")
343
+ return (leader, *(row for row in profile.roles if not row.dynamic))
344
+
345
+
346
+ def _role_instances(
347
+ requirements: Sequence[RoleRequirement],
348
+ role_counts: Mapping[str, int],
349
+ ) -> tuple[RoleInstance, ...]:
350
+ allowed = {row.role for row in requirements}
351
+ unknown = tuple(sorted(set(role_counts) - allowed))
352
+ if unknown:
353
+ raise AssignmentResolutionError(
354
+ f"role counts contain roles outside the profile: {', '.join(unknown)}"
355
+ )
356
+ instances: list[RoleInstance] = []
357
+ for requirement in requirements:
358
+ fixed = requirement.min_count == requirement.max_count
359
+ if requirement.role in role_counts and fixed:
360
+ raise AssignmentResolutionError(
361
+ f"role {requirement.role!r} has a fixed quantity and cannot "
362
+ "receive an explicit count"
363
+ )
364
+ count = role_counts.get(requirement.role, requirement.recommended_count)
365
+ if count < requirement.min_count or count > requirement.max_count:
366
+ raise AssignmentResolutionError(
367
+ f"role {requirement.role!r} count must be in "
368
+ f"{requirement.min_count}..{requirement.max_count}: {count}"
369
+ )
370
+ instances.extend(
371
+ RoleInstance(requirement.role, requirement.duty, ordinal)
372
+ for ordinal in range(1, count + 1)
373
+ )
374
+ return tuple(instances)
375
+
376
+
377
+ def _validate_model_selections(
378
+ instances: Sequence[RoleInstance],
379
+ role_models: Mapping[str, tuple[str, ...]],
380
+ ) -> None:
381
+ counts = {
382
+ role: sum(instance.role == role for instance in instances)
383
+ for role in {instance.role for instance in instances}
384
+ }
385
+ unknown = tuple(sorted(set(role_models) - counts.keys()))
386
+ if unknown:
387
+ raise AssignmentResolutionError(
388
+ f"role models contain roles outside the profile: {', '.join(unknown)}"
389
+ )
390
+ for role, models in role_models.items():
391
+ if len(models) > counts[role]:
392
+ raise AssignmentResolutionError(
393
+ f"role {role!r} has {len(models)} models for {counts[role]} instances"
394
+ )
395
+
396
+
397
+ def _current_leader_assignment(
398
+ instances: Sequence[RoleInstance],
399
+ role_models: Mapping[str, tuple[str, ...]],
400
+ pool: ModelPool,
401
+ host: HostSessionContext,
402
+ environment: AssignmentEnvironment,
403
+ ) -> ResolvedAssignment | None:
404
+ if host.entry_mode != "current-session":
405
+ return None
406
+ leader = next(row for row in instances if row.role == "leader")
407
+ attestation = host.current_model
408
+ model = _validated_current_model(pool, host)
409
+ explicit = role_models.get("leader", ())
410
+ if explicit and (model is None or explicit[0] != str(model.model_ref)):
411
+ return None
412
+ if model is None:
413
+ provider_id = (
414
+ attestation.provider_id
415
+ or environment.host_descriptor.native_provider_id
416
+ )
417
+ return ResolvedAssignment(
418
+ leader,
419
+ None,
420
+ provider_id,
421
+ "unknown",
422
+ "unknown",
423
+ host.host_id,
424
+ "current-session",
425
+ None,
426
+ )
427
+ binding = HostModelBinding(
428
+ runner="current-session",
429
+ catalog_execution_value=model.execution_value,
430
+ resolved_execution_value=model.execution_value,
431
+ host_model_value=attestation.observed_model,
432
+ binding_fidelity=attestation.level,
433
+ worker_write_capability=None,
434
+ )
435
+ return _resolved_assignment(leader, model, host, "current-session", binding)
436
+
437
+
438
+ def _validated_current_model(
439
+ pool: ModelPool,
440
+ host: HostSessionContext,
441
+ ) -> ResolvedModel | None:
442
+ attestation = host.current_model
443
+ if attestation.level == "unknown":
444
+ return None
445
+ model_ref = attestation.normalized_model_ref or ""
446
+ try:
447
+ model = pool.resolve(model_ref)
448
+ except ValueError as exc:
449
+ action = "update the provider adapter catalog or start a registered leader model"
450
+ raise AssignmentResolutionError(
451
+ "unregistered current-session model attestation: "
452
+ f"provider={attestation.provider_id!r}, "
453
+ f"model={attestation.observed_model!r}, source={attestation.source!r}; "
454
+ f"{action}"
455
+ ) from exc
456
+ if model.provider_id != attestation.provider_id:
457
+ raise AssignmentResolutionError(
458
+ "current-session model attestation provider does not match its model ref"
459
+ )
460
+ if attestation.level == "exact" and model.version_kind != "pinned":
461
+ raise AssignmentResolutionError(
462
+ "exact current-session attestation must reference a pinned model"
463
+ )
464
+ if attestation.level == "channel" and model.version_kind != "channel":
465
+ raise AssignmentResolutionError(
466
+ "channel current-session attestation must reference a channel model"
467
+ )
468
+ availability = pool.availability(
469
+ str(model.model_ref),
470
+ "leader",
471
+ host.host_id,
472
+ "current-session",
473
+ )
474
+ if not availability.available:
475
+ raise AssignmentResolutionError(
476
+ "current-session model is unavailable for leader: "
477
+ f"{availability.reason}"
478
+ )
479
+ return model
480
+
481
+
482
+ def _assignment_options(
483
+ instances: Sequence[RoleInstance],
484
+ role_models: Mapping[str, tuple[str, ...]],
485
+ scopes: ModelDefaultScopes,
486
+ pool: ModelPool,
487
+ host: HostSessionContext,
488
+ profile: RoleProfile,
489
+ requirements: Sequence[RoleRequirement],
490
+ current_leader: ResolvedAssignment | None,
491
+ environment: AssignmentEnvironment,
492
+ ) -> tuple[list[tuple[ResolvedAssignment, ...]], list[CandidateAssessment]]:
493
+ options: list[tuple[ResolvedAssignment, ...]] = []
494
+ assessments: list[CandidateAssessment] = []
495
+ for instance in instances:
496
+ if current_leader is not None and instance.role == "leader":
497
+ options.append((current_leader,))
498
+ continue
499
+ rows, evaluated = _instance_options(
500
+ instance,
501
+ role_models,
502
+ scopes,
503
+ pool,
504
+ host,
505
+ profile,
506
+ environment,
507
+ )
508
+ options.append(rows)
509
+ assessments.extend(evaluated)
510
+ if any(not rows for rows in options):
511
+ raise AssignmentResolutionError(
512
+ "one or more role instances have no executable model candidates",
513
+ candidates=assessments,
514
+ unsatisfied_constraints=_unmet_constraints(options, requirements),
515
+ )
516
+ return options, assessments
517
+
518
+
519
+ def _instance_options(
520
+ instance: RoleInstance,
521
+ role_models: Mapping[str, tuple[str, ...]],
522
+ scopes: ModelDefaultScopes,
523
+ pool: ModelPool,
524
+ host: HostSessionContext,
525
+ profile: RoleProfile,
526
+ environment: AssignmentEnvironment,
527
+ ) -> tuple[tuple[ResolvedAssignment, ...], list[CandidateAssessment]]:
528
+ explicit = role_models.get(instance.role, ())
529
+ if instance.ordinal <= len(explicit):
530
+ candidates, scope = (explicit[instance.ordinal - 1],), "explicit"
531
+ else:
532
+ candidates, scope = _rotated_defaults(instance, scopes, pool)
533
+ resolved: list[ResolvedAssignment] = []
534
+ assessments: list[CandidateAssessment] = []
535
+ for model_ref in candidates:
536
+ try:
537
+ assignment = resolve_model_assignment(
538
+ instance=instance,
539
+ model_ref=model_ref,
540
+ pool=pool,
541
+ host=host,
542
+ environment=environment,
543
+ )
544
+ resolved.append(assignment)
545
+ assessments.append(_assessment(instance, model_ref, scope, True, ""))
546
+ except (AssignmentResolutionError, ValueError, RuntimeError) as exc:
547
+ assessments.append(
548
+ _assessment(instance, model_ref, scope, False, str(exc))
549
+ )
550
+ if scope != "bundled" and any(not row.available for row in assessments):
551
+ return (), assessments
552
+ return tuple(resolved), assessments
553
+
554
+
555
+ def _rotated_defaults(
556
+ instance: RoleInstance,
557
+ scopes: ModelDefaultScopes,
558
+ pool: ModelPool,
559
+ ) -> tuple[tuple[str, ...], str]:
560
+ try:
561
+ candidates = default_candidates(instance.role, scopes)
562
+ except ModelDefaultError as exc:
563
+ raise AssignmentResolutionError(str(exc)) from exc
564
+ scope = default_scope(instance.role, scopes)
565
+ if not candidates:
566
+ candidates = tuple(
567
+ str(model.model_ref) for model in pool.default_candidates(instance.role)
568
+ )
569
+ scope = "bundled"
570
+ if not candidates:
571
+ raise AssignmentResolutionError(
572
+ f"role {instance.role!r} has no default model candidates"
573
+ )
574
+ offset = (instance.ordinal - 1) % len(candidates)
575
+ return (*candidates[offset:], *candidates[:offset]), scope or "bundled"
576
+
577
+
578
+ def _assessment(
579
+ instance: RoleInstance,
580
+ model_ref: str,
581
+ scope: str,
582
+ available: bool,
583
+ reason: str,
584
+ ) -> CandidateAssessment:
585
+ return CandidateAssessment(
586
+ instance.role,
587
+ instance.ordinal,
588
+ model_ref,
589
+ scope,
590
+ available,
591
+ reason,
592
+ )
593
+
594
+
595
+ def _first_supported_binding(
596
+ instance: RoleInstance,
597
+ model: ResolvedModel,
598
+ pool: ModelPool,
599
+ environment: AssignmentEnvironment,
600
+ ) -> HostModelBinding:
601
+ provider = pool.resolve_provider(model.provider_id)
602
+ errors: list[str] = []
603
+ for runner in _runner_candidates(instance, environment, provider):
604
+ try:
605
+ resolve_assignment_runner(
606
+ host=environment.host_descriptor,
607
+ provider=provider,
608
+ role=instance.role,
609
+ requested_runner=runner,
610
+ )
611
+ binding = _binding_for_runner(
612
+ instance,
613
+ model,
614
+ provider,
615
+ environment,
616
+ runner,
617
+ )
618
+ _validate_binding_fidelity(model, binding)
619
+ return binding
620
+ except (ValueError, RuntimeError, AttributeError) as exc:
621
+ errors.append(f"{runner}: {exc}")
622
+ raise AssignmentResolutionError(
623
+ f"model {model.model_ref!r} has no executable binding: {'; '.join(errors)}"
624
+ )
625
+
626
+
627
+ def _runner_candidates(
628
+ instance: RoleInstance,
629
+ environment: AssignmentEnvironment,
630
+ provider,
631
+ ) -> tuple[str, ...]:
632
+ if (
633
+ instance.role != "leader"
634
+ and environment.terminal_backend == "cmux-pane"
635
+ ):
636
+ return ("cli-wrapper",) if provider.wrapper else ()
637
+ runners: list[str] = []
638
+ if provider.provider == environment.host_descriptor.native_provider_id:
639
+ runners.append("native-session")
640
+ if provider.wrapper:
641
+ runners.append("cli-wrapper")
642
+ return tuple(runners or ("cli-wrapper",))
643
+
644
+
645
+ def _binding_for_runner(
646
+ instance: RoleInstance,
647
+ model: ResolvedModel,
648
+ provider,
649
+ environment: AssignmentEnvironment,
650
+ runner: str,
651
+ ) -> HostModelBinding:
652
+ execution = model.execution_value
653
+ if runner == "cli-wrapper":
654
+ resolution = environment.execution_resolutions.get(
655
+ (str(model.model_ref), instance.role)
656
+ )
657
+ if resolution is None and provider.execution_normalization_required:
658
+ raise AssignmentResolutionError(
659
+ f"missing execution snapshot for model {model.model_ref!r} "
660
+ f"and role {instance.role!r}"
661
+ )
662
+ if resolution is not None:
663
+ if resolution.error:
664
+ raise AssignmentResolutionError(resolution.error)
665
+ execution = resolution.resolved_execution_value
666
+ capability = _worker_write_capability(
667
+ instance,
668
+ provider,
669
+ environment,
670
+ runner,
671
+ )
672
+ request = HostModelBindingRequest(
673
+ host_runtime=environment.host_descriptor.id,
674
+ provider=model.provider_id,
675
+ model_ref=str(model.model_ref),
676
+ version_kind=model.version_kind,
677
+ model_execution_value=model.execution_value,
678
+ resolved_execution_value=execution,
679
+ runner=runner,
680
+ worker_write_capability=capability,
681
+ )
682
+ try:
683
+ return environment.host_model_port.resolve(request)
684
+ except HostModelBindingError as exc:
685
+ raise AssignmentResolutionError(str(exc)) from exc
686
+
687
+
688
+ def _worker_write_capability(instance, provider, environment, runner):
689
+ if instance.role == "leader":
690
+ return None
691
+ if runner == "native-session":
692
+ return environment.native_worker_write_capability
693
+ if provider.cli_worker_write_capability is None:
694
+ raise AssignmentResolutionError(
695
+ f"provider {provider.provider!r} has no worker execution strategy"
696
+ )
697
+ return provider.cli_worker_write_capability
698
+
699
+
700
+ def _validate_binding_fidelity(
701
+ model: ResolvedModel,
702
+ binding: HostModelBinding,
703
+ ) -> None:
704
+ allowed = {"exact"} if model.version_kind == "pinned" else {"exact", "channel"}
705
+ if binding.binding_fidelity not in allowed:
706
+ raise AssignmentResolutionError(
707
+ f"model {model.model_ref!r} cannot bind with fidelity "
708
+ f"{binding.binding_fidelity!r}"
709
+ )
710
+
711
+
712
+ def _resolved_assignment(instance, model, host, entry_mode, binding):
713
+ return ResolvedAssignment(
714
+ instance=instance,
715
+ model_ref=str(model.model_ref),
716
+ provider_id=model.provider_id,
717
+ model_id=model.model_ref.model_id,
718
+ display_name=model.display_name,
719
+ host_runtime=host.host_id,
720
+ entry_mode=entry_mode,
721
+ binding=binding,
722
+ )
723
+
724
+
725
+ def _first_complete_assignment(
726
+ options: Sequence[Sequence[ResolvedAssignment]],
727
+ requirements: Sequence[RoleRequirement],
728
+ ) -> list[ResolvedAssignment] | None:
729
+ chosen: list[ResolvedAssignment] = []
730
+
731
+ def visit(index: int) -> list[ResolvedAssignment] | None:
732
+ if index == len(options):
733
+ return list(chosen) if not _constraint_failures(chosen, requirements) else None
734
+ for assignment in options[index]:
735
+ chosen.append(assignment)
736
+ if (complete := visit(index + 1)) is not None:
737
+ return complete
738
+ chosen.pop()
739
+ return None
740
+
741
+ return visit(0)
742
+
743
+
744
+ def _constraint_failures(
745
+ assignments: Sequence[ResolvedAssignment],
746
+ requirements: Sequence[RoleRequirement],
747
+ ) -> tuple[UnsatisfiedConstraint, ...]:
748
+ failures: list[UnsatisfiedConstraint] = []
749
+ for requirement in requirements:
750
+ role_rows = [row for row in assignments if row.role == requirement.role]
751
+ if not role_rows:
752
+ continue
753
+ refs = {row.model_ref for row in role_rows}
754
+ if len(refs) < len(role_rows):
755
+ failures.append(UnsatisfiedConstraint(
756
+ requirement.role,
757
+ "uniqueModelRefs",
758
+ len(role_rows),
759
+ len(refs),
760
+ ))
761
+ return tuple(failures)
762
+
763
+
764
+ def _unmet_constraints(
765
+ options: Sequence[Sequence[ResolvedAssignment]],
766
+ requirements: Sequence[RoleRequirement],
767
+ ) -> tuple[UnsatisfiedConstraint, ...]:
768
+ failures: list[UnsatisfiedConstraint] = []
769
+ for requirement in requirements:
770
+ role_groups = [
771
+ candidates
772
+ for candidates in options
773
+ if candidates and candidates[0].role == requirement.role
774
+ ]
775
+ if not role_groups:
776
+ continue
777
+ worker_count = len(role_groups)
778
+ unique_refs = {
779
+ row.model_ref
780
+ for candidates in role_groups
781
+ for row in candidates
782
+ }
783
+ if len(unique_refs) < worker_count:
784
+ failures.append(UnsatisfiedConstraint(
785
+ requirement.role,
786
+ "uniqueModelRefs",
787
+ worker_count,
788
+ len(unique_refs),
789
+ ))
790
+ return tuple(failures)