akm-cli 0.8.7 → 0.8.14

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 (324) hide show
  1. package/CHANGELOG.md +428 -0
  2. package/dist/assets/help/help-proposals.md +1 -2
  3. package/dist/assets/hints/cli-hints-full.md +34 -19
  4. package/dist/assets/hints/cli-hints-short.md +1 -1
  5. package/dist/assets/profiles/catchup.json +13 -0
  6. package/dist/assets/profiles/consolidate.json +13 -0
  7. package/dist/assets/profiles/frequent.json +13 -0
  8. package/dist/assets/tasks/core/backup.yml +4 -0
  9. package/dist/assets/tasks/core/extract.yml +4 -0
  10. package/dist/assets/tasks/core/improve.yml +4 -0
  11. package/dist/assets/tasks/core/index-refresh.yml +4 -0
  12. package/dist/assets/tasks/core/sync.yml +4 -0
  13. package/dist/assets/tasks/core/update-stashes.yml +4 -0
  14. package/dist/assets/tasks/core/version-check.yml +4 -0
  15. package/dist/assets/templates/html/default.html +78 -0
  16. package/dist/assets/templates/html/health.html +560 -0
  17. package/dist/assets/templates/html/vendor/echarts.min.js +45 -0
  18. package/dist/cli/config-migrate.js +6 -6
  19. package/dist/cli/config-validate.js +4 -4
  20. package/dist/cli/confirm.js +3 -3
  21. package/dist/cli/parse-args.js +1 -1
  22. package/dist/cli/shared.js +72 -19
  23. package/dist/cli-node.mjs +26 -0
  24. package/dist/cli.js +206 -3866
  25. package/dist/commands/{agent-dispatch.js → agent/agent-dispatch.js} +6 -6
  26. package/dist/commands/{agent-support.js → agent/agent-support.js} +2 -2
  27. package/dist/commands/agent/contribute-cli.js +200 -0
  28. package/dist/commands/completions.js +1 -1
  29. package/dist/commands/config-cli.js +230 -3
  30. package/dist/commands/db-cli.js +2 -2
  31. package/dist/commands/env/env-cli.js +529 -0
  32. package/dist/commands/env/env.js +410 -0
  33. package/dist/commands/env/secret-cli.js +259 -0
  34. package/dist/commands/{secret.js → env/secret.js} +6 -47
  35. package/dist/commands/events.js +4 -4
  36. package/dist/commands/feedback-cli.js +18 -34
  37. package/dist/commands/graph/graph-cli.js +132 -0
  38. package/dist/commands/{graph.js → graph/graph.js} +22 -16
  39. package/dist/commands/health/checks.js +279 -0
  40. package/dist/commands/health/html-report.js +448 -0
  41. package/dist/commands/health.js +189 -266
  42. package/dist/commands/{consolidate.js → improve/consolidate.js} +48 -36
  43. package/dist/commands/{distill-promotion-policy.js → improve/distill-promotion-policy.js} +3 -3
  44. package/dist/commands/{distill.js → improve/distill.js} +39 -18
  45. package/dist/commands/{eval-cases.js → improve/eval-cases.js} +1 -1
  46. package/dist/commands/{extract-cli.js → improve/extract-cli.js} +4 -4
  47. package/dist/commands/{extract-prompt.js → improve/extract-prompt.js} +2 -2
  48. package/dist/commands/{extract.js → improve/extract.js} +221 -26
  49. package/dist/commands/{improve-auto-accept.js → improve/improve-auto-accept.js} +30 -4
  50. package/dist/commands/{improve-cli.js → improve/improve-cli.js} +44 -22
  51. package/dist/commands/{improve-profiles.js → improve/improve-profiles.js} +13 -7
  52. package/dist/commands/{improve-result-file.js → improve/improve-result-file.js} +1 -1
  53. package/dist/commands/{improve.js → improve/improve.js} +672 -292
  54. package/dist/{core → commands/improve/memory}/memory-belief.js +2 -2
  55. package/dist/{core → commands/improve/memory}/memory-contradiction-detect.js +5 -5
  56. package/dist/{core → commands/improve/memory}/memory-improve.js +4 -4
  57. package/dist/commands/improve/reflect-noise.js +0 -0
  58. package/dist/commands/{reflect.js → improve/reflect.js} +58 -28
  59. package/dist/commands/improve/session-asset.js +248 -0
  60. package/dist/commands/lint/agent-linter.js +1 -1
  61. package/dist/commands/lint/base-linter.js +55 -37
  62. package/dist/commands/lint/command-linter.js +1 -1
  63. package/dist/commands/lint/default-linter.js +1 -1
  64. package/dist/commands/lint/env-key-rules.js +1 -1
  65. package/dist/commands/lint/index.js +19 -25
  66. package/dist/commands/lint/knowledge-linter.js +1 -1
  67. package/dist/commands/lint/memory-linter.js +1 -1
  68. package/dist/commands/lint/registry.js +8 -8
  69. package/dist/commands/lint/skill-linter.js +1 -1
  70. package/dist/commands/lint/task-linter.js +1 -1
  71. package/dist/commands/lint/workflow-linter.js +1 -1
  72. package/dist/commands/lint.js +1 -1
  73. package/dist/commands/observability-cli.js +244 -0
  74. package/dist/commands/proposal/drain-policies.js +3 -3
  75. package/dist/commands/proposal/drain.js +87 -15
  76. package/dist/commands/proposal/proposal-cli.js +490 -0
  77. package/dist/commands/{proposal.js → proposal/proposal.js} +17 -6
  78. package/dist/commands/{propose.js → proposal/propose.js} +11 -11
  79. package/dist/{core → commands/proposal/validators}/proposal-quality-validators.js +8 -3
  80. package/dist/{core → commands/proposal/validators}/proposal-validators.js +5 -5
  81. package/dist/{core → commands/proposal/validators}/proposals.js +374 -345
  82. package/dist/commands/{curate.js → read/curate.js} +7 -7
  83. package/dist/commands/{knowledge.js → read/knowledge.js} +22 -9
  84. package/dist/commands/{registry-search.js → read/registry-search.js} +5 -5
  85. package/dist/commands/{remember-cli.js → read/remember-cli.js} +15 -7
  86. package/dist/commands/read/search-cli.js +207 -0
  87. package/dist/commands/{search.js → read/search.js} +22 -27
  88. package/dist/commands/{show.js → read/show.js} +31 -45
  89. package/dist/commands/registry-cli.js +8 -8
  90. package/dist/commands/remember.js +14 -10
  91. package/dist/commands/sources/add-cli.js +293 -0
  92. package/dist/commands/{history.js → sources/history.js} +27 -25
  93. package/dist/commands/{info.js → sources/info.js} +6 -6
  94. package/dist/commands/{init.js → sources/init.js} +6 -6
  95. package/dist/commands/{installed-stashes.js → sources/installed-stashes.js} +12 -12
  96. package/dist/commands/{migration-help.js → sources/migration-help.js} +3 -2
  97. package/dist/commands/{schema-repair.js → sources/schema-repair.js} +8 -8
  98. package/dist/commands/{self-update.js → sources/self-update.js} +10 -9
  99. package/dist/commands/{source-add.js → sources/source-add.js} +10 -10
  100. package/dist/commands/{source-clone.js → sources/source-clone.js} +7 -7
  101. package/dist/commands/{source-manage.js → sources/source-manage.js} +4 -4
  102. package/dist/commands/sources/sources-cli.js +305 -0
  103. package/dist/commands/sources/stash-cli.js +219 -0
  104. package/dist/commands/{stash-skeleton.js → sources/stash-skeleton.js} +2 -1
  105. package/dist/commands/tasks/default-tasks.js +173 -0
  106. package/dist/commands/tasks/tasks-cli.js +210 -0
  107. package/dist/commands/{tasks.js → tasks/tasks.js} +14 -14
  108. package/dist/commands/wiki-cli.js +307 -0
  109. package/dist/commands/workflow-cli.js +329 -0
  110. package/dist/core/action-contributors.js +1 -1
  111. package/dist/core/assert.js +40 -0
  112. package/dist/core/asset/asset-create.js +54 -0
  113. package/dist/core/{asset-ref.js → asset/asset-ref.js} +21 -4
  114. package/dist/core/{asset-registry.js → asset/asset-registry.js} +3 -3
  115. package/dist/core/{asset-spec.js → asset/asset-spec.js} +17 -31
  116. package/dist/core/{markdown.js → asset/markdown.js} +1 -1
  117. package/dist/core/{stash-meta.js → asset/stash-meta.js} +1 -1
  118. package/dist/core/best-effort.js +64 -0
  119. package/dist/core/common.js +32 -18
  120. package/dist/core/{config-io.js → config/config-io.js} +29 -19
  121. package/dist/core/{config-migration.js → config/config-migration.js} +11 -9
  122. package/dist/core/{config-schema.js → config/config-schema.js} +50 -7
  123. package/dist/core/config/config-types.js +16 -0
  124. package/dist/core/{config-walker.js → config/config-walker.js} +2 -2
  125. package/dist/core/{config.js → config/config.js} +10 -8
  126. package/dist/core/env-secret-ref.js +90 -0
  127. package/dist/core/errors.js +13 -3
  128. package/dist/core/events.js +27 -4
  129. package/dist/core/file-lock.js +1 -1
  130. package/dist/core/improve-types.js +48 -0
  131. package/dist/core/lesson-lint.js +2 -2
  132. package/dist/core/logs-db.js +304 -0
  133. package/dist/core/paths.js +2 -2
  134. package/dist/core/ripgrep/install.js +2 -2
  135. package/dist/core/ripgrep/resolve.js +2 -2
  136. package/dist/core/state-db.js +195 -60
  137. package/dist/core/text-truncation.js +148 -0
  138. package/dist/core/time.js +1 -1
  139. package/dist/core/write-source.js +98 -85
  140. package/dist/indexer/{db-backup.js → db/db-backup.js} +9 -24
  141. package/dist/indexer/{db.js → db/db.js} +128 -118
  142. package/dist/indexer/{graph-db.js → db/graph-db.js} +9 -4
  143. package/dist/indexer/{llm-cache.js → db/llm-cache.js} +15 -12
  144. package/dist/indexer/ensure-index.js +4 -4
  145. package/dist/indexer/{graph-boost.js → graph/graph-boost.js} +1 -1
  146. package/dist/indexer/{graph-extraction.js → graph/graph-extraction.js} +55 -13
  147. package/dist/indexer/indexer.js +37 -30
  148. package/dist/indexer/init.js +54 -0
  149. package/dist/indexer/manifest.js +10 -10
  150. package/dist/indexer/{memory-inference.js → passes/memory-inference.js} +141 -33
  151. package/dist/indexer/{metadata-contributors.js → passes/metadata-contributors.js} +10 -8
  152. package/dist/indexer/{metadata.js → passes/metadata.js} +15 -19
  153. package/dist/indexer/{staleness-detect.js → passes/staleness-detect.js} +53 -12
  154. package/dist/indexer/{db-search.js → search/db-search.js} +28 -16
  155. package/dist/indexer/{ranking-contributors.js → search/ranking-contributors.js} +1 -1
  156. package/dist/indexer/{ranking.js → search/ranking.js} +2 -2
  157. package/dist/indexer/{search-hit-enrichers.js → search/search-hit-enrichers.js} +3 -3
  158. package/dist/indexer/{search-source.js → search/search-source.js} +8 -8
  159. package/dist/indexer/{semantic-status.js → search/semantic-status.js} +3 -3
  160. package/dist/indexer/usage/unmigrated-vaults-guard.js +94 -0
  161. package/dist/indexer/{usage-events.js → usage/usage-events.js} +32 -0
  162. package/dist/indexer/{file-context.js → walk/file-context.js} +10 -15
  163. package/dist/indexer/{matchers.js → walk/matchers.js} +13 -9
  164. package/dist/indexer/{path-resolver.js → walk/path-resolver.js} +6 -6
  165. package/dist/indexer/{project-context.js → walk/project-context.js} +1 -1
  166. package/dist/indexer/{walker.js → walk/walker.js} +4 -3
  167. package/dist/integrations/agent/builder-shared.js +39 -0
  168. package/dist/integrations/agent/builders.js +14 -81
  169. package/dist/integrations/agent/config.js +6 -4
  170. package/dist/integrations/agent/detect.js +1 -1
  171. package/dist/integrations/agent/index.js +23 -8
  172. package/dist/integrations/agent/prompts.js +2 -3
  173. package/dist/integrations/agent/runner.js +22 -3
  174. package/dist/integrations/agent/spawn.js +9 -10
  175. package/dist/integrations/harnesses/claude/agent-builder.js +48 -0
  176. package/dist/integrations/harnesses/claude/config-import.js +70 -0
  177. package/dist/integrations/harnesses/claude/index.js +64 -0
  178. package/dist/integrations/{session-logs/providers/claude-code.js → harnesses/claude/session-log.js} +32 -5
  179. package/dist/integrations/harnesses/index.js +144 -0
  180. package/dist/integrations/harnesses/opencode/agent-builder.js +43 -0
  181. package/dist/integrations/harnesses/opencode/config-import.js +82 -0
  182. package/dist/integrations/harnesses/opencode/index.js +59 -0
  183. package/dist/integrations/{session-logs/providers/opencode.js → harnesses/opencode/session-log.js} +1 -1
  184. package/dist/integrations/harnesses/opencode-sdk/index.js +49 -0
  185. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +234 -0
  186. package/dist/integrations/harnesses/types.js +43 -0
  187. package/dist/integrations/lockfile.js +7 -16
  188. package/dist/integrations/session-logs/index.js +82 -9
  189. package/dist/llm/call-ai.js +4 -4
  190. package/dist/llm/client.js +146 -6
  191. package/dist/llm/embedder.js +6 -6
  192. package/dist/llm/embedders/local.js +9 -22
  193. package/dist/llm/embedders/remote.js +2 -2
  194. package/dist/llm/embedders/types.js +1 -1
  195. package/dist/llm/graph-extract.js +31 -12
  196. package/dist/llm/index-passes.js +1 -1
  197. package/dist/llm/memory-infer.js +12 -5
  198. package/dist/llm/metadata-enhance.js +2 -2
  199. package/dist/llm/usage-persist.js +77 -0
  200. package/dist/llm/usage-telemetry.js +103 -0
  201. package/dist/output/context.js +9 -46
  202. package/dist/output/html-render.js +73 -0
  203. package/dist/output/renderers.js +88 -58
  204. package/dist/output/shapes/curate.js +7 -3
  205. package/dist/output/shapes/distill.js +7 -3
  206. package/dist/output/shapes/env-list.js +18 -16
  207. package/dist/output/shapes/events.js +5 -4
  208. package/dist/output/shapes/helpers.js +19 -5
  209. package/dist/output/shapes/history.js +7 -3
  210. package/dist/output/shapes/passthrough.js +8 -11
  211. package/dist/output/shapes/{proposal-accept.js → proposal/accept.js} +7 -3
  212. package/dist/output/shapes/{proposal-diff.js → proposal/diff.js} +7 -3
  213. package/dist/output/shapes/{proposal-list.js → proposal/list.js} +7 -3
  214. package/dist/output/shapes/{proposal-producer.js → proposal/producer.js} +5 -4
  215. package/dist/output/shapes/{proposal-reject.js → proposal/reject.js} +7 -3
  216. package/dist/output/shapes/{proposal-show.js → proposal/show.js} +7 -3
  217. package/dist/output/shapes/registry-search.js +7 -3
  218. package/dist/output/shapes/registry.js +12 -0
  219. package/dist/output/shapes/search.js +7 -3
  220. package/dist/output/shapes/secret-list.js +18 -16
  221. package/dist/output/shapes/show.js +7 -3
  222. package/dist/output/shapes.js +55 -30
  223. package/dist/output/text/add.js +2 -3
  224. package/dist/output/text/clone.js +2 -3
  225. package/dist/output/text/config.js +2 -3
  226. package/dist/output/text/curate.js +4 -3
  227. package/dist/output/text/distill.js +2 -3
  228. package/dist/output/text/enable-disable.js +5 -4
  229. package/dist/output/text/env.js +13 -0
  230. package/dist/output/text/events.js +5 -4
  231. package/dist/output/text/feedback.js +4 -3
  232. package/dist/output/text/helpers.js +123 -40
  233. package/dist/output/text/history.js +2 -3
  234. package/dist/output/text/import.js +2 -3
  235. package/dist/output/text/index.js +2 -3
  236. package/dist/output/text/info.js +2 -3
  237. package/dist/output/text/init.js +2 -3
  238. package/dist/output/text/list.js +2 -3
  239. package/dist/output/text/proposal/producer.js +9 -0
  240. package/dist/output/text/proposal/proposal.js +13 -0
  241. package/dist/output/text/registry-commands.js +8 -7
  242. package/dist/output/text/registry.js +12 -0
  243. package/dist/output/text/remember.js +4 -3
  244. package/dist/output/text/remove.js +2 -3
  245. package/dist/output/text/save.js +2 -3
  246. package/dist/output/text/search.js +4 -3
  247. package/dist/output/text/show.js +4 -3
  248. package/dist/output/text/update.js +2 -3
  249. package/dist/output/text/upgrade.js +2 -3
  250. package/dist/output/text/wiki.js +12 -11
  251. package/dist/output/text/workflow.js +12 -10
  252. package/dist/output/text.js +66 -32
  253. package/dist/registry/build-index.js +11 -10
  254. package/dist/registry/factory.js +1 -1
  255. package/dist/registry/origin-resolve.js +1 -1
  256. package/dist/registry/providers/index.js +2 -2
  257. package/dist/registry/providers/skills-sh.js +91 -72
  258. package/dist/registry/providers/static-index.js +75 -52
  259. package/dist/registry/resolve.js +3 -3
  260. package/dist/runtime.js +242 -0
  261. package/dist/scripts/migrate-storage.js +1654 -683
  262. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +254 -168
  263. package/dist/setup/detect.js +311 -9
  264. package/dist/setup/harness-config-import.js +6 -120
  265. package/dist/setup/setup.js +454 -43
  266. package/dist/sources/include.js +1 -1
  267. package/dist/sources/provider-factory.js +2 -2
  268. package/dist/sources/providers/filesystem.js +3 -3
  269. package/dist/sources/providers/git.js +9 -9
  270. package/dist/sources/providers/index.js +4 -4
  271. package/dist/sources/providers/npm.js +6 -6
  272. package/dist/sources/providers/provider-utils.js +13 -20
  273. package/dist/sources/providers/sync-from-ref.js +5 -5
  274. package/dist/sources/providers/tar-utils.js +2 -2
  275. package/dist/sources/providers/website.js +2 -2
  276. package/dist/sources/resolve.js +5 -5
  277. package/dist/sources/website-ingest.js +5 -5
  278. package/dist/storage/database.js +102 -0
  279. package/dist/storage/engines/sqlite-migrations.js +42 -0
  280. package/dist/storage/locations.js +25 -0
  281. package/dist/storage/repositories/index-db.js +43 -0
  282. package/dist/storage/repositories/workflow-runs-repository.js +141 -0
  283. package/dist/tasks/backends/cron.js +4 -4
  284. package/dist/tasks/backends/exec-utils.js +32 -0
  285. package/dist/tasks/backends/index.js +3 -3
  286. package/dist/tasks/backends/launchd.js +7 -14
  287. package/dist/tasks/backends/schtasks.js +7 -16
  288. package/dist/tasks/embedded.js +71 -0
  289. package/dist/tasks/parser.js +2 -2
  290. package/dist/tasks/resolveAkmBin.js +1 -1
  291. package/dist/tasks/runner.js +127 -31
  292. package/dist/tasks/schedule.js +1 -1
  293. package/dist/tasks/validator.js +7 -7
  294. package/dist/text-import-hook.mjs +51 -0
  295. package/dist/version.js +2 -1
  296. package/dist/wiki/wiki.js +7 -7
  297. package/dist/workflows/{authoring.js → authoring/authoring.js} +6 -6
  298. package/dist/workflows/{scope-key.js → authoring/scope-key.js} +1 -1
  299. package/dist/workflows/cli.js +1 -1
  300. package/dist/workflows/db.js +54 -32
  301. package/dist/workflows/parser.js +4 -4
  302. package/dist/workflows/renderer.js +5 -5
  303. package/dist/workflows/runtime/agent-identity.js +56 -0
  304. package/dist/workflows/runtime/checkin.js +57 -0
  305. package/dist/workflows/{runs.js → runtime/runs.js} +197 -101
  306. package/dist/workflows/validate-summary.js +82 -0
  307. package/docs/README.md +1 -1
  308. package/docs/data-and-telemetry.md +6 -6
  309. package/package.json +17 -8
  310. package/dist/commands/add-cli.js +0 -279
  311. package/dist/commands/env.js +0 -213
  312. package/dist/integrations/agent/sdk-runner.js +0 -126
  313. package/dist/output/shapes/vault-list.js +0 -19
  314. package/dist/output/text/proposal-producer.js +0 -8
  315. package/dist/output/text/proposal.js +0 -12
  316. package/dist/output/text/vault.js +0 -16
  317. /package/dist/core/{asset-serialize.js → asset/asset-serialize.js} +0 -0
  318. /package/dist/core/{frontmatter.js → asset/frontmatter.js} +0 -0
  319. /package/dist/core/{config-sources.js → config/config-sources.js} +0 -0
  320. /package/dist/indexer/{graph-dedup.js → graph/graph-dedup.js} +0 -0
  321. /package/dist/{core/config-types.js → indexer/passes/pass-context.js} +0 -0
  322. /package/dist/indexer/{search-fields.js → search/search-fields.js} +0 -0
  323. /package/dist/indexer/{index-context.js → walk/index-context.js} +0 -0
  324. /package/dist/workflows/{document-cache.js → runtime/document-cache.js} +0 -0
