akm-cli 0.9.0-rc.1 → 0.9.0-rc.13

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 (587) hide show
  1. package/CHANGELOG.md +1190 -52
  2. package/README.md +62 -37
  3. package/SECURITY.md +46 -31
  4. package/dist/akm +162 -38
  5. package/dist/akm-migrate +44 -0
  6. package/dist/assets/backends/schtasks-template.xml +2 -1
  7. package/dist/assets/hints/cli-hints-full.md +268 -118
  8. package/dist/assets/hints/cli-hints-short.md +87 -24
  9. package/dist/assets/{profiles → improve-strategies}/catchup.json +3 -1
  10. package/dist/assets/{profiles → improve-strategies}/consolidate.json +3 -1
  11. package/dist/assets/{profiles → improve-strategies}/default.json +6 -7
  12. package/dist/assets/improve-strategies/frequent.json +15 -0
  13. package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +4 -2
  14. package/dist/assets/{profiles → improve-strategies}/memory-focus.json +4 -1
  15. package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +5 -5
  16. package/dist/assets/{profiles → improve-strategies}/quick.json +4 -2
  17. package/dist/assets/improve-strategies/reflect-distill.json +30 -0
  18. package/dist/assets/{profiles → improve-strategies}/thorough.json +1 -1
  19. package/dist/assets/prompts/consolidate-system.md +5 -5
  20. package/dist/assets/prompts/extract-session.md +2 -6
  21. package/dist/assets/prompts/memory-infer-user.md +2 -3
  22. package/dist/assets/prompts/reflect-llm-framed-contract.md +11 -0
  23. package/dist/assets/prompts/reflect-llm-schema-contract.md +3 -0
  24. package/dist/assets/prompts/reflect-output-repair.md +3 -0
  25. package/dist/assets/stash-skeleton/README.md +38 -10
  26. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +8 -0
  27. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +8 -0
  28. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +14 -1
  29. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +13 -1
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +9 -1
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +11 -0
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +9 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +9 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +8 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +100 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/domains.md +64 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/organization.md +136 -0
  38. package/dist/assets/tasks/core/extract.yml +3 -2
  39. package/dist/assets/tasks/core/improve.yml +2 -1
  40. package/dist/assets/tasks/core/index-refresh.yml +1 -0
  41. package/dist/assets/tasks/core/sync.yml +1 -0
  42. package/dist/assets/tasks/core/version-check.yml +2 -1
  43. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
  44. package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
  45. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
  46. package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
  47. package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
  48. package/dist/assets/templates/html/health.html +5 -4
  49. package/dist/assets/workflows/workflow-template.md +31 -15
  50. package/dist/cli/invocation.js +279 -0
  51. package/dist/cli/parse-args.js +5 -90
  52. package/dist/cli/retired-commands.js +78 -0
  53. package/dist/cli/shared.js +158 -48
  54. package/dist/cli-node.mjs +2 -1
  55. package/dist/cli.js +747 -293
  56. package/dist/commands/agent/agent-dispatch.js +19 -18
  57. package/dist/commands/agent/agent-support.js +0 -24
  58. package/dist/commands/agent/contribute-cli.js +43 -97
  59. package/dist/commands/completions.js +80 -23
  60. package/dist/commands/config-cli.js +44 -281
  61. package/dist/commands/env/env-binding.js +13 -9
  62. package/dist/commands/env/env-cli.js +76 -159
  63. package/dist/commands/env/env.js +12 -163
  64. package/dist/commands/env/marker-path.js +6 -0
  65. package/dist/commands/env/secret-cli.js +45 -61
  66. package/dist/commands/env/secret.js +32 -62
  67. package/dist/commands/feedback-cli.js +179 -85
  68. package/dist/commands/health/accept-rate.js +58 -0
  69. package/dist/commands/health/advisories.js +7 -8
  70. package/dist/commands/health/checks.js +279 -94
  71. package/dist/commands/health/html-report.js +197 -578
  72. package/dist/commands/health/improve-metrics.js +277 -246
  73. package/dist/commands/health/llm-usage.js +19 -19
  74. package/dist/commands/health/md-report.js +16 -7
  75. package/dist/commands/health/metrics.js +67 -32
  76. package/dist/commands/health/renderers.js +47 -0
  77. package/dist/commands/health/report-view-model.js +508 -0
  78. package/dist/commands/health/stash-exposure.js +1 -1
  79. package/dist/commands/health/surfaces.js +16 -56
  80. package/dist/commands/health/task-runs.js +3 -67
  81. package/dist/{migrate-storage-node.mjs → commands/health/types-checks.js} +1 -5
  82. package/dist/commands/health/types-improve.js +29 -0
  83. package/dist/{output/text/save.js → commands/health/types-metrics.js} +1 -2
  84. package/dist/commands/health/types-result.js +7 -0
  85. package/dist/commands/health/types-runs.js +4 -0
  86. package/dist/commands/health/types-session-log.js +4 -0
  87. package/dist/commands/health/types-windows.js +4 -0
  88. package/dist/commands/health/types.js +26 -21
  89. package/dist/commands/health/windows.js +2 -3
  90. package/dist/commands/health.js +296 -167
  91. package/dist/commands/improve/anti-collapse.js +5 -5
  92. package/dist/commands/improve/autonomy-gate.js +68 -0
  93. package/dist/commands/improve/collapse-detector.js +65 -52
  94. package/dist/commands/improve/consolidate/chunking.js +9 -7
  95. package/dist/commands/improve/consolidate/eligibility.js +1 -23
  96. package/dist/commands/improve/consolidate/merge.js +4 -0
  97. package/dist/commands/improve/consolidate.js +454 -1354
  98. package/dist/commands/improve/content-hash.js +39 -0
  99. package/dist/commands/improve/distill/content-repair.js +4 -10
  100. package/dist/commands/improve/distill/promote-memory.js +89 -64
  101. package/dist/commands/improve/distill/quality-gate.js +118 -42
  102. package/dist/commands/improve/distill-guards.js +1 -1
  103. package/dist/commands/improve/distill-promotion-policy.js +33 -888
  104. package/dist/commands/improve/distill.js +607 -363
  105. package/dist/commands/improve/eligibility.js +165 -79
  106. package/dist/commands/improve/extract-cli.js +35 -126
  107. package/dist/commands/improve/extract-prompt.js +6 -35
  108. package/dist/commands/improve/extract.js +640 -391
  109. package/dist/commands/improve/feedback-valence.js +2 -12
  110. package/dist/commands/improve/improve-cli.js +134 -135
  111. package/dist/commands/improve/improve-result-file.js +30 -50
  112. package/dist/commands/improve/improve-run-types.js +4 -0
  113. package/dist/commands/improve/improve-strategies.js +135 -0
  114. package/dist/commands/improve/improve.js +904 -701
  115. package/dist/commands/improve/locks.js +64 -111
  116. package/dist/commands/improve/loop-stages.js +1110 -923
  117. package/dist/commands/improve/memory/derived-ref.js +124 -0
  118. package/dist/commands/improve/memory/memory-belief.js +79 -7
  119. package/dist/commands/improve/memory/memory-contradiction-detect.js +49 -52
  120. package/dist/commands/improve/memory/memory-improve.js +25 -37
  121. package/dist/commands/improve/outcome-loop.js +25 -88
  122. package/dist/commands/improve/preparation.js +1034 -813
  123. package/dist/commands/improve/proactive-maintenance.js +34 -9
  124. package/dist/commands/improve/proposal-envelope.js +31 -0
  125. package/dist/commands/improve/reflect.js +983 -794
  126. package/dist/commands/improve/run-context.js +119 -0
  127. package/dist/commands/improve/salience.js +24 -127
  128. package/dist/commands/improve/session-asset.js +7 -3
  129. package/dist/commands/improve/shared.js +14 -34
  130. package/dist/commands/improve/source-identity.js +28 -0
  131. package/dist/commands/improve/triage.js +20 -17
  132. package/dist/commands/lint/base-linter.js +340 -313
  133. package/dist/commands/lint/env-key-rules.js +31 -47
  134. package/dist/commands/lint/index.js +185 -30
  135. package/dist/commands/{events.js → log.js} +28 -38
  136. package/dist/commands/migrate-cli.js +54 -0
  137. package/dist/commands/migration-tool.js +55 -0
  138. package/dist/commands/observability-cli.js +70 -208
  139. package/dist/commands/proposal/diff-format.js +50 -0
  140. package/dist/commands/proposal/drain-policies.js +0 -6
  141. package/dist/commands/proposal/drain.js +91 -40
  142. package/dist/commands/proposal/proposal-cli.js +134 -132
  143. package/dist/commands/proposal/proposal-types.js +56 -0
  144. package/dist/commands/proposal/proposal.js +83 -65
  145. package/dist/commands/proposal/propose-cli.js +88 -0
  146. package/dist/commands/proposal/propose.js +105 -88
  147. package/dist/commands/proposal/repository.js +1303 -278
  148. package/dist/commands/proposal/validators/proposal-quality-validators.js +16 -6
  149. package/dist/commands/proposal/validators/proposal-validators.js +61 -12
  150. package/dist/commands/proposal/validators/proposals.js +6 -8
  151. package/dist/commands/read/curate.js +78 -73
  152. package/dist/commands/read/knowledge.js +510 -13
  153. package/dist/commands/read/registry-search.js +2 -2
  154. package/dist/commands/read/remember-cli.js +84 -15
  155. package/dist/commands/read/search-cli.js +203 -96
  156. package/dist/commands/read/search.js +126 -94
  157. package/dist/commands/read/show.js +226 -250
  158. package/dist/commands/registry-cli.js +34 -60
  159. package/dist/commands/remember.js +18 -57
  160. package/dist/commands/sources/add-cli.js +104 -49
  161. package/dist/commands/sources/bundle-cli.js +166 -0
  162. package/dist/commands/sources/bundle-config-ops.js +63 -0
  163. package/dist/commands/sources/info.js +27 -15
  164. package/dist/commands/sources/init.js +30 -40
  165. package/dist/commands/sources/installed-stashes.js +469 -172
  166. package/dist/commands/sources/schema-repair.js +10 -9
  167. package/dist/commands/sources/self-update.js +182 -121
  168. package/dist/commands/sources/source-add.js +169 -178
  169. package/dist/commands/sources/source-clone.js +144 -41
  170. package/dist/commands/sources/source-manage.js +94 -59
  171. package/dist/commands/sources/sources-cli.js +64 -205
  172. package/dist/commands/sources/stash-cli.js +91 -54
  173. package/dist/commands/sources/stash-skeleton.js +1 -1
  174. package/dist/commands/tasks/tasks-cli.js +106 -104
  175. package/dist/commands/tasks/tasks.js +445 -262
  176. package/dist/commands/workflow-cli.js +75 -228
  177. package/dist/core/action-contributors.js +1 -1
  178. package/dist/core/activation-policy.js +49 -0
  179. package/dist/core/adapter/adapters/agent-skills-adapter.js +181 -0
  180. package/dist/core/adapter/adapters/akm-adapter.js +528 -0
  181. package/dist/core/adapter/adapters/akm-lint.js +392 -0
  182. package/dist/core/adapter/adapters/akm-metadata.js +387 -0
  183. package/dist/core/adapter/adapters/akm-task-adapter.js +149 -0
  184. package/dist/core/adapter/adapters/akm-workflow-adapter.js +180 -0
  185. package/dist/core/adapter/adapters/claude-adapter.js +61 -0
  186. package/dist/core/adapter/adapters/dotenv-adapter.js +187 -0
  187. package/dist/core/adapter/adapters/generic-files-adapter.js +119 -0
  188. package/dist/core/adapter/adapters/index.js +80 -0
  189. package/dist/core/adapter/adapters/llm-wiki-adapter.js +419 -0
  190. package/dist/core/adapter/adapters/okf-adapter.js +391 -0
  191. package/dist/core/adapter/adapters/opencode-adapter.js +68 -0
  192. package/dist/core/adapter/adapters/shared.js +286 -0
  193. package/dist/core/adapter/adapters/tool-dir-shared.js +217 -0
  194. package/dist/core/adapter/adapters/website-snapshot-adapter.js +155 -0
  195. package/dist/core/adapter/bundle-adapter.js +4 -0
  196. package/dist/core/adapter/detect-adapter.js +17 -0
  197. package/dist/core/adapter/recognize-match.js +44 -0
  198. package/dist/core/adapter/registry.js +56 -0
  199. package/dist/core/adapter/types.js +4 -0
  200. package/dist/core/asset/akm-markdown.js +30 -0
  201. package/dist/core/asset/asset-placement.js +243 -0
  202. package/dist/core/asset/asset-ref.js +110 -79
  203. package/dist/core/asset/asset-serialize.js +20 -0
  204. package/dist/core/asset/frontmatter.js +28 -12
  205. package/dist/core/asset/markdown.js +40 -51
  206. package/dist/core/asset/resolve-ref.js +274 -0
  207. package/dist/core/asset/stash-meta.js +2 -2
  208. package/dist/core/bundle-id.js +51 -0
  209. package/dist/core/common.js +281 -86
  210. package/dist/core/config/config-io.js +42 -128
  211. package/dist/core/config/config-schema.js +233 -855
  212. package/dist/core/config/config-sources.js +162 -39
  213. package/dist/core/config/config-types.js +16 -11
  214. package/dist/core/config/config-version.js +29 -0
  215. package/dist/core/config/config-walker.js +126 -37
  216. package/dist/core/config/config.js +154 -331
  217. package/dist/core/config/deep-merge.js +41 -0
  218. package/dist/core/config/engine-semantics.js +28 -0
  219. package/dist/core/config/experimental.js +21 -0
  220. package/dist/core/config/schema/embedding.js +38 -0
  221. package/dist/core/config/schema/engines.js +116 -0
  222. package/dist/core/config/schema/experimental.js +47 -0
  223. package/dist/core/config/schema/feedback.js +31 -0
  224. package/dist/core/config/schema/improve-processes.js +389 -0
  225. package/dist/core/config/schema/improve.js +94 -0
  226. package/dist/core/config/schema/index-config.js +176 -0
  227. package/dist/core/config/schema/output.js +18 -0
  228. package/dist/core/config/schema/primitives.js +94 -0
  229. package/dist/core/config/schema/search.js +30 -0
  230. package/dist/core/config/schema/setup.js +18 -0
  231. package/dist/core/config/schema/sources-bundles.js +169 -0
  232. package/dist/core/config/schema/workflow.js +29 -0
  233. package/dist/core/env-secret-ref.js +155 -20
  234. package/dist/core/errors.js +17 -15
  235. package/dist/core/events-types.js +4 -0
  236. package/dist/core/events.js +46 -128
  237. package/dist/core/extra-params.js +62 -0
  238. package/dist/core/file-change.js +17 -0
  239. package/dist/core/file-lock.js +202 -57
  240. package/dist/core/fs-txn.js +392 -0
  241. package/dist/core/git-message.js +59 -0
  242. package/dist/core/improve-result.js +167 -0
  243. package/dist/core/lesson-lint.js +1 -17
  244. package/dist/core/logs-db.js +1 -1
  245. package/dist/core/maintenance-barrier.js +135 -0
  246. package/dist/core/migration-operation.js +44 -0
  247. package/dist/core/mutation-target.js +78 -0
  248. package/dist/core/paths.js +22 -25
  249. package/dist/core/platform.js +10 -0
  250. package/dist/core/recognition-util.js +128 -0
  251. package/dist/core/redaction.js +392 -0
  252. package/dist/core/standards/resolve-standards-context.js +36 -65
  253. package/dist/core/standards/resolve-stash-standards.js +2 -2
  254. package/dist/core/standards/resolve-type-conventions.js +5 -5
  255. package/dist/core/state/migrations.js +242 -11
  256. package/dist/core/state-db.js +98 -10
  257. package/dist/core/structured.js +1 -1
  258. package/dist/core/subprocess.js +303 -0
  259. package/dist/core/text-truncation.js +9 -5
  260. package/dist/core/time.js +20 -0
  261. package/dist/core/type-presentation.js +130 -0
  262. package/dist/core/warn.js +0 -3
  263. package/dist/core/write-source.js +834 -118
  264. package/dist/indexer/bundle-identity-guard.js +92 -0
  265. package/dist/indexer/db/graph-db.js +1 -25
  266. package/dist/indexer/db/llm-cache.js +1 -1
  267. package/dist/indexer/ensure-index.js +30 -9
  268. package/dist/indexer/graph/graph-boost.js +9 -30
  269. package/dist/indexer/graph/graph-extraction.js +41 -27
  270. package/dist/indexer/graph/graph-types.js +4 -0
  271. package/dist/indexer/index-writer-lock.js +93 -49
  272. package/dist/indexer/index-written-assets.js +100 -53
  273. package/dist/indexer/indexer.js +746 -329
  274. package/dist/indexer/init.js +18 -25
  275. package/dist/indexer/installations.js +142 -0
  276. package/dist/indexer/passes/dir-staleness.js +18 -10
  277. package/dist/indexer/passes/memory-inference.js +25 -15
  278. package/dist/indexer/passes/metadata.js +412 -243
  279. package/dist/indexer/scan/doc-to-entry.js +160 -0
  280. package/dist/indexer/scan/drain-dir.js +134 -0
  281. package/dist/indexer/search/db-search.js +292 -108
  282. package/dist/indexer/search/fts-query.js +64 -0
  283. package/dist/indexer/search/ranking-contributors.js +145 -25
  284. package/dist/indexer/search/ranking-types.js +4 -0
  285. package/dist/indexer/search/ranking.js +28 -71
  286. package/dist/indexer/search/search-attribution.js +67 -0
  287. package/dist/indexer/search/search-fields.js +18 -3
  288. package/dist/indexer/search/search-hit-enrichers.js +30 -40
  289. package/dist/indexer/search/search-source.js +157 -111
  290. package/dist/indexer/search/semantic-status.js +4 -1
  291. package/dist/indexer/usage/usage-events.js +10 -30
  292. package/dist/indexer/walk/file-context.js +3 -45
  293. package/dist/indexer/walk/matchers.js +42 -73
  294. package/dist/indexer/walk/path-resolver.js +11 -5
  295. package/dist/indexer/walk/walker.js +42 -14
  296. package/dist/integrations/agent/builder-shared.js +7 -0
  297. package/dist/integrations/agent/builders.js +5 -58
  298. package/dist/integrations/agent/config.js +3 -143
  299. package/dist/integrations/agent/detect.js +17 -2
  300. package/dist/integrations/agent/engine-resolution.js +231 -0
  301. package/dist/integrations/agent/index.js +1 -2
  302. package/dist/integrations/agent/model-aliases.js +8 -3
  303. package/dist/integrations/agent/profiles.js +6 -99
  304. package/dist/integrations/agent/prompts.js +46 -18
  305. package/dist/integrations/agent/runner-dispatch.js +78 -13
  306. package/dist/integrations/agent/runner.js +76 -208
  307. package/dist/integrations/agent/spawn.js +48 -279
  308. package/dist/integrations/harnesses/aider/agent-builder.js +9 -8
  309. package/dist/integrations/harnesses/aider/index.js +2 -12
  310. package/dist/integrations/harnesses/amazonq/agent-builder.js +10 -16
  311. package/dist/integrations/harnesses/amazonq/index.js +3 -17
  312. package/dist/integrations/harnesses/claude/agent-builder.js +2 -3
  313. package/dist/integrations/harnesses/claude/config-import.js +1 -3
  314. package/dist/integrations/harnesses/claude/index.js +1 -14
  315. package/dist/integrations/harnesses/claude/session-log.js +27 -75
  316. package/dist/integrations/harnesses/codex/agent-builder.js +8 -7
  317. package/dist/integrations/harnesses/codex/index.js +2 -13
  318. package/dist/integrations/harnesses/copilot/agent-builder.js +9 -9
  319. package/dist/integrations/harnesses/copilot/index.js +1 -13
  320. package/dist/integrations/harnesses/gemini/agent-builder.js +8 -9
  321. package/dist/integrations/harnesses/gemini/index.js +1 -13
  322. package/dist/integrations/harnesses/ids.js +24 -0
  323. package/dist/integrations/harnesses/index.js +31 -33
  324. package/dist/integrations/harnesses/opencode/agent-builder.js +23 -5
  325. package/dist/integrations/harnesses/opencode/config-import.js +1 -3
  326. package/dist/integrations/harnesses/opencode/index.js +1 -18
  327. package/dist/integrations/harnesses/opencode/session-log.js +67 -125
  328. package/dist/integrations/harnesses/opencode-sdk/harness.js +3 -17
  329. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +274 -191
  330. package/dist/integrations/harnesses/openhands/agent-builder.js +12 -10
  331. package/dist/integrations/harnesses/openhands/index.js +2 -12
  332. package/dist/integrations/harnesses/pi/agent-builder.js +11 -18
  333. package/dist/integrations/harnesses/pi/index.js +3 -16
  334. package/dist/integrations/harnesses/shared.js +17 -0
  335. package/dist/integrations/harnesses/types.js +38 -34
  336. package/dist/integrations/lockfile.js +211 -24
  337. package/dist/integrations/session-logs/index.js +24 -40
  338. package/dist/integrations/session-logs/provider-base.js +113 -0
  339. package/dist/llm/client.js +182 -110
  340. package/dist/llm/embedders/deterministic.js +2 -2
  341. package/dist/llm/embedders/remote.js +21 -9
  342. package/dist/llm/feature-gate.js +17 -57
  343. package/dist/llm/graph-extract.js +12 -13
  344. package/dist/llm/index-passes.js +8 -42
  345. package/dist/llm/memory-infer.js +144 -1
  346. package/dist/llm/metadata-enhance.js +45 -30
  347. package/dist/llm/structured-call.js +16 -8
  348. package/dist/llm/usage-persist.js +30 -5
  349. package/dist/llm/usage-telemetry.js +59 -6
  350. package/dist/output/cli-hints.js +1 -2
  351. package/dist/output/command-registry.js +27 -0
  352. package/dist/output/context.js +22 -7
  353. package/dist/output/format-exempt.js +80 -0
  354. package/dist/output/generic-render.js +251 -0
  355. package/dist/output/html-render.js +11 -16
  356. package/dist/output/render-registry.js +57 -0
  357. package/dist/output/renderers.js +15 -281
  358. package/dist/output/shapes/curate.js +10 -1
  359. package/dist/output/shapes/events.js +12 -7
  360. package/dist/output/shapes/helpers.js +58 -84
  361. package/dist/output/shapes/passthrough.js +8 -40
  362. package/dist/output/shapes/proposal/producer.js +15 -7
  363. package/dist/output/shapes/registry.js +12 -6
  364. package/dist/output/shapes.js +0 -9
  365. package/dist/output/text/{init.js → bundle-create.js} +3 -1
  366. package/dist/output/text/bundle-show.js +7 -0
  367. package/dist/output/text/command-format.js +562 -0
  368. package/dist/output/text/env.js +1 -3
  369. package/dist/output/text/events.js +8 -7
  370. package/dist/output/text/helpers.js +15 -1375
  371. package/dist/output/text/proposal/producer.js +4 -2
  372. package/dist/output/text/proposal-format.js +202 -0
  373. package/dist/output/text/registry-commands.js +1 -2
  374. package/dist/output/text/registry.js +12 -6
  375. package/dist/output/text/show-directives.js +117 -0
  376. package/dist/output/text/show-format.js +103 -0
  377. package/dist/output/text/sync.js +5 -0
  378. package/dist/output/text/workflow-format.js +332 -0
  379. package/dist/output/text/workflow.js +1 -2
  380. package/dist/output/text.js +10 -19
  381. package/dist/registry/factory.js +4 -6
  382. package/dist/registry/origin-resolve.js +16 -27
  383. package/dist/registry/providers/skills-sh.js +3 -3
  384. package/dist/registry/providers/static-index.js +15 -25
  385. package/dist/registry/resolve.js +43 -94
  386. package/dist/registry/semver.js +43 -0
  387. package/dist/runtime.js +81 -12
  388. package/dist/scripts/akm-migrate.js +35529 -0
  389. package/dist/setup/detect.js +5 -7
  390. package/dist/setup/detected-engines.js +136 -0
  391. package/dist/setup/engine-config.js +100 -0
  392. package/dist/setup/registry-stash-loader.js +3 -3
  393. package/dist/setup/semantic-assets.js +12 -9
  394. package/dist/setup/setup.js +444 -208
  395. package/dist/setup/steps/connection-shared.js +120 -0
  396. package/dist/setup/steps/connection.js +108 -305
  397. package/dist/setup/steps/platforms.js +13 -12
  398. package/dist/setup/steps/semantic.js +15 -3
  399. package/dist/setup/steps/sources.js +21 -15
  400. package/dist/setup/steps/stashdir.js +6 -4
  401. package/dist/setup/steps/tasks.js +236 -119
  402. package/dist/setup/steps.js +3 -2
  403. package/dist/sources/freshness.js +39 -0
  404. package/dist/sources/provider-factory.js +11 -17
  405. package/dist/sources/providers/filesystem.js +2 -3
  406. package/dist/sources/providers/git-install.js +278 -34
  407. package/dist/sources/providers/git-provider.js +54 -56
  408. package/dist/sources/providers/git-stash.js +420 -91
  409. package/dist/sources/providers/git.js +2 -2
  410. package/dist/sources/providers/npm.js +16 -19
  411. package/dist/sources/providers/provider-utils.js +47 -22
  412. package/dist/sources/providers/sync-from-ref.js +3 -9
  413. package/dist/sources/providers/website.js +2 -2
  414. package/dist/sources/resolve.js +11 -10
  415. package/dist/sources/snapshot-fetchers/types.js +4 -0
  416. package/dist/sources/{website-ingest.js → snapshot-fetchers/website-ingest.js} +110 -41
  417. package/dist/storage/database.js +60 -4
  418. package/dist/storage/engines/sqlite-migrations.js +156 -5
  419. package/dist/storage/locations.js +1 -2
  420. package/dist/storage/repositories/canaries-repository.js +1 -1
  421. package/dist/storage/repositories/events-repository.js +51 -11
  422. package/dist/storage/repositories/improve-runs-repository.js +6 -32
  423. package/dist/storage/repositories/index-connection.js +79 -0
  424. package/dist/storage/repositories/index-db.js +4 -3
  425. package/dist/storage/repositories/index-entries-repository.js +863 -0
  426. package/dist/{indexer/db/entry-mapper.js → storage/repositories/index-entry-mapper.js} +19 -2
  427. package/dist/storage/repositories/index-entry-types.js +4 -0
  428. package/dist/storage/repositories/index-fts-repository.js +167 -0
  429. package/dist/storage/repositories/index-llm-cache-repository.js +108 -0
  430. package/dist/storage/repositories/index-meta-repository.js +49 -0
  431. package/dist/{indexer/db/schema.js → storage/repositories/index-schema.js} +226 -100
  432. package/dist/storage/repositories/index-sql.js +12 -0
  433. package/dist/storage/repositories/index-utility-repository.js +356 -0
  434. package/dist/storage/repositories/index-vec-repository.js +250 -0
  435. package/dist/storage/repositories/outcome-repository.js +119 -0
  436. package/dist/storage/repositories/proposals-repository.js +317 -75
  437. package/dist/storage/repositories/registry-cache.js +1 -1
  438. package/dist/storage/repositories/salience-repository.js +172 -0
  439. package/dist/storage/repositories/task-history-repository.js +110 -3
  440. package/dist/storage/repositories/workflow-runs-repository.js +68 -35
  441. package/dist/tasks/backends/cron.js +169 -46
  442. package/dist/tasks/backends/exec-utils.js +76 -3
  443. package/dist/tasks/backends/index.js +6 -9
  444. package/dist/tasks/backends/launchd.js +292 -55
  445. package/dist/tasks/backends/schtasks.js +557 -70
  446. package/dist/tasks/backends/types.js +4 -0
  447. package/dist/tasks/command-executable.js +93 -0
  448. package/dist/tasks/embedded.js +56 -38
  449. package/dist/tasks/parser.js +156 -64
  450. package/dist/tasks/resolve-akm-bin.js +144 -51
  451. package/dist/tasks/runner.js +377 -209
  452. package/dist/tasks/schedule.js +108 -19
  453. package/dist/tasks/scheduler-invocation.js +296 -0
  454. package/dist/tasks/schema.js +1 -1
  455. package/dist/tasks/task-id.js +35 -0
  456. package/dist/tasks/validator.js +30 -16
  457. package/dist/workflows/authoring/authoring.js +96 -148
  458. package/dist/workflows/authoring/scope-key.js +1 -1
  459. package/dist/workflows/cli.js +0 -20
  460. package/dist/workflows/concurrency-policy.js +15 -0
  461. package/dist/workflows/exec/brief.js +25 -59
  462. package/dist/workflows/exec/frozen-judge.js +47 -0
  463. package/dist/workflows/exec/native-executor.js +157 -94
  464. package/dist/workflows/exec/report.js +365 -200
  465. package/dist/workflows/exec/run-workflow.js +46 -40
  466. package/dist/workflows/exec/scheduler.js +12 -41
  467. package/dist/workflows/exec/step-work.js +239 -205
  468. package/dist/workflows/exec/workflow-engine-gate.js +67 -0
  469. package/dist/workflows/ir/compile.js +141 -283
  470. package/dist/workflows/ir/freeze.js +233 -0
  471. package/dist/workflows/ir/plan-hash.js +40 -5
  472. package/dist/workflows/ir/schema.js +537 -1
  473. package/dist/workflows/parser.js +878 -306
  474. package/dist/workflows/program/expressions.js +20 -208
  475. package/dist/workflows/program/schema.js +7 -10
  476. package/dist/workflows/renderer.js +99 -121
  477. package/dist/workflows/resource-limits.js +22 -0
  478. package/dist/workflows/runtime/checkin.js +1 -1
  479. package/dist/workflows/runtime/plan-classifier.js +131 -0
  480. package/dist/workflows/runtime/runs.js +200 -113
  481. package/dist/workflows/runtime/unit-checkin.js +1 -1
  482. package/dist/workflows/runtime/unit-phases.js +20 -0
  483. package/dist/workflows/runtime/workflow-asset-loader.js +235 -97
  484. package/dist/workflows/schema.js +1 -11
  485. package/dist/workflows/validate-summary.js +4 -26
  486. package/dist/workflows/validator.js +52 -30
  487. package/docs/README.md +42 -78
  488. package/docs/migration/README.md +8 -0
  489. package/docs/migration/release-notes/0.6.0.md +1 -1
  490. package/docs/migration/release-notes/0.7.0.md +9 -8
  491. package/docs/migration/release-notes/0.9.0.md +158 -14
  492. package/docs/migration/v0.7-to-v0.8.md +46 -47
  493. package/docs/migration/v0.8-to-v0.9.md +844 -0
  494. package/docs/reference/README.md +12 -0
  495. package/docs/reference/data-and-telemetry.md +333 -0
  496. package/package.json +21 -17
  497. package/schemas/akm-asset-envelope.json +93 -0
  498. package/schemas/akm-config.json +4636 -0
  499. package/schemas/akm-task.json +87 -0
  500. package/{dist/schemas → schemas}/akm-workflow.json +127 -82
  501. package/dist/akm-migrate-storage +0 -38
  502. package/dist/assets/help/help-accept.md +0 -12
  503. package/dist/assets/help/help-improve.md +0 -84
  504. package/dist/assets/help/help-proposals.md +0 -17
  505. package/dist/assets/help/help-propose.md +0 -17
  506. package/dist/assets/help/help-reject.md +0 -11
  507. package/dist/assets/profiles/frequent.json +0 -13
  508. package/dist/assets/profiles/recombine-only.json +0 -21
  509. package/dist/assets/profiles/reflect-distill.json +0 -30
  510. package/dist/assets/profiles/synthesize.json +0 -15
  511. package/dist/assets/prompts/procedural-system.md +0 -44
  512. package/dist/assets/prompts/recombine-system.md +0 -40
  513. package/dist/assets/prompts/staleness-detect-system.md +0 -6
  514. package/dist/assets/tasks/core/backup.yml +0 -4
  515. package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
  516. package/dist/assets/templates/html/default.html +0 -78
  517. package/dist/assets/templates/html/vendor/echarts.min.js +0 -45
  518. package/dist/assets/wiki/index-template.md +0 -12
  519. package/dist/assets/wiki/ingest-workflow-template.md +0 -83
  520. package/dist/assets/wiki/log-template.md +0 -8
  521. package/dist/assets/wiki/schema-template.md +0 -61
  522. package/dist/cli/config-migrate.js +0 -150
  523. package/dist/cli/config-validate.js +0 -39
  524. package/dist/commands/graph/graph-cli.js +0 -124
  525. package/dist/commands/graph/graph.js +0 -487
  526. package/dist/commands/improve/calibration.js +0 -161
  527. package/dist/commands/improve/dedup.js +0 -482
  528. package/dist/commands/improve/extract-watch.js +0 -140
  529. package/dist/commands/improve/hot-probation.js +0 -45
  530. package/dist/commands/improve/improve-auto-accept.js +0 -276
  531. package/dist/commands/improve/improve-profiles.js +0 -168
  532. package/dist/commands/improve/procedural.js +0 -398
  533. package/dist/commands/improve/recombine.js +0 -818
  534. package/dist/commands/improve/schema-similarity-gate.js +0 -168
  535. package/dist/commands/lint/agent-linter.js +0 -44
  536. package/dist/commands/lint/command-linter.js +0 -44
  537. package/dist/commands/lint/default-linter.js +0 -16
  538. package/dist/commands/lint/fact-linter.js +0 -39
  539. package/dist/commands/lint/knowledge-linter.js +0 -16
  540. package/dist/commands/lint/memory-linter.js +0 -61
  541. package/dist/commands/lint/registry.js +0 -41
  542. package/dist/commands/lint/skill-linter.js +0 -45
  543. package/dist/commands/lint/task-linter.js +0 -50
  544. package/dist/commands/lint/workflow-linter.js +0 -81
  545. package/dist/commands/proposal/legacy-import.js +0 -115
  546. package/dist/commands/sources/history.js +0 -196
  547. package/dist/commands/tasks/default-tasks.js +0 -186
  548. package/dist/commands/wiki-cli.js +0 -292
  549. package/dist/core/asset/asset-registry.js +0 -76
  550. package/dist/core/asset/asset-spec.js +0 -316
  551. package/dist/core/config/config-migration.js +0 -602
  552. package/dist/core/deep-merge.js +0 -38
  553. package/dist/core/eval/rank-metrics.js +0 -113
  554. package/dist/core/ripgrep/install.js +0 -163
  555. package/dist/core/ripgrep/resolve.js +0 -81
  556. package/dist/indexer/db/db.js +0 -1414
  557. package/dist/indexer/manifest.js +0 -170
  558. package/dist/indexer/passes/metadata-contributors.js +0 -31
  559. package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -94
  560. package/dist/integrations/harnesses/opencode-sdk/index.js +0 -25
  561. package/dist/llm/call-ai.js +0 -62
  562. package/dist/llm/memory-infer-impl.js +0 -138
  563. package/dist/output/shapes/distill.js +0 -14
  564. package/dist/output/shapes/history.js +0 -11
  565. package/dist/output/text/distill.js +0 -6
  566. package/dist/output/text/enable-disable.js +0 -8
  567. package/dist/output/text/history.js +0 -6
  568. package/dist/output/text/wiki.js +0 -16
  569. package/dist/registry/build-index.js +0 -386
  570. package/dist/schemas/akm-config.json +0 -14225
  571. package/dist/scripts/migrate-storage.js +0 -13169
  572. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +0 -10169
  573. package/dist/scripts/migrations/v16-to-v17.js +0 -141
  574. package/dist/setup/legacy-config.js +0 -106
  575. package/dist/storage/repositories/consolidation-repository.js +0 -38
  576. package/dist/storage/repositories/recombine-repository.js +0 -213
  577. package/dist/wiki/wiki-templates.js +0 -15
  578. package/dist/wiki/wiki.js +0 -1012
  579. package/dist/workflows/authoring/workflow-program-template.yaml +0 -31
  580. package/dist/workflows/db.js +0 -350
  581. package/dist/workflows/exec/watch.js +0 -116
  582. package/dist/workflows/program/parser.js +0 -760
  583. package/dist/workflows/program/project.js +0 -105
  584. package/docs/data-and-telemetry.md +0 -227
  585. package/docs/migration/release-notes/0.9.0-beta.60.md +0 -19
  586. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/registry.js +0 -0
  587. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/youtube.js +0 -0
