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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (598) hide show
  1. package/CHANGELOG.md +1283 -22
  2. package/README.md +62 -37
  3. package/SECURITY.md +46 -31
  4. package/dist/akm +162 -38
  5. package/dist/akm-migrate +44 -0
  6. package/dist/assets/backends/schtasks-template.xml +2 -1
  7. package/dist/assets/hints/cli-hints-full.md +268 -118
  8. package/dist/assets/hints/cli-hints-short.md +87 -24
  9. package/dist/assets/{profiles → improve-strategies}/catchup.json +3 -1
  10. package/dist/assets/{profiles → improve-strategies}/consolidate.json +3 -1
  11. package/dist/assets/{profiles → improve-strategies}/default.json +6 -7
  12. package/dist/assets/improve-strategies/frequent.json +15 -0
  13. package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +4 -2
  14. package/dist/assets/{profiles → improve-strategies}/memory-focus.json +4 -1
  15. package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +5 -5
  16. package/dist/assets/{profiles → improve-strategies}/quick.json +4 -2
  17. package/dist/assets/improve-strategies/reflect-distill.json +30 -0
  18. package/dist/assets/{profiles → improve-strategies}/thorough.json +1 -1
  19. package/dist/assets/prompts/consolidate-system.md +5 -5
  20. package/dist/assets/prompts/extract-session.md +2 -6
  21. package/dist/assets/prompts/memory-infer-user.md +2 -3
  22. package/dist/assets/prompts/reflect-llm-framed-contract.md +11 -0
  23. package/dist/assets/prompts/reflect-llm-schema-contract.md +3 -0
  24. package/dist/assets/prompts/reflect-output-repair.md +3 -0
  25. package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
  26. package/dist/assets/stash-skeleton/README.md +38 -10
  27. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +8 -0
  28. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +8 -0
  29. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +14 -1
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +13 -1
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +9 -1
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +11 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +9 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +9 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +8 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +100 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/domains.md +64 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/organization.md +136 -0
  39. package/dist/assets/tasks/core/extract.yml +3 -2
  40. package/dist/assets/tasks/core/improve.yml +2 -1
  41. package/dist/assets/tasks/core/index-refresh.yml +1 -0
  42. package/dist/assets/tasks/core/sync.yml +1 -0
  43. package/dist/assets/tasks/core/version-check.yml +2 -1
  44. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
  45. package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
  46. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
  47. package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
  48. package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
  49. package/dist/assets/templates/html/health.html +5 -4
  50. package/dist/assets/workflows/workflow-template.md +31 -15
  51. package/dist/cli/invocation.js +279 -0
  52. package/dist/cli/parse-args.js +5 -90
  53. package/dist/cli/retired-commands.js +78 -0
  54. package/dist/cli/shared.js +158 -48
  55. package/dist/cli-node.mjs +2 -1
  56. package/dist/cli.js +747 -293
  57. package/dist/commands/agent/agent-dispatch.js +19 -18
  58. package/dist/commands/agent/agent-support.js +0 -24
  59. package/dist/commands/agent/contribute-cli.js +43 -97
  60. package/dist/commands/completions.js +80 -23
  61. package/dist/commands/config-cli.js +44 -281
  62. package/dist/commands/env/env-binding.js +99 -0
  63. package/dist/commands/env/env-cli.js +84 -224
  64. package/dist/commands/env/env.js +12 -163
  65. package/dist/commands/env/marker-path.js +6 -0
  66. package/dist/commands/env/secret-cli.js +45 -61
  67. package/dist/commands/env/secret.js +32 -62
  68. package/dist/commands/feedback-cli.js +179 -85
  69. package/dist/commands/health/accept-rate.js +58 -0
  70. package/dist/commands/health/advisories.js +7 -8
  71. package/dist/commands/health/checks.js +279 -94
  72. package/dist/commands/health/html-report.js +197 -578
  73. package/dist/commands/health/improve-metrics.js +277 -246
  74. package/dist/commands/health/llm-usage.js +19 -19
  75. package/dist/commands/health/md-report.js +16 -7
  76. package/dist/commands/health/metrics.js +67 -32
  77. package/dist/commands/health/renderers.js +47 -0
  78. package/dist/commands/health/report-view-model.js +508 -0
  79. package/dist/commands/health/stash-exposure.js +1 -1
  80. package/dist/commands/health/surfaces.js +16 -56
  81. package/dist/commands/health/task-runs.js +3 -67
  82. package/dist/{migrate-storage-node.mjs → commands/health/types-checks.js} +1 -5
  83. package/dist/commands/health/types-improve.js +29 -0
  84. package/dist/{output/text/save.js → commands/health/types-metrics.js} +1 -2
  85. package/dist/commands/health/types-result.js +7 -0
  86. package/dist/commands/health/types-runs.js +4 -0
  87. package/dist/commands/health/types-session-log.js +4 -0
  88. package/dist/commands/health/types-windows.js +4 -0
  89. package/dist/commands/health/types.js +26 -21
  90. package/dist/commands/health/windows.js +2 -3
  91. package/dist/commands/health.js +296 -167
  92. package/dist/commands/improve/anti-collapse.js +5 -5
  93. package/dist/commands/improve/autonomy-gate.js +68 -0
  94. package/dist/commands/improve/collapse-detector.js +65 -52
  95. package/dist/commands/improve/consolidate/chunking.js +9 -7
  96. package/dist/commands/improve/consolidate/eligibility.js +1 -23
  97. package/dist/commands/improve/consolidate/merge.js +4 -0
  98. package/dist/commands/improve/consolidate.js +454 -1354
  99. package/dist/commands/improve/content-hash.js +39 -0
  100. package/dist/commands/improve/distill/content-repair.js +4 -10
  101. package/dist/commands/improve/distill/promote-memory.js +89 -64
  102. package/dist/commands/improve/distill/quality-gate.js +118 -42
  103. package/dist/commands/improve/distill-guards.js +1 -1
  104. package/dist/commands/improve/distill-promotion-policy.js +33 -888
  105. package/dist/commands/improve/distill.js +607 -363
  106. package/dist/commands/improve/eligibility.js +165 -79
  107. package/dist/commands/improve/extract-cli.js +35 -126
  108. package/dist/commands/improve/extract-prompt.js +6 -35
  109. package/dist/commands/improve/extract.js +640 -391
  110. package/dist/commands/improve/feedback-valence.js +2 -12
  111. package/dist/commands/improve/improve-cli.js +134 -135
  112. package/dist/commands/improve/improve-result-file.js +30 -50
  113. package/dist/commands/improve/improve-run-types.js +4 -0
  114. package/dist/commands/improve/improve-strategies.js +135 -0
  115. package/dist/commands/improve/improve.js +904 -701
  116. package/dist/commands/improve/locks.js +64 -111
  117. package/dist/commands/improve/loop-stages.js +1110 -923
  118. package/dist/commands/improve/memory/derived-ref.js +124 -0
  119. package/dist/commands/improve/memory/memory-belief.js +79 -7
  120. package/dist/commands/improve/memory/memory-contradiction-detect.js +49 -52
  121. package/dist/commands/improve/memory/memory-improve.js +25 -37
  122. package/dist/commands/improve/outcome-loop.js +25 -88
  123. package/dist/commands/improve/preparation.js +1034 -813
  124. package/dist/commands/improve/proactive-maintenance.js +34 -9
  125. package/dist/commands/improve/proposal-envelope.js +31 -0
  126. package/dist/commands/improve/reflect.js +983 -794
  127. package/dist/commands/improve/run-context.js +119 -0
  128. package/dist/commands/improve/salience.js +24 -127
  129. package/dist/commands/improve/session-asset.js +7 -3
  130. package/dist/commands/improve/shared.js +14 -34
  131. package/dist/commands/improve/source-identity.js +28 -0
  132. package/dist/commands/improve/triage.js +20 -17
  133. package/dist/commands/lint/base-linter.js +340 -313
  134. package/dist/commands/lint/env-key-rules.js +31 -47
  135. package/dist/commands/lint/index.js +185 -30
  136. package/dist/commands/{events.js → log.js} +28 -38
  137. package/dist/commands/migrate-cli.js +54 -0
  138. package/dist/commands/migration-tool.js +55 -0
  139. package/dist/commands/observability-cli.js +70 -208
  140. package/dist/commands/proposal/diff-format.js +50 -0
  141. package/dist/commands/proposal/drain-policies.js +0 -6
  142. package/dist/commands/proposal/drain.js +91 -40
  143. package/dist/commands/proposal/proposal-cli.js +134 -132
  144. package/dist/commands/proposal/proposal-types.js +56 -0
  145. package/dist/commands/proposal/proposal.js +83 -65
  146. package/dist/commands/proposal/propose-cli.js +88 -0
  147. package/dist/commands/proposal/propose.js +105 -88
  148. package/dist/commands/proposal/repository.js +1303 -278
  149. package/dist/commands/proposal/validators/proposal-quality-validators.js +16 -6
  150. package/dist/commands/proposal/validators/proposal-validators.js +61 -12
  151. package/dist/commands/proposal/validators/proposals.js +6 -8
  152. package/dist/commands/read/curate.js +78 -73
  153. package/dist/commands/read/knowledge.js +510 -13
  154. package/dist/commands/read/registry-search.js +2 -2
  155. package/dist/commands/read/remember-cli.js +84 -15
  156. package/dist/commands/read/search-cli.js +203 -96
  157. package/dist/commands/read/search.js +126 -94
  158. package/dist/commands/read/show.js +226 -250
  159. package/dist/commands/registry-cli.js +34 -60
  160. package/dist/commands/remember.js +18 -57
  161. package/dist/commands/sources/add-cli.js +104 -49
  162. package/dist/commands/sources/bundle-cli.js +166 -0
  163. package/dist/commands/sources/bundle-config-ops.js +63 -0
  164. package/dist/commands/sources/info.js +27 -15
  165. package/dist/commands/sources/init.js +30 -40
  166. package/dist/commands/sources/installed-stashes.js +469 -172
  167. package/dist/commands/sources/migration-help.js +7 -4
  168. package/dist/commands/sources/schema-repair.js +10 -9
  169. package/dist/commands/sources/self-update.js +182 -121
  170. package/dist/commands/sources/source-add.js +169 -178
  171. package/dist/commands/sources/source-clone.js +144 -41
  172. package/dist/commands/sources/source-manage.js +94 -59
  173. package/dist/commands/sources/sources-cli.js +64 -205
  174. package/dist/commands/sources/stash-cli.js +91 -54
  175. package/dist/commands/sources/stash-skeleton.js +1 -1
  176. package/dist/commands/tasks/tasks-cli.js +106 -104
  177. package/dist/commands/tasks/tasks.js +445 -262
  178. package/dist/commands/workflow-cli.js +232 -121
  179. package/dist/core/action-contributors.js +1 -1
  180. package/dist/core/activation-policy.js +49 -0
  181. package/dist/core/adapter/adapters/agent-skills-adapter.js +181 -0
  182. package/dist/core/adapter/adapters/akm-adapter.js +528 -0
  183. package/dist/core/adapter/adapters/akm-lint.js +392 -0
  184. package/dist/core/adapter/adapters/akm-metadata.js +387 -0
  185. package/dist/core/adapter/adapters/akm-task-adapter.js +149 -0
  186. package/dist/core/adapter/adapters/akm-workflow-adapter.js +180 -0
  187. package/dist/core/adapter/adapters/claude-adapter.js +61 -0
  188. package/dist/core/adapter/adapters/dotenv-adapter.js +187 -0
  189. package/dist/core/adapter/adapters/generic-files-adapter.js +119 -0
  190. package/dist/core/adapter/adapters/index.js +80 -0
  191. package/dist/core/adapter/adapters/llm-wiki-adapter.js +419 -0
  192. package/dist/core/adapter/adapters/okf-adapter.js +391 -0
  193. package/dist/core/adapter/adapters/opencode-adapter.js +68 -0
  194. package/dist/core/adapter/adapters/shared.js +286 -0
  195. package/dist/core/adapter/adapters/tool-dir-shared.js +217 -0
  196. package/dist/core/adapter/adapters/website-snapshot-adapter.js +155 -0
  197. package/dist/core/adapter/bundle-adapter.js +4 -0
  198. package/dist/core/adapter/detect-adapter.js +17 -0
  199. package/dist/core/adapter/recognize-match.js +44 -0
  200. package/dist/core/adapter/registry.js +56 -0
  201. package/dist/core/adapter/types.js +4 -0
  202. package/dist/core/asset/akm-markdown.js +30 -0
  203. package/dist/core/asset/asset-placement.js +243 -0
  204. package/dist/core/asset/asset-ref.js +110 -79
  205. package/dist/core/asset/asset-serialize.js +20 -0
  206. package/dist/core/asset/frontmatter.js +28 -12
  207. package/dist/core/asset/markdown.js +40 -51
  208. package/dist/core/asset/resolve-ref.js +274 -0
  209. package/dist/core/asset/stash-meta.js +2 -2
  210. package/dist/core/bundle-id.js +51 -0
  211. package/dist/core/common.js +281 -86
  212. package/dist/core/config/config-io.js +42 -128
  213. package/dist/core/config/config-schema.js +233 -834
  214. package/dist/core/config/config-sources.js +162 -39
  215. package/dist/core/config/config-types.js +16 -11
  216. package/dist/core/config/config-version.js +29 -0
  217. package/dist/core/config/config-walker.js +126 -37
  218. package/dist/core/config/config.js +154 -331
  219. package/dist/core/config/deep-merge.js +41 -0
  220. package/dist/core/config/engine-semantics.js +28 -0
  221. package/dist/core/config/experimental.js +21 -0
  222. package/dist/core/config/schema/embedding.js +38 -0
  223. package/dist/core/config/schema/engines.js +116 -0
  224. package/dist/core/config/schema/experimental.js +47 -0
  225. package/dist/core/config/schema/feedback.js +31 -0
  226. package/dist/core/config/schema/improve-processes.js +389 -0
  227. package/dist/core/config/schema/improve.js +94 -0
  228. package/dist/core/config/schema/index-config.js +176 -0
  229. package/dist/core/config/schema/output.js +18 -0
  230. package/dist/core/config/schema/primitives.js +94 -0
  231. package/dist/core/config/schema/search.js +30 -0
  232. package/dist/core/config/schema/setup.js +18 -0
  233. package/dist/core/config/schema/sources-bundles.js +169 -0
  234. package/dist/core/config/schema/workflow.js +29 -0
  235. package/dist/core/env-secret-ref.js +155 -20
  236. package/dist/core/errors.js +17 -15
  237. package/dist/core/events-types.js +4 -0
  238. package/dist/core/events.js +46 -128
  239. package/dist/core/extra-params.js +62 -0
  240. package/dist/core/file-change.js +17 -0
  241. package/dist/core/file-lock.js +202 -57
  242. package/dist/core/fs-txn.js +392 -0
  243. package/dist/core/git-message.js +59 -0
  244. package/dist/core/improve-result.js +167 -0
  245. package/dist/core/json-schema.js +142 -0
  246. package/dist/core/lesson-lint.js +1 -17
  247. package/dist/core/logs-db.js +1 -1
  248. package/dist/core/maintenance-barrier.js +135 -0
  249. package/dist/core/migration-operation.js +44 -0
  250. package/dist/core/mutation-target.js +78 -0
  251. package/dist/core/paths.js +22 -25
  252. package/dist/core/platform.js +10 -0
  253. package/dist/core/recognition-util.js +128 -0
  254. package/dist/core/redaction.js +392 -0
  255. package/dist/core/standards/resolve-standards-context.js +36 -65
  256. package/dist/core/standards/resolve-stash-standards.js +2 -2
  257. package/dist/core/standards/resolve-type-conventions.js +5 -5
  258. package/dist/core/state/migrations.js +242 -11
  259. package/dist/core/state-db.js +98 -10
  260. package/dist/core/structured.js +1 -1
  261. package/dist/core/subprocess.js +303 -0
  262. package/dist/core/text-truncation.js +9 -5
  263. package/dist/core/time.js +20 -0
  264. package/dist/core/type-presentation.js +130 -0
  265. package/dist/core/warn.js +0 -3
  266. package/dist/core/write-source.js +834 -118
  267. package/dist/indexer/bundle-identity-guard.js +92 -0
  268. package/dist/indexer/db/graph-db.js +1 -25
  269. package/dist/indexer/db/llm-cache.js +1 -1
  270. package/dist/indexer/ensure-index.js +30 -9
  271. package/dist/indexer/graph/graph-boost.js +9 -30
  272. package/dist/indexer/graph/graph-extraction.js +41 -27
  273. package/dist/indexer/graph/graph-types.js +4 -0
  274. package/dist/indexer/index-writer-lock.js +93 -49
  275. package/dist/indexer/index-written-assets.js +100 -53
  276. package/dist/indexer/indexer.js +746 -329
  277. package/dist/indexer/init.js +18 -25
  278. package/dist/indexer/installations.js +142 -0
  279. package/dist/indexer/passes/dir-staleness.js +18 -10
  280. package/dist/indexer/passes/memory-inference.js +25 -15
  281. package/dist/indexer/passes/metadata.js +412 -243
  282. package/dist/indexer/scan/doc-to-entry.js +160 -0
  283. package/dist/indexer/scan/drain-dir.js +134 -0
  284. package/dist/indexer/search/db-search.js +292 -108
  285. package/dist/indexer/search/fts-query.js +64 -0
  286. package/dist/indexer/search/ranking-contributors.js +145 -25
  287. package/dist/indexer/search/ranking-types.js +4 -0
  288. package/dist/indexer/search/ranking.js +28 -71
  289. package/dist/indexer/search/search-attribution.js +67 -0
  290. package/dist/indexer/search/search-fields.js +18 -3
  291. package/dist/indexer/search/search-hit-enrichers.js +30 -40
  292. package/dist/indexer/search/search-source.js +157 -111
  293. package/dist/indexer/search/semantic-status.js +4 -1
  294. package/dist/indexer/usage/usage-events.js +10 -30
  295. package/dist/indexer/walk/file-context.js +3 -45
  296. package/dist/indexer/walk/matchers.js +42 -34
  297. package/dist/indexer/walk/path-resolver.js +11 -5
  298. package/dist/indexer/walk/walker.js +42 -14
  299. package/dist/integrations/agent/builder-shared.js +7 -0
  300. package/dist/integrations/agent/builders.js +5 -56
  301. package/dist/integrations/agent/config.js +3 -143
  302. package/dist/integrations/agent/detect.js +17 -2
  303. package/dist/integrations/agent/engine-resolution.js +231 -0
  304. package/dist/integrations/agent/index.js +1 -2
  305. package/dist/integrations/agent/model-aliases.js +16 -2
  306. package/dist/integrations/agent/profiles.js +36 -62
  307. package/dist/integrations/agent/prompts.js +46 -18
  308. package/dist/integrations/agent/runner-dispatch.js +93 -4
  309. package/dist/integrations/agent/runner.js +76 -208
  310. package/dist/integrations/agent/spawn.js +88 -196
  311. package/dist/integrations/harnesses/aider/agent-builder.js +114 -0
  312. package/dist/integrations/harnesses/aider/index.js +48 -0
  313. package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
  314. package/dist/integrations/harnesses/amazonq/agent-builder.js +147 -0
  315. package/dist/integrations/harnesses/amazonq/index.js +45 -0
  316. package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
  317. package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
  318. package/dist/integrations/harnesses/claude/config-import.js +1 -3
  319. package/dist/integrations/harnesses/claude/index.js +24 -35
  320. package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
  321. package/dist/integrations/harnesses/claude/session-log.js +27 -75
  322. package/dist/integrations/harnesses/codex/agent-builder.js +138 -0
  323. package/dist/integrations/harnesses/codex/index.js +52 -0
  324. package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
  325. package/dist/integrations/harnesses/copilot/agent-builder.js +122 -0
  326. package/dist/integrations/harnesses/copilot/index.js +48 -0
  327. package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
  328. package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
  329. package/dist/integrations/harnesses/gemini/index.js +48 -0
  330. package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
  331. package/dist/integrations/harnesses/ids.js +24 -0
  332. package/dist/integrations/harnesses/index.js +54 -34
  333. package/dist/integrations/harnesses/opencode/agent-builder.js +23 -5
  334. package/dist/integrations/harnesses/opencode/config-import.js +1 -3
  335. package/dist/integrations/harnesses/opencode/index.js +14 -32
  336. package/dist/integrations/harnesses/opencode/session-log.js +67 -125
  337. package/dist/integrations/harnesses/opencode-sdk/harness.js +51 -0
  338. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
  339. package/dist/integrations/harnesses/openhands/agent-builder.js +128 -0
  340. package/dist/integrations/harnesses/openhands/index.js +48 -0
  341. package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
  342. package/dist/integrations/harnesses/pi/agent-builder.js +97 -0
  343. package/dist/integrations/harnesses/pi/index.js +45 -0
  344. package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
  345. package/dist/integrations/harnesses/shared.js +17 -0
  346. package/dist/integrations/harnesses/types.js +43 -32
  347. package/dist/integrations/lockfile.js +211 -24
  348. package/dist/integrations/session-logs/index.js +36 -39
  349. package/dist/integrations/session-logs/provider-base.js +113 -0
  350. package/dist/llm/client.js +182 -110
  351. package/dist/llm/embedders/deterministic.js +2 -2
  352. package/dist/llm/embedders/remote.js +21 -9
  353. package/dist/llm/feature-gate.js +17 -57
  354. package/dist/llm/graph-extract.js +12 -13
  355. package/dist/llm/index-passes.js +8 -42
  356. package/dist/llm/memory-infer.js +144 -1
  357. package/dist/llm/metadata-enhance.js +45 -30
  358. package/dist/llm/structured-call.js +16 -8
  359. package/dist/llm/usage-persist.js +30 -5
  360. package/dist/llm/usage-telemetry.js +59 -6
  361. package/dist/output/cli-hints.js +1 -2
  362. package/dist/output/command-registry.js +27 -0
  363. package/dist/output/context.js +22 -7
  364. package/dist/output/format-exempt.js +80 -0
  365. package/dist/output/generic-render.js +251 -0
  366. package/dist/output/html-render.js +11 -16
  367. package/dist/output/render-registry.js +57 -0
  368. package/dist/output/renderers.js +14 -279
  369. package/dist/output/shapes/curate.js +10 -1
  370. package/dist/output/shapes/events.js +12 -7
  371. package/dist/output/shapes/helpers.js +58 -84
  372. package/dist/output/shapes/passthrough.js +11 -39
  373. package/dist/output/shapes/proposal/producer.js +15 -7
  374. package/dist/output/shapes/registry.js +12 -6
  375. package/dist/output/shapes.js +0 -9
  376. package/dist/output/text/{init.js → bundle-create.js} +3 -1
  377. package/dist/output/text/bundle-show.js +7 -0
  378. package/dist/output/text/command-format.js +562 -0
  379. package/dist/output/text/env.js +1 -3
  380. package/dist/output/text/events.js +8 -7
  381. package/dist/output/text/helpers.js +15 -1164
  382. package/dist/output/text/proposal/producer.js +4 -2
  383. package/dist/output/text/proposal-format.js +202 -0
  384. package/dist/output/text/registry-commands.js +1 -2
  385. package/dist/output/text/registry.js +12 -6
  386. package/dist/output/text/show-directives.js +117 -0
  387. package/dist/output/text/show-format.js +103 -0
  388. package/dist/output/text/sync.js +5 -0
  389. package/dist/output/text/workflow-format.js +332 -0
  390. package/dist/output/text/workflow.js +3 -2
  391. package/dist/output/text.js +10 -19
  392. package/dist/registry/factory.js +4 -6
  393. package/dist/registry/origin-resolve.js +16 -27
  394. package/dist/registry/providers/skills-sh.js +3 -3
  395. package/dist/registry/providers/static-index.js +15 -25
  396. package/dist/registry/resolve.js +43 -94
  397. package/dist/registry/semver.js +43 -0
  398. package/dist/runtime.js +81 -12
  399. package/dist/scripts/akm-migrate.js +35529 -0
  400. package/dist/setup/detect.js +5 -7
  401. package/dist/setup/detected-engines.js +136 -0
  402. package/dist/setup/engine-config.js +100 -0
  403. package/dist/setup/registry-stash-loader.js +3 -3
  404. package/dist/setup/semantic-assets.js +12 -9
  405. package/dist/setup/setup.js +444 -208
  406. package/dist/setup/steps/connection-shared.js +120 -0
  407. package/dist/setup/steps/connection.js +108 -305
  408. package/dist/setup/steps/platforms.js +13 -12
  409. package/dist/setup/steps/semantic.js +15 -3
  410. package/dist/setup/steps/sources.js +21 -15
  411. package/dist/setup/steps/stashdir.js +6 -4
  412. package/dist/setup/steps/tasks.js +236 -119
  413. package/dist/setup/steps.js +3 -2
  414. package/dist/sources/freshness.js +39 -0
  415. package/dist/sources/provider-factory.js +11 -17
  416. package/dist/sources/providers/filesystem.js +2 -3
  417. package/dist/sources/providers/git-install.js +278 -34
  418. package/dist/sources/providers/git-provider.js +54 -56
  419. package/dist/sources/providers/git-stash.js +420 -91
  420. package/dist/sources/providers/git.js +2 -2
  421. package/dist/sources/providers/npm.js +16 -19
  422. package/dist/sources/providers/provider-utils.js +47 -22
  423. package/dist/sources/providers/sync-from-ref.js +3 -9
  424. package/dist/sources/providers/website.js +2 -2
  425. package/dist/sources/resolve.js +11 -10
  426. package/dist/sources/snapshot-fetchers/types.js +4 -0
  427. package/dist/sources/{website-ingest.js → snapshot-fetchers/website-ingest.js} +110 -41
  428. package/dist/storage/database.js +60 -4
  429. package/dist/storage/engines/sqlite-migrations.js +156 -5
  430. package/dist/storage/locations.js +1 -2
  431. package/dist/storage/repositories/canaries-repository.js +1 -1
  432. package/dist/storage/repositories/events-repository.js +51 -11
  433. package/dist/storage/repositories/improve-runs-repository.js +6 -32
  434. package/dist/storage/repositories/index-connection.js +79 -0
  435. package/dist/storage/repositories/index-db.js +4 -3
  436. package/dist/storage/repositories/index-entries-repository.js +863 -0
  437. package/dist/{indexer/db/entry-mapper.js → storage/repositories/index-entry-mapper.js} +19 -2
  438. package/dist/storage/repositories/index-entry-types.js +4 -0
  439. package/dist/storage/repositories/index-fts-repository.js +167 -0
  440. package/dist/storage/repositories/index-llm-cache-repository.js +108 -0
  441. package/dist/storage/repositories/index-meta-repository.js +49 -0
  442. package/dist/{indexer/db/schema.js → storage/repositories/index-schema.js} +226 -100
  443. package/dist/storage/repositories/index-sql.js +12 -0
  444. package/dist/storage/repositories/index-utility-repository.js +356 -0
  445. package/dist/storage/repositories/index-vec-repository.js +250 -0
  446. package/dist/storage/repositories/outcome-repository.js +119 -0
  447. package/dist/storage/repositories/proposals-repository.js +317 -75
  448. package/dist/storage/repositories/registry-cache.js +1 -1
  449. package/dist/storage/repositories/salience-repository.js +172 -0
  450. package/dist/storage/repositories/task-history-repository.js +110 -3
  451. package/dist/storage/repositories/workflow-runs-repository.js +240 -19
  452. package/dist/tasks/backends/cron.js +169 -46
  453. package/dist/tasks/backends/exec-utils.js +76 -3
  454. package/dist/tasks/backends/index.js +6 -9
  455. package/dist/tasks/backends/launchd.js +292 -55
  456. package/dist/tasks/backends/schtasks.js +557 -70
  457. package/dist/tasks/backends/types.js +4 -0
  458. package/dist/tasks/command-executable.js +93 -0
  459. package/dist/tasks/embedded.js +56 -38
  460. package/dist/tasks/parser.js +156 -64
  461. package/dist/tasks/resolve-akm-bin.js +144 -51
  462. package/dist/tasks/runner.js +377 -209
  463. package/dist/tasks/schedule.js +108 -19
  464. package/dist/tasks/scheduler-invocation.js +296 -0
  465. package/dist/tasks/schema.js +1 -1
  466. package/dist/tasks/task-id.js +35 -0
  467. package/dist/tasks/validator.js +30 -16
  468. package/dist/text-import-hook.mjs +1 -1
  469. package/dist/workflows/authoring/authoring.js +104 -43
  470. package/dist/workflows/authoring/scope-key.js +1 -1
  471. package/dist/workflows/cli.js +0 -16
  472. package/dist/workflows/concurrency-policy.js +15 -0
  473. package/dist/workflows/exec/brief.js +450 -0
  474. package/dist/workflows/exec/frozen-judge.js +47 -0
  475. package/dist/workflows/exec/native-executor.js +1038 -0
  476. package/dist/workflows/exec/param-secrets.js +115 -0
  477. package/dist/workflows/exec/report.js +1460 -0
  478. package/dist/workflows/exec/run-workflow.js +602 -0
  479. package/dist/workflows/exec/scheduler.js +71 -0
  480. package/dist/workflows/exec/step-work.js +1190 -0
  481. package/dist/workflows/exec/unit-writer.js +23 -0
  482. package/dist/workflows/exec/workflow-engine-gate.js +67 -0
  483. package/dist/workflows/exec/worktree.js +171 -0
  484. package/dist/workflows/ir/compile.js +246 -0
  485. package/dist/workflows/ir/freeze.js +233 -0
  486. package/dist/workflows/ir/params.js +54 -0
  487. package/dist/workflows/ir/plan-hash.js +68 -0
  488. package/dist/workflows/ir/schema.js +540 -0
  489. package/dist/workflows/parser.js +878 -304
  490. package/dist/workflows/program/expressions.js +181 -0
  491. package/dist/workflows/program/schema.js +51 -0
  492. package/dist/workflows/renderer.js +100 -45
  493. package/dist/workflows/resource-limits.js +22 -0
  494. package/dist/workflows/runtime/agent-identity.js +59 -14
  495. package/dist/workflows/runtime/checkin.js +1 -1
  496. package/dist/workflows/runtime/plan-classifier.js +131 -0
  497. package/dist/workflows/runtime/runs.js +376 -119
  498. package/dist/workflows/runtime/unit-checkin.js +45 -0
  499. package/dist/workflows/runtime/unit-phases.js +20 -0
  500. package/dist/workflows/runtime/workflow-asset-loader.js +241 -40
  501. package/dist/workflows/schema.js +1 -11
  502. package/dist/workflows/validate-summary.js +2 -3
  503. package/dist/workflows/validator.js +52 -30
  504. package/docs/README.md +42 -78
  505. package/docs/migration/README.md +8 -0
  506. package/docs/migration/release-notes/0.6.0.md +1 -1
  507. package/docs/migration/release-notes/0.7.0.md +9 -8
  508. package/docs/migration/release-notes/0.9.0.md +158 -14
  509. package/docs/migration/v0.7-to-v0.8.md +46 -47
  510. package/docs/migration/v0.8-to-v0.9.md +844 -0
  511. package/docs/reference/README.md +12 -0
  512. package/docs/reference/data-and-telemetry.md +333 -0
  513. package/package.json +21 -17
  514. package/schemas/akm-asset-envelope.json +93 -0
  515. package/schemas/akm-config.json +4636 -0
  516. package/schemas/akm-task.json +87 -0
  517. package/schemas/akm-workflow.json +373 -0
  518. package/dist/akm-migrate-storage +0 -38
  519. package/dist/assets/help/help-accept.md +0 -12
  520. package/dist/assets/help/help-improve.md +0 -84
  521. package/dist/assets/help/help-proposals.md +0 -17
  522. package/dist/assets/help/help-propose.md +0 -17
  523. package/dist/assets/help/help-reject.md +0 -11
  524. package/dist/assets/profiles/frequent.json +0 -13
  525. package/dist/assets/profiles/recombine-only.json +0 -21
  526. package/dist/assets/profiles/reflect-distill.json +0 -30
  527. package/dist/assets/profiles/synthesize.json +0 -15
  528. package/dist/assets/prompts/procedural-system.md +0 -44
  529. package/dist/assets/prompts/recombine-system.md +0 -40
  530. package/dist/assets/prompts/staleness-detect-system.md +0 -6
  531. package/dist/assets/tasks/core/backup.yml +0 -4
  532. package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
  533. package/dist/assets/templates/html/default.html +0 -78
  534. package/dist/assets/templates/html/vendor/echarts.min.js +0 -45
  535. package/dist/assets/wiki/index-template.md +0 -12
  536. package/dist/assets/wiki/ingest-workflow-template.md +0 -83
  537. package/dist/assets/wiki/log-template.md +0 -8
  538. package/dist/assets/wiki/schema-template.md +0 -61
  539. package/dist/cli/config-migrate.js +0 -150
  540. package/dist/cli/config-validate.js +0 -39
  541. package/dist/commands/graph/graph-cli.js +0 -124
  542. package/dist/commands/graph/graph.js +0 -487
  543. package/dist/commands/improve/calibration.js +0 -161
  544. package/dist/commands/improve/dedup.js +0 -482
  545. package/dist/commands/improve/extract-watch.js +0 -140
  546. package/dist/commands/improve/hot-probation.js +0 -45
  547. package/dist/commands/improve/improve-auto-accept.js +0 -276
  548. package/dist/commands/improve/improve-profiles.js +0 -168
  549. package/dist/commands/improve/procedural.js +0 -398
  550. package/dist/commands/improve/recombine.js +0 -818
  551. package/dist/commands/improve/schema-similarity-gate.js +0 -168
  552. package/dist/commands/lint/agent-linter.js +0 -44
  553. package/dist/commands/lint/command-linter.js +0 -44
  554. package/dist/commands/lint/default-linter.js +0 -16
  555. package/dist/commands/lint/fact-linter.js +0 -39
  556. package/dist/commands/lint/knowledge-linter.js +0 -16
  557. package/dist/commands/lint/memory-linter.js +0 -61
  558. package/dist/commands/lint/registry.js +0 -41
  559. package/dist/commands/lint/skill-linter.js +0 -45
  560. package/dist/commands/lint/task-linter.js +0 -50
  561. package/dist/commands/lint/workflow-linter.js +0 -81
  562. package/dist/commands/proposal/legacy-import.js +0 -115
  563. package/dist/commands/sources/history.js +0 -196
  564. package/dist/commands/tasks/default-tasks.js +0 -186
  565. package/dist/commands/wiki-cli.js +0 -292
  566. package/dist/core/asset/asset-registry.js +0 -76
  567. package/dist/core/asset/asset-spec.js +0 -259
  568. package/dist/core/config/config-migration.js +0 -602
  569. package/dist/core/deep-merge.js +0 -38
  570. package/dist/core/eval/rank-metrics.js +0 -113
  571. package/dist/core/ripgrep/install.js +0 -163
  572. package/dist/core/ripgrep/resolve.js +0 -81
  573. package/dist/indexer/db/db.js +0 -1413
  574. package/dist/indexer/manifest.js +0 -170
  575. package/dist/indexer/passes/metadata-contributors.js +0 -31
  576. package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -94
  577. package/dist/integrations/harnesses/opencode-sdk/index.js +0 -49
  578. package/dist/llm/call-ai.js +0 -62
  579. package/dist/llm/memory-infer-impl.js +0 -138
  580. package/dist/output/shapes/distill.js +0 -14
  581. package/dist/output/shapes/history.js +0 -11
  582. package/dist/output/text/distill.js +0 -6
  583. package/dist/output/text/enable-disable.js +0 -8
  584. package/dist/output/text/history.js +0 -6
  585. package/dist/output/text/wiki.js +0 -16
  586. package/dist/registry/build-index.js +0 -386
  587. package/dist/scripts/migrate-storage.js +0 -19108
  588. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +0 -9411
  589. package/dist/scripts/migrations/v16-to-v17.js +0 -141
  590. package/dist/setup/legacy-config.js +0 -106
  591. package/dist/storage/repositories/consolidation-repository.js +0 -38
  592. package/dist/storage/repositories/recombine-repository.js +0 -213
  593. package/dist/wiki/wiki-templates.js +0 -15
  594. package/dist/wiki/wiki.js +0 -1012
  595. package/dist/workflows/db.js +0 -215
  596. package/docs/data-and-telemetry.md +0 -226
  597. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/registry.js +0 -0
  598. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/youtube.js +0 -0
