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,501 @@
1
+ """Unified, validated model lookup over registered provider adapters."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from dataclasses import dataclass
6
+ from types import MappingProxyType
7
+ from typing import TYPE_CHECKING
8
+
9
+ from .domain.provider import ModelSpec, UnknownModelError
10
+ from .domain.role import ROLE_DEFINITIONS, RoleCatalogError, normalize_role
11
+ from .domain.worker_exec import WorkerWriteCapability
12
+
13
+ if TYPE_CHECKING:
14
+ from .domain.provider import ProviderSpec
15
+ from .registry.provider_registry import ProviderRegistry
16
+
17
+
18
+ _BUNDLED_PROVIDER_ORDER = ("claude", "antigravity", "codex", "grok", "kimi")
19
+ _TOKEN = re.compile(r"[a-z0-9][a-z0-9._-]*\Z")
20
+ _VERSION_KINDS = frozenset({"pinned", "channel"})
21
+
22
+
23
+ class ModelRef(str):
24
+ """A canonical `<provider>/<model>` reference."""
25
+
26
+ def __new__(cls, provider_id: str, model_id: str) -> "ModelRef":
27
+ provider = provider_id.strip().lower()
28
+ model = model_id.strip().lower()
29
+ if not provider or not model:
30
+ raise ValueError("model ref requires a provider and model id")
31
+ value = str.__new__(cls, f"{provider}/{model}")
32
+ value.provider_id = provider
33
+ value.model_id = model
34
+ return value
35
+
36
+ @classmethod
37
+ def parse(cls, raw: str) -> "ModelRef":
38
+ provider, separator, model = raw.strip().partition("/")
39
+ if not separator:
40
+ raise ValueError(f"invalid model ref: {raw!r}")
41
+ return cls(provider, model)
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class ResolvedModel:
46
+ model_ref: ModelRef
47
+ provider_id: str
48
+ display_name: str
49
+ execution_value: str
50
+ aliases: tuple[str, ...]
51
+ version_kind: str
52
+ channel_family: str | None
53
+ selectable: bool
54
+ pricing: tuple[float, float, float] | None
55
+ supported_roles: frozenset[str] | None
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class ModelAvailability:
60
+ available: bool
61
+ reason: str
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class ProviderRuntimeFacts:
66
+ """Immutable provider facts required by assignment and rendering."""
67
+
68
+ provider: str
69
+ display_label: str
70
+ wrapper: str
71
+ supported_roles: frozenset[str]
72
+ cli_worker_write_capability: WorkerWriteCapability | None
73
+ execution_normalization_required: bool
74
+
75
+ def supports_role(self, role: str) -> bool:
76
+ try:
77
+ return normalize_role(role) in self.supported_roles
78
+ except RoleCatalogError:
79
+ return False
80
+
81
+
82
+ @dataclass(frozen=True)
83
+ class ModelPoolValidationFailure:
84
+ code: str
85
+ message: str
86
+
87
+
88
+ class ModelPoolValidationError(ValueError):
89
+ """Raised with every invalid adapter-catalog contract row."""
90
+
91
+ def __init__(self, failures: tuple[ModelPoolValidationFailure, ...]) -> None:
92
+ self.failures = failures
93
+ super().__init__("; ".join(f"{row.code}: {row.message}" for row in failures))
94
+
95
+
96
+ class ModelPool:
97
+ """One model-query surface composed from provider-owned catalog entries."""
98
+
99
+ def __init__(self, registry: "ProviderRegistry") -> None:
100
+ self._registry = registry
101
+ self._providers = self._ordered_providers()
102
+ self._provider_defaults = self._snapshot_provider_defaults()
103
+ self._provider_facts = self._snapshot_provider_facts()
104
+ self._models, self._aliases = self._index_models()
105
+
106
+ @classmethod
107
+ def from_registry(cls, registry: "ProviderRegistry") -> "ModelPool":
108
+ pool = cls(registry)
109
+ pool.validate()
110
+ return pool
111
+
112
+ def list(self, *, role: str | None = None) -> tuple[ResolvedModel, ...]:
113
+ canonical_role = self._normalize_role(role) if role else None
114
+ return tuple(
115
+ model
116
+ for model in self._models.values()
117
+ if model.selectable and self._supports_role(model, canonical_role)
118
+ )
119
+
120
+ def list_catalog(self, *, role: str | None = None) -> tuple[ResolvedModel, ...]:
121
+ canonical_role = self._normalize_role(role) if role else None
122
+ return tuple(
123
+ model
124
+ for model in self._models.values()
125
+ if self._supports_role(model, canonical_role)
126
+ )
127
+
128
+ def resolve(self, model_ref: str) -> ResolvedModel:
129
+ try:
130
+ return self._models[ModelRef.parse(model_ref)]
131
+ except (KeyError, ValueError) as exc:
132
+ raise UnknownModelError(f"unknown model ref {model_ref!r}") from exc
133
+
134
+ def resolve_alias(self, provider: str, alias: str) -> ResolvedModel:
135
+ provider_id = provider.strip().lower()
136
+ alias_id = alias.strip().lower()
137
+ try:
138
+ model_ref = self._aliases[(provider_id, alias_id)]
139
+ except KeyError as exc:
140
+ allowed = ", ".join(self.aliases_for(provider_id))
141
+ raise UnknownModelError(
142
+ f"{provider} model {alias!r} is not a supported alias. "
143
+ f"Allowed values: {allowed}"
144
+ ) from exc
145
+ return self.resolve(model_ref)
146
+
147
+ def resolve_provider(self, provider_id: str) -> ProviderRuntimeFacts:
148
+ """Return frozen assignment facts without leaking registry objects."""
149
+ normalized = provider_id.strip().lower()
150
+ try:
151
+ return self._provider_facts[normalized]
152
+ except KeyError:
153
+ self._registry.resolve(provider_id)
154
+ raise AssertionError("unreachable")
155
+
156
+ def default_candidates(self, role: str) -> tuple[ResolvedModel, ...]:
157
+ canonical_role = self._normalize_role(role)
158
+ candidates: list[ResolvedModel] = []
159
+ for provider_id in self._providers:
160
+ default = self._default_for_role(
161
+ self._provider_defaults[provider_id],
162
+ canonical_role,
163
+ )
164
+ if default is not None:
165
+ candidates.append(self.resolve_alias(provider_id, default))
166
+ return tuple(candidates)
167
+
168
+ def default_candidate(
169
+ self,
170
+ provider_id: str,
171
+ role: str,
172
+ ) -> ResolvedModel | None:
173
+ """Return one provider's role default without scanning other providers."""
174
+ canonical_role = self._normalize_role(role)
175
+ provider = self.resolve_provider(provider_id)
176
+ default = self._default_for_role(
177
+ self._provider_defaults[provider.provider],
178
+ canonical_role,
179
+ )
180
+ if default is None:
181
+ return None
182
+ return self.resolve_alias(provider.provider, default)
183
+
184
+ def availability(
185
+ self,
186
+ model_ref: str,
187
+ role: str,
188
+ host_runtime: str | None = None,
189
+ entry_mode: str | None = None,
190
+ ) -> ModelAvailability:
191
+ del host_runtime, entry_mode
192
+ try:
193
+ model = self.resolve(model_ref)
194
+ except UnknownModelError:
195
+ return ModelAvailability(False, "unknown-model")
196
+ if not model.selectable:
197
+ return ModelAvailability(False, "not-selectable")
198
+ try:
199
+ self._normalize_role(role)
200
+ except RoleCatalogError:
201
+ return ModelAvailability(False, "unknown-role")
202
+ return ModelAvailability(True, "available")
203
+
204
+ def validate(self) -> None:
205
+ failures = self._validation_failures()
206
+ if failures:
207
+ raise ModelPoolValidationError(tuple(failures))
208
+
209
+ def aliases_for(self, provider: str) -> tuple[str, ...]:
210
+ provider_id = provider.strip().lower()
211
+ return tuple(
212
+ alias
213
+ for indexed_provider, alias in self._aliases
214
+ if indexed_provider == provider_id
215
+ )
216
+
217
+ def _ordered_providers(self) -> tuple[str, ...]:
218
+ providers = self._registry.providers
219
+ bundled = tuple(provider for provider in _BUNDLED_PROVIDER_ORDER if provider in providers)
220
+ users = tuple(sorted(provider for provider in providers if provider not in bundled))
221
+ return (*bundled, *users)
222
+
223
+ def _snapshot_provider_facts(self) -> dict[str, ProviderRuntimeFacts]:
224
+ facts: dict[str, ProviderRuntimeFacts] = {}
225
+ canonical_roles = tuple(row.id for row in ROLE_DEFINITIONS)
226
+ for provider_id in self._providers:
227
+ spec = self._registry.resolve(provider_id)
228
+ capability = (
229
+ spec.exec_strategy.policy_support().worker_write_capability()
230
+ if spec.exec_strategy is not None
231
+ else None
232
+ )
233
+ facts[provider_id] = ProviderRuntimeFacts(
234
+ provider=spec.provider,
235
+ display_label=spec.display_label,
236
+ wrapper=spec.wrapper,
237
+ supported_roles=frozenset(
238
+ role for role in canonical_roles if spec.supports_role(role)
239
+ ),
240
+ cli_worker_write_capability=capability,
241
+ execution_normalization_required=(
242
+ spec.execution_normalizer is not None
243
+ ),
244
+ )
245
+ return facts
246
+
247
+ def _snapshot_provider_defaults(self):
248
+ return {
249
+ provider_id: MappingProxyType(dict(
250
+ self._registry.resolve(provider_id).default_models
251
+ ))
252
+ for provider_id in self._providers
253
+ }
254
+
255
+ def _index_models(
256
+ self,
257
+ ) -> tuple[dict[ModelRef, ResolvedModel], dict[tuple[str, str], ModelRef]]:
258
+ models: dict[ModelRef, ResolvedModel] = {}
259
+ aliases: dict[tuple[str, str], ModelRef] = {}
260
+ for provider_id in self._providers:
261
+ for mapping_key, spec in self._registry.resolve(provider_id).models.items():
262
+ model_ref = self._model_ref(provider_id, spec.model_id)
263
+ if model_ref is None:
264
+ continue
265
+ models.setdefault(model_ref, self._resolved_model(model_ref, spec))
266
+ for alias in (spec.model_id, mapping_key, *spec.aliases):
267
+ aliases.setdefault((provider_id, alias.strip().lower()), model_ref)
268
+ return models, aliases
269
+
270
+ @staticmethod
271
+ def _resolved_model(model_ref: ModelRef, spec: ModelSpec) -> ResolvedModel:
272
+ return ResolvedModel(
273
+ model_ref=model_ref,
274
+ provider_id=model_ref.provider_id,
275
+ display_name=spec.display_name,
276
+ execution_value=spec.execution_value,
277
+ aliases=spec.aliases,
278
+ version_kind=spec.version_kind,
279
+ channel_family=spec.channel_family,
280
+ selectable=spec.selectable,
281
+ pricing=spec.pricing,
282
+ supported_roles=spec.supported_roles,
283
+ )
284
+
285
+ def _validation_failures(self) -> list[ModelPoolValidationFailure]:
286
+ failures: list[ModelPoolValidationFailure] = []
287
+ seen_refs: set[ModelRef] = set()
288
+ aliases: dict[tuple[str, str], ModelRef] = {}
289
+ for provider_id in self._providers:
290
+ spec = self._registry.resolve(provider_id)
291
+ self._validate_provider_token(provider_id, failures)
292
+ self._validate_provider_roles(spec.supported_roles, provider_id, failures)
293
+ for role in spec.default_models:
294
+ self._validate_role(role, provider_id, failures)
295
+ channel_families: set[str] = set()
296
+ pinned_families: list[str] = []
297
+ for mapping_key, model in spec.models.items():
298
+ model_ref = self._model_ref(provider_id, model.model_id)
299
+ raw_ref = self._raw_model_ref(provider_id, model.model_id)
300
+ if model_ref is not None and model_ref in seen_refs:
301
+ failures.append(ModelPoolValidationFailure(
302
+ "duplicate-model-ref", str(model_ref)
303
+ ))
304
+ if model_ref is not None:
305
+ seen_refs.add(model_ref)
306
+ self._validate_model_structure(
307
+ provider_id, model, raw_ref, channel_families, pinned_families,
308
+ failures,
309
+ )
310
+ self._validate_model_roles(model, provider_id, raw_ref, failures)
311
+ if model_ref is None:
312
+ continue
313
+ for alias in (model.model_id, mapping_key, *model.aliases):
314
+ key = (provider_id, alias.strip().lower())
315
+ previous = aliases.get(key)
316
+ if previous is not None and previous != model_ref:
317
+ failures.append(ModelPoolValidationFailure(
318
+ "alias-collision", f"{provider_id}/{alias}"
319
+ ))
320
+ aliases[key] = model_ref
321
+ self._validate_pinned_families(
322
+ provider_id, pinned_families, channel_families, failures
323
+ )
324
+ self._validate_defaults(provider_id, failures)
325
+ return failures
326
+
327
+ @staticmethod
328
+ def _validate_provider_token(
329
+ provider_id: str,
330
+ failures: list[ModelPoolValidationFailure],
331
+ ) -> None:
332
+ if not _TOKEN.fullmatch(provider_id):
333
+ failures.append(ModelPoolValidationFailure(
334
+ "invalid-model-ref", f"invalid provider token: {provider_id!r}"
335
+ ))
336
+
337
+ @staticmethod
338
+ def _validate_model_structure(
339
+ provider_id: str,
340
+ model: ModelSpec,
341
+ model_ref: str,
342
+ channel_families: set[str],
343
+ pinned_families: list[str],
344
+ failures: list[ModelPoolValidationFailure],
345
+ ) -> None:
346
+ if not _TOKEN.fullmatch(provider_id) or not _TOKEN.fullmatch(model.model_id):
347
+ failures.append(ModelPoolValidationFailure(
348
+ "invalid-model-ref", f"invalid model token: {model_ref!r}"
349
+ ))
350
+ if not model.execution_value.strip():
351
+ failures.append(ModelPoolValidationFailure(
352
+ "missing-execution-value", str(model_ref)
353
+ ))
354
+ ModelPool._validate_model_family(
355
+ provider_id, model, model_ref, channel_families, pinned_families, failures
356
+ )
357
+
358
+ @staticmethod
359
+ def _validate_model_family(
360
+ provider_id: str,
361
+ model: ModelSpec,
362
+ model_ref: str,
363
+ channel_families: set[str],
364
+ pinned_families: list[str],
365
+ failures: list[ModelPoolValidationFailure],
366
+ ) -> None:
367
+ if model.version_kind not in _VERSION_KINDS:
368
+ failures.append(ModelPoolValidationFailure(
369
+ "invalid-version-kind", f"{model_ref}: {model.version_kind}"
370
+ ))
371
+ return
372
+ family = model.channel_family
373
+ if model.version_kind == "channel" and not family:
374
+ failures.append(ModelPoolValidationFailure(
375
+ "missing-channel-family", str(model_ref)
376
+ ))
377
+ return
378
+ if family and not _TOKEN.fullmatch(family):
379
+ failures.append(ModelPoolValidationFailure(
380
+ "invalid-channel-family", f"{model_ref}: {family!r}"
381
+ ))
382
+ return
383
+ if not family:
384
+ return
385
+ if model.version_kind == "channel":
386
+ channel_families.add(family)
387
+ else:
388
+ pinned_families.append(family)
389
+
390
+ @staticmethod
391
+ def _validate_pinned_families(
392
+ provider_id: str,
393
+ pinned_families: list[str],
394
+ channel_families: set[str],
395
+ failures: list[ModelPoolValidationFailure],
396
+ ) -> None:
397
+ for family in pinned_families:
398
+ if family not in channel_families:
399
+ failures.append(ModelPoolValidationFailure(
400
+ "orphan-channel-family", f"{provider_id}: {family}"
401
+ ))
402
+
403
+ @staticmethod
404
+ def _validate_provider_roles(
405
+ roles: frozenset[str], provider_id: str, failures: list[ModelPoolValidationFailure]
406
+ ) -> None:
407
+ for role in roles:
408
+ ModelPool._validate_role(role, provider_id, failures)
409
+
410
+ def _validate_model_roles(
411
+ self,
412
+ model: ModelSpec,
413
+ provider_id: str,
414
+ model_ref: str,
415
+ failures: list[ModelPoolValidationFailure],
416
+ ) -> None:
417
+ provider = self._registry.resolve(provider_id)
418
+ for role in model.supported_roles or ():
419
+ ModelPool._validate_role(role, str(model_ref), failures)
420
+ try:
421
+ canonical_role = normalize_role(role)
422
+ except RoleCatalogError:
423
+ continue
424
+ if not provider.supports_role(canonical_role):
425
+ failures.append(ModelPoolValidationFailure(
426
+ "model-role-exceeds-provider", f"{model_ref}: {canonical_role}"
427
+ ))
428
+
429
+ @staticmethod
430
+ def _validate_role(
431
+ role: str, owner: str, failures: list[ModelPoolValidationFailure]
432
+ ) -> None:
433
+ try:
434
+ normalize_role(role)
435
+ except RoleCatalogError:
436
+ failures.append(ModelPoolValidationFailure("unknown-role", f"{owner}: {role}"))
437
+
438
+ def _validate_defaults(
439
+ self,
440
+ provider_id: str,
441
+ failures: list[ModelPoolValidationFailure],
442
+ ) -> None:
443
+ provider = self._registry.resolve(provider_id)
444
+ for role in self._supported_roles(provider):
445
+ default = self._default_for_role(provider.default_models, role)
446
+ if default is None:
447
+ failures.append(ModelPoolValidationFailure(
448
+ "missing-default", f"{provider_id}: {role}"
449
+ ))
450
+ continue
451
+ try:
452
+ model = self.resolve_alias(provider_id, default)
453
+ except UnknownModelError:
454
+ failures.append(ModelPoolValidationFailure(
455
+ "missing-default", f"{provider_id}: {role} -> {default}"
456
+ ))
457
+ continue
458
+ if not model.selectable or not self._supports_role(model, role):
459
+ failures.append(ModelPoolValidationFailure(
460
+ "missing-default", f"{provider_id}: {role} -> {default}"
461
+ ))
462
+
463
+ @staticmethod
464
+ def _supported_roles(spec: object) -> tuple[str, ...]:
465
+ from .domain.role import ROLE_DEFINITIONS
466
+
467
+ return tuple(
468
+ definition.id
469
+ for definition in ROLE_DEFINITIONS
470
+ if spec.supports_role(definition.id)
471
+ )
472
+
473
+ @staticmethod
474
+ def _default_for_role(defaults: object, canonical_role: str) -> str | None:
475
+ for role, model_id in defaults.items():
476
+ try:
477
+ if normalize_role(role) == canonical_role:
478
+ return model_id
479
+ except RoleCatalogError:
480
+ continue
481
+ return None
482
+
483
+ def _supports_role(self, model: ResolvedModel, role: str | None) -> bool:
484
+ if role is None:
485
+ return True
486
+ return self.availability(model.model_ref, role).available
487
+
488
+ @staticmethod
489
+ def _normalize_role(role: str) -> str:
490
+ return normalize_role(role)
491
+
492
+ @staticmethod
493
+ def _model_ref(provider_id: str, model_id: str) -> ModelRef | None:
494
+ try:
495
+ return ModelRef(provider_id, model_id)
496
+ except ValueError:
497
+ return None
498
+
499
+ @staticmethod
500
+ def _raw_model_ref(provider_id: str, model_id: str) -> str:
501
+ return f"{provider_id}/{model_id}"
@@ -15,10 +15,14 @@ from .domain.provider import (
15
15
  UnknownModelError,
16
16
  UnknownProviderError,
17
17
  )
