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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (598) hide show
  1. package/CHANGELOG.md +1283 -22
  2. package/README.md +62 -37
  3. package/SECURITY.md +46 -31
  4. package/dist/akm +162 -38
  5. package/dist/akm-migrate +44 -0
  6. package/dist/assets/backends/schtasks-template.xml +2 -1
  7. package/dist/assets/hints/cli-hints-full.md +268 -118
  8. package/dist/assets/hints/cli-hints-short.md +87 -24
  9. package/dist/assets/{profiles → improve-strategies}/catchup.json +3 -1
  10. package/dist/assets/{profiles → improve-strategies}/consolidate.json +3 -1
  11. package/dist/assets/{profiles → improve-strategies}/default.json +6 -7
  12. package/dist/assets/improve-strategies/frequent.json +15 -0
  13. package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +4 -2
  14. package/dist/assets/{profiles → improve-strategies}/memory-focus.json +4 -1
  15. package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +5 -5
  16. package/dist/assets/{profiles → improve-strategies}/quick.json +4 -2
  17. package/dist/assets/improve-strategies/reflect-distill.json +30 -0
  18. package/dist/assets/{profiles → improve-strategies}/thorough.json +1 -1
  19. package/dist/assets/prompts/consolidate-system.md +5 -5
  20. package/dist/assets/prompts/extract-session.md +2 -6
  21. package/dist/assets/prompts/memory-infer-user.md +2 -3
  22. package/dist/assets/prompts/reflect-llm-framed-contract.md +11 -0
  23. package/dist/assets/prompts/reflect-llm-schema-contract.md +3 -0
  24. package/dist/assets/prompts/reflect-output-repair.md +3 -0
  25. package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
  26. package/dist/assets/stash-skeleton/README.md +38 -10
  27. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +8 -0
  28. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +8 -0
  29. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +14 -1
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +13 -1
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +9 -1
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +11 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +9 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +9 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +8 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +100 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/domains.md +64 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/organization.md +136 -0
  39. package/dist/assets/tasks/core/extract.yml +3 -2
  40. package/dist/assets/tasks/core/improve.yml +2 -1
  41. package/dist/assets/tasks/core/index-refresh.yml +1 -0
  42. package/dist/assets/tasks/core/sync.yml +1 -0
  43. package/dist/assets/tasks/core/version-check.yml +2 -1
  44. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
  45. package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
  46. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
  47. package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
  48. package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
  49. package/dist/assets/templates/html/health.html +5 -4
  50. package/dist/assets/workflows/workflow-template.md +31 -15
  51. package/dist/cli/invocation.js +279 -0
  52. package/dist/cli/parse-args.js +5 -90
  53. package/dist/cli/retired-commands.js +78 -0
  54. package/dist/cli/shared.js +158 -48
  55. package/dist/cli-node.mjs +2 -1
  56. package/dist/cli.js +747 -293
  57. package/dist/commands/agent/agent-dispatch.js +19 -18
  58. package/dist/commands/agent/agent-support.js +0 -24
  59. package/dist/commands/agent/contribute-cli.js +43 -97
  60. package/dist/commands/completions.js +80 -23
  61. package/dist/commands/config-cli.js +44 -281
  62. package/dist/commands/env/env-binding.js +99 -0
  63. package/dist/commands/env/env-cli.js +84 -224
  64. package/dist/commands/env/env.js +12 -163
  65. package/dist/commands/env/marker-path.js +6 -0
  66. package/dist/commands/env/secret-cli.js +45 -61
  67. package/dist/commands/env/secret.js +32 -62
  68. package/dist/commands/feedback-cli.js +179 -85
  69. package/dist/commands/health/accept-rate.js +58 -0
  70. package/dist/commands/health/advisories.js +7 -8
  71. package/dist/commands/health/checks.js +279 -94
  72. package/dist/commands/health/html-report.js +197 -578
  73. package/dist/commands/health/improve-metrics.js +277 -246
  74. package/dist/commands/health/llm-usage.js +19 -19
  75. package/dist/commands/health/md-report.js +16 -7
  76. package/dist/commands/health/metrics.js +67 -32
  77. package/dist/commands/health/renderers.js +47 -0
  78. package/dist/commands/health/report-view-model.js +508 -0
  79. package/dist/commands/health/stash-exposure.js +1 -1
  80. package/dist/commands/health/surfaces.js +16 -56
  81. package/dist/commands/health/task-runs.js +3 -67
  82. package/dist/{migrate-storage-node.mjs → commands/health/types-checks.js} +1 -5
  83. package/dist/commands/health/types-improve.js +29 -0
  84. package/dist/{output/text/save.js → commands/health/types-metrics.js} +1 -2
  85. package/dist/commands/health/types-result.js +7 -0
  86. package/dist/commands/health/types-runs.js +4 -0
  87. package/dist/commands/health/types-session-log.js +4 -0
  88. package/dist/commands/health/types-windows.js +4 -0
  89. package/dist/commands/health/types.js +26 -21
  90. package/dist/commands/health/windows.js +2 -3
  91. package/dist/commands/health.js +296 -167
  92. package/dist/commands/improve/anti-collapse.js +5 -5
  93. package/dist/commands/improve/autonomy-gate.js +68 -0
  94. package/dist/commands/improve/collapse-detector.js +65 -52
  95. package/dist/commands/improve/consolidate/chunking.js +9 -7
  96. package/dist/commands/improve/consolidate/eligibility.js +1 -23
  97. package/dist/commands/improve/consolidate/merge.js +4 -0
  98. package/dist/commands/improve/consolidate.js +454 -1354
  99. package/dist/commands/improve/content-hash.js +39 -0
  100. package/dist/commands/improve/distill/content-repair.js +4 -10
  101. package/dist/commands/improve/distill/promote-memory.js +89 -64
  102. package/dist/commands/improve/distill/quality-gate.js +118 -42
  103. package/dist/commands/improve/distill-guards.js +1 -1
  104. package/dist/commands/improve/distill-promotion-policy.js +33 -888
  105. package/dist/commands/improve/distill.js +607 -363
  106. package/dist/commands/improve/eligibility.js +165 -79
  107. package/dist/commands/improve/extract-cli.js +35 -126
  108. package/dist/commands/improve/extract-prompt.js +6 -35
  109. package/dist/commands/improve/extract.js +640 -391
  110. package/dist/commands/improve/feedback-valence.js +2 -12
  111. package/dist/commands/improve/improve-cli.js +134 -135
  112. package/dist/commands/improve/improve-result-file.js +30 -50
  113. package/dist/commands/improve/improve-run-types.js +4 -0
  114. package/dist/commands/improve/improve-strategies.js +135 -0
  115. package/dist/commands/improve/improve.js +904 -701
  116. package/dist/commands/improve/locks.js +64 -111
  117. package/dist/commands/improve/loop-stages.js +1110 -923
  118. package/dist/commands/improve/memory/derived-ref.js +124 -0
  119. package/dist/commands/improve/memory/memory-belief.js +79 -7
  120. package/dist/commands/improve/memory/memory-contradiction-detect.js +49 -52
  121. package/dist/commands/improve/memory/memory-improve.js +25 -37
  122. package/dist/commands/improve/outcome-loop.js +25 -88
  123. package/dist/commands/improve/preparation.js +1034 -813
  124. package/dist/commands/improve/proactive-maintenance.js +34 -9
  125. package/dist/commands/improve/proposal-envelope.js +31 -0
  126. package/dist/commands/improve/reflect.js +983 -794
  127. package/dist/commands/improve/run-context.js +119 -0
  128. package/dist/commands/improve/salience.js +24 -127
  129. package/dist/commands/improve/session-asset.js +7 -3
  130. package/dist/commands/improve/shared.js +14 -34
  131. package/dist/commands/improve/source-identity.js +28 -0
  132. package/dist/commands/improve/triage.js +20 -17
  133. package/dist/commands/lint/base-linter.js +340 -313
  134. package/dist/commands/lint/env-key-rules.js +31 -47
  135. package/dist/commands/lint/index.js +185 -30
  136. package/dist/commands/{events.js → log.js} +28 -38
  137. package/dist/commands/migrate-cli.js +54 -0
  138. package/dist/commands/migration-tool.js +55 -0
  139. package/dist/commands/observability-cli.js +70 -208
  140. package/dist/commands/proposal/diff-format.js +50 -0
  141. package/dist/commands/proposal/drain-policies.js +0 -6
  142. package/dist/commands/proposal/drain.js +91 -40
  143. package/dist/commands/proposal/proposal-cli.js +134 -132
  144. package/dist/commands/proposal/proposal-types.js +56 -0
  145. package/dist/commands/proposal/proposal.js +83 -65
  146. package/dist/commands/proposal/propose-cli.js +88 -0
  147. package/dist/commands/proposal/propose.js +105 -88
  148. package/dist/commands/proposal/repository.js +1303 -278
  149. package/dist/commands/proposal/validators/proposal-quality-validators.js +16 -6
  150. package/dist/commands/proposal/validators/proposal-validators.js +61 -12
  151. package/dist/commands/proposal/validators/proposals.js +6 -8
  152. package/dist/commands/read/curate.js +78 -73
  153. package/dist/commands/read/knowledge.js +510 -13
  154. package/dist/commands/read/registry-search.js +2 -2
  155. package/dist/commands/read/remember-cli.js +84 -15
  156. package/dist/commands/read/search-cli.js +203 -96
  157. package/dist/commands/read/search.js +126 -94
  158. package/dist/commands/read/show.js +226 -250
  159. package/dist/commands/registry-cli.js +34 -60
  160. package/dist/commands/remember.js +18 -57
  161. package/dist/commands/sources/add-cli.js +104 -49
  162. package/dist/commands/sources/bundle-cli.js +166 -0
  163. package/dist/commands/sources/bundle-config-ops.js +63 -0
  164. package/dist/commands/sources/info.js +27 -15
  165. package/dist/commands/sources/init.js +30 -40
  166. package/dist/commands/sources/installed-stashes.js +469 -172
  167. package/dist/commands/sources/migration-help.js +7 -4
  168. package/dist/commands/sources/schema-repair.js +10 -9
  169. package/dist/commands/sources/self-update.js +182 -121
  170. package/dist/commands/sources/source-add.js +169 -178
  171. package/dist/commands/sources/source-clone.js +144 -41
  172. package/dist/commands/sources/source-manage.js +94 -59
  173. package/dist/commands/sources/sources-cli.js +64 -205
  174. package/dist/commands/sources/stash-cli.js +91 -54
  175. package/dist/commands/sources/stash-skeleton.js +1 -1
  176. package/dist/commands/tasks/tasks-cli.js +106 -104
  177. package/dist/commands/tasks/tasks.js +445 -262
  178. package/dist/commands/workflow-cli.js +232 -121
  179. package/dist/core/action-contributors.js +1 -1
  180. package/dist/core/activation-policy.js +49 -0
  181. package/dist/core/adapter/adapters/agent-skills-adapter.js +181 -0
  182. package/dist/core/adapter/adapters/akm-adapter.js +528 -0
  183. package/dist/core/adapter/adapters/akm-lint.js +392 -0
  184. package/dist/core/adapter/adapters/akm-metadata.js +387 -0
  185. package/dist/core/adapter/adapters/akm-task-adapter.js +149 -0
  186. package/dist/core/adapter/adapters/akm-workflow-adapter.js +180 -0
  187. package/dist/core/adapter/adapters/claude-adapter.js +61 -0
  188. package/dist/core/adapter/adapters/dotenv-adapter.js +187 -0
  189. package/dist/core/adapter/adapters/generic-files-adapter.js +119 -0
  190. package/dist/core/adapter/adapters/index.js +80 -0
  191. package/dist/core/adapter/adapters/llm-wiki-adapter.js +419 -0
  192. package/dist/core/adapter/adapters/okf-adapter.js +391 -0
  193. package/dist/core/adapter/adapters/opencode-adapter.js +68 -0
  194. package/dist/core/adapter/adapters/shared.js +286 -0
  195. package/dist/core/adapter/adapters/tool-dir-shared.js +217 -0
  196. package/dist/core/adapter/adapters/website-snapshot-adapter.js +155 -0
  197. package/dist/core/adapter/bundle-adapter.js +4 -0
  198. package/dist/core/adapter/detect-adapter.js +17 -0
  199. package/dist/core/adapter/recognize-match.js +44 -0
  200. package/dist/core/adapter/registry.js +56 -0
  201. package/dist/core/adapter/types.js +4 -0
  202. package/dist/core/asset/akm-markdown.js +30 -0
  203. package/dist/core/asset/asset-placement.js +243 -0
  204. package/dist/core/asset/asset-ref.js +110 -79
  205. package/dist/core/asset/asset-serialize.js +20 -0
  206. package/dist/core/asset/frontmatter.js +28 -12
  207. package/dist/core/asset/markdown.js +40 -51
  208. package/dist/core/asset/resolve-ref.js +274 -0
  209. package/dist/core/asset/stash-meta.js +2 -2
  210. package/dist/core/bundle-id.js +51 -0
  211. package/dist/core/common.js +281 -86
  212. package/dist/core/config/config-io.js +42 -128
  213. package/dist/core/config/config-schema.js +233 -834
  214. package/dist/core/config/config-sources.js +162 -39
  215. package/dist/core/config/config-types.js +16 -11
  216. package/dist/core/config/config-version.js +29 -0
  217. package/dist/core/config/config-walker.js +126 -37
  218. package/dist/core/config/config.js +154 -331
  219. package/dist/core/config/deep-merge.js +41 -0
  220. package/dist/core/config/engine-semantics.js +28 -0
  221. package/dist/core/config/experimental.js +21 -0
  222. package/dist/core/config/schema/embedding.js +38 -0
  223. package/dist/core/config/schema/engines.js +116 -0
  224. package/dist/core/config/schema/experimental.js +47 -0
  225. package/dist/core/config/schema/feedback.js +31 -0
  226. package/dist/core/config/schema/improve-processes.js +389 -0
  227. package/dist/core/config/schema/improve.js +94 -0
  228. package/dist/core/config/schema/index-config.js +176 -0
  229. package/dist/core/config/schema/output.js +18 -0
  230. package/dist/core/config/schema/primitives.js +94 -0
  231. package/dist/core/config/schema/search.js +30 -0
  232. package/dist/core/config/schema/setup.js +18 -0
  233. package/dist/core/config/schema/sources-bundles.js +169 -0
  234. package/dist/core/config/schema/workflow.js +29 -0
  235. package/dist/core/env-secret-ref.js +155 -20
  236. package/dist/core/errors.js +17 -15
  237. package/dist/core/events-types.js +4 -0
  238. package/dist/core/events.js +46 -128
  239. package/dist/core/extra-params.js +62 -0
  240. package/dist/core/file-change.js +17 -0
  241. package/dist/core/file-lock.js +202 -57
  242. package/dist/core/fs-txn.js +392 -0
  243. package/dist/core/git-message.js +59 -0
  244. package/dist/core/improve-result.js +167 -0
  245. package/dist/core/json-schema.js +142 -0
  246. package/dist/core/lesson-lint.js +1 -17
  247. package/dist/core/logs-db.js +1 -1
  248. package/dist/core/maintenance-barrier.js +135 -0
  249. package/dist/core/migration-operation.js +44 -0
  250. package/dist/core/mutation-target.js +78 -0
  251. package/dist/core/paths.js +22 -25
  252. package/dist/core/platform.js +10 -0
  253. package/dist/core/recognition-util.js +128 -0
  254. package/dist/core/redaction.js +392 -0
  255. package/dist/core/standards/resolve-standards-context.js +36 -65
  256. package/dist/core/standards/resolve-stash-standards.js +2 -2
  257. package/dist/core/standards/resolve-type-conventions.js +5 -5
  258. package/dist/core/state/migrations.js +242 -11
  259. package/dist/core/state-db.js +98 -10
  260. package/dist/core/structured.js +1 -1
  261. package/dist/core/subprocess.js +303 -0
  262. package/dist/core/text-truncation.js +9 -5
  263. package/dist/core/time.js +20 -0
  264. package/dist/core/type-presentation.js +130 -0
  265. package/dist/core/warn.js +0 -3
  266. package/dist/core/write-source.js +834 -118
  267. package/dist/indexer/bundle-identity-guard.js +92 -0
  268. package/dist/indexer/db/graph-db.js +1 -25
  269. package/dist/indexer/db/llm-cache.js +1 -1
  270. package/dist/indexer/ensure-index.js +30 -9
  271. package/dist/indexer/graph/graph-boost.js +9 -30
  272. package/dist/indexer/graph/graph-extraction.js +41 -27
  273. package/dist/indexer/graph/graph-types.js +4 -0
  274. package/dist/indexer/index-writer-lock.js +93 -49
  275. package/dist/indexer/index-written-assets.js +100 -53
  276. package/dist/indexer/indexer.js +746 -329
  277. package/dist/indexer/init.js +18 -25
  278. package/dist/indexer/installations.js +142 -0
  279. package/dist/indexer/passes/dir-staleness.js +18 -10
  280. package/dist/indexer/passes/memory-inference.js +25 -15
  281. package/dist/indexer/passes/metadata.js +412 -243
  282. package/dist/indexer/scan/doc-to-entry.js +160 -0
  283. package/dist/indexer/scan/drain-dir.js +134 -0
  284. package/dist/indexer/search/db-search.js +292 -108
  285. package/dist/indexer/search/fts-query.js +64 -0
  286. package/dist/indexer/search/ranking-contributors.js +145 -25
  287. package/dist/indexer/search/ranking-types.js +4 -0
  288. package/dist/indexer/search/ranking.js +28 -71
  289. package/dist/indexer/search/search-attribution.js +67 -0
  290. package/dist/indexer/search/search-fields.js +18 -3
  291. package/dist/indexer/search/search-hit-enrichers.js +30 -40
  292. package/dist/indexer/search/search-source.js +157 -111
  293. package/dist/indexer/search/semantic-status.js +4 -1
  294. package/dist/indexer/usage/usage-events.js +10 -30
  295. package/dist/indexer/walk/file-context.js +3 -45
  296. package/dist/indexer/walk/matchers.js +42 -34
  297. package/dist/indexer/walk/path-resolver.js +11 -5
  298. package/dist/indexer/walk/walker.js +42 -14
  299. package/dist/integrations/agent/builder-shared.js +7 -0
  300. package/dist/integrations/agent/builders.js +5 -56
  301. package/dist/integrations/agent/config.js +3 -143
  302. package/dist/integrations/agent/detect.js +17 -2
  303. package/dist/integrations/agent/engine-resolution.js +231 -0
  304. package/dist/integrations/agent/index.js +1 -2
  305. package/dist/integrations/agent/model-aliases.js +16 -2
  306. package/dist/integrations/agent/profiles.js +36 -62
  307. package/dist/integrations/agent/prompts.js +46 -18
  308. package/dist/integrations/agent/runner-dispatch.js +93 -4
  309. package/dist/integrations/agent/runner.js +76 -208
  310. package/dist/integrations/agent/spawn.js +88 -196
  311. package/dist/integrations/harnesses/aider/agent-builder.js +114 -0
  312. package/dist/integrations/harnesses/aider/index.js +48 -0
  313. package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
  314. package/dist/integrations/harnesses/amazonq/agent-builder.js +147 -0
  315. package/dist/integrations/harnesses/amazonq/index.js +45 -0
  316. package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
  317. package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
  318. package/dist/integrations/harnesses/claude/config-import.js +1 -3
  319. package/dist/integrations/harnesses/claude/index.js +24 -35
  320. package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
  321. package/dist/integrations/harnesses/claude/session-log.js +27 -75
  322. package/dist/integrations/harnesses/codex/agent-builder.js +138 -0
  323. package/dist/integrations/harnesses/codex/index.js +52 -0
  324. package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
  325. package/dist/integrations/harnesses/copilot/agent-builder.js +122 -0
  326. package/dist/integrations/harnesses/copilot/index.js +48 -0
  327. package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
  328. package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
  329. package/dist/integrations/harnesses/gemini/index.js +48 -0
  330. package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
  331. package/dist/integrations/harnesses/ids.js +24 -0
  332. package/dist/integrations/harnesses/index.js +54 -34
  333. package/dist/integrations/harnesses/opencode/agent-builder.js +23 -5
  334. package/dist/integrations/harnesses/opencode/config-import.js +1 -3
  335. package/dist/integrations/harnesses/opencode/index.js +14 -32
  336. package/dist/integrations/harnesses/opencode/session-log.js +67 -125
  337. package/dist/integrations/harnesses/opencode-sdk/harness.js +51 -0
  338. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
  339. package/dist/integrations/harnesses/openhands/agent-builder.js +128 -0
  340. package/dist/integrations/harnesses/openhands/index.js +48 -0
  341. package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
  342. package/dist/integrations/harnesses/pi/agent-builder.js +97 -0
  343. package/dist/integrations/harnesses/pi/index.js +45 -0
  344. package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
  345. package/dist/integrations/harnesses/shared.js +17 -0
  346. package/dist/integrations/harnesses/types.js +43 -32
  347. package/dist/integrations/lockfile.js +211 -24
  348. package/dist/integrations/session-logs/index.js +36 -39
  349. package/dist/integrations/session-logs/provider-base.js +113 -0
  350. package/dist/llm/client.js +182 -110
  351. package/dist/llm/embedders/deterministic.js +2 -2
  352. package/dist/llm/embedders/remote.js +21 -9
  353. package/dist/llm/feature-gate.js +17 -57
  354. package/dist/llm/graph-extract.js +12 -13
  355. package/dist/llm/index-passes.js +8 -42
  356. package/dist/llm/memory-infer.js +144 -1
  357. package/dist/llm/metadata-enhance.js +45 -30
  358. package/dist/llm/structured-call.js +16 -8
  359. package/dist/llm/usage-persist.js +30 -5
  360. package/dist/llm/usage-telemetry.js +59 -6
  361. package/dist/output/cli-hints.js +1 -2
  362. package/dist/output/command-registry.js +27 -0
  363. package/dist/output/context.js +22 -7
  364. package/dist/output/format-exempt.js +80 -0
  365. package/dist/output/generic-render.js +251 -0
  366. package/dist/output/html-render.js +11 -16
  367. package/dist/output/render-registry.js +57 -0
  368. package/dist/output/renderers.js +14 -279
  369. package/dist/output/shapes/curate.js +10 -1
  370. package/dist/output/shapes/events.js +12 -7
  371. package/dist/output/shapes/helpers.js +58 -84
  372. package/dist/output/shapes/passthrough.js +11 -39
  373. package/dist/output/shapes/proposal/producer.js +15 -7
  374. package/dist/output/shapes/registry.js +12 -6
  375. package/dist/output/shapes.js +0 -9
  376. package/dist/output/text/{init.js → bundle-create.js} +3 -1
  377. package/dist/output/text/bundle-show.js +7 -0
  378. package/dist/output/text/command-format.js +562 -0
  379. package/dist/output/text/env.js +1 -3
  380. package/dist/output/text/events.js +8 -7
  381. package/dist/output/text/helpers.js +15 -1164
  382. package/dist/output/text/proposal/producer.js +4 -2
  383. package/dist/output/text/proposal-format.js +202 -0
  384. package/dist/output/text/registry-commands.js +1 -2
  385. package/dist/output/text/registry.js +12 -6
  386. package/dist/output/text/show-directives.js +117 -0
  387. package/dist/output/text/show-format.js +103 -0
  388. package/dist/output/text/sync.js +5 -0
  389. package/dist/output/text/workflow-format.js +332 -0
  390. package/dist/output/text/workflow.js +3 -2
  391. package/dist/output/text.js +10 -19
  392. package/dist/registry/factory.js +4 -6
  393. package/dist/registry/origin-resolve.js +16 -27
  394. package/dist/registry/providers/skills-sh.js +3 -3
  395. package/dist/registry/providers/static-index.js +15 -25
  396. package/dist/registry/resolve.js +43 -94
  397. package/dist/registry/semver.js +43 -0
  398. package/dist/runtime.js +81 -12
  399. package/dist/scripts/akm-migrate.js +35529 -0
  400. package/dist/setup/detect.js +5 -7
  401. package/dist/setup/detected-engines.js +136 -0
  402. package/dist/setup/engine-config.js +100 -0
  403. package/dist/setup/registry-stash-loader.js +3 -3
  404. package/dist/setup/semantic-assets.js +12 -9
  405. package/dist/setup/setup.js +444 -208
  406. package/dist/setup/steps/connection-shared.js +120 -0
  407. package/dist/setup/steps/connection.js +108 -305
  408. package/dist/setup/steps/platforms.js +13 -12
  409. package/dist/setup/steps/semantic.js +15 -3
  410. package/dist/setup/steps/sources.js +21 -15
  411. package/dist/setup/steps/stashdir.js +6 -4
  412. package/dist/setup/steps/tasks.js +236 -119
  413. package/dist/setup/steps.js +3 -2
  414. package/dist/sources/freshness.js +39 -0
  415. package/dist/sources/provider-factory.js +11 -17
  416. package/dist/sources/providers/filesystem.js +2 -3
  417. package/dist/sources/providers/git-install.js +278 -34
  418. package/dist/sources/providers/git-provider.js +54 -56
  419. package/dist/sources/providers/git-stash.js +420 -91
  420. package/dist/sources/providers/git.js +2 -2
  421. package/dist/sources/providers/npm.js +16 -19
  422. package/dist/sources/providers/provider-utils.js +47 -22
  423. package/dist/sources/providers/sync-from-ref.js +3 -9
  424. package/dist/sources/providers/website.js +2 -2
  425. package/dist/sources/resolve.js +11 -10
  426. package/dist/sources/snapshot-fetchers/types.js +4 -0
  427. package/dist/sources/{website-ingest.js → snapshot-fetchers/website-ingest.js} +110 -41
  428. package/dist/storage/database.js +60 -4
  429. package/dist/storage/engines/sqlite-migrations.js +156 -5
  430. package/dist/storage/locations.js +1 -2
  431. package/dist/storage/repositories/canaries-repository.js +1 -1
  432. package/dist/storage/repositories/events-repository.js +51 -11
  433. package/dist/storage/repositories/improve-runs-repository.js +6 -32
  434. package/dist/storage/repositories/index-connection.js +79 -0
  435. package/dist/storage/repositories/index-db.js +4 -3
  436. package/dist/storage/repositories/index-entries-repository.js +863 -0
  437. package/dist/{indexer/db/entry-mapper.js → storage/repositories/index-entry-mapper.js} +19 -2
  438. package/dist/storage/repositories/index-entry-types.js +4 -0
  439. package/dist/storage/repositories/index-fts-repository.js +167 -0
  440. package/dist/storage/repositories/index-llm-cache-repository.js +108 -0
  441. package/dist/storage/repositories/index-meta-repository.js +49 -0
  442. package/dist/{indexer/db/schema.js → storage/repositories/index-schema.js} +226 -100
  443. package/dist/storage/repositories/index-sql.js +12 -0
  444. package/dist/storage/repositories/index-utility-repository.js +356 -0
  445. package/dist/storage/repositories/index-vec-repository.js +250 -0
  446. package/dist/storage/repositories/outcome-repository.js +119 -0
  447. package/dist/storage/repositories/proposals-repository.js +317 -75
  448. package/dist/storage/repositories/registry-cache.js +1 -1
  449. package/dist/storage/repositories/salience-repository.js +172 -0
  450. package/dist/storage/repositories/task-history-repository.js +110 -3
  451. package/dist/storage/repositories/workflow-runs-repository.js +240 -19
  452. package/dist/tasks/backends/cron.js +169 -46
  453. package/dist/tasks/backends/exec-utils.js +76 -3
  454. package/dist/tasks/backends/index.js +6 -9
  455. package/dist/tasks/backends/launchd.js +292 -55
  456. package/dist/tasks/backends/schtasks.js +557 -70
  457. package/dist/tasks/backends/types.js +4 -0
  458. package/dist/tasks/command-executable.js +93 -0
  459. package/dist/tasks/embedded.js +56 -38
  460. package/dist/tasks/parser.js +156 -64
  461. package/dist/tasks/resolve-akm-bin.js +144 -51
  462. package/dist/tasks/runner.js +377 -209
  463. package/dist/tasks/schedule.js +108 -19
  464. package/dist/tasks/scheduler-invocation.js +296 -0
  465. package/dist/tasks/schema.js +1 -1
  466. package/dist/tasks/task-id.js +35 -0
  467. package/dist/tasks/validator.js +30 -16
  468. package/dist/text-import-hook.mjs +1 -1
  469. package/dist/workflows/authoring/authoring.js +104 -43
  470. package/dist/workflows/authoring/scope-key.js +1 -1
  471. package/dist/workflows/cli.js +0 -16
  472. package/dist/workflows/concurrency-policy.js +15 -0
  473. package/dist/workflows/exec/brief.js +450 -0
  474. package/dist/workflows/exec/frozen-judge.js +47 -0
  475. package/dist/workflows/exec/native-executor.js +1038 -0
  476. package/dist/workflows/exec/param-secrets.js +115 -0
  477. package/dist/workflows/exec/report.js +1460 -0
  478. package/dist/workflows/exec/run-workflow.js +602 -0
  479. package/dist/workflows/exec/scheduler.js +71 -0
  480. package/dist/workflows/exec/step-work.js +1190 -0
  481. package/dist/workflows/exec/unit-writer.js +23 -0
  482. package/dist/workflows/exec/workflow-engine-gate.js +67 -0
  483. package/dist/workflows/exec/worktree.js +171 -0
  484. package/dist/workflows/ir/compile.js +246 -0
  485. package/dist/workflows/ir/freeze.js +233 -0
  486. package/dist/workflows/ir/params.js +54 -0
  487. package/dist/workflows/ir/plan-hash.js +68 -0
  488. package/dist/workflows/ir/schema.js +540 -0
  489. package/dist/workflows/parser.js +878 -304
  490. package/dist/workflows/program/expressions.js +181 -0
  491. package/dist/workflows/program/schema.js +51 -0
  492. package/dist/workflows/renderer.js +100 -45
  493. package/dist/workflows/resource-limits.js +22 -0
  494. package/dist/workflows/runtime/agent-identity.js +59 -14
  495. package/dist/workflows/runtime/checkin.js +1 -1
  496. package/dist/workflows/runtime/plan-classifier.js +131 -0
  497. package/dist/workflows/runtime/runs.js +376 -119
  498. package/dist/workflows/runtime/unit-checkin.js +45 -0
  499. package/dist/workflows/runtime/unit-phases.js +20 -0
  500. package/dist/workflows/runtime/workflow-asset-loader.js +241 -40
  501. package/dist/workflows/schema.js +1 -11
  502. package/dist/workflows/validate-summary.js +2 -3
  503. package/dist/workflows/validator.js +52 -30
  504. package/docs/README.md +42 -78
  505. package/docs/migration/README.md +8 -0
  506. package/docs/migration/release-notes/0.6.0.md +1 -1
  507. package/docs/migration/release-notes/0.7.0.md +9 -8
  508. package/docs/migration/release-notes/0.9.0.md +158 -14
  509. package/docs/migration/v0.7-to-v0.8.md +46 -47
  510. package/docs/migration/v0.8-to-v0.9.md +844 -0
  511. package/docs/reference/README.md +12 -0
  512. package/docs/reference/data-and-telemetry.md +333 -0
  513. package/package.json +21 -17
  514. package/schemas/akm-asset-envelope.json +93 -0
  515. package/schemas/akm-config.json +4636 -0
  516. package/schemas/akm-task.json +87 -0
  517. package/schemas/akm-workflow.json +373 -0
  518. package/dist/akm-migrate-storage +0 -38
  519. package/dist/assets/help/help-accept.md +0 -12
  520. package/dist/assets/help/help-improve.md +0 -84
  521. package/dist/assets/help/help-proposals.md +0 -17
  522. package/dist/assets/help/help-propose.md +0 -17
  523. package/dist/assets/help/help-reject.md +0 -11
  524. package/dist/assets/profiles/frequent.json +0 -13
  525. package/dist/assets/profiles/recombine-only.json +0 -21
  526. package/dist/assets/profiles/reflect-distill.json +0 -30
  527. package/dist/assets/profiles/synthesize.json +0 -15
  528. package/dist/assets/prompts/procedural-system.md +0 -44
  529. package/dist/assets/prompts/recombine-system.md +0 -40
  530. package/dist/assets/prompts/staleness-detect-system.md +0 -6
  531. package/dist/assets/tasks/core/backup.yml +0 -4
  532. package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
  533. package/dist/assets/templates/html/default.html +0 -78
  534. package/dist/assets/templates/html/vendor/echarts.min.js +0 -45
  535. package/dist/assets/wiki/index-template.md +0 -12
  536. package/dist/assets/wiki/ingest-workflow-template.md +0 -83
  537. package/dist/assets/wiki/log-template.md +0 -8
  538. package/dist/assets/wiki/schema-template.md +0 -61
  539. package/dist/cli/config-migrate.js +0 -150
  540. package/dist/cli/config-validate.js +0 -39
  541. package/dist/commands/graph/graph-cli.js +0 -124
  542. package/dist/commands/graph/graph.js +0 -487
  543. package/dist/commands/improve/calibration.js +0 -161
  544. package/dist/commands/improve/dedup.js +0 -482
  545. package/dist/commands/improve/extract-watch.js +0 -140
  546. package/dist/commands/improve/hot-probation.js +0 -45
  547. package/dist/commands/improve/improve-auto-accept.js +0 -276
  548. package/dist/commands/improve/improve-profiles.js +0 -168
  549. package/dist/commands/improve/procedural.js +0 -398
  550. package/dist/commands/improve/recombine.js +0 -818
  551. package/dist/commands/improve/schema-similarity-gate.js +0 -168
  552. package/dist/commands/lint/agent-linter.js +0 -44
  553. package/dist/commands/lint/command-linter.js +0 -44
  554. package/dist/commands/lint/default-linter.js +0 -16
  555. package/dist/commands/lint/fact-linter.js +0 -39
  556. package/dist/commands/lint/knowledge-linter.js +0 -16
  557. package/dist/commands/lint/memory-linter.js +0 -61
  558. package/dist/commands/lint/registry.js +0 -41
  559. package/dist/commands/lint/skill-linter.js +0 -45
  560. package/dist/commands/lint/task-linter.js +0 -50
  561. package/dist/commands/lint/workflow-linter.js +0 -81
  562. package/dist/commands/proposal/legacy-import.js +0 -115
  563. package/dist/commands/sources/history.js +0 -196
  564. package/dist/commands/tasks/default-tasks.js +0 -186
  565. package/dist/commands/wiki-cli.js +0 -292
  566. package/dist/core/asset/asset-registry.js +0 -76
  567. package/dist/core/asset/asset-spec.js +0 -259
  568. package/dist/core/config/config-migration.js +0 -602
  569. package/dist/core/deep-merge.js +0 -38
  570. package/dist/core/eval/rank-metrics.js +0 -113
  571. package/dist/core/ripgrep/install.js +0 -163
  572. package/dist/core/ripgrep/resolve.js +0 -81
  573. package/dist/indexer/db/db.js +0 -1413
  574. package/dist/indexer/manifest.js +0 -170
  575. package/dist/indexer/passes/metadata-contributors.js +0 -31
  576. package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -94
  577. package/dist/integrations/harnesses/opencode-sdk/index.js +0 -49
  578. package/dist/llm/call-ai.js +0 -62
  579. package/dist/llm/memory-infer-impl.js +0 -138
  580. package/dist/output/shapes/distill.js +0 -14
  581. package/dist/output/shapes/history.js +0 -11
  582. package/dist/output/text/distill.js +0 -6
  583. package/dist/output/text/enable-disable.js +0 -8
  584. package/dist/output/text/history.js +0 -6
  585. package/dist/output/text/wiki.js +0 -16
  586. package/dist/registry/build-index.js +0 -386
  587. package/dist/scripts/migrate-storage.js +0 -19108
  588. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +0 -9411
  589. package/dist/scripts/migrations/v16-to-v17.js +0 -141
  590. package/dist/setup/legacy-config.js +0 -106
  591. package/dist/storage/repositories/consolidation-repository.js +0 -38
  592. package/dist/storage/repositories/recombine-repository.js +0 -213
  593. package/dist/wiki/wiki-templates.js +0 -15
  594. package/dist/wiki/wiki.js +0 -1012
  595. package/dist/workflows/db.js +0 -215
  596. package/docs/data-and-telemetry.md +0 -226
  597. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/registry.js +0 -0
  598. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/youtube.js +0 -0
