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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (598) hide show
  1. package/CHANGELOG.md +1283 -22
  2. package/README.md +62 -37
  3. package/SECURITY.md +46 -31
  4. package/dist/akm +162 -38
  5. package/dist/akm-migrate +44 -0
  6. package/dist/assets/backends/schtasks-template.xml +2 -1
  7. package/dist/assets/hints/cli-hints-full.md +268 -118
  8. package/dist/assets/hints/cli-hints-short.md +87 -24
  9. package/dist/assets/{profiles → improve-strategies}/catchup.json +3 -1
  10. package/dist/assets/{profiles → improve-strategies}/consolidate.json +3 -1
  11. package/dist/assets/{profiles → improve-strategies}/default.json +6 -7
  12. package/dist/assets/improve-strategies/frequent.json +15 -0
  13. package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +4 -2
  14. package/dist/assets/{profiles → improve-strategies}/memory-focus.json +4 -1
  15. package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +5 -5
  16. package/dist/assets/{profiles → improve-strategies}/quick.json +4 -2
  17. package/dist/assets/improve-strategies/reflect-distill.json +30 -0
  18. package/dist/assets/{profiles → improve-strategies}/thorough.json +1 -1
  19. package/dist/assets/prompts/consolidate-system.md +5 -5
  20. package/dist/assets/prompts/extract-session.md +2 -6
  21. package/dist/assets/prompts/memory-infer-user.md +2 -3
  22. package/dist/assets/prompts/reflect-llm-framed-contract.md +11 -0
  23. package/dist/assets/prompts/reflect-llm-schema-contract.md +3 -0
  24. package/dist/assets/prompts/reflect-output-repair.md +3 -0
  25. package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
  26. package/dist/assets/stash-skeleton/README.md +38 -10
  27. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +8 -0
  28. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +8 -0
  29. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +14 -1
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +13 -1
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +9 -1
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +11 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +9 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +9 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +8 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +100 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/domains.md +64 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/organization.md +136 -0
  39. package/dist/assets/tasks/core/extract.yml +3 -2
  40. package/dist/assets/tasks/core/improve.yml +2 -1
  41. package/dist/assets/tasks/core/index-refresh.yml +1 -0
  42. package/dist/assets/tasks/core/sync.yml +1 -0
  43. package/dist/assets/tasks/core/version-check.yml +2 -1
  44. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
  45. package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
  46. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
  47. package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
  48. package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
  49. package/dist/assets/templates/html/health.html +5 -4
  50. package/dist/assets/workflows/workflow-template.md +31 -15
  51. package/dist/cli/invocation.js +279 -0
  52. package/dist/cli/parse-args.js +5 -90
  53. package/dist/cli/retired-commands.js +78 -0
  54. package/dist/cli/shared.js +158 -48
  55. package/dist/cli-node.mjs +2 -1
  56. package/dist/cli.js +747 -293
  57. package/dist/commands/agent/agent-dispatch.js +19 -18
  58. package/dist/commands/agent/agent-support.js +0 -24
  59. package/dist/commands/agent/contribute-cli.js +43 -97
  60. package/dist/commands/completions.js +80 -23
  61. package/dist/commands/config-cli.js +44 -281
  62. package/dist/commands/env/env-binding.js +99 -0
  63. package/dist/commands/env/env-cli.js +84 -224
  64. package/dist/commands/env/env.js +12 -163
  65. package/dist/commands/env/marker-path.js +6 -0
  66. package/dist/commands/env/secret-cli.js +45 -61
  67. package/dist/commands/env/secret.js +32 -62
  68. package/dist/commands/feedback-cli.js +179 -85
  69. package/dist/commands/health/accept-rate.js +58 -0
  70. package/dist/commands/health/advisories.js +7 -8
  71. package/dist/commands/health/checks.js +279 -94
  72. package/dist/commands/health/html-report.js +197 -578
  73. package/dist/commands/health/improve-metrics.js +277 -246
  74. package/dist/commands/health/llm-usage.js +19 -19
  75. package/dist/commands/health/md-report.js +16 -7
  76. package/dist/commands/health/metrics.js +67 -32
  77. package/dist/commands/health/renderers.js +47 -0
  78. package/dist/commands/health/report-view-model.js +508 -0
  79. package/dist/commands/health/stash-exposure.js +1 -1
  80. package/dist/commands/health/surfaces.js +16 -56
  81. package/dist/commands/health/task-runs.js +3 -67
  82. package/dist/{migrate-storage-node.mjs → commands/health/types-checks.js} +1 -5
  83. package/dist/commands/health/types-improve.js +29 -0
  84. package/dist/{output/text/save.js → commands/health/types-metrics.js} +1 -2
  85. package/dist/commands/health/types-result.js +7 -0
  86. package/dist/commands/health/types-runs.js +4 -0
  87. package/dist/commands/health/types-session-log.js +4 -0
  88. package/dist/commands/health/types-windows.js +4 -0
  89. package/dist/commands/health/types.js +26 -21
  90. package/dist/commands/health/windows.js +2 -3
  91. package/dist/commands/health.js +296 -167
  92. package/dist/commands/improve/anti-collapse.js +5 -5
  93. package/dist/commands/improve/autonomy-gate.js +68 -0
  94. package/dist/commands/improve/collapse-detector.js +65 -52
  95. package/dist/commands/improve/consolidate/chunking.js +9 -7
  96. package/dist/commands/improve/consolidate/eligibility.js +1 -23
  97. package/dist/commands/improve/consolidate/merge.js +4 -0
  98. package/dist/commands/improve/consolidate.js +454 -1354
  99. package/dist/commands/improve/content-hash.js +39 -0
  100. package/dist/commands/improve/distill/content-repair.js +4 -10
  101. package/dist/commands/improve/distill/promote-memory.js +89 -64
  102. package/dist/commands/improve/distill/quality-gate.js +118 -42
  103. package/dist/commands/improve/distill-guards.js +1 -1
  104. package/dist/commands/improve/distill-promotion-policy.js +33 -888
  105. package/dist/commands/improve/distill.js +607 -363
  106. package/dist/commands/improve/eligibility.js +165 -79
  107. package/dist/commands/improve/extract-cli.js +35 -126
  108. package/dist/commands/improve/extract-prompt.js +6 -35
  109. package/dist/commands/improve/extract.js +640 -391
  110. package/dist/commands/improve/feedback-valence.js +2 -12
  111. package/dist/commands/improve/improve-cli.js +134 -135
  112. package/dist/commands/improve/improve-result-file.js +30 -50
  113. package/dist/commands/improve/improve-run-types.js +4 -0
  114. package/dist/commands/improve/improve-strategies.js +135 -0
  115. package/dist/commands/improve/improve.js +904 -701
  116. package/dist/commands/improve/locks.js +64 -111
  117. package/dist/commands/improve/loop-stages.js +1110 -923
  118. package/dist/commands/improve/memory/derived-ref.js +124 -0
  119. package/dist/commands/improve/memory/memory-belief.js +79 -7
  120. package/dist/commands/improve/memory/memory-contradiction-detect.js +49 -52
  121. package/dist/commands/improve/memory/memory-improve.js +25 -37
  122. package/dist/commands/improve/outcome-loop.js +25 -88
  123. package/dist/commands/improve/preparation.js +1034 -813
  124. package/dist/commands/improve/proactive-maintenance.js +34 -9
  125. package/dist/commands/improve/proposal-envelope.js +31 -0
  126. package/dist/commands/improve/reflect.js +983 -794
  127. package/dist/commands/improve/run-context.js +119 -0
  128. package/dist/commands/improve/salience.js +24 -127
  129. package/dist/commands/improve/session-asset.js +7 -3
  130. package/dist/commands/improve/shared.js +14 -34
  131. package/dist/commands/improve/source-identity.js +28 -0
  132. package/dist/commands/improve/triage.js +20 -17
  133. package/dist/commands/lint/base-linter.js +340 -313
  134. package/dist/commands/lint/env-key-rules.js +31 -47
  135. package/dist/commands/lint/index.js +185 -30
  136. package/dist/commands/{events.js → log.js} +28 -38
  137. package/dist/commands/migrate-cli.js +54 -0
  138. package/dist/commands/migration-tool.js +55 -0
  139. package/dist/commands/observability-cli.js +70 -208
  140. package/dist/commands/proposal/diff-format.js +50 -0
  141. package/dist/commands/proposal/drain-policies.js +0 -6
  142. package/dist/commands/proposal/drain.js +91 -40
  143. package/dist/commands/proposal/proposal-cli.js +134 -132
  144. package/dist/commands/proposal/proposal-types.js +56 -0
  145. package/dist/commands/proposal/proposal.js +83 -65
  146. package/dist/commands/proposal/propose-cli.js +88 -0
  147. package/dist/commands/proposal/propose.js +105 -88
  148. package/dist/commands/proposal/repository.js +1303 -278
  149. package/dist/commands/proposal/validators/proposal-quality-validators.js +16 -6
  150. package/dist/commands/proposal/validators/proposal-validators.js +61 -12
  151. package/dist/commands/proposal/validators/proposals.js +6 -8
  152. package/dist/commands/read/curate.js +78 -73
  153. package/dist/commands/read/knowledge.js +510 -13
  154. package/dist/commands/read/registry-search.js +2 -2
  155. package/dist/commands/read/remember-cli.js +84 -15
  156. package/dist/commands/read/search-cli.js +203 -96
  157. package/dist/commands/read/search.js +126 -94
  158. package/dist/commands/read/show.js +226 -250
  159. package/dist/commands/registry-cli.js +34 -60
  160. package/dist/commands/remember.js +18 -57
  161. package/dist/commands/sources/add-cli.js +104 -49
  162. package/dist/commands/sources/bundle-cli.js +166 -0
  163. package/dist/commands/sources/bundle-config-ops.js +63 -0
  164. package/dist/commands/sources/info.js +27 -15
  165. package/dist/commands/sources/init.js +30 -40
  166. package/dist/commands/sources/installed-stashes.js +469 -172
  167. package/dist/commands/sources/migration-help.js +7 -4
  168. package/dist/commands/sources/schema-repair.js +10 -9
  169. package/dist/commands/sources/self-update.js +182 -121
  170. package/dist/commands/sources/source-add.js +169 -178
  171. package/dist/commands/sources/source-clone.js +144 -41
  172. package/dist/commands/sources/source-manage.js +94 -59
  173. package/dist/commands/sources/sources-cli.js +64 -205
  174. package/dist/commands/sources/stash-cli.js +91 -54
  175. package/dist/commands/sources/stash-skeleton.js +1 -1
  176. package/dist/commands/tasks/tasks-cli.js +106 -104
  177. package/dist/commands/tasks/tasks.js +445 -262
  178. package/dist/commands/workflow-cli.js +232 -121
  179. package/dist/core/action-contributors.js +1 -1
  180. package/dist/core/activation-policy.js +49 -0
  181. package/dist/core/adapter/adapters/agent-skills-adapter.js +181 -0
  182. package/dist/core/adapter/adapters/akm-adapter.js +528 -0
  183. package/dist/core/adapter/adapters/akm-lint.js +392 -0
  184. package/dist/core/adapter/adapters/akm-metadata.js +387 -0
  185. package/dist/core/adapter/adapters/akm-task-adapter.js +149 -0
  186. package/dist/core/adapter/adapters/akm-workflow-adapter.js +180 -0
  187. package/dist/core/adapter/adapters/claude-adapter.js +61 -0
  188. package/dist/core/adapter/adapters/dotenv-adapter.js +187 -0
  189. package/dist/core/adapter/adapters/generic-files-adapter.js +119 -0
  190. package/dist/core/adapter/adapters/index.js +80 -0
  191. package/dist/core/adapter/adapters/llm-wiki-adapter.js +419 -0
  192. package/dist/core/adapter/adapters/okf-adapter.js +391 -0
  193. package/dist/core/adapter/adapters/opencode-adapter.js +68 -0
  194. package/dist/core/adapter/adapters/shared.js +286 -0
  195. package/dist/core/adapter/adapters/tool-dir-shared.js +217 -0
  196. package/dist/core/adapter/adapters/website-snapshot-adapter.js +155 -0
  197. package/dist/core/adapter/bundle-adapter.js +4 -0
  198. package/dist/core/adapter/detect-adapter.js +17 -0
  199. package/dist/core/adapter/recognize-match.js +44 -0
  200. package/dist/core/adapter/registry.js +56 -0
  201. package/dist/core/adapter/types.js +4 -0
  202. package/dist/core/asset/akm-markdown.js +30 -0
  203. package/dist/core/asset/asset-placement.js +243 -0
  204. package/dist/core/asset/asset-ref.js +110 -79
  205. package/dist/core/asset/asset-serialize.js +20 -0
  206. package/dist/core/asset/frontmatter.js +28 -12
  207. package/dist/core/asset/markdown.js +40 -51
  208. package/dist/core/asset/resolve-ref.js +274 -0
  209. package/dist/core/asset/stash-meta.js +2 -2
  210. package/dist/core/bundle-id.js +51 -0
  211. package/dist/core/common.js +281 -86
  212. package/dist/core/config/config-io.js +42 -128
  213. package/dist/core/config/config-schema.js +233 -834
  214. package/dist/core/config/config-sources.js +162 -39
  215. package/dist/core/config/config-types.js +16 -11
  216. package/dist/core/config/config-version.js +29 -0
  217. package/dist/core/config/config-walker.js +126 -37
  218. package/dist/core/config/config.js +154 -331
  219. package/dist/core/config/deep-merge.js +41 -0
  220. package/dist/core/config/engine-semantics.js +28 -0
  221. package/dist/core/config/experimental.js +21 -0
  222. package/dist/core/config/schema/embedding.js +38 -0
  223. package/dist/core/config/schema/engines.js +116 -0
  224. package/dist/core/config/schema/experimental.js +47 -0
  225. package/dist/core/config/schema/feedback.js +31 -0
  226. package/dist/core/config/schema/improve-processes.js +389 -0
  227. package/dist/core/config/schema/improve.js +94 -0
  228. package/dist/core/config/schema/index-config.js +176 -0
  229. package/dist/core/config/schema/output.js +18 -0
  230. package/dist/core/config/schema/primitives.js +94 -0
  231. package/dist/core/config/schema/search.js +30 -0
  232. package/dist/core/config/schema/setup.js +18 -0
  233. package/dist/core/config/schema/sources-bundles.js +169 -0
  234. package/dist/core/config/schema/workflow.js +29 -0
  235. package/dist/core/env-secret-ref.js +155 -20
  236. package/dist/core/errors.js +17 -15
  237. package/dist/core/events-types.js +4 -0
  238. package/dist/core/events.js +46 -128
  239. package/dist/core/extra-params.js +62 -0
  240. package/dist/core/file-change.js +17 -0
  241. package/dist/core/file-lock.js +202 -57
  242. package/dist/core/fs-txn.js +392 -0
  243. package/dist/core/git-message.js +59 -0
  244. package/dist/core/improve-result.js +167 -0
  245. package/dist/core/json-schema.js +142 -0
  246. package/dist/core/lesson-lint.js +1 -17
  247. package/dist/core/logs-db.js +1 -1
  248. package/dist/core/maintenance-barrier.js +135 -0
  249. package/dist/core/migration-operation.js +44 -0
  250. package/dist/core/mutation-target.js +78 -0
  251. package/dist/core/paths.js +22 -25
  252. package/dist/core/platform.js +10 -0
  253. package/dist/core/recognition-util.js +128 -0
  254. package/dist/core/redaction.js +392 -0
  255. package/dist/core/standards/resolve-standards-context.js +36 -65
  256. package/dist/core/standards/resolve-stash-standards.js +2 -2
  257. package/dist/core/standards/resolve-type-conventions.js +5 -5
  258. package/dist/core/state/migrations.js +242 -11
  259. package/dist/core/state-db.js +98 -10
  260. package/dist/core/structured.js +1 -1
  261. package/dist/core/subprocess.js +303 -0
  262. package/dist/core/text-truncation.js +9 -5
  263. package/dist/core/time.js +20 -0
  264. package/dist/core/type-presentation.js +130 -0
  265. package/dist/core/warn.js +0 -3
  266. package/dist/core/write-source.js +834 -118
  267. package/dist/indexer/bundle-identity-guard.js +92 -0
  268. package/dist/indexer/db/graph-db.js +1 -25
  269. package/dist/indexer/db/llm-cache.js +1 -1
  270. package/dist/indexer/ensure-index.js +30 -9
  271. package/dist/indexer/graph/graph-boost.js +9 -30
  272. package/dist/indexer/graph/graph-extraction.js +41 -27
  273. package/dist/indexer/graph/graph-types.js +4 -0
  274. package/dist/indexer/index-writer-lock.js +93 -49
  275. package/dist/indexer/index-written-assets.js +100 -53
  276. package/dist/indexer/indexer.js +746 -329
  277. package/dist/indexer/init.js +18 -25
  278. package/dist/indexer/installations.js +142 -0
  279. package/dist/indexer/passes/dir-staleness.js +18 -10
  280. package/dist/indexer/passes/memory-inference.js +25 -15
  281. package/dist/indexer/passes/metadata.js +412 -243
  282. package/dist/indexer/scan/doc-to-entry.js +160 -0
  283. package/dist/indexer/scan/drain-dir.js +134 -0
  284. package/dist/indexer/search/db-search.js +292 -108
  285. package/dist/indexer/search/fts-query.js +64 -0
  286. package/dist/indexer/search/ranking-contributors.js +145 -25
  287. package/dist/indexer/search/ranking-types.js +4 -0
  288. package/dist/indexer/search/ranking.js +28 -71
  289. package/dist/indexer/search/search-attribution.js +67 -0
  290. package/dist/indexer/search/search-fields.js +18 -3
  291. package/dist/indexer/search/search-hit-enrichers.js +30 -40
  292. package/dist/indexer/search/search-source.js +157 -111
  293. package/dist/indexer/search/semantic-status.js +4 -1
  294. package/dist/indexer/usage/usage-events.js +10 -30
  295. package/dist/indexer/walk/file-context.js +3 -45
  296. package/dist/indexer/walk/matchers.js +42 -34
  297. package/dist/indexer/walk/path-resolver.js +11 -5
  298. package/dist/indexer/walk/walker.js +42 -14
  299. package/dist/integrations/agent/builder-shared.js +7 -0
  300. package/dist/integrations/agent/builders.js +5 -56
  301. package/dist/integrations/agent/config.js +3 -143
  302. package/dist/integrations/agent/detect.js +17 -2
  303. package/dist/integrations/agent/engine-resolution.js +231 -0
  304. package/dist/integrations/agent/index.js +1 -2
  305. package/dist/integrations/agent/model-aliases.js +16 -2
  306. package/dist/integrations/agent/profiles.js +36 -62
  307. package/dist/integrations/agent/prompts.js +46 -18
  308. package/dist/integrations/agent/runner-dispatch.js +93 -4
  309. package/dist/integrations/agent/runner.js +76 -208
  310. package/dist/integrations/agent/spawn.js +88 -196
  311. package/dist/integrations/harnesses/aider/agent-builder.js +114 -0
  312. package/dist/integrations/harnesses/aider/index.js +48 -0
  313. package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
  314. package/dist/integrations/harnesses/amazonq/agent-builder.js +147 -0
  315. package/dist/integrations/harnesses/amazonq/index.js +45 -0
  316. package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
  317. package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
  318. package/dist/integrations/harnesses/claude/config-import.js +1 -3
  319. package/dist/integrations/harnesses/claude/index.js +24 -35
  320. package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
  321. package/dist/integrations/harnesses/claude/session-log.js +27 -75
  322. package/dist/integrations/harnesses/codex/agent-builder.js +138 -0
  323. package/dist/integrations/harnesses/codex/index.js +52 -0
  324. package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
  325. package/dist/integrations/harnesses/copilot/agent-builder.js +122 -0
  326. package/dist/integrations/harnesses/copilot/index.js +48 -0
  327. package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
  328. package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
  329. package/dist/integrations/harnesses/gemini/index.js +48 -0
  330. package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
  331. package/dist/integrations/harnesses/ids.js +24 -0
  332. package/dist/integrations/harnesses/index.js +54 -34
  333. package/dist/integrations/harnesses/opencode/agent-builder.js +23 -5
  334. package/dist/integrations/harnesses/opencode/config-import.js +1 -3
  335. package/dist/integrations/harnesses/opencode/index.js +14 -32
  336. package/dist/integrations/harnesses/opencode/session-log.js +67 -125
  337. package/dist/integrations/harnesses/opencode-sdk/harness.js +51 -0
  338. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
  339. package/dist/integrations/harnesses/openhands/agent-builder.js +128 -0
  340. package/dist/integrations/harnesses/openhands/index.js +48 -0
  341. package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
  342. package/dist/integrations/harnesses/pi/agent-builder.js +97 -0
  343. package/dist/integrations/harnesses/pi/index.js +45 -0
  344. package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
  345. package/dist/integrations/harnesses/shared.js +17 -0
  346. package/dist/integrations/harnesses/types.js +43 -32
  347. package/dist/integrations/lockfile.js +211 -24
  348. package/dist/integrations/session-logs/index.js +36 -39
  349. package/dist/integrations/session-logs/provider-base.js +113 -0
  350. package/dist/llm/client.js +182 -110
  351. package/dist/llm/embedders/deterministic.js +2 -2
  352. package/dist/llm/embedders/remote.js +21 -9
  353. package/dist/llm/feature-gate.js +17 -57
  354. package/dist/llm/graph-extract.js +12 -13
  355. package/dist/llm/index-passes.js +8 -42
  356. package/dist/llm/memory-infer.js +144 -1
  357. package/dist/llm/metadata-enhance.js +45 -30
  358. package/dist/llm/structured-call.js +16 -8
  359. package/dist/llm/usage-persist.js +30 -5
  360. package/dist/llm/usage-telemetry.js +59 -6
  361. package/dist/output/cli-hints.js +1 -2
  362. package/dist/output/command-registry.js +27 -0
  363. package/dist/output/context.js +22 -7
  364. package/dist/output/format-exempt.js +80 -0
  365. package/dist/output/generic-render.js +251 -0
  366. package/dist/output/html-render.js +11 -16
  367. package/dist/output/render-registry.js +57 -0
  368. package/dist/output/renderers.js +14 -279
  369. package/dist/output/shapes/curate.js +10 -1
  370. package/dist/output/shapes/events.js +12 -7
  371. package/dist/output/shapes/helpers.js +58 -84
  372. package/dist/output/shapes/passthrough.js +11 -39
  373. package/dist/output/shapes/proposal/producer.js +15 -7
  374. package/dist/output/shapes/registry.js +12 -6
  375. package/dist/output/shapes.js +0 -9
  376. package/dist/output/text/{init.js → bundle-create.js} +3 -1
  377. package/dist/output/text/bundle-show.js +7 -0
  378. package/dist/output/text/command-format.js +562 -0
  379. package/dist/output/text/env.js +1 -3
  380. package/dist/output/text/events.js +8 -7
  381. package/dist/output/text/helpers.js +15 -1164
  382. package/dist/output/text/proposal/producer.js +4 -2
  383. package/dist/output/text/proposal-format.js +202 -0
  384. package/dist/output/text/registry-commands.js +1 -2
  385. package/dist/output/text/registry.js +12 -6
  386. package/dist/output/text/show-directives.js +117 -0
  387. package/dist/output/text/show-format.js +103 -0
  388. package/dist/output/text/sync.js +5 -0
  389. package/dist/output/text/workflow-format.js +332 -0
  390. package/dist/output/text/workflow.js +3 -2
  391. package/dist/output/text.js +10 -19
  392. package/dist/registry/factory.js +4 -6
  393. package/dist/registry/origin-resolve.js +16 -27
  394. package/dist/registry/providers/skills-sh.js +3 -3
  395. package/dist/registry/providers/static-index.js +15 -25
  396. package/dist/registry/resolve.js +43 -94
  397. package/dist/registry/semver.js +43 -0
  398. package/dist/runtime.js +81 -12
  399. package/dist/scripts/akm-migrate.js +35529 -0
  400. package/dist/setup/detect.js +5 -7
  401. package/dist/setup/detected-engines.js +136 -0
  402. package/dist/setup/engine-config.js +100 -0
  403. package/dist/setup/registry-stash-loader.js +3 -3
  404. package/dist/setup/semantic-assets.js +12 -9
  405. package/dist/setup/setup.js +444 -208
  406. package/dist/setup/steps/connection-shared.js +120 -0
  407. package/dist/setup/steps/connection.js +108 -305
  408. package/dist/setup/steps/platforms.js +13 -12
  409. package/dist/setup/steps/semantic.js +15 -3
  410. package/dist/setup/steps/sources.js +21 -15
  411. package/dist/setup/steps/stashdir.js +6 -4
  412. package/dist/setup/steps/tasks.js +236 -119
  413. package/dist/setup/steps.js +3 -2
  414. package/dist/sources/freshness.js +39 -0
  415. package/dist/sources/provider-factory.js +11 -17
  416. package/dist/sources/providers/filesystem.js +2 -3
  417. package/dist/sources/providers/git-install.js +278 -34
  418. package/dist/sources/providers/git-provider.js +54 -56
  419. package/dist/sources/providers/git-stash.js +420 -91
  420. package/dist/sources/providers/git.js +2 -2
  421. package/dist/sources/providers/npm.js +16 -19
  422. package/dist/sources/providers/provider-utils.js +47 -22
  423. package/dist/sources/providers/sync-from-ref.js +3 -9
  424. package/dist/sources/providers/website.js +2 -2
  425. package/dist/sources/resolve.js +11 -10
  426. package/dist/sources/snapshot-fetchers/types.js +4 -0
  427. package/dist/sources/{website-ingest.js → snapshot-fetchers/website-ingest.js} +110 -41
  428. package/dist/storage/database.js +60 -4
  429. package/dist/storage/engines/sqlite-migrations.js +156 -5
  430. package/dist/storage/locations.js +1 -2
  431. package/dist/storage/repositories/canaries-repository.js +1 -1
  432. package/dist/storage/repositories/events-repository.js +51 -11
  433. package/dist/storage/repositories/improve-runs-repository.js +6 -32
  434. package/dist/storage/repositories/index-connection.js +79 -0
  435. package/dist/storage/repositories/index-db.js +4 -3
  436. package/dist/storage/repositories/index-entries-repository.js +863 -0
  437. package/dist/{indexer/db/entry-mapper.js → storage/repositories/index-entry-mapper.js} +19 -2
  438. package/dist/storage/repositories/index-entry-types.js +4 -0
  439. package/dist/storage/repositories/index-fts-repository.js +167 -0
  440. package/dist/storage/repositories/index-llm-cache-repository.js +108 -0
  441. package/dist/storage/repositories/index-meta-repository.js +49 -0
  442. package/dist/{indexer/db/schema.js → storage/repositories/index-schema.js} +226 -100
  443. package/dist/storage/repositories/index-sql.js +12 -0
  444. package/dist/storage/repositories/index-utility-repository.js +356 -0
  445. package/dist/storage/repositories/index-vec-repository.js +250 -0
  446. package/dist/storage/repositories/outcome-repository.js +119 -0
  447. package/dist/storage/repositories/proposals-repository.js +317 -75
  448. package/dist/storage/repositories/registry-cache.js +1 -1
  449. package/dist/storage/repositories/salience-repository.js +172 -0
  450. package/dist/storage/repositories/task-history-repository.js +110 -3
  451. package/dist/storage/repositories/workflow-runs-repository.js +240 -19
  452. package/dist/tasks/backends/cron.js +169 -46
  453. package/dist/tasks/backends/exec-utils.js +76 -3
  454. package/dist/tasks/backends/index.js +6 -9
  455. package/dist/tasks/backends/launchd.js +292 -55
  456. package/dist/tasks/backends/schtasks.js +557 -70
  457. package/dist/tasks/backends/types.js +4 -0
  458. package/dist/tasks/command-executable.js +93 -0
  459. package/dist/tasks/embedded.js +56 -38
  460. package/dist/tasks/parser.js +156 -64
  461. package/dist/tasks/resolve-akm-bin.js +144 -51
  462. package/dist/tasks/runner.js +377 -209
  463. package/dist/tasks/schedule.js +108 -19
  464. package/dist/tasks/scheduler-invocation.js +296 -0
  465. package/dist/tasks/schema.js +1 -1
  466. package/dist/tasks/task-id.js +35 -0
  467. package/dist/tasks/validator.js +30 -16
  468. package/dist/text-import-hook.mjs +1 -1
  469. package/dist/workflows/authoring/authoring.js +104 -43
  470. package/dist/workflows/authoring/scope-key.js +1 -1
  471. package/dist/workflows/cli.js +0 -16
  472. package/dist/workflows/concurrency-policy.js +15 -0
  473. package/dist/workflows/exec/brief.js +450 -0
  474. package/dist/workflows/exec/frozen-judge.js +47 -0
  475. package/dist/workflows/exec/native-executor.js +1038 -0
  476. package/dist/workflows/exec/param-secrets.js +115 -0
  477. package/dist/workflows/exec/report.js +1460 -0
  478. package/dist/workflows/exec/run-workflow.js +602 -0
  479. package/dist/workflows/exec/scheduler.js +71 -0
  480. package/dist/workflows/exec/step-work.js +1190 -0
  481. package/dist/workflows/exec/unit-writer.js +23 -0
  482. package/dist/workflows/exec/workflow-engine-gate.js +67 -0
  483. package/dist/workflows/exec/worktree.js +171 -0
  484. package/dist/workflows/ir/compile.js +246 -0
  485. package/dist/workflows/ir/freeze.js +233 -0
  486. package/dist/workflows/ir/params.js +54 -0
  487. package/dist/workflows/ir/plan-hash.js +68 -0
  488. package/dist/workflows/ir/schema.js +540 -0
  489. package/dist/workflows/parser.js +878 -304
  490. package/dist/workflows/program/expressions.js +181 -0
  491. package/dist/workflows/program/schema.js +51 -0
  492. package/dist/workflows/renderer.js +100 -45
  493. package/dist/workflows/resource-limits.js +22 -0
  494. package/dist/workflows/runtime/agent-identity.js +59 -14
  495. package/dist/workflows/runtime/checkin.js +1 -1
  496. package/dist/workflows/runtime/plan-classifier.js +131 -0
  497. package/dist/workflows/runtime/runs.js +376 -119
  498. package/dist/workflows/runtime/unit-checkin.js +45 -0
  499. package/dist/workflows/runtime/unit-phases.js +20 -0
  500. package/dist/workflows/runtime/workflow-asset-loader.js +241 -40
  501. package/dist/workflows/schema.js +1 -11
  502. package/dist/workflows/validate-summary.js +2 -3
  503. package/dist/workflows/validator.js +52 -30
  504. package/docs/README.md +42 -78
  505. package/docs/migration/README.md +8 -0
  506. package/docs/migration/release-notes/0.6.0.md +1 -1
  507. package/docs/migration/release-notes/0.7.0.md +9 -8
  508. package/docs/migration/release-notes/0.9.0.md +158 -14
  509. package/docs/migration/v0.7-to-v0.8.md +46 -47
  510. package/docs/migration/v0.8-to-v0.9.md +844 -0
  511. package/docs/reference/README.md +12 -0
  512. package/docs/reference/data-and-telemetry.md +333 -0
  513. package/package.json +21 -17
  514. package/schemas/akm-asset-envelope.json +93 -0
  515. package/schemas/akm-config.json +4636 -0
  516. package/schemas/akm-task.json +87 -0
  517. package/schemas/akm-workflow.json +373 -0
  518. package/dist/akm-migrate-storage +0 -38
  519. package/dist/assets/help/help-accept.md +0 -12
  520. package/dist/assets/help/help-improve.md +0 -84
  521. package/dist/assets/help/help-proposals.md +0 -17
  522. package/dist/assets/help/help-propose.md +0 -17
  523. package/dist/assets/help/help-reject.md +0 -11
  524. package/dist/assets/profiles/frequent.json +0 -13
  525. package/dist/assets/profiles/recombine-only.json +0 -21
  526. package/dist/assets/profiles/reflect-distill.json +0 -30
  527. package/dist/assets/profiles/synthesize.json +0 -15
  528. package/dist/assets/prompts/procedural-system.md +0 -44
  529. package/dist/assets/prompts/recombine-system.md +0 -40
  530. package/dist/assets/prompts/staleness-detect-system.md +0 -6
  531. package/dist/assets/tasks/core/backup.yml +0 -4
  532. package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
  533. package/dist/assets/templates/html/default.html +0 -78
  534. package/dist/assets/templates/html/vendor/echarts.min.js +0 -45
  535. package/dist/assets/wiki/index-template.md +0 -12
  536. package/dist/assets/wiki/ingest-workflow-template.md +0 -83
  537. package/dist/assets/wiki/log-template.md +0 -8
  538. package/dist/assets/wiki/schema-template.md +0 -61
  539. package/dist/cli/config-migrate.js +0 -150
  540. package/dist/cli/config-validate.js +0 -39
  541. package/dist/commands/graph/graph-cli.js +0 -124
  542. package/dist/commands/graph/graph.js +0 -487
  543. package/dist/commands/improve/calibration.js +0 -161
  544. package/dist/commands/improve/dedup.js +0 -482
  545. package/dist/commands/improve/extract-watch.js +0 -140
  546. package/dist/commands/improve/hot-probation.js +0 -45
  547. package/dist/commands/improve/improve-auto-accept.js +0 -276
  548. package/dist/commands/improve/improve-profiles.js +0 -168
  549. package/dist/commands/improve/procedural.js +0 -398
  550. package/dist/commands/improve/recombine.js +0 -818
  551. package/dist/commands/improve/schema-similarity-gate.js +0 -168
  552. package/dist/commands/lint/agent-linter.js +0 -44
  553. package/dist/commands/lint/command-linter.js +0 -44
  554. package/dist/commands/lint/default-linter.js +0 -16
  555. package/dist/commands/lint/fact-linter.js +0 -39
  556. package/dist/commands/lint/knowledge-linter.js +0 -16
  557. package/dist/commands/lint/memory-linter.js +0 -61
  558. package/dist/commands/lint/registry.js +0 -41
  559. package/dist/commands/lint/skill-linter.js +0 -45
  560. package/dist/commands/lint/task-linter.js +0 -50
  561. package/dist/commands/lint/workflow-linter.js +0 -81
  562. package/dist/commands/proposal/legacy-import.js +0 -115
  563. package/dist/commands/sources/history.js +0 -196
  564. package/dist/commands/tasks/default-tasks.js +0 -186
  565. package/dist/commands/wiki-cli.js +0 -292
  566. package/dist/core/asset/asset-registry.js +0 -76
  567. package/dist/core/asset/asset-spec.js +0 -259
  568. package/dist/core/config/config-migration.js +0 -602
  569. package/dist/core/deep-merge.js +0 -38
  570. package/dist/core/eval/rank-metrics.js +0 -113
  571. package/dist/core/ripgrep/install.js +0 -163
  572. package/dist/core/ripgrep/resolve.js +0 -81
  573. package/dist/indexer/db/db.js +0 -1413
  574. package/dist/indexer/manifest.js +0 -170
  575. package/dist/indexer/passes/metadata-contributors.js +0 -31
  576. package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -94
  577. package/dist/integrations/harnesses/opencode-sdk/index.js +0 -49
  578. package/dist/llm/call-ai.js +0 -62
  579. package/dist/llm/memory-infer-impl.js +0 -138
  580. package/dist/output/shapes/distill.js +0 -14
  581. package/dist/output/shapes/history.js +0 -11
  582. package/dist/output/text/distill.js +0 -6
  583. package/dist/output/text/enable-disable.js +0 -8
  584. package/dist/output/text/history.js +0 -6
  585. package/dist/output/text/wiki.js +0 -16
  586. package/dist/registry/build-index.js +0 -386
  587. package/dist/scripts/migrate-storage.js +0 -19108
  588. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +0 -9411
  589. package/dist/scripts/migrations/v16-to-v17.js +0 -141
  590. package/dist/setup/legacy-config.js +0 -106
  591. package/dist/storage/repositories/consolidation-repository.js +0 -38
  592. package/dist/storage/repositories/recombine-repository.js +0 -213
  593. package/dist/wiki/wiki-templates.js +0 -15
  594. package/dist/wiki/wiki.js +0 -1012
  595. package/dist/workflows/db.js +0 -215
  596. package/docs/data-and-telemetry.md +0 -226
  597. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/registry.js +0 -0
  598. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/youtube.js +0 -0
