akm-cli 0.9.0-beta.9 → 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 (325) hide show
  1. package/CHANGELOG.md +592 -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 -21
  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 +156 -155
  57. package/dist/commands/graph/graph-cli.js +5 -13
  58. package/dist/commands/graph/graph.js +3 -3
  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 -1091
  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 +1295 -1277
  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 +228 -605
  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 +54 -3
  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 +157 -10
  97. package/dist/commands/improve/improve-cli.js +115 -73
  98. package/dist/commands/improve/improve-profiles.js +28 -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 +485 -2764
  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 +37 -35
  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 +206 -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 +2 -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 -895
  138. package/dist/commands/read/curate.js +410 -111
  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 +19 -39
  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 +382 -62
  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 +18 -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 +132 -1126
  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 +259 -769
  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 +36 -92
  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 +18 -11
  200. package/dist/indexer/index-written-assets.js +105 -0
  201. package/dist/indexer/indexer.js +182 -204
  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 +10 -0
  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 +34 -11
  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 +2661 -2369
  261. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +883 -596
  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/website.js +9 -5
  286. package/dist/sources/website-ingest.js +187 -29
  287. package/dist/sources/wiki-fetchers/registry.js +53 -0
  288. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  289. package/dist/storage/database.js +45 -10
  290. package/dist/storage/managed-db.js +82 -0
  291. package/dist/storage/repositories/canaries-repository.js +107 -0
  292. package/dist/storage/repositories/consolidation-repository.js +38 -0
  293. package/dist/storage/repositories/embeddings-repository.js +72 -0
  294. package/dist/storage/repositories/events-repository.js +187 -0
  295. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  296. package/dist/storage/repositories/improve-runs-repository.js +146 -0
  297. package/dist/storage/repositories/index-db.js +14 -8
  298. package/dist/storage/repositories/proposals-repository.js +220 -0
  299. package/dist/storage/repositories/recombine-repository.js +213 -0
  300. package/dist/storage/repositories/registry-cache.js +93 -0
  301. package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
  302. package/dist/storage/repositories/task-history-repository.js +93 -0
  303. package/dist/storage/sqlite-pragmas.js +146 -0
  304. package/dist/tasks/backends/cron.js +1 -1
  305. package/dist/tasks/backends/index.js +9 -0
  306. package/dist/tasks/backends/launchd.js +1 -1
  307. package/dist/tasks/backends/schtasks.js +1 -1
  308. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  309. package/dist/tasks/runner.js +15 -13
  310. package/dist/text-import-hook.mjs +0 -0
  311. package/dist/wiki/wiki.js +52 -11
  312. package/dist/workflows/cli.js +1 -0
  313. package/dist/workflows/db.js +3 -4
  314. package/dist/workflows/runtime/runs.js +43 -118
  315. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  316. package/dist/workflows/validate-summary.js +2 -7
  317. package/docs/README.md +69 -18
  318. package/docs/data-and-telemetry.md +5 -4
  319. package/docs/migration/release-notes/0.7.0.md +1 -1
  320. package/docs/migration/release-notes/0.9.0.md +39 -0
  321. package/package.json +10 -10
  322. package/dist/assets/tasks/core/update-stashes.yml +0 -4
  323. package/dist/commands/db-cli.js +0 -23
  324. package/dist/indexer/db/db-backup.js +0 -376
  325. package/dist/indexer/passes/staleness-detect.js +0 -488
