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