akm-cli 0.9.0-rc.0 → 0.9.0-rc.13

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 (598) hide show
  1. package/CHANGELOG.md +1283 -22
  2. package/README.md +62 -37
  3. package/SECURITY.md +46 -31
  4. package/dist/akm +162 -38
  5. package/dist/akm-migrate +44 -0
  6. package/dist/assets/backends/schtasks-template.xml +2 -1
  7. package/dist/assets/hints/cli-hints-full.md +268 -118
  8. package/dist/assets/hints/cli-hints-short.md +87 -24
  9. package/dist/assets/{profiles → improve-strategies}/catchup.json +3 -1
  10. package/dist/assets/{profiles → improve-strategies}/consolidate.json +3 -1
  11. package/dist/assets/{profiles → improve-strategies}/default.json +6 -7
  12. package/dist/assets/improve-strategies/frequent.json +15 -0
  13. package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +4 -2
  14. package/dist/assets/{profiles → improve-strategies}/memory-focus.json +4 -1
  15. package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +5 -5
  16. package/dist/assets/{profiles → improve-strategies}/quick.json +4 -2
  17. package/dist/assets/improve-strategies/reflect-distill.json +30 -0
  18. package/dist/assets/{profiles → improve-strategies}/thorough.json +1 -1
  19. package/dist/assets/prompts/consolidate-system.md +5 -5
  20. package/dist/assets/prompts/extract-session.md +2 -6
  21. package/dist/assets/prompts/memory-infer-user.md +2 -3
  22. package/dist/assets/prompts/reflect-llm-framed-contract.md +11 -0
  23. package/dist/assets/prompts/reflect-llm-schema-contract.md +3 -0
  24. package/dist/assets/prompts/reflect-output-repair.md +3 -0
  25. package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
  26. package/dist/assets/stash-skeleton/README.md +38 -10
  27. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +8 -0
  28. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +8 -0
  29. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +14 -1
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +13 -1
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +9 -1
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +11 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +9 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +9 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +8 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +100 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/domains.md +64 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/organization.md +136 -0
  39. package/dist/assets/tasks/core/extract.yml +3 -2
  40. package/dist/assets/tasks/core/improve.yml +2 -1
  41. package/dist/assets/tasks/core/index-refresh.yml +1 -0
  42. package/dist/assets/tasks/core/sync.yml +1 -0
  43. package/dist/assets/tasks/core/version-check.yml +2 -1
  44. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
  45. package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
  46. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
  47. package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
  48. package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
  49. package/dist/assets/templates/html/health.html +5 -4
  50. package/dist/assets/workflows/workflow-template.md +31 -15
  51. package/dist/cli/invocation.js +279 -0
  52. package/dist/cli/parse-args.js +5 -90
  53. package/dist/cli/retired-commands.js +78 -0
  54. package/dist/cli/shared.js +158 -48
  55. package/dist/cli-node.mjs +2 -1
  56. package/dist/cli.js +747 -293
  57. package/dist/commands/agent/agent-dispatch.js +19 -18
  58. package/dist/commands/agent/agent-support.js +0 -24
  59. package/dist/commands/agent/contribute-cli.js +43 -97
  60. package/dist/commands/completions.js +80 -23
  61. package/dist/commands/config-cli.js +44 -281
  62. package/dist/commands/env/env-binding.js +99 -0
  63. package/dist/commands/env/env-cli.js +84 -224
  64. package/dist/commands/env/env.js +12 -163
  65. package/dist/commands/env/marker-path.js +6 -0
  66. package/dist/commands/env/secret-cli.js +45 -61
  67. package/dist/commands/env/secret.js +32 -62
  68. package/dist/commands/feedback-cli.js +179 -85
  69. package/dist/commands/health/accept-rate.js +58 -0
  70. package/dist/commands/health/advisories.js +7 -8
  71. package/dist/commands/health/checks.js +279 -94
  72. package/dist/commands/health/html-report.js +197 -578
  73. package/dist/commands/health/improve-metrics.js +277 -246
  74. package/dist/commands/health/llm-usage.js +19 -19
  75. package/dist/commands/health/md-report.js +16 -7
  76. package/dist/commands/health/metrics.js +67 -32
  77. package/dist/commands/health/renderers.js +47 -0
  78. package/dist/commands/health/report-view-model.js +508 -0
  79. package/dist/commands/health/stash-exposure.js +1 -1
  80. package/dist/commands/health/surfaces.js +16 -56
  81. package/dist/commands/health/task-runs.js +3 -67
  82. package/dist/{migrate-storage-node.mjs → commands/health/types-checks.js} +1 -5
  83. package/dist/commands/health/types-improve.js +29 -0
  84. package/dist/{output/text/save.js → commands/health/types-metrics.js} +1 -2
  85. package/dist/commands/health/types-result.js +7 -0
  86. package/dist/commands/health/types-runs.js +4 -0
  87. package/dist/commands/health/types-session-log.js +4 -0
  88. package/dist/commands/health/types-windows.js +4 -0
  89. package/dist/commands/health/types.js +26 -21
  90. package/dist/commands/health/windows.js +2 -3
  91. package/dist/commands/health.js +296 -167
  92. package/dist/commands/improve/anti-collapse.js +5 -5
  93. package/dist/commands/improve/autonomy-gate.js +68 -0
  94. package/dist/commands/improve/collapse-detector.js +65 -52
  95. package/dist/commands/improve/consolidate/chunking.js +9 -7
  96. package/dist/commands/improve/consolidate/eligibility.js +1 -23
  97. package/dist/commands/improve/consolidate/merge.js +4 -0
  98. package/dist/commands/improve/consolidate.js +454 -1354
  99. package/dist/commands/improve/content-hash.js +39 -0
  100. package/dist/commands/improve/distill/content-repair.js +4 -10
  101. package/dist/commands/improve/distill/promote-memory.js +89 -64
  102. package/dist/commands/improve/distill/quality-gate.js +118 -42
  103. package/dist/commands/improve/distill-guards.js +1 -1
  104. package/dist/commands/improve/distill-promotion-policy.js +33 -888
  105. package/dist/commands/improve/distill.js +607 -363
  106. package/dist/commands/improve/eligibility.js +165 -79
  107. package/dist/commands/improve/extract-cli.js +35 -126
  108. package/dist/commands/improve/extract-prompt.js +6 -35
  109. package/dist/commands/improve/extract.js +640 -391
  110. package/dist/commands/improve/feedback-valence.js +2 -12
  111. package/dist/commands/improve/improve-cli.js +134 -135
  112. package/dist/commands/improve/improve-result-file.js +30 -50
  113. package/dist/commands/improve/improve-run-types.js +4 -0
  114. package/dist/commands/improve/improve-strategies.js +135 -0
  115. package/dist/commands/improve/improve.js +904 -701
  116. package/dist/commands/improve/locks.js +64 -111
  117. package/dist/commands/improve/loop-stages.js +1110 -923
  118. package/dist/commands/improve/memory/derived-ref.js +124 -0
  119. package/dist/commands/improve/memory/memory-belief.js +79 -7
  120. package/dist/commands/improve/memory/memory-contradiction-detect.js +49 -52
  121. package/dist/commands/improve/memory/memory-improve.js +25 -37
  122. package/dist/commands/improve/outcome-loop.js +25 -88
  123. package/dist/commands/improve/preparation.js +1034 -813
  124. package/dist/commands/improve/proactive-maintenance.js +34 -9
  125. package/dist/commands/improve/proposal-envelope.js +31 -0
  126. package/dist/commands/improve/reflect.js +983 -794
  127. package/dist/commands/improve/run-context.js +119 -0
  128. package/dist/commands/improve/salience.js +24 -127
  129. package/dist/commands/improve/session-asset.js +7 -3
  130. package/dist/commands/improve/shared.js +14 -34
  131. package/dist/commands/improve/source-identity.js +28 -0
  132. package/dist/commands/improve/triage.js +20 -17
  133. package/dist/commands/lint/base-linter.js +340 -313
  134. package/dist/commands/lint/env-key-rules.js +31 -47
  135. package/dist/commands/lint/index.js +185 -30
  136. package/dist/commands/{events.js → log.js} +28 -38
  137. package/dist/commands/migrate-cli.js +54 -0
  138. package/dist/commands/migration-tool.js +55 -0
  139. package/dist/commands/observability-cli.js +70 -208
  140. package/dist/commands/proposal/diff-format.js +50 -0
  141. package/dist/commands/proposal/drain-policies.js +0 -6
  142. package/dist/commands/proposal/drain.js +91 -40
  143. package/dist/commands/proposal/proposal-cli.js +134 -132
  144. package/dist/commands/proposal/proposal-types.js +56 -0
  145. package/dist/commands/proposal/proposal.js +83 -65
  146. package/dist/commands/proposal/propose-cli.js +88 -0
  147. package/dist/commands/proposal/propose.js +105 -88
  148. package/dist/commands/proposal/repository.js +1303 -278
  149. package/dist/commands/proposal/validators/proposal-quality-validators.js +16 -6
  150. package/dist/commands/proposal/validators/proposal-validators.js +61 -12
  151. package/dist/commands/proposal/validators/proposals.js +6 -8
  152. package/dist/commands/read/curate.js +78 -73
  153. package/dist/commands/read/knowledge.js +510 -13
  154. package/dist/commands/read/registry-search.js +2 -2
  155. package/dist/commands/read/remember-cli.js +84 -15
  156. package/dist/commands/read/search-cli.js +203 -96
  157. package/dist/commands/read/search.js +126 -94
  158. package/dist/commands/read/show.js +226 -250
  159. package/dist/commands/registry-cli.js +34 -60
  160. package/dist/commands/remember.js +18 -57
  161. package/dist/commands/sources/add-cli.js +104 -49
  162. package/dist/commands/sources/bundle-cli.js +166 -0
  163. package/dist/commands/sources/bundle-config-ops.js +63 -0
  164. package/dist/commands/sources/info.js +27 -15
  165. package/dist/commands/sources/init.js +30 -40
  166. package/dist/commands/sources/installed-stashes.js +469 -172
  167. package/dist/commands/sources/migration-help.js +7 -4
  168. package/dist/commands/sources/schema-repair.js +10 -9
  169. package/dist/commands/sources/self-update.js +182 -121
  170. package/dist/commands/sources/source-add.js +169 -178
  171. package/dist/commands/sources/source-clone.js +144 -41
  172. package/dist/commands/sources/source-manage.js +94 -59
  173. package/dist/commands/sources/sources-cli.js +64 -205
  174. package/dist/commands/sources/stash-cli.js +91 -54
  175. package/dist/commands/sources/stash-skeleton.js +1 -1
  176. package/dist/commands/tasks/tasks-cli.js +106 -104
  177. package/dist/commands/tasks/tasks.js +445 -262
  178. package/dist/commands/workflow-cli.js +232 -121
  179. package/dist/core/action-contributors.js +1 -1
  180. package/dist/core/activation-policy.js +49 -0
  181. package/dist/core/adapter/adapters/agent-skills-adapter.js +181 -0
  182. package/dist/core/adapter/adapters/akm-adapter.js +528 -0
  183. package/dist/core/adapter/adapters/akm-lint.js +392 -0
  184. package/dist/core/adapter/adapters/akm-metadata.js +387 -0
  185. package/dist/core/adapter/adapters/akm-task-adapter.js +149 -0
  186. package/dist/core/adapter/adapters/akm-workflow-adapter.js +180 -0
  187. package/dist/core/adapter/adapters/claude-adapter.js +61 -0
  188. package/dist/core/adapter/adapters/dotenv-adapter.js +187 -0
  189. package/dist/core/adapter/adapters/generic-files-adapter.js +119 -0
  190. package/dist/core/adapter/adapters/index.js +80 -0
  191. package/dist/core/adapter/adapters/llm-wiki-adapter.js +419 -0
  192. package/dist/core/adapter/adapters/okf-adapter.js +391 -0
  193. package/dist/core/adapter/adapters/opencode-adapter.js +68 -0
  194. package/dist/core/adapter/adapters/shared.js +286 -0
  195. package/dist/core/adapter/adapters/tool-dir-shared.js +217 -0
  196. package/dist/core/adapter/adapters/website-snapshot-adapter.js +155 -0
  197. package/dist/core/adapter/bundle-adapter.js +4 -0
  198. package/dist/core/adapter/detect-adapter.js +17 -0
  199. package/dist/core/adapter/recognize-match.js +44 -0
  200. package/dist/core/adapter/registry.js +56 -0
  201. package/dist/core/adapter/types.js +4 -0
  202. package/dist/core/asset/akm-markdown.js +30 -0
  203. package/dist/core/asset/asset-placement.js +243 -0
  204. package/dist/core/asset/asset-ref.js +110 -79
  205. package/dist/core/asset/asset-serialize.js +20 -0
  206. package/dist/core/asset/frontmatter.js +28 -12
  207. package/dist/core/asset/markdown.js +40 -51
  208. package/dist/core/asset/resolve-ref.js +274 -0
  209. package/dist/core/asset/stash-meta.js +2 -2
  210. package/dist/core/bundle-id.js +51 -0
  211. package/dist/core/common.js +281 -86
  212. package/dist/core/config/config-io.js +42 -128
  213. package/dist/core/config/config-schema.js +233 -834
  214. package/dist/core/config/config-sources.js +162 -39
  215. package/dist/core/config/config-types.js +16 -11
  216. package/dist/core/config/config-version.js +29 -0
  217. package/dist/core/config/config-walker.js +126 -37
  218. package/dist/core/config/config.js +154 -331
  219. package/dist/core/config/deep-merge.js +41 -0
  220. package/dist/core/config/engine-semantics.js +28 -0
  221. package/dist/core/config/experimental.js +21 -0
  222. package/dist/core/config/schema/embedding.js +38 -0
  223. package/dist/core/config/schema/engines.js +116 -0
  224. package/dist/core/config/schema/experimental.js +47 -0
  225. package/dist/core/config/schema/feedback.js +31 -0
  226. package/dist/core/config/schema/improve-processes.js +389 -0
  227. package/dist/core/config/schema/improve.js +94 -0
  228. package/dist/core/config/schema/index-config.js +176 -0
  229. package/dist/core/config/schema/output.js +18 -0
  230. package/dist/core/config/schema/primitives.js +94 -0
  231. package/dist/core/config/schema/search.js +30 -0
  232. package/dist/core/config/schema/setup.js +18 -0
  233. package/dist/core/config/schema/sources-bundles.js +169 -0
  234. package/dist/core/config/schema/workflow.js +29 -0
  235. package/dist/core/env-secret-ref.js +155 -20
  236. package/dist/core/errors.js +17 -15
  237. package/dist/core/events-types.js +4 -0
  238. package/dist/core/events.js +46 -128
  239. package/dist/core/extra-params.js +62 -0
  240. package/dist/core/file-change.js +17 -0
  241. package/dist/core/file-lock.js +202 -57
  242. package/dist/core/fs-txn.js +392 -0
  243. package/dist/core/git-message.js +59 -0
  244. package/dist/core/improve-result.js +167 -0
  245. package/dist/core/json-schema.js +142 -0
  246. package/dist/core/lesson-lint.js +1 -17
  247. package/dist/core/logs-db.js +1 -1
  248. package/dist/core/maintenance-barrier.js +135 -0
  249. package/dist/core/migration-operation.js +44 -0
  250. package/dist/core/mutation-target.js +78 -0
  251. package/dist/core/paths.js +22 -25
  252. package/dist/core/platform.js +10 -0
  253. package/dist/core/recognition-util.js +128 -0
  254. package/dist/core/redaction.js +392 -0
  255. package/dist/core/standards/resolve-standards-context.js +36 -65
  256. package/dist/core/standards/resolve-stash-standards.js +2 -2
  257. package/dist/core/standards/resolve-type-conventions.js +5 -5
  258. package/dist/core/state/migrations.js +242 -11
  259. package/dist/core/state-db.js +98 -10
  260. package/dist/core/structured.js +1 -1
  261. package/dist/core/subprocess.js +303 -0
  262. package/dist/core/text-truncation.js +9 -5
  263. package/dist/core/time.js +20 -0
  264. package/dist/core/type-presentation.js +130 -0
  265. package/dist/core/warn.js +0 -3
  266. package/dist/core/write-source.js +834 -118
  267. package/dist/indexer/bundle-identity-guard.js +92 -0
  268. package/dist/indexer/db/graph-db.js +1 -25
  269. package/dist/indexer/db/llm-cache.js +1 -1
  270. package/dist/indexer/ensure-index.js +30 -9
  271. package/dist/indexer/graph/graph-boost.js +9 -30
  272. package/dist/indexer/graph/graph-extraction.js +41 -27
  273. package/dist/indexer/graph/graph-types.js +4 -0
  274. package/dist/indexer/index-writer-lock.js +93 -49
  275. package/dist/indexer/index-written-assets.js +100 -53
  276. package/dist/indexer/indexer.js +746 -329
  277. package/dist/indexer/init.js +18 -25
  278. package/dist/indexer/installations.js +142 -0
  279. package/dist/indexer/passes/dir-staleness.js +18 -10
  280. package/dist/indexer/passes/memory-inference.js +25 -15
  281. package/dist/indexer/passes/metadata.js +412 -243
  282. package/dist/indexer/scan/doc-to-entry.js +160 -0
  283. package/dist/indexer/scan/drain-dir.js +134 -0
  284. package/dist/indexer/search/db-search.js +292 -108
  285. package/dist/indexer/search/fts-query.js +64 -0
  286. package/dist/indexer/search/ranking-contributors.js +145 -25
  287. package/dist/indexer/search/ranking-types.js +4 -0
  288. package/dist/indexer/search/ranking.js +28 -71
  289. package/dist/indexer/search/search-attribution.js +67 -0
  290. package/dist/indexer/search/search-fields.js +18 -3
  291. package/dist/indexer/search/search-hit-enrichers.js +30 -40
  292. package/dist/indexer/search/search-source.js +157 -111
  293. package/dist/indexer/search/semantic-status.js +4 -1
  294. package/dist/indexer/usage/usage-events.js +10 -30
  295. package/dist/indexer/walk/file-context.js +3 -45
  296. package/dist/indexer/walk/matchers.js +42 -34
  297. package/dist/indexer/walk/path-resolver.js +11 -5
  298. package/dist/indexer/walk/walker.js +42 -14
  299. package/dist/integrations/agent/builder-shared.js +7 -0
  300. package/dist/integrations/agent/builders.js +5 -56
  301. package/dist/integrations/agent/config.js +3 -143
  302. package/dist/integrations/agent/detect.js +17 -2
  303. package/dist/integrations/agent/engine-resolution.js +231 -0
  304. package/dist/integrations/agent/index.js +1 -2
  305. package/dist/integrations/agent/model-aliases.js +16 -2
  306. package/dist/integrations/agent/profiles.js +36 -62
  307. package/dist/integrations/agent/prompts.js +46 -18
  308. package/dist/integrations/agent/runner-dispatch.js +93 -4
  309. package/dist/integrations/agent/runner.js +76 -208
  310. package/dist/integrations/agent/spawn.js +88 -196
  311. package/dist/integrations/harnesses/aider/agent-builder.js +114 -0
  312. package/dist/integrations/harnesses/aider/index.js +48 -0
  313. package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
  314. package/dist/integrations/harnesses/amazonq/agent-builder.js +147 -0
  315. package/dist/integrations/harnesses/amazonq/index.js +45 -0
  316. package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
  317. package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
  318. package/dist/integrations/harnesses/claude/config-import.js +1 -3
  319. package/dist/integrations/harnesses/claude/index.js +24 -35
  320. package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
  321. package/dist/integrations/harnesses/claude/session-log.js +27 -75
  322. package/dist/integrations/harnesses/codex/agent-builder.js +138 -0
  323. package/dist/integrations/harnesses/codex/index.js +52 -0
  324. package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
  325. package/dist/integrations/harnesses/copilot/agent-builder.js +122 -0
  326. package/dist/integrations/harnesses/copilot/index.js +48 -0
  327. package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
  328. package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
  329. package/dist/integrations/harnesses/gemini/index.js +48 -0
  330. package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
  331. package/dist/integrations/harnesses/ids.js +24 -0
  332. package/dist/integrations/harnesses/index.js +54 -34
  333. package/dist/integrations/harnesses/opencode/agent-builder.js +23 -5
  334. package/dist/integrations/harnesses/opencode/config-import.js +1 -3
  335. package/dist/integrations/harnesses/opencode/index.js +14 -32
  336. package/dist/integrations/harnesses/opencode/session-log.js +67 -125
  337. package/dist/integrations/harnesses/opencode-sdk/harness.js +51 -0
  338. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
  339. package/dist/integrations/harnesses/openhands/agent-builder.js +128 -0
  340. package/dist/integrations/harnesses/openhands/index.js +48 -0
  341. package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
  342. package/dist/integrations/harnesses/pi/agent-builder.js +97 -0
  343. package/dist/integrations/harnesses/pi/index.js +45 -0
  344. package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
  345. package/dist/integrations/harnesses/shared.js +17 -0
  346. package/dist/integrations/harnesses/types.js +43 -32
  347. package/dist/integrations/lockfile.js +211 -24
  348. package/dist/integrations/session-logs/index.js +36 -39
  349. package/dist/integrations/session-logs/provider-base.js +113 -0
  350. package/dist/llm/client.js +182 -110
  351. package/dist/llm/embedders/deterministic.js +2 -2
  352. package/dist/llm/embedders/remote.js +21 -9
  353. package/dist/llm/feature-gate.js +17 -57
  354. package/dist/llm/graph-extract.js +12 -13
  355. package/dist/llm/index-passes.js +8 -42
  356. package/dist/llm/memory-infer.js +144 -1
  357. package/dist/llm/metadata-enhance.js +45 -30
  358. package/dist/llm/structured-call.js +16 -8
  359. package/dist/llm/usage-persist.js +30 -5
  360. package/dist/llm/usage-telemetry.js +59 -6
  361. package/dist/output/cli-hints.js +1 -2
  362. package/dist/output/command-registry.js +27 -0
  363. package/dist/output/context.js +22 -7
  364. package/dist/output/format-exempt.js +80 -0
  365. package/dist/output/generic-render.js +251 -0
  366. package/dist/output/html-render.js +11 -16
  367. package/dist/output/render-registry.js +57 -0
  368. package/dist/output/renderers.js +14 -279
  369. package/dist/output/shapes/curate.js +10 -1
  370. package/dist/output/shapes/events.js +12 -7
  371. package/dist/output/shapes/helpers.js +58 -84
  372. package/dist/output/shapes/passthrough.js +11 -39
  373. package/dist/output/shapes/proposal/producer.js +15 -7
  374. package/dist/output/shapes/registry.js +12 -6
  375. package/dist/output/shapes.js +0 -9
  376. package/dist/output/text/{init.js → bundle-create.js} +3 -1
  377. package/dist/output/text/bundle-show.js +7 -0
  378. package/dist/output/text/command-format.js +562 -0
  379. package/dist/output/text/env.js +1 -3
  380. package/dist/output/text/events.js +8 -7
  381. package/dist/output/text/helpers.js +15 -1164
  382. package/dist/output/text/proposal/producer.js +4 -2
  383. package/dist/output/text/proposal-format.js +202 -0
  384. package/dist/output/text/registry-commands.js +1 -2
  385. package/dist/output/text/registry.js +12 -6
  386. package/dist/output/text/show-directives.js +117 -0
  387. package/dist/output/text/show-format.js +103 -0
  388. package/dist/output/text/sync.js +5 -0
  389. package/dist/output/text/workflow-format.js +332 -0
  390. package/dist/output/text/workflow.js +3 -2
  391. package/dist/output/text.js +10 -19
  392. package/dist/registry/factory.js +4 -6
  393. package/dist/registry/origin-resolve.js +16 -27
  394. package/dist/registry/providers/skills-sh.js +3 -3
  395. package/dist/registry/providers/static-index.js +15 -25
  396. package/dist/registry/resolve.js +43 -94
  397. package/dist/registry/semver.js +43 -0
  398. package/dist/runtime.js +81 -12
  399. package/dist/scripts/akm-migrate.js +35529 -0
  400. package/dist/setup/detect.js +5 -7
  401. package/dist/setup/detected-engines.js +136 -0
  402. package/dist/setup/engine-config.js +100 -0
  403. package/dist/setup/registry-stash-loader.js +3 -3
  404. package/dist/setup/semantic-assets.js +12 -9
  405. package/dist/setup/setup.js +444 -208
  406. package/dist/setup/steps/connection-shared.js +120 -0
  407. package/dist/setup/steps/connection.js +108 -305
  408. package/dist/setup/steps/platforms.js +13 -12
  409. package/dist/setup/steps/semantic.js +15 -3
  410. package/dist/setup/steps/sources.js +21 -15
  411. package/dist/setup/steps/stashdir.js +6 -4
  412. package/dist/setup/steps/tasks.js +236 -119
  413. package/dist/setup/steps.js +3 -2
  414. package/dist/sources/freshness.js +39 -0
  415. package/dist/sources/provider-factory.js +11 -17
  416. package/dist/sources/providers/filesystem.js +2 -3
  417. package/dist/sources/providers/git-install.js +278 -34
  418. package/dist/sources/providers/git-provider.js +54 -56
  419. package/dist/sources/providers/git-stash.js +420 -91
  420. package/dist/sources/providers/git.js +2 -2
  421. package/dist/sources/providers/npm.js +16 -19
  422. package/dist/sources/providers/provider-utils.js +47 -22
  423. package/dist/sources/providers/sync-from-ref.js +3 -9
  424. package/dist/sources/providers/website.js +2 -2
  425. package/dist/sources/resolve.js +11 -10
  426. package/dist/sources/snapshot-fetchers/types.js +4 -0
  427. package/dist/sources/{website-ingest.js → snapshot-fetchers/website-ingest.js} +110 -41
  428. package/dist/storage/database.js +60 -4
  429. package/dist/storage/engines/sqlite-migrations.js +156 -5
  430. package/dist/storage/locations.js +1 -2
  431. package/dist/storage/repositories/canaries-repository.js +1 -1
  432. package/dist/storage/repositories/events-repository.js +51 -11
  433. package/dist/storage/repositories/improve-runs-repository.js +6 -32
  434. package/dist/storage/repositories/index-connection.js +79 -0
  435. package/dist/storage/repositories/index-db.js +4 -3
  436. package/dist/storage/repositories/index-entries-repository.js +863 -0
  437. package/dist/{indexer/db/entry-mapper.js → storage/repositories/index-entry-mapper.js} +19 -2
  438. package/dist/storage/repositories/index-entry-types.js +4 -0
  439. package/dist/storage/repositories/index-fts-repository.js +167 -0
  440. package/dist/storage/repositories/index-llm-cache-repository.js +108 -0
  441. package/dist/storage/repositories/index-meta-repository.js +49 -0
  442. package/dist/{indexer/db/schema.js → storage/repositories/index-schema.js} +226 -100
  443. package/dist/storage/repositories/index-sql.js +12 -0
  444. package/dist/storage/repositories/index-utility-repository.js +356 -0
  445. package/dist/storage/repositories/index-vec-repository.js +250 -0
  446. package/dist/storage/repositories/outcome-repository.js +119 -0
  447. package/dist/storage/repositories/proposals-repository.js +317 -75
  448. package/dist/storage/repositories/registry-cache.js +1 -1
  449. package/dist/storage/repositories/salience-repository.js +172 -0
  450. package/dist/storage/repositories/task-history-repository.js +110 -3
  451. package/dist/storage/repositories/workflow-runs-repository.js +240 -19
  452. package/dist/tasks/backends/cron.js +169 -46
  453. package/dist/tasks/backends/exec-utils.js +76 -3
  454. package/dist/tasks/backends/index.js +6 -9
  455. package/dist/tasks/backends/launchd.js +292 -55
  456. package/dist/tasks/backends/schtasks.js +557 -70
  457. package/dist/tasks/backends/types.js +4 -0
  458. package/dist/tasks/command-executable.js +93 -0
  459. package/dist/tasks/embedded.js +56 -38
  460. package/dist/tasks/parser.js +156 -64
  461. package/dist/tasks/resolve-akm-bin.js +144 -51
  462. package/dist/tasks/runner.js +377 -209
  463. package/dist/tasks/schedule.js +108 -19
  464. package/dist/tasks/scheduler-invocation.js +296 -0
  465. package/dist/tasks/schema.js +1 -1
  466. package/dist/tasks/task-id.js +35 -0
  467. package/dist/tasks/validator.js +30 -16
  468. package/dist/text-import-hook.mjs +1 -1
  469. package/dist/workflows/authoring/authoring.js +104 -43
  470. package/dist/workflows/authoring/scope-key.js +1 -1
  471. package/dist/workflows/cli.js +0 -16
  472. package/dist/workflows/concurrency-policy.js +15 -0
  473. package/dist/workflows/exec/brief.js +450 -0
  474. package/dist/workflows/exec/frozen-judge.js +47 -0
  475. package/dist/workflows/exec/native-executor.js +1038 -0
  476. package/dist/workflows/exec/param-secrets.js +115 -0
  477. package/dist/workflows/exec/report.js +1460 -0
  478. package/dist/workflows/exec/run-workflow.js +602 -0
  479. package/dist/workflows/exec/scheduler.js +71 -0
  480. package/dist/workflows/exec/step-work.js +1190 -0
  481. package/dist/workflows/exec/unit-writer.js +23 -0
  482. package/dist/workflows/exec/workflow-engine-gate.js +67 -0
  483. package/dist/workflows/exec/worktree.js +171 -0
  484. package/dist/workflows/ir/compile.js +246 -0
  485. package/dist/workflows/ir/freeze.js +233 -0
  486. package/dist/workflows/ir/params.js +54 -0
  487. package/dist/workflows/ir/plan-hash.js +68 -0
  488. package/dist/workflows/ir/schema.js +540 -0
  489. package/dist/workflows/parser.js +878 -304
  490. package/dist/workflows/program/expressions.js +181 -0
  491. package/dist/workflows/program/schema.js +51 -0
  492. package/dist/workflows/renderer.js +100 -45
  493. package/dist/workflows/resource-limits.js +22 -0
  494. package/dist/workflows/runtime/agent-identity.js +59 -14
  495. package/dist/workflows/runtime/checkin.js +1 -1
  496. package/dist/workflows/runtime/plan-classifier.js +131 -0
  497. package/dist/workflows/runtime/runs.js +376 -119
  498. package/dist/workflows/runtime/unit-checkin.js +45 -0
  499. package/dist/workflows/runtime/unit-phases.js +20 -0
  500. package/dist/workflows/runtime/workflow-asset-loader.js +241 -40
  501. package/dist/workflows/schema.js +1 -11
  502. package/dist/workflows/validate-summary.js +2 -3
  503. package/dist/workflows/validator.js +52 -30
  504. package/docs/README.md +42 -78
  505. package/docs/migration/README.md +8 -0
  506. package/docs/migration/release-notes/0.6.0.md +1 -1
  507. package/docs/migration/release-notes/0.7.0.md +9 -8
  508. package/docs/migration/release-notes/0.9.0.md +158 -14
  509. package/docs/migration/v0.7-to-v0.8.md +46 -47
  510. package/docs/migration/v0.8-to-v0.9.md +844 -0
  511. package/docs/reference/README.md +12 -0
  512. package/docs/reference/data-and-telemetry.md +333 -0
  513. package/package.json +21 -17
  514. package/schemas/akm-asset-envelope.json +93 -0
  515. package/schemas/akm-config.json +4636 -0
  516. package/schemas/akm-task.json +87 -0
  517. package/schemas/akm-workflow.json +373 -0
  518. package/dist/akm-migrate-storage +0 -38
  519. package/dist/assets/help/help-accept.md +0 -12
  520. package/dist/assets/help/help-improve.md +0 -84
  521. package/dist/assets/help/help-proposals.md +0 -17
  522. package/dist/assets/help/help-propose.md +0 -17
  523. package/dist/assets/help/help-reject.md +0 -11
  524. package/dist/assets/profiles/frequent.json +0 -13
  525. package/dist/assets/profiles/recombine-only.json +0 -21
  526. package/dist/assets/profiles/reflect-distill.json +0 -30
  527. package/dist/assets/profiles/synthesize.json +0 -15
  528. package/dist/assets/prompts/procedural-system.md +0 -44
  529. package/dist/assets/prompts/recombine-system.md +0 -40
  530. package/dist/assets/prompts/staleness-detect-system.md +0 -6
  531. package/dist/assets/tasks/core/backup.yml +0 -4
  532. package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
  533. package/dist/assets/templates/html/default.html +0 -78
  534. package/dist/assets/templates/html/vendor/echarts.min.js +0 -45
  535. package/dist/assets/wiki/index-template.md +0 -12
  536. package/dist/assets/wiki/ingest-workflow-template.md +0 -83
  537. package/dist/assets/wiki/log-template.md +0 -8
  538. package/dist/assets/wiki/schema-template.md +0 -61
  539. package/dist/cli/config-migrate.js +0 -150
  540. package/dist/cli/config-validate.js +0 -39
  541. package/dist/commands/graph/graph-cli.js +0 -124
  542. package/dist/commands/graph/graph.js +0 -487
  543. package/dist/commands/improve/calibration.js +0 -161
  544. package/dist/commands/improve/dedup.js +0 -482
  545. package/dist/commands/improve/extract-watch.js +0 -140
  546. package/dist/commands/improve/hot-probation.js +0 -45
  547. package/dist/commands/improve/improve-auto-accept.js +0 -276
  548. package/dist/commands/improve/improve-profiles.js +0 -168
  549. package/dist/commands/improve/procedural.js +0 -398
  550. package/dist/commands/improve/recombine.js +0 -818
  551. package/dist/commands/improve/schema-similarity-gate.js +0 -168
  552. package/dist/commands/lint/agent-linter.js +0 -44
  553. package/dist/commands/lint/command-linter.js +0 -44
  554. package/dist/commands/lint/default-linter.js +0 -16
  555. package/dist/commands/lint/fact-linter.js +0 -39
  556. package/dist/commands/lint/knowledge-linter.js +0 -16
  557. package/dist/commands/lint/memory-linter.js +0 -61
  558. package/dist/commands/lint/registry.js +0 -41
  559. package/dist/commands/lint/skill-linter.js +0 -45
  560. package/dist/commands/lint/task-linter.js +0 -50
  561. package/dist/commands/lint/workflow-linter.js +0 -81
  562. package/dist/commands/proposal/legacy-import.js +0 -115
  563. package/dist/commands/sources/history.js +0 -196
  564. package/dist/commands/tasks/default-tasks.js +0 -186
  565. package/dist/commands/wiki-cli.js +0 -292
  566. package/dist/core/asset/asset-registry.js +0 -76
  567. package/dist/core/asset/asset-spec.js +0 -259
  568. package/dist/core/config/config-migration.js +0 -602
  569. package/dist/core/deep-merge.js +0 -38
  570. package/dist/core/eval/rank-metrics.js +0 -113
  571. package/dist/core/ripgrep/install.js +0 -163
  572. package/dist/core/ripgrep/resolve.js +0 -81
  573. package/dist/indexer/db/db.js +0 -1413
  574. package/dist/indexer/manifest.js +0 -170
  575. package/dist/indexer/passes/metadata-contributors.js +0 -31
  576. package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -94
  577. package/dist/integrations/harnesses/opencode-sdk/index.js +0 -49
  578. package/dist/llm/call-ai.js +0 -62
  579. package/dist/llm/memory-infer-impl.js +0 -138
  580. package/dist/output/shapes/distill.js +0 -14
  581. package/dist/output/shapes/history.js +0 -11
  582. package/dist/output/text/distill.js +0 -6
  583. package/dist/output/text/enable-disable.js +0 -8
  584. package/dist/output/text/history.js +0 -6
  585. package/dist/output/text/wiki.js +0 -16
  586. package/dist/registry/build-index.js +0 -386
  587. package/dist/scripts/migrate-storage.js +0 -19108
  588. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +0 -9411
  589. package/dist/scripts/migrations/v16-to-v17.js +0 -141
  590. package/dist/setup/legacy-config.js +0 -106
  591. package/dist/storage/repositories/consolidation-repository.js +0 -38
  592. package/dist/storage/repositories/recombine-repository.js +0 -213
  593. package/dist/wiki/wiki-templates.js +0 -15
  594. package/dist/wiki/wiki.js +0 -1012
  595. package/dist/workflows/db.js +0 -215
  596. package/docs/data-and-telemetry.md +0 -226
  597. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/registry.js +0 -0
  598. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/youtube.js +0 -0