@@ -0,0 +1,1038 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * Native executor — executes one frozen step subgraph (`IrStepPlan.root`) on
6
+ * the local machine: fan-out through the scheduler, schema-validated
7
+ * structured output through `runStructured` (core/structured.ts), per-unit
8
+ * persistence through the serialized writer queue, and `workflow_unit_*`
9
+ * events for observability.
10
+ *
11
+ * Data flow (redesign addendum, R1): workflow-authored templates go through
12
+ * the deterministic `${{ … }}` expression language (`program/expressions.ts`)
13
+ * — but ONLY for nodes the frontend marked `templating: "expressions"` (YAML
14
+ * program units). Classic linear markdown instructions are `"verbatim"`:
15
+ * opaque data handed to the agent byte-exact (the stable CLI contract — a
16
+ * literal `${{` there is content, never grammar). Expression templates are
17
+ * parsed ONCE per step and resolved per unit against `{ params, stepOutputs,
18
+ * item, item_index }`; `map.over` resolves as a single whole-value reference.
19
+ * Substituted content is data, never re-scanned — the P1 `{{item}}` re-scan
20
+ * injection class is structurally impossible. There is NO ambient key search:
21
+ * a `steps.<id>.output.<path>` reference addresses INTO that step's recorded
22
+ * output explicitly.
23
+ *
24
+ * Step outputs (`${{ steps.<id>.output… }}`): every engine-executed step
25
+ * journals a promoted ARTIFACT under `evidence.output` — the solo unit's
26
+ * result/text, the collect reducer's per-item array, or the vote reducer's
27
+ * winner — and that artifact is what the expression scope exposes
28
+ * ({@link projectStepOutput}). The documented addressing
29
+ * (`steps.discover.output.files`) therefore resolves against real step
30
+ * results, never the raw evidence envelope (peer review R1). Steps completed
31
+ * manually (no `output` key in their evidence) expose their recorded evidence
32
+ * object as-is.
33
+ *
34
+ * Empty free-text outputs (peer review): a SUCCESSFUL schemaless unit that
35
+ * returns the empty string is normalized to "no output" — {@link dispatchUnit}
36
+ * drops the falsy `text`, `finishUnit` journals `result_json = NULL`, and both
37
+ * durable-row reuse and the R3 report surface rehydrate the same absence
38
+ * (`unitOutcomeFromRow`). This is the ONLY empty-output resolution: `''` never
39
+ * survives on any surface, so the live artifact cannot diverge from the
40
+ * resume/report artifact (the cross-surface parity cardinal rule; the
41
+ * `EMPTY_OUTPUT` driver-parity golden pins it). Consequences that follow from
42
+ * "empty == absent", not special-cased anywhere:
43
+ * - a SOLO empty step promotes `output = null` (the unit's absent text ??
44
+ * null); a `collect` fan-out promotes `null` in that item's slot.
45
+ * - A downstream `${{ steps.x.output }}` of an empty solo step therefore
46
+ * resolves against `null` and fails LOUDLY at expression resolution
47
+ * (`… resolved to null`) — a deterministic `expression_error` on BOTH
48
+ * surfaces, never a silent empty string.
49
+ * - A SCHEMA unit is unaffected by this normalization: an empty response is
50
+ * not parseable JSON, so `runStructured` fails it (`parse_error`) — an
51
+ * empty output can never satisfy a declared schema as a silent `null`.
52
+ *
53
+ * Typed artifacts (addendum, R2): when the step declares an `output` schema
54
+ * (`IrStepPlan.outputSchema`), the promoted artifact is validated with the
55
+ * JSON-schema-subset validator BEFORE the step can complete. A mismatch fails
56
+ * the step (fail-fast) with the validation errors in the summary — a
57
+ * downstream consumer must never receive an artifact the author's contract
58
+ * says cannot exist. The failure is flagged (`artifactSchemaFailure` on the
59
+ * result) so the engine's bounded gate loop can re-run the step with the
60
+ * validation errors as feedback ("gate loops can re-run it") — a step with
61
+ * loop budget left regenerates instead of killing the run.
62
+ *
63
+ * Unit identity (addendum, R2): CONTENT-DERIVED, never positional. A fan-out
64
+ * unit's id is `<node_id>:<sha256(canonicalJson(item))[:12]>`; a solo unit's
65
+ * is `<node_id>:solo`. Identity therefore survives item-list regeneration and
66
+ * reordering — resuming a run whose producer re-emitted the same items in a
67
+ * different order reuses every journaled result. Consequences:
68
+ * - DUPLICATE items in one fan-out list collide on identity. That is an
69
+ * authoring error (the same work dispatched twice under one id): the step
70
+ * fails deterministically after resolving the item list, naming the
71
+ * duplicate, before anything dispatches.
72
+ * - REPLAY DIVERGENCE: a journaled COMPLETED row whose unit_id matches but
73
+ * whose `input_hash` differs is a hard step failure ("replay divergence"),
74
+ * never a silent re-dispatch — under a frozen plan the same identity must
75
+ * reproduce the same inputs, so a mismatch means the journal (or params
76
+ * row) was tampered with. Failed/running/missing rows dispatch live.
77
+ * - Rows with unrelated ids never match a content-derived id and are ignored.
78
+ *
79
+ * Gate loops (addendum, R2 `gate.max_loops`): when the engine re-executes a
80
+ * step subgraph after a gate rejection, it threads the judge's feedback in as
81
+ * `ctx.gateFeedback` (appended to every unit prompt — the input hash changes,
82
+ * so re-dispatch is natural) and marks the attempt with `ctx.gateLoop` (>= 2).
83
+ * Loop attempts journal under `<unitId>~l<loop>` — like `~r<n>` retries, pure
84
+ * journal bookkeeping on top of the content-derived identity, so loop 1's
85
+ * rows are never clobbered. Because gate feedback is JUDGE-authored (a fresh
86
+ * LLM output per invocation, not a pure function of the frozen plan), a
87
+ * journaled loop row whose hash no longer matches re-dispatches live instead
88
+ * of raising replay divergence — the divergence guarantee applies to loop-1
89
+ * rows, whose inputs ARE pure functions of (plan, params, journaled results).
90
+ *
91
+ * Failure policy (addendum, "explicit surface, fail-fast default"):
92
+ * - `onError: "fail"` (default) fails the step on any unit failure;
93
+ * `"continue"` records failures in the evidence and lets the gate decide.
94
+ * - `retry: { max, on }` re-dispatches a failed unit up to `max` extra
95
+ * times when its `failureReason` is in `on`. Every retry journals its OWN
96
+ * row under `<unitId>~r<attempt>` so no attempt's record is clobbered.
97
+ *
98
+ * Worktree isolation (addendum, R2 `isolation: worktree`): each journaled
99
+ * attempt of an isolated agent/sdk unit runs in a FRESH detached git worktree
100
+ * of the engine's working directory (`ctx.workDir`, default `process.cwd()`),
101
+ * minted under a run-scoped tmp dir (`worktree.ts`) and passed to dispatch as
102
+ * the child's cwd. The path is journaled on the unit row (`worktree_path`);
103
+ * after the unit finishes, a clean worktree is removed and a dirty one is
104
+ * retained + logged (uncollected work is never destroyed). "Clean" is
105
+ * `git status --porcelain` WITHOUT `--ignored`, so a worktree whose only
106
+ * residue is `.gitignore`-matched files (build outputs, `node_modules`) counts
107
+ * as clean and IS removed — those files are disposable by the repo's own
108
+ * declaration, and retaining a worktree per build would blow up disk
109
+ * (`worktree.ts` contract). A non-git base directory fails the step cleanly
110
+ * before any dispatch, and llm units reject isolation loudly — there is no
111
+ * child process to isolate.
112
+ *
113
+ * Budget ceilings (addendum, R2): a frozen plan's `budget` block
114
+ * (`max_units` / `max_tokens`) is enforced per RUN. The engine seeds
115
+ * `ctx.unitsDispatched` (journal row count) and `ctx.tokensUsed` (journaled
116
+ * token sum) and threads the running totals across steps; this executor
117
+ * consumes both per ACTUAL dispatch. Hitting a ceiling aborts pending and
118
+ * in-flight dispatches through an AbortController chained onto `ctx.signal`
119
+ * and fails the step with a "budget exceeded (<which> ceiling)" summary —
120
+ * hard, regardless of `on_error`, exactly like the lifetime cap.
121
+ *
122
+ * Layering (see the plan's *Reconciliation* section):
123
+ * - Dispatch goes through ONE injected {@link UnitDispatcher} seam. The
124
+ * default dispatcher composes the EXISTING substrate — `executeRunner`
125
+ * (agent/sdk) and `chatCompletion` (llm, lazily imported so the engine
126
+ * stays offline-capable until a workflow actually declares an llm unit).
127
+ * - This module NEVER writes step rows: advancing the gated spine is the
128
+ * engine loop's job (`run-workflow.ts`) via `completeWorkflowStep`.
129
+ */
130
+ import { deepMergeConfig } from "../../core/config/deep-merge.js";
131
+ import { ConfigError } from "../../core/errors.js";
132
+ import { appendEvent } from "../../core/events.js";
133
+ import { validateJsonSchemaSubset } from "../../core/json-schema.js";
134
+ import { collectSensitiveValues, isEnvPassthroughValueSafeToExpose, redactSensitiveValue } from "../../core/redaction.js";
135
+ import { runStructured } from "../../core/structured.js";
136
+ import { warn } from "../../core/warn.js";
137
+ import { insertEventStrict } from "../../storage/repositories/events-repository.js";
138
+ import { withWorkflowRunsRepo } from "../../storage/repositories/workflow-runs-repository.js";
139
+ import { LIFETIME_UNIT_CAP, scheduleUnits, UnitCapExceededError } from "./scheduler.js";
140
+ // Shared step semantics — the ONE implementation consumed by both the engine
141
+ // (this module + run-workflow.ts) and, from R3, the brief/report driver
142
+ // protocol. This module dispatches; step-work.ts owns the pure decisions.
143
+ import { computeStepWorkList, reduceEmptyStep, reduceStepOutcomes, stepOutputsFromEvidence, unitOutcomeFromRow, } from "./step-work.js";
144
+ import { enqueueUnitWrite } from "./unit-writer.js";
145
+ import { assertGitWorkTree, cleanupUnitWorktree, createUnitWorktree } from "./worktree.js";
146
+ /**
147
+ * Mutable per-step dispatch budget: the lifetime unit cap PLUS the declared
148
+ * run-level budget ceilings (`budget.max_units` / `budget.max_tokens`,
149
+ * addendum R2). Consumed once per journaled dispatch attempt (including
150
+ * retries); durable-row reuses never touch it — the peer-review fix that
151
+ * keeps large partially-completed fan-outs resumable instead of tripping the
152
+ * cap on `journaled + items`. Token usage accumulates per actual dispatch on
153
+ * top of the journal-seeded run total (reused rows' tokens are already in the
154
+ * seed). Check-and-increment is synchronous, so concurrent units cannot race
155
+ * it; crossing a declared ceiling fires `onExceeded` ONCE (the executor's
156
+ * chained AbortController), aborting pending and in-flight dispatches.
157
+ */
158
+ class DispatchBudget {
159
+ used;
160
+ /** Run-total tokens: journal-seeded input + this step's dispatch usage. */
161
+ tokens;
162
+ /** Set (once) when a dispatch was refused; the step fails with this message. */
163
+ capMessage;
164
+ /** Set (once) when a declared budget ceiling was hit; the step fails hard with it. */
165
+ budgetMessage;
166
+ maxUnits;
167
+ maxTokens;
168
+ onExceeded;
169
+ constructor(alreadyDispatched, opts) {
170
+ this.used = alreadyDispatched;
171
+ this.tokens = opts?.tokensUsed ?? 0;
172
+ this.maxUnits = opts?.budget?.maxUnits;
173
+ this.maxTokens = opts?.budget?.maxTokens;
174
+ this.onExceeded = opts?.onExceeded;
175
+ }
176
+ /** Consume one dispatch slot; false (and a sticky message) when a ceiling or the cap is hit. */
177
+ tryConsume() {
178
+ if (this.budgetMessage !== undefined)
179
+ return false;
180
+ if (this.maxUnits !== undefined && this.used >= this.maxUnits) {
181
+ this.exceed(`budget exceeded (max_units ceiling): ${this.used} unit(s) already dispatched for this run ` +
182
+ `against the workflow's declared budget.max_units of ${this.maxUnits} — refusing further dispatch.`);
183
+ return false;
184
+ }
185
+ if (this.maxTokens !== undefined && this.tokens >= this.maxTokens) {
186
+ this.exceed(`budget exceeded (max_tokens ceiling): ${this.tokens} token(s) already spent for this run ` +
187
+ `against the workflow's declared budget.max_tokens of ${this.maxTokens} — refusing further dispatch.`);
188
+ return false;
189
+ }
190
+ if (this.used >= LIFETIME_UNIT_CAP) {
191
+ this.capMessage ??= new UnitCapExceededError(LIFETIME_UNIT_CAP).message;
192
+ return false;
193
+ }
194
+ this.used++;
195
+ return true;
196
+ }
197
+ /** Record one dispatch's reported usage; crossing `maxTokens` trips the ceiling. */
198
+ addTokens(tokens) {
199
+ this.tokens += tokens;
200
+ if (this.budgetMessage === undefined && this.maxTokens !== undefined && this.tokens >= this.maxTokens) {
201
+ this.exceed(`budget exceeded (max_tokens ceiling): ${this.tokens} token(s) spent for this run, ` +
202
+ `reaching the workflow's declared budget.max_tokens of ${this.maxTokens} — aborting pending dispatches.`);
203
+ }
204
+ }
205
+ exceed(message) {
206
+ this.budgetMessage = message;
207
+ this.onExceeded?.();
208
+ }
209
+ }
210
+ function classifyUnitReuse(workUnit, existingUnits, gateLoop) {
211
+ if (!workUnit.resolved.ok)
212
+ return { kind: "dispatch" };
213
+ const inputHash = workUnit.resolved.inputHash;
214
+ const maxAttempts = 1 + Math.max(0, workUnit.retry?.max ?? 0);
215
+ const base = workUnit.journalBaseId;
216
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
217
+ const attemptId = attempt === 0 ? base : `${base}~r${attempt}`;
218
+ const prior = existingUnits?.get(attemptId);
219
+ if (!prior || prior.status !== "completed")
220
+ continue;
221
+ if (prior.input_hash === inputHash)
222
+ return { kind: "reuse", row: prior };
223
+ // Gate-loop rows are NOT replay-deterministic (the prompt embeds a fresh
224
+ // judge output): a stale loop-N row with a different hash re-dispatches
225
+ // live. Divergence only guards loop-1 rows, whose inputs ARE a pure
226
+ // function of (frozen plan, params, journaled results).
227
+ if (gateLoop > 1)
228
+ return { kind: "dispatch" };
229
+ return { kind: "diverge", attemptId };
230
+ }
231
+ return { kind: "dispatch" };
232
+ }
233
+ /**
234
+ * Does the step have at least one unit that will ACTUALLY dispatch? Env
235
+ * resolution and worktree preflight are dispatch prerequisites, so a step whose
236
+ * units are all reused / unresolved / diverged must skip them (reviewer finding
237
+ * #2). Mirrors runUnit's reuse decision exactly (shared {@link classifyUnitReuse}).
238
+ */
239
+ function stepWillDispatch(workUnits, existingUnits, gateLoop) {
240
+ return workUnits.some((u) => u.resolved.ok && classifyUnitReuse(u, existingUnits, gateLoop).kind === "dispatch");
241
+ }
242
+ /** Execute one step plan natively. Never throws for unit-level failures. */
243
+ export async function executeStepPlan(plan, ctx) {
244
+ const dispatched = ctx.unitsDispatched ?? 0;
245
+ // Work-list computation is the SHARED, PURE decision (step-work.ts): resolve
246
+ // the fan-out list, derive content-derived unit ids, assemble each unit's
247
+ // prompt, and hash its resolved input. `brief` (R3) computes the identical
248
+ // list — that shared implementation is the anti-drift guarantee. This module
249
+ // owns only the impure remainder: env/worktree preflight, durable-row reuse,
250
+ // dispatch, journaling, budget.
251
+ const workList = computeStepWorkList(plan, {
252
+ runId: ctx.runId,
253
+ params: ctx.params,
254
+ stepOutputs: stepOutputsFromEvidence(ctx.evidence),
255
+ engines: ctx.engines ?? {},
256
+ ...(ctx.gateLoop !== undefined ? { gateLoop: ctx.gateLoop } : {}),
257
+ ...(ctx.gateFeedback ? { gateFeedback: ctx.gateFeedback } : {}),
258
+ });
259
+ if (!workList.ok) {
260
+ return failedStep(dispatched, workList.error);
261
+ }
262
+ const { template, reducer, isFanOut, items, units: workUnits } = workList.list;
263
+ if (items.length === 0) {
264
+ // Empty fan-out: the promoted artifact is the degenerate empty value, honored
265
+ // against the step's declared output schema. `reduceEmptyStep` is the SHARED
266
+ // decision (step-work.ts) the R3 report surface also uses to auto-complete an
267
+ // empty step the spine reaches, so both surfaces promote the identical
268
+ // artifact + schema verdict.
269
+ return { ...reduceEmptyStep(plan, reducer), unitsDispatched: dispatched };
270
+ }
271
+ const dispatcher = ctx.dispatcher ?? defaultUnitDispatcher;
272
+ // Durable-row resume: load the step's journaled unit rows FIRST — before
273
+ // resolving env or preflighting worktrees. A unit whose previous attempt
274
+ // completed with the SAME input hash (the canonical envelope in step-work.ts)
275
+ // is reused, not re-dispatched — a crash-resume must never double-issue
276
+ // side-effecting work. Loading the rows up front is what lets us skip the
277
+ // dispatch prerequisites below when nothing will actually dispatch.
278
+ const existingUnits = new Map();
279
+ for (const row of await withWorkflowRunsRepo((repo) => repo.getUnitsForStep(ctx.runId, plan.stepId))) {
280
+ existingUnits.set(row.unit_id, row);
281
+ }
282
+ // Reviewer finding #2: env resolution and worktree preflight are DISPATCH
283
+ // prerequisites, so they must run only when a unit will actually dispatch. A
284
+ // fully-journaled step whose units all reuse completed rows must resume to
285
+ // completion even if an env asset was deleted, a secret is unavailable, the
286
+ // cwd is no longer a git worktree, or git is missing — none of that is needed
287
+ // to hand back a cached result. The predicate mirrors runUnit's reuse
288
+ // decision exactly (shared classifyUnitReuse).
289
+ const gateLoop = ctx.gateLoop ?? 1;
290
+ const willDispatch = stepWillDispatch(workUnits, existingUnits, gateLoop);
291
+ // Env bindings resolve once per step, before any dispatch; a binding error
292
+ // fails the whole step cleanly rather than N units racing into it. Skipped
293
+ // entirely when nothing will dispatch.
294
+ let env;
295
+ if (willDispatch && template.env && template.env.length > 0) {
296
+ const resolveEnv = ctx.resolveEnv ?? resolveEnvBindings;
297
+ try {
298
+ env = await resolveEnv(template.env);
299
+ }
300
+ catch (err) {
301
+ return failedStep(dispatched, `Step "${plan.stepId}" env binding failed: ${message(err)}`);
302
+ }
303
+ }
304
+ // Worktree isolation preflight (addendum R2), once per step, before any
305
+ // dispatch — and ONLY when a unit will dispatch: llm units have no working
306
+ // directory to isolate (fail loudly), and a non-git base directory (or a
307
+ // missing git binary) fails the step cleanly instead of N units racing into
308
+ // identical git errors. The actual worktrees are minted per journaled
309
+ // attempt in dispatchJournaledAttempt.
310
+ let worktreeBase;
311
+ if (willDispatch && template.isolation === "worktree") {
312
+ const engine = template.invocation ? ctx.engines?.[template.invocation.engine] : undefined;
313
+ if (engine?.kind === "llm") {
314
+ return failedStep(dispatched, `Step "${plan.stepId}" declares isolation: worktree on an llm unit — the llm runner has no ` +
315
+ `working directory to isolate. Use the agent or sdk runner for worktree-isolated units.`);
316
+ }
317
+ const base = ctx.workDir ?? process.cwd();
318
+ const preflightWorktree = ctx.preflightWorktree ?? assertGitWorkTree;
319
+ const gitError = preflightWorktree(base);
320
+ if (gitError !== undefined) {
321
+ return failedStep(dispatched, `Step "${plan.stepId}" cannot use isolation: worktree: ${gitError}`);
322
+ }
323
+ worktreeBase = base;
324
+ }
325
+ // Budget ceilings (addendum R2): when the frozen plan declares a budget,
326
+ // dispatch runs under an AbortController CHAINED onto ctx.signal — hitting
327
+ // a ceiling aborts pending and in-flight dispatches, and the step fails
328
+ // hard below. Without a budget the context signal passes through untouched
329
+ // (the no-budget path is byte-identical to pre-R2 behavior).
330
+ const declaredBudget = ctx.budget && (ctx.budget.maxUnits !== undefined || ctx.budget.maxTokens !== undefined) ? ctx.budget : undefined;
331
+ let signal = ctx.signal;
332
+ let onExceeded;
333
+ let unchainSignal;
334
+ if (declaredBudget) {
335
+ const controller = new AbortController();
336
+ const upstream = ctx.signal;
337
+ if (upstream) {
338
+ if (upstream.aborted) {
339
+ controller.abort();
340
+ }
341
+ else {
342
+ const onUpstreamAbort = () => controller.abort();
343
+ upstream.addEventListener("abort", onUpstreamAbort, { once: true });
344
+ unchainSignal = () => upstream.removeEventListener("abort", onUpstreamAbort);
345
+ }
346
+ }
347
+ signal = controller.signal;
348
+ onExceeded = () => controller.abort();
349
+ }
350
+ // Lifetime-cap + declared-budget accounting: seeded with the run's
351
+ // journaled dispatch count and token total, consumed per ACTUAL dispatch
352
+ // inside runUnit — never for durable-row reuses, so resuming a large
353
+ // partially-completed fan-out works.
354
+ const budget = new DispatchBudget(dispatched, {
355
+ tokensUsed: ctx.tokensUsed ?? 0,
356
+ ...(declaredBudget ? { budget: declaredBudget } : {}),
357
+ ...(onExceeded ? { onExceeded } : {}),
358
+ });
359
+ let outcomes;
360
+ const selectedEngine = template.invocation ? ctx.engines?.[template.invocation.engine] : undefined;
361
+ const selectedLlmEngine = selectedEngine?.kind === "llm"
362
+ ? selectedEngine
363
+ : selectedEngine?.kind === "agent" && selectedEngine.fallbackLlmEngine
364
+ ? ctx.engines?.[selectedEngine.fallbackLlmEngine]
365
+ : undefined;
366
+ try {
367
+ outcomes = await scheduleUnits(workUnits, (workUnit) => runUnit({
368
+ plan,
369
+ workUnit,
370
+ env,
371
+ ...(worktreeBase !== undefined ? { worktreeBase } : {}),
372
+ ctx,
373
+ signal,
374
+ dispatcher,
375
+ existingUnits,
376
+ budget,
377
+ }), {
378
+ concurrency: workList.list.concurrency,
379
+ signal,
380
+ maxConcurrency: ctx.maxConcurrency,
381
+ ...(selectedLlmEngine?.kind === "llm" ? { llmConcurrency: selectedLlmEngine.concurrency } : {}),
382
+ });
383
+ }
384
+ finally {
385
+ unchainSignal?.();
386
+ }
387
+ // Declared budget ceilings and the lifetime cap are hard backstops: a step
388
+ // that hit one FAILS regardless of on_error policy (a capped run must never
389
+ // quietly pass its gate). The budget message names WHICH ceiling tripped.
390
+ if (budget.budgetMessage) {
391
+ return { ...failedStep(budget.used, budget.budgetMessage), tokensUsed: budget.tokens };
392
+ }
393
+ if (budget.capMessage) {
394
+ return { ...failedStep(budget.used, budget.capMessage), tokensUsed: budget.tokens };
395
+ }
396
+ const units = outcomes.map((outcome, index) => outcome ?? {
397
+ unitId: workUnits[index].unitId,
398
+ ok: false,
399
+ failureReason: "aborted",
400
+ error: "unit was not dispatched (aborted or scheduler failure)",
401
+ });
402
+ // Replay divergence is a HARD failure regardless of on_error: a journal
403
+ // whose completed row disagrees with the frozen plan's inputs must stop the
404
+ // run loudly (module doc), never be tolerated as "just a failed unit".
405
+ const diverged = units.filter((u) => u.failureReason === "replay_divergence");
406
+ if (diverged.length > 0) {
407
+ return failedStep(budget.used, diverged
408
+ .map((u) => u.error ?? `replay divergence: unit "${u.unitId}" was journaled with different inputs`)
409
+ .join(" "));
410
+ }
411
+ // Failure policy + reducer + typed-artifact validation are the SHARED
412
+ // post-dispatch decision (`reduceStepOutcomes`): `onError: "fail"` (default)
413
+ // fails the step on any unit failure, `"continue"` records failures and lets
414
+ // the gate decide, a vote reducer with no majority fails under either policy,
415
+ // and the promoted artifact is validated against the step's declared output
416
+ // schema (fail-fast; the `artifactSchemaFailure` marker lets the bounded gate
417
+ // loop retry that ONE failure class with the errors as feedback). The report
418
+ // path (R3) reduces journal-replayed outcomes through the same function, so a
419
+ // step promotes the SAME artifact whichever surface drove it.
420
+ const reduced = reduceStepOutcomes(plan, reducer, isFanOut, template.onError, units);
421
+ return {
422
+ ...reduced,
423
+ unitsDispatched: budget.used,
424
+ tokensUsed: budget.tokens,
425
+ };
426
+ }
427
+ async function runUnit(input) {
428
+ const { plan, workUnit, env, ctx, dispatcher } = input;
429
+ const unitId = workUnit.unitId;
430
+ // A per-unit expression resolution failure (missing param, bad `item.<path>`)
431
+ // is deterministic authoring/data breakage computed by the shared work-list:
432
+ // the unit fails WITHOUT dispatching — and without journaling a row, since no
433
+ // resolved input exists to hash.
434
+ if (!workUnit.resolved.ok) {
435
+ return { unitId, ok: false, failureReason: "expression_error", error: workUnit.resolved.error };
436
+ }
437
+ if (!workUnit.engine || !workUnit.invocation) {
438
+ return {
439
+ unitId,
440
+ ok: false,
441
+ failureReason: "dispatch_error",
442
+ error: `unit "${unitId}" has no frozen engine snapshot and cannot be dispatched`,
443
+ };
444
+ }
445
+ // The prompt (and therefore the input hash) was built once with the BASE
446
+ // unit id by computeStepWorkList: a retry re-dispatches the SAME input, the
447
+ // `~r<n>` suffix is journal bookkeeping only.
448
+ const { prompt, inputHash } = workUnit.resolved;
449
+ const sensitiveValues = collectWorkflowDispatchSensitiveValues(workUnit, env);
450
+ const request = {
451
+ runId: ctx.runId,
452
+ stepId: plan.stepId,
453
+ unitId,
454
+ nodeId: workUnit.nodeId,
455
+ prompt,
456
+ engine: workUnit.engine,
457
+ ...(workUnit.fallbackEngine ? { fallbackEngine: workUnit.fallbackEngine } : {}),
458
+ invocation: workUnit.invocation,
459
+ timeoutMs: workUnit.timeoutMs,
460
+ ...(workUnit.schema ? { schema: workUnit.schema } : {}),
461
+ ...(env ? { env } : {}),
462
+ ...(sensitiveValues.length > 0 ? { sensitiveValues } : {}),
463
+ ...(input.signal ? { signal: input.signal } : {}),
464
+ };
465
+ // Bounded retry: attempt 0 journals under the base
466
+ // journal id (`<unitId>`, or `<unitId>~l<loop>` in a gate loop — computed by
467
+ // the shared work-list), retry attempt N under `<baseId>~r<N>`. Every attempt
468
+ // keeps its own row. Retries only fire when the failure reason is in
469
+ // `retry.on`.
470
+ const retry = workUnit.retry;
471
+ const maxAttempts = 1 + Math.max(0, retry?.max ?? 0);
472
+ const gateLoop = ctx.gateLoop ?? 1;
473
+ const journalBaseId = workUnit.journalBaseId;
474
+ const attemptIdFor = (attempt) => (attempt === 0 ? journalBaseId : `${journalBaseId}~r${attempt}`);
475
+ // Durable-row reuse (shared classifyUnitReuse — the SAME decision
476
+ // executeStepPlan's preflight gate uses, so the gate can never disagree with
477
+ // what happens here). A completed row with the matching input hash IS the
478
+ // result: return it without touching rows, dispatching, or re-emitting events
479
+ // (a crash-resume must never double-issue work). A completed loop-1 row with
480
+ // a DIFFERENT hash is replay divergence (under a frozen plan the same
481
+ // content-derived identity must reproduce the same inputs — the journal was
482
+ // tampered with; executeStepPlan promotes this to a hard step failure
483
+ // regardless of on_error). Stale gate-loop rows, failed/running/missing rows,
484
+ // and pre-release R1 positional ids all fall through and dispatch live.
485
+ const reuse = classifyUnitReuse(workUnit, input.existingUnits, gateLoop);
486
+ if (reuse.kind === "reuse") {
487
+ // Identity in the durable step evidence is the CONTENT-derived base id, not
488
+ // the `~r<n>` attempt row it was reused from — the report surface reduces
489
+ // from the base id too, so both surfaces' evidence.units[].unitId agree.
490
+ return reuseCompletedUnit(unitId, reuse.row, workUnit.schema !== undefined);
491
+ }
492
+ if (reuse.kind === "diverge") {
493
+ return {
494
+ unitId,
495
+ ok: false,
496
+ failureReason: "replay_divergence",
497
+ error: `replay divergence: unit "${reuse.attemptId}" was journaled with different inputs ` +
498
+ `(journaled input_hash does not match this invocation's) — refusing to re-dispatch.`,
499
+ };
500
+ }
501
+ let outcome;
502
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
503
+ const attemptId = attemptIdFor(attempt);
504
+ // Lifetime cap + declared budget ceilings, consumed per ACTUAL dispatch
505
+ // (reuses above returned before reaching here). Refusal fails this unit
506
+ // without journaling a row — nothing was dispatched — and the sticky
507
+ // capMessage/budgetMessage fails the step.
508
+ if (!input.budget.tryConsume()) {
509
+ const budgetHit = input.budget.budgetMessage !== undefined;
510
+ return (outcome ?? {
511
+ unitId,
512
+ ok: false,
513
+ failureReason: budgetHit ? "budget_exceeded" : "unit_cap_exceeded",
514
+ error: input.budget.budgetMessage ?? input.budget.capMessage ?? "lifetime unit cap exceeded",
515
+ });
516
+ }
517
+ outcome = await dispatchJournaledAttempt({
518
+ plan,
519
+ workUnit,
520
+ ctx,
521
+ dispatcher,
522
+ request: { ...request, unitId: attemptId },
523
+ attemptId,
524
+ inputHash,
525
+ ...(input.worktreeBase !== undefined ? { worktreeBase: input.worktreeBase } : {}),
526
+ });
527
+ // The journal ROW keeps the `~r<n>`/`~l<loop>` attempt id (dispatchJournaledAttempt
528
+ // wrote it), but the returned outcome's identity in the DURABLE step evidence is
529
+ // the content-derived BASE id — the suffix is journal bookkeeping the report
530
+ // surface never sees, so leaking it into evidence.units would diverge the two
531
+ // surfaces (R4 parity, exposed once the conformance graph compares evidence.units).
532
+ outcome.unitId = unitId;
533
+ // Budget token accounting (addendum R2): every actual dispatch's reported
534
+ // usage counts against the run's max_tokens ceiling; crossing it aborts
535
+ // pending dispatches via the chained controller. Reuses never reach here
536
+ // (their tokens are already in the journal-seeded total).
537
+ if (outcome.tokens !== undefined)
538
+ input.budget.addTokens(outcome.tokens);
539
+ if (outcome.ok)
540
+ return outcome;
541
+ const reason = outcome.failureReason;
542
+ if (!retry || reason === undefined || !retry.on.includes(reason))
543
+ return outcome;
544
+ }
545
+ // maxAttempts >= 1, so outcome is always set by the loop above.
546
+ return outcome;
547
+ }
548
+ /** Journal one dispatch attempt: insert row, events, dispatch, finish row. */
549
+ async function dispatchJournaledAttempt(input) {
550
+ const { plan, workUnit, ctx, dispatcher, attemptId, inputHash } = input;
551
+ let request = input.request;
552
+ // Worktree isolation (addendum R2): a FRESH detached worktree per journaled
553
+ // attempt, minted before the row is inserted so worktree_path is journaled
554
+ // with the dispatch. A creation failure fails the unit WITHOUT journaling a
555
+ // row — nothing was dispatched (same contract as an expression failure).
556
+ let worktreePath;
557
+ if (input.worktreeBase !== undefined) {
558
+ const created = createUnitWorktree(input.worktreeBase, ctx.runId, attemptId);
559
+ if (!created.ok) {
560
+ return { unitId: request.unitId, ok: false, failureReason: "worktree_failed", error: created.error };
561
+ }
562
+ if (created.preservedLeftover !== undefined) {
563
+ // Never destroy a dirty (or unverifiable) leftover from a prior
564
+ // invocation of the same attempt — it was moved aside instead.
565
+ warn(`Workflow unit ${attemptId}: a previous attempt left uncollected work in its isolation worktree; ` +
566
+ `preserved at ${created.preservedLeftover}`);
567
+ }
568
+ worktreePath = created.path;
569
+ request = { ...request, cwd: worktreePath };
570
+ }
571
+ await enqueueUnitWrite(async () => {
572
+ await withWorkflowRunsRepo((repo) => repo.insertUnit({
573
+ runId: ctx.runId,
574
+ unitId: attemptId,
575
+ stepId: plan.stepId,
576
+ nodeId: workUnit.nodeId,
577
+ parentUnitId: workUnit.isFanOut ? `${plan.stepId}.map` : null,
578
+ phase: null,
579
+ runner: workUnit.runner,
580
+ engine: request.engine.name,
581
+ model: request.invocation.model,
582
+ inputHash,
583
+ worktreePath: worktreePath ?? null,
584
+ startedAt: new Date().toISOString(),
585
+ }));
586
+ });
587
+ // Ids/status only — instructions and results are workflow-authored content
588
+ // and stay out of the events stream (07 P1-B).
589
+ appendEvent({
590
+ eventType: "workflow_unit_started",
591
+ ref: ctx.workflowRef,
592
+ metadata: { runId: ctx.runId, stepId: plan.stepId, unitId: attemptId },
593
+ });
594
+ const outcome = redactUnitOutcome(await dispatchUnit(request, dispatcher), request.sensitiveValues ?? []);
595
+ const finishedAt = new Date().toISOString();
596
+ await enqueueUnitWrite(() => withWorkflowRunsRepo((repo) => repo.immediateTransaction((db) => {
597
+ const run = repo.getRunById(ctx.runId);
598
+ if (run?.status !== "active")
599
+ return;
600
+ if (ctx.leaseHolder !== undefined && run.engine_lease_holder !== ctx.leaseHolder)
601
+ return;
602
+ repo.finishUnit({
603
+ runId: ctx.runId,
604
+ unitId: attemptId,
605
+ status: outcome.ok ? "completed" : "failed",
606
+ resultJson: outcome.result !== undefined
607
+ ? JSON.stringify(outcome.result)
608
+ : outcome.text
609
+ ? JSON.stringify(outcome.text)
610
+ : null,
611
+ tokens: outcome.tokens ?? null,
612
+ failureReason: outcome.failureReason ?? null,
613
+ // Harness-native session id (P2): journaled so resume can replay the
614
+ // harness's own context cache (e.g. `codex exec resume <id>`).
615
+ sessionId: outcome.sessionId ?? null,
616
+ finishedAt,
617
+ });
618
+ insertEventStrict(db, {
619
+ eventType: "workflow_unit_finished",
620
+ ts: finishedAt,
621
+ ref: ctx.workflowRef,
622
+ metadata: {
623
+ runId: ctx.runId,
624
+ stepId: plan.stepId,
625
+ unitId: attemptId,
626
+ status: outcome.ok ? "completed" : "failed",
627
+ ...(outcome.failureReason ? { failureReason: outcome.failureReason } : {}),
628
+ ...(outcome.tokens !== undefined ? { tokens: outcome.tokens } : {}),
629
+ },
630
+ });
631
+ })));
632
+ // Worktree lifecycle epilogue: a CLEAN worktree (`git status --porcelain`
633
+ // empty) is removed; a DIRTY one is retained and logged — the unit left
634
+ // uncollected work, and its journaled worktree_path says where. Cleanup is
635
+ // best-effort observability, never a unit failure.
636
+ if (worktreePath !== undefined && input.worktreeBase !== undefined) {
637
+ const cleanup = cleanupUnitWorktree(input.worktreeBase, worktreePath);
638
+ if (cleanup.dirty) {
639
+ warn(`Workflow unit ${attemptId} left uncommitted changes in its isolation worktree; retained at ${worktreePath}`);
640
+ }
641
+ else if (!cleanup.removed) {
642
+ warn(`Workflow unit ${attemptId}: could not clean up isolation worktree ${worktreePath}: ${cleanup.error}`);
643
+ }
644
+ }
645
+ return outcome;
646
+ }
647
+ /** Transport failures surface as this sentinel so runStructured doesn't retry them. */
648
+ class UnitTransportError extends Error {
649
+ result;
650
+ constructor(result) {
651
+ super(result.error ?? "unit dispatch failed");
652
+ this.result = result;
653
+ this.name = "UnitTransportError";
654
+ }
655
+ }
656
+ async function dispatchUnit(request, dispatcher) {
657
+ let tokens = 0;
658
+ let sawUsage = false;
659
+ // Harness-native session id revealed by dispatch (P2). Captured across
660
+ // structured-output retries (last one wins) so it survives into the
661
+ // UnitOutcome and gets journaled on the unit row by finishUnit — the seam's
662
+ // contract ("stored opportunistically on the unit row for resume").
663
+ let sessionId;
664
+ const dispatchOnce = async (feedback) => {
665
+ const result = await dispatcher(request, feedback);
666
+ if (result.usage) {
667
+ sawUsage = true;
668
+ tokens +=
669
+ (result.usage.inputTokens ?? 0) + (result.usage.outputTokens ?? 0) + (result.usage.reasoningTokens ?? 0);
670
+ }
671
+ // Capture before the ok-check: a failed attempt can still have configured
672
+ // a session (e.g. codex `session_configured` then a tool crash).
673
+ if (result.sessionId !== undefined)
674
+ sessionId = result.sessionId;
675
+ if (!result.ok)
676
+ throw new UnitTransportError(result);
677
+ return result.text;
678
+ };
679
+ const captured = () => ({
680
+ ...(sawUsage ? { tokens } : {}),
681
+ ...(sessionId !== undefined ? { sessionId } : {}),
682
+ });
683
+ try {
684
+ if (request.schema) {
685
+ const schema = request.schema;
686
+ const structured = await runStructured({
687
+ dispatch: dispatchOnce,
688
+ validate: (candidate) => {
689
+ const errors = validateJsonSchemaSubset(candidate, schema);
690
+ return errors.length === 0 ? { ok: true, value: candidate } : { ok: false, errors };
691
+ },
692
+ });
693
+ if (structured.ok) {
694
+ return { unitId: request.unitId, ok: true, result: structured.value, ...captured() };
695
+ }
696
+ return {
697
+ unitId: request.unitId,
698
+ ok: false,
699
+ failureReason: structured.reason,
700
+ error: structured.errors.join("; "),
701
+ text: structured.raw,
702
+ ...captured(),
703
+ };
704
+ }
705
+ const text = await dispatchOnce();
706
+ // Normalize an EMPTY successful output to "no text". `finishUnit` journals
707
+ // result_json = NULL for a falsy text, so durable-reuse and the R3 report
708
+ // surface both rehydrate NO text from the row (unitOutcomeFromRow). Preserving
709
+ // `text: ""` only in this live outcome would make the LIVE step artifact ("")
710
+ // diverge from the resume/report artifact (null) — the exact byte-identical-
711
+ // graph violation the cardinal rule forbids. Treating empty as absent keeps
712
+ // the live engine, engine resume, and report surfaces identical.
713
+ return { unitId: request.unitId, ok: true, ...(text ? { text } : {}), ...captured() };
714
+ }
715
+ catch (err) {
716
+ if (err instanceof UnitTransportError) {
717
+ return {
718
+ unitId: request.unitId,
719
+ ok: false,
720
+ failureReason: err.result.failureReason ?? "dispatch_error",
721
+ error: err.result.error ?? "unit dispatch failed",
722
+ text: err.result.text,
723
+ ...captured(),
724
+ };
725
+ }
726
+ return {
727
+ unitId: request.unitId,
728
+ ok: false,
729
+ failureReason: "dispatch_error",
730
+ error: message(err),
731
+ ...captured(),
732
+ };
733
+ }
734
+ }
735
+ // ── Env bindings ─────────────────────────────────────────────────────────────
736
+ /**
737
+ * Resolve every unit `env` ref through the extracted `akm env run` core
738
+ * (loadEnv + secret tokens + dangerous-key policy + keys-only audit event).
739
+ * Lazily imported so the engine has no env/secret dependency until a
740
+ * workflow actually declares bindings.
741
+ */
742
+ async function resolveEnvBindings(refs) {
743
+ const { resolveEnvBinding } = await import("../../commands/env/env-binding.js");
744
+ const merged = {};
745
+ for (const ref of refs) {
746
+ Object.assign(merged, resolveEnvBinding(ref).values);
747
+ }
748
+ return merged;
749
+ }
750
+ // ── Default dispatcher (production substrate) ───────────────────────────────
751
+ /**
752
+ * Dispatch through akm's existing execution substrate:
753
+ * llm → `chatCompletion` on the profile/default LLM connection
754
+ * agent → `executeRunner` → `runAgent` (per-harness AgentCommandBuilder)
755
+ * sdk → `executeRunner` → `runOpencodeSdk`
756
+ *
757
+ * Every v3 invocation names a frozen engine; no live profile/default fallback
758
+ * is consulted during dispatch.
759
+ */
760
+ /**
761
+ * Build the platform-agnostic {@link import("../../integrations/agent/builder-shared.js").AgentDispatchRequest}
762
+ * for an agent (CLI) unit from the resolved dispatch request and its final
763
+ * (feedback-augmented) prompt.
764
+ *
765
+ * Threading the unit's output `schema` here is what activates each harness's
766
+ * native structured-output path (plan §"Structured-output normalization"):
767
+ * - Codex (native-schema tier) writes it to a temp file and passes
768
+ * `--output-schema <file>`.
769
+ * - Copilot / Gemini switch stdout to their documented JSON envelope
770
+ * (`--output-format json`) and append their schema-aware prompt directive.
771
+ * - Pi switches to its JSONL event stream (`--mode json`) and appends its
772
+ * directive.
773
+ * Without the schema the argv is byte-identical to the pre-fix plain-prompt
774
+ * shape. The engine's post-hoc `runStructured` validation runs regardless — the
775
+ * harness path constrains/hints, the engine still verifies (constrained output
776
+ * is trusted but verified). The `model` is passed raw so the builder resolves
777
+ * aliases per-harness. Only `prompt` (with any gate feedback already folded in),
778
+ * `model`, and `schema` are engine-derived; `systemPrompt`/`tools`/`cwd` come
779
+ * from the profile/asset, not the workflow unit.
780
+ */
781
+ export function buildAgentDispatchRequest(request, prompt) {
782
+ return {
783
+ prompt,
784
+ ...(request.invocation.model ? { model: request.invocation.model } : {}),
785
+ ...(request.invocation.model ? { modelIsExact: true } : {}),
786
+ ...(request.schema ? { schema: request.schema } : {}),
787
+ };
788
+ }
789
+ export const defaultUnitDispatcher = async (request, feedback) => {
790
+ const prompt = feedback ? `${request.prompt}\n\n${feedback}` : request.prompt;
791
+ const resolved = frozenUnitRunner(request);
792
+ // `env` bindings can only reach a child process. The agent (CLI) runner
793
+ // spawns one per call, and the sdk runner now injects them for real via the
794
+ // env-keyed opencode server registry (sdk-runner.ts module doc, open seam
795
+ // decision 1 resolved in R2) — but the llm runner has no child at all, so
796
+ // it still fails loudly: an audit event claiming an injection that never
797
+ // reached the unit would be a lie.
798
+ if (request.env && Object.keys(request.env).length > 0 && resolved.kind === "llm") {
799
+ return {
800
+ ok: false,
801
+ text: "",
802
+ failureReason: "env_unsupported",
803
+ error: `unit "${request.unitId}" declares env bindings, which require a child process (agent or sdk runner) — ` +
804
+ `the "llm" runner cannot inject a per-unit child environment.`,
805
+ };
806
+ }
807
+ // Same shape for worktree isolation resolved onto llm through `inherit`:
808
+ // the executor already rejects an EXPLICIT llm+isolation pairing before
809
+ // dispatch, but an inherit unit only reveals its runner here.
810
+ if (request.cwd && resolved.kind === "llm") {
811
+ return {
812
+ ok: false,
813
+ text: "",
814
+ failureReason: "isolation_unsupported",
815
+ error: `unit "${request.unitId}" declares isolation: worktree but resolved to the "llm" runner, ` +
816
+ `which has no working directory to isolate. Use the agent or sdk runner for isolated units.`,
817
+ };
818
+ }
819
+ if (resolved.kind === "llm") {
820
+ const { chatCompletion, LlmCallError } = await import("../../llm/client.js");
821
+ const connection = resolved.connection;
822
+ try {
823
+ const text = await chatCompletion(connection, [{ role: "user", content: prompt }], {
824
+ timeoutMs: request.timeoutMs,
825
+ ...(request.signal ? { signal: request.signal } : {}),
826
+ // Native structured output where the connection supports it; the
827
+ // executor's subset validator still runs downstream either way.
828
+ ...(request.schema ? { responseSchema: request.schema } : {}),
829
+ });
830
+ return { ok: true, text };
831
+ }
832
+ catch (err) {
833
+ // Map typed LlmCallError codes into the persisted AgentFailureReason
834
+ // taxonomy — the vocabulary `retry.on` is validated against (program
835
+ // schema PROGRAM_RETRY_REASONS) and the journal's failure_reason column
836
+ // speaks. A collapsed out-of-taxonomy value ("llm_error") made the
837
+ // declared failure policy dead for the entire llm runner.
838
+ const failureReason = err instanceof LlmCallError ? llmFailureReasonFor(err.code) : "dispatch_error";
839
+ return { ok: false, text: "", failureReason, error: message(err) };
840
+ }
841
+ }
842
+ const { executeRunner } = await import("../../integrations/agent/runner-dispatch.js");
843
+ const profile = request.invocation.model
844
+ ? { ...resolved.profile, model: request.invocation.model, modelIsExact: true }
845
+ : resolved.profile;
846
+ const result = await executeRunner(resolved.kind === "sdk"
847
+ ? {
848
+ kind: "sdk",
849
+ profile,
850
+ ...(resolved.fallbackConnection ? { fallbackConnection: resolved.fallbackConnection } : {}),
851
+ }
852
+ : { kind: "agent", profile }, prompt, {
853
+ stdio: "captured",
854
+ parseOutput: "text",
855
+ timeoutMs: request.timeoutMs,
856
+ ...(request.env ? { env: request.env } : {}),
857
+ // Worktree isolation: the unit's fresh checkout is the child's cwd —
858
+ // runAgent spawns there; the sdk runner scopes the session to it.
859
+ ...(request.cwd ? { cwd: request.cwd } : {}),
860
+ ...(request.signal ? { signal: request.signal } : {}),
861
+ // Route CLI dispatch through the platform AgentCommandBuilder so model
862
+ // aliases resolve per-harness (P0.5 model routing) AND the unit's output
863
+ // schema reaches the harness's structured-output path (see
864
+ // buildAgentDispatchRequest).
865
+ ...(resolved.kind === "agent" ? { dispatch: buildAgentDispatchRequest(request, prompt) } : {}),
866
+ });
867
+ // Harness result extraction (P2, plan §"The adapter contract" step 3):
868
+ // when the profile's harness declares a `resultExtractor`, normalize the
869
+ // raw stdout into the final answer (+ opportunistic session id) BEFORE the
870
+ // engine's schema validation / retry loop sees it. Only successful agent
871
+ // (CLI) runs are normalized — failures keep the raw stdout for diagnostics,
872
+ // and the default path is byte-identical when no extractor is registered.
873
+ let text = result.stdout;
874
+ let sessionId = result.sessionId;
875
+ if (resolved.kind === "agent" && result.ok) {
876
+ const extractor = await resolveHarnessExtractor(resolved.profile);
877
+ if (extractor) {
878
+ const extraction = extractor(result);
879
+ text = extraction.text;
880
+ if (extraction.sessionId !== undefined)
881
+ sessionId = extraction.sessionId;
882
+ }
883
+ }
884
+ return {
885
+ ok: result.ok,
886
+ text,
887
+ ...(sessionId !== undefined ? { sessionId } : {}),
888
+ ...(result.reason ? { failureReason: result.reason } : {}),
889
+ ...(result.error ? { error: result.error } : {}),
890
+ ...(result.usage ? { usage: result.usage } : {}),
891
+ };
892
+ };
893
+ function collectWorkflowDispatchSensitiveValues(workUnit, env) {
894
+ const values = new Set(Object.values(env ?? {}));
895
+ const addCredential = (engine) => {
896
+ if (!engine)
897
+ return;
898
+ if (engine.kind === "llm") {
899
+ for (const name of engine.credential?.names ?? []) {
900
+ const value = process.env[name]?.trim();
901
+ if (value)
902
+ values.add(value);
903
+ }
904
+ return;
905
+ }
906
+ for (const name of engine.envPassthrough) {
907
+ const value = process.env[name];
908
+ if (!isEnvPassthroughValueSafeToExpose(name, value) && value)
909
+ values.add(value);
910
+ }
911
+ };
912
+ addCredential(workUnit.engine);
913
+ addCredential(workUnit.fallbackEngine);
914
+ return collectSensitiveValues(values);
915
+ }
916
+ function redactUnitOutcome(outcome, sensitiveValues) {
917
+ const redacted = redactSensitiveValue(outcome, sensitiveValues);
918
+ if (outcome.failureReason !== undefined && redacted.failureReason !== outcome.failureReason) {
919
+ redacted.failureReason = "reported_failure";
920
+ }
921
+ return redacted;
922
+ }
923
+ /**
924
+ * Map a typed {@link import("../../llm/client.js").LlmCallErrorCode} into the
925
+ * persisted `AgentFailureReason` taxonomy (agent/spawn.ts) — the ONLY
926
+ * vocabulary `retry.on` accepts and the journal's `failure_reason` column
927
+ * carries. Exhaustive over the code union (typecheck fails on drift):
928
+ *
929
+ * - `timeout` → `timeout` (wall-clock expiry)
930
+ * - `aborted` → `aborted` (caller/budget cancellation)
931
+ * - `rate_limited` → `llm_rate_limit` (HTTP 429 — the canonical transient)
932
+ * - `parse_error` / `provider_html_error`
933
+ * → `parse_error` (a response arrived but was not the
934
+ * promised JSON)
935
+ * - `network_error` / `provider_error`
936
+ * → `spawn_failed` (the backend could not be reached or
937
+ * could not do the work — the LLM
938
+ * analog of failing to start the
939
+ * child; retryable as a transient)
940
+ */
941
+ export function llmFailureReasonFor(code) {
942
+ switch (code) {
943
+ case "aborted":
944
+ return "aborted";
945
+ case "timeout":
946
+ return "timeout";
947
+ case "rate_limited":
948
+ return "llm_rate_limit";
949
+ case "parse_error":
950
+ case "provider_html_error":
951
+ return "parse_error";
952
+ case "network_error":
953
+ case "provider_error":
954
+ return "spawn_failed";
955
+ }
956
+ }
957
+ /**
958
+ * Resolve the harness `resultExtractor` from the canonical platform frozen
959
+ * from the named engine. Unknown platforms pass raw stdout through unchanged.
960
+ */
961
+ async function resolveHarnessExtractor(profile) {
962
+ const { getHarness } = await import("../../integrations/harnesses/index.js");
963
+ const harness = getHarness(profile.platform ?? profile.name);
964
+ return harness?.resultExtractor;
965
+ }
966
+ /** Reconstruct the existing RunnerSpec substrate from the frozen allowlist only. */
967
+ function frozenUnitRunner(request) {
968
+ const snapshot = request.engine;
969
+ if (snapshot.kind === "llm") {
970
+ return { kind: "llm", connection: materializeFrozenLlm(snapshot, request.invocation) };
971
+ }
972
+ const profile = {
973
+ name: snapshot.name,
974
+ platform: snapshot.platform,
975
+ bin: snapshot.bin,
976
+ args: snapshot.args,
977
+ stdio: "captured",
978
+ envPassthrough: snapshot.envPassthrough,
979
+ parseOutput: "text",
980
+ ...(snapshot.workspace ? { workspace: snapshot.workspace } : {}),
981
+ ...(request.invocation?.model ? { model: request.invocation.model } : {}),
982
+ ...(request.invocation?.model ? { modelIsExact: true } : {}),
983
+ };
984
+ if (snapshot.runnerKind === "agent")
985
+ return { kind: "agent", profile };
986
+ // The catalog is supplied transitively by the work-list only for hashing; the
987
+ // SDK runner receives a frozen fallback copied into the request by its caller.
988
+ const fallback = request.fallbackEngine ? materializeFrozenLlm(request.fallbackEngine, undefined) : undefined;
989
+ return { kind: "sdk", profile, ...(fallback ? { fallbackConnection: fallback } : {}) };
990
+ }
991
+ function materializeFrozenLlm(snapshot, invocation) {
992
+ let apiKey;
993
+ for (const name of snapshot.credential?.names ?? []) {
994
+ const candidate = process.env[name]?.trim();
995
+ if (candidate) {
996
+ apiKey = candidate;
997
+ break;
998
+ }
999
+ }
1000
+ if (snapshot.credential?.required && !apiKey)
1001
+ throw new ConfigError(`Required engine credential ${snapshot.credential.names[0]} is not set.`, "INVALID_CONFIG_FILE");
1002
+ const base = {
1003
+ provider: snapshot.provider,
1004
+ endpoint: snapshot.endpoint,
1005
+ model: invocation?.model ?? snapshot.model,
1006
+ ...(snapshot.temperature !== undefined ? { temperature: snapshot.temperature } : {}),
1007
+ ...(snapshot.maxTokens !== undefined ? { maxTokens: snapshot.maxTokens } : {}),
1008
+ ...(snapshot.supportsJsonSchema !== undefined ? { supportsJsonSchema: snapshot.supportsJsonSchema } : {}),
1009
+ ...(snapshot.extraParams ? { extraParams: snapshot.extraParams } : {}),
1010
+ ...(snapshot.contextLength !== undefined ? { contextLength: snapshot.contextLength } : {}),
1011
+ ...(snapshot.enableThinking !== undefined ? { enableThinking: snapshot.enableThinking } : {}),
1012
+ ...(apiKey ? { apiKey } : {}),
1013
+ };
1014
+ return invocation?.llm ? deepMergeConfig(base, invocation.llm) : base;
1015
+ }
1016
+ // ── Small helpers ────────────────────────────────────────────────────────────
1017
+ /**
1018
+ * Rehydrate a journaled completed unit row into a UnitOutcome (durable-row
1019
+ * reuse). Delegates to the shared {@link unitOutcomeFromRow} — the reuse path
1020
+ * only reaches here for completed rows (the caller guards `status ===
1021
+ * "completed"`), so the mapping is identical to what the R3 report path applies
1022
+ * when it replays the same journal.
1023
+ */
1024
+ function reuseCompletedUnit(unitId, row, hasSchema) {
1025
+ return unitOutcomeFromRow(unitId, row, hasSchema);
1026
+ }
1027
+ function failedStep(dispatched, reason) {
1028
+ return {
1029
+ ok: false,
1030
+ units: [],
1031
+ evidence: { error: reason },
1032
+ summary: reason,
1033
+ unitsDispatched: dispatched,
1034
+ };
1035
+ }
1036
+ function message(err) {
1037
+ return err instanceof Error ? err.message : String(err);
1038
+ }