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,571 @@
1
+ """Canonical invocation write policy and truthful runner enforcement."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import subprocess
7
+ from collections.abc import Mapping, Sequence
8
+ from dataclasses import dataclass
9
+ from pathlib import Path, PurePosixPath
10
+ from typing import Any, Literal
11
+
12
+ BoundaryPrecision = Literal["exact-path", "directory-boundary", "none"]
13
+
14
+ _PRECISION_RANK = {"none": 0, "directory-boundary": 1, "exact-path": 2}
15
+ _POLICY_KEYS = {
16
+ "artifactPolicy",
17
+ "sourcePolicy",
18
+ "gitPolicy",
19
+ "auxiliaryPolicy",
20
+ }
21
+ _ENFORCEMENT_KEYS = {
22
+ "boundaryPrecision",
23
+ "mutationAudit",
24
+ "attribution",
25
+ "auditedRoots",
26
+ "observationCoverage",
27
+ "unobservedWriteSurfaces",
28
+ }
29
+
30
+
31
+ class WritePolicyError(ValueError):
32
+ """Raised when a write policy cannot be made canonical or trustworthy."""
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class WritePolicy:
37
+ artifact_policy: Mapping[str, Any]
38
+ source_policy: Mapping[str, Any]
39
+ git_policy: Mapping[str, Any]
40
+ auxiliary_policy: Mapping[str, Any]
41
+ maximum_boundary_precision: BoundaryPrecision = "none"
42
+
43
+ def to_payload(self) -> dict[str, Any]:
44
+ return {
45
+ "artifactPolicy": dict(self.artifact_policy),
46
+ "sourcePolicy": dict(self.source_policy),
47
+ "gitPolicy": dict(self.git_policy),
48
+ "auxiliaryPolicy": dict(self.auxiliary_policy),
49
+ }
50
+
51
+ @property
52
+ def digest(self) -> str:
53
+ return write_policy_digest(self.to_payload())
54
+
55
+ @property
56
+ def source_mode(self) -> str:
57
+ return str(self.source_policy["mode"])
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class WriteEnforcement:
62
+ boundary_precision: BoundaryPrecision
63
+ mutation_audit: Literal["none", "batch"]
64
+ attribution: Literal["call", "batch-unattributed"]
65
+ audited_roots: tuple[Path, ...]
66
+ observation_coverage: Literal["complete", "scoped", "partial"]
67
+ unobserved_write_surfaces: tuple[str, ...]
68
+
69
+ def to_payload(self) -> dict[str, Any]:
70
+ return {
71
+ "boundaryPrecision": self.boundary_precision,
72
+ "mutationAudit": self.mutation_audit,
73
+ "attribution": self.attribution,
74
+ "auditedRoots": [str(path) for path in self.audited_roots],
75
+ "observationCoverage": self.observation_coverage,
76
+ "unobservedWriteSurfaces": list(self.unobserved_write_surfaces),
77
+ }
78
+
79
+
80
+ def build_write_policy(
81
+ invocation: Mapping[str, Any],
82
+ assignment: Mapping[str, Any],
83
+ worktree: Path | None,
84
+ ) -> WritePolicy:
85
+ """Build one provider-neutral policy from invocation and adapter authority."""
86
+ role = _required_text(invocation, "role")
87
+ project_root = Path(_required_text(invocation, "projectRoot")).resolve()
88
+ source_root = _trusted_root(invocation, worktree)
89
+ artifact_paths = _relative_paths(invocation.get("artifactPaths", ()))
90
+ if not artifact_paths:
91
+ raise WritePolicyError("artifactPaths must contain standard worker artifacts")
92
+ source_mode = "project-mutation" if role == "implementer" else "source-readonly"
93
+ source_policy: dict[str, Any] = {
94
+ "mode": source_mode,
95
+ "allowedRoot": str(source_root),
96
+ "plannedPaths": list(_relative_paths(invocation.get("plannedPaths", ()))),
97
+ "protectedPaths": list(
98
+ _relative_paths(invocation.get("protectedPaths", (".okstra", ".git")))
99
+ ),
100
+ }
101
+ git_policy = _git_policy(invocation, source_root, source_mode)
102
+ external_roots = _validated_auxiliary_roots(invocation, assignment)
103
+ auxiliary_policy = {
104
+ "scratchRoots": [
105
+ str(path) for path in _absolute_roots(invocation.get("scratchRoots", ()))
106
+ ],
107
+ "generatedPaths": list(
108
+ _relative_paths(invocation.get("generatedPaths", ()))
109
+ ),
110
+ "externalRoots": [str(path) for path in external_roots],
111
+ }
112
+ policy = WritePolicy(
113
+ artifact_policy={
114
+ "allowedRoot": str(project_root),
115
+ "allowedPaths": list(artifact_paths),
116
+ },
117
+ source_policy=source_policy,
118
+ git_policy=git_policy,
119
+ auxiliary_policy=auxiliary_policy,
120
+ maximum_boundary_precision=_assignment_precision(assignment),
121
+ )
122
+ validate_write_policy_payload(policy.to_payload())
123
+ return policy
124
+
125
+
126
+ def build_invocation_write_contract(
127
+ *,
128
+ role: str,
129
+ project_root: Path,
130
+ worktree: Path | None,
131
+ artifact_paths: Sequence[Path],
132
+ maximum_precision: BoundaryPrecision,
133
+ planned_paths: Sequence[str] = (),
134
+ scratch_roots: Sequence[Path] = (),
135
+ generated_paths: Sequence[str] = (),
136
+ auxiliary_roots: Sequence[Path] = (),
137
+ validated_auxiliary_roots: Sequence[Path] = (),
138
+ ) -> tuple[WritePolicy, WriteEnforcement]:
139
+ """Build the single canonical contract used by every v2 dispatcher."""
140
+ root = project_root.resolve()
141
+ relative_artifacts = tuple(
142
+ _relative_to_root(path, root, "artifact") for path in artifact_paths
143
+ )
144
+ invocation: dict[str, Any] = {
145
+ "role": role,
146
+ "projectRoot": str(root),
147
+ "artifactPaths": list(relative_artifacts),
148
+ "plannedPaths": list(planned_paths),
149
+ "protectedPaths": [".okstra", ".git"],
150
+ "generatedPaths": list(generated_paths),
151
+ "scratchRoots": [str(path) for path in scratch_roots],
152
+ "auxiliaryRoots": [str(path) for path in auxiliary_roots],
153
+ }
154
+ source_root = worktree.resolve() if worktree is not None else root
155
+ if role == "implementer":
156
+ invocation.update({
157
+ "expectedBaseCommit": _git_output(source_root, "rev-parse", "HEAD"),
158
+ "allowedBranchRef": _git_output(
159
+ source_root, "symbolic-ref", "-q", "HEAD"
160
+ ),
161
+ })
162
+ assignment = {
163
+ "workerWriteCapability": {
164
+ "maxBoundaryPrecision": maximum_precision,
165
+ },
166
+ "validatedAuxiliaryRoots": [str(path) for path in validated_auxiliary_roots],
167
+ }
168
+ policy = build_write_policy(invocation, assignment, source_root)
169
+ return policy, derive_write_enforcement(policy, maximum_precision, role=role)
170
+
171
+
172
+ def planned_paths_from_run_manifest(
173
+ project_root: Path, manifest: Mapping[str, Any]
174
+ ) -> tuple[str, ...]:
175
+ active_value = manifest.get("activeRunContextPath")
176
+ if not isinstance(active_value, str) or not active_value:
177
+ raise WritePolicyError("implementer write policy has no active run context")
178
+ active = _read_json(_rooted(project_root, active_value), "active run context")
179
+ source = active.get("sourceArtifacts")
180
+ run_inputs_value = source.get("runInputsPath") if isinstance(source, Mapping) else None
181
+ run = active.get("run")
182
+ stage_value = run.get("stage") if isinstance(run, Mapping) else None
183
+ if not isinstance(run_inputs_value, str) or not str(stage_value).isdigit():
184
+ raise WritePolicyError("implementer write policy has no approved stage authority")
185
+ inputs_payload = _read_json(_rooted(project_root, run_inputs_value), "run inputs")
186
+ inputs = inputs_payload.get("inputs")
187
+ approved = inputs.get("approvedPlanPath") if isinstance(inputs, Mapping) else None
188
+ if not isinstance(approved, str) or not approved:
189
+ raise WritePolicyError("implementer write policy has no approved plan path")
190
+ return _planned_paths_from_report(Path(approved), int(stage_value))
191
+
192
+
193
+ def derive_write_enforcement(
194
+ policy: WritePolicy,
195
+ maximum_precision: BoundaryPrecision,
196
+ *,
197
+ role: str | None = None,
198
+ ) -> WriteEnforcement:
199
+ """Lower runner capability when policy paths cannot be enforced exactly."""
200
+ if role == "leader":
201
+ enforcement = WriteEnforcement(
202
+ boundary_precision="none",
203
+ mutation_audit="none",
204
+ attribution="batch-unattributed",
205
+ audited_roots=_audited_roots(policy),
206
+ observation_coverage="partial",
207
+ unobserved_write_surfaces=(
208
+ "lead-session-host-writes",
209
+ "other-stage-refs",
210
+ "shared-git-object-store",
211
+ ),
212
+ )
213
+ validate_write_enforcement_payload(enforcement.to_payload())
214
+ return enforcement
215
+ if maximum_precision not in _PRECISION_RANK:
216
+ raise WritePolicyError("worker write capability is invalid")
217
+ exact_policy = (
218
+ policy.source_mode == "source-readonly"
219
+ and not policy.auxiliary_policy.get("externalRoots")
220
+ and not policy.auxiliary_policy.get("scratchRoots")
221
+ and not policy.auxiliary_policy.get("generatedPaths")
222
+ )
223
+ if maximum_precision == "exact-path" and exact_policy:
224
+ boundary: BoundaryPrecision = "exact-path"
225
+ elif _PRECISION_RANK[maximum_precision] >= _PRECISION_RANK["directory-boundary"]:
226
+ boundary = "directory-boundary"
227
+ else:
228
+ boundary = "none"
229
+ exact = boundary == "exact-path" and exact_policy
230
+ external_roots = tuple(policy.auxiliary_policy.get("externalRoots", ()))
231
+ unobserved: list[str] = []
232
+ if boundary == "none":
233
+ unobserved.append("provider-process-outside-audited-roots")
234
+ if external_roots:
235
+ unobserved.append("shared-tool-cache")
236
+ if policy.source_mode == "project-mutation":
237
+ unobserved.extend(("other-stage-refs", "shared-git-object-store"))
238
+ coverage: Literal["complete", "scoped", "partial"]
239
+ if exact:
240
+ coverage = "complete"
241
+ elif unobserved:
242
+ coverage = "partial"
243
+ else:
244
+ coverage = "scoped"
245
+ enforcement = WriteEnforcement(
246
+ boundary_precision=boundary,
247
+ mutation_audit="none" if exact else "batch",
248
+ attribution="call" if exact else "batch-unattributed",
249
+ audited_roots=_audited_roots(policy),
250
+ observation_coverage=coverage,
251
+ unobserved_write_surfaces=tuple(unobserved),
252
+ )
253
+ validate_write_enforcement_payload(enforcement.to_payload())
254
+ return enforcement
255
+
256
+
257
+ def write_policy_from_payload(payload: Mapping[str, Any]) -> WritePolicy:
258
+ validate_write_policy_payload(payload)
259
+ return WritePolicy(
260
+ artifact_policy=dict(payload["artifactPolicy"]),
261
+ source_policy=dict(payload["sourcePolicy"]),
262
+ git_policy=dict(payload["gitPolicy"]),
263
+ auxiliary_policy=dict(payload["auxiliaryPolicy"]),
264
+ )
265
+
266
+
267
+ def write_enforcement_from_payload(
268
+ payload: Mapping[str, Any],
269
+ ) -> WriteEnforcement:
270
+ validate_write_enforcement_payload(payload)
271
+ return WriteEnforcement(
272
+ boundary_precision=payload["boundaryPrecision"],
273
+ mutation_audit=payload["mutationAudit"],
274
+ attribution=payload["attribution"],
275
+ audited_roots=tuple(Path(value) for value in payload["auditedRoots"]),
276
+ observation_coverage=payload["observationCoverage"],
277
+ unobserved_write_surfaces=tuple(payload["unobservedWriteSurfaces"]),
278
+ )
279
+
280
+
281
+ def write_policy_digest(payload: Mapping[str, Any]) -> str:
282
+ validate_write_policy_payload(payload)
283
+ encoded = json.dumps(
284
+ payload,
285
+ sort_keys=True,
286
+ separators=(",", ":"),
287
+ ensure_ascii=False,
288
+ ).encode("utf-8")
289
+ return "sha256:" + hashlib.sha256(encoded).hexdigest()
290
+
291
+
292
+ def validate_write_policy_payload(payload: Mapping[str, Any]) -> None:
293
+ if set(payload) != _POLICY_KEYS:
294
+ raise WritePolicyError("writePolicy must contain four canonical policy sections")
295
+ artifact = _mapping(payload.get("artifactPolicy"), "artifactPolicy")
296
+ source = _mapping(payload.get("sourcePolicy"), "sourcePolicy")
297
+ git = _mapping(payload.get("gitPolicy"), "gitPolicy")
298
+ auxiliary = _mapping(payload.get("auxiliaryPolicy"), "auxiliaryPolicy")
299
+ if set(artifact) != {"allowedRoot", "allowedPaths"}:
300
+ raise WritePolicyError("artifactPolicy is incomplete")
301
+ _absolute_text(artifact.get("allowedRoot"), "artifactPolicy.allowedRoot")
302
+ _relative_paths(artifact.get("allowedPaths", ()))
303
+ if source.get("mode") not in {"source-readonly", "project-mutation"}:
304
+ raise WritePolicyError("sourcePolicy mode is invalid")
305
+ if set(source) != {"mode", "allowedRoot", "plannedPaths", "protectedPaths"}:
306
+ raise WritePolicyError("sourcePolicy is incomplete")
307
+ _absolute_text(source.get("allowedRoot"), "sourcePolicy.allowedRoot")
308
+ _relative_paths(source.get("plannedPaths", ()))
309
+ _relative_paths(source.get("protectedPaths", ()))
310
+ if git.get("mode") not in {"disabled", "fast-forward-descendant-chain"}:
311
+ raise WritePolicyError("gitPolicy mode is invalid")
312
+ if git.get("mode") == "disabled" and set(git) != {"mode"}:
313
+ raise WritePolicyError("disabled gitPolicy must not grant Git paths")
314
+ required_git = {
315
+ "mode", "worktreeGitDir", "gitCommonDir", "expectedBaseCommit",
316
+ "allowedBranchRef", "commitMode",
317
+ }
318
+ if git.get("mode") != "disabled" and set(git) != required_git:
319
+ raise WritePolicyError("project mutation gitPolicy is incomplete")
320
+ if set(auxiliary) != {"scratchRoots", "generatedPaths", "externalRoots"}:
321
+ raise WritePolicyError("auxiliaryPolicy is incomplete")
322
+ _relative_paths(auxiliary.get("generatedPaths", ()))
323
+ for key in ("scratchRoots", "externalRoots"):
324
+ for value in _text_sequence(auxiliary.get(key), f"auxiliaryPolicy.{key}"):
325
+ _absolute_text(value, f"auxiliaryPolicy.{key}")
326
+
327
+
328
+ def validate_write_enforcement_payload(payload: Mapping[str, Any]) -> None:
329
+ if set(payload) != _ENFORCEMENT_KEYS:
330
+ raise WritePolicyError("writeEnforcement is incomplete")
331
+ boundary = payload.get("boundaryPrecision")
332
+ mutation_audit = payload.get("mutationAudit")
333
+ attribution = payload.get("attribution")
334
+ coverage = payload.get("observationCoverage")
335
+ if boundary not in _PRECISION_RANK:
336
+ raise WritePolicyError("writeEnforcement boundaryPrecision is invalid")
337
+ if mutation_audit not in {"none", "batch"}:
338
+ raise WritePolicyError("writeEnforcement mutationAudit is invalid")
339
+ if attribution not in {"call", "batch-unattributed"}:
340
+ raise WritePolicyError("writeEnforcement attribution is invalid")
341
+ if coverage not in {"complete", "scoped", "partial"}:
342
+ raise WritePolicyError("writeEnforcement observationCoverage is invalid")
343
+ if (boundary, mutation_audit, attribution) == ("exact-path", "none", "call"):
344
+ if coverage != "complete":
345
+ raise WritePolicyError("exact enforcement must have complete coverage")
346
+ elif (boundary, mutation_audit, attribution) == (
347
+ "none",
348
+ "none",
349
+ "batch-unattributed",
350
+ ):
351
+ if coverage != "partial":
352
+ raise WritePolicyError("host-session enforcement must be partial")
353
+ elif mutation_audit != "batch" or attribution != "batch-unattributed":
354
+ raise WritePolicyError("non-exact enforcement must use batch attribution")
355
+ elif boundary == "exact-path" or coverage == "complete":
356
+ raise WritePolicyError("batch enforcement cannot claim exact coverage")
357
+ for root in _text_sequence(payload.get("auditedRoots"), "auditedRoots"):
358
+ _absolute_text(root, "auditedRoots")
359
+ _text_sequence(payload.get("unobservedWriteSurfaces"), "unobservedWriteSurfaces")
360
+
361
+
362
+ def _git_policy(
363
+ invocation: Mapping[str, Any], root: Path, source_mode: str
364
+ ) -> dict[str, Any]:
365
+ if source_mode == "source-readonly":
366
+ return {"mode": "disabled"}
367
+ git_dir = _git_path(root, "--git-dir")
368
+ common_dir = _git_path(root, "--git-common-dir")
369
+ expected_base = _required_text(invocation, "expectedBaseCommit")
370
+ allowed_ref = _required_text(invocation, "allowedBranchRef")
371
+ _require_commit(root, expected_base)
372
+ current_ref = _git_output(root, "symbolic-ref", "-q", "HEAD")
373
+ if current_ref != allowed_ref or not allowed_ref.startswith("refs/heads/"):
374
+ raise WritePolicyError("allowedBranchRef does not match assigned worktree")
375
+ return {
376
+ "mode": "fast-forward-descendant-chain",
377
+ "worktreeGitDir": str(git_dir),
378
+ "gitCommonDir": str(common_dir),
379
+ "expectedBaseCommit": expected_base,
380
+ "allowedBranchRef": allowed_ref,
381
+ "commitMode": "fast-forward-descendant-chain",
382
+ }
383
+
384
+
385
+ def _audited_roots(policy: WritePolicy) -> tuple[Path, ...]:
386
+ values = [
387
+ Path(str(policy.source_policy["allowedRoot"])),
388
+ Path(str(policy.artifact_policy["allowedRoot"])),
389
+ ]
390
+ values.extend(Path(value) for value in policy.auxiliary_policy.get("scratchRoots", ()))
391
+ git_dir = policy.git_policy.get("worktreeGitDir")
392
+ if isinstance(git_dir, str) and git_dir:
393
+ values.append(Path(git_dir))
394
+ seen: set[Path] = set()
395
+ roots: list[Path] = []
396
+ for value in values:
397
+ resolved = value.resolve()
398
+ if resolved not in seen:
399
+ roots.append(resolved)
400
+ seen.add(resolved)
401
+ return tuple(roots)
402
+
403
+
404
+ def _trusted_root(invocation: Mapping[str, Any], worktree: Path | None) -> Path:
405
+ raw = worktree if worktree is not None else Path(_required_text(invocation, "projectRoot"))
406
+ if not raw.is_absolute() or not raw.is_dir():
407
+ raise WritePolicyError("assigned worktree must be an existing absolute path")
408
+ return raw.resolve()
409
+
410
+
411
+ def _validated_auxiliary_roots(
412
+ invocation: Mapping[str, Any], assignment: Mapping[str, Any]
413
+ ) -> tuple[Path, ...]:
414
+ requested = _absolute_roots(invocation.get("auxiliaryRoots", ()))
415
+ declared = set(_absolute_roots(assignment.get("validatedAuxiliaryRoots", ())))
416
+ if any(path not in declared for path in requested):
417
+ raise WritePolicyError("auxiliary root is not adapter-validated")
418
+ return requested
419
+
420
+
421
+ def _assignment_precision(assignment: Mapping[str, Any]) -> BoundaryPrecision:
422
+ capability = assignment.get("workerWriteCapability")
423
+ if not isinstance(capability, Mapping):
424
+ raise WritePolicyError("workerWriteCapability is required")
425
+ value = capability.get("maxBoundaryPrecision")
426
+ if value not in _PRECISION_RANK:
427
+ raise WritePolicyError("workerWriteCapability is invalid")
428
+ return value
429
+
430
+
431
+ def _absolute_roots(value: object) -> tuple[Path, ...]:
432
+ roots: list[Path] = []
433
+ for text in _text_sequence(value, "absolute roots"):
434
+ path = Path(text)
435
+ if not path.is_absolute() or not path.exists():
436
+ raise WritePolicyError("auxiliary root must be an existing absolute path")
437
+ if path.absolute() != path.resolve():
438
+ raise WritePolicyError("auxiliary root contains a symlink component")
439
+ roots.append(path.resolve())
440
+ if len(set(roots)) != len(roots):
441
+ raise WritePolicyError("auxiliary roots must be unique")
442
+ return tuple(roots)
443
+
444
+
445
+ def _relative_to_root(path: Path, root: Path, label: str) -> str:
446
+ absolute = path if path.is_absolute() else root / path
447
+ try:
448
+ relative = absolute.absolute().relative_to(root).as_posix()
449
+ except ValueError as exc:
450
+ raise WritePolicyError(f"{label} path is outside project root") from exc
451
+ if not relative or relative == ".":
452
+ raise WritePolicyError(f"{label} path must name a file")
453
+ current = root
454
+ for part in PurePosixPath(relative).parts:
455
+ current /= part
456
+ if current.is_symlink():
457
+ raise WritePolicyError(f"{label} path contains a symlink component")
458
+ return relative
459
+
460
+
461
+ def _planned_paths_from_report(report_path: Path, stage: int) -> tuple[str, ...]:
462
+ payload = _read_json(report_path.with_suffix(".data.json"), "approved plan data")
463
+ planning = payload.get("implementationPlanning")
464
+ stages = planning.get("stages") if isinstance(planning, Mapping) else None
465
+ selected = next(
466
+ (
467
+ row for row in stages
468
+ if isinstance(row, Mapping) and row.get("stage") == stage
469
+ ),
470
+ None,
471
+ ) if isinstance(stages, list) else None
472
+ steps = selected.get("stepwiseExecution") if isinstance(selected, Mapping) else None
473
+ paths = {
474
+ part.strip().strip("`")
475
+ for step in steps if isinstance(step, Mapping)
476
+ for part in str(step.get("files") or "").replace("<br>", ",").split(",")
477
+ if part.strip()
478
+ } if isinstance(steps, list) else set()
479
+ if not paths:
480
+ raise WritePolicyError("implementer write policy stage has no planned paths")
481
+ return _relative_paths(tuple(sorted(paths)))
482
+
483
+
484
+ def _rooted(project_root: Path, value: str) -> Path:
485
+ path = Path(value)
486
+ return path if path.is_absolute() else project_root / path
487
+
488
+
489
+ def _read_json(path: Path, label: str) -> Mapping[str, Any]:
490
+ try:
491
+ value = json.loads(path.read_text(encoding="utf-8"))
492
+ except (OSError, UnicodeError, json.JSONDecodeError) as exc:
493
+ raise WritePolicyError(f"{label} is invalid: {path}") from exc
494
+ if not isinstance(value, Mapping):
495
+ raise WritePolicyError(f"{label} must be an object: {path}")
496
+ return value
497
+
498
+
499
+ def _relative_paths(value: object) -> tuple[str, ...]:
500
+ paths: list[str] = []
501
+ for raw in _text_sequence(value, "relative paths"):
502
+ candidate = PurePosixPath(raw)
503
+ if candidate.is_absolute() or not candidate.parts or ".." in candidate.parts:
504
+ raise WritePolicyError("policy paths must be normalized relative paths")
505
+ normalized = candidate.as_posix()
506
+ if normalized in {"", "."} or normalized != raw:
507
+ raise WritePolicyError("policy paths must be normalized relative paths")
508
+ paths.append(normalized)
509
+ if len(set(paths)) != len(paths):
510
+ raise WritePolicyError("policy paths must be unique")
511
+ return tuple(paths)
512
+
513
+
514
+ def _mapping(value: object, label: str) -> Mapping[str, Any]:
515
+ if not isinstance(value, Mapping):
516
+ raise WritePolicyError(f"{label} must be an object")
517
+ return value
518
+
519
+
520
+ def _text_sequence(value: object, label: str) -> tuple[str, ...]:
521
+ if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
522
+ raise WritePolicyError(f"{label} must be an array")
523
+ rows = tuple(value)
524
+ if any(not isinstance(row, str) or not row for row in rows):
525
+ raise WritePolicyError(f"{label} must contain non-empty strings")
526
+ return rows
527
+
528
+
529
+ def _required_text(payload: Mapping[str, Any], key: str) -> str:
530
+ value = payload.get(key)
531
+ if not isinstance(value, str) or not value.strip():
532
+ raise WritePolicyError(f"{key} must be a non-empty string")
533
+ return value
534
+
535
+
536
+ def _absolute_text(value: object, label: str) -> str:
537
+ if not isinstance(value, str) or not value or not Path(value).is_absolute():
538
+ raise WritePolicyError(f"{label} must be an absolute path")
539
+ return value
540
+
541
+
542
+ def _git_path(root: Path, flag: str) -> Path:
543
+ raw = _git_output(root, "rev-parse", "--path-format=absolute", flag)
544
+ path = Path(raw).resolve()
545
+ if not path.exists():
546
+ raise WritePolicyError(f"Git projection path does not exist: {path}")
547
+ return path
548
+
549
+
550
+ def _require_commit(root: Path, commit: str) -> None:
551
+ result = subprocess.run(
552
+ ["git", "-C", str(root), "cat-file", "-e", f"{commit}^{{commit}}"],
553
+ capture_output=True,
554
+ text=True,
555
+ check=False,
556
+ )
557
+ if result.returncode != 0:
558
+ raise WritePolicyError(result.stderr.strip() or "expected base is not a commit")
559
+
560
+
561
+ def _git_output(root: Path, *args: str) -> str:
562
+ result = subprocess.run(
563
+ ["git", "-C", str(root), *args],
564
+ capture_output=True,
565
+ text=True,
566
+ check=False,
567
+ )
568
+ if result.returncode != 0 or not result.stdout.strip():
569
+ detail = result.stderr.strip() or "Git command returned no value"
570
+ raise WritePolicyError(detail)
571
+ return result.stdout.strip()
@@ -53,6 +53,20 @@ def _load_json(path: Path) -> Optional[dict]:
53
53
  raise StateError(f"failed to parse {path}: {exc}") from exc
54
54
 
55
55
 
56
+ def _next_phase():
57
+ """`okstra_ctl.next_phase` 를 호출 시점에 가져온다.
58
+
59
+ 모듈 최상단에서 import 하면 순환이 된다 — `okstra_ctl/__init__.py` 가
60
+ `.ids` 를 거쳐 이 모듈의 `slugify` 를 도로 import 하기 때문에, 여기서
61
+ okstra_ctl 을 먼저 끌어오면 아직 정의되지 않은 이름을 찾다가 ImportError 로
62
+ 죽는다. 같은 파일의 `_reconcile_task_root_best_effort` 도 같은 이유로 지연
63
+ import 를 쓴다.
64
+ """
65
+ from okstra_ctl import next_phase
66
+
67
+ return next_phase
68
+
69
+
56
70
  def slugify(value: str) -> str:
57
71
  """task-group / task-id segment 를 디스크 경로용 slug 로 정규화한다.
