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