akm-cli 0.8.0-rc2 → 0.8.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 (295) hide show
  1. package/{.github/CHANGELOG.md → CHANGELOG.md} +191 -3
  2. package/README.md +22 -6
  3. package/SECURITY.md +93 -0
  4. package/dist/cli/config-migrate.js +144 -0
  5. package/dist/cli/config-validate.js +39 -0
  6. package/dist/cli/confirm.js +73 -0
  7. package/dist/cli/parse-args.js +93 -3
  8. package/dist/cli/shared.js +129 -0
  9. package/dist/cli.js +2141 -1268
  10. package/dist/commands/add-cli.js +279 -0
  11. package/dist/commands/agent-dispatch.js +20 -12
  12. package/dist/commands/agent-support.js +11 -5
  13. package/dist/commands/completions.js +3 -0
  14. package/dist/commands/config-cli.js +129 -517
  15. package/dist/commands/consolidate.js +1533 -144
  16. package/dist/commands/curate.js +44 -3
  17. package/dist/commands/db-cli.js +23 -0
  18. package/dist/commands/distill-promotion-policy.js +5 -3
  19. package/dist/commands/distill.js +906 -100
  20. package/dist/commands/env.js +213 -0
  21. package/dist/commands/eval-cases.js +3 -0
  22. package/dist/commands/events.js +3 -0
  23. package/dist/commands/extract-cli.js +127 -0
  24. package/dist/commands/extract-prompt.js +204 -0
  25. package/dist/commands/extract.js +477 -0
  26. package/dist/commands/feedback-cli.js +331 -0
  27. package/dist/commands/graph.js +260 -5
  28. package/dist/commands/health.js +977 -51
  29. package/dist/commands/help/help-accept.md +6 -3
  30. package/dist/commands/help/help-improve.md +36 -8
  31. package/dist/commands/help/help-proposals.md +7 -4
  32. package/dist/commands/help/help-reject.md +5 -2
  33. package/dist/commands/history.js +51 -16
  34. package/dist/commands/improve-auto-accept.js +97 -0
  35. package/dist/commands/improve-cli.js +236 -0
  36. package/dist/commands/improve-profiles.js +184 -0
  37. package/dist/commands/improve-result-file.js +167 -0
  38. package/dist/commands/improve.js +1725 -332
  39. package/dist/commands/info.js +3 -0
  40. package/dist/commands/init.js +49 -1
  41. package/dist/commands/installed-stashes.js +6 -23
  42. package/dist/commands/knowledge.js +3 -0
  43. package/dist/commands/lint/agent-linter.js +3 -0
  44. package/dist/commands/lint/base-linter.js +199 -5
  45. package/dist/commands/lint/command-linter.js +3 -0
  46. package/dist/commands/lint/default-linter.js +3 -0
  47. package/dist/commands/lint/env-key-rules.js +154 -0
  48. package/dist/commands/lint/index.js +92 -3
  49. package/dist/commands/lint/knowledge-linter.js +3 -0
  50. package/dist/commands/lint/markdown-insertion.js +343 -0
  51. package/dist/commands/lint/memory-linter.js +3 -0
  52. package/dist/commands/lint/registry.js +3 -0
  53. package/dist/commands/lint/skill-linter.js +3 -0
  54. package/dist/commands/lint/task-linter.js +15 -12
  55. package/dist/commands/lint/types.js +3 -0
  56. package/dist/commands/lint/workflow-linter.js +3 -0
  57. package/dist/commands/lint.js +3 -0
  58. package/dist/commands/migration-help.js +5 -2
  59. package/dist/commands/proposal-drain-policies.js +128 -0
  60. package/dist/commands/proposal-drain.js +477 -0
  61. package/dist/commands/proposal.js +60 -6
  62. package/dist/commands/propose.js +24 -19
  63. package/dist/commands/reflect.js +1004 -94
  64. package/dist/commands/registry-cli.js +150 -0
  65. package/dist/commands/registry-search.js +3 -0
  66. package/dist/commands/remember-cli.js +257 -0
  67. package/dist/commands/remember.js +15 -6
  68. package/dist/commands/schema-repair.js +88 -15
  69. package/dist/commands/search.js +99 -14
  70. package/dist/commands/secret.js +173 -0
  71. package/dist/commands/self-update.js +3 -0
  72. package/dist/commands/show.js +32 -13
  73. package/dist/commands/source-add.js +7 -35
  74. package/dist/commands/source-clone.js +3 -0
  75. package/dist/commands/source-manage.js +3 -0
  76. package/dist/commands/tasks.js +161 -95
  77. package/dist/commands/url-checker.js +3 -0
  78. package/dist/core/action-contributors.js +3 -0
  79. package/dist/core/asset-ref.js +13 -2
  80. package/dist/core/asset-registry.js +9 -2
  81. package/dist/core/asset-serialize.js +88 -0
  82. package/dist/core/asset-spec.js +61 -5
  83. package/dist/core/common.js +93 -5
  84. package/dist/core/concurrent.js +3 -0
  85. package/dist/core/config-io.js +347 -0
  86. package/dist/core/config-migration.js +622 -0
  87. package/dist/core/config-schema.js +558 -0
  88. package/dist/core/config-sources.js +108 -0
  89. package/dist/core/config-types.js +4 -0
  90. package/dist/core/config-walker.js +337 -0
  91. package/dist/core/config.js +366 -1077
  92. package/dist/core/errors.js +42 -20
  93. package/dist/core/events.js +31 -25
  94. package/dist/core/file-lock.js +104 -0
  95. package/dist/core/frontmatter.js +75 -10
  96. package/dist/core/lesson-lint.js +3 -0
  97. package/dist/core/markdown.js +3 -0
  98. package/dist/core/memory-belief.js +62 -0
  99. package/dist/core/memory-contradiction-detect.js +274 -0
  100. package/dist/core/memory-improve.js +142 -14
  101. package/dist/core/parse.js +3 -0
  102. package/dist/core/paths.js +218 -50
  103. package/dist/core/proposal-quality-validators.js +380 -0
  104. package/dist/core/proposal-validators.js +11 -3
  105. package/dist/core/proposals.js +464 -5
  106. package/dist/core/state-db.js +349 -56
  107. package/dist/core/text-truncation.js +107 -0
  108. package/dist/core/time.js +3 -0
  109. package/dist/core/tty.js +59 -0
  110. package/dist/core/warn.js +7 -2
  111. package/dist/core/write-source.js +12 -0
  112. package/dist/indexer/db-backup.js +391 -0
  113. package/dist/indexer/db-search.js +136 -28
  114. package/dist/indexer/db.js +661 -166
  115. package/dist/indexer/ensure-index.js +3 -0
  116. package/dist/indexer/file-context.js +3 -0
  117. package/dist/indexer/graph-boost.js +162 -40
  118. package/dist/indexer/graph-db.js +241 -51
  119. package/dist/indexer/graph-dedup.js +3 -7
  120. package/dist/indexer/graph-extraction.js +242 -149
  121. package/dist/indexer/index-context.js +3 -9
  122. package/dist/indexer/indexer.js +84 -14
  123. package/dist/indexer/llm-cache.js +24 -19
  124. package/dist/indexer/manifest.js +3 -0
  125. package/dist/indexer/matchers.js +184 -11
  126. package/dist/indexer/memory-inference.js +94 -50
  127. package/dist/indexer/metadata-contributors.js +3 -0
  128. package/dist/indexer/metadata.js +110 -50
  129. package/dist/indexer/path-resolver.js +3 -0
  130. package/dist/indexer/project-context.js +192 -0
  131. package/dist/indexer/ranking-contributors.js +134 -7
  132. package/dist/indexer/ranking.js +8 -1
  133. package/dist/indexer/search-fields.js +5 -9
  134. package/dist/indexer/search-hit-enrichers.js +91 -2
  135. package/dist/indexer/search-source.js +20 -1
  136. package/dist/indexer/semantic-status.js +4 -1
  137. package/dist/indexer/staleness-detect.js +447 -0
  138. package/dist/indexer/usage-events.js +12 -9
  139. package/dist/indexer/walker.js +3 -0
  140. package/dist/integrations/agent/builders.js +135 -0
  141. package/dist/integrations/agent/config.js +121 -401
  142. package/dist/integrations/agent/detect.js +3 -0
  143. package/dist/integrations/agent/index.js +6 -14
  144. package/dist/integrations/agent/model-aliases.js +55 -0
  145. package/dist/integrations/agent/profiles.js +3 -0
  146. package/dist/integrations/agent/prompts.js +137 -8
  147. package/dist/integrations/agent/runner.js +208 -0
  148. package/dist/integrations/agent/sdk-runner.js +8 -2
  149. package/dist/integrations/agent/spawn.js +54 -14
  150. package/dist/integrations/github.js +3 -0
  151. package/dist/integrations/lockfile.js +22 -51
  152. package/dist/integrations/session-logs/index.js +4 -0
  153. package/dist/integrations/session-logs/inline-refs.js +35 -0
  154. package/dist/integrations/session-logs/pre-filter.js +152 -0
  155. package/dist/integrations/session-logs/providers/claude-code.js +226 -0
  156. package/dist/integrations/session-logs/providers/opencode.js +231 -25
  157. package/dist/integrations/session-logs/types.js +3 -0
  158. package/dist/llm/call-ai.js +14 -26
  159. package/dist/llm/client.js +16 -2
  160. package/dist/llm/embedder.js +20 -29
  161. package/dist/llm/embedders/cache.js +3 -7
  162. package/dist/llm/embedders/local.js +42 -1
  163. package/dist/llm/embedders/remote.js +20 -8
  164. package/dist/llm/embedders/types.js +3 -7
  165. package/dist/llm/feature-gate.js +92 -56
  166. package/dist/llm/graph-extract.js +401 -30
  167. package/dist/llm/index-passes.js +44 -29
  168. package/dist/llm/memory-infer.js +30 -2
  169. package/dist/llm/metadata-enhance.js +3 -7
  170. package/dist/llm/prompts/extract-session.md +80 -0
  171. package/dist/llm/prompts/graph-extract-user-prompt.md +24 -1
  172. package/dist/output/cli-hints-full.md +60 -32
  173. package/dist/output/cli-hints-short.md +10 -7
  174. package/dist/output/cli-hints.js +5 -2
  175. package/dist/output/context.js +60 -8
  176. package/dist/output/renderers.js +170 -194
  177. package/dist/output/shapes/curate.js +56 -0
  178. package/dist/output/shapes/distill.js +10 -0
  179. package/dist/output/shapes/env-list.js +19 -0
  180. package/dist/output/shapes/events.js +11 -0
  181. package/dist/output/shapes/helpers.js +424 -0
  182. package/dist/output/shapes/history.js +7 -0
  183. package/dist/output/shapes/passthrough.js +105 -0
  184. package/dist/output/shapes/proposal-accept.js +7 -0
  185. package/dist/output/shapes/proposal-diff.js +7 -0
  186. package/dist/output/shapes/proposal-list.js +7 -0
  187. package/dist/output/shapes/proposal-producer.js +11 -0
  188. package/dist/output/shapes/proposal-reject.js +7 -0
  189. package/dist/output/shapes/proposal-show.js +7 -0
  190. package/dist/output/shapes/registry-search.js +6 -0
  191. package/dist/output/shapes/registry.js +30 -0
  192. package/dist/output/shapes/search.js +6 -0
  193. package/dist/output/shapes/secret-list.js +19 -0
  194. package/dist/output/shapes/show.js +6 -0
  195. package/dist/output/shapes/vault-list.js +19 -0
  196. package/dist/output/shapes.js +51 -549
  197. package/dist/output/text/add.js +6 -0
  198. package/dist/output/text/clone.js +6 -0
  199. package/dist/output/text/config.js +6 -0
  200. package/dist/output/text/curate.js +6 -0
  201. package/dist/output/text/distill.js +7 -0
  202. package/dist/output/text/enable-disable.js +7 -0
  203. package/dist/output/text/events.js +10 -0
  204. package/dist/output/text/feedback.js +6 -0
  205. package/dist/output/text/helpers.js +1059 -0
  206. package/dist/output/text/history.js +7 -0
  207. package/dist/output/text/import.js +6 -0
  208. package/dist/output/text/index.js +6 -0
  209. package/dist/output/text/info.js +6 -0
  210. package/dist/output/text/init.js +6 -0
  211. package/dist/output/text/list.js +6 -0
  212. package/dist/output/text/proposal-producer.js +8 -0
  213. package/dist/output/text/proposal.js +12 -0
  214. package/dist/output/text/registry-commands.js +11 -0
  215. package/dist/output/text/registry.js +30 -0
  216. package/dist/output/text/remember.js +6 -0
  217. package/dist/output/text/remove.js +6 -0
  218. package/dist/output/text/save.js +6 -0
  219. package/dist/output/text/search.js +6 -0
  220. package/dist/output/text/show.js +6 -0
  221. package/dist/output/text/update.js +6 -0
  222. package/dist/output/text/upgrade.js +6 -0
  223. package/dist/output/text/vault.js +16 -0
  224. package/dist/output/text/wiki.js +15 -0
  225. package/dist/output/text/workflow.js +14 -0
  226. package/dist/output/text.js +44 -1329
  227. package/dist/registry/build-index.js +3 -0
  228. package/dist/registry/create-provider-registry.js +3 -0
  229. package/dist/registry/factory.js +4 -1
  230. package/dist/registry/origin-resolve.js +3 -0
  231. package/dist/registry/providers/index.js +3 -0
  232. package/dist/registry/providers/skills-sh.js +11 -2
  233. package/dist/registry/providers/static-index.js +10 -1
  234. package/dist/registry/providers/types.js +3 -24
  235. package/dist/registry/resolve.js +11 -16
  236. package/dist/registry/types.js +3 -0
  237. package/dist/scripts/migrate-storage.js +17767 -0
  238. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
  239. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  240. package/dist/setup/detect.js +3 -0
  241. package/dist/setup/ripgrep-install.js +3 -0
  242. package/dist/setup/ripgrep-resolve.js +3 -0
  243. package/dist/setup/setup.js +306 -67
  244. package/dist/setup/steps.js +3 -15
  245. package/dist/sources/include.js +3 -0
  246. package/dist/sources/provider-factory.js +3 -11
  247. package/dist/sources/provider.js +3 -20
  248. package/dist/sources/providers/filesystem.js +19 -23
  249. package/dist/sources/providers/git.js +171 -21
  250. package/dist/sources/providers/index.js +3 -0
  251. package/dist/sources/providers/install-types.js +3 -13
  252. package/dist/sources/providers/npm.js +3 -4
  253. package/dist/sources/providers/provider-utils.js +3 -0
  254. package/dist/sources/providers/sync-from-ref.js +3 -11
  255. package/dist/sources/providers/tar-utils.js +3 -0
  256. package/dist/sources/providers/website.js +18 -22
  257. package/dist/sources/resolve.js +3 -0
  258. package/dist/sources/types.js +3 -0
  259. package/dist/sources/website-ingest.js +3 -0
  260. package/dist/tasks/backends/cron.js +3 -0
  261. package/dist/tasks/backends/exec-utils.js +3 -0
  262. package/dist/tasks/backends/index.js +3 -11
  263. package/dist/tasks/backends/launchd.js +3 -0
  264. package/dist/tasks/backends/schtasks.js +3 -0
  265. package/dist/tasks/parser.js +51 -38
  266. package/dist/tasks/resolveAkmBin.js +3 -0
  267. package/dist/tasks/runner.js +35 -9
  268. package/dist/tasks/schedule.js +20 -1
  269. package/dist/tasks/schema.js +5 -3
  270. package/dist/tasks/validator.js +6 -3
  271. package/dist/version.js +3 -0
  272. package/dist/wiki/wiki-templates.js +3 -0
  273. package/dist/wiki/wiki.js +3 -0
  274. package/dist/workflows/authoring.js +3 -0
  275. package/dist/workflows/cli.js +3 -0
  276. package/dist/workflows/db.js +140 -10
  277. package/dist/workflows/document-cache.js +3 -10
  278. package/dist/workflows/parser.js +3 -0
  279. package/dist/workflows/renderer.js +3 -0
  280. package/dist/workflows/runs.js +18 -1
  281. package/dist/workflows/schema.js +3 -0
  282. package/dist/workflows/scope-key.js +3 -0
  283. package/dist/workflows/validator.js +5 -9
  284. package/docs/README.md +7 -2
  285. package/docs/data-and-telemetry.md +225 -0
  286. package/docs/migration/release-notes/0.7.5.md +2 -2
  287. package/docs/migration/release-notes/0.8.0.md +57 -5
  288. package/docs/migration/v0.7-to-v0.8.md +1378 -0
  289. package/package.json +28 -11
  290. package/.github/LICENSE +0 -374
  291. package/dist/commands/install-audit.js +0 -385
  292. package/dist/commands/vault.js +0 -310
  293. package/dist/indexer/match-contributors.js +0 -141
  294. package/dist/integrations/agent/pipeline.js +0 -39
  295. package/dist/integrations/agent/runners.js +0 -31
