akm-cli 0.9.30 → 0.10.26101001-alpha

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 (353) hide show
  1. package/CHANGELOG.md +282 -0
  2. package/README.md +10 -10
  3. package/ROADMAP.md +112 -0
  4. package/STABILITY.md +79 -53
  5. package/dist/api-node.mjs +20 -0
  6. package/dist/api.d.ts +23 -0
  7. package/dist/api.js +45 -0
  8. package/dist/assets/hints/cli-hints-full.md +13 -27
  9. package/dist/assets/hints/cli-hints-short.md +1 -1
  10. package/dist/assets/improve-strategies/default.json +3 -3
  11. package/dist/assets/prompts/consolidate-pair.md +9 -7
  12. package/dist/cli/shared.js +31 -48
  13. package/dist/cli/unknown-flags.js +6 -43
  14. package/dist/cli.js +8 -34
  15. package/dist/commands/agent/contribute-cli.js +1 -1
  16. package/dist/commands/completions.js +1 -2
  17. package/dist/commands/config-cli.js +5 -28
  18. package/dist/commands/env/env-cli.js +5 -5
  19. package/dist/commands/env/secret-cli.js +2 -2
  20. package/dist/commands/health/checks.js +41 -16
  21. package/dist/commands/health/plugin-staleness.js +52 -22
  22. package/dist/commands/improve/autonomy-gate.js +5 -3
  23. package/dist/commands/improve/consolidate/pair-pass.js +7 -1
  24. package/dist/commands/improve/consolidate.js +6 -35
  25. package/dist/commands/improve/distill/lesson-update.js +91 -0
  26. package/dist/commands/improve/distill-guards.js +1 -43
  27. package/dist/commands/improve/distill.js +140 -20
  28. package/dist/commands/improve/extract-cli.js +1 -1
  29. package/dist/commands/improve/improve-cli.js +120 -114
  30. package/dist/commands/improve/improve-strategies.js +8 -12
  31. package/dist/commands/improve/loop-stages.js +0 -1
  32. package/dist/commands/improve/planner.js +0 -1
  33. package/dist/commands/improve/preparation.js +40 -99
  34. package/dist/commands/improve/proactive-maintenance.js +2 -2
  35. package/dist/commands/improve/reflect-noise.js +3 -55
  36. package/dist/commands/improve/reflect.js +3 -8
  37. package/dist/commands/improve/retrieval-scope.js +2 -2
  38. package/dist/commands/improve/salience.js +2 -9
  39. package/dist/commands/improve/stage.js +14 -9
  40. package/dist/commands/lint/index.js +5 -20
  41. package/dist/commands/observability-cli.js +3 -9
  42. package/dist/commands/proposal/drain.js +39 -6
  43. package/dist/commands/proposal/proposal-cli.js +10 -30
  44. package/dist/commands/proposal/repository.js +3 -3
  45. package/dist/commands/read/knowledge.js +3 -3
  46. package/dist/commands/read/remember-cli.js +0 -13
  47. package/dist/commands/read/search-cli.js +29 -97
  48. package/dist/commands/read/search.js +0 -11
  49. package/dist/commands/read/show.js +0 -19
  50. package/dist/commands/sources/plugin-upgrade.js +164 -101
  51. package/dist/commands/sources/sources-cli.js +1 -15
  52. package/dist/commands/sources/stash-cli.js +4 -12
  53. package/dist/commands/tasks/tasks-cli.js +4 -115
  54. package/dist/commands/workflow/plan.js +11 -33
  55. package/dist/commands/workflow-cli.js +2 -7
  56. package/dist/core/adapter/adapter-ids.js +1 -2
  57. package/dist/core/adapter/adapters/agent-skills-adapter.js +0 -4
  58. package/dist/core/adapter/adapters/akm-adapter.js +9 -48
  59. package/dist/core/adapter/adapters/akm-lint.js +10 -26
  60. package/dist/core/adapter/adapters/akm-metadata.js +2 -72
  61. package/dist/core/adapter/adapters/akm-task-adapter.js +0 -5
  62. package/dist/core/adapter/adapters/akm-workflow-adapter.js +9 -28
  63. package/dist/core/adapter/adapters/dotenv-adapter.js +1 -20
  64. package/dist/core/adapter/adapters/generic-files-adapter.js +0 -10
  65. package/dist/core/adapter/adapters/llm-wiki-adapter.js +0 -4
  66. package/dist/core/adapter/adapters/tool-dir-shared.js +0 -24
  67. package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -4
  68. package/dist/core/adapter/recognize-match.js +3 -4
  69. package/dist/core/common.js +18 -2
  70. package/dist/core/config/config-schema.js +12 -15
  71. package/dist/core/config/config-types.js +6 -15
  72. package/dist/core/config/config-walker.js +6 -23
  73. package/dist/core/config/config.js +17 -80
  74. package/dist/core/config/engine-semantics.js +7 -3
  75. package/dist/core/config/schema/engines.js +7 -7
  76. package/dist/core/config/schema/improve-processes.js +8 -61
  77. package/dist/core/config/schema/improve.js +2 -21
  78. package/dist/core/config/schema/index-config.js +6 -39
  79. package/dist/core/env-secret-ref.js +2 -45
  80. package/dist/core/errors.js +1 -2
  81. package/dist/core/extra-params.js +2 -86
  82. package/dist/core/file-lock.js +10 -1
  83. package/dist/core/improve-result.js +11 -1
  84. package/dist/core/mutation-target.js +2 -2
  85. package/dist/core/recognition-util.js +4 -5
  86. package/dist/core/warn.js +21 -4
  87. package/dist/core/write-source.js +3 -3
  88. package/dist/execution/target-ref.js +1 -2
  89. package/dist/indexer/search/db-search.js +4 -19
  90. package/dist/indexer/search/fts-query.js +0 -19
  91. package/dist/integrations/agent/engine-resolution.js +5 -2
  92. package/dist/integrations/agent/index.js +1 -1
  93. package/dist/integrations/agent/profiles.js +41 -98
  94. package/dist/integrations/agent/request-lowering.js +3 -3
  95. package/dist/integrations/harnesses/aider/index.js +5 -0
  96. package/dist/integrations/harnesses/amazonq/index.js +1 -0
  97. package/dist/integrations/harnesses/claude/config-import.js +2 -3
  98. package/dist/integrations/harnesses/claude/index.js +5 -0
  99. package/dist/integrations/harnesses/codex/index.js +1 -0
  100. package/dist/integrations/harnesses/copilot/index.js +1 -0
  101. package/dist/integrations/harnesses/gemini/index.js +1 -0
  102. package/dist/integrations/harnesses/index.js +18 -39
  103. package/dist/integrations/harnesses/opencode/agent-builder.js +60 -35
  104. package/dist/integrations/harnesses/opencode/config-import.js +121 -52
  105. package/dist/integrations/harnesses/opencode/index.js +22 -0
  106. package/dist/integrations/harnesses/opencode/plugin-layout.js +68 -0
  107. package/dist/integrations/harnesses/opencode/session-log-types.js +12 -0
  108. package/dist/integrations/harnesses/opencode/session-log-v1.js +72 -0
  109. package/dist/integrations/harnesses/opencode/session-log-v2.js +140 -0
  110. package/dist/integrations/harnesses/opencode/session-log.js +104 -115
  111. package/dist/integrations/harnesses/opencode/version.js +98 -0
  112. package/dist/integrations/harnesses/opencode-sdk/{harness.js → index.js} +5 -17
  113. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +105 -206
  114. package/dist/integrations/harnesses/opencode-sdk/v1-adapter.js +199 -0
  115. package/dist/integrations/harnesses/opencode-sdk/v2-adapter.js +228 -0
  116. package/dist/integrations/harnesses/opencode-sdk/wire.js +66 -0
  117. package/dist/integrations/harnesses/openhands/index.js +1 -0
  118. package/dist/integrations/harnesses/pi/index.js +1 -0
  119. package/dist/integrations/harnesses/shared.js +2 -0
  120. package/dist/integrations/harnesses/types.js +2 -0
  121. package/dist/llm/client.js +74 -14
  122. package/dist/output/cli-hints.js +1 -1
  123. package/dist/output/context.js +10 -19
  124. package/dist/output/format-exempt.js +5 -8
  125. package/dist/output/shapes/helpers.js +1 -22
  126. package/dist/output/shapes/passthrough.js +0 -1
  127. package/dist/output/shapes.js +3 -31
  128. package/dist/output/stdout.js +5 -1
  129. package/dist/output/text/command-format.js +6 -1
  130. package/dist/output/text/workflow-format.js +4 -22
  131. package/dist/scripts/akm-migrate-node.js +15746 -16706
  132. package/dist/scripts/akm-migrate.js +16319 -17275
  133. package/dist/setup/detect.js +10 -4
  134. package/dist/setup/harness-config-import.js +5 -14
  135. package/dist/setup/setup.js +1 -1
  136. package/dist/setup/steps/tasks.js +12 -13
  137. package/dist/storage/repositories/task-history-repository.js +0 -5
  138. package/dist/storage/repositories/workflow-runs-repository.js +5 -15
  139. package/dist/tasks/embedded.js +2 -1
  140. package/dist/tasks/run/task-history.js +0 -2
  141. package/dist/tasks/schedule-gates.js +39 -0
  142. package/dist/tasks/scheduler-binding.js +0 -16
  143. package/dist/tasks/scheduler-sync.js +3 -97
  144. package/dist/tasks/source/bounded-document.js +1 -1
  145. package/dist/tasks/source/task-source-v4.js +2 -12
  146. package/dist/tasks/task-run-reserved-flags.js +2 -31
  147. package/dist/workflows/authoring/authoring.js +6 -6
  148. package/dist/workflows/compile.js +8 -65
  149. package/dist/workflows/exec/child-workflow.js +22 -3
  150. package/dist/workflows/exec/native-executor.js +12 -147
  151. package/dist/workflows/exec/run-workflow.js +31 -103
  152. package/dist/workflows/exec/step-work.js +9 -178
  153. package/dist/workflows/exec/unit-dispatch.js +0 -10
  154. package/dist/workflows/freeze/environment.js +3 -44
  155. package/dist/workflows/freeze/freeze.js +8 -41
  156. package/dist/workflows/freeze/targets/child-workflow.js +27 -63
  157. package/dist/workflows/freeze/targets/command.js +2 -16
  158. package/dist/workflows/freeze/targets/shell.js +0 -2
  159. package/dist/workflows/freeze/task-bindings.js +2 -28
  160. package/dist/workflows/ir/params.js +12 -2
  161. package/dist/workflows/parser.js +82 -232
  162. package/dist/workflows/plan.js +1 -1
  163. package/dist/workflows/renderer.js +10 -19
  164. package/dist/workflows/runtime/run-plan.js +11 -53
  165. package/dist/workflows/runtime/runs.js +4 -27
  166. package/dist/workflows/source-files.js +12 -16
  167. package/dist/workflows/source-semantics.js +1 -93
  168. package/docs/README.md +43 -42
  169. package/docs/agents/README.md +15 -0
  170. package/docs/agents/agent-install.md +14 -0
  171. package/docs/architecture/README.md +67 -0
  172. package/docs/architecture/adapters.md +258 -0
  173. package/docs/architecture/akm-architecture-decision-history.md +2055 -0
  174. package/docs/architecture/akm-core-principles.md +126 -0
  175. package/docs/architecture/architecture.md +578 -0
  176. package/docs/architecture/comparisons/claude-code-vs-akm-workflows-full.md +550 -0
  177. package/docs/architecture/decisions/0001-no-interpolation-attached-structured-context.md +72 -0
  178. package/docs/architecture/decisions/0002-unit-reuse-and-input-hash-scope.md +142 -0
  179. package/docs/architecture/decisions/0003-child-env-allowlist-and-provenance.md +189 -0
  180. package/docs/architecture/decisions/0004-task-input-contract-and-flag-coercion.md +97 -0
  181. package/docs/architecture/decisions/0005-task-result-vocabulary-and-legacy-read-mapping.md +108 -0
  182. package/docs/architecture/decisions/0006-task-source-version-routing.md +124 -0
  183. package/docs/architecture/decisions/0007-workflow-composition-bounds.md +83 -0
  184. package/docs/architecture/decisions/0008-task-binding-normalization.md +88 -0
  185. package/docs/architecture/decisions/0009-child-run-publication-column-parity.md +80 -0
  186. package/docs/architecture/decisions/0010-driver-protocol-cut.md +36 -0
  187. package/docs/architecture/decisions/0011-engine-run-loop-invariants.md +125 -0
  188. package/docs/architecture/decisions/README.md +62 -0
  189. package/docs/architecture/improvement.md +471 -0
  190. package/docs/architecture/internals/classification.md +154 -0
  191. package/docs/architecture/internals/fresh-host-rebuild-runbook.md +40 -0
  192. package/docs/architecture/internals/functional-contract-patterns.md +340 -0
  193. package/docs/architecture/internals/health-advisories.md +110 -0
  194. package/docs/architecture/internals/improve-workflow.md +720 -0
  195. package/docs/architecture/internals/indexing.md +498 -0
  196. package/docs/architecture/internals/registry-network-boundary.md +57 -0
  197. package/docs/architecture/internals/search.md +248 -0
  198. package/docs/architecture/internals/storage-locations.md +943 -0
  199. package/docs/architecture/persisted-data-compat.md +68 -0
  200. package/docs/architecture/reviews/env-secret-access.md +319 -0
  201. package/docs/architecture/runtime-boundary-design.md +153 -0
  202. package/docs/architecture/specs/0.9.0-close-out-plan.md +361 -0
  203. package/docs/architecture/specs/0.9.0-decisions.md +532 -0
  204. package/docs/architecture/specs/0.9.0-docs-code-drift-register.md +745 -0
  205. package/docs/architecture/specs/0.9.0-open-items-register.md +135 -0
  206. package/docs/architecture/specs/0.9.0-public-api-issue-backlog.md +740 -0
  207. package/docs/architecture/specs/0.9.0-release-surface-review.md +636 -0
  208. package/docs/architecture/specs/agent-command-engine-model-design.md +513 -0
  209. package/docs/architecture/specs/akm-0.9.0-bundle-adapter-spec.md +501 -0
  210. package/docs/architecture/specs/akm-0.9.0-ref-grammar-decision.md +291 -0
  211. package/docs/architecture/specs/akm-format-neutral-bundle-workspace-spec.md +2059 -0
  212. package/docs/architecture/specs/di-seams-plan.md +539 -0
  213. package/docs/architecture/specs/driver-protocol-keep-or-cut.md +275 -0
  214. package/docs/architecture/specs/fact-asset-type.md +142 -0
  215. package/docs/architecture/specs/improve-collapse-churn-detector-design.md +634 -0
  216. package/docs/architecture/specs/okf-support.md +148 -0
  217. package/docs/architecture/specs/ref.md +329 -0
  218. package/docs/architecture/specs/reviews/task-workflow-unification/README.md +72 -0
  219. package/docs/architecture/specs/reviews/task-workflow-unification/findings-consolidated.md +152 -0
  220. package/docs/architecture/specs/reviews/task-workflow-unification/library-eval-alternatives.md +352 -0
  221. package/docs/architecture/specs/reviews/task-workflow-unification/library-eval-bunqueue.md +300 -0
  222. package/docs/architecture/specs/reviews/task-workflow-unification/library-eval-openworkflow.md +402 -0
  223. package/docs/architecture/specs/reviews/task-workflow-unification/opus-review-1.md +758 -0
  224. package/docs/architecture/specs/reviews/task-workflow-unification/opus-review-2.md +645 -0
  225. package/docs/architecture/specs/reviews/task-workflow-unification/opus-review-3.md +602 -0
  226. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-adversarial.md +27 -0
  227. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-architecture.md +27 -0
  228. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-coverage.md +74 -0
  229. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-security.md +15 -0
  230. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r2-adversarial.md +132 -0
  231. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r2-verification.md +147 -0
  232. package/docs/architecture/specs/stash-conventions-code-spec.md +275 -0
  233. package/docs/architecture/specs/stash-organization-conventions.md +377 -0
  234. package/docs/architecture/specs/task-workflow-format-unification.md +721 -0
  235. package/docs/architecture/specs/workflow-engine-buy-vs-build.md +231 -0
  236. package/docs/architecture/specs/workflow-format-unification.md +432 -0
  237. package/docs/architecture/testing/manual-testing-checklist.md +3411 -0
  238. package/docs/architecture/testing/okf-v0.2-conformance-runbook.md +924 -0
  239. package/docs/architecture/testing/task-workflow-manual-tests.md +1720 -0
  240. package/docs/architecture/testing/testing-workflow.md +446 -0
  241. package/docs/architecture/workflow-engine.md +223 -0
  242. package/docs/guides/README.md +33 -0
  243. package/docs/guides/author-bundles.md +310 -0
  244. package/docs/guides/author-workflows.md +490 -0
  245. package/docs/guides/bundles.md +321 -0
  246. package/docs/guides/capture-knowledge.md +136 -0
  247. package/docs/guides/claude-code-vs-akm-workflows.md +29 -0
  248. package/docs/guides/concepts.md +93 -0
  249. package/docs/guides/discover-and-load.md +158 -0
  250. package/docs/guides/getting-started.md +240 -0
  251. package/docs/guides/improve-the-library.md +246 -0
  252. package/docs/guides/knowledge-management.md +18 -0
  253. package/docs/guides/recipes/headless-install.md +97 -0
  254. package/docs/guides/recipes/website-source.md +28 -0
  255. package/docs/guides/run-workflows.md +279 -0
  256. package/docs/guides/scheduling.md +123 -0
  257. package/docs/guides/use-with-any-agent.md +174 -0
  258. package/docs/guides/wikis.md +81 -0
  259. package/docs/integration/bundling-akm.md +7 -6
  260. package/docs/maintainers/README.md +19 -0
  261. package/docs/maintainers/curate-workmap.md +335 -0
  262. package/docs/maintainers/eval.md +3 -0
  263. package/docs/maintainers/local-development.md +108 -0
  264. package/docs/maintainers/release-checklist.md +132 -0
  265. package/docs/migration/README.md +2 -2
  266. package/docs/migration/release-notes/0.0.13.md +1 -1
  267. package/docs/migration/release-notes/0.1.0.md +1 -1
  268. package/docs/migration/release-notes/0.2.0.md +1 -1
  269. package/docs/migration/release-notes/0.3.0.md +1 -1
  270. package/docs/migration/release-notes/0.5.0.md +1 -1
  271. package/docs/migration/release-notes/0.6.0.md +1 -1
  272. package/docs/migration/release-notes/0.7.0.md +1 -1
  273. package/docs/migration/release-notes/0.7.3.md +2 -2
  274. package/docs/migration/release-notes/0.7.4.md +1 -1
  275. package/docs/migration/release-notes/0.7.5.md +1 -1
  276. package/docs/migration/release-notes/0.8.0.md +1 -1
  277. package/docs/migration/release-notes/0.9.0.md +1 -1
  278. package/docs/migration/release-notes/0.9.14.md +1 -1
  279. package/docs/migration/release-notes/0.9.15.md +1 -1
  280. package/docs/migration/release-notes/0.9.16.md +1 -1
  281. package/docs/migration/release-notes/0.9.17.md +1 -1
  282. package/docs/migration/release-notes/0.9.19.md +1 -1
  283. package/docs/migration/release-notes/0.9.2.md +1 -1
  284. package/docs/migration/release-notes/README.md +27 -31
  285. package/docs/migration/v0.5-to-v0.6.md +759 -0
  286. package/docs/migration/v0.8-to-v0.9.md +1 -1
  287. package/docs/plans/0.9.2-agent-command-workflow-plan.md +423 -0
  288. package/docs/posts/README.md +30 -0
  289. package/docs/posts/akm-command-tour.md +495 -0
  290. package/docs/posts/akm-logo-sized.webp +0 -0
  291. package/docs/posts/belief-aware-memory-12.md +139 -0
  292. package/docs/posts/config-v2-lessons-16.md +191 -0
  293. package/docs/posts/improve-loop-autonomous-10.md +225 -0
  294. package/docs/posts/improve-pipeline-debugging-0803.md +179 -0
  295. package/docs/posts/intro-part-01.md +76 -0
  296. package/docs/posts/intro-part-02.md +221 -0
  297. package/docs/posts/intro-part-03.md +190 -0
  298. package/docs/posts/intro-part-04.md +221 -0
  299. package/docs/posts/llm-mode-reflect-13.md +166 -0
  300. package/docs/posts/openpalm-built-on-akm.md +86 -0
  301. package/docs/posts/progressive-disclosure-06.md +129 -0
  302. package/docs/posts/proposal-safety-net-15.md +112 -0
  303. package/docs/posts/release-0.5.0.md +247 -0
  304. package/docs/posts/release-0.6.0.md +117 -0
  305. package/docs/posts/release-0.7.0.md +159 -0
  306. package/docs/posts/release-0.8.0.md +332 -0
  307. package/docs/posts/scattered-skills-05.md +100 -0
  308. package/docs/posts/task-assets-persistent-workflows-11.md +222 -0
  309. package/docs/posts/team-skills-07.md +198 -0
  310. package/docs/posts/wiki-agent-knowledge-08.md +223 -0
  311. package/docs/posts/workflows-vaults-09.md +321 -0
  312. package/docs/product-surface.md +54 -0
  313. package/docs/reference/README.md +2 -2
  314. package/docs/reference/api.md +65 -0
  315. package/docs/reference/asset-types.md +166 -0
  316. package/docs/reference/bundle-types.md +2 -4
  317. package/docs/reference/cli.md +129 -175
  318. package/docs/reference/configuration.md +84 -19
  319. package/docs/reference/data-and-telemetry.md +1 -1
  320. package/docs/reference/env-and-secrets.md +126 -0
  321. package/docs/reference/memory.md +124 -0
  322. package/docs/reference/refs.md +156 -0
  323. package/docs/reference/registry.md +489 -0
  324. package/docs/reference/supported-formats.md +3 -4
  325. package/docs/reference/tasks.md +12 -19
  326. package/docs/reference/website-sources.md +141 -0
  327. package/docs/reference/workflow-schema.md +89 -317
  328. package/docs/reference/workflows.md +20 -48
  329. package/package.json +23 -16
  330. package/schemas/akm-config.json +2 -243
  331. package/schemas/akm-task.json +2 -3
  332. package/schemas/akm-workflow.json +47 -84
  333. package/dist/assets/improve-strategies/catchup.json +0 -41
  334. package/dist/assets/improve-strategies/proactive-maintenance.json +0 -24
  335. package/dist/assets/improve-strategies/quick.json +0 -17
  336. package/dist/assets/improve-strategies/reflect-distill.json +0 -29
  337. package/dist/assets/improve-strategies/thorough.json +0 -49
  338. package/dist/assets/tasks/improve/akm-improve-catchup.yml +0 -6
  339. package/dist/assets/tasks/improve/akm-improve-frequent.yml +0 -4
  340. package/dist/assets/tasks/improve/akm-improve-nightly.yml +0 -4
  341. package/dist/cli/retired-commands.js +0 -118
  342. package/dist/commands/tasks/validate.js +0 -176
  343. package/dist/core/config/legacy-source-shape-shim.js +0 -117
  344. package/dist/integrations/harnesses/ids.js +0 -28
  345. package/dist/tasks/source/task-source-v3-frozen.js +0 -424
  346. package/dist/tasks/source/task-to-v4.js +0 -863
  347. package/dist/workflows/exec/worktree.js +0 -372
  348. package/dist/workflows/freeze/child-output-references.js +0 -83
  349. package/dist/workflows/freeze/identity.js +0 -22
  350. package/dist/workflows/freeze/targets/script.js +0 -50
  351. package/dist/workflows/freeze/targets/task.js +0 -124
  352. package/dist/workflows/github-yaml.js +0 -506
  353. package/dist/workflows/runtime/run-outputs.js +0 -80