@@ -13,7 +13,7 @@
13
13
  *
14
14
  * # Architectural seams
15
15
  *
16
- * - **Single bounded in-tree LLM call.** Wrapped in {@link tryLlmFeature}
16
+ * - **Single bounded in-tree LLM call.** Routed through `callStructured`
17
17
  * under the `distill` gate (v1 spec §14; 0.8.0 unified the orchestration
18
18
  * and LLM-call gates under `processes.distill.enabled`). The wrapper
19
19
  * enforces a hard timeout (default 600s / 10 min — overridable via
@@ -34,14 +34,10 @@
34
34
  *
35
35
  * # Lesson-name derivation rule
36
36
  *
37
- * The proposed lesson ref is `lesson:<original-ref-slug>-lesson`, where
38
- * `<original-ref-slug>` is `<type>-<name>` from the parsed input ref (so
39
- * `skill:deploy` → `lesson:skill-deploy-lesson`, and `team//memory:auth-tips`
40
- * → `lesson:memory-auth-tips-lesson`). Origin prefixes are dropped from the
41
- * derived name so two sources with the same asset-type/name collapse onto
42
- * the same lesson queue entry rather than each generating its own — the
43
- * proposal queue tolerates duplicate refs (id is a UUID), so the human
44
- * reviewer can decide which one to accept.
37
+ * A nested input preserves its first legitimate scope segment
38
+ * (`memories/project-a/deploy` → `lessons/project-a/memory-deploy-lesson`). An
39
+ * unscoped input stays flat; asset types are not project scopes. Origin prefixes
40
+ * remain durable provenance but are not embedded in the output path.
45
41
  *
46
42
  * # Why we do not call `runAgent`
47
43
  *
@@ -53,42 +49,47 @@
53
49
  import fs from "node:fs";
54
50
  import distillKnowledgeSystemPrompt from "../../assets/prompts/distill-knowledge-system.md" with { type: "text" };
55
51
  import distillLessonSystemPrompt from "../../assets/prompts/distill-lesson-system.md" with { type: "text" };
56
- import { parseAssetRef } from "../../core/asset/asset-ref.js";
57
- import { assembleAssetFromString } from "../../core/asset/asset-serialize.js";
52
+ import { assembleAsset, assembleAssetFromString, serializeFrontmatterQuoted } from "../../core/asset/asset-serialize.js";
58
53
  import { parseFrontmatter, writeSalienceToFrontmatter } from "../../core/asset/frontmatter.js";
59
54
  import { stripMarkdownFences } from "../../core/asset/markdown.js";
55
+ import { conceptIdFromTypeName, parseRefInput } from "../../core/asset/resolve-ref.js";
60
56
  import { authoringRulesForType } from "../../core/authoring-rules.js";
61
- import { resolveStashDir } from "../../core/common.js";
62
- import { getDefaultLlmConfig, getImproveProcessConfig, loadConfig } from "../../core/config/config.js";
63
- import { ConfigError, UsageError } from "../../core/errors.js";
57
+ import { getImproveProcessConfig, loadConfig } from "../../core/config/config.js";
58
+ import { UsageError } from "../../core/errors.js";
64
59
  import { appendEvent, readEvents } from "../../core/events.js";
65
60
  import { lintLessonContent } from "../../core/lesson-lint.js";
61
+ import { parseEmbeddedJsonResponse } from "../../core/parse.js";
66
62
  import { getDbPath } from "../../core/paths.js";
67
- import { resolveStashStandards } from "../../core/standards/resolve-stash-standards.js";
63
+ import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
68
64
  import { withStateDb } from "../../core/state-db.js";
69
65
  import { warnVerbose } from "../../core/warn.js";
70
- import { closeDatabase, getAllEntries, openIndexDatabase } from "../../indexer/db/db.js";
71
66
  import { resolveAssetPath } from "../../indexer/walk/path-resolver.js";
72
- import { chatCompletion, parseEmbeddedJsonResponse } from "../../llm/client.js";
73
- import { isLlmFeatureEnabled, tryLlmFeature } from "../../llm/feature-gate.js";
74
- import { createProposal, isProposalSkipped, listProposals, } from "../proposal/repository.js";
75
- import { stripFrontmatterBody as stripBodyForFidelity } from "./dedup.js";
67
+ import { getDefaultLlmConfig } from "../../integrations/agent/engine-resolution.js";
68
+ import { materializeLlmRunnerConnection, resolveImproveProcessRunner } from "../../integrations/agent/runner.js";
69
+ import { chatCompletion } from "../../llm/client.js";
70
+ import { callStructured } from "../../llm/structured-call.js";
71
+ import { closeDatabase, openIndexDatabase } from "../../storage/repositories/index-connection.js";
72
+ import { getAllEntries } from "../../storage/repositories/index-entries-repository.js";
73
+ import { isProposalSkipped, listProposals, proposalContent, } from "../proposal/repository.js";
74
+ import { stripFrontmatterBody as stripBodyForFidelity } from "./content-hash.js";
76
75
  import { autoRepairLessonFrontmatter, autoSwapDescriptionWhenToUse, collectLessonQualityFindings, repairLessonDescriptionTruncation, } from "./distill/content-repair.js";
77
76
  import { promoteMemoryToKnowledge } from "./distill/promote-memory.js";
78
77
  import { fetchTopSimilarLessons, persistOutputEncodingSalience, runLessonQualityJudge, writeQualityRejection, } from "./distill/quality-gate.js";
79
78
  import { buildClsContext, checkDistillFidelity } from "./distill-guards.js";
80
79
  import { deriveKnowledgeRef } from "./distill-promotion-policy.js";
81
80
  import { buildRefVocabulary, scoreEncodingSalience } from "./encoding-salience.js";
81
+ import { resolveImproveStrategy, resolveProcessEnabled } from "./improve-strategies.js";
82
+ import { emitProposal } from "./proposal-envelope.js";
83
+ import { createRunContext, resolveRunStashDir } from "./run-context.js";
82
84
  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 };
