akm-cli 0.9.0-rc.9 → 0.9.0

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 (450) hide show
  1. package/CHANGELOG.md +1063 -44
  2. package/README.md +51 -25
  3. package/SECURITY.md +14 -1
  4. package/STABILITY.md +497 -0
  5. package/dist/akm +148 -35
  6. package/dist/{akm-migrate-storage → akm-migrate} +6 -9
  7. package/dist/assets/hints/cli-hints-full.md +223 -95
  8. package/dist/assets/hints/cli-hints-short.md +85 -22
  9. package/dist/assets/improve-strategies/default.json +1 -1
  10. package/dist/assets/improve-strategies/reflect-distill.json +1 -1
  11. package/dist/assets/prompts/memory-infer-user.md +2 -3
  12. package/dist/assets/stash-skeleton/README.md +6 -5
  13. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +2 -0
  14. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +2 -0
  15. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +2 -0
  16. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +2 -0
  17. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +2 -0
  18. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +2 -0
  19. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +2 -0
  20. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +2 -0
  21. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +2 -0
  22. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +2 -0
  23. package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -0
  24. package/dist/assets/stash-skeleton/facts/conventions/organization.md +20 -9
  25. package/dist/assets/tasks/core/extract.yml +1 -1
  26. package/dist/assets/tasks/core/version-check.yml +1 -1
  27. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
  28. package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
  29. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
  30. package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
  31. package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
  32. package/dist/assets/templates/html/health.html +1 -3
  33. package/dist/assets/workflows/workflow-template.md +32 -15
  34. package/dist/cli/invocation.js +40 -15
  35. package/dist/cli/parse-args.js +0 -22
  36. package/dist/cli/retired-commands.js +121 -0
  37. package/dist/cli/shared.js +154 -22
  38. package/dist/cli/unknown-flags.js +236 -0
  39. package/dist/cli-node.mjs +2 -1
  40. package/dist/cli.js +696 -258
  41. package/dist/commands/agent/agent-dispatch.js +14 -3
  42. package/dist/commands/agent/contribute-cli.js +73 -88
  43. package/dist/commands/completions.js +79 -22
  44. package/dist/commands/config-cli.js +17 -150
  45. package/dist/commands/env/env-cli.js +59 -143
  46. package/dist/commands/env/env.js +12 -163
  47. package/dist/commands/env/marker-path.js +6 -0
  48. package/dist/commands/env/secret-cli.js +36 -66
  49. package/dist/commands/env/secret.js +24 -57
  50. package/dist/commands/feedback-cli.js +141 -87
  51. package/dist/commands/health/accept-rate.js +58 -0
  52. package/dist/commands/health/advisories.js +3 -4
  53. package/dist/commands/health/checks.js +85 -23
  54. package/dist/commands/health/html-report.js +7 -10
  55. package/dist/commands/health/improve-metrics.js +25 -83
  56. package/dist/commands/health/md-report.js +5 -9
  57. package/dist/commands/health/metrics.js +62 -20
  58. package/dist/commands/health/renderers.js +47 -0
  59. package/dist/commands/health/report-view-model.js +4 -5
  60. package/dist/commands/health/stash-exposure.js +1 -1
  61. package/dist/commands/health/surfaces.js +3 -48
  62. package/dist/commands/health/task-runs.js +3 -67
  63. package/dist/commands/health/types-improve.js +7 -0
  64. package/dist/commands/health.js +99 -28
  65. package/dist/commands/improve/anti-collapse.js +2 -2
  66. package/dist/commands/improve/autonomy-gate.js +68 -0
  67. package/dist/commands/improve/collapse-detector.js +41 -40
  68. package/dist/commands/improve/consolidate/eligibility.js +1 -23
  69. package/dist/commands/improve/consolidate/merge.js +4 -0
  70. package/dist/commands/improve/consolidate.js +140 -1000
  71. package/dist/commands/improve/distill/promote-memory.js +12 -12
  72. package/dist/commands/improve/distill/quality-gate.js +6 -6
  73. package/dist/commands/improve/distill.js +58 -69
  74. package/dist/commands/improve/eligibility.js +95 -55
  75. package/dist/commands/improve/extract-cli.js +14 -133
  76. package/dist/commands/improve/improve-cli.js +98 -114
  77. package/dist/commands/improve/improve-result-file.js +1 -28
  78. package/dist/commands/improve/improve-strategies.js +8 -5
  79. package/dist/commands/improve/improve.js +128 -91
  80. package/dist/commands/improve/loop-stages.js +182 -20
  81. package/dist/commands/improve/memory/derived-ref.js +45 -43
  82. package/dist/commands/improve/memory/memory-belief.js +1 -1
  83. package/dist/commands/improve/memory/memory-contradiction-detect.js +4 -12
  84. package/dist/commands/improve/memory/memory-improve.js +6 -5
  85. package/dist/commands/improve/outcome-loop.js +22 -65
  86. package/dist/commands/improve/preparation.js +114 -123
  87. package/dist/commands/improve/proactive-maintenance.js +2 -5
  88. package/dist/commands/improve/reflect.js +56 -160
  89. package/dist/commands/improve/salience.js +11 -122
  90. package/dist/commands/improve/source-identity.js +10 -38
  91. package/dist/commands/lint/base-linter.js +20 -124
  92. package/dist/commands/lint/env-key-rules.js +31 -47
  93. package/dist/commands/lint/index.js +249 -43
  94. package/dist/commands/{events.js → log.js} +33 -38
  95. package/dist/commands/migrate-cli.js +92 -12
  96. package/dist/commands/migration-tool.js +46 -0
  97. package/dist/commands/observability-cli.js +70 -209
  98. package/dist/commands/proposal/drain.js +101 -29
  99. package/dist/commands/proposal/proposal-cli.js +76 -48
  100. package/dist/commands/proposal/proposal.js +54 -18
  101. package/dist/commands/proposal/propose-cli.js +88 -0
  102. package/dist/commands/proposal/propose.js +23 -15
  103. package/dist/commands/proposal/repository.js +701 -278
  104. package/dist/commands/proposal/validators/proposal-quality-validators.js +2 -8
  105. package/dist/commands/proposal/validators/proposal-validators.js +55 -7
  106. package/dist/commands/proposal/validators/proposals.js +4 -7
  107. package/dist/commands/read/curate.js +34 -53
  108. package/dist/commands/read/knowledge.js +150 -95
  109. package/dist/commands/read/registry-search.js +2 -2
  110. package/dist/commands/read/remember-cli.js +42 -15
  111. package/dist/commands/read/search-cli.js +180 -78
  112. package/dist/commands/read/search.js +58 -43
  113. package/dist/commands/read/show.js +197 -141
  114. package/dist/commands/registry-cli.js +12 -51
  115. package/dist/commands/remember.js +14 -57
  116. package/dist/commands/sources/add-cli.js +100 -31
  117. package/dist/commands/sources/bundle-cli.js +166 -0
  118. package/dist/commands/sources/bundle-config-ops.js +7 -2
  119. package/dist/commands/sources/info.js +18 -5
  120. package/dist/commands/sources/init.js +12 -12
  121. package/dist/commands/sources/installed-stashes.js +382 -98
  122. package/dist/commands/sources/schema-repair.js +3 -2
  123. package/dist/commands/sources/self-update.js +131 -38
  124. package/dist/commands/sources/source-add.js +72 -17
  125. package/dist/commands/sources/source-clone.js +129 -45
  126. package/dist/commands/sources/source-manage.js +43 -23
  127. package/dist/commands/sources/sources-cli.js +57 -208
  128. package/dist/commands/sources/stash-cli.js +46 -53
  129. package/dist/commands/tasks/tasks-cli.js +91 -97
  130. package/dist/commands/tasks/tasks.js +276 -421
  131. package/dist/commands/workflow-cli.js +175 -450
  132. package/dist/core/adapter/adapters/akm-adapter.js +47 -28
  133. package/dist/core/adapter/adapters/akm-lint.js +42 -27
  134. package/dist/core/adapter/adapters/akm-metadata.js +15 -44
  135. package/dist/core/adapter/adapters/akm-task-adapter.js +15 -13
  136. package/dist/core/adapter/adapters/akm-workflow-adapter.js +55 -71
  137. package/dist/core/adapter/adapters/dotenv-adapter.js +1 -1
  138. package/dist/core/adapter/adapters/generic-files-adapter.js +2 -0
  139. package/dist/core/adapter/adapters/index.js +6 -6
  140. package/dist/core/adapter/adapters/llm-wiki-adapter.js +14 -8
  141. package/dist/core/adapter/adapters/okf-adapter.js +187 -19
  142. package/dist/core/adapter/adapters/shared.js +3 -19
  143. package/dist/core/adapter/adapters/tool-dir-shared.js +8 -3
  144. package/dist/core/adapter/adapters/website-snapshot-adapter.js +1 -0
  145. package/dist/core/adapter/detect-adapter.js +17 -0
  146. package/dist/core/adapter/recognize-match.js +6 -4
  147. package/dist/core/adapter/validate-context.js +214 -0
  148. package/dist/core/asset/akm-markdown.js +63 -0
  149. package/dist/core/asset/asset-placement.js +20 -6
  150. package/dist/core/asset/asset-ref.js +11 -9
  151. package/dist/core/asset/frontmatter-lint.js +30 -0
  152. package/dist/core/asset/frontmatter.js +25 -9
  153. package/dist/core/asset/markdown.js +40 -51
  154. package/dist/core/asset/resolve-ref.js +89 -18
  155. package/dist/core/asset/stash-meta.js +1 -1
  156. package/dist/core/bundle-id.js +51 -0
  157. package/dist/core/common.js +152 -38
  158. package/dist/core/config/config-io.js +12 -1
  159. package/dist/core/config/config-schema.js +35 -8
  160. package/dist/core/config/config-sources.js +55 -11
  161. package/dist/core/config/config-walker.js +25 -9
  162. package/dist/core/config/config.js +9 -48
  163. package/dist/core/config/experimental.js +21 -0
  164. package/dist/core/config/schema/embedding.js +5 -1
  165. package/dist/core/config/schema/experimental.js +30 -0
  166. package/dist/core/config/schema/improve-processes.js +0 -6
  167. package/dist/core/config/schema/improve.js +21 -3
  168. package/dist/core/config/schema/index-config.js +8 -15
  169. package/dist/core/config/schema/output.js +4 -1
  170. package/dist/core/config/schema/setup.js +9 -18
  171. package/dist/core/config/schema/sources-bundles.js +49 -33
  172. package/dist/core/config/schema/workflow.js +3 -3
  173. package/dist/core/env-secret-ref.js +76 -46
  174. package/dist/core/errors.js +18 -12
  175. package/dist/core/events.js +46 -128
  176. package/dist/core/file-change.js +6 -5
  177. package/dist/core/fs-txn.js +83 -7
  178. package/dist/core/git-message.js +2 -2
  179. package/dist/core/improve-result.js +1 -100
  180. package/dist/core/lesson-lint.js +1 -17
  181. package/dist/core/logs-db.js +2 -1
  182. package/dist/core/migration-operation.js +16 -0
  183. package/dist/core/mutation-target.js +78 -0
  184. package/dist/core/parse.js +4 -1
  185. package/dist/core/paths.js +17 -20
  186. package/dist/core/recognition-util.js +12 -14
  187. package/dist/core/redaction.js +34 -0
  188. package/dist/core/standards/resolve-standards-context.js +2 -14
  189. package/dist/core/standards/resolve-stash-standards.js +2 -2
  190. package/dist/core/standards/resolve-type-conventions.js +2 -2
  191. package/dist/core/state/migrations.js +41 -18
  192. package/dist/core/state-db.js +5 -14
  193. package/dist/core/structured.js +1 -1
  194. package/dist/core/subprocess.js +6 -4
  195. package/dist/core/text-truncation.js +9 -5
  196. package/dist/core/type-presentation.js +3 -3
  197. package/dist/core/warn.js +0 -3
  198. package/dist/core/write-source.js +771 -95
  199. package/dist/indexer/bundle-identity-guard.js +3 -2
  200. package/dist/indexer/db/graph-db.js +0 -24
  201. package/dist/indexer/ensure-index.js +1 -0
  202. package/dist/indexer/graph/graph-boost.js +9 -34
  203. package/dist/indexer/graph/graph-extraction.js +8 -5
  204. package/dist/indexer/index-writer-lock.js +53 -17
  205. package/dist/indexer/index-written-assets.js +16 -22
  206. package/dist/indexer/indexer.js +497 -239
  207. package/dist/indexer/installations.js +14 -96
  208. package/dist/indexer/passes/dir-staleness.js +16 -9
  209. package/dist/indexer/passes/memory-inference.js +11 -9
  210. package/dist/indexer/passes/metadata.js +113 -47
  211. package/dist/indexer/scan/doc-to-entry.js +38 -1
  212. package/dist/indexer/scan/drain-dir.js +13 -23
  213. package/dist/indexer/search/db-search.js +99 -54
  214. package/dist/indexer/search/fts-query.js +47 -24
  215. package/dist/indexer/search/ranking-contributors.js +42 -20
  216. package/dist/indexer/search/ranking.js +18 -99
  217. package/dist/indexer/search/search-fields.js +7 -2
  218. package/dist/indexer/search/search-source.js +82 -93
  219. package/dist/indexer/usage/usage-events.js +0 -89
  220. package/dist/indexer/walk/file-context.js +2 -1
  221. package/dist/indexer/walk/matchers.js +30 -43
  222. package/dist/indexer/walk/path-resolver.js +7 -2
  223. package/dist/indexer/walk/walker.js +38 -12
  224. package/dist/integrations/agent/builders.js +0 -6
  225. package/dist/integrations/agent/config.js +2 -2
  226. package/dist/integrations/agent/detect.js +49 -19
  227. package/dist/integrations/agent/engine-fallback.js +76 -0
  228. package/dist/integrations/agent/profiles.js +14 -0
  229. package/dist/integrations/agent/prompts.js +12 -8
  230. package/dist/integrations/agent/runner-dispatch.js +4 -2
  231. package/dist/integrations/agent/runner.js +0 -1
  232. package/dist/integrations/agent/spawn.js +5 -6
  233. package/dist/integrations/github.js +1 -1
  234. package/dist/integrations/harnesses/aider/agent-builder.js +6 -4
  235. package/dist/integrations/harnesses/amazonq/agent-builder.js +7 -4
  236. package/dist/integrations/harnesses/claude/session-log.js +0 -10
  237. package/dist/integrations/harnesses/codex/agent-builder.js +5 -2
  238. package/dist/integrations/harnesses/copilot/agent-builder.js +5 -3
  239. package/dist/integrations/harnesses/gemini/agent-builder.js +5 -3
  240. package/dist/integrations/harnesses/index.js +3 -7
  241. package/dist/integrations/harnesses/opencode/agent-builder.js +21 -2
  242. package/dist/integrations/harnesses/opencode/session-log.js +0 -15
  243. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +13 -4
  244. package/dist/integrations/harnesses/openhands/agent-builder.js +9 -6
  245. package/dist/integrations/harnesses/pi/agent-builder.js +6 -4
  246. package/dist/integrations/lockfile.js +88 -7
  247. package/dist/integrations/session-logs/index.js +3 -28
  248. package/dist/llm/client.js +136 -100
  249. package/dist/llm/embedders/remote.js +13 -5
  250. package/dist/llm/feature-gate.js +4 -12
  251. package/dist/llm/graph-extract.js +5 -11
  252. package/dist/llm/memory-infer.js +144 -1
  253. package/dist/llm/metadata-enhance.js +5 -7
  254. package/dist/llm/structured-call.js +1 -1
  255. package/dist/llm/usage-persist.js +26 -5
  256. package/dist/llm/usage-telemetry.js +25 -2
  257. package/dist/output/cli-hints.js +1 -2
  258. package/dist/output/context.js +22 -7
  259. package/dist/output/format-exempt.js +80 -0
  260. package/dist/output/generic-render.js +259 -0
  261. package/dist/output/render-registry.js +57 -0
  262. package/dist/output/renderers.js +14 -36
  263. package/dist/output/shapes/curate.js +10 -1
  264. package/dist/output/shapes/events.js +12 -7
  265. package/dist/output/shapes/helpers.js +56 -83
  266. package/dist/output/shapes/migrate.js +8 -0
  267. package/dist/output/shapes/passthrough.js +7 -41
  268. package/dist/output/shapes/proposal/producer.js +15 -7
  269. package/dist/output/shapes.js +2 -9
  270. package/dist/output/text/{init.js → bundle-create.js} +3 -1
  271. package/dist/output/text/bundle-show.js +7 -0
  272. package/dist/output/text/command-format.js +164 -96
  273. package/dist/output/text/env.js +1 -3
  274. package/dist/output/text/events.js +8 -7
  275. package/dist/output/text/health-format.js +103 -0
  276. package/dist/output/text/health.js +7 -0
  277. package/dist/output/text/helpers.js +10 -8
  278. package/dist/output/text/lint-format.js +43 -0
  279. package/dist/output/text/{save.js → lint.js} +2 -2
  280. package/dist/output/text/migrate.js +88 -0
  281. package/dist/output/text/proposal/producer.js +4 -2
  282. package/dist/output/text/proposal-format.js +44 -72
  283. package/dist/output/text/registry-commands.js +1 -2
  284. package/dist/output/text/show-directives.js +15 -7
  285. package/dist/output/text/status-list.js +32 -0
  286. package/dist/output/text/sync.js +5 -0
  287. package/dist/output/text/workflow-format.js +24 -203
  288. package/dist/output/text/workflow.js +1 -7
  289. package/dist/output/text.js +16 -17
  290. package/dist/registry/factory.js +4 -6
  291. package/dist/registry/origin-resolve.js +16 -27
  292. package/dist/registry/providers/skills-sh.js +3 -3
  293. package/dist/registry/providers/static-index.js +13 -23
  294. package/dist/registry/resolve.js +42 -7
  295. package/dist/registry/semver.js +34 -84
  296. package/dist/runtime.js +2 -23
  297. package/dist/scripts/akm-migrate-node.js +60290 -0
  298. package/dist/scripts/akm-migrate.js +59628 -0
  299. package/dist/setup/detect.js +42 -15
  300. package/dist/setup/registry-stash-loader.js +2 -2
  301. package/dist/setup/setup.js +236 -136
  302. package/dist/setup/steps/connection.js +7 -9
  303. package/dist/setup/steps/platforms.js +9 -9
  304. package/dist/setup/steps/semantic.js +15 -3
  305. package/dist/setup/steps/sources.js +12 -13
  306. package/dist/setup/steps/stashdir.js +2 -3
  307. package/dist/setup/steps/tasks.js +237 -120
  308. package/dist/sources/freshness.js +1 -1
  309. package/dist/sources/provider-factory.js +11 -17
  310. package/dist/sources/providers/filesystem.js +2 -3
  311. package/dist/sources/providers/git-install.js +278 -34
  312. package/dist/sources/providers/git-provider.js +25 -23
  313. package/dist/sources/providers/git-stash.js +395 -106
  314. package/dist/sources/providers/git.js +2 -2
  315. package/dist/sources/providers/npm.js +16 -19
  316. package/dist/sources/providers/provider-utils.js +7 -4
  317. package/dist/sources/providers/sync-from-ref.js +3 -9
  318. package/dist/sources/providers/website.js +6 -1
  319. package/dist/sources/resolve.js +6 -5
  320. package/dist/sources/snapshot-fetchers/bluesky.js +146 -0
  321. package/dist/sources/snapshot-fetchers/content-extract.js +566 -0
  322. package/dist/sources/snapshot-fetchers/fetcher-util.js +41 -0
  323. package/dist/sources/snapshot-fetchers/github.js +100 -0
  324. package/dist/sources/snapshot-fetchers/host-guard.js +291 -0
  325. package/dist/sources/snapshot-fetchers/registry.js +17 -1
  326. package/dist/sources/snapshot-fetchers/robots.js +348 -0
  327. package/dist/sources/snapshot-fetchers/rss.js +282 -0
  328. package/dist/sources/snapshot-fetchers/secret-seam.js +42 -0
  329. package/dist/sources/snapshot-fetchers/website-ingest.js +566 -268
  330. package/dist/sources/snapshot-fetchers/x.js +910 -0
  331. package/dist/storage/database.js +7 -0
  332. package/dist/storage/engines/sqlite-migrations.js +23 -111
  333. package/dist/storage/managed-db.js +2 -2
  334. package/dist/storage/repositories/canaries-repository.js +1 -1
  335. package/dist/storage/repositories/events-repository.js +27 -11
  336. package/dist/storage/repositories/improve-runs-repository.js +6 -12
  337. package/dist/storage/repositories/index-connection.js +17 -6
  338. package/dist/storage/repositories/index-entries-repository.js +151 -240
  339. package/dist/storage/repositories/index-entry-mapper.js +15 -11
  340. package/dist/storage/repositories/index-fts-repository.js +5 -2
  341. package/dist/storage/repositories/index-llm-cache-repository.js +0 -1
  342. package/dist/storage/repositories/index-meta-repository.js +2 -3
  343. package/dist/storage/repositories/index-schema.js +10 -25
  344. package/dist/storage/repositories/index-utility-repository.js +15 -28
  345. package/dist/storage/repositories/index-vec-repository.js +6 -1
  346. package/dist/storage/repositories/outcome-repository.js +119 -0
  347. package/dist/storage/repositories/proposals-repository.js +296 -59
  348. package/dist/storage/repositories/registry-cache.js +19 -0
  349. package/dist/storage/repositories/salience-repository.js +172 -0
  350. package/dist/storage/repositories/task-history-repository.js +15 -13
  351. package/dist/storage/repositories/workflow-runs-repository.js +52 -40
  352. package/dist/tasks/backends/cron.js +105 -15
  353. package/dist/tasks/backends/index.js +1 -1
  354. package/dist/tasks/backends/launchd.js +85 -38
  355. package/dist/tasks/backends/schtasks.js +135 -15
  356. package/dist/tasks/embedded.js +56 -40
  357. package/dist/tasks/parser.js +7 -157
  358. package/dist/tasks/resolve-akm-bin.js +137 -59
  359. package/dist/tasks/runner.js +79 -42
  360. package/dist/tasks/scheduler-invocation.js +220 -10
  361. package/dist/tasks/schema.js +24 -1
  362. package/dist/tasks/task-id.js +1 -3
  363. package/dist/tasks/validator.js +20 -6
  364. package/dist/workflows/authoring/authoring.js +94 -143
  365. package/dist/workflows/authoring/scope-key.js +1 -1
  366. package/dist/workflows/exec/frozen-judge.js +28 -2
  367. package/dist/workflows/exec/native-executor.js +77 -57
  368. package/dist/workflows/exec/param-secrets.js +9 -9
  369. package/dist/workflows/exec/run-workflow.js +133 -79
  370. package/dist/workflows/exec/step-work.js +219 -346
  371. package/dist/{migrate-storage-node.mjs → workflows/exec/unit-dispatch.js} +1 -5
  372. package/dist/workflows/ir/compile.js +141 -270
  373. package/dist/workflows/ir/freeze.js +40 -30
  374. package/dist/workflows/ir/params.js +135 -11
  375. package/dist/workflows/ir/plan-hash.js +1 -1
  376. package/dist/workflows/ir/schema.js +25 -26
  377. package/dist/workflows/parser.js +872 -307
  378. package/dist/workflows/program/expressions.js +20 -208
  379. package/dist/workflows/program/schema.js +7 -10
  380. package/dist/workflows/renderer.js +95 -68
  381. package/dist/workflows/resource-limits.js +2 -0
  382. package/dist/workflows/runtime/checkin.js +3 -3
  383. package/dist/workflows/runtime/plan-classifier.js +16 -75
  384. package/dist/workflows/runtime/runs.js +186 -127
  385. package/dist/workflows/runtime/unit-checkin.js +1 -1
  386. package/dist/workflows/runtime/unit-phases.js +2 -2
  387. package/dist/workflows/runtime/workflow-asset-loader.js +232 -83
  388. package/dist/workflows/schema.js +1 -11
  389. package/dist/workflows/validate-summary.js +30 -36
  390. package/dist/workflows/validator.js +21 -62
  391. package/docs/README.md +68 -0
  392. package/docs/migration/README.md +8 -0
  393. package/docs/migration/release-notes/0.7.0.md +11 -11
  394. package/docs/migration/release-notes/0.9.0.md +208 -27
  395. package/docs/migration/v0.7-to-v0.8.md +46 -47
  396. package/docs/migration/v0.8-to-v0.9.md +564 -208
  397. package/docs/migration/v0.9.0-troubleshooting.md +561 -0
  398. package/docs/reference/README.md +12 -0
  399. package/docs/reference/cli.md +2253 -0
  400. package/docs/reference/configuration.md +358 -0
  401. package/docs/reference/data-and-telemetry.md +105 -42
  402. package/docs/reference/workflows.md +647 -0
  403. package/package.json +22 -11
  404. package/schemas/akm-asset-envelope.json +93 -0
  405. package/schemas/akm-config.json +81 -128
  406. package/schemas/akm-workflow.json +74 -73
  407. package/dist/assets/tasks/core/backup.yml +0 -5
  408. package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
  409. package/dist/cli/config-migrate.js +0 -1878
  410. package/dist/cli/config-validate.js +0 -41
  411. package/dist/commands/backup-cli.js +0 -56
  412. package/dist/commands/bundle/bundle-cli.js +0 -68
  413. package/dist/commands/bundle/bundle.js +0 -219
  414. package/dist/commands/graph/graph-cli.js +0 -124
  415. package/dist/commands/graph/graph.js +0 -489
  416. package/dist/commands/improve/extract-watch.js +0 -140
  417. package/dist/commands/mv-cli.js +0 -1221
  418. package/dist/commands/sources/history.js +0 -201
  419. package/dist/commands/tasks/default-tasks.js +0 -186
  420. package/dist/core/migration-backup.js +0 -1231
  421. package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -95
  422. package/dist/llm/memory-infer-impl.js +0 -138
  423. package/dist/migrate/legacy/config-source-migration.js +0 -257
  424. package/dist/migrate/legacy/content-migration.js +0 -350
  425. package/dist/migrate/legacy/legacy-layout.js +0 -779
  426. package/dist/migrate/legacy/legacy-paths.js +0 -25
  427. package/dist/migrate/legacy/legacy-stash-json.js +0 -80
  428. package/dist/migrate/legacy/proposal-fs-import.js +0 -168
  429. package/dist/migrate/legacy/task-target-ref-migration.js +0 -278
  430. package/dist/migrate/legacy/three-db-cutover.js +0 -845
  431. package/dist/migrate/legacy/workflow-migrations-bodies.js +0 -52
  432. package/dist/migrate/legacy/workflow-migrations-frozen.js +0 -21
  433. package/dist/migrate/legacy-ref-grammar.js +0 -214
  434. package/dist/output/shapes/distill.js +0 -14
  435. package/dist/output/shapes/history.js +0 -11
  436. package/dist/output/text/distill.js +0 -6
  437. package/dist/output/text/enable-disable.js +0 -8
  438. package/dist/output/text/history.js +0 -6
  439. package/dist/registry/build-index.js +0 -382
  440. package/dist/schemas/akm-config.json +0 -4704
  441. package/dist/schemas/akm-task.json +0 -87
  442. package/dist/schemas/akm-workflow.json +0 -372
  443. package/dist/scripts/migrate-storage.js +0 -3816
  444. package/dist/workflows/authoring/workflow-program-template.yaml +0 -31
  445. package/dist/workflows/cli.js +0 -53
  446. package/dist/workflows/exec/brief.js +0 -481
  447. package/dist/workflows/exec/report.js +0 -1460
  448. package/dist/workflows/exec/watch.js +0 -116
  449. package/dist/workflows/program/parser.js +0 -813
  450. package/dist/workflows/program/project.js +0 -104
