akm-cli 0.6.1 → 0.7.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 (333) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/{cli.js → src/cli.js} +712 -34
  3. package/dist/{commands → src/commands}/config-cli.js +47 -4
  4. package/dist/src/commands/distill.js +283 -0
  5. package/dist/src/commands/events.js +108 -0
  6. package/dist/src/commands/history.js +191 -0
  7. package/dist/{commands → src/commands}/installed-stashes.js +1 -1
  8. package/dist/src/commands/proposal.js +119 -0
  9. package/dist/src/commands/propose.js +171 -0
  10. package/dist/src/commands/reflect.js +193 -0
  11. package/dist/{commands → src/commands}/registry-search.js +71 -7
  12. package/dist/{commands → src/commands}/remember.js +12 -0
  13. package/dist/{commands → src/commands}/search.js +104 -4
  14. package/dist/{commands → src/commands}/self-update.js +4 -3
  15. package/dist/{commands → src/commands}/show.js +73 -0
  16. package/dist/{commands → src/commands}/source-add.js +5 -1
  17. package/dist/{commands → src/commands}/source-manage.js +7 -1
  18. package/dist/{core → src/core}/asset-ref.js +5 -5
  19. package/dist/{core → src/core}/asset-spec.js +12 -0
  20. package/dist/{core → src/core}/common.js +1 -1
  21. package/dist/{core → src/core}/config.js +203 -121
  22. package/dist/{core → src/core}/errors.js +4 -0
  23. package/dist/src/core/events.js +239 -0
  24. package/dist/src/core/lesson-lint.js +86 -0
  25. package/dist/src/core/proposals.js +406 -0
  26. package/dist/src/core/warn.js +72 -0
  27. package/dist/{core → src/core}/write-source.js +80 -5
  28. package/dist/{indexer → src/indexer}/db-search.js +114 -24
  29. package/dist/{indexer → src/indexer}/db.js +76 -23
  30. package/dist/{indexer → src/indexer}/file-context.js +0 -3
  31. package/dist/src/indexer/graph-boost.js +179 -0
  32. package/dist/src/indexer/graph-extraction.js +212 -0
  33. package/dist/{indexer → src/indexer}/indexer.js +88 -7
  34. package/dist/{indexer → src/indexer}/matchers.js +1 -1
  35. package/dist/src/indexer/memory-inference.js +263 -0
  36. package/dist/{indexer → src/indexer}/metadata.js +111 -3
  37. package/dist/{indexer → src/indexer}/search-source.js +4 -2
  38. package/dist/src/integrations/agent/config.js +292 -0
  39. package/dist/src/integrations/agent/detect.js +94 -0
  40. package/dist/src/integrations/agent/index.js +17 -0
  41. package/dist/src/integrations/agent/profiles.js +65 -0
  42. package/dist/src/integrations/agent/prompts.js +167 -0
  43. package/dist/src/integrations/agent/spawn.js +272 -0
  44. package/dist/{integrations → src/integrations}/github.js +9 -3
  45. package/dist/{integrations → src/integrations}/lockfile.js +0 -26
  46. package/dist/{llm → src/llm}/client.js +33 -2
  47. package/dist/{llm → src/llm}/embedders/remote.js +37 -3
  48. package/dist/src/llm/feature-gate.js +108 -0
  49. package/dist/src/llm/graph-extract.js +107 -0
  50. package/dist/src/llm/index-passes.js +35 -0
  51. package/dist/src/llm/memory-infer.js +86 -0
  52. package/dist/{output → src/output}/cli-hints.js +15 -2
  53. package/dist/{output → src/output}/renderers.js +63 -2
  54. package/dist/src/output/shapes.js +523 -0
  55. package/dist/src/output/text.js +1116 -0
  56. package/dist/{registry → src/registry}/build-index.js +19 -8
  57. package/dist/{registry → src/registry}/factory.js +0 -8
  58. package/dist/{registry → src/registry}/providers/static-index.js +6 -3
  59. package/dist/{registry → src/registry}/resolve.js +68 -2
  60. package/dist/{setup → src/setup}/setup.js +52 -5
  61. package/dist/{sources → src/sources}/providers/git.js +7 -15
  62. package/dist/{wiki → src/wiki}/wiki.js +54 -6
  63. package/dist/{workflows → src/workflows}/runs.js +37 -3
  64. package/dist/tests/add-website-source.test.js +119 -0
  65. package/dist/tests/agent/agent-config-loader.test.js +70 -0
  66. package/dist/tests/agent/agent-config.test.js +221 -0
  67. package/dist/tests/agent/agent-detect.test.js +100 -0
  68. package/dist/tests/agent/agent-spawn.test.js +234 -0
  69. package/dist/tests/agent-output.test.js +186 -0
  70. package/dist/tests/architecture/agent-no-llm-sdk-guard.test.js +103 -0
  71. package/dist/tests/architecture/agent-spawn-seam.test.js +193 -0
  72. package/dist/tests/architecture/llm-stateless-seam.test.js +112 -0
  73. package/dist/tests/asset-ref.test.js +192 -0
  74. package/dist/tests/asset-registry.test.js +103 -0
  75. package/dist/tests/asset-spec.test.js +241 -0
  76. package/dist/tests/bench/attribution.test.js +996 -0
  77. package/dist/tests/bench/cleanup-sigint.test.js +83 -0
  78. package/dist/tests/bench/cleanup.js +234 -0
  79. package/dist/tests/bench/cleanup.test.js +166 -0
  80. package/dist/tests/bench/cli.js +1018 -0
  81. package/dist/tests/bench/cli.test.js +445 -0
  82. package/dist/tests/bench/compare.test.js +556 -0
  83. package/dist/tests/bench/corpus.js +317 -0
  84. package/dist/tests/bench/corpus.test.js +258 -0
  85. package/dist/tests/bench/doctor.js +525 -0
  86. package/dist/tests/bench/driver.js +401 -0
  87. package/dist/tests/bench/driver.test.js +584 -0
  88. package/dist/tests/bench/environment.js +233 -0
  89. package/dist/tests/bench/environment.test.js +199 -0
  90. package/dist/tests/bench/evolve-metrics.js +179 -0
  91. package/dist/tests/bench/evolve-metrics.test.js +187 -0
  92. package/dist/tests/bench/evolve.js +647 -0
  93. package/dist/tests/bench/evolve.test.js +624 -0
  94. package/dist/tests/bench/failure-modes.test.js +349 -0
  95. package/dist/tests/bench/feedback-integrity.test.js +457 -0
  96. package/dist/tests/bench/leakage.test.js +228 -0
  97. package/dist/tests/bench/learning-curve.test.js +134 -0
  98. package/dist/tests/bench/metrics.js +2395 -0
  99. package/dist/tests/bench/metrics.test.js +1150 -0
  100. package/dist/tests/bench/no-os-tmpdir-invariant.test.js +43 -0
  101. package/dist/tests/bench/opencode-config.js +194 -0
  102. package/dist/tests/bench/opencode-config.test.js +370 -0
  103. package/dist/tests/bench/report.js +1885 -0
  104. package/dist/tests/bench/report.test.js +1038 -0
  105. package/dist/tests/bench/run-config.js +355 -0
  106. package/dist/tests/bench/run-config.test.js +298 -0
  107. package/dist/tests/bench/run-curate-test.js +32 -0
  108. package/dist/tests/bench/run-failing-tasks.js +56 -0
  109. package/dist/tests/bench/run-full-bench.js +51 -0
  110. package/dist/tests/bench/run-items36-targeted.js +69 -0
  111. package/dist/tests/bench/run-nano-quick.js +42 -0
  112. package/dist/tests/bench/run-waveg-targeted.js +62 -0
  113. package/dist/tests/bench/runner.js +699 -0
  114. package/dist/tests/bench/runner.test.js +958 -0
  115. package/dist/tests/bench/search-bridge.test.js +331 -0
  116. package/dist/tests/bench/tmp.js +131 -0
  117. package/dist/tests/bench/trajectory.js +116 -0
  118. package/dist/tests/bench/trajectory.test.js +127 -0
  119. package/dist/tests/bench/verifier.js +114 -0
  120. package/dist/tests/bench/verifier.test.js +118 -0
  121. package/dist/tests/bench/workflow-evaluator.js +557 -0
  122. package/dist/tests/bench/workflow-evaluator.test.js +421 -0
  123. package/dist/tests/bench/workflow-spec.js +345 -0
  124. package/dist/tests/bench/workflow-spec.test.js +363 -0
  125. package/dist/tests/bench/workflow-trace.js +472 -0
  126. package/dist/tests/bench/workflow-trace.test.js +254 -0
  127. package/dist/tests/benchmark-search-quality.js +536 -0
  128. package/dist/tests/benchmark-suite.js +1441 -0
  129. package/dist/tests/capture-cli.test.js +112 -0
  130. package/dist/tests/cli-errors.test.js +204 -0
  131. package/dist/tests/commands/events.test.js +370 -0
  132. package/dist/tests/commands/history.test.js +418 -0
  133. package/dist/tests/commands/import.test.js +103 -0
  134. package/dist/tests/commands/proposal-cli.test.js +209 -0
  135. package/dist/tests/commands/reflect-propose-cli.test.js +333 -0
  136. package/dist/tests/commands/remember.test.js +97 -0
  137. package/dist/tests/commands/scope-flags.test.js +300 -0
  138. package/dist/tests/commands/search.test.js +537 -0
  139. package/dist/tests/commands/show-indexer-parity.test.js +117 -0
  140. package/dist/tests/commands/show.test.js +294 -0
  141. package/dist/tests/common.test.js +266 -0
  142. package/dist/tests/completions.test.js +142 -0
  143. package/dist/tests/config-cli.test.js +193 -0
  144. package/dist/tests/config-llm-features.test.js +139 -0
  145. package/dist/tests/config.test.js +569 -0
  146. package/dist/tests/contracts/migration-baseline.test.js +43 -0
  147. package/dist/tests/contracts/reflect-propose-envelope.test.js +139 -0
  148. package/dist/tests/contracts/spec-helpers.js +46 -0
  149. package/dist/tests/contracts/v1-spec-section-11-proposal-queue.test.js +228 -0
  150. package/dist/tests/contracts/v1-spec-section-12-agent-config.test.js +56 -0
  151. package/dist/tests/contracts/v1-spec-section-13-lesson-type.test.js +34 -0
  152. package/dist/tests/contracts/v1-spec-section-14-llm-features.test.js +94 -0
  153. package/dist/tests/contracts/v1-spec-section-4-1-asset-types.test.js +39 -0
  154. package/dist/tests/contracts/v1-spec-section-4-2-quality-rules.test.js +44 -0
  155. package/dist/tests/contracts/v1-spec-section-5-configuration.test.js +47 -0
  156. package/dist/tests/contracts/v1-spec-section-6-orchestration.test.js +40 -0
  157. package/dist/tests/contracts/v1-spec-section-7-module-layout.test.js +58 -0
  158. package/dist/tests/contracts/v1-spec-section-8-extension-points.test.js +34 -0
  159. package/dist/tests/contracts/v1-spec-section-9-4-cli-surface.test.js +75 -0
  160. package/dist/tests/contracts/v1-spec-section-9-7-llm-agent-boundary.test.js +36 -0
  161. package/dist/tests/core/write-source.test.js +366 -0
  162. package/dist/tests/curate-command.test.js +87 -0
  163. package/dist/tests/db-scoring.test.js +201 -0
  164. package/dist/tests/db.test.js +654 -0
  165. package/dist/tests/distill-cli-flag.test.js +208 -0
  166. package/dist/tests/distill.test.js +515 -0
  167. package/dist/tests/docker-install.test.js +120 -0
  168. package/dist/tests/e2e.test.js +1419 -0
  169. package/dist/tests/embedder.test.js +340 -0
  170. package/dist/tests/embedding-model-config.test.js +379 -0
  171. package/dist/tests/feedback-command.test.js +172 -0
  172. package/dist/tests/file-context.test.js +552 -0
  173. package/dist/tests/fixtures/scripts/git/summarize-diff.js +9 -0
  174. package/dist/tests/fixtures/scripts/lint/eslint-check.js +7 -0
  175. package/dist/tests/fixtures/stashes/load.js +166 -0
  176. package/dist/tests/fixtures/stashes/load.test.js +97 -0
  177. package/dist/tests/fixtures/stashes/ranking-baseline/scripts/mem0-search.js +12 -0
  178. package/dist/tests/frontmatter.test.js +190 -0
  179. package/dist/tests/fts-field-weighting.test.js +254 -0
  180. package/dist/tests/fuzzy-search.test.js +230 -0
  181. package/dist/tests/git-provider-clone.test.js +45 -0
  182. package/dist/tests/github.test.js +161 -0
  183. package/dist/tests/graph-boost-ranking.test.js +305 -0
  184. package/dist/tests/graph-extraction.test.js +282 -0
  185. package/dist/tests/helpers/usage-events.js +8 -0
  186. package/dist/tests/index-pass-llm.test.js +161 -0
  187. package/dist/tests/indexer.test.js +570 -0
  188. package/dist/tests/info-command.test.js +166 -0
  189. package/dist/tests/init.test.js +69 -0
  190. package/dist/tests/install-script.test.js +246 -0
  191. package/dist/tests/integration/agent-real-profile.test.js +94 -0
  192. package/dist/tests/issue-36-repro.test.js +304 -0
  193. package/dist/tests/issues-191-194.test.js +160 -0
  194. package/dist/tests/lesson-lint.test.js +111 -0
  195. package/dist/tests/llm-client.test.js +115 -0
  196. package/dist/tests/llm-feature-gate.test.js +151 -0
  197. package/dist/tests/llm.test.js +139 -0
  198. package/dist/tests/lockfile.test.js +216 -0
  199. package/dist/tests/manifest.test.js +205 -0
  200. package/dist/tests/markdown.test.js +126 -0
  201. package/dist/tests/matchers-unit.test.js +189 -0
  202. package/dist/tests/memory-inference.test.js +299 -0
  203. package/dist/tests/merge-scoring.test.js +136 -0
  204. package/dist/tests/metadata.test.js +313 -0
  205. package/dist/tests/migration-help.test.js +89 -0
  206. package/dist/tests/origin-resolve.test.js +124 -0
  207. package/dist/tests/output-baseline.test.js +218 -0
  208. package/dist/tests/output-shapes-unit.test.js +478 -0
  209. package/dist/tests/parallel-search.test.js +272 -0
  210. package/dist/tests/parameter-metadata.test.js +365 -0
  211. package/dist/tests/paths.test.js +177 -0
  212. package/dist/tests/progressive-disclosure.test.js +280 -0
  213. package/dist/tests/proposals.test.js +279 -0
  214. package/dist/tests/proposed-quality.test.js +271 -0
  215. package/dist/tests/provider-registry.test.js +32 -0
  216. package/dist/tests/ranking-regression.test.js +548 -0
  217. package/dist/tests/reflect-propose.test.js +455 -0
  218. package/dist/tests/registry-build-index.test.js +394 -0
  219. package/dist/tests/registry-cli.test.js +290 -0
  220. package/dist/tests/registry-index-v2.test.js +430 -0
  221. package/dist/tests/registry-install.test.js +728 -0
  222. package/dist/tests/registry-providers/parity.test.js +189 -0
  223. package/dist/tests/registry-providers/skills-sh.test.js +309 -0
  224. package/dist/tests/registry-providers/static-index.test.js +238 -0
  225. package/dist/tests/registry-resolve.test.js +126 -0
  226. package/dist/tests/registry-search.test.js +923 -0
  227. package/dist/tests/remember-frontmatter.test.js +378 -0
  228. package/dist/tests/remember-unit.test.js +123 -0
  229. package/dist/tests/ripgrep-install.test.js +251 -0
  230. package/dist/tests/ripgrep-resolve.test.js +108 -0
  231. package/dist/tests/ripgrep.test.js +163 -0
  232. package/dist/tests/save-command.test.js +94 -0
  233. package/dist/tests/save-trust-qa-fixes.test.js +270 -0
  234. package/dist/tests/scoring-pipeline.test.js +648 -0
  235. package/dist/tests/search-include-proposed-cli.test.js +118 -0
  236. package/dist/tests/self-update.test.js +442 -0
  237. package/dist/tests/semantic-search-e2e.test.js +512 -0
  238. package/dist/tests/semantic-status.test.js +471 -0
  239. package/dist/tests/setup-run.integration.js +877 -0
  240. package/dist/tests/setup-wizard.test.js +198 -0
  241. package/dist/tests/setup.test.js +131 -0
  242. package/dist/tests/source-add.test.js +11 -0
  243. package/dist/tests/source-clone.test.js +254 -0
  244. package/dist/tests/source-manage.test.js +366 -0
  245. package/dist/tests/source-providers/filesystem.test.js +82 -0
  246. package/dist/tests/source-providers/git.test.js +252 -0
  247. package/dist/tests/source-providers/website.test.js +128 -0
  248. package/dist/tests/source-qa-fixes.test.js +286 -0
  249. package/dist/tests/source-registry.test.js +350 -0
  250. package/dist/tests/source-resolve.test.js +100 -0
  251. package/dist/tests/source-source.test.js +281 -0
  252. package/dist/tests/source.test.js +533 -0
  253. package/dist/tests/tar-utils-scan.test.js +73 -0
  254. package/dist/tests/toggle-components.test.js +73 -0
  255. package/dist/tests/usage-telemetry.test.js +265 -0
  256. package/dist/tests/utility-scoring.test.js +558 -0
  257. package/dist/tests/vault-load-error.test.js +78 -0
  258. package/dist/tests/vault-qa-fixes.test.js +194 -0
  259. package/dist/tests/vault.test.js +429 -0
  260. package/dist/tests/vector-search.test.js +608 -0
  261. package/dist/tests/walker.test.js +252 -0
  262. package/dist/tests/wave2-cluster-bc.test.js +228 -0
  263. package/dist/tests/wave2-cluster-d.test.js +180 -0
  264. package/dist/tests/wave2-cluster-e.test.js +179 -0
  265. package/dist/tests/wiki-qa-fixes.test.js +270 -0
  266. package/dist/tests/wiki.test.js +529 -0
  267. package/dist/tests/workflow-cli.test.js +271 -0
  268. package/dist/tests/workflow-markdown.test.js +171 -0
  269. package/dist/tests/workflow-path-escape.test.js +132 -0
  270. package/dist/tests/workflow-qa-fixes.test.js +395 -0
  271. package/dist/tests/workflows/indexer-rejection.test.js +213 -0
  272. package/docs/README.md +8 -0
  273. package/docs/migration/release-notes/0.7.0.md +244 -0
  274. package/package.json +2 -2
  275. package/dist/core/warn.js +0 -27
  276. package/dist/output/shapes.js +0 -212
  277. package/dist/output/text.js +0 -520
  278. /package/dist/{commands → src/commands}/completions.js +0 -0
  279. /package/dist/{commands → src/commands}/curate.js +0 -0
  280. /package/dist/{commands → src/commands}/info.js +0 -0
  281. /package/dist/{commands → src/commands}/init.js +0 -0
  282. /package/dist/{commands → src/commands}/install-audit.js +0 -0
  283. /package/dist/{commands → src/commands}/migration-help.js +0 -0
  284. /package/dist/{commands → src/commands}/source-clone.js +0 -0
  285. /package/dist/{commands → src/commands}/vault.js +0 -0
  286. /package/dist/{core → src/core}/asset-registry.js +0 -0
  287. /package/dist/{core → src/core}/frontmatter.js +0 -0
  288. /package/dist/{core → src/core}/markdown.js +0 -0
  289. /package/dist/{core → src/core}/paths.js +0 -0
  290. /package/dist/{indexer → src/indexer}/manifest.js +0 -0
  291. /package/dist/{indexer → src/indexer}/search-fields.js +0 -0
  292. /package/dist/{indexer → src/indexer}/semantic-status.js +0 -0
  293. /package/dist/{indexer → src/indexer}/usage-events.js +0 -0
  294. /package/dist/{indexer → src/indexer}/walker.js +0 -0
  295. /package/dist/{llm → src/llm}/embedder.js +0 -0
  296. /package/dist/{llm → src/llm}/embedders/cache.js +0 -0
  297. /package/dist/{llm → src/llm}/embedders/local.js +0 -0
  298. /package/dist/{llm → src/llm}/embedders/types.js +0 -0
  299. /package/dist/{llm → src/llm}/metadata-enhance.js +0 -0
  300. /package/dist/{output → src/output}/context.js +0 -0
  301. /package/dist/{registry → src/registry}/create-provider-registry.js +0 -0
  302. /package/dist/{registry → src/registry}/origin-resolve.js +0 -0
  303. /package/dist/{registry → src/registry}/providers/index.js +0 -0
  304. /package/dist/{registry → src/registry}/providers/skills-sh.js +0 -0
  305. /package/dist/{registry → src/registry}/providers/types.js +0 -0
  306. /package/dist/{registry → src/registry}/types.js +0 -0
  307. /package/dist/{setup → src/setup}/detect.js +0 -0
  308. /package/dist/{setup → src/setup}/ripgrep-install.js +0 -0
  309. /package/dist/{setup → src/setup}/ripgrep-resolve.js +0 -0
  310. /package/dist/{setup → src/setup}/steps.js +0 -0
  311. /package/dist/{sources → src/sources}/include.js +0 -0
  312. /package/dist/{sources → src/sources}/provider-factory.js +0 -0
  313. /package/dist/{sources → src/sources}/provider.js +0 -0
  314. /package/dist/{sources → src/sources}/providers/filesystem.js +0 -0
  315. /package/dist/{sources → src/sources}/providers/index.js +0 -0
  316. /package/dist/{sources → src/sources}/providers/install-types.js +0 -0
  317. /package/dist/{sources → src/sources}/providers/npm.js +0 -0
  318. /package/dist/{sources → src/sources}/providers/provider-utils.js +0 -0
  319. /package/dist/{sources → src/sources}/providers/sync-from-ref.js +0 -0
  320. /package/dist/{sources → src/sources}/providers/tar-utils.js +0 -0
  321. /package/dist/{sources → src/sources}/providers/website.js +0 -0
  322. /package/dist/{sources → src/sources}/resolve.js +0 -0
  323. /package/dist/{sources → src/sources}/types.js +0 -0
  324. /package/dist/{templates → src/templates}/wiki-templates.js +0 -0
  325. /package/dist/{version.js → src/version.js} +0 -0
  326. /package/dist/{workflows → src/workflows}/authoring.js +0 -0
  327. /package/dist/{workflows → src/workflows}/cli.js +0 -0
  328. /package/dist/{workflows → src/workflows}/db.js +0 -0
  329. /package/dist/{workflows → src/workflows}/document-cache.js +0 -0
  330. /package/dist/{workflows → src/workflows}/parser.js +0 -0
  331. /package/dist/{workflows → src/workflows}/renderer.js +0 -0
  332. /package/dist/{workflows → src/workflows}/schema.js +0 -0
  333. /package/dist/{workflows → src/workflows}/validator.js +0 -0
