okstra 0.173.0 → 0.175.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (402) hide show
  1. package/README.md +18 -9
  2. package/bin/okstra +8 -4
  3. package/dist/cli-registry.d.mts +16 -0
  4. package/dist/cli-registry.mjs +560 -0
  5. package/dist/cli-registry.mjs.map +1 -0
  6. package/dist/commands/execute/agent-prompt.d.mts +1 -0
  7. package/{src → dist}/commands/execute/agent-prompt.mjs +10 -11
  8. package/dist/commands/execute/agent-prompt.mjs.map +1 -0
  9. package/dist/commands/execute/codex-dispatch.d.mts +3 -0
  10. package/dist/commands/execute/codex-dispatch.mjs +6 -0
  11. package/dist/commands/execute/codex-dispatch.mjs.map +1 -0
  12. package/dist/commands/execute/codex-run.d.mts +3 -0
  13. package/{src → dist}/commands/execute/codex-run.mjs +30 -37
  14. package/dist/commands/execute/codex-run.mjs.map +1 -0
  15. package/dist/commands/execute/convergence.d.mts +1 -0
  16. package/{src → dist}/commands/execute/convergence.mjs +15 -16
  17. package/dist/commands/execute/convergence.mjs.map +1 -0
  18. package/dist/commands/execute/error-log.d.mts +1 -0
  19. package/{src → dist}/commands/execute/error-log.mjs +2 -3
  20. package/dist/commands/execute/error-log.mjs.map +1 -0
  21. package/dist/commands/execute/git-reconcile.d.mts +1 -0
  22. package/{src → dist}/commands/execute/git-reconcile.mjs +10 -11
  23. package/dist/commands/execute/git-reconcile.mjs.map +1 -0
  24. package/dist/commands/execute/handoff.d.mts +1 -0
  25. package/{src → dist}/commands/execute/handoff.mjs +10 -11
  26. package/dist/commands/execute/handoff.mjs.map +1 -0
  27. package/dist/commands/execute/incremental-carry.d.mts +1 -0
  28. package/{src → dist}/commands/execute/incremental-carry.mjs +2 -3
  29. package/dist/commands/execute/incremental-carry.mjs.map +1 -0
  30. package/dist/commands/execute/incremental-scope.d.mts +1 -0
  31. package/{src → dist}/commands/execute/incremental-scope.mjs +2 -3
  32. package/dist/commands/execute/incremental-scope.mjs.map +1 -0
  33. package/dist/commands/execute/integrate-stages.d.mts +1 -0
  34. package/{src → dist}/commands/execute/integrate-stages.mjs +10 -11
  35. package/dist/commands/execute/integrate-stages.mjs.map +1 -0
  36. package/dist/commands/execute/pane-title.d.mts +1 -0
  37. package/dist/commands/execute/pane-title.mjs +20 -0
  38. package/dist/commands/execute/pane-title.mjs.map +1 -0
  39. package/dist/commands/execute/plan-items.d.mts +1 -0
  40. package/dist/commands/execute/plan-items.mjs +9 -0
  41. package/dist/commands/execute/plan-items.mjs.map +1 -0
  42. package/dist/commands/execute/plan-validate.d.mts +1 -0
  43. package/dist/commands/execute/plan-validate.mjs +68 -0
  44. package/dist/commands/execute/plan-validate.mjs.map +1 -0
  45. package/dist/commands/execute/plan-verify.d.mts +1 -0
  46. package/{src → dist}/commands/execute/plan-verify.mjs +13 -16
  47. package/dist/commands/execute/plan-verify.mjs.map +1 -0
  48. package/dist/commands/execute/render-bundle.d.mts +3 -0
  49. package/{src → dist}/commands/execute/render-bundle.mjs +65 -61
  50. package/dist/commands/execute/render-bundle.mjs.map +1 -0
  51. package/dist/commands/execute/run.d.mts +24 -0
  52. package/dist/commands/execute/run.mjs +196 -0
  53. package/dist/commands/execute/run.mjs.map +1 -0
  54. package/dist/commands/execute/spawn-followups.d.mts +1 -0
  55. package/{src → dist}/commands/execute/spawn-followups.mjs +6 -7
  56. package/dist/commands/execute/spawn-followups.mjs.map +1 -0
  57. package/dist/commands/execute/team.d.mts +3 -0
  58. package/dist/commands/execute/team.mjs +56 -0
  59. package/dist/commands/execute/team.mjs.map +1 -0
  60. package/dist/commands/execute/token-usage.d.mts +1 -0
  61. package/{src → dist}/commands/execute/token-usage.mjs +2 -3
  62. package/dist/commands/execute/token-usage.mjs.map +1 -0
  63. package/dist/commands/execute/wizard.d.mts +8 -0
  64. package/dist/commands/execute/wizard.mjs +191 -0
  65. package/dist/commands/execute/wizard.mjs.map +1 -0
  66. package/dist/commands/execute/worker-audit-check.d.mts +1 -0
  67. package/{src → dist}/commands/execute/worker-audit-check.mjs +11 -12
  68. package/dist/commands/execute/worker-audit-check.mjs.map +1 -0
  69. package/dist/commands/execute/worker-dispatch.d.mts +7 -0
  70. package/{src → dist}/commands/execute/worker-dispatch.mjs +34 -46
  71. package/dist/commands/execute/worker-dispatch.mjs.map +1 -0
  72. package/dist/commands/execute/worker-state.d.mts +1 -0
  73. package/{src → dist}/commands/execute/worker-state.mjs +15 -16
  74. package/dist/commands/execute/worker-state.mjs.map +1 -0
  75. package/dist/commands/execute/worktree-lookup.d.mts +1 -0
  76. package/{src → dist}/commands/execute/worktree-lookup.mjs +38 -37
  77. package/dist/commands/execute/worktree-lookup.mjs.map +1 -0
  78. package/dist/commands/execute/worktree-status.d.mts +1 -0
  79. package/dist/commands/execute/worktree-status.mjs +115 -0
  80. package/dist/commands/execute/worktree-status.mjs.map +1 -0
  81. package/dist/commands/inspect/code-review.d.mts +1 -0
  82. package/{src → dist}/commands/inspect/code-review.mjs +16 -19
  83. package/dist/commands/inspect/code-review.mjs.map +1 -0
  84. package/dist/commands/inspect/container.d.mts +1 -0
  85. package/{src → dist}/commands/inspect/container.mjs +11 -13
  86. package/dist/commands/inspect/container.mjs.map +1 -0
  87. package/dist/commands/inspect/context-cost.d.mts +1 -0
  88. package/{src → dist}/commands/inspect/context-cost.mjs +11 -13
  89. package/dist/commands/inspect/context-cost.mjs.map +1 -0
  90. package/dist/commands/inspect/design-prep.d.mts +1 -0
  91. package/{src → dist}/commands/inspect/design-prep.mjs +10 -11
  92. package/dist/commands/inspect/design-prep.mjs.map +1 -0
  93. package/dist/commands/inspect/error-report.d.mts +1 -0
  94. package/{src → dist}/commands/inspect/error-report.mjs +11 -13
  95. package/dist/commands/inspect/error-report.mjs.map +1 -0
  96. package/dist/commands/inspect/error-zip.d.mts +1 -0
  97. package/{src → dist}/commands/inspect/error-zip.mjs +11 -12
  98. package/dist/commands/inspect/error-zip.mjs.map +1 -0
  99. package/dist/commands/inspect/log-report.d.mts +1 -0
  100. package/{src → dist}/commands/inspect/log-report.mjs +11 -13
  101. package/dist/commands/inspect/log-report.mjs.map +1 -0
  102. package/dist/commands/inspect/profile-show.d.mts +1 -0
  103. package/{src → dist}/commands/inspect/profile-show.mjs +10 -11
  104. package/dist/commands/inspect/profile-show.mjs.map +1 -0
  105. package/dist/commands/inspect/recap.d.mts +1 -0
  106. package/{src → dist}/commands/inspect/recap.mjs +11 -13
  107. package/dist/commands/inspect/recap.mjs.map +1 -0
  108. package/dist/commands/inspect/resolve-task-key.d.mts +1 -0
  109. package/{src → dist}/commands/inspect/resolve-task-key.mjs +11 -13
  110. package/dist/commands/inspect/resolve-task-key.mjs.map +1 -0
  111. package/dist/commands/inspect/rollup.d.mts +1 -0
  112. package/{src → dist}/commands/inspect/rollup.mjs +11 -13
  113. package/dist/commands/inspect/rollup.mjs.map +1 -0
  114. package/dist/commands/inspect/run-audit.d.mts +1 -0
  115. package/{src → dist}/commands/inspect/run-audit.mjs +11 -12
  116. package/dist/commands/inspect/run-audit.mjs.map +1 -0
  117. package/dist/commands/inspect/set-work-status.d.mts +1 -0
  118. package/{src → dist}/commands/inspect/set-work-status.mjs +11 -13
  119. package/dist/commands/inspect/set-work-status.mjs.map +1 -0
  120. package/dist/commands/inspect/stage-map.d.mts +1 -0
  121. package/dist/commands/inspect/stage-map.mjs +110 -0
  122. package/dist/commands/inspect/stage-map.mjs.map +1 -0
  123. package/dist/commands/inspect/task-list.d.mts +1 -0
  124. package/dist/commands/inspect/task-list.mjs +103 -0
  125. package/dist/commands/inspect/task-list.mjs.map +1 -0
  126. package/dist/commands/inspect/task-show.d.mts +1 -0
  127. package/dist/commands/inspect/task-show.mjs +108 -0
  128. package/dist/commands/inspect/task-show.mjs.map +1 -0
  129. package/dist/commands/inspect/time-report.d.mts +1 -0
  130. package/{src → dist}/commands/inspect/time-report.mjs +11 -13
  131. package/dist/commands/inspect/time-report.mjs.map +1 -0
  132. package/dist/commands/inspect/usage-report.d.mts +1 -0
  133. package/{src → dist}/commands/inspect/usage-report.mjs +11 -13
  134. package/dist/commands/inspect/usage-report.mjs.map +1 -0
  135. package/dist/commands/inspect/user-response.d.mts +1 -0
  136. package/{src → dist}/commands/inspect/user-response.mjs +10 -11
  137. package/dist/commands/inspect/user-response.mjs.map +1 -0
  138. package/dist/commands/inspect/worker-liveness.d.mts +1 -0
  139. package/{src → dist}/commands/inspect/worker-liveness.mjs +11 -13
  140. package/dist/commands/inspect/worker-liveness.mjs.map +1 -0
  141. package/dist/commands/lifecycle/check-project.d.mts +21 -0
  142. package/dist/commands/lifecycle/check-project.mjs +175 -0
  143. package/dist/commands/lifecycle/check-project.mjs.map +1 -0
  144. package/dist/commands/lifecycle/config.d.mts +1 -0
  145. package/dist/commands/lifecycle/config.mjs +369 -0
  146. package/dist/commands/lifecycle/config.mjs.map +1 -0
  147. package/dist/commands/lifecycle/doctor.d.mts +10 -0
  148. package/dist/commands/lifecycle/doctor.mjs +317 -0
  149. package/dist/commands/lifecycle/doctor.mjs.map +1 -0
  150. package/dist/commands/lifecycle/install.d.mts +17 -0
  151. package/dist/commands/lifecycle/install.mjs +1077 -0
  152. package/dist/commands/lifecycle/install.mjs.map +1 -0
  153. package/dist/commands/lifecycle/migrate.d.mts +1 -0
  154. package/{src → dist}/commands/lifecycle/migrate.mjs +10 -11
  155. package/dist/commands/lifecycle/migrate.mjs.map +1 -0
  156. package/dist/commands/lifecycle/model.d.mts +1 -0
  157. package/dist/commands/lifecycle/model.mjs +22 -0
  158. package/dist/commands/lifecycle/model.mjs.map +1 -0
  159. package/dist/commands/lifecycle/paths.d.mts +1 -0
  160. package/dist/commands/lifecycle/paths.mjs +102 -0
  161. package/dist/commands/lifecycle/paths.mjs.map +1 -0
  162. package/dist/commands/lifecycle/preflight.d.mts +1 -0
  163. package/dist/commands/lifecycle/preflight.mjs +124 -0
  164. package/dist/commands/lifecycle/preflight.mjs.map +1 -0
  165. package/dist/commands/lifecycle/setup.d.mts +1 -0
  166. package/dist/commands/lifecycle/setup.mjs +297 -0
  167. package/dist/commands/lifecycle/setup.mjs.map +1 -0
  168. package/dist/commands/lifecycle/uninstall.d.mts +4 -0
  169. package/dist/commands/lifecycle/uninstall.mjs +262 -0
  170. package/dist/commands/lifecycle/uninstall.mjs.map +1 -0
  171. package/dist/commands/manager.d.mts +3 -0
  172. package/{src → dist}/commands/manager.mjs +25 -32
  173. package/dist/commands/manager.mjs.map +1 -0
  174. package/dist/commands/memory/memory.d.mts +1 -0
  175. package/dist/commands/memory/memory.mjs +569 -0
  176. package/dist/commands/memory/memory.mjs.map +1 -0
  177. package/dist/commands/pr/pr.d.mts +33 -0
  178. package/dist/commands/pr/pr.mjs +296 -0
  179. package/dist/commands/pr/pr.mjs.map +1 -0
  180. package/dist/commands/report/agent-activity.d.mts +1 -0
  181. package/{src → dist}/commands/report/agent-activity.mjs +11 -12
  182. package/dist/commands/report/agent-activity.mjs.map +1 -0
  183. package/dist/commands/report/finalize.d.mts +4 -0
  184. package/dist/commands/report/finalize.mjs +64 -0
  185. package/dist/commands/report/finalize.mjs.map +1 -0
  186. package/dist/commands/report/inject-report-index.d.mts +1 -0
  187. package/{src → dist}/commands/report/inject-report-index.mjs +6 -7
  188. package/dist/commands/report/inject-report-index.mjs.map +1 -0
  189. package/dist/commands/report/render-final-report.d.mts +1 -0
  190. package/{src → dist}/commands/report/render-final-report.mjs +6 -7
  191. package/dist/commands/report/render-final-report.mjs.map +1 -0
  192. package/dist/commands/report/render-views.d.mts +1 -0
  193. package/{src → dist}/commands/report/render-views.mjs +6 -7
  194. package/dist/commands/report/render-views.mjs.map +1 -0
  195. package/dist/commands/report/translate.d.mts +1 -0
  196. package/{src → dist}/commands/report/translate.mjs +6 -7
  197. package/dist/commands/report/translate.mjs.map +1 -0
  198. package/dist/lib/helper-scripts.d.mts +3 -0
  199. package/{src → dist}/lib/helper-scripts.mjs +11 -14
  200. package/dist/lib/helper-scripts.mjs.map +1 -0
  201. package/dist/lib/host-registry-client.d.mts +25 -0
  202. package/dist/lib/host-registry-client.mjs +152 -0
  203. package/dist/lib/host-registry-client.mjs.map +1 -0
  204. package/dist/lib/install-assets.d.mts +8 -0
  205. package/{src → dist}/lib/install-assets.mjs +17 -20
  206. package/dist/lib/install-assets.mjs.map +1 -0
  207. package/dist/lib/okstra-dirs.d.mts +4 -0
  208. package/{src → dist}/lib/okstra-dirs.mjs +3 -6
  209. package/dist/lib/okstra-dirs.mjs.map +1 -0
  210. package/dist/lib/paths.d.mts +4 -0
  211. package/dist/lib/paths.mjs +78 -0
  212. package/dist/lib/paths.mjs.map +1 -0
  213. package/dist/lib/proc.d.mts +3 -0
  214. package/dist/lib/proc.mjs +31 -0
  215. package/dist/lib/proc.mjs.map +1 -0
  216. package/dist/lib/python-helper.d.mts +8 -0
  217. package/dist/lib/python-helper.mjs +123 -0
  218. package/dist/lib/python-helper.mjs.map +1 -0
  219. package/dist/lib/runtime-manifest.d.mts +7 -0
  220. package/dist/lib/runtime-manifest.mjs +73 -0
  221. package/dist/lib/runtime-manifest.mjs.map +1 -0
  222. package/dist/lib/skill-catalog.d.mts +4 -0
  223. package/dist/lib/skill-catalog.mjs +50 -0
  224. package/dist/lib/skill-catalog.mjs.map +1 -0
  225. package/dist/lib/types.d.mts +129 -0
  226. package/dist/lib/types.mjs +2 -0
  227. package/dist/lib/types.mjs.map +1 -0
  228. package/dist/lib/version.d.mts +2 -0
  229. package/dist/lib/version.mjs +27 -0
  230. package/dist/lib/version.mjs.map +1 -0
  231. package/docs/architecture/storage-model.md +17 -3
  232. package/docs/architecture.md +12 -22
  233. package/docs/cli.md +29 -8
  234. package/docs/container.md +1 -1
  235. package/docs/contributor-change-matrix.md +3 -3
  236. package/docs/project-structure-overview.md +29 -20
  237. package/docs/task-process/README.md +3 -1
  238. package/docs/task-process/implementation-planning.md +2 -2
  239. package/docs/task-process/implementation.md +1 -1
  240. package/package.json +11 -5
  241. package/runtime/BUILD.json +2 -2
  242. package/runtime/agents/workers/claude-worker.md +3 -2
  243. package/runtime/agents/workers/report-writer-worker.md +3 -1
  244. package/runtime/agents/workers/translator-worker.md +3 -1
  245. package/runtime/bin/lib/okstra/cli.sh +12 -0
  246. package/runtime/bin/lib/okstra/globals.sh +4 -1
  247. package/runtime/bin/lib/okstra/usage.sh +4 -4
  248. package/runtime/bin/okstra-error-log.py +27 -0
  249. package/runtime/bin/okstra-provider-exec.py +245 -16
  250. package/runtime/bin/okstra-trace-cleanup.sh +58 -129
  251. package/runtime/bin/okstra.sh +10 -0
  252. package/runtime/prompts/lead/adapters/cmux.md +2 -0
  253. package/runtime/prompts/lead/convergence.md +27 -18
  254. package/runtime/prompts/lead/okstra-lead-contract.md +4 -2
  255. package/runtime/prompts/lead/plan-body-verification.md +13 -6
  256. package/runtime/prompts/lead/report-writer.md +6 -6
  257. package/runtime/prompts/lead/team-contract.md +1 -1
  258. package/runtime/prompts/profiles/_common-contract.md +2 -2
  259. package/runtime/prompts/profiles/_implementation-executor.md +2 -0
  260. package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
  261. package/runtime/prompts/profiles/change-impact-analysis.md +16 -0
  262. package/runtime/prompts/profiles/error-analysis.md +21 -1
  263. package/runtime/prompts/profiles/feature-analysis.md +16 -0
  264. package/runtime/prompts/profiles/final-verification.md +16 -0
  265. package/runtime/prompts/profiles/implementation-option-selection.md +16 -0
  266. package/runtime/prompts/profiles/implementation-planning.md +32 -9
  267. package/runtime/prompts/profiles/implementation.md +17 -1
  268. package/runtime/prompts/profiles/improvement-discovery.md +16 -0
  269. package/runtime/prompts/profiles/project-analysis.md +16 -0
  270. package/runtime/prompts/profiles/release-handoff.md +5 -1
  271. package/runtime/prompts/profiles/requirements-discovery.md +20 -0
  272. package/runtime/prompts/wizard/prompts.ko.json +18 -0
  273. package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +1 -6
  274. package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +8 -0
  275. package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +8 -0
  276. package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
  277. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +2 -2
  278. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +25 -5
  279. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
  280. package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +4 -1
  281. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +4 -4
  282. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
  283. package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +2 -2
  284. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +79 -11
  285. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +100 -25
  286. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +59 -14
  287. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +55 -9
  288. package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +51 -12
  289. package/runtime/python/okstra_ctl/agent_activity.py +5 -1
  290. package/runtime/python/okstra_ctl/agent_invocation.py +608 -14
  291. package/runtime/python/okstra_ctl/agent_prompt_cli.py +249 -50
  292. package/runtime/python/okstra_ctl/application/resolve_assignment.py +53 -12
  293. package/runtime/python/okstra_ctl/assignment_environment.py +102 -0
  294. package/runtime/python/okstra_ctl/assignment_resolver.py +790 -0
  295. package/runtime/python/okstra_ctl/attempt_evidence.py +342 -0
  296. package/runtime/python/okstra_ctl/conformance.py +68 -0
  297. package/runtime/python/okstra_ctl/convergence.py +409 -3
  298. package/runtime/python/okstra_ctl/convergence_engine.py +248 -15
  299. package/runtime/python/okstra_ctl/convergence_store.py +170 -1
  300. package/runtime/python/okstra_ctl/dispatch_core.py +1181 -102
  301. package/runtime/python/okstra_ctl/dispatch_state.py +835 -22
  302. package/runtime/python/okstra_ctl/doctor.py +16 -2
  303. package/runtime/python/okstra_ctl/domain/host.py +32 -1
  304. package/runtime/python/okstra_ctl/domain/provider.py +212 -6
  305. package/runtime/python/okstra_ctl/domain/role.py +125 -0
  306. package/runtime/python/okstra_ctl/domain/wizard/interaction.py +13 -0
  307. package/runtime/python/okstra_ctl/domain/worker_exec.py +39 -1
  308. package/runtime/python/okstra_ctl/domain/worker_role.py +13 -3
  309. package/runtime/python/okstra_ctl/domain/worker_stream.py +24 -0
  310. package/runtime/python/okstra_ctl/error_log_write.py +9 -0
  311. package/runtime/python/okstra_ctl/error_report.py +21 -1
  312. package/runtime/python/okstra_ctl/execution_identity.py +529 -0
  313. package/runtime/python/okstra_ctl/execution_manifest.py +1281 -0
  314. package/runtime/python/okstra_ctl/execution_mutation_audit.py +619 -0
  315. package/runtime/python/okstra_ctl/final_report_schema.py +5 -4
  316. package/runtime/python/okstra_ctl/index.py +6 -2
  317. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +43 -3
  318. package/runtime/python/okstra_ctl/legacy_model_selection.py +490 -0
  319. package/runtime/python/okstra_ctl/model_cli.py +278 -0
  320. package/runtime/python/okstra_ctl/model_defaults.py +46 -0
  321. package/runtime/python/okstra_ctl/model_discovery.py +26 -7
  322. package/runtime/python/okstra_ctl/model_pool.py +501 -0
  323. package/runtime/python/okstra_ctl/models.py +47 -27
  324. package/runtime/python/okstra_ctl/mutation_recovery.py +293 -0
  325. package/runtime/python/okstra_ctl/pane_reclaim.py +13 -22
  326. package/runtime/python/okstra_ctl/pane_title.py +151 -0
  327. package/runtime/python/okstra_ctl/ports/host_model.py +48 -17
  328. package/runtime/python/okstra_ctl/ports/worker_dispatch.py +4 -0
  329. package/runtime/python/okstra_ctl/registry/provider_registry.py +8 -4
  330. package/runtime/python/okstra_ctl/render.py +78 -10
  331. package/runtime/python/okstra_ctl/render_final_report.py +34 -19
  332. package/runtime/python/okstra_ctl/report_contract.py +39 -1
  333. package/runtime/python/okstra_ctl/report_finalize.py +84 -9
  334. package/runtime/python/okstra_ctl/report_html/render.py +1 -0
  335. package/runtime/python/okstra_ctl/role_requirements.py +217 -0
  336. package/runtime/python/okstra_ctl/run.py +1250 -260
  337. package/runtime/python/okstra_ctl/run_index_row.py +7 -1
  338. package/runtime/python/okstra_ctl/schema_excerpt.py +1 -1
  339. package/runtime/python/okstra_ctl/scope_provenance.py +1 -1
  340. package/runtime/python/okstra_ctl/session.py +69 -12
  341. package/runtime/python/okstra_ctl/team.py +51 -25
  342. package/runtime/python/okstra_ctl/time_report.py +38 -4
  343. package/runtime/python/okstra_ctl/tmux.py +19 -149
  344. package/runtime/python/okstra_ctl/usage_identity.py +86 -0
  345. package/runtime/python/okstra_ctl/usage_report.py +15 -3
  346. package/runtime/python/okstra_ctl/wizard.py +1253 -62
  347. package/runtime/python/okstra_ctl/worker_audit_ledger.py +68 -65
  348. package/runtime/python/okstra_ctl/worker_request.py +9 -1
  349. package/runtime/python/okstra_ctl/worker_runner.py +130 -5
  350. package/runtime/python/okstra_ctl/workers.py +7 -5
  351. package/runtime/python/okstra_ctl/worktree.py +69 -3
  352. package/runtime/python/okstra_ctl/write_policy.py +571 -0
  353. package/runtime/python/okstra_token_usage/cli.py +1 -1
  354. package/runtime/python/okstra_token_usage/collect.py +66 -6
  355. package/runtime/schemas/convergence-groups-v2.0.schema.json +128 -0
  356. package/runtime/schemas/execution-manifest-v2.schema.json +340 -0
  357. package/runtime/schemas/final-report-v2.0.schema.json +25 -0
  358. package/runtime/skills/okstra-run/SKILL.md +9 -9
  359. package/runtime/skills/okstra-setup/references/project-config.md +11 -0
  360. package/runtime/templates/report-writer-prompt-preamble.md +2 -0
  361. package/runtime/templates/reports/final-report-v2.template.md +7 -1
  362. package/runtime/templates/reports/html/base.template.html +21 -0
  363. package/runtime/templates/reports/html/i18n/en.json +4 -0
  364. package/runtime/templates/reports/html/i18n/ko.json +4 -0
  365. package/runtime/templates/reports/settings.template.json +0 -24
  366. package/runtime/templates/worker-prompt-preamble.md +6 -2
  367. package/runtime/validators/lib/fixtures.sh +98 -33
  368. package/runtime/validators/validate-implementation-plan-stages.py +63 -3
  369. package/runtime/validators/validate-run.py +443 -508
  370. package/runtime/validators/validate_session_conformance.py +26 -8
  371. package/runtime/bin/okstra-subagent-reclaim.sh +0 -26
  372. package/runtime/schemas/final-report-v1.0.schema.json +0 -6366
  373. package/runtime/templates/reports/final-report.template.md +0 -1258
  374. package/src/cli-registry.mjs +0 -532
  375. package/src/commands/execute/codex-dispatch.mjs +0 -10
  376. package/src/commands/execute/plan-items.mjs +0 -9
  377. package/src/commands/execute/plan-validate.mjs +0 -68
  378. package/src/commands/execute/run.mjs +0 -208
  379. package/src/commands/execute/team.mjs +0 -63
  380. package/src/commands/execute/wizard.mjs +0 -167
  381. package/src/commands/inspect/stage-map.mjs +0 -98
  382. package/src/commands/inspect/task-list.mjs +0 -97
  383. package/src/commands/inspect/task-show.mjs +0 -100
  384. package/src/commands/lifecycle/check-project.mjs +0 -187
  385. package/src/commands/lifecycle/config.mjs +0 -385
  386. package/src/commands/lifecycle/doctor.mjs +0 -308
  387. package/src/commands/lifecycle/install.mjs +0 -1172
  388. package/src/commands/lifecycle/paths.mjs +0 -104
  389. package/src/commands/lifecycle/preflight.mjs +0 -125
  390. package/src/commands/lifecycle/setup.mjs +0 -333
  391. package/src/commands/lifecycle/uninstall.mjs +0 -279
  392. package/src/commands/memory/memory.mjs +0 -524
  393. package/src/commands/pr/pr.mjs +0 -261
  394. package/src/commands/report/finalize.mjs +0 -64
  395. package/src/lib/host-registry-client.mjs +0 -176
  396. package/src/lib/paths.mjs +0 -86
  397. package/src/lib/proc.mjs +0 -31
  398. package/src/lib/python-helper.mjs +0 -142
  399. package/src/lib/runtime-manifest.mjs +0 -70
  400. package/src/lib/skill-catalog.mjs +0 -53
  401. package/src/lib/version.mjs +0 -20
  402. /package/{src → dist}/commands/pr/default.md +0 -0