58
72
 
@@ -83,8 +97,32 @@ def read_latest_task(project_root: Path) -> Optional[dict]:
83
97
 
84
98
 
85
99
  def read_task_manifest(task_root: Path) -> Optional[dict]:
86
- """<task-root>/task-manifest.json 을 dict 로. 파일 없으면 None."""
87
- return _load_json(Path(task_root) / TASK_MANIFEST_FILENAME)
100
+ """<task-root>/task-manifest.json 을 dict 로. 파일 없으면 None.
101
+
102
+ 구형 문자열 포인터는 반환값에서 승격한다. 읽는 곳이 여럿이므로 승격을 seam
103
+ 하나에 두지 않으면 형태가 갈라진다 (ADR-0004).
104
+
105
+ **디스크는 건드리지 않는다.** 승격한 값을 여기서 되적으면 lock 없는
106
+ read-modify-write 가 되어, 파싱과 쓰기 사이에 다른 프로세스가 적은
107
+ `currentPhase` · `phaseStates` · `latestReportPath` 를 되돌린다
108
+ (`os.replace` 는 찢어진 읽기만 막고 갱신 유실은 못 막는다). 이 분기는
109
+ 디스크 포인터가 아직 문자열일 때 — 즉 업그레이드 직후 진행 중인 task 가
110
+ 살아 있을 때 — 만 발화하므로 되돌릴 수 있는 값이 바로 phase 진행 상태다.
111
+ 그래서 디스크 형태는 다음 writer 가 손댈 때까지 구형으로 남는다. 모든
112
+ 읽기가 여기서 승격하므로 소비자는 그 차이를 보지 않는다.
113
+ """
114
+ manifest = _load_json(Path(task_root) / TASK_MANIFEST_FILENAME)
115
+ if not isinstance(manifest, dict):
116
+ return manifest
117
+ workflow = manifest.get("workflow")
118
+ if not isinstance(workflow, dict):
119
+ return manifest
120
+ workflow["nextRecommendedPhase"] = _next_phase().promote(
121
+ workflow.get("nextRecommendedPhase")
122
+ )
123
+ # routingStatus 는 포인터 status 로 흡수됐다 — 승격과 같은 자리에서 지운다.
124
+ workflow.pop("routingStatus", None)
125
+ return manifest
88
126
 
