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,571 @@
1
+ """Canonical invocation write policy and truthful runner enforcement."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import subprocess
7
+ from collections.abc import Mapping, Sequence
8
+ from dataclasses import dataclass
9
+ from pathlib import Path, PurePosixPath
10
+ from typing import Any, Literal
11
+
12
+ BoundaryPrecision = Literal["exact-path", "directory-boundary", "none"]
13
+
14
+ _PRECISION_RANK = {"none": 0, "directory-boundary": 1, "exact-path": 2}
15
+ _POLICY_KEYS = {
16
+ "artifactPolicy",
17
+ "sourcePolicy",
18
+ "gitPolicy",
19
+ "auxiliaryPolicy",
20
+ }
21
+ _ENFORCEMENT_KEYS = {
22
+ "boundaryPrecision",
23
+ "mutationAudit",
24
+ "attribution",
25
+ "auditedRoots",
26
+ "observationCoverage",
27
+ "unobservedWriteSurfaces",
28
+ }
29
+
30
+
31
+ class WritePolicyError(ValueError):
32
+ """Raised when a write policy cannot be made canonical or trustworthy."""
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class WritePolicy:
37
+ artifact_policy: Mapping[str, Any]
38
+ source_policy: Mapping[str, Any]
39
+ git_policy: Mapping[str, Any]
40
+ auxiliary_policy: Mapping[str, Any]
41
+ maximum_boundary_precision: BoundaryPrecision = "none"
42
+
43
+ def to_payload(self) -> dict[str, Any]:
44
+ return {
45
+ "artifactPolicy": dict(self.artifact_policy),
46
+ "sourcePolicy": dict(self.source_policy),
47
+ "gitPolicy": dict(self.git_policy),
48
+ "auxiliaryPolicy": dict(self.auxiliary_policy),
49
+ }
50
+
51
+ @property
52
+ def digest(self) -> str:
53
+ return write_policy_digest(self.to_payload())
54
+
55
+ @property
56
+ def source_mode(self) -> str:
57
+ return str(self.source_policy["mode"])
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class WriteEnforcement:
62
+ boundary_precision: BoundaryPrecision
63
+ mutation_audit: Literal["none", "batch"]
64
+ attribution: Literal["call", "batch-unattributed"]
65
+ audited_roots: tuple[Path, ...]
66
+ observation_coverage: Literal["complete", "scoped", "partial"]
67
+ unobserved_write_surfaces: tuple[str, ...]
68
+
69
+ def to_payload(self) -> dict[str, Any]:
70
+ return {
71
+ "boundaryPrecision": self.boundary_precision,
72
+ "mutationAudit": self.mutation_audit,
73
+ "attribution": self.attribution,
74
+ "auditedRoots": [str(path) for path in self.audited_roots],
75
+ "observationCoverage": self.observation_coverage,
76
+ "unobservedWriteSurfaces": list(self.unobserved_write_surfaces),
77
+ }
78
+
79
+
80
+ def build_write_policy(
81
+ invocation: Mapping[str, Any],
82
+ assignment: Mapping[str, Any],
83
+ worktree: Path | None,
84
+ ) -> WritePolicy:
85
+ """Build one provider-neutral policy from invocation and adapter authority."""
86
+ role = _required_text(invocation, "role")
87
+ project_root = Path(_required_text(invocation, "projectRoot")).resolve()
88
+ source_root = _trusted_root(invocation, worktree)
89
+ artifact_paths = _relative_paths(invocation.get("artifactPaths", ()))
90
+ if not artifact_paths:
91
+ raise WritePolicyError("artifactPaths must contain standard worker artifacts")
92
+ source_mode = "project-mutation" if role == "implementer" else "source-readonly"
93
+ source_policy: dict[str, Any] = {
94
+ "mode": source_mode,
95
+ "allowedRoot": str(source_root),
96
+ "plannedPaths": list(_relative_paths(invocation.get("plannedPaths", ()))),
97
+ "protectedPaths": list(
98
+ _relative_paths(invocation.get("protectedPaths", (".okstra", ".git")))
99
+ ),
100
+ }
101
+ git_policy = _git_policy(invocation, source_root, source_mode)
102
+ external_roots = _validated_auxiliary_roots(invocation, assignment)
103
+ auxiliary_policy = {
104
+ "scratchRoots": [
105
+ str(path) for path in _absolute_roots(invocation.get("scratchRoots", ()))
106
+ ],
107
+ "generatedPaths": list(
108
+ _relative_paths(invocation.get("generatedPaths", ()))
109
+ ),
110
+ "externalRoots": [str(path) for path in external_roots],
111
+ }
112
+ policy = WritePolicy(
113
+ artifact_policy={
114
+ "allowedRoot": str(project_root),
115
+ "allowedPaths": list(artifact_paths),
116
+ },
117
+ source_policy=source_policy,
118
+ git_policy=git_policy,
119
+ auxiliary_policy=auxiliary_policy,
120
+ maximum_boundary_precision=_assignment_precision(assignment),
121
+ )
122
+ validate_write_policy_payload(policy.to_payload())
123
+ return policy
124
+
125
+
126
+ def build_invocation_write_contract(
127
+ *,
128
+ role: str,
129
+ project_root: Path,
130
+ worktree: Path | None,
131
+ artifact_paths: Sequence[Path],
132
+ maximum_precision: BoundaryPrecision,
133
+ planned_paths: Sequence[str] = (),
134
+ scratch_roots: Sequence[Path] = (),
135
+ generated_paths: Sequence[str] = (),
136
+ auxiliary_roots: Sequence[Path] = (),
137
+ validated_auxiliary_roots: Sequence[Path] = (),
138
+ ) -> tuple[WritePolicy, WriteEnforcement]:
139
+ """Build the single canonical contract used by every v2 dispatcher."""
140
+ root = project_root.resolve()
141
+ relative_artifacts = tuple(
142
+ _relative_to_root(path, root, "artifact") for path in artifact_paths
143
+ )
144
+ invocation: dict[str, Any] = {
145
+ "role": role,
146
+ "projectRoot": str(root),
147
+ "artifactPaths": list(relative_artifacts),
148
+ "plannedPaths": list(planned_paths),
149
+ "protectedPaths": [".okstra", ".git"],
150
+ "generatedPaths": list(generated_paths),
151
+ "scratchRoots": [str(path) for path in scratch_roots],
152
+ "auxiliaryRoots": [str(path) for path in auxiliary_roots],
153
+ }
154
+ source_root = worktree.resolve() if worktree is not None else root
155
+ if role == "implementer":
156
+ invocation.update({
157
+ "expectedBaseCommit": _git_output(source_root, "rev-parse", "HEAD"),
158
+ "allowedBranchRef": _git_output(
159
+ source_root, "symbolic-ref", "-q", "HEAD"
160
+ ),
161
+ })
162
+ assignment = {
163
+ "workerWriteCapability": {
164
+ "maxBoundaryPrecision": maximum_precision,
165
+ },
166
+ "validatedAuxiliaryRoots": [str(path) for path in validated_auxiliary_roots],
167
+ }
168
+ policy = build_write_policy(invocation, assignment, source_root)
169
+ return policy, derive_write_enforcement(policy, maximum_precision, role=role)
170
+
171
+
172
+ def planned_paths_from_run_manifest(
173
+ project_root: Path, manifest: Mapping[str, Any]
174
+ ) -> tuple[str, ...]:
175
+ active_value = manifest.get("activeRunContextPath")
176
+ if not isinstance(active_value, str) or not active_value:
177
+ raise WritePolicyError("implementer write policy has no active run context")
178
+ active = _read_json(_rooted(project_root, active_value), "active run context")
179
+ source = active.get("sourceArtifacts")
180
+ run_inputs_value = source.get("runInputsPath") if isinstance(source, Mapping) else None
181
+ run = active.get("run")
182
+ stage_value = run.get("stage") if isinstance(run, Mapping) else None
183
+ if not isinstance(run_inputs_value, str) or not str(stage_value).isdigit():
184
+ raise WritePolicyError("implementer write policy has no approved stage authority")
185
+ inputs_payload = _read_json(_rooted(project_root, run_inputs_value), "run inputs")
186
+ inputs = inputs_payload.get("inputs")
187
+ approved = inputs.get("approvedPlanPath") if isinstance(inputs, Mapping) else None
188
+ if not isinstance(approved, str) or not approved:
189
+ raise WritePolicyError("implementer write policy has no approved plan path")
190
+ return _planned_paths_from_report(Path(approved), int(stage_value))
191
+
192
+
193
+ def derive_write_enforcement(
194
+ policy: WritePolicy,
195
+ maximum_precision: BoundaryPrecision,
196
+ *,
197
+ role: str | None = None,
198
+ ) -> WriteEnforcement:
199
+ """Lower runner capability when policy paths cannot be enforced exactly."""
200
+ if role == "leader":
201
+ enforcement = WriteEnforcement(
202
+ boundary_precision="none",
203
+ mutation_audit="none",
204
+ attribution="batch-unattributed",
205
+ audited_roots=_audited_roots(policy),
206
+ observation_coverage="partial",
207
+ unobserved_write_surfaces=(
208
+ "lead-session-host-writes",
209
+ "other-stage-refs",
210
+ "shared-git-object-store",
211
+ ),
212
+ )
213
+ validate_write_enforcement_payload(enforcement.to_payload())
214
+ return enforcement
215
+ if maximum_precision not in _PRECISION_RANK:
216
+ raise WritePolicyError("worker write capability is invalid")
217
+ exact_policy = (
218
+ policy.source_mode == "source-readonly"
219
+ and not policy.auxiliary_policy.get("externalRoots")
220
+ and not policy.auxiliary_policy.get("scratchRoots")
221
+ and not policy.auxiliary_policy.get("generatedPaths")
222
+ )
223
+ if maximum_precision == "exact-path" and exact_policy:
224
+ boundary: BoundaryPrecision = "exact-path"
225
+ elif _PRECISION_RANK[maximum_precision] >= _PRECISION_RANK["directory-boundary"]:
226
+ boundary = "directory-boundary"
227
+ else:
228
+ boundary = "none"
229
+ exact = boundary == "exact-path" and exact_policy
230
+ external_roots = tuple(policy.auxiliary_policy.get("externalRoots", ()))
231
+ unobserved: list[str] = []
232
+ if boundary == "none":
233
+ unobserved.append("provider-process-outside-audited-roots")
234
+ if external_roots:
235
+ unobserved.append("shared-tool-cache")
236
+ if policy.source_mode == "project-mutation":
237
+ unobserved.extend(("other-stage-refs", "shared-git-object-store"))
238
+ coverage: Literal["complete", "scoped", "partial"]
239
+ if exact:
240
+ coverage = "complete"
241
+ elif unobserved:
242
+ coverage = "partial"
243
+ else:
244
+ coverage = "scoped"
245
+ enforcement = WriteEnforcement(
246
+ boundary_precision=boundary,
247
+ mutation_audit="none" if exact else "batch",
248
+ attribution="call" if exact else "batch-unattributed",
249
+ audited_roots=_audited_roots(policy),
250
+ observation_coverage=coverage,
251
+ unobserved_write_surfaces=tuple(unobserved),
252
+ )
253
+ validate_write_enforcement_payload(enforcement.to_payload())
254
+ return enforcement
255
+
256
+
257
+ def write_policy_from_payload(payload: Mapping[str, Any]) -> WritePolicy:
258
+ validate_write_policy_payload(payload)
259
+ return WritePolicy(
260
+ artifact_policy=dict(payload["artifactPolicy"]),
261
+ source_policy=dict(payload["sourcePolicy"]),
262
+ git_policy=dict(payload["gitPolicy"]),
263
+ auxiliary_policy=dict(payload["auxiliaryPolicy"]),
264
+ )
265
+
266
+
267
+ def write_enforcement_from_payload(
268
+ payload: Mapping[str, Any],
269
+ ) -> WriteEnforcement:
270
+ validate_write_enforcement_payload(payload)
271
+ return WriteEnforcement(
272
+ boundary_precision=payload["boundaryPrecision"],
273
+ mutation_audit=payload["mutationAudit"],
274
+ attribution=payload["attribution"],
275
+ audited_roots=tuple(Path(value) for value in payload["auditedRoots"]),
276
+ observation_coverage=payload["observationCoverage"],
277
+ unobserved_write_surfaces=tuple(payload["unobservedWriteSurfaces"]),
278
+ )
279
+
280
+
281
+ def write_policy_digest(payload: Mapping[str, Any]) -> str:
282
+ validate_write_policy_payload(payload)
283
+ encoded = json.dumps(
284
+ payload,
285
+ sort_keys=True,
286
+ separators=(",", ":"),
287
+ ensure_ascii=False,
288
+ ).encode("utf-8")
289
+ return "sha256:" + hashlib.sha256(encoded).hexdigest()
290
+
291
+
292
+ def validate_write_policy_payload(payload: Mapping[str, Any]) -> None:
293
+ if set(payload) != _POLICY_KEYS:
294
+ raise WritePolicyError("writePolicy must contain four canonical policy sections")
295
+ artifact = _mapping(payload.get("artifactPolicy"), "artifactPolicy")
296
+ source = _mapping(payload.get("sourcePolicy"), "sourcePolicy")
297
+ git = _mapping(payload.get("gitPolicy"), "gitPolicy")
298
+ auxiliary = _mapping(payload.get("auxiliaryPolicy"), "auxiliaryPolicy")
299
+ if set(artifact) != {"allowedRoot", "allowedPaths"}:
300
+ raise WritePolicyError("artifactPolicy is incomplete")
301
+ _absolute_text(artifact.get("allowedRoot"), "artifactPolicy.allowedRoot")
302
+ _relative_paths(artifact.get("allowedPaths", ()))
303
+ if source.get("mode") not in {"source-readonly", "project-mutation"}:
304
+ raise WritePolicyError("sourcePolicy mode is invalid")
305
+ if set(source) != {"mode", "allowedRoot", "plannedPaths", "protectedPaths"}:
306
+ raise WritePolicyError("sourcePolicy is incomplete")
307
+ _absolute_text(source.get("allowedRoot"), "sourcePolicy.allowedRoot")
308
+ _relative_paths(source.get("plannedPaths", ()))
309
+ _relative_paths(source.get("protectedPaths", ()))
310
+ if git.get("mode") not in {"disabled", "fast-forward-descendant-chain"}:
311
+ raise WritePolicyError("gitPolicy mode is invalid")
312
+ if git.get("mode") == "disabled" and set(git) != {"mode"}:
313
+ raise WritePolicyError("disabled gitPolicy must not grant Git paths")
314
+ required_git = {
315
+ "mode", "worktreeGitDir", "gitCommonDir", "expectedBaseCommit",
316
+ "allowedBranchRef", "commitMode",
317
+ }
318
+ if git.get("mode") != "disabled" and set(git) != required_git:
319
+ raise WritePolicyError("project mutation gitPolicy is incomplete")
320
+ if set(auxiliary) != {"scratchRoots", "generatedPaths", "externalRoots"}:
321
+ raise WritePolicyError("auxiliaryPolicy is incomplete")
322
+ _relative_paths(auxiliary.get("generatedPaths", ()))
323
+ for key in ("scratchRoots", "externalRoots"):
324
+ for value in _text_sequence(auxiliary.get(key), f"auxiliaryPolicy.{key}"):
325
+ _absolute_text(value, f"auxiliaryPolicy.{key}")
326
+
327
+
328
+ def validate_write_enforcement_payload(payload: Mapping[str, Any]) -> None:
329
+ if set(payload) != _ENFORCEMENT_KEYS:
330
+ raise WritePolicyError("writeEnforcement is incomplete")
331
+ boundary = payload.get("boundaryPrecision")
332
+ mutation_audit = payload.get("mutationAudit")
333
+ attribution = payload.get("attribution")
334
+ coverage = payload.get("observationCoverage")
335
+ if boundary not in _PRECISION_RANK:
336
+ raise WritePolicyError("writeEnforcement boundaryPrecision is invalid")
337
+ if mutation_audit not in {"none", "batch"}:
338
+ raise WritePolicyError("writeEnforcement mutationAudit is invalid")
339
+ if attribution not in {"call", "batch-unattributed"}:
340
+ raise WritePolicyError("writeEnforcement attribution is invalid")
341
+ if coverage not in {"complete", "scoped", "partial"}:
342
+ raise WritePolicyError("writeEnforcement observationCoverage is invalid")
343
+ if (boundary, mutation_audit, attribution) == ("exact-path", "none", "call"):
344
+ if coverage != "complete":
345
+ raise WritePolicyError("exact enforcement must have complete coverage")
346
+ elif (boundary, mutation_audit, attribution) == (
347
+ "none",
348
+ "none",
349
+ "batch-unattributed",
350
+ ):
351
+ if coverage != "partial":
352
+ raise WritePolicyError("host-session enforcement must be partial")
353
+ elif mutation_audit != "batch" or attribution != "batch-unattributed":
354
+ raise WritePolicyError("non-exact enforcement must use batch attribution")
355
+ elif boundary == "exact-path" or coverage == "complete":
356
+ raise WritePolicyError("batch enforcement cannot claim exact coverage")
357
+ for root in _text_sequence(payload.get("auditedRoots"), "auditedRoots"):
358
+ _absolute_text(root, "auditedRoots")
359
+ _text_sequence(payload.get("unobservedWriteSurfaces"), "unobservedWriteSurfaces")
360
+
361
+
362
+ def _git_policy(
363
+ invocation: Mapping[str, Any], root: Path, source_mode: str
364
+ ) -> dict[str, Any]:
365
+ if source_mode == "source-readonly":
366
+ return {"mode": "disabled"}
367
+ git_dir = _git_path(root, "--git-dir")
368
+ common_dir = _git_path(root, "--git-common-dir")
369
+ expected_base = _required_text(invocation, "expectedBaseCommit")
370
+ allowed_ref = _required_text(invocation, "allowedBranchRef")
371
+ _require_commit(root, expected_base)
372
+ current_ref = _git_output(root, "symbolic-ref", "-q", "HEAD")
373
+ if current_ref != allowed_ref or not allowed_ref.startswith("refs/heads/"):
374
+ raise WritePolicyError("allowedBranchRef does not match assigned worktree")
375
+ return {
376
+ "mode": "fast-forward-descendant-chain",
377
+ "worktreeGitDir": str(git_dir),
378
+ "gitCommonDir": str(common_dir),
379
+ "expectedBaseCommit": expected_base,
380
+ "allowedBranchRef": allowed_ref,
381
+ "commitMode": "fast-forward-descendant-chain",
382
+ }
383
+
384
+
385
+ def _audited_roots(policy: WritePolicy) -> tuple[Path, ...]:
386
+ values = [
387
+ Path(str(policy.source_policy["allowedRoot"])),
388
+ Path(str(policy.artifact_policy["allowedRoot"])),
389
+ ]
390
+ values.extend(Path(value) for value in policy.auxiliary_policy.get("scratchRoots", ()))
391
+ git_dir = policy.git_policy.get("worktreeGitDir")
392
+ if isinstance(git_dir, str) and git_dir:
393
+ values.append(Path(git_dir))
394
+ seen: set[Path] = set()
395
+ roots: list[Path] = []
396
+ for value in values:
397
+ resolved = value.resolve()
398
+ if resolved not in seen:
399
+ roots.append(resolved)
400
+ seen.add(resolved)
401
+ return tuple(roots)
402
+
403
+
404
+ def _trusted_root(invocation: Mapping[str, Any], worktree: Path | None) -> Path:
405
+ raw = worktree if worktree is not None else Path(_required_text(invocation, "projectRoot"))
406
+ if not raw.is_absolute() or not raw.is_dir():
407
+ raise WritePolicyError("assigned worktree must be an existing absolute path")
408
+ return raw.resolve()
409
+
410
+
411
+ def _validated_auxiliary_roots(
412
+ invocation: Mapping[str, Any], assignment: Mapping[str, Any]
413
+ ) -> tuple[Path, ...]:
414
+ requested = _absolute_roots(invocation.get("auxiliaryRoots", ()))
415
+ declared = set(_absolute_roots(assignment.get("validatedAuxiliaryRoots", ())))
416
+ if any(path not in declared for path in requested):
417
+ raise WritePolicyError("auxiliary root is not adapter-validated")
418
+ return requested
419
+
420
+
421
+ def _assignment_precision(assignment: Mapping[str, Any]) -> BoundaryPrecision:
422
+ capability = assignment.get("workerWriteCapability")
423
+ if not isinstance(capability, Mapping):
424
+ raise WritePolicyError("workerWriteCapability is required")
425
+ value = capability.get("maxBoundaryPrecision")
426
+ if value not in _PRECISION_RANK:
427
+ raise WritePolicyError("workerWriteCapability is invalid")
428
+ return value
429
+
430
+
431
+ def _absolute_roots(value: object) -> tuple[Path, ...]:
432
+ roots: list[Path] = []
433
+ for text in _text_sequence(value, "absolute roots"):
434
+ path = Path(text)
435
+ if not path.is_absolute() or not path.exists():
436
+ raise WritePolicyError("auxiliary root must be an existing absolute path")
437
+ if path.absolute() != path.resolve():
438
+ raise WritePolicyError("auxiliary root contains a symlink component")
439
+ roots.append(path.resolve())
440
+ if len(set(roots)) != len(roots):
441
+ raise WritePolicyError("auxiliary roots must be unique")
442
+ return tuple(roots)
443
+
444
+
445
+ def _relative_to_root(path: Path, root: Path, label: str) -> str:
446
+ absolute = path if path.is_absolute() else root / path
447
+ try:
448
+ relative = absolute.absolute().relative_to(root).as_posix()
449
+ except ValueError as exc:
450
+ raise WritePolicyError(f"{label} path is outside project root") from exc
451
+ if not relative or relative == ".":
452
+ raise WritePolicyError(f"{label} path must name a file")
453
+ current = root
454
+ for part in PurePosixPath(relative).parts:
455
+ current /= part
456
+ if current.is_symlink():
457
+ raise WritePolicyError(f"{label} path contains a symlink component")
458
+ return relative
459
+
460
+
461
+ def _planned_paths_from_report(report_path: Path, stage: int) -> tuple[str, ...]:
462
+ payload = _read_json(report_path.with_suffix(".data.json"), "approved plan data")
463
+ planning = payload.get("implementationPlanning")
464
+ stages = planning.get("stages") if isinstance(planning, Mapping) else None
465
+ selected = next(
466
+ (
467
+ row for row in stages
468
+ if isinstance(row, Mapping) and row.get("stage") == stage
469
+ ),
470
+ None,
471
+ ) if isinstance(stages, list) else None
472
+ steps = selected.get("stepwiseExecution") if isinstance(selected, Mapping) else None
473
+ paths = {
474
+ part.strip().strip("`")
475
+ for step in steps if isinstance(step, Mapping)
476
+ for part in str(step.get("files") or "").replace("<br>", ",").split(",")
477
+ if part.strip()
478
+ } if isinstance(steps, list) else set()
479
+ if not paths:
480
+ raise WritePolicyError("implementer write policy stage has no planned paths")
481
+ return _relative_paths(tuple(sorted(paths)))
482
+
483
+
484
+ def _rooted(project_root: Path, value: str) -> Path:
485
+ path = Path(value)
486
+ return path if path.is_absolute() else project_root / path
487
+
488
+
489
+ def _read_json(path: Path, label: str) -> Mapping[str, Any]:
490
+ try:
491
+ value = json.loads(path.read_text(encoding="utf-8"))
492
+ except (OSError, UnicodeError, json.JSONDecodeError) as exc:
493
+ raise WritePolicyError(f"{label} is invalid: {path}") from exc
494
+ if not isinstance(value, Mapping):
495
+ raise WritePolicyError(f"{label} must be an object: {path}")
496
+ return value
497
+
498
+
499
+ def _relative_paths(value: object) -> tuple[str, ...]:
500
+ paths: list[str] = []
501
+ for raw in _text_sequence(value, "relative paths"):
502
+ candidate = PurePosixPath(raw)
503
+ if candidate.is_absolute() or not candidate.parts or ".." in candidate.parts:
504
+ raise WritePolicyError("policy paths must be normalized relative paths")
505
+ normalized = candidate.as_posix()
506
+ if normalized in {"", "."} or normalized != raw:
507
+ raise WritePolicyError("policy paths must be normalized relative paths")
508
+ paths.append(normalized)
509
+ if len(set(paths)) != len(paths):
510
+ raise WritePolicyError("policy paths must be unique")
511
+ return tuple(paths)
512
+
513
+
514
+ def _mapping(value: object, label: str) -> Mapping[str, Any]:
515
+ if not isinstance(value, Mapping):
516
+ raise WritePolicyError(f"{label} must be an object")
517
+ return value
518
+
519
+
520
+ def _text_sequence(value: object, label: str) -> tuple[str, ...]:
521
+ if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
522
+ raise WritePolicyError(f"{label} must be an array")
523
+ rows = tuple(value)
524
+ if any(not isinstance(row, str) or not row for row in rows):
525
+ raise WritePolicyError(f"{label} must contain non-empty strings")
526
+ return rows
527
+
528
+
529
+ def _required_text(payload: Mapping[str, Any], key: str) -> str:
530
+ value = payload.get(key)
531
+ if not isinstance(value, str) or not value.strip():
532
+ raise WritePolicyError(f"{key} must be a non-empty string")
533
+ return value
534
+
535
+
536
+ def _absolute_text(value: object, label: str) -> str:
537
+ if not isinstance(value, str) or not value or not Path(value).is_absolute():
538
+ raise WritePolicyError(f"{label} must be an absolute path")
539
+ return value
540
+
541
+
542
+ def _git_path(root: Path, flag: str) -> Path:
543
+ raw = _git_output(root, "rev-parse", "--path-format=absolute", flag)
544
+ path = Path(raw).resolve()
545
+ if not path.exists():
546
+ raise WritePolicyError(f"Git projection path does not exist: {path}")
547
+ return path
548
+
549
+
550
+ def _require_commit(root: Path, commit: str) -> None:
551
+ result = subprocess.run(
552
+ ["git", "-C", str(root), "cat-file", "-e", f"{commit}^{{commit}}"],
553
+ capture_output=True,
554
+ text=True,
555
+ check=False,
556
+ )
557
+ if result.returncode != 0:
558
+ raise WritePolicyError(result.stderr.strip() or "expected base is not a commit")
559
+
560
+
561
+ def _git_output(root: Path, *args: str) -> str:
562
+ result = subprocess.run(
563
+ ["git", "-C", str(root), *args],
564
+ capture_output=True,
565
+ text=True,
566
+ check=False,
567
+ )
568
+ if result.returncode != 0 or not result.stdout.strip():
569
+ detail = result.stderr.strip() or "Git command returned no value"
570
+ raise WritePolicyError(detail)
571
+ return result.stdout.strip()
@@ -57,7 +57,7 @@ def main() -> int:
57
57
  "from the freshly computed usageSummary, then re-render the "
58
58
  "sibling final-report markdown via the renderer. The data.json "
59
59
  "is the SSOT; the markdown is regenerated from it. "
60
- "See schemas/final-report-v1.0.schema.json for the data shape."
60
+ "See schemas/final-report-v2.0.schema.json for the data shape."
61
61
  ),
62
62
  )
63
63
  parser.add_argument(
@@ -21,6 +21,7 @@ from .antigravity import (
21
21
  )
22
22
  from .paths import claude_project_dir, utc_now
23
23
  from .pricing import antigravity_cost_usd, provider_cost_usd
24
+ from okstra_ctl.dispatch_state import worker_session_ids
24
25
  from okstra_ctl.models import provider_wrappers
25
26
  from okstra_ctl.wrapper_status import read_wrapper_status, status_path_for_prompt
26
27
 
@@ -864,22 +865,66 @@ def collect_claude_runtime_usage(
864
865
  f"lead session jsonl not found under {claude_project_dir(cwd)} (sessionId={lead_sid})"
865
866
  )
866
867
 
867
- # Workers — match by prefix and aggregate every session that belongs to
868
- # the same role (re-dispatches with `-002`, convergence `-reverify-r1`,
869
- # implementation `-executor`, report-writer `-impl` / `-2`, etc.).
868
+ # Workers — dispatch 가 기록한 세션 id 로 짚은 세션과 agentName prefix 로
869
+ # 찾은 세션의 합집합을 합산한다(재배치 `-002`, convergence `-reverify-r1`,
870
+ # implementation `-executor`, report-writer `-impl` / `-2` 등).
871
+ sessions_dir = claude_project_dir(cwd)
872
+ # sid 로 귀속한 세션 id 전체 — 아래 unattributed 폴드에서 빼는 데 쓴다.
873
+ attributed_sids: set[str] = set()
870
874
  for worker in state.get("workers", []):
871
875
  worker_id = worker.get("workerId")
872
876
  agent = worker.get("agent")
873
877
  prefixes = match_prefixes(worker_id) if worker_id else []
874
878
 
879
+ # pane 워커는 별도 `claude -p` 프로세스라 jsonl 에 agentName 도 teamName
880
+ # 도 안 남긴다 — 아래 prefix 경로로는 영원히 매칭되지 않는다. dispatch 가
881
+ # 발급해 적어 둔 이 id 가 그 세션을 짚는 유일한 결정적 단서다. 재시도는
882
+ # attempt 마다 새 세션이므로 기록된 id 를 전부 합산한다.
883
+ # 헬퍼에서 worker_id=None 은 "run 전체"라, 이름 없는 워커에 그대로 넘기면
884
+ # 그 워커가 run 의 모든 세션을 흡수한다.
885
+ dispatched_sids = worker_session_ids(state, worker_id) if worker_id else []
875
886
  matched: list[tuple[str, Path, dict]] = []
887
+ matched_sids: set[str] = set()
888
+ for sid in dispatched_sids:
889
+ path = sessions_dir / f"{sid}.jsonl"
890
+ if not path.is_file():
891
+ continue
892
+ totals = claude_session_totals(path, since=run_since, until=run_until,
893
+ incremental=incremental)
894
+ # 창 밖 세션은 여기서 버린다 — agentName 발견 경로가 위에서 같은
895
+ # `startedAt` 검사로 거르는 것과 같은 이유다. 넣으면 0 토큰 totals 가
896
+ # usage_block 을 타고 "이 워커는 0 을 썼다"로 보고되어, unavailable 로
897
+ # 남아야 할 상태를 허위 0 이 덮는다.
898
+ if not totals.get("startedAt"):
899
+ continue
900
+ matched.append((sid, path, totals))
901
+ matched_sids.add(sid)
902
+ attributed_sids.add(sid)
903
+
904
+ # 조건부 폴백이 아니라 합집합이다. 한 워커의 attempt 1 이 pane(sid 로만
905
+ # 찾힌다), attempt 2 가 in-process 서브에이전트(agentName 으로만 찾힌다)일
906
+ # 수 있고, `if not matched:` 로 두면 sid 가 하나라도 맞는 순간 attempt 2 의
907
+ # 토큰이 통째로 누락된다. 양쪽에서 온 같은 세션은 sid 로 한 번만 센다.
876
908
  for agent_name, entries in by_agent.items():
877
- if agent_matches(agent_name, prefixes):
878
- matched.extend(entries)
909
+ if not agent_matches(agent_name, prefixes):
910
+ continue
911
+ for entry in entries:
912
+ if entry[0] in matched_sids:
913
+ continue
914
+ # team-needle 경로는 창 검사 없이 by_agent 를 채운다. 같은 리드
915
+ # 세션의 다음 run 이 띄운 워커도 같은 needle 에 걸리는데, 창 밖이라
916
+ # startedAt 이 없어 아래 정렬에서 맨 앞에 서고 `sessionId` 를
917
+ # 차지한다 — 토큰은 0 이라 합계는 안 틀리고 리포트가 가리키는
918
+ # 세션만 남의 것이 된다.
919
+ if not entry[2].get("startedAt"):
920
+ continue
921
+ matched.append(entry)
922
+ matched_sids.add(entry[0])
879
923
 
880
924
  if not matched:
881
925
  worker["usage"] = na_block(
882
- f"no Claude subagent jsonl found with agentName matching prefixes {prefixes}"
926
+ "no Claude session jsonl in the run window for dispatched "
927
+ f"sessionIds {dispatched_sids} or agentName prefixes {prefixes}"
883
928
  )
884
929
  continue
885
930
 
@@ -919,6 +964,21 @@ def collect_claude_runtime_usage(
919
964
  # session the harness never tagged with `name`. Without this, the run-level
920
965
  # Worker total reads 0 and the report validator hard-fails a legitimate run.
921
966
  # Attribution is aggregate, not per-worker; usageSummary records it openly.
967
+ #
968
+ # 먼저 sid 로 귀속된 세션을 뺀다. 이 두 집합은 `agentName` 유무로 배타적이었지만
969
+ # (by_agent 는 있어야 들어가고 unattributed 는 없어야 들어간다) sid 경로는
970
+ # agentName 을 안 보므로 그 배타성이 더는 성립하지 않는다. team needle 에
971
+ # 걸리면서 agentName 이 없는 세션이 동시에 기록된 dispatch sid 이기도 하면,
972
+ # 빼지 않을 경우 그 토큰이 워커 usage 와 이 폴드 양쪽에 들어가 `_populate_usage_summary`
973
+ # 의 workerTotalTokens 를 부풀린다. 두 리스트는 인덱스가 대응하므로 함께 거른다.
974
+ kept = [
975
+ (sid, totals)
976
+ for sid, totals in zip(unattributed_sessions, unattributed_totals)
977
+ if sid not in attributed_sids
978
+ ]
979
+ unattributed_sessions = [sid for sid, _ in kept]
980
+ unattributed_totals = [totals for _, totals in kept]
981
+
922
982
  unattributed_usage = None
923
983
  if unattributed_totals:
924
984
  unattributed_usage = usage_block(