@@ -52,6 +52,7 @@ import { spawnSync } from "node:child_process";
52
52
  import fs from "node:fs";
53
53
  import os from "node:os";
54
54
  import path from "node:path";
55
+ import { OPENCODE_V1, OPENCODE_V2, readPackageVersion, } from "../../integrations/harnesses/opencode/plugin-layout.js";
55
56
  import { isExactSemver, isSemverRange, maxSatisfying, satisfiesRange } from "../../registry/semver.js";
56
57
  /**
57
58
  * Root directory holding Claude Code's plugin cache + marketplace clones.
@@ -66,41 +67,69 @@ function claudePluginsDir() {
66
67
  /**
67
68
  * Root of OpenCode's package cache, same override pattern as
68
69
  * {@link claudePluginsDir} (`AKM_OPENCODE_CACHE_DIR` for tests).
69
- * The OpenCode plugin (`akm-opencode`) bundles its own `akm-cli` under
70
- * `packages/akm-opencode/node_modules/akm-cli` and runs it in-process,
70
+ * The OpenCode plugins (`akm-opencode`, and `akm-opencode-v2` for OpenCode 2) bundle their own `akm-cli`
71
+ * (layout: `integrations/harnesses/opencode/plugin-layout.ts`) and run it in-process,
71
72
  * sharing the host's databases with every other akm install — its version
72
73
  * lag matters the same way a stale Claude plugin's does.
73
74
  */
