okstra 0.174.0 → 0.176.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. package/README.md +18 -9
  2. package/bin/okstra +8 -4
  3. package/dist/cli-registry.d.mts +16 -0
  4. package/dist/cli-registry.mjs +560 -0
  5. package/dist/cli-registry.mjs.map +1 -0
  6. package/dist/commands/execute/agent-prompt.d.mts +1 -0
  7. package/{src → dist}/commands/execute/agent-prompt.mjs +10 -11
  8. package/dist/commands/execute/agent-prompt.mjs.map +1 -0
  9. package/dist/commands/execute/codex-dispatch.d.mts +3 -0
  10. package/dist/commands/execute/codex-dispatch.mjs +6 -0
  11. package/dist/commands/execute/codex-dispatch.mjs.map +1 -0
  12. package/dist/commands/execute/codex-run.d.mts +3 -0
  13. package/{src → dist}/commands/execute/codex-run.mjs +30 -37
  14. package/dist/commands/execute/codex-run.mjs.map +1 -0
  15. package/dist/commands/execute/convergence.d.mts +1 -0
  16. package/{src → dist}/commands/execute/convergence.mjs +15 -16
  17. package/dist/commands/execute/convergence.mjs.map +1 -0
  18. package/dist/commands/execute/error-log.d.mts +1 -0
  19. package/{src → dist}/commands/execute/error-log.mjs +2 -3
  20. package/dist/commands/execute/error-log.mjs.map +1 -0
  21. package/dist/commands/execute/git-reconcile.d.mts +1 -0
  22. package/{src → dist}/commands/execute/git-reconcile.mjs +10 -11
  23. package/dist/commands/execute/git-reconcile.mjs.map +1 -0
  24. package/dist/commands/execute/handoff.d.mts +1 -0
  25. package/{src → dist}/commands/execute/handoff.mjs +10 -11
  26. package/dist/commands/execute/handoff.mjs.map +1 -0
  27. package/dist/commands/execute/incremental-carry.d.mts +1 -0
  28. package/{src → dist}/commands/execute/incremental-carry.mjs +2 -3
  29. package/dist/commands/execute/incremental-carry.mjs.map +1 -0
  30. package/dist/commands/execute/incremental-scope.d.mts +1 -0
  31. package/{src → dist}/commands/execute/incremental-scope.mjs +2 -3
  32. package/dist/commands/execute/incremental-scope.mjs.map +1 -0
  33. package/dist/commands/execute/integrate-stages.d.mts +1 -0
  34. package/{src → dist}/commands/execute/integrate-stages.mjs +10 -11
  35. package/dist/commands/execute/integrate-stages.mjs.map +1 -0
  36. package/dist/commands/execute/pane-title.d.mts +1 -0
  37. package/dist/commands/execute/pane-title.mjs +20 -0
  38. package/dist/commands/execute/pane-title.mjs.map +1 -0
  39. package/dist/commands/execute/plan-items.d.mts +1 -0
  40. package/dist/commands/execute/plan-items.mjs +9 -0
  41. package/dist/commands/execute/plan-items.mjs.map +1 -0
  42. package/dist/commands/execute/plan-validate.d.mts +1 -0
  43. package/dist/commands/execute/plan-validate.mjs +68 -0
  44. package/dist/commands/execute/plan-validate.mjs.map +1 -0
  45. package/dist/commands/execute/plan-verify.d.mts +1 -0
  46. package/{src → dist}/commands/execute/plan-verify.mjs +13 -16
  47. package/dist/commands/execute/plan-verify.mjs.map +1 -0
  48. package/dist/commands/execute/render-bundle.d.mts +3 -0
  49. package/{src → dist}/commands/execute/render-bundle.mjs +65 -61
  50. package/dist/commands/execute/render-bundle.mjs.map +1 -0
  51. package/dist/commands/execute/run.d.mts +24 -0
  52. package/dist/commands/execute/run.mjs +196 -0
  53. package/dist/commands/execute/run.mjs.map +1 -0
  54. package/dist/commands/execute/spawn-followups.d.mts +1 -0
  55. package/{src → dist}/commands/execute/spawn-followups.mjs +6 -7
  56. package/dist/commands/execute/spawn-followups.mjs.map +1 -0
  57. package/dist/commands/execute/team.d.mts +3 -0
  58. package/{src → dist}/commands/execute/team.mjs +29 -36
  59. package/dist/commands/execute/team.mjs.map +1 -0
  60. package/dist/commands/execute/token-usage.d.mts +1 -0
  61. package/{src → dist}/commands/execute/token-usage.mjs +2 -3
  62. package/dist/commands/execute/token-usage.mjs.map +1 -0
  63. package/dist/commands/execute/wizard.d.mts +8 -0
  64. package/dist/commands/execute/wizard.mjs +191 -0
  65. package/dist/commands/execute/wizard.mjs.map +1 -0
  66. package/dist/commands/execute/worker-audit-check.d.mts +1 -0
  67. package/{src → dist}/commands/execute/worker-audit-check.mjs +11 -12
  68. package/dist/commands/execute/worker-audit-check.mjs.map +1 -0
  69. package/dist/commands/execute/worker-dispatch.d.mts +7 -0
  70. package/{src → dist}/commands/execute/worker-dispatch.mjs +34 -46
  71. package/dist/commands/execute/worker-dispatch.mjs.map +1 -0
  72. package/dist/commands/execute/worker-state.d.mts +1 -0
  73. package/{src → dist}/commands/execute/worker-state.mjs +15 -16
  74. package/dist/commands/execute/worker-state.mjs.map +1 -0
  75. package/dist/commands/execute/worktree-lookup.d.mts +1 -0
  76. package/{src → dist}/commands/execute/worktree-lookup.mjs +38 -37
  77. package/dist/commands/execute/worktree-lookup.mjs.map +1 -0
  78. package/dist/commands/execute/worktree-status.d.mts +1 -0
  79. package/{src → dist}/commands/execute/worktree-status.mjs +52 -46
  80. package/dist/commands/execute/worktree-status.mjs.map +1 -0
  81. package/dist/commands/inspect/code-review.d.mts +1 -0
  82. package/{src → dist}/commands/inspect/code-review.mjs +16 -19
  83. package/dist/commands/inspect/code-review.mjs.map +1 -0
  84. package/dist/commands/inspect/container.d.mts +1 -0
  85. package/{src → dist}/commands/inspect/container.mjs +11 -13
  86. package/dist/commands/inspect/container.mjs.map +1 -0
  87. package/dist/commands/inspect/context-cost.d.mts +1 -0
  88. package/{src → dist}/commands/inspect/context-cost.mjs +11 -13
  89. package/dist/commands/inspect/context-cost.mjs.map +1 -0
  90. package/dist/commands/inspect/design-prep.d.mts +1 -0
  91. package/{src → dist}/commands/inspect/design-prep.mjs +10 -11
  92. package/dist/commands/inspect/design-prep.mjs.map +1 -0
  93. package/dist/commands/inspect/error-report.d.mts +1 -0
  94. package/{src → dist}/commands/inspect/error-report.mjs +11 -13
  95. package/dist/commands/inspect/error-report.mjs.map +1 -0
  96. package/dist/commands/inspect/error-zip.d.mts +1 -0
  97. package/{src → dist}/commands/inspect/error-zip.mjs +11 -12
  98. package/dist/commands/inspect/error-zip.mjs.map +1 -0
  99. package/dist/commands/inspect/log-report.d.mts +1 -0
  100. package/{src → dist}/commands/inspect/log-report.mjs +11 -13
  101. package/dist/commands/inspect/log-report.mjs.map +1 -0
  102. package/dist/commands/inspect/profile-show.d.mts +1 -0
  103. package/{src → dist}/commands/inspect/profile-show.mjs +10 -11
  104. package/dist/commands/inspect/profile-show.mjs.map +1 -0
  105. package/dist/commands/inspect/recap.d.mts +1 -0
  106. package/{src → dist}/commands/inspect/recap.mjs +11 -13
  107. package/dist/commands/inspect/recap.mjs.map +1 -0
  108. package/dist/commands/inspect/resolve-task-key.d.mts +1 -0
  109. package/{src → dist}/commands/inspect/resolve-task-key.mjs +11 -13
  110. package/dist/commands/inspect/resolve-task-key.mjs.map +1 -0
  111. package/dist/commands/inspect/rollup.d.mts +1 -0
  112. package/{src → dist}/commands/inspect/rollup.mjs +11 -13
  113. package/dist/commands/inspect/rollup.mjs.map +1 -0
  114. package/dist/commands/inspect/run-audit.d.mts +1 -0
  115. package/{src → dist}/commands/inspect/run-audit.mjs +11 -12
  116. package/dist/commands/inspect/run-audit.mjs.map +1 -0
  117. package/dist/commands/inspect/set-work-status.d.mts +1 -0
  118. package/{src → dist}/commands/inspect/set-work-status.mjs +11 -13
  119. package/dist/commands/inspect/set-work-status.mjs.map +1 -0
  120. package/dist/commands/inspect/stage-map.d.mts +1 -0
  121. package/dist/commands/inspect/stage-map.mjs +110 -0
  122. package/dist/commands/inspect/stage-map.mjs.map +1 -0
  123. package/dist/commands/inspect/task-list.d.mts +1 -0
  124. package/dist/commands/inspect/task-list.mjs +103 -0
  125. package/dist/commands/inspect/task-list.mjs.map +1 -0
  126. package/dist/commands/inspect/task-show.d.mts +1 -0
  127. package/dist/commands/inspect/task-show.mjs +108 -0
  128. package/dist/commands/inspect/task-show.mjs.map +1 -0
  129. package/dist/commands/inspect/time-report.d.mts +1 -0
  130. package/{src → dist}/commands/inspect/time-report.mjs +11 -13
  131. package/dist/commands/inspect/time-report.mjs.map +1 -0
  132. package/dist/commands/inspect/usage-report.d.mts +1 -0
  133. package/{src → dist}/commands/inspect/usage-report.mjs +11 -13
  134. package/dist/commands/inspect/usage-report.mjs.map +1 -0
  135. package/dist/commands/inspect/user-response.d.mts +1 -0
  136. package/{src → dist}/commands/inspect/user-response.mjs +10 -11
  137. package/dist/commands/inspect/user-response.mjs.map +1 -0
  138. package/dist/commands/inspect/worker-liveness.d.mts +1 -0
  139. package/{src → dist}/commands/inspect/worker-liveness.mjs +11 -13
  140. package/dist/commands/inspect/worker-liveness.mjs.map +1 -0
  141. package/dist/commands/lifecycle/check-project.d.mts +21 -0
  142. package/dist/commands/lifecycle/check-project.mjs +175 -0
  143. package/dist/commands/lifecycle/check-project.mjs.map +1 -0
  144. package/dist/commands/lifecycle/config.d.mts +1 -0
  145. package/dist/commands/lifecycle/config.mjs +369 -0
  146. package/dist/commands/lifecycle/config.mjs.map +1 -0
  147. package/dist/commands/lifecycle/doctor.d.mts +10 -0
  148. package/dist/commands/lifecycle/doctor.mjs +317 -0
  149. package/dist/commands/lifecycle/doctor.mjs.map +1 -0
  150. package/dist/commands/lifecycle/install.d.mts +17 -0
  151. package/dist/commands/lifecycle/install.mjs +1077 -0
  152. package/dist/commands/lifecycle/install.mjs.map +1 -0
  153. package/dist/commands/lifecycle/migrate.d.mts +1 -0
  154. package/{src → dist}/commands/lifecycle/migrate.mjs +10 -11
  155. package/dist/commands/lifecycle/migrate.mjs.map +1 -0
  156. package/dist/commands/lifecycle/model.d.mts +1 -0
  157. package/dist/commands/lifecycle/model.mjs +22 -0
  158. package/dist/commands/lifecycle/model.mjs.map +1 -0
  159. package/dist/commands/lifecycle/paths.d.mts +1 -0
  160. package/dist/commands/lifecycle/paths.mjs +102 -0
  161. package/dist/commands/lifecycle/paths.mjs.map +1 -0
  162. package/dist/commands/lifecycle/preflight.d.mts +1 -0
  163. package/dist/commands/lifecycle/preflight.mjs +124 -0
  164. package/dist/commands/lifecycle/preflight.mjs.map +1 -0
  165. package/dist/commands/lifecycle/setup.d.mts +1 -0
  166. package/dist/commands/lifecycle/setup.mjs +297 -0
  167. package/dist/commands/lifecycle/setup.mjs.map +1 -0
  168. package/dist/commands/lifecycle/uninstall.d.mts +4 -0
  169. package/dist/commands/lifecycle/uninstall.mjs +262 -0
  170. package/dist/commands/lifecycle/uninstall.mjs.map +1 -0
  171. package/dist/commands/manager.d.mts +3 -0
  172. package/{src → dist}/commands/manager.mjs +25 -32
  173. package/dist/commands/manager.mjs.map +1 -0
  174. package/dist/commands/memory/memory.d.mts +1 -0
  175. package/dist/commands/memory/memory.mjs +569 -0
  176. package/dist/commands/memory/memory.mjs.map +1 -0
  177. package/dist/commands/pr/pr.d.mts +33 -0
  178. package/dist/commands/pr/pr.mjs +296 -0
  179. package/dist/commands/pr/pr.mjs.map +1 -0
  180. package/dist/commands/report/agent-activity.d.mts +1 -0
  181. package/{src → dist}/commands/report/agent-activity.mjs +11 -12
  182. package/dist/commands/report/agent-activity.mjs.map +1 -0
  183. package/dist/commands/report/finalize.d.mts +4 -0
  184. package/{src → dist}/commands/report/finalize.mjs +25 -32
  185. package/dist/commands/report/finalize.mjs.map +1 -0
  186. package/dist/commands/report/inject-report-index.d.mts +1 -0
  187. package/{src → dist}/commands/report/inject-report-index.mjs +6 -7
  188. package/dist/commands/report/inject-report-index.mjs.map +1 -0
  189. package/dist/commands/report/render-final-report.d.mts +1 -0
  190. package/{src → dist}/commands/report/render-final-report.mjs +6 -7
  191. package/dist/commands/report/render-final-report.mjs.map +1 -0
  192. package/dist/commands/report/render-views.d.mts +1 -0
  193. package/{src → dist}/commands/report/render-views.mjs +6 -7
  194. package/dist/commands/report/render-views.mjs.map +1 -0
  195. package/dist/commands/report/translate.d.mts +1 -0
  196. package/{src → dist}/commands/report/translate.mjs +6 -7
  197. package/dist/commands/report/translate.mjs.map +1 -0
  198. package/dist/lib/helper-scripts.d.mts +3 -0
  199. package/{src → dist}/lib/helper-scripts.mjs +11 -14
  200. package/dist/lib/helper-scripts.mjs.map +1 -0
  201. package/dist/lib/host-registry-client.d.mts +25 -0
  202. package/dist/lib/host-registry-client.mjs +152 -0
  203. package/dist/lib/host-registry-client.mjs.map +1 -0
  204. package/dist/lib/install-assets.d.mts +8 -0
  205. package/{src → dist}/lib/install-assets.mjs +17 -20
  206. package/dist/lib/install-assets.mjs.map +1 -0
  207. package/dist/lib/okstra-dirs.d.mts +4 -0
  208. package/{src → dist}/lib/okstra-dirs.mjs +3 -6
  209. package/dist/lib/okstra-dirs.mjs.map +1 -0
  210. package/dist/lib/paths.d.mts +4 -0
  211. package/dist/lib/paths.mjs +78 -0
  212. package/dist/lib/paths.mjs.map +1 -0
  213. package/dist/lib/proc.d.mts +3 -0
  214. package/dist/lib/proc.mjs +31 -0
  215. package/dist/lib/proc.mjs.map +1 -0
  216. package/dist/lib/python-helper.d.mts +8 -0
  217. package/dist/lib/python-helper.mjs +123 -0
  218. package/dist/lib/python-helper.mjs.map +1 -0
  219. package/dist/lib/runtime-manifest.d.mts +7 -0
  220. package/dist/lib/runtime-manifest.mjs +73 -0
  221. package/dist/lib/runtime-manifest.mjs.map +1 -0
  222. package/dist/lib/skill-catalog.d.mts +4 -0
  223. package/dist/lib/skill-catalog.mjs +50 -0
  224. package/dist/lib/skill-catalog.mjs.map +1 -0
  225. package/dist/lib/types.d.mts +129 -0
  226. package/dist/lib/types.mjs +2 -0
  227. package/dist/lib/types.mjs.map +1 -0
  228. package/dist/lib/version.d.mts +2 -0
  229. package/dist/lib/version.mjs +27 -0
  230. package/dist/lib/version.mjs.map +1 -0
  231. package/docs/architecture/storage-model.md +6 -2
  232. package/docs/architecture.md +29 -20
  233. package/docs/cli.md +40 -16
  234. package/docs/contributor-change-matrix.md +2 -2
  235. package/docs/for-ai/skills/okstra-inspect.md +2 -3
  236. package/docs/for-ai/skills/okstra-rollup.md +1 -0
  237. package/docs/project-structure-overview.md +72 -62
  238. package/docs/task-process/README.md +12 -8
  239. package/docs/task-process/common-flow.md +13 -16
  240. package/docs/task-process/error-analysis.md +9 -10
  241. package/docs/task-process/final-verification.md +7 -7
  242. package/docs/task-process/implementation-planning.md +9 -9
  243. package/docs/task-process/implementation.md +6 -6
  244. package/docs/task-process/release-handoff.md +8 -7
  245. package/docs/task-process/requirements-discovery.md +8 -8
  246. package/package.json +11 -5
  247. package/runtime/BUILD.json +2 -2
  248. package/runtime/agents/workers/claude-worker.md +2 -1
  249. package/runtime/agents/workers/report-writer-worker.md +3 -1
  250. package/runtime/agents/workers/translator-worker.md +3 -1
  251. package/runtime/bin/lib/okstra/cli.sh +12 -0
  252. package/runtime/bin/lib/okstra/globals.sh +3 -0
  253. package/runtime/bin/lib/okstra/interactive.sh +12 -6
  254. package/runtime/bin/lib/okstra/usage.sh +7 -6
  255. package/runtime/bin/okstra-error-log.py +27 -0
  256. package/runtime/bin/okstra-provider-exec.py +217 -5
  257. package/runtime/bin/okstra-spawn-followups.py +4 -2
  258. package/runtime/bin/okstra.sh +10 -0
  259. package/runtime/prompts/launch.template.md +2 -2
  260. package/runtime/prompts/lead/context-loader.md +1 -2
  261. package/runtime/prompts/lead/convergence.md +27 -18
  262. package/runtime/prompts/lead/okstra-lead-contract.md +6 -5
  263. package/runtime/prompts/lead/plan-body-verification.md +10 -3
  264. package/runtime/prompts/lead/report-writer.md +15 -1
  265. package/runtime/prompts/lead/team-contract.md +17 -13
  266. package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
  267. package/runtime/prompts/profiles/_implementation-executor.md +0 -3
  268. package/runtime/prompts/profiles/_implementation-verifier.md +3 -7
  269. package/runtime/prompts/profiles/change-impact-analysis.md +20 -0
  270. package/runtime/prompts/profiles/error-analysis.md +27 -1
  271. package/runtime/prompts/profiles/feature-analysis.md +20 -0
  272. package/runtime/prompts/profiles/final-verification.md +20 -1
  273. package/runtime/prompts/profiles/implementation-option-selection.md +20 -0
  274. package/runtime/prompts/profiles/implementation-planning.md +26 -0
  275. package/runtime/prompts/profiles/implementation.md +19 -0
  276. package/runtime/prompts/profiles/improvement-discovery.md +21 -1
  277. package/runtime/prompts/profiles/project-analysis.md +20 -0
  278. package/runtime/prompts/profiles/release-handoff.md +4 -0
  279. package/runtime/prompts/profiles/requirements-discovery.md +27 -1
  280. package/runtime/prompts/wizard/prompts.ko.json +35 -0
  281. package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +8 -0
  282. package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +8 -0
  283. package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
  284. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +2 -2
  285. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +25 -5
  286. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
  287. package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +4 -1
  288. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
  289. package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +2 -2
  290. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +79 -11
  291. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +95 -25
  292. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +59 -14
  293. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +55 -9
  294. package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +51 -12
  295. package/runtime/python/okstra_ctl/adapters/runtime/__init__.py +1 -0
  296. package/runtime/python/okstra_ctl/adapters/runtime/assembly.py +27 -0
  297. package/runtime/python/okstra_ctl/adapters/runtime/cli_wrapper.py +49 -0
  298. package/runtime/python/okstra_ctl/adapters/runtime/cmux.py +74 -0
  299. package/runtime/python/okstra_ctl/agent_activity.py +5 -1
  300. package/runtime/python/okstra_ctl/agent_invocation.py +608 -14
  301. package/runtime/python/okstra_ctl/agent_prompt_cli.py +249 -50
  302. package/runtime/python/okstra_ctl/application/open_worker.py +29 -0
  303. package/runtime/python/okstra_ctl/application/resolve_assignment.py +53 -12
  304. package/runtime/python/okstra_ctl/assignment_environment.py +102 -0
  305. package/runtime/python/okstra_ctl/assignment_resolver.py +790 -0
  306. package/runtime/python/okstra_ctl/attempt_evidence.py +342 -0
  307. package/runtime/python/okstra_ctl/convergence.py +409 -3
  308. package/runtime/python/okstra_ctl/convergence_engine.py +248 -15
  309. package/runtime/python/okstra_ctl/convergence_store.py +170 -1
  310. package/runtime/python/okstra_ctl/dispatch_core.py +1204 -155
  311. package/runtime/python/okstra_ctl/dispatch_state.py +696 -11
  312. package/runtime/python/okstra_ctl/doctor.py +14 -0
  313. package/runtime/python/okstra_ctl/domain/host.py +34 -1
  314. package/runtime/python/okstra_ctl/domain/provider.py +212 -6
  315. package/runtime/python/okstra_ctl/domain/role.py +125 -0
  316. package/runtime/python/okstra_ctl/domain/wizard/interaction.py +18 -0
  317. package/runtime/python/okstra_ctl/domain/worker_exec.py +34 -1
  318. package/runtime/python/okstra_ctl/domain/worker_role.py +13 -3
  319. package/runtime/python/okstra_ctl/domain/worker_runtime.py +44 -0
  320. package/runtime/python/okstra_ctl/domain/worker_stream.py +24 -0
  321. package/runtime/python/okstra_ctl/error_log_write.py +9 -0
  322. package/runtime/python/okstra_ctl/error_report.py +21 -1
  323. package/runtime/python/okstra_ctl/execution_identity.py +529 -0
  324. package/runtime/python/okstra_ctl/execution_manifest.py +1281 -0
  325. package/runtime/python/okstra_ctl/execution_mutation_audit.py +619 -0
  326. package/runtime/python/okstra_ctl/implementation_outcome.py +21 -5
  327. package/runtime/python/okstra_ctl/index.py +6 -2
  328. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +43 -3
  329. package/runtime/python/okstra_ctl/legacy_model_selection.py +578 -0
  330. package/runtime/python/okstra_ctl/manager_sync.py +4 -1
  331. package/runtime/python/okstra_ctl/model_cli.py +278 -0
  332. package/runtime/python/okstra_ctl/model_defaults.py +46 -0
  333. package/runtime/python/okstra_ctl/model_discovery.py +26 -7
  334. package/runtime/python/okstra_ctl/model_pool.py +501 -0
  335. package/runtime/python/okstra_ctl/models.py +47 -27
  336. package/runtime/python/okstra_ctl/mutation_recovery.py +293 -0
  337. package/runtime/python/okstra_ctl/next_phase.py +236 -0
  338. package/runtime/python/okstra_ctl/pane_title.py +151 -0
  339. package/runtime/python/okstra_ctl/ports/host_model.py +48 -17
  340. package/runtime/python/okstra_ctl/ports/worker_dispatch.py +4 -0
  341. package/runtime/python/okstra_ctl/ports/worker_runtime.py +26 -0
  342. package/runtime/python/okstra_ctl/recap.py +4 -1
  343. package/runtime/python/okstra_ctl/registry/provider_registry.py +8 -4
  344. package/runtime/python/okstra_ctl/render.py +124 -57
  345. package/runtime/python/okstra_ctl/render_final_report.py +19 -0
  346. package/runtime/python/okstra_ctl/report_contract.py +39 -0
  347. package/runtime/python/okstra_ctl/report_finalize.py +16 -0
  348. package/runtime/python/okstra_ctl/report_html/render.py +1 -0
  349. package/runtime/python/okstra_ctl/role_requirements.py +210 -0
  350. package/runtime/python/okstra_ctl/rollup.py +4 -1
  351. package/runtime/python/okstra_ctl/run.py +1210 -258
  352. package/runtime/python/okstra_ctl/run_index_row.py +7 -1
  353. package/runtime/python/okstra_ctl/stage_fix_carry.py +17 -1
  354. package/runtime/python/okstra_ctl/team.py +14 -18
  355. package/runtime/python/okstra_ctl/time_report.py +38 -4
  356. package/runtime/python/okstra_ctl/usage_identity.py +86 -0
  357. package/runtime/python/okstra_ctl/usage_report.py +15 -3
  358. package/runtime/python/okstra_ctl/wizard.py +1449 -66
  359. package/runtime/python/okstra_ctl/worker_audit_ledger.py +68 -65
  360. package/runtime/python/okstra_ctl/worker_prompt_body.py +18 -2
  361. package/runtime/python/okstra_ctl/worker_prompt_contract.py +40 -9
  362. package/runtime/python/okstra_ctl/worker_prompt_headers.py +16 -1
  363. package/runtime/python/okstra_ctl/worker_request.py +7 -1
  364. package/runtime/python/okstra_ctl/worker_runner.py +130 -5
  365. package/runtime/python/okstra_ctl/workers.py +7 -5
  366. package/runtime/python/okstra_ctl/workflow.py +18 -32
  367. package/runtime/python/okstra_ctl/worktree.py +3 -3
  368. package/runtime/python/okstra_ctl/worktree_registry.py +5 -4
  369. package/runtime/python/okstra_ctl/write_policy.py +571 -0
  370. package/runtime/python/okstra_project/state.py +54 -6
  371. package/runtime/schemas/convergence-groups-v2.0.schema.json +128 -0
  372. package/runtime/schemas/execution-manifest-v2.schema.json +340 -0
  373. package/runtime/schemas/final-report-v2.0.schema.json +68 -5
  374. package/runtime/skills/okstra-inspect/facets/recap.md +2 -0
  375. package/runtime/skills/okstra-inspect/facets/report.md +1 -1
  376. package/runtime/skills/okstra-inspect/facets/status.md +15 -13
  377. package/runtime/skills/okstra-rollup/SKILL.md +1 -0
  378. package/runtime/skills/okstra-run/SKILL.md +14 -14
  379. package/runtime/templates/implementation-worker-preamble.md +3 -18
  380. package/runtime/templates/project-docs/task-index.template.md +0 -1
  381. package/runtime/templates/report-writer-prompt-preamble.md +2 -0
  382. package/runtime/templates/reports/final-report-v2.template.md +7 -1
  383. package/runtime/templates/reports/html/base.template.html +21 -0
  384. package/runtime/templates/reports/html/i18n/en.json +4 -0
  385. package/runtime/templates/reports/html/i18n/ko.json +4 -0
  386. package/runtime/templates/reports/html/macros/forms.html +5 -4
  387. package/runtime/templates/reports/html/tasks/final-verification.template.html +1 -1
  388. package/runtime/templates/reports/html/tasks/implementation.template.html +1 -1
  389. package/runtime/templates/worker-prompt-preamble.md +6 -35
  390. package/runtime/validators/lib/fixtures.sh +49 -16
  391. package/runtime/validators/validate-run.py +486 -134
  392. package/runtime/validators/validate_session_conformance.py +25 -7
  393. package/src/cli-registry.mjs +0 -539
  394. package/src/commands/execute/codex-dispatch.mjs +0 -10
  395. package/src/commands/execute/plan-items.mjs +0 -9
  396. package/src/commands/execute/plan-validate.mjs +0 -68
  397. package/src/commands/execute/run.mjs +0 -208
  398. package/src/commands/execute/wizard.mjs +0 -167
  399. package/src/commands/inspect/stage-map.mjs +0 -98
  400. package/src/commands/inspect/task-list.mjs +0 -97
  401. package/src/commands/inspect/task-show.mjs +0 -100
  402. package/src/commands/lifecycle/check-project.mjs +0 -187
  403. package/src/commands/lifecycle/config.mjs +0 -385
  404. package/src/commands/lifecycle/doctor.mjs +0 -308
  405. package/src/commands/lifecycle/install.mjs +0 -1170
  406. package/src/commands/lifecycle/paths.mjs +0 -104
  407. package/src/commands/lifecycle/preflight.mjs +0 -125
  408. package/src/commands/lifecycle/setup.mjs +0 -333
  409. package/src/commands/lifecycle/uninstall.mjs +0 -279
  410. package/src/commands/memory/memory.mjs +0 -524
  411. package/src/commands/pr/pr.mjs +0 -261
  412. package/src/lib/host-registry-client.mjs +0 -176
  413. package/src/lib/paths.mjs +0 -86
  414. package/src/lib/proc.mjs +0 -31
  415. package/src/lib/python-helper.mjs +0 -142
  416. package/src/lib/runtime-manifest.mjs +0 -70
  417. package/src/lib/skill-catalog.mjs +0 -53
  418. package/src/lib/version.mjs +0 -20
  419. /package/{src → dist}/commands/pr/default.md +0 -0
