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,619 @@
1
+ """Batch-scoped source and Git mutation audit for worker invocations."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import os
7
+ import subprocess
8
+ from collections.abc import Mapping, Sequence
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from .write_policy import WritePolicy
14
+
15
+
16
+ class MutationAuditError(ValueError):
17
+ """Raised before dispatch when policies cannot share one audit batch."""
18
+
19
+
20
+ def assert_compatible_batch(policies: Sequence[WritePolicy]) -> None:
21
+ """Reject a dispatch batch before any attempt is recorded."""
22
+ _validate_batch(tuple(policies))
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class MutationSnapshot:
27
+ root: Path
28
+ artifact_root: Path
29
+ policy_digests: tuple[str, ...]
30
+ file_digests: Mapping[str, str]
31
+ artifact_digests: Mapping[str, str]
32
+ scratch_digests: Mapping[str, str]
33
+ git_projection: Mapping[str, Any]
34
+ digest: str
35
+ orchestrator_paths: tuple[str, ...] = ()
36
+
37
+ def to_payload(self) -> dict[str, Any]:
38
+ return {
39
+ "root": str(self.root),
40
+ "artifactRoot": str(self.artifact_root),
41
+ "policyDigests": list(self.policy_digests),
42
+ "fileDigests": dict(self.file_digests),
43
+ "artifactDigests": dict(self.artifact_digests),
44
+ "scratchDigests": dict(self.scratch_digests),
45
+ "gitProjection": dict(self.git_projection),
46
+ "digest": self.digest,
47
+ "orchestratorPaths": list(self.orchestrator_paths),
48
+ }
49
+
50
+ @classmethod
51
+ def from_payload(cls, payload: Mapping[str, Any]) -> "MutationSnapshot":
52
+ snapshot = cls(
53
+ root=Path(str(payload["root"])),
54
+ artifact_root=Path(str(payload["artifactRoot"])),
55
+ policy_digests=tuple(payload["policyDigests"]),
56
+ file_digests=dict(payload["fileDigests"]),
57
+ artifact_digests=dict(payload["artifactDigests"]),
58
+ scratch_digests=dict(payload["scratchDigests"]),
59
+ git_projection=dict(payload["gitProjection"]),
60
+ digest=str(payload["digest"]),
61
+ orchestrator_paths=tuple(payload.get("orchestratorPaths", ())),
62
+ )
63
+ expected = _snapshot_digest(
64
+ snapshot.root,
65
+ snapshot.artifact_root,
66
+ snapshot.policy_digests,
67
+ snapshot.file_digests,
68
+ snapshot.artifact_digests,
69
+ snapshot.scratch_digests,
70
+ snapshot.git_projection,
71
+ snapshot.orchestrator_paths,
72
+ )
73
+ if snapshot.digest != expected:
74
+ raise MutationAuditError("mutation snapshot digest does not match payload")
75
+ return snapshot
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class MutationAuditResult:
80
+ status: str
81
+ changed_paths: tuple[str, ...]
82
+ source_changed: bool
83
+ git_changed: bool
84
+ attribution: str
85
+ retry_allowed: bool
86
+ violations: tuple[str, ...]
87
+ before_digest: str
88
+ after_digest: str
89
+ git_projection: Mapping[str, Any]
90
+
91
+ def change_summary(self) -> dict[str, Any]:
92
+ return {
93
+ "mutationStatus": self.status,
94
+ "changedPaths": list(self.changed_paths),
95
+ "sourceChanged": self.source_changed,
96
+ "gitChanged": self.git_changed,
97
+ "attribution": self.attribution,
98
+ "retryAllowed": self.retry_allowed,
99
+ "violations": list(self.violations),
100
+ "beforeDigest": self.before_digest,
101
+ "afterDigest": self.after_digest,
102
+ }
103
+
104
+
105
+ class ExecutionMutationAudit:
106
+ """Capture and compare one invocation batch without global state."""
107
+
108
+ def snapshot(
109
+ self,
110
+ policies: Sequence[WritePolicy],
111
+ *,
112
+ orchestrator_paths: Sequence[Path] = (),
113
+ ) -> MutationSnapshot:
114
+ rows = tuple(policies)
115
+ root, artifact_root = _validate_batch(rows)
116
+ generated = _generated_paths(rows)
117
+ file_digests = _content_snapshot(root, generated)
118
+ artifact_digests = (
119
+ file_digests
120
+ if artifact_root == root
121
+ else _content_snapshot(artifact_root, generated)
122
+ )
123
+ scratch_digests = _scratch_snapshot(rows)
124
+ git_projection = _git_projection(root)
125
+ orchestrator = tuple(
126
+ sorted(str(path.resolve()) for path in orchestrator_paths)
127
+ )
128
+ policy_digests = tuple(policy.digest for policy in rows)
129
+ digest = _snapshot_digest(
130
+ root,
131
+ artifact_root,
132
+ policy_digests,
133
+ file_digests,
134
+ artifact_digests,
135
+ scratch_digests,
136
+ git_projection,
137
+ orchestrator,
138
+ )
139
+ return MutationSnapshot(
140
+ root=root,
141
+ artifact_root=artifact_root,
142
+ policy_digests=policy_digests,
143
+ file_digests=file_digests,
144
+ artifact_digests=artifact_digests,
145
+ scratch_digests=scratch_digests,
146
+ git_projection=git_projection,
147
+ digest=digest,
148
+ orchestrator_paths=orchestrator,
149
+ )
150
+
151
+ def compare(
152
+ self,
153
+ before: MutationSnapshot,
154
+ policies: Sequence[WritePolicy],
155
+ *,
156
+ out_of_plan_edits: Sequence[str] = (),
157
+ attempt_succeeded: bool = True,
158
+ result_present: bool = True,
159
+ ) -> MutationAuditResult:
160
+ rows = tuple(policies)
161
+ after = self.snapshot(
162
+ rows,
163
+ orchestrator_paths=tuple(Path(path) for path in before.orchestrator_paths),
164
+ )
165
+ _validate_snapshot_authority(before, after, rows)
166
+ changed = _changed_keys(before.file_digests, after.file_digests)
167
+ artifact_changed = _changed_keys(
168
+ before.artifact_digests, after.artifact_digests
169
+ )
170
+ scratch_changed = _changed_keys(
171
+ before.scratch_digests, after.scratch_digests
172
+ )
173
+ source_changes = _source_changes(changed, rows, before)
174
+ git_changed = before.git_projection != after.git_projection
175
+ violations = _policy_violations(
176
+ before,
177
+ after,
178
+ rows,
179
+ source_changes,
180
+ out_of_plan_edits,
181
+ )
182
+ violations.extend(
183
+ _artifact_policy_failures(before, rows, artifact_changed)
184
+ )
185
+ status = _terminal_status(
186
+ rows,
187
+ source_changed=bool(source_changes),
188
+ git_changed=git_changed,
189
+ attempt_succeeded=attempt_succeeded,
190
+ result_present=result_present,
191
+ violations=violations,
192
+ )
193
+ retry_allowed = _retry_allowed(
194
+ status, bool(source_changes), git_changed, bool(scratch_changed)
195
+ )
196
+ return MutationAuditResult(
197
+ status=status,
198
+ changed_paths=tuple(sorted(source_changes)),
199
+ source_changed=bool(source_changes),
200
+ git_changed=git_changed,
201
+ attribution="batch-unattributed",
202
+ retry_allowed=retry_allowed,
203
+ violations=tuple(violations),
204
+ before_digest=before.digest,
205
+ after_digest=after.digest,
206
+ git_projection=after.git_projection,
207
+ )
208
+
209
+
210
+ def _validate_batch(policies: tuple[WritePolicy, ...]) -> tuple[Path, Path]:
211
+ if not policies:
212
+ raise MutationAuditError("mutation audit requires at least one policy")
213
+ roots = {Path(str(row.source_policy["allowedRoot"])).resolve() for row in policies}
214
+ artifact_roots = {
215
+ Path(str(row.artifact_policy["allowedRoot"])).resolve()
216
+ for row in policies
217
+ }
218
+ if len(roots) != 1:
219
+ raise MutationAuditError("one mutation batch must use one assigned worktree")
220
+ if len(artifact_roots) != 1:
221
+ raise MutationAuditError("one mutation batch must use one artifact root")
222
+ mutation = any(row.source_mode == "project-mutation" for row in policies)
223
+ non_exact_readonly = any(
224
+ row.source_mode == "source-readonly"
225
+ and _maximum_precision(row) != "exact-path"
226
+ for row in policies
227
+ )
228
+ if mutation and non_exact_readonly:
229
+ raise MutationAuditError(
230
+ "project mutation and non-exact readonly calls must run in separate batches"
231
+ )
232
+ return next(iter(roots)), next(iter(artifact_roots))
233
+
234
+
235
+ def _validate_snapshot_authority(
236
+ before: MutationSnapshot,
237
+ after: MutationSnapshot,
238
+ policies: tuple[WritePolicy, ...],
239
+ ) -> None:
240
+ expected = tuple(policy.digest for policy in policies)
241
+ if (
242
+ before.root != after.root
243
+ or before.artifact_root != after.artifact_root
244
+ or before.policy_digests != expected
245
+ ):
246
+ raise MutationAuditError("mutation snapshot does not match batch policies")
247
+
248
+
249
+ def _maximum_precision(policy: WritePolicy) -> str:
250
+ return policy.maximum_boundary_precision
251
+
252
+
253
+ def _content_snapshot(root: Path, generated: frozenset[str]) -> dict[str, str]:
254
+ rows: dict[str, str] = {}
255
+ for current, directories, filenames in os.walk(root, followlinks=False):
256
+ current_path = Path(current)
257
+ traversable: list[str] = []
258
+ for name in sorted(directories):
259
+ path = current_path / name
260
+ relative_path = path.relative_to(root)
261
+ if name == ".git" or _excluded(relative_path, generated):
262
+ continue
263
+ if path.is_symlink():
264
+ rows[relative_path.as_posix()] = _path_digest(path)
265
+ continue
266
+ traversable.append(name)
267
+ directories[:] = traversable
268
+ for name in sorted(filenames):
269
+ path = current_path / name
270
+ relative = path.relative_to(root).as_posix()
271
+ if relative == ".git" or _excluded(Path(relative), generated):
272
+ continue
273
+ rows[relative] = _path_digest(path)
274
+ return rows
275
+
276
+
277
+ def _scratch_snapshot(policies: Sequence[WritePolicy]) -> dict[str, str]:
278
+ roots = {
279
+ Path(value)
280
+ for policy in policies
281
+ for value in policy.auxiliary_policy.get("scratchRoots", ())
282
+ }
283
+ rows: dict[str, str] = {}
284
+ for root in sorted(roots):
285
+ if not root.exists():
286
+ continue
287
+ if root.is_file() or root.is_symlink():
288
+ rows[str(root)] = _path_digest(root)
289
+ continue
290
+ for path in sorted(item for item in root.rglob("*") if not item.is_dir()):
291
+ rows[str(path)] = _path_digest(path)
292
+ return rows
293
+
294
+
295
+ def _path_digest(path: Path) -> str:
296
+ if path.is_symlink():
297
+ value = b"symlink:" + os.readlink(path).encode("utf-8")
298
+ else:
299
+ value = path.read_bytes()
300
+ return "sha256:" + hashlib.sha256(value).hexdigest()
301
+
302
+
303
+ def _excluded(path: Path, generated: frozenset[str]) -> bool:
304
+ relative = path.as_posix()
305
+ return any(relative == item or relative.startswith(item + "/") for item in generated)
306
+
307
+
308
+ def _generated_paths(policies: Sequence[WritePolicy]) -> frozenset[str]:
309
+ return frozenset(
310
+ path
311
+ for policy in policies
312
+ for path in policy.auxiliary_policy.get("generatedPaths", ())
313
+ )
314
+
315
+
316
+ def _git_projection(root: Path) -> dict[str, Any]:
317
+ probe = subprocess.run(
318
+ ["git", "-C", str(root), "rev-parse", "--is-inside-work-tree"],
319
+ capture_output=True,
320
+ text=True,
321
+ check=False,
322
+ )
323
+ if probe.returncode != 0:
324
+ return {"available": False}
325
+ git_dir = Path(_git_output(root, "rev-parse", "--path-format=absolute", "--git-dir"))
326
+ common_dir = Path(
327
+ _git_output(root, "rev-parse", "--path-format=absolute", "--git-common-dir")
328
+ )
329
+ branch_ref = _git_output(root, "symbolic-ref", "-q", "HEAD")
330
+ head = _git_output(root, "rev-parse", "HEAD")
331
+ index_path = Path(_git_output(root, "rev-parse", "--path-format=absolute", "--git-path", "index"))
332
+ return {
333
+ "available": True,
334
+ "head": head,
335
+ "allowedBranchRef": branch_ref,
336
+ "branchCommit": _git_output(root, "rev-parse", branch_ref),
337
+ "gitDir": str(git_dir.resolve()),
338
+ "gitCommonDir": str(common_dir.resolve()),
339
+ "indexDigest": _optional_file_digest(index_path),
340
+ "stagedPaths": sorted(_git_changed_paths(root, "--cached")),
341
+ "worktreeRegistration": _worktree_registration(root),
342
+ "reflogDigest": _reflog_digest(root),
343
+ }
344
+
345
+
346
+ def _worktree_registration(root: Path) -> str:
347
+ output = _git_output(root, "worktree", "list", "--porcelain")
348
+ blocks = output.split("\n\n")
349
+ prefix = f"worktree {root.resolve()}\n"
350
+ block = next((item for item in blocks if (item + "\n").startswith(prefix)), "")
351
+ stable_lines = [
352
+ line for line in block.splitlines()
353
+ if not line.startswith("HEAD ")
354
+ ]
355
+ return "\n".join(stable_lines)
356
+
357
+
358
+ def _optional_file_digest(path: Path) -> str | None:
359
+ return _path_digest(path) if path.is_file() else None
360
+
361
+
362
+ def _git_output(root: Path, *args: str) -> str:
363
+ result = subprocess.run(
364
+ ["git", "-C", str(root), *args],
365
+ capture_output=True,
366
+ text=True,
367
+ check=False,
368
+ )
369
+ if result.returncode != 0:
370
+ raise MutationAuditError(result.stderr.strip() or "Git projection failed")
371
+ return result.stdout.strip()
372
+
373
+
374
+ def _snapshot_digest(
375
+ root: Path,
376
+ artifact_root: Path,
377
+ policy_digests: Sequence[str],
378
+ files: Mapping[str, str],
379
+ artifacts: Mapping[str, str],
380
+ scratch: Mapping[str, str],
381
+ git_projection: Mapping[str, Any],
382
+ orchestrator_paths: Sequence[str],
383
+ ) -> str:
384
+ encoded = json.dumps(
385
+ {
386
+ "root": str(root),
387
+ "artifactRoot": str(artifact_root),
388
+ "policyDigests": list(policy_digests),
389
+ "files": files,
390
+ "artifacts": artifacts,
391
+ "scratch": scratch,
392
+ "gitProjection": git_projection,
393
+ "orchestratorPaths": list(orchestrator_paths),
394
+ },
395
+ sort_keys=True,
396
+ separators=(",", ":"),
397
+ ).encode("utf-8")
398
+ return "sha256:" + hashlib.sha256(encoded).hexdigest()
399
+
400
+
401
+ def _changed_keys(before: Mapping[str, str], after: Mapping[str, str]) -> set[str]:
402
+ return {
403
+ key for key in set(before) | set(after) if before.get(key) != after.get(key)
404
+ }
405
+
406
+
407
+ def _source_changes(
408
+ changed: set[str],
409
+ policies: Sequence[WritePolicy],
410
+ snapshot: MutationSnapshot,
411
+ ) -> set[str]:
412
+ artifacts = _allowed_artifact_paths(policies)
413
+ generated = _generated_paths(policies)
414
+ orchestrator = {
415
+ Path(value).relative_to(snapshot.root).as_posix()
416
+ for value in snapshot.orchestrator_paths
417
+ if _is_relative_to(Path(value), snapshot.root)
418
+ }
419
+ return {
420
+ path for path in changed
421
+ if not (
422
+ snapshot.artifact_root == snapshot.root
423
+ and any(_is_within(path, item) for item in artifacts)
424
+ )
425
+ and path not in orchestrator
426
+ and not _excluded(Path(path), generated)
427
+ }
428
+
429
+
430
+ def _artifact_policy_failures(
431
+ snapshot: MutationSnapshot,
432
+ policies: Sequence[WritePolicy],
433
+ changed: set[str],
434
+ ) -> list[str]:
435
+ allowed = _allowed_artifact_paths(policies)
436
+ orchestrator = {
437
+ Path(value).relative_to(snapshot.artifact_root).as_posix()
438
+ for value in snapshot.orchestrator_paths
439
+ if _is_relative_to(Path(value), snapshot.artifact_root)
440
+ }
441
+ unauthorized = {
442
+ path for path in changed
443
+ if not any(_is_within(path, item) for item in allowed)
444
+ and path not in orchestrator
445
+ }
446
+ if snapshot.artifact_root == snapshot.root:
447
+ return []
448
+ return ["artifact-root change exceeds batch policy union"] if unauthorized else []
449
+
450
+
451
+ def _allowed_artifact_paths(policies: Sequence[WritePolicy]) -> set[str]:
452
+ return {
453
+ path
454
+ for policy in policies
455
+ for path in policy.artifact_policy.get("allowedPaths", ())
456
+ }
457
+
458
+
459
+ def _is_relative_to(path: Path, root: Path) -> bool:
460
+ try:
461
+ path.relative_to(root)
462
+ except ValueError:
463
+ return False
464
+ return True
465
+
466
+
467
+ def _policy_violations(
468
+ before: MutationSnapshot,
469
+ after: MutationSnapshot,
470
+ policies: tuple[WritePolicy, ...],
471
+ source_changes: set[str],
472
+ out_of_plan_edits: Sequence[str],
473
+ ) -> list[str]:
474
+ if all(policy.source_mode == "source-readonly" for policy in policies):
475
+ failures = ["readonly source changed"] if source_changes else []
476
+ if before.git_projection != after.git_projection:
477
+ failures.append("gitPolicy disabled but Git projection changed")
478
+ return failures
479
+ policy = next(row for row in policies if row.source_mode == "project-mutation")
480
+ failures = _source_policy_failures(policy, source_changes, out_of_plan_edits)
481
+ failures.extend(_git_policy_failures(before, after, policy, source_changes, out_of_plan_edits))
482
+ return failures
483
+
484
+
485
+ def _source_policy_failures(
486
+ policy: WritePolicy,
487
+ changed: set[str],
488
+ out_of_plan_edits: Sequence[str],
489
+ ) -> list[str]:
490
+ planned = set(policy.source_policy.get("plannedPaths", ()))
491
+ declared = set(out_of_plan_edits)
492
+ protected = set(policy.source_policy.get("protectedPaths", ()))
493
+ failures: list[str] = []
494
+ if not changed <= planned | declared:
495
+ failures.append("source changes exceed planned and declared out-of-plan paths")
496
+ if not declared <= changed:
497
+ failures.append("declared out-of-plan path did not change")
498
+ if any(_is_within(path, item) for path in changed for item in protected):
499
+ failures.append("protected path changed")
500
+ return failures
501
+
502
+
503
+ def _git_policy_failures(
504
+ before: MutationSnapshot,
505
+ after: MutationSnapshot,
506
+ policy: WritePolicy,
507
+ changed: set[str],
508
+ out_of_plan_edits: Sequence[str],
509
+ ) -> list[str]:
510
+ git = policy.git_policy
511
+ root = before.root
512
+ failures: list[str] = []
513
+ if before.git_projection.get("head") != git.get("expectedBaseCommit"):
514
+ failures.append("audit baseline does not match expectedBaseCommit")
515
+ if after.git_projection.get("allowedBranchRef") != git.get("allowedBranchRef"):
516
+ failures.append("assigned branch changed")
517
+ if after.git_projection.get("gitDir") != git.get("worktreeGitDir"):
518
+ failures.append("assigned worktree Git directory changed")
519
+ if after.git_projection.get("gitCommonDir") != git.get("gitCommonDir"):
520
+ failures.append("assigned Git common directory changed")
521
+ if before.git_projection.get("worktreeRegistration") != after.git_projection.get("worktreeRegistration"):
522
+ failures.append("worktree registration changed")
523
+ if after.git_projection.get("stagedPaths"):
524
+ failures.append("final Git index contains staged changes")
525
+ if not _is_ancestor(root, str(git.get("expectedBaseCommit")), str(after.git_projection.get("head"))):
526
+ failures.append("final HEAD is not a fast-forward descendant")
527
+ allowed = set(policy.source_policy.get("plannedPaths", ())) | set(out_of_plan_edits)
528
+ if any(
529
+ not paths <= allowed
530
+ for paths in _commit_paths_by_commit(
531
+ root,
532
+ str(git.get("expectedBaseCommit")),
533
+ str(after.git_projection.get("head")),
534
+ )
535
+ ):
536
+ failures.append("commit chain changed paths outside source policy")
537
+ if not changed <= allowed:
538
+ failures.append("working tree changed paths outside source policy")
539
+ return failures
540
+
541
+
542
+ def _is_ancestor(root: Path, base: str, head: str) -> bool:
543
+ result = subprocess.run(
544
+ ["git", "-C", str(root), "merge-base", "--is-ancestor", base, head],
545
+ capture_output=True,
546
+ check=False,
547
+ )
548
+ return result.returncode == 0
549
+
550
+
551
+ def _commit_paths_by_commit(root: Path, base: str, head: str) -> tuple[set[str], ...]:
552
+ if base == head:
553
+ return ()
554
+ commits = _git_output(root, "rev-list", "--reverse", f"{base}..{head}").splitlines()
555
+ return tuple(
556
+ {
557
+ line
558
+ for line in _git_output(
559
+ root, "diff-tree", "--no-commit-id", "--name-only", "-r", commit
560
+ ).splitlines()
561
+ if line
562
+ }
563
+ for commit in commits
564
+ )
565
+
566
+
567
+ def _reflog_digest(root: Path) -> str:
568
+ payload = _git_output(root, "reflog", "show", "--no-abbrev", "--format=%H %gs", "HEAD")
569
+ return "sha256:" + hashlib.sha256(payload.encode("utf-8")).hexdigest()
570
+
571
+
572
+ def _git_changed_paths(root: Path, *args: str) -> set[str]:
573
+ result = subprocess.run(
574
+ ["git", "-C", str(root), "diff", *args, "--name-only"],
575
+ capture_output=True,
576
+ text=True,
577
+ check=False,
578
+ )
579
+ if result.returncode != 0:
580
+ raise MutationAuditError(result.stderr.strip() or "Git diff failed")
581
+ return {line for line in result.stdout.splitlines() if line}
582
+
583
+
584
+ def _is_within(path: str, parent: str) -> bool:
585
+ return path == parent or path.startswith(parent + "/")
586
+
587
+
588
+ def _terminal_status(
589
+ policies: Sequence[WritePolicy],
590
+ *,
591
+ source_changed: bool,
592
+ git_changed: bool,
593
+ attempt_succeeded: bool,
594
+ result_present: bool,
595
+ violations: Sequence[str],
596
+ ) -> str:
597
+ project_mutation = any(row.source_mode == "project-mutation" for row in policies)
598
+ if project_mutation and (source_changed or git_changed) and (
599
+ not attempt_succeeded or not result_present
600
+ ):
601
+ return "mutation-present-unresolved"
602
+ if violations:
603
+ return "contract-failed-unattributed"
604
+ if not attempt_succeeded or not result_present:
605
+ return "failed-no-mutation"
606
+ return "ok"
607
+
608
+
609
+ def _retry_allowed(
610
+ status: str,
611
+ source_changed: bool,
612
+ git_changed: bool,
613
+ scratch_changed: bool,
614
+ ) -> bool:
615
+ if status in {"mutation-present-unresolved", "contract-failed-unattributed"}:
616
+ return False
617
+ if source_changed or git_changed:
618
+ return False
619
+ return status == "failed-no-mutation" and not scratch_changed
@@ -11,9 +11,10 @@ from .consumers import (
11
11
  carry_head_commit,
12
12
  read_stage_consumer_state,
13
13
  )
14
+ from . import next_phase
14
15
  from .paths import RunRef, task_manifest_file
15
16
  from .stage_map import StageMapError, load_task_stage_map
16
- from .workflow import DEFAULT_NEXT_PHASE, PHASE_SEQUENCE
17
+ from .workflow import PHASE_SEQUENCE
17
18
 
18
19
 
19
20
  @dataclass(frozen=True)
@@ -22,7 +23,6 @@ class ImplementationOutcome:
22
23
  stages: list[int] = field(default_factory=list)
23
24
  head_commit: str = ""
24
25
  report_path: str = ""
25
- next_recommended_phase: str = ""
26
26
  reason: str = ""
27
27
 
28
28
 
@@ -66,7 +66,6 @@ def derive_implementation_outcome(task_root: Path) -> ImplementationOutcome:
66
66
  stages=sorted(required_stages),
67
67
  head_commit=str(latest_done.get("head_commit") or "") or carry_head_commit(latest_carry),
68
68
  report_path=_latest_implementation_report(task_root),
69
- next_recommended_phase=DEFAULT_NEXT_PHASE["implementation"],
70
69
  reason="all implementation stages have pass-grade carry and done rows",
71
70
  )
72
71
 
@@ -174,7 +173,22 @@ def _promote_workflow(workflow: dict[str, Any], outcome: ImplementationOutcome)
174
173
  workflow["currentPhaseState"] = "completed"
175
174
  phase_states["implementation"] = "completed"
176
175
  workflow["lastCompletedPhase"] = "implementation"
177
- workflow["nextRecommendedPhase"] = outcome.next_recommended_phase
176
+ # 재조정기의 사실은 "모든 stage 가 끝났다"는 상태다. 대상 phase 는 마지막
177
+ # stage 검증이 이미 정했으므로 건드리지 않는다 (ADR-0004).
178
+ #
179
+ # 대상 phase 가 비어 있으면 status 도 올리지 않는다. `ready` 는 "지금 이
180
+ # phase 를 시작할 수 있다"는 뜻인데 가리킬 phase 가 없으면 시작할 대상이
181
+ # 없다. 마지막 stage 리포트에 라우팅 대상이 없으면 이 자리에 빈 phase 의
182
+ # pending 포인터가 들어오므로, 그때는 그대로 둔다.
183
+ pointer = next_phase.promote(workflow.get("nextRecommendedPhase"))
184
+ if pointer["phase"]:
185
+ workflow["nextRecommendedPhase"] = next_phase.make(
186
+ phase=pointer["phase"],
187
+ status=next_phase.STATUS_READY,
188
+ rationale=pointer["rationale"],
189
+ )
190
+ else:
191
+ workflow["nextRecommendedPhase"] = pointer
178
192
  after = json.dumps(workflow, sort_keys=True, ensure_ascii=False)
179
193
  return before != after
180
194
 
@@ -195,7 +209,9 @@ def _update_task_catalog(project_root: Path, manifest: dict[str, Any]) -> bool:
195
209
  entry["currentPhase"] = workflow.get("currentPhase", "")
196
210
  entry["currentPhaseState"] = workflow.get("currentPhaseState", "")
197
211
  entry["lastCompletedPhase"] = workflow.get("lastCompletedPhase", "")
198
- entry["nextRecommendedPhase"] = workflow.get("nextRecommendedPhase", "")
212
+ entry["nextRecommendedPhase"] = next_phase.promote(
213
+ workflow.get("nextRecommendedPhase")
214
+ )
199
215
  entry["latestReportPath"] = manifest.get("latestReportPath", "")
200
216
  entry["latestRunStatus"] = manifest.get("latestRunStatus", entry.get("latestRunStatus", ""))
201
217
  entry["currentStatus"] = manifest.get("currentStatus", entry.get("currentStatus", ""))
@@ -39,7 +39,9 @@ def record_start(home: Path, *, project_id: str, project_root: str,
39
39
  okstra_version: str = "",
40
40
  brief_sha256: str = "",
41
41
  initial_status: str = "running",
42
- final_status_rel: str = "") -> str:
42
+ final_status_rel: str = "",
43
+ execution_manifest_path: str = "",
44
+ role_execution_refs: list | None = None) -> str:
43
45
  """run 시작 시점의 인덱스/invocation/meta 를 1 트랜잭션으로 기록.
44
46
  반환값: 생성된 runId.
45
47
  """
@@ -54,7 +56,9 @@ def record_start(home: Path, *, project_id: str, project_root: str,
54
56
  finished_at=when if is_terminal else None,
55
57
  workers=workers, lead_model=lead_model, validation="not-run",
56
58
  run_dir_rel=run_dir_rel, final_report_rel=final_report_rel,
57
- final_status_rel=final_status_rel)
59
+ final_status_rel=final_status_rel,
60
+ execution_manifest_path=execution_manifest_path,
61
+ role_execution_refs=role_execution_refs)
58
62
  # okstra-ctl 가 미리 'reserving' 예약 row 를 작성했을 수 있다(P1-2). 같은
59
63
  # runId 의 예약 row 가 있으면 update; 없으면 append.
60
64
  existing_rows = read_jsonl(home / "active.jsonl")