akm-cli 0.9.0-rc.9 → 0.9.1-beta.1

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 (477) hide show
  1. package/CHANGELOG.md +1770 -44
  2. package/README.md +38 -47
  3. package/SECURITY.md +14 -1
  4. package/STABILITY.md +499 -0
  5. package/dist/akm +148 -35
  6. package/dist/{akm-migrate-storage → akm-migrate} +6 -9
  7. package/dist/assets/hints/cli-hints-full.md +223 -95
  8. package/dist/assets/hints/cli-hints-short.md +85 -22
  9. package/dist/assets/improve-strategies/default.json +1 -1
  10. package/dist/assets/improve-strategies/reflect-distill.json +1 -1
  11. package/dist/assets/prompts/memory-infer-user.md +2 -3
  12. package/dist/assets/stash-skeleton/README.md +6 -5
  13. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +2 -0
  14. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +2 -0
  15. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +2 -0
  16. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +2 -0
  17. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +2 -0
  18. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +2 -0
  19. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +2 -0
  20. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +2 -0
  21. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +2 -0
  22. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +2 -0
  23. package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -0
  24. package/dist/assets/stash-skeleton/facts/conventions/organization.md +20 -9
  25. package/dist/assets/tasks/core/extract.yml +1 -1
  26. package/dist/assets/tasks/core/version-check.yml +1 -1
  27. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
  28. package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
  29. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
  30. package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
  31. package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
  32. package/dist/assets/templates/html/health.html +1 -3
  33. package/dist/assets/workflows/workflow-template.md +32 -15
  34. package/dist/cli/invocation.js +40 -15
  35. package/dist/cli/parse-args.js +0 -22
  36. package/dist/cli/retired-commands.js +121 -0
  37. package/dist/cli/shared.js +154 -22
  38. package/dist/cli/unknown-flags.js +236 -0
  39. package/dist/cli-node.mjs +2 -1
  40. package/dist/cli.js +696 -258
  41. package/dist/commands/agent/agent-dispatch.js +14 -3
  42. package/dist/commands/agent/contribute-cli.js +74 -89
  43. package/dist/commands/completions.js +79 -22
  44. package/dist/commands/config-cli.js +17 -150
  45. package/dist/commands/env/env-cli.js +59 -143
  46. package/dist/commands/env/env.js +12 -163
  47. package/dist/commands/env/marker-path.js +6 -0
  48. package/dist/commands/env/secret-cli.js +36 -66
  49. package/dist/commands/env/secret.js +24 -57
  50. package/dist/commands/feedback-cli.js +148 -88
  51. package/dist/commands/health/accept-rate.js +58 -0
  52. package/dist/commands/health/advisories.js +3 -4
  53. package/dist/commands/health/checks.js +85 -23
  54. package/dist/commands/health/html-report.js +7 -10
  55. package/dist/commands/health/improve-metrics.js +25 -83
  56. package/dist/commands/health/llm-usage.js +2 -1
  57. package/dist/commands/health/md-report.js +5 -9
  58. package/dist/commands/health/metrics.js +62 -20
  59. package/dist/commands/health/renderers.js +47 -0
  60. package/dist/commands/health/report-view-model.js +4 -5
  61. package/dist/commands/health/stash-exposure.js +1 -1
  62. package/dist/commands/health/surfaces.js +4 -122
  63. package/dist/commands/health/task-runs.js +3 -67
  64. package/dist/commands/health/types-improve.js +7 -0
  65. package/dist/commands/health.js +160 -35
  66. package/dist/commands/improve/anti-collapse.js +2 -2
  67. package/dist/commands/improve/autonomy-gate.js +68 -0
  68. package/dist/commands/improve/collapse-detector.js +41 -40
  69. package/dist/commands/improve/consolidate/eligibility.js +1 -23
  70. package/dist/commands/improve/consolidate/merge.js +4 -0
  71. package/dist/commands/improve/consolidate.js +140 -1000
  72. package/dist/commands/improve/distill/promote-memory.js +12 -12
  73. package/dist/commands/improve/distill/quality-gate.js +11 -6
  74. package/dist/commands/improve/distill.js +58 -69
  75. package/dist/commands/improve/eligibility.js +101 -55
  76. package/dist/commands/improve/extract-cli.js +14 -133
  77. package/dist/commands/improve/improve-cli.js +98 -114
  78. package/dist/commands/improve/improve-result-file.js +1 -28
  79. package/dist/commands/improve/improve-strategies.js +8 -5
  80. package/dist/commands/improve/improve.js +254 -101
  81. package/dist/commands/improve/locks.js +7 -0
  82. package/dist/commands/improve/loop-stages.js +182 -20
  83. package/dist/commands/improve/memory/derived-ref.js +45 -43
  84. package/dist/commands/improve/memory/memory-belief.js +1 -1
  85. package/dist/commands/improve/memory/memory-contradiction-detect.js +4 -12
  86. package/dist/commands/improve/memory/memory-improve.js +14 -5
  87. package/dist/commands/improve/outcome-loop.js +22 -65
  88. package/dist/commands/improve/preparation.js +114 -123
  89. package/dist/commands/improve/proactive-maintenance.js +2 -5
  90. package/dist/commands/improve/reflect.js +56 -160
  91. package/dist/commands/improve/run-context.js +5 -0
  92. package/dist/commands/improve/salience.js +11 -122
  93. package/dist/commands/improve/session-asset.js +4 -0
  94. package/dist/commands/improve/source-identity.js +10 -38
  95. package/dist/commands/lint/base-linter.js +51 -131
  96. package/dist/commands/lint/env-key-rules.js +31 -47
  97. package/dist/commands/lint/index.js +444 -88
  98. package/dist/commands/lint/types.js +22 -1
  99. package/dist/commands/{events.js → log.js} +33 -38
  100. package/dist/commands/migrate-cli.js +92 -12
  101. package/dist/commands/migration-tool.js +46 -0
  102. package/dist/commands/observability-cli.js +70 -209
  103. package/dist/commands/proposal/drain.js +101 -29
  104. package/dist/commands/proposal/proposal-cli.js +76 -48
  105. package/dist/commands/proposal/proposal.js +54 -18
  106. package/dist/commands/proposal/propose-cli.js +88 -0
  107. package/dist/commands/proposal/propose.js +23 -15
  108. package/dist/commands/proposal/repository.js +718 -279
  109. package/dist/commands/proposal/validators/proposal-quality-validators.js +2 -8
  110. package/dist/commands/proposal/validators/proposal-validators.js +55 -7
  111. package/dist/commands/proposal/validators/proposals.js +4 -7
  112. package/dist/commands/read/curate.js +34 -53
  113. package/dist/commands/read/knowledge.js +150 -95
  114. package/dist/commands/read/registry-search.js +2 -2
  115. package/dist/commands/read/remember-cli.js +42 -15
  116. package/dist/commands/read/search-cli.js +180 -78
  117. package/dist/commands/read/search.js +58 -43
  118. package/dist/commands/read/show.js +197 -141
  119. package/dist/commands/registry-cli.js +12 -51
  120. package/dist/commands/remember.js +14 -57
  121. package/dist/commands/sources/add-cli.js +100 -31
  122. package/dist/commands/sources/bundle-cli.js +166 -0
  123. package/dist/commands/sources/bundle-config-ops.js +7 -2
  124. package/dist/commands/sources/info.js +30 -7
  125. package/dist/commands/sources/init.js +12 -12
  126. package/dist/commands/sources/installed-stashes.js +387 -98
  127. package/dist/commands/sources/schema-repair.js +3 -2
  128. package/dist/commands/sources/self-update.js +131 -38
  129. package/dist/commands/sources/source-add.js +72 -17
  130. package/dist/commands/sources/source-clone.js +129 -45
  131. package/dist/commands/sources/source-manage.js +43 -23
  132. package/dist/commands/sources/sources-cli.js +57 -208
  133. package/dist/commands/sources/stash-cli.js +46 -53
  134. package/dist/commands/tasks/tasks-cli.js +91 -97
  135. package/dist/commands/tasks/tasks.js +284 -423
  136. package/dist/commands/workflow-cli.js +181 -450
  137. package/dist/core/abort-deadline.js +28 -0
  138. package/dist/core/adapter/adapters/agent-skills-adapter.js +67 -5
  139. package/dist/core/adapter/adapters/akm-adapter.js +60 -38
  140. package/dist/core/adapter/adapters/akm-lint.js +109 -42
  141. package/dist/core/adapter/adapters/akm-metadata.js +15 -44
  142. package/dist/core/adapter/adapters/akm-task-adapter.js +54 -31
  143. package/dist/core/adapter/adapters/akm-workflow-adapter.js +55 -71
  144. package/dist/core/adapter/adapters/dotenv-adapter.js +1 -1
  145. package/dist/core/adapter/adapters/generic-files-adapter.js +2 -0
  146. package/dist/core/adapter/adapters/index.js +6 -6
  147. package/dist/core/adapter/adapters/llm-wiki-adapter.js +14 -8
  148. package/dist/core/adapter/adapters/okf-adapter.js +187 -19
  149. package/dist/core/adapter/adapters/shared.js +3 -19
  150. package/dist/core/adapter/adapters/tool-dir-shared.js +13 -6
  151. package/dist/core/adapter/adapters/website-snapshot-adapter.js +1 -0
  152. package/dist/core/adapter/detect-adapter.js +17 -0
  153. package/dist/core/adapter/recognize-match.js +6 -4
  154. package/dist/core/adapter/validate-context.js +214 -0
  155. package/dist/core/asset/akm-markdown.js +63 -0
  156. package/dist/core/asset/asset-placement.js +20 -6
  157. package/dist/core/asset/asset-ref.js +11 -9
  158. package/dist/core/asset/frontmatter-lint.js +30 -0
  159. package/dist/core/asset/frontmatter.js +29 -9
  160. package/dist/core/asset/markdown.js +40 -51
  161. package/dist/core/asset/resolve-ref.js +89 -18
  162. package/dist/core/asset/stash-meta.js +1 -1
  163. package/dist/core/bundle-id.js +51 -0
  164. package/dist/core/common.js +218 -44
  165. package/dist/core/concurrent.js +32 -0
  166. package/dist/core/config/config-io.js +12 -1
  167. package/dist/core/config/config-schema.js +35 -8
  168. package/dist/core/config/config-sources.js +55 -11
  169. package/dist/core/config/config-walker.js +25 -9
  170. package/dist/core/config/config.js +9 -48
  171. package/dist/core/config/experimental.js +21 -0
  172. package/dist/core/config/schema/embedding.js +5 -1
  173. package/dist/core/config/schema/experimental.js +30 -0
  174. package/dist/core/config/schema/improve-processes.js +0 -6
  175. package/dist/core/config/schema/improve.js +21 -3
  176. package/dist/core/config/schema/index-config.js +8 -15
  177. package/dist/core/config/schema/output.js +4 -1
  178. package/dist/core/config/schema/setup.js +9 -18
  179. package/dist/core/config/schema/sources-bundles.js +49 -33
  180. package/dist/core/config/schema/workflow.js +14 -3
  181. package/dist/core/env-secret-ref.js +76 -46
  182. package/dist/core/errors.js +43 -12
  183. package/dist/core/events.js +76 -152
  184. package/dist/core/file-change.js +6 -5
  185. package/dist/core/file-lock.js +7 -1
  186. package/dist/core/fs-txn.js +83 -7
  187. package/dist/core/git-message.js +2 -2
  188. package/dist/core/improve-result.js +6 -100
  189. package/dist/core/json-schema.js +327 -9
  190. package/dist/core/lesson-lint.js +1 -17
  191. package/dist/core/logs-db.js +2 -1
  192. package/dist/core/loopback.js +89 -0
  193. package/dist/core/migration-operation.js +33 -2
  194. package/dist/core/mutation-target.js +78 -0
  195. package/dist/core/parse.js +4 -1
  196. package/dist/core/path-access.js +107 -0
  197. package/dist/core/paths.js +17 -20
  198. package/dist/core/recognition-util.js +12 -14
  199. package/dist/core/redaction.js +98 -17
  200. package/dist/core/spawn-env.js +234 -0
  201. package/dist/core/standards/resolve-standards-context.js +2 -14
  202. package/dist/core/standards/resolve-stash-standards.js +2 -2
  203. package/dist/core/standards/resolve-type-conventions.js +2 -2
  204. package/dist/core/state/migrations.js +41 -18
  205. package/dist/core/state-db-scope.js +134 -0
  206. package/dist/core/state-db.js +5 -14
  207. package/dist/core/structured.js +1 -1
  208. package/dist/core/subprocess.js +182 -36
  209. package/dist/core/text-truncation.js +9 -5
  210. package/dist/core/type-presentation.js +3 -3
  211. package/dist/core/warn.js +0 -3
  212. package/dist/core/write-provenance.js +85 -0
  213. package/dist/core/write-source.js +778 -95
  214. package/dist/indexer/bundle-identity-guard.js +3 -2
  215. package/dist/indexer/db/graph-db.js +17 -30
  216. package/dist/indexer/ensure-index.js +11 -3
  217. package/dist/indexer/graph/graph-boost.js +9 -34
  218. package/dist/indexer/graph/graph-extraction.js +8 -5
  219. package/dist/indexer/index-writer-lock.js +53 -17
  220. package/dist/indexer/index-written-assets.js +33 -24
  221. package/dist/indexer/indexer.js +551 -253
  222. package/dist/indexer/installations.js +14 -96
  223. package/dist/indexer/passes/dir-staleness.js +16 -9
  224. package/dist/indexer/passes/memory-inference.js +15 -9
  225. package/dist/indexer/passes/metadata.js +113 -47
  226. package/dist/indexer/scan/doc-to-entry.js +38 -1
  227. package/dist/indexer/scan/drain-dir.js +13 -23
  228. package/dist/indexer/search/db-search.js +107 -69
  229. package/dist/indexer/search/fts-query.js +47 -24
  230. package/dist/indexer/search/ranking-contributors.js +42 -20
  231. package/dist/indexer/search/ranking.js +18 -99
  232. package/dist/indexer/search/search-fields.js +7 -2
  233. package/dist/indexer/search/search-source.js +82 -93
  234. package/dist/indexer/usage/usage-events.js +0 -89
  235. package/dist/indexer/walk/file-context.js +2 -1
  236. package/dist/indexer/walk/matchers.js +30 -43
  237. package/dist/indexer/walk/path-resolver.js +7 -2
  238. package/dist/indexer/walk/walker.js +38 -12
  239. package/dist/integrations/agent/builders.js +0 -6
  240. package/dist/integrations/agent/config.js +2 -2
  241. package/dist/integrations/agent/detect.js +49 -19
  242. package/dist/integrations/agent/engine-fallback.js +76 -0
  243. package/dist/integrations/agent/engine-resolution.js +24 -11
  244. package/dist/integrations/agent/model-aliases.js +1 -1
  245. package/dist/integrations/agent/profiles.js +23 -1
  246. package/dist/integrations/agent/prompts.js +12 -8
  247. package/dist/integrations/agent/runner-dispatch.js +4 -2
  248. package/dist/integrations/agent/runner.js +0 -1
  249. package/dist/integrations/agent/spawn.js +20 -93
  250. package/dist/integrations/github.js +1 -1
  251. package/dist/integrations/harnesses/aider/agent-builder.js +6 -4
  252. package/dist/integrations/harnesses/amazonq/agent-builder.js +7 -4
  253. package/dist/integrations/harnesses/claude/session-log.js +0 -10
  254. package/dist/integrations/harnesses/codex/agent-builder.js +5 -2
  255. package/dist/integrations/harnesses/copilot/agent-builder.js +5 -3
  256. package/dist/integrations/harnesses/gemini/agent-builder.js +5 -3
  257. package/dist/integrations/harnesses/index.js +3 -7
  258. package/dist/integrations/harnesses/opencode/agent-builder.js +21 -2
  259. package/dist/integrations/harnesses/opencode/session-log.js +0 -15
  260. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +13 -4
  261. package/dist/integrations/harnesses/openhands/agent-builder.js +9 -6
  262. package/dist/integrations/harnesses/pi/agent-builder.js +6 -4
  263. package/dist/integrations/lockfile.js +132 -8
  264. package/dist/integrations/session-logs/index.js +3 -28
  265. package/dist/llm/client.js +136 -100
  266. package/dist/llm/embedders/remote.js +13 -5
  267. package/dist/llm/feature-gate.js +4 -12
  268. package/dist/llm/graph-extract.js +5 -11
  269. package/dist/llm/memory-infer.js +144 -1
  270. package/dist/llm/metadata-enhance.js +5 -7
  271. package/dist/llm/structured-call.js +1 -1
  272. package/dist/llm/usage-persist.js +26 -5
  273. package/dist/llm/usage-telemetry.js +25 -2
  274. package/dist/output/cli-hints.js +1 -2
  275. package/dist/output/context.js +22 -7
  276. package/dist/output/format-exempt.js +80 -0
  277. package/dist/output/generic-render.js +259 -0
  278. package/dist/output/render-registry.js +57 -0
  279. package/dist/output/renderers.js +14 -36
  280. package/dist/output/shapes/curate.js +10 -1
  281. package/dist/output/shapes/events.js +12 -7
  282. package/dist/output/shapes/helpers.js +56 -83
  283. package/dist/output/shapes/migrate.js +8 -0
  284. package/dist/output/shapes/passthrough.js +7 -41
  285. package/dist/output/shapes/proposal/producer.js +15 -7
  286. package/dist/output/shapes.js +2 -9
  287. package/dist/output/text/{init.js → bundle-create.js} +3 -1
  288. package/dist/output/text/bundle-show.js +7 -0
  289. package/dist/output/text/command-format.js +164 -96
  290. package/dist/output/text/env.js +1 -3
  291. package/dist/output/text/events.js +8 -7
  292. package/dist/output/text/health-format.js +103 -0
  293. package/dist/output/text/health.js +7 -0
  294. package/dist/output/text/helpers.js +10 -8
  295. package/dist/output/text/lint-format.js +56 -0
  296. package/dist/{migrate-storage-node.mjs → output/text/lint.js} +2 -5
  297. package/dist/output/text/migrate.js +88 -0
  298. package/dist/output/text/proposal/producer.js +4 -2
  299. package/dist/output/text/proposal-format.js +44 -72
  300. package/dist/output/text/registry-commands.js +1 -2
  301. package/dist/output/text/show-directives.js +15 -7
  302. package/dist/output/text/status-list.js +32 -0
  303. package/dist/output/text/{save.js → sync.js} +2 -2
  304. package/dist/output/text/workflow-format.js +24 -203
  305. package/dist/output/text/workflow.js +1 -7
  306. package/dist/output/text.js +16 -17
  307. package/dist/registry/factory.js +4 -6
  308. package/dist/registry/origin-resolve.js +16 -27
  309. package/dist/registry/providers/skills-sh.js +3 -3
  310. package/dist/registry/providers/static-index.js +13 -23
  311. package/dist/registry/resolve.js +42 -7
  312. package/dist/registry/semver.js +34 -84
  313. package/dist/runtime.js +2 -23
  314. package/dist/scripts/akm-migrate-node.js +60972 -0
  315. package/dist/scripts/akm-migrate.js +60310 -0
  316. package/dist/setup/detect.js +42 -15
  317. package/dist/setup/registry-stash-loader.js +2 -2
  318. package/dist/setup/setup.js +236 -136
  319. package/dist/setup/steps/connection.js +7 -9
  320. package/dist/setup/steps/platforms.js +9 -9
  321. package/dist/setup/steps/semantic.js +15 -3
  322. package/dist/setup/steps/sources.js +12 -13
  323. package/dist/setup/steps/stashdir.js +2 -3
  324. package/dist/setup/steps/tasks.js +237 -120
  325. package/dist/sources/freshness.js +1 -1
  326. package/dist/sources/provider-factory.js +11 -17
  327. package/dist/sources/providers/filesystem.js +2 -3
  328. package/dist/sources/providers/git-install.js +278 -34
  329. package/dist/sources/providers/git-provider.js +25 -23
  330. package/dist/sources/providers/git-stash.js +414 -106
  331. package/dist/sources/providers/git.js +2 -2
  332. package/dist/sources/providers/npm.js +16 -19
  333. package/dist/sources/providers/provider-utils.js +7 -4
  334. package/dist/sources/providers/sync-from-ref.js +3 -9
  335. package/dist/sources/providers/website.js +6 -1
  336. package/dist/sources/resolve.js +6 -5
  337. package/dist/sources/snapshot-fetchers/bluesky.js +146 -0
  338. package/dist/sources/snapshot-fetchers/content-extract.js +566 -0
  339. package/dist/sources/snapshot-fetchers/fetcher-util.js +41 -0
  340. package/dist/sources/snapshot-fetchers/github.js +100 -0
  341. package/dist/sources/snapshot-fetchers/host-guard.js +291 -0
  342. package/dist/sources/snapshot-fetchers/registry.js +17 -1
  343. package/dist/sources/snapshot-fetchers/robots.js +348 -0
  344. package/dist/sources/snapshot-fetchers/rss.js +282 -0
  345. package/dist/sources/snapshot-fetchers/secret-seam.js +42 -0
  346. package/dist/sources/snapshot-fetchers/website-ingest.js +686 -282
  347. package/dist/sources/snapshot-fetchers/x.js +910 -0
  348. package/dist/storage/database.js +13 -1
  349. package/dist/storage/engines/sqlite-migrations.js +23 -111
  350. package/dist/storage/managed-db.js +21 -2
  351. package/dist/storage/repositories/canaries-repository.js +1 -1
  352. package/dist/storage/repositories/events-repository.js +27 -11
  353. package/dist/storage/repositories/improve-runs-repository.js +6 -12
  354. package/dist/storage/repositories/index-connection.js +44 -8
  355. package/dist/storage/repositories/index-entries-repository.js +157 -241
  356. package/dist/storage/repositories/index-entry-mapper.js +15 -11
  357. package/dist/storage/repositories/index-fts-repository.js +5 -2
  358. package/dist/storage/repositories/index-llm-cache-repository.js +0 -1
  359. package/dist/storage/repositories/index-meta-repository.js +2 -3
  360. package/dist/storage/repositories/index-schema.js +10 -25
  361. package/dist/storage/repositories/index-utility-repository.js +15 -28
  362. package/dist/storage/repositories/index-vec-repository.js +6 -1
  363. package/dist/storage/repositories/outcome-repository.js +119 -0
  364. package/dist/storage/repositories/proposals-repository.js +296 -59
  365. package/dist/storage/repositories/registry-cache.js +19 -0
  366. package/dist/storage/repositories/salience-repository.js +172 -0
  367. package/dist/storage/repositories/task-history-repository.js +15 -13
  368. package/dist/storage/repositories/workflow-runs-repository.js +118 -53
  369. package/dist/tasks/backends/cron.js +105 -15
  370. package/dist/tasks/backends/index.js +1 -1
  371. package/dist/tasks/backends/launchd.js +85 -38
  372. package/dist/tasks/backends/schtasks.js +135 -15
  373. package/dist/tasks/embedded.js +56 -40
  374. package/dist/tasks/log-redaction.js +156 -0
  375. package/dist/tasks/parser.js +87 -160
  376. package/dist/tasks/resolve-akm-bin.js +137 -59
  377. package/dist/tasks/runner.js +209 -44
  378. package/dist/tasks/scheduler-invocation.js +220 -10
  379. package/dist/tasks/schema.js +89 -1
  380. package/dist/tasks/task-id.js +1 -3
  381. package/dist/tasks/validator.js +20 -6
  382. package/dist/workflows/authoring/authoring.js +94 -143
  383. package/dist/workflows/authoring/scope-key.js +1 -1
  384. package/dist/workflows/concurrency-policy.js +95 -1
  385. package/dist/workflows/exec/dispatch-redaction.js +114 -0
  386. package/dist/workflows/exec/exec-unit.js +542 -0
  387. package/dist/workflows/exec/frozen-judge.js +130 -32
  388. package/dist/workflows/exec/native-executor.js +513 -274
  389. package/dist/workflows/exec/param-secrets.js +12 -11
  390. package/dist/workflows/exec/run-workflow.js +496 -237
  391. package/dist/workflows/exec/step-work.js +616 -412
  392. package/dist/workflows/exec/unit-dispatch.js +34 -0
  393. package/dist/workflows/exec/unit-writer.js +53 -13
  394. package/dist/workflows/exec/worktree.js +454 -41
  395. package/dist/workflows/ir/compile.js +166 -271
  396. package/dist/workflows/ir/freeze.js +119 -42
  397. package/dist/workflows/ir/params.js +135 -11
  398. package/dist/workflows/ir/plan-hash.js +1 -1
  399. package/dist/workflows/ir/schema.js +129 -45
  400. package/dist/workflows/parser.js +1070 -307
  401. package/dist/workflows/program/expressions.js +20 -208
  402. package/dist/workflows/program/schema.js +31 -10
  403. package/dist/workflows/renderer.js +123 -68
  404. package/dist/workflows/resource-limits.js +184 -0
  405. package/dist/workflows/runtime/checkin.js +3 -3
  406. package/dist/workflows/runtime/plan-classifier.js +16 -75
  407. package/dist/workflows/runtime/runs.js +331 -132
  408. package/dist/workflows/runtime/unit-checkin.js +1 -1
  409. package/dist/workflows/runtime/unit-phases.js +2 -2
  410. package/dist/workflows/runtime/workflow-asset-loader.js +232 -83
  411. package/dist/workflows/schema.js +1 -11
  412. package/dist/workflows/validate-summary.js +47 -38
  413. package/dist/workflows/validator.js +21 -62
  414. package/docs/README.md +110 -0
  415. package/docs/migration/README.md +8 -0
  416. package/docs/migration/release-notes/0.7.0.md +11 -11
  417. package/docs/migration/release-notes/0.9.0.md +209 -27
  418. package/docs/migration/v0.7-to-v0.8.md +48 -48
  419. package/docs/migration/v0.8-to-v0.9.md +567 -209
  420. package/docs/migration/v0.9.0-troubleshooting.md +561 -0
  421. package/docs/reference/README.md +19 -0
  422. package/docs/reference/bundle-types.md +19 -0
  423. package/docs/reference/cli.md +2342 -0
  424. package/docs/reference/configuration.md +371 -0
  425. package/docs/reference/data-and-telemetry.md +129 -46
  426. package/docs/reference/supported-formats.md +50 -0
  427. package/docs/reference/workflow-schema.md +1014 -0
  428. package/docs/reference/workflows.md +51 -0
  429. package/package.json +31 -13
  430. package/schemas/akm-asset-envelope.json +93 -0
  431. package/schemas/akm-config.json +89 -128
  432. package/schemas/akm-task.json +27 -5
  433. package/schemas/akm-workflow.json +157 -81
  434. package/dist/assets/tasks/core/backup.yml +0 -5
  435. package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
  436. package/dist/cli/config-migrate.js +0 -1878
  437. package/dist/cli/config-validate.js +0 -41
  438. package/dist/commands/backup-cli.js +0 -56
  439. package/dist/commands/bundle/bundle-cli.js +0 -68
  440. package/dist/commands/bundle/bundle.js +0 -219
  441. package/dist/commands/graph/graph-cli.js +0 -124
  442. package/dist/commands/graph/graph.js +0 -489
  443. package/dist/commands/improve/extract-watch.js +0 -140
  444. package/dist/commands/mv-cli.js +0 -1221
  445. package/dist/commands/sources/history.js +0 -201
  446. package/dist/commands/tasks/default-tasks.js +0 -186
  447. package/dist/core/migration-backup.js +0 -1231
  448. package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -95
  449. package/dist/llm/memory-infer-impl.js +0 -138
  450. package/dist/migrate/legacy/config-source-migration.js +0 -257
  451. package/dist/migrate/legacy/content-migration.js +0 -350
  452. package/dist/migrate/legacy/legacy-layout.js +0 -779
  453. package/dist/migrate/legacy/legacy-paths.js +0 -25
  454. package/dist/migrate/legacy/legacy-stash-json.js +0 -80
  455. package/dist/migrate/legacy/proposal-fs-import.js +0 -168
  456. package/dist/migrate/legacy/task-target-ref-migration.js +0 -278
  457. package/dist/migrate/legacy/three-db-cutover.js +0 -845
  458. package/dist/migrate/legacy/workflow-migrations-bodies.js +0 -52
  459. package/dist/migrate/legacy/workflow-migrations-frozen.js +0 -21
  460. package/dist/migrate/legacy-ref-grammar.js +0 -214
  461. package/dist/output/shapes/distill.js +0 -14
  462. package/dist/output/shapes/history.js +0 -11
  463. package/dist/output/text/distill.js +0 -6
  464. package/dist/output/text/enable-disable.js +0 -8
  465. package/dist/output/text/history.js +0 -6
  466. package/dist/registry/build-index.js +0 -382
  467. package/dist/schemas/akm-config.json +0 -4704
  468. package/dist/schemas/akm-task.json +0 -87
  469. package/dist/schemas/akm-workflow.json +0 -372
  470. package/dist/scripts/migrate-storage.js +0 -3816
  471. package/dist/workflows/authoring/workflow-program-template.yaml +0 -31
  472. package/dist/workflows/cli.js +0 -53
  473. package/dist/workflows/exec/brief.js +0 -481
  474. package/dist/workflows/exec/report.js +0 -1460
  475. package/dist/workflows/exec/watch.js +0 -116
  476. package/dist/workflows/program/parser.js +0 -813
  477. package/dist/workflows/program/project.js +0 -104
