akm-cli 0.9.17-alpha.2 → 0.9.17-alpha.4

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 (343) hide show
  1. package/CHANGELOG.md +756 -0
  2. package/dist/akm +94 -196
  3. package/dist/cli/shared.js +6 -2
  4. package/dist/cli.js +22 -9
  5. package/dist/commands/agent/agent-dispatch.js +1 -1
  6. package/dist/commands/command/command-execution.js +24 -62
  7. package/dist/commands/feedback-cli.js +0 -1
  8. package/dist/commands/health/accept-rate.js +2 -2
  9. package/dist/commands/health/checks.js +30 -75
  10. package/dist/commands/health/config-skew.js +38 -0
  11. package/dist/commands/health/egress.js +54 -0
  12. package/dist/commands/health/html-report.js +0 -38
  13. package/dist/commands/health/improve-metrics.js +123 -562
  14. package/dist/commands/health/plugin-staleness.js +53 -3
  15. package/dist/commands/health/renderers.js +12 -4
  16. package/dist/commands/health/report-view-model.js +11 -106
  17. package/dist/commands/health/types-improve.js +4 -19
  18. package/dist/commands/health/windows.js +64 -73
  19. package/dist/commands/health.js +122 -143
  20. package/dist/commands/improve/consolidate/chunking.js +25 -100
  21. package/dist/commands/improve/consolidate/sanitize.js +54 -149
  22. package/dist/commands/improve/consolidate.js +538 -1075
  23. package/dist/commands/improve/content-hash.js +16 -24
  24. package/dist/commands/improve/distill/content-repair.js +18 -100
  25. package/dist/commands/improve/distill-guards.js +20 -81
  26. package/dist/commands/improve/distill-promotion-policy.js +23 -243
  27. package/dist/commands/improve/distill.js +608 -1075
  28. package/dist/commands/improve/eligibility.js +126 -400
  29. package/dist/commands/improve/execution.js +3 -5
  30. package/dist/commands/improve/extract.js +487 -1046
  31. package/dist/commands/improve/feedback-valence.js +0 -25
  32. package/dist/commands/improve/improve-cli.js +29 -166
  33. package/dist/commands/improve/improve-result-file.js +10 -66
  34. package/dist/commands/improve/improve-strategies.js +12 -7
  35. package/dist/commands/improve/improve-usage-report.js +18 -64
  36. package/dist/commands/improve/improve.js +443 -1063
  37. package/dist/commands/improve/ledger.js +114 -0
  38. package/dist/commands/improve/locks.js +2 -8
  39. package/dist/commands/improve/loop-stages.js +459 -1172
  40. package/dist/commands/improve/memory/derived-ref.js +12 -77
  41. package/dist/commands/improve/memory/memory-belief.js +14 -118
  42. package/dist/commands/improve/memory/memory-improve.js +4 -3
  43. package/dist/commands/improve/outcome-loop.js +28 -156
  44. package/dist/commands/improve/planner.js +5 -10
  45. package/dist/commands/improve/preparation.js +851 -2339
  46. package/dist/commands/improve/proactive-maintenance.js +34 -101
  47. package/dist/commands/improve/reflect-noise.js +104 -280
  48. package/dist/commands/improve/reflect.js +621 -1367
  49. package/dist/commands/improve/salience.js +46 -232
  50. package/dist/commands/improve/session-asset.js +19 -100
  51. package/dist/commands/improve/stage.js +323 -0
  52. package/dist/commands/proposal/drain.js +251 -644
  53. package/dist/commands/proposal/proposal-cli.js +3 -18
  54. package/dist/commands/proposal/proposal-types.js +20 -41
  55. package/dist/commands/proposal/proposal.js +1 -2
  56. package/dist/commands/proposal/propose.js +134 -160
  57. package/dist/commands/proposal/repository.js +502 -1487
  58. package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
  59. package/dist/commands/proposal/validators/proposal-validators.js +1 -1
  60. package/dist/commands/proposal/validators/proposals.js +13 -89
  61. package/dist/commands/read/curate.js +63 -413
  62. package/dist/commands/read/search-cli.js +16 -33
  63. package/dist/commands/read/search.js +17 -23
  64. package/dist/commands/read/show.js +2 -13
  65. package/dist/commands/sources/bundle-cli.js +25 -2
  66. package/dist/commands/sources/bundle-config-ops.js +7 -0
  67. package/dist/commands/sources/dangerous-env-audit.js +1 -2
  68. package/dist/commands/sources/info.js +2 -11
  69. package/dist/commands/sources/installed-stashes.js +197 -746
  70. package/dist/commands/sources/schema-repair.js +98 -129
  71. package/dist/commands/sources/source-add.js +62 -12
  72. package/dist/commands/sources/stash-cli.js +1 -1
  73. package/dist/commands/tasks/explain.js +10 -13
  74. package/dist/commands/tasks/tasks-cli.js +9 -8
  75. package/dist/commands/tasks/tasks.js +326 -930
  76. package/dist/commands/tasks/validate.js +42 -21
  77. package/dist/commands/workflow/plan.js +22 -29
  78. package/dist/commands/workflow-cli.js +4 -4
  79. package/dist/core/adapter/adapters/akm-adapter.js +0 -1
  80. package/dist/core/adapter/adapters/akm-lint.js +2 -3
  81. package/dist/core/adapter/adapters/akm-metadata.js +11 -12
  82. package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
  83. package/dist/core/adapter/execution-source.js +17 -29
  84. package/dist/core/asset/resolve-ref.js +1 -1
  85. package/dist/core/bundle-id.js +42 -5
  86. package/dist/core/bundle-rename.js +291 -0
  87. package/dist/core/config/config-io.js +1 -2
  88. package/dist/core/config/config-schema.js +1 -33
  89. package/dist/core/config/config-walker.js +1 -1
  90. package/dist/core/config/config.js +163 -68
  91. package/dist/core/config/legacy-source-shape-shim.js +38 -9
  92. package/dist/core/config/schema/embedding.js +20 -5
  93. package/dist/core/config/schema/engines.js +5 -0
  94. package/dist/core/config/schema/execution.js +1 -1
  95. package/dist/core/config/schema/experimental.js +1 -1
  96. package/dist/core/config/schema/improve-processes.js +21 -95
  97. package/dist/core/config/schema/improve.js +4 -42
  98. package/dist/core/config/schema/scheduler.js +12 -12
  99. package/dist/core/config/schema/search.js +6 -22
  100. package/dist/core/env-secret-ref.js +0 -1
  101. package/dist/core/errors.js +8 -9
  102. package/dist/core/file-lock.js +76 -173
  103. package/dist/core/logs-db.js +2 -2
  104. package/dist/core/paths.js +0 -27
  105. package/dist/core/redaction.js +109 -2
  106. package/dist/core/run-lock.js +2 -5
  107. package/dist/core/spawn-env.js +1 -1
  108. package/dist/core/state/migrations.js +108 -61
  109. package/dist/core/state-db-scope.js +2 -4
  110. package/dist/core/state-db.js +126 -692
  111. package/dist/core/type-presentation.js +1 -9
  112. package/dist/core/write-source.js +293 -1012
  113. package/dist/execution/input-contract.js +1 -1
  114. package/dist/execution/resolved-request.js +135 -689
  115. package/dist/execution/source.js +63 -257
  116. package/dist/execution/target-ref.js +1 -1
  117. package/dist/indexer/bundle-identity-guard.js +2 -2
  118. package/dist/indexer/db/graph-db.js +106 -46
  119. package/dist/indexer/ensure-index.js +44 -85
  120. package/dist/indexer/graph/graph-extraction.js +340 -562
  121. package/dist/indexer/graph/graph-related.js +130 -0
  122. package/dist/indexer/index-rebuild-lock.js +3 -11
  123. package/dist/indexer/index-writer-lock.js +8 -17
  124. package/dist/indexer/index-written-assets.js +139 -151
  125. package/dist/indexer/indexer.js +524 -846
  126. package/dist/indexer/materialize-embeddings.js +60 -397
  127. package/dist/indexer/passes/memory-inference.js +81 -90
  128. package/dist/indexer/passes/metadata.js +132 -200
  129. package/dist/indexer/read-preflight.js +0 -7
  130. package/dist/indexer/scan/doc-to-entry.js +1 -3
  131. package/dist/indexer/scan/drain-dir.js +1 -1
  132. package/dist/indexer/search/db-search.js +181 -590
  133. package/dist/indexer/search/fts-query.js +30 -41
  134. package/dist/indexer/search/ranking.js +28 -154
  135. package/dist/indexer/search/search-attribution.js +12 -32
  136. package/dist/indexer/search/search-fields.js +11 -15
  137. package/dist/indexer/search/search-hit-enrichers.js +54 -85
  138. package/dist/indexer/search/search-source.js +1 -4
  139. package/dist/indexer/usage/usage-events.js +2 -7
  140. package/dist/integrations/agent/engine-fallback.js +23 -40
  141. package/dist/integrations/agent/engine-resolution.js +93 -183
  142. package/dist/integrations/agent/execution.js +507 -0
  143. package/dist/integrations/agent/model-map.js +28 -156
  144. package/dist/integrations/agent/request-lowering.js +66 -141
  145. package/dist/integrations/agent/runner-dispatch.js +143 -321
  146. package/dist/integrations/agent/runner.js +54 -14
  147. package/dist/integrations/lockfile.js +53 -101
  148. package/dist/llm/embedders/deterministic.js +2 -3
  149. package/dist/llm/embedders/profile.js +71 -0
  150. package/dist/llm/embedders/remote.js +10 -15
  151. package/dist/llm/graph-extract.js +3 -12
  152. package/dist/llm/index-passes.js +3 -5
  153. package/dist/llm/memory-infer.js +1 -2
  154. package/dist/llm/metadata-enhance.js +1 -2
  155. package/dist/llm/structured-call.js +5 -24
  156. package/dist/output/generic-render.js +23 -11
  157. package/dist/output/html-render.js +13 -10
  158. package/dist/output/render-registry.js +3 -32
  159. package/dist/output/shapes/helpers.js +2 -34
  160. package/dist/output/shapes/passthrough.js +1 -9
  161. package/dist/{indexer/search/ranking-types.js → output/text/bundle-rename.js} +4 -1
  162. package/dist/output/text/command-format.js +60 -23
  163. package/dist/output/text/helpers.js +1 -1
  164. package/dist/output/text/migrate.js +5 -14
  165. package/dist/output/text/proposal-format.js +1 -2
  166. package/dist/output/text/workflow-format.js +0 -32
  167. package/dist/output/text.js +2 -0
  168. package/dist/registry/factory.js +4 -19
  169. package/dist/registry/network.js +66 -220
  170. package/dist/registry/providers/index.js +0 -2
  171. package/dist/registry/providers/skills-sh.js +3 -14
  172. package/dist/registry/providers/static-index.js +24 -26
  173. package/dist/registry/resolve.js +55 -131
  174. package/dist/scripts/akm-migrate-node.js +43937 -93313
  175. package/dist/scripts/akm-migrate.js +43697 -93071
  176. package/dist/setup/registry-stash-loader.js +4 -13
  177. package/dist/setup/semantic-assets.js +3 -44
  178. package/dist/setup/setup.js +1 -1
  179. package/dist/setup/steps/tasks.js +25 -15
  180. package/dist/sources/provider-factory.js +17 -18
  181. package/dist/sources/providers/filesystem.js +2 -3
  182. package/dist/sources/providers/git-install.js +7 -1
  183. package/dist/sources/providers/git-provider.js +0 -3
  184. package/dist/sources/providers/git-stash.js +0 -17
  185. package/dist/sources/providers/npm.js +2 -4
  186. package/dist/sources/providers/provider-utils.js +5 -10
  187. package/dist/sources/providers/website.js +0 -2
  188. package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
  189. package/dist/sources/website-url.js +2 -2
  190. package/dist/storage/database.js +9 -35
  191. package/dist/storage/repositories/improve-ledger-repository.js +168 -0
  192. package/dist/storage/repositories/index-connection.js +34 -70
  193. package/dist/storage/repositories/index-entries-repository.js +69 -111
  194. package/dist/storage/repositories/index-entry-mapper.js +1 -2
  195. package/dist/storage/repositories/index-entry-schema.js +83 -269
  196. package/dist/storage/repositories/index-fts-repository.js +86 -256
  197. package/dist/storage/repositories/index-llm-cache-repository.js +17 -0
  198. package/dist/storage/repositories/index-meta-repository.js +6 -4
  199. package/dist/storage/repositories/index-schema.js +192 -220
  200. package/dist/storage/repositories/index-utility-repository.js +8 -29
  201. package/dist/storage/repositories/index-vec-repository.js +133 -414
  202. package/dist/storage/repositories/outcome-repository.js +2 -1
  203. package/dist/storage/repositories/proposals-repository.js +35 -0
  204. package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
  205. package/dist/storage/repositories/task-history-repository.js +26 -4
  206. package/dist/storage/repositories/workflow-runs-repository.js +53 -244
  207. package/dist/storage/sqlite-migrations.js +136 -0
  208. package/dist/storage/sqlite-pragmas.js +11 -9
  209. package/dist/storage/sqlite-transaction.js +170 -0
  210. package/dist/storage/state-db-integrity.js +34 -27
  211. package/dist/tasks/activation-config.js +134 -62
  212. package/dist/tasks/backends/cron.js +129 -277
  213. package/dist/tasks/backends/exec-utils.js +2 -5
  214. package/dist/tasks/backends/launchd.js +125 -745
  215. package/dist/tasks/backends/schtasks.js +101 -620
  216. package/dist/tasks/prepare/prepare-support.js +5 -15
  217. package/dist/tasks/prepare/prepare.js +0 -2
  218. package/dist/tasks/resolve-akm-bin.js +20 -79
  219. package/dist/tasks/run/attempt-lifecycle.js +0 -1
  220. package/dist/tasks/scheduler-binding.js +18 -238
  221. package/dist/tasks/scheduler-invocation.js +52 -52
  222. package/dist/tasks/scheduler-lock.js +53 -0
  223. package/dist/tasks/scheduler-sync.js +363 -679
  224. package/dist/tasks/source/parse-task-source.js +160 -10
  225. package/dist/tasks/source/task-source-v3-frozen.js +3 -4
  226. package/dist/tasks/source/task-to-v4.js +2 -2
  227. package/dist/workflows/authoring/authoring.js +3 -12
  228. package/dist/workflows/compile.js +211 -0
  229. package/dist/workflows/concurrency-policy.js +13 -74
  230. package/dist/workflows/exec/child-invocation.js +3 -17
  231. package/dist/workflows/exec/child-workflow.js +32 -141
  232. package/dist/workflows/exec/dispatch-redaction.js +13 -53
  233. package/dist/workflows/exec/environment.js +98 -0
  234. package/dist/workflows/exec/exec-unit.js +33 -140
  235. package/dist/workflows/exec/frozen-judge.js +7 -59
  236. package/dist/workflows/exec/native-executor.js +82 -341
  237. package/dist/workflows/exec/param-secrets.js +29 -47
  238. package/dist/workflows/exec/run-workflow.js +154 -387
  239. package/dist/workflows/exec/scheduler.js +9 -36
  240. package/dist/workflows/exec/step-work.js +127 -430
  241. package/dist/workflows/exec/unit-dispatch.js +11 -63
  242. package/dist/workflows/exec/unit-writer.js +8 -52
  243. package/dist/workflows/exec/worktree.js +39 -273
  244. package/dist/workflows/freeze/child-output-references.js +4 -15
  245. package/dist/workflows/freeze/environment.js +99 -92
  246. package/dist/workflows/freeze/freeze.js +172 -0
  247. package/dist/workflows/freeze/step-values.js +19 -21
  248. package/dist/workflows/freeze/targets/child-workflow.js +23 -92
  249. package/dist/workflows/freeze/targets/command.js +10 -33
  250. package/dist/workflows/freeze/targets/script.js +5 -12
  251. package/dist/workflows/freeze/targets/shell.js +3 -6
  252. package/dist/workflows/freeze/targets/task.js +25 -80
  253. package/dist/workflows/freeze/task-bindings.js +20 -67
  254. package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
  255. package/dist/workflows/ir/params.js +6 -51
  256. package/dist/workflows/ir/plan-hash.js +2 -34
  257. package/dist/workflows/parser.js +140 -43
  258. package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
  259. package/dist/workflows/renderer.js +36 -69
  260. package/dist/workflows/resource-limits.js +12 -120
  261. package/dist/workflows/runtime/agent-identity.js +8 -40
  262. package/dist/workflows/runtime/run-outputs.js +3 -6
  263. package/dist/workflows/runtime/run-plan.js +316 -0
  264. package/dist/workflows/runtime/runs.js +48 -200
  265. package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
  266. package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
  267. package/dist/workflows/validate-summary.js +2 -7
  268. package/docs/integration/bundling-akm.md +49 -42
  269. package/docs/migration/README.md +1 -0
  270. package/docs/migration/release-notes/0.9.17.md +41 -0
  271. package/docs/migration/v0.9.1-to-v0.9.2.md +19 -7
  272. package/docs/reference/cli.md +182 -125
  273. package/docs/reference/configuration.md +49 -56
  274. package/docs/reference/data-and-telemetry.md +19 -20
  275. package/docs/reference/tasks.md +86 -38
  276. package/docs/reference/workflow-schema.md +14 -18
  277. package/docs/reference/workflows.md +6 -9
  278. package/package.json +1 -1
  279. package/schemas/akm-config.json +87 -406
  280. package/dist/commands/health/advisories.js +0 -150
  281. package/dist/commands/health/metrics.js +0 -329
  282. package/dist/commands/health/surfaces.js +0 -102
  283. package/dist/commands/improve/anti-collapse.js +0 -83
  284. package/dist/commands/improve/collapse-detector.js +0 -432
  285. package/dist/commands/improve/consolidate/eligibility.js +0 -48
  286. package/dist/commands/improve/consolidate/merge.js +0 -146
  287. package/dist/commands/improve/distill/promote-memory.js +0 -329
  288. package/dist/commands/improve/distill/quality-gate.js +0 -500
  289. package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
  290. package/dist/commands/improve/proposal-envelope.js +0 -31
  291. package/dist/commands/improve/run-context.js +0 -123
  292. package/dist/commands/improve/shared.js +0 -21
  293. package/dist/commands/improve/source-identity.js +0 -28
  294. package/dist/commands/improve/triage.js +0 -96
  295. package/dist/commands/proposal/drain-policies.js +0 -151
  296. package/dist/commands/sources/update-transaction.js +0 -220
  297. package/dist/core/action-contributors.js +0 -28
  298. package/dist/core/config/config-version-shim.js +0 -101
  299. package/dist/core/config/retired-experimental-keys-shim.js +0 -62
  300. package/dist/core/fs-txn.js +0 -405
  301. package/dist/core/lexical-score.js +0 -25
  302. package/dist/core/maintenance-barrier.js +0 -167
  303. package/dist/execution/executable-identity.js +0 -105
  304. package/dist/execution/guarded-source.js +0 -427
  305. package/dist/indexer/graph/graph-boost.js +0 -427
  306. package/dist/indexer/graph/graph-dedup.js +0 -95
  307. package/dist/indexer/search/name-match.js +0 -35
  308. package/dist/indexer/search/ranking-contributors.js +0 -515
  309. package/dist/indexer/walk/project-context.js +0 -192
  310. package/dist/integrations/agent/execution-cascade.js +0 -566
  311. package/dist/integrations/agent/execution-definitions.js +0 -202
  312. package/dist/integrations/agent/execution-lowering.js +0 -841
  313. package/dist/integrations/agent/execution-preparation.js +0 -98
  314. package/dist/integrations/agent/inline-execution.js +0 -74
  315. package/dist/registry/create-provider-registry.js +0 -29
  316. package/dist/registry/pinned-request-helper.js +0 -247
  317. package/dist/registry/pinned-transport.js +0 -717
  318. package/dist/sources/providers/index.js +0 -14
  319. package/dist/storage/engines/sqlite-migrations.js +0 -271
  320. package/dist/storage/repositories/canaries-repository.js +0 -107
  321. package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
  322. package/dist/storage/repositories/registry-cache.js +0 -113
  323. package/dist/tasks/scheduler-sync-preview.js +0 -52
  324. package/dist/workflows/freeze/resolve-steps.js +0 -86
  325. package/dist/workflows/freeze/source-freeze.js +0 -64
  326. package/dist/workflows/ir/compile.js +0 -321
  327. package/dist/workflows/ir/environment-v4.js +0 -330
  328. package/dist/workflows/ir/freeze-v4.js +0 -153
  329. package/dist/workflows/ir/schema-v4.js +0 -745
  330. package/dist/workflows/ir/schema.js +0 -354
  331. package/dist/workflows/program/schema.js +0 -78
  332. package/dist/workflows/runtime/checkin.js +0 -57
  333. package/dist/workflows/runtime/plan-classifier.js +0 -196
  334. package/dist/workflows/runtime/unit-checkin.js +0 -45
  335. package/dist/workflows/runtime/unit-phases.js +0 -20
  336. package/dist/workflows/schema.js +0 -4
  337. package/dist/workflows/source-ir/compile.js +0 -200
  338. package/dist/workflows/source-ir/program.js +0 -50
  339. package/dist/workflows/source-ir/result.js +0 -26
  340. package/dist/workflows/source-ir/schema.js +0 -786
  341. package/dist/workflows/source-ir/triggers.js +0 -79
  342. package/dist/workflows/source-ir/uses.js +0 -40
  343. package/dist/workflows/validator.js +0 -60
