akm-cli 0.7.4 → 0.8.0-rc.10

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 (300) hide show
  1. package/CHANGELOG.md +224 -1
  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 +133 -0
  8. package/dist/cli/shared.js +129 -0
  9. package/dist/cli.js +2631 -1440
  10. package/dist/commands/add-cli.js +279 -0
  11. package/dist/commands/agent-dispatch.js +110 -0
  12. package/dist/commands/agent-support.js +68 -0
  13. package/dist/commands/completions.js +3 -0
  14. package/dist/commands/config-cli.js +130 -534
  15. package/dist/commands/consolidate.js +2122 -0
  16. package/dist/commands/curate.js +45 -3
  17. package/dist/commands/db-cli.js +23 -0
  18. package/dist/commands/distill-promotion-policy.js +660 -0
  19. package/dist/commands/distill.js +1081 -73
  20. package/dist/commands/env.js +213 -0
  21. package/dist/commands/eval-cases.js +43 -0
  22. package/dist/commands/events.js +15 -24
  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 +477 -0
  28. package/dist/commands/health.js +1302 -0
  29. package/dist/commands/help/help-accept.md +12 -0
  30. package/dist/commands/help/help-improve.md +69 -0
  31. package/dist/commands/help/help-proposals.md +18 -0
  32. package/dist/commands/help/help-propose.md +17 -0
  33. package/dist/commands/help/help-reject.md +11 -0
  34. package/dist/commands/history.js +54 -46
  35. package/dist/commands/improve-auto-accept.js +97 -0
  36. package/dist/commands/improve-cli.js +217 -0
  37. package/dist/commands/improve-profiles.js +166 -0
  38. package/dist/commands/improve-result-file.js +167 -0
  39. package/dist/commands/improve.js +2373 -0
  40. package/dist/commands/info.js +5 -2
  41. package/dist/commands/init.js +50 -2
  42. package/dist/commands/installed-stashes.js +102 -139
  43. package/dist/commands/knowledge.js +136 -0
  44. package/dist/commands/lint/agent-linter.js +49 -0
  45. package/dist/commands/lint/base-linter.js +479 -0
  46. package/dist/commands/lint/command-linter.js +49 -0
  47. package/dist/commands/lint/default-linter.js +16 -0
  48. package/dist/commands/lint/env-key-rules.js +154 -0
  49. package/dist/commands/lint/index.js +196 -0
  50. package/dist/commands/lint/knowledge-linter.js +16 -0
  51. package/dist/commands/lint/markdown-insertion.js +343 -0
  52. package/dist/commands/lint/memory-linter.js +61 -0
  53. package/dist/commands/lint/registry.js +36 -0
  54. package/dist/commands/lint/skill-linter.js +45 -0
  55. package/dist/commands/lint/task-linter.js +50 -0
  56. package/dist/commands/lint/types.js +4 -0
  57. package/dist/commands/lint/workflow-linter.js +56 -0
  58. package/dist/commands/lint.js +4 -0
  59. package/dist/commands/migration-help.js +3 -0
  60. package/dist/commands/proposal.js +67 -12
  61. package/dist/commands/propose.js +120 -45
  62. package/dist/commands/reflect.js +1104 -60
  63. package/dist/commands/registry-cli.js +150 -0
  64. package/dist/commands/registry-search.js +5 -2
  65. package/dist/commands/remember-cli.js +257 -0
  66. package/dist/commands/remember.js +70 -7
  67. package/dist/commands/schema-repair.js +203 -0
  68. package/dist/commands/search.js +115 -14
  69. package/dist/commands/secret.js +173 -0
  70. package/dist/commands/self-update.js +3 -0
  71. package/dist/commands/show.js +158 -60
  72. package/dist/commands/source-add.js +17 -45
  73. package/dist/commands/source-clone.js +3 -0
  74. package/dist/commands/source-manage.js +14 -19
  75. package/dist/commands/tasks.js +437 -0
  76. package/dist/commands/url-checker.js +42 -0
  77. package/dist/core/action-contributors.js +28 -0
  78. package/dist/core/asset-ref.js +17 -2
  79. package/dist/core/asset-registry.js +12 -17
  80. package/dist/core/asset-serialize.js +88 -0
  81. package/dist/core/asset-spec.js +67 -1
  82. package/dist/core/common.js +182 -0
  83. package/dist/core/concurrent.js +25 -0
  84. package/dist/core/config-io.js +347 -0
  85. package/dist/core/config-migration.js +622 -0
  86. package/dist/core/config-schema.js +534 -0
  87. package/dist/core/config-sources.js +108 -0
  88. package/dist/core/config-types.js +4 -0
  89. package/dist/core/config-walker.js +337 -0
  90. package/dist/core/config.js +364 -968
  91. package/dist/core/errors.js +42 -20
  92. package/dist/core/events.js +105 -135
  93. package/dist/core/file-lock.js +104 -0
  94. package/dist/core/frontmatter.js +75 -8
  95. package/dist/core/lesson-lint.js +3 -0
  96. package/dist/core/markdown.js +20 -0
  97. package/dist/core/memory-belief.js +62 -0
  98. package/dist/core/memory-contradiction-detect.js +274 -0
  99. package/dist/core/memory-improve.js +806 -0
  100. package/dist/core/parse.js +158 -0
  101. package/dist/core/paths.js +280 -14
  102. package/dist/core/proposal-quality-validators.js +380 -0
  103. package/dist/core/proposal-validators.js +69 -0
  104. package/dist/core/proposals.js +512 -42
  105. package/dist/core/state-db.js +1068 -0
  106. package/dist/core/text-truncation.js +107 -0
  107. package/dist/core/time.js +54 -0
  108. package/dist/core/tty.js +59 -0
  109. package/dist/core/warn.js +64 -1
  110. package/dist/core/write-source.js +3 -0
  111. package/dist/indexer/db-backup.js +391 -0
  112. package/dist/indexer/db-search.js +198 -489
  113. package/dist/indexer/db.js +990 -108
  114. package/dist/indexer/ensure-index.js +136 -0
  115. package/dist/indexer/file-context.js +3 -0
  116. package/dist/indexer/graph-boost.js +376 -101
  117. package/dist/indexer/graph-db.js +391 -0
  118. package/dist/indexer/graph-dedup.js +95 -0
  119. package/dist/indexer/graph-extraction.js +550 -114
  120. package/dist/indexer/index-context.js +4 -0
  121. package/dist/indexer/indexer.js +547 -309
  122. package/dist/indexer/llm-cache.js +52 -0
  123. package/dist/indexer/manifest.js +3 -0
  124. package/dist/indexer/matchers.js +167 -160
  125. package/dist/indexer/memory-inference.js +152 -74
  126. package/dist/indexer/metadata-contributors.js +29 -0
  127. package/dist/indexer/metadata.js +275 -196
  128. package/dist/indexer/path-resolver.js +92 -0
  129. package/dist/indexer/project-context.js +192 -0
  130. package/dist/indexer/ranking-contributors.js +331 -0
  131. package/dist/indexer/ranking.js +81 -0
  132. package/dist/indexer/search-fields.js +5 -9
  133. package/dist/indexer/search-hit-enrichers.js +111 -0
  134. package/dist/indexer/search-source.js +44 -10
  135. package/dist/indexer/semantic-status.js +6 -17
  136. package/dist/indexer/staleness-detect.js +447 -0
  137. package/dist/indexer/usage-events.js +12 -9
  138. package/dist/indexer/walker.js +28 -0
  139. package/dist/integrations/agent/builders.js +135 -0
  140. package/dist/integrations/agent/config.js +122 -230
  141. package/dist/integrations/agent/detect.js +3 -0
  142. package/dist/integrations/agent/index.js +7 -13
  143. package/dist/integrations/agent/model-aliases.js +55 -0
  144. package/dist/integrations/agent/profiles.js +70 -5
  145. package/dist/integrations/agent/prompts.js +250 -36
  146. package/dist/integrations/agent/runner.js +151 -0
  147. package/dist/integrations/agent/sdk-runner.js +126 -0
  148. package/dist/integrations/agent/spawn.js +183 -35
  149. package/dist/integrations/github.js +3 -0
  150. package/dist/integrations/lockfile.js +32 -69
  151. package/dist/integrations/session-logs/index.js +69 -0
  152. package/dist/integrations/session-logs/inline-refs.js +35 -0
  153. package/dist/integrations/session-logs/pre-filter.js +152 -0
  154. package/dist/integrations/session-logs/providers/claude-code.js +282 -0
  155. package/dist/integrations/session-logs/providers/opencode.js +258 -0
  156. package/dist/integrations/session-logs/types.js +4 -0
  157. package/dist/llm/call-ai.js +62 -0
  158. package/dist/llm/client.js +79 -88
  159. package/dist/llm/embedder.js +20 -29
  160. package/dist/llm/embedders/cache.js +3 -7
  161. package/dist/llm/embedders/local.js +42 -1
  162. package/dist/llm/embedders/remote.js +20 -8
  163. package/dist/llm/embedders/types.js +3 -7
  164. package/dist/llm/feature-gate.js +95 -48
  165. package/dist/llm/graph-extract.js +676 -72
  166. package/dist/llm/index-passes.js +44 -29
  167. package/dist/llm/memory-infer.js +80 -71
  168. package/dist/llm/metadata-enhance.js +42 -29
  169. package/dist/llm/prompts/extract-session.md +80 -0
  170. package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
  171. package/dist/output/cli-hints-full.md +292 -0
  172. package/dist/output/cli-hints-short.md +66 -0
  173. package/dist/output/cli-hints.js +7 -311
  174. package/dist/output/context.js +60 -8
  175. package/dist/output/renderers.js +306 -258
  176. package/dist/output/shapes/curate.js +56 -0
  177. package/dist/output/shapes/distill.js +10 -0
  178. package/dist/output/shapes/env-list.js +19 -0
  179. package/dist/output/shapes/events.js +11 -0
  180. package/dist/output/shapes/helpers.js +424 -0
  181. package/dist/output/shapes/history.js +7 -0
  182. package/dist/output/shapes/passthrough.js +102 -0
  183. package/dist/output/shapes/proposal-accept.js +7 -0
  184. package/dist/output/shapes/proposal-diff.js +7 -0
  185. package/dist/output/shapes/proposal-list.js +7 -0
  186. package/dist/output/shapes/proposal-producer.js +11 -0
  187. package/dist/output/shapes/proposal-reject.js +7 -0
  188. package/dist/output/shapes/proposal-show.js +7 -0
  189. package/dist/output/shapes/registry-search.js +6 -0
  190. package/dist/output/shapes/registry.js +30 -0
  191. package/dist/output/shapes/search.js +6 -0
  192. package/dist/output/shapes/secret-list.js +19 -0
  193. package/dist/output/shapes/show.js +6 -0
  194. package/dist/output/shapes/vault-list.js +19 -0
  195. package/dist/output/shapes.js +51 -511
  196. package/dist/output/text/add.js +6 -0
  197. package/dist/output/text/clone.js +6 -0
  198. package/dist/output/text/config.js +6 -0
  199. package/dist/output/text/curate.js +6 -0
  200. package/dist/output/text/distill.js +7 -0
  201. package/dist/output/text/enable-disable.js +7 -0
  202. package/dist/output/text/events.js +10 -0
  203. package/dist/output/text/feedback.js +6 -0
  204. package/dist/output/text/helpers.js +1039 -0
  205. package/dist/output/text/history.js +7 -0
  206. package/dist/output/text/import.js +6 -0
  207. package/dist/output/text/index.js +6 -0
  208. package/dist/output/text/info.js +6 -0
  209. package/dist/output/text/init.js +6 -0
  210. package/dist/output/text/list.js +6 -0
  211. package/dist/output/text/proposal-producer.js +8 -0
  212. package/dist/output/text/proposal.js +11 -0
  213. package/dist/output/text/registry-commands.js +11 -0
  214. package/dist/output/text/registry.js +30 -0
  215. package/dist/output/text/remember.js +6 -0
  216. package/dist/output/text/remove.js +6 -0
  217. package/dist/output/text/save.js +6 -0
  218. package/dist/output/text/search.js +6 -0
  219. package/dist/output/text/show.js +6 -0
  220. package/dist/output/text/update.js +6 -0
  221. package/dist/output/text/upgrade.js +6 -0
  222. package/dist/output/text/vault.js +16 -0
  223. package/dist/output/text/wiki.js +15 -0
  224. package/dist/output/text/workflow.js +14 -0
  225. package/dist/output/text.js +44 -1093
  226. package/dist/registry/build-index.js +3 -0
  227. package/dist/registry/create-provider-registry.js +3 -0
  228. package/dist/registry/factory.js +4 -1
  229. package/dist/registry/origin-resolve.js +3 -0
  230. package/dist/registry/providers/index.js +3 -0
  231. package/dist/registry/providers/skills-sh.js +71 -50
  232. package/dist/registry/providers/static-index.js +53 -48
  233. package/dist/registry/providers/types.js +3 -24
  234. package/dist/registry/resolve.js +11 -16
  235. package/dist/registry/types.js +3 -0
  236. package/dist/scripts/migrate-storage.js +17750 -0
  237. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
  238. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  239. package/dist/setup/detect.js +3 -0
  240. package/dist/setup/ripgrep-install.js +3 -0
  241. package/dist/setup/ripgrep-resolve.js +3 -0
  242. package/dist/setup/setup.js +775 -37
  243. package/dist/setup/steps.js +3 -15
  244. package/dist/sources/include.js +3 -0
  245. package/dist/sources/provider-factory.js +5 -12
  246. package/dist/sources/provider.js +3 -20
  247. package/dist/sources/providers/filesystem.js +19 -23
  248. package/dist/sources/providers/git.js +179 -20
  249. package/dist/sources/providers/index.js +3 -0
  250. package/dist/sources/providers/install-types.js +3 -13
  251. package/dist/sources/providers/npm.js +3 -4
  252. package/dist/sources/providers/provider-utils.js +3 -0
  253. package/dist/sources/providers/sync-from-ref.js +3 -11
  254. package/dist/sources/providers/tar-utils.js +3 -0
  255. package/dist/sources/providers/website.js +18 -22
  256. package/dist/sources/resolve.js +3 -0
  257. package/dist/sources/types.js +3 -0
  258. package/dist/sources/website-ingest.js +7 -0
  259. package/dist/tasks/backends/cron.js +203 -0
  260. package/dist/tasks/backends/exec-utils.js +28 -0
  261. package/dist/tasks/backends/index.js +24 -0
  262. package/dist/tasks/backends/launchd-template.xml +19 -0
  263. package/dist/tasks/backends/launchd.js +187 -0
  264. package/dist/tasks/backends/schtasks-template.xml +29 -0
  265. package/dist/tasks/backends/schtasks.js +215 -0
  266. package/dist/tasks/parser.js +211 -0
  267. package/dist/tasks/resolveAkmBin.js +87 -0
  268. package/dist/tasks/runner.js +458 -0
  269. package/dist/tasks/schedule.js +227 -0
  270. package/dist/tasks/schema.js +15 -0
  271. package/dist/tasks/validator.js +62 -0
  272. package/dist/version.js +3 -0
  273. package/dist/wiki/index-template.md +12 -0
  274. package/dist/wiki/ingest-workflow-template.md +54 -0
  275. package/dist/wiki/log-template.md +8 -0
  276. package/dist/wiki/schema-template.md +61 -0
  277. package/dist/wiki/wiki-templates.js +15 -0
  278. package/dist/wiki/wiki.js +13 -61
  279. package/dist/workflows/authoring.js +8 -25
  280. package/dist/workflows/cli.js +3 -0
  281. package/dist/workflows/db.js +141 -2
  282. package/dist/workflows/document-cache.js +3 -10
  283. package/dist/workflows/parser.js +3 -0
  284. package/dist/workflows/renderer.js +11 -3
  285. package/dist/workflows/runs.js +91 -89
  286. package/dist/workflows/schema.js +3 -0
  287. package/dist/workflows/scope-key.js +79 -0
  288. package/dist/workflows/validator.js +4 -8
  289. package/dist/workflows/workflow-template.md +24 -0
  290. package/docs/README.md +10 -2
  291. package/docs/data-and-telemetry.md +225 -0
  292. package/docs/migration/release-notes/0.7.0.md +1 -1
  293. package/docs/migration/release-notes/0.7.4.md +1 -1
  294. package/docs/migration/release-notes/0.7.5.md +20 -0
  295. package/docs/migration/release-notes/0.8.0.md +48 -0
  296. package/docs/migration/v0.7-to-v0.8.md +1307 -0
  297. package/package.json +29 -11
  298. package/dist/commands/install-audit.js +0 -381
  299. package/dist/commands/vault.js +0 -333
  300. package/dist/templates/wiki-templates.js +0 -100
