akm-cli 0.9.0-beta.9 → 0.9.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. package/CHANGELOG.md +592 -0
  2. package/README.md +12 -4
  3. package/dist/akm +38 -0
  4. package/dist/akm-migrate-storage +38 -0
  5. package/dist/assets/help/help-improve.md +9 -6
  6. package/dist/assets/hints/cli-hints-full.md +6 -5
  7. package/dist/assets/profiles/default.json +9 -4
  8. package/dist/assets/profiles/frequent.json +1 -1
  9. package/dist/assets/profiles/memory-focus.json +1 -1
  10. package/dist/assets/profiles/proactive-maintenance.json +25 -0
  11. package/dist/assets/profiles/quick.json +1 -1
  12. package/dist/assets/profiles/recombine-only.json +21 -0
  13. package/dist/assets/profiles/reflect-distill.json +30 -0
  14. package/dist/assets/profiles/synthesize.json +15 -0
  15. package/dist/assets/profiles/thorough.json +1 -1
  16. package/dist/assets/prompts/consolidate-system.md +23 -0
  17. package/dist/assets/prompts/contradiction-judge.md +33 -0
  18. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  19. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  20. package/dist/assets/prompts/extract-session.md +11 -3
  21. package/dist/assets/prompts/graph-extract-system.md +1 -0
  22. package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
  23. package/dist/assets/prompts/memory-infer-system.md +1 -0
  24. package/dist/assets/prompts/memory-infer-user.md +5 -0
  25. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  26. package/dist/assets/prompts/procedural-system.md +44 -0
  27. package/dist/assets/prompts/recombine-system.md +40 -0
  28. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  29. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
  39. package/dist/assets/templates/html/health.html +281 -111
  40. package/dist/assets/wiki/ingest-workflow-template.md +45 -16
  41. package/dist/assets/wiki/schema-template.md +4 -4
  42. package/dist/cli/clack.js +56 -0
  43. package/dist/cli/config-migrate.js +7 -1
  44. package/dist/cli/confirm.js +1 -1
  45. package/dist/cli/parse-args.js +46 -1
  46. package/dist/cli/shared.js +28 -0
  47. package/dist/cli.js +25 -21
  48. package/dist/commands/agent/agent-dispatch.js +3 -2
  49. package/dist/commands/agent/agent-support.js +0 -7
  50. package/dist/commands/agent/contribute-cli.js +26 -7
  51. package/dist/commands/config-cli.js +26 -13
  52. package/dist/commands/env/child-env.js +47 -0
  53. package/dist/commands/env/env-cli.js +220 -227
  54. package/dist/commands/env/env.js +14 -67
  55. package/dist/commands/env/secret-cli.js +140 -138
  56. package/dist/commands/feedback-cli.js +156 -155
  57. package/dist/commands/graph/graph-cli.js +5 -13
  58. package/dist/commands/graph/graph.js +3 -3
  59. package/dist/commands/health/advisories.js +151 -0
  60. package/dist/commands/health/checks.js +103 -16
  61. package/dist/commands/health/html-report.js +447 -81
  62. package/dist/commands/health/improve-metrics.js +771 -0
  63. package/dist/commands/health/llm-usage.js +65 -0
  64. package/dist/commands/health/md-report.js +103 -0
  65. package/dist/commands/health/metrics.js +278 -0
  66. package/dist/commands/health/stash-exposure.js +46 -0
  67. package/dist/commands/health/surfaces.js +216 -0
  68. package/dist/commands/health/task-runs.js +135 -0
  69. package/dist/commands/health/types.js +26 -0
  70. package/dist/commands/health/windows.js +195 -0
  71. package/dist/commands/health.js +91 -1091
  72. package/dist/commands/improve/anti-collapse.js +170 -0
  73. package/dist/commands/improve/calibration.js +161 -0
  74. package/dist/commands/improve/collapse-detector.js +421 -0
  75. package/dist/commands/improve/consolidate/chunking.js +141 -0
  76. package/dist/commands/improve/consolidate/eligibility.js +64 -0
  77. package/dist/commands/improve/consolidate/merge.js +145 -0
  78. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  79. package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
  80. package/dist/commands/improve/consolidate.js +1295 -1277
  81. package/dist/commands/improve/dedup.js +482 -0
  82. package/dist/commands/improve/distill/content-repair.js +202 -0
  83. package/dist/commands/improve/distill/promote-memory.js +229 -0
  84. package/dist/commands/improve/distill/quality-gate.js +236 -0
  85. package/dist/commands/improve/distill-guards.js +127 -0
  86. package/dist/commands/improve/distill-promotion-policy.js +826 -167
  87. package/dist/commands/improve/distill.js +228 -605
  88. package/dist/commands/improve/eligibility.js +434 -0
  89. package/dist/commands/improve/encoding-salience.js +205 -0
  90. package/dist/commands/improve/extract-cli.js +179 -59
  91. package/dist/commands/improve/extract-prompt.js +54 -3
  92. package/dist/commands/improve/extract-watch.js +140 -0
  93. package/dist/commands/improve/extract.js +409 -43
  94. package/dist/commands/improve/feedback-valence.js +54 -0
  95. package/dist/commands/improve/hot-probation.js +45 -0
  96. package/dist/commands/improve/improve-auto-accept.js +157 -10
  97. package/dist/commands/improve/improve-cli.js +115 -73
  98. package/dist/commands/improve/improve-profiles.js +28 -8
  99. package/dist/commands/improve/improve-result-file.js +15 -25
  100. package/dist/commands/improve/improve-session.js +58 -0
  101. package/dist/commands/improve/improve.js +485 -2764
  102. package/dist/commands/improve/locks.js +154 -0
  103. package/dist/commands/improve/loop-stages.js +1100 -0
  104. package/dist/commands/improve/memory/memory-belief.js +14 -15
  105. package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
  106. package/dist/commands/improve/memory/memory-improve.js +27 -27
  107. package/dist/commands/improve/outcome-loop.js +270 -0
  108. package/dist/commands/improve/preparation.js +2002 -0
  109. package/dist/commands/improve/proactive-maintenance.js +37 -35
  110. package/dist/commands/improve/procedural.js +398 -0
  111. package/dist/commands/improve/recombine.js +818 -0
  112. package/dist/commands/improve/reflect-noise.js +0 -0
  113. package/dist/commands/improve/reflect.js +206 -45
  114. package/dist/commands/improve/salience.js +455 -0
  115. package/dist/commands/improve/schema-similarity-gate.js +168 -0
  116. package/dist/commands/improve/shared.js +51 -0
  117. package/dist/commands/improve/triage.js +93 -0
  118. package/dist/commands/lint/agent-linter.js +19 -24
  119. package/dist/commands/lint/base-linter.js +173 -60
  120. package/dist/commands/lint/command-linter.js +19 -24
  121. package/dist/commands/lint/env-key-rules.js +38 -1
  122. package/dist/commands/lint/fact-linter.js +39 -0
  123. package/dist/commands/lint/index.js +31 -13
  124. package/dist/commands/lint/memory-linter.js +1 -1
  125. package/dist/commands/lint/registry.js +7 -2
  126. package/dist/commands/lint/task-linter.js +3 -3
  127. package/dist/commands/lint/workflow-linter.js +26 -1
  128. package/dist/commands/observability-cli.js +4 -4
  129. package/dist/commands/proposal/drain-policies.js +13 -4
  130. package/dist/commands/proposal/drain.js +45 -51
  131. package/dist/commands/proposal/legacy-import.js +115 -0
  132. package/dist/commands/proposal/proposal-cli.js +24 -34
  133. package/dist/commands/proposal/proposal.js +2 -1
  134. package/dist/commands/proposal/propose.js +8 -3
  135. package/dist/commands/proposal/repository.js +829 -0
  136. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  137. package/dist/commands/proposal/validators/proposals.js +93 -895
  138. package/dist/commands/read/curate.js +410 -111
  139. package/dist/commands/read/knowledge.js +10 -3
  140. package/dist/commands/read/remember-cli.js +133 -138
  141. package/dist/commands/read/search-cli.js +15 -8
  142. package/dist/commands/read/search.js +22 -11
  143. package/dist/commands/read/show.js +106 -14
  144. package/dist/commands/registry-cli.js +76 -87
  145. package/dist/commands/remember.js +11 -12
  146. package/dist/commands/sources/add-cli.js +91 -95
  147. package/dist/commands/sources/history.js +1 -1
  148. package/dist/commands/sources/init.js +66 -18
  149. package/dist/commands/sources/installed-stashes.js +11 -3
  150. package/dist/commands/sources/schema-repair.js +44 -46
  151. package/dist/commands/sources/self-update.js +2 -2
  152. package/dist/commands/sources/source-add.js +7 -3
  153. package/dist/commands/sources/sources-cli.js +3 -3
  154. package/dist/commands/sources/stash-cli.js +19 -39
  155. package/dist/commands/sources/stash-skeleton.js +57 -8
  156. package/dist/commands/tasks/default-tasks.js +15 -2
  157. package/dist/commands/tasks/tasks-cli.js +20 -29
  158. package/dist/commands/tasks/tasks.js +39 -11
  159. package/dist/commands/wiki-cli.js +23 -38
  160. package/dist/commands/workflow-cli.js +15 -1
  161. package/dist/core/asset/asset-registry.js +3 -1
  162. package/dist/core/asset/asset-spec.js +21 -4
  163. package/dist/core/asset/frontmatter.js +188 -167
  164. package/dist/core/asset/markdown.js +8 -0
  165. package/dist/core/authoring-rules.js +92 -0
  166. package/dist/core/common.js +4 -23
  167. package/dist/core/concurrent.js +10 -1
  168. package/dist/core/config/config-io.js +10 -1
  169. package/dist/core/config/config-migration.js +18 -40
  170. package/dist/core/config/config-schema.js +382 -62
  171. package/dist/core/config/config-types.js +3 -3
  172. package/dist/core/config/config.js +67 -22
  173. package/dist/core/deep-merge.js +38 -0
  174. package/dist/core/errors.js +1 -0
  175. package/dist/core/eval/rank-metrics.js +113 -0
  176. package/dist/core/events.js +4 -7
  177. package/dist/core/improve-types.js +47 -8
  178. package/dist/core/logs-db.js +14 -75
  179. package/dist/core/parse.js +36 -16
  180. package/dist/core/paths.js +18 -18
  181. package/dist/core/standards/resolve-standards-context.js +87 -0
  182. package/dist/core/standards/resolve-stash-standards.js +99 -0
  183. package/dist/core/standards/resolve-type-conventions.js +66 -0
  184. package/dist/core/state/migrations.js +770 -0
  185. package/dist/core/state-db.js +132 -1126
  186. package/dist/core/structured.js +69 -0
  187. package/dist/core/time.js +53 -0
  188. package/dist/core/warn.js +21 -0
  189. package/dist/core/write-source.js +37 -0
  190. package/dist/indexer/db/db.js +259 -769
  191. package/dist/indexer/db/entry-mapper.js +41 -0
  192. package/dist/indexer/db/graph-db.js +129 -86
  193. package/dist/indexer/db/llm-cache.js +2 -2
  194. package/dist/indexer/db/schema.js +516 -0
  195. package/dist/indexer/ensure-index.js +36 -92
  196. package/dist/indexer/feedback/utility-policy.js +75 -0
  197. package/dist/indexer/graph/graph-boost.js +51 -41
  198. package/dist/indexer/graph/graph-extraction.js +207 -4
  199. package/dist/indexer/index-writer-lock.js +18 -11
  200. package/dist/indexer/index-written-assets.js +105 -0
  201. package/dist/indexer/indexer.js +182 -204
  202. package/dist/indexer/passes/dir-staleness.js +114 -0
  203. package/dist/indexer/passes/memory-inference.js +13 -5
  204. package/dist/indexer/passes/metadata.js +20 -0
  205. package/dist/indexer/read-preflight.js +23 -0
  206. package/dist/indexer/search/db-search.js +89 -13
  207. package/dist/indexer/search/fts-query.js +51 -0
  208. package/dist/indexer/search/ranking-contributors.js +95 -9
  209. package/dist/indexer/search/ranking.js +79 -3
  210. package/dist/indexer/search/search-fields.js +6 -0
  211. package/dist/indexer/search/search-source.js +32 -21
  212. package/dist/indexer/search/semantic-status.js +4 -0
  213. package/dist/indexer/walk/matchers.js +9 -0
  214. package/dist/indexer/walk/walker.js +21 -13
  215. package/dist/integrations/agent/builders.js +39 -13
  216. package/dist/integrations/agent/config.js +20 -59
  217. package/dist/integrations/agent/detect.js +9 -0
  218. package/dist/integrations/agent/index.js +3 -19
  219. package/dist/integrations/agent/model-aliases.js +7 -2
  220. package/dist/integrations/agent/profiles.js +7 -1
  221. package/dist/integrations/agent/prompts.js +75 -9
  222. package/dist/integrations/agent/runner-dispatch.js +59 -0
  223. package/dist/integrations/agent/runner.js +13 -9
  224. package/dist/integrations/agent/spawn.js +69 -67
  225. package/dist/integrations/harnesses/claude/agent-builder.js +1 -1
  226. package/dist/integrations/harnesses/claude/index.js +2 -0
  227. package/dist/integrations/harnesses/claude/session-log.js +10 -0
  228. package/dist/integrations/harnesses/index.js +2 -3
  229. package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
  230. package/dist/integrations/harnesses/opencode/index.js +2 -0
  231. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  232. package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
  233. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +98 -17
  234. package/dist/integrations/harnesses/types.js +1 -0
  235. package/dist/integrations/session-logs/index.js +16 -0
  236. package/dist/llm/call-ai.js +2 -2
  237. package/dist/llm/client.js +34 -11
  238. package/dist/llm/embedder.js +67 -4
  239. package/dist/llm/embedders/cache.js +3 -1
  240. package/dist/llm/embedders/deterministic.js +66 -0
  241. package/dist/llm/embedders/local.js +73 -3
  242. package/dist/llm/feature-gate.js +16 -15
  243. package/dist/llm/graph-extract.js +67 -44
  244. package/dist/llm/memory-infer-impl.js +138 -0
  245. package/dist/llm/memory-infer.js +1 -127
  246. package/dist/llm/metadata-enhance.js +44 -31
  247. package/dist/llm/structured-call.js +49 -0
  248. package/dist/migrate-storage-node.mjs +8 -0
  249. package/dist/output/context.js +5 -5
  250. package/dist/output/renderers.js +85 -14
  251. package/dist/output/shapes/curate.js +14 -2
  252. package/dist/output/shapes/helpers.js +0 -3
  253. package/dist/output/shapes/passthrough.js +2 -1
  254. package/dist/output/text/helpers.js +29 -1
  255. package/dist/output/text/workflow.js +1 -0
  256. package/dist/registry/providers/skills-sh.js +21 -147
  257. package/dist/registry/providers/static-index.js +15 -157
  258. package/dist/registry/resolve.js +27 -9
  259. package/dist/runtime.js +25 -1
  260. package/dist/scripts/migrate-storage.js +2661 -2369
  261. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +883 -596
  262. package/dist/setup/detect.js +9 -0
  263. package/dist/setup/legacy-config.js +106 -0
  264. package/dist/setup/prompt.js +57 -0
  265. package/dist/setup/providers.js +14 -0
  266. package/dist/setup/registry-stash-loader.js +12 -0
  267. package/dist/setup/semantic-assets.js +124 -0
  268. package/dist/setup/setup.js +52 -1614
  269. package/dist/setup/steps/connection.js +734 -0
  270. package/dist/setup/steps/output.js +31 -0
  271. package/dist/setup/steps/platforms.js +124 -0
  272. package/dist/setup/steps/semantic.js +27 -0
  273. package/dist/setup/steps/sources.js +222 -0
  274. package/dist/setup/steps/stashdir.js +42 -0
  275. package/dist/setup/steps/tasks.js +152 -0
  276. package/dist/sources/include.js +6 -2
  277. package/dist/sources/providers/filesystem.js +0 -1
  278. package/dist/sources/providers/git-install.js +210 -0
  279. package/dist/sources/providers/git-provider.js +234 -0
  280. package/dist/sources/providers/git-stash.js +248 -0
  281. package/dist/sources/providers/git.js +10 -661
  282. package/dist/sources/providers/npm.js +2 -6
  283. package/dist/sources/providers/provider-utils.js +13 -7
  284. package/dist/sources/providers/sync-from-ref.js +9 -1
  285. package/dist/sources/providers/website.js +9 -5
  286. package/dist/sources/website-ingest.js +187 -29
  287. package/dist/sources/wiki-fetchers/registry.js +53 -0
  288. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  289. package/dist/storage/database.js +45 -10
  290. package/dist/storage/managed-db.js +82 -0
  291. package/dist/storage/repositories/canaries-repository.js +107 -0
  292. package/dist/storage/repositories/consolidation-repository.js +38 -0
  293. package/dist/storage/repositories/embeddings-repository.js +72 -0
  294. package/dist/storage/repositories/events-repository.js +187 -0
  295. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  296. package/dist/storage/repositories/improve-runs-repository.js +146 -0
  297. package/dist/storage/repositories/index-db.js +14 -8
  298. package/dist/storage/repositories/proposals-repository.js +220 -0
  299. package/dist/storage/repositories/recombine-repository.js +213 -0
  300. package/dist/storage/repositories/registry-cache.js +93 -0
  301. package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
  302. package/dist/storage/repositories/task-history-repository.js +93 -0
  303. package/dist/storage/sqlite-pragmas.js +146 -0
  304. package/dist/tasks/backends/cron.js +1 -1
  305. package/dist/tasks/backends/index.js +9 -0
  306. package/dist/tasks/backends/launchd.js +1 -1
  307. package/dist/tasks/backends/schtasks.js +1 -1
  308. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  309. package/dist/tasks/runner.js +15 -13
  310. package/dist/text-import-hook.mjs +0 -0
  311. package/dist/wiki/wiki.js +52 -11
  312. package/dist/workflows/cli.js +1 -0
  313. package/dist/workflows/db.js +3 -4
  314. package/dist/workflows/runtime/runs.js +43 -118
  315. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  316. package/dist/workflows/validate-summary.js +2 -7
  317. package/docs/README.md +69 -18
  318. package/docs/data-and-telemetry.md +5 -4
  319. package/docs/migration/release-notes/0.7.0.md +1 -1
  320. package/docs/migration/release-notes/0.9.0.md +39 -0
  321. package/package.json +10 -10
  322. package/dist/assets/tasks/core/update-stashes.yml +0 -4
  323. package/dist/commands/db-cli.js +0 -23
  324. package/dist/indexer/db/db-backup.js +0 -376
  325. package/dist/indexer/passes/staleness-detect.js +0 -488