@@ -8,9 +8,9 @@
8
8
  * updated together with this builder.
9
9
  */
10
10
  import fs from "node:fs";
11
- import os from "node:os";
12
11
  import path from "node:path";
13
12
  import { fetchWithRetry, jsonWithByteCap } from "../core/common";
13
+ import { getCacheDir } from "../core/paths";
14
14
  import { generateMetadataFlat, loadStashFile } from "../indexer/metadata";
15
15
  import { walkStashFlat } from "../indexer/walker";
16
16
  import { asRecord, asString, GITHUB_API_BASE, githubHeaders } from "../integrations/github";
@@ -19,15 +19,16 @@ import { detectStashRoot } from "../sources/providers/provider-utils";
19
19
  import { extractTarGzSecure } from "../sources/providers/tar-utils";
20
20
  import { parseRegistryIndex } from "./providers/static-index";
21
21
  const DEFAULT_NPM_REGISTRY_BASE = "https://registry.npmjs.org";
22
- const DEFAULT_MANUAL_ENTRIES_PATH = path.resolve("manual-entries.json");
23
- const DEFAULT_OUTPUT_PATH = path.resolve("index.json");
24
22
  const REQUIRED_KEYWORDS = ["akm-stash"];
25
23
  const GITHUB_TOPICS = ["akm-stash"];
