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
@@ -12,7 +12,7 @@ from __future__ import annotations
12
12
 
13
13
  import json
14
14
  import re
15
- from collections.abc import Mapping, Sequence
15
+ from collections.abc import Mapping
16
16
  from dataclasses import dataclass
17
17
  from pathlib import Path
18
18
 
@@ -45,25 +45,26 @@ _EVIDENCE_COMMAND_RE = re.compile(
45
45
  r"^- Evidence command:[ \t]*(?P<payload>.*)$", re.MULTILINE
46
46
  )
47
47
  _EVIDENCE_COMMAND_FIELDS = ("command", "cwd", "exitCode", "outputSummary")
48
- _EVIDENCE_COMMAND_STRING_FIELDS = ("command", "cwd", "outputSummary")
49
- _SENSITIVE_KEY_RE = re.compile(
50
- r"(?i)(authorization|auth|credential|pass(?:wd|word)?|pwd|token|secret|"
51
- r"api[_-]?key|access[_-]?key)"
52
- )
53
- _SENSITIVE_TEXT_RE = re.compile(
54
- r"(?i)(authorization\s*:\s*\S+|"
55
- r"\b(?:[A-Za-z_][A-Za-z0-9_-]*)?(?:auth|credential|pass(?:wd|word)?|pwd|"
56
- r"token|secret|api[_-]?key|access[_-]?key)[A-Za-z0-9_-]*\s*=|"
57
- r"--[\w-]*(?:auth|credential|pass(?:wd|word)?|pwd|token|secret|"
58
- r"api[-_]?key|access[-_]?key)[\w-]*(?:[ =])\S+|"
59
- r"\w+://[^\s:/@]+:[^\s@/]+@)"
60
- )
61
- _KNOWN_SECRET_RE = re.compile(
62
- r"(?:AKIA|ASIA)[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9_]{8,}|"
63
- r"github_pat_[A-Za-z0-9_]{8,}|xox[abprs]-[A-Za-z0-9-]{8,}|"
64
- r"\bsk-[A-Za-z0-9_-]{8,}\b|"
65
- r"\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b"
48
+ _EVIDENCE_COMMAND_IDENTITY_FIELDS = (
49
+ "participantRef",
50
+ "roleExecutionRef",
51
+ "invocationRef",
52
+ "attempt",
53
+ "executionLabel",
66
54
  )
55
+ _EVIDENCE_COMMAND_STRING_FIELDS = ("command", "cwd", "outputSummary")
56
+ # There is deliberately no secret detector here.
57
+ #
58
+ # One lived on this path and only ever cost runs. It matched the *name* half of
59
+ # an assignment, so `AUTH_MODE=off` and `TOKEN_TTL=60` were "sensitive material"
60
+ # and the row was refused; a worker that cannot record the command it ran has no
61
+ # way to satisfy the evidence ledger that demands it, and the run ends
62
+ # `contract-violated` over a switch. Judging the value half instead only moves
63
+ # the guesswork. What belongs in a row is the command's shape — the keys — and a
64
+ # value that must not be written down is the worker's to leave out or pass as a
65
+ # `$VAR` reference. A run does not need it: at execution time the value comes
66
+ # from the environment or from the person running it, and if it is absent the
67
+ # command fails then, where the failure is legible.
67
68
  _FILE_LINE_CITATION_RE = re.compile(
68
69
  r"`(?P<path>(?!https?://)[^`\n]+?):(?P<line>\d+(?:-\d+)?)`"
69
70
  )
@@ -116,34 +117,18 @@ class EvidenceCommand:
116
117
  cwd: str
117
118
  exit_code: int
118
119
  output_summary: str
120
+ identity: dict[str, object] | None = None
119
121
 
120
122
  def to_record(self) -> dict[str, object]:
121
- return {
123
+ record: dict[str, object] = {
122
124
  "command": self.command,
123
125
  "cwd": self.cwd,
124
126
  "exitCode": self.exit_code,
125
127
  "outputSummary": self.output_summary,
126
128
  }
127
-
128
-
129
- def _contains_sensitive_material(value: object) -> bool:
130
- if isinstance(value, Mapping):
131
- return any(
132
- (
133
- _SENSITIVE_KEY_RE.search(str(key)) is not None
134
- and nested not in (None, "", False)
135
- )
136
- or _contains_sensitive_material(nested)
137
- for key, nested in value.items()
138
- )
139
- if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
140
- return any(_contains_sensitive_material(item) for item in value)
141
- if not isinstance(value, str):
142
- return False
143
- return (
144
- _SENSITIVE_TEXT_RE.search(value) is not None
145
- or _KNOWN_SECRET_RE.search(value) is not None
146
- )
129
+ if self.identity:
130
+ record.update(self.identity)
131
+ return record
147
132
 
148
133
 
149
134
  def _evidence_command_contract_failures(
@@ -151,7 +136,9 @@ def _evidence_command_contract_failures(
151
136
  ) -> list[str]:
152
137
  failures: list[str] = []
153
138
  missing = [key for key in _EVIDENCE_COMMAND_FIELDS if key not in payload]
154
- unexpected = sorted(set(payload) - set(_EVIDENCE_COMMAND_FIELDS))
139
+ unexpected = sorted(
140
+ set(payload) - set(_EVIDENCE_COMMAND_FIELDS) - set(_EVIDENCE_COMMAND_IDENTITY_FIELDS)
141
+ )
155
142
  if missing:
156
143
  failures.append(
157
144
  f"Evidence command row {row_number} is missing {', '.join(missing)}"
@@ -173,21 +160,15 @@ def _evidence_command_contract_failures(
173
160
  return failures
174
161
 
175
162
 
176
- def _evidence_command_from_payload(
177
- payload: Mapping[str, object], sensitive: bool
178
- ) -> EvidenceCommand:
179
- if sensitive:
180
- return EvidenceCommand(
181
- command="[redacted: potential secret]",
182
- cwd="[redacted: potential secret]",
183
- exit_code=payload["exitCode"],
184
- output_summary="omitted because potential secret material was detected",
185
- )
163
+ def _evidence_command_from_payload(payload: Mapping[str, object]) -> EvidenceCommand:
164
+ from okstra_ctl.execution_identity import stored_identity
165
+
186
166
  return EvidenceCommand(
187
167
  command=payload["command"],
188
168
  cwd=payload["cwd"],
189
169
  exit_code=payload["exitCode"],
190
170
  output_summary=payload["outputSummary"],
171
+ identity=stored_identity(payload) or None,
191
172
  )
192
173
 
193
174
 
@@ -209,16 +190,11 @@ def parse_evidence_commands(
209
190
  f"Evidence command row {row_number} must be a JSON object"
210
191
  )
211
192
  continue
212
- sensitive = _contains_sensitive_material(payload)
213
- if sensitive:
214
- failures.append(
215
- f"Evidence command row {row_number} contains sensitive material"
216
- )
217
193
  contract_failures = _evidence_command_contract_failures(payload, row_number)
218
194
  failures.extend(contract_failures)
219
195
  if contract_failures:
220
196
  continue
221
- commands.append(_evidence_command_from_payload(payload, sensitive))
197
+ commands.append(_evidence_command_from_payload(payload))
222
198
  return tuple(commands), tuple(failures)
223
199
 
224
200
 
@@ -375,7 +351,26 @@ def check_worker_results_audit(
375
351
  *,
376
352
  worker: str | None = None,
377
353
  ) -> list[str]:
378
- """Every audit-sidecar contract failure among this run's worker results.
354
+ """Every audit-sidecar contract finding among this run's worker results.
355
+
356
+ Blocking findings first, then the advisory citation-ledger ones. Mid-run
357
+ callers want both in one list: the worker session is still alive and can fix
358
+ either. Phase 7 separates them — see `worker_results_audit_findings`.
359
+ """
360
+ blocking, advisories = worker_results_audit_findings(
361
+ run_dir, task_type, seq, worker=worker
362
+ )
363
+ return blocking + advisories
364
+
365
+
366
+ def worker_results_audit_findings(
367
+ run_dir: Path,
368
+ task_type: str,
369
+ seq: str | None,
370
+ *,
371
+ worker: str | None = None,
372
+ ) -> tuple[list[str], list[str]]:
373
+ """The same checks, split into `(blocking, advisory)`.
379
374
 
380
375
  *run_dir* is `runs/<task-type>/`; `worker-results/` and `prompts/` hang off
381
376
  it. *seq* scopes the check to one run — `worker-results/` accumulates every
@@ -383,16 +378,24 @@ def check_worker_results_audit(
383
378
  did not produce. Pass ``None`` only when the seq is genuinely unknown, which
384
379
  falls back to not filtering rather than silently checking nothing.
385
380
 
386
- For each result file this checks that it carries no `## 0. Reading
387
- Confirmation` heading (that block moved to the sidecar), that the matching
388
- sidecar exists, and — for prompts carrying the required-v1 evidence-ledger
389
- marker — that every backticked `path:line` citation has an Evidence read row.
381
+ Blocking: a result file that still carries the `## 0. Reading Confirmation`
382
+ heading (that block moved to the sidecar), a missing sidecar, and a
383
+ malformed Evidence command row. Each of those means the audit trail itself
384
+ is absent or unreadable.
385
+
386
+ Advisory: a `path:line` citation with no matching Evidence read row. The
387
+ ledger exists and is readable; what the check found is a citation whose
388
+ spelling does not match a row. That is worth reporting and not worth
389
+ voiding a finished run over — one unmatched citation among hundreds used to
390
+ take the whole phase to `contract-violated`, which stops the run without
391
+ telling anyone whether the finding itself was wrong.
390
392
  """
391
393
  failures: list[str] = []
394
+ advisories: list[str] = []
392
395
  if not (run_dir / "worker-results").is_dir():
393
396
  # No worker-results directory means no analysis workers ran (e.g.
394
397
  # `release-handoff`, which is single-lead). Nothing to enforce.
395
- return failures
398
+ return failures, advisories
396
399
 
397
400
  for path, worker_role, result_seq in worker_result_files(run_dir, task_type, seq, worker):
398
401
  rel = path.name
@@ -426,7 +429,7 @@ def check_worker_results_audit(
426
429
 
427
430
  _, command_failures = read_evidence_commands(audit_path)
428
431
  failures.extend(command_failures)
429
- failures.extend(_evidence_read_ledger_failures(
432
+ advisories.extend(_evidence_read_ledger_failures(
430
433
  run_dir=run_dir,
431
434
  worker_role=worker_role,
432
435
  task_type=task_type,
@@ -435,4 +438,4 @@ def check_worker_results_audit(
435
438
  result_content=content,
436
439
  audit_path=audit_path,
437
440
  ))
438
- return failures
441
+ return failures, advisories
@@ -14,6 +14,7 @@ from pathlib import Path
14
14
 
15
15
  from .domain.worker_exec import ExecutionPolicy, WorkerExecRequest
16
16
  from .domain.worker_role import role_spec
17
+ from .write_policy import WriteEnforcement, WritePolicy
17
18
 
18
19
  # Carried over from the codex wrapper, where it was one provider's rule: cargo's
19
20
  # package-cache flock and its registry live outside every workspace, so a
@@ -35,6 +36,9 @@ def build_request(
35
36
  worktree_path: Path | None,
36
37
  role: str,
37
38
  idle_timeout_seconds: int,
39
+ session_id: str = "",
40
+ write_policy: WritePolicy | None = None,
41
+ write_enforcement: WriteEnforcement | None = None,
38
42
  ) -> WorkerExecRequest:
39
43
  """One dispatch, with every value the strategies will not re-derive.
40
44
 
@@ -52,9 +56,13 @@ def build_request(
52
56
  # Nobody is at the keyboard to answer an approval prompt, so the gate is
53
57
  # open and the boundary comes from the write scope instead.
54
58
  policy=ExecutionPolicy(
55
- auto_approve=True, write_scope=write_scope(root, worktree, role)
59
+ auto_approve=True,
60
+ write_scope=write_scope(root, worktree, role),
61
+ write_policy=write_policy,
62
+ write_enforcement=write_enforcement,
56
63
  ),
57
64
  idle_timeout_seconds=idle_timeout_seconds,
65
+ session_id=session_id,
58
66
  )
59
67
 
60
68
 
@@ -21,7 +21,9 @@ from functools import partial
21
21
  from pathlib import Path
22
22
  from typing import Any, Callable, Mapping
23
23
 
24
+ from .domain.provider import ServedModelAttestation, ServedModelNormalizer
24
25
  from .domain.worker_exec import (
26
+ SERVED_MODEL_MISMATCH_EXIT_CODE,
25
27
  STREAM_JSON,
26
28
  ExecCommand,
27
29
  ExecutionStrategy,
@@ -67,7 +69,11 @@ def run_worker(
67
69
  log_path: Path,
68
70
  status_path: Path,
69
71
  status_extra: Mapping[str, Any] = _NO_STATUS_EXTRA,
72
+ served_model_normalizer: ServedModelNormalizer | None = None,
73
+ served_model_verifier: Callable[[ServedModelAttestation], str] | None = None,
70
74
  ) -> int:
75
+ _validate_status_extra(status_extra)
76
+ _validate_request_write_contract(request, status_extra)
71
77
  command = strategy.build_command(request)
72
78
  started_monotonic = time.monotonic()
73
79
  status = _started_status(status_extra, log_path)
@@ -76,7 +82,7 @@ def run_worker(
76
82
 
77
83
  try:
78
84
  with guard:
79
- exit_code, timed_out, idle_seconds = _launch(
85
+ exit_code, timed_out, idle_seconds, raw_model = _launch(
80
86
  command,
81
87
  log_path,
82
88
  presentation=presentation,
@@ -94,6 +100,14 @@ def run_worker(
94
100
  raise
95
101
 
96
102
  status = _closed(status, started_monotonic)
103
+ if served_model_normalizer is not None:
104
+ attestation = served_model_normalizer(raw_model)
105
+ status["servedModelAttestation"] = _attestation_payload(attestation)
106
+ if served_model_verifier is not None:
107
+ failure = served_model_verifier(attestation)
108
+ if failure:
109
+ status["failure"] = failure
110
+ exit_code = SERVED_MODEL_MISMATCH_EXIT_CODE
97
111
  status["exit_code"] = exit_code
98
112
  if timed_out:
99
113
  status.update(
@@ -106,6 +120,57 @@ def run_worker(
106
120
  return exit_code
107
121
 
108
122
 
123
+ def _validate_status_extra(status_extra: Mapping[str, Any]) -> None:
124
+ schema = status_extra.get("schemaVersion")
125
+ identity_version = status_extra.get("executionIdentityVersion")
126
+ identity_keys = {
127
+ "participantRef",
128
+ "roleExecutionRef",
129
+ "executionLabel",
130
+ "dutyId",
131
+ "invocationRef",
132
+ }
133
+ if schema is None and identity_version is None:
134
+ if set(status_extra) & (
135
+ identity_keys | {"attempt", "servedModelAttestation"}
136
+ ):
137
+ raise ValueError("worker status mixes v1 and v2 execution identity")
138
+ return
139
+ if schema != "2.0" or identity_version != 2:
140
+ raise ValueError("worker status mixes v1 and v2 execution identity")
141
+ if any(
142
+ not isinstance(status_extra.get(key), str) or not status_extra[key]
143
+ for key in identity_keys
144
+ ):
145
+ raise ValueError("worker status v2 execution identity is incomplete")
146
+ attempt = status_extra.get("attempt")
147
+ if not isinstance(attempt, int) or isinstance(attempt, bool) or attempt < 1:
148
+ raise ValueError("worker status v2 attempt must be positive")
149
+ attestation = status_extra.get("servedModelAttestation")
150
+ if not isinstance(attestation, Mapping) or set(attestation) != {
151
+ "observedModel",
152
+ "normalizedModelRef",
153
+ "level",
154
+ "source",
155
+ }:
156
+ raise ValueError("worker status served model attestation is invalid")
157
+
158
+
159
+ def _validate_request_write_contract(
160
+ request: WorkerExecRequest, status_extra: Mapping[str, Any]
161
+ ) -> None:
162
+ if status_extra.get("executionIdentityVersion") != 2:
163
+ return
164
+ policy = request.policy.write_policy
165
+ enforcement = request.policy.write_enforcement
166
+ if policy is None or enforcement is None:
167
+ raise ValueError("v2 worker request has no write contract")
168
+ if status_extra.get("writePolicyDigest") != policy.digest:
169
+ raise ValueError("v2 worker request write policy digest changed")
170
+ if status_extra.get("writeEnforcement") != enforcement.to_payload():
171
+ raise ValueError("v2 worker request write enforcement changed")
172
+
173
+
109
174
  def _launch(
110
175
  command: ExecCommand,
111
176
  log_path: Path,
@@ -113,7 +178,7 @@ def _launch(
113
178
  presentation: str,
114
179
  idle_timeout_seconds: int,
115
180
  on_spawn: Callable[[subprocess.Popen[bytes]], None],
116
- ) -> tuple[int, bool, int]:
181
+ ) -> tuple[int, bool, int, str | None]:
117
182
  log_path.parent.mkdir(parents=True, exist_ok=True)
118
183
  with log_path.open("w", encoding="utf-8") as log_file:
119
184
  # The strategy decided where this provider runs — some CLIs work in the
@@ -128,7 +193,8 @@ def _launch(
128
193
  env=_child_env(),
129
194
  )
130
195
  on_spawn(process)
131
- return _pump(
196
+ observation = _ServedModelObservation()
197
+ exit_code, timed_out, idle_seconds = _pump(
132
198
  process,
133
199
  log_file,
134
200
  stream_format=command.stream_format,
@@ -136,7 +202,19 @@ def _launch(
136
202
  presentation=presentation,
137
203
  idle_timeout_seconds=idle_timeout_seconds,
138
204
  stdin_text=command.stdin_text,
205
+ observe_served_model=command.observe_served_model,
206
+ model_observation=observation,
139
207
  )
208
+ return exit_code, timed_out, idle_seconds, observation.raw_model
209
+
210
+
211
+ class _ServedModelObservation:
212
+ def __init__(self) -> None:
213
+ self.raw_model: str | None = None
214
+
215
+ def record(self, raw_model: str | None) -> None:
216
+ if self.raw_model is None and isinstance(raw_model, str) and raw_model.strip():
217
+ self.raw_model = raw_model
140
218
 
141
219
 
142
220
  class _AbnormalExit:
@@ -253,6 +331,8 @@ def _pump(
253
331
  presentation: str,
254
332
  idle_timeout_seconds: int,
255
333
  stdin_text: str | None = None,
334
+ observe_served_model: Callable[[Mapping[str, Any]], str | None],
335
+ model_observation: _ServedModelObservation,
256
336
  ) -> tuple[int, bool, int]:
257
337
  selector = selectors.DefaultSelector()
258
338
  readers, finalize_log = _register_output(
@@ -261,6 +341,8 @@ def _pump(
261
341
  log_file,
262
342
  stream_format=stream_format,
263
343
  normalise=normalise,
344
+ observe_served_model=observe_served_model,
345
+ model_observation=model_observation,
264
346
  presentation=presentation,
265
347
  )
266
348
  outgoing = _register_prompt(selector, process, stdin_text)
@@ -357,6 +439,8 @@ def _register_output(
357
439
  *,
358
440
  stream_format: str,
359
441
  normalise: Normalise,
442
+ observe_served_model: Callable[[Mapping[str, Any]], str | None],
443
+ model_observation: _ServedModelObservation,
360
444
  presentation: str,
361
445
  ) -> tuple[dict[int, _LineReader], Callable[[], None]]:
362
446
  """Register every stream this child speaks, each with its own destination.
@@ -367,7 +451,17 @@ def _register_output(
367
451
  assert process.stdout is not None
368
452
  if stream_format == STREAM_JSON:
369
453
  sinks = [
370
- (process.stdout, partial(_emit_event, log_file, presentation, normalise))
454
+ (
455
+ process.stdout,
456
+ partial(
457
+ _emit_event,
458
+ log_file,
459
+ presentation,
460
+ normalise,
461
+ observe_served_model,
462
+ model_observation,
463
+ ),
464
+ )
371
465
  ]
372
466
  finalize_log: Callable[[], None] = _nothing_to_finalize
373
467
  else:
@@ -519,7 +613,12 @@ class _ProgressSink:
519
613
 
520
614
 
521
615
  def _emit_event(
522
- log_file, presentation: str, normalise: Normalise, line: str
616
+ log_file,
617
+ presentation: str,
618
+ normalise: Normalise,
619
+ observe_served_model: Callable[[Mapping[str, Any]], str | None],
620
+ model_observation: _ServedModelObservation,
621
+ line: str,
523
622
  ) -> str | None:
524
623
  """Write one line of a JSON stream out; return closing text if this is it.
525
624
 
@@ -542,6 +641,7 @@ def _emit_event(
542
641
  return None
543
642
  if not isinstance(event, dict):
544
643
  return None
644
+ model_observation.record(observe_served_model(event))
545
645
 
546
646
  closing: str | None = None
547
647
  for entry in normalise(event):
@@ -560,6 +660,15 @@ def _emit_event(
560
660
  return closing
561
661
 
562
662
 
663
+ def _attestation_payload(attestation: ServedModelAttestation) -> dict[str, Any]:
664
+ return {
665
+ "observedModel": attestation.observed_model,
666
+ "normalizedModelRef": attestation.normalized_model_ref,
667
+ "level": attestation.level,
668
+ "source": attestation.source,
669
+ }
670
+
671
+
563
672
  def _write_log(log_file, lines) -> None:
564
673
  for line in lines:
565
674
  log_file.write(line + "\n")
@@ -605,6 +714,22 @@ def _child_env() -> dict[str, str]:
605
714
  return env
606
715
 
607
716
 
717
+ def write_host_event(
718
+ path: Path,
719
+ *,
720
+ invocation_ref: str,
721
+ attempt: int,
722
+ event_type: str,
723
+ payload: Mapping[str, Any],
724
+ ) -> Mapping[str, Any]:
725
+ """Append one orchestrator-owned host event from the runner boundary."""
726
+ from .attempt_evidence import HostEventStreamWriter
727
+
728
+ return HostEventStreamWriter(
729
+ path, invocation_ref=invocation_ref, attempt=attempt
730
+ ).append(event_type, payload)
731
+
732
+
608
733
  def _write_status(path: Path, status: Mapping[str, Any]) -> None:
609
734
  """Best-effort status write; a sidecar failure must not break the run.
610
735
 
@@ -1,4 +1,4 @@
1
- """Worker roster resolution.
1
+ """V1 provider-worker roster compatibility parsing.
2
2
 
3
3
  Profile 파일에서 권장 worker 목록을 뽑고(`resolve_profile_workers`), 사용자
4
4
  오버라이드와 합쳐 정규화한다(`normalize_workers`). bash workers.sh 의
@@ -28,8 +28,10 @@ class WorkersError(Exception):
28
28
  """invalid worker selection — surface to user."""
29
29
 
30
30
 
31
- def _resolve_workers_under(profile_path: Path, headers: set[str]) -> list[str]:
32
- """Generic parser: collect ` - <id> …` sub-bullets under any of `headers`
31
+ def _resolve_v1_workers_under(profile_path: Path, headers: set[str]) -> list[str]:
32
+ """Read legacy provider-worker bullets without interpreting role YAML.
33
+
34
+ Collect ` - <id> …` sub-bullets under any of `headers`
33
35
  until the next top-level bullet. Returns first token (before any
34
36
  ` — ` / ` -- ` / whitespace) of each captured line.
35
37
  """
@@ -61,7 +63,7 @@ def resolve_profile_workers(profile_path: Path) -> list[str]:
61
63
  `- Workers:` / `- Reviewers:` / `- Analysers:`) 섹션 아래 sub-bullet 들을
62
64
  worker id 리스트로 돌려준다. profile 파일이 없거나 섹션이 없으면 빈 리스트.
63
65
  """
64
- return _resolve_workers_under(profile_path, PROFILE_BULLET_HEADERS)
66
+ return _resolve_v1_workers_under(profile_path, PROFILE_BULLET_HEADERS)
65
67
 
66
68
 
67
69
  def resolve_optional_workers(profile_path: Path) -> list[str]:
@@ -69,7 +71,7 @@ def resolve_optional_workers(profile_path: Path) -> list[str]:
69
71
  들에서 worker id 만 추출한다. (` - antigravity — when added …` → `antigravity`)
70
72
  Required 와 중복되는 항목은 제거. ALLOWED_WORKERS 밖 토큰도 제거.
71
73
  """
72
- raw = _resolve_workers_under(profile_path, PROFILE_BULLET_HEADERS_OPTIONAL)
74
+ raw = _resolve_v1_workers_under(profile_path, PROFILE_BULLET_HEADERS_OPTIONAL)
73
75
  required = set(resolve_profile_workers(profile_path))
74
76
  seen: set[str] = set()
75
77
  out: list[str] = []
@@ -77,6 +77,27 @@ DEFAULT_WORKTREE_SYNC_DIRS: tuple[str, ...] = (
77
77
  )
78
78
 
79
79
 
80
+ # Sync dirs materialised as a REAL directory whose children are symlinked one
81
+ # by one, instead of a single symlink standing in for the whole directory.
82
+ #
83
+ # Why the split exists: git does not follow a symlink, so a symlinked directory
84
+ # is one *file* to it. A project that ignores host config by its contents
85
+ # (`.claude/*`) matches every child but never the bare `.claude` path, so the
86
+ # symlink lands in `git status` as
87
+ # `?? .claude` while the same directory is invisible in the main checkout. Any
88
+ # plan step asserting a clean worktree then fails on okstra's own provisioning.
89
+ # Linking the children instead reproduces the main checkout's shape, so
90
+ # whatever the project's ignore rules do there, they do here too.
91
+ #
92
+ # Only `.claude` qualifies. The other sync dirs are shared okstra state that
93
+ # okstra WRITES into, and a directory symlink is what makes a newly created
94
+ # top-level entry land in the main checkout rather than diverging inside the
95
+ # worktree. `.claude` is host configuration okstra reads; its one okstra write
96
+ # is the fixed `settings.local.json` child seeded by
97
+ # `_seed_worktree_settings_symlink`.
98
+ CHILD_LINKED_SYNC_DIRS: tuple[str, ...] = (".claude",)
99
+
100
+
80
101
  # Project-root-relative FILES (not dirs) symlinked from MAIN → task worktree
81
102
  # at provision time. Same symlink semantics as `DEFAULT_WORKTREE_SYNC_DIRS`:
82
103
  # every task sees the live shared file. The split exists because the original
@@ -522,16 +543,27 @@ def is_ancestor(cwd, commit: str, head: str) -> bool:
522
543
  return _git(Path(cwd), "merge-base", "--is-ancestor", commit, head).returncode == 0
523
544
 
524
545
 
525
- def is_dirty_excluding_okstra(cwd) -> bool:
526
- """True iff the worktree has changes outside okstra-owned paths.
546
+ def dirty_entries_excluding_okstra(cwd) -> list[str]:
547
+ """`git status --short` rows for changes outside okstra-owned paths.
527
548
 
528
549
  okstra-owned = `okstra_clean_gate_excludes` (e.g. `.okstra`, synced dirs)
529
550
  plus `nested_worktree_excludes` (stage worktrees nested under `cwd`).
551
+
552
+ This is the clean-worktree question every okstra gate asks, and the one a
553
+ plan step must ask through `okstra worktree-status` rather than through a
554
+ bare `git status --porcelain`: okstra provisions `.okstra` plus the synced
555
+ entries into every task worktree, so a bare status is never empty there and
556
+ an assertion built on it fails on okstra's own scaffolding.
530
557
  """
531
558
  owned = (*okstra_clean_gate_excludes(Path(cwd)), *nested_worktree_excludes(cwd))
532
559
  excludes = [f":(exclude){p}" for p in owned]
533
560
  out = _git(Path(cwd), "status", "--short", "--", ".", *excludes).stdout
534
- return bool(out.strip())
561
+ return [line for line in out.splitlines() if line.strip()]
562
+
563
+
564
+ def is_dirty_excluding_okstra(cwd) -> bool:
565
+ """True iff the worktree has changes outside okstra-owned paths."""
566
+ return bool(dirty_entries_excluding_okstra(cwd))
535
567
 
536
568
 
537
569
  class MergeError(RuntimeError):
@@ -580,6 +612,9 @@ def _link_sync_dirs(source_root: Path, worktree_path: Path) -> list[str]:
580
612
  version-controlled files.
581
613
  - Parent directories are created as needed for nested entries.
582
614
 
615
+ Entries listed in `CHILD_LINKED_SYNC_DIRS` are materialised as a real
616
+ directory of per-child symlinks instead (see that constant).
617
+
583
618
  Returns a list of human-readable notes (one per linked entry) so the
584
619
  caller can include them in the provisioning note.
585
620
  """
@@ -592,6 +627,10 @@ def _link_sync_dirs(source_root: Path, worktree_path: Path) -> list[str]:
592
627
  if dst.exists() or dst.is_symlink():
593
628
  continue
594
629
  dst.parent.mkdir(parents=True, exist_ok=True)
630
+ if rel in CHILD_LINKED_SYNC_DIRS and src.is_dir():
631
+ _link_dir_children(src, dst)
632
+ notes.append(rel)
633
+ continue
595
634
  try:
596
635
  os.symlink(src, dst)
597
636
  except FileExistsError:
@@ -600,6 +639,33 @@ def _link_sync_dirs(source_root: Path, worktree_path: Path) -> list[str]:
600
639
  return notes
601
640
 
602
641
 
642
+ def _link_dir_children(src: Path, dst: Path) -> None:
643
+ """Create `dst` as a real directory whose entries symlink to `src`'s
644
+ children, so git sees the same directory shape it sees in the main
645
+ checkout (`CHILD_LINKED_SYNC_DIRS`).
646
+
647
+ Each link points at the MAIN checkout's child rather than its resolved
648
+ target, so a child that is itself a symlink (e.g. `.claude/settings.local.json`
649
+ → `~/.okstra/templates/settings.local.json`) keeps following whatever the
650
+ main checkout currently points at. An unreadable source or a child that
651
+ cannot be linked degrades to "that child is absent in this worktree" —
652
+ provisioning is not worth failing over host config.
653
+ """
654
+ dst.mkdir(parents=True, exist_ok=True)
655
+ try:
656
+ children = sorted(src.iterdir())
657
+ except OSError:
658
+ return
659
+ for child in children:
660
+ link = dst / child.name
661
+ if link.exists() or link.is_symlink():
662
+ continue
663
+ try:
664
+ os.symlink(child, link)
665
+ except OSError:
666
+ continue
667
+
668
+
603
669
  def _link_sync_files(source_root: Path, worktree_path: Path) -> list[str]:
604
670
  """File-level counterpart to `_link_sync_dirs` (FU-V2).
605
671