@@ -1,37 +1,27 @@
1
- /**
2
- * Typed error classes for structured exit code classification.
3
- *
4
- * - ConfigError -> exit 78 (configuration / environment problems)
5
- * - UsageError -> exit 2 (bad CLI arguments or invalid input)
6
- * - NotFoundError -> exit 1 (requested resource missing)
7
- *
8
- * Each error carries a machine-readable `code` field. Codes are stable
9
- * identifiers safe to consume from scripts and JSON output. Existing throw
10
- * sites without an explicit code receive a default code per error class so
11
- * older call sites continue to compile and behave unchanged.
12
- *
13
- * Each error also exposes a `hint()` method returning an actionable hint
14
- * string (or `undefined`). Hints can be supplied at construction time or
15
- * derived from the error `code` via the per-class default mapping below.
16
- * The CLI surfaces this via `error.hint()` rather than message-regex parsing.
17
- */
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
18
4
  /**
19
5
  * Default hint for each ConfigError code. Keep these short, actionable, and
20
6
  * imperative. Returning undefined means "no canned hint".
21
7
  */
22
8
  const CONFIG_HINTS = {
23
- STASH_DIR_NOT_FOUND: "Run `akm init` to create the default stash, or set stashDir in your config.",
9
+ STASH_DIR_NOT_FOUND: "Run `akm setup` to create and configure your stash, or set stashDir in your config.",
24
10
  STASH_DIR_NOT_A_DIRECTORY: "The configured stashDir exists but isn't a directory. Update stashDir to point at a folder.",
25
11
  STASH_DIR_UNREADABLE: "Check the path exists and your user has read permission, or update stashDir.",
26
12
  EMBEDDING_NOT_CONFIGURED: 'Run `akm config set embedding \'{"endpoint":"...","model":"..."}\'` to enable embeddings.',
27
- LLM_NOT_CONFIGURED: 'Run `akm config set llm \'{"endpoint":"...","model":"..."}\'` to configure the LLM.',
13
+ LLM_NOT_CONFIGURED: 'Run `akm setup` or `akm config set profiles.llm.default \'{"endpoint":"...","model":"..."}\' to configure an LLM profile.',
14
+ TEST_ISOLATION_MISSING: "Under bun test, when AKM_STASH_DIR is set you MUST also set XDG_DATA_HOME (or AKM_DATA_DIR) and XDG_STATE_HOME (or AKM_STATE_DIR) to temp directories so the test does not touch the developer's real ~/.local/share/akm or ~/.local/state/akm.",
15
+ SETUP_TMP_STASH_REFUSED: "Use a persistent directory, or set AKM_FORCE_SETUP_TMP_STASH=1 to opt in to a sandboxed setup (setup also pre-sets AKM_STASH_DIR so config and cache writes auto-isolate into $stashDir/.akm/ — host config is preserved).",
16
+ UNSAFE_STASH_DIR: "Choose a path inside your home directory (e.g. ~/akm) or another empty workspace. The stash directory cannot be the filesystem root, your home directory itself, or a sensitive system path like /etc, /var, ~/.config, or ~/.ssh.",
28
17
  };
