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
@@ -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
+ }
@@ -1,7 +1,10 @@
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 fs from "node:fs";
2
5
  import path from "node:path";
3
- import { stringify as yamlStringify } from "yaml";
4
6
  import { makeAssetRef, parseAssetRef } from "./asset-ref";
7
+ import { assembleAsset } from "./asset-serialize";
5
8
  import { firstString, groupBy, stringArray } from "./common";
6
9
  import { parseFrontmatter } from "./frontmatter";
7
10
  const DERIVED_SUFFIX = ".derived";
@@ -172,6 +175,34 @@ export function applyMemoryCleanup(stashDir, plan) {
172
175
  warnings.push(formatApplyWarning("archive", candidate.ref, error));
173
176
  }
174
177
  }
178
+ // M-5 / #396: Resolve relative dates for flagged candidates.
179
+ // Anchor: use the file's `createdAt` frontmatter field, or fall back to the
180
+ // file's mtime. Graphiti arXiv:2501.13956, HeidelTime (Strötgen & Gertz 2010).
181
+ let relativeDatesResolved = 0;
182
+ for (const candidate of plan.relativeDateCandidates) {
183
+ try {
184
+ const raw = fs.readFileSync(candidate.filePath, "utf8");
185
+ const fm = parseFrontmatter(raw);
186
+ const createdAtStr = fm.data.createdAt;
187
+ let referenceDate;
188
+ if (createdAtStr) {
189
+ const parsed = new Date(createdAtStr);
190
+ referenceDate = Number.isNaN(parsed.getTime()) ? new Date(fs.statSync(candidate.filePath).mtimeMs) : parsed;
191
+ }
192
+ else {
193
+ referenceDate = new Date(fs.statSync(candidate.filePath).mtimeMs);
194
+ }
195
+ const resolvedBody = resolveRelativeDates(fm.content ?? "", referenceDate);
196
+ if (resolvedBody === (fm.content ?? ""))
197
+ continue; // no change
198
+ const newContent = assembleAsset(fm.data, resolvedBody);
199
+ fs.writeFileSync(candidate.filePath, newContent, "utf8");
200
+ relativeDatesResolved++;
201
+ }
202
+ catch (error) {
203
+ warnings.push(formatApplyWarning("relative-date-resolve", candidate.ref, error));
204
+ }
205
+ }
175
206
  archived.sort((a, b) => a.ref.localeCompare(b.ref));
176
207
  appliedBeliefTransitions.sort(compareBeliefTransitions);
177
208
  return {
@@ -179,6 +210,7 @@ export function applyMemoryCleanup(stashDir, plan) {
179
210
  beliefStateTransitions: appliedBeliefTransitions,
180
211
  ...(transitionLogPath ? { transitionLogPath: path.relative(stashDir, transitionLogPath).replace(/\\/g, "/") } : {}),
181
212
  ...(transitionLogPath ? { transitionLogEntries: appliedBeliefTransitions.length } : {}),
213
+ ...(relativeDatesResolved > 0 ? { relativeDatesResolved } : {}),
182
214
  ...(warnings.length > 0 ? { warnings } : {}),
183
215
  };
184
216
  }
@@ -186,6 +218,52 @@ function formatApplyWarning(stage, ref, error) {
186
218
  const detail = error instanceof Error ? error.message : String(error);
187
219
  return `${stage} failed for ${ref}: ${detail}`;
188
220
  }
