okstra 0.174.0 → 0.176.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. package/README.md +18 -9
  2. package/bin/okstra +8 -4
  3. package/dist/cli-registry.d.mts +16 -0
  4. package/dist/cli-registry.mjs +560 -0
  5. package/dist/cli-registry.mjs.map +1 -0
  6. package/dist/commands/execute/agent-prompt.d.mts +1 -0
  7. package/{src → dist}/commands/execute/agent-prompt.mjs +10 -11
  8. package/dist/commands/execute/agent-prompt.mjs.map +1 -0
  9. package/dist/commands/execute/codex-dispatch.d.mts +3 -0
  10. package/dist/commands/execute/codex-dispatch.mjs +6 -0
  11. package/dist/commands/execute/codex-dispatch.mjs.map +1 -0
  12. package/dist/commands/execute/codex-run.d.mts +3 -0
  13. package/{src → dist}/commands/execute/codex-run.mjs +30 -37
  14. package/dist/commands/execute/codex-run.mjs.map +1 -0
  15. package/dist/commands/execute/convergence.d.mts +1 -0
  16. package/{src → dist}/commands/execute/convergence.mjs +15 -16
  17. package/dist/commands/execute/convergence.mjs.map +1 -0
  18. package/dist/commands/execute/error-log.d.mts +1 -0
  19. package/{src → dist}/commands/execute/error-log.mjs +2 -3
  20. package/dist/commands/execute/error-log.mjs.map +1 -0
  21. package/dist/commands/execute/git-reconcile.d.mts +1 -0
  22. package/{src → dist}/commands/execute/git-reconcile.mjs +10 -11
  23. package/dist/commands/execute/git-reconcile.mjs.map +1 -0
  24. package/dist/commands/execute/handoff.d.mts +1 -0
  25. package/{src → dist}/commands/execute/handoff.mjs +10 -11
  26. package/dist/commands/execute/handoff.mjs.map +1 -0
  27. package/dist/commands/execute/incremental-carry.d.mts +1 -0
  28. package/{src → dist}/commands/execute/incremental-carry.mjs +2 -3
  29. package/dist/commands/execute/incremental-carry.mjs.map +1 -0
  30. package/dist/commands/execute/incremental-scope.d.mts +1 -0
  31. package/{src → dist}/commands/execute/incremental-scope.mjs +2 -3
  32. package/dist/commands/execute/incremental-scope.mjs.map +1 -0
  33. package/dist/commands/execute/integrate-stages.d.mts +1 -0
  34. package/{src → dist}/commands/execute/integrate-stages.mjs +10 -11
  35. package/dist/commands/execute/integrate-stages.mjs.map +1 -0
  36. package/dist/commands/execute/pane-title.d.mts +1 -0
  37. package/dist/commands/execute/pane-title.mjs +20 -0
  38. package/dist/commands/execute/pane-title.mjs.map +1 -0
  39. package/dist/commands/execute/plan-items.d.mts +1 -0
  40. package/dist/commands/execute/plan-items.mjs +9 -0
  41. package/dist/commands/execute/plan-items.mjs.map +1 -0
  42. package/dist/commands/execute/plan-validate.d.mts +1 -0
  43. package/dist/commands/execute/plan-validate.mjs +68 -0
  44. package/dist/commands/execute/plan-validate.mjs.map +1 -0
  45. package/dist/commands/execute/plan-verify.d.mts +1 -0
  46. package/{src → dist}/commands/execute/plan-verify.mjs +13 -16
  47. package/dist/commands/execute/plan-verify.mjs.map +1 -0
  48. package/dist/commands/execute/render-bundle.d.mts +3 -0
  49. package/{src → dist}/commands/execute/render-bundle.mjs +65 -61
  50. package/dist/commands/execute/render-bundle.mjs.map +1 -0
  51. package/dist/commands/execute/run.d.mts +24 -0
  52. package/dist/commands/execute/run.mjs +196 -0
  53. package/dist/commands/execute/run.mjs.map +1 -0
  54. package/dist/commands/execute/spawn-followups.d.mts +1 -0
  55. package/{src → dist}/commands/execute/spawn-followups.mjs +6 -7
  56. package/dist/commands/execute/spawn-followups.mjs.map +1 -0
  57. package/dist/commands/execute/team.d.mts +3 -0
  58. package/{src → dist}/commands/execute/team.mjs +29 -36
  59. package/dist/commands/execute/team.mjs.map +1 -0
  60. package/dist/commands/execute/token-usage.d.mts +1 -0
  61. package/{src → dist}/commands/execute/token-usage.mjs +2 -3
  62. package/dist/commands/execute/token-usage.mjs.map +1 -0
  63. package/dist/commands/execute/wizard.d.mts +8 -0
  64. package/dist/commands/execute/wizard.mjs +191 -0
  65. package/dist/commands/execute/wizard.mjs.map +1 -0
  66. package/dist/commands/execute/worker-audit-check.d.mts +1 -0
  67. package/{src → dist}/commands/execute/worker-audit-check.mjs +11 -12
  68. package/dist/commands/execute/worker-audit-check.mjs.map +1 -0
  69. package/dist/commands/execute/worker-dispatch.d.mts +7 -0
  70. package/{src → dist}/commands/execute/worker-dispatch.mjs +34 -46
  71. package/dist/commands/execute/worker-dispatch.mjs.map +1 -0
  72. package/dist/commands/execute/worker-state.d.mts +1 -0
  73. package/{src → dist}/commands/execute/worker-state.mjs +15 -16
  74. package/dist/commands/execute/worker-state.mjs.map +1 -0
  75. package/dist/commands/execute/worktree-lookup.d.mts +1 -0
  76. package/{src → dist}/commands/execute/worktree-lookup.mjs +38 -37
  77. package/dist/commands/execute/worktree-lookup.mjs.map +1 -0
  78. package/dist/commands/execute/worktree-status.d.mts +1 -0
  79. package/{src → dist}/commands/execute/worktree-status.mjs +52 -46
  80. package/dist/commands/execute/worktree-status.mjs.map +1 -0
  81. package/dist/commands/inspect/code-review.d.mts +1 -0
  82. package/{src → dist}/commands/inspect/code-review.mjs +16 -19
  83. package/dist/commands/inspect/code-review.mjs.map +1 -0
  84. package/dist/commands/inspect/container.d.mts +1 -0
  85. package/{src → dist}/commands/inspect/container.mjs +11 -13
  86. package/dist/commands/inspect/container.mjs.map +1 -0
  87. package/dist/commands/inspect/context-cost.d.mts +1 -0
  88. package/{src → dist}/commands/inspect/context-cost.mjs +11 -13
  89. package/dist/commands/inspect/context-cost.mjs.map +1 -0
  90. package/dist/commands/inspect/design-prep.d.mts +1 -0
  91. package/{src → dist}/commands/inspect/design-prep.mjs +10 -11
  92. package/dist/commands/inspect/design-prep.mjs.map +1 -0
  93. package/dist/commands/inspect/error-report.d.mts +1 -0
  94. package/{src → dist}/commands/inspect/error-report.mjs +11 -13
  95. package/dist/commands/inspect/error-report.mjs.map +1 -0
  96. package/dist/commands/inspect/error-zip.d.mts +1 -0
  97. package/{src → dist}/commands/inspect/error-zip.mjs +11 -12
  98. package/dist/commands/inspect/error-zip.mjs.map +1 -0
  99. package/dist/commands/inspect/log-report.d.mts +1 -0
  100. package/{src → dist}/commands/inspect/log-report.mjs +11 -13
  101. package/dist/commands/inspect/log-report.mjs.map +1 -0
  102. package/dist/commands/inspect/profile-show.d.mts +1 -0
  103. package/{src → dist}/commands/inspect/profile-show.mjs +10 -11
  104. package/dist/commands/inspect/profile-show.mjs.map +1 -0
  105. package/dist/commands/inspect/recap.d.mts +1 -0
  106. package/{src → dist}/commands/inspect/recap.mjs +11 -13
  107. package/dist/commands/inspect/recap.mjs.map +1 -0
  108. package/dist/commands/inspect/resolve-task-key.d.mts +1 -0
  109. package/{src → dist}/commands/inspect/resolve-task-key.mjs +11 -13
  110. package/dist/commands/inspect/resolve-task-key.mjs.map +1 -0
  111. package/dist/commands/inspect/rollup.d.mts +1 -0
  112. package/{src → dist}/commands/inspect/rollup.mjs +11 -13
  113. package/dist/commands/inspect/rollup.mjs.map +1 -0
  114. package/dist/commands/inspect/run-audit.d.mts +1 -0
  115. package/{src → dist}/commands/inspect/run-audit.mjs +11 -12
  116. package/dist/commands/inspect/run-audit.mjs.map +1 -0
  117. package/dist/commands/inspect/set-work-status.d.mts +1 -0
  118. package/{src → dist}/commands/inspect/set-work-status.mjs +11 -13
  119. package/dist/commands/inspect/set-work-status.mjs.map +1 -0
  120. package/dist/commands/inspect/stage-map.d.mts +1 -0
  121. package/dist/commands/inspect/stage-map.mjs +110 -0
  122. package/dist/commands/inspect/stage-map.mjs.map +1 -0
  123. package/dist/commands/inspect/task-list.d.mts +1 -0
  124. package/dist/commands/inspect/task-list.mjs +103 -0
  125. package/dist/commands/inspect/task-list.mjs.map +1 -0
  126. package/dist/commands/inspect/task-show.d.mts +1 -0
  127. package/dist/commands/inspect/task-show.mjs +108 -0
  128. package/dist/commands/inspect/task-show.mjs.map +1 -0
  129. package/dist/commands/inspect/time-report.d.mts +1 -0
  130. package/{src → dist}/commands/inspect/time-report.mjs +11 -13
  131. package/dist/commands/inspect/time-report.mjs.map +1 -0
  132. package/dist/commands/inspect/usage-report.d.mts +1 -0
  133. package/{src → dist}/commands/inspect/usage-report.mjs +11 -13
  134. package/dist/commands/inspect/usage-report.mjs.map +1 -0
  135. package/dist/commands/inspect/user-response.d.mts +1 -0
  136. package/{src → dist}/commands/inspect/user-response.mjs +10 -11
  137. package/dist/commands/inspect/user-response.mjs.map +1 -0
  138. package/dist/commands/inspect/worker-liveness.d.mts +1 -0
  139. package/{src → dist}/commands/inspect/worker-liveness.mjs +11 -13
  140. package/dist/commands/inspect/worker-liveness.mjs.map +1 -0
  141. package/dist/commands/lifecycle/check-project.d.mts +21 -0
  142. package/dist/commands/lifecycle/check-project.mjs +175 -0
  143. package/dist/commands/lifecycle/check-project.mjs.map +1 -0
  144. package/dist/commands/lifecycle/config.d.mts +1 -0
  145. package/dist/commands/lifecycle/config.mjs +369 -0
  146. package/dist/commands/lifecycle/config.mjs.map +1 -0
  147. package/dist/commands/lifecycle/doctor.d.mts +10 -0
  148. package/dist/commands/lifecycle/doctor.mjs +317 -0
  149. package/dist/commands/lifecycle/doctor.mjs.map +1 -0
  150. package/dist/commands/lifecycle/install.d.mts +17 -0
  151. package/dist/commands/lifecycle/install.mjs +1077 -0
  152. package/dist/commands/lifecycle/install.mjs.map +1 -0
  153. package/dist/commands/lifecycle/migrate.d.mts +1 -0
  154. package/{src → dist}/commands/lifecycle/migrate.mjs +10 -11
  155. package/dist/commands/lifecycle/migrate.mjs.map +1 -0
  156. package/dist/commands/lifecycle/model.d.mts +1 -0
  157. package/dist/commands/lifecycle/model.mjs +22 -0
  158. package/dist/commands/lifecycle/model.mjs.map +1 -0
  159. package/dist/commands/lifecycle/paths.d.mts +1 -0
  160. package/dist/commands/lifecycle/paths.mjs +102 -0
  161. package/dist/commands/lifecycle/paths.mjs.map +1 -0
  162. package/dist/commands/lifecycle/preflight.d.mts +1 -0
  163. package/dist/commands/lifecycle/preflight.mjs +124 -0
  164. package/dist/commands/lifecycle/preflight.mjs.map +1 -0
  165. package/dist/commands/lifecycle/setup.d.mts +1 -0
  166. package/dist/commands/lifecycle/setup.mjs +297 -0
  167. package/dist/commands/lifecycle/setup.mjs.map +1 -0
  168. package/dist/commands/lifecycle/uninstall.d.mts +4 -0
  169. package/dist/commands/lifecycle/uninstall.mjs +262 -0
  170. package/dist/commands/lifecycle/uninstall.mjs.map +1 -0
  171. package/dist/commands/manager.d.mts +3 -0
  172. package/{src → dist}/commands/manager.mjs +25 -32
  173. package/dist/commands/manager.mjs.map +1 -0
  174. package/dist/commands/memory/memory.d.mts +1 -0
  175. package/dist/commands/memory/memory.mjs +569 -0
  176. package/dist/commands/memory/memory.mjs.map +1 -0
  177. package/dist/commands/pr/pr.d.mts +33 -0
  178. package/dist/commands/pr/pr.mjs +296 -0
  179. package/dist/commands/pr/pr.mjs.map +1 -0
  180. package/dist/commands/report/agent-activity.d.mts +1 -0
  181. package/{src → dist}/commands/report/agent-activity.mjs +11 -12
  182. package/dist/commands/report/agent-activity.mjs.map +1 -0
  183. package/dist/commands/report/finalize.d.mts +4 -0
  184. package/{src → dist}/commands/report/finalize.mjs +25 -32
  185. package/dist/commands/report/finalize.mjs.map +1 -0
  186. package/dist/commands/report/inject-report-index.d.mts +1 -0
  187. package/{src → dist}/commands/report/inject-report-index.mjs +6 -7
  188. package/dist/commands/report/inject-report-index.mjs.map +1 -0
  189. package/dist/commands/report/render-final-report.d.mts +1 -0
  190. package/{src → dist}/commands/report/render-final-report.mjs +6 -7
  191. package/dist/commands/report/render-final-report.mjs.map +1 -0
  192. package/dist/commands/report/render-views.d.mts +1 -0
  193. package/{src → dist}/commands/report/render-views.mjs +6 -7
  194. package/dist/commands/report/render-views.mjs.map +1 -0
  195. package/dist/commands/report/translate.d.mts +1 -0
  196. package/{src → dist}/commands/report/translate.mjs +6 -7
  197. package/dist/commands/report/translate.mjs.map +1 -0
  198. package/dist/lib/helper-scripts.d.mts +3 -0
  199. package/{src → dist}/lib/helper-scripts.mjs +11 -14
  200. package/dist/lib/helper-scripts.mjs.map +1 -0
  201. package/dist/lib/host-registry-client.d.mts +25 -0
  202. package/dist/lib/host-registry-client.mjs +152 -0
  203. package/dist/lib/host-registry-client.mjs.map +1 -0
  204. package/dist/lib/install-assets.d.mts +8 -0
  205. package/{src → dist}/lib/install-assets.mjs +17 -20
  206. package/dist/lib/install-assets.mjs.map +1 -0
  207. package/dist/lib/okstra-dirs.d.mts +4 -0
  208. package/{src → dist}/lib/okstra-dirs.mjs +3 -6
  209. package/dist/lib/okstra-dirs.mjs.map +1 -0
  210. package/dist/lib/paths.d.mts +4 -0
  211. package/dist/lib/paths.mjs +78 -0
  212. package/dist/lib/paths.mjs.map +1 -0
  213. package/dist/lib/proc.d.mts +3 -0
  214. package/dist/lib/proc.mjs +31 -0
  215. package/dist/lib/proc.mjs.map +1 -0
  216. package/dist/lib/python-helper.d.mts +8 -0
  217. package/dist/lib/python-helper.mjs +123 -0
  218. package/dist/lib/python-helper.mjs.map +1 -0
  219. package/dist/lib/runtime-manifest.d.mts +7 -0
  220. package/dist/lib/runtime-manifest.mjs +73 -0
  221. package/dist/lib/runtime-manifest.mjs.map +1 -0
  222. package/dist/lib/skill-catalog.d.mts +4 -0
  223. package/dist/lib/skill-catalog.mjs +50 -0
  224. package/dist/lib/skill-catalog.mjs.map +1 -0
  225. package/dist/lib/types.d.mts +129 -0
  226. package/dist/lib/types.mjs +2 -0
  227. package/dist/lib/types.mjs.map +1 -0
  228. package/dist/lib/version.d.mts +2 -0
  229. package/dist/lib/version.mjs +27 -0
  230. package/dist/lib/version.mjs.map +1 -0
  231. package/docs/architecture/storage-model.md +6 -2
  232. package/docs/architecture.md +29 -20
  233. package/docs/cli.md +40 -16
  234. package/docs/contributor-change-matrix.md +2 -2
  235. package/docs/for-ai/skills/okstra-inspect.md +2 -3
  236. package/docs/for-ai/skills/okstra-rollup.md +1 -0
  237. package/docs/project-structure-overview.md +72 -62
  238. package/docs/task-process/README.md +12 -8
  239. package/docs/task-process/common-flow.md +13 -16
  240. package/docs/task-process/error-analysis.md +9 -10
  241. package/docs/task-process/final-verification.md +7 -7
  242. package/docs/task-process/implementation-planning.md +9 -9
  243. package/docs/task-process/implementation.md +6 -6
  244. package/docs/task-process/release-handoff.md +8 -7
  245. package/docs/task-process/requirements-discovery.md +8 -8
  246. package/package.json +11 -5
  247. package/runtime/BUILD.json +2 -2
  248. package/runtime/agents/workers/claude-worker.md +2 -1
  249. package/runtime/agents/workers/report-writer-worker.md +3 -1
  250. package/runtime/agents/workers/translator-worker.md +3 -1
  251. package/runtime/bin/lib/okstra/cli.sh +12 -0
  252. package/runtime/bin/lib/okstra/globals.sh +3 -0
  253. package/runtime/bin/lib/okstra/interactive.sh +12 -6
  254. package/runtime/bin/lib/okstra/usage.sh +7 -6
  255. package/runtime/bin/okstra-error-log.py +27 -0
  256. package/runtime/bin/okstra-provider-exec.py +217 -5
  257. package/runtime/bin/okstra-spawn-followups.py +4 -2
  258. package/runtime/bin/okstra.sh +10 -0
  259. package/runtime/prompts/launch.template.md +2 -2
  260. package/runtime/prompts/lead/context-loader.md +1 -2
  261. package/runtime/prompts/lead/convergence.md +27 -18
  262. package/runtime/prompts/lead/okstra-lead-contract.md +6 -5
  263. package/runtime/prompts/lead/plan-body-verification.md +10 -3
  264. package/runtime/prompts/lead/report-writer.md +15 -1
  265. package/runtime/prompts/lead/team-contract.md +17 -13
  266. package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
  267. package/runtime/prompts/profiles/_implementation-executor.md +0 -3
  268. package/runtime/prompts/profiles/_implementation-verifier.md +3 -7
  269. package/runtime/prompts/profiles/change-impact-analysis.md +20 -0
  270. package/runtime/prompts/profiles/error-analysis.md +27 -1
  271. package/runtime/prompts/profiles/feature-analysis.md +20 -0
  272. package/runtime/prompts/profiles/final-verification.md +20 -1
  273. package/runtime/prompts/profiles/implementation-option-selection.md +20 -0
  274. package/runtime/prompts/profiles/implementation-planning.md +26 -0
  275. package/runtime/prompts/profiles/implementation.md +19 -0
  276. package/runtime/prompts/profiles/improvement-discovery.md +21 -1
  277. package/runtime/prompts/profiles/project-analysis.md +20 -0
  278. package/runtime/prompts/profiles/release-handoff.md +4 -0
  279. package/runtime/prompts/profiles/requirements-discovery.md +27 -1
  280. package/runtime/prompts/wizard/prompts.ko.json +35 -0
  281. package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +8 -0
  282. package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +8 -0
  283. package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
  284. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +2 -2
  285. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +25 -5
  286. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
  287. package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +4 -1
  288. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
  289. package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +2 -2
  290. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +79 -11
  291. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +95 -25
  292. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +59 -14
  293. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +55 -9
  294. package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +51 -12
  295. package/runtime/python/okstra_ctl/adapters/runtime/__init__.py +1 -0
  296. package/runtime/python/okstra_ctl/adapters/runtime/assembly.py +27 -0
  297. package/runtime/python/okstra_ctl/adapters/runtime/cli_wrapper.py +49 -0
  298. package/runtime/python/okstra_ctl/adapters/runtime/cmux.py +74 -0
  299. package/runtime/python/okstra_ctl/agent_activity.py +5 -1
  300. package/runtime/python/okstra_ctl/agent_invocation.py +608 -14
  301. package/runtime/python/okstra_ctl/agent_prompt_cli.py +249 -50
  302. package/runtime/python/okstra_ctl/application/open_worker.py +29 -0
  303. package/runtime/python/okstra_ctl/application/resolve_assignment.py +53 -12
  304. package/runtime/python/okstra_ctl/assignment_environment.py +102 -0
  305. package/runtime/python/okstra_ctl/assignment_resolver.py +790 -0
  306. package/runtime/python/okstra_ctl/attempt_evidence.py +342 -0
  307. package/runtime/python/okstra_ctl/convergence.py +409 -3
  308. package/runtime/python/okstra_ctl/convergence_engine.py +248 -15
  309. package/runtime/python/okstra_ctl/convergence_store.py +170 -1
  310. package/runtime/python/okstra_ctl/dispatch_core.py +1204 -155
  311. package/runtime/python/okstra_ctl/dispatch_state.py +696 -11
  312. package/runtime/python/okstra_ctl/doctor.py +14 -0
  313. package/runtime/python/okstra_ctl/domain/host.py +34 -1
  314. package/runtime/python/okstra_ctl/domain/provider.py +212 -6
  315. package/runtime/python/okstra_ctl/domain/role.py +125 -0
  316. package/runtime/python/okstra_ctl/domain/wizard/interaction.py +18 -0
  317. package/runtime/python/okstra_ctl/domain/worker_exec.py +34 -1
  318. package/runtime/python/okstra_ctl/domain/worker_role.py +13 -3
  319. package/runtime/python/okstra_ctl/domain/worker_runtime.py +44 -0
  320. package/runtime/python/okstra_ctl/domain/worker_stream.py +24 -0
  321. package/runtime/python/okstra_ctl/error_log_write.py +9 -0
  322. package/runtime/python/okstra_ctl/error_report.py +21 -1
  323. package/runtime/python/okstra_ctl/execution_identity.py +529 -0
  324. package/runtime/python/okstra_ctl/execution_manifest.py +1281 -0
  325. package/runtime/python/okstra_ctl/execution_mutation_audit.py +619 -0
  326. package/runtime/python/okstra_ctl/implementation_outcome.py +21 -5
  327. package/runtime/python/okstra_ctl/index.py +6 -2
  328. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +43 -3
  329. package/runtime/python/okstra_ctl/legacy_model_selection.py +578 -0
  330. package/runtime/python/okstra_ctl/manager_sync.py +4 -1
  331. package/runtime/python/okstra_ctl/model_cli.py +278 -0
  332. package/runtime/python/okstra_ctl/model_defaults.py +46 -0
  333. package/runtime/python/okstra_ctl/model_discovery.py +26 -7
  334. package/runtime/python/okstra_ctl/model_pool.py +501 -0
  335. package/runtime/python/okstra_ctl/models.py +47 -27
  336. package/runtime/python/okstra_ctl/mutation_recovery.py +293 -0
  337. package/runtime/python/okstra_ctl/next_phase.py +236 -0
  338. package/runtime/python/okstra_ctl/pane_title.py +151 -0
  339. package/runtime/python/okstra_ctl/ports/host_model.py +48 -17
  340. package/runtime/python/okstra_ctl/ports/worker_dispatch.py +4 -0
  341. package/runtime/python/okstra_ctl/ports/worker_runtime.py +26 -0
  342. package/runtime/python/okstra_ctl/recap.py +4 -1
  343. package/runtime/python/okstra_ctl/registry/provider_registry.py +8 -4
  344. package/runtime/python/okstra_ctl/render.py +124 -57
  345. package/runtime/python/okstra_ctl/render_final_report.py +19 -0
  346. package/runtime/python/okstra_ctl/report_contract.py +39 -0
  347. package/runtime/python/okstra_ctl/report_finalize.py +16 -0
  348. package/runtime/python/okstra_ctl/report_html/render.py +1 -0
  349. package/runtime/python/okstra_ctl/role_requirements.py +210 -0
  350. package/runtime/python/okstra_ctl/rollup.py +4 -1
  351. package/runtime/python/okstra_ctl/run.py +1210 -258
  352. package/runtime/python/okstra_ctl/run_index_row.py +7 -1
  353. package/runtime/python/okstra_ctl/stage_fix_carry.py +17 -1
  354. package/runtime/python/okstra_ctl/team.py +14 -18
  355. package/runtime/python/okstra_ctl/time_report.py +38 -4
  356. package/runtime/python/okstra_ctl/usage_identity.py +86 -0
  357. package/runtime/python/okstra_ctl/usage_report.py +15 -3
  358. package/runtime/python/okstra_ctl/wizard.py +1449 -66
  359. package/runtime/python/okstra_ctl/worker_audit_ledger.py +68 -65
  360. package/runtime/python/okstra_ctl/worker_prompt_body.py +18 -2
  361. package/runtime/python/okstra_ctl/worker_prompt_contract.py +40 -9
  362. package/runtime/python/okstra_ctl/worker_prompt_headers.py +16 -1
  363. package/runtime/python/okstra_ctl/worker_request.py +7 -1
  364. package/runtime/python/okstra_ctl/worker_runner.py +130 -5
  365. package/runtime/python/okstra_ctl/workers.py +7 -5
  366. package/runtime/python/okstra_ctl/workflow.py +18 -32
  367. package/runtime/python/okstra_ctl/worktree.py +3 -3
  368. package/runtime/python/okstra_ctl/worktree_registry.py +5 -4
  369. package/runtime/python/okstra_ctl/write_policy.py +571 -0
  370. package/runtime/python/okstra_project/state.py +54 -6
  371. package/runtime/schemas/convergence-groups-v2.0.schema.json +128 -0
  372. package/runtime/schemas/execution-manifest-v2.schema.json +340 -0
  373. package/runtime/schemas/final-report-v2.0.schema.json +68 -5
  374. package/runtime/skills/okstra-inspect/facets/recap.md +2 -0
  375. package/runtime/skills/okstra-inspect/facets/report.md +1 -1
  376. package/runtime/skills/okstra-inspect/facets/status.md +15 -13
  377. package/runtime/skills/okstra-rollup/SKILL.md +1 -0
  378. package/runtime/skills/okstra-run/SKILL.md +14 -14
  379. package/runtime/templates/implementation-worker-preamble.md +3 -18
  380. package/runtime/templates/project-docs/task-index.template.md +0 -1
  381. package/runtime/templates/report-writer-prompt-preamble.md +2 -0
  382. package/runtime/templates/reports/final-report-v2.template.md +7 -1
  383. package/runtime/templates/reports/html/base.template.html +21 -0
  384. package/runtime/templates/reports/html/i18n/en.json +4 -0
  385. package/runtime/templates/reports/html/i18n/ko.json +4 -0
  386. package/runtime/templates/reports/html/macros/forms.html +5 -4
  387. package/runtime/templates/reports/html/tasks/final-verification.template.html +1 -1
  388. package/runtime/templates/reports/html/tasks/implementation.template.html +1 -1
  389. package/runtime/templates/worker-prompt-preamble.md +6 -35
  390. package/runtime/validators/lib/fixtures.sh +49 -16
  391. package/runtime/validators/validate-run.py +486 -134
  392. package/runtime/validators/validate_session_conformance.py +25 -7
  393. package/src/cli-registry.mjs +0 -539
  394. package/src/commands/execute/codex-dispatch.mjs +0 -10
  395. package/src/commands/execute/plan-items.mjs +0 -9
  396. package/src/commands/execute/plan-validate.mjs +0 -68
  397. package/src/commands/execute/run.mjs +0 -208
  398. package/src/commands/execute/wizard.mjs +0 -167
  399. package/src/commands/inspect/stage-map.mjs +0 -98
  400. package/src/commands/inspect/task-list.mjs +0 -97
  401. package/src/commands/inspect/task-show.mjs +0 -100
  402. package/src/commands/lifecycle/check-project.mjs +0 -187
  403. package/src/commands/lifecycle/config.mjs +0 -385
  404. package/src/commands/lifecycle/doctor.mjs +0 -308
  405. package/src/commands/lifecycle/install.mjs +0 -1170
  406. package/src/commands/lifecycle/paths.mjs +0 -104
  407. package/src/commands/lifecycle/preflight.mjs +0 -125
  408. package/src/commands/lifecycle/setup.mjs +0 -333
  409. package/src/commands/lifecycle/uninstall.mjs +0 -279
  410. package/src/commands/memory/memory.mjs +0 -524
  411. package/src/commands/pr/pr.mjs +0 -261
  412. package/src/lib/host-registry-client.mjs +0 -176
  413. package/src/lib/paths.mjs +0 -86
  414. package/src/lib/proc.mjs +0 -31
  415. package/src/lib/python-helper.mjs +0 -142
  416. package/src/lib/runtime-manifest.mjs +0 -70
  417. package/src/lib/skill-catalog.mjs +0 -53
  418. package/src/lib/version.mjs +0 -20
  419. /package/{src → dist}/commands/pr/default.md +0 -0
