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
@@ -51,23 +51,38 @@
51
51
  * be invoked from CI / automation without spinning up an agent harness.
52
52
  */
53
53
  import fs from "node:fs";
54
- import path from "node:path";
54
+ import distillKnowledgeSystemPrompt from "../../assets/prompts/distill-knowledge-system.md" with { type: "text" };
55
+ import distillLessonSystemPrompt from "../../assets/prompts/distill-lesson-system.md" with { type: "text" };
55
56
  import { parseAssetRef } from "../../core/asset/asset-ref.js";
56
57
  import { assembleAssetFromString } from "../../core/asset/asset-serialize.js";
57
- import { parseFrontmatter } from "../../core/asset/frontmatter.js";
58
+ import { parseFrontmatter, writeSalienceToFrontmatter } from "../../core/asset/frontmatter.js";
58
59
  import { stripMarkdownFences } from "../../core/asset/markdown.js";
59
- import { resolveStashDir, timestampForFilename } from "../../core/common.js";
60
- import { getDefaultLlmConfig, loadConfig } from "../../core/config/config.js";
60
+ import { authoringRulesForType } from "../../core/authoring-rules.js";
61
+ import { resolveStashDir } from "../../core/common.js";
62
+ import { getDefaultLlmConfig, getImproveProcessConfig, loadConfig } from "../../core/config/config.js";
61
63
  import { ConfigError, UsageError } from "../../core/errors.js";
62
64
  import { appendEvent, readEvents } from "../../core/events.js";
63
65
  import { lintLessonContent } from "../../core/lesson-lint.js";
66
+ import { getDbPath } from "../../core/paths.js";
67
+ import { resolveStashStandards } from "../../core/standards/resolve-stash-standards.js";
68
+ import { withStateDb } from "../../core/state-db.js";
64
69
  import { warnVerbose } from "../../core/warn.js";
70
+ import { closeDatabase, getAllEntries, openIndexDatabase } from "../../indexer/db/db.js";
65
71
  import { resolveAssetPath } from "../../indexer/walk/path-resolver.js";
66
72
  import { chatCompletion, parseEmbeddedJsonResponse } from "../../llm/client.js";
67
73
  import { isLlmFeatureEnabled, tryLlmFeature } from "../../llm/feature-gate.js";
68
- import { createProposal, isProposalSkipped, listProposals, } from "../proposal/validators/proposals.js";
69
- import { akmSearch } from "../read/search.js";
70
- import { assessMemoryKnowledgePromotionCandidate, deriveKnowledgeRef } from "./distill-promotion-policy.js";
74
+ import { createProposal, isProposalSkipped, listProposals, } from "../proposal/repository.js";
75
+ import { stripFrontmatterBody as stripBodyForFidelity } from "./dedup.js";
76
+ import { autoRepairLessonFrontmatter, autoSwapDescriptionWhenToUse, collectLessonQualityFindings, repairLessonDescriptionTruncation, } from "./distill/content-repair.js";
77
+ import { promoteMemoryToKnowledge } from "./distill/promote-memory.js";
78
+ import { fetchTopSimilarLessons, persistOutputEncodingSalience, runLessonQualityJudge, writeQualityRejection, } from "./distill/quality-gate.js";
79
+ import { buildClsContext, checkDistillFidelity } from "./distill-guards.js";
80
+ import { deriveKnowledgeRef } from "./distill-promotion-policy.js";
81
+ import { buildRefVocabulary, scoreEncodingSalience } from "./encoding-salience.js";
82
+ import { computeSalience, upsertAssetSalience } from "./salience.js";
83
+ // Re-exported for `reflect.ts`, which applies the same LLM-as-judge gate to
84
+ // reflect proposals (R-5 / #374).
85
+ export { runLessonQualityJudge };
71
86
  /**
72
87
  * Asset-ref types that `akm distill` structurally refuses as inputs.
73
88
  *
@@ -76,6 +91,11 @@ import { assessMemoryKnowledgePromotionCandidate, deriveKnowledgeRef } from "./d
76
91
  * `lesson:lesson-<name>-lesson-lesson` (double `-lesson` suffix) — the
77
92
  * recursive-ref defect observed across 323 archived rejected proposals.
78
93
  *
94
+ * 08-F2: `env` and `secret` are refused as a STRUCTURAL floor — distill reads
95
+ * the input asset's bytes via `readFileSync` and hands them to the LLM, so
96
+ * secret material must never be a distill input. This gate is code, not config:
97
+ * it holds even when `allowedTypes` config is mis-set in unattended cron.
98
+ *
79
99
  * The runtime gate inside {@link akmDistill} still refuses these inputs
80
100
  * defensively (returning an `outcome: "skipped"` envelope with `skipReason:
81
101
  * "recursive_lesson_input"`). This exported set is the planner-side companion:
@@ -87,7 +107,7 @@ import { assessMemoryKnowledgePromotionCandidate, deriveKnowledgeRef } from "./d
87
107
  * type means updating this constant — the planner picks the change up for
88
108
  * free.
89
109
  */