@@ -2,19 +2,13 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  /**
5
- * Shared step semantics — the ONE implementation of a step's orchestration
6
- * decisions, consumed by the engine loop (`run-workflow.ts` +
7
- * `native-executor.ts`) on both the fresh-execution and the resume/replay
8
- * path, so a first run and a resumed run of the same frozen plan produce
9
- * byte-identical unit graphs. `computeStepWorkList` and its reducer/gate/route
10
- * helpers are PURE (no clock, no IO, no journal read); the gate-evaluation
11
- * journaling functions are the one deliberate exception. This module never
12
- * dispatches a unit and never writes step rows.
13
- *
14
- * See docs/architecture/decisions/0002-unit-reuse-and-input-hash-scope.md for
15
- * the full purity-contract design history.
5
+ * Shared step semantics: the one implementation of a step's orchestration
6
+ * decisions, used by the engine on both a fresh run and a resume, so the same
7
+ * frozen plan produces byte-identical unit graphs. Pure except the
8
+ * gate-evaluation journaling; never dispatches and never writes step rows.
9
+ * See docs/architecture/decisions/0002-unit-reuse-and-input-hash-scope.md.
16
10
  */
17
- import { createHash, randomUUID } from "node:crypto";
11
+ import { createHash } from "node:crypto";
18
12
  import unitPreambleTemplate from "../../assets/prompts/workflow-unit-preamble.md" with { type: "text" };
