okstra 0.173.0 → 0.175.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (402) hide show
  1. package/README.md +18 -9
  2. package/bin/okstra +8 -4
  3. package/dist/cli-registry.d.mts +16 -0
  4. package/dist/cli-registry.mjs +560 -0
  5. package/dist/cli-registry.mjs.map +1 -0
  6. package/dist/commands/execute/agent-prompt.d.mts +1 -0
  7. package/{src → dist}/commands/execute/agent-prompt.mjs +10 -11
  8. package/dist/commands/execute/agent-prompt.mjs.map +1 -0
  9. package/dist/commands/execute/codex-dispatch.d.mts +3 -0
  10. package/dist/commands/execute/codex-dispatch.mjs +6 -0
  11. package/dist/commands/execute/codex-dispatch.mjs.map +1 -0
  12. package/dist/commands/execute/codex-run.d.mts +3 -0
  13. package/{src → dist}/commands/execute/codex-run.mjs +30 -37
  14. package/dist/commands/execute/codex-run.mjs.map +1 -0
  15. package/dist/commands/execute/convergence.d.mts +1 -0
  16. package/{src → dist}/commands/execute/convergence.mjs +15 -16
  17. package/dist/commands/execute/convergence.mjs.map +1 -0
  18. package/dist/commands/execute/error-log.d.mts +1 -0
  19. package/{src → dist}/commands/execute/error-log.mjs +2 -3
  20. package/dist/commands/execute/error-log.mjs.map +1 -0
  21. package/dist/commands/execute/git-reconcile.d.mts +1 -0
  22. package/{src → dist}/commands/execute/git-reconcile.mjs +10 -11
  23. package/dist/commands/execute/git-reconcile.mjs.map +1 -0
  24. package/dist/commands/execute/handoff.d.mts +1 -0
  25. package/{src → dist}/commands/execute/handoff.mjs +10 -11
  26. package/dist/commands/execute/handoff.mjs.map +1 -0
  27. package/dist/commands/execute/incremental-carry.d.mts +1 -0
  28. package/{src → dist}/commands/execute/incremental-carry.mjs +2 -3
  29. package/dist/commands/execute/incremental-carry.mjs.map +1 -0
  30. package/dist/commands/execute/incremental-scope.d.mts +1 -0
  31. package/{src → dist}/commands/execute/incremental-scope.mjs +2 -3
  32. package/dist/commands/execute/incremental-scope.mjs.map +1 -0
  33. package/dist/commands/execute/integrate-stages.d.mts +1 -0
  34. package/{src → dist}/commands/execute/integrate-stages.mjs +10 -11
  35. package/dist/commands/execute/integrate-stages.mjs.map +1 -0
  36. package/dist/commands/execute/pane-title.d.mts +1 -0
  37. package/dist/commands/execute/pane-title.mjs +20 -0
  38. package/dist/commands/execute/pane-title.mjs.map +1 -0
  39. package/dist/commands/execute/plan-items.d.mts +1 -0
  40. package/dist/commands/execute/plan-items.mjs +9 -0
  41. package/dist/commands/execute/plan-items.mjs.map +1 -0
  42. package/dist/commands/execute/plan-validate.d.mts +1 -0
  43. package/dist/commands/execute/plan-validate.mjs +68 -0
  44. package/dist/commands/execute/plan-validate.mjs.map +1 -0
  45. package/dist/commands/execute/plan-verify.d.mts +1 -0
  46. package/{src → dist}/commands/execute/plan-verify.mjs +13 -16
  47. package/dist/commands/execute/plan-verify.mjs.map +1 -0
  48. package/dist/commands/execute/render-bundle.d.mts +3 -0
  49. package/{src → dist}/commands/execute/render-bundle.mjs +65 -61
  50. package/dist/commands/execute/render-bundle.mjs.map +1 -0
  51. package/dist/commands/execute/run.d.mts +24 -0
  52. package/dist/commands/execute/run.mjs +196 -0
  53. package/dist/commands/execute/run.mjs.map +1 -0
  54. package/dist/commands/execute/spawn-followups.d.mts +1 -0
  55. package/{src → dist}/commands/execute/spawn-followups.mjs +6 -7
  56. package/dist/commands/execute/spawn-followups.mjs.map +1 -0
  57. package/dist/commands/execute/team.d.mts +3 -0
  58. package/dist/commands/execute/team.mjs +56 -0
  59. package/dist/commands/execute/team.mjs.map +1 -0
  60. package/dist/commands/execute/token-usage.d.mts +1 -0
  61. package/{src → dist}/commands/execute/token-usage.mjs +2 -3
  62. package/dist/commands/execute/token-usage.mjs.map +1 -0
  63. package/dist/commands/execute/wizard.d.mts +8 -0
  64. package/dist/commands/execute/wizard.mjs +191 -0
  65. package/dist/commands/execute/wizard.mjs.map +1 -0
  66. package/dist/commands/execute/worker-audit-check.d.mts +1 -0
  67. package/{src → dist}/commands/execute/worker-audit-check.mjs +11 -12
  68. package/dist/commands/execute/worker-audit-check.mjs.map +1 -0
  69. package/dist/commands/execute/worker-dispatch.d.mts +7 -0
  70. package/{src → dist}/commands/execute/worker-dispatch.mjs +34 -46
  71. package/dist/commands/execute/worker-dispatch.mjs.map +1 -0
  72. package/dist/commands/execute/worker-state.d.mts +1 -0
  73. package/{src → dist}/commands/execute/worker-state.mjs +15 -16
  74. package/dist/commands/execute/worker-state.mjs.map +1 -0
  75. package/dist/commands/execute/worktree-lookup.d.mts +1 -0
  76. package/{src → dist}/commands/execute/worktree-lookup.mjs +38 -37
  77. package/dist/commands/execute/worktree-lookup.mjs.map +1 -0
  78. package/dist/commands/execute/worktree-status.d.mts +1 -0
  79. package/dist/commands/execute/worktree-status.mjs +115 -0
  80. package/dist/commands/execute/worktree-status.mjs.map +1 -0
  81. package/dist/commands/inspect/code-review.d.mts +1 -0
  82. package/{src → dist}/commands/inspect/code-review.mjs +16 -19
  83. package/dist/commands/inspect/code-review.mjs.map +1 -0
  84. package/dist/commands/inspect/container.d.mts +1 -0
  85. package/{src → dist}/commands/inspect/container.mjs +11 -13
  86. package/dist/commands/inspect/container.mjs.map +1 -0
  87. package/dist/commands/inspect/context-cost.d.mts +1 -0
  88. package/{src → dist}/commands/inspect/context-cost.mjs +11 -13
  89. package/dist/commands/inspect/context-cost.mjs.map +1 -0
  90. package/dist/commands/inspect/design-prep.d.mts +1 -0
  91. package/{src → dist}/commands/inspect/design-prep.mjs +10 -11
  92. package/dist/commands/inspect/design-prep.mjs.map +1 -0
  93. package/dist/commands/inspect/error-report.d.mts +1 -0
  94. package/{src → dist}/commands/inspect/error-report.mjs +11 -13
  95. package/dist/commands/inspect/error-report.mjs.map +1 -0
  96. package/dist/commands/inspect/error-zip.d.mts +1 -0
  97. package/{src → dist}/commands/inspect/error-zip.mjs +11 -12
  98. package/dist/commands/inspect/error-zip.mjs.map +1 -0
  99. package/dist/commands/inspect/log-report.d.mts +1 -0
  100. package/{src → dist}/commands/inspect/log-report.mjs +11 -13
  101. package/dist/commands/inspect/log-report.mjs.map +1 -0
  102. package/dist/commands/inspect/profile-show.d.mts +1 -0
  103. package/{src → dist}/commands/inspect/profile-show.mjs +10 -11
  104. package/dist/commands/inspect/profile-show.mjs.map +1 -0
  105. package/dist/commands/inspect/recap.d.mts +1 -0
  106. package/{src → dist}/commands/inspect/recap.mjs +11 -13
  107. package/dist/commands/inspect/recap.mjs.map +1 -0
  108. package/dist/commands/inspect/resolve-task-key.d.mts +1 -0
  109. package/{src → dist}/commands/inspect/resolve-task-key.mjs +11 -13
  110. package/dist/commands/inspect/resolve-task-key.mjs.map +1 -0
  111. package/dist/commands/inspect/rollup.d.mts +1 -0
  112. package/{src → dist}/commands/inspect/rollup.mjs +11 -13
  113. package/dist/commands/inspect/rollup.mjs.map +1 -0
  114. package/dist/commands/inspect/run-audit.d.mts +1 -0
  115. package/{src → dist}/commands/inspect/run-audit.mjs +11 -12
  116. package/dist/commands/inspect/run-audit.mjs.map +1 -0
  117. package/dist/commands/inspect/set-work-status.d.mts +1 -0
  118. package/{src → dist}/commands/inspect/set-work-status.mjs +11 -13
  119. package/dist/commands/inspect/set-work-status.mjs.map +1 -0
  120. package/dist/commands/inspect/stage-map.d.mts +1 -0
  121. package/dist/commands/inspect/stage-map.mjs +110 -0
  122. package/dist/commands/inspect/stage-map.mjs.map +1 -0
  123. package/dist/commands/inspect/task-list.d.mts +1 -0
  124. package/dist/commands/inspect/task-list.mjs +103 -0
  125. package/dist/commands/inspect/task-list.mjs.map +1 -0
  126. package/dist/commands/inspect/task-show.d.mts +1 -0
  127. package/dist/commands/inspect/task-show.mjs +108 -0
  128. package/dist/commands/inspect/task-show.mjs.map +1 -0
  129. package/dist/commands/inspect/time-report.d.mts +1 -0
  130. package/{src → dist}/commands/inspect/time-report.mjs +11 -13
  131. package/dist/commands/inspect/time-report.mjs.map +1 -0
  132. package/dist/commands/inspect/usage-report.d.mts +1 -0
  133. package/{src → dist}/commands/inspect/usage-report.mjs +11 -13
  134. package/dist/commands/inspect/usage-report.mjs.map +1 -0
  135. package/dist/commands/inspect/user-response.d.mts +1 -0
  136. package/{src → dist}/commands/inspect/user-response.mjs +10 -11
  137. package/dist/commands/inspect/user-response.mjs.map +1 -0
  138. package/dist/commands/inspect/worker-liveness.d.mts +1 -0
  139. package/{src → dist}/commands/inspect/worker-liveness.mjs +11 -13
  140. package/dist/commands/inspect/worker-liveness.mjs.map +1 -0
  141. package/dist/commands/lifecycle/check-project.d.mts +21 -0
  142. package/dist/commands/lifecycle/check-project.mjs +175 -0
  143. package/dist/commands/lifecycle/check-project.mjs.map +1 -0
  144. package/dist/commands/lifecycle/config.d.mts +1 -0
  145. package/dist/commands/lifecycle/config.mjs +369 -0
  146. package/dist/commands/lifecycle/config.mjs.map +1 -0
  147. package/dist/commands/lifecycle/doctor.d.mts +10 -0
  148. package/dist/commands/lifecycle/doctor.mjs +317 -0
  149. package/dist/commands/lifecycle/doctor.mjs.map +1 -0
  150. package/dist/commands/lifecycle/install.d.mts +17 -0
  151. package/dist/commands/lifecycle/install.mjs +1077 -0
  152. package/dist/commands/lifecycle/install.mjs.map +1 -0
  153. package/dist/commands/lifecycle/migrate.d.mts +1 -0
  154. package/{src → dist}/commands/lifecycle/migrate.mjs +10 -11
  155. package/dist/commands/lifecycle/migrate.mjs.map +1 -0
  156. package/dist/commands/lifecycle/model.d.mts +1 -0
  157. package/dist/commands/lifecycle/model.mjs +22 -0
  158. package/dist/commands/lifecycle/model.mjs.map +1 -0
  159. package/dist/commands/lifecycle/paths.d.mts +1 -0
  160. package/dist/commands/lifecycle/paths.mjs +102 -0
  161. package/dist/commands/lifecycle/paths.mjs.map +1 -0
  162. package/dist/commands/lifecycle/preflight.d.mts +1 -0
  163. package/dist/commands/lifecycle/preflight.mjs +124 -0
  164. package/dist/commands/lifecycle/preflight.mjs.map +1 -0
  165. package/dist/commands/lifecycle/setup.d.mts +1 -0
  166. package/dist/commands/lifecycle/setup.mjs +297 -0
  167. package/dist/commands/lifecycle/setup.mjs.map +1 -0
  168. package/dist/commands/lifecycle/uninstall.d.mts +4 -0
  169. package/dist/commands/lifecycle/uninstall.mjs +262 -0
  170. package/dist/commands/lifecycle/uninstall.mjs.map +1 -0
  171. package/dist/commands/manager.d.mts +3 -0
  172. package/{src → dist}/commands/manager.mjs +25 -32
  173. package/dist/commands/manager.mjs.map +1 -0
  174. package/dist/commands/memory/memory.d.mts +1 -0
  175. package/dist/commands/memory/memory.mjs +569 -0
  176. package/dist/commands/memory/memory.mjs.map +1 -0
  177. package/dist/commands/pr/pr.d.mts +33 -0
  178. package/dist/commands/pr/pr.mjs +296 -0
  179. package/dist/commands/pr/pr.mjs.map +1 -0
  180. package/dist/commands/report/agent-activity.d.mts +1 -0
  181. package/{src → dist}/commands/report/agent-activity.mjs +11 -12
  182. package/dist/commands/report/agent-activity.mjs.map +1 -0
  183. package/dist/commands/report/finalize.d.mts +4 -0
  184. package/dist/commands/report/finalize.mjs +64 -0
  185. package/dist/commands/report/finalize.mjs.map +1 -0
  186. package/dist/commands/report/inject-report-index.d.mts +1 -0
  187. package/{src → dist}/commands/report/inject-report-index.mjs +6 -7
  188. package/dist/commands/report/inject-report-index.mjs.map +1 -0
  189. package/dist/commands/report/render-final-report.d.mts +1 -0
  190. package/{src → dist}/commands/report/render-final-report.mjs +6 -7
  191. package/dist/commands/report/render-final-report.mjs.map +1 -0
  192. package/dist/commands/report/render-views.d.mts +1 -0
  193. package/{src → dist}/commands/report/render-views.mjs +6 -7
  194. package/dist/commands/report/render-views.mjs.map +1 -0
  195. package/dist/commands/report/translate.d.mts +1 -0
  196. package/{src → dist}/commands/report/translate.mjs +6 -7
  197. package/dist/commands/report/translate.mjs.map +1 -0
  198. package/dist/lib/helper-scripts.d.mts +3 -0
  199. package/{src → dist}/lib/helper-scripts.mjs +11 -14
  200. package/dist/lib/helper-scripts.mjs.map +1 -0
  201. package/dist/lib/host-registry-client.d.mts +25 -0
  202. package/dist/lib/host-registry-client.mjs +152 -0
  203. package/dist/lib/host-registry-client.mjs.map +1 -0
  204. package/dist/lib/install-assets.d.mts +8 -0
  205. package/{src → dist}/lib/install-assets.mjs +17 -20
  206. package/dist/lib/install-assets.mjs.map +1 -0
  207. package/dist/lib/okstra-dirs.d.mts +4 -0
  208. package/{src → dist}/lib/okstra-dirs.mjs +3 -6
  209. package/dist/lib/okstra-dirs.mjs.map +1 -0
  210. package/dist/lib/paths.d.mts +4 -0
  211. package/dist/lib/paths.mjs +78 -0
  212. package/dist/lib/paths.mjs.map +1 -0
  213. package/dist/lib/proc.d.mts +3 -0
  214. package/dist/lib/proc.mjs +31 -0
  215. package/dist/lib/proc.mjs.map +1 -0
  216. package/dist/lib/python-helper.d.mts +8 -0
  217. package/dist/lib/python-helper.mjs +123 -0
  218. package/dist/lib/python-helper.mjs.map +1 -0
  219. package/dist/lib/runtime-manifest.d.mts +7 -0
  220. package/dist/lib/runtime-manifest.mjs +73 -0
  221. package/dist/lib/runtime-manifest.mjs.map +1 -0
  222. package/dist/lib/skill-catalog.d.mts +4 -0
  223. package/dist/lib/skill-catalog.mjs +50 -0
  224. package/dist/lib/skill-catalog.mjs.map +1 -0
  225. package/dist/lib/types.d.mts +129 -0
  226. package/dist/lib/types.mjs +2 -0
  227. package/dist/lib/types.mjs.map +1 -0
  228. package/dist/lib/version.d.mts +2 -0
  229. package/dist/lib/version.mjs +27 -0
  230. package/dist/lib/version.mjs.map +1 -0
  231. package/docs/architecture/storage-model.md +17 -3
  232. package/docs/architecture.md +12 -22
  233. package/docs/cli.md +29 -8
  234. package/docs/container.md +1 -1
  235. package/docs/contributor-change-matrix.md +3 -3
  236. package/docs/project-structure-overview.md +29 -20
  237. package/docs/task-process/README.md +3 -1
  238. package/docs/task-process/implementation-planning.md +2 -2
  239. package/docs/task-process/implementation.md +1 -1
  240. package/package.json +11 -5
  241. package/runtime/BUILD.json +2 -2
  242. package/runtime/agents/workers/claude-worker.md +3 -2
  243. package/runtime/agents/workers/report-writer-worker.md +3 -1
  244. package/runtime/agents/workers/translator-worker.md +3 -1
  245. package/runtime/bin/lib/okstra/cli.sh +12 -0
  246. package/runtime/bin/lib/okstra/globals.sh +4 -1
  247. package/runtime/bin/lib/okstra/usage.sh +4 -4
  248. package/runtime/bin/okstra-error-log.py +27 -0
  249. package/runtime/bin/okstra-provider-exec.py +245 -16
  250. package/runtime/bin/okstra-trace-cleanup.sh +58 -129
  251. package/runtime/bin/okstra.sh +10 -0
  252. package/runtime/prompts/lead/adapters/cmux.md +2 -0
  253. package/runtime/prompts/lead/convergence.md +27 -18
  254. package/runtime/prompts/lead/okstra-lead-contract.md +4 -2
  255. package/runtime/prompts/lead/plan-body-verification.md +13 -6
  256. package/runtime/prompts/lead/report-writer.md +6 -6
  257. package/runtime/prompts/lead/team-contract.md +1 -1
  258. package/runtime/prompts/profiles/_common-contract.md +2 -2
  259. package/runtime/prompts/profiles/_implementation-executor.md +2 -0
  260. package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
  261. package/runtime/prompts/profiles/change-impact-analysis.md +16 -0
  262. package/runtime/prompts/profiles/error-analysis.md +21 -1
  263. package/runtime/prompts/profiles/feature-analysis.md +16 -0
  264. package/runtime/prompts/profiles/final-verification.md +16 -0
  265. package/runtime/prompts/profiles/implementation-option-selection.md +16 -0
  266. package/runtime/prompts/profiles/implementation-planning.md +32 -9
  267. package/runtime/prompts/profiles/implementation.md +17 -1
  268. package/runtime/prompts/profiles/improvement-discovery.md +16 -0
  269. package/runtime/prompts/profiles/project-analysis.md +16 -0
  270. package/runtime/prompts/profiles/release-handoff.md +5 -1
  271. package/runtime/prompts/profiles/requirements-discovery.md +20 -0
  272. package/runtime/prompts/wizard/prompts.ko.json +18 -0
  273. package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +1 -6
  274. package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +8 -0
  275. package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +8 -0
  276. package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
  277. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +2 -2
  278. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +25 -5
  279. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
  280. package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +4 -1
  281. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +4 -4
  282. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
  283. package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +2 -2
  284. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +79 -11
  285. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +100 -25
  286. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +59 -14
  287. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +55 -9
  288. package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +51 -12
  289. package/runtime/python/okstra_ctl/agent_activity.py +5 -1
  290. package/runtime/python/okstra_ctl/agent_invocation.py +608 -14
  291. package/runtime/python/okstra_ctl/agent_prompt_cli.py +249 -50
  292. package/runtime/python/okstra_ctl/application/resolve_assignment.py +53 -12
  293. package/runtime/python/okstra_ctl/assignment_environment.py +102 -0
  294. package/runtime/python/okstra_ctl/assignment_resolver.py +790 -0
  295. package/runtime/python/okstra_ctl/attempt_evidence.py +342 -0
  296. package/runtime/python/okstra_ctl/conformance.py +68 -0
  297. package/runtime/python/okstra_ctl/convergence.py +409 -3
  298. package/runtime/python/okstra_ctl/convergence_engine.py +248 -15
  299. package/runtime/python/okstra_ctl/convergence_store.py +170 -1
  300. package/runtime/python/okstra_ctl/dispatch_core.py +1181 -102
  301. package/runtime/python/okstra_ctl/dispatch_state.py +835 -22
  302. package/runtime/python/okstra_ctl/doctor.py +16 -2
  303. package/runtime/python/okstra_ctl/domain/host.py +32 -1
  304. package/runtime/python/okstra_ctl/domain/provider.py +212 -6
  305. package/runtime/python/okstra_ctl/domain/role.py +125 -0
  306. package/runtime/python/okstra_ctl/domain/wizard/interaction.py +13 -0
  307. package/runtime/python/okstra_ctl/domain/worker_exec.py +39 -1
  308. package/runtime/python/okstra_ctl/domain/worker_role.py +13 -3
  309. package/runtime/python/okstra_ctl/domain/worker_stream.py +24 -0
  310. package/runtime/python/okstra_ctl/error_log_write.py +9 -0
  311. package/runtime/python/okstra_ctl/error_report.py +21 -1
  312. package/runtime/python/okstra_ctl/execution_identity.py +529 -0
  313. package/runtime/python/okstra_ctl/execution_manifest.py +1281 -0
  314. package/runtime/python/okstra_ctl/execution_mutation_audit.py +619 -0
  315. package/runtime/python/okstra_ctl/final_report_schema.py +5 -4
  316. package/runtime/python/okstra_ctl/index.py +6 -2
  317. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +43 -3
  318. package/runtime/python/okstra_ctl/legacy_model_selection.py +490 -0
  319. package/runtime/python/okstra_ctl/model_cli.py +278 -0
  320. package/runtime/python/okstra_ctl/model_defaults.py +46 -0
  321. package/runtime/python/okstra_ctl/model_discovery.py +26 -7
  322. package/runtime/python/okstra_ctl/model_pool.py +501 -0
  323. package/runtime/python/okstra_ctl/models.py +47 -27
  324. package/runtime/python/okstra_ctl/mutation_recovery.py +293 -0
  325. package/runtime/python/okstra_ctl/pane_reclaim.py +13 -22
  326. package/runtime/python/okstra_ctl/pane_title.py +151 -0
  327. package/runtime/python/okstra_ctl/ports/host_model.py +48 -17
  328. package/runtime/python/okstra_ctl/ports/worker_dispatch.py +4 -0
  329. package/runtime/python/okstra_ctl/registry/provider_registry.py +8 -4
  330. package/runtime/python/okstra_ctl/render.py +78 -10
  331. package/runtime/python/okstra_ctl/render_final_report.py +34 -19
  332. package/runtime/python/okstra_ctl/report_contract.py +39 -1
  333. package/runtime/python/okstra_ctl/report_finalize.py +84 -9
  334. package/runtime/python/okstra_ctl/report_html/render.py +1 -0
  335. package/runtime/python/okstra_ctl/role_requirements.py +217 -0
  336. package/runtime/python/okstra_ctl/run.py +1250 -260
  337. package/runtime/python/okstra_ctl/run_index_row.py +7 -1
  338. package/runtime/python/okstra_ctl/schema_excerpt.py +1 -1
  339. package/runtime/python/okstra_ctl/scope_provenance.py +1 -1
  340. package/runtime/python/okstra_ctl/session.py +69 -12
  341. package/runtime/python/okstra_ctl/team.py +51 -25
  342. package/runtime/python/okstra_ctl/time_report.py +38 -4
  343. package/runtime/python/okstra_ctl/tmux.py +19 -149
  344. package/runtime/python/okstra_ctl/usage_identity.py +86 -0
  345. package/runtime/python/okstra_ctl/usage_report.py +15 -3
  346. package/runtime/python/okstra_ctl/wizard.py +1253 -62
  347. package/runtime/python/okstra_ctl/worker_audit_ledger.py +68 -65
  348. package/runtime/python/okstra_ctl/worker_request.py +9 -1
  349. package/runtime/python/okstra_ctl/worker_runner.py +130 -5
  350. package/runtime/python/okstra_ctl/workers.py +7 -5
  351. package/runtime/python/okstra_ctl/worktree.py +69 -3
  352. package/runtime/python/okstra_ctl/write_policy.py +571 -0
  353. package/runtime/python/okstra_token_usage/cli.py +1 -1
  354. package/runtime/python/okstra_token_usage/collect.py +66 -6
  355. package/runtime/schemas/convergence-groups-v2.0.schema.json +128 -0
  356. package/runtime/schemas/execution-manifest-v2.schema.json +340 -0
  357. package/runtime/schemas/final-report-v2.0.schema.json +25 -0
  358. package/runtime/skills/okstra-run/SKILL.md +9 -9
  359. package/runtime/skills/okstra-setup/references/project-config.md +11 -0
  360. package/runtime/templates/report-writer-prompt-preamble.md +2 -0
  361. package/runtime/templates/reports/final-report-v2.template.md +7 -1
  362. package/runtime/templates/reports/html/base.template.html +21 -0
  363. package/runtime/templates/reports/html/i18n/en.json +4 -0
  364. package/runtime/templates/reports/html/i18n/ko.json +4 -0
  365. package/runtime/templates/reports/settings.template.json +0 -24
  366. package/runtime/templates/worker-prompt-preamble.md +6 -2
  367. package/runtime/validators/lib/fixtures.sh +98 -33
  368. package/runtime/validators/validate-implementation-plan-stages.py +63 -3
  369. package/runtime/validators/validate-run.py +443 -508
  370. package/runtime/validators/validate_session_conformance.py +26 -8
  371. package/runtime/bin/okstra-subagent-reclaim.sh +0 -26
  372. package/runtime/schemas/final-report-v1.0.schema.json +0 -6366
  373. package/runtime/templates/reports/final-report.template.md +0 -1258
  374. package/src/cli-registry.mjs +0 -532
  375. package/src/commands/execute/codex-dispatch.mjs +0 -10
  376. package/src/commands/execute/plan-items.mjs +0 -9
  377. package/src/commands/execute/plan-validate.mjs +0 -68
  378. package/src/commands/execute/run.mjs +0 -208
  379. package/src/commands/execute/team.mjs +0 -63
  380. package/src/commands/execute/wizard.mjs +0 -167
  381. package/src/commands/inspect/stage-map.mjs +0 -98
  382. package/src/commands/inspect/task-list.mjs +0 -97
  383. package/src/commands/inspect/task-show.mjs +0 -100
  384. package/src/commands/lifecycle/check-project.mjs +0 -187
  385. package/src/commands/lifecycle/config.mjs +0 -385
  386. package/src/commands/lifecycle/doctor.mjs +0 -308
  387. package/src/commands/lifecycle/install.mjs +0 -1172
  388. package/src/commands/lifecycle/paths.mjs +0 -104
  389. package/src/commands/lifecycle/preflight.mjs +0 -125
  390. package/src/commands/lifecycle/setup.mjs +0 -333
  391. package/src/commands/lifecycle/uninstall.mjs +0 -279
  392. package/src/commands/memory/memory.mjs +0 -524
  393. package/src/commands/pr/pr.mjs +0 -261
  394. package/src/commands/report/finalize.mjs +0 -64
  395. package/src/lib/host-registry-client.mjs +0 -176
  396. package/src/lib/paths.mjs +0 -86
  397. package/src/lib/proc.mjs +0 -31
  398. package/src/lib/python-helper.mjs +0 -142
  399. package/src/lib/runtime-manifest.mjs +0 -70
  400. package/src/lib/skill-catalog.mjs +0 -53
  401. package/src/lib/version.mjs +0 -20
  402. /package/{src → dist}/commands/pr/default.md +0 -0
