akm-cli 0.8.0-rc1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/{.github/CHANGELOG.md → CHANGELOG.md} +191 -3
  2. package/README.md +22 -6
  3. package/SECURITY.md +93 -0
  4. package/dist/cli/config-migrate.js +144 -0
  5. package/dist/cli/config-validate.js +39 -0
  6. package/dist/cli/confirm.js +73 -0
  7. package/dist/cli/parse-args.js +93 -3
  8. package/dist/cli/shared.js +129 -0
  9. package/dist/cli.js +2162 -1258
  10. package/dist/commands/add-cli.js +279 -0
  11. package/dist/commands/agent-dispatch.js +20 -12
  12. package/dist/commands/agent-support.js +11 -5
  13. package/dist/commands/completions.js +3 -0
  14. package/dist/commands/config-cli.js +129 -517
  15. package/dist/commands/consolidate.js +1533 -144
  16. package/dist/commands/curate.js +44 -3
  17. package/dist/commands/db-cli.js +23 -0
  18. package/dist/commands/distill-promotion-policy.js +5 -3
  19. package/dist/commands/distill.js +906 -100
  20. package/dist/commands/env.js +213 -0
  21. package/dist/commands/eval-cases.js +3 -0
  22. package/dist/commands/events.js +3 -0
  23. package/dist/commands/extract-cli.js +127 -0
  24. package/dist/commands/extract-prompt.js +204 -0
  25. package/dist/commands/extract.js +477 -0
  26. package/dist/commands/feedback-cli.js +331 -0
  27. package/dist/commands/graph.js +260 -5
  28. package/dist/commands/health.js +977 -51
  29. package/dist/commands/help/help-accept.md +6 -3
  30. package/dist/commands/help/help-improve.md +36 -8
  31. package/dist/commands/help/help-proposals.md +7 -4
  32. package/dist/commands/help/help-reject.md +5 -2
  33. package/dist/commands/history.js +51 -16
  34. package/dist/commands/improve-auto-accept.js +97 -0
  35. package/dist/commands/improve-cli.js +236 -0
  36. package/dist/commands/improve-profiles.js +184 -0
  37. package/dist/commands/improve-result-file.js +167 -0
  38. package/dist/commands/improve.js +1725 -332
  39. package/dist/commands/info.js +3 -0
  40. package/dist/commands/init.js +49 -1
  41. package/dist/commands/installed-stashes.js +6 -23
  42. package/dist/commands/knowledge.js +3 -0
  43. package/dist/commands/lint/agent-linter.js +3 -0
  44. package/dist/commands/lint/base-linter.js +233 -5
  45. package/dist/commands/lint/command-linter.js +3 -0
  46. package/dist/commands/lint/default-linter.js +3 -0
  47. package/dist/commands/lint/env-key-rules.js +154 -0
  48. package/dist/commands/lint/index.js +92 -3
  49. package/dist/commands/lint/knowledge-linter.js +3 -0
  50. package/dist/commands/lint/markdown-insertion.js +343 -0
  51. package/dist/commands/lint/memory-linter.js +3 -0
  52. package/dist/commands/lint/registry.js +3 -0
  53. package/dist/commands/lint/skill-linter.js +3 -0
  54. package/dist/commands/lint/task-linter.js +15 -12
  55. package/dist/commands/lint/types.js +3 -0
  56. package/dist/commands/lint/workflow-linter.js +3 -0
  57. package/dist/commands/lint.js +3 -0
  58. package/dist/commands/migration-help.js +5 -2
  59. package/dist/commands/proposal-drain-policies.js +128 -0
  60. package/dist/commands/proposal-drain.js +477 -0
  61. package/dist/commands/proposal.js +60 -6
  62. package/dist/commands/propose.js +24 -19
  63. package/dist/commands/reflect.js +1004 -94
  64. package/dist/commands/registry-cli.js +150 -0
  65. package/dist/commands/registry-search.js +3 -0
  66. package/dist/commands/remember-cli.js +257 -0
  67. package/dist/commands/remember.js +15 -6
  68. package/dist/commands/schema-repair.js +88 -15
  69. package/dist/commands/search.js +99 -14
  70. package/dist/commands/secret.js +173 -0
  71. package/dist/commands/self-update.js +3 -0
  72. package/dist/commands/show.js +32 -13
  73. package/dist/commands/source-add.js +7 -35
  74. package/dist/commands/source-clone.js +3 -0
  75. package/dist/commands/source-manage.js +3 -0
  76. package/dist/commands/tasks.js +161 -95
  77. package/dist/commands/url-checker.js +3 -0
  78. package/dist/core/action-contributors.js +3 -0
  79. package/dist/core/asset-ref.js +17 -2
  80. package/dist/core/asset-registry.js +9 -2
  81. package/dist/core/asset-serialize.js +88 -0
  82. package/dist/core/asset-spec.js +61 -5
  83. package/dist/core/common.js +93 -5
  84. package/dist/core/concurrent.js +3 -0
  85. package/dist/core/config-io.js +347 -0
  86. package/dist/core/config-migration.js +622 -0
  87. package/dist/core/config-schema.js +558 -0
  88. package/dist/core/config-sources.js +108 -0
  89. package/dist/core/config-types.js +4 -0
  90. package/dist/core/config-walker.js +337 -0
  91. package/dist/core/config.js +366 -1077
  92. package/dist/core/errors.js +42 -20
  93. package/dist/core/events.js +31 -25
  94. package/dist/core/file-lock.js +104 -0
  95. package/dist/core/frontmatter.js +75 -10
  96. package/dist/core/lesson-lint.js +3 -0
  97. package/dist/core/markdown.js +3 -0
  98. package/dist/core/memory-belief.js +62 -0
  99. package/dist/core/memory-contradiction-detect.js +274 -0
  100. package/dist/core/memory-improve.js +142 -14
  101. package/dist/core/parse.js +3 -0
  102. package/dist/core/paths.js +218 -50
  103. package/dist/core/proposal-quality-validators.js +380 -0
  104. package/dist/core/proposal-validators.js +11 -3
  105. package/dist/core/proposals.js +464 -5
  106. package/dist/core/state-db.js +349 -56
  107. package/dist/core/text-truncation.js +107 -0
  108. package/dist/core/time.js +3 -0
  109. package/dist/core/tty.js +59 -0
  110. package/dist/core/warn.js +7 -2
  111. package/dist/core/write-source.js +12 -0
  112. package/dist/indexer/db-backup.js +391 -0
  113. package/dist/indexer/db-search.js +136 -28
  114. package/dist/indexer/db.js +662 -166
  115. package/dist/indexer/ensure-index.js +3 -0
  116. package/dist/indexer/file-context.js +3 -0
  117. package/dist/indexer/graph-boost.js +162 -40
  118. package/dist/indexer/graph-db.js +241 -51
  119. package/dist/indexer/graph-dedup.js +3 -7
  120. package/dist/indexer/graph-extraction.js +242 -149
  121. package/dist/indexer/index-context.js +3 -9
  122. package/dist/indexer/indexer.js +84 -14
  123. package/dist/indexer/llm-cache.js +24 -19
  124. package/dist/indexer/manifest.js +3 -0
  125. package/dist/indexer/matchers.js +184 -11
  126. package/dist/indexer/memory-inference.js +94 -50
  127. package/dist/indexer/metadata-contributors.js +3 -0
  128. package/dist/indexer/metadata.js +114 -48
  129. package/dist/indexer/path-resolver.js +3 -0
  130. package/dist/indexer/project-context.js +192 -0
  131. package/dist/indexer/ranking-contributors.js +134 -7
  132. package/dist/indexer/ranking.js +8 -1
  133. package/dist/indexer/search-fields.js +5 -9
  134. package/dist/indexer/search-hit-enrichers.js +91 -2
  135. package/dist/indexer/search-source.js +20 -1
  136. package/dist/indexer/semantic-status.js +4 -1
  137. package/dist/indexer/staleness-detect.js +447 -0
  138. package/dist/indexer/usage-events.js +12 -9
  139. package/dist/indexer/walker.js +3 -0
  140. package/dist/integrations/agent/builders.js +135 -0
  141. package/dist/integrations/agent/config.js +121 -401
  142. package/dist/integrations/agent/detect.js +3 -0
  143. package/dist/integrations/agent/index.js +6 -14
  144. package/dist/integrations/agent/model-aliases.js +55 -0
  145. package/dist/integrations/agent/profiles.js +3 -0
  146. package/dist/integrations/agent/prompts.js +137 -8
  147. package/dist/integrations/agent/runner.js +208 -0
  148. package/dist/integrations/agent/sdk-runner.js +8 -2
  149. package/dist/integrations/agent/spawn.js +54 -14
  150. package/dist/integrations/github.js +3 -0
  151. package/dist/integrations/lockfile.js +22 -51
  152. package/dist/integrations/session-logs/index.js +4 -0
  153. package/dist/integrations/session-logs/inline-refs.js +35 -0
  154. package/dist/integrations/session-logs/pre-filter.js +152 -0
  155. package/dist/integrations/session-logs/providers/claude-code.js +226 -0
  156. package/dist/integrations/session-logs/providers/opencode.js +231 -25
  157. package/dist/integrations/session-logs/types.js +3 -0
  158. package/dist/llm/call-ai.js +14 -26
  159. package/dist/llm/client.js +16 -2
  160. package/dist/llm/embedder.js +20 -29
  161. package/dist/llm/embedders/cache.js +3 -7
  162. package/dist/llm/embedders/local.js +42 -1
  163. package/dist/llm/embedders/remote.js +20 -8
  164. package/dist/llm/embedders/types.js +3 -7
  165. package/dist/llm/feature-gate.js +92 -56
  166. package/dist/llm/graph-extract.js +401 -30
  167. package/dist/llm/index-passes.js +44 -29
  168. package/dist/llm/memory-infer.js +30 -2
  169. package/dist/llm/metadata-enhance.js +3 -7
  170. package/dist/llm/prompts/extract-session.md +80 -0
  171. package/dist/llm/prompts/graph-extract-user-prompt.md +24 -1
  172. package/dist/output/cli-hints-full.md +60 -32
  173. package/dist/output/cli-hints-short.md +10 -7
  174. package/dist/output/cli-hints.js +5 -2
  175. package/dist/output/context.js +60 -8
  176. package/dist/output/renderers.js +170 -194
  177. package/dist/output/shapes/curate.js +56 -0
  178. package/dist/output/shapes/distill.js +10 -0
  179. package/dist/output/shapes/env-list.js +19 -0
  180. package/dist/output/shapes/events.js +11 -0
  181. package/dist/output/shapes/helpers.js +424 -0
  182. package/dist/output/shapes/history.js +7 -0
  183. package/dist/output/shapes/passthrough.js +105 -0
  184. package/dist/output/shapes/proposal-accept.js +7 -0
  185. package/dist/output/shapes/proposal-diff.js +7 -0
  186. package/dist/output/shapes/proposal-list.js +7 -0
  187. package/dist/output/shapes/proposal-producer.js +11 -0
  188. package/dist/output/shapes/proposal-reject.js +7 -0
  189. package/dist/output/shapes/proposal-show.js +7 -0
  190. package/dist/output/shapes/registry-search.js +6 -0
  191. package/dist/output/shapes/registry.js +30 -0
  192. package/dist/output/shapes/search.js +6 -0
  193. package/dist/output/shapes/secret-list.js +19 -0
  194. package/dist/output/shapes/show.js +6 -0
  195. package/dist/output/shapes/vault-list.js +19 -0
  196. package/dist/output/shapes.js +51 -549
  197. package/dist/output/text/add.js +6 -0
  198. package/dist/output/text/clone.js +6 -0
  199. package/dist/output/text/config.js +6 -0
  200. package/dist/output/text/curate.js +6 -0
  201. package/dist/output/text/distill.js +7 -0
  202. package/dist/output/text/enable-disable.js +7 -0
  203. package/dist/output/text/events.js +10 -0
  204. package/dist/output/text/feedback.js +6 -0
  205. package/dist/output/text/helpers.js +1059 -0
  206. package/dist/output/text/history.js +7 -0
  207. package/dist/output/text/import.js +6 -0
  208. package/dist/output/text/index.js +6 -0
  209. package/dist/output/text/info.js +6 -0
  210. package/dist/output/text/init.js +6 -0
  211. package/dist/output/text/list.js +6 -0
  212. package/dist/output/text/proposal-producer.js +8 -0
  213. package/dist/output/text/proposal.js +12 -0
  214. package/dist/output/text/registry-commands.js +11 -0
  215. package/dist/output/text/registry.js +30 -0
  216. package/dist/output/text/remember.js +6 -0
  217. package/dist/output/text/remove.js +6 -0
  218. package/dist/output/text/save.js +6 -0
  219. package/dist/output/text/search.js +6 -0
  220. package/dist/output/text/show.js +6 -0
  221. package/dist/output/text/update.js +6 -0
  222. package/dist/output/text/upgrade.js +6 -0
  223. package/dist/output/text/vault.js +16 -0
  224. package/dist/output/text/wiki.js +15 -0
  225. package/dist/output/text/workflow.js +14 -0
  226. package/dist/output/text.js +44 -1329
  227. package/dist/registry/build-index.js +3 -0
  228. package/dist/registry/create-provider-registry.js +3 -0
  229. package/dist/registry/factory.js +4 -1
  230. package/dist/registry/origin-resolve.js +3 -0
  231. package/dist/registry/providers/index.js +3 -0
  232. package/dist/registry/providers/skills-sh.js +11 -2
  233. package/dist/registry/providers/static-index.js +10 -1
  234. package/dist/registry/providers/types.js +3 -24
  235. package/dist/registry/resolve.js +11 -16
  236. package/dist/registry/types.js +3 -0
  237. package/dist/scripts/migrate-storage.js +17767 -0
  238. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
  239. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  240. package/dist/setup/detect.js +3 -0
  241. package/dist/setup/ripgrep-install.js +3 -0
  242. package/dist/setup/ripgrep-resolve.js +3 -0
  243. package/dist/setup/setup.js +306 -67
  244. package/dist/setup/steps.js +3 -15
  245. package/dist/sources/include.js +3 -0
  246. package/dist/sources/provider-factory.js +3 -11
  247. package/dist/sources/provider.js +3 -20
  248. package/dist/sources/providers/filesystem.js +19 -23
  249. package/dist/sources/providers/git.js +171 -21
  250. package/dist/sources/providers/index.js +3 -0
  251. package/dist/sources/providers/install-types.js +3 -13
  252. package/dist/sources/providers/npm.js +3 -4
  253. package/dist/sources/providers/provider-utils.js +3 -0
  254. package/dist/sources/providers/sync-from-ref.js +3 -11
  255. package/dist/sources/providers/tar-utils.js +3 -0
  256. package/dist/sources/providers/website.js +18 -22
  257. package/dist/sources/resolve.js +3 -0
  258. package/dist/sources/types.js +3 -0
  259. package/dist/sources/website-ingest.js +3 -0
  260. package/dist/tasks/backends/cron.js +3 -0
  261. package/dist/tasks/backends/exec-utils.js +3 -0
  262. package/dist/tasks/backends/index.js +3 -11
  263. package/dist/tasks/backends/launchd.js +3 -0
  264. package/dist/tasks/backends/schtasks.js +3 -0
  265. package/dist/tasks/parser.js +51 -38
  266. package/dist/tasks/resolveAkmBin.js +3 -0
  267. package/dist/tasks/runner.js +35 -9
  268. package/dist/tasks/schedule.js +20 -1
  269. package/dist/tasks/schema.js +5 -3
  270. package/dist/tasks/validator.js +6 -3
  271. package/dist/version.js +3 -0
  272. package/dist/wiki/wiki-templates.js +3 -0
  273. package/dist/wiki/wiki.js +3 -0
  274. package/dist/workflows/authoring.js +3 -0
  275. package/dist/workflows/cli.js +3 -0
  276. package/dist/workflows/db.js +140 -10
  277. package/dist/workflows/document-cache.js +3 -10
  278. package/dist/workflows/parser.js +3 -0
  279. package/dist/workflows/renderer.js +3 -0
  280. package/dist/workflows/runs.js +18 -1
  281. package/dist/workflows/schema.js +3 -0
  282. package/dist/workflows/scope-key.js +3 -0
  283. package/dist/workflows/validator.js +5 -9
  284. package/docs/README.md +7 -2
  285. package/docs/data-and-telemetry.md +225 -0
  286. package/docs/migration/release-notes/0.7.5.md +2 -2
  287. package/docs/migration/release-notes/0.8.0.md +57 -5
  288. package/docs/migration/v0.7-to-v0.8.md +1378 -0
  289. package/package.json +28 -11
  290. package/.github/LICENSE +0 -374
  291. package/dist/commands/install-audit.js +0 -385
  292. package/dist/commands/vault.js +0 -307
  293. package/dist/indexer/match-contributors.js +0 -141
  294. package/dist/integrations/agent/pipeline.js +0 -39
  295. package/dist/integrations/agent/runners.js +0 -31
