akm-cli 0.9.0-rc.8 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (450) hide show
  1. package/CHANGELOG.md +1063 -44
  2. package/README.md +51 -25
  3. package/SECURITY.md +14 -1
  4. package/STABILITY.md +497 -0
  5. package/dist/akm +148 -35
  6. package/dist/{akm-migrate-storage → akm-migrate} +6 -9
  7. package/dist/assets/hints/cli-hints-full.md +223 -95
  8. package/dist/assets/hints/cli-hints-short.md +85 -22
  9. package/dist/assets/improve-strategies/default.json +1 -1
  10. package/dist/assets/improve-strategies/reflect-distill.json +1 -1
  11. package/dist/assets/prompts/memory-infer-user.md +2 -3
  12. package/dist/assets/stash-skeleton/README.md +6 -5
  13. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +2 -0
  14. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +2 -0
  15. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +2 -0
  16. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +2 -0
  17. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +2 -0
  18. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +2 -0
  19. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +2 -0
  20. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +2 -0
  21. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +2 -0
  22. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +2 -0
  23. package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -0
  24. package/dist/assets/stash-skeleton/facts/conventions/organization.md +20 -9
  25. package/dist/assets/tasks/core/extract.yml +1 -1
  26. package/dist/assets/tasks/core/version-check.yml +1 -1
  27. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
  28. package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
  29. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
  30. package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
  31. package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
  32. package/dist/assets/templates/html/health.html +1 -3
  33. package/dist/assets/workflows/workflow-template.md +32 -15
  34. package/dist/cli/invocation.js +40 -15
  35. package/dist/cli/parse-args.js +0 -22
  36. package/dist/cli/retired-commands.js +121 -0
  37. package/dist/cli/shared.js +154 -22
  38. package/dist/cli/unknown-flags.js +236 -0
  39. package/dist/cli-node.mjs +2 -1
  40. package/dist/cli.js +696 -258
  41. package/dist/commands/agent/agent-dispatch.js +14 -3
  42. package/dist/commands/agent/contribute-cli.js +73 -88
  43. package/dist/commands/completions.js +79 -22
  44. package/dist/commands/config-cli.js +17 -150
  45. package/dist/commands/env/env-cli.js +59 -143
  46. package/dist/commands/env/env.js +12 -163
  47. package/dist/commands/env/marker-path.js +6 -0
  48. package/dist/commands/env/secret-cli.js +36 -66
  49. package/dist/commands/env/secret.js +24 -57
  50. package/dist/commands/feedback-cli.js +141 -87
  51. package/dist/commands/health/accept-rate.js +58 -0
  52. package/dist/commands/health/advisories.js +3 -4
  53. package/dist/commands/health/checks.js +85 -23
  54. package/dist/commands/health/html-report.js +7 -10
  55. package/dist/commands/health/improve-metrics.js +25 -83
  56. package/dist/commands/health/md-report.js +5 -9
  57. package/dist/commands/health/metrics.js +62 -20
  58. package/dist/commands/health/renderers.js +47 -0
  59. package/dist/commands/health/report-view-model.js +4 -5
  60. package/dist/commands/health/stash-exposure.js +1 -1
  61. package/dist/commands/health/surfaces.js +3 -48
  62. package/dist/commands/health/task-runs.js +3 -67
  63. package/dist/commands/health/types-improve.js +7 -0
  64. package/dist/commands/health.js +99 -28
  65. package/dist/commands/improve/anti-collapse.js +2 -2
  66. package/dist/commands/improve/autonomy-gate.js +68 -0
  67. package/dist/commands/improve/collapse-detector.js +41 -40
  68. package/dist/commands/improve/consolidate/eligibility.js +1 -23
  69. package/dist/commands/improve/consolidate/merge.js +4 -0
  70. package/dist/commands/improve/consolidate.js +140 -1000
  71. package/dist/commands/improve/distill/promote-memory.js +12 -12
  72. package/dist/commands/improve/distill/quality-gate.js +6 -6
  73. package/dist/commands/improve/distill.js +58 -69
  74. package/dist/commands/improve/eligibility.js +105 -57
  75. package/dist/commands/improve/extract-cli.js +14 -133
  76. package/dist/commands/improve/improve-cli.js +98 -114
  77. package/dist/commands/improve/improve-result-file.js +1 -28
  78. package/dist/commands/improve/improve-strategies.js +8 -5
  79. package/dist/commands/improve/improve.js +128 -91
  80. package/dist/commands/improve/loop-stages.js +182 -20
  81. package/dist/commands/improve/memory/derived-ref.js +45 -43
  82. package/dist/commands/improve/memory/memory-belief.js +1 -1
  83. package/dist/commands/improve/memory/memory-contradiction-detect.js +4 -12
  84. package/dist/commands/improve/memory/memory-improve.js +6 -5
  85. package/dist/commands/improve/outcome-loop.js +22 -65
  86. package/dist/commands/improve/preparation.js +114 -123
  87. package/dist/commands/improve/proactive-maintenance.js +2 -5
  88. package/dist/commands/improve/reflect.js +56 -160
  89. package/dist/commands/improve/salience.js +11 -122
  90. package/dist/commands/improve/source-identity.js +10 -38
  91. package/dist/commands/lint/base-linter.js +20 -124
  92. package/dist/commands/lint/env-key-rules.js +31 -47
  93. package/dist/commands/lint/index.js +249 -43
  94. package/dist/commands/{events.js → log.js} +33 -38
  95. package/dist/commands/migrate-cli.js +92 -12
  96. package/dist/commands/migration-tool.js +46 -0
  97. package/dist/commands/observability-cli.js +70 -209
  98. package/dist/commands/proposal/drain.js +101 -29
  99. package/dist/commands/proposal/proposal-cli.js +76 -48
  100. package/dist/commands/proposal/proposal.js +54 -18
  101. package/dist/commands/proposal/propose-cli.js +88 -0
  102. package/dist/commands/proposal/propose.js +23 -15
  103. package/dist/commands/proposal/repository.js +701 -278
  104. package/dist/commands/proposal/validators/proposal-quality-validators.js +2 -8
  105. package/dist/commands/proposal/validators/proposal-validators.js +55 -7
  106. package/dist/commands/proposal/validators/proposals.js +4 -7
  107. package/dist/commands/read/curate.js +34 -53
  108. package/dist/commands/read/knowledge.js +150 -95
  109. package/dist/commands/read/registry-search.js +2 -2
  110. package/dist/commands/read/remember-cli.js +42 -15
  111. package/dist/commands/read/search-cli.js +180 -78
  112. package/dist/commands/read/search.js +58 -43
  113. package/dist/commands/read/show.js +197 -141
  114. package/dist/commands/registry-cli.js +12 -51
  115. package/dist/commands/remember.js +14 -57
  116. package/dist/commands/sources/add-cli.js +100 -31
  117. package/dist/commands/sources/bundle-cli.js +166 -0
  118. package/dist/commands/sources/bundle-config-ops.js +7 -2
  119. package/dist/commands/sources/info.js +18 -5
  120. package/dist/commands/sources/init.js +12 -12
  121. package/dist/commands/sources/installed-stashes.js +382 -98
  122. package/dist/commands/sources/schema-repair.js +3 -2
  123. package/dist/commands/sources/self-update.js +131 -38
  124. package/dist/commands/sources/source-add.js +72 -17
  125. package/dist/commands/sources/source-clone.js +129 -45
  126. package/dist/commands/sources/source-manage.js +43 -23
  127. package/dist/commands/sources/sources-cli.js +57 -208
  128. package/dist/commands/sources/stash-cli.js +46 -53
  129. package/dist/commands/tasks/tasks-cli.js +91 -97
  130. package/dist/commands/tasks/tasks.js +276 -421
  131. package/dist/commands/workflow-cli.js +175 -450
  132. package/dist/core/adapter/adapters/akm-adapter.js +47 -28
  133. package/dist/core/adapter/adapters/akm-lint.js +42 -27
  134. package/dist/core/adapter/adapters/akm-metadata.js +15 -44
  135. package/dist/core/adapter/adapters/akm-task-adapter.js +15 -13
  136. package/dist/core/adapter/adapters/akm-workflow-adapter.js +55 -71
  137. package/dist/core/adapter/adapters/dotenv-adapter.js +1 -1
  138. package/dist/core/adapter/adapters/generic-files-adapter.js +2 -0
  139. package/dist/core/adapter/adapters/index.js +6 -6
  140. package/dist/core/adapter/adapters/llm-wiki-adapter.js +14 -8
  141. package/dist/core/adapter/adapters/okf-adapter.js +187 -19
  142. package/dist/core/adapter/adapters/shared.js +3 -19
  143. package/dist/core/adapter/adapters/tool-dir-shared.js +8 -3
  144. package/dist/core/adapter/adapters/website-snapshot-adapter.js +1 -0
  145. package/dist/core/adapter/detect-adapter.js +17 -0
  146. package/dist/core/adapter/recognize-match.js +6 -4
  147. package/dist/core/adapter/validate-context.js +214 -0
  148. package/dist/core/asset/akm-markdown.js +63 -0
  149. package/dist/core/asset/asset-placement.js +20 -6
  150. package/dist/core/asset/asset-ref.js +11 -9
  151. package/dist/core/asset/frontmatter-lint.js +30 -0
  152. package/dist/core/asset/frontmatter.js +37 -9
  153. package/dist/core/asset/markdown.js +40 -51
  154. package/dist/core/asset/resolve-ref.js +89 -18
  155. package/dist/core/asset/stash-meta.js +1 -1
  156. package/dist/core/bundle-id.js +51 -0
  157. package/dist/core/common.js +152 -38
  158. package/dist/core/config/config-io.js +12 -1
  159. package/dist/core/config/config-schema.js +35 -8
  160. package/dist/core/config/config-sources.js +55 -11
  161. package/dist/core/config/config-walker.js +25 -9
  162. package/dist/core/config/config.js +9 -48
  163. package/dist/core/config/experimental.js +21 -0
  164. package/dist/core/config/schema/embedding.js +5 -1
  165. package/dist/core/config/schema/experimental.js +30 -0
  166. package/dist/core/config/schema/improve-processes.js +0 -6
  167. package/dist/core/config/schema/improve.js +21 -3
  168. package/dist/core/config/schema/index-config.js +8 -15
  169. package/dist/core/config/schema/output.js +4 -1
  170. package/dist/core/config/schema/setup.js +9 -18
  171. package/dist/core/config/schema/sources-bundles.js +49 -33
  172. package/dist/core/config/schema/workflow.js +3 -3
  173. package/dist/core/env-secret-ref.js +76 -46
  174. package/dist/core/errors.js +18 -12
  175. package/dist/core/events.js +46 -128
  176. package/dist/core/file-change.js +6 -5
  177. package/dist/core/fs-txn.js +83 -7
  178. package/dist/core/git-message.js +2 -2
  179. package/dist/core/improve-result.js +1 -100
  180. package/dist/core/lesson-lint.js +1 -17
  181. package/dist/core/logs-db.js +2 -1
  182. package/dist/core/migration-operation.js +16 -0
  183. package/dist/core/mutation-target.js +78 -0
  184. package/dist/core/parse.js +4 -1
  185. package/dist/core/paths.js +17 -20
  186. package/dist/core/recognition-util.js +12 -14
  187. package/dist/core/redaction.js +34 -0
  188. package/dist/core/standards/resolve-standards-context.js +2 -14
  189. package/dist/core/standards/resolve-stash-standards.js +2 -2
  190. package/dist/core/standards/resolve-type-conventions.js +2 -2
  191. package/dist/core/state/migrations.js +41 -18
  192. package/dist/core/state-db.js +5 -14
  193. package/dist/core/structured.js +1 -1
  194. package/dist/core/subprocess.js +6 -4
  195. package/dist/core/text-truncation.js +9 -5
  196. package/dist/core/type-presentation.js +3 -3
  197. package/dist/core/warn.js +0 -3
  198. package/dist/core/write-source.js +771 -95
  199. package/dist/indexer/bundle-identity-guard.js +3 -2
  200. package/dist/indexer/db/graph-db.js +0 -24
  201. package/dist/indexer/ensure-index.js +1 -0
  202. package/dist/indexer/graph/graph-boost.js +9 -34
  203. package/dist/indexer/graph/graph-extraction.js +8 -5
  204. package/dist/indexer/index-writer-lock.js +53 -17
  205. package/dist/indexer/index-written-assets.js +16 -22
  206. package/dist/indexer/indexer.js +497 -239
  207. package/dist/indexer/installations.js +14 -96
  208. package/dist/indexer/passes/dir-staleness.js +16 -9
  209. package/dist/indexer/passes/memory-inference.js +11 -9
  210. package/dist/indexer/passes/metadata.js +113 -47
  211. package/dist/indexer/scan/doc-to-entry.js +38 -1
  212. package/dist/indexer/scan/drain-dir.js +13 -23
  213. package/dist/indexer/search/db-search.js +99 -54
  214. package/dist/indexer/search/fts-query.js +47 -24
  215. package/dist/indexer/search/ranking-contributors.js +42 -20
  216. package/dist/indexer/search/ranking.js +18 -99
  217. package/dist/indexer/search/search-fields.js +7 -2
  218. package/dist/indexer/search/search-source.js +82 -93
  219. package/dist/indexer/usage/usage-events.js +0 -89
  220. package/dist/indexer/walk/file-context.js +2 -1
  221. package/dist/indexer/walk/matchers.js +30 -43
  222. package/dist/indexer/walk/path-resolver.js +7 -2
  223. package/dist/indexer/walk/walker.js +38 -12
  224. package/dist/integrations/agent/builders.js +0 -6
  225. package/dist/integrations/agent/config.js +2 -2
  226. package/dist/integrations/agent/detect.js +49 -19
  227. package/dist/integrations/agent/engine-fallback.js +76 -0
  228. package/dist/integrations/agent/profiles.js +14 -0
  229. package/dist/integrations/agent/prompts.js +12 -8
  230. package/dist/integrations/agent/runner-dispatch.js +4 -2
  231. package/dist/integrations/agent/runner.js +0 -1
  232. package/dist/integrations/agent/spawn.js +5 -6
  233. package/dist/integrations/github.js +1 -1
  234. package/dist/integrations/harnesses/aider/agent-builder.js +6 -4
  235. package/dist/integrations/harnesses/amazonq/agent-builder.js +7 -4
  236. package/dist/integrations/harnesses/claude/session-log.js +0 -10
  237. package/dist/integrations/harnesses/codex/agent-builder.js +5 -2
  238. package/dist/integrations/harnesses/copilot/agent-builder.js +5 -3
  239. package/dist/integrations/harnesses/gemini/agent-builder.js +5 -3
  240. package/dist/integrations/harnesses/index.js +3 -7
  241. package/dist/integrations/harnesses/opencode/agent-builder.js +21 -2
  242. package/dist/integrations/harnesses/opencode/session-log.js +0 -15
  243. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +13 -4
  244. package/dist/integrations/harnesses/openhands/agent-builder.js +9 -6
  245. package/dist/integrations/harnesses/pi/agent-builder.js +6 -4
  246. package/dist/integrations/lockfile.js +101 -6
  247. package/dist/integrations/session-logs/index.js +3 -28
  248. package/dist/llm/client.js +136 -100
  249. package/dist/llm/embedders/remote.js +13 -5
  250. package/dist/llm/feature-gate.js +4 -12
  251. package/dist/llm/graph-extract.js +5 -11
  252. package/dist/llm/memory-infer.js +144 -1
  253. package/dist/llm/metadata-enhance.js +5 -7
  254. package/dist/llm/structured-call.js +1 -1
  255. package/dist/llm/usage-persist.js +26 -5
  256. package/dist/llm/usage-telemetry.js +25 -2
  257. package/dist/output/cli-hints.js +1 -2
  258. package/dist/output/context.js +22 -7
  259. package/dist/output/format-exempt.js +80 -0
  260. package/dist/output/generic-render.js +259 -0
  261. package/dist/output/render-registry.js +57 -0
  262. package/dist/output/renderers.js +14 -36
  263. package/dist/output/shapes/curate.js +10 -1
  264. package/dist/output/shapes/events.js +12 -7
  265. package/dist/output/shapes/helpers.js +56 -83
  266. package/dist/output/shapes/migrate.js +8 -0
  267. package/dist/output/shapes/passthrough.js +7 -41
  268. package/dist/output/shapes/proposal/producer.js +15 -7
  269. package/dist/output/shapes.js +2 -9
  270. package/dist/output/text/{init.js → bundle-create.js} +3 -1
  271. package/dist/output/text/bundle-show.js +7 -0
  272. package/dist/output/text/command-format.js +164 -96
  273. package/dist/output/text/env.js +1 -3
  274. package/dist/output/text/events.js +8 -7
  275. package/dist/output/text/health-format.js +103 -0
  276. package/dist/output/text/health.js +7 -0
  277. package/dist/output/text/helpers.js +10 -8
  278. package/dist/output/text/lint-format.js +43 -0
  279. package/dist/output/text/{save.js → lint.js} +2 -2
  280. package/dist/output/text/migrate.js +88 -0
  281. package/dist/output/text/proposal/producer.js +4 -2
  282. package/dist/output/text/proposal-format.js +44 -72
  283. package/dist/output/text/registry-commands.js +1 -2
  284. package/dist/output/text/show-directives.js +15 -7
  285. package/dist/output/text/status-list.js +32 -0
  286. package/dist/output/text/sync.js +5 -0
  287. package/dist/output/text/workflow-format.js +24 -203
  288. package/dist/output/text/workflow.js +1 -7
  289. package/dist/output/text.js +16 -17
  290. package/dist/registry/factory.js +4 -6
  291. package/dist/registry/origin-resolve.js +16 -27
  292. package/dist/registry/providers/skills-sh.js +3 -3
  293. package/dist/registry/providers/static-index.js +13 -23
  294. package/dist/registry/resolve.js +42 -7
  295. package/dist/registry/semver.js +34 -84
  296. package/dist/runtime.js +2 -23
  297. package/dist/scripts/akm-migrate-node.js +60290 -0
  298. package/dist/scripts/akm-migrate.js +59628 -0
  299. package/dist/setup/detect.js +42 -15
  300. package/dist/setup/registry-stash-loader.js +2 -2
  301. package/dist/setup/setup.js +236 -136
  302. package/dist/setup/steps/connection.js +7 -9
  303. package/dist/setup/steps/platforms.js +9 -9
  304. package/dist/setup/steps/semantic.js +15 -3
  305. package/dist/setup/steps/sources.js +12 -13
  306. package/dist/setup/steps/stashdir.js +2 -3
  307. package/dist/setup/steps/tasks.js +237 -120
  308. package/dist/sources/freshness.js +1 -1
  309. package/dist/sources/provider-factory.js +11 -17
  310. package/dist/sources/providers/filesystem.js +2 -3
  311. package/dist/sources/providers/git-install.js +278 -34
  312. package/dist/sources/providers/git-provider.js +25 -23
  313. package/dist/sources/providers/git-stash.js +395 -106
  314. package/dist/sources/providers/git.js +2 -2
  315. package/dist/sources/providers/npm.js +16 -19
  316. package/dist/sources/providers/provider-utils.js +7 -4
  317. package/dist/sources/providers/sync-from-ref.js +3 -9
  318. package/dist/sources/providers/website.js +6 -1
  319. package/dist/sources/resolve.js +6 -5
  320. package/dist/sources/snapshot-fetchers/bluesky.js +146 -0
  321. package/dist/sources/snapshot-fetchers/content-extract.js +566 -0
  322. package/dist/sources/snapshot-fetchers/fetcher-util.js +41 -0
  323. package/dist/sources/snapshot-fetchers/github.js +100 -0
  324. package/dist/sources/snapshot-fetchers/host-guard.js +291 -0
  325. package/dist/sources/snapshot-fetchers/registry.js +17 -1
  326. package/dist/sources/snapshot-fetchers/robots.js +348 -0
  327. package/dist/sources/snapshot-fetchers/rss.js +282 -0
  328. package/dist/sources/snapshot-fetchers/secret-seam.js +42 -0
  329. package/dist/sources/snapshot-fetchers/website-ingest.js +566 -268
  330. package/dist/sources/snapshot-fetchers/x.js +910 -0
  331. package/dist/storage/database.js +7 -0
  332. package/dist/storage/engines/sqlite-migrations.js +23 -111
  333. package/dist/storage/managed-db.js +2 -2
  334. package/dist/storage/repositories/canaries-repository.js +1 -1
  335. package/dist/storage/repositories/events-repository.js +27 -11
  336. package/dist/storage/repositories/improve-runs-repository.js +6 -12
  337. package/dist/storage/repositories/index-connection.js +17 -6
  338. package/dist/storage/repositories/index-entries-repository.js +151 -240
  339. package/dist/storage/repositories/index-entry-mapper.js +15 -11
  340. package/dist/storage/repositories/index-fts-repository.js +5 -2
  341. package/dist/storage/repositories/index-llm-cache-repository.js +0 -1
  342. package/dist/storage/repositories/index-meta-repository.js +2 -3
  343. package/dist/storage/repositories/index-schema.js +10 -25
  344. package/dist/storage/repositories/index-utility-repository.js +15 -28
  345. package/dist/storage/repositories/index-vec-repository.js +6 -1
  346. package/dist/storage/repositories/outcome-repository.js +119 -0
  347. package/dist/storage/repositories/proposals-repository.js +296 -59
  348. package/dist/storage/repositories/registry-cache.js +19 -0
  349. package/dist/storage/repositories/salience-repository.js +172 -0
  350. package/dist/storage/repositories/task-history-repository.js +15 -13
  351. package/dist/storage/repositories/workflow-runs-repository.js +52 -40
  352. package/dist/tasks/backends/cron.js +105 -15
  353. package/dist/tasks/backends/index.js +1 -1
  354. package/dist/tasks/backends/launchd.js +85 -38
  355. package/dist/tasks/backends/schtasks.js +135 -15
  356. package/dist/tasks/embedded.js +56 -40
  357. package/dist/tasks/parser.js +7 -157
  358. package/dist/tasks/resolve-akm-bin.js +137 -59
  359. package/dist/tasks/runner.js +79 -42
  360. package/dist/tasks/scheduler-invocation.js +220 -10
  361. package/dist/tasks/schema.js +24 -1
  362. package/dist/tasks/task-id.js +1 -3
  363. package/dist/tasks/validator.js +20 -6
  364. package/dist/workflows/authoring/authoring.js +94 -143
  365. package/dist/workflows/authoring/scope-key.js +1 -1
  366. package/dist/workflows/exec/frozen-judge.js +28 -2
  367. package/dist/workflows/exec/native-executor.js +77 -57
  368. package/dist/workflows/exec/param-secrets.js +9 -9
  369. package/dist/workflows/exec/run-workflow.js +133 -79
  370. package/dist/workflows/exec/step-work.js +219 -346
  371. package/dist/{migrate-storage-node.mjs → workflows/exec/unit-dispatch.js} +1 -5
  372. package/dist/workflows/ir/compile.js +141 -270
  373. package/dist/workflows/ir/freeze.js +40 -30
  374. package/dist/workflows/ir/params.js +135 -11
  375. package/dist/workflows/ir/plan-hash.js +1 -1
  376. package/dist/workflows/ir/schema.js +25 -26
  377. package/dist/workflows/parser.js +872 -307
  378. package/dist/workflows/program/expressions.js +20 -208
  379. package/dist/workflows/program/schema.js +7 -10
  380. package/dist/workflows/renderer.js +95 -68
  381. package/dist/workflows/resource-limits.js +2 -0
  382. package/dist/workflows/runtime/checkin.js +3 -3
  383. package/dist/workflows/runtime/plan-classifier.js +16 -75
  384. package/dist/workflows/runtime/runs.js +186 -127
  385. package/dist/workflows/runtime/unit-checkin.js +1 -1
  386. package/dist/workflows/runtime/unit-phases.js +2 -2
  387. package/dist/workflows/runtime/workflow-asset-loader.js +232 -83
  388. package/dist/workflows/schema.js +1 -11
  389. package/dist/workflows/validate-summary.js +30 -36
  390. package/dist/workflows/validator.js +21 -62
  391. package/docs/README.md +68 -0
  392. package/docs/migration/README.md +8 -0
  393. package/docs/migration/release-notes/0.7.0.md +11 -11
  394. package/docs/migration/release-notes/0.9.0.md +208 -27
  395. package/docs/migration/v0.7-to-v0.8.md +46 -47
  396. package/docs/migration/v0.8-to-v0.9.md +564 -208
  397. package/docs/migration/v0.9.0-troubleshooting.md +561 -0
  398. package/docs/reference/README.md +12 -0
  399. package/docs/reference/cli.md +2253 -0
  400. package/docs/reference/configuration.md +358 -0
  401. package/docs/reference/data-and-telemetry.md +105 -42
  402. package/docs/reference/workflows.md +647 -0
  403. package/package.json +22 -11
  404. package/schemas/akm-asset-envelope.json +93 -0
  405. package/schemas/akm-config.json +81 -128
  406. package/schemas/akm-workflow.json +74 -73
  407. package/dist/assets/tasks/core/backup.yml +0 -5
  408. package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
  409. package/dist/cli/config-migrate.js +0 -1806
  410. package/dist/cli/config-validate.js +0 -41
  411. package/dist/commands/backup-cli.js +0 -56
  412. package/dist/commands/bundle/bundle-cli.js +0 -68
  413. package/dist/commands/bundle/bundle.js +0 -219
  414. package/dist/commands/graph/graph-cli.js +0 -124
  415. package/dist/commands/graph/graph.js +0 -489
  416. package/dist/commands/improve/extract-watch.js +0 -140
  417. package/dist/commands/mv-cli.js +0 -1221
  418. package/dist/commands/sources/history.js +0 -201
  419. package/dist/commands/tasks/default-tasks.js +0 -186
  420. package/dist/core/migration-backup.js +0 -1234
  421. package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -95
  422. package/dist/llm/memory-infer-impl.js +0 -138
  423. package/dist/migrate/legacy/config-source-migration.js +0 -223
  424. package/dist/migrate/legacy/content-migration.js +0 -305
  425. package/dist/migrate/legacy/legacy-layout.js +0 -779
  426. package/dist/migrate/legacy/legacy-paths.js +0 -25
  427. package/dist/migrate/legacy/legacy-stash-json.js +0 -72
  428. package/dist/migrate/legacy/proposal-fs-import.js +0 -168
  429. package/dist/migrate/legacy/task-target-ref-migration.js +0 -272
  430. package/dist/migrate/legacy/three-db-cutover.js +0 -841
  431. package/dist/migrate/legacy/workflow-migrations-bodies.js +0 -52
  432. package/dist/migrate/legacy/workflow-migrations-frozen.js +0 -21
  433. package/dist/migrate/legacy-ref-grammar.js +0 -214
  434. package/dist/output/shapes/distill.js +0 -14
  435. package/dist/output/shapes/history.js +0 -11
  436. package/dist/output/text/distill.js +0 -6
  437. package/dist/output/text/enable-disable.js +0 -8
  438. package/dist/output/text/history.js +0 -6
  439. package/dist/registry/build-index.js +0 -382
  440. package/dist/schemas/akm-config.json +0 -4704
  441. package/dist/schemas/akm-task.json +0 -87
  442. package/dist/schemas/akm-workflow.json +0 -372
  443. package/dist/scripts/migrate-storage.js +0 -3816
  444. package/dist/workflows/authoring/workflow-program-template.yaml +0 -31
  445. package/dist/workflows/cli.js +0 -53
  446. package/dist/workflows/exec/brief.js +0 -481
  447. package/dist/workflows/exec/report.js +0 -1460
  448. package/dist/workflows/exec/watch.js +0 -116
  449. package/dist/workflows/program/parser.js +0 -813
  450. package/dist/workflows/program/project.js +0 -104