@@ -0,0 +1,93 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * Default minimum total score a session must reach to PASS the gate. Tuned
6
+ * conservatively: a single real narrative-lesson session, or a procedurally
7
+ * dense session, clears it — but pure read-only Q&A does not. Config-overridable
8
+ * via `processes.extract.triage.minScore`.
9
+ */
10
+ export const DEFAULT_TRIAGE_MIN_SCORE = 2;
11
+ // Decision / outcome / error narrative markers. Case-insensitive, word-bounded.
12
+ const MARKER_RE = /\b(error|failed|fix(?:ed)?|root cause|turns out|because|decided|instead|gotcha|workaround|regress(?:ed)?|broke|TIL)\b/i;
13
+ // Edit / commit / file-write markers in tool event text.
14
+ const EDIT_COMMIT_RE = /\b(Edit|Write|MultiEdit|git commit|diff)\b/i;
15
+ // A substantive turn is an assistant or tool turn whose text is non-trivial.
16
+ const SUBSTANTIVE_MIN_CHARS = 40;
17
+ export function scoreSessionTriage(data, minScore, options) {
18
+ const events = data.events;
19
+ const total = events.length;
20
+ // (a) markers — count word-bounded marker hits across all event text, capped.
21
+ let markerHits = 0;
22
+ for (const e of events) {
23
+ if (MARKER_RE.test(e.text))
24
+ markerHits += 1;
25
+ }
26
+ // Each marker-bearing event contributes 1 point, capped at 2 (a single real
27
+ // narrative session clears the bar on markers alone).
28
+ const markers = Math.min(markerHits, 2);
29
+ // (b) toolDensity — bounded contribution from tool-use events.
30
+ let toolEvents = 0;
31
+ for (const e of events) {
32
+ if (e.role === "tool")
33
+ toolEvents += 1;
34
+ }
35
+ // 0.25 per tool event, capped at 1.5.
36
+ const toolDensity = Math.min(toolEvents * 0.25, 1.5);
37
+ // (c) editCommit — bounded contribution from edit/write/commit markers on
38
+ // events (filePath present, or text matching the edit/commit regex).
39
+ let editCommitEvents = 0;
40
+ for (const e of events) {
41
+ if (e.filePath || EDIT_COMMIT_RE.test(e.text))
42
+ editCommitEvents += 1;
43
+ }
44
+ // 0.25 per edit/commit event, capped at 1.5. Together with toolDensity a
45
+ // procedurally dense session reaches well above the default threshold.
46
+ const editCommit = Math.min(editCommitEvents * 0.25, 1.5);
47
+ // (d) substantiveRatio — scaled fraction of non-trivial assistant+tool turns.
48
+ let substantive = 0;
49
+ for (const e of events) {
50
+ if ((e.role === "assistant" || e.role === "tool") && e.text.length >= SUBSTANTIVE_MIN_CHARS) {
51
+ substantive += 1;
52
+ }
53
+ }
54
+ const ratio = total > 0 ? substantive / total : 0;
55
+ // Scale to a bounded [0,1] contribution.
56
+ const substantiveRatio = Math.min(ratio, 1);
57
+ const subscores = { markers, toolDensity, editCommit, substantiveRatio };
58
+ const score = markers + toolDensity + editCommit + substantiveRatio;
59
+ // Base gate: score must clear the floor.
60
+ let pass = score >= minScore;
61
+ // #641 procedural-aware floor (opt-in, DEFAULT OFF).
62
+ // When enabled, a session that clears the score gate must ALSO have at least
63
+ // one narrative marker (markers >= 1) OR meaningful edit/commit signal
64
+ // (editCommit >= 0.5). Pure read-only Q&A sessions that pass only via
65
+ // toolDensity + substantiveRatio are rejected as low-signal.
66
+ if (pass && options?.proceduralAwareFloor === true) {
67
+ const hasProceduralSignal = markers >= 1 || editCommit >= 0.5;
68
+ if (!hasProceduralSignal) {
69
+ pass = false;
70
+ }
71
+ }
72
+ return {
73
+ pass,
74
+ score,
75
+ subscores,
76
+ ...(pass ? {} : { reason: "low_signal" }),
77
+ };
78
+ }
79
+ /**
80
+ * Resolve the effective triage config from the extract process config. Mirrors
81
+ * the minContentChars / schemaSimilarity resolution style in akmExtract.
82
+ *
83
+ * Default-off: `enabled` is FALSE unless `triage.enabled === true`. `minScore`
84
+ * defaults to {@link DEFAULT_TRIAGE_MIN_SCORE}.
85
+ */
86
+ export function resolveTriageConfig(extractProcess) {
87
+ const triage = extractProcess?.triage;
88
+ const enabled = triage?.enabled === true;
89
+ const minScore = typeof triage?.minScore === "number" ? triage.minScore : DEFAULT_TRIAGE_MIN_SCORE;
90
+ // #641: default-off — only true when explicitly set to true.
91
+ const proceduralAwareFloor = triage?.proceduralAwareFloor === true;
92
+ return { enabled, minScore, proceduralAwareFloor };
93
+ }
@@ -1,22 +1,25 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
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
- import path from "node:path";
5
4
  import { BaseLinter } from "./base-linter.js";
