akm-cli 0.9.16 → 0.9.17-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (403) hide show
  1. package/CHANGELOG.md +2101 -0
  2. package/STABILITY.md +11 -10
  3. package/dist/akm +124 -193
  4. package/dist/akm-migrate +38 -19
  5. package/dist/assets/hints/cli-hints-full.md +6 -7
  6. package/dist/assets/improve-strategies/catchup.json +0 -3
  7. package/dist/assets/improve-strategies/consolidate.json +0 -1
  8. package/dist/assets/improve-strategies/default.json +1 -2
  9. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -2
  10. package/dist/assets/improve-strategies/quick.json +1 -2
  11. package/dist/assets/improve-strategies/reflect-distill.json +1 -2
  12. package/dist/assets/improve-strategies/thorough.json +0 -3
  13. package/dist/assets/prompts/consolidate-pair.md +20 -0
  14. package/dist/assets/prompts/consolidate-system.md +4 -11
  15. package/dist/assets/prompts/retrieval-relevance-judge.md +6 -0
  16. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +20 -20
  17. package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -2
  18. package/dist/assets/templates/html/health.html +3 -5
  19. package/dist/cli/retired-commands.js +1 -1
  20. package/dist/cli/shared.js +6 -2
  21. package/dist/cli/unknown-flags.js +24 -1
  22. package/dist/cli.js +68 -10
  23. package/dist/commands/agent/agent-dispatch.js +1 -1
  24. package/dist/commands/command/command-execution.js +24 -62
  25. package/dist/commands/feedback-cli.js +0 -1
  26. package/dist/commands/health/accept-rate.js +6 -0
  27. package/dist/commands/health/archive-usage.js +92 -0
  28. package/dist/commands/health/checks.js +83 -74
  29. package/dist/commands/health/config-skew.js +38 -0
  30. package/dist/commands/health/data-dir-usage.js +25 -13
  31. package/dist/commands/health/egress.js +54 -0
  32. package/dist/commands/health/html-report.js +1 -42
  33. package/dist/commands/health/improve-metrics.js +136 -591
  34. package/dist/commands/health/md-report.js +1 -6
  35. package/dist/commands/health/plugin-staleness.js +53 -3
  36. package/dist/commands/health/renderers.js +12 -4
  37. package/dist/commands/health/report-view-model.js +14 -120
  38. package/dist/commands/health/types-improve.js +4 -19
  39. package/dist/commands/health/windows.js +64 -74
  40. package/dist/commands/health.js +145 -143
  41. package/dist/commands/improve/consolidate/chunking.js +26 -117
  42. package/dist/commands/improve/consolidate/continuity-check.js +137 -0
  43. package/dist/commands/improve/consolidate/pair-pass.js +791 -0
  44. package/dist/commands/improve/consolidate/sanitize.js +54 -149
  45. package/dist/commands/improve/consolidate.js +589 -1127
  46. package/dist/commands/improve/content-hash.js +16 -24
  47. package/dist/commands/improve/distill/content-repair.js +18 -100
  48. package/dist/commands/improve/distill-guards.js +20 -81
  49. package/dist/commands/improve/distill-promotion-policy.js +23 -243
  50. package/dist/commands/improve/distill.js +608 -1041
  51. package/dist/commands/improve/eligibility.js +126 -390
  52. package/dist/commands/improve/execution.js +8 -10
  53. package/dist/commands/improve/extract-prompt.js +1 -2
  54. package/dist/commands/improve/extract.js +487 -1046
  55. package/dist/commands/improve/feedback-valence.js +0 -25
  56. package/dist/commands/improve/improve-cli.js +75 -169
  57. package/dist/commands/improve/improve-result-file.js +10 -66
  58. package/dist/commands/improve/improve-strategies.js +52 -4
  59. package/dist/commands/improve/improve-usage-report.js +18 -64
  60. package/dist/commands/improve/improve.js +480 -1074
  61. package/dist/commands/improve/ledger.js +119 -0
  62. package/dist/commands/improve/locks.js +2 -8
  63. package/dist/commands/improve/loop-stages.js +415 -1073
  64. package/dist/commands/improve/memory/derived-ref.js +12 -77
  65. package/dist/commands/improve/memory/memory-belief.js +16 -118
  66. package/dist/commands/improve/memory/memory-improve.js +266 -14
  67. package/dist/commands/improve/outcome-loop.js +28 -156
  68. package/dist/commands/improve/planner.js +5 -15
  69. package/dist/commands/improve/preparation.js +779 -2319
  70. package/dist/commands/improve/proactive-maintenance.js +34 -101
  71. package/dist/commands/improve/reflect-noise.js +104 -280
  72. package/dist/commands/improve/reflect.js +642 -1353
  73. package/dist/commands/improve/retrieval-gate.js +127 -0
  74. package/dist/commands/improve/retrieval-scope.js +92 -0
  75. package/dist/commands/improve/salience.js +41 -240
  76. package/dist/commands/improve/session-asset.js +19 -100
  77. package/dist/commands/improve/stage.js +322 -0
  78. package/dist/commands/lint/base-linter.js +37 -15
  79. package/dist/commands/proposal/drain.js +261 -578
  80. package/dist/commands/proposal/proposal-cli.js +19 -20
  81. package/dist/commands/proposal/proposal-types.js +31 -24
  82. package/dist/commands/proposal/proposal.js +38 -8
  83. package/dist/commands/proposal/propose.js +134 -160
  84. package/dist/commands/proposal/repository.js +1097 -1394
  85. package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
  86. package/dist/commands/proposal/validators/proposal-validators.js +1 -1
  87. package/dist/commands/proposal/validators/proposals.js +22 -89
  88. package/dist/commands/read/curate.js +105 -462
  89. package/dist/commands/read/knowledge.js +3 -2
  90. package/dist/commands/read/search-cli.js +16 -33
  91. package/dist/commands/read/search.js +17 -23
  92. package/dist/commands/read/show.js +57 -108
  93. package/dist/commands/sources/bundle-cli.js +25 -2
  94. package/dist/commands/sources/bundle-config-ops.js +4 -0
  95. package/dist/commands/sources/dangerous-env-audit.js +1 -2
  96. package/dist/commands/sources/info.js +127 -29
  97. package/dist/commands/sources/installed-stashes.js +197 -746
  98. package/dist/commands/sources/schema-repair.js +98 -129
  99. package/dist/commands/sources/source-add.js +62 -12
  100. package/dist/commands/sources/source-manage.js +9 -2
  101. package/dist/commands/sources/stash-cli.js +24 -4
  102. package/dist/commands/tasks/explain.js +10 -13
  103. package/dist/commands/tasks/tasks-cli.js +12 -13
  104. package/dist/commands/tasks/tasks.js +350 -936
  105. package/dist/commands/tasks/validate.js +26 -24
  106. package/dist/commands/workflow/plan.js +22 -29
  107. package/dist/commands/workflow-cli.js +4 -4
  108. package/dist/core/adapter/adapters/akm-adapter.js +2 -1
  109. package/dist/core/adapter/adapters/akm-lint.js +2 -3
  110. package/dist/core/adapter/adapters/akm-metadata.js +42 -12
  111. package/dist/core/adapter/adapters/akm-task-adapter.js +29 -8
  112. package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
  113. package/dist/core/adapter/execution-source.js +17 -29
  114. package/dist/core/asset/asset-placement.js +4 -13
  115. package/dist/core/asset/frontmatter.js +106 -1
  116. package/dist/core/asset/resolve-ref.js +1 -1
  117. package/dist/core/bundle-id.js +42 -5
  118. package/dist/core/bundle-rename.js +285 -0
  119. package/dist/core/config/config-io.js +1 -2
  120. package/dist/core/config/config-schema.js +9 -34
  121. package/dist/core/config/config-walker.js +1 -1
  122. package/dist/core/config/config.js +184 -111
  123. package/dist/core/config/engine-semantics.js +0 -2
  124. package/dist/core/config/legacy-source-shape-shim.js +38 -9
  125. package/dist/core/config/schema/embedding.js +20 -5
  126. package/dist/core/config/schema/engines.js +5 -0
  127. package/dist/core/config/schema/execution.js +1 -1
  128. package/dist/core/config/schema/experimental.js +1 -1
  129. package/dist/core/config/schema/improve-processes.js +54 -125
  130. package/dist/core/config/schema/improve.js +4 -42
  131. package/dist/core/config/schema/index-config.js +9 -48
  132. package/dist/core/config/schema/scheduler.js +12 -12
  133. package/dist/core/config/schema/search.js +6 -22
  134. package/dist/core/env-secret-ref.js +0 -1
  135. package/dist/core/errors.js +8 -9
  136. package/dist/core/file-change.js +13 -5
  137. package/dist/core/file-lock.js +76 -173
  138. package/dist/core/improve-result.js +35 -7
  139. package/dist/core/improve-types.js +0 -1
  140. package/dist/core/logs-db.js +2 -2
  141. package/dist/core/loopback.js +7 -12
  142. package/dist/core/non-task-input.js +20 -0
  143. package/dist/core/parse.js +13 -16
  144. package/dist/core/paths.js +0 -24
  145. package/dist/core/redaction.js +109 -2
  146. package/dist/core/run-lock.js +2 -5
  147. package/dist/core/spawn-env.js +1 -1
  148. package/dist/core/state/migrations.js +123 -61
  149. package/dist/core/state-db-scope.js +2 -4
  150. package/dist/core/state-db.js +126 -692
  151. package/dist/core/time.js +0 -20
  152. package/dist/core/type-presentation.js +1 -9
  153. package/dist/core/write-source.js +294 -1005
  154. package/dist/execution/input-contract.js +1 -1
  155. package/dist/execution/resolved-request.js +135 -689
  156. package/dist/execution/source.js +63 -257
  157. package/dist/execution/target-ref.js +1 -1
  158. package/dist/indexer/bundle-identity-guard.js +2 -2
  159. package/dist/indexer/db/llm-cache.js +2 -2
  160. package/dist/indexer/ensure-index.js +77 -73
  161. package/dist/indexer/index-rebuild-lock.js +3 -11
  162. package/dist/indexer/index-writer-lock.js +8 -17
  163. package/dist/indexer/index-written-assets.js +141 -154
  164. package/dist/indexer/indexer.js +400 -1124
  165. package/dist/indexer/links/declared-links.js +90 -0
  166. package/dist/indexer/materialize-embeddings.js +60 -397
  167. package/dist/indexer/passes/memory-inference.js +96 -90
  168. package/dist/indexer/passes/metadata.js +132 -219
  169. package/dist/indexer/read-preflight.js +0 -7
  170. package/dist/indexer/scan/doc-to-entry.js +2 -3
  171. package/dist/indexer/scan/drain-dir.js +1 -1
  172. package/dist/indexer/search/db-search.js +190 -590
  173. package/dist/indexer/search/fts-query.js +30 -41
  174. package/dist/indexer/search/ranking.js +28 -154
  175. package/dist/indexer/search/search-attribution.js +12 -32
  176. package/dist/indexer/search/search-fields.js +11 -15
  177. package/dist/indexer/search/search-hit-enrichers.js +54 -85
  178. package/dist/indexer/search/search-source.js +1 -4
  179. package/dist/indexer/usage/usage-events.js +36 -7
  180. package/dist/indexer/walk/walker.js +3 -4
  181. package/dist/integrations/agent/engine-fallback.js +23 -40
  182. package/dist/integrations/agent/engine-resolution.js +93 -183
  183. package/dist/integrations/agent/execution.js +507 -0
  184. package/dist/integrations/agent/model-map.js +28 -156
  185. package/dist/integrations/agent/request-lowering.js +66 -141
  186. package/dist/integrations/agent/runner-dispatch.js +143 -321
  187. package/dist/integrations/agent/runner.js +54 -14
  188. package/dist/integrations/lockfile.js +53 -101
  189. package/dist/llm/client.js +18 -6
  190. package/dist/llm/embedders/deterministic.js +2 -3
  191. package/dist/llm/embedders/profile.js +71 -0
  192. package/dist/llm/embedders/remote.js +11 -17
  193. package/dist/llm/feature-gate.js +0 -8
  194. package/dist/llm/index-passes.js +3 -5
  195. package/dist/llm/memory-infer.js +1 -2
  196. package/dist/llm/structured-call.js +5 -24
  197. package/dist/output/generic-render.js +23 -11
  198. package/dist/output/html-render.js +13 -10
  199. package/dist/output/render-registry.js +3 -32
  200. package/dist/output/shapes/helpers.js +25 -38
  201. package/dist/output/shapes/passthrough.js +1 -9
  202. package/dist/{indexer/graph/graph-types.js → output/text/bundle-rename.js} +4 -1
  203. package/dist/output/text/command-format.js +69 -31
  204. package/dist/output/text/helpers.js +1 -1
  205. package/dist/output/text/migrate.js +5 -14
  206. package/dist/output/text/proposal-format.js +48 -3
  207. package/dist/output/text/show-format.js +13 -17
  208. package/dist/output/text/workflow-format.js +0 -32
  209. package/dist/output/text.js +2 -0
  210. package/dist/registry/factory.js +4 -19
  211. package/dist/registry/network.js +66 -220
  212. package/dist/registry/providers/index.js +0 -2
  213. package/dist/registry/providers/skills-sh.js +3 -14
  214. package/dist/registry/providers/static-index.js +24 -26
  215. package/dist/registry/resolve.js +55 -131
  216. package/dist/scripts/akm-migrate-node.js +42948 -92369
  217. package/dist/scripts/akm-migrate.js +42935 -92354
  218. package/dist/setup/registry-stash-loader.js +4 -13
  219. package/dist/setup/semantic-assets.js +3 -44
  220. package/dist/setup/setup.js +1 -1
  221. package/dist/setup/steps/connection.js +5 -6
  222. package/dist/setup/steps/platforms.js +2 -2
  223. package/dist/setup/steps/tasks.js +25 -15
  224. package/dist/sources/provider-factory.js +17 -18
  225. package/dist/sources/providers/filesystem.js +2 -3
  226. package/dist/sources/providers/git-install.js +7 -1
  227. package/dist/sources/providers/git-provider.js +0 -3
  228. package/dist/sources/providers/git-stash.js +83 -21
  229. package/dist/sources/providers/npm.js +2 -4
  230. package/dist/sources/providers/provider-utils.js +5 -10
  231. package/dist/sources/providers/website.js +0 -2
  232. package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
  233. package/dist/sources/website-url.js +2 -2
  234. package/dist/storage/database.js +9 -35
  235. package/dist/storage/repositories/improve-ledger-repository.js +209 -0
  236. package/dist/storage/repositories/index-connection.js +39 -72
  237. package/dist/storage/repositories/index-entries-repository.js +131 -129
  238. package/dist/storage/repositories/index-entry-mapper.js +1 -2
  239. package/dist/storage/repositories/index-entry-schema.js +101 -268
  240. package/dist/storage/repositories/index-fts-repository.js +86 -256
  241. package/dist/storage/repositories/index-links-repository.js +143 -0
  242. package/dist/storage/repositories/index-llm-cache-repository.js +7 -9
  243. package/dist/storage/repositories/index-meta-repository.js +6 -4
  244. package/dist/storage/repositories/index-schema.js +257 -325
  245. package/dist/storage/repositories/index-utility-repository.js +8 -29
  246. package/dist/storage/repositories/index-vec-repository.js +133 -414
  247. package/dist/storage/repositories/outcome-repository.js +2 -1
  248. package/dist/storage/repositories/proposals-repository.js +104 -1
  249. package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
  250. package/dist/storage/repositories/salience-repository.js +1 -19
  251. package/dist/storage/repositories/task-history-repository.js +26 -4
  252. package/dist/storage/repositories/workflow-runs-repository.js +53 -244
  253. package/dist/storage/sqlite-migrations.js +136 -0
  254. package/dist/storage/sqlite-pragmas.js +11 -9
  255. package/dist/storage/sqlite-transaction.js +170 -0
  256. package/dist/storage/state-db-integrity.js +130 -0
  257. package/dist/tasks/activation-config.js +134 -62
  258. package/dist/tasks/backends/cron.js +191 -302
  259. package/dist/tasks/backends/exec-utils.js +2 -5
  260. package/dist/tasks/backends/launchd.js +141 -748
  261. package/dist/tasks/backends/schtasks.js +119 -623
  262. package/dist/tasks/prepare/prepare-support.js +5 -15
  263. package/dist/tasks/prepare/prepare.js +0 -2
  264. package/dist/tasks/resolve-akm-bin.js +20 -79
  265. package/dist/tasks/run/attempt-lifecycle.js +0 -1
  266. package/dist/tasks/run/load-task.js +1 -1
  267. package/dist/tasks/scheduler-binding.js +20 -238
  268. package/dist/tasks/scheduler-invocation.js +136 -244
  269. package/dist/tasks/scheduler-lock.js +53 -0
  270. package/dist/tasks/scheduler-sync.js +368 -679
  271. package/dist/tasks/source/parse-task-source.js +55 -9
  272. package/dist/tasks/source/task-source-v3-frozen.js +3 -4
  273. package/dist/tasks/source/task-to-v4.js +464 -88
  274. package/dist/workflows/authoring/authoring.js +3 -12
  275. package/dist/workflows/compile.js +211 -0
  276. package/dist/workflows/concurrency-policy.js +13 -74
  277. package/dist/workflows/exec/child-invocation.js +3 -17
  278. package/dist/workflows/exec/child-workflow.js +32 -141
  279. package/dist/workflows/exec/dispatch-redaction.js +13 -53
  280. package/dist/workflows/exec/environment.js +98 -0
  281. package/dist/workflows/exec/exec-unit.js +33 -140
  282. package/dist/workflows/exec/frozen-judge.js +7 -59
  283. package/dist/workflows/exec/native-executor.js +82 -341
  284. package/dist/workflows/exec/param-secrets.js +29 -47
  285. package/dist/workflows/exec/run-workflow.js +154 -387
  286. package/dist/workflows/exec/scheduler.js +9 -36
  287. package/dist/workflows/exec/step-work.js +127 -430
  288. package/dist/workflows/exec/unit-dispatch.js +11 -63
  289. package/dist/workflows/exec/unit-writer.js +8 -52
  290. package/dist/workflows/exec/worktree.js +39 -273
  291. package/dist/workflows/freeze/child-output-references.js +4 -15
  292. package/dist/workflows/freeze/environment.js +99 -92
  293. package/dist/workflows/freeze/freeze.js +172 -0
  294. package/dist/workflows/freeze/step-values.js +19 -21
  295. package/dist/workflows/freeze/targets/child-workflow.js +23 -92
  296. package/dist/workflows/freeze/targets/command.js +10 -33
  297. package/dist/workflows/freeze/targets/script.js +5 -12
  298. package/dist/workflows/freeze/targets/shell.js +3 -6
  299. package/dist/workflows/freeze/targets/task.js +25 -80
  300. package/dist/workflows/freeze/task-bindings.js +20 -67
  301. package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
  302. package/dist/workflows/ir/params.js +6 -51
  303. package/dist/workflows/ir/plan-hash.js +2 -34
  304. package/dist/workflows/parser.js +140 -43
  305. package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
  306. package/dist/workflows/renderer.js +36 -69
  307. package/dist/workflows/resource-limits.js +12 -120
  308. package/dist/workflows/runtime/agent-identity.js +8 -40
  309. package/dist/workflows/runtime/run-outputs.js +3 -6
  310. package/dist/workflows/runtime/run-plan.js +316 -0
  311. package/dist/workflows/runtime/runs.js +48 -200
  312. package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
  313. package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
  314. package/dist/workflows/validate-summary.js +2 -7
  315. package/docs/integration/bundling-akm.md +49 -42
  316. package/docs/migration/README.md +1 -0
  317. package/docs/migration/release-notes/0.9.17.md +43 -0
  318. package/docs/migration/v0.9.1-to-v0.9.2.md +23 -7
  319. package/docs/reference/cli.md +232 -135
  320. package/docs/reference/configuration.md +71 -57
  321. package/docs/reference/data-and-telemetry.md +20 -21
  322. package/docs/reference/tasks.md +105 -39
  323. package/docs/reference/workflow-schema.md +14 -18
  324. package/docs/reference/workflows.md +6 -9
  325. package/package.json +1 -1
  326. package/schemas/akm-config.json +115 -738
  327. package/schemas/akm-workflow.json +1 -0
  328. package/dist/assets/improve-strategies/graph-refresh.json +0 -15
  329. package/dist/assets/prompts/contradiction-judge.md +0 -33
  330. package/dist/assets/prompts/graph-extract-system.md +0 -1
  331. package/dist/assets/prompts/graph-extract-user-prompt.md +0 -35
  332. package/dist/assets/prompts/metadata-enhance-system.md +0 -1
  333. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +0 -4
  334. package/dist/commands/health/advisories.js +0 -150
  335. package/dist/commands/health/metrics.js +0 -329
  336. package/dist/commands/health/surfaces.js +0 -102
  337. package/dist/commands/improve/anti-collapse.js +0 -83
  338. package/dist/commands/improve/collapse-detector.js +0 -432
  339. package/dist/commands/improve/consolidate/eligibility.js +0 -48
  340. package/dist/commands/improve/consolidate/merge.js +0 -149
  341. package/dist/commands/improve/distill/promote-memory.js +0 -291
  342. package/dist/commands/improve/distill/quality-gate.js +0 -337
  343. package/dist/commands/improve/eval-cases.js +0 -52
  344. package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
  345. package/dist/commands/improve/proposal-envelope.js +0 -31
  346. package/dist/commands/improve/run-context.js +0 -123
  347. package/dist/commands/improve/shared.js +0 -31
  348. package/dist/commands/improve/source-identity.js +0 -28
  349. package/dist/commands/improve/triage.js +0 -96
  350. package/dist/commands/proposal/drain-policies.js +0 -151
  351. package/dist/commands/sources/update-transaction.js +0 -220
  352. package/dist/core/action-contributors.js +0 -28
  353. package/dist/core/config/config-version-shim.js +0 -101
  354. package/dist/core/fs-txn.js +0 -405
  355. package/dist/core/lexical-score.js +0 -25
  356. package/dist/core/maintenance-barrier.js +0 -167
  357. package/dist/execution/executable-identity.js +0 -105
  358. package/dist/execution/guarded-source.js +0 -427
  359. package/dist/indexer/db/graph-db.js +0 -444
  360. package/dist/indexer/graph/graph-boost.js +0 -427
  361. package/dist/indexer/graph/graph-dedup.js +0 -95
  362. package/dist/indexer/graph/graph-extraction.js +0 -1108
  363. package/dist/indexer/search/name-match.js +0 -35
  364. package/dist/indexer/search/ranking-contributors.js +0 -515
  365. package/dist/indexer/search/ranking-types.js +0 -4
  366. package/dist/indexer/walk/project-context.js +0 -192
  367. package/dist/integrations/agent/execution-cascade.js +0 -566
  368. package/dist/integrations/agent/execution-definitions.js +0 -202
  369. package/dist/integrations/agent/execution-lowering.js +0 -841
  370. package/dist/integrations/agent/execution-preparation.js +0 -98
  371. package/dist/integrations/agent/inline-execution.js +0 -74
  372. package/dist/llm/graph-extract.js +0 -728
  373. package/dist/llm/metadata-enhance.js +0 -96
  374. package/dist/registry/create-provider-registry.js +0 -29
  375. package/dist/registry/pinned-request-helper.js +0 -247
  376. package/dist/registry/pinned-transport.js +0 -717
  377. package/dist/sources/providers/index.js +0 -14
  378. package/dist/storage/engines/sqlite-migrations.js +0 -271
  379. package/dist/storage/repositories/canaries-repository.js +0 -107
  380. package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
  381. package/dist/storage/repositories/registry-cache.js +0 -113
  382. package/dist/tasks/scheduler-sync-preview.js +0 -52
  383. package/dist/tasks/source/task-to-v3.js +0 -507
  384. package/dist/workflows/freeze/resolve-steps.js +0 -86
  385. package/dist/workflows/freeze/source-freeze.js +0 -64
  386. package/dist/workflows/ir/compile.js +0 -321
  387. package/dist/workflows/ir/environment-v4.js +0 -330
  388. package/dist/workflows/ir/freeze-v4.js +0 -153
  389. package/dist/workflows/ir/schema-v4.js +0 -745
  390. package/dist/workflows/ir/schema.js +0 -354
  391. package/dist/workflows/program/schema.js +0 -77
  392. package/dist/workflows/runtime/checkin.js +0 -57
  393. package/dist/workflows/runtime/plan-classifier.js +0 -196
  394. package/dist/workflows/runtime/unit-checkin.js +0 -45
  395. package/dist/workflows/runtime/unit-phases.js +0 -20
  396. package/dist/workflows/schema.js +0 -4
  397. package/dist/workflows/source-ir/compile.js +0 -200
  398. package/dist/workflows/source-ir/program.js +0 -50
  399. package/dist/workflows/source-ir/result.js +0 -26
  400. package/dist/workflows/source-ir/schema.js +0 -786
  401. package/dist/workflows/source-ir/triggers.js +0 -79
  402. package/dist/workflows/source-ir/uses.js +0 -40
  403. package/dist/workflows/validator.js +0 -60
