akm-cli 0.9.0-rc.0 → 0.9.0-rc.13

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 (598) hide show
  1. package/CHANGELOG.md +1283 -22
  2. package/README.md +62 -37
  3. package/SECURITY.md +46 -31
  4. package/dist/akm +162 -38
  5. package/dist/akm-migrate +44 -0
  6. package/dist/assets/backends/schtasks-template.xml +2 -1
  7. package/dist/assets/hints/cli-hints-full.md +268 -118
  8. package/dist/assets/hints/cli-hints-short.md +87 -24
  9. package/dist/assets/{profiles → improve-strategies}/catchup.json +3 -1
  10. package/dist/assets/{profiles → improve-strategies}/consolidate.json +3 -1
  11. package/dist/assets/{profiles → improve-strategies}/default.json +6 -7
  12. package/dist/assets/improve-strategies/frequent.json +15 -0
  13. package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +4 -2
  14. package/dist/assets/{profiles → improve-strategies}/memory-focus.json +4 -1
  15. package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +5 -5
  16. package/dist/assets/{profiles → improve-strategies}/quick.json +4 -2
  17. package/dist/assets/improve-strategies/reflect-distill.json +30 -0
  18. package/dist/assets/{profiles → improve-strategies}/thorough.json +1 -1
  19. package/dist/assets/prompts/consolidate-system.md +5 -5
  20. package/dist/assets/prompts/extract-session.md +2 -6
  21. package/dist/assets/prompts/memory-infer-user.md +2 -3
  22. package/dist/assets/prompts/reflect-llm-framed-contract.md +11 -0
  23. package/dist/assets/prompts/reflect-llm-schema-contract.md +3 -0
  24. package/dist/assets/prompts/reflect-output-repair.md +3 -0
  25. package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
  26. package/dist/assets/stash-skeleton/README.md +38 -10
  27. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +8 -0
  28. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +8 -0
  29. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +14 -1
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +13 -1
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +9 -1
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +11 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +9 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +9 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +8 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +100 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/domains.md +64 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/organization.md +136 -0
  39. package/dist/assets/tasks/core/extract.yml +3 -2
  40. package/dist/assets/tasks/core/improve.yml +2 -1
  41. package/dist/assets/tasks/core/index-refresh.yml +1 -0
  42. package/dist/assets/tasks/core/sync.yml +1 -0
  43. package/dist/assets/tasks/core/version-check.yml +2 -1
  44. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
  45. package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
  46. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
  47. package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
  48. package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
  49. package/dist/assets/templates/html/health.html +5 -4
  50. package/dist/assets/workflows/workflow-template.md +31 -15
  51. package/dist/cli/invocation.js +279 -0
  52. package/dist/cli/parse-args.js +5 -90
  53. package/dist/cli/retired-commands.js +78 -0
  54. package/dist/cli/shared.js +158 -48
  55. package/dist/cli-node.mjs +2 -1
  56. package/dist/cli.js +747 -293
  57. package/dist/commands/agent/agent-dispatch.js +19 -18
  58. package/dist/commands/agent/agent-support.js +0 -24
  59. package/dist/commands/agent/contribute-cli.js +43 -97
  60. package/dist/commands/completions.js +80 -23
  61. package/dist/commands/config-cli.js +44 -281
  62. package/dist/commands/env/env-binding.js +99 -0
  63. package/dist/commands/env/env-cli.js +84 -224
  64. package/dist/commands/env/env.js +12 -163
  65. package/dist/commands/env/marker-path.js +6 -0
  66. package/dist/commands/env/secret-cli.js +45 -61
  67. package/dist/commands/env/secret.js +32 -62
  68. package/dist/commands/feedback-cli.js +179 -85
  69. package/dist/commands/health/accept-rate.js +58 -0
  70. package/dist/commands/health/advisories.js +7 -8
  71. package/dist/commands/health/checks.js +279 -94
  72. package/dist/commands/health/html-report.js +197 -578
  73. package/dist/commands/health/improve-metrics.js +277 -246
  74. package/dist/commands/health/llm-usage.js +19 -19
  75. package/dist/commands/health/md-report.js +16 -7
  76. package/dist/commands/health/metrics.js +67 -32
  77. package/dist/commands/health/renderers.js +47 -0
  78. package/dist/commands/health/report-view-model.js +508 -0
  79. package/dist/commands/health/stash-exposure.js +1 -1
  80. package/dist/commands/health/surfaces.js +16 -56
  81. package/dist/commands/health/task-runs.js +3 -67
  82. package/dist/{migrate-storage-node.mjs → commands/health/types-checks.js} +1 -5
  83. package/dist/commands/health/types-improve.js +29 -0
  84. package/dist/{output/text/save.js → commands/health/types-metrics.js} +1 -2
  85. package/dist/commands/health/types-result.js +7 -0
  86. package/dist/commands/health/types-runs.js +4 -0
  87. package/dist/commands/health/types-session-log.js +4 -0
  88. package/dist/commands/health/types-windows.js +4 -0
  89. package/dist/commands/health/types.js +26 -21
  90. package/dist/commands/health/windows.js +2 -3
  91. package/dist/commands/health.js +296 -167
  92. package/dist/commands/improve/anti-collapse.js +5 -5
  93. package/dist/commands/improve/autonomy-gate.js +68 -0
  94. package/dist/commands/improve/collapse-detector.js +65 -52
  95. package/dist/commands/improve/consolidate/chunking.js +9 -7
  96. package/dist/commands/improve/consolidate/eligibility.js +1 -23
  97. package/dist/commands/improve/consolidate/merge.js +4 -0
  98. package/dist/commands/improve/consolidate.js +454 -1354
  99. package/dist/commands/improve/content-hash.js +39 -0
  100. package/dist/commands/improve/distill/content-repair.js +4 -10
  101. package/dist/commands/improve/distill/promote-memory.js +89 -64
  102. package/dist/commands/improve/distill/quality-gate.js +118 -42
  103. package/dist/commands/improve/distill-guards.js +1 -1
  104. package/dist/commands/improve/distill-promotion-policy.js +33 -888
  105. package/dist/commands/improve/distill.js +607 -363
  106. package/dist/commands/improve/eligibility.js +165 -79
  107. package/dist/commands/improve/extract-cli.js +35 -126
  108. package/dist/commands/improve/extract-prompt.js +6 -35
  109. package/dist/commands/improve/extract.js +640 -391
  110. package/dist/commands/improve/feedback-valence.js +2 -12
  111. package/dist/commands/improve/improve-cli.js +134 -135
  112. package/dist/commands/improve/improve-result-file.js +30 -50
  113. package/dist/commands/improve/improve-run-types.js +4 -0
  114. package/dist/commands/improve/improve-strategies.js +135 -0
  115. package/dist/commands/improve/improve.js +904 -701
  116. package/dist/commands/improve/locks.js +64 -111
  117. package/dist/commands/improve/loop-stages.js +1110 -923
  118. package/dist/commands/improve/memory/derived-ref.js +124 -0
  119. package/dist/commands/improve/memory/memory-belief.js +79 -7
  120. package/dist/commands/improve/memory/memory-contradiction-detect.js +49 -52
  121. package/dist/commands/improve/memory/memory-improve.js +25 -37
  122. package/dist/commands/improve/outcome-loop.js +25 -88
  123. package/dist/commands/improve/preparation.js +1034 -813
  124. package/dist/commands/improve/proactive-maintenance.js +34 -9
  125. package/dist/commands/improve/proposal-envelope.js +31 -0
  126. package/dist/commands/improve/reflect.js +983 -794
  127. package/dist/commands/improve/run-context.js +119 -0
  128. package/dist/commands/improve/salience.js +24 -127
  129. package/dist/commands/improve/session-asset.js +7 -3
  130. package/dist/commands/improve/shared.js +14 -34
  131. package/dist/commands/improve/source-identity.js +28 -0
  132. package/dist/commands/improve/triage.js +20 -17
  133. package/dist/commands/lint/base-linter.js +340 -313
  134. package/dist/commands/lint/env-key-rules.js +31 -47
  135. package/dist/commands/lint/index.js +185 -30
  136. package/dist/commands/{events.js → log.js} +28 -38
  137. package/dist/commands/migrate-cli.js +54 -0
  138. package/dist/commands/migration-tool.js +55 -0
  139. package/dist/commands/observability-cli.js +70 -208
  140. package/dist/commands/proposal/diff-format.js +50 -0
  141. package/dist/commands/proposal/drain-policies.js +0 -6
  142. package/dist/commands/proposal/drain.js +91 -40
  143. package/dist/commands/proposal/proposal-cli.js +134 -132
  144. package/dist/commands/proposal/proposal-types.js +56 -0
  145. package/dist/commands/proposal/proposal.js +83 -65
  146. package/dist/commands/proposal/propose-cli.js +88 -0
  147. package/dist/commands/proposal/propose.js +105 -88
  148. package/dist/commands/proposal/repository.js +1303 -278
  149. package/dist/commands/proposal/validators/proposal-quality-validators.js +16 -6
  150. package/dist/commands/proposal/validators/proposal-validators.js +61 -12
  151. package/dist/commands/proposal/validators/proposals.js +6 -8
  152. package/dist/commands/read/curate.js +78 -73
  153. package/dist/commands/read/knowledge.js +510 -13
  154. package/dist/commands/read/registry-search.js +2 -2
  155. package/dist/commands/read/remember-cli.js +84 -15
  156. package/dist/commands/read/search-cli.js +203 -96
  157. package/dist/commands/read/search.js +126 -94
  158. package/dist/commands/read/show.js +226 -250
  159. package/dist/commands/registry-cli.js +34 -60
  160. package/dist/commands/remember.js +18 -57
  161. package/dist/commands/sources/add-cli.js +104 -49
  162. package/dist/commands/sources/bundle-cli.js +166 -0
  163. package/dist/commands/sources/bundle-config-ops.js +63 -0
  164. package/dist/commands/sources/info.js +27 -15
  165. package/dist/commands/sources/init.js +30 -40
  166. package/dist/commands/sources/installed-stashes.js +469 -172
  167. package/dist/commands/sources/migration-help.js +7 -4
  168. package/dist/commands/sources/schema-repair.js +10 -9
  169. package/dist/commands/sources/self-update.js +182 -121
  170. package/dist/commands/sources/source-add.js +169 -178
  171. package/dist/commands/sources/source-clone.js +144 -41
  172. package/dist/commands/sources/source-manage.js +94 -59
  173. package/dist/commands/sources/sources-cli.js +64 -205
  174. package/dist/commands/sources/stash-cli.js +91 -54
  175. package/dist/commands/sources/stash-skeleton.js +1 -1
  176. package/dist/commands/tasks/tasks-cli.js +106 -104
  177. package/dist/commands/tasks/tasks.js +445 -262
  178. package/dist/commands/workflow-cli.js +232 -121
  179. package/dist/core/action-contributors.js +1 -1
  180. package/dist/core/activation-policy.js +49 -0
  181. package/dist/core/adapter/adapters/agent-skills-adapter.js +181 -0
  182. package/dist/core/adapter/adapters/akm-adapter.js +528 -0
  183. package/dist/core/adapter/adapters/akm-lint.js +392 -0
  184. package/dist/core/adapter/adapters/akm-metadata.js +387 -0
  185. package/dist/core/adapter/adapters/akm-task-adapter.js +149 -0
  186. package/dist/core/adapter/adapters/akm-workflow-adapter.js +180 -0
  187. package/dist/core/adapter/adapters/claude-adapter.js +61 -0
  188. package/dist/core/adapter/adapters/dotenv-adapter.js +187 -0
  189. package/dist/core/adapter/adapters/generic-files-adapter.js +119 -0
  190. package/dist/core/adapter/adapters/index.js +80 -0
  191. package/dist/core/adapter/adapters/llm-wiki-adapter.js +419 -0
  192. package/dist/core/adapter/adapters/okf-adapter.js +391 -0
  193. package/dist/core/adapter/adapters/opencode-adapter.js +68 -0
  194. package/dist/core/adapter/adapters/shared.js +286 -0
  195. package/dist/core/adapter/adapters/tool-dir-shared.js +217 -0
  196. package/dist/core/adapter/adapters/website-snapshot-adapter.js +155 -0
  197. package/dist/core/adapter/bundle-adapter.js +4 -0
  198. package/dist/core/adapter/detect-adapter.js +17 -0
  199. package/dist/core/adapter/recognize-match.js +44 -0
  200. package/dist/core/adapter/registry.js +56 -0
  201. package/dist/core/adapter/types.js +4 -0
  202. package/dist/core/asset/akm-markdown.js +30 -0
  203. package/dist/core/asset/asset-placement.js +243 -0
  204. package/dist/core/asset/asset-ref.js +110 -79
  205. package/dist/core/asset/asset-serialize.js +20 -0
  206. package/dist/core/asset/frontmatter.js +28 -12
  207. package/dist/core/asset/markdown.js +40 -51
  208. package/dist/core/asset/resolve-ref.js +274 -0
  209. package/dist/core/asset/stash-meta.js +2 -2
  210. package/dist/core/bundle-id.js +51 -0
  211. package/dist/core/common.js +281 -86
  212. package/dist/core/config/config-io.js +42 -128
  213. package/dist/core/config/config-schema.js +233 -834
  214. package/dist/core/config/config-sources.js +162 -39
  215. package/dist/core/config/config-types.js +16 -11
  216. package/dist/core/config/config-version.js +29 -0
  217. package/dist/core/config/config-walker.js +126 -37
  218. package/dist/core/config/config.js +154 -331
  219. package/dist/core/config/deep-merge.js +41 -0
  220. package/dist/core/config/engine-semantics.js +28 -0
  221. package/dist/core/config/experimental.js +21 -0
  222. package/dist/core/config/schema/embedding.js +38 -0
  223. package/dist/core/config/schema/engines.js +116 -0
  224. package/dist/core/config/schema/experimental.js +47 -0
  225. package/dist/core/config/schema/feedback.js +31 -0
  226. package/dist/core/config/schema/improve-processes.js +389 -0
  227. package/dist/core/config/schema/improve.js +94 -0
  228. package/dist/core/config/schema/index-config.js +176 -0
  229. package/dist/core/config/schema/output.js +18 -0
  230. package/dist/core/config/schema/primitives.js +94 -0
  231. package/dist/core/config/schema/search.js +30 -0
  232. package/dist/core/config/schema/setup.js +18 -0
  233. package/dist/core/config/schema/sources-bundles.js +169 -0
  234. package/dist/core/config/schema/workflow.js +29 -0
  235. package/dist/core/env-secret-ref.js +155 -20
  236. package/dist/core/errors.js +17 -15
  237. package/dist/core/events-types.js +4 -0
  238. package/dist/core/events.js +46 -128
  239. package/dist/core/extra-params.js +62 -0
  240. package/dist/core/file-change.js +17 -0
  241. package/dist/core/file-lock.js +202 -57
  242. package/dist/core/fs-txn.js +392 -0
  243. package/dist/core/git-message.js +59 -0
  244. package/dist/core/improve-result.js +167 -0
  245. package/dist/core/json-schema.js +142 -0
  246. package/dist/core/lesson-lint.js +1 -17
  247. package/dist/core/logs-db.js +1 -1
  248. package/dist/core/maintenance-barrier.js +135 -0
  249. package/dist/core/migration-operation.js +44 -0
  250. package/dist/core/mutation-target.js +78 -0
  251. package/dist/core/paths.js +22 -25
  252. package/dist/core/platform.js +10 -0
  253. package/dist/core/recognition-util.js +128 -0
  254. package/dist/core/redaction.js +392 -0
  255. package/dist/core/standards/resolve-standards-context.js +36 -65
  256. package/dist/core/standards/resolve-stash-standards.js +2 -2
  257. package/dist/core/standards/resolve-type-conventions.js +5 -5
  258. package/dist/core/state/migrations.js +242 -11
  259. package/dist/core/state-db.js +98 -10
  260. package/dist/core/structured.js +1 -1
  261. package/dist/core/subprocess.js +303 -0
  262. package/dist/core/text-truncation.js +9 -5
  263. package/dist/core/time.js +20 -0
  264. package/dist/core/type-presentation.js +130 -0
  265. package/dist/core/warn.js +0 -3
  266. package/dist/core/write-source.js +834 -118
  267. package/dist/indexer/bundle-identity-guard.js +92 -0
  268. package/dist/indexer/db/graph-db.js +1 -25
  269. package/dist/indexer/db/llm-cache.js +1 -1
  270. package/dist/indexer/ensure-index.js +30 -9
  271. package/dist/indexer/graph/graph-boost.js +9 -30
  272. package/dist/indexer/graph/graph-extraction.js +41 -27
  273. package/dist/indexer/graph/graph-types.js +4 -0
  274. package/dist/indexer/index-writer-lock.js +93 -49
  275. package/dist/indexer/index-written-assets.js +100 -53
  276. package/dist/indexer/indexer.js +746 -329
  277. package/dist/indexer/init.js +18 -25
  278. package/dist/indexer/installations.js +142 -0
  279. package/dist/indexer/passes/dir-staleness.js +18 -10
  280. package/dist/indexer/passes/memory-inference.js +25 -15
  281. package/dist/indexer/passes/metadata.js +412 -243
  282. package/dist/indexer/scan/doc-to-entry.js +160 -0
  283. package/dist/indexer/scan/drain-dir.js +134 -0
  284. package/dist/indexer/search/db-search.js +292 -108
  285. package/dist/indexer/search/fts-query.js +64 -0
  286. package/dist/indexer/search/ranking-contributors.js +145 -25
  287. package/dist/indexer/search/ranking-types.js +4 -0
  288. package/dist/indexer/search/ranking.js +28 -71
  289. package/dist/indexer/search/search-attribution.js +67 -0
  290. package/dist/indexer/search/search-fields.js +18 -3
  291. package/dist/indexer/search/search-hit-enrichers.js +30 -40
  292. package/dist/indexer/search/search-source.js +157 -111
  293. package/dist/indexer/search/semantic-status.js +4 -1
  294. package/dist/indexer/usage/usage-events.js +10 -30
  295. package/dist/indexer/walk/file-context.js +3 -45
  296. package/dist/indexer/walk/matchers.js +42 -34
  297. package/dist/indexer/walk/path-resolver.js +11 -5
  298. package/dist/indexer/walk/walker.js +42 -14
  299. package/dist/integrations/agent/builder-shared.js +7 -0
  300. package/dist/integrations/agent/builders.js +5 -56
  301. package/dist/integrations/agent/config.js +3 -143
  302. package/dist/integrations/agent/detect.js +17 -2
  303. package/dist/integrations/agent/engine-resolution.js +231 -0
  304. package/dist/integrations/agent/index.js +1 -2
  305. package/dist/integrations/agent/model-aliases.js +16 -2
  306. package/dist/integrations/agent/profiles.js +36 -62
  307. package/dist/integrations/agent/prompts.js +46 -18
  308. package/dist/integrations/agent/runner-dispatch.js +93 -4
  309. package/dist/integrations/agent/runner.js +76 -208
  310. package/dist/integrations/agent/spawn.js +88 -196
  311. package/dist/integrations/harnesses/aider/agent-builder.js +114 -0
  312. package/dist/integrations/harnesses/aider/index.js +48 -0
  313. package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
  314. package/dist/integrations/harnesses/amazonq/agent-builder.js +147 -0
  315. package/dist/integrations/harnesses/amazonq/index.js +45 -0
  316. package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
  317. package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
  318. package/dist/integrations/harnesses/claude/config-import.js +1 -3
  319. package/dist/integrations/harnesses/claude/index.js +24 -35
  320. package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
  321. package/dist/integrations/harnesses/claude/session-log.js +27 -75
  322. package/dist/integrations/harnesses/codex/agent-builder.js +138 -0
  323. package/dist/integrations/harnesses/codex/index.js +52 -0
  324. package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
  325. package/dist/integrations/harnesses/copilot/agent-builder.js +122 -0
  326. package/dist/integrations/harnesses/copilot/index.js +48 -0
  327. package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
  328. package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
  329. package/dist/integrations/harnesses/gemini/index.js +48 -0
  330. package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
  331. package/dist/integrations/harnesses/ids.js +24 -0
  332. package/dist/integrations/harnesses/index.js +54 -34
  333. package/dist/integrations/harnesses/opencode/agent-builder.js +23 -5
  334. package/dist/integrations/harnesses/opencode/config-import.js +1 -3
  335. package/dist/integrations/harnesses/opencode/index.js +14 -32
  336. package/dist/integrations/harnesses/opencode/session-log.js +67 -125
  337. package/dist/integrations/harnesses/opencode-sdk/harness.js +51 -0
  338. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
  339. package/dist/integrations/harnesses/openhands/agent-builder.js +128 -0
  340. package/dist/integrations/harnesses/openhands/index.js +48 -0
  341. package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
  342. package/dist/integrations/harnesses/pi/agent-builder.js +97 -0
  343. package/dist/integrations/harnesses/pi/index.js +45 -0
  344. package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
  345. package/dist/integrations/harnesses/shared.js +17 -0
  346. package/dist/integrations/harnesses/types.js +43 -32
  347. package/dist/integrations/lockfile.js +211 -24
  348. package/dist/integrations/session-logs/index.js +36 -39
  349. package/dist/integrations/session-logs/provider-base.js +113 -0
  350. package/dist/llm/client.js +182 -110
  351. package/dist/llm/embedders/deterministic.js +2 -2
  352. package/dist/llm/embedders/remote.js +21 -9
  353. package/dist/llm/feature-gate.js +17 -57
  354. package/dist/llm/graph-extract.js +12 -13
  355. package/dist/llm/index-passes.js +8 -42
  356. package/dist/llm/memory-infer.js +144 -1
  357. package/dist/llm/metadata-enhance.js +45 -30
  358. package/dist/llm/structured-call.js +16 -8
  359. package/dist/llm/usage-persist.js +30 -5
  360. package/dist/llm/usage-telemetry.js +59 -6
  361. package/dist/output/cli-hints.js +1 -2
  362. package/dist/output/command-registry.js +27 -0
  363. package/dist/output/context.js +22 -7
  364. package/dist/output/format-exempt.js +80 -0
  365. package/dist/output/generic-render.js +251 -0
  366. package/dist/output/html-render.js +11 -16
  367. package/dist/output/render-registry.js +57 -0
  368. package/dist/output/renderers.js +14 -279
  369. package/dist/output/shapes/curate.js +10 -1
  370. package/dist/output/shapes/events.js +12 -7
  371. package/dist/output/shapes/helpers.js +58 -84
  372. package/dist/output/shapes/passthrough.js +11 -39
  373. package/dist/output/shapes/proposal/producer.js +15 -7
  374. package/dist/output/shapes/registry.js +12 -6
  375. package/dist/output/shapes.js +0 -9
  376. package/dist/output/text/{init.js → bundle-create.js} +3 -1
  377. package/dist/output/text/bundle-show.js +7 -0
  378. package/dist/output/text/command-format.js +562 -0
  379. package/dist/output/text/env.js +1 -3
  380. package/dist/output/text/events.js +8 -7
  381. package/dist/output/text/helpers.js +15 -1164
  382. package/dist/output/text/proposal/producer.js +4 -2
  383. package/dist/output/text/proposal-format.js +202 -0
  384. package/dist/output/text/registry-commands.js +1 -2
  385. package/dist/output/text/registry.js +12 -6
  386. package/dist/output/text/show-directives.js +117 -0
  387. package/dist/output/text/show-format.js +103 -0
  388. package/dist/output/text/sync.js +5 -0
  389. package/dist/output/text/workflow-format.js +332 -0
  390. package/dist/output/text/workflow.js +3 -2
  391. package/dist/output/text.js +10 -19
  392. package/dist/registry/factory.js +4 -6
  393. package/dist/registry/origin-resolve.js +16 -27
  394. package/dist/registry/providers/skills-sh.js +3 -3
  395. package/dist/registry/providers/static-index.js +15 -25
  396. package/dist/registry/resolve.js +43 -94
  397. package/dist/registry/semver.js +43 -0
  398. package/dist/runtime.js +81 -12
  399. package/dist/scripts/akm-migrate.js +35529 -0
  400. package/dist/setup/detect.js +5 -7
  401. package/dist/setup/detected-engines.js +136 -0
  402. package/dist/setup/engine-config.js +100 -0
  403. package/dist/setup/registry-stash-loader.js +3 -3
  404. package/dist/setup/semantic-assets.js +12 -9
  405. package/dist/setup/setup.js +444 -208
  406. package/dist/setup/steps/connection-shared.js +120 -0
  407. package/dist/setup/steps/connection.js +108 -305
  408. package/dist/setup/steps/platforms.js +13 -12
  409. package/dist/setup/steps/semantic.js +15 -3
  410. package/dist/setup/steps/sources.js +21 -15
  411. package/dist/setup/steps/stashdir.js +6 -4
  412. package/dist/setup/steps/tasks.js +236 -119
  413. package/dist/setup/steps.js +3 -2
  414. package/dist/sources/freshness.js +39 -0
  415. package/dist/sources/provider-factory.js +11 -17
  416. package/dist/sources/providers/filesystem.js +2 -3
  417. package/dist/sources/providers/git-install.js +278 -34
  418. package/dist/sources/providers/git-provider.js +54 -56
  419. package/dist/sources/providers/git-stash.js +420 -91
  420. package/dist/sources/providers/git.js +2 -2
  421. package/dist/sources/providers/npm.js +16 -19
  422. package/dist/sources/providers/provider-utils.js +47 -22
  423. package/dist/sources/providers/sync-from-ref.js +3 -9
  424. package/dist/sources/providers/website.js +2 -2
  425. package/dist/sources/resolve.js +11 -10
  426. package/dist/sources/snapshot-fetchers/types.js +4 -0
  427. package/dist/sources/{website-ingest.js → snapshot-fetchers/website-ingest.js} +110 -41
  428. package/dist/storage/database.js +60 -4
  429. package/dist/storage/engines/sqlite-migrations.js +156 -5
  430. package/dist/storage/locations.js +1 -2
  431. package/dist/storage/repositories/canaries-repository.js +1 -1
  432. package/dist/storage/repositories/events-repository.js +51 -11
  433. package/dist/storage/repositories/improve-runs-repository.js +6 -32
  434. package/dist/storage/repositories/index-connection.js +79 -0
  435. package/dist/storage/repositories/index-db.js +4 -3
  436. package/dist/storage/repositories/index-entries-repository.js +863 -0
  437. package/dist/{indexer/db/entry-mapper.js → storage/repositories/index-entry-mapper.js} +19 -2
  438. package/dist/storage/repositories/index-entry-types.js +4 -0
  439. package/dist/storage/repositories/index-fts-repository.js +167 -0
  440. package/dist/storage/repositories/index-llm-cache-repository.js +108 -0
  441. package/dist/storage/repositories/index-meta-repository.js +49 -0
  442. package/dist/{indexer/db/schema.js → storage/repositories/index-schema.js} +226 -100
  443. package/dist/storage/repositories/index-sql.js +12 -0
  444. package/dist/storage/repositories/index-utility-repository.js +356 -0
  445. package/dist/storage/repositories/index-vec-repository.js +250 -0
  446. package/dist/storage/repositories/outcome-repository.js +119 -0
  447. package/dist/storage/repositories/proposals-repository.js +317 -75
  448. package/dist/storage/repositories/registry-cache.js +1 -1
  449. package/dist/storage/repositories/salience-repository.js +172 -0
  450. package/dist/storage/repositories/task-history-repository.js +110 -3
  451. package/dist/storage/repositories/workflow-runs-repository.js +240 -19
  452. package/dist/tasks/backends/cron.js +169 -46
  453. package/dist/tasks/backends/exec-utils.js +76 -3
  454. package/dist/tasks/backends/index.js +6 -9
  455. package/dist/tasks/backends/launchd.js +292 -55
  456. package/dist/tasks/backends/schtasks.js +557 -70
  457. package/dist/tasks/backends/types.js +4 -0
  458. package/dist/tasks/command-executable.js +93 -0
  459. package/dist/tasks/embedded.js +56 -38
  460. package/dist/tasks/parser.js +156 -64
  461. package/dist/tasks/resolve-akm-bin.js +144 -51
  462. package/dist/tasks/runner.js +377 -209
  463. package/dist/tasks/schedule.js +108 -19
  464. package/dist/tasks/scheduler-invocation.js +296 -0
  465. package/dist/tasks/schema.js +1 -1
  466. package/dist/tasks/task-id.js +35 -0
  467. package/dist/tasks/validator.js +30 -16
  468. package/dist/text-import-hook.mjs +1 -1
  469. package/dist/workflows/authoring/authoring.js +104 -43
  470. package/dist/workflows/authoring/scope-key.js +1 -1
  471. package/dist/workflows/cli.js +0 -16
  472. package/dist/workflows/concurrency-policy.js +15 -0
  473. package/dist/workflows/exec/brief.js +450 -0
  474. package/dist/workflows/exec/frozen-judge.js +47 -0
  475. package/dist/workflows/exec/native-executor.js +1038 -0
  476. package/dist/workflows/exec/param-secrets.js +115 -0
  477. package/dist/workflows/exec/report.js +1460 -0
  478. package/dist/workflows/exec/run-workflow.js +602 -0
  479. package/dist/workflows/exec/scheduler.js +71 -0
  480. package/dist/workflows/exec/step-work.js +1190 -0
  481. package/dist/workflows/exec/unit-writer.js +23 -0
  482. package/dist/workflows/exec/workflow-engine-gate.js +67 -0
  483. package/dist/workflows/exec/worktree.js +171 -0
  484. package/dist/workflows/ir/compile.js +246 -0
  485. package/dist/workflows/ir/freeze.js +233 -0
  486. package/dist/workflows/ir/params.js +54 -0
  487. package/dist/workflows/ir/plan-hash.js +68 -0
  488. package/dist/workflows/ir/schema.js +540 -0
  489. package/dist/workflows/parser.js +878 -304
  490. package/dist/workflows/program/expressions.js +181 -0
  491. package/dist/workflows/program/schema.js +51 -0
  492. package/dist/workflows/renderer.js +100 -45
  493. package/dist/workflows/resource-limits.js +22 -0
  494. package/dist/workflows/runtime/agent-identity.js +59 -14
  495. package/dist/workflows/runtime/checkin.js +1 -1
  496. package/dist/workflows/runtime/plan-classifier.js +131 -0
  497. package/dist/workflows/runtime/runs.js +376 -119
  498. package/dist/workflows/runtime/unit-checkin.js +45 -0
  499. package/dist/workflows/runtime/unit-phases.js +20 -0
  500. package/dist/workflows/runtime/workflow-asset-loader.js +241 -40
  501. package/dist/workflows/schema.js +1 -11
  502. package/dist/workflows/validate-summary.js +2 -3
  503. package/dist/workflows/validator.js +52 -30
  504. package/docs/README.md +42 -78
  505. package/docs/migration/README.md +8 -0
  506. package/docs/migration/release-notes/0.6.0.md +1 -1
  507. package/docs/migration/release-notes/0.7.0.md +9 -8
  508. package/docs/migration/release-notes/0.9.0.md +158 -14
  509. package/docs/migration/v0.7-to-v0.8.md +46 -47
  510. package/docs/migration/v0.8-to-v0.9.md +844 -0
  511. package/docs/reference/README.md +12 -0
  512. package/docs/reference/data-and-telemetry.md +333 -0
  513. package/package.json +21 -17
  514. package/schemas/akm-asset-envelope.json +93 -0
  515. package/schemas/akm-config.json +4636 -0
  516. package/schemas/akm-task.json +87 -0
  517. package/schemas/akm-workflow.json +373 -0
  518. package/dist/akm-migrate-storage +0 -38
  519. package/dist/assets/help/help-accept.md +0 -12
  520. package/dist/assets/help/help-improve.md +0 -84
  521. package/dist/assets/help/help-proposals.md +0 -17
  522. package/dist/assets/help/help-propose.md +0 -17
  523. package/dist/assets/help/help-reject.md +0 -11
  524. package/dist/assets/profiles/frequent.json +0 -13
  525. package/dist/assets/profiles/recombine-only.json +0 -21
  526. package/dist/assets/profiles/reflect-distill.json +0 -30
  527. package/dist/assets/profiles/synthesize.json +0 -15
  528. package/dist/assets/prompts/procedural-system.md +0 -44
  529. package/dist/assets/prompts/recombine-system.md +0 -40
  530. package/dist/assets/prompts/staleness-detect-system.md +0 -6
  531. package/dist/assets/tasks/core/backup.yml +0 -4
  532. package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
  533. package/dist/assets/templates/html/default.html +0 -78
  534. package/dist/assets/templates/html/vendor/echarts.min.js +0 -45
  535. package/dist/assets/wiki/index-template.md +0 -12
  536. package/dist/assets/wiki/ingest-workflow-template.md +0 -83
  537. package/dist/assets/wiki/log-template.md +0 -8
  538. package/dist/assets/wiki/schema-template.md +0 -61
  539. package/dist/cli/config-migrate.js +0 -150
  540. package/dist/cli/config-validate.js +0 -39
  541. package/dist/commands/graph/graph-cli.js +0 -124
  542. package/dist/commands/graph/graph.js +0 -487
  543. package/dist/commands/improve/calibration.js +0 -161
  544. package/dist/commands/improve/dedup.js +0 -482
  545. package/dist/commands/improve/extract-watch.js +0 -140
  546. package/dist/commands/improve/hot-probation.js +0 -45
  547. package/dist/commands/improve/improve-auto-accept.js +0 -276
  548. package/dist/commands/improve/improve-profiles.js +0 -168
  549. package/dist/commands/improve/procedural.js +0 -398
  550. package/dist/commands/improve/recombine.js +0 -818
  551. package/dist/commands/improve/schema-similarity-gate.js +0 -168
  552. package/dist/commands/lint/agent-linter.js +0 -44
  553. package/dist/commands/lint/command-linter.js +0 -44
  554. package/dist/commands/lint/default-linter.js +0 -16
  555. package/dist/commands/lint/fact-linter.js +0 -39
  556. package/dist/commands/lint/knowledge-linter.js +0 -16
  557. package/dist/commands/lint/memory-linter.js +0 -61
  558. package/dist/commands/lint/registry.js +0 -41
  559. package/dist/commands/lint/skill-linter.js +0 -45
  560. package/dist/commands/lint/task-linter.js +0 -50
  561. package/dist/commands/lint/workflow-linter.js +0 -81
  562. package/dist/commands/proposal/legacy-import.js +0 -115
  563. package/dist/commands/sources/history.js +0 -196
  564. package/dist/commands/tasks/default-tasks.js +0 -186
  565. package/dist/commands/wiki-cli.js +0 -292
  566. package/dist/core/asset/asset-registry.js +0 -76
  567. package/dist/core/asset/asset-spec.js +0 -259
  568. package/dist/core/config/config-migration.js +0 -602
  569. package/dist/core/deep-merge.js +0 -38
  570. package/dist/core/eval/rank-metrics.js +0 -113
  571. package/dist/core/ripgrep/install.js +0 -163
  572. package/dist/core/ripgrep/resolve.js +0 -81
  573. package/dist/indexer/db/db.js +0 -1413
  574. package/dist/indexer/manifest.js +0 -170
  575. package/dist/indexer/passes/metadata-contributors.js +0 -31
  576. package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -94
  577. package/dist/integrations/harnesses/opencode-sdk/index.js +0 -49
  578. package/dist/llm/call-ai.js +0 -62
  579. package/dist/llm/memory-infer-impl.js +0 -138
  580. package/dist/output/shapes/distill.js +0 -14
  581. package/dist/output/shapes/history.js +0 -11
  582. package/dist/output/text/distill.js +0 -6
  583. package/dist/output/text/enable-disable.js +0 -8
  584. package/dist/output/text/history.js +0 -6
  585. package/dist/output/text/wiki.js +0 -16
  586. package/dist/registry/build-index.js +0 -386
  587. package/dist/scripts/migrate-storage.js +0 -19108
  588. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +0 -9411
  589. package/dist/scripts/migrations/v16-to-v17.js +0 -141
  590. package/dist/setup/legacy-config.js +0 -106
  591. package/dist/storage/repositories/consolidation-repository.js +0 -38
  592. package/dist/storage/repositories/recombine-repository.js +0 -213
  593. package/dist/wiki/wiki-templates.js +0 -15
  594. package/dist/wiki/wiki.js +0 -1012
  595. package/dist/workflows/db.js +0 -215
  596. package/docs/data-and-telemetry.md +0 -226
  597. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/registry.js +0 -0
  598. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/youtube.js +0 -0