5
+ /** Valid values for the `type` field in agent frontmatter. */
6
+ const VALID_AGENT_TYPES = ["agent"];
6
7
  /**
7
8
  * Linter for `agents/` assets.
8
9
  *
9
- * Extra check beyond base:
10
+ * Extra checks beyond base:
10
11
  * - `missing-name-or-type`: frontmatter exists but `name` or `type` field is
11
12
  * absent. Not auto-fixable; detail includes a suggested slug.
13
+ * - `missing-name-or-type` (invalid value): `type` is present but not a
14
+ * recognised agent type value.
12
15
  */
13
16
  export class AgentLinter extends BaseLinter {
14
17
  types = ["agents"];
15
18
  lint(ctx) {
16
19
  const issues = this.runBaseChecks(ctx);
17
- const missingFieldDetail = this.#checkMissingNameOrType(ctx.data, ctx.frontmatter);
20
+ const missingFieldDetail = this.checkMissingNameOrType(ctx.data, ctx.frontmatter);
18
21
  if (missingFieldDetail) {
19
- const slug = this.#suggestSlug(ctx.filePath);
22
+ const slug = this.suggestSlug(ctx.filePath);
20
23
  issues.push({
21
24
  file: ctx.relPath,
22
25
  issue: "missing-name-or-type",
@@ -24,26 +27,18 @@ export class AgentLinter extends BaseLinter {
24
27
  fixed: false,
25
28
  });
26
29
  }
30
+ else {
31
+ // Only validate the value when the field is actually present.
32
+ const invalidTypeDetail = this.checkInvalidTypeValue(ctx.data, VALID_AGENT_TYPES);
33
+ if (invalidTypeDetail) {
34
+ issues.push({
35
+ file: ctx.relPath,
36
+ issue: "missing-name-or-type",
37
+ detail: invalidTypeDetail,
38
+ fixed: false,
39
+ });
40
+ }
41
+ }
27
42
  return issues;
28
43
  }
29
- #checkMissingNameOrType(data, frontmatterText) {
30
- if (!frontmatterText)
31
- return null;
32
- const missingFields = [];
33
- if (!("name" in data) || !data.name)
34
- missingFields.push("name");
35
- if (!("type" in data) || !data.type)
36
- missingFields.push("type");
37
- if (missingFields.length === 0)
38
- return null;
39
- return `missing fields: ${missingFields.join(", ")}`;
40
- }
41
- #suggestSlug(filePath) {
42
- return path
43
- .basename(filePath, ".md")
44
- .toLowerCase()
45
- .replace(/[^a-z0-9-]+/g, "-")
46
- .replace(/-+/g, "-")
47
- .replace(/^-|-$/g, "");
48
- }
49
44
  }
@@ -39,7 +39,7 @@
39
39
  import fs from "node:fs";
40
40
  import path from "node:path";
41
41
  import { getAssetTypes, resolveAssetPathFromName, TYPE_DIRS } from "../../core/asset/asset-spec.js";
42
- import { findSafeInsertionPoint } from "./markdown-insertion.js";
42
+ import { findFenceRegions, findSafeInsertionPoint } from "./markdown-insertion.js";
43
43
  // ── Helpers ───────────────────────────────────────────────────────────────────