90
- export const DISTILL_REFUSED_INPUT_TYPES = new Set(["lesson"]);
110
+ export const DISTILL_REFUSED_INPUT_TYPES = new Set(["lesson", "env", "secret"]);
91
111
  /**
92
112
  * Returns true when `type` is structurally refused as an input by
93
113
  * {@link akmDistill}. See {@link DISTILL_REFUSED_INPUT_TYPES}.
@@ -112,7 +132,6 @@ export function deriveLessonRef(inputRef) {
112
132
  .replace(/^-|-$/g, "");
113
133
  return `lesson:${safe}-lesson`;
114
134
  }
115
- import { repairTruncatedDescription } from "../../core/text-truncation.js";
116
135
  // ── Content quality validators ──────────────────────────────────────────────
117
136
  //
118
137
  // The actual implementations now live in `core/proposal-quality-validators.ts`
@@ -122,68 +141,8 @@ import { repairTruncatedDescription } from "../../core/text-truncation.js";
122
141
  import { detectDoubleFrontmatter, isValidDescription, isValidWhenToUse, } from "../proposal/validators/proposal-quality-validators.js";
123
142
  export { detectDoubleFrontmatter, isValidDescription, isValidWhenToUse };
124
143
  // ── Prompt assembly ─────────────────────────────────────────────────────────
125
- const LESSON_SYSTEM_PROMPT = [
126
- "You are the akm `distill` distiller.",
127
- "Given an asset and recent feedback events about it, produce a single",
128
- "concise *lesson* an agent should remember next time it works on this",
129
- "asset's domain.",
130
- "",
131
- "YOUR RESPONSE MUST START EXACTLY WITH `---` ON THE VERY FIRST LINE.",
132
- "DO NOT output any prose, explanation, or code fences before or after.",
133
- "",
134
- "Required output format — copy this structure exactly:",
135
- "---",
136
- "description: <one complete sentence (ending with `.`) summarising what the lesson teaches>",
137
- "when_to_use: <one complete sentence describing the concrete trigger condition>",
138
- "---",
139
- "",
140
- "<lesson body — plain markdown, 1–3 short paragraphs of practical guidance>",
141
- "",
142
- "## description field (MANDATORY)",
143
- "- A single complete sentence in present tense, 80-200 chars, NO markdown.",
144
- "- Self-contained: a reviewer must understand the lesson from this field alone.",
145
- '- DO NOT start with "When ", "If ", or a connector word — that belongs in when_to_use.',
146
- '- DO NOT copy a section heading ("Key takeaways", "For example", "Key pitfalls").',
147
- "- DO NOT begin with a numbered list marker, code fence, or markdown heading.",
148
- "",
149
- 'GOOD: "Always validate ref existence before promoting a memory to knowledge; missing refs surface as silent 404s during accept."',
150
- 'BAD: "Key pitfalls"',
151
- 'BAD: "When working with the akm CLI"',
152
- 'BAD: "For example, you might..."',
153
- 'BAD: "1. Check the file"',
154
- "",
155
- "RULES:",
156
- "- `when_to_use` MUST be a complete sentence describing a concrete trigger. Never write `When working with <asset-name>` — that is circular and useless.",
157
- "- `description` and `when_to_use` MUST differ from each other.",
158
- "- The lesson body MUST be non-empty markdown prose. Do NOT restate `description:` or `when_to_use:` inside the body (no `**description:** ...` or `**when_to_use:** ...` lines — the frontmatter is the only place those keys belong).",
159
- "- Do NOT emit a second `---` fence after the opening frontmatter — there are exactly two `---` lines in the output, both belonging to the single frontmatter block at the top.",
160
- "- Do NOT reproduce the source asset verbatim — distil what a caller needs to know.",
161
- "- Output ONLY the lesson file. No preamble, no code fences, no trailing prose.",
162
- ].join("\n");
163
- const KNOWLEDGE_SYSTEM_PROMPT = [
164
- "You are the akm `distill` distiller.",
165
- "Given an asset and recent feedback events about it, produce a concise",
166
- "*knowledge* markdown document capturing the durable, reusable facts.",
167
- "Prefer stable guidance over narrative recap.",
168
- "",
169
- "YOUR RESPONSE MUST START EXACTLY WITH `---` ON THE VERY FIRST LINE.",
170
- "DO NOT output any prose, explanation, or code fences before or after.",
171
- "",
172
- "Required output format:",
173
- "---",
174
- "description: <one-line summary of the knowledge asset>",
175
- "tags: [<tag1>, <tag2>]",
176
- "---",
177
- "",
178
- "# <Title>",
179
- "",
180
- "<body — structured markdown, durable facts only>",
181
- "",
182
- "RULES:",
183
- "- `description` MUST be a non-empty single-line string.",
184
- "- Include a meaningful markdown body with a `# Title` heading.",
185
- "- Output ONLY the knowledge file. No preamble, no code fences, no trailing prose.",
186
- ].join("\n");
144
+ const LESSON_SYSTEM_PROMPT = distillLessonSystemPrompt;
145
+ const KNOWLEDGE_SYSTEM_PROMPT = distillKnowledgeSystemPrompt;
187
146
  // ── Structured-output schemas (responseSchema lift) ─────────────────────────
188
147
  //
189
148
  // PR 1 of the asset-writers decision (see knowledge:projects/akm/
@@ -358,6 +317,18 @@ export function buildDistillPrompt(input) {
358
317
  const lines = [];
359
318
  lines.push(`Asset ref: ${input.inputRef}`);
360
319
  lines.push("");
320
+ if (input.standardsContext?.trim()) {
321
+ lines.push("Standards to follow (the rulebook for this target):");
322
+ lines.push(input.standardsContext.trim());
323
+ lines.push("");
324
+ }
325
+ {
326
+ const authoringRules = authoringRulesForType(input.proposalKind ?? "lesson");
327
+ if (authoringRules) {
328
+ lines.push(authoringRules);
329
+ lines.push("");
330
+ }
331
+ }
361
332
  lines.push("Asset content:");
362
333
  if (input.assetContent) {
363
334
  const body = input.assetContent.trim().slice(0, 3000);
@@ -443,178 +414,6 @@ export function buildDistillPrompt(input) {
443
414
  }
444
415
  return lines.join("\n");
445
416
  }
446
- // ── D-4 / #390: Top-3 similar lessons retrieval ──────────────────────────────
447
- /**
448
- * Default implementation: use akmSearch to find top-N similar lesson assets.
449
- * Returns empty array when search fails or returns no results.
450
- * Requires embedding configured for semantic similarity; degrades gracefully.
451
- */
452
- async function fetchTopSimilarLessons(query, n, _stashDir) {
453
- try {
454
- const result = await akmSearch({
455
- query,
456
- type: "lesson",
457
- limit: n,
458
- skipLogging: true,
459
- eventSource: "improve",
460
- });
461
- const hits = result?.hits ?? [];
462
- return hits
463
- .filter((h) => "path" in h && typeof h.path === "string")
464
- .slice(0, n)
465
- .map((h) => {
466
- let content = "";
467
- try {
468
- if (h.path && fs.existsSync(h.path)) {
469
- content = fs.readFileSync(h.path, "utf8");
470
- }
471
- }
472
- catch {
473
- /* best-effort */
474
- }
475
- return { ref: h.ref, content };
476
- });
477
- }
478
- catch {
479
- return [];
480
- }
481
- }
482
- // ── LLM-as-judge quality gate (P2-B) ────────────────────────────────────────
483
- /**
484
- * D-4 / #390: Build the LLM-as-judge prompt.
485
- *
486
- * When similarLessons are provided (top-3 by embedding similarity), they are
487
- * included in the context so the judge can lower the score for near-duplicates.
488
- * Voyager arXiv:2305.16291 — skill library admission requires similarity check
489
- * against the existing library. A-MEM arXiv:2502.12110 — new notes are checked
490
- * against existing notes before linking.
491
- */
492
- function buildJudgePrompt(lessonContent, sourceContent, similarLessons) {
493
- const lines = [
494
- "You are evaluating a proposed lesson asset for an akm knowledge base.",
495
- "",
496
- "Score this lesson on each criterion from 1 (poor) to 5 (excellent):",
497
- "1. NOVELTY: Does the lesson add information not already present in the source asset?",
498
- "2. ACTIONABILITY: Can an agent follow this lesson without additional context?",
499
- "3. NON-REDUNDANCY: Is this lesson meaningfully different from what the source already says?",
500
- "",
501
- "Source asset content:",
502
- "```",
503
- sourceContent.slice(0, 2000),
504
- "```",
505
- ];
506
- if (similarLessons && similarLessons.length > 0) {
507
- lines.push("");
508
- lines.push("Existing similar lessons (top-3 by similarity). Rate lower if the proposed lesson is substantially similar to any of these:");
509
- for (const sl of similarLessons) {
510
- lines.push(`\nExisting lesson ref: ${sl.ref}`);
511
- lines.push("```");
512
- lines.push(sl.content.slice(0, 500));
513
- lines.push("```");
514
- }
515
- }
516
- lines.push("");
517
- lines.push("Proposed lesson content:");
518
- lines.push("```");
519
- lines.push(lessonContent.slice(0, 1000));
520
- lines.push("```");
521
- lines.push("");
522
- lines.push('Return ONLY valid JSON, no prose: {"score": <average score 1-5 as float>, "reason": "<one sentence>"}');
523
- return lines.join("\n");
524
- }
525
- /**
526
- * Run the LLM-as-judge quality gate on a proposal's content.
527
- *
528
- * Exported so reflect.ts can apply the same gate to reflect proposals (R-5 / #374).
529
- * Gated by the flag name `lesson_quality_gate` (or its alias
530
- * `proposal_quality_gate`) via {@link isLlmFeatureEnabled} — which reads
531
- * `profiles.improve.default.processes.distill.qualityGate.enabled` (and the
532
- * corresponding `.reflect.qualityGate.enabled` for proposals).
533
- *
534
- * Fail-open: returns `pass: true` on timeout, parse failure, or missing LLM.
535
- */
536
- export async function runLessonQualityJudge(config, lessonContent, sourceContent, chat,
537
- /** D-4 / #390: top-3 similar existing lessons for dedup check. */
538
- similarLessons) {
539
- const llmConfig = getDefaultLlmConfig(config);
540
- if (!llmConfig) {
541
- return { pass: true, score: -1, reason: "no LLM configured — passing through" };
542
- }
543
- const judgeLlmConfig = llmConfig.judgeModel ? { ...llmConfig, model: llmConfig.judgeModel } : llmConfig;
544
- const JUDGE_TIMEOUT_MS = 8_000;
545
- try {
546
- const raw = await Promise.race([
547
- chat(judgeLlmConfig, [
548
- { role: "system", content: "Return only valid JSON. No prose." },
549
- { role: "user", content: buildJudgePrompt(lessonContent, sourceContent, similarLessons) },
550
- ]),
551
- new Promise((_, reject) => setTimeout(() => reject(new Error("judge timeout")), JUDGE_TIMEOUT_MS)),
552
- ]);
553
- const parsed = parseEmbeddedJsonResponse(raw);
554
- if (!parsed || typeof parsed.score !== "number") {
555
- return { pass: true, score: -1, reason: "judge parse failed — passing through" };
556
- }
557
- // D-5 / #388: Three-band system (MT-Bench arXiv:2306.05685 — ~±0.5 judge variance).
558
- // >= 3.5: auto-queue as pending (pass: true)
559
- // 2.5–3.5: review-needed band — uncertain, escalate to human (reviewNeeded: true)
560
- // < 2.5: auto-reject (pass: false)
561
- const score = parsed.score;
562
- const reason = parsed.reason ?? "";
563
- if (score >= 3.5) {
564
- return { pass: true, score, reason };
565
- }
566
- if (score >= 2.5) {
567
- // Uncertainty band: treat as failed for auto-queuing but flag for review.
568
- return { pass: false, score, reason, reviewNeeded: true };
569
- }
570
- return { pass: false, score, reason };
571
- }
572
- catch {
573
- return { pass: true, score: -1, reason: "judge failed — passing through" };
574
- }
575
- }
576
- // ── Quality-rejection helper ─────────────────────────────────────────────────
577
- /**
578
- * Write a rejected lesson to `.akm/distill-rejected/`, append a `distill_invoked`
579
- * quality-rejected event, and return the `quality_rejected` envelope.
580
- *
581
- * @param stash - Root stash directory.
582
- * @param inputRef - The original input ref (for the event).
583
- * @param lessonRef - The proposed lesson/knowledge ref.
584
- * @param content - The raw content that failed the quality gate.
585
- * @param score - Quality score from the judge.
586
- * @param reason - Human-readable rejection reason.
587
- * @param extraMeta - Optional additional metadata for the event.
588
- */
589
- function writeQualityRejection(stash, inputRef, lessonRef, content, score, reason, extraMeta = {}) {
590
- // D-5 / #388: reviewNeeded flag selects "review_needed" vs "quality_rejected" outcome.
591
- const outcome = extraMeta.reviewNeeded ? "review_needed" : "quality_rejected";
592
- const rejectDir = path.join(stash, ".akm", "distill-rejected");
593
- fs.mkdirSync(rejectDir, { recursive: true });
594
- const ts = timestampForFilename();
595
- fs.writeFileSync(path.join(rejectDir, `${ts}-${lessonRef}.md`), `---\nscore: ${score}\nreason: ${reason}\noutcome: ${outcome}\n---\n\n${content}`, "utf8");
596
- appendEvent({
597
- eventType: "distill_invoked",
598
- ref: inputRef,
599
- metadata: {
600
- outcome,
601
- lessonRef,
602
- score,
603
- reason,
604
- ...extraMeta,
605
- },
606
- });
607
- return {
608
- schemaVersion: 1,
609
- ok: true,
610
- outcome,
611
- inputRef,
612
- lessonRef,
613
- score,
614
- reason,
615
- ...extraMeta,
616
- };
617
- }
618
417
  // ── Main entry point ────────────────────────────────────────────────────────
