akm-cli 0.9.0-beta.6 → 0.9.0-rc.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 (326) hide show
  1. package/CHANGELOG.md +663 -0
  2. package/README.md +12 -4
  3. package/dist/akm +38 -0
  4. package/dist/akm-migrate-storage +38 -0
  5. package/dist/assets/help/help-improve.md +9 -6
  6. package/dist/assets/hints/cli-hints-full.md +6 -5
  7. package/dist/assets/profiles/default.json +9 -4
  8. package/dist/assets/profiles/frequent.json +1 -1
  9. package/dist/assets/profiles/memory-focus.json +1 -1
  10. package/dist/assets/profiles/proactive-maintenance.json +25 -0
  11. package/dist/assets/profiles/quick.json +1 -1
  12. package/dist/assets/profiles/recombine-only.json +21 -0
  13. package/dist/assets/profiles/reflect-distill.json +30 -0
  14. package/dist/assets/profiles/synthesize.json +15 -0
  15. package/dist/assets/profiles/thorough.json +1 -1
  16. package/dist/assets/prompts/consolidate-system.md +23 -0
  17. package/dist/assets/prompts/contradiction-judge.md +33 -0
  18. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  19. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  20. package/dist/assets/prompts/extract-session.md +11 -3
  21. package/dist/assets/prompts/graph-extract-system.md +1 -0
  22. package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
  23. package/dist/assets/prompts/memory-infer-system.md +1 -0
  24. package/dist/assets/prompts/memory-infer-user.md +5 -0
  25. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  26. package/dist/assets/prompts/procedural-system.md +44 -0
  27. package/dist/assets/prompts/recombine-system.md +40 -0
  28. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  29. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
  39. package/dist/assets/templates/html/health.html +281 -111
  40. package/dist/assets/wiki/ingest-workflow-template.md +45 -16
  41. package/dist/assets/wiki/schema-template.md +4 -4
  42. package/dist/cli/clack.js +56 -0
  43. package/dist/cli/config-migrate.js +7 -1
  44. package/dist/cli/confirm.js +1 -1
  45. package/dist/cli/parse-args.js +46 -1
  46. package/dist/cli/shared.js +28 -0
  47. package/dist/cli.js +25 -14
  48. package/dist/commands/agent/agent-dispatch.js +3 -2
  49. package/dist/commands/agent/agent-support.js +0 -7
  50. package/dist/commands/agent/contribute-cli.js +26 -7
  51. package/dist/commands/config-cli.js +26 -13
  52. package/dist/commands/env/child-env.js +47 -0
  53. package/dist/commands/env/env-cli.js +220 -227
  54. package/dist/commands/env/env.js +14 -67
  55. package/dist/commands/env/secret-cli.js +140 -138
  56. package/dist/commands/feedback-cli.js +153 -147
  57. package/dist/commands/graph/graph-cli.js +5 -13
  58. package/dist/commands/graph/graph.js +76 -72
  59. package/dist/commands/health/advisories.js +151 -0
  60. package/dist/commands/health/checks.js +103 -16
  61. package/dist/commands/health/html-report.js +447 -81
  62. package/dist/commands/health/improve-metrics.js +771 -0
  63. package/dist/commands/health/llm-usage.js +65 -0
  64. package/dist/commands/health/md-report.js +103 -0
  65. package/dist/commands/health/metrics.js +278 -0
  66. package/dist/commands/health/stash-exposure.js +46 -0
  67. package/dist/commands/health/surfaces.js +216 -0
  68. package/dist/commands/health/task-runs.js +135 -0
  69. package/dist/commands/health/types.js +26 -0
  70. package/dist/commands/health/windows.js +195 -0
  71. package/dist/commands/health.js +91 -1083
  72. package/dist/commands/improve/anti-collapse.js +170 -0
  73. package/dist/commands/improve/calibration.js +161 -0
  74. package/dist/commands/improve/collapse-detector.js +421 -0
  75. package/dist/commands/improve/consolidate/chunking.js +141 -0
  76. package/dist/commands/improve/consolidate/eligibility.js +64 -0
  77. package/dist/commands/improve/consolidate/merge.js +145 -0
  78. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  79. package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
  80. package/dist/commands/improve/consolidate.js +1313 -1278
  81. package/dist/commands/improve/dedup.js +482 -0
  82. package/dist/commands/improve/distill/content-repair.js +202 -0
  83. package/dist/commands/improve/distill/promote-memory.js +229 -0
  84. package/dist/commands/improve/distill/quality-gate.js +236 -0
  85. package/dist/commands/improve/distill-guards.js +127 -0
  86. package/dist/commands/improve/distill-promotion-policy.js +826 -167
  87. package/dist/commands/improve/distill.js +243 -599
  88. package/dist/commands/improve/eligibility.js +434 -0
  89. package/dist/commands/improve/encoding-salience.js +205 -0
  90. package/dist/commands/improve/extract-cli.js +179 -59
  91. package/dist/commands/improve/extract-prompt.js +55 -4
  92. package/dist/commands/improve/extract-watch.js +140 -0
  93. package/dist/commands/improve/extract.js +409 -43
  94. package/dist/commands/improve/feedback-valence.js +54 -0
  95. package/dist/commands/improve/hot-probation.js +45 -0
  96. package/dist/commands/improve/improve-auto-accept.js +160 -7
  97. package/dist/commands/improve/improve-cli.js +115 -73
  98. package/dist/commands/improve/improve-profiles.js +32 -8
  99. package/dist/commands/improve/improve-result-file.js +15 -25
  100. package/dist/commands/improve/improve-session.js +58 -0
  101. package/dist/commands/improve/improve.js +510 -2537
  102. package/dist/commands/improve/locks.js +154 -0
  103. package/dist/commands/improve/loop-stages.js +1100 -0
  104. package/dist/commands/improve/memory/memory-belief.js +14 -15
  105. package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
  106. package/dist/commands/improve/memory/memory-improve.js +27 -27
  107. package/dist/commands/improve/outcome-loop.js +270 -0
  108. package/dist/commands/improve/preparation.js +2002 -0
  109. package/dist/commands/improve/proactive-maintenance.js +115 -0
  110. package/dist/commands/improve/procedural.js +398 -0
  111. package/dist/commands/improve/recombine.js +818 -0
  112. package/dist/commands/improve/reflect-noise.js +0 -0
  113. package/dist/commands/improve/reflect.js +212 -45
  114. package/dist/commands/improve/salience.js +455 -0
  115. package/dist/commands/improve/schema-similarity-gate.js +168 -0
  116. package/dist/commands/improve/shared.js +51 -0
  117. package/dist/commands/improve/triage.js +93 -0
  118. package/dist/commands/lint/agent-linter.js +19 -24
  119. package/dist/commands/lint/base-linter.js +173 -60
  120. package/dist/commands/lint/command-linter.js +19 -24
  121. package/dist/commands/lint/env-key-rules.js +38 -1
  122. package/dist/commands/lint/fact-linter.js +39 -0
  123. package/dist/commands/lint/index.js +31 -13
  124. package/dist/commands/lint/memory-linter.js +1 -1
  125. package/dist/commands/lint/registry.js +7 -2
  126. package/dist/commands/lint/task-linter.js +3 -3
  127. package/dist/commands/lint/workflow-linter.js +26 -1
  128. package/dist/commands/observability-cli.js +4 -4
  129. package/dist/commands/proposal/drain-policies.js +13 -4
  130. package/dist/commands/proposal/drain.js +45 -51
  131. package/dist/commands/proposal/legacy-import.js +115 -0
  132. package/dist/commands/proposal/proposal-cli.js +24 -34
  133. package/dist/commands/proposal/proposal.js +7 -1
  134. package/dist/commands/proposal/propose.js +8 -3
  135. package/dist/commands/proposal/repository.js +829 -0
  136. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  137. package/dist/commands/proposal/validators/proposals.js +93 -882
  138. package/dist/commands/read/curate.js +419 -103
  139. package/dist/commands/read/knowledge.js +10 -3
  140. package/dist/commands/read/remember-cli.js +133 -138
  141. package/dist/commands/read/search-cli.js +15 -8
  142. package/dist/commands/read/search.js +22 -11
  143. package/dist/commands/read/show.js +106 -14
  144. package/dist/commands/registry-cli.js +76 -87
  145. package/dist/commands/remember.js +11 -12
  146. package/dist/commands/sources/add-cli.js +91 -95
  147. package/dist/commands/sources/history.js +1 -1
  148. package/dist/commands/sources/init.js +66 -18
  149. package/dist/commands/sources/installed-stashes.js +11 -3
  150. package/dist/commands/sources/schema-repair.js +44 -46
  151. package/dist/commands/sources/self-update.js +2 -2
  152. package/dist/commands/sources/source-add.js +7 -3
  153. package/dist/commands/sources/sources-cli.js +3 -3
  154. package/dist/commands/sources/stash-cli.js +29 -41
  155. package/dist/commands/sources/stash-skeleton.js +57 -8
  156. package/dist/commands/tasks/default-tasks.js +15 -2
  157. package/dist/commands/tasks/tasks-cli.js +20 -29
  158. package/dist/commands/tasks/tasks.js +39 -11
  159. package/dist/commands/wiki-cli.js +23 -38
  160. package/dist/commands/workflow-cli.js +15 -1
  161. package/dist/core/asset/asset-registry.js +3 -1
  162. package/dist/core/asset/asset-spec.js +21 -4
  163. package/dist/core/asset/frontmatter.js +188 -167
  164. package/dist/core/asset/markdown.js +8 -0
  165. package/dist/core/authoring-rules.js +92 -0
  166. package/dist/core/common.js +4 -23
  167. package/dist/core/concurrent.js +10 -1
  168. package/dist/core/config/config-io.js +10 -1
  169. package/dist/core/config/config-migration.js +18 -40
  170. package/dist/core/config/config-schema.js +389 -58
  171. package/dist/core/config/config-types.js +3 -3
  172. package/dist/core/config/config.js +67 -22
  173. package/dist/core/deep-merge.js +38 -0
  174. package/dist/core/errors.js +1 -0
  175. package/dist/core/eval/rank-metrics.js +113 -0
  176. package/dist/core/events.js +4 -7
  177. package/dist/core/improve-types.js +47 -8
  178. package/dist/core/logs-db.js +14 -75
  179. package/dist/core/parse.js +36 -16
  180. package/dist/core/paths.js +21 -18
  181. package/dist/core/standards/resolve-standards-context.js +87 -0
  182. package/dist/core/standards/resolve-stash-standards.js +99 -0
  183. package/dist/core/standards/resolve-type-conventions.js +66 -0
  184. package/dist/core/state/migrations.js +770 -0
  185. package/dist/core/state-db.js +142 -1091
  186. package/dist/core/structured.js +69 -0
  187. package/dist/core/time.js +53 -0
  188. package/dist/core/warn.js +21 -0
  189. package/dist/core/write-source.js +37 -0
  190. package/dist/indexer/db/db.js +356 -780
  191. package/dist/indexer/db/entry-mapper.js +41 -0
  192. package/dist/indexer/db/graph-db.js +129 -86
  193. package/dist/indexer/db/llm-cache.js +2 -2
  194. package/dist/indexer/db/schema.js +516 -0
  195. package/dist/indexer/ensure-index.js +103 -24
  196. package/dist/indexer/feedback/utility-policy.js +75 -0
  197. package/dist/indexer/graph/graph-boost.js +51 -41
  198. package/dist/indexer/graph/graph-extraction.js +207 -4
  199. package/dist/indexer/index-writer-lock.js +106 -0
  200. package/dist/indexer/index-written-assets.js +105 -0
  201. package/dist/indexer/indexer.js +291 -310
  202. package/dist/indexer/passes/dir-staleness.js +114 -0
  203. package/dist/indexer/passes/memory-inference.js +13 -5
  204. package/dist/indexer/passes/metadata.js +20 -0
  205. package/dist/indexer/read-preflight.js +23 -0
  206. package/dist/indexer/search/db-search.js +89 -13
  207. package/dist/indexer/search/fts-query.js +51 -0
  208. package/dist/indexer/search/ranking-contributors.js +95 -9
  209. package/dist/indexer/search/ranking.js +79 -3
  210. package/dist/indexer/search/search-fields.js +6 -0
  211. package/dist/indexer/search/search-source.js +32 -21
  212. package/dist/indexer/search/semantic-status.js +4 -0
  213. package/dist/indexer/walk/matchers.js +9 -0
  214. package/dist/indexer/walk/walker.js +21 -13
  215. package/dist/integrations/agent/builders.js +39 -13
  216. package/dist/integrations/agent/config.js +20 -59
  217. package/dist/integrations/agent/detect.js +9 -0
  218. package/dist/integrations/agent/index.js +3 -19
  219. package/dist/integrations/agent/model-aliases.js +7 -2
  220. package/dist/integrations/agent/profiles.js +7 -1
  221. package/dist/integrations/agent/prompts.js +75 -9
  222. package/dist/integrations/agent/runner-dispatch.js +59 -0
  223. package/dist/integrations/agent/runner.js +13 -9
  224. package/dist/integrations/agent/spawn.js +69 -67
  225. package/dist/integrations/harnesses/claude/agent-builder.js +1 -1
  226. package/dist/integrations/harnesses/claude/index.js +2 -0
  227. package/dist/integrations/harnesses/claude/session-log.js +11 -1
  228. package/dist/integrations/harnesses/index.js +2 -3
  229. package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
  230. package/dist/integrations/harnesses/opencode/index.js +2 -0
  231. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  232. package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
  233. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +98 -17
  234. package/dist/integrations/harnesses/types.js +1 -0
  235. package/dist/integrations/session-logs/index.js +16 -0
  236. package/dist/llm/call-ai.js +2 -2
  237. package/dist/llm/client.js +57 -15
  238. package/dist/llm/embedder.js +67 -4
  239. package/dist/llm/embedders/cache.js +3 -1
  240. package/dist/llm/embedders/deterministic.js +66 -0
  241. package/dist/llm/embedders/local.js +73 -3
  242. package/dist/llm/feature-gate.js +16 -15
  243. package/dist/llm/graph-extract.js +67 -44
  244. package/dist/llm/memory-infer-impl.js +138 -0
  245. package/dist/llm/memory-infer.js +1 -127
  246. package/dist/llm/metadata-enhance.js +44 -31
  247. package/dist/llm/structured-call.js +49 -0
  248. package/dist/migrate-storage-node.mjs +8 -0
  249. package/dist/output/context.js +5 -5
  250. package/dist/output/renderers.js +85 -14
  251. package/dist/output/shapes/curate.js +14 -2
  252. package/dist/output/shapes/helpers.js +0 -3
  253. package/dist/output/shapes/passthrough.js +2 -1
  254. package/dist/output/text/helpers.js +29 -1
  255. package/dist/output/text/workflow.js +1 -0
  256. package/dist/registry/providers/skills-sh.js +21 -147
  257. package/dist/registry/providers/static-index.js +15 -157
  258. package/dist/registry/resolve.js +27 -9
  259. package/dist/runtime.js +25 -1
  260. package/dist/scripts/migrate-storage.js +2718 -2354
  261. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +891 -597
  262. package/dist/setup/detect.js +9 -0
  263. package/dist/setup/legacy-config.js +106 -0
  264. package/dist/setup/prompt.js +57 -0
  265. package/dist/setup/providers.js +14 -0
  266. package/dist/setup/registry-stash-loader.js +12 -0
  267. package/dist/setup/semantic-assets.js +124 -0
  268. package/dist/setup/setup.js +52 -1614
  269. package/dist/setup/steps/connection.js +734 -0
  270. package/dist/setup/steps/output.js +31 -0
  271. package/dist/setup/steps/platforms.js +124 -0
  272. package/dist/setup/steps/semantic.js +27 -0
  273. package/dist/setup/steps/sources.js +222 -0
  274. package/dist/setup/steps/stashdir.js +42 -0
  275. package/dist/setup/steps/tasks.js +152 -0
  276. package/dist/sources/include.js +6 -2
  277. package/dist/sources/providers/filesystem.js +0 -1
  278. package/dist/sources/providers/git-install.js +210 -0
  279. package/dist/sources/providers/git-provider.js +234 -0
  280. package/dist/sources/providers/git-stash.js +248 -0
  281. package/dist/sources/providers/git.js +10 -661
  282. package/dist/sources/providers/npm.js +2 -6
  283. package/dist/sources/providers/provider-utils.js +13 -7
  284. package/dist/sources/providers/sync-from-ref.js +9 -1
  285. package/dist/sources/providers/tar-utils.js +16 -8
  286. package/dist/sources/providers/website.js +9 -5
  287. package/dist/sources/website-ingest.js +187 -29
  288. package/dist/sources/wiki-fetchers/registry.js +53 -0
  289. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  290. package/dist/storage/database.js +45 -10
  291. package/dist/storage/managed-db.js +82 -0
  292. package/dist/storage/repositories/canaries-repository.js +107 -0
  293. package/dist/storage/repositories/consolidation-repository.js +38 -0
  294. package/dist/storage/repositories/embeddings-repository.js +72 -0
  295. package/dist/storage/repositories/events-repository.js +187 -0
  296. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  297. package/dist/storage/repositories/improve-runs-repository.js +146 -0
  298. package/dist/storage/repositories/index-db.js +14 -8
  299. package/dist/storage/repositories/proposals-repository.js +220 -0
  300. package/dist/storage/repositories/recombine-repository.js +213 -0
  301. package/dist/storage/repositories/registry-cache.js +93 -0
  302. package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
  303. package/dist/storage/repositories/task-history-repository.js +93 -0
  304. package/dist/storage/sqlite-pragmas.js +146 -0
  305. package/dist/tasks/backends/cron.js +1 -1
  306. package/dist/tasks/backends/index.js +9 -0
  307. package/dist/tasks/backends/launchd.js +1 -1
  308. package/dist/tasks/backends/schtasks.js +1 -1
  309. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  310. package/dist/tasks/runner.js +15 -13
  311. package/dist/text-import-hook.mjs +0 -0
  312. package/dist/wiki/wiki.js +52 -11
  313. package/dist/workflows/cli.js +1 -0
  314. package/dist/workflows/db.js +3 -4
  315. package/dist/workflows/runtime/runs.js +43 -118
  316. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  317. package/dist/workflows/validate-summary.js +2 -7
  318. package/docs/README.md +69 -18
  319. package/docs/data-and-telemetry.md +5 -4
  320. package/docs/migration/release-notes/0.7.0.md +1 -1
  321. package/docs/migration/release-notes/0.9.0.md +39 -0
  322. package/package.json +10 -10
  323. package/dist/assets/tasks/core/update-stashes.yml +0 -4
  324. package/dist/commands/db-cli.js +0 -23
  325. package/dist/indexer/db/db-backup.js +0 -376
  326. package/dist/indexer/passes/staleness-detect.js +0 -488