@@ -2,39 +2,20 @@
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
  /**
5
- * Deterministic proposal-drain engine (Proposal-Queue Triage, Phase 1).
6
- *
7
- * Drains the *standing pending backlog* of proposals using a deterministic,
8
- * no-LLM policy keyed on generator (proposal `source`) and diff size. This is
9
- * the engine behind `akm proposal drain` and (later) the `triage` improve
10
- * pre-pass.
11
- *
12
- * Design:
13
- * - Reuses `listProposals` (no source filter — generator filtering is
14
- * in-memory) and the `akmProposalAccept` / `akmProposalReject` wrappers from
15
- * `proposal.ts` so the standard `promoted` / `rejected` events are emitted.
16
- * Deterministic by design; only the configured drain policy decides.
17
- * - Backlog-only: `excludeIds` removes this-run's fresh proposals so triage
18
- * never re-adjudicates a current run's output (decision #2).
19
- * - Hard guardrails enforced in code: a `maxAccepts` ceiling checked *before*
20
- * the promote loop (remainder → `skippedByCap`); `maxDiffLines` defers large
21
- * accepts; `applyMode: "queue"` (the safe default) never promotes (stage
22
- * only); `rejectEmpty` rejects empty / near-empty diffs.
23
- * - The judgment tier (Phase 3) adjudicates the deferred items: when a
24
- * `judgment` RunnerSpec is supplied the engine pre-fetches context (the live
25
- * asset + sibling pending proposals for the same ref) into a prompt,
26
- * dispatches it through the shared resolved/lowered execution boundary,
27
- * and performs the resulting accept / reject *itself* (the runner only
28
- * judges).
29
- * Items the runner cannot resolve — and any deferred items when no runner is
30
- * configured — surface a `triage_deferred` event so "enabled, no agent"
31
- * never silently looks like full success.
32
- *
33
- * The promote / reject functions and the runner dispatch are injectable
34
- * (mirrors reflect's dual test seams) so tests can run the full engine without
35
- * touching the filesystem or spawning a process.
5
+ * The proposal drain behind `akm proposal drain` and improve's triage pre-pass.
6
+ * One rule decides the pending backlog:
7
+ * - an empty diff is rejected;
8
+ * - a proposal whose quality judge passed on this exact content (a `staged`
9
+ * gate decision carrying its content hash) is accepted, unless its target
10
+ * changed since mint — then it is auto-rejected as `stale-target`, never
11
+ * overwritten;
12
+ * - everything else needs a judge: the judgment tier decides it when a runner
13
+ * is configured, and whatever stays undecided is left for review
14
+ * (`review_needed` in the improve ledger).
15
+ * `maxAccepts` caps promotions across both tiers; `applyMode: "queue"` never
16
+ * promotes; `excludeIds` keeps this run's fresh proposals out; a proposal the
17
+ * distill quality gate routed to a human is left for that human.
36
18
  */
