akm-cli 0.9.16 → 0.9.17-alpha.10

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 (403) hide show
  1. package/CHANGELOG.md +2101 -0
  2. package/STABILITY.md +11 -10
  3. package/dist/akm +124 -193
  4. package/dist/akm-migrate +38 -19
  5. package/dist/assets/hints/cli-hints-full.md +6 -7
  6. package/dist/assets/improve-strategies/catchup.json +0 -3
  7. package/dist/assets/improve-strategies/consolidate.json +0 -1
  8. package/dist/assets/improve-strategies/default.json +1 -2
  9. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -2
  10. package/dist/assets/improve-strategies/quick.json +1 -2
  11. package/dist/assets/improve-strategies/reflect-distill.json +1 -2
  12. package/dist/assets/improve-strategies/thorough.json +0 -3
  13. package/dist/assets/prompts/consolidate-pair.md +20 -0
  14. package/dist/assets/prompts/consolidate-system.md +4 -11
  15. package/dist/assets/prompts/retrieval-relevance-judge.md +6 -0
  16. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +20 -20
  17. package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -2
  18. package/dist/assets/templates/html/health.html +3 -5
  19. package/dist/cli/retired-commands.js +1 -1
  20. package/dist/cli/shared.js +6 -2
  21. package/dist/cli/unknown-flags.js +24 -1
  22. package/dist/cli.js +68 -10
  23. package/dist/commands/agent/agent-dispatch.js +1 -1
  24. package/dist/commands/command/command-execution.js +24 -62
  25. package/dist/commands/feedback-cli.js +0 -1
  26. package/dist/commands/health/accept-rate.js +6 -0
  27. package/dist/commands/health/archive-usage.js +92 -0
  28. package/dist/commands/health/checks.js +83 -74
  29. package/dist/commands/health/config-skew.js +38 -0
  30. package/dist/commands/health/data-dir-usage.js +25 -13
  31. package/dist/commands/health/egress.js +54 -0
  32. package/dist/commands/health/html-report.js +1 -42
  33. package/dist/commands/health/improve-metrics.js +136 -591
  34. package/dist/commands/health/md-report.js +1 -6
  35. package/dist/commands/health/plugin-staleness.js +53 -3
  36. package/dist/commands/health/renderers.js +12 -4
  37. package/dist/commands/health/report-view-model.js +14 -120
  38. package/dist/commands/health/types-improve.js +4 -19
  39. package/dist/commands/health/windows.js +64 -74
  40. package/dist/commands/health.js +145 -143
  41. package/dist/commands/improve/consolidate/chunking.js +26 -117
  42. package/dist/commands/improve/consolidate/continuity-check.js +137 -0
  43. package/dist/commands/improve/consolidate/pair-pass.js +791 -0
  44. package/dist/commands/improve/consolidate/sanitize.js +54 -149
  45. package/dist/commands/improve/consolidate.js +589 -1127
  46. package/dist/commands/improve/content-hash.js +16 -24
  47. package/dist/commands/improve/distill/content-repair.js +18 -100
  48. package/dist/commands/improve/distill-guards.js +20 -81
  49. package/dist/commands/improve/distill-promotion-policy.js +23 -243
  50. package/dist/commands/improve/distill.js +608 -1041
  51. package/dist/commands/improve/eligibility.js +126 -390
  52. package/dist/commands/improve/execution.js +8 -10
  53. package/dist/commands/improve/extract-prompt.js +1 -2
  54. package/dist/commands/improve/extract.js +487 -1046
  55. package/dist/commands/improve/feedback-valence.js +0 -25
  56. package/dist/commands/improve/improve-cli.js +75 -169
  57. package/dist/commands/improve/improve-result-file.js +10 -66
  58. package/dist/commands/improve/improve-strategies.js +52 -4
  59. package/dist/commands/improve/improve-usage-report.js +18 -64
  60. package/dist/commands/improve/improve.js +480 -1074
  61. package/dist/commands/improve/ledger.js +119 -0
  62. package/dist/commands/improve/locks.js +2 -8
  63. package/dist/commands/improve/loop-stages.js +415 -1073
  64. package/dist/commands/improve/memory/derived-ref.js +12 -77
  65. package/dist/commands/improve/memory/memory-belief.js +16 -118
  66. package/dist/commands/improve/memory/memory-improve.js +266 -14
  67. package/dist/commands/improve/outcome-loop.js +28 -156
  68. package/dist/commands/improve/planner.js +5 -15
  69. package/dist/commands/improve/preparation.js +779 -2319
  70. package/dist/commands/improve/proactive-maintenance.js +34 -101
  71. package/dist/commands/improve/reflect-noise.js +104 -280
  72. package/dist/commands/improve/reflect.js +642 -1353
  73. package/dist/commands/improve/retrieval-gate.js +127 -0
  74. package/dist/commands/improve/retrieval-scope.js +92 -0
  75. package/dist/commands/improve/salience.js +41 -240
  76. package/dist/commands/improve/session-asset.js +19 -100
  77. package/dist/commands/improve/stage.js +322 -0
  78. package/dist/commands/lint/base-linter.js +37 -15
  79. package/dist/commands/proposal/drain.js +261 -578
  80. package/dist/commands/proposal/proposal-cli.js +19 -20
  81. package/dist/commands/proposal/proposal-types.js +31 -24
  82. package/dist/commands/proposal/proposal.js +38 -8
  83. package/dist/commands/proposal/propose.js +134 -160
  84. package/dist/commands/proposal/repository.js +1097 -1394
  85. package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
  86. package/dist/commands/proposal/validators/proposal-validators.js +1 -1
  87. package/dist/commands/proposal/validators/proposals.js +22 -89
  88. package/dist/commands/read/curate.js +105 -462
  89. package/dist/commands/read/knowledge.js +3 -2
  90. package/dist/commands/read/search-cli.js +16 -33
  91. package/dist/commands/read/search.js +17 -23
  92. package/dist/commands/read/show.js +57 -108
  93. package/dist/commands/sources/bundle-cli.js +25 -2
  94. package/dist/commands/sources/bundle-config-ops.js +4 -0
  95. package/dist/commands/sources/dangerous-env-audit.js +1 -2
  96. package/dist/commands/sources/info.js +127 -29
  97. package/dist/commands/sources/installed-stashes.js +197 -746
  98. package/dist/commands/sources/schema-repair.js +98 -129
  99. package/dist/commands/sources/source-add.js +62 -12
  100. package/dist/commands/sources/source-manage.js +9 -2
  101. package/dist/commands/sources/stash-cli.js +24 -4
  102. package/dist/commands/tasks/explain.js +10 -13
  103. package/dist/commands/tasks/tasks-cli.js +12 -13
  104. package/dist/commands/tasks/tasks.js +350 -936
  105. package/dist/commands/tasks/validate.js +26 -24
  106. package/dist/commands/workflow/plan.js +22 -29
  107. package/dist/commands/workflow-cli.js +4 -4
  108. package/dist/core/adapter/adapters/akm-adapter.js +2 -1
  109. package/dist/core/adapter/adapters/akm-lint.js +2 -3
  110. package/dist/core/adapter/adapters/akm-metadata.js +42 -12
  111. package/dist/core/adapter/adapters/akm-task-adapter.js +29 -8
  112. package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
  113. package/dist/core/adapter/execution-source.js +17 -29
  114. package/dist/core/asset/asset-placement.js +4 -13
  115. package/dist/core/asset/frontmatter.js +106 -1
  116. package/dist/core/asset/resolve-ref.js +1 -1
  117. package/dist/core/bundle-id.js +42 -5
  118. package/dist/core/bundle-rename.js +285 -0
  119. package/dist/core/config/config-io.js +1 -2
  120. package/dist/core/config/config-schema.js +9 -34
  121. package/dist/core/config/config-walker.js +1 -1
  122. package/dist/core/config/config.js +184 -111
  123. package/dist/core/config/engine-semantics.js +0 -2
  124. package/dist/core/config/legacy-source-shape-shim.js +38 -9
  125. package/dist/core/config/schema/embedding.js +20 -5
  126. package/dist/core/config/schema/engines.js +5 -0
  127. package/dist/core/config/schema/execution.js +1 -1
  128. package/dist/core/config/schema/experimental.js +1 -1
  129. package/dist/core/config/schema/improve-processes.js +54 -125
  130. package/dist/core/config/schema/improve.js +4 -42
  131. package/dist/core/config/schema/index-config.js +9 -48
  132. package/dist/core/config/schema/scheduler.js +12 -12
  133. package/dist/core/config/schema/search.js +6 -22
  134. package/dist/core/env-secret-ref.js +0 -1
  135. package/dist/core/errors.js +8 -9
  136. package/dist/core/file-change.js +13 -5
  137. package/dist/core/file-lock.js +76 -173
  138. package/dist/core/improve-result.js +35 -7
  139. package/dist/core/improve-types.js +0 -1
  140. package/dist/core/logs-db.js +2 -2
  141. package/dist/core/loopback.js +7 -12
  142. package/dist/core/non-task-input.js +20 -0
  143. package/dist/core/parse.js +13 -16
  144. package/dist/core/paths.js +0 -24
  145. package/dist/core/redaction.js +109 -2
  146. package/dist/core/run-lock.js +2 -5
  147. package/dist/core/spawn-env.js +1 -1
  148. package/dist/core/state/migrations.js +123 -61
  149. package/dist/core/state-db-scope.js +2 -4
  150. package/dist/core/state-db.js +126 -692
  151. package/dist/core/time.js +0 -20
  152. package/dist/core/type-presentation.js +1 -9
  153. package/dist/core/write-source.js +294 -1005
  154. package/dist/execution/input-contract.js +1 -1
  155. package/dist/execution/resolved-request.js +135 -689
  156. package/dist/execution/source.js +63 -257
  157. package/dist/execution/target-ref.js +1 -1
  158. package/dist/indexer/bundle-identity-guard.js +2 -2
  159. package/dist/indexer/db/llm-cache.js +2 -2
  160. package/dist/indexer/ensure-index.js +77 -73
  161. package/dist/indexer/index-rebuild-lock.js +3 -11
  162. package/dist/indexer/index-writer-lock.js +8 -17
  163. package/dist/indexer/index-written-assets.js +141 -154
  164. package/dist/indexer/indexer.js +400 -1124
  165. package/dist/indexer/links/declared-links.js +90 -0
  166. package/dist/indexer/materialize-embeddings.js +60 -397
  167. package/dist/indexer/passes/memory-inference.js +96 -90
  168. package/dist/indexer/passes/metadata.js +132 -219
  169. package/dist/indexer/read-preflight.js +0 -7
  170. package/dist/indexer/scan/doc-to-entry.js +2 -3
  171. package/dist/indexer/scan/drain-dir.js +1 -1
  172. package/dist/indexer/search/db-search.js +190 -590
  173. package/dist/indexer/search/fts-query.js +30 -41
  174. package/dist/indexer/search/ranking.js +28 -154
  175. package/dist/indexer/search/search-attribution.js +12 -32
  176. package/dist/indexer/search/search-fields.js +11 -15
  177. package/dist/indexer/search/search-hit-enrichers.js +54 -85
  178. package/dist/indexer/search/search-source.js +1 -4
  179. package/dist/indexer/usage/usage-events.js +36 -7
  180. package/dist/indexer/walk/walker.js +3 -4
  181. package/dist/integrations/agent/engine-fallback.js +23 -40
  182. package/dist/integrations/agent/engine-resolution.js +93 -183
  183. package/dist/integrations/agent/execution.js +507 -0
  184. package/dist/integrations/agent/model-map.js +28 -156
  185. package/dist/integrations/agent/request-lowering.js +66 -141
  186. package/dist/integrations/agent/runner-dispatch.js +143 -321
  187. package/dist/integrations/agent/runner.js +54 -14
  188. package/dist/integrations/lockfile.js +53 -101
  189. package/dist/llm/client.js +18 -6
  190. package/dist/llm/embedders/deterministic.js +2 -3
  191. package/dist/llm/embedders/profile.js +71 -0
  192. package/dist/llm/embedders/remote.js +11 -17
  193. package/dist/llm/feature-gate.js +0 -8
  194. package/dist/llm/index-passes.js +3 -5
  195. package/dist/llm/memory-infer.js +1 -2
  196. package/dist/llm/structured-call.js +5 -24
  197. package/dist/output/generic-render.js +23 -11
  198. package/dist/output/html-render.js +13 -10
  199. package/dist/output/render-registry.js +3 -32
  200. package/dist/output/shapes/helpers.js +25 -38
  201. package/dist/output/shapes/passthrough.js +1 -9
  202. package/dist/{indexer/graph/graph-types.js → output/text/bundle-rename.js} +4 -1
  203. package/dist/output/text/command-format.js +69 -31
  204. package/dist/output/text/helpers.js +1 -1
  205. package/dist/output/text/migrate.js +5 -14
  206. package/dist/output/text/proposal-format.js +48 -3
  207. package/dist/output/text/show-format.js +13 -17
  208. package/dist/output/text/workflow-format.js +0 -32
  209. package/dist/output/text.js +2 -0
  210. package/dist/registry/factory.js +4 -19
  211. package/dist/registry/network.js +66 -220
  212. package/dist/registry/providers/index.js +0 -2
  213. package/dist/registry/providers/skills-sh.js +3 -14
  214. package/dist/registry/providers/static-index.js +24 -26
  215. package/dist/registry/resolve.js +55 -131
  216. package/dist/scripts/akm-migrate-node.js +42948 -92369
  217. package/dist/scripts/akm-migrate.js +42935 -92354
  218. package/dist/setup/registry-stash-loader.js +4 -13
  219. package/dist/setup/semantic-assets.js +3 -44
  220. package/dist/setup/setup.js +1 -1
  221. package/dist/setup/steps/connection.js +5 -6
  222. package/dist/setup/steps/platforms.js +2 -2
  223. package/dist/setup/steps/tasks.js +25 -15
  224. package/dist/sources/provider-factory.js +17 -18
  225. package/dist/sources/providers/filesystem.js +2 -3
  226. package/dist/sources/providers/git-install.js +7 -1
  227. package/dist/sources/providers/git-provider.js +0 -3
  228. package/dist/sources/providers/git-stash.js +83 -21
  229. package/dist/sources/providers/npm.js +2 -4
  230. package/dist/sources/providers/provider-utils.js +5 -10
  231. package/dist/sources/providers/website.js +0 -2
  232. package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
  233. package/dist/sources/website-url.js +2 -2
  234. package/dist/storage/database.js +9 -35
  235. package/dist/storage/repositories/improve-ledger-repository.js +209 -0
  236. package/dist/storage/repositories/index-connection.js +39 -72
  237. package/dist/storage/repositories/index-entries-repository.js +131 -129
  238. package/dist/storage/repositories/index-entry-mapper.js +1 -2
  239. package/dist/storage/repositories/index-entry-schema.js +101 -268
  240. package/dist/storage/repositories/index-fts-repository.js +86 -256
  241. package/dist/storage/repositories/index-links-repository.js +143 -0
  242. package/dist/storage/repositories/index-llm-cache-repository.js +7 -9
  243. package/dist/storage/repositories/index-meta-repository.js +6 -4
  244. package/dist/storage/repositories/index-schema.js +257 -325
  245. package/dist/storage/repositories/index-utility-repository.js +8 -29
  246. package/dist/storage/repositories/index-vec-repository.js +133 -414
  247. package/dist/storage/repositories/outcome-repository.js +2 -1
  248. package/dist/storage/repositories/proposals-repository.js +104 -1
  249. package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
  250. package/dist/storage/repositories/salience-repository.js +1 -19
  251. package/dist/storage/repositories/task-history-repository.js +26 -4
  252. package/dist/storage/repositories/workflow-runs-repository.js +53 -244
  253. package/dist/storage/sqlite-migrations.js +136 -0
  254. package/dist/storage/sqlite-pragmas.js +11 -9
  255. package/dist/storage/sqlite-transaction.js +170 -0
  256. package/dist/storage/state-db-integrity.js +130 -0
  257. package/dist/tasks/activation-config.js +134 -62
  258. package/dist/tasks/backends/cron.js +191 -302
  259. package/dist/tasks/backends/exec-utils.js +2 -5
  260. package/dist/tasks/backends/launchd.js +141 -748
  261. package/dist/tasks/backends/schtasks.js +119 -623
  262. package/dist/tasks/prepare/prepare-support.js +5 -15
  263. package/dist/tasks/prepare/prepare.js +0 -2
  264. package/dist/tasks/resolve-akm-bin.js +20 -79
  265. package/dist/tasks/run/attempt-lifecycle.js +0 -1
  266. package/dist/tasks/run/load-task.js +1 -1
  267. package/dist/tasks/scheduler-binding.js +20 -238
  268. package/dist/tasks/scheduler-invocation.js +136 -244
  269. package/dist/tasks/scheduler-lock.js +53 -0
  270. package/dist/tasks/scheduler-sync.js +368 -679
  271. package/dist/tasks/source/parse-task-source.js +55 -9
  272. package/dist/tasks/source/task-source-v3-frozen.js +3 -4
  273. package/dist/tasks/source/task-to-v4.js +464 -88
  274. package/dist/workflows/authoring/authoring.js +3 -12
  275. package/dist/workflows/compile.js +211 -0
  276. package/dist/workflows/concurrency-policy.js +13 -74
  277. package/dist/workflows/exec/child-invocation.js +3 -17
  278. package/dist/workflows/exec/child-workflow.js +32 -141
  279. package/dist/workflows/exec/dispatch-redaction.js +13 -53
  280. package/dist/workflows/exec/environment.js +98 -0
  281. package/dist/workflows/exec/exec-unit.js +33 -140
  282. package/dist/workflows/exec/frozen-judge.js +7 -59
  283. package/dist/workflows/exec/native-executor.js +82 -341
  284. package/dist/workflows/exec/param-secrets.js +29 -47
  285. package/dist/workflows/exec/run-workflow.js +154 -387
  286. package/dist/workflows/exec/scheduler.js +9 -36
  287. package/dist/workflows/exec/step-work.js +127 -430
  288. package/dist/workflows/exec/unit-dispatch.js +11 -63
  289. package/dist/workflows/exec/unit-writer.js +8 -52
  290. package/dist/workflows/exec/worktree.js +39 -273
  291. package/dist/workflows/freeze/child-output-references.js +4 -15
  292. package/dist/workflows/freeze/environment.js +99 -92
  293. package/dist/workflows/freeze/freeze.js +172 -0
  294. package/dist/workflows/freeze/step-values.js +19 -21
  295. package/dist/workflows/freeze/targets/child-workflow.js +23 -92
  296. package/dist/workflows/freeze/targets/command.js +10 -33
  297. package/dist/workflows/freeze/targets/script.js +5 -12
  298. package/dist/workflows/freeze/targets/shell.js +3 -6
  299. package/dist/workflows/freeze/targets/task.js +25 -80
  300. package/dist/workflows/freeze/task-bindings.js +20 -67
  301. package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
  302. package/dist/workflows/ir/params.js +6 -51
  303. package/dist/workflows/ir/plan-hash.js +2 -34
  304. package/dist/workflows/parser.js +140 -43
  305. package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
  306. package/dist/workflows/renderer.js +36 -69
  307. package/dist/workflows/resource-limits.js +12 -120
  308. package/dist/workflows/runtime/agent-identity.js +8 -40
  309. package/dist/workflows/runtime/run-outputs.js +3 -6
  310. package/dist/workflows/runtime/run-plan.js +316 -0
  311. package/dist/workflows/runtime/runs.js +48 -200
  312. package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
  313. package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
  314. package/dist/workflows/validate-summary.js +2 -7
  315. package/docs/integration/bundling-akm.md +49 -42
  316. package/docs/migration/README.md +1 -0
  317. package/docs/migration/release-notes/0.9.17.md +43 -0
  318. package/docs/migration/v0.9.1-to-v0.9.2.md +23 -7
  319. package/docs/reference/cli.md +232 -135
  320. package/docs/reference/configuration.md +71 -57
  321. package/docs/reference/data-and-telemetry.md +20 -21
  322. package/docs/reference/tasks.md +105 -39
  323. package/docs/reference/workflow-schema.md +14 -18
  324. package/docs/reference/workflows.md +6 -9
  325. package/package.json +1 -1
  326. package/schemas/akm-config.json +115 -738
  327. package/schemas/akm-workflow.json +1 -0
  328. package/dist/assets/improve-strategies/graph-refresh.json +0 -15
  329. package/dist/assets/prompts/contradiction-judge.md +0 -33
  330. package/dist/assets/prompts/graph-extract-system.md +0 -1
  331. package/dist/assets/prompts/graph-extract-user-prompt.md +0 -35
  332. package/dist/assets/prompts/metadata-enhance-system.md +0 -1
  333. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +0 -4
  334. package/dist/commands/health/advisories.js +0 -150
  335. package/dist/commands/health/metrics.js +0 -329
  336. package/dist/commands/health/surfaces.js +0 -102
  337. package/dist/commands/improve/anti-collapse.js +0 -83
  338. package/dist/commands/improve/collapse-detector.js +0 -432
  339. package/dist/commands/improve/consolidate/eligibility.js +0 -48
  340. package/dist/commands/improve/consolidate/merge.js +0 -149
  341. package/dist/commands/improve/distill/promote-memory.js +0 -291
  342. package/dist/commands/improve/distill/quality-gate.js +0 -337
  343. package/dist/commands/improve/eval-cases.js +0 -52
  344. package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
  345. package/dist/commands/improve/proposal-envelope.js +0 -31
  346. package/dist/commands/improve/run-context.js +0 -123
  347. package/dist/commands/improve/shared.js +0 -31
  348. package/dist/commands/improve/source-identity.js +0 -28
  349. package/dist/commands/improve/triage.js +0 -96
  350. package/dist/commands/proposal/drain-policies.js +0 -151
  351. package/dist/commands/sources/update-transaction.js +0 -220
  352. package/dist/core/action-contributors.js +0 -28
  353. package/dist/core/config/config-version-shim.js +0 -101
  354. package/dist/core/fs-txn.js +0 -405
  355. package/dist/core/lexical-score.js +0 -25
  356. package/dist/core/maintenance-barrier.js +0 -167
  357. package/dist/execution/executable-identity.js +0 -105
  358. package/dist/execution/guarded-source.js +0 -427
  359. package/dist/indexer/db/graph-db.js +0 -444
  360. package/dist/indexer/graph/graph-boost.js +0 -427
  361. package/dist/indexer/graph/graph-dedup.js +0 -95
  362. package/dist/indexer/graph/graph-extraction.js +0 -1108
  363. package/dist/indexer/search/name-match.js +0 -35
  364. package/dist/indexer/search/ranking-contributors.js +0 -515
  365. package/dist/indexer/search/ranking-types.js +0 -4
  366. package/dist/indexer/walk/project-context.js +0 -192
  367. package/dist/integrations/agent/execution-cascade.js +0 -566
  368. package/dist/integrations/agent/execution-definitions.js +0 -202
  369. package/dist/integrations/agent/execution-lowering.js +0 -841
  370. package/dist/integrations/agent/execution-preparation.js +0 -98
  371. package/dist/integrations/agent/inline-execution.js +0 -74
  372. package/dist/llm/graph-extract.js +0 -728
  373. package/dist/llm/metadata-enhance.js +0 -96
  374. package/dist/registry/create-provider-registry.js +0 -29
  375. package/dist/registry/pinned-request-helper.js +0 -247
  376. package/dist/registry/pinned-transport.js +0 -717
  377. package/dist/sources/providers/index.js +0 -14
  378. package/dist/storage/engines/sqlite-migrations.js +0 -271
  379. package/dist/storage/repositories/canaries-repository.js +0 -107
  380. package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
  381. package/dist/storage/repositories/registry-cache.js +0 -113
  382. package/dist/tasks/scheduler-sync-preview.js +0 -52
  383. package/dist/tasks/source/task-to-v3.js +0 -507
  384. package/dist/workflows/freeze/resolve-steps.js +0 -86
  385. package/dist/workflows/freeze/source-freeze.js +0 -64
  386. package/dist/workflows/ir/compile.js +0 -321
  387. package/dist/workflows/ir/environment-v4.js +0 -330
  388. package/dist/workflows/ir/freeze-v4.js +0 -153
  389. package/dist/workflows/ir/schema-v4.js +0 -745
  390. package/dist/workflows/ir/schema.js +0 -354
  391. package/dist/workflows/program/schema.js +0 -77
  392. package/dist/workflows/runtime/checkin.js +0 -57
  393. package/dist/workflows/runtime/plan-classifier.js +0 -196
  394. package/dist/workflows/runtime/unit-checkin.js +0 -45
  395. package/dist/workflows/runtime/unit-phases.js +0 -20
  396. package/dist/workflows/schema.js +0 -4
  397. package/dist/workflows/source-ir/compile.js +0 -200
  398. package/dist/workflows/source-ir/program.js +0 -50
  399. package/dist/workflows/source-ir/result.js +0 -26
  400. package/dist/workflows/source-ir/schema.js +0 -786
  401. package/dist/workflows/source-ir/triggers.js +0 -79
  402. package/dist/workflows/source-ir/uses.js +0 -40
  403. package/dist/workflows/validator.js +0 -60