@@ -0,0 +1,293 @@
1
+ """Mutation recovery decisions after sealed or unsealed implementer attempts."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Any, Literal, Mapping, Sequence
7
+
8
+ from .attempt_evidence import AttemptEvidenceSeal, _sha
9
+ from .dispatch_state import build_dispatch_id
10
+ from .execution_identity import ExecutionManifestError
11
+ from .execution_manifest import (
12
+ _replace_manifest,
13
+ _write_manifest_over_existing,
14
+ read_execution_manifest,
15
+ )
16
+ from .run_context import task_mutex
17
+
18
+
19
+ class MutationRecoveryError(ValueError):
20
+ """Raised when a recovery decision cannot be recorded or applied."""
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class MutationRecoveryInput:
25
+ manifest_path: Path
26
+ source_invocation_ref: str
27
+ source_attempt_ref: str
28
+ source_evidence_seal_ref: str | None
29
+ original_baseline_digest: str
30
+ terminal_source_diff_digest: str
31
+ terminal_git_chain_digest: str
32
+ verifier_role_execution_ref: str | None
33
+ verifier_invocation_ref: str | None
34
+ verifier_participant_ref: str | None
35
+ implementer_participant_ref: str
36
+ recovered_evidence_refs: tuple[Mapping[str, Any], ...]
37
+ recovered_out_of_plan_edits: tuple[Mapping[str, Any], ...]
38
+ current_source_diff_digest: str
39
+ current_git_chain_digest: str
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class MutationRecoveryDecision:
44
+ recovery_ref: str
45
+ source_invocation_ref: str
46
+ source_attempt_ref: str
47
+ source_evidence_seal_ref: str | None
48
+ original_baseline_digest: str
49
+ terminal_source_diff_digest: str
50
+ terminal_git_chain_digest: str
51
+ verifier_role_execution_ref: str | None
52
+ verifier_invocation_ref: str | None
53
+ recovered_evidence_refs: tuple[Mapping[str, Any], ...]
54
+ recovered_out_of_plan_edits: tuple[Mapping[str, Any], ...]
55
+ unmet_obligations: tuple[str, ...]
56
+ decision: Literal["carry-forward", "reject", "cleanup-confirmed"]
57
+ reason: str = ""
58
+
59
+ def to_payload(self) -> dict[str, Any]:
60
+ return {
61
+ "recoveryRef": self.recovery_ref,
62
+ "sourceInvocationRef": self.source_invocation_ref,
63
+ "sourceAttemptRef": self.source_attempt_ref,
64
+ "sourceEvidenceSealRef": self.source_evidence_seal_ref,
65
+ "originalBaselineDigest": self.original_baseline_digest,
66
+ "terminalSourceDiffDigest": self.terminal_source_diff_digest,
67
+ "terminalGitChainDigest": self.terminal_git_chain_digest,
68
+ "verifierRoleExecutionRef": self.verifier_role_execution_ref,
69
+ "verifierInvocationRef": self.verifier_invocation_ref,
70
+ "recoveredEvidenceRefs": [dict(row) for row in self.recovered_evidence_refs],
71
+ "recoveredOutOfPlanEdits": [
72
+ dict(row) for row in self.recovered_out_of_plan_edits
73
+ ],
74
+ "unmetObligations": list(self.unmet_obligations),
75
+ "decision": self.decision,
76
+ "reason": self.reason,
77
+ }
78
+
79
+ @classmethod
80
+ def from_payload(cls, payload: Mapping[str, Any]) -> "MutationRecoveryDecision":
81
+ return cls(
82
+ recovery_ref=str(payload["recoveryRef"]),
83
+ source_invocation_ref=str(payload["sourceInvocationRef"]),
84
+ source_attempt_ref=str(payload["sourceAttemptRef"]),
85
+ source_evidence_seal_ref=payload.get("sourceEvidenceSealRef"),
86
+ original_baseline_digest=str(payload.get("originalBaselineDigest") or ""),
87
+ terminal_source_diff_digest=str(
88
+ payload.get("terminalSourceDiffDigest") or ""
89
+ ),
90
+ terminal_git_chain_digest=str(payload.get("terminalGitChainDigest") or ""),
91
+ verifier_role_execution_ref=payload.get("verifierRoleExecutionRef"),
92
+ verifier_invocation_ref=payload.get("verifierInvocationRef"),
93
+ recovered_evidence_refs=tuple(payload.get("recoveredEvidenceRefs") or ()),
94
+ recovered_out_of_plan_edits=tuple(
95
+ payload.get("recoveredOutOfPlanEdits") or ()
96
+ ),
97
+ unmet_obligations=tuple(payload.get("unmetObligations") or ()),
98
+ decision=str(payload["decision"]),
99
+ reason=str(payload.get("reason") or ""),
100
+ )
101
+
102
+
103
+ def _seal_for(manifest, seal_ref: str | None) -> AttemptEvidenceSeal | None:
104
+ if not seal_ref:
105
+ return None
106
+ row = next(
107
+ (
108
+ item
109
+ for item in manifest.attempt_evidence_seals
110
+ if item.get("sealRef") == seal_ref
111
+ ),
112
+ None,
113
+ )
114
+ return AttemptEvidenceSeal.from_payload(row) if row is not None else None
115
+
116
+
117
+ def _verify_sealed_bytes(seal: AttemptEvidenceSeal) -> None:
118
+ stream_path = seal.host_event_stream.get("path")
119
+ if isinstance(stream_path, str) and stream_path:
120
+ data = Path(stream_path).read_bytes()
121
+ if _sha(data) != seal.host_event_stream.get("sha256"):
122
+ raise MutationRecoveryError("sealed bytes changed")
123
+ if len(data) != seal.host_event_stream.get("byteLength"):
124
+ raise MutationRecoveryError("sealed bytes changed")
125
+ for artifact in seal.artifacts:
126
+ path = Path(str(artifact["normalizedPath"]))
127
+ data = path.read_bytes() if path.is_file() else b""
128
+ if _sha(data) != artifact.get("sha256"):
129
+ raise MutationRecoveryError("sealed bytes changed")
130
+
131
+
132
+ def evaluate_mutation_recovery(
133
+ recovery: MutationRecoveryInput,
134
+ ) -> MutationRecoveryDecision:
135
+ """Decide carry-forward or reject from sealed evidence."""
136
+ manifest = read_execution_manifest(recovery.manifest_path)
137
+ attempt = next(
138
+ (
139
+ row
140
+ for row in manifest.attempts
141
+ if build_dispatch_id(row.invocation_ref, row.attempt)
142
+ == recovery.source_attempt_ref
143
+ ),
144
+ None,
145
+ )
146
+ if attempt is None:
147
+ raise MutationRecoveryError("source attempt is unknown")
148
+ if attempt.status == "evidence-unsealed":
149
+ return MutationRecoveryDecision(
150
+ recovery_ref=f"recovery-reject-{attempt.attempt}",
151
+ source_invocation_ref=recovery.source_invocation_ref,
152
+ source_attempt_ref=recovery.source_attempt_ref,
153
+ source_evidence_seal_ref=None,
154
+ original_baseline_digest=recovery.original_baseline_digest,
155
+ terminal_source_diff_digest=recovery.terminal_source_diff_digest,
156
+ terminal_git_chain_digest=recovery.terminal_git_chain_digest,
157
+ verifier_role_execution_ref=None,
158
+ verifier_invocation_ref=None,
159
+ recovered_evidence_refs=(),
160
+ recovered_out_of_plan_edits=(),
161
+ unmet_obligations=("evidence-unsealed",),
162
+ decision="reject",
163
+ reason="evidence-unsealed",
164
+ )
165
+ unmet: list[str] = []
166
+ if not recovery.source_evidence_seal_ref:
167
+ unmet.append("missing-source-seal")
168
+ if not recovery.recovered_evidence_refs:
169
+ unmet.append("missing-host-tdd-events")
170
+ if (
171
+ recovery.verifier_participant_ref
172
+ and recovery.verifier_participant_ref == recovery.implementer_participant_ref
173
+ ):
174
+ unmet.append("verifier-not-independent")
175
+ if not recovery.verifier_invocation_ref:
176
+ unmet.append("missing-verifier-invocation")
177
+ recovered_paths = {
178
+ str(row.get("path"))
179
+ for row in recovery.recovered_out_of_plan_edits
180
+ if isinstance(row, Mapping) and row.get("path")
181
+ }
182
+ sealed = _seal_for(manifest, recovery.source_evidence_seal_ref)
183
+ if sealed is not None:
184
+ _verify_sealed_bytes(sealed)
185
+ if recovered_paths - set(sealed.changed_paths):
186
+ raise MutationRecoveryError("out-of-plan path set differs")
187
+ if recovered_paths:
188
+ raise MutationRecoveryError("out-of-plan path set differs")
189
+ if unmet:
190
+ return MutationRecoveryDecision(
191
+ recovery_ref=f"recovery-reject-{attempt.attempt}",
192
+ source_invocation_ref=recovery.source_invocation_ref,
193
+ source_attempt_ref=recovery.source_attempt_ref,
194
+ source_evidence_seal_ref=recovery.source_evidence_seal_ref,
195
+ original_baseline_digest=recovery.original_baseline_digest,
196
+ terminal_source_diff_digest=recovery.terminal_source_diff_digest,
197
+ terminal_git_chain_digest=recovery.terminal_git_chain_digest,
198
+ verifier_role_execution_ref=recovery.verifier_role_execution_ref,
199
+ verifier_invocation_ref=recovery.verifier_invocation_ref,
200
+ recovered_evidence_refs=recovery.recovered_evidence_refs,
201
+ recovered_out_of_plan_edits=recovery.recovered_out_of_plan_edits,
202
+ unmet_obligations=tuple(unmet),
203
+ decision="reject",
204
+ reason="unmet-obligations",
205
+ )
206
+ return MutationRecoveryDecision(
207
+ recovery_ref=f"recovery-carry-{attempt.attempt}",
208
+ source_invocation_ref=recovery.source_invocation_ref,
209
+ source_attempt_ref=recovery.source_attempt_ref,
210
+ source_evidence_seal_ref=recovery.source_evidence_seal_ref,
211
+ original_baseline_digest=recovery.original_baseline_digest,
212
+ terminal_source_diff_digest=recovery.terminal_source_diff_digest,
213
+ terminal_git_chain_digest=recovery.terminal_git_chain_digest,
214
+ verifier_role_execution_ref=recovery.verifier_role_execution_ref,
215
+ verifier_invocation_ref=recovery.verifier_invocation_ref,
216
+ recovered_evidence_refs=recovery.recovered_evidence_refs,
217
+ recovered_out_of_plan_edits=recovery.recovered_out_of_plan_edits,
218
+ unmet_obligations=(),
219
+ decision="carry-forward",
220
+ reason="",
221
+ )
222
+
223
+
224
+ def confirm_mutation_cleanup(
225
+ manifest_path: Path,
226
+ recovery_ref: str,
227
+ *,
228
+ original_baseline_digest: str,
229
+ current_baseline_digest: str,
230
+ task_key: str,
231
+ ) -> MutationRecoveryDecision:
232
+ """Allow a new invocation after user cleanup matches the original baseline."""
233
+ if current_baseline_digest != original_baseline_digest:
234
+ raise MutationRecoveryError("cleanup does not restore the original baseline")
235
+ with task_mutex(task_key):
236
+ manifest = read_execution_manifest(manifest_path)
237
+ source = next(
238
+ (
239
+ MutationRecoveryDecision.from_payload(row)
240
+ for row in manifest.mutation_recovery_decisions
241
+ if row.get("recoveryRef") == recovery_ref
242
+ ),
243
+ None,
244
+ )
245
+ if source is None or source.decision != "reject":
246
+ raise MutationRecoveryError("cleanup requires a prior reject decision")
247
+ confirmed = MutationRecoveryDecision(
248
+ recovery_ref=f"{recovery_ref}-cleanup",
249
+ source_invocation_ref=source.source_invocation_ref,
250
+ source_attempt_ref=source.source_attempt_ref,
251
+ source_evidence_seal_ref=None,
252
+ original_baseline_digest=original_baseline_digest,
253
+ terminal_source_diff_digest="",
254
+ terminal_git_chain_digest="",
255
+ verifier_role_execution_ref=None,
256
+ verifier_invocation_ref=None,
257
+ recovered_evidence_refs=(),
258
+ recovered_out_of_plan_edits=(),
259
+ unmet_obligations=(),
260
+ decision="cleanup-confirmed",
261
+ reason="cleanup-confirmed",
262
+ )
263
+ updated = _replace_manifest(
264
+ manifest,
265
+ mutation_recovery_decisions=(
266
+ *manifest.mutation_recovery_decisions,
267
+ confirmed.to_payload(),
268
+ ),
269
+ )
270
+ _write_manifest_over_existing(manifest_path, updated)
271
+ return confirmed
272
+
273
+
274
+ def start_new_invocation(recovery: MutationRecoveryInput) -> None:
275
+ """Refuse a new invocation while an unresolved mutation has no decision."""
276
+ manifest = read_execution_manifest(recovery.manifest_path)
277
+ unresolved = [
278
+ row
279
+ for row in manifest.attempts
280
+ if row.status in {"mutation-present-unresolved", "evidence-unsealed"}
281
+ ]
282
+ confirmed = {
283
+ row.get("sourceAttemptRef")
284
+ for row in manifest.mutation_recovery_decisions
285
+ if row.get("decision") in {"carry-forward", "cleanup-confirmed"}
286
+ }
287
+ pending = [
288
+ row
289
+ for row in unresolved
290
+ if build_dispatch_id(row.invocation_ref, row.attempt) not in confirmed
291
+ ]
292
+ if pending:
293
+ raise MutationRecoveryError("recovery decision required")
@@ -1,5 +1,13 @@
1
- """완료(terminal) worker 판정 — pane 회수용. 판정 SSOT 는
2
- wrapper_status.read_wrapper_status 이며 여기서 재사용한다."""
1
+ """활성 run 스코프 조회 — pane 정리 의무를 어느 run 에 걸지 고르는 데 쓴다.
2
+
3
+ 호출자는 `SessionStart(compact)` 훅(`okstra-compact-reminder.sh`) 하나다. 압축
4
+ 직후 리드에게 "이 run 의 완료 teammate pane 을 라운드 경계마다 회수하라"는 의무를
5
+ 재주입할 때, 그 대상이 되는 이 프로젝트의 진행 중 run 을 여기서 찾는다.
6
+
7
+ 완료 판정(`is_completed_status`)과 프로젝트 무관 전체 조회(`active_run_dirs`)도
8
+ 여기 있었으나, 그것을 쓰던 `okstra-trace-cleanup.sh --reclaim-completed` 와
9
+ `okstra-subagent-reclaim.sh` 가 제거되면서 함께 사라졌다.
10
+ """
3
11
  from __future__ import annotations
4
12
 
5
13
  import json
@@ -8,12 +16,6 @@ from pathlib import Path
8
16
 
9
17
  from .paths import resolve_under_root
10
18
  from .reconcile import NON_TERMINAL_RECENT_STATUSES
11
- from .wrapper_status import read_wrapper_status
12
-
13
-
14
- def is_completed_status(status_path: str) -> bool:
15
- status = read_wrapper_status(Path(status_path))
16
- return status is not None and status.is_terminal
17
19
 
18
20
 
19
21
  def _iter_active_run_dirs(home: Path):
@@ -30,9 +32,9 @@ def _iter_active_run_dirs(home: Path):
30
32
  row = json.loads(line)
31
33
  except json.JSONDecodeError:
32
34
  continue
33
- # 회수 대상(진행 중) 집합은 reconcile 의 NON_TERMINAL_RECENT_STATUSES 가
34
- # SSOT. reserving 은 아직 run-dir 산출물이 없어(= 회수할 pane 이 없어) 이
35
- # 집합에 들어있지 않으므로 자연히 제외된다(allowlist).
35
+ # 대상(진행 중) 집합은 reconcile 의 NON_TERMINAL_RECENT_STATUSES 가
36
+ # SSOT. reserving 은 아직 run-dir 산출물이 없어 이 집합에 들어있지
37
+ # 않으므로 자연히 제외된다(allowlist).
36
38
  if row.get("status") not in NON_TERMINAL_RECENT_STATUSES:
37
39
  continue
38
40
  run_dir = resolve_under_root(row.get("projectRoot"), row.get("runDirRel"))
@@ -40,11 +42,6 @@ def _iter_active_run_dirs(home: Path):
40
42
  yield row.get("projectRoot"), run_dir
41
43
 
42
44
 
43
- def active_run_dirs(home: Path) -> list[Path]:
44
- """active.jsonl 의 진행 중 run 들의 절대 run-dir 목록."""
45
- return [run_dir for _project_root, run_dir in _iter_active_run_dirs(home)]
46
-
47
-
48
45
  def active_run_dirs_for_project(home: Path, cwd: Path) -> list[Path]:
49
46
  """진행 중 run 중 projectRoot 이 cwd(또는 그 상위)인 것의 절대 run-dir 목록.
