akm-cli 0.9.1 → 0.9.2-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/CHANGELOG.md +103 -28
  2. package/README.md +3 -1
  3. package/SECURITY.md +1 -1
  4. package/STABILITY.md +1 -1
  5. package/dist/akm +2 -2
  6. package/dist/akm-migrate +2 -2
  7. package/dist/assets/hints/cli-hints-full.md +14 -9
  8. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
  9. package/dist/assets/improve-strategies/reflect-distill.json +1 -1
  10. package/dist/assets/models.json +35 -0
  11. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +3 -4
  12. package/dist/assets/stash-skeleton/facts/conventions/organization.md +1 -3
  13. package/dist/assets/tasks/core/extract.yml +6 -5
  14. package/dist/assets/tasks/core/improve.yml +6 -5
  15. package/dist/assets/tasks/core/index-refresh.yml +6 -5
  16. package/dist/assets/tasks/core/sync.yml +6 -5
  17. package/dist/assets/tasks/core/version-check.yml +6 -5
  18. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
  19. package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
  20. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
  21. package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
  22. package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
  23. package/dist/cli/confirm.js +2 -2
  24. package/dist/cli/parse-args.js +3 -24
  25. package/dist/cli/retired-commands.js +1 -1
  26. package/dist/cli/shared.js +2 -2
  27. package/dist/cli.js +11 -9
  28. package/dist/commands/agent/agent-dispatch.js +55 -89
  29. package/dist/commands/agent/contribute-cli.js +12 -45
  30. package/dist/commands/command/builtin-action.js +32 -0
  31. package/dist/commands/command/command-cli.js +99 -0
  32. package/dist/commands/command/command-execution.js +308 -0
  33. package/dist/commands/command/execution-source-loader.js +176 -0
  34. package/dist/commands/command/portable-template.js +60 -0
  35. package/dist/commands/config-cli.js +10 -4
  36. package/dist/commands/env/env.js +4 -2
  37. package/dist/commands/feedback-cli.js +1 -1
  38. package/dist/commands/health/checks.js +241 -29
  39. package/dist/commands/health/html-report.js +0 -14
  40. package/dist/commands/health/report-view-model.js +0 -1
  41. package/dist/commands/health/surfaces.js +6 -7
  42. package/dist/commands/health/types.js +0 -2
  43. package/dist/commands/health.js +63 -18
  44. package/dist/commands/improve/collapse-detector.js +5 -6
  45. package/dist/commands/improve/consolidate.js +251 -214
  46. package/dist/commands/improve/distill/promote-memory.js +71 -34
  47. package/dist/commands/improve/distill/quality-gate.js +17 -5
  48. package/dist/commands/improve/distill.js +232 -155
  49. package/dist/commands/improve/eligibility.js +112 -79
  50. package/dist/commands/improve/execution.js +57 -0
  51. package/dist/commands/improve/extract-cli.js +5 -5
  52. package/dist/commands/improve/extract-prompt.js +64 -22
  53. package/dist/commands/improve/extract.js +608 -360
  54. package/dist/commands/improve/improve-strategies.js +43 -14
  55. package/dist/commands/improve/improve.js +249 -29
  56. package/dist/commands/improve/loop-stages.js +11 -17
  57. package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
  58. package/dist/commands/improve/outcome-loop.js +22 -38
  59. package/dist/commands/improve/planner.js +134 -0
  60. package/dist/commands/improve/preparation.js +730 -409
  61. package/dist/commands/improve/reflect.js +386 -223
  62. package/dist/commands/improve/run-context.js +3 -4
  63. package/dist/commands/improve/salience.js +6 -58
  64. package/dist/commands/improve/session-asset.js +12 -12
  65. package/dist/commands/lint/index.js +101 -29
  66. package/dist/commands/migrate-cli.js +11 -69
  67. package/dist/commands/migration-tool.js +6 -9
  68. package/dist/commands/models-cli.js +27 -0
  69. package/dist/commands/proposal/drain.js +258 -186
  70. package/dist/commands/proposal/proposal-cli.js +32 -10
  71. package/dist/commands/proposal/proposal.js +2 -5
  72. package/dist/commands/proposal/propose.js +192 -172
  73. package/dist/commands/proposal/repository.js +54 -91
  74. package/dist/commands/proposal/validators/proposal-validators.js +9 -7
  75. package/dist/commands/read/curate.js +53 -22
  76. package/dist/commands/read/registry-search.js +25 -9
  77. package/dist/commands/read/remember-cli.js +14 -2
  78. package/dist/commands/read/search.js +10 -4
  79. package/dist/commands/read/show.js +139 -153
  80. package/dist/commands/registry-cli.js +16 -7
  81. package/dist/commands/remember.js +33 -18
  82. package/dist/commands/sources/add-cli.js +19 -178
  83. package/dist/commands/sources/bundle-cli.js +15 -3
  84. package/dist/commands/sources/dangerous-env-audit.js +135 -0
  85. package/dist/commands/sources/info.js +2 -1
  86. package/dist/commands/sources/installed-stashes.js +901 -177
  87. package/dist/commands/sources/schema-repair.js +174 -95
  88. package/dist/commands/sources/self-update.js +30 -74
  89. package/dist/commands/sources/source-add.js +3 -5
  90. package/dist/commands/sources/sources-cli.js +2 -15
  91. package/dist/commands/sources/update-transaction.js +220 -0
  92. package/dist/commands/tasks/tasks-cli.js +3 -3
  93. package/dist/commands/tasks/tasks.js +736 -317
  94. package/dist/commands/workflow-cli.js +2 -2
  95. package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
  96. package/dist/core/adapter/adapters/akm-adapter.js +85 -35
  97. package/dist/core/adapter/adapters/akm-lint.js +54 -39
  98. package/dist/core/adapter/adapters/akm-metadata.js +45 -45
  99. package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
  100. package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
  101. package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
  102. package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
  103. package/dist/core/adapter/adapters/index.js +0 -9
  104. package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
  105. package/dist/core/adapter/adapters/okf-adapter.js +4 -0
  106. package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
  107. package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
  108. package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
  109. package/dist/core/adapter/execution-source.js +308 -0
  110. package/dist/core/adapter/recognize-match.js +36 -13
  111. package/dist/core/adapter/registry.js +0 -9
  112. package/dist/core/asset/stash-meta.js +94 -4
  113. package/dist/core/common.js +6 -11
  114. package/dist/core/config/config-io.js +3 -3
  115. package/dist/core/config/config-schema.js +18 -40
  116. package/dist/core/config/config-sources.js +11 -21
  117. package/dist/core/config/config-walker.js +31 -13
  118. package/dist/core/config/config.js +23 -26
  119. package/dist/core/config/schema/engines.js +8 -7
  120. package/dist/core/config/schema/improve-processes.js +29 -5
  121. package/dist/core/config/schema/index-config.js +0 -27
  122. package/dist/core/config/schema/primitives.js +1 -23
  123. package/dist/core/config/schema/sources-bundles.js +13 -16
  124. package/dist/core/errors.js +2 -0
  125. package/dist/core/events.js +68 -32
  126. package/dist/core/extra-params.js +1 -0
  127. package/dist/core/improve-result.js +315 -0
  128. package/dist/core/lesson-lint.js +0 -6
  129. package/dist/core/maintenance-barrier.js +4 -4
  130. package/dist/core/network-policy.js +152 -0
  131. package/dist/core/paths.js +1 -1
  132. package/dist/core/recognition-util.js +4 -4
  133. package/dist/core/registry-url.js +456 -0
  134. package/dist/core/state/migrations.js +161 -47
  135. package/dist/core/state-db.js +453 -80
  136. package/dist/core/system-error.js +32 -0
  137. package/dist/core/time.js +2 -12
  138. package/dist/core/write-source.js +0 -18
  139. package/dist/execution/directory-identity.js +52 -0
  140. package/dist/execution/executable-identity.js +107 -0
  141. package/dist/execution/guarded-source.js +398 -0
  142. package/dist/execution/json.js +95 -0
  143. package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
  144. package/dist/execution/record.js +55 -0
  145. package/dist/execution/resolved-request.js +730 -0
  146. package/dist/execution/source.js +320 -0
  147. package/dist/indexer/bundle-identity-guard.js +5 -4
  148. package/dist/indexer/db/graph-db.js +33 -0
  149. package/dist/indexer/graph/graph-boost.js +3 -4
  150. package/dist/indexer/graph/graph-extraction.js +562 -373
  151. package/dist/indexer/index-written-assets.js +78 -39
  152. package/dist/indexer/indexer.js +471 -432
  153. package/dist/indexer/installations.js +6 -0
  154. package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
  155. package/dist/indexer/materialize-embeddings.js +155 -0
  156. package/dist/indexer/passes/memory-inference.js +227 -174
  157. package/dist/indexer/passes/metadata.js +263 -118
  158. package/dist/indexer/scan/doc-to-entry.js +7 -10
  159. package/dist/indexer/scan/drain-dir.js +51 -23
  160. package/dist/indexer/search/db-search.js +156 -50
  161. package/dist/indexer/search/fts-query.js +40 -40
  162. package/dist/indexer/search/ranking.js +36 -1
  163. package/dist/indexer/search/search-attribution.js +3 -1
  164. package/dist/indexer/search/search-fields.js +23 -14
  165. package/dist/indexer/search/search-hit-enrichers.js +1 -1
  166. package/dist/indexer/search/search-source.js +7 -16
  167. package/dist/indexer/search/semantic-status.js +10 -1
  168. package/dist/indexer/usage/show-usage.js +105 -0
  169. package/dist/indexer/usage/usage-events.js +7 -2
  170. package/dist/indexer/walk/matchers.js +40 -10
  171. package/dist/indexer/walk/path-resolver.js +5 -2
  172. package/dist/indexer/walk/walker.js +20 -2
  173. package/dist/integrations/agent/builder-shared.js +3 -6
  174. package/dist/integrations/agent/conversation-fallback.js +16 -0
  175. package/dist/integrations/agent/engine-resolution.js +87 -87
  176. package/dist/integrations/agent/execution-cascade.js +566 -0
  177. package/dist/integrations/agent/execution-definitions.js +211 -0
  178. package/dist/integrations/agent/execution-lowering.js +811 -0
  179. package/dist/integrations/agent/execution-preparation.js +67 -0
  180. package/dist/integrations/agent/index.js +0 -2
  181. package/dist/integrations/agent/inline-execution.js +74 -0
  182. package/dist/integrations/agent/model-map.js +515 -0
  183. package/dist/integrations/agent/persona-fallback.js +30 -0
  184. package/dist/integrations/agent/request-lowering.js +186 -0
  185. package/dist/integrations/agent/runner-dispatch.js +230 -37
  186. package/dist/integrations/agent/runner.js +12 -83
  187. package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
  188. package/dist/integrations/harnesses/aider/index.js +0 -1
  189. package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
  190. package/dist/integrations/harnesses/amazonq/index.js +0 -1
  191. package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
  192. package/dist/integrations/harnesses/claude/index.js +1 -5
  193. package/dist/integrations/harnesses/claude/session-log.js +3 -33
  194. package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
  195. package/dist/integrations/harnesses/codex/index.js +0 -1
  196. package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
  197. package/dist/integrations/harnesses/copilot/index.js +0 -1
  198. package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
  199. package/dist/integrations/harnesses/gemini/index.js +0 -1
  200. package/dist/integrations/harnesses/index.js +4 -44
  201. package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
  202. package/dist/integrations/harnesses/opencode/index.js +0 -2
  203. package/dist/integrations/harnesses/opencode/session-log.js +14 -204
  204. package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
  205. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
  206. package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
  207. package/dist/integrations/harnesses/openhands/index.js +0 -1
  208. package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
  209. package/dist/integrations/harnesses/pi/index.js +0 -1
  210. package/dist/integrations/harnesses/shared.js +0 -1
  211. package/dist/integrations/harnesses/types.js +1 -3
  212. package/dist/integrations/lockfile.js +82 -79
  213. package/dist/integrations/session-logs/index.js +6 -17
  214. package/dist/integrations/session-logs/provider-base.js +1 -29
  215. package/dist/llm/client.js +10 -5
  216. package/dist/llm/embedder.js +6 -7
  217. package/dist/llm/embedders/local.js +37 -88
  218. package/dist/llm/embedders/types.js +1 -1
  219. package/dist/llm/graph-extract.js +75 -50
  220. package/dist/llm/index-passes.js +43 -5
  221. package/dist/llm/memory-infer.js +8 -6
  222. package/dist/llm/metadata-enhance.js +5 -3
  223. package/dist/llm/structured-call.js +122 -25
  224. package/dist/output/format-exempt.js +1 -1
  225. package/dist/output/render-registry.js +0 -16
  226. package/dist/output/renderers.js +12 -7
  227. package/dist/output/shapes/curate.js +1 -0
  228. package/dist/output/shapes/helpers.js +10 -2
  229. package/dist/output/shapes/passthrough.js +2 -0
  230. package/dist/output/text/command-format.js +31 -33
  231. package/dist/output/text/health-format.js +1 -29
  232. package/dist/output/text/migrate.js +6 -56
  233. package/dist/output/text/proposal-format.js +16 -1
  234. package/dist/output/text/workflow-format.js +16 -0
  235. package/dist/registry/network.js +279 -0
  236. package/dist/registry/pinned-request-helper.js +247 -0
  237. package/dist/registry/pinned-transport.js +717 -0
  238. package/dist/registry/providers/skills-sh.js +18 -6
  239. package/dist/registry/providers/static-index.js +20 -7
  240. package/dist/registry/resolve.js +53 -28
  241. package/dist/scripts/akm-migrate-node.js +19334 -52269
  242. package/dist/scripts/akm-migrate.js +19270 -51612
  243. package/dist/setup/registry-stash-loader.js +64 -20
  244. package/dist/setup/semantic-assets.js +9 -34
  245. package/dist/setup/setup.js +12 -30
  246. package/dist/setup/source-identity.js +17 -0
  247. package/dist/setup/steps/sources.js +36 -15
  248. package/dist/setup/steps/tasks.js +39 -11
  249. package/dist/sources/providers/git-provider.js +3 -3
  250. package/dist/sources/providers/npm.js +2 -2
  251. package/dist/sources/providers/provider-utils.js +4 -3
  252. package/dist/sources/providers/website.js +11 -7
  253. package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
  254. package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
  255. package/dist/sources/website-url.js +73 -0
  256. package/dist/storage/engines/sqlite-migrations.js +81 -26
  257. package/dist/storage/managed-db.js +27 -24
  258. package/dist/storage/repositories/events-repository.js +3 -0
  259. package/dist/storage/repositories/index-connection.js +42 -10
  260. package/dist/storage/repositories/index-entries-repository.js +203 -229
  261. package/dist/storage/repositories/index-entry-mapper.js +8 -12
  262. package/dist/storage/repositories/index-entry-schema.js +255 -0
  263. package/dist/storage/repositories/index-fts-repository.js +64 -71
  264. package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
  265. package/dist/storage/repositories/index-meta-repository.js +0 -11
  266. package/dist/storage/repositories/index-schema.js +74 -350
  267. package/dist/storage/repositories/index-utility-repository.js +12 -17
  268. package/dist/storage/repositories/index-vec-repository.js +56 -7
  269. package/dist/storage/repositories/proposals-repository.js +4 -127
  270. package/dist/storage/repositories/registry-cache.js +2 -1
  271. package/dist/storage/repositories/task-history-repository.js +20 -40
  272. package/dist/storage/repositories/workflow-runs-repository.js +228 -129
  273. package/dist/storage/sqlite-read-snapshot.js +148 -0
  274. package/dist/tasks/backends/cron.js +170 -42
  275. package/dist/tasks/backends/index.js +1 -1
  276. package/dist/tasks/backends/launchd.js +787 -202
  277. package/dist/tasks/backends/schtasks.js +282 -83
  278. package/dist/tasks/embedded.js +7 -7
  279. package/dist/tasks/frozen-script.js +50 -0
  280. package/dist/tasks/resolve-akm-bin.js +5 -1
  281. package/dist/tasks/runner.js +239 -251
  282. package/dist/tasks/runtime-v3.js +281 -0
  283. package/dist/tasks/scheduler-binding.js +272 -0
  284. package/dist/tasks/scheduler-invocation.js +57 -43
  285. package/dist/tasks/scheduler-sync.js +654 -0
  286. package/dist/tasks/source-v3.js +752 -0
  287. package/dist/tasks/standalone-script-entry.js +5 -0
  288. package/dist/tasks/task-id.js +29 -0
  289. package/dist/workflows/authoring/authoring.js +15 -32
  290. package/dist/workflows/exec/dispatch-redaction.js +14 -8
  291. package/dist/workflows/exec/exec-unit.js +7 -28
  292. package/dist/workflows/exec/frozen-judge.js +57 -89
  293. package/dist/workflows/exec/lowering-notices.js +23 -0
  294. package/dist/workflows/exec/native-executor.js +301 -458
  295. package/dist/workflows/exec/param-secrets.js +4 -3
  296. package/dist/workflows/exec/run-workflow.js +26 -32
  297. package/dist/workflows/exec/step-work.js +105 -109
  298. package/dist/workflows/exec/unit-dispatch.js +103 -27
  299. package/dist/workflows/exec/unit-writer.js +3 -3
  300. package/dist/workflows/exec/worktree.js +2 -2
  301. package/dist/workflows/ir/compile.js +86 -72
  302. package/dist/workflows/ir/environment-v4.js +328 -0
  303. package/dist/workflows/ir/freeze-v4.js +122 -0
  304. package/dist/workflows/ir/plan-hash.js +13 -7
  305. package/dist/workflows/ir/schema-v4.js +525 -0
  306. package/dist/workflows/ir/schema.js +25 -284
  307. package/dist/workflows/ir/source-freeze-v4.js +506 -0
  308. package/dist/workflows/parser.js +27 -24
  309. package/dist/workflows/program/schema.js +1 -2
  310. package/dist/workflows/renderer.js +42 -29
  311. package/dist/workflows/resource-limits.js +4 -5
  312. package/dist/workflows/runtime/agent-identity.js +11 -13
  313. package/dist/workflows/runtime/plan-classifier.js +8 -8
  314. package/dist/workflows/runtime/runs.js +27 -43
  315. package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
  316. package/dist/workflows/source-files.js +373 -0
  317. package/dist/workflows/source-ir/compile.js +196 -0
  318. package/dist/workflows/source-ir/github-yaml.js +577 -0
  319. package/dist/workflows/source-ir/ordering.js +38 -0
  320. package/dist/workflows/source-ir/program.js +50 -0
  321. package/dist/workflows/source-ir/result.js +26 -0
  322. package/dist/workflows/source-ir/schema.js +772 -0
  323. package/dist/workflows/source-ir/semantics.js +242 -0
  324. package/dist/workflows/source-ir/uses.js +14 -0
  325. package/docs/README.md +2 -0
  326. package/docs/migration/README.md +3 -1
  327. package/docs/migration/release-notes/0.9.2.md +55 -0
  328. package/docs/migration/release-notes/README.md +5 -0
  329. package/docs/migration/v0.8-to-v0.9.md +76 -1077
  330. package/docs/migration/v0.9.0-troubleshooting.md +104 -516
  331. package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
  332. package/docs/reference/README.md +1 -0
  333. package/docs/reference/cli.md +230 -98
  334. package/docs/reference/configuration.md +159 -36
  335. package/docs/reference/data-and-telemetry.md +19 -1
  336. package/docs/reference/supported-formats.md +23 -3
  337. package/docs/reference/tasks.md +182 -0
  338. package/docs/reference/workflow-schema.md +91 -40
  339. package/docs/reference/workflows.md +33 -6
  340. package/package.json +10 -6
  341. package/schemas/akm-config.json +372 -224
  342. package/schemas/akm-task.json +324 -80
  343. package/schemas/akm-workflow.json +6 -9
  344. package/dist/core/migration-operation.js +0 -75
  345. package/dist/integrations/agent/model-aliases.js +0 -74
  346. package/dist/tasks/parser.js +0 -380
  347. package/dist/tasks/schema.js +0 -123
  348. package/dist/tasks/validator.js +0 -80
  349. package/dist/workflows/ir/freeze.js +0 -320
  350. package/dist/workflows/runtime/document-cache.js +0 -13
