akm-cli 0.8.0-rc2 → 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 +2141 -1268
  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 +199 -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 +13 -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 +661 -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 +110 -50
  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 -310
  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,331 @@
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 fs from "node:fs";
5
+ import { defineCommand } from "citty";
6
+ import { output, parseAllFlagValues, runWithJsonErrors } from "../cli/shared";
7
+ import { parseAssetRef } from "../core/asset-ref";
8
+ import { assembleAsset } from "../core/asset-serialize";
9
+ import { writeFileAtomic } from "../core/common";
10
+ import { FEEDBACK_FAILURE_MODES, loadConfig } from "../core/config";
11
+ import { UsageError } from "../core/errors";
12
+ import { appendEvent } from "../core/events";
13
+ import { parseFrontmatter, parseFrontmatterBlock } from "../core/frontmatter";
14
+ import { warn } from "../core/warn";
15
+ import { applyFeedbackToUtilityScore, closeDatabase, findEntryIdByRef, openExistingDatabase } from "../indexer/db";
16
+ import { ensureIndex } from "../indexer/ensure-index";
17
+ import { resolveSourceEntries } from "../indexer/search-source";
18
+ import { insertUsageEvent } from "../indexer/usage-events";
19
+ // ── Tag validation ────────────────────────────────────────────────────────────
20
+ const TAG_KEY_RE = /^[a-z_][a-z0-9_]*$/;
21
+ const MAX_FEEDBACK_TAGS = 10;
22
+ function validateFeedbackTags(raw) {
23
+ const seen = new Set();
24
+ const out = [];
25
+ for (const tag of raw) {
26
+ const parts = tag.split(":");
27
+ if (parts.length < 2 || parts[0] === "" || parts.slice(1).join("") === "") {
28
+ throw new UsageError(`Invalid tag "${tag}". Tags must be in key:value format where key matches [a-z_][a-z0-9_]* and value is non-empty.`, "INVALID_FLAG_VALUE");
29
+ }
30
+ const key = parts[0];
31
+ if (!TAG_KEY_RE.test(key)) {
32
+ throw new UsageError(`Invalid tag key "${key}" in "${tag}". Key must match [a-z_][a-z0-9_]*.`, "INVALID_FLAG_VALUE");
33
+ }
34
+ if (seen.has(tag))
35
+ continue;
36
+ seen.add(tag);
37
+ out.push(tag);
38
+ }
39
+ if (out.length > MAX_FEEDBACK_TAGS) {
40
+ throw new UsageError(`Too many tags: ${out.length}. Maximum is ${MAX_FEEDBACK_TAGS}.`, "INVALID_FLAG_VALUE");
41
+ }
42
+ return out;
43
+ }
44
+ // ── Lesson strength helper ────────────────────────────────────────────────────
45
+ /**
46
+ * Phase 7A: append a feedback ref to a lesson's `lessonStrength[]`
47
+ * frontmatter array. Returns `{ strength }` (post-update count) on success,
48
+ * or `null` when the lesson cannot be located. Idempotent: if the ref is
49
+ * already credited, no write occurs.
50
+ *
51
+ * The function looks up the lesson's file via the indexer DB so the write
52
+ * targets the canonical on-disk location. Frontmatter is rewritten in
53
+ * place (no asset-spec round-trip) because we're modifying a single key on
54
+ * an existing asset — the same pattern memory-inference uses for
55
+ * `inferenceProcessed`.
56
+ */
57
+ function appendLessonStrength(type, name, feedbackRef) {
58
+ const ref = `${type}:${name}`;
59
+ let filePath;
60
+ const db = openExistingDatabase();
61
+ try {
62
+ const entryId = findEntryIdByRef(db, ref);
63
+ if (entryId === undefined) {
64
+ warn(`[feedback] --applied-to: lesson ${ref} is not in the index.`);
65
+ return null;
66
+ }
67
+ const row = db.prepare("SELECT file_path FROM entries WHERE id = ?").get(entryId);
68
+ if (!row?.file_path) {
69
+ warn(`[feedback] --applied-to: cannot resolve file path for ${ref}.`);
70
+ return null;
71
+ }
72
+ filePath = row.file_path;
73
+ }
74
+ finally {
75
+ closeDatabase(db);
76
+ }
77
+ if (!filePath || !fs.existsSync(filePath)) {
78
+ warn(`[feedback] --applied-to: lesson file missing on disk for ${ref}.`);
79
+ return null;
80
+ }
81
+ const raw = fs.readFileSync(filePath, "utf8");
82
+ const parsed = parseFrontmatter(raw);
83
+ const data = { ...parsed.data };
84
+ const existing = data.lessonStrength;
85
+ const strengthList = Array.isArray(existing)
86
+ ? existing.filter((x) => typeof x === "string" && x.trim().length > 0).map((x) => x.trim())
87
+ : typeof existing === "string" && existing.trim().length > 0
88
+ ? [existing.trim()]
89
+ : [];
90
+ if (strengthList.includes(feedbackRef)) {
91
+ // Already credited — idempotent no-op.
92
+ return { strength: strengthList.length };
93
+ }
94
+ strengthList.push(feedbackRef);
95
+ data.lessonStrength = strengthList;
96
+ const block = parseFrontmatterBlock(raw);
97
+ const body = block?.content ?? raw;
98
+ const next = assembleAsset(data, body);
99
+ try {
100
+ // Preserve the existing file's permission bits (markdown assets are
101
+ // typically 0o644); writeFileAtomic defaults to 0o600 otherwise.
102
+ const mode = fs.statSync(filePath).mode & 0o777;
103
+ writeFileAtomic(filePath, next, mode);
104
+ }
105
+ catch (err) {
106
+ warn(`[feedback] --applied-to: failed to write ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
107
+ return null;
108
+ }
109
+ return { strength: strengthList.length };
110
+ }
111
+ // ── Command definition ────────────────────────────────────────────────────────
112
+ export const feedbackCommand = defineCommand({
113
+ meta: {
114
+ name: "feedback",
115
+ description: "Record positive or negative feedback for any indexed stash asset.\n\n" +
116
+ "Positive feedback boosts an asset's EMA utility score, making it rank higher\n" +
117
+ "in future searches without requiring a full reindex.\n\n" +
118
+ "Negative feedback records a negative signal in usage_events and state.db events.\n" +
119
+ "It does NOT immediately lower the asset's ranking — the EMA utility score is\n" +
120
+ "updated the next time `akm index` runs (incremental or full). Run `akm index`\n" +
121
+ "after recording negative feedback to have it reflected in search results.",
122
+ },
123
+ args: {
124
+ // Optional in citty so run() is invoked even when omitted; we re-validate
125
+ // and throw a structured UsageError below so exit code is 2 (USAGE) rather
126
+ // than citty's default 0 (help banner).
127
+ ref: { type: "positional", description: "Asset ref (type:name)", required: false },
128
+ positive: { type: "boolean", description: "Record positive feedback (boosts ranking immediately)", default: false },
129
+ negative: {
130
+ type: "boolean",
131
+ description: "Record negative feedback (suppresses ranking after next `akm index`). " +
132
+ "Reindexing is required for the signal to affect search results.",
133
+ default: false,
134
+ },
135
+ reason: {
136
+ type: "string",
137
+ description: "Reason for the feedback (required for negative feedback by default; used by distillation)",
138
+ },
139
+ note: { type: "string", description: "Alias for --reason (backward-compatible, prefer --reason)" },
140
+ "failure-mode": {
141
+ type: "string",
142
+ description: `Structured failure-mode taxonomy for negative feedback (F-3 / #384). ` +
143
+ `Accepted values: ${FEEDBACK_FAILURE_MODES.join(", ")}. ` +
144
+ "Stored alongside --reason in event metadata for aggregation by the distill pipeline.",
145
+ },
146
+ tag: {
147
+ type: "string",
148
+ description: "Tag to attach to the feedback (repeatable, e.g. --tag slice:train --tag team:platform)",
149
+ },
150
+ "applied-to": {
151
+ type: "string",
152
+ description: "Credit a lesson that helped resolve this task. Accepts a `lesson:<name>` ref. " +
153
+ "When combined with --positive, appends this feedback ref to the target lesson's " +
154
+ "`lessonStrength[]` frontmatter array (dedup, idempotent). Ignored on non-lesson targets.",
155
+ },
156
+ },
157
+ run({ args }) {
158
+ return runWithJsonErrors(async () => {
159
+ const ref = (args.ref ?? "").trim();
160
+ if (!ref) {
161
+ throw new UsageError("Asset ref is required. Usage: akm feedback <ref> --positive|--negative", "MISSING_REQUIRED_ARGUMENT", "Pass a ref like `skill:deploy` and either --positive or --negative.");
162
+ }
163
+ parseAssetRef(ref);
164
+ if (args.positive && args.negative) {
165
+ throw new UsageError("Specify either --positive or --negative, not both.");
166
+ }
167
+ if (!args.positive && !args.negative) {
168
+ throw new UsageError("Specify --positive or --negative.");
169
+ }
170
+ const signal = args.positive ? "positive" : "negative";
171
+ // `--note` is a deprecated back-compat alias for `--reason` (removed in
172
+ // 0.9.0). Warn on stderr when it is used as the sole source (i.e. without
173
+ // an explicit `--reason`). Warnings go to stderr only so JSON stdout
174
+ // consumers are unaffected.
175
+ if (args.note !== undefined && args.reason === undefined) {
176
+ warn("warning: '--note' is deprecated for 'akm feedback'; use '--reason'. Removed in 0.9.0.");
177
+ }
178
+ const reason = args.reason ?? args.note;
179
+ // F-3 / #384: Validate --failure-mode against the curated enum.
180
+ const failureMode = args["failure-mode"]?.trim() || undefined;
181
+ if (failureMode) {
182
+ if (args.positive) {
183
+ throw new UsageError("--failure-mode is only valid for negative feedback.", "INVALID_FLAG_VALUE", "Remove --failure-mode or switch to --negative.");
184
+ }
185
+ const cfg = loadConfig();
186
+ const allowedModes = cfg.feedback?.allowedFailureModes ?? FEEDBACK_FAILURE_MODES;
187
+ if (allowedModes.length > 0 && !allowedModes.includes(failureMode)) {
188
+ throw new UsageError(`Invalid --failure-mode "${failureMode}". Accepted values: ${allowedModes.join(", ")}.`, "INVALID_FLAG_VALUE", `Use one of: ${allowedModes.join(", ")}`);
189
+ }
190
+ }
191
+ if (args.negative === true && !reason?.trim()) {
192
+ // F-3 / #384: Default requireReason is now true. Load config to allow
193
+ // operators to opt out via feedback.requireReason: false in akm.json.
194
+ const cfg = loadConfig();
195
+ const requireReason = cfg.feedback?.requireReason ?? true; // Default: true (F-3 / #384)
196
+ if (requireReason) {
197
+ throw new UsageError("Negative feedback requires --reason (structured failure signals are needed for distillation). " +
198
+ "Use --failure-mode for a curated taxonomy or --reason for free text. " +
199
+ "Set feedback.requireReason: false in akm.json to downgrade to a warning.", "MISSING_REQUIRED_ARGUMENT", `Hint: akm feedback ${ref} --negative --reason "..." [--failure-mode incorrect|outdated|dangerous|incomplete|redundant]`);
200
+ }
201
+ else {
202
+ warn("Warning: negative feedback without --reason provides less distillation signal.");
203
+ }
204
+ }
205
+ const rawTags = parseAllFlagValues("--tag");
206
+ const validatedTags = validateFeedbackTags(rawTags);
207
+ const metadataObj = {
208
+ signal,
209
+ ...(reason?.trim() ? { reason: reason.trim() } : {}),
210
+ ...(failureMode ? { failureMode } : {}),
211
+ ...(validatedTags.length > 0 ? { tags: validatedTags } : {}),
212
+ };
213
+ const metadataStr = Object.keys(metadataObj).length > 1 ? JSON.stringify(metadataObj) : undefined;
214
+ // Auto-index when stale so the index is current before recording feedback.
215
+ const sources = resolveSourceEntries();
216
+ if (sources.length > 0) {
217
+ await ensureIndex(sources[0].path);
218
+ }
219
+ let utilityResult;
220
+ const db = openExistingDatabase();
221
+ try {
222
+ const entryId = findEntryIdByRef(db, ref);
223
+ if (entryId === undefined) {
224
+ throw new UsageError(`Ref "${ref}" is not in the index. ` +
225
+ "Run 'akm search' to verify the asset exists, then 'akm index' if it was recently added.");
226
+ }
227
+ // Persist the feedback signal into usage_events. For positive signals,
228
+ // the EMA utility score is updated immediately on the next read path.
229
+ // For negative signals, the score is adjusted the next time `akm index`
230
+ // runs — the signal is durable in the DB but does NOT suppress ranking
231
+ // in search results until after reindexing.
232
+ insertUsageEvent(db, {
233
+ event_type: "feedback",
234
+ entry_ref: ref,
235
+ entry_id: entryId,
236
+ signal,
237
+ metadata: metadataStr,
238
+ });
239
+ // Apply feedback-derived utility score adjustment immediately so that
240
+ // positive/negative signals influence search ranking without requiring
241
+ // a full reindex. We query the total accumulated feedback counts from
242
+ // usage_events so the delta reflects the entire signal history.
243
+ // Uses MemRL bounded-step EMA (F-5 / #386, arXiv:2601.03192).
244
+ try {
245
+ const counts = db
246
+ .prepare(`SELECT
247
+ SUM(CASE WHEN signal = 'positive' THEN 1 ELSE 0 END) AS pos,
248
+ SUM(CASE WHEN signal = 'negative' THEN 1 ELSE 0 END) AS neg
249
+ FROM usage_events
250
+ WHERE event_type = 'feedback' AND entry_id = ?`)
251
+ .get(entryId);
252
+ const pos = counts?.pos ?? 0;
253
+ const neg = counts?.neg ?? 0;
254
+ utilityResult = applyFeedbackToUtilityScore(db, entryId, pos, neg);
255
+ }
256
+ catch {
257
+ // best-effort — feedback recording succeeds even if utility update fails
258
+ }
259
+ }
260
+ finally {
261
+ closeDatabase(db);
262
+ }
263
+ appendEvent({
264
+ eventType: "feedback",
265
+ ref,
266
+ metadata: metadataObj,
267
+ });
268
+ // F-5 / #386: When a high-utility asset crosses below the review threshold,
269
+ // auto-create a review-needed escalation proposal so a human can confirm
270
+ // whether the negative feedback is valid before the asset falls out of
271
+ // the improve loop. Best-effort — failure is logged but does not fail the
272
+ // feedback command.
273
+ // Emit a structured event rather than a proposal so the review-needed
274
+ // signal is queryable via `akm events list --type improve_review_needed`
275
+ // without risking accidental asset overwrite if the proposal is accepted.
276
+ if (utilityResult?.crossedReviewThreshold) {
277
+ try {
278
+ appendEvent({
279
+ eventType: "improve_review_needed",
280
+ ref,
281
+ metadata: {
282
+ previousUtility: utilityResult.previousUtility,
283
+ nextUtility: utilityResult.nextUtility,
284
+ reason: reason?.trim() ?? null,
285
+ failureMode: failureMode ?? null,
286
+ },
287
+ });
288
+ }
289
+ catch (escalationErr) {
290
+ warn(`[feedback] Could not emit review-needed event for ${ref}: ${escalationErr instanceof Error ? escalationErr.message : String(escalationErr)}`);
291
+ }
292
+ }
293
+ // Phase 7A / Advantage D4b: --applied-to credits a lesson. When the
294
+ // target is a `lesson:<name>` ref and the signal is positive, append
295
+ // the feedback ref to the target lesson's `lessonStrength[]`
296
+ // frontmatter array (dedup, idempotent). Non-lesson targets are
297
+ // ignored. Failures here are warnings — feedback recording is the
298
+ // primary contract and must not regress on lesson-write errors.
299
+ const appliedToRaw = args["applied-to"]?.trim();
300
+ let appliedToResult = null;
301
+ if (appliedToRaw && signal === "positive") {
302
+ try {
303
+ const parsedApplied = parseAssetRef(appliedToRaw);
304
+ if (parsedApplied.type === "lesson") {
305
+ const updated = appendLessonStrength(parsedApplied.type, parsedApplied.name, ref);
306
+ if (updated) {
307
+ appliedToResult = { lessonRef: appliedToRaw, strength: updated.strength };
308
+ }
309
+ }
310
+ }
311
+ catch (err) {
312
+ warn(`[feedback] --applied-to failed for ${appliedToRaw}: ${err instanceof Error ? err.message : String(err)}`);
313
+ }
314
+ }
315
+ else if (appliedToRaw && signal !== "positive") {
316
+ warn("[feedback] --applied-to is ignored without --positive; lesson credit is only recorded on positive signals.");
317
+ }
318
+ output("feedback", {
319
+ ok: true,
320
+ ref,
321
+ signal,
322
+ reason: reason?.trim() ?? null,
323
+ failureMode: failureMode ?? null,
324
+ tags: validatedTags,
325
+ ...(appliedToResult
326
+ ? { appliedTo: { ref: appliedToResult.lessonRef, lessonStrength: appliedToResult.strength } }
327
+ : {}),
328
+ });
329
+ });
330
+ },
331
+ });
@@ -1,11 +1,17 @@
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 fs from "node:fs";
2
5
  import path from "node:path";