50
47
  SessionStart(compact) 훅이 이 프로젝트의 run 에만 리마인더를 걸도록 스코프한다."""
@@ -63,16 +60,10 @@ def active_run_dirs_for_project(home: Path, cwd: Path) -> list[Path]:
63
60
 
64
61
 
65
62
  def main(argv: list[str]) -> int:
66
- if len(argv) == 2 and argv[0] == "--active-dirs":
67
- for run_dir in active_run_dirs(Path(argv[1])):
68
- print(run_dir)
69
- return 0
70
63
  if len(argv) == 3 and argv[0] == "--active-dirs-for":
71
64
  for run_dir in active_run_dirs_for_project(Path(argv[1]), Path(argv[2])):
72
65
  print(run_dir)
73
66
  return 0
74
- if len(argv) == 1:
75
- return 0 if is_completed_status(argv[0]) else 1
76
67
  return 1
77
68
 
78
69
 
@@ -0,0 +1,151 @@
1
+ """Lease pane titles from stored execution labels."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import secrets
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Protocol
9
+
10
+
11
+ class PaneTitleError(ValueError):
12
+ """Raised when a title lease cannot be acquired or released."""
13
+
14
+
15
+ class PaneTitleBackend(Protocol):
16
+ def get_title(self, pane_id: str) -> str: ...
17
+ def set_title(self, pane_id: str, title: str) -> None: ...
18
+ def available(self) -> bool: ...
19
+
20
+
21
+ @dataclass
22
+ class MemoryPaneBackend:
23
+ titles: dict[str, str]
24
+ capable: bool = True
25
+
26
+ def get_title(self, pane_id: str) -> str:
27
+ return self.titles.get(pane_id, "")
28
+
29
+ def set_title(self, pane_id: str, title: str) -> None:
30
+ self.titles[pane_id] = title
31
+
32
+ def available(self) -> bool:
33
+ return self.capable
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class PaneTitleLease:
38
+ run_ref: str
39
+ participant_ref: str
40
+ role_execution_ref: str
41
+ invocation_ref: str | None
42
+ pane_backend: str
43
+ pane_id: str
44
+ previous_title: str
45
+ applied_title: str
46
+ owner_token: str
47
+ status: str
48
+
49
+
50
+ class UnavailablePaneBackend:
51
+ def get_title(self, pane_id: str) -> str:
52
+ return ""
53
+
54
+ def set_title(self, pane_id: str, title: str) -> None:
55
+ return None
56
+
57
+ def available(self) -> bool:
58
+ return False
59
+
60
+
61
+ def acquire_pane_title(
62
+ *,
63
+ backend: PaneTitleBackend,
64
+ pane_id: str,
65
+ execution_label: str,
66
+ run_ref: str,
67
+ participant_ref: str,
68
+ role_execution_ref: str,
69
+ invocation_ref: str | None = None,
70
+ pane_backend: str = "cmux",
71
+ ) -> PaneTitleLease:
72
+ if not backend.available():
73
+ return PaneTitleLease(
74
+ run_ref=run_ref,
75
+ participant_ref=participant_ref,
76
+ role_execution_ref=role_execution_ref,
77
+ invocation_ref=invocation_ref,
78
+ pane_backend=pane_backend,
79
+ pane_id=pane_id,
80
+ previous_title="",
81
+ applied_title=execution_label,
82
+ owner_token="",
83
+ status="not-applicable",
84
+ )
85
+ previous = backend.get_title(pane_id)
86
+ backend.set_title(pane_id, execution_label)
87
+ return PaneTitleLease(
88
+ run_ref=run_ref,
89
+ participant_ref=participant_ref,
90
+ role_execution_ref=role_execution_ref,
91
+ invocation_ref=invocation_ref,
92
+ pane_backend=pane_backend,
93
+ pane_id=pane_id,
94
+ previous_title=previous,
95
+ applied_title=execution_label,
96
+ owner_token=secrets.token_hex(8),
97
+ status="acquired",
98
+ )
99
+
100
+
101
+ def release_pane_title(
102
+ lease: PaneTitleLease,
103
+ backend: PaneTitleBackend,
104
+ *,
105
+ owner_token: str,
106
+ ) -> PaneTitleLease:
107
+ if lease.status == "not-applicable":
108
+ return lease
109
+ if owner_token != lease.owner_token:
110
+ raise PaneTitleError("owner token does not match")
111
+ current = backend.get_title(lease.pane_id)
112
+ if current != lease.applied_title:
113
+ return PaneTitleLease(**{**lease.__dict__, "status": "manual-change-preserved"})
114
+ backend.set_title(lease.pane_id, lease.previous_title)
115
+ return PaneTitleLease(**{**lease.__dict__, "status": "released"})
116
+
117
+
118
+ def cleanup_pane_title(
119
+ lease: PaneTitleLease, backend: PaneTitleBackend
120
+ ) -> PaneTitleLease:
121
+ if lease.status == "not-applicable":
122
+ return lease
123
+ backend.set_title(lease.pane_id, lease.previous_title)
124
+ return PaneTitleLease(**{**lease.__dict__, "status": "cleaned"})
125
+
126
+
127
+ def main(argv: list[str] | None = None) -> int:
128
+ import argparse
129
+
130
+ parser = argparse.ArgumentParser(prog="okstra pane-title")
131
+ parser.add_argument("action", choices=("acquire", "release", "cleanup"))
132
+ parser.add_argument("--execution-label", default="")
133
+ parser.add_argument("--pane-id", default="")
134
+ parser.add_argument("--json", action="store_true")
135
+ args = parser.parse_args(argv)
136
+ backend = UnavailablePaneBackend()
137
+ lease = acquire_pane_title(
138
+ backend=backend,
139
+ pane_id=args.pane_id or "none",
140
+ execution_label=args.execution_label or "unused",
141
+ run_ref="run",
142
+ participant_ref="participant",
143
+ role_execution_ref="role",
144
+ )
145
+ payload = {"status": lease.status, "appliedTitle": lease.applied_title}
146
+ print(json.dumps(payload, ensure_ascii=False))
147
+ return 0
148
+
149
+
150
+ if __name__ == "__main__":
151
+ raise SystemExit(main())
@@ -2,7 +2,10 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  from dataclasses import dataclass
5
- from typing import Protocol
5
+ from typing import Literal, Protocol
6
+
7
+ from okstra_ctl.domain.provider import HostModelBinding
8
+ from okstra_ctl.domain.worker_exec import WorkerWriteCapability
6
9
 