19
13
  import { UsageError } from "../../core/errors.js";
20
14
  import { validateJsonSchemaSubset } from "../../core/json-schema.js";
@@ -25,40 +19,18 @@ import { canonicalJson } from "../ir/plan-hash.js";
25
19
  import { parseReference, resolveReferenceString, } from "../program/expressions.js";
26
20
  import { clip, WORKFLOW_UNIT_DIAGNOSTIC_CLIP } from "../resource-limits.js";
27
21
  import { completeWorkflowStep } from "../runtime/runs.js";
28
- import { GATE_EVALUATION_PHASE } from "../runtime/unit-phases.js";
29
22
  import { parseJudgeVerdict } from "../validate-summary.js";
30
23
  import { gateNodeId } from "./frozen-judge.js";
31
24
  import { enqueueUnitWrite } from "./unit-writer.js";
32
25
  /** How much raw unit output is retained in step evidence (full text lives on the unit row). */
33
26
  const EVIDENCE_TEXT_CLIP = 2_000;
34
- /** How much artifact JSON the completion-criteria judge receives (addendum R2, artifact-judging gates). */
27
+ /** How much artifact JSON the completion-criteria judge receives. */
35
28
  const GATE_ARTIFACT_CLIP = 4_000;
36
29
  /**
37
- * Compute a step's expected work-list PURELY from the frozen plan and its
38
- * inputs: resolve the fan-out list, derive content-derived unit ids, assemble
39
- * each unit's prompt (preamble + interpolated instructions + gate feedback +
40
- * schema directive), and hash the resolved input. Same inputs ⇒ byte-identical
41
- * ids/hashes/prompts — the invariant resume/replay relies on to recognize the
42
- * units an earlier run already journaled.
43
- *
44
- * Whole-list failures (missing subgraph, unresolvable / non-array `over`,
45
- * null or duplicate fan-out items) return `{ ok: false }`. Per-unit resolution
46
- * cannot fail in the shared source IR — prose is never scanned for references,
47
- * and everything that CAN fail (map.over / route.input / inputs:) resolves
48
- * once per step, failing the whole list above.
49
- */
50
- /**
51
- * Validate a fan-out item list BEFORE any identity/dispatch work: expansion
52
- * within the resource limit, no null/undefined items, no canonical duplicates.
53
- * Returns the failure message, or undefined when the list is dispatchable.
54
- *
55
- * Null items: producer garbage — there is nothing to hand the unit as its work
56
- * item. The pre-unification format rejected them incidentally (substituting
57
- * `${{ item }}` failed); with items attached as context instead of spliced,
58
- * nothing later would stop a unit from being dispatched with "Item: null", so
59
- * the rejection is explicit here. Duplicates: content-derived unit identity
60
- * makes canonical duplicates collide on id — an authoring error caught
61
- * deterministically, before dispatch.
30
+ * Validate a fan-out item list before any identity/dispatch work: no
31
+ * null/undefined items (there would be nothing to hand the unit) and no
32
+ * canonical duplicates (content-derived unit ids would collide). Returns the
33
+ * failure message, or undefined when the list is dispatchable.
62
34
  */
63
35
  function validateFanOutItems(stepId, items) {
64
36
  const nullIndex = items.findIndex((item) => item === null || item === undefined);
@@ -69,17 +41,9 @@ function validateFanOutItems(stepId, items) {
69
41
  return undefined;
70
42
  }
71
43
  /**
72
- * Resolve one whole-value reference, refusing a value a persisted TRUNCATION
73
- * ENVELOPE stands in for (`clipStepEvidenceForPersistence`, runtime/runs.ts).
74
- *
75
- * The first occurrence of a given canonical value keeps the byte-identical id
76
- * {@link unitIdFor} always produced for it — so a plan with no duplicates (the
77
- * overwhelming common case) is completely unaffected, and no prior journal
78
- * entry is ever invalidated by this change. Only the SECOND and later
79
- * occurrences gain a `#<n>` suffix (`#2`, `#3`, …), computed purely from each
80
- * item's position in `items` — deterministic across a fresh run and a
81
- * resumed one, since both call this from the same place in
82
- * {@link computeStepWorkList} over the same resolved list.
44
+ * Unit ids for a fan-out list: the first occurrence of a canonical value keeps
45
+ * {@link unitIdFor}'s id; later occurrences gain `#2`, `#3`, … by position, so
46
+ * a fresh run and a resume derive the same ids.
83
47
  */
84
48
  function occurrenceSuffixedUnitIds(nodeId, items) {
85
49
  const occurrenceByCanonical = new Map();
@@ -91,35 +55,15 @@ function occurrenceSuffixedUnitIds(nodeId, items) {
91
55
  return occurrence === 1 ? base : `${base}#${occurrence}`;
92
56
  });
93
57
  }
94
- /**
95
- * Resolve one whole-value reference (`inputs[]`, `map.over`, `route.input`).
96
- *
97
- * Every step artifact is now persisted whole (issue C), so this is a thin
98
- * wrapper: source adapters may retain GitHub's whole-value `${{ ... }}`
99
- * spelling, and this work-list seam unwraps only an exact whole-value
100
- * wrapper — it never interpolates prose.
101
- */
58
+ /** Resolve one whole-value reference (`inputs[]`, `map.over`, `route.input`, a binding's `from`). */
102
59
  function resolveStepReference(reference, scope) {
103
- // Source adapters may retain GitHub's whole-value `${{ ... }}` spelling.
104
- // The source IR owns GitHub's whole-value spelling; this work-list seam
105
- // unwraps only an exact whole-value wrapper and never interpolates prose.
106
- const exactWrapper = /^\$\{\{\s*([^{}]+?)\s*\}\}$/.exec(reference);
107
- const canonicalReference = exactWrapper?.[1] ?? reference;
108
- return resolveReferenceString(canonicalReference, scope);
60
+ return resolveReferenceString(reference, scope);
109
61
  }
110
62
  /**
111
- * Pre-attempt resolution of a task-composing step's frozen `inputBindings`
112
- * (spec docs/plans/specs/p2b-input-bindings.md §3.6, B-31..B-34): a
113
- * `{kind:"literal"}` passes through unchanged — its schema was already
114
- * checked at FREEZE (`freezeTaskInputBindings`,
115
- * `src/workflows/freeze/task-bindings.ts`), so it is never re-validated. A
116
- * `{kind:"reference"}` resolves via the SAME {@link resolveStepReference}
117
- * every other whole-value position uses, then validates the resolved value
118
- * against the binding's own frozen `schema` — a mismatch (or a reference that
119
- * fails to resolve at all) fails the WHOLE step here, before
120
- * `reserveUnitAttempt` is ever called by the native executor. Absent
121
- * `bindings` (the overwhelmingly common case — no `with:` on this step's
122
- * target) resolves trivially to `{}` with no scope access at all.
63
+ * Resolve a composing step's frozen `inputBindings` before any attempt: a
64
+ * literal passes through (checked at freeze); a reference resolves like every
65
+ * other whole-value position and is validated against its frozen schema. A
66
+ * failure fails the whole step. No bindings resolve to `{}`.
123
67
  */
124
68
  function resolveTaskInputBindings(bindings, stepId, scope) {
125
69
  if (!bindings || bindings.length === 0)
@@ -155,6 +99,13 @@ function resolveTaskInputBindings(bindings, stepId, scope) {
155
99
  }
156
100
  return { ok: true, values };
157
101
  }