@@ -4,32 +4,18 @@
4
4
  import { createHash } from "node:crypto";
5
5
  import fs from "node:fs";
6
6
  import path from "node:path";
7
- import readline from "node:readline";
8
7
  import consolidateSystemPrompt from "../../assets/prompts/consolidate-system.md" with { type: "text" };
8
+ import { detectAdapterId } from "../../core/adapter/detect-adapter.js";
9
9
  import { assembleAssetFromString, serializeFrontmatter } from "../../core/asset/asset-serialize.js";
10
10
  import { parseFrontmatter } from "../../core/asset/frontmatter.js";
11
11
  import { conceptIdFromTypeName, displayRef, parseRefInput } from "../../core/asset/resolve-ref.js";
12
- import { timestampForFilename } from "../../core/common.js";
13
12
  import { getImproveProcessConfig, loadConfig } from "../../core/config/config.js";
14
- import { ConfigError } from "../../core/errors.js";
15
- import { advanceTxn, beginTxn, cleanupTxn, registerTxnKind, txnNamespaceDir, } from "../../core/fs-txn.js";
16
- // Note: appendEvent import removed (WS-3a: archive TTL machinery retired)
17
13
  import { parseEmbeddedJsonResponse } from "../../core/parse.js";
18
14
  import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
19
- import { detectTruncatedDescription } from "../../core/text-truncation.js";
20
- import { parseSinceToIsoLenient } from "../../core/time.js";
21
- import { isProposalSkipped, listProposals, proposalContent } from "../proposal/repository.js";
22
- import { hasSupersededStatus, MERGE_ABSOLUTE_FLOOR_CHARS, MERGE_SHRINK_RATIO_MIN, validateProposalFrontmatter, } from "../proposal/validators/proposal-quality-validators.js";
23
- import { checkGenerationGuard, checkLexicalDiversity, checkMergeInformationFloor, computeMergedGeneration, readAssetGeneration, } from "./anti-collapse.js";
24
- import { cacheHash, stripFrontmatterBody } from "./content-hash.js";
25
- import { writeContradictEdge } from "./memory/memory-belief.js";
26
- import { emitProposal } from "./proposal-envelope.js";
27
- import { createRunContext } from "./run-context.js";
28
- // Re-export the moved helpers so existing test imports continue to resolve.
29
- export { hasSupersededStatus, validateProposalFrontmatter };
30
15
  import { openStateDatabase } from "../../core/state-db.js";