@@ -2,295 +2,314 @@
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";
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";
16
40
  import { WORKFLOW_SCHEMA_VERSION, } from "./schema.js";
17
41
  import { runSemanticChecks } from "./validator.js";
18
- const WORKFLOW_TITLE_PREFIX = "Workflow:";
19
- const STEP_PREFIX = "Step:";
20
- const STEP_ID_LINE = /^Step ID:\s+(.+?)\s*$/;
21
- const BULLET_LINE = /^[-*]\s+(.+)$/;
22
- const SUBSECTION_INSTRUCTIONS = "Instructions";
23
- 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"]);
24
72
  /**
25
- * Cheap structural probe for the matcher. Returns true if the body has the
26
- * unmistakable shape of a workflow file. Used in `src/indexer/matchers.ts` so
27
- * 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.
28
78
  */
29
- export function looksLikeWorkflow(body) {
30
- const structuralBody = stripFencedCodeBlocks(body);
31
- return (/^#\s+Workflow:\s+/m.test(structuralBody) &&
32
- /^##\s+Step:\s+/m.test(structuralBody) &&
33
- /^Step ID:\s+/m.test(structuralBody) &&
34
- /^###\s+Instructions\s*$/m.test(structuralBody));
35
- }
36
- function stripFencedCodeBlocks(body) {
37
- let inFence = false;
38
- const lines = body.split(/\r?\n/);
39
- const stripped = [];
40
- for (const line of lines) {
41
- if (/^\s*```/.test(line) || /^\s*~~~/.test(line)) {
42
- inFence = !inFence;
43
- stripped.push("");
44
- continue;
45
- }
46
- stripped.push(inFence ? "" : line);
47
- }
48
- 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);
49
84
  }
50
85
  export function parseWorkflow(markdown, source) {
86
+ if (utf8Bytes(markdown) > WORKFLOW_MAX_SOURCE_BYTES) {
87
+ return {
88
+ ok: false,
89
+ errors: [{ line: 1, message: "Workflow source exceeds the 1 MiB resource limit." }],
90
+ };
91
+ }
51
92
  const errors = [];
52
93
  const path = source.path;
53
94
  const lines = markdown.split(/\r?\n/);
54
95
  const totalLines = lines.length;
55
96
  const fmBlock = parseFrontmatterBlock(markdown);
56
- const frontmatterEndLine = fmBlock ? Math.max(1, fmBlock.bodyStartLine - 1) : 1;
57
- const fmData = readFrontmatter(fmBlock?.frontmatter, errors);
58
- const description = readDescription(fmData);
59
- const tags = readTags(fmData, errors, frontmatterEndLine);
60
- const parameters = readParameters(fmData, errors, frontmatterEndLine, path);
61
- const toc = parseMarkdownToc(markdown);
62
- const { title, titleLine } = extractTitle(toc.headings, errors);
63
- // Disallow stray level-1 and non-Step level-2 headings.
64
- for (const h of toc.headings) {
65
- if (h.level === 1 && !h.text.startsWith(WORKFLOW_TITLE_PREFIX)) {
66
- errors.push({
67
- line: h.line,
68
- message: `Unexpected top-level heading "# ${h.text}" on line ${h.line}. A workflow file may only contain one "# Workflow: <title>" heading.`,
69
- });
70
- }
71
- if (h.level === 2 && !h.text.startsWith(STEP_PREFIX)) {
72
- errors.push({
73
- line: h.line,
74
- message: `Unexpected level-2 heading "## ${h.text}" on line ${h.line}. Only "## Step: <title>" sections are allowed.`,
75
- });
76
- }
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
+ };
77
124
  }
78
- const steps = extractSteps(toc.headings, lines, totalLines, path, errors);
79
- if (steps.length === 0 && titleLine > 0) {
125
+ for (const problem of doc.errors) {
126
+ const offset = Array.isArray(problem.pos) ? problem.pos[0] : 0;
80
127
  errors.push({
81
- line: titleLine,
82
- 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),
83
130
  });
84
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
+ });
85
225
  const draft = {
86
226
  schemaVersion: WORKFLOW_SCHEMA_VERSION,
87
- title,
88
227
  ...(description ? { description } : {}),
89
228
  ...(tags ? { tags } : {}),
90
- ...(parameters ? { parameters } : {}),
229
+ ...(params ? { params } : {}),
230
+ ...(defaults ? { defaults } : {}),
231
+ ...(budget ? { budget } : {}),
91
232
  steps,
233
+ ...(preamble ? { preamble } : {}),
92
234
  source: { path, lineCount: totalLines },
93
235
  };
94
- runSemanticChecks(draft, fmData, frontmatterEndLine, errors);
95
- if (errors.length > 0) {
236
+ runSemanticChecks(draft, root, frontmatterEndLine, errors);
237
+ if (errors.length > 0)
96
238
  return { ok: false, errors: sortErrors(errors) };
97
- }
98
239
  return { ok: true, document: draft };
99
240
  }
100
- // ── Title ───────────────────────────────────────────────────────────────────
101
- function extractTitle(headings, errors) {
102
- const titleHeadings = headings.filter((h) => h.level === 1 && h.text.startsWith(WORKFLOW_TITLE_PREFIX));
103
- if (titleHeadings.length === 0) {
104
- errors.push({
105
- line: 1,
106
- message: `Workflow markdown must start with a "# Workflow: <title>" heading. Add one at the top of the file.`,
107
- });
108
- return { title: "", titleLine: 0 };
109
- }
110
- if (titleHeadings.length > 1) {
111
- for (const extra of titleHeadings.slice(1)) {
112
- errors.push({
113
- line: extra.line,
114
- message: `Found a second "# Workflow:" heading on line ${extra.line}. A workflow file must contain exactly one.`,
115
- });
116
- }
117
- }
118
- const first = titleHeadings[0];
119
- const title = first.text.slice(WORKFLOW_TITLE_PREFIX.length).trim();
120
- if (!title) {
121
- errors.push({
122
- line: first.line,
123
- message: `The "# Workflow:" heading on line ${first.line} is missing a title. Use "# Workflow: <title>".`,
124
- });
125
- }
126
- return { title, titleLine: first.line };
127
- }
128
- // ── Steps ───────────────────────────────────────────────────────────────────
129
- function extractSteps(headings, lines, totalLines, path, errors) {
130
- const steps = [];
131
- 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();
132
248
  for (let i = 0; i < headings.length; i++) {
133
249
  const h = headings[i];
134
- if (h.level !== 2 || !h.text.startsWith(STEP_PREFIX))
250
+ if (h.level !== 2)
135
251
  continue;
136
- const stepTitle = h.text.slice(STEP_PREFIX.length).trim();
137
- if (!stepTitle) {
252
+ if (!declaredIds.has(h.text)) {
138
253
  errors.push({
139
254
  line: h.line,
140
- 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.`,
141
256
  });
142
257
  continue;
143
258
  }
144
- const stepEnd = Math.min(findNextSiblingOrParentLine(headings, i, 2) - 1, totalLines);
145
- const stepSource = { path, start: h.line, end: stepEnd };
146
- const subsections = collectSubsections(headings, i, stepEnd);
147
- const stepIdSearchEnd = subsections.length > 0 ? subsections[0].headingLine - 1 : stepEnd;
148
- const stepId = scanStepId(lines, h.line + 1, stepIdSearchEnd, stepTitle, errors);
149
- const { instructions, completionCriteria } = collectStepBody(subsections, lines, path, stepTitle, errors);
150
- if (!stepId)
151
- continue; // scanStepId already pushed the missing-id error
152
- if (!instructions) {
259
+ if (sections.has(h.text)) {
153
260
  errors.push({
154
261
  line: h.line,
155
- 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.`,
156
263
  });
157
264
  continue;
158
265
  }
159
- steps.push({
160
- id: stepId,
161
- title: stepTitle,
162
- sequenceIndex: sequenceIndex++,
163
- instructions,
164
- ...(completionCriteria ? { completionCriteria } : {}),
165
- source: stepSource,
166
- });
167
- }
168
- return steps;
169
- }
170
- function collectSubsections(headings, stepIndex, stepEnd) {
171
- const subs = [];
172
- for (let j = stepIndex + 1; j < headings.length; j++) {
173
- const sub = headings[j];
174
- if (sub.level <= 2)
175
- break;
176
- if (sub.level !== 3)
177
- continue;
178
- const next = headings[j + 1];
179
- const rawEnd = next ? next.line - 1 : stepEnd;
180
- subs.push({
181
- name: sub.text,
182
- headingLine: sub.line,
183
- bodyStart: sub.line + 1,
184
- bodyEnd: Math.min(rawEnd, stepEnd),
185
- });
186
- }
187
- return subs;
188
- }
189
- function collectStepBody(subsections, lines, path, stepTitle, errors) {
190
- let instructions;
191
- let completionCriteria;
192
- for (const sub of subsections) {
193
- if (sub.name === SUBSECTION_INSTRUCTIONS) {
194
- if (instructions) {
195
- errors.push({
196
- line: sub.headingLine,
197
- message: `Step "${stepTitle}" has more than one "### Instructions" section (line ${sub.headingLine}). Keep only one.`,
198
- });
199
- continue;
200
- }
201
- const text = sliceLines(lines, sub.bodyStart, sub.bodyEnd).trim();
202
- if (!text) {
203
- errors.push({
204
- line: sub.headingLine,
205
- message: `Step "${stepTitle}" has an empty "### Instructions" section. Add the instructions text below the heading.`,
206
- });
207
- continue;
208
- }
209
- instructions = {
210
- text,
211
- source: { path, start: sub.bodyStart, end: sub.bodyEnd },
212
- };
213
- 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 } };
214
273
  }
215
- if (sub.name === SUBSECTION_COMPLETION_CRITERIA) {
216
- if (completionCriteria) {
217
- errors.push({
218
- line: sub.headingLine,
219
- message: `Step "${stepTitle}" has more than one "### Completion Criteria" section (line ${sub.headingLine}). Keep only one.`,
220
- });
221
- continue;
222
- }
223
- const items = collectBullets(lines, sub.bodyStart, sub.bodyEnd, path);
224
- if (items.length === 0) {
225
- errors.push({
226
- line: sub.headingLine,
227
- message: `Step "${stepTitle}" has an empty "### Completion Criteria" section. Add at least one "- criterion" bullet.`,
228
- });
229
- 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 } };
230
278
  }
231
- completionCriteria = items;
232
- continue;
233
279
  }
234
- errors.push({
235
- line: sub.headingLine,
236
- message: `Step "${stepTitle}" has an unknown "### ${sub.name}" section. Only "### Instructions" and "### Completion Criteria" are supported.`,
237
- });
280
+ sections.set(h.text, section);
238
281
  }
239
- return {
240
- ...(instructions ? { instructions } : {}),
241
- ...(completionCriteria ? { completionCriteria } : {}),
242
- };
282
+ return { sections, preamble: preambleRaw || undefined };
243
283
  }
244
- function scanStepId(lines, startLineInclusive, endLineInclusive, stepTitle, errors) {
245
- let foundId;
246
- let foundLine = -1;
247
- for (let lineNum = startLineInclusive; lineNum <= endLineInclusive; lineNum++) {
248
- const trimmed = (lines[lineNum - 1] ?? "").trim();
249
- if (!trimmed)
250
- continue;
251
- const match = trimmed.match(STEP_ID_LINE);
252
- 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")
253
294
  continue;
254
- if (foundId !== undefined) {
295
+ if (found) {
255
296
  errors.push({
256
- line: lineNum,
257
- 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.`,
258
299
  });
259
300
  continue;
260
301
  }
261
- foundId = match[1].trim();
262
- foundLine = lineNum;
302
+ found = { headingLine: h.line, bodyStart: h.line + 1, bodyEnd: sectionEnd };
263
303
  }
264
- if (!foundId) {
265
- errors.push({
266
- line: startLineInclusive,
267
- message: `Step "${stepTitle}" is missing a "Step ID: <id>" line. Add one between the step heading and its subsections.`,
268
- });
269
- }
270
- return foundId;
271
- }
272
- function collectBullets(lines, startLineInclusive, endLineInclusive, path) {
273
- const items = [];
274
- for (let lineNum = startLineInclusive; lineNum <= endLineInclusive; lineNum++) {
275
- const trimmed = (lines[lineNum - 1] ?? "").trim();
276
- if (!trimmed)
277
- continue;
278
- const match = trimmed.match(BULLET_LINE);
279
- if (!match)
280
- continue;
281
- items.push({
282
- text: match[1].trim(),
283
- source: { path, start: lineNum, end: lineNum },
284
- });
285
- }
286
- return items;
304
+ void path;
305
+ return found;
287
306
  }