102
+ /**
103
+ * Compute a step's work list purely from the frozen plan and its inputs:
104
+ * resolve the fan-out list, derive content-derived unit ids, assemble each
105
+ * unit's prompt, and hash its input. Same inputs give byte-identical
106
+ * ids/hashes/prompts — what resume relies on to recognize journaled units.
107
+ * Every reference resolves once per step, so failures fail the whole list.
108
+ */
158
109
  export function computeStepWorkList(plan, input) {
159
110
  const root = plan.root;
160
111
  // Route-only steps (YAML `route:`) carry no execution subgraph.
@@ -185,15 +136,7 @@ export function computeStepWorkList(plan, input) {
185
136
  }
186
137
  resolvedInputs.push({ reference, value: resolved.value });
187
138
  }
188
- // P2b Lane A2 — pre-attempt resolution of a task-composing step's frozen
189
- // `inputBindings` (spec §3.6, B-31..B-34): a `{kind:"literal"}` passes
190
- // through unchanged (its schema was already checked at freeze, B-34); a
191
- // `{kind:"reference"}` resolves against this SAME scope, then its resolved
192
- // value is validated against the binding's own frozen `schema` — a
193
- // mismatch fails the WHOLE step here, before `reserveUnitAttempt` is ever
194
- // reached (B-32). This runs for every target kind (command/shell/script);
195
- // Lane B's delivery consumes the result via `StepWorkUnitContext.taskInputs`
196
- // / `taskInputsJson` below.
139
+ // A composing step's frozen `inputBindings`, resolved against this scope for every target kind.
197
140
  const taskInputsResolution = resolveTaskInputBindings(template.frozenTarget.inputBindings, plan.stepId, scope);
198
141
  if (!taskInputsResolution.ok)
199
142
  return taskInputsResolution;
