akm-cli 0.9.1 → 0.9.2-alpha.2

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 (350) hide show
  1. package/CHANGELOG.md +103 -28
  2. package/README.md +3 -1
  3. package/SECURITY.md +1 -1
  4. package/STABILITY.md +1 -1
  5. package/dist/akm +2 -2
  6. package/dist/akm-migrate +2 -2
  7. package/dist/assets/hints/cli-hints-full.md +14 -9
  8. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
  9. package/dist/assets/improve-strategies/reflect-distill.json +1 -1
  10. package/dist/assets/models.json +35 -0
  11. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +3 -4
  12. package/dist/assets/stash-skeleton/facts/conventions/organization.md +1 -3
  13. package/dist/assets/tasks/core/extract.yml +6 -5
  14. package/dist/assets/tasks/core/improve.yml +6 -5
  15. package/dist/assets/tasks/core/index-refresh.yml +6 -5
  16. package/dist/assets/tasks/core/sync.yml +6 -5
  17. package/dist/assets/tasks/core/version-check.yml +6 -5
  18. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
  19. package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
  20. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
  21. package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
  22. package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
  23. package/dist/cli/confirm.js +2 -2
  24. package/dist/cli/parse-args.js +3 -24
  25. package/dist/cli/retired-commands.js +1 -1
  26. package/dist/cli/shared.js +2 -2
  27. package/dist/cli.js +11 -9
  28. package/dist/commands/agent/agent-dispatch.js +55 -89
  29. package/dist/commands/agent/contribute-cli.js +12 -45
  30. package/dist/commands/command/builtin-action.js +32 -0
  31. package/dist/commands/command/command-cli.js +99 -0
  32. package/dist/commands/command/command-execution.js +308 -0
  33. package/dist/commands/command/execution-source-loader.js +176 -0
  34. package/dist/commands/command/portable-template.js +60 -0
  35. package/dist/commands/config-cli.js +10 -4
  36. package/dist/commands/env/env.js +4 -2
  37. package/dist/commands/feedback-cli.js +1 -1
  38. package/dist/commands/health/checks.js +241 -29
  39. package/dist/commands/health/html-report.js +0 -14
  40. package/dist/commands/health/report-view-model.js +0 -1
  41. package/dist/commands/health/surfaces.js +6 -7
  42. package/dist/commands/health/types.js +0 -2
  43. package/dist/commands/health.js +63 -18
  44. package/dist/commands/improve/collapse-detector.js +5 -6
  45. package/dist/commands/improve/consolidate.js +251 -214
  46. package/dist/commands/improve/distill/promote-memory.js +71 -34
  47. package/dist/commands/improve/distill/quality-gate.js +17 -5
  48. package/dist/commands/improve/distill.js +232 -155
  49. package/dist/commands/improve/eligibility.js +112 -79
  50. package/dist/commands/improve/execution.js +57 -0
  51. package/dist/commands/improve/extract-cli.js +5 -5
  52. package/dist/commands/improve/extract-prompt.js +64 -22
  53. package/dist/commands/improve/extract.js +608 -360
  54. package/dist/commands/improve/improve-strategies.js +43 -14
  55. package/dist/commands/improve/improve.js +249 -29
  56. package/dist/commands/improve/loop-stages.js +11 -17
  57. package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
  58. package/dist/commands/improve/outcome-loop.js +22 -38
  59. package/dist/commands/improve/planner.js +134 -0
  60. package/dist/commands/improve/preparation.js +730 -409
  61. package/dist/commands/improve/reflect.js +386 -223
  62. package/dist/commands/improve/run-context.js +3 -4
  63. package/dist/commands/improve/salience.js +6 -58
  64. package/dist/commands/improve/session-asset.js +12 -12
  65. package/dist/commands/lint/index.js +101 -29
  66. package/dist/commands/migrate-cli.js +11 -69
  67. package/dist/commands/migration-tool.js +6 -9
  68. package/dist/commands/models-cli.js +27 -0
  69. package/dist/commands/proposal/drain.js +258 -186
  70. package/dist/commands/proposal/proposal-cli.js +32 -10
  71. package/dist/commands/proposal/proposal.js +2 -5
  72. package/dist/commands/proposal/propose.js +192 -172
  73. package/dist/commands/proposal/repository.js +54 -91
  74. package/dist/commands/proposal/validators/proposal-validators.js +9 -7
  75. package/dist/commands/read/curate.js +53 -22
  76. package/dist/commands/read/registry-search.js +25 -9
  77. package/dist/commands/read/remember-cli.js +14 -2
  78. package/dist/commands/read/search.js +10 -4
  79. package/dist/commands/read/show.js +139 -153
  80. package/dist/commands/registry-cli.js +16 -7
  81. package/dist/commands/remember.js +33 -18
  82. package/dist/commands/sources/add-cli.js +19 -178
  83. package/dist/commands/sources/bundle-cli.js +15 -3
  84. package/dist/commands/sources/dangerous-env-audit.js +135 -0
  85. package/dist/commands/sources/info.js +2 -1
  86. package/dist/commands/sources/installed-stashes.js +901 -177
  87. package/dist/commands/sources/schema-repair.js +174 -95
  88. package/dist/commands/sources/self-update.js +30 -74
  89. package/dist/commands/sources/source-add.js +3 -5
  90. package/dist/commands/sources/sources-cli.js +2 -15
  91. package/dist/commands/sources/update-transaction.js +220 -0
  92. package/dist/commands/tasks/tasks-cli.js +3 -3
  93. package/dist/commands/tasks/tasks.js +736 -317
  94. package/dist/commands/workflow-cli.js +2 -2
  95. package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
  96. package/dist/core/adapter/adapters/akm-adapter.js +85 -35
  97. package/dist/core/adapter/adapters/akm-lint.js +54 -39
  98. package/dist/core/adapter/adapters/akm-metadata.js +45 -45
  99. package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
  100. package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
  101. package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
  102. package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
  103. package/dist/core/adapter/adapters/index.js +0 -9
  104. package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
  105. package/dist/core/adapter/adapters/okf-adapter.js +4 -0
  106. package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
  107. package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
  108. package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
  109. package/dist/core/adapter/execution-source.js +308 -0
  110. package/dist/core/adapter/recognize-match.js +36 -13
  111. package/dist/core/adapter/registry.js +0 -9
  112. package/dist/core/asset/stash-meta.js +94 -4
  113. package/dist/core/common.js +6 -11
  114. package/dist/core/config/config-io.js +3 -3
  115. package/dist/core/config/config-schema.js +18 -40
  116. package/dist/core/config/config-sources.js +11 -21
  117. package/dist/core/config/config-walker.js +31 -13
  118. package/dist/core/config/config.js +23 -26
  119. package/dist/core/config/schema/engines.js +8 -7
  120. package/dist/core/config/schema/improve-processes.js +29 -5
  121. package/dist/core/config/schema/index-config.js +0 -27
  122. package/dist/core/config/schema/primitives.js +1 -23
  123. package/dist/core/config/schema/sources-bundles.js +13 -16
  124. package/dist/core/errors.js +2 -0
  125. package/dist/core/events.js +68 -32
  126. package/dist/core/extra-params.js +1 -0
  127. package/dist/core/improve-result.js +315 -0
  128. package/dist/core/lesson-lint.js +0 -6
  129. package/dist/core/maintenance-barrier.js +4 -4
  130. package/dist/core/network-policy.js +152 -0
  131. package/dist/core/paths.js +1 -1
  132. package/dist/core/recognition-util.js +4 -4
  133. package/dist/core/registry-url.js +456 -0
  134. package/dist/core/state/migrations.js +161 -47
  135. package/dist/core/state-db.js +453 -80
  136. package/dist/core/system-error.js +32 -0
  137. package/dist/core/time.js +2 -12
  138. package/dist/core/write-source.js +0 -18
  139. package/dist/execution/directory-identity.js +52 -0
  140. package/dist/execution/executable-identity.js +107 -0
  141. package/dist/execution/guarded-source.js +398 -0
  142. package/dist/execution/json.js +95 -0
  143. package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
  144. package/dist/execution/record.js +55 -0
  145. package/dist/execution/resolved-request.js +730 -0
  146. package/dist/execution/source.js +320 -0
  147. package/dist/indexer/bundle-identity-guard.js +5 -4
  148. package/dist/indexer/db/graph-db.js +33 -0
  149. package/dist/indexer/graph/graph-boost.js +3 -4
  150. package/dist/indexer/graph/graph-extraction.js +562 -373
  151. package/dist/indexer/index-written-assets.js +78 -39
  152. package/dist/indexer/indexer.js +471 -432
  153. package/dist/indexer/installations.js +6 -0
  154. package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
  155. package/dist/indexer/materialize-embeddings.js +155 -0
  156. package/dist/indexer/passes/memory-inference.js +227 -174
  157. package/dist/indexer/passes/metadata.js +263 -118
  158. package/dist/indexer/scan/doc-to-entry.js +7 -10
  159. package/dist/indexer/scan/drain-dir.js +51 -23
  160. package/dist/indexer/search/db-search.js +156 -50
  161. package/dist/indexer/search/fts-query.js +40 -40
  162. package/dist/indexer/search/ranking.js +36 -1
  163. package/dist/indexer/search/search-attribution.js +3 -1
  164. package/dist/indexer/search/search-fields.js +23 -14
  165. package/dist/indexer/search/search-hit-enrichers.js +1 -1
  166. package/dist/indexer/search/search-source.js +7 -16
  167. package/dist/indexer/search/semantic-status.js +10 -1
  168. package/dist/indexer/usage/show-usage.js +105 -0
  169. package/dist/indexer/usage/usage-events.js +7 -2
  170. package/dist/indexer/walk/matchers.js +40 -10
  171. package/dist/indexer/walk/path-resolver.js +5 -2
  172. package/dist/indexer/walk/walker.js +20 -2
  173. package/dist/integrations/agent/builder-shared.js +3 -6
  174. package/dist/integrations/agent/conversation-fallback.js +16 -0
  175. package/dist/integrations/agent/engine-resolution.js +87 -87
  176. package/dist/integrations/agent/execution-cascade.js +566 -0
  177. package/dist/integrations/agent/execution-definitions.js +211 -0
  178. package/dist/integrations/agent/execution-lowering.js +811 -0
  179. package/dist/integrations/agent/execution-preparation.js +67 -0
  180. package/dist/integrations/agent/index.js +0 -2
  181. package/dist/integrations/agent/inline-execution.js +74 -0
  182. package/dist/integrations/agent/model-map.js +515 -0
  183. package/dist/integrations/agent/persona-fallback.js +30 -0
  184. package/dist/integrations/agent/request-lowering.js +186 -0
  185. package/dist/integrations/agent/runner-dispatch.js +230 -37
  186. package/dist/integrations/agent/runner.js +12 -83
  187. package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
  188. package/dist/integrations/harnesses/aider/index.js +0 -1
  189. package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
  190. package/dist/integrations/harnesses/amazonq/index.js +0 -1
  191. package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
  192. package/dist/integrations/harnesses/claude/index.js +1 -5
  193. package/dist/integrations/harnesses/claude/session-log.js +3 -33
  194. package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
  195. package/dist/integrations/harnesses/codex/index.js +0 -1
  196. package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
  197. package/dist/integrations/harnesses/copilot/index.js +0 -1
  198. package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
  199. package/dist/integrations/harnesses/gemini/index.js +0 -1
  200. package/dist/integrations/harnesses/index.js +4 -44
  201. package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
  202. package/dist/integrations/harnesses/opencode/index.js +0 -2
  203. package/dist/integrations/harnesses/opencode/session-log.js +14 -204
  204. package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
  205. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
  206. package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
  207. package/dist/integrations/harnesses/openhands/index.js +0 -1
  208. package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
  209. package/dist/integrations/harnesses/pi/index.js +0 -1
  210. package/dist/integrations/harnesses/shared.js +0 -1
  211. package/dist/integrations/harnesses/types.js +1 -3
  212. package/dist/integrations/lockfile.js +82 -79
  213. package/dist/integrations/session-logs/index.js +6 -17
  214. package/dist/integrations/session-logs/provider-base.js +1 -29
  215. package/dist/llm/client.js +10 -5
  216. package/dist/llm/embedder.js +6 -7
  217. package/dist/llm/embedders/local.js +37 -88
  218. package/dist/llm/embedders/types.js +1 -1
  219. package/dist/llm/graph-extract.js +75 -50
  220. package/dist/llm/index-passes.js +43 -5
  221. package/dist/llm/memory-infer.js +8 -6
  222. package/dist/llm/metadata-enhance.js +5 -3
  223. package/dist/llm/structured-call.js +122 -25
  224. package/dist/output/format-exempt.js +1 -1
  225. package/dist/output/render-registry.js +0 -16
  226. package/dist/output/renderers.js +12 -7
  227. package/dist/output/shapes/curate.js +1 -0
  228. package/dist/output/shapes/helpers.js +10 -2
  229. package/dist/output/shapes/passthrough.js +2 -0
  230. package/dist/output/text/command-format.js +31 -33
  231. package/dist/output/text/health-format.js +1 -29
  232. package/dist/output/text/migrate.js +6 -56
  233. package/dist/output/text/proposal-format.js +16 -1
  234. package/dist/output/text/workflow-format.js +16 -0
  235. package/dist/registry/network.js +279 -0
  236. package/dist/registry/pinned-request-helper.js +247 -0
  237. package/dist/registry/pinned-transport.js +717 -0
  238. package/dist/registry/providers/skills-sh.js +18 -6
  239. package/dist/registry/providers/static-index.js +20 -7
  240. package/dist/registry/resolve.js +53 -28
  241. package/dist/scripts/akm-migrate-node.js +19334 -52269
  242. package/dist/scripts/akm-migrate.js +19270 -51612
  243. package/dist/setup/registry-stash-loader.js +64 -20
  244. package/dist/setup/semantic-assets.js +9 -34
  245. package/dist/setup/setup.js +12 -30
  246. package/dist/setup/source-identity.js +17 -0
  247. package/dist/setup/steps/sources.js +36 -15
  248. package/dist/setup/steps/tasks.js +39 -11
  249. package/dist/sources/providers/git-provider.js +3 -3
  250. package/dist/sources/providers/npm.js +2 -2
  251. package/dist/sources/providers/provider-utils.js +4 -3
  252. package/dist/sources/providers/website.js +11 -7
  253. package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
  254. package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
  255. package/dist/sources/website-url.js +73 -0
  256. package/dist/storage/engines/sqlite-migrations.js +81 -26
  257. package/dist/storage/managed-db.js +27 -24
  258. package/dist/storage/repositories/events-repository.js +3 -0
  259. package/dist/storage/repositories/index-connection.js +42 -10
  260. package/dist/storage/repositories/index-entries-repository.js +203 -229
  261. package/dist/storage/repositories/index-entry-mapper.js +8 -12
  262. package/dist/storage/repositories/index-entry-schema.js +255 -0
  263. package/dist/storage/repositories/index-fts-repository.js +64 -71
  264. package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
  265. package/dist/storage/repositories/index-meta-repository.js +0 -11
  266. package/dist/storage/repositories/index-schema.js +74 -350
  267. package/dist/storage/repositories/index-utility-repository.js +12 -17
  268. package/dist/storage/repositories/index-vec-repository.js +56 -7
  269. package/dist/storage/repositories/proposals-repository.js +4 -127
  270. package/dist/storage/repositories/registry-cache.js +2 -1
  271. package/dist/storage/repositories/task-history-repository.js +20 -40
  272. package/dist/storage/repositories/workflow-runs-repository.js +228 -129
  273. package/dist/storage/sqlite-read-snapshot.js +148 -0
  274. package/dist/tasks/backends/cron.js +170 -42
  275. package/dist/tasks/backends/index.js +1 -1
  276. package/dist/tasks/backends/launchd.js +787 -202
  277. package/dist/tasks/backends/schtasks.js +282 -83
  278. package/dist/tasks/embedded.js +7 -7
  279. package/dist/tasks/frozen-script.js +50 -0
  280. package/dist/tasks/resolve-akm-bin.js +5 -1
  281. package/dist/tasks/runner.js +239 -251
  282. package/dist/tasks/runtime-v3.js +281 -0
  283. package/dist/tasks/scheduler-binding.js +272 -0
  284. package/dist/tasks/scheduler-invocation.js +57 -43
  285. package/dist/tasks/scheduler-sync.js +654 -0
  286. package/dist/tasks/source-v3.js +752 -0
  287. package/dist/tasks/standalone-script-entry.js +5 -0
  288. package/dist/tasks/task-id.js +29 -0
  289. package/dist/workflows/authoring/authoring.js +15 -32
  290. package/dist/workflows/exec/dispatch-redaction.js +14 -8
  291. package/dist/workflows/exec/exec-unit.js +7 -28
  292. package/dist/workflows/exec/frozen-judge.js +57 -89
  293. package/dist/workflows/exec/lowering-notices.js +23 -0
  294. package/dist/workflows/exec/native-executor.js +301 -458
  295. package/dist/workflows/exec/param-secrets.js +4 -3
  296. package/dist/workflows/exec/run-workflow.js +26 -32
  297. package/dist/workflows/exec/step-work.js +105 -109
  298. package/dist/workflows/exec/unit-dispatch.js +103 -27
  299. package/dist/workflows/exec/unit-writer.js +3 -3
  300. package/dist/workflows/exec/worktree.js +2 -2
  301. package/dist/workflows/ir/compile.js +86 -72
  302. package/dist/workflows/ir/environment-v4.js +328 -0
  303. package/dist/workflows/ir/freeze-v4.js +122 -0
  304. package/dist/workflows/ir/plan-hash.js +13 -7
  305. package/dist/workflows/ir/schema-v4.js +525 -0
  306. package/dist/workflows/ir/schema.js +25 -284
  307. package/dist/workflows/ir/source-freeze-v4.js +506 -0
  308. package/dist/workflows/parser.js +27 -24
  309. package/dist/workflows/program/schema.js +1 -2
  310. package/dist/workflows/renderer.js +42 -29
  311. package/dist/workflows/resource-limits.js +4 -5
  312. package/dist/workflows/runtime/agent-identity.js +11 -13
  313. package/dist/workflows/runtime/plan-classifier.js +8 -8
  314. package/dist/workflows/runtime/runs.js +27 -43
  315. package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
  316. package/dist/workflows/source-files.js +373 -0
  317. package/dist/workflows/source-ir/compile.js +196 -0
  318. package/dist/workflows/source-ir/github-yaml.js +577 -0
  319. package/dist/workflows/source-ir/ordering.js +38 -0
  320. package/dist/workflows/source-ir/program.js +50 -0
  321. package/dist/workflows/source-ir/result.js +26 -0
  322. package/dist/workflows/source-ir/schema.js +772 -0
  323. package/dist/workflows/source-ir/semantics.js +242 -0
  324. package/dist/workflows/source-ir/uses.js +14 -0
  325. package/docs/README.md +2 -0
  326. package/docs/migration/README.md +3 -1
  327. package/docs/migration/release-notes/0.9.2.md +55 -0
  328. package/docs/migration/release-notes/README.md +5 -0
  329. package/docs/migration/v0.8-to-v0.9.md +76 -1077
  330. package/docs/migration/v0.9.0-troubleshooting.md +104 -516
  331. package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
  332. package/docs/reference/README.md +1 -0
  333. package/docs/reference/cli.md +230 -98
  334. package/docs/reference/configuration.md +159 -36
  335. package/docs/reference/data-and-telemetry.md +19 -1
  336. package/docs/reference/supported-formats.md +23 -3
  337. package/docs/reference/tasks.md +182 -0
  338. package/docs/reference/workflow-schema.md +91 -40
  339. package/docs/reference/workflows.md +33 -6
  340. package/package.json +10 -6
  341. package/schemas/akm-config.json +372 -224
  342. package/schemas/akm-task.json +324 -80
  343. package/schemas/akm-workflow.json +6 -9
  344. package/dist/core/migration-operation.js +0 -75
  345. package/dist/integrations/agent/model-aliases.js +0 -74
  346. package/dist/tasks/parser.js +0 -380
  347. package/dist/tasks/schema.js +0 -123
  348. package/dist/tasks/validator.js +0 -80
  349. package/dist/workflows/ir/freeze.js +0 -320
  350. package/dist/workflows/runtime/document-cache.js +0 -13
