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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (598) hide show
  1. package/CHANGELOG.md +1283 -22
  2. package/README.md +62 -37
  3. package/SECURITY.md +46 -31
  4. package/dist/akm +162 -38
  5. package/dist/akm-migrate +44 -0
  6. package/dist/assets/backends/schtasks-template.xml +2 -1
  7. package/dist/assets/hints/cli-hints-full.md +268 -118
  8. package/dist/assets/hints/cli-hints-short.md +87 -24
  9. package/dist/assets/{profiles → improve-strategies}/catchup.json +3 -1
  10. package/dist/assets/{profiles → improve-strategies}/consolidate.json +3 -1
  11. package/dist/assets/{profiles → improve-strategies}/default.json +6 -7
  12. package/dist/assets/improve-strategies/frequent.json +15 -0
  13. package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +4 -2
  14. package/dist/assets/{profiles → improve-strategies}/memory-focus.json +4 -1
  15. package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +5 -5
  16. package/dist/assets/{profiles → improve-strategies}/quick.json +4 -2
  17. package/dist/assets/improve-strategies/reflect-distill.json +30 -0
  18. package/dist/assets/{profiles → improve-strategies}/thorough.json +1 -1
  19. package/dist/assets/prompts/consolidate-system.md +5 -5
  20. package/dist/assets/prompts/extract-session.md +2 -6
  21. package/dist/assets/prompts/memory-infer-user.md +2 -3
  22. package/dist/assets/prompts/reflect-llm-framed-contract.md +11 -0
  23. package/dist/assets/prompts/reflect-llm-schema-contract.md +3 -0
  24. package/dist/assets/prompts/reflect-output-repair.md +3 -0
  25. package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
  26. package/dist/assets/stash-skeleton/README.md +38 -10
  27. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +8 -0
  28. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +8 -0
  29. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +14 -1
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +13 -1
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +9 -1
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +11 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +9 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +9 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +8 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +100 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/domains.md +64 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/organization.md +136 -0
  39. package/dist/assets/tasks/core/extract.yml +3 -2
  40. package/dist/assets/tasks/core/improve.yml +2 -1
  41. package/dist/assets/tasks/core/index-refresh.yml +1 -0
  42. package/dist/assets/tasks/core/sync.yml +1 -0
  43. package/dist/assets/tasks/core/version-check.yml +2 -1
  44. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
  45. package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
  46. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
  47. package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
  48. package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
  49. package/dist/assets/templates/html/health.html +5 -4
  50. package/dist/assets/workflows/workflow-template.md +31 -15
  51. package/dist/cli/invocation.js +279 -0
  52. package/dist/cli/parse-args.js +5 -90
  53. package/dist/cli/retired-commands.js +78 -0
  54. package/dist/cli/shared.js +158 -48
  55. package/dist/cli-node.mjs +2 -1
  56. package/dist/cli.js +747 -293
  57. package/dist/commands/agent/agent-dispatch.js +19 -18
  58. package/dist/commands/agent/agent-support.js +0 -24
  59. package/dist/commands/agent/contribute-cli.js +43 -97
  60. package/dist/commands/completions.js +80 -23
  61. package/dist/commands/config-cli.js +44 -281
  62. package/dist/commands/env/env-binding.js +99 -0
  63. package/dist/commands/env/env-cli.js +84 -224
  64. package/dist/commands/env/env.js +12 -163
  65. package/dist/commands/env/marker-path.js +6 -0
  66. package/dist/commands/env/secret-cli.js +45 -61
  67. package/dist/commands/env/secret.js +32 -62
  68. package/dist/commands/feedback-cli.js +179 -85
  69. package/dist/commands/health/accept-rate.js +58 -0
  70. package/dist/commands/health/advisories.js +7 -8
  71. package/dist/commands/health/checks.js +279 -94
  72. package/dist/commands/health/html-report.js +197 -578
  73. package/dist/commands/health/improve-metrics.js +277 -246
  74. package/dist/commands/health/llm-usage.js +19 -19
  75. package/dist/commands/health/md-report.js +16 -7
  76. package/dist/commands/health/metrics.js +67 -32
  77. package/dist/commands/health/renderers.js +47 -0
  78. package/dist/commands/health/report-view-model.js +508 -0
  79. package/dist/commands/health/stash-exposure.js +1 -1
  80. package/dist/commands/health/surfaces.js +16 -56
  81. package/dist/commands/health/task-runs.js +3 -67
  82. package/dist/{migrate-storage-node.mjs → commands/health/types-checks.js} +1 -5
  83. package/dist/commands/health/types-improve.js +29 -0
  84. package/dist/{output/text/save.js → commands/health/types-metrics.js} +1 -2
  85. package/dist/commands/health/types-result.js +7 -0
  86. package/dist/commands/health/types-runs.js +4 -0
  87. package/dist/commands/health/types-session-log.js +4 -0
  88. package/dist/commands/health/types-windows.js +4 -0
  89. package/dist/commands/health/types.js +26 -21
  90. package/dist/commands/health/windows.js +2 -3
  91. package/dist/commands/health.js +296 -167
  92. package/dist/commands/improve/anti-collapse.js +5 -5
  93. package/dist/commands/improve/autonomy-gate.js +68 -0
  94. package/dist/commands/improve/collapse-detector.js +65 -52
  95. package/dist/commands/improve/consolidate/chunking.js +9 -7
  96. package/dist/commands/improve/consolidate/eligibility.js +1 -23
  97. package/dist/commands/improve/consolidate/merge.js +4 -0
  98. package/dist/commands/improve/consolidate.js +454 -1354
  99. package/dist/commands/improve/content-hash.js +39 -0
  100. package/dist/commands/improve/distill/content-repair.js +4 -10
  101. package/dist/commands/improve/distill/promote-memory.js +89 -64
  102. package/dist/commands/improve/distill/quality-gate.js +118 -42
  103. package/dist/commands/improve/distill-guards.js +1 -1
  104. package/dist/commands/improve/distill-promotion-policy.js +33 -888
  105. package/dist/commands/improve/distill.js +607 -363
  106. package/dist/commands/improve/eligibility.js +165 -79
  107. package/dist/commands/improve/extract-cli.js +35 -126
  108. package/dist/commands/improve/extract-prompt.js +6 -35
  109. package/dist/commands/improve/extract.js +640 -391
  110. package/dist/commands/improve/feedback-valence.js +2 -12
  111. package/dist/commands/improve/improve-cli.js +134 -135
  112. package/dist/commands/improve/improve-result-file.js +30 -50
  113. package/dist/commands/improve/improve-run-types.js +4 -0
  114. package/dist/commands/improve/improve-strategies.js +135 -0
  115. package/dist/commands/improve/improve.js +904 -701
  116. package/dist/commands/improve/locks.js +64 -111
  117. package/dist/commands/improve/loop-stages.js +1110 -923
  118. package/dist/commands/improve/memory/derived-ref.js +124 -0
  119. package/dist/commands/improve/memory/memory-belief.js +79 -7
  120. package/dist/commands/improve/memory/memory-contradiction-detect.js +49 -52
  121. package/dist/commands/improve/memory/memory-improve.js +25 -37
  122. package/dist/commands/improve/outcome-loop.js +25 -88
  123. package/dist/commands/improve/preparation.js +1034 -813
  124. package/dist/commands/improve/proactive-maintenance.js +34 -9
  125. package/dist/commands/improve/proposal-envelope.js +31 -0
  126. package/dist/commands/improve/reflect.js +983 -794
  127. package/dist/commands/improve/run-context.js +119 -0
  128. package/dist/commands/improve/salience.js +24 -127
  129. package/dist/commands/improve/session-asset.js +7 -3
  130. package/dist/commands/improve/shared.js +14 -34
  131. package/dist/commands/improve/source-identity.js +28 -0
  132. package/dist/commands/improve/triage.js +20 -17
  133. package/dist/commands/lint/base-linter.js +340 -313
  134. package/dist/commands/lint/env-key-rules.js +31 -47
  135. package/dist/commands/lint/index.js +185 -30
  136. package/dist/commands/{events.js → log.js} +28 -38
  137. package/dist/commands/migrate-cli.js +54 -0
  138. package/dist/commands/migration-tool.js +55 -0
  139. package/dist/commands/observability-cli.js +70 -208
  140. package/dist/commands/proposal/diff-format.js +50 -0
  141. package/dist/commands/proposal/drain-policies.js +0 -6
  142. package/dist/commands/proposal/drain.js +91 -40
  143. package/dist/commands/proposal/proposal-cli.js +134 -132
  144. package/dist/commands/proposal/proposal-types.js +56 -0
  145. package/dist/commands/proposal/proposal.js +83 -65
  146. package/dist/commands/proposal/propose-cli.js +88 -0
  147. package/dist/commands/proposal/propose.js +105 -88
  148. package/dist/commands/proposal/repository.js +1303 -278
  149. package/dist/commands/proposal/validators/proposal-quality-validators.js +16 -6
  150. package/dist/commands/proposal/validators/proposal-validators.js +61 -12
  151. package/dist/commands/proposal/validators/proposals.js +6 -8
  152. package/dist/commands/read/curate.js +78 -73
  153. package/dist/commands/read/knowledge.js +510 -13
  154. package/dist/commands/read/registry-search.js +2 -2
  155. package/dist/commands/read/remember-cli.js +84 -15
  156. package/dist/commands/read/search-cli.js +203 -96
  157. package/dist/commands/read/search.js +126 -94
  158. package/dist/commands/read/show.js +226 -250
  159. package/dist/commands/registry-cli.js +34 -60
  160. package/dist/commands/remember.js +18 -57
  161. package/dist/commands/sources/add-cli.js +104 -49
  162. package/dist/commands/sources/bundle-cli.js +166 -0
  163. package/dist/commands/sources/bundle-config-ops.js +63 -0
  164. package/dist/commands/sources/info.js +27 -15
  165. package/dist/commands/sources/init.js +30 -40
  166. package/dist/commands/sources/installed-stashes.js +469 -172
  167. package/dist/commands/sources/migration-help.js +7 -4
  168. package/dist/commands/sources/schema-repair.js +10 -9
  169. package/dist/commands/sources/self-update.js +182 -121
  170. package/dist/commands/sources/source-add.js +169 -178
  171. package/dist/commands/sources/source-clone.js +144 -41
  172. package/dist/commands/sources/source-manage.js +94 -59
  173. package/dist/commands/sources/sources-cli.js +64 -205
  174. package/dist/commands/sources/stash-cli.js +91 -54
  175. package/dist/commands/sources/stash-skeleton.js +1 -1
  176. package/dist/commands/tasks/tasks-cli.js +106 -104
  177. package/dist/commands/tasks/tasks.js +445 -262
  178. package/dist/commands/workflow-cli.js +232 -121
  179. package/dist/core/action-contributors.js +1 -1
  180. package/dist/core/activation-policy.js +49 -0
  181. package/dist/core/adapter/adapters/agent-skills-adapter.js +181 -0
  182. package/dist/core/adapter/adapters/akm-adapter.js +528 -0
  183. package/dist/core/adapter/adapters/akm-lint.js +392 -0
  184. package/dist/core/adapter/adapters/akm-metadata.js +387 -0
  185. package/dist/core/adapter/adapters/akm-task-adapter.js +149 -0
  186. package/dist/core/adapter/adapters/akm-workflow-adapter.js +180 -0
  187. package/dist/core/adapter/adapters/claude-adapter.js +61 -0
  188. package/dist/core/adapter/adapters/dotenv-adapter.js +187 -0
  189. package/dist/core/adapter/adapters/generic-files-adapter.js +119 -0
  190. package/dist/core/adapter/adapters/index.js +80 -0
  191. package/dist/core/adapter/adapters/llm-wiki-adapter.js +419 -0
  192. package/dist/core/adapter/adapters/okf-adapter.js +391 -0
  193. package/dist/core/adapter/adapters/opencode-adapter.js +68 -0
  194. package/dist/core/adapter/adapters/shared.js +286 -0
  195. package/dist/core/adapter/adapters/tool-dir-shared.js +217 -0
  196. package/dist/core/adapter/adapters/website-snapshot-adapter.js +155 -0
  197. package/dist/core/adapter/bundle-adapter.js +4 -0
  198. package/dist/core/adapter/detect-adapter.js +17 -0
  199. package/dist/core/adapter/recognize-match.js +44 -0
  200. package/dist/core/adapter/registry.js +56 -0
  201. package/dist/core/adapter/types.js +4 -0
  202. package/dist/core/asset/akm-markdown.js +30 -0
  203. package/dist/core/asset/asset-placement.js +243 -0
  204. package/dist/core/asset/asset-ref.js +110 -79
  205. package/dist/core/asset/asset-serialize.js +20 -0
  206. package/dist/core/asset/frontmatter.js +28 -12
  207. package/dist/core/asset/markdown.js +40 -51
  208. package/dist/core/asset/resolve-ref.js +274 -0
  209. package/dist/core/asset/stash-meta.js +2 -2
  210. package/dist/core/bundle-id.js +51 -0
  211. package/dist/core/common.js +281 -86
  212. package/dist/core/config/config-io.js +42 -128
  213. package/dist/core/config/config-schema.js +233 -834
  214. package/dist/core/config/config-sources.js +162 -39
  215. package/dist/core/config/config-types.js +16 -11
  216. package/dist/core/config/config-version.js +29 -0
  217. package/dist/core/config/config-walker.js +126 -37
  218. package/dist/core/config/config.js +154 -331
  219. package/dist/core/config/deep-merge.js +41 -0
  220. package/dist/core/config/engine-semantics.js +28 -0
  221. package/dist/core/config/experimental.js +21 -0
  222. package/dist/core/config/schema/embedding.js +38 -0
  223. package/dist/core/config/schema/engines.js +116 -0
  224. package/dist/core/config/schema/experimental.js +47 -0
  225. package/dist/core/config/schema/feedback.js +31 -0
  226. package/dist/core/config/schema/improve-processes.js +389 -0
  227. package/dist/core/config/schema/improve.js +94 -0
  228. package/dist/core/config/schema/index-config.js +176 -0
  229. package/dist/core/config/schema/output.js +18 -0
  230. package/dist/core/config/schema/primitives.js +94 -0
  231. package/dist/core/config/schema/search.js +30 -0
  232. package/dist/core/config/schema/setup.js +18 -0
  233. package/dist/core/config/schema/sources-bundles.js +169 -0
  234. package/dist/core/config/schema/workflow.js +29 -0
  235. package/dist/core/env-secret-ref.js +155 -20
  236. package/dist/core/errors.js +17 -15
  237. package/dist/core/events-types.js +4 -0
  238. package/dist/core/events.js +46 -128
  239. package/dist/core/extra-params.js +62 -0
  240. package/dist/core/file-change.js +17 -0
  241. package/dist/core/file-lock.js +202 -57
  242. package/dist/core/fs-txn.js +392 -0
  243. package/dist/core/git-message.js +59 -0
  244. package/dist/core/improve-result.js +167 -0
  245. package/dist/core/json-schema.js +142 -0
  246. package/dist/core/lesson-lint.js +1 -17
  247. package/dist/core/logs-db.js +1 -1
  248. package/dist/core/maintenance-barrier.js +135 -0
  249. package/dist/core/migration-operation.js +44 -0
  250. package/dist/core/mutation-target.js +78 -0
  251. package/dist/core/paths.js +22 -25
  252. package/dist/core/platform.js +10 -0
  253. package/dist/core/recognition-util.js +128 -0
  254. package/dist/core/redaction.js +392 -0
  255. package/dist/core/standards/resolve-standards-context.js +36 -65
  256. package/dist/core/standards/resolve-stash-standards.js +2 -2
  257. package/dist/core/standards/resolve-type-conventions.js +5 -5
  258. package/dist/core/state/migrations.js +242 -11
  259. package/dist/core/state-db.js +98 -10
  260. package/dist/core/structured.js +1 -1
  261. package/dist/core/subprocess.js +303 -0
  262. package/dist/core/text-truncation.js +9 -5
  263. package/dist/core/time.js +20 -0
  264. package/dist/core/type-presentation.js +130 -0
  265. package/dist/core/warn.js +0 -3
  266. package/dist/core/write-source.js +834 -118
  267. package/dist/indexer/bundle-identity-guard.js +92 -0
  268. package/dist/indexer/db/graph-db.js +1 -25
  269. package/dist/indexer/db/llm-cache.js +1 -1
  270. package/dist/indexer/ensure-index.js +30 -9
  271. package/dist/indexer/graph/graph-boost.js +9 -30
  272. package/dist/indexer/graph/graph-extraction.js +41 -27
  273. package/dist/indexer/graph/graph-types.js +4 -0
  274. package/dist/indexer/index-writer-lock.js +93 -49
  275. package/dist/indexer/index-written-assets.js +100 -53
  276. package/dist/indexer/indexer.js +746 -329
  277. package/dist/indexer/init.js +18 -25
  278. package/dist/indexer/installations.js +142 -0
  279. package/dist/indexer/passes/dir-staleness.js +18 -10
  280. package/dist/indexer/passes/memory-inference.js +25 -15
  281. package/dist/indexer/passes/metadata.js +412 -243
  282. package/dist/indexer/scan/doc-to-entry.js +160 -0
  283. package/dist/indexer/scan/drain-dir.js +134 -0
  284. package/dist/indexer/search/db-search.js +292 -108
  285. package/dist/indexer/search/fts-query.js +64 -0
  286. package/dist/indexer/search/ranking-contributors.js +145 -25
  287. package/dist/indexer/search/ranking-types.js +4 -0
  288. package/dist/indexer/search/ranking.js +28 -71
  289. package/dist/indexer/search/search-attribution.js +67 -0
  290. package/dist/indexer/search/search-fields.js +18 -3
  291. package/dist/indexer/search/search-hit-enrichers.js +30 -40
  292. package/dist/indexer/search/search-source.js +157 -111
  293. package/dist/indexer/search/semantic-status.js +4 -1
  294. package/dist/indexer/usage/usage-events.js +10 -30
  295. package/dist/indexer/walk/file-context.js +3 -45
  296. package/dist/indexer/walk/matchers.js +42 -34
  297. package/dist/indexer/walk/path-resolver.js +11 -5
  298. package/dist/indexer/walk/walker.js +42 -14
  299. package/dist/integrations/agent/builder-shared.js +7 -0
  300. package/dist/integrations/agent/builders.js +5 -56
  301. package/dist/integrations/agent/config.js +3 -143
  302. package/dist/integrations/agent/detect.js +17 -2
  303. package/dist/integrations/agent/engine-resolution.js +231 -0
  304. package/dist/integrations/agent/index.js +1 -2
  305. package/dist/integrations/agent/model-aliases.js +16 -2
  306. package/dist/integrations/agent/profiles.js +36 -62
  307. package/dist/integrations/agent/prompts.js +46 -18
  308. package/dist/integrations/agent/runner-dispatch.js +93 -4
  309. package/dist/integrations/agent/runner.js +76 -208
  310. package/dist/integrations/agent/spawn.js +88 -196
  311. package/dist/integrations/harnesses/aider/agent-builder.js +114 -0
  312. package/dist/integrations/harnesses/aider/index.js +48 -0
  313. package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
  314. package/dist/integrations/harnesses/amazonq/agent-builder.js +147 -0
  315. package/dist/integrations/harnesses/amazonq/index.js +45 -0
  316. package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
  317. package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
  318. package/dist/integrations/harnesses/claude/config-import.js +1 -3
  319. package/dist/integrations/harnesses/claude/index.js +24 -35
  320. package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
  321. package/dist/integrations/harnesses/claude/session-log.js +27 -75
  322. package/dist/integrations/harnesses/codex/agent-builder.js +138 -0
  323. package/dist/integrations/harnesses/codex/index.js +52 -0
  324. package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
  325. package/dist/integrations/harnesses/copilot/agent-builder.js +122 -0
  326. package/dist/integrations/harnesses/copilot/index.js +48 -0
  327. package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
  328. package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
  329. package/dist/integrations/harnesses/gemini/index.js +48 -0
  330. package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
  331. package/dist/integrations/harnesses/ids.js +24 -0
  332. package/dist/integrations/harnesses/index.js +54 -34
  333. package/dist/integrations/harnesses/opencode/agent-builder.js +23 -5
  334. package/dist/integrations/harnesses/opencode/config-import.js +1 -3
  335. package/dist/integrations/harnesses/opencode/index.js +14 -32
  336. package/dist/integrations/harnesses/opencode/session-log.js +67 -125
  337. package/dist/integrations/harnesses/opencode-sdk/harness.js +51 -0
  338. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
  339. package/dist/integrations/harnesses/openhands/agent-builder.js +128 -0
  340. package/dist/integrations/harnesses/openhands/index.js +48 -0
  341. package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
  342. package/dist/integrations/harnesses/pi/agent-builder.js +97 -0
  343. package/dist/integrations/harnesses/pi/index.js +45 -0
  344. package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
  345. package/dist/integrations/harnesses/shared.js +17 -0
  346. package/dist/integrations/harnesses/types.js +43 -32
  347. package/dist/integrations/lockfile.js +211 -24
  348. package/dist/integrations/session-logs/index.js +36 -39
  349. package/dist/integrations/session-logs/provider-base.js +113 -0
  350. package/dist/llm/client.js +182 -110
  351. package/dist/llm/embedders/deterministic.js +2 -2
  352. package/dist/llm/embedders/remote.js +21 -9
  353. package/dist/llm/feature-gate.js +17 -57
  354. package/dist/llm/graph-extract.js +12 -13
  355. package/dist/llm/index-passes.js +8 -42
  356. package/dist/llm/memory-infer.js +144 -1
  357. package/dist/llm/metadata-enhance.js +45 -30
  358. package/dist/llm/structured-call.js +16 -8
  359. package/dist/llm/usage-persist.js +30 -5
  360. package/dist/llm/usage-telemetry.js +59 -6
  361. package/dist/output/cli-hints.js +1 -2
  362. package/dist/output/command-registry.js +27 -0
  363. package/dist/output/context.js +22 -7
  364. package/dist/output/format-exempt.js +80 -0
  365. package/dist/output/generic-render.js +251 -0
  366. package/dist/output/html-render.js +11 -16
  367. package/dist/output/render-registry.js +57 -0
  368. package/dist/output/renderers.js +14 -279
  369. package/dist/output/shapes/curate.js +10 -1
  370. package/dist/output/shapes/events.js +12 -7
  371. package/dist/output/shapes/helpers.js +58 -84
  372. package/dist/output/shapes/passthrough.js +11 -39
  373. package/dist/output/shapes/proposal/producer.js +15 -7
  374. package/dist/output/shapes/registry.js +12 -6
  375. package/dist/output/shapes.js +0 -9
  376. package/dist/output/text/{init.js → bundle-create.js} +3 -1
  377. package/dist/output/text/bundle-show.js +7 -0
  378. package/dist/output/text/command-format.js +562 -0
  379. package/dist/output/text/env.js +1 -3
  380. package/dist/output/text/events.js +8 -7
  381. package/dist/output/text/helpers.js +15 -1164
  382. package/dist/output/text/proposal/producer.js +4 -2
  383. package/dist/output/text/proposal-format.js +202 -0
  384. package/dist/output/text/registry-commands.js +1 -2
  385. package/dist/output/text/registry.js +12 -6
  386. package/dist/output/text/show-directives.js +117 -0
  387. package/dist/output/text/show-format.js +103 -0
  388. package/dist/output/text/sync.js +5 -0
  389. package/dist/output/text/workflow-format.js +332 -0
  390. package/dist/output/text/workflow.js +3 -2
  391. package/dist/output/text.js +10 -19
  392. package/dist/registry/factory.js +4 -6
  393. package/dist/registry/origin-resolve.js +16 -27
  394. package/dist/registry/providers/skills-sh.js +3 -3
  395. package/dist/registry/providers/static-index.js +15 -25
  396. package/dist/registry/resolve.js +43 -94
  397. package/dist/registry/semver.js +43 -0
  398. package/dist/runtime.js +81 -12
  399. package/dist/scripts/akm-migrate.js +35529 -0
  400. package/dist/setup/detect.js +5 -7
  401. package/dist/setup/detected-engines.js +136 -0
  402. package/dist/setup/engine-config.js +100 -0
  403. package/dist/setup/registry-stash-loader.js +3 -3
  404. package/dist/setup/semantic-assets.js +12 -9
  405. package/dist/setup/setup.js +444 -208
  406. package/dist/setup/steps/connection-shared.js +120 -0
  407. package/dist/setup/steps/connection.js +108 -305
  408. package/dist/setup/steps/platforms.js +13 -12
  409. package/dist/setup/steps/semantic.js +15 -3
  410. package/dist/setup/steps/sources.js +21 -15
  411. package/dist/setup/steps/stashdir.js +6 -4
  412. package/dist/setup/steps/tasks.js +236 -119
  413. package/dist/setup/steps.js +3 -2
  414. package/dist/sources/freshness.js +39 -0
  415. package/dist/sources/provider-factory.js +11 -17
  416. package/dist/sources/providers/filesystem.js +2 -3
  417. package/dist/sources/providers/git-install.js +278 -34
  418. package/dist/sources/providers/git-provider.js +54 -56
  419. package/dist/sources/providers/git-stash.js +420 -91
  420. package/dist/sources/providers/git.js +2 -2
  421. package/dist/sources/providers/npm.js +16 -19
  422. package/dist/sources/providers/provider-utils.js +47 -22
  423. package/dist/sources/providers/sync-from-ref.js +3 -9
  424. package/dist/sources/providers/website.js +2 -2
  425. package/dist/sources/resolve.js +11 -10
  426. package/dist/sources/snapshot-fetchers/types.js +4 -0
  427. package/dist/sources/{website-ingest.js → snapshot-fetchers/website-ingest.js} +110 -41
  428. package/dist/storage/database.js +60 -4
  429. package/dist/storage/engines/sqlite-migrations.js +156 -5
  430. package/dist/storage/locations.js +1 -2
  431. package/dist/storage/repositories/canaries-repository.js +1 -1
  432. package/dist/storage/repositories/events-repository.js +51 -11
  433. package/dist/storage/repositories/improve-runs-repository.js +6 -32
  434. package/dist/storage/repositories/index-connection.js +79 -0
  435. package/dist/storage/repositories/index-db.js +4 -3
  436. package/dist/storage/repositories/index-entries-repository.js +863 -0
  437. package/dist/{indexer/db/entry-mapper.js → storage/repositories/index-entry-mapper.js} +19 -2
  438. package/dist/storage/repositories/index-entry-types.js +4 -0
  439. package/dist/storage/repositories/index-fts-repository.js +167 -0
  440. package/dist/storage/repositories/index-llm-cache-repository.js +108 -0
  441. package/dist/storage/repositories/index-meta-repository.js +49 -0
  442. package/dist/{indexer/db/schema.js → storage/repositories/index-schema.js} +226 -100
  443. package/dist/storage/repositories/index-sql.js +12 -0
  444. package/dist/storage/repositories/index-utility-repository.js +356 -0
  445. package/dist/storage/repositories/index-vec-repository.js +250 -0
  446. package/dist/storage/repositories/outcome-repository.js +119 -0
  447. package/dist/storage/repositories/proposals-repository.js +317 -75
  448. package/dist/storage/repositories/registry-cache.js +1 -1
  449. package/dist/storage/repositories/salience-repository.js +172 -0
  450. package/dist/storage/repositories/task-history-repository.js +110 -3
  451. package/dist/storage/repositories/workflow-runs-repository.js +240 -19
  452. package/dist/tasks/backends/cron.js +169 -46
  453. package/dist/tasks/backends/exec-utils.js +76 -3
  454. package/dist/tasks/backends/index.js +6 -9
  455. package/dist/tasks/backends/launchd.js +292 -55
  456. package/dist/tasks/backends/schtasks.js +557 -70
  457. package/dist/tasks/backends/types.js +4 -0
  458. package/dist/tasks/command-executable.js +93 -0
  459. package/dist/tasks/embedded.js +56 -38
  460. package/dist/tasks/parser.js +156 -64
  461. package/dist/tasks/resolve-akm-bin.js +144 -51
  462. package/dist/tasks/runner.js +377 -209
  463. package/dist/tasks/schedule.js +108 -19
  464. package/dist/tasks/scheduler-invocation.js +296 -0
  465. package/dist/tasks/schema.js +1 -1
  466. package/dist/tasks/task-id.js +35 -0
  467. package/dist/tasks/validator.js +30 -16
  468. package/dist/text-import-hook.mjs +1 -1
  469. package/dist/workflows/authoring/authoring.js +104 -43
  470. package/dist/workflows/authoring/scope-key.js +1 -1
  471. package/dist/workflows/cli.js +0 -16
  472. package/dist/workflows/concurrency-policy.js +15 -0
  473. package/dist/workflows/exec/brief.js +450 -0
  474. package/dist/workflows/exec/frozen-judge.js +47 -0
  475. package/dist/workflows/exec/native-executor.js +1038 -0
  476. package/dist/workflows/exec/param-secrets.js +115 -0
  477. package/dist/workflows/exec/report.js +1460 -0
  478. package/dist/workflows/exec/run-workflow.js +602 -0
  479. package/dist/workflows/exec/scheduler.js +71 -0
  480. package/dist/workflows/exec/step-work.js +1190 -0
  481. package/dist/workflows/exec/unit-writer.js +23 -0
  482. package/dist/workflows/exec/workflow-engine-gate.js +67 -0
  483. package/dist/workflows/exec/worktree.js +171 -0
  484. package/dist/workflows/ir/compile.js +246 -0
  485. package/dist/workflows/ir/freeze.js +233 -0
  486. package/dist/workflows/ir/params.js +54 -0
  487. package/dist/workflows/ir/plan-hash.js +68 -0
  488. package/dist/workflows/ir/schema.js +540 -0
  489. package/dist/workflows/parser.js +878 -304
  490. package/dist/workflows/program/expressions.js +181 -0
  491. package/dist/workflows/program/schema.js +51 -0
  492. package/dist/workflows/renderer.js +100 -45
  493. package/dist/workflows/resource-limits.js +22 -0
  494. package/dist/workflows/runtime/agent-identity.js +59 -14
  495. package/dist/workflows/runtime/checkin.js +1 -1
  496. package/dist/workflows/runtime/plan-classifier.js +131 -0
  497. package/dist/workflows/runtime/runs.js +376 -119
  498. package/dist/workflows/runtime/unit-checkin.js +45 -0
  499. package/dist/workflows/runtime/unit-phases.js +20 -0
  500. package/dist/workflows/runtime/workflow-asset-loader.js +241 -40
  501. package/dist/workflows/schema.js +1 -11
  502. package/dist/workflows/validate-summary.js +2 -3
  503. package/dist/workflows/validator.js +52 -30
  504. package/docs/README.md +42 -78
  505. package/docs/migration/README.md +8 -0
  506. package/docs/migration/release-notes/0.6.0.md +1 -1
  507. package/docs/migration/release-notes/0.7.0.md +9 -8
  508. package/docs/migration/release-notes/0.9.0.md +158 -14
  509. package/docs/migration/v0.7-to-v0.8.md +46 -47
  510. package/docs/migration/v0.8-to-v0.9.md +844 -0
  511. package/docs/reference/README.md +12 -0
  512. package/docs/reference/data-and-telemetry.md +333 -0
  513. package/package.json +21 -17
  514. package/schemas/akm-asset-envelope.json +93 -0
  515. package/schemas/akm-config.json +4636 -0
  516. package/schemas/akm-task.json +87 -0
  517. package/schemas/akm-workflow.json +373 -0
  518. package/dist/akm-migrate-storage +0 -38
  519. package/dist/assets/help/help-accept.md +0 -12
  520. package/dist/assets/help/help-improve.md +0 -84
  521. package/dist/assets/help/help-proposals.md +0 -17
  522. package/dist/assets/help/help-propose.md +0 -17
  523. package/dist/assets/help/help-reject.md +0 -11
  524. package/dist/assets/profiles/frequent.json +0 -13
  525. package/dist/assets/profiles/recombine-only.json +0 -21
  526. package/dist/assets/profiles/reflect-distill.json +0 -30
  527. package/dist/assets/profiles/synthesize.json +0 -15
  528. package/dist/assets/prompts/procedural-system.md +0 -44
  529. package/dist/assets/prompts/recombine-system.md +0 -40
  530. package/dist/assets/prompts/staleness-detect-system.md +0 -6
  531. package/dist/assets/tasks/core/backup.yml +0 -4
  532. package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
  533. package/dist/assets/templates/html/default.html +0 -78
  534. package/dist/assets/templates/html/vendor/echarts.min.js +0 -45
  535. package/dist/assets/wiki/index-template.md +0 -12
  536. package/dist/assets/wiki/ingest-workflow-template.md +0 -83
  537. package/dist/assets/wiki/log-template.md +0 -8
  538. package/dist/assets/wiki/schema-template.md +0 -61
  539. package/dist/cli/config-migrate.js +0 -150
  540. package/dist/cli/config-validate.js +0 -39
  541. package/dist/commands/graph/graph-cli.js +0 -124
  542. package/dist/commands/graph/graph.js +0 -487
  543. package/dist/commands/improve/calibration.js +0 -161
  544. package/dist/commands/improve/dedup.js +0 -482
  545. package/dist/commands/improve/extract-watch.js +0 -140
  546. package/dist/commands/improve/hot-probation.js +0 -45
  547. package/dist/commands/improve/improve-auto-accept.js +0 -276
  548. package/dist/commands/improve/improve-profiles.js +0 -168
  549. package/dist/commands/improve/procedural.js +0 -398
  550. package/dist/commands/improve/recombine.js +0 -818
  551. package/dist/commands/improve/schema-similarity-gate.js +0 -168
  552. package/dist/commands/lint/agent-linter.js +0 -44
  553. package/dist/commands/lint/command-linter.js +0 -44
  554. package/dist/commands/lint/default-linter.js +0 -16
  555. package/dist/commands/lint/fact-linter.js +0 -39
  556. package/dist/commands/lint/knowledge-linter.js +0 -16
  557. package/dist/commands/lint/memory-linter.js +0 -61
  558. package/dist/commands/lint/registry.js +0 -41
  559. package/dist/commands/lint/skill-linter.js +0 -45
  560. package/dist/commands/lint/task-linter.js +0 -50
  561. package/dist/commands/lint/workflow-linter.js +0 -81
  562. package/dist/commands/proposal/legacy-import.js +0 -115
  563. package/dist/commands/sources/history.js +0 -196
  564. package/dist/commands/tasks/default-tasks.js +0 -186
  565. package/dist/commands/wiki-cli.js +0 -292
  566. package/dist/core/asset/asset-registry.js +0 -76
  567. package/dist/core/asset/asset-spec.js +0 -259
  568. package/dist/core/config/config-migration.js +0 -602
  569. package/dist/core/deep-merge.js +0 -38
  570. package/dist/core/eval/rank-metrics.js +0 -113
  571. package/dist/core/ripgrep/install.js +0 -163
  572. package/dist/core/ripgrep/resolve.js +0 -81
  573. package/dist/indexer/db/db.js +0 -1413
  574. package/dist/indexer/manifest.js +0 -170
  575. package/dist/indexer/passes/metadata-contributors.js +0 -31
  576. package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -94
  577. package/dist/integrations/harnesses/opencode-sdk/index.js +0 -49
  578. package/dist/llm/call-ai.js +0 -62
  579. package/dist/llm/memory-infer-impl.js +0 -138
  580. package/dist/output/shapes/distill.js +0 -14
  581. package/dist/output/shapes/history.js +0 -11
  582. package/dist/output/text/distill.js +0 -6
  583. package/dist/output/text/enable-disable.js +0 -8
  584. package/dist/output/text/history.js +0 -6
  585. package/dist/output/text/wiki.js +0 -16
  586. package/dist/registry/build-index.js +0 -386
  587. package/dist/scripts/migrate-storage.js +0 -19108
  588. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +0 -9411
  589. package/dist/scripts/migrations/v16-to-v17.js +0 -141
  590. package/dist/setup/legacy-config.js +0 -106
  591. package/dist/storage/repositories/consolidation-repository.js +0 -38
  592. package/dist/storage/repositories/recombine-repository.js +0 -213
  593. package/dist/wiki/wiki-templates.js +0 -15
  594. package/dist/wiki/wiki.js +0 -1012
  595. package/dist/workflows/db.js +0 -215
  596. package/docs/data-and-telemetry.md +0 -226
  597. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/registry.js +0 -0
  598. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/youtube.js +0 -0
