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
@@ -36,6 +36,20 @@ class DoctorCheck:
36
36
  return asdict(self)
37
37
 
38
38
 
39
+ def model_pool_diagnostics(
40
+ *,
41
+ host_runtime: str = "claude-code",
42
+ cwd: str | Path | None = None,
43
+ ) -> dict[str, object]:
44
+ """Diagnose the model pool without making an inference call."""
45
+ from .model_cli import model_diagnostics
46
+
47
+ return model_diagnostics(
48
+ host_runtime=host_runtime,
49
+ cwd=None if cwd is None else Path(cwd),
50
+ )
51
+
52
+
39
53
  def phase_diagnostics(
40
54
  phase: str,
41
55
  *,
@@ -1,11 +1,41 @@
1
1
  """Host selection and session value objects."""
2
2
  from __future__ import annotations
3
3
 
4
- from dataclasses import dataclass
4
+ from dataclasses import dataclass, field
5
5
  from pathlib import Path
6
6
  from typing import Literal, Mapping
7
7
 
8
8
 
9
+ @dataclass(frozen=True)
10
+ class CurrentSessionModelAttestation:
11
+ """Host-observed identity of the already running lead session."""
12
+
13
+ provider_id: str
14
+ observed_model: str | None
15
+ normalized_model_ref: str | None
16
+ level: Literal["exact", "channel", "unknown"]
17
+ source: Literal[
18
+ "host-session-metadata",
19
+ "native-api",
20
+ "cli-handoff",
21
+ "unavailable",
22
+ ]
23
+ # 호스트가 알려 줄 때만 채운다. 스크레이핑하지 않는다.
24
+ effort: str | None = None
25
+
26
+ @classmethod
27
+ def unknown(
28
+ cls,
29
+ provider_id: str,
30
+ source: Literal["unavailable"] = "unavailable",
31
+ ) -> CurrentSessionModelAttestation:
32
+ return cls(provider_id, None, None, "unknown", source, None)
33
+
34
+
35
+ def _unavailable_current_model() -> CurrentSessionModelAttestation:
36
+ return CurrentSessionModelAttestation.unknown("")
37
+
38
+
9
39
  @dataclass(frozen=True)
10
40
  class HostDescriptor:
11
41
  id: str
@@ -29,6 +59,9 @@ class HostSessionContext:
29
59
  entry_mode: Literal["spawn-process", "current-session"]
30
60
  available_functions: frozenset[str]
31
61
  interaction_surface: str
62
+ current_model: CurrentSessionModelAttestation = field(
63
+ default_factory=_unavailable_current_model
64
+ )
32
65
 
33
66
 
34
67
  @dataclass(frozen=True)
@@ -1,19 +1,168 @@
1
1
  """Provider and model value objects shared by the registry and adapters."""
2
2
  from __future__ import annotations
3
3
 
4
- from dataclasses import dataclass
5
- from typing import TYPE_CHECKING, Mapping, Optional
4
+ from collections.abc import Callable
5
+ from dataclasses import dataclass, field
6
+ from typing import TYPE_CHECKING, Literal, Mapping, Optional, Protocol
7
+
8
+ from .role import RoleCatalogError, normalize_role, ROLE_CATALOG
9
+ from .worker_exec import WorkerWriteCapability
6
10
 
7
11
  if TYPE_CHECKING:
8
12
  from okstra_ctl.domain.worker_exec import ExecutionStrategy
9
13
 
10
14
 
15
+ _MISSING = object()
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class ServedModelAttestation:
20
+ """Provider-observed model identity after one execution attempt."""
21
+
22
+ observed_model: str | None
23
+ normalized_model_ref: str | None
24
+ level: Literal["exact", "channel", "unknown"]
25
+ source: Literal[
26
+ "provider-output",
27
+ "native-api",
28
+ "cli-handoff",
29
+ "unavailable",
30
+ ]
31
+
32
+ @classmethod
33
+ def unknown(cls) -> ServedModelAttestation:
34
+ return cls(None, None, "unknown", "unavailable")
35
+
36
+
37
+ ServedModelNormalizer = Callable[[str | None], ServedModelAttestation]
38
+
39
+
40
+ def unavailable_served_model(_raw_model: str | None) -> ServedModelAttestation:
41
+ return ServedModelAttestation.unknown()
42
+
43
+
11
44
  @dataclass(frozen=True)
45
+ class HostModelBinding:
46
+ """Final provider value and host-native value for one selected model."""
47
+
48
+ runner: str
49
+ catalog_execution_value: str
50
+ resolved_execution_value: str
51
+ host_model_value: str | None
52
+ binding_fidelity: Literal["exact", "channel", "unsupported"]
53
+ worker_write_capability: WorkerWriteCapability | None
54
+
55
+
56
+ @dataclass(frozen=True, init=False)
12
57
  class ModelSpec:
13
- display: str
14
- execution: str
15
- pricing: Optional[tuple[float, float, float]] = None
16
- in_picker: bool = False
58
+ """One canonical model plus provider-owned aliases and execution value."""
59
+
60
+ model_id: str
61
+ display_name: str
62
+ execution_value: str
63
+ aliases: tuple[str, ...]
64
+ version_kind: Literal["pinned", "channel"]
65
+ channel_family: str | None
66
+ supported_roles: frozenset[str] | None
67
+ selectable: bool
68
+ pricing: Optional[tuple[float, float, float]]
69
+
70
+ def __init__(
71
+ self,
72
+ model_id: str,
73
+ display_name: str,
74
+ execution_value: str | tuple[float, float, float] | None | object = _MISSING,
75
+ aliases: tuple[str, ...] = (),
76
+ version_kind: Literal["pinned", "channel"] = "pinned",
77
+ channel_family: str | None = None,
78
+ supported_roles: frozenset[str] | None = None,
79
+ selectable: bool = True,
80
+ pricing: Optional[tuple[float, float, float]] = None,
81
+ *,
82
+ in_picker: bool | None = None,
83
+ ) -> None:
84
+ """Accept the former display/execution form while adapters migrate."""
85
+ is_legacy = execution_value is _MISSING or not isinstance(execution_value, str)
86
+ if is_legacy:
87
+ legacy_pricing = execution_value if isinstance(execution_value, tuple) else None
88
+ canonical_id = display_name
89
+ resolved_display = model_id
90
+ resolved_execution = display_name
91
+ resolved_pricing = legacy_pricing
92
+ else:
93
+ canonical_id = model_id
94
+ resolved_display = display_name
95
+ resolved_execution = execution_value
96
+ resolved_pricing = pricing
97
+ object.__setattr__(self, "model_id", canonical_id)
98
+ object.__setattr__(self, "display_name", resolved_display)
99
+ object.__setattr__(self, "execution_value", resolved_execution)
100
+ object.__setattr__(self, "aliases", aliases)
101
+ object.__setattr__(self, "version_kind", version_kind)
102
+ object.__setattr__(self, "channel_family", channel_family)
103
+ object.__setattr__(self, "supported_roles", supported_roles)
104
+ object.__setattr__(self, "selectable", selectable if in_picker is None else in_picker)
105
+ object.__setattr__(self, "pricing", resolved_pricing)
106
+
107
+ @property
108
+ def display(self) -> str:
109
+ """Legacy display-name view."""
110
+ return self.display_name
111
+
112
+ @property
113
+ def execution(self) -> str:
114
+ """Legacy execution-value view."""
115
+ return self.execution_value
116
+
117
+ @property
118
+ def in_picker(self) -> bool:
119
+ """Legacy picker view."""
120
+ return self.selectable
121
+
122
+
123
+ @dataclass(frozen=True)
124
+ class ExecutionNormalization:
125
+ """One precomputed CLI execution value or its discovery-time error."""
126
+
127
+ resolved_execution_value: str | None
128
+ error: str = ""
129
+
130
+ def __post_init__(self) -> None:
131
+ if bool(self.resolved_execution_value) == bool(self.error):
132
+ raise ValueError(
133
+ "execution normalization requires exactly one value or error"
134
+ )
135
+
136
+
137
+ class ExecutionNormalizer(Protocol):
138
+ """Provider-owned batch snapshot boundary for ambient CLI discovery."""
139
+
140
+ def snapshot(
141
+ self,
142
+ models: tuple[ModelSpec, ...],
143
+ roles: tuple[str, ...],
144
+ ) -> Mapping[tuple[str, str], ExecutionNormalization]: ...
145
+
146
+
147
+ def snapshot_execution_normalizations(
148
+ models: tuple[ModelSpec, ...],
149
+ roles: tuple[str, ...],
150
+ normalize: Callable[[str, str], str],
151
+ ) -> Mapping[tuple[str, str], ExecutionNormalization]:
152
+ """Evaluate a pure provider normalizer for every model/role pair."""
153
+ outcomes: dict[tuple[str, str], ExecutionNormalization] = {}
154
+ for model in models:
155
+ for role in roles:
156
+ try:
157
+ resolved = normalize(model.execution_value, role)
158
+ except Exception as exc: # provider errors become immutable facts
159
+ outcomes[(model.model_id, role)] = ExecutionNormalization(
160
+ None,
161
+ str(exc),
162
+ )
163
+ else:
164
+ outcomes[(model.model_id, role)] = ExecutionNormalization(resolved)
165
+ return outcomes
17
166
 
18
167
 
19
168
  @dataclass(frozen=True)
@@ -37,8 +186,37 @@ class ProviderSpec:
37
186
  default_models: Mapping[str, str]
38
187
  wrapper: str
39
188
  supported_roles: frozenset[str]
189
+ execution_capabilities: frozenset[str] = frozenset()
40
190
  lead_launch: Optional[LeadLaunchSpec] = None
41
191
  exec_strategy: Optional["ExecutionStrategy"] = None
192
+ execution_normalizer: Optional[ExecutionNormalizer] = None
193
+ served_model_normalizer: ServedModelNormalizer = unavailable_served_model
194
+ _legacy_role_restriction: frozenset[str] = field(
195
+ init=False,
196
+ repr=False,
197
+ compare=False,
198
+ )
199
+
200
+ def __post_init__(self) -> None:
201
+ legacy_roles = _normalized_legacy_roles(self.supported_roles)
202
+ if self.execution_capabilities:
203
+ capabilities = self.execution_capabilities
204
+ restriction = frozenset()
205
+ else:
206
+ capabilities = _legacy_execution_capabilities(legacy_roles)
207
+ restriction = legacy_roles
208
+ object.__setattr__(self, "execution_capabilities", capabilities)
209
+ object.__setattr__(self, "_legacy_role_restriction", restriction)
210
+
211
+ def supports_role(self, role: str) -> bool:
212
+ try:
213
+ canonical_role = normalize_role(role)
214
+ except RoleCatalogError:
215
+ return False
216
+ if self._legacy_role_restriction:
217
+ return canonical_role in self._legacy_role_restriction
218
+ required = ROLE_CATALOG.required_capabilities(canonical_role)
219
+ return required.issubset(self.execution_capabilities)
42
220
 
43
221
 
44
222
  @dataclass(frozen=True)
@@ -60,6 +238,8 @@ class LeadProviderAssignment:
60
238
  class ModelAssignment:
61
239
  display: str
62
240
  execution: str
241
+ model_ref: str = ""
242
+ version_kind: Literal["pinned", "channel"] = "pinned"
63
243
 
64
244
 
65
245
  class UnknownModelError(ValueError):
@@ -68,3 +248,29 @@ class UnknownModelError(ValueError):
68
248
 
69
249
  class UnknownProviderError(ValueError):
70
250
  """Raised when a requested provider is absent from the registry."""
251
+
252
+
253
+ def _normalized_legacy_roles(roles: frozenset[str]) -> frozenset[str]:
254
+ normalized: set[str] = set()
255
+ for role in roles:
256
+ try:
257
+ normalized.add(normalize_role(role))
258
+ except RoleCatalogError:
259
+ continue
260
+ return frozenset(normalized)
261
+
262
+
263
+ def _legacy_execution_capabilities(roles: frozenset[str]) -> frozenset[str]:
264
+ capabilities: set[str] = set()
265
+ for role in roles:
266
+ if role == "leader":
267
+ capabilities.add("lead-session")
268
+ continue
269
+ capabilities.add("worker-artifact-io")
270
+ if role == "analyser":
271
+ capabilities.add("source-readonly")
272
+ if role == "implementer":
273
+ capabilities.add("project-mutation")
274
+ if role in {"report-writer", "translator"}:
275
+ capabilities.add("extended-artifact-authoring")
276
+ return frozenset(capabilities)
@@ -0,0 +1,125 @@
1
+ """Canonical role identities and the duties that execute them."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from typing import Mapping, Sequence
6
+
7
+
8
+ class RoleCatalogError(ValueError):
9
+ """Raised when a role or duty is absent from the canonical catalog."""
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class RoleDefinition:
14
+ id: str
15
+ legacy_aliases: tuple[str, ...]
16
+ required_capabilities: frozenset[str]
17
+
18
+
19
+ _READONLY_CAPABILITIES = frozenset({"worker-artifact-io", "source-readonly"})
20
+
21
+ ROLE_DEFINITIONS = (
22
+ RoleDefinition("leader", ("lead",), frozenset({"lead-session"})),
23
+ RoleDefinition("analyser", (), _READONLY_CAPABILITIES),
24
+ RoleDefinition("critic", (), _READONLY_CAPABILITIES),
25
+ RoleDefinition("designer", (), _READONLY_CAPABILITIES),
26
+ RoleDefinition("planner", (), _READONLY_CAPABILITIES),
27
+ RoleDefinition(
28
+ "implementer",
29
+ ("executor",),
30
+ frozenset({"worker-artifact-io", "project-mutation"}),
31
+ ),
32
+ RoleDefinition("verifier", (), _READONLY_CAPABILITIES),
33
+ RoleDefinition(
34
+ "report-writer",
35
+ (),
36
+ _READONLY_CAPABILITIES | frozenset({"extended-artifact-authoring"}),
37
+ ),
38
+ RoleDefinition(
39
+ "translator",
40
+ (),
41
+ _READONLY_CAPABILITIES | frozenset({"extended-artifact-authoring"}),
42
+ ),
43
+ )
44
+
45
+ DUTY_ROLE_IDS = {
46
+ "lead": "leader",
47
+ "analysis-worker": "analyser",
48
+ "discovery-worker": "analyser",
49
+ "diagnosis-worker": "analyser",
50
+ "scope-critic": "critic",
51
+ "acceptance-critic": "critic",
52
+ "direction-selection-worker": "designer",
53
+ "planning-worker": "planner",
54
+ "implementation-executor": "implementer",
55
+ "implementation-verifier": "verifier",
56
+ "acceptance-verifier": "verifier",
57
+ "reverification-worker": "verifier",
58
+ "code-reviewer": "verifier",
59
+ "schedule-verifier": "verifier",
60
+ "report-writer": "report-writer",
61
+ "translator": "translator",
62
+ }
63
+
64
+
65
+ class RoleCatalog:
66
+ def __init__(
67
+ self,
68
+ definitions: Sequence[RoleDefinition],
69
+ duty_role_ids: Mapping[str, str],
70
+ ) -> None:
71
+ definition_list = tuple(definitions)
72
+ self._definition_count = len(definition_list)
73
+ self._definitions = {definition.id: definition for definition in definition_list}
74
+ self._aliases = {
75
+ alias: definition.id
76
+ for definition in definition_list
77
+ for alias in definition.legacy_aliases
78
+ }
79
+ self._duty_role_ids = dict(duty_role_ids)
80
+ self._validate()
81
+
82
+ def normalize(self, raw: str) -> str:
83
+ role = raw.strip()
84
+ canonical = self._aliases.get(role, role)
85
+ if canonical not in self._definitions:
86
+ raise RoleCatalogError(f"unknown role: {raw}")
87
+ return canonical
88
+
89
+ def required_capabilities(self, role: str) -> frozenset[str]:
90
+ return self._definitions[self.normalize(role)].required_capabilities
91
+
92
+ def role_for_duty(self, duty_id: str) -> str:
93
+ try:
94
+ return self._duty_role_ids[duty_id]
95
+ except KeyError as exc:
96
+ raise RoleCatalogError(f"unknown duty: {duty_id}") from exc
97
+
98
+ def _validate(self) -> None:
99
+ if len(self._definitions) != self._definition_count:
100
+ raise RoleCatalogError("duplicate canonical role")
101
+ if len(self._aliases) != sum(
102
+ len(definition.legacy_aliases) for definition in self._definitions.values()
103
+ ):
104
+ raise RoleCatalogError("duplicate legacy role alias")
105
+ for role in self._duty_role_ids.values():
106
+ self.normalize(role)
107
+
108
+
109
+ ROLE_CATALOG = RoleCatalog(ROLE_DEFINITIONS, DUTY_ROLE_IDS)
110
+
111
+ # Ids plus legacy aliases. Bundled adapters declare this set so every
112
+ # catalog model can be assigned to every role.
113
+ ALL_ROLE_TOKENS = frozenset(
114
+ token
115
+ for definition in ROLE_DEFINITIONS
116
+ for token in (definition.id, *definition.legacy_aliases)
117
+ )
118
+
119
+
120
+ def normalize_role(raw: str) -> str:
121
+ return ROLE_CATALOG.normalize(raw)
122
+
123
+
124
+ def role_for_duty(duty_id: str) -> str:
125
+ return ROLE_CATALOG.role_for_duty(duty_id)
@@ -24,6 +24,11 @@ class WizardPrompt:
24
24
  multi: bool = False
25
25
  questions: tuple["WizardPrompt", ...] = ()
26
26
 
27
+ @property
28
+ def id(self) -> str:
29
+ """Stable public identifier shared by static and role-instance prompts."""
30
+ return self.step
31
+
27
32
 
28
33
  @dataclass(frozen=True)
29
34
  class AnswerProtocol:
@@ -66,6 +71,19 @@ def _normalize_numbered_item(prompt: WizardPrompt, answer: str) -> str:
66
71
  number = int(candidate)
67
72
  if 1 <= number <= len(prompt.options):
68
73
  return prompt.options[number - 1].value
74
+ # 옵션 값 자체가 숫자인 role-count 는 목록 밖 번호를 값으로도 허용한다
75
+ # (예: 옵션 [3,4,5] 에서 "4" → 수량 4). 목록에 없으면 범위 오류.
76
+ if prompt.step.startswith("role-count:"):
77
+ exact_count = _exact_value_match(prompt, candidate)
78
+ if exact_count is not None:
79
+ return exact_count
80
+ role = prompt.step.split(":", 1)[1]
81
+ minimum = prompt.options[0].value if prompt.options else "?"
82
+ maximum = prompt.options[-1].value if prompt.options else "?"
83
+ raise WizardAnswerError(
84
+ f"role {role!r} count must be in {minimum}..{maximum}: "
85
+ f"{candidate}"
86
+ )
69
87
  out_of_list = _exact_value_match(prompt, candidate)
70
88
  if out_of_list is not None:
71
89
  return out_of_list
@@ -9,15 +9,26 @@ from __future__ import annotations
9
9
 
10
10
  from dataclasses import dataclass, field
11
11
  from pathlib import Path
12
- from typing import Protocol, runtime_checkable
12
+ from collections.abc import Callable, Mapping
13
+ from typing import Any, Literal, Protocol, runtime_checkable
13
14
 
14
15
  from .worker_stream import Normalise, no_events
16
+ from ..write_policy import WriteEnforcement, WritePolicy
15
17
 
16
18
  # What ``ExecCommand.stream_format`` may hold. The value decides whether the
17
19
  # runner pipes the CLI's output through the stream formatter or forwards it
18
20
  # unchanged, so it is part of the contract rather than a display hint.
19
21
  STREAM_JSON = "stream-json"
20
22
  TEXT = "text"
23
+ SERVED_MODEL_MISMATCH_EXIT_CODE = 78
24
+
25
+
26
+ def no_model_observation(_event: Mapping[str, Any]) -> str | None:
27
+ """Truthful default for provider output that exposes no model identity."""
28
+ return None
29
+
30
+
31
+ ObserveServedModel = Callable[[Mapping[str, Any]], str | None]
21
32
 
22
33
 
23
34
  @dataclass(frozen=True)
@@ -33,6 +44,8 @@ class ExecutionPolicy:
33
44
 
34
45
  auto_approve: bool
35
46
  write_scope: tuple[Path, ...]
47
+ write_policy: WritePolicy | None = None
48
+ write_enforcement: WriteEnforcement | None = None
36
49
 
37
50
 
38
51
  @dataclass(frozen=True)
@@ -84,6 +97,20 @@ class ExecCommand:
84
97
  stream_format: str
85
98
  cwd: Path
86
99
  normalise: Normalise = field(default=no_events)
100
+ observe_served_model: ObserveServedModel = field(
101
+ default=no_model_observation
102
+ )
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class WorkerWriteCapability:
107
+ """Maximum write-boundary precision exposed by a worker runner."""
108
+
109
+ max_boundary_precision: Literal[
110
+ "exact-path",
111
+ "directory-boundary",
112
+ "none",
113
+ ]
87
114
 
88
115
 
89
116
  @dataclass(frozen=True)
@@ -99,6 +126,12 @@ class PolicySupport:
99
126
  can_bound_write_scope: bool
100
127
  note: str = ""
101
128
 
129
+ def worker_write_capability(self) -> WorkerWriteCapability:
130
+ precision = (
131
+ "directory-boundary" if self.can_bound_write_scope else "none"
132
+ )
133
+ return WorkerWriteCapability(precision)
134
+
102
135
 
103
136
  @runtime_checkable
104
137
  class ExecutionStrategy(Protocol):
@@ -9,6 +9,8 @@ from __future__ import annotations
9
9
 
10
10
  from dataclasses import dataclass
11
11
 
12
+ from .role import RoleCatalogError, normalize_role
13
+
12
14
  # Implementation executor / verifier dispatches run whole build + test suites
13
15
  # that legitimately emit nothing for minutes; the 600s cap reaped them mid-suite
14
16
  # (observed: a silent jest + tsc build TERM'd at ~929s). This stays below the
@@ -16,7 +18,7 @@ from dataclasses import dataclass
16
18
  _BUILD_RUNNING_IDLE_SECONDS = 1500
17
19
  _DEFAULT_IDLE_SECONDS = 600
18
20
 
19
- _BUILD_RUNNING_ROLES = frozenset({"executor", "verifier"})
21
+ _BUILD_RUNNING_ROLES = frozenset({"implementer", "verifier"})
20
22
 
21
23
 
22
24
  @dataclass(frozen=True)
@@ -26,9 +28,17 @@ class WorkerRoleSpec:
26
28
 
27
29
 
28
30
  def role_spec(role: str) -> WorkerRoleSpec:
31
+ canonical_role = _canonical_role_or_raw(role)
29
32
  seconds = (
30
33
  _BUILD_RUNNING_IDLE_SECONDS
31
- if role in _BUILD_RUNNING_ROLES
34
+ if canonical_role in _BUILD_RUNNING_ROLES
32
35
  else _DEFAULT_IDLE_SECONDS
33
36
  )
34
- return WorkerRoleSpec(role=role, idle_timeout_seconds=seconds)
37
+ return WorkerRoleSpec(role=canonical_role, idle_timeout_seconds=seconds)
38
+
39
+
40
+ def _canonical_role_or_raw(role: str) -> str:
41
+ try:
42
+ return normalize_role(role)
43
+ except RoleCatalogError:
44
+ return role
@@ -0,0 +1,44 @@
1
+ """Execution-surface value objects.
2
+
3
+ The surface is where a worker process starts and where its output lands.
4
+ It is not a host, a provider, or a role.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+
11
+
12
+ SURFACE_CMUX_PANE = "cmux-pane"
13
+ SURFACE_CLI_WRAPPER = "cli-wrapper"
14
+
15
+
16
+ class SurfaceUnavailable(Exception):
17
+ """The surface could not be opened. The application may try the next one."""
18
+
19
+
20
+ class EnvironmentBlocked(Exception):
21
+ """A sandbox hid the surface and the worker CLI config. Do not degrade."""
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class WorkerSpawnRequest:
26
+ command: tuple[str, ...]
27
+ cwd: Path
28
+ title: str
29
+ owned_surface_ids: tuple[str, ...] = ()
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class RuntimeHandle:
34
+ surface: str
35
+ identifier: str
36
+ waitable: bool
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class ProgressEvent:
41
+ message: str
42
+ level: str = "info"
43
+ notify_title: str | None = None
44
+ notify_body: str | None = None
@@ -88,6 +88,30 @@ StreamEvent = Text | ToolCall | ToolResult | Denial | Result
88
88
  Normalise = Callable[[Mapping[str, Any]], tuple[StreamEvent, ...]]
89
89
 
90
90
 
91
+ def host_event_row(
92
+ *,
93
+ event_id: str,
94
+ sequence: int,
95
+ event_type: str,
96
+ invocation_ref: str,
97
+ attempt: int,
98
+ payload: Mapping[str, Any],
99
+ timestamp: str,
100
+ ) -> dict[str, Any]:
101
+ """Build one orchestrator-owned host-event row without I/O."""
102
+ row = {
103
+ "schemaVersion": "1.0",
104
+ "eventId": event_id,
105
+ "sequence": sequence,
106
+ "timestamp": timestamp,
107
+ "eventType": event_type,
108
+ "invocationRef": invocation_ref,
109
+ "attempt": attempt,
110
+ }
111
+ row.update(dict(payload))
112
+ return row
113
+
114
+
91
115
  def no_events(event: Mapping[str, Any]) -> tuple[StreamEvent, ...]:
92
116
  """The normaliser for a CLI that emits no events at all.
93
117