89
127
 
90
128
  def read_task_key(task_root: Path) -> str:
@@ -146,11 +184,18 @@ def _entry_with_manifest_state(entry: dict, task_root: Path) -> dict:
146
184
  ("currentPhase", "currentPhase"),
147
185
  ("currentPhaseState", "currentPhaseState"),
148
186
  ("lastCompletedPhase", "lastCompletedPhase"),
149
- ("nextRecommendedPhase", "nextRecommendedPhase"),
150
187
  ):
151
188
  value = workflow.get(source)
152
189
  if isinstance(value, str):
153
190
  out[target] = value
191
+ # 포인터는 구조체라 위 루프의 str 검사에 걸리지 않는다 — 그 검사에 남겨두면
192
+ # 값이 예외도 로그도 없이 버려진다. 매니페스트가 값을 가질 때만 카탈로그
193
+ # 항목을 덮는 규칙은 위 세 필드와 같게 유지하고, 덮지 않을 때는 카탈로그에
194
+ # 남아 있던 구형 문자열을 승격해 내보낸다.
195
+ raw = workflow.get("nextRecommendedPhase")
196
+ if raw is None:
197
+ raw = entry.get("nextRecommendedPhase")
198
+ out["nextRecommendedPhase"] = _next_phase().promote(raw)
154
199
  out.update(derived_task_status(manifest))