@@ -9,19 +9,22 @@
9
9
  * Collects all choices and writes config once at the end.
10
10
  *
11
11
  * This module holds the wizard orchestration; the individual wizard steps,
12
- * config-shape adapters, prompt shims, provider table, and semantic-asset
13
- * preparation live in sibling modules (`steps/*`, `legacy-config`, `prompt`,
12
+ * engine writers, prompt shims, provider table, and semantic-asset preparation
13
+ * live in sibling modules (`steps/*`, `engine-config`, `prompt`,
14
14
  * `providers`, `semantic-assets`).
15
15
  */
16
16
  import { promises as dnsPromises } from "node:dns";
17
17
  import fs from "node:fs";
18
18
  import os from "node:os";
19
19
  import path from "node:path";
20
+ import { isDeepStrictEqual } from "node:util";
20
21
  import * as p from "../cli/clack.js";
21
22
  import { akmInit } from "../commands/sources/init.js";
22
- import { DEFAULT_CONFIG, getDefaultLlmConfig, loadUserConfig, saveConfig } from "../core/config/config.js";
23
- import { backupExistingConfig } from "../core/config/config-io.js";
24
- import { deepMergeConfig } from "../core/deep-merge.js";
23
+ import { deriveBundleIds } from "../core/bundle-id.js";
24
+ import { bundleEntryToSourceEntry, DEFAULT_CONFIG, loadUserConfig, mutateConfigWithPrecommit, parseAndValidateConfigText, primaryBundlePath, validateCompleteConfig, } from "../core/config/config.js";
25
+ import { readConfigText } from "../core/config/config-io.js";
26
+ import { listTopLevelConfigKeys } from "../core/config/config-schema.js";
27
+ import { deepMergeConfig } from "../core/config/deep-merge.js";
25
28
  import { ConfigError, UsageError } from "../core/errors.js";
26
29
  import { getConfigPath, getDefaultStashDir, isTransientStashPath } from "../core/paths.js";
27
30
  import { warn } from "../core/warn.js";
@@ -29,11 +32,12 @@ import { akmIndex } from "../indexer/indexer.js";
29
32
  import { clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus, } from "../indexer/search/semantic-status.js";
30
33
  import { detectAgentCliProfiles, pickDefaultAgentProfile } from "../integrations/agent/index.js";
31
34
  import { defaultProfileName } from "../integrations/harnesses/index.js";
35
+ import { readLockfile } from "../integrations/lockfile.js";
32
36
  import { probeLlmCapabilities } from "../llm/client.js";
33
- import { getOutputMode } from "../output/context.js";
34
37
  import { detectEnvironment, detectLMStudio, renderDetectionSummary, } from "./detect.js";
38
+ import { upsertDetectedAgentEngine, upsertDetectedLlmEngine, verifyOpenAiCompatibleEndpoint } from "./detected-engines.js";
39
+ import { readCurrentLlmEngine, writeAgentEngines, writeLlmEngine } from "./engine-config.js";
35
40
  import { detectHarnessConfigs } from "./harness-config-import.js";
36
- import { applyLegacyAgent, applyLegacyLlm } from "./legacy-config.js";
37
41
  import { bail, prompt } from "./prompt.js";
38
42
  import { PROVIDER_DEFAULTS } from "./providers.js";
39
43
  import { prepareSemanticSearchAssets } from "./semantic-assets.js";
@@ -44,21 +48,21 @@ import { printCapabilitySummary, stepAgentCliDetection, stepAgentPlatforms, step
44
48
  import { stepSemanticSearch } from "./steps/semantic.js";
45
49
  import { stepAdditionalSources, stepAddSources, stepRegistries } from "./steps/sources.js";
46
50
  import { stepStashDir } from "./steps/stashdir.js";
47
- import { stepDefaultImproveTasks, stepScheduledTasks } from "./steps/tasks.js";
51
+ import { stepScheduledTasks } from "./steps/tasks.js";
48
52
  // ── Setup sandbox guard ─────────────────────────────────────────────────────
49
53
  /**
50
- * Refuse to persist an explicit `--dir /tmp/...` stashDir to the user's
51
- * config. The OS may reap the directory at any time, and the next run will
52
- * see a `stashDir` that points at a deleted path (falling back to ~/akm
53
- * silently). Mirrors the `assertInitSandbox` check in commands/init.ts, but
54
+ * Refuse to persist an explicit `--dir /tmp/...` as the default bundle path.
55
+ * The OS may reap the directory at any time, leaving the next run with a
56
+ * default bundle that points at a deleted path. Mirrors the
57
+ * `assertInitSandbox` check in commands/init.ts, but
54
58
  * fires under all runtimes (not just `bun test`) because `akm setup --dir
55
59
  * /tmp/X` is a documented isolation pattern that has been observed to
56
- * silently clobber the host config — see
57
- * `docs/technical/incidents/2026-05-23-setup-clobbers-user-config.md`.
60
+ * silently clobber the host config (the 2026-05-23 setup-clobbers-user-config
61
+ * incident).
58
62
  *
59
63
  * Escape hatch: set `AKM_FORCE_SETUP_TMP_STASH=1` to override. When the
60
64
  * escape hatch is on, `applyStashIsolationToEnv` below also pre-sets
61
- * `AKM_STASH_DIR` so that the `getConfigDir` / `getCacheDir` isolation
65
+ * `AKM_BUNDLE_DIR` so that the `getConfigDir` / `getCacheDir` isolation
62
66
  * rules fire and config + cache writes route into `$stashDir/.akm/`
63
67
  * instead of the user's host `~/.config/akm`.
64
68
  */
@@ -70,7 +74,7 @@ function assertSetupSandbox(stashDir, dirExplicitlyProvided) {
70
74
  if (!isTransientStashPath(stashDir))
71
75
  return;
72
76
  throw new ConfigError(`refusing to run \`akm setup --dir ${stashDir}\`: the path is in a transient/sandbox directory family the OS may reap. ` +
73
- "Persisting it as the user's stashDir would leave the next run pointing at a deleted path (silently falling back to ~/akm). " +
77
+ "Persisting it as the default bundle would leave the next run pointing at a deleted path. " +
74
78
  "Use a persistent directory, OR set AKM_FORCE_SETUP_TMP_STASH=1 if you intentionally want a sandbox setup " +
75
79
  "(setup will also auto-isolate config + cache writes into $stashDir/.akm/ so the host config is preserved).", "SETUP_TMP_STASH_REFUSED");
76
80
  }
@@ -78,11 +82,11 @@ function assertSetupSandbox(stashDir, dirExplicitlyProvided) {
78
82
  * Propagate the explicit `--dir <stashDir>` choice to the env so that the
79
83
  * `getConfigDir` / `getCacheDir` isolation rules in `src/core/paths.ts`
80
84
  * actually fire for the duration of this setup run. Without this, a CLI
81
- * caller who passes `--dir /tmp/X` but doesn't pre-export `AKM_STASH_DIR`
85
+ * caller who passes `--dir /tmp/X` but doesn't pre-export `AKM_BUNDLE_DIR`
82
86
  * would still write config to the host `~/.config/akm/config.json`. We
83
87
  * only set the env var when:
84
88
  * - `--dir` was explicitly provided (we have an operator-stated stash), AND
85
- * - `AKM_STASH_DIR` is not already set (caller's explicit env wins).
89
+ * - `AKM_BUNDLE_DIR` is not already set (caller's explicit env wins).
86
90
  * The set is process-wide; for the CLI that's the right scope (the process
87
91
  * is about to do all its work against this stash). For tests, each test
88
92
  * already isolates env via beforeEach/afterEach so there is no leak.
@@ -90,11 +94,215 @@ function assertSetupSandbox(stashDir, dirExplicitlyProvided) {
90
94
  function applyStashIsolationToEnv(stashDir, dirExplicitlyProvided) {
91
95
  if (!dirExplicitlyProvided)
92
96
  return;
93
- if (process.env.AKM_STASH_DIR?.trim())
97
+ if (process.env.AKM_BUNDLE_DIR?.trim())
94
98
  return;
95
- process.env.AKM_STASH_DIR = stashDir;
99
+ process.env.AKM_BUNDLE_DIR = stashDir;
96
100
  }
97
101
  // ── Helpers ─────────────────────────────────────────────────────────────────
102
+ /** Raw preflight used before setup performs prompts, initialization, or writes. */
103
+ export function assertSetupConfigPreflight() {
104
+ const configPath = getConfigPath();
105
+ let text;
106
+ try {
107
+ text = readConfigText(configPath);
108
+ }
109
+ catch (error) {
110
+ throw new ConfigError(`Could not read config at ${configPath}: ${error instanceof Error ? error.message : String(error)}`, "INVALID_CONFIG_FILE");
111
+ }
112
+ if (text !== undefined)
113
+ parseAndValidateConfigText(text, configPath);
114
+ }
115
+ function isPlainRecord(value) {
116
+ if (!value || typeof value !== "object" || Array.isArray(value))
117
+ return false;
118
+ const prototype = Object.getPrototypeOf(value);
119
+ return prototype === Object.prototype || prototype === null;
120
+ }
121
+ function sameConfigValue(left, right) {
122
+ return isDeepStrictEqual(left, right);
123
+ }
124
+ function configArrayItemKey(value) {
125
+ if (!isPlainRecord(value))
126
+ return `value:${JSON.stringify(value)}`;
127
+ if (typeof value.id === "string")
128
+ return `id:${value.id}`;
129
+ if (typeof value.type === "string" && typeof value.url === "string")
130
+ return `source:${value.type}:url:${value.url}`;
131
+ if (typeof value.type === "string" && typeof value.path === "string")
132
+ return `source:${value.type}:path:${path.resolve(value.path)}`;
133
+ if (typeof value.url === "string")
134
+ return `url:${value.url}`;
135
+ if (typeof value.name === "string")
136
+ return `name:${value.name}`;
137
+ return `value:${JSON.stringify(value)}`;
138
+ }
139
+ function setupConflict(pathParts) {
140
+ const field = pathParts.length > 0 ? pathParts.join(".") : "(root)";
141
+ throw new ConfigError(`Setup config conflict at ${field}: another process changed the same field after setup started. Rerun setup against the latest config.`, "INVALID_CONFIG_FILE");
142
+ }
143
+ /** Reapply setup's changes while rejecting concurrent edits to the same field. */
144
+ export function rebaseSetupChanges(original, desired, latest, pathParts = []) {
145
+ if (sameConfigValue(original, desired))
146
+ return latest;
147
+ if (Array.isArray(original) && Array.isArray(desired)) {
148
+ if (latest !== undefined && !Array.isArray(latest))
149
+ setupConflict(pathParts);
150
+ const latestItems = Array.isArray(latest) ? latest : [];
151
+ const originalByKey = new Map(original.map((item) => [configArrayItemKey(item), item]));
152
+ const desiredByKey = new Map(desired.map((item) => [configArrayItemKey(item), item]));
153
+ const latestByKey = new Map(latestItems.map((item) => [configArrayItemKey(item), item]));
154
+ const result = [];
155
+ for (const [key, desiredItem] of desiredByKey) {
156
+ const originalItem = originalByKey.get(key);
157
+ const latestItem = latestByKey.get(key);
158
+ if (originalByKey.has(key)) {
159
+ if (!latestByKey.has(key))
160
+ setupConflict([...pathParts, key]);
161
+ result.push(rebaseSetupChanges(originalItem, desiredItem, latestItem, [...pathParts, key]));
162
+ }
163
+ else if (!latestByKey.has(key) || sameConfigValue(latestItem, desiredItem)) {
164
+ result.push(desiredItem);
165
+ }
166
+ else {
167
+ setupConflict([...pathParts, key]);
168
+ }
169
+ }
170
+ for (const [key, originalItem] of originalByKey) {
171
+ if (desiredByKey.has(key) || !latestByKey.has(key))
172
+ continue;
173
+ if (!sameConfigValue(latestByKey.get(key), originalItem))
174
+ setupConflict([...pathParts, key]);
175
+ }
176
+ for (const item of latestItems) {
177
+ const key = configArrayItemKey(item);
178
+ if (!originalByKey.has(key) && !desiredByKey.has(key))
179
+ result.push(item);
180
+ }
181
+ return result;
182
+ }
183
+ if (!isPlainRecord(original) || !isPlainRecord(desired)) {
184
+ if (!sameConfigValue(latest, original) && !sameConfigValue(latest, desired))
185
+ setupConflict(pathParts);
186
+ return desired;
187
+ }
188
+ if (latest !== undefined && !isPlainRecord(latest))
189
+ setupConflict(pathParts);
190
+ const result = isPlainRecord(latest) ? { ...latest } : {};
191
+ for (const key of new Set([...Object.keys(original), ...Object.keys(desired)])) {
192
+ if (!Object.hasOwn(desired, key)) {
193
+ if (Object.hasOwn(result, key) && !sameConfigValue(result[key], original[key]))
194
+ setupConflict([...pathParts, key]);
195
+ delete result[key];
196
+ continue;
197
+ }
198
+ result[key] = rebaseSetupChanges(original[key], desired[key], result[key], [...pathParts, key]);
199
+ }
200
+ return result;
201
+ }
202
+ async function saveSetupConfig(original, desired, precommit) {
203
+ const result = await mutateConfigWithPrecommit((latest) => rebaseSetupChanges(original, desired, latest), precommit);
204
+ return { config: result.config, precommit: result.precommit };
205
+ }
206
+ /** Registry-managed bundles are not editable through setup's source picker. */
207
+ function managedBundleIds() {
208
+ return new Set(readLockfile().map((entry) => entry.id));
209
+ }
210
+ function setupSourceDescriptor(source) {
211
+ switch (source.type) {
212
+ case "filesystem":
213
+ return source.path ? { path: source.path } : undefined;
214
+ case "git":
215
+ return source.url ? { git: source.url } : undefined;
216
+ case "website":
217
+ return source.url ? { website: { url: source.url, ...(source.options ?? {}) } } : undefined;
218
+ case "npm":
219
+ return source.path ? { npm: source.path } : undefined;
220
+ default:
221
+ return undefined;
222
+ }
223
+ }
224
+ function selectsExistingBundle(source, id, bundle) {
225
+ const configured = bundleEntryToSourceEntry(id, bundle);
226
+ return (configured !== undefined &&
227
+ configured.type === source.type &&
228
+ configured.path === source.path &&
229
+ configured.url === source.url);
230
+ }
231
+ /** Convert only the wizard's private drafts, leaving current bundle entries intact. */
232
+ function finalizeSetupDraft(draft) {
233
+ const { primaryPath, additionalSources, ...canonical } = draft;
234
+ const finalized = canonical;
235
+ if (primaryPath === undefined && additionalSources === undefined)
236
+ return finalized;
237
+ const bundles = { ...(draft.bundles ?? {}) };
238
+ const managedIds = managedBundleIds();
239
+ const newBundles = [];
240
+ if (additionalSources !== undefined) {
241
+ const selectedExistingIds = new Set();
242
+ for (const source of additionalSources) {
243
+ const existing = source.name ? bundles[source.name] : undefined;
244
+ if (source.name &&
245
+ source.name !== draft.defaultBundle &&
246
+ existing &&
247
+ selectsExistingBundle(source, source.name, existing)) {
248
+ selectedExistingIds.add(source.name);
249
+ continue;
250
+ }
251
+ const descriptor = setupSourceDescriptor(source);
252
+ if (!descriptor)
253
+ continue;
254
+ newBundles.push({
255
+ descriptor,
256
+ derivationPath: source.path ? path.resolve(source.path) : (source.url ?? ""),
257
+ preferredId: source.name,
258
+ primary: false,
259
+ writable: source.writable,
260
+ });
261
+ }
262
+ for (const id of Object.keys(bundles)) {
263
+ if (id === draft.defaultBundle || managedIds.has(id) || selectedExistingIds.has(id))
264
+ continue;
265
+ delete bundles[id];
266
+ }
267
+ }
268
+ let defaultBundle = draft.defaultBundle;
269
+ const configuredPrimaryPath = primaryBundlePath(draft);
270
+ if (primaryPath !== undefined &&
271
+ (configuredPrimaryPath === undefined || path.resolve(configuredPrimaryPath) !== path.resolve(primaryPath))) {
272
+ if (defaultBundle && !managedIds.has(defaultBundle))
273
+ delete bundles[defaultBundle];
274
+ defaultBundle = undefined;
275
+ newBundles.unshift({
276
+ descriptor: { path: primaryPath },
277
+ derivationPath: path.resolve(primaryPath),
278
+ primary: true,
279
+ writable: true,
280
+ });
281
+ }
282
+ const reserved = Object.keys(bundles).map((id) => ({ path: id, registryId: id }));
283
+ const ids = deriveBundleIds([
284
+ ...reserved,
285
+ ...newBundles.map((bundle) => ({ path: bundle.derivationPath, registryId: bundle.preferredId })),
286
+ ]).slice(reserved.length);
287
+ for (const [index, bundle] of newBundles.entries()) {
288
+ const id = ids[index];
289
+ bundles[id] = {
290
+ ...bundle.descriptor,
291
+ ...(bundle.writable !== undefined ? { writable: bundle.writable } : {}),
292
+ };
293
+ if (bundle.primary)
294
+ defaultBundle = id;
295
+ }
296
+ if (Object.keys(bundles).length > 0)
297
+ finalized.bundles = bundles;
298
+ else
299
+ delete finalized.bundles;
300
+ if (defaultBundle !== undefined)
301
+ finalized.defaultBundle = defaultBundle;
302
+ else
303
+ delete finalized.defaultBundle;
304
+ return finalized;
305
+ }
98
306
  /**
99
307
  * Quick connectivity check. Returns true if we can resolve a hostname
100
308
  * the user has already implicitly trusted within 3 seconds, false
@@ -145,14 +353,14 @@ export function buildSetupSteps(options) {
145
353
  const steps = [
146
354
  {
147
355
  id: "stash-dir",
148
- label: "Stash Directory",
356
+ label: "Bundle Directory",
149
357
  nonInteractive: true,
150
358
  async run(ctx) {
151
359
  const stashDir = await stepStashDir(ctx.config, {
152
360
  nonInteractive: ctx.nonInteractive,
153
361
  preferredDir: options.preferredStashDir,
154
362
  });
155
- ctx.apply({ stashDir });
363
+ ctx.apply({ primaryPath: stashDir });
156
364
  },
157
365
  },
158
366
  {
@@ -178,7 +386,7 @@ export function buildSetupSteps(options) {
178
386
  return;
179
387
  }
180
388
  const llm = await stepLlm(ctx.config, ollamaEndpoint, ollamaChatModels, lmStudioResult, harnessConfigs);
181
- ctx.apply(applyLegacyLlm(ctx.config, llm));
389
+ ctx.apply(writeLlmEngine(ctx.config, llm));
182
390
  },
183
391
  },
184
392
  {
@@ -200,17 +408,17 @@ export function buildSetupSteps(options) {
200
408
  },
201
409
  {
202
410
  id: "stash-sources",
203
- label: "Stash Sources",
411
+ label: "Bundle Sources",
204
412
  async run(ctx) {
205
413
  const stashes = await stepAddSources(ctx.config, { promptForAdditional: false });
206
- const platforms = await stepAgentPlatforms({ ...ctx.config, sources: stashes });
414
+ const platforms = await stepAgentPlatforms({ ...ctx.config, additionalSources: stashes });
207
415
  const merged = [...stashes];
208
416
  for (const ps of platforms) {
209
417
  if (!merged.some((s) => s.path === ps.path))
210
418
  merged.push(ps);
211
419
  }
212
420
  const withAdditional = await stepAdditionalSources(merged);
213
- ctx.apply({ sources: withAdditional.length > 0 ? withAdditional : undefined });
421
+ ctx.apply({ additionalSources: withAdditional });
214
422
  },
215
423
  },
216
424
  {
@@ -226,11 +434,10 @@ export function buildSetupSteps(options) {
226
434
  else {
227
435
  p.log.info("No agent CLIs detected on PATH. Agent commands will be disabled until one is installed and `akm setup` is re-run.");
228
436
  }
229
- // Inject the detected agent block into a synthetic AkmConfig so
230
- // stepAgentSelection can read it via getCurrentAgentBlock().
231
- const synthConfig = { ...ctx.config, ...applyLegacyAgent(ctx.config, result.agent) };
437
+ // Apply detected engines to a synthetic config for the selection UI.
438
+ const synthConfig = deepMergeConfig(ctx.config, writeAgentEngines(ctx.config, result.agent));
232
439
  const agent = await stepAgentSelection(synthConfig, result.detections);
233
- ctx.apply(applyLegacyAgent(ctx.config, agent));
440
+ ctx.apply(writeAgentEngines(ctx.config, agent));
234
441
  },
235
442
  },
236
443
  {
@@ -241,34 +448,27 @@ export function buildSetupSteps(options) {
241
448
  ctx.apply({ output });
242
449
  },
243
450
  },
244
- {
245
- id: "scheduled-tasks",
246
- label: "Scheduled Tasks",
247
- // Interactive-only: `akm init` / `--yes` skip this step so headless
248
- // runs never enable a scheduled task (see issue #512).
249
- async run() {
250
- await stepDefaultImproveTasks();
251
- await stepScheduledTasks();
252
- },
253
- },
451
+ // NOTE: the "Scheduled Tasks" step is deliberately NOT part of this list.
452
+ // It is the only wizard step with externally-visible side effects (task
453
+ // files + OS scheduler entries), so `runSetupWizard` runs it by hand AFTER
454
+ // the config is confirmed and persisted — see the call there. Keeping it
455
+ // out of the step list also preserves the issue #512 guard: the
456
+ // non-interactive entry points (`akm init` / `--yes`) run this list but
457
+ // never that step, so headless runs never enable a scheduled task.
254
458
  ];
255
459
  return { steps, outcome };
256
460
  }
257
461
  export async function runSetupWizard(opts) {
462
+ assertSetupConfigPreflight();
258
463
  p.intro("akm setup");
259
464
  const current = loadUserConfig();
260
465
  const configPath = getConfigPath();
261
466
  // Resolve stash directory early so akmInit can run before any prompts
262
- const resolvedStashDir = opts?.dir ? path.resolve(opts.dir) : (current.stashDir ?? getDefaultStashDir());
467
+ const resolvedStashDir = opts?.dir ? path.resolve(opts.dir) : (primaryBundlePath(current) ?? getDefaultStashDir());
263
468
  // Refuse explicit --dir /tmp/... before doing any work — protects the host
264
469
  // config from being clobbered with a stashDir that the OS may reap.
265
470
  assertSetupSandbox(resolvedStashDir, opts?.dir != null);
266
471
  applyStashIsolationToEnv(resolvedStashDir, opts?.dir != null);
267
- // Bootstrap directory structure before any prompts so the stash exists
268
- // even if the wizard is interrupted after this point.
269
- if (!opts?.noInit) {
270
- await akmInit({ dir: resolvedStashDir, setDefault: true });
271
- }
272
472
  // Quick connectivity check — skip network-dependent steps when offline
273
473
  const online = await isOnline();
274
474
  if (!online) {
@@ -278,9 +478,9 @@ export async function runSetupWizard(opts) {
278
478
  // Aggregate environment detection — run once before any prompt and surface
279
479
  // a summary so the user sees what was auto-detected. NAMES only, never
280
480
  // API key values.
281
- const detection = await detectEnvironment({ existingStashDir: current.stashDir });
481
+ const detection = await detectEnvironment({ existingStashDir: primaryBundlePath(current) });
282
482
  p.note(renderDetectionSummary(detection), "Detected environment");
283
- // Interactive entry point for `--reset-recommended`: offer to apply the
483
+ // Interactive entry point for `runResetRecommended`: offer to apply the
284
484
  // opinionated, detection-derived defaults and skip the step-by-step wizard.
285
485
  const useRecommended = await prompt(() => p.confirm({
286
486
  message: "Apply recommended defaults from the detected environment (merged into your existing config)?",
@@ -313,35 +513,31 @@ export async function runSetupWizard(opts) {
313
513
  // Step 1/2: Small model connection (for enrichment features)
314
514
  const smallModelResult = await stepSmallModelConnection(ctx.config);
315
515
  if (!smallModelResult.skipped) {
316
- ctx.apply(applyLegacyLlm(ctx.config, smallModelResult.llm));
516
+ ctx.apply(writeLlmEngine(ctx.config, smallModelResult.llm));
317
517
  }
318
518
  // Step 2/2: Agent connection (for agentic features)
319
519
  const agentConfig = await stepAgentConnection(ctx.config, smallModelResult);
320
- ctx.apply(applyLegacyAgent(ctx.config, agentConfig));
321
- const newConfig = {
322
- ...ctx.config,
323
- // Preserve fields the steps don't manage explicitly.
324
- installed: current.installed,
325
- };
520
+ ctx.apply(writeAgentEngines(ctx.config, agentConfig));
521
+ const newConfig = { ...ctx.config };
326
522
  const semanticSearchMode = outcome.semantic;
327
- const stashDir = newConfig.stashDir ?? current.stashDir ?? getDefaultStashDir();
523
+ const stashDir = newConfig.primaryPath ?? primaryBundlePath(current) ?? getDefaultStashDir();
328
524
  const embedding = newConfig.embedding;
329
- const llm = getDefaultLlmConfig(newConfig);
525
+ const llm = readCurrentLlmEngine(newConfig);
330
526
  const registries = newConfig.registries;
331
- const allStashes = newConfig.sources ?? [];
527
+ const allStashes = newConfig.additionalSources ?? [];
332
528
  // Feature capability summary
333
- const agentConfigured = Boolean(agentConfig);
529
+ const agentConfigured = Boolean(agentConfig && !agentConfig.disabled);
334
530
  printCapabilitySummary(smallModelResult.skipped, agentConfigured);
335
531
  // Confirm before saving
336
532
  const effectiveRegistries = registries ?? DEFAULT_CONFIG.registries ?? [];
337
533
  p.note([
338
- `Stash directory: ${stashDir}`,
534
+ `Bundle directory: ${stashDir}`,
339
535
  `Embedding: ${embedding ? `${embedding.provider ?? "remote"} / ${embedding.model}` : "built-in local"}`,
340
536
  `LLM: ${llm ? `${llm.provider ?? "remote"} / ${llm.model}` : "disabled"}`,
341
537
  `Semantic search: ${semanticSearchMode.mode}`,
342
538
  `Registries: ${effectiveRegistries.filter((r) => r.enabled !== false).length} enabled`,
343
- `Stash sources: ${allStashes.length}`,
344
- `Agent default: ${newConfig.defaults?.agent ?? "disabled"}`,
539
+ `Bundle sources: ${allStashes.length}`,
540
+ `Agent default: ${newConfig.defaults?.engine ?? "disabled"}`,
345
541
  `Output: ${newConfig.output?.format ?? "json"} / ${newConfig.output?.detail ?? "brief"}`,
346
542
  ].join("\n"), "Configuration Summary");
347
543
  const shouldSave = await prompt(() => p.confirm({
@@ -350,22 +546,29 @@ export async function runSetupWizard(opts) {
350
546
  }));
351
547
  if (!shouldSave)
352
548
  bail();
353
- // Save config
354
- const cfgPath1 = getConfigPath();
355
- backupAndAnnounce(cfgPath1);
356
- saveConfig(newConfig);
549
+ const finalConfig = finalizeSetupDraft(newConfig);
550
+ validateCompleteConfig(finalConfig);
551
+ const { config: savedConfig } = await saveSetupConfig(current, finalConfig, async () => {
552
+ if (!opts?.noInit)
553
+ await akmInit({ dir: stashDir, setDefault: true, persistConfig: false });
554
+ });
555
+ // After config persistence, the task step reviews the plan and asks one
556
+ // explicit confirmation before changing task files or scheduler state.
557
+ // Non-interactive setup paths never reach this interactive-only step.
558
+ p.log.step("Scheduled Tasks");
559
+ await stepScheduledTasks();
357
560
  if (semanticSearchMode.mode === "off") {
358
561
  clearSemanticStatus();
359
562
  }
360
563
  if (semanticSearchMode.mode === "auto") {
361
564
  if (semanticSearchMode.prepareAssets) {
362
- const ready = await prepareSemanticSearchAssets(newConfig);
565
+ const ready = await prepareSemanticSearchAssets(savedConfig);
363
566
  if (!ready.ok) {
364
567
  writeSemanticStatus({
365
568
  status: "blocked",
366
569
  reason: ready.reason,
367
570
  message: ready.message,
368
- providerFingerprint: deriveSemanticProviderFingerprint(newConfig.embedding),
571
+ providerFingerprint: deriveSemanticProviderFingerprint(savedConfig.embedding),
369
572
  lastCheckedAt: new Date().toISOString(),
370
573
  });
371
574
  p.log.warn("Semantic search remains set to auto, but is currently blocked. Re-run `akm index --full --verbose` once the issue is resolved.");
@@ -374,7 +577,7 @@ export async function runSetupWizard(opts) {
374
577
  writeSemanticStatus({
375
578
  status: "pending",
376
579
  message: "Semantic prerequisites verified. Building the index to finish activation.",
377
- providerFingerprint: deriveSemanticProviderFingerprint(newConfig.embedding),
580
+ providerFingerprint: deriveSemanticProviderFingerprint(savedConfig.embedding),
378
581
  lastCheckedAt: new Date().toISOString(),
379
582
  });
380
583
  }
@@ -383,7 +586,7 @@ export async function runSetupWizard(opts) {
383
586
  writeSemanticStatus({
384
587
  status: "pending",
385
588
  message: "Semantic search is enabled, but asset preparation was skipped.",
386
- providerFingerprint: deriveSemanticProviderFingerprint(newConfig.embedding),
589
+ providerFingerprint: deriveSemanticProviderFingerprint(savedConfig.embedding),
387
590
  lastCheckedAt: new Date().toISOString(),
388
591
  });
389
592
  p.log.info("Semantic search is set to auto, but asset preparation was skipped. Run `akm index --full --verbose` later to verify it.");
@@ -396,7 +599,7 @@ export async function runSetupWizard(opts) {
396
599
  try {
397
600
  const indexResult = await akmIndex({ stashDir });
398
601
  spin.stop(`Indexed ${indexResult.totalEntries} assets.`);
399
- if (newConfig.semanticSearchMode === "auto") {
602
+ if (savedConfig.semanticSearchMode === "auto") {
400
603
  if (indexResult.verification.ok) {
401
604
  p.log.success(indexResult.verification.message);
402
605
  }
@@ -411,12 +614,12 @@ export async function runSetupWizard(opts) {
411
614
  catch (err) {
412
615
  spin.stop("Indexing failed — you can run `akm index` manually later.");
413
616
  p.log.warn(String(err));
414
- if (newConfig.semanticSearchMode === "auto") {
617
+ if (savedConfig.semanticSearchMode === "auto") {
415
618
  writeSemanticStatus({
416
619
  status: "blocked",
417
620
  reason: "index-failed",
418
621
  message: String(err),
419
- providerFingerprint: deriveSemanticProviderFingerprint(newConfig.embedding),
622
+ providerFingerprint: deriveSemanticProviderFingerprint(savedConfig.embedding),
420
623
  lastCheckedAt: new Date().toISOString(),
421
624
  });
422
625
  }
@@ -433,57 +636,32 @@ export async function runSetupWizard(opts) {
433
636
  p.log.info("Reminder: Set your LLM API key via the AKM_LLM_API_KEY environment variable.");
434
637
  }
435
638
  }
436
- p.outro(`Configuration saved to ${configPath}`);
639
+ p.outro(`Configuration saved to ${configPath}\n` +
640
+ 'Next: `akm bundle add <source>`, `akm index`, `akm search "<query>"`, `akm help agents`');
437
641
  }
438
642
  // ── Non-interactive / scripting entry points ─────────────────────────────────
439
- /**
440
- * Back up an existing config file and print the real, timestamped backup
441
- * location (not a generic display string). On a fresh install where there is
442
- * nothing to back up, print a "nothing to back up" notice instead.
443
- */
444
- function backupAndAnnounce(configPath) {
445
- const result = backupExistingConfig(configPath);
446
- const message = result ? `Config backed up to ${result.timestamped}` : "No existing config to back up.";
447
- // In JSON output mode the structured envelope (which already carries
448
- // `configPath`) MUST be the only thing on stdout — `setup --yes | jq` is a
449
- // supported scripting contract. @clack's `p.log.info` writes to stdout, which
450
- // would corrupt that envelope, so route this human-progress notice to stderr
451
- // when emitting JSON. Interactive/text runs keep the inline clack banner.
452
- if (isJsonOutputMode()) {
453
- process.stderr.write(`${message}\n`);
454
- }
455
- else {
456
- p.log.info(message);
457
- }
458
- }
459
- /**
460
- * True when the process-level output mode is JSON (the default machine format).
461
- * Defensive: setup is also invoked programmatically (tests) where the output
462
- * mode singleton may not be initialized — treat that as "not JSON" so the
463
- * human-readable clack banner is used.
464
- */
465
- function isJsonOutputMode() {
466
- try {
467
- return getOutputMode().format === "json";
468
- }
469
- catch {
470
- return false;
471
- }
472
- }
473
643
  /**
474
644
  * Run setup in non-interactive mode, applying all defaults.
475
645
  * Safe to call from CI or scripts. Idempotent — re-running produces the same result.
476
646
  */
477
647
  export async function runSetupWithDefaults(opts) {
478
- const current = loadUserConfig();
479
- const stashDir = opts.dir ? path.resolve(opts.dir) : (current.stashDir ?? getDefaultStashDir());
480
- assertSetupSandbox(stashDir, opts.dir != null);
481
- applyStashIsolationToEnv(stashDir, opts.dir != null);
482
- // Bootstrap directory structure first
483
- let initResult;
484
- if (!opts.noInit) {
485
- initResult = await akmInit({ dir: stashDir, setDefault: true });
648
+ assertSetupConfigPreflight();
649
+ const explicitStashDir = opts.dir != null ? path.resolve(opts.dir) : undefined;
650
+ // R-066 #4: a second `assertSetupSandbox(stashDir, explicitStashDir != null)`
651
+ // + `applyStashIsolationToEnv(stashDir, explicitStashDir != null)` pair used
652
+ // to follow `stashDir`'s resolution below. It was strictly redundant: unlike
653
+ // `runSetupFromConfig` (which can also derive an explicit stash dir from an
654
+ // incoming config file's bundle path, a case the early block below can't
655
+ // see yet), `runSetupWithDefaults` has only one source of an explicit dir —
656
+ // `opts.dir` — so `stashDir` always equals `explicitStashDir` whenever the
657
+ // `if` below ran, and always fails `dirExplicitlyProvided` (an immediate
658
+ // no-op in both helpers) whenever it didn't. Removed rather than duplicated.
659
+ if (explicitStashDir) {
660
+ assertSetupSandbox(explicitStashDir, true);
661
+ applyStashIsolationToEnv(explicitStashDir, true);
486
662
  }
663
+ const current = loadUserConfig();
664
+ const stashDir = explicitStashDir ?? primaryBundlePath(current) ?? getDefaultStashDir();
487
665
  // Run steps in non-interactive mode (applies defaults, skips prompts)
488
666
  const ctx = createSetupContext(current, { nonInteractive: true });
489
667
  const { steps } = buildSetupSteps({
@@ -492,18 +670,17 @@ export async function runSetupWithDefaults(opts) {
492
670
  preferredStashDir: stashDir,
493
671
  });
494
672
  await runSetupSteps(steps, ctx);
495
- // Ensure stashDir is set
496
- if (!ctx.config.stashDir)
497
- ctx.apply({ stashDir });
673
+ if (!ctx.config.primaryPath)
674
+ ctx.apply({ primaryPath: stashDir });
498
675
  // Aggregate environment detection — apply detected values directly.
499
- const env = await detectEnvironment({ existingStashDir: ctx.config.stashDir });
676
+ const env = await detectEnvironment({ existingStashDir: ctx.config.primaryPath });
500
677
  // Apply a detected LLM (live local server) when the config has none yet.
501
- if (!getDefaultLlmConfig(ctx.config)) {
678
+ if (!readCurrentLlmEngine(ctx.config) && opts.probe) {
502
679
  const liveLocal = env.localServers.find((s) => s.available && s.defaultModel);
503
680
  if (liveLocal?.defaultModel) {
504
681
  const llm = {
505
682
  provider: "local",
506
- endpoint: `${liveLocal.baseUrl.replace(/\/$/, "")}/v1`,
683
+ endpoint: `${liveLocal.baseUrl.replace(/\/$/, "")}/v1/chat/completions`,
507
684
  model: liveLocal.defaultModel,
508
685
  };
509
686
  // A required field being unresolvable must fail loudly rather than write
@@ -511,11 +688,22 @@ export async function runSetupWithDefaults(opts) {
511
688
  if (!llm.endpoint?.trim() || !llm.model?.trim()) {
512
689
  throw new UsageError("Detected a local LLM server but could not resolve a required field (endpoint/model). Re-run `akm setup` interactively.", "MISSING_REQUIRED_ARGUMENT");
513
690
  }
514
- ctx.apply(applyLegacyLlm(ctx.config, llm));
691
+ const verified = await verifyOpenAiCompatibleEndpoint(llm);
692
+ if (verified.ok) {
693
+ const applied = upsertDetectedLlmEngine(ctx.config, {
694
+ provider: llm.provider ?? "local",
695
+ endpoint: verified.endpoint,
696
+ model: llm.model,
697
+ });
698
+ ctx.apply(applied.config);
699
+ }
700
+ else {
701
+ warn(`[akm setup] Skipping detected local LLM: ${verified.reason}. Verify it and rerun setup.`);
702
+ }
515
703
  }
516
704
  }
517
705
  // Auto-detect agent CLI if not already configured
518
- if (!ctx.config.defaults?.agent) {
706
+ if (!ctx.config.defaults?.engine) {
519
707
  let defaultProfile;
520
708
  if (env.harness !== "none") {
521
709
  defaultProfile = env.harness;
@@ -525,31 +713,33 @@ export async function runSetupWithDefaults(opts) {
525
713
  defaultProfile = pickDefaultAgentProfile(detected, undefined);
526
714
  }
527
715
  if (defaultProfile) {
528
- ctx.apply(applyLegacyAgent(ctx.config, { default: defaultProfile }));
716
+ ctx.apply(upsertDetectedAgentEngine(ctx.config, defaultProfile).config);
529
717
  }
530
718
  }
531
- const cfgPath2 = getConfigPath();
532
- backupAndAnnounce(cfgPath2);
533
- saveConfig(ctx.config);
719
+ const finalConfig = finalizeSetupDraft(ctx.config);
720
+ validateCompleteConfig(finalConfig);
721
+ const { precommit: initResult } = await saveSetupConfig(current, finalConfig, async () => {
722
+ if (opts.noInit)
723
+ return undefined;
724
+ return akmInit({ dir: stashDir, setDefault: true, persistConfig: false });
725
+ });
534
726
  return {
535
727
  configPath: getConfigPath(),
536
- stashDir,
728
+ bundleDir: stashDir,
537
729
  stashCreated: initResult?.created ?? false,
538
730
  written: true,
539
- fields: Object.keys(ctx.config).filter((k) => ctx.config[k] !== undefined),
540
- ripgrep: initResult?.ripgrep,
731
+ fields: Object.keys(finalConfig).filter((k) => finalConfig[k] !== undefined),
541
732
  };
542
733
  }
543
734
  /**
544
735
  * Run ONLY environment detection and return the typed result. Performs no
545
- * config writes and shows no prompts. Backs `akm setup --detect-only`.
736
+ * config writes and shows no prompts.
546
737
  *
547
738
  * SAFETY: The returned object carries env var NAMES only — never any API key
548
739
  * value.
549
740
  */
550
741
  export async function runDetectOnly() {
551
- const current = loadUserConfig();
552
- return detectEnvironment({ existingStashDir: current.stashDir });
742
+ return detectEnvironment();
553
743
  }
554
744
  /**
555
745
  * Derive opinionated defaults from a detection result.
@@ -557,9 +747,8 @@ export async function runDetectOnly() {
557
747
  * - Best harness → agent default (when a profile maps to it).
558
748
  * - Fastest live local model, else the first detected cloud key's provider.
559
749
  * - `nomic-embed-text` embeddings when a local LLM is live.
560
- * - improve task `0 2 * * *`, index task `0 4 * * *`.
561
750
  *
562
- * Returns a partial `AkmConfig`-shaped object plus a legacy `llm` block, ready
751
+ * Returns a partial `AkmConfig`-shaped object plus an LLM connection block, ready
563
752
  * to merge. Never includes an API key value.
564
753
  */
565
754
  export function deriveRecommendedConfig(env) {
@@ -590,31 +779,59 @@ export function deriveRecommendedConfig(env) {
590
779
  const defaults = PROVIDER_DEFAULTS[cloud.provider];
591
780
  if (defaults) {
592
781
  result.llm = { provider: cloud.provider, endpoint: defaults.endpoint, model: defaults.model };
782
+ result.llmApiKeyEnvVar = cloud.envVar;
593
783
  }
594
784
  }
595
785
  }
596
- result.taskSchedules = { improve: "0 2 * * *", index: "0 4 * * *" };
597
786
  return result;
598
787
  }
599
788
  /**
600
- * `akm setup --reset-recommended`: merge opinionated, detection-derived
601
- * defaults into the existing config WITHOUT removing pre-existing custom keys.
602
- * Uses the same merge path as {@link runSetupFromConfig} so custom keys survive
603
- * (follows #511 semantics).
789
+ * Merge opinionated, detection-derived defaults into the existing config
790
+ * WITHOUT removing pre-existing custom keys. Backs the interactive wizard's
791
+ * "apply recommended defaults" prompt. Uses the same merge path as
792
+ * {@link runSetupFromConfig} so custom keys survive (follows #511 semantics).
604
793
  */
605
794
  export async function runResetRecommended(opts) {
795
+ assertSetupConfigPreflight();
796
+ const explicitStashDir = opts.dir != null ? path.resolve(opts.dir) : undefined;
797
+ if (explicitStashDir) {
798
+ assertSetupSandbox(explicitStashDir, true);
799
+ applyStashIsolationToEnv(explicitStashDir, true);
800
+ }
606
801
  const current = loadUserConfig();
607
- const env = await detectEnvironment({ existingStashDir: current.stashDir });
802
+ const env = await detectEnvironment({ existingStashDir: primaryBundlePath(current) });
608
803
  const recommended = deriveRecommendedConfig(env);
609
- const incoming = {};
610
- if (recommended.llm)
611
- incoming.llm = recommended.llm;
804
+ let incoming = {};
805
+ if (recommended.llm && recommended.llm.provider !== "anthropic") {
806
+ let endpoint = recommended.llm.endpoint;
807
+ let accepted = true;
808
+ if (opts.probe) {
809
+ const verified = await verifyOpenAiCompatibleEndpoint({
810
+ endpoint,
811
+ model: recommended.llm.model,
812
+ apiKeyEnvVar: recommended.llmApiKeyEnvVar,
813
+ });
814
+ if (verified.ok) {
815
+ endpoint = verified.endpoint;
816
+ }
817
+ else {
818
+ accepted = false;
819
+ warn(`[akm setup] Skipping detected LLM: ${verified.reason}. Verify it and rerun setup.`);
820
+ }
821
+ }
822
+ if (accepted) {
823
+ incoming = upsertDetectedLlmEngine(incoming, {
824
+ provider: recommended.llm.provider ?? "local",
825
+ endpoint,
826
+ model: recommended.llm.model,
827
+ apiKeyEnvVar: recommended.llmApiKeyEnvVar,
828
+ }).config;
829
+ }
830
+ }
612
831
  if (recommended.embedding)
613
832
  incoming.embedding = recommended.embedding;
614
- if (recommended.agentDefault)
615
- incoming.agent = { default: recommended.agentDefault };
616
- if (recommended.taskSchedules) {
617
- incoming.setup = { taskSchedules: recommended.taskSchedules };
833
+ if (recommended.agentDefault) {
834
+ incoming = deepMergeConfig(incoming, writeAgentEngines(incoming, { default: recommended.agentDefault }));
618
835
  }
619
836
  return runSetupFromConfig({
620
837
  configJson: JSON.stringify(incoming),
@@ -628,25 +845,38 @@ export async function runResetRecommended(opts) {
628
845
  * Validates required sub-fields and strips unknown/restricted keys.
629
846
  */
630
847
  export async function runSetupFromConfig(opts) {
631
- let incoming;
848
+ assertSetupConfigPreflight();
849
+ // Phase 1: Parse JSON
850
+ let parsed;
632
851
  try {
633
- incoming = JSON.parse(opts.configJson);
852
+ parsed = JSON.parse(opts.configJson);
634
853
  }
635
854
  catch (e) {
636
855
  throw new Error(`Invalid JSON in --config: ${e.message}`);
637
856
  }
857
+ if (!isPlainRecord(parsed)) {
858
+ throw new ConfigError("Setup config must contain a top-level object.", "INVALID_CONFIG_FILE");
859
+ }
860
+ const incoming = parsed;
638
861
  // Phase 2: Validate — only allow safe top-level keys
639
- const ALLOWED_KEYS = new Set([
640
- "stashDir",
641
- "llm",
642
- "embedding",
643
- "agent",
644
- "semanticSearchMode",
645
- "output",
646
- "profiles",
647
- "defaults",
648
- "setup",
649
- ]);
862
+ for (const key of ["stashDir", "sources", "installed"]) {
863
+ if (key in incoming) {
864
+ throw new ConfigError(`${key} is not supported by the current config schema; run the standalone akm-migrate tool first.`, "INVALID_CONFIG_FILE");
865
+ }
866
+ }
867
+ // Derived from AkmConfigShape (via `listTopLevelConfigKeys`) rather than a
868
+ // hand-copied set: a hand-copied allowlist silently fell out of sync as the
869
+ // schema grew (R-017 — `index`, `search`, `feedback`, `archiveRetentionDays`,
870
+ // `workflow`, and `experimental` were all valid schema keys that this list
871
+ // forgot, each dropped with only a warning while the command exited 0).
872
+ // Every key the schema recognizes is a legitimate thing to set via
873
+ // `--config`/`--from`; there is no key a user can set here that they
874
+ // couldn't set by hand-editing config.json and letting `akm config set`
875
+ // validate it. Keys that remain genuinely retired (`profiles`, `llm`,
876
+ // `agent`, `features`, `stashes`, `bindings`, `writable`) are not part of
877
+ // the schema shape, so they still fall into the warn-and-drop branch below;
878
+ // `stashDir`/`sources`/`installed` get the more specific migration error above.
879
+ const ALLOWED_KEYS = new Set(listTopLevelConfigKeys());
650
880
  for (const key of Object.keys(incoming)) {
651
881
  if (!ALLOWED_KEYS.has(key)) {
652
882
  warn(`[akm setup] Ignoring unknown or restricted config key: "${key}"`);
@@ -654,12 +884,6 @@ export async function runSetupFromConfig(opts) {
654
884
  }
655
885
  }
656
886
  // Validate required sub-fields
657
- if (incoming.llm) {
658
- if (!incoming.llm.endpoint?.trim())
659
- throw new Error("llm.endpoint is required when llm is provided");
660
- if (!incoming.llm.model?.trim())
661
- throw new Error("llm.model is required when llm is provided");
662
- }
663
887
  if (incoming.embedding) {
664
888
  if (!incoming.embedding.endpoint?.trim())
665
889
  throw new Error("embedding.endpoint is required when embedding is provided");
@@ -667,34 +891,45 @@ export async function runSetupFromConfig(opts) {
667
891
  throw new Error("embedding.model is required when embedding is provided");
668
892
  }
669
893
  // Phase 3: Merge with existing config
894
+ //
895
+ // R-066 #4 (verified, NOT reduced — unlike the `runSetupWithDefaults` pair
896
+ // above, this one is NOT strictly redundant): the `assertSetupSandbox` +
897
+ // `applyStashIsolationToEnv` pair below looks like a duplicate of the pair
898
+ // here, but each guards a case the other cannot:
899
+ // - THIS early pair must run before `loadUserConfig()` so an explicit
900
+ // `--dir` pointed at a force-escaped transient sandbox isolates the
901
+ // config READ too, not just the later write (the isolation env var has
902
+ // to be set before `loadUserConfig()` executes, or `current` below is
903
+ // read from the host config instead of the sandboxed one — the exact
904
+ // failure class the "2026-05-23 setup-clobbers-user-config incident"
905
+ // comment on `assertSetupSandbox` describes).
906
+ // - The LATER pair below additionally covers a case this one can't see
907
+ // yet: an incoming `--config`/`--file` JSON blob that names its own
908
+ // bundle path (`incomingPrimaryPath`) with no `--dir` at all — that
909
+ // path is only known after the merge, so it cannot be checked early.
910
+ // When `explicitStashDir` is set both pairs do end up asserting the same
911
+ // (stashDir, true), but removing either one changes behavior in the case
912
+ // it uniquely covers, so both stay.
913
+ const explicitStashDir = opts.dir != null ? path.resolve(opts.dir) : undefined;
914
+ if (explicitStashDir) {
915
+ assertSetupSandbox(explicitStashDir, true);
916
+ applyStashIsolationToEnv(explicitStashDir, true);
917
+ }
670
918
  const current = loadUserConfig();
671
- const stashDir = opts.dir
672
- ? path.resolve(opts.dir)
673
- : incoming.stashDir
674
- ? path.resolve(incoming.stashDir)
675
- : (current.stashDir ?? getDefaultStashDir());
676
- const stashDirExplicit = opts.dir != null || incoming.stashDir != null;
919
+ let merged = deepMergeConfig(current, incoming);
920
+ const incomingPrimaryPath = primaryBundlePath(incoming);
921
+ const configuredPrimaryPath = primaryBundlePath(merged);
922
+ const stashDir = explicitStashDir ?? configuredPrimaryPath ?? getDefaultStashDir();
923
+ const stashDirExplicit = explicitStashDir != null || incomingPrimaryPath !== undefined;
677
924
  assertSetupSandbox(stashDir, stashDirExplicit);
678
925
  applyStashIsolationToEnv(stashDir, stashDirExplicit);
679
- let merged = { ...current, stashDir };
680
- // Deep-merge non-llm/agent keys: nested objects merge key-by-key so a
926
+ if (explicitStashDir !== undefined || configuredPrimaryPath === undefined) {
927
+ merged = deepMergeConfig(merged, { primaryPath: stashDir });
928
+ }
929
+ // Deep-merge canonical keys: nested objects merge key-by-key so a
681
930
  // partial `--file` only updates the keys it carries and never drops sibling
682
931
  // subkeys (e.g. output.detail survives an output.format-only file). Arrays
683
932
  // and scalars replace wholesale.
684
- for (const key of Object.keys(incoming)) {
685
- if (key === "llm" || key === "agent")
686
- continue;
687
- const incomingVal = incoming[key];
688
- const mergedRec = merged;
689
- mergedRec[key] = deepMergeConfig(mergedRec[key], incomingVal);
690
- }
691
- // Translate legacy llm/agent inputs into the new shape.
692
- if (incoming.llm) {
693
- merged = { ...merged, ...applyLegacyLlm(merged, incoming.llm) };
694
- }
695
- if (incoming.agent) {
696
- merged = { ...merged, ...applyLegacyAgent(merged, incoming.agent) };
697
- }
698
933
  // With `--yes`, fill keys still missing after the merge with non-interactive
699
934
  // defaults. Steps start from `merged` and their nonInteractive path only
700
935
  // populates absent values, so nothing the file or existing config supplied
@@ -707,31 +942,30 @@ export async function runSetupFromConfig(opts) {
707
942
  preferredStashDir: stashDir,
708
943
  });
709
944
  await runSetupSteps(steps, ctx);
710
- if (!ctx.config.stashDir)
711
- ctx.apply({ stashDir });
712
- if (!ctx.config.defaults?.agent) {
945
+ if (!ctx.config.primaryPath)
946
+ ctx.apply({ primaryPath: stashDir });
947
+ if (!ctx.config.defaults?.engine) {
713
948
  const detected = detectAgentCliProfiles(undefined);
714
949
  const defaultProfile = pickDefaultAgentProfile(detected, undefined);
715
950
  if (defaultProfile) {
716
- ctx.apply(applyLegacyAgent(ctx.config, { default: defaultProfile }));
951
+ ctx.apply(upsertDetectedAgentEngine(ctx.config, defaultProfile).config);
717
952
  }
718
953
  }
719
954
  merged = ctx.config;
720
955
  }
721
- // Bootstrap directory structure
722
- let initResult;
723
- if (!opts.noInit) {
724
- initResult = await akmInit({ dir: stashDir, setDefault: true });
725
- }
956
+ // Convert the private setup draft to the persisted bundle shape.
957
+ let finalizedMerged = finalizeSetupDraft(merged);
958
+ // Reject an invalid merged engine graph before probing or touching the stash.
959
+ validateCompleteConfig(finalizedMerged);
726
960
  // Optional probe
727
- const mergedLlm = getDefaultLlmConfig(merged);
961
+ const mergedLlm = readCurrentLlmEngine(finalizedMerged);
728
962
  if (opts.probe && mergedLlm) {
729
963
  try {
730
964
  const caps = await probeLlmCapabilities(mergedLlm);
731
965
  if (caps.reachable) {
732
- merged = {
733
- ...merged,
734
- ...applyLegacyLlm(merged, {
966
+ finalizedMerged = {
967
+ ...finalizedMerged,
968
+ ...writeLlmEngine(finalizedMerged, {
735
969
  ...mergedLlm,
736
970
  capabilities: { structuredOutput: caps.structuredOutput ?? false },
737
971
  }),
@@ -742,16 +976,18 @@ export async function runSetupFromConfig(opts) {
742
976
  // Non-fatal: probe failure is informational only
743
977
  }
744
978
  }
745
- const cfgPath3 = getConfigPath();
746
- backupAndAnnounce(cfgPath3);
747
- saveConfig(merged);
979
+ validateCompleteConfig(finalizedMerged);
980
+ const { precommit: initResult } = await saveSetupConfig(current, finalizedMerged, async () => {
981
+ if (opts.noInit)
982
+ return undefined;
983
+ return akmInit({ dir: stashDir, setDefault: true, persistConfig: false });
984
+ });
748
985
  return {
749
986
  configPath: getConfigPath(),
750
- stashDir,
987
+ bundleDir: stashDir,
751
988
  stashCreated: initResult?.created ?? false,
752
989
  written: true,
753
990
  fields: Object.keys(incoming).filter((k) => incoming[k] !== undefined),
754
- ripgrep: initResult?.ripgrep,
755
991
  };
756
992
  }
757
993
  // ── Setup --from <file> bootstrap helper ────────────────────────────────────