@@ -4,12 +4,12 @@
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { parse as parseYaml } from "yaml";
7
- import { resolveStashDir } from "../../core/common";
8
- import { loadConfig } from "../../core/config";
9
- import { parseFrontmatter } from "../../core/frontmatter";
10
- import { resolveSourceEntries } from "../../indexer/search-source";
11
- import { checkVaultForDangerousKeys } from "./env-key-rules";
12
- import { getLinterForType } from "./registry";
7
+ import { parseFrontmatter } from "../../core/asset/frontmatter.js";
8
+ import { resolveStashDir } from "../../core/common.js";
9
+ import { loadConfig } from "../../core/config/config.js";
10
+ import { resolveSourceEntries } from "../../indexer/search/search-source.js";
11
+ import { checkEnvForDangerousKeys } from "./env-key-rules.js";
12
+ import { getLinterForType } from "./registry.js";
13
13
  // ── Constants ─────────────────────────────────────────────────────────────────
14
14
  const STASH_SUBDIRS = [
15
15
  "agents",
@@ -154,27 +154,21 @@ export function akmLint(options = {}) {
154
154
  }
155
155
  }
156
156
  // ── Env dangerous-key pass ─────────────────────────────────────────────────
157
- // Scan every `.env` file under <stashRoot>/env/ (and the deprecated
158
- // <stashRoot>/vaults/) across all stash roots for keys that are known to
159
- // enable process-execution hijacking. Warn-only — findings go into `flagged`,
160
- // never `fixed`.
157
+ // Scan every `.env` file under <stashRoot>/env/ across all stash roots for
158
+ // keys that are known to enable process-execution hijacking. Warn-only
159
+ // findings go into `flagged`, never `fixed`.
161
160
  const envRoots = [stashRoot, ...extraStashRoots];