7
10
 
8
11
  class HostModelBindingError(ValueError):
@@ -13,48 +16,76 @@ class HostModelBindingError(ValueError):
13
16
  class HostModelBindingRequest:
14
17
  host_runtime: str
15
18
  provider: str
16
- model: str
19
+ model_ref: str
20
+ version_kind: Literal["pinned", "channel"]
17
21
  model_execution_value: str
18
22
  runner: str
23
+ resolved_execution_value: str | None = None
24
+ worker_write_capability: WorkerWriteCapability | None = None
25
+
26
+ @property
27
+ def execution_value(self) -> str:
28
+ return self.resolved_execution_value or self.model_execution_value
19
29
 
20
30
 
21
31
  class HostModelBindingPort(Protocol):
22
- def resolve(self, request: HostModelBindingRequest) -> str | None: ...
32
+ def resolve(self, request: HostModelBindingRequest) -> HostModelBinding: ...
33
+
34
+
35
+ def _cli_binding(request: HostModelBindingRequest) -> HostModelBinding:
36
+ return HostModelBinding(
37
+ runner=request.runner,
38
+ catalog_execution_value=request.model_execution_value,
39
+ resolved_execution_value=request.execution_value,
40
+ host_model_value=None,
41
+ binding_fidelity="exact",
42
+ worker_write_capability=request.worker_write_capability,
43
+ )
23
44
 