@@ -2,51 +2,85 @@
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
- * Workflow markdown WorkflowDocument JSON.
5
+ * Unified workflow markdown parser (workflow-format-unification).
6
6
  *
7
- * Composition over invention: frontmatter is parsed with the `yaml` package,
8
- * heading discovery with `parseMarkdownToc`, and section bodies with
9
- * `extractLineRange` all already in the codebase. The parser walks the
10
- * heading list once to assemble a `WorkflowDocument` with `SourceRef`
11
- * line spans, accumulating `WorkflowError`s rather than throwing.
7
+ * One frontmatter+body parser replaces the two prior format grammars: the
8
+ * classic `# Workflow:` / `## Step:` markdown parser, and the YAML program's
9
+ * field validator. Composition over invention: the frontmatter block is
10
+ * parsed with the `yaml` package's `parseDocument` + `LineCounter` (best-effort
11
+ * per-key line anchoring), the body's heading list with `parseMarkdownToc`
12
+ * (already fence-aware) — both already in the codebase. The parser
13
+ * accumulates `WorkflowError`s rather than throwing.
14
+ *
15
+ * Frontmatter carries the orchestration graph (params/defaults/budget/steps);
16
+ * the body carries per-step prose, joined to the graph by step id. Body rules
17
+ * (spec §2.2), exactly three:
18
+ *
19
+ * 1. Every level-2 heading must be `## <step-id>` for a DECLARED step,
20
+ * exactly (fenced code blocks are skipped when scanning for headings).
21
+ * 2. A unit/map step MUST have a section (its instructions, byte-exact to
22
+ * the next H2 or EOF); a route step MAY. Everything before the first H2
23
+ * is free preamble.
24
+ * 3. Inside a step section, an optional `### gate` sub-heading starts the
25
+ * step's gate rubric (to the section end) — the format's single
26
+ * reserved marker. Omitted or empty rubric text skips validation.
27
+ *
28
+ * Prose (instructions, gate rubrics, preamble) is NEVER templated or scanned
29
+ * for reference syntax — it reaches the dispatched unit byte-exact. Only
30
+ * three whole-value frontmatter positions carry the closed reference grammar
31
+ * (`program/expressions.ts`): `map.over`, `route.input`, `inputs[]`.
12
32
  */
