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
@@ -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,578 @@
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
+ # 같은 값이면 정본을 남기고 레거시를 접는다. 다르면 prepare 전에 거부.
87
+ legacy = _reconcile_legacy_with_canonical(counts, models, legacy)
88
+ merged_counts = {**counts, **legacy.role_counts}
89
+ merged_models = {**models, **legacy.role_models}
90
+ _validate_selection(profile, merged_counts, merged_models, counts)
91
+ return CanonicalModelSelection(
92
+ merged_counts,
93
+ merged_models,
94
+ legacy.provider_constraints,
95
+ legacy.legacy_model_values,
96
+ legacy.legacy_sources,
97
+ )
98
+
99
+
100
+ def _parse_role_counts(raw_values: Sequence[str]) -> dict[str, int]:
101
+ counts: dict[str, int] = {}
102
+ for raw in raw_values:
103
+ role, value = _split_role_value(raw, "--role-count")
104
+ if role in counts:
105
+ raise ModelSelectionInputError(f"duplicate --role-count role: {role}")
106
+ try:
107
+ count = int(value)
108
+ except ValueError as exc:
109
+ raise ModelSelectionInputError(
110
+ f"--role-count requires role=<non-negative integer>: {raw!r}"
111
+ ) from exc
112
+ if count < 0 or str(count) != value:
113
+ raise ModelSelectionInputError(
114
+ f"--role-count requires role=<non-negative integer>: {raw!r}"
115
+ )
116
+ counts[role] = count
117
+ return counts
118
+
119
+
120
+ def _parse_role_models(raw_values: Sequence[str]) -> dict[str, tuple[str, ...]]:
121
+ grouped: dict[str, list[str]] = {}
122
+ for raw in raw_values:
123
+ role, model_ref = _split_role_value(raw, "--role-model")
124
+ grouped.setdefault(role, []).append(model_ref)
125
+ return {role: tuple(values) for role, values in grouped.items()}
126
+
127
+
128
+ def _split_role_value(raw: str, option: str) -> tuple[str, str]:
129
+ role_raw, separator, value = raw.partition("=")
130
+ role_raw = role_raw.strip()
131
+ value = value.strip()
132
+ if not separator or not role_raw or not value:
133
+ raise ModelSelectionInputError(f"{option} requires role=value: {raw!r}")
134
+ try:
135
+ role = normalize_role(role_raw)
136
+ except RoleCatalogError as exc:
137
+ raise ModelSelectionInputError(str(exc)) from exc
138
+ return role, value
139
+
140
+
141
+ def _legacy_selection(
142
+ *,
143
+ profile: RoleProfile,
144
+ workers: str,
145
+ worker_model: str,
146
+ critic: str,
147
+ executor: str,
148
+ lead_provider: str,
149
+ lead_model: str,
150
+ report_writer_provider: str,
151
+ report_writer_model: str,
152
+ provider_models: Mapping[str, str],
153
+ ) -> CanonicalModelSelection:
154
+ counts: dict[str, int] = {}
155
+ constraints: dict[str, tuple[str, ...]] = {}
156
+ values: dict[str, dict[str, str]] = {}
157
+ sources: list[str] = []
158
+ needs_initial = bool(
159
+ workers.strip()
160
+ or worker_model.strip()
161
+ or any(model.strip() for model in provider_models.values())
162
+ )
163
+ initial = (
164
+ _initial_cross_verification_requirement(profile)
165
+ if needs_initial
166
+ else None
167
+ )
168
+ if workers.strip() and initial is not None:
169
+ providers = _legacy_worker_providers(workers)
170
+ _validate_legacy_workers(initial, providers)
171
+ counts[initial.role] = len(providers)
172
+ constraints[initial.role] = providers
173
+ report_writer = _requirement(profile, "report-writer")
174
+ if report_writer is not None:
175
+ counts["report-writer"] = report_writer.recommended_count
176
+ sources.append("--workers")
177
+ worker_values = _parse_worker_models(worker_model, provider_models)
178
+ if worker_values and initial is not None:
179
+ values[initial.role] = worker_values
180
+ sources.append("worker-model")
181
+ _add_provider_selection(
182
+ constraints, values, sources, "leader", lead_provider, lead_model, "lead"
183
+ )
184
+ _add_provider_selection(
185
+ constraints,
186
+ values,
187
+ sources,
188
+ "report-writer",
189
+ report_writer_provider,
190
+ report_writer_model,
191
+ "report-writer",
192
+ )
193
+ _add_simple_legacy_roles(counts, constraints, sources, critic, executor)
194
+ return CanonicalModelSelection(counts, {}, constraints, values, tuple(sources))
195
+
196
+
197
+ def _initial_cross_verification_requirement(profile: RoleProfile) -> RoleRequirement:
198
+ for requirement in profile.roles:
199
+ if not requirement.dynamic and requirement.role in {
200
+ "analyser", "designer", "planner", "verifier"
201
+ }:
202
+ return requirement
203
+ raise ModelSelectionInputError(
204
+ "profile has no initial cross-verification role for legacy --workers"
205
+ )
206
+
207
+
208
+ def _legacy_worker_providers(workers: str) -> tuple[str, ...]:
209
+ tokens = tuple(
210
+ token.strip().lower() for token in workers.split(",") if token.strip()
211
+ )
212
+ return tuple(token for token in tokens if token != "report-writer")
213
+
214
+
215
+ def _validate_legacy_workers(
216
+ requirement: RoleRequirement,
217
+ providers: tuple[str, ...],
218
+ ) -> None:
219
+ # minDistinctProviders 제거. 모델 참조 중복 거부는 Task 2.
220
+ if (
221
+ len(providers) < requirement.min_count
222
+ or len(providers) > requirement.max_count
223
+ ):
224
+ raise ModelSelectionInputError(
225
+ f"legacy --workers selects {len(providers)} {requirement.role} instances; "
226
+ f"profile requires {requirement.min_count}..{requirement.max_count}"
227
+ )
228
+
229
+
230
+ def _parse_worker_models(
231
+ worker_model: str,
232
+ provider_models: Mapping[str, str],
233
+ ) -> dict[str, str]:
234
+ values = {
235
+ provider.strip().lower(): model.strip()
236
+ for provider, model in provider_models.items()
237
+ if model.strip()
238
+ }
239
+ for item in worker_model.split(","):
240
+ if not item.strip():
241
+ continue
242
+ provider, separator, model = item.partition("=")
243
+ provider = provider.strip().lower()
244
+ model = model.strip()
245
+ if not separator or not provider or not model:
246
+ raise ModelSelectionInputError(
247
+ "--worker-model must use provider=model entries separated by commas"
248
+ )
249
+ if provider in values:
250
+ raise ModelSelectionInputError(
251
+ f"duplicate legacy worker model provider: {provider}"
252
+ )
253
+ values[provider] = model
254
+ return values
255
+
256
+
257
+ def _add_provider_selection(
258
+ constraints: dict[str, tuple[str, ...]],
259
+ values: dict[str, dict[str, str]],
260
+ sources: list[str],
261
+ role: str,
262
+ provider_raw: str,
263
+ model_raw: str,
264
+ source: str,
265
+ ) -> None:
266
+ provider = provider_raw.strip().lower()
267
+ model = model_raw.strip()
268
+ if not provider and not model:
269
+ return
270
+ constraints[role] = (provider,)
271
+ if model:
272
+ values[role] = {provider: model}
273
+ sources.append(source)
274
+
275
+
276
+ def _add_simple_legacy_roles(
277
+ counts: dict[str, int],
278
+ constraints: dict[str, tuple[str, ...]],
279
+ sources: list[str],
280
+ critic_raw: str,
281
+ executor_raw: str,
282
+ ) -> None:
283
+ critic = critic_raw.strip().lower()
284
+ if critic:
285
+ counts["critic"] = 0 if critic == "off" else 1
286
+ if critic != "off":
287
+ constraints["critic"] = (critic,)
288
+ sources.append("critic")
289
+ executor = executor_raw.strip().lower()
290
+ if executor:
291
+ constraints["implementer"] = (executor,)
292
+ sources.append("executor")
293
+
294
+
295
+ def _reconcile_legacy_with_canonical(
296
+ canonical_counts: Mapping[str, int],
297
+ canonical_models: Mapping[str, tuple[str, ...]],
298
+ legacy: CanonicalModelSelection,
299
+ ) -> CanonicalModelSelection:
300
+ """충돌하는 레거시 입력을 거부하고, 같은 값 겹침은 정본만 남긴다."""
301
+ canonical_roles = set(canonical_counts) | set(canonical_models)
302
+ legacy_roles = (
303
+ set(legacy.role_counts)
304
+ | set(legacy.provider_constraints)
305
+ | set(legacy.legacy_model_values)
306
+ )
307
+ overlap = sorted(canonical_roles & legacy_roles)
308
+ if not overlap:
309
+ return legacy
310
+
311
+ counts = dict(legacy.role_counts)
312
+ constraints = dict(legacy.provider_constraints)
313
+ values = {
314
+ role: dict(models)
315
+ for role, models in legacy.legacy_model_values.items()
316
+ }
317
+ for role in overlap:
318
+ if not _legacy_matches_canonical(
319
+ role,
320
+ canonical_counts,
321
+ canonical_models,
322
+ legacy,
323
+ ):
324
+ raise ModelSelectionInputError(
325
+ "canonical and legacy model selections conflict for role "
326
+ f"{role!r}"
327
+ )
328
+ # 정본 모델이 있으면 레거시 공급자 제약을 접어 materialize 덮어쓰기를 막는다.
329
+ if role in canonical_models:
330
+ constraints.pop(role, None)
331
+ values.pop(role, None)
332
+ if role in canonical_counts:
333
+ counts.pop(role, None)
334
+ return CanonicalModelSelection(
335
+ counts,
336
+ {},
337
+ constraints,
338
+ values,
339
+ legacy.legacy_sources,
340
+ )
341
+
342
+
343
+ def _legacy_matches_canonical(
344
+ role: str,
345
+ canonical_counts: Mapping[str, int],
346
+ canonical_models: Mapping[str, tuple[str, ...]],
347
+ legacy: CanonicalModelSelection,
348
+ ) -> bool:
349
+ """레거시(--workers 등)가 변환 후 정본과 같은 값으로 수렴하는지 본다."""
350
+ leg_count = legacy.role_counts.get(role)
351
+ can_count = canonical_counts.get(role)
352
+ if can_count is not None and leg_count is not None and can_count != leg_count:
353
+ return False
354
+
355
+ leg_providers = legacy.provider_constraints.get(role)
356
+ can_models = canonical_models.get(role)
357
+ if can_models is not None and leg_providers is not None:
358
+ can_providers = tuple(
359
+ _provider_from_model_ref(model_ref) for model_ref in can_models
360
+ )
361
+ if can_providers != tuple(
362
+ provider.lower() for provider in leg_providers
363
+ ):
364
+ return False
365
+ # 워커 목록은 칸 전체 순서다. 칸 수가 다르면 다른 선택이다.
366
+ if len(can_models) != len(leg_providers):
367
+ return False
368
+ leg_models = legacy.legacy_model_values.get(role, {})
369
+ for provider, model_ref in zip(leg_providers, can_models):
370
+ explicit = leg_models.get(provider)
371
+ if not explicit:
372
+ continue
373
+ model_id = (
374
+ model_ref.split("/", 1)[1] if "/" in model_ref else model_ref
375
+ )
376
+ if explicit != model_id and model_ref != f"{provider}/{explicit}":
377
+ return False
378
+ return True
379
+
380
+ if can_count is not None and leg_providers is not None:
381
+ if can_count != len(leg_providers):
382
+ return False
383
+ if (
384
+ can_models is not None
385
+ and leg_count is not None
386
+ and leg_providers is None
387
+ and len(can_models) > leg_count
388
+ ):
389
+ return False
390
+ return True
391
+
392
+
393
+ def _provider_from_model_ref(model_ref: str) -> str:
394
+ provider, separator, _model = model_ref.partition("/")
395
+ if separator:
396
+ return provider.strip().lower()
397
+ return model_ref.strip().lower()
398
+
399
+
400
+ def _validate_selection(
401
+ profile: RoleProfile,
402
+ counts: Mapping[str, int],
403
+ models: Mapping[str, tuple[str, ...]],
404
+ explicit_counts: Mapping[str, int],
405
+ ) -> None:
406
+ requirements = {row.role: row for row in profile.roles if not row.dynamic}
407
+ requirements["leader"] = RoleRequirement("leader", 1, 1, 1, "lead")
408
+ for role in sorted(set(counts) | set(models)):
409
+ requirement = requirements.get(role)
410
+ if requirement is None:
411
+ raise ModelSelectionInputError(
412
+ f"role {role!r} is not static in the profile"
413
+ )
414
+ _validate_role_count(
415
+ role,
416
+ counts,
417
+ requirement,
418
+ explicit=role in explicit_counts,
419
+ )
420
+ selected_count = counts.get(role, requirement.recommended_count)
421
+ model_count = len(models.get(role, ()))
422
+ if model_count > selected_count:
423
+ if role not in explicit_counts:
424
+ raise ModelSelectionInputError(
425
+ f"set --role-count {role}={model_count} before assigning "
426
+ f"{model_count} models"
427
+ )
428
+ raise ModelSelectionInputError(
429
+ f"role {role!r} has {model_count} models for {selected_count} instances"
430
+ )
431
+
432
+
433
+ def _validate_role_count(
434
+ role: str,
435
+ counts: Mapping[str, int],
436
+ requirement: RoleRequirement,
437
+ *,
438
+ explicit: bool,
439
+ ) -> None:
440
+ if role not in counts:
441
+ return
442
+ count = counts[role]
443
+ fixed = requirement.min_count == requirement.max_count
444
+ if explicit and fixed:
445
+ raise ModelSelectionInputError(
446
+ f"role {role!r} has a fixed quantity and cannot receive --role-count"
447
+ )
448
+ if fixed and count != requirement.min_count:
449
+ raise ModelSelectionInputError(
450
+ f"role {role!r} has a fixed quantity of {requirement.min_count}"
451
+ )
452
+ if count < requirement.min_count or count > requirement.max_count:
453
+ raise ModelSelectionInputError(
454
+ f"role {role!r} count must be in "
455
+ f"{requirement.min_count}..{requirement.max_count}: {count}"
456
+ )
457
+
458
+
459
+ def _requirement(profile: RoleProfile, role: str) -> RoleRequirement | None:
460
+ return next((row for row in profile.roles if row.role == role), None)
461
+
462
+
463
+ def serialize_host_session_context(context: HostSessionContext) -> str:
464
+ """Serialize host-owned session facts as stable canonical JSON."""
465
+ current = context.current_model
466
+ payload = {
467
+ "availableFunctions": sorted(context.available_functions),
468
+ "currentModel": {
469
+ "effort": current.effort,
470
+ "level": current.level,
471
+ "normalizedModelRef": current.normalized_model_ref,
472
+ "observedModel": current.observed_model,
473
+ "providerId": current.provider_id,
474
+ "source": current.source,
475
+ },
476
+ "entryMode": context.entry_mode,
477
+ "hostId": context.host_id,
478
+ "interactionSurface": context.interaction_surface,
479
+ }
480
+ return json.dumps(
481
+ payload,
482
+ ensure_ascii=False,
483
+ separators=(",", ":"),
484
+ sort_keys=True,
485
+ )
486
+
487
+
488
+ def deserialize_host_session_context(payload: str) -> HostSessionContext:
489
+ """Deserialize only the shared context schema; never infer model identity."""
490
+ if not payload:
491
+ return HostSessionContext(
492
+ host_id="",
493
+ entry_mode="spawn-process",
494
+ available_functions=frozenset(),
495
+ interaction_surface="unavailable",
496
+ current_model=CurrentSessionModelAttestation.unknown(""),
497
+ )
498
+ try:
499
+ raw = json.loads(payload)
500
+ except json.JSONDecodeError as exc:
501
+ raise ModelSelectionInputError("invalid host session context JSON") from exc
502
+ if not isinstance(raw, dict):
503
+ raise ModelSelectionInputError("host session context must be a JSON object")
504
+ current = _deserialize_attestation(raw.get("currentModel"))
505
+ return HostSessionContext(
506
+ host_id=_required_string(raw, "hostId"),
507
+ entry_mode=_enum(raw, "entryMode", {"spawn-process", "current-session"}),
508
+ available_functions=frozenset(_string_list(raw, "availableFunctions")),
509
+ interaction_surface=_required_string(raw, "interactionSurface"),
510
+ current_model=current,
511
+ )
512
+
513
+
514
+ def _deserialize_attestation(raw: object) -> CurrentSessionModelAttestation:
515
+ if raw is None:
516
+ return CurrentSessionModelAttestation.unknown("")
517
+ if not isinstance(raw, dict):
518
+ raise ModelSelectionInputError("currentModel must be a JSON object")
519
+ level = _enum(raw, "level", {"exact", "channel", "unknown"})
520
+ source = _enum(
521
+ raw,
522
+ "source",
523
+ {"host-session-metadata", "native-api", "cli-handoff", "unavailable"},
524
+ )
525
+ observed = _nullable_string(raw, "observedModel")
526
+ normalized = _nullable_string(raw, "normalizedModelRef")
527
+ # 키 없으면 null. 호스트 어댑터가 아직 안 채운 경우를 허용한다.
528
+ effort = _nullable_string(raw, "effort")
529
+ if level == "unknown" and (observed is not None or normalized is not None):
530
+ raise ModelSelectionInputError("unknown currentModel must use null model fields")
531
+ if level != "unknown" and normalized is None:
532
+ raise ModelSelectionInputError(
533
+ "attested currentModel requires normalizedModelRef"
534
+ )
535
+ return CurrentSessionModelAttestation(
536
+ _required_string(raw, "providerId"),
537
+ observed,
538
+ normalized,
539
+ level,
540
+ source,
541
+ effort,
542
+ )
543
+
544
+
545
+ def _required_string(raw: Mapping[str, object], key: str) -> str:
546
+ value = raw.get(key)
547
+ if not isinstance(value, str):
548
+ raise ModelSelectionInputError(f"host session context {key} must be a string")
549
+ return value
550
+
551
+
552
+ def _nullable_string(raw: Mapping[str, object], key: str) -> str | None:
553
+ value = raw.get(key)
554
+ if value is not None and not isinstance(value, str):
555
+ raise ModelSelectionInputError(
556
+ f"host session context {key} must be a string or null"
557
+ )
558
+ return value
559
+
560
+
561
+ def _enum(raw: Mapping[str, object], key: str, allowed: set[str]) -> str:
562
+ value = _required_string(raw, key)
563
+ if value not in allowed:
564
+ raise ModelSelectionInputError(
565
+ f"host session context {key} must be one of: {', '.join(sorted(allowed))}"
566
+ )
567
+ return value
568
+
569
+
570
+ def _string_list(raw: Mapping[str, object], key: str) -> tuple[str, ...]:
571
+ value = raw.get(key)
572
+ if not isinstance(value, list) or not all(
573
+ isinstance(item, str) for item in value
574
+ ):
575
+ raise ModelSelectionInputError(
576
+ f"host session context {key} must be an array of strings"
577
+ )
578
+ return tuple(value)
@@ -5,6 +5,7 @@ from pathlib import Path
5
5
 
6
6
  from okstra_project import TASKS_RELATIVE, StateError, parse_task_key, read_task_catalog, read_task_manifest, slugify
7
7
 
8
+ from . import next_phase
8
9
  from .manager_paths import children_json_path, projects_json_path, snapshots_json_path, task_manifest_path
9
10
  from .manager_store import _now_iso, _read_json, _read_json_default, _write_json
10
11
 
@@ -84,7 +85,9 @@ def _snapshot_child(project_root: Path, child: dict) -> dict:
84
85
  "currentPhase": str(workflow.get("currentPhase") or ""),
85
86
  "currentPhaseState": str(workflow.get("currentPhaseState") or ""),
86
87
  "lastCompletedPhase": str(workflow.get("lastCompletedPhase") or ""),
87
- "nextRecommendedPhase": str(workflow.get("nextRecommendedPhase") or ""),
88
+ "nextRecommendedPhase": next_phase.promote(
89
+ workflow.get("nextRecommendedPhase")
90
+ ),
88
91
  "latestRunStatus": str(manifest.get("latestRunStatus") or manifest.get("currentStatus") or ""),
89
92
  "latestReportPath": str(manifest.get("latestReportPath") or ""),
90
93
  "finalVerdict": str(outcome.get("finalVerdict") or ""),