37
- import { createHash } from "node:crypto";
38
19
  import fs from "node:fs";
39
20
  import path from "node:path";
40
21
  import { assetPathForName, stashDirFor } from "../../core/asset/asset-placement.js";
@@ -44,165 +25,111 @@ import { ConfigError } from "../../core/errors.js";
44
25
  import { appendEvent } from "../../core/events.js";
45
26
  import { escapeJsonStringControls, stripCodeFences, stripThinkBlocks } from "../../core/parse.js";
46
27
  import { info, warn } from "../../core/warn.js";
47
- import { acquireLoweredExecutionDispatchLease, dispatchLoweredExecutionRequest, disposeLoweredExecutionDispatchLease, lowerResolvedExecutionRequestWithRunner, } from "../../integrations/agent/execution-lowering.js";
48
- import { prepareInlineExecutionWithRunner } from "../../integrations/agent/inline-execution.js";
28
+ import { buildExecution, resolveExecution } from "../../integrations/agent/execution.js";
29
+ import { assertRunnerCredentials, runExecution, } from "../../integrations/agent/runner-dispatch.js";
30
+ import { errMessage, noticeSet } from "../improve/stage.js";
49
31
  import { akmProposalAccept, akmProposalReject } from "./proposal.js";
50
- import { listProposals, listProposalsReadOnly, preflightProposalPromotion, proposalContent, recordGateDecision, } from "./repository.js";
51
- // ---------------------------------------------------------------------------
52
- // Content helpers
53
- // ---------------------------------------------------------------------------
54
- /** Number of non-empty body lines (frontmatter excluded). */
55
- export function contentBodyLineCount(content) {
56
- // Reuse the canonical frontmatter parser so CRLF / BOM are handled
57
- // consistently with the rest of the stash (parseFrontmatter returns the body
58
- // in `content`).
59
- return parseFrontmatter(content)
60
- .content.split("\n")
61
- .filter((line) => line.trim().length > 0).length;
62
- }
63
- /** Total line count of the proposed content (matches the bulk-accept measure). */
64
- export function contentLineCount(content) {
65
- return content.split("\n").length;
66
- }
67
- /** An empty / near-empty diff has no meaningful body content. */
32
+ import { isRetireProposal, STALE_TARGET_GATE_REASON } from "./proposal-types.js";
33
+ import { listProposals, listProposalsReadOnly, preflightProposalPromotion, proposalContent, proposalContentHash, readFreshProposalTarget, recordGateDecision, } from "./repository.js";
34
+ /** The gate label on every decision the drain records. */
35
+ const DRAIN_GATE = "triage";
36
+ /** An empty diff: no non-blank body line outside the frontmatter. */
68
37
  export function isEmptyDiff(proposal) {
69
38
  const content = proposalContent(proposal);
70
39
  if (content.trim().length === 0)
71
40
  return true;
72
- return contentBodyLineCount(content) === 0;
73
- }
74
- /**
75
- * Decide a deterministic verdict for a single backlog proposal under `policy`.
76
- * Returns `null` when no rule applies (the proposal is left pending untouched).
77
- */
78
- export function classifyProposal(proposal, policy, maxDiffLines) {
79
- const content = proposalContent(proposal);
80
- // Empty / near-empty diffs reject first (the reject-empty floor).
81
- if (policy.rejectEmpty && isEmptyDiff(proposal)) {
82
- return { verdict: "reject", reason: "empty diff", gate: { reason: "empty-diff" } };
83
- }
84
- const rule = policy.accept.find((r) => {
85
- if (r.generator !== proposal.source)
86
- return false;
87
- if (r.requireType !== undefined) {
88
- const fm = parseFrontmatter(proposalContent(proposal)).data;
89
- if (typeof fm.type !== "string" || fm.type !== r.requireType)
90
- return false;
91
- }
92
- return true;
93
- });
94
- if (rule) {
95
- const lines = contentLineCount(content);
96
- const body = contentBodyLineCount(content);
97
- // Per-rule and global diff bounds defer large accepts (no silent rewrites).
98
- const effectiveMax = Math.min(rule.maxDiffLines ?? Number.POSITIVE_INFINITY, maxDiffLines ?? Number.POSITIVE_INFINITY);
99
- if (lines > effectiveMax) {
100
- return {
101
- verdict: "defer",
102
- reason: "mid-band",
103
- gate: { reason: "max-diff-lines", measured: lines, thresholds: { maxDiffLines: effectiveMax } },
104
- };
105
- }
106
- if (rule.minContentLines !== undefined && body < rule.minContentLines) {
107
- // Too little content to confidently auto-accept — leave for judgment.
108
- return {
109
- verdict: "defer",
110
- reason: "mid-band",
111
- gate: { reason: "min-content-lines", measured: body, thresholds: { minContentLines: rule.minContentLines } },
112
- };
113
- }
114
- return { verdict: "accept", gate: { reason: "policy-accept" } };
115
- }
116
- if (policy.defer.includes(proposal.source)) {
117
- const reason = deferReasonForSource(proposal.source);
118
- return { verdict: "defer", reason, gate: { reason } };
119
- }
120
- // No matching rule — leave pending, untouched.
121
- return null;
122
- }
123
- function deferReasonForSource(source) {
124
- return source === "distill" ? "possible-dup" : "mid-band";
41
+ return !parseFrontmatter(content)
42
+ .content.split("\n")
43
+ .some((line) => line.trim().length > 0);
125
44
  }
