akm-cli 0.7.4 → 0.8.0-rc.10

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 (300) hide show
  1. package/CHANGELOG.md +224 -1
  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 +133 -0
  8. package/dist/cli/shared.js +129 -0
  9. package/dist/cli.js +2631 -1440
  10. package/dist/commands/add-cli.js +279 -0
  11. package/dist/commands/agent-dispatch.js +110 -0
  12. package/dist/commands/agent-support.js +68 -0
  13. package/dist/commands/completions.js +3 -0
  14. package/dist/commands/config-cli.js +130 -534
  15. package/dist/commands/consolidate.js +2122 -0
  16. package/dist/commands/curate.js +45 -3
  17. package/dist/commands/db-cli.js +23 -0
  18. package/dist/commands/distill-promotion-policy.js +660 -0
  19. package/dist/commands/distill.js +1081 -73
  20. package/dist/commands/env.js +213 -0
  21. package/dist/commands/eval-cases.js +43 -0
  22. package/dist/commands/events.js +15 -24
  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 +477 -0
  28. package/dist/commands/health.js +1302 -0
  29. package/dist/commands/help/help-accept.md +12 -0
  30. package/dist/commands/help/help-improve.md +69 -0
  31. package/dist/commands/help/help-proposals.md +18 -0
  32. package/dist/commands/help/help-propose.md +17 -0
  33. package/dist/commands/help/help-reject.md +11 -0
  34. package/dist/commands/history.js +54 -46
  35. package/dist/commands/improve-auto-accept.js +97 -0
  36. package/dist/commands/improve-cli.js +217 -0
  37. package/dist/commands/improve-profiles.js +166 -0
  38. package/dist/commands/improve-result-file.js +167 -0
  39. package/dist/commands/improve.js +2373 -0
  40. package/dist/commands/info.js +5 -2
  41. package/dist/commands/init.js +50 -2
  42. package/dist/commands/installed-stashes.js +102 -139
  43. package/dist/commands/knowledge.js +136 -0
  44. package/dist/commands/lint/agent-linter.js +49 -0
  45. package/dist/commands/lint/base-linter.js +479 -0
  46. package/dist/commands/lint/command-linter.js +49 -0
  47. package/dist/commands/lint/default-linter.js +16 -0
  48. package/dist/commands/lint/env-key-rules.js +154 -0
  49. package/dist/commands/lint/index.js +196 -0
  50. package/dist/commands/lint/knowledge-linter.js +16 -0
  51. package/dist/commands/lint/markdown-insertion.js +343 -0
  52. package/dist/commands/lint/memory-linter.js +61 -0
  53. package/dist/commands/lint/registry.js +36 -0
  54. package/dist/commands/lint/skill-linter.js +45 -0
  55. package/dist/commands/lint/task-linter.js +50 -0
  56. package/dist/commands/lint/types.js +4 -0
  57. package/dist/commands/lint/workflow-linter.js +56 -0
  58. package/dist/commands/lint.js +4 -0
  59. package/dist/commands/migration-help.js +3 -0
  60. package/dist/commands/proposal.js +67 -12
  61. package/dist/commands/propose.js +120 -45
  62. package/dist/commands/reflect.js +1104 -60
  63. package/dist/commands/registry-cli.js +150 -0
  64. package/dist/commands/registry-search.js +5 -2
  65. package/dist/commands/remember-cli.js +257 -0
  66. package/dist/commands/remember.js +70 -7
  67. package/dist/commands/schema-repair.js +203 -0
  68. package/dist/commands/search.js +115 -14
  69. package/dist/commands/secret.js +173 -0
  70. package/dist/commands/self-update.js +3 -0
  71. package/dist/commands/show.js +158 -60
  72. package/dist/commands/source-add.js +17 -45
  73. package/dist/commands/source-clone.js +3 -0
  74. package/dist/commands/source-manage.js +14 -19
  75. package/dist/commands/tasks.js +437 -0
  76. package/dist/commands/url-checker.js +42 -0
  77. package/dist/core/action-contributors.js +28 -0
  78. package/dist/core/asset-ref.js +17 -2
  79. package/dist/core/asset-registry.js +12 -17
  80. package/dist/core/asset-serialize.js +88 -0
  81. package/dist/core/asset-spec.js +67 -1
  82. package/dist/core/common.js +182 -0
  83. package/dist/core/concurrent.js +25 -0
  84. package/dist/core/config-io.js +347 -0
  85. package/dist/core/config-migration.js +622 -0
  86. package/dist/core/config-schema.js +534 -0
  87. package/dist/core/config-sources.js +108 -0
  88. package/dist/core/config-types.js +4 -0
  89. package/dist/core/config-walker.js +337 -0
  90. package/dist/core/config.js +364 -968
  91. package/dist/core/errors.js +42 -20
  92. package/dist/core/events.js +105 -135
  93. package/dist/core/file-lock.js +104 -0
  94. package/dist/core/frontmatter.js +75 -8
  95. package/dist/core/lesson-lint.js +3 -0
  96. package/dist/core/markdown.js +20 -0
  97. package/dist/core/memory-belief.js +62 -0
  98. package/dist/core/memory-contradiction-detect.js +274 -0
  99. package/dist/core/memory-improve.js +806 -0
  100. package/dist/core/parse.js +158 -0
  101. package/dist/core/paths.js +280 -14
  102. package/dist/core/proposal-quality-validators.js +380 -0
  103. package/dist/core/proposal-validators.js +69 -0
  104. package/dist/core/proposals.js +512 -42
  105. package/dist/core/state-db.js +1068 -0
  106. package/dist/core/text-truncation.js +107 -0
  107. package/dist/core/time.js +54 -0
  108. package/dist/core/tty.js +59 -0
  109. package/dist/core/warn.js +64 -1
  110. package/dist/core/write-source.js +3 -0
  111. package/dist/indexer/db-backup.js +391 -0
  112. package/dist/indexer/db-search.js +198 -489
  113. package/dist/indexer/db.js +990 -108
  114. package/dist/indexer/ensure-index.js +136 -0
  115. package/dist/indexer/file-context.js +3 -0
  116. package/dist/indexer/graph-boost.js +376 -101
  117. package/dist/indexer/graph-db.js +391 -0
  118. package/dist/indexer/graph-dedup.js +95 -0
  119. package/dist/indexer/graph-extraction.js +550 -114
  120. package/dist/indexer/index-context.js +4 -0
  121. package/dist/indexer/indexer.js +547 -309
  122. package/dist/indexer/llm-cache.js +52 -0
  123. package/dist/indexer/manifest.js +3 -0
  124. package/dist/indexer/matchers.js +167 -160
  125. package/dist/indexer/memory-inference.js +152 -74
  126. package/dist/indexer/metadata-contributors.js +29 -0
  127. package/dist/indexer/metadata.js +275 -196
  128. package/dist/indexer/path-resolver.js +92 -0
  129. package/dist/indexer/project-context.js +192 -0
  130. package/dist/indexer/ranking-contributors.js +331 -0
  131. package/dist/indexer/ranking.js +81 -0
  132. package/dist/indexer/search-fields.js +5 -9
  133. package/dist/indexer/search-hit-enrichers.js +111 -0
  134. package/dist/indexer/search-source.js +44 -10
  135. package/dist/indexer/semantic-status.js +6 -17
  136. package/dist/indexer/staleness-detect.js +447 -0
  137. package/dist/indexer/usage-events.js +12 -9
  138. package/dist/indexer/walker.js +28 -0
  139. package/dist/integrations/agent/builders.js +135 -0
  140. package/dist/integrations/agent/config.js +122 -230
  141. package/dist/integrations/agent/detect.js +3 -0
  142. package/dist/integrations/agent/index.js +7 -13
  143. package/dist/integrations/agent/model-aliases.js +55 -0
  144. package/dist/integrations/agent/profiles.js +70 -5
  145. package/dist/integrations/agent/prompts.js +250 -36
  146. package/dist/integrations/agent/runner.js +151 -0
  147. package/dist/integrations/agent/sdk-runner.js +126 -0
  148. package/dist/integrations/agent/spawn.js +183 -35
  149. package/dist/integrations/github.js +3 -0
  150. package/dist/integrations/lockfile.js +32 -69
  151. package/dist/integrations/session-logs/index.js +69 -0
  152. package/dist/integrations/session-logs/inline-refs.js +35 -0
  153. package/dist/integrations/session-logs/pre-filter.js +152 -0
  154. package/dist/integrations/session-logs/providers/claude-code.js +282 -0
  155. package/dist/integrations/session-logs/providers/opencode.js +258 -0
  156. package/dist/integrations/session-logs/types.js +4 -0
  157. package/dist/llm/call-ai.js +62 -0
  158. package/dist/llm/client.js +79 -88
  159. package/dist/llm/embedder.js +20 -29
  160. package/dist/llm/embedders/cache.js +3 -7
  161. package/dist/llm/embedders/local.js +42 -1
  162. package/dist/llm/embedders/remote.js +20 -8
  163. package/dist/llm/embedders/types.js +3 -7
  164. package/dist/llm/feature-gate.js +95 -48
  165. package/dist/llm/graph-extract.js +676 -72
  166. package/dist/llm/index-passes.js +44 -29
  167. package/dist/llm/memory-infer.js +80 -71
  168. package/dist/llm/metadata-enhance.js +42 -29
  169. package/dist/llm/prompts/extract-session.md +80 -0
  170. package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
  171. package/dist/output/cli-hints-full.md +292 -0
  172. package/dist/output/cli-hints-short.md +66 -0
  173. package/dist/output/cli-hints.js +7 -311
  174. package/dist/output/context.js +60 -8
  175. package/dist/output/renderers.js +306 -258
  176. package/dist/output/shapes/curate.js +56 -0
  177. package/dist/output/shapes/distill.js +10 -0
  178. package/dist/output/shapes/env-list.js +19 -0
  179. package/dist/output/shapes/events.js +11 -0
  180. package/dist/output/shapes/helpers.js +424 -0
  181. package/dist/output/shapes/history.js +7 -0
  182. package/dist/output/shapes/passthrough.js +102 -0
  183. package/dist/output/shapes/proposal-accept.js +7 -0
  184. package/dist/output/shapes/proposal-diff.js +7 -0
  185. package/dist/output/shapes/proposal-list.js +7 -0
  186. package/dist/output/shapes/proposal-producer.js +11 -0
  187. package/dist/output/shapes/proposal-reject.js +7 -0
  188. package/dist/output/shapes/proposal-show.js +7 -0
  189. package/dist/output/shapes/registry-search.js +6 -0
  190. package/dist/output/shapes/registry.js +30 -0
  191. package/dist/output/shapes/search.js +6 -0
  192. package/dist/output/shapes/secret-list.js +19 -0
  193. package/dist/output/shapes/show.js +6 -0
  194. package/dist/output/shapes/vault-list.js +19 -0
  195. package/dist/output/shapes.js +51 -511
  196. package/dist/output/text/add.js +6 -0
  197. package/dist/output/text/clone.js +6 -0
  198. package/dist/output/text/config.js +6 -0
  199. package/dist/output/text/curate.js +6 -0
  200. package/dist/output/text/distill.js +7 -0
  201. package/dist/output/text/enable-disable.js +7 -0
  202. package/dist/output/text/events.js +10 -0
  203. package/dist/output/text/feedback.js +6 -0
  204. package/dist/output/text/helpers.js +1039 -0
  205. package/dist/output/text/history.js +7 -0
  206. package/dist/output/text/import.js +6 -0
  207. package/dist/output/text/index.js +6 -0
  208. package/dist/output/text/info.js +6 -0
  209. package/dist/output/text/init.js +6 -0
  210. package/dist/output/text/list.js +6 -0
  211. package/dist/output/text/proposal-producer.js +8 -0
  212. package/dist/output/text/proposal.js +11 -0
  213. package/dist/output/text/registry-commands.js +11 -0
  214. package/dist/output/text/registry.js +30 -0
  215. package/dist/output/text/remember.js +6 -0
  216. package/dist/output/text/remove.js +6 -0
  217. package/dist/output/text/save.js +6 -0
  218. package/dist/output/text/search.js +6 -0
  219. package/dist/output/text/show.js +6 -0
  220. package/dist/output/text/update.js +6 -0
  221. package/dist/output/text/upgrade.js +6 -0
  222. package/dist/output/text/vault.js +16 -0
  223. package/dist/output/text/wiki.js +15 -0
  224. package/dist/output/text/workflow.js +14 -0
  225. package/dist/output/text.js +44 -1093
  226. package/dist/registry/build-index.js +3 -0
  227. package/dist/registry/create-provider-registry.js +3 -0
  228. package/dist/registry/factory.js +4 -1
  229. package/dist/registry/origin-resolve.js +3 -0
  230. package/dist/registry/providers/index.js +3 -0
  231. package/dist/registry/providers/skills-sh.js +71 -50
  232. package/dist/registry/providers/static-index.js +53 -48
  233. package/dist/registry/providers/types.js +3 -24
  234. package/dist/registry/resolve.js +11 -16
  235. package/dist/registry/types.js +3 -0
  236. package/dist/scripts/migrate-storage.js +17750 -0
  237. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
  238. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  239. package/dist/setup/detect.js +3 -0
  240. package/dist/setup/ripgrep-install.js +3 -0
  241. package/dist/setup/ripgrep-resolve.js +3 -0
  242. package/dist/setup/setup.js +775 -37
  243. package/dist/setup/steps.js +3 -15
  244. package/dist/sources/include.js +3 -0
  245. package/dist/sources/provider-factory.js +5 -12
  246. package/dist/sources/provider.js +3 -20
  247. package/dist/sources/providers/filesystem.js +19 -23
  248. package/dist/sources/providers/git.js +179 -20
  249. package/dist/sources/providers/index.js +3 -0
  250. package/dist/sources/providers/install-types.js +3 -13
  251. package/dist/sources/providers/npm.js +3 -4
  252. package/dist/sources/providers/provider-utils.js +3 -0
  253. package/dist/sources/providers/sync-from-ref.js +3 -11
  254. package/dist/sources/providers/tar-utils.js +3 -0
  255. package/dist/sources/providers/website.js +18 -22
  256. package/dist/sources/resolve.js +3 -0
  257. package/dist/sources/types.js +3 -0
  258. package/dist/sources/website-ingest.js +7 -0
  259. package/dist/tasks/backends/cron.js +203 -0
  260. package/dist/tasks/backends/exec-utils.js +28 -0
  261. package/dist/tasks/backends/index.js +24 -0
  262. package/dist/tasks/backends/launchd-template.xml +19 -0
  263. package/dist/tasks/backends/launchd.js +187 -0
  264. package/dist/tasks/backends/schtasks-template.xml +29 -0
  265. package/dist/tasks/backends/schtasks.js +215 -0
  266. package/dist/tasks/parser.js +211 -0
  267. package/dist/tasks/resolveAkmBin.js +87 -0
  268. package/dist/tasks/runner.js +458 -0
  269. package/dist/tasks/schedule.js +227 -0
  270. package/dist/tasks/schema.js +15 -0
  271. package/dist/tasks/validator.js +62 -0
  272. package/dist/version.js +3 -0
  273. package/dist/wiki/index-template.md +12 -0
  274. package/dist/wiki/ingest-workflow-template.md +54 -0
  275. package/dist/wiki/log-template.md +8 -0
  276. package/dist/wiki/schema-template.md +61 -0
  277. package/dist/wiki/wiki-templates.js +15 -0
  278. package/dist/wiki/wiki.js +13 -61
  279. package/dist/workflows/authoring.js +8 -25
  280. package/dist/workflows/cli.js +3 -0
  281. package/dist/workflows/db.js +141 -2
  282. package/dist/workflows/document-cache.js +3 -10
  283. package/dist/workflows/parser.js +3 -0
  284. package/dist/workflows/renderer.js +11 -3
  285. package/dist/workflows/runs.js +91 -89
  286. package/dist/workflows/schema.js +3 -0
  287. package/dist/workflows/scope-key.js +79 -0
  288. package/dist/workflows/validator.js +4 -8
  289. package/dist/workflows/workflow-template.md +24 -0
  290. package/docs/README.md +10 -2
  291. package/docs/data-and-telemetry.md +225 -0
  292. package/docs/migration/release-notes/0.7.0.md +1 -1
  293. package/docs/migration/release-notes/0.7.4.md +1 -1
  294. package/docs/migration/release-notes/0.7.5.md +20 -0
  295. package/docs/migration/release-notes/0.8.0.md +48 -0
  296. package/docs/migration/v0.7-to-v0.8.md +1307 -0
  297. package/package.json +29 -11
  298. package/dist/commands/install-audit.js +0 -381
  299. package/dist/commands/vault.js +0 -333
  300. package/dist/templates/wiki-templates.js +0 -100