29
18
  /** Default hint for each UsageError code. */
30
19
  const USAGE_HINTS = {
31
20
  INVALID_FLAG_VALUE: "Run `akm <command> --help` to see accepted values.",
32
21
  INVALID_SOURCE_VALUE: "Pick one of: stash, registry, both.",
33
22
  INVALID_FORMAT_VALUE: "Pick one of: json, jsonl, text, yaml.",
34
- INVALID_DETAIL_VALUE: "Pick one of: brief, normal, full, summary, agent.",
23
+ INVALID_DETAIL_VALUE: "Pick one of: brief, normal, full. For agent/summary projections use --shape.",
24
+ INVALID_SHAPE_VALUE: "Pick one of: human, agent, summary (summary is only valid on `akm show`).",
35
25
  INVALID_JSON_CONFIG_VALUE: 'Quote JSON values in your shell, for example: akm config set embedding \'{"endpoint":"http://localhost:11434/v1/embeddings","model":"nomic-embed-text"}\'.',
36
26
  MISSING_OR_AMBIGUOUS_TARGET: "Use `akm update --all` or pass a target like `akm update npm:@scope/pkg` (not both).",
37
27
  TARGET_NOT_UPDATABLE: "Run `akm list` to view your sources, then retry with one of those values.",
@@ -92,3 +82,35 @@ export class NotFoundError extends Error {
92
82
  return this._hint ?? NOT_FOUND_HINTS[this.code];
93
83
  }
94
84
  }