288
- function findNextSiblingOrParentLine(headings, fromIndex, level) {
307
+ function findNextHeadingAtOrAboveLevel(headings, fromIndex, level, totalLines) {
289
308
  for (let i = fromIndex + 1; i < headings.length; i++) {
290
309
  if (headings[i].level <= level)
291
- return headings[i].line;
310
+ return headings[i].line - 1;
292
311
  }
293
- return Number.MAX_SAFE_INTEGER;
312
+ return totalLines;
294
313
  }
295
314
  function sliceLines(lines, startLineInclusive, endLineInclusive) {
296
315
  if (endLineInclusive < startLineInclusive)
@@ -299,99 +318,654 @@ function sliceLines(lines, startLineInclusive, endLineInclusive) {
299
318
  const e = Math.min(endLineInclusive, lines.length);
300
319
  return lines.slice(s - 1, e).join("\n");
301
320
  }
302
- // ── Frontmatter ─────────────────────────────────────────────────────────────
303
- function readFrontmatter(frontmatter, errors) {
304
- if (!frontmatter)
305
- return {};
306
- let parsed;
307
- try {
308
- 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)}).`);
309
336
  }
310
- catch (err) {
311
- const msg = err instanceof Error ? err.message : String(err);
312
- errors.push({
313
- line: 1,
314
- message: `Workflow frontmatter is not valid YAML: ${msg}`,
315
- });
316
- 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.`);
317
339
  }