85
+ import { MAX_REJECTED_PROPOSALS } from "./shared.js";
86
+ import { durableImproveRef } from "./source-identity.js";
86
87
  /**
87
88
  * Asset-ref types that `akm distill` structurally refuses as inputs.
88
89
  *
89
90
  * Distill *produces* lessons from non-lesson sources (memory, skill, knowledge,
90
- * etc.). Calling distill on an existing `lesson:*` ref would derive
91
- * `lesson:lesson-<name>-lesson-lesson` (double `-lesson` suffix) — the
91
+ * etc.). Calling distill on an existing `lessons/*` ref would derive
92
+ * `lessons/lesson-<name>-lesson-lesson` (double `-lesson` suffix) — the
92
93
  * recursive-ref defect observed across 323 archived rejected proposals.
93
94
  *
94
95
  * 08-F2: `env` and `secret` are refused as a STRUCTURAL floor — distill reads
@@ -118,38 +119,42 @@ export function isDistillRefusedInputType(type) {
118
119
  // ── Lesson-ref derivation ───────────────────────────────────────────────────
119
120
  /** Derive the proposed lesson ref from the input ref. See module docblock. */
120
121
  export function deriveLessonRef(inputRef) {
121
- const parsed = parseAssetRef(inputRef);
122
- // Strip origin: a feedback signal recorded against `team//skill:deploy`
123
- // distils into the same lesson namespace as `skill:deploy`. The proposal
122
+ const parsed = parseRefInput(inputRef);
123
+ // Strip the bundle: a feedback signal recorded against `team//skills/deploy`
124
+ // distils into the same lesson namespace as `skills/deploy`. The proposal
124
125
  // id (a UUID) keeps the queue entries distinct, so collisions are not a
125
126
  // problem — and reviewers want to see them next to each other anyway.
126
- const slug = `${parsed.type}-${parsed.name}`.toLowerCase();
127
+ const parts = parsed.name.split("/");
128
+ const scope = parts.length > 1 ? parts.shift() : undefined;
129
+ const slug = `${parsed.type}-${parts.join("-")}`.toLowerCase();
127
130
  // Replace anything outside the canonical asset-name charset with `-`. Keep
128
131
  // it deterministic so re-runs produce the same ref.
129
132
  const safe = slug
130
133
  .replace(/[^a-z0-9-]+/g, "-")
131
134
  .replace(/-+/g, "-")
132
135
  .replace(/^-|-$/g, "");
133
- return `lesson:${safe}-lesson`;
136
+ const safeScope = scope
137
+ ?.toLowerCase()
138
+ .replace(/[^a-z0-9-]+/g, "-")
139
+ .replace(/-+/g, "-")
140
+ .replace(/^-|-$/g, "");
141
+ return `lessons/${safeScope ? `${safeScope}/` : ""}${safe}-lesson`;
134
142
  }
