akm-cli 0.9.0-beta.6 → 0.9.0-rc.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 (326) hide show
  1. package/CHANGELOG.md +663 -0
  2. package/README.md +12 -4
  3. package/dist/akm +38 -0
  4. package/dist/akm-migrate-storage +38 -0
  5. package/dist/assets/help/help-improve.md +9 -6
  6. package/dist/assets/hints/cli-hints-full.md +6 -5
  7. package/dist/assets/profiles/default.json +9 -4
  8. package/dist/assets/profiles/frequent.json +1 -1
  9. package/dist/assets/profiles/memory-focus.json +1 -1
  10. package/dist/assets/profiles/proactive-maintenance.json +25 -0
  11. package/dist/assets/profiles/quick.json +1 -1
  12. package/dist/assets/profiles/recombine-only.json +21 -0
  13. package/dist/assets/profiles/reflect-distill.json +30 -0
  14. package/dist/assets/profiles/synthesize.json +15 -0
  15. package/dist/assets/profiles/thorough.json +1 -1
  16. package/dist/assets/prompts/consolidate-system.md +23 -0
  17. package/dist/assets/prompts/contradiction-judge.md +33 -0
  18. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  19. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  20. package/dist/assets/prompts/extract-session.md +11 -3
  21. package/dist/assets/prompts/graph-extract-system.md +1 -0
  22. package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
  23. package/dist/assets/prompts/memory-infer-system.md +1 -0
  24. package/dist/assets/prompts/memory-infer-user.md +5 -0
  25. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  26. package/dist/assets/prompts/procedural-system.md +44 -0
  27. package/dist/assets/prompts/recombine-system.md +40 -0
  28. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  29. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
  39. package/dist/assets/templates/html/health.html +281 -111
  40. package/dist/assets/wiki/ingest-workflow-template.md +45 -16
  41. package/dist/assets/wiki/schema-template.md +4 -4
  42. package/dist/cli/clack.js +56 -0
  43. package/dist/cli/config-migrate.js +7 -1
  44. package/dist/cli/confirm.js +1 -1
  45. package/dist/cli/parse-args.js +46 -1
  46. package/dist/cli/shared.js +28 -0
  47. package/dist/cli.js +25 -14
  48. package/dist/commands/agent/agent-dispatch.js +3 -2
  49. package/dist/commands/agent/agent-support.js +0 -7
  50. package/dist/commands/agent/contribute-cli.js +26 -7
  51. package/dist/commands/config-cli.js +26 -13
  52. package/dist/commands/env/child-env.js +47 -0
  53. package/dist/commands/env/env-cli.js +220 -227
  54. package/dist/commands/env/env.js +14 -67
  55. package/dist/commands/env/secret-cli.js +140 -138
  56. package/dist/commands/feedback-cli.js +153 -147
  57. package/dist/commands/graph/graph-cli.js +5 -13
  58. package/dist/commands/graph/graph.js +76 -72
  59. package/dist/commands/health/advisories.js +151 -0
  60. package/dist/commands/health/checks.js +103 -16
  61. package/dist/commands/health/html-report.js +447 -81
  62. package/dist/commands/health/improve-metrics.js +771 -0
  63. package/dist/commands/health/llm-usage.js +65 -0
  64. package/dist/commands/health/md-report.js +103 -0
  65. package/dist/commands/health/metrics.js +278 -0
  66. package/dist/commands/health/stash-exposure.js +46 -0
  67. package/dist/commands/health/surfaces.js +216 -0
  68. package/dist/commands/health/task-runs.js +135 -0
  69. package/dist/commands/health/types.js +26 -0
  70. package/dist/commands/health/windows.js +195 -0
  71. package/dist/commands/health.js +91 -1083
  72. package/dist/commands/improve/anti-collapse.js +170 -0
  73. package/dist/commands/improve/calibration.js +161 -0
  74. package/dist/commands/improve/collapse-detector.js +421 -0
  75. package/dist/commands/improve/consolidate/chunking.js +141 -0
  76. package/dist/commands/improve/consolidate/eligibility.js +64 -0
  77. package/dist/commands/improve/consolidate/merge.js +145 -0
  78. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  79. package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
  80. package/dist/commands/improve/consolidate.js +1313 -1278
  81. package/dist/commands/improve/dedup.js +482 -0
  82. package/dist/commands/improve/distill/content-repair.js +202 -0
  83. package/dist/commands/improve/distill/promote-memory.js +229 -0
  84. package/dist/commands/improve/distill/quality-gate.js +236 -0
  85. package/dist/commands/improve/distill-guards.js +127 -0
  86. package/dist/commands/improve/distill-promotion-policy.js +826 -167
  87. package/dist/commands/improve/distill.js +243 -599
  88. package/dist/commands/improve/eligibility.js +434 -0
  89. package/dist/commands/improve/encoding-salience.js +205 -0
  90. package/dist/commands/improve/extract-cli.js +179 -59
  91. package/dist/commands/improve/extract-prompt.js +55 -4
  92. package/dist/commands/improve/extract-watch.js +140 -0
  93. package/dist/commands/improve/extract.js +409 -43
  94. package/dist/commands/improve/feedback-valence.js +54 -0
  95. package/dist/commands/improve/hot-probation.js +45 -0
  96. package/dist/commands/improve/improve-auto-accept.js +160 -7
  97. package/dist/commands/improve/improve-cli.js +115 -73
  98. package/dist/commands/improve/improve-profiles.js +32 -8
  99. package/dist/commands/improve/improve-result-file.js +15 -25
  100. package/dist/commands/improve/improve-session.js +58 -0
  101. package/dist/commands/improve/improve.js +510 -2537
  102. package/dist/commands/improve/locks.js +154 -0
  103. package/dist/commands/improve/loop-stages.js +1100 -0
  104. package/dist/commands/improve/memory/memory-belief.js +14 -15
  105. package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
  106. package/dist/commands/improve/memory/memory-improve.js +27 -27
  107. package/dist/commands/improve/outcome-loop.js +270 -0
  108. package/dist/commands/improve/preparation.js +2002 -0
  109. package/dist/commands/improve/proactive-maintenance.js +115 -0
  110. package/dist/commands/improve/procedural.js +398 -0
  111. package/dist/commands/improve/recombine.js +818 -0
  112. package/dist/commands/improve/reflect-noise.js +0 -0
  113. package/dist/commands/improve/reflect.js +212 -45
  114. package/dist/commands/improve/salience.js +455 -0
  115. package/dist/commands/improve/schema-similarity-gate.js +168 -0
  116. package/dist/commands/improve/shared.js +51 -0
  117. package/dist/commands/improve/triage.js +93 -0
  118. package/dist/commands/lint/agent-linter.js +19 -24
  119. package/dist/commands/lint/base-linter.js +173 -60
  120. package/dist/commands/lint/command-linter.js +19 -24
  121. package/dist/commands/lint/env-key-rules.js +38 -1
  122. package/dist/commands/lint/fact-linter.js +39 -0
  123. package/dist/commands/lint/index.js +31 -13
  124. package/dist/commands/lint/memory-linter.js +1 -1
  125. package/dist/commands/lint/registry.js +7 -2
  126. package/dist/commands/lint/task-linter.js +3 -3
  127. package/dist/commands/lint/workflow-linter.js +26 -1
  128. package/dist/commands/observability-cli.js +4 -4
  129. package/dist/commands/proposal/drain-policies.js +13 -4
  130. package/dist/commands/proposal/drain.js +45 -51
  131. package/dist/commands/proposal/legacy-import.js +115 -0
  132. package/dist/commands/proposal/proposal-cli.js +24 -34
  133. package/dist/commands/proposal/proposal.js +7 -1
  134. package/dist/commands/proposal/propose.js +8 -3
  135. package/dist/commands/proposal/repository.js +829 -0
  136. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  137. package/dist/commands/proposal/validators/proposals.js +93 -882
  138. package/dist/commands/read/curate.js +419 -103
  139. package/dist/commands/read/knowledge.js +10 -3
  140. package/dist/commands/read/remember-cli.js +133 -138
  141. package/dist/commands/read/search-cli.js +15 -8
  142. package/dist/commands/read/search.js +22 -11
  143. package/dist/commands/read/show.js +106 -14
  144. package/dist/commands/registry-cli.js +76 -87
  145. package/dist/commands/remember.js +11 -12
  146. package/dist/commands/sources/add-cli.js +91 -95
  147. package/dist/commands/sources/history.js +1 -1
  148. package/dist/commands/sources/init.js +66 -18
  149. package/dist/commands/sources/installed-stashes.js +11 -3
  150. package/dist/commands/sources/schema-repair.js +44 -46
  151. package/dist/commands/sources/self-update.js +2 -2
  152. package/dist/commands/sources/source-add.js +7 -3
  153. package/dist/commands/sources/sources-cli.js +3 -3
  154. package/dist/commands/sources/stash-cli.js +29 -41
  155. package/dist/commands/sources/stash-skeleton.js +57 -8
  156. package/dist/commands/tasks/default-tasks.js +15 -2
  157. package/dist/commands/tasks/tasks-cli.js +20 -29
  158. package/dist/commands/tasks/tasks.js +39 -11
  159. package/dist/commands/wiki-cli.js +23 -38
  160. package/dist/commands/workflow-cli.js +15 -1
  161. package/dist/core/asset/asset-registry.js +3 -1
  162. package/dist/core/asset/asset-spec.js +21 -4
  163. package/dist/core/asset/frontmatter.js +188 -167
  164. package/dist/core/asset/markdown.js +8 -0
  165. package/dist/core/authoring-rules.js +92 -0
  166. package/dist/core/common.js +4 -23
  167. package/dist/core/concurrent.js +10 -1
  168. package/dist/core/config/config-io.js +10 -1
  169. package/dist/core/config/config-migration.js +18 -40
  170. package/dist/core/config/config-schema.js +389 -58
  171. package/dist/core/config/config-types.js +3 -3
  172. package/dist/core/config/config.js +67 -22
  173. package/dist/core/deep-merge.js +38 -0
  174. package/dist/core/errors.js +1 -0
  175. package/dist/core/eval/rank-metrics.js +113 -0
  176. package/dist/core/events.js +4 -7
  177. package/dist/core/improve-types.js +47 -8
  178. package/dist/core/logs-db.js +14 -75
  179. package/dist/core/parse.js +36 -16
  180. package/dist/core/paths.js +21 -18
  181. package/dist/core/standards/resolve-standards-context.js +87 -0
  182. package/dist/core/standards/resolve-stash-standards.js +99 -0
  183. package/dist/core/standards/resolve-type-conventions.js +66 -0
  184. package/dist/core/state/migrations.js +770 -0
  185. package/dist/core/state-db.js +142 -1091
  186. package/dist/core/structured.js +69 -0
  187. package/dist/core/time.js +53 -0
  188. package/dist/core/warn.js +21 -0
  189. package/dist/core/write-source.js +37 -0
  190. package/dist/indexer/db/db.js +356 -780
  191. package/dist/indexer/db/entry-mapper.js +41 -0
  192. package/dist/indexer/db/graph-db.js +129 -86
  193. package/dist/indexer/db/llm-cache.js +2 -2
  194. package/dist/indexer/db/schema.js +516 -0
  195. package/dist/indexer/ensure-index.js +103 -24
  196. package/dist/indexer/feedback/utility-policy.js +75 -0
  197. package/dist/indexer/graph/graph-boost.js +51 -41
  198. package/dist/indexer/graph/graph-extraction.js +207 -4
  199. package/dist/indexer/index-writer-lock.js +106 -0
  200. package/dist/indexer/index-written-assets.js +105 -0
  201. package/dist/indexer/indexer.js +291 -310
  202. package/dist/indexer/passes/dir-staleness.js +114 -0
  203. package/dist/indexer/passes/memory-inference.js +13 -5
  204. package/dist/indexer/passes/metadata.js +20 -0
  205. package/dist/indexer/read-preflight.js +23 -0
  206. package/dist/indexer/search/db-search.js +89 -13
  207. package/dist/indexer/search/fts-query.js +51 -0
  208. package/dist/indexer/search/ranking-contributors.js +95 -9
  209. package/dist/indexer/search/ranking.js +79 -3
  210. package/dist/indexer/search/search-fields.js +6 -0
  211. package/dist/indexer/search/search-source.js +32 -21
  212. package/dist/indexer/search/semantic-status.js +4 -0
  213. package/dist/indexer/walk/matchers.js +9 -0
  214. package/dist/indexer/walk/walker.js +21 -13
  215. package/dist/integrations/agent/builders.js +39 -13
  216. package/dist/integrations/agent/config.js +20 -59
  217. package/dist/integrations/agent/detect.js +9 -0
  218. package/dist/integrations/agent/index.js +3 -19
  219. package/dist/integrations/agent/model-aliases.js +7 -2
  220. package/dist/integrations/agent/profiles.js +7 -1
  221. package/dist/integrations/agent/prompts.js +75 -9
  222. package/dist/integrations/agent/runner-dispatch.js +59 -0
  223. package/dist/integrations/agent/runner.js +13 -9
  224. package/dist/integrations/agent/spawn.js +69 -67
  225. package/dist/integrations/harnesses/claude/agent-builder.js +1 -1
  226. package/dist/integrations/harnesses/claude/index.js +2 -0
  227. package/dist/integrations/harnesses/claude/session-log.js +11 -1
  228. package/dist/integrations/harnesses/index.js +2 -3
  229. package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
  230. package/dist/integrations/harnesses/opencode/index.js +2 -0
  231. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  232. package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
  233. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +98 -17
  234. package/dist/integrations/harnesses/types.js +1 -0
  235. package/dist/integrations/session-logs/index.js +16 -0
  236. package/dist/llm/call-ai.js +2 -2
  237. package/dist/llm/client.js +57 -15
  238. package/dist/llm/embedder.js +67 -4
  239. package/dist/llm/embedders/cache.js +3 -1
  240. package/dist/llm/embedders/deterministic.js +66 -0
  241. package/dist/llm/embedders/local.js +73 -3
  242. package/dist/llm/feature-gate.js +16 -15
  243. package/dist/llm/graph-extract.js +67 -44
  244. package/dist/llm/memory-infer-impl.js +138 -0
  245. package/dist/llm/memory-infer.js +1 -127
  246. package/dist/llm/metadata-enhance.js +44 -31
  247. package/dist/llm/structured-call.js +49 -0
  248. package/dist/migrate-storage-node.mjs +8 -0
  249. package/dist/output/context.js +5 -5
  250. package/dist/output/renderers.js +85 -14
  251. package/dist/output/shapes/curate.js +14 -2
  252. package/dist/output/shapes/helpers.js +0 -3
  253. package/dist/output/shapes/passthrough.js +2 -1
  254. package/dist/output/text/helpers.js +29 -1
  255. package/dist/output/text/workflow.js +1 -0
  256. package/dist/registry/providers/skills-sh.js +21 -147
  257. package/dist/registry/providers/static-index.js +15 -157
  258. package/dist/registry/resolve.js +27 -9
  259. package/dist/runtime.js +25 -1
  260. package/dist/scripts/migrate-storage.js +2718 -2354
  261. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +891 -597
  262. package/dist/setup/detect.js +9 -0
  263. package/dist/setup/legacy-config.js +106 -0
  264. package/dist/setup/prompt.js +57 -0
  265. package/dist/setup/providers.js +14 -0
  266. package/dist/setup/registry-stash-loader.js +12 -0
  267. package/dist/setup/semantic-assets.js +124 -0
  268. package/dist/setup/setup.js +52 -1614
  269. package/dist/setup/steps/connection.js +734 -0
  270. package/dist/setup/steps/output.js +31 -0
  271. package/dist/setup/steps/platforms.js +124 -0
  272. package/dist/setup/steps/semantic.js +27 -0
  273. package/dist/setup/steps/sources.js +222 -0
  274. package/dist/setup/steps/stashdir.js +42 -0
  275. package/dist/setup/steps/tasks.js +152 -0
  276. package/dist/sources/include.js +6 -2
  277. package/dist/sources/providers/filesystem.js +0 -1
  278. package/dist/sources/providers/git-install.js +210 -0
  279. package/dist/sources/providers/git-provider.js +234 -0
  280. package/dist/sources/providers/git-stash.js +248 -0
  281. package/dist/sources/providers/git.js +10 -661
  282. package/dist/sources/providers/npm.js +2 -6
  283. package/dist/sources/providers/provider-utils.js +13 -7
  284. package/dist/sources/providers/sync-from-ref.js +9 -1
  285. package/dist/sources/providers/tar-utils.js +16 -8
  286. package/dist/sources/providers/website.js +9 -5
  287. package/dist/sources/website-ingest.js +187 -29
  288. package/dist/sources/wiki-fetchers/registry.js +53 -0
  289. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  290. package/dist/storage/database.js +45 -10
  291. package/dist/storage/managed-db.js +82 -0
  292. package/dist/storage/repositories/canaries-repository.js +107 -0
  293. package/dist/storage/repositories/consolidation-repository.js +38 -0
  294. package/dist/storage/repositories/embeddings-repository.js +72 -0
  295. package/dist/storage/repositories/events-repository.js +187 -0
  296. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  297. package/dist/storage/repositories/improve-runs-repository.js +146 -0
  298. package/dist/storage/repositories/index-db.js +14 -8
  299. package/dist/storage/repositories/proposals-repository.js +220 -0
  300. package/dist/storage/repositories/recombine-repository.js +213 -0
  301. package/dist/storage/repositories/registry-cache.js +93 -0
  302. package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
  303. package/dist/storage/repositories/task-history-repository.js +93 -0
  304. package/dist/storage/sqlite-pragmas.js +146 -0
  305. package/dist/tasks/backends/cron.js +1 -1
  306. package/dist/tasks/backends/index.js +9 -0
  307. package/dist/tasks/backends/launchd.js +1 -1
  308. package/dist/tasks/backends/schtasks.js +1 -1
  309. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  310. package/dist/tasks/runner.js +15 -13
  311. package/dist/text-import-hook.mjs +0 -0
  312. package/dist/wiki/wiki.js +52 -11
  313. package/dist/workflows/cli.js +1 -0
  314. package/dist/workflows/db.js +3 -4
  315. package/dist/workflows/runtime/runs.js +43 -118
  316. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  317. package/dist/workflows/validate-summary.js +2 -7
  318. package/docs/README.md +69 -18
  319. package/docs/data-and-telemetry.md +5 -4
  320. package/docs/migration/release-notes/0.7.0.md +1 -1
  321. package/docs/migration/release-notes/0.9.0.md +39 -0
  322. package/package.json +10 -10
  323. package/dist/assets/tasks/core/update-stashes.yml +0 -4
  324. package/dist/commands/db-cli.js +0 -23
  325. package/dist/indexer/db/db-backup.js +0 -376
  326. package/dist/indexer/passes/staleness-detect.js +0 -488