44
44
  function formatDate(d) {
45
45
  const y = d.getFullYear();
@@ -65,34 +65,69 @@ function checkUnquotedColon(frontmatterText) {
65
65
  return null;
66
66
  }
67
67
  function fixUnquotedColon(raw) {
68
- return raw.replace(/^(description:\s*)(.*)/m, (_match, prefix, value) => {
69
- const trimmed = value.trim();
70
- if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
71
- return _match;
72
- }
73
- const escaped = trimmed.replace(/"/g, '\\"');
74
- return `${prefix}"${escaped}"`;
75
- });
68
+ const lines = raw.split(/\r?\n/);
69
+ if (lines[0]?.trim() !== "---")
70
+ return raw;
71
+ const closeIdx = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
72
+ if (closeIdx === -1)
73
+ return raw;
74
+ for (let i = 1; i < closeIdx; i++) {
75
+ const m = lines[i].match(/^(description:\s*)(.*)/);
76
+ if (!m)
77
+ continue;
78
+ const prefix = m[1];
79
+ const value = m[2].trim();
80
+ if ((value.startsWith('"') && value.endsWith('"') && value.length >= 2) ||
81
+ (value.startsWith("'") && value.endsWith("'") && value.length >= 2))
82
+ continue;
83
+ lines[i] = `${prefix}"${value.replace(/"/g, '\\"')}"`;
84
+ break;
85
+ }
86
+ return lines.join("\n");
76
87
  }
77
88
  function checkMissingUpdated(data, frontmatterText) {
78
89
  return frontmatterText !== null && !("updated" in data);
79
90
  }
80
91
  function fixMissingUpdated(raw, mtime) {
81
92
  const dateStr = formatDate(mtime);
82
- return raw.replace(/^(---\n[\s\S]*?)\n---/m, `$1\nupdated: ${dateStr}\n---`);
93
+ const lines = raw.split(/\r?\n/);
94
+ if (lines[0]?.trim() !== "---")
95
+ return raw;
96
+ const closeIdx = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
97
+ if (closeIdx === -1)
98
+ return raw;
99
+ lines.splice(closeIdx, 0, `updated: ${dateStr}`);
100
+ return lines.join("\n");
83
101
  }
84
102
  // ── stale-path helpers ────────────────────────────────────────────────────────
85
103
  function checkStalePath(body) {
86
- const pathRe = /\/home\/[^\s"'`)\]>,]+/g;
104
+ const pathRe = /(?:\/home\/|\/tmp\/|\/var\/|\/root\/|\/opt\/)[^\s"'`)\]>,\n]+/g;
87
105
  let match;
106
+ const stale = [];
88
107
  // biome-ignore lint/suspicious/noAssignInExpressions: idiomatic regex loop
89
108
  while ((match = pathRe.exec(body)) !== null) {
90
109
  const candidate = match[0];
91
110
  if (!fs.existsSync(candidate)) {
92
- return candidate;
111
+ stale.push(candidate);
93
112
  }
94
113
  }
95
- return null;
114
+ return stale;
115
+ }
116
+ // ── fence-strip helper ────────────────────────────────────────────────────────
117
+ /**
118
+ * Returns `body` with all fenced code block lines replaced by empty strings,
119
+ * so that ref-shaped tokens inside ``` examples are not flagged as broken refs.
120
+ */
121
+ function stripFencedBlocks(body) {
122
+ const lines = body.split(/\r?\n/);
123
+ const regions = findFenceRegions(lines);
124
+ if (regions.length === 0)
125
+ return body;
126
+ for (const { start, end } of regions) {
127
+ for (let i = start; i <= end; i++)
128
+ lines[i] = "";
129
+ }
130
+ return lines.join("\n");
96
131
  }
97
132
  // ── missing-ref helpers ───────────────────────────────────────────────────────
98
133
  /**
@@ -122,9 +157,8 @@ const REF_RE = new RegExp(`(?:^|[\\s\`"'(])((${buildRefTypeAlternation()}):[^\\s
122
157
  * preserved to keep pre-0.9 behaviour byte-identical:
123
158
  * - `script`: returns null (scripts live in nested dirs with arbitrary
124
159
  * extensions — unresolvable by the slug-based walker, as the contract pins).
125
- * - `task`: the registry stores tasks as `<id>.yml`, but the missing-ref
126
- * linter has always resolved `task:` refs against `tasks/<id>.md`; that
127
- * behaviour is held constant here (non-env/secret behaviour is unchanged).
160
+ * - `task`: M1 fix tasks are stored as `<id>.yml` on disk, so resolve
161
+ * `task:` refs against `tasks/<id>.yml` to match actual on-disk layout.
128
162
  *
129
163
  * Exported for contract testing — see header CONTRACT block.
130
164
  */
@@ -132,9 +166,9 @@ export function refToRelPath(refType, refName) {
132
166
  // script is intentionally unresolvable (contract-pinned).
133
167
  if (refType === "script")
134
168
  return null;
135
- // Preserve the legacy `.md` resolution for tasks.
169
+ // M1: tasks are stored as .yml on disk; resolve task: refs against tasks/<id>.yml.
136
170
  if (refType === "task")
137
- return path.join(TYPE_DIRS.task ?? "tasks", `${refName}.md`);
171
+ return path.join(TYPE_DIRS.task ?? "tasks", `${refName}.yml`);
138
172
  const typeDir = TYPE_DIRS[refType];
139
173
  if (!typeDir)
140
174
  return null; // unknown type — skip
@@ -169,9 +203,11 @@ export function refExistsInAnyStash(relPath, refType, refName, stashRoots) {
169
203
  try {
170
204
  const knowledgeDir = path.join(root, "knowledge");
171
205
  if (fs.existsSync(knowledgeDir) && fs.statSync(knowledgeDir).isDirectory()) {
172
- const entries = fs.readdirSync(knowledgeDir);
206
+ const entries = fs.readdirSync(knowledgeDir, { withFileTypes: true });
173
207
  for (const entry of entries) {
174
- const subPath = path.join(knowledgeDir, entry, `${refName}.md`);
208
+ if (!entry.isDirectory())
209
+ continue;
210
+ const subPath = path.join(knowledgeDir, entry.name, `${refName}.md`);
175
211
  if (fs.existsSync(subPath))
176
212
  return true;
177
213
  }
@@ -207,8 +243,10 @@ function checkMissingRefs(body, stashRoot, extraStashRoots = []) {
207
243
  const missing = [];
208
244
  let match;
209
245
  const re = new RegExp(REF_RE.source, REF_RE.flags);
246
+ // C1: Strip fenced code blocks so example refs inside ``` are not flagged.
247
+ const scanBody = stripFencedBlocks(body);
210
248
  // biome-ignore lint/suspicious/noAssignInExpressions: idiomatic regex loop
211
- while ((match = re.exec(body)) !== null) {
249
+ while ((match = re.exec(scanBody)) !== null) {
212
250
  const fullRef = match[1]; // e.g. "workflow:foo" or "local//workflow:foo"
213
251
  // Skip shell variables: memory:$(cmd) or knowledge:${VAR}
214
252
  if (fullRef.includes("$(") || fullRef.includes("${")) {
@@ -241,6 +279,10 @@ function checkMissingRefs(body, stashRoot, extraStashRoots = []) {
241
279
  if (refName.length <= 1 || refName === "**") {
242
280
  continue;
243
281
  }
282
+ // Skip template placeholder refs like skill:<name> or workflow:<my-workflow>
283
+ if (refName.startsWith("<") || refName.includes("<")) {
284
+ continue;
285
+ }
244
286
  const relPath = refToRelPath(refType, refName);
245
287
  if (relPath === null)
246
288
  continue; // type is skipped
@@ -279,7 +321,15 @@ function extractFrontmatterRefs(data, body) {
279
321
  const fromInner = readRefsArray(innerData.refs);
280
322
  if (fromInner !== null)
281
323
  return fromInner;
324
+ // Session-checkpoint bodies are raw transcripts; ref-shaped tokens in the
325
+ // body are literals (grep patterns, JSON, tool output), not live refs.
326
+ // Return [] so missing-ref skips the body scan entirely.
327
+ if (typeof innerData.akm_memory_kind === "string")
328
+ return [];
282
329
  }
330
+ // Same guard for outer frontmatter (e.g. opencode session files).
331
+ if (typeof data.akm_memory_kind === "string")
332
+ return [];
283
333
  return null;
284
334
  }
285
335
  function readRefsArray(value) {
@@ -368,6 +418,47 @@ function parseInnerFrontmatterBlock(body) {
368
418
  * (in practice the base class updates `ctx.raw` in place when `fix` is true).
369
419
  */
370
420
  export class BaseLinter {
421
+ /**
422
+ * Check for missing `name` or `type` fields in frontmatter.
423
+ *
424
+ * Returns a detail string if fields are absent/empty, `null` if all present.
425
+ */
426
+ checkMissingNameOrType(data, frontmatterText) {
427
+ if (!frontmatterText)
428
+ return null;
429
+ const missingFields = [];
430
+ if (!("name" in data) || !data.name)
431
+ missingFields.push("name");
432
+ if (!("type" in data) || !data.type)
433
+ missingFields.push("type");
434
+ if (missingFields.length === 0)
435
+ return null;
436
+ return `missing fields: ${missingFields.join(", ")}`;
437
+ }
438
+ /**
439
+ * Validate that the `type` field value is one of an allowed set.
440
+ *
441
+ * Returns a detail string if the value is present but invalid, `null` if valid or absent.
442
+ */
443
+ checkInvalidTypeValue(data, allowedTypes) {
444
+ if (!("type" in data) || !data.type)
445
+ return null; // absent — covered by checkMissingNameOrType
446
+ const value = String(data.type);
447
+ if (allowedTypes.includes(value))
448
+ return null;
449
+ return `type field has invalid value '${value}'; expected one of: ${allowedTypes.join(", ")}`;
450
+ }
451
+ /**
452
+ * Derive a URL-safe slug from a file path.
453
+ */
454
+ suggestSlug(filePath) {
455
+ return path
456
+ .basename(filePath, ".md")
457
+ .toLowerCase()
458
+ .replace(/[^a-z0-9-]+/g, "-")
459
+ .replace(/-+/g, "-")
460
+ .replace(/^-|-$/g, "");
461
+ }
371
462
  /**
372
463
  * Insert one or more lines into a markdown body at a safe location.
373
464
  *
@@ -401,30 +492,41 @@ export class BaseLinter {
401
492
  const issues = [];
402
493
  let currentRaw = ctx.raw;
403
494
  let modified = false;
495
+ // M8: Parse lint_skip from frontmatter for per-file rule suppression.
496
+ // Accept both an array (`lint_skip: [missing-ref, stale-path]`) and a
497
+ // single scalar (`lint_skip: missing-ref`). Non-string entries are coerced
498
+ // and trimmed so loosely-typed YAML still gates correctly.
499
+ const rawLintSkip = ctx.data?.lint_skip;
500
+ const lintSkip = (Array.isArray(rawLintSkip) ? rawLintSkip : rawLintSkip != null ? [rawLintSkip] : [])
501
+ .map((v) => String(v).trim())
502
+ .filter(Boolean);
503
+ const shouldRun = (issueType) => !lintSkip.includes(issueType);
404
504
  // ── 1. unquoted-colon ──────────────────────────────────────────────────
405
- const unquotedColonDetail = checkUnquotedColon(ctx.frontmatter);
406
- if (unquotedColonDetail) {
407
- if (ctx.fix) {
408
- currentRaw = fixUnquotedColon(currentRaw);
409
- modified = true;
410
- issues.push({
411
- file: ctx.relPath,
412
- issue: "unquoted-colon",
413
- detail: unquotedColonDetail,
414
- fixed: true,
415
- });
416
- }
417
- else {
418
- issues.push({
419
- file: ctx.relPath,
420
- issue: "unquoted-colon",
421
- detail: unquotedColonDetail,
422
- fixed: false,
423
- });
505
+ if (shouldRun("unquoted-colon")) {
506
+ const unquotedColonDetail = checkUnquotedColon(ctx.frontmatter);
507
+ if (unquotedColonDetail) {
508
+ if (ctx.fix) {
509
+ currentRaw = fixUnquotedColon(currentRaw);
510
+ modified = true;
511
+ issues.push({
512
+ file: ctx.relPath,
513
+ issue: "unquoted-colon",
514
+ detail: unquotedColonDetail,
515
+ fixed: true,
516
+ });
517
+ }
518
+ else {
519
+ issues.push({
520
+ file: ctx.relPath,
521
+ issue: "unquoted-colon",
522
+ detail: unquotedColonDetail,
523
+ fixed: false,
524
+ });
525
+ }
424
526
  }
425
- }
527
+ } // end shouldRun("unquoted-colon")
426
528
  // ── 2. missing-updated ─────────────────────────────────────────────────
427
- if (checkMissingUpdated(ctx.data, ctx.frontmatter)) {
529
+ if (shouldRun("missing-updated") && checkMissingUpdated(ctx.data, ctx.frontmatter)) {
428
530
  if (ctx.fix) {
429
531
  let mtime;
430
532
  try {
@@ -457,14 +559,23 @@ export class BaseLinter {
457
559
  ctx.raw = currentRaw;
458
560
  }
459
561
  // ── 3. stale-path ──────────────────────────────────────────────────────
460
- const stalePathMatch = checkStalePath(ctx.body);
461
- if (stalePathMatch) {
462
- issues.push({
463
- file: ctx.relPath,
464
- issue: "stale-path",
465
- detail: `nonexistent path: ${stalePathMatch}`,
466
- fixed: false,
467
- });
562
+ // M3: checkStalePath returns all stale matches; push one issue per path.
563
+ // M4: Also scan ctx.frontmatter for stale paths (absolute paths in frontmatter).
564
+ if (shouldRun("stale-path")) {
565
+ const staleInBody = checkStalePath(ctx.body);
566
+ const staleInFrontmatter = ctx.frontmatter ? checkStalePath(ctx.frontmatter) : [];
567
+ for (const candidate of [...staleInBody, ...staleInFrontmatter]) {
568
+ // M4: Suggest portable replacement when path is under stashRoot.
569
+ const portableHint = candidate.startsWith(ctx.stashRoot)
570
+ ? ` (portable form: $AKM_STASH_DIR${candidate.slice(ctx.stashRoot.length)})`
571
+ : "";
572
+ issues.push({
573
+ file: ctx.relPath,
574
+ issue: "stale-path",
575
+ detail: `nonexistent path: ${candidate}${portableHint}`,
576
+ fixed: false,
577
+ });
578
+ }
468
579
  }
469
580
  // ── 4. missing-ref ─────────────────────────────────────────────────────
470
581
  // Carve-out for assets that declare an explicit `refs:` array in
@@ -481,16 +592,18 @@ export class BaseLinter {
481
592
  // still run `checkMissingRefs` against the array itself to catch
482
593
  // refs that were valid at capture time but later removed from the
483
594
  // stash.
484
- const explicitRefs = extractFrontmatterRefs(ctx.data, ctx.body);
485
- const refSource = explicitRefs !== null ? explicitRefs.join("\n") : ctx.body;
486
- const missingRefs = checkMissingRefs(refSource, ctx.stashRoot, ctx.extraStashRoots);
487
- for (const { ref, resolvedRelPath } of missingRefs) {
488
- issues.push({
489
- file: ctx.relPath,
490
- issue: "missing-ref",
491
- detail: `missing ref: ${ref} (resolved to ${resolvedRelPath})`,
492
- fixed: false,
493
- });
595
+ if (shouldRun("missing-ref")) {
596
+ const explicitRefs = extractFrontmatterRefs(ctx.data, ctx.body);
597
+ const refSource = explicitRefs !== null ? explicitRefs.join("\n") : ctx.body;
598
+ const missingRefs = checkMissingRefs(refSource, ctx.stashRoot, ctx.extraStashRoots);
599
+ for (const { ref, resolvedRelPath } of missingRefs) {
600
+ issues.push({
601
+ file: ctx.relPath,
602
+ issue: "missing-ref",
603
+ detail: `missing ref: ${ref} (resolved to ${resolvedRelPath})`,
604
+ fixed: false,
605
+ });
606
+ }
494
607
  }
495
608
  return issues;
496
609
  }
@@ -1,22 +1,25 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
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
- import path from "node:path";
5
4
  import { BaseLinter } from "./base-linter.js";
5
+ /** Valid values for the `type` field in command frontmatter. */
6
+ const VALID_COMMAND_TYPES = ["command"];
6
7
  /**
7
8
  * Linter for `commands/` assets.
8
9
  *
9
- * Extra check beyond base:
10
+ * Extra checks beyond base:
10
11
  * - `missing-name-or-type`: frontmatter exists but `name` or `type` field is
11
12
  * absent. Not auto-fixable; detail includes a suggested slug.
13
+ * - `missing-name-or-type` (invalid value): `type` is present but not a
14
+ * recognised command type value.
12
15
  */
13
16
  export class CommandLinter extends BaseLinter {
14
17
  types = ["commands"];
15
18
  lint(ctx) {
16
19
  const issues = this.runBaseChecks(ctx);
17
- const missingFieldDetail = this.#checkMissingNameOrType(ctx.data, ctx.frontmatter);
20
+ const missingFieldDetail = this.checkMissingNameOrType(ctx.data, ctx.frontmatter);
18
21
  if (missingFieldDetail) {
19
- const slug = this.#suggestSlug(ctx.filePath);
22
+ const slug = this.suggestSlug(ctx.filePath);
20
23
  issues.push({
21
24
  file: ctx.relPath,
22
25
  issue: "missing-name-or-type",
@@ -24,26 +27,18 @@ export class CommandLinter extends BaseLinter {
24
27
  fixed: false,
25
28
  });
26
29
  }
30
+ else {
31
+ // Only validate the value when the field is actually present.
32
+ const invalidTypeDetail = this.checkInvalidTypeValue(ctx.data, VALID_COMMAND_TYPES);
33
+ if (invalidTypeDetail) {
34
+ issues.push({
35
+ file: ctx.relPath,
36
+ issue: "missing-name-or-type",
37
+ detail: invalidTypeDetail,
38
+ fixed: false,
39
+ });
40
+ }
41
+ }
27
42
  return issues;
28
43
  }
29
- #checkMissingNameOrType(data, frontmatterText) {
30
- if (!frontmatterText)
31
- return null;
32
- const missingFields = [];
33
- if (!("name" in data) || !data.name)
34
- missingFields.push("name");
35
- if (!("type" in data) || !data.type)
36
- missingFields.push("type");
37
- if (missingFields.length === 0)
38
- return null;
39
- return `missing fields: ${missingFields.join(", ")}`;
40
- }
41
- #suggestSlug(filePath) {
42
- return path
43
- .basename(filePath, ".md")
44
- .toLowerCase()
45
- .replace(/[^a-z0-9-]+/g, "-")
46
- .replace(/-+/g, "-")
47
- .replace(/^-|-$/g, "");
48
- }
49
44
  }
@@ -24,6 +24,7 @@
24
24
  * where the operator legitimately wants to set their editor — accept the
25
25
  * FP and bypass with `--allow-insecure` after review.
26
26
  */
27
+ import fs from "node:fs";
27
28
  import { listKeys } from "../env/env.js";
28
29
  // ── Dangerous key set ─────────────────────────────────────────────────────────
29
30
  export const DANGEROUS_VAULT_KEYS = new Set([
@@ -95,6 +96,10 @@ export const DANGEROUS_VAULT_KEY_PATTERNS = [
95
96
  pattern: /^BASH_FUNC_/,
96
97
  reason: "Shellshock-class bash function injection (CVE-2014-6271)",
97
98
  },
99
+ {
100
+ pattern: /^GIT_CONFIG_/,
101
+ reason: "Git config injection through environment override variables",
102
+ },
98
103
  ];
99
104
  /**
100
105
  * Returns `true` if the given key name is dangerous — either by literal match
@@ -122,16 +127,48 @@ export function isDangerousVaultKey(key) {
122
127
  * @param vaultRef Human-readable vault ref (e.g. `"vault:prod"`) shown in
123
128
  * the finding message.
124
129
  */
130
+ /** Suppression comment token checked case-insensitively on the preceding non-empty line. */
131
+ const SUPPRESSION_COMMENT = "# akm-lint-ok: dangerous-vault-key";
132
+ /**
133
+ * Returns the set of keys suppressed by an inline `# akm-lint-ok: dangerous-vault-key`
134
+ * comment on the line immediately preceding the key assignment in the `.env` file.
135
+ */
136
+ function collectSuppressedKeys(vaultPath) {
137
+ const suppressed = new Set();
138
+ let raw;
139
+ try {
140
+ raw = fs.readFileSync(vaultPath, "utf8");
141
+ }
142
+ catch {
143
+ return suppressed;
144
+ }
145
+ const lines = raw.split(/\r?\n/);
146
+ let prevNonEmpty = "";
147
+ for (const line of lines) {
148
+ const trimmed = line.trim();
149
+ if (trimmed === "")
150
+ continue;
151
+ const keyMatch = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=/);
152
+ if (keyMatch && prevNonEmpty.toLowerCase() === SUPPRESSION_COMMENT) {
153
+ suppressed.add(keyMatch[1]);
154
+ }
155
+ prevNonEmpty = trimmed;
156
+ }
157
+ return suppressed;
158
+ }
125
159
  export function checkVaultForDangerousKeys(vaultPath, relPath, vaultRef) {
126
160
  const { keys } = listKeys(vaultPath);
161
+ const suppressed = collectSuppressedKeys(vaultPath);
127
162
  const issues = [];
128
163
  for (const key of keys) {
129
164
  if (!isDangerousVaultKey(key))
130
165
  continue;
166
+ if (suppressed.has(key))
167
+ continue;
131
168
  issues.push({
132
169
  file: relPath,
133
170
  issue: "dangerous-vault-key",
134
- detail: `Env key \`${key}\` can be used to hijack process execution when injected via \`akm env run\`. Ref: ${vaultRef}. Review this file before running \`akm env run\` commands against untrusted stashes.`,
171
+ detail: `Env key \`${key}\` can be used to hijack process execution when injected via \`akm env run\`. Ref: ${vaultRef}. Review this file before running \`akm env run\` commands against untrusted stashes. (suppress with: ${SUPPRESSION_COMMENT} on previous line)`,
135
172
  fixed: false,
136
173
  });
137
174
  }