@@ -0,0 +1,477 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import { stringify as yamlStringify } from "yaml";
5
+ import { assembleAssetFromString } from "../core/asset-serialize";
6
+ import { resolveStashDir, timestampForFilename } from "../core/common";
7
+ import { getDefaultLlmConfig, loadConfig } from "../core/config";
8
+ import { ConfigError, UsageError } from "../core/errors";
9
+ import { appendEvent } from "../core/events";
10
+ import { createProposal, isProposalSkipped } from "../core/proposals";
11
+ import { getExtractedSessionsMap, openStateDatabase, shouldSkipAlreadyExtractedSession, upsertExtractedSession, } from "../core/state-db";
12
+ import { warn } from "../core/warn";
13
+ import { resolveImproveProcessRunnerFromProfile } from "../integrations/agent/runner";
14
+ import { getAvailableHarnesses } from "../integrations/session-logs";
15
+ import { preFilterSession } from "../integrations/session-logs/pre-filter";
16
+ import { chatCompletion } from "../llm/client";
17
+ import { isLlmFeatureEnabled, tryLlmFeature } from "../llm/feature-gate";
18
+ import { buildExtractPrompt, EXTRACT_JSON_SCHEMA, parseExtractPayload } from "./extract-prompt";
19
+ // ── Helpers ──────────────────────────────────────────────────────────────────
20
+ /**
21
+ * Parse a since-string into an absolute ms-epoch cutoff. Accepts:
22
+ * - ISO timestamps (parsed via Date.parse)
23
+ * - Relative durations: `<n>m`, `<n>h`, `<n>d` (minutes / hours / days)
24
+ *
25
+ * Throws UsageError on unparseable input so the CLI surfaces a clear error
26
+ * rather than silently defaulting.
27
+ */
28
+ export function parseSinceArg(value, now = Date.now()) {
29
+ if (!value || value.trim() === "") {
30
+ return now - 24 * 60 * 60 * 1000; // default: 24h
31
+ }
32
+ const trimmed = value.trim();
33
+ const relMatch = trimmed.match(/^(\d+)\s*([mhd])$/i);
34
+ if (relMatch) {
35
+ const n = Number.parseInt(relMatch[1] ?? "0", 10);
36
+ const unit = (relMatch[2] ?? "h").toLowerCase();
37
+ const ms = unit === "m" ? n * 60_000 : unit === "h" ? n * 3_600_000 : n * 86_400_000;
38
+ return now - ms;
39
+ }
40
+ const iso = Date.parse(trimmed);
41
+ if (!Number.isNaN(iso))
42
+ return iso;
43
+ throw new UsageError(`--since value "${value}" could not be parsed (expected ISO timestamp or duration like 24h / 7d / 30m)`, "INVALID_FLAG_VALUE");
44
+ }
45
+ /**
46
+ * Resolve a harness instance for the given type, either from the explicit
47
+ * `harnesses` seam or the {@link getAvailableHarnesses} registry. Returns
48
+ * `undefined` when no harness matches (the caller surfaces that as a warning).
49
+ */
50
+ function resolveHarness(type, harnesses) {
51
+ const pool = harnesses ?? getAvailableHarnesses();
52
+ return pool.find((h) => h.name === type);
53
+ }
54
+ /**
55
+ * Build the ref + content for a candidate. The body must contain a
56
+ * frontmatter block carrying `description` (and `when_to_use` for lessons)
57
+ * so the accept-time descriptionQualityValidator passes — same pattern as
58
+ * the consolidate-writer fix at consolidate.ts.
59
+ */
60
+ function buildCandidateProposal(candidate, sourceRef) {
61
+ const ref = `${candidate.type}:${candidate.name}`;
62
+ const fm = {
63
+ description: candidate.description,
64
+ sources: [`session:${sourceRef.harness}:${sourceRef.sessionId}`],
65
+ };
66
+ if (candidate.type === "lesson" && candidate.when_to_use) {
67
+ fm.when_to_use = candidate.when_to_use;
68
+ }
69
+ const serialized = yamlStringify(fm).trimEnd();
70
+ const content = assembleAssetFromString(serialized, candidate.body);
71
+ return { ref, content };
72
+ }
73
+ /**
74
+ * Process one session through the full pipeline: read → pre-filter → LLM →
75
+ * parse → createProposal-per-candidate. Returns the per-session result.
76
+ *
77
+ * On any non-fatal failure (LLM error, unparseable response, individual
78
+ * proposal validation failure) the session result records a warning and
79
+ * keeps going — one session's bad luck never aborts a multi-session run.
80
+ */
81
+ async function processSession(harness, sessionRef, stashDir, config, llmConfig, chat, ctx, sourceRun, dryRun, timeoutMs, maxTotalChars) {
82
+ const warnings = [];
83
+ let data;
84
+ try {
85
+ data = harness.readSession(sessionRef);
86
+ }
87
+ catch (err) {
88
+ return {
89
+ sessionId: sessionRef.sessionId,
90
+ harness: harness.name,
91
+ candidateCount: 0,
92
+ proposalIds: [],
93
+ preFilter: { inputCount: 0, outputCount: 0, truncatedCount: 0 },
94
+ warnings: [`readSession failed: ${err instanceof Error ? err.message : String(err)}`],
95
+ skipped: true,
96
+ skipReason: "read_failed",
97
+ };
98
+ }
99
+ const filtered = preFilterSession(data, {
100
+ ...(typeof maxTotalChars === "number" ? { maxTotalChars } : {}),
101
+ });
102
+ const prompt = buildExtractPrompt({ data, events: filtered.events, inlineRefs: data.inlineRefs });
103
+ let llmRaw = "";
104
+ const llmResult = await tryLlmFeature("session_extraction", config, async () => {
105
+ llmRaw = await chat(llmConfig, [{ role: "user", content: prompt }], {
106
+ timeoutMs,
107
+ responseSchema: EXTRACT_JSON_SCHEMA,
108
+ });
109
+ return llmRaw;
110
+ }, "", { timeoutMs });
111
+ if (llmResult === "" && !llmRaw) {
112
+ // tryLlmFeature took the fallback path (disabled / timeout / error). Return skipped.
113
+ return {
114
+ sessionId: sessionRef.sessionId,
115
+ harness: harness.name,
116
+ candidateCount: 0,
117
+ proposalIds: [],
118
+ preFilter: {
119
+ inputCount: filtered.stats.inputCount,
120
+ outputCount: filtered.stats.outputCount,
121
+ truncatedCount: filtered.stats.truncatedCount,
122
+ },
123
+ warnings: ["session_extraction feature returned empty (disabled / timeout / error)"],
124
+ skipped: true,
125
+ skipReason: "llm_unavailable",
126
+ };
127
+ }
128
+ const payload = parseExtractPayload(llmRaw);
129
+ const proposalIds = [];
130
+ if (payload.candidates.length === 0) {
131
+ appendEvent({
132
+ eventType: "extract_invoked",
133
+ ref: `session:${harness.name}:${sessionRef.sessionId}`,
134
+ metadata: {
135
+ outcome: "no_candidates",
136
+ sessionId: sessionRef.sessionId,
137
+ harness: harness.name,
138
+ sourceRun,
139
+ rationale: payload.rationale_if_empty,
140
+ preFilterInput: filtered.stats.inputCount,
141
+ preFilterOutput: filtered.stats.outputCount,
142
+ },
143
+ }, ctx);
144
+ return {
145
+ sessionId: sessionRef.sessionId,
146
+ harness: harness.name,
147
+ candidateCount: 0,
148
+ proposalIds: [],
149
+ ...(payload.rationale_if_empty ? { rationaleIfEmpty: payload.rationale_if_empty } : {}),
150
+ preFilter: {
151
+ inputCount: filtered.stats.inputCount,
152
+ outputCount: filtered.stats.outputCount,
153
+ truncatedCount: filtered.stats.truncatedCount,
154
+ },
155
+ warnings,
156
+ };
157
+ }
158
+ for (const candidate of payload.candidates) {
159
+ if (dryRun) {
160
+ proposalIds.push(`dry-run:${candidate.type}:${candidate.name}`);
161
+ continue;
162
+ }
163
+ try {
164
+ const { ref, content } = buildCandidateProposal(candidate, sessionRef);
165
+ const result = createProposal(stashDir, {
166
+ ref,
167
+ source: "extract",
168
+ sourceRun,
169
+ payload: {
170
+ content,
171
+ frontmatter: {
172
+ description: candidate.description,
173
+ ...(candidate.when_to_use ? { when_to_use: candidate.when_to_use } : {}),
174
+ ...(typeof candidate.confidence === "number" ? { confidence: candidate.confidence } : {}),
175
+ sources: [`session:${sessionRef.harness}:${sessionRef.sessionId}`],
176
+ evidence: candidate.evidence,
177
+ },
178
+ },
179
+ }, ctx);
180
+ if (isProposalSkipped(result)) {
181
+ warnings.push(`candidate ${candidate.type}:${candidate.name} skipped: ${result.reason}: ${result.message}`);
182
+ }
183
+ else {
184
+ proposalIds.push(result.id);
185
+ }
186
+ }
187
+ catch (err) {
188
+ warnings.push(`candidate ${candidate.type}:${candidate.name} failed: ${err instanceof Error ? err.message : String(err)}`);
189
+ }
190
+ }
191
+ appendEvent({
192
+ eventType: "extract_invoked",
193
+ ref: `session:${harness.name}:${sessionRef.sessionId}`,
194
+ metadata: {
195
+ outcome: "candidates_queued",
196
+ sessionId: sessionRef.sessionId,
197
+ harness: harness.name,
198
+ sourceRun,
199
+ candidateCount: payload.candidates.length,
200
+ proposalCount: proposalIds.length,
201
+ preFilterInput: filtered.stats.inputCount,
202
+ preFilterOutput: filtered.stats.outputCount,
203
+ },
204
+ }, ctx);
205
+ return {
206
+ sessionId: sessionRef.sessionId,
207
+ harness: harness.name,
208
+ candidateCount: payload.candidates.length,
209
+ proposalIds,
210
+ preFilter: {
211
+ inputCount: filtered.stats.inputCount,
212
+ outputCount: filtered.stats.outputCount,
213
+ truncatedCount: filtered.stats.truncatedCount,
214
+ },
215
+ warnings,
216
+ };
217
+ }
218
+ // ── Public entrypoint ────────────────────────────────────────────────────────
219
+ export async function akmExtract(options) {
220
+ const startMs = Date.now();
221
+ if (!options.type || options.type.trim() === "") {
222
+ throw new UsageError("--type is required. Pass a harness name (e.g. --type claude-code).", "MISSING_REQUIRED_ARGUMENT");
223
+ }
224
+ const config = options.config ?? loadConfig();
225
+ const stashDir = options.stashDir ?? resolveStashDir();
226
+ const dryRun = options.dryRun ?? false;
227
+ const sourceRun = options.sourceRun ?? `extract-${timestampForFilename()}`;
228
+ // Read the per-process extract config from the active improve profile. Matches
229
+ // the pattern reflect/distill/consolidate use: `profiles.improve.<active>.processes.extract`.
230
+ // Only the `default` improve profile is consulted here — extract isn't invoked
231
+ // with a profile flag yet (parity item for a future change).
232
+ const extractProcess = config.profiles?.improve?.default?.processes?.extract;
233
+ // Feature-gate early so we get a clean "skipped because disabled" envelope.
234
+ if (!isLlmFeatureEnabled(config, "session_extraction")) {
235
+ return {
236
+ schemaVersion: 1,
237
+ ok: true,
238
+ shape: "extract-result",
239
+ dryRun,
240
+ type: options.type,
241
+ sessionsProcessed: 0,
242
+ sessionsSkipped: 0,
243
+ candidatesCreated: 0,
244
+ proposals: [],
245
+ sessions: [],
246
+ warnings: [
247
+ "session_extraction feature disabled — set profiles.improve.default.processes.extract.enabled: true to use",
248
+ ],
249
+ durationMs: Date.now() - startMs,
250
+ };
251
+ }
252
+ // Resolve the LLM connection. Priority order:
253
+ // 1. Options.config.profiles.improve.default.processes.extract.profile
254
+ // (per-process override, matches reflect/distill/consolidate)
255
+ // 2. config.defaults.llm (the default LLM profile)
256
+ // 3. throw — extract requires an LLM.
257
+ let llmConfig;
258
+ const runnerSpec = resolveImproveProcessRunnerFromProfile(extractProcess, config);
259
+ if (runnerSpec) {
260
+ if (runnerSpec.kind !== "llm") {
261
+ throw new ConfigError(`Extract only supports mode: "llm" (in-tree LLM call). Got mode: "${runnerSpec.kind}" from profiles.improve.default.processes.extract — change it to "llm" or remove the override.`, "INVALID_CONFIG_FILE");
262
+ }
263
+ llmConfig = runnerSpec.connection;
264
+ }
265
+ else {
266
+ llmConfig = getDefaultLlmConfig(config) ?? undefined;
267
+ }
268
+ if (!llmConfig) {
269
+ 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.");
270
+ }
271
+ // Honor per-process timeoutMs override; fall back to options.timeoutMs; then 60s.
272
+ const timeoutMs = options.timeoutMs ??
273
+ (typeof extractProcess?.timeoutMs === "number" ? extractProcess.timeoutMs : undefined) ??
274
+ 60_000;
275
+ // Pre-filter budget — process config can raise it for large-context models.
276
+ const maxTotalChars = typeof extractProcess?.maxTotalChars === "number" ? extractProcess.maxTotalChars : undefined;
277
+ // Default discovery window — process config can override the built-in 24h.
278
+ const effectiveSince = options.since ?? extractProcess?.defaultSince;
279
+ const harness = resolveHarness(options.type, options.harnesses);
280
+ if (!harness) {
281
+ return {
282
+ schemaVersion: 1,
283
+ ok: false,
284
+ shape: "extract-result",
285
+ dryRun,
286
+ type: options.type,
287
+ sessionsProcessed: 0,
288
+ sessionsSkipped: 0,
289
+ candidatesCreated: 0,
290
+ proposals: [],
291
+ sessions: [],
292
+ warnings: [`no available harness matches type "${options.type}" (check that the platform is installed)`],
293
+ durationMs: Date.now() - startMs,
294
+ };
295
+ }
296
+ if (!harness.isAvailable()) {
297
+ return {
298
+ schemaVersion: 1,
299
+ ok: false,
300
+ shape: "extract-result",
301
+ dryRun,
302
+ type: options.type,
303
+ sessionsProcessed: 0,
304
+ sessionsSkipped: 0,
305
+ candidatesCreated: 0,
306
+ proposals: [],
307
+ sessions: [],
308
+ warnings: [`harness ${options.type} is registered but reports not-available (no session data on this machine)`],
309
+ durationMs: Date.now() - startMs,
310
+ };
311
+ }
312
+ // Decide which sessions to process: explicit sessionId OR discovery via since.
313
+ let candidates;
314
+ if (options.sessionId) {
315
+ const all = harness.listSessions({
316
+ ...(options.location ? { location: options.location } : {}),
317
+ });
318
+ const target = all.find((s) => s.sessionId === options.sessionId);
319
+ if (!target) {
320
+ return {
321
+ schemaVersion: 1,
322
+ ok: false,
323
+ shape: "extract-result",
324
+ dryRun,
325
+ type: options.type,
326
+ sessionsProcessed: 0,
327
+ sessionsSkipped: 0,
328
+ candidatesCreated: 0,
329
+ proposals: [],
330
+ sessions: [],
331
+ warnings: [`session ${options.sessionId} not found for harness ${options.type}`],
332
+ durationMs: Date.now() - startMs,
333
+ };
334
+ }
335
+ candidates = [target];
336
+ }
337
+ else {
338
+ const sinceMs = parseSinceArg(effectiveSince);
339
+ candidates = harness.listSessions({
340
+ sinceMs,
341
+ ...(options.location ? { location: options.location } : {}),
342
+ });
343
+ }
344
+ const sessions = [];
345
+ let processedCount = 0;
346
+ let skippedCount = 0;
347
+ const allProposalIds = [];
348
+ const topLevelWarnings = [];
349
+ const chat = options.chat ?? chatCompletion;
350
+ // Open state.db once for the run and bulk-load seen-rows for the candidate
351
+ // set so we can decide skip/process in O(1) per session. Tracking is opt-out
352
+ // via options.skipTracking (used by tests + one-shot debug calls).
353
+ const trackingEnabled = options.skipTracking !== true;
354
+ let stateDb;
355
+ let seenMap = new Map();
356
+ if (trackingEnabled && candidates.length > 0) {
357
+ try {
358
+ stateDb = options.stateDb ?? openStateDatabase();
359
+ seenMap = getExtractedSessionsMap(stateDb, harness.name, candidates.map((c) => c.sessionId));
360
+ }
361
+ catch (err) {
362
+ // state.db open is best-effort — log and proceed without skip-tracking
363
+ // so a transient sqlite error never blocks the actual extraction.
364
+ const msg = err instanceof Error ? err.message : String(err);
365
+ warn(`[extract] state.db unavailable, processing without skip-tracking: ${msg}`);
366
+ topLevelWarnings.push(`state.db unavailable: ${msg}`);
367
+ stateDb = undefined;
368
+ }
369
+ }
370
+ for (const summary of candidates) {
371
+ // Skip-tracking: if this session was already processed AND no new events
372
+ // have arrived since (live endedAt <= recorded endedAt), don't burn an LLM
373
+ // call. --force or single-session mode (explicit sessionId) bypasses.
374
+ const prior = seenMap.get(summary.sessionId);
375
+ if (!options.force && !options.sessionId && shouldSkipAlreadyExtractedSession(prior, summary.endedAt)) {
376
+ sessions.push({
377
+ sessionId: summary.sessionId,
378
+ harness: harness.name,
379
+ candidateCount: 0,
380
+ proposalIds: [],
381
+ preFilter: { inputCount: 0, outputCount: 0, truncatedCount: 0 },
382
+ warnings: [
383
+ `already extracted at ${prior?.processed_at}; pass --force to re-process or wait until the session has new content`,
384
+ ],
385
+ skipped: true,
386
+ skipReason: "already_extracted",
387
+ });
388
+ skippedCount += 1;
389
+ continue;
390
+ }
391
+ try {
392
+ const result = await processSession(harness, summary, stashDir, config, llmConfig, chat, options.ctx, sourceRun, dryRun, timeoutMs, maxTotalChars);
393
+ sessions.push(result);
394
+ if (result.skipped)
395
+ skippedCount += 1;
396
+ else
397
+ processedCount += 1;
398
+ allProposalIds.push(...result.proposalIds);
399
+ // Persist outcome so the next run skips this session unless new events
400
+ // arrive. We only track non-dry-run paths — dry-run is for inspection
401
+ // and should never poison the seen-table.
402
+ if (trackingEnabled && stateDb && !dryRun) {
403
+ try {
404
+ const outcome = result.skipped
405
+ ? result.skipReason === "read_failed" || result.skipReason === "exception"
406
+ ? "failed"
407
+ : "skipped"
408
+ : result.candidateCount === 0
409
+ ? "no_candidates"
410
+ : "candidates_queued";
411
+ upsertExtractedSession(stateDb, {
412
+ harness: harness.name,
413
+ sessionId: summary.sessionId,
414
+ processedAt: new Date().toISOString(),
415
+ sessionEndedAt: summary.endedAt ?? null,
416
+ outcome,
417
+ candidateCount: result.candidateCount,
418
+ proposalCount: result.proposalIds.length,
419
+ rationale: result.rationaleIfEmpty ?? null,
420
+ sourceRun,
421
+ metadata: {
422
+ preFilterInputCount: result.preFilter.inputCount,
423
+ preFilterOutputCount: result.preFilter.outputCount,
424
+ preFilterTruncatedCount: result.preFilter.truncatedCount,
425
+ ...(result.skipReason ? { skipReason: result.skipReason } : {}),
426
+ },
427
+ });
428
+ }
429
+ catch (err) {
430
+ // Tracking failure must not abort the run — log + continue.
431
+ const msg = err instanceof Error ? err.message : String(err);
432
+ warn(`[extract] failed to record session ${summary.sessionId} in state.db: ${msg}`);
433
+ }
434
+ }
435
+ }
436
+ catch (err) {
437
+ const msg = err instanceof Error ? err.message : String(err);
438
+ warn(`[extract] session ${summary.sessionId} threw: ${msg}`);
439
+ topLevelWarnings.push(`session ${summary.sessionId} threw: ${msg}`);
440
+ sessions.push({
441
+ sessionId: summary.sessionId,
442
+ harness: harness.name,
443
+ candidateCount: 0,
444
+ proposalIds: [],
445
+ preFilter: { inputCount: 0, outputCount: 0, truncatedCount: 0 },
446
+ warnings: [msg],
447
+ skipped: true,
448
+ skipReason: "exception",
449
+ });
450
+ skippedCount += 1;
451
+ }
452
+ }
453
+ // Close the state.db connection we opened. Callers that injected stateDb
454
+ // via the test seam own its lifecycle.
455
+ if (stateDb && !options.stateDb) {
456
+ try {
457
+ stateDb.close();
458
+ }
459
+ catch {
460
+ // best-effort close
461
+ }
462
+ }
463
+ return {
464
+ schemaVersion: 1,
465
+ ok: true,
466
+ shape: "extract-result",
467
+ dryRun,
468
+ type: options.type,
469
+ sessionsProcessed: processedCount,
470
+ sessionsSkipped: skippedCount,
471
+ candidatesCreated: allProposalIds.length,
472
+ proposals: allProposalIds,
473
+ sessions,
474
+ warnings: topLevelWarnings,
475
+ durationMs: Date.now() - startMs,
476
+ };
477
+ }