akm-cli 0.8.1 → 0.9.0-beta.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 (318) hide show
  1. package/CHANGELOG.md +258 -0
  2. package/dist/assets/help/help-proposals.md +1 -2
  3. package/dist/assets/hints/cli-hints-full.md +34 -19
  4. package/dist/assets/hints/cli-hints-short.md +1 -1
  5. package/dist/assets/profiles/catchup.json +13 -0
  6. package/dist/assets/profiles/consolidate.json +13 -0
  7. package/dist/assets/profiles/frequent.json +13 -0
  8. package/dist/assets/stash-skeleton/README.md +76 -0
  9. package/dist/assets/tasks/core/backup.yml +4 -0
  10. package/dist/assets/tasks/core/extract.yml +4 -0
  11. package/dist/assets/tasks/core/improve.yml +4 -0
  12. package/dist/assets/tasks/core/index-refresh.yml +4 -0
  13. package/dist/assets/tasks/core/sync.yml +4 -0
  14. package/dist/assets/tasks/core/update-stashes.yml +4 -0
  15. package/dist/assets/tasks/core/version-check.yml +4 -0
  16. package/dist/cli/config-migrate.js +6 -6
  17. package/dist/cli/config-validate.js +4 -4
  18. package/dist/cli/confirm.js +3 -3
  19. package/dist/cli/parse-args.js +1 -1
  20. package/dist/cli/shared.js +51 -14
  21. package/dist/cli-node.mjs +26 -0
  22. package/dist/cli.js +171 -3857
  23. package/dist/commands/{agent-dispatch.js → agent/agent-dispatch.js} +6 -6
  24. package/dist/commands/{agent-support.js → agent/agent-support.js} +2 -2
  25. package/dist/commands/agent/contribute-cli.js +200 -0
  26. package/dist/commands/completions.js +1 -1
  27. package/dist/commands/config-cli.js +240 -3
  28. package/dist/commands/config-edit.js +344 -0
  29. package/dist/commands/db-cli.js +2 -2
  30. package/dist/commands/env/env-cli.js +529 -0
  31. package/dist/commands/env/env.js +410 -0
  32. package/dist/commands/env/secret-cli.js +259 -0
  33. package/dist/commands/{secret.js → env/secret.js} +6 -47
  34. package/dist/commands/events.js +4 -4
  35. package/dist/commands/feedback-cli.js +18 -34
  36. package/dist/commands/graph/graph-cli.js +132 -0
  37. package/dist/commands/{graph.js → graph/graph.js} +22 -16
  38. package/dist/commands/health/checks.js +279 -0
  39. package/dist/commands/health.js +101 -249
  40. package/dist/commands/{consolidate.js → improve/consolidate.js} +52 -40
  41. package/dist/commands/{distill-promotion-policy.js → improve/distill-promotion-policy.js} +3 -3
  42. package/dist/commands/{distill.js → improve/distill.js} +39 -18
  43. package/dist/commands/{eval-cases.js → improve/eval-cases.js} +1 -1
  44. package/dist/commands/{extract-cli.js → improve/extract-cli.js} +4 -4
  45. package/dist/commands/{extract-prompt.js → improve/extract-prompt.js} +2 -2
  46. package/dist/commands/{extract.js → improve/extract.js} +185 -26
  47. package/dist/commands/{improve-auto-accept.js → improve/improve-auto-accept.js} +4 -4
  48. package/dist/commands/{improve-cli.js → improve/improve-cli.js} +45 -23
  49. package/dist/commands/{improve-profiles.js → improve/improve-profiles.js} +13 -7
  50. package/dist/commands/{improve-result-file.js → improve/improve-result-file.js} +10 -5
  51. package/dist/commands/{improve.js → improve/improve.js} +536 -248
  52. package/dist/{core → commands/improve/memory}/memory-belief.js +2 -2
  53. package/dist/{core → commands/improve/memory}/memory-contradiction-detect.js +5 -5
  54. package/dist/{core → commands/improve/memory}/memory-improve.js +4 -4
  55. package/dist/commands/{reflect.js → improve/reflect.js} +33 -28
  56. package/dist/commands/improve/session-asset.js +248 -0
  57. package/dist/commands/lint/agent-linter.js +1 -1
  58. package/dist/commands/lint/base-linter.js +55 -37
  59. package/dist/commands/lint/command-linter.js +1 -1
  60. package/dist/commands/lint/default-linter.js +1 -1
  61. package/dist/commands/lint/env-key-rules.js +1 -1
  62. package/dist/commands/lint/index.js +19 -25
  63. package/dist/commands/lint/knowledge-linter.js +1 -1
  64. package/dist/commands/lint/memory-linter.js +1 -1
  65. package/dist/commands/lint/registry.js +8 -8
  66. package/dist/commands/lint/skill-linter.js +1 -1
  67. package/dist/commands/lint/task-linter.js +1 -1
  68. package/dist/commands/lint/workflow-linter.js +1 -1
  69. package/dist/commands/lint.js +1 -1
  70. package/dist/commands/observability-cli.js +244 -0
  71. package/dist/commands/{proposal-drain-policies.js → proposal/drain-policies.js} +3 -3
  72. package/dist/commands/{proposal-drain.js → proposal/drain.js} +15 -10
  73. package/dist/commands/proposal/proposal-cli.js +478 -0
  74. package/dist/commands/{proposal.js → proposal/proposal.js} +5 -5
  75. package/dist/commands/{propose.js → proposal/propose.js} +11 -11
  76. package/dist/{core → commands/proposal/validators}/proposal-quality-validators.js +8 -3
  77. package/dist/{core → commands/proposal/validators}/proposal-validators.js +5 -5
  78. package/dist/{core → commands/proposal/validators}/proposals.js +13 -7
  79. package/dist/commands/{curate.js → read/curate.js} +7 -7
  80. package/dist/commands/{knowledge.js → read/knowledge.js} +22 -9
  81. package/dist/commands/{registry-search.js → read/registry-search.js} +5 -5
  82. package/dist/commands/{remember-cli.js → read/remember-cli.js} +15 -7
  83. package/dist/commands/read/search-cli.js +207 -0
  84. package/dist/commands/{search.js → read/search.js} +22 -27
  85. package/dist/commands/{show.js → read/show.js} +77 -44
  86. package/dist/commands/registry-cli.js +8 -8
  87. package/dist/commands/remember.js +8 -8
  88. package/dist/commands/sources/add-cli.js +293 -0
  89. package/dist/commands/{history.js → sources/history.js} +27 -25
  90. package/dist/commands/{info.js → sources/info.js} +6 -6
  91. package/dist/commands/{init.js → sources/init.js} +10 -5
  92. package/dist/commands/{installed-stashes.js → sources/installed-stashes.js} +12 -12
  93. package/dist/commands/{migration-help.js → sources/migration-help.js} +3 -2
  94. package/dist/commands/{schema-repair.js → sources/schema-repair.js} +8 -8
  95. package/dist/commands/{self-update.js → sources/self-update.js} +10 -9
  96. package/dist/commands/{source-add.js → sources/source-add.js} +10 -10
  97. package/dist/commands/{source-clone.js → sources/source-clone.js} +7 -7
  98. package/dist/commands/{source-manage.js → sources/source-manage.js} +4 -4
  99. package/dist/commands/sources/sources-cli.js +305 -0
  100. package/dist/commands/sources/stash-cli.js +219 -0
  101. package/dist/commands/sources/stash-skeleton.js +79 -0
  102. package/dist/commands/tasks/default-tasks.js +173 -0
  103. package/dist/commands/tasks/tasks-cli.js +210 -0
  104. package/dist/commands/{tasks.js → tasks/tasks.js} +14 -14
  105. package/dist/commands/wiki-cli.js +307 -0
  106. package/dist/commands/workflow-cli.js +329 -0
  107. package/dist/core/action-contributors.js +1 -1
  108. package/dist/core/assert.js +40 -0
  109. package/dist/core/asset/asset-create.js +54 -0
  110. package/dist/core/{asset-ref.js → asset/asset-ref.js} +21 -4
  111. package/dist/core/{asset-registry.js → asset/asset-registry.js} +3 -3
  112. package/dist/core/{asset-spec.js → asset/asset-spec.js} +17 -31
  113. package/dist/core/{markdown.js → asset/markdown.js} +1 -1
  114. package/dist/core/asset/stash-meta.js +110 -0
  115. package/dist/core/best-effort.js +64 -0
  116. package/dist/core/common.js +32 -18
  117. package/dist/core/{config-io.js → config/config-io.js} +29 -19
  118. package/dist/core/{config-migration.js → config/config-migration.js} +11 -9
  119. package/dist/core/{config-schema.js → config/config-schema.js} +45 -1
  120. package/dist/core/config/config-types.js +16 -0
  121. package/dist/core/{config-walker.js → config/config-walker.js} +2 -2
  122. package/dist/core/{config.js → config/config.js} +10 -8
  123. package/dist/core/env-secret-ref.js +90 -0
  124. package/dist/core/errors.js +13 -3
  125. package/dist/core/events.js +27 -4
  126. package/dist/core/file-lock.js +1 -1
  127. package/dist/core/improve-types.js +48 -0
  128. package/dist/core/lesson-lint.js +2 -2
  129. package/dist/core/paths.js +2 -2
  130. package/dist/{setup/ripgrep-install.js → core/ripgrep/install.js} +2 -2
  131. package/dist/{setup/ripgrep-resolve.js → core/ripgrep/resolve.js} +2 -2
  132. package/dist/core/state-db.js +88 -46
  133. package/dist/core/text-truncation.js +148 -0
  134. package/dist/core/time.js +1 -1
  135. package/dist/core/write-source.js +98 -85
  136. package/dist/indexer/{db-backup.js → db/db-backup.js} +9 -24
  137. package/dist/indexer/{db.js → db/db.js} +126 -116
  138. package/dist/indexer/{graph-db.js → db/graph-db.js} +9 -4
  139. package/dist/indexer/{llm-cache.js → db/llm-cache.js} +15 -12
  140. package/dist/indexer/ensure-index.js +4 -4
  141. package/dist/indexer/{graph-boost.js → graph/graph-boost.js} +1 -1
  142. package/dist/indexer/{graph-extraction.js → graph/graph-extraction.js} +55 -13
  143. package/dist/indexer/indexer.js +37 -30
  144. package/dist/indexer/init.js +54 -0
  145. package/dist/indexer/manifest.js +10 -10
  146. package/dist/indexer/{memory-inference.js → passes/memory-inference.js} +92 -23
  147. package/dist/indexer/{metadata-contributors.js → passes/metadata-contributors.js} +10 -8
  148. package/dist/indexer/{metadata.js → passes/metadata.js} +15 -19
  149. package/dist/indexer/{staleness-detect.js → passes/staleness-detect.js} +53 -12
  150. package/dist/indexer/{db-search.js → search/db-search.js} +28 -16
  151. package/dist/indexer/{ranking-contributors.js → search/ranking-contributors.js} +1 -1
  152. package/dist/indexer/{ranking.js → search/ranking.js} +2 -2
  153. package/dist/indexer/{search-hit-enrichers.js → search/search-hit-enrichers.js} +3 -3
  154. package/dist/indexer/{search-source.js → search/search-source.js} +8 -8
  155. package/dist/indexer/{semantic-status.js → search/semantic-status.js} +3 -3
  156. package/dist/indexer/usage/unmigrated-vaults-guard.js +94 -0
  157. package/dist/indexer/{usage-events.js → usage/usage-events.js} +32 -0
  158. package/dist/indexer/{file-context.js → walk/file-context.js} +10 -15
  159. package/dist/indexer/{matchers.js → walk/matchers.js} +13 -9
  160. package/dist/indexer/{path-resolver.js → walk/path-resolver.js} +6 -6
  161. package/dist/indexer/{project-context.js → walk/project-context.js} +1 -1
  162. package/dist/indexer/{walker.js → walk/walker.js} +4 -3
  163. package/dist/integrations/agent/builder-shared.js +39 -0
  164. package/dist/integrations/agent/builders.js +14 -81
  165. package/dist/integrations/agent/config.js +6 -4
  166. package/dist/integrations/agent/detect.js +1 -1
  167. package/dist/integrations/agent/index.js +23 -8
  168. package/dist/integrations/agent/prompts.js +2 -3
  169. package/dist/integrations/agent/runner.js +22 -3
  170. package/dist/integrations/agent/spawn.js +9 -10
  171. package/dist/integrations/harnesses/claude/agent-builder.js +48 -0
  172. package/dist/integrations/harnesses/claude/config-import.js +70 -0
  173. package/dist/integrations/harnesses/claude/index.js +64 -0
  174. package/dist/integrations/{session-logs/providers/claude-code.js → harnesses/claude/session-log.js} +16 -1
  175. package/dist/integrations/harnesses/index.js +144 -0
  176. package/dist/integrations/harnesses/opencode/agent-builder.js +43 -0
  177. package/dist/integrations/harnesses/opencode/config-import.js +82 -0
  178. package/dist/integrations/harnesses/opencode/index.js +59 -0
  179. package/dist/integrations/{session-logs/providers/opencode.js → harnesses/opencode/session-log.js} +1 -1
  180. package/dist/integrations/harnesses/opencode-sdk/index.js +49 -0
  181. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +234 -0
  182. package/dist/integrations/harnesses/types.js +43 -0
  183. package/dist/integrations/lockfile.js +7 -16
  184. package/dist/integrations/session-logs/index.js +82 -9
  185. package/dist/llm/call-ai.js +4 -4
  186. package/dist/llm/client.js +131 -6
  187. package/dist/llm/embedder.js +6 -6
  188. package/dist/llm/embedders/local.js +9 -22
  189. package/dist/llm/embedders/remote.js +2 -2
  190. package/dist/llm/embedders/types.js +1 -1
  191. package/dist/llm/graph-extract.js +31 -12
  192. package/dist/llm/index-passes.js +1 -1
  193. package/dist/llm/memory-infer.js +12 -5
  194. package/dist/llm/metadata-enhance.js +2 -2
  195. package/dist/output/context.js +6 -44
  196. package/dist/output/renderers.js +88 -58
  197. package/dist/output/shapes/curate.js +7 -3
  198. package/dist/output/shapes/distill.js +7 -3
  199. package/dist/output/shapes/env-list.js +18 -16
  200. package/dist/output/shapes/events.js +5 -4
  201. package/dist/output/shapes/helpers.js +2 -4
  202. package/dist/output/shapes/history.js +7 -3
  203. package/dist/output/shapes/passthrough.js +8 -11
  204. package/dist/output/shapes/{proposal-accept.js → proposal/accept.js} +7 -3
  205. package/dist/output/shapes/{proposal-diff.js → proposal/diff.js} +7 -3
  206. package/dist/output/shapes/{proposal-list.js → proposal/list.js} +7 -3
  207. package/dist/output/shapes/{proposal-producer.js → proposal/producer.js} +5 -4
  208. package/dist/output/shapes/{proposal-reject.js → proposal/reject.js} +7 -3
  209. package/dist/output/shapes/{proposal-show.js → proposal/show.js} +7 -3
  210. package/dist/output/shapes/registry-search.js +7 -3
  211. package/dist/output/shapes/registry.js +12 -0
  212. package/dist/output/shapes/search.js +7 -3
  213. package/dist/output/shapes/secret-list.js +18 -16
  214. package/dist/output/shapes/show.js +7 -3
  215. package/dist/output/shapes.js +55 -30
  216. package/dist/output/text/add.js +2 -3
  217. package/dist/output/text/clone.js +2 -3
  218. package/dist/output/text/config.js +2 -3
  219. package/dist/output/text/curate.js +4 -3
  220. package/dist/output/text/distill.js +2 -3
  221. package/dist/output/text/enable-disable.js +5 -4
  222. package/dist/output/text/env.js +13 -0
  223. package/dist/output/text/events.js +5 -4
  224. package/dist/output/text/feedback.js +4 -3
  225. package/dist/output/text/helpers.js +54 -39
  226. package/dist/output/text/history.js +2 -3
  227. package/dist/output/text/import.js +2 -3
  228. package/dist/output/text/index.js +2 -3
  229. package/dist/output/text/info.js +2 -3
  230. package/dist/output/text/init.js +2 -3
  231. package/dist/output/text/list.js +2 -3
  232. package/dist/output/text/proposal/producer.js +9 -0
  233. package/dist/output/text/proposal/proposal.js +13 -0
  234. package/dist/output/text/registry-commands.js +8 -7
  235. package/dist/output/text/registry.js +12 -0
  236. package/dist/output/text/remember.js +4 -3
  237. package/dist/output/text/remove.js +2 -3
  238. package/dist/output/text/save.js +2 -3
  239. package/dist/output/text/search.js +4 -3
  240. package/dist/output/text/show.js +4 -3
  241. package/dist/output/text/update.js +2 -3
  242. package/dist/output/text/upgrade.js +2 -3
  243. package/dist/output/text/wiki.js +12 -11
  244. package/dist/output/text/workflow.js +12 -10
  245. package/dist/output/text.js +66 -32
  246. package/dist/registry/build-index.js +11 -10
  247. package/dist/registry/factory.js +1 -1
  248. package/dist/registry/origin-resolve.js +1 -1
  249. package/dist/registry/providers/index.js +2 -2
  250. package/dist/registry/providers/skills-sh.js +91 -72
  251. package/dist/registry/providers/static-index.js +75 -52
  252. package/dist/registry/resolve.js +3 -3
  253. package/dist/runtime.js +242 -0
  254. package/dist/scripts/migrate-storage.js +1594 -673
  255. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +240 -166
  256. package/dist/setup/detect.js +338 -9
  257. package/dist/setup/harness-config-import.js +56 -0
  258. package/dist/setup/registry-stash-loader.js +99 -0
  259. package/dist/setup/setup.js +664 -96
  260. package/dist/sources/include.js +1 -1
  261. package/dist/sources/provider-factory.js +2 -2
  262. package/dist/sources/providers/filesystem.js +3 -3
  263. package/dist/sources/providers/git.js +9 -9
  264. package/dist/sources/providers/index.js +4 -4
  265. package/dist/sources/providers/npm.js +6 -6
  266. package/dist/sources/providers/provider-utils.js +13 -20
  267. package/dist/sources/providers/sync-from-ref.js +5 -5
  268. package/dist/sources/providers/tar-utils.js +2 -2
  269. package/dist/sources/providers/website.js +2 -2
  270. package/dist/sources/resolve.js +5 -5
  271. package/dist/sources/website-ingest.js +5 -5
  272. package/dist/storage/database.js +102 -0
  273. package/dist/storage/engines/sqlite-migrations.js +42 -0
  274. package/dist/storage/locations.js +25 -0
  275. package/dist/storage/repositories/index-db.js +43 -0
  276. package/dist/storage/repositories/workflow-runs-repository.js +141 -0
  277. package/dist/tasks/backends/cron.js +4 -4
  278. package/dist/tasks/backends/exec-utils.js +32 -0
  279. package/dist/tasks/backends/index.js +3 -3
  280. package/dist/tasks/backends/launchd.js +7 -14
  281. package/dist/tasks/backends/schtasks.js +7 -16
  282. package/dist/tasks/embedded.js +71 -0
  283. package/dist/tasks/parser.js +2 -2
  284. package/dist/tasks/resolveAkmBin.js +1 -1
  285. package/dist/tasks/runner.js +28 -15
  286. package/dist/tasks/schedule.js +1 -1
  287. package/dist/tasks/validator.js +7 -7
  288. package/dist/text-import-hook.mjs +51 -0
  289. package/dist/version.js +2 -1
  290. package/dist/wiki/wiki.js +7 -7
  291. package/dist/workflows/{authoring.js → authoring/authoring.js} +6 -6
  292. package/dist/workflows/{scope-key.js → authoring/scope-key.js} +1 -1
  293. package/dist/workflows/cli.js +1 -1
  294. package/dist/workflows/db.js +50 -32
  295. package/dist/workflows/parser.js +4 -4
  296. package/dist/workflows/renderer.js +5 -5
  297. package/dist/workflows/runtime/agent-identity.js +56 -0
  298. package/dist/workflows/runtime/checkin.js +57 -0
  299. package/dist/workflows/{runs.js → runtime/runs.js} +197 -101
  300. package/dist/workflows/validate-summary.js +82 -0
  301. package/docs/README.md +1 -1
  302. package/docs/data-and-telemetry.md +6 -6
  303. package/package.json +16 -8
  304. package/dist/commands/add-cli.js +0 -279
  305. package/dist/commands/env.js +0 -213
  306. package/dist/integrations/agent/sdk-runner.js +0 -126
  307. package/dist/output/shapes/vault-list.js +0 -19
  308. package/dist/output/text/proposal-producer.js +0 -8
  309. package/dist/output/text/proposal.js +0 -12
  310. package/dist/output/text/vault.js +0 -16
  311. /package/dist/core/{asset-serialize.js → asset/asset-serialize.js} +0 -0
  312. /package/dist/core/{frontmatter.js → asset/frontmatter.js} +0 -0
  313. /package/dist/core/{config-sources.js → config/config-sources.js} +0 -0
  314. /package/dist/indexer/{graph-dedup.js → graph/graph-dedup.js} +0 -0
  315. /package/dist/{core/config-types.js → indexer/passes/pass-context.js} +0 -0
  316. /package/dist/indexer/{search-fields.js → search/search-fields.js} +0 -0
  317. /package/dist/indexer/{index-context.js → walk/index-context.js} +0 -0
  318. /package/dist/workflows/{document-cache.js → runtime/document-cache.js} +0 -0