@@ -2,8 +2,7 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import fs from "node:fs";
5
- import { defineCommand } from "citty";
6
- import { output, parseAllFlagValues, runWithJsonErrors } from "../cli/shared.js";
5
+ import { defineJsonCommand, output, parseAllFlagValues } from "../cli/shared.js";
7
6
  import { parseAssetRef } from "../core/asset/asset-ref.js";
8
7
  import { assembleAsset } from "../core/asset/asset-serialize.js";
9
8
  import { parseFrontmatter, parseFrontmatterBlock } from "../core/asset/frontmatter.js";
@@ -11,10 +10,9 @@ import { writeFileAtomic } from "../core/common.js";
11
10
  import { FEEDBACK_FAILURE_MODES, loadConfig } from "../core/config/config.js";
12
11
  import { UsageError } from "../core/errors.js";
13
12
  import { appendEvent } from "../core/events.js";
13
+ import { getDbPath } from "../core/paths.js";
14
14
  import { warn } from "../core/warn.js";
15
15
  import { applyFeedbackToUtilityScore, closeDatabase, findEntryIdByRef, getEntryFilePathById, openExistingDatabase, } from "../indexer/db/db.js";
16
- import { ensureIndex } from "../indexer/ensure-index.js";
17
- import { resolveSourceEntries } from "../indexer/search/search-source.js";
18
16
  import { countFeedbackSignals, insertUsageEvent } from "../indexer/usage/usage-events.js";