619
418
  /**
620
419
  * Run a single bounded distillation pass for `ref`. Always emits exactly one
@@ -629,6 +428,12 @@ export async function akmDistill(options) {
629
428
  // Validate the ref shape up front so a typo never reaches the LLM.
630
429
  const parsedInputRef = parseAssetRef(inputRef);
631
430
  const targetKind = options.proposalKind ?? "lesson";
431
+ // Attribution tagging: spread into every distill_invoked event's metadata so
432
+ // the lane that selected this asset is recorded uniformly across all outcome
433
+ // branches. Empty object when no lane was supplied (direct `akm distill`).
434
+ const eligMeta = options.eligibilitySource
435
+ ? { eligibilitySource: options.eligibilitySource }
436
+ : {};
632
437
  // Recursive-distillation guard. Distill produces *lessons* from non-lesson
633
438
  // sources (memory, skill, knowledge, etc.). Calling distill on an existing
634
439
  // lesson would derive `lesson:lesson-<name>-lesson-lesson` (double `-lesson`
@@ -641,15 +446,22 @@ export async function akmDistill(options) {
641
446
  // the improve planner can skip these refs before queuing distill attempts;
642
447
  // this runtime check stays as a defensive backstop for direct callers.
643
448
  if (isDistillRefusedInputType(parsedInputRef.type)) {
644
- const skippedRef = `lesson:${parsedInputRef.name}`;
449
+ // 08-F2: env/secret are a secret-material refusal (never read the bytes);
450
+ // lesson is the recursive-form refusal. Both skip BEFORE any readFileSync.
451
+ const isSecretInput = parsedInputRef.type === "env" || parsedInputRef.type === "secret";
452
+ const skippedRef = isSecretInput ? inputRef : `lesson:${parsedInputRef.name}`;
453
+ const message = isSecretInput
454
+ ? `Distill refuses ${parsedInputRef.type} inputs — secret material must never be sent to the LLM.`
455
+ : "Distill refuses lesson inputs — lessons are the distilled form, not a source.";
645
456
  appendEvent({
646
457
  eventType: "distill_invoked",
647
458
  ref: inputRef,
648
459
  metadata: {
649
460
  outcome: "skipped",
650
461
  lessonRef: skippedRef,
651
- message: "distill refuses lesson inputs — lessons are the distilled form, not a source",
652
- skipReason: "recursive_lesson_input",
462
+ message,
463
+ skipReason: isSecretInput ? "refused_secret_input" : "recursive_lesson_input",
464
+ ...eligMeta,
653
465
  },
654
466
  });
655
467
  return {
@@ -658,7 +470,7 @@ export async function akmDistill(options) {
658
470
  outcome: "skipped",
659
471
  inputRef,
660
472
  lessonRef: skippedRef,
661
- message: "Distill refuses lesson inputs — lessons are the distilled form, not a source.",
473
+ message,
662
474
  };
663
475
  }
664
476
  const config = options.config ?? loadConfig();
@@ -666,20 +478,95 @@ export async function akmDistill(options) {
666
478
  const chat = options.chat ?? chatCompletion;
667
479
  const lookup = options.lookupFn ?? defaultLookup;
668
480
  const readEventsImpl = options.readEventsFn ?? readEvents;
481
+ // R1 opt-out must flow into every computeSalience call this command makes so
482
+ // distill-written rank_score rows use the same weights as preparation's.
483
+ const outcomeWeightEnabled = config.improve?.salience?.outcomeWeightEnabled !== false;
669
484
  // D-4 / #390: similar-lessons retrieval seam (test-injectable).
670
485
  const fetchSimilarLessonsFn = options.fetchSimilarLessonsFn ?? ((query, n) => fetchTopSimilarLessons(query, n, options.stashDir));
671
486
  // Best-effort load: when the asset is not yet indexed we still proceed —
672
487
  // the LLM is asked to distil from "available signal" (feedback alone).
673
488
  let assetContent = null;
489
+ let assetFilePath = null;
674
490
  try {
675
491
  const filePath = await lookup(inputRef);
676
492
  if (filePath && fs.existsSync(filePath)) {
493
+ assetFilePath = filePath;
677
494
  assetContent = fs.readFileSync(filePath, "utf8");
678
495
  }
679
496
  }
680
497
  catch {
681
498
  assetContent = null;
682
499
  }
500
+ // ── #608: Encoding-time salience scoring ────────────────────────────────
501
+ // Score the source asset with the three-signal model (novelty × 0.40 +
502
+ // magnitude × 0.35 + predictionError × 0.25) and persist the result to:
503
+ // 1. The asset's frontmatter (human-readable mirror; idempotent delta gate).
504
+ // 2. state.db :: asset_salience (canonical; feeds improve's high-salience gate).
505
+ // Both writes are best-effort — a DB error never blocks distillation.
506
+ //
507
+ // The bigram ref vocabulary is built ONCE per invocation — the novelty signal
508
+ // reuses it when scoring the distilled OUTPUT at proposal creation (G4).
509
+ let existingRefVocabulary = new Set();
510
+ try {
511
+ const embCfg = config?.embedding;
512
+ const indexDb = openIndexDatabase(getDbPath(), embCfg?.dimension ? { embeddingDim: embCfg.dimension } : undefined);
513
+ try {
514
+ const allRefs = getAllEntries(indexDb).map((e) => e.entryKey);
515
+ existingRefVocabulary = buildRefVocabulary(allRefs);
516
+ }
517
+ finally {
518
+ closeDatabase(indexDb);
519
+ }
520
+ }
521
+ catch {
522
+ // Index not available — novelty defaults to type-floor.
523
+ }
524
+ if (assetContent && assetFilePath) {
525
+ try {
526
+ const parsedRef = parseAssetRef(inputRef);
527
+ // G4: predictionError decays with revision count — the prior hardcoded
528
+ // `revisionCount: 0` made it a dead constant 1.0. Use the number of
529
+ // proposals ever raised against this ref as the revision proxy.
530
+ let revisionCount = 0;
531
+ try {
532
+ revisionCount = listProposals(stash, { ref: inputRef, includeArchive: true }).length;
533
+ }
534
+ catch {
535
+ // best-effort: unknown history scores as a first encounter
536
+ }
537
+ const salienceResult = scoreEncodingSalience({
538
+ body: assetContent,
539
+ type: parsedRef.type,
540
+ existingRefVocabulary,
541
+ revisionCount,
542
+ });
543
+ // 1. Write salience to the source asset frontmatter (idempotent).
544
+ const updatedContent = writeSalienceToFrontmatter(assetContent, salienceResult.score, salienceResult);
545
+ if (updatedContent !== assetContent) {
546
+ fs.writeFileSync(assetFilePath, updatedContent, "utf8");
547
+ assetContent = updatedContent;
548
+ }
549
+ // 2. Persist encoding_salience to state.db.
550
+ try {
551
+ withStateDb((stateDb) => {
552
+ const vector = computeSalience({
553
+ ref: inputRef,
554
+ type: parsedRef.type,
555
+ retrievalFreq: 0,
556
+ encodingSalience: salienceResult.score,
557
+ outcomeWeightEnabled,
558
+ });
559
+ upsertAssetSalience(stateDb, inputRef, vector);
560
+ });
561
+ }
562
+ catch {
563
+ // State DB unavailable — frontmatter mirror is the only persistence.
564
+ }
565
+ }
566
+ catch {
567
+ // Scoring errors never block distillation.
568
+ }
569
+ }
683
570
  const { events } = readEventsImpl({
684
571
  ref: inputRef,
685
572
  type: "feedback",
@@ -703,185 +590,34 @@ export async function akmDistill(options) {
703
590
  eventType: e.eventType,
704
591
  ...(e.metadata !== undefined ? { metadata: e.metadata } : {}),
705
592
  }));
706
- const promotion = targetKind === "lesson"
707
- ? null
708
- : assessMemoryKnowledgePromotionCandidate({
709
- inputRef,
710
- assetContent,
711
- feedbackEvents: filteredEvents.map((event) => ({
712
- ...(event.metadata !== undefined ? { metadata: event.metadata } : {}),
713
- })),
714
- });
715
- if (promotion?.promote && promotion.content && (targetKind === "knowledge" || targetKind === "auto")) {
716
- // D-1 / #369: When the destination knowledge file already exists, route
717
- // through the LLM for contradiction resolution instead of silently
718
- // overwriting. Follows mem0 ADD/UPDATE/DELETE/NOOP pattern (arXiv:2504.19413 §3.2)
719
- // and A-MEM dynamic linking (arXiv:2502.12110).
720
- let resolvedPromotionContent = promotion.content;
721
- const existingKnowledgePath = await lookup(promotion.knowledgeRef);
722
- const existingKnowledgeContent = existingKnowledgePath && fs.existsSync(existingKnowledgePath)
723
- ? (() => {
724
- try {
725
- return fs.readFileSync(existingKnowledgePath, "utf8");
726
- }
727
- catch {
728
- return null;
729
- }
730
- })()
731
- : null;
732
- if (existingKnowledgeContent && config && getDefaultLlmConfig(config)) {
733
- // Existing content found: call LLM for contradiction-resolution merge.
734
- const mergePrompt = [
735
- "You are merging two versions of a knowledge document.",
736
- "Existing content is already committed; new content comes from a memory distillation run.",
737
- "Choose one of: ADD (combine both), UPDATE (replace existing with new), NOOP (keep existing unchanged).",
738
- 'Return ONLY valid JSON: {"action": "ADD"|"UPDATE"|"NOOP", "content": "<merged markdown if ADD/UPDATE, empty string if NOOP>"}',
739
- "",
740
- "## Existing knowledge content",
741
- "```",
742
- existingKnowledgeContent.slice(0, 3000),
743
- "```",
744
- "",
745
- "## New content from distillation",
746
- "```",
747
- promotion.content.slice(0, 3000),
748
- "```",
749
- ].join("\n");
750
- try {
751
- const mergeLlm = getDefaultLlmConfig(config);
752
- if (!mergeLlm) {
753
- throw new ConfigError("LLM is not configured for distillation merge.", "LLM_NOT_CONFIGURED");
754
- }
755
- const mergeResponse = await chat(mergeLlm, [
756
- { role: "system", content: "Return only valid JSON. No prose." },
757
- { role: "user", content: mergePrompt },
758
- ]);
759
- const mergeResult = parseEmbeddedJsonResponse(mergeResponse);
760
- if (mergeResult?.action === "NOOP") {
761
- // Existing content is authoritative — no update needed.
762
- appendEvent({
763
- eventType: "distill_invoked",
764
- ref: inputRef,
765
- metadata: {
766
- outcome: "skipped",
767
- lessonRef: promotion.knowledgeRef,
768
- message: "D-1: LLM resolved destination conflict as NOOP — existing content kept",
769
- },
770
- });
771
- return {
772
- schemaVersion: 1,
773
- ok: true,
774
- outcome: "skipped",
775
- inputRef,
776
- lessonRef: promotion.knowledgeRef,
777
- message: "Existing knowledge content unchanged (contradiction resolution: NOOP)",
778
- };
779
- }
780
- if (mergeResult?.action && (mergeResult.action === "ADD" || mergeResult.action === "UPDATE")) {
781
- if (mergeResult.content?.trim()) {
782
- resolvedPromotionContent = mergeResult.content;
783
- }
784
- }
785
- }
786
- catch {
787
- // LLM merge failed — fall through with the original promotion content.
788
- // The reviewer will see both versions in the proposal diff.
789
- }
790
- }
791
- else if (existingKnowledgeContent && config && !getDefaultLlmConfig(config)) {
792
- // No LLM configured: include existing content as context in the proposal
793
- // so the reviewer can do the contradiction resolution manually.
794
- resolvedPromotionContent = [
795
- promotion.content,
796
- "",
797
- "---",
798
- "<!-- D-1 / #369: Existing knowledge content is shown below for reviewer reference. -->",
799
- "<!-- Review: decide whether to ADD (merge), UPDATE (replace), or NOOP (keep existing). -->",
800
- "",
801
- "## Existing content (for reviewer reference)",
802
- "",
803
- existingKnowledgeContent,
804
- ].join("\n");
805
- }
806
- // Apply quality gate to fast-path knowledge promotion (Risk 4 fix).
807
- // D-5 / #388: Three-band system — review_needed band queues to proposal
808
- // queue with review_needed outcome rather than auto-rejecting.
809
- let knowledgeJudgeConfidence;
810
- if (isLlmFeatureEnabled(config, "lesson_quality_gate")) {
811
- // D-4 / #390: retrieve top-3 similar lessons for dedup check in judge.
812
- const similarLessons = await fetchSimilarLessonsFn(resolvedPromotionContent.slice(0, 500), 3);
813
- const judgeResult = await runLessonQualityJudge(config, resolvedPromotionContent, assetContent ?? "", chat, similarLessons.length > 0 ? similarLessons : undefined);
814
- if (!judgeResult.pass) {
815
- if (judgeResult.reviewNeeded) {
816
- // Uncertainty band (2.5–3.5): queue as review_needed instead of rejecting.
817
- return writeQualityRejection(stash, inputRef, promotion.knowledgeRef, resolvedPromotionContent, judgeResult.score, judgeResult.reason, { reviewNeeded: true });
818
- }
819
- return writeQualityRejection(stash, inputRef, promotion.knowledgeRef, resolvedPromotionContent, judgeResult.score, judgeResult.reason);
820
- }
821
- // Normalize 1-5 judge score to [0, 1]. Score of -1 means pass-through
822
- // (no LLM / timeout / parse failure) — leave confidence undefined so
823
- // the auto-accept gate treats the proposal as unscored and skips it.
824
- if (judgeResult.score > 0)
825
- knowledgeJudgeConfidence = judgeResult.score / 5;
826
- }
827
- const knowledgeParsed = parseFrontmatter(resolvedPromotionContent);
828
- const proposalResult = createProposal(stash, {
829
- ref: promotion.knowledgeRef,
830
- source: "distill",
831
- ...(options.sourceRun !== undefined ? { sourceRun: options.sourceRun } : {}),
832
- payload: {
833
- content: resolvedPromotionContent,
834
- ...(Object.keys(knowledgeParsed.data).length > 0 ? { frontmatter: knowledgeParsed.data } : {}),
835
- },
836
- ...(knowledgeJudgeConfidence !== undefined ? { confidence: knowledgeJudgeConfidence } : {}),
837
- }, options.ctx);
838
- if (isProposalSkipped(proposalResult)) {
839
- appendEvent({
840
- eventType: "distill_invoked",
841
- ref: inputRef,
842
- metadata: {
843
- outcome: "skipped",
844
- lessonRef: promotion.knowledgeRef,
845
- message: proposalResult.message,
846
- skipReason: proposalResult.reason,
847
- },
848
- });
849
- return {
850
- schemaVersion: 1,
851
- ok: true,
852
- outcome: "skipped",
853
- inputRef,
854
- lessonRef: promotion.knowledgeRef,
855
- message: proposalResult.message,
856
- };
857
- }
858
- const proposal = proposalResult;
859
- appendEvent({
860
- eventType: "distill_invoked",
861
- ref: inputRef,
862
- metadata: {
863
- outcome: "queued",
864
- lessonRef: promotion.knowledgeRef,
865
- proposalRef: promotion.knowledgeRef,
866
- proposalKind: "knowledge",
867
- proposalId: proposal.id,
868
- ...(options.sourceRun !== undefined ? { sourceRun: options.sourceRun } : {}),
869
- ...(exclusionSet.size > 0 ? { filteredFeedbackCount } : {}),
870
- },
871
- });
872
- return {
873
- schemaVersion: 1,
874
- ok: true,
875
- outcome: "queued",
876
- inputRef,
877
- lessonRef: promotion.knowledgeRef,
878
- proposalRef: promotion.knowledgeRef,
879
- proposalKind: "knowledge",
880
- proposalId: proposal.id,
881
- proposal,
882
- ...(exclusionSet.size > 0 ? { filteredFeedbackCount, feedbackFullyFiltered } : {}),
883
- };
884
- }
593
+ // Memory→knowledge promotion branch (D-1/#369). When the target ref is a
594
+ // reinforced memory, distill graduates it into a knowledge proposal instead
595
+ // of a lesson — the whole branch (LLM contradiction-merge, quality gate,
596
+ // proposal creation, event emit) lives in `promoteMemoryToKnowledge` and is
597
+ // terminal when it fires. A `null` return means "not a promotion candidate";
598
+ // fall through to the ordinary lesson/knowledge distillation path.
599
+ const promotionResult = await promoteMemoryToKnowledge({
600
+ targetKind,
601
+ inputRef,
602
+ assetContent,
603
+ filteredEvents,
604
+ config,
605
+ chat,
606
+ stash,
607
+ lookup,
608
+ fetchSimilarLessonsFn,
609
+ existingRefVocabulary,
610
+ outcomeWeightEnabled,
611
+ eligMeta,
612
+ eligibilitySource: options.eligibilitySource,
613
+ sourceRun: options.sourceRun,
614
+ proposalsCtx: options.ctx,
615
+ exclusionSetSize: exclusionSet.size,
616
+ filteredFeedbackCount,
617
+ feedbackFullyFiltered,
618
+ });
619
+ if (promotionResult)
620
+ return promotionResult;
885
621
  const effectiveProposalKind = targetKind === "knowledge" ? "knowledge" : "lesson";
886
622
  const effectiveLessonRef = effectiveProposalKind === "knowledge" ? deriveKnowledgeRef(inputRef) : deriveLessonRef(inputRef);
887
623
  // Inject last 1–3 rejected proposals for this ref as Reflexion-style
@@ -894,13 +630,36 @@ export async function akmDistill(options) {
894
630
  reason: p.review?.reason ?? "no reason given",
895
631
  contentPreview: p.payload.content.slice(0, 500),
896
632
  }));
897
- const userPrompt = buildDistillPrompt({
633
+ // WS-3b CLS interleaving (step 9).
634
+ // When cls.enabled, inject embedding-retrieved adjacent lessons/knowledge
635
+ // into the distill prompt so the LLM avoids overwriting prior generalizations
636
+ // (catastrophic interference). DEFAULT OFF.
637
+ const clsConfig = getImproveProcessConfig(config, "distill", options.improveProfile)?.cls ?? {};
638
+ let clsContext = "";
639
+ if (clsConfig.enabled) {
640
+ try {
641
+ const adjacentCount = clsConfig.adjacentCount ?? 3;
642
+ // Use the asset content or input ref as the query for adjacent retrieval.
643
+ const clsQuery = assetContent ? assetContent.slice(0, 500) : inputRef;
644
+ const adjacentItems = await fetchSimilarLessonsFn(clsQuery, adjacentCount);
645
+ clsContext = buildClsContext(adjacentItems, clsConfig);
646
+ }
647
+ catch {
648
+ // Fail open — CLS is supplemental, never required.
649
+ }
650
+ }
651
+ // Distill output is a lesson/knowledge (non-wiki) → stash authoring
652
+ // standards. Resolved once for this single call.
653
+ const standardsContext = resolveStashStandards(stash);
654
+ const baseUserPrompt = buildDistillPrompt({
898
655
  inputRef,
899
656
  assetContent,
900
657
  feedback,
901
658
  proposalKind: effectiveProposalKind,
902
659
  ...(rejectedForRef.length > 0 ? { rejectedProposals: rejectedForRef } : {}),
660
+ ...(standardsContext.trim() ? { standardsContext } : {}),
903
661
  });
662
+ const userPrompt = clsContext ? `${baseUserPrompt}${clsContext}` : baseUserPrompt;
904
663
  const messages = [
905
664
  { role: "system", content: effectiveProposalKind === "knowledge" ? KNOWLEDGE_SYSTEM_PROMPT : LESSON_SYSTEM_PROMPT },
906
665
  { role: "user", content: userPrompt },
@@ -979,6 +738,7 @@ export async function akmDistill(options) {
979
738
  lessonRef: effectiveLessonRef,
980
739
  proposalKind: effectiveProposalKind,
981
740
  ...(exclusionSet.size > 0 ? { filteredFeedbackCount } : {}),
741
+ ...eligMeta,
982
742
  },
983
743
  });
984
744
  return {
@@ -1011,136 +771,20 @@ export async function akmDistill(options) {
1011
771
  // Strip any stray fence the LLM might have added around the markdown.
1012
772
  content = stripMarkdownFences(raw);
1013
773
  }
1014
- // Auto-repair missing frontmatter fields before hard-failing. Small models
1015
- // frequently produce a good lesson body but omit the YAML header entirely.
1016
- // Rather than discarding valid content, we extract description/when_to_use
1017
- // from the body and prepend the required frontmatter block.
1018
- //
1019
- // IMPORTANT: We do NOT synthesise placeholder strings here. If the body
1020
- // does not contain text that passes the post-LLM validators
1021
- // (`isValidDescription` / `isValidWhenToUse`), we leave the field missing
1022
- // and let the lesson lint reject the proposal as `validation_failed`.
1023
- // Emitting placeholders like `"Lesson distilled from <ref>"` or
1024
- // `"When working with <slug>"` is what produced the systematic broken
1025
- // proposals observed across 323 archived rejections.
774
+ // Lesson-path content normalization (see distill/content-repair): auto-repair
775
+ // missing frontmatter, description↔when_to_use auto-swap, and truncation
776
+ // repair. Knowledge output skips all three (no lesson frontmatter contract).
1026
777
  if (effectiveProposalKind !== "knowledge") {
1027
- const parsed = parseFrontmatter(content);
1028
- const fm = (parsed.data ?? {});
1029
- const missingDesc = typeof fm.description !== "string" || !fm.description.trim();
1030
- const missingWtu = typeof fm.when_to_use !== "string" || !fm.when_to_use.trim();
1031
- if (missingDesc || missingWtu) {
1032
- const body = parsed.content.trim();
1033
- // Strip markdown formatting tokens from a line so extracted text is clean.
1034
- const stripMd = (l) => l
1035
- .replace(/\*\*([^*]+)\*\*/g, "$1")
1036
- .replace(/\*([^*]+)\*/g, "$1")
1037
- .replace(/`([^`]+)`/g, "$1")
1038
- .replace(/^[#*\->_]+\s*/, "")
1039
- .replace(/:\s*$/, "")
1040
- .trim();
1041
- // Skip lines that look like YAML field assignments (key: value) or frontmatter delimiters.
1042
- // These appear when the LLM leaks frontmatter content into the body, causing
1043
- // auto-repair to produce description: "description: Key Takeaways".
1044
- const isYamlLike = (l) => /^---/.test(l) || /^[a-z_]+:\s/i.test(l);
1045
- const bodyLines = body.split("\n").map(stripMd);
1046
- // Extract description: first body line that BOTH looks like prose AND
1047
- // passes isValidDescription. If nothing qualifies, leave the field
1048
- // missing — the lint pass will reject the proposal cleanly.
1049
- let descLine;
1050
- for (const l of bodyLines) {
1051
- if (isYamlLike(l))
1052
- continue;
1053
- if (l.length <= 10 || l.length >= 400)
1054
- continue;
1055
- if (isValidDescription(l, inputRef).ok) {
1056
- descLine = l;
1057
- break;
1058
- }
1059
- }
1060
- // Extract when_to_use: a line starting with "When" / "Use when" / "Apply when"
1061
- // that ALSO passes isValidWhenToUse (rejects circular fallbacks).
1062
- let wtuLine;
1063
- for (const l of bodyLines) {
1064
- if (!/^(when |use when|apply when)/i.test(l))
1065
- continue;
1066
- if (l.length >= 400)
1067
- continue;
1068
- if (isValidWhenToUse(l, inputRef).ok) {
1069
- wtuLine = l;
1070
- break;
1071
- }
1072
- }
1073
- const repairedFm = {
1074
- ...fm,
1075
- ...(missingDesc && descLine ? { description: descLine } : {}),
1076
- ...(missingWtu && wtuLine ? { when_to_use: wtuLine } : {}),
1077
- };
1078
- const fmLines = Object.entries(repairedFm)
1079
- .map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
1080
- .join("\n");
1081
- // Only rewrite content if we actually have at least one field to write.
1082
- // Otherwise leave the original content for the lint pass to reject.
1083
- if (Object.keys(repairedFm).length > 0) {
1084
- content = assembleAssetFromString(fmLines, body);
1085
- }
1086
- }
778
+ content = autoRepairLessonFrontmatter(content, inputRef);
1087
779
  }
1088
- // Description ↔ when_to_use auto-swap normalization (recover ~93% of
1089
- // qwen-9b's `^when\b/i` rejections at zero LLM cost). When the LLM emits
1090
- // a conditional-framed description ("When X happens, do Y") and the
1091
- // when_to_use field looks like a declarative description (or is empty),
1092
- // the two fields are mis-fielded — exactly what `isValidDescription`'s
1093
- // error message says ("that pattern belongs in when_to_use"). We swap
1094
- // them and revalidate; the swap is committed only if BOTH fields pass
1095
- // their respective validators afterwards. If revalidation still fails,
1096
- // we fall through to the existing reject path.
1097
780
  let descriptionSwapped = 0;
1098
781
  if (effectiveProposalKind !== "knowledge") {
1099
- const parsedSwap = parseFrontmatter(content);
1100
- const fmSwap = (parsedSwap.data ?? {});
1101
- const descRaw = typeof fmSwap.description === "string" ? fmSwap.description.trim() : "";
1102
- const wtuRaw = typeof fmSwap.when_to_use === "string" ? fmSwap.when_to_use.trim() : "";
1103
- const descStartsConditional = /^(when|if)\b/i.test(descRaw);
1104
- const wtuStartsConditional = /^(when|if)\b/i.test(wtuRaw);
1105
- if (descStartsConditional && !wtuStartsConditional && wtuRaw.length > 0) {
1106
- // Try the swap and revalidate. The when_to_use validator requires the
1107
- // value not match `/^when working with\b/i` (the circular fallback) —
1108
- // a real description rarely does, so this usually passes.
1109
- const swappedDescCheck = isValidDescription(wtuRaw, inputRef);
1110
- const swappedWtuCheck = isValidWhenToUse(descRaw, inputRef);
1111
- if (swappedDescCheck.ok && swappedWtuCheck.ok) {
1112
- const swappedFm = {
1113
- ...fmSwap,
1114
- description: wtuRaw,
1115
- when_to_use: descRaw,
1116
- };
1117
- const swappedFmLines = Object.entries(swappedFm)
1118
- .map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
1119
- .join("\n");
1120
- content = assembleAssetFromString(swappedFmLines, parsedSwap.content);
1121
- descriptionSwapped = 1;
1122
- }
1123
- }
782
+ const swapResult = autoSwapDescriptionWhenToUse(content, inputRef);
783
+ content = swapResult.content;
784
+ descriptionSwapped = swapResult.swapped;
1124
785
  }
1125
- // Post-generation truncation repair (#556): if the LLM sliced the
1126
- // description mid-sentence, deterministically complete it from its own text
1127
- // / the lesson body BEFORE the lint + quality validators run. No-op
1128
- // (byte-identical) for already-complete descriptions, so this never alters
1129
- // a valid proposal. Runs on the lesson path only (knowledge has no
1130
- // description field gate here).
1131
786
  if (effectiveProposalKind !== "knowledge") {
1132
- const parsedRepair = parseFrontmatter(content);
1133
- const fmRepair = (parsedRepair.data ?? {});
1134
- const descRepairRaw = typeof fmRepair.description === "string" ? fmRepair.description : "";
1135
- if (descRepairRaw) {
1136
- const repaired = repairTruncatedDescription(descRepairRaw, parsedRepair.content);
1137
- if (repaired !== descRepairRaw) {
1138
- const repairedFmLines = Object.entries({ ...fmRepair, description: repaired })
1139
- .map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
1140
- .join("\n");
1141
- content = assembleAssetFromString(repairedFmLines, parsedRepair.content);
1142
- }
1143
- }
787
+ content = repairLessonDescriptionTruncation(content);
1144
788
  }
1145
789
  // Parse + lint the lesson before creating the proposal. The lint is the
1146
790
  // canonical gate for required frontmatter (v1 spec §13). On failure we
@@ -1149,49 +793,10 @@ export async function akmDistill(options) {
1149
793
  const findings = effectiveProposalKind === "knowledge"
1150
794
  ? validateKnowledgeContent(content, inputRef)
1151
795
  : lintLessonContent(content, `distill:${inputRef}`).findings;
1152
- // Additional quality validators run only on lessons. lesson-lint checks
1153
- // "field is present and non-empty"; these reject the systematic failure
1154
- // modes observed across 323 archived rejected proposals:
1155
- // - description is a body fragment, section heading, or placeholder
1156
- // - when_to_use is the circular "When working with <ref>" fallback
1157
- // - description == when_to_use (LLM duplicated a single sentence)
1158
- // - body contains a second pseudo-frontmatter block
796
+ // Additional lesson-only quality validators reject the systematic failure
797
+ // modes seen across 323 archived rejected proposals (see distill/content-repair).
1159
798
  if (effectiveProposalKind !== "knowledge" && findings.length === 0) {
1160
- const parsedQC = parseFrontmatter(content);
1161
- const fmQC = (parsedQC.data ?? {});
1162
- const descCheck = isValidDescription(fmQC.description, inputRef);
1163
- if (!descCheck.ok) {
1164
- findings.push({
1165
- kind: "invalid-description",
1166
- field: "description",
1167
- message: `Distilled lesson for ${inputRef} has an invalid description: ${descCheck.reason}.`,
1168
- });
1169
- }
1170
- const wtuCheck = isValidWhenToUse(fmQC.when_to_use, inputRef);
1171
- if (!wtuCheck.ok) {
1172
- findings.push({
1173
- kind: "invalid-when_to_use",
1174
- field: "when_to_use",
1175
- message: `Distilled lesson for ${inputRef} has an invalid when_to_use: ${wtuCheck.reason}.`,
1176
- });
1177
- }
1178
- // description and when_to_use must say different things.
1179
- if (descCheck.ok &&
1180
- wtuCheck.ok &&
1181
- typeof fmQC.description === "string" &&
1182
- typeof fmQC.when_to_use === "string" &&
1183
- fmQC.description.trim().toLowerCase() === fmQC.when_to_use.trim().toLowerCase()) {
1184
- findings.push({
1185
- kind: "description-equals-when_to_use",
1186
- field: "description",
1187
- message: `Distilled lesson for ${inputRef} has identical description and when_to_use.`,
1188
- });
1189
- }
1190
- // Double-frontmatter / pseudo-frontmatter pollution in the body.
1191
- const dfm = detectDoubleFrontmatter(content);
1192
- if (dfm) {
1193
- findings.push({ kind: dfm.kind, field: "body", message: `Distilled lesson for ${inputRef}: ${dfm.message}` });
1194
- }
799
+ findings.push(...collectLessonQualityFindings(content, inputRef));
1195
800
  }
1196
801
  if (findings.length > 0) {
1197
802
  appendEvent({
@@ -1203,6 +808,7 @@ export async function akmDistill(options) {
1203
808
  proposalKind: effectiveProposalKind,
1204
809
  findingKinds: findings.map((f) => f.kind),
1205
810
  ...(exclusionSet.size > 0 ? { filteredFeedbackCount } : {}),
811
+ ...eligMeta,
1206
812
  },
1207
813
  });
1208
814
  const message = findings.map((f) => f.message).join("\n");
@@ -1211,7 +817,8 @@ export async function akmDistill(options) {
1211
817
  : "Lessons require non-empty `description` and `when_to_use` frontmatter fields. See v1 spec §13.");
1212
818
  }
1213
819
  // LLM-as-judge quality gate (P2-B). Only active when the feature flag is
1214
- // explicitly enabled. Fail-open: judge failures always pass through.
820
+ // explicitly enabled. Fail-CLOSED (07 P0-2): an unjudgeable proposal (no LLM
821
+ // / timeout / parse failure) is rejected, not passed through.
1215
822
  // D-5 / #388: Three-band system — review_needed band queues a proposal
1216
823
  // with review_needed outcome rather than auto-rejecting.
1217
824
  let lessonJudgeConfidence;
@@ -1224,16 +831,42 @@ export async function akmDistill(options) {
1224
831
  return writeQualityRejection(stash, inputRef, effectiveLessonRef, content, judgeResult.score, judgeResult.reason, {
1225
832
  reviewNeeded: true,
1226
833
  ...(exclusionSet.size > 0 ? { filteredFeedbackCount, feedbackFullyFiltered } : {}),
1227
- });
834
+ }, options.eligibilitySource);
1228
835
  }
1229
- return writeQualityRejection(stash, inputRef, effectiveLessonRef, content, judgeResult.score, judgeResult.reason, exclusionSet.size > 0 ? { filteredFeedbackCount, feedbackFullyFiltered } : {});
836
+ return writeQualityRejection(stash, inputRef, effectiveLessonRef, content, judgeResult.score, judgeResult.reason, exclusionSet.size > 0 ? { filteredFeedbackCount, feedbackFullyFiltered } : {}, options.eligibilitySource);
1230
837
  }
1231
- // Normalize 1-5 judge score to [0, 1]. Score of -1 means pass-through
1232
- // (no LLM / timeout / parse failure) leave confidence undefined so
1233
- // the auto-accept gate treats the proposal as unscored and skips it.
838
+ // Normalize 1-5 judge score to [0, 1]. Only a real passing verdict
839
+ // reaches here (07 P0-2: the judge now fails CLOSED on no-LLM / timeout /
840
+ // parse failure, so those return pass:false and never fall through to
841
+ // this line). A defensive score>0 guard keeps confidence undefined for any
842
+ // non-positive score the auto-accept gate should treat as unscored.
1234
843
  if (judgeResult.score > 0)
1235
844
  lessonJudgeConfidence = judgeResult.score / 5;
1236
845
  }
846
+ // WS-3b: Distill→source fidelity check (step 10).
847
+ // When fidelityCheck.enabled, check the distill proposal against its cited
848
+ // source memories. A contradiction flag routes to human review (not auto-accept).
849
+ // DEFAULT OFF. Fail-open: any error is treated as no-contradiction.
850
+ const fidelityConfig = getImproveProcessConfig(config, "distill", options.improveProfile)?.fidelityCheck ?? {};
851
+ if (fidelityConfig.enabled && assetContent) {
852
+ try {
853
+ const proposalBody = stripBodyForFidelity(content);
854
+ const sourceBodies = [stripBodyForFidelity(assetContent)];
855
+ const fidelityResult = checkDistillFidelity(proposalBody, sourceBodies, fidelityConfig);
856
+ if (fidelityResult.contradictionDetected) {
857
+ // Route to human review by writing a quality rejection with reviewNeeded=true.
858
+ return writeQualityRejection(stash, inputRef, effectiveLessonRef, content, 2.0, // below auto-accept threshold, signals review needed
859
+ fidelityResult.reason ?? "Proposal may contradict cited source memories.", {
860
+ reviewNeeded: true,
861
+ fidelityContradiction: true,
862
+ ...(exclusionSet.size > 0 ? { filteredFeedbackCount, feedbackFullyFiltered } : {}),
863
+ }, options.eligibilitySource);
864
+ }
865
+ }
866
+ catch {
867
+ // Fail open — fidelity check is supplemental.
868
+ }
869
+ }
1237
870
  // Round-trip the parsed frontmatter so the proposal carries it as a
1238
871
  // structured payload alongside the raw content (matches the shape used by
1239
872
  // other proposal sources).
@@ -1256,6 +889,8 @@ export async function akmDistill(options) {
1256
889
  frontmatter: frontmatterWithSources,
1257
890
  },
1258
891
  ...(lessonJudgeConfidence !== undefined ? { confidence: lessonJudgeConfidence } : {}),
892
+ // Attribution tagging: persist the eligibility lane on the proposal.
893
+ ...(options.eligibilitySource ? { eligibilitySource: options.eligibilitySource } : {}),
1259
894
  }, options.ctx);
1260
895
  if (isProposalSkipped(proposalResult2)) {
1261
896
  appendEvent({
@@ -1266,6 +901,7 @@ export async function akmDistill(options) {
1266
901
  lessonRef: effectiveLessonRef,
1267
902
  message: proposalResult2.message,
1268
903
  skipReason: proposalResult2.reason,
904
+ ...eligMeta,
1269
905
  },
1270
906
  });
1271
907
  return {
@@ -1278,6 +914,10 @@ export async function akmDistill(options) {
1278
914
  };
1279
915
  }
1280
916
  const proposal2 = proposalResult2;
917
+ // G4: content-score the distilled OUTPUT so it carries a real encoding
918
+ // salience (encoding_source='content') from creation — lessons never get
919
+ // another chance (they are refused as distill inputs).
920
+ persistOutputEncodingSalience(effectiveLessonRef, content, existingRefVocabulary, outcomeWeightEnabled);
1281
921
  appendEvent({
1282
922
  eventType: "distill_invoked",
1283
923
  ref: inputRef,
@@ -1287,9 +927,13 @@ export async function akmDistill(options) {
1287
927
  proposalRef: effectiveLessonRef,
1288
928
  proposalKind: effectiveProposalKind,
1289
929
  proposalId: proposal2.id,
930
+ // R3: judge verdicts are longitudinally queryable, not just a one-shot
931
+ // proposal.confidence write (normalized 1–5 score / 5).
932
+ ...(lessonJudgeConfidence !== undefined ? { judgeConfidence: lessonJudgeConfidence } : {}),
1290
933
  ...(options.sourceRun !== undefined ? { sourceRun: options.sourceRun } : {}),
1291
934
  ...(exclusionSet.size > 0 ? { filteredFeedbackCount } : {}),
1292
935
  ...(descriptionSwapped > 0 ? { descriptionSwapped } : {}),
936
+ ...eligMeta,
1293
937
  },
1294
938
  });
1295
939
  return {