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