85
+ /**
86
+ * Test-isolation guard helper.
87
+ *
88
+ * `src/core/paths.ts` throws `ConfigError("TEST_ISOLATION_MISSING")` under
89
+ * `bun test` when `AKM_STASH_DIR` is set without a paired data-dir or
90
+ * state-dir override. That throw must never be swallowed by best-effort
91
+ * catches around DB/data-dir operations — otherwise the guard's loud failure
92
+ * silently degrades into a "no result" outcome (cold cache, missing snapshot,
93
+ * etc.) and the underlying test leak goes undetected.
94
+ *
95
+ * Call `rethrowIfTestIsolationError(err)` from any catch block that returns
96
+ * a fallback value (null, [], empty result) after touching DB or data-dir
97
+ * paths. It re-throws when the caught error is the guard violation, otherwise
98
+ * does nothing so the existing benign-fallback path can proceed unchanged.
99
+ *
100
+ * Usage:
101
+ * try {
102
+ * const db = openDatabase();
103
+ * // ...
104
+ * } catch (err) {
105
+ * rethrowIfTestIsolationError(err);
106
+ * // existing benign-fallback handling
107
+ * }
108
+ */
109
+ export function isTestIsolationError(err) {
110
+ return err instanceof ConfigError && err.code === "TEST_ISOLATION_MISSING";
111
+ }
112
+ export function rethrowIfTestIsolationError(err) {
113
+ if (isTestIsolationError(err)) {
114
+ throw err;
115
+ }
116
+ }
@@ -1,27 +1,8 @@
1
- /**
2
- * Append-only events stream backed by state.db (#204, Phase 3).
3
- *
4
- * Every mutating CLI verb funnels through `appendEvent` so external
5
- * observers (sync, replication, audit, dashboards) can react to stash
6
- * changes. Events are stored in the `events` table in `state.db`
7
- * (SQLite, WAL mode) instead of a flat `events.jsonl` file.
8
- *
9
- * The helper is the only thing in akm that writes to the events table. It
10
- * accepts an injectable `dbPath` (via `EventsContext`) so tests can pin a
11
- * tmpdir without any global mutation.
12
- *
13
- * Format (each EventEnvelope):
14
- * { "schemaVersion": 1, "id": <number>, "ts": "<ISO>",
15
- * "eventType": "<verb>", "ref"?: "<asset-ref>", ... }
16
- *
17
- * - `id` is a monotonic SQLite AUTOINCREMENT rowid. Callers can persist it
18
- * as a durable cursor for `--since` resumption (replaces the old byte-offset
19
- * cursor). The public API still surfaces this as `nextOffset` (an opaque
20
- * number) for backward compatibility with callers that stored byte-offset
21
- * cursors.
22
- * - `ts` is ISO-8601 (UTC, millisecond precision).
23
- */
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
24
4
  import path from "node:path";