31
- import { warn } from "../../core/warn.js";
32
- import { commitWriteTargetBoundary, deleteAssetFromSource, resolveWriteTarget, writeAssetToSource, } from "../../core/write-source.js";
16
+ import { parseSinceToIsoLenient } from "../../core/time.js";
17
+ import { warn, warnVerbose } from "../../core/warn.js";
18
+ import { resolveWriteTarget } from "../../core/write-source.js";
33
19
  import { getDefaultLlmConfig } from "../../integrations/agent/engine-resolution.js";
34
20
  import { materializeLlmRunnerConnection, resolveImproveProcessRunner } from "../../integrations/agent/runner.js";
35
21
  import { cosineSimilarity, embedBatch, resolveEmbeddingModelId } from "../../llm/embedder.js";
@@ -38,28 +24,27 @@ import { getBodyEmbeddings, upsertBodyEmbeddings } from "../../storage/repositor
38
24
  import { closeDatabase, openExistingDatabase } from "../../storage/repositories/index-connection.js";
39
25
  import { findEntryIdByRef, getAllEntries, getEntryById } from "../../storage/repositories/index-entries-repository.js";
40
26
  import { getNeighborsByEntryId } from "../../storage/repositories/index-vec-repository.js";
27
+ import { isProposalSkipped, listProposals, proposalContent } from "../proposal/repository.js";
28
+ import { hasSupersededStatus, validateProposalFrontmatter } from "../proposal/validators/proposal-quality-validators.js";
29
+ import { cacheHash } from "./content-hash.js";
41
30
  import { resolveImproveStrategy, resolveProcessEnabled } from "./improve-strategies.js";
31
+ import { emitProposal } from "./proposal-envelope.js";
32
+ import { createRunContext } from "./run-context.js";
42
33
  // Chunk sizing + per-chunk prompt assembly live in ./consolidate/chunking.
43
- // Imported for internal use by the orchestrator and re-exported for importers.
44
34
  import { buildChunkPrompt, computeSafeChunkSize, DEFAULT_CONTEXT_LENGTH_TOKENS } from "./consolidate/chunking.js";
45
- export { buildChunkPrompt, computeSafeChunkSize, DEFAULT_CONTEXT_LENGTH_TOKENS } from "./consolidate/chunking.js";
46
- // LLM-output sanitization (pure string/frontmatter transforms) lives in
47
- // ./consolidate/sanitize. Imported for internal use + re-exported for importers.
48
- import { normalizeUpdatedField, sanitizeMergedContent } from "./consolidate/sanitize.js";
49
- export { normalizeUpdatedField, sanitizeMergedContent, stripOuterCodeFence } from "./consolidate/sanitize.js";
50
- // Eligibility / safety predicates live in ./consolidate/eligibility. Imported
51
- // for internal guard use; the two public predicates are re-exported.
52
- import { consolidateGuardStatus, isConsolidationEligibleMemoryName, isHotCapturedMemory, } from "./consolidate/eligibility.js";
53
- export { isConsolidationEligibleMemoryName, isHotCapturedMemory } from "./consolidate/eligibility.js";
35
+ // Eligibility / safety predicates live in ./consolidate/eligibility.
36
+ import { isConsolidationEligibleMemoryName, isHotCapturedMemory } from "./consolidate/eligibility.js";
54
37
  // Plan parsing / merging (pure op-reconciliation algebra) lives in
55
- // ./consolidate/merge. Imported for internal use; mergePlans re-exported.
38
+ // ./consolidate/merge.
56
39
  import { isValidOp, mergePlans } from "./consolidate/merge.js";
57
- export { mergePlans } from "./consolidate/merge.js";
40
+ // LLM-output sanitization (pure string/frontmatter transforms) lives in
41
+ // ./consolidate/sanitize.
42
+ import { sanitizeMergedContent } from "./consolidate/sanitize.js";
58
43
  // ── Prompts ─────────────────────────────────────────────────────────────────
59
44
  const CONSOLIDATE_SYSTEM_PROMPT = consolidateSystemPrompt;