@@ -0,0 +1,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
@@ -1,7 +1,7 @@
1
1
  """Mini JSON Schema validator for the final-report data.json.
2
2
 
3
3
  This is a deliberately narrow JSON Schema implementation that supports
4
- exactly the keywords used by ``schemas/final-report-v1.0.schema.json``:
4
+ exactly the keywords used by ``schemas/final-report-v2.0.schema.json``:
5
5
 
6
6
  type, required, properties, additionalProperties, enum, const, pattern,
7
7
  minLength, minItems, maxItems, uniqueItems, items, minimum, maximum, $ref ($defs),
@@ -23,6 +23,8 @@ import re
23
23
  from pathlib import Path
24
24
  from typing import Any
25
25
 
26
+ from okstra_ctl.report_contract import CURRENT_REPORT_SCHEMA_VERSION
27
+
26
28
 
27
29
  class SchemaError(ValueError):
28
30
  """Raised when a schema itself is malformed (e.g. a $ref points
@@ -260,7 +262,6 @@ def validate(data: Any, schema: dict) -> list[str]:
260
262
 
261
263
 
262
264
  SCHEMA_FILENAMES = {
263
- "1.0": "final-report-v1.0.schema.json",
264
265
  "2.0": "final-report-v2.0.schema.json",
265
266
  }
266
267
 
@@ -295,9 +296,9 @@ def load_schema_for_data(data: dict, start: Path | None = None) -> dict:
295
296
 
296
297
  def load_schema(schema_path: Path | None = None) -> dict:
297
298
  """Load the final-report schema. If ``schema_path`` is None, locate
298
- ``schemas/final-report-v1.0.schema.json`` relative to this file's
299
+ ``schemas/final-report-v2.0.schema.json`` relative to this file's
299
300
  repo root.
300
301
  """
301
302
  if schema_path is not None:
302
303
  return json.loads(Path(schema_path).read_text(encoding="utf-8"))
303
- return load_schema_version("1.0")
304
+ return load_schema_version(CURRENT_REPORT_SCHEMA_VERSION)
@@ -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")