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,35 +2,185 @@
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
- * Current task-source router.
5
+ * The task source version router.
6
6
  *
7
- * Runtime code accepts only the current v4 grammar. Historical v2/v3 files
8
- * and the former source-owned `schedule[].enabled` field are handled only by
9
- * the explicit `akm migrate` executable; execution never translates legacy
10
- * bytes in memory.
7
+ * Runs the bounded YAML front end ONCE (`readBoundedTaskSourceYaml`), reads
8
+ * `root.version`, and either dispatches into `parseTaskSourceV4Document` or
9
+ * routes through the in-memory read shim below — no second parse of the v4
10
+ * grammar itself, no re-serialization back to disk, no synthetic document
11
+ * (the shim adds a pure bytes-in/bytes-out detour, never a disk write).
12
+ *
13
+ * The terminal routing table:
14
+ *
15
+ * | root `version` | outcome |
16
+ * |------------------------|-----------------------------------------------------------------|
17
+ * | `4`, no retired `schedule[].enabled` | `parseTaskSourceV4Document` — the current grammar |
18
+ * | `4`, some `schedule[]` entry carries `enabled` | in-memory read shim (below): the SAME `planTaskToV4File` call `akm migrate apply` uses for this exact case (`./task-to-v4.ts`'s `version === 4` branch, which strips every `schedule[].enabled` and reports `source-enablement-removed`) runs on the bytes already in hand; the result is parsed and returned with a one-line stderr deprecation warning (once per file per process). The value is never read either way — `enabled: false` cannot suppress a granted task and `enabled: true` cannot schedule an ungranted one, since activation is host-local `scheduler.enabled`. Version 4 is supported, so if the planner cannot produce a valid document it is an ordinary grammar defect: falls back to `TASK_SOURCE_INVALID` with the v4 parser's own error, not the unmigratable-version decision |
19
+ * | `2` or `3` | in-memory read shim (below): the SAME pure planners `akm migrate apply` uses (`./task-to-v3.ts`, `./task-to-v4.ts`) convert the bytes already in hand to v4 in memory; the result is parsed and returned with a one-line stderr deprecation warning (once per file per process). If the deterministic conversion itself fails (an unmigratable shape — the file needs a human decision, not a re-run), falls back to `TASK_SCHEMA_VERSION_UNSUPPORTED` naming the specific blocked reason — the shim removes friction for the deterministic case, it never hides a real problem |
20
+ * | any other number | `TASK_SCHEMA_VERSION_UNSUPPORTED`, naming the migrator |
21
+ * | absent / not a number | `parseTaskSourceV4Document` — its own `TASK_SOURCE_INVALID` "version is required and must be 4" / "must be exactly 4" wording |
22
+ *
23
+ * A missing or non-numeric `version:` is a MALFORMED v4 document, not a
24
+ * legacy one — it routes into the v4 parser so the field error names the
25
+ * one grammar `src` still accepts, rather than a generic "unsupported"
26
+ * message that would send the user to the migrator for a document that was
27
+ * never task v2 or v3 in the first place.
28
+ *
29
+ * task v2 and task v3 sources are no longer read as their own standing
30
+ * grammar anywhere else in `src` — the only readers of that grammar are the
31
+ * pure, byte-producing planners (`./task-to-v3.ts`, `./task-to-v4.ts`, and
32
+ * the frozen v3 reader `./task-source-v3-frozen.ts`), reached either through
33
+ * this shim (bytes in, bytes out, never touches disk) or through
34
+ * `akm migrate apply` / `akm-migrate` (`scripts/akm-migrate`, which
35
+ * additionally rewrites the file on disk once the user asks for that).
36
+ * Policy: a deterministic byte transform is the tool's job, not the user's —
37
+ * upgrading past a schema bump must not silently break a scheduled task, so
38
+ * v2/v3 files keep reading successfully at the cost of a one-line
39
+ * deprecation warning, and `akm migrate apply` remains available to rewrite
40
+ * the file and silence it. Activation itself is host-local (scheduler
41
+ * config, `src/core/activation-policy.ts`) and this shim never touches it:
42
+ * the v3->v4 planner never hoists the retired `akm.enabled` field to v4's
43
+ * top level and never carries a schedule entry's `enabled` key, so the
44
+ * document this shim hands back carries no enablement at all — the same
45
+ * shape a native v4 document has. A declared `version: 4` document that
46
+ * still carries a `schedule[].enabled` key (0.9.15's v4 grammar accepted
47
+ * it; this release's does not) gets the identical treatment: routed
48
+ * through `planTaskToV4File`'s own `version === 4` branch, which strips
49
+ * every `schedule[].enabled` key without reading its value and reports
50
+ * `source-enablement-removed`, then re-parsed and returned with the same
51
+ * one-line deprecation warning. The front end's own pre-version failures
52
+ * (source not a string, source too large, YAML parse/warning/expansion)
53
+ * render with the label `task source`.
11
54
  */
12
55
  import { UsageError } from "../../core/errors.js";
56
+ import { warnOnce } from "../../core/warn.js";
13
57
  import { readBoundedTaskSourceYaml } from "./bounded-document.js";
14
- import { parseTaskSourceV4Document, TASK_SOURCE_V4_VERSION } from "./task-source-v4.js";
58
+ import { parseTaskSourceV4, parseTaskSourceV4Document, TASK_SOURCE_V4_VERSION, } from "./task-source-v4.js";
59
+ import { planTaskToV3File } from "./task-to-v3.js";
60
+ import { planTaskToV4File } from "./task-to-v4.js";
61
+ /** Read the root `version` field without over-accepting non-number values (e.g. the string `"4"`). */
15
62
  export function peekTaskSourceVersion(root) {
16
63
  if (root === null || typeof root !== "object" || Array.isArray(root))
17
64
  return undefined;
18
65
  const value = root.version;
19
66
  return typeof value === "number" ? value : undefined;
20
67
  }
68
+ const TASK_MIGRATE_HINT = "Run `akm migrate apply --dry-run` to preview the task-v3 to task-source-v4 conversion, then run `akm migrate apply`.";
69
+ /**
70
+ * Thrown only when the deterministic conversion itself could not produce a
71
+ * task source v4 document — a case where a person must decide the intended
72
+ * behavior (e.g. an ambiguous shell command), not one the migrator can just
73
+ * be re-run to fix. `reason`/`detail` are the SAME blocked outcome
74
+ * `akm migrate status`/`apply` reports for this file, so the message names
75
+ * the actual decision instead of pointing at a command that will report the
76
+ * identical block.
77
+ */
78
+ function unmigratableVersionError(filePath, version, reason, detail) {
79
+ return new UsageError(`TASK_SCHEMA_VERSION_UNSUPPORTED: Task at ${filePath} uses task schema version ${version} and needs a human decision before it can run — the deterministic migrator cannot convert it automatically (${reason}${detail ? `: ${detail}` : ""}).`, "TASK_SCHEMA_VERSION_UNSUPPORTED", "Review the file and resolve the ambiguity by hand, then it will convert normally; `akm migrate status` reports the same reason.");
80
+ }
21
81
  function unsupportedVersionError(filePath, version) {
22
- return new UsageError(`TASK_SCHEMA_VERSION_UNSUPPORTED: Task at ${filePath} uses task schema version ${version}, which this release does not accept.`, "TASK_SCHEMA_VERSION_UNSUPPORTED", "Run `akm migrate apply --dry-run`, review the plan, then run `akm migrate apply`.");
82
+ return new UsageError(`TASK_SCHEMA_VERSION_UNSUPPORTED: Task at ${filePath} uses task schema version ${version}, which this release does not accept.`, "TASK_SCHEMA_VERSION_UNSUPPORTED", TASK_MIGRATE_HINT);
23
83
  }
84
+ /**
85
+ * Plan the SAME bytes already in hand through the pure v3->v4 (and, for v2,
86
+ * chained v2->v3->v4) migration planner(s) — never touches disk, never
87
+ * writes the file, never re-reads it from disk. `version === 4` runs the
88
+ * identical bytes straight through `planTaskToV4File`'s own `version === 4`
89
+ * branch instead (the retired `schedule[].enabled` case, `./task-to-v4.ts`),
90
+ * so there is exactly one helper for every version this shim reads, not a
91
+ * second one for the v4-only case. Returns the produced v4 YAML text, or the
92
+ * blocked reason/detail when the deterministic conversion cannot proceed
93
+ * (an unmigratable v2/v3 shape, or a v4 document `planTaskToV4File` cannot
94
+ * revalidate once `schedule[].enabled` is stripped) — the caller falls back
95
+ * to the same hard error this gate threw before the shim existed, now
96
+ * naming that reason.
97
+ */
98
+ function planInMemoryV4Bytes(version, yaml, filePath, workspaceRoot) {
99
+ const bytes = Buffer.from(yaml, "utf8");
100
+ // `writable`/`onDiskWritable` gate the DISK apply path's "don't touch a
101
+ // read-only file" check inside the planners; this shim never writes
102
+ // anything to disk, so that check does not apply here and must not block
103
+ // an otherwise-legal read of a task file that happens to be read-only.
104
+ const baseInput = {
105
+ filePath,
106
+ bytes,
107
+ mode: 0o644,
108
+ writable: true,
109
+ onDiskWritable: true,
110
+ ...(workspaceRoot ? { containmentRoot: workspaceRoot } : {}),
111
+ };
112
+ let v3Bytes;
113
+ if (version === 3 || version === 4) {
114
+ v3Bytes = bytes;
115
+ }
116
+ else {
117
+ const v3Outcome = planTaskToV3File(baseInput);
118
+ if (v3Outcome.status !== "changed")
119
+ return { reason: v3Outcome.reason, detail: v3Outcome.detail };
120
+ v3Bytes = v3Outcome.after;
121
+ }
122
+ const v4Outcome = planTaskToV4File({ ...baseInput, bytes: v3Bytes });
123
+ if (v4Outcome.status !== "changed")
124
+ return { reason: v4Outcome.reason, detail: v4Outcome.detail };
125
+ return v4Outcome.after.toString("utf8");
126
+ }
127
+ /**
128
+ * True when `root`'s `schedule:` is a sequence with at least one mapping
129
+ * entry that carries an `enabled` key, regardless of that key's value or
130
+ * type — the shape 0.9.15's v4 grammar accepted and this release's does
131
+ * not (`schedule[].enabled`). The value is never inspected: activation is
132
+ * host-local `scheduler.enabled`, so this is purely a presence check that
133
+ * decides whether to route through the in-memory shim.
134
+ */
135
+ export function v4ScheduleHasRetiredEnabledKey(root) {
136
+ if (root === null || typeof root !== "object" || Array.isArray(root))
137
+ return false;
138
+ const schedule = root.schedule;
139
+ if (!Array.isArray(schedule))
140
+ return false;
141
+ return schedule.some((entry) => entry !== null && typeof entry === "object" && !Array.isArray(entry) && Object.hasOwn(entry, "enabled"));
142
+ }
143
+ /** Parse task source YAML, routing per the terminal table above. */
24
144
  export function parseTaskSource(input) {
25
145
  const { root, lineAt } = readBoundedTaskSourceYaml(input, { sourceLabel: "task source" });
26
146
  const version = peekTaskSourceVersion(root);
27
147
  if (version !== undefined && version !== TASK_SOURCE_V4_VERSION) {
148
+ if (version === 2 || version === 3) {
149
+ const shimmed = planInMemoryV4Bytes(version, input.yaml, input.filePath, input.workspaceRoot);
150
+ if (typeof shimmed === "string") {
151
+ const v4 = parseTaskSourceV4({
152
+ yaml: shimmed,
153
+ filePath: input.filePath,
154
+ ...(input.workspaceRoot ? { workspaceRoot: input.workspaceRoot } : {}),
155
+ });
156
+ warnOnce(`task-source:v${version}-shim:${input.filePath}`, `akm: task ${input.filePath} uses schema v${version} — auto-read as v4; run \`akm migrate apply\` to rewrite it and silence this`);
157
+ return Object.freeze({ version: 4, v4 });
158
+ }
159
+ throw unmigratableVersionError(input.filePath, version, shimmed.reason, shimmed.detail);
160
+ }
28
161
  throw unsupportedVersionError(input.filePath, version);
29
162
  }
30
- const v4 = parseTaskSourceV4Document(root, {
163
+ if (version === TASK_SOURCE_V4_VERSION && v4ScheduleHasRetiredEnabledKey(root)) {
164
+ const shimmed = planInMemoryV4Bytes(4, input.yaml, input.filePath, input.workspaceRoot);
165
+ if (typeof shimmed === "string") {
166
+ const v4 = parseTaskSourceV4({
167
+ yaml: shimmed,
168
+ filePath: input.filePath,
169
+ ...(input.workspaceRoot ? { workspaceRoot: input.workspaceRoot } : {}),
170
+ });
171
+ warnOnce(`task-source:v4-schedule-enabled-shim:${input.filePath}`, `akm: task ${input.filePath} uses the retired schedule[].enabled field — auto-read with it ignored; run \`akm migrate apply\` to rewrite it and silence this`);
172
+ return Object.freeze({ version: 4, v4 });
173
+ }
174
+ // Version 4 is supported; a blocked outcome here means the file itself
175
+ // is malformed (`generated-v4-validation-failed`), not that this
176
+ // version needs a human decision — throw the v4 grammar error, not
177
+ // `unmigratableVersionError`.
178
+ throw new UsageError(shimmed.detail ?? shimmed.reason, "TASK_SOURCE_INVALID");
179
+ }
180
+ const documentOptions = {
31
181
  filePath: input.filePath,
32
182
  ...(input.workspaceRoot ? { workspaceRoot: input.workspaceRoot } : {}),
33
183
  lineAt,
34
- });
35
- return Object.freeze({ version: 4, v4 });
184
+ };
185
+ return Object.freeze({ version: 4, v4: parseTaskSourceV4Document(root, documentOptions) });
36
186
  }
@@ -312,10 +312,9 @@ function parseTaskV3TriggerFields(input, ctx) {
312
312
  * the original file this was moved body-intact from, and because
313
313
  * `parseTaskV3Document` shares its trigger-parsing helpers
314
314
  * (`parseTaskV3TriggerFields`/`compileTriggers`/`parseOn`/`parseAkm`) with
315
- * it. The LIVE, `src`-side classifier this was re-homed from is
316
- * `classifyWorkflowYamlTriggers` (`src/workflows/source-ir/triggers.ts`,
317
- * P4-N3) — that one, not this frozen copy, is what
318
- * `src/workflows/source-ir/compile.ts` injects.
315
+ * it. The LIVE, `src`-side `on:` parser is `parseTriggers`
316
+ * (`src/workflows/github-yaml.ts`, P4-N3) — that one, not this frozen copy,
317
+ * is what workflow compilation uses.
319
318
  */
320
319
  export function classifyTaskV3Triggers(value, options) {
321
320
  const ctx = ctxFrom(options);
@@ -318,8 +318,8 @@ function planV3DataToV4(input, data) {
318
318
  // `uses: workflows/` it was equally inert there — nothing ever consumed
319
319
  // it. Hoisting it unconditionally would therefore emit bytes the real
320
320
  // parseTaskSourceV4 below rejects, blocking a valid, previously-runnable
321
- // v3 file — and one blocked file aborts the whole plan
322
- // (`applyTaskToV4MigrationPlan`, ./task-files-to-v4.ts). Dropping an
321
+ // v3 file from both the in-memory read shim and `akm migrate apply`
322
+ // (scripts/akm-migrate/migrate/task-files.ts). Dropping an
323
323
  // already-inert field and SAYING SO is the faithful translation, and
324
324
  // keeps spec row B-66 / §5.3's `changed` guarantee intact.
325
325
  if (usesTarget !== undefined && (usesTarget.kind === "command" || usesTarget.kind === "builtin-command")) {
@@ -13,8 +13,7 @@ import { defaultBundleForTarget } from "../../core/mutation-target.js";
13
13
  import { canonicalizeWorkflowName, WORKFLOW_EXTENSIONS } from "../../core/recognition-util.js";
14
14
  import { warn } from "../../core/warn.js";
15
15
  import { prepareWriteTargetForMutation, resolveWriteTarget, withWriteTargetMutation } from "../../core/write-source.js";
16
- import { compileWorkflowPlan } from "../ir/compile.js";
17
- import { compileWorkflowSource } from "../source-ir/compile.js";
16
+ import { checkWorkflowPlan, compileWorkflowSource } from "../compile.js";
18
17
  const DEFAULT_WORKFLOW_TEMPLATE = renderWorkflowTemplate("New Workflow");
19
18
  export function getWorkflowTemplate() {
20
19
  return DEFAULT_WORKFLOW_TEMPLATE;
@@ -37,7 +36,7 @@ function validateWorkflowContent(content, sourcePath) {
37
36
  if (!result.ok) {
38
37
  throw new UsageError(formatWorkflowErrors(sourcePath, result.errors));
39
38
  }
40
- const compiled = compileWorkflowPlan(result.ir, slugifyWorkflowStepId(sourcePath));
39
+ const compiled = checkWorkflowPlan(result.plan);
41
40
  if (!compiled.ok) {
42
41
  throw new UsageError(formatWorkflowErrors(sourcePath, compiled.errors));
43
42
  }
@@ -98,7 +97,7 @@ export function createWorkflowAsset(input) {
98
97
  const mode = fs.existsSync(assetPath) ? fs.lstatSync(assetPath).mode & 0o777 : 0o644;
99
98
  const defaultBundle = defaultBundleForTarget(config);
100
99
  const ref = makeBundleRef(target.source.name === defaultBundle ? undefined : target.source.name, conceptId);
101
- withWriteTargetMutation(target, [assetPath], { ignored: "reject", purpose: "workflow-authoring", message: `Create ${ref}` }, () => {
100
+ withWriteTargetMutation(target, [assetPath], { purpose: "workflow-authoring", message: `Create ${ref}` }, () => {
102
101
  fs.mkdirSync(path.dirname(assetPath), { recursive: true });
103
102
  writeFileAtomic(assetPath, authoredContent.endsWith("\n") ? authoredContent : `${authoredContent}\n`, mode);
104
103
  });
@@ -154,14 +153,6 @@ function humanizeWorkflowName(name) {
154
153
  .replace(/\b\w/g, (match) => match.toUpperCase())
155
154
  .trim() || "New Workflow");
156
155
  }
157
- function slugifyWorkflowStepId(name) {
158
- return (name
159
- .split("/")
160
- .pop()
161
- ?.toLowerCase()
162
- .replace(/[^a-z0-9]+/g, "-")
163
- .replace(/^-+|-+$/g, "") || "workflow");
164
- }
165
156
  export function formatWorkflowErrors(path, errors) {
166
157
  const lines = errors.map((e) => ` ${path}:${e.line} — ${e.message}`);
167
158
  const heading = errors.length === 1 ? "Workflow has 1 error:" : `Workflow has ${errors.length} errors:`;
@@ -0,0 +1,211 @@
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
+ * The workflow compiler's front door. `.md` goes through the Markdown grammar
6
+ * (`parser.ts`), `.yml` through the GitHub-shaped YAML grammar
7
+ * (`github-yaml.ts`); both produce a {@link WorkflowPlan} directly.
8
+ *
9
+ * {@link checkWorkflowPlan} is the one cross-step pass the grammars do not
10
+ * run: every `steps.<id>` reference must name an earlier step (outputs may
11
+ * name any step), `inputs:` never name a param, and two non-fatal advisories.
12
+ */
13
+ import path from "node:path";
14
+ import { parseBuiltinCommandAction } from "../commands/command/builtin-action.js";
15
+ import { PORTABLE_ARGUMENTS_PLACEHOLDER } from "../commands/command/portable-template.js";
16
+ import { parseGithubWorkflowSource, WorkflowSourceFailure } from "./github-yaml.js";
17
+ import { parseWorkflow } from "./parser.js";
18
+ import { formatReference, parseReference } from "./program/expressions.js";
19
+ import { canonicalizeWorkflowWorkingDirectory, WorkflowSourceSemanticError } from "./source-semantics.js";
20
+ export { looksLikeGithubWorkflowSource } from "./github-yaml.js";
21
+ /** Compile by authoritative source extension. */
22
+ export function compileWorkflowSource(source, options) {
23
+ const extension = path.extname(options.path).toLowerCase();
24
+ const title = options.title ?? path.basename(options.path, path.extname(options.path));
25
+ if (extension === ".yml") {
26
+ try {
27
+ return { ok: true, plan: parseGithubWorkflowSource(source, { ...options, title }) };
28
+ }
29
+ catch (cause) {
30
+ if (cause instanceof WorkflowSourceFailure)
31
+ return { ok: false, errors: [cause.error] };
32
+ const message = cause instanceof Error ? cause.message : String(cause);
33
+ return { ok: false, errors: [{ code: "invalid-workflow-source", message, path: options.path, line: 1 }] };
34
+ }
35
+ }
36
+ if (extension !== ".md") {
37
+ return {
38
+ ok: false,
39
+ errors: [
40
+ {
41
+ code: "unsupported-workflow-extension",
42
+ message: `Workflow source ${options.path} must use .md or .yml.`,
43
+ path: options.path,
44
+ line: 1,
45
+ },
46
+ ],
47
+ };
48
+ }
49
+ const parsed = parseWorkflow(source, {
50
+ path: options.path,
51
+ title,
52
+ validateExecCwd: (value) => {
53
+ try {
54
+ return { ok: true, value: canonicalizeWorkflowWorkingDirectory(value, options.workspaceRoot) };
55
+ }
56
+ catch (cause) {
57
+ if (cause instanceof WorkflowSourceSemanticError)
58
+ return { ok: false, code: cause.code, message: cause.message };
59
+ return { ok: false, code: "working-directory-unverifiable", message: "cwd cannot be physically verified." };
60
+ }
61
+ },
62
+ });
63
+ if (parsed.ok)
64
+ return parsed;
65
+ return {
66
+ ok: false,
67
+ errors: parsed.errors.map((error) => ({
68
+ code: error.code ?? "invalid-markdown-workflow",
69
+ message: error.message,
70
+ path: options.path,
71
+ line: error.line,
72
+ })),
73
+ };
74
+ }
75
+ /**
76
+ * The display text a step contributes to `show`, search hints, and the run
77
+ * spine: its authored prose, else what its target does. Empty for a route
78
+ * step with no section.
79
+ */
80
+ export function workflowStepInstructions(step) {
81
+ const spec = step.spec;
82
+ if (!spec)
83
+ return "";
84
+ if (spec.instructions !== undefined)
85
+ return spec.instructions;
86
+ if (spec.uses === "akm/command") {
87
+ const action = parseBuiltinCommandAction(spec.with);
88
+ if (action.kind === "stored") {
89
+ return `Invoke stored command ${action.ref}${action.arguments === undefined ? "" : " with arguments"}.`;
90
+ }
91
+ if (spec.commandMode === "literal")
92
+ return action.content;
93
+ return action.content.split(PORTABLE_ARGUMENTS_PLACEHOLDER).join(action.arguments ?? "");
94
+ }
95
+ if (spec.uses !== undefined)
96
+ return `Invoke local target ${spec.uses}.`;
97
+ return "";
98
+ }
99
+ /** A route step's deterministic one-line description of its branch table. */
100
+ export function routeDescription(route) {
101
+ const branches = Object.entries(route.when).map(([match, stepId]) => `"${match}" -> ${stepId}`);
102
+ if (route.defaultStepId !== undefined)
103
+ branches.push(`default -> ${route.defaultStepId}`);
104
+ return `Route on ${route.input}: ${branches.join(", ")}.`;
105
+ }
106
+ /** Cross-step reference validation plus the non-fatal advisories. Pure. */
107
+ export function checkWorkflowPlan(plan) {
108
+ const errors = [];
109
+ const allStepIds = new Set(plan.steps.map((step) => step.stepId));
110
+ const earlierStepIds = new Set();
111
+ const check = (text, line, label, paramsAllowed) => {
112
+ const parsed = parseReference(text);
113
+ if (!parsed.ok) {
114
+ errors.push({ line, message: `${label}: ${parsed.message}` });
115
+ return;
116
+ }
117
+ if (parsed.expr.kind === "param") {
118
+ if (paramsAllowed)
119
+ return;
120
+ errors.push({
121
+ line,
122
+ message: `${label}: "${formatReference(parsed.expr)}" names a param, not a step output — params are already ` +
123
+ `attached to every unit, so declaring one as an input is redundant. "inputs:" only names step outputs ` +
124
+ `(steps.<id>.output...).`,
125
+ });
126
+ return;
127
+ }
128
+ if (earlierStepIds.has(parsed.expr.stepId))
129
+ return;
130
+ const why = allStepIds.has(parsed.expr.stepId)
131
+ ? `step "${parsed.expr.stepId}" does not come before this step — references must name an earlier step (a producer that has already run)`
132
+ : `"${parsed.expr.stepId}" is not a step in this workflow`;
133
+ errors.push({ line, message: `${label}: "${formatReference(parsed.expr)}" cannot be resolved — ${why}.` });
134
+ };
135
+ for (const step of plan.steps) {
136
+ const line = step.spec?.source.start ?? 1;
137
+ if (step.spec?.map)
138
+ check(step.spec.map.over, line, `Step "${step.stepId}" map.over`, true);
139
+ if (step.route)
140
+ check(step.route.input, line, `Step "${step.stepId}" route.input`, true);
141
+ for (const [index, reference] of (step.spec?.inputs ?? []).entries()) {
142
+ check(reference, line, `Step "${step.stepId}" inputs[${index}]`, false);
143
+ }
144
+ earlierStepIds.add(step.stepId);
145
+ }
146
+ // Outputs resolve at run completion, so they may name any declared step.
147
+ for (const [name, declaration] of Object.entries(plan.outputs ?? {})) {
148
+ const parsed = parseReference(declaration.from);
149
+ const label = `Output "${name}" from`;
150
+ if (!parsed.ok)
151
+ errors.push({ line: 1, message: `${label}: ${parsed.message}` });
152
+ else if (parsed.expr.kind === "param") {
153
+ errors.push({
154
+ line: 1,
155
+ message: `${label}: "${formatReference(parsed.expr)}" names a param, not a step output — an output projects a ` +
156
+ `STEP artifact, never a param. "outputs:" only names step outputs (steps.<id>.output...).`,
157
+ });
158
+ }
159
+ else if (!allStepIds.has(parsed.expr.stepId)) {
160
+ errors.push({
161
+ line: 1,
162
+ message: `${label}: "${formatReference(parsed.expr)}" cannot be resolved — "${parsed.expr.stepId}" is not a step in this workflow.`,
163
+ });
164
+ }
165
+ }
166
+ if (errors.length > 0)
167
+ return { ok: false, errors };
168
+ return { ok: true, warnings: workflowWarnings(plan) };
169
+ }
170
+ /**
171
+ * Advisories that never fail compilation or change the plan:
172
+ * A. a `params.<name>` reference (in `map.over`/`route.input`) to a param the
173
+ * document's `params:` block does not declare — a likely typo;
174
+ * B. `gate.max_loops` above 1 on an exec step, which is judged but never
175
+ * looped (a frozen argv cannot read the judge's feedback).
176
+ */
177
+ function workflowWarnings(plan) {
178
+ const warnings = [];
179
+ const declared = plan.paramSchemas ? new Set(Object.keys(plan.paramSchemas)) : undefined;
180
+ for (const step of plan.steps) {
181
+ const line = step.spec?.source.start ?? 1;
182
+ const maxLoops = step.gate.maxLoops;
183
+ if (maxLoops > 1 && step.spec?.exec && step.gate.criteria.length > 0) {
184
+ warnings.push({
185
+ line,
186
+ message: `Step "${step.stepId}" declares \`gate.max_loops: ${maxLoops}\` on an \`exec\` step — it runs its command ` +
187
+ `ONCE. A gate loop re-executes the step so it can address the judge's feedback, and a frozen argv cannot ` +
188
+ `read that feedback; looping would only repeat the command's side effects. The gate still evaluates and ` +
189
+ `can still fail the step.`,
190
+ });
191
+ }
192
+ if (!declared)
193
+ continue;
194
+ const scan = (text, label) => {
195
+ if (!text)
196
+ return;
197
+ const parsed = parseReference(text);
198
+ if (!parsed.ok || parsed.expr.kind !== "param" || declared.has(parsed.expr.name))
199
+ return;
200
+ warnings.push({
201
+ line,
202
+ message: `${label}: "${formatReference(parsed.expr)}" references a param not declared in \`params:\` ` +
203
+ `(declared: ${[...declared].join(", ") || "none"}) — likely a typo. An undeclared param supplied at start ` +
204
+ `still resolves at run time.`,
205
+ });
206
+ };
207
+ scan(step.spec?.map?.over, `Step "${step.stepId}" map.over`);
208
+ scan(step.route?.input, `Step "${step.stepId}" route.input`);
209
+ }
210
+ return warnings;
211
+ }
@@ -4,13 +4,7 @@
4
4
  import os from "node:os";
5
5
  import { isLoopbackEndpoint } from "../core/loopback.js";
6
6
  import { WORKFLOW_MAX_CONCURRENCY } from "./resource-limits.js";
7
- /**
8
- * Run-level ceiling on `workflow.maxConcurrency`. It is deliberately the SAME
9
- * value the frozen-plan decoder enforces on `execution.maxConcurrency` and on
10
- * per-step `map.concurrency` — a clamp above the decoder's bound would freeze
11
- * plans the decoder then rejects — so it reads the single shared constant
12
- * (`./resource-limits`) rather than repeating the literal.
13
- */
7
+ /** Run-level ceiling on `workflow.maxConcurrency` (the same bound as `map.concurrency`). */
14
8
  export const WORKFLOW_MAX_CONCURRENCY_CEILING = WORKFLOW_MAX_CONCURRENCY;
15
9
  export function cpuDerivedUnitConcurrency(cpuCount = os.cpus()?.length ?? 4) {
16
10
  return Math.min(16, Math.max(1, cpuCount - 2));
@@ -24,84 +18,29 @@ export function workflowMaxConcurrency(configured, cpuCount = os.cpus()?.length
24
18
  }
25
19
  // ── Fan-out defaults ─────────────────────────────────────────────────────────
26
20
  //
27
- // Four independent limits clamp a `map` step's real width, and the effective
28
- // value is their minimum:
29
- //
30
- // 1. the step's own `map.concurrency` (this file's default below)
31
- // 2. the run's frozen `execution.maxConcurrency` ({@link workflowMaxConcurrency})
32
- // 3. the selected LLM engine's frozen concurrency ({@link defaultLlmEngineConcurrency})
33
- // 4. the CURRENT host's CPU safety cap ({@link cpuDerivedUnitConcurrency})
34
- //
35
- // (1) and (3) both defaulted to 1 before 0.9.1, which made every fan-out serial
36
- // unless the author opted in at BOTH layers — so (2) and (4), the limits that
37
- // actually encode machine capacity, never bound anything. The defaults below
38
- // replace those two 1s. They are deliberately modest rather than "as wide as
39
- // the host allows": a `map` is independent by construction, but its units call
40
- // out to rate-limited providers and RAM-hungry agent processes, so the value
41
- // that a plan freezes should be one a laptop and a CI box can both survive.
21
+ // A `map` step's real width is the minimum of its own `map.concurrency`, the
22
+ // run's frozen `execution.maxConcurrency`, the LLM engine's frozen
23
+ // concurrency, and the host CPU cap. The defaults are modest: map units call
24
+ // rate-limited providers and RAM-hungry agents.
42
25
  /**
43
- * Default width of a `map` step that declares no `concurrency:` (0.9.1+).
44
- *
45
- * 4 is chosen over the host cap on purpose. It is a real, predictable speedup
46
- * (4× on any fan-out longer than four items) while staying below
47
- * {@link cpuDerivedUnitConcurrency} on every machine with ≥6 cores, so the
48
- * frozen number — not the host — is what an author reasons about, and a plan
49
- * frozen on a 32-core CI box behaves the same when it resumes on a laptop.
50
- *
51
- * Overridable in both directions:
52
- * - per step: `map.concurrency: <n>` (an explicit `1` still means serial),
53
- * - per install: `workflow.defaultMapConcurrency` — set it to `1` to restore
54
- * the pre-0.9.1 serial default for every workflow at once.
26
+ * Default width of a `map` step with no `concurrency:` — a predictable number
27
+ * rather than the host cap, so a plan behaves the same wherever it resumes.
28
+ * Overridden per step (`map.concurrency`) or per install
29
+ * (`workflow.defaultMapConcurrency`; `1` restores serial fan-out).
55
30
  */
56
31
  export const DEFAULT_MAP_CONCURRENCY = 4;
57
- /**
58
- * Default `engines.<name>.concurrency` for an LLM engine on a LOOPBACK
59
- * endpoint. Stays at 1, matching `getDefaultLlmConcurrency`
60
- * (`src/indexer/indexer.ts`) and AGENTS.md's "lowest common denominator — a
61
- * slow local model on a single-threaded server" rule. A local model server
62
- * (LM Studio, Ollama) holds ONE loaded model; parallel inference triggers
63
- * reload thrash and HTTP 500s, which is a hard failure, not a slow one.
64
- */
32
+ /** Default concurrency for an LLM engine on a loopback endpoint: a local model server runs one inference. */
65
33
  export const DEFAULT_LOCAL_LLM_ENGINE_CONCURRENCY = 1;
66
- /**
67
- * Default `engines.<name>.concurrency` for an LLM engine on a REMOTE endpoint.
68
- *
69
- * Deliberately equal to {@link DEFAULT_MAP_CONCURRENCY} so this limit does not
70
- * silently re-serialize a fan-out the author already asked for: the step's own
71
- * `concurrency:` stays the number that decides. Indexing's remote default is a
72
- * lower 2 because indexing fans out implicitly over the whole stash; a
73
- * workflow `map` is an explicit, bounded, author-declared fan-out, and four
74
- * concurrent completions sit far inside any hosted provider's entry tier.
75
- * Rate-limited installs set `engines.<name>.concurrency` to pin their own.
76
- */
34
+ /** Default concurrency for a remote LLM engine: equal to {@link DEFAULT_MAP_CONCURRENCY}, so it never re-serializes a map. */
77
35
  export const DEFAULT_REMOTE_LLM_ENGINE_CONCURRENCY = 4;
78
- // ── Loopback classification ──────────────────────────────────────────────────
79
- //
80
- // Everything above turns on ONE question: does this endpoint name a model
81
- // server running on THIS machine? The classifier lives in `core/loopback.ts`
82
- // (shared with the indexer's LLM pool default); the re-export keeps this
83
- // module the policy surface workflow callers and the boundary-case table in
84
- // `tests/workflows/concurrency-defaults.test.ts` import from. The check is
85
- // purely syntactic — no DNS, no interface list — so freeze produces the same
86
- // plan on a laptop, on CI, and on a machine with no network.
87
36
  export { isLoopbackEndpoint, isLoopbackHost } from "../core/loopback.js";
88
- /**
89
- * Concurrency to freeze for an LLM engine. An explicit
90
- * `engines.<name>.concurrency` always wins (clamped into the decoder's
91
- * `[1, 64]` range so a fat-fingered config cannot freeze an unloadable plan);
92
- * otherwise the endpoint decides.
93
- */
37
+ /** Concurrency to freeze for an LLM engine: an explicit `engines.<name>.concurrency` (clamped), else by endpoint. */
94
38
  export function defaultLlmEngineConcurrency(endpoint, configured) {
95
39
  if (typeof configured === "number" && Number.isFinite(configured))
96
40
  return clampMaxConcurrency(configured);
97
41
  return isLoopbackEndpoint(endpoint) ? DEFAULT_LOCAL_LLM_ENGINE_CONCURRENCY : DEFAULT_REMOTE_LLM_ENGINE_CONCURRENCY;
98
42
  }
99
- /**
100
- * Width to freeze for a `map` step that declared no `concurrency:`. `configured`
101
- * is `workflow.defaultMapConcurrency`; unset means {@link DEFAULT_MAP_CONCURRENCY}.
102
- * An explicit `map.concurrency` never reaches this function — the caller keeps
103
- * "author wrote 1" distinguishable from "author wrote nothing".
104
- */
43
+ /** Width to freeze for a `map` step with no `concurrency:` (`configured` is `workflow.defaultMapConcurrency`). */
105
44
  export function defaultMapConcurrency(configured) {
106
45
  return configured === undefined || !Number.isFinite(configured)
107
46
  ? DEFAULT_MAP_CONCURRENCY
@@ -2,26 +2,12 @@
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
- * A child workflow run's idempotency key (spec
6
- * docs/plans/specs/p3a-plan-v5-child-freeze.md §3.4, rows A-17…A-19). Pure:
7
- * no IO, no config, no clock, no randomness — imports exactly node:crypto
8
- * and canonicalJson.
9
- *
10
- * P3a has no production caller: P3b's child executor derives this key from
11
- * the parent unit's `hashVersion` 7 input hash and passes it to
12
- * `publishChildWorkflowRun` (src/storage/repositories/workflow-runs-repository.ts,
13
- * Lane C) as the `(parent_run_id, invocation_key)` idempotency pair.
5
+ * A child workflow run's idempotency key, derived from the parent unit's input
6
+ * hash: `publishChildWorkflowRun` is idempotent on `(parent_run_id, invocation_key)`.
14
7
  */
15
8
  import { createHash } from "node:crypto";
16
9
  import { canonicalJson } from "../ir/plan-hash.js";
17
- /**
18
- * `sha256hex("akm.workflow.child-invocation\0v1\0" + canonicalJson({parentRunId, parentUnitId, unitInputHash}))`.
19
- *
20
- * The `\0v1\0` here is this helper's OWN vocabulary version, deliberately
21
- * independent of `hashVersion`: `unitInputHash` enters this preimage as an
22
- * opaque value, so this key's preimage does not change when the unit-hash
23
- * vocabulary itself bumps.
24
- */
10
+ /** `sha256hex("akm.workflow.child-invocation\0v1\0" + canonicalJson({parentRunId, parentUnitId, unitInputHash}))`. */
25
11
  export function computeChildInvocationKey(input) {
26
12
  return createHash("sha256")
27
13
  .update("akm.workflow.child-invocation\0v1\0")