@@ -0,0 +1,1190 @@
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
+ * Shared step semantics — the ONE implementation of a step's orchestration
6
+ * decisions, consumed by BOTH the engine loop (`run-workflow.ts` +
7
+ * `native-executor.ts`) and, from R3 on, the harness-neutral driver protocol
8
+ * (`workflow brief` / `workflow report`). The cardinal rule of the driver
9
+ * protocol (redesign addendum R3) is *no duplicated semantics*: work-list
10
+ * computation, prompt assembly, reducer/artifact promotion, output-schema
11
+ * validation, artifact-judged gate summaries, gate-feedback recovery, and
12
+ * route evaluation live here so an engine-driven run and a brief/report-driven
13
+ * run of the same frozen plan produce byte-identical unit graphs.
14
+ *
15
+ * ## What is PURE here
16
+ *
17
+ * {@link computeStepWorkList} — given the frozen step plan and a
18
+ * {@link WorkListInput} (params, prior step outputs, gate-loop number + its
19
+ * recovered feedback) — is a pure function: same inputs ⇒ same unit ids, input
20
+ * hashes, and fully-resolved prompts. It takes NO clock, NO IO, and NO journal
21
+ * (journal-derived state, i.e. the recovered gate feedback, is passed in). This
22
+ * is the load-bearing guarantee that `brief` can predict exactly the units the
23
+ * engine would dispatch. So are the reducer/artifact helpers
24
+ * ({@link buildEvidence}, {@link projectStepOutput}, {@link validateStepArtifact},
25
+ * {@link buildArtifactSummary}), the gate-feedback recovery
26
+ * ({@link recoverGateFeedback} / {@link activeGateLoop}), and route evaluation
27
+ * ({@link evaluateRoute} and its bookkeeping).
28
+ *
29
+ * ## What does IO here
30
+ *
31
+ * The gate-evaluation journaling ({@link journalGateEvaluationStart} /
32
+ * {@link journalGateEvaluationFinish}) writes `workflow_run_units` rows through
33
+ * the serialized writer queue — an engine-driven judge call is an LLM call and
34
+ * is journaled like a unit. It lives here (not in the engine loop) so the
35
+ * report path journals gate evaluations through the identical writer.
36
+ *
37
+ * This module NEVER dispatches a unit and NEVER writes step rows: dispatch is
38
+ * the executor's job (`native-executor.ts`), advancing the gated spine is the
39
+ * engine loop's job (`run-workflow.ts` via `completeWorkflowStep`).
40
+ */
41
+ import { createHash } from "node:crypto";
42
+ import unitPreambleTemplate from "../../assets/prompts/workflow-unit-preamble.md" with { type: "text" };
43
+ import { UsageError } from "../../core/errors.js";
44
+ import { appendEvent } from "../../core/events.js";
45
+ import { validateJsonSchemaSubset } from "../../core/json-schema.js";
46
+ import { withWorkflowRunsRepo } from "../../storage/repositories/workflow-runs-repository.js";
47
+ import { canonicalJson as canonicalJsonString } from "../ir/plan-hash.js";
48
+ import { resolveReferenceString } from "../program/expressions.js";
49
+ import { WORKFLOW_MAX_MAP_EXPANSION } from "../resource-limits.js";
50
+ import { requireExecutableWorkflowPlan } from "../runtime/plan-classifier.js";
51
+ import { completeWorkflowStep } from "../runtime/runs.js";
52
+ import { GATE_EVALUATION_PHASE } from "../runtime/unit-phases.js";
53
+ import { enqueueUnitWrite } from "./unit-writer.js";
54
+ /**
55
+ * Default per-unit timeout. Deliberately NOT the 60 s agent default
56
+ * (`DEFAULT_AGENT_TIMEOUT_MS`) — workflow units routinely run real coding
57
+ * tasks on slow local models; 10 minutes matches the LLM-path default
58
+ * (`tryLlmFeature`). A unit's `timeout` declaration overrides this; `none`
59
+ * disables.
60
+ */
61
+ export const DEFAULT_UNIT_TIMEOUT_MS = 600_000;
62
+ /** How much raw unit output is retained in step evidence (full text lives on the unit row). */
63
+ const EVIDENCE_TEXT_CLIP = 2_000;
64
+ /** How much artifact JSON the completion-criteria judge receives (addendum R2, artifact-judging gates). */
65
+ const GATE_ARTIFACT_CLIP = 4_000;
66
+ /**
67
+ * Compute a step's expected work-list PURELY from the frozen plan and its
68
+ * inputs: resolve the fan-out list, derive content-derived unit ids, assemble
69
+ * each unit's prompt (preamble + interpolated instructions + gate feedback +
70
+ * schema directive), and hash the resolved input. Same inputs ⇒ byte-identical
71
+ * ids/hashes/prompts — the invariant `brief` relies on to predict the engine.
72
+ *
73
+ * Whole-list failures (missing subgraph, unresolvable / non-array `over`,
74
+ * null or duplicate fan-out items) return `{ ok: false }`. The per-unit
75
+ * `resolved: { ok: false }` branch is STRUCTURALLY UNREACHABLE in the unified
76
+ * format — prose is never scanned for references, and everything that CAN
77
+ * fail (map.over / route.input / inputs:) resolves once per step, failing the
78
+ * whole list above. The branch is retained because brief/report/executor all
79
+ * share the shape and defensively handle it; if a future unit kind
80
+ * reintroduces per-unit resolution (e.g. an exec/shell unit with real
81
+ * substitution), the cross-surface failure plumbing is already in place.
82
+ */
83
+ /**
84
+ * Validate a fan-out item list BEFORE any identity/dispatch work: expansion
85
+ * within the resource limit, no null/undefined items, no canonical duplicates.
86
+ * Returns the failure message, or undefined when the list is dispatchable.
87
+ *
88
+ * Null items: producer garbage — there is nothing to hand the unit as its work
89
+ * item. The pre-unification format rejected them incidentally (substituting
90
+ * `${{ item }}` failed); with items attached as context instead of spliced,
91
+ * nothing later would stop a unit from being dispatched with "Item: null", so
92
+ * the rejection is explicit here. Duplicates: content-derived unit identity
93
+ * makes canonical duplicates collide on id — an authoring error caught
94
+ * deterministically, before dispatch.
95
+ */
96
+ function validateFanOutItems(stepId, items) {
97
+ if (items.length > WORKFLOW_MAX_MAP_EXPANSION) {
98
+ return `Step "${stepId}" fan-out expands to ${items.length} units, exceeding the ${WORKFLOW_MAX_MAP_EXPANSION}-unit resource limit.`;
99
+ }
100
+ const nullIndex = items.findIndex((item) => item === null || item === undefined);
101
+ if (nullIndex !== -1) {
102
+ return (`Step "${stepId}" fan-out list contains a null item (index ${nullIndex}). ` +
103
+ `Every item must be a concrete value — fix the producing step's output.`);
104
+ }
105
+ const firstIndexByCanonical = new Map();
106
+ for (let i = 0; i < items.length; i++) {
107
+ const canonical = canonicalJson(items[i]) ?? "null";
108
+ const firstIndex = firstIndexByCanonical.get(canonical);
109
+ if (firstIndex !== undefined) {
110
+ return (`Step "${stepId}" fan-out list contains duplicate items (indices ${firstIndex} and ${i}: ` +
111
+ `${clip(canonical, 200)}). Content-derived unit identity requires distinct items — ` +
112
+ `deduplicate the list this workflow fans out over.`);
113
+ }
114
+ firstIndexByCanonical.set(canonical, i);
115
+ }
116
+ return undefined;
117
+ }
118
+ export function computeStepWorkList(plan, input) {
119
+ const root = plan.root;
120
+ // Route-only steps (YAML `route:`) carry no execution subgraph.
121
+ if (!root) {
122
+ return {
123
+ ok: false,
124
+ error: `Step "${plan.stepId}" has no execution subgraph (a route-only step); the native executor cannot dispatch it.`,
125
+ };
126
+ }
127
+ const template = root.kind === "map" ? root.template : root;
128
+ const reducer = root.kind === "map" ? root.reducer : "collect";
129
+ const scope = { params: input.params, stepOutputs: input.stepOutputs };
130
+ // Instructions are ALWAYS the step's body prose, byte-exact — never
131
+ // templated, never scanned for reference syntax (workflow-format-
132
+ // unification, spec §2.3). Only `map.over` / `route.input` / `inputs[]`
133
+ // carry the closed reference grammar.
134
+ // Resolve the step's declared `inputs:` ONCE (shared by every unit in this
135
+ // step — map items differ, declared inputs do not): prior-step artifacts
136
+ // attached to every dispatched unit as structured context.
137
+ const resolvedInputs = [];
138
+ for (const reference of template.inputs ?? []) {
139
+ const resolved = resolveReferenceString(reference, scope);
140
+ if (!resolved.ok) {
141
+ return {
142
+ ok: false,
143
+ error: `Step "${plan.stepId}" declared input "${reference}" failed to resolve: ${resolved.error.message}`,
144
+ };
145
+ }
146
+ resolvedInputs.push({ reference, value: resolved.value });
147
+ }
148
+ // Resolve fan-out items: `over` is a single whole-value reference naming
149
+ // its producer explicitly — no ambient key search.
150
+ let items;
151
+ if (root.kind === "map") {
152
+ const source = resolveReferenceString(root.over, scope);
153
+ if (!source.ok) {
154
+ return {
155
+ ok: false,
156
+ error: `Step "${plan.stepId}" fan-out "over" (${root.over}) failed to resolve: ${source.error.message}`,
157
+ };
158
+ }
159
+ if (!Array.isArray(source.value)) {
160
+ return {
161
+ ok: false,
162
+ error: `Step "${plan.stepId}" fan-out "over" (${root.over}) resolved to ${typeof source.value}, not an array.`,
163
+ };
164
+ }
165
+ items = source.value;
166
+ }
167
+ else {
168
+ items = [undefined];
169
+ }
170
+ const isFanOut = root.kind === "map";
171
+ const fanOutProblem = isFanOut ? validateFanOutItems(plan.stepId, items) : undefined;
172
+ if (fanOutProblem)
173
+ return { ok: false, error: fanOutProblem };
174
+ // Content-derived unit identity: compute every id up front (duplicate items
175
+ // were rejected above — identity requires distinct items).
176
+ const unitIds = items.map((item) => unitIdFor(template.id, item, isFanOut));
177
+ const gateLoop = input.gateLoop ?? 1;
178
+ const frozenInvocation = template.invocation;
179
+ if (!frozenInvocation)
180
+ return { ok: false, error: `Step "${plan.stepId}" has no frozen invocation.` };
181
+ const frozenEngine = input.engines?.[frozenInvocation.engine];
182
+ if (!frozenEngine) {
183
+ return { ok: false, error: `Step "${plan.stepId}" references missing frozen engine "${frozenInvocation.engine}".` };
184
+ }
185
+ const runner = frozenEngine.kind === "llm" ? "llm" : frozenEngine.runnerKind;
186
+ const timeoutMs = frozenInvocation.timeoutMs;
187
+ const units = items.map((item, index) => {
188
+ const unitId = unitIds[index];
189
+ // Gate loops (>= 2) journal under `<unitId>~l<loop>` so loop 1's rows are
190
+ // never clobbered; the content-derived identity (and the prompt's
191
+ // {{UNIT_ID}}) stays the base id.
192
+ const journalBaseId = gateLoop > 1 ? `${unitId}~l${gateLoop}` : unitId;
193
+ // Context attachment (workflow-format-unification, spec §4): every unit
194
+ // receives the run params (already in the preamble), its item + index if
195
+ // it is a map unit, and the artifacts named by its step's `inputs:`.
196
+ // Instructions reach the unit byte-exact — never interpolated.
197
+ const prompt = buildUnitPrompt({
198
+ runId: input.runId,
199
+ stepId: plan.stepId,
200
+ unitId,
201
+ params: input.params,
202
+ ...(isFanOut ? { item, itemIndex: index } : {}),
203
+ ...(resolvedInputs.length > 0 ? { inputs: resolvedInputs } : {}),
204
+ ...(input.gateFeedback ? { gateFeedback: input.gateFeedback } : {}),
205
+ ...(template.schema ? { schema: template.schema } : {}),
206
+ instructions: template.instructions,
207
+ });
208
+ // Canonical dispatch-input envelope (reviewer finding #1). Every field
209
+ // here is a PLAN-FROZEN input that changes what the backend is actually
210
+ // asked to do, so a completed unit is reused ONLY when all of them match;
211
+ // a change to any of them re-dispatches. Key order is FIXED — it is the
212
+ // hash preimage (JSON.stringify preserves insertion order) — and shared
213
+ // by ALL surfaces, since this is the ONE place a unit's inputHash is
214
+ // computed (engine, brief, and report all call computeStepWorkList), so
215
+ // the byte-identical hash across surfaces is structural, not coincidental.
216
+ //
217
+ // Unit identity (workflow-format-unification, spec §2.3/§4) hashes the
218
+ // FROZEN TEMPLATE BYTES (`template.instructions`, byte-exact, never an
219
+ // instantiated/interpolated string) + the canonical item JSON + the
220
+ // declared-input artifact hashes + the params snapshot — instead of a
221
+ // resolved/spliced prompt string, since there is no more splicing. The
222
+ // assembled `prompt` above is what the harness SEES; the hash is over the
223
+ // plan-frozen INPUTS that determine it, which is the same replay contract
224
+ // the old resolved-prompt hash gave (same inputs ⇒ same hash) with the
225
+ // interpolation step removed.
226
+ //
227
+ // Included beyond the R4 baseline (template/runner/model/schema): resolved
228
+ // timeoutMs, the env asset ref NAMES, and isolation — each reaches
229
+ // dispatch (native-executor's UnitDispatchRequest) and a changed one
230
+ // yields a materially different call. `env` carries NAMES ONLY, never
231
+ // resolved values: hashing a resolved secret would leak it into a durable
232
+ // hash oracle and would spuriously re-dispatch on every secret rotation.
233
+ // `retry`/`onError` are DELIBERATELY excluded — they govern failed-unit
234
+ // re-dispatch and step-level failure reduction, not a COMPLETED unit's
235
+ // inputs/output, so a completed row stays valid across policy changes.
236
+ //
237
+ // `gateFeedback` IS included (conditionally, so a no-feedback unit's
238
+ // preimage is byte-identical to before): it is appended to the prompt by
239
+ // `buildUnitPrompt`, so a gate loop's retry is materially a different ask
240
+ // than the rejected attempt — omitting it made loop 1 and loop 2 journal
241
+ // identical hashes for different prompts, breaking the "changed inputs ⇒
242
+ // changed hash" audit contract. Replay-safe: feedback is re-derived from
243
+ // the journaled gate decision, so a resumed retry re-hashes identically.
244
+ //
245
+ // Ambient config is DELIBERATELY excluded — the model-alias table, the
246
+ // resolved backend/connection, and the working directory (`ctx.workDir` /
247
+ // process.cwd()) are NOT plan-frozen. The frozen plan is the identity
248
+ // boundary (redesign addendum determinism bar #2): config drift under an
249
+ // in-flight run is out of scope by design.
250
+ const dispatch = transitiveDispatchSnapshot(frozenEngine, input.engines ?? {});
251
+ const inputHash = createHash("sha256")
252
+ .update(canonicalJsonString({
253
+ hashVersion: 4,
254
+ template: template.instructions,
255
+ item: isFanOut ? (item ?? null) : null,
256
+ inputs: resolvedInputs,
257
+ params: input.params,
258
+ dispatch,
259
+ invocation: frozenInvocation,
260
+ schema: template.schema ?? null,
261
+ env: template.env ?? null,
262
+ isolation: template.isolation ?? "none",
263
+ ...(input.gateFeedback ? { gateFeedback: input.gateFeedback } : {}),
264
+ }))
265
+ .digest("hex");
266
+ const resolved = { ok: true, prompt, inputHash };
267
+ return {
268
+ unitId,
269
+ nodeId: template.id,
270
+ index,
271
+ item,
272
+ isFanOut,
273
+ journalBaseId,
274
+ runner,
275
+ engine: frozenEngine,
276
+ ...(frozenEngine?.kind === "agent" &&
277
+ frozenEngine.fallbackLlmEngine &&
278
+ input.engines?.[frozenEngine.fallbackLlmEngine]?.kind === "llm"
279
+ ? {
280
+ fallbackEngine: input.engines[frozenEngine.fallbackLlmEngine],
281
+ }
282
+ : {}),
283
+ invocation: frozenInvocation,
284
+ ...(frozenInvocation.model ? { model: frozenInvocation.model } : {}),
285
+ timeoutMs,
286
+ ...(template.schema ? { schema: template.schema } : {}),
287
+ ...(template.env ? { env: template.env } : {}),
288
+ ...(template.retry ? { retry: template.retry } : {}),
289
+ onError: template.onError,
290
+ ...(template.isolation ? { isolation: template.isolation } : {}),
291
+ resolved,
292
+ };
293
+ });
294
+ const concurrency = root.kind === "map" ? root.concurrency : 1;
295
+ return {
296
+ ok: true,
297
+ list: { template, reducer, isFanOut, ...(concurrency !== undefined ? { concurrency } : {}), items, units },
298
+ };
299
+ }
300
+ /**
301
+ * Assemble the final prompt: engine preamble (run params + item/index +
302
+ * declared-input artifacts, all as structured JSON context) + the step's
303
+ * BYTE-EXACT prose instructions (+ gate feedback on loop re-executions, +
304
+ * schema directive). Instructions are NEVER interpolated (workflow-format-
305
+ * unification, spec §2.3) — data reaches the unit as attached context, not
306
+ * string splices; only the ENGINE's own preamble placeholders are substituted
307
+ * here.
308
+ */
309
+ export function buildUnitPrompt(input) {
310
+ const { runId, stepId, unitId, params, itemIndex, item, inputs, gateFeedback, schema, instructions } = input;
311
+ // Function replacements throughout: a string replacement would interpret
312
+ // GetSubstitution patterns ($&, $$, $', $`) inside VALUES and silently
313
+ // corrupt the prompt (e.g. a param value containing "$&").
314
+ const preamble = unitPreambleTemplate
315
+ .replaceAll("{{RUN_ID}}", () => runId)
316
+ .replaceAll("{{STEP_ID}}", () => stepId)
317
+ .replaceAll("{{UNIT_ID}}", () => unitId)
318
+ .replaceAll("{{PARAMS_JSON}}", () => safeJson(params));
319
+ // Map-unit context: the item this unit was given, plus its index. Attached
320
+ // as structured JSON — the engine never splices it into the instructions.
321
+ const itemBlock = itemIndex !== undefined
322
+ ? `\n\n## Item (index ${itemIndex})\nYou were given this item from the fan-out list:\n${safeJson(item)}`
323
+ : "";
324
+ // Declared `inputs:` context: the prior-step artifacts this step named.
325
+ const inputsBlock = inputs && inputs.length > 0
326
+ ? `\n\n## Declared inputs\n${inputs.map((i) => `### ${i.reference}\n${safeJson(i.value)}`).join("\n\n")}`
327
+ : "";
328
+ // Gate-loop feedback (R2 max_loops): the judge's rejection is appended so
329
+ // the re-executed unit can address it — and so the input hash changes,
330
+ // making the loop's re-dispatch natural instead of a durable-row reuse.
331
+ const gateBlock = gateFeedback
332
+ ? `\n\n## Completion-gate feedback (previous attempt rejected)\n` +
333
+ `A completion-criteria judge rejected this step's previous results. Address this feedback:\n` +
334
+ gateFeedback.feedback +
335
+ (gateFeedback.missing.length > 0
336
+ ? `\nUnmet criteria:\n${gateFeedback.missing.map((m) => `- ${m}`).join("\n")}`
337
+ : "")
338
+ : "";
339
+ const schemaDirective = schema
340
+ ? `\n\nRespond with ONLY a JSON value matching this JSON Schema (no prose, no code fences):\n${safeJson(schema)}`
341
+ : "";
342
+ return `${preamble}\n${instructions}${itemBlock}${inputsBlock}${gateBlock}${schemaDirective}`;
343
+ }
344
+ /**
345
+ * Content-derived unit identity (module doc): `<node_id>:<hash12>` for a
346
+ * fan-out item, `<node_id>:solo` otherwise. The hash is over the item's
347
+ * canonical JSON (sorted keys — same canonicalization the vote reducer
348
+ * counts with), so identity survives list reordering/regeneration and is
349
+ * independent of item position. Retry attempts stack `~r<n>` on top.
350
+ */
351
+ export function unitIdFor(nodeId, item, isFanOut) {
352
+ if (!isFanOut)
353
+ return `${nodeId}:solo`;
354
+ const canonical = canonicalJson(item) ?? "null";
355
+ return `${nodeId}:${createHash("sha256").update(canonical).digest("hex").slice(0, 12)}`;
356
+ }
357
+ /** Include an SDK fallback in v3 call identity without copying catalog entries onto nodes. */
358
+ function transitiveDispatchSnapshot(engine, engines) {
359
+ if (engine.kind !== "agent" || !engine.fallbackLlmEngine)
360
+ return engine;
361
+ const fallback = engines[engine.fallbackLlmEngine];
362
+ if (!fallback || fallback.kind !== "llm") {
363
+ throw new UsageError(`Frozen agent engine "${engine.name}" has no valid LLM fallback snapshot.`);
364
+ }
365
+ return { engine, fallback };
366
+ }
367
+ // ── Step outputs + reducers + typed artifacts ────────────────────────────────
368
+ /**
369
+ * The value `${{ steps.<id>.output }}` resolves to for ONE step, given that
370
+ * step's journaled evidence: an engine-executed step carries a promoted
371
+ * ARTIFACT under `evidence.output` (solo unit result/text, collect array, or
372
+ * vote winner); evidence without an `output` key (manually-completed steps) is
373
+ * exposed as-is.
374
+ */
375
+ export function projectStepOutput(evidence) {
376
+ return Object.hasOwn(evidence, "output") ? evidence.output : evidence;
377
+ }
378
+ /** Project the engine's evidence map into the expression scope's `stepOutputs`. */
379
+ export function stepOutputsFromEvidence(evidence) {
380
+ const outputs = {};
381
+ for (const [stepId, stepEvidence] of Object.entries(evidence)) {
382
+ if (stepEvidence !== undefined)
383
+ outputs[stepId] = projectStepOutput(stepEvidence);
384
+ }
385
+ return outputs;
386
+ }
387
+ /**
388
+ * Typed artifacts (addendum, R2): validate the promoted step artifact against
389
+ * `IrStepPlan.outputSchema`. Returns the step-failure summary (validation
390
+ * errors included) on mismatch, undefined when valid or when no schema is
391
+ * declared.
392
+ */
393
+ export function validateStepArtifact(plan, evidence) {
394
+ if (!plan.outputSchema)
395
+ return undefined;
396
+ const errors = validateJsonSchemaSubset(projectStepOutput(evidence), plan.outputSchema);
397
+ if (errors.length === 0)
398
+ return undefined;
399
+ return (`Step "${plan.stepId}" artifact failed validation against the step's declared output schema: ` +
400
+ `${errors.join("; ")}.`);
401
+ }
402
+ /**
403
+ * Build the summary the completion-criteria gate judges for a step (addendum
404
+ * R2, "typed artifacts, honest gates"): a one-line unit count followed by the
405
+ * promoted step artifact as canonical JSON, clipped at {@link GATE_ARTIFACT_CLIP}
406
+ * chars. This replaces machine-prose so the gate evaluates real results.
407
+ */
408
+ export function buildArtifactSummary(stepId, units, evidence) {
409
+ const failedCount = units.filter((u) => !u.ok).length;
410
+ const json = canonicalJson(projectStepOutput(evidence)) ?? "null";
411
+ return (`Step "${stepId}" executed ${units.length} unit(s) (${units.length - failedCount} succeeded, ${failedCount} failed). ` +
412
+ `Step artifact (canonical JSON${json.length > GATE_ARTIFACT_CLIP ? `, clipped at ${GATE_ARTIFACT_CLIP} chars` : ""}):\n` +
413
+ clip(json, GATE_ARTIFACT_CLIP));
414
+ }
415
+ /** A unit's contribution to the step artifact: structured result, else text, else null (failures). */
416
+ function unitOutputValue(unit) {
417
+ if (!unit.ok)
418
+ return null;
419
+ if (unit.result !== undefined)
420
+ return unit.result;
421
+ return unit.text ?? null;
422
+ }
423
+ export function buildEvidence(units, reducer, isFanOut) {
424
+ // Per-unit evidence is the DURABLE, surface-independent projection the two
425
+ // driver surfaces (engine + brief/report) must agree on byte-for-byte (R4
426
+ // conformance, "identical unit graph"). It therefore carries ONLY fields both
427
+ // surfaces can reproduce from the journal:
428
+ // - a SUCCESS keeps its promoted contribution (structured `result` or clipped
429
+ // `text`) — the report path rehydrates exactly these from the unit row;
430
+ // - a FAILURE keeps only its `failureReason` (the durable, journaled failure
431
+ // vocabulary). The engine's in-memory dispatch diagnostic (`error`) and any
432
+ // residual `text` on a failed unit are NOT persisted here: a driver-reported
433
+ // failure carries neither, so persisting them on the engine surface alone
434
+ // would diverge the durable graph. The full raw text/reason still lives on
435
+ // the unit row for engine-side diagnostics; this is the shared graph.
436
+ const collected = units.map((u) => u.ok
437
+ ? {
438
+ unitId: u.unitId,
439
+ ok: true,
440
+ ...(u.result !== undefined ? { result: u.result } : {}),
441
+ ...(u.text !== undefined ? { text: clip(u.text, EVIDENCE_TEXT_CLIP) } : {}),
442
+ }
443
+ : {
444
+ unitId: u.unitId,
445
+ ok: false,
446
+ ...(u.failureReason ? { failureReason: u.failureReason } : {}),
447
+ });
448
+ const evidence = { units: collected, itemCount: units.length };
449
+ // Promoted step artifact (`evidence.output`) — what `${{ steps.<id>.output }}`
450
+ // resolves to (see projectStepOutput). Values are UNCLIPPED.
451
+ if (reducer === "vote") {
452
+ evidence.output = null;
453
+ }
454
+ else {
455
+ evidence.output = isFanOut ? units.map(unitOutputValue) : unitOutputValue(units[0]);
456
+ }
457
+ if (reducer === "vote") {
458
+ const counts = new Map();
459
+ for (const unit of units) {
460
+ if (!unit.ok)
461
+ continue;
462
+ const value = unit.result !== undefined ? unit.result : unit.text;
463
+ const key = canonicalJson(value);
464
+ const entry = counts.get(key);
465
+ if (entry)
466
+ entry.count++;
467
+ else
468
+ counts.set(key, { value, count: 1 });
469
+ }
470
+ const ranked = [...counts.values()].sort((a, b) => b.count - a.count);
471
+ if (ranked.length === 0) {
472
+ evidence.voteError = "Vote reducer had no successful unit results to count.";
473
+ }
474
+ else if (ranked.length > 1 && ranked[0].count === ranked[1].count) {
475
+ evidence.voteError = `Vote reducer tied at ${ranked[0].count} vote(s) — no majority.`;
476
+ }
477
+ else {
478
+ evidence.vote = { winner: ranked[0].value, votes: ranked[0].count, total: units.length };
479
+ evidence.output = ranked[0].value;
480
+ }
481
+ }
482
+ return evidence;
483
+ }
484
+ /**
485
+ * Reduce a step's terminal unit outcomes into the promoted artifact + step
486
+ * verdict — the shared semantics between native dispatch and the report path.
487
+ * Applies the `on_error` policy (`fail` vs `continue`), the reducer (via
488
+ * {@link buildEvidence}), the vote-tie failure, and the typed-artifact schema
489
+ * validation (fail-fast, errors in the summary, `artifactSchemaFailure` marker).
490
+ * Callers own dispatch-specific concerns (replay-divergence, budget) BEFORE
491
+ * calling this; those never occur on the report path (units are journaled).
492
+ */
493
+ export function reduceStepOutcomes(plan, reducer, isFanOut, onError, units) {
494
+ const failed = units.filter((u) => !u.ok);
495
+ const evidence = buildEvidence(units, reducer, isFanOut);
496
+ const reducerNote = typeof evidence.voteError === "string" ? ` ${evidence.voteError}` : "";
497
+ const tolerateFailures = onError === "continue";
498
+ let ok = (tolerateFailures || failed.length === 0) && !evidence.voteError;
499
+ let summary = `Executed ${units.length} unit(s) for step "${plan.stepId}" via workflow orchestration: ` +
500
+ `${units.length - failed.length} succeeded, ${failed.length} failed.` +
501
+ (failed.length > 0
502
+ ? ` Failures${tolerateFailures ? " (recorded, on_error: continue)" : ""}: ${failed
503
+ .map((u) => `${u.unitId} (${u.failureReason ?? "error"})`)
504
+ .join(", ")}.`
505
+ : "") +
506
+ reducerNote;
507
+ let artifactSchemaFailure = false;
508
+ if (ok) {
509
+ const schemaFailure = validateStepArtifact(plan, evidence);
510
+ if (schemaFailure !== undefined) {
511
+ ok = false;
512
+ summary = schemaFailure;
513
+ artifactSchemaFailure = true;
514
+ }
515
+ }
516
+ return { ok, units, evidence, summary, ...(artifactSchemaFailure ? { artifactSchemaFailure: true } : {}) };
517
+ }
518
+ /**
519
+ * The reduced outcome of a step whose fan-out list resolved to EMPTY (`over: []`
520
+ * or a producer that yielded `[]`): no units are dispatched, so the promoted
521
+ * artifact is the degenerate empty value — the empty array for a `collect`
522
+ * reducer, `null` for `vote` (references into a missing winner fail loudly at
523
+ * resolution rather than silently reading the envelope). Even the degenerate
524
+ * artifact must honor the step's declared `outputSchema` before it can complete.
525
+ *
526
+ * Shared by native dispatch (`executeStepPlan`'s `items.length === 0` branch)
527
+ * and the R3 driver protocol (`report` auto-completes an empty step the spine
528
+ * reaches, since no `report --unit` can ever advance a zero-unit step) so both
529
+ * surfaces promote the SAME artifact and apply the SAME schema verdict — the
530
+ * anti-drift guarantee. Deliberately does NOT run the reducer/vote-tie logic:
531
+ * an empty step has no successful results to count, and a vote-tie "failure"
532
+ * would diverge from the engine's long-standing empty-list semantics.
533
+ */
534
+ export function reduceEmptyStep(plan, reducer) {
535
+ const evidence = { units: [], itemCount: 0, output: reducer === "collect" ? [] : null };
536
+ const schemaFailure = validateStepArtifact(plan, evidence);
537
+ return {
538
+ ok: schemaFailure === undefined,
539
+ units: [],
540
+ evidence,
541
+ summary: schemaFailure ?? `Step "${plan.stepId}" fan-out list was empty — no units dispatched.`,
542
+ ...(schemaFailure !== undefined ? { artifactSchemaFailure: true } : {}),
543
+ };
544
+ }
545
+ /**
546
+ * Rehydrate a journaled unit row into a {@link UnitOutcome}. Shared by the
547
+ * executor's durable-row reuse (`native-executor.ts`, completed rows only) and
548
+ * the R3 report path (which reduces completed AND failed rows replayed from the
549
+ * journal). A completed row's text unit journals its output as a JSON string; a
550
+ * schema unit journals the validated structure. A failed row carries its
551
+ * `failure_reason`; any journaled text is surfaced too.
552
+ */
553
+ export function unitOutcomeFromRow(unitId, row, hasSchema) {
554
+ let parsed;
555
+ try {
556
+ parsed = row.result_json === null ? undefined : JSON.parse(row.result_json);
557
+ }
558
+ catch {
559
+ parsed = undefined;
560
+ }
561
+ if (row.status === "completed") {
562
+ return {
563
+ unitId,
564
+ ok: true,
565
+ ...(hasSchema
566
+ ? { result: parsed }
567
+ : typeof parsed === "string"
568
+ ? { text: parsed }
569
+ : parsed !== undefined
570
+ ? { result: parsed }
571
+ : {}),
572
+ ...(row.tokens !== null ? { tokens: row.tokens } : {}),
573
+ ...(row.session_id !== null && row.session_id !== undefined ? { sessionId: row.session_id } : {}),
574
+ };
575
+ }
576
+ return {
577
+ unitId,
578
+ ok: false,
579
+ failureReason: row.failure_reason ?? "reported_failure",
580
+ ...(typeof parsed === "string" ? { text: parsed } : {}),
581
+ ...(row.tokens !== null ? { tokens: row.tokens } : {}),
582
+ };
583
+ }
584
+ /**
585
+ * Select the journaled attempt row that determines a unit's TERMINAL outcome on
586
+ * a REPLAY surface — the engine's durable-row reuse AND the harness-neutral
587
+ * brief/report driver protocol — given the run's dispatch rows indexed by
588
+ * unit_id. This is the ONE place all surfaces resolve "which journaled row IS
589
+ * this unit's outcome," so they cannot drift from each other or from the engine.
590
+ *
591
+ * It mirrors the executor's {@link classifyUnitReuse} attempt scan
592
+ * (native-executor.ts): among the base attempt and its `~r<n>` retries — all
593
+ * stacked on `journalBaseId`, which already carries the active `~l<loop>` gate
594
+ * suffix — the FIRST completed attempt is the effective result. So a unit whose
595
+ * base attempt FAILED but whose later retry COMPLETED reduces as COMPLETED,
596
+ * exactly like an engine resume reusing the `~r1` row (Codex round-3 finding C);
597
+ * reading only the base row would reduce it as failed and diverge the two
598
+ * surfaces. With no completed attempt the HIGHEST journaled attempt stands (a
599
+ * terminal failure, or a still-running row); no attempt row at all ⇒ `undefined`
600
+ * (the unit is still outstanding).
601
+ */
602
+ export function selectUnitAttemptRow(workUnit, dispatchRows) {
603
+ const base = workUnit.journalBaseId;
604
+ const maxAttempts = 1 + Math.max(0, workUnit.retry?.max ?? 0);
605
+ let fallback;
606
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
607
+ const row = dispatchRows.get(attempt === 0 ? base : `${base}~r${attempt}`);
608
+ if (!row)
609
+ continue;
610
+ if (row.status === "completed")
611
+ return row;
612
+ fallback = row; // remember the highest journaled (non-completed) attempt
613
+ }
614
+ return fallback;
615
+ }
616
+ /**
617
+ * Is a FAILED unit still RETRY-ELIGIBLE — i.e. NOT terminal, because a driver
618
+ * could still re-run it via the `--rerun` form (the engine's automatic
619
+ * `<baseId>~r<n>` retry)? A unit whose declared `retry.on` matches the recorded
620
+ * failure reason AND whose attempt budget (`1 + retry.max`) is not yet spent can
621
+ * still be re-run. No `retry`, an off-list reason, or an exhausted attempt budget
622
+ * ⇒ the failure IS terminal. Shared by the report fail-fast decision, the
623
+ * `--settle` refusal, and `brief`'s fully-terminal detection so all three agree
624
+ * on when a failed unit is genuinely done vs. still re-runnable. The normalized
625
+ * failure reason is compared against `retry.on` directly (a canonical taxonomy
626
+ * reason is stored verbatim; an `external:*` reason is by construction outside
627
+ * the taxonomy `retry.on` lists).
628
+ */
629
+ export function isRetryEligibleFailure(workUnit, row, failureReason) {
630
+ const retry = workUnit.retry;
631
+ if (!retry || failureReason === null || !retry.on.includes(failureReason))
632
+ return false;
633
+ const attempts = row?.attempts ?? 1;
634
+ return attempts < 1 + Math.max(0, retry.max);
635
+ }
636
+ /**
637
+ * Does a resolvable unit still need a driver to execute + report it (or re-run
638
+ * it)? True for a unit with no terminal row (pending), a still-`running` row (a
639
+ * live/stale claim another driver holds), or a FAILED row that is still
640
+ * retry-eligible. False for a COMPLETED row, a terminal non-retry-eligible
641
+ * FAILURE, or an UNRESOLVABLE unit (a currently-unreachable defensive branch —
642
+ * see the computeStepWorkList doc — never reportable). The best terminal attempt (base + `~r<n>` retries) is the
643
+ * one consulted, the SAME reuse the engine and reducer apply.
644
+ */
645
+ export function unitStillNeedsReport(workUnit, dispatchRows) {
646
+ if (!workUnit.resolved.ok)
647
+ return false;
648
+ const row = selectUnitAttemptRow(workUnit, dispatchRows);
649
+ if (!row)
650
+ return true; // no journal row → pending
651
+ if (row.status === "running")
652
+ return true; // a live/stale claim is still in flight
653
+ if (row.status === "failed")
654
+ return isRetryEligibleFailure(workUnit, row, row.failure_reason);
655
+ return false; // completed (or a non-retry-eligible failure) → terminal
656
+ }
657
+ /**
658
+ * Is the active step's work-list FULLY TERMINAL — every resolvable unit run to a
659
+ * terminal (done, or non-retry-eligible failed) state with nothing left to
660
+ * execute or per-unit report — yet still needing finalization? This is the
661
+ * driver-recovery state after a required-gate block is resumed, or a crash
662
+ * between the last unit write and the step's completion (owner manual-validation
663
+ * finding 3): the work-list is done but the step never advanced. `brief`
664
+ * surfaces it with a single `report --settle` command and `--settle` runs the
665
+ * shared completion path for it. A list with ANY outstanding unit (pending,
666
+ * in-flight, or retry-eligible failed) is NOT fully terminal — the driver
667
+ * `report --unit`s those. A route-only / empty / all-unresolvable list (no
668
+ * resolvable units) is a DIFFERENT non-dispatching state, handled separately.
669
+ */
670
+ export function isWorkListFullyTerminal(workList, dispatchRows) {
671
+ if (!workList.units.some((u) => u.resolved.ok))
672
+ return false;
673
+ return workList.units.every((u) => !unitStillNeedsReport(u, dispatchRows));
674
+ }
675
+ /** Stable stringify (sorted object keys, recursively) so equal values vote together. */
676
+ export function canonicalJson(value) {
677
+ return JSON.stringify(sortKeys(value));
678
+ }
679
+ function sortKeys(value) {
680
+ if (Array.isArray(value))
681
+ return value.map(sortKeys);
682
+ if (value && typeof value === "object") {
683
+ return Object.fromEntries(Object.entries(value)
684
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
685
+ .map(([k, v]) => [k, sortKeys(v)]));
686
+ }
687
+ return value;
688
+ }
689
+ // ── Gate-feedback recovery (PURE) ────────────────────────────────────────────
690
+ //
691
+ // A gate rejection is journaled as `<stepId>.gate:l<loop>` with result_json
692
+ // `{ complete: false, missing, feedback }` (see journalGateEvaluationFinish).
693
+ // The feedback stored there is BYTE-IDENTICAL to what the engine threads into
694
+ // the next loop's prompts — both are the same `rejection.feedback`/`.missing`.
695
+ // `brief` recovers it from the journal so its loop-N work-list matches the
696
+ // engine's (redesign addendum R3, task item 2). `native-executor.test.ts`
697
+ // asserts the round-trip identity.
698
+ // GATE_EVALUATION_PHASE moved to ../runtime/unit-phases.ts (leaf) so
699
+ // unit-checkin can key on it without closing the exec ↔ runtime cycle.
700
+ /** The unit id of a step's gate-evaluation row for a given 1-based loop. */
701
+ export function gateUnitId(stepId, loop) {
702
+ return `${stepId}.gate:l${loop}`;
703
+ }
704
+ /**
705
+ * The gate loop the engine is about to (re-)run for an ACTIVE step, derived
706
+ * purely from the journal: one past the highest journaled loop that REJECTED
707
+ * (`complete: false`). No rejected gate rows ⇒ loop 1 (the first execution).
708
+ * A passed gate would have advanced the spine, so an active step never has a
709
+ * `complete: true` row as its latest gate evaluation.
710
+ *
711
+ * Reviewer #17: a gate row that EXISTS but cannot be parsed (or carries an
712
+ * invalid verdict shape) is CORRUPTION — {@link parseGateVerdict} throws loudly
713
+ * rather than letting `gateRowRejected` swallow the parse error, which would
714
+ * silently drop the loop back to 1 and re-dispatch work whose gate outcome is
715
+ * unknown.
716
+ */
717
+ export function activeGateLoop(rows, stepId) {
718
+ let maxRejectedLoop = 0;
719
+ for (const row of rows) {
720
+ if (row.phase !== GATE_EVALUATION_PHASE || row.step_id !== stepId)
721
+ continue;
722
+ const loop = gateLoopOf(row.unit_id, stepId);
723
+ if (loop === undefined)
724
+ continue;
725
+ // Throws loudly on a corrupt/malformed gate row — never treated as absent.
726
+ if (parseGateVerdict(row).kind === "rejected" && loop > maxRejectedLoop)
727
+ maxRejectedLoop = loop;
728
+ }
729
+ return maxRejectedLoop + 1;
730
+ }
731
+ /**
732
+ * Recover the gate feedback the engine threads into `loop`'s unit prompts: the
733
+ * `{ feedback, missing }` journaled by the previous loop's rejection
734
+ * (`<stepId>.gate:l<loop-1>`). Loop 1 (or a missing/passed/errored previous row)
735
+ * has no feedback. Pure — the journal rows are passed in.
736
+ *
737
+ * Reviewer #17: a PRESENT previous gate row that cannot be parsed fails LOUDLY
738
+ * (via {@link parseGateVerdict}) instead of returning undefined — a corrupt row
739
+ * must not make an in-loop step look like loop 1 with no recovered feedback.
740
+ */
741
+ export function recoverGateFeedback(rows, stepId, loop) {
742
+ if (loop <= 1)
743
+ return undefined;
744
+ const prevId = gateUnitId(stepId, loop - 1);
745
+ const prev = rows.find((r) => r.unit_id === prevId && r.phase === GATE_EVALUATION_PHASE);
746
+ if (!prev)
747
+ return undefined;
748
+ const verdict = parseGateVerdict(prev);
749
+ return verdict.kind === "rejected" ? { feedback: verdict.feedback, missing: verdict.missing } : undefined;
750
+ }
751
+ /** The 1-based loop encoded in a `<stepId>.gate:l<n>` unit id, if well-formed. */
752
+ function gateLoopOf(unitId, stepId) {
753
+ const prefix = `${stepId}.gate:l`;
754
+ if (!unitId.startsWith(prefix))
755
+ return undefined;
756
+ const n = Number.parseInt(unitId.slice(prefix.length), 10);
757
+ return Number.isInteger(n) && n >= 1 ? n : undefined;
758
+ }
759
+ /**
760
+ * Classify a gate-evaluation row's journaled verdict, failing LOUDLY on a
761
+ * corrupt one (reviewer #17). A NULL `result_json` is the LEGITIMATE
762
+ * errored-judge / in-flight shape (`journalGateEvaluationFinish` writes null for
763
+ * an errored judge, and a `running` row has no verdict yet) and classifies as
764
+ * `empty`. But a PRESENT `result_json` that does not parse as JSON, or parses to
765
+ * anything other than an object with a boolean `complete` field, is corruption —
766
+ * a truncated or hand-edited row — and MUST NOT be silently treated as absent
767
+ * (which would reset an active step's gate loop to 1 and re-dispatch work whose
768
+ * completion outcome is unknown). We refuse to guess.
769
+ */
770
+ function parseGateVerdict(row) {
771
+ if (row.result_json === null)
772
+ return { kind: "empty" };
773
+ let verdict;
774
+ try {
775
+ verdict = JSON.parse(row.result_json);
776
+ }
777
+ catch {
778
+ throw new UsageError(gateCorruptionMessage(row, "its result_json is not valid JSON"));
779
+ }
780
+ if (typeof verdict !== "object" || verdict === null || Array.isArray(verdict)) {
781
+ throw new UsageError(gateCorruptionMessage(row, "its result_json is not a JSON object"));
782
+ }
783
+ const v = verdict;
784
+ if (typeof v.complete !== "boolean") {
785
+ throw new UsageError(gateCorruptionMessage(row, 'its verdict has no boolean "complete" field'));
786
+ }
787
+ if (v.complete === false) {
788
+ const feedback = typeof v.feedback === "string" ? v.feedback : "";
789
+ const missing = Array.isArray(v.missing) ? v.missing.filter((m) => typeof m === "string") : [];
790
+ return { kind: "rejected", missing, feedback };
791
+ }
792
+ return { kind: "passed" };
793
+ }
794
+ function gateCorruptionMessage(row, why) {
795
+ return (`Workflow run ${row.run_id} has a corrupt gate-evaluation row "${row.unit_id}" for step "${row.step_id}" — ${why}. ` +
796
+ `A gate verdict must be {"complete": true|false, …}; refusing to treat a malformed gate row as absent, which would ` +
797
+ `silently restart the step's gate loop and re-dispatch work whose completion outcome is unknown. Fix or remove the ` +
798
+ `journaled row, then resume the run.`);
799
+ }
800
+ /** Insert the gate-evaluation unit row (running) just before the judge runs. */
801
+ export async function journalGateEvaluationStart(gate) {
802
+ const unitId = gateUnitId(gate.stepId, gate.loop);
803
+ await enqueueUnitWrite(() => withWorkflowRunsRepo((repo) => repo.insertUnit({
804
+ runId: gate.runId,
805
+ unitId,
806
+ stepId: gate.stepId,
807
+ nodeId: `${gate.stepId}.gate`,
808
+ parentUnitId: null,
809
+ // Marks the row as a judge call, NOT a dispatch: the budget/lifetime
810
+ // seed in `driveRun` skips these so resume accounting matches live.
811
+ phase: GATE_EVALUATION_PHASE,
812
+ runner: "llm",
813
+ engine: gate.invocation.engine,
814
+ model: gate.invocation.model,
815
+ inputHash: gate.inputHash,
816
+ startedAt: new Date().toISOString(),
817
+ })));
818
+ appendEvent({
819
+ eventType: "workflow_unit_started",
820
+ ref: gate.workflowRef,
821
+ metadata: { runId: gate.runId, stepId: gate.stepId, unitId },
822
+ });
823
+ }
824
+ /**
825
+ * Finish the gate-evaluation unit row with the verdict as observed from the
826
+ * completion outcome: a rejection journals `{ complete: false, missing,
827
+ * feedback }`; a pass journals `{ complete: true, missing: [] }`; a judge that
828
+ * threw journals a failed row with a NULL verdict and validation is skipped.
829
+ */
830
+ export async function journalGateEvaluationFinish(gate, errored, rejection) {
831
+ const unitId = gateUnitId(gate.stepId, gate.loop);
832
+ const verdict = errored
833
+ ? null
834
+ : rejection
835
+ ? { complete: false, missing: rejection.missing, feedback: rejection.feedback }
836
+ : { complete: true, missing: [] };
837
+ const status = errored ? "failed" : "completed";
838
+ await enqueueUnitWrite(() => withWorkflowRunsRepo((repo) => repo.finishUnit({
839
+ runId: gate.runId,
840
+ unitId,
841
+ status,
842
+ resultJson: verdict ? JSON.stringify(verdict) : null,
843
+ tokens: null,
844
+ failureReason: errored ? "dispatch_error" : null,
845
+ finishedAt: new Date().toISOString(),
846
+ })));
847
+ appendEvent({
848
+ eventType: "workflow_unit_finished",
849
+ ref: gate.workflowRef,
850
+ metadata: { runId: gate.runId, stepId: gate.stepId, unitId, status },
851
+ });
852
+ }
853
+ /**
854
+ * Resolve a route's input (a single whole-value `${{ … }}` reference) and pick
855
+ * the branch. No ambient key search. Only primitive values route; the
856
+ * comparison is exact string equality against the declared `when:` matches.
857
+ */
858
+ export function evaluateRoute(route, scope) {
859
+ const resolved = resolveReferenceString(route.input, scope);
860
+ if (!resolved.ok) {
861
+ return { ok: false, error: `route input ${route.input} failed to resolve: ${resolved.error.message}` };
862
+ }
863
+ const value = resolved.value;
864
+ if (typeof value === "object" && value !== null) {
865
+ return {
866
+ ok: false,
867
+ error: `route input ${route.input} resolved to a non-primitive value; branches match on strings/numbers/booleans.`,
868
+ };
869
+ }
870
+ const valueString = typeof value === "string" ? value : String(value);
871
+ // Own-property check: `when` is author-controlled, and a value such as
872
+ // "constructor" must not resolve through Object.prototype.
873
+ const selected = Object.hasOwn(route.when, valueString) ? route.when[valueString] : route.defaultStepId;
874
+ if (!selected) {
875
+ return {
876
+ ok: false,
877
+ error: `value "${valueString}" matched no "when:" branch and the route declares no default.`,
878
+ };
879
+ }
880
+ return { ok: true, value: valueString, selected };
881
+ }
882
+ /**
883
+ * Cascade a SKIPPED router: it never evaluated its route, so every declared
884
+ * target (branches + default) is marked skip-on-reach unless an earlier router
885
+ * already claimed it. Shared by the live skip path and the journal replay.
886
+ */
887
+ export function cascadeSkippedRouter(route, routerId, routeUnselected) {
888
+ const targets = [...Object.values(route.when), ...(route.defaultStepId ? [route.defaultStepId] : [])];
889
+ for (const target of targets) {
890
+ if (!routeUnselected.has(target)) {
891
+ routeUnselected.set(target, { router: routerId, selected: null });
892
+ }
893
+ }
894
+ }
895
+ /**
896
+ * Record one router's decision in the skip bookkeeping: the selected target is
897
+ * protected, every other declared target (branches + default) is marked
898
+ * skip-on-reach unless an earlier router already claimed it. Shared by the live
899
+ * evaluation path and the journal replay.
900
+ */
901
+ export function applyRouteDecision(route, routerId, selected, routeSelected, routeUnselected) {
902
+ routeSelected.add(selected);
903
+ const targets = [...Object.values(route.when), ...(route.defaultStepId ? [route.defaultStepId] : [])];
904
+ for (const target of targets) {
905
+ if (target !== selected && !routeUnselected.has(target)) {
906
+ routeUnselected.set(target, { router: routerId, selected });
907
+ }
908
+ }
909
+ }
910
+ /**
911
+ * The `stepOutputs` scope a route resolves against: every prior step's recorded
912
+ * evidence plus the just-finished step's fresh evidence — each projected
913
+ * through {@link projectStepOutput}. Same projection as unit templates, so the
914
+ * two scopes cannot drift.
915
+ */
916
+ export function routeStepOutputs(evidence, currentStepId, currentEvidence) {
917
+ const outputs = {};
918
+ for (const [stepId, stepEvidence] of Object.entries(evidence)) {
919
+ if (stepEvidence !== undefined)
920
+ outputs[stepId] = projectStepOutput(stepEvidence);
921
+ }
922
+ outputs[currentStepId] = projectStepOutput(currentEvidence);
923
+ return outputs;
924
+ }
925
+ /** The `selected` target journaled on a route step's evidence, if well-formed. */
926
+ function journaledRouteSelection(evidence) {
927
+ const route = evidence?.route;
928
+ if (typeof route !== "object" || route === null || Array.isArray(route))
929
+ return undefined;
930
+ const selected = route.selected;
931
+ return typeof selected === "string" && selected !== "" ? selected : undefined;
932
+ }
933
+ /** The set of steps a route may legally select: its `when` branches + default. */
934
+ function routeTargets(route) {
935
+ return new Set([...Object.values(route.when), ...(route.defaultStepId ? [route.defaultStepId] : [])]);
936
+ }
937
+ /**
938
+ * Reviewer #7: a journaled route decision must name a target the route actually
939
+ * DECLARES (`when` branch or `default`). Corrupted or hand-edited evidence can
940
+ * otherwise mark a non-existent step as `selected` — which unselects and skips
941
+ * every REAL branch target, silently steering the run down a phantom branch.
942
+ * `evaluateRoute` can only ever produce a declared target, so a stored value
943
+ * outside that set is provably tampered evidence: fail loudly rather than seed a
944
+ * bogus skip set.
945
+ */
946
+ function assertRouteTargetDeclared(route, stepId, selected, runId) {
947
+ const targets = routeTargets(route);
948
+ if (!targets.has(selected)) {
949
+ throw new UsageError(`Workflow run ${runId} has a completed route step "${stepId}" whose journaled route decision selected ` +
950
+ `"${selected}", which is not a declared branch or default target of the route (valid targets: ` +
951
+ `${[...targets].join(", ") || "(none)"}). The route evidence was corrupted or manually edited — refusing to ` +
952
+ `apply a bogus route decision that would skip the real branch targets. Start a new run.`);
953
+ }
954
+ }
955
+ /**
956
+ * Validate every COMPLETED route step's journaled selection against its declared
957
+ * targets (reviewer #7). Read-only: it throws on a PRESENT-but-invalid selection
958
+ * and is silent on an absent one, so it never false-positives on a healthy run —
959
+ * making it safe to call from the read-only `brief` surface as well as the
960
+ * resume/report surfaces that already re-apply the decisions.
961
+ */
962
+ export function assertJournaledRouteSelectionsValid(plan, state) {
963
+ for (const stepPlan of plan.steps) {
964
+ if (!stepPlan.route)
965
+ continue;
966
+ const stepState = state.workflow.steps.find((s) => s.id === stepPlan.stepId);
967
+ if (!stepState || stepState.status !== "completed")
968
+ continue;
969
+ const selected = journaledRouteSelection(stepState.evidence);
970
+ if (selected !== undefined) {
971
+ assertRouteTargetDeclared(stepPlan.route, stepPlan.stepId, selected, state.run.id);
972
+ }
973
+ }
974
+ }
975
+ /**
976
+ * Replay journaled route decisions into the skip bookkeeping (resume path).
977
+ * For every COMPLETED route step of the frozen plan, in spine order: the
978
+ * journaled decision wins; else a re-derivation from the frozen plan +
979
+ * journaled evidence; else fail loudly. A SKIPPED route step cascades its
980
+ * targets into the skip set exactly as on the live path.
981
+ */
982
+ export function seedJournaledRouteDecisions(plan, state, routeSelected, routeUnselected) {
983
+ const evidence = {};
984
+ for (const s of state.workflow.steps)
985
+ evidence[s.id] = s.evidence;
986
+ for (const stepPlan of plan.steps) {
987
+ if (!stepPlan.route)
988
+ continue;
989
+ const stepState = state.workflow.steps.find((s) => s.id === stepPlan.stepId);
990
+ if (!stepState)
991
+ continue;
992
+ if (stepState.status === "skipped") {
993
+ cascadeSkippedRouter(stepPlan.route, stepPlan.stepId, routeUnselected);
994
+ continue;
995
+ }
996
+ if (stepState.status !== "completed")
997
+ continue;
998
+ let selected = journaledRouteSelection(stepState.evidence);
999
+ if (selected !== undefined) {
1000
+ // Reviewer #7: a stored decision must name a declared target — a bogus one
1001
+ // (tampered/hand-edited evidence) fails loudly rather than seeding a skip
1002
+ // set that buries the real branches.
1003
+ assertRouteTargetDeclared(stepPlan.route, stepPlan.stepId, selected, state.run.id);
1004
+ }
1005
+ if (selected === undefined) {
1006
+ const scope = {
1007
+ params: state.run.params ?? {},
1008
+ stepOutputs: routeStepOutputs(evidence, stepPlan.stepId, stepState.evidence ?? {}),
1009
+ };
1010
+ const decision = evaluateRoute(stepPlan.route, scope);
1011
+ if (decision.ok)
1012
+ selected = decision.selected;
1013
+ }
1014
+ if (selected === undefined) {
1015
+ throw new UsageError(`Workflow run ${state.run.id} has a completed route step "${stepPlan.stepId}" with no journaled route ` +
1016
+ `decision, and the decision cannot be re-derived from the journaled evidence. Refusing to guess which ` +
1017
+ `branch was selected — advance the remaining steps manually with \`akm workflow complete\`.`);
1018
+ }
1019
+ applyRouteDecision(stepPlan.route, stepPlan.stepId, selected, routeSelected, routeUnselected);
1020
+ }
1021
+ }
1022
+ /**
1023
+ * Perform ONE completion attempt for an executed step:
1024
+ *
1025
+ * - a hard unit failure completes the step `failed` (a retryable typed-artifact
1026
+ * mismatch with loops remaining returns `retry` WITHOUT journaling a gate row
1027
+ * — no judge ran, exactly like the engine);
1028
+ * - a route decision is evaluated against params + prior/fresh step outputs; an
1029
+ * unroutable value fails the step; a valid decision is journaled on the
1030
+ * step evidence and applied to the skip bookkeeping;
1031
+ * - the completion gate judges a summary BUILT FROM the promoted artifact (when
1032
+ * the step declares criteria), journaled as a `<stepId>.gate:l<loop>` unit
1033
+ * row; a rejection with loops remaining returns `retry` (feedback threaded
1034
+ * into the next loop), a rejection with none returns `gate-exhausted`, a pass
1035
+ * returns `advanced`.
1036
+ *
1037
+ * Every DB advance goes through {@link completeWorkflowStep} — the gate spine is
1038
+ * never bypassed. Behavior is byte-identical to the engine's former inline loop
1039
+ * body (its tests prove it).
1040
+ */
1041
+ export async function finalizeExecutedStep(input) {
1042
+ const { runId, workflowRef, stepId, stepPlan, completionCriteria, gateLoop, loopsRemaining, result } = input;
1043
+ const lease = input.leaseHolder !== undefined ? { leaseHolder: input.leaseHolder } : {};
1044
+ if (!result.ok) {
1045
+ // Typed-artifact mismatch with loop budget left: regenerate-with-errors
1046
+ // (the validation errors become the next loop's feedback). No judge ran, so
1047
+ // no gate row is journaled for this attempt.
1048
+ if (result.artifactSchemaFailure && loopsRemaining) {
1049
+ return { kind: "retry", gateFeedback: { feedback: result.summary, missing: [] } };
1050
+ }
1051
+ await completeWorkflowStep({
1052
+ runId,
1053
+ stepId,
1054
+ status: "failed",
1055
+ notes: result.summary,
1056
+ evidence: result.evidence,
1057
+ ...lease,
1058
+ });
1059
+ return { kind: "failed", summary: result.summary };
1060
+ }
1061
+ // Resolve the completion-criteria judge ONCE (reused by the gate below). A
1062
+ // A frozen plan either supplies its judge at the dispatch boundary or has no
1063
+ // judge. Re-selecting defaults here would let config drift change a run.
1064
+ const innerJudge = input.summaryJudge ?? null;
1065
+ // Route evaluation BEFORE completion: an unroutable value is an
1066
+ // authoring/config failure that must fail the step deterministically.
1067
+ let summaryOverride;
1068
+ if (stepPlan.route) {
1069
+ const scope = {
1070
+ params: input.params,
1071
+ stepOutputs: routeStepOutputs(input.priorEvidence, stepId, result.evidence),
1072
+ };
1073
+ const decision = evaluateRoute(stepPlan.route, scope);
1074
+ if (!decision.ok) {
1075
+ const notes = `Step "${stepId}" route failed: ${decision.error}`;
1076
+ await completeWorkflowStep({ runId, stepId, status: "failed", notes, evidence: result.evidence, ...lease });
1077
+ return { kind: "failed", summary: notes, routeFailure: true };
1078
+ }
1079
+ applyRouteDecision(stepPlan.route, stepId, decision.selected, input.routeSelected, input.routeUnselected);
1080
+ // Journal the decision on the evidence: resume replays it via
1081
+ // seedJournaledRouteDecisions, so the skip set survives re-invocation.
1082
+ result.evidence.route = { input: stepPlan.route.input, value: decision.value, selected: decision.selected };
1083
+ if (!stepPlan.root) {
1084
+ summaryOverride = `Step "${stepId}" routed on ${stepPlan.route.input}: value "${decision.value}" selected step "${decision.selected}".`;
1085
+ }
1086
+ }
1087
+ // Artifact-judging gate: a criteria-bearing executing step is judged on a
1088
+ // summary BUILT FROM the promoted artifact; everything else keeps the machine
1089
+ // summary (a route-only step's summary IS its decision).
1090
+ const summary = stepPlan.root && completionCriteria.length > 0
1091
+ ? buildArtifactSummary(stepId, result.units, result.evidence)
1092
+ : (summaryOverride ?? result.summary);
1093
+ // Journal engine-driven judge calls as unit rows (they are LLM calls). The
1094
+ // wrapper's `invoked` stays false when the gate is fail-open (no criteria / no
1095
+ // judge) — nothing is journaled, and human approvals are never cached.
1096
+ const frozenGate = innerJudge
1097
+ ? await withWorkflowRunsRepo((repo) => {
1098
+ const row = repo.getRunById(runId);
1099
+ if (!row)
1100
+ throw new UsageError(`Workflow run ${runId} was not found.`);
1101
+ const plan = requireExecutableWorkflowPlan(row);
1102
+ const invocation = plan.steps.find((step) => step.stepId === stepId)?.gate.judge ?? null;
1103
+ return invocation ? { invocation, engine: plan.execution?.engines[invocation.engine] ?? null } : null;
1104
+ })
1105
+ : null;
1106
+ const gateInvocation = frozenGate?.invocation ?? null;
1107
+ let gateUnit;
1108
+ const judgeState = { invoked: false, errored: false };
1109
+ const summaryJudge = innerJudge
1110
+ ? async (prompt) => {
1111
+ judgeState.invoked = true;
1112
+ if (gateInvocation) {
1113
+ gateUnit = {
1114
+ runId,
1115
+ workflowRef,
1116
+ stepId,
1117
+ loop: gateLoop,
1118
+ invocation: gateInvocation,
1119
+ inputHash: createHash("sha256")
1120
+ .update(canonicalJsonString({
1121
+ hashVersion: 3,
1122
+ dispatch: frozenGate?.engine ?? null,
1123
+ invocation: gateInvocation,
1124
+ prompt,
1125
+ }))
1126
+ .digest("hex"),
1127
+ };
1128
+ await journalGateEvaluationStart(gateUnit);
1129
+ }
1130
+ try {
1131
+ return await innerJudge(prompt);
1132
+ }
1133
+ catch (err) {
1134
+ judgeState.errored = true;
1135
+ throw err;
1136
+ }
1137
+ }
1138
+ : null;
1139
+ // Reviewer #6: once the judge is invoked, its gate row is journaled `running`
1140
+ // (journalGateEvaluationStart) and MUST be finished on every exit. The
1141
+ // already-fixed window is the judge itself throwing (caught inside
1142
+ // validateStepSummary — `judgeState.errored` records it). The remaining
1143
+ // window is `completeWorkflowStep` throwing AFTER the judge ran — a stolen
1144
+ // lease, a concurrent state change, a DB error — which would otherwise skip the
1145
+ // finish and strand the gate row in `running`. Finish it as an errored row (the
1146
+ // observed outcome: the completion did not succeed), then re-propagate.
1147
+ let completion;
1148
+ try {
1149
+ completion = await completeWorkflowStep({
1150
+ runId,
1151
+ stepId,
1152
+ status: "completed",
1153
+ summary,
1154
+ evidence: result.evidence,
1155
+ summaryJudge,
1156
+ ...lease,
1157
+ });
1158
+ }
1159
+ catch (err) {
1160
+ if (gateUnit)
1161
+ await journalGateEvaluationFinish(gateUnit, true, undefined);
1162
+ throw err;
1163
+ }
1164
+ const rejection = "ok" in completion && completion.ok === false ? completion : undefined;
1165
+ if (gateUnit) {
1166
+ await journalGateEvaluationFinish(gateUnit, judgeState.errored, rejection);
1167
+ }
1168
+ if (!rejection) {
1169
+ return { kind: "advanced", ...(summaryOverride !== undefined ? { summaryOverride } : {}) };
1170
+ }
1171
+ if (loopsRemaining) {
1172
+ return { kind: "retry", gateFeedback: { feedback: rejection.feedback, missing: rejection.missing } };
1173
+ }
1174
+ return {
1175
+ kind: "gate-exhausted",
1176
+ gateRejection: { stepId, missing: rejection.missing, feedback: rejection.feedback },
1177
+ };
1178
+ }
1179
+ // ── Small helpers ────────────────────────────────────────────────────────────
1180
+ function safeJson(value) {
1181
+ try {
1182
+ return JSON.stringify(value) ?? "null";
1183
+ }
1184
+ catch {
1185
+ return "null";
1186
+ }
1187
+ }
1188
+ function clip(text, max) {
1189
+ return text.length > max ? `${text.slice(0, max)}…` : text;
1190
+ }