@@ -1,21 +1,47 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- import { stringify as yamlStringify } from "yaml";
5
- import { assembleAssetFromString } from "../core/asset-serialize";
6
- import { resolveStashDir, timestampForFilename } from "../core/common";
7
- import { getDefaultLlmConfig, loadConfig } from "../core/config";
8
- import { ConfigError, UsageError } from "../core/errors";
9
- import { appendEvent } from "../core/events";
10
- import { createProposal, isProposalSkipped } from "../core/proposals";
11
- import { getExtractedSessionsMap, openStateDatabase, shouldSkipAlreadyExtractedSession, upsertExtractedSession, } from "../core/state-db";
12
- import { warn } from "../core/warn";
13
- import { resolveImproveProcessRunnerFromProfile } from "../integrations/agent/runner";
14
- import { getAvailableHarnesses } from "../integrations/session-logs";
15
- import { preFilterSession } from "../integrations/session-logs/pre-filter";
16
- import { chatCompletion } from "../llm/client";
17
- import { isLlmFeatureEnabled, tryLlmFeature } from "../llm/feature-gate";
18
- import { buildExtractPrompt, EXTRACT_JSON_SCHEMA, parseExtractPayload } from "./extract-prompt";
4
+ /**
5
+ * `akm extract` session-insight extractor.
6
+ *
7
+ * Replaces the akm-plugin session-checkpoint hook with an on-demand extractor
8
+ * that reads native session files (claude-code JSONL, opencode storage tree)
9
+ * through the {@link SessionLogHarness} registry, pre-filters noise, and asks
10
+ * a bounded in-tree LLM to produce candidate memory/lesson/knowledge proposals
11
+ * for content the agent did NOT preserve via inline `akm remember`/`akm feedback`.
12
+ *
13
+ * Architectural notes:
14
+ * - Stateless. All file/LLM access goes through injectable seams so tests
15
+ * never touch a real platform.
16
+ * - Bounded LLM call wrapped by {@link tryLlmFeature} under the
17
+ * `session_extraction` gate (default-on; opt out via
18
+ * `profiles.improve.default.processes.extract.enabled: false`).
19
+ * - Proposals routed via `createProposal({ source: "extract", ... })` — the
20
+ * same review queue as reflect / distill / consolidate. Never direct-write.
21
+ * - Per-candidate body assembly merges description (+ when_to_use for lessons)
22
+ * into the body's YAML frontmatter so the accept-time
23
+ * descriptionQualityValidator passes — same pattern as the
24
+ * consolidate-writer fix.
25
+ */
26
+ import { assembleAsset } from "../../core/asset/asset-serialize.js";
27
+ import { resolveStashDir, timestampForFilename } from "../../core/common.js";
28
+ import { getDefaultLlmConfig, loadConfig } from "../../core/config/config.js";
29
+ import { ConfigError, UsageError } from "../../core/errors.js";
30
+ import { appendEvent } from "../../core/events.js";
31
+ import { getExtractedSessionsMap, openStateDatabase, shouldSkipAlreadyExtractedSession, upsertExtractedSession, } from "../../core/state-db.js";
32
+ import { repairTruncatedDescription } from "../../core/text-truncation.js";
33
+ import { warn } from "../../core/warn.js";
34
+ import { resolveImproveProcessRunnerFromProfile, runnerIsLlm } from "../../integrations/agent/runner.js";
35
+ import { normalizeHarnessId } from "../../integrations/harnesses/index.js";
36
+ import { getAvailableHarnesses } from "../../integrations/session-logs/index.js";
37
+ import { preFilterSession } from "../../integrations/session-logs/pre-filter.js";
38
+ import { chatCompletion } from "../../llm/client.js";
39
+ import { isLlmFeatureEnabled, tryLlmFeature } from "../../llm/feature-gate.js";
40
+ import { createProposal, isProposalSkipped } from "../proposal/validators/proposals.js";
41
+ import { buildExtractPrompt, EXTRACT_JSON_SCHEMA, parseExtractPayload } from "./extract-prompt.js";
42
+ import { buildSessionSummaryPrompt, parseSessionSummary, SESSION_SUMMARY_JSON_SCHEMA, sessionMeetsDurationGate, writeSessionAsset, } from "./session-asset.js";
43
+ /** Default minimum session duration (minutes) for session indexing (#561). */
44
+ const DEFAULT_MIN_SESSION_DURATION_MINUTES = 5;
19
45
  // ── Helpers ──────────────────────────────────────────────────────────────────
