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
@@ -1,12 +1,15 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * `akm improve`: under one run lock, bootstrap the index, drain the proposal
6
+ * backlog (triage), select candidates, run preparation → loop → post-loop, then
7
+ * commit what the run wrote (auto-sync). A dry run plans on read-only state.
8
+ */
4
9
  import fs from "node:fs";
5
10
  import path from "node:path";
6
- import { assertNever } from "../../core/assert.js";
7
11
  import { parseRefInput } from "../../core/asset/resolve-ref.js";
8
- import { daysToMs } from "../../core/common.js";
9
- import { bundlesToSourceEntries, loadConfig } from "../../core/config/config.js";
12
+ import { bundlesToSourceEntries, loadConfig, } from "../../core/config/config.js";
10
13
  import { ConfigError, rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
11
14
  import { appendEvent, readEvents } from "../../core/events.js";
12
15
  import { classifyImproveAction, foldDistillSkipped } from "../../core/improve-types.js";
@@ -18,37 +21,33 @@ import { info, warn, warnVerbose } from "../../core/warn.js";
18
21
  import { beginWriteProvenance, relativeWrittenPath } from "../../core/write-provenance.js";
19
22
  import { resolveWritable, resolveWriteTarget } from "../../core/write-source.js";
20
23
  import { ensureIndex } from "../../indexer/ensure-index.js";
24
+ import { indexWrittenAssets } from "../../indexer/index-written-assets.js";
21
25
  import { akmIndex } from "../../indexer/indexer.js";
22
26
  import { collectPendingMemories } from "../../indexer/passes/memory-inference.js";
23
27
  import { resolveEntryContentDir, resolveSourceEntries } from "../../indexer/search/search-source.js";
24
28
  import { collectEngineCredentialValues } from "../../integrations/agent/engine-resolution.js";
25
29
  import { installLlmUsagePersistence, LLM_USAGE_EVENT } from "../../llm/usage-persist.js";
30
+ import { withLlmStage } from "../../llm/usage-telemetry.js";
26
31
  import { isGitBackedStash, listGitChangedPaths, resolveWritableOverride, saveGitStash, } from "../../sources/providers/git.js";
27
32
  import { closeDatabase, openExistingDatabase } from "../../storage/repositories/index-connection.js";
28
33
  import { getEntryCount } from "../../storage/repositories/index-entries-repository.js";
29
34
  import { openSqliteReadSnapshot, SqliteReadSnapshotUnavailableError } from "../../storage/sqlite-read-snapshot.js";
30
35
  import { summarizeLlmUsageCrossTab } from "../health/llm-usage.js";
31
36
  import { drainProposals } from "../proposal/drain.js";
32
- import { resolveDrainPolicy } from "../proposal/drain-policies.js";
33
37
  import { describeGatedLanes, isAutonomyLaneAllowed } from "./autonomy-gate.js";
34
38
  import { akmDistill } from "./distill.js";
35
- // Eligibility / candidate-selection predicates live in ./eligibility.
36
- import { buildLatestProposalTsMap, collectEligibleRefs, collectEligibleRefsReadOnly, memoryCleanupParentRef, resolveImproveScope, shouldAnalyzeMemoryCleanup, } from "./eligibility.js";
37
- import { countEvalCases } from "./eval-cases.js";
38
- import { projectResolvedProcessRouting, resolveImprovePlan, resolveImproveStrategy, shouldSkipRef, } from "./improve-strategies.js";
39
+ import { collectEligibleRefs, collectEligibleRefsReadOnly, memoryCleanupParentRef, resolveImproveScope, shouldAnalyzeMemoryCleanup, } from "./eligibility.js";
40
+ import { eligibleRefCount, projectResolvedProcessRouting, resolveImprovePlan, resolveImproveStrategy, } from "./improve-strategies.js";
39
41
  import { buildImproveUsageReport } from "./improve-usage-report.js";
42
+ import { lastAttemptByRef, loadLedgerSnapshot } from "./ledger.js";
40
43
  import { improveLockPath, releaseImproveLock, tryAcquireImproveLock } from "./locks.js";
41
- // The cycle loop / post-loop / maintenance stages live in ./loop-stages.
42
44
  import { runImproveLoopStage, runImprovePostLoopStage } from "./loop-stages.js";
43
- import { analyzeMemoryCleanup } from "./memory/memory-improve.js";
45
+ import { analyzeMemoryCleanup, purgeGracedArchive, RETIRE_GRACE_DAYS, } from "./memory/memory-improve.js";
44
46
  import { buildImproveExecutionPlan } from "./planner.js";
45
- // The pre-loop preparation pipeline lives in ./preparation.
46
- import { runImprovePreparationStage } from "./preparation.js";
47
+ import { CONSOLIDATION_CONFIG_KEYS, pickDefined, recordImproveSkip, runImprovePreparationStage } from "./preparation.js";
47
48
  import { DEFAULT_DUE_DAYS, filterProactiveDue } from "./proactive-maintenance.js";
48
49
  import { akmReflect } from "./reflect.js";
49
- import { createRunContext } from "./run-context.js";
50
- import { errMessage } from "./shared.js";
51
- // Re-exported from ./loop-stages for test importers (improve-db-locking).
50
+ import { errMessage, noticeSet } from "./stage.js";
52
51
  export { runImproveMaintenancePasses } from "./loop-stages.js";
53
52
  export function renderSyncCommitMessage(template, result, nowMs) {
54
53
  const iso = new Date(nowMs).toISOString();
@@ -66,18 +65,16 @@ export function renderSyncCommitMessage(template, result, nowMs) {
66
65
  return template.replace(/\{(\w+)\}/g, (match, key) => tokens[key] ?? match);
67
66
  }
68
67
  /**
69
- * How long the improve loop waits for its FIRST engine response (success or
70
- * error — any terminal record proves the run is not silent) before printing
71
- * one default-level line. The timer is armed once the triage/index prepass
72
- * finishes and the loop is about to start dispatching engine requests — not
73
- * at run start — so it measures engine latency, not prepass time. Field
74
- * re-test (#957): an engine pointed at a dead endpoint produced zero output
75
- * for minutes, so a genuine hang looked identical to a normal-but-slow run.
76
- * A few seconds is short enough that an operator watching a scheduled run's
77
- * live log sees something promptly, long enough that an ordinary fast
78
- * response never prints it.
68
+ * How long the loop waits for its first engine response before printing one
69
+ * "still waiting" line (#957): a dead endpoint otherwise looks like a slow run.
70
+ * Armed when the loop starts, not at run start.
79
71
  */
80
72
  export const FIRST_ENGINE_RESPONSE_HEARTBEAT_MS = 5_000;
73
+ /**
74
+ * Abort the run at the budget; force-exit (0 — budget exhaustion is a normal
75
+ * scheduled-task outcome) only if the drain overruns the grace period.
76
+ * Returns an idempotent disposer.
77
+ */
81
78
  export function armBudgetWatchdog(budgetMs, controller, deps) {
82
79
  const setTimeoutFn = deps?.setTimeoutFn ?? setTimeout;
83
80
  const clearTimeoutFn = deps?.clearTimeoutFn ?? clearTimeout;
@@ -85,15 +82,10 @@ export function armBudgetWatchdog(budgetMs, controller, deps) {
85
82
  const hardKillGraceMs = deps?.hardKillGraceMs ?? 5_000;
86
83
  let hardKillTimer;
87
84
  const budgetTimer = setTimeoutFn(() => {
88
- // Cooperative cancellation first: let the run drain.
89
85
  controller.abort("improve budget exhausted");
90
- // Watchdog: only force-exit if the drain itself overruns the grace period.
91
- // Exit 0: budget exhaustion is a normal scheduled-task condition, not an error.
92
86
  hardKillTimer = setTimeoutFn(() => exitFn(0), hardKillGraceMs);
93
- // Never keep the event loop alive solely for the watchdog.
94
87
  hardKillTimer.unref?.();
95
88
  }, budgetMs);
96
- // RAII dispose: clears whichever timer is still pending. Idempotent.
97
89
  return () => {
98
90
  clearTimeoutFn(budgetTimer);
99
91
  if (hardKillTimer !== undefined) {
@@ -102,61 +94,30 @@ export function armBudgetWatchdog(budgetMs, controller, deps) {
102
94
  }
103
95
  };
104
96
  }
105
- /**
106
- * The run's write-provenance journal lifecycle as one named unit (#652).
107
- *
108
- * The journal is opened once the run owns its lock — so it spans exactly the
109
- * window in which this run, and only this run, is allowed to write — and closed
110
- * on every exit path including the crash-safety commit. Keeping the mutable
111
- * handle behind this factory rather than as a bare `let` in {@link akmImprove}
112
- * is also what keeps that function under the R31 size gate
113
- * (`tests/architecture/improve-fn-size-ratchet.test.ts`, absolute 220-line bar
114
- * with an empty baseline): lifecycle state belongs in a named unit, not in the
115
- * orchestrator's preamble.
116
- */
117
- function createRunWriteJournal() {
118
- let journal;
119
- return {
120
- open: () => {
121
- journal = beginWriteProvenance();
122
- },
123
- close: () => {
124
- journal?.end();
125
- journal = undefined;
126
- },
127
- current: () => journal,
128
- };
129
- }
130
97
  export async function akmImprove(options = {}) {
131
98
  const setup = resolveImproveRunSetup(options);
132
99
  options = setup.options;
133
- const { budgetMs, budgetAbortController, scope, selectedStrategy, syncRepoDir, resolvedStateDbPath, resolvedLockPath, } = setup;
100
+ const { budgetMs, budgetAbortController, scope, selectedStrategy, syncRepoDir, resolvedStateDbPath } = setup;
134
101
  let clearBudgetTimer = () => { };
135
102
  let clearFirstResponseHeartbeat = () => { };
136
- // #957: set by the usage sink's onRecord callback the moment any engine
137
- // call terminates (success or error), including one issued by the prepass
138
- // itself — makes arming the heartbeat below a no-op when the run is
139
- // already known not to be silent.
103
+ // Set by the usage sink when any engine call terminates, prepass included.
140
104
  let firstEngineResponseSeen = false;
141
105
  let initialGitPaths = new Set();
142
- const runJournal = createRunWriteJournal();
106
+ // The write-provenance journal (#652) spans exactly the window this run holds the lock.
107
+ let journal;
108
+ const closeJournal = () => {
109
+ journal?.end();
110
+ journal = undefined;
111
+ };
143
112
  const preEnsureCleanupWarnings = [];
144
- let plannedRefs = [];
145
- let memorySummary = { eligible: 0, derived: 0 };
146
- let strategyFilteredRefs = [];
147
- let indexSnapshot;
148
- let memoryCleanupPlan;
149
- let autonomyGatedDirectLanes = [];
150
- let guidance;
113
+ let collected;
151
114
  let triageDrain;
115
+ let ensureIndexDurationMs;
152
116
  let improveLockOwnership;
153
117
  let exitBackstop;
154
- // I1: open a single state.db connection for the main improve run so all
155
- // appendEvent calls reuse one handle instead of open/migrate/close per call.
156
118
  let eventsDb;
157
- // Start boundary-pinned for prepass telemetry, then replace this binding with
158
- // the long-lived handle after the prepass. The usage sink resolves it per
159
- // append, so one owner and counter span both lifecycle phases.
119
+ // Boundary-pinned for the prepass; replaced by the long-lived handle after it.
120
+ // The usage sink resolves it per append.
160
121
  let eventsCtx = { dbPath: resolvedStateDbPath };
161
122
  let disposeLlmUsageSink = () => { };
162
123
  const releaseRunLock = () => {
@@ -168,15 +129,27 @@ export async function akmImprove(options = {}) {
168
129
  releaseImproveLock(ownership);
169
130
  }
170
131
  catch {
171
- // Best-effort cleanup. Exact ownership prevents deleting a successor.
132
+ // Best-effort; exact ownership never deletes a successor's lock.
133
+ }
134
+ };
135
+ const teardown = () => {
136
+ // The usage sink goes before eventsDb closes; the journal closes after the
137
+ // crash-safety commit that still needs it.
138
+ disposeLlmUsageSink();
139
+ clearFirstResponseHeartbeat();
140
+ clearBudgetTimer();
141
+ if (exitBackstop) {
142
+ // Only our own listener: removeAllListeners would drop the host's.
143
+ process.removeListener("exit", exitBackstop);
144
+ exitBackstop = undefined;
172
145
  }
146
+ releaseRunLock();
147
+ closeJournal();
173
148
  };
174
149
  const commitStashBatch = makeCommitStashBatch({
175
150
  run: setup,
176
151
  getInitialGitPaths: () => initialGitPaths,
177
- getWriteJournal: () => runJournal.current(),
178
- // Captured via getter: the live `eventsCtx` binding is reassigned after the
179
- // prepass, and the catch-path sync must write through the current context.
152
+ getWriteJournal: () => journal,
180
153
  getEventsCtx: () => eventsCtx,
181
154
  });
182
155
  if (!options.dryRun) {
@@ -185,8 +158,7 @@ export async function akmImprove(options = {}) {
185
158
  }
186
159
  try {
187
160
  if (!options.dryRun) {
188
- const acquisition = tryAcquireImproveLock(resolvedLockPath, options.skipIfLocked, {
189
- // R25: C2 boundary-pinned path — the long-lived handle doesn't exist yet.
161
+ const acquisition = tryAcquireImproveLock(setup.resolvedLockPath, options.skipIfLocked, {
190
162
  dbPath: resolvedStateDbPath,
191
163
  });
192
164
  if (acquisition.state === "skipped") {
@@ -202,67 +174,53 @@ export async function akmImprove(options = {}) {
202
174
  process.on("exit", exitBackstop);
203
175
  initialGitPaths =
204
176
  syncRepoDir && isGitBackedStash(syncRepoDir) ? new Set(listGitChangedPaths(syncRepoDir)) : new Set();
205
- // #652: open AFTER the lock, so the journal covers exactly the window in
206
- // which this run — and only this run — is allowed to write.
207
- runJournal.open();
208
- // Drain the standing proposal backlog before indexing so fresh proposal
209
- // generation sees promotions from this same serialized run.
177
+ journal = beginWriteProvenance();
178
+ // Step 8 (alpha.9 plan §5.4): the purge sweep. Deterministic, no LLM,
179
+ // and first — before anything else this run might touch the archive
180
+ // for. Git-backed bundles only; a bundle with no git history of its
181
+ // own is left untouched (akm health reports its size instead).
182
+ if (setup.primaryStashDir) {
183
+ try {
184
+ const purge = purgeGracedArchive(setup.primaryStashDir);
185
+ if (purge.purgedFiles > 0) {
186
+ info(`[improve] archive purge: ${purge.purgedFiles} file(s) across ${purge.purgedDirs} retirement(s) past the ${RETIRE_GRACE_DAYS}d grace period removed`);
187
+ }
188
+ }
189
+ catch (err) {
190
+ preEnsureCleanupWarnings.push(`archive purge failed: ${err instanceof Error ? err.message : String(err)}`);
191
+ }
192
+ }
193
+ // The index is made current BEFORE triage (R6): triage promotes into the
194
+ // stash, and a reindex after it would always find fresh work.
195
+ const bootstrap = await runIndexBootstrapPass(setup, budgetAbortController.signal);
196
+ preEnsureCleanupWarnings.push(...bootstrap.warnings);
197
+ ensureIndexDurationMs = bootstrap.ensureIndexDurationMs;
210
198
  triageDrain = await runTriagePrePass(setup);
199
+ // Index triage's own writes incrementally so selection sees them.
200
+ const triageWrittenPaths = journal?.writtenPaths() ?? [];
201
+ if (setup.primaryStashDir && triageWrittenPaths.length > 0) {
202
+ await indexWrittenAssets(setup.primaryStashDir, triageWrittenPaths);
203
+ }
211
204
  }
212
- // #339 fix: ensureIndex MUST run BEFORE collectEligibleRefs (inside the
213
- // indexAndCollect helper).
214
- const collected = await indexAndCollect({ run: setup, signal: budgetAbortController.signal });
215
- plannedRefs = collected.plannedRefs;
216
- memorySummary = collected.memorySummary;
217
- strategyFilteredRefs = collected.strategyFilteredRefs;
218
- indexSnapshot = collected.indexSnapshot;
219
- memoryCleanupPlan = collected.memoryCleanupPlan;
220
- autonomyGatedDirectLanes = collected.autonomyGatedDirectLanes;
221
- guidance = collected.guidance;
222
- preEnsureCleanupWarnings.push(...collected.warnings);
205
+ collected = await indexAndCollect(setup);
223
206
  if (options.dryRun) {
224
- const result = await runDryPlanningStage({
225
- run: setup,
226
- collected,
227
- resolvedStateDbPath,
228
- budgetMs,
229
- initialCleanupWarnings: preEnsureCleanupWarnings,
230
- signal: budgetAbortController.signal,
231
- });
207
+ const result = await runDryPlanningStage(setup, collected, preEnsureCleanupWarnings);
232
208
  clearBudgetTimer();
233
209
  return result;
234
210
  }
235
211
  }
236
212
  catch (err) {
237
- // If the live prepass fails, emit its summary and clear the owning sink
238
- // before any run teardown. The disposer is idempotent with the main finalizer.
239
- disposeLlmUsageSink();
240
- clearFirstResponseHeartbeat();
241
- clearBudgetTimer();
242
- if (exitBackstop) {
243
- process.removeListener("exit", exitBackstop);
244
- exitBackstop = undefined;
245
- }
246
- releaseRunLock();
247
- runJournal.close();
213
+ teardown();
248
214
  throw err;
249
215
  }
250
216
  try {
251
- const openedEvents = openImproveEventsContext(resolvedStateDbPath);
252
- eventsDb = openedEvents.db;
253
- eventsCtx = openedEvents.ctx;
254
- // WI-9.10: construct the run's RunContext here — the first point after
255
- // run-setup where config/stashDir/eventsCtx/proposalsCtx/sourceRun/dryRun
256
- // are all in hand. See buildImproveRunContext for exactly which
257
- // already-resolved values back each field.
258
- const ctx = buildImproveRunContext(setup, eventsCtx);
259
- // #957: arm the heartbeat here, immediately before the improve loop
260
- // starts dispatching engine requests — not at run start, where its timer
261
- // would measure the triage/index prepass instead of engine latency. A
262
- // no-op when the prepass already produced a terminal LLM record (the
263
- // onRecord callback above already saw it). Cleared the moment any call
264
- // terminates (success or error) — never rearmed, so this prints at most
265
- // once per run.
217
+ try {
218
+ eventsDb = openStateDatabase(resolvedStateDbPath);
219
+ eventsCtx = { db: eventsDb };
220
+ }
221
+ catch (err) {
222
+ rethrowIfTestIsolationError(err);
223
+ }
266
224
  if (!firstEngineResponseSeen) {
267
225
  const firstResponseTimer = setTimeout(() => {
268
226
  warn("[improve] Still waiting for the first engine response...");
@@ -270,45 +228,11 @@ export async function akmImprove(options = {}) {
270
228
  firstResponseTimer.unref?.();
271
229
  clearFirstResponseHeartbeat = () => clearTimeout(firstResponseTimer);
272
230
  }
273
- const seq = await runImproveStageSequence({
274
- run: setup,
275
- strategyFilteredRefs,
276
- plannedRefs,
277
- memoryCleanupPlan,
278
- autonomyGatedDirectLanes,
279
- memorySummary,
280
- preEnsureCleanupWarnings,
281
- eventsCtx,
282
- ctx,
283
- });
284
- const result = finalizeImproveResult({
285
- run: setup,
286
- seq,
287
- guidance,
288
- memorySummary,
289
- memoryCleanupPlan,
290
- strategyFilteredRefs,
291
- rawPlannedRefs: plannedRefs,
292
- indexSnapshot,
293
- triageDrain,
294
- eventsCtx,
295
- });
296
- // End-of-run BATCH auto-sync — the converged commit. Recognition is
297
- // decoupled from the per-write path (see write-source.ts case-3): the primary
298
- // stash writes as a filesystem source during the run, then is committed via
299
- // the same `saveGitStash` that `akm sync` calls. The gating (git-backed
300
- // primary stash, sync not disabled) and the NON-FATAL guarantee now live in
301
- // `commitStashBatch` (#662); the inter-cycle and catch-path calls reuse it.
302
- // dry-run already returned above, so this always runs on a completed live
303
- // run. `result.sync` reflects this final commit (for a one-cycle run it is
304
- // the only commit; for a multi-cycle run the earlier cycles were banked by
305
- // the inter-cycle calls and this records the last batch). `result` carries
306
- // the full `{accepted}`/`{refs}`/`{triage_*}` token data for the message.
307
- //
308
- // #652: surface the run's write provenance on the envelope BEFORE the sync
309
- // (the sync itself writes no assets), so `result.writtenPaths` describes
310
- // exactly the set the commit below was scoped to.
311
- const writtenPaths = describeRunWrittenPaths(setup, runJournal.current()?.writtenPaths() ?? []);
231
+ const seq = await runImproveStageSequence(setup, collected, preEnsureCleanupWarnings, eventsCtx);
232
+ const result = finalizeImproveResult({ run: setup, seq, collected, triageDrain, ensureIndexDurationMs, eventsCtx });
233
+ // The run's write provenance goes on the envelope before the sync, so
234
+ // `writtenPaths` is exactly the set the commit is scoped to.
235
+ const writtenPaths = describeRunWrittenPaths(setup, journal?.writtenPaths() ?? []);
312
236
  if (writtenPaths.length > 0)
313
237
  result.writtenPaths = writtenPaths;
314
238
  result.sync = commitStashBatch(result);
@@ -316,64 +240,24 @@ export async function akmImprove(options = {}) {
316
240
  }
317
241
  catch (err) {
318
242
  recordImproveFailure(err, setup, eventsCtx);
319
- // #662 crash/abort safety net: commit whatever this run already wrote to the
320
- // primary stash BEFORE rethrowing, so an interrupted run (mid-cycle crash or
321
- // a cooperative budget abort that surfaces as a throw) does not leave its
322
- // writes uncommitted until a later clean run sweeps them up. Best-effort —
323
- // `commitStashBatch` swallows its own errors and no-ops a clean tree, so this
324
- // never masks or supersedes the original failure being rethrown below.
325
- commitStashBatch({ scope, plannedRefs, runId: options.runId });
243
+ // Crash/abort safety net (#662): commit what this run already wrote.
244
+ // commitStashBatch never throws and no-ops a clean tree.
245
+ commitStashBatch({ scope, plannedRefs: collected.plannedRefs, runId: options.runId });
326
246
  throw err;
327
247
  }
328
248
  finally {
329
- // #576: clear the per-run LLM usage sink BEFORE closing `eventsDb` below, so
330
- // no late sink invocation can write through a closed handle.
331
- disposeLlmUsageSink();
332
- // #957: never leave the first-response heartbeat timer pending past the run.
333
- clearFirstResponseHeartbeat();
334
- // O-1 (#364): Clear the budget abort timer so it does not keep the event
335
- // loop alive after the run completes.
336
- clearBudgetTimer();
337
- // Drop ONLY our own process.exit backstop so it does not fire later (or
338
- // accumulate across repeated in-process calls). Must NOT use
339
- // removeAllListeners("exit") here: in the in-process model (tests and
340
- // programmatic callers import cli.ts) that would silently destroy exit
341
- // handlers owned by the host or other commands.
342
- if (exitBackstop) {
343
- process.removeListener("exit", exitBackstop);
344
- exitBackstop = undefined;
345
- }
346
- releaseRunLock();
347
- // #652: close the write-provenance journal LAST among the write-facing
348
- // teardown steps — the catch path's crash-safety commit above still needs
349
- // it open to know what this run wrote.
350
- runJournal.close();
351
- // I1: close the long-lived state.db connection opened at the top of the run.
249
+ teardown();
352
250
  try {
353
251
  eventsDb?.close();
354
252
  }
355
253
  catch {
356
- // ignore — DB may already be closed
254
+ // already closed
357
255
  }
358
256
  }
359
257
  }
360
- /** Open the run-owned state handle while preserving the boundary-pinned fallback. */
361
- function openImproveEventsContext(dbPath) {
362
- try {
363
- const db = openStateDatabase(dbPath);
364
- return { db, ctx: { db } };
365
- }
366
- catch (err) {
367
- rethrowIfTestIsolationError(err);
368
- return { ctx: { dbPath } };
369
- }
370
- }
371
258
  /**
372
- * Shape the run's journaled absolute paths for `result.writtenPaths` (#652):
373
- * POSIX-relative to the run's primary stash dir (the repo root for a git-backed
374
- * stash) when the write landed inside it, absolute otherwise — a run writing to
375
- * a `--target` bundle outside the stash still reports what it wrote. Deduped and
376
- * sorted so the field is stable across runs.
259
+ * The run's journaled paths for `result.writtenPaths`: POSIX-relative to the
260
+ * primary stash when inside it, absolute otherwise; deduped and sorted.
377
261
  */
378
262
  function describeRunWrittenPaths(setup, writtenPaths) {
379
263
  const root = setup.primaryStashDir ?? setup.syncRepoDir;
@@ -385,19 +269,16 @@ function describeRunWrittenPaths(setup, writtenPaths) {
385
269
  return [...described].sort();
386
270
  }
387
271
  /**
388
- * Resolve a dry-run inspection source without adapting it into a write target.
389
- * Exported so `improve-cli.ts`'s `--show-prompt` (#952) can resolve the same
390
- * read-only bundle a plain `--dry-run` would, without duplicating this
391
- * selector/target/fallback precedence.
272
+ * The source a dry run (or `--show-prompt`) inspects, without adapting it into
273
+ * a write target.
392
274
  */
393
275
  export function resolveImproveReadSource(config, scopedRef, explicitTarget, fallbackStashDir) {
394
276
  if (scopedRef?.origin && explicitTarget && scopedRef.origin !== explicitTarget) {
395
277
  throw new UsageError(`Qualified ref bundle "${scopedRef.origin}" conflicts with --target "${explicitTarget}".`, "INVALID_FLAG_VALUE", `Drop --target or use --target ${scopedRef.origin}.`);
396
278
  }
397
279
  const selector = scopedRef?.origin ?? explicitTarget ?? config.defaultWriteTarget;
398
- if (!selector && fallbackStashDir) {
280
+ if (!selector && fallbackStashDir)
399
281
  return { source: { name: "stash", path: fallbackStashDir } };
400
- }
401
282
  const configuredSelector = selector ?? config.defaultBundle;
402
283
  if (configuredSelector) {
403
284
  const entry = bundlesToSourceEntries(config)?.find((source) => source.name === configuredSelector);
@@ -411,17 +292,14 @@ export function resolveImproveReadSource(config, scopedRef, explicitTarget, fall
411
292
  return { selector: configuredSelector, source: { name: configuredSelector, path: sourcePath } };
412
293
  }
413
294
  const implicit = resolveSourceEntries(undefined, config)[0];
414
- if (!implicit) {
295
+ if (!implicit)
415
296
  throw new ConfigError("no source configured; run `akm bundle create`", "STASH_DIR_NOT_FOUND");
416
- }
417
297
  return { source: { name: implicit.registryId ?? "stash", path: implicit.path } };
418
298
  }
419
299
  /**
420
- * Run-setup: budget/watchdog plumbing, scope + seam resolution, the invocation
421
- * plan, write-target resolution, the profile-defaulted options rebuild, and
422
- * the boundary-pinned state.db/lock paths. Fully SYNCHRONOUS — the C2 boundary
423
- * snapshot (resolvedStateDbPath) must be taken before the first await of the
424
- * run.
300
+ * Run setup, fully synchronous: the budget signal, the invocation plan, the
301
+ * write target, the profile-defaulted options, and the state.db and lock paths
302
+ * pinned before the first await (C2: a later env change cannot redirect them).
425
303
  */
426
304
  function resolveImproveRunSetup(options) {
427
305
  const startMs = Date.now();
@@ -433,30 +311,15 @@ function resolveImproveRunSetup(options) {
433
311
  configurable: true,
434
312
  });
435
313
  const scope = resolveImproveScope(options.scope);
436
- const reflectFn = options.reflectFn ?? akmReflect;
437
- const distillFn = options.distillFn ?? akmDistill;
438
- const ensureIndexFn = options.ensureIndexFn ?? ensureIndex;
439
- const reindexFn = options.reindexFn ?? akmIndex;
440
- const drainProposalsFn = options.drainProposalsFn ?? drainProposals;
441
- // #616 multi-cycle test seams. Default to the real module-local fns.
442
- const collectEligibleRefsImpl = options.collectEligibleRefsFn ?? (options.dryRun ? collectEligibleRefsReadOnly : collectEligibleRefs);
443
- const runImprovePreparationStageImpl = options.runImprovePreparationStageFn ?? runImprovePreparationStage;
444
- const runImproveLoopStageImpl = options.runImproveLoopStageFn ?? runImproveLoopStage;
445
- const runImprovePostLoopStageImpl = options.runImprovePostLoopStageFn ?? runImprovePostLoopStage;
446
- // Resolve the improve profile for this run. Profile drives type filtering,
447
- // process gating, and the default limit value.
448
- const _earlyConfig = options.config ?? loadConfig();
449
- const configuredImproveProfile = resolveImproveStrategy(options.strategy, _earlyConfig).config;
314
+ const config = options.config ?? loadConfig();
315
+ const configuredImproveProfile = resolveImproveStrategy(options.strategy, config).config;
316
+ // A dry run never dispatches, so an all-disabled strategy must not abort it.
450
317
  const resolvedPlan = options.resolvedPlan ??
451
- // #800/#957 round 3 — same dry-run exemption as improve-cli.ts's own
452
- // resolveImprovePlan call: a dry run never dispatches, so a strategy left
453
- // fully disabled by an unreachable credential must not abort here either.
454
- resolveImprovePlan(options.strategy, _earlyConfig, {
318
+ resolveImprovePlan(options.strategy, config, {
455
319
  repairValidationFailures: options.repairValidationFailures,
456
320
  allowAllDisabled: options.dryRun,
457
321
  });
458
322
  const selectedStrategy = resolvedPlan.strategy;
459
- const improveSensitiveValues = collectEngineCredentialValues(_earlyConfig);
460
323
  const improveProfile = selectedStrategy.config;
461
324
  const configuredLimits = {
462
325
  ...(options.limit !== undefined ? { cli: options.limit } : {}),
@@ -465,52 +328,36 @@ function resolveImproveRunSetup(options) {
465
328
  ? { reflect: configuredImproveProfile.processes.reflect.limit }
466
329
  : {}),
467
330
  };
331
+ // --limit, then the reflect process limit, then the profile limit.
468
332
  const effectiveLimit = options.limit ?? improveProfile?.processes?.reflect?.limit ?? improveProfile.limit;
469
333
  const scopedRef = scope.mode === "ref" && scope.value ? parseRefInput(scope.value) : undefined;
470
334
  const readSource = options.dryRun
471
335
  ? options.writeTarget
472
336
  ? { selector: options.writeTarget.selector, source: options.writeTarget.source }
473
- : resolveImproveReadSource(_earlyConfig, scopedRef, options.target, options.stashDir)
337
+ : resolveImproveReadSource(config, scopedRef, options.target, options.stashDir)
474
338
  : undefined;
475
339
  const writeTarget = options.dryRun
476
340
  ? undefined
477
341
  : scopedRef?.origin
478
- ? resolveMutationTarget(_earlyConfig, scopedRef, options.writeTarget?.source.name ?? options.target).target
342
+ ? resolveMutationTarget(config, scopedRef, options.writeTarget?.source.name ?? options.target).target
479
343
  : (options.writeTarget ??
480
- (options.target || _earlyConfig.defaultWriteTarget || !options.stashDir
481
- ? resolveWriteTarget(_earlyConfig, options.target)
344
+ (options.target || config.defaultWriteTarget || !options.stashDir
345
+ ? resolveWriteTarget(config, options.target)
482
346
  : {
483
347
  source: { kind: "filesystem", name: "stash", path: options.stashDir },
484
- config: {
485
- type: "filesystem",
486
- name: "stash",
487
- path: options.stashDir,
488
- writable: true,
489
- },
348
+ config: { type: "filesystem", name: "stash", path: options.stashDir, writable: true },
490
349
  }));
491
350
  const selectedSource = writeTarget?.source ?? readSource?.source;
492
351
  if (!selectedSource)
493
352
  throw new ConfigError("improve could not resolve a source", "STASH_DIR_NOT_FOUND");
494
- const selectedSelector = writeTarget?.selector ?? readSource?.selector;
495
- // Apply profile defaults — CLI flags take precedence over profile defaults.
496
- // Rebuild options with effective values so all downstream stage functions
497
- // automatically pick up the profile-driven defaults.
353
+ // Every stage reads this one config snapshot; nothing reloads it mid-run.
498
354
  options = {
499
355
  ...options,
500
- // Pin nested calls and quality gates to the same config snapshot as the
501
- // invocation plan. They must never reload a changed config mid-run.
502
- config: _earlyConfig,
503
- target: selectedSelector,
356
+ config,
357
+ target: writeTarget?.selector ?? readSource?.selector,
504
358
  sourceName: selectedSource.name,
505
359
  ...(writeTarget ? { writeTarget } : {}),
506
360
  stashDir: selectedSource.path,
507
- consolidateOptions: {
508
- ...options.consolidateOptions,
509
- target: selectedSelector,
510
- ...(writeTarget ? { writeTarget } : {}),
511
- },
512
- // Profile-level limit, then process-level reflect.limit as fallback.
513
- // CLI --limit takes precedence over both.
514
361
  limit: effectiveLimit,
515
362
  };
516
363
  let primaryStashDir;
@@ -520,42 +367,24 @@ function resolveImproveRunSetup(options) {
520
367
  catch {
521
368
  primaryStashDir = undefined;
522
369
  }
523
- const syncRepoDir = writeTarget?.source.repoPath ?? primaryStashDir;
524
- // C2 (#553/#554/#499): resolve the state.db path ONCE, synchronously, at the
525
- // command boundary — before the first `await` below. Every state.db open in
526
- // this run (`openStateDatabase`, every default-path `appendEvent`) is pinned
527
- // to this snapshot via `eventsCtx.dbPath`, so a parallel test file mutating
528
- // `process.env.XDG_DATA_HOME` across an await boundary can never redirect this
529
- // run's DB opens to a wrong/just-deleted tmpdir mid-flight (the parallel-load
530
- // timeout root cause). Because beforeEach runs synchronously, env is still the
531
- // calling test's own at this point; we capture it before yielding the loop.
532
- const resolvedStateDbPath = getStateDbPathInDataDir();
533
- // One conservative run lock protects the complete mutation window, including
534
- // triage, indexing, proposal work, maintenance, and final stash sync. Moved
535
- // out of `$STASH/.akm` to `$STATE/locks/<stash>/` (itlackey/akm#890): a lock
536
- // file is machine-local coordination state, not content that must travel
537
- // with the bundle.
538
- const lockBaseDir = getStashLocksDir(primaryStashDir ?? options.stashDir ?? ".");
539
- const resolvedLockPath = improveLockPath(lockBaseDir);
540
- const effectiveSync = { ...improveProfile.sync, ...options.sync };
541
370
  return {
542
371
  startMs,
543
372
  budgetMs,
544
373
  budgetAbortController,
545
374
  scope,
546
- reflectFn,
547
- distillFn,
548
- ensureIndexFn,
549
- reindexFn,
550
- drainProposalsFn,
551
- collectEligibleRefsImpl,
552
- runImprovePreparationStageImpl,
553
- runImproveLoopStageImpl,
554
- runImprovePostLoopStageImpl,
555
- _earlyConfig,
375
+ reflectFn: options.reflectFn ?? akmReflect,
376
+ distillFn: options.distillFn ?? akmDistill,
377
+ ensureIndexFn: options.ensureIndexFn ?? ensureIndex,
378
+ reindexFn: options.reindexFn ?? akmIndex,
379
+ drainProposalsFn: options.drainProposalsFn ?? drainProposals,
380
+ collectEligibleRefsImpl: options.collectEligibleRefsFn ?? (options.dryRun ? collectEligibleRefsReadOnly : collectEligibleRefs),
381
+ runImprovePreparationStageImpl: options.runImprovePreparationStageFn ?? runImprovePreparationStage,
382
+ runImproveLoopStageImpl: options.runImproveLoopStageFn ?? runImproveLoopStage,
383
+ runImprovePostLoopStageImpl: options.runImprovePostLoopStageFn ?? runImprovePostLoopStage,
384
+ config,
556
385
  resolvedPlan,
557
386
  selectedStrategy,
558
- improveSensitiveValues,
387
+ improveSensitiveValues: collectEngineCredentialValues(config),
559
388
  improveProfile,
560
389
  configuredImproveProfile,
561
390
  configuredLimits,
@@ -563,171 +392,99 @@ function resolveImproveRunSetup(options) {
563
392
  writeTarget,
564
393
  options,
565
394
  primaryStashDir,
566
- syncRepoDir,
567
- resolvedStateDbPath,
568
- resolvedLockPath,
569
- effectiveSync,
395
+ syncRepoDir: writeTarget?.source.repoPath ?? primaryStashDir,
396
+ resolvedStateDbPath: getStateDbPathInDataDir(),
397
+ // The run lock is machine-local state, kept outside the bundle (#890).
398
+ resolvedLockPath: improveLockPath(getStashLocksDir(primaryStashDir ?? options.stashDir ?? ".")),
399
+ effectiveSync: { ...improveProfile.sync, ...options.sync },
570
400
  };
571
401
  }
572
- /**
573
- * WI-9.10: build the run's {@link RunContext} purely from values
574
- * `resolveImproveRunSetup` and the long-lived state.db open already
575
- * resolved — no second config load, no new db handle. Called once, right
576
- * after `eventsCtx` is resolved (the last of {config, stashDir, eventsCtx,
577
- * proposalsCtx, sourceRun, dryRun} to become available in `akmImprove`).
578
- */
579
- function buildImproveRunContext(run, eventsCtx) {
580
- return createRunContext({
581
- config: run._earlyConfig,
582
- // primaryStashDir can fail to resolve (rare); "." mirrors the existing
583
- // lockBaseDir fallback in resolveImproveRunSetup for the same case. This
584
- // is a BEST-EFFORT value for the required RunContext field only — the
585
- // loop stage reads `ImproveLoopState.primaryStashDir` (the honest
586
- // optional), so its `if (primaryStashDir)` guards still skip on the
587
- // unresolvable path; no RunContext consumer reads `ctx.stashDir` there.
588
- stashDir: run.primaryStashDir ?? run.options.stashDir ?? ".",
589
- eventsCtx,
590
- // ProposalsContext threads only a dbPath seam (D14: no db handle is
591
- // threaded in), mirroring eventsCtx's own dbPath-only fallback shape.
592
- // Not yet wired into any proposal call site this stage — verb-level
593
- // RunContext adoption (reflect/distill/extract/consolidate) is later.
594
- proposalsCtx: { dbPath: run.resolvedStateDbPath },
595
- // Representative symbolic runner for this run: reflect is the loop's
596
- // primary model-driving process. Credentials remain unresolved here.
597
- getLlmRunner: () => run.resolvedPlan.processes.reflect.runner,
598
- sourceRun: run.options.runId ?? `improve-${run.startMs}`,
599
- // Always false here: callers only reach this point past the dry-run
600
- // early return in akmImprove.
601
- dryRun: run.options.dryRun ?? false,
602
- // Same AbortSignal instance armBudgetWatchdog stamped a live
603
- // `remainingBudgetMs` getter onto (#616) — identity preserved so
604
- // `ctx.signal.remainingBudgetMs` resolves correctly for every consumer.
605
- signal: run.budgetAbortController.signal,
606
- });
607
- }
608
- /** The catch-path improve_failed audit event (D3), redacted. */
402
+ /** The redacted `improve_failed` event for a crashed run. */
609
403
  function recordImproveFailure(err, run, eventsCtx) {
610
- const { scope, selectedStrategy, improveSensitiveValues, startMs } = run;
611
- // D3: emit improve_failed on unexpected crash so dashboards can detect failures.
612
404
  appendEvent({
613
405
  eventType: "improve_failed",
614
- ref: scope.mode === "ref" ? scope.value : `improve:${scope.mode}:${scope.value ?? "all"}`,
406
+ ref: run.scope.mode === "ref" ? run.scope.value : `improve:${run.scope.mode}:${run.scope.value ?? "all"}`,
615
407
  metadata: {
616
- strategy: selectedStrategy.name,
617
- error: redactSensitiveText(errMessage(err), improveSensitiveValues),
618
- durationMs: Date.now() - startMs,
408
+ strategy: run.selectedStrategy.name,
409
+ error: redactSensitiveText(errMessage(err), run.improveSensitiveValues),
410
+ durationMs: Date.now() - run.startMs,
619
411
  },
620
412
  }, eventsCtx);
621
413
  }
622
- /**
623
- * ensureIndex + collectEligibleRefs + the contradiction pre-pass + the
624
- * memory-cleanup recompute (#339 ordering). Formerly the runIndexAndCollect
625
- * closure mutating outer `let`s; now a pure pass returning its results.
626
- */
627
- async function indexAndCollect(args) {
628
- const { signal } = args;
629
- const { scope, options, primaryStashDir, improveProfile, _earlyConfig, ensureIndexFn, collectEligibleRefsImpl } = args.run;
630
- const warnings = [];
631
- let plannedRefs = [];
632
- let memorySummary = { eligible: 0, derived: 0 };
633
- let strategyFilteredRefs = [];
634
- let indexSnapshot;
635
- // #339 fix: ensureIndex MUST run BEFORE collectEligibleRefs. The eligible-ref
636
- // query reads the `entries` table; if a DB version upgrade just dropped that
637
- // table (or the index is otherwise empty), the prior run order silently
638
- // returned plannedRefs=[] and the improve loop no-op'd. Hoisting the call
639
- // here repopulates the index first so the subsequent query sees fresh data.
640
- if (primaryStashDir && !options.dryRun) {
641
- // Probe pre-ensureIndex entry count to drive the loud-fail warning below.
642
- // Best-effort: a missing DB / unreadable schema is the fresh-install case
643
- // and not a bug — we silently skip the probe.
644
- let preEnsureEntryCount;
645
- try {
646
- const dbPath = getDbPath();
647
- if (fs.existsSync(dbPath)) {
648
- const probeDb = openExistingDatabase();
649
- try {
650
- preEnsureEntryCount = getEntryCount(probeDb);
651
- }
652
- finally {
653
- closeDatabase(probeDb);
654
- }
655
- }
656
- }
657
- catch (err) {
658
- rethrowIfTestIsolationError(err);
659
- // best-effort; leave preEnsureEntryCount undefined
660
- }
414
+ function probeEntryCount() {
415
+ try {
416
+ if (!fs.existsSync(getDbPath()))
417
+ return undefined;
418
+ const db = openExistingDatabase();
661
419
  try {
662
- await ensureIndexFn(primaryStashDir, { mode: "blocking", signal: signal });
420
+ return getEntryCount(db);
663
421
  }
664
- catch (err) {
665
- if (signal.aborted)
666
- throw err;
667
- warnings.push(`ensureIndex failed: ${errMessage(err)}`);
668
- }
669
- // #339 loud-fail: if the index was empty pre-ensureIndex but is now
670
- // populated, a version-upgrade-triggered rebuild just happened. Surface
671
- // that on stderr so the improve run is not silently masked by stale
672
- // index state. Zero-before AND zero-after is the empty-stash case and
673
- // is intentionally not warned (not a bug).
674
- if (preEnsureEntryCount === 0) {
675
- try {
676
- const probeDb = openExistingDatabase();
677
- let postCount = 0;
678
- try {
679
- postCount = getEntryCount(probeDb);
680
- }
681
- finally {
682
- closeDatabase(probeDb);
683
- }
684
- if (postCount > 0) {
685
- warn("[improve] index was empty after DB version upgrade — repopulating before continuing");
686
- }
687
- }
688
- catch (err) {
689
- rethrowIfTestIsolationError(err);
690
- // best-effort
691
- }
422
+ finally {
423
+ closeDatabase(db);
692
424
  }
693
425
  }
694
- ({
695
- plannedRefs,
696
- memorySummary,
697
- strategyFilteredRefs = [],
698
- indexSnapshot,
699
- } = await collectEligibleRefsImpl(scope, options.stashDir, improveProfile, _earlyConfig));
426
+ catch (err) {
427
+ rethrowIfTestIsolationError(err);
428
+ return undefined;
429
+ }
430
+ }
431
+ /**
432
+ * ensureIndex before selection (#339): the eligible-ref query reads `entries`,
433
+ * which a DB version upgrade may just have dropped. An index that was empty
434
+ * and is now populated means an upgrade rebuild happened; say so.
435
+ */
436
+ async function runIndexBootstrapPass(run, signal) {
437
+ const { primaryStashDir, options } = run;
438
+ const warnings = [];
439
+ if (!primaryStashDir || options.dryRun)
440
+ return { warnings };
441
+ const preEnsureEntryCount = probeEntryCount();
442
+ let ensureIndexDurationMs;
443
+ try {
444
+ await run.ensureIndexFn(primaryStashDir, {
445
+ mode: "blocking",
446
+ signal,
447
+ onReindexTiming: ({ durationMs }) => {
448
+ ensureIndexDurationMs = durationMs;
449
+ },
450
+ });
451
+ }
452
+ catch (err) {
453
+ if (signal.aborted)
454
+ throw err;
455
+ warnings.push(`ensureIndex failed: ${errMessage(err)}`);
456
+ }
457
+ if (preEnsureEntryCount === 0 && (probeEntryCount() ?? 0) > 0) {
458
+ warn("[improve] index was empty after DB version upgrade — repopulating before continuing");
459
+ }
460
+ return { warnings, ensureIndexDurationMs };
461
+ }
462
+ /** Candidate selection plus the memory-cleanup plan and the autonomy-gated direct lanes. */
463
+ async function indexAndCollect(run) {
464
+ const { scope, options, primaryStashDir, improveProfile, config } = run;
465
+ const { plannedRefs, memorySummary, strategyFilteredRefs, indexSnapshot } = await run.collectEligibleRefsImpl(scope, options.stashDir, improveProfile, config);
700
466
  const cleanupParentRef = memoryCleanupParentRef(scope, options.stashDir);
701
- // D8 — the two direct lanes share one eligibility predicate, which reads scope
702
- // and eligible-memory count and no strategy flag at all. Compute it once: it
703
- // decides whether each lane would have run, which is also what decides whether
704
- // a gated lane is worth REPORTING. A lane that was never eligible was not
705
- // suppressed by the gate, so claiming it was would be noise.
706
- const cleanupEligible = shouldAnalyzeMemoryCleanup(scope, memorySummary.eligible, primaryStashDir);
707
- // Cleanup remains independent of the disabled contradiction writer.
708
- const memoryCleanupPlan = cleanupEligible
467
+ const memoryCleanupPlan = shouldAnalyzeMemoryCleanup(scope, memorySummary.eligible, primaryStashDir)
709
468
  ? analyzeMemoryCleanup(primaryStashDir, cleanupParentRef ? { parentRef: cleanupParentRef } : undefined)
710
469
  : undefined;
470
+ // A lane that would not have run anyway was not suppressed by the gate.
711
471
  const cleanupWouldMutate = Boolean(memoryCleanupPlan &&
712
472
  (memoryCleanupPlan.pruneCandidates.length > 0 ||
713
473
  memoryCleanupPlan.beliefStateTransitions.length > 0 ||
714
474
  memoryCleanupPlan.relativeDateCandidates.length > 0));
715
- const autonomyGatedDirectLanes = cleanupWouldMutate && !isAutonomyLaneAllowed("memoryCleanup", _earlyConfig) ? ["memoryCleanup"] : [];
716
- const guidance = memorySummary.eligible > 0
717
- ? "Improve folds memory cleanup into the same proposal queue: speculative promotions still go through reflect/distill proposals, while high-confidence redundant derived memories are moved into a recoverable cleanup archive instead of being left active in the stash."
718
- : undefined;
719
475
  return {
720
476
  plannedRefs,
721
477
  memorySummary,
722
478
  strategyFilteredRefs: strategyFilteredRefs ?? [],
723
479
  indexSnapshot,
724
480
  memoryCleanupPlan,
725
- autonomyGatedDirectLanes,
726
- guidance,
727
- warnings,
481
+ autonomyGatedDirectLanes: cleanupWouldMutate && !isAutonomyLaneAllowed("memoryCleanup", config) ? ["memoryCleanup"] : [],
482
+ guidance: memorySummary.eligible > 0
483
+ ? "Improve folds memory cleanup into the same proposal queue: speculative promotions still go through reflect/distill proposals, while high-confidence redundant derived memories are moved into a recoverable cleanup archive instead of being left active in the stash."
484
+ : undefined,
728
485
  };
729
486
  }
730
- /** The P2 lock-skipped envelope (graceful `skipIfLocked` early return). Exported for unit tests. */
487
+ /** The envelope for a run skipped because another run holds the lock. */
731
488
  export function buildLockSkippedResult(strategyName, scope, runId) {
732
489
  return {
733
490
  schemaVersion: 2,
@@ -742,14 +499,13 @@ export function buildLockSkippedResult(strategyName, scope, runId) {
742
499
  ...(runId !== undefined ? { runId } : {}),
743
500
  };
744
501
  }
745
- /** Evaluate the real selectors on read-only state, stopping before every writer boundary. */
746
- async function runDryPlanningStage(args) {
747
- const { run, collected, resolvedStateDbPath, budgetMs, initialCleanupWarnings, signal } = args;
502
+ /** A dry run: the real selectors on a read-only state.db snapshot, stopping before every writer. */
503
+ async function runDryPlanningStage(run, collected, initialCleanupWarnings) {
748
504
  let stateDb;
749
505
  let stateSnapshotUnavailable = false;
750
506
  try {
751
507
  try {
752
- stateDb = openSqliteReadSnapshot(resolvedStateDbPath);
508
+ stateDb = openSqliteReadSnapshot(run.resolvedStateDbPath);
753
509
  stateSnapshotUnavailable = !stateDb;
754
510
  }
755
511
  catch (error) {
@@ -757,27 +513,13 @@ async function runDryPlanningStage(args) {
757
513
  throw error;
758
514
  stateSnapshotUnavailable = true;
759
515
  }
760
- const eventsCtx = {
761
- ...(stateDb ? { db: stateDb } : { dbPath: resolvedStateDbPath }),
762
- readOnly: true,
763
- ...(stateSnapshotUnavailable ? { readOnlySnapshotUnavailable: true } : {}),
764
- };
765
516
  const preparation = await run.runImprovePreparationStageImpl({
766
- scope: run.scope,
767
- options: run.options,
768
- plannedRefs: collected.plannedRefs,
769
- memoryCleanupPlan: collected.memoryCleanupPlan,
770
- primaryStashDir: run.primaryStashDir,
771
- memorySummary: collected.memorySummary,
772
- reindexFn: run.reindexFn,
773
- startMs: run.startMs,
774
- budgetMs,
775
- eventsCtx,
776
- initialCleanupWarnings,
777
- improveProfile: run.improveProfile,
778
- resolvedPlan: run.resolvedPlan,
779
- strategyName: run.selectedStrategy.name,
780
- budgetSignal: signal,
517
+ ...preparationArgs(run, collected, initialCleanupWarnings),
518
+ eventsCtx: {
519
+ ...(stateDb ? { db: stateDb } : { dbPath: run.resolvedStateDbPath }),
520
+ readOnly: true,
521
+ ...(stateSnapshotUnavailable ? { readOnlySnapshotUnavailable: true } : {}),
522
+ },
781
523
  planOnly: true,
782
524
  });
783
525
  return buildDryRunResult(run, collected, preparation);
@@ -786,123 +528,100 @@ async function runDryPlanningStage(args) {
786
528
  stateDb?.close();
787
529
  }
788
530
  }
789
- /** The P3 dry-run envelope (plan-only early return). Exported for unit tests. */
531
+ function preparationArgs(run, collected, initialCleanupWarnings) {
532
+ return {
533
+ scope: run.scope,
534
+ options: run.options,
535
+ plannedRefs: collected.plannedRefs,
536
+ memoryCleanupPlan: collected.memoryCleanupPlan,
537
+ primaryStashDir: run.primaryStashDir,
538
+ memorySummary: collected.memorySummary,
539
+ reindexFn: run.reindexFn,
540
+ startMs: run.startMs,
541
+ budgetMs: run.budgetMs,
542
+ initialCleanupWarnings,
543
+ improveProfile: run.improveProfile,
544
+ resolvedPlan: run.resolvedPlan,
545
+ strategyName: run.selectedStrategy.name,
546
+ budgetSignal: run.budgetAbortController.signal,
547
+ };
548
+ }
549
+ /** The plan-only envelope of a dry run. */
790
550
  export function buildDryRunResult(run, collected, preparation) {
791
- const { selectedStrategy, scope } = run;
792
551
  const { guidance, memorySummary, memoryCleanupPlan, plannedRefs, strategyFilteredRefs } = collected;
793
- const effectiveRefs = preparation?.loopRefs ?? plannedRefs;
794
- const notices = collectImproveNotices({ resolvedPlan: run.resolvedPlan });
552
+ const notices = collectImproveNotices(run.resolvedPlan, []);
795
553
  return {
796
554
  schemaVersion: 2,
797
555
  ok: true,
798
- strategy: selectedStrategy.name,
799
- scope,
556
+ strategy: run.selectedStrategy.name,
557
+ scope: run.scope,
800
558
  dryRun: true,
801
- ...(notices.length > 0 ? { notices } : {}),
559
+ ...notices.fields(),
802
560
  ...(guidance ? { guidance } : {}),
803
561
  memorySummary,
804
562
  ...(memoryCleanupPlan ? { memoryCleanup: shapeMemoryCleanup(memoryCleanupPlan) } : {}),
805
- plannedRefs: effectiveRefs,
806
- ...(preparation?.planning
807
- ? {
808
- plan: buildResultExecutionPlan(run, preparation, plannedRefs, strategyFilteredRefs, collected.indexSnapshot, true),
809
- }
810
- : {}),
563
+ plannedRefs: preparation?.loopRefs ?? plannedRefs,
564
+ ...(preparation?.planning ? { plan: buildResultExecutionPlan(run, preparation, collected, true) } : {}),
811
565
  ...(strategyFilteredRefs.length > 0 ? { strategyFilteredRefs } : {}),
812
566
  ...(run.resolvedPlan.engineUnavailable.length > 0 ? { skippedProcesses: run.resolvedPlan.engineUnavailable } : {}),
567
+ ...(run.options.engineProbe !== undefined ? { engineProbe: run.options.engineProbe } : {}),
813
568
  ...(preparation?.proactiveMaintenance ? { proactiveMaintenance: preparation.proactiveMaintenance } : {}),
814
569
  };
815
570
  }
816
- /** One public plan projection for the dry and live result assemblers. */
817
- function buildResultExecutionPlan(run, preparation, rawProfileEligibleRefs, strategyFilteredRefs, indexSnapshot, dryRun) {
818
- const { improveProfile, configuredImproveProfile, resolvedPlan, configuredLimits, effectiveLimit, scope } = run;
819
- const triageConfig = improveProfile.processes?.triage;
571
+ /** The public execution-plan projection shared by the dry and live results. */
572
+ function buildResultExecutionPlan(run, preparation, collected, dryRun) {
573
+ const { improveProfile, configuredImproveProfile, resolvedPlan, scope } = run;
574
+ const { strategyFilteredRefs } = collected;
820
575
  const configuredTriage = configuredImproveProfile.processes?.triage;
821
- const configuredProactive = configuredImproveProfile.processes?.proactiveMaintenance;
822
- const configuredConsolidation = configuredImproveProfile.processes?.consolidate;
823
- const triageEnabled = scope.mode !== "ref" && resolvedPlan.processes.triage.enabled;
824
- const distillOnlySet = new Set(preparation.distillOnlyRefs.map((entry) => entry.ref));
825
576
  const inferenceMinPending = improveProfile.processes?.memoryInference?.minPendingCount;
826
577
  const pendingMemories = run.primaryStashDir && inferenceMinPending !== undefined && inferenceMinPending > 0
827
578
  ? collectPendingMemories(run.primaryStashDir).length
828
579
  : undefined;
829
- const memoryInferenceEnabled = resolvedPlan.processes.memoryInference.enabled &&
830
- !(pendingMemories !== undefined && inferenceMinPending !== undefined && pendingMemories < inferenceMinPending);
831
- const graphExtractionEnabled = resolvedPlan.processes.graphExtraction.enabled && run.primaryStashDir !== undefined;
832
- const profileGate = {
833
- name: "profile",
834
- removed: strategyFilteredRefs.length,
835
- reason: "all enabled per-ref processes refuse the asset type",
836
- };
837
- // #947 — per-process resolved engine/model/notices, plus how many of this
838
- // run's effective refs each ref-scoped process (reflect/distill/consolidate)
839
- // would act on. Counts only (not a per-ref matrix) to bound result_json size.
840
- const REF_SCOPED_PROCESSES = new Set(["reflect", "distill", "consolidate"]);
580
+ const belowMinPending = pendingMemories !== undefined && inferenceMinPending !== undefined && pendingMemories < inferenceMinPending;
581
+ const memoryInferenceEnabled = resolvedPlan.processes.memoryInference.enabled && !belowMinPending;
582
+ // Per-process routing, plus how many effective refs each ref-scoped process would act on (#947).
841
583
  const processes = projectResolvedProcessRouting(resolvedPlan).map((row) => {
842
- if (!REF_SCOPED_PROCESSES.has(row.process))
843
- return row;
844
- const eligibleRefs = preparation.loopRefs.filter((entry) => !shouldSkipRef(entry.ref, row.process, resolvedPlan.strategy.config)
845
- .skip).length;
846
- return { ...row, eligibleRefs };
584
+ const eligibleRefs = eligibleRefCount(preparation.loopRefs, row.process, resolvedPlan.strategy.config);
585
+ return eligibleRefs === undefined ? row : { ...row, eligibleRefs };
847
586
  });
848
587
  const proactive = preparation.planning.proactive
849
588
  ? {
850
589
  ...preparation.planning.proactive,
851
- configured: {
852
- ...(configuredProactive?.dueDays !== undefined ? { dueDays: configuredProactive.dueDays } : {}),
853
- ...(configuredProactive?.maxPerRun !== undefined ? { maxPerRun: configuredProactive.maxPerRun } : {}),
854
- ...(configuredProactive?.limit !== undefined ? { limit: configuredProactive.limit } : {}),
855
- },
590
+ configured: pickDefined(configuredImproveProfile.processes?.proactiveMaintenance, [
591
+ "dueDays",
592
+ "maxPerRun",
593
+ "limit",
594
+ ]),
856
595
  }
857
596
  : undefined;
858
- const consolidation = {
859
- ...preparation.planning.consolidation,
860
- configured: {
861
- ...(configuredConsolidation?.enabled !== undefined ? { enabled: configuredConsolidation.enabled } : {}),
862
- ...(configuredConsolidation?.minPoolSize !== undefined
863
- ? { minPoolSize: configuredConsolidation.minPoolSize }
864
- : {}),
865
- ...(configuredConsolidation?.limit !== undefined ? { limit: configuredConsolidation.limit } : {}),
866
- ...(configuredConsolidation?.maxChunkSize !== undefined
867
- ? { maxChunkSize: configuredConsolidation.maxChunkSize }
868
- : {}),
869
- ...(configuredConsolidation?.incrementalSince !== undefined
870
- ? { incrementalSince: configuredConsolidation.incrementalSince }
871
- : {}),
872
- },
873
- };
874
597
  return buildImproveExecutionPlan({
875
598
  dryRun,
876
- snapshot: indexSnapshot ?? {
599
+ snapshot: collected.indexSnapshot ?? {
877
600
  status: "unknown",
878
601
  reason: "the injected selector did not report an index snapshot status",
879
602
  },
880
- rawInScope: rawProfileEligibleRefs.length + strategyFilteredRefs.length,
603
+ rawInScope: collected.plannedRefs.length + strategyFilteredRefs.length,
881
604
  selectedRefs: preparation.actionableRefs,
882
605
  effectiveRefs: preparation.loopRefs,
883
- distillOnlyRefs: distillOnlySet,
884
- configuredLimits,
885
- effectiveLimit,
886
- replayBudget: preparation.planning.replayBudget,
887
- gates: [profileGate, ...preparation.planning.gates],
606
+ distillOnlyRefs: new Set(preparation.distillOnlyRefs.map((entry) => entry.ref)),
607
+ configuredLimits: run.configuredLimits,
608
+ effectiveLimit: run.effectiveLimit,
609
+ gates: [
610
+ {
611
+ name: "profile",
612
+ removed: strategyFilteredRefs.length,
613
+ reason: "all enabled per-ref processes refuse the asset type",
614
+ },
615
+ ...preparation.planning.gates,
616
+ ],
888
617
  processes,
889
618
  ...(proactive ? { proactive } : {}),
890
- consolidation,
619
+ consolidation: {
620
+ ...preparation.planning.consolidation,
621
+ configured: pickDefined(configuredImproveProfile.processes?.consolidate, CONSOLIDATION_CONFIG_KEYS),
622
+ },
891
623
  stageConfig: {
892
- extract: {
893
- enabled: preparation.planning.extract.wouldRun,
894
- reason: preparation.planning.extract.reason,
895
- },
896
- graphExtraction: {
897
- enabled: graphExtractionEnabled,
898
- reason: !resolvedPlan.processes.graphExtraction.enabled
899
- ? "disabled"
900
- : graphExtractionEnabled
901
- ? improveProfile.processes?.graphExtraction?.fullScan === true
902
- ? "enabled for a full-corpus scan"
903
- : "enabled for refs touched by this run"
904
- : "enabled but no primary source is available",
905
- },
624
+ extract: { enabled: preparation.planning.extract.wouldRun, reason: preparation.planning.extract.reason },
906
625
  memoryInference: {
907
626
  enabled: memoryInferenceEnabled,
908
627
  reason: !resolvedPlan.processes.memoryInference.enabled
@@ -913,78 +632,51 @@ function buildResultExecutionPlan(run, preparation, rawProfileEligibleRefs, stra
913
632
  },
914
633
  },
915
634
  triage: {
916
- enabled: triageEnabled,
635
+ enabled: scope.mode !== "ref" && resolvedPlan.processes.triage.enabled,
917
636
  configuredMode: configuredTriage?.applyMode ?? "queue",
918
- mode: triageConfig?.applyMode ?? "queue",
637
+ mode: improveProfile.processes?.triage?.applyMode ?? "queue",
919
638
  maxAcceptsPerRun: configuredTriage?.maxAcceptsPerRun ?? 25,
920
- ...(configuredTriage?.maxDiffLines !== undefined ? { maxDiffLines: configuredTriage.maxDiffLines } : {}),
921
639
  },
922
640
  });
923
641
  }
924
- /** The triage drain pre-pass (non-fatal; single-ref scope never drains). */
642
+ /** Drain the proposal backlog before generating more (non-fatal; a single-ref scope never drains). */
925
643
  async function runTriagePrePass(run) {
926
- const { primaryStashDir, resolvedPlan, scope, options, improveProfile, drainProposalsFn } = run;
927
- let triageDrain;
928
- if (primaryStashDir && resolvedPlan.processes.triage.enabled) {
929
- if (scope.mode === "ref") {
930
- warn("[improve] triage pre-pass skipped (single-ref scope never drains the whole queue)");
931
- }
932
- else {
933
- try {
934
- const triageConfig = improveProfile.processes?.triage;
935
- const policy = resolveDrainPolicy(triageConfig?.policy);
936
- const applyMode = triageConfig?.applyMode ?? "queue";
937
- const maxAccepts = triageConfig?.maxAcceptsPerRun ?? 25;
938
- triageDrain = await drainProposalsFn({
939
- stashDir: primaryStashDir,
940
- ...(options.target ? { target: options.target } : {}),
941
- config: options.config,
942
- policy,
943
- applyMode,
944
- maxAccepts,
945
- dryRun: false,
946
- excludeIds: new Set(),
947
- ...(triageConfig?.maxDiffLines !== undefined ? { maxDiffLines: triageConfig.maxDiffLines } : {}),
948
- judgment: resolvedPlan.triageJudgment,
949
- });
950
- }
951
- catch (err) {
952
- warn(`[improve] triage pre-pass failed (non-fatal): ${errMessage(err)}`);
953
- }
954
- }
644
+ const { primaryStashDir, resolvedPlan, scope, options, improveProfile } = run;
645
+ if (!primaryStashDir || !resolvedPlan.processes.triage.enabled)
646
+ return undefined;
647
+ if (scope.mode === "ref") {
648
+ warn("[improve] triage pre-pass skipped (single-ref scope never drains the whole queue)");
649
+ return undefined;
650
+ }
651
+ try {
652
+ const triageConfig = improveProfile.processes?.triage;
653
+ return await withLlmStage("triage", () => run.drainProposalsFn({
654
+ stashDir: primaryStashDir,
655
+ ...(options.target ? { target: options.target } : {}),
656
+ config: options.config,
657
+ applyMode: triageConfig?.applyMode ?? "queue",
658
+ maxAccepts: triageConfig?.maxAcceptsPerRun ?? 25,
659
+ dryRun: false,
660
+ excludeIds: new Set(),
661
+ judgment: resolvedPlan.triageJudgment,
662
+ }), { engine: resolvedPlan.triageJudgment?.engine, process: "triage.judgment" });
663
+ }
664
+ catch (err) {
665
+ warn(`[improve] triage pre-pass failed (non-fatal): ${errMessage(err)}`);
666
+ return undefined;
955
667
  }
956
- return triageDrain;
957
- }
958
- /** A path is never staged by auto-sync when it is (or looks like) a lock file. */
959
- function isSyncExcludedPath(relativePath) {
960
- return path.basename(relativePath).includes(".lock");
961
668
  }
962
669
  /**
963
- * Resolve the EXACT repo-relative path set the auto-sync commit stages (#652).
964
- *
965
- * Precedence:
966
- *
967
- * 1. **Run-scoped write provenance** (`writtenPaths`, absolute) — the paths
968
- * this run actually wrote, created, or removed, intersected with the paths
969
- * Git currently reports as changed. The intersection is what makes the set
970
- * correct rather than merely plausible: it drops a journaled path whose
971
- * final bytes match HEAD (nothing to commit), a journaled path that was
972
- * created and then removed again (never existed for Git), and any journaled
973
- * path Git ignores (which `saveGitStash` would otherwise reject outright).
974
- * A journaled path that was ALREADY dirty when the run started stays IN —
975
- * this run rewrote it, so this run owns it.
976
- * 2. **Dirty-path diff** (pre-#652 behaviour) — only when no journal was open,
977
- * which a live run never hits. Kept as the defensive fallback so a future
978
- * caller that commits outside the journal window degrades to the previous,
979
- * battle-tested heuristic instead of silently committing nothing.
980
- *
981
- * `unattributed` counts in-scope paths that became dirty DURING the run without
982
- * this run writing them: concurrent human edits (correctly excluded) and, if the
983
- * number is ever surprising, the fingerprint of a missing journal call site.
670
+ * The exact repo-relative paths the auto-sync commit stages (#652): the paths
671
+ * this run wrote that Git reports changed (a journaled path already dirty at
672
+ * start stays in — this run rewrote it). Without a journal, the pre-#652
673
+ * dirty-path diff. `unattributed` counts in-scope paths that went dirty during
674
+ * the run without this run writing them (concurrent edits, left alone). Lock
675
+ * files are never staged.
984
676
  */
985
677
  export function resolveSyncPathSet(input) {
986
678
  const { repoDir, assetPrefix, changedPaths, initialPaths, writtenPaths, provenance } = input;
987
- const inScope = (relativePath) => !isSyncExcludedPath(relativePath) &&
679
+ const inScope = (relativePath) => !path.basename(relativePath).includes(".lock") &&
988
680
  (!assetPrefix || relativePath === assetPrefix || relativePath.startsWith(`${assetPrefix}/`));
989
681
  if (!provenance) {
990
682
  return { paths: changedPaths.filter((p) => !initialPaths.has(p) && inScope(p)), unattributed: [] };
@@ -993,9 +685,8 @@ export function resolveSyncPathSet(input) {
993
685
  const attributed = new Set();
994
686
  for (const absolutePath of writtenPaths) {
995
687
  const relativePath = relativeWrittenPath(repoDir, absolutePath);
996
- if (!relativePath || !changed.has(relativePath) || !inScope(relativePath))
997
- continue;
998
- attributed.add(relativePath);
688
+ if (relativePath && changed.has(relativePath) && inScope(relativePath))
689
+ attributed.add(relativePath);
999
690
  }
1000
691
  return {
1001
692
  paths: [...attributed].sort(),
@@ -1003,60 +694,46 @@ export function resolveSyncPathSet(input) {
1003
694
  };
1004
695
  }
1005
696
  /**
1006
- * Crash-safe / incremental stash sync (#662) — see the factory-returned
1007
- * closure's original doc block: the primary stash writes as a filesystem
1008
- * source DURING the run; this commits them at end-of-run AND from the catch
1009
- * path. Idempotent + NON-FATAL. `getEventsCtx`/`getInitialGitPaths`/
1010
- * `getWriteJournal` are getters because those outer bindings are (re)assigned
1011
- * after this factory runs — the returned closure must observe the live values.
697
+ * The auto-sync commit (#662), used at end of run and from the crash path:
698
+ * idempotent and never throws. The getters read bindings reassigned after this
699
+ * factory runs.
1012
700
  */
1013
701
  function makeCommitStashBatch(deps) {
1014
- const { writeTarget, primaryStashDir, effectiveSync, options, _earlyConfig, improveProfile } = deps.run;
702
+ const { writeTarget, primaryStashDir, effectiveSync, options, config, improveProfile } = deps.run;
1015
703
  return (messageContext) => {
1016
704
  const eventsCtx = deps.getEventsCtx();
1017
- const initialGitPaths = deps.getInitialGitPaths();
1018
705
  const writeJournal = deps.getWriteJournal();
1019
706
  const repoDir = writeTarget?.source.repoPath ?? primaryStashDir;
1020
707
  if (!primaryStashDir || !repoDir || effectiveSync.enabled === false || !isGitBackedStash(repoDir)) {
1021
708
  return undefined;
1022
709
  }
1023
710
  const saveGitStashFn = options.saveGitStashFn ?? saveGitStash;
1024
- const writableOverride = writeTarget ? resolveWritable(writeTarget.config) : resolveWritableOverride(_earlyConfig);
711
+ const writableOverride = writeTarget ? resolveWritable(writeTarget.config) : resolveWritableOverride(config);
1025
712
  const push = options.sync?.push ?? improveProfile.sync?.push ?? true;
1026
713
  const message = renderSyncCommitMessage(effectiveSync.message ?? "akm improve auto-sync", messageContext, Date.now());
714
+ const record = (metadata) => appendEvent({ eventType: "stash_synced", metadata }, eventsCtx);
1027
715
  try {
1028
716
  const assetRoot = writeTarget?.source.path ?? primaryStashDir;
1029
- const assetPrefix = assetRoot ? path.relative(repoDir, assetRoot).replaceAll(path.sep, "/") : "";
1030
717
  const { paths, unattributed } = resolveSyncPathSet({
1031
718
  repoDir,
1032
- assetPrefix,
719
+ assetPrefix: path.relative(repoDir, assetRoot).replaceAll(path.sep, "/"),
1033
720
  changedPaths: listGitChangedPaths(repoDir),
1034
- initialPaths: initialGitPaths,
721
+ initialPaths: deps.getInitialGitPaths(),
1035
722
  writtenPaths: writeJournal?.writtenPaths() ?? [],
1036
723
  provenance: writeJournal !== undefined,
1037
724
  });
1038
725
  if (unattributed.length > 0) {
1039
726
  warnVerbose(`[improve] auto-sync left ${unattributed.length} path(s) uncommitted — not written by this run: ${unattributed.join(", ")}`);
1040
727
  }
1041
- const syncResult = saveGitStashFn(undefined, message, writableOverride, {
1042
- push,
1043
- repoDir,
1044
- paths,
728
+ const syncResult = saveGitStashFn(undefined, message, writableOverride, { push, repoDir, paths });
729
+ record({
730
+ committed: syncResult.committed,
731
+ pushed: syncResult.pushed,
732
+ skipped: syncResult.skipped,
733
+ reason: syncResult.reason ?? null,
734
+ attributed: paths.length,
735
+ unattributed: unattributed.length,
1045
736
  });
1046
- appendEvent({
1047
- eventType: "stash_synced",
1048
- metadata: {
1049
- committed: syncResult.committed,
1050
- pushed: syncResult.pushed,
1051
- skipped: syncResult.skipped,
1052
- reason: syncResult.reason ?? null,
1053
- // #652 provenance audit trail: how many paths this run staged, and
1054
- // how many in-scope paths went dirty during the run that it did NOT
1055
- // write (concurrent edits — deliberately left for their author).
1056
- attributed: paths.length,
1057
- unattributed: unattributed.length,
1058
- },
1059
- }, eventsCtx);
1060
737
  return {
1061
738
  committed: syncResult.committed,
1062
739
  pushed: syncResult.pushed,
@@ -1067,298 +744,131 @@ function makeCommitStashBatch(deps) {
1067
744
  catch (syncErr) {
1068
745
  const reason = errMessage(syncErr);
1069
746
  warn(`improve: stash sync failed (non-fatal): ${reason}`);
1070
- appendEvent({
1071
- eventType: "stash_synced",
1072
- metadata: { committed: false, pushed: false, skipped: true, reason },
1073
- }, eventsCtx);
747
+ record({ committed: false, pushed: false, skipped: true, reason });
1074
748
  return { committed: false, pushed: false, skipped: true, reason };
1075
749
  }
1076
750
  };
1077
751
  }
1078
- /** D6: pre-load the last 30 days of proposal_rejected events once per run. */
1079
- function preloadRejectedProposals() {
1080
- // D6: pre-load all proposal_rejected events from the last 30 days once,
1081
- // so the per-asset loop can use a Map lookup instead of N DB round trips.
1082
- const REJECTED_PROPOSAL_WINDOW_MS = daysToMs(30);
1083
- const rejectedProposalSince = new Date(Date.now() - REJECTED_PROPOSAL_WINDOW_MS).toISOString();
1084
- const allRejectedProposalEvents = readEvents({ type: "proposal_rejected", since: rejectedProposalSince }).events;
1085
- const rejectedProposalsByRef = new Map();
1086
- for (const e of allRejectedProposalEvents) {
1087
- if (e.ref && (!rejectedProposalsByRef.has(e.ref) || e.ts > (rejectedProposalsByRef.get(e.ref)?.ts ?? ""))) {
1088
- rejectedProposalsByRef.set(e.ref, e);
1089
- }
1090
- }
1091
- return rejectedProposalsByRef;
1092
- }
1093
752
  /**
1094
- * Post-lock proactive cooldown re-filter: re-read cooldown timestamps
1095
- * immediately before the loop so external proposal writes that occurred
1096
- * before this run acquired its lock are visible.
753
+ * Re-read the improve ledger under the lock and drop proactive refs another run
754
+ * attempted after this one planned.
1097
755
  */
1098
- export function refilterProactiveLoopRefs(loopRefs, improveProfile) {
1099
- // Re-read cooldown timestamps immediately before execution so external
1100
- // proposal writes that occurred before this run acquired its lock are visible.
756
+ export function refilterProactiveLoopRefs(loopRefs, improveProfile, ledgerAccess) {
1101
757
  const proactiveLoopRefs = loopRefs.filter((r) => r.eligibilitySource === "proactive");
1102
- let postLockLoopRefs = loopRefs;
1103
- if (proactiveLoopRefs.length > 0) {
1104
- const proactiveRefStrs = proactiveLoopRefs.map((r) => r.ref);
1105
- // Correlate proposal timestamps on each candidate's durable key.
1106
- const proactiveItemRefByRef = new Map(proactiveLoopRefs.map((r) => [r.ref, r.itemRef]));
1107
- const freshReflectTs = buildLatestProposalTsMap(proactiveRefStrs, "reflect", proactiveItemRefByRef);
1108
- const freshDistillTs = buildLatestProposalTsMap(proactiveRefStrs, "distill", proactiveItemRefByRef);
1109
- const pmDueDays = improveProfile.processes?.proactiveMaintenance?.dueDays ?? DEFAULT_DUE_DAYS;
1110
- const stillDue = new Set(filterProactiveDue(proactiveLoopRefs, freshReflectTs, freshDistillTs, pmDueDays, Date.now()).map((r) => r.ref));
1111
- const dropped = proactiveLoopRefs.filter((r) => !stillDue.has(r.ref));
1112
- if (dropped.length > 0) {
1113
- info(`[improve] post-lock cooldown re-filter: dropped ${dropped.length} proactive ref(s) claimed by concurrent run (${dropped.map((r) => r.ref).join(", ")})`);
1114
- postLockLoopRefs = loopRefs.filter((r) => r.eligibilitySource !== "proactive" || stillDue.has(r.ref));
1115
- }
1116
- }
1117
- return postLockLoopRefs;
758
+ if (proactiveLoopRefs.length === 0 || !ledgerAccess.stashDir)
759
+ return loopRefs;
760
+ const ledger = loadLedgerSnapshot({ eventsCtx: ledgerAccess.eventsCtx }, ledgerAccess.stashDir, [
761
+ "reflect",
762
+ "distill",
763
+ ]);
764
+ const stillDue = new Set(filterProactiveDue(proactiveLoopRefs, lastAttemptByRef(ledger, "reflect", proactiveLoopRefs), lastAttemptByRef(ledger, "distill", proactiveLoopRefs), improveProfile.processes?.proactiveMaintenance?.dueDays ?? DEFAULT_DUE_DAYS, Date.now()).map((r) => r.ref));
765
+ const dropped = proactiveLoopRefs.filter((r) => !stillDue.has(r.ref));
766
+ if (dropped.length === 0)
767
+ return loopRefs;
768
+ info(`[improve] post-lock cooldown re-filter: dropped ${dropped.length} proactive ref(s) claimed by concurrent run (${dropped.map((r) => r.ref).join(", ")})`);
769
+ return loopRefs.filter((r) => r.eligibilitySource !== "proactive" || stillDue.has(r.ref));
1118
770
  }
1119
771
  /**
1120
- * Post-loop stage, or a no-op skip when the shared wall-clock budget is
1121
- * already exhausted. The result still finalizes normally on skip, so
1122
- * scheduled budget exhaustion exits 0 (extracted verbatim from
1123
- * `runImproveStageSequence` — fn-size decomposition, no logic change).
772
+ * The audit events for refs and lanes this run will not touch, then
773
+ * preparation → loop → post-loop. No post-loop work starts past the budget; the
774
+ * result still finalizes, so budget exhaustion exits 0.
1124
775
  */
1125
- async function runPostLoopStageOrSkip(args) {
1126
- const { budgetAbortController, scope, options, primaryStashDir, preparation, memoryRefsForInference, reindexFn, eventsCtx, improveProfile, resolvedPlan, runImprovePostLoopStageImpl, } = args;
1127
- // Do not start new post-loop work after the shared wall-clock budget. The
1128
- // result still finalizes normally, so scheduled budget exhaustion exits 0.
1129
- const emptyPostLoopResult = {
1130
- allWarnings: [],
1131
- memoryInferenceDurationMs: 0,
1132
- graphExtractionDurationMs: 0,
1133
- };
1134
- const remainingBudget = budgetAbortController.signal.remainingBudgetMs;
1135
- if (budgetAbortController.signal.aborted || (remainingBudget !== undefined && remainingBudget <= 0)) {
1136
- info("[improve] post-loop maintenance skipped (wall-clock budget exhausted)");
1137
- return emptyPostLoopResult;
776
+ async function runImproveStageSequence(run, collected, preEnsureCleanupWarnings, eventsCtx) {
777
+ const { scope, options, primaryStashDir, improveProfile, resolvedPlan, budgetAbortController } = run;
778
+ const strategy = run.selectedStrategy.name;
779
+ // One count-only row for planner-filtered refs, never one per ref (#592).
780
+ if (collected.strategyFilteredRefs.length > 0) {
781
+ recordImproveSkip(eventsCtx, undefined, {
782
+ strategy,
783
+ reason: "strategy_filtered_all_passes",
784
+ count: collected.strategyFilteredRefs.length,
785
+ });
1138
786
  }
1139
- return runImprovePostLoopStageImpl({
1140
- scope,
1141
- options,
1142
- primaryStashDir,
1143
- actionableRefs: preparation.actionableRefs,
1144
- appliedCleanup: preparation.appliedCleanup,
1145
- cleanupWarnings: preparation.cleanupWarnings,
1146
- memoryRefsForInference,
1147
- reindexFn,
787
+ // A gated lane names the config key that would enable it.
788
+ for (const lane of [...resolvedPlan.autonomyGated, ...describeGatedLanes(collected.autonomyGatedDirectLanes)]) {
789
+ warn(`[improve] ${lane.lane} skipped — it ${lane.reason}. Set \`${lane.configKey}: true\` to enable it.`);
790
+ recordImproveSkip(eventsCtx, undefined, {
791
+ strategy,
792
+ reason: "autonomy_gated",
793
+ lane: lane.lane,
794
+ configKey: lane.configKey,
795
+ });
796
+ }
797
+ for (const item of resolvedPlan.engineUnavailable) {
798
+ warn(`[improve] ${item.process} skipped — it ${item.reason}.`);
799
+ recordImproveSkip(eventsCtx, undefined, {
800
+ strategy,
801
+ reason: "engine_unavailable",
802
+ process: item.process,
803
+ configKey: item.configKey,
804
+ });
805
+ }
806
+ const preparation = await run.runImprovePreparationStageImpl({
807
+ ...preparationArgs(run, collected, preEnsureCleanupWarnings),
808
+ eventsCtx,
809
+ });
810
+ const loopResult = await run.runImproveLoopStageImpl({
1148
811
  eventsCtx,
1149
812
  budgetSignal: budgetAbortController.signal,
813
+ primaryStashDir,
814
+ scope,
815
+ options,
816
+ reflectFn: run.reflectFn,
817
+ distillFn: run.distillFn,
818
+ loopRefs: refilterProactiveLoopRefs(preparation.loopRefs, improveProfile, {
819
+ stashDir: primaryStashDir ?? options.stashDir,
820
+ eventsCtx,
821
+ }),
822
+ actions: preparation.actions,
823
+ signalBearingSet: preparation.signalBearingSet,
824
+ distillCooledRefs: preparation.distillCooledRefs,
825
+ distillOnlyRefs: preparation.distillOnlyRefs,
826
+ recentErrors: preparation.recentErrors,
827
+ startMs: run.startMs,
828
+ budgetMs: run.budgetMs,
1150
829
  improveProfile,
1151
830
  resolvedPlan,
1152
- consolidationRan: preparation.consolidationRan,
1153
- // R5: floor violations from this run's consolidate pass, for the collapse
1154
- // detector's merge-floor advisory.
1155
- consolidationMergeFloorViolations: preparation.consolidation.mergeFloorViolations ?? 0,
1156
831
  });
1157
- }
1158
- /**
1159
- * Stage-sequencing: the strategy-filtered audit event, then the single
1160
- * prep → loop → post-loop pass via the #616 seams (D12). Returns every
1161
- * accumulator the result assembly reads — the old closure-scoped `let`s.
1162
- */
1163
- async function runImproveStageSequence(args) {
1164
- const { strategyFilteredRefs, plannedRefs, memoryCleanupPlan, memorySummary, preEnsureCleanupWarnings, eventsCtx, ctx, } = args;
1165
- const { scope, options, primaryStashDir, startMs, budgetMs, improveProfile, resolvedPlan, budgetAbortController, selectedStrategy, reflectFn, distillFn, reindexFn, runImprovePreparationStageImpl, runImproveLoopStageImpl, runImprovePostLoopStageImpl, } = args.run;
1166
- // 2026-05-27: one summary `improve_skipped` audit event (count only) for
1167
- // planner-pre-filtered refs — never per-ref (#592: O(n) sequential state.db
1168
- // writes cost ~500 s on a 9 000-ref stash; health reads the counters).
1169
- if (strategyFilteredRefs.length > 0) {
1170
- appendEvent({
1171
- eventType: "improve_skipped",
1172
- ref: undefined,
1173
- metadata: {
1174
- strategy: selectedStrategy.name,
1175
- reason: "strategy_filtered_all_passes",
1176
- count: strategyFilteredRefs.length,
1177
- },
1178
- }, eventsCtx);
1179
- }
1180
- // D8 — one event per lane the autonomy gate downgraded, naming the lane AND
1181
- // the config key that would enable it. This is the difference between a gate
1182
- // and a silent no-op: whatever the user would have seen happen, they now see
1183
- // explained. `health` already aggregates `improve_skipped` by reason
1184
- // (buildImproveSkipSummary), so these surface there without new machinery.
1185
- for (const lane of [...args.run.resolvedPlan.autonomyGated, ...describeGatedLanes(args.autonomyGatedDirectLanes)]) {
1186
- warn(`[improve] ${lane.lane} skipped — it ${lane.reason}. Set \`${lane.configKey}: true\` to enable it.`);
1187
- appendEvent({
1188
- eventType: "improve_skipped",
1189
- ref: undefined,
1190
- metadata: {
1191
- strategy: selectedStrategy.name,
1192
- reason: "autonomy_gated",
1193
- lane: lane.lane,
1194
- configKey: lane.configKey,
1195
- },
1196
- }, eventsCtx);
1197
- }
1198
- for (const item of resolvedPlan.engineUnavailable) {
1199
- warn(`[improve] ${item.process} skipped — it ${item.reason}.`);
1200
- appendEvent({
1201
- eventType: "improve_skipped",
1202
- ref: undefined,
1203
- metadata: {
1204
- strategy: selectedStrategy.name,
1205
- reason: "engine_unavailable",
1206
- process: item.process,
1207
- configKey: item.configKey,
1208
- },
1209
- }, eventsCtx);
832
+ let postLoop = { allWarnings: [], memoryInferenceDurationMs: 0 };
833
+ const remainingBudget = budgetAbortController.signal.remainingBudgetMs;
834
+ if (budgetAbortController.signal.aborted || (remainingBudget !== undefined && remainingBudget <= 0)) {
835
+ info("[improve] post-loop maintenance skipped (wall-clock budget exhausted)");
1210
836
  }
1211
- // Single prep->loop->post-loop pass, run under the invocation's lock.
1212
- // Accumulators are direct assignments from the single pass's results.
1213
- let preparation;
1214
- let memoryRefsForInference = new Set();
1215
- let consolidation;
1216
- let memoryInference;
1217
- let graphExtraction;
1218
- let cycleMetrics;
1219
- // Summed counters/durations.
1220
- let reflectsWithErrorContext = 0;
1221
- let memoryInferenceDurationMs = 0;
1222
- let graphExtractionDurationMs = 0;
1223
- let orphansPurged;
1224
- let proposalsExpired;
1225
- // Concatenated arrays.
1226
- const allWarnings = [];
1227
- let deadUrls;
1228
- let deadUrlCoverage;
1229
- const finalActions = [];
1230
- {
1231
- const runPreparation = () => runImprovePreparationStageImpl({
837
+ else {
838
+ postLoop = await run.runImprovePostLoopStageImpl({
1232
839
  scope,
1233
840
  options,
1234
- plannedRefs,
1235
- memoryCleanupPlan,
1236
841
  primaryStashDir,
1237
- memorySummary,
1238
- reindexFn,
1239
- startMs,
1240
- budgetMs,
842
+ actionableRefs: preparation.actionableRefs,
843
+ appliedCleanup: preparation.appliedCleanup,
844
+ cleanupWarnings: preparation.cleanupWarnings,
845
+ memoryRefsForInference: loopResult.memoryRefsForInference,
1241
846
  eventsCtx,
1242
- initialCleanupWarnings: preEnsureCleanupWarnings,
1243
- improveProfile,
1244
- resolvedPlan,
1245
- strategyName: selectedStrategy.name,
1246
847
  budgetSignal: budgetAbortController.signal,
1247
- });
1248
- preparation = await runPreparation();
1249
- const rejectedProposalsByRef = preloadRejectedProposals();
1250
- const runLoop = () => {
1251
- const postLockLoopRefs = refilterProactiveLoopRefs(preparation.loopRefs, improveProfile);
1252
- return runImproveLoopStageImpl({
1253
- ctx,
1254
- primaryStashDir,
1255
- scope,
1256
- options,
1257
- reflectFn,
1258
- distillFn,
1259
- loopRefs: postLockLoopRefs,
1260
- actions: preparation.actions,
1261
- signalBearingSet: preparation.signalBearingSet,
1262
- distillCooledRefs: preparation.distillCooledRefs,
1263
- distillOnlyRefs: preparation.distillOnlyRefs,
1264
- recentErrors: preparation.recentErrors,
1265
- rejectedProposalsByRef,
1266
- utilityMap: preparation.utilityMap,
1267
- startMs,
1268
- budgetMs,
1269
- improveProfile,
1270
- resolvedPlan,
1271
- });
1272
- };
1273
- const loopResult = await runLoop();
1274
- reflectsWithErrorContext += loopResult.reflectsWithErrorContext;
1275
- memoryRefsForInference = loopResult.memoryRefsForInference;
1276
- // #551: consolidation now runs in the preparation stage (before extract);
1277
- // its result and run-flag are read from `preparation`, not the post-loop.
1278
- consolidation = preparation.consolidation;
1279
- const postLoopResult = await runPostLoopStageOrSkip({
1280
- budgetAbortController,
1281
- scope,
1282
- options,
1283
- primaryStashDir,
1284
- preparation,
1285
- memoryRefsForInference,
1286
- reindexFn,
1287
- eventsCtx,
1288
848
  improveProfile,
1289
849
  resolvedPlan,
1290
- runImprovePostLoopStageImpl,
1291
850
  });
1292
- // Result objects (single pass — no cycle accumulation).
1293
- memoryInference = postLoopResult.memoryInference;
1294
- graphExtraction = postLoopResult.graphExtraction;
1295
- if (postLoopResult.cycleMetrics)
1296
- cycleMetrics = postLoopResult.cycleMetrics;
1297
- // Summed counters/durations.
1298
- memoryInferenceDurationMs += postLoopResult.memoryInferenceDurationMs;
1299
- graphExtractionDurationMs += postLoopResult.graphExtractionDurationMs;
1300
- if (postLoopResult.orphansPurged !== undefined) {
1301
- orphansPurged = (orphansPurged ?? 0) + postLoopResult.orphansPurged;
1302
- }
1303
- if (postLoopResult.proposalsExpired !== undefined) {
1304
- proposalsExpired = (proposalsExpired ?? 0) + postLoopResult.proposalsExpired;
1305
- }
1306
- // Concatenated arrays.
1307
- allWarnings.push(...postLoopResult.allWarnings);
1308
- if (postLoopResult.deadUrls !== undefined) {
1309
- deadUrls = [...(deadUrls ?? []), ...postLoopResult.deadUrls];
1310
- }
1311
- if (postLoopResult.deadUrlCoverage !== undefined) {
1312
- deadUrlCoverage = postLoopResult.deadUrlCoverage;
1313
- }
1314
- const maintenanceActions = postLoopResult.maintenanceActions;
1315
- if (maintenanceActions && maintenanceActions.length > 0) {
1316
- finalActions.push(...preparation.actions, ...maintenanceActions);
1317
- }
1318
- else {
1319
- finalActions.push(...preparation.actions);
1320
- }
1321
851
  }
1322
852
  return {
1323
853
  preparation,
1324
- memoryRefsForInference,
1325
- consolidation,
1326
- memoryInference,
1327
- graphExtraction,
1328
- cycleMetrics,
1329
- reflectsWithErrorContext,
1330
- memoryInferenceDurationMs,
1331
- graphExtractionDurationMs,
1332
- orphansPurged,
1333
- proposalsExpired,
1334
- allWarnings,
1335
- deadUrls,
1336
- deadUrlCoverage,
1337
- finalActions,
854
+ postLoop,
855
+ reflectsWithErrorContext: loopResult.reflectsWithErrorContext,
856
+ finalActions: [...preparation.actions, ...(postLoop.maintenanceActions ?? [])],
1338
857
  };
1339
858
  }
1340
- /**
1341
- * Run-teardown, result half: assemble the AkmImproveResult envelope and emit
1342
- * the improve_completed event (same adjacency as the old inline code — no
1343
- * side effects run between assembly and emit).
1344
- */
859
+ /** Assemble the result envelope and emit `improve_completed`. */
1345
860
  function finalizeImproveResult(args) {
1346
- const { guidance, memorySummary, memoryCleanupPlan, strategyFilteredRefs, rawPlannedRefs, indexSnapshot, triageDrain, eventsCtx, } = args;
1347
- const { selectedStrategy, scope, options, primaryStashDir, startMs, resolvedPlan } = args.run;
1348
- const { preparation, consolidation, memoryInference, graphExtraction, cycleMetrics, reflectsWithErrorContext, memoryInferenceDurationMs, graphExtractionDurationMs, orphansPurged, proposalsExpired, allWarnings, deadUrls, deadUrlCoverage, finalActions, } = args.seq;
1349
- // C1 (13-bus-factor): fold the per-ref `distill-skipped` rows (~13k/run,
1350
- // ~91% of result_json bytes) into a bounded aggregate BEFORE persistence.
1351
- // The metric total + per-reason breakdown are preserved on `distillSkipped`;
1352
- // the unbounded row list never reaches result_json. Reflect skip counters
1353
- // below still read `finalActions` (reflect skips are not folded).
861
+ const { run, collected, triageDrain, ensureIndexDurationMs, eventsCtx } = args;
862
+ const { preparation, postLoop, reflectsWithErrorContext, finalActions } = args.seq;
863
+ const { options, startMs, resolvedPlan } = run;
864
+ const { memoryCleanupPlan, strategyFilteredRefs } = collected;
865
+ const consolidation = preparation.consolidation;
866
+ const { memoryInference, allWarnings, deadUrls, deadUrlCoverage, orphansPurged, proposalsExpired } = postLoop;
867
+ const { memoryInferenceDurationMs } = postLoop;
868
+ // The per-ref distill-skipped rows fold into a bounded aggregate before persistence (C1).
1354
869
  const { actions: persistedActions, aggregate: distillSkippedAggregate } = foldDistillSkipped(finalActions);
1355
- // #944 — this run's LLM call/token accounting, split by process x engine x
1356
- // model, plus which enabled processes made zero calls and why. `llm_usage`
1357
- // events carry no runId column, so bound the read to this run's own wall
1358
- // clock — the same per-run event-scoping technique `health/windows.ts`
1359
- // already uses for wall time. `until` is "now" (assembly happens at
1360
- // teardown, after every LLM call this run will make has already emitted
1361
- // its event).
870
+ // This run's LLM accounting (#944): llm_usage rows carry no run id, so the
871
+ // read is bounded by the run's own wall clock.
1362
872
  const usageEvents = readEvents({ since: new Date(startMs).toISOString(), type: LLM_USAGE_EVENT }, eventsCtx).events;
1363
873
  const usageReport = buildImproveUsageReport({
1364
874
  resolvedPlan,
@@ -1368,37 +878,35 @@ function finalizeImproveResult(args) {
1368
878
  persistedActions,
1369
879
  distillSkippedAggregate,
1370
880
  });
1371
- const notices = collectImproveNotices({
1372
- resolvedPlan: args.run.resolvedPlan,
1373
- actions: finalActions,
1374
- schemaRepairs: preparation.schemaRepairs,
881
+ const notices = collectImproveNotices(resolvedPlan, [
882
+ ...finalActions.map((action) => action.result),
883
+ ...preparation.schemaRepairs,
1375
884
  consolidation,
1376
- extract: preparation.extract,
885
+ ...(preparation.extract ?? []).flatMap((extract) => [extract, ...(extract.sessions ?? [])]),
1377
886
  memoryInference,
1378
- graphExtraction,
1379
887
  triageDrain,
1380
- });
888
+ ]);
889
+ const applied = preparation.appliedCleanup;
890
+ const countMode = (mode) => finalActions.filter((a) => a.mode === mode).length;
1381
891
  const result = {
1382
892
  schemaVersion: 2,
1383
893
  ok: true,
1384
- strategy: selectedStrategy.name,
1385
- scope,
894
+ strategy: run.selectedStrategy.name,
895
+ scope: run.scope,
1386
896
  dryRun: false,
1387
- ...(notices.length > 0 ? { notices } : {}),
1388
- ...(guidance ? { guidance } : {}),
1389
- memorySummary,
897
+ ...notices.fields(),
898
+ ...(collected.guidance ? { guidance: collected.guidance } : {}),
899
+ memorySummary: collected.memorySummary,
1390
900
  ...(memoryCleanupPlan
1391
901
  ? {
1392
902
  memoryCleanup: {
1393
903
  ...shapeMemoryCleanup(memoryCleanupPlan),
1394
- ...(preparation.appliedCleanup
904
+ ...(applied
1395
905
  ? {
1396
- archived: preparation.appliedCleanup.archived,
1397
- ...(preparation.appliedCleanup.transitionLogPath
1398
- ? { transitionLogPath: preparation.appliedCleanup.transitionLogPath }
1399
- : {}),
1400
- ...(preparation.appliedCleanup.transitionLogEntries !== undefined
1401
- ? { transitionLogEntries: preparation.appliedCleanup.transitionLogEntries }
906
+ archived: applied.archived,
907
+ ...(applied.transitionLogPath ? { transitionLogPath: applied.transitionLogPath } : {}),
908
+ ...(applied.transitionLogEntries !== undefined
909
+ ? { transitionLogEntries: applied.transitionLogEntries }
1402
910
  : {}),
1403
911
  ...(allWarnings.length > 0 ? { warnings: allWarnings } : {}),
1404
912
  }
@@ -1409,13 +917,10 @@ function finalizeImproveResult(args) {
1409
917
  }
1410
918
  : {}),
1411
919
  plannedRefs: preparation.loopRefs,
1412
- ...(preparation.planning
1413
- ? {
1414
- plan: buildResultExecutionPlan(args.run, preparation, rawPlannedRefs, strategyFilteredRefs, indexSnapshot, false),
1415
- }
1416
- : {}),
920
+ ...(preparation.planning ? { plan: buildResultExecutionPlan(run, preparation, collected, false) } : {}),
1417
921
  ...(strategyFilteredRefs.length > 0 ? { strategyFilteredRefs } : {}),
1418
922
  ...(resolvedPlan.engineUnavailable.length > 0 ? { skippedProcesses: resolvedPlan.engineUnavailable } : {}),
923
+ ...(options.engineProbe !== undefined ? { engineProbe: options.engineProbe } : {}),
1419
924
  ...(usageReport ? { usageReport } : {}),
1420
925
  actions: persistedActions,
1421
926
  ...(distillSkippedAggregate ? { distillSkipped: distillSkippedAggregate } : {}),
@@ -1426,35 +931,20 @@ function finalizeImproveResult(args) {
1426
931
  ...(preparation.memoryIndexHealth !== undefined ? { memoryIndexHealth: preparation.memoryIndexHealth } : {}),
1427
932
  ...(preparation.coverageGaps.length > 0 ? { coverageGaps: preparation.coverageGaps } : {}),
1428
933
  ...(preparation.extract && preparation.extract.length > 0 ? { extract: preparation.extract } : {}),
1429
- ...(primaryStashDir !== undefined ? { evalCasesWritten: countEvalCases(primaryStashDir) } : {}),
1430
934
  ...(deadUrls !== undefined && deadUrls.length > 0 ? { deadUrls } : {}),
1431
- // Present whenever the check ran, unlike `deadUrls` above — a clean run
1432
- // (zero dead links) still needs to tell the health report how much of
1433
- // the bundle it actually covered (#892).
935
+ // Present whenever the check ran, so health knows the coverage of a clean run (#892).
1434
936
  ...(deadUrlCoverage !== undefined ? { deadUrlCoverage } : {}),
1435
937
  ...(reflectsWithErrorContext > 0 ? { reflectsWithErrorContext } : {}),
1436
938
  ...(memoryInference ? { memoryInference } : {}),
1437
- ...(graphExtraction ? { graphExtraction } : {}),
1438
- // Per-phase wall-clock durations. Surfaced at the top level of the
1439
- // envelope (not nested) because `health.ts`'s `wallTime.byPhase`
1440
- // aggregator and the existing `memoryInference.durationMs` /
1441
- // `graphExtraction.durationMs` health buckets all read
1442
- // `result.{memoryInferenceDurationMs,graphExtractionDurationMs}`
1443
- // directly. Mirrors how `consolidation.durationMs` is surfaced inside
1444
- // the consolidation sub-object (different convention because the
1445
- // consolidation result type already owns that field). Phases that did
1446
- // not run (zero duration) are omitted so the aggregator's
1447
- // "phase actually ran" filter (`> 0`) excludes them from the median/p95
1448
- // sample. Plumbed in d1273d0's follow-up — see
1449
- // `/tmp/akm-health-investigations/metrics-taxonomy-review.md` §1k / §3.
939
+ // Top-level phase durations feed health's wall-time buckets; a phase that
940
+ // did not run is omitted, not zero.
1450
941
  ...(memoryInferenceDurationMs > 0 ? { memoryInferenceDurationMs } : {}),
1451
- ...(graphExtractionDurationMs > 0 ? { graphExtractionDurationMs } : {}),
1452
- ...(cycleMetrics ? { cycleMetrics } : {}),
942
+ ...(ensureIndexDurationMs !== undefined ? { ensureIndexDurationMs } : {}),
1453
943
  ...(orphansPurged !== undefined ? { orphansPurged } : {}),
1454
944
  ...(proposalsExpired !== undefined && proposalsExpired > 0 ? { proposalsExpired } : {}),
1455
- reflectCooldownActions: finalActions.filter((a) => a.mode === "reflect-cooldown").length,
1456
- reflectSkippedActions: finalActions.filter((a) => a.mode === "reflect-skipped").length,
1457
- reflectGuardRejectedActions: finalActions.filter((a) => a.mode === "reflect-guard-rejected").length,
945
+ reflectCooldownActions: countMode("reflect-cooldown"),
946
+ reflectSkippedActions: countMode("reflect-skipped"),
947
+ reflectGuardRejectedActions: countMode("reflect-guard-rejected"),
1458
948
  ...(triageDrain
1459
949
  ? {
1460
950
  triage: {
@@ -1469,116 +959,57 @@ function finalizeImproveResult(args) {
1469
959
  ...(preparation.proactiveMaintenance ? { proactiveMaintenance: preparation.proactiveMaintenance } : {}),
1470
960
  ...(options.runId !== undefined ? { runId: options.runId } : {}),
1471
961
  };
1472
- if (!result.dryRun)
1473
- emitImproveCompletedEvent(result, {
1474
- memoryInferenceDurationMs,
1475
- graphExtractionDurationMs,
1476
- totalDurationMs: Date.now() - startMs,
1477
- warningCount: allWarnings.length,
1478
- orphansPurged: orphansPurged ?? 0,
1479
- }, eventsCtx);
962
+ emitImproveCompletedEvent(result, {
963
+ memoryInferenceDurationMs,
964
+ totalDurationMs: Date.now() - startMs,
965
+ warningCount: allWarnings.length,
966
+ orphansPurged: orphansPurged ?? 0,
967
+ }, eventsCtx);
1480
968
  return result;
1481
969
  }
1482
- function collectImproveNotices(args) {
1483
- const byKey = new Map();
970
+ /** Every lowering notice the plan and this run's stage results carry, deduplicated. */
971
+ function collectImproveNotices(resolvedPlan, carriers) {
972
+ const notices = noticeSet();
1484
973
  const collect = (carrier) => {
1485
- if (typeof carrier !== "object" || carrier === null)
1486
- return;
1487
- const notices = carrier.notices;
1488
- if (!Array.isArray(notices))
1489
- return;
1490
- for (const notice of notices)
1491
- byKey.set(JSON.stringify(notice), notice);
974
+ const list = carrier?.notices;
975
+ if (typeof carrier === "object" && Array.isArray(list))
976
+ notices.add(list);
1492
977
  };
1493
- for (const process of Object.values(args.resolvedPlan.processes))
978
+ for (const process of Object.values(resolvedPlan.processes))
1494
979
  collect(process);
1495
- for (const notice of args.resolvedPlan.triageJudgmentNotices ?? []) {
1496
- byKey.set(JSON.stringify(notice), notice);
1497
- }
1498
- for (const action of args.actions ?? [])
1499
- collect(action.result);
1500
- for (const repair of args.schemaRepairs ?? [])
1501
- collect(repair);
1502
- collect(args.consolidation);
1503
- for (const extract of args.extract ?? []) {
1504
- collect(extract);
1505
- for (const session of extract.sessions ?? [])
1506
- collect(session);
1507
- }
1508
- collect(args.memoryInference);
1509
- collect(args.graphExtraction);
1510
- collect(args.triageDrain);
1511
- return Object.freeze([...byKey.values()]);
980
+ notices.add(resolvedPlan.triageJudgmentNotices ?? []);
981
+ for (const carrier of carriers)
982
+ collect(carrier);
983
+ return notices;
1512
984
  }
985
+ /** `improve_completed` per-mode counters; keyed by every mode so a new one cannot be dropped. */
986
+ const ACTION_COUNTER = {
987
+ reflect: "reflectActions",
988
+ distill: "distillActions",
989
+ "distill-skipped": "distillSkippedActions",
990
+ "memory-prune": "memoryPruneActions",
991
+ "memory-inference": "memoryInferenceActions",
992
+ error: "errorActions",
993
+ "reflect-failed": "reflectFailedActions",
994
+ "reflect-cooldown": "reflectCooldownActions",
995
+ "reflect-skipped": "reflectSkippedActions",
996
+ "reflect-guard-rejected": "reflectGuardRejectedActions",
997
+ };
1513
998
  function emitImproveCompletedEvent(result, durations, eventsCtx) {
1514
- const actionCounts = {
1515
- reflect: 0,
1516
- reflectFailed: 0,
1517
- reflectCooldown: 0,
1518
- reflectSkipped: 0,
1519
- reflectGuardRejected: 0,
1520
- distill: 0,
1521
- distillSkipped: 0,
1522
- memoryPrune: 0,
1523
- memoryInference: 0,
1524
- graphExtraction: 0,
1525
- error: 0,
1526
- };
1527
- // Coarse audit buckets, derived from the SAME classifyImproveAction the
1528
- // persisted metrics_json uses (state-db.ts#computeImproveRunMetrics) so the
1529
- // emitted event and the stored row can never disagree.
999
+ const counts = Object.fromEntries(Object.values(ACTION_COUNTER).map((key) => [key, 0]));
1000
+ // The coarse buckets come from the same classifier the persisted metrics use.
1530
1001
  const classCounts = { accepted: 0, rejected: 0, skipped: 0, error: 0, noop: 0 };
1531
1002
  for (const action of result.actions ?? []) {
1003
+ const key = ACTION_COUNTER[action.mode];
1004
+ if (key)
1005
+ counts[key] = (counts[key] ?? 0) + 1;
1532
1006
  classCounts[classifyImproveAction(action.mode)] += 1;
1533
- // Per-variant counters for the event metadata. The default arm makes any
1534
- // new ImproveActionMode variant a compile error so a future variant cannot
1535
- // be silently dropped from the improve_completed event (the `reflect-guard-
1536
- // rejected` case below was previously missing here entirely).
1537
- switch (action.mode) {
1538
- case "reflect":
1539
- actionCounts.reflect += 1;
1540
- break;
1541
- case "reflect-failed":
1542
- actionCounts.reflectFailed += 1;
1543
- break;
1544
- case "reflect-cooldown":
1545
- actionCounts.reflectCooldown += 1;
1546
- break;
1547
- case "reflect-skipped":
1548
- actionCounts.reflectSkipped += 1;
1549
- break;
1550
- case "reflect-guard-rejected":
1551
- actionCounts.reflectGuardRejected += 1;
1552
- break;
1553
- case "distill":
1554
- actionCounts.distill += 1;
1555
- break;
1556
- case "distill-skipped":
1557
- actionCounts.distillSkipped += 1;
1558
- break;
1559
- case "memory-prune":
1560
- actionCounts.memoryPrune += 1;
1561
- break;
1562
- case "memory-inference":
1563
- actionCounts.memoryInference += 1;
1564
- break;
1565
- case "graph-extraction":
1566
- actionCounts.graphExtraction += 1;
1567
- break;
1568
- case "error":
1569
- actionCounts.error += 1;
1570
- break;
1571
- default:
1572
- assertNever(action.mode);
1573
- }
1574
1007
  }
1575
- // C1: distill-skipped rows are no longer in `result.actions` (folded into the
1576
- // bounded `distillSkipped` aggregate at assembly). Add the aggregate total to
1577
- // the per-variant counter AND the coarse `skipped` bucket so the emitted event
1578
- // still reports the true skipped volume.
1008
+ // distill-skipped rows were folded into the aggregate; count them back in.
1579
1009
  const distillSkippedTotal = result.distillSkipped?.total ?? 0;
1580
- actionCounts.distillSkipped += distillSkippedTotal;
1010
+ counts.distillSkippedActions = (counts.distillSkippedActions ?? 0) + distillSkippedTotal;
1581
1011
  classCounts.skipped += distillSkippedTotal;
1012
+ const cleanup = result.memoryCleanup;
1582
1013
  appendEvent({
1583
1014
  eventType: "improve_completed",
1584
1015
  ref: result.scope.mode === "ref"
@@ -1587,60 +1018,35 @@ function emitImproveCompletedEvent(result, durations, eventsCtx) {
1587
1018
  metadata: {
1588
1019
  strategy: result.strategy,
1589
1020
  plannedRefs: result.plannedRefs.length,
1590
- reflectActions: actionCounts.reflect,
1591
- distillActions: actionCounts.distill,
1592
- distillSkippedActions: actionCounts.distillSkipped,
1593
- memoryPruneActions: actionCounts.memoryPrune,
1594
- memoryInferenceActions: actionCounts.memoryInference,
1595
- graphExtractionActions: actionCounts.graphExtraction,
1596
- errorActions: actionCounts.error,
1597
- reflectFailedActions: actionCounts.reflectFailed,
1598
- reflectCooldownActions: actionCounts.reflectCooldown,
1599
- reflectSkippedActions: actionCounts.reflectSkipped,
1600
- // Previously dropped from the event entirely; now emitted so the guard
1601
- // rejections are visible in improve_completed telemetry.
1602
- reflectGuardRejectedActions: actionCounts.reflectGuardRejected,
1021
+ ...counts,
1603
1022
  acceptedActions: classCounts.accepted,
1604
1023
  rejectedActions: classCounts.rejected,
1605
1024
  skippedActions: classCounts.skipped,
1606
1025
  noopActions: classCounts.noop,
1607
1026
  reflectsWithErrorContext: result.reflectsWithErrorContext ?? 0,
1608
1027
  coverageGapCount: result.coverageGaps?.length ?? 0,
1609
- evalCasesWritten: result.evalCasesWritten ?? 0,
1610
1028
  deadUrlCount: result.deadUrls?.length ?? 0,
1611
1029
  deadUrlsChecked: result.deadUrlCoverage?.checked ?? 0,
1612
1030
  deadUrlsTotal: result.deadUrlCoverage?.total ?? 0,
1613
1031
  deadUrlsSkipped: result.deadUrlCoverage?.skipped ?? 0,
1614
1032
  memoryEligible: result.memorySummary.eligible,
1615
1033
  memoryDerived: result.memorySummary.derived,
1616
- memoryCleanupPruneCandidates: result.memoryCleanup?.pruneCandidates.length ?? 0,
1617
- memoryCleanupContradictionCandidates: result.memoryCleanup?.contradictionCandidates.length ?? 0,
1618
- memoryCleanupBeliefStateTransitions: result.memoryCleanup?.beliefStateTransitions.length ?? 0,
1619
- memoryCleanupConsolidationCandidates: result.memoryCleanup?.consolidationCandidates.length ?? 0,
1620
- memoryCleanupArchived: result.memoryCleanup?.archived?.length ?? 0,
1621
- memoryCleanupWarnings: result.memoryCleanup?.warnings?.length ?? 0,
1034
+ memoryCleanupPruneCandidates: cleanup?.pruneCandidates.length ?? 0,
1035
+ memoryCleanupContradictionCandidates: cleanup?.contradictionCandidates.length ?? 0,
1036
+ memoryCleanupBeliefStateTransitions: cleanup?.beliefStateTransitions.length ?? 0,
1037
+ memoryCleanupConsolidationCandidates: cleanup?.consolidationCandidates.length ?? 0,
1038
+ memoryCleanupArchived: cleanup?.archived?.length ?? 0,
1039
+ memoryCleanupWarnings: cleanup?.warnings?.length ?? 0,
1622
1040
  consolidationProcessed: result.consolidation?.processed ?? 0,
1623
1041
  consolidationDurationMs: result.consolidation?.durationMs ?? 0,
1624
1042
  memoryInferenceWrites: result.memoryInference?.writtenFacts ?? 0,
1625
1043
  memoryInferenceDurationMs: durations.memoryInferenceDurationMs,
1626
- graphExtractionExtractedFiles: result.graphExtraction?.quality.extractedFiles ?? 0,
1627
- graphExtractionDurationMs: durations.graphExtractionDurationMs,
1628
- // Layer-2 proactive-maintenance coverage (0 when the process is disabled
1629
- // or the run was ref-scoped) so a scheduled sweep's reach is trackable.
1630
1044
  proactiveSelected: result.proactiveMaintenance?.selected ?? 0,
1631
1045
  proactiveDueTotal: result.proactiveMaintenance?.dueTotal ?? 0,
1632
1046
  proactiveNeverReflected: result.proactiveMaintenance?.neverReflected ?? 0,
1633
- // New metrics for tuning the improve loop.
1634
- ...(durations.totalDurationMs !== undefined ? { durationMs: durations.totalDurationMs } : {}),
1635
- ...(durations.warningCount !== undefined ? { warningCount: durations.warningCount } : {}),
1636
- ...(durations.orphansPurged !== undefined ? { orphansPurged: durations.orphansPurged } : {}),
1637
- ...(result.graphExtraction?.quality
1638
- ? {
1639
- graphCoverage: result.graphExtraction.quality.extractionCoverage,
1640
- graphDensity: result.graphExtraction.quality.density,
1641
- graphEntities: result.graphExtraction.quality.entityCount,
1642
- }
1643
- : {}),
1047
+ durationMs: durations.totalDurationMs,
1048
+ warningCount: durations.warningCount,
1049
+ orphansPurged: durations.orphansPurged,
1644
1050
  },
1645
1051
  }, eventsCtx);
1646
1052
  }