@@ -71,9 +71,10 @@ function valueLooksSecret(value) {
71
71
  return true;
72
72
  return false;
73
73
  }
74
- const MOVE_TO_ENV = "Workflow params are copied verbatim into every unit prompt shown to any driver (they are part of the unit " +
75
- "input hash and CANNOT be redacted) move secrets to an env binding (`env:` ref), which `brief` surfaces by " +
76
- "name only and never resolves.";
74
+ const MOVE_TO_ENV = "Workflow params are copied verbatim into every native unit execution context and returned in `akm workflow run` and " +
75
+ "`akm workflow status` output (they are part of the unit input hash and CANNOT be redacted) move " +
76
+ "secrets to an env binding (`env:` ref), whose value native execution resolves only at dispatch instead of storing " +
77
+ "it as a run param.";
77
78
  /**
78
79
  * Scan run params for secret-shaped values. Returns human-readable WARNING
79
80
  * strings (best-effort; never throws, never blocks). Recurses into nested
@@ -69,11 +69,11 @@ import { disposeDispatchResources } from "../../integrations/agent/runner-dispat
69
69
  import { withWorkflowRunsConnection, withWorkflowRunsRepo } from "../../storage/repositories/workflow-runs-repository.js";
70
70
  import { assertRunParamsSatisfyPlan } from "../ir/params.js";
71
71
  import { computePlanHash } from "../ir/plan-hash.js";
72
- import { decodeWorkflowPlanV3 } from "../ir/schema.js";
72
+ import { decodeWorkflowPlanV4 } from "../ir/schema-v4.js";
73
73
  import { requireExecutableWorkflowPlan } from "../runtime/plan-classifier.js";
74
74
  import { completeWorkflowStep, getNextWorkflowStep, resumeWorkflowRun } from "../runtime/runs.js";
75
- import { GATE_EVALUATION_PHASE } from "../runtime/unit-phases.js";
76
75
  import { frozenSummaryJudge } from "./frozen-judge.js";
76
+ import { mergeLoweringNotices } from "./lowering-notices.js";
77
77
  import { defaultUnitDispatcher, executeStepPlan, } from "./native-executor.js";
78
78
  // Shared step semantics — route evaluation + cascaded-skip bookkeeping,
79
79
  // gate-evaluation journaling, and the whole step-completion path
@@ -103,7 +103,8 @@ export async function runWorkflowSteps(options) {
103
103
  }, liveEvidence);
104
104
  executed.push(...result.executed);
105
105
  stepsProcessed += result.stepsProcessed;
106
- const aggregate = { ...result, executed, stepsProcessed };
106
+ const notices = mergeLoweringNotices(...executed.map((step) => step.notices));
107
+ const aggregate = { ...result, executed, stepsProcessed, ...(notices ? { notices } : {}) };
107
108
  if (result.run.status !== "failed" || result.aborted || result.gateRejection || remainingRetries <= 0) {
108
109
  return aggregate;
109
110
  }
@@ -128,7 +129,7 @@ async function runWorkflowAttempt(options, liveEvidence) {
128
129
  });
129
130
  // Version/canonical/hash validation precedes every executable mutation,
130
131
  // including lease acquisition. Historical rows remain inspectable/abandonable.
131
- if (!next.done && !options.loadPlan) {
132
+ if (!next.done) {
132
133
  await withWorkflowRunsRepo((repo) => {
133
134
  const row = repo.getRunById(next.run.id);
134
135
  if (!row)
@@ -367,22 +368,20 @@ async function completedRunResult(runId) {
367
368
  ...(doneState.run.status === "completed" ? { done: true } : {}),
368
369
  };
369
370
  }
370
- function workflowSummaryJudge(options, plan, stepPlan, signal, owner) {
371
+ function workflowSummaryJudge(options, stepPlan, signal, owner) {
371
372
  if (options.summaryJudge !== undefined)
372
373
  return options.summaryJudge;
373
374
  // The judge dispatches under the REAL run/step identity; the per-loop gate row
374
375
  // identity is threaded in per call by the completion path that journals it.
375
- return frozenSummaryJudge(plan, stepPlan.gate.judge, signal, options.dispatcher ?? defaultUnitDispatcher, owner);
376
+ return frozenSummaryJudge(stepPlan.gate.frozenJudge, signal, options.dispatcher ?? defaultUnitDispatcher, owner);
376
377
  }
377
378
  /**
378
379
  * Seed the lifetime unit cap AND the budget ceilings from the journal so
379
380
  * both are truly per-RUN: a resumed or re-invoked run must not restart the
380
- * runaway backstop — or a declared `budget` — at zero. Journal rows = past
381
- * dispatch ATTEMPTS (counted against `budget.max_units`); their summed
382
- * `tokens` column is the run's spend so far (counted against
383
- * `budget.max_tokens`). The executor consumes both only on new dispatches
384
- * (durable-row reuses are free), so a large partially-completed fan-out
385
- * stays resumable.
381
+ * runaway backstop — or a declared `budget` — at zero. The append-only attempt
382
+ * journal is authoritative: dispatch attempts count against
383
+ * `budget.max_units`, and their known tokens count against
384
+ * `budget.max_tokens`. Durable result reuse is free.
386
385
  *
387
386
  * Gate-evaluation rows (`phase = "gate"`, journaled by the completion-gate
388
387
  * judge) are EXCLUDED from the seed: the live path never consumes
@@ -392,20 +391,14 @@ function workflowSummaryJudge(options, plan, stepPlan, signal, owner) {
392
391
  * that `on_error` cannot soften. The seed must reproduce exactly what live
393
392
  * accounting would have accumulated.
394
393
  *
395
- * The seed sums each dispatch row's `attempts` (migration 008), NOT the row
396
- * COUNT: a crash between a unit's dispatch and its finish leaves a `running`
397
- * row that resume re-dispatches under the SAME content-derived unit_id, and
398
- * `insertUnit` REPLACES that one row while bumping `attempts`. Counting rows
399
- * would erase every prior crash-retried dispatch from budget/lifetime
400
- * accounting, letting the run spend past its declared ceiling; summing
401
- * `attempts` charges each dispatch exactly once.
394
+ * Attempt rows are append-only, so retries cannot collapse or erase prior
395
+ * dispatch accounting.
402
396
  */