24
45
 
25
46
  class FailClosedHostModelBindingPort:
26
47
  def __init__(self, host_runtime: str) -> None:
27
48
  self._host_runtime = host_runtime
28
49
 
29
- def resolve(self, request: HostModelBindingRequest) -> str | None:
50
+ def resolve(self, request: HostModelBindingRequest) -> HostModelBinding:
30
51
  if request.runner == "cli-wrapper":
31
- return None
52
+ return _cli_binding(request)
32
53
  raise HostModelBindingError(
33
54
  "unsupported host model: "
34
55
  f"host={self._host_runtime!r}, provider={request.provider!r}, "
35
- f"model={request.model_execution_value!r}"
56
+ f"model={request.execution_value!r}"
36
57
  )
37
58
 
38
59
 
39
60
  class NativeExecutionValueHostModelBindingPort:
40
- """Explicit policy for hosts whose native API accepts provider CLI values."""
61
+ """Binding for hosts whose native API accepts provider execution values."""
41
62
 
42
63
  def __init__(self, host_runtime: str, native_provider: str) -> None:
43
64
  self._host_runtime = host_runtime
44
65
  self._native_provider = native_provider
45
66
 
46
- def resolve(self, request: HostModelBindingRequest) -> str | None:
67
+ def resolve(self, request: HostModelBindingRequest) -> HostModelBinding:
47
68
  if request.runner == "cli-wrapper":