3
6
  import { parseAssetRef } from "../core/asset-ref";
4
7
  import { loadConfig } from "../core/config";
5
8
  import { NotFoundError, UsageError } from "../core/errors";
6
- import { closeDatabase, openExistingDatabase } from "../indexer/db";
9
+ import { getDbPath } from "../core/paths";
10
+ import { warn } from "../core/warn";
11
+ import { closeDatabase, findEntryIdByRef, getEntryById, openDatabase, openExistingDatabase } from "../indexer/db";
7
12
  import { listRelatedPathsForFile } from "../indexer/graph-boost";
8
13
  import { loadStoredGraphSnapshot } from "../indexer/graph-db";
14
+ import { runGraphExtractionPass } from "../indexer/graph-extraction";
9
15
  import { lookup } from "../indexer/indexer";
10
16
  import { resolveAssetPath } from "../indexer/path-resolver";
11
17
  import { findSourceForPath, resolveSourceEntries } from "../indexer/search-source";
@@ -40,6 +46,7 @@ function loadGraph(source) {
40
46
  entities: snapshot.entities,
41
47
  relations: snapshot.relations,
42
48
  ...(snapshot.quality ? { quality: snapshot.quality } : {}),
49
+ ...(snapshot.telemetry ? { telemetry: snapshot.telemetry } : {}),
43
50
  },