@@ -235,31 +178,14 @@ export function computeStepWorkList(plan, input) {
235
178
  const target = template.frozenTarget;
236
179
  const frozenExec = target.kind === "shell" || target.kind === "script" ? target.exec : undefined;
237
180
  const runner = target.kind === "command" ? target.runner.kind : "exec";
238
- // Taken VERBATIM from the frozen plan — there is no engine-side backstop, by
239
- // design. The whole timeout decision happens once at freeze time
240
- // (`ir/freeze.ts` `effectiveTimeout`: unit `timeout:` → document
241
- // `defaults.timeout` → `engines.<name>.timeoutMs` → the engine-kind default,
242
- // `DEFAULT_LLM_TIMEOUT_MS` / `DEFAULT_AGENT_TIMEOUT_MS`). A frozen `null`
243
- // means genuinely unbounded and is honored as such: it is reached either by an
244
- // author writing `timeout: none` — an explicit, documented opt-out that a
245
- // silent cap here would break — or by `DEFAULT_AGENT_TIMEOUT_MS`, which is
246
- // itself `null` because agent harnesses own their own lifetime. The frozen IR
247
- // collapses both to `timeoutMs: null`, so this layer could not tell them apart
248
- // even if it wanted to; anything that should bound a unit belongs in
249
- // `effectiveTimeout`, not here.
250
- // An exec unit's budget is frozen on its exec spec (there is no engine to
251
- // inherit one from); `ir/freeze.ts` resolved it once from unit `timeout:` →
252
- // `defaults.timeout` → DEFAULT_EXEC_TIMEOUT_MS.
181
+ // Taken verbatim from the frozen plan, resolved once at freeze (an exec
182
+ // unit's on its exec spec). A frozen `null` means genuinely unbounded
183
+ // (`timeout: none`, or an agent harness that owns its own lifetime).
253
184
  const timeoutMs = target.kind === "command"
254
185
  ? (target.runner.timeoutMs ?? null)
255
186
  : target.kind === "child-workflow"
256
- ? // A child-workflow target carries no exec spec of its own (§3.5).
257
- // computeStepWorkList still builds this unit's context
258
- // unconditionally — the child executor (child-workflow.ts,
259
- // reached from native-executor.ts's dispatch seam, P3b §3.2) is
260
- // what actually drives a child-workflow unit, not this line, so
261
- // `null` only needs to be a value this layer can carry, never one
262
- // an engine acts on.
187
+ ? // A child-workflow target carries no exec spec of its own.
188
+ // A child-workflow unit is driven by child-workflow.ts, never by this value.
263
189
  null
264
190
  : target.exec.timeoutMs;
265
191
  // Step-constant exec context: `AKM_PARAMS` / `AKM_INPUTS` depend only on
@@ -271,7 +197,7 @@ export function computeStepWorkList(plan, input) {
271
197
  const execInputsJson = frozenExec && resolvedInputs.length > 0
272
198
  ? (canonicalJson(Object.fromEntries(resolvedInputs.map((entry) => [entry.reference, entry.value]))) ?? "{}")
273
199
  : undefined;
274
- // P2b Lane A2 (§3.6): the resolved effective task-composition inputs,
200
+ // P2b Lane A2: the resolved effective task-composition inputs,
275
201
  // serialized ONCE here (mirrors execParamsJson/execInputsJson above) —
276
202
  // Lane B's delivery (buildUnitPrompt's "## Task inputs" block,
277
203
  // buildExecContextEnv's AKM_TASK_INPUTS) reads both back per unit.
@@ -298,33 +224,17 @@ export function computeStepWorkList(plan, input) {
298
224
  list: { template, reducer, isFanOut, ...(concurrency !== undefined ? { concurrency } : {}), items, units },
299
225
  };
300
226
  }
301
- /**
302
- * Build ONE unit of the step's work list: its journal id, its assembled prompt,
303
- * its exec context env (exec units only), and its canonical input hash.
304
- *
305
- * Extracted from {@link computeStepWorkList} verbatim — same inputs, same
306
- * bytes. It is a separate named pass only because the step-level resolution
307
- * (inputs, fan-out items, runner, timeout) and the per-unit instantiation are
308
- * two different jobs, and keeping them in one function had grown it past the
309
- * repo's 220-line function bar.
310
- */
227
+ /** Build one unit of the step's work list: journal id, prompt, exec context env, and input hash. */
311
228
  function buildStepWorkUnit(ctx, unitId, item, index) {
312
229
  const { plan, input, template, isFanOut, resolvedInputs, target, frozenExec, taskInputs } = ctx;
313
230
  // Gate loops (>= 2) journal under `<unitId>~l<loop>` so loop 1's rows are
314
231
  // never clobbered; the content-derived identity (and the prompt's
315
232
  // {{UNIT_ID}}) stays the base id.
316
233
  const journalBaseId = ctx.gateLoop > 1 ? `${unitId}~l${ctx.gateLoop}` : unitId;
317
- // Context attachment (workflow-format-unification, spec §4): every unit
318
- // receives the run params (already in the preamble), its item + index if
319
- // it is a map unit, and the artifacts named by its step's `inputs:`.
320
- // Instructions reach the unit byte-exact — never interpolated.
321
- //
322
- // An EXEC unit gets NO prompt: there is no model to read one, the exec
323
- // dispatch branch returns before ever touching `request.prompt`, and the input
324
- // hash is built from `template.instructions`, not from the assembled string.
325
- // Its context reaches the child through {@link buildExecContextEnv} instead —
326
- // attached as environment, never spliced into argv, which is the argv-array
327
- // analogue of "data is attached context, not string splices".
234
+ // Every unit receives the run params, its item + index if it is a map unit,
235
+ // and its step's `inputs:` artifacts as attached context; instructions are
236
+ // never interpolated. An exec unit gets no prompt: its context reaches the
237
+ // child as environment ({@link buildExecContextEnv}), never spliced into argv.
328
238
  const prompt = frozenExec
329
239
  ? ""
330
240
  : buildUnitPrompt({
@@ -334,7 +244,7 @@ function buildStepWorkUnit(ctx, unitId, item, index) {
334
244
  params: input.params,
335
245
  ...(isFanOut ? { item, itemIndex: index } : {}),
336
246
  ...(resolvedInputs.length > 0 ? { inputs: resolvedInputs } : {}),
337
- // P2b Lane B (§4.2, B-38/B-39): the composed task's resolved
247
+ // P2b Lane B: the composed task's resolved
338
248
  // `inputBindings`, when non-empty — see StepWorkUnitContext.taskInputs.
339
249
  ...(taskInputs && Object.keys(taskInputs).length > 0 ? { taskInputs } : {}),
340
250
  ...(input.gateFeedback ? { gateFeedback: input.gateFeedback } : {}),
@@ -359,7 +269,7 @@ function buildStepWorkUnit(ctx, unitId, item, index) {
359
269
  ...(template.retry ? { retry: template.retry } : {}),
360
270
  onError: template.onError,
361
271
  ...(template.isolation ? { isolation: template.isolation } : {}),
362
- // P3b §3.3 step 2: the SAME resolved `with:` bindings `taskInputs` already
272
+ // the SAME resolved `with:` bindings `taskInputs` already
363
273
  // carries, exposed under the name `child-workflow.ts`'s drive contract
364
274
  // reads. Absent (never `{}`) when the step binds nothing.
365
275
  ...(taskInputs && Object.keys(taskInputs).length > 0 ? { childParams: taskInputs } : {}),
@@ -368,30 +278,11 @@ function buildStepWorkUnit(ctx, unitId, item, index) {
368
278
  };
369
279
  }
370
280
  /**
371
- * The `AKM_*` context environment an exec unit's child receives.
372
- *
373
- * An exec unit's argv is FROZEN and never interpolated (the shared source IR has
374
- * no substitution language at all), so this is how a fan-out item, the run
375
- * params, and the step's declared `inputs:` artifacts actually reach a command
376
- * — as attached environment, exactly as they reach an engine unit as attached
377
- * prompt context. Values are canonical JSON so a command can parse them.
378
- *
379
- * These are applied on top of the resolved `env:` bindings in the child, so an
380
- * engine-authored context variable can never be shadowed by a binding. Params
381
- * are DECLARED NON-SECRET (`exec/param-secrets.ts` explains why: they are in
382
- * every unit prompt and in the input hash, so they cannot be redacted);
383
- * secrets belong in `env:` bindings, which reach the child by name.
384
- *
385
- * SIZE is not bounded here, on purpose. A workflow artifact has no bound
386
- * comparable to an OS environment entry, so `AKM_INPUTS` (and `AKM_PARAMS` /
387
- * `AKM_ITEM` / `AKM_TASK_INPUTS`) can serialize past what `execve` accepts and
388
- * make PROCESS CREATION
389
- * fail with a bare `E2BIG`. The check belongs at the spawn boundary, where the
390
- * failure can be journaled as a unit outcome with an actionable message naming
391
- * the variable: `checkExecContextSize` in `exec/exec-unit.ts`, against
392
- * `execContextLimits()` for the platform the run is actually on (a Linux run is
393
- * checked against Linux's ceiling, not against the smallest supported one).
394
- * This function stays PURE and total.
281
+ * The `AKM_*` context environment an exec unit's child receives: how a fan-out
282
+ * item, the run params, and the step's `inputs:` artifacts reach a frozen argv
283
+ * (as canonical JSON). Applied over the resolved `env:` bindings so a binding
284
+ * cannot shadow it. Size is checked at the spawn boundary
285
+ * (`checkExecContextSize`, exec-unit.ts), where an E2BIG can be reported by name.
395
286
  */
396
287
  function buildExecContextEnv(args) {
397
288
  const { ctx, unitId, item, index } = args;
@@ -409,36 +300,17 @@ function buildExecContextEnv(args) {
409
300
  }
410
301
  if (ctx.execInputsJson !== undefined)
411
302
  env.AKM_INPUTS = ctx.execInputsJson;
412
- // P2b Lane B (spec §4.1, B-35/B-36/B-39, B-N1): ONE variable carrying the
413
- // composed task's effective `inputBindings` as canonical JSON — never one
414
- // var per input. Absent when the frozen target carries no `inputBindings`
415
- // or every resolved value is empty. Sizing is enforced by the SAME generic
416
- // `checkExecContextSize` loop as every other `AKM_*` entry (exec-unit.ts) —
417
- // no change there, the roster is just longer by one name (B-37).
303
+ // One variable carrying the resolved `inputBindings` as canonical JSON; absent when nothing is bound.
418
304
  if (ctx.taskInputsJson !== undefined)
419
305
  env.AKM_TASK_INPUTS = ctx.taskInputsJson;
420
306
  return env;
421
307
  }
422
308
  /**
423
- * The canonical dispatch-input envelope: every field here is an input that
424
- * changes what the backend is actually asked to do, so a completed unit is
425
- * reused ONLY when all of them match. `env` carries names only, never
426
- * resolved secret values. `retry`/`onError` are deliberately excluded — they
427
- * govern failed-unit re-dispatch, not a completed unit's inputs/output.
428
- * `gateFeedback` is included conditionally (a gate retry is a materially
429
- * different ask). `taskInputs` is likewise included conditionally (R-R15,
430
- * `hashVersion` 7): a reference binding's RESOLVED value reaches the unit's
431
- * prompt / `AKM_TASK_INPUTS` / `childParams`, so a changed upstream value is a
432
- * materially different ask even though the binding's authored shape inside
433
- * `frozenTarget` is unchanged — hashing it makes a resume whose journaled
434
- * upstream output was altered fail loudly as replay divergence instead of
435
- * silently reusing the stale row. The key is absent for a unit whose target
436
- * carries no `inputBindings`, so a binding-free unit's preimage keeps the same
437
- * shape it had (only the version fields moved 6 → 7). This is the ONE place a
438
- * unit's inputHash is computed.
439
- *
440
- * See docs/architecture/decisions/0002-unit-reuse-and-input-hash-scope.md for
441
- * the full field-by-field inclusion/exclusion rationale (reviewer finding #1).
309
+ * The unit's `input_hash`: every input that changes what the backend is asked
310
+ * to do (names, never secret values). Informational — resume reuses a
311
+ * completed row by unit id. `gateFeedback`/`taskInputs` are included only when
312
+ * present so the loop-1, binding-free preimage keeps its `hashVersion` 7 shape.
313
+ * See docs/architecture/decisions/0002-unit-reuse-and-input-hash-scope.md.
442
314
  */
443
315
  function computeUnitInputHash(ctx, item) {
444
316
  return createHash("sha256")
@@ -462,13 +334,10 @@ function computeUnitInputHash(ctx, item) {
462
334
  .digest("hex");
463
335
  }
464
336
  /**
465
- * Assemble the final prompt: engine preamble (run params + item/index +
466
- * declared-input artifacts, all as structured JSON context) + the step's
467
- * BYTE-EXACT prose instructions (+ gate feedback on loop re-executions, +
468
- * schema directive). Instructions are NEVER interpolated (workflow-format-
469
- * unification, spec §2.3) — data reaches the unit as attached context, not
470
- * string splices; only the ENGINE's own preamble placeholders are substituted
471
- * here.
337
+ * Assemble the final prompt: the engine preamble (params, item/index, input
338
+ * artifacts as JSON context) + the step's byte-exact instructions (+ gate
339
+ * feedback on a loop, + schema directive). Only the preamble's own
340
+ * placeholders are substituted.
472
341
  */
473
342
  export function buildUnitPrompt(input) {
474
343
  const { runId, stepId, unitId, params, itemIndex, item, inputs, taskInputs, gateFeedback, schema, instructions } = input;
@@ -489,12 +358,7 @@ export function buildUnitPrompt(input) {
489
358
  const inputsBlock = inputs && inputs.length > 0
490
359
  ? `\n\n## Declared inputs\n${inputs.map((i) => `### ${i.reference}\n${safeJson(i.value)}`).join("\n\n")}`
491
360
  : "";
492
- // P2b Lane B (spec §4.2, B-38/B-39, B-N2): the composed task's resolved
493
- // `inputBindings`, as a structured fenced JSON block — the same "attached
494
- // context, never a splice" mechanism as itemBlock/inputsBlock above.
495
- // `canonicalInputJson` (sorted keys) matches the AKM_TASK_INPUTS env var's
496
- // own serialization, so the effective-inputs value reads identically on
497
- // every delivery surface. Absent (or empty) appends nothing (B-39).
361
+ // The resolved `inputBindings` as a fenced JSON block, serialized exactly like AKM_TASK_INPUTS.
498
362
  const taskInputsBlock = taskInputs && Object.keys(taskInputs).length > 0
499
363
  ? `\n\n## Task inputs\nThe composed task's declared inputs resolved to:\n\`\`\`json\n${canonicalInputJson(taskInputs)}\n\`\`\``
500
364
  : "";
@@ -556,17 +420,9 @@ function stepTemplate(stepPlan) {
556
420
  return root.kind === "map" ? root.template : root;
557
421
  }
558
422
  /**
559
- * The step ids that ANOTHER step of the frozen plan can still read: the
560
- * producers named by an `inputs[]` entry, a `map.over`, or a `route.input`.
561
- * Those three fields are the WHOLE reference surface — instructions are never
562
- * scanned (workflow-format-unification, spec §2.3) — so a step outside this set
563
- * has no in-plan consumer and nothing needs to hold its artifact in memory once
564
- * it is journaled.
565
- *
566
- * Derived from the plan alone: O(plan), independent of run state, and stable
567
- * across the retry and gate loops (a retry re-opens one failed step, and a
568
- * looping step has not advanced, so neither can turn an unreferenced producer
569
- * into a referenced one mid-invocation).
423
+ * The step ids another step can still read (named by `inputs[]`, `map.over`,
424
+ * or `route.input` — the whole reference surface). A step outside this set
425
+ * need not keep its artifact in memory once journaled. Derived from the plan alone.
570
426
  */
571
427
  export function referencedStepIds(plan) {
572
428
  const referenced = new Set();
@@ -586,8 +442,8 @@ export function referencedStepIds(plan) {
586
442
  return referenced;
587
443
  }
588
444
  /**
589
- * Typed artifacts (addendum, R2): validate the promoted step artifact against
590
- * `IrStepPlan.outputSchema`. Returns the step-failure summary (validation
445
+ * Typed artifacts: validate the promoted step artifact against
446
+ * `WorkflowPlanStep.outputSchema`. Returns the step-failure summary (validation
591
447
  * errors included) on mismatch, undefined when valid or when no schema is
592
448
  * declared.
593
449
  */
@@ -601,14 +457,9 @@ export function validateStepArtifact(plan, evidence) {
601
457
  `${errors.join("; ")}.`);
602
458
  }
603
459
  /**
604
- * Warn-only check of each successful unit's own promoted value against its
605
- * template's declared `schema` (`unit.output`) — the one field a harness that
606
- * cannot request structured output drops during lowering (`untranslated-field`,
607
- * field `outputSchema`), after which nothing else ever compares the returned
608
- * text to it. Unlike {@link validateStepArtifact} this never fails the step:
609
- * a harness that DID honor the schema already returned a compliant `result`
610
- * (this re-check then finds nothing), and one that could not is exactly the
611
- * case this exists to surface — the run continues either way.
460
+ * Warn-only check of each successful unit's value against its declared
461
+ * `unit.output` schema — the field a harness without structured output drops
462
+ * during lowering. Never fails the step (unlike {@link validateStepArtifact}).
612
463
  */
613
464
  export function unitSchemaWarning(plan, units) {
614
465
  const schema = stepTemplate(plan)?.schema;
@@ -657,17 +508,9 @@ function unitOutputValue(unit) {
657
508
  return unit.text ?? null;
658
509
  }
659
510
  export function buildEvidence(units, reducer, isFanOut) {
660
- // Per-unit evidence is the DURABLE projection of the unit graph — a fresh run
661
- // and a resumed run of the same plan must agree on it byte-for-byte. It
662
- // therefore carries ONLY fields that can be reproduced from the journal alone:
663
- // - a SUCCESS keeps its promoted contribution (structured `result` or clipped
664
- // `text`) — the reuse path rehydrates exactly these from the unit row;
665
- // - a FAILURE keeps only its `failureReason` (the durable, journaled failure
666
- // vocabulary). The in-memory dispatch diagnostic (`error`) and any residual
667
- // `text` on a failed unit are NOT persisted here: they do not survive a
668
- // restart, so persisting them on the live-dispatch path alone would make
669
- // the durable graph depend on WHEN it was built. The full raw text/reason
670
- // still lives on the unit row for diagnostics; this is the shared graph.
511
+ // Per-unit evidence carries only what the journal can reproduce, so a fresh
512
+ // run and a resume agree byte-for-byte: a success keeps its `result`/clipped
513
+ // `text`, a failure only its `failureReason` (diagnostics stay on the unit row).
671
514
  const collected = units.map((u) => u.ok
672
515
  ? {
673
516
  unitId: u.unitId,
@@ -712,32 +555,17 @@ export function buildEvidence(units, reducer, isFanOut) {
712
555
  else {
713
556
  const winner = ranked[0].value;
714
557
  evidence.vote = { winner, votes: ranked[0].count, total: units.length };
715
- // An empty free-text unit normalizes to absent text, so its vote value is
716
- // `undefined`. Assigning that to `evidence.output` made the key vanish
717
- // under JSON serialization: a LIVE run then saw `output` absent (and fell
718
- // back to the whole evidence envelope), while a RESUMED run rehydrated the
719
- // same step from the journal and produced a different artifact — with the
720
- // raw envelope exposed as `steps.<id>.output`. Normalize to an explicit
721
- // empty string so both paths promote the same value.
558
+ // An empty free-text winner is `undefined`; normalize to "" so a live run
559
+ // and a resume promote the same `output` key.
722
560
  evidence.output = winner === undefined ? "" : winner;
723
561
  }
724
562
  }
725
563
  return evidence;
726
564
  }
727
565
  /**
728
- * The FIRST failed unit's diagnostic, appended to the step summary.
729
- *
730
- * A failure reason alone is not a diagnosis. `non_zero_exit` says a command
731
- * failed; for an exec unit the reason it failed is on stderr, and the summary is
732
- * what `akm workflow run` prints and what the failed step row keeps as its
733
- * notes. Bounded on both axes: ONE unit (a 10 000-wide fan-out must not turn its
734
- * summary into a log) clipped to {@link WORKFLOW_UNIT_DIAGNOSTIC_CLIP} — the same
735
- * bound the journal and `status --units` use.
736
- *
737
- * Reproducible on both surfaces: a live dispatch carries the diagnostic as
738
- * `error`; a unit rehydrated from the journal carries it as `text` (the column
739
- * `journaledUnitResultJson` wrote it to), so the fallback below composes the
740
- * SAME summary from either.
566
+ * The first failed unit's diagnostic (e.g. an exec unit's stderr), clipped to
567
+ * {@link WORKFLOW_UNIT_DIAGNOSTIC_CLIP}, for the step summary. A live outcome
568
+ * carries it as `error`, a rehydrated one as `text`; both give the same summary.
741
569
  */
742
570
  function firstFailureDiagnostic(failed) {
743
571
  const first = failed.find((u) => (u.error ?? u.text)?.trim());
@@ -747,13 +575,9 @@ function firstFailureDiagnostic(failed) {
747
575
  return ` First failure diagnostic (${first.unitId}): ${clip(diagnostic, WORKFLOW_UNIT_DIAGNOSTIC_CLIP)}`;
748
576
  }
749
577
  /**
750
- * Reduce a step's terminal unit outcomes into the promoted artifact + step
751
- * verdict — the shared semantics between native dispatch and the report path.
752
- * Applies the `on_error` policy (`fail` vs `continue`), the reducer (via
753
- * {@link buildEvidence}), the vote-tie failure, and the typed-artifact schema
754
- * validation (fail-fast, errors in the summary, `artifactSchemaFailure` marker).
755
- * Callers own dispatch-specific concerns (replay-divergence, budget) BEFORE
756
- * calling this; those never occur on the report path (units are journaled).
578
+ * Reduce a step's terminal unit outcomes into the promoted artifact and step
579
+ * verdict: the `on_error` policy, the reducer, the vote-tie failure, and the
580
+ * typed-artifact schema check (`artifactSchemaFailure` marks a retryable one).
757
581
  */
758
582
  export function reduceStepOutcomes(plan, reducer, isFanOut, onError, units) {
759
583
  const failed = units.filter((u) => !u.ok);
@@ -784,12 +608,8 @@ export function reduceStepOutcomes(plan, reducer, isFanOut, onError, units) {
784
608
  if (schemaWarning !== undefined)
785
609
  summary += ` ${schemaWarning}`;
786
610
  }
787
- // P3b §3.4: a composed child workflow that blocked is carried on the
788
- // failed unit's LIVE-ONLY `childRun` field (child-workflow.ts's
789
- // driveChildWorkflowUnit). Surfaced here, unconditionally on the unit
790
- // list, so `finalizeExecutedStep` can check it before deciding whether
791
- // this step's failure is retryable — an `onError: "continue"` step that
792
- // tolerates the failure (`ok` stays true) never reaches that check at all.
611
+ // A blocked child workflow (the failed unit's live-only `childRun`) is
612
+ // surfaced so `finalizeExecutedStep` blocks the step instead of retrying.
793
613
  const blockedChildUnit = failed.find((u) => u.failureReason === "child_workflow_blocked" && u.childRun !== undefined);
794
614
  const childBlocked = blockedChildUnit?.childRun
795
615
  ? {
@@ -808,18 +628,9 @@ export function reduceStepOutcomes(plan, reducer, isFanOut, onError, units) {
808
628
  };
809
629
  }
810
630
  /**
811
- * The reduced outcome of a step whose fan-out list resolved to EMPTY (`over: []`
812
- * or a producer that yielded `[]`): no units are dispatched, so the promoted
813
- * artifact is the degenerate empty value — the empty array for a `collect`
814
- * reducer, `null` for `vote` (references into a missing winner fail loudly at
815
- * resolution rather than silently reading the envelope). Even the degenerate
816
- * artifact must honor the step's declared `outputSchema` before it can complete.
817
- *
818
- * Used by native dispatch (`executeStepPlan`'s `items.length === 0` branch): a
819
- * zero-unit step can never be advanced by a unit completion, so it is promoted
820
- * here instead. Deliberately does NOT run the reducer/vote-tie logic: an empty
821
- * step has no successful results to count, and a vote-tie "failure" would
822
- * diverge from the engine's long-standing empty-list semantics.
631
+ * The outcome of a step whose fan-out list is empty: no units dispatch and the
632
+ * artifact is `[]` (collect) or `null` (vote), still checked against the step's
633
+ * `outputSchema`. The reducer/vote-tie logic does not run.
823
634
  */
824
635
  export function reduceEmptyStep(plan, reducer) {
825
636
  const evidence = { units: [], itemCount: 0, output: reducer === "collect" ? [] : null };
@@ -833,16 +644,9 @@ export function reduceEmptyStep(plan, reducer) {
833
644
  };
834
645
  }
835
646
  /**
836
- * Rehydrate a journaled unit row into a {@link UnitOutcome}. The executor's
837
- * durable-row reuse (`native-executor.ts`) calls it for completed rows; the
838
- * failed-row branch keeps the mapping TOTAL, so any reduction driven off the
839
- * journal yields the same outcome the live dispatch produced. A completed row's
840
- * text unit journals its output as a JSON string; a schema unit journals the
841
- * validated structure. A failed row carries its `failure_reason` plus whatever
842
- * `journaledUnitResultJson` (native-executor.ts) wrote to `result_json` —
843
- * surfaced as `text`, its historical meaning. {@link firstFailureDiagnostic} is
844
- * the one consumer that wants it as a diagnostic and falls back to `text`, so
845
- * the step summary stays the same on both surfaces.
647
+ * Rehydrate a journaled unit row into the {@link UnitOutcome} its live dispatch
648
+ * produced: a completed row's JSON text or structured result, or a failed row's
649
+ * `failure_reason` with its journaled diagnostic as `text`.
846
650
  */
847
651
  export function unitOutcomeFromRow(unitId, row, hasSchema) {
848
652
  let parsed;
@@ -879,37 +683,19 @@ export function unitOutcomeFromRow(unitId, row, hasSchema) {
879
683
  export { canonicalJson };
880
684
  // ── Gate-feedback recovery (PURE) ────────────────────────────────────────────
881
685
  //
882
- // A gate rejection is journaled as `<stepId>.gate:l<loop>` with result_json
883
- // `{ complete: false, missing, feedback }` (see journalGateEvaluationFinish).
884
- // The feedback stored there is BYTE-IDENTICAL to what the engine threads into
885
- // the next loop's prompts — both are the same `rejection.feedback`/`.missing`.
886
- // A resume recovers it from the journal so its loop-N work-list (and therefore
887
- // every unit id and input hash in it) matches the one the original run built.
888
- // `native-executor.test.ts` asserts the round-trip identity.
889
- // GATE_EVALUATION_PHASE moved to ../runtime/unit-phases.ts (leaf) so
890
- // unit-checkin can key on it without closing the exec ↔ runtime cycle.
686
+ // A gate rejection journals `{ complete: false, missing, feedback }` under
687
+ // `<stepId>.gate:l<loop>`, byte-identical to what the next loop's prompts
688
+ // carry, so a resume rebuilds the same loop-N work list.
689
+ /** `phase` marker on gate-evaluation unit rows (dispatch rows journal `phase: null`). */
690
+ const GATE_EVALUATION_PHASE = "gate";
891
691
  /** The unit id of a step's gate-evaluation row for a given 1-based loop. */
892
692
  export function gateUnitId(stepId, loop) {
893
693
  return `${stepId}.gate:l${loop}`;
894
694
  }
895
695
  /**
896
- * How many times a step's subgraph may run under its completion gate — the
897
- * bound the engine loop walks and the one `loopsRemaining` is derived from.
898
- *
899
- * A gate loop only earns its re-dispatch when the subgraph can ANSWER the
900
- * judge: an engine unit reads the rejection feedback in its prompt and produces
901
- * different work. An `exec` unit cannot. Its argv is frozen and never
902
- * interpolated, {@link buildExecContextEnv} exposes no feedback variable, and
903
- * the default dispatcher drops feedback for exec — so a second loop re-runs the
904
- * BYTE-IDENTICAL command for a verdict that cannot change, which for a deploy /
905
- * publish / migrate command means performing the side effect twice. The same
906
- * reasoning already pins exec structured output to a single attempt and makes
907
- * `exec_capture_incomplete` non-retryable (`native-executor.ts`).
908
- *
909
- * So an exec step's gate still EVALUATES — the verdict can still fail the step
910
- * — but it never loops: a rejection lands on the gate-exhausted terminal
911
- * instead of re-dispatching. An authored `gate.max_loops` on an engine step is
912
- * untouched.
696
+ * How many times a step's subgraph may run under its gate. An exec step never
697
+ * loops: its frozen argv cannot read the judge's feedback, so a second loop
698
+ * would only repeat the command's side effects. Its gate still evaluates.
913
699
  */
914
700
  export function effectiveGateMaxLoops(stepPlan) {
915
701
  const declared = Math.max(1, stepPlan.gate.maxLoops ?? 1);
@@ -917,17 +703,9 @@ export function effectiveGateMaxLoops(stepPlan) {
917
703
  return target && target.kind !== "command" ? 1 : declared;
918
704
  }
919
705
  /**
920
- * The gate loop the engine is about to (re-)run for an ACTIVE step, derived
921
- * purely from the journal: one past the highest journaled loop that REJECTED
922
- * (`complete: false`). No rejected gate rows ⇒ loop 1 (the first execution).
923
- * A passed gate would have advanced the spine, so an active step never has a
924
- * `complete: true` row as its latest gate evaluation.
925
- *
926
- * Reviewer #17: a gate row that EXISTS but cannot be parsed (or carries an
927
- * invalid verdict shape) is CORRUPTION — {@link parseGateVerdict} throws loudly
928
- * rather than letting `gateRowRejected` swallow the parse error, which would
929
- * silently drop the loop back to 1 and re-dispatch work whose gate outcome is
930
- * unknown.
706
+ * The gate loop the engine is about to run for an active step: one past the
707
+ * highest journaled rejected loop (loop 1 when none). An unparseable gate row
708
+ * throws ({@link parseGateVerdict}) rather than silently restarting at loop 1.
931
709
  */
932
710
  export function activeGateLoop(rows, stepId) {
933
711
  let maxRejectedLoop = 0;
@@ -944,14 +722,8 @@ export function activeGateLoop(rows, stepId) {
944
722
  return maxRejectedLoop + 1;
945
723
  }
946
724
  /**
947
- * Recover the gate feedback the engine threads into `loop`'s unit prompts: the
948
- * `{ feedback, missing }` journaled by the previous loop's rejection
949
- * (`<stepId>.gate:l<loop-1>`). Loop 1 (or a missing/passed/errored previous row)
950
- * has no feedback. Pure — the journal rows are passed in.
951
- *
952
- * Reviewer #17: a PRESENT previous gate row that cannot be parsed fails LOUDLY
953
- * (via {@link parseGateVerdict}) instead of returning undefined — a corrupt row
954
- * must not make an in-loop step look like loop 1 with no recovered feedback.
725
+ * The `{ feedback, missing }` the previous loop's rejection journaled, which
726
+ * `loop`'s prompts carry; none for loop 1. An unparseable previous row throws.
955
727
  */
956
728
  export function recoverGateFeedback(rows, stepId, loop) {
957
729
  if (loop <= 1)
@@ -972,16 +744,9 @@ function gateLoopOf(unitId, stepId) {
972
744
  return Number.isInteger(n) && n >= 1 ? n : undefined;
973
745
  }
974
746
  /**
975
- * Classify a gate-evaluation row's journaled verdict, failing LOUDLY on a
976
- * corrupt one (reviewer #17). A NULL `result_json` is the LEGITIMATE
977
- * completion-error / in-flight shape (`journalGateEvaluationFinish` writes null
978
- * if completion itself throws after judge invocation, and a `running` row has no
979
- * verdict yet) and classifies as `empty`. But a PRESENT `result_json` that does
980
- * not parse as JSON, or parses to
981
- * anything other than an object with a boolean `complete` field, is corruption —
982
- * a truncated or hand-edited row — and MUST NOT be silently treated as absent
983
- * (which would reset an active step's gate loop to 1 and re-dispatch work whose
984
- * completion outcome is unknown). We refuse to guess.
747
+ * Classify a gate row's journaled verdict. A NULL `result_json` (in flight, or
748
+ * a completion error) is `empty`; a present value that is not `{ complete:
749
+ * boolean }` throws rather than resetting the gate loop to 1.
985
750
  */
986
751
  function parseGateVerdict(row) {
987
752
  if (row.result_json === null)
@@ -1016,8 +781,6 @@ function gateCorruptionMessage(row, why) {
1016
781
  /** Insert the gate-evaluation unit row (running) just before the judge runs. */
1017
782
  export async function journalGateEvaluationStart(gate) {
1018
783
  const unitId = gateUnitId(gate.stepId, gate.loop);
1019
- const now = new Date().toISOString();
1020
- const claimHolder = gate.claimHolder ?? `direct:${randomUUID()}`;
1021
784
  const reserved = await enqueueUnitWrite(() => withWorkflowRunsRepo((repo) => repo.reserveUnitAttempt({
1022
785
  runId: gate.runId,
1023
786
  unitId,
@@ -1028,25 +791,15 @@ export async function journalGateEvaluationStart(gate) {
1028
791
  engine: gate.engine,
1029
792
  model: gate.model,
1030
793
  inputHash: gate.inputHash,
1031
- claimHolder,
1032
- claimExpiresAt: new Date(Date.parse(now) + 90_000).toISOString(),
1033
- now,
1034
- leaseMode: gate.claimHolder === undefined ? "direct" : "engine",
794
+ now: new Date().toISOString(),
1035
795
  })));
1036
- if (reserved.kind === "busy") {
1037
- throw new UsageError(`Gate ${unitId} has a live durable attempt held by another engine.`);
1038
- }
1039
- return { ...gate, claimHolder, durableAttempt: reserved.attempt };
796
+ return { ...gate, durableAttempt: reserved.attempt };
1040
797
  }
1041
798
  /**
1042
- * Finish the gate-evaluation unit row with the verdict as observed from the
1043
- * completion outcome: a rejection journals `{ complete: false, missing,
1044
- * feedback }`; a pass journals `{ complete: true, missing: [] }`. An ERRORED
1045
- * evaluation (thrown judge, malformed verdict, completion failure after the
1046
- * judge ran) journals a failed row with NO verdict (`result_json` NULL) —
1047
- * `errored` takes precedence over any synthesized fail-closed rejection, so
1048
- * `activeGateLoop`/`recoverGateFeedback` never mistake a judge outage for an
1049
- * honest rejection and burn a gate loop on resume.
799
+ * Finish the gate-evaluation row: a rejection journals `{ complete: false,
800
+ * missing, feedback }`, a pass `{ complete: true, missing: [] }`. An errored
801
+ * evaluation journals a failed row with no verdict, so a judge outage never
802
+ * burns a gate loop on resume.
1050
803
  */
1051
804
  export async function journalGateEvaluationFinish(gate, errored, rejection) {
1052
805
  const unitId = gateUnitId(gate.stepId, gate.loop);
@@ -1065,7 +818,6 @@ export async function journalGateEvaluationFinish(gate, errored, rejection) {
1065
818
  unitId,
1066
819
  attempt: durableAttempt.attempt,
1067
820
  dispatchId: durableAttempt.dispatch_id,
1068
- claimHolder: durableAttempt.claim_holder,
1069
821
  status,
1070
822
  resultJson: verdict ? JSON.stringify(verdict) : null,
1071
823
  tokens: gate.tokens ?? null,
@@ -1074,7 +826,7 @@ export async function journalGateEvaluationFinish(gate, errored, rejection) {
1074
826
  });
1075
827
  }));
1076
828
  if (!finished) {
1077
- throw new UsageError(`Gate ${unitId} no longer owns its durable attempt; refusing a late terminal write.`);
829
+ throw new UsageError(`Gate ${unitId} was already finished; refusing a duplicate terminal write.`);
1078
830
  }
1079
831
  }
1080
832
  /**
@@ -1162,15 +914,7 @@ function journaledRouteSelection(evidence) {
1162
914
  function routeTargets(route) {
1163
915
  return new Set([...Object.values(route.when), ...(route.defaultStepId ? [route.defaultStepId] : [])]);
1164
916
  }
1165
- /**
1166
- * Reviewer #7: a journaled route decision must name a target the route actually
1167
- * DECLARES (`when` branch or `default`). Corrupted or hand-edited evidence can
1168
- * otherwise mark a non-existent step as `selected` — which unselects and skips
1169
- * every REAL branch target, silently steering the run down a phantom branch.
1170
- * `evaluateRoute` can only ever produce a declared target, so a stored value
1171
- * outside that set is provably tampered evidence: fail loudly rather than seed a
1172
- * bogus skip set.
1173
- */
917
+ /** A journaled route decision must name a target the route declares; anything else fails loudly. */
1174
918
  function assertRouteTargetDeclared(route, stepId, selected, runId) {
1175
919
  const targets = routeTargets(route);
1176
920
  if (!targets.has(selected)) {
@@ -1205,7 +949,7 @@ export function seedJournaledRouteDecisions(plan, state, routeSelected, routeUns
1205
949
  continue;
1206
950
  let selected = journaledRouteSelection(stepState.evidence);
1207
951
  if (selected !== undefined) {
1208
- // Reviewer #7: a stored decision must name a declared target — a bogus one
952
+ // a stored decision must name a declared target — a bogus one
1209
953
  // (tampered/hand-edited evidence) fails loudly rather than seeding a skip
1210
954
  // set that buries the real branches.
1211
955
  assertRouteTargetDeclared(stepPlan.route, stepPlan.stepId, selected, state.run.id);
@@ -1255,7 +999,6 @@ export async function blockStepForJudgeFailure(input) {
1255
999
  status: "blocked",
1256
1000
  notes,
1257
1001
  ...(input.evidence !== undefined ? { evidence: input.evidence } : {}),
1258
- ...(input.leaseHolder !== undefined ? { leaseHolder: input.leaseHolder } : {}),
1259
1002
  });
1260
1003
  return notes;
1261
1004
  }
@@ -1266,19 +1009,10 @@ async function blockFinalizedStep(input, cause) {
1266
1009
  stepId: input.stepId,
1267
1010
  cause,
1268
1011
  evidence: input.result.evidence,
1269
- ...(input.leaseHolder !== undefined ? { leaseHolder: input.leaseHolder } : {}),
1270
1012
  });
1271
1013
  return { kind: "judge-failed", summary };
1272
1014
  }
1273
- /**
1274
- * §3.4's exact `blockStepForChildWorkflow` notes — the ONE place the
1275
- * blocked-child resume sequence is worded, mirroring {@link judgeFailureNotes}.
1276
- * Two properties this wording pins (each its own test): the CHILD is resumed
1277
- * FIRST, and the PARENT's own re-drive is what advances it (the child drive
1278
- * never calls `resumeWorkflowRun` itself, row A-22); the notes name the child
1279
- * run id and both commands verbatim, so the text renderer needs no change
1280
- * (B-N15 — Lane A touches no output module).
1281
- */
1015
+ /** The blocked-child resume notes: resume the child first, then re-drive the parent. */
1282
1016
  function childWorkflowBlockedNotes(runId, stepId, childRunId, childRef, childStepId) {
1283
1017
  return (`Step "${stepId}" composes child workflow run ${childRunId} (${childRef}), ` +
1284
1018
  `which is blocked at its own step "${childStepId ?? "(unknown)"}". Nothing in this run advances ` +
@@ -1287,14 +1021,7 @@ function childWorkflowBlockedNotes(runId, stepId, childRunId, childRef, childSte
1287
1021
  `\`akm workflow resume ${runId}\` and \`akm workflow run ${runId}\` to ` +
1288
1022
  `continue: re-driving the parent drives the resumed child.`);
1289
1023
  }
1290
- /**
1291
- * Complete a step `blocked` because the child workflow it composes is
1292
- * blocked, and return the notes written (P3b §3.4). Sits beside
1293
- * {@link blockStepForJudgeFailure} — the SAME shape of "infrastructure-like"
1294
- * block: the step is completed `blocked`, and `akm workflow resume` is what
1295
- * clears it (of the CHILD first, then the parent) rather than an automatic
1296
- * in-step re-dispatch.
1297
- */
1024
+ /** Complete a step `blocked` because its child workflow is blocked; `akm workflow resume` clears it. */
1298
1025
  export async function blockStepForChildWorkflow(input) {
1299
1026
  const notes = childWorkflowBlockedNotes(input.runId, input.stepId, input.childRunId, input.childRef, input.childStepId);
1300
1027
  await completeWorkflowStep({
@@ -1303,7 +1030,6 @@ export async function blockStepForChildWorkflow(input) {
1303
1030
  status: "blocked",
1304
1031
  notes,
1305
1032
  ...(input.evidence !== undefined ? { evidence: input.evidence } : {}),
1306
- ...(input.leaseHolder !== undefined ? { leaseHolder: input.leaseHolder } : {}),
1307
1033
  });
1308
1034
  return notes;
1309
1035
  }
@@ -1316,38 +1042,25 @@ async function blockFinalizedStepForChildWorkflow(input, childBlocked) {
1316
1042
  childRef: childBlocked.childRef,
1317
1043
  childStepId: childBlocked.childStepId,
1318
1044
  evidence: input.result.evidence,
1319
- ...(input.leaseHolder !== undefined ? { leaseHolder: input.leaseHolder } : {}),
1320
1045
  });
1321
1046
  return { kind: "child-blocked", summary };
1322
1047
  }
1323
1048
  /**
1324
- * Perform ONE completion attempt for an executed step:
1325
- *
1326
- * - a hard unit failure completes the step `failed` (a retryable typed-artifact
1327
- * mismatch with loops remaining returns `retry` WITHOUT journaling a gate row
1328
- * — no judge ran, exactly like the engine);
1329
- * - a route decision is evaluated against params + prior/fresh step outputs; an
1330
- * unroutable value fails the step; a valid decision is journaled on the
1331
- * step evidence and applied to the skip bookkeeping;
1332
- * - the completion gate judges a summary BUILT FROM the promoted artifact (when
1333
- * the step declares criteria), journaled as a `<stepId>.gate:l<loop>` unit
1334
- * row; a rejection with loops remaining returns `retry` (feedback threaded
1335
- * into the next loop), a rejection with none returns `gate-exhausted`, a pass
1336
- * returns `advanced`;
1337
- * - a judge INFRASTRUCTURE failure (missing judge, thrown judge call, or a
1338
- * malformed verdict) is NOT a verdict: it consumes no gate loop and blocks
1339
- * the step for `akm workflow resume` (`judge-failed`) instead of feeding
1340
- * the bounded loop's re-dispatch.
1341
- *
1342
- * Every DB advance goes through {@link completeWorkflowStep} — the gate spine is
1343
- * never bypassed. Behavior is byte-identical to the engine's former inline loop
1344
- * body (its tests prove it).
1049
+ * Perform one completion attempt for an executed step:
1050
+ * - a hard unit failure fails the step (a retryable artifact-schema mismatch
1051
+ * with loops left returns `retry` without a gate row);
1052
+ * - a route decision is evaluated, journaled on the evidence, and applied to
1053
+ * the skip bookkeeping; an unroutable value fails the step;
1054
+ * - the gate judges a summary built from the promoted artifact: a rejection
1055
+ * returns `retry` (loops left) or `gate-exhausted`, a pass `advanced`;
1056
+ * - a judge infrastructure failure is not a verdict: it blocks the step for
1057
+ * `akm workflow resume` (`judge-failed`) without consuming a loop.
1058
+ * Every advance goes through {@link completeWorkflowStep}.
1345
1059
  */
1346
1060
  export async function finalizeExecutedStep(input) {
1347
1061
  const { runId, workflowRef, stepId, stepPlan, completionCriteria, gateLoop, loopsRemaining, result } = input;
1348
- const lease = input.leaseHolder !== undefined ? { leaseHolder: input.leaseHolder } : {};
1349
1062
  if (!result.ok) {
1350
- // P3b §3.4: a composed child workflow that blocked is never fed into the
1063
+ // a composed child workflow that blocked is never fed into the
1351
1064
  // bounded gate loop — a gate is a gate for a child workflow too. Checked
1352
1065
  // FIRST, before the artifactSchemaFailure retry branch below.
1353
1066
  if (result.childBlocked) {
@@ -1365,7 +1078,6 @@ export async function finalizeExecutedStep(input) {
1365
1078
  status: "failed",
1366
1079
  notes: result.summary,
1367
1080
  evidence: result.evidence,
1368
- ...lease,
1369
1081
  });
1370
1082
  return { kind: "failed", summary: result.summary };
1371
1083
  }
@@ -1391,7 +1103,7 @@ export async function finalizeExecutedStep(input) {
1391
1103
  const decision = evaluateRoute(stepPlan.route, scope);
1392
1104
  if (!decision.ok) {
1393
1105
  const notes = `Step "${stepId}" route failed: ${decision.error}`;
1394
- await completeWorkflowStep({ runId, stepId, status: "failed", notes, evidence: result.evidence, ...lease });
1106
+ await completeWorkflowStep({ runId, stepId, status: "failed", notes, evidence: result.evidence });
1395
1107
  return { kind: "failed", summary: notes, routeFailure: true };
1396
1108
  }
1397
1109
  applyRouteDecision(stepPlan.route, stepId, decision.selected, input.routeSelected, input.routeUnselected);
@@ -1446,7 +1158,6 @@ export async function finalizeExecutedStep(input) {
1446
1158
  prompt,
1447
1159
  }))
1448
1160
  .digest("hex"),
1449
- ...(input.leaseHolder !== undefined ? { claimHolder: input.leaseHolder } : {}),
1450
1161
  };
1451
1162
  gateUnit = await journalGateEvaluationStart(gateUnit);
1452
1163
  }
@@ -1486,23 +1197,10 @@ export async function finalizeExecutedStep(input) {
1486
1197
  return raw;
1487
1198
  }
1488
1199
  : null;
1489
- // Reviewer #6: once the judge is invoked, its gate row is journaled `running`
1490
- // (journalGateEvaluationStart) and MUST be finished on every exit. The
1491
- // already-fixed window is the judge itself throwing (caught inside
1492
- // validateStepSummary — `judgeFailure` records it). The remaining
1493
- // window is `completeWorkflowStep` throwing AFTER the judge ran — a stolen
1494
- // lease, a concurrent state change, a DB error — which would otherwise skip the
1495
- // finish and strand the gate row in `running`. Finish it as an errored row (the
1496
- // observed outcome: the completion did not succeed), then re-propagate.
1497
- //
1498
- // The signal handed down is the DISPATCH signal (the judge call runs under
1499
- // it), not just the caller's: the interruption guard inside the completion
1500
- // path rethrows an abort instead of classifying it as a judge outage, and a
1501
- // lost lease aborting mid-judge is an interruption — recording it as a
1502
- // verifier failure would blame infrastructure and durably block a step whose
1503
- // gate simply never finished evaluating.
1200
+ // Once the judge runs, its `running` gate row must be finished on every
1201
+ // exit: if `completeWorkflowStep` throws afterwards, finish it as errored,
1202
+ // then re-propagate.
1504
1203
  let completion;
1505
- const completionSignal = input.dispatchSignal ?? input.signal;
1506
1204
  try {
1507
1205
  completion = await completeWorkflowStep({
1508
1206
  runId,
@@ -1511,8 +1209,7 @@ export async function finalizeExecutedStep(input) {
1511
1209
  summary,
1512
1210
  evidence: result.evidence,
1513
1211
  summaryJudge,
1514
- ...(completionSignal ? { signal: completionSignal } : {}),
1515
- ...lease,
1212
+ ...(input.signal ? { signal: input.signal } : {}),
1516
1213
  });
1517
1214
  }
1518
1215
  catch (err) {