@@ -0,0 +1,791 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * The consolidate pair pass (0.9.17-alpha.9 plan §5.2, brief §A): runs
6
+ * alongside the existing promote pass inside `akmConsolidate`, over new and
7
+ * changed memory-tier assets. Judges each near-duplicate/superseding pair
8
+ * with the calibrated relation prompt and mints a reviewed `retire` proposal
9
+ * for the `duplicate` / `subsumed` / `supersedes` classes — never merges,
10
+ * never writes belief edges for `contradicts`, and is never auto-accepted by
11
+ * triage (`drain.ts`).
12
+ *
13
+ * Calibration (owner grades, 2026-09-28; see the plan's "Human calibration,
14
+ * O5" section): the combined retire class (duplicate ∪ subsumed ∪
15
+ * supersedes) is 20/22 = 0.91 precision against the owner. `T_PAIR` is 0.93,
16
+ * not the initial 0.90, because the 0.90–0.93 band alone graded 3/4 (R1).
17
+ *
18
+ * Initiator eligibility and dating (post-review, alpha.9): "created" is the
19
+ * asset's git first-add time (one `git log` per run, {@link loadGitFirstAddedMap}),
20
+ * not frontmatter or mtime — mtime is only the fallback for a file git does
21
+ * not know. An initiator is eligible when it has no prior `consolidate-pair`
22
+ * ledger row, or its current body hash differs from the row's recorded one;
23
+ * that row is written only once ALL of the initiator's own candidates were
24
+ * judged this run (a run capped mid-way through its candidates leaves it
25
+ * without a row, so the next run picks it back up) — see
26
+ * {@link selectInitiators} and the ledger-write step in
27
+ * {@link runConsolidatePairPass}.
28
+ */
29
+ import fs from "node:fs";
30
+ import path from "node:path";
31
+ import consolidatePairPrompt from "../../../assets/prompts/consolidate-pair.md" with { type: "text" };
32
+ import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
33
+ import { conceptIdFromTypeName } from "../../../core/asset/resolve-ref.js";
34
+ import { asNonEmptyString } from "../../../core/common.js";
35
+ import { concurrentMap } from "../../../core/concurrent.js";
36
+ import { parseEmbeddedJsonResponse } from "../../../core/parse.js";
37
+ import { DERIVED_SUFFIX } from "../../../core/recognition-util.js";
38
+ import { warnOnce } from "../../../core/warn.js";
39
+ import { assertRunnerCredentials } from "../../../integrations/agent/runner-dispatch.js";
40
+ import { runGit } from "../../../sources/providers/git-install.js";
41
+ import { closeDatabase, openExistingDatabase, openReadonlyExistingDatabase, } from "../../../storage/repositories/index-connection.js";
42
+ import { getAllEntries, getEntryById } from "../../../storage/repositories/index-entries-repository.js";
43
+ import { getNeighborsByEntryId } from "../../../storage/repositories/index-vec-repository.js";
44
+ import { isRetireProposal } from "../../proposal/proposal-types.js";
45
+ import { createRetireProposal, listProposalsReadOnly } from "../../proposal/repository.js";
46
+ import { isHotCapturedMemory } from "../consolidate.js";
47
+ import { contentHash, stripFrontmatterBody } from "../content-hash.js";
48
+ import { loadLedgerSnapshot, PAIR_PASS_LEDGER_SOURCE, recordLedgerAttempt, stripBundle } from "../ledger.js";
49
+ import { isInRetrievalScope, loadRetrievalScope } from "../retrieval-scope.js";
50
+ import { callStage } from "../stage.js";
51
+ import { checkRetirementContinuity, createContinuitySearch } from "./continuity-check.js";
52
+ export { PAIR_PASS_LEDGER_SOURCE };
53
+ /** Neighbours fetched per initiator before filtering (S2: wide enough that self/twin/bundle/tier misses rarely starve the kept 5 below). */
54
+ export const PAIR_NEIGHBOR_FETCH_K = 20;
55
+ /** Passing candidates kept per initiator, nearest-cosine-first. */
56
+ export const PAIR_NEIGHBOR_K = 5;
57
+ /** Calibrated floor (R1): the 0.90–0.93 band alone graded 3/4 against the owner. */
58
+ export const T_PAIR = 0.93;
59
+ /** O2: the existing backlog (an initiator with no prior pair-pass attempt) goes >= 0.95 first. */
60
+ export const BACKFILL_FLOOR = 0.95;
61
+ /**
62
+ * An initiator git first-added within this many days judges at `T_PAIR` even
63
+ * with no prior ledger row: new material earns the same scrutiny as an edit,
64
+ * not the higher bar reserved for working through the pre-existing backlog.
65
+ */
66
+ export const NEW_MATERIAL_DAYS = 7;
67
+ /** Pairs judged per run, highest cosine first (plan §7's nightly cost budget). */
68
+ export const MAX_PAIRS_PER_RUN = 300;
69
+ /** Body characters sent to the judge per side (plan §4.3: bodies were truncated at this length for calibration). */
70
+ const PAIR_BODY_TRUNCATE_CHARS = 2500;
71
+ const MS_PER_DAY = 86_400_000;
72
+ const RELATION_LABELS = ["duplicate", "subsumed", "supersedes", "contradicts", "overlap", "unrelated"];
73
+ const RETIRE_LABELS = new Set(["duplicate", "subsumed", "supersedes"]);
74
+ /** The classes that mint a `retire` proposal (the calibrated combined retire class, 20/22 precision). */
75
+ function isRetireLabel(label) {
76
+ return RETIRE_LABELS.has(label);
77
+ }
78
+ const PAIR_JUDGE_JSON_SCHEMA = {
79
+ type: "object",
80
+ required: ["relation", "redundant", "stale", "confidence", "reason"],
81
+ additionalProperties: false,
82
+ properties: {
83
+ relation: { type: "string", enum: [...RELATION_LABELS] },
84
+ redundant: { type: ["string", "null"], enum: ["A", "B", null] },
85
+ stale: { type: ["string", "null"], enum: ["A", null] },
86
+ confidence: { type: "number", minimum: 0, maximum: 1 },
87
+ reason: { type: "string", maxLength: 400 },
88
+ },
89
+ };
90
+ /** Hand-validates the judge's JSON, independent of whatever the provider's own schema enforcement did. */
91
+ export function parsePairJudgeResponse(raw) {
92
+ const parsed = parseEmbeddedJsonResponse(raw);
93
+ if (!parsed)
94
+ return undefined;
95
+ if (typeof parsed.relation !== "string" || !RELATION_LABELS.includes(parsed.relation)) {
96
+ return undefined;
97
+ }
98
+ const redundant = parsed.redundant;
99
+ if (redundant !== "A" && redundant !== "B" && redundant !== null)
100
+ return undefined;
101
+ if (typeof parsed.confidence !== "number" || !Number.isFinite(parsed.confidence))
102
+ return undefined;
103
+ const confidence = Math.max(0, Math.min(1, parsed.confidence));
104
+ const reason = typeof parsed.reason === "string" ? parsed.reason : "";
105
+ return { relation: parsed.relation, redundant, confidence, reason };
106
+ }
107
+ function isFlatName(name) {
108
+ return !name.includes("/");
109
+ }
110
+ /**
111
+ * True only for a memory's own direct `.derived` child or parent — never a
112
+ * sibling or an unrelated pair. @internal exported for unit tests.
113
+ */
114
+ export function isOwnTwinOrParent(a, b) {
115
+ if (a.type !== "memory" || b.type !== "memory")
116
+ return false;
117
+ return a.name === `${b.name}${DERIVED_SUFFIX}` || b.name === `${a.name}${DERIVED_SUFFIX}`;
118
+ }
119
+ /**
120
+ * Every asset eligible to be a pair-pass initiator or candidate: memory (any
121
+ * depth), flat knowledge, or a lesson. @internal exported for unit tests.
122
+ */
123
+ export function loadPairPassPool(db, bundleId) {
124
+ const assets = [];
125
+ for (const e of getAllEntries(db, "memory")) {
126
+ if (e.bundleId !== bundleId || !fs.existsSync(e.filePath))
127
+ continue;
128
+ assets.push({
129
+ ref: conceptIdFromTypeName("memory", e.entry.name),
130
+ type: "memory",
131
+ name: e.entry.name,
132
+ filePath: e.filePath,
133
+ entryId: e.id,
134
+ });
135
+ }
136
+ for (const e of getAllEntries(db, "knowledge")) {
137
+ if (e.bundleId !== bundleId || !isFlatName(e.entry.name) || !fs.existsSync(e.filePath))
138
+ continue;
139
+ assets.push({
140
+ ref: conceptIdFromTypeName("knowledge", e.entry.name),
141
+ type: "knowledge",
142
+ name: e.entry.name,
143
+ filePath: e.filePath,
144
+ entryId: e.id,
145
+ });
146
+ }
147
+ for (const e of getAllEntries(db, "lesson")) {
148
+ if (e.bundleId !== bundleId || !fs.existsSync(e.filePath))
149
+ continue;
150
+ assets.push({
151
+ ref: conceptIdFromTypeName("lesson", e.entry.name),
152
+ type: "lesson",
153
+ name: e.entry.name,
154
+ filePath: e.filePath,
155
+ entryId: e.id,
156
+ });
157
+ }
158
+ return assets;
159
+ }
160
+ // ── B1: created/updated dates from git, not frontmatter/mtime ──────────────
161
+ /** A path relative to `stashDir`, POSIX-separated — how `git log --name-only` spells it. */
162
+ function repoRelativeKey(stashDir, filePath) {
163
+ return path.relative(stashDir, filePath).replace(/\\/g, "/");
164
+ }
165
+ /** 16 MiB — see the buffer-overflow comment inside {@link loadGitFirstAddedMap}. */
166
+ const GIT_LOG_MAX_BUFFER = 16 * 1024 * 1024;
167
+ /**
168
+ * Every tracked path's first-add time (unix ms), from one `git log` over the
169
+ * whole bundle (~2.1s measured against the owner's real bundle) — never
170
+ * shelled out per pair or per initiator. Follows renames (`-M
171
+ * --diff-filter=AR`, oldest-first via `--reverse`): a renamed path inherits
172
+ * its pre-rename first-add time, not the rename's own timestamp — the owner's
173
+ * 2026-08-24 bulk rename alone re-dated 966 files under `--no-renames`, and
174
+ * 11% of real candidate pairs flipped which side counted as older. A raised
175
+ * `diff.renameLimit` keeps a large bulk-rename commit (exactly this
176
+ * scenario) from silently falling back to detecting no renames at all.
177
+ * `undefined` when `stashDir` is not itself a git root (no `.git` directly
178
+ * inside it): every asset then falls back to mtime in {@link createdMsOf},
179
+ * one fallback code path instead of a second git-aware one for a bundle
180
+ * nested inside a larger repo.
181
+ */
182
+ export function loadGitFirstAddedMap(stashDir) {
183
+ if (!fs.existsSync(path.join(stashDir, ".git")))
184
+ return undefined;
185
+ let result;
186
+ try {
187
+ result = runGit(["-c", "diff.renameLimit=20000", "log", "--reverse", "-M", "--diff-filter=AR", "--name-status", "--format=@%ct"], {
188
+ cwd: stashDir,
189
+ // spawnSync's default maxBuffer (1 MB) is too small for a bundle
190
+ // with real history — the owner's real bundle alone prints
191
+ // multiple MB here (migration-tool.ts's own git subprocess call
192
+ // uses the same 16 MiB figure). Silently exceeding it looks
193
+ // identical to "git failed" from the caller's side (status stays
194
+ // non-zero) — checked explicitly below instead of folded into the
195
+ // same silent fallback as "no .git", since raising the buffer
196
+ // again is an actual fix and worth telling the operator about.
197
+ maxBuffer: GIT_LOG_MAX_BUFFER,
198
+ });
199
+ }
200
+ catch {
201
+ return undefined;
202
+ }
203
+ if (result.error) {
204
+ const code = result.error.code;
205
+ if (code === "ENOBUFS" || /maxBuffer/i.test(result.error.message ?? "")) {
206
+ warnOnce("pair-pass-git-log-maxbuffer", `[consolidate] pair pass: git log for first-add dates in ${stashDir} exceeded its ${GIT_LOG_MAX_BUFFER / (1024 * 1024)} MiB buffer — dates fall back to mtime this run.`);
207
+ }
208
+ return undefined;
209
+ }
210
+ if (result.status !== 0 || typeof result.stdout !== "string")
211
+ return undefined;
212
+ // Oldest-first (--reverse): the FIRST time a path is seen, whether as a
213
+ // plain add or as a rename's destination, IS its true first-add time — no
214
+ // backward walk needed. A rename's destination inherits the source's
215
+ // already-recorded time (or, failing that — the source itself predates
216
+ // this log's window — this commit's own time).
217
+ const firstAdd = new Map();
218
+ let currentMs;
219
+ for (const line of result.stdout.split("\n")) {
220
+ if (line.startsWith("@")) {
221
+ const sec = Number(line.slice(1));
222
+ currentMs = Number.isFinite(sec) ? sec * 1000 : undefined;
223
+ continue;
224
+ }
225
+ if (currentMs === undefined)
226
+ continue;
227
+ const tab = line.indexOf("\t");
228
+ if (tab < 0)
229
+ continue;
230
+ const status = line.slice(0, tab);
231
+ if (status === "A") {
232
+ const p = line.slice(tab + 1).trim();
233
+ if (p && !firstAdd.has(p))
234
+ firstAdd.set(p, currentMs);
235
+ }
236
+ else if (status.startsWith("R")) {
237
+ const rest = line.slice(tab + 1);
238
+ const tab2 = rest.indexOf("\t");
239
+ if (tab2 < 0)
240
+ continue;
241
+ const oldPath = rest.slice(0, tab2).trim();
242
+ const newPath = rest.slice(tab2 + 1).trim();
243
+ if (oldPath && newPath && !firstAdd.has(newPath)) {
244
+ firstAdd.set(newPath, firstAdd.get(oldPath) ?? currentMs);
245
+ }
246
+ }
247
+ }
248
+ return firstAdd;
249
+ }
250
+ /** Created instant (ms): git first-add when known, else file mtime (B1) — the one fallback both dating and the S1 new-material check use. */
251
+ function createdMsOf(asset, gitFirstAdded, stashDir) {
252
+ const known = gitFirstAdded?.get(repoRelativeKey(stashDir, asset.filePath));
253
+ if (known !== undefined)
254
+ return known;
255
+ try {
256
+ return fs.statSync(asset.filePath).mtimeMs;
257
+ }
258
+ catch {
259
+ return 0;
260
+ }
261
+ }
262
+ /** An unordered pair's dedup key, so a pair reachable from either side is judged once. */
263
+ function pairKey(a, b) {
264
+ return [a, b].sort().join("\u0000");
265
+ }
266
+ /**
267
+ * A rejected retirement's dedup key (item 0): the exact ref pair plus both
268
+ * content hashes at judge time, so a re-selected initiator (its own ledger
269
+ * row missing because a sibling candidate was dropped or failed, not because
270
+ * this pair changed) does not get this same, already-rejected pair re-judged
271
+ * into a new proposal.
272
+ */
273
+ function rejectedPairKey(retiredRef, successorRef, retiredHash, successorHash) {
274
+ return [retiredRef, successorRef, retiredHash, successorHash].join("\u0000");
275
+ }
276
+ /**
277
+ * Initiators (plan §5.2 step 1, S1 post-review): the pool, in the retrieval
278
+ * scope, and content-eligible — no prior `consolidate-pair` ledger row, or a
279
+ * row whose recorded body hash differs from the asset's current one. A row's
280
+ * `next_eligible_at` is never consulted (the pair pass's own source carries
281
+ * no timer at all — see `windowDays` in improve-ledger-repository.ts):
282
+ * eligibility here is purely a function of content, matching the brief's "no
283
+ * row, or changed" rule and the ledger-write step this run finishes with.
284
+ */
285
+ export function selectInitiators(pool, opts, stashDir, gitFirstAdded) {
286
+ const retrievalScope = loadRetrievalScope({ proposalsCtx: opts.proposalsCtx }, stashDir);
287
+ const ledger = loadLedgerSnapshot({ proposalsCtx: opts.proposalsCtx }, stashDir, [PAIR_PASS_LEDGER_SOURCE]);
288
+ const nowMs = (opts.proposalsCtx?.now ?? Date.now)();
289
+ const initiators = [];
290
+ for (const asset of pool) {
291
+ if (!isInRetrievalScope(retrievalScope, asset.ref, asset.filePath))
292
+ continue;
293
+ const row = ledger.get(`${PAIR_PASS_LEDGER_SOURCE}\0${asset.ref}`);
294
+ let raw;
295
+ try {
296
+ raw = fs.readFileSync(asset.filePath, "utf8");
297
+ }
298
+ catch {
299
+ continue; // unreadable: selectCandidates/judgeOne would skip it anyway
300
+ }
301
+ const bodyHash = contentHash(raw, "body");
302
+ if (row && row.contentHash === bodyHash)
303
+ continue; // unchanged since the last full attempt: not eligible
304
+ const createdMs = createdMsOf(asset, gitFirstAdded, stashDir);
305
+ const newMaterial = nowMs - createdMs <= NEW_MATERIAL_DAYS * MS_PER_DAY;
306
+ initiators.push({ ...asset, backlog: row === undefined, newMaterial, bodyHash });
307
+ }
308
+ return { initiators };
309
+ }
310
+ /**
311
+ * Candidates (plan §5.2 step 2, S1/S2 post-review): each initiator's nearest
312
+ * neighbours, filtered and thresholded — the FULL set, sorted by cosine
313
+ * descending, uncapped. `runConsolidatePairPass` applies `MAX_PAIRS_PER_RUN`
314
+ * (needing the uncapped per-initiator totals to tell a cap-cut initiator
315
+ * apart from a fully-judged one). S2: fetches `PAIR_NEIGHBOR_FETCH_K` (20)
316
+ * raw neighbours and keeps the first `PAIR_NEIGHBOR_K` (5) that clear every
317
+ * filter, so a few self/twin/bundle/tier misses in the raw top-5 no longer
318
+ * starve an initiator down to zero real candidates.
319
+ */
320
+ export function selectCandidates(db, initiators, bundleId) {
321
+ const candidates = [];
322
+ const seenPairs = new Set();
323
+ for (const initiator of initiators) {
324
+ // S1: a changed-content or new-material initiator judges at T_PAIR; the
325
+ // rest of the backlog (no row, not recently git-added) needs the higher
326
+ // BACKFILL_FLOOR.
327
+ const floor = initiator.backlog && !initiator.newMaterial ? BACKFILL_FLOOR : T_PAIR;
328
+ let kept = 0;
329
+ for (const hit of getNeighborsByEntryId(db, initiator.entryId, PAIR_NEIGHBOR_FETCH_K)) {
330
+ if (kept >= PAIR_NEIGHBOR_K)
331
+ break;
332
+ if (hit.id === initiator.entryId)
333
+ continue;
334
+ const entry = getEntryById(db, hit.id);
335
+ if (!entry || entry.bundleId !== bundleId)
336
+ continue;
337
+ if (entry.entry.type !== "memory" && entry.entry.type !== "knowledge" && entry.entry.type !== "lesson")
338
+ continue;
339
+ if (entry.entry.type === "knowledge" && !isFlatName(entry.entry.name))
340
+ continue;
341
+ if (!fs.existsSync(entry.filePath))
342
+ continue;
343
+ const other = {
344
+ ref: conceptIdFromTypeName(entry.entry.type, entry.entry.name),
345
+ type: entry.entry.type,
346
+ name: entry.entry.name,
347
+ filePath: entry.filePath,
348
+ entryId: hit.id,
349
+ };
350
+ if (other.ref === initiator.ref || isOwnTwinOrParent(initiator, other))
351
+ continue;
352
+ // distance = sqrt(2 * (1 - cosine)) (index-vec-repository.ts) — invert it back to cosine.
353
+ const cosine = 1 - (hit.distance * hit.distance) / 2;
354
+ if (cosine < floor)
355
+ continue;
356
+ const key = pairKey(initiator.ref, other.ref);
357
+ if (seenPairs.has(key))
358
+ continue;
359
+ seenPairs.add(key);
360
+ candidates.push({ initiator, other, cosine });
361
+ kept++;
362
+ }
363
+ }
364
+ candidates.sort((a, b) => b.cosine - a.cosine);
365
+ return candidates;
366
+ }
367
+ function loadSide(asset, gitFirstAdded, stashDir) {
368
+ let raw;
369
+ try {
370
+ raw = fs.readFileSync(asset.filePath, "utf8");
371
+ }
372
+ catch {
373
+ return undefined;
374
+ }
375
+ let frontmatter;
376
+ try {
377
+ frontmatter = parseFrontmatter(raw).data;
378
+ }
379
+ catch {
380
+ frontmatter = {};
381
+ }
382
+ // B1: created is the git first-add time (mtime only when git does not know
383
+ // the file, or the bundle has none) — frontmatter createdAt/created is not
384
+ // consulted; too few real assets carry it to be a reliable ordering.
385
+ // Updated stays frontmatter `updated` when present, else falls back to created.
386
+ const createdIso = new Date(createdMsOf(asset, gitFirstAdded, stashDir)).toISOString();
387
+ return {
388
+ asset,
389
+ frontmatter,
390
+ raw,
391
+ createdIso,
392
+ updatedIso: asNonEmptyString(frontmatter.updated) ?? createdIso,
393
+ };
394
+ }
395
+ function sideSection(label, side) {
396
+ return [
397
+ `Asset ${label}:`,
398
+ `Ref: ${side.asset.ref}`,
399
+ `Type: ${side.asset.type}`,
400
+ `Created: ${side.createdIso}`,
401
+ `Updated: ${side.updatedIso}`,
402
+ `Description: ${asNonEmptyString(side.frontmatter.description) ?? "(none)"}`,
403
+ "Content:",
404
+ "```",
405
+ stripFrontmatterBody(side.raw).slice(0, PAIR_BODY_TRUNCATE_CHARS),
406
+ "```",
407
+ "",
408
+ ];
409
+ }
410
+ /** Orders two loaded sides by date (older/newer) for the "A (older)"/"B (newer)" labelling the prompt requires. */
411
+ function orderByAge(x, y) {
412
+ const xMs = Date.parse(x.createdIso);
413
+ const yMs = Date.parse(y.createdIso);
414
+ const xIsOlder = Number.isFinite(xMs) && Number.isFinite(yMs) ? xMs <= yMs : true;
415
+ return xIsOlder ? { older: x, newer: y } : { older: y, newer: x };
416
+ }
417
+ /** The user message: dates decide "A (older)" / "B (newer)" (plan Appendix A), matching the calibration sample's own ordering. */
418
+ function buildPairUserPrompt(older, newer) {
419
+ return [...sideSection("A (older)", older), ...sideSection("B (newer)", newer)].join("\n");
420
+ }
421
+ /** The tombstone-vocabulary reason a judge label maps to (`supersedes` -> `superseded`; the rest unchanged). */
422
+ export function tombstoneReason(label) {
423
+ return label === "supersedes" ? "superseded" : label;
424
+ }
425
+ /**
426
+ * The calibrated outcome table (owner grades, replacing plan §5.2's
427
+ * "shorter body" rule): `duplicate`/`supersedes` keep the newer copy;
428
+ * `subsumed` keeps the side the judge did NOT name `redundant` (no proposal
429
+ * if that pointer is missing or invalid).
430
+ */
431
+ export function decideRetirement(label, redundant, older, newer) {
432
+ if (label === "duplicate" || label === "supersedes")
433
+ return { retired: older, successor: newer };
434
+ if (label === "subsumed") {
435
+ if (redundant === "A")
436
+ return { retired: older, successor: newer };
437
+ if (redundant === "B")
438
+ return { retired: newer, successor: older };
439
+ return undefined; // the judge's pointer is missing or invalid — no proposal
440
+ }
441
+ return undefined;
442
+ }
443
+ /**
444
+ * One pair: judge it, then (for a retire class) apply the guards and mint
445
+ * the proposal. Never throws — a failure is counted in `failedJudgments` or
446
+ * pushed to `warnings`, never lost silently and never aborting the run.
447
+ */
448
+ async function judgeOne(ctx, candidate) {
449
+ const initiatorSide = loadSide(candidate.initiator, ctx.gitFirstAdded, ctx.stashDir);
450
+ const otherSide = loadSide(candidate.other, ctx.gitFirstAdded, ctx.stashDir);
451
+ if (!initiatorSide || !otherSide)
452
+ return { failed: false }; // unreadable since selection — skip, not a judge failure
453
+ // Item 0 / S1: this exact pair (same two refs, same two content hashes,
454
+ // EITHER orientation) was already judged and rejected OR reverted. Checked
455
+ // HERE, before the judge call, not after — the judge (not yet run) is what
456
+ // decides which side would be "retired" this time, so both orientations
457
+ // are checked against the current content hashes rather than waiting for
458
+ // a verdict to pick one. A settled pair therefore costs no LLM call.
459
+ const initiatorHash = contentHash(initiatorSide.raw, "body");
460
+ const otherHash = contentHash(otherSide.raw, "body");
461
+ if (ctx.rejectedPairKeys.has(rejectedPairKey(initiatorSide.asset.ref, otherSide.asset.ref, initiatorHash, otherHash)) ||
462
+ ctx.rejectedPairKeys.has(rejectedPairKey(otherSide.asset.ref, initiatorSide.asset.ref, otherHash, initiatorHash))) {
463
+ return { failed: false };
464
+ }
465
+ const { older, newer } = orderByAge(initiatorSide, otherSide);
466
+ const outcome = await callStage({
467
+ feature: "memory_consolidation",
468
+ runner: ctx.llmRunner,
469
+ system: consolidatePairPrompt,
470
+ prompt: buildPairUserPrompt(older, newer),
471
+ gate: { config: ctx.config, enabled: true },
472
+ request: {
473
+ responseSchema: PAIR_JUDGE_JSON_SCHEMA,
474
+ enableThinking: false,
475
+ timeoutMs: ctx.llmRunner.timeoutMs,
476
+ signal: ctx.opts.signal,
477
+ ...(ctx.chat ? { chat: ctx.chat } : {}),
478
+ },
479
+ ...(ctx.opts.onNotices ? { onNotices: ctx.opts.onNotices } : {}),
480
+ });
481
+ if (!outcome.ok)
482
+ return { failed: true };
483
+ const verdict = parsePairJudgeResponse(outcome.raw);
484
+ if (!verdict)
485
+ return { failed: true };
486
+ ctx.labelCounts[verdict.relation]++;
487
+ if (verdict.relation === "contradicts")
488
+ return { failed: false }; // counted; stays human — no proposal, no belief write
489
+ if (!isRetireLabel(verdict.relation))
490
+ return { failed: false }; // overlap / unrelated: judged_no_action
491
+ const decision = decideRetirement(verdict.relation, verdict.redundant, older, newer);
492
+ if (!decision)
493
+ return { failed: false };
494
+ const { retired, successor } = decision;
495
+ // Guards (plan §5.2 step 4 / brief §A "Guards").
496
+ if (retired.asset.type === "memory" && isHotCapturedMemory(retired.asset.filePath)) {
497
+ return { failed: false }; // never propose retiring a captureMode: hot memory — leave the pair alone
498
+ }
499
+ if (retired.asset.type === "memory" && retired.asset.name.endsWith(DERIVED_SUFFIX)) {
500
+ // S3: derive the parent path from the FULL file path, not from name +
501
+ // dirname — for a subfolder memory (e.g. memories/sub/foo.derived) the
502
+ // name already carries "sub/", so joining dirname(filePath) (which ALSO
503
+ // ends in "sub") with it used to double the subfolder segment.
504
+ const parentPath = retired.asset.filePath.replace(/\.derived\.md$/, ".md");
505
+ if (fs.existsSync(parentPath))
506
+ return { failed: false }; // never retire a .derived memory whose parent still exists
507
+ }
508
+ const retiredHash = contentHash(retired.raw, "body");
509
+ const successorHash = contentHash(successor.raw, "body");
510
+ // B2: an asset retired (or already spent as a successor) earlier in this
511
+ // run cannot be retired or reused as a successor again — the same-run half
512
+ // of the chain guard (the accept-time hash/existence check is the other,
513
+ // durable half).
514
+ const retiredKey = stripBundle(retired.asset.ref);
515
+ const successorKey = stripBundle(successor.asset.ref);
516
+ // Must-fix 1 (third review round): a retire-worthy verdict the same-run
517
+ // chain guard drops is not a settled "no action" — a LATER run, once the
518
+ // conflicting retirement has been reviewed, may well mint it. Counted as
519
+ // failed so its initiator gets no row (real data: night 1 alone judged 177
520
+ // duplicate verdicts into only 59 proposals — 118 silently abandoned).
521
+ if (ctx.retiredThisRun.has(retiredKey) || ctx.retiredThisRun.has(successorKey))
522
+ return { failed: true };
523
+ ctx.retiredThisRun.add(retiredKey);
524
+ ctx.retiredThisRun.add(successorKey);
525
+ const reason = tombstoneReason(verdict.relation);
526
+ if (ctx.opts.dryRun) {
527
+ ctx.retired.push(`${retired.asset.ref} -> ${successor.asset.ref}`);
528
+ ctx.perInitiatorProposed.add(candidate.initiator.ref);
529
+ return { failed: false };
530
+ }
531
+ // Continuity check (plan §5.4, rule R3): replay the retired asset's own
532
+ // past queries and flag, but do not block, a pair where the successor
533
+ // would not have shown up where the retired asset did.
534
+ const continuityRisk = await checkRetirementContinuity({
535
+ stashDir: ctx.stashDir,
536
+ config: ctx.config,
537
+ retiredRef: retired.asset.ref,
538
+ successorRef: successor.asset.ref,
539
+ retiredRaw: retired.raw,
540
+ successorRaw: successor.raw,
541
+ ledgerAccess: { proposalsCtx: ctx.opts.proposalsCtx },
542
+ search: ctx.continuitySearch,
543
+ });
544
+ const retirement = {
545
+ retiredRef: retired.asset.ref,
546
+ successorRef: successor.asset.ref,
547
+ cosine: candidate.cosine,
548
+ judgeLabel: verdict.relation,
549
+ judgeReason: verdict.reason,
550
+ retiredContentHash: retiredHash,
551
+ successorContentHash: successorHash,
552
+ reason,
553
+ ...(continuityRisk ? { continuityRisk } : {}),
554
+ };
555
+ try {
556
+ const proposal = createRetireProposal(ctx.stashDir, {
557
+ ref: retired.asset.ref,
558
+ // S6: its own generator, kept apart from the promote pass's
559
+ // "consolidate" proposals — `accept --generator consolidate` (bulk
560
+ // promotion review) never sweeps a retire proposal, and the reverse.
561
+ source: "consolidate-pair",
562
+ sourceRun: ctx.opts.sourceRun,
563
+ ...(ctx.opts.writeTarget
564
+ ? { target: { source: ctx.opts.writeTarget.source.name, root: ctx.opts.writeTarget.source.path } }
565
+ : {}),
566
+ confidence: verdict.confidence,
567
+ retirement,
568
+ }, ctx.opts.proposalsCtx);
569
+ ctx.retired.push(proposal.id);
570
+ ctx.perInitiatorProposed.add(candidate.initiator.ref);
571
+ return { failed: false };
572
+ }
573
+ catch (error) {
574
+ ctx.warnings.push(`Pair pass: could not mint a retire proposal for ${retired.asset.ref}: ${error instanceof Error ? error.message : String(error)}`);
575
+ // Must-fix 1: a mint failure is transient (a lock, a disk error, a
576
+ // validation hiccup) — treated the same as a same-run drop, so no row is
577
+ // written and the pair is retried next run instead of abandoned.
578
+ return { failed: true };
579
+ }
580
+ }
581
+ const emptyLabelCounts = () => ({
582
+ duplicate: 0,
583
+ subsumed: 0,
584
+ supersedes: 0,
585
+ contradicts: 0,
586
+ overlap: 0,
587
+ unrelated: 0,
588
+ });
589
+ /**
590
+ * The pair pass (alpha.9): initiators -> candidates -> one judge call per
591
+ * pair -> retire proposals for the calibrated classes. Runs alongside the
592
+ * promote pass, sharing its gate, its frozen LLM runner and its engine
593
+ * concurrency. `bundleId` is the target bundle's id (`undefined` skips the
594
+ * pass — nothing to scope candidates to).
595
+ */
596
+ export async function runConsolidatePairPass(opts, config, stashDir, bundleId, warnings,
597
+ /** Test seams: a transport override for the judge call, and for the continuity check's search call. Production callers omit both. */
598
+ seams = {}) {
599
+ const empty = {
600
+ initiators: 0,
601
+ initiatorsBacklog: 0,
602
+ pairsJudged: 0,
603
+ labelCounts: emptyLabelCounts(),
604
+ retired: [],
605
+ failedJudgments: 0,
606
+ };
607
+ const llmRunner = opts.llmRunner ?? undefined;
608
+ if (!bundleId || !llmRunner)
609
+ return empty;
610
+ let initiators;
611
+ let candidates;
612
+ let gitFirstAdded;
613
+ let db;
614
+ try {
615
+ db = opts.dryRun ? openReadonlyExistingDatabase(undefined, { isolatedSnapshot: true }) : openExistingDatabase();
616
+ if (!db)
617
+ return empty;
618
+ gitFirstAdded = loadGitFirstAddedMap(stashDir);
619
+ const pool = loadPairPassPool(db, bundleId);
620
+ initiators = selectInitiators(pool, opts, stashDir, gitFirstAdded).initiators;
621
+ candidates = selectCandidates(db, initiators, bundleId);
622
+ }
623
+ catch (error) {
624
+ warnings.push(`Pair pass: index unavailable — skipped (${error instanceof Error ? error.message : String(error)}).`);
625
+ return empty;
626
+ }
627
+ finally {
628
+ if (db)
629
+ closeDatabase(db);
630
+ }
631
+ const initiatorsBacklog = initiators.filter((i) => i.backlog).length;
632
+ // Never judge a pair when either side already has a pending retire
633
+ // proposal, as the retired ref OR its successor (B2 widens this from the
634
+ // retired ref alone): an asset spoken for by one pending decision cannot
635
+ // also be judged as part of another until that decision resolves.
636
+ const pendingRetireRefs = new Set();
637
+ try {
638
+ for (const p of listProposalsReadOnly(stashDir, { status: "pending" })) {
639
+ if (!isRetireProposal(p))
640
+ continue;
641
+ pendingRetireRefs.add(stripBundle(p.ref));
642
+ if (p.retirement?.successorRef)
643
+ pendingRetireRefs.add(stripBundle(p.retirement.successorRef));
644
+ }
645
+ }
646
+ catch {
647
+ // Best-effort de-dup only; a failed read never blocks judging.
648
+ }
649
+ const isPendingBlocked = (c) => pendingRetireRefs.has(stripBundle(c.initiator.ref)) || pendingRetireRefs.has(stripBundle(c.other.ref));
650
+ // Item 0 / S1: every rejected OR reverted consolidate-pair retirement on
651
+ // record, keyed by its exact ref pair and both content hashes — read once
652
+ // per run, the same shape as pendingRetireRefs above. `reverted` is
653
+ // included alongside `rejected`: a person undoing an accept via `akm
654
+ // proposal revert` is the same "no, not this" signal as a reject — without
655
+ // it, the next run would re-mint the identical retirement, and a bulk
656
+ // accept could re-apply a decision the person just undid.
657
+ const rejectedPairKeys = new Set();
658
+ try {
659
+ for (const status of ["rejected", "reverted"]) {
660
+ for (const p of listProposalsReadOnly(stashDir, { status, includeArchive: true })) {
661
+ if (!isRetireProposal(p) || !p.retirement)
662
+ continue;
663
+ rejectedPairKeys.add(rejectedPairKey(p.retirement.retiredRef, p.retirement.successorRef, p.retirement.retiredContentHash, p.retirement.successorContentHash));
664
+ }
665
+ }
666
+ }
667
+ catch {
668
+ // Best-effort de-dup only; a failed read never blocks judging.
669
+ }
670
+ // Blocker 2: admit WHOLE initiators under MAX_PAIRS_PER_RUN, never
671
+ // individual pairs — the old flat "top 300 candidates by cosine" cap let a
672
+ // pending-blocked pair spend a budget slot doing nothing, and left
673
+ // whichever initiators landed past slot 300 partially judged forever (no
674
+ // row per S1's own rule, so the SAME pairs got re-judged every night with
675
+ // no way to ever finish; the reviewer's simulation measured 30 nights
676
+ // making 9,000 calls but completing only 455 distinct pairs). A group with
677
+ // any pending-blocked pair is skipped before it can spend any budget at
678
+ // all. New-or-changed initiators (T_PAIR floor) are admitted before ANY
679
+ // backlog initiator regardless of cosine, then backlog initiators by their
680
+ // own best cosine — within a tier, a later, smaller group that still fits
681
+ // is admitted even after an earlier, larger one did not (first-fit), so
682
+ // the budget is not left idle just because the next-best group overflows
683
+ // it. Simulated, this drains the real backlog in ~12 nights instead of
684
+ // never.
685
+ const byInitiator = new Map();
686
+ for (const c of candidates) {
687
+ const list = byInitiator.get(c.initiator.ref);
688
+ if (list)
689
+ list.push(c);
690
+ else
691
+ byInitiator.set(c.initiator.ref, [c]);
692
+ }
693
+ const isNewOrChanged = (i) => !i.backlog || i.newMaterial;
694
+ const groups = [...byInitiator.values()]
695
+ .filter((group) => !group.some(isPendingBlocked))
696
+ .sort((a, b) => {
697
+ const tierA = isNewOrChanged(a[0].initiator) ? 0 : 1;
698
+ const tierB = isNewOrChanged(b[0].initiator) ? 0 : 1;
699
+ if (tierA !== tierB)
700
+ return tierA - tierB;
701
+ return b[0].cosine - a[0].cosine; // candidates is cosine-desc, so group[0] is this initiator's best.
702
+ });
703
+ const judgeable = [];
704
+ for (const group of groups) {
705
+ if (judgeable.length + group.length > MAX_PAIRS_PER_RUN)
706
+ continue; // first-fit: a smaller later group may still fit.
707
+ judgeable.push(...group);
708
+ }
709
+ const ctx = {
710
+ opts,
711
+ config,
712
+ stashDir,
713
+ llmRunner,
714
+ gitFirstAdded,
715
+ labelCounts: emptyLabelCounts(),
716
+ perInitiatorProposed: new Set(),
717
+ retired: [],
718
+ warnings,
719
+ rejectedPairKeys,
720
+ retiredThisRun: new Set(),
721
+ ...(seams.chat ? { chat: seams.chat } : {}),
722
+ // S2: one instance for the whole run (not one per proposal), so its
723
+ // "fell back once, go keyword-only from here" throttle actually covers
724
+ // every remaining query in this run, not just one proposal's own five.
725
+ continuitySearch: seams.continuitySearch ?? createContinuitySearch(stashDir, config),
726
+ };
727
+ let failedJudgments = 0;
728
+ // Should-fix 3: an initiator with any failed (or never-sent) judge call
729
+ // gets no ledger row — a failure attempted nothing conclusive, so writing
730
+ // one would mean the pair is never retried.
731
+ const failedInitiators = new Set();
732
+ if (judgeable.length > 0) {
733
+ // The promote pass validates opts.llmRunner's credentials once, but only
734
+ // when it has memories to dispatch — the pair pass can still have work
735
+ // when that pool is empty, so it validates independently before its
736
+ // first real dispatch. A test-injected chat seam bypasses the transport
737
+ // entirely and needs no credential.
738
+ if (!seams.chat)
739
+ assertRunnerCredentials(llmRunner);
740
+ const results = await concurrentMap(judgeable, (candidate) => judgeOne(ctx, candidate), llmRunner.connection.concurrency ?? 1, { signal: opts.signal });
741
+ results.forEach((r, idx) => {
742
+ // Must-fix 3: `concurrentMap` leaves an entry `undefined` for a call an
743
+ // aborted run never sent at all — `r?.failed === true` reads that as
744
+ // `undefined === true` (false), so an unsent call counted as a clean
745
+ // "no action" verdict. Explicit `undefined` check closes that gap.
746
+ if (r === undefined || r.failed === true) {
747
+ failedJudgments++;
748
+ failedInitiators.add(judgeable[idx].initiator.ref);
749
+ }
750
+ });
751
+ }
752
+ // S1: a ledger row is written for an initiator only once ALL of its OWN
753
+ // candidates were admitted (whole-initiator admission above makes this a
754
+ // simple membership check: judgeable either has every one of an
755
+ // initiator's candidates, or none of them) and none of them failed
756
+ // (should-fix 3) — including an initiator with zero candidates, which
757
+ // trivially satisfies both. One left out by the cap or a pending-proposal
758
+ // collision gets no row at all, so the next run reconsiders it rather
759
+ // than treating it as settled.
760
+ if (!opts.dryRun) {
761
+ const admittedRefs = new Set(judgeable.map((c) => c.initiator.ref));
762
+ const ledgerInputs = initiators
763
+ .filter((i) => {
764
+ const hasCandidates = byInitiator.has(i.ref);
765
+ if (!hasCandidates)
766
+ return true;
767
+ if (!admittedRefs.has(i.ref))
768
+ return false;
769
+ return !failedInitiators.has(i.ref);
770
+ })
771
+ .map((i) => ({
772
+ stashDir,
773
+ ref: i.ref,
774
+ source: PAIR_PASS_LEDGER_SOURCE,
775
+ outcome: ctx.perInitiatorProposed.has(i.ref) ? "proposed" : "judged_no_action",
776
+ contentHash: i.bodyHash,
777
+ }));
778
+ recordLedgerAttempt({ proposalsCtx: opts.proposalsCtx }, ledgerInputs);
779
+ }
780
+ return {
781
+ initiators: initiators.length,
782
+ initiatorsBacklog,
783
+ pairsConsidered: candidates.length,
784
+ // Must-fix 3: parsed verdicts only — judgeable.length counted pairs that
785
+ // were admitted, not pairs a verdict actually came back for.
786
+ pairsJudged: judgeable.length - failedJudgments,
787
+ labelCounts: ctx.labelCounts,
788
+ retired: ctx.retired,
789
+ failedJudgments,
790
+ };
791
+ }