44
51
  stashPath,
45
52
  graphPath: snapshot.graphPath,
@@ -63,6 +70,29 @@ function countEntitiesByFile(nodes) {
63
70
  }
64
71
  return counts;
65
72
  }
73
+ function aggregateEntityStats(nodes) {
74
+ const stats = new Map();
75
+ for (const node of nodes) {
76
+ const seen = new Set();
77
+ for (const entity of node.entities) {
78
+ if (seen.has(entity))
79
+ continue;
80
+ seen.add(entity);
81
+ const existing = stats.get(entity);
82
+ const nodeConf = typeof node.confidence === "number" && Number.isFinite(node.confidence) ? node.confidence : undefined;
83
+ if (existing) {
84
+ existing.fileCount += 1;
85
+ if (nodeConf !== undefined && (existing.confidence === undefined || nodeConf > existing.confidence)) {
86
+ existing.confidence = nodeConf;
87
+ }
88
+ }
89
+ else {
90
+ stats.set(entity, { fileCount: 1, ...(nodeConf !== undefined ? { confidence: nodeConf } : {}) });
91
+ }
92
+ }
93
+ }
94
+ return stats;
95
+ }
66
96
  export function akmGraphSummary(options) {
67
97
  const { graph, stashPath, graphPath } = loadGraph(options?.source);
68
98
  return {
@@ -77,6 +107,7 @@ export function akmGraphSummary(options) {
77
107
  ? graph.relations.length
78
108
  : graph.files.reduce((sum, node) => sum + node.relations.length, 0),
79
109
  ...(graph.quality ? { quality: graph.quality } : {}),
110
+ ...(graph.telemetry ? { telemetry: graph.telemetry } : {}),
80
111
  };
81
112
  }
82
113
  export function akmGraphEntities(options) {
@@ -85,9 +116,13 @@ export function akmGraphEntities(options) {
85
116
  if (limit !== undefined && (!Number.isFinite(limit) || limit <= 0)) {
86
117
  throw new UsageError("--limit must be a positive integer.", "INVALID_FLAG_VALUE");
87
118
  }
88
- const counts = countEntitiesByFile(graph.files);
89
- const entities = [...counts.entries()]
90
- .map(([name, fileCount]) => ({ name, fileCount }))
119
+ const stats = aggregateEntityStats(graph.files);
120
+ const entities = [...stats.entries()]
121
+ .map(([name, info]) => ({
122
+ name,
123
+ fileCount: info.fileCount,
124
+ ...(info.confidence !== undefined ? { confidence: info.confidence } : {}),
125
+ }))
91
126
  .sort((a, b) => b.fileCount - a.fileCount || a.name.localeCompare(b.name));
92
127
  const sliced = typeof limit === "number" ? entities.slice(0, limit) : entities;
93
128
  return {
@@ -110,12 +145,22 @@ export function akmGraphRelations(options) {
110
145
  for (const node of graph.files) {
111
146
  for (const rel of node.relations) {
112
147
  const key = `${rel.from}\u0000${rel.to}\u0000${rel.type ?? ""}`;
148
+ const relConf = typeof rel.confidence === "number" && Number.isFinite(rel.confidence) ? rel.confidence : undefined;
113
149
  const existing = counts.get(key);
114
150
  if (existing) {
115
151
  existing.count += 1;
152
+ if (relConf !== undefined && (existing.confidence === undefined || relConf > existing.confidence)) {
153
+ existing.confidence = relConf;
154
+ }
116
155
  }
117
156
  else {
118
- counts.set(key, { from: rel.from, to: rel.to, ...(rel.type ? { type: rel.type } : {}), count: 1 });
157
+ counts.set(key, {
158
+ from: rel.from,
159
+ to: rel.to,
160
+ ...(rel.type ? { type: rel.type } : {}),
161
+ count: 1,
162
+ ...(relConf !== undefined ? { confidence: relConf } : {}),
163
+ });
119
164
  }
120
165
  }
121
166
  }
@@ -194,6 +239,216 @@ export async function akmGraphRelated(options) {
194
239
  ...(related.length === 0 ? { tip: "No related graph neighbors were found for this asset." } : {}),
195
240
  };
196
241
  }
242
+ function normalizeGraphName(value) {
243
+ return value.trim().toLowerCase();
244
+ }
245
+ function buildRefByPath(stashRoot, db) {
246
+ const rows = db
247
+ .prepare("SELECT file_path, entry_json FROM entries WHERE stash_dir = ? OR file_path LIKE ?")
248
+ .all(stashRoot, `${stashRoot}%`);
249
+ const map = new Map();
250
+ for (const row of rows) {
251
+ if (map.has(row.file_path))
252
+ continue;
253
+ try {
254
+ const entry = JSON.parse(row.entry_json);
255
+ if (typeof entry.type === "string" && typeof entry.name === "string") {
256
+ map.set(row.file_path, { ref: `${entry.type}:${entry.name}`, type: entry.type });
257
+ }
258
+ }
259
+ catch {
260
+ // ignore corrupt entry_json
261
+ }
262
+ }
263
+ return map;
264
+ }
265
+ export function akmGraphEntity(options) {
266
+ const name = options.name?.trim();
267
+ if (!name) {
268
+ throw new UsageError("`akm graph entity` requires <name>.", "MISSING_REQUIRED_ARGUMENT");
269
+ }
270
+ const limit = options.limit;
271
+ if (limit !== undefined && (!Number.isFinite(limit) || limit <= 0)) {
272
+ throw new UsageError("--limit must be a positive integer.", "INVALID_FLAG_VALUE");
273
+ }
274
+ const { graph, stashPath, graphPath } = loadGraph(options.source);
275
+ const target = normalizeGraphName(name);
276
+ let db;
277
+ let refByPath;
278
+ try {
279
+ db = openExistingDatabase();
280
+ refByPath = buildRefByPath(stashPath, db);
281
+ }
282
+ finally {
283
+ if (db)
284
+ closeDatabase(db);
285
+ }
286
+ const matches = [];
287
+ for (const node of graph.files) {
288
+ const found = node.entities.some((entity) => normalizeGraphName(entity) === target);
289
+ if (!found)
290
+ continue;
291
+ const lookup = refByPath.get(node.path);
292
+ const conf = typeof node.confidence === "number" && Number.isFinite(node.confidence) ? node.confidence : undefined;
293
+ matches.push({
294
+ ...(lookup?.ref ? { ref: lookup.ref } : {}),
295
+ path: node.path,
296
+ type: node.type,
297
+ ...(conf !== undefined ? { confidence: conf } : {}),
298
+ });
299
+ }
300
+ matches.sort((a, b) => {
301
+ const ca = a.confidence ?? 0;
302
+ const cb = b.confidence ?? 0;
303
+ if (cb !== ca)
304
+ return cb - ca;
305
+ return a.path.localeCompare(b.path);
306
+ });
307
+ const sliced = typeof limit === "number" ? matches.slice(0, limit) : matches;
308
+ return {
309
+ schemaVersion: 1,
310
+ shape: "graph-entity",
311
+ stashPath,
312
+ graphPath,
313
+ generatedAt: graph.generatedAt,
314
+ entity: name,
315
+ total: matches.length,
316
+ matches: sliced,
317
+ };
318
+ }
319
+ export function akmGraphOrphans(options) {
320
+ const limit = options?.limit;
321
+ if (limit !== undefined && (!Number.isFinite(limit) || limit <= 0)) {
322
+ throw new UsageError("--limit must be a positive integer.", "INVALID_FLAG_VALUE");
323
+ }
324
+ const { graph, stashPath, graphPath } = loadGraph(options?.source);
325
+ let db;
326
+ let refByPath;
327
+ try {
328
+ db = openExistingDatabase();
329
+ refByPath = buildRefByPath(stashPath, db);
330
+ }
331
+ finally {
332
+ if (db)
333
+ closeDatabase(db);
334
+ }
335
+ const orphans = [];
336
+ for (const node of graph.files) {
337
+ if ((node.status ?? (node.entities.length > 0 ? "extracted" : "empty")) === "extracted")
338
+ continue;
339
+ const lookup = refByPath.get(node.path);
340
+ orphans.push({
341
+ ...(lookup?.ref ? { ref: lookup.ref } : {}),
342
+ path: node.path,
343
+ type: node.type,
344
+ ...(node.status ? { status: node.status } : {}),
345
+ ...(node.reason ? { reason: node.reason } : {}),
346
+ });
347
+ }
348
+ orphans.sort((a, b) => a.type.localeCompare(b.type) || a.path.localeCompare(b.path));
349
+ const sliced = typeof limit === "number" ? orphans.slice(0, limit) : orphans;
350
+ return {
351
+ schemaVersion: 1,
352
+ shape: "graph-orphans",
353
+ stashPath,
354
+ graphPath,
355
+ generatedAt: graph.generatedAt,
356
+ totalConsidered: graph.files.length,
357
+ total: orphans.length,
358
+ orphans: sliced,
359
+ };
360
+ }
361
+ /**
362
+ * Re-run graph extraction, optionally scoped to specific asset refs.
363
+ *
364
+ * When `refs` is provided, only those files are re-extracted (incremental).
365
+ * When no refs are given, the full eligible set is re-extracted.
366
+ */
367
+ export async function akmGraphUpdate(options) {
368
+ const config = options.config ?? loadConfig();
369
+ const sources = resolveSourceEntries(options.stashDir, config);
370
+ if (sources.length === 0) {
371
+ throw new NotFoundError("No stash sources are configured.", "STASH_NOT_FOUND");
372
+ }
373
+ if (options.source && options.source !== "primary") {
374
+ const matched = sources.find((s) => s.registryId === options.source || s.path === options.source);
375
+ if (!matched) {
376
+ throw new NotFoundError(`Source not found: ${options.source}`, "SOURCE_NOT_FOUND", "Run `akm list` to see source names.");
377
+ }
378
+ }
379
+ const scoped = Array.isArray(options.refs) && options.refs.length > 0;
380
+ let candidatePaths;
381
+ if (scoped && options.refs) {
382
+ // Resolve each ref to an absolute file path via the index DB.
383
+ const dbPath = getDbPath();
384
+ let db;
385
+ const resolvedPaths = new Set();
386
+ try {
387
+ db = openDatabase(dbPath);
388
+ for (const ref of options.refs) {
389
+ const trimmed = ref.trim();
390
+ if (!trimmed)
391
+ continue;
392
+ const entryId = findEntryIdByRef(db, trimmed);
393
+ if (entryId === undefined) {
394
+ warn(`[graph] ref not found in index, skipping: ${trimmed}`);
395
+ continue;
396
+ }
397
+ const row = getEntryById(db, entryId);
398
+ if (!row?.filePath) {
399
+ warn(`[graph] could not resolve path for ref, skipping: ${trimmed}`);
400
+ continue;
401
+ }
402
+ resolvedPaths.add(row.filePath);
403
+ }
404
+ }
405
+ finally {
406
+ if (db)
407
+ closeDatabase(db);
408
+ }
409
+ if (resolvedPaths.size === 0) {
410
+ warn("[graph] none of the provided refs resolved to indexed paths — no extraction performed.");
411
+ return {
412
+ shape: "graph-update",
413
+ ok: true,
414
+ filesExtracted: 0,
415
+ entitiesUpserted: 0,
416
+ relationsUpserted: 0,
417
+ durationMs: 0,
418
+ scoped: true,
419
+ };
420
+ }
421
+ candidatePaths = resolvedPaths;
422
+ }
423
+ const extractionFn = options.graphExtractionFn ?? runGraphExtractionPass;
424
+ const passOptions = candidatePaths ? { candidatePaths } : {};
425
+ let db;
426
+ const startMs = Date.now();
427
+ try {
428
+ db = openDatabase(getDbPath());
429
+ const onProgress = (event) => {
430
+ if (!event.currentPath)
431
+ return;
432
+ const file = path.basename(event.currentPath);
433
+ warn(`[graph] extracting ${event.processed}/${event.total} ${file}`);
434
+ };
435
+ const result = await extractionFn(config, sources, undefined, db, false, onProgress, passOptions);
436
+ const durationMs = Date.now() - startMs;
437
+ return {
438
+ shape: "graph-update",
439
+ ok: true,
440
+ filesExtracted: result.quality.extractedFiles,
441
+ entitiesUpserted: result.quality.entityCount,
442
+ relationsUpserted: result.quality.relationCount,
443
+ durationMs,
444
+ scoped,
445
+ };
446
+ }
447
+ finally {
448
+ if (db)
449
+ closeDatabase(db);
450
+ }
451
+ }
197
452
  async function resolveGraphTarget(ref, source) {
198
453
  const parsedRef = parseAssetRef(ref);
199
454
  const filePath = (await resolveAssetPath(parsedRef, {