162
161
  for (const root of envRoots) {
163
- for (const [subdir, prefix] of [
164
- ["env", "env"],
165
- ["vaults", "vault"],
166
- ]) {
167
- const dir = path.join(root, subdir);
168
- if (!fs.existsSync(dir))
169
- continue;
170
- for (const envPath of collectEnvFiles(dir)) {
171
- const baseName = path.basename(envPath, ".env");
172
- // "default" (or empty) maps to ".env" → <prefix>:default
173
- const ref = baseName === "" ? `${prefix}:default` : `${prefix}:${baseName}`;
174
- const relPath = path.relative(root, envPath);
175
- for (const issue of checkVaultForDangerousKeys(envPath, relPath, ref)) {
176
- flagged.push(issue);
177
- }
162
+ const dir = path.join(root, "env");
163
+ if (!fs.existsSync(dir))
164
+ continue;
165
+ for (const envPath of collectEnvFiles(dir)) {
166
+ const baseName = path.basename(envPath, ".env");
167
+ // "default" (or empty) maps to ".env" → env:default
168
+ const ref = baseName === "" ? "env:default" : `env:${baseName}`;
169
+ const relPath = path.relative(root, envPath);
170
+ for (const issue of checkEnvForDangerousKeys(envPath, relPath, ref)) {
171
+ flagged.push(issue);
178
172
  }
179
173
  }
180
174
  }
@@ -1,7 +1,7 @@
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 { BaseLinter } from "./base-linter";
4
+ import { BaseLinter } from "./base-linter.js";
5
5
  /**
6
6
  * Linter for `knowledge/` assets.
7
7
  *
@@ -2,7 +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 { BaseLinter } from "./base-linter";
5
+ import { BaseLinter } from "./base-linter.js";
6
6
  /**
7
7
  * Linter for `memories/` assets.
8
8
  *
@@ -1,14 +1,14 @@
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 { AgentLinter } from "./agent-linter";
5
- import { CommandLinter } from "./command-linter";
6
- import { DefaultLinter } from "./default-linter";
7
- import { KnowledgeLinter } from "./knowledge-linter";
8
- import { MemoryLinter } from "./memory-linter";
9
- import { SkillLinter } from "./skill-linter";
10
- import { TaskLinter } from "./task-linter";
11
- import { WorkflowLinter } from "./workflow-linter";
4
+ import { AgentLinter } from "./agent-linter.js";
5
+ import { CommandLinter } from "./command-linter.js";
6
+ import { DefaultLinter } from "./default-linter.js";
7
+ import { KnowledgeLinter } from "./knowledge-linter.js";
8
+ import { MemoryLinter } from "./memory-linter.js";
9
+ import { SkillLinter } from "./skill-linter.js";
10
+ import { TaskLinter } from "./task-linter.js";
11
+ import { WorkflowLinter } from "./workflow-linter.js";
12
12
  // Singleton instances — one per type, shared across all lint runs.
13
13
  const LINTERS = [
14
14
  new AgentLinter(),
@@ -3,7 +3,7 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
- import { BaseLinter } from "./base-linter";
6
+ import { BaseLinter } from "./base-linter.js";
7
7
  /**
8
8
  * Linter for `skills/` assets.
9
9
  *
@@ -1,7 +1,7 @@
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 { BaseLinter } from "./base-linter";
4
+ import { BaseLinter } from "./base-linter.js";
5
5
  /**
6
6
  * Linter for `tasks/` assets.
7
7
  *
@@ -2,7 +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 { BaseLinter } from "./base-linter";
5
+ import { BaseLinter } from "./base-linter.js";
6
6
  const PLACEHOLDER_STRINGS = ["Describe what this workflow accomplishes", "Example Workflow"];
7
7
  /**
8
8
  * Linter for `workflows/` assets.
@@ -1,4 +1,4 @@
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
- export { akmLint } from "./lint/index";
4
+ export { akmLint } from "./lint/index.js";
@@ -0,0 +1,244 @@
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
+ * Observability command cluster — `akm log` (events list/tail), `akm lessons`
6
+ * (coverage), and `akm hints`. Extracted verbatim from src/cli.ts (WS6) so the
7
+ * God Module shrinks; the `main.subCommands.{log,lessons,hints}` keys and every
8
+ * subcommand's args/output shape are byte-identical.
9
+ *
10
+ * These three surfaces are cohesive read-only "tell me what happened / what to
11
+ * do" commands: `log` reads the append-only state.db events stream, `lessons
12
+ * coverage` reports tag-coverage gaps from the index, and `hints` prints the
13
+ * embedded AGENTS.md guidance. They share no helpers with any command still
14
+ * inline in cli.ts, so the `loadHints` private helper and the
15
+ * `formatEventLine` / `EMBEDDED_HINTS*` / db-tag-set imports move with them.
16
+ *
17
+ * The leaf handlers whose body is a plain `runWithJsonErrors(...) + output(...)`
18
+ * (`events list`, `lessons coverage`) are migrated onto `defineJsonCommand`,
19
+ * which emits the same JSON envelope (stdout/stderr/exit-code) as the inline
20
+ * form. `events tail` (manual streaming console/stderr writes) and `hints`
21
+ * (direct `process.stdout.write`) keep a plain `defineCommand` wrapping
22
+ * `runWithJsonErrors` so their byte-for-byte output stays untouched.
23
+ */
24
+ import fs from "node:fs";
25
+ import path from "node:path";
26
+ import { defineCommand } from "citty";
27
+ import { parsePositiveIntFlag } from "../cli/parse-args.js";
28
+ import { defineJsonCommand, output, parseAllFlagValues, runWithJsonErrors } from "../cli/shared.js";
29
+ import { closeDatabase, collectTagSetFromEntries, openExistingDatabase } from "../indexer/db/db.js";
30
+ import { EMBEDDED_HINTS, EMBEDDED_HINTS_FULL } from "../output/cli-hints.js";
31
+ import { getHyphenatedArg, getOutputMode, parseDetailLevel } from "../output/context.js";
32
+ import { formatEventLine } from "../output/text.js";
33
+ import { getDirname } from "../runtime.js";
34
+ import { akmEventsList, akmEventsTail } from "./events.js";
35
+ // ── `akm log` ────────────────────────────────────────────────────────────────
36
+ // Append-only events stream surface (#204). `list` reads state.db events
37
+ // with optional --since/--type/--ref filters; `tail` follows the table via
38
+ // a polling loop and prints each event as a single JSONL line.
39
+ const eventsListCommand = defineJsonCommand({
40
+ meta: { name: "list", description: "List events from the append-only state.db events stream" },
41
+ args: {
42
+ since: {
43
+ type: "string",
44
+ description: "ISO timestamp / epoch ms, OR `@offset:<id>` for a durable row-id cursor (resume across processes)",
45
+ },
46
+ type: { type: "string", description: "Filter by event type (add, remove, remember, feedback, ...)" },
47
+ ref: { type: "string", description: "Filter by asset ref (type:name)" },
48
+ "exclude-tags": {
49
+ type: "string",
50
+ description: "Exclude events matching these tags (repeatable)",
51
+ },
52
+ "include-tags": {
53
+ type: "string",
54
+ description: "Only include events with ALL these tags (repeatable)",
55
+ },
56
+ },
57
+ run({ args }) {
58
+ const excludeTags = parseAllFlagValues("--exclude-tags");
59
+ const includeTags = parseAllFlagValues("--include-tags");
60
+ const result = akmEventsList({
61
+ since: args.since,
62
+ type: args.type,
63
+ ref: args.ref,
64
+ ...(excludeTags.length > 0 ? { excludeTags } : {}),
65
+ ...(includeTags.length > 0 ? { includeTags } : {}),
66
+ });
67
+ output("events-list", result);
68
+ },
69
+ });
70
+ const eventsTailCommand = defineCommand({
71
+ meta: { name: "tail", description: "Follow the append-only state.db events stream (polling)" },
72
+ args: {
73
+ since: {
74
+ type: "string",
75
+ description: "ISO timestamp / epoch ms, OR `@offset:<id>` for a durable row-id cursor (resume across processes)",
76
+ },
77
+ type: { type: "string", description: "Filter by event type" },
78
+ ref: { type: "string", description: "Filter by asset ref (type:name)" },
79
+ "interval-ms": { type: "string", description: "Polling interval in ms (default: 75)" },
80
+ "max-duration-ms": { type: "string", description: "Stop after this many ms (default: never)" },
81
+ "max-events": { type: "string", description: "Stop after observing this many events" },
82
+ "exclude-tags": {
83
+ type: "string",
84
+ description: "Exclude events matching these tags (repeatable)",
85
+ },
86
+ "include-tags": {
87
+ type: "string",
88
+ description: "Only include events with ALL these tags (repeatable)",
89
+ },
90
+ },
91
+ async run({ args }) {
92
+ await runWithJsonErrors(async () => {
93
+ const intervalMs = parsePositiveIntFlag(getHyphenatedArg(args, "interval-ms"), "--interval-ms");
94
+ const maxDurationMs = parsePositiveIntFlag(getHyphenatedArg(args, "max-duration-ms"), "--max-duration-ms");
95
+ const maxEvents = parsePositiveIntFlag(getHyphenatedArg(args, "max-events"), "--max-events");
96
+ const mode = getOutputMode();
97
+ // In streaming text mode we want each event to print as soon as it
98
+ // arrives. The polling loop emits via `onEvent`; the final result is
99
+ // also rendered through the standard output() pipeline so JSON
100
+ // consumers always get the canonical envelope.
101
+ const stream = mode.format === "text" || mode.format === "jsonl";
102
+ const excludeTags = parseAllFlagValues("--exclude-tags");
103
+ const includeTags = parseAllFlagValues("--include-tags");
104
+ const result = await akmEventsTail({
105
+ since: args.since,
106
+ type: args.type,
107
+ ref: args.ref,
108
+ intervalMs,
109
+ maxDurationMs,
110
+ maxEvents,
111
+ ...(excludeTags.length > 0 ? { excludeTags } : {}),
112
+ ...(includeTags.length > 0 ? { includeTags } : {}),
113
+ onEvent: stream
114
+ ? (event) => {
115
+ if (mode.format === "jsonl") {
116
+ console.log(JSON.stringify(event));
117
+ }
118
+ else {
119
+ console.log(formatEventLine(event));
120
+ }
121
+ }
122
+ : undefined,
123
+ });
124
+ // Emit the canonical envelope last (JSON/YAML modes rely on this;
125
+ // streaming modes already printed each event but we still emit a
126
+ // trailer so callers can persist the resumable cursor).
127
+ if (!stream) {
128
+ output("events-tail", result);
129
+ }
130
+ else if (mode.format === "jsonl") {
131
+ // Final discriminated trailer row so jsonl consumers can resume.
132
+ const trailer = {
133
+ _kind: "trailer",
134
+ schemaVersion: 1,
135
+ nextOffset: result.nextOffset,
136
+ totalCount: result.totalCount,
137
+ reason: result.reason,
138
+ };
139
+ console.log(JSON.stringify(trailer));
140
+ }
141
+ else {
142
+ // text mode: keep stdout pristine for line-oriented parsers and
143
+ // emit the trailer on stderr.
144
+ process.stderr.write(`[events-tail] reason=${result.reason} nextOffset=${result.nextOffset} total=${result.totalCount}\n`);
145
+ }
146
+ });
147
+ },
148
+ });
149
+ export const logCommand = defineCommand({
150
+ meta: {
151
+ name: "log",
152
+ description: "Read or follow the append-only state.db events stream (mutations, feedback, indexing)",
153
+ },
154
+ subCommands: {
155
+ list: eventsListCommand,
156
+ tail: eventsTailCommand,
157
+ },
158
+ });
159
+ // ── lessons subcommands (Phase 7A / Advantage D4c) ──────────────────────────
160
+ const lessonsCoverageCommand = defineJsonCommand({
161
+ meta: {
162
+ name: "coverage",
163
+ description: "Report tags that exist on indexed assets but are NOT yet covered by any lesson.\n\n" +
164
+ "Useful for spotting topics where the stash has skills/commands/scripts but no\n" +
165
+ "crystallized lesson — a signal that the team has tacit knowledge worth distilling.\n\n" +
166
+ "Default output is JSON: { uncoveredTags: string[], lessonTagCount: number, totalTagCount: number }.\n" +
167
+ "Pass --format text for a plain-text bulleted list.",
168
+ },
169
+ args: {},
170
+ run() {
171
+ const db = openExistingDatabase();
172
+ try {
173
+ const allTagSet = collectTagSetFromEntries(db, undefined);
174
+ const lessonTagSet = collectTagSetFromEntries(db, "lesson");
175
+ const uncovered = [];
176
+ for (const tag of allTagSet) {
177
+ if (!lessonTagSet.has(tag))
178
+ uncovered.push(tag);
179
+ }
180
+ uncovered.sort((a, b) => a.localeCompare(b));
181
+ output("lessons-coverage", {
182
+ ok: true,
183
+ uncoveredTags: uncovered,
184
+ lessonTagCount: lessonTagSet.size,
185
+ totalTagCount: allTagSet.size,
186
+ });
187
+ }
188
+ finally {
189
+ closeDatabase(db);
190
+ }
191
+ },
192
+ });
193
+ export const lessonsCommand = defineCommand({
194
+ meta: {
195
+ name: "lessons",
196
+ alias: "lesson",
197
+ description: "Lesson-asset tooling: tag-coverage gaps, strength queries.",
198
+ },
199
+ subCommands: {
200
+ coverage: lessonsCoverageCommand,
201
+ },
202
+ });
203
+ // ── `akm hints` ──────────────────────────────────────────────────────────────
204
+ export const hintsCommand = defineCommand({
205
+ meta: {
206
+ name: "hints",
207
+ description: "Print agent instructions on how to use akm, use --detail full for a complete guide",
208
+ },
209
+ args: {
210
+ detail: {
211
+ type: "string",
212
+ description: "Hints detail level (brief|normal|full). `brief` prints the short guide; `normal`/`full` print the complete guide.",
213
+ default: "normal",
214
+ },
215
+ },
216
+ run({ args }) {
217
+ return runWithJsonErrors(() => {
218
+ // Let the global parser validate the value so an invalid `--detail`
219
+ // returns the standard JSON error envelope (exit 2) rather than a raw
220
+ // stack trace + exit 1. `brief` → short doc; `normal`/`full` → full doc.
221
+ const detail = parseDetailLevel(args.detail) ?? "normal";
222
+ process.stdout.write(loadHints(detail === "brief" ? "brief" : "full"));
223
+ });
224
+ },
225
+ });
226
+ // ── Hints (embedded AGENTS.md) ──────────────────────────────────────────────
227
+ function loadHints(detail = "normal") {
228
+ // `brief` → the short AGENTS.md guide; `normal`/`full` → the complete guide.
229
+ const wantFull = detail !== "brief";
230
+ const filename = wantFull ? "AGENTS.full.md" : "AGENTS.md";
231
+ const fallback = wantFull ? EMBEDDED_HINTS_FULL : EMBEDDED_HINTS;
232
+ // Try reading from the docs/ directory (works in dev and when installed via npm)
233
+ try {
234
+ const docsPath = path.resolve(getDirname(import.meta.url), `../../docs/agents/${filename}`);
235
+ if (fs.existsSync(docsPath)) {
236
+ return fs.readFileSync(docsPath, "utf8");
237
+ }
238
+ }
239
+ catch {
240
+ // fall through
241
+ }
242
+ // Fallback for compiled binary — inline content
243
+ return fallback;
244
+ }
@@ -16,10 +16,10 @@
16
16
  */
17
17
  import fs from "node:fs";
18
18
  import { z } from "zod";
19
- import { UsageError } from "../../core/errors";
20
- import { PROPOSAL_SOURCES } from "../../core/proposals";
19
+ import { UsageError } from "../../core/errors.js";
20
+ import { PROPOSAL_SOURCES } from "./validators/proposals.js";
21
21
  // Valid `generator` values for a drain rule are exactly the canonical proposal
22
- // `source` values (see {@link PROPOSAL_SOURCES} in src/core/proposals.ts). The
22
+ // `source` values (see {@link PROPOSAL_SOURCES} in src/commands/proposal/validators/proposals.ts). The
23
23
  // engine matches rules via `policy.accept.find(r => r.generator === proposal.source)`,
24
24
  // so a generator that is not a real source can never match — it would be a
25
25
  // silent permanent no-op. Validate against the closed set to surface typos.
@@ -36,16 +36,17 @@
36
36
  */
37
37
  import fs from "node:fs";
38
38
  import path from "node:path";
39
- import { parseAssetRef } from "../../core/asset-ref";
40
- import { resolveAssetPathFromName, TYPE_DIRS } from "../../core/asset-spec";
41
- import { appendEvent } from "../../core/events";
42
- import { parseFrontmatter } from "../../core/frontmatter";
43
- import { listProposals } from "../../core/proposals";
44
- import { info, warn } from "../../core/warn";
45
- import { runAgent } from "../../integrations/agent";
46
- import { runOpencodeSdk } from "../../integrations/agent/sdk-runner";
47
- import { chatCompletion, stripJsonFences } from "../../llm/client";
48
- import { akmProposalAccept, akmProposalReject } from "../proposal";
39
+ import { assertNever } from "../../core/assert.js";
40
+ import { parseAssetRef } from "../../core/asset/asset-ref.js";
41
+ import { resolveAssetPathFromName, TYPE_DIRS } from "../../core/asset/asset-spec.js";
42
+ import { parseFrontmatter } from "../../core/asset/frontmatter.js";
43
+ import { appendEvent } from "../../core/events.js";
44
+ import { info, warn } from "../../core/warn.js";
45
+ import { runAgent } from "../../integrations/agent/index.js";
46
+ import { runOpencodeSdk } from "../../integrations/harnesses/opencode-sdk/index.js";
47
+ import { chatCompletion, stripJsonFences } from "../../llm/client.js";
48
+ import { akmProposalAccept, akmProposalReject } from "./proposal.js";
49
+ import { listProposals, recordGateDecision } from "./validators/proposals.js";
49
50
  // ---------------------------------------------------------------------------
50
51
  // Content helpers
51
52
  // ---------------------------------------------------------------------------
@@ -77,7 +78,7 @@ export function classifyProposal(proposal, policy, maxDiffLines) {
77
78
  const content = proposal.payload.content ?? "";
78
79
  // Empty / near-empty diffs reject first (the reject-empty floor).
79
80
  if (policy.rejectEmpty && isEmptyDiff(proposal)) {
80
- return { verdict: "reject", reason: "empty diff" };
81
+ return { verdict: "reject", reason: "empty diff", gate: { reason: "empty-diff" } };
81
82
  }
82
83
  const rule = policy.accept.find((r) => r.generator === proposal.source);
83
84
  if (rule) {
@@ -86,16 +87,25 @@ export function classifyProposal(proposal, policy, maxDiffLines) {
86
87
  // Per-rule and global diff bounds defer large accepts (no silent rewrites).
87
88
  const effectiveMax = Math.min(rule.maxDiffLines ?? Number.POSITIVE_INFINITY, maxDiffLines ?? Number.POSITIVE_INFINITY);
88
89
  if (lines > effectiveMax) {
89
- return { verdict: "defer", reason: "mid-band" };
90
+ return {
91
+ verdict: "defer",
92
+ reason: "mid-band",
93
+ gate: { reason: "max-diff-lines", measured: lines, thresholds: { maxDiffLines: effectiveMax } },
94
+ };
90
95
  }
91
96
  if (rule.minContentLines !== undefined && body < rule.minContentLines) {
92
97
  // Too little content to confidently auto-accept — leave for judgment.
93
- return { verdict: "defer", reason: "mid-band" };
98
+ return {
99
+ verdict: "defer",
100
+ reason: "mid-band",
101
+ gate: { reason: "min-content-lines", measured: body, thresholds: { minContentLines: rule.minContentLines } },
102
+ };
94
103
  }
95
- return { verdict: "accept" };
104
+ return { verdict: "accept", gate: { reason: "policy-accept" } };
96
105
  }
97
106
  if (policy.defer.includes(proposal.source)) {
98
- return { verdict: "defer", reason: deferReasonForSource(proposal.source) };
107
+ const reason = deferReasonForSource(proposal.source);
108
+ return { verdict: "defer", reason, gate: { reason } };
99
109
  }
100
110
  // No matching rule — leave pending, untouched.
101
111
  return null;
@@ -220,6 +230,10 @@ async function dispatchJudgment(runner, prompt, seams) {
220
230
  raw = stdout;
221
231
  break;
222
232
  }
233
+ default:
234
+ // Exhaustiveness arm (H1): a 4th RunnerSpec kind becomes a compile error
235
+ // here instead of leaving `raw` undefined at runtime.
236
+ return assertNever(runner);
223
237
  }
224
238
  return parseJudgmentVerdict(raw);
225
239
  }
@@ -342,10 +356,31 @@ export async function drainProposals(opts, promoteFn = akmProposalAccept, reject
342
356
  // First, classify every proposal deterministically.
343
357
  const acceptIds = [];
344
358
  const rejectTargets = [];
359
+ const gateLabel = `triage:${opts.policy.name}`;
360
+ // Items deferred purely because they need a judge (no threshold-based reason)
361
+ // — these are re-stamped `no-judge-configured` when no runner resolves them.
362
+ const needsJudge = new Set();
345
363
  for (const proposal of pending) {
346
364
  const decision = classifyProposal(proposal, opts.policy, opts.maxDiffLines);
347
365
  if (decision === null)
348
366
  continue;
367
+ // #577: stamp the gate's verdict onto the proposal so `akm proposal show`
368
+ // can explain WHY it landed here. A dry-run performs zero writes, so it
369
+ // records nothing.
370
+ const outcome = decision.verdict === "accept" ? "auto-accepted" : decision.verdict === "reject" ? "auto-rejected" : "deferred";
371
+ stampGateDecision(opts, proposal.id, {
372
+ outcome,
373
+ reason: decision.gate.reason,
374
+ ...(decision.gate.measured !== undefined ? { measured: decision.gate.measured } : {}),
375
+ ...(decision.gate.thresholds ? { thresholds: decision.gate.thresholds } : {}),
376
+ gate: gateLabel,
377
+ });
378
+ // A defer with no threshold (mid-band / possible-dup from the defer list) is
379
+ // pending only because it needs adjudication — re-stampable to
380
+ // `no-judge-configured`. A band-based defer keeps its specific reason.
381
+ if (decision.verdict === "defer" && !decision.gate.thresholds) {
382
+ needsJudge.add(proposal.id);
383
+ }
349
384
  if (decision.verdict === "accept") {
350
385
  acceptIds.push(proposal.id);
351
386
  }
@@ -429,14 +464,51 @@ export async function drainProposals(opts, promoteFn = akmProposalAccept, reject
429
464
  if (tier.skippedByCap.length > 0) {
430
465
  info(`[triage] accept ceiling reached in judgment tier: ${tier.skippedByCap.length} judged-accept items skipped by cap (maxAccepts=${opts.maxAccepts})`);
431
466
  }
467
+ // #577: re-stamp the gate decision for items the judgment tier resolved so
468
+ // `akm proposal show` reflects the judge's verdict, not the earlier
469
+ // deterministic defer.
470
+ for (const id of tier.promoted) {
471
+ stampGateDecision(opts, id, { outcome: "auto-accepted", reason: "judgment-accept", gate: gateLabel });
472
+ }
473
+ for (const id of tier.rejected) {
474
+ stampGateDecision(opts, id, { outcome: "auto-rejected", reason: "judgment-reject", gate: gateLabel });
475
+ }
432
476
  // Replace the deferred list with only the items the judgment tier could NOT
433
477
  // resolve (verdict "defer", parse failure, or runner error). Staged
434
478
  // queue-mode accepts are RESOLVED and tracked in result.staged instead.
435
479
  result.deferred = tier.stillDeferred;
436
480
  }
481
+ else if (result.deferred.length > 0) {
482
+ // #577: no judgment runner configured — items deferred *because they need a
483
+ // judge* (mid-band / possible-dup, no threshold reason) stay pending solely
484
+ // for lack of one. Re-stamp those as `no-judge-configured` so the operator
485
+ // sees a per-proposal reason instead of inferring it from the run-level
486
+ // triage_deferred aggregate. Band-deferred items keep their specific reason
487
+ // (e.g. `max-diff-lines`), which is more actionable than "no judge".
488
+ for (const item of result.deferred) {
489
+ if (needsJudge.has(item.id)) {
490
+ stampGateDecision(opts, item.id, { outcome: "deferred", reason: "no-judge-configured", gate: gateLabel });
491
+ }
492
+ }
493
+ }
437
494
  emitDrainEvents(opts, result);
438
495
  return result;
439
496
  }
497
+ /**
498
+ * Persist a gate decision onto a proposal, honouring the dry-run contract
499
+ * (a dry run performs zero writes, so it records nothing) and never letting a
500
+ * persistence failure abort the drain (#577). Best-effort by design.
501
+ */
502
+ function stampGateDecision(opts, id, decision) {
503
+ if (opts.dryRun)
504
+ return;
505
+ try {
506
+ recordGateDecision(opts.stashDir, id, decision);
507
+ }
508
+ catch (err) {
509
+ warn(`[triage] failed to record gate decision for ${id}: ${err instanceof Error ? err.message : String(err)}`);
510
+ }
511
+ }
440
512
  // ---------------------------------------------------------------------------
441
513
  // Events
442
514
  // ---------------------------------------------------------------------------