126
- /**
127
- * Map a thrown error's message to one of `DrainResult.failed`'s stable reason
128
- * codes, falling back to `fallback` for anything not specifically recognized.
129
- * Recognizes the write-time guards a proposal can trip during promotion
130
- * (see repository.ts's `promoteProposalWithLease` / `preflightProposalPromotion`).
131
- */
132
45
  function categorizeDrainFailure(message, fallback) {
133
46
  if (/target (?:changed after|was created after) proposal/.test(message))
134
- return "stale-target";
47
+ return STALE_TARGET_GATE_REASON;
135
48
  if (/failed validation:/.test(message))
136
49
  return "validation";
137
50
  return fallback;
138
51
  }
139
- function pushDrainFailure(result, id, err, fallbackReason) {
140
- const message = err instanceof Error ? err.message : String(err);
141
- result.failed.push({ id, reason: categorizeDrainFailure(message, fallbackReason), detail: message });
142
- return message;
143
- }
144
52
  /**
145
- * Mirror repository.ts's `promoteProposalWithLease` stale-target guard so a
146
- * dry-run preflight predicts the same refusal a real promote would hit,
147
- * without writing anything. `assetPath` is the path `preflightProposalPromotion`
148
- * already resolved for this proposal.
53
+ * The one accept path both tiers share. A dry run exercises the same stamped
54
+ * candidate, lint and freshness boundary a promotion would (tests that pass no
55
+ * config keep the classification-only seam). A stale target is not a merit
56
+ * rejection, so instead of failing identically every run it is auto-rejected
57
+ * once; the ledger records `failed`, keeping the ref re-proposable.
149
58
  */