318
- if (parsed === null || parsed === undefined)
319
- return {};
320
- if (typeof parsed !== "object" || Array.isArray(parsed)) {
321
- errors.push({
322
- line: 1,
323
- message: `Workflow frontmatter must be a YAML mapping (key: value pairs). Use "key: value" lines between the --- markers.`,
324
- });
325
- 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.`);
326
384
  }
327
- return parsed;
328
385
  }
329
- function readDescription(data) {
330
- const v = data.description;
331
- 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.`);
332
391
  return undefined;
333
- const trimmed = v.trim();
334
- 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
+ }
335
401
  }
336
- function readTags(data, errors, fmEndLine) {
337
- const v = data.tags;
338
- 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 }).`);
339
410
  return undefined;
340
- if (typeof v === "string") {
341
- const t = v.trim();
342
- return t ? [t] : undefined;
343
411
  }
344
- if (!Array.isArray(v) || !v.every((tag) => typeof tag === "string" && tag.trim().length > 0)) {
345
- errors.push({
346
- line: fmEndLine,
347
- message: `Workflow frontmatter "tags" must be a string or a list of non-empty strings.`,
348
- });
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(", ")}.`);
349
435
  return undefined;
350
436
  }
351
- 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;
352
461
  }
353
- function readParameters(data, errors, fmEndLine, path) {
354
- const v = data.params;
355
- if (v === undefined || v === null)
462
+ function parseBudget(ctx, raw) {
463
+ if (raw === undefined)
356
464
  return undefined;
357
- if (typeof v !== "object" || Array.isArray(v)) {
358
- errors.push({
359
- line: fmEndLine,
360
- message: `Workflow frontmatter "params" must be a mapping of parameter names to descriptions.`,
361
- });
465
+ const path = ["budget"];
466
+ if (!isPlainRecord(raw)) {
467
+ ctx.err(path, `"budget" must be a mapping with any of: ${BUDGET_KEYS.join(", ")}.`);
362
468
  return undefined;
363
469
  }
364
- const entries = Object.entries(v);
365
- 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).`);
366
604
  return undefined;
367
- const out = [];
368
- for (const [name, desc] of entries) {
369
- const trimmedName = name.trim();
370
- if (!trimmedName) {
371
- errors.push({
372
- line: fmEndLine,
373
- message: `Workflow parameter names must be non-empty.`,
374
- });
375
- 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());
376
638
  }
377
- if (typeof desc !== "string" || !desc.trim()) {
378
- errors.push({
379
- line: fmEndLine,
380
- message: `Workflow parameter "${trimmedName}" must have a non-empty string description in frontmatter "params".`,
381
- });
382
- 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;
666
+ }
667
+ else {
668
+ ctx.err([...path, "concurrency"], `${stepLabel} "concurrency" must be a positive integer.`);
383
669
  }
384
- out.push({
385
- name: trimmedName,
386
- description: desc.trim(),
387
- // The frontmatter parser doesn't track per-key line numbers; anchor to the
388
- // frontmatter block end so editors land somewhere sensible.
389
- source: { path, start: 1, end: fmEndLine },
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 });
390
733
  });
391
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
+ });
392
771
  return out.length > 0 ? out : undefined;
393
772
  }
394
- // ── 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
+ }
395
969
  function sortErrors(errors) {
396
970
  return [...errors].sort((a, b) => a.line - b.line);
397
971
  }