135
143
  // ── Content quality validators ──────────────────────────────────────────────
136
144
  //
137
145
  // The actual implementations now live in `core/proposal-quality-validators.ts`
138
146
  // so the same checks run inside `runProposalValidators` on `proposal accept`.
139
- // We re-export the public-facing helpers here so existing imports
140
- // (`from "../../src/commands/distill"`) continue to resolve.
141
- import { detectDoubleFrontmatter, isValidDescription, isValidWhenToUse, } from "../proposal/validators/proposal-quality-validators.js";
142
- export { detectDoubleFrontmatter, isValidDescription, isValidWhenToUse };
147
+ import { detectDoubleFrontmatter, isValidDescription } from "../proposal/validators/proposal-quality-validators.js";
143
148
  // ── Prompt assembly ─────────────────────────────────────────────────────────
144
149
  const LESSON_SYSTEM_PROMPT = distillLessonSystemPrompt;
145
150
  const KNOWLEDGE_SYSTEM_PROMPT = distillKnowledgeSystemPrompt;
146
151
  // ── Structured-output schemas (responseSchema lift) ─────────────────────────
147
152
  //
148
- // PR 1 of the asset-writers decision (see knowledge:projects/akm/
153
+ // PR 1 of the asset-writers decision (see knowledge/projects/akm/
149
154
  // asset-writers-investigation/00-synthesis): on providers that honour
150
155
  // `response_format: json_schema`, ask the LLM for a typed JSON object and
151
- // re-assemble the markdown locally. The previous "emit raw markdown with
152
- // embedded frontmatter" path remains as a fallback for providers that ignore
156
+ // re-assemble the markdown locally. The "emit raw markdown with embedded
157
+ // frontmatter" path supports providers that ignore
153
158
  // the schema (and for the `chat` test seam, which is wired to return strings
154
159
  // today). Shape-level rejection codes — MALFORMED_FRONTMATTER_BLOCK,
155
160
  // FRONTMATTER_NOT_OBJECT, INVALID_YAML, UNBALANCED_CODE_FENCE — become
@@ -252,16 +257,9 @@ export function assembleStructuredDistillMarkdown(payload, kind) {
252
257
  if (kind === "knowledge" && Array.isArray(payload.sources)) {
253
258
  const sources = payload.sources.filter((s) => typeof s === "string" && s.trim().length > 0);
254
259
  if (sources.length > 0)
255
- fm.sources = sources;
256
- }
257
- const fmLines = Object.entries(fm)
258
- .map(([k, v]) => {
259
- if (Array.isArray(v))
260
- return `${k}: [${v.map((s) => JSON.stringify(s)).join(", ")}]`;
261
- return `${k}: ${JSON.stringify(v)}`;
262
- })
263
- .join("\n");
264
- return assembleAssetFromString(fmLines, body);
260
+ fm.xrefs = sources;
261
+ }
262
+ return assembleAssetFromString(serializeFrontmatterQuoted(fm), body);
265
263
  }