150
- function assertProposalTargetFresh(proposal, assetPath) {
151
- const backup = fs.existsSync(assetPath) ? fs.readFileSync(assetPath) : undefined;
152
- const currentHash = backup ? createHash("sha256").update(backup).digest("hex") : undefined;
153
- if (proposal.beforeHash !== undefined && (!backup || currentHash !== proposal.beforeHash)) {
154
- throw new Error(`Proposal target changed after proposal ${proposal.id} was created; refusing to overwrite newer content.`);
59
+ async function acceptProposal(opts, proposal, id, reason, promoteFn, rejectFn) {
60
+ const gateDecision = { outcome: "auto-accepted", reason, gate: DRAIN_GATE };
61
+ try {
62
+ if (!opts.dryRun) {
63
+ await promoteFn({
64
+ stashDir: opts.stashDir,
65
+ id,
66
+ ...(opts.target ? { target: opts.target } : {}),
67
+ ...(opts.config ? { config: opts.config } : {}),
68
+ gateDecision,
69
+ });
70
+ }
71
+ else if (opts.config) {
72
+ if (!proposal)
73
+ throw new Error(`Proposal ${id} disappeared during drain preflight.`);
74
+ const preflight = preflightProposalPromotion(opts.config, proposal, {
75
+ ...(opts.target ? { target: opts.target } : {}),
76
+ gateDecision,
77
+ });
78
+ readFreshProposalTarget(proposal, preflight.assetPath, preflight.stampedContent);
79
+ }
80
+ return "promoted";
155
81
  }
156
- if (proposal.beforeHash === undefined &&
157
- backup !== undefined &&
158
- proposal.changes.some((change) => change.op === "create")) {
159
- throw new Error(`Proposal target was created after proposal ${proposal.id} was created; refusing to overwrite newer content.`);
82
+ catch (err) {
83
+ const message = errMessage(err);
84
+ if (categorizeDrainFailure(message, "") !== STALE_TARGET_GATE_REASON)
85
+ return { message };
86
+ if (opts.dryRun)
87
+ return "rejected";
88
+ try {
89
+ await rejectFn({
90
+ stashDir: opts.stashDir,
91
+ id,
92
+ reason: `stale-target: ${message}`,
93
+ gateDecision: { outcome: "auto-rejected", reason: STALE_TARGET_GATE_REASON, gate: DRAIN_GATE },
94
+ });
95
+ return "rejected";
96
+ }
97
+ catch (rejectErr) {
98
+ warn(`[triage] stale-target auto-reject failed for ${id}: ${errMessage(rejectErr)}`);
99
+ return { message };
100
+ }
160
101
  }
161
102
  }
162
- // ---------------------------------------------------------------------------
163
- // Judgment tier (Phase 3)
164
- // ---------------------------------------------------------------------------
165
- /** Read the live on-disk content of a proposal's target asset, if it exists. */
166
- function readLiveAssetContent(stashDir, ref) {
103
+ /** Reject one proposal (nothing in a dry run); the error message on failure. */
104
+ async function rejectProposal(opts, id, reason, gateReason, rejectFn) {
105
+ if (opts.dryRun)
106
+ return undefined;
167
107
  try {
168
- const parsed = parseRefInput(ref);
169
- const typeDir = stashDirFor(parsed.type);
170
- if (!typeDir)
171
- return undefined;
172
- const typeRoot = path.join(stashDir, typeDir);
173
- const assetPath = assetPathForName(parsed.type, typeRoot, parsed.name);
174
- if (!fs.existsSync(assetPath))
175
- return undefined;
176
- return fs.readFileSync(assetPath, "utf8");
177
- }
178
- catch {
108
+ await rejectFn({
109
+ stashDir: opts.stashDir,
110
+ id,
111
+ reason,
112
+ gateDecision: { outcome: "auto-rejected", reason: gateReason, gate: DRAIN_GATE },
113
+ });
179
114
  return undefined;
180
115
  }
116
+ catch (err) {
117
+ return errMessage(err);
118
+ }
181
119
  }
182
- /**
183
- * Pre-fetch the context the judgment runner needs to adjudicate one deferred
184
- * proposal: the proposed content, the live asset it would overwrite, and the
185
- * sibling pending proposals for the same ref (so a dedup verdict can compare).
186
- */
187
- function prefetchJudgmentContext(stashDir, proposal, pending) {
188
- const liveAsset = readLiveAssetContent(stashDir, proposal.ref);
189
- const siblings = pending.filter((p) => p.ref === proposal.ref && p.id !== proposal.id);
190
- return { liveAsset, siblings };
191
- }
192
- /** Build the judgment prompt with the proposed content + pre-fetched context. */
120
+ /** The judgment prompt: the proposal, the live asset it would overwrite, and same-ref siblings. */
193
121
  export function buildJudgmentPrompt(proposal, reason, ctx) {
194
- const proposed = proposalContent(proposal);
195
122
  const sections = [
196
- "You are adjudicating a pending knowledge-base proposal that the deterministic",
197
- "triage pass could not resolve. Decide whether to accept, reject, or defer it.",
123
+ "You are adjudicating a pending knowledge-base proposal no quality judge has",
124
+ "passed yet. Decide whether to accept, reject, or defer it.",
198
125
  "",
199
126
  `Asset ref: ${proposal.ref}`,
200
127
  `Generator (source): ${proposal.source}`,
201
- `Deferred because: ${reason}`,
128
+ `Left for judgment because: ${reason === "needs-judgment" ? "no quality judge has passed this content yet" : reason}`,
202
129
  "",
203
130
  "## Proposed content",
204
131
  "```",
205
- proposed,
132
+ proposalContent(proposal),
206
133
  "```",
207
134
  ];
208
135
  if (ctx.liveAsset !== undefined) {
@@ -220,15 +147,12 @@ export function buildJudgmentPrompt(proposal, reason, ctx) {
220
147
  sections.push("", "## Your task", 'Return ONLY a JSON object: {"decision": "accept" | "reject" | "defer", "reason": "<short reason>"}.', "- accept: the proposed content is a correct, valuable update worth committing.", "- reject: the proposal is wrong, a duplicate, or contradicts the live asset.", "- defer: you cannot decide from the provided context (leave it pending).", "Output the JSON object and nothing else.");
221
148
  return sections.join("\n");
222
149
  }
223
- /** Parse a {@link JudgmentVerdict} from raw runner output. Lenient. */
150
+ /** A verdict from raw runner output (the first JSON object), or null. */
224
151
  export function parseJudgmentVerdict(raw) {
225
152
  const cleaned = escapeJsonStringControls(stripCodeFences(stripThinkBlocks(raw))).trim();
226
- if (!cleaned)
227
- return null;
228
- // Find the first balanced-looking JSON object in the output.
229
153
  const start = cleaned.indexOf("{");
230
154
  const end = cleaned.lastIndexOf("}");
231
- if (start === -1 || end === -1 || end <= start)
155
+ if (start === -1 || end <= start)
232
156
  return null;
233
157
  let obj;
234
158
  try {
@@ -237,479 +161,244 @@ export function parseJudgmentVerdict(raw) {
237
161
  catch {
238
162
  return null;
239
163
  }
240
- if (typeof obj !== "object" || obj === null)
241
- return null;
242
- const decision = obj.decision;
243
- const reason = obj.reason;
164
+ const { decision, reason } = (obj ?? {});
244
165
  if (decision !== "accept" && decision !== "reject" && decision !== "defer")
245
166
  return null;
246
167
  return { decision, reason: typeof reason === "string" ? reason : "" };
247
168
  }
248
- async function dispatchJudgment(runner, prompt, seams, lease) {
249
- const prepared = prepareInlineExecutionWithRunner({
250
- content: prompt,
251
- runner,
252
- invocationKind: "direct",
253
- });
254
- const lowered = lowerResolvedExecutionRequestWithRunner(prepared.request, prepared.runner);
255
- const chat = seams.chat;
256
- const llmRunner = lowered.runner.kind === "llm" ? lowered.runner : undefined;
257
- const dispatchOptions = {
258
- lease,
259
- ...(seams.runAgentFn ? { runAgent: seams.runAgentFn } : {}),
260
- ...(seams.runSdkFn ? { runSdk: seams.runSdkFn } : {}),
261
- ...(chat && llmRunner
262
- ? {
263
- chat: async (connection, messages) => chat({ ...llmRunner, connection }, messages),
264
- }
265
- : {}),
266
- };
267
- let result;
169
+ /** Lower the judgment prompt through the frozen runner and dispatch it. */
170
+ async function dispatchJudgment(runner, prompt, seams) {
171
+ let notices = [];
268
172
  try {
269
- result = await dispatchLoweredExecutionRequest(lowered, dispatchOptions);
173
+ const prepared = resolveExecution({ content: prompt, runner });
174
+ const lowered = buildExecution(prepared.request, prepared.runner);
175
+ notices = lowered.notices;
176
+ const chat = seams.chat;
177
+ const llmRunner = lowered.runner.kind === "llm" ? lowered.runner : undefined;
178
+ const result = await runExecution(lowered, {
179
+ ...(seams.runAgentFn ? { runAgent: seams.runAgentFn } : {}),
180
+ ...(seams.runSdkFn ? { runSdk: seams.runSdkFn } : {}),
181
+ ...(chat && llmRunner
182
+ ? { chat: async (connection, messages) => chat({ ...llmRunner, connection }, messages) }
183
+ : {}),
184
+ });
185
+ if (!result.ok)
186
+ return { verdict: null, notices, error: result.error ?? result.reason ?? "unknown error" };
187
+ return { verdict: parseJudgmentVerdict(result.stdout), notices };
270
188
  }
271
189
  catch (error) {
272
190
  if (error instanceof ConfigError)
273
191
  throw error;
274
- return {
275
- verdict: null,
276
- notices: lowered.notices,
277
- error: error instanceof Error ? error.message : String(error),
278
- };
192
+ return { verdict: null, notices, error: errMessage(error) };
279
193
  }
280
- if (!result.ok) {
281
- return {
282
- verdict: null,
283
- notices: lowered.notices,
284
- error: result.error ?? result.reason ?? "unknown error",
285
- };
286
- }
287
- return { verdict: parseJudgmentVerdict(result.stdout), notices: lowered.notices };
288
- }
289
- /** Validate symbolic judgment credentials without contacting a provider. */
290
- async function preflightJudgmentRunner(runner) {
291
- const prepared = prepareInlineExecutionWithRunner({
292
- content: "Validate the selected proposal judgment runner before mutation.",
293
- runner,
294
- invocationKind: "direct",
295
- });
296
- const lowered = lowerResolvedExecutionRequestWithRunner(prepared.request, prepared.runner);
297
- return acquireLoweredExecutionDispatchLease(lowered);
298
- }
299
- function judgedContentHash(proposal) {
300
- return createHash("sha256").update(proposalContent(proposal), "utf8").digest("hex");
301
194
  }
302
195
  /**
303
- * Run the judgment tier over the deferred items. The runner only *judges*; the
304
- * engine performs the resulting accept (respecting `applyMode`) / reject write.
305
- * Returns the ids the engine promoted / rejected, the ids staged (judge said
306
- * "accept" but queue mode did not promote), the ids dropped by the accept cap,
307
- * and the items still unresolved (verdict "defer", parse failure, or a runner
308
- * error).
196
+ * The judgment tier: the runner only judges; the drain performs the accept
197
+ * (under `applyMode` and the remaining accept budget) or the reject. A defer, an
198
+ * unparseable verdict or a runner error leaves the item undecided.
309
199
  */
310
- async function runJudgmentTier(input) {
311
- const byId = new Map(input.pending.map((p) => [p.id, p]));
312
- const promoted = [];
313
- const rejected = [];
314
- const staged = [];
315
- const skippedByCap = [];
200
+ async function runJudgmentTier(opts, result, pending, acceptBudget, promoteFn, rejectFn, seams) {
201
+ const byId = new Map(pending.map((p) => [p.id, p]));
202
+ const notices = noticeSet();
316
203
  const stillDeferred = [];
317
- const noticesByKey = new Map();
318
- // Remaining accept budget shared with the deterministic promote loop.
319
- let acceptBudget = Math.max(0, input.remainingAcceptBudget);
320
- for (const item of input.deferred) {
204
+ const cappedBefore = result.skippedByCap.length;
205
+ for (const item of result.deferred) {
321
206
  const proposal = byId.get(item.id);
322
207
  if (!proposal) {
323
208
  stillDeferred.push(item);
324
209
  continue;
325
210
  }
326
- const ctx = prefetchJudgmentContext(input.stashDir, proposal, input.pending);
327
- const prompt = buildJudgmentPrompt(proposal, item.reason, ctx);
328
- let dispatch;
329
- try {
330
- dispatch = await dispatchJudgment(input.runner, prompt, input.seams, input.lease);
331
- }
332
- catch (err) {
333
- if (err instanceof ConfigError)
334
- throw err;
335
- warn(`[triage] judgment dispatch failed for ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
336
- stillDeferred.push(item);
337
- continue;
338
- }
339
- for (const notice of dispatch.notices) {
340
- const key = JSON.stringify(notice);
341
- if (!noticesByKey.has(key))
342
- noticesByKey.set(key, notice);
343
- }
344
- if (dispatch.error) {
211
+ const prompt = buildJudgmentPrompt(proposal, item.reason, {
212
+ liveAsset: readLiveAssetContent(opts.stashDir, proposal.ref),
213
+ siblings: pending.filter((p) => p.ref === proposal.ref && p.id !== proposal.id),
214
+ });
215
+ const dispatch = await dispatchJudgment(opts.judgment, prompt, seams);
216
+ notices.add(dispatch.notices);
217
+ if (dispatch.error)
345
218
  warn(`[triage] judgment dispatch failed for ${item.id}: ${dispatch.error}`);
346
- stillDeferred.push(item);
347
- continue;
348
- }
349
- const verdict = dispatch.verdict;
219
+ const verdict = dispatch.error ? null : dispatch.verdict;
350
220
  if (!verdict || verdict.decision === "defer") {
351
221
  stillDeferred.push(item);
352
222
  continue;
353
223
  }
354
224
  if (verdict.decision === "reject") {
355
- if (input.dryRun) {
356
- rejected.push(item.id);
225
+ const failure = await rejectProposal(opts, item.id, verdict.reason || "judgment: reject", "judgment-reject", rejectFn);
226
+ if (failure === undefined) {
227
+ result.rejected.push(item.id);
228
+ }
229
+ else {
230
+ warn(`[triage] judgment reject failed for ${item.id}: ${failure}`);
231
+ stillDeferred.push(item);
232
+ }
233
+ continue;
234
+ }
235
+ // Queue mode never writes the asset: the verdict is staged for a later promote run.
236
+ if (opts.applyMode !== "promote") {
237
+ if (opts.dryRun) {
238
+ result.staged.push(item.id);
357
239
  continue;
358
240
  }
359
241
  try {
360
- await input.rejectFn({
361
- stashDir: input.stashDir,
362
- id: item.id,
363
- reason: verdict.reason || "judgment: reject",
364
- gateDecision: { outcome: "auto-rejected", reason: "judgment-reject", gate: input.gateLabel },
242
+ recordGateDecision(opts.stashDir, item.id, {
243
+ outcome: "staged",
244
+ reason: "judgment-accept",
245
+ contentHash: proposalContentHash(proposal),
246
+ gate: DRAIN_GATE,
365
247
  });
366
- rejected.push(item.id);
248
+ result.staged.push(item.id);
367
249
  }
368
250
  catch (err) {
369
- warn(`[triage] judgment reject failed for ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
251
+ warn(`[triage] failed to stage judgment for ${item.id}: ${errMessage(err)}`);
370
252
  stillDeferred.push(item);
371
253
  }
372
254
  continue;
373
255
  }
374
- // decision === "accept" — gated on applyMode, exactly like the
375
- // deterministic accept path (queue mode never writes).
376
- if (input.applyMode !== "promote") {
377
- // Staged: a queue-mode run never promotes, so the item stays pending but
378
- // is RESOLVED (the runner judged it). Track separately so it is NOT
379
- // reported as "left unresolved" and a follow-up promote run picks it up.
380
- staged.push(item.id);
381
- if (!input.dryRun) {
382
- try {
383
- recordGateDecision(input.stashDir, item.id, {
384
- outcome: "staged",
385
- reason: "judgment-accept",
386
- contentHash: judgedContentHash(proposal),
387
- gate: input.gateLabel,
388
- });
389
- }
390
- catch (err) {
391
- warn(`[triage] failed to stage judgment for ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
392
- staged.pop();
393
- stillDeferred.push(item);
394
- }
395
- }
396
- continue;
397
- }
398
- // Accept cap: once the shared budget is exhausted, route further accepts to
399
- // skippedByCap instead of promoting (keeps total promotions ≤ maxAccepts).
400
256
  if (acceptBudget <= 0) {
401
- skippedByCap.push(item.id);
257
+ result.skippedByCap.push(item.id);
402
258
  continue;
403
259
  }
404
- if (input.dryRun) {
405
- try {
406
- if (input.config) {
407
- preflightProposalPromotion(input.config, proposal, {
408
- ...(input.target ? { target: input.target } : {}),
409
- gateDecision: { outcome: "auto-accepted", reason: "judgment-accept", gate: input.gateLabel },
410
- });
411
- }
412
- }
413
- catch (err) {
414
- warn(`[triage] judgment preflight failed for ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
415
- stillDeferred.push(item);
416
- continue;
417
- }
418
- promoted.push(item.id);
260
+ const outcome = await acceptProposal(opts, proposal, item.id, "judgment-accept", promoteFn, rejectFn);
261
+ if (outcome === "promoted") {
262
+ result.promoted.push(item.id);
419
263
  acceptBudget -= 1;
420
- continue;
421
264
  }
422
- try {
423
- await input.promoteFn({
424
- stashDir: input.stashDir,
425
- id: item.id,
426
- ...(input.target ? { target: input.target } : {}),
427
- ...(input.config ? { config: input.config } : {}),
428
- gateDecision: { outcome: "auto-accepted", reason: "judgment-accept", gate: input.gateLabel },
429
- });
430
- promoted.push(item.id);
431
- acceptBudget -= 1;
265
+ else if (outcome === "rejected") {
266
+ result.rejected.push(item.id);
432
267
  }
433
- catch (err) {
434
- warn(`[triage] judgment promote failed for ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
268
+ else {
269
+ warn(`[triage] judgment ${opts.dryRun ? "preflight" : "promote"} failed for ${item.id}: ${outcome.message}`);
435
270
  stillDeferred.push(item);
436
271
  }
437
272
  }
438
- return {
439
- promoted,
440
- rejected,
441
- staged,
442
- skippedByCap,
443
- stillDeferred,
444
- notices: Object.freeze([...noticesByKey.values()]),
445
- };
273
+ const capped = result.skippedByCap.length - cappedBefore;
274
+ if (capped > 0) {
275
+ info(`[triage] accept ceiling reached in judgment tier: ${capped} judged-accept items skipped by cap (maxAccepts=${opts.maxAccepts})`);
276
+ }
277
+ if (notices.list().length > 0)
278
+ result.notices = notices.list();
279
+ result.deferred = stillDeferred;
280
+ }
281
+ /** The live asset a proposal would overwrite, if any. */
282
+ function readLiveAssetContent(stashDir, ref) {
283
+ try {
284
+ const parsed = parseRefInput(ref);
285
+ const typeDir = stashDirFor(parsed.type);
286
+ if (!typeDir)
287
+ return undefined;
288
+ const assetPath = assetPathForName(parsed.type, path.join(stashDir, typeDir), parsed.name);
289
+ return fs.existsSync(assetPath) ? fs.readFileSync(assetPath, "utf8") : undefined;
290
+ }
291
+ catch {
292
+ return undefined;
293
+ }
446
294
  }
447
- /** Classify the queue without mutating proposal, event, or promotion state. */
448
- function classifyPendingProposals(opts) {
295
+ /**
296
+ * Drain the pending backlog. `promoteFn` / `rejectFn` / `judgmentSeams` are
297
+ * test seams.
298
+ */
299
+ export async function drainProposals(opts, promoteFn = akmProposalAccept, rejectFn = akmProposalReject, judgmentSeams = {}) {
449
300
  const exclude = opts.excludeIds ?? new Set();
450
- // A configured judgment runner must be credential-validated before any live
451
- // state connection or migration. Its classification pass therefore reads an
452
- // isolated SQLite snapshot; deterministic-only drains retain the historical
453
- // live/migrating queue read.
454
- const pending = (opts.judgment ? listProposalsReadOnly : listProposals)(opts.stashDir, {
455
- status: "pending",
456
- }).filter((proposal) => !exclude.has(proposal.id));
457
- const acceptIds = [];
458
- const acceptGateReasons = new Map();
459
- const rejectTargets = [];
460
- const deferred = [];
461
- const deferredGateDecisions = [];
462
- const gateLabel = `triage:${opts.policy.name}`;
463
- const needsJudge = new Set();
301
+ // A judgment runner's credentials are validated before any live state
302
+ // connection, so its classification reads an isolated snapshot.
303
+ const pending = (opts.judgment ? listProposalsReadOnly : listProposals)(opts.stashDir, { status: "pending" }).filter((proposal) => !exclude.has(proposal.id));
304
+ const result = { promoted: [], rejected: [], deferred: [], skippedByCap: [], staged: [], failed: [] };
305
+ const accepts = [];
306
+ const empties = [];
464
307
  for (const proposal of pending) {
465
- // An authoritative rejection from another gate stays pending and is never
466
- // silently overwritten by this triage policy.
467
- if (proposal.gateDecision?.outcome === "auto-rejected" && !proposal.gateDecision.gate?.startsWith("triage:")) {
308
+ // A consolidate pair-pass `retire` proposal is never auto-decided here,
309
+ // whatever `applyMode` says (alpha.9 brief §A "Review"; spec §25.6):
310
+ // untouched, still pending, waiting for a direct `akm proposal accept`.
311
+ // Checked before isEmptyDiff, which reads proposalContent() and has
312
+ // nothing meaningful to read on a delete-primary change anyway.
313
+ if (isRetireProposal(proposal))
468
314
  continue;
469
- }
470
- if (proposal.gateDecision?.outcome === "staged" &&
471
- proposal.gateDecision.gate === gateLabel &&
472
- proposal.gateDecision.contentHash === judgedContentHash(proposal)) {
473
- acceptIds.push(proposal.id);
474
- acceptGateReasons.set(proposal.id, "judgment-accept");
315
+ const decision = proposal.gateDecision;
316
+ // Another gate's rejection stands; a human-review deferral from the distill
317
+ // quality gate is left for that human.
318
+ if (decision?.outcome === "auto-rejected" && !decision.gate?.startsWith(DRAIN_GATE))
475
319
  continue;
476
- }
477
- const decision = classifyProposal(proposal, opts.policy, opts.maxDiffLines);
478
- if (decision === null)
320
+ if (decision?.outcome === "deferred" && decision.gate === "quality-gate")
479
321
  continue;
480
- if (decision.verdict === "defer") {
481
- deferredGateDecisions.push({
482
- id: proposal.id,
483
- decision: {
484
- outcome: "deferred",
485
- reason: decision.gate.reason,
486
- ...(decision.gate.measured !== undefined ? { measured: decision.gate.measured } : {}),
487
- ...(decision.gate.thresholds ? { thresholds: decision.gate.thresholds } : {}),
488
- gate: gateLabel,
489
- },
490
- });
491
- if (!decision.gate.thresholds)
492
- needsJudge.add(proposal.id);
493
- }
494
- if (decision.verdict === "accept") {
495
- acceptIds.push(proposal.id);
496
- acceptGateReasons.set(proposal.id, "policy-accept");
322
+ if (isEmptyDiff(proposal)) {
323
+ empties.push(proposal.id);
497
324
  }
498
- else if (decision.verdict === "reject") {
499
- rejectTargets.push({ id: proposal.id, reason: decision.reason });
325
+ else if (decision?.outcome === "staged" && decision.contentHash === proposalContentHash(proposal)) {
326
+ accepts.push({ id: proposal.id, reason: decision.gate === "quality-gate" ? "judge-passed" : "judgment-accept" });
500
327
  }
501
328
  else {
502
- deferred.push({ id: proposal.id, reason: decision.reason });
329
+ result.deferred.push({ id: proposal.id, reason: "needs-judgment" });
503
330
  }
504
331
  }
505
- return {
506
- pending,
507
- acceptIds,
508
- acceptGateReasons,
509
- rejectTargets,
510
- deferred,
511
- deferredGateDecisions,
512
- gateLabel,
513
- needsJudge,
514
- };
515
- }
516
- /**
517
- * Drain the standing pending backlog under a deterministic policy.
518
- *
519
- * @param opts Drain options (policy, applyMode, ceilings, dry-run).
520
- * @param promoteFn Injectable override for `akmProposalAccept` (test seam).
521
- * @param rejectFn Injectable override for `akmProposalReject` (test seam).
522
- */
523
- export async function drainProposals(opts, promoteFn = akmProposalAccept, rejectFn = akmProposalReject, judgmentSeams = {}) {
524
- const classification = classifyPendingProposals(opts);
525
- const { pending, acceptIds, acceptGateReasons, rejectTargets, deferredGateDecisions, gateLabel, needsJudge } = classification;
526
- const result = {
527
- promoted: [],
528
- rejected: [],
529
- deferred: classification.deferred,
530
- skippedByCap: [],
531
- staged: [],
532
- failed: [],
533
- };
534
- // A configured judgment runner makes every deferred item dispatch-eligible.
535
- // Validate its symbolic credentials before applying any deterministic gate,
536
- // reject, promote, or event mutation. Provider/runtime failures remain the
537
- // judgment tier's fail-soft responsibility after this configuration fence.
538
- const dispatchLease = opts.judgment && result.deferred.length > 0 ? await preflightJudgmentRunner(opts.judgment) : undefined;
539
- try {
540
- for (const { id, decision } of deferredGateDecisions)
541
- stampGateDecision(opts, id, decision);
542
- // --- Reject empties (independent of the accept ceiling / applyMode) ---
543
- for (const target of rejectTargets) {
544
- if (opts.dryRun) {
545
- result.rejected.push(target.id);
546
- continue;
547
- }
548
- try {
549
- await rejectFn({
550
- stashDir: opts.stashDir,
551
- id: target.id,
552
- reason: target.reason,
553
- gateDecision: { outcome: "auto-rejected", reason: "empty-diff", gate: gateLabel },
554
- });
555
- result.rejected.push(target.id);
556
- }
557
- catch (err) {
558
- const message = pushDrainFailure(result, target.id, err, "reject-error");
559
- warn(`[triage] reject failed for ${target.id}: ${message}`);
560
- }
332
+ if (opts.judgment && result.deferred.length > 0) {
333
+ // Symbolic credentials are checked before any gate, reject or promote.
334
+ const prepared = resolveExecution({
335
+ content: "Validate the selected proposal judgment runner before mutation.",
336
+ runner: opts.judgment,
337
+ });
338
+ assertRunnerCredentials(buildExecution(prepared.request, prepared.runner).runner);
339
+ }
340
+ for (const id of empties) {
341
+ const failure = await rejectProposal(opts, id, "empty diff", "empty-diff", rejectFn);
342
+ if (failure === undefined) {
343
+ result.rejected.push(id);
561
344
  }
562
- // --- Accept ceiling: enforced BEFORE the promote loop ---
563
- const withinCap = acceptIds.slice(0, Math.max(0, opts.maxAccepts));
564
- result.skippedByCap = acceptIds.slice(Math.max(0, opts.maxAccepts));
565
- if (result.skippedByCap.length > 0) {
566
- info(`[triage] accept ceiling reached: ${withinCap.length} promoted, ${result.skippedByCap.length} skipped by cap (maxAccepts=${opts.maxAccepts})`);
345
+ else {
346
+ result.failed.push({ id, reason: categorizeDrainFailure(failure, "reject-error"), detail: failure });
347
+ warn(`[triage] reject failed for ${id}: ${failure}`);
567
348
  }
568
- // --- Promotion gate: applyMode "queue" never promotes (stage only) ---
569
- // Count deterministic promotions so the judgment tier shares the same accept
570
- // budget (deterministic + judgment promotions ≤ maxAccepts).
571
- let deterministicPromoted = 0;
572
- if (opts.applyMode === "promote" && !opts.dryRun) {
349
+ }
350
+ const cap = Math.max(0, opts.maxAccepts);
351
+ const withinCap = accepts.slice(0, cap);
352
+ result.skippedByCap = accepts.slice(cap).map((a) => a.id);
353
+ if (result.skippedByCap.length > 0) {
354
+ info(`[triage] accept ceiling reached: ${withinCap.length} promoted, ${result.skippedByCap.length} skipped by cap (maxAccepts=${opts.maxAccepts})`);
355
+ }
356
+ let promotedHere = 0;
357
+ if (opts.applyMode === "promote") {
358
+ if (!opts.dryRun)
573
359
  info(`[triage] auto-promote active: ${withinCap.length} accepts allowed this run`);
574
- for (const id of withinCap) {
575
- try {
576
- await promoteFn({
577
- stashDir: opts.stashDir,
578
- id,
579
- ...(opts.target ? { target: opts.target } : {}),
580
- ...(opts.config ? { config: opts.config } : {}),
581
- gateDecision: {
582
- outcome: "auto-accepted",
583
- reason: acceptGateReasons.get(id) ?? "policy-accept",
584
- gate: gateLabel,
585
- },
586
- });
587
- result.promoted.push(id);
588
- deterministicPromoted += 1;
589
- }
590
- catch (err) {
591
- const message = pushDrainFailure(result, id, err, "promote-error");
592
- warn(`[triage] promote failed for ${id}: ${message}`);
593
- }
360
+ const byId = new Map(pending.map((proposal) => [proposal.id, proposal]));
361
+ for (const { id, reason } of withinCap) {
362
+ const outcome = await acceptProposal(opts, byId.get(id), id, reason, promoteFn, rejectFn);
363
+ if (outcome === "promoted") {
364
+ result.promoted.push(id);
365
+ promotedHere += 1;
594
366
  }
595
- }
596
- else if (opts.applyMode === "promote" && opts.dryRun) {
597
- // Exercise the same stamped candidate, lint, and stale-target boundary as
598
- // real promotion so a dry-run's predicted promotions match what a real
599
- // run would do. Tests that omit config retain the classification-only seam.
600
- const byId = new Map(pending.map((proposal) => [proposal.id, proposal]));
601
- for (const id of withinCap) {
602
- try {
603
- if (opts.config) {
604
- const proposal = byId.get(id);
605
- if (!proposal)
606
- throw new Error(`Proposal ${id} disappeared during drain preflight.`);
607
- const preflight = preflightProposalPromotion(opts.config, proposal, {
608
- ...(opts.target ? { target: opts.target } : {}),
609
- gateDecision: {
610
- outcome: "auto-accepted",
611
- reason: acceptGateReasons.get(id) ?? "policy-accept",
612
- gate: gateLabel,
613
- },
614
- });
615
- assertProposalTargetFresh(proposal, preflight.assetPath);
616
- }
617
- result.promoted.push(id);
618
- deterministicPromoted += 1;
619
- }
620
- catch (err) {
621
- const message = pushDrainFailure(result, id, err, "promote-error");
622
- warn(`[triage] preflight failed for ${id}: ${message}`);
623
- }
367
+ else if (outcome === "rejected") {
368
+ result.rejected.push(id);
624
369
  }
625
- }
626
- // applyMode "queue": leave accept candidates pending (staged). No promotion.
627
- // Remaining accept budget for the judgment tier: maxAccepts minus what was
628
- // actually promoted deterministically. Bounds the TOTAL promotions, not just
629
- // the deterministic path. Moot in queue mode (it promotes nothing).
630
- const remainingAcceptBudget = Math.max(0, Math.max(0, opts.maxAccepts) - deterministicPromoted);
631
- // --- Judgment tier (Phase 3): adjudicate the deferred items ---
632
- // Only runs when a RunnerSpec is configured. The runner returns a verdict; the
633
- // ENGINE performs the resulting accept (respecting applyMode) / reject write.
634
- if (opts.judgment && result.deferred.length > 0) {
635
- if (!dispatchLease)
636
- throw new TypeError("proposal judgment work requires an operation dispatch lease");
637
- const tier = await runJudgmentTier({
638
- stashDir: opts.stashDir,
639
- applyMode: opts.applyMode,
640
- dryRun: opts.dryRun,
641
- runner: opts.judgment,
642
- lease: dispatchLease,
643
- deferred: result.deferred,
644
- pending,
645
- promoteFn,
646
- rejectFn,
647
- seams: judgmentSeams,
648
- ...(opts.target ? { target: opts.target } : {}),
649
- ...(opts.config ? { config: opts.config } : {}),
650
- remainingAcceptBudget,
651
- gateLabel,
652
- });
653
- result.promoted.push(...tier.promoted);
654
- result.rejected.push(...tier.rejected);
655
- result.staged.push(...tier.staged);
656
- if (tier.notices.length > 0)
657
- result.notices = tier.notices;
658
- // Judgment-tier accepts dropped by the shared accept cap surface under
659
- // skippedByCap, same as deterministic cap drops.
660
- result.skippedByCap.push(...tier.skippedByCap);
661
- if (tier.skippedByCap.length > 0) {
662
- info(`[triage] accept ceiling reached in judgment tier: ${tier.skippedByCap.length} judged-accept items skipped by cap (maxAccepts=${opts.maxAccepts})`);
663
- }
664
- // Replace the deferred list with only the items the judgment tier could NOT
665
- // resolve (verdict "defer", parse failure, or runner error). Staged
666
- // queue-mode accepts are RESOLVED and tracked in result.staged instead.
667
- result.deferred = tier.stillDeferred;
668
- }
669
- else if (result.deferred.length > 0) {
670
- // #577: no judgment runner configured — items deferred *because they need a
671
- // judge* (mid-band / possible-dup, no threshold reason) stay pending solely
672
- // for lack of one. Re-stamp those as `no-judge-configured` so the operator
673
- // sees a per-proposal reason instead of inferring it from the run-level
674
- // triage_deferred aggregate. Band-deferred items keep their specific reason
675
- // (e.g. `max-diff-lines`), which is more actionable than "no judge".
676
- for (const item of result.deferred) {
677
- if (needsJudge.has(item.id)) {
678
- stampGateDecision(opts, item.id, { outcome: "deferred", reason: "no-judge-configured", gate: gateLabel });
679
- }
370
+ else {
371
+ result.failed.push({
372
+ id,
373
+ reason: categorizeDrainFailure(outcome.message, "promote-error"),
374
+ detail: outcome.message,
375
+ });
376
+ warn(`[triage] ${opts.dryRun ? "preflight" : "promote"} failed for ${id}: ${outcome.message}`);
680
377
  }
681
378
  }
682
- emitDrainEvents(opts, result);
683
- return result;
684
379
  }
685
- finally {
686
- if (dispatchLease)
687
- disposeLoweredExecutionDispatchLease(dispatchLease);
380
+ if (opts.judgment && result.deferred.length > 0) {
381
+ await runJudgmentTier({ ...opts, judgment: opts.judgment }, result, pending, cap - promotedHere, promoteFn, rejectFn, judgmentSeams);
688
382
  }
689
- }
690
- /**
691
- * Persist a gate decision onto a proposal, honouring the dry-run contract
692
- * (a dry run performs zero writes, so it records nothing) and never letting a
693
- * persistence failure abort the drain (#577). Best-effort by design.
694
- */
695
- function stampGateDecision(opts, id, decision) {
696
- if (opts.dryRun)
697
- return;
698
- try {
699
- recordGateDecision(opts.stashDir, id, decision);
700
- }
701
- catch (err) {
702
- warn(`[triage] failed to record gate decision for ${id}: ${err instanceof Error ? err.message : String(err)}`);
383
+ // #577: whatever stays undecided is left for review (`review_needed` in the ledger).
384
+ if (!opts.dryRun) {
385
+ const reviewReason = opts.judgment ? "judgment-deferred" : "no-judge-configured";
386
+ for (const item of result.deferred) {
387
+ try {
388
+ recordGateDecision(opts.stashDir, item.id, { outcome: "deferred", reason: reviewReason, gate: DRAIN_GATE });
389
+ }
390
+ catch (err) {
391
+ warn(`[triage] failed to record gate decision for ${item.id}: ${errMessage(err)}`);
392
+ }
393
+ }
703
394
  }
395
+ emitDrainEvents(opts, result);
396
+ return result;
704
397
  }
705
- // ---------------------------------------------------------------------------
706
- // Events
707
- // ---------------------------------------------------------------------------
708
398
  function emitDrainEvents(opts, result) {
709
399
  const deferredByReason = {};
710
- for (const d of result.deferred) {
400
+ for (const d of result.deferred)
711
401
  deferredByReason[d.reason] = (deferredByReason[d.reason] ?? 0) + 1;
712
- }
713
402
  appendEvent({
714
403
  eventType: "triage_drained",
715
404
  metadata: {
@@ -718,17 +407,11 @@ function emitDrainEvents(opts, result) {
718
407
  deferredByReason,
719
408
  skippedByCap: result.skippedByCap.length,
720
409
  ...(result.staged.length > 0 ? { staged: result.staged.length } : {}),
721
- policy: opts.policy.name,
722
410
  applyMode: opts.applyMode,
723
411
  ...(opts.dryRun ? { dryRun: true } : {}),
724
412
  },
725
413
  }, opts.eventsCtx ?? {});
726
- // Surface any items the judge could NOT resolve after the (optional) judgment
727
- // tier so a backlog of deferred items never silently looks like full success.
728
- // This fires when no runner is configured OR the judgment tier ran but could
729
- // not resolve every item (verdict "defer", parse failure, or a runner error).
730
- // Queue-mode staged accepts are RESOLVED (the judge decided) and live in
731
- // result.staged, so they are deliberately excluded from this "unresolved" count.
414
+ // Undecided items must never look like full success. Staged accepts were decided.
732
415
  if (result.deferred.length > 0) {
733
416
  appendEvent({
734
417
  eventType: "triage_deferred",