akm-cli 0.9.0-beta.9 → 0.9.0-rc.1

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 (381) hide show
  1. package/CHANGELOG.md +715 -0
  2. package/README.md +12 -4
  3. package/dist/akm +38 -0
  4. package/dist/akm-migrate-storage +38 -0
  5. package/dist/assets/help/help-improve.md +9 -6
  6. package/dist/assets/hints/cli-hints-full.md +6 -5
  7. package/dist/assets/profiles/default.json +9 -4
  8. package/dist/assets/profiles/frequent.json +1 -1
  9. package/dist/assets/profiles/memory-focus.json +1 -1
  10. package/dist/assets/profiles/proactive-maintenance.json +25 -0
  11. package/dist/assets/profiles/quick.json +1 -1
  12. package/dist/assets/profiles/recombine-only.json +21 -0
  13. package/dist/assets/profiles/reflect-distill.json +30 -0
  14. package/dist/assets/profiles/synthesize.json +15 -0
  15. package/dist/assets/profiles/thorough.json +1 -1
  16. package/dist/assets/prompts/consolidate-system.md +23 -0
  17. package/dist/assets/prompts/contradiction-judge.md +33 -0
  18. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  19. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  20. package/dist/assets/prompts/extract-session.md +11 -3
  21. package/dist/assets/prompts/graph-extract-system.md +1 -0
  22. package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
  23. package/dist/assets/prompts/memory-infer-system.md +1 -0
  24. package/dist/assets/prompts/memory-infer-user.md +5 -0
  25. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  26. package/dist/assets/prompts/procedural-system.md +44 -0
  27. package/dist/assets/prompts/recombine-system.md +40 -0
  28. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  29. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  30. package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
  39. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
  40. package/dist/assets/templates/html/health.html +281 -111
  41. package/dist/assets/wiki/ingest-workflow-template.md +45 -16
  42. package/dist/assets/wiki/schema-template.md +4 -4
  43. package/dist/cli/clack.js +56 -0
  44. package/dist/cli/config-migrate.js +7 -1
  45. package/dist/cli/confirm.js +1 -1
  46. package/dist/cli/parse-args.js +46 -1
  47. package/dist/cli/shared.js +28 -0
  48. package/dist/cli.js +25 -21
  49. package/dist/commands/agent/agent-dispatch.js +3 -2
  50. package/dist/commands/agent/agent-support.js +0 -7
  51. package/dist/commands/agent/contribute-cli.js +26 -7
  52. package/dist/commands/config-cli.js +26 -13
  53. package/dist/commands/env/child-env.js +47 -0
  54. package/dist/commands/env/env-binding.js +95 -0
  55. package/dist/commands/env/env-cli.js +228 -292
  56. package/dist/commands/env/env.js +14 -67
  57. package/dist/commands/env/secret-cli.js +140 -138
  58. package/dist/commands/feedback-cli.js +156 -155
  59. package/dist/commands/graph/graph-cli.js +5 -13
  60. package/dist/commands/graph/graph.js +3 -3
  61. package/dist/commands/health/advisories.js +151 -0
  62. package/dist/commands/health/checks.js +103 -16
  63. package/dist/commands/health/html-report.js +447 -81
  64. package/dist/commands/health/improve-metrics.js +771 -0
  65. package/dist/commands/health/llm-usage.js +65 -0
  66. package/dist/commands/health/md-report.js +103 -0
  67. package/dist/commands/health/metrics.js +278 -0
  68. package/dist/commands/health/stash-exposure.js +46 -0
  69. package/dist/commands/health/surfaces.js +216 -0
  70. package/dist/commands/health/task-runs.js +135 -0
  71. package/dist/commands/health/types.js +26 -0
  72. package/dist/commands/health/windows.js +195 -0
  73. package/dist/commands/health.js +91 -1091
  74. package/dist/commands/improve/anti-collapse.js +170 -0
  75. package/dist/commands/improve/calibration.js +161 -0
  76. package/dist/commands/improve/collapse-detector.js +421 -0
  77. package/dist/commands/improve/consolidate/chunking.js +141 -0
  78. package/dist/commands/improve/consolidate/eligibility.js +64 -0
  79. package/dist/commands/improve/consolidate/merge.js +145 -0
  80. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  81. package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
  82. package/dist/commands/improve/consolidate.js +1295 -1277
  83. package/dist/commands/improve/dedup.js +482 -0
  84. package/dist/commands/improve/distill/content-repair.js +202 -0
  85. package/dist/commands/improve/distill/promote-memory.js +229 -0
  86. package/dist/commands/improve/distill/quality-gate.js +236 -0
  87. package/dist/commands/improve/distill-guards.js +127 -0
  88. package/dist/commands/improve/distill-promotion-policy.js +826 -167
  89. package/dist/commands/improve/distill.js +228 -605
  90. package/dist/commands/improve/eligibility.js +434 -0
  91. package/dist/commands/improve/encoding-salience.js +205 -0
  92. package/dist/commands/improve/extract-cli.js +179 -59
  93. package/dist/commands/improve/extract-prompt.js +54 -3
  94. package/dist/commands/improve/extract-watch.js +140 -0
  95. package/dist/commands/improve/extract.js +409 -43
  96. package/dist/commands/improve/feedback-valence.js +54 -0
  97. package/dist/commands/improve/hot-probation.js +45 -0
  98. package/dist/commands/improve/improve-auto-accept.js +157 -10
  99. package/dist/commands/improve/improve-cli.js +115 -73
  100. package/dist/commands/improve/improve-profiles.js +28 -8
  101. package/dist/commands/improve/improve-result-file.js +15 -25
  102. package/dist/commands/improve/improve-session.js +58 -0
  103. package/dist/commands/improve/improve.js +485 -2764
  104. package/dist/commands/improve/locks.js +154 -0
  105. package/dist/commands/improve/loop-stages.js +1100 -0
  106. package/dist/commands/improve/memory/memory-belief.js +14 -15
  107. package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
  108. package/dist/commands/improve/memory/memory-improve.js +27 -27
  109. package/dist/commands/improve/outcome-loop.js +270 -0
  110. package/dist/commands/improve/preparation.js +2002 -0
  111. package/dist/commands/improve/proactive-maintenance.js +37 -35
  112. package/dist/commands/improve/procedural.js +398 -0
  113. package/dist/commands/improve/recombine.js +818 -0
  114. package/dist/commands/improve/reflect-noise.js +0 -0
  115. package/dist/commands/improve/reflect.js +206 -45
  116. package/dist/commands/improve/salience.js +455 -0
  117. package/dist/commands/improve/schema-similarity-gate.js +168 -0
  118. package/dist/commands/improve/shared.js +51 -0
  119. package/dist/commands/improve/triage.js +93 -0
  120. package/dist/commands/lint/agent-linter.js +19 -24
  121. package/dist/commands/lint/base-linter.js +173 -60
  122. package/dist/commands/lint/command-linter.js +19 -24
  123. package/dist/commands/lint/env-key-rules.js +38 -1
  124. package/dist/commands/lint/fact-linter.js +39 -0
  125. package/dist/commands/lint/index.js +31 -13
  126. package/dist/commands/lint/memory-linter.js +1 -1
  127. package/dist/commands/lint/registry.js +7 -2
  128. package/dist/commands/lint/task-linter.js +3 -3
  129. package/dist/commands/lint/workflow-linter.js +26 -1
  130. package/dist/commands/observability-cli.js +4 -4
  131. package/dist/commands/proposal/drain-policies.js +13 -4
  132. package/dist/commands/proposal/drain.js +45 -51
  133. package/dist/commands/proposal/legacy-import.js +115 -0
  134. package/dist/commands/proposal/proposal-cli.js +24 -34
  135. package/dist/commands/proposal/proposal.js +2 -1
  136. package/dist/commands/proposal/propose.js +8 -3
  137. package/dist/commands/proposal/repository.js +829 -0
  138. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  139. package/dist/commands/proposal/validators/proposals.js +93 -895
  140. package/dist/commands/read/curate.js +410 -111
  141. package/dist/commands/read/knowledge.js +10 -3
  142. package/dist/commands/read/remember-cli.js +133 -138
  143. package/dist/commands/read/search-cli.js +15 -8
  144. package/dist/commands/read/search.js +22 -11
  145. package/dist/commands/read/show.js +106 -14
  146. package/dist/commands/registry-cli.js +76 -87
  147. package/dist/commands/remember.js +11 -12
  148. package/dist/commands/sources/add-cli.js +91 -95
  149. package/dist/commands/sources/history.js +1 -1
  150. package/dist/commands/sources/init.js +66 -18
  151. package/dist/commands/sources/installed-stashes.js +11 -3
  152. package/dist/commands/sources/migration-help.js +7 -4
  153. package/dist/commands/sources/schema-repair.js +44 -46
  154. package/dist/commands/sources/self-update.js +2 -2
  155. package/dist/commands/sources/source-add.js +7 -3
  156. package/dist/commands/sources/sources-cli.js +3 -3
  157. package/dist/commands/sources/stash-cli.js +19 -39
  158. package/dist/commands/sources/stash-skeleton.js +57 -8
  159. package/dist/commands/tasks/default-tasks.js +15 -2
  160. package/dist/commands/tasks/tasks-cli.js +20 -29
  161. package/dist/commands/tasks/tasks.js +39 -11
  162. package/dist/commands/wiki-cli.js +23 -38
  163. package/dist/commands/workflow-cli.js +291 -13
  164. package/dist/core/asset/asset-registry.js +3 -1
  165. package/dist/core/asset/asset-spec.js +79 -5
  166. package/dist/core/asset/frontmatter.js +188 -167
  167. package/dist/core/asset/markdown.js +8 -0
  168. package/dist/core/authoring-rules.js +92 -0
  169. package/dist/core/common.js +4 -23
  170. package/dist/core/concurrent.js +10 -1
  171. package/dist/core/config/config-io.js +10 -1
  172. package/dist/core/config/config-migration.js +18 -40
  173. package/dist/core/config/config-schema.js +403 -62
  174. package/dist/core/config/config-types.js +3 -3
  175. package/dist/core/config/config.js +67 -22
  176. package/dist/core/deep-merge.js +38 -0
  177. package/dist/core/errors.js +1 -0
  178. package/dist/core/eval/rank-metrics.js +113 -0
  179. package/dist/core/events.js +4 -7
  180. package/dist/core/improve-types.js +47 -8
  181. package/dist/core/json-schema.js +142 -0
  182. package/dist/core/logs-db.js +14 -75
  183. package/dist/core/parse.js +36 -16
  184. package/dist/core/paths.js +18 -18
  185. package/dist/core/standards/resolve-standards-context.js +87 -0
  186. package/dist/core/standards/resolve-stash-standards.js +99 -0
  187. package/dist/core/standards/resolve-type-conventions.js +66 -0
  188. package/dist/core/state/migrations.js +770 -0
  189. package/dist/core/state-db.js +132 -1126
  190. package/dist/core/structured.js +69 -0
  191. package/dist/core/time.js +53 -0
  192. package/dist/core/warn.js +21 -0
  193. package/dist/core/write-source.js +37 -0
  194. package/dist/indexer/db/db.js +261 -770
  195. package/dist/indexer/db/entry-mapper.js +41 -0
  196. package/dist/indexer/db/graph-db.js +129 -86
  197. package/dist/indexer/db/llm-cache.js +2 -2
  198. package/dist/indexer/db/schema.js +516 -0
  199. package/dist/indexer/ensure-index.js +36 -92
  200. package/dist/indexer/feedback/utility-policy.js +75 -0
  201. package/dist/indexer/graph/graph-boost.js +51 -41
  202. package/dist/indexer/graph/graph-extraction.js +207 -4
  203. package/dist/indexer/index-writer-lock.js +18 -11
  204. package/dist/indexer/index-written-assets.js +105 -0
  205. package/dist/indexer/indexer.js +182 -204
  206. package/dist/indexer/passes/dir-staleness.js +114 -0
  207. package/dist/indexer/passes/memory-inference.js +13 -5
  208. package/dist/indexer/passes/metadata.js +20 -0
  209. package/dist/indexer/read-preflight.js +23 -0
  210. package/dist/indexer/search/db-search.js +89 -13
  211. package/dist/indexer/search/fts-query.js +51 -0
  212. package/dist/indexer/search/ranking-contributors.js +95 -9
  213. package/dist/indexer/search/ranking.js +79 -3
  214. package/dist/indexer/search/search-fields.js +6 -0
  215. package/dist/indexer/search/search-source.js +32 -21
  216. package/dist/indexer/search/semantic-status.js +4 -0
  217. package/dist/indexer/walk/matchers.js +48 -0
  218. package/dist/indexer/walk/walker.js +21 -13
  219. package/dist/integrations/agent/builders.js +41 -13
  220. package/dist/integrations/agent/config.js +20 -59
  221. package/dist/integrations/agent/detect.js +9 -0
  222. package/dist/integrations/agent/index.js +3 -19
  223. package/dist/integrations/agent/model-aliases.js +16 -2
  224. package/dist/integrations/agent/profiles.js +79 -6
  225. package/dist/integrations/agent/prompts.js +75 -9
  226. package/dist/integrations/agent/runner-dispatch.js +83 -0
  227. package/dist/integrations/agent/runner.js +13 -9
  228. package/dist/integrations/agent/spawn.js +206 -81
  229. package/dist/integrations/harnesses/aider/agent-builder.js +113 -0
  230. package/dist/integrations/harnesses/aider/index.js +58 -0
  231. package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
  232. package/dist/integrations/harnesses/amazonq/agent-builder.js +153 -0
  233. package/dist/integrations/harnesses/amazonq/index.js +59 -0
  234. package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
  235. package/dist/integrations/harnesses/claude/agent-builder.js +46 -7
  236. package/dist/integrations/harnesses/claude/index.js +27 -23
  237. package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
  238. package/dist/integrations/harnesses/claude/session-log.js +10 -0
  239. package/dist/integrations/harnesses/codex/agent-builder.js +137 -0
  240. package/dist/integrations/harnesses/codex/index.js +63 -0
  241. package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
  242. package/dist/integrations/harnesses/copilot/agent-builder.js +122 -0
  243. package/dist/integrations/harnesses/copilot/index.js +60 -0
  244. package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
  245. package/dist/integrations/harnesses/gemini/agent-builder.js +121 -0
  246. package/dist/integrations/harnesses/gemini/index.js +60 -0
  247. package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
  248. package/dist/integrations/harnesses/index.js +28 -7
  249. package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
  250. package/dist/integrations/harnesses/opencode/index.js +17 -16
  251. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  252. package/dist/integrations/harnesses/opencode-sdk/harness.js +65 -0
  253. package/dist/integrations/harnesses/opencode-sdk/index.js +10 -34
  254. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +642 -71
  255. package/dist/integrations/harnesses/openhands/agent-builder.js +126 -0
  256. package/dist/integrations/harnesses/openhands/index.js +58 -0
  257. package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
  258. package/dist/integrations/harnesses/pi/agent-builder.js +104 -0
  259. package/dist/integrations/harnesses/pi/index.js +58 -0
  260. package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
  261. package/dist/integrations/harnesses/types.js +8 -0
  262. package/dist/integrations/session-logs/index.js +40 -11
  263. package/dist/llm/call-ai.js +2 -2
  264. package/dist/llm/client.js +34 -11
  265. package/dist/llm/embedder.js +67 -4
  266. package/dist/llm/embedders/cache.js +3 -1
  267. package/dist/llm/embedders/deterministic.js +66 -0
  268. package/dist/llm/embedders/local.js +73 -3
  269. package/dist/llm/feature-gate.js +16 -15
  270. package/dist/llm/graph-extract.js +67 -44
  271. package/dist/llm/memory-infer-impl.js +138 -0
  272. package/dist/llm/memory-infer.js +1 -127
  273. package/dist/llm/metadata-enhance.js +44 -31
  274. package/dist/llm/structured-call.js +49 -0
  275. package/dist/migrate-storage-node.mjs +8 -0
  276. package/dist/output/context.js +5 -5
  277. package/dist/output/renderers.js +87 -15
  278. package/dist/output/shapes/curate.js +14 -2
  279. package/dist/output/shapes/helpers.js +0 -3
  280. package/dist/output/shapes/passthrough.js +6 -1
  281. package/dist/output/text/helpers.js +241 -2
  282. package/dist/output/text/workflow.js +4 -1
  283. package/dist/registry/providers/skills-sh.js +21 -147
  284. package/dist/registry/providers/static-index.js +15 -157
  285. package/dist/registry/resolve.js +27 -9
  286. package/dist/runtime.js +25 -1
  287. package/dist/schemas/akm-config.json +14225 -0
  288. package/dist/schemas/akm-workflow.json +328 -0
  289. package/dist/scripts/migrate-storage.js +2743 -8390
  290. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +1652 -607
  291. package/dist/setup/detect.js +9 -0
  292. package/dist/setup/legacy-config.js +106 -0
  293. package/dist/setup/prompt.js +57 -0
  294. package/dist/setup/providers.js +14 -0
  295. package/dist/setup/registry-stash-loader.js +12 -0
  296. package/dist/setup/semantic-assets.js +124 -0
  297. package/dist/setup/setup.js +52 -1614
  298. package/dist/setup/steps/connection.js +734 -0
  299. package/dist/setup/steps/output.js +31 -0
  300. package/dist/setup/steps/platforms.js +124 -0
  301. package/dist/setup/steps/semantic.js +27 -0
  302. package/dist/setup/steps/sources.js +222 -0
  303. package/dist/setup/steps/stashdir.js +42 -0
  304. package/dist/setup/steps/tasks.js +152 -0
  305. package/dist/sources/include.js +6 -2
  306. package/dist/sources/providers/filesystem.js +0 -1
  307. package/dist/sources/providers/git-install.js +210 -0
  308. package/dist/sources/providers/git-provider.js +234 -0
  309. package/dist/sources/providers/git-stash.js +248 -0
  310. package/dist/sources/providers/git.js +10 -661
  311. package/dist/sources/providers/npm.js +2 -6
  312. package/dist/sources/providers/provider-utils.js +13 -7
  313. package/dist/sources/providers/sync-from-ref.js +9 -1
  314. package/dist/sources/providers/website.js +9 -5
  315. package/dist/sources/website-ingest.js +187 -29
  316. package/dist/sources/wiki-fetchers/registry.js +53 -0
  317. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  318. package/dist/storage/database.js +45 -10
  319. package/dist/storage/managed-db.js +82 -0
  320. package/dist/storage/repositories/canaries-repository.js +107 -0
  321. package/dist/storage/repositories/consolidation-repository.js +38 -0
  322. package/dist/storage/repositories/embeddings-repository.js +72 -0
  323. package/dist/storage/repositories/events-repository.js +187 -0
  324. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  325. package/dist/storage/repositories/improve-runs-repository.js +146 -0
  326. package/dist/storage/repositories/index-db.js +14 -8
  327. package/dist/storage/repositories/proposals-repository.js +220 -0
  328. package/dist/storage/repositories/recombine-repository.js +213 -0
  329. package/dist/storage/repositories/registry-cache.js +93 -0
  330. package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
  331. package/dist/storage/repositories/task-history-repository.js +93 -0
  332. package/dist/storage/repositories/workflow-runs-repository.js +189 -1
  333. package/dist/storage/sqlite-pragmas.js +146 -0
  334. package/dist/tasks/backends/cron.js +1 -1
  335. package/dist/tasks/backends/index.js +9 -0
  336. package/dist/tasks/backends/launchd.js +1 -1
  337. package/dist/tasks/backends/schtasks.js +1 -1
  338. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  339. package/dist/tasks/runner.js +15 -13
  340. package/dist/text-import-hook.mjs +1 -1
  341. package/dist/wiki/wiki.js +52 -11
  342. package/dist/workflows/authoring/authoring.js +123 -10
  343. package/dist/workflows/authoring/workflow-program-template.yaml +31 -0
  344. package/dist/workflows/cli.js +5 -0
  345. package/dist/workflows/db.js +138 -4
  346. package/dist/workflows/exec/brief.js +484 -0
  347. package/dist/workflows/exec/native-executor.js +975 -0
  348. package/dist/workflows/exec/param-secrets.js +115 -0
  349. package/dist/workflows/exec/report.js +1295 -0
  350. package/dist/workflows/exec/run-workflow.js +596 -0
  351. package/dist/workflows/exec/scheduler.js +100 -0
  352. package/dist/workflows/exec/step-work.js +1156 -0
  353. package/dist/workflows/exec/unit-writer.js +23 -0
  354. package/dist/workflows/exec/watch.js +116 -0
  355. package/dist/workflows/exec/worktree.js +171 -0
  356. package/dist/workflows/ir/compile.js +388 -0
  357. package/dist/workflows/ir/params.js +54 -0
  358. package/dist/workflows/ir/plan-hash.js +33 -0
  359. package/dist/workflows/ir/schema.js +4 -0
  360. package/dist/workflows/parser.js +3 -1
  361. package/dist/workflows/program/expressions.js +369 -0
  362. package/dist/workflows/program/parser.js +760 -0
  363. package/dist/workflows/program/project.js +105 -0
  364. package/dist/workflows/program/schema.js +54 -0
  365. package/dist/workflows/renderer.js +82 -5
  366. package/dist/workflows/runtime/agent-identity.js +59 -14
  367. package/dist/workflows/runtime/runs.js +248 -153
  368. package/dist/workflows/runtime/unit-checkin.js +45 -0
  369. package/dist/workflows/runtime/workflow-asset-loader.js +188 -0
  370. package/dist/workflows/validate-summary.js +26 -10
  371. package/dist/workflows/validator.js +1 -1
  372. package/docs/README.md +69 -18
  373. package/docs/data-and-telemetry.md +7 -5
  374. package/docs/migration/release-notes/0.7.0.md +1 -1
  375. package/docs/migration/release-notes/0.9.0-beta.60.md +19 -0
  376. package/docs/migration/release-notes/0.9.0.md +39 -0
  377. package/package.json +10 -10
  378. package/dist/assets/tasks/core/update-stashes.yml +0 -4
  379. package/dist/commands/db-cli.js +0 -23
  380. package/dist/indexer/db/db-backup.js +0 -376
  381. package/dist/indexer/passes/staleness-detect.js +0 -488