48
- return None
49
- if (
50
- request.runner != "native-session"
51
- or request.host_runtime != self._host_runtime
52
- or request.provider != self._native_provider
53
- or not request.model_execution_value.strip()
54
- ):
69
+ return _cli_binding(request)
70
+ if not self._supports_native(request):
55
71
  raise HostModelBindingError(
56
72
  "unsupported host model: "
57
73
  f"host={request.host_runtime!r}, provider={request.provider!r}, "
58
- f"model={request.model_execution_value!r}"
74
+ f"model={request.execution_value!r}"
59
75
  )
60
- return request.model_execution_value
76
+ return HostModelBinding(
77
+ runner=request.runner,
78
+ catalog_execution_value=request.model_execution_value,
79
+ resolved_execution_value=request.execution_value,
80
+ host_model_value=request.execution_value,
81
+ binding_fidelity="exact",
82
+ worker_write_capability=request.worker_write_capability,
83
+ )
84
+
85
+ def _supports_native(self, request: HostModelBindingRequest) -> bool:
86
+ return (
87
+ request.runner == "native-session"
88
+ and request.host_runtime == self._host_runtime
89
+ and request.provider == self._native_provider
90
+ and bool(request.execution_value.strip())
91
+ )
@@ -5,6 +5,8 @@ from dataclasses import dataclass
5
5
  from pathlib import Path