@@ -13,9 +13,9 @@
13
13
  * Architectural notes:
14
14
  * - Stateless. All file/LLM access goes through injectable seams so tests
15
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`).
16
+ * - Bounded LLM call routed through `callStructured`. Improve-stage
17
+ * enablement comes from the active strategy; explicit `akm extract` always
18
+ * runs regardless of that stage toggle.
19
19
  * - Proposals routed via `createProposal({ source: "extract", ... })` — the
20
20
  * same review queue as reflect / distill / consolidate. Never direct-write.
21
21
  * - Per-candidate body assembly merges description (+ when_to_use for lessons)
@@ -26,30 +26,32 @@
26
26
  import fs from "node:fs";
27
27
  import path from "node:path";
28
28
  import { assembleAsset } from "../../core/asset/asset-serialize.js";
29
- import { resolveStashDir, timestampForFilename } from "../../core/common.js";
30
- import { getDefaultLlmConfig, getImproveProcessConfig, loadConfig } from "../../core/config/config.js";
29
+ import { timestampForFilename } from "../../core/common.js";
30
+ import { getImproveProcessConfig, loadConfig } from "../../core/config/config.js";
31
31
  import { ConfigError, UsageError } from "../../core/errors.js";
32
32
  import { appendEvent } from "../../core/events.js";
33
- import { probeLock, releaseLock, tryAcquireLockSync } from "../../core/file-lock.js";
33
+ import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync, } from "../../core/file-lock.js";
34
+ import { tryAcquireMaintenanceBarrier } from "../../core/maintenance-barrier.js";
34
35
  import { resolveStashStandards } from "../../core/standards/resolve-stash-standards.js";
36
+ import { resolveTypeConventions, typeConventionRef } from "../../core/standards/resolve-type-conventions.js";
35
37
  import { getStateDbPath, openStateDatabase, withStateDb } from "../../core/state-db.js";
36
38
  import { repairTruncatedDescription } from "../../core/text-truncation.js";
39
+ import { DURATION_UNITS, parseDuration } from "../../core/time.js";
37
40
  import { warn } from "../../core/warn.js";
38
41
  import { indexWrittenAssets } from "../../indexer/index-written-assets.js";
39
- import { resolveImproveProcessRunnerFromProfile, runnerIsLlm } from "../../integrations/agent/runner.js";
42
+ import { resolveLlmEngineUse } from "../../integrations/agent/engine-resolution.js";
43
+ import { materializeLlmRunnerConnection, resolveImproveProcessRunner, } from "../../integrations/agent/runner.js";
40
44
  import { normalizeHarnessId } from "../../integrations/harnesses/index.js";
41
45
  import { getAvailableHarnesses } from "../../integrations/session-logs/index.js";
42
46
  import { preFilterSession } from "../../integrations/session-logs/pre-filter.js";
43
- import { chatCompletion } from "../../llm/client.js";
44
- import { embed } from "../../llm/embedder.js";
45
- import { tryLlmFeature } from "../../llm/feature-gate.js";
47
+ import { callStructured } from "../../llm/structured-call.js";
46
48
  import { sha256Hex } from "../../runtime.js";
47
49
  import { getExtractedSessionsMap, getLastExtractRunAt, shouldSkipAlreadyExtractedSession, upsertExtractedSession, } from "../../storage/repositories/extract-sessions-repository.js";
48
- import { createProposal, isProposalSkipped } from "../proposal/repository.js";
50
+ import { isProposalSkipped } from "../proposal/repository.js";
49
51
  import { buildExtractPrompt, EXTRACT_JSON_SCHEMA, parseExtractPayload } from "./extract-prompt.js";
50
- import { buildHotProbationFrontmatter } from "./hot-probation.js";
51
- import { resolveProcessEnabled } from "./improve-profiles.js";
52
- import { applySchemaSimilarityPenalty, loadDerivedLayerEmbeddings, } from "./schema-similarity-gate.js";
52
+ import { resolveImproveStrategy, resolveProcessEnabled } from "./improve-strategies.js";
53
+ import { emitProposal } from "./proposal-envelope.js";
54
+ import { createRunContext, resolveRunStashDir } from "./run-context.js";
53
55
  import { buildSessionSummaryPrompt, parseSessionSummary, SESSION_SUMMARY_JSON_SCHEMA, sessionMeetsDurationGate, writeSessionAsset, } from "./session-asset.js";
54
56
  import { resolveTriageConfig, scoreSessionTriage } from "./triage.js";
55
57
  /** Default minimum session duration (minutes) for session indexing (#561). */
@@ -123,20 +125,75 @@ function getExtractSessionLockPath(harness, sessionId, stateDbPath) {
123
125
  * extraction outright.
124
126
  */
125
127
  function acquireExtractSessionLock(lockPath) {
128
+ const releaseBarrier = tryAcquireMaintenanceBarrier();
129
+ if (!releaseBarrier)
130
+ return { proceed: false };
126
131
  try {
127
132
  fs.mkdirSync(path.dirname(lockPath), { recursive: true });
128
- if (tryAcquireLockSync(lockPath, String(process.pid)))
129
- return true;
133
+ let ownership = tryAcquireLockSync(lockPath, createLockPayload());
134
+ if (ownership)
135
+ return { proceed: true, ownership };
130
136
  const probe = probeLock(lockPath, { staleAfterMs: EXTRACT_SESSION_LOCK_STALE_MS });
131
137
  if (probe.state === "held")
132
- return false;
133
- // absent (released between attempt + probe) or stale → reclaim and retry once.
134
- releaseLock(lockPath);
135
- return tryAcquireLockSync(lockPath, String(process.pid));
138
+ return { proceed: false };
139
+ // Absent (released between attempt + probe) or successfully reclaimed stale lock → retry once.
140
+ if (probe.state === "stale" && !reclaimStaleLock(lockPath, probe))
141
+ return { proceed: false };
142
+ ownership = tryAcquireLockSync(lockPath, createLockPayload());
143
+ return ownership ? { proceed: true, ownership } : { proceed: false };
136
144
  }
137
145
  catch {
138
- return true;
146
+ return { proceed: true };
139
147
  }
148
+ finally {
149
+ releaseBarrier();
150
+ }
151
+ }
152
+ function cloneAndFreeze(value) {
153
+ const clone = structuredClone(value);
154
+ const freeze = (item) => {
155
+ if (typeof item !== "object" || item === null || Object.isFrozen(item))
156
+ return;
157
+ for (const child of Object.values(item))
158
+ freeze(child);
159
+ Object.freeze(item);
160
+ };
161
+ freeze(clone);
162
+ return clone;
163
+ }
164
+ /** Resolve standalone extract selection once before discovery, auto iteration, or watch startup. */
165
+ export function resolveStandaloneExtractPlan(config, selection) {
166
+ if (selection.engine && selection.strategy) {
167
+ throw new UsageError("--engine and --strategy are mutually exclusive. Pick one.", "INVALID_FLAG_VALUE");
168
+ }
169
+ const selected = resolveImproveStrategy(selection.strategy, config);
170
+ const process = cloneAndFreeze(getImproveProcessConfig("extract", selected.config) ?? {});
171
+ const invocation = {
172
+ ...(selection.engine ? { engine: selection.engine } : {}),
173
+ ...(Object.hasOwn(selection, "timeoutMs") ? { timeoutMs: selection.timeoutMs ?? null } : {}),
174
+ };
175
+ const resolved = resolveLlmEngineUse(config, [selected.config, process, invocation], { optional: true });
176
+ if (!resolved) {
177
+ throw new ConfigError("No LLM engine configured for extract. Set defaults.llmEngine, pass --engine, or select an improve strategy with processes.extract.engine.", "LLM_NOT_CONFIGURED");
178
+ }
179
+ const runner = {
180
+ kind: "llm",
181
+ engine: resolved.engine,
182
+ connection: resolved.connection,
183
+ ...(resolved.credential ? { credential: resolved.credential } : {}),
184
+ timeoutMs: resolved.timeoutMs,
185
+ };
186
+ return Object.freeze({
187
+ strategy: selected.name,
188
+ engine: resolved.engine,
189
+ // `akm extract` is an explicit operation. The strategy supplies behavior,
190
+ // but its improve-stage enablement gate does not disable this command.
191
+ enabled: true,
192
+ process,
193
+ runner: cloneAndFreeze(runner),
194
+ timeoutMs: resolved.timeoutMs,
195
+ embeddingConfig: cloneAndFreeze(config.embedding),
196
+ });
140
197
  }
141
198
  // ── Helpers ──────────────────────────────────────────────────────────────────
142
199
  /**
@@ -146,6 +203,12 @@ function acquireExtractSessionLock(lockPath) {
146
203
  *
147
204
  * Throws UsageError on unparseable input so the CLI surfaces a clear error
148
205
  * rather than silently defaulting.
206
+ *
207
+ * The recognizer is deliberately CASE-INSENSITIVE and whitespace-tolerant —
208
+ * `5M` means 5 MINUTES here, diverging from the core grammar's case-sensitive
209
+ * `M`=months (pinned by tests/commands/goldens-duration-flags.test.ts); only
210
+ * the unit arithmetic is delegated to the canonical {@link DURATION_UNITS}
211
+ * table via {@link parseDuration}.
149
212
  */
150
213
  export function parseSinceArg(value, now = Date.now()) {
151
214
  if (!value || value.trim() === "") {
@@ -154,10 +217,9 @@ export function parseSinceArg(value, now = Date.now()) {
154
217
  const trimmed = value.trim();
155
218
  const relMatch = trimmed.match(/^(\d+)\s*([mhd])$/i);
156
219
  if (relMatch) {
157
- const n = Number.parseInt(relMatch[1] ?? "0", 10);
158
- const unit = (relMatch[2] ?? "h").toLowerCase();
159
- const ms = unit === "m" ? n * 60_000 : unit === "h" ? n * 3_600_000 : n * 86_400_000;
160
- return now - ms;
220
+ const ms = parseDuration(`${relMatch[1] ?? "0"}${(relMatch[2] ?? "h").toLowerCase()}`, DURATION_UNITS);
221
+ if (ms !== null)
222
+ return now - ms;
161
223
  }
162
224
  const iso = Date.parse(trimmed);
163
225
  if (!Number.isNaN(iso))
@@ -187,31 +249,53 @@ function resolveHarness(type, harnesses) {
187
249
  * so the accept-time descriptionQualityValidator passes — same pattern as
188
250
  * the consolidate-writer fix at consolidate.ts.
189
251
  */
190
- function buildCandidateProposal(candidate, sourceRef) {
191
- const ref = `${candidate.type}:${candidate.name}`;
252
+ function buildCandidateProposal(candidate, sourceRef, sessionAssetRef) {
253
+ const ref = deriveExtractCandidateRef(candidate, sourceRef);
192
254
  // Post-generation repair pass (#556): deterministically complete a
193
255
  // description the LLM sliced mid-sentence before it reaches the
194
256
  // auto-accept validators. No-op (byte-identical) for valid descriptions.
195
257
  const description = repairTruncatedDescription(candidate.description, candidate.body);
196
258
  const fm = {
197
259
  description,
198
- sources: [`session:${sourceRef.harness}:${sourceRef.sessionId}`],
260
+ ...(sessionAssetRef ? { xrefs: [sessionAssetRef] } : {}),
199
261
  };
200
262
  if (candidate.type === "lesson" && candidate.when_to_use) {
201
263
  fm.when_to_use = candidate.when_to_use;
202
264
  }
203
- // #615 WS-0: preserve ordered-action + outcome data in frontmatter so the data
204
- // survives even if source transcripts are not re-extractable later. The
205
- // procedural-compilation feature (detection/compilation) is deferred to 0.10+.
206
- if (candidate.orderedActions && candidate.orderedActions.length > 0) {
207
- fm.orderedActions = candidate.orderedActions;
208
- if (candidate.outcomeData) {
209
- fm.outcomeData = candidate.outcomeData;
210
- }
211
- }
212
265
  const content = assembleAsset(fm, candidate.body);
213
266
  return { ref, content, description };
214
267
  }
268
+ function canonicalSegment(value) {
269
+ return value
270
+ .trim()
271
+ .toLowerCase()
272
+ .replace(/[^a-z0-9-]+/g, "-")
273
+ .replace(/-+/g, "-")
274
+ .replace(/^-|-$/g, "");
275
+ }
276
+ export function deriveExtractCandidateRef(candidate, sourceRef) {
277
+ const candidateParts = candidate.name.split("/").map(canonicalSegment).filter(Boolean);
278
+ const leaf = candidateParts.at(-1) ?? "extracted-insight";
279
+ if (candidate.type === "memory" || candidate.type === "lesson") {
280
+ const projectName = sourceRef.projectHint?.split(/[\\/]/).filter(Boolean).at(-1);
281
+ const scope = projectName ? canonicalSegment(projectName) : "";
282
+ const subdir = candidate.type === "memory" ? "memories" : "lessons";
283
+ return `${subdir}/${scope ? `${scope}/` : ""}${leaf}`;
284
+ }
285
+ return `knowledge/${leaf}`;
286
+ }
287
+ function resolveExtractStandards(stashDir) {
288
+ const sections = [];
289
+ const general = resolveStashStandards(stashDir);
290
+ if (general)
291
+ sections.push(general);
292
+ for (const type of ["memory", "lesson", "knowledge"]) {
293
+ const body = resolveTypeConventions(stashDir, type);
294
+ if (body)
295
+ sections.push(`# ${typeConventionRef(type)} (soft per-type conventions — guidance, not enforced)\n${body}`);
296
+ }
297
+ return sections.join("\n\n");
298
+ }
215
299
  /**
216
300
  * Canonicalize a session's content into a single deterministic string for
217
301
  * hashing (#602). Each event is rendered `<role>\n<text>` and events are joined
@@ -245,36 +329,32 @@ export function hashSessionContent(data) {
245
329
  * proposal validation failure) the session result records a warning and
246
330
  * keeps going — one session's bad luck never aborts a multi-session run.
247
331
  */