@@ -0,0 +1,54 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * Symmetric valence weighting for the improve eligibility sort (#614).
6
+ *
7
+ * BACKGROUND. The improve attention/eligibility ranking historically combined
8
+ * utility with a NEGATIVE-ONLY feedback term: `negative / (positive + negative)`.
9
+ * Under that formula a strong-positive asset contributes a feedback ratio of
10
+ * `0` — i.e. positive feedback never drives attention. Only complaints could
11
+ * lift an asset up the ranking, so a heavily-praised, heavily-used asset that
12
+ * deserves REINFORCEMENT (distill / promote the win) is treated identically to
13
+ * a never-rated one.
14
+ *
15
+ * FIX (gated, default-off). When symmetric valence is enabled we replace the
16
+ * negative-only ratio with a |valence| MAGNITUDE term so that BOTH strong
17
+ * positive and strong negative feedback drive attention. Utility remains the
18
+ * dominant ordering factor — valence is a secondary attention nudge with a
19
+ * small fixed weight, never a utility override.
20
+ *
21
+ * This module is intentionally pure and storage-free: it takes pre-aggregated
22
+ * positive/negative counts plus a utility lookup and returns a deterministic
23
+ * score and lane. All DB access stays in the caller.
24
+ */
25
+ /** Weight on utility in the combined eligibility score. Utility is dominant. */
26
+ export const UTILITY_WEIGHT = 0.7;
27
+ /** Weight on the feedback attention term in the combined eligibility score. */
28
+ export const FEEDBACK_WEIGHT = 0.3;
29
+ /**
30
+ * Minimum |valence| magnitude for an item to be ROUTED to the fix / reinforce
31
+ * lane. Below this the feedback is too weak/mixed to be a confident signal and
32
+ * the item carries no attention lane (`null`). Pure magnitude in [0, 1].
33
+ */
34
+ export const STRONG_VALENCE_THRESHOLD = 0.5;
35
+ /**
36
+ * Compute the symmetric-valence attention score for one asset's feedback.
37
+ *
38
+ * Deterministic: depends only on the integer counts. No clock, no randomness.
39
+ */
40
+ export function computeValenceScore(counts) {
41
+ const positive = Math.max(0, counts.positive);
42
+ const negative = Math.max(0, counts.negative);
43
+ const total = positive + negative;
44
+ if (total === 0) {
45
+ return { valence: 0, magnitude: 0, attention: 0, lane: null };
46
+ }
47
+ const valence = (positive - negative) / total;
48
+ const magnitude = Math.abs(valence);
49
+ let lane = null;
50
+ if (magnitude >= STRONG_VALENCE_THRESHOLD) {
51
+ lane = valence < 0 ? "fix" : "reinforce";
52
+ }
53
+ return { valence, magnitude, attention: magnitude, lane };
54
+ }
@@ -0,0 +1,45 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * WS-3b Step 0c — Hot-probation intake buffer (#604).
6
+ *
7
+ * New system-generated extractions enter `captureMode: hot-probation` and spend
8
+ * ONE consolidation cycle in probation before promotion to the main stash;
9
+ * dedup + quality second-pass runs against them. Stops noisy extractions from
10
+ * polluting the stash at the source. Reuses shared dedupHash + body_embeddings.
11
+ * Default OFF.
12
+ *
13
+ * @module hot-probation
14
+ */
15
+ /**
16
+ * captureMode value for system-generated extractions in probation.
17
+ * Automatic counterpart to the user-explicit `captureMode: hot`.
18
+ */
19
+ export const CAPTURE_MODE_HOT_PROBATION = "hot-probation";
20
+ /**
21
+ * Returns true when an asset is in hot-probation (system-generated, not yet
22
+ * graduated from the intake dedup+quality pass).
23
+ */
24
+ export function isHotProbation(captureModeValue) {
25
+ return captureModeValue === CAPTURE_MODE_HOT_PROBATION;
26
+ }
27
+ /**
28
+ * Returns true when an asset should be skipped by the consolidation LLM
29
+ * because it's still in hot-probation (hasn't completed the intake pass yet).
30
+ *
31
+ * Hot-probation assets are processed by the consolidation dedup pre-pass
32
+ * (runDeterministicDedup) but excluded from the LLM merge clustering, so
33
+ * noisy extractions can't pollute the LLM context.
34
+ */
35
+ export function shouldSkipHotProbationInLlm(frontmatterData) {
36
+ return isHotProbation(frontmatterData.captureMode);
37
+ }
38
+ /**
39
+ * Build frontmatter fields to inject when creating a hot-probation proposal.
40
+ * The proposal will carry `captureMode: hot-probation` so downstream logic
41
+ * knows to run the intake dedup pass before graduating it.
42
+ */
43
+ export function buildHotProbationFrontmatter() {
44
+ return { captureMode: CAPTURE_MODE_HOT_PROBATION };
45
+ }
@@ -2,9 +2,34 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { loadConfig } from "../../core/config/config.js";
5
+ import { UsageError } from "../../core/errors.js";
5
6
  import { appendEvent } from "../../core/events.js";
7
+ import { withStateDb } from "../../core/state-db.js";
6
8
  import { info, warn } from "../../core/warn.js";
7
- import { promoteProposal, recordGateDecision } from "../proposal/validators/proposals.js";
9
+ import { getPhaseThreshold } from "../../storage/repositories/improve-runs-repository.js";
10
+ import { archiveProposal, getProposal, promoteProposal, recordGateDecision } from "../proposal/repository.js";
11
+ async function sha256Hex(input) {
12
+ const data = new TextEncoder().encode(input);
13
+ const digest = await crypto.subtle.digest("SHA-256", data);
14
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
15
+ }
16
+ /**
17
+ * Derive a stable, low-cardinality reason bucket from an auto-accept promotion
18
+ * error. `promoteProposal` throws a `validateProposal` report formatted as
19
+ * `[kind] message` lines; we extract the first finding kind. Non-validation
20
+ * throws collapse to `promote-error`.
21
+ */
22
+ function classifyPromoteFailure(err) {
23
+ const message = err instanceof Error ? err.message : String(err);
24
+ const finding = /\[([a-z][a-z0-9-]*)\]/i.exec(message);
25
+ if (finding)
26
+ return `validation:${finding[1]}`;
27
+ if (/not pending/i.test(message))
28
+ return "not-pending";
29
+ if (/unknown asset type/i.test(message))
30
+ return "unknown-type";
31
+ return "promote-error";
32
+ }
8
33
  // ---------------------------------------------------------------------------
9
34
  // Gate implementation
10
35
  // ---------------------------------------------------------------------------
@@ -18,13 +43,32 @@ import { promoteProposal, recordGateDecision } from "../proposal/validators/prop
18
43
  * @param promoteFn Injectable override for `promoteProposal` (test seam).
19
44
  */
20
45
  export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProposal) {
21
- const result = { promoted: [], skipped: [], failed: [] };
46
+ const result = {
47
+ promoted: [],
48
+ skipped: [],
49
+ failed: [],
50
+ suppressed: [],
51
+ failedByReason: {},
52
+ failedBySource: {},
53
+ };
22
54
  // --- Guard: gate is disabled or context is incomplete ---
23
55
  if (cfg.dryRun || cfg.globalThreshold === undefined || !cfg.stashDir) {
24
56
  result.skipped = candidates.map((c) => c.proposalId);
25
57
  return result;
26
58
  }
27
- const effectiveThreshold = Math.max(cfg.globalThreshold, cfg.minimumThreshold ?? 0) / 100;
59
+ // WS-4: per-phase threshold from state.db overrides the global threshold.
60
+ // The per-phase value is populated by makeGateConfig when a stateDbPath is
61
+ // available; callers that don't pass it get the global threshold unchanged.
62
+ const resolvedThreshold = cfg.phaseThreshold ?? cfg.globalThreshold;
63
+ const effectiveThreshold = Math.max(resolvedThreshold, cfg.minimumThreshold ?? 0) / 100;
64
+ // WS-4: Exploration budget — promote at most N candidates regardless of
65
+ // confidence to prevent the gate converging to pure exploitation.
66
+ // Exploration candidates are chosen from the LOWEST-confidence eligible set
67
+ // (i.e. those that would be deferred) so the budget truly samples the low-
68
+ // confidence tail and is meaningfully distinct from normal auto-accept.
69
+ // Promoted exploration proposals are logged with eligibilitySource="exploration".
70
+ const explorationBudget = cfg.explorationBudgetCount ?? 0;
71
+ let explorationRemaining = explorationBudget;
28
72
  const resolvedConfig = typeof cfg.config === "function" ? cfg.config() : cfg.config;
29
73
  const gateLabel = `improve:${cfg.phase}`;
30
74
  // #577: stamp the gate's verdict onto each proposal so `akm proposal show`
@@ -40,7 +84,20 @@ export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProp
40
84
  };
41
85
  for (const candidate of candidates) {
42
86
  const { proposalId, confidence } = candidate;
43
- if (confidence === undefined || confidence < effectiveThreshold) {
87
+ let currentProposal;
88
+ try {
89
+ currentProposal = cfg.stashDir ? getProposal(cfg.stashDir, proposalId) : undefined;
90
+ }
91
+ catch {
92
+ currentProposal = undefined;
93
+ }
94
+ const currentContentHash = currentProposal ? await sha256Hex(currentProposal.payload.content) : undefined;
95
+ // Determine if this candidate is exploration-eligible: below-threshold
96
+ // (would normally be deferred) but with a valid confidence score and budget
97
+ // remaining. No-confidence candidates are never exploration-promoted.
98
+ const belowThreshold = confidence === undefined || confidence < effectiveThreshold;
99
+ const isExploration = belowThreshold && confidence !== undefined && explorationRemaining > 0;
100
+ if (belowThreshold && !isExploration) {
44
101
  stamp(proposalId, {
45
102
  outcome: "deferred",
46
103
  reason: confidence === undefined ? "no-confidence" : "below-threshold",
@@ -51,15 +108,32 @@ export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProp
51
108
  result.skipped.push(proposalId);
52
109
  continue;
53
110
  }
111
+ // Either above-threshold (normal auto-accept) or exploration-budget promoted.
112
+ if (isExploration)
113
+ explorationRemaining -= 1;
114
+ const promoteReason = isExploration ? "exploration-budget" : "above-threshold";
115
+ if (currentProposal?.gateDecision?.outcome === "auto-rejected" &&
116
+ currentProposal.gateDecision.contentHash !== undefined &&
117
+ currentProposal.gateDecision.contentHash === currentContentHash) {
118
+ result.suppressed.push(proposalId);
119
+ continue;
120
+ }
54
121
  try {
55
122
  const promotion = await promoteFn(cfg.stashDir, resolvedConfig, proposalId, {}, undefined);
56
123
  stamp(promotion.proposal.id, {
57
124
  outcome: "auto-accepted",
58
- reason: "above-threshold",
125
+ reason: promoteReason,
59
126
  confidence,
60
127
  thresholds: { autoAccept: effectiveThreshold },
128
+ ...(currentContentHash !== undefined ? { contentHash: currentContentHash } : {}),
61
129
  gate: gateLabel,
62
130
  });
131
+ // Resolve the eligibilitySource: exploration-promoted proposals get
132
+ // eligibilitySource="exploration" (WS-4); normal auto-accepts carry
133
+ // whatever the proposal was tagged with at selection time.
134
+ const resolvedEligibilitySource = isExploration
135
+ ? "exploration"
136
+ : promotion.proposal.eligibilitySource;
63
137
  appendEvent({
64
138
  eventType: "promoted",
65
139
  ref: promotion.ref,
@@ -75,17 +149,59 @@ export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProp
75
149
  // Attribution tagging: carry the eligibility lane from the proposal
76
150
  // record onto the auto-accept promoted event so the lane survives to
77
151
  // accept time even when promotion happens in a later run.
78
- ...(promotion.proposal.eligibilitySource !== undefined
79
- ? { eligibilitySource: promotion.proposal.eligibilitySource }
80
- : {}),
152
+ ...(resolvedEligibilitySource !== undefined ? { eligibilitySource: resolvedEligibilitySource } : {}),
153
+ // WS-4: mark exploration promotions so health/telemetry can
154
+ // distinguish them from calibration-signal promotions.
155
+ ...(isExploration ? { explorationBudget: true } : {}),
81
156
  },
82
157
  }, cfg.eventsCtx ?? {});
83
- info(`[improve] auto-accepted ${promotion.ref} (${cfg.phase}; confidence=${confidence.toFixed(2)} >= threshold=${effectiveThreshold.toFixed(2)})`);
158
+ if (isExploration) {
159
+ info(`[improve] exploration-accepted ${promotion.ref} (${cfg.phase}; confidence=${confidence.toFixed(2)}; budgetRemaining=${explorationRemaining})`);
160
+ }
161
+ else {
162
+ info(`[improve] auto-accepted ${promotion.ref} (${cfg.phase}; confidence=${confidence.toFixed(2)} >= threshold=${effectiveThreshold.toFixed(2)})`);
163
+ }
84
164
  result.promoted.push(proposalId);
85
165
  }
86
166
  catch (err) {
87
- warn(`[improve] ${cfg.phase} auto-accept failed for ${proposalId}: ${err instanceof Error ? err.message : String(err)}`);
167
+ const reason = classifyPromoteFailure(err);
168
+ warn(`[improve] ${cfg.phase} auto-accept failed for ${proposalId} (${reason}): ${err instanceof Error ? err.message : String(err)}`);
88
169
  result.failed.push(proposalId);
170
+ result.failedByReason[reason] = (result.failedByReason[reason] ?? 0) + 1;
171
+ // Record WHY on the proposal so `akm proposal show` explains the rejection
172
+ // and the leak is no longer blind. Best-effort.
173
+ stamp(proposalId, {
174
+ outcome: "auto-rejected",
175
+ reason,
176
+ confidence,
177
+ thresholds: { autoAccept: effectiveThreshold },
178
+ ...(currentContentHash !== undefined ? { contentHash: currentContentHash } : {}),
179
+ gate: gateLabel,
180
+ });
181
+ // A validation failure is permanent — the minted content can never satisfy
182
+ // the asset schema, so archive it as rejected instead of leaving it pending
183
+ // to be retried on every future run (the 90-day TTL is otherwise the first
184
+ // thing ever to touch these zombies). Best-effort; ctx=undefined mirrors the
185
+ // promoteFn call above so the archive hits the same stashDir-derived DB.
186
+ //
187
+ // Discriminate on the STRUCTURED error, not the `reason` string: promoteProposal
188
+ // throws UsageError("MISSING_REQUIRED_ARGUMENT") only for the validateProposal
189
+ // failure branch. Message-sniffing (`reason` = "validation:<kind>") would also
190
+ // match a transient git-push rejection ("[rejected] ... non-fast-forward") from
191
+ // a git-backed write target and permanently archive a valid, retryable proposal.
192
+ const isPermanentValidationFailure = err instanceof UsageError && err.code === "MISSING_REQUIRED_ARGUMENT";
193
+ if (isPermanentValidationFailure) {
194
+ try {
195
+ archiveProposal(cfg.stashDir, proposalId, "rejected", `auto-accept ${reason}`, undefined);
196
+ }
197
+ catch (archiveErr) {
198
+ warn(`[improve] ${cfg.phase} failed to archive validation-failed proposal ${proposalId}: ${archiveErr instanceof Error ? archiveErr.message : String(archiveErr)}`);
199
+ }
200
+ }
201
+ // If exploration budget was consumed but promotion failed, restore the slot
202
+ // so the budget isn't exhausted on errors.
203
+ if (isExploration)
204
+ explorationRemaining += 1;
89
205
  }
90
206
  }
91
207
  return result;
@@ -114,15 +230,46 @@ export function resolveExtractConfidence(proposal) {
114
230
  /**
115
231
  * Build a gate config for a phase, inheriting global settings from the
116
232
  * improve options. Callers supply only the phase-specific overrides.
233
+ *
234
+ * WS-4 additions:
235
+ * - When `shared.stateDbPath` is provided, reads the persisted per-phase
236
+ * threshold from `improve_gate_thresholds` (Migration 012). The phase
237
+ * value overrides `globalThreshold` but is still floored by
238
+ * `minimumThreshold`. Falls back to `globalThreshold` when no row exists.
239
+ * - Computes `explorationBudgetCount` from
240
+ * `config.improve.exploration.budgetFraction × candidateCount` when the
241
+ * exploration budget is enabled. Defaults to 0 (no exploration).
117
242
  */
118
243
  export function makeGateConfig(phase, shared, overrides = {}) {
244
+ // WS-4: read per-phase threshold from state.db when available.
245
+ let phaseThreshold;
246
+ if (shared.stateDbPath && shared.globalThreshold !== undefined) {
247
+ try {
248
+ phaseThreshold = withStateDb((db) => getPhaseThreshold(db, phase) ?? undefined, {
249
+ path: shared.stateDbPath,
250
+ });
251
+ }
252
+ catch {
253
+ // DB unavailable — fall back to globalThreshold silently.
254
+ }
255
+ }
256
+ // WS-4: compute exploration budget count from config fraction × candidateCount.
257
+ let explorationBudgetCount;
258
+ const resolvedConfig = typeof shared.config === "function" ? shared.config() : shared.config;
259
+ const explorationCfg = resolvedConfig.improve?.exploration;
260
+ if (explorationCfg?.enabled && shared.candidateCount !== undefined && shared.candidateCount > 0) {
261
+ const fraction = Math.min(1, Math.max(0, explorationCfg.budgetFraction ?? 0.05));
262
+ explorationBudgetCount = Math.max(0, Math.floor(fraction * shared.candidateCount));
263
+ }
119
264
  return {
120
265
  phase,
121
266
  globalThreshold: shared.globalThreshold,
267
+ ...(phaseThreshold !== undefined ? { phaseThreshold } : {}),
122
268
  dryRun: shared.dryRun,
123
269
  stashDir: shared.stashDir,
124
270
  config: shared.config,
125
271
  eventsCtx: shared.eventsCtx,
272
+ ...(explorationBudgetCount !== undefined && explorationBudgetCount > 0 ? { explorationBudgetCount } : {}),
126
273
  ...overrides,
127
274
  };
128
275
  }
@@ -8,15 +8,71 @@ import { output, runWithJsonErrors } from "../../cli/shared.js";
8
8
  import { loadConfig } from "../../core/config/config.js";
9
9
  import { UsageError } from "../../core/errors.js";
10
10
  import { getCacheDir } from "../../core/paths.js";
11
+ import { withStateDb } from "../../core/state-db.js";
11
12
  import { clearLogFile, setLogFile } from "../../core/warn.js";
13
+ import { closeDatabase, openExistingDatabase } from "../../indexer/db/db.js";
12
14
  import { resolveSourceEntries } from "../../indexer/search/search-source.js";
13
- import { getHyphenatedArg, getHyphenatedBoolean, parseFlagValue } from "../../output/context.js";
15
+ import { parseFlagValue } from "../../output/context.js";
16
+ import { getActiveCanaries, queryRecentCycleMetrics } from "../../storage/repositories/canaries-repository.js";
17
+ import { refreshCanarySet } from "./collapse-detector.js";
14
18
  import { akmImprove } from "./improve.js";
15
19
  import { buildImproveRunId, recordTerminatedImproveRun, relativeImproveResultPath, writeImproveResultFile, } from "./improve-result-file.js";
20
+ import { runImproveSession } from "./improve-session.js";
21
+ // R5 — collapse-detector canary set inspection / explicit refresh. The
22
+ // detector NEVER auto-refreshes the canary set (silent re-baselining is how a
23
+ // slow collapse hides); this verb is the only refresh path.
24
+ //
25
+ // Dispatched from the parent improve run() on `scope === "canary"` — NOT a
26
+ // citty subCommand: registering subCommands makes citty treat EVERY first
27
+ // positional as a subcommand name, breaking `akm improve <type|ref>` outright
28
+ // (citty throws "Unknown command memory"), and citty also re-runs the parent
29
+ // run() after a matched subcommand.
30
+ async function runCanaryInspection(refresh) {
31
+ const config = loadConfig();
32
+ const cfg = config.improve?.collapseDetector ?? {};
33
+ const result = withStateDb((stateDb) => {
34
+ let refreshOutcome;
35
+ if (refresh) {
36
+ const indexDb = openExistingDatabase();
37
+ try {
38
+ // Mint-first, deactivate-after (refreshCanarySet): an empty/unreadable
39
+ // index keeps the old baseline instead of destroying it.
40
+ refreshOutcome = refreshCanarySet(stateDb, indexDb, cfg) === null ? "kept-old-set" : "refreshed";
41
+ }
42
+ finally {
43
+ closeDatabase(indexDb);
44
+ }
45
+ }
46
+ const canaries = getActiveCanaries(stateDb);
47
+ const canarySetId = canaries[0]?.canary_set_id;
48
+ const recentCycles = canarySetId ? queryRecentCycleMetrics(stateDb, canarySetId, cfg.windowCycles ?? 5) : [];
49
+ return {
50
+ schemaVersion: 1,
51
+ ok: true,
52
+ refreshed: refreshOutcome === "refreshed",
53
+ ...(refreshOutcome === "kept-old-set"
54
+ ? { warning: "refresh skipped: no mintable learning entries in the index — existing canary set kept" }
55
+ : {}),
56
+ canarySetId: canarySetId ?? null,
57
+ canaries: canaries.map((c) => ({ id: c.id, anchorRef: c.anchor_ref, query: c.query })),
58
+ recentCycles: recentCycles.map((r) => ({
59
+ ts: r.ts,
60
+ pass: r.pass,
61
+ meanRecall: r.mean_recall,
62
+ meanNdcg: r.mean_ndcg,
63
+ distinctContentRatio: r.distinct_content_ratio,
64
+ acceptedActions: r.accepted_actions,
65
+ mergeFloorViolations: r.merge_floor_violations,
66
+ alerts: JSON.parse(r.alerts_json),
67
+ })),
68
+ };
69
+ });
70
+ output("improve-canary", result);
71
+ }
16
72
  export const improveCommand = defineCommand({
17
73
  meta: {
18
74
  name: "improve",
19
- description: "Analyze existing AKM assets and generate improvement proposals; also consolidates memories when profiles.improve.default.processes.consolidate.enabled is true",
75
+ description: "Analyze existing AKM assets and generate improvement proposals; also consolidates memories when profiles.improve.default.processes.consolidate.enabled is true. `akm improve canary [--refresh]` inspects the collapse-detector canary set.",
20
76
  },
21
77
  args: {
22
78
  scope: {
@@ -25,6 +81,11 @@ export const improveCommand = defineCommand({
25
81
  required: false,
26
82
  },
27
83
  task: { type: "string", description: "Add extra guidance for this improvement pass" },
84
+ refresh: {
85
+ type: "boolean",
86
+ description: "(canary scope only) Mint a new collapse-detector canary set and deactivate the old one; old rows and their cycle history are retained",
87
+ default: false,
88
+ },
28
89
  "dry-run": { type: "boolean", description: "Show planned actions without writing", default: false },
29
90
  target: { type: "string", description: "Override the write target for accepted proposals" },
30
91
  "auto-accept": {
@@ -73,37 +134,44 @@ export const improveCommand = defineCommand({
73
134
  },
74
135
  },
75
136
  async run({ args }) {
137
+ // "canary" is a reserved scope word (never a valid asset type, and refs
138
+ // contain ":"): dispatch to the detector inspection verb instead of an
139
+ // improve run.
140
+ if (args.scope === "canary") {
141
+ await runWithJsonErrors(() => runCanaryInspection(args.refresh));
142
+ return;
143
+ }
76
144
  await runWithJsonErrors(async () => {
77
145
  const formatFlagValue = parseFlagValue(process.argv, "--format");
78
146
  if (formatFlagValue !== undefined) {
79
147
  throw new UsageError(`akm improve does not accept --format. That flag controls output formatting for other commands (search, show, etc.).\n` +
80
148
  `Did you mean: akm improve (no --format flag)?`, "INVALID_FLAG_VALUE");
81
149
  }
82
- const jsonToStdout = getHyphenatedBoolean(args, "json-to-stdout");
83
- const autoAcceptRaw = getHyphenatedArg(args, "auto-accept");
150
+ const jsonToStdout = args["json-to-stdout"];
151
+ const autoAcceptRaw = args["auto-accept"];
84
152
  const autoAccept = parseAutoAcceptFlag(autoAcceptRaw);
85
153
  const targetArg = getStringArg(args, "target");
86
154
  const taskArg = getStringArg(args, "task");
87
- const dryRun = getHyphenatedBoolean(args, "dry-run");
155
+ const dryRun = args["dry-run"];
88
156
  const limitRaw = parsePositiveIntFlag(args.limit ?? undefined);
89
- const timeoutMs = parsePositiveIntFlag(getHyphenatedArg(args, "timeout-ms"), "--timeout-ms");
90
- const consolidateRecoveryRaw = getHyphenatedArg(args, "consolidate-recovery");
157
+ const timeoutMs = parsePositiveIntFlag(args["timeout-ms"], "--timeout-ms");
158
+ const consolidateRecoveryRaw = args["consolidate-recovery"];
91
159
  const consolidateRecovery = consolidateRecoveryRaw === undefined
92
160
  ? undefined
93
161
  : consolidateRecoveryRaw.trim().toLowerCase();
94
162
  if (consolidateRecovery !== undefined && consolidateRecovery !== "abort" && consolidateRecovery !== "clean") {
95
163
  throw new UsageError(`Invalid --consolidate-recovery value: "${consolidateRecoveryRaw}". Must be one of: abort, clean.`, "INVALID_FLAG_VALUE");
96
164
  }
97
- const minRetrievalCountRaw = getHyphenatedArg(args, "min-retrieval-count");
165
+ const minRetrievalCountRaw = args["min-retrieval-count"];
98
166
  const minRetrievalCount = parseNonNegativeIntFlag(minRetrievalCountRaw, "--min-retrieval-count");
99
- const requireFeedbackSignal = getHyphenatedBoolean(args, "require-feedback-signal");
100
- const skipIfLocked = getHyphenatedBoolean(args, "skip-if-locked");
167
+ const requireFeedbackSignal = args["require-feedback-signal"];
168
+ const skipIfLocked = args["skip-if-locked"];
101
169
  const profileArg = getStringArg(args, "profile");
102
170
  // Only set the keys the user actually passed (citty leaves the flag
103
171
  // undefined unless `--sync`/`--no-sync` / `--push`/`--no-push` appears),
104
172
  // so the resolved profile `sync` block wins by default.
105
- const syncFlag = getHyphenatedArg(args, "sync");
106
- const pushFlag = getHyphenatedArg(args, "push");
173
+ const syncFlag = args.sync;
174
+ const pushFlag = args.push;
107
175
  const syncOverride = {};
108
176
  if (syncFlag !== undefined)
109
177
  syncOverride.enabled = syncFlag;
@@ -144,67 +212,44 @@ export const improveCommand = defineCommand({
144
212
  process.stderr.write(`warning: failed to persist terminated improve run ${runId}: ${err instanceof Error ? err.message : String(err)}\n`);
145
213
  }
146
214
  };
147
- // M5 (code-health round 2): signal -> {exit code, reason, ack message}
148
- // as an explicit table instead of three near-identical handlers. The
149
- // persist of the terminated-run row MUST complete before process.exit so
150
- // a SIGTERM'd run (e.g. cron timeout) always leaves a row in
151
- // improve_runs. recordTerminatedImproveRun is fully synchronous
152
- // (bun:sqlite writes are sync), so the in-line call below blocks until
153
- // the row is flushed before we exit.
154
- const SIGNAL_TABLE = {
155
- SIGTERM: { code: 143, reason: "SIGTERM", ack: true },
156
- SIGINT: { code: 130, reason: "SIGINT", ack: true },
157
- SIGHUP: { code: 129, reason: "SIGHUP", ack: false },
158
- };
159
- const makeSignalHandler = (sig) => () => {
160
- const { code, reason, ack } = SIGNAL_TABLE[sig];
161
- // Hard-exit fallback: if the synchronous persist ever hangs (e.g. a
162
- // stuck sqlite lock under contention), the watchdog still exits with
163
- // the correct code instead of leaving a zombie process. .unref() keeps
164
- // the timer from holding the loop open on the normal (fast) path.
165
- const watchdog = setTimeout(() => process.exit(code), 2000);
166
- if (typeof watchdog.unref === "function")
167
- watchdog.unref();
168
- try {
169
- persistTerminated(reason);
170
- }
171
- finally {
172
- clearTimeout(watchdog);
173
- }
174
- if (ack) {
175
- process.stderr.write(`[improve] received ${sig}; recorded terminated run ${runId}\n`);
176
- }
177
- process.exit(code);
178
- };
179
- const sigtermHandler = makeSignalHandler("SIGTERM");
180
- const sigintHandler = makeSignalHandler("SIGINT");
181
- const sighupHandler = makeSignalHandler("SIGHUP");
182
- process.once("SIGTERM", sigtermHandler);
183
- process.once("SIGINT", sigintHandler);
184
- process.once("SIGHUP", sighupHandler);
215
+ // R8: the signal table / handlers / watchdog / persist-before-exit
216
+ // choreography lives in `runImproveSession`. It registers the
217
+ // SIGTERM/SIGINT/SIGHUP handlers (each persists the terminated-run row
218
+ // BEFORE process.exit so a SIGTERM'd run e.g. cron timeout always
219
+ // leaves a row in improve_runs), awaits the work, then removes the
220
+ // handlers on the way out. `onTerminate` persists synchronously
221
+ // (recordTerminatedImproveRun -> bun:sqlite writes are sync), and the
222
+ // 2000ms watchdog inside the session force-exits if that ever hangs.
185
223
  let improveResult;
186
224
  try {
187
- improveResult = await akmImprove({
188
- scope: scopeArg,
189
- task: taskArg,
190
- dryRun,
191
- target: targetArg,
192
- autoAccept,
193
- ...(runId !== undefined ? { runId } : {}),
194
- ...(limitRaw !== undefined ? { limit: limitRaw } : {}),
195
- ...(timeoutMs !== undefined ? { timeoutMs } : {}),
196
- ...(minRetrievalCount !== undefined ? { minRetrievalCount } : {}),
197
- ...(requireFeedbackSignal ? { requireFeedbackSignal } : {}),
198
- ...(skipIfLocked ? { skipIfLocked } : {}),
199
- ...(profileArg !== undefined ? { profile: profileArg } : {}),
200
- ...(Object.keys(syncOverride).length > 0 ? { sync: syncOverride } : {}),
201
- consolidateOptions: {
202
- target: targetArg,
225
+ improveResult = await runImproveSession({
226
+ runWork: () => akmImprove({
227
+ scope: scopeArg,
228
+ task: taskArg,
203
229
  dryRun,
230
+ target: targetArg,
204
231
  autoAccept,
205
- task: taskArg,
206
- ...(consolidateRecovery !== undefined ? { recoveryMode: consolidateRecovery } : {}),
207
- },
232
+ ...(runId !== undefined ? { runId } : {}),
233
+ ...(limitRaw !== undefined ? { limit: limitRaw } : {}),
234
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
235
+ ...(minRetrievalCount !== undefined ? { minRetrievalCount } : {}),
236
+ ...(requireFeedbackSignal ? { requireFeedbackSignal } : {}),
237
+ ...(skipIfLocked ? { skipIfLocked } : {}),
238
+ ...(profileArg !== undefined ? { profile: profileArg } : {}),
239
+ ...(Object.keys(syncOverride).length > 0 ? { sync: syncOverride } : {}),
240
+ consolidateOptions: {
241
+ target: targetArg,
242
+ dryRun,
243
+ autoAccept,
244
+ task: taskArg,
245
+ ...(consolidateRecovery !== undefined ? { recoveryMode: consolidateRecovery } : {}),
246
+ },
247
+ }),
248
+ }, {
249
+ signalSource: process,
250
+ exit: process.exit,
251
+ onTerminate: (reason) => persistTerminated(reason),
252
+ ack: (message) => process.stderr.write(`[improve] ${message}; recorded terminated run ${runId}\n`),
208
253
  });
209
254
  }
210
255
  catch (err) {
@@ -216,9 +261,6 @@ export const improveCommand = defineCommand({
216
261
  throw err;
217
262
  }
218
263
  finally {
219
- process.removeListener("SIGTERM", sigtermHandler);
220
- process.removeListener("SIGINT", sigintHandler);
221
- process.removeListener("SIGHUP", sighupHandler);
222
264
  clearLogFile();
223
265
  }
224
266
  const durationMs = Date.now() - startedAtMs;
@@ -246,7 +288,7 @@ export const improveCommand = defineCommand({
246
288
  runRecorded = true; // Suppress any late signal-handler write — the success path owns the row now.
247
289
  if (primaryStashDir) {
248
290
  try {
249
- writeImproveResultFile(primaryStashDir, runId, improveResult, startedAtIso);
291
+ writeImproveResultFile(primaryStashDir, runId, improveResult, startedAtIso, profileArg ?? null);
250
292
  }
251
293
  catch (err) {
252
294
  // Stderr warning on the failure path is preferable to crashing