403
397
  async function seedRunAccountingFromJournal(runId) {
404
- const journaledUnits = await withWorkflowRunsRepo((repo) => repo.getUnitsForRun(runId));
405
- const journaledDispatches = journaledUnits.filter((row) => row.phase !== GATE_EVALUATION_PHASE);
398
+ const accounting = await withWorkflowRunsRepo((repo) => repo.getAttemptAccounting(runId));
406
399
  return {
407
- unitsDispatched: journaledDispatches.reduce((sum, row) => sum + row.attempts, 0),
408
- tokensUsed: journaledDispatches.reduce((sum, row) => sum + (row.tokens ?? 0), 0),
400
+ unitsDispatched: accounting.dispatchAttempts,
401
+ tokensUsed: accounting.dispatchTokens,
409
402
  };
410
403
  }
411
404
  /**
@@ -418,14 +411,14 @@ async function seedRunAccountingFromJournal(runId) {
418
411
  * unit is dispatched (start already validated the params it stored).
419
412
  */
420
413
  async function loadAuthoritativeRunPlan(options, next) {
421
- const plan = await loadFrozenPlan(next.run.id);
414
+ const stored = await loadStoredPlan(next.run.id);
422
415
  if (options.loadPlan) {
423
- const expected = decodeWorkflowPlanV3(await options.loadPlan(next.run.workflowRef));
424
- if (computePlanHash(expected) !== computePlanHash(plan))
416
+ const expected = decodeWorkflowPlanV4(await options.loadPlan(next.run.workflowRef));
417
+ if (computePlanHash(expected) !== computePlanHash(stored))
425
418
  throw new UsageError(`Injected workflow plan for run ${next.run.id} differs from its frozen plan.`);
426
419
  }
427
- assertRunParamsSatisfyPlan(next.run.id, plan, next.run.params ?? {});
428
- return plan;
420
+ assertRunParamsSatisfyPlan(next.run.id, stored, next.run.params ?? {});
421
+ return stored;
429
422
  }