20
46
  /**
21
47
  * Parse a since-string into an absolute ms-epoch cutoff. Accepts:
@@ -49,7 +75,15 @@ export function parseSinceArg(value, now = Date.now()) {
49
75
  */
50
76
  function resolveHarness(type, harnesses) {
51
77
  const pool = harnesses ?? getAvailableHarnesses();
52
- return pool.find((h) => h.name === type);
78
+ // #563 id-normalization bridge: a provider's `name` is its runtime id (e.g.
79
+ // the Claude provider is "claude-code"), but the canonical harness id is
80
+ // "claude". Normalize BOTH the requested `--type` and each provider name to
81
+ // canonical before comparing, so `--type claude` and `--type claude-code`
82
+ // both resolve to the Claude provider. Behaviour fix: previously only the
83
+ // exact runtime string ("claude-code") matched; the canonical "claude" used
84
+ // everywhere else (agent profiles, config schema) silently found nothing.
85
+ const wanted = normalizeHarnessId(type);
86
+ return pool.find((h) => normalizeHarnessId(h.name) === wanted);
53
87
  }
54
88
  /**
55
89
  * Build the ref + content for a candidate. The body must contain a
@@ -59,16 +93,19 @@ function resolveHarness(type, harnesses) {
59
93
  */
60
94
  function buildCandidateProposal(candidate, sourceRef) {
61
95
  const ref = `${candidate.type}:${candidate.name}`;
96
+ // Post-generation repair pass (#556): deterministically complete a
97
+ // description the LLM sliced mid-sentence before it reaches the
98
+ // auto-accept validators. No-op (byte-identical) for valid descriptions.
99
+ const description = repairTruncatedDescription(candidate.description, candidate.body);
62
100
  const fm = {
63
- description: candidate.description,
101
+ description,
64
102
  sources: [`session:${sourceRef.harness}:${sourceRef.sessionId}`],
65
103
  };
66
104
  if (candidate.type === "lesson" && candidate.when_to_use) {
67
105
  fm.when_to_use = candidate.when_to_use;
68
106
  }
69
- const serialized = yamlStringify(fm).trimEnd();
70
- const content = assembleAssetFromString(serialized, candidate.body);
71
- return { ref, content };
107
+ const content = assembleAsset(fm, candidate.body);
108
+ return { ref, content, description };
72
109
  }