@@ -0,0 +1,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")
@@ -0,0 +1,236 @@
1
+ """다음 Phase 포인터의 shape · 승격 · 투영 SSOT.
2
+
3
+ 포인터는 `{phase, status, rationale}` 구조체다. 저작은 리드(report-writer)가
4
+ 하고, Phase 7 검증기가 리포트의 Phase 라우팅과 대조해 어긋나면 정정한다.
5
+ 이 모듈은 세 가지만 소유한다 — 구조체 판별, 구형 문자열 승격, 리포트 투영.
6
+ 다음 phase 를 계산하는 다른 알고리즘을 다시 만들지 않는다.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Mapping
11
+
12
+ STATUS_READY = "ready"
13
+ STATUS_PENDING = "pending"
14
+ STATUS_BLOCKED = "blocked"
15
+ STATUS_TERMINAL = "terminal"
16
+
17
+ POINTER_STATUSES = frozenset(
18
+ {STATUS_READY, STATUS_PENDING, STATUS_BLOCKED, STATUS_TERMINAL}
19
+ )
20
+
21
+ # 구형 문자열 포인터가 쓰던 센티널 → status. 이 표는 승격에만 쓰이며
22
+ # 새 포인터를 만드는 데는 절대 쓰지 않는다.
23
+ _LEGACY_SENTINELS = {
24
+ "": STATUS_PENDING,
25
+ "unknown": STATUS_PENDING,
26
+ "pending-routing-decision": STATUS_PENDING,
27
+ "pending-release-handoff": STATUS_PENDING,
28
+ "done-or-follow-up": STATUS_TERMINAL,
29
+ }
30
+
31
+ # 라우팅 필드가 없어 항상 terminal 인 task-type.
32
+ _TERMINAL_TASK_TYPES = frozenset({"release-handoff"})
33
+
34
+ # 라우팅 필드가 없어 항상 pending 인 사이드트랙 task-type.
35
+ _SIDETRACK_TASK_TYPES = frozenset(
36
+ {
37
+ "improvement-discovery",
38
+ "project-analysis",
39
+ "feature-analysis",
40
+ "change-impact-analysis",
41
+ }
42
+ )
43
+
44
+ # project() 가 명시 분기로 다루는 task-type 전체를 손으로 적은 선언.
45
+ # project() 는 이 집합을 읽지 않는다 — 분기는 아래에 그대로 나열돼 있다.
46
+ #
47
+ # 이 집합을 대조하는 계약 테스트는 PHASE_SEQUENCE 에 있으면서 여기 없는 phase 만
48
+ # 잡아낸다. 선언 두 개를 비교하는 것이므로, 여기에 이름을 적고 project() 에 분기를
49
+ # 안 넣으면 그 phase 는 여전히 pending 폴백으로 조용히 흐른다. 분기의 실재는
50
+ # tests/contract/test_next_phase_projection.py 의
51
+ # test_project_has_a_live_branch_for_every_lifecycle_phase 가 정상 리포트를 실제로
52
+ # 투영해 확인한다.
53
+ HANDLED_TASK_TYPES = frozenset(
54
+ _TERMINAL_TASK_TYPES
55
+ | _SIDETRACK_TASK_TYPES
56
+ | {
57
+ "requirements-discovery",
58
+ "error-analysis",
59
+ "implementation-option-selection",
60
+ "implementation-planning",
61
+ "implementation",
62
+ "final-verification",
63
+ }
64
+ )
65
+
66
+ # implementation-option-selection 의 routing enum 중 phase 가 아닌 값.
67
+ _OPTION_SELECTION_NON_PHASE = {
68
+ "pending-direction-selection": STATUS_PENDING,
69
+ "blocked": STATUS_BLOCKED,
70
+ }
71
+
72
+ # final-verification 의 routing enum 중 phase 이름이 아니라 phase 에 붙은 범위
73
+ # 한정자인 값 → 실제로 실행할 phase. `release-handoff(stage-group)` 은 넘길 stage
74
+ # 묶음을 좁힌다는 뜻이지 다른 phase 가 아니다. 범위는 위저드의 handoff_stage_pick
75
+ # 이 따로 묻는다. 토큰을 그대로 phase 에 두면 `autofill_task_type` 이 그 문자열을
76
+ # 셸에 TASK_TYPE 으로 건네는데, 그런 task-type 은 존재하지 않는다.
77
+ _FINAL_VERIFICATION_PHASE_ALIASES = {
78
+ "release-handoff(stage-group)": "release-handoff",
79
+ }
80
+
81
+
82
+ def make(
83
+ phase: str = "", status: str = STATUS_PENDING, rationale: str = ""
84
+ ) -> dict[str, str]:
85
+ """포인터 구조체 하나를 만든다. status 가 허용 집합 밖이면 거부한다."""
86
+ if status not in POINTER_STATUSES:
87
+ raise ValueError(f"unknown next-phase status: {status!r}")
88
+ return {"phase": phase, "status": status, "rationale": rationale}
89
+
90
+
91
+ def is_pointer(value: Any) -> bool:
92
+ """value 가 유효한 포인터 구조체인지."""
93
+ return (
94
+ isinstance(value, Mapping)
95
+ and isinstance(value.get("phase"), str)
96
+ and value.get("status") in POINTER_STATUSES
97
+ and isinstance(value.get("rationale"), str)
98
+ )
99
+
100
+
101
+ def promote(value: Any) -> dict[str, str]:
102
+ """구형 문자열·None 을 포인터로 올린다.
103
+
104
+ 이미 포인터인 입력은 같은 값을 담은 새 dict 로 돌려준다 — 입력을 그대로
105
+ 돌려주지 않는다. 호출부가 반환값을 그 자리에서 변형해도 manifest 원본이
106
+ 오염되지 않아야 하기 때문이다.
107
+
108
+ pre-v1 무호환 원칙의 의도적 예외다. 근거는 업그레이드를 가로지르는
109
+ 진행 중 태스크가 영구히 막히는 기존 사례 — ADR-0004 Consequences.
110
+ """
111
+ if is_pointer(value):
112
+ return {
113
+ "phase": value["phase"],
114
+ "status": value["status"],
115
+ "rationale": value["rationale"],
116
+ }
117
+ if not isinstance(value, str):
118
+ return make()
119
+ text = value.strip()
120
+ if text in _LEGACY_SENTINELS:
121
+ return make(status=_LEGACY_SENTINELS[text])
122
+ return make(phase=text, status=STATUS_READY)
123
+
124
+
125
+ def autofill_task_type(manifest: Mapping[str, Any]) -> str:
126
+ """매니페스트의 포인터에서 바로 실행 가능한 task-type 을 뽑는다.
127
+
128
+ `ready` 가 아니면 빈 문자열이다. 셸 진입점의 autofill 이 이 함수를 쓴다.
129
+ """
130
+ workflow = manifest.get("workflow")
131
+ if not isinstance(workflow, Mapping):
132
+ return ""
133
+ pointer = promote(workflow.get("nextRecommendedPhase"))
134
+ return pointer["phase"] if pointer["status"] == STATUS_READY else ""
135
+
136
+
137
+ def project(report_data: Mapping[str, Any]) -> dict[str, str]:
138
+ """리포트 data.json 하나를 포인터로 투영한다.
139
+
140
+ 라우팅 판단의 옳고 그름은 다루지 않는다. 리포트가 이미 내린 판단을
141
+ 옮기기만 한다. 규칙표는 이 프로젝트의 구현 계획 문서에 있다.
142
+ """
143
+ header = report_data.get("header")
144
+ task_type = ""
145
+ if isinstance(header, Mapping):
146
+ task_type = str(header.get("taskType") or "")
147
+
148
+ if task_type in _TERMINAL_TASK_TYPES:
149
+ return make(status=STATUS_TERMINAL)
150
+ if task_type in _SIDETRACK_TASK_TYPES:
151
+ return make(status=STATUS_PENDING)
152
+
153
+ if task_type == "requirements-discovery":
154
+ return _from_nested_routing(report_data, "requirementsDiscovery")
155
+ if task_type == "error-analysis":
156
+ return _from_nested_routing(report_data, "errorAnalysis")
157
+ if task_type == "implementation-option-selection":
158
+ return _from_option_selection(report_data)
159
+ if task_type == "implementation-planning":
160
+ return _from_planning(report_data)
161
+ if task_type == "implementation":
162
+ return _from_target(report_data, "implementation")
163
+ if task_type == "final-verification":
164
+ return _from_final_verification(report_data)
165
+ return make(status=STATUS_PENDING)
166
+
167
+
168
+ def _block(report_data: Mapping[str, Any], key: str) -> Mapping[str, Any]:
169
+ block = report_data.get(key)
170
+ return block if isinstance(block, Mapping) else {}
171
+
172
+
173
+ def _from_nested(
174
+ report_data: Mapping[str, Any],
175
+ block_key: str,
176
+ routing_key: str,
177
+ target_key: str,
178
+ ) -> dict[str, str]:
179
+ """블록 → 라우팅 → 대상 phase 로 두 단 내려가 읽는 공용 판독부.
180
+
181
+ 대상이 문자열이 아니면 결측과 똑같이 취급한다. 문자열이 아닌 값을 str() 로
182
+ 강제하면 dict repr 같은 것이 phase 이름 자리에 들어앉고, status 는 ready 가
183
+ 되어 소비자에게 "지금 시작 가능한 phase" 로 읽힌다. 결측은 pending 에서
184
+ 무해하게 멈추지만 이쪽은 존재하지 않는 phase 로 소비자를 보낸다.
185
+ """
186
+ routing = _block(report_data, block_key).get(routing_key)
187
+ target = ""
188
+ if isinstance(routing, Mapping):
189
+ raw = routing.get(target_key)
190
+ target = raw.strip() if isinstance(raw, str) else ""
191
+ if not target:
192
+ return make(status=STATUS_PENDING)
193
+ return make(phase=target, status=STATUS_READY)
194
+
195
+
196
+ def _from_nested_routing(report_data: Mapping[str, Any], key: str) -> dict[str, str]:
197
+ return _from_nested(report_data, key, "routing", "nextTaskType")
198
+
199
+
200
+ def _from_option_selection(report_data: Mapping[str, Any]) -> dict[str, str]:
201
+ # 이 phase 의 routing 은 문자열 enum 이다. 이웃 두 phase 가 쓰는 중첩 dict 가
202
+ # 잘못 들어오면 결측과 똑같이 취급한다 — str() 로 강제하면 dict repr 이 phase
203
+ # 이름이 되어 ready 로 나간다.
204
+ raw = _block(report_data, "implementationOptionSelection").get("routing")
205
+ routing = raw.strip() if isinstance(raw, str) else ""
206
+ if routing in _OPTION_SELECTION_NON_PHASE:
207
+ return make(status=_OPTION_SELECTION_NON_PHASE[routing])
208
+ if not routing:
209
+ return make(status=STATUS_PENDING)
210
+ return make(phase=routing, status=STATUS_READY)
211
+
212
+
213
+ def _from_planning(report_data: Mapping[str, Any]) -> dict[str, str]:
214
+ outcome = str(_block(report_data, "implementationPlanning").get("outcome") or "")
215
+ if outcome == "plan-ready":
216
+ return make(phase="implementation", status=STATUS_READY)
217
+ if outcome == "direction-invalidated":
218
+ return make(phase="implementation-option-selection", status=STATUS_READY)
219
+ return make(status=STATUS_PENDING)
220
+
221
+
222
+ def _from_target(report_data: Mapping[str, Any], key: str) -> dict[str, str]:
223
+ return _from_nested(report_data, key, "routingRecommendation", "target")
224
+
225
+
226
+ def _from_final_verification(report_data: Mapping[str, Any]) -> dict[str, str]:
227
+ pointer = _from_target(report_data, "finalVerification")
228
+ if pointer["phase"] == "done":
229
+ return make(status=STATUS_TERMINAL)
230
+ if pointer["phase"] in _FINAL_VERIFICATION_PHASE_ALIASES:
231
+ return make(
232
+ phase=_FINAL_VERIFICATION_PHASE_ALIASES[pointer["phase"]],
233
+ status=pointer["status"],
234
+ rationale=pointer["rationale"],
235
+ )
236
+ return pointer
@@ -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())