@@ -7,6 +7,7 @@ import { writeFileAtomic } from "../core/common.js";
7
7
  import { ConfigError, rethrowIfTestIsolationError } from "../core/errors.js";
8
8
  import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync } from "../core/file-lock.js";
9
9
  import { acquireMaintenanceBarrier } from "../core/maintenance-barrier.js";
10
+ import { classifyPathAccess, describeInaccessiblePath } from "../core/path-access.js";
10
11
  import { getDataDir, getLockfileLockPath, getLockfilePath } from "../core/paths.js";
11
12
  // ── Lock sentinel ────────────────────────────────────────────────────────────
12
13
  const LOCK_MAX_RETRIES = 3;
@@ -54,6 +55,79 @@ export function readLockfile() {
54
55
  return [];
55
56
  }
56
57
  }
58
+ /**
59
+ * Refuse to treat an UNREADABLE lockfile (or data dir) as an absent one (#791).
60
+ *
61
+ * Every write path here is read-modify-WRITE: it loads the current entries and
62
+ * writes the whole array back. An unreadable `akm.lock` that reads as `[]`
63
+ * therefore does not merely lose information — the very next
64
+ * `writeFileAtomic` replaces the operator's entire lock record with the single
65
+ * entry this call happened to be adding. That is the same catastrophe R-012
66
+ * guards against for a *corrupt* file, reached instead through a permission
67
+ * fault, and `fs.existsSync`/a swallowed `readFileSync` could not tell the two
68
+ * apart from "the file was never created".
69
+ *
70
+ * No-op when the path is genuinely absent — that case really does have nothing
71
+ * to preserve.
72
+ */
73
+ function assertLockfilePathReadable(target) {
74
+ const { access, code } = classifyPathAccess(target);
75
+ if (access !== "inaccessible")
76
+ return;
77
+ throw new ConfigError(`Refusing to modify the lockfile: ${describeInaccessiblePath(target, code)}. akm cannot read the existing lock ` +
78
+ "records, and writing over them would destroy every bundle they track. Fix the ownership or mode of that " +
79
+ "path (or point AKM_DATA_DIR / XDG_DATA_HOME somewhere this user owns) and retry.", "DATA_DIR_UNREADABLE");
80
+ }
81
+ /**
82
+ * Like {@link readLockfile}, but THROWS instead of silently degrading to `[]`
83
+ * when the on-disk lockfile exists yet is not parseable JSON or not a JSON
84
+ * array (R-012).
85
+ *
86
+ * `readLockfile`'s fail-open contract is intentional for READ paths — a
87
+ * corrupt lock degrades a managed bundle to "unmanaged" rather than erroring
88
+ * every read-only command (`list`, `installed-stashes`, …). But
89
+ * {@link upsertLockEntry} and {@link removeLockEntry} read the current
90
+ * entries and then WRITE `[...entries, change]` back out; if that read
91
+ * silently returned `[]` for a corrupt file, the write would silently
92
+ * replace the corrupt file with one containing only the single new/changed
93
+ * entry — permanently destroying every other surviving lock record. Write
94
+ * paths use this strict variant so a corrupt lockfile fails the operation
95
+ * loudly (matching the guard `mergeLockEntriesSync` already applies) instead
96
+ * of quietly deleting user state. A missing file is NOT corruption — there
97
+ * is nothing to preserve, so that case still returns `[]`. Entries that fail
98
+ * per-entry validation are still tolerated (filtered out), matching
99
+ * `readLockfile`'s existing shape-tolerant behavior.
100
+ */
101
+ function readLockfileOrThrow() {
102
+ const lockfilePath = getLockfilePath();
103
+ let raw;
104
+ try {
105
+ raw = fs.readFileSync(lockfilePath, "utf8");
106
+ }
107
+ catch (err) {
108
+ rethrowIfTestIsolationError(err);
109
+ // "Missing file" is the only failure with nothing to preserve. An
110
+ // UNREADABLE lockfile has everything to preserve and we cannot see it —
111
+ // degrading it to `[]` here is precisely the destructive overwrite this
112
+ // function was written to prevent, only triggered by a permission fault
113
+ // instead of a corrupt file (#791). Classify AFTER the failed read so the
114
+ // happy path costs no extra syscall and the answer describes the failure
115
+ // we actually got.
116
+ assertLockfilePathReadable(lockfilePath);
117
+ return [];
118
+ }
119
+ let parsed;
120
+ try {
121
+ parsed = JSON.parse(raw);
122
+ }
123
+ catch (error) {
124
+ throw new ConfigError(`Refusing to modify lockfile ${lockfilePath}: existing content is not valid JSON (${error instanceof Error ? error.message : String(error)}). Fix or remove the file by hand before retrying — every existing lock entry would otherwise be lost.`, "INVALID_CONFIG_FILE");
125
+ }
126
+ if (!Array.isArray(parsed)) {
127
+ throw new ConfigError(`Refusing to modify lockfile ${lockfilePath}: existing content is not a JSON array. Fix or remove the file by hand before retrying — every existing lock entry would otherwise be lost.`, "INVALID_CONFIG_FILE");
128
+ }
129
+ return parsed.filter(isValidLockfileEntry);
130
+ }
57
131
  /**
58
132
  * The materialized content root recorded in the lock for a managed (git/npm)
59
133
  * bundle — spec §10.2 desired/resolved split, where the desired config carries
@@ -93,10 +167,27 @@ export async function writeLockfile(entries) {
93
167
  release();
94
168
  }
95
169
  }
170
+ /** Replace one exact parsed lock generation while the lockfile sentinel is held. */
171
+ export async function compareAndSwapLockfile(expected, desired) {
172
+ const release = await acquireLockSentinel();
173
+ try {
174
+ const current = readLockfileOrThrow();
175
+ if (JSON.stringify(current) !== JSON.stringify(expected))
176
+ return false;
177
+ writeLockfileUnlocked(desired);
178
+ return true;
179
+ }
180
+ finally {
181
+ release();
182
+ }
183
+ }
96
184
  export async function upsertLockEntry(entry) {
97
185
  const release = await acquireLockSentinel();
98
186
  try {
99
- const entries = readLockfile();
187
+ // R-012: readLockfileOrThrow (not readLockfile) — a corrupt/malformed
188
+ // lockfile must abort the upsert loudly rather than read as `[]` and get
189
+ // silently overwritten with just this one entry.
190
+ const entries = readLockfileOrThrow();
100
191
  const withoutExisting = entries.filter((e) => e.id !== entry.id);
101
192
  writeLockfileUnlocked([...withoutExisting, entry]);
102
193
  }
@@ -110,11 +201,14 @@ export async function upsertLockEntry(entry) {
110
201
  * migrate-apply's config lock + maintenance barrier) whose synchronous body
111
202
  * cannot await the sentinel's retry loop. No-op for an empty list.
112
203
  */
113
- export function mergeLockEntriesSync(entries) {
114
- if (entries.length === 0)
115
- return;
204
+ function readLockEntriesForMigration() {
116
205
  let existing = [];
117
206
  const lockfilePath = getLockfilePath();
207
+ // `mergeLockEntriesSync` writes `existing` straight back out, so an
208
+ // unreadable lockfile read as absent would be overwritten with just the
209
+ // migrator's sparse entries (#791). This is also what
210
+ // `assertMigrationLockfileReadable` promises to have checked.
211
+ assertLockfilePathReadable(lockfilePath);
118
212
  if (fs.existsSync(lockfilePath)) {
119
213
  let raw;
120
214
  try {
@@ -128,15 +222,45 @@ export function mergeLockEntriesSync(entries) {
128
222
  }
129
223
  existing = raw;
130
224
  }
131
- const incoming = new Set(entries.map((e) => e.id));
132
- writeLockfileUnlocked([...existing.filter((e) => !incoming.has(e.id)), ...entries]);
225
+ return existing;
226
+ }
227
+ /** Validate the current lockfile before migrate-apply creates its backup or sentinel. */
228
+ export function assertMigrationLockfileReadable() {
229
+ readLockEntriesForMigration();
230
+ }
231
+ export function mergeLockEntriesSync(entries) {
232
+ const existing = readLockEntriesForMigration();
233
+ if (entries.length === 0)
234
+ return;
235
+ // MERGE per id, never replace: the migrator's entries are sparse (id/source/
236
+ // ref/localRoot), while an existing row may carry `resolvedVersion`,
237
+ // `resolvedRevision`, `integrity`, `installedAt`, … from a real install.
238
+ // Replacing the whole row discarded the user's recorded resolution/pin and
239
+ // left later update reporting comparing against an unknown prior version.
240
+ // Incoming DEFINED fields win; existing fields absent from the incoming
241
+ // entry are preserved.
242
+ const byId = new Map(existing.map((e) => [e.id, e]));
243
+ const merged = entries.map((incoming) => {
244
+ const prior = byId.get(incoming.id);
245
+ return prior ? { ...prior, ...incoming } : incoming;
246
+ });
247
+ const incomingIds = new Set(entries.map((e) => e.id));
248
+ writeLockfileUnlocked([...existing.filter((e) => !incomingIds.has(e.id)), ...merged]);
133
249
  }
134
250
  export async function removeLockEntry(id) {
135
- if (!fs.existsSync(getDataDir()))
251
+ // Returning early says "there is no lock record to remove", and the uninstall
252
+ // that called us reports success on that basis. Only an absent data dir earns
253
+ // it — one we cannot read may hold the very entry we were asked to drop, and
254
+ // silently leaving it behind is how a bundle stays "installed" forever (#791).
255
+ const dataDir = getDataDir();
256
+ assertLockfilePathReadable(dataDir);
257
+ if (!fs.existsSync(dataDir))
136
258
  return;
137
259
  const release = await acquireLockSentinel();
138
260
  try {
139
- const entries = readLockfile();
261
+ // R-012: see upsertLockEntry — same destructive-overwrite risk on a
262
+ // corrupt lockfile, so the same strict reader is used here.
263
+ const entries = readLockfileOrThrow();
140
264
  writeLockfileUnlocked(entries.filter((e) => e.id !== id));
141
265
  }
142
266
  finally {
@@ -50,22 +50,6 @@ const ERROR_PATTERNS = /error|failed|exception|cannot|undefined|null pointer|ENO
50
50
  export function getAvailableHarnesses() {
51
51
  return HARNESSES.filter((harness) => harness.isAvailable());
52
52
  }
53
- /**
54
- * Map each available harness to its `{ harnessName, roots }` watch target,
55
- * skipping harnesses that expose no roots (absent `watchRoots()` or an empty
56
- * result). This is the one stable entry point the watcher uses so it never
57
- * reaches into providers directly.
58
- */
59
- export function getWatchTargets() {
60
- const targets = [];
61
- for (const harness of getAvailableHarnesses()) {
62
- const roots = harness.watchRoots?.() ?? [];
63
- if (roots.length === 0)
64
- continue;
65
- targets.push({ harnessName: harness.name, roots });
66
- }
67
- return targets;
68
- }
69
53
  export function normalizeSessionTopic(text) {
70
54
  const normalized = text.replace(/\s+/g, " ").trim().toLowerCase();
71
55
  if (normalized.length < 10)
@@ -107,16 +91,13 @@ export function aggregateSessionEvents(events) {
107
91
  * Collect normalized session events from a set of harnesses for the health
108
92
  * candidate scan (#568).
109
93
  *
110
- * Pipeline selection per harness (capability-gated):
111
- * - readSession-capable harness (`supportsReadSession !== false`): drive the
112
- * richer `listSessions()` + `readSession()` pipeline. `readSession` flattens
94
+ * Pipeline selection per harness:
95
+ * - Drive the richer `listSessions()` + `readSession()` pipeline. `readSession` flattens
113
96
  * structured content — tool calls, assistant content blocks, thinking,
114
97
  * tool_result — into event text (e.g. ClaudeCodeProvider's `parseClaudeEvent`
115
98
  * surfaces `[tool:*]` / `[tool_result]` blocks that the legacy flat
116
99
  * `readEvents` scan drops entirely). This is what lets health advisories see
117
100
  * repeated tool failures / long runs that the flat scan hid.
118
- * - legacy-only harness (`supportsReadSession === false`): fall back to the
119
- * legacy flat `readEvents()` scan (behaviour-preserving).
120
101
  *
121
102
  * Extracted as a pure function (harnesses injected) so it is unit-testable
122
103
  * without touching the real on-disk session-log locations.
@@ -129,19 +110,13 @@ export function aggregateSessionEvents(events) {
129
110
  * health command (`akm health`, which calls this synchronously) blow past its
130
111
  * latency budget. `listSessions()` returns summaries sorted newest-first, so
131
112
  * capping to the most-recent N sessions per harness keeps the richer signal
132
- * for what actually matters (recent activity) while bounding cost. The legacy
133
- * flat-scan path is naturally cheaper and is left uncapped.
113
+ * for what actually matters (recent activity) while bounding cost.
134
114
  */
135
115
  const DEFAULT_MAX_SESSIONS_PER_HARNESS = 50;
136
116
  export function collectSessionEvents(harnesses, sinceMs, maxSessionsPerHarness = DEFAULT_MAX_SESSIONS_PER_HARNESS) {
137
117
  const events = [];
138
118
  for (const harness of harnesses) {
139
119
  try {
140
- if (harness.supportsReadSession === false) {
141
- // Legacy-only harness: only the flat event scan is available.
142
- events.push(...harness.readEvents({ sinceMs }));
143
- continue;
144
- }
145
120
  // Rich path: enumerate sessions cheaply, then read each one's full
146
121
  // structured event stream. Falls back to readEvents if listSessions
147
122
  // surfaces nothing (e.g. a harness that wired readSession but whose
@@ -4,23 +4,17 @@
4
4
  /**
5
5
  * Low-level OpenAI-compatible chat completions client and capability probing.
6
6
  *
7
- * Split out of `llm.ts` to keep the transport-layer concerns (HTTP request,
8
- * response parsing, JSON-fence stripping, capability probe, availability
9
- * check) separate from higher-level metadata-enhancement workflows.
10
- *
11
- * `llm.ts` re-exports everything from this module for backward compatibility.
7
+ * Keeps transport-layer concerns (HTTP request, response parsing, capability
8
+ * probing, and availability checks) separate from higher-level workflows.
12
9
  */
13
- import { fetchWithTimeout } from "../core/common.js";
10
+ import { fetchWithTimeout, readBodyWithByteCap } from "../core/common.js";
14
11
  import { resolveSecret } from "../core/config/config.js";
15
12
  import { formatExtraParamsIssue, validateExtraParams } from "../core/extra-params.js";
16
- import { escapeJsonStringControls, parseJsonResponse, stripCodeFences, stripThinkBlocks } from "../core/parse.js";
17
- import { redactSensitiveText } from "../core/redaction.js";
13
+ import { parseJsonResponse } from "../core/parse.js";
14
+ import { redactCredentialPatterns, redactSensitiveText } from "../core/redaction.js";
18
15
  import { warnVerbose } from "../core/warn.js";
19
16
  import { DEFAULT_LLM_TIMEOUT_MS } from "../integrations/agent/config.js";
20
- import { emitLlmUsage, extractUsageTokens } from "./usage-telemetry.js";
21
- // Re-export shared parse utilities so existing importers of `client.ts` continue
22
- // to resolve `parseJsonResponse` and `parseEmbeddedJsonResponse` from this module.
23
- export { escapeJsonStringControls, parseEmbeddedJsonResponse, parseJsonResponse, stripCodeFences, stripThinkBlocks, } from "../core/parse.js";
17
+ import { emitLlmUsage, extractUsageTokens, } from "./usage-telemetry.js";
24
18
  /** Maximum length of an LLM error response body included in thrown errors. */
25
19
  const ERROR_BODY_MAX_LEN = 200;
26
20
  /** Stable OpenAI-compatible response-schema name used for every structured call. */
@@ -30,24 +24,14 @@ const JSON_SCHEMA_RESPONSE_NAME = "akm_response";
30
24
  * including it in a thrown Error. The body is also trimmed to a fixed length
31
25
  * so that a verbose provider response cannot leak large amounts of context.
32
26
  *
33
- * Targets:
34
- * - `Bearer <token>` headers echoed back by the provider
35
- * - `sk-…` / `sk_…` style API keys (OpenAI / Anthropic-shaped)
36
- * - `key-…` / `key_…` shorthand keys
37
- * - `"api_key": "…"` / `"apiKey": "…"` JSON fields
27
+ * The pattern set itself lives in {@link redactCredentialPatterns}
28
+ * (src/core/redaction.ts) so other output paths (e.g. task run logs) can
29
+ * reuse it without this function's length cap.
38
30
  */
39
31
  export function redactErrorBody(input) {
40
32
  if (!input)
41
33
  return "";
42
- let out = input
43
- // Bearer tokens (case-insensitive)
44
- .replace(/\bBearer\s+[A-Za-z0-9._\-+/=]+/gi, "Bearer [REDACTED]")
45
- // sk-/sk_ style keys
46
- .replace(/\bsk[-_][A-Za-z0-9._-]{6,}/g, "[REDACTED]")
47
- // key-/key_ shorthand keys
48
- .replace(/\bkey[-_][A-Za-z0-9._-]{6,}/g, "[REDACTED]")
49
- // JSON-style "api_key": "...", "apiKey": "...", "api-key": "..."
50
- .replace(/("(?:api[_-]?key|apiKey|authorization|token)"\s*:\s*")([^"]*)(")/gi, "$1[REDACTED]$3");
34
+ let out = redactCredentialPatterns(input);
51
35
  if (out.length > ERROR_BODY_MAX_LEN) {
52
36
  out = `${out.slice(0, ERROR_BODY_MAX_LEN)}…`;
53
37
  }
@@ -103,6 +87,19 @@ const RETRY_BUDGET_FRACTION = 0.9;
103
87
  export function sleep(ms) {
104
88
  return new Promise((resolve) => setTimeout(resolve, ms));
105
89
  }
90
+ async function waitForRetry(ms, wait, signal) {
91
+ if (!signal)
92
+ return wait(ms);
93
+ if (signal.aborted)
94
+ throw new LlmCallError("LLM request aborted", "aborted");
95
+ await new Promise((resolve, reject) => {
96
+ const onAbort = () => reject(new LlmCallError("LLM request aborted", "aborted"));
97
+ signal.addEventListener("abort", onAbort, { once: true });
98
+ wait(ms)
99
+ .then(resolve, reject)
100
+ .finally(() => signal.removeEventListener("abort", onAbort));
101
+ });
102
+ }
106
103
  /** Compute a uniform jittered backoff in the [200, 800)ms range. */
107
104
  function retryBackoffMs() {
108
105
  return RETRY_BACKOFF_MIN_MS + Math.random() * (RETRY_BACKOFF_MAX_MS - RETRY_BACKOFF_MIN_MS);
@@ -209,7 +206,7 @@ async function chatCompletionReal(config, messages, options) {
209
206
  // authoritative.
210
207
  warnVerbose(`[akm] LLM transient failure (${err.code}); retrying once: ${err.message}`);
211
208
  const wait = retryBackoffMs();
212
- await (options?.sleep ?? sleep)(wait);
209
+ await waitForRetry(wait, options?.sleep ?? sleep, options?.signal);
213
210
  // The retry must not exceed the original budget.
214
211
  return await chatCompletionAttempt(config, messages, options, remaining);
215
212
  }
@@ -248,90 +245,129 @@ async function chatCompletionAttempt(config, messages, options, timeoutMs) {
248
245
  : config.provider === "vllm"
249
246
  ? { chat_template_kwargs: { enable_thinking: resolvedEnableThinking } }
250
247
  : { enable_thinking: resolvedEnableThinking };
251
- // Wall-clock start for per-call usage telemetry (#576). Captured here so the
248
+ const requestBody = JSON.stringify({
249
+ model: config.model,
250
+ messages,
251
+ temperature: options?.temperature ?? config.temperature ?? 0.3,
252
+ ...(resolvedMaxTokens !== undefined ? { max_tokens: resolvedMaxTokens } : {}),
253
+ ...responseFormat,
254
+ ...thinkingParams,
255
+ ...config.extraParams,
256
+ });
257
+ // Wall-clock start for per-attempt usage telemetry (#576). Captured here so the
252
258
  // emitted duration covers the full request/response/parse cycle of a single
253
259
  // attempt, not the retry-wrapping `chatCompletion`.
254
260
  const requestStartedAt = Date.now();
255
- let response;
261
+ const requestDeadlineAt = timeoutMs === null ? null : requestStartedAt + timeoutMs;
262
+ const remainingAttemptMs = () => requestDeadlineAt === null ? undefined : Math.max(0, requestDeadlineAt - Date.now());
263
+ let terminalFields = {
264
+ model: config.model,
265
+ modelSource: "configured",
266
+ };
256
267
  try {
257
- response = await fetchWithTimeout(config.endpoint, {
258
- method: "POST",
259
- headers,
260
- body: JSON.stringify({
261
- model: config.model,
262
- messages,
263
- temperature: options?.temperature ?? config.temperature ?? 0.3,
264
- ...(resolvedMaxTokens !== undefined ? { max_tokens: resolvedMaxTokens } : {}),
265
- ...responseFormat,
266
- ...thinkingParams,
267
- ...config.extraParams,
268
- }),
269
- }, timeoutMs, options?.signal);
270
- }
271
- catch (err) {
272
- // fetchWithTimeout throws a plain Error with a message containing
273
- // "timed out" for AbortController-driven timeouts, or "aborted" for
274
- // caller-driven cancellations. Map both to typed LlmCallError.
275
- const msg = err instanceof Error ? err.message : String(err);
276
- if (err instanceof DOMException && err.name === "AbortError") {
277
- throw new LlmCallError(`Request timed out${timeoutMs === null ? "" : ` after ${timeoutMs}ms`}`, "timeout");
268
+ let response;
269
+ try {
270
+ response = await fetchWithTimeout(config.endpoint, {
271
+ method: "POST",
272
+ headers,
273
+ body: requestBody,
274
+ }, timeoutMs, options?.signal);
278
275
  }
279
- if (msg.includes("timed out")) {
280
- throw new LlmCallError(`Request timed out${timeoutMs === null ? "" : ` after ${timeoutMs}ms`}`, "timeout");
276
+ catch (err) {
277
+ // fetchWithTimeout throws a plain Error with a message containing
278
+ // "timed out" for AbortController-driven timeouts, or "aborted" for
279
+ // caller-driven cancellations. Map both to typed LlmCallError.
280
+ const msg = err instanceof Error ? err.message : String(err);
281
+ if (options?.signal?.aborted || msg.includes("Request aborted")) {
282
+ throw new LlmCallError("LLM request aborted", "aborted");
283
+ }
284
+ if (err instanceof DOMException && err.name === "AbortError") {
285
+ throw new LlmCallError(`Request timed out${timeoutMs === null ? "" : ` after ${timeoutMs}ms`}`, "timeout");
286
+ }
287
+ if (msg.includes("timed out")) {
288
+ throw new LlmCallError(`Request timed out${timeoutMs === null ? "" : ` after ${timeoutMs}ms`}`, "timeout");
289
+ }
290
+ throw new LlmCallError(`Network error: ${msg}`, "network_error");
281
291
  }
282
- throw new LlmCallError(`Network error: ${msg}`, "network_error");
283
- }
284
- if (!response.ok) {
285
- const rawBody = await response.text().catch(() => "");
286
- const safeBody = redactSensitiveText(redactErrorBody(rawBody), resolvedKey ? [resolvedKey] : []);
287
- const status = response.status;
288
- if (status === 429) {
289
- throw new LlmCallError(`LLM request rate limited (429) ${config.endpoint}: ${safeBody}`, "rate_limited", status);
292
+ if (!response.ok) {
293
+ const rawBody = await readBodyWithByteCap(response, undefined, {
294
+ ...(requestDeadlineAt === null ? {} : { bodyTimeoutMs: remainingAttemptMs() }),
295
+ signal: options?.signal,
296
+ }).catch((err) => {
297
+ if (options?.signal?.aborted)
298
+ throw new LlmCallError("LLM response read aborted", "aborted");
299
+ if (err instanceof Error && err.name === "BodyReadTimeoutError") {
300
+ throw new LlmCallError("LLM response body read timed out", "timeout");
301
+ }
302
+ return "";
303
+ });
304
+ const safeBody = redactSensitiveText(redactErrorBody(rawBody), resolvedKey ? [resolvedKey] : []);
305
+ const status = response.status;
306
+ if (status === 429) {
307
+ throw new LlmCallError(`LLM request rate limited (429) ${config.endpoint}: ${safeBody}`, "rate_limited", status);
308
+ }
309
+ if (status >= 500 && isHtmlResponse(rawBody)) {
310
+ throw new LlmCallError(`LLM provider returned HTML instead of JSON (${status}) ${config.endpoint}: ${redactSensitiveText(htmlExcerpt(rawBody), resolvedKey ? [resolvedKey] : [])}`, "provider_html_error", status);
311
+ }
312
+ if (status >= 500) {
313
+ throw new LlmCallError(`LLM provider error (${status}) ${config.endpoint}: ${safeBody}`, "provider_error", status);
314
+ }
315
+ throw new LlmCallError(`LLM request failed (${status}) ${config.endpoint}: ${safeBody}`, "provider_error", status);
290
316
  }
291
- if (status >= 500 && isHtmlResponse(rawBody)) {
292
- throw new LlmCallError(`LLM provider returned HTML instead of JSON (${status}) ${config.endpoint}: ${redactSensitiveText(htmlExcerpt(rawBody), resolvedKey ? [resolvedKey] : [])}`, "provider_html_error", status);
317
+ // A 2xx response is still an error if the body is HTML where JSON was
318
+ // expected (e.g. a provider serving its web UI). Read the raw body first so
319
+ // we can categorize an HTML page distinctly from a malformed-JSON parse_error.
320
+ let rawOkBody;
321
+ try {
322
+ rawOkBody = await readBodyWithByteCap(response, undefined, {
323
+ ...(requestDeadlineAt === null ? {} : { bodyTimeoutMs: remainingAttemptMs() }),
324
+ signal: options?.signal,
325
+ });
293
326
  }
294
- if (status >= 500) {
295
- throw new LlmCallError(`LLM provider error (${status}) ${config.endpoint}: ${safeBody}`, "provider_error", status);
327
+ catch (err) {
328
+ if (options?.signal?.aborted)
329
+ throw new LlmCallError("LLM response read aborted", "aborted");
330
+ if (err instanceof Error && err.name === "BodyReadTimeoutError") {
331
+ throw new LlmCallError("LLM response body read timed out", "timeout");
332
+ }
333
+ throw err;
296
334
  }
297
- throw new LlmCallError(`LLM request failed (${status}) ${config.endpoint}: ${safeBody}`, "provider_error", status);
298
- }
299
- // A 2xx response is still an error if the body is HTML where JSON was
300
- // expected (e.g. a provider serving its web UI). Read the raw body first so
301
- // we can categorize an HTML page distinctly from a malformed-JSON parse_error.
302
- const rawOkBody = await response.text();
303
- if (isHtmlResponse(rawOkBody)) {
304
- throw new LlmCallError(`LLM provider returned HTML instead of JSON (${response.status}) ${config.endpoint}: ${redactSensitiveText(htmlExcerpt(rawOkBody), resolvedKey ? [resolvedKey] : [])}`, "provider_html_error", response.status);
305
- }
306
- let json;
307
- try {
308
- json = JSON.parse(rawOkBody);
335
+ if (isHtmlResponse(rawOkBody)) {
336
+ throw new LlmCallError(`LLM provider returned HTML instead of JSON (${response.status}) ${config.endpoint}: ${redactSensitiveText(htmlExcerpt(rawOkBody), resolvedKey ? [resolvedKey] : [])}`, "provider_html_error", response.status);
337
+ }
338
+ let json;
339
+ try {
340
+ json = JSON.parse(rawOkBody);
341
+ }
342
+ catch {
343
+ throw new LlmCallError(`LLM response was not valid JSON ${config.endpoint}: ${redactSensitiveText(redactErrorBody(rawOkBody), resolvedKey ? [resolvedKey] : [])}`, "parse_error", response.status);
344
+ }
345
+ const responseModel = typeof json.model === "string" && json.model.trim().length > 0 ? json.model : undefined;
346
+ terminalFields = {
347
+ model: responseModel ?? config.model,
348
+ modelSource: responseModel === undefined ? "configured" : "response",
349
+ finishReason: typeof json.choices?.[0]?.finish_reason === "string" ? json.choices[0].finish_reason : undefined,
350
+ ...extractUsageTokens(json.usage),
351
+ };
352
+ const content = (json.choices?.[0]?.message?.content ?? "").trim();
353
+ const reasoning = (json.choices?.[0]?.message?.reasoning_content ?? "").trim();
354
+ const result = redactSensitiveText(content || reasoning, resolvedKey ? [resolvedKey] : []);
355
+ emitLlmUsage({
356
+ ...terminalFields,
357
+ outcome: "success",
358
+ durationMs: Date.now() - requestStartedAt,
359
+ });
360
+ return result;
309
361
  }
310
- catch {
311
- throw new LlmCallError(`LLM response was not valid JSON ${config.endpoint}: ${redactSensitiveText(redactErrorBody(rawOkBody), resolvedKey ? [resolvedKey] : [])}`, "parse_error", response.status);
362
+ catch (err) {
363
+ emitLlmUsage({
364
+ ...terminalFields,
365
+ outcome: "error",
366
+ durationMs: Date.now() - requestStartedAt,
367
+ errorCode: err instanceof LlmCallError ? err.code : "unknown_error",
368
+ });
369
+ throw err;
312
370
  }
313
- // Per-call usage telemetry (#576). Best-effort and fully isolated: a missing
314
- // or garbled usage block still records duration + model, and a throwing sink
315
- // can never fail the call (emitLlmUsage swallows its own errors). The stage
316
- // is supplied ambiently by emitLlmUsage; no `stage` param is threaded here.
317
- emitLlmUsage({
318
- model: typeof json.model === "string" && json.model ? json.model : config.model,
319
- durationMs: Date.now() - requestStartedAt,
320
- finishReason: typeof json.choices?.[0]?.finish_reason === "string" ? json.choices[0].finish_reason : undefined,
321
- ...extractUsageTokens(json.usage),
322
- });
323
- const content = (json.choices?.[0]?.message?.content ?? "").trim();
324
- const reasoning = (json.choices?.[0]?.message?.reasoning_content ?? "").trim();
325
- return redactSensitiveText(content || reasoning, resolvedKey ? [resolvedKey] : []);
326
- }
327
- /**
328
- * Strip `<think>` blocks, code fences, and escape control characters in JSON
329
- * strings. Thin wrapper kept for backward compatibility with call sites that
330
- * import `stripJsonFences` from this module. New code should prefer the
331
- * granular helpers from `../core/parse`.
332
- */
333
- export function stripJsonFences(raw) {
334
- return escapeJsonStringControls(stripCodeFences(stripThinkBlocks(raw)));
335
371
  }
336
372
  // ── Availability check ──────────────────────────────────────────────────────
337
373
  /**
@@ -7,7 +7,7 @@
7
7
  * Calls the configured `/embeddings` endpoint and L2-normalizes the returned
8
8
  * vectors so the scoring pipeline's L2-to-cosine conversion is correct.
9
9
  */
10
- import { fetchWithTimeout, isHttpUrl } from "../../core/common.js";
10
+ import { fetchWithTimeout, isHttpUrl, readBodyWithByteCap } from "../../core/common.js";
11
11
  import { resolveSecret } from "../../core/config/config.js";
12
12
  const DEFAULT_REMOTE_BATCH_SIZE = 100;
13
13
  /** Cheap token estimator: 4 chars ≈ 1 token. Used in verbose logging and error messages. */
@@ -49,10 +49,14 @@ export class RemoteEmbedder {
49
49
  body: JSON.stringify(body),
50
50
  }, 30_000, signal);
51
51
  if (!response.ok) {
52
- const errBody = await response.text().catch(() => "");
52
+ const errBody = await readBodyWithByteCap(response, undefined, { bodyTimeoutMs: 30_000, signal }).catch((err) => {
53
+ if (signal?.aborted)
54
+ throw err;
55
+ return "";
56
+ });
53
57
  throw new Error(`Embedding request failed (${response.status}): ${errBody}`);
54
58
  }
55
- const json = (await response.json());
59
+ const json = JSON.parse(await readBodyWithByteCap(response, undefined, { bodyTimeoutMs: 30_000, signal }));
56
60
  if (!json.data?.[0]?.embedding) {
57
61
  throw new Error(`Unexpected embedding response format: missing data[0].embedding.${embeddingEndpointPathHint(this.endpoint)}`);
58
62
  }
@@ -85,10 +89,14 @@ export class RemoteEmbedder {
85
89
  body: JSON.stringify(body),
86
90
  }, 30_000, signal);
87
91
  if (!response.ok) {
88
- const respBody = await response.text().catch(() => "");
92
+ const respBody = await readBodyWithByteCap(response, undefined, { bodyTimeoutMs: 30_000, signal }).catch((err) => {
93
+ if (signal?.aborted)
94
+ throw err;
95
+ return "";
96
+ });
89
97
  throw new Error(`Embedding batch request failed (${response.status}): ${respBody}`);
90
98
  }
91
- const json = (await response.json());
99
+ const json = JSON.parse(await readBodyWithByteCap(response, undefined, { bodyTimeoutMs: 30_000, signal }));
92
100
  if (!json.data || json.data.length !== batch.length) {
93
101
  throw new Error(`Unexpected embedding batch response: expected ${batch.length} embeddings, got ${json.data?.length ?? 0}.${embeddingEndpointPathHint(this.endpoint)}`);
94
102
  }
@@ -3,16 +3,11 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  /**
5
5
  * For each feature key, return the effective enabled state by reading the
6
- * 0.9.0 config shape. Defaults match the legacy `LlmFeatureFlags` docstrings.
6
+ * 0.9.0 config shape.
7
7
  */
8
- // Defaults below mirror the legacy LlmFeatureFlags docstrings so existing
9
- // behaviour is preserved when a config is silent on a flag.
10
8
  const FEATURE_LOCATION = {
11
- // Legacy default: true
12
9
  memory_inference: (cfg) => cfg.index?.memory?.enabled ?? true,
13
- // Legacy default: true
14
10
  graph_extraction: (cfg) => cfg.index?.graph?.enabled ?? true,
15
- // Legacy default: false
16
11
  metadata_enhance: (cfg) => cfg.index?.metadataEnhance?.enabled ?? false,
17
12
  // Always on at the LLM-wrapper level. Enablement is decided ONCE at the
18
13
  // extract entry point (`akmExtract`): the `extract.enabled` process toggle
@@ -26,8 +21,7 @@ const FEATURE_LOCATION = {
26
21
  /**
27
22
  * Pure predicate: is the named feature gate enabled in `config`?
28
23
  *
29
- * Reads from the unified 0.9.0 config shape. Defaults follow the legacy
30
- * `LlmFeatureFlags` docstring defaults.
24
+ * Reads from the unified 0.9.0 config shape.
31
25
  */
32
26
  export function isLlmFeatureEnabled(config, feature, improveEnabled) {
33
27
  if (!config)
@@ -71,10 +65,8 @@ export async function tryLlmFeature(feature, config, fn, fallback, opts) {
71
65
  }
72
66
  }
73
67
  /**
74
- * Section-agnostic process gate. After the 0.8.0 migration, the canonical
75
- * accessor is the `FEATURE_LOCATION` map above; this helper exists so older
76
- * call sites that knew the (section, processName) pair don't all need to
77
- * relearn the new mapping.
68
+ * Section-agnostic process gate that maps process identifiers to their current
69
+ * config locations.
78
70
  *
79
71
  * For unknown (section, processName) pairs the result is `false`.
80
72
  */