@@ -0,0 +1,145 @@
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
+ export function isValidOp(op) {
5
+ if (typeof op !== "object" || op === null)
6
+ return false;
7
+ const o = op;
8
+ if (o.op === "merge") {
9
+ return typeof o.primary === "string" && Array.isArray(o.secondaries);
10
+ }
11
+ if (o.op === "delete") {
12
+ return typeof o.ref === "string";
13
+ }
14
+ if (o.op === "promote") {
15
+ return typeof o.ref === "string" && typeof o.knowledgeRef === "string";
16
+ }
17
+ if (o.op === "contradict") {
18
+ return typeof o.ref === "string" && typeof o.contradictedByRef === "string";
19
+ }
20
+ return false;
21
+ }
22
+ export function mergePlans(chunks, knownRefs) {
23
+ const mergeOps = new Map();
24
+ const deleteOps = new Map();
25
+ const promoteOps = new Map();
26
+ // C-3 / #382: contradict ops keyed by `ref|contradictedByRef` to deduplicate.
27
+ const contradictOps = new Map();
28
+ const warnings = [];
29
+ for (const chunk of chunks) {
30
+ for (const op of chunk) {
31
+ if (op.op === "merge") {
32
+ // Drop ops whose primary the LLM hallucinated (not in the loaded memory
33
+ // pool). Without this guard, a hallucinated primary flows all the way to
34
+ // Phase B where !memoryByRef.has(primary) fires and charges every real
35
+ // secondary with merge_primary_missing — masking LLM hallucinations as
36
+ // filter regressions in health metrics.
37
+ if (knownRefs && !knownRefs.has(op.primary)) {
38
+ warnings.push(`mergePlans: primary ${op.primary} not in loaded memory pool (LLM hallucination) — dropping op before execution.`);
39
+ // Use a dedicated skip reason so dashboards can distinguish
40
+ // hallucinated primaries from stale-DB regressions.
41
+ // Secondaries are real refs; they are NOT charged here — they remain
42
+ // available for other ops to claim.
43
+ continue;
44
+ }
45
+ // Filter hallucinated secondaries while preserving real ones.
46
+ let mergeOp = op;
47
+ if (knownRefs) {
48
+ const filteredSecondaries = op.secondaries.filter((sec) => {
49
+ if (!knownRefs.has(sec)) {
50
+ warnings.push(`mergePlans: secondary ${sec} not in loaded memory pool (LLM hallucination) — dropping from op.`);
51
+ return false;
52
+ }
53
+ return true;
54
+ });
55
+ if (filteredSecondaries.length !== op.secondaries.length) {
56
+ mergeOp = { ...op, secondaries: filteredSecondaries };
57
+ }
58
+ }
59
+ // merge wins over delete
60
+ if (deleteOps.has(mergeOp.primary)) {
61
+ deleteOps.delete(mergeOp.primary);
62
+ }
63
+ for (const sec of mergeOp.secondaries) {
64
+ if (deleteOps.has(sec))
65
+ deleteOps.delete(sec);
66
+ }
67
+ mergeOps.set(mergeOp.primary, mergeOp);
68
+ }
69
+ else if (op.op === "delete") {
70
+ // merge and promote both win over delete. A promote is non-destructive
71
+ // (creates a proposal) but the source memory is counted in `promoted`;
72
+ // if a delete also fires, the ref lands in both `promoted` and
73
+ // `skipReasons`, breaking the invariant by +1.
74
+ if (!mergeOps.has(op.ref) && !promoteOps.has(op.ref)) {
75
+ deleteOps.set(op.ref, op);
76
+ }
77
+ }
78
+ else if (op.op === "promote") {
79
+ // C-2 / #381: when both a promote and a merge target the same ref,
80
+ // queue the promote FIRST rather than discarding it. The promote op
81
+ // routes through createProposal (the human-gated proposal queue), so
82
+ // it is non-destructive. The merge follows after the proposal is
83
+ // created. This preserves the human reviewer's ability to inspect the
84
+ // promotion before the source memory is merged/deleted.
85
+ // AGM K*8 — retain the maximally informative consistent subset.
86
+ promoteOps.set(op.ref, op);
87
+ }
88
+ else if (op.op === "contradict") {
89
+ // Deduplicate by ref+contradictedByRef pair.
90
+ const key = `${op.ref}|${op.contradictedByRef}`;
91
+ if (!contradictOps.has(key)) {
92
+ contradictOps.set(key, op);
93
+ }
94
+ }
95
+ }
96
+ }
97
+ // Second pass: enforce merge-wins-over-delete and deduplicate secondaries.
98
+ //
99
+ // 1. Delete/secondary ordering bug: the per-chunk loop removes delete ops
100
+ // for secondaries that were already in deleteOps, but misses the case
101
+ // where the delete chunk came first. A full sweep here fixes both orders.
102
+ //
103
+ // 2. Cross-merge secondary dedup: if ref A is a secondary in two merge ops,
104
+ // only the first (insertion-order) retains it. Without this, a successful
105
+ // merge credits A to mergedSecondaries and a later merge's emitMerge-
106
+ // FailureSkips also charges A to skipReasons — double-counting A while
107
+ // processed has it only once.
108
+ //
109
+ // 3. Primary-as-secondary dedup: if ref A is a primary in one merge op and
110
+ // a secondary in another, remove A from the secondary list. Both merges
111
+ // would otherwise claim A (merged++ for A, then mergedSecondaries++ for A)
112
+ // breaking the invariant the same way.
113
+ // Also remove delete ops for any ref claimed by a promote op (handles the
114
+ // case where the delete chunk appeared before the promote chunk).
115
+ for (const ref of promoteOps.keys()) {
116
+ deleteOps.delete(ref);
117
+ }
118
+ const claimedSecondaries = new Set();
119
+ for (const mergeOp of mergeOps.values()) {
120
+ deleteOps.delete(mergeOp.primary);
121
+ mergeOp.secondaries = mergeOp.secondaries.filter((sec) => {
122
+ if (mergeOps.has(sec)) {
123
+ warnings.push(`Merge: secondary ${sec} is also a merge primary — removing from secondary list to avoid double-count.`);
124
+ return false;
125
+ }
126
+ if (claimedSecondaries.has(sec)) {
127
+ warnings.push(`Merge: secondary ${sec} appears in multiple merge ops — retaining in first op only.`);
128
+ return false;
129
+ }
130
+ claimedSecondaries.add(sec);
131
+ deleteOps.delete(sec);
132
+ return true;
133
+ });
134
+ }
135
+ // C-2 / #381: promote ops are ordered BEFORE merge ops so that the
136
+ // human-gated proposal queue entry is created before any destructive merge.
137
+ // Phase B processes ops in array order, so promote executes first.
138
+ const ops = [
139
+ ...promoteOps.values(),
140
+ ...mergeOps.values(),
141
+ ...deleteOps.values(),
142
+ ...contradictOps.values(),
143
+ ];
144
+ return { ops, warnings };
145
+ }
@@ -0,0 +1,231 @@
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
+ // ── LLM-output sanitization ─────────────────────────────────────────────────
5
+ //
6
+ // Pure string/frontmatter transforms that defend against three classes of LLM
7
+ // defect observed across hundreds of consolidate proposals:
8
+ //
9
+ // 1. Code-fence leakage: the entire merged asset is wrapped in
10
+ // ```markdown … ``` (or ```yaml … ```) despite the prompt forbidding
11
+ // fences. The post-processor used to pass this through verbatim, so the
12
+ // first character of the asset content became a backtick rather than
13
+ // `---`, defeating the frontmatter parser.
14
+ // 2. YAML quote-escaping bugs: descriptions like `'"Specialty intro...:`
15
+ // with unbalanced quotes that break the YAML reader. The post-processor
16
+ // historically passed the LLM's raw scalar straight into a manually
17
+ // assembled `description: <raw>` line.
18
+ // 3. Truncated descriptions hitting token cutoffs — the model's max_tokens
19
+ // runs out mid-sentence, leaving things like
20
+ // `description: "Tables in narrow column containers need max-width:100% +"`
21
+ // with no closing context.
22
+ //
23
+ // `sanitizeMergedContent` and `validateProposalFrontmatter` defend against
24
+ // all three at the point where LLM output is consumed.
25
+ import { parse as yamlParse } from "yaml";
26
+ import { assembleAssetFromString, serializeFrontmatter } from "../../../core/asset/asset-serialize.js";
27
+ import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
28
+ /**
29
+ * Attempt to recover a frontmatter block that is missing its closing `---`.
30
+ *
31
+ * Scans lines after the opening `---` for the first blank line or the first
32
+ * line that cannot be a YAML scalar (i.e. not a key-value, indented
33
+ * continuation, comment, or list item). Injects `---` before that line so
34
+ * the normal parser can proceed.
35
+ *
36
+ * Returns the patched string on success, or `null` if the structure is too
37
+ * ambiguous to recover safely (e.g. no opening `---`, or no body content
38
+ * found after the frontmatter key-value lines).
39
+ */
40
+ function recoverMalformedFrontmatter(raw) {
41
+ if (!raw.startsWith("---"))
42
+ return null;
43
+ const lines = raw.split(/\r?\n/);
44
+ // Skip the opening `---` line (index 0).
45
+ let insertAt = -1;
46
+ for (let i = 1; i < lines.length; i++) {
47
+ const line = lines[i];
48
+ // A blank line marks the end of the frontmatter block in many YAML variants.
49
+ if (line.trim() === "") {
50
+ insertAt = i;
51
+ break;
52
+ }
53
+ // A line that is clearly body content: doesn't look like a YAML key, an
54
+ // indented continuation, a comment, or a sequence item.
55
+ const isYaml = /^\w[\w-]*\s*:/.test(line) || // key: value
56
+ /^\s+\S/.test(line) || // indented continuation / nested
57
+ /^\s*#/.test(line) || // YAML comment
58
+ /^\s*-\s/.test(line); // sequence item
59
+ if (!isYaml) {
60
+ insertAt = i;
61
+ break;
62
+ }
63
+ }
64
+ if (insertAt < 0)
65
+ return null;
66
+ const result = [...lines.slice(0, insertAt), "---", ...lines.slice(insertAt)].join("\n");
67
+ return result;
68
+ }
69
+ /**
70
+ * Outer-fence stripper specific to consolidate. Unlike the shared
71
+ * `stripMarkdownFences` helper (which only handles markdown fences), this
72
+ * variant additionally recognises `yaml` and bare-language fences and refuses
73
+ * to strip an unbalanced fence — i.e. a leading ``` with no trailing ``` is
74
+ * treated as a malformed response, not partially sanitized.
75
+ *
76
+ * Returns `null` when only one half of a fence pair is present (caller
77
+ * should reject the response entirely).
78
+ */
79
+ export function stripOuterCodeFence(raw) {
80
+ const trimmed = raw.trim();
81
+ const leading = trimmed.match(/^```(?:markdown|md|yaml|yml)?\s*\r?\n/i);
82
+ const trailing = trimmed.match(/\r?\n```\s*$/);
83
+ if (!leading && !trailing)
84
+ return { content: trimmed, stripped: false };
85
+ if (!leading || !trailing)
86
+ return null; // unbalanced — refuse
87
+ const inner = trimmed.slice(leading[0].length, trimmed.length - trailing[0].length).trim();
88
+ return { content: inner, stripped: true };
89
+ }
90
+ export function sanitizeMergedContent(raw) {
91
+ // Step 1: Strip outer code fence.
92
+ // Recovery path: if only the leading fence is present, strip it and continue
93
+ // provided the inner content starts with `---`. Trailing-only fences are NOT
94
+ // recovered — a trailing ``` is more likely a body code block than a forgotten
95
+ // wrapper, so recovering would silently corrupt the body.
96
+ let body;
97
+ {
98
+ const fenceResult = stripOuterCodeFence(raw);
99
+ if (fenceResult) {
100
+ body = fenceResult.content;
101
+ }
102
+ else {
103
+ const trimmed = raw.trim();
104
+ const leadingMatch = trimmed.match(/^```(?:markdown|md|yaml|yml)?\s*\r?\n([\s\S]*)$/i);
105
+ const inner = leadingMatch ? leadingMatch[1].trim() : null;
106
+ if (!inner?.startsWith("---")) {
107
+ return { ok: false, reason: "UNBALANCED_CODE_FENCE" };
108
+ }
109
+ body = inner;
110
+ }
111
+ }
112
+ // Strip <think> blocks (some local models still emit them despite system prompts).
113
+ body = body.replace(/<think>[\s\S]*?<\/think>/gi, "").trim();
114
+ // Step 2: Verify frontmatter sentinel.
115
+ // Recovery path: LLM sometimes emits 1-2 lines of preamble (e.g. "Here is the
116
+ // merged content:") before the `---`. Accept if `---` appears within 300 chars.
117
+ // Beyond that it's more likely a body section divider, not a frontmatter start.
118
+ if (!body.startsWith("---")) {
119
+ const nlIdx = body.indexOf("\n---");
120
+ if (nlIdx >= 0 && nlIdx < 300) {
121
+ body = body.slice(nlIdx + 1);
122
+ }
123
+ else {
124
+ return { ok: false, reason: "MISSING_FRONTMATTER_SENTINEL" };
125
+ }
126
+ }
127
+ // Extract frontmatter block.
128
+ // Recovery path: LLM sometimes omits the closing `---` delimiter. Detect this
129
+ // by scanning lines after the opening `---` for the first blank line or the
130
+ // first line that isn't a YAML key-value pair, then inject `---` there.
131
+ let match = body.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r\n|\r|\n|$)([\s\S]*)$/);
132
+ if (!match) {
133
+ const recovered = recoverMalformedFrontmatter(body);
134
+ if (recovered) {
135
+ match = recovered.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r\n|\r|\n|$)([\s\S]*)$/);
136
+ }
137
+ if (!match) {
138
+ return { ok: false, reason: "MALFORMED_FRONTMATTER_BLOCK" };
139
+ }
140
+ }
141
+ // Re-parse via the yaml library so any quote-escaping mistakes either get
142
+ // normalised or surface as a parse error we can reject.
143
+ // Recovery: if the strict yaml library fails, fall back to the lenient
144
+ // hand-rolled parseFrontmatter parser, which tolerates common LLM YAML
145
+ // quirks (unescaped special chars, bare scalars, etc.). If it recovers
146
+ // at least one key, proceed — serializeFrontmatter below will re-serialize
147
+ // cleanly. Only reject if both parsers fail to extract any data.
148
+ let parsedFm;
149
+ try {
150
+ parsedFm = yamlParse(match[1]);
151
+ }
152
+ catch (e) {
153
+ const fallback = parseFrontmatter(`---\n${match[1]}\n---\n${match[2]}`);
154
+ if (fallback.frontmatter !== null && Object.keys(fallback.data).length > 0) {
155
+ parsedFm = fallback.data;
156
+ }
157
+ else {
158
+ return { ok: false, reason: `INVALID_YAML: ${e instanceof Error ? e.message : String(e)}` };
159
+ }
160
+ }
161
+ if (parsedFm === null || typeof parsedFm !== "object" || Array.isArray(parsedFm)) {
162
+ return { ok: false, reason: "FRONTMATTER_NOT_OBJECT" };
163
+ }
164
+ const fm = parsedFm;
165
+ // Normalise placeholder leaks like `updated: today`, `updated: {today: null}`,
166
+ // `updated: now`, etc. The consolidate prompt instructs the LLM not to emit
167
+ // these, but small models still do. Replace any such leak with today's ISO
168
+ // date OR drop the field if we can't safely normalise it.
169
+ normalizeUpdatedField(fm);
170
+ // Re-serialise via yaml.stringify to fix any quoting quirks.
171
+ let serialized;
172
+ try {
173
+ serialized = serializeFrontmatter(fm);
174
+ }
175
+ catch (e) {
176
+ return { ok: false, reason: `YAML_STRINGIFY_FAILED: ${e instanceof Error ? e.message : String(e)}` };
177
+ }
178
+ const cleaned = assembleAssetFromString(serialized, match[2]);
179
+ return { ok: true, result: { content: cleaned, frontmatter: fm } };
180
+ }
181
+ /**
182
+ * Mutate `fm.updated` in place to normalise placeholder leaks emitted by the
183
+ * LLM. The consolidate prompt forbids these, but small models still produce
184
+ * literal `today` / `{today: null}` / `now` values.
185
+ *
186
+ * Rules:
187
+ * - A real ISO-style date string (YYYY-MM-DD, optionally with time) stays as-is.
188
+ * - A Date object (some YAML parsers materialise dates) is converted to its
189
+ * ISO yyyy-mm-dd form.
190
+ * - A placeholder string ("today", "now", "{today}", "${today}", template
191
+ * variables) is replaced with today's ISO date.
192
+ * - A map/object (e.g. `{today: null}`) is replaced with today's ISO date.
193
+ * - `null`, empty string, missing → left alone (no field added; reviewers
194
+ * should not silently gain metadata they didn't write).
195
+ *
196
+ * Exported for unit testing.
197
+ */
198
+ export function normalizeUpdatedField(fm) {
199
+ if (!("updated" in fm))
200
+ return;
201
+ const v = fm.updated;
202
+ if (v === null || v === undefined || v === "")
203
+ return;
204
+ const todayIso = new Date().toISOString().slice(0, 10);
205
+ if (v instanceof Date) {
206
+ fm.updated = v.toISOString().slice(0, 10);
207
+ return;
208
+ }
209
+ if (typeof v === "string") {
210
+ const trimmed = v.trim().toLowerCase();
211
+ if (/^\d{4}-\d{2}-\d{2}/.test(v.trim()))
212
+ return; // already a real date
213
+ if (trimmed === "today" ||
214
+ trimmed === "now" ||
215
+ trimmed === "{today}" ||
216
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: matches the literal user-typed placeholder text "${today}" so we can normalize it to today's ISO date
217
+ trimmed === "${today}" ||
218
+ trimmed === "{{today}}" ||
219
+ /^\{?\s*today\s*\}?$/.test(trimmed)) {
220
+ fm.updated = todayIso;
221
+ return;
222
+ }
223
+ // Unknown string format — leave alone so it's visible in the diff.
224
+ return;
225
+ }
226
+ if (typeof v === "object") {
227
+ // Maps like `{today: null}`, `{now: null}` — clearly a template leak.
228
+ fm.updated = todayIso;
229
+ return;
230
+ }
231
+ }
@@ -1,4 +1,4 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- export { akmLint } from "./lint/index.js";
4
+ export {};