73
110
  /**
74
111
  * Process one session through the full pipeline: read → pre-filter → LLM →
@@ -78,7 +115,7 @@ function buildCandidateProposal(candidate, sourceRef) {
78
115
  * proposal validation failure) the session result records a warning and
79
116
  * keeps going — one session's bad luck never aborts a multi-session run.
80
117
  */
81
- async function processSession(harness, sessionRef, stashDir, config, llmConfig, chat, ctx, sourceRun, dryRun, timeoutMs, maxTotalChars) {
118
+ async function processSession(harness, sessionRef, stashDir, config, llmConfig, chat, ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, sessionIndexing) {
82
119
  const warnings = [];
83
120
  let data;
84
121
  try {
@@ -100,6 +137,30 @@ async function processSession(harness, sessionRef, stashDir, config, llmConfig,
100
137
  ...(typeof maxTotalChars === "number" ? { maxTotalChars } : {}),
101
138
  });
102
139
  const prompt = buildExtractPrompt({ data, events: filtered.events, inlineRefs: data.inlineRefs });
140
+ // #561 — ADDITIVE session indexing. Generate + write the session asset
141
+ // (`sessions/<harness>/<id>.md`). FAIL-OPEN: any failure only records a
142
+ // warning; it NEVER changes the proposal/skip outcome of extract. Returns the
143
+ // frontmatter fields to merge into the per-session result for state-db
144
+ // correlation. When disabled this closure makes NO LLM call and writes NOTHING.
145
+ const maybeWriteSessionAsset = async () => {
146
+ if (!sessionIndexing.enabled || dryRun)
147
+ return {};
148
+ if (!sessionMeetsDurationGate(data, sessionIndexing.minDurationMinutes))
149
+ return {};
150
+ try {
151
+ const result = await writeSessionAsset(data, stashDir, sessionIndexing.generate);
152
+ if (result.written) {
153
+ return {
154
+ ...(result.ref ? { sessionAssetRef: result.ref } : {}),
155
+ ...(result.logPath ? { sessionLogPath: result.logPath } : {}),
156
+ };
157
+ }
158
+ }
159
+ catch (err) {
160
+ warnings.push(`session asset write failed: ${err instanceof Error ? err.message : String(err)}`);
161
+ }
162
+ return {};
163
+ };
103
164
  let llmRaw = "";
104
165
  const llmResult = await tryLlmFeature("session_extraction", config, async () => {
105
166
  llmRaw = await chat(llmConfig, [{ role: "user", content: prompt }], {
@@ -141,6 +202,7 @@ async function processSession(harness, sessionRef, stashDir, config, llmConfig,
141
202
  preFilterOutput: filtered.stats.outputCount,
142
203
  },
143
204
  }, ctx);
205
+ const sessionAsset = await maybeWriteSessionAsset();
144
206
  return {
145
207
  sessionId: sessionRef.sessionId,
146
208
  harness: harness.name,
@@ -153,6 +215,7 @@ async function processSession(harness, sessionRef, stashDir, config, llmConfig,
153
215
  truncatedCount: filtered.stats.truncatedCount,
154
216
  },
155
217
  warnings,
218
+ ...sessionAsset,
156
219
  };
157
220
  }
158
221
  for (const candidate of payload.candidates) {
@@ -161,7 +224,7 @@ async function processSession(harness, sessionRef, stashDir, config, llmConfig,
161
224
  continue;
162
225
  }
163
226
  try {
164
- const { ref, content } = buildCandidateProposal(candidate, sessionRef);
227
+ const { ref, content, description } = buildCandidateProposal(candidate, sessionRef);
165
228
  const result = createProposal(stashDir, {
166
229
  ref,
167
230
  source: "extract",
@@ -169,7 +232,7 @@ async function processSession(harness, sessionRef, stashDir, config, llmConfig,
169
232
  payload: {
170
233
  content,
171
234
  frontmatter: {
172
- description: candidate.description,
235
+ description,
173
236
  ...(candidate.when_to_use ? { when_to_use: candidate.when_to_use } : {}),
174
237
  ...(typeof candidate.confidence === "number" ? { confidence: candidate.confidence } : {}),
175
238
  sources: [`session:${sessionRef.harness}:${sessionRef.sessionId}`],
@@ -202,6 +265,7 @@ async function processSession(harness, sessionRef, stashDir, config, llmConfig,
202
265
  preFilterOutput: filtered.stats.outputCount,
203
266
  },
204
267
  }, ctx);
268
+ const sessionAsset = await maybeWriteSessionAsset();
205
269
  return {
206
270
  sessionId: sessionRef.sessionId,
207
271
  harness: harness.name,
@@ -213,6 +277,7 @@ async function processSession(harness, sessionRef, stashDir, config, llmConfig,
213
277
  truncatedCount: filtered.stats.truncatedCount,
214
278
  },
215
279
  warnings,
280
+ ...sessionAsset,
216
281
  };
217
282
  }
218
283
  // ── Public entrypoint ────────────────────────────────────────────────────────
@@ -257,7 +322,7 @@ export async function akmExtract(options) {
257
322
  let llmConfig;
258
323
  const runnerSpec = resolveImproveProcessRunnerFromProfile(extractProcess, config);
259
324
  if (runnerSpec) {
260
- if (runnerSpec.kind !== "llm") {
325
+ if (!runnerIsLlm(runnerSpec)) {
261
326
  throw new ConfigError(`Extract only supports mode: "llm" (in-tree LLM call). Got mode: "${runnerSpec.kind}" from profiles.improve.default.processes.extract — change it to "llm" or remove the override.`, "INVALID_CONFIG_FILE");
262
327
  }
263
328
  llmConfig = runnerSpec.connection;
@@ -276,6 +341,35 @@ export async function akmExtract(options) {
276
341
  const maxTotalChars = typeof extractProcess?.maxTotalChars === "number" ? extractProcess.maxTotalChars : undefined;
277
342
  // Default discovery window — process config can override the built-in 24h.
278
343
  const effectiveSince = options.since ?? extractProcess?.defaultSince;
344
+ // #561 — resolve session-indexing config. Default ON: we only reach this code
345
+ // when `session_extraction` is enabled AND an LLM is configured (both checked
346
+ // above), so defaulting on costs nothing offline (the summary call fails open)
347
+ // while making sessions searchable in the common LLM-configured case. Set
348
+ // `processes.extract.indexSessions: false` for byte-identical legacy behaviour.
349
+ const sessionIndexingEnabled = extractProcess?.indexSessions ?? true;
350
+ const minSessionDuration = typeof extractProcess?.minSessionDuration === "number"
351
+ ? extractProcess.minSessionDuration
352
+ : DEFAULT_MIN_SESSION_DURATION_MINUTES;
353
+ // Production summary generator: a bounded in-tree LLM call wrapped in the same
354
+ // fail-open `tryLlmFeature` seam as the rest of extract. Returns `undefined`
355
+ // on disablement / timeout / error so no asset is written. Tests inject a fake.
356
+ const chatForSummary = options.chat ?? chatCompletion;
357
+ const defaultSessionSummaryGenerator = async (data) => {
358
+ let raw = "";
359
+ await tryLlmFeature("session_extraction", config, async () => {
360
+ raw = await chatForSummary(llmConfig, [{ role: "user", content: buildSessionSummaryPrompt(data) }], {
361
+ timeoutMs,
362
+ responseSchema: SESSION_SUMMARY_JSON_SCHEMA,
363
+ });
364
+ return raw;
365
+ }, "", { timeoutMs });
366
+ return parseSessionSummary(raw);
367
+ };
368
+ const sessionIndexing = {
369
+ enabled: sessionIndexingEnabled,
370
+ minDurationMinutes: minSessionDuration,
371
+ generate: options.generateSessionSummary ?? defaultSessionSummaryGenerator,
372
+ };
279
373
  const harness = resolveHarness(options.type, options.harnesses);
280
374
  if (!harness) {
281
375
  return {
@@ -355,7 +449,7 @@ export async function akmExtract(options) {
355
449
  let seenMap = new Map();
356
450
  if (trackingEnabled && candidates.length > 0) {
357
451
  try {
358
- stateDb = options.stateDb ?? openStateDatabase();
452
+ stateDb = options.stateDb ?? openStateDatabase(options.stateDbPath);
359
453
  seenMap = getExtractedSessionsMap(stateDb, harness.name, candidates.map((c) => c.sessionId));
360
454
  }
361
455
  catch (err) {
@@ -389,7 +483,7 @@ export async function akmExtract(options) {
389
483
  continue;
390
484
  }
391
485
  try {
392
- const result = await processSession(harness, summary, stashDir, config, llmConfig, chat, options.ctx, sourceRun, dryRun, timeoutMs, maxTotalChars);
486
+ const result = await processSession(harness, summary, stashDir, config, llmConfig, chat, options.ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, sessionIndexing);
393
487
  sessions.push(result);
394
488
  if (result.skipped)
395
489
  skippedCount += 1;
@@ -423,6 +517,11 @@ export async function akmExtract(options) {
423
517
  preFilterOutputCount: result.preFilter.outputCount,
424
518
  preFilterTruncatedCount: result.preFilter.truncatedCount,
425
519
  ...(result.skipReason ? { skipReason: result.skipReason } : {}),
520
+ // #561 — record the session's log_path for correlation across
521
+ // index rebuilds (the session asset frontmatter is the primary
522
+ // durable key; this is the state-db mirror of it).
523
+ ...(result.sessionLogPath ? { logPath: result.sessionLogPath } : {}),
524
+ ...(result.sessionAssetRef ? { sessionAssetRef: result.sessionAssetRef } : {}),
426
525
  },
427
526
  });
428
527
  }
@@ -475,3 +574,63 @@ export async function akmExtract(options) {
475
574
  durationMs: Date.now() - startMs,
476
575
  };
477
576
  }
577
+ /**
578
+ * Count NEW (unseen, in-window) extract candidate sessions across all available
579
+ * harnesses WITHOUT making any LLM calls. Mirrors the discovery + seen-filter
580
+ * logic in {@link akmExtract} so the `#554 minNewSessions` gate in `improve`
581
+ * can decide whether the extract pass is worth running before any work begins.
582
+ *
583
+ * A session is a "new candidate" when it is in the `since` window AND it would
584
+ * not be skipped by {@link shouldSkipAlreadyExtractedSession} (i.e. it has never
585
+ * been extracted, or new events have arrived since it was last extracted).
586
+ */
587
+ export function countNewExtractCandidates(config, options = {}) {
588
+ const extractProcess = config.profiles?.improve?.default?.processes?.extract;
589
+ const effectiveSince = options.since ?? extractProcess?.defaultSince;
590
+ const sinceMs = parseSinceArg(effectiveSince);
591
+ const harnesses = (options.harnesses ?? getAvailableHarnesses()).filter((h) => h.isAvailable());
592
+ let stateDb = options.stateDb;
593
+ let openedStateDb = false;
594
+ let total = 0;
595
+ try {
596
+ for (const harness of harnesses) {
597
+ const candidates = harness.listSessions({ sinceMs });
598
+ if (candidates.length === 0)
599
+ continue;
600
+ let seenMap = new Map();
601
+ try {
602
+ if (!stateDb) {
603
+ stateDb = openStateDatabase(options.stateDbPath);
604
+ openedStateDb = true;
605
+ }
606
+ seenMap = getExtractedSessionsMap(stateDb, harness.name, candidates.map((c) => c.sessionId));
607
+ }
608
+ catch (err) {
609
+ // state.db unavailable — treat every in-window session as a new
610
+ // candidate (fail-open: never let a transient sqlite error wrongly
611
+ // trip the gate and skip a pass that should have run).
612
+ const msg = err instanceof Error ? err.message : String(err);
613
+ warn(`[extract] state.db unavailable while counting candidates, treating all as new: ${msg}`);
614
+ total += candidates.length;
615
+ continue;
616
+ }
617
+ for (const summary of candidates) {
618
+ const prior = seenMap.get(summary.sessionId);
619
+ if (shouldSkipAlreadyExtractedSession(prior, summary.endedAt))
620
+ continue;
621
+ total += 1;
622
+ }
623
+ }
624
+ }
625
+ finally {
626
+ if (stateDb && openedStateDb) {
627
+ try {
628
+ stateDb.close();
629
+ }
630
+ catch {
631
+ // best-effort close
632
+ }
633
+ }
634
+ }
635
+ return total;
636
+ }
@@ -1,10 +1,10 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- import { loadConfig } from "../core/config";
5
- import { appendEvent } from "../core/events";
6
- import { promoteProposal } from "../core/proposals";
7
- import { info, warn } from "../core/warn";
4
+ import { loadConfig } from "../../core/config/config.js";
5
+ import { appendEvent } from "../../core/events.js";
6
+ import { info, warn } from "../../core/warn.js";
7
+ import { promoteProposal } from "../proposal/validators/proposals.js";
8
8
  // ---------------------------------------------------------------------------
9
9
  // Gate implementation
10
10
  // ---------------------------------------------------------------------------
@@ -3,16 +3,16 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import path from "node:path";
5
5
  import { defineCommand } from "citty";
6
- import { getStringArg, parseAutoAcceptFlag, parseNonNegativeIntFlag, parsePositiveIntFlag } from "../cli/parse-args";
7
- import { output, runWithJsonErrors } from "../cli/shared";
8
- import { loadConfig } from "../core/config";
9
- import { UsageError } from "../core/errors";
10
- import { getCacheDir } from "../core/paths";
11
- import { clearLogFile, setLogFile } from "../core/warn";
12
- import { resolveSourceEntries } from "../indexer/search-source";
13
- import { getHyphenatedArg, getHyphenatedBoolean, parseFlagValue } from "../output/context";
14
- import { akmImprove } from "./improve";
15
- import { buildImproveRunId, recordTerminatedImproveRun, relativeImproveResultPath, writeImproveResultFile, } from "./improve-result-file";
6
+ import { getStringArg, parseAutoAcceptFlag, parseNonNegativeIntFlag, parsePositiveIntFlag } from "../../cli/parse-args.js";
7
+ import { output, runWithJsonErrors } from "../../cli/shared.js";
8
+ import { loadConfig } from "../../core/config/config.js";
9
+ import { UsageError } from "../../core/errors.js";
10
+ import { getCacheDir } from "../../core/paths.js";
11
+ import { clearLogFile, setLogFile } from "../../core/warn.js";
12
+ import { resolveSourceEntries } from "../../indexer/search/search-source.js";
13
+ import { getHyphenatedArg, getHyphenatedBoolean, parseFlagValue } from "../../output/context.js";
14
+ import { akmImprove } from "./improve.js";
15
+ import { buildImproveRunId, recordTerminatedImproveRun, relativeImproveResultPath, writeImproveResultFile, } from "./improve-result-file.js";
16
16
  export const improveCommand = defineCommand({
17
17
  meta: {
18
18
  name: "improve",
@@ -138,20 +138,41 @@ export const improveCommand = defineCommand({
138
138
  process.stderr.write(`warning: failed to persist terminated improve run ${runId}: ${err instanceof Error ? err.message : String(err)}\n`);
139
139
  }
140
140
  };
141
- const sigtermHandler = () => {
142
- persistTerminated("SIGTERM");
143
- process.stderr.write(`[improve] received SIGTERM; recorded terminated run ${runId}\n`);
144
- process.exit(143);
141
+ // M5 (code-health round 2): signal -> {exit code, reason, ack message}
142
+ // as an explicit table instead of three near-identical handlers. The
143
+ // persist of the terminated-run row MUST complete before process.exit so
144
+ // a SIGTERM'd run (e.g. cron timeout) always leaves a row in
145
+ // improve_runs. recordTerminatedImproveRun is fully synchronous
146
+ // (bun:sqlite writes are sync), so the in-line call below blocks until
147
+ // the row is flushed before we exit.
148
+ const SIGNAL_TABLE = {
149
+ SIGTERM: { code: 143, reason: "SIGTERM", ack: true },
150
+ SIGINT: { code: 130, reason: "SIGINT", ack: true },
151
+ SIGHUP: { code: 129, reason: "SIGHUP", ack: false },
145
152
  };
146
- const sigintHandler = () => {
147
- persistTerminated("SIGINT");
148
- process.stderr.write(`[improve] received SIGINT; recorded terminated run ${runId}\n`);
149
- process.exit(130);
150
- };
151
- const sighupHandler = () => {
152
- persistTerminated("SIGHUP");
153
- process.exit(129);
153
+ const makeSignalHandler = (sig) => () => {
154
+ const { code, reason, ack } = SIGNAL_TABLE[sig];
155
+ // Hard-exit fallback: if the synchronous persist ever hangs (e.g. a
156
+ // stuck sqlite lock under contention), the watchdog still exits with
157
+ // the correct code instead of leaving a zombie process. .unref() keeps
158
+ // the timer from holding the loop open on the normal (fast) path.
159
+ const watchdog = setTimeout(() => process.exit(code), 2000);
160
+ if (typeof watchdog.unref === "function")
161
+ watchdog.unref();
162
+ try {
163
+ persistTerminated(reason);
164
+ }
165
+ finally {
166
+ clearTimeout(watchdog);
167
+ }
168
+ if (ack) {
169
+ process.stderr.write(`[improve] received ${sig}; recorded terminated run ${runId}\n`);
170
+ }
171
+ process.exit(code);
154
172
  };
173
+ const sigtermHandler = makeSignalHandler("SIGTERM");
174
+ const sigintHandler = makeSignalHandler("SIGINT");
175
+ const sighupHandler = makeSignalHandler("SIGHUP");
155
176
  process.once("SIGTERM", sigtermHandler);
156
177
  process.once("SIGINT", sigintHandler);
157
178
  process.once("SIGHUP", sighupHandler);
@@ -163,6 +184,7 @@ export const improveCommand = defineCommand({
163
184
  dryRun,
164
185
  target: targetArg,
165
186
  autoAccept,
187
+ ...(runId !== undefined ? { runId } : {}),
166
188
  ...(limitRaw !== undefined ? { limit: limitRaw } : {}),
167
189
  ...(timeoutMs !== undefined ? { timeoutMs } : {}),
168
190
  ...(minRetrievalCount !== undefined ? { minRetrievalCount } : {}),
@@ -217,7 +239,7 @@ export const improveCommand = defineCommand({
217
239
  runRecorded = true; // Suppress any late signal-handler write — the success path owns the row now.
218
240
  if (primaryStashDir) {
219
241
  try {
220
- writeImproveResultFile(primaryStashDir, runId, improveResult);
242
+ writeImproveResultFile(primaryStashDir, runId, improveResult, startedAtIso);
221
243
  }
222
244
  catch (err) {
223
245
  // Stderr warning on the failure path is preferable to crashing
@@ -1,13 +1,16 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- import profileDefault from "../assets/profiles/default.json" with { type: "json" };
5
- import profileGraphRefresh from "../assets/profiles/graph-refresh.json" with { type: "json" };
6
- import profileMemoryFocus from "../assets/profiles/memory-focus.json" with { type: "json" };
7
- import profileQuick from "../assets/profiles/quick.json" with { type: "json" };
8
- import profileThorough from "../assets/profiles/thorough.json" with { type: "json" };
9
- import { parseAssetRef } from "../core/asset-ref";
10
- import { warn } from "../core/warn";
4
+ import profileCatchup from "../../assets/profiles/catchup.json" with { type: "json" };
5
+ import profileConsolidate from "../../assets/profiles/consolidate.json" with { type: "json" };
6
+ import profileDefault from "../../assets/profiles/default.json" with { type: "json" };
7
+ import profileFrequent from "../../assets/profiles/frequent.json" with { type: "json" };
8
+ import profileGraphRefresh from "../../assets/profiles/graph-refresh.json" with { type: "json" };
9
+ import profileMemoryFocus from "../../assets/profiles/memory-focus.json" with { type: "json" };
10
+ import profileQuick from "../../assets/profiles/quick.json" with { type: "json" };
11
+ import profileThorough from "../../assets/profiles/thorough.json" with { type: "json" };
12
+ import { parseAssetRef } from "../../core/asset/asset-ref.js";
13
+ import { warn } from "../../core/warn.js";
11
14
  /** Profile name used as the final fallback when nothing else resolves. */
12
15
  const FALLBACK_PROFILE_NAME = "default";
13
16
  // Built-in default allowed types per process
@@ -25,6 +28,9 @@ const BUILTIN_PROFILES = {
25
28
  thorough: profileThorough,
26
29
  "memory-focus": profileMemoryFocus,
27
30
  "graph-refresh": profileGraphRefresh,
31
+ frequent: profileFrequent,
32
+ consolidate: profileConsolidate,
33
+ catchup: profileCatchup,
28
34
  };
29
35
  /**
30
36
  * Default enabled-state for known improve processes when neither the user
@@ -27,7 +27,7 @@
27
27
  */
28
28
  import crypto from "node:crypto";
29
29
  import path from "node:path";
30
- import { openStateDatabase, recordImproveRun } from "../core/state-db";
30
+ import { openStateDatabase, recordImproveRun } from "../../core/state-db.js";
31
31
  /**
32
32
  * Build a stable run-id for a single improve invocation.
33
33
  *
@@ -73,14 +73,19 @@ export function relativeImproveResultPath(runId) {
73
73
  * (closes the dry-run/real-run artifact-trap recorded in MEMORY.md
74
74
  * `feedback_akm_dryrun_artifact_trap`).
75
75
  */
76
- export function writeImproveResultFile(stashDir, runId, result) {
76
+ export function writeImproveResultFile(stashDir, runId, result, startedAt) {
77
77
  const db = openStateDatabase();
78
78
  try {
79
- const startedAt = new Date().toISOString();
79
+ const completedAt = new Date().toISOString();
80
+ // startedAt is the ISO timestamp captured at process launch (passed from the
81
+ // CLI entry point). If omitted, fall back to the run-id's embedded timestamp
82
+ // so started_at != completed_at even on older call sites.
83
+ const resolvedStartedAt = startedAt ??
84
+ runId.slice(0, 24).replace(/^(\d{4}-\d{2}-\d{2}T)(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/, "$1$2:$3:$4.$5Z");
80
85
  recordImproveRun(db, {
81
86
  id: runId,
82
- startedAt,
83
- completedAt: startedAt,
87
+ startedAt: resolvedStartedAt,
88
+ completedAt,
84
89
  stashDir,
85
90
  dryRun: Boolean(result.dryRun),
86
91
  profile: null,