13
- import { parse as yamlParse } from "yaml";
33
+ import { LineCounter, parseDocument } from "yaml";
14
34
  import { parseFrontmatterBlock } from "../core/asset/frontmatter.js";
15
35
  import { parseMarkdownToc } from "../core/asset/markdown.js";
16
- import { utf8Bytes, WORKFLOW_MAX_SOURCE_BYTES } from "./resource-limits.js";
36
+ import { formatExtraParamsIssue, validateExtraParams } from "../core/extra-params.js";
37
+ import { parseReference } from "./program/expressions.js";
38
+ import { PROGRAM_ISOLATION_KINDS, PROGRAM_ON_ERROR, PROGRAM_PARAM_NAME_PATTERN, PROGRAM_REDUCERS, PROGRAM_RETRY_REASONS, PROGRAM_STEP_ID_PATTERN, } from "./program/schema.js";
39
+ import { jsonBytes, utf8Bytes, WORKFLOW_MAX_EXTRA_PARAMS_BYTES, WORKFLOW_MAX_INPUTS, WORKFLOW_MAX_MAP_EXPANSION, WORKFLOW_MAX_PARAMS, WORKFLOW_MAX_ROUTE_BRANCHES, WORKFLOW_MAX_SCHEMA_BYTES, WORKFLOW_MAX_SOURCE_BYTES, WORKFLOW_MAX_STEPS, } from "./resource-limits.js";
17
40
  import { WORKFLOW_SCHEMA_VERSION, } from "./schema.js";
18
41
  import { runSemanticChecks } from "./validator.js";
19
- const WORKFLOW_TITLE_PREFIX = "Workflow:";
20
- const STEP_PREFIX = "Step:";
21
- const STEP_ID_LINE = /^Step ID:\s+(.+?)\s*$/;
22
- const BULLET_LINE = /^[-*]\s+(.+)$/;
23
- const SUBSECTION_INSTRUCTIONS = "Instructions";
24
- const SUBSECTION_COMPLETION_CRITERIA = "Completion Criteria";
42
+ /** Envelope keys every AKM markdown asset carries ($ref'd from schemas/akm-asset-envelope.json). */
43
+ const ENVELOPE_KEYS = [
44
+ "type",
45
+ "description",
46
+ "tags",
47
+ "when_to_use",
48
+ "xrefs",
49
+ "updated",
50
+ "timestamp",
51
+ "generated",
52
+ "verified",
53
+ "provenance",
54
+ "status",
55
+ "stale_after",
56
+ ];
57
+ const WORKFLOW_KEYS = ["params", "defaults", "budget", "steps"];
58
+ const TOP_LEVEL_KEYS = [...ENVELOPE_KEYS, ...WORKFLOW_KEYS];
59
+ const DEFAULTS_KEYS = ["engine", "model", "timeout", "on_error", "llm"];
60
+ const BUDGET_KEYS = ["max_tokens", "max_units"];
61
+ const STEP_KEYS = ["id", "unit", "map", "route", "inputs", "output", "gate"];
62
+ const UNIT_KEYS = ["engine", "model", "llm", "timeout", "retry", "on_error", "output", "env", "isolation"];
63
+ const MAP_KEYS = ["over", "concurrency", "reducer", "unit"];
64
+ const ROUTE_KEYS = ["input", "when", "default"];
65
+ const RETRY_KEYS = ["max", "on"];
66
+ const GATE_KEYS = ["max_loops"];
67
+ const ROUTE_BRANCH_KEYS = ["match", "step"];
68
+ const ACTOR_STAMP_KEYS = ["by", "at"];
69
+ const TIMEOUT_VALUE = /^(\d+)(ms|s|m)?$/;
70
+ const TIMEOUT_HINT = `Use "<n>ms", "<n>s", "<n>m" (e.g. "10m"), or "none"`;
71
+ const LIFECYCLE_STATUSES = new Set(["draft", "stable", "deprecated"]);
25
72
  /**
26
- * Cheap structural probe for the matcher. Returns true if the body has the
27
- * unmistakable shape of a workflow file. Used in `src/indexer/matchers.ts` so
28
- * the matcher and parser cannot drift.
73
+ * Cheap structural probe retained ONLY for callers that still need a fast
74
+ * "is this workflow-shaped" content check without touching the filesystem's
75
+ * directory. Recognition itself no longer sniffs content (spec §2.5) — this
76
+ * is a best-effort convenience for content-only contexts (e.g. a proposal
77
+ * whose ref carries no path). It looks for `type: workflow` in frontmatter.
29
78
  */