19
17
  // ── Tag validation ────────────────────────────────────────────────────────────
20
18
  const TAG_KEY_RE = /^[a-z_][a-z0-9_]*$/;
@@ -109,7 +107,7 @@ function appendLessonStrength(type, name, feedbackRef) {
109
107
  return { strength: strengthList.length };
110
108
  }
111
109
  // ── Command definition ────────────────────────────────────────────────────────
112
- export const feedbackCommand = defineCommand({
110
+ export const feedbackCommand = defineJsonCommand({
113
111
  meta: {
114
112
  name: "feedback",
115
113
  description: "Record positive or negative feedback for any indexed stash asset.\n\n" +
@@ -153,163 +151,171 @@ export const feedbackCommand = defineCommand({
153
151
  "`lessonStrength[]` frontmatter array (dedup, idempotent). Ignored on non-lesson targets.",
154
152
  },
155
153
  },
156
- run({ args }) {
157
- return runWithJsonErrors(async () => {
158
- const ref = (args.ref ?? "").trim();
159
- if (!ref) {
160
- 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.");
154
+ async run({ args }) {
155
+ const ref = (args.ref ?? "").trim();
156
+ if (!ref) {
157
+ 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.");
158
+ }
159
+ parseAssetRef(ref);
160
+ if (args.positive && args.negative) {
161
+ throw new UsageError("Specify either --positive or --negative, not both.");
162
+ }
163
+ if (!args.positive && !args.negative) {
164
+ throw new UsageError("Specify --positive or --negative.");
165
+ }
166
+ const signal = args.positive ? "positive" : "negative";
167
+ const reason = args.reason;
168
+ // F-3 / #384: Validate --failure-mode against the curated enum.
169
+ const failureMode = args["failure-mode"]?.trim() || undefined;
170
+ if (failureMode) {
171
+ if (args.positive) {
172
+ throw new UsageError("--failure-mode is only valid for negative feedback.", "INVALID_FLAG_VALUE", "Remove --failure-mode or switch to --negative.");
161
173
  }
162
- parseAssetRef(ref);
163
- if (args.positive && args.negative) {
164
- throw new UsageError("Specify either --positive or --negative, not both.");
174
+ const cfg = loadConfig();
175
+ const allowedModes = cfg.feedback?.allowedFailureModes ?? FEEDBACK_FAILURE_MODES;
176
+ if (allowedModes.length > 0 && !allowedModes.includes(failureMode)) {
177
+ throw new UsageError(`Invalid --failure-mode "${failureMode}". Accepted values: ${allowedModes.join(", ")}.`, "INVALID_FLAG_VALUE", `Use one of: ${allowedModes.join(", ")}`);
165
178
  }
166
- if (!args.positive && !args.negative) {
167
- throw new UsageError("Specify --positive or --negative.");
179
+ }
180
+ if (args.negative === true && !reason?.trim()) {
181
+ // F-3 / #384: Default requireReason is now true. Load config to allow
182
+ // operators to opt out via feedback.requireReason: false in akm.json.
183
+ const cfg = loadConfig();
184
+ const requireReason = cfg.feedback?.requireReason ?? true; // Default: true (F-3 / #384)
185
+ if (requireReason) {
186
+ throw new UsageError("Negative feedback requires --reason (structured failure signals are needed for distillation). " +
187
+ "Use --failure-mode for a curated taxonomy or --reason for free text. " +
188
+ "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]`);
168
189
  }
169
- const signal = args.positive ? "positive" : "negative";
170
- const reason = args.reason;
171
- // F-3 / #384: Validate --failure-mode against the curated enum.
172
- const failureMode = args["failure-mode"]?.trim() || undefined;
173
- if (failureMode) {
174
- if (args.positive) {
175
- throw new UsageError("--failure-mode is only valid for negative feedback.", "INVALID_FLAG_VALUE", "Remove --failure-mode or switch to --negative.");
176
- }
177
- const cfg = loadConfig();
178
- const allowedModes = cfg.feedback?.allowedFailureModes ?? FEEDBACK_FAILURE_MODES;
179
- if (allowedModes.length > 0 && !allowedModes.includes(failureMode)) {
180
- throw new UsageError(`Invalid --failure-mode "${failureMode}". Accepted values: ${allowedModes.join(", ")}.`, "INVALID_FLAG_VALUE", `Use one of: ${allowedModes.join(", ")}`);
181
- }
190
+ else {
191
+ warn("Warning: negative feedback without --reason provides less distillation signal.");
182
192
  }
183
- if (args.negative === true && !reason?.trim()) {
184
- // F-3 / #384: Default requireReason is now true. Load config to allow
185
- // operators to opt out via feedback.requireReason: false in akm.json.
186
- const cfg = loadConfig();
187
- const requireReason = cfg.feedback?.requireReason ?? true; // Default: true (F-3 / #384)
188
- if (requireReason) {
189
- throw new UsageError("Negative feedback requires --reason (structured failure signals are needed for distillation). " +
190
- "Use --failure-mode for a curated taxonomy or --reason for free text. " +
191
- "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]`);
192
- }
193
- else {
194
- warn("Warning: negative feedback without --reason provides less distillation signal.");
195
- }
193
+ }
194
+ const rawTags = parseAllFlagValues("--tag");
195
+ const validatedTags = validateFeedbackTags(rawTags);
196
+ const metadataObj = {
197
+ signal,
198
+ ...(reason?.trim() ? { reason: reason.trim() } : {}),
199
+ ...(failureMode ? { failureMode } : {}),
200
+ ...(validatedTags.length > 0 ? { tags: validatedTags } : {}),
201
+ };
202
+ const metadataStr = Object.keys(metadataObj).length > 1 ? JSON.stringify(metadataObj) : undefined;
203
+ // Feedback only needs the index to exist, not to be current. A stale index
204
+ // is fine the ref lookup works against any populated DB. We do NOT call
205
+ // ensureIndex here: it either blocks (3+ min inline reindex) or spawns a
206
+ // background process that holds the writer lock, causing the feedback write
207
+ // to spin-wait for the full reindex duration. If the DB is absent we give a
208
+ // clear error below rather than silently triggering a rebuild.
209
+ if (!fs.existsSync(getDbPath())) {
210
+ throw new UsageError("Index not found. Run 'akm index' first to build the index before recording feedback.", "MISSING_REQUIRED_ARGUMENT", "akm index");
211
+ }
212
+ // Feedback writes exactly 2 rows (usage_events + utility_score). SQLite
213
+ // WAL mode + busy_timeout=30s handles concurrent access with an ongoing
214
+ // `akm improve` run without needing the application-level writer lock.
215
+ // The lock was originally needed to prevent feedback from racing a
216
+ // background reindex it spawned — now that ensureIndex is removed, holding
217
+ // the lock only causes feedback to block for the full improve run duration.
218
+ let utilityResult;
219
+ const db = openExistingDatabase();
220
+ try {
221
+ const entryId = findEntryIdByRef(db, ref);
222
+ if (entryId === undefined) {
223
+ throw new UsageError(`Ref "${ref}" is not in the index. ` +
224
+ "Run 'akm search' to verify the asset exists, then 'akm index' if it was recently added.");
196
225
  }
197
- const rawTags = parseAllFlagValues("--tag");
198
- const validatedTags = validateFeedbackTags(rawTags);
199
- const metadataObj = {
226
+ // Persist the feedback signal into usage_events. For positive signals,
227
+ // the EMA utility score is updated immediately on the next read path.
228
+ // For negative signals, the score is adjusted the next time `akm index`
229
+ // runs — the signal is durable in the DB but does NOT suppress ranking
230
+ // in search results until after reindexing.
231
+ insertUsageEvent(db, {
232
+ event_type: "feedback",
233
+ entry_ref: ref,
234
+ entry_id: entryId,
200
235
  signal,
201
- ...(reason?.trim() ? { reason: reason.trim() } : {}),
202
- ...(failureMode ? { failureMode } : {}),
203
- ...(validatedTags.length > 0 ? { tags: validatedTags } : {}),
204
- };
205
- const metadataStr = Object.keys(metadataObj).length > 1 ? JSON.stringify(metadataObj) : undefined;
206
- // Auto-index when stale so the index is current before recording feedback.
207
- const sources = resolveSourceEntries();
208
- if (sources.length > 0) {
209
- await ensureIndex(sources[0].path);
236
+ metadata: metadataStr,
237
+ });
238
+ // Apply feedback-derived utility score adjustment immediately so that
239
+ // positive/negative signals influence search ranking without requiring
240
+ // a full reindex. We query the total accumulated feedback counts from
241
+ // usage_events so the delta reflects the entire signal history.
242
+ // Uses MemRL bounded-step EMA (F-5 / #386, arXiv:2601.03192).
243
+ try {
244
+ const { pos, neg } = countFeedbackSignals(db, entryId);
245
+ utilityResult = applyFeedbackToUtilityScore(db, entryId, pos, neg);
210
246
  }
211
- let utilityResult;
212
- const db = openExistingDatabase();
247
+ catch {
248
+ // best-effort feedback recording succeeds even if utility update fails
249
+ }
250
+ }
251
+ finally {
252
+ closeDatabase(db);
253
+ }
254
+ appendEvent({
255
+ eventType: "feedback",
256
+ ref,
257
+ metadata: metadataObj,
258
+ });
259
+ // F-5 / #386: When a high-utility asset crosses below the review threshold,
260
+ // auto-create a review-needed escalation proposal so a human can confirm
261
+ // whether the negative feedback is valid before the asset falls out of
262
+ // the improve loop. Best-effort — failure is logged but does not fail the
263
+ // feedback command.
264
+ // Emit a structured event rather than a proposal so the review-needed
265
+ // signal is queryable via `akm events list --type improve_review_needed`
266
+ // without risking accidental asset overwrite if the proposal is accepted.
267
+ if (utilityResult?.crossedReviewThreshold) {
213
268
  try {
214
- const entryId = findEntryIdByRef(db, ref);
215
- if (entryId === undefined) {
216
- throw new UsageError(`Ref "${ref}" is not in the index. ` +
217
- "Run 'akm search' to verify the asset exists, then 'akm index' if it was recently added.");
218
- }
219
- // Persist the feedback signal into usage_events. For positive signals,
220
- // the EMA utility score is updated immediately on the next read path.
221
- // For negative signals, the score is adjusted the next time `akm index`
222
- // runs — the signal is durable in the DB but does NOT suppress ranking
223
- // in search results until after reindexing.
224
- insertUsageEvent(db, {
225
- event_type: "feedback",
226
- entry_ref: ref,
227
- entry_id: entryId,
228
- signal,
229
- metadata: metadataStr,
269
+ appendEvent({
270
+ eventType: "improve_review_needed",
271
+ ref,
272
+ metadata: {
273
+ previousUtility: utilityResult.previousUtility,
274
+ nextUtility: utilityResult.nextUtility,
275
+ reason: reason?.trim() ?? null,
276
+ failureMode: failureMode ?? null,
277
+ },
230
278
  });
231
- // Apply feedback-derived utility score adjustment immediately so that
232
- // positive/negative signals influence search ranking without requiring
233
- // a full reindex. We query the total accumulated feedback counts from
234
- // usage_events so the delta reflects the entire signal history.
235
- // Uses MemRL bounded-step EMA (F-5 / #386, arXiv:2601.03192).
236
- try {
237
- const { pos, neg } = countFeedbackSignals(db, entryId);
238
- utilityResult = applyFeedbackToUtilityScore(db, entryId, pos, neg);
239
- }
240
- catch {
241
- // best-effort — feedback recording succeeds even if utility update fails
242
- }
243
279
  }
244
- finally {
245
- closeDatabase(db);
246
- }
247
- appendEvent({
248
- eventType: "feedback",
249
- ref,
250
- metadata: metadataObj,
251
- });
252
- // F-5 / #386: When a high-utility asset crosses below the review threshold,
253
- // auto-create a review-needed escalation proposal so a human can confirm
254
- // whether the negative feedback is valid before the asset falls out of
255
- // the improve loop. Best-effort — failure is logged but does not fail the
256
- // feedback command.
257
- // Emit a structured event rather than a proposal so the review-needed
258
- // signal is queryable via `akm events list --type improve_review_needed`
259
- // without risking accidental asset overwrite if the proposal is accepted.
260
- if (utilityResult?.crossedReviewThreshold) {
261
- try {
262
- appendEvent({
263
- eventType: "improve_review_needed",
264
- ref,
265
- metadata: {
266
- previousUtility: utilityResult.previousUtility,
267
- nextUtility: utilityResult.nextUtility,
268
- reason: reason?.trim() ?? null,
269
- failureMode: failureMode ?? null,
270
- },
271
- });
272
- }
273
- catch (escalationErr) {
274
- warn(`[feedback] Could not emit review-needed event for ${ref}: ${escalationErr instanceof Error ? escalationErr.message : String(escalationErr)}`);
275
- }
280
+ catch (escalationErr) {
281
+ warn(`[feedback] Could not emit review-needed event for ${ref}: ${escalationErr instanceof Error ? escalationErr.message : String(escalationErr)}`);
276
282
  }
277
- // Phase 7A / Advantage D4b: --applied-to credits a lesson. When the
278
- // target is a `lesson:<name>` ref and the signal is positive, append
279
- // the feedback ref to the target lesson's `lessonStrength[]`
280
- // frontmatter array (dedup, idempotent). Non-lesson targets are
281
- // ignored. Failures here are warnings feedback recording is the
282
- // primary contract and must not regress on lesson-write errors.
283
- const appliedToRaw = args["applied-to"]?.trim();
284
- let appliedToResult = null;
285
- if (appliedToRaw && signal === "positive") {
286
- try {
287
- const parsedApplied = parseAssetRef(appliedToRaw);
288
- if (parsedApplied.type === "lesson") {
289
- const updated = appendLessonStrength(parsedApplied.type, parsedApplied.name, ref);
290
- if (updated) {
291
- appliedToResult = { lessonRef: appliedToRaw, strength: updated.strength };
292
- }
283
+ }
284
+ // Phase 7A / Advantage D4b: --applied-to credits a lesson. When the
285
+ // target is a `lesson:<name>` ref and the signal is positive, append
286
+ // the feedback ref to the target lesson's `lessonStrength[]`
287
+ // frontmatter array (dedup, idempotent). Non-lesson targets are
288
+ // ignored. Failures here are warnings feedback recording is the
289
+ // primary contract and must not regress on lesson-write errors.
290
+ const appliedToRaw = args["applied-to"]?.trim();
291
+ let appliedToResult = null;
292
+ if (appliedToRaw && signal === "positive") {
293
+ try {
294
+ const parsedApplied = parseAssetRef(appliedToRaw);
295
+ if (parsedApplied.type === "lesson") {
296
+ const updated = appendLessonStrength(parsedApplied.type, parsedApplied.name, ref);
297
+ if (updated) {
298
+ appliedToResult = { lessonRef: appliedToRaw, strength: updated.strength };
293
299
  }
294
300
  }
295
- catch (err) {
296
- warn(`[feedback] --applied-to failed for ${appliedToRaw}: ${err instanceof Error ? err.message : String(err)}`);
297
- }
298
301
  }
299
- else if (appliedToRaw && signal !== "positive") {
300
- warn("[feedback] --applied-to is ignored without --positive; lesson credit is only recorded on positive signals.");
302
+ catch (err) {
303
+ warn(`[feedback] --applied-to failed for ${appliedToRaw}: ${err instanceof Error ? err.message : String(err)}`);
301
304
  }
302
- output("feedback", {
303
- ok: true,
304
- ref,
305
- signal,
306
- reason: reason?.trim() ?? null,
307
- failureMode: failureMode ?? null,
308
- tags: validatedTags,
309
- ...(appliedToResult
310
- ? { appliedTo: { ref: appliedToResult.lessonRef, lessonStrength: appliedToResult.strength } }
311
- : {}),
312
- });
305
+ }
306
+ else if (appliedToRaw && signal !== "positive") {
307
+ warn("[feedback] --applied-to is ignored without --positive; lesson credit is only recorded on positive signals.");
308
+ }
309
+ output("feedback", {
310
+ ok: true,
311
+ ref,
312
+ signal,
313
+ reason: reason?.trim() ?? null,
314
+ failureMode: failureMode ?? null,
315
+ tags: validatedTags,
316
+ ...(appliedToResult
317
+ ? { appliedTo: { ref: appliedToResult.lessonRef, lessonStrength: appliedToResult.strength } }
318
+ : {}),
313
319
  });
314
320
  },
315
321
  });
@@ -9,12 +9,9 @@
9
9
  * same JSON envelope (stdout/stderr/exit-code) as the inline `runWithJsonErrors`
10
10
  * form it replaces.
11
11
  */
12
- import { defineCommand } from "citty";
13
- import { hasSubcommand, parsePositiveIntFlag } from "../../cli/parse-args.js";
14
- import { defineJsonCommand, output, runWithJsonErrors } from "../../cli/shared.js";
12
+ import { parsePositiveIntFlag } from "../../cli/parse-args.js";
13
+ import { defineGroupCommand, defineJsonCommand, output } from "../../cli/shared.js";
15
14
  import { akmGraphEntities, akmGraphEntity, akmGraphExport, akmGraphOrphans, akmGraphRelated, akmGraphRelations, akmGraphSummary, akmGraphUpdate, } from "./graph.js";
16
- // Single source of truth: the routing set is derived from the subCommands keys
17
- // (M10) so adding a subcommand can never silently desync from `hasSubcommand`.
18
15
  const graphSubCommands = {
19
16
  summary: defineJsonCommand({
20
17
  meta: { name: "summary", description: "Show entity-graph counts and quality telemetry" },
@@ -118,15 +115,10 @@ const graphSubCommands = {
118
115
  },
119
116
  }),
120
117
  };
121
- const GRAPH_SUBCOMMAND_SET = new Set(Object.keys(graphSubCommands));
122
- export const graphCommand = defineCommand({
118
+ export const graphCommand = defineGroupCommand({
123
119
  meta: { name: "graph", description: "Inspect the indexed entity graph stored in SQLite" },
124
120
  subCommands: graphSubCommands,
125
- run({ args }) {
126
- return runWithJsonErrors(() => {
127
- if (hasSubcommand(args, GRAPH_SUBCOMMAND_SET))
128
- return;
129
- output("graph-summary", akmGraphSummary());
130
- });
121
+ defaultRun() {
122
+ output("graph-summary", akmGraphSummary());
131
123
  },
132
124
  });
@@ -8,10 +8,11 @@ import { loadConfig } from "../../core/config/config.js";
8
8
  import { NotFoundError, UsageError } from "../../core/errors.js";
9
9
  import { getDbPath } from "../../core/paths.js";
10
10
  import { warn } from "../../core/warn.js";
11
- import { closeDatabase, findEntryIdByRef, getEntryById, getEntryRefRowsForStashRoot, openDatabase, openExistingDatabase, } from "../../indexer/db/db.js";
11
+ import { closeDatabase, findEntryIdByRef, getEntryById, getEntryRefRowsForStashRoot, openExistingDatabase, openIndexDatabase, } from "../../indexer/db/db.js";
12
12
  import { loadStoredGraphSnapshot } from "../../indexer/db/graph-db.js";
13
13
  import { listRelatedPathsForFile } from "../../indexer/graph/graph-boost.js";
14
14
  import { runGraphExtractionPass } from "../../indexer/graph/graph-extraction.js";
15
+ import { withIndexWriterLease } from "../../indexer/index-writer-lock.js";
15
16
  import { lookup } from "../../indexer/indexer.js";
16
17
  import { findSourceForPath, resolveSourceEntries } from "../../indexer/search/search-source.js";
17
18
  import { resolveAssetPath } from "../../indexer/walk/path-resolver.js";
@@ -375,85 +376,88 @@ export async function akmGraphUpdate(options) {
375
376
  }
376
377
  }
377
378
  const scoped = Array.isArray(options.refs) && options.refs.length > 0;
378
- let candidatePaths;
379
- if (scoped && options.refs) {
380
- // Resolve each ref to an absolute file path via the index DB.
381
- const dbPath = getDbPath();
382
- let db;
383
- const resolvedPaths = new Set();
384
- try {
385
- db = openDatabase(dbPath);
386
- for (const ref of options.refs) {
387
- const trimmed = ref.trim();
388
- if (!trimmed)
389
- continue;
390
- const entryId = findEntryIdByRef(db, trimmed);
391
- if (entryId === undefined) {
392
- warn(`[graph] ref not found in index, skipping: ${trimmed}`);
393
- continue;
379
+ return withIndexWriterLease({ purpose: "graph-update" }, async () => {
380
+ let candidatePaths;
381
+ if (scoped && options.refs) {
382
+ // Resolve each ref to an absolute file path while the writer lease is held
383
+ // so the scoped graph write sees the same index snapshot it resolved from.
384
+ const dbPath = getDbPath();
385
+ let db;
386
+ const resolvedPaths = new Set();
387
+ try {
388
+ db = openIndexDatabase(dbPath);
389
+ for (const ref of options.refs) {
390
+ const trimmed = ref.trim();
391
+ if (!trimmed)
392
+ continue;
393
+ const entryId = findEntryIdByRef(db, trimmed);
394
+ if (entryId === undefined) {
395
+ warn(`[graph] ref not found in index, skipping: ${trimmed}`);
396
+ continue;
397
+ }
398
+ const row = getEntryById(db, entryId);
399
+ if (!row?.filePath) {
400
+ warn(`[graph] could not resolve path for ref, skipping: ${trimmed}`);
401
+ continue;
402
+ }
403
+ resolvedPaths.add(row.filePath);
394
404
  }
395
- const row = getEntryById(db, entryId);
396
- if (!row?.filePath) {
397
- warn(`[graph] could not resolve path for ref, skipping: ${trimmed}`);
398
- continue;
399
- }
400
- resolvedPaths.add(row.filePath);
401
405
  }
406
+ finally {
407
+ if (db)
408
+ closeDatabase(db);
409
+ }
410
+ if (resolvedPaths.size === 0) {
411
+ warn("[graph] none of the provided refs resolved to indexed paths — no extraction performed.");
412
+ return {
413
+ shape: "graph-update",
414
+ ok: true,
415
+ filesExtracted: 0,
416
+ entitiesUpserted: 0,
417
+ relationsUpserted: 0,
418
+ durationMs: 0,
419
+ scoped: true,
420
+ };
421
+ }
422
+ candidatePaths = resolvedPaths;
402
423
  }
403
- finally {
404
- if (db)
405
- closeDatabase(db);
406
- }
407
- if (resolvedPaths.size === 0) {
408
- warn("[graph] none of the provided refs resolved to indexed paths — no extraction performed.");
424
+ const extractionFn = options.graphExtractionFn ?? runGraphExtractionPass;
425
+ const passOptions = candidatePaths ? { candidatePaths } : {};
426
+ let db;
427
+ const startMs = Date.now();
428
+ try {
429
+ db = openIndexDatabase(getDbPath());
430
+ const onProgress = (event) => {
431
+ if (!event.currentPath)
432
+ return;
433
+ const file = path.basename(event.currentPath);
434
+ warn(`[graph] extracting ${event.processed}/${event.total} ${file}`);
435
+ };
436
+ const result = await extractionFn({
437
+ config,
438
+ sources,
439
+ signal: undefined,
440
+ db,
441
+ reEnrich: false,
442
+ onProgress,
443
+ options: passOptions,
444
+ });
445
+ const durationMs = Date.now() - startMs;
409
446
  return {
410
447
  shape: "graph-update",
411
448
  ok: true,
412
- filesExtracted: 0,
413
- entitiesUpserted: 0,
414
- relationsUpserted: 0,
415
- durationMs: 0,
416
- scoped: true,
449
+ filesExtracted: result.quality.extractedFiles,
450
+ entitiesUpserted: result.quality.entityCount,
451
+ relationsUpserted: result.quality.relationCount,
452
+ durationMs,
453
+ scoped,
417
454
  };
418
455
  }
419
- candidatePaths = resolvedPaths;
420
- }
421
- const extractionFn = options.graphExtractionFn ?? runGraphExtractionPass;
422
- const passOptions = candidatePaths ? { candidatePaths } : {};
423
- let db;
424
- const startMs = Date.now();
425
- try {
426
- db = openDatabase(getDbPath());
427
- const onProgress = (event) => {
428
- if (!event.currentPath)
429
- return;
430
- const file = path.basename(event.currentPath);
431
- warn(`[graph] extracting ${event.processed}/${event.total} ${file}`);
432
- };
433
- const result = await extractionFn({
434
- config,
435
- sources,
436
- signal: undefined,
437
- db,
438
- reEnrich: false,
439
- onProgress,
440
- options: passOptions,
441
- });
442
- const durationMs = Date.now() - startMs;
443
- return {
444
- shape: "graph-update",
445
- ok: true,
446
- filesExtracted: result.quality.extractedFiles,
447
- entitiesUpserted: result.quality.entityCount,
448
- relationsUpserted: result.quality.relationCount,
449
- durationMs,
450
- scoped,
451
- };
452
- }
453
- finally {
454
- if (db)
455
- closeDatabase(db);
456
- }
456
+ finally {
457
+ if (db)
458
+ closeDatabase(db);
459
+ }
460
+ });
457
461
  }
458
462
  async function resolveGraphTarget(ref, source) {
459
463
  const parsedRef = parseAssetRef(ref);