155
200
  return out
156
201
 
@@ -334,7 +379,9 @@ def resolve_task_identity(project_root: Path, task_key: str) -> dict:
334
379
  "taskType": manifest.get("taskType") or "",
335
380
  "currentPhase": workflow.get("currentPhase") or "",
336
381
  "currentPhaseState": workflow.get("currentPhaseState") or "",
337
- "nextRecommendedPhase": workflow.get("nextRecommendedPhase") or "",
382
+ "nextRecommendedPhase": _next_phase().promote(
383
+ workflow.get("nextRecommendedPhase")
384
+ ),
338
385
  "lastCompletedPhase": workflow.get("lastCompletedPhase") or "",
339
386
  "awaitingApproval": bool(workflow.get("awaitingApproval", False)),
340
387
  "taskBriefPath": manifest.get("taskBriefPath") or "",
@@ -363,8 +410,9 @@ def task_read_side_snapshot(project_root: Path, task_key: str) -> dict:
363
410
  "currentPhase": workflow.get("currentPhase"),
364
411
  "currentPhaseState": workflow.get("currentPhaseState"),
365
412
  "lastCompletedPhase": workflow.get("lastCompletedPhase"),
366
- "nextRecommendedPhase": workflow.get("nextRecommendedPhase"),
367
- "routingStatus": workflow.get("routingStatus"),
413
+ "nextRecommendedPhase": _next_phase().promote(
414
+ workflow.get("nextRecommendedPhase")
415
+ ),
368
416
  "phaseStates": workflow.get("phaseStates"),
369
417
  },
370
418
  "status": derived_task_status(manifest),