248
- async function processSession(harness, sessionRef, stashDir, config, llmConfig, chat, ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, minContentChars,
249
- // #626 — pre-LLM heuristic triage gate. Default-off (enabled:false) takes the
250
- // exact pre-change path (no scorer call, no new skipReason).
251
- // #641 — proceduralAwareFloor is opt-in, DEFAULT OFF.
252
- triage, sessionIndexing, schemaSimilarityCtx,
253
- // #602 — already-extracted skip moved INSIDE processSession: the content hash
254
- // can only be computed after readSession, so the skip decision lives here. The
255
- // prior row + bypass flags are threaded in from the caller. Skipping here still
256
- // costs ZERO LLM calls (the expensive resource #602 protects); only the cheap
257
- // file read is incurred.
258
- prior, force,
259
- // Stash authoring standards (convention/meta fact bodies) for non-wiki
260
- // output. Resolved ONCE per run by the caller and threaded in so facts are
261
- // not re-read per session. Empty string when none exist.
262
- standardsContext) {
263
- const warnings = [];
332
+ /**
333
+ * The zero-LLM pre-flight gates for one session: read, the #602 content-hash
334
+ * already-extracted skip, the #595/#596 minContentChars floor, and the #626
335
+ * heuristic triage gate. Returns a terminal skip result, or the read `data` +
336
+ * pre-filtered events + content hash to carry into the extraction prompt.
337
+ * Extracted verbatim from `processSession` — every skip shape/reason is
338
+ * byte-identical.
339
+ */
340
+ function runPreLlmSessionGates(args) {
341
+ const { harness, sessionRef, prior, force, maxTotalChars, minContentChars, triage } = args;
264
342
  let data;
265
343
  try {
266
344
  data = harness.readSession(sessionRef);
267
345
  }
268
346
  catch (err) {
269
347
  return {
270
- sessionId: sessionRef.sessionId,
271
- harness: harness.name,
272
- candidateCount: 0,
273
- proposalIds: [],
274
- preFilter: { inputCount: 0, outputCount: 0, truncatedCount: 0 },
275
- warnings: [`readSession failed: ${err instanceof Error ? err.message : String(err)}`],
276
- skipped: true,
277
- skipReason: "read_failed",
348
+ skip: {
349
+ sessionId: sessionRef.sessionId,
350
+ harness: harness.name,
351
+ candidateCount: 0,
352
+ proposalIds: [],
353
+ preFilter: { inputCount: 0, outputCount: 0, truncatedCount: 0 },
354
+ warnings: [`readSession failed: ${err instanceof Error ? err.message : String(err)}`],
355
+ skipped: true,
356
+ skipReason: "read_failed",
357
+ },
278
358
  };
279
359
  }
280
360
  // #602 — content-hash skip. Computed on the RAW event stream immediately after
@@ -287,15 +367,17 @@ standardsContext) {
287
367
  const contentHash = hashSessionContent(data);
288
368
  if (!force && shouldSkipAlreadyExtractedSession(prior, contentHash)) {
289
369
  return {
290
- sessionId: sessionRef.sessionId,
291
- harness: harness.name,
292
- candidateCount: 0,
293
- proposalIds: [],
294
- preFilter: { inputCount: 0, outputCount: 0, truncatedCount: 0 },
295
- warnings: [`already extracted (content unchanged) at ${prior?.processed_at}; pass --force to re-process`],
296
- skipped: true,
297
- skipReason: "already_extracted",
298
- contentHash,
370
+ skip: {
371
+ sessionId: sessionRef.sessionId,
372
+ harness: harness.name,
373
+ candidateCount: 0,
374
+ proposalIds: [],
375
+ preFilter: { inputCount: 0, outputCount: 0, truncatedCount: 0 },
376
+ warnings: [`already extracted (content unchanged) at ${prior?.processed_at}; pass --force to re-process`],
377
+ skipped: true,
378
+ skipReason: "already_extracted",
379
+ contentHash,
380
+ },
299
381
  };
300
382
  }
301
383
  const filtered = preFilterSession(data, {
@@ -312,32 +394,7 @@ standardsContext) {
312
394
  const rawContentChars = data.events.reduce((sum, event) => sum + event.text.length, 0);
313
395
  if (minContentChars > 0 && rawContentChars < minContentChars) {
314
396
  return {
315
- sessionId: sessionRef.sessionId,
316
- harness: harness.name,
317
- candidateCount: 0,
318
- proposalIds: [],
319
- preFilter: {
320
- inputCount: filtered.stats.inputCount,
321
- outputCount: filtered.stats.outputCount,
322
- truncatedCount: filtered.stats.truncatedCount,
323
- },
324
- warnings: [],
325
- skipped: true,
326
- skipReason: "too_short",
327
- contentHash,
328
- };
329
- }
330
- // #626 — pre-LLM heuristic triage gate. Runs AFTER minContentChars + the
331
- // already-extracted skip check (both in the caller / above), BEFORE the
332
- // extraction prompt and the session-asset write. When the session scores below
333
- // the configured threshold we triage it out: no chat() call, no session asset,
334
- // no proposals. Pure-heuristic — zero added LLM cost. Default-off → skipped.
335
- if (triage.enabled) {
336
- const t = scoreSessionTriage(data, triage.minScore, {
337
- proceduralAwareFloor: triage.proceduralAwareFloor,
338
- });
339
- if (!t.pass) {
340
- return {
397
+ skip: {
341
398
  sessionId: sessionRef.sessionId,
342
399
  harness: harness.name,
343
400
  candidateCount: 0,
@@ -349,11 +406,91 @@ standardsContext) {
349
406
  },
350
407
  warnings: [],
351
408
  skipped: true,
352
- skipReason: "triaged_out",
409
+ skipReason: "too_short",
353
410
  contentHash,
411
+ },
412
+ };
413
+ }
414
+ // #626 — pre-LLM heuristic triage gate. Runs AFTER minContentChars + the
415
+ // already-extracted skip check (both in the caller / above), BEFORE the
416
+ // extraction prompt and the session-asset write. When the session scores below
417
+ // the configured threshold we triage it out: no chat() call, no session asset,
418
+ // no proposals. Pure-heuristic — zero added LLM cost. Default-off → skipped.
419
+ if (triage.enabled) {
420
+ const t = scoreSessionTriage(data, triage.minScore);
421
+ if (!t.pass) {
422
+ return {
423
+ skip: {
424
+ sessionId: sessionRef.sessionId,
425
+ harness: harness.name,
426
+ candidateCount: 0,
427
+ proposalIds: [],
428
+ preFilter: {
429
+ inputCount: filtered.stats.inputCount,
430
+ outputCount: filtered.stats.outputCount,
431
+ truncatedCount: filtered.stats.truncatedCount,
432
+ },
433
+ warnings: [],
434
+ skipped: true,
435
+ skipReason: "triaged_out",
436
+ contentHash,
437
+ },
354
438
  };
355
439
  }
356
440
  }
441
+ return { data, filtered, contentHash };
442
+ }
443
+ /**
444
+ * The bounded per-session extraction LLM call. Resolves the connection with
445
+ * the same fail-open contract the gated fn had (a `getLlmConfig()` throw —
446
+ * `materializeLlmConnection` can raise ConfigError — takes the skipped path,
447
+ * never propagates), then routes through `callStructured` under the
448
+ * `session_extraction` gate. Returns the seam result plus the `llmRaw`
449
+ * side-channel value that distinguishes fallback-took-over from a
450
+ * genuinely-empty response.
451
+ */
452
+ async function runSessionExtractionLlmCall(args) {
453
+ const { config, getLlmConfig, chat, prompt, timeoutMs, signal } = args;
454
+ let extractLlm;
455
+ try {
456
+ extractLlm = getLlmConfig();
457
+ }
458
+ catch {
459
+ extractLlm = undefined;
460
+ }
461
+ let llmRaw = "";
462
+ const llmResult = extractLlm === undefined
463
+ ? ""
464
+ : await callStructured({
465
+ feature: "session_extraction",
466
+ akmConfig: config,
467
+ config: extractLlm,
468
+ messages: [{ role: "user", content: prompt }],
469
+ request: {
470
+ timeoutMs,
471
+ responseSchema: EXTRACT_JSON_SCHEMA,
472
+ ...(signal ? { signal } : {}),
473
+ ...(chat ? { chat } : {}),
474
+ },
475
+ parse: (raw) => {
476
+ llmRaw = raw ?? "";
477
+ return llmRaw;
478
+ },
479
+ // A transport throw takes the "" fallback with llmRaw left unset —
480
+ // the same skipped path the gated-fn throw produced before.
481
+ onError: () => "",
482
+ fallback: "",
483
+ });
484
+ return { llmResult, llmRaw };
485
+ }
486
+ async function processSession(runCtx, session) {
487
+ const { harness, stashDir, config, getLlmConfig, chat, ctx, eventsCtx, sourceRun, dryRun, timeoutMs, maxTotalChars, minContentChars, triage, sessionIndexing, signal, standardsContext, } = runCtx;
488
+ const { sessionRef, prior, force } = session;
489
+ const warnings = [];
490
+ const gate = runPreLlmSessionGates({ harness, sessionRef, prior, force, maxTotalChars, minContentChars, triage });
491
+ if ("skip" in gate)
492
+ return gate.skip;
493
+ const { data, filtered, contentHash } = gate;
357
494
  const prompt = buildExtractPrompt({
358
495
  data,
359
496
  events: filtered.events,
@@ -388,16 +525,16 @@ standardsContext) {
388
525
  }
389
526
  return {};
390
527
  };
391
- let llmRaw = "";
392
- const llmResult = await tryLlmFeature("session_extraction", config, async () => {
393
- llmRaw = await chat(llmConfig, [{ role: "user", content: prompt }], {
394
- timeoutMs,
395
- responseSchema: EXTRACT_JSON_SCHEMA,
396
- });
397
- return llmRaw;
398
- }, "", { timeoutMs });
528
+ const { llmResult, llmRaw } = await runSessionExtractionLlmCall({
529
+ config,
530
+ getLlmConfig,
531
+ chat,
532
+ prompt,
533
+ timeoutMs,
534
+ signal,
535
+ });
399
536
  if (llmResult === "" && !llmRaw) {
400
- // tryLlmFeature took the fallback path (disabled / timeout / error). Return skipped.
537
+ // The seam took the fallback path (disabled / timeout / error). Return skipped.
401
538
  return {
402
539
  sessionId: sessionRef.sessionId,
403
540
  harness: harness.name,
@@ -416,10 +553,12 @@ standardsContext) {
416
553
  }
417
554
  const payload = parseExtractPayload(llmRaw);
418
555
  const proposalIds = [];
556
+ // Provenance refs are added only after the cited session asset exists.
557
+ const sessionAsset = await maybeWriteSessionAsset();
419
558
  if (payload.candidates.length === 0) {
420
559
  appendEvent({
421
560
  eventType: "extract_invoked",
422
- ref: `session:${harness.name}:${sessionRef.sessionId}`,
561
+ ...(sessionAsset.sessionAssetRef ? { ref: sessionAsset.sessionAssetRef } : {}),
423
562
  metadata: {
424
563
  outcome: "no_candidates",
425
564
  sessionId: sessionRef.sessionId,
@@ -429,8 +568,7 @@ standardsContext) {
429
568
  preFilterInput: filtered.stats.inputCount,
430
569
  preFilterOutput: filtered.stats.outputCount,
431
570
  },
432
- }, ctx);
433
- const sessionAsset = await maybeWriteSessionAsset();
571
+ }, eventsCtx);
434
572
  return {
435
573
  sessionId: sessionRef.sessionId,
436
574
  harness: harness.name,
@@ -447,63 +585,41 @@ standardsContext) {
447
585
  ...sessionAsset,
448
586
  };
449
587
  }
450
- // WS-3b step 0c: hot-probation intake buffer (#604).
451
- // When enabled, system-generated extractions enter captureMode: hot-probation
452
- // so they spend ONE consolidation cycle in probation before the deterministic
453
- // dedup+quality pass promotes them. Default OFF.
454
- // Reads the `default` profile deliberately: this per-session hot-probation
455
- // flag lives inside the 18-arg `processSession`, which has no handle to the
456
- // active profile, and threading a 19th positional arg for a DEFAULT-OFF flag
457
- // isn't worth the param bloat. The extract STAGE toggle (in `akmExtract`,
458
- // below) already honors `--profile`.
459
- const hotProbationEnabled = getImproveProcessConfig(config, "extract")?.hotProbation?.enabled === true;
588
+ // §23.6 fingerprint model-id term: the profile resolved for this session's
589
+ // LLM call (best-effort — an unconfigured profile leaves the term empty).
590
+ let extractModelId;
591
+ try {
592
+ extractModelId = runCtx.getLlmConfig().model;
593
+ }
594
+ catch {
595
+ extractModelId = undefined;
596
+ }
460
597
  for (const candidate of payload.candidates) {
598
+ const built = buildCandidateProposal(candidate, data.ref, sessionAsset.sessionAssetRef);
461
599
  if (dryRun) {
462
- proposalIds.push(`dry-run:${candidate.type}:${candidate.name}`);
600
+ proposalIds.push(`dry-run:${built.ref}`);
463
601
  continue;
464
602
  }
465
603
  try {
466
- // WS-3b Step-0b: schema-similarity intake gate. When enabled and the
467
- // candidate is a lesson/knowledge whose body embedding is within ε of an
468
- // existing derived-layer node, down-prioritize by multiplying confidence by
469
- // the penalty. PARITY: schemaSimilarityCtx is null when the flag is off →
470
- // applySchemaSimilarityPenalty returns the original confidence untouched and
471
- // never embeds. (Logic lives in schema-similarity-gate.ts so it is unit-testable.)
472
- const gateResult = await applySchemaSimilarityPenalty(candidate, schemaSimilarityCtx, (text) => schemaSimilarityCtx?.embedFn
473
- ? schemaSimilarityCtx.embedFn(text)
474
- : embed(text, schemaSimilarityCtx?.embeddingConfig));
475
- const effectiveConfidence = gateResult.effectiveConfidence;
476
- if (gateResult.warning)
477
- warn(gateResult.warning);
478
- const { ref, content, description } = buildCandidateProposal(candidate, sessionRef);
479
- const result = createProposal(stashDir, {
604
+ const { ref, content, description } = built;
605
+ const result = emitProposal({ stashDir, proposalsCtx: ctx }, {
480
606
  ref,
481
607
  source: "extract",
482
608
  sourceRun,
609
+ // §23.6 fingerprint model-id term (WI-6.4). The LLM already ran for
610
+ // this session, so the profile is resolvable; guard anyway.
611
+ ...(extractModelId ? { modelId: extractModelId } : {}),
483
612
  payload: {
484
613
  content,
485
614
  frontmatter: {
486
615
  description,
487
616
  ...(candidate.when_to_use ? { when_to_use: candidate.when_to_use } : {}),
488
- ...(effectiveConfidence !== undefined ? { confidence: effectiveConfidence } : {}),
489
- sources: [`session:${sessionRef.harness}:${sessionRef.sessionId}`],
617
+ confidence: candidate.confidence,
618
+ ...(sessionAsset.sessionAssetRef ? { xrefs: [sessionAsset.sessionAssetRef] } : {}),
490
619
  evidence: candidate.evidence,
491
- // #615 WS-0: mirror ordered-action + outcome data in the proposal
492
- // frontmatter record so downstream tooling can read it without
493
- // re-parsing the content body. Omitted when not present.
494
- ...(candidate.orderedActions && candidate.orderedActions.length > 0
495
- ? { orderedActions: candidate.orderedActions }
496
- : {}),
497
- ...(candidate.outcomeData ? { outcomeData: candidate.outcomeData } : {}),
498
- // WS-3b step 0c: tag system-generated extractions as hot-probation
499
- // when the feature is enabled. The consolidation pass will exclude
500
- // them from the LLM merge pool until the intake dedup+quality pass
501
- // runs against them. User-explicit `akm remember` (captureMode: hot)
502
- // is unaffected — this only applies to extract-generated proposals.
503
- ...(hotProbationEnabled ? buildHotProbationFrontmatter() : {}),
504
620
  },
505
621
  },
506
- }, ctx);
622
+ });
507
623
  if (isProposalSkipped(result)) {
508
624
  warnings.push(`candidate ${candidate.type}:${candidate.name} skipped: ${result.reason}: ${result.message}`);
509
625
  }
@@ -517,7 +633,7 @@ standardsContext) {
517
633
  }
518
634
  appendEvent({
519
635
  eventType: "extract_invoked",
520
- ref: `session:${harness.name}:${sessionRef.sessionId}`,
636
+ ...(sessionAsset.sessionAssetRef ? { ref: sessionAsset.sessionAssetRef } : {}),
521
637
  metadata: {
522
638
  outcome: "candidates_queued",
523
639
  sessionId: sessionRef.sessionId,
@@ -528,8 +644,7 @@ standardsContext) {
528
644
  preFilterInput: filtered.stats.inputCount,
529
645
  preFilterOutput: filtered.stats.outputCount,
530
646
  },
531
- }, ctx);
532
- const sessionAsset = await maybeWriteSessionAsset();
647
+ }, eventsCtx);
533
648
  return {
534
649
  sessionId: sessionRef.sessionId,
535
650
  harness: harness.name,
@@ -545,191 +660,34 @@ standardsContext) {
545
660
  ...sessionAsset,
546
661
  };
547
662
  }
548
- // ── Public entrypoint ────────────────────────────────────────────────────────
549
- export async function akmExtract(options) {
550
- const startMs = Date.now();
551
- if (!options.type || options.type.trim() === "") {
552
- throw new UsageError("--type is required. Pass a harness name (e.g. --type claude-code).", "MISSING_REQUIRED_ARGUMENT");
553
- }
554
- const config = options.config ?? loadConfig();
555
- const stashDir = options.stashDir ?? resolveStashDir();
556
- const dryRun = options.dryRun ?? false;
557
- const sourceRun = options.sourceRun ?? `extract-${timestampForFilename()}`;
558
- // Read the per-process extract config + enabled-state from the ACTIVE improve
559
- // profile when `akmImprove` threaded it; otherwise fall back to the `default`
560
- // profile (the explicit `akm extract` path, which has no active profile). This
561
- // is what stops a non-default profile's `extract.enabled` from being silently
562
- // overridden by the default profile and vice-versa.
563
- const activeProfile = options.improveProfile;
564
- const extractProcess = activeProfile ? activeProfile.processes?.extract : getImproveProcessConfig(config, "extract");
565
- // The `extract.enabled` process toggle gates extract as a STAGE of `akm improve`
566
- // (the activeProfile path) — consistent with #593/#594 where the active profile,
567
- // not `default`, is the source of truth. An EXPLICIT `akm extract` invocation
568
- // (no activeProfile) is a direct user/cron action and always runs; gating it on
569
- // the default improve profile's stage toggle was a footgun — dropping extract
570
- // from the daily improve profile would silently disable the standalone command.
571
- const extractEnabled = activeProfile ? resolveProcessEnabled("extract", activeProfile) : true;
572
- // Feature-gate early so we get a clean "skipped because disabled" envelope.
573
- if (!extractEnabled) {
574
- return {
575
- schemaVersion: 1,
576
- ok: true,
577
- shape: "extract-result",
578
- dryRun,
579
- type: options.type,
580
- sessionsProcessed: 0,
581
- sessionsSkipped: 0,
582
- candidatesCreated: 0,
583
- proposals: [],
584
- sessions: [],
585
- warnings: [
586
- "session_extraction feature disabled — set profiles.improve.default.processes.extract.enabled: true to use",
587
- ],
588
- durationMs: Date.now() - startMs,
589
- };
590
- }
591
- // Resolve the LLM connection. Priority order:
592
- // 1. Options.config.profiles.improve.default.processes.extract.profile
593
- // (per-process override, matches reflect/distill/consolidate)
594
- // 2. config.defaults.llm (the default LLM profile)
595
- // 3. throw — extract requires an LLM.
596
- let llmConfig;
597
- const runnerSpec = resolveImproveProcessRunnerFromProfile(extractProcess, config);
598
- if (runnerSpec) {
599
- if (!runnerIsLlm(runnerSpec)) {
600
- 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");
601
- }
602
- llmConfig = runnerSpec.connection;
603
- }
604
- else {
605
- llmConfig = getDefaultLlmConfig(config) ?? undefined;
606
- }
607
- if (!llmConfig) {
608
- throw new ConfigError("No LLM connection configured for extract. Set profiles.llm + defaults.llm, or set profiles.improve.default.processes.extract.profile to a configured LLM profile.");
609
- }
610
- // Honor per-process timeoutMs override; fall back to options.timeoutMs; then 60s.
611
- const timeoutMs = options.timeoutMs ??
612
- (typeof extractProcess?.timeoutMs === "number" ? extractProcess.timeoutMs : undefined) ??
613
- 60_000;
614
- // Pre-filter budget — process config can raise it for large-context models.
615
- const maxTotalChars = typeof extractProcess?.maxTotalChars === "number" ? extractProcess.maxTotalChars : undefined;
616
- // #595/#596 — minimum raw session size; sessions below it skip the LLM call
617
- // entirely. Set `processes.extract.minContentChars: 0` to disable the gate.
618
- const minContentChars = typeof extractProcess?.minContentChars === "number" ? extractProcess.minContentChars : DEFAULT_MIN_CONTENT_CHARS;
619
- // Cap on NEW sessions LLM-processed per run; 0 disables. Absent = default.
620
- // Bounds per-run wall time / LLM cost so a backlog can't push a run past its
621
- // task timeout — the overflow stays unseen and is picked up by later runs.
622
- const maxSessionsPerRun = typeof extractProcess?.maxSessionsPerRun === "number"
623
- ? extractProcess.maxSessionsPerRun
624
- : DEFAULT_MAX_SESSIONS_PER_RUN;
625
- // Default discovery window — process config can override the built-in 24h.
626
- const effectiveSince = options.since ?? extractProcess?.defaultSince;
627
- // #626 — resolve the triage gate config once per run. Default-off → the
628
- // per-session path never calls the scorer and emits no telemetry.
629
- const triage = resolveTriageConfig(extractProcess);
630
- // #561 — resolve session-indexing config. Default ON: we only reach this code
631
- // when `session_extraction` is enabled AND an LLM is configured (both checked
632
- // above), so defaulting on costs nothing offline (the summary call fails open)
633
- // while making sessions searchable in the common LLM-configured case. Set
634
- // `processes.extract.indexSessions: false` for byte-identical legacy behaviour.
635
- const sessionIndexingEnabled = extractProcess?.indexSessions ?? true;
636
- const minSessionDuration = typeof extractProcess?.minSessionDuration === "number"
637
- ? extractProcess.minSessionDuration
638
- : DEFAULT_MIN_SESSION_DURATION_MINUTES;
639
- // Production summary generator: a bounded in-tree LLM call wrapped in the same
640
- // fail-open `tryLlmFeature` seam as the rest of extract. Returns `undefined`
641
- // on disablement / timeout / error so no asset is written. Tests inject a fake.
642
- const chatForSummary = options.chat ?? chatCompletion;
643
- const defaultSessionSummaryGenerator = async (data) => {
644
- let raw = "";
645
- await tryLlmFeature("session_extraction", config, async () => {
646
- raw = await chatForSummary(llmConfig, [{ role: "user", content: buildSessionSummaryPrompt(data) }], {
647
- timeoutMs,
648
- responseSchema: SESSION_SUMMARY_JSON_SCHEMA,
649
- });
650
- return raw;
651
- }, "", { timeoutMs });
652
- return parseSessionSummary(raw);
653
- };
654
- const sessionIndexing = {
655
- enabled: sessionIndexingEnabled,
656
- minDurationMinutes: minSessionDuration,
657
- generate: options.generateSessionSummary ?? defaultSessionSummaryGenerator,
663
+ /**
664
+ * Iterate the discovered candidate sessions: enforce the per-run cap, take the
665
+ * per-session cross-process lock, dispatch to {@link processSession}, aggregate
666
+ * the #626 triage counters, and persist each seen-row outcome. Extracted verbatim
667
+ * from `akmExtract` — the maxSessionsPerRun break, lock/skip accounting, triage
668
+ * aggregation, and seen-row upsert are byte-identical.
669
+ */
670
+ async function runExtractSessionLoop(args) {
671
+ const { candidates, options, harness, seenMap, stateDb, trackingEnabled, dryRun, stashDir, config, getLlmConfig, chat, sourceRun, timeoutMs, maxTotalChars, minContentChars, maxSessionsPerRun, triage, sessionIndexing, extractStandardsContext, topLevelWarnings, } = args;
672
+ // WI-7.7 §2: run-scoped processSession inputs, resolved once per run.
673
+ const sessionRunCtx = {
674
+ harness,
675
+ stashDir,
676
+ config,
677
+ getLlmConfig,
678
+ chat,
679
+ ctx: options.ctx,
680
+ eventsCtx: options.eventsCtx,
681
+ sourceRun,
682
+ dryRun,
683
+ timeoutMs,
684
+ maxTotalChars,
685
+ minContentChars,
686
+ triage,
687
+ sessionIndexing,
688
+ signal: options.signal,
689
+ standardsContext: extractStandardsContext,
658
690
  };
659
- const harness = resolveHarness(options.type, options.harnesses);
660
- if (!harness) {
661
- return {
662
- schemaVersion: 1,
663
- ok: false,
664
- shape: "extract-result",
665
- dryRun,
666
- type: options.type,
667
- sessionsProcessed: 0,
668
- sessionsSkipped: 0,
669
- candidatesCreated: 0,
670
- proposals: [],
671
- sessions: [],
672
- warnings: [`no available harness matches type "${options.type}" (check that the platform is installed)`],
673
- durationMs: Date.now() - startMs,
674
- };
675
- }
676
- if (!harness.isAvailable()) {
677
- return {
678
- schemaVersion: 1,
679
- ok: false,
680
- shape: "extract-result",
681
- dryRun,
682
- type: options.type,
683
- sessionsProcessed: 0,
684
- sessionsSkipped: 0,
685
- candidatesCreated: 0,
686
- proposals: [],
687
- sessions: [],
688
- warnings: [`harness ${options.type} is registered but reports not-available (no session data on this machine)`],
689
- durationMs: Date.now() - startMs,
690
- };
691
- }
692
- // Decide which sessions to process: explicit sessionId OR discovery via since.
693
- let candidates;
694
- if (options.sessionId) {
695
- const all = harness.listSessions({
696
- ...(options.location ? { location: options.location } : {}),
697
- });
698
- const target = all.find((s) => s.sessionId === options.sessionId);
699
- if (!target) {
700
- return {
701
- schemaVersion: 1,
702
- ok: false,
703
- shape: "extract-result",
704
- dryRun,
705
- type: options.type,
706
- sessionsProcessed: 0,
707
- sessionsSkipped: 0,
708
- candidatesCreated: 0,
709
- proposals: [],
710
- sessions: [],
711
- warnings: [`session ${options.sessionId} not found for harness ${options.type}`],
712
- durationMs: Date.now() - startMs,
713
- };
714
- }
715
- candidates = [target];
716
- }
717
- else {
718
- // No explicit `--since`/`defaultSince` → default to "since the last run"
719
- // (floored at 48h) so an intermittently-online host doesn't lose sessions
720
- // that ended while it was off. See {@link resolveDefaultSinceMs}.
721
- const sinceMs = effectiveSince
722
- ? parseSinceArg(effectiveSince)
723
- : resolveDefaultSinceMs(harness.name, startMs, {
724
- ...(options.stateDb ? { stateDb: options.stateDb } : {}),
725
- ...(options.stateDbPath ? { stateDbPath: options.stateDbPath } : {}),
726
- ...(options.skipTracking ? { skipTracking: options.skipTracking } : {}),
727
- });
728
- candidates = harness.listSessions({
729
- sinceMs,
730
- ...(options.location ? { location: options.location } : {}),
731
- });
732
- }
733
691
  const sessions = [];
734
692
  let processedCount = 0;
735
693
  let skippedCount = 0;
@@ -738,51 +696,9 @@ export async function akmExtract(options) {
738
696
  let triagePassed = 0;
739
697
  let triagedOut = 0;
740
698
  const allProposalIds = [];
741
- const topLevelWarnings = [];
742
- const chat = options.chat ?? chatCompletion;
743
- // Open state.db once for the run and bulk-load seen-rows for the candidate
744
- // set so we can decide skip/process in O(1) per session. Tracking is opt-out
745
- // via options.skipTracking (used by tests + one-shot debug calls).
746
- const trackingEnabled = options.skipTracking !== true;
747
- let stateDb;
748
- let seenMap = new Map();
749
- if (trackingEnabled && candidates.length > 0) {
750
- try {
751
- stateDb = options.stateDb ?? openStateDatabase(options.stateDbPath);
752
- seenMap = getExtractedSessionsMap(stateDb, harness.name, candidates.map((c) => c.sessionId));
753
- }
754
- catch (err) {
755
- // state.db open is best-effort — log and proceed without skip-tracking
756
- // so a transient sqlite error never blocks the actual extraction.
757
- const msg = err instanceof Error ? err.message : String(err);
758
- warn(`[extract] state.db unavailable, processing without skip-tracking: ${msg}`);
759
- topLevelWarnings.push(`state.db unavailable: ${msg}`);
760
- stateDb = undefined;
761
- }
762
- }
763
- // WS-3b Step-0b: schema-similarity intake gate.
764
- // DEFAULT ON since R3 (docs/design/improve-self-learning-analysis.md G5):
765
- // extract is the highest-volume acquisition path with no LLM judge, so the
766
- // cheap embedding-dedup check (one embed per lesson/knowledge candidate,
767
- // fail-open) is the intake quality gate. Opt out via
768
- // processes.extract.schemaSimilarity.enabled: false. The gate is inert in
769
- // practice when no derived-layer embeddings exist (empty ctx → no penalty).
770
- const schemaSimilarityCfg = extractProcess?.schemaSimilarity;
771
- let schemaSimilarityCtx = null;
772
- if (schemaSimilarityCfg?.enabled !== false) {
773
- const derivedEmbeddings = options.schemaSimilarityEmbeddings ?? loadDerivedLayerEmbeddings();
774
- schemaSimilarityCtx = {
775
- config: { ...schemaSimilarityCfg, enabled: true },
776
- derivedEmbeddings,
777
- embeddingConfig: config.embedding,
778
- embedFn: options.schemaSimilarityEmbedFn,
779
- };
780
- }
781
- // Stash authoring standards (convention/meta fact bodies) for non-wiki
782
- // extract output. Resolved ONCE per run and threaded into each session's
783
- // prompt so facts are not re-read per session.
784
- const extractStandardsContext = resolveStashStandards(stashDir);
785
699
  for (const summary of candidates) {
700
+ if (options.signal?.aborted)
701
+ break;
786
702
  // #602 — the already-extracted skip moved INTO processSession (the content
787
703
  // hash needs the session body, only available after readSession). The prior
788
704
  // row + bypass flags are threaded through; an unchanged session returns
@@ -802,10 +718,11 @@ export async function akmExtract(options) {
802
718
  // runs (those that open their own state.db): dry-run is read-only, an
803
719
  // injected `stateDb` handle is an in-process/test scenario with no cross-
804
720
  // process race, and skip-tracking-off opts out entirely.
805
- let sessionLockPath;
721
+ let sessionLockOwnership;
806
722
  if (trackingEnabled && !dryRun && !options.stateDb) {
807
- sessionLockPath = getExtractSessionLockPath(harness.name, summary.sessionId, options.stateDbPath ?? getStateDbPath());
808
- if (!acquireExtractSessionLock(sessionLockPath)) {
723
+ const sessionLockPath = getExtractSessionLockPath(harness.name, summary.sessionId, options.stateDbPath ?? getStateDbPath());
724
+ const sessionLock = acquireExtractSessionLock(sessionLockPath);
725
+ if (!sessionLock.proceed) {
809
726
  sessions.push({
810
727
  sessionId: summary.sessionId,
811
728
  harness: harness.name,
@@ -819,9 +736,14 @@ export async function akmExtract(options) {
819
736
  skippedCount += 1;
820
737
  continue;
821
738
  }
739
+ sessionLockOwnership = sessionLock.ownership;
822
740
  }
823
741
  try {
824
- const result = await processSession(harness, summary, stashDir, config, llmConfig, chat, options.ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, minContentChars, triage, sessionIndexing, schemaSimilarityCtx, prior, options.force === true, extractStandardsContext);
742
+ const result = await processSession(sessionRunCtx, {
743
+ sessionRef: summary,
744
+ prior,
745
+ force: options.force === true,
746
+ });
825
747
  sessions.push(result);
826
748
  // #626 — triage aggregation. A session reached the triage gate only when it
827
749
  // was NOT already preempted by an earlier skip (read_failed / too_short /
@@ -916,10 +838,332 @@ export async function akmExtract(options) {
916
838
  skippedCount += 1;
917
839
  }
918
840
  finally {
919
- if (sessionLockPath)
920
- releaseLock(sessionLockPath);
841
+ if (sessionLockOwnership)
842
+ releaseLock(sessionLockOwnership);
843
+ }
844
+ }
845
+ return { sessions, processedCount, skippedCount, triageEvaluated, triagePassed, triagedOut, allProposalIds };
846
+ }
847
+ /**
848
+ * Resolve the run-scoped LLM/engine, budget, triage, and session-indexing
849
+ * settings for one extract invocation (throwing when no engine is configured).
850
+ * Extracted verbatim from `akmExtract` — the timeout precedence chain, the
851
+ * session-summary generator seam, and the default resolutions are byte-identical.
852
+ */
853
+ function resolveExtractRunConfig(options, config, extractProcess, activeProfile) {
854
+ // Improve supplies its invocation-owned connection. Standalone extract
855
+ // resolves the selected process engine, then defaults.llmEngine.
856
+ const runnerSpec = options.resolvedPlan
857
+ ? options.resolvedPlan.runner
858
+ : resolveImproveProcessRunner(activeProfile, "extract", config);
859
+ const fixedLlmConfig = options.resolvedPlan ? undefined : options.llmConfig;
860
+ if (!runnerSpec && !fixedLlmConfig) {
861
+ throw new ConfigError("No LLM engine configured for extract. Set defaults.llmEngine or improve.strategies.<name>.processes.extract.engine.", "LLM_NOT_CONFIGURED");
862
+ }
863
+ const timeoutMs = options.resolvedPlan
864
+ ? options.resolvedPlan.timeoutMs
865
+ : Object.hasOwn(options, "timeoutMs")
866
+ ? (options.timeoutMs ?? null)
867
+ : runnerSpec?.timeoutMs !== undefined
868
+ ? runnerSpec.timeoutMs
869
+ : fixedLlmConfig && Object.hasOwn(fixedLlmConfig, "timeoutMs")
870
+ ? (fixedLlmConfig.timeoutMs ?? null)
871
+ : 600_000;
872
+ const getLlmConfig = () => runnerSpec ? materializeLlmRunnerConnection(runnerSpec) : fixedLlmConfig;
873
+ // Pre-filter budget — process config can raise it for large-context models.
874
+ const maxTotalChars = typeof extractProcess?.maxTotalChars === "number" ? extractProcess.maxTotalChars : undefined;
875
+ // #595/#596 — minimum raw session size; sessions below it skip the LLM call
876
+ // entirely. Set `processes.extract.minContentChars: 0` to disable the gate.
877
+ const minContentChars = typeof extractProcess?.minContentChars === "number" ? extractProcess.minContentChars : DEFAULT_MIN_CONTENT_CHARS;
878
+ // Cap on NEW sessions LLM-processed per run; 0 disables. Absent = default.
879
+ // Bounds per-run wall time / LLM cost so a backlog can't push a run past its
880
+ // task timeout — the overflow stays unseen and is picked up by later runs.
881
+ const maxSessionsPerRun = typeof extractProcess?.maxSessionsPerRun === "number"
882
+ ? extractProcess.maxSessionsPerRun
883
+ : DEFAULT_MAX_SESSIONS_PER_RUN;
884
+ // Default discovery window — process config can override the built-in 24h.
885
+ const effectiveSince = options.since ?? extractProcess?.defaultSince;
886
+ // #626 — resolve the triage gate config once per run. Default-off → the
887
+ // per-session path never calls the scorer and emits no telemetry.
888
+ const triage = resolveTriageConfig(extractProcess);
889
+ // #561 — resolve session-indexing config. Default ON: we only reach this code
890
+ // when `session_extraction` is enabled AND an LLM is configured (both checked
891
+ // above), so defaulting on costs nothing offline (the summary call fails open)
892
+ // while making sessions searchable in the common LLM-configured case. Set
893
+ // `processes.extract.indexSessions: false` for byte-identical legacy behaviour.
894
+ const sessionIndexingEnabled = extractProcess?.indexSessions ?? true;
895
+ const minSessionDuration = typeof extractProcess?.minSessionDuration === "number"
896
+ ? extractProcess.minSessionDuration
897
+ : DEFAULT_MIN_SESSION_DURATION_MINUTES;
898
+ // Production summary generator: a bounded in-tree LLM call wrapped in the
899
+ // same fail-open `callStructured` seam as the rest of extract. Returns
900
+ // `undefined` on disablement / timeout / error so no asset is written.
901
+ // Tests inject a fake.
902
+ const defaultSessionSummaryGenerator = async (data) => {
903
+ // Same fail-open contract as the per-session call: a getLlmConfig()
904
+ // throw takes the "" fallback rather than propagating.
905
+ let summaryLlm;
906
+ try {
907
+ summaryLlm = getLlmConfig();
908
+ }
909
+ catch {
910
+ summaryLlm = undefined;
911
+ }
912
+ let raw = "";
913
+ if (summaryLlm !== undefined) {
914
+ await callStructured({
915
+ feature: "session_extraction",
916
+ akmConfig: config,
917
+ config: summaryLlm,
918
+ messages: [{ role: "user", content: buildSessionSummaryPrompt(data) }],
919
+ request: {
920
+ timeoutMs,
921
+ responseSchema: SESSION_SUMMARY_JSON_SCHEMA,
922
+ ...(options.chat ? { chat: options.chat } : {}),
923
+ },
924
+ parse: (r) => {
925
+ raw = r ?? "";
926
+ return raw;
927
+ },
928
+ onError: () => "",
929
+ fallback: "",
930
+ });
921
931
  }
932
+ return parseSessionSummary(raw);
933
+ };
934
+ const sessionIndexing = {
935
+ enabled: sessionIndexingEnabled,
936
+ minDurationMinutes: minSessionDuration,
937
+ generate: options.generateSessionSummary ?? defaultSessionSummaryGenerator,
938
+ };
939
+ return {
940
+ timeoutMs,
941
+ getLlmConfig,
942
+ maxTotalChars,
943
+ minContentChars,
944
+ maxSessionsPerRun,
945
+ effectiveSince,
946
+ triage,
947
+ sessionIndexing,
948
+ };
949
+ }
950
+ /**
951
+ * Resolve the session set to process: the single `--session-id` target (or a
952
+ * not-found envelope) or the discovery-window listing. Extracted verbatim from
953
+ * `akmExtract`; the 48h default-since floor and location filter are unchanged.
954
+ */
955
+ function discoverExtractCandidates(options, harness, effectiveSince, startMs, dryRun) {
956
+ if (options.sessionId) {
957
+ const all = harness.listSessions({
958
+ ...(options.location ? { location: options.location } : {}),
959
+ });
960
+ const target = all.find((s) => s.sessionId === options.sessionId);
961
+ if (!target) {
962
+ return {
963
+ notFound: {
964
+ schemaVersion: 1,
965
+ ok: false,
966
+ shape: "extract-result",
967
+ dryRun,
968
+ type: options.type,
969
+ sessionsProcessed: 0,
970
+ sessionsSkipped: 0,
971
+ candidatesCreated: 0,
972
+ proposals: [],
973
+ sessions: [],
974
+ warnings: [`session ${options.sessionId} not found for harness ${options.type}`],
975
+ durationMs: Date.now() - startMs,
976
+ },
977
+ };
978
+ }
979
+ return { candidates: [target] };
922
980
  }
981
+ // No explicit `--since`/`defaultSince` → default to "since the last run"
982
+ // (floored at 48h) so an intermittently-online host doesn't lose sessions
983
+ // that ended while it was off. See {@link resolveDefaultSinceMs}.
984
+ const sinceMs = effectiveSince
985
+ ? parseSinceArg(effectiveSince)
986
+ : resolveDefaultSinceMs(harness.name, startMs, {
987
+ ...(options.stateDb ? { stateDb: options.stateDb } : {}),
988
+ ...(options.stateDbPath ? { stateDbPath: options.stateDbPath } : {}),
989
+ ...(options.skipTracking ? { skipTracking: options.skipTracking } : {}),
990
+ });
991
+ return {
992
+ candidates: harness.listSessions({
993
+ sinceMs,
994
+ ...(options.location ? { location: options.location } : {}),
995
+ }),
996
+ };
997
+ }
998
+ // ── Public entrypoint ────────────────────────────────────────────────────────
999
+ /**
1000
+ * WI-9.10: build one `akm extract` run's {@link RunContext} from values
1001
+ * `akmExtract` has already resolved by the time it calls this (config,
1002
+ * stashDir, dryRun, sourceRun, and `resolveExtractRunConfig`'s own
1003
+ * `getLlmConfig`) — no second config load, no new db handle.
1004
+ *
1005
+ * `RunContext.getLlmConfig` is typed `() => LlmConnectionConfig | null`, but
1006
+ * extract's own resolved `getLlmConfig` returns `LlmProfileConfig` (a
1007
+ * superset — `supportsJsonSchema` — of `LlmConnectionConfig`) and, per its
1008
+ * documented fail-open contract, MAY THROW (`materializeLlmConnection` can
1009
+ * raise ConfigError) rather than return null; every existing caller in this
1010
+ * file wraps it in try/catch for exactly that reason. The thin closure below
1011
+ * adapts at the boundary: it derives from the SAME already-resolved
1012
+ * runner/profile (this doesn't widen `RunContext.getLlmConfig`'s type), and —
1013
+ * matching the file's own fail-open contract — coalesces a throw to `null`
1014
+ * instead of propagating.
1015
+ */
1016
+ function buildExtractRunContext(args) {
1017
+ const { options, config, stashDir, dryRun, sourceRun, getLlmConfig } = args;
1018
+ const getRunContextLlmConfig = () => {
1019
+ try {
1020
+ return getLlmConfig();
1021
+ }
1022
+ catch {
1023
+ return null;
1024
+ }
1025
+ };
1026
+ return createRunContext({
1027
+ stashDir,
1028
+ config,
1029
+ eventsCtx: options.eventsCtx ?? {},
1030
+ // Not yet wired into any proposal call site this stage (mirrors
1031
+ // buildImproveRunContext's proposalsCtx comment in improve.ts).
1032
+ proposalsCtx: options.ctx ?? {},
1033
+ getLlmConfig: getRunContextLlmConfig,
1034
+ sourceRun,
1035
+ dryRun,
1036
+ signal: options.signal,
1037
+ });
1038
+ }
1039
+ export async function akmExtract(options) {
1040
+ const startMs = Date.now();
1041
+ if (!options.type || options.type.trim() === "") {
1042
+ throw new UsageError("--type is required. Pass a harness name (e.g. --type claude-code).", "MISSING_REQUIRED_ARGUMENT");
1043
+ }
1044
+ const config = options.config ?? loadConfig();
1045
+ const stashDir = resolveRunStashDir(options.stashDir);
1046
+ const dryRun = options.dryRun ?? false;
1047
+ const sourceRun = options.sourceRun ?? `extract-${timestampForFilename()}`;
1048
+ // Read process behavior from the frozen standalone plan or the active improve
1049
+ // strategy. This prevents config changes during watch mode from changing later
1050
+ // triggers and prevents one improve strategy from overriding another.
1051
+ const activeProfile = options.improveProfile ?? (options.resolvedPlan ? undefined : resolveImproveStrategy(undefined, config).config);
1052
+ const extractProcess = options.resolvedPlan?.process ?? getImproveProcessConfig("extract", activeProfile);
1053
+ // The `extract.enabled` process toggle gates extract as a STAGE of `akm improve`
1054
+ // (the activeProfile path) — consistent with #593/#594 where the active profile,
1055
+ // not `default`, is the source of truth. An EXPLICIT `akm extract` invocation
1056
+ // (no activeProfile) is a direct user/cron action and always runs; gating it on
1057
+ // the default improve profile's stage toggle was a footgun — dropping extract
1058
+ // from the daily improve profile would silently disable the standalone command.
1059
+ const extractEnabled = options.resolvedPlan?.enabled ??
1060
+ (options.improveProfile ? resolveProcessEnabled("extract", options.improveProfile) : true);
1061
+ // Feature-gate early so we get a clean "skipped because disabled" envelope.
1062
+ if (!extractEnabled) {
1063
+ return {
1064
+ schemaVersion: 1,
1065
+ ok: true,
1066
+ shape: "extract-result",
1067
+ dryRun,
1068
+ type: options.type,
1069
+ sessionsProcessed: 0,
1070
+ sessionsSkipped: 0,
1071
+ candidatesCreated: 0,
1072
+ proposals: [],
1073
+ sessions: [],
1074
+ warnings: ["extract is disabled by the selected improve strategy"],
1075
+ durationMs: Date.now() - startMs,
1076
+ };
1077
+ }
1078
+ const { timeoutMs, getLlmConfig, maxTotalChars, minContentChars, maxSessionsPerRun, effectiveSince, triage, sessionIndexing, } = resolveExtractRunConfig(options, config, extractProcess, activeProfile);
1079
+ // WI-9.10: construct this run's RunContext (extracted to
1080
+ // buildExtractRunContext to keep akmExtract under the fn-size bar — R31).
1081
+ const ctx = buildExtractRunContext({ options, config, stashDir, dryRun, sourceRun, getLlmConfig });
1082
+ const harness = resolveHarness(options.type, options.harnesses);
1083
+ if (!harness) {
1084
+ return {
1085
+ schemaVersion: 1,
1086
+ ok: false,
1087
+ shape: "extract-result",
1088
+ dryRun,
1089
+ type: options.type,
1090
+ sessionsProcessed: 0,
1091
+ sessionsSkipped: 0,
1092
+ candidatesCreated: 0,
1093
+ proposals: [],
1094
+ sessions: [],
1095
+ warnings: [`no available harness matches type "${options.type}" (check that the platform is installed)`],
1096
+ durationMs: Date.now() - startMs,
1097
+ };
1098
+ }
1099
+ if (!harness.isAvailable()) {
1100
+ return {
1101
+ schemaVersion: 1,
1102
+ ok: false,
1103
+ shape: "extract-result",
1104
+ dryRun,
1105
+ type: options.type,
1106
+ sessionsProcessed: 0,
1107
+ sessionsSkipped: 0,
1108
+ candidatesCreated: 0,
1109
+ proposals: [],
1110
+ sessions: [],
1111
+ warnings: [`harness ${options.type} is registered but reports not-available (no session data on this machine)`],
1112
+ durationMs: Date.now() - startMs,
1113
+ };
1114
+ }
1115
+ // Decide which sessions to process: explicit sessionId OR discovery via since.
1116
+ const discovery = discoverExtractCandidates(options, harness, effectiveSince, startMs, dryRun);
1117
+ if ("notFound" in discovery)
1118
+ return discovery.notFound;
1119
+ const candidates = discovery.candidates;
1120
+ const topLevelWarnings = [];
1121
+ // Open state.db once for the run and bulk-load seen-rows for the candidate
1122
+ // set so we can decide skip/process in O(1) per session. Tracking is opt-out
1123
+ // via options.skipTracking (used by tests + one-shot debug calls).
1124
+ const trackingEnabled = options.skipTracking !== true;
1125
+ let stateDb;
1126
+ let seenMap = new Map();
1127
+ if (trackingEnabled && candidates.length > 0) {
1128
+ try {
1129
+ stateDb = options.stateDb ?? openStateDatabase(options.stateDbPath);
1130
+ seenMap = getExtractedSessionsMap(stateDb, harness.name, candidates.map((c) => c.sessionId));
1131
+ }
1132
+ catch (err) {
1133
+ // state.db open is best-effort — log and proceed without skip-tracking
1134
+ // so a transient sqlite error never blocks the actual extraction.
1135
+ const msg = err instanceof Error ? err.message : String(err);
1136
+ warn(`[extract] state.db unavailable, processing without skip-tracking: ${msg}`);
1137
+ topLevelWarnings.push(`state.db unavailable: ${msg}`);
1138
+ stateDb = undefined;
1139
+ }
1140
+ }
1141
+ // Stash authoring standards (convention/meta fact bodies) for non-wiki
1142
+ // extract output. Resolved ONCE per run and threaded into each session's
1143
+ // prompt so facts are not re-read per session.
1144
+ const extractStandardsContext = resolveExtractStandards(stashDir);
1145
+ const { sessions, processedCount, skippedCount, triageEvaluated, triagePassed, triagedOut, allProposalIds } = await runExtractSessionLoop({
1146
+ candidates,
1147
+ options,
1148
+ harness,
1149
+ seenMap,
1150
+ stateDb,
1151
+ trackingEnabled,
1152
+ dryRun,
1153
+ stashDir,
1154
+ config,
1155
+ getLlmConfig,
1156
+ chat: options.chat,
1157
+ sourceRun,
1158
+ timeoutMs,
1159
+ maxTotalChars,
1160
+ minContentChars,
1161
+ maxSessionsPerRun,
1162
+ triage,
1163
+ sessionIndexing,
1164
+ extractStandardsContext,
1165
+ topLevelWarnings,
1166
+ });
923
1167
  // Close the state.db connection we opened. Callers that injected stateDb
924
1168
  // via the test seam own its lifecycle.
925
1169
  if (stateDb && !options.stateDb) {
@@ -942,13 +1186,18 @@ export async function akmExtract(options) {
942
1186
  triagedOut,
943
1187
  sourceRun,
944
1188
  },
945
- }, options.ctx);
1189
+ }, options.eventsCtx);
946
1190
  }
947
1191
  return {
948
1192
  schemaVersion: 1,
949
1193
  ok: true,
950
1194
  shape: "extract-result",
951
- dryRun,
1195
+ // Sourced from ctx (identical value to the local `dryRun` — see the
1196
+ // RunContext construction above) so the constructed RunContext has a
1197
+ // genuine downstream reference in this verb, which currently has no
1198
+ // content-read site to route through ctx.readAsset (see the WI-9.10c
1199
+ // report).
1200
+ dryRun: ctx.dryRun,
952
1201
  type: options.type,
953
1202
  sessionsProcessed: processedCount,
954
1203
  sessionsSkipped: skippedCount,
@@ -975,8 +1224,8 @@ export async function akmExtract(options) {
975
1224
  * over-/under-count here only affects whether the pass RUNS, never whether a
976
1225
  * changed session is actually re-processed.
977
1226
  */
978
- export function countNewExtractCandidates(config, options = {}) {
979
- const extractProcess = getImproveProcessConfig(config, "extract", options.improveProfile);
1227
+ export function countNewExtractCandidates(_config, options = {}) {
1228
+ const extractProcess = getImproveProcessConfig("extract", options.improveProfile);
980
1229
  const effectiveSince = options.since ?? extractProcess?.defaultSince;
981
1230
  // Mirror akmExtract: when no explicit window is set, default per-harness to
982
1231
  // "since the last run" (floored at 48h) instead of a fixed 24h. Keeps this