okstra 0.174.0 → 0.176.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. package/README.md +18 -9
  2. package/bin/okstra +8 -4
  3. package/dist/cli-registry.d.mts +16 -0
  4. package/dist/cli-registry.mjs +560 -0
  5. package/dist/cli-registry.mjs.map +1 -0
  6. package/dist/commands/execute/agent-prompt.d.mts +1 -0
  7. package/{src → dist}/commands/execute/agent-prompt.mjs +10 -11
  8. package/dist/commands/execute/agent-prompt.mjs.map +1 -0
  9. package/dist/commands/execute/codex-dispatch.d.mts +3 -0
  10. package/dist/commands/execute/codex-dispatch.mjs +6 -0
  11. package/dist/commands/execute/codex-dispatch.mjs.map +1 -0
  12. package/dist/commands/execute/codex-run.d.mts +3 -0
  13. package/{src → dist}/commands/execute/codex-run.mjs +30 -37
  14. package/dist/commands/execute/codex-run.mjs.map +1 -0
  15. package/dist/commands/execute/convergence.d.mts +1 -0
  16. package/{src → dist}/commands/execute/convergence.mjs +15 -16
  17. package/dist/commands/execute/convergence.mjs.map +1 -0
  18. package/dist/commands/execute/error-log.d.mts +1 -0
  19. package/{src → dist}/commands/execute/error-log.mjs +2 -3
  20. package/dist/commands/execute/error-log.mjs.map +1 -0
  21. package/dist/commands/execute/git-reconcile.d.mts +1 -0
  22. package/{src → dist}/commands/execute/git-reconcile.mjs +10 -11
  23. package/dist/commands/execute/git-reconcile.mjs.map +1 -0
  24. package/dist/commands/execute/handoff.d.mts +1 -0
  25. package/{src → dist}/commands/execute/handoff.mjs +10 -11
  26. package/dist/commands/execute/handoff.mjs.map +1 -0
  27. package/dist/commands/execute/incremental-carry.d.mts +1 -0
  28. package/{src → dist}/commands/execute/incremental-carry.mjs +2 -3
  29. package/dist/commands/execute/incremental-carry.mjs.map +1 -0
  30. package/dist/commands/execute/incremental-scope.d.mts +1 -0
  31. package/{src → dist}/commands/execute/incremental-scope.mjs +2 -3
  32. package/dist/commands/execute/incremental-scope.mjs.map +1 -0
  33. package/dist/commands/execute/integrate-stages.d.mts +1 -0
  34. package/{src → dist}/commands/execute/integrate-stages.mjs +10 -11
  35. package/dist/commands/execute/integrate-stages.mjs.map +1 -0
  36. package/dist/commands/execute/pane-title.d.mts +1 -0
  37. package/dist/commands/execute/pane-title.mjs +20 -0
  38. package/dist/commands/execute/pane-title.mjs.map +1 -0
  39. package/dist/commands/execute/plan-items.d.mts +1 -0
  40. package/dist/commands/execute/plan-items.mjs +9 -0
  41. package/dist/commands/execute/plan-items.mjs.map +1 -0
  42. package/dist/commands/execute/plan-validate.d.mts +1 -0
  43. package/dist/commands/execute/plan-validate.mjs +68 -0
  44. package/dist/commands/execute/plan-validate.mjs.map +1 -0
  45. package/dist/commands/execute/plan-verify.d.mts +1 -0
  46. package/{src → dist}/commands/execute/plan-verify.mjs +13 -16
  47. package/dist/commands/execute/plan-verify.mjs.map +1 -0
  48. package/dist/commands/execute/render-bundle.d.mts +3 -0
  49. package/{src → dist}/commands/execute/render-bundle.mjs +65 -61
  50. package/dist/commands/execute/render-bundle.mjs.map +1 -0
  51. package/dist/commands/execute/run.d.mts +24 -0
  52. package/dist/commands/execute/run.mjs +196 -0
  53. package/dist/commands/execute/run.mjs.map +1 -0
  54. package/dist/commands/execute/spawn-followups.d.mts +1 -0
  55. package/{src → dist}/commands/execute/spawn-followups.mjs +6 -7
  56. package/dist/commands/execute/spawn-followups.mjs.map +1 -0
  57. package/dist/commands/execute/team.d.mts +3 -0
  58. package/{src → dist}/commands/execute/team.mjs +29 -36
  59. package/dist/commands/execute/team.mjs.map +1 -0
  60. package/dist/commands/execute/token-usage.d.mts +1 -0
  61. package/{src → dist}/commands/execute/token-usage.mjs +2 -3
  62. package/dist/commands/execute/token-usage.mjs.map +1 -0
  63. package/dist/commands/execute/wizard.d.mts +8 -0
  64. package/dist/commands/execute/wizard.mjs +191 -0
  65. package/dist/commands/execute/wizard.mjs.map +1 -0
  66. package/dist/commands/execute/worker-audit-check.d.mts +1 -0
  67. package/{src → dist}/commands/execute/worker-audit-check.mjs +11 -12
  68. package/dist/commands/execute/worker-audit-check.mjs.map +1 -0
  69. package/dist/commands/execute/worker-dispatch.d.mts +7 -0
  70. package/{src → dist}/commands/execute/worker-dispatch.mjs +34 -46
  71. package/dist/commands/execute/worker-dispatch.mjs.map +1 -0
  72. package/dist/commands/execute/worker-state.d.mts +1 -0
  73. package/{src → dist}/commands/execute/worker-state.mjs +15 -16
  74. package/dist/commands/execute/worker-state.mjs.map +1 -0
  75. package/dist/commands/execute/worktree-lookup.d.mts +1 -0
  76. package/{src → dist}/commands/execute/worktree-lookup.mjs +38 -37
  77. package/dist/commands/execute/worktree-lookup.mjs.map +1 -0
  78. package/dist/commands/execute/worktree-status.d.mts +1 -0
  79. package/{src → dist}/commands/execute/worktree-status.mjs +52 -46
  80. package/dist/commands/execute/worktree-status.mjs.map +1 -0
  81. package/dist/commands/inspect/code-review.d.mts +1 -0
  82. package/{src → dist}/commands/inspect/code-review.mjs +16 -19
  83. package/dist/commands/inspect/code-review.mjs.map +1 -0
  84. package/dist/commands/inspect/container.d.mts +1 -0
  85. package/{src → dist}/commands/inspect/container.mjs +11 -13
  86. package/dist/commands/inspect/container.mjs.map +1 -0
  87. package/dist/commands/inspect/context-cost.d.mts +1 -0
  88. package/{src → dist}/commands/inspect/context-cost.mjs +11 -13
  89. package/dist/commands/inspect/context-cost.mjs.map +1 -0
  90. package/dist/commands/inspect/design-prep.d.mts +1 -0
  91. package/{src → dist}/commands/inspect/design-prep.mjs +10 -11
  92. package/dist/commands/inspect/design-prep.mjs.map +1 -0
  93. package/dist/commands/inspect/error-report.d.mts +1 -0
  94. package/{src → dist}/commands/inspect/error-report.mjs +11 -13
  95. package/dist/commands/inspect/error-report.mjs.map +1 -0
  96. package/dist/commands/inspect/error-zip.d.mts +1 -0
  97. package/{src → dist}/commands/inspect/error-zip.mjs +11 -12
  98. package/dist/commands/inspect/error-zip.mjs.map +1 -0
  99. package/dist/commands/inspect/log-report.d.mts +1 -0
  100. package/{src → dist}/commands/inspect/log-report.mjs +11 -13
  101. package/dist/commands/inspect/log-report.mjs.map +1 -0
  102. package/dist/commands/inspect/profile-show.d.mts +1 -0
  103. package/{src → dist}/commands/inspect/profile-show.mjs +10 -11
  104. package/dist/commands/inspect/profile-show.mjs.map +1 -0
  105. package/dist/commands/inspect/recap.d.mts +1 -0
  106. package/{src → dist}/commands/inspect/recap.mjs +11 -13
  107. package/dist/commands/inspect/recap.mjs.map +1 -0
  108. package/dist/commands/inspect/resolve-task-key.d.mts +1 -0
  109. package/{src → dist}/commands/inspect/resolve-task-key.mjs +11 -13
  110. package/dist/commands/inspect/resolve-task-key.mjs.map +1 -0
  111. package/dist/commands/inspect/rollup.d.mts +1 -0
  112. package/{src → dist}/commands/inspect/rollup.mjs +11 -13
  113. package/dist/commands/inspect/rollup.mjs.map +1 -0
  114. package/dist/commands/inspect/run-audit.d.mts +1 -0
  115. package/{src → dist}/commands/inspect/run-audit.mjs +11 -12
  116. package/dist/commands/inspect/run-audit.mjs.map +1 -0
  117. package/dist/commands/inspect/set-work-status.d.mts +1 -0
  118. package/{src → dist}/commands/inspect/set-work-status.mjs +11 -13
  119. package/dist/commands/inspect/set-work-status.mjs.map +1 -0
  120. package/dist/commands/inspect/stage-map.d.mts +1 -0
  121. package/dist/commands/inspect/stage-map.mjs +110 -0
  122. package/dist/commands/inspect/stage-map.mjs.map +1 -0
  123. package/dist/commands/inspect/task-list.d.mts +1 -0
  124. package/dist/commands/inspect/task-list.mjs +103 -0
  125. package/dist/commands/inspect/task-list.mjs.map +1 -0
  126. package/dist/commands/inspect/task-show.d.mts +1 -0
  127. package/dist/commands/inspect/task-show.mjs +108 -0
  128. package/dist/commands/inspect/task-show.mjs.map +1 -0
  129. package/dist/commands/inspect/time-report.d.mts +1 -0
  130. package/{src → dist}/commands/inspect/time-report.mjs +11 -13
  131. package/dist/commands/inspect/time-report.mjs.map +1 -0
  132. package/dist/commands/inspect/usage-report.d.mts +1 -0
  133. package/{src → dist}/commands/inspect/usage-report.mjs +11 -13
  134. package/dist/commands/inspect/usage-report.mjs.map +1 -0
  135. package/dist/commands/inspect/user-response.d.mts +1 -0
  136. package/{src → dist}/commands/inspect/user-response.mjs +10 -11
  137. package/dist/commands/inspect/user-response.mjs.map +1 -0
  138. package/dist/commands/inspect/worker-liveness.d.mts +1 -0
  139. package/{src → dist}/commands/inspect/worker-liveness.mjs +11 -13
  140. package/dist/commands/inspect/worker-liveness.mjs.map +1 -0
  141. package/dist/commands/lifecycle/check-project.d.mts +21 -0
  142. package/dist/commands/lifecycle/check-project.mjs +175 -0
  143. package/dist/commands/lifecycle/check-project.mjs.map +1 -0
  144. package/dist/commands/lifecycle/config.d.mts +1 -0
  145. package/dist/commands/lifecycle/config.mjs +369 -0
  146. package/dist/commands/lifecycle/config.mjs.map +1 -0
  147. package/dist/commands/lifecycle/doctor.d.mts +10 -0
  148. package/dist/commands/lifecycle/doctor.mjs +317 -0
  149. package/dist/commands/lifecycle/doctor.mjs.map +1 -0
  150. package/dist/commands/lifecycle/install.d.mts +17 -0
  151. package/dist/commands/lifecycle/install.mjs +1077 -0
  152. package/dist/commands/lifecycle/install.mjs.map +1 -0
  153. package/dist/commands/lifecycle/migrate.d.mts +1 -0
  154. package/{src → dist}/commands/lifecycle/migrate.mjs +10 -11
  155. package/dist/commands/lifecycle/migrate.mjs.map +1 -0
  156. package/dist/commands/lifecycle/model.d.mts +1 -0
  157. package/dist/commands/lifecycle/model.mjs +22 -0
  158. package/dist/commands/lifecycle/model.mjs.map +1 -0
  159. package/dist/commands/lifecycle/paths.d.mts +1 -0
  160. package/dist/commands/lifecycle/paths.mjs +102 -0
  161. package/dist/commands/lifecycle/paths.mjs.map +1 -0
  162. package/dist/commands/lifecycle/preflight.d.mts +1 -0
  163. package/dist/commands/lifecycle/preflight.mjs +124 -0
  164. package/dist/commands/lifecycle/preflight.mjs.map +1 -0
  165. package/dist/commands/lifecycle/setup.d.mts +1 -0
  166. package/dist/commands/lifecycle/setup.mjs +297 -0
  167. package/dist/commands/lifecycle/setup.mjs.map +1 -0
  168. package/dist/commands/lifecycle/uninstall.d.mts +4 -0
  169. package/dist/commands/lifecycle/uninstall.mjs +262 -0
  170. package/dist/commands/lifecycle/uninstall.mjs.map +1 -0
  171. package/dist/commands/manager.d.mts +3 -0
  172. package/{src → dist}/commands/manager.mjs +25 -32
  173. package/dist/commands/manager.mjs.map +1 -0
  174. package/dist/commands/memory/memory.d.mts +1 -0
  175. package/dist/commands/memory/memory.mjs +569 -0
  176. package/dist/commands/memory/memory.mjs.map +1 -0
  177. package/dist/commands/pr/pr.d.mts +33 -0
  178. package/dist/commands/pr/pr.mjs +296 -0
  179. package/dist/commands/pr/pr.mjs.map +1 -0
  180. package/dist/commands/report/agent-activity.d.mts +1 -0
  181. package/{src → dist}/commands/report/agent-activity.mjs +11 -12
  182. package/dist/commands/report/agent-activity.mjs.map +1 -0
  183. package/dist/commands/report/finalize.d.mts +4 -0
  184. package/{src → dist}/commands/report/finalize.mjs +25 -32
  185. package/dist/commands/report/finalize.mjs.map +1 -0
  186. package/dist/commands/report/inject-report-index.d.mts +1 -0
  187. package/{src → dist}/commands/report/inject-report-index.mjs +6 -7
  188. package/dist/commands/report/inject-report-index.mjs.map +1 -0
  189. package/dist/commands/report/render-final-report.d.mts +1 -0
  190. package/{src → dist}/commands/report/render-final-report.mjs +6 -7
  191. package/dist/commands/report/render-final-report.mjs.map +1 -0
  192. package/dist/commands/report/render-views.d.mts +1 -0
  193. package/{src → dist}/commands/report/render-views.mjs +6 -7
  194. package/dist/commands/report/render-views.mjs.map +1 -0
  195. package/dist/commands/report/translate.d.mts +1 -0
  196. package/{src → dist}/commands/report/translate.mjs +6 -7
  197. package/dist/commands/report/translate.mjs.map +1 -0
  198. package/dist/lib/helper-scripts.d.mts +3 -0
  199. package/{src → dist}/lib/helper-scripts.mjs +11 -14
  200. package/dist/lib/helper-scripts.mjs.map +1 -0
  201. package/dist/lib/host-registry-client.d.mts +25 -0
  202. package/dist/lib/host-registry-client.mjs +152 -0
  203. package/dist/lib/host-registry-client.mjs.map +1 -0
  204. package/dist/lib/install-assets.d.mts +8 -0
  205. package/{src → dist}/lib/install-assets.mjs +17 -20
  206. package/dist/lib/install-assets.mjs.map +1 -0
  207. package/dist/lib/okstra-dirs.d.mts +4 -0
  208. package/{src → dist}/lib/okstra-dirs.mjs +3 -6
  209. package/dist/lib/okstra-dirs.mjs.map +1 -0
  210. package/dist/lib/paths.d.mts +4 -0
  211. package/dist/lib/paths.mjs +78 -0
  212. package/dist/lib/paths.mjs.map +1 -0
  213. package/dist/lib/proc.d.mts +3 -0
  214. package/dist/lib/proc.mjs +31 -0
  215. package/dist/lib/proc.mjs.map +1 -0
  216. package/dist/lib/python-helper.d.mts +8 -0
  217. package/dist/lib/python-helper.mjs +123 -0
  218. package/dist/lib/python-helper.mjs.map +1 -0
  219. package/dist/lib/runtime-manifest.d.mts +7 -0
  220. package/dist/lib/runtime-manifest.mjs +73 -0
  221. package/dist/lib/runtime-manifest.mjs.map +1 -0
  222. package/dist/lib/skill-catalog.d.mts +4 -0
  223. package/dist/lib/skill-catalog.mjs +50 -0
  224. package/dist/lib/skill-catalog.mjs.map +1 -0
  225. package/dist/lib/types.d.mts +129 -0
  226. package/dist/lib/types.mjs +2 -0
  227. package/dist/lib/types.mjs.map +1 -0
  228. package/dist/lib/version.d.mts +2 -0
  229. package/dist/lib/version.mjs +27 -0
  230. package/dist/lib/version.mjs.map +1 -0
  231. package/docs/architecture/storage-model.md +6 -2
  232. package/docs/architecture.md +29 -20
  233. package/docs/cli.md +40 -16
  234. package/docs/contributor-change-matrix.md +2 -2
  235. package/docs/for-ai/skills/okstra-inspect.md +2 -3
  236. package/docs/for-ai/skills/okstra-rollup.md +1 -0
  237. package/docs/project-structure-overview.md +72 -62
  238. package/docs/task-process/README.md +12 -8
  239. package/docs/task-process/common-flow.md +13 -16
  240. package/docs/task-process/error-analysis.md +9 -10
  241. package/docs/task-process/final-verification.md +7 -7
  242. package/docs/task-process/implementation-planning.md +9 -9
  243. package/docs/task-process/implementation.md +6 -6
  244. package/docs/task-process/release-handoff.md +8 -7
  245. package/docs/task-process/requirements-discovery.md +8 -8
  246. package/package.json +11 -5
  247. package/runtime/BUILD.json +2 -2
  248. package/runtime/agents/workers/claude-worker.md +2 -1
  249. package/runtime/agents/workers/report-writer-worker.md +3 -1
  250. package/runtime/agents/workers/translator-worker.md +3 -1
  251. package/runtime/bin/lib/okstra/cli.sh +12 -0
  252. package/runtime/bin/lib/okstra/globals.sh +3 -0
  253. package/runtime/bin/lib/okstra/interactive.sh +12 -6
  254. package/runtime/bin/lib/okstra/usage.sh +7 -6
  255. package/runtime/bin/okstra-error-log.py +27 -0
  256. package/runtime/bin/okstra-provider-exec.py +217 -5
  257. package/runtime/bin/okstra-spawn-followups.py +4 -2
  258. package/runtime/bin/okstra.sh +10 -0
  259. package/runtime/prompts/launch.template.md +2 -2
  260. package/runtime/prompts/lead/context-loader.md +1 -2
  261. package/runtime/prompts/lead/convergence.md +27 -18
  262. package/runtime/prompts/lead/okstra-lead-contract.md +6 -5
  263. package/runtime/prompts/lead/plan-body-verification.md +10 -3
  264. package/runtime/prompts/lead/report-writer.md +15 -1
  265. package/runtime/prompts/lead/team-contract.md +17 -13
  266. package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
  267. package/runtime/prompts/profiles/_implementation-executor.md +0 -3
  268. package/runtime/prompts/profiles/_implementation-verifier.md +3 -7
  269. package/runtime/prompts/profiles/change-impact-analysis.md +20 -0
  270. package/runtime/prompts/profiles/error-analysis.md +27 -1
  271. package/runtime/prompts/profiles/feature-analysis.md +20 -0
  272. package/runtime/prompts/profiles/final-verification.md +20 -1
  273. package/runtime/prompts/profiles/implementation-option-selection.md +20 -0
  274. package/runtime/prompts/profiles/implementation-planning.md +26 -0
  275. package/runtime/prompts/profiles/implementation.md +19 -0
  276. package/runtime/prompts/profiles/improvement-discovery.md +21 -1
  277. package/runtime/prompts/profiles/project-analysis.md +20 -0
  278. package/runtime/prompts/profiles/release-handoff.md +4 -0
  279. package/runtime/prompts/profiles/requirements-discovery.md +27 -1
  280. package/runtime/prompts/wizard/prompts.ko.json +35 -0
  281. package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +8 -0
  282. package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +8 -0
  283. package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
  284. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +2 -2
  285. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +25 -5
  286. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
  287. package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +4 -1
  288. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
  289. package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +2 -2
  290. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +79 -11
  291. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +95 -25
  292. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +59 -14
  293. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +55 -9
  294. package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +51 -12
  295. package/runtime/python/okstra_ctl/adapters/runtime/__init__.py +1 -0
  296. package/runtime/python/okstra_ctl/adapters/runtime/assembly.py +27 -0
  297. package/runtime/python/okstra_ctl/adapters/runtime/cli_wrapper.py +49 -0
  298. package/runtime/python/okstra_ctl/adapters/runtime/cmux.py +74 -0
  299. package/runtime/python/okstra_ctl/agent_activity.py +5 -1
  300. package/runtime/python/okstra_ctl/agent_invocation.py +608 -14
  301. package/runtime/python/okstra_ctl/agent_prompt_cli.py +249 -50
  302. package/runtime/python/okstra_ctl/application/open_worker.py +29 -0
  303. package/runtime/python/okstra_ctl/application/resolve_assignment.py +53 -12
  304. package/runtime/python/okstra_ctl/assignment_environment.py +102 -0
  305. package/runtime/python/okstra_ctl/assignment_resolver.py +790 -0
  306. package/runtime/python/okstra_ctl/attempt_evidence.py +342 -0
  307. package/runtime/python/okstra_ctl/convergence.py +409 -3
  308. package/runtime/python/okstra_ctl/convergence_engine.py +248 -15
  309. package/runtime/python/okstra_ctl/convergence_store.py +170 -1
  310. package/runtime/python/okstra_ctl/dispatch_core.py +1204 -155
  311. package/runtime/python/okstra_ctl/dispatch_state.py +696 -11
  312. package/runtime/python/okstra_ctl/doctor.py +14 -0
  313. package/runtime/python/okstra_ctl/domain/host.py +34 -1
  314. package/runtime/python/okstra_ctl/domain/provider.py +212 -6
  315. package/runtime/python/okstra_ctl/domain/role.py +125 -0
  316. package/runtime/python/okstra_ctl/domain/wizard/interaction.py +18 -0
  317. package/runtime/python/okstra_ctl/domain/worker_exec.py +34 -1
  318. package/runtime/python/okstra_ctl/domain/worker_role.py +13 -3
  319. package/runtime/python/okstra_ctl/domain/worker_runtime.py +44 -0
  320. package/runtime/python/okstra_ctl/domain/worker_stream.py +24 -0
  321. package/runtime/python/okstra_ctl/error_log_write.py +9 -0
  322. package/runtime/python/okstra_ctl/error_report.py +21 -1
  323. package/runtime/python/okstra_ctl/execution_identity.py +529 -0
  324. package/runtime/python/okstra_ctl/execution_manifest.py +1281 -0
  325. package/runtime/python/okstra_ctl/execution_mutation_audit.py +619 -0
  326. package/runtime/python/okstra_ctl/implementation_outcome.py +21 -5
  327. package/runtime/python/okstra_ctl/index.py +6 -2
  328. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +43 -3
  329. package/runtime/python/okstra_ctl/legacy_model_selection.py +578 -0
  330. package/runtime/python/okstra_ctl/manager_sync.py +4 -1
  331. package/runtime/python/okstra_ctl/model_cli.py +278 -0
  332. package/runtime/python/okstra_ctl/model_defaults.py +46 -0
  333. package/runtime/python/okstra_ctl/model_discovery.py +26 -7
  334. package/runtime/python/okstra_ctl/model_pool.py +501 -0
  335. package/runtime/python/okstra_ctl/models.py +47 -27
  336. package/runtime/python/okstra_ctl/mutation_recovery.py +293 -0
  337. package/runtime/python/okstra_ctl/next_phase.py +236 -0
  338. package/runtime/python/okstra_ctl/pane_title.py +151 -0
  339. package/runtime/python/okstra_ctl/ports/host_model.py +48 -17
  340. package/runtime/python/okstra_ctl/ports/worker_dispatch.py +4 -0
  341. package/runtime/python/okstra_ctl/ports/worker_runtime.py +26 -0
  342. package/runtime/python/okstra_ctl/recap.py +4 -1
  343. package/runtime/python/okstra_ctl/registry/provider_registry.py +8 -4
  344. package/runtime/python/okstra_ctl/render.py +124 -57
  345. package/runtime/python/okstra_ctl/render_final_report.py +19 -0
  346. package/runtime/python/okstra_ctl/report_contract.py +39 -0
  347. package/runtime/python/okstra_ctl/report_finalize.py +16 -0
  348. package/runtime/python/okstra_ctl/report_html/render.py +1 -0
  349. package/runtime/python/okstra_ctl/role_requirements.py +210 -0
  350. package/runtime/python/okstra_ctl/rollup.py +4 -1
  351. package/runtime/python/okstra_ctl/run.py +1210 -258
  352. package/runtime/python/okstra_ctl/run_index_row.py +7 -1
  353. package/runtime/python/okstra_ctl/stage_fix_carry.py +17 -1
  354. package/runtime/python/okstra_ctl/team.py +14 -18
  355. package/runtime/python/okstra_ctl/time_report.py +38 -4
  356. package/runtime/python/okstra_ctl/usage_identity.py +86 -0
  357. package/runtime/python/okstra_ctl/usage_report.py +15 -3
  358. package/runtime/python/okstra_ctl/wizard.py +1449 -66
  359. package/runtime/python/okstra_ctl/worker_audit_ledger.py +68 -65
  360. package/runtime/python/okstra_ctl/worker_prompt_body.py +18 -2
  361. package/runtime/python/okstra_ctl/worker_prompt_contract.py +40 -9
  362. package/runtime/python/okstra_ctl/worker_prompt_headers.py +16 -1
  363. package/runtime/python/okstra_ctl/worker_request.py +7 -1
  364. package/runtime/python/okstra_ctl/worker_runner.py +130 -5
  365. package/runtime/python/okstra_ctl/workers.py +7 -5
  366. package/runtime/python/okstra_ctl/workflow.py +18 -32
  367. package/runtime/python/okstra_ctl/worktree.py +3 -3
  368. package/runtime/python/okstra_ctl/worktree_registry.py +5 -4
  369. package/runtime/python/okstra_ctl/write_policy.py +571 -0
  370. package/runtime/python/okstra_project/state.py +54 -6
  371. package/runtime/schemas/convergence-groups-v2.0.schema.json +128 -0
  372. package/runtime/schemas/execution-manifest-v2.schema.json +340 -0
  373. package/runtime/schemas/final-report-v2.0.schema.json +68 -5
  374. package/runtime/skills/okstra-inspect/facets/recap.md +2 -0
  375. package/runtime/skills/okstra-inspect/facets/report.md +1 -1
  376. package/runtime/skills/okstra-inspect/facets/status.md +15 -13
  377. package/runtime/skills/okstra-rollup/SKILL.md +1 -0
  378. package/runtime/skills/okstra-run/SKILL.md +14 -14
  379. package/runtime/templates/implementation-worker-preamble.md +3 -18
  380. package/runtime/templates/project-docs/task-index.template.md +0 -1
  381. package/runtime/templates/report-writer-prompt-preamble.md +2 -0
  382. package/runtime/templates/reports/final-report-v2.template.md +7 -1
  383. package/runtime/templates/reports/html/base.template.html +21 -0
  384. package/runtime/templates/reports/html/i18n/en.json +4 -0
  385. package/runtime/templates/reports/html/i18n/ko.json +4 -0
  386. package/runtime/templates/reports/html/macros/forms.html +5 -4
  387. package/runtime/templates/reports/html/tasks/final-verification.template.html +1 -1
  388. package/runtime/templates/reports/html/tasks/implementation.template.html +1 -1
  389. package/runtime/templates/worker-prompt-preamble.md +6 -35
  390. package/runtime/validators/lib/fixtures.sh +49 -16
  391. package/runtime/validators/validate-run.py +486 -134
  392. package/runtime/validators/validate_session_conformance.py +25 -7
  393. package/src/cli-registry.mjs +0 -539
  394. package/src/commands/execute/codex-dispatch.mjs +0 -10
  395. package/src/commands/execute/plan-items.mjs +0 -9
  396. package/src/commands/execute/plan-validate.mjs +0 -68
  397. package/src/commands/execute/run.mjs +0 -208
  398. package/src/commands/execute/wizard.mjs +0 -167
  399. package/src/commands/inspect/stage-map.mjs +0 -98
  400. package/src/commands/inspect/task-list.mjs +0 -97
  401. package/src/commands/inspect/task-show.mjs +0 -100
  402. package/src/commands/lifecycle/check-project.mjs +0 -187
  403. package/src/commands/lifecycle/config.mjs +0 -385
  404. package/src/commands/lifecycle/doctor.mjs +0 -308
  405. package/src/commands/lifecycle/install.mjs +0 -1170
  406. package/src/commands/lifecycle/paths.mjs +0 -104
  407. package/src/commands/lifecycle/preflight.mjs +0 -125
  408. package/src/commands/lifecycle/setup.mjs +0 -333
  409. package/src/commands/lifecycle/uninstall.mjs +0 -279
  410. package/src/commands/memory/memory.mjs +0 -524
  411. package/src/commands/pr/pr.mjs +0 -261
  412. package/src/lib/host-registry-client.mjs +0 -176
  413. package/src/lib/paths.mjs +0 -86
  414. package/src/lib/proc.mjs +0 -31
  415. package/src/lib/python-helper.mjs +0 -142
  416. package/src/lib/runtime-manifest.mjs +0 -70
  417. package/src/lib/skill-catalog.mjs +0 -53
  418. package/src/lib/version.mjs +0 -20
  419. /package/{src → dist}/commands/pr/default.md +0 -0
