okstra 0.173.0 → 0.175.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (402) 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/dist/commands/execute/team.mjs +56 -0
  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/dist/commands/execute/worktree-status.mjs +115 -0
  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/dist/commands/report/finalize.mjs +64 -0
  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 +17 -3
  232. package/docs/architecture.md +12 -22
  233. package/docs/cli.md +29 -8
  234. package/docs/container.md +1 -1
  235. package/docs/contributor-change-matrix.md +3 -3
  236. package/docs/project-structure-overview.md +29 -20
  237. package/docs/task-process/README.md +3 -1
  238. package/docs/task-process/implementation-planning.md +2 -2
  239. package/docs/task-process/implementation.md +1 -1
  240. package/package.json +11 -5
  241. package/runtime/BUILD.json +2 -2
  242. package/runtime/agents/workers/claude-worker.md +3 -2
  243. package/runtime/agents/workers/report-writer-worker.md +3 -1
  244. package/runtime/agents/workers/translator-worker.md +3 -1
  245. package/runtime/bin/lib/okstra/cli.sh +12 -0
  246. package/runtime/bin/lib/okstra/globals.sh +4 -1
  247. package/runtime/bin/lib/okstra/usage.sh +4 -4
  248. package/runtime/bin/okstra-error-log.py +27 -0
  249. package/runtime/bin/okstra-provider-exec.py +245 -16
  250. package/runtime/bin/okstra-trace-cleanup.sh +58 -129
  251. package/runtime/bin/okstra.sh +10 -0
  252. package/runtime/prompts/lead/adapters/cmux.md +2 -0
  253. package/runtime/prompts/lead/convergence.md +27 -18
  254. package/runtime/prompts/lead/okstra-lead-contract.md +4 -2
  255. package/runtime/prompts/lead/plan-body-verification.md +13 -6
  256. package/runtime/prompts/lead/report-writer.md +6 -6
  257. package/runtime/prompts/lead/team-contract.md +1 -1
  258. package/runtime/prompts/profiles/_common-contract.md +2 -2
  259. package/runtime/prompts/profiles/_implementation-executor.md +2 -0
  260. package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
  261. package/runtime/prompts/profiles/change-impact-analysis.md +16 -0
  262. package/runtime/prompts/profiles/error-analysis.md +21 -1
  263. package/runtime/prompts/profiles/feature-analysis.md +16 -0
  264. package/runtime/prompts/profiles/final-verification.md +16 -0
  265. package/runtime/prompts/profiles/implementation-option-selection.md +16 -0
  266. package/runtime/prompts/profiles/implementation-planning.md +32 -9
  267. package/runtime/prompts/profiles/implementation.md +17 -1
  268. package/runtime/prompts/profiles/improvement-discovery.md +16 -0
  269. package/runtime/prompts/profiles/project-analysis.md +16 -0
  270. package/runtime/prompts/profiles/release-handoff.md +5 -1
  271. package/runtime/prompts/profiles/requirements-discovery.md +20 -0
  272. package/runtime/prompts/wizard/prompts.ko.json +18 -0
  273. package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +1 -6
  274. package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +8 -0
  275. package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +8 -0
  276. package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
  277. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +2 -2
  278. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +25 -5
  279. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
  280. package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +4 -1
  281. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +4 -4
  282. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
  283. package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +2 -2
  284. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +79 -11
  285. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +100 -25
  286. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +59 -14
  287. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +55 -9
  288. package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +51 -12
  289. package/runtime/python/okstra_ctl/agent_activity.py +5 -1
  290. package/runtime/python/okstra_ctl/agent_invocation.py +608 -14
  291. package/runtime/python/okstra_ctl/agent_prompt_cli.py +249 -50
  292. package/runtime/python/okstra_ctl/application/resolve_assignment.py +53 -12
  293. package/runtime/python/okstra_ctl/assignment_environment.py +102 -0
  294. package/runtime/python/okstra_ctl/assignment_resolver.py +790 -0
  295. package/runtime/python/okstra_ctl/attempt_evidence.py +342 -0
  296. package/runtime/python/okstra_ctl/conformance.py +68 -0
  297. package/runtime/python/okstra_ctl/convergence.py +409 -3
  298. package/runtime/python/okstra_ctl/convergence_engine.py +248 -15
  299. package/runtime/python/okstra_ctl/convergence_store.py +170 -1
  300. package/runtime/python/okstra_ctl/dispatch_core.py +1181 -102
  301. package/runtime/python/okstra_ctl/dispatch_state.py +835 -22
  302. package/runtime/python/okstra_ctl/doctor.py +16 -2
  303. package/runtime/python/okstra_ctl/domain/host.py +32 -1
  304. package/runtime/python/okstra_ctl/domain/provider.py +212 -6
  305. package/runtime/python/okstra_ctl/domain/role.py +125 -0
  306. package/runtime/python/okstra_ctl/domain/wizard/interaction.py +13 -0
  307. package/runtime/python/okstra_ctl/domain/worker_exec.py +39 -1
  308. package/runtime/python/okstra_ctl/domain/worker_role.py +13 -3
  309. package/runtime/python/okstra_ctl/domain/worker_stream.py +24 -0
  310. package/runtime/python/okstra_ctl/error_log_write.py +9 -0
  311. package/runtime/python/okstra_ctl/error_report.py +21 -1
  312. package/runtime/python/okstra_ctl/execution_identity.py +529 -0
  313. package/runtime/python/okstra_ctl/execution_manifest.py +1281 -0
  314. package/runtime/python/okstra_ctl/execution_mutation_audit.py +619 -0
  315. package/runtime/python/okstra_ctl/final_report_schema.py +5 -4
  316. package/runtime/python/okstra_ctl/index.py +6 -2
  317. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +43 -3
  318. package/runtime/python/okstra_ctl/legacy_model_selection.py +490 -0
  319. package/runtime/python/okstra_ctl/model_cli.py +278 -0
  320. package/runtime/python/okstra_ctl/model_defaults.py +46 -0
  321. package/runtime/python/okstra_ctl/model_discovery.py +26 -7
  322. package/runtime/python/okstra_ctl/model_pool.py +501 -0
  323. package/runtime/python/okstra_ctl/models.py +47 -27
  324. package/runtime/python/okstra_ctl/mutation_recovery.py +293 -0
  325. package/runtime/python/okstra_ctl/pane_reclaim.py +13 -22
  326. package/runtime/python/okstra_ctl/pane_title.py +151 -0
  327. package/runtime/python/okstra_ctl/ports/host_model.py +48 -17
  328. package/runtime/python/okstra_ctl/ports/worker_dispatch.py +4 -0
  329. package/runtime/python/okstra_ctl/registry/provider_registry.py +8 -4
  330. package/runtime/python/okstra_ctl/render.py +78 -10
  331. package/runtime/python/okstra_ctl/render_final_report.py +34 -19
  332. package/runtime/python/okstra_ctl/report_contract.py +39 -1
  333. package/runtime/python/okstra_ctl/report_finalize.py +84 -9
  334. package/runtime/python/okstra_ctl/report_html/render.py +1 -0
  335. package/runtime/python/okstra_ctl/role_requirements.py +217 -0
  336. package/runtime/python/okstra_ctl/run.py +1250 -260
  337. package/runtime/python/okstra_ctl/run_index_row.py +7 -1
  338. package/runtime/python/okstra_ctl/schema_excerpt.py +1 -1
  339. package/runtime/python/okstra_ctl/scope_provenance.py +1 -1
  340. package/runtime/python/okstra_ctl/session.py +69 -12
  341. package/runtime/python/okstra_ctl/team.py +51 -25
  342. package/runtime/python/okstra_ctl/time_report.py +38 -4
  343. package/runtime/python/okstra_ctl/tmux.py +19 -149
  344. package/runtime/python/okstra_ctl/usage_identity.py +86 -0
  345. package/runtime/python/okstra_ctl/usage_report.py +15 -3
  346. package/runtime/python/okstra_ctl/wizard.py +1253 -62
  347. package/runtime/python/okstra_ctl/worker_audit_ledger.py +68 -65
  348. package/runtime/python/okstra_ctl/worker_request.py +9 -1
  349. package/runtime/python/okstra_ctl/worker_runner.py +130 -5
  350. package/runtime/python/okstra_ctl/workers.py +7 -5
  351. package/runtime/python/okstra_ctl/worktree.py +69 -3
  352. package/runtime/python/okstra_ctl/write_policy.py +571 -0
  353. package/runtime/python/okstra_token_usage/cli.py +1 -1
  354. package/runtime/python/okstra_token_usage/collect.py +66 -6
  355. package/runtime/schemas/convergence-groups-v2.0.schema.json +128 -0
  356. package/runtime/schemas/execution-manifest-v2.schema.json +340 -0
  357. package/runtime/schemas/final-report-v2.0.schema.json +25 -0
  358. package/runtime/skills/okstra-run/SKILL.md +9 -9
  359. package/runtime/skills/okstra-setup/references/project-config.md +11 -0
  360. package/runtime/templates/report-writer-prompt-preamble.md +2 -0
  361. package/runtime/templates/reports/final-report-v2.template.md +7 -1
  362. package/runtime/templates/reports/html/base.template.html +21 -0
  363. package/runtime/templates/reports/html/i18n/en.json +4 -0
  364. package/runtime/templates/reports/html/i18n/ko.json +4 -0
  365. package/runtime/templates/reports/settings.template.json +0 -24
  366. package/runtime/templates/worker-prompt-preamble.md +6 -2
  367. package/runtime/validators/lib/fixtures.sh +98 -33
  368. package/runtime/validators/validate-implementation-plan-stages.py +63 -3
  369. package/runtime/validators/validate-run.py +443 -508
  370. package/runtime/validators/validate_session_conformance.py +26 -8
  371. package/runtime/bin/okstra-subagent-reclaim.sh +0 -26
  372. package/runtime/schemas/final-report-v1.0.schema.json +0 -6366
  373. package/runtime/templates/reports/final-report.template.md +0 -1258
  374. package/src/cli-registry.mjs +0 -532
  375. package/src/commands/execute/codex-dispatch.mjs +0 -10
  376. package/src/commands/execute/plan-items.mjs +0 -9
  377. package/src/commands/execute/plan-validate.mjs +0 -68
  378. package/src/commands/execute/run.mjs +0 -208
  379. package/src/commands/execute/team.mjs +0 -63
  380. package/src/commands/execute/wizard.mjs +0 -167
  381. package/src/commands/inspect/stage-map.mjs +0 -98
  382. package/src/commands/inspect/task-list.mjs +0 -97
  383. package/src/commands/inspect/task-show.mjs +0 -100
  384. package/src/commands/lifecycle/check-project.mjs +0 -187
  385. package/src/commands/lifecycle/config.mjs +0 -385
  386. package/src/commands/lifecycle/doctor.mjs +0 -308
  387. package/src/commands/lifecycle/install.mjs +0 -1172
  388. package/src/commands/lifecycle/paths.mjs +0 -104
  389. package/src/commands/lifecycle/preflight.mjs +0 -125
  390. package/src/commands/lifecycle/setup.mjs +0 -333
  391. package/src/commands/lifecycle/uninstall.mjs +0 -279
  392. package/src/commands/memory/memory.mjs +0 -524
  393. package/src/commands/pr/pr.mjs +0 -261
  394. package/src/commands/report/finalize.mjs +0 -64
  395. package/src/lib/host-registry-client.mjs +0 -176
  396. package/src/lib/paths.mjs +0 -86
  397. package/src/lib/proc.mjs +0 -31
  398. package/src/lib/python-helper.mjs +0 -142
  399. package/src/lib/runtime-manifest.mjs +0 -70
  400. package/src/lib/skill-catalog.mjs +0 -53
  401. package/src/lib/version.mjs +0 -20
  402. /package/{src → dist}/commands/pr/default.md +0 -0