430
423
  /**
431
424
  * Complete a branch target no completed router selected as `skipped` — no
@@ -515,7 +508,6 @@ async function executeStepSubgraph(ctx, loop) {
515
508
  // Budget ceilings ride the FROZEN plan (addendum R2): a mid-run
516
509
  // asset edit can never loosen or tighten a run's budget.
517
510
  ...(plan.budget ? { budget: plan.budget } : {}),
518
- ...(plan.execution ? { engines: plan.execution.engines } : {}),
519
511
  gateLoop,
520
512
  ...(gateFeedback ? { gateFeedback } : {}),
521
513
  // The heartbeat's signal is the effective dispatch signal: a lost
@@ -571,6 +563,7 @@ async function runStepGateLoop(ctx, gate, totals) {
571
563
  unitCount: result.units.length,
572
564
  failedUnits: result.units.filter((u) => !u.ok).length,
573
565
  summary: result.summary,
566
+ ...(result.notices ? { notices: result.notices } : {}),
574
567
  });
575
568
  // Route evaluation + artifact-judged completion gate + gate-row
576
569
  // journaling + the bounded-loop rejection contract are the SHARED
@@ -594,7 +587,6 @@ async function runStepGateLoop(ctx, gate, totals) {
594
587
  routeSelected,
595
588
  routeUnselected,
596
589
  summaryJudge,
597
- ...(ctx.plan.execution ? { engines: ctx.plan.execution.engines } : {}),
598
590
  signal: options.signal,
599
591
  // The judge runs under the DISPATCH signal, so the completion path must
600
592
  // see it too: an abort delivered there (a lost lease, a caller Ctrl-C)
@@ -783,7 +775,7 @@ liveEvidence) {
783
775
  // verify. No gate loop is consumed and nothing is dispatched.
784
776
  let summaryJudge;
785
777
  try {
786
- summaryJudge = workflowSummaryJudge(options, plan, stepPlan, dispatchSignal, {
778
+ summaryJudge = workflowSummaryJudge(options, stepPlan, dispatchSignal, {
787
779
  runId: next.run.id,
788
780
  stepId: step.id,
789
781
  });
@@ -838,10 +830,12 @@ liveEvidence) {
838
830
  }
839
831
  // Re-read for the freshest run state (the loop may have exited on maxSteps).
840
832
  const finalState = await getNextWorkflowStep(next.run.id);
833
+ const notices = mergeLoweringNotices(...executed.map((step) => step.notices));
841
834
  return {
842
835
  run: finalState.run,
843
836
  executed,
844
837
  stepsProcessed,
838
+ ...(notices ? { notices } : {}),
845
839
  ...(finalState.run.status === "completed" ? { done: true } : {}),
846
840
  ...(gateRejection ? { gateRejection } : {}),
847
841
  ...(judgeFailure ? { judgeFailure } : {}),
@@ -858,7 +852,7 @@ liveEvidence) {
858
852
  * Missing and non-current plans fail validation and are never rebuilt from a
859
853
  * mutable source asset.
860
854
  */