6
6
  from typing import Literal, Protocol
7
7
 
8
+ from okstra_ctl.domain.worker_exec import WorkerWriteCapability
9
+
8
10
 
9
11
  @dataclass(frozen=True)
10
12
  class WorkerDispatchRequest:
@@ -30,3 +32,5 @@ class WorkerDispatchPlan:
30
32
 
31
33
  class WorkerDispatchPort(Protocol):
32
34
  def build_plan(self, request: WorkerDispatchRequest) -> WorkerDispatchPlan: ...
35
+
36
+ def worker_write_capability(self) -> WorkerWriteCapability: ...
@@ -46,7 +46,7 @@ class ProviderRegistry:
46
46
  return tuple(
47
47
  provider_id
48
48
  for provider_id, spec in self._providers.items()
49
- if role in spec.supported_roles
49
+ if spec.supports_role(role)
50
50
  )
51
51
 
52
52
  @property
@@ -72,10 +72,14 @@ def _registry_for_roots(
72
72
  ) -> ProviderRegistry:
73
73
  discovered = ProviderRegistry.from_roots((bundled_root, user_root)).providers
74
74
  bundled_order = ("claude", "antigravity", "codex", "grok", "kimi")
75
- ordered = {provider_id: discovered[provider_id] for provider_id in bundled_order}
75
+ ordered = {
76
+ provider_id: discovered[provider_id]
77
+ for provider_id in bundled_order
78
+ if provider_id in discovered
79
+ }
76
80
  ordered.update(
77
- (provider_id, spec)
78
- for provider_id, spec in discovered.items()
81
+ (provider_id, discovered[provider_id])
82
+ for provider_id in sorted(discovered)
79
83
  if provider_id not in ordered
80
84
  )
81
85
  return ProviderRegistry(ordered)