30
- export function looksLikeWorkflow(body) {
31
- const structuralBody = stripFencedCodeBlocks(body);
32
- return (/^#\s+Workflow:\s+/m.test(structuralBody) &&
33
- /^##\s+Step:\s+/m.test(structuralBody) &&
34
- /^Step ID:\s+/m.test(structuralBody) &&
35
- /^###\s+Instructions\s*$/m.test(structuralBody));
36
- }
37
- function stripFencedCodeBlocks(body) {
38
- let inFence = false;
39
- const lines = body.split(/\r?\n/);
40
- const stripped = [];
41
- for (const line of lines) {
42
- if (/^\s*```/.test(line) || /^\s*~~~/.test(line)) {
43
- inFence = !inFence;
44
- stripped.push("");
45
- continue;
46
- }
47
- stripped.push(inFence ? "" : line);
48
- }
49
- return stripped.join("\n");
79
+ export function looksLikeWorkflow(raw) {
80
+ const fmBlock = parseFrontmatterBlock(raw);
81
+ if (!fmBlock)
82
+ return false;
83
+ return /^type:\s*['"]?workflow['"]?\s*(#.*)?$/m.test(fmBlock.frontmatter);
50
84
  }
51
85
  export function parseWorkflow(markdown, source) {
52
86
  if (utf8Bytes(markdown) > WORKFLOW_MAX_SOURCE_BYTES) {
@@ -60,246 +94,222 @@ export function parseWorkflow(markdown, source) {
60
94
  const lines = markdown.split(/\r?\n/);
61
95
  const totalLines = lines.length;
62
96
  const fmBlock = parseFrontmatterBlock(markdown);
63
- const frontmatterEndLine = fmBlock ? Math.max(1, fmBlock.bodyStartLine - 1) : 1;
64
- const fmData = readFrontmatter(fmBlock?.frontmatter, errors);
65
- const description = readDescription(fmData);
66
- const tags = readTags(fmData, errors, frontmatterEndLine);
67
- const parameters = readParameters(fmData, errors, frontmatterEndLine, path);
68
- const toc = parseMarkdownToc(markdown);
69
- const { title, titleLine } = extractTitle(toc.headings, errors);
70
- // Disallow stray level-1 and non-Step level-2 headings.
71
- for (const h of toc.headings) {
72
- if (h.level === 1 && !h.text.startsWith(WORKFLOW_TITLE_PREFIX)) {
73
- errors.push({
74
- line: h.line,
75
- message: `Unexpected top-level heading "# ${h.text}" on line ${h.line}. A workflow file may only contain one "# Workflow: <title>" heading.`,
76
- });
77
- }
78
- if (h.level === 2 && !h.text.startsWith(STEP_PREFIX)) {
79
- errors.push({
80
- line: h.line,
81
- message: `Unexpected level-2 heading "## ${h.text}" on line ${h.line}. Only "## Step: <title>" sections are allowed.`,
82
- });
83
- }
97
+ if (!fmBlock) {
98
+ return {
99
+ ok: false,
100
+ errors: [
101
+ {
102
+ line: 1,
103
+ message: `Workflow markdown must start with a YAML frontmatter block ("---" ... "---") declaring at least ` +
104
+ `a non-empty "steps" list.`,
105
+ },
106
+ ],
107
+ };
108
+ }
109
+ // The frontmatter substring always starts right after the opening "---"
110
+ // line (exactly one line), so a yaml LineCounter position over just this
111
+ // substring is always exactly one less than the real file line.
112
+ const lineOffset = 1;
113
+ const frontmatterEndLine = Math.max(1, fmBlock.bodyStartLine - 1);
114
+ const lineCounter = new LineCounter();
115
+ let doc;
116
+ try {
117
+ doc = parseDocument(fmBlock.frontmatter, { lineCounter });
118
+ }
119
+ catch (cause) {
120
+ return {
121
+ ok: false,
122
+ errors: [{ line: 2, message: `Workflow frontmatter is not valid YAML: ${describeError(cause)}` }],
123
+ };
84
124
  }
85
- const steps = extractSteps(toc.headings, lines, totalLines, path, errors);
86
- if (steps.length === 0 && titleLine > 0) {
125
+ for (const problem of doc.errors) {
126
+ const offset = Array.isArray(problem.pos) ? problem.pos[0] : 0;
87
127
  errors.push({
88
- line: titleLine,
89
- message: `Workflow has no "## Step: <title>" sections. Add at least one step.`,
128
+ line: Math.max(1, lineCounter.linePos(offset).line + lineOffset),
129
+ message: yamlErrorMessage(problem.message),
90
130
  });
91
131
  }
132
+ if (errors.length > 0)
133
+ return { ok: false, errors };
134
+ let root;
135
+ try {
136
+ root = doc.toJS();
137
+ }
138
+ catch (cause) {
139
+ return { ok: false, errors: [{ line: 2, message: `YAML expansion failed: ${describeError(cause)}` }] };
140
+ }
141
+ const lineAt = (p) => {
142
+ for (let depth = p.length; depth >= 0; depth--) {
143
+ const node = depth === 0 ? doc.contents : doc.getIn(p.slice(0, depth), true);
144
+ const range = node?.range;
145
+ if (range)
146
+ return Math.max(1, lineCounter.linePos(range[0]).line + lineOffset);
147
+ }
148
+ return 2;
149
+ };
150
+ const ctx = {
151
+ filePath: path,
152
+ errors,
153
+ lineAt,
154
+ lineAtOffset: (offset) => Math.max(1, lineCounter.linePos(offset).line + lineOffset),
155
+ nodeAt: (p) => (p.length === 0 ? doc.contents : doc.getIn(p, true)),
156
+ refAt: (p) => {
157
+ for (let depth = p.length; depth >= 0; depth--) {
158
+ const node = depth === 0 ? doc.contents : doc.getIn(p.slice(0, depth), true);
159
+ const range = node?.range;
160
+ if (range) {
161
+ const start = Math.max(1, lineCounter.linePos(range[0]).line + lineOffset);
162
+ const end = Math.max(start, lineCounter.linePos(Math.max(range[0], range[1] - 1)).line + lineOffset);
163
+ return { path, start, end };
164
+ }
165
+ }
166
+ return { path, start: frontmatterEndLine, end: frontmatterEndLine };
167
+ },
168
+ err: (p, message) => errors.push({ line: lineAt(p), message }),
169
+ errAtLine: (line, message) => errors.push({ line, message }),
170
+ };
171
+ if (root === null || root === undefined)
172
+ root = {};
173
+ if (!isPlainRecord(root)) {
174
+ return {
175
+ ok: false,
176
+ errors: [{ line: 2, message: `Workflow frontmatter must be a YAML mapping (key: value pairs).` }],
177
+ };
178
+ }
179
+ checkUnknownKeys(ctx, root, [], TOP_LEVEL_KEYS, "workflow frontmatter");
180
+ checkEnvelopeFields(ctx, root, frontmatterEndLine);
181
+ const description = typeof root.description === "string" ? root.description : undefined;
182
+ const tags = readTags(ctx, root.tags, frontmatterEndLine);
183
+ const params = parseParams(ctx, root.params);
184
+ const defaults = parseDefaults(ctx, root.defaults);
185
+ const budget = parseBudget(ctx, root.budget);
186
+ const parsedSteps = parseSteps(ctx, root.steps);
187
+ // ── Body binding ────────────────────────────────────────────────────────
188
+ const toc = parseMarkdownToc(markdown);
189
+ const declaredIds = new Set(parsedSteps.map((s) => s.id));
190
+ const { sections, preamble } = bindStepSections(toc.headings, lines, fmBlock.bodyStartLine, totalLines, path, declaredIds, errors);
191
+ const steps = parsedSteps.map((step, index) => {
192
+ const section = sections.get(step.id);
193
+ if (!section) {
194
+ if (step.route === undefined) {
195
+ errors.push({
196
+ line: step.source.start,
197
+ message: `Step "${step.id}" is a unit/map step and must have a "## ${step.id}" body section with its instructions.`,
198
+ });
199
+ }
200
+ }
201
+ else {
202
+ if (step.route === undefined && !section.instructions) {
203
+ errors.push({
204
+ line: section.headingLine,
205
+ message: `Step "${step.id}" section ("## ${step.id}") is empty. Add the step's instructions below the heading.`,
206
+ });
207
+ }
208
+ }
209
+ const gate = step.gate ?? (section?.gateRubric ? {} : undefined);
210
+ const out = {
211
+ id: step.id,
212
+ sequenceIndex: index,
213
+ ...(step.unit ? { unit: step.unit } : {}),
214
+ ...(step.map ? { map: step.map } : {}),
215
+ ...(step.route ? { route: step.route } : {}),
216
+ ...(step.inputs ? { inputs: step.inputs } : {}),
217
+ ...(step.output !== undefined ? { output: step.output } : {}),
218
+ ...(gate ? { gate } : {}),
219
+ ...(section?.instructions ? { instructions: section.instructions } : {}),
220
+ ...(section?.gateRubric ? { gateRubric: section.gateRubric } : {}),
221
+ source: step.source,
222
+ };
223
+ return out;
224
+ });
92
225
  const draft = {
93
226
  schemaVersion: WORKFLOW_SCHEMA_VERSION,
94
- title,
95
227
  ...(description ? { description } : {}),
96
228
  ...(tags ? { tags } : {}),
97
- ...(parameters ? { parameters } : {}),
229
+ ...(params ? { params } : {}),
230
+ ...(defaults ? { defaults } : {}),
231
+ ...(budget ? { budget } : {}),
98
232
  steps,
233
+ ...(preamble ? { preamble } : {}),
99
234
  source: { path, lineCount: totalLines },
100
235
  };
101
- runSemanticChecks(draft, fmData, frontmatterEndLine, errors);
102
- if (errors.length > 0) {
236
+ runSemanticChecks(draft, root, frontmatterEndLine, errors);
237
+ if (errors.length > 0)
103
238
  return { ok: false, errors: sortErrors(errors) };
104
- }
105
239
  return { ok: true, document: draft };
106
240
  }
