akm-cli 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/{cli.js → src/cli.js} +712 -34
  3. package/dist/{commands → src/commands}/config-cli.js +47 -4
  4. package/dist/src/commands/distill.js +283 -0
  5. package/dist/src/commands/events.js +108 -0
  6. package/dist/src/commands/history.js +191 -0
  7. package/dist/{commands → src/commands}/installed-stashes.js +1 -1
  8. package/dist/src/commands/proposal.js +119 -0
  9. package/dist/src/commands/propose.js +171 -0
  10. package/dist/src/commands/reflect.js +193 -0
  11. package/dist/{commands → src/commands}/registry-search.js +71 -7
  12. package/dist/{commands → src/commands}/remember.js +12 -0
  13. package/dist/{commands → src/commands}/search.js +104 -4
  14. package/dist/{commands → src/commands}/self-update.js +4 -3
  15. package/dist/{commands → src/commands}/show.js +73 -0
  16. package/dist/{commands → src/commands}/source-add.js +5 -1
  17. package/dist/{commands → src/commands}/source-manage.js +7 -1
  18. package/dist/{core → src/core}/asset-ref.js +5 -5
  19. package/dist/{core → src/core}/asset-spec.js +12 -0
  20. package/dist/{core → src/core}/common.js +1 -1
  21. package/dist/{core → src/core}/config.js +203 -121
  22. package/dist/{core → src/core}/errors.js +4 -0
  23. package/dist/src/core/events.js +239 -0
  24. package/dist/src/core/lesson-lint.js +86 -0
  25. package/dist/src/core/proposals.js +406 -0
  26. package/dist/src/core/warn.js +72 -0
  27. package/dist/{core → src/core}/write-source.js +80 -5
  28. package/dist/{indexer → src/indexer}/db-search.js +114 -24
  29. package/dist/{indexer → src/indexer}/db.js +76 -23
  30. package/dist/{indexer → src/indexer}/file-context.js +0 -3
  31. package/dist/src/indexer/graph-boost.js +179 -0
  32. package/dist/src/indexer/graph-extraction.js +212 -0
  33. package/dist/{indexer → src/indexer}/indexer.js +88 -7
  34. package/dist/{indexer → src/indexer}/matchers.js +1 -1
  35. package/dist/src/indexer/memory-inference.js +263 -0
  36. package/dist/{indexer → src/indexer}/metadata.js +111 -3
  37. package/dist/{indexer → src/indexer}/search-source.js +4 -2
  38. package/dist/src/integrations/agent/config.js +292 -0
  39. package/dist/src/integrations/agent/detect.js +94 -0
  40. package/dist/src/integrations/agent/index.js +17 -0
  41. package/dist/src/integrations/agent/profiles.js +65 -0
  42. package/dist/src/integrations/agent/prompts.js +167 -0
  43. package/dist/src/integrations/agent/spawn.js +272 -0
  44. package/dist/{integrations → src/integrations}/github.js +9 -3
  45. package/dist/{integrations → src/integrations}/lockfile.js +0 -26
  46. package/dist/{llm → src/llm}/client.js +33 -2
  47. package/dist/{llm → src/llm}/embedders/remote.js +37 -3
  48. package/dist/src/llm/feature-gate.js +108 -0
  49. package/dist/src/llm/graph-extract.js +107 -0
  50. package/dist/src/llm/index-passes.js +35 -0
  51. package/dist/src/llm/memory-infer.js +86 -0
  52. package/dist/{output → src/output}/cli-hints.js +15 -2
  53. package/dist/{output → src/output}/renderers.js +63 -2
  54. package/dist/src/output/shapes.js +523 -0
  55. package/dist/src/output/text.js +1116 -0
  56. package/dist/{registry → src/registry}/build-index.js +19 -8
  57. package/dist/{registry → src/registry}/factory.js +0 -8
  58. package/dist/{registry → src/registry}/providers/static-index.js +6 -3
  59. package/dist/{registry → src/registry}/resolve.js +68 -2
  60. package/dist/{setup → src/setup}/setup.js +52 -5
  61. package/dist/{sources → src/sources}/providers/git.js +7 -15
  62. package/dist/{wiki → src/wiki}/wiki.js +54 -6
  63. package/dist/{workflows → src/workflows}/runs.js +37 -3
  64. package/dist/tests/add-website-source.test.js +119 -0
  65. package/dist/tests/agent/agent-config-loader.test.js +70 -0
  66. package/dist/tests/agent/agent-config.test.js +221 -0
  67. package/dist/tests/agent/agent-detect.test.js +100 -0
  68. package/dist/tests/agent/agent-spawn.test.js +234 -0
  69. package/dist/tests/agent-output.test.js +186 -0
  70. package/dist/tests/architecture/agent-no-llm-sdk-guard.test.js +103 -0
  71. package/dist/tests/architecture/agent-spawn-seam.test.js +193 -0
  72. package/dist/tests/architecture/llm-stateless-seam.test.js +112 -0
  73. package/dist/tests/asset-ref.test.js +192 -0
  74. package/dist/tests/asset-registry.test.js +103 -0
  75. package/dist/tests/asset-spec.test.js +241 -0
  76. package/dist/tests/bench/attribution.test.js +996 -0
  77. package/dist/tests/bench/cleanup-sigint.test.js +83 -0
  78. package/dist/tests/bench/cleanup.js +234 -0
  79. package/dist/tests/bench/cleanup.test.js +166 -0
  80. package/dist/tests/bench/cli.js +1018 -0
  81. package/dist/tests/bench/cli.test.js +445 -0
  82. package/dist/tests/bench/compare.test.js +556 -0
  83. package/dist/tests/bench/corpus.js +317 -0
  84. package/dist/tests/bench/corpus.test.js +258 -0
  85. package/dist/tests/bench/doctor.js +525 -0
  86. package/dist/tests/bench/driver.js +401 -0
  87. package/dist/tests/bench/driver.test.js +584 -0
  88. package/dist/tests/bench/environment.js +233 -0
  89. package/dist/tests/bench/environment.test.js +199 -0
  90. package/dist/tests/bench/evolve-metrics.js +179 -0
  91. package/dist/tests/bench/evolve-metrics.test.js +187 -0
  92. package/dist/tests/bench/evolve.js +647 -0
  93. package/dist/tests/bench/evolve.test.js +624 -0
  94. package/dist/tests/bench/failure-modes.test.js +349 -0
  95. package/dist/tests/bench/feedback-integrity.test.js +457 -0
  96. package/dist/tests/bench/leakage.test.js +228 -0
  97. package/dist/tests/bench/learning-curve.test.js +134 -0
  98. package/dist/tests/bench/metrics.js +2395 -0
  99. package/dist/tests/bench/metrics.test.js +1150 -0
  100. package/dist/tests/bench/no-os-tmpdir-invariant.test.js +43 -0
  101. package/dist/tests/bench/opencode-config.js +194 -0
  102. package/dist/tests/bench/opencode-config.test.js +370 -0
  103. package/dist/tests/bench/report.js +1885 -0
  104. package/dist/tests/bench/report.test.js +1038 -0
  105. package/dist/tests/bench/run-config.js +355 -0
  106. package/dist/tests/bench/run-config.test.js +298 -0
  107. package/dist/tests/bench/run-curate-test.js +32 -0
  108. package/dist/tests/bench/run-failing-tasks.js +56 -0
  109. package/dist/tests/bench/run-full-bench.js +51 -0
  110. package/dist/tests/bench/run-items36-targeted.js +69 -0
  111. package/dist/tests/bench/run-nano-quick.js +42 -0
  112. package/dist/tests/bench/run-waveg-targeted.js +62 -0
  113. package/dist/tests/bench/runner.js +699 -0
  114. package/dist/tests/bench/runner.test.js +958 -0
  115. package/dist/tests/bench/search-bridge.test.js +331 -0
  116. package/dist/tests/bench/tmp.js +131 -0
  117. package/dist/tests/bench/trajectory.js +116 -0
  118. package/dist/tests/bench/trajectory.test.js +127 -0
  119. package/dist/tests/bench/verifier.js +114 -0
  120. package/dist/tests/bench/verifier.test.js +118 -0
  121. package/dist/tests/bench/workflow-evaluator.js +557 -0
  122. package/dist/tests/bench/workflow-evaluator.test.js +421 -0
  123. package/dist/tests/bench/workflow-spec.js +345 -0
  124. package/dist/tests/bench/workflow-spec.test.js +363 -0
  125. package/dist/tests/bench/workflow-trace.js +472 -0
  126. package/dist/tests/bench/workflow-trace.test.js +254 -0
  127. package/dist/tests/benchmark-search-quality.js +536 -0
  128. package/dist/tests/benchmark-suite.js +1441 -0
  129. package/dist/tests/capture-cli.test.js +112 -0
  130. package/dist/tests/cli-errors.test.js +204 -0
  131. package/dist/tests/commands/events.test.js +370 -0
  132. package/dist/tests/commands/history.test.js +418 -0
  133. package/dist/tests/commands/import.test.js +103 -0
  134. package/dist/tests/commands/proposal-cli.test.js +209 -0
  135. package/dist/tests/commands/reflect-propose-cli.test.js +333 -0
  136. package/dist/tests/commands/remember.test.js +97 -0
  137. package/dist/tests/commands/scope-flags.test.js +300 -0
  138. package/dist/tests/commands/search.test.js +537 -0
  139. package/dist/tests/commands/show-indexer-parity.test.js +117 -0
  140. package/dist/tests/commands/show.test.js +294 -0
  141. package/dist/tests/common.test.js +266 -0
  142. package/dist/tests/completions.test.js +142 -0
  143. package/dist/tests/config-cli.test.js +193 -0
  144. package/dist/tests/config-llm-features.test.js +139 -0
  145. package/dist/tests/config.test.js +569 -0
  146. package/dist/tests/contracts/migration-baseline.test.js +43 -0
  147. package/dist/tests/contracts/reflect-propose-envelope.test.js +139 -0
  148. package/dist/tests/contracts/spec-helpers.js +46 -0
  149. package/dist/tests/contracts/v1-spec-section-11-proposal-queue.test.js +228 -0
  150. package/dist/tests/contracts/v1-spec-section-12-agent-config.test.js +56 -0
  151. package/dist/tests/contracts/v1-spec-section-13-lesson-type.test.js +34 -0
  152. package/dist/tests/contracts/v1-spec-section-14-llm-features.test.js +94 -0
  153. package/dist/tests/contracts/v1-spec-section-4-1-asset-types.test.js +39 -0
  154. package/dist/tests/contracts/v1-spec-section-4-2-quality-rules.test.js +44 -0
  155. package/dist/tests/contracts/v1-spec-section-5-configuration.test.js +47 -0
  156. package/dist/tests/contracts/v1-spec-section-6-orchestration.test.js +40 -0
  157. package/dist/tests/contracts/v1-spec-section-7-module-layout.test.js +58 -0
  158. package/dist/tests/contracts/v1-spec-section-8-extension-points.test.js +34 -0
  159. package/dist/tests/contracts/v1-spec-section-9-4-cli-surface.test.js +75 -0
  160. package/dist/tests/contracts/v1-spec-section-9-7-llm-agent-boundary.test.js +36 -0
  161. package/dist/tests/core/write-source.test.js +366 -0
  162. package/dist/tests/curate-command.test.js +87 -0
  163. package/dist/tests/db-scoring.test.js +201 -0
  164. package/dist/tests/db.test.js +654 -0
  165. package/dist/tests/distill-cli-flag.test.js +208 -0
  166. package/dist/tests/distill.test.js +515 -0
  167. package/dist/tests/docker-install.test.js +120 -0
  168. package/dist/tests/e2e.test.js +1419 -0
  169. package/dist/tests/embedder.test.js +340 -0
  170. package/dist/tests/embedding-model-config.test.js +379 -0
  171. package/dist/tests/feedback-command.test.js +172 -0
  172. package/dist/tests/file-context.test.js +552 -0
  173. package/dist/tests/fixtures/scripts/git/summarize-diff.js +9 -0
  174. package/dist/tests/fixtures/scripts/lint/eslint-check.js +7 -0
  175. package/dist/tests/fixtures/stashes/load.js +166 -0
  176. package/dist/tests/fixtures/stashes/load.test.js +97 -0
  177. package/dist/tests/fixtures/stashes/ranking-baseline/scripts/mem0-search.js +12 -0
  178. package/dist/tests/frontmatter.test.js +190 -0
  179. package/dist/tests/fts-field-weighting.test.js +254 -0
  180. package/dist/tests/fuzzy-search.test.js +230 -0
  181. package/dist/tests/git-provider-clone.test.js +45 -0
  182. package/dist/tests/github.test.js +161 -0
  183. package/dist/tests/graph-boost-ranking.test.js +305 -0
  184. package/dist/tests/graph-extraction.test.js +282 -0
  185. package/dist/tests/helpers/usage-events.js +8 -0
  186. package/dist/tests/index-pass-llm.test.js +161 -0
  187. package/dist/tests/indexer.test.js +570 -0
  188. package/dist/tests/info-command.test.js +166 -0
  189. package/dist/tests/init.test.js +69 -0
  190. package/dist/tests/install-script.test.js +246 -0
  191. package/dist/tests/integration/agent-real-profile.test.js +94 -0
  192. package/dist/tests/issue-36-repro.test.js +304 -0
  193. package/dist/tests/issues-191-194.test.js +160 -0
  194. package/dist/tests/lesson-lint.test.js +111 -0
  195. package/dist/tests/llm-client.test.js +115 -0
  196. package/dist/tests/llm-feature-gate.test.js +151 -0
  197. package/dist/tests/llm.test.js +139 -0
  198. package/dist/tests/lockfile.test.js +216 -0
  199. package/dist/tests/manifest.test.js +205 -0
  200. package/dist/tests/markdown.test.js +126 -0
  201. package/dist/tests/matchers-unit.test.js +189 -0
  202. package/dist/tests/memory-inference.test.js +299 -0
  203. package/dist/tests/merge-scoring.test.js +136 -0
  204. package/dist/tests/metadata.test.js +313 -0
  205. package/dist/tests/migration-help.test.js +89 -0
  206. package/dist/tests/origin-resolve.test.js +124 -0
  207. package/dist/tests/output-baseline.test.js +218 -0
  208. package/dist/tests/output-shapes-unit.test.js +478 -0
  209. package/dist/tests/parallel-search.test.js +272 -0
  210. package/dist/tests/parameter-metadata.test.js +365 -0
  211. package/dist/tests/paths.test.js +177 -0
  212. package/dist/tests/progressive-disclosure.test.js +280 -0
  213. package/dist/tests/proposals.test.js +279 -0
  214. package/dist/tests/proposed-quality.test.js +271 -0
  215. package/dist/tests/provider-registry.test.js +32 -0
  216. package/dist/tests/ranking-regression.test.js +548 -0
  217. package/dist/tests/reflect-propose.test.js +455 -0
  218. package/dist/tests/registry-build-index.test.js +394 -0
  219. package/dist/tests/registry-cli.test.js +290 -0
  220. package/dist/tests/registry-index-v2.test.js +430 -0
  221. package/dist/tests/registry-install.test.js +728 -0
  222. package/dist/tests/registry-providers/parity.test.js +189 -0
  223. package/dist/tests/registry-providers/skills-sh.test.js +309 -0
  224. package/dist/tests/registry-providers/static-index.test.js +238 -0
  225. package/dist/tests/registry-resolve.test.js +126 -0
  226. package/dist/tests/registry-search.test.js +923 -0
  227. package/dist/tests/remember-frontmatter.test.js +378 -0
  228. package/dist/tests/remember-unit.test.js +123 -0
  229. package/dist/tests/ripgrep-install.test.js +251 -0
  230. package/dist/tests/ripgrep-resolve.test.js +108 -0
  231. package/dist/tests/ripgrep.test.js +163 -0
  232. package/dist/tests/save-command.test.js +94 -0
  233. package/dist/tests/save-trust-qa-fixes.test.js +270 -0
  234. package/dist/tests/scoring-pipeline.test.js +648 -0
  235. package/dist/tests/search-include-proposed-cli.test.js +118 -0
  236. package/dist/tests/self-update.test.js +442 -0
  237. package/dist/tests/semantic-search-e2e.test.js +512 -0
  238. package/dist/tests/semantic-status.test.js +471 -0
  239. package/dist/tests/setup-run.integration.js +877 -0
  240. package/dist/tests/setup-wizard.test.js +198 -0
  241. package/dist/tests/setup.test.js +131 -0
  242. package/dist/tests/source-add.test.js +11 -0
  243. package/dist/tests/source-clone.test.js +254 -0
  244. package/dist/tests/source-manage.test.js +366 -0
  245. package/dist/tests/source-providers/filesystem.test.js +82 -0
  246. package/dist/tests/source-providers/git.test.js +252 -0
  247. package/dist/tests/source-providers/website.test.js +128 -0
  248. package/dist/tests/source-qa-fixes.test.js +286 -0
  249. package/dist/tests/source-registry.test.js +350 -0
  250. package/dist/tests/source-resolve.test.js +100 -0
  251. package/dist/tests/source-source.test.js +281 -0
  252. package/dist/tests/source.test.js +533 -0
  253. package/dist/tests/tar-utils-scan.test.js +73 -0
  254. package/dist/tests/toggle-components.test.js +73 -0
  255. package/dist/tests/usage-telemetry.test.js +265 -0
  256. package/dist/tests/utility-scoring.test.js +558 -0
  257. package/dist/tests/vault-load-error.test.js +78 -0
  258. package/dist/tests/vault-qa-fixes.test.js +194 -0
  259. package/dist/tests/vault.test.js +429 -0
  260. package/dist/tests/vector-search.test.js +608 -0
  261. package/dist/tests/walker.test.js +252 -0
  262. package/dist/tests/wave2-cluster-bc.test.js +228 -0
  263. package/dist/tests/wave2-cluster-d.test.js +180 -0
  264. package/dist/tests/wave2-cluster-e.test.js +179 -0
  265. package/dist/tests/wiki-qa-fixes.test.js +270 -0
  266. package/dist/tests/wiki.test.js +529 -0
  267. package/dist/tests/workflow-cli.test.js +271 -0
  268. package/dist/tests/workflow-markdown.test.js +171 -0
  269. package/dist/tests/workflow-path-escape.test.js +132 -0
  270. package/dist/tests/workflow-qa-fixes.test.js +395 -0
  271. package/dist/tests/workflows/indexer-rejection.test.js +213 -0
  272. package/docs/README.md +8 -0
  273. package/docs/migration/release-notes/0.7.0.md +244 -0
  274. package/package.json +2 -2
  275. package/dist/core/warn.js +0 -27
  276. package/dist/output/shapes.js +0 -212
  277. package/dist/output/text.js +0 -520
  278. /package/dist/{commands → src/commands}/completions.js +0 -0
  279. /package/dist/{commands → src/commands}/curate.js +0 -0
  280. /package/dist/{commands → src/commands}/info.js +0 -0
  281. /package/dist/{commands → src/commands}/init.js +0 -0
  282. /package/dist/{commands → src/commands}/install-audit.js +0 -0
  283. /package/dist/{commands → src/commands}/migration-help.js +0 -0
  284. /package/dist/{commands → src/commands}/source-clone.js +0 -0
  285. /package/dist/{commands → src/commands}/vault.js +0 -0
  286. /package/dist/{core → src/core}/asset-registry.js +0 -0
  287. /package/dist/{core → src/core}/frontmatter.js +0 -0
  288. /package/dist/{core → src/core}/markdown.js +0 -0
  289. /package/dist/{core → src/core}/paths.js +0 -0
  290. /package/dist/{indexer → src/indexer}/manifest.js +0 -0
  291. /package/dist/{indexer → src/indexer}/search-fields.js +0 -0
  292. /package/dist/{indexer → src/indexer}/semantic-status.js +0 -0
  293. /package/dist/{indexer → src/indexer}/usage-events.js +0 -0
  294. /package/dist/{indexer → src/indexer}/walker.js +0 -0
  295. /package/dist/{llm → src/llm}/embedder.js +0 -0
  296. /package/dist/{llm → src/llm}/embedders/cache.js +0 -0
  297. /package/dist/{llm → src/llm}/embedders/local.js +0 -0
  298. /package/dist/{llm → src/llm}/embedders/types.js +0 -0
  299. /package/dist/{llm → src/llm}/metadata-enhance.js +0 -0
  300. /package/dist/{output → src/output}/context.js +0 -0
  301. /package/dist/{registry → src/registry}/create-provider-registry.js +0 -0
  302. /package/dist/{registry → src/registry}/origin-resolve.js +0 -0
  303. /package/dist/{registry → src/registry}/providers/index.js +0 -0
  304. /package/dist/{registry → src/registry}/providers/skills-sh.js +0 -0
  305. /package/dist/{registry → src/registry}/providers/types.js +0 -0
  306. /package/dist/{registry → src/registry}/types.js +0 -0
  307. /package/dist/{setup → src/setup}/detect.js +0 -0
  308. /package/dist/{setup → src/setup}/ripgrep-install.js +0 -0
  309. /package/dist/{setup → src/setup}/ripgrep-resolve.js +0 -0
  310. /package/dist/{setup → src/setup}/steps.js +0 -0
  311. /package/dist/{sources → src/sources}/include.js +0 -0
  312. /package/dist/{sources → src/sources}/provider-factory.js +0 -0
  313. /package/dist/{sources → src/sources}/provider.js +0 -0
  314. /package/dist/{sources → src/sources}/providers/filesystem.js +0 -0
  315. /package/dist/{sources → src/sources}/providers/index.js +0 -0
  316. /package/dist/{sources → src/sources}/providers/install-types.js +0 -0
  317. /package/dist/{sources → src/sources}/providers/npm.js +0 -0
  318. /package/dist/{sources → src/sources}/providers/provider-utils.js +0 -0
  319. /package/dist/{sources → src/sources}/providers/sync-from-ref.js +0 -0
  320. /package/dist/{sources → src/sources}/providers/tar-utils.js +0 -0
  321. /package/dist/{sources → src/sources}/providers/website.js +0 -0
  322. /package/dist/{sources → src/sources}/resolve.js +0 -0
  323. /package/dist/{sources → src/sources}/types.js +0 -0
  324. /package/dist/{templates → src/templates}/wiki-templates.js +0 -0
  325. /package/dist/{version.js → src/version.js} +0 -0
  326. /package/dist/{workflows → src/workflows}/authoring.js +0 -0
  327. /package/dist/{workflows → src/workflows}/cli.js +0 -0
  328. /package/dist/{workflows → src/workflows}/db.js +0 -0
  329. /package/dist/{workflows → src/workflows}/document-cache.js +0 -0
  330. /package/dist/{workflows → src/workflows}/parser.js +0 -0
  331. /package/dist/{workflows → src/workflows}/renderer.js +0 -0
  332. /package/dist/{workflows → src/workflows}/schema.js +0 -0
  333. /package/dist/{workflows → src/workflows}/validator.js +0 -0
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Append-only events stream — `events.jsonl` (#204).
3
+ *
4
+ * Every mutating CLI verb funnels through `appendEvent` so external
5
+ * observers (sync, replication, audit, dashboards) can react to stash
6
+ * changes by tailing a single file. The file is plain newline-delimited
7
+ * JSON; each line is a self-contained event envelope.
8
+ *
9
+ * The helper is the only thing in akm that writes to events.jsonl. It
10
+ * accepts injectable `now()` and `path` so tests can pin time and use a
11
+ * tmpdir without any global mutation.
12
+ *
13
+ * Format (each line):
14
+ * { "schemaVersion": 1, "id": <number>, "ts": "<ISO>",
15
+ * "eventType": "<verb>", "ref"?: "<asset-ref>", ... }
16
+ *
17
+ * - `id` is a monotonic integer per file. We use the file's pre-write
18
+ * byte length as a durable cursor for `--since` (stable across processes
19
+ * because every appender holds an O_APPEND write). Callers can also pass
20
+ * a string ISO timestamp to `--since` and we filter by `ts >= since`.
21
+ * - `ts` is ISO-8601 (UTC, millisecond precision).
22
+ *
23
+ * The event `id` is derived at read time (line index) — the file itself
24
+ * is the source of truth, so the writer never has to coordinate with a
25
+ * counter. Tail consumers can persist a byte offset (durable cursor).
26
+ */
27
+ import fs from "node:fs";
28
+ import path from "node:path";
29
+ import { getCacheDir } from "./paths";
30
+ /**
31
+ * Default events.jsonl location: `<cacheDir>/events.jsonl`.
32
+ *
33
+ * Env-isolation caveat: `getCacheDir()` reads `XDG_CACHE_HOME` at the time of
34
+ * each call. Two cooperating processes (e.g. one writing events, one tailing)
35
+ * MUST inherit the same `XDG_CACHE_HOME` or they will read/write different
36
+ * `events.jsonl` files. This is the same env-isolation behaviour as the rest
37
+ * of akm — config, indexes, and caches all key off XDG paths — so set
38
+ * `XDG_CACHE_HOME` consistently across processes that share the events bus.
39
+ */
40
+ export function getEventsPath() {
41
+ return path.join(getCacheDir(), "events.jsonl");
42
+ }
43
+ function resolvePath(ctx) {
44
+ return ctx?.filePath ?? getEventsPath();
45
+ }
46
+ function resolveNow(ctx) {
47
+ return ctx?.now ?? Date.now;
48
+ }
49
+ /**
50
+ * Append a single event. Best-effort: a write failure is logged once to
51
+ * stderr but never propagates — observability must not break mutation.
52
+ *
53
+ * The id field is intentionally omitted on write (the line index is the
54
+ * id; the reader assigns it). Keeping it off the wire avoids a coordination
55
+ * step between concurrent appenders.
56
+ */
57
+ export function appendEvent(input, ctx) {
58
+ const filePath = resolvePath(ctx);
59
+ const now = resolveNow(ctx);
60
+ const ts = new Date(now()).toISOString();
61
+ const envelope = {
62
+ schemaVersion: 1,
63
+ ts,
64
+ eventType: input.eventType,
65
+ ...(input.ref !== undefined ? { ref: input.ref } : {}),
66
+ ...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
67
+ };
68
+ const line = `${JSON.stringify(envelope)}\n`;
69
+ try {
70
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
71
+ // O_APPEND guarantees atomic appends ≤ PIPE_BUF (4 KiB on Linux); our
72
+ // events are well under that ceiling, so concurrent processes can write
73
+ // safely without locking. `appendFileSync` opens with `'a'` which sets
74
+ // O_APPEND.
75
+ fs.appendFileSync(filePath, line, { encoding: "utf8" });
76
+ }
77
+ catch (err) {
78
+ // Best-effort: events stream failures must not break the mutating verb.
79
+ // Surface once to stderr so operators can diagnose.
80
+ const message = err instanceof Error ? err.message : String(err);
81
+ process.stderr.write(`akm: events.jsonl append failed (${message})\n`);
82
+ }
83
+ }
84
+ /**
85
+ * Read all events matching the filter. Returns a `nextOffset` that callers
86
+ * can persist between processes for monotonic resumption — `sinceOffset`
87
+ * is the durable cursor referenced in the acceptance criteria.
88
+ */
89
+ export function readEvents(options = {}, ctx) {
90
+ const filePath = resolvePath(ctx);
91
+ if (!fs.existsSync(filePath)) {
92
+ return { events: [], nextOffset: 0 };
93
+ }
94
+ const stat = fs.statSync(filePath);
95
+ const startOffset = options.sinceOffset && options.sinceOffset > 0 ? options.sinceOffset : 0;
96
+ if (startOffset >= stat.size) {
97
+ return { events: [], nextOffset: stat.size };
98
+ }
99
+ const fd = fs.openSync(filePath, "r");
100
+ try {
101
+ const length = stat.size - startOffset;
102
+ const buf = Buffer.alloc(length);
103
+ fs.readSync(fd, buf, 0, length, startOffset);
104
+ const text = buf.toString("utf8");
105
+ const events = parseEventLines(text, options, startOffset);
106
+ return { events, nextOffset: stat.size };
107
+ }
108
+ finally {
109
+ fs.closeSync(fd);
110
+ }
111
+ }
112
+ function parseEventLines(text, options, startOffset) {
113
+ // Each line that ends with \n is a complete event. A trailing partial
114
+ // line (no terminating \n) is ignored — the next read will pick it up
115
+ // once it is fully written.
116
+ const out = [];
117
+ let lineStart = 0;
118
+ // The envelope id is the 1-based line index across the whole file. We
119
+ // approximate that here as the line index from the start of the read
120
+ // window plus a synthetic offset — for callers using `--since`, the
121
+ // absolute id is less useful than the byte cursor anyway. To keep ids
122
+ // monotonic across reads we use absolute byte position as a stable
123
+ // surrogate identifier.
124
+ for (let i = 0; i < text.length; i += 1) {
125
+ if (text.charCodeAt(i) !== 10 /* \n */)
126
+ continue;
127
+ const line = text.slice(lineStart, i);
128
+ const absStart = startOffset + lineStart;
129
+ lineStart = i + 1;
130
+ if (!line.trim())
131
+ continue;
132
+ let parsed;
133
+ try {
134
+ parsed = JSON.parse(line);
135
+ }
136
+ catch {
137
+ // Skip malformed lines — better than crashing the read pipeline.
138
+ continue;
139
+ }
140
+ const envelope = {
141
+ schemaVersion: 1,
142
+ id: absStart,
143
+ ts: typeof parsed.ts === "string" ? parsed.ts : "",
144
+ eventType: typeof parsed.eventType === "string" ? parsed.eventType : "unknown",
145
+ ...(typeof parsed.ref === "string" ? { ref: parsed.ref } : {}),
146
+ ...(parsed.metadata !== undefined ? { metadata: parsed.metadata } : {}),
147
+ };
148
+ if (!matchesFilter(envelope, options))
149
+ continue;
150
+ out.push(envelope);
151
+ }
152
+ return out;
153
+ }
154
+ function matchesFilter(envelope, options) {
155
+ if (options.type && envelope.eventType !== options.type)
156
+ return false;
157
+ if (options.ref && envelope.ref !== options.ref)
158
+ return false;
159
+ if (options.since && envelope.ts && envelope.ts < options.since)
160
+ return false;
161
+ return true;
162
+ }
163
+ /**
164
+ * Follow events.jsonl. Polls at `intervalMs` (default 75ms) and emits
165
+ * every new event to `onEvent`. Resolves when `signal` aborts, when
166
+ * `maxEvents` events have been observed, or when `maxDurationMs` elapses.
167
+ *
168
+ * The polling cursor is byte-offset based, so concurrent writers cannot
169
+ * cause skips: between two reads we always pick up everything appended
170
+ * since the last `nextOffset`.
171
+ */
172
+ export async function tailEvents(options = {}, ctx) {
173
+ const intervalMs = options.intervalMs ?? 75;
174
+ const collected = [];
175
+ let cursor = options.sinceOffset ?? 0;
176
+ // Seed the cursor: if the caller passed --since (timestamp) but no
177
+ // sinceOffset, do an initial filtered read so they see history before
178
+ // we start polling. This matches the documented behaviour of `tail
179
+ // --since`: emit existing events that match, then follow.
180
+ if (options.sinceOffset === undefined) {
181
+ const initial = readEvents({ since: options.since, type: options.type, ref: options.ref }, ctx);
182
+ for (const event of initial.events) {
183
+ collected.push(event);
184
+ options.onEvent?.(event);
185
+ if (options.maxEvents !== undefined && collected.length >= options.maxEvents) {
186
+ return { events: collected, nextOffset: initial.nextOffset, reason: "maxEvents" };
187
+ }
188
+ }
189
+ cursor = initial.nextOffset;
190
+ }
191
+ const startedAt = Date.now();
192
+ return new Promise((resolve) => {
193
+ let resolved = false;
194
+ let timer;
195
+ function finish(reason) {
196
+ if (resolved)
197
+ return;
198
+ resolved = true;
199
+ if (timer)
200
+ clearInterval(timer);
201
+ resolve({ events: collected, nextOffset: cursor, reason });
202
+ }
203
+ function tick() {
204
+ try {
205
+ const result = readEvents({ sinceOffset: cursor, type: options.type, ref: options.ref }, ctx);
206
+ cursor = result.nextOffset;
207
+ for (const event of result.events) {
208
+ // Apply --since filter inside the polling loop too — the cursor is
209
+ // byte-offset so it can hand us events the user filtered out.
210
+ if (options.since && event.ts && event.ts < options.since)
211
+ continue;
212
+ collected.push(event);
213
+ options.onEvent?.(event);
214
+ if (options.maxEvents !== undefined && collected.length >= options.maxEvents) {
215
+ finish("maxEvents");
216
+ return;
217
+ }
218
+ }
219
+ }
220
+ catch {
221
+ // Non-fatal: stay in the loop.
222
+ }
223
+ if (options.maxDurationMs !== undefined && Date.now() - startedAt >= options.maxDurationMs) {
224
+ finish("maxDuration");
225
+ }
226
+ }
227
+ if (options.signal) {
228
+ if (options.signal.aborted) {
229
+ finish("signal");
230
+ return;
231
+ }
232
+ options.signal.addEventListener("abort", () => finish("signal"), { once: true });
233
+ }
234
+ timer = setInterval(tick, intervalMs);
235
+ // Run one tick immediately so callers don't have to wait an interval
236
+ // for events written in the same tick as the tail starts.
237
+ tick();
238
+ });
239
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Deterministic frontmatter lint for `lesson` assets (v1 spec §13).
3
+ *
4
+ * The contract is fixed:
5
+ *
6
+ * - Required: `description` — a non-empty single-line string describing
7
+ * what the lesson teaches.
8
+ * - Required: `when_to_use` — a non-empty single-line string describing
9
+ * the trigger that should make a caller apply the lesson.
10
+ *
11
+ * Lint produces structured findings rather than throwing so callers can
12
+ * batch-validate (e.g. `akm proposal accept` over a queue) and surface every
13
+ * violation in a single pass. A strict wrapper (`assertLessonValid`) throws
14
+ * a `UsageError` for call sites that want a fail-fast contract — most
15
+ * notably the proposal-accept path described in v1 spec §13.1.
16
+ *
17
+ * The lint is intentionally side-effect free and does not import the indexer
18
+ * or filesystem walker; it operates on a single file path + raw string. This
19
+ * lets it run from any code path (CLI, proposal-accept, asset-spec tests)
20
+ * without dragging in the rest of the runtime.
21
+ */
22
+ import fs from "node:fs";
23
+ import { UsageError } from "./errors";
24
+ import { parseFrontmatter } from "./frontmatter";
25
+ function isNonEmptyString(value) {
26
+ return typeof value === "string" && value.trim().length > 0;
27
+ }
28
+ /**
29
+ * Lint a lesson given its raw markdown source.
30
+ *
31
+ * `pathForMessages` is woven into every finding's message so callers can
32
+ * surface the offending file in CLI/proposal flows without having to map
33
+ * findings back to paths separately.
34
+ */
35
+ export function lintLessonContent(raw, pathForMessages) {
36
+ const findings = [];
37
+ const parsed = parseFrontmatter(raw);
38
+ const fm = parsed.data;
39
+ if (!("description" in fm)) {
40
+ findings.push({
41
+ kind: "missing-description",
42
+ field: "description",
43
+ message: `Lesson at ${pathForMessages} is missing required frontmatter field \`description\`.`,
44
+ });
45
+ }
46
+ else if (!isNonEmptyString(fm.description)) {
47
+ findings.push({
48
+ kind: "empty-description",
49
+ field: "description",
50
+ message: `Lesson at ${pathForMessages} has an empty \`description\` frontmatter field; it must be a non-empty single-line string.`,
51
+ });
52
+ }
53
+ if (!("when_to_use" in fm)) {
54
+ findings.push({
55
+ kind: "missing-when_to_use",
56
+ field: "when_to_use",
57
+ message: `Lesson at ${pathForMessages} is missing required frontmatter field \`when_to_use\`.`,
58
+ });
59
+ }
60
+ else if (!isNonEmptyString(fm.when_to_use)) {
61
+ findings.push({
62
+ kind: "empty-when_to_use",
63
+ field: "when_to_use",
64
+ message: `Lesson at ${pathForMessages} has an empty \`when_to_use\` frontmatter field; it must be a non-empty single-line string.`,
65
+ });
66
+ }
67
+ return { path: pathForMessages, findings };
68
+ }
69
+ /** Lint a lesson file on disk. Throws if the file cannot be read. */
70
+ export function lintLessonFile(filePath) {
71
+ const raw = fs.readFileSync(filePath, "utf8");
72
+ return lintLessonContent(raw, filePath);
73
+ }
74
+ /**
75
+ * Strict variant: throws a `UsageError` if any finding is present. The thrown
76
+ * error carries the full set of findings on its message and a hint pointing
77
+ * at v1 spec §13. The first finding's `field` becomes the error's primary
78
+ * field for callers that want to highlight the first violation.
79
+ */
80
+ export function assertLessonValid(filePath) {
81
+ const report = lintLessonFile(filePath);
82
+ if (report.findings.length === 0)
83
+ return;
84
+ const message = report.findings.map((f) => f.message).join("\n");
85
+ throw new UsageError(message, "MISSING_REQUIRED_ARGUMENT", "Lessons require non-empty `description` and `when_to_use` frontmatter fields. See v1 spec §13.");
86
+ }