@@ -18,6 +18,7 @@ from .agent_invocation import (
18
18
  AgentInvocationRequest,
19
19
  agent_model_assignment_from_payload,
20
20
  compose_agent_prompt,
21
+ invocation_execution_identity_from_manifest,
21
22
  prepare_agent_invocation,
22
23
  verify_agent_invocation,
23
24
  )
@@ -127,6 +128,11 @@ _MODEL_ASSIGNMENT_KEYS = {
127
128
  "hostRuntime",
128
129
  "hostModelValue",
129
130
  }
131
+ _V2_EXECUTION_IDENTITY_KEYS = {
132
+ "participantRef",
133
+ "roleExecutionRef",
134
+ "dutyId",
135
+ }
130
136
  _RUN_INVOCATION_PATH_KEYS = {
131
137
  "runManifestPath",
132
138
  "workerPromptsDirectoryPath",
@@ -259,6 +265,26 @@ def _validate_invocation_manifest_authority(
259
265
  return
260
266
  contract = manifest.get("agentContract")
261
267
  assignments = manifest.get("invocationAssignments")
268
+ schema = manifest.get("schemaVersion")
269
+ identity_version = manifest.get("executionIdentityVersion")
270
+ is_v1 = schema in (None, 1, "1", "1.0") and identity_version is None
271
+ is_v2 = schema == "2.0" and identity_version == 2
272
+ if not is_v1 and not is_v2:
273
+ raise InitialPromptMaterializationError(
274
+ "required_input_missing",
275
+ "invocation manifest mixes v1 and v2 execution identity",
276
+ )
277
+ allowed_assignment_shapes = {frozenset(_MODEL_ASSIGNMENT_KEYS)}
278
+ if is_v2 and isinstance(assignments, Mapping):
279
+ if any(
280
+ isinstance(item, Mapping)
281
+ and bool(set(item) & _V2_EXECUTION_IDENTITY_KEYS)
282
+ for item in assignments.values()
283
+ ):
284
+ raise InitialPromptMaterializationError(
285
+ "required_input_missing",
286
+ "invocation assignment duplicates v2 execution identity",
287
+ )
262
288
  complete = (
263
289
  isinstance(contract, Mapping)
264
290
  and set(contract) == _AGENT_CONTRACT_KEYS
@@ -267,7 +293,7 @@ def _validate_invocation_manifest_authority(
267
293
  and bool(assignments)
268
294
  and all(
269
295
  isinstance(item, Mapping)
270
- and set(item) == _MODEL_ASSIGNMENT_KEYS
296
+ and frozenset(item) in allowed_assignment_shapes
271
297
  for item in assignments.values()
272
298
  )
273
299
  and all(
@@ -453,6 +479,12 @@ def _agent_invocation_request(
453
479
  payload = assignments.get(assignment_ref) if isinstance(assignments, Mapping) else None
454
480
  try:
455
481
  assignment = agent_model_assignment_from_payload(payload)
482
+ identity = invocation_execution_identity_from_manifest(
483
+ context.manifest,
484
+ assignment=assignment,
485
+ assignment_ref=assignment_ref,
486
+ duty_id=item.plan.duty_audience,
487
+ )
456
488
  except AgentInvocationError as exc:
457
489
  raise InitialPromptMaterializationError(
458
490
  "required_input_missing", str(exc)
@@ -475,9 +507,10 @@ def _agent_invocation_request(
475
507
  "project", _required_string(context.manifest, "teamStatePath")
476
508
  ),
477
509
  )
510
+ invocation_id = _worker_invocation_id(context.manifest, item.worker.worker_id)
478
511
  return AgentInvocationRequest(
479
- invocation_id=_worker_invocation_id(context.manifest, item.worker.worker_id),
480
- worker_id=item.worker.worker_id,
512
+ invocation_id=invocation_id,
513
+ worker_id=item.worker.worker_id if identity is None else None,
481
514
  audience=item.plan.duty_audience,
482
515
  assignment_ref=assignment_ref,
483
516
  purpose=None,
@@ -493,6 +526,13 @@ def _agent_invocation_request(
493
526
  prompt_path=item.final_path,
494
527
  metadata_path=item.final_path.with_name(item.final_path.name + ".meta.json"),
495
528
  dispatch_kind="initial",
529
+ participant_ref=identity.participant_ref if identity is not None else None,
530
+ role_execution_ref=(
531
+ identity.role_execution_ref if identity is not None else None
532
+ ),
533
+ duty_id=identity.duty_id if identity is not None else None,
534
+ invocation_ref=invocation_id if identity is not None else None,
535
+ attempt=1,
496
536
  )
497
537
 
498
538
 
@@ -0,0 +1,490 @@
1
+ """Normalize canonical role selections and legacy provider CLI inputs."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from dataclasses import dataclass
6
+ from types import MappingProxyType
7
+ from typing import Mapping, Sequence
8
+
9
+ from .domain.host import (
10
+ CurrentSessionModelAttestation,
11
+ HostSessionContext,
12
+ )
13
+ from .domain.role import RoleCatalogError, normalize_role
14
+ from .role_requirements import RoleProfile, RoleRequirement
15
+
16
+
17
+ class ModelSelectionInputError(ValueError):
18
+ """Raised before prepare side effects when role selection is invalid."""
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class CanonicalModelSelection:
23
+ role_counts: Mapping[str, int]
24
+ role_models: Mapping[str, tuple[str, ...]]
25
+ provider_constraints: Mapping[str, tuple[str, ...]]
26
+ legacy_model_values: Mapping[str, Mapping[str, str]]
27
+ legacy_sources: tuple[str, ...]
28
+
29
+ def __post_init__(self) -> None:
30
+ model_values = {
31
+ role: MappingProxyType(dict(values))
32
+ for role, values in self.legacy_model_values.items()
33
+ }
34
+ object.__setattr__(
35
+ self,
36
+ "role_counts",
37
+ MappingProxyType(dict(self.role_counts)),
38
+ )
39
+ object.__setattr__(
40
+ self,
41
+ "role_models",
42
+ MappingProxyType(dict(self.role_models)),
43
+ )
44
+ object.__setattr__(
45
+ self,
46
+ "provider_constraints",
47
+ MappingProxyType(dict(self.provider_constraints)),
48
+ )
49
+ object.__setattr__(
50
+ self,
51
+ "legacy_model_values",
52
+ MappingProxyType(model_values),
53
+ )
54
+
55
+
56
+ def normalize_model_selection_inputs(
57
+ *,
58
+ profile: RoleProfile,
59
+ role_counts_raw: Sequence[str],
60
+ role_models_raw: Sequence[str],
61
+ workers: str,
62
+ worker_model: str,
63
+ critic: str,
64
+ executor: str,
65
+ lead_provider: str,
66
+ lead_model: str,
67
+ report_writer_provider: str,
68
+ report_writer_model: str,
69
+ provider_models: Mapping[str, str],
70
+ ) -> CanonicalModelSelection:
71
+ """Return one ordered role-selection view for canonical and legacy inputs."""
72
+ counts = _parse_role_counts(role_counts_raw)
73
+ models = _parse_role_models(role_models_raw)
74
+ legacy = _legacy_selection(
75
+ profile=profile,
76
+ workers=workers,
77
+ worker_model=worker_model,
78
+ critic=critic,
79
+ executor=executor,
80
+ lead_provider=lead_provider,
81
+ lead_model=lead_model,
82
+ report_writer_provider=report_writer_provider,
83
+ report_writer_model=report_writer_model,
84
+ provider_models=provider_models,
85
+ )
86
+ _reject_source_conflicts(counts, models, legacy)
87
+ merged_counts = {**counts, **legacy.role_counts}
88
+ merged_models = {**models, **legacy.role_models}
89
+ _validate_selection(profile, merged_counts, merged_models, counts)
90
+ return CanonicalModelSelection(
91
+ merged_counts,
92
+ merged_models,
93
+ legacy.provider_constraints,
94
+ legacy.legacy_model_values,
95
+ legacy.legacy_sources,
96
+ )
97
+
98
+
99
+ def _parse_role_counts(raw_values: Sequence[str]) -> dict[str, int]:
100
+ counts: dict[str, int] = {}
101
+ for raw in raw_values:
102
+ role, value = _split_role_value(raw, "--role-count")
103
+ if role in counts:
104
+ raise ModelSelectionInputError(f"duplicate --role-count role: {role}")
105
+ try:
106
+ count = int(value)
107
+ except ValueError as exc:
108
+ raise ModelSelectionInputError(
109
+ f"--role-count requires role=<non-negative integer>: {raw!r}"
110
+ ) from exc
111
+ if count < 0 or str(count) != value:
112
+ raise ModelSelectionInputError(
113
+ f"--role-count requires role=<non-negative integer>: {raw!r}"
114
+ )
115
+ counts[role] = count
116
+ return counts
117
+
118
+
119
+ def _parse_role_models(raw_values: Sequence[str]) -> dict[str, tuple[str, ...]]:
120
+ grouped: dict[str, list[str]] = {}
121
+ for raw in raw_values:
122
+ role, model_ref = _split_role_value(raw, "--role-model")
123
+ grouped.setdefault(role, []).append(model_ref)
124
+ return {role: tuple(values) for role, values in grouped.items()}
125
+
126
+
127
+ def _split_role_value(raw: str, option: str) -> tuple[str, str]:
128
+ role_raw, separator, value = raw.partition("=")
129
+ role_raw = role_raw.strip()
130
+ value = value.strip()
131
+ if not separator or not role_raw or not value:
132
+ raise ModelSelectionInputError(f"{option} requires role=value: {raw!r}")
133
+ try:
134
+ role = normalize_role(role_raw)
135
+ except RoleCatalogError as exc:
136
+ raise ModelSelectionInputError(str(exc)) from exc
137
+ return role, value
138
+
139
+
140
+ def _legacy_selection(
141
+ *,
142
+ profile: RoleProfile,
143
+ workers: str,
144
+ worker_model: str,
145
+ critic: str,
146
+ executor: str,
147
+ lead_provider: str,
148
+ lead_model: str,
149
+ report_writer_provider: str,
150
+ report_writer_model: str,
151
+ provider_models: Mapping[str, str],
152
+ ) -> CanonicalModelSelection:
153
+ counts: dict[str, int] = {}
154
+ constraints: dict[str, tuple[str, ...]] = {}
155
+ values: dict[str, dict[str, str]] = {}
156
+ sources: list[str] = []
157
+ needs_initial = bool(
158
+ workers.strip()
159
+ or worker_model.strip()
160
+ or any(model.strip() for model in provider_models.values())
161
+ )
162
+ initial = (
163
+ _initial_cross_verification_requirement(profile)
164
+ if needs_initial
165
+ else None
166
+ )
167
+ if workers.strip() and initial is not None:
168
+ providers = _legacy_worker_providers(workers)
169
+ _validate_legacy_workers(initial, providers)
170
+ counts[initial.role] = len(providers)
171
+ constraints[initial.role] = providers
172
+ report_writer = _requirement(profile, "report-writer")
173
+ if report_writer is not None:
174
+ counts["report-writer"] = report_writer.count
175
+ sources.append("--workers")
176
+ worker_values = _parse_worker_models(worker_model, provider_models)
177
+ if worker_values and initial is not None:
178
+ values[initial.role] = worker_values
179
+ sources.append("worker-model")
180
+ _add_provider_selection(
181
+ constraints, values, sources, "leader", lead_provider, lead_model, "lead"
182
+ )
183
+ _add_provider_selection(
184
+ constraints,
185
+ values,
186
+ sources,
187
+ "report-writer",
188
+ report_writer_provider,
189
+ report_writer_model,
190
+ "report-writer",
191
+ )
192
+ _add_simple_legacy_roles(counts, constraints, sources, critic, executor)
193
+ return CanonicalModelSelection(counts, {}, constraints, values, tuple(sources))
194
+
195
+
196
+ def _initial_cross_verification_requirement(profile: RoleProfile) -> RoleRequirement:
197
+ for requirement in profile.roles:
198
+ if not requirement.dynamic and requirement.role in {
199
+ "analyser", "designer", "planner", "verifier"
200
+ }:
201
+ return requirement
202
+ raise ModelSelectionInputError(
203
+ "profile has no initial cross-verification role for legacy --workers"
204
+ )
205
+
206
+
207
+ def _legacy_worker_providers(workers: str) -> tuple[str, ...]:
208
+ tokens = tuple(
209
+ token.strip().lower() for token in workers.split(",") if token.strip()
210
+ )
211
+ return tuple(token for token in tokens if token != "report-writer")
212
+
213
+
214
+ def _validate_legacy_workers(
215
+ requirement: RoleRequirement,
216
+ providers: tuple[str, ...],
217
+ ) -> None:
218
+ maximum = requirement.count + requirement.optional_count
219
+ diversity = requirement.min_distinct_providers
220
+ if diversity is not None and len(set(providers)) < diversity:
221
+ raise ModelSelectionInputError(
222
+ "legacy --workers roster violates "
223
+ f"minDistinctProviders={diversity}; select at least {diversity} "
224
+ "providers or migrate to --role-model"
225
+ )
226
+ if len(providers) < requirement.count or len(providers) > maximum:
227
+ raise ModelSelectionInputError(
228
+ f"legacy --workers selects {len(providers)} {requirement.role} instances; "
229
+ f"profile requires {requirement.count}..{maximum}"
230
+ )
231
+
232
+
233
+ def _parse_worker_models(
234
+ worker_model: str,
235
+ provider_models: Mapping[str, str],
236
+ ) -> dict[str, str]:
237
+ values = {
238
+ provider.strip().lower(): model.strip()
239
+ for provider, model in provider_models.items()
240
+ if model.strip()
241
+ }
242
+ for item in worker_model.split(","):
243
+ if not item.strip():
244
+ continue
245
+ provider, separator, model = item.partition("=")
246
+ provider = provider.strip().lower()
247
+ model = model.strip()
248
+ if not separator or not provider or not model:
249
+ raise ModelSelectionInputError(
250
+ "--worker-model must use provider=model entries separated by commas"
251
+ )
252
+ if provider in values:
253
+ raise ModelSelectionInputError(
254
+ f"duplicate legacy worker model provider: {provider}"
255
+ )
256
+ values[provider] = model
257
+ return values
258
+
259
+
260
+ def _add_provider_selection(
261
+ constraints: dict[str, tuple[str, ...]],
262
+ values: dict[str, dict[str, str]],
263
+ sources: list[str],
264
+ role: str,
265
+ provider_raw: str,
266
+ model_raw: str,
267
+ source: str,
268
+ ) -> None:
269
+ provider = provider_raw.strip().lower()
270
+ model = model_raw.strip()
271
+ if not provider and not model:
272
+ return
273
+ constraints[role] = (provider,)
274
+ if model:
275
+ values[role] = {provider: model}
276
+ sources.append(source)
277
+
278
+
279
+ def _add_simple_legacy_roles(
280
+ counts: dict[str, int],
281
+ constraints: dict[str, tuple[str, ...]],
282
+ sources: list[str],
283
+ critic_raw: str,
284
+ executor_raw: str,
285
+ ) -> None:
286
+ critic = critic_raw.strip().lower()
287
+ if critic:
288
+ counts["critic"] = 0 if critic == "off" else 1
289
+ if critic != "off":
290
+ constraints["critic"] = (critic,)
291
+ sources.append("critic")
292
+ executor = executor_raw.strip().lower()
293
+ if executor:
294
+ constraints["implementer"] = (executor,)
295
+ sources.append("executor")
296
+
297
+
298
+ def _reject_source_conflicts(
299
+ canonical_counts: Mapping[str, int],
300
+ canonical_models: Mapping[str, tuple[str, ...]],
301
+ legacy: CanonicalModelSelection,
302
+ ) -> None:
303
+ canonical_roles = set(canonical_counts) | set(canonical_models)
304
+ legacy_roles = (
305
+ set(legacy.role_counts)
306
+ | set(legacy.provider_constraints)
307
+ | set(legacy.legacy_model_values)
308
+ )
309
+ conflicts = sorted(canonical_roles & legacy_roles)
310
+ if conflicts:
311
+ raise ModelSelectionInputError(
312
+ "canonical and legacy model selections conflict for role "
313
+ f"{conflicts[0]!r}"
314
+ )
315
+
316
+
317
+ def _validate_selection(
318
+ profile: RoleProfile,
319
+ counts: Mapping[str, int],
320
+ models: Mapping[str, tuple[str, ...]],
321
+ explicit_counts: Mapping[str, int],
322
+ ) -> None:
323
+ requirements = {row.role: row for row in profile.roles if not row.dynamic}
324
+ requirements["leader"] = RoleRequirement("leader", 1, "lead")
325
+ for role in sorted(set(counts) | set(models)):
326
+ requirement = requirements.get(role)
327
+ if requirement is None:
328
+ raise ModelSelectionInputError(
329
+ f"role {role!r} is not static in the profile"
330
+ )
331
+ _validate_role_count(
332
+ role,
333
+ counts,
334
+ requirement,
335
+ explicit=role in explicit_counts,
336
+ )
337
+ selected_count = counts.get(role, requirement.count)
338
+ model_count = len(models.get(role, ()))
339
+ if model_count > selected_count:
340
+ if role not in explicit_counts:
341
+ raise ModelSelectionInputError(
342
+ f"set --role-count {role}={model_count} before assigning "
343
+ f"{model_count} models"
344
+ )
345
+ raise ModelSelectionInputError(
346
+ f"role {role!r} has {model_count} models for {selected_count} instances"
347
+ )
348
+
349
+
350
+ def _validate_role_count(
351
+ role: str,
352
+ counts: Mapping[str, int],
353
+ requirement: RoleRequirement,
354
+ *,
355
+ explicit: bool,
356
+ ) -> None:
357
+ if role not in counts:
358
+ return
359
+ count = counts[role]
360
+ maximum = requirement.count + requirement.optional_count
361
+ if explicit and requirement.optional_count == 0:
362
+ raise ModelSelectionInputError(
363
+ f"role {role!r} has a fixed quantity and cannot receive --role-count"
364
+ )
365
+ if requirement.optional_count == 0 and count != requirement.count:
366
+ raise ModelSelectionInputError(
367
+ f"role {role!r} has a fixed quantity of {requirement.count}"
368
+ )
369
+ if count < requirement.count or count > maximum:
370
+ raise ModelSelectionInputError(
371
+ f"role {role!r} count must be in {requirement.count}..{maximum}: {count}"
372
+ )
373
+
374
+
375
+ def _requirement(profile: RoleProfile, role: str) -> RoleRequirement | None:
376
+ return next((row for row in profile.roles if row.role == role), None)
377
+
378
+
379
+ def serialize_host_session_context(context: HostSessionContext) -> str:
380
+ """Serialize host-owned session facts as stable canonical JSON."""
381
+ current = context.current_model
382
+ payload = {
383
+ "availableFunctions": sorted(context.available_functions),
384
+ "currentModel": {
385
+ "level": current.level,
386
+ "normalizedModelRef": current.normalized_model_ref,
387
+ "observedModel": current.observed_model,
388
+ "providerId": current.provider_id,
389
+ "source": current.source,
390
+ },
391
+ "entryMode": context.entry_mode,
392
+ "hostId": context.host_id,
393
+ "interactionSurface": context.interaction_surface,
394
+ }
395
+ return json.dumps(
396
+ payload,
397
+ ensure_ascii=False,
398
+ separators=(",", ":"),
399
+ sort_keys=True,
400
+ )
401
+
402
+
403
+ def deserialize_host_session_context(payload: str) -> HostSessionContext:
404
+ """Deserialize only the shared context schema; never infer model identity."""
405
+ if not payload:
406
+ return HostSessionContext(
407
+ host_id="",
408
+ entry_mode="spawn-process",
409
+ available_functions=frozenset(),
410
+ interaction_surface="unavailable",
411
+ current_model=CurrentSessionModelAttestation.unknown(""),
412
+ )
413
+ try:
414
+ raw = json.loads(payload)
415
+ except json.JSONDecodeError as exc:
416
+ raise ModelSelectionInputError("invalid host session context JSON") from exc
417
+ if not isinstance(raw, dict):
418
+ raise ModelSelectionInputError("host session context must be a JSON object")
419
+ current = _deserialize_attestation(raw.get("currentModel"))
420
+ return HostSessionContext(
421
+ host_id=_required_string(raw, "hostId"),
422
+ entry_mode=_enum(raw, "entryMode", {"spawn-process", "current-session"}),
423
+ available_functions=frozenset(_string_list(raw, "availableFunctions")),
424
+ interaction_surface=_required_string(raw, "interactionSurface"),
425
+ current_model=current,
426
+ )
427
+
428
+
429
+ def _deserialize_attestation(raw: object) -> CurrentSessionModelAttestation:
430
+ if raw is None:
431
+ return CurrentSessionModelAttestation.unknown("")
432
+ if not isinstance(raw, dict):
433
+ raise ModelSelectionInputError("currentModel must be a JSON object")
434
+ level = _enum(raw, "level", {"exact", "channel", "unknown"})
435
+ source = _enum(
436
+ raw,
437
+ "source",
438
+ {"host-session-metadata", "native-api", "cli-handoff", "unavailable"},
439
+ )
440
+ observed = _nullable_string(raw, "observedModel")
441
+ normalized = _nullable_string(raw, "normalizedModelRef")
442
+ if level == "unknown" and (observed is not None or normalized is not None):
443
+ raise ModelSelectionInputError("unknown currentModel must use null model fields")
444
+ if level != "unknown" and normalized is None:
445
+ raise ModelSelectionInputError(
446
+ "attested currentModel requires normalizedModelRef"
447
+ )
448
+ return CurrentSessionModelAttestation(
449
+ _required_string(raw, "providerId"),
450
+ observed,
451
+ normalized,
452
+ level,
453
+ source,
454
+ )
455
+
456
+
457
+ def _required_string(raw: Mapping[str, object], key: str) -> str:
458
+ value = raw.get(key)
459
+ if not isinstance(value, str):
460
+ raise ModelSelectionInputError(f"host session context {key} must be a string")
461
+ return value
462
+
463
+
464
+ def _nullable_string(raw: Mapping[str, object], key: str) -> str | None:
465
+ value = raw.get(key)
466
+ if value is not None and not isinstance(value, str):
467
+ raise ModelSelectionInputError(
468
+ f"host session context {key} must be a string or null"
469
+ )
470
+ return value
471
+
472
+
473
+ def _enum(raw: Mapping[str, object], key: str, allowed: set[str]) -> str:
474
+ value = _required_string(raw, key)
475
+ if value not in allowed:
476
+ raise ModelSelectionInputError(
477
+ f"host session context {key} must be one of: {', '.join(sorted(allowed))}"
478
+ )
479
+ return value
480
+
481
+
482
+ def _string_list(raw: Mapping[str, object], key: str) -> tuple[str, ...]:
483
+ value = raw.get(key)
484
+ if not isinstance(value, list) or not all(
485
+ isinstance(item, str) for item in value
486
+ ):
487
+ raise ModelSelectionInputError(
488
+ f"host session context {key} must be an array of strings"
489
+ )
490
+ return tuple(value)