107
- // ── Title ───────────────────────────────────────────────────────────────────
108
- function extractTitle(headings, errors) {
109
- const titleHeadings = headings.filter((h) => h.level === 1 && h.text.startsWith(WORKFLOW_TITLE_PREFIX));
110
- if (titleHeadings.length === 0) {
111
- errors.push({
112
- line: 1,
113
- message: `Workflow markdown must start with a "# Workflow: <title>" heading. Add one at the top of the file.`,
114
- });
115
- return { title: "", titleLine: 0 };
116
- }
117
- if (titleHeadings.length > 1) {
118
- for (const extra of titleHeadings.slice(1)) {
119
- errors.push({
120
- line: extra.line,
121
- message: `Found a second "# Workflow:" heading on line ${extra.line}. A workflow file must contain exactly one.`,
122
- });
123
- }
124
- }
125
- const first = titleHeadings[0];
126
- const title = first.text.slice(WORKFLOW_TITLE_PREFIX.length).trim();
127
- if (!title) {
128
- errors.push({
129
- line: first.line,
130
- message: `The "# Workflow:" heading on line ${first.line} is missing a title. Use "# Workflow: <title>".`,
131
- });
132
- }
133
- return { title, titleLine: first.line };
134
- }
135
- // ── Steps ───────────────────────────────────────────────────────────────────
136
- function extractSteps(headings, lines, totalLines, path, errors) {
137
- const steps = [];
138
- let sequenceIndex = 0;
241
+ function bindStepSections(headings, lines, bodyStartLine, totalLines, path, declaredIds, errors) {
242
+ const sections = new Map();
243
+ const h2s = headings.filter((h) => h.level === 2);
244
+ const firstH2Line = h2s[0]?.line;
245
+ const preambleRaw = firstH2Line
246
+ ? sliceLines(lines, bodyStartLine, firstH2Line - 1).trim()
247
+ : sliceLines(lines, bodyStartLine, totalLines).trim();
139
248
  for (let i = 0; i < headings.length; i++) {
140
249
  const h = headings[i];
141
- if (h.level !== 2 || !h.text.startsWith(STEP_PREFIX))
250
+ if (h.level !== 2)
142
251
  continue;
143
- const stepTitle = h.text.slice(STEP_PREFIX.length).trim();
144
- if (!stepTitle) {
252
+ if (!declaredIds.has(h.text)) {
145
253
  errors.push({
146
254
  line: h.line,
147
- message: `The "## Step:" heading on line ${h.line} is missing a title. Use "## Step: <title>".`,
255
+ message: `Unexpected level-2 heading "## ${h.text}" on line ${h.line} no step "${h.text}" is declared in frontmatter "steps:". Level-2 headings must exactly match a declared step id.`,
148
256
  });
149
257
  continue;
150
258
  }
151
- const stepEnd = Math.min(findNextSiblingOrParentLine(headings, i, 2) - 1, totalLines);
152
- const stepSource = { path, start: h.line, end: stepEnd };
153
- const subsections = collectSubsections(headings, i, stepEnd);
154
- const stepIdSearchEnd = subsections.length > 0 ? subsections[0].headingLine - 1 : stepEnd;
155
- const stepId = scanStepId(lines, h.line + 1, stepIdSearchEnd, stepTitle, errors);
156
- const { instructions, completionCriteria } = collectStepBody(subsections, lines, path, stepTitle, errors);
157
- if (!stepId)
158
- continue; // scanStepId already pushed the missing-id error
159
- if (!instructions) {
259
+ if (sections.has(h.text)) {
160
260
  errors.push({
161
261
  line: h.line,
162
- message: `Step "${stepTitle}" is missing the required "### Instructions" section. Add one under the step.`,
262
+ message: `Step "${h.text}" has more than one "## ${h.text}" section (first on line ${sections.get(h.text).headingLine}). Keep only one.`,
163
263
  });
164
264
  continue;
165
265
  }
166
- steps.push({
167
- id: stepId,
168
- title: stepTitle,
169
- sequenceIndex: sequenceIndex++,
170
- instructions,
171
- ...(completionCriteria ? { completionCriteria } : {}),
172
- source: stepSource,
173
- });
174
- }
175
- return steps;
176
- }
177
- function collectSubsections(headings, stepIndex, stepEnd) {
178
- const subs = [];
179
- for (let j = stepIndex + 1; j < headings.length; j++) {
180
- const sub = headings[j];
181
- if (sub.level <= 2)
182
- break;
183
- if (sub.level !== 3)
184
- continue;
185
- const next = headings[j + 1];
186
- const rawEnd = next ? next.line - 1 : stepEnd;
187
- subs.push({
188
- name: sub.text,
189
- headingLine: sub.line,
190
- bodyStart: sub.line + 1,
191
- bodyEnd: Math.min(rawEnd, stepEnd),
192
- });
193
- }
194
- return subs;
195
- }
196
- function collectStepBody(subsections, lines, path, stepTitle, errors) {
197
- let instructions;
198
- let completionCriteria;
199
- for (const sub of subsections) {
200
- if (sub.name === SUBSECTION_INSTRUCTIONS) {
201
- if (instructions) {
202
- errors.push({
203
- line: sub.headingLine,
204
- message: `Step "${stepTitle}" has more than one "### Instructions" section (line ${sub.headingLine}). Keep only one.`,
205
- });
206
- continue;
207
- }
208
- const text = sliceLines(lines, sub.bodyStart, sub.bodyEnd).trim();
209
- if (!text) {
210
- errors.push({
211
- line: sub.headingLine,
212
- message: `Step "${stepTitle}" has an empty "### Instructions" section. Add the instructions text below the heading.`,
213
- });
214
- continue;
215
- }
216
- instructions = {
217
- text,
218
- source: { path, start: sub.bodyStart, end: sub.bodyEnd },
219
- };
220
- continue;
266
+ const sectionEnd = findNextHeadingAtOrAboveLevel(headings, i, 2, totalLines);
267
+ const gate = findGateSubsection(headings, i, sectionEnd, path, h.text, errors);
268
+ const instructionsEnd = gate ? gate.headingLine - 1 : sectionEnd;
269
+ const instructionsText = sliceProseLines(lines, h.line + 1, instructionsEnd);
270
+ const section = { headingLine: h.line };
271
+ if (instructionsText) {
272
+ section.instructions = { text: instructionsText, source: { path, start: h.line + 1, end: instructionsEnd } };
221
273
  }
222
- if (sub.name === SUBSECTION_COMPLETION_CRITERIA) {
223
- if (completionCriteria) {
224
- errors.push({
225
- line: sub.headingLine,
226
- message: `Step "${stepTitle}" has more than one "### Completion Criteria" section (line ${sub.headingLine}). Keep only one.`,
227
- });
228
- continue;
274
+ if (gate) {
275
+ const gateText = sliceProseLines(lines, gate.bodyStart, gate.bodyEnd);
276
+ if (gateText) {
277
+ section.gateRubric = { text: gateText, source: { path, start: gate.bodyStart, end: gate.bodyEnd } };
229
278
  }
230
- const items = collectBullets(lines, sub.bodyStart, sub.bodyEnd, path);
231
- if (items.length === 0) {
232
- errors.push({
233
- line: sub.headingLine,
234
- message: `Step "${stepTitle}" has an empty "### Completion Criteria" section. Add at least one "- criterion" bullet.`,
235
- });
236
- continue;
237
- }
238
- completionCriteria = items;
239
- continue;
240
279
  }
241
- errors.push({
242
- line: sub.headingLine,
243
- message: `Step "${stepTitle}" has an unknown "### ${sub.name}" section. Supported sections: ` +
244
- `"### Instructions", "### Completion Criteria". For orchestrated workflows (runners, ` +
245
- `fan-out, routing), author a YAML workflow program instead — see \`akm workflow template --yaml\`.`,
246
- });
280
+ sections.set(h.text, section);
247
281
  }
248
- return {
249
- ...(instructions ? { instructions } : {}),
250
- ...(completionCriteria ? { completionCriteria } : {}),
251
- };
282
+ return { sections, preamble: preambleRaw || undefined };
252
283
  }
253
- function scanStepId(lines, startLineInclusive, endLineInclusive, stepTitle, errors) {
254
- let foundId;
255
- let foundLine = -1;
256
- for (let lineNum = startLineInclusive; lineNum <= endLineInclusive; lineNum++) {
257
- const trimmed = (lines[lineNum - 1] ?? "").trim();
258
- if (!trimmed)
259
- continue;
260
- const match = trimmed.match(STEP_ID_LINE);
261
- if (!match)
284
+ /** Find a step section's `### gate` sub-heading, if any — the format's single reserved marker. */
285
+ function findGateSubsection(headings, stepHeadingIndex, sectionEnd, path, stepId, errors) {
286
+ let found;
287
+ for (let j = stepHeadingIndex + 1; j < headings.length; j++) {
288
+ const h = headings[j];
289
+ if (h.line > sectionEnd)
290
+ break;
291
+ if (h.level <= 2)
292
+ break; // next step / preamble heading — section already bounded here
293
+ if (h.level !== 3 || h.text !== "gate")
262
294
  continue;
263
- if (foundId !== undefined) {
295
+ if (found) {
264
296
  errors.push({
265
- line: lineNum,
266
- message: `Step "${stepTitle}" has more than one "Step ID:" line (first on line ${foundLine}). Keep only one.`,
297
+ line: h.line,
298
+ message: `Step "${stepId}" has more than one "### gate" sub-heading (first on line ${found.headingLine}). Keep only one.`,
267
299
  });
268
300
  continue;
269
301
  }
270
- foundId = match[1].trim();
271
- foundLine = lineNum;
272
- }
273
- if (!foundId) {
274
- errors.push({
275
- line: startLineInclusive,
276
- message: `Step "${stepTitle}" is missing a "Step ID: <id>" line. Add one between the step heading and its subsections.`,
277
- });
278
- }
279
- return foundId;
280
- }
281
- function collectBullets(lines, startLineInclusive, endLineInclusive, path) {
282
- const items = [];
283
- for (let lineNum = startLineInclusive; lineNum <= endLineInclusive; lineNum++) {
284
- const trimmed = (lines[lineNum - 1] ?? "").trim();
285
- if (!trimmed)
286
- continue;
287
- const match = trimmed.match(BULLET_LINE);
288
- if (!match)
289
- continue;
290
- items.push({
291
- text: match[1].trim(),
292
- source: { path, start: lineNum, end: lineNum },
293
- });
302
+ found = { headingLine: h.line, bodyStart: h.line + 1, bodyEnd: sectionEnd };
294
303
  }
295
- return items;
304
+ void path;
305
+ return found;
296
306
  }
297
- function findNextSiblingOrParentLine(headings, fromIndex, level) {
307
+ function findNextHeadingAtOrAboveLevel(headings, fromIndex, level, totalLines) {
298
308
  for (let i = fromIndex + 1; i < headings.length; i++) {
299
309
  if (headings[i].level <= level)
300
- return headings[i].line;
310
+ return headings[i].line - 1;
301
311
  }
302
- return Number.MAX_SAFE_INTEGER;
312
+ return totalLines;
303
313
  }
304
314
  function sliceLines(lines, startLineInclusive, endLineInclusive) {
305
315
  if (endLineInclusive < startLineInclusive)
@@ -308,99 +318,654 @@ function sliceLines(lines, startLineInclusive, endLineInclusive) {
308
318
  const e = Math.min(endLineInclusive, lines.length);
309
319
  return lines.slice(s - 1, e).join("\n");
310
320
  }
311
- // ── Frontmatter ─────────────────────────────────────────────────────────────
312
- function readFrontmatter(frontmatter, errors) {
313
- if (!frontmatter)
314
- return {};
315
- let parsed;
316
- try {
317
- parsed = yamlParse(frontmatter);
321
+ function sliceProseLines(lines, startLineInclusive, endLineInclusive) {
322
+ let start = Math.max(1, startLineInclusive);
323
+ let end = Math.min(endLineInclusive, lines.length);
324
+ while (start <= end && /^\s*$/.test(lines[start - 1]))
325
+ start++;
326
+ while (end >= start && /^\s*$/.test(lines[end - 1]))
327
+ end--;
328
+ return sliceLines(lines, start, end);
329
+ }
330
+ // ---------------------------------------------------------------------------
331
+ // Envelope fields
332
+ // ---------------------------------------------------------------------------
333
+ function checkEnvelopeFields(ctx, root, fmEndLine) {
334
+ if (root.type !== undefined && root.type !== "workflow") {
335
+ ctx.err(["type"], `Workflow frontmatter "type" must be "workflow" (got ${JSON.stringify(root.type)}).`);
318
336
  }
319
- catch (err) {
320
- const msg = err instanceof Error ? err.message : String(err);
321
- errors.push({
322
- line: 1,
323
- message: `Workflow frontmatter is not valid YAML: ${msg}`,
324
- });
325
- return {};
337
+ if (root.when_to_use !== undefined && typeof root.when_to_use !== "string") {
338
+ ctx.err(["when_to_use"], `Workflow frontmatter "when_to_use" must be a string.`);
326
339
  }
327
- if (parsed === null || parsed === undefined)
328
- return {};
329
- if (typeof parsed !== "object" || Array.isArray(parsed)) {
330
- errors.push({
331
- line: 1,
332
- message: `Workflow frontmatter must be a YAML mapping (key: value pairs). Use "key: value" lines between the --- markers.`,
333
- });
334
- return {};
340
+ if (root.description !== undefined && typeof root.description !== "string") {
341
+ ctx.err(["description"], `Workflow frontmatter "description" must be a string.`);
342
+ }
343
+ checkXrefs(ctx, root.xrefs, fmEndLine);
344
+ if (root.updated !== undefined && typeof root.updated !== "string") {
345
+ ctx.err(["updated"], `Workflow frontmatter "updated" must be a string.`);
346
+ }
347
+ if (root.timestamp !== undefined && typeof root.timestamp !== "string") {
348
+ ctx.err(["timestamp"], `Workflow frontmatter "timestamp" must be a string.`);
349
+ }
350
+ checkActorStamp(ctx, root.generated, ["generated"], `"generated"`);
351
+ if (root.verified !== undefined) {
352
+ if (Array.isArray(root.verified)) {
353
+ root.verified.forEach((entry, i) => {
354
+ checkActorStamp(ctx, entry, ["verified", i], `"verified"`);
355
+ });
356
+ }
357
+ else {
358
+ checkActorStamp(ctx, root.verified, ["verified"], `"verified"`);
359
+ }
360
+ }
361
+ if (root.provenance !== undefined && !isPlainRecord(root.provenance)) {
362
+ ctx.err(["provenance"], `Workflow frontmatter "provenance" must be a mapping.`);
363
+ }
364
+ if (root.status !== undefined && (typeof root.status !== "string" || !LIFECYCLE_STATUSES.has(root.status))) {
365
+ ctx.err(["status"], `Workflow frontmatter "status" must be one of: draft, stable, deprecated.`);
366
+ }
367
+ if (root.stale_after !== undefined && typeof root.stale_after !== "string") {
368
+ ctx.err(["stale_after"], `Workflow frontmatter "stale_after" must be a string.`);
369
+ }
370
+ }
371
+ function checkActorStamp(ctx, value, path, label) {
372
+ if (value === undefined)
373
+ return;
374
+ if (!isPlainRecord(value)) {
375
+ ctx.err(path, `Workflow frontmatter ${label} must be a mapping with a non-empty "by".`);
376
+ return;
377
+ }
378
+ checkUnknownKeys(ctx, value, path, ACTOR_STAMP_KEYS, `${label} actor stamp`);
379
+ if (typeof value.by !== "string" || value.by.length === 0) {
380
+ ctx.err([...path, "by"], `Workflow frontmatter ${label} must be a mapping with a non-empty "by".`);
381
+ }
382
+ if (value.at !== undefined && typeof value.at !== "string") {
383
+ ctx.err([...path, "at"], `Workflow frontmatter ${label} actor stamp "at" must be a string.`);
335
384
  }
336
- return parsed;
337
385
  }
338
- function readDescription(data) {
339
- const v = data.description;
340
- if (typeof v !== "string")
386
+ function readTags(ctx, value, fmEndLine) {
387
+ if (value === undefined)
388
+ return undefined;
389
+ if (!Array.isArray(value) || !value.every((tag) => typeof tag === "string" && tag.trim().length > 0)) {
390
+ ctx.errAtLine(fmEndLine, `Workflow frontmatter "tags" must be an array of non-empty strings.`);
341
391
  return undefined;
342
- const trimmed = v.trim();
343
- return trimmed || undefined;
392
+ }
393
+ return value.map((tag) => tag.trim());
394
+ }
395
+ function checkXrefs(ctx, value, fmEndLine) {
396
+ if (value === undefined)
397
+ return;
398
+ if (!Array.isArray(value) || !value.every((ref) => typeof ref === "string")) {
399
+ ctx.errAtLine(fmEndLine, `Workflow frontmatter "xrefs" must be an array of canonical asset refs.`);
400
+ }
344
401
  }
345
- function readTags(data, errors, fmEndLine) {
346
- const v = data.tags;
347
- if (v === undefined || v === null)
402
+ // ---------------------------------------------------------------------------
403
+ // Top-level sections (frontmatter graph)
404
+ // ---------------------------------------------------------------------------
405
+ function parseParams(ctx, raw) {
406
+ if (raw === undefined)
407
+ return undefined;
408
+ if (!isPlainRecord(raw)) {
409
+ ctx.err(["params"], `"params" must be a mapping of param name to a JSON Schema object (e.g. changed_files: { type: array }).`);
348
410
  return undefined;
349
- if (typeof v === "string") {
350
- const t = v.trim();
351
- return t ? [t] : undefined;
352
411
  }
353
- if (!Array.isArray(v) || !v.every((tag) => typeof tag === "string" && tag.trim().length > 0)) {
354
- errors.push({
355
- line: fmEndLine,
356
- message: `Workflow frontmatter "tags" must be a string or a list of non-empty strings.`,
357
- });
412
+ if (Object.keys(raw).length > WORKFLOW_MAX_PARAMS) {
413
+ ctx.err(["params"], `"params" must contain at most ${WORKFLOW_MAX_PARAMS} entries.`);
414
+ }
415
+ const params = {};
416
+ for (const [paramName, value] of Object.entries(raw)) {
417
+ if (!PROGRAM_PARAM_NAME_PATTERN.test(paramName)) {
418
+ ctx.err(["params", paramName], `Param name "${paramName}" is invalid. Use letters, digits, and underscores, starting with a letter or underscore, so "params.${paramName}" can address it.`);
419
+ continue;
420
+ }
421
+ if (!isPlainRecord(value)) {
422
+ ctx.err(["params", paramName], `Param "${paramName}" must be a JSON Schema object (e.g. { type: string }).`);
423
+ continue;
424
+ }
425
+ params[paramName] = value;
426
+ }
427
+ return Object.keys(params).length > 0 ? params : undefined;
428
+ }
429
+ function parseDefaults(ctx, raw) {
430
+ if (raw === undefined)
431
+ return undefined;
432
+ const path = ["defaults"];
433
+ if (!isPlainRecord(raw)) {
434
+ ctx.err(path, `"defaults" must be a mapping with any of: ${DEFAULTS_KEYS.join(", ")}.`);
358
435
  return undefined;
359
436
  }
360
- return v.map((tag) => tag.trim());
437
+ checkUnknownKeys(ctx, raw, path, DEFAULTS_KEYS, `"defaults"`);
438
+ const defaults = {};
439
+ if (raw.engine !== undefined) {
440
+ if (typeof raw.engine === "string" && raw.engine.trim() !== "")
441
+ defaults.engine = raw.engine.trim();
442
+ else
443
+ ctx.err([...path, "engine"], `"defaults.engine" must be a non-empty engine name.`);
444
+ }
445
+ if (raw.model !== undefined) {
446
+ if (typeof raw.model === "string" && raw.model.trim() !== "")
447
+ defaults.model = raw.model.trim();
448
+ else
449
+ ctx.err([...path, "model"], `"defaults.model" must be a non-empty string (a model alias or exact id).`);
450
+ }
451
+ const timeoutMs = parseTimeoutField(ctx, raw.timeout, [...path, "timeout"], `"defaults.timeout"`);
452
+ if (timeoutMs !== undefined)
453
+ defaults.timeoutMs = timeoutMs;
454
+ const onError = parseEnumField(ctx, raw.on_error, [...path, "on_error"], `"defaults.on_error"`, PROGRAM_ON_ERROR);
455
+ if (onError !== undefined)
456
+ defaults.onError = onError;
457
+ const llm = parseLlmOverrides(ctx, raw.llm, [...path, "llm"], `"defaults.llm"`);
458
+ if (llm !== undefined)
459
+ defaults.llm = llm;
460
+ return Object.keys(defaults).length > 0 ? defaults : undefined;
361
461
  }
362
- function readParameters(data, errors, fmEndLine, path) {
363
- const v = data.params;
364
- if (v === undefined || v === null)
462
+ function parseBudget(ctx, raw) {
463
+ if (raw === undefined)
365
464
  return undefined;
366
- if (typeof v !== "object" || Array.isArray(v)) {
367
- errors.push({
368
- line: fmEndLine,
369
- message: `Workflow frontmatter "params" must be a mapping of parameter names to descriptions.`,
370
- });
465
+ const path = ["budget"];
466
+ if (!isPlainRecord(raw)) {
467
+ ctx.err(path, `"budget" must be a mapping with any of: ${BUDGET_KEYS.join(", ")}.`);
371
468
  return undefined;
372
469
  }
373
- const entries = Object.entries(v);
374
- if (entries.length === 0)
470
+ checkUnknownKeys(ctx, raw, path, BUDGET_KEYS, `"budget"`);
471
+ const budget = {};
472
+ if (raw.max_tokens !== undefined) {
473
+ if (typeof raw.max_tokens === "number" && Number.isInteger(raw.max_tokens) && raw.max_tokens >= 1) {
474
+ budget.maxTokens = raw.max_tokens;
475
+ }
476
+ else {
477
+ ctx.err([...path, "max_tokens"], `"budget.max_tokens" must be an integer >= 1.`);
478
+ }
479
+ }
480
+ if (raw.max_units !== undefined) {
481
+ if (typeof raw.max_units === "number" &&
482
+ Number.isInteger(raw.max_units) &&
483
+ raw.max_units >= 1 &&
484
+ raw.max_units <= WORKFLOW_MAX_MAP_EXPANSION) {
485
+ budget.maxUnits = raw.max_units;
486
+ }
487
+ else {
488
+ ctx.err([...path, "max_units"], `"budget.max_units" must be an integer from 1 through ${WORKFLOW_MAX_MAP_EXPANSION}.`);
489
+ }
490
+ }
491
+ return Object.keys(budget).length > 0 ? budget : undefined;
492
+ }
493
+ function parseSteps(ctx, raw) {
494
+ if (!Array.isArray(raw) || raw.length === 0) {
495
+ ctx.err(["steps"], `"steps" is required and must be a list with at least one step.`);
496
+ return [];
497
+ }
498
+ if (raw.length > WORKFLOW_MAX_STEPS) {
499
+ ctx.err(["steps"], `"steps" must contain at most ${WORKFLOW_MAX_STEPS} entries.`);
500
+ }
501
+ // First pass: collect ids so route targets can be checked against ALL steps
502
+ // (including ones that fail their own validation).
503
+ const idIndex = new Map();
504
+ raw.forEach((rawStep, index) => {
505
+ if (isPlainRecord(rawStep) && typeof rawStep.id === "string" && !idIndex.has(rawStep.id)) {
506
+ idIndex.set(rawStep.id, index);
507
+ }
508
+ });
509
+ const steps = [];
510
+ const seenIds = new Map();
511
+ const routeChecks = [];
512
+ raw.forEach((rawStep, index) => {
513
+ const path = ["steps", index];
514
+ if (!isPlainRecord(rawStep)) {
515
+ ctx.err(path, `Step ${index + 1} must be a mapping with an "id".`);
516
+ return;
517
+ }
518
+ const label = typeof rawStep.id === "string" && rawStep.id !== "" ? `Step "${rawStep.id}"` : `Step ${index + 1}`;
519
+ checkUnknownKeys(ctx, rawStep, path, STEP_KEYS, label);
520
+ let id = "";
521
+ if (typeof rawStep.id !== "string" || rawStep.id === "") {
522
+ ctx.err([...path, "id"], `${label} requires a non-empty string "id".`);
523
+ }
524
+ else if (!PROGRAM_STEP_ID_PATTERN.test(rawStep.id)) {
525
+ ctx.err([...path, "id"], `${label} has an invalid id "${rawStep.id}". A step id cannot be referenced from steps.${rawStep.id}.output ` +
526
+ `unless it matches [A-Za-z_][A-Za-z0-9_-]* (a letter or underscore first, then letters, digits, ` +
527
+ `underscores, or dashes; no dots, no leading digit).`);
528
+ }
529
+ else {
530
+ id = rawStep.id;
531
+ const firstIndex = seenIds.get(id);
532
+ if (firstIndex !== undefined) {
533
+ ctx.err([...path, "id"], `Duplicate step id "${id}" (first used by step ${firstIndex + 1}). Step ids must be unique.`);
534
+ }
535
+ else {
536
+ seenIds.set(id, index);
537
+ }
538
+ }
539
+ const declaredKinds = ["map", "route"].filter((kind) => rawStep[kind] !== undefined);
540
+ if (declaredKinds.length > 1) {
541
+ ctx.err(path, `${label} must declare at most one of "map" or "route" (found ${declaredKinds.join(" + ")}).`);
542
+ }
543
+ const isRoute = rawStep.route !== undefined;
544
+ const isMapStep = rawStep.map !== undefined;
545
+ if (isRoute && rawStep.unit !== undefined) {
546
+ ctx.err(path, `${label} is a route step and cannot also declare "unit" (route steps dispatch no unit).`);
547
+ }
548
+ if (isMapStep && rawStep.unit !== undefined) {
549
+ ctx.err(path, `${label} is a map step; the per-item dispatch-override bag belongs at "map.unit", not top-level "unit".`);
550
+ }
551
+ if (isRoute && rawStep.inputs !== undefined) {
552
+ ctx.err(path, `${label} is a route step and cannot declare "inputs" (route steps dispatch no unit).`);
553
+ }
554
+ const unit = rawStep.unit !== undefined && !isRoute && !isMapStep
555
+ ? parseUnit(ctx, rawStep.unit, [...path, "unit"], label)
556
+ : undefined;
557
+ const map = isMapStep ? parseMap(ctx, rawStep.map, [...path, "map"], label) : undefined;
558
+ const route = isRoute ? parseRoute(ctx, rawStep.route, [...path, "route"], label, index, routeChecks) : undefined;
559
+ const inputs = !isRoute ? parseInputs(ctx, rawStep.inputs, [...path, "inputs"], label) : undefined;
560
+ const output = parseSchemaObject(ctx, rawStep.output, [...path, "output"], `${label} "output"`);
561
+ const gate = rawStep.gate !== undefined ? parseGate(ctx, rawStep.gate, [...path, "gate"], label) : undefined;
562
+ const step = { id, source: ctx.refAt(path) };
563
+ if (unit)
564
+ step.unit = unit;
565
+ if (map)
566
+ step.map = map;
567
+ if (route)
568
+ step.route = route;
569
+ if (inputs)
570
+ step.inputs = inputs;
571
+ if (output !== undefined)
572
+ step.output = output;
573
+ if (gate !== undefined)
574
+ step.gate = gate;
575
+ steps.push(step);
576
+ });
577
+ // Route target post-pass: targets exist, come after the routing step, and
578
+ // never point back at it.
579
+ for (const check of routeChecks) {
580
+ const targets = [...check.branches.map((b) => ({ stepId: b.stepId, line: b.line }))];
581
+ if (check.defaultTarget)
582
+ targets.push(check.defaultTarget);
583
+ for (const target of targets) {
584
+ const targetIndex = idIndex.get(target.stepId);
585
+ if (targetIndex === undefined) {
586
+ ctx.errAtLine(target.line, `${check.stepLabel} routes to unknown step "${target.stepId}". Route targets must name a step id in this workflow.`);
587
+ }
588
+ else if (targetIndex === check.stepIndex) {
589
+ ctx.errAtLine(target.line, `${check.stepLabel} must not route to itself.`);
590
+ }
591
+ else if (targetIndex < check.stepIndex) {
592
+ ctx.errAtLine(target.line, `${check.stepLabel} routes backward to "${target.stepId}" (step ${targetIndex + 1}). Route targets must come after the routing step.`);
593
+ }
594
+ }
595
+ }
596
+ return steps;
597
+ }
598
+ // ---------------------------------------------------------------------------
599
+ // Step blocks
600
+ // ---------------------------------------------------------------------------
601
+ function parseUnit(ctx, raw, path, stepLabel) {
602
+ if (!isPlainRecord(raw)) {
603
+ ctx.err(path, `${stepLabel} "unit" must be a mapping (a dispatch-override bag).`);
375
604
  return undefined;
376
- const out = [];
377
- for (const [name, desc] of entries) {
378
- const trimmedName = name.trim();
379
- if (!trimmedName) {
380
- errors.push({
381
- line: fmEndLine,
382
- message: `Workflow parameter names must be non-empty.`,
383
- });
384
- continue;
605
+ }
606
+ checkUnknownKeys(ctx, raw, path, UNIT_KEYS, `${stepLabel} "unit"`);
607
+ const unit = { source: ctx.refAt(path) };
608
+ if (raw.engine !== undefined) {
609
+ if (typeof raw.engine === "string" && raw.engine.trim() !== "")
610
+ unit.engine = raw.engine.trim();
611
+ else
612
+ ctx.err([...path, "engine"], `${stepLabel} "engine" must be a non-empty engine name.`);
613
+ }
614
+ if (raw.model !== undefined) {
615
+ if (typeof raw.model === "string" && raw.model.trim() !== "")
616
+ unit.model = raw.model.trim();
617
+ else
618
+ ctx.err([...path, "model"], `${stepLabel} "model" must be a non-empty string (a model alias or exact id).`);
619
+ }
620
+ const llm = parseLlmOverrides(ctx, raw.llm, [...path, "llm"], `${stepLabel} "llm"`);
621
+ if (llm !== undefined)
622
+ unit.llm = llm;
623
+ const timeoutMs = parseTimeoutField(ctx, raw.timeout, [...path, "timeout"], `${stepLabel} "timeout"`);
624
+ if (timeoutMs !== undefined)
625
+ unit.timeoutMs = timeoutMs;
626
+ const retry = parseRetry(ctx, raw.retry, [...path, "retry"], stepLabel);
627
+ if (retry !== undefined)
628
+ unit.retry = retry;
629
+ const onError = parseEnumField(ctx, raw.on_error, [...path, "on_error"], `${stepLabel} "on_error"`, PROGRAM_ON_ERROR);
630
+ if (onError !== undefined)
631
+ unit.onError = onError;
632
+ const output = parseSchemaObject(ctx, raw.output, [...path, "output"], `${stepLabel} unit "output"`);
633
+ if (output !== undefined)
634
+ unit.output = output;
635
+ if (raw.env !== undefined) {
636
+ if (Array.isArray(raw.env) && raw.env.every((entry) => typeof entry === "string" && entry.trim() !== "")) {
637
+ unit.env = raw.env.map((entry) => entry.trim());
385
638
  }
386
- if (typeof desc !== "string" || !desc.trim()) {
387
- errors.push({
388
- line: fmEndLine,
389
- message: `Workflow parameter "${trimmedName}" must have a non-empty string description in frontmatter "params".`,
390
- });
391
- continue;
639
+ else {
640
+ ctx.err([...path, "env"], `${stepLabel} "env" must be a list of non-empty env asset refs.`);
641
+ }
642
+ }
643
+ const isolation = parseEnumField(ctx, raw.isolation, [...path, "isolation"], `${stepLabel} "isolation"`, PROGRAM_ISOLATION_KINDS);
644
+ if (isolation !== undefined)
645
+ unit.isolation = isolation;
646
+ return unit;
647
+ }
648
+ function parseMap(ctx, raw, path, stepLabel) {
649
+ if (!isPlainRecord(raw)) {
650
+ ctx.err(path, `${stepLabel} "map" must be a mapping with an "over" key.`);
651
+ return undefined;
652
+ }
653
+ checkUnknownKeys(ctx, raw, path, MAP_KEYS, `${stepLabel} "map"`);
654
+ let over = "";
655
+ if (typeof raw.over === "string" && raw.over.trim() !== "") {
656
+ over = raw.over.trim();
657
+ checkReferenceSyntax(ctx, over, [...path, "over"], `${stepLabel} "over"`);
658
+ }
659
+ else {
660
+ ctx.err([...path, "over"], `${stepLabel} "map" requires "over": a reference naming the item list (e.g. steps.discover.output.files).`);
661
+ }
662
+ let concurrency;
663
+ if (raw.concurrency !== undefined) {
664
+ if (typeof raw.concurrency === "number" && Number.isInteger(raw.concurrency) && raw.concurrency > 0) {
665
+ concurrency = raw.concurrency;
392
666
  }
393
- out.push({
394
- name: trimmedName,
395
- description: desc.trim(),
396
- // The frontmatter parser doesn't track per-key line numbers; anchor to the
397
- // frontmatter block end so editors land somewhere sensible.
398
- source: { path, start: 1, end: fmEndLine },
667
+ else {
668
+ ctx.err([...path, "concurrency"], `${stepLabel} "concurrency" must be a positive integer.`);
669
+ }
670
+ }
671
+ const reducer = parseEnumField(ctx, raw.reducer, [...path, "reducer"], `${stepLabel} "reducer"`, PROGRAM_REDUCERS);
672
+ const unit = raw.unit !== undefined ? parseUnit(ctx, raw.unit, [...path, "unit"], stepLabel) : undefined;
673
+ const map = { over };
674
+ if (concurrency !== undefined)
675
+ map.concurrency = concurrency;
676
+ if (reducer !== undefined)
677
+ map.reducer = reducer;
678
+ if (unit !== undefined)
679
+ map.unit = unit;
680
+ return map;
681
+ }
682
+ function parseRoute(ctx, raw, path, stepLabel, stepIndex, routeChecks) {
683
+ if (!isPlainRecord(raw)) {
684
+ ctx.err(path, `${stepLabel} "route" must be a mapping with "input" and "when" keys.`);
685
+ return undefined;
686
+ }
687
+ checkUnknownKeys(ctx, raw, path, ROUTE_KEYS, `${stepLabel} "route"`);
688
+ let input = "";
689
+ if (typeof raw.input === "string" && raw.input.trim() !== "") {
690
+ input = raw.input.trim();
691
+ checkReferenceSyntax(ctx, input, [...path, "input"], `${stepLabel} "route.input"`);
692
+ }
693
+ else {
694
+ ctx.err([...path, "input"], `${stepLabel} "route" requires "input": a reference naming the value to route on.`);
695
+ }
696
+ const check = { stepIndex, stepLabel, branches: [] };
697
+ const whenPath = [...path, "when"];
698
+ if (!Array.isArray(raw.when) || raw.when.length === 0) {
699
+ ctx.err(whenPath, `${stepLabel} "route" requires "when": a non-empty list of { match, step } branches (e.g. when: [{ match: pass, step: ship }]).`);
700
+ }
701
+ else {
702
+ if (raw.when.length > WORKFLOW_MAX_ROUTE_BRANCHES) {
703
+ ctx.err(whenPath, `${stepLabel} "when" must contain at most ${WORKFLOW_MAX_ROUTE_BRANCHES} branches.`);
704
+ }
705
+ const seenMatches = new Map();
706
+ raw.when.forEach((branch, i) => {
707
+ const branchPath = [...whenPath, i];
708
+ if (!isPlainRecord(branch)) {
709
+ ctx.err(branchPath, `${stepLabel} "when[${i}]" must be a mapping: { match, step }.`);
710
+ return;
711
+ }
712
+ checkUnknownKeys(ctx, branch, branchPath, ROUTE_BRANCH_KEYS, `${stepLabel} "when[${i}]"`);
713
+ const matchLine = ctx.lineAt([...branchPath, "match"]);
714
+ if (branch.match === undefined ||
715
+ (typeof branch.match !== "string" && typeof branch.match !== "number" && typeof branch.match !== "boolean")) {
716
+ ctx.err([...branchPath, "match"], `${stepLabel} "when[${i}].match" must be a string, number, or boolean.`);
717
+ return;
718
+ }
719
+ const match = String(branch.match);
720
+ if (typeof branch.step !== "string" || branch.step.trim() === "") {
721
+ ctx.err([...branchPath, "step"], `${stepLabel} "when[${i}].step" must be a step id string.`);
722
+ return;
723
+ }
724
+ const stepId = branch.step.trim();
725
+ const stepLine = ctx.lineAt([...branchPath, "step"]);
726
+ const firstLine = seenMatches.get(match);
727
+ if (firstLine !== undefined) {
728
+ ctx.errAtLine(matchLine, `${stepLabel} has a duplicate "when" match "${match}" (first declared on line ${firstLine}). Matches must be unique.`);
729
+ return;
730
+ }
731
+ seenMatches.set(match, matchLine);
732
+ check.branches.push({ match, stepId, line: stepLine });
399
733
  });
400
734
  }
735
+ let defaultStepId;
736
+ if (raw.default !== undefined) {
737
+ if (typeof raw.default === "string" && raw.default.trim() !== "") {
738
+ defaultStepId = raw.default.trim();
739
+ check.defaultTarget = { stepId: defaultStepId, line: ctx.lineAt([...path, "default"]) };
740
+ }
741
+ else {
742
+ ctx.err([...path, "default"], `${stepLabel} "route.default" must be a step id string.`);
743
+ }
744
+ }
745
+ routeChecks.push(check);
746
+ const route = { input, branches: check.branches.map(({ match, stepId }) => ({ match, stepId })) };
747
+ if (defaultStepId !== undefined)
748
+ route.defaultStepId = defaultStepId;
749
+ return route;
750
+ }
751
+ function parseInputs(ctx, raw, path, stepLabel) {
752
+ if (raw === undefined)
753
+ return undefined;
754
+ if (!Array.isArray(raw) || raw.length === 0) {
755
+ ctx.err(path, `${stepLabel} "inputs" must be a non-empty list of reference strings.`);
756
+ return undefined;
757
+ }
758
+ if (raw.length > WORKFLOW_MAX_INPUTS) {
759
+ ctx.err(path, `${stepLabel} "inputs" must contain at most ${WORKFLOW_MAX_INPUTS} entries.`);
760
+ }
761
+ const out = [];
762
+ raw.forEach((entry, i) => {
763
+ if (typeof entry !== "string" || entry.trim() === "") {
764
+ ctx.err([...path, i], `${stepLabel} "inputs[${i}]" must be a non-empty reference string.`);
765
+ return;
766
+ }
767
+ const value = entry.trim();
768
+ checkReferenceSyntax(ctx, value, [...path, i], `${stepLabel} "inputs[${i}]"`);
769
+ out.push(value);
770
+ });
401
771
  return out.length > 0 ? out : undefined;
402
772
  }
403
- // ── Error sorting ───────────────────────────────────────────────────────────
773
+ function parseGate(ctx, raw, path, stepLabel) {
774
+ if (!isPlainRecord(raw)) {
775
+ ctx.err(path, `${stepLabel} "gate" must be a mapping with any of: ${GATE_KEYS.join(", ")}.`);
776
+ return undefined;
777
+ }
778
+ checkUnknownKeys(ctx, raw, path, GATE_KEYS, `${stepLabel} "gate"`);
779
+ const gate = {};
780
+ if (raw.max_loops !== undefined) {
781
+ if (typeof raw.max_loops === "number" && Number.isInteger(raw.max_loops) && raw.max_loops >= 1) {
782
+ gate.maxLoops = raw.max_loops;
783
+ }
784
+ else {
785
+ ctx.err([...path, "max_loops"], `${stepLabel} "gate.max_loops" must be an integer >= 1.`);
786
+ }
787
+ }
788
+ return gate;
789
+ }
790
+ // ---------------------------------------------------------------------------
791
+ // Field helpers
792
+ // ---------------------------------------------------------------------------
793
+ function parseRetry(ctx, raw, path, stepLabel) {
794
+ if (raw === undefined)
795
+ return undefined;
796
+ if (!isPlainRecord(raw)) {
797
+ ctx.err(path, `${stepLabel} "retry" must be a mapping: { max: <n>, on: [<failure_reason>, …] }.`);
798
+ return undefined;
799
+ }
800
+ checkUnknownKeys(ctx, raw, path, RETRY_KEYS, `${stepLabel} "retry"`);
801
+ let ok = true;
802
+ if (!(typeof raw.max === "number" && Number.isInteger(raw.max) && raw.max >= 0)) {
803
+ ctx.err([...path, "max"], `${stepLabel} "retry.max" is required and must be a non-negative integer.`);
804
+ ok = false;
805
+ }
806
+ const on = [];
807
+ if (Array.isArray(raw.on) && raw.on.length > 0) {
808
+ raw.on.forEach((reason, i) => {
809
+ if (typeof reason === "string" && PROGRAM_RETRY_REASONS.includes(reason)) {
810
+ on.push(reason);
811
+ }
812
+ else {
813
+ ctx.err([...path, "on", i], `${stepLabel} "retry.on" has unknown failure reason ${JSON.stringify(reason)}. Valid reasons: ${PROGRAM_RETRY_REASONS.join(", ")}.`);
814
+ ok = false;
815
+ }
816
+ });
817
+ }
818
+ else {
819
+ ctx.err([...path, "on"], `${stepLabel} "retry.on" is required and must be a non-empty list of failure reasons (${PROGRAM_RETRY_REASONS.join(", ")}).`);
820
+ ok = false;
821
+ }
822
+ return ok ? { max: raw.max, on } : undefined;
823
+ }
824
+ function parseTimeoutField(ctx, raw, path, label) {
825
+ if (raw === undefined)
826
+ return undefined;
827
+ if (typeof raw === "number") {
828
+ if (Number.isInteger(raw) && raw > 0)
829
+ return raw;
830
+ ctx.err(path, `${label} has a non-positive timeout ${JSON.stringify(raw)}. ${TIMEOUT_HINT}.`);
831
+ return undefined;
832
+ }
833
+ if (typeof raw !== "string") {
834
+ ctx.err(path, `${label} must be a duration string. ${TIMEOUT_HINT}.`);
835
+ return undefined;
836
+ }
837
+ const value = raw.trim().toLowerCase();
838
+ if (value === "none")
839
+ return null;
840
+ const match = value.match(TIMEOUT_VALUE);
841
+ if (!match) {
842
+ ctx.err(path, `${label} has an invalid timeout "${raw}". ${TIMEOUT_HINT}.`);
843
+ return undefined;
844
+ }
845
+ const n = Number.parseInt(match[1], 10);
846
+ const unit = match[2] ?? "ms";
847
+ const timeoutMs = unit === "m" ? n * 60_000 : unit === "s" ? n * 1_000 : n;
848
+ if (timeoutMs <= 0) {
849
+ ctx.err(path, `${label} has a non-positive timeout "${raw}". Use a positive duration or "none".`);
850
+ return undefined;
851
+ }
852
+ return timeoutMs;
853
+ }
854
+ function parseEnumField(ctx, raw, path, label, allowed) {
855
+ if (raw === undefined)
856
+ return undefined;
857
+ if (typeof raw === "string" && allowed.includes(raw))
858
+ return raw;
859
+ ctx.err(path, `${label} must be one of: ${allowed.join(" | ")} (got ${JSON.stringify(raw)}).`);
860
+ return undefined;
861
+ }
862
+ /** Parse only invocation tuning. Connection identity belongs to a named engine. */
863
+ function parseLlmOverrides(ctx, raw, path, label) {
864
+ if (raw === undefined)
865
+ return undefined;
866
+ if (!isPlainRecord(raw)) {
867
+ ctx.err(path, `${label} must be a mapping of LLM invocation overrides.`);
868
+ return undefined;
869
+ }
870
+ const keys = [
871
+ "temperature",
872
+ "max_tokens",
873
+ "supports_json_schema",
874
+ "extra_params",
875
+ "context_length",
876
+ "enable_thinking",
877
+ ];
878
+ checkUnknownKeys(ctx, raw, path, keys, label);
879
+ const result = {};
880
+ if (raw.temperature !== undefined) {
881
+ if (typeof raw.temperature === "number" && Number.isFinite(raw.temperature))
882
+ result.temperature = raw.temperature;
883
+ else
884
+ ctx.err([...path, "temperature"], `${label}.temperature must be a finite number.`);
885
+ }
886
+ if (raw.max_tokens !== undefined) {
887
+ if (typeof raw.max_tokens === "number" && Number.isInteger(raw.max_tokens) && raw.max_tokens > 0) {
888
+ result.maxTokens = raw.max_tokens;
889
+ }
890
+ else
891
+ ctx.err([...path, "max_tokens"], `${label}.max_tokens must be a positive integer.`);
892
+ }
893
+ if (raw.supports_json_schema !== undefined) {
894
+ if (typeof raw.supports_json_schema === "boolean")
895
+ result.supportsJsonSchema = raw.supports_json_schema;
896
+ else
897
+ ctx.err([...path, "supports_json_schema"], `${label}.supports_json_schema must be a boolean.`);
898
+ }
899
+ if (raw.extra_params !== undefined) {
900
+ if (!isPlainRecord(raw.extra_params)) {
901
+ ctx.err([...path, "extra_params"], `${label}.extra_params must be a JSON object.`);
902
+ }
903
+ else {
904
+ const issues = validateExtraParams(raw.extra_params);
905
+ for (const issue of issues) {
906
+ ctx.err([...path, "extra_params", ...issue.path], `${formatExtraParamsIssue(`${label}.extra_params`, issue)}.`);
907
+ }
908
+ if (jsonBytes(raw.extra_params) > WORKFLOW_MAX_EXTRA_PARAMS_BYTES) {
909
+ ctx.err([...path, "extra_params"], `${label}.extra_params exceeds the 64 KiB resource limit.`);
910
+ }
911
+ if (issues.length === 0 && jsonBytes(raw.extra_params) <= WORKFLOW_MAX_EXTRA_PARAMS_BYTES) {
912
+ result.extraParams = raw.extra_params;
913
+ }
914
+ }
915
+ }
916
+ if (raw.context_length !== undefined) {
917
+ if (typeof raw.context_length === "number" && Number.isInteger(raw.context_length) && raw.context_length > 0) {
918
+ result.contextLength = raw.context_length;
919
+ }
920
+ else
921
+ ctx.err([...path, "context_length"], `${label}.context_length must be a positive integer.`);
922
+ }
923
+ if (raw.enable_thinking !== undefined) {
924
+ if (typeof raw.enable_thinking === "boolean")
925
+ result.enableThinking = raw.enable_thinking;
926
+ else
927
+ ctx.err([...path, "enable_thinking"], `${label}.enable_thinking must be a boolean.`);
928
+ }
929
+ return Object.keys(result).length > 0 ? result : undefined;
930
+ }
931
+ function parseSchemaObject(ctx, raw, path, label) {
932
+ if (raw === undefined)
933
+ return undefined;
934
+ if (!isPlainRecord(raw)) {
935
+ ctx.err(path, `${label} must be a JSON Schema object (e.g. { type: object, properties: { … } }).`);
936
+ return undefined;
937
+ }
938
+ if (jsonBytes(raw) > WORKFLOW_MAX_SCHEMA_BYTES) {
939
+ ctx.err(path, `${label} exceeds the 256 KiB resource limit.`);
940
+ }
941
+ return raw;
942
+ }
943
+ function checkReferenceSyntax(ctx, text, path, label) {
944
+ const result = parseReference(text);
945
+ if (!result.ok)
946
+ ctx.err(path, `${label}: ${result.message}`);
947
+ }
948
+ function checkUnknownKeys(ctx, obj, path, allowed, label) {
949
+ for (const key of Object.keys(obj)) {
950
+ if (!allowed.includes(key)) {
951
+ ctx.err([...path, key], `Unknown ${label} key "${key}". Allowed keys: ${allowed.join(", ")}.`);
952
+ }
953
+ }
954
+ }
955
+ // ---------------------------------------------------------------------------
956
+ // Utilities
957
+ // ---------------------------------------------------------------------------
958
+ function isPlainRecord(value) {
959
+ return typeof value === "object" && value !== null && !Array.isArray(value);
960
+ }
961
+ function describeError(cause) {
962
+ return cause instanceof Error ? cause.message : String(cause);
963
+ }
964
+ /** Strip the yaml package's multi-line code frame down to the first line. */
965
+ function yamlErrorMessage(message) {
966
+ const first = message.split("\n", 1)[0] ?? message;
967
+ return first.replace(/ at line \d+, column \d+:?\s*$/, "").trim();
968
+ }
404
969
  function sortErrors(errors) {
405
970
  return [...errors].sort((a, b) => a.line - b.line);
406
971
  }