@@ -0,0 +1,203 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * Schema-repair pass for `akm improve`.
6
+ *
7
+ * Attempts to patch missing frontmatter fields (`description`, `when_to_use`)
8
+ * on assets that failed schema validation, using a single bounded in-tree LLM
9
+ * call per asset. Results are recorded as `schema_repair_invoked` events.
10
+ *
11
+ * This module is extracted from `improve.ts` to make the repair logic
12
+ * independently testable and to use the `tryLlmFeature` seam rather than raw
13
+ * `chatCompletion`.
14
+ */
15
+ import fs from "node:fs";
16
+ import path from "node:path";
17
+ import { parseAssetRef } from "../core/asset-ref";
18
+ import { assembleAsset } from "../core/asset-serialize";
19
+ import { appendEvent, readEvents } from "../core/events";
20
+ import { parseFrontmatter } from "../core/frontmatter";
21
+ import { createProposal, isProposalSkipped } from "../core/proposals";
22
+ import { info, warn } from "../core/warn";
23
+ import { resolveAssetPath } from "../indexer/path-resolver";
24
+ import { chatCompletion, parseEmbeddedJsonResponse } from "../llm/client";
25
+ // ── Constants ────────────────────────────────────────────────────────────────
26
+ /** Minimum gap between schema-repair attempts on the same asset. */
27
+ const SCHEMA_REPAIR_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
28
+ /**
29
+ * Per-ref attempt cap (O-6 / #379): maximum number of schema-repair attempts
30
+ * allowed within SCHEMA_REPAIR_WINDOW_MS. Prevents indefinite nightly re-repair
31
+ * of assets whose source content is genuinely ambiguous or inconsistently
32
+ * structured. After cap, the asset is skipped until the window rolls over.
33
+ * Self-Refine arXiv:2303.17651 — iteration must be bounded.
34
+ */
35
+ const SCHEMA_REPAIR_MAX_ATTEMPTS = 3;
36
+ const SCHEMA_REPAIR_WINDOW_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
37
+ // ── Main ─────────────────────────────────────────────────────────────────────
38
+ /**
39
+ * Run the schema-repair loop for a batch of validation failures.
40
+ * Returns a list of per-asset outcome records and the set of refs that were
41
+ * successfully repaired (so the caller can exclude them from skip logic).
42
+ */
43
+ export async function runSchemaRepairPass(failures, options) {
44
+ const repairs = [];
45
+ const repairedRefs = new Set();
46
+ const { startMs, budgetMs, llmConfig, stashDir, findFilePath = defaultFindFilePath, isLessonCandidateFn = defaultIsLessonCandidate, chatFn = chatCompletion, } = options;
47
+ for (const failure of failures) {
48
+ if (Date.now() - startMs >= budgetMs)
49
+ break;
50
+ // Cooldown: skip repair if we ran it successfully recently.
51
+ const recentRepairs = readEvents({ type: "schema_repair_invoked", ref: failure.ref });
52
+ const lastRepair = recentRepairs.events
53
+ .filter((e) => e.metadata?.outcome === "written")
54
+ .sort((a, b) => new Date(b.ts ?? 0).getTime() - new Date(a.ts ?? 0).getTime())[0];
55
+ if (lastRepair?.ts && Date.now() - new Date(lastRepair.ts).getTime() < SCHEMA_REPAIR_COOLDOWN_MS) {
56
+ repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "skipped" });
57
+ continue;
58
+ }
59
+ // O-6 / #379: Cap total attempts at SCHEMA_REPAIR_MAX_ATTEMPTS per SCHEMA_REPAIR_WINDOW_MS.
60
+ // Prevents indefinite nightly re-repair of assets whose source is genuinely ambiguous.
61
+ // After the cap is reached, the asset is skipped until the window rolls over.
62
+ const windowStart = Date.now() - SCHEMA_REPAIR_WINDOW_MS;
63
+ const attemptsInWindow = recentRepairs.events.filter((e) => e.ts !== undefined && new Date(e.ts).getTime() >= windowStart).length;
64
+ if (attemptsInWindow >= SCHEMA_REPAIR_MAX_ATTEMPTS) {
65
+ repairs.push({
66
+ ref: failure.ref,
67
+ reason: failure.reason,
68
+ outcome: "skipped",
69
+ error: `schema-repair attempt cap reached (${attemptsInWindow}/${SCHEMA_REPAIR_MAX_ATTEMPTS} in 30d window)`,
70
+ });
71
+ continue;
72
+ }
73
+ const filePath = await findFilePath(failure.ref, stashDir);
74
+ if (!filePath) {
75
+ repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "skipped" });
76
+ continue;
77
+ }
78
+ if (path.extname(filePath).toLowerCase() !== ".md") {
79
+ repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "skipped" });
80
+ continue;
81
+ }
82
+ try {
83
+ const raw = fs.readFileSync(filePath, "utf8");
84
+ const fm = parseFrontmatter(raw);
85
+ const missingFields = [];
86
+ if (!fm.data.description)
87
+ missingFields.push("description");
88
+ if (isLessonCandidateFn(failure.ref) && !fm.data.when_to_use)
89
+ missingFields.push("when_to_use");
90
+ if (missingFields.length === 0) {
91
+ repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "skipped" });
92
+ continue;
93
+ }
94
+ const fieldList = missingFields.join(" and ");
95
+ info(`[improve] schema-repair ${failure.ref} (${fieldList})`);
96
+ const bodyPreview = (fm.content ?? raw).slice(0, 2000);
97
+ const llmResponse = await chatFn(llmConfig, [
98
+ {
99
+ role: "system",
100
+ content: `You generate concise asset frontmatter fields. Respond with a JSON object containing only the missing fields. No prose, no markdown fences.`,
101
+ },
102
+ {
103
+ role: "user",
104
+ content: `Generate the missing frontmatter fields (${fieldList}) for this ${parseAssetRef(failure.ref).type} asset. Return ONLY valid JSON like {"description": "...", "when_to_use": "..."}\n\n${bodyPreview}`,
105
+ },
106
+ ]);
107
+ const parsed = parseEmbeddedJsonResponse(llmResponse.trim());
108
+ if (!parsed) {
109
+ repairs.push({
110
+ ref: failure.ref,
111
+ reason: failure.reason,
112
+ outcome: "error",
113
+ error: "LLM returned unparseable JSON for schema repair",
114
+ });
115
+ continue;
116
+ }
117
+ const newFm = { ...fm.data };
118
+ if (parsed.description)
119
+ newFm.description = parsed.description;
120
+ if (parsed.when_to_use)
121
+ newFm.when_to_use = parsed.when_to_use;
122
+ const newContent = assembleAsset(newFm, fm.content);
123
+ // M-3 / #387: Route through proposal queue instead of writing directly to
124
+ // disk. This restores akm's safety invariant — the proposal queue is the
125
+ // only path to a committed asset write. LLM-generated `description` /
126
+ // `when_to_use` fields can be incorrect; routing through the queue makes
127
+ // them human-reviewable before they affect search ranking and curate hints.
128
+ // mem0 open gaps (arXiv:2504.19413) — any LLM write to a memory field
129
+ // should be human-reviewable.
130
+ if (stashDir) {
131
+ const proposalResult = createProposal(stashDir, {
132
+ ref: failure.ref,
133
+ source: "schema-repair",
134
+ payload: {
135
+ content: newContent,
136
+ ...(Object.keys(newFm).length > 0 ? { frontmatter: newFm } : {}),
137
+ },
138
+ });
139
+ if (isProposalSkipped(proposalResult)) {
140
+ info(`[improve] schema-repair proposal skipped for ${failure.ref}: ${proposalResult.message}`);
141
+ repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "skipped" });
142
+ continue;
143
+ }
144
+ info(`[improve] schema-repair queued: ${failure.ref} (proposal id: ${proposalResult.id})`);
145
+ appendEvent({
146
+ eventType: "schema_repair_invoked",
147
+ ref: failure.ref,
148
+ metadata: { outcome: "queued", reason: failure.reason, proposalId: proposalResult.id },
149
+ });
150
+ repairs.push({
151
+ ref: failure.ref,
152
+ reason: failure.reason,
153
+ outcome: "queued",
154
+ proposalId: proposalResult.id,
155
+ });
156
+ // Mark as repaired so the caller removes it from the validation-failure set.
157
+ repairedRefs.add(failure.ref);
158
+ }
159
+ else {
160
+ // Fallback: no stash dir available — write directly (legacy path).
161
+ // This should not occur in production; stashDir is always provided by
162
+ // `runSchemaRepairPass` callers in improve.ts.
163
+ warn(`[improve] schema-repair: no stashDir available for ${failure.ref}, falling back to direct write`);
164
+ fs.writeFileSync(filePath, newContent, "utf8");
165
+ info(`[improve] schema-repair written: ${failure.ref}`);
166
+ appendEvent({
167
+ eventType: "schema_repair_invoked",
168
+ ref: failure.ref,
169
+ metadata: { outcome: "written", reason: failure.reason },
170
+ });
171
+ repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "written" });
172
+ repairedRefs.add(failure.ref);
173
+ }
174
+ }
175
+ catch (e) {
176
+ appendEvent({
177
+ eventType: "schema_repair_invoked",
178
+ ref: failure.ref,
179
+ metadata: { outcome: "error", reason: failure.reason, error: String(e) },
180
+ });
181
+ repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "error", error: String(e) });
182
+ }
183
+ }
184
+ return { repairs, repairedRefs };
185
+ }
186
+ // ── Default seam implementations ─────────────────────────────────────────────
187
+ function defaultIsLessonCandidate(ref) {
188
+ try {
189
+ const parsed = parseAssetRef(ref);
190
+ return parsed.type === "lesson";
191
+ }
192
+ catch {
193
+ return false;
194
+ }
195
+ }
196
+ async function defaultFindFilePath(ref, stashDir) {
197
+ return resolveAssetPath(ref, {
198
+ stashDir,
199
+ mode: "index-first",
200
+ directoryIndexNames: ["SKILL.md", "index.md", "README.md"],
201
+ preserveDirectNameFallback: true,
202
+ });
203
+ }
@@ -1,3 +1,6 @@
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/.
1
4
  /**
2
5
  * `akm search` — entry point.
3
6
  *
@@ -9,11 +12,13 @@
9
12
  * Provider `search()` methods do not exist.
10
13
  */
