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,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
+ });