@@ -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
+ }
@@ -0,0 +1,274 @@
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
+ * LLM-based contradiction-detection pass for derived memories (M-1 / #367).
6
+ *
7
+ * Runs BEFORE `analyzeMemoryCleanup` to populate `contradictedBy` frontmatter
8
+ * edges so the existing `resolveFamilyContradictions` SCC resolver has real
9
+ * input to work on. Without this pass the SCC resolver operates on a nearly
10
+ * empty edge graph because no automated subsystem was previously generating
11
+ * contradiction edges — the elegant Tarjan implementation in memory-improve.ts
12
+ * had no input.
13
+ *
14
+ * # Algorithm
15
+ *
16
+ * 1. Collect all derived memories grouped by `parentRef` family.
17
+ * 2. For each family, enumerate candidate pairs (limited to MAX_FAMILY_SIZE).
18
+ * 3. For each pair, call the LLM to judge whether the two memories are in
19
+ * direct factual conflict.
20
+ * 4. For confirmed contradictions, write `contradictedBy` edges directly to
21
+ * the losing memory's frontmatter (same mechanism as `persistBeliefStateTransition`).
22
+ *
23
+ * # LLM Feature Gate
24
+ *
25
+ * The pass is gated behind `profiles.improve.default.processes.consolidate.contradictionDetection.enabled`.
26
+ * When the gate is disabled or no LLM is configured,
27
+ * the pass is a no-op and `analyzeMemoryCleanup` proceeds with only manually
28
+ * annotated edges.
29
+ *
30
+ * # References
31
+ *
32
+ * - Zep / Graphiti (arXiv:2501.13956): writes contradiction edges at detection time.
33
+ * - ATMS (de Kleer 1986): assumption-based truth maintenance via edge propagation.
34
+ * - mem0 contradiction probe (arXiv:2504.19413): pairwise LLM-judge pattern.
35
+ */
36
+ import fs from "node:fs";
37
+ import path from "node:path";
38
+ import { chatCompletion, parseEmbeddedJsonResponse } from "../llm/client";
39
+ import { tryLlmFeature } from "../llm/feature-gate";
40
+ import { assembleAsset } from "./asset-serialize";
41
+ import { getDefaultLlmConfig } from "./config";
42
+ import { parseFrontmatter } from "./frontmatter";
43
+ // ── Constants ────────────────────────────────────────────────────────────────
44
+ /**
45
+ * Maximum family size for pairwise contradiction checking. Families larger
46
+ * than this are skipped to bound the LLM call count (O(n²) pairs).
47
+ */
48
+ const MAX_FAMILY_SIZE = 8;
49
+ /**
50
+ * Maximum number of contradiction pairs to check per improve run, across all
51
+ * families. Prevents runaway LLM usage on stashes with many memories.
52
+ */
53
+ const MAX_PAIRS_PER_RUN = 20;
54
+ /**
55
+ * Truncation limit for memory body content sent to the LLM judge.
56
+ * Keeps prompts compact while preserving the key factual claims.
57
+ */
58
+ const BODY_TRUNCATION = 800;
59
+ // ── Prompt builder ────────────────────────────────────────────────────────────
60
+ function buildContradictionJudgePrompt(a, b) {
61
+ return [
62
+ "You are evaluating two derived memory entries to determine if they contain",
63
+ "directly contradictory factual claims about the same subject.",
64
+ "",
65
+ "Memory A:",
66
+ `Ref: ${a.ref}`,
67
+ `Description: ${a.description || "(none)"}`,
68
+ "Content:",
69
+ "```",
70
+ a.body.slice(0, BODY_TRUNCATION),
71
+ "```",
72
+ "",
73
+ "Memory B:",
74
+ `Ref: ${b.ref}`,
75
+ `Description: ${b.description || "(none)"}`,
76
+ "Content:",
77
+ "```",
78
+ b.body.slice(0, BODY_TRUNCATION),
79
+ "```",
80
+ "",
81
+ "Answer ONLY with valid JSON — no prose, no code fences:",
82
+ '{"contradicts": true|false, "reason": "<one sentence explaining why or why not>"}',
83
+ "",
84
+ "A contradiction means the memories make mutually exclusive factual claims about the",
85
+ "same topic (e.g. Memory A says 'always use VPN' while Memory B says 'VPN is optional').",
86
+ "If the memories are complementary, about different topics, or one supersedes the other",
87
+ "without direct conflict, return false.",
88
+ ].join("\n");
89
+ }
90
+ // ── Filesystem helpers ────────────────────────────────────────────────────────
91
+ function* walkMarkdownFilesLocal(root) {
92
+ if (!fs.existsSync(root))
93
+ return;
94
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
95
+ const full = path.join(root, entry.name);
96
+ if (entry.isDirectory())
97
+ yield* walkMarkdownFilesLocal(full);
98
+ else if (entry.isFile() && entry.name.endsWith(".md"))
99
+ yield full;
100
+ }
101
+ }
102
+ function toMemoryRef(memoriesDir, filePath) {
103
+ const rel = path.relative(memoriesDir, filePath);
104
+ if (!rel || rel.startsWith(".."))
105
+ return undefined;
106
+ const name = rel.replace(/\\/g, "/").replace(/\.md$/i, "");
107
+ return `memory:${name}`;
108
+ }
109
+ function isDerivedMemory(filePath, frontmatter) {
110
+ // Name-based guard (M-2): the .derived suffix is structural and immutable.
111
+ const base = path.basename(filePath, ".md");
112
+ if (base.endsWith(".derived"))
113
+ return true;
114
+ // Frontmatter-based guard: inferred: true marks explicit child memories.
115
+ return frontmatter.inferred === true;
116
+ }
117
+ function resolveParentRef(filePath, frontmatter, memoriesRootDir) {
118
+ // Prefer the explicit source: frontmatter.
119
+ const source = frontmatter.source;
120
+ if (typeof source === "string" && source.startsWith("memory:"))
121
+ return source;
122
+ // Fall back to deriving parent from the file name (strip .derived suffix).
123
+ const base = path.basename(filePath, ".md");
124
+ if (base.endsWith(".derived")) {
125
+ const parentName = base.slice(0, -".derived".length);
126
+ // Use the stash memories root so nested paths (e.g. memories/nested/foo.derived.md)
127
+ // resolve to the correct relative ref (memory:nested/foo, not memory:foo).
128
+ const rootDir = memoriesRootDir ?? path.dirname(filePath);
129
+ const rel = path.relative(rootDir, path.join(path.dirname(filePath), parentName));
130
+ return `memory:${rel.replace(/\\/g, "/")}`;
131
+ }
132
+ return undefined;
133
+ }
134
+ // ── Edge writing ─────────────────────────────────────────────────────────────
135
+ /**
136
+ * Write a `contradictedBy` edge to the losing memory's frontmatter file.
137
+ * Preserves all existing frontmatter keys; only adds/updates `contradictedBy`
138
+ * and `beliefState: contradicted`.
139
+ */
140
+ /** Returns true if the edge was newly written, false if it already existed. */
141
+ function writeContradictedByEdge(filePath, contradictedByRef) {
142
+ const raw = fs.readFileSync(filePath, "utf8");
143
+ const parsed = parseFrontmatter(raw);
144
+ const existing = Array.isArray(parsed.data.contradictedBy) ? parsed.data.contradictedBy : [];
145
+ if (existing.includes(contradictedByRef))
146
+ return false; // Edge already written.
147
+ const updatedContradictedBy = [...new Set([...existing, contradictedByRef])].sort();
148
+ const nextFrontmatter = {
149
+ ...parsed.data,
150
+ contradictedBy: updatedContradictedBy,
151
+ beliefState: "contradicted",
152
+ };
153
+ fs.writeFileSync(filePath, assembleAsset(nextFrontmatter, parsed.content), "utf8");
154
+ return true;
155
+ }
156
+ // ── Main entry point ──────────────────────────────────────────────────────────
157
+ /**
158
+ * Run the LLM-based contradiction-detection pass on derived memories in
159
+ * `<stashDir>/memories/`. Writes `contradictedBy` frontmatter edges for
160
+ * confirmed contradiction pairs so the subsequent `resolveFamilyContradictions`
161
+ * SCC pass has edges to work on.
162
+ *
163
+ * @param stashDir - Root stash directory.
164
+ * @param config - Loaded AKM config (used to access LLM settings).
165
+ * @param chat - Optional chat seam for testing (defaults to chatCompletion).
166
+ */
167
+ export async function detectAndWriteContradictions(stashDir, config, chat = chatCompletion) {
168
+ const result = {
169
+ familiesExamined: 0,
170
+ pairsChecked: 0,
171
+ edgesWritten: 0,
172
+ warnings: [],
173
+ };
174
+ const contradictionLlm = getDefaultLlmConfig(config);
175
+ if (!contradictionLlm)
176
+ return result;
177
+ // Collect derived memories grouped by parent.
178
+ const memoriesDir = path.join(stashDir, "memories");
179
+ const byParent = new Map();
180
+ for (const filePath of walkMarkdownFilesLocal(memoriesDir)) {
181
+ let raw;
182
+ try {
183
+ raw = fs.readFileSync(filePath, "utf8");
184
+ }
185
+ catch {
186
+ continue;
187
+ }
188
+ const parsed = parseFrontmatter(raw);
189
+ if (!isDerivedMemory(filePath, parsed.data))
190
+ continue;
191
+ const parentRef = resolveParentRef(filePath, parsed.data, memoriesDir);
192
+ if (!parentRef)
193
+ continue;
194
+ const ref = toMemoryRef(memoriesDir, filePath);
195
+ if (!ref)
196
+ continue;
197
+ const entry = {
198
+ filePath,
199
+ ref,
200
+ parentRef,
201
+ body: parsed.content.trim(),
202
+ description: typeof parsed.data.description === "string" ? parsed.data.description : "",
203
+ };
204
+ const family = byParent.get(parentRef) ?? [];
205
+ family.push(entry);
206
+ byParent.set(parentRef, family);
207
+ }
208
+ let totalPairsChecked = 0;
209
+ for (const [, family] of byParent) {
210
+ if (family.length < 2)
211
+ continue;
212
+ if (family.length > MAX_FAMILY_SIZE) {
213
+ result.warnings.push(`Skipping contradiction check for family of ${family.length} members (exceeds MAX_FAMILY_SIZE=${MAX_FAMILY_SIZE})`);
214
+ continue;
215
+ }
216
+ result.familiesExamined++;
217
+ for (let i = 0; i < family.length - 1; i++) {
218
+ for (let j = i + 1; j < family.length; j++) {
219
+ if (totalPairsChecked >= MAX_PAIRS_PER_RUN)
220
+ break;
221
+ const a = family[i];
222
+ const b = family[j];
223
+ if (!a || !b)
224
+ continue;
225
+ // Skip pairs where edges already exist in BOTH directions (no new information).
226
+ const aRaw = fs.readFileSync(a.filePath, "utf8");
227
+ const aParsed = parseFrontmatter(aRaw);
228
+ const aCB = Array.isArray(aParsed.data.contradictedBy)
229
+ ? aParsed.data.contradictedBy
230
+ : [];
231
+ const bRaw = fs.readFileSync(b.filePath, "utf8");
232
+ const bParsed = parseFrontmatter(bRaw);
233
+ const bCB = Array.isArray(bParsed.data.contradictedBy)
234
+ ? bParsed.data.contradictedBy
235
+ : [];
236
+ if (aCB.includes(b.ref) && bCB.includes(a.ref))
237
+ continue;
238
+ const prompt = buildContradictionJudgePrompt(a, b);
239
+ const judgeResult = await tryLlmFeature("memory_contradiction_detection", config, async () => {
240
+ return chat(contradictionLlm, [
241
+ { role: "system", content: "Return only valid JSON. No prose." },
242
+ { role: "user", content: prompt },
243
+ ]);
244
+ }, null);
245
+ totalPairsChecked++;
246
+ result.pairsChecked++;
247
+ if (!judgeResult)
248
+ continue; // Feature gate disabled or LLM call failed.
249
+ let parsed = null;
250
+ try {
251
+ parsed = parseEmbeddedJsonResponse(judgeResult);
252
+ }
253
+ catch {
254
+ result.warnings.push(`Could not parse contradiction judge response for pair ${a.ref} / ${b.ref}`);
255
+ continue;
256
+ }
257
+ if (!parsed?.contradicts)
258
+ continue;
259
+ // Write contradiction edges: both members get contradictedBy pointing to each other.
260
+ try {
261
+ const wroteA = writeContradictedByEdge(a.filePath, b.ref);
262
+ const wroteB = writeContradictedByEdge(b.filePath, a.ref);
263
+ result.edgesWritten += (wroteA ? 1 : 0) + (wroteB ? 1 : 0);
264
+ }
265
+ catch (err) {
266
+ result.warnings.push(`Failed to write contradiction edge ${a.ref} <-> ${b.ref}: ${err instanceof Error ? err.message : String(err)}`);
267
+ }
268
+ }
269
+ if (totalPairsChecked >= MAX_PAIRS_PER_RUN)
270
+ break;
271
+ }
272
+ }
273
+ return result;
274
+ }