@@ -23,9 +23,9 @@
23
23
  * - The judgment tier (Phase 3) adjudicates the deferred items: when a
24
24
  * `judgment` RunnerSpec is supplied the engine pre-fetches context (the live
25
25
  * asset + sibling pending proposals for the same ref) into a prompt,
26
- * dispatches it to the configured runner (llm → `chatCompletion`, agent →
27
- * `runAgent`, sdk `runOpencodeSdk`, mirroring `reflect.ts`'s switch), and
28
- * performs the resulting accept / reject *itself* (the runner only judges).
26
+ * dispatches it through the shared resolved/lowered execution boundary,
27
+ * and performs the resulting accept / reject *itself* (the runner only
28
+ * judges).
29
29
  * Items the runner cannot resolve — and any deferred items when no runner is
30
30
  * configured — surface a `triage_deferred` event so "enabled, no agent"
31
31
  * never silently looks like full success.
@@ -40,13 +40,14 @@ import path from "node:path";
40
40
  import { assetPathForName, stashDirFor } from "../../core/asset/asset-placement.js";
41
41
  import { parseFrontmatter } from "../../core/asset/frontmatter.js";
42
42
  import { parseRefInput } from "../../core/asset/resolve-ref.js";
43
+ import { ConfigError } from "../../core/errors.js";
43
44
  import { appendEvent } from "../../core/events.js";
44
45
  import { escapeJsonStringControls, stripCodeFences, stripThinkBlocks } from "../../core/parse.js";
45
46
  import { info, warn } from "../../core/warn.js";
46
- import { executeRunner } from "../../integrations/agent/runner-dispatch.js";
47
- import { chatCompletion } from "../../llm/client.js";
47
+ import { acquireLoweredExecutionDispatchLease, dispatchLoweredExecutionRequest, disposeLoweredExecutionDispatchLease, lowerResolvedExecutionRequestWithRunner, } from "../../integrations/agent/execution-lowering.js";
48
+ import { prepareInlineExecutionWithRunner } from "../../integrations/agent/inline-execution.js";
48
49
  import { akmProposalAccept, akmProposalReject } from "./proposal.js";
49
- import { listProposals, preflightProposalPromotion, proposalContent, recordGateDecision, } from "./repository.js";
50
+ import { listProposals, listProposalsReadOnly, preflightProposalPromotion, proposalContent, recordGateDecision, } from "./repository.js";
50
51
  // ---------------------------------------------------------------------------
51
52
  // Content helpers
52
53
  // ---------------------------------------------------------------------------
@@ -208,39 +209,56 @@ export function parseJudgmentVerdict(raw) {
208
209
  return null;
209
210
  return { decision, reason: typeof reason === "string" ? reason : "" };
210
211
  }
211
- /**
212
- * Dispatch a single judgment prompt to the resolved runner via the unified
213
- * {@link executeRunner} seam (X3). The `llm` arm is drain-specific (wraps
214
- * `chatCompletion` — no filesystem) so it is supplied as the `llm` handler; the
215
- * byte-identical `agent` / `sdk` arms route to the default profile runners (or
216
- * the injected {@link JudgmentSeams} test fakes). A failed spawn warns and
217
- * yields `null`, matching the prior per-arm behavior.
218
- */
219
- async function dispatchJudgment(runner, prompt, seams) {
220
- const runOptions = {
221
- stdio: "captured",
222
- parseOutput: "text",
223
- ...(runner.timeoutMs !== undefined ? { timeoutMs: runner.timeoutMs } : {}),
224
- };
225
- const result = await executeRunner(runner, prompt, runOptions, {
226
- llm: async (spec, p) => {
227
- const messages = [{ role: "user", content: p }];
228
- const raw = seams.chat
229
- ? await seams.chat(spec, messages)
230
- : await chatCompletion(spec.connection, messages, {
231
- ...(spec.timeoutMs !== undefined ? { timeoutMs: spec.timeoutMs } : {}),
232
- });
233
- // chatCompletion has no failure envelope — a returned string is success.
234
- return { ok: true, exitCode: 0, stdout: raw, stderr: "", durationMs: 0 };
235
- },
212
+ async function dispatchJudgment(runner, prompt, seams, lease) {
213
+ const prepared = prepareInlineExecutionWithRunner({
214
+ content: prompt,
215
+ runner,
216
+ invocationKind: "direct",
217
+ });
218
+ const lowered = lowerResolvedExecutionRequestWithRunner(prepared.request, prepared.runner);
219
+ const chat = seams.chat;
220
+ const llmRunner = lowered.runner.kind === "llm" ? lowered.runner : undefined;
221
+ const dispatchOptions = {
222
+ lease,
236
223
  ...(seams.runAgentFn ? { runAgent: seams.runAgentFn } : {}),
237
224
  ...(seams.runSdkFn ? { runSdk: seams.runSdkFn } : {}),
238
- });
225
+ ...(chat && llmRunner
226
+ ? {
227
+ chat: async (connection, messages) => chat({ ...llmRunner, connection }, messages),
228
+ }
229
+ : {}),
230
+ };
231
+ let result;
232
+ try {
233
+ result = await dispatchLoweredExecutionRequest(lowered, dispatchOptions);
234
+ }
235
+ catch (error) {
236
+ if (error instanceof ConfigError)
237
+ throw error;
238
+ return {
239
+ verdict: null,
240
+ notices: lowered.notices,
241
+ error: error instanceof Error ? error.message : String(error),
242
+ };
243
+ }
239
244
  if (!result.ok) {
240
- warn(`[triage] judgment ${runner.kind} failed: ${result.error ?? result.reason ?? "unknown error"}`);
241
- return null;
245
+ return {
246
+ verdict: null,
247
+ notices: lowered.notices,
248
+ error: result.error ?? result.reason ?? "unknown error",
249
+ };
242
250
  }
243
- return parseJudgmentVerdict(result.stdout);
251
+ return { verdict: parseJudgmentVerdict(result.stdout), notices: lowered.notices };
252
+ }
253
+ /** Validate symbolic judgment credentials without contacting a provider. */
254
+ async function preflightJudgmentRunner(runner) {
255
+ const prepared = prepareInlineExecutionWithRunner({
256
+ content: "Validate the selected proposal judgment runner before mutation.",
257
+ runner,
258
+ invocationKind: "direct",
259
+ });
260
+ const lowered = lowerResolvedExecutionRequestWithRunner(prepared.request, prepared.runner);
261
+ return acquireLoweredExecutionDispatchLease(lowered);
244
262
  }
245
263
  function judgedContentHash(proposal) {
246
264
  return createHash("sha256").update(proposalContent(proposal), "utf8").digest("hex");
@@ -260,6 +278,7 @@ async function runJudgmentTier(input) {
260
278
  const staged = [];
261
279
  const skippedByCap = [];
262
280
  const stillDeferred = [];
281
+ const noticesByKey = new Map();
263
282
  // Remaining accept budget shared with the deterministic promote loop.
264
283
  let acceptBudget = Math.max(0, input.remainingAcceptBudget);
265
284
  for (const item of input.deferred) {
@@ -270,15 +289,28 @@ async function runJudgmentTier(input) {
270
289
  }
271
290
  const ctx = prefetchJudgmentContext(input.stashDir, proposal, input.pending);
272
291
  const prompt = buildJudgmentPrompt(proposal, item.reason, ctx);
273
- let verdict;
292
+ let dispatch;
274
293
  try {
275
- verdict = await dispatchJudgment(input.runner, prompt, input.seams);
294
+ dispatch = await dispatchJudgment(input.runner, prompt, input.seams, input.lease);
276
295
  }
277
296
  catch (err) {
297
+ if (err instanceof ConfigError)
298
+ throw err;
278
299
  warn(`[triage] judgment dispatch failed for ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
279
300
  stillDeferred.push(item);
280
301
  continue;
281
302
  }
303
+ for (const notice of dispatch.notices) {
304
+ const key = JSON.stringify(notice);
305
+ if (!noticesByKey.has(key))
306
+ noticesByKey.set(key, notice);
307
+ }
308
+ if (dispatch.error) {
309
+ warn(`[triage] judgment dispatch failed for ${item.id}: ${dispatch.error}`);
310
+ stillDeferred.push(item);
311
+ continue;
312
+ }
313
+ const verdict = dispatch.verdict;
282
314
  if (!verdict || verdict.decision === "defer") {
283
315
  stillDeferred.push(item);
284
316
  continue;
@@ -367,37 +399,35 @@ async function runJudgmentTier(input) {
367
399
  stillDeferred.push(item);
368
400
  }
369
401
  }
370
- return { promoted, rejected, staged, skippedByCap, stillDeferred };
402
+ return {
403
+ promoted,
404
+ rejected,
405
+ staged,
406
+ skippedByCap,
407
+ stillDeferred,
408
+ notices: Object.freeze([...noticesByKey.values()]),
409
+ };
371
410
  }
372
- // ---------------------------------------------------------------------------
373
- // Engine
374
- // ---------------------------------------------------------------------------
375
- /**
376
- * Drain the standing pending backlog under a deterministic policy.
377
- *
378
- * @param opts Drain options (policy, applyMode, ceilings, dry-run).
379
- * @param promoteFn Injectable override for `akmProposalAccept` (test seam).
380
- * @param rejectFn Injectable override for `akmProposalReject` (test seam).
381
- */
382
- export async function drainProposals(opts, promoteFn = akmProposalAccept, rejectFn = akmProposalReject, judgmentSeams = {}) {
383
- const result = { promoted: [], rejected: [], deferred: [], skippedByCap: [], staged: [] };
411
+ /** Classify the queue without mutating proposal, event, or promotion state. */
412
+ function classifyPendingProposals(opts) {
384
413
  const exclude = opts.excludeIds ?? new Set();
385
- const pending = listProposals(opts.stashDir, { status: "pending" }).filter((p) => !exclude.has(p.id));
386
- // First, classify every proposal deterministically.
414
+ // A configured judgment runner must be credential-validated before any live
415
+ // state connection or migration. Its classification pass therefore reads an
416
+ // isolated SQLite snapshot; deterministic-only drains retain the historical
417
+ // live/migrating queue read.
418
+ const pending = (opts.judgment ? listProposalsReadOnly : listProposals)(opts.stashDir, {
419
+ status: "pending",
420
+ }).filter((proposal) => !exclude.has(proposal.id));
387
421
  const acceptIds = [];
388
422
  const acceptGateReasons = new Map();
389
423
  const rejectTargets = [];
424
+ const deferred = [];
425
+ const deferredGateDecisions = [];
390
426
  const gateLabel = `triage:${opts.policy.name}`;
391
- // Items deferred purely because they need a judge (no threshold-based reason)
392
- // — these are re-stamped `no-judge-configured` when no runner resolves them.
393
427
  const needsJudge = new Set();
394
428
  for (const proposal of pending) {
395
- // Do NOT reclassify a proposal that was already conclusively stamped
396
- // `auto-rejected` by a prior gate run (e.g. the improve confidence gate).
397
- // Overwriting an authoritative rejection with `auto-accepted` would corrupt
398
- // the audit trail and silently promote content the gate explicitly rejected.
399
- // Such proposals remain pending until the TTL expires them — the queue is
400
- // audited-autonomous; no manual-review rung exists (06-M3).
429
+ // An authoritative rejection from another gate stays pending and is never
430
+ // silently overwritten by this triage policy.
401
431
  if (proposal.gateDecision?.outcome === "auto-rejected" && !proposal.gateDecision.gate?.startsWith("triage:")) {
402
432
  continue;
403
433
  }
@@ -411,23 +441,19 @@ export async function drainProposals(opts, promoteFn = akmProposalAccept, reject
411
441
  const decision = classifyProposal(proposal, opts.policy, opts.maxDiffLines);
412
442
  if (decision === null)
413
443
  continue;
414
- // #577: stamp the gate's verdict onto the proposal so `akm proposal show`
415
- // can explain WHY it landed here. A dry-run performs zero writes, so it
416
- // records nothing.
417
444
  if (decision.verdict === "defer") {
418
- stampGateDecision(opts, proposal.id, {
419
- outcome: "deferred",
420
- reason: decision.gate.reason,
421
- ...(decision.gate.measured !== undefined ? { measured: decision.gate.measured } : {}),
422
- ...(decision.gate.thresholds ? { thresholds: decision.gate.thresholds } : {}),
423
- gate: gateLabel,
445
+ deferredGateDecisions.push({
446
+ id: proposal.id,
447
+ decision: {
448
+ outcome: "deferred",
449
+ reason: decision.gate.reason,
450
+ ...(decision.gate.measured !== undefined ? { measured: decision.gate.measured } : {}),
451
+ ...(decision.gate.thresholds ? { thresholds: decision.gate.thresholds } : {}),
452
+ gate: gateLabel,
453
+ },
424
454
  });
425
- }
426
- // A defer with no threshold (mid-band / possible-dup from the defer list) is
427
- // pending only because it needs adjudication — re-stampable to
428
- // `no-judge-configured`. A band-based defer keeps its specific reason.
429
- if (decision.verdict === "defer" && !decision.gate.thresholds) {
430
- needsJudge.add(proposal.id);
455
+ if (!decision.gate.thresholds)
456
+ needsJudge.add(proposal.id);
431
457
  }
432
458
  if (decision.verdict === "accept") {
433
459
  acceptIds.push(proposal.id);
@@ -437,141 +463,187 @@ export async function drainProposals(opts, promoteFn = akmProposalAccept, reject
437
463
  rejectTargets.push({ id: proposal.id, reason: decision.reason });
438
464
  }
439
465
  else {
440
- result.deferred.push({ id: proposal.id, reason: decision.reason });
466
+ deferred.push({ id: proposal.id, reason: decision.reason });
441
467
  }
442
468
  }
443
- // --- Reject empties (independent of the accept ceiling / applyMode) ---
444
- for (const target of rejectTargets) {
445
- if (opts.dryRun) {
446
- result.rejected.push(target.id);
447
- continue;
448
- }
449
- try {
450
- await rejectFn({
451
- stashDir: opts.stashDir,
452
- id: target.id,
453
- reason: target.reason,
454
- gateDecision: { outcome: "auto-rejected", reason: "empty-diff", gate: gateLabel },
455
- });
456
- result.rejected.push(target.id);
457
- }
458
- catch (err) {
459
- warn(`[triage] reject failed for ${target.id}: ${err instanceof Error ? err.message : String(err)}`);
460
- }
461
- }
462
- // --- Accept ceiling: enforced BEFORE the promote loop ---
463
- const withinCap = acceptIds.slice(0, Math.max(0, opts.maxAccepts));
464
- result.skippedByCap = acceptIds.slice(Math.max(0, opts.maxAccepts));
465
- if (result.skippedByCap.length > 0) {
466
- info(`[triage] accept ceiling reached: ${withinCap.length} promoted, ${result.skippedByCap.length} skipped by cap (maxAccepts=${opts.maxAccepts})`);
467
- }
468
- // --- Promotion gate: applyMode "queue" never promotes (stage only) ---
469
- // Count deterministic promotions so the judgment tier shares the same accept
470
- // budget (deterministic + judgment promotions ≤ maxAccepts).
471
- let deterministicPromoted = 0;
472
- if (opts.applyMode === "promote" && !opts.dryRun) {
473
- info(`[triage] auto-promote active: ${withinCap.length} accepts allowed this run`);
474
- for (const id of withinCap) {
469
+ return {
470
+ pending,
471
+ acceptIds,
472
+ acceptGateReasons,
473
+ rejectTargets,
474
+ deferred,
475
+ deferredGateDecisions,
476
+ gateLabel,
477
+ needsJudge,
478
+ };
479
+ }
480
+ /**
481
+ * Drain the standing pending backlog under a deterministic policy.
482
+ *
483
+ * @param opts Drain options (policy, applyMode, ceilings, dry-run).
484
+ * @param promoteFn Injectable override for `akmProposalAccept` (test seam).
485
+ * @param rejectFn Injectable override for `akmProposalReject` (test seam).
486
+ */
487
+ export async function drainProposals(opts, promoteFn = akmProposalAccept, rejectFn = akmProposalReject, judgmentSeams = {}) {
488
+ const classification = classifyPendingProposals(opts);
489
+ const { pending, acceptIds, acceptGateReasons, rejectTargets, deferredGateDecisions, gateLabel, needsJudge } = classification;
490
+ const result = {
491
+ promoted: [],
492
+ rejected: [],
493
+ deferred: classification.deferred,
494
+ skippedByCap: [],
495
+ staged: [],
496
+ };
497
+ // A configured judgment runner makes every deferred item dispatch-eligible.
498
+ // Validate its symbolic credentials before applying any deterministic gate,
499
+ // reject, promote, or event mutation. Provider/runtime failures remain the
500
+ // judgment tier's fail-soft responsibility after this configuration fence.
501
+ const dispatchLease = opts.judgment && result.deferred.length > 0 ? await preflightJudgmentRunner(opts.judgment) : undefined;
502
+ try {
503
+ for (const { id, decision } of deferredGateDecisions)
504
+ stampGateDecision(opts, id, decision);
505
+ // --- Reject empties (independent of the accept ceiling / applyMode) ---
506
+ for (const target of rejectTargets) {
507
+ if (opts.dryRun) {
508
+ result.rejected.push(target.id);
509
+ continue;
510
+ }
475
511
  try {
476
- await promoteFn({
512
+ await rejectFn({
477
513
  stashDir: opts.stashDir,
478
- id,
479
- ...(opts.target ? { target: opts.target } : {}),
480
- ...(opts.config ? { config: opts.config } : {}),
481
- gateDecision: {
482
- outcome: "auto-accepted",
483
- reason: acceptGateReasons.get(id) ?? "policy-accept",
484
- gate: gateLabel,
485
- },
514
+ id: target.id,
515
+ reason: target.reason,
516
+ gateDecision: { outcome: "auto-rejected", reason: "empty-diff", gate: gateLabel },
486
517
  });
487
- result.promoted.push(id);
488
- deterministicPromoted += 1;
518
+ result.rejected.push(target.id);
489
519
  }
490
520
  catch (err) {
491
- warn(`[triage] promote failed for ${id}: ${err instanceof Error ? err.message : String(err)}`);
521
+ warn(`[triage] reject failed for ${target.id}: ${err instanceof Error ? err.message : String(err)}`);
492
522
  }
493
523
  }
494
- }
495
- else if (opts.applyMode === "promote" && opts.dryRun) {
496
- // Exercise the same stamped candidate and lint boundary as real promotion.
497
- // Tests that omit config retain the classification-only seam.
498
- const byId = new Map(pending.map((proposal) => [proposal.id, proposal]));
499
- for (const id of withinCap) {
500
- try {
501
- if (opts.config) {
502
- const proposal = byId.get(id);
503
- if (!proposal)
504
- throw new Error(`Proposal ${id} disappeared during drain preflight.`);
505
- preflightProposalPromotion(opts.config, proposal, {
524
+ // --- Accept ceiling: enforced BEFORE the promote loop ---
525
+ const withinCap = acceptIds.slice(0, Math.max(0, opts.maxAccepts));
526
+ result.skippedByCap = acceptIds.slice(Math.max(0, opts.maxAccepts));
527
+ if (result.skippedByCap.length > 0) {
528
+ info(`[triage] accept ceiling reached: ${withinCap.length} promoted, ${result.skippedByCap.length} skipped by cap (maxAccepts=${opts.maxAccepts})`);
529
+ }
530
+ // --- Promotion gate: applyMode "queue" never promotes (stage only) ---
531
+ // Count deterministic promotions so the judgment tier shares the same accept
532
+ // budget (deterministic + judgment promotions ≤ maxAccepts).
533
+ let deterministicPromoted = 0;
534
+ if (opts.applyMode === "promote" && !opts.dryRun) {
535
+ info(`[triage] auto-promote active: ${withinCap.length} accepts allowed this run`);
536
+ for (const id of withinCap) {
537
+ try {
538
+ await promoteFn({
539
+ stashDir: opts.stashDir,
540
+ id,
506
541
  ...(opts.target ? { target: opts.target } : {}),
542
+ ...(opts.config ? { config: opts.config } : {}),
507
543
  gateDecision: {
508
544
  outcome: "auto-accepted",
509
545
  reason: acceptGateReasons.get(id) ?? "policy-accept",
510
546
  gate: gateLabel,
511
547
  },
512
548
  });
549
+ result.promoted.push(id);
550
+ deterministicPromoted += 1;
551
+ }
552
+ catch (err) {
553
+ warn(`[triage] promote failed for ${id}: ${err instanceof Error ? err.message : String(err)}`);
513
554
  }
514
- result.promoted.push(id);
515
- deterministicPromoted += 1;
516
555
  }
517
- catch (err) {
518
- warn(`[triage] preflight failed for ${id}: ${err instanceof Error ? err.message : String(err)}`);
556
+ }
557
+ else if (opts.applyMode === "promote" && opts.dryRun) {
558
+ // Exercise the same stamped candidate and lint boundary as real promotion.
559
+ // Tests that omit config retain the classification-only seam.
560
+ const byId = new Map(pending.map((proposal) => [proposal.id, proposal]));
561
+ for (const id of withinCap) {
562
+ try {
563
+ if (opts.config) {
564
+ const proposal = byId.get(id);
565
+ if (!proposal)
566
+ throw new Error(`Proposal ${id} disappeared during drain preflight.`);
567
+ preflightProposalPromotion(opts.config, proposal, {
568
+ ...(opts.target ? { target: opts.target } : {}),
569
+ gateDecision: {
570
+ outcome: "auto-accepted",
571
+ reason: acceptGateReasons.get(id) ?? "policy-accept",
572
+ gate: gateLabel,
573
+ },
574
+ });
575
+ }
576
+ result.promoted.push(id);
577
+ deterministicPromoted += 1;
578
+ }
579
+ catch (err) {
580
+ warn(`[triage] preflight failed for ${id}: ${err instanceof Error ? err.message : String(err)}`);
581
+ }
519
582
  }
520
583
  }
521
- }
522
- // applyMode "queue": leave accept candidates pending (staged). No promotion.
523
- // Remaining accept budget for the judgment tier: maxAccepts minus what was
524
- // actually promoted deterministically. Bounds the TOTAL promotions, not just
525
- // the deterministic path. Moot in queue mode (it promotes nothing).
526
- const remainingAcceptBudget = Math.max(0, Math.max(0, opts.maxAccepts) - deterministicPromoted);
527
- // --- Judgment tier (Phase 3): adjudicate the deferred items ---
528
- // Only runs when a RunnerSpec is configured. The runner returns a verdict; the
529
- // ENGINE performs the resulting accept (respecting applyMode) / reject write.
530
- if (opts.judgment && result.deferred.length > 0) {
531
- const tier = await runJudgmentTier({
532
- stashDir: opts.stashDir,
533
- applyMode: opts.applyMode,
534
- dryRun: opts.dryRun,
535
- runner: opts.judgment,
536
- deferred: result.deferred,
537
- pending,
538
- promoteFn,
539
- rejectFn,
540
- seams: judgmentSeams,
541
- ...(opts.target ? { target: opts.target } : {}),
542
- ...(opts.config ? { config: opts.config } : {}),
543
- remainingAcceptBudget,
544
- gateLabel,
545
- });
546
- result.promoted.push(...tier.promoted);
547
- result.rejected.push(...tier.rejected);
548
- result.staged.push(...tier.staged);
549
- // Judgment-tier accepts dropped by the shared accept cap surface under
550
- // skippedByCap, same as deterministic cap drops.
551
- result.skippedByCap.push(...tier.skippedByCap);
552
- if (tier.skippedByCap.length > 0) {
553
- info(`[triage] accept ceiling reached in judgment tier: ${tier.skippedByCap.length} judged-accept items skipped by cap (maxAccepts=${opts.maxAccepts})`);
584
+ // applyMode "queue": leave accept candidates pending (staged). No promotion.
585
+ // Remaining accept budget for the judgment tier: maxAccepts minus what was
586
+ // actually promoted deterministically. Bounds the TOTAL promotions, not just
587
+ // the deterministic path. Moot in queue mode (it promotes nothing).
588
+ const remainingAcceptBudget = Math.max(0, Math.max(0, opts.maxAccepts) - deterministicPromoted);
589
+ // --- Judgment tier (Phase 3): adjudicate the deferred items ---
590
+ // Only runs when a RunnerSpec is configured. The runner returns a verdict; the
591
+ // ENGINE performs the resulting accept (respecting applyMode) / reject write.
592
+ if (opts.judgment && result.deferred.length > 0) {
593
+ if (!dispatchLease)
594
+ throw new TypeError("proposal judgment work requires an operation dispatch lease");
595
+ const tier = await runJudgmentTier({
596
+ stashDir: opts.stashDir,
597
+ applyMode: opts.applyMode,
598
+ dryRun: opts.dryRun,
599
+ runner: opts.judgment,
600
+ lease: dispatchLease,
601
+ deferred: result.deferred,
602
+ pending,
603
+ promoteFn,
604
+ rejectFn,
605
+ seams: judgmentSeams,
606
+ ...(opts.target ? { target: opts.target } : {}),
607
+ ...(opts.config ? { config: opts.config } : {}),
608
+ remainingAcceptBudget,
609
+ gateLabel,
610
+ });
611
+ result.promoted.push(...tier.promoted);
612
+ result.rejected.push(...tier.rejected);
613
+ result.staged.push(...tier.staged);
614
+ if (tier.notices.length > 0)
615
+ result.notices = tier.notices;
616
+ // Judgment-tier accepts dropped by the shared accept cap surface under
617
+ // skippedByCap, same as deterministic cap drops.
618
+ result.skippedByCap.push(...tier.skippedByCap);
619
+ if (tier.skippedByCap.length > 0) {
620
+ info(`[triage] accept ceiling reached in judgment tier: ${tier.skippedByCap.length} judged-accept items skipped by cap (maxAccepts=${opts.maxAccepts})`);
621
+ }
622
+ // Replace the deferred list with only the items the judgment tier could NOT
623
+ // resolve (verdict "defer", parse failure, or runner error). Staged
624
+ // queue-mode accepts are RESOLVED and tracked in result.staged instead.
625
+ result.deferred = tier.stillDeferred;
554
626
  }
555
- // Replace the deferred list with only the items the judgment tier could NOT
556
- // resolve (verdict "defer", parse failure, or runner error). Staged
557
- // queue-mode accepts are RESOLVED and tracked in result.staged instead.
558
- result.deferred = tier.stillDeferred;
559
- }
560
- else if (result.deferred.length > 0) {
561
- // #577: no judgment runner configured items deferred *because they need a
562
- // judge* (mid-band / possible-dup, no threshold reason) stay pending solely
563
- // for lack of one. Re-stamp those as `no-judge-configured` so the operator
564
- // sees a per-proposal reason instead of inferring it from the run-level
565
- // triage_deferred aggregate. Band-deferred items keep their specific reason
566
- // (e.g. `max-diff-lines`), which is more actionable than "no judge".
567
- for (const item of result.deferred) {
568
- if (needsJudge.has(item.id)) {
569
- stampGateDecision(opts, item.id, { outcome: "deferred", reason: "no-judge-configured", gate: gateLabel });
627
+ else if (result.deferred.length > 0) {
628
+ // #577: no judgment runner configured items deferred *because they need a
629
+ // judge* (mid-band / possible-dup, no threshold reason) stay pending solely
630
+ // for lack of one. Re-stamp those as `no-judge-configured` so the operator
631
+ // sees a per-proposal reason instead of inferring it from the run-level
632
+ // triage_deferred aggregate. Band-deferred items keep their specific reason
633
+ // (e.g. `max-diff-lines`), which is more actionable than "no judge".
634
+ for (const item of result.deferred) {
635
+ if (needsJudge.has(item.id)) {
636
+ stampGateDecision(opts, item.id, { outcome: "deferred", reason: "no-judge-configured", gate: gateLabel });
637
+ }
570
638
  }
571
639
  }
640
+ emitDrainEvents(opts, result);
641
+ return result;
642
+ }
643
+ finally {
644
+ if (dispatchLease)
645
+ disposeLoweredExecutionDispatchLease(dispatchLease);
572
646
  }
573
- emitDrainEvents(opts, result);
574
- return result;
575
647
  }
576
648
  /**
577
649
  * Persist a gate decision onto a proposal, honouring the dry-run contract
@@ -18,16 +18,24 @@ import { parsePositiveIntFlag } from "../../cli/parse-args.js";
18
18
  import { defineGroupCommand, defineJsonCommand, output } from "../../cli/shared.js";
19
19
  import { resolveStashDir } from "../../core/common.js";
20
20
  import { loadConfig } from "../../core/config/config.js";
21
- import { UsageError } from "../../core/errors.js";
22
- import { resolveTriageJudgmentRunner } from "../../integrations/agent/runner.js";
21
+ import { ConfigError, UsageError } from "../../core/errors.js";
23
22
  import { installLlmUsagePersistenceIfAbsent } from "../../llm/usage-persist.js";
24
23
  import { withLlmStage } from "../../llm/usage-telemetry.js";
24
+ import { resolveImproveExecution } from "../improve/execution.js";
25
25
  import { extractCommand } from "../improve/extract-cli.js";
26
26
  import { resolveImproveStrategy } from "../improve/improve-strategies.js";
27
27
  import { drainProposals } from "./drain.js";
28
28
  import { resolveDrainPolicy } from "./drain-policies.js";
29
29
  import { akmProposalAccept, akmProposalDiff, akmProposalList, akmProposalReject, akmProposalRevert, akmProposalShow, bulkAdjudicateProposals, } from "./proposal.js";
30
30
  import { proposeCommand } from "./propose-cli.js";
31
+ export function mergeProposalDrainNotices(resolutionNotices, dispatchNotices) {
32
+ const byKey = new Map();
33
+ for (const notice of resolutionNotices ?? [])
34
+ byKey.set(JSON.stringify(notice), notice);
35
+ for (const notice of dispatchNotices ?? [])
36
+ byKey.set(JSON.stringify(notice), notice);
37
+ return byKey.size > 0 ? Object.freeze([...byKey.values()]) : undefined;
38
+ }
31
39
  /**
32
40
  * `--source` was renamed to `--generator` on `proposal accept`/`proposal
33
41
  * reject` in 0.9 (WS3/S8 — "Removed in 0.9.0"). citty is non-strict, so the
@@ -352,7 +360,7 @@ const proposalDrainCommand = defineJsonCommand({
352
360
  },
353
361
  judgment: {
354
362
  type: "boolean",
355
- description: "Opt into the judgment tier (llm by default; agent/sdk per config) for deferred items. No-op with a logged triage_deferred summary when no runner is configured.",
363
+ description: "Explicitly enable the judgment tier for this drain (overrides judgment.enabled=false; agent/sdk per config). No-op with a logged triage_deferred summary when no runner is configured.",
356
364
  default: false,
357
365
  },
358
366
  strategy: {
@@ -408,14 +416,25 @@ const proposalDrainCommand = defineJsonCommand({
408
416
  })
409
417
  .map((proposal) => proposal.id));
410
418
  }
411
- // Phase 3: resolve the judgment runner when --judgment is set. Default
412
- // mode is llm; falls back to defaults.llm when the triage block sets
413
- // no explicit engine selection. null when
414
- // nothing is configured the engine leaves deferred items unresolved and
415
- // emits triage_deferred.
416
- const judgment = args.judgment === true
417
- ? resolveTriageJudgmentRunner(triageConfig?.judgment, cfg, triageConfig, selectedStrategy.config)
419
+ // Phase 3: --judgment is an invocation-level opt-in. It deliberately
420
+ // overrides judgment.enabled=false for this standalone drain while still
421
+ // reusing that block's execution overrides. Without the flag, configured
422
+ // judgment enablement is owned only by `akm improve`. A missing runner is
423
+ // a documented standalone no-op that leaves deferred items unresolved.
424
+ const judgmentResolution = args.judgment === true
425
+ ? resolveImproveExecution({
426
+ config: cfg,
427
+ profile: selectedStrategy.config,
428
+ process: triageConfig,
429
+ current: triageConfig?.judgment,
430
+ processName: "proposal-triage-judgment",
431
+ })
418
432
  : null;
433
+ const judgment = judgmentResolution?.runner ?? null;
434
+ const effectiveJudgmentLlm = triageConfig?.judgment?.llm ?? triageConfig?.llm ?? selectedStrategy.config.llm;
435
+ if (judgment && judgment.kind !== "llm" && effectiveJudgmentLlm) {
436
+ throw new ConfigError(`Triage judgment engine "${judgment.engine ?? "unknown"}" is an agent engine and cannot receive llm overrides.`, "INVALID_CONFIG_FILE");
437
+ }
419
438
  // #576: persist + attribute per-call LLM usage for the standalone drain
420
439
  // path. `IfAbsent` keeps an enclosing `akm improve` sink in charge when
421
440
  // drain runs as a sub-step; the disposer clears only a sink we installed.
@@ -437,6 +456,8 @@ const proposalDrainCommand = defineJsonCommand({
437
456
  finally {
438
457
  disposeDrainUsageSink();
439
458
  }
459
+ const dispatchNotices = result.notices;
460
+ const notices = mergeProposalDrainNotices(judgmentResolution?.notices, dispatchNotices);
440
461
  output("proposal-drain", {
441
462
  schemaVersion: 1,
442
463
  ok: true,
@@ -446,6 +467,7 @@ const proposalDrainCommand = defineJsonCommand({
446
467
  strategy: selectedStrategy.name,
447
468
  judgmentEngine: judgment?.engine ?? null,
448
469
  judgmentKind: judgment?.kind ?? null,
470
+ ...(notices ? { notices } : {}),
449
471
  promoted: result.promoted,
450
472
  rejected: result.rejected,
451
473
  deferred: result.deferred,