74
75
  function opencodeCacheDir() {
75
76
  return process.env.AKM_OPENCODE_CACHE_DIR ?? path.join(os.homedir(), ".cache", "opencode");
76
77
  }
78
+ const OPENCODE_PLUGIN_LOCATIONS = [
79
+ {
80
+ // OpenCode 1: <cache>/packages/akm-opencode/node_modules/akm-cli (the folder read since this check began).
81
+ flavor: OPENCODE_V1,
82
+ advisory: "opencode-plugin-version",
83
+ label: "OpenCode's",
84
+ remedy: "update the OpenCode plugin",
85
+ folders: (cacheRoot) => [{ folder: path.join(cacheRoot, OPENCODE_V1.cacheFolder(OPENCODE_V1.pkg)) }],
86
+ },
87
+ {
88
+ // OpenCode 2: <cache>/npm/akm-opencode-v2@<spec>/<epoch-ms>/node_modules/akm-cli, newest build per spec.
89
+ flavor: OPENCODE_V2,
90
+ advisory: "opencode-v2-plugin-version",
91
+ label: "OpenCode 2's akm-opencode-v2",
92
+ remedy: "update the akm-opencode-v2 plugin",
93
+ folders: (cacheRoot) => {
94
+ const npmDir = path.join(cacheRoot, OPENCODE_V2.cacheFolder(""));
95
+ let specs;
96
+ try {
97
+ specs = fs.readdirSync(npmDir).filter((name) => name.startsWith(`${OPENCODE_V2.pkg}@`));
98
+ }
99
+ catch {
100
+ return [];
101
+ }
102
+ return specs.sort().map((spec) => ({ spec, folder: path.join(npmDir, spec) }));
103
+ },
104
+ },
105
+ ];
77
106
  /**
78
- * Read the OpenCode plugin's bundled `akm-cli` version from its
79
- * `package.json` — never executed, just parsed, mirroring
80
- * {@link detectInstalledPlugins}'s manifest read. `undefined` when the
81
- * OpenCode plugin, or its bundled akm-cli, is not installed.
107
+ * The bundled `akm-cli` of each cached copy of `plugin`: its `package.json` is read, never executed,
108
+ * mirroring {@link detectInstalledPlugins}'s manifest read. Copies with no bundled akm-cli are skipped.
82
109
  */