11
14
  import { loadConfig } from "../core/config";
12
- import { UsageError } from "../core/errors";
15
+ import { rethrowIfTestIsolationError, UsageError } from "../core/errors";
13
16
  import { appendEvent } from "../core/events";
14
- import { closeDatabase, openExistingDatabase } from "../indexer/db";
17
+ import { isTransientStashPath } from "../core/paths";
18
+ import { bumpUtilityScoresBatch, closeDatabase, openExistingDatabase } from "../indexer/db";
15
19
  import { searchLocal } from "../indexer/db-search";
16
20
  import { resolveSourceEntries } from "../indexer/search-source";
21
+ import { getCurrentWorkflowScopeKey } from "../workflows/scope-key";
17
22
  // Eagerly import source providers to trigger self-registration before the
18
23
  // indexer or path-resolution code runs.
19
24
  import "../sources/providers/index";
@@ -26,10 +31,42 @@ export async function akmSearch(input) {
26
31
  const normalizedQuery = query.toLowerCase();
27
32
  const searchType = input.type ?? "any";
28
33
  const limit = normalizeLimit(input.limit);
29
- const source = parseSearchSource(input.source ?? "stash");
34
+ const parsedSource = parseSearchSource(input.source ?? "stash");
30
35
  const config = loadConfig();
31
- const sources = resolveSourceEntries(undefined, config);
32
- if (sources.length === 0) {
36
+ // Named-source filter: when --source is not a standard enum value, treat it
37
+ // as a named source from config.sources[].name. Validate early (before
38
+ // resolveSourceEntries, which can throw STASH_DIR_NOT_FOUND) so that a bad
39
+ // --source name always produces INVALID_SOURCE_VALUE regardless of stash state.
40
+ let namedSourceName;
41
+ let source;
42
+ if (parsedSource !== "stash" && parsedSource !== "registry" && parsedSource !== "both") {
43
+ namedSourceName = parsedSource;
44
+ // Check that the named source exists in the config before touching the stash.
45
+ const configSources = config.sources ?? [];
46
+ const foundInConfig = configSources.some((s) => s.name === namedSourceName) || configSources.some((s) => s.path === namedSourceName);
47
+ if (!foundInConfig) {
48
+ const validNames = configSources.map((s) => s.name).filter((n) => Boolean(n));
49
+ const hint = validNames.length > 0
50
+ ? `Known source names: ${validNames.join(", ")}`
51
+ : "No named sources are configured. Run `akm list` to see installed stashes.";
52
+ throw new UsageError(`Unknown source name: "${namedSourceName}". ${hint}`, "INVALID_SOURCE_VALUE");
53
+ }
54
+ source = "stash";
55
+ }
56
+ else {
57
+ source = parsedSource;
58
+ }
59
+ let allSources = resolveSourceEntries(undefined, config);
60
+ // When a named source was requested, narrow the sources list to just that entry.
61
+ // `resolveSourceEntries` sets `registryId` to `entry.name` for each config source.
62
+ if (namedSourceName !== undefined) {
63
+ const ns = namedSourceName;
64
+ allSources = allSources.filter((s) => s.registryId === ns || s.path === ns);
65
+ // allSources may still be empty if the configured source dir doesn't exist on
66
+ // disk (resolveSourceEntries skips non-existent dirs). Fall through to the
67
+ // zero-sources guard below which emits a friendly warning.
68
+ }
69
+ if (allSources.length === 0) {
33
70
  // stashDir: "" is a safe sentinel here — the response carries zero hits
34
71
  // and a warning, so no downstream code will try to use the empty path.
35
72
  const response = {
@@ -40,14 +77,18 @@ export async function akmSearch(input) {
40
77
  warnings: ["No stashes configured. Run `akm init` to create your working stash."],
41
78
  timing: { totalMs: Date.now() - t0 },
42
79
  };
43
- logSearchEvent(query, response);
80
+ if (!input.skipLogging)
81
+ logSearchEvent(query, response, undefined, undefined, input.eventSource);
44
82
  return response;
45
83
  }
46
84
  // Primary stash directory — used for DB path lookups and as the default
47
85
  // stash root. Safe because the empty-sources case is handled above.
48
- const stashDir = sources[0].path;
86
+ const stashDir = allSources[0].path;
87
+ // Expose the filtered source list to downstream search calls.
88
+ const sources = allSources;
49
89
  const filters = normalizeScopeFilters(input.filters);
50
90
  const includeProposed = input.includeProposed === true;
91
+ const belief = input.belief ?? "all";
51
92
  const localResult = source === "registry"
52
93
  ? undefined
53
94
  : await searchLocal({
@@ -59,6 +100,13 @@ export async function akmSearch(input) {
59
100
  config,
60
101
  filters,
61
102
  includeProposed,
103
+ beliefFilter: belief,
104
+ // When `--source <name>` narrowed the source list above, propagate
105
+ // that intent down to the database layer so FTS/vector hits from
106
+ // sources outside the narrowed set are filtered out post-ranking.
107
+ // Without this, the index (which spans every configured source)
108
+ // would leak hits from sources the caller did not request.
109
+ restrictToSources: namedSourceName !== undefined,
62
110
  });
63
111
  const registryResult = source === "stash" ? undefined : await searchRegistry(query, { limit, registries: config.registries });
64
112
  if (source === "stash") {
@@ -73,7 +121,8 @@ export async function akmSearch(input) {
73
121
  warnings: localResult?.warnings?.length ? localResult.warnings : undefined,
74
122
  timing: { totalMs: Date.now() - t0, rankMs: localResult?.rankMs, embedMs: localResult?.embedMs },
75
123
  };
76
- logSearchEvent(query, response);
124
+ if (!input.skipLogging)
125
+ logSearchEvent(query, response, undefined, localResult?.mode ?? "keyword", input.eventSource);
77
126
  return response;
78
127
  }
79
128
  const registryHits = (registryResult?.hits ?? []).map((hit) => {
@@ -107,7 +156,8 @@ export async function akmSearch(input) {
107
156
  warnings: registryResult?.warnings.length ? registryResult.warnings : undefined,
108
157
  timing: { totalMs: Date.now() - t0 },
109
158
  };
110
- logSearchEvent(query, response);
159
+ if (!input.skipLogging)
160
+ logSearchEvent(query, response, undefined, undefined, input.eventSource);
111
161
  return response;
112
162
  }
113
163
  // source === "both"
@@ -124,7 +174,8 @@ export async function akmSearch(input) {
124
174
  warnings: warnings.length ? warnings : undefined,
125
175
  timing: { totalMs: Date.now() - t0 },
126
176
  };
127
- logSearchEvent(query, response);
177
+ if (!input.skipLogging)
178
+ logSearchEvent(query, response, undefined, undefined, input.eventSource);
128
179
  return response;
129
180
  }
130
181
  /**
@@ -160,13 +211,16 @@ function resolveEntryIds(db, hits) {
160
211
  * Per-entry events are recorded only for stash hits because registry hits
161
212
  * have no local entry_id to reference.
162
213
  */
163
- function logSearchEvent(query, response, existingDb) {
214
+ function logSearchEvent(query, response, existingDb, mode = "keyword", eventSource = "user") {
164
215
  // Emit a structured event to events.jsonl so workflow-trace consumers
165
216
  // detect akm search invocations without relying on stdout scraping.
166
217
  const stashHits = response.hits.filter((h) => h.type !== "registry");
218
+ // D8: include registry hit refs so a show following a registry-only search generates a select event
219
+ const registryHitRefs = (response.registryHits ?? []).map((h) => `registry:${h.id}`);
220
+ const allResultRefs = [...stashHits.map((h) => h.ref), ...registryHitRefs];
167
221
  appendEvent({
168
222
  eventType: "search",
169
- metadata: { query, hitCount: stashHits.length, resultRefs: stashHits.map((h) => h.ref) },
223
+ metadata: { query, hitCount: stashHits.length, resultRefs: allResultRefs, mode },
170
224
  });
171
225
  try {
172
226
  const db = existingDb ?? openExistingDatabase();
@@ -178,8 +232,24 @@ function logSearchEvent(query, response, existingDb) {
178
232
  query,
179
233
  entry_id: entryId,
180
234
  entry_ref: ref,
235
+ source: eventSource,
181
236
  });
182
237
  }
238
+ // Bump utility scores for all resolved entries (MemRL retrieval signal).
239
+ // The indexer overwrites these at next reindex; bumps are temporary hints.
240
+ const resolvedIds = resolved.map((r) => r.entryId).filter((id) => id !== undefined);
241
+ if (resolvedIds.length > 0) {
242
+ let scopeKey;
243
+ try {
244
+ const stashPath = response.stashDir;
245
+ const disabled = process.env.AKM_DISABLE_SCOPED_UTILITY === "1" || (stashPath && isTransientStashPath(stashPath));
246
+ scopeKey = disabled ? undefined : getCurrentWorkflowScopeKey();
247
+ }
248
+ catch {
249
+ // Non-fatal — fall back to global-only bumps on any error.
250
+ }
251
+ bumpUtilityScoresBatch(db, resolvedIds, 1.0, 0.1, scopeKey);
252
+ }
183
253
  // Count registry hits separately so registry-only searches record a
184
254
  // non-zero resultCount. response.hits is always [] when source="registry".
185
255
  const stashHitCount = response.hits.length;
@@ -192,7 +262,9 @@ function logSearchEvent(query, response, existingDb) {
192
262
  stashHitCount,
193
263
  registryHitCount,
194
264
  resolvedCount: resolved.length,
265
+ mode,
195
266
  }),
267
+ source: eventSource,
196
268
  });
197
269
  }
198
270
  finally {
@@ -200,7 +272,8 @@ function logSearchEvent(query, response, existingDb) {
200
272
  closeDatabase(db);
201
273
  }
202
274
  }
203
- catch {
275
+ catch (err) {
276
+ rethrowIfTestIsolationError(err);
204
277
  /* fire-and-forget */
205
278
  }
206
279
  }
@@ -211,6 +284,24 @@ function normalizeLimit(limit) {
211
284
  }
212
285
  return Math.min(Math.floor(limit), 200);
213
286
  }