@@ -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
@@ -6,11 +6,27 @@ from typing import Any, Mapping, Sequence
6
6
  from .worker_prompt_policy import PromptPlan
7
7
 
8
8
 
9
- ANALYSIS_WORKER_LABELS = {
9
+ _ANALYSIS_WORKER_LABELS = {
10
10
  "claude": "Claude worker",
11
11
  "codex": "Codex worker",
12
12
  "antigravity": "Antigravity worker",
13
13
  }
14
+
15
+
16
+ def analysis_worker_label(worker_id: str) -> str:
17
+ """The role label this worker's prompt body is titled with.
18
+
19
+ The map above only supplies display capitalization for the three providers
20
+ that predate it; every other worker id (`grok`, `kimi`, a user-installed
21
+ adapter) takes the id itself. Both branches are identity deltas the equality
22
+ group must normalize away, so `worker_prompt_contract` erases exactly what
23
+ this function returns rather than restating the map — an enumeration that
24
+ covered only the map's keys let `# grok worker Dispatch` through and failed
25
+ every roster carrying grok or kimi before publication.
26
+ """
27
+ return _ANALYSIS_WORKER_LABELS.get(worker_id, f"{worker_id} worker")
28
+
29
+
14
30
  def analysis_prompt_body(
15
31
  manifest: Mapping[str, Any],
16
32
  active_context: Mapping[str, Any],
@@ -19,7 +35,7 @@ def analysis_prompt_body(
19
35
  plan: PromptPlan,
20
36
  ) -> list[str]:
21
37
  """Render the provider-neutral body for an initial analysis audience."""
22
- label = ANALYSIS_WORKER_LABELS.get(worker_id, f"{worker_id} worker")
38
+ label = analysis_worker_label(worker_id)
23
39
  pane_role = {
24
40
  "implementation-executor": "executor",
25
41
  "implementation-verifier": "verifier",
@@ -5,8 +5,9 @@ import json
5
5
  import re
6
6
  from dataclasses import dataclass
7
7
  from pathlib import Path
8
- from typing import Any, Mapping, Sequence
8
+ from typing import Any, Iterable, Mapping, Sequence
9
9
 
10
+ from .worker_prompt_body import analysis_worker_label
10
11
  from .worker_prompt_policy import (
11
12
  ERRORS_PATH_HEADERS,
12
13
  IMPLEMENTATION_HEADERS,
@@ -88,10 +89,7 @@ _WORKER_SPECIFIC_PREFIXES = (
88
89
  "**Host runtime:**",
89
90
  "**Host model value:**",
90
91
  )
91
- _WORKER_LABEL_RE = re.compile(
92
- r"\b(?:Claude|Codex|Antigravity) worker\b",
93
- re.IGNORECASE,
94
- )
92
+ _WORKER_LABEL_SUBSTITUTE = "<analysis-worker>"
95
93
 
96
94
 
97
95
  @dataclass(frozen=True)
@@ -156,8 +154,38 @@ def validate_final_verification_initial_prompt(text: str) -> list[str]:
156
154
  return errors
157
155
 
158
156
 
159
- def normalise_analysis_prompt(text: str) -> str:
160
- """Remove only permitted worker identity, model, role, and path deltas."""
157
+ def _worker_label_pattern(worker_ids: Iterable[str]) -> re.Pattern[str] | None:
158
+ """Match the role label the body renderer titled each compared worker with.
159
+
160
+ Built from `analysis_worker_label`, the same function that writes the label,
161
+ so a provider outside its display map (`grok`, `kimi`) is covered as it
162
+ comes. Restating the map here is what forked the roster: the enumeration
163
+ named only Claude / Codex / Antigravity, `# grok worker Dispatch` survived
164
+ normalization, and every run rostering grok or kimi failed the equality group
165
+ before publication with no prompt defect to fix.
166
+ """
167
+ labels = sorted(
168
+ {
169
+ analysis_worker_label(worker_id.strip())
170
+ for worker_id in worker_ids
171
+ if worker_id.strip()
172
+ },
173
+ key=lambda label: (-len(label), label),
174
+ )
175
+ if not labels:
176
+ return None
177
+ alternation = "|".join(re.escape(label) for label in labels)
178
+ return re.compile(rf"\b(?:{alternation})\b", re.IGNORECASE)
179
+
180
+
181
+ def normalise_analysis_prompt(text: str, *, worker_ids: Iterable[str]) -> str:
182
+ """Remove only permitted worker identity, model, role, and path deltas.
183
+
184
+ ``worker_ids`` is every worker in the comparison group, not just this
185
+ prompt's own: a body that names a sibling worker must normalize to the same
186
+ bytes in all of them, or the mention itself reads as divergence.
187
+ """
188
+ label = _worker_label_pattern(worker_ids)
161
189
  normalized: list[str] = []
162
190
  for line in text.replace("\r\n", "\n").replace("\r", "\n").splitlines():
163
191
  stripped = line.strip()
@@ -167,7 +195,10 @@ def normalise_analysis_prompt(text: str) -> str:
167
195
  )
168
196
  if prefix:
169
197
  continue
170
- normalized.append(_WORKER_LABEL_RE.sub("<analysis-worker>", line.rstrip()))
198
+ line = line.rstrip()
199
+ normalized.append(
200
+ line if label is None else label.sub(_WORKER_LABEL_SUBSTITUTE, line)
201
+ )
171
202
  return "\n".join(normalized).strip() + "\n"
172
203
 
173
204
 
@@ -176,7 +207,7 @@ def validate_analysis_prompt_set(prompts: Mapping[str, str]) -> list[str]:
176
207
  if len(prompts) < 2:
177
208
  return []
178
209
  normalized = {
179
- worker_id: normalise_analysis_prompt(text)
210
+ worker_id: normalise_analysis_prompt(text, worker_ids=prompts.keys())
180
211
  for worker_id, text in sorted(prompts.items())
181
212
  }
182
213
  baseline_worker = next(iter(normalized))
@@ -54,6 +54,17 @@ EVIDENCE_CITATION_HEADER = (
54
54
  "ledger row and fails exactly like a file you never opened, however many "
55
55
  "times you cited the full path earlier."
56
56
  )
57
+ # 파일 인용과 같이 첫 결론 명령 앞에 형식을 둔다. 분석 프리앰블에만 있으면
58
+ # 구현 워커는 명령 행을 쓰지 못한다.
59
+ EVIDENCE_COMMANDS_HEADER = (
60
+ "**Evidence commands:** Append one `- Evidence command: "
61
+ "{\"command\":\"<exact command>\",\"cwd\":\"<project-root>\","
62
+ "\"exitCode\":0,\"outputSummary\":\"<one-line result>\"}` row to the audit "
63
+ "sidecar for each command that produced or verified a conclusion. Do not "
64
+ "record exploratory `rg`, `ls`, or file-opening commands. Write the command "
65
+ "as you ran it. A value that must not be written down is yours to omit or "
66
+ "pass as a `$VAR` reference."
67
+ )
57
68
 
58
69
  # `agy`'s write tool validates the target against the Gemini artifact store
59
70
  # whenever the model attaches ArtifactMetadata, and rejects every path outside
@@ -120,7 +131,11 @@ def worker_prompt_headers(
120
131
  f"**Coding preflight pack:** {_coding_preflight_pack_path(active_context)}"
121
132
  )
122
133
  if dispatch_kind == "initial" and plan.audience != "report-writer":
123
- headers += [EVIDENCE_LEDGER_HEADER, EVIDENCE_CITATION_HEADER]
134
+ headers += [
135
+ EVIDENCE_LEDGER_HEADER,
136
+ EVIDENCE_CITATION_HEADER,
137
+ EVIDENCE_COMMANDS_HEADER,
138
+ ]
124
139
  headers.extend([
125
140
  f"**Errors log path:** {errors_log_path}",
126
141
  f"**Errors sidecar path:** {errors_sidecar_path}",
@@ -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
@@ -36,6 +37,8 @@ def build_request(
36
37
  role: str,
37
38
  idle_timeout_seconds: int,
38
39
  session_id: str = "",
40
+ write_policy: WritePolicy | None = None,
41
+ write_enforcement: WriteEnforcement | None = None,
39
42
  ) -> WorkerExecRequest:
40
43
  """One dispatch, with every value the strategies will not re-derive.
41
44
 
@@ -53,7 +56,10 @@ def build_request(
53
56
  # Nobody is at the keyboard to answer an approval prompt, so the gate is
54
57
  # open and the boundary comes from the write scope instead.
55
58
  policy=ExecutionPolicy(
56
- 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,
57
63
  ),
58
64
  idle_timeout_seconds=idle_timeout_seconds,
59
65
  session_id=session_id,
@@ -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] = []