@@ -4,46 +4,47 @@
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { assertNever } from "../../core/assert.js";
7
+ import { parseRefInput } from "../../core/asset/resolve-ref.js";
7
8
  import { daysToMs } from "../../core/common.js";
8
- import { loadConfig } from "../../core/config/config.js";
9
- import { rethrowIfTestIsolationError } from "../../core/errors.js";
9
+ import { bundlesToSourceEntries, loadConfig } from "../../core/config/config.js";
10
+ import { ConfigError, rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
10
11
  import { appendEvent, readEvents } from "../../core/events.js";
11
12
  import { classifyImproveAction, foldDistillSkipped } from "../../core/improve-types.js";
13
+ import { resolveMutationTarget } from "../../core/mutation-target.js";
12
14
  import { getDbPath, getStateDbPathInDataDir } from "../../core/paths.js";
15
+ import { redactSensitiveText } from "../../core/redaction.js";
13
16
  import { openStateDatabase } from "../../core/state-db.js";
14
17
  import { info, warn } from "../../core/warn.js";
15
- import { closeDatabase, getEntryCount, openExistingDatabase } from "../../indexer/db/db.js";
18
+ import { resolveWritable, resolveWriteTarget } from "../../core/write-source.js";
16
19
  import { ensureIndex } from "../../indexer/ensure-index.js";
17
20
  import { akmIndex } from "../../indexer/indexer.js";
18
- import { resolveSourceEntries } from "../../indexer/search/search-source.js";
19
- import { resolveTriageJudgmentRunner } from "../../integrations/agent/runner.js";
21
+ import { resolveEntryContentDir, resolveSourceEntries } from "../../indexer/search/search-source.js";
22
+ import { collectEngineCredentialValues } from "../../integrations/agent/engine-resolution.js";
23
+ import { materializeLlmRunnerConnection } from "../../integrations/agent/runner.js";
20
24
  import { installLlmUsagePersistence } from "../../llm/usage-persist.js";
21
- import { withLlmStage } from "../../llm/usage-telemetry.js";
22
- import { isGitBackedStash, resolveWritableOverride, saveGitStash } from "../../sources/providers/git.js";
25
+ import { isGitBackedStash, listGitChangedPaths, resolveWritableOverride, saveGitStash, } from "../../sources/providers/git.js";
26
+ import { closeDatabase, openExistingDatabase } from "../../storage/repositories/index-connection.js";
27
+ import { getEntryCount } from "../../storage/repositories/index-entries-repository.js";
23
28
  import { drainProposals } from "../proposal/drain.js";
24
29
  import { resolveDrainPolicy } from "../proposal/drain-policies.js";
30
+ import { describeGatedLanes, isAutonomyLaneAllowed } from "./autonomy-gate.js";
25
31
  import { akmDistill } from "./distill.js";
26
32
  // Eligibility / candidate-selection predicates live in ./eligibility.
27
- import { buildLatestProposalTsMap, collectEligibleRefs, memoryCleanupParentRef, resolveImproveScope, shouldAnalyzeMemoryCleanup, } from "./eligibility.js";
33
+ import { buildLatestProposalTsMap, collectEligibleRefs, collectEligibleRefsReadOnly, memoryCleanupParentRef, resolveImproveScope, shouldAnalyzeMemoryCleanup, } from "./eligibility.js";
28
34
  import { countEvalCases } from "./eval-cases.js";
29
- import { resolveImproveProfile, resolveProcessEnabled } from "./improve-profiles.js";
30
- // #607 per-process lock primitives live in ./locks. Imported for internal use;
31
- // resetHeldProcessLocks is re-exported (the test seam imports it from here).
32
- import { PROCESS_LOCK_DEFS, processLockPath, releaseAllProcessLocks, releaseHeldLocksIfOwned, releaseProcessLock, tryAcquireProcessLock, withOptionalProcessLock, } from "./locks.js";
35
+ import { resolveImprovePlan } from "./improve-strategies.js";
36
+ import { improveLockPath, MIN_IMPROVE_LOCK_STALE_MS, releaseImproveLock, tryAcquireImproveLock } from "./locks.js";
33
37
  // The cycle loop / post-loop / maintenance stages live in ./loop-stages.
34
38
  import { runImproveLoopStage, runImprovePostLoopStage } from "./loop-stages.js";
35
- import { detectAndWriteContradictions } from "./memory/memory-contradiction-detect.js";
36
39
  import { analyzeMemoryCleanup } from "./memory/memory-improve.js";
37
40
  // The pre-loop preparation pipeline lives in ./preparation.
38
41
  import { runImprovePreparationStage } from "./preparation.js";
39
42
  import { DEFAULT_DUE_DAYS, filterProactiveDue } from "./proactive-maintenance.js";
40
43
  import { akmReflect } from "./reflect.js";
44
+ import { createRunContext } from "./run-context.js";
41
45
  import { errMessage } from "./shared.js";
42
- export { resetHeldProcessLocks } from "./locks.js";
43
46
  // Re-exported from ./loop-stages for test importers (improve-db-locking).
44
47
  export { runImproveMaintenancePasses } from "./loop-stages.js";
45
- // Re-exported from ./preparation so existing importers (tests, callers) resolve.
46
- export { maybeAutoTuneThreshold } from "./preparation.js";
47
48
  export function renderSyncCommitMessage(template, result, nowMs) {
48
49
  const iso = new Date(nowMs).toISOString();
49
50
  const tokens = {
@@ -57,7 +58,7 @@ export function renderSyncCommitMessage(template, result, nowMs) {
57
58
  triage_rejected: String(result.triage?.rejected ?? 0),
58
59
  runId: result.runId ?? "",
59
60
  };
60
- return template.replace(/\{(\w+)\}/g, (match, key) => (Object.hasOwn(tokens, key) ? tokens[key] : match));
61
+ return template.replace(/\{(\w+)\}/g, (match, key) => tokens[key] ?? match);
61
62
  }
62
63
  export function armBudgetWatchdog(budgetMs, controller, deps) {
63
64
  const setTimeoutFn = deps?.setTimeoutFn ?? setTimeout;
@@ -84,6 +85,234 @@ export function armBudgetWatchdog(budgetMs, controller, deps) {
84
85
  };
85
86
  }
86
87
  export async function akmImprove(options = {}) {
88
+ const setup = resolveImproveRunSetup(options);
89
+ options = setup.options;
90
+ const { budgetMs, budgetAbortController, scope, selectedStrategy, syncRepoDir, resolvedStateDbPath, resolvedLockPath, lockStaleAfterMs, } = setup;
91
+ let clearBudgetTimer = () => { };
92
+ let initialGitPaths = new Set();
93
+ const preEnsureCleanupWarnings = [];
94
+ // Assigned by runIndexAndCollect() (closure) so TS cannot prove definite
95
+ // assignment — seed with empty values; the runIndexAndCollect() call below
96
+ // always overwrites them before any read.
97
+ let plannedRefs = [];
98
+ let memorySummary = { eligible: 0, derived: 0 };
99
+ let strategyFilteredRefs = [];
100
+ let memoryCleanupPlan;
101
+ let autonomyGatedDirectLanes = [];
102
+ let guidance;
103
+ let triageDrain;
104
+ let improveLockOwnership;
105
+ let exitBackstop;
106
+ // I1: open a single state.db connection for the main improve run so all
107
+ // appendEvent calls reuse one handle instead of open/migrate/close per call.
108
+ let eventsDb;
109
+ // Start boundary-pinned for prepass telemetry, then replace this binding with
110
+ // the long-lived handle after the prepass. The usage sink resolves it per
111
+ // append, so one owner and counter span both lifecycle phases.
112
+ let eventsCtx = { dbPath: resolvedStateDbPath };
113
+ let disposeLlmUsageSink = () => { };
114
+ const releaseRunLock = () => {
115
+ const ownership = improveLockOwnership;
116
+ if (!ownership)
117
+ return;
118
+ improveLockOwnership = undefined;
119
+ try {
120
+ releaseImproveLock(ownership);
121
+ }
122
+ catch {
123
+ // Best-effort cleanup. Exact ownership prevents deleting a successor.
124
+ }
125
+ };
126
+ const commitStashBatch = makeCommitStashBatch({
127
+ run: setup,
128
+ getInitialGitPaths: () => initialGitPaths,
129
+ // Captured via getter: the live `eventsCtx` binding is reassigned after the
130
+ // prepass, and the catch-path sync must write through the current context.
131
+ getEventsCtx: () => eventsCtx,
132
+ });
133
+ if (!options.dryRun) {
134
+ const remainingBudget = budgetAbortController.signal.remainingBudgetMs;
135
+ clearBudgetTimer = armBudgetWatchdog(Math.max(1, remainingBudget ?? budgetMs), budgetAbortController);
136
+ }
137
+ try {
138
+ if (!options.dryRun) {
139
+ const acquisition = tryAcquireImproveLock(resolvedLockPath, lockStaleAfterMs, options.skipIfLocked, {
140
+ // R25: C2 boundary-pinned path — the long-lived handle doesn't exist yet.
141
+ dbPath: resolvedStateDbPath,
142
+ });
143
+ if (acquisition.state === "skipped") {
144
+ clearBudgetTimer();
145
+ return buildLockSkippedResult(selectedStrategy.name, scope, options.runId);
146
+ }
147
+ improveLockOwnership = acquisition.ownership;
148
+ disposeLlmUsageSink = installLlmUsagePersistence(() => eventsCtx);
149
+ exitBackstop = releaseRunLock;
150
+ process.on("exit", exitBackstop);
151
+ initialGitPaths =
152
+ syncRepoDir && isGitBackedStash(syncRepoDir) ? new Set(listGitChangedPaths(syncRepoDir)) : new Set();
153
+ // Drain the standing proposal backlog before indexing so fresh proposal
154
+ // generation sees promotions from this same serialized run.
155
+ triageDrain = await runTriagePrePass(setup);
156
+ }
157
+ // #339 fix: ensureIndex MUST run BEFORE collectEligibleRefs (inside the
158
+ // indexAndCollect helper).
159
+ const collected = await indexAndCollect({ run: setup, signal: budgetAbortController.signal });
160
+ plannedRefs = collected.plannedRefs;
161
+ memorySummary = collected.memorySummary;
162
+ strategyFilteredRefs = collected.strategyFilteredRefs;
163
+ memoryCleanupPlan = collected.memoryCleanupPlan;
164
+ autonomyGatedDirectLanes = collected.autonomyGatedDirectLanes;
165
+ guidance = collected.guidance;
166
+ preEnsureCleanupWarnings.push(...collected.warnings);
167
+ if (options.dryRun) {
168
+ const result = buildDryRunResult(setup, collected);
169
+ clearBudgetTimer();
170
+ return result;
171
+ }
172
+ }
173
+ catch (err) {
174
+ // If the live prepass fails, emit its summary and clear the owning sink
175
+ // before any run teardown. The disposer is idempotent with the main finalizer.
176
+ disposeLlmUsageSink();
177
+ clearBudgetTimer();
178
+ if (exitBackstop) {
179
+ process.removeListener("exit", exitBackstop);
180
+ exitBackstop = undefined;
181
+ }
182
+ releaseRunLock();
183
+ throw err;
184
+ }
185
+ try {
186
+ try {
187
+ eventsDb = openStateDatabase(resolvedStateDbPath);
188
+ eventsCtx = { db: eventsDb };
189
+ }
190
+ catch (err) {
191
+ rethrowIfTestIsolationError(err);
192
+ // If we cannot open state.db up-front, fall back to per-call opens — but
193
+ // still pinned to the boundary-resolved path, never a live env re-read.
194
+ eventsCtx = { dbPath: resolvedStateDbPath };
195
+ }
196
+ // WI-9.10: construct the run's RunContext here — the first point after
197
+ // run-setup where config/stashDir/eventsCtx/proposalsCtx/sourceRun/dryRun
198
+ // are all in hand. See buildImproveRunContext for exactly which
199
+ // already-resolved values back each field.
200
+ const ctx = buildImproveRunContext(setup, eventsCtx);
201
+ const seq = await runImproveStageSequence({
202
+ run: setup,
203
+ strategyFilteredRefs,
204
+ plannedRefs,
205
+ memoryCleanupPlan,
206
+ autonomyGatedDirectLanes,
207
+ memorySummary,
208
+ preEnsureCleanupWarnings,
209
+ eventsCtx,
210
+ ctx,
211
+ });
212
+ const result = finalizeImproveResult({
213
+ run: setup,
214
+ seq,
215
+ guidance,
216
+ memorySummary,
217
+ memoryCleanupPlan,
218
+ strategyFilteredRefs,
219
+ triageDrain,
220
+ eventsCtx,
221
+ });
222
+ // End-of-run BATCH auto-sync — the converged commit. Recognition is
223
+ // decoupled from the per-write path (see write-source.ts case-3): the primary
224
+ // stash writes as a filesystem source during the run, then is committed via
225
+ // the same `saveGitStash` that `akm sync` calls. The gating (git-backed
226
+ // primary stash, sync not disabled) and the NON-FATAL guarantee now live in
227
+ // `commitStashBatch` (#662); the inter-cycle and catch-path calls reuse it.
228
+ // dry-run already returned above, so this always runs on a completed live
229
+ // run. `result.sync` reflects this final commit (for a one-cycle run it is
230
+ // the only commit; for a multi-cycle run the earlier cycles were banked by
231
+ // the inter-cycle calls and this records the last batch). `result` carries
232
+ // the full `{accepted}`/`{refs}`/`{triage_*}` token data for the message.
233
+ result.sync = commitStashBatch(result);
234
+ return result;
235
+ }
236
+ catch (err) {
237
+ recordImproveFailure(err, setup, eventsCtx);
238
+ // #662 crash/abort safety net: commit whatever this run already wrote to the
239
+ // primary stash BEFORE rethrowing, so an interrupted run (mid-cycle crash or
240
+ // a cooperative budget abort that surfaces as a throw) does not leave its
241
+ // writes uncommitted until a later clean run sweeps them up. Best-effort —
242
+ // `commitStashBatch` swallows its own errors and no-ops a clean tree, so this
243
+ // never masks or supersedes the original failure being rethrown below.
244
+ commitStashBatch({ scope, plannedRefs, runId: options.runId });
245
+ throw err;
246
+ }
247
+ finally {
248
+ // #576: clear the per-run LLM usage sink BEFORE closing `eventsDb` below, so
249
+ // no late sink invocation can write through a closed handle.
250
+ disposeLlmUsageSink();
251
+ // O-1 (#364): Clear the budget abort timer so it does not keep the event
252
+ // loop alive after the run completes.
253
+ clearBudgetTimer();
254
+ // Drop ONLY our own process.exit backstop so it does not fire later (or
255
+ // accumulate across repeated in-process calls). Must NOT use
256
+ // removeAllListeners("exit") here: in the in-process model (tests and
257
+ // programmatic callers import cli.ts) that would silently destroy exit
258
+ // handlers owned by the host or other commands.
259
+ if (exitBackstop) {
260
+ process.removeListener("exit", exitBackstop);
261
+ exitBackstop = undefined;
262
+ }
263
+ releaseRunLock();
264
+ // I1: close the long-lived state.db connection opened at the top of the run.
265
+ try {
266
+ eventsDb?.close();
267
+ }
268
+ catch {
269
+ // ignore — DB may already be closed
270
+ }
271
+ }
272
+ }
273
+ /** Resolve a dry-run inspection source without adapting it into a write target. */
274
+ function resolveImproveReadSource(config, scopedRef, explicitTarget, fallbackStashDir) {
275
+ if (scopedRef?.origin && explicitTarget && scopedRef.origin !== explicitTarget) {
276
+ throw new UsageError(`Qualified ref bundle "${scopedRef.origin}" conflicts with --target "${explicitTarget}".`, "INVALID_FLAG_VALUE", `Drop --target or use --target ${scopedRef.origin}.`);
277
+ }
278
+ const selector = scopedRef?.origin ?? explicitTarget ?? config.defaultWriteTarget;
279
+ if (!selector && fallbackStashDir) {
280
+ return { source: { name: "stash", path: fallbackStashDir } };
281
+ }
282
+ const configuredSelector = selector ?? config.defaultBundle;
283
+ if (configuredSelector) {
284
+ const entry = bundlesToSourceEntries(config)?.find((source) => source.name === configuredSelector);
285
+ if (!entry) {
286
+ throw new UsageError(`No source named "${configuredSelector}" is configured. Run \`akm bundle list\` to see available sources.`, "INVALID_FLAG_VALUE");
287
+ }
288
+ const sourcePath = resolveEntryContentDir(entry);
289
+ if (!sourcePath) {
290
+ throw new ConfigError(`Source "${configuredSelector}" has no resolvable on-disk path; improve cannot inspect this entry.`, "INVALID_CONFIG_FILE");
291
+ }
292
+ return { selector: configuredSelector, source: { name: configuredSelector, path: sourcePath } };
293
+ }
294
+ const implicit = resolveSourceEntries(undefined, config)[0];
295
+ if (!implicit) {
296
+ throw new ConfigError("no source configured; run `akm bundle create`", "STASH_DIR_NOT_FOUND");
297
+ }
298
+ return { source: { name: implicit.registryId ?? "stash", path: implicit.path } };
299
+ }
300
+ /**
301
+ * Run-setup: budget/watchdog plumbing, scope + seam resolution, the invocation
302
+ * plan, write-target resolution, the profile-defaulted options rebuild, and
303
+ * the boundary-pinned state.db/lock paths. Fully SYNCHRONOUS — the C2 boundary
304
+ * snapshot (resolvedStateDbPath) must be taken before the first await of the
305
+ * run.
306
+ */
307
+ function resolveImproveRunSetup(options) {
308
+ const startMs = Date.now();
309
+ const budgetMs = options.timeoutMs ?? 2 * 60 * 60 * 1000;
310
+ const budgetAbortController = new AbortController();
311
+ Object.defineProperty(budgetAbortController.signal, "remainingBudgetMs", {
312
+ get: () => Math.max(0, budgetMs - (Date.now() - startMs)),
313
+ enumerable: false,
314
+ configurable: true,
315
+ });
87
316
  const scope = resolveImproveScope(options.scope);
88
317
  const reflectFn = options.reflectFn ?? akmReflect;
89
318
  const distillFn = options.distillFn ?? akmDistill;
@@ -91,27 +320,67 @@ export async function akmImprove(options = {}) {
91
320
  const reindexFn = options.reindexFn ?? akmIndex;
92
321
  const drainProposalsFn = options.drainProposalsFn ?? drainProposals;
93
322
  // #616 multi-cycle test seams. Default to the real module-local fns.
94
- const collectEligibleRefsImpl = options.collectEligibleRefsFn ?? collectEligibleRefs;
323
+ const collectEligibleRefsImpl = options.collectEligibleRefsFn ?? (options.dryRun ? collectEligibleRefsReadOnly : collectEligibleRefs);
95
324
  const runImprovePreparationStageImpl = options.runImprovePreparationStageFn ?? runImprovePreparationStage;
96
325
  const runImproveLoopStageImpl = options.runImproveLoopStageFn ?? runImproveLoopStage;
97
326
  const runImprovePostLoopStageImpl = options.runImprovePostLoopStageFn ?? runImprovePostLoopStage;
98
327
  // Resolve the improve profile for this run. Profile drives type filtering,
99
- // process gating, and default autoAccept/limit values.
328
+ // process gating, and the default limit value.
100
329
  const _earlyConfig = options.config ?? loadConfig();
101
- const improveProfile = resolveImproveProfile(options.profile, _earlyConfig);
330
+ const resolvedPlan = options.resolvedPlan ??
331
+ resolveImprovePlan(options.strategy, _earlyConfig, {
332
+ repairValidationFailures: options.repairValidationFailures,
333
+ });
334
+ const selectedStrategy = resolvedPlan.strategy;
335
+ const improveSensitiveValues = collectEngineCredentialValues(_earlyConfig);
336
+ const improveProfile = selectedStrategy.config;
337
+ const scopedRef = scope.mode === "ref" && scope.value ? parseRefInput(scope.value) : undefined;
338
+ const readSource = options.dryRun
339
+ ? options.writeTarget
340
+ ? { selector: options.writeTarget.selector, source: options.writeTarget.source }
341
+ : resolveImproveReadSource(_earlyConfig, scopedRef, options.target, options.stashDir)
342
+ : undefined;
343
+ const writeTarget = options.dryRun
344
+ ? undefined
345
+ : scopedRef?.origin
346
+ ? resolveMutationTarget(_earlyConfig, scopedRef, options.writeTarget?.source.name ?? options.target).target
347
+ : (options.writeTarget ??
348
+ (options.target || _earlyConfig.defaultWriteTarget || !options.stashDir
349
+ ? resolveWriteTarget(_earlyConfig, options.target)
350
+ : {
351
+ source: { kind: "filesystem", name: "stash", path: options.stashDir },
352
+ config: {
353
+ type: "filesystem",
354
+ name: "stash",
355
+ path: options.stashDir,
356
+ writable: true,
357
+ },
358
+ }));
359
+ const selectedSource = writeTarget?.source ?? readSource?.source;
360
+ if (!selectedSource)
361
+ throw new ConfigError("improve could not resolve a source", "STASH_DIR_NOT_FOUND");
362
+ const selectedSelector = writeTarget?.selector ?? readSource?.selector;
102
363
  // Apply profile defaults — CLI flags take precedence over profile defaults.
103
364
  // Rebuild options with effective values so all downstream stage functions
104
365
  // automatically pick up the profile-driven defaults.
105
366
  options = {
106
367
  ...options,
107
- autoAccept: options.autoAccept ?? improveProfile.autoAccept,
368
+ // Pin nested calls and quality gates to the same config snapshot as the
369
+ // invocation plan. They must never reload a changed config mid-run.
370
+ config: _earlyConfig,
371
+ target: selectedSelector,
372
+ sourceName: selectedSource.name,
373
+ ...(writeTarget ? { writeTarget } : {}),
374
+ stashDir: selectedSource.path,
375
+ consolidateOptions: {
376
+ ...options.consolidateOptions,
377
+ target: selectedSelector,
378
+ ...(writeTarget ? { writeTarget } : {}),
379
+ },
108
380
  // Profile-level limit, then process-level reflect.limit as fallback.
109
381
  // CLI --limit takes precedence over both.
110
382
  limit: options.limit ?? improveProfile?.processes?.reflect?.limit ?? improveProfile.limit,
111
383
  };
112
- // #616 — bounded multi-cycle phasing. CLI/programmatic override wins over
113
- // profile.maxCycles; default 1 => single pass (byte-identical to pre-#616).
114
- const maxCycles = Math.max(1, Math.trunc(options.maxCycles ?? improveProfile.maxCycles ?? 1));
115
384
  let primaryStashDir;
116
385
  try {
117
386
  primaryStashDir = resolveSourceEntries(options.stashDir)[0]?.path;
@@ -119,6 +388,7 @@ export async function akmImprove(options = {}) {
119
388
  catch {
120
389
  primaryStashDir = undefined;
121
390
  }
391
+ const syncRepoDir = writeTarget?.source.repoPath ?? primaryStashDir;
122
392
  // C2 (#553/#554/#499): resolve the state.db path ONCE, synchronously, at the
123
393
  // command boundary — before the first `await` below. Every state.db open in
124
394
  // this run (`openStateDatabase`, every default-path `appendEvent`) is pinned
@@ -128,281 +398,303 @@ export async function akmImprove(options = {}) {
128
398
  // timeout root cause). Because beforeEach runs synchronously, env is still the
129
399
  // calling test's own at this point; we capture it before yielding the loop.
130
400
  const resolvedStateDbPath = getStateDbPathInDataDir();
131
- // #612 / WS-4 — bounded, OPT-IN per-phase auto-accept threshold auto-tune.
132
- // DEFAULT OFF: `autoTune: false` (or absent) is a complete no-op.
133
- //
134
- // WS-4 change: thresholds are now PER PHASE. The old single global mutation
135
- // of `options.autoAccept` is retired — it caused every phase to share one
136
- // calibration signal, so a reflect-dominated run could tighten the consolidate
137
- // gate (or vice-versa). Instead:
138
- // - Each `makeGateConfig` call reads the phase's stored threshold from
139
- // state.db (Migration 012) and uses it as `phaseThreshold`, overriding
140
- // the `globalThreshold` (= options.autoAccept) for that phase.
141
- // - Per-phase `maybeAutoTuneThreshold` calls fire AFTER each phase's gate
142
- // has run and persist the new threshold to state.db for the NEXT run.
143
- // - `options.autoAccept` stays unchanged (it is the operator-supplied
144
- // baseline, not a mutable run-time state).
145
- //
146
- // The global tune call is intentionally removed here. See per-phase calls
147
- // below (near each makeGateConfig / runAutoAcceptGate block).
148
- // #607 Lock decomposition: three per-process locks replace the single
149
- // `improve.lock`. Each process acquires only the lock(s) it needs, so
150
- // quick-shredder consolidate can run alongside daily reflect+distill.
151
- //
152
- // consolidate.lock — protects consolidate + memoryInference + graphExtraction (index.db writers)
153
- // reflect-distill.lock — protects reflect + distill (state.db proposal writers)
154
- // triage.lock — protects triage pre-pass (state.db proposal promotions)
155
- //
156
- // Lock base directory — same `.akm/` under the primary stash dir.
401
+ // One conservative run lock protects the complete mutation window, including
402
+ // triage, indexing, proposal work, maintenance, and final stash sync.
157
403
  const lockBaseDir = primaryStashDir ? path.join(primaryStashDir, ".akm") : path.join(options.stashDir ?? ".", ".akm");
158
- const preEnsureCleanupWarnings = [];
159
- // #616: assigned by runIndexAndCollect() (closure) so TS cannot prove definite
160
- // assignment — seed with empty values; the first runIndexAndCollect() call
161
- // (cycle 1, in the first try) always overwrites them before any read.
404
+ const resolvedLockPath = improveLockPath(lockBaseDir);
405
+ const lockStaleAfterMs = Math.max(MIN_IMPROVE_LOCK_STALE_MS, budgetMs + 10 * 60 * 1000);
406
+ const effectiveSync = { ...improveProfile.sync, ...options.sync };
407
+ return {
408
+ startMs,
409
+ budgetMs,
410
+ budgetAbortController,
411
+ scope,
412
+ reflectFn,
413
+ distillFn,
414
+ ensureIndexFn,
415
+ reindexFn,
416
+ drainProposalsFn,
417
+ collectEligibleRefsImpl,
418
+ runImprovePreparationStageImpl,
419
+ runImproveLoopStageImpl,
420
+ runImprovePostLoopStageImpl,
421
+ _earlyConfig,
422
+ resolvedPlan,
423
+ selectedStrategy,
424
+ improveSensitiveValues,
425
+ improveProfile,
426
+ writeTarget,
427
+ options,
428
+ primaryStashDir,
429
+ syncRepoDir,
430
+ resolvedStateDbPath,
431
+ resolvedLockPath,
432
+ lockStaleAfterMs,
433
+ effectiveSync,
434
+ };
435
+ }
436
+ /**
437
+ * WI-9.10: build the run's {@link RunContext} purely from values
438
+ * `resolveImproveRunSetup` and the long-lived state.db open already
439
+ * resolved — no second config load, no new db handle. Called once, right
440
+ * after `eventsCtx` is resolved (the last of {config, stashDir, eventsCtx,
441
+ * proposalsCtx, sourceRun, dryRun} to become available in `akmImprove`).
442
+ */
443
+ function buildImproveRunContext(run, eventsCtx) {
444
+ return createRunContext({
445
+ config: run._earlyConfig,
446
+ // primaryStashDir can fail to resolve (rare); "." mirrors the existing
447
+ // lockBaseDir fallback in resolveImproveRunSetup for the same case. This
448
+ // is a BEST-EFFORT value for the required RunContext field only — the
449
+ // loop stage reads `ImproveLoopState.primaryStashDir` (the honest
450
+ // optional), so its `if (primaryStashDir)` guards still skip on the
451
+ // unresolvable path; no RunContext consumer reads `ctx.stashDir` there.
452
+ stashDir: run.primaryStashDir ?? run.options.stashDir ?? ".",
453
+ eventsCtx,
454
+ // ProposalsContext threads only a dbPath seam (D14: no db handle is
455
+ // threaded in), mirroring eventsCtx's own dbPath-only fallback shape.
456
+ // Not yet wired into any proposal call site this stage — verb-level
457
+ // RunContext adoption (reflect/distill/extract/consolidate) is later.
458
+ proposalsCtx: { dbPath: run.resolvedStateDbPath },
459
+ // Representative connection for this run: reflect is the loop's primary
460
+ // LLM-driving process. Mirrors the existing
461
+ // `runner ? materializeLlmRunnerConnection(runner) : null` pattern
462
+ // already used for the consolidate process elsewhere (contradiction pass).
463
+ getLlmConfig: () => {
464
+ const runner = run.resolvedPlan.processes.reflect.runner;
465
+ return runner ? materializeLlmRunnerConnection(runner) : null;
466
+ },
467
+ sourceRun: run.options.runId ?? `improve-${run.startMs}`,
468
+ // Always false here: callers only reach this point past the dry-run
469
+ // early return in akmImprove.
470
+ dryRun: run.options.dryRun ?? false,
471
+ // Same AbortSignal instance armBudgetWatchdog stamped a live
472
+ // `remainingBudgetMs` getter onto (#616) — identity preserved so
473
+ // `ctx.signal.remainingBudgetMs` resolves correctly for every consumer.
474
+ signal: run.budgetAbortController.signal,
475
+ });
476
+ }
477
+ /** The catch-path improve_failed audit event (D3), redacted. */
478
+ function recordImproveFailure(err, run, eventsCtx) {
479
+ const { scope, selectedStrategy, improveSensitiveValues, startMs } = run;
480
+ // D3: emit improve_failed on unexpected crash so dashboards can detect failures.
481
+ appendEvent({
482
+ eventType: "improve_failed",
483
+ ref: scope.mode === "ref" ? scope.value : `improve:${scope.mode}:${scope.value ?? "all"}`,
484
+ metadata: {
485
+ strategy: selectedStrategy.name,
486
+ error: redactSensitiveText(errMessage(err), improveSensitiveValues),
487
+ durationMs: Date.now() - startMs,
488
+ },
489
+ }, eventsCtx);
490
+ }
491
+ /**
492
+ * ensureIndex + collectEligibleRefs + the contradiction pre-pass + the
493
+ * memory-cleanup recompute (#339 ordering). Formerly the runIndexAndCollect
494
+ * closure mutating outer `let`s; now a pure pass returning its results.
495
+ */
496
+ async function indexAndCollect(args) {
497
+ const { signal } = args;
498
+ const { scope, options, primaryStashDir, improveProfile, _earlyConfig, ensureIndexFn, collectEligibleRefsImpl } = args.run;
499
+ const warnings = [];
162
500
  let plannedRefs = [];
163
501
  let memorySummary = { eligible: 0, derived: 0 };
164
- let profileFilteredRefs = [];
165
- let memoryCleanupPlan;
166
- let guidance;
167
- let triageDrain;
168
- // #616 — ensureIndex + collectEligibleRefs + memory-cleanup recompute, lifted
169
- // into a helper so the SAME sequence runs once for cycle 1 (below, in the
170
- // first try) and is re-run at the top of each subsequent multi-cycle cycle.
171
- // Re-running ensureIndex between cycles makes cycle N's gate-promoted
172
- // proposals visible to cycle N+1's collectEligibleRefs. Mutates the
173
- // outer-scope plannedRefs/memorySummary/profileFilteredRefs/memoryCleanupPlan/
174
- // guidance so for maxCycles:1 the body is byte-identical to pre-#616.
175
- const runIndexAndCollect = async () => {
176
- // #339 fix: ensureIndex MUST run BEFORE collectEligibleRefs. The eligible-ref
177
- // query reads the `entries` table; if a DB version upgrade just dropped that
178
- // table (or the index is otherwise empty), the prior run order silently
179
- // returned plannedRefs=[] and the improve loop no-op'd. Hoisting the call
180
- // here repopulates the index first so the subsequent query sees fresh data.
181
- if (primaryStashDir) {
182
- // Probe pre-ensureIndex entry count to drive the loud-fail warning below.
183
- // Best-effort: a missing DB / unreadable schema is the fresh-install case
184
- // and not a bug — we silently skip the probe.
185
- let preEnsureEntryCount;
186
- try {
187
- const dbPath = getDbPath();
188
- if (fs.existsSync(dbPath)) {
189
- const probeDb = openExistingDatabase();
190
- try {
191
- preEnsureEntryCount = getEntryCount(probeDb);
192
- }
193
- finally {
194
- closeDatabase(probeDb);
195
- }
196
- }
197
- }
198
- catch (err) {
199
- rethrowIfTestIsolationError(err);
200
- // best-effort; leave preEnsureEntryCount undefined
201
- }
202
- try {
203
- await ensureIndexFn(primaryStashDir, { mode: "blocking" });
204
- }
205
- catch (err) {
206
- preEnsureCleanupWarnings.push(`ensureIndex failed: ${errMessage(err)}`);
207
- }
208
- // #339 loud-fail: if the index was empty pre-ensureIndex but is now
209
- // populated, a version-upgrade-triggered rebuild just happened. Surface
210
- // that on stderr so the improve run is not silently masked by stale
211
- // index state. Zero-before AND zero-after is the empty-stash case and
212
- // is intentionally not warned (not a bug).
213
- if (preEnsureEntryCount === 0) {
502
+ let strategyFilteredRefs = [];
503
+ // #339 fix: ensureIndex MUST run BEFORE collectEligibleRefs. The eligible-ref
504
+ // query reads the `entries` table; if a DB version upgrade just dropped that
505
+ // table (or the index is otherwise empty), the prior run order silently
506
+ // returned plannedRefs=[] and the improve loop no-op'd. Hoisting the call
507
+ // here repopulates the index first so the subsequent query sees fresh data.
508
+ if (primaryStashDir && !options.dryRun) {
509
+ // Probe pre-ensureIndex entry count to drive the loud-fail warning below.
510
+ // Best-effort: a missing DB / unreadable schema is the fresh-install case
511
+ // and not a bug — we silently skip the probe.
512
+ let preEnsureEntryCount;
513
+ try {
514
+ const dbPath = getDbPath();
515
+ if (fs.existsSync(dbPath)) {
516
+ const probeDb = openExistingDatabase();
214
517
  try {
215
- const probeDb = openExistingDatabase();
216
- let postCount = 0;
217
- try {
218
- postCount = getEntryCount(probeDb);
219
- }
220
- finally {
221
- closeDatabase(probeDb);
222
- }
223
- if (postCount > 0) {
224
- warn("[improve] index was empty after DB version upgrade — repopulating before continuing");
225
- }
518
+ preEnsureEntryCount = getEntryCount(probeDb);
226
519
  }
227
- catch (err) {
228
- rethrowIfTestIsolationError(err);
229
- // best-effort
520
+ finally {
521
+ closeDatabase(probeDb);
230
522
  }
231
523
  }
232
524
  }
233
- ({ plannedRefs, memorySummary, profileFilteredRefs } = await collectEligibleRefsImpl(scope, options.stashDir, improveProfile));
234
- const cleanupParentRef = memoryCleanupParentRef(scope, options.stashDir);
235
- // M-1 (#367): Run contradiction-detection BEFORE analyzeMemoryCleanup so
236
- // the SCC resolver in resolveFamilyContradictions has edges to work on.
237
- // Best-effort: failures are warnings, never fatal.
238
- if (primaryStashDir && shouldAnalyzeMemoryCleanup(scope, memorySummary.eligible, primaryStashDir)) {
525
+ catch (err) {
526
+ rethrowIfTestIsolationError(err);
527
+ // best-effort; leave preEnsureEntryCount undefined
528
+ }
529
+ try {
530
+ await ensureIndexFn(primaryStashDir, { mode: "blocking", signal: signal });
531
+ }
532
+ catch (err) {
533
+ if (signal.aborted)
534
+ throw err;
535
+ warnings.push(`ensureIndex failed: ${errMessage(err)}`);
536
+ }
537
+ // #339 loud-fail: if the index was empty pre-ensureIndex but is now
538
+ // populated, a version-upgrade-triggered rebuild just happened. Surface
539
+ // that on stderr so the improve run is not silently masked by stale
540
+ // index state. Zero-before AND zero-after is the empty-stash case and
541
+ // is intentionally not warned (not a bug).
542
+ if (preEnsureEntryCount === 0) {
239
543
  try {
240
- // Reuse the config resolved at the top of the run instead of a second load.
241
- await withLlmStage("memory-contradiction", () => detectAndWriteContradictions(primaryStashDir, _earlyConfig));
544
+ const probeDb = openExistingDatabase();
545
+ let postCount = 0;
546
+ try {
547
+ postCount = getEntryCount(probeDb);
548
+ }
549
+ finally {
550
+ closeDatabase(probeDb);
551
+ }
552
+ if (postCount > 0) {
553
+ warn("[improve] index was empty after DB version upgrade — repopulating before continuing");
554
+ }
242
555
  }
243
556
  catch (err) {
244
- // Non-fatal: contradiction detection is a best-effort pass.
245
- warn(`[improve] contradiction detection failed (non-fatal): ${errMessage(err)}`);
557
+ rethrowIfTestIsolationError(err);
558
+ // best-effort
246
559
  }
247
560
  }
248
- memoryCleanupPlan = shouldAnalyzeMemoryCleanup(scope, memorySummary.eligible, primaryStashDir)
249
- ? analyzeMemoryCleanup(primaryStashDir, cleanupParentRef ? { parentRef: cleanupParentRef } : undefined)
250
- : undefined;
251
- guidance =
252
- memorySummary.eligible > 0
253
- ? "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."
254
- : undefined;
561
+ }
562
+ ({
563
+ plannedRefs,
564
+ memorySummary,
565
+ strategyFilteredRefs = [],
566
+ } = await collectEligibleRefsImpl(scope, options.stashDir, improveProfile, _earlyConfig));
567
+ const cleanupParentRef = memoryCleanupParentRef(scope, options.stashDir);
568
+ // D8 — the two direct lanes share one eligibility predicate, which reads scope
569
+ // and eligible-memory count and no strategy flag at all. Compute it once: it
570
+ // decides whether each lane would have run, which is also what decides whether
571
+ // a gated lane is worth REPORTING. A lane that was never eligible was not
572
+ // suppressed by the gate, so claiming it was would be noise.
573
+ const cleanupEligible = shouldAnalyzeMemoryCleanup(scope, memorySummary.eligible, primaryStashDir);
574
+ // Cleanup remains independent of the disabled contradiction writer.
575
+ const memoryCleanupPlan = cleanupEligible
576
+ ? analyzeMemoryCleanup(primaryStashDir, cleanupParentRef ? { parentRef: cleanupParentRef } : undefined)
577
+ : undefined;
578
+ const cleanupWouldMutate = Boolean(memoryCleanupPlan &&
579
+ (memoryCleanupPlan.pruneCandidates.length > 0 ||
580
+ memoryCleanupPlan.beliefStateTransitions.length > 0 ||
581
+ memoryCleanupPlan.relativeDateCandidates.length > 0));
582
+ const autonomyGatedDirectLanes = cleanupWouldMutate && !isAutonomyLaneAllowed("memoryCleanup", _earlyConfig) ? ["memoryCleanup"] : [];
583
+ const guidance = memorySummary.eligible > 0
584
+ ? "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."
585
+ : undefined;
586
+ return {
587
+ plannedRefs,
588
+ memorySummary,
589
+ strategyFilteredRefs: strategyFilteredRefs ?? [],
590
+ memoryCleanupPlan,
591
+ autonomyGatedDirectLanes,
592
+ guidance,
593
+ warnings,
255
594
  };
256
- // Holds our own process.on("exit") backstop so the finally can remove EXACTLY
257
- // that handler (not every exit listener in the process). Declared in the scope
258
- // shared by the try and its finally; assigned when the backstop is registered.
259
- let exitBackstop;
260
- try {
261
- // #607: Per-process lock acquisition. Each process acquires only the lock(s)
262
- // it needs. The dry-run branch produces plannedRefs/memorySummary WITHOUT any
263
- // locks (decision: dry-run never mutates the queue).
264
- if (!options.dryRun) {
265
- // Backstop release on process.exit() (signal handler / budget watchdog),
266
- // which skips the finally below. Removed in that finally on the normal path.
267
- const releaseAllOnExit = () => releaseHeldLocksIfOwned(process.pid);
268
- exitBackstop = releaseAllOnExit;
269
- process.on("exit", releaseAllOnExit);
270
- // #607 triage pre-pass: acquire triage.lock, drain the standing pending
271
- // backlog BEFORE ensureIndex so improve generates fresh proposals against
272
- // a cleared queue (no `duplicate_pending` collisions) and ensureIndex
273
- // absorbs triage's promotions for free. Release immediately after —
274
- // triage.lock is not needed again until the next improve run.
275
- if (primaryStashDir && resolveProcessEnabled("triage", improveProfile)) {
276
- if (scope.mode === "ref") {
277
- warn("[improve] triage pre-pass skipped (single-ref scope never drains the whole queue)");
278
- }
279
- else {
280
- const triageLPath = processLockPath(lockBaseDir, "triage");
281
- const triageResult = tryAcquireProcessLock(triageLPath, PROCESS_LOCK_DEFS.triage.staleAfterMs, options.skipIfLocked, "triage");
282
- if (triageResult === "skipped") {
283
- triageDrain = undefined;
284
- }
285
- else {
286
- try {
287
- const triageConfig = improveProfile.processes?.triage;
288
- const policy = resolveDrainPolicy(triageConfig?.policy);
289
- const applyMode = triageConfig?.applyMode ?? "queue";
290
- const maxAccepts = triageConfig?.maxAcceptsPerRun ?? 25;
291
- const judgment = triageConfig?.judgment
292
- ? resolveTriageJudgmentRunner(triageConfig.judgment, _earlyConfig)
293
- : null;
294
- triageDrain = await drainProposalsFn({
295
- stashDir: primaryStashDir,
296
- policy,
297
- applyMode,
298
- maxAccepts,
299
- dryRun: false,
300
- excludeIds: new Set(),
301
- ...(triageConfig?.maxDiffLines !== undefined ? { maxDiffLines: triageConfig.maxDiffLines } : {}),
302
- judgment,
303
- });
304
- }
305
- catch (err) {
306
- warn(`[improve] triage pre-pass failed (non-fatal): ${errMessage(err)}`);
307
- }
308
- finally {
309
- releaseProcessLock(triageLPath);
310
- }
311
- }
312
- }
313
- }
595
+ }
596
+ /** The P2 lock-skipped envelope (graceful `skipIfLocked` early return). Exported for unit tests. */
597
+ export function buildLockSkippedResult(strategyName, scope, runId) {
598
+ return {
599
+ schemaVersion: 2,
600
+ ok: true,
601
+ strategy: strategyName,
602
+ scope,
603
+ dryRun: false,
604
+ skipped: { reason: "lock-held" },
605
+ memorySummary: { eligible: 0, derived: 0 },
606
+ plannedRefs: [],
607
+ actions: [],
608
+ ...(runId !== undefined ? { runId } : {}),
609
+ };
610
+ }
611
+ /** The P3 dry-run envelope (plan-only early return). Exported for unit tests. */
612
+ export function buildDryRunResult(run, collected) {
613
+ const { selectedStrategy, scope } = run;
614
+ const { guidance, memorySummary, memoryCleanupPlan, plannedRefs, strategyFilteredRefs } = collected;
615
+ return {
616
+ schemaVersion: 2,
617
+ ok: true,
618
+ strategy: selectedStrategy.name,
619
+ scope,
620
+ dryRun: true,
621
+ ...(guidance ? { guidance } : {}),
622
+ memorySummary,
623
+ ...(memoryCleanupPlan ? { memoryCleanup: shapeMemoryCleanup(memoryCleanupPlan) } : {}),
624
+ plannedRefs,
625
+ ...(strategyFilteredRefs.length > 0 ? { strategyFilteredRefs } : {}),
626
+ };
627
+ }
628
+ /** The triage drain pre-pass (non-fatal; single-ref scope never drains). */
629
+ async function runTriagePrePass(run) {
630
+ const { primaryStashDir, resolvedPlan, scope, options, improveProfile, drainProposalsFn } = run;
631
+ let triageDrain;
632
+ if (primaryStashDir && resolvedPlan.processes.triage.enabled) {
633
+ if (scope.mode === "ref") {
634
+ warn("[improve] triage pre-pass skipped (single-ref scope never drains the whole queue)");
314
635
  }
315
- // #339 fix: ensureIndex MUST run BEFORE collectEligibleRefs (now inside the
316
- // helper). Cycle 1 runs it here; subsequent multi-cycle cycles re-run it via
317
- // the same helper at the top of each cycle below.
318
- await runIndexAndCollect();
319
- if (options.dryRun) {
320
- const result = {
321
- schemaVersion: 1,
322
- ok: true,
323
- scope,
324
- dryRun: true,
325
- ...(guidance ? { guidance } : {}),
326
- memorySummary,
327
- ...(memoryCleanupPlan ? { memoryCleanup: shapeMemoryCleanup(memoryCleanupPlan) } : {}),
328
- plannedRefs,
329
- ...(profileFilteredRefs.length > 0 ? { profileFilteredRefs } : {}),
330
- };
331
- return result;
636
+ else {
637
+ try {
638
+ const triageConfig = improveProfile.processes?.triage;
639
+ const policy = resolveDrainPolicy(triageConfig?.policy);
640
+ const applyMode = triageConfig?.applyMode ?? "queue";
641
+ const maxAccepts = triageConfig?.maxAcceptsPerRun ?? 25;
642
+ triageDrain = await drainProposalsFn({
643
+ stashDir: primaryStashDir,
644
+ ...(options.target ? { target: options.target } : {}),
645
+ config: options.config,
646
+ policy,
647
+ applyMode,
648
+ maxAccepts,
649
+ dryRun: false,
650
+ excludeIds: new Set(),
651
+ ...(triageConfig?.maxDiffLines !== undefined ? { maxDiffLines: triageConfig.maxDiffLines } : {}),
652
+ judgment: resolvedPlan.triageJudgment,
653
+ });
654
+ }
655
+ catch (err) {
656
+ warn(`[improve] triage pre-pass failed (non-fatal): ${errMessage(err)}`);
657
+ }
332
658
  }
333
659
  }
334
- catch (err) {
335
- releaseAllProcessLocks();
336
- throw err;
337
- }
338
- // #607: per-process locks are acquired/released around each stage below.
339
- // The triage pre-pass already ran under triage.lock (released). The
340
- // preparation stage runs under consolidate.lock, the loop stage under
341
- // reflect-distill.lock, and the post-loop stage under consolidate.lock again.
342
- // Each stage acquires its lock just before starting and releases in finally.
343
- // best-effort `unlinkSync` is a no-op when no lock file exists.
344
- const startMs = Date.now();
345
- const budgetMs = options.timeoutMs ?? 2 * 60 * 60 * 1000; // default 2 hours
346
- // O-1 (#364): Create a shared AbortController derived from startMs + budgetMs.
347
- // Every async seam receives this signal so a hung sub-call cannot extend the
348
- // run past the declared budget.
349
- // References: Anthropic *Building Effective Agents* (2024); CoALA §5 (arXiv:2309.02427).
350
- const budgetAbortController = new AbortController();
351
- // Attach a live `remainingBudgetMs` getter to the signal so sub-callers
352
- // (e.g. consolidate.ts cold-start budget estimation) can read the remaining
353
- // wall-clock budget without needing an extra plumbing parameter. The property
354
- // is computed at access time via a getter so it always reflects the actual
355
- // elapsed time rather than a stale snapshot taken at arm time.
356
- Object.defineProperty(budgetAbortController.signal, "remainingBudgetMs", {
357
- get: () => Math.max(0, budgetMs - (Date.now() - startMs)),
358
- enumerable: false,
359
- configurable: true,
360
- });
361
- // Declared in the outer scope so the `finally` can clear the timer even if a
362
- // throw occurs before/after it is armed. Defaults to a no-op until armed.
363
- let clearBudgetTimer = () => { };
364
- // I1: open a single state.db connection for the entire improve run so all
365
- // appendEvent calls reuse one handle instead of open/migrate/close per call.
366
- let eventsDb;
367
- // `eventsCtx` is read by the main catch (improve_failed) and finally, so it
368
- // lives in the outer scope. It is always assigned at the top of the try.
369
- // Pinned to the boundary snapshot so the fallback per-call `appendEvent`
370
- // opens (when the long-lived handle below fails to open) never re-read env.
371
- let eventsCtx = { dbPath: resolvedStateDbPath };
372
- // #576: clears the per-run LLM usage sink. Defaults to a no-op until the sink
373
- // is installed inside the try; the `finally` always calls it.
374
- let disposeLlmUsageSink = () => { };
375
- // ── Crash-safe / incremental stash sync (#662) ──────────────────────────────
376
- // The primary stash writes as a filesystem source DURING the run
377
- // (write-source.ts case-3); those writes become a git commit only when this
378
- // closure runs. Historically the only call site was a single BATCH commit at
379
- // the very end of the happy path, so a run interrupted AFTER writing but
380
- // BEFORE finishing — a mid-cycle crash, a budget abort, or an external
381
- // SIGTERM/`process.exit` — left every write uncommitted until some LATER run
382
- // happened to finish cleanly and swept the whole backlog up. We now call this
383
- // from THREE places: between cycles (bank each completed cycle), at end-of-run
384
- // (the converged commit), and from the catch path (commit what was written
385
- // before the crash). That shrinks the worst-case loss from "the entire run" to
386
- // "the in-flight cycle".
387
- //
388
- // Declared in the OUTER scope (not inside the try) so the catch block can reach
389
- // it. Idempotent + NON-FATAL: `saveGitStash` short-circuits a clean working
390
- // tree ("nothing to commit") and a thrown sync error is swallowed here, so a
391
- // repeat call after a no-op cycle is cheap and a failed push never fails the
392
- // run. Gated identically to the original end-of-run block (git-backed primary
393
- // stash, sync not disabled). `eventsCtx` is captured by reference, so calls
394
- // after the db-backed context is installed inside the try use the live handle.
395
- const effectiveSync = { ...improveProfile.sync, ...options.sync };
396
- const commitStashBatch = (messageContext) => {
397
- if (!primaryStashDir || effectiveSync.enabled === false || !isGitBackedStash(primaryStashDir)) {
660
+ return triageDrain;
661
+ }
662
+ /**
663
+ * Crash-safe / incremental stash sync (#662) — see the factory-returned
664
+ * closure's original doc block: the primary stash writes as a filesystem
665
+ * source DURING the run; this commits them at end-of-run AND from the catch
666
+ * path. Idempotent + NON-FATAL. `getEventsCtx`/`getInitialGitPaths` are
667
+ * getters because both outer bindings are (re)assigned after this factory
668
+ * runs — the returned closure must observe the live values.
669
+ */
670
+ function makeCommitStashBatch(deps) {
671
+ const { writeTarget, primaryStashDir, effectiveSync, options, _earlyConfig, improveProfile } = deps.run;
672
+ return (messageContext) => {
673
+ const eventsCtx = deps.getEventsCtx();
674
+ const initialGitPaths = deps.getInitialGitPaths();
675
+ const repoDir = writeTarget?.source.repoPath ?? primaryStashDir;
676
+ if (!primaryStashDir || !repoDir || effectiveSync.enabled === false || !isGitBackedStash(repoDir)) {
398
677
  return undefined;
399
678
  }
400
679
  const saveGitStashFn = options.saveGitStashFn ?? saveGitStash;
401
- const writableOverride = resolveWritableOverride(_earlyConfig);
402
- const push = effectiveSync.push !== false;
680
+ const writableOverride = writeTarget ? resolveWritable(writeTarget.config) : resolveWritableOverride(_earlyConfig);
681
+ const push = options.sync?.push ?? improveProfile.sync?.push ?? true;
403
682
  const message = renderSyncCommitMessage(effectiveSync.message ?? "akm improve auto-sync", messageContext, Date.now());
404
683
  try {
405
- const syncResult = saveGitStashFn(undefined, message, writableOverride, { push, repoDir: primaryStashDir });
684
+ const assetRoot = writeTarget?.source.path ?? primaryStashDir;
685
+ const assetPrefix = assetRoot ? path.relative(repoDir, assetRoot).replaceAll(path.sep, "/") : "";
686
+ const paths = listGitChangedPaths(repoDir).filter((changedPath) => {
687
+ if (initialGitPaths.has(changedPath))
688
+ return false;
689
+ if (path.basename(changedPath).includes(".lock"))
690
+ return false;
691
+ return !assetPrefix || changedPath === assetPrefix || changedPath.startsWith(`${assetPrefix}/`);
692
+ });
693
+ const syncResult = saveGitStashFn(undefined, message, writableOverride, {
694
+ push,
695
+ repoDir,
696
+ paths,
697
+ });
406
698
  appendEvent({
407
699
  eventType: "stash_synced",
408
700
  metadata: {
@@ -429,443 +721,353 @@ export async function akmImprove(options = {}) {
429
721
  return { committed: false, pushed: false, skipped: true, reason };
430
722
  }
431
723
  };
432
- try {
433
- // H7 (#566): arm the budget watchdog. `armBudgetWatchdog` captures both the
434
- // budget timer and the hard-kill timer it schedules on exhaustion, returning
435
- // a single dispose() that clears whichever are still pending. The `finally`
436
- // calls dispose() via `clearBudgetTimer` (RAII), so a clean cooperative
437
- // drain cancels the pending hard-kill before it can fire — the process then
438
- // exits naturally instead of being force-`exit(0)`-ed mid-flush, which could
439
- // truncate an in-flight log or `state.db` transaction.
440
- clearBudgetTimer = armBudgetWatchdog(budgetMs, budgetAbortController);
441
- try {
442
- eventsDb = openStateDatabase(resolvedStateDbPath);
443
- eventsCtx = { db: eventsDb };
444
- }
445
- catch (err) {
446
- rethrowIfTestIsolationError(err);
447
- // If we cannot open state.db up-front, fall back to per-call opens — but
448
- // still pinned to the boundary-resolved path, never a live env re-read.
449
- eventsCtx = { dbPath: resolvedStateDbPath };
724
+ }
725
+ /** D6: pre-load the last 30 days of proposal_rejected events once per run. */
726
+ function preloadRejectedProposals() {
727
+ // D6: pre-load all proposal_rejected events from the last 30 days once,
728
+ // so the per-asset loop can use a Map lookup instead of N DB round trips.
729
+ const REJECTED_PROPOSAL_WINDOW_MS = daysToMs(30);
730
+ const rejectedProposalSince = new Date(Date.now() - REJECTED_PROPOSAL_WINDOW_MS).toISOString();
731
+ const allRejectedProposalEvents = readEvents({ type: "proposal_rejected", since: rejectedProposalSince }).events;
732
+ const rejectedProposalsByRef = new Map();
733
+ for (const e of allRejectedProposalEvents) {
734
+ if (e.ref && (!rejectedProposalsByRef.has(e.ref) || e.ts > (rejectedProposalsByRef.get(e.ref)?.ts ?? ""))) {
735
+ rejectedProposalsByRef.set(e.ref, e);
450
736
  }
451
- // #576: persist per-call LLM usage telemetry for this run as `llm_usage`
452
- // events, reusing the same boundary-pinned events context (and long-lived
453
- // handle when available). Disposed in `finally` so the sink never leaks
454
- // across runs. Wrapping is best-effort end to end — see usage-telemetry.ts.
455
- disposeLlmUsageSink = installLlmUsagePersistence(eventsCtx);
456
- // 2026-05-27: emit an `improve_skipped` audit event for refs the planner
457
- // pre-filtered (reflect AND distill both refuse them under the active
458
- // profile). Emitted as a single summary event (count only) rather than one
459
- // event per ref (#592) — the per-ref loop caused O(n) sequential state.db
460
- // writes that consumed ~500 s on a 9 000-ref stash. No downstream consumer
461
- // needs the per-ref audit trail: health's skip histogram reads the
462
- // `profile_filtered_all_passes` counters from `improve_completed` metadata.
463
- if (profileFilteredRefs.length > 0) {
464
- appendEvent({
465
- eventType: "improve_skipped",
466
- ref: undefined,
467
- metadata: {
468
- reason: "profile_filtered_all_passes",
469
- count: profileFilteredRefs.length,
470
- },
471
- }, eventsCtx);
737
+ }
738
+ return rejectedProposalsByRef;
739
+ }
740
+ /**
741
+ * Post-lock proactive cooldown re-filter: re-read cooldown timestamps
742
+ * immediately before the loop so external proposal writes that occurred
743
+ * before this run acquired its lock are visible.
744
+ */
745
+ export function refilterProactiveLoopRefs(loopRefs, improveProfile) {
746
+ // Re-read cooldown timestamps immediately before execution so external
747
+ // proposal writes that occurred before this run acquired its lock are visible.
748
+ const proactiveLoopRefs = loopRefs.filter((r) => r.eligibilitySource === "proactive");
749
+ let postLockLoopRefs = loopRefs;
750
+ if (proactiveLoopRefs.length > 0) {
751
+ const proactiveRefStrs = proactiveLoopRefs.map((r) => r.ref);
752
+ // Correlate proposal timestamps on each candidate's durable key.
753
+ const proactiveItemRefByRef = new Map(proactiveLoopRefs.map((r) => [r.ref, r.itemRef]));
754
+ const freshReflectTs = buildLatestProposalTsMap(proactiveRefStrs, "reflect", proactiveItemRefByRef);
755
+ const freshDistillTs = buildLatestProposalTsMap(proactiveRefStrs, "distill", proactiveItemRefByRef);
756
+ const pmDueDays = improveProfile.processes?.proactiveMaintenance?.dueDays ?? DEFAULT_DUE_DAYS;
757
+ const stillDue = new Set(filterProactiveDue(proactiveLoopRefs, freshReflectTs, freshDistillTs, pmDueDays, Date.now()).map((r) => r.ref));
758
+ const dropped = proactiveLoopRefs.filter((r) => !stillDue.has(r.ref));
759
+ if (dropped.length > 0) {
760
+ info(`[improve] post-lock cooldown re-filter: dropped ${dropped.length} proactive ref(s) claimed by concurrent run (${dropped.map((r) => r.ref).join(", ")})`);
761
+ postLockLoopRefs = loopRefs.filter((r) => r.eligibilitySource !== "proactive" || stillDue.has(r.ref));
472
762
  }
473
- // #616 — bounded multi-cycle phasing. The prep->loop->post-loop sequence is
474
- // wrapped in an N-cycle loop. Each cycle re-runs ensureIndex +
475
- // collectEligibleRefs (via runIndexAndCollect) so gate-accepted output of
476
- // cycle N becomes selectable input to cycle N+1. The per-stage process locks
477
- // (consolidate / reflect-distill) are acquired+released INSIDE each cycle,
478
- // exactly as the single-pass path did. For maxCycles:1 the loop runs once and
479
- // every accumulator below collapses to the single-cycle value (sum-of-one,
480
- // concat-of-one, last==only) => BYTE-IDENTICAL to pre-#616.
481
- //
482
- // Accumulators (see CONSTRAINTS / aggregation plan in #616): SUM the count
483
- // fields and durations; CONCAT the array fields; LAST-WINS for point-in-time
484
- // objects (the final cycle's value reflects the converged state).
485
- let cyclesRun = 0;
486
- // Last-wins point-in-time values (assigned every cycle; the final cycle wins).
487
- let preparation;
488
- let memoryRefsForInference = new Set();
489
- let consolidation;
490
- let memoryInference;
491
- let graphExtraction;
492
- let recombination;
493
- let proceduralCompilation;
494
- let cycleMetrics;
495
- // Summed counters/durations.
496
- let prepGateCount = 0;
497
- let prepGateFailedCount = 0;
498
- let reflectsWithErrorContext = 0;
499
- let loopGateCount = 0;
500
- let loopGateFailedCount = 0;
501
- let postLoopGateCount = 0;
502
- let postLoopGateFailedCount = 0;
503
- let memoryInferenceDurationMs = 0;
504
- let graphExtractionDurationMs = 0;
505
- let orphansPurged;
506
- let proposalsExpired;
507
- // Concatenated arrays.
508
- const allWarnings = [];
509
- let deadUrls;
510
- const finalActions = [];
511
- for (let cycleIndex = 0; cycleIndex < maxCycles; cycleIndex++) {
512
- // #616 budget gate: never start a NEW cycle once the run's wall-clock
513
- // budget is exhausted (or the run was aborted). Cycle 0 ALWAYS runs so
514
- // maxCycles:1 is byte-identical regardless of budget.
515
- if (cycleIndex > 0) {
516
- const remaining = budgetAbortController.signal.remainingBudgetMs;
517
- if (budgetAbortController.signal.aborted || (remaining !== undefined && remaining <= 0)) {
518
- break;
519
- }
520
- }
521
- // #662 incremental sync: bank the PREVIOUS cycle's writes before starting a
522
- // new one, so a crash/abort/timeout mid-run loses at most the in-flight
523
- // cycle rather than the whole run. Guarded on `cycleIndex > 0`, so the
524
- // common maxCycles:1 path never calls this — its single end-of-run commit
525
- // below stays the only sync and the serialized envelope is byte-identical
526
- // to pre-#662. `saveGitStash` no-ops a clean tree, so a cycle that wrote
527
- // nothing costs only a `git status`.
528
- if (cycleIndex > 0) {
529
- commitStashBatch({ scope, plannedRefs, runId: options.runId });
530
- }
531
- // Re-run ensureIndex + collectEligibleRefs + memory-cleanup recompute for
532
- // cycles 2+ (cycle 1 already ran them in the first try above). This makes
533
- // cycle N's gate-promoted proposals visible to this cycle's ref selection.
534
- if (cycleIndex > 0) {
535
- await runIndexAndCollect();
536
- // Re-emit the profile-filtered audit summary for this cycle's selection.
537
- if (profileFilteredRefs.length > 0) {
538
- appendEvent({
539
- eventType: "improve_skipped",
540
- ref: undefined,
541
- metadata: { reason: "profile_filtered_all_passes", count: profileFilteredRefs.length },
542
- }, eventsCtx);
543
- }
544
- }
545
- // #607: acquire consolidate.lock for the preparation stage (consolidate,
546
- // ensureIndex, extract all write index.db). Released immediately after.
547
- const consolidateLPath = processLockPath(lockBaseDir, "consolidate");
548
- preparation = await withOptionalProcessLock({
549
- lockPath: consolidateLPath,
550
- staleAfterMs: PROCESS_LOCK_DEFS.consolidate.staleAfterMs,
551
- skipIfLocked: options.skipIfLocked,
552
- label: "consolidate",
553
- }, () => runImprovePreparationStageImpl({
763
+ }
764
+ return postLockLoopRefs;
765
+ }
766
+ /**
767
+ * Post-loop stage, or a no-op skip when the shared wall-clock budget is
768
+ * already exhausted. The result still finalizes normally on skip, so
769
+ * scheduled budget exhaustion exits 0 (extracted verbatim from
770
+ * `runImproveStageSequence` — fn-size decomposition, no logic change).
771
+ */
772
+ async function runPostLoopStageOrSkip(args) {
773
+ const { budgetAbortController, scope, options, primaryStashDir, preparation, memoryRefsForInference, reindexFn, eventsCtx, improveProfile, resolvedPlan, runImprovePostLoopStageImpl, } = args;
774
+ // Do not start new post-loop work after the shared wall-clock budget. The
775
+ // result still finalizes normally, so scheduled budget exhaustion exits 0.
776
+ const emptyPostLoopResult = {
777
+ allWarnings: [],
778
+ memoryInferenceDurationMs: 0,
779
+ graphExtractionDurationMs: 0,
780
+ };
781
+ const remainingBudget = budgetAbortController.signal.remainingBudgetMs;
782
+ if (budgetAbortController.signal.aborted || (remainingBudget !== undefined && remainingBudget <= 0)) {
783
+ info("[improve] post-loop maintenance skipped (wall-clock budget exhausted)");
784
+ return emptyPostLoopResult;
785
+ }
786
+ return runImprovePostLoopStageImpl({
787
+ scope,
788
+ options,
789
+ primaryStashDir,
790
+ actionableRefs: preparation.actionableRefs,
791
+ appliedCleanup: preparation.appliedCleanup,
792
+ cleanupWarnings: preparation.cleanupWarnings,
793
+ memoryRefsForInference,
794
+ reindexFn,
795
+ eventsCtx,
796
+ budgetSignal: budgetAbortController.signal,
797
+ improveProfile,
798
+ resolvedPlan,
799
+ consolidationRan: preparation.consolidationRan,
800
+ // R5: floor violations from this run's consolidate pass, for the collapse
801
+ // detector's merge-floor advisory.
802
+ consolidationMergeFloorViolations: preparation.consolidation.mergeFloorViolations ?? 0,
803
+ });
804
+ }
805
+ /**
806
+ * Stage-sequencing: the strategy-filtered audit event, then the single
807
+ * prep → loop → post-loop pass via the #616 seams (D12). Returns every
808
+ * accumulator the result assembly reads — the old closure-scoped `let`s.
809
+ */
810
+ async function runImproveStageSequence(args) {
811
+ const { strategyFilteredRefs, plannedRefs, memoryCleanupPlan, memorySummary, preEnsureCleanupWarnings, eventsCtx, ctx, } = args;
812
+ const { scope, options, primaryStashDir, startMs, budgetMs, improveProfile, resolvedPlan, budgetAbortController, selectedStrategy, reflectFn, distillFn, reindexFn, runImprovePreparationStageImpl, runImproveLoopStageImpl, runImprovePostLoopStageImpl, } = args.run;
813
+ // 2026-05-27: one summary `improve_skipped` audit event (count only) for
814
+ // planner-pre-filtered refs — never per-ref (#592: O(n) sequential state.db
815
+ // writes cost ~500 s on a 9 000-ref stash; health reads the counters).
816
+ if (strategyFilteredRefs.length > 0) {
817
+ appendEvent({
818
+ eventType: "improve_skipped",
819
+ ref: undefined,
820
+ metadata: {
821
+ strategy: selectedStrategy.name,
822
+ reason: "strategy_filtered_all_passes",
823
+ count: strategyFilteredRefs.length,
824
+ },
825
+ }, eventsCtx);
826
+ }
827
+ // D8 — one event per lane the autonomy gate downgraded, naming the lane AND
828
+ // the config key that would enable it. This is the difference between a gate
829
+ // and a silent no-op: whatever the user would have seen happen, they now see
830
+ // explained. `health` already aggregates `improve_skipped` by reason
831
+ // (buildImproveSkipSummary), so these surface there without new machinery.
832
+ for (const lane of [...args.run.resolvedPlan.autonomyGated, ...describeGatedLanes(args.autonomyGatedDirectLanes)]) {
833
+ warn(`[improve] ${lane.lane} skipped — it ${lane.reason}. Set \`${lane.configKey}: true\` to enable it.`);
834
+ appendEvent({
835
+ eventType: "improve_skipped",
836
+ ref: undefined,
837
+ metadata: {
838
+ strategy: selectedStrategy.name,
839
+ reason: "autonomy_gated",
840
+ lane: lane.lane,
841
+ configKey: lane.configKey,
842
+ },
843
+ }, eventsCtx);
844
+ }
845
+ // Single prep->loop->post-loop pass, run under the invocation's lock.
846
+ // Accumulators are direct assignments from the single pass's results.
847
+ let preparation;
848
+ let memoryRefsForInference = new Set();
849
+ let consolidation;
850
+ let memoryInference;
851
+ let graphExtraction;
852
+ let cycleMetrics;
853
+ // Summed counters/durations.
854
+ let reflectsWithErrorContext = 0;
855
+ let memoryInferenceDurationMs = 0;
856
+ let graphExtractionDurationMs = 0;
857
+ let orphansPurged;
858
+ let proposalsExpired;
859
+ // Concatenated arrays.
860
+ const allWarnings = [];
861
+ let deadUrls;
862
+ const finalActions = [];
863
+ {
864
+ const runPreparation = () => runImprovePreparationStageImpl({
865
+ scope,
866
+ options,
867
+ plannedRefs,
868
+ memoryCleanupPlan,
869
+ primaryStashDir,
870
+ memorySummary,
871
+ reindexFn,
872
+ startMs,
873
+ budgetMs,
874
+ eventsCtx,
875
+ initialCleanupWarnings: preEnsureCleanupWarnings,
876
+ improveProfile,
877
+ resolvedPlan,
878
+ strategyName: selectedStrategy.name,
879
+ budgetSignal: budgetAbortController.signal,
880
+ });
881
+ preparation = await runPreparation();
882
+ const rejectedProposalsByRef = preloadRejectedProposals();
883
+ const runLoop = () => {
884
+ const postLockLoopRefs = refilterProactiveLoopRefs(preparation.loopRefs, improveProfile);
885
+ return runImproveLoopStageImpl({
886
+ ctx,
887
+ primaryStashDir,
554
888
  scope,
555
889
  options,
556
- plannedRefs,
557
- memoryCleanupPlan,
558
- primaryStashDir,
559
- memorySummary,
560
- reindexFn,
890
+ reflectFn,
891
+ distillFn,
892
+ loopRefs: postLockLoopRefs,
893
+ actions: preparation.actions,
894
+ signalBearingSet: preparation.signalBearingSet,
895
+ distillCooledRefs: preparation.distillCooledRefs,
896
+ distillOnlyRefs: preparation.distillOnlyRefs,
897
+ recentErrors: preparation.recentErrors,
898
+ rejectedProposalsByRef,
899
+ utilityMap: preparation.utilityMap,
561
900
  startMs,
562
901
  budgetMs,
563
- eventsCtx,
564
- initialCleanupWarnings: preEnsureCleanupWarnings,
565
902
  improveProfile,
566
- budgetSignal: budgetAbortController.signal,
567
- }));
568
- prepGateCount += preparation.gateAutoAcceptedCount;
569
- prepGateFailedCount += preparation.gateAutoAcceptFailedCount;
570
- // D6: pre-load all proposal_rejected events from the last 30 days once,
571
- // so the per-asset loop can use a Map lookup instead of N DB round trips.
572
- const REJECTED_PROPOSAL_WINDOW_MS = daysToMs(30);
573
- const rejectedProposalSince = new Date(Date.now() - REJECTED_PROPOSAL_WINDOW_MS).toISOString();
574
- const allRejectedProposalEvents = readEvents({ type: "proposal_rejected", since: rejectedProposalSince }).events;
575
- const rejectedProposalsByRef = new Map();
576
- for (const e of allRejectedProposalEvents) {
577
- if (e.ref && (!rejectedProposalsByRef.has(e.ref) || e.ts > (rejectedProposalsByRef.get(e.ref)?.ts ?? ""))) {
578
- rejectedProposalsByRef.set(e.ref, e);
579
- }
580
- }
581
- // #607: acquire reflect-distill.lock for the loop stage (reflect + distill
582
- // both write proposals to state.db). Released immediately after.
583
- const reflectDistillLPath = processLockPath(lockBaseDir, "reflectDistill");
584
- const loopResult = await withOptionalProcessLock({
585
- lockPath: reflectDistillLPath,
586
- staleAfterMs: PROCESS_LOCK_DEFS.reflectDistill.staleAfterMs,
587
- skipIfLocked: options.skipIfLocked,
588
- label: "reflect-distill",
589
- }, () => {
590
- // Post-lock cooldown re-filter for proactive refs (#SELECT-TIME-LEAK).
591
- // Planning built `lastReflectProposalTs` BEFORE acquiring this lock, so a
592
- // concurrent run's `reflect_invoked` writes are invisible to it. Now that
593
- // we hold the lock, re-read fresh timestamp maps for the proactive subset
594
- // and drop any ref whose cooldown has been consumed by the concurrent run.
595
- const proactiveLoopRefs = preparation.loopRefs.filter((r) => r.eligibilitySource === "proactive");
596
- let postLockLoopRefs = preparation.loopRefs;
597
- if (proactiveLoopRefs.length > 0) {
598
- const proactiveRefStrs = proactiveLoopRefs.map((r) => r.ref);
599
- const freshReflectTs = buildLatestProposalTsMap(proactiveRefStrs, "reflect");
600
- const freshDistillTs = buildLatestProposalTsMap(proactiveRefStrs, "distill");
601
- const pmDueDays = improveProfile.processes?.proactiveMaintenance?.dueDays ?? DEFAULT_DUE_DAYS;
602
- const stillDue = new Set(filterProactiveDue(proactiveLoopRefs, freshReflectTs, freshDistillTs, pmDueDays, Date.now()).map((r) => r.ref));
603
- const dropped = proactiveLoopRefs.filter((r) => !stillDue.has(r.ref));
604
- if (dropped.length > 0) {
605
- info(`[improve] post-lock cooldown re-filter: dropped ${dropped.length} proactive ref(s) claimed by concurrent run (${dropped.map((r) => r.ref).join(", ")})`);
606
- postLockLoopRefs = preparation.loopRefs.filter((r) => r.eligibilitySource !== "proactive" || stillDue.has(r.ref));
607
- }
608
- }
609
- return runImproveLoopStageImpl({
610
- scope,
611
- options,
612
- primaryStashDir,
613
- reflectFn,
614
- distillFn,
615
- loopRefs: postLockLoopRefs,
616
- actions: preparation.actions,
617
- signalBearingSet: preparation.signalBearingSet,
618
- distillCooledRefs: preparation.distillCooledRefs,
619
- distillOnlyRefs: preparation.distillOnlyRefs,
620
- recentErrors: preparation.recentErrors,
621
- rejectedProposalsByRef,
622
- utilityMap: preparation.utilityMap,
623
- startMs,
624
- budgetMs,
625
- eventsCtx,
626
- improveProfile,
627
- budgetSignal: budgetAbortController.signal,
628
- });
903
+ resolvedPlan,
629
904
  });
630
- const loopGateCountThisCycle = loopResult.gateAutoAcceptedCount;
631
- reflectsWithErrorContext += loopResult.reflectsWithErrorContext;
632
- loopGateCount += loopResult.gateAutoAcceptedCount;
633
- loopGateFailedCount += loopResult.gateAutoAcceptFailedCount;
634
- memoryRefsForInference = loopResult.memoryRefsForInference;
635
- // #551: consolidation now runs in the preparation stage (before extract);
636
- // its result and run-flag are read from `preparation`, not the post-loop.
637
- consolidation = preparation.consolidation;
638
- // #607: acquire consolidate.lock for the post-loop stage (memoryInference +
639
- // graphExtraction both write index.db). Released immediately after.
640
- const consolidatePostLPath = processLockPath(lockBaseDir, "consolidate");
641
- const postLoopResult = await withOptionalProcessLock({
642
- lockPath: consolidatePostLPath,
643
- staleAfterMs: PROCESS_LOCK_DEFS.consolidate.staleAfterMs,
644
- skipIfLocked: options.skipIfLocked,
645
- label: "consolidate",
646
- }, () => runImprovePostLoopStageImpl({
647
- scope,
648
- options,
649
- primaryStashDir,
650
- actionableRefs: preparation.actionableRefs,
651
- appliedCleanup: preparation.appliedCleanup,
652
- cleanupWarnings: preparation.cleanupWarnings,
653
- memoryRefsForInference,
654
- reindexFn,
655
- eventsCtx,
656
- budgetSignal: budgetAbortController.signal,
657
- improveProfile,
658
- consolidationRan: preparation.consolidationRan,
659
- // R5: floor violations from this run's consolidate pass + the
660
- // auto-accepted volume so far (prep + loop gates) for churn detection.
661
- consolidationMergeFloorViolations: preparation.consolidation.mergeFloorViolations ?? 0,
662
- acceptedActions: preparation.gateAutoAcceptedCount + loopGateCountThisCycle,
663
- }));
664
- const postLoopGateCountThisCycle = postLoopResult.gateAutoAcceptedCount;
665
- // Last-wins point-in-time objects.
666
- memoryInference = postLoopResult.memoryInference;
667
- graphExtraction = postLoopResult.graphExtraction;
668
- recombination = postLoopResult.recombination;
669
- proceduralCompilation = postLoopResult.proceduralCompilation;
670
- // Keep the last QUALIFYING cycle's snapshot — a later non-qualifying
671
- // cycle in a maxCycles>1 run must not clobber it with undefined.
672
- if (postLoopResult.cycleMetrics)
673
- cycleMetrics = postLoopResult.cycleMetrics;
674
- // Summed counters/durations.
675
- postLoopGateCount += postLoopResult.gateAutoAcceptedCount;
676
- postLoopGateFailedCount += postLoopResult.gateAutoAcceptFailedCount;
677
- memoryInferenceDurationMs += postLoopResult.memoryInferenceDurationMs;
678
- graphExtractionDurationMs += postLoopResult.graphExtractionDurationMs;
679
- if (postLoopResult.orphansPurged !== undefined) {
680
- orphansPurged = (orphansPurged ?? 0) + postLoopResult.orphansPurged;
681
- }
682
- if (postLoopResult.proposalsExpired !== undefined) {
683
- proposalsExpired = (proposalsExpired ?? 0) + postLoopResult.proposalsExpired;
684
- }
685
- // Concatenated arrays.
686
- allWarnings.push(...postLoopResult.allWarnings);
687
- if (postLoopResult.deadUrls !== undefined) {
688
- deadUrls = [...(deadUrls ?? []), ...postLoopResult.deadUrls];
689
- }
690
- const maintenanceActions = postLoopResult.maintenanceActions;
691
- if (maintenanceActions && maintenanceActions.length > 0) {
692
- finalActions.push(...preparation.actions, ...maintenanceActions);
693
- }
694
- else {
695
- finalActions.push(...preparation.actions);
696
- }
697
- cyclesRun++;
698
- // #616 fixed-point stop: a cycle that produced ZERO gate-accepted proposals
699
- // (summed across prep + loop + post-loop) would feed cycle N+1 an identical
700
- // ref set, so end the loop here rather than spin a pointless next cycle.
701
- const gateAcceptedThisCycle = preparation.gateAutoAcceptedCount + loopGateCountThisCycle + postLoopGateCountThisCycle;
702
- if (gateAcceptedThisCycle === 0)
703
- break;
704
- }
705
- // C1 (13-bus-factor): fold the per-ref `distill-skipped` rows (~13k/run,
706
- // ~91% of result_json bytes) into a bounded aggregate BEFORE persistence.
707
- // The metric total + per-reason breakdown are preserved on `distillSkipped`;
708
- // the unbounded row list never reaches result_json. Reflect skip counters
709
- // below still read `finalActions` (reflect skips are not folded).
710
- const { actions: persistedActions, aggregate: distillSkippedAggregate } = foldDistillSkipped(finalActions);
711
- const result = {
712
- schemaVersion: 1,
713
- ok: true,
714
- scope,
715
- dryRun: false,
716
- ...(guidance ? { guidance } : {}),
717
- memorySummary,
718
- ...(memoryCleanupPlan
719
- ? {
720
- memoryCleanup: {
721
- ...shapeMemoryCleanup(memoryCleanupPlan),
722
- ...(preparation.appliedCleanup
723
- ? {
724
- archived: preparation.appliedCleanup.archived,
725
- ...(preparation.appliedCleanup.transitionLogPath
726
- ? { transitionLogPath: preparation.appliedCleanup.transitionLogPath }
727
- : {}),
728
- ...(preparation.appliedCleanup.transitionLogEntries !== undefined
729
- ? { transitionLogEntries: preparation.appliedCleanup.transitionLogEntries }
730
- : {}),
731
- ...(allWarnings.length > 0 ? { warnings: allWarnings } : {}),
732
- }
733
- : preparation.cleanupWarnings.length > 0
734
- ? { warnings: preparation.cleanupWarnings }
735
- : {}),
736
- },
737
- }
738
- : {}),
739
- plannedRefs: preparation.actionableRefs,
740
- ...(profileFilteredRefs.length > 0 ? { profileFilteredRefs } : {}),
741
- actions: persistedActions,
742
- ...(distillSkippedAggregate ? { distillSkipped: distillSkippedAggregate } : {}),
743
- ...(preparation.validationFailures.length > 0 ? { validationFailures: preparation.validationFailures } : {}),
744
- ...(preparation.schemaRepairs.length > 0 ? { schemaRepairs: preparation.schemaRepairs } : {}),
745
- ...(consolidation.processed > 0 || consolidation.warnings.length > 0 ? { consolidation } : {}),
746
- ...(preparation.lintSummary !== undefined ? { lintSummary: preparation.lintSummary } : {}),
747
- ...(preparation.memoryIndexHealth !== undefined ? { memoryIndexHealth: preparation.memoryIndexHealth } : {}),
748
- ...(preparation.coverageGaps.length > 0 ? { coverageGaps: preparation.coverageGaps } : {}),
749
- ...(preparation.extract && preparation.extract.length > 0 ? { extract: preparation.extract } : {}),
750
- ...(primaryStashDir !== undefined ? { evalCasesWritten: countEvalCases(primaryStashDir) } : {}),
751
- ...(deadUrls !== undefined && deadUrls.length > 0 ? { deadUrls } : {}),
752
- ...(reflectsWithErrorContext > 0 ? { reflectsWithErrorContext } : {}),
753
- ...(memoryInference ? { memoryInference } : {}),
754
- ...(graphExtraction ? { graphExtraction } : {}),
755
- // Per-phase wall-clock durations. Surfaced at the top level of the
756
- // envelope (not nested) because `health.ts`'s `wallTime.byPhase`
757
- // aggregator and the existing `memoryInference.durationMs` /
758
- // `graphExtraction.durationMs` health buckets all read
759
- // `result.{memoryInferenceDurationMs,graphExtractionDurationMs}`
760
- // directly. Mirrors how `consolidation.durationMs` is surfaced inside
761
- // the consolidation sub-object (different convention because the
762
- // consolidation result type already owns that field). Phases that did
763
- // not run (zero duration) are omitted so the aggregator's
764
- // "phase actually ran" filter (`> 0`) excludes them from the median/p95
765
- // sample. Plumbed in d1273d0's follow-up — see
766
- // `/tmp/akm-health-investigations/metrics-taxonomy-review.md` §1k / §3.
767
- ...(memoryInferenceDurationMs > 0 ? { memoryInferenceDurationMs } : {}),
768
- ...(graphExtractionDurationMs > 0 ? { graphExtractionDurationMs } : {}),
769
- ...(recombination ? { recombination } : {}),
770
- ...(proceduralCompilation ? { proceduralCompilation } : {}),
771
- ...(cycleMetrics ? { cycleMetrics } : {}),
772
- ...(orphansPurged !== undefined ? { orphansPurged } : {}),
773
- ...(proposalsExpired !== undefined && proposalsExpired > 0 ? { proposalsExpired } : {}),
774
- reflectCooldownActions: finalActions.filter((a) => a.mode === "reflect-cooldown").length,
775
- reflectSkippedActions: finalActions.filter((a) => a.mode === "reflect-skipped").length,
776
- reflectGuardRejectedActions: finalActions.filter((a) => a.mode === "reflect-guard-rejected").length,
777
- ...(() => {
778
- const t = prepGateCount + loopGateCount + postLoopGateCount;
779
- return t > 0 ? { gateAutoAcceptedCount: t } : {};
780
- })(),
781
- ...(() => {
782
- const f = prepGateFailedCount + loopGateFailedCount + postLoopGateFailedCount;
783
- return f > 0 ? { gateAutoAcceptFailedCount: f } : {};
784
- })(),
785
- ...(triageDrain
786
- ? {
787
- triage: {
788
- promoted: triageDrain.promoted.length,
789
- rejected: triageDrain.rejected.length,
790
- deferred: triageDrain.deferred.length,
791
- skippedByCap: triageDrain.skippedByCap.length,
792
- },
793
- }
794
- : {}),
795
- ...(preparation.proactiveMaintenance ? { proactiveMaintenance: preparation.proactiveMaintenance } : {}),
796
- // #616 — report cycles run only when >1 so the default single-pass
797
- // serialized envelope stays byte-identical to pre-#616 (AC1).
798
- ...(cyclesRun > 1 ? { cyclesRun } : {}),
799
- ...(options.runId !== undefined ? { runId: options.runId } : {}),
800
905
  };
801
- if (!result.dryRun)
802
- emitImproveCompletedEvent(result, {
803
- memoryInferenceDurationMs,
804
- graphExtractionDurationMs,
805
- totalDurationMs: Date.now() - startMs,
806
- warningCount: allWarnings.length,
807
- orphansPurged: orphansPurged ?? 0,
808
- }, eventsCtx);
809
- // End-of-run BATCH auto-sync — the converged commit. Recognition is
810
- // decoupled from the per-write path (see write-source.ts case-3): the primary
811
- // stash writes as a filesystem source during the run, then is committed via
812
- // the same `saveGitStash` that `akm sync` calls. The gating (git-backed
813
- // primary stash, sync not disabled) and the NON-FATAL guarantee now live in
814
- // `commitStashBatch` (#662); the inter-cycle and catch-path calls reuse it.
815
- // dry-run already returned above, so this always runs on a completed live
816
- // run. `result.sync` reflects this final commit (for a one-cycle run it is
817
- // the only commit; for a multi-cycle run the earlier cycles were banked by
818
- // the inter-cycle calls and this records the last batch). `result` carries
819
- // the full `{accepted}`/`{refs}`/`{triage_*}` token data for the message.
820
- result.sync = commitStashBatch(result);
821
- return result;
822
- }
823
- catch (err) {
824
- // D3: emit improve_failed on unexpected crash so dashboards can detect failures.
825
- appendEvent({
826
- eventType: "improve_failed",
827
- ref: scope.mode === "ref" ? scope.value : `improve:${scope.mode}:${scope.value ?? "all"}`,
828
- metadata: {
829
- error: errMessage(err),
830
- durationMs: Date.now() - startMs,
831
- },
832
- }, eventsCtx);
833
- // #662 crash/abort safety net: commit whatever this run already wrote to the
834
- // primary stash BEFORE rethrowing, so an interrupted run (mid-cycle crash or
835
- // a cooperative budget abort that surfaces as a throw) does not leave its
836
- // writes uncommitted until a later clean run sweeps them up. Best-effort —
837
- // `commitStashBatch` swallows its own errors and no-ops a clean tree, so this
838
- // never masks or supersedes the original failure being rethrown below.
839
- commitStashBatch({ scope, plannedRefs, runId: options.runId });
840
- throw err;
841
- }
842
- finally {
843
- // #576: clear the per-run LLM usage sink BEFORE closing `eventsDb` below, so
844
- // no late sink invocation can write through a closed handle.
845
- disposeLlmUsageSink();
846
- // O-1 (#364): Clear the budget abort timer so it does not keep the event
847
- // loop alive after the run completes.
848
- clearBudgetTimer();
849
- // #607: release any per-process locks still held (backstop for error paths;
850
- // the normal path already released each lock after its stage completed).
851
- releaseAllProcessLocks();
852
- // Drop ONLY our own process.exit backstop so it does not fire later (or
853
- // accumulate across repeated in-process calls). Must NOT use
854
- // removeAllListeners("exit") here: in the in-process model (tests and
855
- // programmatic callers import cli.ts) that would silently destroy exit
856
- // handlers owned by the host or other commands.
857
- if (exitBackstop) {
858
- process.removeListener("exit", exitBackstop);
859
- exitBackstop = undefined;
906
+ const loopResult = await runLoop();
907
+ reflectsWithErrorContext += loopResult.reflectsWithErrorContext;
908
+ memoryRefsForInference = loopResult.memoryRefsForInference;
909
+ // #551: consolidation now runs in the preparation stage (before extract);
910
+ // its result and run-flag are read from `preparation`, not the post-loop.
911
+ consolidation = preparation.consolidation;
912
+ const postLoopResult = await runPostLoopStageOrSkip({
913
+ budgetAbortController,
914
+ scope,
915
+ options,
916
+ primaryStashDir,
917
+ preparation,
918
+ memoryRefsForInference,
919
+ reindexFn,
920
+ eventsCtx,
921
+ improveProfile,
922
+ resolvedPlan,
923
+ runImprovePostLoopStageImpl,
924
+ });
925
+ // Result objects (single pass — no cycle accumulation).
926
+ memoryInference = postLoopResult.memoryInference;
927
+ graphExtraction = postLoopResult.graphExtraction;
928
+ if (postLoopResult.cycleMetrics)
929
+ cycleMetrics = postLoopResult.cycleMetrics;
930
+ // Summed counters/durations.
931
+ memoryInferenceDurationMs += postLoopResult.memoryInferenceDurationMs;
932
+ graphExtractionDurationMs += postLoopResult.graphExtractionDurationMs;
933
+ if (postLoopResult.orphansPurged !== undefined) {
934
+ orphansPurged = (orphansPurged ?? 0) + postLoopResult.orphansPurged;
860
935
  }
861
- // I1: close the long-lived state.db connection opened at the top of the run.
862
- try {
863
- eventsDb?.close();
936
+ if (postLoopResult.proposalsExpired !== undefined) {
937
+ proposalsExpired = (proposalsExpired ?? 0) + postLoopResult.proposalsExpired;
864
938
  }
865
- catch {
866
- // ignore — DB may already be closed
939
+ // Concatenated arrays.
940
+ allWarnings.push(...postLoopResult.allWarnings);
941
+ if (postLoopResult.deadUrls !== undefined) {
942
+ deadUrls = [...(deadUrls ?? []), ...postLoopResult.deadUrls];
943
+ }
944
+ const maintenanceActions = postLoopResult.maintenanceActions;
945
+ if (maintenanceActions && maintenanceActions.length > 0) {
946
+ finalActions.push(...preparation.actions, ...maintenanceActions);
947
+ }
948
+ else {
949
+ finalActions.push(...preparation.actions);
867
950
  }
868
951
  }
952
+ return {
953
+ preparation,
954
+ memoryRefsForInference,
955
+ consolidation,
956
+ memoryInference,
957
+ graphExtraction,
958
+ cycleMetrics,
959
+ reflectsWithErrorContext,
960
+ memoryInferenceDurationMs,
961
+ graphExtractionDurationMs,
962
+ orphansPurged,
963
+ proposalsExpired,
964
+ allWarnings,
965
+ deadUrls,
966
+ finalActions,
967
+ };
968
+ }
969
+ /**
970
+ * Run-teardown, result half: assemble the AkmImproveResult envelope and emit
971
+ * the improve_completed event (same adjacency as the old inline code — no
972
+ * side effects run between assembly and emit).
973
+ */
974
+ function finalizeImproveResult(args) {
975
+ const { guidance, memorySummary, memoryCleanupPlan, strategyFilteredRefs, triageDrain, eventsCtx } = args;
976
+ const { selectedStrategy, scope, options, primaryStashDir, startMs } = args.run;
977
+ const { preparation, consolidation, memoryInference, graphExtraction, cycleMetrics, reflectsWithErrorContext, memoryInferenceDurationMs, graphExtractionDurationMs, orphansPurged, proposalsExpired, allWarnings, deadUrls, finalActions, } = args.seq;
978
+ // C1 (13-bus-factor): fold the per-ref `distill-skipped` rows (~13k/run,
979
+ // ~91% of result_json bytes) into a bounded aggregate BEFORE persistence.
980
+ // The metric total + per-reason breakdown are preserved on `distillSkipped`;
981
+ // the unbounded row list never reaches result_json. Reflect skip counters
982
+ // below still read `finalActions` (reflect skips are not folded).
983
+ const { actions: persistedActions, aggregate: distillSkippedAggregate } = foldDistillSkipped(finalActions);
984
+ const result = {
985
+ schemaVersion: 2,
986
+ ok: true,
987
+ strategy: selectedStrategy.name,
988
+ scope,
989
+ dryRun: false,
990
+ ...(guidance ? { guidance } : {}),
991
+ memorySummary,
992
+ ...(memoryCleanupPlan
993
+ ? {
994
+ memoryCleanup: {
995
+ ...shapeMemoryCleanup(memoryCleanupPlan),
996
+ ...(preparation.appliedCleanup
997
+ ? {
998
+ archived: preparation.appliedCleanup.archived,
999
+ ...(preparation.appliedCleanup.transitionLogPath
1000
+ ? { transitionLogPath: preparation.appliedCleanup.transitionLogPath }
1001
+ : {}),
1002
+ ...(preparation.appliedCleanup.transitionLogEntries !== undefined
1003
+ ? { transitionLogEntries: preparation.appliedCleanup.transitionLogEntries }
1004
+ : {}),
1005
+ ...(allWarnings.length > 0 ? { warnings: allWarnings } : {}),
1006
+ }
1007
+ : preparation.cleanupWarnings.length > 0
1008
+ ? { warnings: preparation.cleanupWarnings }
1009
+ : {}),
1010
+ },
1011
+ }
1012
+ : {}),
1013
+ plannedRefs: preparation.actionableRefs,
1014
+ ...(strategyFilteredRefs.length > 0 ? { strategyFilteredRefs } : {}),
1015
+ actions: persistedActions,
1016
+ ...(distillSkippedAggregate ? { distillSkipped: distillSkippedAggregate } : {}),
1017
+ ...(preparation.validationFailures.length > 0 ? { validationFailures: preparation.validationFailures } : {}),
1018
+ ...(preparation.schemaRepairs.length > 0 ? { schemaRepairs: preparation.schemaRepairs } : {}),
1019
+ ...(consolidation.processed > 0 || consolidation.warnings.length > 0 ? { consolidation } : {}),
1020
+ ...(preparation.lintSummary !== undefined ? { lintSummary: preparation.lintSummary } : {}),
1021
+ ...(preparation.memoryIndexHealth !== undefined ? { memoryIndexHealth: preparation.memoryIndexHealth } : {}),
1022
+ ...(preparation.coverageGaps.length > 0 ? { coverageGaps: preparation.coverageGaps } : {}),
1023
+ ...(preparation.extract && preparation.extract.length > 0 ? { extract: preparation.extract } : {}),
1024
+ ...(primaryStashDir !== undefined ? { evalCasesWritten: countEvalCases(primaryStashDir) } : {}),
1025
+ ...(deadUrls !== undefined && deadUrls.length > 0 ? { deadUrls } : {}),
1026
+ ...(reflectsWithErrorContext > 0 ? { reflectsWithErrorContext } : {}),
1027
+ ...(memoryInference ? { memoryInference } : {}),
1028
+ ...(graphExtraction ? { graphExtraction } : {}),
1029
+ // Per-phase wall-clock durations. Surfaced at the top level of the
1030
+ // envelope (not nested) because `health.ts`'s `wallTime.byPhase`
1031
+ // aggregator and the existing `memoryInference.durationMs` /
1032
+ // `graphExtraction.durationMs` health buckets all read
1033
+ // `result.{memoryInferenceDurationMs,graphExtractionDurationMs}`
1034
+ // directly. Mirrors how `consolidation.durationMs` is surfaced inside
1035
+ // the consolidation sub-object (different convention because the
1036
+ // consolidation result type already owns that field). Phases that did
1037
+ // not run (zero duration) are omitted so the aggregator's
1038
+ // "phase actually ran" filter (`> 0`) excludes them from the median/p95
1039
+ // sample. Plumbed in d1273d0's follow-up — see
1040
+ // `/tmp/akm-health-investigations/metrics-taxonomy-review.md` §1k / §3.
1041
+ ...(memoryInferenceDurationMs > 0 ? { memoryInferenceDurationMs } : {}),
1042
+ ...(graphExtractionDurationMs > 0 ? { graphExtractionDurationMs } : {}),
1043
+ ...(cycleMetrics ? { cycleMetrics } : {}),
1044
+ ...(orphansPurged !== undefined ? { orphansPurged } : {}),
1045
+ ...(proposalsExpired !== undefined && proposalsExpired > 0 ? { proposalsExpired } : {}),
1046
+ reflectCooldownActions: finalActions.filter((a) => a.mode === "reflect-cooldown").length,
1047
+ reflectSkippedActions: finalActions.filter((a) => a.mode === "reflect-skipped").length,
1048
+ reflectGuardRejectedActions: finalActions.filter((a) => a.mode === "reflect-guard-rejected").length,
1049
+ ...(triageDrain
1050
+ ? {
1051
+ triage: {
1052
+ promoted: triageDrain.promoted.length,
1053
+ rejected: triageDrain.rejected.length,
1054
+ deferred: triageDrain.deferred.length,
1055
+ skippedByCap: triageDrain.skippedByCap.length,
1056
+ },
1057
+ }
1058
+ : {}),
1059
+ ...(preparation.proactiveMaintenance ? { proactiveMaintenance: preparation.proactiveMaintenance } : {}),
1060
+ ...(options.runId !== undefined ? { runId: options.runId } : {}),
1061
+ };
1062
+ if (!result.dryRun)
1063
+ emitImproveCompletedEvent(result, {
1064
+ memoryInferenceDurationMs,
1065
+ graphExtractionDurationMs,
1066
+ totalDurationMs: Date.now() - startMs,
1067
+ warningCount: allWarnings.length,
1068
+ orphansPurged: orphansPurged ?? 0,
1069
+ }, eventsCtx);
1070
+ return result;
869
1071
  }
870
1072
  function emitImproveCompletedEvent(result, durations, eventsCtx) {
871
1073
  const actionCounts = {
@@ -942,6 +1144,7 @@ function emitImproveCompletedEvent(result, durations, eventsCtx) {
942
1144
  ? result.scope.value
943
1145
  : `improve:${result.scope.mode}:${result.scope.value ?? "all"}`,
944
1146
  metadata: {
1147
+ strategy: result.strategy,
945
1148
  plannedRefs: result.plannedRefs.length,
946
1149
  reflectActions: actionCounts.reflect,
947
1150
  distillActions: actionCounts.distill,