@@ -0,0 +1,1077 @@
1
+ import { promises as fs } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import { homedir } from "node:os";
4
+ import { join, relative, resolve as resolveAbs } from "node:path";
5
+ import { getPackageRoot } from "../../lib/version.mjs";
6
+ import { resolvePaths } from "../../lib/paths.mjs";
7
+ import { fileExists } from "../../lib/proc.mjs";
8
+ import { buildRuntimeManifest, RUNTIMES_MANIFEST_REL } from "../../lib/runtime-manifest.mjs";
9
+ import { normalizeHostRequest, resolveRuntime } from "../../lib/host-registry-client.mjs";
10
+ import { OBSOLETE_SKILL_NAMES, USER_SKILL_NAMES } from "../../lib/skill-catalog.mjs";
11
+ import { AGENTS_MANIFEST_REL, LEGACY_TRANSPORT_AGENT_NAMES, SKILLS_MANIFEST_REL, } from "../../lib/install-assets.mjs";
12
+ const USER_HOME = homedir();
13
+ const CLAUDE_HOME = join(USER_HOME, ".claude");
14
+ const AGENTS_HOME = join(USER_HOME, ".agents");
15
+ const CLAUDE_SKILLS_DIR = join(CLAUDE_HOME, "skills");
16
+ const AGENTS_SKILLS_DIR = join(AGENTS_HOME, "skills");
17
+ const CLAUDE_AGENTS_DIR = join(CLAUDE_HOME, "agents");
18
+ // Source template (relative to runtime root in copy mode, or repo root in link mode).
19
+ const SETTINGS_TEMPLATE_SRC_REL = ["templates", "reports", "settings.template.json"];
20
+ // Destination under ~/.okstra/. Project-local .claude/settings.local.json symlinks here.
21
+ const SETTINGS_TEMPLATE_DST_REL = ["templates", "settings.local.json"];
22
+ const PYTHON_PACKAGES = ["okstra_project", "okstra_ctl", "okstra_token_usage", "okstra_vendor"];
23
+ const BIN_ENTRYPOINTS = [
24
+ "okstra.sh",
25
+ "okstra-codex-exec.sh",
26
+ "okstra-claude-exec.sh",
27
+ "okstra-antigravity-exec.sh",
28
+ "okstra-grok-exec.sh",
29
+ "okstra-kimi-exec.sh",
30
+ "okstra-provider-exec.py",
31
+ "okstra-trace-cleanup.sh",
32
+ "okstra-team-reconcile.sh",
33
+ "okstra-central.sh",
34
+ "okstra-token-usage.py",
35
+ "okstra-error-log.py",
36
+ "okstra-render-report-views.py",
37
+ "okstra-render-final-report.py",
38
+ "okstra-wrapper-status.py",
39
+ "okstra-spawn-followups.py",
40
+ ];
41
+ // Runtime prompt resources the launch prompt and routed coding-preflight pack
42
+ // depend on. Intentionally an exact allowlist, not a directory wildcard.
43
+ const REQUIRED_PROMPT_RESOURCE_FILES = Object.freeze([
44
+ ["prompts", "lead", "okstra-lead-contract.md"],
45
+ ["prompts", "lead", "context-loader.md"],
46
+ ["prompts", "lead", "team-contract.md"],
47
+ ["prompts", "lead", "convergence.md"],
48
+ ["prompts", "lead", "plan-body-verification.md"],
49
+ ["prompts", "lead", "report-writer.md"],
50
+ ["prompts", "coding-preflight", "overview.md"],
51
+ ["prompts", "coding-preflight", "clean-code.md"],
52
+ ["prompts", "coding-preflight", "languages", "javascript-typescript.md"],
53
+ ["prompts", "coding-preflight", "languages", "python.md"],
54
+ ["prompts", "coding-preflight", "languages", "rust.md"],
55
+ ["prompts", "coding-preflight", "languages", "java.md"],
56
+ ["prompts", "coding-preflight", "languages", "kotlin.md"],
57
+ ["prompts", "coding-preflight", "languages", "sql.md"],
58
+ ["prompts", "coding-preflight", "frameworks", "node-server.md"],
59
+ ["prompts", "coding-preflight", "architectures", "hexagonal.md"],
60
+ ]);
61
+ const INSTALL_USAGE = `okstra install — install runtime into ~/.okstra
62
+
63
+ Usage:
64
+ okstra install Install/update runtime (copy mode)
65
+ okstra install --runtime <host-id|alias|all>
66
+ okstra install --link <p> Dev only: symlink ~/.okstra/{lib/python,bin}
67
+ to the source repo at <p>. agents/ resolves
68
+ to <p>/agents. Not advertised to end users.
69
+ okstra install --dry-run Print the plan without touching disk
70
+ okstra install --refresh Re-copy even files that match by hash
71
+
72
+ Effect (copy mode):
73
+ ${"$HOME"}/.okstra/lib/python <- runtime/python
74
+ ${"$HOME"}/.okstra/lib/validators <- runtime/validators (stage/report structure validators)
75
+ ${"$HOME"}/.okstra/bin <- runtime/bin
76
+ ${"$HOME"}/.okstra/templates <- runtime/templates (v1 assets + v2 AI/HTML template trees)
77
+ ${"$HOME"}/.okstra/schemas <- runtime/schemas (final-report schema v1 + v2)
78
+ ${"$HOME"}/.okstra/prompts <- runtime/prompts (lead contracts + coding-preflight pack)
79
+ ${"$HOME"}/.okstra/templates/settings.local.json <- runtime/templates/reports/settings.template.json
80
+ ${"$HOME"}/.claude/skills/<name> <- skills when ${"$HOME"}/.claude exists
81
+ ${"$HOME"}/.agents/skills/<name> <- skills (created when needed)
82
+ ${"$HOME"}/.claude/agents/<worker>.md <- worker agents when ${"$HOME"}/.claude exists
83
+ ${"$HOME"}/.okstra/installed-runtimes.json <- install request/resolution manifest
84
+ ${"$HOME"}/.okstra/installed-skills.json <- target-aware manifest of installed skills
85
+ ${"$HOME"}/.okstra/installed-agents.json <- manifest of installed workers
86
+ ${"$HOME"}/.okstra/version <- installed package version stamp
87
+
88
+ Effect (link mode):
89
+ ${"$HOME"}/.okstra/lib/python/<pkg> -> <repo>/scripts/<pkg> (symlink)
90
+ ${"$HOME"}/.okstra/bin/<name>.sh -> <repo>/scripts/<name>.sh
91
+ ${"$HOME"}/.okstra/prompts -> <repo>/prompts
92
+ ${"$HOME"}/.okstra/templates/settings.local.json -> <repo>/templates/reports/settings.template.json
93
+ ${"$HOME"}/.claude/skills/<name> -> <repo>/skills/<name> when ${"$HOME"}/.claude exists
94
+ ${"$HOME"}/.agents/skills/<name> -> <repo>/skills/<name> (created when needed)
95
+ ${"$HOME"}/.claude/agents/<worker>.md -> <repo>/agents/workers/<worker>.md when ${"$HOME"}/.claude exists
96
+ ${"$HOME"}/.okstra/dev-link <- <repo> path stamp
97
+ ${"$HOME"}/.okstra/version <- installed package version stamp
98
+
99
+ The settings.local.json file is the symlink target referenced by every
100
+ project-local <project>/.claude/settings.local.json that okstra-setup
101
+ provisions, granting per-project Claude Code permissions for okstra
102
+ worker wrapper scripts without modifying the user's global settings.
103
+
104
+ Worker agent definitions are installed into ${"$HOME"}/.claude/agents/ only
105
+ when ${"$HOME"}/.claude exists, so Claude Code's subagent discovery can pick
106
+ them up. Agent skill targets receive skill markdown only.
107
+
108
+ Default auto runtime installation does not require host detection; it still
109
+ installs shared runtime files, the Agent skill target, and any Claude home it finds.
110
+ `;
111
+ const ENSURE_USAGE = `okstra ensure-installed — idempotent install check
112
+
113
+ Usage:
114
+ okstra ensure-installed Verify install; reinstall if version/files drift
115
+ okstra ensure-installed --runtime <host-id|alias|all>
116
+ okstra ensure-installed -q Same, but suppress success output
117
+
118
+ Skills call this on every run. Returns 0 quickly when the install is fresh.
119
+ `;
120
+ export function runtimeIncludesClaudeAssets(resolution) {
121
+ return resolution?.installTargets?.includes("claude") === true;
122
+ }
123
+ function readRuntimeFlag(args, index) {
124
+ const arg = args[index];
125
+ if (arg.startsWith("--runtime=")) {
126
+ return { value: arg.slice("--runtime=".length), nextIndex: index };
127
+ }
128
+ const next = args[index + 1];
129
+ if (!next || next.startsWith("--")) {
130
+ throw new Error("--runtime requires a host ID, alias, or all");
131
+ }
132
+ return { value: next, nextIndex: index + 1 };
133
+ }
134
+ export function validateInstallRuntime(value) {
135
+ return normalizeHostRequest(value);
136
+ }
137
+ async function resolveInstallRuntime(runtime, command) {
138
+ const resolution = await resolveRuntime({
139
+ requestedRuntime: runtime,
140
+ command,
141
+ env: process.env,
142
+ capabilities: { tmuxAvailable: Boolean(process.env.TMUX) },
143
+ });
144
+ if (resolution.ok || runtime !== "auto")
145
+ return resolution;
146
+ return {
147
+ ...resolution,
148
+ confidence: "none",
149
+ warnings: [resolution.reason],
150
+ };
151
+ }
152
+ function formatInstallRuntimeLabel(runtime, resolution) {
153
+ if (resolution.resolvedRuntime)
154
+ return `${runtime} -> ${resolution.resolvedRuntime}`;
155
+ return `${runtime} (host unresolved; installing target assets)`;
156
+ }
157
+ async function hashFile(path) {
158
+ const buf = await fs.readFile(path);
159
+ return createHash("sha256").update(buf).digest("hex");
160
+ }
161
+ async function dirExists(path) {
162
+ try {
163
+ const st = await fs.stat(path);
164
+ return st.isDirectory();
165
+ }
166
+ catch {
167
+ return false;
168
+ }
169
+ }
170
+ async function pathEntryExists(path) {
171
+ try {
172
+ await fs.lstat(path);
173
+ return true;
174
+ }
175
+ catch (err) {
176
+ if (err.code === "ENOENT")
177
+ return false;
178
+ throw err;
179
+ }
180
+ }
181
+ async function* walkFiles(root) {
182
+ let entries;
183
+ try {
184
+ entries = await fs.readdir(root, { withFileTypes: true });
185
+ }
186
+ catch (err) {
187
+ if (err.code === "ENOENT")
188
+ return;
189
+ throw err;
190
+ }
191
+ for (const ent of entries) {
192
+ if (ent.name === "__pycache__" || ent.name === ".DS_Store")
193
+ continue;
194
+ if (ent.name.endsWith(".pyc"))
195
+ continue;
196
+ const abs = join(root, ent.name);
197
+ if (ent.isDirectory()) {
198
+ yield* walkFiles(abs);
199
+ }
200
+ else if (ent.isFile()) {
201
+ yield abs;
202
+ }
203
+ }
204
+ }
205
+ async function writeFileAtomic(target, data, mode) {
206
+ await fs.mkdir(join(target, ".."), { recursive: true });
207
+ const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
208
+ await fs.writeFile(tmp, data, { mode });
209
+ await fs.rename(tmp, target);
210
+ }
211
+ // Files the destination has and the payload no longer ships. Left in place they
212
+ // are indistinguishable from current code — Python imports whatever is on disk,
213
+ // so a module dropped in release N+1 keeps being importable after the upgrade,
214
+ // and a rename leaves both spellings live at once.
215
+ async function pruneOrphans(srcRoot, dstRoot, opts) {
216
+ const { dryRun = false } = opts ?? {};
217
+ if (!(await dirExists(dstRoot)))
218
+ return 0;
219
+ let pruned = 0;
220
+ for await (const dstPath of walkFiles(dstRoot)) {
221
+ const rel = relative(dstRoot, dstPath);
222
+ try {
223
+ await fs.access(join(srcRoot, rel));
224
+ continue;
225
+ }
226
+ catch {
227
+ // not in the payload → orphan
228
+ }
229
+ if (dryRun) {
230
+ process.stdout.write(`[dry-run] prune ${dstPath}\n`);
231
+ }
232
+ else {
233
+ await fs.rm(dstPath, { force: true });
234
+ }
235
+ pruned++;
236
+ }
237
+ return pruned;
238
+ }
239
+ async function copyTreeIfChanged(srcRoot, dstRoot, opts) {
240
+ const { refresh = false, dryRun = false, mode } = opts ?? {};
241
+ let copied = 0;
242
+ let skipped = 0;
243
+ let pruned = 0;
244
+ let missingSource = false;
245
+ if (!(await dirExists(srcRoot))) {
246
+ missingSource = true;
247
+ return { copied, skipped, pruned, missingSource };
248
+ }
249
+ for await (const srcPath of walkFiles(srcRoot)) {
250
+ const rel = relative(srcRoot, srcPath);
251
+ const dstPath = join(dstRoot, rel);
252
+ let needsCopy = refresh;
253
+ if (!needsCopy) {
254
+ try {
255
+ await fs.access(dstPath);
256
+ const [srcHash, dstHash] = await Promise.all([hashFile(srcPath), hashFile(dstPath)]);
257
+ needsCopy = srcHash !== dstHash;
258
+ }
259
+ catch {
260
+ needsCopy = true;
261
+ }
262
+ }
263
+ if (!needsCopy) {
264
+ skipped++;
265
+ continue;
266
+ }
267
+ if (dryRun) {
268
+ process.stdout.write(`[dry-run] copy ${srcPath} -> ${dstPath}\n`);
269
+ }
270
+ else {
271
+ const buf = await fs.readFile(srcPath);
272
+ await writeFileAtomic(dstPath, buf, mode);
273
+ }
274
+ copied++;
275
+ }
276
+ pruned = await pruneOrphans(srcRoot, dstRoot, { dryRun });
277
+ return { copied, skipped, pruned, missingSource };
278
+ }
279
+ async function ensureSymlink(target, linkPath, opts) {
280
+ const { dryRun = false } = opts ?? {};
281
+ try {
282
+ const current = await fs.readlink(linkPath);
283
+ if (current === target)
284
+ return "skipped";
285
+ }
286
+ catch (err) {
287
+ if (err.code !== "ENOENT" && err.code !== "EINVAL")
288
+ throw err;
289
+ }
290
+ if (dryRun) {
291
+ process.stdout.write(`[dry-run] symlink ${linkPath} -> ${target}\n`);
292
+ return "linked";
293
+ }
294
+ await fs.mkdir(join(linkPath, ".."), { recursive: true });
295
+ // readlink returns EINVAL for both a missing link and an existing non-symlink
296
+ // file, so unlink alone would silently destroy a real file. lstat lets us
297
+ // refuse to clobber any existing entry that is not a symlink.
298
+ let existing;
299
+ try {
300
+ existing = await fs.lstat(linkPath);
301
+ }
302
+ catch (err) {
303
+ if (err.code !== "ENOENT")
304
+ throw err;
305
+ }
306
+ if (existing && !existing.isSymbolicLink()) {
307
+ throw new Error(`refuse to replace ${linkPath} (not a symlink). Remove it manually and retry, or run 'okstra uninstall' first.`);
308
+ }
309
+ if (existing)
310
+ await fs.unlink(linkPath);
311
+ await fs.symlink(target, linkPath);
312
+ return "linked";
313
+ }
314
+ async function installLinkMode(repoPath, paths, opts) {
315
+ const { dryRun, quiet, runtime } = opts;
316
+ const repoAbs = resolveAbs(repoPath);
317
+ if (!(await dirExists(repoAbs))) {
318
+ throw new Error(`repo path not found: ${repoAbs}`);
319
+ }
320
+ const scriptsDir = join(repoAbs, "scripts");
321
+ if (!(await dirExists(scriptsDir))) {
322
+ throw new Error(`<repo>/scripts not found: ${scriptsDir}`);
323
+ }
324
+ const agentsDir = join(repoAbs, "agents");
325
+ if (!(await dirExists(agentsDir))) {
326
+ throw new Error(`<repo>/agents not found: ${agentsDir}`);
327
+ }
328
+ if (!quiet) {
329
+ process.stdout.write(`installing okstra runtime in LINK mode (package ${paths.package})\n`);
330
+ process.stdout.write(` runtime: ${runtime}\n`);
331
+ process.stdout.write(` repo: ${repoAbs}\n`);
332
+ process.stdout.write(` home: ${paths.home}\n`);
333
+ }
334
+ if (!dryRun) {
335
+ await fs.mkdir(paths.pythonpath, { recursive: true });
336
+ await fs.mkdir(paths.bin, { recursive: true });
337
+ }
338
+ for (const pkg of PYTHON_PACKAGES) {
339
+ const src = join(scriptsDir, pkg);
340
+ const dst = join(paths.pythonpath, pkg);
341
+ if (!(await dirExists(src))) {
342
+ if (!quiet)
343
+ process.stdout.write(` python/${pkg}: missing in repo — skipped\n`);
344
+ continue;
345
+ }
346
+ const action = await ensureSymlink(src, dst, { dryRun });
347
+ if (!quiet)
348
+ process.stdout.write(` python/${pkg}: ${action}\n`);
349
+ }
350
+ for (const name of BIN_ENTRYPOINTS) {
351
+ const src = join(scriptsDir, name);
352
+ const dst = join(paths.bin, name);
353
+ if (!(await fileExists(src))) {
354
+ if (!quiet)
355
+ process.stdout.write(` bin/${name}: missing in repo — skipped\n`);
356
+ continue;
357
+ }
358
+ const action = await ensureSymlink(src, dst, { dryRun });
359
+ if (!quiet)
360
+ process.stdout.write(` bin/${name}: ${action}\n`);
361
+ }
362
+ const promptsSrc = join(repoAbs, "prompts");
363
+ const promptsDst = join(paths.home, "prompts");
364
+ if (await dirExists(promptsSrc)) {
365
+ const action = await ensureSymlink(promptsSrc, promptsDst, { dryRun });
366
+ if (!quiet)
367
+ process.stdout.write(` prompts: ${action}\n`);
368
+ }
369
+ else if (!quiet) {
370
+ process.stdout.write(" prompts: missing in repo — skipped\n");
371
+ }
372
+ const skillTargets = await detectSkillTargets();
373
+ const installedSkillTargets = await installSkillTargetsLink(repoAbs, skillTargets, { dryRun, quiet });
374
+ await writeSkillsManifest(paths.home, installedSkillTargets, { dryRun });
375
+ let agentResult = { installed: [] };
376
+ if (skillTargets.some((target) => target.provider === "claude")) {
377
+ await pruneOwnedLegacyTransportAgents(paths.home, { dryRun, quiet });
378
+ agentResult = await installAgentsLink(repoAbs, { dryRun, quiet });
379
+ }
380
+ await writeAgentsManifest(paths.home, agentResult.installed, { dryRun });
381
+ await installNamedTemplate(repoAbs, paths, { mode: "link", dryRun, quiet }, SETTINGS_TEMPLATE_DESCRIPTOR);
382
+ await writeRuntimeManifest(paths.home, runtime, {
383
+ dryRun,
384
+ resolution: opts.resolution,
385
+ installedAssets: installedAssetOverrides(installedSkillTargets, agentResult.installed),
386
+ });
387
+ if (!dryRun) {
388
+ await writeFileAtomic(join(paths.home, "dev-link"), repoAbs + "\n", 0o644);
389
+ await writeFileAtomic(join(paths.home, "version"), paths.package + "\n", 0o644);
390
+ }
391
+ if (!quiet) {
392
+ process.stdout.write(` dev-link stamp: ${repoAbs}\n`);
393
+ process.stdout.write(` version stamp: ${paths.package}\n`);
394
+ process.stdout.write("done.\n");
395
+ process.stdout.write("\nNext step: register the current project.\n" +
396
+ " cd <your project> && npx -y okstra@latest setup --project-id <id>\n" +
397
+ " (or run /okstra-setup inside a Harness Agent session (e.g. claude codex, codex, pi.. ))\n" +
398
+ "\nTip: to use a bare 'okstra' command instead of npx, run:\n" +
399
+ " npm i -g okstra\n");
400
+ }
401
+ return 0;
402
+ }
403
+ async function detectSkillTargets() {
404
+ const targets = [];
405
+ const hasClaude = await dirExists(CLAUDE_HOME);
406
+ if (hasClaude) {
407
+ targets.push({ provider: "claude", home: CLAUDE_HOME, skillsDir: CLAUDE_SKILLS_DIR });
408
+ }
409
+ targets.push({ provider: "agent", home: AGENTS_HOME, skillsDir: AGENTS_SKILLS_DIR });
410
+ return targets;
411
+ }
412
+ function skillTargetManifest(targets) {
413
+ return targets.map((target) => ({
414
+ provider: target.provider,
415
+ root: target.skillsDir,
416
+ skills: Array.from(new Set(target.installed)).sort(),
417
+ }));
418
+ }
419
+ function installedAssetOverrides(skillTargets, agentNames) {
420
+ const providers = new Set(skillTargets.map((target) => target.provider));
421
+ return {
422
+ claudeSkills: providers.has("claude"),
423
+ claudeAgents: agentNames.length > 0,
424
+ codexSkills: providers.has("codex"),
425
+ skillTargets: Array.from(providers).sort(),
426
+ };
427
+ }
428
+ function installedSkillNames(targets) {
429
+ return Array.from(new Set(targets.flatMap((target) => target.installed))).sort();
430
+ }
431
+ async function writeSkillsManifest(home, targets, opts) {
432
+ const { dryRun = false } = opts ?? {};
433
+ const manifestTargets = skillTargetManifest(targets);
434
+ const data = {
435
+ version: 2,
436
+ installedAt: new Date().toISOString(),
437
+ skills: installedSkillNames(targets),
438
+ targets: manifestTargets,
439
+ };
440
+ if (dryRun) {
441
+ process.stdout.write(`[dry-run] write skills manifest: ${data.skills.length} skills across ${data.targets.length} targets\n`);
442
+ return;
443
+ }
444
+ await writeFileAtomic(join(home, SKILLS_MANIFEST_REL), JSON.stringify(data, null, 2) + "\n", 0o644);
445
+ }
446
+ async function writeAgentsManifest(home, names, opts) {
447
+ const { dryRun = false } = opts ?? {};
448
+ const data = {
449
+ version: 1,
450
+ installedAt: new Date().toISOString(),
451
+ agents: Array.from(new Set(names)).sort(),
452
+ };
453
+ if (dryRun) {
454
+ process.stdout.write(`[dry-run] write agents manifest: ${data.agents.length} entries\n`);
455
+ return;
456
+ }
457
+ await writeFileAtomic(join(home, AGENTS_MANIFEST_REL), JSON.stringify(data, null, 2) + "\n", 0o644);
458
+ }
459
+ async function pruneOwnedLegacyTransportAgents(home, opts) {
460
+ const { dryRun = false, quiet = false } = opts ?? {};
461
+ let previous = [];
462
+ try {
463
+ const data = JSON.parse(await fs.readFile(join(home, AGENTS_MANIFEST_REL), "utf8"));
464
+ if (Array.isArray(data?.agents))
465
+ previous = data.agents;
466
+ }
467
+ catch (err) {
468
+ if (err.code !== "ENOENT" && !(err instanceof SyntaxError))
469
+ throw err;
470
+ }
471
+ const ownedLegacy = LEGACY_TRANSPORT_AGENT_NAMES.filter((name) => (previous.includes(name)));
472
+ for (const name of ownedLegacy) {
473
+ const target = join(CLAUDE_AGENTS_DIR, name);
474
+ if (dryRun) {
475
+ process.stdout.write(`[dry-run] remove retired agent ${target}\n`);
476
+ continue;
477
+ }
478
+ try {
479
+ await fs.unlink(target);
480
+ if (!quiet)
481
+ process.stdout.write(` agents/${name}: removed retired wrapper\n`);
482
+ }
483
+ catch (err) {
484
+ if (err.code !== "ENOENT")
485
+ throw err;
486
+ }
487
+ }
488
+ }
489
+ async function writeRuntimeManifest(home, runtime, opts) {
490
+ const { dryRun = false, resolution = null, installedAssets = null } = opts ?? {};
491
+ const data = buildRuntimeManifest(runtime, new Date().toISOString(), resolution, installedAssets);
492
+ if (dryRun) {
493
+ process.stdout.write(`[dry-run] write runtime manifest: ${data.installedRuntimes.join(",")}\n`);
494
+ return;
495
+ }
496
+ await writeFileAtomic(join(home, RUNTIMES_MANIFEST_REL), JSON.stringify(data, null, 2) + "\n", 0o644);
497
+ }
498
+ async function listWorkerFiles(workersRoot) {
499
+ try {
500
+ const entries = await fs.readdir(workersRoot, { withFileTypes: true });
501
+ return entries
502
+ .filter((e) => (e.isFile() && e.name.endsWith(".md") && !e.name.startsWith("_")))
503
+ .map((e) => e.name);
504
+ }
505
+ catch (err) {
506
+ if (err.code === "ENOENT")
507
+ return [];
508
+ throw err;
509
+ }
510
+ }
511
+ async function installAgentsCopy(runtimeRoot, opts) {
512
+ const { refresh, dryRun, quiet } = opts;
513
+ const srcRoot = join(runtimeRoot, "agents", "workers");
514
+ const names = await listWorkerFiles(srcRoot);
515
+ if (names.length === 0) {
516
+ if (!quiet)
517
+ process.stdout.write(" agents: runtime/agents/workers empty — skipped\n");
518
+ return { installed: [] };
519
+ }
520
+ if (!dryRun)
521
+ await fs.mkdir(CLAUDE_AGENTS_DIR, { recursive: true });
522
+ let copied = 0;
523
+ let skipped = 0;
524
+ for (const name of names) {
525
+ const src = join(srcRoot, name);
526
+ const dst = join(CLAUDE_AGENTS_DIR, name);
527
+ let needsCopy = refresh;
528
+ if (!needsCopy) {
529
+ try {
530
+ await fs.access(dst);
531
+ const [a, b] = await Promise.all([hashFile(src), hashFile(dst)]);
532
+ needsCopy = a !== b;
533
+ }
534
+ catch {
535
+ needsCopy = true;
536
+ }
537
+ }
538
+ if (!needsCopy) {
539
+ skipped++;
540
+ continue;
541
+ }
542
+ if (dryRun) {
543
+ process.stdout.write(`[dry-run] copy ${src} -> ${dst}\n`);
544
+ }
545
+ else {
546
+ const buf = await fs.readFile(src);
547
+ await writeFileAtomic(dst, buf, 0o644);
548
+ }
549
+ copied++;
550
+ }
551
+ if (!quiet) {
552
+ process.stdout.write(` agents: copied=${copied} skipped=${skipped} -> ${CLAUDE_AGENTS_DIR}/ (${names.length} workers)\n`);
553
+ }
554
+ return { installed: names };
555
+ }
556
+ async function installAgentsLink(repoAbs, opts) {
557
+ const { dryRun, quiet } = opts;
558
+ const srcRoot = join(repoAbs, "agents", "workers");
559
+ const sourceNames = await listWorkerFiles(srcRoot);
560
+ const names = [...sourceNames].sort();
561
+ if (names.length === 0) {
562
+ if (!quiet)
563
+ process.stdout.write(" agents: <repo>/agents/workers missing — skipped\n");
564
+ return { installed: [] };
565
+ }
566
+ if (!dryRun)
567
+ await fs.mkdir(CLAUDE_AGENTS_DIR, { recursive: true });
568
+ for (const name of sourceNames) {
569
+ const src = join(srcRoot, name);
570
+ const dst = join(CLAUDE_AGENTS_DIR, name);
571
+ const action = await ensureSymlink(src, dst, { dryRun });
572
+ if (!quiet)
573
+ process.stdout.write(` agents/${name}: ${action}\n`);
574
+ }
575
+ return { installed: names };
576
+ }
577
+ async function installNamedTemplate(srcRoot, paths, opts, descriptor) {
578
+ const { mode, refresh = false, dryRun = false, quiet = false } = opts;
579
+ const { srcRel, dstRel, label } = descriptor;
580
+ const src = join(srcRoot, ...srcRel);
581
+ const dst = join(paths.home, ...dstRel);
582
+ if (!(await fileExists(src))) {
583
+ if (!quiet)
584
+ process.stdout.write(` ${label}: source missing — skipped (${src})\n`);
585
+ return { installed: false };
586
+ }
587
+ if (!dryRun)
588
+ await fs.mkdir(join(dst, ".."), { recursive: true });
589
+ if (mode === "link") {
590
+ const action = await ensureSymlink(src, dst, { dryRun });
591
+ if (!quiet)
592
+ process.stdout.write(` ${label}: ${action} (${dst} -> ${src})\n`);
593
+ return { installed: action !== "skipped" };
594
+ }
595
+ // copy mode — hash-skip mirrors copyTreeIfChanged behavior for a single file.
596
+ let needsCopy = refresh;
597
+ if (!needsCopy) {
598
+ try {
599
+ await fs.access(dst);
600
+ const [srcHash, dstHash] = await Promise.all([hashFile(src), hashFile(dst)]);
601
+ needsCopy = srcHash !== dstHash;
602
+ }
603
+ catch {
604
+ needsCopy = true;
605
+ }
606
+ }
607
+ if (!needsCopy) {
608
+ if (!quiet)
609
+ process.stdout.write(` ${label}: skipped (hash match)\n`);
610
+ return { installed: false };
611
+ }
612
+ if (dryRun) {
613
+ process.stdout.write(`[dry-run] copy ${src} -> ${dst}\n`);
614
+ }
615
+ else {
616
+ const buf = await fs.readFile(src);
617
+ await writeFileAtomic(dst, buf, 0o644);
618
+ }
619
+ if (!quiet)
620
+ process.stdout.write(` ${label}: copied -> ${dst}\n`);
621
+ return { installed: true };
622
+ }
623
+ const SETTINGS_TEMPLATE_DESCRIPTOR = {
624
+ srcRel: SETTINGS_TEMPLATE_SRC_REL,
625
+ dstRel: SETTINGS_TEMPLATE_DST_REL,
626
+ label: "settings template",
627
+ };
628
+ async function installSkillsCopy(runtimeRoot, target, opts) {
629
+ const { refresh, dryRun, quiet } = opts;
630
+ const srcRoot = join(runtimeRoot, "skills");
631
+ let copied = 0;
632
+ let skipped = 0;
633
+ for (const name of USER_SKILL_NAMES) {
634
+ const r = await copyTreeIfChanged(join(srcRoot, name), join(target.skillsDir, name), { refresh, dryRun, mode: 0o644 });
635
+ copied += r.copied;
636
+ skipped += r.skipped;
637
+ }
638
+ const pruned = await pruneObsoleteSkills(target, opts);
639
+ if (!quiet) {
640
+ process.stdout.write(` ${target.provider} skills: copied=${copied} skipped=${skipped} pruned=${pruned} -> ${target.skillsDir}/ (${USER_SKILL_NAMES.length} skills)\n`);
641
+ }
642
+ return { ...target, installed: [...USER_SKILL_NAMES] };
643
+ }
644
+ async function installSkillsLink(repoAbs, target, opts) {
645
+ const { dryRun, quiet } = opts;
646
+ const srcRoot = join(repoAbs, "skills");
647
+ if (!dryRun)
648
+ await fs.mkdir(target.skillsDir, { recursive: true });
649
+ for (const name of USER_SKILL_NAMES) {
650
+ const src = join(srcRoot, name);
651
+ const dst = join(target.skillsDir, name);
652
+ const action = await ensureSymlink(src, dst, { dryRun });
653
+ if (!quiet)
654
+ process.stdout.write(` ${target.provider} skills/${name}: ${action}\n`);
655
+ }
656
+ const pruned = await pruneObsoleteSkills(target, opts);
657
+ if (!quiet && pruned > 0) {
658
+ process.stdout.write(` ${target.provider} obsolete internal skills: pruned=${pruned}\n`);
659
+ }
660
+ return { ...target, installed: [...USER_SKILL_NAMES] };
661
+ }
662
+ // Remove former skill directories left by older installs. Exact-name only
663
+ // (OBSOLETE_SKILL_NAMES) — never wildcard-delete okstra-*.
664
+ async function pruneObsoleteSkills(target, opts) {
665
+ let pruned = 0;
666
+ for (const name of OBSOLETE_SKILL_NAMES) {
667
+ const path = join(target.skillsDir, name);
668
+ if (!(await pathEntryExists(path)))
669
+ continue;
670
+ pruned += 1;
671
+ if (opts.dryRun) {
672
+ process.stdout.write(`[dry-run] rm ${path}\n`);
673
+ continue;
674
+ }
675
+ await fs.rm(path, { recursive: true, force: true });
676
+ }
677
+ return pruned;
678
+ }
679
+ async function installSkillTargetsCopy(runtimeRoot, targets, opts) {
680
+ if (targets.length === 0) {
681
+ if (!opts.quiet)
682
+ process.stdout.write(" skills: no skill target found — skipped\n");
683
+ return [];
684
+ }
685
+ return await Promise.all(targets.map((target) => installSkillsCopy(runtimeRoot, target, opts)));
686
+ }
687
+ async function installSkillTargetsLink(repoAbs, targets, opts) {
688
+ if (targets.length === 0) {
689
+ if (!opts.quiet)
690
+ process.stdout.write(" skills: no skill target found — skipped\n");
691
+ return [];
692
+ }
693
+ return await Promise.all(targets.map((target) => installSkillsLink(repoAbs, target, opts)));
694
+ }
695
+ export function parseInstallArgs(args) {
696
+ const result = {
697
+ dryRun: false,
698
+ refresh: false,
699
+ quiet: false,
700
+ linkRepo: null,
701
+ runtime: "auto",
702
+ };
703
+ for (let i = 0; i < args.length; i++) {
704
+ const a = args[i];
705
+ if (a === "--dry-run")
706
+ result.dryRun = true;
707
+ else if (a === "--refresh")
708
+ result.refresh = true;
709
+ else if (a === "-q" || a === "--quiet")
710
+ result.quiet = true;
711
+ else if (a === "--runtime" || a.startsWith("--runtime=")) {
712
+ const parsed = readRuntimeFlag(args, i);
713
+ result.runtime = validateInstallRuntime(parsed.value);
714
+ i = parsed.nextIndex;
715
+ }
716
+ else if (a === "--link") {
717
+ const next = args[i + 1];
718
+ if (!next || next.startsWith("--")) {
719
+ throw new Error("--link requires a repo path");
720
+ }
721
+ result.linkRepo = next;
722
+ i++;
723
+ }
724
+ else {
725
+ throw new Error(`unknown argument '${a}'`);
726
+ }
727
+ }
728
+ return result;
729
+ }
730
+ export async function runInstall(args) {
731
+ if (args.includes("--help") || args.includes("-h")) {
732
+ process.stdout.write(INSTALL_USAGE);
733
+ return 0;
734
+ }
735
+ let opts;
736
+ try {
737
+ opts = parseInstallArgs(args);
738
+ }
739
+ catch (err) {
740
+ process.stderr.write(`error: ${err.message}\n`);
741
+ return 2;
742
+ }
743
+ const pkgRoot = getPackageRoot();
744
+ const runtimeRoot = join(pkgRoot, "runtime");
745
+ const paths = await resolvePaths();
746
+ const runtimeResolution = await resolveInstallRuntime(opts.runtime, "install");
747
+ if (!runtimeResolution.ok && opts.runtime !== "auto") {
748
+ process.stderr.write(`error: ${runtimeResolution.reason}\n`);
749
+ return 2;
750
+ }
751
+ if (!opts.dryRun) {
752
+ await fs.mkdir(paths.home, { recursive: true });
753
+ }
754
+ if (opts.linkRepo) {
755
+ return await installLinkMode(opts.linkRepo, paths, { ...opts, resolution: runtimeResolution });
756
+ }
757
+ // copy mode — if a prior dev-link is present, refuse rather than silently overwriting.
758
+ if (paths["dev-link"]) {
759
+ throw new Error(`~/.okstra/dev-link is set to ${paths["dev-link"]}. Run 'okstra uninstall' first, then 'okstra install' in copy mode.`);
760
+ }
761
+ if (!opts.quiet) {
762
+ process.stdout.write(`installing okstra runtime (package ${paths.package})\n`);
763
+ process.stdout.write(` runtime: ${formatInstallRuntimeLabel(opts.runtime, runtimeResolution)}\n`);
764
+ process.stdout.write(` source: ${runtimeRoot}\n`);
765
+ process.stdout.write(` home: ${paths.home}\n`);
766
+ }
767
+ const pythonResult = await copyTreeIfChanged(join(runtimeRoot, "python"), paths.pythonpath, { refresh: opts.refresh, dryRun: opts.dryRun, mode: 0o644 });
768
+ const binResult = await copyTreeIfChanged(join(runtimeRoot, "bin"), paths.bin, { refresh: opts.refresh, dryRun: opts.dryRun, mode: 0o755 });
769
+ // templates/ tree — the v1 report.css/report.js compatibility assets, the
770
+ // v2 AI handoff template, and the task-specific html/** tree are consumed by
771
+ // okstra-render-report-views.py and final-report assembly. They
772
+ // are NOT covered by installNamedTemplate (which only handles the renamed
773
+ // settings.local.json sidecar), so without this step copy-mode installs
774
+ // miss every asset other than that single file. See
775
+ // okstra-render-report-views.py _TEMPLATES_DIRS for the lookup path.
776
+ const templatesResult = await copyTreeIfChanged(join(runtimeRoot, "templates"), join(paths.home, "templates"), { refresh: opts.refresh, dryRun: opts.dryRun, mode: 0o644 });
777
+ // schemas/ tree — version-selected final-report v1/v2 schemas are loaded by
778
+ // okstra_ctl.final_report_schema. They support bundle excerpts, legacy
779
+ // re-renders, new task HTML, and validate-run. Link mode resolves the same
780
+ // files through the repo symlink.
781
+ const schemasResult = await copyTreeIfChanged(join(runtimeRoot, "schemas"), join(paths.home, "schemas"), { refresh: opts.refresh, dryRun: opts.dryRun, mode: 0o644 });
782
+ // validators/ tree — validate-implementation-plan-stages.py (+ lib/) is
783
+ // resolved at runtime by run.py / validate-run.py via the installed package's
784
+ // `parents[2]/validators`, i.e. ~/.okstra/lib/validators. Without this step a
785
+ // copy-mode UPGRADE bumps the version stamp but never refreshes the
786
+ // validators, so a stale validator (e.g. a pre-renumber section schema)
787
+ // survives and render-bundle's stage-structure check diverges from the
788
+ // current template (observed: render-bundle demanding `## 4.5 Stage Map`
789
+ // while the report + wizard validator use `## 5.5`). Mode 0o755 preserves the
790
+ // executable .sh helpers in the tree.
791
+ const validatorsResult = await copyTreeIfChanged(join(runtimeRoot, "validators"), join(paths.home, "lib", "validators"), { refresh: opts.refresh, dryRun: opts.dryRun, mode: 0o755 });
792
+ // prompts/ tree — lead/internal operating contracts (prompts/lead/*.md) and
793
+ // the coding-preflight resource pack (prompts/coding-preflight/*) are read at
794
+ // runtime by the lead and executor via absolute ~/.okstra/prompts/ paths
795
+ // (paths.py OKSTRA_LEAD_CONTRACT_PATH / OKSTRA_CODING_PREFLIGHT_DIR). Run-prep
796
+ // reads prompts/profiles + launch.template from the package runtime dir, but
797
+ // the lead's runtime Read needs them under the home tree like templates.
798
+ const promptsResult = await copyTreeIfChanged(join(runtimeRoot, "prompts"), join(paths.home, "prompts"), { refresh: opts.refresh, dryRun: opts.dryRun, mode: 0o644 });
799
+ if (!opts.quiet) {
800
+ summarise("python", pythonResult, paths.pythonpath);
801
+ summarise("bin", binResult, paths.bin);
802
+ summarise("templates", templatesResult, join(paths.home, "templates"));
803
+ summarise("schemas", schemasResult, join(paths.home, "schemas"));
804
+ summarise("validators", validatorsResult, join(paths.home, "lib", "validators"));
805
+ summarise("prompts", promptsResult, join(paths.home, "prompts"));
806
+ }
807
+ if (pythonResult.missingSource && binResult.missingSource) {
808
+ process.stderr.write("warning: runtime/{python,bin} are both empty. Runtime sync (build step) has not been performed.\n");
809
+ }
810
+ if (templatesResult.missingSource) {
811
+ process.stderr.write("warning: runtime/templates is empty. final-report Markdown and HTML templates will be missing — re-run the build step.\n");
812
+ }
813
+ if (schemasResult.missingSource) {
814
+ process.stderr.write("warning: runtime/schemas is empty. final-report schema validation + excerpt generation will be unavailable — re-run the build step.\n");
815
+ }
816
+ if (validatorsResult.missingSource) {
817
+ process.stderr.write("warning: runtime/validators is empty. stage-structure / report validation will use stale or missing validators — re-run the build step.\n");
818
+ }
819
+ if (promptsResult.missingSource) {
820
+ process.stderr.write("warning: runtime/prompts is empty. lead contracts + coding-preflight resources will be missing — re-run the build step.\n");
821
+ }
822
+ const skillTargets = await detectSkillTargets();
823
+ const installedSkillTargets = await installSkillTargetsCopy(runtimeRoot, skillTargets, opts);
824
+ await writeSkillsManifest(paths.home, installedSkillTargets, { dryRun: opts.dryRun });
825
+ let agentResult = { installed: [] };
826
+ if (skillTargets.some((target) => target.provider === "claude")) {
827
+ await pruneOwnedLegacyTransportAgents(paths.home, opts);
828
+ agentResult = await installAgentsCopy(runtimeRoot, opts);
829
+ }
830
+ await writeAgentsManifest(paths.home, agentResult.installed, { dryRun: opts.dryRun });
831
+ await installNamedTemplate(runtimeRoot, paths, { mode: "copy", ...opts }, SETTINGS_TEMPLATE_DESCRIPTOR);
832
+ await writeRuntimeManifest(paths.home, opts.runtime, {
833
+ dryRun: opts.dryRun,
834
+ resolution: runtimeResolution,
835
+ installedAssets: installedAssetOverrides(installedSkillTargets, agentResult.installed),
836
+ });
837
+ if (!opts.dryRun) {
838
+ await writeFileAtomic(join(paths.home, "version"), paths.package + "\n", 0o644);
839
+ }
840
+ if (!opts.quiet) {
841
+ // The source is already named at the top of the run; what the stamp line
842
+ // could not say is whether this install moved anything. `copied=0 skipped=N`
843
+ // plus a bare version reads identically for "already current" and "an older
844
+ // payload just landed over a newer home" — the latter cost a full run,
845
+ // caught only by hand-diffing installed files.
846
+ process.stdout.write(` version stamp: ${formatVersionTransition(paths.version, paths.package)}\n`);
847
+ process.stdout.write("done.\n");
848
+ process.stdout.write("\nNext step: register the current project.\n" +
849
+ " cd <your project> && npx -y okstra@latest setup --project-id <id>\n" +
850
+ " (or run /okstra-setup inside a Harness Agent session (e.g. claude codex, codex, pi.. ))\n" +
851
+ "\nTip: to use a bare 'okstra' command instead of npx, run:\n" +
852
+ " npm i -g okstra\n");
853
+ }
854
+ return 0;
855
+ }
856
+ async function skillTargetDriftReasons(targets) {
857
+ const reasons = [];
858
+ for (const target of targets) {
859
+ for (const name of USER_SKILL_NAMES) {
860
+ const skillPath = join(target.skillsDir, name, "SKILL.md");
861
+ if (!(await fileExists(skillPath))) {
862
+ reasons.push(`missing ${skillPath}`);
863
+ }
864
+ }
865
+ }
866
+ return reasons;
867
+ }
868
+ async function promptResourceDriftReasons(paths) {
869
+ const reasons = [];
870
+ for (const parts of REQUIRED_PROMPT_RESOURCE_FILES) {
871
+ const path = join(paths.home, ...parts);
872
+ if (!(await fileExists(path)))
873
+ reasons.push(`missing ${path}`);
874
+ }
875
+ return reasons;
876
+ }
877
+ async function hostRelayDriftReasons(paths) {
878
+ const reasons = [];
879
+ const hostsRoot = join(paths.pythonpath, "okstra_ctl", "adapters", "hosts");
880
+ let entries;
881
+ try {
882
+ entries = await fs.readdir(hostsRoot, { withFileTypes: true });
883
+ }
884
+ catch (err) {
885
+ if (err.code === "ENOENT")
886
+ return [`missing ${hostsRoot}`];
887
+ throw err;
888
+ }
889
+ for (const entry of entries) {
890
+ if (!entry.isDirectory())
891
+ continue;
892
+ const adapterRoot = join(hostsRoot, entry.name);
893
+ const manifestPath = join(adapterRoot, "manifest.json");
894
+ if (!(await fileExists(manifestPath)))
895
+ continue;
896
+ let manifest;
897
+ try {
898
+ manifest = JSON.parse(await fs.readFile(manifestPath, "utf8"));
899
+ }
900
+ catch {
901
+ reasons.push(`invalid ${manifestPath}`);
902
+ continue;
903
+ }
904
+ const relayContract = manifest?.relayContract;
905
+ if (typeof relayContract !== "string" || !relayContract) {
906
+ reasons.push(`invalid relayContract in ${manifestPath}`);
907
+ continue;
908
+ }
909
+ const relayPath = resolveAbs(adapterRoot, relayContract);
910
+ const relayRelative = relative(adapterRoot, relayPath);
911
+ if (relayRelative.startsWith("..") || resolveAbs(adapterRoot) === relayPath) {
912
+ reasons.push(`invalid relayContract in ${manifestPath}`);
913
+ }
914
+ else if (!(await fileExists(relayPath))) {
915
+ reasons.push(`missing ${relayPath}`);
916
+ }
917
+ }
918
+ return reasons;
919
+ }
920
+ async function agentDriftReasons(paths) {
921
+ const reasons = [];
922
+ const workersDir = join(paths.agents, "workers");
923
+ const expected = await listWorkerFiles(workersDir);
924
+ if (expected.length === 0)
925
+ return reasons;
926
+ for (const name of expected) {
927
+ const target = join(CLAUDE_AGENTS_DIR, name);
928
+ try {
929
+ const lst = await fs.lstat(target);
930
+ if (lst.isSymbolicLink()) {
931
+ try {
932
+ await fs.stat(target);
933
+ }
934
+ catch {
935
+ reasons.push(`dangling symlink ${target}`);
936
+ }
937
+ }
938
+ }
939
+ catch {
940
+ reasons.push(`missing agent ${target}`);
941
+ }
942
+ }
943
+ return reasons;
944
+ }
945
+ // What the stamp did, not just where it landed: `0.165.3 -> 0.166.0` and
946
+ // `0.166.0 (unchanged)` are the two cases a bare version line cannot tell apart,
947
+ // and they mean opposite things about whether the install did anything.
948
+ export function formatVersionTransition(previous, next) {
949
+ const from = String(previous ?? "").trim();
950
+ if (!from)
951
+ return next;
952
+ return from === next ? `${next} (unchanged)` : `${from} -> ${next}`;
953
+ }
954
+ function summarise(label, result, target) {
955
+ if (result.missingSource) {
956
+ process.stdout.write(` ${label}: source directory missing — skipped\n`);
957
+ return;
958
+ }
959
+ process.stdout.write(` ${label}: copied=${result.copied} skipped=${result.skipped} -> ${target}\n`);
960
+ }
961
+ // `0.156.0` → [0, 156, 0]. Anything else → null, so a caller falls back to the
962
+ // plain inequality test rather than guessing an ordering.
963
+ function parseSemver(value) {
964
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(String(value ?? "").trim());
965
+ return match ? match.slice(1, 4).map(Number) : null;
966
+ }
967
+ export function installedStampIsNewer(stamp, packageVersion) {
968
+ const installed = parseSemver(stamp);
969
+ const running = parseSemver(packageVersion);
970
+ if (!installed || !running)
971
+ return false;
972
+ for (let i = 0; i < 3; i++) {
973
+ if (installed[i] !== running[i])
974
+ return installed[i] > running[i];
975
+ }
976
+ return false;
977
+ }
978
+ export function parseEnsureInstalledArgs(args) {
979
+ const result = {
980
+ quiet: false,
981
+ runtime: "auto",
982
+ };
983
+ for (let i = 0; i < args.length; i++) {
984
+ const arg = args[i];
985
+ if (arg === "-q" || arg === "--quiet") {
986
+ result.quiet = true;
987
+ }
988
+ else if (arg === "--runtime" || arg.startsWith("--runtime=")) {
989
+ const parsed = readRuntimeFlag(args, i);
990
+ result.runtime = validateInstallRuntime(parsed.value);
991
+ i = parsed.nextIndex;
992
+ }
993
+ else {
994
+ throw new Error(`unknown argument '${arg}'`);
995
+ }
996
+ }
997
+ return result;
998
+ }
999
+ export async function runEnsureInstalled(args) {
1000
+ if (args.includes("--help") || args.includes("-h")) {
1001
+ process.stdout.write(ENSURE_USAGE);
1002
+ return 0;
1003
+ }
1004
+ let opts;
1005
+ try {
1006
+ opts = parseEnsureInstalledArgs(args);
1007
+ }
1008
+ catch (err) {
1009
+ process.stderr.write(`error: ${err.message}\n`);
1010
+ return 2;
1011
+ }
1012
+ const paths = await resolvePaths();
1013
+ const runtimeResolution = await resolveInstallRuntime(opts.runtime, "ensure-installed");
1014
+ if (!runtimeResolution.ok && opts.runtime !== "auto") {
1015
+ process.stderr.write(`error: ${runtimeResolution.reason}\n`);
1016
+ return 2;
1017
+ }
1018
+ // A newer install than the package running this check means an upgrade
1019
+ // arrived by another route — `npx -y okstra@latest install`, or an install
1020
+ // from a checkout — while this command still resolves an older payload.
1021
+ // Reinstalling here would quietly DOWNGRADE the user's runtime, and every
1022
+ // skill preflight calls this, so the downgrade lands on the next command
1023
+ // after the upgrade. Refuse and say so; an explicit `okstra install` is
1024
+ // still honoured, because that one is the user's own decision.
1025
+ if (installedStampIsNewer(paths.version, paths.package)) {
1026
+ process.stderr.write(`okstra: installed runtime ${paths.version} is newer than this package ` +
1027
+ `(${paths.package}) — skipping auto-install so it is not downgraded. ` +
1028
+ `Run 'okstra install' explicitly to force this package's payload, or ` +
1029
+ `update the okstra package itself.\n`);
1030
+ return 0;
1031
+ }
1032
+ const reasons = [];
1033
+ if (!paths.version)
1034
+ reasons.push("no version stamp");
1035
+ else if (paths.version !== paths.package) {
1036
+ reasons.push(`version drift (stamp=${paths.version}, package=${paths.package})`);
1037
+ }
1038
+ if (!(await dirExists(paths.pythonpath)))
1039
+ reasons.push(`missing ${paths.pythonpath}`);
1040
+ if (!(await dirExists(paths.agents)))
1041
+ reasons.push(`missing agents dir ${paths.agents}`);
1042
+ for (const relative of [
1043
+ ["schemas", "final-report-v2.0.schema.json"],
1044
+ ["templates", "reports", "final-report-v2.template.md"],
1045
+ ["templates", "reports", "html", "base.template.html"],
1046
+ ]) {
1047
+ const installedPath = join(paths.home, ...relative);
1048
+ if (!(await fileExists(installedPath)))
1049
+ reasons.push(`missing ${installedPath}`);
1050
+ }
1051
+ for (const reason of await promptResourceDriftReasons(paths)) {
1052
+ reasons.push(reason);
1053
+ }
1054
+ for (const reason of await hostRelayDriftReasons(paths)) {
1055
+ reasons.push(reason);
1056
+ }
1057
+ const skillTargets = await detectSkillTargets();
1058
+ for (const reason of await skillTargetDriftReasons(skillTargets)) {
1059
+ reasons.push(reason);
1060
+ }
1061
+ if (skillTargets.some((target) => target.provider === "claude")) {
1062
+ for (const reason of await agentDriftReasons(paths)) {
1063
+ reasons.push(reason);
1064
+ }
1065
+ }
1066
+ if (reasons.length === 0) {
1067
+ if (!opts.quiet)
1068
+ process.stdout.write(`okstra runtime OK (package ${paths.package})\n`);
1069
+ return 0;
1070
+ }
1071
+ process.stderr.write(`okstra runtime needs install: ${reasons.join("; ")}\n`);
1072
+ const installArgs = ["--runtime", opts.runtime];
1073
+ if (opts.quiet)
1074
+ installArgs.push("-q");
1075
+ return await runInstall(installArgs);
1076
+ }
1077
+ //# sourceMappingURL=install.mjs.map