26
24
  const EXCLUDED_REPOS = new Set(["itlackey/akm"]);
27
25
  const EXCLUDED_NPM_PACKAGES = new Set(["akm-cli"]);
28
26
  const EMPTY_INSPECTION = {};
27
+ function getDefaultRegistryBuildDir() {
28
+ return path.join(getCacheDir(), "registry-build");
29
+ }
29
30
  export async function buildRegistryIndex(options) {
30
- const manualEntriesPath = path.resolve(options?.manualEntriesPath ?? DEFAULT_MANUAL_ENTRIES_PATH);
31
+ const manualEntriesPath = path.resolve(options?.manualEntriesPath ?? path.join(getDefaultRegistryBuildDir(), "manual-entries.json"));
31
32
  const npmRegistryBase = trimTrailingSlash(options?.npmRegistryBase ?? DEFAULT_NPM_REGISTRY_BASE);
32
33
  const githubApiBase = trimTrailingSlash(options?.githubApiBase ?? GITHUB_API_BASE);
33
34
  const [manualKits, npmKits, githubKits] = await Promise.all([
@@ -55,7 +56,7 @@ export async function buildRegistryIndex(options) {
55
56
  };
56
57
  }
57
58
  export function writeRegistryIndex(index, outPath) {
58
- const resolved = path.resolve(outPath ?? DEFAULT_OUTPUT_PATH);
59
+ const resolved = path.resolve(outPath ?? path.join(getDefaultRegistryBuildDir(), "index.json"));
59
60
  fs.mkdirSync(path.dirname(resolved), { recursive: true });
60
61
  fs.writeFileSync(resolved, `${JSON.stringify(index, null, 2)}\n`, "utf8");
61
62
  return resolved;
@@ -173,7 +174,16 @@ async function scanGithub(githubApiBase) {
173
174
  return stashes;
174
175
  }
175
176
  async function inspectArchive(url, headers) {
176
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-registry-build-"));
177
+ // Route registry-build scratch space under the akm cache dir instead of
178
+ // the system temp directory. Long-running registry builds that crash
179
+ // mid-flight previously left orphan dirs under `/tmp`, which can fill
180
+ // the OS partition. Pinning to `${getCacheDir()}/registry-build/` keeps
181
+ // a single `rm -rf` of that directory sufficient to reclaim the space
182
+ // and leaves the operator's `/tmp` untouched. See #276 for the
183
+ // bench-tmp precedent and #284 for this non-bench rollout.
184
+ const tmpRoot = path.join(getCacheDir(), "registry-build");
185
+ fs.mkdirSync(tmpRoot, { recursive: true });
186
+ const tempDir = fs.mkdtempSync(path.join(tmpRoot, "build-"));
177
187
  const archivePath = path.join(tempDir, "archive.tgz");
178
188
  const extractDir = path.join(tempDir, "extract");
179
189
  try {
@@ -283,7 +293,9 @@ async function loadManualEntries(manualEntriesPath) {
283
293
  const parsed = parseRegistryIndex({ version: 3, updatedAt: new Date().toISOString(), stashes: candidateKits });
284
294
  if (!parsed)
285
295
  return [];
286
- return parsed.stashes.map((stash) => normalizeStash({ ...stash, curated: stash.curated ?? true }));
296
+ // Legacy `curated` flag on input entries is ignored (v1 spec §4.2). The
297
+ // builder no longer emits it on the resulting index.
298
+ return parsed.stashes.map((stash) => normalizeStash(stash));
287
299
  }
288
300
  catch {
289
301
  return [];
@@ -327,7 +339,6 @@ function mergeEntries(a, b) {
327
339
  author: a.author ?? b.author,
328
340
  license: a.license ?? b.license,
329
341
  latestVersion: a.latestVersion ?? b.latestVersion,
330
- curated: a.curated || b.curated || undefined,
331
342
  });
332
343
  }
333
344
  function mergeAssets(a, b) {
@@ -23,11 +23,3 @@ export function registerProvider(type, factory) {
23
23
  export function resolveProviderFactory(type) {
24
24
  return registry.resolve(type);
25
25
  }
26
- /**
27
- * Iterate over all registered registry providers. Used by the orchestrator
28
- * (`src/commands/registry-search.ts`) to fan out queries through the same
29
- * `RegistryProvider` interface regardless of provider kind.
30
- */
31
- export function listProviderTypes() {
32
- return registry.list();
33
- }
@@ -190,7 +190,9 @@ export function parseRegistryIndex(data) {
190
190
  if (typeof data !== "object" || data === null || Array.isArray(data))
191
191
  return null;
192
192
  const obj = data;
193
- if (typeof obj.version !== "number" || obj.version !== 3)
193
+ // Accept version 2 and 3 both use the same stashes[] wire format.
194
+ // The live official registry currently publishes version 2.
195
+ if (typeof obj.version !== "number" || (obj.version !== 2 && obj.version !== 3))
194
196
  return null;
195
197
  if (typeof obj.updatedAt !== "string")
196
198
  return null;
@@ -213,6 +215,9 @@ function parseStashEntry(raw) {
213
215
  const source = asSource(obj.source);
214
216
  if (!id || !name || !ref || !source)
215
217
  return null;
218
+ // The legacy registry boolean `curated` is removed in v1. Legacy index JSON
219
+ // containing a `curated` key parses normally; the key is silently ignored
220
+ // (v1 spec §4.2, docs/migration/v1.md).
216
221
  return {
217
222
  id,
218
223
  name,
@@ -226,7 +231,6 @@ function parseStashEntry(raw) {
226
231
  author: asString(obj.author),
227
232
  license: asString(obj.license),
228
233
  latestVersion: asString(obj.latestVersion),
229
- curated: obj.curated === true ? true : undefined,
230
234
  };
231
235
  }
232
236
  // ── Scoring ─────────────────────────────────────────────────────────────────
@@ -294,7 +298,6 @@ function toSearchHit(stash, score, registryName) {
294
298
  homepage: stash.homepage,
295
299
  score: Math.round(score * 1000) / 1000,
296
300
  metadata,
297
- curated: stash.curated,
298
301
  registryName,
299
302
  };
300
303
  }
@@ -4,7 +4,7 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { fetchWithRetry, jsonWithByteCap } from "../core/common";
7
- import { UsageError } from "../core/errors";
7
+ import { NotFoundError, UsageError } from "../core/errors";
8
8
  import { asRecord, asString, GITHUB_API_BASE, githubHeaders } from "../integrations/github";
9
9
  /**
10
10
  * Validate that a URL is safe to pass to git.
@@ -200,7 +200,7 @@ function tryParseLocalRef(rawRef, explicitPath) {
200
200
  catch {
201
201
  // Explicit paths (./foo, ../bar, /abs) should throw on missing
202
202
  if (explicitPath) {
203
- throw new Error(`Local path not found: ${resolvedPath}`);
203
+ throw new NotFoundError(`Local path not found: ${resolvedPath}`, "FILE_NOT_FOUND", "Check the path exists and is readable.");
204
204
  }
205
205
  // Bare names that don't exist on disk — let caller fall through to npm/github
206
206
  return undefined;
@@ -231,6 +231,71 @@ function isPathLikeRef(ref) {
231
231
  }
232
232
  return ref.includes("/") || ref.includes("\\");
233
233
  }
234
+ /** Default public npm registry host. */
235
+ const DEFAULT_NPM_REGISTRY_HOST = "registry.npmjs.org";
236
+ /**
237
+ * Typed error raised when the npm registry returns a tarball URL on a host
238
+ * that is not the public registry or the operator-configured mirror. Carries
239
+ * a stable `.code` so callers (and JSON envelope output) can branch on it
240
+ * without parsing the message string.
241
+ */
242
+ export class UntrustedNpmTarballError extends Error {
243
+ code = "UNTRUSTED_NPM_TARBALL";
244
+ _hint;
245
+ constructor(msg, hint) {
246
+ super(msg);
247
+ this.name = "UntrustedNpmTarballError";
248
+ this._hint = hint;
249
+ Object.setPrototypeOf(this, new.target.prototype);
250
+ }
251
+ hint() {
252
+ return (this._hint ??
253
+ "Set AKM_NPM_REGISTRY to your private npm mirror's base URL if you install from a non-default registry.");
254
+ }
255
+ }
256
+ /**
257
+ * Resolve the set of npm registry hosts whose tarballs are considered trusted.
258
+ * Always includes the public npm registry, plus the host of an operator-set
259
+ * `AKM_NPM_REGISTRY` environment variable (if it parses to a valid URL).
260
+ */
261
+ export function trustedNpmTarballHosts() {
262
+ const hosts = new Set([DEFAULT_NPM_REGISTRY_HOST]);
263
+ const override = process.env.AKM_NPM_REGISTRY?.trim();
264
+ if (override) {
265
+ try {
266
+ const overrideHost = new URL(override).hostname.toLowerCase();
267
+ if (overrideHost)
268
+ hosts.add(overrideHost);
269
+ }
270
+ catch {
271
+ // Ignore unparseable overrides; the default host still applies.
272
+ }
273
+ }
274
+ return hosts;
275
+ }
276
+ /**
277
+ * Validate that an npm tarball URL points at a trusted registry host. A
278
+ * compromised mirror could otherwise return an attacker-controlled
279
+ * `dist.tarball` field, redirecting installs to a third-party host.
280
+ */
281
+ export function validateNpmTarballUrl(tarballUrl, packageRef) {
282
+ let url;
283
+ try {
284
+ url = new URL(tarballUrl);
285
+ }
286
+ catch {
287
+ throw new UntrustedNpmTarballError(`npm package ${packageRef} returned an invalid tarball URL: ${tarballUrl}`);
288
+ }
289
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
290
+ throw new UntrustedNpmTarballError(`npm package ${packageRef} returned a tarball with disallowed scheme "${url.protocol}".`);
291
+ }
292
+ const trusted = trustedNpmTarballHosts();
293
+ const host = url.hostname.toLowerCase();
294
+ if (!trusted.has(host)) {
295
+ const allowed = Array.from(trusted).join(", ");
296
+ throw new UntrustedNpmTarballError(`npm package ${packageRef} returned a tarball URL on untrusted host "${host}" (allowed: ${allowed}).`);
297
+ }
298
+ }
234
299
  async function resolveNpmArtifact(parsed) {
235
300
  const encodedName = encodeURIComponent(parsed.packageName);
236
301
  const metadata = await fetchJson(`https://registry.npmjs.org/${encodedName}`);
@@ -262,6 +327,7 @@ async function resolveNpmArtifact(parsed) {
262
327
  if (!tarballUrl) {
263
328
  throw new Error(`npm package ${parsed.packageName}@${resolvedVersion} does not expose a tarball URL.`);
264
329
  }
330
+ validateNpmTarballUrl(tarballUrl, `${parsed.packageName}@${resolvedVersion}`);
265
331
  const resolvedRevision = asString(dist.shasum) ?? asString(dist.integrity);
266
332
  return {
267
333
  id: parsed.id,
@@ -14,6 +14,7 @@ import { getDefaultStashDir } from "../core/paths";
14
14
  import { closeDatabase, isVecAvailable, openDatabase } from "../indexer/db";
15
15
  import { akmIndex } from "../indexer/indexer";
16
16
  import { clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus, } from "../indexer/semantic-status";
17
+ import { detectAgentCliProfiles, pickDefaultAgentProfile, } from "../integrations/agent";
17
18
  import { probeLlmCapabilities } from "../llm/client";
18
19
  import { checkEmbeddingAvailability, DEFAULT_LOCAL_MODEL, isTransformersAvailable } from "../llm/embedder";
19
20
  import { detectAgentPlatforms, detectOllama } from "./detect";
@@ -282,8 +283,10 @@ async function stepOllama(current) {
282
283
  mxbai: 1024,
283
284
  minilm: 384,
284
285
  bge: 384,
286
+ qwen3: 1024,
285
287
  };
286
288
  const guessedDim = Object.entries(knownDims).find(([k]) => embChoice.includes(k))?.[1] ?? 384;
289
+ p.note("Embedding dimension must match the model. Common values: 384 (BGE small), 768 (BGE base), 1024 (BGE large). Press Enter to accept the detected default.", "Embedding dimension");
287
290
  const dimChoice = await prompt(() => p.text({
288
291
  message: `Embedding dimension for ${embChoice}:`,
289
292
  placeholder: String(guessedDim),
@@ -300,6 +303,14 @@ async function stepOllama(current) {
300
303
  model: embChoice,
301
304
  dimension: Number(dimChoice),
302
305
  };
306
+ p.note([
307
+ "Recommended Qwen embedding models (modern, high context support):",
308
+ " • qwen3-embedding-0.6b — fast and lightweight (ollama pull qwen3-embedding-0.6b)",
309
+ " • qwen3-embedding-4b — higher quality (ollama pull qwen3-embedding-4b)",
310
+ "",
311
+ "For long documents (wiki pages, large files), set context length to avoid 400 errors:",
312
+ " akm config set embedding.contextLength 8192",
313
+ ].join("\n"), "Embedding tips");
303
314
  }
304
315
  // else: undefined → use built-in local
305
316
  // Surface Ollama details to the LLM step so it can offer Ollama as a preset.
@@ -313,7 +324,6 @@ const LLM_PRESETS = [
313
324
  endpoint: "https://api.anthropic.com/v1/chat/completions",
314
325
  defaultModel: "claude-sonnet-4-5",
315
326
  hint: "beta OpenAI-compat layer; set AKM_LLM_API_KEY; override the model if the default is unavailable",
316
- contextWindow: 200_000,
317
327
  },
318
328
  {
319
329
  value: "openai",
@@ -321,7 +331,6 @@ const LLM_PRESETS = [
321
331
  endpoint: "https://api.openai.com/v1/chat/completions",
322
332
  defaultModel: "gpt-4o-mini",
323
333
  hint: "AKM_LLM_API_KEY required",
324
- contextWindow: 128_000,
325
334
  },
326
335
  {
327
336
  value: "google",
@@ -329,7 +338,6 @@ const LLM_PRESETS = [
329
338
  endpoint: "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
330
339
  defaultModel: "gemini-2.0-flash",
331
340
  hint: "OpenAI-compat endpoint, AKM_LLM_API_KEY required",
332
- contextWindow: 1_000_000,
333
341
  },
334
342
  ];
335
343
  /**
@@ -431,7 +439,6 @@ export async function stepLlm(current, ollamaEndpoint, ollamaChatModels) {
431
439
  model: model.trim() || preset.defaultModel,
432
440
  temperature: 0.3,
433
441
  maxTokens: 1024,
434
- contextWindow: preset.contextWindow,
435
442
  };
436
443
  }
437
444
  // Remind the user about API key placement. We do not offer a "store in config"
@@ -649,6 +656,29 @@ async function stepAgentPlatforms(current) {
649
656
  }
650
657
  return entries;
651
658
  }
659
+ /**
660
+ * Detect installed agent CLIs and produce an updated `agent` config block
661
+ * with a sensible `default` (the first detected profile that the user has
662
+ * not already overridden).
663
+ *
664
+ * Pure-ish: file system / PATH probes are routed through `detectFn` so
665
+ * tests can drive the branches without touching the real PATH.
666
+ *
667
+ * @internal Exported for testing only.
668
+ */
669
+ export function stepAgentCliDetection(current, detectFn = detectAgentCliProfiles) {
670
+ const detections = detectFn(current.agent);
671
+ const defaultName = pickDefaultAgentProfile(detections, current.agent?.default);
672
+ // No installed agents found and no existing config → leave block absent.
673
+ if (!defaultName && !current.agent) {
674
+ return { detections };
675
+ }
676
+ const agent = {
677
+ ...(current.agent ?? {}),
678
+ ...(defaultName ? { default: defaultName } : {}),
679
+ };
680
+ return { agent, detections };
681
+ }
652
682
  // ── Main Wizard ─────────────────────────────────────────────────────────────
653
683
  /**
654
684
  * Build the canonical list of `SetupStep`s for the interactive wizard.
@@ -731,6 +761,23 @@ export function buildSetupSteps(options) {
731
761
  ctx.apply({ sources: merged.length > 0 ? merged : undefined });
732
762
  },
733
763
  },
764
+ {
765
+ id: "agent-cli",
766
+ label: "Agent CLI",
767
+ nonInteractive: true,
768
+ async run(ctx) {
769
+ const result = stepAgentCliDetection(ctx.config);
770
+ const detected = result.detections.filter((d) => d.available);
771
+ if (detected.length > 0) {
772
+ p.log.info(`Detected agent CLIs: ${detected.map((d) => d.name).join(", ")}.` +
773
+ (result.agent?.default ? ` Default profile: ${result.agent.default}.` : ""));
774
+ }
775
+ else {
776
+ p.log.info("No agent CLIs detected on PATH. Agent commands will be disabled until one is installed and `akm setup` is re-run.");
777
+ }
778
+ ctx.apply({ agent: result.agent });
779
+ },
780
+ },
734
781
  ];
735
782
  return { steps, outcome };
736
783
  }
@@ -771,7 +818,7 @@ export async function runSetupWizard() {
771
818
  const embedding = newConfig.embedding;
772
819
  const llm = newConfig.llm;
773
820
  const registries = newConfig.registries;
774
- const allStashes = newConfig.stashes ?? [];
821
+ const allStashes = newConfig.sources ?? newConfig.stashes ?? [];
775
822
  // Confirm before saving
776
823
  const effectiveRegistries = registries ?? DEFAULT_CONFIG.registries ?? [];
777
824
  p.note([
@@ -6,6 +6,7 @@ import { resolveStashDir } from "../../core/common";
6
6
  import { loadConfig } from "../../core/config";
7
7
  import { ConfigError, UsageError } from "../../core/errors";
8
8
  import { getRegistryCacheDir, getRegistryIndexCacheDir } from "../../core/paths";
9
+ import { sanitizeCommitMessage } from "../../core/write-source";
9
10
  import { parseRegistryRef, resolveRegistryArtifact, validateGitRef, validateGitUrl } from "../../registry/resolve";
10
11
  import { registerSourceProvider } from "../provider-factory";
11
12
  import { applyAkmIncludeConfig, buildInstallCacheDir, copyDirectoryContents, detectStashRoot, isDirectory, isExpired, sanitizeString, } from "./provider-utils";
@@ -77,20 +78,6 @@ function getCachePaths(repoUrl) {
77
78
  const key = createHash("sha256").update(repoUrl).digest("hex").slice(0, 16);
78
79
  const cacheRoot = getRegistryIndexCacheDir();
79
80
  const rootDir = path.join(cacheRoot, `git-${key}`);
80
- // One-time silent migration: legacy `context-hub-${key}` directories were
81
- // created for ALL git stashes (not just the andrewyng/context-hub repo). If
82
- // the new path doesn't yet exist but the legacy one does, rename it in place
83
- // so existing clones aren't silently invalidated. Failures are non-fatal —
84
- // worst case the repo is re-cloned on the next refresh.
85
- try {
86
- const legacyRootDir = path.join(cacheRoot, `context-hub-${key}`);
87
- if (!fs.existsSync(rootDir) && fs.existsSync(legacyRootDir)) {
88
- fs.renameSync(legacyRootDir, rootDir);
89
- }
90
- }
91
- catch {
92
- /* migration is best-effort */
93
- }
94
81
  return {
95
82
  rootDir,
96
83
  repoDir: path.join(rootDir, "repo"),
@@ -398,7 +385,12 @@ function parseGitRepoUrl(rawUrl) {
398
385
  */
399
386
  export function saveGitStash(name, message, writableOverride) {
400
387
  const timestamp = new Date().toISOString().replace("T", " ").slice(0, 19);
401
- const commitMessage = message?.trim() || `akm save ${timestamp}`;
388
+ // Sanitize the user-supplied message: strip CR/LF/NUL, collapse whitespace,
389
+ // clamp length. An attacker can otherwise pass `--message "subject\n\n\
390
+ // Co-Authored-By: someone-else"` and forge trailers in the commit log.
391
+ // Empty result falls back to the timestamped default.
392
+ const sanitized = message ? sanitizeCommitMessage(message) : "";
393
+ const commitMessage = sanitized || `akm save ${timestamp}`;
402
394
  let repoDir;
403
395
  let writable = false;
404
396
  if (name) {
@@ -11,6 +11,32 @@
11
11
  * Principle: "akm surfaces. The agent writes." akm owns lifecycle, raw-slug
12
12
  * generation, structural lint, and `index.md` regeneration. The agent uses
13
13
  * its native file tools for every other page operation.
14
+ *
15
+ * ## Canonical wiki content contract
16
+ *
17
+ * The three "infrastructure" files at the wiki root — `schema.md`, `index.md`,
18
+ * and `log.md` — are excluded from all user-facing content surfaces:
19
+ *
20
+ * | Surface | schema/index/log | raw/<slug>.md | <page>.md |
21
+ * | -------------------- | ---------------- | ------------- | --------- |
22
+ * | `wiki pages` | excluded | included | included |
23
+ * | scoped wiki search | excluded | included | included |
24
+ * | stash-wide FTS index | excluded | included | included |
25
+ * | `wiki lint` | excluded | tracked | tracked |
26
+ *
27
+ * `raw/` files are first-class addressable content (`wiki:<n>/raw/<slug>`),
28
+ * searchable, and listed. They are NOT authored pages — they are source
29
+ * material the agent turns into pages. `lint` tracks whether each raw file
30
+ * has been cited by a page's `sources:` frontmatter field.
31
+ *
32
+ * ## Regeneration contract
33
+ *
34
+ * `regenerateWikiIndex` / `regenerateAllWikiIndexes` apply ONLY to
35
+ * stash-owned wikis (directories under `<stashDir>/wikis/`). External wikis
36
+ * registered via `akm wiki register` are read-only caches; mutating their
37
+ * `index.md` would corrupt source-of-truth content that akm does not own.
38
+ * The indexer therefore calls `regenerateAllWikiIndexes(stashDir)` — which
39
+ * only iterates `<stashDir>/wikis/` — and never touches registered sources.
14
40
  */
15
41
  import fs from "node:fs";
16
42
  import path from "node:path";
@@ -496,10 +522,15 @@ function readPageFrontmatter(absPath) {
496
522
  return out;
497
523
  }
498
524
  /**
499
- * List the addressable markdown entries in a wiki, excluding only the
500
- * infrastructure files `schema.md`, `index.md`, and `log.md`. This includes
501
- * both authored pages and `raw/` sources so `wiki pages` can inventory content
502
- * written via `akm wiki stash`.
525
+ * List all addressable wiki content entries.
526
+ *
527
+ * Per the canonical wiki contract: `schema.md`, `index.md`, and `log.md` at
528
+ * the wiki root are infrastructure files and are excluded. Everything else —
529
+ * authored pages AND `raw/<slug>.md` sources — is included and addressable as
530
+ * `wiki:<name>/<rel-path-without-.md>`.
531
+ *
532
+ * Callers that need to distinguish authored pages from raw sources should
533
+ * check whether the returned `name` starts with `"raw/"`.
503
534
  */
504
535
  export function listPages(stashDir, name) {
505
536
  const wikiDir = resolveWikiSource(stashDir, name).path;
@@ -521,6 +552,10 @@ export function listPages(stashDir, name) {
521
552
  * Uses `akmSearch({ type: "wiki" })` to reuse the full FTS5+boost pipeline,
522
553
  * then drops hits that aren't inside `wikis/<name>/`. No parallel scorer.
523
554
  *
555
+ * Per the canonical wiki contract: infrastructure files (`schema.md`,
556
+ * `index.md`, `log.md`) at the wiki root are excluded. `raw/<slug>.md`
557
+ * sources are included — they are first-class addressable content.
558
+ *
524
559
  * When the index is absent (e.g. fresh stash), `akmSearch` falls back to its
525
560
  * substring walker; hits still come through path-filtered here.
526
561
  */
@@ -795,7 +830,13 @@ export function lintWiki(stashDir, name) {
795
830
  }
796
831
  // ── Index regeneration ─────────────────────────────────────────────────────
797
832
  /**
798
- * Rebuild a wiki's `index.md` from its pages' frontmatter.
833
+ * Rebuild a stash-owned wiki's `index.md` from its pages' frontmatter.
834
+ *
835
+ * This function uses `resolveWikiDir` (not `resolveWikiSource`) so it only
836
+ * ever operates on the stash-owned path `<stashDir>/wikis/<name>/`. External
837
+ * wikis registered via `akm wiki register` are never regenerated here — they
838
+ * are read-only caches. See the canonical wiki contract at the top of this
839
+ * file for the full regeneration rule.
799
840
  *
800
841
  * Pages are grouped by `pageKind` (falling back to `uncategorised`) and
801
842
  * listed alphabetically inside each group. If the wiki directory doesn't
@@ -861,7 +902,14 @@ export function regenerateWikiIndex(stashDir, name) {
861
902
  }
862
903
  }
863
904
  /**
864
- * Regenerate `index.md` for every wiki found under `<stashDir>/wikis/`.
905
+ * Regenerate `index.md` for every stash-owned wiki under `<stashDir>/wikis/`.
906
+ *
907
+ * Per the canonical wiki contract: regeneration applies ONLY to stash-owned
908
+ * wikis. External wikis registered via `akm wiki register` are read-only
909
+ * caches whose source-of-truth lives outside this stash; mutating their
910
+ * `index.md` would corrupt content that akm does not own. Those wikis
911
+ * therefore appear only in the FTS index (read), never in regeneration
912
+ * (write).
865
913
  *
866
914
  * Called from `akmIndex()` as a side effect after the FTS rebuild. Never
867
915
  * throws; returns the list of wiki names that were regenerated.
@@ -3,6 +3,7 @@ import fs from "node:fs";
3
3
  import { parseAssetRef } from "../core/asset-ref";
4
4
  import { loadConfig } from "../core/config";
5
5
  import { NotFoundError, UsageError } from "../core/errors";
6
+ import { appendEvent } from "../core/events";
6
7
  import { getDbPath } from "../core/paths";
7
8
  import { closeDatabase, openDatabase } from "../indexer/db";
8
9
  import { resolveSourceEntries } from "../indexer/search-source";
@@ -32,7 +33,13 @@ export async function startWorkflowRun(ref, params = {}) {
32
33
  insertStep.run(runId, step.id, step.title, step.instructions, step.completionCriteria ? JSON.stringify(step.completionCriteria) : null, step.sequenceIndex ?? 0);
33
34
  }
34
35
  })();
35
- return getWorkflowStatus(runId);
36
+ const result = getWorkflowStatus(runId);
37
+ appendEvent({
38
+ eventType: "workflow_started",
39
+ ref: ref,
40
+ metadata: { runId: result.run.id, title: result.run.workflowTitle },
41
+ });
42
+ return result;
36
43
  }
37
44
  finally {
38
45
  closeWorkflowDatabase(workflowDb);
@@ -173,7 +180,16 @@ export function completeWorkflowStep(input) {
173
180
  completed_at: state.completedAt,
174
181
  };
175
182
  })();
176
- return buildWorkflowRunDetail(updatedRun, refreshedSteps);
183
+ const detail = buildWorkflowRunDetail(updatedRun, refreshedSteps);
184
+ appendEvent({
185
+ eventType: "workflow_step_completed",
186
+ ref: detail.run.workflowRef,
187
+ metadata: { runId: input.runId, stepId: input.stepId, notes: input.notes },
188
+ });
189
+ if (detail.run.status === "completed") {
190
+ appendEvent({ eventType: "workflow_finished", ref: detail.run.workflowRef, metadata: { runId: input.runId } });
191
+ }
192
+ return detail;
177
193
  }
178
194
  finally {
179
195
  closeWorkflowDatabase(workflowDb);
@@ -187,6 +203,9 @@ async function resolveRunSpecifier(db, specifier, params) {
187
203
  }
188
204
  return { run: explicitRun, autoStarted: false };
189
205
  }
206
+ if (!specifier.includes(":")) {
207
+ throw new NotFoundError(`Workflow run "${specifier}" not found.`, "WORKFLOW_NOT_FOUND");
208
+ }
190
209
  const parsed = parseAssetRef(specifier);
191
210
  if (parsed.type !== "workflow") {
192
211
  throw new UsageError(`Expected a workflow ref or workflow run id, got "${specifier}".`);
@@ -316,7 +335,7 @@ function resolveWorkflowEntryId(sourcePath, ref) {
316
335
  function readWorkflowRun(db, runId) {
317
336
  const run = db.prepare("SELECT * FROM workflow_runs WHERE id = ?").get(runId);
318
337
  if (!run) {
319
- throw new NotFoundError(`Workflow run not found: ${runId}`);
338
+ throw new NotFoundError(`Workflow run "${runId}" not found.`, "WORKFLOW_NOT_FOUND");
320
339
  }
321
340
  return run;
322
341
  }
@@ -416,3 +435,18 @@ function parseJsonArray(value) {
416
435
  }
417
436
  return undefined;
418
437
  }
438
+ export function getActiveWorkflowRun() {
439
+ try {
440
+ const workflowDb = openWorkflowDatabase();
441
+ const row = workflowDb
442
+ .query("SELECT id, current_step_id, workflow_ref FROM workflow_runs WHERE status IN ('active', 'blocked') ORDER BY updated_at DESC LIMIT 1")
443
+ .get();
444
+ closeWorkflowDatabase(workflowDb);
445
+ if (!row)
446
+ return null;
447
+ return { runId: row.id, stepId: row.current_step_id, workflowRef: row.workflow_ref };
448
+ }
449
+ catch {
450
+ return null; // fail-open: never crash show output due to DB error
451
+ }
452
+ }