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.
- package/CHANGELOG.md +1283 -22
- package/README.md +62 -37
- package/SECURITY.md +46 -31
- package/dist/akm +162 -38
- package/dist/akm-migrate +44 -0
- package/dist/assets/backends/schtasks-template.xml +2 -1
- package/dist/assets/hints/cli-hints-full.md +268 -118
- package/dist/assets/hints/cli-hints-short.md +87 -24
- package/dist/assets/{profiles → improve-strategies}/catchup.json +3 -1
- package/dist/assets/{profiles → improve-strategies}/consolidate.json +3 -1
- package/dist/assets/{profiles → improve-strategies}/default.json +6 -7
- package/dist/assets/improve-strategies/frequent.json +15 -0
- package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +4 -2
- package/dist/assets/{profiles → improve-strategies}/memory-focus.json +4 -1
- package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +5 -5
- package/dist/assets/{profiles → improve-strategies}/quick.json +4 -2
- package/dist/assets/improve-strategies/reflect-distill.json +30 -0
- package/dist/assets/{profiles → improve-strategies}/thorough.json +1 -1
- package/dist/assets/prompts/consolidate-system.md +5 -5
- package/dist/assets/prompts/extract-session.md +2 -6
- package/dist/assets/prompts/memory-infer-user.md +2 -3
- package/dist/assets/prompts/reflect-llm-framed-contract.md +11 -0
- package/dist/assets/prompts/reflect-llm-schema-contract.md +3 -0
- package/dist/assets/prompts/reflect-output-repair.md +3 -0
- package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
- package/dist/assets/stash-skeleton/README.md +38 -10
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +8 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +8 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +14 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +13 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +9 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +11 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +9 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +9 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +8 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +100 -0
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +64 -0
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +136 -0
- package/dist/assets/tasks/core/extract.yml +3 -2
- package/dist/assets/tasks/core/improve.yml +2 -1
- package/dist/assets/tasks/core/index-refresh.yml +1 -0
- package/dist/assets/tasks/core/sync.yml +1 -0
- package/dist/assets/tasks/core/version-check.yml +2 -1
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
- package/dist/assets/templates/html/health.html +5 -4
- package/dist/assets/workflows/workflow-template.md +31 -15
- package/dist/cli/invocation.js +279 -0
- package/dist/cli/parse-args.js +5 -90
- package/dist/cli/retired-commands.js +78 -0
- package/dist/cli/shared.js +158 -48
- package/dist/cli-node.mjs +2 -1
- package/dist/cli.js +747 -293
- package/dist/commands/agent/agent-dispatch.js +19 -18
- package/dist/commands/agent/agent-support.js +0 -24
- package/dist/commands/agent/contribute-cli.js +43 -97
- package/dist/commands/completions.js +80 -23
- package/dist/commands/config-cli.js +44 -281
- package/dist/commands/env/env-binding.js +99 -0
- package/dist/commands/env/env-cli.js +84 -224
- package/dist/commands/env/env.js +12 -163
- package/dist/commands/env/marker-path.js +6 -0
- package/dist/commands/env/secret-cli.js +45 -61
- package/dist/commands/env/secret.js +32 -62
- package/dist/commands/feedback-cli.js +179 -85
- package/dist/commands/health/accept-rate.js +58 -0
- package/dist/commands/health/advisories.js +7 -8
- package/dist/commands/health/checks.js +279 -94
- package/dist/commands/health/html-report.js +197 -578
- package/dist/commands/health/improve-metrics.js +277 -246
- package/dist/commands/health/llm-usage.js +19 -19
- package/dist/commands/health/md-report.js +16 -7
- package/dist/commands/health/metrics.js +67 -32
- package/dist/commands/health/renderers.js +47 -0
- package/dist/commands/health/report-view-model.js +508 -0
- package/dist/commands/health/stash-exposure.js +1 -1
- package/dist/commands/health/surfaces.js +16 -56
- package/dist/commands/health/task-runs.js +3 -67
- package/dist/{migrate-storage-node.mjs → commands/health/types-checks.js} +1 -5
- package/dist/commands/health/types-improve.js +29 -0
- package/dist/{output/text/save.js → commands/health/types-metrics.js} +1 -2
- package/dist/commands/health/types-result.js +7 -0
- package/dist/commands/health/types-runs.js +4 -0
- package/dist/commands/health/types-session-log.js +4 -0
- package/dist/commands/health/types-windows.js +4 -0
- package/dist/commands/health/types.js +26 -21
- package/dist/commands/health/windows.js +2 -3
- package/dist/commands/health.js +296 -167
- package/dist/commands/improve/anti-collapse.js +5 -5
- package/dist/commands/improve/autonomy-gate.js +68 -0
- package/dist/commands/improve/collapse-detector.js +65 -52
- package/dist/commands/improve/consolidate/chunking.js +9 -7
- package/dist/commands/improve/consolidate/eligibility.js +1 -23
- package/dist/commands/improve/consolidate/merge.js +4 -0
- package/dist/commands/improve/consolidate.js +454 -1354
- package/dist/commands/improve/content-hash.js +39 -0
- package/dist/commands/improve/distill/content-repair.js +4 -10
- package/dist/commands/improve/distill/promote-memory.js +89 -64
- package/dist/commands/improve/distill/quality-gate.js +118 -42
- package/dist/commands/improve/distill-guards.js +1 -1
- package/dist/commands/improve/distill-promotion-policy.js +33 -888
- package/dist/commands/improve/distill.js +607 -363
- package/dist/commands/improve/eligibility.js +165 -79
- package/dist/commands/improve/extract-cli.js +35 -126
- package/dist/commands/improve/extract-prompt.js +6 -35
- package/dist/commands/improve/extract.js +640 -391
- package/dist/commands/improve/feedback-valence.js +2 -12
- package/dist/commands/improve/improve-cli.js +134 -135
- package/dist/commands/improve/improve-result-file.js +30 -50
- package/dist/commands/improve/improve-run-types.js +4 -0
- package/dist/commands/improve/improve-strategies.js +135 -0
- package/dist/commands/improve/improve.js +904 -701
- package/dist/commands/improve/locks.js +64 -111
- package/dist/commands/improve/loop-stages.js +1110 -923
- package/dist/commands/improve/memory/derived-ref.js +124 -0
- package/dist/commands/improve/memory/memory-belief.js +79 -7
- package/dist/commands/improve/memory/memory-contradiction-detect.js +49 -52
- package/dist/commands/improve/memory/memory-improve.js +25 -37
- package/dist/commands/improve/outcome-loop.js +25 -88
- package/dist/commands/improve/preparation.js +1034 -813
- package/dist/commands/improve/proactive-maintenance.js +34 -9
- package/dist/commands/improve/proposal-envelope.js +31 -0
- package/dist/commands/improve/reflect.js +983 -794
- package/dist/commands/improve/run-context.js +119 -0
- package/dist/commands/improve/salience.js +24 -127
- package/dist/commands/improve/session-asset.js +7 -3
- package/dist/commands/improve/shared.js +14 -34
- package/dist/commands/improve/source-identity.js +28 -0
- package/dist/commands/improve/triage.js +20 -17
- package/dist/commands/lint/base-linter.js +340 -313
- package/dist/commands/lint/env-key-rules.js +31 -47
- package/dist/commands/lint/index.js +185 -30
- package/dist/commands/{events.js → log.js} +28 -38
- package/dist/commands/migrate-cli.js +54 -0
- package/dist/commands/migration-tool.js +55 -0
- package/dist/commands/observability-cli.js +70 -208
- package/dist/commands/proposal/diff-format.js +50 -0
- package/dist/commands/proposal/drain-policies.js +0 -6
- package/dist/commands/proposal/drain.js +91 -40
- package/dist/commands/proposal/proposal-cli.js +134 -132
- package/dist/commands/proposal/proposal-types.js +56 -0
- package/dist/commands/proposal/proposal.js +83 -65
- package/dist/commands/proposal/propose-cli.js +88 -0
- package/dist/commands/proposal/propose.js +105 -88
- package/dist/commands/proposal/repository.js +1303 -278
- package/dist/commands/proposal/validators/proposal-quality-validators.js +16 -6
- package/dist/commands/proposal/validators/proposal-validators.js +61 -12
- package/dist/commands/proposal/validators/proposals.js +6 -8
- package/dist/commands/read/curate.js +78 -73
- package/dist/commands/read/knowledge.js +510 -13
- package/dist/commands/read/registry-search.js +2 -2
- package/dist/commands/read/remember-cli.js +84 -15
- package/dist/commands/read/search-cli.js +203 -96
- package/dist/commands/read/search.js +126 -94
- package/dist/commands/read/show.js +226 -250
- package/dist/commands/registry-cli.js +34 -60
- package/dist/commands/remember.js +18 -57
- package/dist/commands/sources/add-cli.js +104 -49
- package/dist/commands/sources/bundle-cli.js +166 -0
- package/dist/commands/sources/bundle-config-ops.js +63 -0
- package/dist/commands/sources/info.js +27 -15
- package/dist/commands/sources/init.js +30 -40
- package/dist/commands/sources/installed-stashes.js +469 -172
- package/dist/commands/sources/migration-help.js +7 -4
- package/dist/commands/sources/schema-repair.js +10 -9
- package/dist/commands/sources/self-update.js +182 -121
- package/dist/commands/sources/source-add.js +169 -178
- package/dist/commands/sources/source-clone.js +144 -41
- package/dist/commands/sources/source-manage.js +94 -59
- package/dist/commands/sources/sources-cli.js +64 -205
- package/dist/commands/sources/stash-cli.js +91 -54
- package/dist/commands/sources/stash-skeleton.js +1 -1
- package/dist/commands/tasks/tasks-cli.js +106 -104
- package/dist/commands/tasks/tasks.js +445 -262
- package/dist/commands/workflow-cli.js +232 -121
- package/dist/core/action-contributors.js +1 -1
- package/dist/core/activation-policy.js +49 -0
- package/dist/core/adapter/adapters/agent-skills-adapter.js +181 -0
- package/dist/core/adapter/adapters/akm-adapter.js +528 -0
- package/dist/core/adapter/adapters/akm-lint.js +392 -0
- package/dist/core/adapter/adapters/akm-metadata.js +387 -0
- package/dist/core/adapter/adapters/akm-task-adapter.js +149 -0
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +180 -0
- package/dist/core/adapter/adapters/claude-adapter.js +61 -0
- package/dist/core/adapter/adapters/dotenv-adapter.js +187 -0
- package/dist/core/adapter/adapters/generic-files-adapter.js +119 -0
- package/dist/core/adapter/adapters/index.js +80 -0
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +419 -0
- package/dist/core/adapter/adapters/okf-adapter.js +391 -0
- package/dist/core/adapter/adapters/opencode-adapter.js +68 -0
- package/dist/core/adapter/adapters/shared.js +286 -0
- package/dist/core/adapter/adapters/tool-dir-shared.js +217 -0
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +155 -0
- package/dist/core/adapter/bundle-adapter.js +4 -0
- package/dist/core/adapter/detect-adapter.js +17 -0
- package/dist/core/adapter/recognize-match.js +44 -0
- package/dist/core/adapter/registry.js +56 -0
- package/dist/core/adapter/types.js +4 -0
- package/dist/core/asset/akm-markdown.js +30 -0
- package/dist/core/asset/asset-placement.js +243 -0
- package/dist/core/asset/asset-ref.js +110 -79
- package/dist/core/asset/asset-serialize.js +20 -0
- package/dist/core/asset/frontmatter.js +28 -12
- package/dist/core/asset/markdown.js +40 -51
- package/dist/core/asset/resolve-ref.js +274 -0
- package/dist/core/asset/stash-meta.js +2 -2
- package/dist/core/bundle-id.js +51 -0
- package/dist/core/common.js +281 -86
- package/dist/core/config/config-io.js +42 -128
- package/dist/core/config/config-schema.js +233 -834
- package/dist/core/config/config-sources.js +162 -39
- package/dist/core/config/config-types.js +16 -11
- package/dist/core/config/config-version.js +29 -0
- package/dist/core/config/config-walker.js +126 -37
- package/dist/core/config/config.js +154 -331
- package/dist/core/config/deep-merge.js +41 -0
- package/dist/core/config/engine-semantics.js +28 -0
- package/dist/core/config/experimental.js +21 -0
- package/dist/core/config/schema/embedding.js +38 -0
- package/dist/core/config/schema/engines.js +116 -0
- package/dist/core/config/schema/experimental.js +47 -0
- package/dist/core/config/schema/feedback.js +31 -0
- package/dist/core/config/schema/improve-processes.js +389 -0
- package/dist/core/config/schema/improve.js +94 -0
- package/dist/core/config/schema/index-config.js +176 -0
- package/dist/core/config/schema/output.js +18 -0
- package/dist/core/config/schema/primitives.js +94 -0
- package/dist/core/config/schema/search.js +30 -0
- package/dist/core/config/schema/setup.js +18 -0
- package/dist/core/config/schema/sources-bundles.js +169 -0
- package/dist/core/config/schema/workflow.js +29 -0
- package/dist/core/env-secret-ref.js +155 -20
- package/dist/core/errors.js +17 -15
- package/dist/core/events-types.js +4 -0
- package/dist/core/events.js +46 -128
- package/dist/core/extra-params.js +62 -0
- package/dist/core/file-change.js +17 -0
- package/dist/core/file-lock.js +202 -57
- package/dist/core/fs-txn.js +392 -0
- package/dist/core/git-message.js +59 -0
- package/dist/core/improve-result.js +167 -0
- package/dist/core/json-schema.js +142 -0
- package/dist/core/lesson-lint.js +1 -17
- package/dist/core/logs-db.js +1 -1
- package/dist/core/maintenance-barrier.js +135 -0
- package/dist/core/migration-operation.js +44 -0
- package/dist/core/mutation-target.js +78 -0
- package/dist/core/paths.js +22 -25
- package/dist/core/platform.js +10 -0
- package/dist/core/recognition-util.js +128 -0
- package/dist/core/redaction.js +392 -0
- package/dist/core/standards/resolve-standards-context.js +36 -65
- package/dist/core/standards/resolve-stash-standards.js +2 -2
- package/dist/core/standards/resolve-type-conventions.js +5 -5
- package/dist/core/state/migrations.js +242 -11
- package/dist/core/state-db.js +98 -10
- package/dist/core/structured.js +1 -1
- package/dist/core/subprocess.js +303 -0
- package/dist/core/text-truncation.js +9 -5
- package/dist/core/time.js +20 -0
- package/dist/core/type-presentation.js +130 -0
- package/dist/core/warn.js +0 -3
- package/dist/core/write-source.js +834 -118
- package/dist/indexer/bundle-identity-guard.js +92 -0
- package/dist/indexer/db/graph-db.js +1 -25
- package/dist/indexer/db/llm-cache.js +1 -1
- package/dist/indexer/ensure-index.js +30 -9
- package/dist/indexer/graph/graph-boost.js +9 -30
- package/dist/indexer/graph/graph-extraction.js +41 -27
- package/dist/indexer/graph/graph-types.js +4 -0
- package/dist/indexer/index-writer-lock.js +93 -49
- package/dist/indexer/index-written-assets.js +100 -53
- package/dist/indexer/indexer.js +746 -329
- package/dist/indexer/init.js +18 -25
- package/dist/indexer/installations.js +142 -0
- package/dist/indexer/passes/dir-staleness.js +18 -10
- package/dist/indexer/passes/memory-inference.js +25 -15
- package/dist/indexer/passes/metadata.js +412 -243
- package/dist/indexer/scan/doc-to-entry.js +160 -0
- package/dist/indexer/scan/drain-dir.js +134 -0
- package/dist/indexer/search/db-search.js +292 -108
- package/dist/indexer/search/fts-query.js +64 -0
- package/dist/indexer/search/ranking-contributors.js +145 -25
- package/dist/indexer/search/ranking-types.js +4 -0
- package/dist/indexer/search/ranking.js +28 -71
- package/dist/indexer/search/search-attribution.js +67 -0
- package/dist/indexer/search/search-fields.js +18 -3
- package/dist/indexer/search/search-hit-enrichers.js +30 -40
- package/dist/indexer/search/search-source.js +157 -111
- package/dist/indexer/search/semantic-status.js +4 -1
- package/dist/indexer/usage/usage-events.js +10 -30
- package/dist/indexer/walk/file-context.js +3 -45
- package/dist/indexer/walk/matchers.js +42 -34
- package/dist/indexer/walk/path-resolver.js +11 -5
- package/dist/indexer/walk/walker.js +42 -14
- package/dist/integrations/agent/builder-shared.js +7 -0
- package/dist/integrations/agent/builders.js +5 -56
- package/dist/integrations/agent/config.js +3 -143
- package/dist/integrations/agent/detect.js +17 -2
- package/dist/integrations/agent/engine-resolution.js +231 -0
- package/dist/integrations/agent/index.js +1 -2
- package/dist/integrations/agent/model-aliases.js +16 -2
- package/dist/integrations/agent/profiles.js +36 -62
- package/dist/integrations/agent/prompts.js +46 -18
- package/dist/integrations/agent/runner-dispatch.js +93 -4
- package/dist/integrations/agent/runner.js +76 -208
- package/dist/integrations/agent/spawn.js +88 -196
- package/dist/integrations/harnesses/aider/agent-builder.js +114 -0
- package/dist/integrations/harnesses/aider/index.js +48 -0
- package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
- package/dist/integrations/harnesses/amazonq/agent-builder.js +147 -0
- package/dist/integrations/harnesses/amazonq/index.js +45 -0
- package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
- package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
- package/dist/integrations/harnesses/claude/config-import.js +1 -3
- package/dist/integrations/harnesses/claude/index.js +24 -35
- package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
- package/dist/integrations/harnesses/claude/session-log.js +27 -75
- package/dist/integrations/harnesses/codex/agent-builder.js +138 -0
- package/dist/integrations/harnesses/codex/index.js +52 -0
- package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
- package/dist/integrations/harnesses/copilot/agent-builder.js +122 -0
- package/dist/integrations/harnesses/copilot/index.js +48 -0
- package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
- package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
- package/dist/integrations/harnesses/gemini/index.js +48 -0
- package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
- package/dist/integrations/harnesses/ids.js +24 -0
- package/dist/integrations/harnesses/index.js +54 -34
- package/dist/integrations/harnesses/opencode/agent-builder.js +23 -5
- package/dist/integrations/harnesses/opencode/config-import.js +1 -3
- package/dist/integrations/harnesses/opencode/index.js +14 -32
- package/dist/integrations/harnesses/opencode/session-log.js +67 -125
- package/dist/integrations/harnesses/opencode-sdk/harness.js +51 -0
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
- package/dist/integrations/harnesses/openhands/agent-builder.js +128 -0
- package/dist/integrations/harnesses/openhands/index.js +48 -0
- package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
- package/dist/integrations/harnesses/pi/agent-builder.js +97 -0
- package/dist/integrations/harnesses/pi/index.js +45 -0
- package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
- package/dist/integrations/harnesses/shared.js +17 -0
- package/dist/integrations/harnesses/types.js +43 -32
- package/dist/integrations/lockfile.js +211 -24
- package/dist/integrations/session-logs/index.js +36 -39
- package/dist/integrations/session-logs/provider-base.js +113 -0
- package/dist/llm/client.js +182 -110
- package/dist/llm/embedders/deterministic.js +2 -2
- package/dist/llm/embedders/remote.js +21 -9
- package/dist/llm/feature-gate.js +17 -57
- package/dist/llm/graph-extract.js +12 -13
- package/dist/llm/index-passes.js +8 -42
- package/dist/llm/memory-infer.js +144 -1
- package/dist/llm/metadata-enhance.js +45 -30
- package/dist/llm/structured-call.js +16 -8
- package/dist/llm/usage-persist.js +30 -5
- package/dist/llm/usage-telemetry.js +59 -6
- package/dist/output/cli-hints.js +1 -2
- package/dist/output/command-registry.js +27 -0
- package/dist/output/context.js +22 -7
- package/dist/output/format-exempt.js +80 -0
- package/dist/output/generic-render.js +251 -0
- package/dist/output/html-render.js +11 -16
- package/dist/output/render-registry.js +57 -0
- package/dist/output/renderers.js +14 -279
- package/dist/output/shapes/curate.js +10 -1
- package/dist/output/shapes/events.js +12 -7
- package/dist/output/shapes/helpers.js +58 -84
- package/dist/output/shapes/passthrough.js +11 -39
- package/dist/output/shapes/proposal/producer.js +15 -7
- package/dist/output/shapes/registry.js +12 -6
- package/dist/output/shapes.js +0 -9
- package/dist/output/text/{init.js → bundle-create.js} +3 -1
- package/dist/output/text/bundle-show.js +7 -0
- package/dist/output/text/command-format.js +562 -0
- package/dist/output/text/env.js +1 -3
- package/dist/output/text/events.js +8 -7
- package/dist/output/text/helpers.js +15 -1164
- package/dist/output/text/proposal/producer.js +4 -2
- package/dist/output/text/proposal-format.js +202 -0
- package/dist/output/text/registry-commands.js +1 -2
- package/dist/output/text/registry.js +12 -6
- package/dist/output/text/show-directives.js +117 -0
- package/dist/output/text/show-format.js +103 -0
- package/dist/output/text/sync.js +5 -0
- package/dist/output/text/workflow-format.js +332 -0
- package/dist/output/text/workflow.js +3 -2
- package/dist/output/text.js +10 -19
- package/dist/registry/factory.js +4 -6
- package/dist/registry/origin-resolve.js +16 -27
- package/dist/registry/providers/skills-sh.js +3 -3
- package/dist/registry/providers/static-index.js +15 -25
- package/dist/registry/resolve.js +43 -94
- package/dist/registry/semver.js +43 -0
- package/dist/runtime.js +81 -12
- package/dist/scripts/akm-migrate.js +35529 -0
- package/dist/setup/detect.js +5 -7
- package/dist/setup/detected-engines.js +136 -0
- package/dist/setup/engine-config.js +100 -0
- package/dist/setup/registry-stash-loader.js +3 -3
- package/dist/setup/semantic-assets.js +12 -9
- package/dist/setup/setup.js +444 -208
- package/dist/setup/steps/connection-shared.js +120 -0
- package/dist/setup/steps/connection.js +108 -305
- package/dist/setup/steps/platforms.js +13 -12
- package/dist/setup/steps/semantic.js +15 -3
- package/dist/setup/steps/sources.js +21 -15
- package/dist/setup/steps/stashdir.js +6 -4
- package/dist/setup/steps/tasks.js +236 -119
- package/dist/setup/steps.js +3 -2
- package/dist/sources/freshness.js +39 -0
- package/dist/sources/provider-factory.js +11 -17
- package/dist/sources/providers/filesystem.js +2 -3
- package/dist/sources/providers/git-install.js +278 -34
- package/dist/sources/providers/git-provider.js +54 -56
- package/dist/sources/providers/git-stash.js +420 -91
- package/dist/sources/providers/git.js +2 -2
- package/dist/sources/providers/npm.js +16 -19
- package/dist/sources/providers/provider-utils.js +47 -22
- package/dist/sources/providers/sync-from-ref.js +3 -9
- package/dist/sources/providers/website.js +2 -2
- package/dist/sources/resolve.js +11 -10
- package/dist/sources/snapshot-fetchers/types.js +4 -0
- package/dist/sources/{website-ingest.js → snapshot-fetchers/website-ingest.js} +110 -41
- package/dist/storage/database.js +60 -4
- package/dist/storage/engines/sqlite-migrations.js +156 -5
- package/dist/storage/locations.js +1 -2
- package/dist/storage/repositories/canaries-repository.js +1 -1
- package/dist/storage/repositories/events-repository.js +51 -11
- package/dist/storage/repositories/improve-runs-repository.js +6 -32
- package/dist/storage/repositories/index-connection.js +79 -0
- package/dist/storage/repositories/index-db.js +4 -3
- package/dist/storage/repositories/index-entries-repository.js +863 -0
- package/dist/{indexer/db/entry-mapper.js → storage/repositories/index-entry-mapper.js} +19 -2
- package/dist/storage/repositories/index-entry-types.js +4 -0
- package/dist/storage/repositories/index-fts-repository.js +167 -0
- package/dist/storage/repositories/index-llm-cache-repository.js +108 -0
- package/dist/storage/repositories/index-meta-repository.js +49 -0
- package/dist/{indexer/db/schema.js → storage/repositories/index-schema.js} +226 -100
- package/dist/storage/repositories/index-sql.js +12 -0
- package/dist/storage/repositories/index-utility-repository.js +356 -0
- package/dist/storage/repositories/index-vec-repository.js +250 -0
- package/dist/storage/repositories/outcome-repository.js +119 -0
- package/dist/storage/repositories/proposals-repository.js +317 -75
- package/dist/storage/repositories/registry-cache.js +1 -1
- package/dist/storage/repositories/salience-repository.js +172 -0
- package/dist/storage/repositories/task-history-repository.js +110 -3
- package/dist/storage/repositories/workflow-runs-repository.js +240 -19
- package/dist/tasks/backends/cron.js +169 -46
- package/dist/tasks/backends/exec-utils.js +76 -3
- package/dist/tasks/backends/index.js +6 -9
- package/dist/tasks/backends/launchd.js +292 -55
- package/dist/tasks/backends/schtasks.js +557 -70
- package/dist/tasks/backends/types.js +4 -0
- package/dist/tasks/command-executable.js +93 -0
- package/dist/tasks/embedded.js +56 -38
- package/dist/tasks/parser.js +156 -64
- package/dist/tasks/resolve-akm-bin.js +144 -51
- package/dist/tasks/runner.js +377 -209
- package/dist/tasks/schedule.js +108 -19
- package/dist/tasks/scheduler-invocation.js +296 -0
- package/dist/tasks/schema.js +1 -1
- package/dist/tasks/task-id.js +35 -0
- package/dist/tasks/validator.js +30 -16
- package/dist/text-import-hook.mjs +1 -1
- package/dist/workflows/authoring/authoring.js +104 -43
- package/dist/workflows/authoring/scope-key.js +1 -1
- package/dist/workflows/cli.js +0 -16
- package/dist/workflows/concurrency-policy.js +15 -0
- package/dist/workflows/exec/brief.js +450 -0
- package/dist/workflows/exec/frozen-judge.js +47 -0
- package/dist/workflows/exec/native-executor.js +1038 -0
- package/dist/workflows/exec/param-secrets.js +115 -0
- package/dist/workflows/exec/report.js +1460 -0
- package/dist/workflows/exec/run-workflow.js +602 -0
- package/dist/workflows/exec/scheduler.js +71 -0
- package/dist/workflows/exec/step-work.js +1190 -0
- package/dist/workflows/exec/unit-writer.js +23 -0
- package/dist/workflows/exec/workflow-engine-gate.js +67 -0
- package/dist/workflows/exec/worktree.js +171 -0
- package/dist/workflows/ir/compile.js +246 -0
- package/dist/workflows/ir/freeze.js +233 -0
- package/dist/workflows/ir/params.js +54 -0
- package/dist/workflows/ir/plan-hash.js +68 -0
- package/dist/workflows/ir/schema.js +540 -0
- package/dist/workflows/parser.js +878 -304
- package/dist/workflows/program/expressions.js +181 -0
- package/dist/workflows/program/schema.js +51 -0
- package/dist/workflows/renderer.js +100 -45
- package/dist/workflows/resource-limits.js +22 -0
- package/dist/workflows/runtime/agent-identity.js +59 -14
- package/dist/workflows/runtime/checkin.js +1 -1
- package/dist/workflows/runtime/plan-classifier.js +131 -0
- package/dist/workflows/runtime/runs.js +376 -119
- package/dist/workflows/runtime/unit-checkin.js +45 -0
- package/dist/workflows/runtime/unit-phases.js +20 -0
- package/dist/workflows/runtime/workflow-asset-loader.js +241 -40
- package/dist/workflows/schema.js +1 -11
- package/dist/workflows/validate-summary.js +2 -3
- package/dist/workflows/validator.js +52 -30
- package/docs/README.md +42 -78
- package/docs/migration/README.md +8 -0
- package/docs/migration/release-notes/0.6.0.md +1 -1
- package/docs/migration/release-notes/0.7.0.md +9 -8
- package/docs/migration/release-notes/0.9.0.md +158 -14
- package/docs/migration/v0.7-to-v0.8.md +46 -47
- package/docs/migration/v0.8-to-v0.9.md +844 -0
- package/docs/reference/README.md +12 -0
- package/docs/reference/data-and-telemetry.md +333 -0
- package/package.json +21 -17
- package/schemas/akm-asset-envelope.json +93 -0
- package/schemas/akm-config.json +4636 -0
- package/schemas/akm-task.json +87 -0
- package/schemas/akm-workflow.json +373 -0
- package/dist/akm-migrate-storage +0 -38
- package/dist/assets/help/help-accept.md +0 -12
- package/dist/assets/help/help-improve.md +0 -84
- package/dist/assets/help/help-proposals.md +0 -17
- package/dist/assets/help/help-propose.md +0 -17
- package/dist/assets/help/help-reject.md +0 -11
- package/dist/assets/profiles/frequent.json +0 -13
- package/dist/assets/profiles/recombine-only.json +0 -21
- package/dist/assets/profiles/reflect-distill.json +0 -30
- package/dist/assets/profiles/synthesize.json +0 -15
- package/dist/assets/prompts/procedural-system.md +0 -44
- package/dist/assets/prompts/recombine-system.md +0 -40
- package/dist/assets/prompts/staleness-detect-system.md +0 -6
- package/dist/assets/tasks/core/backup.yml +0 -4
- package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
- package/dist/assets/templates/html/default.html +0 -78
- package/dist/assets/templates/html/vendor/echarts.min.js +0 -45
- package/dist/assets/wiki/index-template.md +0 -12
- package/dist/assets/wiki/ingest-workflow-template.md +0 -83
- package/dist/assets/wiki/log-template.md +0 -8
- package/dist/assets/wiki/schema-template.md +0 -61
- package/dist/cli/config-migrate.js +0 -150
- package/dist/cli/config-validate.js +0 -39
- package/dist/commands/graph/graph-cli.js +0 -124
- package/dist/commands/graph/graph.js +0 -487
- package/dist/commands/improve/calibration.js +0 -161
- package/dist/commands/improve/dedup.js +0 -482
- package/dist/commands/improve/extract-watch.js +0 -140
- package/dist/commands/improve/hot-probation.js +0 -45
- package/dist/commands/improve/improve-auto-accept.js +0 -276
- package/dist/commands/improve/improve-profiles.js +0 -168
- package/dist/commands/improve/procedural.js +0 -398
- package/dist/commands/improve/recombine.js +0 -818
- package/dist/commands/improve/schema-similarity-gate.js +0 -168
- package/dist/commands/lint/agent-linter.js +0 -44
- package/dist/commands/lint/command-linter.js +0 -44
- package/dist/commands/lint/default-linter.js +0 -16
- package/dist/commands/lint/fact-linter.js +0 -39
- package/dist/commands/lint/knowledge-linter.js +0 -16
- package/dist/commands/lint/memory-linter.js +0 -61
- package/dist/commands/lint/registry.js +0 -41
- package/dist/commands/lint/skill-linter.js +0 -45
- package/dist/commands/lint/task-linter.js +0 -50
- package/dist/commands/lint/workflow-linter.js +0 -81
- package/dist/commands/proposal/legacy-import.js +0 -115
- package/dist/commands/sources/history.js +0 -196
- package/dist/commands/tasks/default-tasks.js +0 -186
- package/dist/commands/wiki-cli.js +0 -292
- package/dist/core/asset/asset-registry.js +0 -76
- package/dist/core/asset/asset-spec.js +0 -259
- package/dist/core/config/config-migration.js +0 -602
- package/dist/core/deep-merge.js +0 -38
- package/dist/core/eval/rank-metrics.js +0 -113
- package/dist/core/ripgrep/install.js +0 -163
- package/dist/core/ripgrep/resolve.js +0 -81
- package/dist/indexer/db/db.js +0 -1413
- package/dist/indexer/manifest.js +0 -170
- package/dist/indexer/passes/metadata-contributors.js +0 -31
- package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -94
- package/dist/integrations/harnesses/opencode-sdk/index.js +0 -49
- package/dist/llm/call-ai.js +0 -62
- package/dist/llm/memory-infer-impl.js +0 -138
- package/dist/output/shapes/distill.js +0 -14
- package/dist/output/shapes/history.js +0 -11
- package/dist/output/text/distill.js +0 -6
- package/dist/output/text/enable-disable.js +0 -8
- package/dist/output/text/history.js +0 -6
- package/dist/output/text/wiki.js +0 -16
- package/dist/registry/build-index.js +0 -386
- package/dist/scripts/migrate-storage.js +0 -19108
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +0 -9411
- package/dist/scripts/migrations/v16-to-v17.js +0 -141
- package/dist/setup/legacy-config.js +0 -106
- package/dist/storage/repositories/consolidation-repository.js +0 -38
- package/dist/storage/repositories/recombine-repository.js +0 -213
- package/dist/wiki/wiki-templates.js +0 -15
- package/dist/wiki/wiki.js +0 -1012
- package/dist/workflows/db.js +0 -215
- package/docs/data-and-telemetry.md +0 -226
- /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/registry.js +0 -0
- /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/youtube.js +0 -0
package/dist/workflows/parser.js
CHANGED
|
@@ -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
|
-
*
|
|
5
|
+
* Unified workflow markdown parser (workflow-format-unification).
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* line
|
|
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 {
|
|
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
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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(
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
79
|
-
|
|
125
|
+
for (const problem of doc.errors) {
|
|
126
|
+
const offset = Array.isArray(problem.pos) ? problem.pos[0] : 0;
|
|
80
127
|
errors.push({
|
|
81
|
-
line:
|
|
82
|
-
message:
|
|
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
|
-
...(
|
|
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,
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
250
|
+
if (h.level !== 2)
|
|
135
251
|
continue;
|
|
136
|
-
|
|
137
|
-
if (!stepTitle) {
|
|
252
|
+
if (!declaredIds.has(h.text)) {
|
|
138
253
|
errors.push({
|
|
139
254
|
line: h.line,
|
|
140
|
-
message: `
|
|
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
|
-
|
|
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 "${
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
source:
|
|
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 (
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
let
|
|
247
|
-
for (let
|
|
248
|
-
const
|
|
249
|
-
if (
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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 (
|
|
295
|
+
if (found) {
|
|
255
296
|
errors.push({
|
|
256
|
-
line:
|
|
257
|
-
message: `Step "${
|
|
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
|
-
|
|
262
|
-
foundLine = lineNum;
|
|
302
|
+
found = { headingLine: h.line, bodyStart: h.line + 1, bodyEnd: sectionEnd };
|
|
263
303
|
}
|
|
264
|
-
|
|
265
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
311
|
-
|
|
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 (
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
334
|
-
return
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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 (
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
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
|
|
354
|
-
|
|
355
|
-
if (v === undefined || v === null)
|
|
462
|
+
function parseBudget(ctx, raw) {
|
|
463
|
+
if (raw === undefined)
|
|
356
464
|
return undefined;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
365
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
});
|
|
375
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
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
|
}
|