akm-cli 0.6.1 → 0.7.0-rc1

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 (319) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/{cli.js → src/cli.js} +620 -26
  3. package/dist/{commands → src/commands}/config-cli.js +5 -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 +120 -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 +2 -1
  12. package/dist/{commands → src/commands}/remember.js +12 -0
  13. package/dist/{commands → src/commands}/search.js +74 -1
  14. package/dist/{commands → src/commands}/self-update.js +4 -3
  15. package/dist/{commands → src/commands}/show.js +44 -0
  16. package/dist/{core → src/core}/asset-ref.js +5 -5
  17. package/dist/{core → src/core}/asset-spec.js +12 -0
  18. package/dist/{core → src/core}/common.js +1 -1
  19. package/dist/{core → src/core}/config.js +175 -121
  20. package/dist/{core → src/core}/errors.js +4 -0
  21. package/dist/src/core/events.js +239 -0
  22. package/dist/src/core/lesson-lint.js +86 -0
  23. package/dist/src/core/proposals.js +406 -0
  24. package/dist/src/core/warn.js +72 -0
  25. package/dist/{core → src/core}/write-source.js +80 -5
  26. package/dist/{indexer → src/indexer}/db-search.js +113 -24
  27. package/dist/{indexer → src/indexer}/db.js +76 -23
  28. package/dist/{indexer → src/indexer}/file-context.js +0 -3
  29. package/dist/src/indexer/graph-boost.js +179 -0
  30. package/dist/src/indexer/graph-extraction.js +212 -0
  31. package/dist/{indexer → src/indexer}/indexer.js +73 -6
  32. package/dist/src/indexer/memory-inference.js +263 -0
  33. package/dist/{indexer → src/indexer}/metadata.js +111 -3
  34. package/dist/src/integrations/agent/config.js +292 -0
  35. package/dist/src/integrations/agent/detect.js +94 -0
  36. package/dist/src/integrations/agent/index.js +17 -0
  37. package/dist/src/integrations/agent/profiles.js +65 -0
  38. package/dist/src/integrations/agent/prompts.js +167 -0
  39. package/dist/src/integrations/agent/spawn.js +221 -0
  40. package/dist/{integrations → src/integrations}/lockfile.js +0 -26
  41. package/dist/{llm → src/llm}/client.js +33 -2
  42. package/dist/src/llm/feature-gate.js +108 -0
  43. package/dist/src/llm/graph-extract.js +107 -0
  44. package/dist/src/llm/index-passes.js +35 -0
  45. package/dist/src/llm/memory-infer.js +86 -0
  46. package/dist/{output → src/output}/renderers.js +60 -1
  47. package/dist/src/output/shapes.js +516 -0
  48. package/dist/{output → src/output}/text.js +447 -4
  49. package/dist/{registry → src/registry}/build-index.js +14 -4
  50. package/dist/{registry → src/registry}/factory.js +0 -8
  51. package/dist/{registry → src/registry}/providers/static-index.js +3 -2
  52. package/dist/{registry → src/registry}/resolve.js +68 -2
  53. package/dist/{setup → src/setup}/setup.js +43 -5
  54. package/dist/{sources → src/sources}/providers/git.js +7 -15
  55. package/dist/tests/add-website-source.test.js +119 -0
  56. package/dist/tests/agent/agent-config-loader.test.js +70 -0
  57. package/dist/tests/agent/agent-config.test.js +221 -0
  58. package/dist/tests/agent/agent-detect.test.js +100 -0
  59. package/dist/tests/agent/agent-spawn.test.js +234 -0
  60. package/dist/tests/agent-output.test.js +186 -0
  61. package/dist/tests/architecture/agent-no-llm-sdk-guard.test.js +103 -0
  62. package/dist/tests/architecture/agent-spawn-seam.test.js +193 -0
  63. package/dist/tests/architecture/llm-stateless-seam.test.js +112 -0
  64. package/dist/tests/asset-ref.test.js +192 -0
  65. package/dist/tests/asset-registry.test.js +103 -0
  66. package/dist/tests/asset-spec.test.js +241 -0
  67. package/dist/tests/bench/attribution.test.js +995 -0
  68. package/dist/tests/bench/cleanup-sigint.test.js +83 -0
  69. package/dist/tests/bench/cleanup.js +203 -0
  70. package/dist/tests/bench/cleanup.test.js +166 -0
  71. package/dist/tests/bench/cli.js +683 -0
  72. package/dist/tests/bench/cli.test.js +177 -0
  73. package/dist/tests/bench/compare.test.js +556 -0
  74. package/dist/tests/bench/corpus.js +314 -0
  75. package/dist/tests/bench/corpus.test.js +258 -0
  76. package/dist/tests/bench/driver.js +346 -0
  77. package/dist/tests/bench/driver.test.js +443 -0
  78. package/dist/tests/bench/evolve-metrics.js +179 -0
  79. package/dist/tests/bench/evolve-metrics.test.js +187 -0
  80. package/dist/tests/bench/evolve.js +580 -0
  81. package/dist/tests/bench/evolve.test.js +616 -0
  82. package/dist/tests/bench/failure-modes.test.js +300 -0
  83. package/dist/tests/bench/feedback-integrity.test.js +456 -0
  84. package/dist/tests/bench/leakage.test.js +125 -0
  85. package/dist/tests/bench/learning-curve.test.js +133 -0
  86. package/dist/tests/bench/metrics.js +2319 -0
  87. package/dist/tests/bench/metrics.test.js +1144 -0
  88. package/dist/tests/bench/no-os-tmpdir-invariant.test.js +43 -0
  89. package/dist/tests/bench/report.js +1821 -0
  90. package/dist/tests/bench/report.test.js +989 -0
  91. package/dist/tests/bench/runner.js +536 -0
  92. package/dist/tests/bench/runner.test.js +958 -0
  93. package/dist/tests/bench/search-bridge.test.js +331 -0
  94. package/dist/tests/bench/tmp.js +41 -0
  95. package/dist/tests/bench/trajectory.js +116 -0
  96. package/dist/tests/bench/trajectory.test.js +127 -0
  97. package/dist/tests/bench/verifier.js +109 -0
  98. package/dist/tests/bench/verifier.test.js +118 -0
  99. package/dist/tests/bench/workflow-evaluator.js +557 -0
  100. package/dist/tests/bench/workflow-evaluator.test.js +421 -0
  101. package/dist/tests/bench/workflow-spec.js +358 -0
  102. package/dist/tests/bench/workflow-spec.test.js +363 -0
  103. package/dist/tests/bench/workflow-trace.js +438 -0
  104. package/dist/tests/bench/workflow-trace.test.js +254 -0
  105. package/dist/tests/benchmark-search-quality.js +536 -0
  106. package/dist/tests/benchmark-suite.js +1441 -0
  107. package/dist/tests/capture-cli.test.js +112 -0
  108. package/dist/tests/cli-errors.test.js +203 -0
  109. package/dist/tests/commands/events.test.js +370 -0
  110. package/dist/tests/commands/history.test.js +223 -0
  111. package/dist/tests/commands/import.test.js +103 -0
  112. package/dist/tests/commands/proposal-cli.test.js +209 -0
  113. package/dist/tests/commands/reflect-propose-cli.test.js +333 -0
  114. package/dist/tests/commands/remember.test.js +97 -0
  115. package/dist/tests/commands/scope-flags.test.js +300 -0
  116. package/dist/tests/commands/search.test.js +537 -0
  117. package/dist/tests/commands/show-indexer-parity.test.js +117 -0
  118. package/dist/tests/commands/show.test.js +294 -0
  119. package/dist/tests/common.test.js +266 -0
  120. package/dist/tests/completions.test.js +142 -0
  121. package/dist/tests/config-cli.test.js +193 -0
  122. package/dist/tests/config-llm-features.test.js +139 -0
  123. package/dist/tests/config.test.js +544 -0
  124. package/dist/tests/contracts/migration-baseline.test.js +43 -0
  125. package/dist/tests/contracts/reflect-propose-envelope.test.js +139 -0
  126. package/dist/tests/contracts/spec-helpers.js +46 -0
  127. package/dist/tests/contracts/v1-spec-section-11-proposal-queue.test.js +228 -0
  128. package/dist/tests/contracts/v1-spec-section-12-agent-config.test.js +56 -0
  129. package/dist/tests/contracts/v1-spec-section-13-lesson-type.test.js +34 -0
  130. package/dist/tests/contracts/v1-spec-section-14-llm-features.test.js +94 -0
  131. package/dist/tests/contracts/v1-spec-section-4-1-asset-types.test.js +39 -0
  132. package/dist/tests/contracts/v1-spec-section-4-2-quality-rules.test.js +44 -0
  133. package/dist/tests/contracts/v1-spec-section-5-configuration.test.js +47 -0
  134. package/dist/tests/contracts/v1-spec-section-6-orchestration.test.js +40 -0
  135. package/dist/tests/contracts/v1-spec-section-7-module-layout.test.js +58 -0
  136. package/dist/tests/contracts/v1-spec-section-8-extension-points.test.js +34 -0
  137. package/dist/tests/contracts/v1-spec-section-9-4-cli-surface.test.js +75 -0
  138. package/dist/tests/contracts/v1-spec-section-9-7-llm-agent-boundary.test.js +36 -0
  139. package/dist/tests/core/write-source.test.js +366 -0
  140. package/dist/tests/curate-command.test.js +87 -0
  141. package/dist/tests/db-scoring.test.js +201 -0
  142. package/dist/tests/db.test.js +654 -0
  143. package/dist/tests/distill-cli-flag.test.js +208 -0
  144. package/dist/tests/distill.test.js +515 -0
  145. package/dist/tests/docker-install.test.js +120 -0
  146. package/dist/tests/e2e.test.js +1398 -0
  147. package/dist/tests/embedder.test.js +340 -0
  148. package/dist/tests/embedding-model-config.test.js +379 -0
  149. package/dist/tests/feedback-command.test.js +172 -0
  150. package/dist/tests/file-context.test.js +552 -0
  151. package/dist/tests/fixtures/scripts/git/summarize-diff.js +9 -0
  152. package/dist/tests/fixtures/scripts/lint/eslint-check.js +7 -0
  153. package/dist/tests/fixtures/stashes/load.js +166 -0
  154. package/dist/tests/fixtures/stashes/load.test.js +88 -0
  155. package/dist/tests/fixtures/stashes/ranking-baseline/scripts/mem0-search.js +12 -0
  156. package/dist/tests/frontmatter.test.js +190 -0
  157. package/dist/tests/fts-field-weighting.test.js +254 -0
  158. package/dist/tests/fuzzy-search.test.js +230 -0
  159. package/dist/tests/git-provider-clone.test.js +45 -0
  160. package/dist/tests/github.test.js +161 -0
  161. package/dist/tests/graph-boost-ranking.test.js +305 -0
  162. package/dist/tests/graph-extraction.test.js +282 -0
  163. package/dist/tests/helpers/usage-events.js +8 -0
  164. package/dist/tests/index-pass-llm.test.js +161 -0
  165. package/dist/tests/indexer.test.js +559 -0
  166. package/dist/tests/info-command.test.js +166 -0
  167. package/dist/tests/init.test.js +69 -0
  168. package/dist/tests/install-script.test.js +246 -0
  169. package/dist/tests/integration/agent-real-profile.test.js +94 -0
  170. package/dist/tests/issue-36-repro.test.js +304 -0
  171. package/dist/tests/issues-191-194.test.js +160 -0
  172. package/dist/tests/lesson-lint.test.js +111 -0
  173. package/dist/tests/llm-client.test.js +115 -0
  174. package/dist/tests/llm-feature-gate.test.js +151 -0
  175. package/dist/tests/llm.test.js +139 -0
  176. package/dist/tests/lockfile.test.js +216 -0
  177. package/dist/tests/manifest.test.js +205 -0
  178. package/dist/tests/markdown.test.js +126 -0
  179. package/dist/tests/matchers-unit.test.js +189 -0
  180. package/dist/tests/memory-inference.test.js +299 -0
  181. package/dist/tests/merge-scoring.test.js +136 -0
  182. package/dist/tests/metadata.test.js +313 -0
  183. package/dist/tests/migration-help.test.js +89 -0
  184. package/dist/tests/origin-resolve.test.js +124 -0
  185. package/dist/tests/output-baseline.test.js +217 -0
  186. package/dist/tests/output-shapes-unit.test.js +476 -0
  187. package/dist/tests/parallel-search.test.js +272 -0
  188. package/dist/tests/parameter-metadata.test.js +365 -0
  189. package/dist/tests/paths.test.js +177 -0
  190. package/dist/tests/progressive-disclosure.test.js +280 -0
  191. package/dist/tests/proposals.test.js +279 -0
  192. package/dist/tests/proposed-quality.test.js +271 -0
  193. package/dist/tests/provider-registry.test.js +32 -0
  194. package/dist/tests/ranking-regression.test.js +548 -0
  195. package/dist/tests/reflect-propose.test.js +455 -0
  196. package/dist/tests/registry-build-index.test.js +378 -0
  197. package/dist/tests/registry-cli.test.js +290 -0
  198. package/dist/tests/registry-index-v2.test.js +430 -0
  199. package/dist/tests/registry-install.test.js +728 -0
  200. package/dist/tests/registry-providers/parity.test.js +189 -0
  201. package/dist/tests/registry-providers/skills-sh.test.js +309 -0
  202. package/dist/tests/registry-providers/static-index.test.js +204 -0
  203. package/dist/tests/registry-resolve.test.js +126 -0
  204. package/dist/tests/registry-search.test.js +723 -0
  205. package/dist/tests/remember-frontmatter.test.js +380 -0
  206. package/dist/tests/remember-unit.test.js +123 -0
  207. package/dist/tests/ripgrep-install.test.js +251 -0
  208. package/dist/tests/ripgrep-resolve.test.js +108 -0
  209. package/dist/tests/ripgrep.test.js +163 -0
  210. package/dist/tests/save-command.test.js +94 -0
  211. package/dist/tests/save-trust-qa-fixes.test.js +270 -0
  212. package/dist/tests/scoring-pipeline.test.js +648 -0
  213. package/dist/tests/search-include-proposed-cli.test.js +118 -0
  214. package/dist/tests/self-update.test.js +442 -0
  215. package/dist/tests/semantic-search-e2e.test.js +512 -0
  216. package/dist/tests/semantic-status.test.js +471 -0
  217. package/dist/tests/setup-run.integration.js +877 -0
  218. package/dist/tests/setup-wizard.test.js +198 -0
  219. package/dist/tests/setup.test.js +131 -0
  220. package/dist/tests/source-add.test.js +11 -0
  221. package/dist/tests/source-clone.test.js +254 -0
  222. package/dist/tests/source-manage.test.js +366 -0
  223. package/dist/tests/source-providers/filesystem.test.js +82 -0
  224. package/dist/tests/source-providers/git.test.js +252 -0
  225. package/dist/tests/source-providers/website.test.js +128 -0
  226. package/dist/tests/source-qa-fixes.test.js +268 -0
  227. package/dist/tests/source-registry.test.js +350 -0
  228. package/dist/tests/source-resolve.test.js +100 -0
  229. package/dist/tests/source-source.test.js +221 -0
  230. package/dist/tests/source.test.js +533 -0
  231. package/dist/tests/tar-utils-scan.test.js +73 -0
  232. package/dist/tests/toggle-components.test.js +73 -0
  233. package/dist/tests/usage-telemetry.test.js +265 -0
  234. package/dist/tests/utility-scoring.test.js +558 -0
  235. package/dist/tests/vault-load-error.test.js +78 -0
  236. package/dist/tests/vault-qa-fixes.test.js +194 -0
  237. package/dist/tests/vault.test.js +429 -0
  238. package/dist/tests/vector-search.test.js +608 -0
  239. package/dist/tests/walker.test.js +252 -0
  240. package/dist/tests/wave2-cluster-bc.test.js +228 -0
  241. package/dist/tests/wave2-cluster-d.test.js +180 -0
  242. package/dist/tests/wave2-cluster-e.test.js +179 -0
  243. package/dist/tests/wiki-qa-fixes.test.js +270 -0
  244. package/dist/tests/wiki.test.js +529 -0
  245. package/dist/tests/workflow-cli.test.js +271 -0
  246. package/dist/tests/workflow-markdown.test.js +171 -0
  247. package/dist/tests/workflow-path-escape.test.js +132 -0
  248. package/dist/tests/workflow-qa-fixes.test.js +377 -0
  249. package/dist/tests/workflows/indexer-rejection.test.js +213 -0
  250. package/docs/README.md +8 -0
  251. package/docs/migration/release-notes/0.7.0.md +244 -0
  252. package/package.json +2 -2
  253. package/dist/core/warn.js +0 -27
  254. package/dist/output/shapes.js +0 -212
  255. /package/dist/{commands → src/commands}/completions.js +0 -0
  256. /package/dist/{commands → src/commands}/curate.js +0 -0
  257. /package/dist/{commands → src/commands}/info.js +0 -0
  258. /package/dist/{commands → src/commands}/init.js +0 -0
  259. /package/dist/{commands → src/commands}/install-audit.js +0 -0
  260. /package/dist/{commands → src/commands}/migration-help.js +0 -0
  261. /package/dist/{commands → src/commands}/source-add.js +0 -0
  262. /package/dist/{commands → src/commands}/source-clone.js +0 -0
  263. /package/dist/{commands → src/commands}/source-manage.js +0 -0
  264. /package/dist/{commands → src/commands}/vault.js +0 -0
  265. /package/dist/{core → src/core}/asset-registry.js +0 -0
  266. /package/dist/{core → src/core}/frontmatter.js +0 -0
  267. /package/dist/{core → src/core}/markdown.js +0 -0
  268. /package/dist/{core → src/core}/paths.js +0 -0
  269. /package/dist/{indexer → src/indexer}/manifest.js +0 -0
  270. /package/dist/{indexer → src/indexer}/matchers.js +0 -0
  271. /package/dist/{indexer → src/indexer}/search-fields.js +0 -0
  272. /package/dist/{indexer → src/indexer}/search-source.js +0 -0
  273. /package/dist/{indexer → src/indexer}/semantic-status.js +0 -0
  274. /package/dist/{indexer → src/indexer}/usage-events.js +0 -0
  275. /package/dist/{indexer → src/indexer}/walker.js +0 -0
  276. /package/dist/{integrations → src/integrations}/github.js +0 -0
  277. /package/dist/{llm → src/llm}/embedder.js +0 -0
  278. /package/dist/{llm → src/llm}/embedders/cache.js +0 -0
  279. /package/dist/{llm → src/llm}/embedders/local.js +0 -0
  280. /package/dist/{llm → src/llm}/embedders/remote.js +0 -0
  281. /package/dist/{llm → src/llm}/embedders/types.js +0 -0
  282. /package/dist/{llm → src/llm}/metadata-enhance.js +0 -0
  283. /package/dist/{output → src/output}/cli-hints.js +0 -0
  284. /package/dist/{output → src/output}/context.js +0 -0
  285. /package/dist/{registry → src/registry}/create-provider-registry.js +0 -0
  286. /package/dist/{registry → src/registry}/origin-resolve.js +0 -0
  287. /package/dist/{registry → src/registry}/providers/index.js +0 -0
  288. /package/dist/{registry → src/registry}/providers/skills-sh.js +0 -0
  289. /package/dist/{registry → src/registry}/providers/types.js +0 -0
  290. /package/dist/{registry → src/registry}/types.js +0 -0
  291. /package/dist/{setup → src/setup}/detect.js +0 -0
  292. /package/dist/{setup → src/setup}/ripgrep-install.js +0 -0
  293. /package/dist/{setup → src/setup}/ripgrep-resolve.js +0 -0
  294. /package/dist/{setup → src/setup}/steps.js +0 -0
  295. /package/dist/{sources → src/sources}/include.js +0 -0
  296. /package/dist/{sources → src/sources}/provider-factory.js +0 -0
  297. /package/dist/{sources → src/sources}/provider.js +0 -0
  298. /package/dist/{sources → src/sources}/providers/filesystem.js +0 -0
  299. /package/dist/{sources → src/sources}/providers/index.js +0 -0
  300. /package/dist/{sources → src/sources}/providers/install-types.js +0 -0
  301. /package/dist/{sources → src/sources}/providers/npm.js +0 -0
  302. /package/dist/{sources → src/sources}/providers/provider-utils.js +0 -0
  303. /package/dist/{sources → src/sources}/providers/sync-from-ref.js +0 -0
  304. /package/dist/{sources → src/sources}/providers/tar-utils.js +0 -0
  305. /package/dist/{sources → src/sources}/providers/website.js +0 -0
  306. /package/dist/{sources → src/sources}/resolve.js +0 -0
  307. /package/dist/{sources → src/sources}/types.js +0 -0
  308. /package/dist/{templates → src/templates}/wiki-templates.js +0 -0
  309. /package/dist/{version.js → src/version.js} +0 -0
  310. /package/dist/{wiki → src/wiki}/wiki.js +0 -0
  311. /package/dist/{workflows → src/workflows}/authoring.js +0 -0
  312. /package/dist/{workflows → src/workflows}/cli.js +0 -0
  313. /package/dist/{workflows → src/workflows}/db.js +0 -0
  314. /package/dist/{workflows → src/workflows}/document-cache.js +0 -0
  315. /package/dist/{workflows → src/workflows}/parser.js +0 -0
  316. /package/dist/{workflows → src/workflows}/renderer.js +0 -0
  317. /package/dist/{workflows → src/workflows}/runs.js +0 -0
  318. /package/dist/{workflows → src/workflows}/schema.js +0 -0
  319. /package/dist/{workflows → src/workflows}/validator.js +0 -0
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Shared prompt builders for proposal-producing agent commands (#226).
3
+ *
4
+ * `akm reflect` and `akm propose` both shell out to the configured agent CLI
5
+ * (via {@link runAgent}) and ask it for a structured proposal payload. The
6
+ * prompts are intentionally similar — both ask the agent to return a single
7
+ * JSON object containing `ref`, `content`, and (optionally) `frontmatter` —
8
+ * so we share the construction here. Keeping the prompt builders in
9
+ * `src/integrations/agent/` rather than `src/llm/` is deliberate: these are
10
+ * shell-out prompts targeting an agent CLI, not in-tree LLM API calls.
11
+ *
12
+ * The output the agent must produce is a *strict* JSON object:
13
+ *
14
+ * ```json
15
+ * {
16
+ * "ref": "lesson:my-lesson",
17
+ * "content": "---\ndescription: ...\nwhen_to_use: ...\n---\n\nbody",
18
+ * "frontmatter": { "description": "...", "when_to_use": "..." }
19
+ * }
20
+ * ```
21
+ *
22
+ * `frontmatter` is optional — the proposal queue parses it from `content`
23
+ * during validation. We carry it through if the agent supplies it.
24
+ */
25
+ import { TYPE_DIRS } from "../../core/asset-spec";
26
+ /**
27
+ * Per-asset-type frontmatter / authoring hints surfaced in the prompt so
28
+ * the agent can produce content that passes proposal validation. Kept tiny:
29
+ * full schema docs live in `docs/` — these are nudges, not contracts.
30
+ */
31
+ const TYPE_HINTS = {
32
+ lesson: "lesson assets MUST start with frontmatter containing `description` and `when_to_use` keys (both non-empty). Body should be 1–3 short paragraphs of practical guidance.",
33
+ skill: "skill assets are stored as `skills/<name>/SKILL.md`. Frontmatter typically includes `name`, `description`, and `when_to_use`.",
34
+ command: "command assets are markdown with optional frontmatter (`name`, `description`). The body is the prompt template the user invokes.",
35
+ agent: "agent assets are markdown with frontmatter describing the agent role (`name`, `description`, optional `tools`, `model`).",
36
+ knowledge: "knowledge assets are reference markdown documents. Include a top-level `# Title` and concise sections.",
37
+ memory: "memory assets are short factual notes the user wants persisted across sessions. Frontmatter usually includes `description`.",
38
+ workflow: "workflow assets are markdown describing a multi-step process. Include `# <Title>` and ordered `## Step N` sections.",
39
+ script: "script assets are executable text files. Include a shebang and minimal usage comment.",
40
+ vault: "vault assets store environment variables (KEY=VALUE pairs). Comments use `#`. Never echo secret values back to the user.",
41
+ wiki: "wiki assets are markdown reference pages with `# Title` and structured headings.",
42
+ };
43
+ function hintForType(type) {
44
+ return TYPE_HINTS[type] ?? `assets of type "${type}" — produce sensible markdown with optional frontmatter.`;
45
+ }
46
+ function knownTypeList() {
47
+ return Object.keys(TYPE_DIRS).sort().join(", ");
48
+ }
49
+ /**
50
+ * Common envelope every prompt asks the agent to honour. The wrapper code
51
+ * uses `JSON.parse(stdout)` to extract the payload — anything outside the
52
+ * JSON object will be treated as a parse error.
53
+ */
54
+ const RESPONSE_CONTRACT = [
55
+ "Respond ONLY with a single JSON object. No prose before or after.",
56
+ 'Shape: {"ref": "<type>:<name>", "content": "<full file contents>", "frontmatter": {...}}',
57
+ "`content` is the full file body that will be written if accepted.",
58
+ "`frontmatter` is optional — include it if `content` starts with `---` so reviewers can sanity-check the keys.",
59
+ ].join("\n");
60
+ /**
61
+ * Build the prompt for `akm reflect [ref]`. Asks the agent to review an
62
+ * existing asset (plus any negative feedback / lint findings) and propose
63
+ * an improved version. Returns a single string — the agent runtime will
64
+ * forward it as the trailing positional arg.
65
+ */
66
+ export function buildReflectPrompt(input) {
67
+ const sections = [];
68
+ if (input.ref && input.type && input.name) {
69
+ sections.push(`You are reviewing an akm stash asset (${input.type}) called "${input.name}" and proposing an improved version.`);
70
+ sections.push(`Target ref: ${input.ref}`);
71
+ sections.push(`Asset-type guidance: ${hintForType(input.type)}`);
72
+ }
73
+ else {
74
+ sections.push("You are reviewing recent akm feedback and proposing a single improved asset revision.");
75
+ sections.push("No target ref was supplied. Choose the best target from the feedback below and return it in `ref`.");
76
+ sections.push(`Known asset types: ${knownTypeList()}.`);
77
+ }
78
+ if (input.task?.trim()) {
79
+ sections.push(`Task / focus: ${input.task.trim()}`);
80
+ }
81
+ if (input.assetContent?.trim()) {
82
+ sections.push("Current asset content (verbatim):");
83
+ sections.push("```");
84
+ sections.push(input.assetContent.trimEnd());
85
+ sections.push("```");
86
+ }
87
+ else if (input.ref) {
88
+ sections.push("(No existing content — propose a fresh asset that fits the ref.)");
89
+ }
90
+ else {
91
+ sections.push("(No existing asset content was supplied.)");
92
+ }
93
+ if (input.feedback && input.feedback.length > 0) {
94
+ sections.push("Recent feedback / signals:");
95
+ for (const line of input.feedback)
96
+ sections.push(`- ${line}`);
97
+ }
98
+ else if (!input.ref) {
99
+ sections.push("Recent feedback / signals:");
100
+ sections.push("- (no feedback events recorded)");
101
+ }
102
+ if (input.schemaHints && input.schemaHints.length > 0) {
103
+ sections.push("Schema / lint hints to address:");
104
+ for (const line of input.schemaHints)
105
+ sections.push(`- ${line}`);
106
+ }
107
+ sections.push("Produce a single proposal that addresses the feedback and respects the asset-type contract.");
108
+ sections.push(RESPONSE_CONTRACT);
109
+ return sections.join("\n\n");
110
+ }
111
+ /**
112
+ * Build the prompt for `akm propose <type> <name> --task ...`. Asks the
113
+ * agent to author a brand-new asset of the given type fulfilling `task`.
114
+ */
115
+ export function buildProposePrompt(input) {
116
+ const sections = [];
117
+ sections.push(`Author a new akm stash asset of type "${input.type}" named "${input.name}".`);
118
+ sections.push(`Task: ${input.task}`);
119
+ sections.push(`Asset-type guidance: ${hintForType(input.type)}`);
120
+ sections.push(`(Known asset types: ${knownTypeList()}.)`);
121
+ if (input.schemaHints && input.schemaHints.length > 0) {
122
+ sections.push("Schema / lint hints:");
123
+ for (const line of input.schemaHints)
124
+ sections.push(`- ${line}`);
125
+ }
126
+ sections.push("Produce a single proposal that, if accepted, would land as the asset described above.");
127
+ sections.push(RESPONSE_CONTRACT);
128
+ return sections.join("\n\n");
129
+ }
130
+ /**
131
+ * Parse agent stdout into a proposal payload. The agent contract requires a
132
+ * single JSON object; anything else is reported as a parse error so callers
133
+ * can map to {@link AgentFailureReason} `parse_error`.
134
+ */
135
+ export function parseAgentProposalPayload(stdout) {
136
+ const trimmed = stripJsonFences(stdout).trim();
137
+ if (!trimmed)
138
+ throw new Error("agent produced empty output");
139
+ const parsed = JSON.parse(trimmed);
140
+ if (typeof parsed.ref !== "string" || !parsed.ref.trim()) {
141
+ throw new Error('agent response missing required string field "ref"');
142
+ }
143
+ if (typeof parsed.content !== "string" || !parsed.content.trim()) {
144
+ throw new Error('agent response missing required string field "content"');
145
+ }
146
+ const out = {
147
+ ref: parsed.ref.trim(),
148
+ content: parsed.content,
149
+ };
150
+ if (parsed.frontmatter && typeof parsed.frontmatter === "object" && !Array.isArray(parsed.frontmatter)) {
151
+ out.frontmatter = parsed.frontmatter;
152
+ }
153
+ return out;
154
+ }
155
+ /**
156
+ * Strip `\`\`\`json … \`\`\`` fences if the agent wrapped its JSON output.
157
+ * Mirrors the same helper in `src/llm/client.ts` but kept local here so
158
+ * `agent/` does not import from `llm/` (the boundary is one-way per
159
+ * v1 spec §9.7 — agents are shell-out only).
160
+ */
161
+ export function stripJsonFences(text) {
162
+ const trimmed = text.trim();
163
+ const fenced = trimmed.match(/^```(?:json)?\s*\n([\s\S]*?)\n```$/);
164
+ if (fenced)
165
+ return fenced[1] ?? trimmed;
166
+ return trimmed;
167
+ }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Agent CLI spawn wrapper (v1 spec §12.2).
3
+ *
4
+ * Single helper that owns:
5
+ * • Process spawn (Bun's subprocess API).
6
+ * • Captured vs interactive stdio.
7
+ * • Hard timeout (per-call override or profile default).
8
+ * • Structured failure reasons — `timeout`, `spawn_failed`,
9
+ * `non_zero_exit`, `parse_error`.
10
+ *
11
+ * NEVER imports an LLM SDK. Agents are reachable only via shell-out;
12
+ * this is a pre-emptive guarantee against the #222 invariant.
13
+ */
14
+ import { DEFAULT_AGENT_TIMEOUT_MS } from "./config";
15
+ const DEFAULT_TIMEOUT_MS = DEFAULT_AGENT_TIMEOUT_MS;
16
+ function resolveSpawnFn(options) {
17
+ if (options.spawn)
18
+ return options.spawn;
19
+ // Pull from globalThis so tests that swap it out at module level are honoured.
20
+ const bun = globalThis.Bun;
21
+ if (!bun?.spawn) {
22
+ throw new Error("Bun.spawn is unavailable; pass options.spawn for non-Bun environments.");
23
+ }
24
+ return bun.spawn.bind(bun);
25
+ }
26
+ /**
27
+ * Build the child env. Starts empty and copies through:
28
+ * • Every name in `profile.envPassthrough`.
29
+ * • Every entry in `profile.env`.
30
+ * • Every entry in `options.env` (highest precedence).
31
+ */
32
+ function buildChildEnv(profile, options) {
33
+ const source = options.envSource ?? process.env;
34
+ const env = {};
35
+ for (const name of profile.envPassthrough) {
36
+ const value = source[name];
37
+ if (value !== undefined)
38
+ env[name] = value;
39
+ }
40
+ if (profile.env) {
41
+ for (const [k, v] of Object.entries(profile.env))
42
+ env[k] = v;
43
+ }
44
+ if (options.env) {
45
+ for (const [k, v] of Object.entries(options.env))
46
+ env[k] = v;
47
+ }
48
+ return env;
49
+ }
50
+ async function readStream(stream) {
51
+ if (!stream)
52
+ return "";
53
+ try {
54
+ return await new Response(stream).text();
55
+ }
56
+ catch {
57
+ return "";
58
+ }
59
+ }
60
+ /**
61
+ * Spawn the agent CLI described by `profile` with `prompt` (forwarded as
62
+ * the last positional arg by default) and return a structured result.
63
+ *
64
+ * The `prompt` argument is appended to `profile.args` (and `options.args`)
65
+ * unless it is `undefined`. Pass `prompt = ""` to forward an explicit
66
+ * empty positional, or pass extra args via `options.args`.
67
+ *
68
+ * Failure modes (see {@link AgentFailureReason}):
69
+ *
70
+ * • `spawn_failed` — `Bun.spawn` threw synchronously.
71
+ * • `timeout` — exceeded the resolved timeout.
72
+ * • `non_zero_exit` — child exited with a non-zero code.
73
+ * • `parse_error` — `parseOutput === "json"` and stdout was not JSON.
74
+ *
75
+ * `ok === true` requires exit code 0 and (if `parseOutput === "json"`)
76
+ * a successful `JSON.parse`.
77
+ */
78
+ export async function runAgent(profile, prompt, options = {}) {
79
+ const stdioMode = options.stdio ?? profile.stdio;
80
+ const timeoutMs = options.timeoutMs ?? profile.timeoutMs ?? DEFAULT_TIMEOUT_MS;
81
+ const parseOutput = options.parseOutput ?? profile.parseOutput;
82
+ const setTimeoutImpl = options.setTimeoutFn ?? setTimeout;
83
+ const clearTimeoutImpl = options.clearTimeoutFn ?? clearTimeout;
84
+ const args = [...profile.args, ...(options.args ?? [])];
85
+ if (prompt !== undefined)
86
+ args.push(prompt);
87
+ const env = buildChildEnv(profile, options);
88
+ const start = Date.now();
89
+ let proc;
90
+ try {
91
+ const spawnFn = resolveSpawnFn(options);
92
+ proc = spawnFn([profile.bin, ...args], {
93
+ stdin: stdioMode === "captured" ? "pipe" : "inherit",
94
+ stdout: stdioMode === "captured" ? "pipe" : "inherit",
95
+ stderr: stdioMode === "captured" ? "pipe" : "inherit",
96
+ env,
97
+ ...(options.cwd ? { cwd: options.cwd } : {}),
98
+ });
99
+ }
100
+ catch (err) {
101
+ const durationMs = Date.now() - start;
102
+ return {
103
+ ok: false,
104
+ exitCode: null,
105
+ stdout: "",
106
+ stderr: "",
107
+ durationMs,
108
+ reason: "spawn_failed",
109
+ error: err instanceof Error ? err.message : String(err),
110
+ };
111
+ }
112
+ // Hard timeout. We prefer SIGTERM, then SIGKILL if SIGTERM is ignored,
113
+ // but Bun.spawn only exposes a single .kill() — one signal is enough
114
+ // for the structured-failure contract.
115
+ //
116
+ // BUG-M3: only flag `timedOut` when the child has not already exited. A
117
+ // timer firing in the same microtask as `proc.exited` resolving could
118
+ // otherwise label a clean exit as a timeout.
119
+ let timedOut = false;
120
+ const timer = setTimeoutImpl(() => {
121
+ if (proc.exitCode !== null)
122
+ return;
123
+ timedOut = true;
124
+ try {
125
+ proc.kill("SIGTERM");
126
+ }
127
+ catch {
128
+ /* ignore */
129
+ }
130
+ }, timeoutMs);
131
+ const stdoutPromise = stdioMode === "captured" ? readStream(proc.stdout ?? null) : Promise.resolve("");
132
+ const stderrPromise = stdioMode === "captured" ? readStream(proc.stderr ?? null) : Promise.resolve("");
133
+ // Optional stdin payload (captured mode only).
134
+ //
135
+ // BUG-H1: race the stdin write/close against `proc.exited` and the
136
+ // timeout timer. If the child never drains stdin, an unraced
137
+ // `await writer.write()` would block forever and prevent `runAgent`
138
+ // from ever returning.
139
+ if (options.stdin !== undefined && stdioMode === "captured" && proc.stdin) {
140
+ const stdinPayload = options.stdin;
141
+ const stdinStream = proc.stdin;
142
+ const stdinDone = (async () => {
143
+ try {
144
+ const writer = stdinStream.getWriter();
145
+ const bytes = new TextEncoder().encode(stdinPayload);
146
+ await writer.write(bytes);
147
+ await writer.close();
148
+ }
149
+ catch {
150
+ // Best-effort: ignore stdin write failures, the child will get EOF.
151
+ }
152
+ })();
153
+ // Resolve as soon as either the write completes or the child exits.
154
+ // We don't await the result — only that one of the two has settled —
155
+ // so a stuck writer cannot keep us pinned past the timeout.
156
+ await Promise.race([stdinDone, proc.exited.catch(() => undefined)]);
157
+ }
158
+ let exitCode = null;
159
+ try {
160
+ exitCode = await proc.exited;
161
+ }
162
+ catch (err) {
163
+ clearTimeoutImpl(timer);
164
+ // BUG-H2: drain stream readers before the early return so they don't
165
+ // surface as unhandled rejections after the function resolves.
166
+ await Promise.allSettled([stdoutPromise, stderrPromise]);
167
+ const durationMs = Date.now() - start;
168
+ return {
169
+ ok: false,
170
+ exitCode: null,
171
+ stdout: "",
172
+ stderr: "",
173
+ durationMs,
174
+ reason: "spawn_failed",
175
+ error: err instanceof Error ? err.message : String(err),
176
+ };
177
+ }
178
+ clearTimeoutImpl(timer);
179
+ const [stdout, stderr] = await Promise.all([stdoutPromise, stderrPromise]);
180
+ const durationMs = Date.now() - start;
181
+ if (timedOut) {
182
+ return {
183
+ ok: false,
184
+ exitCode,
185
+ stdout,
186
+ stderr,
187
+ durationMs,
188
+ reason: "timeout",
189
+ error: `agent CLI "${profile.name}" timed out after ${timeoutMs}ms`,
190
+ };
191
+ }
192
+ if (exitCode !== 0) {
193
+ return {
194
+ ok: false,
195
+ exitCode,
196
+ stdout,
197
+ stderr,
198
+ durationMs,
199
+ reason: "non_zero_exit",
200
+ error: `agent CLI "${profile.name}" exited with code ${exitCode}`,
201
+ };
202
+ }
203
+ if (parseOutput === "json" && stdioMode === "captured") {
204
+ try {
205
+ const parsed = JSON.parse(stdout);
206
+ return { ok: true, exitCode, stdout, stderr, durationMs, parsed };
207
+ }
208
+ catch (err) {
209
+ return {
210
+ ok: false,
211
+ exitCode,
212
+ stdout,
213
+ stderr,
214
+ durationMs,
215
+ reason: "parse_error",
216
+ error: err instanceof Error ? err.message : String(err),
217
+ };
218
+ }
219
+ }
220
+ return { ok: true, exitCode, stdout, stderr, durationMs };
221
+ }
@@ -3,34 +3,9 @@ import path from "node:path";
3
3
  import { getConfigDir } from "../core/config";
4
4
  // ── Paths ───────────────────────────────────────────────────────────────────
5
5
  const LOCKFILE_NAME = "akm.lock";
6
- const LEGACY_LOCKFILE_NAME = "stash.lock";
7
6
  function getLockfilePath() {
8
7
  return path.join(getConfigDir(), LOCKFILE_NAME);
9
8
  }
10
- function getLegacyLockfilePath() {
11
- return path.join(getConfigDir(), LEGACY_LOCKFILE_NAME);
12
- }
13
- /**
14
- * One-time migration: if the new `akm.lock` does not exist but the legacy
15
- * `stash.lock` does, copy it across so installed-stash tracking survives the
16
- * rename. Best-effort; failures are silent because the lockfile loader treats
17
- * a missing file as an empty lockfile.
18
- */
19
- function migrateLegacyLockfileIfNeeded() {
20
- const newPath = getLockfilePath();
21
- const legacyPath = getLegacyLockfilePath();
22
- try {
23
- if (fs.existsSync(newPath))
24
- return;
25
- if (!fs.existsSync(legacyPath))
26
- return;
27
- fs.mkdirSync(path.dirname(newPath), { recursive: true });
28
- fs.copyFileSync(legacyPath, newPath);
29
- }
30
- catch {
31
- /* best-effort — fall through to empty lockfile */
32
- }
33
- }
34
9
  // ── Lock sentinel ────────────────────────────────────────────────────────────
35
10
  const LOCK_MAX_RETRIES = 3;
36
11
  const LOCK_RETRY_DELAY_MS = 100;
@@ -100,7 +75,6 @@ function releaseLockSentinel() {
100
75
  }
101
76
  // ── Read / Write ────────────────────────────────────────────────────────────
102
77
  export function readLockfile() {
103
- migrateLegacyLockfileIfNeeded();
104
78
  const lockfilePath = getLockfilePath();
105
79
  try {
106
80
  const raw = JSON.parse(fs.readFileSync(lockfilePath, "utf8"));
@@ -8,6 +8,36 @@
8
8
  * `llm.ts` re-exports everything from this module for backward compatibility.
9
9
  */
10
10
  import { fetchWithTimeout } from "../core/common";
11
+ /** Maximum length of an LLM error response body included in thrown errors. */
12
+ const ERROR_BODY_MAX_LEN = 200;
13
+ /**
14
+ * Redact credential-shaped substrings from an upstream error body before
15
+ * including it in a thrown Error. The body is also trimmed to a fixed length
16
+ * so that a verbose provider response cannot leak large amounts of context.
17
+ *
18
+ * Targets:
19
+ * - `Bearer <token>` headers echoed back by the provider
20
+ * - `sk-…` / `sk_…` style API keys (OpenAI / Anthropic-shaped)
21
+ * - `key-…` / `key_…` shorthand keys
22
+ * - `"api_key": "…"` / `"apiKey": "…"` JSON fields
23
+ */
24
+ export function redactErrorBody(input) {
25
+ if (!input)
26
+ return "";
27
+ let out = input
28
+ // Bearer tokens (case-insensitive)
29
+ .replace(/\bBearer\s+[A-Za-z0-9._\-+/=]+/gi, "Bearer [REDACTED]")
30
+ // sk-/sk_ style keys
31
+ .replace(/\bsk[-_][A-Za-z0-9._-]{6,}/g, "[REDACTED]")
32
+ // key-/key_ shorthand keys
33
+ .replace(/\bkey[-_][A-Za-z0-9._-]{6,}/g, "[REDACTED]")
34
+ // JSON-style "api_key": "...", "apiKey": "...", "api-key": "..."
35
+ .replace(/("(?:api[_-]?key|apiKey|authorization|token)"\s*:\s*")([^"]*)(")/gi, "$1[REDACTED]$3");
36
+ if (out.length > ERROR_BODY_MAX_LEN) {
37
+ out = `${out.slice(0, ERROR_BODY_MAX_LEN)}…`;
38
+ }
39
+ return out;
40
+ }
11
41
  export async function chatCompletion(config, messages, options) {
12
42
  const headers = { "Content-Type": "application/json" };
13
43
  if (config.apiKey) {
@@ -24,8 +54,9 @@ export async function chatCompletion(config, messages, options) {
24
54
  }),
25
55
  });
26
56
  if (!response.ok) {
27
- const body = await response.text().catch(() => "");
28
- throw new Error(`LLM request failed (${response.status}): ${body}`);
57
+ const rawBody = await response.text().catch(() => "");
58
+ const safeBody = redactErrorBody(rawBody);
59
+ throw new Error(`LLM request failed (${response.status}) ${config.endpoint}: ${safeBody}`);
29
60
  }
30
61
  const json = (await response.json());
31
62
  return json.choices?.[0]?.message?.content?.trim() ?? "";
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Per-feature LLM gates (v1 spec §14).
3
+ *
4
+ * Every bounded in-tree LLM call site in akm is addressed by exactly one
5
+ * feature key under `llm.features.*`. This module is the single seam call
6
+ * sites use to ask "should I run?" and "if I run and fail, what do I return?"
7
+ *
8
+ * The seam is intentionally tiny:
9
+ *
10
+ * - `isLlmFeatureEnabled(config, feature)` — pure predicate, no side
11
+ * effects, no I/O. Returns `true` only when the feature flag is the
12
+ * literal boolean `true` in config. Defaults are `false` per v1
13
+ * spec §14 — adding a flag to the schema is a non-event until the user
14
+ * opts in.
15
+ * - `tryLlmFeature(feature, config, fn, fallback, opts?)` — single-call
16
+ * wrapper that runs `fn()` only when the gate is open, enforces a hard
17
+ * timeout (default 30s — overridable per call), and returns `fallback`
18
+ * on disablement, throw, or timeout. The wrapper is referentially
19
+ * transparent for any given (gate-state, fn-result) pair: no module
20
+ * state is mutated.
21
+ *
22
+ * Statelessness invariant (v1 spec §14.4): nothing in this module holds
23
+ * state across calls. There are no caches, no module-level singletons, no
24
+ * persistent connections. The architecture seam test
25
+ * (`tests/architecture/llm-stateless-seam.test.ts`) does not currently
26
+ * inspect this file but the same rule applies — keep all exports as pure
27
+ * functions.
28
+ */
29
+ /**
30
+ * Pure predicate: is the named feature gate explicitly enabled in `config`?
31
+ *
32
+ * Returns `false` when:
33
+ * - the LLM block is missing,
34
+ * - the `features` block is missing,
35
+ * - the key is absent (defaults are `false`),
36
+ * - the key is set to `false`.
37
+ */
38
+ export function isLlmFeatureEnabled(config, feature) {
39
+ if (!config?.llm?.features)
40
+ return false;
41
+ return config.llm.features[feature] === true;
42
+ }
43
+ const DEFAULT_TIMEOUT_MS = 30_000;
44
+ /**
45
+ * Run `fn()` only if `isLlmFeatureEnabled(config, feature)` is `true`. On
46
+ * disablement, throw, or timeout, return `fallback` (or — if it is a
47
+ * thunk — the value produced by calling it).
48
+ *
49
+ * The fallback may be a value or a synchronous/async function returning a
50
+ * value. The thunk form lets call sites encode "run the deterministic
51
+ * pipeline" without paying for it in the success path:
52
+ *
53
+ * ```ts
54
+ * const ranked = await tryLlmFeature(
55
+ * "curate_rerank",
56
+ * config,
57
+ * () => llmRerank(candidates),
58
+ * () => deterministicRerank(candidates),
59
+ * );
60
+ * ```
61
+ */
62
+ export async function tryLlmFeature(feature, config, fn, fallback, opts) {
63
+ const resolveFallback = async () => typeof fallback === "function" ? await fallback() : fallback;
64
+ if (!isLlmFeatureEnabled(config, feature)) {
65
+ opts?.onFallback?.({ feature, reason: "disabled" });
66
+ return resolveFallback();
67
+ }
68
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
69
+ try {
70
+ if (timeoutMs <= 0) {
71
+ return await fn();
72
+ }
73
+ return await runWithTimeout(fn, timeoutMs, feature);
74
+ }
75
+ catch (err) {
76
+ const error = err instanceof Error ? err : new Error(String(err));
77
+ const reason = error instanceof LlmFeatureTimeoutError ? "timeout" : "error";
78
+ opts?.onFallback?.({ feature, reason, error });
79
+ return resolveFallback();
80
+ }
81
+ }
82
+ /** Specific error class so call sites and the wrapper can tell timeouts apart from generic throws. */
83
+ export class LlmFeatureTimeoutError extends Error {
84
+ feature;
85
+ timeoutMs;
86
+ constructor(feature, timeoutMs) {
87
+ super(`LLM feature "${feature}" timed out after ${timeoutMs}ms.`);
88
+ this.name = "LlmFeatureTimeoutError";
89
+ this.feature = feature;
90
+ this.timeoutMs = timeoutMs;
91
+ Object.setPrototypeOf(this, new.target.prototype);
92
+ }
93
+ }
94
+ async function runWithTimeout(fn, timeoutMs, feature) {
95
+ let timer;
96
+ try {
97
+ return await new Promise((resolve, reject) => {
98
+ timer = setTimeout(() => reject(new LlmFeatureTimeoutError(feature, timeoutMs)), timeoutMs);
99
+ Promise.resolve()
100
+ .then(() => fn())
101
+ .then(resolve, reject);
102
+ });
103
+ }
104
+ finally {
105
+ if (timer)
106
+ clearTimeout(timer);
107
+ }
108
+ }