83
- function detectOpencodeBundledAkmVersion(cacheRoot) {
84
- const pkgPath = path.join(cacheRoot, "packages", "akm-opencode", "node_modules", "akm-cli", "package.json");
85
- try {
86
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
87
- return typeof pkg.version === "string" ? pkg.version : undefined;
88
- }
89
- catch {
90
- return undefined;
110
+ function findBundledAkm(plugin, cacheRoot) {
111
+ const found = [];
112
+ for (const { spec, folder } of plugin.folders(cacheRoot)) {
113
+ const nodeModules = plugin.flavor.nodeModulesDir(folder);
114
+ const version = nodeModules && readPackageVersion(path.join(nodeModules, "akm-cli", "package.json"));
115
+ if (version !== undefined)
116
+ found.push(spec ? { spec, version } : { version });
91
117
  }
118
+ return found;
92
119
  }
93
- function buildOpencodeBundledAdvisory(bundledVersion, cliVersion) {
120
+ function buildOpencodeBundledAdvisory(plugin, bundled, cliVersion) {
121
+ const bundledVersion = bundled.version;
94
122
  const stale = bundledVersion !== cliVersion;
123
+ const where = bundled.spec ? ` (${bundled.spec})` : "";
95
124
  return {
96
- name: "opencode-plugin-version",
125
+ name: plugin.advisory,
97
126
  kind: "deterministic",
98
127
  status: stale ? "warn" : "pass",
99
128
  confidence: "high",
100
129
  message: stale
101
- ? `OpenCode's bundled akm-cli is v${bundledVersion}, but the running CLI is v${cliVersion} — it shares the same databases in-process; update the OpenCode plugin.`
102
- : `OpenCode's bundled akm-cli matches the running v${cliVersion}.`,
103
- evidence: { bundledVersion, cliVersion },
130
+ ? `${plugin.label} bundled akm-cli${where} is v${bundledVersion}, but the running CLI is v${cliVersion} — it shares the same databases in-process; ${plugin.remedy}.`
131
+ : `${plugin.label} bundled akm-cli${where} matches the running v${cliVersion}.`,
132
+ evidence: bundled.spec ? { bundledVersion, cliVersion, spec: bundled.spec } : { bundledVersion, cliVersion },
104
133
  };
105
134
  }
106
135
  /**
@@ -218,9 +247,10 @@ export function collectPluginStalenessAdvisories(options) {
218
247
  const plugins = detectInstalledPlugins(pluginsRoot);
219
248
  const results = plugins.map((plugin) => buildAdvisory(plugin, options.cliVersion, pluginsRoot, listRemoteTags));
220
249
  const opencodeCacheRoot = options.opencodeCacheRoot ?? opencodeCacheDir();
221
- const bundledVersion = detectOpencodeBundledAkmVersion(opencodeCacheRoot);
222
- if (bundledVersion !== undefined) {
223
- results.push(buildOpencodeBundledAdvisory(bundledVersion, options.cliVersion));
250
+ for (const plugin of OPENCODE_PLUGIN_LOCATIONS) {
251
+ for (const bundled of findBundledAkm(plugin, opencodeCacheRoot)) {
252
+ results.push(buildOpencodeBundledAdvisory(plugin, bundled, options.cliVersion));
253
+ }
224
254
  }
225
255
  return results;
226
256
  }
@@ -6,7 +6,7 @@ import { IMPROVE_AUTONOMY_CONFIG_KEY, isImproveAutonomyEnabled, } from "../../co
6
6
  export const AUTONOMY_LANES = ["memoryInference", "triagePromote", "memoryCleanup"];
7
7
  const LANE_REASONS = {
8
8
  memoryInference: "writes derived memory children and rewrites parent frontmatter",
9
- triagePromote: "auto-accepts queued proposals into the stash (downgraded to queue)",
9
+ triagePromote: "auto-accepts queued proposals the triage judgment tier approves (downgraded to queue)",
10
10
  memoryCleanup: "rewrites belief-state frontmatter and moves files into the cleanup archive",
11
11
  };
12
12
  /**
@@ -59,8 +59,10 @@ export function applyAutonomyGate(strategy, config) {
59
59
  }
60
60
  // Triage stays ENABLED — queued proposals are still triaged, they just are not
61
61
  // auto-accepted. Disabling it would remove review work the user asked for,
62
- // which is the opposite of what a review-first default should do.
63
- if (processes.triage?.applyMode === "promote") {
62
+ // which is the opposite of what a review-first default should do. Only a
63
+ // promote that runs the judgment tier is gated (#1143); a deterministic-only
64
+ // promote is the default and needs no opt-in.
65
+ if (processes.triage?.applyMode === "promote" && processes.triage.judgment?.enabled === true) {
64
66
  processes.triage = { ...processes.triage, applyMode: "queue" };
65
67
  gated.push(gatedLane("triagePromote"));
66
68
  }
@@ -465,7 +465,13 @@ function orderByAge(x, y) {
465
465
  }
466
466
  /** The user message: dates decide "A (older)" / "B (newer)" (plan Appendix A), matching the calibration sample's own ordering. */
467
467
  export function buildPairUserPrompt(older, newer) {
468
- return [...sideSection("A (older)", older), ...sideSection("B (newer)", newer)].join("\n");
468
+ // Equal dates (a batch added in one commit, notes from one file time) make neither side newer: say so, or the judge
469
+ // reads "B (newer)" as proof that B replaces A (#1134).
470
+ const tied = Date.parse(older.createdIso) === Date.parse(newer.createdIso);
471
+ return [
472
+ ...sideSection(tied ? "A (same age as B)" : "A (older)", older),
473
+ ...sideSection(tied ? "B (same age as A)" : "B (newer)", newer),
474
+ ].join("\n");
469
475
  }
470
476
  /** The tombstone-vocabulary reason a judge label maps to (`supersedes` -> `superseded`; the rest unchanged). */
471
477
  export function tombstoneReason(label) {
@@ -28,7 +28,7 @@ import { detectAdapterId } from "../../core/adapter/detect-adapter.js";
28
28
  import { assembleAssetFromString, serializeFrontmatter } from "../../core/asset/asset-serialize.js";
29
29
  import { parseFrontmatter } from "../../core/asset/frontmatter.js";
30
30
  import { conceptIdFromTypeName, displayRef, parseRefInput } from "../../core/asset/resolve-ref.js";
31
- import { getImproveProcessConfig, loadConfig } from "../../core/config/config.js";
31
+ import { loadConfig } from "../../core/config/config.js";
32
32
  import { parseEmbeddedJsonResponse } from "../../core/parse.js";
33
33
  import { openStateDatabase } from "../../core/state-db.js";
34
34
  import { warn, warnVerbose } from "../../core/warn.js";
@@ -210,36 +210,6 @@ async function clusterMemoriesBySimilarity(memories, config, stateDb, signal) {
210
210
  }
211
211
  return { ordered, embedTelemetry: telemetry };
212
212
  }
213
- /**
214
- * Anti-collapse (default on, `antiCollapse.enabled: false` opts out): a small
215
- * deterministic sample of the pool is spread through the similarity order so
216
- * consolidation is not purely similarity-driven.
217
- */
218
- function injectRandomClusterMembers(memories, profile, warnings) {
219
- const config = getImproveProcessConfig("consolidate", profile)?.antiCollapse ?? {};
220
- if (config.enabled === false || memories.length <= 2)
221
- return memories;
222
- const fraction = config.randomClusterFraction ?? 0.05;
223
- const randomCount = Math.max(1, Math.floor(memories.length * fraction));
224
- const sample = [...memories]
225
- .sort((a, b) => contentHash(a.name).localeCompare(contentHash(b.name)))
226
- .slice(0, randomCount);
227
- const sampled = new Set(sample.map((m) => m.name));
228
- const interval = Math.max(2, Math.floor(memories.length / randomCount));
229
- const out = [];
230
- let next = 0;
231
- for (let i = 0; i < memories.length; i++) {
232
- const m = memories[i];
233
- if (m && !sampled.has(m.name))
234
- out.push(m);
235
- if (i > 0 && i % interval === 0 && next < sample.length)
236
- out.push(sample[next++]);
237
- }
238
- while (next < sample.length)
239
- out.push(sample[next++]);
240
- warnings.push(`Anti-collapse: injected ${randomCount} random (non-similarity-driven) cluster member(s) into consolidation pool (fraction=${fraction}).`);
241
- return out;
242
- }
243
213
  /** Body hashes of pending consolidate proposals, so the prompt can mark memories already queued. */
244
214
  function loadPendingConsolidateProposalHashes(stashDir, proposalsCtx) {
245
215
  const hashes = new Set();
@@ -670,11 +640,12 @@ async function judgeConsolidationChunks(args) {
670
640
  }
671
641
  return planned;
672
642
  }
643
+ /** Assumed wall-clock seconds per consolidation chunk for the up-front budget cap. */
644
+ const CHUNK_SECONDS_ESTIMATE = 30;
673
645
  /**
674
646
  * The model's plan for the narrowed pool: chunk size from the context window,
675
647
  * an up-front cap when the remaining budget cannot cover every chunk (oldest
676
- * first, the rest deferred), similarity clustering, anti-collapse, then the
677
- * chunk loop.
648
+ * first, the rest deferred), similarity clustering, then the chunk loop.
678
649
  */
679
650
  async function planConsolidation(opts, config, stashDir, memories, warnings, stateDb, acc) {
680
651
  const llmRunner = opts.llmRunner ?? undefined;
@@ -685,7 +656,7 @@ async function planConsolidation(opts, config, stashDir, memories, warnings, sta
685
656
  let budgeted = memories;
686
657
  const budgetMs = opts.signal?.remainingBudgetMs;
687
658
  if (opts.signal && budgetMs !== undefined) {
688
- const safeChunks = Math.max(0, Math.floor((Math.max(0, budgetMs) / 1000 / (opts.p90ChunkSecondsDefault ?? 30)) * 0.6));
659
+ const safeChunks = Math.max(0, Math.floor((Math.max(0, budgetMs) / 1000 / CHUNK_SECONDS_ESTIMATE) * 0.6));
689
660
  if (safeChunks * chunkSize < memories.length) {
690
661
  budgeted = [...memories]
691
662
  .sort((a, b) => mtimeMsOf(a) - mtimeMsOf(b) || a.name.localeCompare(b.name))
@@ -700,7 +671,7 @@ async function planConsolidation(opts, config, stashDir, memories, warnings, sta
700
671
  if (llmRunner && willDispatch)
701
672
  assertRunnerCredentials(llmRunner);
702
673
  const { ordered, embedTelemetry } = await clusterMemoriesBySimilarity(budgeted, config, stateDb, opts.signal);
703
- const chunks = slice(injectRandomClusterMembers(ordered, opts.improveProfile, warnings));
674
+ const chunks = slice(ordered);
704
675
  const pendingProposalBodyHashes = loadPendingConsolidateProposalHashes(stashDir, opts.proposalsCtx);
705
676
  warn(`[consolidate] ${budgeted.length} memories / ${chunks.length} chunk(s) / chunk_size=${chunkSize}` +
706
677
  ` / pending-proposal hashes: ${pendingProposalBodyHashes.size}`);
@@ -0,0 +1,91 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * Distill's lesson update (#1090): when a memory's lesson repeats a lesson the library already holds, the writer
6
+ * is asked to extend that lesson with what the memory adds instead of the duplicate being thrown away. These are
7
+ * the prompt, the reply schema and the two deterministic checks; the call, the judge and the proposal are in
8
+ * `../distill`.
9
+ */
10
+ import { parseEmbeddedJsonResponse } from "../../../core/parse.js";
11
+ export const DISTILL_LESSON_UPDATE_JSON_SCHEMA = {
12
+ type: "object",
13
+ required: ["reason", "new_facts", "decision", "ref", "body"],
14
+ additionalProperties: false,
15
+ properties: {
16
+ reason: {
17
+ type: "string",
18
+ description: "One sentence: which listed lesson states the rule and what the memory adds to it, or why nothing.",
19
+ },
20
+ new_facts: {
21
+ type: "array",
22
+ items: { type: "string" },
23
+ description: "Written before the body: each cause, step, number, limit or consequence the memory states and the lesson lacks, one short phrase each. Empty when there is none.",
24
+ },
25
+ decision: {
26
+ type: "string",
27
+ enum: ["update", "none"],
28
+ description: "`none` when no listed lesson states the memory's rule, or `new_facts` is empty: leave `ref` and `body` empty. Otherwise `update`.",
29
+ },
30
+ ref: { type: "string", description: "The `Existing lesson ref:` of the lesson to extend. Empty for `none`." },
31
+ body: {
32
+ type: "string",
33
+ description: "That lesson's whole body with the new lines added, every existing line unchanged. No frontmatter. Empty for `none`.",
34
+ },
35
+ },
36
+ };
37
+ export function buildLessonUpdatePrompt(memory, feedback, candidates) {
38
+ const lines = [
39
+ "A lesson written from the memory below would repeat a lesson the library already holds. Do not write a new lesson. Extend the existing one with what the memory adds.",
40
+ "",
41
+ "Memory:",
42
+ "```",
43
+ memory.slice(0, 3000),
44
+ "```",
45
+ ];
46
+ if (feedback.length > 0)
47
+ lines.push("", "Feedback recorded about the memory:", "```", feedback.join("\n").slice(0, 1500), "```");
48
+ lines.push("", "Existing lessons, nearest first (they may be on another subject):");
49
+ for (const candidate of candidates) {
50
+ lines.push(`\nExisting lesson ref: ${candidate.ref}`, "```", candidate.body.slice(0, 3000), "```");
51
+ }
52
+ lines.push("", "Pick the one existing lesson that already states the rule the memory gives. Write its whole body again with the new lines added:", "- Copy every existing line unchanged, in its place. Do not reword, shorten, reorder or drop any of them.", "First list in new_facts each cause, step, number, limit or consequence the memory states and the lesson lacks, one short phrase each: check every sentence of the memory against the lesson. The memory's own incident retold (what happened in one test, project or day) is not a new fact unless it carries one of these. Then add one line to the body for each new fact, and say nothing the memory does not say.", "- If the memory contradicts a line of the lesson, answer none.", "- If no listed lesson states the memory's rule, or the memory adds nothing the lesson lacks, answer none.", "", 'Return ONLY valid JSON, no prose: {"reason": "...", "new_facts": ["..."], "decision": "update" | "none", "ref": "<the existing lesson ref>", "body": "<the whole body>"}');
53
+ return lines.join("\n");
54
+ }
55
+ /** The writer's chosen update, or `null` for `none` and for any reply that names no listed lesson, no new fact or no body. */
56
+ export function parseLessonUpdate(raw, candidates) {
57
+ const payload = parseEmbeddedJsonResponse(raw);
58
+ if (!payload || typeof payload !== "object" || Array.isArray(payload) || payload.decision !== "update")
59
+ return null;
60
+ const ref = typeof payload.ref === "string" ? payload.ref.trim() : "";
61
+ const body = typeof payload.body === "string" ? payload.body.trim() : "";
62
+ const candidate = candidates.find((c) => c.ref === ref);
63
+ const facts = Array.isArray(payload.new_facts)
64
+ ? payload.new_facts.filter((f) => typeof f === "string" && f.trim() !== "")
65
+ : [];
66
+ if (!candidate || !body || facts.length === 0)
67
+ return null;
68
+ return { candidate, body, reason: typeof payload.reason === "string" ? payload.reason.trim() : "" };
69
+ }
70
+ const normalizeLine = (line) => line.trim().replace(/\s+/g, " ").toLowerCase();
71
+ const bodyLines = (body) => body
72
+ .split("\n")
73
+ .map(normalizeLine)
74
+ .filter((line) => line !== "");
75
+ /**
76
+ * What an update does to a lesson's body: the existing lines it no longer holds (compared by words, in any case
77
+ * and spacing, anywhere in the new body) and the lines it adds.
78
+ */
79
+ export function diffLessonBody(existingBody, mergedBody) {
80
+ const existing = bodyLines(existingBody);
81
+ const merged = bodyLines(mergedBody);
82
+ const kept = new Set(merged);
83
+ const had = new Set(existing);
84
+ return {
85
+ dropped: existing.filter((line) => !kept.has(line)),
86
+ added: mergedBody
87
+ .split("\n")
88
+ .filter((line) => line.trim() !== "" && !had.has(normalizeLine(line)))
89
+ .map((line) => line.trim()),
90
+ };
91
+ }
@@ -3,8 +3,7 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  /**
5
5
  * Distill guards: the related lessons, knowledge notes and skills shown to the
6
- * writer so it does not repeat or overwrite them (CLS context), and a cheap check
7
- * that a proposal does not contradict the memories it came from.
6
+ * writer so it does not repeat or overwrite them (CLS context).
8
7
  */
9
8
  export const DEFAULT_CLS_ADJACENT_COUNT = 3;
10
9
  /** The CLS prompt section (each entry capped at 600 chars); empty when disabled (on unless `enabled: false`) or nothing is related. */
@@ -22,44 +21,3 @@ export function buildClsContext(adjacentItems, config) {
22
21
  lines.push(`### ${item.ref}`, item.content.trim().slice(0, 600), "");
23
22
  return lines.join("\n");
24
23
  }
25
- /**
26
- * Flag a proposal whose "always/must X" (or "never/must not X") claim meets
27
- * the opposite claim about X in a source. Deliberately conservative: a flag
28
- * only costs a human review, while a model call per proposal is expensive.
29
- */
30
- export function checkDistillFidelity(proposalBody, sourceBodies, config) {
31
- if (!config.enabled || sourceBodies.length === 0)
32
- return { contradictionDetected: false };
33
- const strongClaims = extractStrongClaims(proposalBody.toLowerCase());
34
- for (const sourceBody of sourceBodies) {
35
- const sourceLow = sourceBody.toLowerCase();
36
- for (const { polarity, term } of strongClaims) {
37
- if (hasStrongClaim(sourceLow, term, polarity === "positive" ? "negative" : "positive")) {
38
- return {
39
- contradictionDetected: true,
40
- reason: `Proposal makes a ${polarity} strong claim about "${term}" that conflicts with an opposing claim in a cited source. Route to human review.`,
41
- };
42
- }
43
- }
44
- }
45
- return { contradictionDetected: false };
46
- }
47
- const CLAIM_PATTERNS = [
48
- { polarity: "positive", re: /\b(?:always|must)\s+(\w+)/g },
49
- { polarity: "negative", re: /\b(?:never|must\s+not|should\s+not)\s+(\w+)/g },
50
- ];
51
- function extractStrongClaims(text) {
52
- const claims = [];
53
- for (const { polarity, re } of CLAIM_PATTERNS) {
54
- for (const m of text.matchAll(re)) {
55
- const term = m[1];
56
- if (term && term.length > 2)
57
- claims.push({ polarity, term });
58
- }
59
- }
60
- return claims;
61
- }
62
- function hasStrongClaim(text, term, polarity) {
63
- const marker = polarity === "positive" ? /\b(?:always|must)\s/ : /\b(?:never|must\s+not|should\s+not)\s/;
64
- return marker.test(text) && text.includes(term);
65
- }
@@ -40,15 +40,15 @@ import { getAllEntries } from "../../storage/repositories/index-entries-reposito
40
40
  import { listProposals } from "../proposal/repository.js";
41
41
  import { detectDoubleFrontmatter, isValidDescription } from "../proposal/validators/proposal-quality-validators.js";
42
42
  import { akmSearch } from "../read/search.js";
43
- import { stripFrontmatterBody } from "./content-hash.js";
44
43
  import { autoRepairLessonFrontmatter, autoSwapDescriptionWhenToUse, collectLessonQualityFindings, repairLessonDescriptionTruncation, } from "./distill/content-repair.js";
45
- import { buildClsContext, checkDistillFidelity, DEFAULT_CLS_ADJACENT_COUNT } from "./distill-guards.js";
44
+ import { buildLessonUpdatePrompt, DISTILL_LESSON_UPDATE_JSON_SCHEMA, diffLessonBody, parseLessonUpdate, } from "./distill/lesson-update.js";
45
+ import { buildClsContext, DEFAULT_CLS_ADJACENT_COUNT } from "./distill-guards.js";
46
46
  import { assessMemoryKnowledgePromotionCandidate, deriveKnowledgeRef } from "./distill-promotion-policy.js";
47
47
  import { buildRefVocabulary, scoreEncodingSalience } from "./encoding-salience.js";
48
48
  import { resolveImproveStrategy, resolveProcessEnabled } from "./improve-strategies.js";
49
49
  import { recordLedgerAttempt } from "./ledger.js";
50
50
  import { computeSalience, upsertAssetSalience } from "./salience.js";
51
- import { callStage, mintProposal, noticeSet, rejectedProposalContext, resolveQualityGateJudge, runLessonQualityJudge, stageRunner, } from "./stage.js";
51
+ import { callStage, LESSON_REJECT_MAX_SCORE, mintProposal, noticeSet, rejectedProposalContext, resolveQualityGateJudge, runLessonQualityJudge, stageRunner, } from "./stage.js";
52
52
  /**
53
53
  * Input types distill structurally refuses: a lesson is the distilled form
54
54
  * (distilling one would mint `lessons/lesson-…-lesson`), and env/secret bytes
@@ -361,7 +361,6 @@ export async function akmDistill(options) {
361
361
  eligMeta,
362
362
  asset,
363
363
  vocabulary: loadRefVocabulary(),
364
- outcomeWeightEnabled: config.improve?.salience?.outcomeWeightEnabled !== false,
365
364
  related: options.fetchRelatedFn ?? fetchRelatedAssets,
366
365
  lookup,
367
366
  };
@@ -436,6 +435,12 @@ async function distill(run, targetKind, kind, outputRef, feedbackEvents) {
436
435
  }
437
436
  const none = answeredNone(call.raw);
438
437
  if (none) {
438
+ // The writer may have answered NONE because a lesson already states the rule (the related list says to).
439
+ if (kind === "lesson") {
440
+ const update = await proposeLessonUpdate(run, feedbackLines(feedback), none.reason);
441
+ if (update)
442
+ return update;
443
+ }
439
444
  return skipDistill(run, outputRef, kind, "nothing_reusable", `The writer found no lesson in ${run.inputRef}${none.reason ? `: ${none.reason}` : "."}`);
440
445
  }
441
446
  const assembled = assembleDistilledContent(run, call.raw, kind, outputRef);
@@ -537,6 +542,13 @@ async function judgeAndQueue(run, out) {
537
542
  onNotices: run.notices.add,
538
543
  });
539
544
  if (!verdict.pass) {
545
+ if (out.kind === "lesson" &&
546
+ !out.promotion &&
547
+ (verdict.criteria?.nonRedundancy ?? 5) <= LESSON_REJECT_MAX_SCORE) {
548
+ const update = await proposeLessonUpdate(run, out.feedback ?? [], verdict.reason, related);
549
+ if (update)
550
+ return update;
551
+ }
540
552
  return rejectDistilled(run, out.ref, content, verdict.score, verdict.reason, {
541
553
  ...(verdict.reviewNeeded ? { reviewNeeded: true } : {}),
542
554
  ...(verdict.criteria ? { criteria: verdict.criteria } : {}),
@@ -553,19 +565,6 @@ async function judgeAndQueue(run, out) {
553
565
  frontmatter = data;
554
566
  }
555
567
  else {
556
- // Optional check against the cited source; a contradiction goes to a human.
557
- const fidelity = getImproveProcessConfig("distill", run.profile)?.fidelityCheck ?? {};
558
- if (fidelity.enabled && out.source) {
559
- try {
560
- const verdict = checkDistillFidelity(stripFrontmatterBody(content), [stripFrontmatterBody(out.source)], fidelity);
561
- if (verdict.contradictionDetected) {
562
- return rejectDistilled(run, out.ref, content, 2.0, verdict.reason ?? "Proposal may contradict cited source memories.", { reviewNeeded: true, fidelityContradiction: true });
563
- }
564
- }
565
- catch {
566
- // The fidelity check is supplemental.
567
- }
568
- }
569
568
  // Canonical provenance goes into the content promotion writes.
570
569
  const parsed = parseFrontmatter(content);
571
570
  const xrefs = Array.isArray(parsed.data.xrefs) ? parsed.data.xrefs.map(String) : [];
@@ -622,6 +621,129 @@ async function judgeAndQueue(run, out) {
622
621
  ...swapped,
623
622
  };
624
623
  }
624
+ /**
625
+ * A memory whose lesson repeats a lesson the library holds extends that lesson instead (#1090): the writer returns
626
+ * the lesson's body with the memory's new lines added, and the update is proposed on the lesson's own ref, for a
627
+ * person to review (never staged for the drain). `finding` is what the writer (its NONE reason) or the judge said
628
+ * about the repeat: the related lessons it names are the candidates, or the only related lesson when it names none,
629
+ * so a lesson that merely sits among several near the memory costs no call. Nothing is proposed, and `undefined`
630
+ * returned, when no lesson qualifies, the writer finds the memory adds
631
+ * nothing or contradicts the lesson, the body drops or rewords a line, the judge does not pass the extended lesson,
632
+ * or the lesson changed meanwhile. Needs the quality gate: the judge is what holds the added lines to the memory.
633
+ */
634
+ async function proposeLessonUpdate(run, feedback, finding, related) {
635
+ const memory = run.asset.content ? parseFrontmatter(run.asset.content).content.trim() : "";
636
+ const said = finding.toLowerCase();
637
+ if (!run.runner || !qualityGateEnabled(run) || !memory)
638
+ return undefined;
639
+ const lessons = [];
640
+ for (const asset of related ?? (await run.related(memory.slice(0, 500), RELATED_COUNT))) {
641
+ const parsed = parseRefInput(asset.ref);
642
+ if (parsed.type !== "lesson" || !asset.path || !isWithin(asset.path, run.stash))
643
+ continue;
644
+ try {
645
+ const content = fs.readFileSync(asset.path, "utf8");
646
+ const ref = conceptIdFromTypeName("lesson", parsed.name);
647
+ lessons.push({ ref, path: asset.path, content, body: parseFrontmatter(content).content.trim() });
648
+ }
649
+ catch {
650
+ // A lesson that cannot be read cannot be extended.
651
+ }
652
+ }
653
+ const named = lessons.filter((l) => said.includes(l.ref.toLowerCase()) || said.includes(l.ref.split("/").slice(1).join("/").toLowerCase()));
654
+ const candidates = named.length > 0 ? named : lessons.length === 1 ? lessons : [];
655
+ if (candidates.length === 0)
656
+ return undefined;
657
+ const call = await callStage({
658
+ feature: "distill",
659
+ runner: run.runner,
660
+ system: "Return only valid JSON. No prose.",
661
+ prompt: buildLessonUpdatePrompt(memory, feedback, candidates),
662
+ gate: { config: run.config, enabled: true },
663
+ request: {
664
+ ...(run.options.chat === undefined
665
+ ? { responseSchema: DISTILL_LESSON_UPDATE_JSON_SCHEMA }
666
+ : { chat: run.options.chat }),
667
+ ...(run.options.signal ? { signal: run.options.signal } : {}),
668
+ },
669
+ onNotices: run.notices.add,
670
+ });
671
+ const update = call.ok ? parseLessonUpdate(call.raw, candidates) : null;
672
+ if (!update)
673
+ return undefined;
674
+ const target = update.candidate;
675
+ const { dropped, added } = diffLessonBody(target.body, update.body);
676
+ if (dropped.length > 0) {
677
+ warnVerbose(`[akm] distill update of ${target.ref} dropped or reworded ${dropped.length} line(s); not proposed`);
678
+ return undefined;
679
+ }
680
+ if (added.length === 0)
681
+ return undefined;
682
+ // An edit is judged as the lesson it makes: the existing lesson with the lines added, as one lesson (reusable,
683
+ // grounded in the memory or the lesson), whose added lines must each say something the lesson did not (one call).
684
+ const extended = assembleAsset(parseFrontmatter(target.content).data, update.body);
685
+ const verdict = await runLessonQualityJudge(run.config, extended, memory, run.options.chat, {
686
+ extending: target.body,
687
+ ...(feedback.length > 0 ? { feedback } : {}),
688
+ ...((run.judgeRunner ?? run.runner) ? { llmRunner: run.judgeRunner ?? run.runner } : {}),
689
+ ...(run.options.signal ? { signal: run.options.signal } : {}),
690
+ onNotices: run.notices.add,
691
+ });
692
+ if (!verdict.pass) {
693
+ warnVerbose(`[akm] distill update of ${target.ref} not proposed: ${verdict.reason}`);
694
+ return undefined;
695
+ }
696
+ // The proposal's before-hash is taken at mint: a lesson edited since it was read is not what the writer extended.
697
+ let proposal;
698
+ try {
699
+ if (fs.readFileSync(target.path, "utf8") !== target.content)
700
+ return undefined;
701
+ const existing = parseFrontmatter(target.content);
702
+ const xrefs = Array.isArray(existing.data.xrefs) ? existing.data.xrefs.map(String) : [];
703
+ const frontmatter = { ...existing.data, xrefs: [...new Set([...xrefs, run.inputRef])] };
704
+ proposal = mintProposal(run.stash, run.options.ctx, {
705
+ ref: target.ref,
706
+ source: "distill",
707
+ ...(run.options.sourceRun !== undefined ? { sourceRun: run.options.sourceRun } : {}),
708
+ payload: { content: assembleAsset(frontmatter, update.body), frontmatter },
709
+ ...(verdict.score > 0 ? { confidence: verdict.score / 5 } : {}),
710
+ ...(run.options.eligibilitySource ? { eligibilitySource: run.options.eligibilitySource } : {}),
711
+ attemptedRefs: [run.ledgerRef],
712
+ }, {
713
+ review: {
714
+ reason: "distill-update",
715
+ gate: "quality-gate",
716
+ ...(verdict.criteria ? { scores: verdict.criteria } : {}),
717
+ judgeReason: verdict.reason,
718
+ },
719
+ });
720
+ }
721
+ catch (error) {
722
+ warnVerbose(`[akm] distill update of ${target.ref} not proposed: ${error instanceof Error ? error.message : error}`);
723
+ return undefined;
724
+ }
725
+ emitDistill(run, {
726
+ outcome: "queued",
727
+ proposalRef: target.ref,
728
+ proposalKind: "lesson",
729
+ proposalId: proposal.id,
730
+ updatesExisting: true,
731
+ ...(run.options.sourceRun !== undefined ? { sourceRun: run.options.sourceRun } : {}),
732
+ ...exclusionMeta(run, false),
733
+ });
734
+ return {
735
+ schemaVersion: 1,
736
+ ok: true,
737
+ outcome: "queued",
738
+ inputRef: run.inputRef,
739
+ proposalRef: target.ref,
740
+ proposalKind: "lesson",
741
+ proposalId: proposal.id,
742
+ proposal,
743
+ updatesExisting: true,
744
+ ...exclusionMeta(run, true),
745
+ };
746
+ }
625
747
  function rejectDistilled(run, proposalRef, content, score, reason, meta) {
626
748
  return writeQualityRejection({
627
749
  stash: run.stash,
@@ -863,7 +985,6 @@ function stampInputSalience(run) {
863
985
  type,
864
986
  retrievalFreq: 0,
865
987
  encodingSalience: scored.score,
866
- outcomeWeightEnabled: run.outcomeWeightEnabled,
867
988
  })));
868
989
  }
869
990
  catch {
@@ -887,7 +1008,6 @@ function persistOutputEncodingSalience(run, ref, body) {
887
1008
  type,
888
1009
  retrievalFreq: 0,
889
1010
  encodingSalience: scored.score,
890
- outcomeWeightEnabled: run.outcomeWeightEnabled,
891
1011
  })));
892
1012
  }
893
1013
  catch {
@@ -974,7 +1094,7 @@ async function fetchRelatedAssets(query, n) {
974
1094
  catch {
975
1095
  // best-effort
976
1096
  }
977
- return { ref: h.ref, content };
1097
+ return { ref: h.ref, content, ...(h.path ? { path: h.path } : {}) };
978
1098
  });
979
1099
  }
980
1100
  catch {
@@ -26,7 +26,7 @@ import { akmExtract, resolveStandaloneExtractPlan } from "./extract.js";
26
26
  export const extractCommand = defineJsonCommand({
27
27
  meta: {
28
28
  name: "extract",
29
- description: "Experimental: extract durable insights from native session files (claude, codex, opencode) and queue them as proposals.",
29
+ description: "Extract durable insights from native session files (claude, codex, opencode) and queue them as proposals.",
30
30
  },
31
31
  args: {
32
32
  type: {