266
264
  function validateKnowledgeContent(content, inputRef) {
267
265
  const findings = [];
@@ -331,7 +329,11 @@ export function buildDistillPrompt(input) {
331
329
  }
332
330
  lines.push("Asset content:");
333
331
  if (input.assetContent) {
334
- const body = input.assetContent.trim().slice(0, 3000);
332
+ // The output contract also uses YAML fences. Feeding source frontmatter
333
+ // verbatim caused local models to copy it into the lesson body, producing
334
+ // deterministic double-frontmatter rejection. Distillation needs the
335
+ // source body; its metadata is not evidence to reproduce.
336
+ const body = parseFrontmatter(input.assetContent).content.trim().slice(0, 3000);
335
337
  lines.push("```");
336
338
  lines.push(body);
337
339
  lines.push("```");
@@ -420,78 +422,26 @@ export function buildDistillPrompt(input) {
420
422
  * `distill_invoked` event (with `outcome` in the metadata) regardless of the
421
423
  * branch taken — so observers can count invocations cheaply.
422
424
  */
423
- export async function akmDistill(options) {
424
- const inputRef = options.ref.trim();
425
- if (!inputRef) {
426
- throw new UsageError("Asset ref is required. Usage: akm distill <ref>", "MISSING_REQUIRED_ARGUMENT");
427
- }
428
- // Validate the ref shape up front so a typo never reaches the LLM.
429
- const parsedInputRef = parseAssetRef(inputRef);
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
- : {};
437
- // Recursive-distillation guard. Distill produces *lessons* from non-lesson
438
- // sources (memory, skill, knowledge, etc.). Calling distill on an existing
439
- // lesson would derive `lesson:lesson-<name>-lesson-lesson` (double `-lesson`
440
- // suffix) and route a "lesson of a lesson" through the proposal queue —
441
- // observed in 323 reviewed archived proposals as the recursive-ref defect.
442
- // Refuse the input here so the improve loop (or other callers) get a clean
443
- // skipped outcome instead of producing nonsense refs.
444
- //
445
- // The refused-type set is exported as {@link DISTILL_REFUSED_INPUT_TYPES} so
446
- // the improve planner can skip these refs before queuing distill attempts;
447
- // this runtime check stays as a defensive backstop for direct callers.
448
- if (isDistillRefusedInputType(parsedInputRef.type)) {
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.";
456
- appendEvent({
457
- eventType: "distill_invoked",
458
- ref: inputRef,
459
- metadata: {
460
- outcome: "skipped",
461
- lessonRef: skippedRef,
462
- message,
463
- skipReason: isSecretInput ? "refused_secret_input" : "recursive_lesson_input",
464
- ...eligMeta,
465
- },
466
- });
467
- return {
468
- schemaVersion: 1,
469
- ok: true,
470
- outcome: "skipped",
471
- inputRef,
472
- lessonRef: skippedRef,
473
- message,
474
- };
475
- }
476
- const config = options.config ?? loadConfig();
477
- const stash = options.stashDir ?? resolveStashDir();
478
- const chat = options.chat ?? chatCompletion;
479
- const lookup = options.lookupFn ?? defaultLookup;
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;
484
- // D-4 / #390: similar-lessons retrieval seam (test-injectable).
485
- const fetchSimilarLessonsFn = options.fetchSimilarLessonsFn ?? ((query, n) => fetchTopSimilarLessons(query, n, options.stashDir));
425
+ /**
426
+ * Best-effort load of the distill INPUT asset plus the #608 encoding-time
427
+ * salience scoring: read the source, build the once-per-invocation bigram ref
428
+ * vocabulary, then score the asset (novelty×0.40 + magnitude×0.35 +
429
+ * predictionError×0.25) and mirror the result to both the asset frontmatter and
430
+ * `state.db :: asset_salience`. Every write is best-effort. Extracted verbatim
431
+ * from `akmDistill`; returns the (possibly salience-stamped) content plus the
432
+ * ref vocabulary the caller reuses when scoring the distilled OUTPUT (G4).
433
+ */
434
+ async function loadAndScoreInputSalience(args) {
435
+ const { inputRef, durableInputRef, salienceWriteKey, stash, config, outcomeWeightEnabled, lookup, ctx } = args;
486
436
  // Best-effort load: when the asset is not yet indexed we still proceed —
487
437
  // the LLM is asked to distil from "available signal" (feedback alone).
488
438
  let assetContent = null;
489
439
  let assetFilePath = null;
490
440
  try {
491
- const filePath = await lookup(inputRef);
441
+ const filePath = await lookup(durableInputRef);
492
442
  if (filePath && fs.existsSync(filePath)) {
493
443
  assetFilePath = filePath;
494
- assetContent = fs.readFileSync(filePath, "utf8");
444
+ assetContent = ctx.readAsset(filePath);
495
445
  }
496
446
  }
497
447
  catch {
@@ -523,7 +473,7 @@ export async function akmDistill(options) {
523
473
  }
524
474
  if (assetContent && assetFilePath) {
525
475
  try {
526
- const parsedRef = parseAssetRef(inputRef);
476
+ const parsedRef = parseRefInput(inputRef);
527
477
  // G4: predictionError decays with revision count — the prior hardcoded
528
478
  // `revisionCount: 0` made it a dead constant 1.0. Use the number of
529
479
  // proposals ever raised against this ref as the revision proxy.
@@ -543,7 +493,7 @@ export async function akmDistill(options) {
543
493
  // 1. Write salience to the source asset frontmatter (idempotent).
544
494
  const updatedContent = writeSalienceToFrontmatter(assetContent, salienceResult.score, salienceResult);
545
495
  if (updatedContent !== assetContent) {
546
- fs.writeFileSync(assetFilePath, updatedContent, "utf8");
496
+ ctx.writeAsset(assetFilePath, updatedContent);
547
497
  assetContent = updatedContent;
548
498
  }
549
499
  // 2. Persist encoding_salience to state.db.
@@ -556,7 +506,7 @@ export async function akmDistill(options) {
556
506
  encodingSalience: salienceResult.score,
557
507
  outcomeWeightEnabled,
558
508
  });
559
- upsertAssetSalience(stateDb, inputRef, vector);
509
+ upsertAssetSalience(stateDb, salienceWriteKey, vector);
560
510
  });
561
511
  }
562
512
  catch {
@@ -567,24 +517,131 @@ export async function akmDistill(options) {
567
517
  // Scoring errors never block distillation.
568
518
  }
569
519
  }
570
- const { events } = readEventsImpl({
571
- ref: inputRef,
572
- type: "feedback",
573
- excludeTags: options.excludeTags,
574
- includeTags: options.includeTags,
520
+ return { assetContent, existingRefVocabulary };
521
+ }
522
+ /**
523
+ * Recursive-distillation + secret-material input guard. Distill produces
524
+ * *lessons* from non-lesson sources; a lesson input would derive a recursive
525
+ * `lessons/lesson-<name>` ref (the 323-archived-proposals defect) and
526
+ * env/secret inputs must never be read or sent to the LLM. Emits the
527
+ * `distill_invoked(skipped)` event and returns the terminal skipped result,
528
+ * or `null` when the input type is allowed. Extracted verbatim from
529
+ * `akmDistill` (R25/R31 — the events-ctx threading pushed it over the bar).
530
+ */
531
+ function refuseDisallowedDistillInput(args) {
532
+ const { options, parsedInputRef, inputRef, durableInputRef, eligMeta } = args;
533
+ if (!isDistillRefusedInputType(parsedInputRef.type))
534
+ return null;
535
+ // 08-F2: env/secret are a secret-material refusal (never read the bytes);
536
+ // lesson is the recursive-form refusal. Both skip BEFORE any readFileSync.
537
+ const isSecretInput = parsedInputRef.type === "env" || parsedInputRef.type === "secret";
538
+ const skippedRef = isSecretInput ? inputRef : conceptIdFromTypeName("lesson", parsedInputRef.name);
539
+ const message = isSecretInput
540
+ ? `Distill refuses ${parsedInputRef.type} inputs — secret material must never be sent to the LLM.`
541
+ : "Distill refuses lesson inputs — lessons are the distilled form, not a source.";
542
+ appendEvent({
543
+ eventType: "distill_invoked",
544
+ // Key on item_ref when the planner resolved one, otherwise the conceptId.
545
+ ref: options.itemRef ?? durableInputRef,
546
+ metadata: {
547
+ outcome: "skipped",
548
+ proposalRef: skippedRef,
549
+ message,
550
+ skipReason: isSecretInput ? "refused_secret_input" : "recursive_lesson_input",
551
+ ...eligMeta,
552
+ },
553
+ }, options.eventsCtx);
554
+ return {
555
+ schemaVersion: 1,
556
+ ok: true,
557
+ outcome: "skipped",
558
+ inputRef,
559
+ proposalRef: skippedRef,
560
+ skipReason: isSecretInput ? "refused_secret_input" : "recursive_lesson_input",
561
+ message,
562
+ };
563
+ }
564
+ export async function akmDistill(options) {
565
+ const inputRef = options.ref.trim();
566
+ if (!inputRef) {
567
+ throw new UsageError("Asset ref is required. Usage: akm distill <ref>", "MISSING_REQUIRED_ARGUMENT");
568
+ }
569
+ // Validate the ref shape up front so a typo never reaches the LLM.
570
+ const parsedInputRef = parseRefInput(inputRef);
571
+ const durableInputRef = durableImproveRef(inputRef);
572
+ // The input asset's durable salience write key is item_ref when resolved,
573
+ // otherwise the input conceptId.
574
+ const salienceWriteKey = options.itemRef ?? durableInputRef;
575
+ const targetKind = options.proposalKind ?? "lesson";
576
+ const config = options.config ?? loadConfig();
577
+ const improveProfile = options.improveProfile ?? resolveImproveStrategy(undefined, config).config;
578
+ options = { ...options, improveProfile };
579
+ if (!resolveProcessEnabled("distill", improveProfile)) {
580
+ const proposalKind = targetKind === "knowledge" ? "knowledge" : "lesson";
581
+ return {
582
+ schemaVersion: 1,
583
+ ok: true,
584
+ outcome: "config_disabled",
585
+ inputRef,
586
+ proposalRef: proposalKind === "knowledge" ? deriveKnowledgeRef(inputRef) : deriveLessonRef(inputRef),
587
+ proposalKind,
588
+ message: "distill is disabled in config; enable processes.distill.enabled to activate.",
589
+ };
590
+ }
591
+ // Attribution tagging: spread into every distill_invoked event's metadata so
592
+ // the lane that selected this asset is recorded uniformly across all outcome
593
+ // branches. Empty object when no lane was supplied (direct `akm distill`).
594
+ const eligMeta = options.eligibilitySource
595
+ ? { eligibilitySource: options.eligibilitySource }
596
+ : {};
597
+ // Recursive-distillation guard (see refuseDisallowedDistillInput). The
598
+ // refused-type set is exported as {@link DISTILL_REFUSED_INPUT_TYPES} so
599
+ // the improve planner can skip these refs before queuing distill attempts;
600
+ // this runtime check stays as a defensive backstop for direct callers.
601
+ const refused = refuseDisallowedDistillInput({ options, parsedInputRef, inputRef, durableInputRef, eligMeta });
602
+ if (refused)
603
+ return refused;
604
+ const stash = resolveRunStashDir(options.stashDir);
605
+ const chat = options.chat ?? chatCompletion;
606
+ const distillLlm = Object.hasOwn(options, "llmConfig")
607
+ ? (options.llmConfig ?? undefined)
608
+ : (() => {
609
+ const runner = resolveImproveProcessRunner(options.improveProfile, "distill", config);
610
+ return runner ? materializeLlmRunnerConnection(runner) : getDefaultLlmConfig(config);
611
+ })();
612
+ const lookup = options.lookupFn ?? ((ref) => defaultLookup(ref, stash));
613
+ const readEventsImpl = options.readEventsFn ?? readEvents;
614
+ // R1 opt-out must flow into every computeSalience call this command makes so
615
+ // distill-written rank_score rows use the same weights as preparation's.
616
+ const outcomeWeightEnabled = config.improve?.salience?.outcomeWeightEnabled !== false;
617
+ // D-4 / #390: similar-lessons retrieval seam (test-injectable).
618
+ const fetchSimilarLessonsFn = options.fetchSimilarLessonsFn ?? ((query, n) => fetchTopSimilarLessons(query, n, options.stashDir));
619
+ const assetCtx = createRunContext({
620
+ stashDir: stash,
621
+ config,
622
+ eventsCtx: options.eventsCtx ?? {},
623
+ proposalsCtx: options.ctx ?? {},
624
+ chat,
625
+ getLlmConfig: () => distillLlm ?? null,
626
+ sourceRun: options.sourceRun ?? `distill-${Date.now()}`,
627
+ dryRun: false,
628
+ signal: options.signal,
629
+ }).withFreshAssetMemo();
630
+ const { assetContent, existingRefVocabulary } = await loadAndScoreInputSalience({
631
+ inputRef,
632
+ durableInputRef,
633
+ salienceWriteKey,
634
+ stash,
635
+ config,
636
+ outcomeWeightEnabled,
637
+ lookup,
638
+ ctx: assetCtx,
639
+ });
640
+ const { filteredEvents, exclusionSet, filteredFeedbackCount, feedbackFullyFiltered } = readDistillFeedback({
641
+ readEventsImpl,
642
+ options,
643
+ durableInputRef,
575
644
  });
576
- // #267 — feedback exclusion. Filter events whose `ref` matches the
577
- // exclusion list BEFORE the prompt is built. The original event stream
578
- // is never mutated; only the `feedback` slice that reaches the LLM is
579
- // affected. The exclusion set is normalised through `parseAssetRef` →
580
- // re-serialised so callers can pass canonical or origin-prefixed refs
581
- // and the comparison still works against the event payload's `ref`.
582
- const exclusionList = options.excludeFeedbackFromRefs ?? [];
583
- const exclusionSet = new Set(exclusionList.map((ref) => ref.trim()).filter((ref) => ref.length > 0));
584
- const originalEventCount = events.length;
585
- const filteredEvents = exclusionSet.size > 0 ? events.filter((e) => !(e.ref !== undefined && exclusionSet.has(e.ref))) : events;
586
- const filteredFeedbackCount = originalEventCount - filteredEvents.length;
587
- const feedbackFullyFiltered = exclusionSet.size > 0 && originalEventCount > 0 && filteredEvents.length === 0;
588
645
  const feedback = filteredEvents.slice(-20).map((e) => ({
589
646
  ts: e.ts,
590
647
  eventType: e.eventType,
@@ -599,9 +656,14 @@ export async function akmDistill(options) {
599
656
  const promotionResult = await promoteMemoryToKnowledge({
600
657
  targetKind,
601
658
  inputRef,
659
+ durableInputRef,
660
+ ...(options.itemRef ? { itemRef: options.itemRef } : {}),
602
661
  assetContent,
603
662
  filteredEvents,
604
663
  config,
664
+ strategy: options.improveProfile,
665
+ llmConfig: distillLlm,
666
+ signal: options.signal,
605
667
  chat,
606
668
  stash,
607
669
  lookup,
@@ -612,6 +674,7 @@ export async function akmDistill(options) {
612
674
  eligibilitySource: options.eligibilitySource,
613
675
  sourceRun: options.sourceRun,
614
676
  proposalsCtx: options.ctx,
677
+ eventsCtx: options.eventsCtx,
615
678
  exclusionSetSize: exclusionSet.size,
616
679
  filteredFeedbackCount,
617
680
  feedbackFullyFiltered,
@@ -620,143 +683,226 @@ export async function akmDistill(options) {
620
683
  return promotionResult;
621
684
  const effectiveProposalKind = targetKind === "knowledge" ? "knowledge" : "lesson";
622
685
  const effectiveLessonRef = effectiveProposalKind === "knowledge" ? deriveKnowledgeRef(inputRef) : deriveLessonRef(inputRef);
623
- // Inject last 1–3 rejected proposals for this ref as Reflexion-style
624
- // verbal-RL context so the LLM avoids regenerating refused proposals.
625
- const MAX_REJECTED_PROPOSALS = 3;
626
- const rejectedForRef = listProposals(stash, { ref: inputRef, status: "rejected", includeArchive: true })
627
- .sort((a, b) => new Date(b.updatedAt ?? 0).getTime() - new Date(a.updatedAt ?? 0).getTime())
628
- .slice(0, MAX_REJECTED_PROPOSALS)
629
- .map((p) => ({
630
- reason: p.review?.reason ?? "no reason given",
631
- contentPreview: p.payload.content.slice(0, 500),
632
- }));
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({
686
+ const messages = await buildDistillMessages({
687
+ options,
688
+ stash,
655
689
  inputRef,
656
690
  assetContent,
657
691
  feedback,
658
- proposalKind: effectiveProposalKind,
659
- ...(rejectedForRef.length > 0 ? { rejectedProposals: rejectedForRef } : {}),
660
- ...(standardsContext.trim() ? { standardsContext } : {}),
692
+ effectiveProposalKind,
693
+ effectiveLessonRef,
694
+ fetchSimilarLessonsFn,
661
695
  });
662
- const userPrompt = clsContext ? `${baseUserPrompt}${clsContext}` : baseUserPrompt;
663
- const messages = [
664
- { role: "system", content: effectiveProposalKind === "knowledge" ? KNOWLEDGE_SYSTEM_PROMPT : LESSON_SYSTEM_PROMPT },
665
- { role: "user", content: userPrompt },
666
- ];
667
- // Single bounded LLM call. The wrapper handles the gate-check, 600s
668
- // (10 min) default timeout, and error fallback (returning `null`).
669
- //
670
- // Capture the fallback reason so we can distinguish "config gate is off"
671
- // (no LLM was called — operator action required) from "LLM call was made
672
- // but returned no usable output" (transport/timeout/empty — observability).
673
- // The previous conflated message ("disabled or the LLM call failed") gave
674
- // operators no signal to act on; a 108-run audit found 100% of skipped
675
- // outcomes were actually the config-gate-off branch.
676
- //
677
- // responseSchema lift (PR 1, asset-writers-investigation §5): on the
678
- // production path (no test `chat` seam) we pass the lesson/knowledge JSON
679
- // schema to `chatCompletion`. Providers with `supportsJsonSchema: true`
680
- // return a typed JSON object the post-call code re-assembles into markdown,
681
- // bypassing the four shape-level rejection codes the validator log catches.
682
- // The test seam keeps its two-arg signature, so injected fakes still pin
683
- // markdown responses verbatim and the existing assertion suite is unchanged.
684
- const distillSchema = effectiveProposalKind === "knowledge" ? DISTILL_KNOWLEDGE_JSON_SCHEMA : DISTILL_LESSON_JSON_SCHEMA;
685
- let fallbackReason;
686
- const raw = await tryLlmFeature("distill", config, async () => {
687
- const distillLlm = getDefaultLlmConfig(config);
688
- if (!distillLlm) {
689
- // No LLM connection configured — treat as gate-disabled. Throwing
690
- // here lets `tryLlmFeature` route us through the "error" fallback,
691
- // which is the same graceful skipped path.
692
- throw new ConfigError("No LLM connection configured. Set `defaults.llm` and a profile under `profiles.llm`.", "LLM_NOT_CONFIGURED");
696
+ const { raw, fallbackReason } = await runDistillLlmCall({
697
+ config,
698
+ options,
699
+ distillLlm,
700
+ messages,
701
+ effectiveProposalKind,
702
+ });
703
+ if (raw === null || raw.trim() === "") {
704
+ return distillEmptyResponseResult({
705
+ fallbackReason,
706
+ inputRef,
707
+ durableInputRef,
708
+ ...(options.itemRef ? { itemRef: options.itemRef } : {}),
709
+ effectiveLessonRef,
710
+ effectiveProposalKind,
711
+ exclusionSet,
712
+ filteredFeedbackCount,
713
+ feedbackFullyFiltered,
714
+ eligMeta,
715
+ eventsCtx: options.eventsCtx,
716
+ });
717
+ }
718
+ const { content, descriptionSwapped } = assembleAndValidateDistillContent({
719
+ raw,
720
+ effectiveProposalKind,
721
+ inputRef,
722
+ durableInputRef,
723
+ ...(options.itemRef ? { itemRef: options.itemRef } : {}),
724
+ effectiveLessonRef,
725
+ exclusionSet,
726
+ filteredFeedbackCount,
727
+ eligMeta,
728
+ eventsCtx: options.eventsCtx,
729
+ });
730
+ const gate = await applyDistillQualityGate({
731
+ config,
732
+ options,
733
+ content,
734
+ assetContent,
735
+ chat,
736
+ distillLlm,
737
+ fetchSimilarLessonsFn,
738
+ stash,
739
+ inputRef,
740
+ effectiveLessonRef,
741
+ exclusionSet,
742
+ filteredFeedbackCount,
743
+ feedbackFullyFiltered,
744
+ });
745
+ if ("rejection" in gate)
746
+ return gate.rejection;
747
+ const lessonJudgeConfidence = gate.confidence;
748
+ return emitDistillLessonProposal({
749
+ content,
750
+ options,
751
+ distillLlm,
752
+ assetContent,
753
+ inputRef,
754
+ durableInputRef,
755
+ effectiveLessonRef,
756
+ effectiveProposalKind,
757
+ stash,
758
+ exclusionSet,
759
+ filteredFeedbackCount,
760
+ feedbackFullyFiltered,
761
+ lessonJudgeConfidence,
762
+ existingRefVocabulary,
763
+ outcomeWeightEnabled,
764
+ descriptionSwapped,
765
+ eligMeta,
766
+ });
767
+ }
768
+ // ── Helpers ─────────────────────────────────────────────────────────────────
769
+ /**
770
+ * The distill-propose pass: the optional WS-3b distill→source fidelity check
771
+ * (routes contradictions to human review), provenance xref round-trip, proposal
772
+ * creation, and the queued/skipped `distill_invoked` emit + output salience
773
+ * scoring (G4). Extracted verbatim from `akmDistill`; every outcome shape and
774
+ * event is byte-identical.
775
+ */
776
+ async function emitDistillLessonProposal(args) {
777
+ const { options, distillLlm, assetContent, inputRef, durableInputRef, effectiveLessonRef, effectiveProposalKind, stash, exclusionSet, filteredFeedbackCount, feedbackFullyFiltered, lessonJudgeConfidence, existingRefVocabulary, outcomeWeightEnabled, descriptionSwapped, eligMeta, } = args;
778
+ let content = args.content;
779
+ // WS-3b: Distill→source fidelity check (step 10).
780
+ // When fidelityCheck.enabled, check the distill proposal against its cited
781
+ // source memories. A contradiction flag routes to human review (not auto-accept).
782
+ // DEFAULT OFF. Fail-open: any error is treated as no-contradiction.
783
+ const fidelityConfig = getImproveProcessConfig("distill", options.improveProfile)?.fidelityCheck ??
784
+ {};
785
+ if (fidelityConfig.enabled && assetContent) {
786
+ try {
787
+ const proposalBody = stripBodyForFidelity(content);
788
+ const sourceBodies = [stripBodyForFidelity(assetContent)];
789
+ const fidelityResult = checkDistillFidelity(proposalBody, sourceBodies, fidelityConfig);
790
+ if (fidelityResult.contradictionDetected) {
791
+ // Route to human review by writing a quality rejection with reviewNeeded=true.
792
+ return writeQualityRejection(stash, inputRef, effectiveLessonRef, content, 2.0, // below auto-accept threshold, signals review needed
793
+ fidelityResult.reason ?? "Proposal may contradict cited source memories.", {
794
+ reviewNeeded: true,
795
+ fidelityContradiction: true,
796
+ ...(exclusionSet.size > 0 ? { filteredFeedbackCount, feedbackFullyFiltered } : {}),
797
+ }, options.eligibilitySource, options.eventsCtx);
798
+ }
693
799
  }
694
- // Production path: pass the JSON schema so providers that honour
695
- // `response_format: json_schema` enforce shape upstream. Providers that
696
- // ignore the option fall through to the prompt-contract markdown path.
697
- if (options.chat === undefined) {
698
- return chatCompletion(distillLlm, messages, { responseSchema: distillSchema });
800
+ catch {
801
+ // Fail open — fidelity check is supplemental.
699
802
  }
700
- // Test seam: preserve the two-arg signature so existing fake `chat`
701
- // functions (which return markdown strings) continue to work.
702
- return chat(distillLlm, messages);
703
- }, null, {
704
- onFallback: (evt) => {
705
- fallbackReason = evt.reason;
706
- // Log the fallback reason; the caller (raw === null path) handles
707
- // emitting the distill_invoked event so we don't double-emit here.
708
- warnVerbose(`[akm] LLM fallback for ${evt.feature}: ${evt.reason}`);
803
+ }
804
+ // Round-trip the parsed frontmatter so the proposal carries it as a
805
+ // structured payload alongside the raw content (matches the shape used by
806
+ // other proposal sources).
807
+ //
808
+ // Serialize canonical provenance into the content that promotion writes.
809
+ const parsed = parseFrontmatter(content);
810
+ const existingXrefs = Array.isArray(parsed.data.xrefs) ? parsed.data.xrefs.map(String) : [];
811
+ const frontmatterWithXrefs = {
812
+ ...parsed.data,
813
+ xrefs: [...new Set([...existingXrefs, durableInputRef])],
814
+ };
815
+ delete frontmatterWithXrefs.sources;
816
+ content = assembleAsset(frontmatterWithXrefs, parsed.content);
817
+ const proposalResult2 = emitProposal({ stashDir: stash, proposalsCtx: options.ctx }, {
818
+ ref: effectiveLessonRef,
819
+ // §23.6 fingerprint model-id term (WI-6.4). Uses the RESOLVED connection
820
+ // (profile/config fallback included), not the raw option — a standalone
821
+ // `akm distill` run must fingerprint under the model that actually
822
+ // generated the content, matching the promote-memory branch.
823
+ ...(distillLlm?.model ? { modelId: distillLlm.model } : {}),
824
+ source: "distill",
825
+ ...(options.sourceRun !== undefined ? { sourceRun: options.sourceRun } : {}),
826
+ payload: {
827
+ content,
828
+ frontmatter: frontmatterWithXrefs,
709
829
  },
830
+ ...(lessonJudgeConfidence !== undefined ? { confidence: lessonJudgeConfidence } : {}),
831
+ // Attribution tagging: persist the eligibility lane on the proposal.
832
+ ...(options.eligibilitySource ? { eligibilitySource: options.eligibilitySource } : {}),
710
833
  });
711
- if (raw === null || raw.trim() === "") {
712
- // Distinguish "config gate disabled" from "LLM call failed". For the
713
- // config-disabled branch, we ALSO suppress the `distill_invoked` event
714
- // because no LLM work was actually invoked — emitting the event causes
715
- // the planner to accumulate phantom invocations that drown out real
716
- // signal.
717
- if (fallbackReason === "disabled") {
718
- return {
719
- schemaVersion: 1,
720
- ok: true,
721
- outcome: "config_disabled",
722
- inputRef,
723
- lessonRef: effectiveLessonRef,
724
- proposalRef: effectiveLessonRef,
725
- proposalKind: effectiveProposalKind,
726
- message: "distill is disabled in config; enable processes.distill.enabled to activate.",
727
- ...(exclusionSet.size > 0 ? { filteredFeedbackCount, feedbackFullyFiltered } : {}),
728
- };
729
- }
730
- // LLM was actually invoked but produced nothing usable (transport error,
731
- // timeout, or empty/whitespace response). Emit the event so the failure
732
- // is observable.
834
+ if (isProposalSkipped(proposalResult2)) {
733
835
  appendEvent({
734
836
  eventType: "distill_invoked",
735
- ref: inputRef,
837
+ // Use item_ref when resolved, otherwise the input conceptId.
838
+ ref: options.itemRef ?? durableInputRef,
736
839
  metadata: {
737
- outcome: "llm_failed",
738
- lessonRef: effectiveLessonRef,
739
- proposalKind: effectiveProposalKind,
740
- ...(exclusionSet.size > 0 ? { filteredFeedbackCount } : {}),
840
+ outcome: "skipped",
841
+ proposalRef: effectiveLessonRef,
842
+ message: proposalResult2.message,
843
+ skipReason: proposalResult2.reason,
741
844
  ...eligMeta,
742
845
  },
743
- });
846
+ }, options.eventsCtx);
744
847
  return {
745
848
  schemaVersion: 1,
746
849
  ok: true,
747
- outcome: "llm_failed",
850
+ outcome: "skipped",
748
851
  inputRef,
749
- lessonRef: effectiveLessonRef,
750
852
  proposalRef: effectiveLessonRef,
751
- proposalKind: effectiveProposalKind,
752
- message: "LLM call returned no usable output (timeout, empty, or error).",
753
- ...(exclusionSet.size > 0 ? { filteredFeedbackCount, feedbackFullyFiltered } : {}),
853
+ skipReason: proposalResult2.reason,
854
+ message: proposalResult2.message,
754
855
  };
755
856
  }
857
+ const proposal2 = proposalResult2;
858
+ // G4: content-score the distilled OUTPUT so it carries a real encoding
859
+ // salience (encoding_source='content') from creation — lessons never get
860
+ // another chance (they are refused as distill inputs).
861
+ persistOutputEncodingSalience(durableImproveRef(effectiveLessonRef), content, existingRefVocabulary, outcomeWeightEnabled);
862
+ appendEvent({
863
+ eventType: "distill_invoked",
864
+ // Use item_ref when resolved, otherwise the input conceptId.
865
+ ref: options.itemRef ?? durableInputRef,
866
+ metadata: {
867
+ outcome: "queued",
868
+ proposalRef: effectiveLessonRef,
869
+ proposalKind: effectiveProposalKind,
870
+ proposalId: proposal2.id,
871
+ // R3: judge verdicts are longitudinally queryable, not just a one-shot
872
+ // proposal.confidence write (normalized 1–5 score / 5).
873
+ ...(lessonJudgeConfidence !== undefined ? { judgeConfidence: lessonJudgeConfidence } : {}),
874
+ ...(options.sourceRun !== undefined ? { sourceRun: options.sourceRun } : {}),
875
+ ...(exclusionSet.size > 0 ? { filteredFeedbackCount } : {}),
876
+ ...(descriptionSwapped > 0 ? { descriptionSwapped } : {}),
877
+ ...eligMeta,
878
+ },
879
+ }, options.eventsCtx);
880
+ return {
881
+ schemaVersion: 1,
882
+ ok: true,
883
+ outcome: "queued",
884
+ inputRef,
885
+ proposalRef: effectiveLessonRef,
886
+ proposalKind: effectiveProposalKind,
887
+ proposalId: proposal2.id,
888
+ proposal: proposal2,
889
+ ...(exclusionSet.size > 0 ? { filteredFeedbackCount, feedbackFullyFiltered } : {}),
890
+ ...(descriptionSwapped > 0 ? { descriptionSwapped } : {}),
891
+ };
892
+ }
893
+ /**
894
+ * Turn the raw LLM response into validated proposal content: prefer the
895
+ * structured-JSON assembly, else strip markdown fences; run the lesson-only
896
+ * auto-repair chain (frontmatter repair, description↔when_to_use swap, truncation
897
+ * repair); then lint/validate, emitting `distill_invoked(validation_failed)` and
898
+ * throwing a `UsageError` on any finding. Extracted verbatim from `akmDistill`.
899
+ */
900
+ function assembleAndValidateDistillContent(args) {
901
+ const { raw, effectiveProposalKind, inputRef, durableInputRef, itemRef, effectiveLessonRef, exclusionSet, filteredFeedbackCount, eligMeta, eventsCtx, } = args;
756
902
  // Structured-output path: when the provider honoured the JSON schema, `raw`
757
903
  // is a JSON object string (not a markdown blob). Try to parse it and assemble
758
- // the canonical `---\nfm\n---\n\nbody` form before falling through to the
759
- // legacy markdown pipeline. Failure here (non-JSON response, missing
904
+ // the canonical `---\nfm\n---\n\nbody` form before using the markdown
905
+ // response path. Failure here (non-JSON response, missing
760
906
  // required field, unexpected types) is non-fatal — we drop down to the
761
907
  // markdown path which has its own auto-repair + lint pass.
762
908
  let content;
@@ -801,156 +947,254 @@ export async function akmDistill(options) {
801
947
  if (findings.length > 0) {
802
948
  appendEvent({
803
949
  eventType: "distill_invoked",
804
- ref: inputRef,
950
+ // Use item_ref when resolved, otherwise the input conceptId.
951
+ ref: itemRef ?? durableInputRef,
805
952
  metadata: {
806
953
  outcome: "validation_failed",
807
- lessonRef: effectiveLessonRef,
954
+ proposalRef: effectiveLessonRef,
808
955
  proposalKind: effectiveProposalKind,
809
956
  findingKinds: findings.map((f) => f.kind),
810
957
  ...(exclusionSet.size > 0 ? { filteredFeedbackCount } : {}),
811
958
  ...eligMeta,
812
959
  },
813
- });
960
+ }, eventsCtx);
814
961
  const message = findings.map((f) => f.message).join("\n");
815
962
  throw new UsageError(`Distilled ${effectiveProposalKind} failed validation:\n${message}`, "MISSING_REQUIRED_ARGUMENT", effectiveProposalKind === "knowledge"
816
963
  ? "Knowledge proposals require a non-empty markdown body."
817
964
  : "Lessons require non-empty `description` and `when_to_use` frontmatter fields. See v1 spec §13.");
818
965
  }
819
- // LLM-as-judge quality gate (P2-B). Only active when the feature flag is
820
- // explicitly enabled. Fail-CLOSED (07 P0-2): an unjudgeable proposal (no LLM
821
- // / timeout / parse failure) is rejected, not passed through.
822
- // D-5 / #388: Three-band system — review_needed band queues a proposal
823
- // with review_needed outcome rather than auto-rejecting.
824
- let lessonJudgeConfidence;
825
- if (isLlmFeatureEnabled(config, "lesson_quality_gate")) {
826
- // D-4 / #390: retrieve top-3 similar lessons for dedup check in judge.
827
- const similarLessons = await fetchSimilarLessonsFn(content.slice(0, 500), 3);
828
- const judgeResult = await runLessonQualityJudge(config, content, assetContent ?? "", chat, similarLessons.length > 0 ? similarLessons : undefined);
829
- if (!judgeResult.pass) {
830
- if (judgeResult.reviewNeeded) {
831
- return writeQualityRejection(stash, inputRef, effectiveLessonRef, content, judgeResult.score, judgeResult.reason, {
832
- reviewNeeded: true,
833
- ...(exclusionSet.size > 0 ? { filteredFeedbackCount, feedbackFullyFiltered } : {}),
834
- }, options.eligibilitySource);
835
- }
836
- return writeQualityRejection(stash, inputRef, effectiveLessonRef, content, judgeResult.score, judgeResult.reason, exclusionSet.size > 0 ? { filteredFeedbackCount, feedbackFullyFiltered } : {}, options.eligibilitySource);
837
- }
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.
843
- if (judgeResult.score > 0)
844
- lessonJudgeConfidence = judgeResult.score / 5;
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
- }
870
- // Round-trip the parsed frontmatter so the proposal carries it as a
871
- // structured payload alongside the raw content (matches the shape used by
872
- // other proposal sources).
873
- //
874
- // D-7 / #398: Inject `sources: [inputRef]` into the LLM-path proposal
875
- // frontmatter when the field is absent, providing reviewers with provenance
876
- // without requiring them to open event history. A-MEM arXiv:2502.12110 —
877
- // all notes carry explicit provenance links.
878
- const parsed = parseFrontmatter(content);
879
- const frontmatterWithSources = { ...parsed.data };
880
- if (!Array.isArray(frontmatterWithSources.sources) || frontmatterWithSources.sources.length === 0) {
881
- frontmatterWithSources.sources = [inputRef];
966
+ return { content, descriptionSwapped };
967
+ }
968
+ /**
969
+ * The single bounded distill LLM call: gate-checked via `callStructured`
970
+ * (R26 migration off the raw `chatCompletion` scaffold), passing the
971
+ * lesson/knowledge JSON schema on the production path and keeping the
972
+ * injected test fake schema-blind. Returns the raw response (or `null`) and
973
+ * the fallback reason.
974
+ */
975
+ async function runDistillLlmCall(args) {
976
+ const { config, options, distillLlm, messages, effectiveProposalKind } = args;
977
+ const distillSchema = effectiveProposalKind === "knowledge" ? DISTILL_KNOWLEDGE_JSON_SCHEMA : DISTILL_LESSON_JSON_SCHEMA;
978
+ let fallbackReason;
979
+ const enabled = resolveProcessEnabled("distill", options.improveProfile ?? resolveImproveStrategy(undefined, config).config);
980
+ const recordFallback = (feature, reason) => {
981
+ fallbackReason = reason;
982
+ // Log the fallback reason; the caller (raw === null path) handles
983
+ // emitting the distill_invoked event so we don't double-emit here.
984
+ warnVerbose(`[akm] LLM fallback for ${feature}: ${reason}`);
985
+ };
986
+ if (enabled && !distillLlm) {
987
+ // No LLM connection configured. At HEAD this threw a ConfigError inside
988
+ // the gated fn and tryLlmFeature routed it through the "error" fallback;
989
+ // reproduce that terminal state directly (the gate-disabled case above
990
+ // still dominates: when disabled, callStructured takes the "disabled"
991
+ // fallback before any LLM lookup, exactly as before).
992
+ recordFallback("distill", "error");
993
+ return { raw: null, fallbackReason };
882
994
  }
883
- const proposalResult2 = createProposal(stash, {
884
- ref: effectiveLessonRef,
885
- source: "distill",
886
- ...(options.sourceRun !== undefined ? { sourceRun: options.sourceRun } : {}),
887
- payload: {
888
- content,
889
- frontmatter: frontmatterWithSources,
995
+ const raw = await callStructured({
996
+ feature: "distill",
997
+ akmConfig: config,
998
+ enabled,
999
+ // Safe: when the gate is open, distillLlm is defined (guard above); when
1000
+ // it is closed, the transport never runs and config is never read.
1001
+ config: distillLlm,
1002
+ messages,
1003
+ request: options.chat === undefined
1004
+ ? // Production path: pass the JSON schema so providers that honour
1005
+ // `response_format: json_schema` enforce shape upstream. Providers
1006
+ // that ignore the option fall through to the prompt-contract
1007
+ // markdown path.
1008
+ { responseSchema: distillSchema }
1009
+ : // Test seam: keep the injected fake as the transport; fakes never
1010
+ // see the schema (they return markdown strings).
1011
+ { chat: options.chat, ...(options.signal ? { signal: options.signal } : {}) },
1012
+ parse: (raw) => raw ?? null,
1013
+ onError: (_cls, err) => {
1014
+ // At HEAD a transport throw escaped to tryLlmFeature's catch, which
1015
+ // fired onFallback("error"); reproduce that observable state.
1016
+ void err;
1017
+ recordFallback("distill", "error");
1018
+ return null;
890
1019
  },
891
- ...(lessonJudgeConfidence !== undefined ? { confidence: lessonJudgeConfidence } : {}),
892
- // Attribution tagging: persist the eligibility lane on the proposal.
893
- ...(options.eligibilitySource ? { eligibilitySource: options.eligibilitySource } : {}),
894
- }, options.ctx);
895
- if (isProposalSkipped(proposalResult2)) {
896
- appendEvent({
897
- eventType: "distill_invoked",
898
- ref: inputRef,
899
- metadata: {
900
- outcome: "skipped",
901
- lessonRef: effectiveLessonRef,
902
- message: proposalResult2.message,
903
- skipReason: proposalResult2.reason,
904
- ...eligMeta,
905
- },
906
- });
1020
+ fallback: null,
1021
+ onFallback: (evt) => recordFallback(evt.feature, evt.reason),
1022
+ });
1023
+ return { raw, fallbackReason };
1024
+ }
1025
+ /**
1026
+ * Build the terminal result for an empty/failed distill LLM response,
1027
+ * distinguishing the config-gate-off branch (event suppressed) from a real
1028
+ * transport/timeout/empty failure (emits `distill_invoked(llm_failed)`).
1029
+ * Extracted verbatim from `akmDistill`.
1030
+ */
1031
+ function distillEmptyResponseResult(args) {
1032
+ const { fallbackReason, inputRef, durableInputRef, itemRef, effectiveLessonRef, effectiveProposalKind, exclusionSet, filteredFeedbackCount, feedbackFullyFiltered, eligMeta, eventsCtx, } = args;
1033
+ // Distinguish "config gate disabled" from "LLM call failed". For the
1034
+ // config-disabled branch, we ALSO suppress the `distill_invoked` event
1035
+ // because no LLM work was actually invoked — emitting the event causes
1036
+ // the planner to accumulate phantom invocations that drown out real
1037
+ // signal.
1038
+ if (fallbackReason === "disabled") {
907
1039
  return {
908
1040
  schemaVersion: 1,
909
1041
  ok: true,
910
- outcome: "skipped",
1042
+ outcome: "config_disabled",
911
1043
  inputRef,
912
- lessonRef: effectiveLessonRef,
913
- message: proposalResult2.message,
1044
+ proposalRef: effectiveLessonRef,
1045
+ proposalKind: effectiveProposalKind,
1046
+ message: "distill is disabled in config; enable processes.distill.enabled to activate.",
1047
+ ...(exclusionSet.size > 0 ? { filteredFeedbackCount, feedbackFullyFiltered } : {}),
914
1048
  };
915
1049
  }
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);
1050
+ // LLM was actually invoked but produced nothing usable (transport error,
1051
+ // timeout, or empty/whitespace response). Emit the event so the failure
1052
+ // is observable.
921
1053
  appendEvent({
922
1054
  eventType: "distill_invoked",
923
- ref: inputRef,
1055
+ // Use item_ref when resolved, otherwise the input conceptId.
1056
+ ref: itemRef ?? durableInputRef,
924
1057
  metadata: {
925
- outcome: "queued",
926
- lessonRef: effectiveLessonRef,
1058
+ outcome: "llm_failed",
927
1059
  proposalRef: effectiveLessonRef,
928
1060
  proposalKind: effectiveProposalKind,
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 } : {}),
933
- ...(options.sourceRun !== undefined ? { sourceRun: options.sourceRun } : {}),
934
1061
  ...(exclusionSet.size > 0 ? { filteredFeedbackCount } : {}),
935
- ...(descriptionSwapped > 0 ? { descriptionSwapped } : {}),
936
1062
  ...eligMeta,
937
1063
  },
938
- });
1064
+ }, eventsCtx);
939
1065
  return {
940
1066
  schemaVersion: 1,
941
1067
  ok: true,
942
- outcome: "queued",
1068
+ outcome: "llm_failed",
943
1069
  inputRef,
944
- lessonRef: effectiveLessonRef,
945
1070
  proposalRef: effectiveLessonRef,
946
1071
  proposalKind: effectiveProposalKind,
947
- proposalId: proposal2.id,
948
- proposal: proposal2,
1072
+ message: "LLM call returned no usable output (timeout, empty, or error).",
949
1073
  ...(exclusionSet.size > 0 ? { filteredFeedbackCount, feedbackFullyFiltered } : {}),
950
- ...(descriptionSwapped > 0 ? { descriptionSwapped } : {}),
951
1074
  };
952
1075
  }
953
- // ── Helpers ─────────────────────────────────────────────────────────────────
954
- async function defaultLookup(ref) {
955
- return resolveAssetPath(ref, { mode: "index-only" });
1076
+ /**
1077
+ * The P2-B LLM-as-judge quality gate (fail-CLOSED; D-5/#388 three-band). Returns
1078
+ * a terminal rejection result when the judge rejects (or routes to review), or
1079
+ * the normalized [0,1] confidence to carry onto the proposal. Extracted verbatim
1080
+ * from `akmDistill`.
1081
+ */
1082
+ async function applyDistillQualityGate(args) {
1083
+ const { config, options, content, assetContent, chat, distillLlm, fetchSimilarLessonsFn, stash, inputRef, effectiveLessonRef, exclusionSet, filteredFeedbackCount, feedbackFullyFiltered, } = args;
1084
+ if (!(options.improveProfile?.processes?.distill?.qualityGate?.enabled ?? true)) {
1085
+ return { confidence: undefined };
1086
+ }
1087
+ // D-4 / #390: retrieve top-3 similar lessons for dedup check in judge.
1088
+ const similarLessons = await fetchSimilarLessonsFn(content.slice(0, 500), 3);
1089
+ const judgeResult = await runLessonQualityJudge(config, content, assetContent ?? "", chat, {
1090
+ ...(similarLessons.length > 0 ? { similarLessons } : {}),
1091
+ ...(distillLlm ? { llmConfig: distillLlm } : {}),
1092
+ ...(options.signal ? { signal: options.signal } : {}),
1093
+ });
1094
+ if (!judgeResult.pass) {
1095
+ if (judgeResult.reviewNeeded) {
1096
+ return {
1097
+ rejection: writeQualityRejection(stash, inputRef, effectiveLessonRef, content, judgeResult.score, judgeResult.reason, {
1098
+ reviewNeeded: true,
1099
+ ...(exclusionSet.size > 0 ? { filteredFeedbackCount, feedbackFullyFiltered } : {}),
1100
+ }, options.eligibilitySource, options.eventsCtx),
1101
+ };
1102
+ }
1103
+ return {
1104
+ rejection: writeQualityRejection(stash, inputRef, effectiveLessonRef, content, judgeResult.score, judgeResult.reason, exclusionSet.size > 0 ? { filteredFeedbackCount, feedbackFullyFiltered } : {}, options.eligibilitySource, options.eventsCtx),
1105
+ };
1106
+ }
1107
+ // Normalize 1-5 judge score to [0, 1]. Only a real passing verdict
1108
+ // reaches here (07 P0-2: the judge now fails CLOSED on no-LLM / timeout /
1109
+ // parse failure, so those return pass:false and never fall through to
1110
+ // this line). A defensive score>0 guard keeps confidence undefined for any
1111
+ // non-positive score the auto-accept gate should treat as unscored.
1112
+ return { confidence: judgeResult.score > 0 ? judgeResult.score / 5 : undefined };
1113
+ }
1114
+ /**
1115
+ * Read the target ref's `feedback` events and apply the #267 exclusion filter.
1116
+ * Returns the filtered events plus the exclusion tallies the outcome branches
1117
+ * carry. Extracted verbatim from `akmDistill`.
1118
+ */
1119
+ function readDistillFeedback(args) {
1120
+ const { readEventsImpl, options, durableInputRef } = args;
1121
+ const { events: unfilteredEvents } = readEventsImpl({
1122
+ ref: options.itemRef ?? durableInputRef,
1123
+ type: "feedback",
1124
+ excludeTags: options.excludeTags,
1125
+ includeTags: options.includeTags,
1126
+ });
1127
+ const events = unfilteredEvents;
1128
+ // #267 — feedback exclusion. Filter events whose `ref` matches the
1129
+ // exclusion list BEFORE the prompt is built. The original event stream
1130
+ // is never mutated; only the `feedback` slice that reaches the LLM is
1131
+ // affected. Exclusion refs are compared with event refs exactly.
1132
+ const exclusionList = options.excludeFeedbackFromRefs ?? [];
1133
+ const exclusionSet = new Set(exclusionList.map((ref) => ref.trim()).filter((ref) => ref.length > 0));
1134
+ const originalEventCount = events.length;
1135
+ const filteredEvents = exclusionSet.size > 0 ? events.filter((e) => !(e.ref !== undefined && exclusionSet.has(e.ref))) : events;
1136
+ const filteredFeedbackCount = originalEventCount - filteredEvents.length;
1137
+ const feedbackFullyFiltered = exclusionSet.size > 0 && originalEventCount > 0 && filteredEvents.length === 0;
1138
+ return { filteredEvents, exclusionSet, filteredFeedbackCount, feedbackFullyFiltered };
1139
+ }
1140
+ /**
1141
+ * Build the distill chat messages: inject the last 1–3 rejected proposals
1142
+ * (Reflexion verbal-RL), the optional WS-3b CLS adjacent-context, and the stash
1143
+ * authoring standards, then assemble the system+user prompt. Extracted verbatim
1144
+ * from `akmDistill`.
1145
+ */
1146
+ async function buildDistillMessages(args) {
1147
+ const { options, stash, inputRef, assetContent, feedback, effectiveProposalKind, effectiveLessonRef, fetchSimilarLessonsFn, } = args;
1148
+ // Inject last 1–3 rejected proposals for this ref as Reflexion-style
1149
+ // verbal-RL context so the LLM avoids regenerating refused proposals.
1150
+ const rejectedForRef = listProposals(stash, { ref: inputRef, status: "rejected", includeArchive: true })
1151
+ .sort((a, b) => new Date(b.updatedAt ?? 0).getTime() - new Date(a.updatedAt ?? 0).getTime())
1152
+ .slice(0, MAX_REJECTED_PROPOSALS)
1153
+ .map((p) => ({
1154
+ reason: p.review?.reason ?? "no reason given",
1155
+ contentPreview: proposalContent(p).slice(0, 500),
1156
+ }));
1157
+ // WS-3b CLS interleaving (step 9).
1158
+ // When cls.enabled, inject embedding-retrieved adjacent lessons/knowledge
1159
+ // into the distill prompt so the LLM avoids overwriting prior generalizations
1160
+ // (catastrophic interference). DEFAULT OFF.
1161
+ const clsConfig = getImproveProcessConfig("distill", options.improveProfile)?.cls ?? {};
1162
+ let clsContext = "";
1163
+ if (clsConfig.enabled) {
1164
+ try {
1165
+ const adjacentCount = clsConfig.adjacentCount ?? 3;
1166
+ // Use the asset content or input ref as the query for adjacent retrieval.
1167
+ const clsQuery = assetContent ? assetContent.slice(0, 500) : inputRef;
1168
+ const adjacentItems = await fetchSimilarLessonsFn(clsQuery, adjacentCount);
1169
+ clsContext = buildClsContext(adjacentItems, clsConfig);
1170
+ }
1171
+ catch {
1172
+ // Fail open — CLS is supplemental, never required.
1173
+ }
1174
+ }
1175
+ // Distill output is a lesson/knowledge (non-wiki) → stash authoring
1176
+ // standards. Resolved once for this single call.
1177
+ const standardsContext = resolveStandardsContext(effectiveLessonRef, stash);
1178
+ const baseUserPrompt = buildDistillPrompt({
1179
+ inputRef,
1180
+ assetContent,
1181
+ feedback,
1182
+ proposalKind: effectiveProposalKind,
1183
+ ...(rejectedForRef.length > 0 ? { rejectedProposals: rejectedForRef } : {}),
1184
+ ...(standardsContext.trim() ? { standardsContext } : {}),
1185
+ });
1186
+ const userPrompt = clsContext ? `${baseUserPrompt}${clsContext}` : baseUserPrompt;
1187
+ return [
1188
+ { role: "system", content: effectiveProposalKind === "knowledge" ? KNOWLEDGE_SYSTEM_PROMPT : LESSON_SYSTEM_PROMPT },
1189
+ { role: "user", content: userPrompt },
1190
+ ];
1191
+ }
1192
+ async function defaultLookup(ref, stashDir) {
1193
+ return resolveAssetPath(ref, {
1194
+ stashDir,
1195
+ mode: "disk-only",
1196
+ directoryIndexNames: ["SKILL.md"],
1197
+ preserveDirectNameFallback: true,
1198
+ honorOrigin: false,
1199
+ });
956
1200
  }