861
- async function loadFrozenPlan(runId) {
855
+ async function loadStoredPlan(runId) {
862
856
  const row = await withWorkflowRunsRepo((repo) => {
863
857
  const run = repo.getRunById(runId);
864
858
  return run;
@@ -38,14 +38,12 @@
38
38
  * the executor's job (`native-executor.ts`), advancing the gated spine is the
39
39
  * engine loop's job (`run-workflow.ts` via `completeWorkflowStep`).
40
40
  */
41
- import { createHash } from "node:crypto";
41
+ import { createHash, randomUUID } from "node:crypto";
42
42
  import unitPreambleTemplate from "../../assets/prompts/workflow-unit-preamble.md" with { type: "text" };
43
43
  import { UsageError } from "../../core/errors.js";
44
- import { appendEvent } from "../../core/events.js";
45
44
  import { validateJsonSchemaSubset } from "../../core/json-schema.js";
46
- import { withWorkflowRunsRepo } from "../../storage/repositories/workflow-runs-repository.js";
45
+ import { withWorkflowRunsRepo, } from "../../storage/repositories/workflow-runs-repository.js";
47
46
  import { canonicalJson as canonicalJsonString } from "../ir/plan-hash.js";
48
- import { engineRuntimeKind } from "../ir/schema.js";
49
47
  import { parseReference, resolveReferenceString, } from "../program/expressions.js";
50
48
  import { clip, WORKFLOW_MAX_MAP_EXPANSION, WORKFLOW_UNIT_DIAGNOSTIC_CLIP } from "../resource-limits.js";
51
49
  import { completeWorkflowStep, isTruncatedEvidence, } from "../runtime/runs.js";
@@ -67,7 +65,7 @@ const GATE_ARTIFACT_CLIP = 4_000;
67
65
  *
68
66
  * Whole-list failures (missing subgraph, unresolvable / non-array `over`,
69
67
  * null or duplicate fan-out items) return `{ ok: false }`. Per-unit resolution
70
- * cannot fail in the unified format — prose is never scanned for references,
68
+ * cannot fail in the shared source IR — prose is never scanned for references,
71
69
  * and everything that CAN fail (map.over / route.input / inputs:) resolves
72
70
  * once per step, failing the whole list above.
73
71
  */
@@ -119,8 +117,13 @@ function validateFanOutItems(stepId, items) {
119
117
  * goes through here.
120
118
  */
121
119
  function resolveStepReference(reference, scope) {
122
- const resolved = resolveReferenceString(reference, scope);
123
- const truncated = truncatedReferenceTarget(reference, scope, resolved);
120
+ // Source adapters may retain GitHub's whole-value `${{ ... }}` spelling.
121
+ // The source IR owns GitHub's whole-value spelling; this work-list seam
122
+ // unwraps only an exact whole-value wrapper and never interpolates prose.
123
+ const exactWrapper = /^\$\{\{\s*([^{}]+?)\s*\}\}$/.exec(reference);
124
+ const canonicalReference = exactWrapper?.[1] ?? reference;
125
+ const resolved = resolveReferenceString(canonicalReference, scope);
126
+ const truncated = truncatedReferenceTarget(canonicalReference, scope, resolved);
124
127
  if (!truncated)
125
128
  return resolved;
126
129
  return {
@@ -212,22 +215,11 @@ export function computeStepWorkList(plan, input) {
212
215
  return { ok: false, error: fanOutProblem };
213
216
  // Content-derived unit identity: compute every id up front (duplicate items
214
217
  // were rejected above — identity requires distinct items).
215
- const unitIds = items.map((item) => unitIdFor(template.id, item, isFanOut));
218
+ const unitIds = items.map((item) => unitIdFor(template.id, item, isFanOut, true));
216
219
  const gateLoop = input.gateLoop ?? 1;
217
- // An `exec` unit dispatches a child process instead of an engine call, so it
218
- // carries a frozen exec spec and NO invocation (the frozen-plan decoder
219
- // enforces that exclusive-or). Everything downstream identity, hashing,
220
- // journaling, retry, budget — is shared; only the dispatch mechanism differs.
221
- const frozenExec = template.exec;
222
- const frozenInvocation = template.invocation;
223
- if (!frozenExec && !frozenInvocation)
224
- return { ok: false, error: `Step "${plan.stepId}" has no frozen invocation.` };
225
- const frozenEngine = frozenInvocation ? input.engines?.[frozenInvocation.engine] : undefined;
226
- if (frozenInvocation && !frozenEngine) {
227
- return { ok: false, error: `Step "${plan.stepId}" references missing frozen engine "${frozenInvocation.engine}".` };
228
- }
229
- // No frozen engine at all means an exec unit: it carries argv, not an invocation.
230
- const runner = frozenEngine ? engineRuntimeKind(frozenEngine) : "exec";
220
+ const target = template.frozenTarget;
221
+ const frozenExec = target.kind === "shell" || target.kind === "script" ? target.exec : undefined;
222
+ const runner = target.kind === "command" ? target.runner.kind : "exec";
231
223
  // Taken VERBATIM from the frozen plan — there is no engine-side backstop, by
232
224
  // design. The whole timeout decision happens once at freeze time
233
225
  // (`ir/freeze.ts` `effectiveTimeout`: unit `timeout:` → document
@@ -243,7 +235,7 @@ export function computeStepWorkList(plan, input) {
243
235
  // An exec unit's budget is frozen on its exec spec (there is no engine to
244
236
  // inherit one from); `ir/freeze.ts` resolved it once from unit `timeout:` →
245
237
  // `defaults.timeout` → DEFAULT_EXEC_TIMEOUT_MS.
246
- const timeoutMs = frozenExec ? frozenExec.timeoutMs : (frozenInvocation?.timeoutMs ?? null);
238
+ const timeoutMs = target.kind === "command" ? (target.runner.timeoutMs ?? null) : target.exec.timeoutMs;
247
239
  // Step-constant exec context: `AKM_PARAMS` / `AKM_INPUTS` depend only on
248
240
  // step-level values, so they are serialized ONCE here and shared by every
249
241
  // unit. Building them inside the per-unit loop deep-cloned and re-stringified
@@ -262,8 +254,7 @@ export function computeStepWorkList(plan, input) {
262
254
  resolvedInputs,
263
255
  runner,
264
256
  timeoutMs,
265
- ...(frozenEngine ? { frozenEngine } : {}),
266
- ...(frozenInvocation ? { frozenInvocation } : {}),
257
+ target,
267
258
  ...(frozenExec ? { frozenExec } : {}),
268
259
  ...(execParamsJson !== undefined ? { execParamsJson } : {}),
269
260
  ...(execInputsJson !== undefined ? { execInputsJson } : {}),
@@ -286,7 +277,7 @@ export function computeStepWorkList(plan, input) {
286
277
  * repo's 220-line function bar.
287
278
  */
288
279
  function buildStepWorkUnit(ctx, unitId, item, index) {
289
- const { plan, input, template, isFanOut, resolvedInputs, frozenEngine, frozenInvocation, frozenExec } = ctx;
280
+ const { plan, input, template, isFanOut, resolvedInputs, target, frozenExec } = ctx;
290
281
  // Gate loops (>= 2) journal under `<unitId>~l<loop>` so loop 1's rows are
291
282
  // never clobbered; the content-derived identity (and the prompt's
292
283
  // {{UNIT_ID}}) stays the base id.
@@ -324,21 +315,12 @@ function buildStepWorkUnit(ctx, unitId, item, index) {
324
315
  isFanOut,
325
316
  journalBaseId,
326
317
  runner: ctx.runner,
327
- ...(frozenEngine ? { engine: frozenEngine } : {}),
328
- ...(frozenEngine?.kind === "agent" &&
329
- frozenEngine.fallbackLlmEngine &&
330
- input.engines?.[frozenEngine.fallbackLlmEngine]?.kind === "llm"
331
- ? {
332
- fallbackEngine: input.engines[frozenEngine.fallbackLlmEngine],
333
- }
334
- : {}),
335
- ...(frozenInvocation ? { invocation: frozenInvocation } : {}),
336
- ...(frozenExec ? { exec: frozenExec } : {}),
318
+ frozenTarget: target,
319
+ environment: template.environment,
337
320
  ...(frozenExec ? { execContext: buildExecContextEnv({ ctx, unitId, item, index }) } : {}),
338
- ...(frozenInvocation?.model ? { model: frozenInvocation.model } : {}),
321
+ ...(target.kind === "command" && target.request.model?.resolved ? { model: target.request.model.resolved } : {}),
339
322
  timeoutMs: ctx.timeoutMs,
340
323
  ...(template.schema ? { schema: template.schema } : {}),
341
- ...(template.env ? { env: template.env } : {}),
342
324
  ...(template.retry ? { retry: template.retry } : {}),
343
325
  onError: template.onError,
344
326
  ...(template.isolation ? { isolation: template.isolation } : {}),
@@ -349,7 +331,7 @@ function buildStepWorkUnit(ctx, unitId, item, index) {
349
331
  /**
350
332
  * The `AKM_*` context environment an exec unit's child receives.
351
333
  *
352
- * An exec unit's argv is FROZEN and never interpolated (the unified format has
334
+ * An exec unit's argv is FROZEN and never interpolated (the shared source IR has
353
335
  * no substitution language at all), so this is how a fan-out item, the run
354
336
  * params, and the step's declared `inputs:` artifacts actually reach a command
355
337
  * — as attached environment, exactly as they reach an engine unit as attached
@@ -405,8 +387,8 @@ function buildExecContextEnv(args) {
405
387
  * declared-input artifacts + the params snapshot — instead of a
406
388
  * resolved/spliced prompt string, since there is no more splicing.
407
389
  *
408
- * Included beyond the R4 baseline (template/runner/model/schema): resolved
409
- * timeoutMs (via `invocation`/`exec`), the env asset ref NAMES, and isolation —
390
+ * Included beyond the template/runner/model/schema baseline: resolved
391
+ * timeoutMs, named environment bindings, and isolation —
410
392
  * each reaches dispatch and a changed one yields a materially different call.
411
393
  * `env` carries NAMES ONLY, never resolved values: hashing a resolved secret
412
394
  * would leak it into a durable hash oracle and would spuriously re-dispatch on
@@ -421,33 +403,30 @@ function buildExecContextEnv(args) {
421
403
  * the rejected attempt. Replay-safe: feedback is re-derived from the journaled
422
404
  * gate decision, so a resumed retry re-hashes identically.
423
405
  *
424
- * `exec` is likewise conditional, which is what made the exec unit ADDITIVE:
425
- * an exec unit has no engine and no invocation, and its frozen spec (argv, cwd,
426
- * resolved timeout) is exactly what makes its dispatch different so it gets
427
- * its own key, present only on exec units, and `hashVersion` stays 4. Every
428
- * previously-frozen llm/agent/sdk unit therefore hashes byte-identically to
429
- * before, and no in-flight run re-dispatches work it already completed.
406
+ * Command targets carry their frozen argv/script/cwd/timeout identity in the
407
+ * same target slot used by agent, SDK, and direct-LLM work. The complete
408
+ * current target is hashed once, so a completed unit is reused only for the
409
+ * exact durable request that originally produced it.
430
410
  *
431
- * Ambient config is DELIBERATELY excluded the model-alias table, the resolved
432
- * backend/connection, and the working directory (`ctx.workDir` /
433
- * `process.cwd()`) are NOT plan-frozen. The frozen plan is the identity
434
- * boundary (redesign addendum determinism bar #2): config drift under an
435
- * in-flight run is out of scope by design.
411
+ * Ambient config is deliberately excluded because it is not consulted during
412
+ * execution. The frozen target and named environment bindings are the runtime
413
+ * identity boundary; only the values behind those names remain live.
436
414
  */
437
415
  function computeUnitInputHash(ctx, item) {
438
- const dispatch = ctx.frozenEngine ? transitiveDispatchSnapshot(ctx.frozenEngine, ctx.input.engines ?? {}) : null;
439
416
  return createHash("sha256")
417
+ .update("akm.workflow.unit\0v5\0")
440
418
  .update(canonicalJsonString({
441
- hashVersion: 4,
419
+ hashVersion: 5,
420
+ role: "unit",
421
+ stepId: ctx.plan.stepId,
422
+ nodeId: ctx.template.id,
442
423
  template: ctx.template.instructions,
443
424
  item: ctx.isFanOut ? (item ?? null) : null,
444
425
  inputs: ctx.resolvedInputs,
445
426
  params: ctx.input.params,
446
- dispatch,
447
- invocation: ctx.frozenInvocation ?? null,
448
- ...(ctx.frozenExec ? { exec: ctx.frozenExec } : {}),
427
+ frozenTarget: ctx.target,
428
+ environment: ctx.template.environment,
449
429
  schema: ctx.template.schema ?? null,
450
- env: ctx.template.env ?? null,
451
430
  isolation: ctx.template.isolation ?? "none",
452
431
  ...(ctx.input.gateFeedback ? { gateFeedback: ctx.input.gateFeedback } : {}),
453
432
  }))
@@ -498,27 +477,18 @@ export function buildUnitPrompt(input) {
498
477
  return `${preamble}\n${instructions}${itemBlock}${inputsBlock}${gateBlock}${schemaDirective}`;
499
478
  }
500
479
  /**
501
- * Content-derived unit identity (module doc): `<node_id>:<hash12>` for a
480
+ * Content-derived unit identity (module doc): `<node_id>:<sha256>` for a
502
481
  * fan-out item, `<node_id>:solo` otherwise. The hash is over the item's
503
482
  * canonical JSON (sorted keys — same canonicalization the vote reducer
504
483
  * counts with), so identity survives list reordering/regeneration and is
505
484
  * independent of item position. Retry attempts stack `~r<n>` on top.
506
485
  */
507
- export function unitIdFor(nodeId, item, isFanOut) {
486
+ export function unitIdFor(nodeId, item, isFanOut, collisionSafe = false) {
508
487
  if (!isFanOut)
509
488
  return `${nodeId}:solo`;
510
489
  const canonical = canonicalJson(item) ?? "null";
511
- return `${nodeId}:${createHash("sha256").update(canonical).digest("hex").slice(0, 12)}`;
512
- }
513
- /** Include an SDK fallback in v3 call identity without copying catalog entries onto nodes. */
514
- function transitiveDispatchSnapshot(engine, engines) {
515
- if (engine.kind !== "agent" || !engine.fallbackLlmEngine)
516
- return engine;
517
- const fallback = engines[engine.fallbackLlmEngine];
518
- if (!fallback || fallback.kind !== "llm") {
519
- throw new UsageError(`Frozen agent engine "${engine.name}" has no valid LLM fallback snapshot.`);
520
- }
521
- return { engine, fallback };
490
+ const digest = createHash("sha256").update(canonical).digest("hex");
491
+ return `${nodeId}:${collisionSafe ? digest : digest.slice(0, 12)}`;
522
492
  }
523
493
  // ── Step outputs + reducers + typed artifacts ────────────────────────────────
524
494
  /**
@@ -856,7 +826,8 @@ export function gateUnitId(stepId, loop) {
856
826
  */
857
827
  export function effectiveGateMaxLoops(stepPlan) {
858
828
  const declared = Math.max(1, stepPlan.gate.maxLoops ?? 1);
859
- return stepTemplate(stepPlan)?.exec ? 1 : declared;
829
+ const target = stepTemplate(stepPlan)?.frozenTarget;
830
+ return target && target.kind !== "command" ? 1 : declared;
860
831
  }
861
832
  /**
862
833
  * The gate loop the engine is about to (re-)run for an ACTIVE step, derived
@@ -958,26 +929,27 @@ function gateCorruptionMessage(row, why) {
958
929
  /** Insert the gate-evaluation unit row (running) just before the judge runs. */
959
930
  export async function journalGateEvaluationStart(gate) {
960
931
  const unitId = gateUnitId(gate.stepId, gate.loop);
961
- await enqueueUnitWrite(() => withWorkflowRunsRepo((repo) => repo.insertUnit({
932
+ const now = new Date().toISOString();
933
+ const claimHolder = gate.claimHolder ?? `direct:${randomUUID()}`;
934
+ const reserved = await enqueueUnitWrite(() => withWorkflowRunsRepo((repo) => repo.reserveUnitAttempt({
962
935
  runId: gate.runId,
963
936
  unitId,
964
937
  stepId: gate.stepId,
965
938
  nodeId: gateNodeId(gate.stepId),
966
- parentUnitId: null,
967
- // Marks the row as a judge call, NOT a dispatch: the budget/lifetime
968
- // seed in `driveRun` skips these so resume accounting matches live.
969
939
  phase: GATE_EVALUATION_PHASE,
970
940
  runner: gate.runner,
971
- engine: gate.invocation.engine,
972
- model: gate.invocation.model,
941
+ engine: gate.engine,
942
+ model: gate.model,
973
943
  inputHash: gate.inputHash,
974
- startedAt: new Date().toISOString(),
944
+ claimHolder,
945
+ claimExpiresAt: new Date(Date.parse(now) + 90_000).toISOString(),
946
+ now,
947
+ leaseMode: gate.claimHolder === undefined ? "direct" : "engine",
975
948
  })));
976
- appendEvent({
977
- eventType: "workflow_unit_started",
978
- ref: gate.workflowRef,
979
- metadata: { runId: gate.runId, stepId: gate.stepId, unitId },
980
- });
949
+ if (reserved.kind === "busy") {
950
+ throw new UsageError(`Gate ${unitId} has a live durable attempt held by another engine.`);
951
+ }
952
+ return { ...gate, claimHolder, durableAttempt: reserved.attempt };
981
953
  }
982
954
  /**
983
955
  * Finish the gate-evaluation unit row with the verdict as observed from the
@@ -997,20 +969,26 @@ export async function journalGateEvaluationFinish(gate, errored, rejection) {
997
969
  ? { complete: false, missing: rejection.missing, feedback: rejection.feedback }
998
970
  : { complete: true, missing: [] };
999
971
  const status = errored ? "failed" : "completed";
1000
- await enqueueUnitWrite(() => withWorkflowRunsRepo((repo) => repo.finishUnit({
1001
- runId: gate.runId,
1002
- unitId,
1003
- status,
1004
- resultJson: verdict ? JSON.stringify(verdict) : null,
1005
- tokens: null,
1006
- failureReason: errored ? "dispatch_error" : null,
1007
- finishedAt: new Date().toISOString(),
1008
- })));
1009
- appendEvent({
1010
- eventType: "workflow_unit_finished",
1011
- ref: gate.workflowRef,
1012
- metadata: { runId: gate.runId, stepId: gate.stepId, unitId, status },
1013
- });
972
+ if (!gate.durableAttempt)
973
+ throw new UsageError(`Gate ${unitId} has no durable attempt to finish.`);
974
+ const durableAttempt = gate.durableAttempt;
975
+ const finished = await enqueueUnitWrite(() => withWorkflowRunsRepo((repo) => {
976
+ return repo.finishUnitAttempt({
977
+ runId: gate.runId,
978
+ unitId,
979
+ attempt: durableAttempt.attempt,
980
+ dispatchId: durableAttempt.dispatch_id,
981
+ claimHolder: durableAttempt.claim_holder,
982
+ status,
983
+ resultJson: verdict ? JSON.stringify(verdict) : null,
984
+ tokens: gate.tokens ?? null,
985
+ failureReason: errored ? "dispatch_error" : null,
986
+ finishedAt: new Date().toISOString(),
987
+ });
988
+ }));
989
+ if (!finished) {
990
+ throw new UsageError(`Gate ${unitId} no longer owns its durable attempt; refusing a late terminal write.`);
991
+ }
1014
992
  }
1015
993
  /**
1016
994
  * Resolve a route's input (a single whole-value reference string — `params.x` or
@@ -1290,8 +1268,7 @@ export async function finalizeExecutedStep(input) {
1290
1268
  // Journal engine-driven judge calls as unit rows. With no criteria there is
1291
1269
  // no judge invocation or row; a criteria-bearing plan without a judge is a
1292
1270
  // configuration error rather than a silent bypass.
1293
- const gateInvocation = innerJudge ? (stepPlan.gate.judge ?? null) : null;
1294
- const gateEngine = gateInvocation ? (input.engines?.[gateInvocation.engine] ?? null) : null;
1271
+ const gateTarget = innerJudge ? stepPlan.gate.frozenJudge : null;
1295
1272
  let gateUnit;
1296
1273
  // `judgeFailure` records a verifier INFRASTRUCTURE failure observed during
1297
1274
  // the judge call — a throw (transport/service error) or a response that is
@@ -1301,32 +1278,51 @@ export async function finalizeExecutedStep(input) {
1301
1278
  let judgeFailure;
1302
1279
  const summaryJudge = innerJudge
1303
1280
  ? async (prompt) => {
1304
- if (gateInvocation) {
1281
+ if (gateTarget) {
1282
+ const engineName = gateTarget.request.engine.name;
1283
+ if (!engineName)
1284
+ throw new Error(`Gate ${stepId} has no frozen engine identity.`);
1305
1285
  gateUnit = {
1306
1286
  runId,
1307
1287
  workflowRef,
1308
1288
  stepId,
1309
1289
  loop: gateLoop,
1310
- invocation: gateInvocation,
1311
- // No engine snapshot means the built-in llm judge.
1312
- runner: gateEngine ? engineRuntimeKind(gateEngine) : "llm",
1290
+ engine: engineName,
1291
+ model: gateTarget.request.model?.resolved ?? null,
1292
+ runner: gateTarget.runner.kind,
1313
1293
  inputHash: createHash("sha256")
1294
+ .update("akm.workflow.gate\0v5\0")
1314
1295
  .update(canonicalJsonString({
1315
- hashVersion: 3,
1316
- dispatch: gateEngine,
1317
- invocation: gateInvocation,
1296
+ hashVersion: 5,
1297
+ dispatch: gateTarget,
1298
+ invocation: null,
1318
1299
  prompt,
1319
1300
  }))
1320
1301
  .digest("hex"),
1302
+ ...(input.leaseHolder !== undefined ? { claimHolder: input.leaseHolder } : {}),
1321
1303
  };
1322
- await journalGateEvaluationStart(gateUnit);
1304
+ gateUnit = await journalGateEvaluationStart(gateUnit);
1323
1305
  }
1324
1306
  // The judge dispatch must describe the SAME thing the gate row does, so
1325
1307
  // the row identity is threaded down to the dispatcher from right here —
1326
1308
  // the one place that computes it — instead of being re-derived (or, as
1327
1309
  // before, synthesized as a constant "gate"). Both ids come from the same
1328
1310
  // helpers `journalGateEvaluationStart/Finish` use.
1329
- const identity = { runId, stepId, unitId: gateUnitId(stepId, gateLoop) };
1311
+ const identity = {
1312
+ runId,
1313
+ stepId,
1314
+ unitId: gateUnitId(stepId, gateLoop),
1315
+ ...(gateUnit?.durableAttempt
1316
+ ? {
1317
+ attempt: gateUnit.durableAttempt.attempt,
1318
+ dispatchId: gateUnit.durableAttempt.dispatch_id,
1319
+ recordTokens: (tokens) => {
1320
+ if (gateUnit)
1321
+ gateUnit.tokens = tokens;
1322
+ },
1323
+ }
1324
+ : {}),
1325
+ };
1330
1326
  let raw;
1331
1327
  try {
1332
1328
  raw = await innerJudge(prompt, identity);