5
+ import { rethrowIfTestIsolationError } from "./errors";
25
6
  import { getDataDir } from "./paths";
26
7
  import { insertEvent, openStateDatabase, readStateEvents } from "./state-db";
27
8
  import { error } from "./warn";
@@ -51,11 +32,32 @@ function resolveNow(ctx) {
51
32
  * stderr but never propagates — observability must not break mutation.
52
33
  *
53
34
  * Events are written exclusively to the `events` table in `state.db`.
35
+ *
36
+ * I1: when `ctx.db` is provided (a pre-opened long-lived connection), the
37
+ * function writes directly to that handle without opening or closing the DB.
38
+ * This eliminates per-event open/migrate/close overhead for high-frequency
39
+ * callers such as `akmImprove`.
54
40
  */
55
41
  export function appendEvent(input, ctx) {
56
- const dbPath = resolveDbPath(ctx);
57
42
  const now = resolveNow(ctx);
58
43
  const ts = new Date(now()).toISOString();
44
+ // Fast path: caller provided a long-lived connection — use it directly.
45
+ if (ctx?.db) {
46
+ try {
47
+ insertEvent(ctx.db, {
48
+ eventType: input.eventType,
49
+ ts,
50
+ ref: input.ref,
51
+ metadata: input.metadata,
52
+ });
53
+ }
54
+ catch (err) {
55
+ error(`akm: appendEvent failed: ${String(err)}`);
56
+ }
57
+ return;
58
+ }
59
+ // Default path: open, insert, close.
60
+ const dbPath = resolveDbPath(ctx);
59
61
  try {
60
62
  const db = openStateDatabase(dbPath);
61
63
  try {
@@ -71,6 +73,8 @@ export function appendEvent(input, ctx) {
71
73
  }
72
74
  }
73
75
  catch (err) {
76
+ // Never mask the bun-test isolation guard as a silent "events failed".
77
+ rethrowIfTestIsolationError(err);
74
78
  // Best-effort: events stream failures must not break the mutating verb.
75
79
  // Surface once to stderr so operators can diagnose.
76
80
  error(`akm: appendEvent failed: ${String(err)}`);
@@ -86,7 +90,9 @@ export function readEvents(options = {}, ctx) {
86
90
  try {
87
91
  db = openStateDatabase(dbPath);
88
92
  }
89
- catch {
93
+ catch (err) {
94
+ // Never mask the bun-test isolation guard as "no events".
95
+ rethrowIfTestIsolationError(err);
90
96
  // DB does not exist yet or cannot be opened — return empty result.
91
97
  return { events: [], nextOffset: 0 };
92
98
  }
@@ -0,0 +1,104 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import fs from "node:fs";
5
+ import { isProcessAlive } from "./common";
6
+ /**
7
+ * Atomically create a sentinel at `lockPath` with `payload` as the body.
8
+ * Returns true if we now own the lock, false if a sentinel already
9
+ * exists (EEXIST). Throws any other error (permissions, missing parent
10
+ * dir, etc.) — callers must ensure the parent directory exists.
11
+ *
12
+ * `payload` is typically `String(process.pid)` for the simple cases or
13
+ * a small JSON envelope for callers that want richer metadata
14
+ * (improve.ts records pid + startedAt so audit can correlate runs).
15
+ */
16
+ export function tryAcquireLockSync(lockPath, payload) {
17
+ try {
18
+ fs.writeFileSync(lockPath, payload, { flag: "wx" });
19
+ return true;
20
+ }
21
+ catch (err) {
22
+ if (err.code === "EEXIST")
23
+ return false;
24
+ throw err;
25
+ }
26
+ }
27
+ /**
28
+ * Inspect an existing sentinel at `lockPath` without modifying it.
29
+ * Returns:
30
+ * - `absent` if the file does not exist.
31
+ * - `stale` if the file is present but should be reclaimed (the holding
32
+ * PID is dead, the content is unparseable, or the lock has exceeded
33
+ * `staleAfterMs`). Includes the failure reason so callers can log it.
34
+ * - `held` if the lock has a live holder and is not yet age-expired.
35
+ *
36
+ * Does NOT remove the file. Callers decide recovery policy.
37
+ */
38
+ export function probeLock(lockPath, opts) {
39
+ let rawContent;
40
+ let ageMs;
41
+ try {
42
+ rawContent = fs.readFileSync(lockPath, "utf8");
43
+ }
44
+ catch (err) {
45
+ if (err.code === "ENOENT")
46
+ return { state: "absent" };
47
+ return { state: "stale", reason: "unreadable" };
48
+ }
49
+ try {
50
+ const stat = fs.statSync(lockPath);
51
+ ageMs = Date.now() - stat.mtimeMs;
52
+ }
53
+ catch {
54
+ // Stat failed even though read succeeded — race-y removal in flight.
55
+ return { state: "stale", reason: "unreadable", rawContent };
56
+ }
57
+ const holderPid = extractHolderPid(rawContent);
58
+ if (holderPid === undefined) {
59
+ return { state: "stale", reason: "invalid_pid", ageMs, rawContent };
60
+ }
61
+ if (!isProcessAlive(holderPid)) {
62
+ return { state: "stale", reason: "pid_dead", holderPid, ageMs, rawContent };
63
+ }
64
+ if (opts?.staleAfterMs !== undefined && ageMs > opts.staleAfterMs) {
65
+ return { state: "stale", reason: "age_exceeded", holderPid, ageMs, rawContent };
66
+ }
67
+ return { state: "held", holderPid, ageMs, rawContent };
68
+ }
69
+ /**
70
+ * Remove a lock file. Idempotent — silently ignores ENOENT. Used both to
71
+ * reclaim stale locks (after probeLock returns `state: "stale"`) and to
72
+ * release locks we own (after a successful tryAcquireLockSync).
73
+ */
74
+ export function releaseLock(lockPath) {
75
+ try {
76
+ fs.unlinkSync(lockPath);
77
+ }
78
+ catch {
79
+ // Sentinel already gone — fine.
80
+ }
81
+ }
82
+ /**
83
+ * Extract a PID from a sentinel body. Accepts the two shapes used across
84
+ * the codebase: a bare numeric string (config-io, vault, lockfile) and
85
+ * a JSON object with a `pid` field (improve). Returns undefined when the
86
+ * body is unparseable or yields a non-positive integer.
87
+ */
88
+ function extractHolderPid(content) {
89
+ const trimmed = content.trim();
90
+ if (!trimmed)
91
+ return undefined;
92
+ if (trimmed.startsWith("{")) {
93
+ try {
94
+ const parsed = JSON.parse(trimmed);
95
+ const pid = typeof parsed.pid === "number" ? parsed.pid : Number.NaN;
96
+ return Number.isInteger(pid) && pid > 0 ? pid : undefined;
97
+ }
98
+ catch {
99
+ return undefined;
100
+ }
101
+ }
102
+ const pid = Number.parseInt(trimmed, 10);
103
+ return Number.isInteger(pid) && pid > 0 ? pid : undefined;
104
+ }
@@ -1,10 +1,12 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
1
4
  /**
2
5
  * Shared frontmatter parsing utilities.
3
6
  *
4
7
  * Provides a single, canonical YAML-subset frontmatter parser used by both
5
8
  * the stash open logic and the metadata generator.
6
9
  */
7
- import { asNonEmptyString } from "./common";
8
10
  /**
9
11
  * Parse YAML-subset frontmatter from a Markdown (or similar) string.
10
12
  *
@@ -19,6 +21,9 @@ import { asNonEmptyString } from "./common";
19
21
  * booleans, or numbers.
20
22
  * - **No nested objects beyond one level**: Only a single level of indented
21
23
  * key-value pairs is supported.
24
+ * - **Block scalars**: `|` (literal), `|-` (strip), and `|+` (keep) block
25
+ * scalars are supported for multi-line string values as emitted by the
26
+ * `yaml` library's `stringify`.
22
27
  */
23
28
  export function parseFrontmatter(raw) {
24
29
  const parsedBlock = parseFrontmatterBlock(raw);
@@ -27,10 +32,18 @@ export function parseFrontmatter(raw) {
27
32
  }
28
33
  const data = {};
29
34
  let currentKey = null;
30
- /** "scalar" | "list" | "object" | "pending" — "pending" means empty value, mode determined by next line */
35
+ /**
36
+ * "scalar" | "list" | "object" | "pending" | "block" —
37
+ * "pending" means empty value, mode determined by next line.
38
+ * "block" means we are accumulating lines for a `|`-block scalar.
39
+ */
31
40
  let mode = "scalar";
32
41
  let nested = null;
33
42
  let currentList = null;
43
+ /** Lines collected while in "block" mode. */
44
+ let blockLines = null;
45
+ /** Block scalar chomping: "clip" (|), "strip" (|-), "keep" (|+). */
46
+ let blockChomping = "clip";
34
47
  const flushPending = () => {
35
48
  // Called when we start a new top-level key and the previous key was still "pending".
36
49
  // An empty-value key followed by another top-level key means it was an empty scalar.
@@ -38,7 +51,41 @@ export function parseFrontmatter(raw) {
38
51
  data[currentKey] = "";
39
52
  }
40
53
  };
54
+ const flushBlock = () => {
55
+ // Commit the accumulated block-scalar lines to `data[currentKey]`.
56
+ if (mode !== "block" || currentKey === null || blockLines === null)
57
+ return;
58
+ // De-indent: strip the common 2-space prefix `yaml.stringify` emits.
59
+ const deindented = blockLines.map((l) => (l.startsWith(" ") ? l.slice(2) : l));
60
+ // Chomping: apply trailing-newline policy.
61
+ // "clip" (|): single trailing newline.
62
+ // "strip" (|-): no trailing newline.
63
+ // "keep" (|+): keep all trailing newlines as-is.
64
+ if (blockChomping === "keep") {
65
+ data[currentKey] = deindented.join("\n");
66
+ }
67
+ else if (blockChomping === "strip") {
68
+ data[currentKey] = deindented.join("\n").replace(/\n+$/, "");
69
+ }
70
+ else {
71
+ // "clip": exactly one trailing newline
72
+ data[currentKey] = `${deindented.join("\n").replace(/\n+$/, "")}\n`;
73
+ }
74
+ };
41
75
  for (const line of parsedBlock.frontmatter.split(/\r?\n/)) {
76
+ // If we are in block-scalar mode, collect indented lines or end the block.
77
+ if (mode === "block") {
78
+ if (line.startsWith(" ") || line === "") {
79
+ // Continuation of the block scalar (indented content or blank line).
80
+ blockLines.push(line);
81
+ continue;
82
+ }
83
+ // Non-indented line ends the block scalar — flush and fall through to
84
+ // parse the new line as a top-level key.
85
+ flushBlock();
86
+ mode = "scalar";
87
+ blockLines = null;
88
+ }
42
89
  // Block-sequence item: "- value" or " - value" (optional 2-space indent)
43
90
  // Only match when the current key is in list or pending mode.
44
91
  const seqItem = line.match(/^(?: {2})?- (.*)$/);
@@ -52,6 +99,18 @@ export function parseFrontmatter(raw) {
52
99
  currentList.push(parseYamlScalar(seqItem[1].trim()));
53
100
  continue;
54
101
  }
102
+ // Plain-style multi-line scalar continuation: a 2-space-indented line that
103
+ // is not a sequence item or nested key. YAML plain scalars fold newlines
104
+ // into a single space, so we append with a space. This handles LLM-emitted
105
+ // descriptions like:
106
+ // description: Use 4-colon outer containers when mixing
107
+ // nesting depths in markdown-it-container plugins.
108
+ // Without this, only the first line is captured and the truncation
109
+ // heuristic wrongly flags it as cut off mid-sentence.
110
+ if (mode === "scalar" && currentKey !== null && /^ {2}\S/.test(line)) {
111
+ data[currentKey] = `${String(data[currentKey])} ${line.trim()}`;
112
+ continue;
113
+ }
55
114
  // Indented nested key-value (object under a key with empty value)
56
115
  const indented = line.match(/^ {2}(\w[\w-]*):\s*(.+)$/);
57
116
  if (indented && currentKey !== null && (mode === "object" || mode === "pending")) {
@@ -73,7 +132,15 @@ export function parseFrontmatter(raw) {
73
132
  flushPending();
74
133
  currentKey = top[1];
75
134
  const value = top[2].trim();
76
- if (value === "") {
135
+ if (value === "|" || value === "|-" || value === "|+") {
136
+ // Block scalar header — collect subsequent indented lines.
137
+ mode = "block";
138
+ blockLines = [];
139
+ blockChomping = value === "|-" ? "strip" : value === "|+" ? "keep" : "clip";
140
+ nested = null;
141
+ currentList = null;
142
+ }
143
+ else if (value === "") {
77
144
  // Defer mode decision until we see the next line
78
145
  mode = "pending";
79
146
  nested = null;
@@ -84,6 +151,7 @@ export function parseFrontmatter(raw) {
84
151
  // Inline flow array: tags: [ops, networking]
85
152
  mode = "list";
86
153
  nested = null;
154
+ currentList = null;
87
155
  currentList = parseFlowArray(value);
88
156
  data[currentKey] = currentList;
89
157
  }
@@ -94,6 +162,10 @@ export function parseFrontmatter(raw) {
94
162
  data[currentKey] = parseYamlScalar(value);
95
163
  }
96
164
  }
165
+ // Flush any in-progress block scalar at end of frontmatter.
166
+ if (mode === "block") {
167
+ flushBlock();
168
+ }
97
169
  // Flush the last key if it was still pending (empty value, no continuation)
98
170
  flushPending();
99
171
  return {
@@ -151,10 +223,3 @@ export function parseYamlScalar(value) {
151
223
  }
152
224
  return value;
153
225
  }
154
- /**
155
- * Coerce an unknown value to a trimmed string, or return undefined if empty/non-string.
156
- * @deprecated Use `asNonEmptyString` from `core/common` directly.
157
- */
158
- export function toStringOrUndefined(value) {
159
- return asNonEmptyString(value);
160
- }
@@ -1,3 +1,6 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
1
4
  /**
2
5
  * Deterministic frontmatter lint for `lesson` assets (v1 spec §13).
3
6
  *
@@ -1,3 +1,6 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
1
4
  import { parseFrontmatter } from "./frontmatter";
2
5
  // ── Parsing ─────────────────────────────────────────────────────────────────
3
6
  export function parseMarkdownToc(content) {
@@ -0,0 +1,62 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * Shared memory belief-state machinery (C-3 / #382).
6
+ *
7
+ * Extracted from `memory-improve.ts` so both `akmConsolidate` and
8
+ * `analyzeMemoryCleanup` can emit `MemoryBeliefTransitionLogRecord` entries
9
+ * through a unified state-transition model.
10
+ *
11
+ * # Design
12
+ *
13
+ * The 4-state belief lifecycle (active → superseded | contradicted → archived)
14
+ * was previously encoded only in `memory-improve.ts`. `akmConsolidate` used a
15
+ * flat merge/delete/promote model with no belief states, causing the two engines
16
+ * to diverge. This module:
17
+ *
18
+ * 1. Re-exports the belief-state types from `memory-improve.ts` so callers
19
+ * can import from one canonical location.
20
+ * 2. Provides `writeContradictEdge` — a shared primitive that both engines
21
+ * use when an LLM (or heuristic) identifies a contradiction between two
22
+ * memories. This is the bridge between `akmConsolidate`'s LLM-detected
23
+ * contradictions and `resolveFamilyContradictions`' SCC resolver.
24
+ *
25
+ * # References
26
+ *
27
+ * - Zep / Graphiti (arXiv:2501.13956 §3) — unified belief-revision pipeline
28
+ * - MemOS (arXiv:2507.03724) — formal archive/merge/transition with shared state model
29
+ */
30
+ import fs from "node:fs";
31
+ import { assembleAsset } from "./asset-serialize";
32
+ import { parseFrontmatter } from "./frontmatter";
33
+ // ── Contradiction edge writer ─────────────────────────────────────────────────
34
+ /**
35
+ * Write `contradictedBy` and `beliefState: contradicted` edges to a memory
36
+ * file's frontmatter (C-3 / #382).
37
+ *
38
+ * This is the shared primitive used by:
39
+ * - `akmConsolidate` when its LLM plan includes a `contradict` op
40
+ * - `memory-contradiction-detect.ts` for the M-1 automated contradiction pass
41
+ * - `resolveFamilyContradictions` in `memory-improve.ts` for SCC resolution
42
+ *
43
+ * Idempotent: if the `contradictedByRef` is already in `contradictedBy`,
44
+ * the file is not rewritten.
45
+ *
46
+ * @param filePath - Absolute path to the memory markdown file.
47
+ * @param contradictedByRef - The ref that contradicts this memory.
48
+ */
49
+ export function writeContradictEdge(filePath, contradictedByRef) {
50
+ const raw = fs.readFileSync(filePath, "utf8");
51
+ const parsed = parseFrontmatter(raw);
52
+ const existing = Array.isArray(parsed.data.contradictedBy) ? parsed.data.contradictedBy : [];
53
+ if (existing.includes(contradictedByRef))
54
+ return; // Already written — idempotent.
55
+ const nextContradictedBy = [...new Set([...existing, contradictedByRef])].sort();
56
+ const nextFrontmatter = {
57
+ ...parsed.data,
58
+ contradictedBy: nextContradictedBy,
59
+ beliefState: "contradicted",
60
+ };
61
+ fs.writeFileSync(filePath, assembleAsset(nextFrontmatter, parsed.content), "utf8");
62
+ }