60
45
  /**
61
46
  * JSON Schema for structured consolidate plans (PR 1 of the asset-writers
62
- * decision — see knowledge:projects/akm/asset-writers-investigation/00-synthesis).
47
+ * decision — see knowledge/projects/akm/asset-writers-investigation/00-synthesis).
63
48
  * Mirrors the {ops[], warnings?[]} shape currently described in
64
49
  * CONSOLIDATE_SYSTEM_PROMPT. Providers with `supportsJsonSchema: true` enforce
65
50
  * the shape upstream so the chunk-level "invalid plan from AI — skipping"
@@ -90,6 +75,7 @@ export const CONSOLIDATE_PLAN_JSON_SCHEMA = {
90
75
  secondaries: {
91
76
  type: "array",
92
77
  minItems: 1,
78
+ maxItems: 1,
93
79
  items: { type: "string", minLength: 1 },
94
80
  },
95
81
  mergeStrategy: { type: "string", minLength: 1 },
@@ -142,7 +128,7 @@ export const CONSOLIDATE_PLAN_JSON_SCHEMA = {
142
128
  },
143
129
  },
144
130
  };
145
- async function clusterMemoriesBySimilarity(memories, config, stateDb) {
131
+ async function clusterMemoriesBySimilarity(memories, config, stateDb, signal) {
146
132
  const noTelemetry = { embedMs: 0, cacheHits: 0, cacheMisses: 0 };
147
133
  if (memories.length < 3 || !config.embedding)
148
134
  return { ordered: memories, embedTelemetry: noTelemetry };
@@ -193,7 +179,7 @@ async function clusterMemoriesBySimilarity(memories, config, stateDb) {
193
179
  if (missTexts.length > 0) {
194
180
  const embedStart = Date.now();
195
181
  try {
196
- missVecs = await embedBatch(missTexts, config.embedding);
182
+ missVecs = await embedBatch(missTexts, config.embedding, signal);
197
183
  }
198
184
  catch {
199
185
  // Fail open: embedding failures degrade gracefully to original order.
@@ -300,157 +286,19 @@ function loadPendingConsolidateProposalHashes(stashDir) {
300
286
  }
301
287
  return hashes;
302
288
  }
303
- const CONSOLIDATE_TXN_KIND = "consolidate";
304
- // Checklist semantics: never auto-recovered. commitPhase is the initial phase
305
- // so the engine never classifies these as rollback-able; the registered
306
- // handlers below abort loudly if generic recovery ever reaches one.
307
- const CONSOLIDATE_TXN_PHASES = ["applying", "committed"];
308
- function consolidateBackupDir(txn) {
309
- return path.join(txn.dir, "backup");
310
- }
311
- /** Open the checklist journal for this run (durably, before any mutation). */
312
- function beginConsolidateTxn(stashDir, ops) {
313
- return beginTxn({
314
- kind: CONSOLIDATE_TXN_KIND,
315
- root: stashDir,
316
- changes: [],
317
- payload: { startedAt: new Date().toISOString(), operations: ops, completed: [] },
318
- });
319
- }
320
- /** Durably mark one op complete on the checklist. Best-effort, like before. */
321
- function markJournalCompleted(txn, opRef) {
322
- try {
323
- txn.journal.payload.completed.push(opRef);
324
- advanceTxn(txn, "applying");
325
- }
326
- catch {
327
- // best-effort
328
- }
329
- }
330
- /**
331
- * Run-entry recovery decision for stale consolidate journals (unchanged
332
- * semantics: `abort` refuses the run, `clean` removes stale artifacts). A
333
- * journal whose checklist is complete is a committed leftover and is swept
334
- * quietly — under the legacy single-path home the next run's journal write
335
- * overwrote it as a side effect; per-transaction dirs need the sweep to be
336
- * explicit (the legacy orphaned-backup leak documented in the journal
337
- * lifecycle golden is gone with it).
338
- */
339
- function checkForIncompleteJournal(stashDir, recoveryMode, warnings) {
340
- const nsDir = txnNamespaceDir(stashDir);
341
- if (!fs.existsSync(nsDir))
342
- return;
343
- for (const entry of fs.readdirSync(nsDir, { withFileTypes: true })) {
344
- if (!entry.isDirectory())
345
- continue;
346
- const transactionDir = path.join(nsDir, entry.name);
347
- const journalPath = path.join(transactionDir, "journal.json");
348
- if (!fs.existsSync(journalPath))
349
- continue;
350
- let journal;
351
- try {
352
- journal = JSON.parse(fs.readFileSync(journalPath, "utf8"));
353
- }
354
- catch {
355
- // An unreadable journal CANNOT be attributed to a kind — it may fence a
356
- // sibling domain's interrupted, irreversible mutation (mv/proposal
357
- // journals share the stash namespace). Never destroy it from here:
358
- // `clean` skips it with a warning; the default aborts fail-closed with
359
- // kind-agnostic guidance.
360
- if (recoveryMode === "clean") {
361
- warnings.push(`Unreadable transaction journal at ${journalPath} may belong to another transaction kind; not removed — inspect manually.`);
362
- continue;
363
- }
364
- throw new ConfigError(`Incomplete transaction state detected: unreadable journal at ${journalPath}. It may fence an interrupted mv or proposal mutation — inspect or remove the file manually.`, "INVALID_CONFIG_FILE");
365
- }
366
- if (journal.kind !== CONSOLIDATE_TXN_KIND)
367
- continue;
368
- const operationCount = Array.isArray(journal.payload?.operations) ? journal.payload.operations.length : 0;
369
- const completedCount = Array.isArray(journal.payload?.completed) ? journal.payload.completed.length : 0;
370
- if (completedCount >= operationCount) {
371
- // Committed leftover — sweep quietly (see doc comment above).
372
- cleanupTxn(transactionDir);
373
- continue;
374
- }
375
- if (recoveryMode === "clean") {
376
- const cleanupWarning = cleanupTxn(transactionDir);
377
- warnings.push(cleanupWarning
378
- ? `Failed to remove stale consolidation journal at ${journalPath}.`
379
- : `Removed stale consolidation journal at ${journalPath} (${completedCount}/${operationCount} operations completed).`);
380
- continue;
381
- }
382
- throw new ConfigError(`Incomplete consolidation run detected at ${journalPath} (${completedCount}/${operationCount} operations completed). Re-run with --consolidate-recovery clean to remove stale journal artifacts. Backup dir: ${path.join(transactionDir, "backup")}.`, "INVALID_CONFIG_FILE");
383
- }
384
- }
385
- function backupFile(filePath, backupDir, name) {
386
- try {
387
- fs.mkdirSync(backupDir, { recursive: true });
388
- fs.copyFileSync(filePath, path.join(backupDir, `${name}.md`));
389
- }
390
- catch {
391
- // best-effort
392
- }
393
- }
394
- // ── WS-3b: Generation frontmatter injection ───────────────────────────────────
395
- /**
396
- * Inject `generation` and canonical `xrefs` into merged content.
397
- * generation = max(sourceGenerations) + 1.
398
- * xrefs = UNION of the provided provenance refs (participants + their cited
399
- * sources) with anything already present in xrefs or legacy source_refs —
400
- * R5 §4.2: the old set-if-absent behavior dropped second-generation
401
- * provenance whenever the LLM emitted its own (partial) source_refs.
402
- * Fails open — returns original content if frontmatter can't be parsed.
403
- */
404
- export function injectGenerationFrontmatter(mergedContent, sourceGenerations, provenanceRefs) {
405
- try {
406
- const parsed = parseFrontmatter(mergedContent);
407
- const existingFm = parsed.data;
408
- const existingRefs = [
409
- ...(Array.isArray(existingFm.xrefs) ? existingFm.xrefs.map(String) : []),
410
- ...(Array.isArray(existingFm.source_refs) ? existingFm.source_refs.map(String) : []),
411
- ];
412
- const canonicalRefs = [...existingRefs, ...provenanceRefs].flatMap((ref) => {
413
- try {
414
- // Canonical stored provenance xref, WI-8.5b display-flip: emit the D-R5
415
- // new-grammar spelling (short conceptId for the primary/default bundle,
416
- // `bundle//conceptId` for a slug source, legacy `origin//type:name` only
417
- // for a non-slug registry origin) via `displayRef`, so a merged asset's
418
- // stored `xrefs` unify with the WI-8.5a content/frontmatter xref writer.
419
- const p = parseRefInput(ref);
420
- return [displayRef({ type: p.type, name: p.name, bundleId: p.origin })];
421
- }
422
- catch {
423
- return [];
424
- }
425
- });
426
- const updatedFm = {
427
- ...existingFm,
428
- generation: computeMergedGeneration(sourceGenerations),
429
- xrefs: [...new Set(canonicalRefs)],
430
- };
431
- delete updatedFm.source_refs;
432
- return assembleAssetFromString(serializeFrontmatter(updatedFm), parsed.content);
433
- }
434
- catch {
435
- return mergedContent; // fail open
436
- }
437
- }
438
- /**
439
- * WI-8.5b — canonical D-R5 display spelling of a stored provenance xref: short
440
- * conceptId for the primary/default bundle, `bundle//conceptId` for a slug
441
- * source, legacy `origin//type:name` only for a non-slug registry origin.
442
- * Returns the input unchanged when it cannot be parsed as a ref (fail-open, the
443
- * pre-flip behavior for hand-authored provenance strings).
444
- */
445
- function canonicalXref(ref) {
289
+ /** Parse a stored provenance ref and emit its canonical D-R5 display spelling. */
290
+ function canonicalStoredXref(ref) {
446
291
  try {
447
292
  const p = parseRefInput(ref);
448
293
  return displayRef({ type: p.type, name: p.name, bundleId: p.origin });
449
294
  }
450
295
  catch {
451
- return ref;
296
+ return undefined;
452
297
  }
453
298
  }
299
+ function canonicalXref(ref) {
300
+ return canonicalStoredXref(ref) ?? ref;
301
+ }
454
302
  /**
455
303
  * The promoted asset's provenance xref set: existing body-frontmatter xrefs +
456
304
  * the promoted source ref, deduped after canonicalization (WI-8.5b: emitted in
@@ -460,62 +308,13 @@ function promoteProvenanceXrefs(existing, sourceRef) {
460
308
  const priors = Array.isArray(existing) ? existing.map(String) : [];
461
309
  return [...new Set([...priors, sourceRef].map(canonicalXref))];
462
310
  }
463
- // ── Archive helper (P1-B: soft-invalidation) ─────────────────────────────────
464
- /**
465
- * Move a memory asset to `.akm/archive/` with `status: superseded` frontmatter
466
- * instead of deleting it outright. The live stash delete still happens after
467
- * this call — this is belt-and-suspenders archival that survives the hard delete.
468
- *
469
- * Archive filename: `<iso-ts>-<opIndex>-<basename>.md`
470
- * New frontmatter fields: status, superseded_at, superseded_by (optional),
471
- * superseded_reason.
472
- */
473
- function archiveMemory(filePath, stashDir, ref, reason, opIndex, supersededBy, warnings) {
474
- const archiveDir = path.join(stashDir, ".akm", "archive");
475
- fs.mkdirSync(archiveDir, { recursive: true });
476
- let raw;
477
- try {
478
- raw = fs.readFileSync(filePath, "utf8");
479
- }
480
- catch {
481
- if (warnings)
482
- warnings.push(`archiveMemory: could not read ${ref} for archiving — skipping archive write`);
483
- return;
484
- }
485
- let content = raw;
486
- try {
487
- const parsed = parseFrontmatter(raw);
488
- const newFm = {
489
- ...parsed.data,
490
- status: "superseded",
491
- superseded_at: new Date().toISOString(),
492
- ...(supersededBy ? { superseded_by: supersededBy } : {}),
493
- superseded_reason: reason,
494
- };
495
- content = assembleAssetFromString(serializeFrontmatter(newFm), parsed.content);
496
- }
497
- catch {
498
- if (warnings)
499
- warnings.push(`archiveMemory: could not parse frontmatter for ${ref} — archiving raw`);
500
- }
501
- const ts = timestampForFilename();
502
- const safeName = path.basename(filePath, ".md");
503
- const archivePath = path.join(archiveDir, `${ts}-${opIndex}-${safeName}.md`);
504
- try {
505
- fs.writeFileSync(archivePath, content, "utf8");
506
- }
507
- catch (e) {
508
- if (warnings)
509
- warnings.push(`archiveMemory: write failed for ${ref}: ${String(e)}`);
510
- }
511
- }
512
311
  // ── LLM resolution ──────────────────────────────────────────────────────────
513
312
  /**
514
313
  * Resolve the LLM connection for the consolidate pass.
515
314
  *
516
315
  * Priority order (mirrors extract / reflect / distill — see
517
- * `src/commands/extract.ts:421-438` and the canonical
518
- * `resolveImproveProcessRunner` pattern):
316
+ * `resolveExtractRunConfig` in `src/commands/improve/extract.ts` and the
317
+ * canonical `resolveImproveProcessRunner` pattern):
519
318
  *
520
319
  * 1. `improve.strategies.<name>.processes.consolidate.engine`
521
320
  * via {@link resolveImproveProcessRunner}. Lets the user pin
@@ -562,22 +361,24 @@ export function makeConsolidateResult(overrides) {
562
361
  // ── Main entry point ─────────────────────────────────────────────────────────
563
362
  function resolveConsolidationWriteTarget(opts, config) {
564
363
  if (opts.writeTarget) {
364
+ const root = path.resolve(opts.writeTarget.source.path);
565
365
  return {
566
366
  ...opts.writeTarget,
567
- source: { ...opts.writeTarget.source, path: path.resolve(opts.writeTarget.source.path) },
367
+ source: {
368
+ ...opts.writeTarget.source,
369
+ path: root,
370
+ adapterId: opts.writeTarget.source.adapterId ?? detectAdapterId(root),
371
+ },
568
372
  };
569
373
  }
570
- if (opts.target && !path.isAbsolute(opts.target)) {
374
+ if (opts.target) {
571
375
  const target = resolveWriteTarget(config, opts.target);
572
376
  return { ...target, source: { ...target.source, path: path.resolve(target.source.path) } };
573
377
  }
574
- // Programmatic callers historically supplied an absolute stash path. Normalize
575
- // it immediately into the same unambiguous write-target shape used by named sources.
576
- const explicitRoot = opts.target ?? opts.stashDir;
577
- if (explicitRoot) {
578
- const root = path.resolve(explicitRoot);
378
+ if (opts.stashDir) {
379
+ const root = path.resolve(opts.stashDir);
579
380
  return {
580
- source: { kind: "filesystem", name: "stash", path: root },
381
+ source: { kind: "filesystem", name: "stash", path: root, adapterId: detectAdapterId(root) },
581
382
  config: { type: "filesystem", name: "stash", path: root, writable: true },
582
383
  };
583
384
  }
@@ -600,7 +401,7 @@ export async function akmConsolidate(opts = {}) {
600
401
  // handle. consolidate.ts has no `eventsCtx`/proposals-`ctx` option at all
601
402
  // (WS-3a retired its only appendEvent usage; `emitProposal` here is always
602
403
  // called with the default, seam-less ProposalsContext — see
603
- // handlePromoteOp below), so both get the safe empty-object default,
404
+ // emitPromotionProposal below), so both get the safe empty-object default,
604
405
  // behaviorally identical to `undefined` (EventsContext/ProposalsContext
605
406
  // fields are all optional-chained by their consumers). `getLlmConfig`
606
407
  // mirrors `planConsolidation`'s own resolution (`resolveConsolidateLlmConfig`)
@@ -624,6 +425,7 @@ export async function akmConsolidate(opts = {}) {
624
425
  dryRun: opts.dryRun ?? false,
625
426
  signal: opts.signal,
626
427
  });
428
+ const warnings = [];
627
429
  if (!resolveProcessEnabled("consolidate", opts.improveProfile ?? resolveImproveStrategy(undefined, config).config)) {
628
430
  return makeConsolidateResult({
629
431
  // Sourced from runContext (identical value to `opts.dryRun ?? false`)
@@ -633,10 +435,9 @@ export async function akmConsolidate(opts = {}) {
633
435
  dryRun: runContext.dryRun,
634
436
  target: opts.target ?? stashDir,
635
437
  durationMs: Date.now() - startMs,
438
+ warnings,
636
439
  });
637
440
  }
638
- const warnings = [];
639
- checkForIncompleteJournal(stashDir, opts.recoveryMode ?? "abort", warnings);
640
441
  // WS-3a: open one state.db handle shared by the body-embedding cache (dedup
641
442
  // + cluster) and the judged-state cache. All callers in the function body
642
443
  // receive this handle; it is closed in the `finally` block below.
@@ -649,7 +450,7 @@ export async function akmConsolidate(opts = {}) {
649
450
  // State DB unavailable → skip the embedding cache for this run.
650
451
  }
651
452
  try {
652
- return await akmConsolidateInner(opts, config, stashDir, startMs, sourceRun, warnings, sharedStateDb);
453
+ return await akmConsolidateInner(opts, config, stashDir, startMs, warnings, sharedStateDb);
653
454
  }
654
455
  finally {
655
456
  sharedStateDb?.close();
@@ -903,7 +704,12 @@ async function judgeConsolidationChunks(args) {
903
704
  { role: "system", content: CONSOLIDATE_SYSTEM_PROMPT },
904
705
  { role: "user", content: userPrompt },
905
706
  ],
906
- request: { responseSchema: CONSOLIDATE_PLAN_JSON_SCHEMA, enableThinking: false },
707
+ request: {
708
+ responseSchema: CONSOLIDATE_PLAN_JSON_SCHEMA,
709
+ enableThinking: false,
710
+ timeoutMs: llmConfig.timeoutMs,
711
+ signal: opts.signal,
712
+ },
907
713
  parse: (raw) => ({ ok: true, content: raw ?? "" }),
908
714
  // A transport throw was caught INSIDE the gated fn and returned as an
909
715
  // {ok:false} envelope (never reaching the gate's fallback); onError
@@ -933,9 +739,12 @@ async function judgeConsolidationChunks(args) {
933
739
  }
934
740
  raw = retry;
935
741
  }
936
- if (process.env.AKM_DEBUG_LLM) {
742
+ // C9 action 1: AKM_DEBUG_LLM was a separate, undocumented env var for this
743
+ // one diagnostic; folded into the standard AKM_VERBOSE gate (warnVerbose)
744
+ // rather than kept as its own toggle.
745
+ {
937
746
  const preview = (raw.content ?? "").slice(0, 500);
938
- warn(`[akm:consolidate] chunk ${chunkIdx + 1} raw response (first 500 chars): ${preview}`);
747
+ warnVerbose(`[akm:consolidate] chunk ${chunkIdx + 1} raw response (first 500 chars): ${preview}`);
939
748
  }
940
749
  const parsed = parseEmbeddedJsonResponse(raw.content);
941
750
  if (!parsed || !Array.isArray(parsed.operations)) {
@@ -980,7 +789,6 @@ async function planConsolidation(opts, config, stashDir, _startMs, memories, war
980
789
  const llmConfig = Object.hasOwn(opts, "llmConfig")
981
790
  ? (opts.llmConfig ?? undefined)
982
791
  : resolveConsolidateLlmConfig(config, opts.improveProfile);
983
- const isHttpPath = !!llmConfig;
984
792
  // Chunk sizing: derive a safe chunk size from the configured model context
985
793
  // window so that the full prompt (system prompt + chunk user prompt) never
986
794
  // exceeds the model's n_ctx limit. When no context length is configured we
@@ -996,15 +804,43 @@ async function planConsolidation(opts, config, stashDir, _startMs, memories, war
996
804
  const chunkSize = computeSafeChunkSize(modelContextLength, bodyTruncation, opts.maxChunkSize);
997
805
  // -- Phase A: plan generation -----------------------------------------------
998
806
  const sourceName = opts.target ?? stashDir;
999
- // WS-5: capture llmPoolSize = memories entering the LLM (after all filtering).
1000
- const llmPoolSize = memories.length;
807
+ let budgetedMemories = memories;
808
+ if (opts.signal) {
809
+ const budgetMs = opts.signal.remainingBudgetMs;
810
+ if (budgetMs !== undefined) {
811
+ const p90Chunk = opts.p90ChunkSecondsDefault ?? 30;
812
+ const safeChunks = Math.max(0, Math.floor((Math.max(0, budgetMs) / 1000 / p90Chunk) * 0.6));
813
+ const cap = safeChunks * chunkSize;
814
+ if (cap < memories.length) {
815
+ budgetedMemories = memories
816
+ .map((entry) => {
817
+ let mtimeMs = 0;
818
+ try {
819
+ mtimeMs = fs.statSync(entry.filePath).mtimeMs;
820
+ }
821
+ catch {
822
+ // Missing files sort first and are filtered by the existing guards.
823
+ }
824
+ return { entry, mtimeMs };
825
+ })
826
+ .sort((a, b) => a.mtimeMs - b.mtimeMs || a.entry.name.localeCompare(b.entry.name))
827
+ .map(({ entry }) => entry)
828
+ .slice(0, cap);
829
+ const msg = `[consolidate] cold-start budget: reducing pool from ${memories.length} to ${budgetedMemories.length} memories (${safeChunks} safe chunks; remainder deferred).`;
830
+ warn(msg);
831
+ warnings.push(msg);
832
+ }
833
+ }
834
+ }
835
+ // WS-5: capture llmPoolSize after every pre-LLM cap.
836
+ const llmPoolSize = budgetedMemories.length;
1001
837
  // C-1 / #380: Pre-cluster memories by embedding similarity before chunking.
1002
838
  // This ensures that semantically similar memories land in the same LLM
1003
839
  // context window, allowing the model to detect and merge duplicates that
1004
840
  // would otherwise be split across chunks and survive indefinitely.
1005
841
  // mem0 arXiv:2504.19413, A-MEM arXiv:2502.12110.
1006
842
  // Fails open: if embeddings are unavailable or fail, original order is used.
1007
- const { ordered: clusteredMemories, embedTelemetry } = await clusterMemoriesBySimilarity(memories, config, sharedStateDb);
843
+ const { ordered: clusteredMemories, embedTelemetry } = await clusterMemoriesBySimilarity(budgetedMemories, config, sharedStateDb, opts.signal);
1008
844
  // WS-3b Anti-collapse step 8c: inject random (non-similar) clusters.
1009
845
  // A small fraction (default 5%) of the pool is shuffled into random positions
1010
846
  // so the pipeline isn't PURELY similarity-driven. This prevents rich-get-richer
@@ -1063,41 +899,7 @@ async function planConsolidation(opts, config, stashDir, _startMs, memories, war
1063
899
  // 4h on this user's stack. See
1064
900
  // /tmp/akm-health-investigations/tuning-reasons-investigation.md §Q3.
1065
901
  const pendingProposalBodyHashes = loadPendingConsolidateProposalHashes(stashDir);
1066
- // ── Cold-start budget estimation ─────────────────────────────────────────────
1067
- // Estimate wall-clock cost BEFORE issuing any LLM calls. When a signal is
1068
- // provided and the estimated cost exceeds ~60% of the remaining budget we
1069
- // auto-reduce the pool and log the reduction so the run never starts work
1070
- // it cannot finish (avoiding SIGTERM mid-LLM-call).
1071
- //
1072
- // Formula: chunks.length × p90_chunk_seconds. The p90 comes from
1073
- // `opts.p90ChunkSecondsDefault` (caller-supplied, typically from the profile
1074
- // config); absent = 30 s (conservative default matching a medium local LLM).
1075
- //
1076
- // "Remaining budget" is read from a custom property on the AbortSignal if
1077
- // the caller (improve.ts) has attached one. Without it no auto-reduction
1078
- // fires but the check is still cheap to run.
1079
- if (chunks.length > 10 && opts.signal) {
1080
- const p90Chunk = opts.p90ChunkSecondsDefault ?? 30;
1081
- const estimatedSeconds = chunks.length * p90Chunk;
1082
- // remainingBudgetMs is a non-standard extension set by improve.ts when it
1083
- // creates the budget AbortController. Undefined = no budget information.
1084
- const budgetMs = opts.signal.remainingBudgetMs;
1085
- if (budgetMs !== undefined && budgetMs > 0) {
1086
- const remainingSeconds = budgetMs / 1000;
1087
- if (estimatedSeconds > remainingSeconds * 0.6) {
1088
- const safeCaps = Math.max(1, Math.floor((remainingSeconds * 0.6) / p90Chunk));
1089
- const removedChunks = chunks.length - safeCaps;
1090
- if (removedChunks > 0) {
1091
- const msg = `[consolidate] cold-start budget: estimated ${estimatedSeconds.toFixed(0)}s > 60% of remaining ${remainingSeconds.toFixed(0)}s; ` +
1092
- `reducing pool from ${chunks.length} to ${safeCaps} chunks (${removedChunks} deferred to next run).`;
1093
- warn(msg);
1094
- warnings.push(msg);
1095
- chunks.splice(safeCaps);
1096
- }
1097
- }
1098
- }
1099
- }
1100
- warn(`[consolidate] ${memories.length} memories / ${chunks.length} chunk(s) / chunk_size=${chunkSize}` +
902
+ warn(`[consolidate] ${budgetedMemories.length} memories / ${chunks.length} chunk(s) / chunk_size=${chunkSize}` +
1101
903
  ` / pending-proposal hashes: ${pendingProposalBodyHashes.size}`);
1102
904
  // Consolidate output merges memories (non-wiki) → stash authoring standards.
1103
905
  // Resolved ONCE per run and passed to each chunk prompt (facts not re-read
@@ -1117,104 +919,19 @@ async function planConsolidation(opts, config, stashDir, _startMs, memories, war
1117
919
  });
1118
920
  // Build the known-refs set from the already-filtered memory pool so
1119
921
  // mergePlans() can reject LLM-hallucinated primary refs before execution.
1120
- const knownRefs = new Set(memories.map((m) => conceptIdFromTypeName("memory", m.name)));
922
+ const knownRefs = new Set(budgetedMemories.map((m) => conceptIdFromTypeName("memory", m.name)));
1121
923
  const { ops: allOps, warnings: mergeWarnings } = mergePlans(chunkOpsArrays, knownRefs);
1122
924
  warnings.push(...mergeWarnings);
1123
- return { allOps, totalChunks: chunks.length, llmPoolSize, embedTelemetry, isHttpPath, sourceName };
1124
- }
1125
- /**
1126
- * Pass 3 — execute the reconciled plan against the filesystem: resolve the
1127
- * write target, journal the batch, dispatch each op to its handler, then commit
1128
- * the batch at the boundary and clean up the journal. Mutates `accounting` via
1129
- * the op-handlers' `pushSkipReason`. Behavior-identical to the former inlined
1130
- * write block. Never invoked on the dry-run or aborted-confirm paths.
1131
- */
1132
- async function applyConsolidationPlan(config, stashDir, target, sourceRun, memories, warnings, allOps, accounting, activeProfile, llmConfig) {
1133
- // -- Phase B + writes -------------------------------------------------------
1134
- // Open the checklist journal (durably) before any mutations; backups live
1135
- // under the transaction directory.
1136
- const txn = beginConsolidateTxn(stashDir, allOps);
1137
- const backupDir = consolidateBackupDir(txn);
1138
- const counts = {
1139
- merged: 0,
1140
- deleted: 0,
1141
- contradicted: 0, // C-3 / #382: count of contradiction edges written
1142
- mergeFloorViolations: 0, // R5 §4.2: advisory merge-information-floor failures
1143
- mergedSecondaries: 0,
1144
- };
1145
- const promoted = [];
1146
- // Within-run dedup: track source refs for which a promote proposal was
1147
- // already created this run. The LLM can return multiple promote ops for
1148
- // different source memories that happen to have identical content (all are
1149
- // duplicate memories), so we also need a content-hash guard below.
1150
- const promotedSourceRefs = new Set();
1151
- // Build a lookup map: ref → MemoryEntry
1152
- const memoryByRef = new Map();
1153
- for (const m of memories) {
1154
- memoryByRef.set(conceptIdFromTypeName("memory", m.name), m);
1155
- }
1156
- const opCtx = {
1157
- config,
1158
- txn,
1159
- improveProfile: activeProfile,
1160
- llmConfig: llmConfig ?? null,
1161
- stashDir,
1162
- sourceRun,
1163
- target,
1164
- backupDir,
1165
- memoryByRef,
1166
- promoted,
1167
- promotedSourceRefs,
1168
- warnings,
1169
- counts,
1170
- pushSkipReason: accounting.pushSkipReason,
925
+ return {
926
+ allOps,
927
+ totalChunks: chunks.length,
928
+ llmPoolSize,
929
+ deferredMemories: memories.length - budgetedMemories.length,
930
+ embedTelemetry,
931
+ sourceName,
1171
932
  };
1172
- // Thin dispatch over the op discriminator — each branch is now an isolated,
1173
- // independently-testable handler that mutates `opCtx`.
1174
- for (let opIndex = 0; opIndex < allOps.length; opIndex++) {
1175
- const op = allOps[opIndex];
1176
- const opDisplayRef = op.op === "merge" ? op.primary : op.op === "contradict" ? `${op.ref} ↔ ${op.contradictedByRef}` : op.ref;
1177
- warn(`[consolidate] ${opIndex + 1}/${allOps.length} ${op.op} ${opDisplayRef}`);
1178
- switch (op.op) {
1179
- case "merge":
1180
- await handleMergeOp(op, opIndex, opCtx);
1181
- break;
1182
- case "delete":
1183
- await handleDeleteOp(op, opIndex, opCtx);
1184
- break;
1185
- case "promote":
1186
- await handlePromoteOp(op, opCtx);
1187
- break;
1188
- case "contradict":
1189
- await handleContradictOp(op, opCtx);
1190
- break;
1191
- }
1192
- }
1193
- const { merged, deleted, contradicted, mergeFloorViolations, mergedSecondaries } = counts;
1194
- // 0.9.0 (issue #507): batch-at-boundary commit. The merge/delete loop above
1195
- // wrote one merged primary and deleted N secondaries to the resolved target
1196
- // with NO per-asset commit. If the target is a writable git source and any
1197
- // asset was mutated, commit the whole batch ONCE here (stages .akm/ +
1198
- // siblings together). No-op for filesystem/primary-stash targets.
1199
- if (merged > 0 || deleted > 0) {
1200
- commitWriteTargetBoundary(target, `Consolidate: ${merged} merged, ${deleted} removed`);
1201
- }
1202
- advanceTxn(txn, "committed");
1203
- cleanupTxn(txn.dir);
1204
- // [signoff 2026-06-15] TTL archive cleanup machinery RETIRED (WS-3a).
1205
- // The elaborate archiveRetentionDays / archive-dir scan existed only to satisfy
1206
- // the old irrecoverability constraint. Stashes are now git-backed, so git
1207
- // history is the recovery path — no bespoke archive TTL needed. Any files in
1208
- // .akm/archive/ will stay there harmlessly until the operator prunes them with
1209
- // `git rm` or `find .akm/archive -mtime +90 -delete`. Changed N files this
1210
- // run; recover any via `git show <sha>:<path>` or `git restore <path>`.
1211
- if (merged > 0 || deleted > 0) {
1212
- const totalChanged = merged + deleted;
1213
- warnings.push(`Changed ${totalChanged} file(s) this run. Recover any via git if needed (git history is the backstop).`);
1214
- }
1215
- return { merged, deleted, contradicted, mergeFloorViolations, mergedSecondaries, promoted };
1216
933
  }
1217
- async function akmConsolidateInner(opts, config, stashDir, startMs, sourceRun, warnings, sharedStateDb) {
934
+ async function akmConsolidateInner(opts, config, stashDir, startMs, warnings, sharedStateDb) {
1218
935
  // -- Pass 1: narrow the memory pool (may early-return an envelope) ----------
1219
936
  const narrowed = await narrowConsolidationPool(opts, stashDir, startMs, warnings);
1220
937
  if (narrowed.done)
@@ -1222,14 +939,14 @@ async function akmConsolidateInner(opts, config, stashDir, startMs, sourceRun, w
1222
939
  const { memories, dedupPoolSize } = narrowed;
1223
940
  // -- Pass 2: build the LLM plan (populates the shared accounting counters) ---
1224
941
  const accounting = createConsolidateAccounting();
1225
- const { allOps, totalChunks, llmPoolSize, embedTelemetry, isHttpPath, sourceName } = await planConsolidation(opts, config, stashDir, startMs, memories, warnings, sharedStateDb, accounting);
942
+ const { allOps, totalChunks, llmPoolSize, deferredMemories, embedTelemetry, sourceName } = await planConsolidation(opts, config, stashDir, startMs, memories, warnings, sharedStateDb, accounting);
1226
943
  // -- Dry-run: show AI plan without executing any writes --------------------
1227
944
  if (opts.dryRun) {
1228
945
  return makeConsolidateResult({
1229
946
  dryRun: true,
1230
947
  previewOnly: true,
1231
948
  target: sourceName,
1232
- processed: memories.length,
949
+ processed: llmPoolSize,
1233
950
  failedChunks: accounting.totalChunksFailed,
1234
951
  totalChunks,
1235
952
  judgedNoAction: accounting.judgedNoAction,
@@ -1238,420 +955,64 @@ async function akmConsolidateInner(opts, config, stashDir, startMs, sourceRun, w
1238
955
  // provably still 0 here (it only increments in the op-execution loop).
1239
956
  mergedSecondaries: 0,
1240
957
  failedChunkMemories: accounting.failedChunkMemories,
958
+ deferredMemories,
1241
959
  planned: allOps,
1242
960
  warnings,
1243
961
  durationMs: Date.now() - startMs,
1244
962
  });
1245
963
  }
1246
964
  warn(`[consolidate] plan: ${allOps.length} operation(s)`);
1247
- // -- HTTP path: warn about quality and confirm unless assumeYes ------------
1248
- if (isHttpPath) {
1249
- warnings.push("Running on HTTP path — plan generated from truncated memory excerpts; quality may vary.");
1250
- // Gate the interactive-confirm path for manual/HTTP invocations. Callers
1251
- // that must apply without a prompt (goldens, programmatic batch runs)
1252
- // pass `assumeYes: true`.
1253
- if (opts.assumeYes !== true && allOps.length > 0) {
1254
- const n = allOps.length;
1255
- // Non-interactive contexts (CI / test runners / piped stdin) must not
1256
- // block on an unanswerable prompt. Default to a non-destructive "no"
1257
- // so callers in those contexts get the same "aborted, preview only"
1258
- // shape they'd get from explicit user dismissal. AKM_NON_INTERACTIVE
1259
- // lets callers force this path even when stdin happens to be a TTY.
1260
- const nonInteractive = process.stdin.isTTY === false || process.env.AKM_NON_INTERACTIVE === "1";
1261
- const answer = nonInteractive ? false : await promptConfirm(`Apply ${n} operations? [y/N] `);
1262
- if (!answer) {
1263
- return makeConsolidateResult({
1264
- previewOnly: true,
1265
- target: sourceName,
1266
- processed: memories.length,
1267
- failedChunks: accounting.totalChunksFailed,
1268
- totalChunks,
1269
- judgedNoAction: accounting.judgedNoAction,
1270
- skipReasons: accounting.skipReasons,
1271
- // No merge executed on the abort path — mergedSecondaries is still 0.
1272
- mergedSecondaries: 0,
1273
- failedChunkMemories: accounting.failedChunkMemories,
1274
- planned: allOps,
1275
- warnings: [...warnings, nonInteractive ? "Non-interactive context: skipped apply." : "Aborted by user."],
1276
- durationMs: Date.now() - startMs,
1277
- });
1278
- }
1279
- }
965
+ // Destructive operations remain advisory. Promote is safe to execute because
966
+ // it emits a reviewable proposal rather than mutating an asset.
967
+ const promoted = [];
968
+ const promotionFailures = { count: 0 };
969
+ const memoryByRef = new Map(memories.map((memory) => [conceptIdFromTypeName("memory", memory.name), memory]));
970
+ const promoteContext = {
971
+ config,
972
+ stashDir,
973
+ sourceRun: opts.sourceRun ?? `consolidate-${startMs}`,
974
+ proposalsCtx: opts.proposalsCtx,
975
+ target: opts.writeTarget,
976
+ memoryByRef,
977
+ promoted,
978
+ promotedSourceRefs: new Set(),
979
+ promotionFailures,
980
+ warnings,
981
+ pushSkipReason: accounting.pushSkipReason,
982
+ llmConfig: Object.hasOwn(opts, "llmConfig")
983
+ ? (opts.llmConfig ?? null)
984
+ : (resolveConsolidateLlmConfig(config, opts.improveProfile) ?? null),
985
+ };
986
+ for (const op of allOps) {
987
+ if (op.op === "promote")
988
+ await emitPromotionProposal(op, promoteContext);
1280
989
  }
1281
- // -- Pass 3: execute the plan against the filesystem ------------------------
1282
- const { merged, deleted, contradicted, mergeFloorViolations, mergedSecondaries, promoted } = await applyConsolidationPlan(config, stashDir, opts.writeTarget, sourceRun, memories, warnings, allOps, accounting, opts.improveProfile, Object.hasOwn(opts, "llmConfig")
1283
- ? (opts.llmConfig ?? null)
1284
- : (resolveConsolidateLlmConfig(config, opts.improveProfile) ?? null));
1285
- const runDurationMs = Date.now() - startMs;
1286
- const budgetFraction = opts.runBudgetMs !== undefined && opts.runBudgetMs > 0 ? runDurationMs / opts.runBudgetMs : undefined;
1287
- return {
1288
- schemaVersion: 1,
1289
- ok: true,
1290
- shape: "consolidate-result",
1291
- dryRun: false,
1292
- previewOnly: false,
990
+ return makeConsolidateResult({
1293
991
  target: sourceName,
1294
- processed: memories.length,
1295
- merged,
1296
- deleted,
1297
- promoted,
1298
- contradicted,
1299
- mergeFloorViolations,
992
+ processed: llmPoolSize,
1300
993
  failedChunks: accounting.totalChunksFailed,
1301
994
  totalChunks,
1302
995
  judgedNoAction: accounting.judgedNoAction,
1303
996
  skipReasons: accounting.skipReasons,
1304
- mergedSecondaries,
997
+ mergedSecondaries: 0,
1305
998
  failedChunkMemories: accounting.failedChunkMemories,
999
+ deferredMemories,
1000
+ promoted,
1001
+ failedPromotions: promotionFailures.count,
1002
+ planned: allOps,
1306
1003
  warnings,
1307
- durationMs: runDurationMs,
1004
+ durationMs: Date.now() - startMs,
1308
1005
  perfTelemetry: {
1309
1006
  dedupPoolSize,
1310
1007
  llmPoolSize,
1311
1008
  embedMs: embedTelemetry.embedMs,
1312
1009
  embedCacheHits: embedTelemetry.cacheHits,
1313
1010
  embedCacheMisses: embedTelemetry.cacheMisses,
1314
- ...(budgetFraction !== undefined ? { estimatedBudgetFractionUsed: budgetFraction } : {}),
1315
1011
  },
1316
- };
1317
- }
1318
- /** Execute one `merge` op (behavior-identical to the former inlined branch). */
1319
- /**
1320
- * Finalize a merge whose content has already been generated and validated:
1321
- * provenance/anti-collapse guards (generation guard, lexical-diversity + info-floor
1322
- * advisories), generation frontmatter injection, then backup/write/archive/journal
1323
- * and the 2026-05-26 accounting counters. Extracted verbatim from `handleMergeOp`
1324
- * — logic and every counter increment are byte-identical (the
1325
- * `processed == actioned + noAction + Σskips` invariant is preserved). All
1326
- * refusal paths emit a skip reason and return.
1327
- */
1328
- async function finalizeMerge(op, opIndex, ctx, mergedContentArg, emitMergeFailureSkips) {
1329
- const { stashDir, target, backupDir, memoryByRef, warnings, counts } = ctx;
1330
- let mergedContent = mergedContentArg;
1331
- const allParticipants = [op.primary, ...op.secondaries];
1332
- // Generation and provenance are mandatory merge metadata, independent of
1333
- // whether the optional anti-collapse refusal/advisory checks are enabled.
1334
- const participantInfo = allParticipants.map((ref) => {
1335
- const e = memoryByRef.get(ref);
1336
- if (!e)
1337
- return { ref, generation: 0, body: "", sourceRefs: [] };
1338
- try {
1339
- const raw = fs.readFileSync(e.filePath, "utf8");
1340
- const parsed = parseFrontmatter(raw);
1341
- const fm = parsed.data;
1342
- const sourceRefs = [
1343
- ...(Array.isArray(fm.xrefs) ? fm.xrefs.map(String) : []),
1344
- ...(Array.isArray(fm.source_refs) ? fm.source_refs.map(String) : []),
1345
- ];
1346
- return { ref, generation: readAssetGeneration(fm), body: stripFrontmatterBody(raw), sourceRefs };
1347
- }
1348
- catch {
1349
- return { ref, generation: 0, body: "", sourceRefs: [] };
1350
- }
1351
- });
1352
- const sourceGenerations = participantInfo.map((p) => p.generation);
1353
- // WS-3b: Anti-collapse generation guard (step 8a).
1354
- // DEFAULT ON since R5 (opt out via antiCollapse.enabled: false). Refuses
1355
- // to merge two assets both above generation N (default 2) — prevents the
1356
- // pipeline from building ever-deeper LLM-merged trees that lose the
1357
- // source fidelity of the original episodes.
1358
- const antiCollapseConfig = getImproveProcessConfig("consolidate", ctx.improveProfile)?.antiCollapse ?? {};
1359
- if (antiCollapseConfig.enabled !== false) {
1360
- const generationCheck = checkGenerationGuard(sourceGenerations, antiCollapseConfig);
1361
- if (generationCheck.refused) {
1362
- warnings.push(`Merge: ${generationCheck.reason}`);
1363
- emitMergeFailureSkips("merge_generation_guard");
1364
- return;
1365
- }
1366
- // WS-3b: Lexical diversity check (step 8b).
1367
- // Low n-gram diversity ⇒ likely correlated-extraction artifact; raise merge threshold.
1368
- if (antiCollapseConfig.lexicalDiversityCheck !== false) {
1369
- const bodies = participantInfo.map((p) => p.body).filter((b) => b.length > 0);
1370
- const diversityCheck = checkLexicalDiversity(bodies, antiCollapseConfig);
1371
- if (diversityCheck.lowDiversity) {
1372
- // Low-diversity cluster: just warn (don't refuse merge since the dedup
1373
- // path handles exact twins). The warning surfaces in health telemetry.
1374
- warnings.push(`Merge: cluster around ${op.primary} has low lexical diversity (${diversityCheck.diversity?.toFixed(2) ?? "?"} < 0.30) — likely correlated extraction; merge proceeds but review is recommended.`);
1375
- }
1376
- }
1377
- }
1378
- // merged.generation = max(sourceGenerations) + 1. xrefs is the UNION of
1379
- // participants + canonical and legacy provenance already carried by them.
1380
- const provenanceUnion = [...new Set([...allParticipants, ...participantInfo.flatMap((p) => p.sourceRefs)])];
1381
- mergedContent = injectGenerationFrontmatter(mergedContent, sourceGenerations, provenanceUnion);
1382
- if (antiCollapseConfig.enabled !== false) {
1383
- // R5 §4.2: merge-information floor — ADVISORY in v1. A merge that
1384
- // shrinks provenance or genericizes below the retention floor is
1385
- // counted + warned, never refused (promotion path: design doc §7).
1386
- try {
1387
- const mergedParsed = parseFrontmatter(mergedContent);
1388
- const mergedFm = mergedParsed.data;
1389
- const mergedSourceRefs = Array.isArray(mergedFm.xrefs) ? mergedFm.xrefs.map(String) : [];
1390
- const floorCheck = checkMergeInformationFloor(mergedParsed.content, mergedSourceRefs, participantInfo, antiCollapseConfig);
1391
- if (!floorCheck.passed) {
1392
- counts.mergeFloorViolations++;
1393
- warnings.push(`Merge: information floor advisory for ${op.primary}: ${floorCheck.reason ?? "unspecified"} — merge proceeds (v1 observe-only).`);
1394
- }
1395
- }
1396
- catch {
1397
- // Floor measurement is best-effort; never blocks the merge path.
1398
- }
1399
- }
1400
- // Backup secondaries before deleting
1401
- for (const secRef of op.secondaries) {
1402
- const secEntry = memoryByRef.get(secRef);
1403
- if (secEntry && fs.existsSync(secEntry.filePath)) {
1404
- backupFile(secEntry.filePath, backupDir, secEntry.name);
1405
- }
1406
- }
1407
- // Write merged primary
1408
- try {
1409
- const parsedPrimary = parseRefInput(op.primary);
1410
- await writeAssetToSource(target.source, target.config, parsedPrimary, mergedContent);
1411
- }
1412
- catch (e) {
1413
- warnings.push(`Merge: write failed for ${op.primary}: ${String(e)}`);
1414
- emitMergeFailureSkips("merge_write_failed");
1415
- return;
1416
- }
1417
- // Archive and delete secondaries (P1-B: soft-invalidation)
1418
- for (const secRef of op.secondaries) {
1419
- const secEntry = memoryByRef.get(secRef);
1420
- if (!secEntry)
1421
- continue;
1422
- if (fs.existsSync(secEntry.filePath)) {
1423
- archiveMemory(secEntry.filePath, stashDir, secRef, "merged into primary", opIndex, op.primary, warnings);
1424
- }
1425
- try {
1426
- const parsedSec = parseRefInput(secRef);
1427
- await deleteAssetFromSource(target.source, target.config, parsedSec);
1428
- markJournalCompleted(ctx.txn, secRef);
1429
- }
1430
- catch (e) {
1431
- warnings.push(`Merge: delete failed for ${secRef}: ${String(e)}`);
1432
- }
1433
- }
1434
- markJournalCompleted(ctx.txn, op.primary);
1435
- counts.merged++;
1436
- // 2026-05-26 accounting-leak fix: `merged` is op-level, but each
1437
- // successful merge actions `1 + secondaries.length` memories. Without
1438
- // this counter the accounting invariant breaks by `secondaries.length`
1439
- // per successful merge (chunk loop excluded all secondaries from
1440
- // judgedNoAction via targetRefs, but only the primary is credited to
1441
- // `merged`). Count only loaded-memory secondaries; phantom secondary
1442
- // refs never affected any chunk's targetRefs in the first place.
1443
- for (const secRef of op.secondaries) {
1444
- if (memoryByRef.has(secRef))
1445
- counts.mergedSecondaries++;
1446
- }
1447
- // Prune consumed refs from memoryByRef so later ops in this run cannot
1448
- // reference an absorbed secondary as a merge primary and proceed with a
1449
- // stale entry. Primary is rewritten (not deleted), so we only remove
1450
- // secondaries; the primary ref remains valid under its new content.
1451
- for (const secRef of op.secondaries) {
1452
- memoryByRef.delete(secRef);
1453
- }
1454
- }
1455
- export async function handleMergeOp(op, opIndex, ctx) {
1456
- const { config, memoryByRef, warnings, pushSkipReason } = ctx;
1457
- // Accounting helper: emit a per-participant skipReason for failed
1458
- // merges so primary + every loaded-memory secondary land in the
1459
- // structured skip histogram. Pre-2026-05-26 only the primary was
1460
- // counted (1 skipReason per failed merge), leaving N secondaries
1461
- // unaccounted for in the `processed == actioned + noAction + Σskips`
1462
- // invariant — the source of the 4–11 silent leaks per run.
1463
- const emitMergeFailureSkips = (reason) => {
1464
- if (memoryByRef.has(op.primary))
1465
- pushSkipReason("merge", op.primary, reason);
1466
- for (const secRef of op.secondaries) {
1467
- if (memoryByRef.has(secRef))
1468
- pushSkipReason("merge", secRef, reason);
1469
- }
1470
- };
1471
- const primaryEntry = memoryByRef.get(op.primary);
1472
- if (!primaryEntry) {
1473
- // This fires when a prior op in the same run consumed this ref as a
1474
- // secondary and Fix-A pruned it from memoryByRef. It should NOT fire
1475
- // for hallucinated primaries (those are dropped by mergePlans() before
1476
- // reaching here). If this counter is non-zero, suspect an intra-run
1477
- // cross-chunk race, not a filter regression.
1478
- warnings.push(`Merge: primary ${op.primary} not found in loaded memories (pruned by prior op this run) — skipping.`);
1479
- emitMergeFailureSkips("merge_primary_missing");
1480
- return;
1481
- }
1482
- // Defense-in-depth: even if the entry is in memoryByRef (pre-flight ran
1483
- // before this run's own ops), the file may have been deleted by a
1484
- // concurrent process or an edge case the pre-flight filter missed.
1485
- if (!fs.existsSync(primaryEntry.filePath)) {
1486
- warnings.push(`Merge: primary ${op.primary} file gone at execution time (stale entry) — skipping.`);
1487
- emitMergeFailureSkips("merge_primary_file_gone");
1488
- return;
1489
- }
1490
- // Phase B: generate merged content
1491
- const secondaryBodies = [];
1492
- for (const secRef of op.secondaries) {
1493
- const secEntry = memoryByRef.get(secRef);
1494
- if (!secEntry) {
1495
- warnings.push(`Merge: secondary ${secRef} not found — skipping merge op.`);
1496
- // No accounting impact: a missing secondary is a phantom ref and
1497
- // never contributed to any chunk's targetRefs reduction. We still
1498
- // continue the loop to gather the remaining valid secondaries.
1499
- continue;
1500
- }
1501
- secondaryBodies.push(secRef);
1502
- }
1503
- if (secondaryBodies.length === 0) {
1504
- warnings.push(`Merge: ${op.primary} has no valid secondaries — skipping.`);
1505
- emitMergeFailureSkips("merge_no_valid_secondaries");
1506
- return;
1507
- }
1508
- // Pre-flight hot guard — skip the LLM call entirely if any participant
1509
- // is hot or unparseable. Without this, mixed chunks still send hot merges
1510
- // to the planner which proposes them; generateMergedContent() is then
1511
- // called, produces output without `description`, and the skip is
1512
- // misattributed to merge_missing_description instead of the real cause.
1513
- const preflightParticipants = [op.primary, ...op.secondaries];
1514
- const preflightBlocked = preflightParticipants.flatMap((ref) => {
1515
- const e = memoryByRef.get(ref);
1516
- if (!e)
1517
- return [];
1518
- const verdict = consolidateGuardStatus(e.filePath);
1519
- if (verdict === "hot" || verdict === "unparseable")
1520
- return [{ ref, verdict }];
1521
- return [];
1522
- });
1523
- if (preflightBlocked.length > 0) {
1524
- const detail = preflightBlocked.map((p) => `${p.ref} (${p.verdict})`).join(", ");
1525
- warnings.push(`Merge: refused for ${op.primary} — ${preflightBlocked.length} participant(s) blocked by hot/unparseable frontmatter guard (pre-flight): ${detail}`);
1526
- emitMergeFailureSkips("merge_participant_blocked");
1527
- return;
1528
- }
1529
- let primaryBody = "";
1530
- try {
1531
- primaryBody = fs.readFileSync(primaryEntry.filePath, "utf8");
1532
- }
1533
- catch {
1534
- warnings.push(`Merge: could not read primary ${op.primary} — skipping.`);
1535
- emitMergeFailureSkips("merge_read_failed");
1536
- return;
1537
- }
1538
- const mergeResult = await (ctx.generateMergedContentFn ?? generateMergedContent)(config, op.primary, primaryBody, op.secondaries, memoryByRef, ctx.improveProfile, ctx.llmConfig);
1539
- if ("error" in mergeResult) {
1540
- warnings.push(`Merge: ${mergeResult.error} for ${mergeResult.detail}.`);
1541
- emitMergeFailureSkips(mergeResult.error);
1542
- return;
1543
- }
1544
- const mergedContent = mergeResult.content;
1545
- // Validate frontmatter of merged content — must have a `---` block
1546
- // with at minimum a `description` field. We parse via the hand-rolled
1547
- // parser (cheap) AND require non-empty description. This guards against
1548
- // the historical defect where merged memories were written back with
1549
- // empty `description` and later polluted the promote path.
1550
- let parsedMerged;
1551
- try {
1552
- parsedMerged = parseFrontmatter(mergedContent);
1553
- }
1554
- catch {
1555
- warnings.push(`Merge: merged content for ${op.primary} has invalid frontmatter — skipping.`);
1556
- emitMergeFailureSkips("merge_invalid_frontmatter");
1557
- return;
1558
- }
1559
- if (parsedMerged.frontmatter === null) {
1560
- warnings.push(`Merge: merged content for ${op.primary} has no frontmatter block — skipping.`);
1561
- emitMergeFailureSkips("merge_invalid_frontmatter");
1562
- return;
1563
- }
1564
- const mergedDesc = parsedMerged.data.description;
1565
- if (typeof mergedDesc !== "string" || mergedDesc.trim().length === 0) {
1566
- warnings.push(`Merge: merged content for ${op.primary} missing description — skipping.`);
1567
- emitMergeFailureSkips("merge_missing_description");
1568
- return;
1569
- }
1570
- const truncReason = detectTruncatedDescription(mergedDesc);
1571
- if (truncReason) {
1572
- warnings.push(`Merge: merged content for ${op.primary} has truncated description (${truncReason}) — skipping.`);
1573
- emitMergeFailureSkips("merge_truncated_description");
1574
- return;
1575
- }
1576
- // captureMode:hot guard — refuse the merge if ANY participating memory
1577
- // (primary or secondary) was user-captured or has unparseable frontmatter
1578
- // (could have hidden a hot flag). Hot memories are user-explicit and
1579
- // must not be deleted/overwritten by the consolidate LLM. 14 user
1580
- // memories were silent-deleted by consolidate before this guard landed;
1581
- // recovery required copying from .akm/archive/ by hand.
1582
- const mergeParticipants = [op.primary, ...op.secondaries];
1583
- const blockedParticipants = mergeParticipants.flatMap((ref) => {
1584
- const e = memoryByRef.get(ref);
1585
- if (!e)
1586
- return [];
1587
- const verdict = consolidateGuardStatus(e.filePath);
1588
- if (verdict === "hot" || verdict === "unparseable")
1589
- return [{ ref, verdict }];
1590
- return [];
1591
1012
  });
1592
- if (blockedParticipants.length > 0) {
1593
- const detail = blockedParticipants.map((p) => `${p.ref} (${p.verdict})`).join(", ");
1594
- warnings.push(`Merge: refused for ${op.primary} — ${blockedParticipants.length} participant(s) blocked by hot/unparseable frontmatter guard: ${detail}`);
1595
- emitMergeFailureSkips("merge_participant_blocked");
1596
- return;
1597
- }
1598
- await finalizeMerge(op, opIndex, ctx, mergedContent, emitMergeFailureSkips);
1599
- }
1600
- /** Execute one `delete` op (behavior-identical to the former inlined branch). */
1601
- export async function handleDeleteOp(op, opIndex, ctx) {
1602
- const { stashDir, target, backupDir, memoryByRef, warnings, pushSkipReason, counts } = ctx;
1603
- const entry = memoryByRef.get(op.ref);
1604
- if (!entry) {
1605
- warnings.push(`Delete: ${op.ref} not found in loaded memories — skipping.`);
1606
- // Phantom ref: not in the batch so not in processed. Pushing to
1607
- // skipReasons would inflate Σ(skipReasons) without a matching processed
1608
- // entry, breaking the accounting invariant. Visibility is preserved via
1609
- // the warnings array above.
1610
- return;
1611
- }
1612
- // captureMode:hot guard — refuse to delete user-captured memories OR
1613
- // memories whose frontmatter is unparseable (could have hidden the hot
1614
- // flag). The consolidate LLM was deleting hot-captured user memos as
1615
- // "redundant" — 14 such deletes were silently archived between
1616
- // 2026-05-19 and 2026-05-20 before this guard. Hot memories are
1617
- // user-explicit and may only be deleted by the user.
1618
- const guard = consolidateGuardStatus(entry.filePath);
1619
- if (guard === "hot" || guard === "unparseable") {
1620
- warnings.push(`Delete: refused for ${op.ref} — ${guard === "hot" ? "captureMode:hot (user-explicit; never auto-delete)" : "frontmatter unparseable (cannot verify hot flag absent)"}. Reason from LLM: "${op.reason ?? "n/a"}"`);
1621
- pushSkipReason("delete", op.ref, "captureMode_hot_refused");
1622
- return;
1623
- }
1624
- if (fs.existsSync(entry.filePath)) {
1625
- backupFile(entry.filePath, backupDir, entry.name);
1626
- // P1-B: soft-invalidation archive before hard delete
1627
- archiveMemory(entry.filePath, stashDir, op.ref, op.reason, opIndex, undefined, warnings);
1628
- }
1629
- try {
1630
- const parsedRef = parseRefInput(op.ref);
1631
- await deleteAssetFromSource(target.source, target.config, parsedRef);
1632
- markJournalCompleted(ctx.txn, op.ref);
1633
- counts.deleted++;
1634
- // Prune from memoryByRef so later ops in this run cannot reference a
1635
- // deleted memory as a merge primary or secondary.
1636
- memoryByRef.delete(op.ref);
1637
- }
1638
- catch (e) {
1639
- // Distinguish "file already absent" from genuine failures. A prior run
1640
- // may have deleted the file but the DB was not yet re-indexed, so the
1641
- // ref still appeared in memoryByRef. The delete goal is already met.
1642
- const msg = e instanceof Error ? e.message : String(e);
1643
- if (msg.includes("not found in source")) {
1644
- warnings.push(`Delete: ${op.ref} — file already absent (stale DB entry); skipping.`);
1645
- pushSkipReason("delete", op.ref, "delete_already_gone");
1646
- }
1647
- else {
1648
- warnings.push(`Delete: failed for ${op.ref}: ${String(e)}`);
1649
- pushSkipReason("delete", op.ref, "delete_failed");
1650
- }
1651
- }
1652
1013
  }
1653
- /** Execute one `promote` op (behavior-identical to the former inlined branch). */
1654
- export async function handlePromoteOp(op, ctx) {
1014
+ /** Execute one reconciled promotion by emitting a reviewable proposal. */
1015
+ async function emitPromotionProposal(op, ctx) {
1655
1016
  const { config, stashDir, sourceRun, target, memoryByRef, warnings, pushSkipReason, promoted, promotedSourceRefs } = ctx;
1656
1017
  const entry = memoryByRef.get(op.ref);
1657
1018
  if (!entry) {
@@ -1669,11 +1030,7 @@ export async function handlePromoteOp(op, ctx) {
1669
1030
  pushSkipReason("promote", op.ref, "promote_already_promoted_this_run");
1670
1031
  return;
1671
1032
  }
1672
- const proposedName = op.knowledgeRef
1673
- .replace(/^knowledge:/, "")
1674
- .split("/")
1675
- .filter(Boolean)
1676
- .at(-1) ??
1033
+ const proposedName = op.knowledgeRef.split("/").filter(Boolean).at(-1) ??
1677
1034
  entry.name.split("/").filter(Boolean).at(-1) ??
1678
1035
  "promoted-memory";
1679
1036
  const slug = proposedName
@@ -1686,9 +1043,8 @@ export async function handlePromoteOp(op, ctx) {
1686
1043
  if (knowledgeRef !== op.knowledgeRef) {
1687
1044
  warnings.push(`Normalized generated ref "${op.knowledgeRef}" → "${knowledgeRef}"`);
1688
1045
  }
1689
- // Idempotency: a pending proposal already queued for this knowledge concept
1690
- // (grammar-independent WI-8.5a stores proposals.ref as the item_ref, so an
1691
- // exact `{ ref: knowledge:slug }` filter would miss it).
1046
+ // A pending proposal may carry a qualified item_ref, so compare its parsed
1047
+ // conceptId rather than exact display spelling.
1692
1048
  if (hasPendingProposalForConcept(stashDir, knowledgeRef)) {
1693
1049
  warnings.push(`Skipping promote: pending proposal already exists for ${knowledgeRef}`);
1694
1050
  pushSkipReason("promote", op.ref, "promote_pending_proposal_exists");
@@ -1711,9 +1067,7 @@ export async function handlePromoteOp(op, ctx) {
1711
1067
  pushSkipReason("promote", op.ref, "promote_read_failed");
1712
1068
  return;
1713
1069
  }
1714
- // Defensive sanitization: legacy memory files written by older
1715
- // consolidate runs may still carry outer code fences or broken YAML.
1716
- // Strip them here so we never propose a polluted asset.
1070
+ // Validate and normalize source content before proposing a promoted asset.
1717
1071
  const promoteSanitized = sanitizeMergedContent(memoryContent);
1718
1072
  if (!promoteSanitized.ok) {
1719
1073
  warnings.push(`Promote: rejected ${op.ref} — source memory failed sanitization (${promoteSanitized.reason}).`);
@@ -1816,8 +1170,9 @@ export async function handlePromoteOp(op, ctx) {
1816
1170
  pushSkipReason("promote", op.ref, "promote_dedup_window");
1817
1171
  return;
1818
1172
  }
1819
- const proposalResult = emitProposal({ stashDir }, {
1173
+ const proposalResult = emitProposal({ stashDir, proposalsCtx: ctx.proposalsCtx }, {
1820
1174
  ref: knowledgeRef,
1175
+ target: { source: target.source.name, root: target.source.path },
1821
1176
  source: "consolidate",
1822
1177
  sourceRun,
1823
1178
  // §23.6 fingerprint model-id term (WI-6.4).
@@ -1835,56 +1190,14 @@ export async function handlePromoteOp(op, ctx) {
1835
1190
  else {
1836
1191
  promoted.push(proposalResult.id);
1837
1192
  promotedSourceRefs.add(op.ref);
1838
- markJournalCompleted(ctx.txn, op.ref);
1839
1193
  }
1840
1194
  }
1841
1195
  catch (e) {
1196
+ ctx.promotionFailures.count++;
1842
1197
  warnings.push(`Promote: createProposal failed for ${op.ref}: ${String(e)}`);
1843
1198
  pushSkipReason("promote", op.ref, "promote_create_failed");
1844
1199
  }
1845
1200
  }
1846
- /** Execute one `contradict` op (behavior-identical to the former inlined branch). */
1847
- export async function handleContradictOp(op, ctx) {
1848
- const { memoryByRef, warnings, pushSkipReason, counts } = ctx;
1849
- // Confidence gate: surface-level topic overlap causes false positives
1850
- // (investigation 2026-06-18). Require ≥0.92 confidence before writing
1851
- // contradiction edges. Missing confidence field defaults to 1.0 for
1852
- // backward compatibility with responses that predate this field.
1853
- const opConfidence = typeof op.confidence === "number" ? op.confidence : 1.0;
1854
- if (opConfidence < 0.92) {
1855
- warnings.push(`Contradict: confidence ${opConfidence.toFixed(2)} below 0.92 threshold for ${op.ref} <-> ${op.contradictedByRef} — skipping.`);
1856
- pushSkipReason("contradict", op.ref, "contradict_low_confidence");
1857
- return;
1858
- }
1859
- // C-3 / #382: Write contradictedBy edges so resolveFamilyContradictions
1860
- // (the SCC resolver in memory-improve.ts) has edges to work on.
1861
- // Zep arXiv:2501.13956 §3 — unified belief-revision with contradiction edges.
1862
- const entry = memoryByRef.get(op.ref);
1863
- const contradictorEntry = memoryByRef.get(op.contradictedByRef);
1864
- if (!entry) {
1865
- warnings.push(`Contradict: ${op.ref} not found in loaded memories — skipping.`);
1866
- // Phantom ref: not in processed, so no skipReason (same rationale as
1867
- // delete_ref_missing).
1868
- return;
1869
- }
1870
- if (!contradictorEntry) {
1871
- warnings.push(`Contradict: ${op.contradictedByRef} not found — skipping.`);
1872
- // op.ref IS in the batch (entry found above) so the skipReason is
1873
- // correctly charged against a real processed memory.
1874
- pushSkipReason("contradict", op.ref, "contradict_target_missing");
1875
- return;
1876
- }
1877
- try {
1878
- // Write the contradiction edge: op.ref is contradicted by op.contradictedByRef
1879
- writeContradictEdge(entry.filePath, op.contradictedByRef);
1880
- counts.contradicted++;
1881
- markJournalCompleted(ctx.txn, op.ref);
1882
- }
1883
- catch (e) {
1884
- warnings.push(`Contradict: failed to write edge for ${op.ref}: ${String(e)}`);
1885
- pushSkipReason("contradict", op.ref, "contradict_write_failed");
1886
- }
1887
- }
1888
1201
  // ── Helpers ─────────────────────────────────────────────────────────────────
1889
1202
  /**
1890
1203
  * Normalise a knowledge slug for variant-aware deduplication. Collapses:
@@ -1896,7 +1209,7 @@ export async function handleContradictOp(op, ctx) {
1896
1209
  * Two slugs that normalise to the same string are considered the same asset
1897
1210
  * for dedup purposes even if they don't share an exact ref.
1898
1211
  */
1899
- /** The conceptId a proposal ref maps to in EITHER grammar (WI-8.5a), or undefined. */
1212
+ /** The conceptId a proposal ref maps to, or undefined for an invalid ref. */
1900
1213
  function conceptIdForRef(ref) {
1901
1214
  try {
1902
1215
  const p = parseRefInput(ref);
@@ -1906,23 +1219,13 @@ function conceptIdForRef(ref) {
1906
1219
  return undefined;
1907
1220
  }
1908
1221
  }
1909
- /** Is a pending proposal already queued for `conceptRef`'s concept (grammar-independent)? */
1222
+ /** Is a pending proposal already queued for `conceptRef`'s concept? */
1910
1223
  function hasPendingProposalForConcept(stashDir, conceptRef) {
1911
1224
  const want = conceptIdForRef(conceptRef);
1912
1225
  return (want !== undefined && listProposals(stashDir, { status: "pending" }).some((p) => conceptIdForRef(p.ref) === want));
1913
1226
  }
1914
1227
  function normalizeSlugForDedup(ref) {
1915
- // Extract the bare asset name from EITHER grammar (WI-8.5a: proposals.ref is the
1916
- // item_ref `bundle//conceptId`, which carries no `type:` colon — the old
1917
- // `replace(/^[^:]+:/,"")` strip would leave the bundle/type prefix in). The name
1918
- // is identical across grammars, so the dedup slug stays stable across the flip.
1919
- let slug;
1920
- try {
1921
- slug = parseRefInput(ref).name;
1922
- }
1923
- catch {
1924
- slug = ref.replace(/^[^:]+:/, "");
1925
- }
1228
+ const slug = parseRefInput(ref).name;
1926
1229
  const monthRe = /(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)/i;
1927
1230
  const tokens = slug
1928
1231
  .toLowerCase()
@@ -2091,166 +1394,3 @@ function loadMemoriesForSource(source, stashDir, warnings) {
2091
1394
  }
2092
1395
  return memories;
2093
1396
  }
2094
- async function generateMergedContent(config, primaryRef, primaryBody, secondaryRefs, memoryByRef, activeProfile, resolvedLlmConfig) {
2095
- // Only handle single-secondary merges per design (one call per merge op)
2096
- const secRef = secondaryRefs[0];
2097
- const secEntry = secRef !== undefined ? memoryByRef.get(secRef) : undefined;
2098
- if (!secEntry)
2099
- return { error: "merge_read_failed", detail: `secondary ${secRef} not in memoryByRef` };
2100
- let secBody = "";
2101
- try {
2102
- secBody = fs.readFileSync(secEntry.filePath, "utf8");
2103
- }
2104
- catch {
2105
- return { error: "merge_read_failed", detail: `could not read secondary ${secRef}` };
2106
- }
2107
- const primaryFmKeys = Object.keys(parseFrontmatter(primaryBody).data);
2108
- const secFmKeys = Object.keys(parseFrontmatter(secBody).data);
2109
- const requiredFmKeys = [...new Set([...primaryFmKeys, ...secFmKeys])];
2110
- const prompt = [
2111
- "Merge these two memory assets into one. Output ONLY the merged markdown (with YAML frontmatter). Do not explain, do not use code fences.",
2112
- "",
2113
- "## OUTPUT FORMAT (MANDATORY)",
2114
- "Return raw markdown content beginning DIRECTLY with the `---` frontmatter delimiter.",
2115
- "DO NOT wrap your entire response in a code fence.",
2116
- "",
2117
- 'GOOD: "---\\ndescription: ...\\n---\\nBody content."',
2118
- 'BAD: "```markdown\\n---\\ndescription: ...\\n---\\nBody content.\\n```"',
2119
- 'BAD: "```yaml\\n---\\ndescription: ...\\n---\\nBody content.\\n```"',
2120
- "",
2121
- "## FRONTMATTER RULES (MANDATORY)",
2122
- "- The `updated:` field, if present, MUST be a real ISO date (e.g. `updated: 2026-05-20`). NEVER emit `updated: today`, `updated: now`, or `updated: {today: null}`. If you don't have a real date, OMIT the field — the post-processor will not invent one.",
2123
- "- REQUIRED: The merged frontmatter MUST include a `description` field with a concise one-sentence summary of the merged asset's content. If neither source has a `description` field, synthesize one from the content.",
2124
- requiredFmKeys.length > 0
2125
- ? `- CRITICAL: The merged frontmatter MUST include ALL of these keys from both source memories: ${requiredFmKeys.join(", ")}. Do NOT drop any of them.`
2126
- : null,
2127
- "",
2128
- `=== Primary memory (${primaryRef}) ===`,
2129
- primaryBody,
2130
- "",
2131
- `=== Secondary memory (${secRef}) ===`,
2132
- secBody,
2133
- ]
2134
- .filter((line) => line !== null)
2135
- .join("\n");
2136
- // Use the same per-process profile resolution as the chunk-plan call above
2137
- // so the merge generation step doesn't silently revert to the default LLM.
2138
- const llmConfig = resolvedLlmConfig === null ? undefined : (resolvedLlmConfig ?? resolveConsolidateLlmConfig(config, activeProfile));
2139
- // Same envelope semantics as the chunk-plan call above: the gate runs with
2140
- // enabled:true, the missing-LLM guard returns the envelope the gated fn
2141
- // used to return, a transport throw becomes an {ok:false} envelope via
2142
- // onError, and the fallback fires only on wrapper timeout.
2143
- const result = !llmConfig
2144
- ? { ok: false, error: "No LLM configured for consolidation" }
2145
- : await callStructured({
2146
- feature: "memory_consolidation",
2147
- akmConfig: config,
2148
- enabled: true,
2149
- config: llmConfig,
2150
- messages: [{ role: "user", content: prompt }],
2151
- request: { enableThinking: false },
2152
- parse: (raw) => ({ ok: true, content: raw ?? "" }),
2153
- onError: (_cls, e) => ({ ok: false, error: String(e) }),
2154
- fallback: { ok: false, error: `merge content generation failed for ${primaryRef}` },
2155
- });
2156
- if (!result.ok) {
2157
- return {
2158
- error: "merge_transport_failed",
2159
- detail: result.error ?? `merge content generation failed for ${primaryRef}`,
2160
- };
2161
- }
2162
- // Sanitize LLM output: strip outer code fences (defends against the
2163
- // ```markdown … ``` leak observed in production), re-serialise frontmatter
2164
- // through the yaml lib (fixes quote-escaping mistakes), and reject empty
2165
- // or fence-only responses.
2166
- const sanitized = sanitizeMergedContent(result.content ?? "");
2167
- if (!sanitized.ok) {
2168
- const reason = sanitized.reason;
2169
- const isFenceError = reason === "UNBALANCED_CODE_FENCE" ||
2170
- reason === "MISSING_FRONTMATTER_SENTINEL" ||
2171
- reason === "MALFORMED_FRONTMATTER_BLOCK" ||
2172
- reason === "FRONTMATTER_NOT_OBJECT";
2173
- const mergeReason = isFenceError ? "merge_fence_rejected" : "merge_yaml_invalid";
2174
- return { error: mergeReason, detail: `${primaryRef} — ${reason}` };
2175
- }
2176
- const mergedRaw = sanitized.result.content;
2177
- // C-4 / #383: Content-preservation lint (mem0 §3.2, arXiv:2504.19413).
2178
- // Guards against LLM-generated merged content that silently drops information
2179
- // from the source assets. Two checks:
2180
- // 1. Body size: merged body must be >= 50% of the larger source body.
2181
- // 2. Frontmatter superset: merged frontmatter must contain all keys present
2182
- // in both source frontmatters.
2183
- // Failures return a discriminated error so the call site can emit a specific
2184
- // skip-reason key in the histogram.
2185
- try {
2186
- const primaryFm = parseFrontmatter(primaryBody);
2187
- const secFm = parseFrontmatter(secBody);
2188
- const mergedFm = parseFrontmatter(mergedRaw);
2189
- // Check body size — blended floor: max(ratio × largerLen, absoluteFloor).
2190
- // Deduplication is expected, so the ratio is lower than the reflect gate
2191
- // (0.3 vs 0.5). The absolute floor protects very short memory pairs where
2192
- // the ratio alone would produce a near-zero threshold.
2193
- const primaryBodyLen = (primaryFm.content ?? "").trim().length;
2194
- const secBodyLen = (secFm.content ?? "").trim().length;
2195
- const mergedBodyLen = (mergedFm.content ?? "").trim().length;
2196
- const largerBodyLen = Math.max(primaryBodyLen, secBodyLen);
2197
- const mergeFloor = Math.max(MERGE_SHRINK_RATIO_MIN * largerBodyLen, MERGE_ABSOLUTE_FLOOR_CHARS);
2198
- if (largerBodyLen > 0 && mergedBodyLen < mergeFloor) {
2199
- return {
2200
- error: "merge_content_too_short",
2201
- detail: `${primaryRef} — merged body (${mergedBodyLen} chars) is less than floor (${Math.round(mergeFloor)} chars; max(${MERGE_SHRINK_RATIO_MIN}×${largerBodyLen}, ${MERGE_ABSOLUTE_FLOOR_CHARS}))`,
2202
- };
2203
- }
2204
- // Check frontmatter superset — attempt repair before rejecting.
2205
- const primaryKeys = Object.keys(primaryFm.data ?? {});
2206
- const secKeys = Object.keys(secFm.data ?? {});
2207
- const mergedKeys = new Set(Object.keys(mergedFm.data ?? {}));
2208
- const missingKeys = [...new Set([...primaryKeys, ...secKeys])].filter((k) => !mergedKeys.has(k));
2209
- if (missingKeys.length > 0) {
2210
- // Inject missing keys from source FMs. Primary value wins on conflict.
2211
- const repairedFmData = { ...mergedFm.data };
2212
- for (const key of missingKeys) {
2213
- repairedFmData[key] =
2214
- key in primaryFm.data
2215
- ? primaryFm.data[key]
2216
- : secFm.data[key];
2217
- }
2218
- normalizeUpdatedField(repairedFmData);
2219
- const repairedYaml = serializeFrontmatter(repairedFmData);
2220
- const bodyPart = typeof mergedFm.content === "string" ? mergedFm.content : "";
2221
- return { content: assembleAssetFromString(repairedYaml, bodyPart) };
2222
- }
2223
- }
2224
- catch {
2225
- // parseFrontmatter failures are non-fatal — allow the merge to proceed.
2226
- }
2227
- return { content: mergedRaw };
2228
- }
2229
- async function promptConfirm(message) {
2230
- process.stdout.write(message);
2231
- return new Promise((resolve) => {
2232
- let settled = false;
2233
- const done = (answer) => {
2234
- if (settled)
2235
- return;
2236
- settled = true;
2237
- rl.close();
2238
- resolve(answer);
2239
- };
2240
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2241
- rl.once("line", (line) => done(line.trim().toLowerCase() === "y"));
2242
- rl.once("close", () => done(false));
2243
- });
2244
- }
2245
- // Checklist journals are never auto-recovered — recovery is a run-entry
2246
- // decision (`--consolidate-recovery abort|clean`). Registered so any generic
2247
- // engine recovery that reaches one aborts with the same guidance instead of
2248
- // improvising a rollback.
2249
- registerTxnKind(CONSOLIDATE_TXN_KIND, {
2250
- phases: CONSOLIDATE_TXN_PHASES,
2251
- commitPhase: "applying",
2252
- rollback: () => { },
2253
- finalize: (txn) => {
2254
- throw new ConfigError(`Incomplete consolidation run detected at ${txn.journalPath}. Re-run with --consolidate-recovery clean to remove stale journal artifacts.`, "INVALID_CONFIG_FILE");
2255
- },
2256
- });