221
+ /**
222
+ * M-5 / #396: Resolve relative date expressions to absolute ISO dates.
223
+ *
224
+ * Uses `referenceDate` as the anchor point (Graphiti arXiv:2501.13956,
225
+ * HeidelTime Strötgen & Gertz 2010 — document creation time as reference).
226
+ * Replaces patterns like "yesterday", "3 days ago", "last week" with their
227
+ * ISO 8601 date string (YYYY-MM-DD).
228
+ *
229
+ * Returns the rewritten string; returns the original if no matches.
230
+ */
231
+ function resolveRelativeDates(text, referenceDate) {
232
+ const RELATIVE_DATE_RE = /\b(yesterday|last week|last month|last year|\d+ days? ago|\d+ weeks? ago|\d+ months? ago)\b/gi;
233
+ return text.replace(RELATIVE_DATE_RE, (match) => {
234
+ const lower = match.toLowerCase().trim();
235
+ const d = new Date(referenceDate);
236
+ if (lower === "yesterday") {
237
+ d.setDate(d.getDate() - 1);
238
+ }
239
+ else if (lower === "last week") {
240
+ d.setDate(d.getDate() - 7);
241
+ }
242
+ else if (lower === "last month") {
243
+ d.setMonth(d.getMonth() - 1);
244
+ }
245
+ else if (lower === "last year") {
246
+ d.setFullYear(d.getFullYear() - 1);
247
+ }
248
+ else {
249
+ const numMatch = lower.match(/^(\d+)\s+(day|week|month)s?\s+ago$/);
250
+ if (numMatch) {
251
+ const n = Number.parseInt(numMatch[1] ?? "0", 10);
252
+ const unit = numMatch[2] ?? "day";
253
+ if (unit === "day")
254
+ d.setDate(d.getDate() - n);
255
+ else if (unit === "week")
256
+ d.setDate(d.getDate() - n * 7);
257
+ else if (unit === "month")
258
+ d.setMonth(d.getMonth() - n);
259
+ }
260
+ else {
261
+ return match; // unrecognized pattern — leave as-is
262
+ }
263
+ }
264
+ return d.toISOString().slice(0, 10); // YYYY-MM-DD
265
+ });
266
+ }
189
267
  function resolveFamilyContradictions(family) {
190
268
  if (family.length === 0)
191
269
  return { contradictionCandidates: [], transitions: [] };
@@ -203,12 +281,25 @@ function resolveFamilyContradictions(family) {
203
281
  return {
204
282
  contradictionCandidates: [],
205
283
  transitions: family
206
- .filter((record) => record.beliefState !== "active" || record.contradictedBy.length > 0 || record.currentBeliefRefs.length > 0)
284
+ .filter((record) => {
285
+ // `deprecated` is a frozen historical state — never refresh it to active.
286
+ // (`superseded` is intentionally still refreshable to preserve pre-Phase-1A behavior.)
287
+ if (isFrozenHistoricalBeliefState(record.beliefState))
288
+ return false;
289
+ // `active` and `asserted` are both "current/believed" states. Only emit a
290
+ // refresh if there is something to clear (contradictions / currentBeliefRefs)
291
+ // or the state is something else entirely (e.g. lingering `contradicted`).
292
+ if (isActiveLikeBeliefState(record.beliefState)) {
293
+ return record.contradictedBy.length > 0 || record.currentBeliefRefs.length > 0;
294
+ }
295
+ return true;
296
+ })
207
297
  .map((record) => ({
208
298
  ref: record.ref,
209
299
  parentRef: record.parentRef,
210
300
  fromState: record.beliefState,
211
- toState: "active",
301
+ // Preserve `asserted` authority; otherwise refresh to plain `active`.
302
+ toState: record.beliefState === "asserted" ? "asserted" : "active",
212
303
  reason: "belief-refresh",
213
304
  })),
214
305
  };
@@ -289,14 +380,22 @@ function resolveFamilyContradictions(family) {
289
380
  }
290
381
  const componentRefs = [...components[componentIndex]].sort();
291
382
  const peerCurrentRefs = componentRefs.filter((ref) => ref !== record.ref);
292
- if (record.beliefState !== "active" ||
293
- record.contradictedBy.length > 0 ||
294
- !sameStringArray(record.currentBeliefRefs, peerCurrentRefs)) {
383
+ // `deprecated` is a frozen historical state — never refresh to active.
384
+ // (`superseded` is intentionally still refreshable to preserve pre-Phase-1A behavior.)
385
+ if (isFrozenHistoricalBeliefState(record.beliefState)) {
386
+ continue;
387
+ }
388
+ // For `active` / `asserted` records, only refresh when something changes.
389
+ // For everything else (e.g. lingering `contradicted`) always refresh.
390
+ const isActiveLike = isActiveLikeBeliefState(record.beliefState);
391
+ const needsRefresh = !isActiveLike || record.contradictedBy.length > 0 || !sameStringArray(record.currentBeliefRefs, peerCurrentRefs);
392
+ if (needsRefresh) {
295
393
  transitions.push({
296
394
  ref: record.ref,
297
395
  parentRef: record.parentRef,
298
396
  fromState: record.beliefState,
299
- toState: "active",
397
+ // Preserve `asserted` authority; otherwise refresh to plain `active`.
398
+ toState: record.beliefState === "asserted" ? "asserted" : "active",
300
399
  reason: "belief-refresh",
301
400
  ...(peerCurrentRefs[0] ? { relatedRef: peerCurrentRefs[0], relatedRefs: peerCurrentRefs } : {}),
302
401
  ...(peerCurrentRefs.length > 0 ? { currentBeliefRefs: peerCurrentRefs } : {}),
@@ -364,7 +463,7 @@ function archiveCleanupCandidate(stashDir, candidate, filePath) {
364
463
  const archiveRef = path.relative(stashDir, archivedPath).replace(/\\/g, "/");
365
464
  const auditPath = path.join(archiveDir, "cleanup.md");
366
465
  const auditRef = path.relative(stashDir, auditPath).replace(/\\/g, "/");
367
- const auditFrontmatter = yamlStringify({
466
+ const auditAsset = assembleAsset({
368
467
  schemaVersion: 1,
369
468
  kind: "memory-cleanup-archive",
370
469
  archivedAt,
@@ -376,8 +475,8 @@ function archiveCleanupCandidate(stashDir, candidate, filePath) {
376
475
  ...(candidate.survivorRef ? { survivorRef: candidate.survivorRef } : {}),
377
476
  originalPath,
378
477
  archivedPath: archiveRef,
379
- }).trimEnd();
380
- fs.writeFileSync(auditPath, `---\n${auditFrontmatter}\n---\n\nArchived derived memory for recoverable cleanup.\n`, "utf8");
478
+ }, "Archived derived memory for recoverable cleanup.\n");
479
+ fs.writeFileSync(auditPath, auditAsset, "utf8");
381
480
  return {
382
481
  ref: candidate.ref,
383
482
  parentRef: candidate.parentRef,
@@ -412,9 +511,7 @@ function persistBeliefStateTransition(filePath, transition) {
412
511
  nextFrontmatter.currentBeliefRefs = [...currentBeliefRefs];
413
512
  else
414
513
  delete nextFrontmatter.currentBeliefRefs;
415
- const frontmatter = yamlStringify(nextFrontmatter).trimEnd();
416
- const body = parsed.content.replace(/^\n+/, "");
417
- fs.writeFileSync(filePath, `---\n${frontmatter}\n---\n\n${body}`, "utf8");
514
+ fs.writeFileSync(filePath, assembleAsset(nextFrontmatter, parsed.content), "utf8");
418
515
  }
419
516
  function appendBeliefStateTransitionLog(stashDir, transitions) {
420
517
  const logDir = path.join(stashDir, ".akm", "memory-cleanup");
@@ -512,9 +609,40 @@ function collectDerivedMemories(stashDir, parentRefFilter) {
512
609
  }
513
610
  return records.sort(compareRecords);
514
611
  }
612
+ /**
613
+ * `active` and `asserted` are both "currently believed" states. `asserted` carries
614
+ * stronger user-explicit authority (set by the hot-path `akm remember`) but for
615
+ * state-machine purposes (contradiction resolution, refresh logic) they are
616
+ * equivalent.
617
+ */
618
+ function isActiveLikeBeliefState(state) {
619
+ return state === "active" || state === "asserted";
620
+ }
621
+ /**
622
+ * `deprecated` is a frozen historical state introduced in Phase 1A. Once
623
+ * recorded, the contradiction-resolution pass must not refresh it back to
624
+ * active. (`contradicted` is also historical but it *is* updated by the
625
+ * contradiction resolver, so it is treated separately.)
626
+ *
627
+ * Note: `superseded` is deliberately NOT included here. Pre-Phase-1A,
628
+ * `superseded` records were refreshed to `active` by the belief-refresh
629
+ * pass (the old guard was `record.beliefState !== "active"`). In practice
630
+ * most `superseded` records are pruned earlier via `supersededBy` metadata
631
+ * in `analyzeMemoryCleanup`, so they never reach belief refresh — but a
632
+ * record marked `beliefState: superseded` without `supersededBy` metadata
633
+ * was previously refreshable. Preserving that behavior here avoids a
634
+ * surprise regression; only `deprecated` is the new frozen state.
635
+ */
636
+ function isFrozenHistoricalBeliefState(state) {
637
+ return state === "deprecated";
638
+ }
515
639
  function resolveBeliefState(frontmatter) {
516
640
  const explicit = firstString(frontmatter.beliefState);
517
- if (explicit === "active" || explicit === "superseded" || explicit === "contradicted") {
641
+ if (explicit === "active" ||
642
+ explicit === "asserted" ||
643
+ explicit === "deprecated" ||
644
+ explicit === "superseded" ||
645
+ explicit === "contradicted") {
518
646
  return explicit;
519
647
  }
520
648
  return "active";
@@ -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
  * Shared JSON parsing utilities for LLM and agent output.
3
6
  *