@@ -0,0 +1,596 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * Engine-driven workflow execution — `akm workflow run` (orchestration plan
6
+ * P1, owner decision 5). akm itself walks the plan and dispatches units; the
7
+ * existing `next`/`complete` loop remains for manual/agent-driven advancement
8
+ * of the same runs.
9
+ *
10
+ * Invariant (plan §*Never bypass the gate spine*): every step advances
11
+ * through `completeWorkflowStep`, never by writing step rows directly, so the
12
+ * summary-validation gate and run-state derivation stay authoritative. A gate
13
+ * rejection (SummaryValidationFailure) STOPS the engine and surfaces the
14
+ * corrective feedback — a gate is a gate, even for the engine.
15
+ *
16
+ * Artifact-judging gates (redesign addendum, R2): when a step declares
17
+ * completion criteria, the engine hands the gate a summary BUILT FROM the
18
+ * step's promoted artifact (canonical JSON, clipped, prefixed with a one-line
19
+ * unit count — `buildArtifactSummary`) instead of the machine-prose execution
20
+ * summary, so the judge evaluates real results. Each engine-driven judge call
21
+ * is journaled as a unit row (`node_id "<stepId>.gate"`, `unit_id
22
+ * "<stepId>.gate:l<loop>"`, runner "llm", result_json = the verdict) through
23
+ * the writer queue — it is an LLM call like any other. Human approvals are
24
+ * never cached: a blocked gate stays blocked.
25
+ *
26
+ * Bounded gate loops (`gate.max_loops`, addendum R2): a rejection on a step
27
+ * with maxLoops > 1 re-executes the step subgraph with the judge's feedback +
28
+ * missing[] threaded into every unit prompt (`gateFeedback` on
29
+ * StepExecutionContext) — the feedback changes each unit's input hash, so the
30
+ * loop re-dispatches naturally instead of reusing the rejected rows. After
31
+ * maxLoops rejections the engine stops with the gate feedback, exactly like
32
+ * the one-shot case. A typed-artifact schema mismatch feeds the same loop
33
+ * (the validation errors are the feedback; no judge ran, so no gate unit is
34
+ * journaled for that attempt) — only the FINAL loop's mismatch fails the run.
35
+ *
36
+ * Frozen plan (redesign addendum, R1): the plan graph is read from the run
37
+ * row (`plan_json`, persisted by `startWorkflowRun` under migration 006) with
38
+ * a `plan_hash` integrity check — the workflow asset file is NEVER re-read
39
+ * for an in-flight run, so a mid-run asset edit cannot change behavior.
40
+ * Legacy runs (created before migration 006, NULL plan_json) fall back to
41
+ * compile-from-asset with a warning. Durable-row resume: re-invoking a
42
+ * partially-executed run re-dispatches only work that never completed.
43
+ *
44
+ * Run lease (redesign addendum, R2): exactly one engine invocation drives a
45
+ * run at a time. The lease (random holder id + 90s expiry on the run row) is
46
+ * acquired before any dispatch, renewed between steps, and released in a
47
+ * `finally`; a second `workflow run` on a live-leased run refuses up front,
48
+ * and an expired lease is claimable (crash recovery). While the lease is
49
+ * live, manual `workflow complete` is refused too — the engine owns the
50
+ * spine while driving (enforced inside `completeWorkflowStep`).
51
+ *
52
+ * Process-lifecycle contract (owner finding 4 — no leaked handles): the SDK
53
+ * dispatch path caches `opencode serve` CHILD PROCESSES in a per-env registry
54
+ * for reuse across units. Each live child is an OS handle that keeps Bun's
55
+ * event loop open; the registry's own teardown is wired only to
56
+ * `process.once('exit')`, which never fires while a child holds the loop open.
57
+ * That deadlock hangs a one-shot CLI (`akm workflow run` has no `process.exit`
58
+ * on success — it relies on the loop draining). The engine therefore DRAINS
59
+ * the dispatch registry ({@link disposeDispatchResources}) in its run `finally`,
60
+ * on EVERY exit path, so the process exits cleanly the moment the run resolves.
61
+ * The drain is synchronous, idempotent, and a no-op when no SDK server started.
62
+ */
63
+ import { randomUUID } from "node:crypto";
64
+ import { UsageError } from "../../core/errors.js";
65
+ import { warn } from "../../core/warn.js";
66
+ import { disposeDispatchResources } from "../../integrations/agent/runner-dispatch.js";
67
+ import { withWorkflowRunsRepo } from "../../storage/repositories/workflow-runs-repository.js";
68
+ import { assertRunParamsSatisfyPlan } from "../ir/params.js";
69
+ import { completeWorkflowStep, getNextWorkflowStep } from "../runtime/runs.js";
70
+ import { compileWorkflowAssetPlan, loadWorkflowAsset } from "../runtime/workflow-asset-loader.js";
71
+ import { executeStepPlan } from "./native-executor.js";
72
+ // Shared step semantics — route evaluation + cascaded-skip bookkeeping,
73
+ // gate-evaluation journaling, and the whole step-completion path
74
+ // (`finalizeExecutedStep`) live in step-work.ts so the engine loop and the R3
75
+ // brief/report driver protocol share ONE implementation (no drift).
76
+ import { activeGateLoop, cascadeSkippedRouter, finalizeExecutedStep, GATE_EVALUATION_PHASE, parseFrozenPlan, recoverGateFeedback, seedJournaledRouteDecisions, } from "./step-work.js";
77
+ export async function runWorkflowSteps(options) {
78
+ const next = await getNextWorkflowStep(options.target, options.params);
79
+ // Refuse non-active runs BEFORE any dispatch — completeWorkflowStep would
80
+ // reject the completion anyway, but only after the units already ran (and
81
+ // cost money). Mirror its preflight up front.
82
+ if (!next.done && next.run.status !== "active") {
83
+ throw new UsageError(`Workflow run ${next.run.id} is ${next.run.status} and cannot be executed. ` +
84
+ `Use \`akm workflow resume ${next.run.id}\` to reopen it first.`);
85
+ }
86
+ // Run lease (R2 single-driver enforcement): claim the run BEFORE any
87
+ // dispatch — a second `akm workflow run` on a live-leased run refuses up
88
+ // front instead of racing the first engine's spine. An expired lease is
89
+ // claimable (crash recovery). Released in the finally below; renewed
90
+ // between steps inside the loop. A done run takes no lease: nothing will
91
+ // dispatch, and the status re-read below must stay a pure no-op.
92
+ const runId = next.run.id;
93
+ const leaseHolder = randomUUID();
94
+ const leased = !next.done;
95
+ if (leased) {
96
+ await acquireRunLease(runId, leaseHolder);
97
+ }
98
+ // Lease heartbeat (P1 fix): the lease TTL is renewed BETWEEN steps, but a
99
+ // single unit's dispatch can outlive the TTL (the default unit timeout is 10
100
+ // minutes, > the 90s lease). An unheartbeated lease would silently expire
101
+ // mid-dispatch, letting a second `akm workflow run` claim the run and
102
+ // re-dispatch the same units — the two engines clobber each other's journal
103
+ // rows and double-run side effects. A timer INSIDE this invocation renews the
104
+ // lease while dispatch is in flight; it is cleared in the `finally`, so it
105
+ // dies with the process — exactly when the lease SHOULD become claimable
106
+ // after TTL. A renewal that fails (the lease was genuinely stolen after an
107
+ // expiry, e.g. the process was suspended) aborts dispatch and fails the run
108
+ // loudly rather than keep double-driving.
109
+ const heartbeat = leased
110
+ ? new LeaseHeartbeat(runId, leaseHolder, options.heartbeatScheduler, options.signal)
111
+ : undefined;
112
+ heartbeat?.start();
113
+ try {
114
+ return await driveRun(options, next, leaseHolder, heartbeat);
115
+ }
116
+ finally {
117
+ heartbeat?.stop();
118
+ try {
119
+ if (leased) {
120
+ await withWorkflowRunsRepo((repo) => {
121
+ repo.releaseEngineLease(runId, leaseHolder);
122
+ });
123
+ }
124
+ }
125
+ finally {
126
+ // Process-lifecycle drain (owner finding 4): release any cached SDK server
127
+ // child processes so a one-shot CLI invocation exits cleanly instead of
128
+ // hanging on the leaked handle. Runs even if lease release itself fails;
129
+ // a teardown-time repository error must not skip dispatch cleanup.
130
+ try {
131
+ (options.disposeDispatchResources ?? disposeDispatchResources)();
132
+ }
133
+ catch {
134
+ /* disposal is best-effort; never let cleanup mask the run outcome */
135
+ }
136
+ }
137
+ }
138
+ }
139
+ /** Lease lifetime: long enough to survive slow steps between renewals, short
140
+ * enough that a crashed engine frees the run quickly. Renewed per step. */
141
+ const RUN_LEASE_TTL_MS = 90_000;
142
+ function leaseExpiry() {
143
+ return new Date(Date.now() + RUN_LEASE_TTL_MS).toISOString();
144
+ }
145
+ /**
146
+ * Atomically claim the run lease or refuse with a UsageError naming the
147
+ * current holder + expiry. The single-UPDATE claim in the repository is the
148
+ * arbiter — two racing invocations cannot both win.
149
+ */
150
+ async function acquireRunLease(runId, holder) {
151
+ await withWorkflowRunsRepo((repo) => {
152
+ if (repo.acquireEngineLease(runId, holder, leaseExpiry(), new Date().toISOString()))
153
+ return;
154
+ const row = repo.getRunById(runId);
155
+ throw new UsageError(`Workflow run ${runId} is already being driven by engine ${row?.engine_lease_holder ?? "(unknown)"} ` +
156
+ `(run lease expires ${row?.engine_lease_until ?? "(unknown)"}). A second \`akm workflow run\` would race it — ` +
157
+ `wait for that invocation to finish or for the lease to expire.`);
158
+ });
159
+ }
160
+ /**
161
+ * Renew the lease between steps. Losing the lease mid-run (it expired during
162
+ * a long step and another engine claimed it) is a hard stop: the new owner
163
+ * drives the spine now, and continuing would race it.
164
+ */
165
+ async function renewRunLease(runId, holder) {
166
+ await withWorkflowRunsRepo((repo) => {
167
+ if (repo.renewEngineLease(runId, holder, leaseExpiry()))
168
+ return;
169
+ const row = repo.getRunById(runId);
170
+ throw new UsageError(`Workflow run ${runId} lost its run lease (now held by ${row?.engine_lease_holder ?? "(nobody)"}). ` +
171
+ `Another engine invocation claimed the run after this one's lease expired — stopping to avoid racing it.`);
172
+ });
173
+ }
174
+ /** Renew mid-dispatch this often. Well under the TTL so a slow/skipped tick
175
+ * still leaves ample margin before the lease would expire. */
176
+ const HEARTBEAT_INTERVAL_MS = RUN_LEASE_TTL_MS / 3;
177
+ /** Real timer: an unref'd interval so a live heartbeat never keeps the process alive. */
178
+ function defaultHeartbeatScheduler(tick) {
179
+ const id = setInterval(() => void tick(), HEARTBEAT_INTERVAL_MS);
180
+ id.unref?.();
181
+ return () => clearInterval(id);
182
+ }
183
+ /**
184
+ * Keeps the run lease alive while a step dispatches (P1 fix — the between-step
185
+ * renewal cannot cover a unit that runs longer than the TTL). A timer inside
186
+ * the engine invocation renews the lease through the holder-guarded
187
+ * {@link renewEngineLease}; the heartbeat owns an {@link AbortController}
188
+ * (chained onto the caller's signal) that becomes the effective DISPATCH
189
+ * signal, so a lost lease aborts in-flight dispatch PROMPTLY. After the abort,
190
+ * {@link assertAlive} throws a loud UsageError, so the engine stops instead of
191
+ * continuing to drive a run another engine now owns. No background daemon: the
192
+ * timer is cleared in the caller's `finally` and dies with the process.
193
+ */
194
+ class LeaseHeartbeat {
195
+ runId;
196
+ holder;
197
+ controller = new AbortController();
198
+ detachUpstream;
199
+ schedule;
200
+ cancel;
201
+ renewing = false;
202
+ /** Set once a renewal failed — the lease was stolen after a genuine expiry. */
203
+ lost = false;
204
+ /** The holder that stole the lease, captured for the loud error. */
205
+ stolenBy = null;
206
+ constructor(runId, holder, scheduler, upstream) {
207
+ this.runId = runId;
208
+ this.holder = holder;
209
+ this.schedule = scheduler ?? defaultHeartbeatScheduler;
210
+ // A caller abort (Ctrl-C, budget) must abort dispatch too; chain it into
211
+ // the effective signal. Distinct from a lost lease: a caller abort does
212
+ // NOT set `lost`, so `assertAlive` stays quiet and the existing graceful
213
+ // break on `options.signal` handles it.
214
+ if (upstream) {
215
+ if (upstream.aborted) {
216
+ this.controller.abort();
217
+ }
218
+ else {
219
+ const onAbort = () => this.controller.abort();
220
+ upstream.addEventListener("abort", onAbort, { once: true });
221
+ this.detachUpstream = () => upstream.removeEventListener("abort", onAbort);
222
+ }
223
+ }
224
+ }
225
+ /** The effective dispatch signal: aborts on a lost lease OR a caller abort. */
226
+ get signal() {
227
+ return this.controller.signal;
228
+ }
229
+ start() {
230
+ this.cancel ??= this.schedule(() => this.tick());
231
+ }
232
+ /** One renewal attempt. A failure marks the lease lost and aborts dispatch. */
233
+ async tick() {
234
+ if (this.lost || this.renewing || this.controller.signal.aborted)
235
+ return;
236
+ this.renewing = true;
237
+ try {
238
+ const renewed = await withWorkflowRunsRepo((repo) => repo.renewEngineLease(this.runId, this.holder, leaseExpiry()));
239
+ if (!renewed) {
240
+ this.stolenBy = await withWorkflowRunsRepo((repo) => repo.getRunById(this.runId)?.engine_lease_holder ?? null);
241
+ this.loseLease();
242
+ }
243
+ }
244
+ catch {
245
+ // A renewal that THREW (a DB error / connection failure, or the follow-up
246
+ // getRunById itself throwing) is treated exactly like a stolen lease: we
247
+ // can no longer PROVE we still hold it, so abort in-flight dispatch and let
248
+ // `assertAlive` stop the engine loudly. Swallowing the error here is what
249
+ // keeps the fire-and-forget `void tick()` in the default scheduler from
250
+ // leaking an unhandled promise rejection.
251
+ this.loseLease();
252
+ }
253
+ finally {
254
+ this.renewing = false;
255
+ }
256
+ }
257
+ /** Mark the lease lost, stop the timer, and abort in-flight dispatch — the new
258
+ * owner drives the spine now (or, on a renewal error, we can no longer prove we
259
+ * do). Idempotent: repeated calls are harmless. */
260
+ loseLease() {
261
+ this.lost = true;
262
+ this.stop();
263
+ this.controller.abort();
264
+ }
265
+ /**
266
+ * Throw loudly if a heartbeat renewal failed. Called at dispatch boundaries:
267
+ * a lost lease means another engine claimed the run mid-step, so continuing
268
+ * (completing steps, dispatching more units) would double-drive it.
269
+ */
270
+ assertAlive() {
271
+ if (!this.lost)
272
+ return;
273
+ throw new UsageError(`Workflow run ${this.runId} lost its run lease mid-dispatch (heartbeat renewal failed; lease now held by ` +
274
+ `${this.stolenBy ?? "(nobody)"}). Another engine invocation claimed the run after this one's lease expired — ` +
275
+ `aborting to avoid double-driving it.`);
276
+ }
277
+ stop() {
278
+ this.cancel?.();
279
+ this.cancel = undefined;
280
+ this.detachUpstream?.();
281
+ }
282
+ }
283
+ /** The engine loop proper — runs under the lease held by `runWorkflowSteps`. */
284
+ async function driveRun(options, initial, leaseHolder, heartbeat) {
285
+ let next = initial;
286
+ // A terminal (completed) run is a PURE no-op. `runWorkflowSteps` already
287
+ // skipped lease acquisition for a done run (`leased = !next.done`), and this
288
+ // path must ALSO refuse to read the journal or load/integrity-check the
289
+ // frozen plan: a run that finished cleanly, then had its `plan_json` corrupted
290
+ // or tampered afterwards, must still report `done` here rather than throwing a
291
+ // frozen-plan integrity error (loadFrozenPlan would). Nothing will dispatch
292
+ // and the engine_lease_* columns stay exactly as they were, so return the
293
+ // fresh run state immediately.
294
+ if (initial.done) {
295
+ const doneState = await getNextWorkflowStep(initial.run.id);
296
+ return {
297
+ run: doneState.run,
298
+ executed: [],
299
+ ...(doneState.run.status === "completed" ? { done: true } : {}),
300
+ };
301
+ }
302
+ // The effective dispatch signal: the heartbeat's controller (a lost lease or
303
+ // a caller abort aborts it) while leased, else the raw caller signal.
304
+ const dispatchSignal = heartbeat?.signal ?? options.signal;
305
+ const executed = [];
306
+ let gateRejection;
307
+ const maxSteps = options.maxSteps ?? Number.POSITIVE_INFINITY;
308
+ // Seed the lifetime unit cap AND the budget ceilings from the journal so
309
+ // both are truly per-RUN: a resumed or re-invoked run must not restart the
310
+ // runaway backstop — or a declared `budget` — at zero. Journal rows = past
311
+ // dispatch ATTEMPTS (counted against `budget.max_units`); their summed
312
+ // `tokens` column is the run's spend so far (counted against
313
+ // `budget.max_tokens`). The executor consumes both only on new dispatches
314
+ // (durable-row reuses are free), so a large partially-completed fan-out
315
+ // stays resumable.
316
+ //
317
+ // Gate-evaluation rows (`phase = "gate"`, journaled by the completion-gate
318
+ // judge below) are EXCLUDED from the seed: the live path never consumes
319
+ // DispatchBudget for a judge call, so counting its journal row on resume
320
+ // would make an interrupted run hit `max_units` (and the lifetime cap)
321
+ // earlier than the identical uninterrupted run — a spurious hard failure
322
+ // that `on_error` cannot soften. The seed must reproduce exactly what live
323
+ // accounting would have accumulated.
324
+ //
325
+ // The seed sums each dispatch row's `attempts` (migration 008), NOT the row
326
+ // COUNT: a crash between a unit's dispatch and its finish leaves a `running`
327
+ // row that resume re-dispatches under the SAME content-derived unit_id, and
328
+ // `insertUnit` REPLACES that one row while bumping `attempts`. Counting rows
329
+ // would erase every prior crash-retried dispatch from budget/lifetime
330
+ // accounting, letting the run spend past its declared ceiling; summing
331
+ // `attempts` charges each dispatch exactly once.
332
+ const journaledUnits = await withWorkflowRunsRepo((repo) => repo.getUnitsForRun(next.run.id));
333
+ const journaledDispatches = journaledUnits.filter((row) => row.phase !== GATE_EVALUATION_PHASE);
334
+ let unitsDispatched = journaledDispatches.reduce((sum, row) => sum + row.attempts, 0);
335
+ let tokensUsed = journaledDispatches.reduce((sum, row) => sum + (row.tokens ?? 0), 0);
336
+ // One plan per invocation: the test seam receives the workflow ref; the
337
+ // default reads the run's frozen plan and never touches the asset file.
338
+ const plan = options.loadPlan
339
+ ? await options.loadPlan(next.run.workflowRef)
340
+ : await loadFrozenPlan(next.run.id, next.run.workflowRef);
341
+ // Reviewer #12: the journaled params row must still satisfy the frozen param
342
+ // schemas before the engine resolves any unit prompt from it. Applied on ALL
343
+ // THREE driver surfaces (engine here, brief, report) so schema-violating
344
+ // params — post-start corruption — fail loudly and IDENTICALLY, preserving
345
+ // cross-surface parity (start already validated the params it stored).
346
+ assertRunParamsSatisfyPlan(next.run.id, plan, next.run.params ?? {});
347
+ // Route bookkeeping: targets a completed router did NOT select are skipped
348
+ // when the spine reaches them; a target ANY router selected is protected
349
+ // (two routers may share a target).
350
+ const routeSelected = new Set();
351
+ const routeUnselected = new Map();
352
+ // Resume contract: route decisions are journaled in the route step's
353
+ // evidence (`evidence.route.selected`) and must be REPLAYED into the
354
+ // bookkeeping before the spine advances — a re-invoked run (crash, Ctrl-C,
355
+ // maxSteps, gate rejection after the route completed) would otherwise reach
356
+ // the unselected targets with empty in-memory state and execute the wrong
357
+ // branch. Decisions stay pure functions of (frozen plan, params, journaled
358
+ // results) — the addendum determinism bar. A done run skips the seeding:
359
+ // nothing will dispatch, so an unrecoverable historical decision must not
360
+ // block the no-op status return below.
361
+ if (!next.done) {
362
+ seedJournaledRouteDecisions(plan, next, routeSelected, routeUnselected);
363
+ }
364
+ while (!next.done && next.step && next.run.status === "active" && executed.length < maxSteps) {
365
+ // A LOST lease (the heartbeat's renewal failed mid-step) is a loud stop —
366
+ // another engine owns the spine now. A caller abort (options.signal) is a
367
+ // graceful break, distinct from a lost lease.
368
+ heartbeat?.assertAlive();
369
+ if (options.signal?.aborted)
370
+ break;
371
+ // Renew the run lease between steps (a fresh 90s window per iteration).
372
+ // Losing it (expired mid-step + claimed by another engine) throws — the
373
+ // new owner drives the spine now.
374
+ await renewRunLease(next.run.id, leaseHolder);
375
+ const step = next.step;
376
+ const stepPlan = plan.steps.find((s) => s.stepId === step.id);
377
+ if (!stepPlan) {
378
+ throw new UsageError(`Step "${step.id}" of run ${next.run.id} is not present in the current workflow asset (${next.run.workflowRef}). ` +
379
+ `The source file changed since the run started — advance this step manually with \`akm workflow complete\`.`);
380
+ }
381
+ // A branch target no completed router selected → auto-skip, no dispatch.
382
+ const skipInfo = routeUnselected.get(step.id);
383
+ if (skipInfo && !routeSelected.has(step.id)) {
384
+ // Cascade (peer review R1): a skipped step that is ITSELF a router
385
+ // never evaluates its route, so none of its declared targets were
386
+ // selected — mark them all skip-on-reach too (a target another
387
+ // completed router selects stays protected via routeSelected). Without
388
+ // this, every branch of the skipped router would run unconditionally.
389
+ if (stepPlan.route) {
390
+ cascadeSkippedRouter(stepPlan.route, step.id, routeUnselected);
391
+ }
392
+ const notes = skipInfo.selected === null
393
+ ? `Skipped by route: step "${skipInfo.router}" was itself skipped, so none of its branch targets run.`
394
+ : `Skipped by route: step "${skipInfo.router}" selected "${skipInfo.selected}".`;
395
+ executed.push({ stepId: step.id, ok: true, unitCount: 0, failedUnits: 0, summary: notes });
396
+ await completeWorkflowStep({ runId: next.run.id, stepId: step.id, status: "skipped", notes, leaseHolder });
397
+ next = await getNextWorkflowStep(next.run.id);
398
+ continue;
399
+ }
400
+ // `dependsOn` edges (reserved in IR v2 — no frontend emits them today,
401
+ // but a frozen plan may carry them) are a declared ordering contract:
402
+ // every dependency must already be resolved before this step dispatches.
403
+ // Execution is sequential (spine order), so a violation means the plan
404
+ // ordered steps inconsistently with its declared edges — fail fast,
405
+ // before spending.
406
+ for (const dep of stepPlan.dependsOn ?? []) {
407
+ const depState = next.workflow.steps.find((s) => s.id === dep);
408
+ if (!depState || (depState.status !== "completed" && depState.status !== "skipped")) {
409
+ throw new UsageError(`Step "${step.id}" depends on step "${dep}", which is ${depState?.status ?? "missing"}. ` +
410
+ `Reorder the workflow so dependencies come first (execution is sequential in step order).`);
411
+ }
412
+ }
413
+ const evidence = {};
414
+ for (const s of next.workflow.steps)
415
+ evidence[s.id] = s.evidence;
416
+ // Bounded gate loop (addendum R2, `gate.max_loops`): loop 1 is the normal
417
+ // execution; a gate rejection with attempts left re-executes the subgraph
418
+ // with the judge's feedback threaded into unit prompts. `advanced` = the
419
+ // step completed and the spine may move on; `stopEngine` = failure or
420
+ // final rejection — this invocation is done.
421
+ const maxLoops = Math.max(1, stepPlan.gate.maxLoops ?? 1);
422
+ // Crash-resume gate state (Codex P1): SEED the starting gate loop from the
423
+ // journal exactly as the brief/report surfaces do — the SAME shared helpers,
424
+ // no fork. A run interrupted after a rejected gate was journaled
425
+ // (`<step>.gate:l<n>`, complete:false) must resume at loop n+1 with the
426
+ // stored corrective feedback threaded into the unit prompts; without this
427
+ // the engine restarts at loop 1, reuses the rejected loop-1 rows, overwrites
428
+ // `<step>.gate:l1`, and re-judges the stale artifact — breaking journaled
429
+ // replay and diverging from what brief computes for the same run. The rows
430
+ // are re-read here (NOT the once-at-start `journaledUnits` budget seed) so a
431
+ // step reached later within THIS same invocation still starts fresh at loop 1.
432
+ const stepJournal = await withWorkflowRunsRepo((repo) => repo.getUnitsForRun(next.run.id));
433
+ const startLoop = activeGateLoop(stepJournal, step.id);
434
+ const seededFeedback = recoverGateFeedback(stepJournal, step.id, startLoop);
435
+ // Resume AFTER the FINAL rejection (`startLoop` past the loop bound): the
436
+ // gate was already exhausted before the crash, so there is NO fresh loop to
437
+ // run — reproduce the documented gateRejection outcome from the stored
438
+ // final-loop feedback instead of re-dispatching a spurious extra loop. The
439
+ // l1..l<maxLoops> rows stay untouched and the step stays active, exactly as
440
+ // when the engine first exhausted the gate.
441
+ if (startLoop > maxLoops) {
442
+ gateRejection = {
443
+ stepId: step.id,
444
+ missing: seededFeedback?.missing ?? [],
445
+ feedback: seededFeedback?.feedback ?? "",
446
+ };
447
+ break;
448
+ }
449
+ let gateFeedback = seededFeedback;
450
+ let advanced = false;
451
+ let stopEngine = false;
452
+ for (let gateLoop = startLoop; gateLoop <= maxLoops; gateLoop++) {
453
+ // A loop re-execution dispatches a fresh round of units — renew the
454
+ // lease so a long evaluator-optimizer cycle cannot outlive the TTL.
455
+ if (gateLoop > 1)
456
+ await renewRunLease(next.run.id, leaseHolder);
457
+ // Route-only steps (YAML `route:` — no execution subgraph) dispatch no
458
+ // units; they only decide the spine's path below. Everything else
459
+ // executes its subgraph through the native executor.
460
+ const result = !stepPlan.root && stepPlan.route
461
+ ? {
462
+ ok: true,
463
+ units: [],
464
+ evidence: {},
465
+ summary: `Step "${step.id}" is a route step — no units dispatched.`,
466
+ unitsDispatched,
467
+ }
468
+ : await executeStepPlan(stepPlan, {
469
+ runId: next.run.id,
470
+ workflowRef: next.run.workflowRef,
471
+ params: next.run.params ?? {},
472
+ evidence,
473
+ unitsDispatched,
474
+ tokensUsed,
475
+ // Budget ceilings ride the FROZEN plan (addendum R2): a mid-run
476
+ // asset edit can never loosen or tighten a run's budget.
477
+ ...(plan.budget ? { budget: plan.budget } : {}),
478
+ gateLoop,
479
+ ...(gateFeedback ? { gateFeedback } : {}),
480
+ // The heartbeat's signal is the effective dispatch signal: a lost
481
+ // lease (or a caller abort) aborts in-flight units promptly.
482
+ ...(dispatchSignal ? { signal: dispatchSignal } : {}),
483
+ ...(options.dispatcher ? { dispatcher: options.dispatcher } : {}),
484
+ ...(options.maxConcurrency !== undefined ? { maxConcurrency: options.maxConcurrency } : {}),
485
+ });
486
+ // If the heartbeat lost the lease WHILE this step dispatched, another
487
+ // engine now owns the run — stop loudly BEFORE finalizing the step
488
+ // (completeWorkflowStep would race the new owner's spine).
489
+ heartbeat?.assertAlive();
490
+ unitsDispatched = result.unitsDispatched;
491
+ if (result.tokensUsed !== undefined)
492
+ tokensUsed = result.tokensUsed;
493
+ executed.push({
494
+ stepId: step.id,
495
+ ok: result.ok,
496
+ unitCount: result.units.length,
497
+ failedUnits: result.units.filter((u) => !u.ok).length,
498
+ summary: result.summary,
499
+ });
500
+ // Route evaluation + artifact-judged completion gate + gate-row
501
+ // journaling + the bounded-loop rejection contract are the SHARED
502
+ // completion path (`finalizeExecutedStep`): the R3 report surface drives
503
+ // the identical sequence, so an engine-driven and a report-driven run of
504
+ // the same frozen plan promote the same artifact and advance (or reject)
505
+ // the spine identically. The engine owns only the loop control the result
506
+ // maps onto (retry re-executes; advanced moves on; failure/exhaustion
507
+ // stops this invocation).
508
+ const finalize = await finalizeExecutedStep({
509
+ runId: next.run.id,
510
+ workflowRef: next.run.workflowRef,
511
+ stepId: step.id,
512
+ stepPlan,
513
+ completionCriteria: step.completionCriteria ?? [],
514
+ gateLoop,
515
+ loopsRemaining: gateLoop < maxLoops,
516
+ result,
517
+ priorEvidence: evidence,
518
+ params: next.run.params ?? {},
519
+ routeSelected,
520
+ routeUnselected,
521
+ summaryJudge: options.summaryJudge,
522
+ ...(options.requireGates ? { requireGates: true } : {}),
523
+ leaseHolder,
524
+ });
525
+ if (finalize.kind === "retry") {
526
+ // Re-execute the subgraph with the judge/validation feedback threaded
527
+ // into unit prompts — the changed prompt changes each unit's input
528
+ // hash, so the re-run dispatches fresh work instead of reusing rows.
529
+ gateFeedback = finalize.gateFeedback;
530
+ continue;
531
+ }
532
+ if (finalize.kind === "advanced") {
533
+ // A route-only step's summary IS its decision (finalize surfaces it).
534
+ if (finalize.summaryOverride !== undefined) {
535
+ executed[executed.length - 1] = { ...executed[executed.length - 1], summary: finalize.summaryOverride };
536
+ }
537
+ advanced = true;
538
+ break;
539
+ }
540
+ if (finalize.kind === "failed") {
541
+ // A route-failure was pushed as ok:true (the units succeeded); reflect
542
+ // the deterministic route failure in the executed report.
543
+ if (finalize.routeFailure) {
544
+ executed[executed.length - 1] = { ...executed[executed.length - 1], ok: false, summary: finalize.summary };
545
+ }
546
+ stopEngine = true;
547
+ break;
548
+ }
549
+ if (finalize.kind === "blocked") {
550
+ // Reviewer #18: a required gate with no judge available — the step is
551
+ // BLOCKED (not failed) for a human. The units succeeded, so overwrite the
552
+ // report's ok/summary to reflect the block, then stop this invocation.
553
+ executed[executed.length - 1] = { ...executed[executed.length - 1], ok: false, summary: finalize.summary };
554
+ stopEngine = true;
555
+ break;
556
+ }
557
+ // gate-exhausted: rejected with no loop budget left — stop with feedback.
558
+ gateRejection = finalize.gateRejection;
559
+ stopEngine = true;
560
+ }
561
+ if (stopEngine || !advanced)
562
+ break;
563
+ next = await getNextWorkflowStep(next.run.id);
564
+ }
565
+ // Re-read for the freshest run state (the loop may have exited on maxSteps).
566
+ const finalState = await getNextWorkflowStep(next.run.id);
567
+ return {
568
+ run: finalState.run,
569
+ executed,
570
+ ...(finalState.run.status === "completed" ? { done: true } : {}),
571
+ ...(gateRejection ? { gateRejection } : {}),
572
+ };
573
+ }
574
+ /**
575
+ * Load the plan a run executes (frozen-plan contract, migration 006):
576
+ *
577
+ * - `plan_json` present → parse it and verify `plan_hash` (sha256 of the
578
+ * canonical JSON). A mismatch means the journaled plan was tampered with
579
+ * or corrupted — fail loudly, never silently recompile. The workflow
580
+ * asset file is NEVER touched on this path.
581
+ * - `plan_json` NULL → the run predates frozen plans (created before
582
+ * migration 006). Warn and fall back to compiling from the live asset,
583
+ * preserving pre-006 behavior for in-flight legacy runs.
584
+ */
585
+ async function loadFrozenPlan(runId, workflowRef) {
586
+ const row = await withWorkflowRunsRepo((repo) => {
587
+ const run = repo.getRunById(runId);
588
+ return run ? { planJson: run.plan_json, planHash: run.plan_hash } : undefined;
589
+ });
590
+ if (row?.planJson) {
591
+ return parseFrozenPlan(runId, row.planJson, row.planHash);
592
+ }
593
+ warn(`Workflow run ${runId} predates frozen plans (no plan_json on the run row); ` +
594
+ `compiling the plan from the live asset ${workflowRef}. New runs freeze their plan at start.`);
595
+ return compileWorkflowAssetPlan(await loadWorkflowAsset(workflowRef));
596
+ }