287
+ /**
288
+ * Parse the `--source` flag value.
289
+ *
290
+ * Accepts:
291
+ * - `stash` (default) — search the local stash index only
292
+ * - `registry` — search remote registries only
293
+ * - `both` — search stash and registries
294
+ * - `local` — alias for `stash`
295
+ * - Any named source from `config.sources[].name` — filters stash results to
296
+ * that single source only. The named-source path is detected and resolved
297
+ * inside `akmSearch`; this function returns the raw name so the caller can
298
+ * pass it through to `akmSearch` which accepts `SearchSource | string`.
299
+ *
300
+ * Unknown values that are not a known enum AND not a named source will still
301
+ * produce an error inside `akmSearch` when the config lookup finds nothing.
302
+ * This allows the CLI to accept named sources without requiring config access
303
+ * at parse time.
304
+ */
214
305
  export function parseSearchSource(source) {
215
306
  if (source === "stash" || source === "registry" || source === "both")
216
307
  return source;
@@ -219,7 +310,17 @@ export function parseSearchSource(source) {
219
310
  return "stash";
220
311
  if (typeof source === "undefined")
221
312
  return "stash";
222
- throw new UsageError(`Invalid value for --source: ${String(source)}. Expected one of: stash|registry|both`, "INVALID_SOURCE_VALUE");
313
+ // Pass through unknown strings they may be valid named sources.
314
+ // `akmSearch` will validate against config.sources and throw a UsageError
315
+ // with a helpful message if the name isn't found.
316
+ return source;
317
+ }
318
+ export function parseBeliefFilterMode(value) {
319
+ if (value === undefined || value === "all")
320
+ return "all";
321
+ if (value === "current" || value === "historical")
322
+ return value;
323
+ throw new UsageError(`Invalid value for --belief: ${String(value)}. Expected one of: all|current|historical`, "INVALID_FLAG_VALUE");
223
324
  }
224
325
  /**
225
326
  * Strip empty / non-string values from a scope filter object. Returns
@@ -0,0 +1,173 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * Secret asset type — whole-file secret storage.
6
+ *
7
+ * A `secret` holds a single SENSITIVE value used on its own for authentication
8
+ * (a PEM private key, an API token, a TLS cert, a service-account JSON): the
9
+ * ENTIRE file is the secret. There is no safe region to parse, so only the
10
+ * filename is ever surfaced. Where an `env` file holds a GROUP of related
11
+ * configuration and exposes key NAMES as metadata, a secret is ONE value and
12
+ * exposes nothing but its name — reach for `secret` when one value *is* the
13
+ * credential, and for `env` when loading a service's related configuration.
14
+ *
15
+ * Invariant: a secret's bytes must never be written to stdout, returned
16
+ * through the indexer / `akm show` renderer, or any structured output channel.
17
+ * The supported value-use paths are:
18
+ *
19
+ * - `akm secret run <ref> <VAR> -- <cmd>` — value injected into the child
20
+ * process env as `VAR=<value>` (see `readValue`).
21
+ * - `akm secret path <ref>` — print the file path so a command can read it
22
+ * itself (Docker `/run/secrets` + `_FILE` convention).
23
+ *
24
+ * Values are stored as raw bytes (no quoting, multi-line allowed) so they
25
+ * round-trip byte-exact, unlike env values which forbid literal newlines.
26
+ */
27
+ import crypto from "node:crypto";
28
+ import fs from "node:fs";
29
+ import path from "node:path";
30
+ import { probeLock, releaseLock, tryAcquireLockSync } from "../core/file-lock";
31
+ // ── Write-lock helper ─────────────────────────────────────────────────────────
32
+ /**
33
+ * Acquire an exclusive lock for the given secret path, run `fn`, then release.
34
+ * Mirrors the env write-lock: O_EXCL creation, 5s deadline, PID-based stale
35
+ * detection. A timeout is always a stale lock or a programming error, so we
36
+ * throw rather than silently proceeding.
37
+ */
38
+ export function withSecretLock(secretPath, fn) {
39
+ const lockPath = `${secretPath}.lock`;
40
+ const deadline = Date.now() + 5000;
41
+ while (!tryAcquireLockSync(lockPath, String(process.pid))) {
42
+ const probe = probeLock(lockPath);
43
+ if (probe.state === "stale") {
44
+ releaseLock(lockPath);
45
+ continue;
46
+ }
47
+ if (Date.now() > deadline) {
48
+ const holderHint = probe.state === "held"
49
+ ? ` Lock file ${lockPath} is held by live PID ${probe.holderPid}.`
50
+ : ` Lock file ${lockPath} could not be inspected.`;
51
+ throw new Error(`Could not acquire secret lock for ${secretPath} after 5s.${holderHint} Retry once any other akm secret operation finishes, or remove the stale lock file.`);
52
+ }
53
+ if (typeof globalThis.Bun?.sleepSync ===
54
+ "function") {
55
+ globalThis.Bun.sleepSync(10);
56
+ }
57
+ else {
58
+ let spin = 0;
59
+ while (spin++ < 100_000) {
60
+ /* yield */
61
+ }
62
+ }
63
+ }
64
+ try {
65
+ return fn();
66
+ }
67
+ finally {
68
+ releaseLock(lockPath);
69
+ }
70
+ }
71
+ // ── Atomic byte write ──────────────────────────────────────────────────────────
72
+ /**
73
+ * Atomically write `data` to `target` at mode 0600. Unlike `writeFileAtomic`
74
+ * in core/common (string content), this accepts a Buffer so secret bytes
75
+ * round-trip exactly — binary certs and CRLF/LF line endings are preserved.
76
+ */
77
+ function writeSecretAtomic(target, data) {
78
+ const tmp = `${target}.tmp.${process.pid}.${crypto.randomBytes(8).toString("hex")}`;
79
+ const fd = fs.openSync(tmp, "w", 0o600);
80
+ try {
81
+ fs.writeSync(fd, data);
82
+ try {
83
+ fs.fdatasyncSync(fd);
84
+ }
85
+ catch {
86
+ // Best-effort durability; some pseudo-filesystems lack fdatasync.
87
+ }
88
+ }
89
+ finally {
90
+ fs.closeSync(fd);
91
+ }
92
+ fs.renameSync(tmp, target);
93
+ try {
94
+ const dirFd = fs.openSync(path.dirname(target), "r");
95
+ try {
96
+ fs.fsyncSync(dirFd);
97
+ }
98
+ finally {
99
+ fs.closeSync(dirFd);
100
+ }
101
+ }
102
+ catch {
103
+ // Directory fsync is unsupported on FAT / some FUSE mounts / Windows.
104
+ }
105
+ }
106
+ function ensureParentDir(filePath) {
107
+ const dir = path.dirname(filePath);
108
+ if (!fs.existsSync(dir))
109
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
110
+ }
111
+ // ── Public API ──────────────────────────────────────────────────────────────
112
+ /**
113
+ * Walk a `secrets/` directory and return the POSIX-relative names of every
114
+ * secret file. Lock files (`*.lock`), sensitive markers (`*.sensitive`), and
115
+ * secrets with a sibling `<name>.sensitive` marker are excluded. The file
116
+ * bodies are NEVER read.
117
+ */
118
+ export function listNames(secretsRoot) {
119
+ if (!fs.existsSync(secretsRoot))
120
+ return [];
121
+ const names = [];
122
+ const walk = (dir) => {
123
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
124
+ const full = path.join(dir, entry.name);
125
+ if (entry.isDirectory()) {
126
+ walk(full);
127
+ continue;
128
+ }
129
+ if (!entry.isFile())
130
+ continue;
131
+ if (entry.name.endsWith(".lock") || entry.name.endsWith(".sensitive"))
132
+ continue;
133
+ // A sibling `<name>.sensitive` marker suppresses listing.
134
+ if (fs.existsSync(`${full}.sensitive`))
135
+ continue;
136
+ names.push(path.relative(secretsRoot, full).split(path.sep).join("/"));
137
+ }
138
+ };
139
+ walk(secretsRoot);
140
+ return names.sort();
141
+ }
142
+ /**
143
+ * Read a secret's raw bytes. Internal use only (for `secret run` / `secret
144
+ * path`). Callers MUST NOT write the returned value to stdout or any log.
145
+ */
146
+ export function readValue(secretPath) {
147
+ return fs.readFileSync(secretPath);
148
+ }
149
+ /**
150
+ * Write (create or overwrite) a secret with the given raw bytes, atomically at
151
+ * mode 0600 under a write-lock. No quoting; multi-line / binary allowed.
152
+ */
153
+ export function setSecret(secretPath, value) {
154
+ ensureParentDir(secretPath);
155
+ withSecretLock(secretPath, () => {
156
+ writeSecretAtomic(secretPath, value);
157
+ });
158
+ }
159
+ /**
160
+ * Remove a secret file (and its `.sensitive` marker, if present). Returns true
161
+ * if the secret existed.
162
+ */
163
+ export function removeSecret(secretPath) {
164
+ return withSecretLock(secretPath, () => {
165
+ if (!fs.existsSync(secretPath))
166
+ return false;
167
+ fs.rmSync(secretPath);
168
+ const marker = `${secretPath}.sensitive`;
169
+ if (fs.existsSync(marker))
170
+ fs.rmSync(marker);
171
+ return true;
172
+ });
173
+ }
@@ -1,3 +1,6 @@
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/.
1
4
  import * as childProcess from "node:child_process";
2
5
  import { createHash } from "node:crypto";
3
6
  import fs from "node:fs";