18
+ from .domain.role import RoleCatalogError, normalize_role
19
+ from .model_pool import ModelPool
18
20
  from .registry.provider_registry import default_provider_registry
19
21
 
20
22
 
21
- PROVIDERS = default_provider_registry().providers
23
+ _REGISTRY = default_provider_registry()
24
+ _MODEL_POOL = ModelPool.from_registry(_REGISTRY)
25
+ PROVIDERS = _REGISTRY.providers
22
26
  CLAUDE = PROVIDERS["claude"].models
23
27
  ANTIGRAVITY = PROVIDERS["antigravity"].models
24
28
  CODEX = PROVIDERS["codex"].models
@@ -95,7 +99,7 @@ def provider_ids(role: str | None = None) -> list[str]:
95
99
 
96
100
  def provider_supports_role(provider: str, role: str) -> bool:
97
101
  """Whether the provider has passed the registry gate for a role."""
98
- return role in provider_spec(provider).supported_roles
102
+ return provider_spec(provider).supports_role(role)
99
103
 
100
104
 
101
105
  def provider_wrappers(role: str | None = None) -> dict[str, str]:
@@ -110,7 +114,14 @@ def provider_wrappers(role: str | None = None) -> dict[str, str]:
110
114
 
111
115
  def provider_default_model(provider: str, role: str) -> Optional[str]:
112
116
  """Resolve a provider explicit default for one functional role."""
113
- return provider_spec(provider).default_models.get(role)
117
+ try:
118
+ canonical_role = normalize_role(role)
119
+ except RoleCatalogError:
120
+ return None
121
+ for model in _model_pool().default_candidates(canonical_role):
122
+ if model.provider_id == provider.strip().lower():
123
+ return model.model_ref.model_id
124
+ return None
114
125
 
115
126
 
116
127
  def resolve_model_metadata(
@@ -118,40 +129,49 @@ def resolve_model_metadata(
118
129
  ) -> ModelAssignment:
119
130
  """Resolve a model alias to its display and execution values."""
120
131
  raw_value = (raw_value or "").strip()
121
- mapping = default_provider_registry().providers.get(provider)
122
- models = mapping.models if mapping else None
123
- if raw_value and models is not None:
124
- normalized = raw_value.lower()
125
- if normalized not in models:
126
- allowed = ", ".join(sorted(models))
127
- raise UnknownModelError(
128
- f"{provider} model {raw_value!r} is not a supported alias. "
129
- f"Allowed values: {allowed}"
130
- )
132
+ registry = default_provider_registry()
133
+ mapping = registry.providers.get(provider)
134
+ if raw_value and mapping is not None:
135
+ model = _model_pool().resolve_alias(provider, raw_value)
136
+ return ModelAssignment(display=model.display_name, execution=model.execution_value)
131
137
  value = raw_value or default_display
132
- display = value
133
- execution = raw_value or default_execution
134
- normalized = value.strip().lower()
135
- if models and normalized in models:
136
- spec = models[normalized]
137
- display, execution = spec.display, spec.execution
138
- return ModelAssignment(display=display, execution=execution)
138
+ return ModelAssignment(display=value, execution=raw_value or default_execution)
139
139
 
140
140
 
141
141
  def picker_options(provider: str) -> list[str]:
142
142
  """Picker-exposed aliases for a provider, in catalog insertion order."""
143
- spec = default_provider_registry().providers.get(provider)
144
- models = spec.models if spec else {}
145
- return [alias for alias, model in models.items() if model.in_picker]
143
+ provider_id = provider.strip().lower()
144
+ return [
145
+ model.model_ref.model_id
146
+ for model in _model_pool().list()
147
+ if model.provider_id == provider_id
148
+ ]
146
149
 
147
150
 
148
151
  def default_model(role: str) -> Optional[str]:
149
152
  """Role-to-default model alias; environment overrides belong to the caller."""
150
- return ROLE_DEFAULTS.get(role)
153
+ if role in default_provider_registry().providers:
154
+ return provider_default_model(role, "leader")
155
+ try:
156
+ candidates = _model_pool().default_candidates(role)
157
+ except RoleCatalogError:
158
+ return None
159
+ return candidates[0].model_ref.model_id if candidates else None
151
160
 
152
161
 
153
162
  def catalog_pricing(provider: str) -> dict:
154
163
  """Selectable-model pricing as input, cached-input, output USD per million."""
155
- spec = default_provider_registry().providers.get(provider)
156
- models = spec.models if spec else {}
157
- return {alias: model.pricing for alias, model in models.items() if model.pricing is not None}
164
+ provider_id = provider.strip().lower()
165
+ return {
166
+ model.model_ref.model_id: model.pricing
167
+ for model in _model_pool().list()
168
+ if model.provider_id == provider_id and model.pricing is not None
169
+ }
170
+
171
+
172
+ def _model_pool() -> ModelPool:
173
+ """Return the pool built from the registry identity cached for this home."""
174
+ registry = default_provider_registry()
175
+ if registry is _REGISTRY:
176
+ return _MODEL_POOL
177
+ return ModelPool.from_registry(registry)