akm-cli 0.9.0-rc.0 → 0.9.0-rc.2
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 +339 -2
- package/SECURITY.md +23 -24
- package/dist/assets/help/help-improve.md +10 -10
- package/dist/assets/hints/cli-hints-full.md +44 -10
- package/dist/assets/hints/cli-hints-short.md +6 -2
- package/dist/assets/{profiles → improve-strategies}/default.json +1 -0
- package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +1 -1
- package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +2 -3
- package/dist/assets/{profiles → improve-strategies}/reflect-distill.json +3 -4
- package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
- package/dist/assets/stash-skeleton/README.md +28 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +12 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +11 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +9 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +7 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +7 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +98 -0
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +63 -0
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +127 -0
- package/dist/assets/tasks/core/backup.yml +1 -0
- package/dist/assets/tasks/core/extract.yml +1 -0
- package/dist/assets/tasks/core/improve.yml +1 -0
- 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 +1 -0
- package/dist/assets/tasks/graph-refresh-weekly.yml +4 -4
- package/dist/assets/templates/html/health.html +5 -1
- package/dist/cli/config-migrate.js +31 -138
- package/dist/cli/config-validate.js +10 -8
- package/dist/cli.js +48 -14
- package/dist/commands/agent/agent-dispatch.js +17 -16
- package/dist/commands/agent/agent-support.js +0 -24
- package/dist/commands/agent/contribute-cli.js +5 -15
- package/dist/commands/backup-cli.js +54 -0
- package/dist/commands/config-cli.js +45 -159
- package/dist/commands/env/env-binding.js +95 -0
- package/dist/commands/env/env-cli.js +8 -65
- package/dist/commands/env/secret.js +8 -5
- package/dist/commands/health/checks.js +130 -83
- package/dist/commands/health/html-report.js +4 -0
- package/dist/commands/health/improve-metrics.js +30 -32
- package/dist/commands/health/llm-usage.js +19 -19
- package/dist/commands/health/md-report.js +4 -0
- package/dist/commands/health/metrics.js +2 -1
- package/dist/commands/health/surfaces.js +5 -4
- package/dist/commands/health.js +1 -1
- package/dist/commands/improve/consolidate/chunking.js +2 -2
- package/dist/commands/improve/consolidate.js +28 -25
- package/dist/commands/improve/distill/promote-memory.js +5 -12
- package/dist/commands/improve/distill/quality-gate.js +5 -7
- package/dist/commands/improve/distill.js +16 -5
- package/dist/commands/improve/eligibility.js +22 -12
- package/dist/commands/improve/extract-cli.js +47 -19
- package/dist/commands/improve/extract.js +110 -62
- package/dist/commands/improve/improve-cli.js +38 -16
- package/dist/commands/improve/improve-result-file.js +30 -24
- package/dist/commands/improve/improve-strategies.js +137 -0
- package/dist/commands/improve/improve.js +60 -30
- package/dist/commands/improve/locks.js +66 -45
- package/dist/commands/improve/loop-stages.js +75 -33
- package/dist/commands/improve/memory/memory-belief.js +79 -7
- package/dist/commands/improve/memory/memory-contradiction-detect.js +12 -4
- package/dist/commands/improve/preparation.js +71 -73
- package/dist/commands/improve/procedural.js +3 -2
- package/dist/commands/improve/recombine.js +2 -1
- package/dist/commands/improve/reflect.js +119 -214
- package/dist/commands/improve/shared.js +11 -5
- package/dist/commands/lint/base-linter.js +152 -42
- package/dist/commands/mv-cli.js +809 -0
- package/dist/commands/proposal/proposal-cli.js +18 -8
- package/dist/commands/proposal/propose.js +64 -69
- package/dist/commands/read/knowledge.js +436 -4
- package/dist/commands/read/remember-cli.js +39 -2
- package/dist/commands/read/search-cli.js +6 -1
- package/dist/commands/registry-cli.js +29 -14
- package/dist/commands/remember.js +2 -0
- package/dist/commands/sources/init.js +13 -14
- package/dist/commands/sources/migration-help.js +7 -4
- package/dist/commands/sources/schema-repair.js +2 -4
- package/dist/commands/sources/source-add.js +62 -73
- package/dist/commands/sources/source-manage.js +50 -46
- package/dist/commands/sources/stash-cli.js +41 -4
- package/dist/commands/tasks/default-tasks.js +12 -12
- package/dist/commands/tasks/tasks-cli.js +7 -3
- package/dist/commands/tasks/tasks.js +113 -18
- package/dist/commands/wiki-cli.js +9 -10
- package/dist/commands/workflow-cli.js +276 -12
- package/dist/core/asset/asset-spec.js +58 -1
- package/dist/core/asset/frontmatter.js +12 -2
- package/dist/core/common.js +5 -3
- package/dist/core/config/config-io.js +28 -17
- package/dist/core/config/config-schema.js +379 -66
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config-version.js +29 -0
- package/dist/core/config/config-walker.js +98 -27
- package/dist/core/config/config.js +132 -266
- package/dist/core/config/deep-merge.js +41 -0
- package/dist/core/config/engine-semantics.js +32 -0
- package/dist/core/errors.js +2 -2
- package/dist/core/extra-params.js +61 -0
- package/dist/core/file-lock.js +201 -56
- package/dist/core/improve-result.js +178 -0
- package/dist/core/json-schema.js +142 -0
- package/dist/core/maintenance-barrier.js +119 -0
- package/dist/core/migration-backup.js +416 -0
- package/dist/core/paths.js +3 -0
- package/dist/core/redaction.js +358 -0
- package/dist/core/state/migrations.js +17 -2
- package/dist/core/state-db.js +44 -1
- package/dist/indexer/db/db.js +118 -2
- package/dist/indexer/graph/graph-extraction.js +28 -16
- package/dist/indexer/index-writer-lock.js +31 -24
- package/dist/indexer/index-written-assets.js +15 -6
- package/dist/indexer/indexer.js +47 -2
- package/dist/indexer/passes/memory-inference.js +10 -6
- package/dist/indexer/passes/metadata.js +250 -0
- package/dist/indexer/search/db-search.js +111 -44
- package/dist/indexer/search/fts-query.js +41 -0
- package/dist/indexer/search/ranking-contributors.js +48 -0
- package/dist/indexer/search/ranking.js +36 -23
- package/dist/indexer/search/search-fields.js +11 -1
- package/dist/indexer/walk/matchers.js +39 -0
- package/dist/integrations/agent/builder-shared.js +7 -0
- package/dist/integrations/agent/builders.js +5 -50
- package/dist/integrations/agent/config.js +3 -143
- package/dist/integrations/agent/detect.js +17 -2
- package/dist/integrations/agent/engine-resolution.js +202 -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/runner-dispatch.js +91 -4
- package/dist/integrations/agent/runner.js +76 -207
- package/dist/integrations/agent/spawn.js +141 -20
- package/dist/integrations/harnesses/aider/agent-builder.js +112 -0
- package/dist/integrations/harnesses/aider/index.js +57 -0
- package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
- package/dist/integrations/harnesses/amazonq/agent-builder.js +152 -0
- package/dist/integrations/harnesses/amazonq/index.js +58 -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/index.js +25 -25
- package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
- package/dist/integrations/harnesses/codex/agent-builder.js +136 -0
- package/dist/integrations/harnesses/codex/index.js +62 -0
- package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
- package/dist/integrations/harnesses/copilot/agent-builder.js +121 -0
- package/dist/integrations/harnesses/copilot/index.js +59 -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 +59 -0
- package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
- package/dist/integrations/harnesses/index.js +27 -28
- package/dist/integrations/harnesses/opencode/agent-builder.js +2 -3
- package/dist/integrations/harnesses/opencode/index.js +15 -22
- package/dist/integrations/harnesses/opencode-sdk/harness.js +60 -0
- package/dist/integrations/harnesses/opencode-sdk/index.js +8 -32
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
- package/dist/integrations/harnesses/openhands/agent-builder.js +125 -0
- package/dist/integrations/harnesses/openhands/index.js +57 -0
- package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
- package/dist/integrations/harnesses/pi/agent-builder.js +103 -0
- package/dist/integrations/harnesses/pi/index.js +57 -0
- package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
- package/dist/integrations/harnesses/types.js +8 -32
- package/dist/integrations/lockfile.js +32 -21
- package/dist/integrations/session-logs/index.js +24 -11
- package/dist/llm/client.js +48 -14
- package/dist/llm/feature-gate.js +15 -47
- package/dist/llm/graph-extract.js +1 -1
- package/dist/llm/index-passes.js +8 -42
- package/dist/llm/memory-infer-impl.js +1 -1
- package/dist/llm/usage-persist.js +4 -0
- package/dist/llm/usage-telemetry.js +35 -5
- package/dist/output/renderers.js +3 -2
- package/dist/output/shapes/helpers.js +2 -1
- package/dist/output/shapes/passthrough.js +6 -0
- package/dist/output/text/helpers.js +215 -2
- package/dist/output/text/workflow.js +3 -1
- package/dist/schemas/akm-config.json +16638 -0
- package/dist/schemas/akm-task.json +87 -0
- package/dist/schemas/akm-workflow.json +372 -0
- package/dist/scripts/migrate-storage.js +10944 -8801
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9247 -350
- package/dist/setup/detected-engines.js +142 -0
- package/dist/setup/engine-config.js +89 -0
- package/dist/setup/setup.js +236 -132
- package/dist/setup/steps/connection.js +61 -32
- package/dist/setup/steps/platforms.js +4 -4
- package/dist/setup/steps.js +3 -2
- package/dist/storage/database.js +13 -1
- package/dist/storage/engines/sqlite-migrations.js +1 -0
- package/dist/storage/repositories/improve-runs-repository.js +5 -5
- package/dist/storage/repositories/task-history-repository.js +78 -0
- package/dist/storage/repositories/workflow-runs-repository.js +190 -1
- package/dist/tasks/parser.js +138 -52
- package/dist/tasks/runner.js +71 -75
- package/dist/tasks/schema.js +1 -1
- package/dist/tasks/validator.js +11 -6
- package/dist/text-import-hook.mjs +1 -1
- package/dist/wiki/wiki.js +9 -8
- package/dist/workflows/authoring/authoring.js +123 -10
- package/dist/workflows/authoring/workflow-program-template.yaml +31 -0
- package/dist/workflows/cli.js +4 -0
- package/dist/workflows/concurrency-policy.js +15 -0
- package/dist/workflows/db.js +200 -13
- package/dist/workflows/exec/brief.js +478 -0
- package/dist/workflows/exec/frozen-judge.js +47 -0
- package/dist/workflows/exec/native-executor.js +1034 -0
- package/dist/workflows/exec/param-secrets.js +115 -0
- package/dist/workflows/exec/report.js +1355 -0
- package/dist/workflows/exec/run-workflow.js +609 -0
- package/dist/workflows/exec/scheduler.js +71 -0
- package/dist/workflows/exec/step-work.js +1212 -0
- package/dist/workflows/exec/unit-writer.js +23 -0
- package/dist/workflows/exec/watch.js +116 -0
- package/dist/workflows/exec/worktree.js +171 -0
- package/dist/workflows/ir/compile.js +375 -0
- package/dist/workflows/ir/freeze.js +243 -0
- package/dist/workflows/ir/params.js +54 -0
- package/dist/workflows/ir/plan-hash.js +68 -0
- package/dist/workflows/ir/schema.js +545 -0
- package/dist/workflows/parser.js +10 -1
- package/dist/workflows/program/expressions.js +369 -0
- package/dist/workflows/program/parser.js +869 -0
- package/dist/workflows/program/project.js +104 -0
- package/dist/workflows/program/schema.js +54 -0
- package/dist/workflows/renderer.js +82 -5
- package/dist/workflows/resource-limits.js +20 -0
- package/dist/workflows/runtime/agent-identity.js +59 -14
- package/dist/workflows/runtime/plan-classifier.js +187 -0
- package/dist/workflows/runtime/runs.js +246 -69
- package/dist/workflows/runtime/unit-checkin.js +45 -0
- package/dist/workflows/runtime/workflow-asset-loader.js +42 -1
- package/dist/workflows/validate-summary.js +24 -3
- package/dist/workflows/validator.js +26 -1
- package/docs/data-and-telemetry.md +4 -3
- package/docs/migration/release-notes/0.6.0.md +1 -1
- package/docs/migration/release-notes/0.7.0.md +5 -4
- package/docs/migration/release-notes/0.9.0-beta.60.md +19 -0
- package/docs/migration/v0.8-to-v0.9.md +401 -0
- package/package.json +4 -2
- package/schemas/akm-config.json +16638 -0
- package/schemas/akm-task.json +87 -0
- package/schemas/akm-workflow.json +372 -0
- package/dist/commands/improve/improve-profiles.js +0 -168
- package/dist/core/config/config-migration.js +0 -602
- package/dist/core/deep-merge.js +0 -38
- package/dist/llm/call-ai.js +0 -62
- package/dist/setup/legacy-config.js +0 -106
- package/docs/README.md +0 -104
- /package/dist/assets/{profiles → improve-strategies}/catchup.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/consolidate.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/frequent.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/memory-focus.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/quick.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/recombine-only.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/synthesize.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/thorough.json +0 -0
|
@@ -0,0 +1,869 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* YAML workflow program → `WorkflowProgram` (redesign addendum, R1).
|
|
6
|
+
*
|
|
7
|
+
* Composition over invention: the document is parsed once with the `yaml`
|
|
8
|
+
* package (`parseDocument` + `LineCounter` for best-effort line anchoring),
|
|
9
|
+
* then validated field-by-field, accumulating `WorkflowError`s rather than
|
|
10
|
+
* throwing. Structural rules mirror the published JSON Schema
|
|
11
|
+
* (`schemas/akm-workflow.json`); semantic rules this parser owns:
|
|
12
|
+
*
|
|
13
|
+
* - duplicate step ids;
|
|
14
|
+
* - exactly one of `unit` | `map` | `route` per step;
|
|
15
|
+
* - route targets exist, come AFTER the routing step, never self-route,
|
|
16
|
+
* and `when` matches are unique (even across YAML key types);
|
|
17
|
+
* - timeout format (`<n>ms` | `<n>s` | `<n>m` | `none`, positive);
|
|
18
|
+
* - `retry.on` values within the persisted `AgentFailureReason` taxonomy;
|
|
19
|
+
* - `params` is a name → JSON-Schema-ish-object map.
|
|
20
|
+
*
|
|
21
|
+
* Expressions (`${{ … }}`) are NOT resolved here — the compiler/validator
|
|
22
|
+
* owns reference checking. This parser only runs a cheap syntactic pass so
|
|
23
|
+
* obviously malformed templates fail at lint time.
|
|
24
|
+
*/
|
|
25
|
+
import { createRequire } from "node:module";
|
|
26
|
+
import { isMap, isScalar, LineCounter, parseDocument } from "yaml";
|
|
27
|
+
import { formatExtraParamsIssue, validateExtraParams } from "../../core/extra-params.js";
|
|
28
|
+
import { jsonBytes, utf8Bytes, WORKFLOW_MAX_EXTRA_PARAMS_BYTES, WORKFLOW_MAX_INSTRUCTION_BYTES, 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";
|
|
29
|
+
import { PROGRAM_ISOLATION_KINDS, PROGRAM_ON_ERROR, PROGRAM_PARAM_NAME_PATTERN, PROGRAM_REDUCERS, PROGRAM_RETRY_REASONS, PROGRAM_STEP_ID_PATTERN, WORKFLOW_PROGRAM_VERSION, } from "./schema.js";
|
|
30
|
+
const TOP_LEVEL_KEYS = ["version", "name", "description", "params", "defaults", "budget", "steps"];
|
|
31
|
+
const DEFAULTS_KEYS = ["engine", "model", "timeout", "on_error", "llm"];
|
|
32
|
+
const BUDGET_KEYS = ["max_tokens", "max_units"];
|
|
33
|
+
const STEP_KEYS = ["id", "title", "unit", "map", "route", "output", "gate"];
|
|
34
|
+
const UNIT_KEYS = [
|
|
35
|
+
"engine",
|
|
36
|
+
"model",
|
|
37
|
+
"llm",
|
|
38
|
+
"timeout",
|
|
39
|
+
"retry",
|
|
40
|
+
"on_error",
|
|
41
|
+
"instructions",
|
|
42
|
+
"output",
|
|
43
|
+
"env",
|
|
44
|
+
"isolation",
|
|
45
|
+
];
|
|
46
|
+
const MAP_KEYS = ["over", "concurrency", "reducer", "unit"];
|
|
47
|
+
const ROUTE_KEYS = ["input", "when", "default"];
|
|
48
|
+
const RETRY_KEYS = ["max", "on"];
|
|
49
|
+
const GATE_KEYS = ["criteria", "max_loops", "required"];
|
|
50
|
+
const STEP_KINDS = ["unit", "map", "route"];
|
|
51
|
+
const TIMEOUT_VALUE = /^(\d+)(ms|s|m)?$/;
|
|
52
|
+
const TIMEOUT_HINT = `Use "<n>ms", "<n>s", "<n>m" (e.g. "10m"), or "none"`;
|
|
53
|
+
/**
|
|
54
|
+
* Cheap structural probe for the indexer matcher (mirrors `looksLikeWorkflow`
|
|
55
|
+
* in ../parser.ts). Returns true if the text has the unmistakable top-level
|
|
56
|
+
* shape of a YAML workflow program: `version: 2` and a `steps:` key, both at
|
|
57
|
+
* column 0. Used so the matcher and parser cannot drift.
|
|
58
|
+
*/
|
|
59
|
+
export function looksLikeWorkflowProgram(yamlText) {
|
|
60
|
+
return /^version[ \t]*:[ \t]*['"]?(?:1|2)['"]?[ \t]*(#.*)?$/m.test(yamlText) && /^steps[ \t]*:/m.test(yamlText);
|
|
61
|
+
}
|
|
62
|
+
export function parseWorkflowProgram(yamlText, source) {
|
|
63
|
+
if (utf8Bytes(yamlText) > WORKFLOW_MAX_SOURCE_BYTES) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
errors: [{ line: 1, message: "Workflow source exceeds the 1 MiB resource limit." }],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const errors = [];
|
|
70
|
+
const lineCounter = new LineCounter();
|
|
71
|
+
let doc;
|
|
72
|
+
try {
|
|
73
|
+
doc = parseDocument(yamlText, { lineCounter });
|
|
74
|
+
}
|
|
75
|
+
catch (cause) {
|
|
76
|
+
return { ok: false, errors: [{ line: 1, message: `YAML parse failed: ${describeError(cause)}` }] };
|
|
77
|
+
}
|
|
78
|
+
for (const problem of doc.errors) {
|
|
79
|
+
const offset = Array.isArray(problem.pos) ? problem.pos[0] : 0;
|
|
80
|
+
errors.push({ line: Math.max(1, lineCounter.linePos(offset).line), message: yamlErrorMessage(problem.message) });
|
|
81
|
+
}
|
|
82
|
+
if (errors.length > 0)
|
|
83
|
+
return { ok: false, errors };
|
|
84
|
+
let root;
|
|
85
|
+
try {
|
|
86
|
+
root = doc.toJS();
|
|
87
|
+
}
|
|
88
|
+
catch (cause) {
|
|
89
|
+
// e.g. the alias-expansion bomb guard (maxAliasCount) throwing.
|
|
90
|
+
return { ok: false, errors: [{ line: 1, message: `YAML expansion failed: ${describeError(cause)}` }] };
|
|
91
|
+
}
|
|
92
|
+
const lineAt = (path) => {
|
|
93
|
+
for (let depth = path.length; depth >= 0; depth--) {
|
|
94
|
+
const node = depth === 0 ? doc.contents : doc.getIn(path.slice(0, depth), true);
|
|
95
|
+
const range = node?.range;
|
|
96
|
+
if (range)
|
|
97
|
+
return Math.max(1, lineCounter.linePos(range[0]).line);
|
|
98
|
+
}
|
|
99
|
+
return 1;
|
|
100
|
+
};
|
|
101
|
+
const ctx = {
|
|
102
|
+
filePath: source.path,
|
|
103
|
+
errors,
|
|
104
|
+
lineAt,
|
|
105
|
+
lineAtOffset: (offset) => Math.max(1, lineCounter.linePos(offset).line),
|
|
106
|
+
nodeAt: (path) => (path.length === 0 ? doc.contents : doc.getIn(path, true)),
|
|
107
|
+
refAt: (path) => {
|
|
108
|
+
for (let depth = path.length; depth >= 0; depth--) {
|
|
109
|
+
const node = depth === 0 ? doc.contents : doc.getIn(path.slice(0, depth), true);
|
|
110
|
+
const range = node?.range;
|
|
111
|
+
if (range) {
|
|
112
|
+
const start = Math.max(1, lineCounter.linePos(range[0]).line);
|
|
113
|
+
const end = Math.max(start, lineCounter.linePos(Math.max(range[0], range[1] - 1)).line);
|
|
114
|
+
return { path: source.path, start, end };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { path: source.path, start: 1, end: 1 };
|
|
118
|
+
},
|
|
119
|
+
err: (path, message) => errors.push({ line: lineAt(path), message }),
|
|
120
|
+
errAtLine: (line, message) => errors.push({ line, message }),
|
|
121
|
+
checkTemplates: (text, path, label) => checkTemplates(ctx, text, path, label),
|
|
122
|
+
};
|
|
123
|
+
if (!isPlainRecord(root)) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
errors: [
|
|
127
|
+
{ line: 1, message: `A workflow program must be a YAML mapping with "version: 2", "name", and "steps".` },
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
checkUnknownKeys(ctx, root, [], TOP_LEVEL_KEYS, "top-level");
|
|
132
|
+
if (root.version !== WORKFLOW_PROGRAM_VERSION) {
|
|
133
|
+
if (root.version === 1) {
|
|
134
|
+
ctx.err(["version"], `Workflow version 1 retired; version 2 is required. Replace runner/profile selectors with engine.`);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
const got = root.version === undefined ? "it is missing" : `got ${JSON.stringify(root.version)}`;
|
|
138
|
+
ctx.err(["version"], `"version: 2" is required at the top level (${got}). Only the number 2 is a valid version.`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
let name = "";
|
|
142
|
+
if (typeof root.name === "string" && root.name.trim() !== "") {
|
|
143
|
+
name = root.name.trim();
|
|
144
|
+
ctx.checkTemplates(root.name, ["name"], `"name"`);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
ctx.err(["name"], `"name" is required and must be a non-empty string.`);
|
|
148
|
+
}
|
|
149
|
+
let description;
|
|
150
|
+
if (root.description !== undefined) {
|
|
151
|
+
if (typeof root.description === "string") {
|
|
152
|
+
description = root.description;
|
|
153
|
+
ctx.checkTemplates(root.description, ["description"], `"description"`);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
ctx.err(["description"], `"description" must be a string.`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const params = parseParams(ctx, root.params);
|
|
160
|
+
const defaults = parseDefaults(ctx, root.defaults);
|
|
161
|
+
const budget = parseBudget(ctx, root.budget);
|
|
162
|
+
const steps = parseSteps(ctx, root.steps);
|
|
163
|
+
if (errors.length > 0)
|
|
164
|
+
return { ok: false, errors };
|
|
165
|
+
const program = {
|
|
166
|
+
version: WORKFLOW_PROGRAM_VERSION,
|
|
167
|
+
name,
|
|
168
|
+
...(description !== undefined ? { description } : {}),
|
|
169
|
+
...(params !== undefined ? { params } : {}),
|
|
170
|
+
...(defaults !== undefined ? { defaults } : {}),
|
|
171
|
+
...(budget !== undefined ? { budget } : {}),
|
|
172
|
+
steps,
|
|
173
|
+
source: { path: source.path },
|
|
174
|
+
};
|
|
175
|
+
return { ok: true, program };
|
|
176
|
+
}
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Top-level sections
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
function parseParams(ctx, raw) {
|
|
181
|
+
if (raw === undefined)
|
|
182
|
+
return undefined;
|
|
183
|
+
if (!isPlainRecord(raw)) {
|
|
184
|
+
ctx.err(["params"], `"params" must be a mapping of param name to a JSON Schema object (e.g. changed_files: { type: array }).`);
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
if (Object.keys(raw).length > WORKFLOW_MAX_PARAMS) {
|
|
188
|
+
ctx.err(["params"], `"params" must contain at most ${WORKFLOW_MAX_PARAMS} entries.`);
|
|
189
|
+
}
|
|
190
|
+
const params = {};
|
|
191
|
+
for (const [paramName, value] of Object.entries(raw)) {
|
|
192
|
+
if (!PROGRAM_PARAM_NAME_PATTERN.test(paramName)) {
|
|
193
|
+
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.`);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (!isPlainRecord(value)) {
|
|
197
|
+
ctx.err(["params", paramName], `Param "${paramName}" must be a JSON Schema object (e.g. { type: string }).`);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
params[paramName] = value;
|
|
201
|
+
}
|
|
202
|
+
return Object.keys(params).length > 0 ? params : undefined;
|
|
203
|
+
}
|
|
204
|
+
function parseDefaults(ctx, raw) {
|
|
205
|
+
if (raw === undefined)
|
|
206
|
+
return undefined;
|
|
207
|
+
const path = ["defaults"];
|
|
208
|
+
if (!isPlainRecord(raw)) {
|
|
209
|
+
ctx.err(path, `"defaults" must be a mapping with any of: ${DEFAULTS_KEYS.join(", ")}.`);
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
checkUnknownKeys(ctx, raw, path, DEFAULTS_KEYS, `"defaults"`);
|
|
213
|
+
const defaults = {};
|
|
214
|
+
if (raw.engine !== undefined) {
|
|
215
|
+
if (typeof raw.engine === "string" && raw.engine.trim() !== "")
|
|
216
|
+
defaults.engine = raw.engine.trim();
|
|
217
|
+
else
|
|
218
|
+
ctx.err([...path, "engine"], `"defaults.engine" must be a non-empty engine name.`);
|
|
219
|
+
}
|
|
220
|
+
if (raw.model !== undefined) {
|
|
221
|
+
if (typeof raw.model === "string" && raw.model.trim() !== "")
|
|
222
|
+
defaults.model = raw.model.trim();
|
|
223
|
+
else
|
|
224
|
+
ctx.err([...path, "model"], `"defaults.model" must be a non-empty string (a model alias or exact id).`);
|
|
225
|
+
}
|
|
226
|
+
const timeoutMs = parseTimeoutField(ctx, raw.timeout, [...path, "timeout"], `"defaults.timeout"`);
|
|
227
|
+
if (timeoutMs !== undefined)
|
|
228
|
+
defaults.timeoutMs = timeoutMs;
|
|
229
|
+
const onError = parseEnumField(ctx, raw.on_error, [...path, "on_error"], `"defaults.on_error"`, PROGRAM_ON_ERROR);
|
|
230
|
+
if (onError !== undefined)
|
|
231
|
+
defaults.onError = onError;
|
|
232
|
+
const llm = parseLlmOverrides(ctx, raw.llm, [...path, "llm"], `"defaults.llm"`);
|
|
233
|
+
if (llm !== undefined)
|
|
234
|
+
defaults.llm = llm;
|
|
235
|
+
return Object.keys(defaults).length > 0 ? defaults : undefined;
|
|
236
|
+
}
|
|
237
|
+
function parseBudget(ctx, raw) {
|
|
238
|
+
if (raw === undefined)
|
|
239
|
+
return undefined;
|
|
240
|
+
const path = ["budget"];
|
|
241
|
+
if (!isPlainRecord(raw)) {
|
|
242
|
+
ctx.err(path, `"budget" must be a mapping with any of: ${BUDGET_KEYS.join(", ")}.`);
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
checkUnknownKeys(ctx, raw, path, BUDGET_KEYS, `"budget"`);
|
|
246
|
+
const budget = {};
|
|
247
|
+
if (raw.max_tokens !== undefined) {
|
|
248
|
+
if (typeof raw.max_tokens === "number" && Number.isInteger(raw.max_tokens) && raw.max_tokens >= 1) {
|
|
249
|
+
budget.maxTokens = raw.max_tokens;
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
ctx.err([...path, "max_tokens"], `"budget.max_tokens" must be an integer >= 1.`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (raw.max_units !== undefined) {
|
|
256
|
+
if (typeof raw.max_units === "number" &&
|
|
257
|
+
Number.isInteger(raw.max_units) &&
|
|
258
|
+
raw.max_units >= 1 &&
|
|
259
|
+
raw.max_units <= WORKFLOW_MAX_MAP_EXPANSION) {
|
|
260
|
+
budget.maxUnits = raw.max_units;
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
ctx.err([...path, "max_units"], `"budget.max_units" must be an integer from 1 through ${WORKFLOW_MAX_MAP_EXPANSION}.`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return Object.keys(budget).length > 0 ? budget : undefined;
|
|
267
|
+
}
|
|
268
|
+
function parseSteps(ctx, raw) {
|
|
269
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
270
|
+
ctx.err(["steps"], `"steps" is required and must be a list with at least one step.`);
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
if (raw.length > WORKFLOW_MAX_STEPS) {
|
|
274
|
+
ctx.err(["steps"], `"steps" must contain at most ${WORKFLOW_MAX_STEPS} entries.`);
|
|
275
|
+
}
|
|
276
|
+
// First pass: collect ids so route targets can be checked against ALL steps
|
|
277
|
+
// (including ones that fail their own validation).
|
|
278
|
+
const idIndex = new Map();
|
|
279
|
+
raw.forEach((rawStep, index) => {
|
|
280
|
+
if (isPlainRecord(rawStep) && typeof rawStep.id === "string" && !idIndex.has(rawStep.id)) {
|
|
281
|
+
idIndex.set(rawStep.id, index);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
const steps = [];
|
|
285
|
+
const seenIds = new Map();
|
|
286
|
+
const routeChecks = [];
|
|
287
|
+
raw.forEach((rawStep, index) => {
|
|
288
|
+
const path = ["steps", index];
|
|
289
|
+
if (!isPlainRecord(rawStep)) {
|
|
290
|
+
ctx.err(path, `Step ${index + 1} must be a mapping with an "id" and exactly one of "unit", "map", or "route".`);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const label = typeof rawStep.id === "string" && rawStep.id !== "" ? `Step "${rawStep.id}"` : `Step ${index + 1}`;
|
|
294
|
+
checkUnknownKeys(ctx, rawStep, path, STEP_KEYS, label);
|
|
295
|
+
let id = "";
|
|
296
|
+
if (typeof rawStep.id !== "string" || rawStep.id === "") {
|
|
297
|
+
ctx.err([...path, "id"], `${label} requires a non-empty string "id".`);
|
|
298
|
+
}
|
|
299
|
+
else if (!PROGRAM_STEP_ID_PATTERN.test(rawStep.id)) {
|
|
300
|
+
ctx.err([...path, "id"], `${label} has an invalid id "${rawStep.id}". A step id cannot be referenced from \${{ }} expressions ` +
|
|
301
|
+
`unless it matches [A-Za-z_][A-Za-z0-9_-]* (a letter or underscore first, then letters, digits, ` +
|
|
302
|
+
`underscores, or dashes; no dots, no leading digit) — otherwise \${{ steps.${rawStep.id}.output }} ` +
|
|
303
|
+
`cannot be written.`);
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
id = rawStep.id;
|
|
307
|
+
const firstIndex = seenIds.get(id);
|
|
308
|
+
if (firstIndex !== undefined) {
|
|
309
|
+
ctx.err([...path, "id"], `Duplicate step id "${id}" (first used by step ${firstIndex + 1}). Step ids must be unique.`);
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
seenIds.set(id, index);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
let title;
|
|
316
|
+
if (rawStep.title !== undefined) {
|
|
317
|
+
if (typeof rawStep.title === "string" && rawStep.title.trim() !== "") {
|
|
318
|
+
title = rawStep.title.trim();
|
|
319
|
+
ctx.checkTemplates(rawStep.title, [...path, "title"], `${label} "title"`);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
ctx.err([...path, "title"], `${label} "title" must be a non-empty string.`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const declaredKinds = STEP_KINDS.filter((kind) => rawStep[kind] !== undefined);
|
|
326
|
+
if (declaredKinds.length !== 1) {
|
|
327
|
+
const found = declaredKinds.length === 0 ? "found none" : `found ${declaredKinds.join(" + ")}`;
|
|
328
|
+
ctx.err(path, `${label} must declare exactly one of "unit", "map", or "route" (${found}).`);
|
|
329
|
+
}
|
|
330
|
+
// Parse every declared block (even when the exactly-one rule already
|
|
331
|
+
// failed) so all inner problems surface in a single validate run.
|
|
332
|
+
const unit = rawStep.unit !== undefined ? parseUnit(ctx, rawStep.unit, [...path, "unit"], label) : undefined;
|
|
333
|
+
const map = rawStep.map !== undefined ? parseMap(ctx, rawStep.map, [...path, "map"], label) : undefined;
|
|
334
|
+
const route = rawStep.route !== undefined
|
|
335
|
+
? parseRoute(ctx, rawStep.route, [...path, "route"], label, index, routeChecks)
|
|
336
|
+
: undefined;
|
|
337
|
+
const output = parseSchemaObject(ctx, rawStep.output, [...path, "output"], `${label} "output"`);
|
|
338
|
+
const gate = rawStep.gate !== undefined ? parseGate(ctx, rawStep.gate, [...path, "gate"], label) : undefined;
|
|
339
|
+
const step = { id, source: ctx.refAt(path) };
|
|
340
|
+
if (title !== undefined)
|
|
341
|
+
step.title = title;
|
|
342
|
+
if (declaredKinds.length === 1) {
|
|
343
|
+
if (unit)
|
|
344
|
+
step.unit = unit;
|
|
345
|
+
if (map)
|
|
346
|
+
step.map = map;
|
|
347
|
+
if (route)
|
|
348
|
+
step.route = route;
|
|
349
|
+
}
|
|
350
|
+
if (output !== undefined)
|
|
351
|
+
step.output = output;
|
|
352
|
+
if (gate !== undefined)
|
|
353
|
+
step.gate = gate;
|
|
354
|
+
steps.push(step);
|
|
355
|
+
});
|
|
356
|
+
// Route target post-pass: targets exist, come after the routing step, and
|
|
357
|
+
// never point back at it.
|
|
358
|
+
for (const check of routeChecks) {
|
|
359
|
+
const targets = [...check.branches.map((b) => ({ stepId: b.stepId, line: b.line }))];
|
|
360
|
+
if (check.defaultTarget)
|
|
361
|
+
targets.push(check.defaultTarget);
|
|
362
|
+
for (const target of targets) {
|
|
363
|
+
const targetIndex = idIndex.get(target.stepId);
|
|
364
|
+
if (targetIndex === undefined) {
|
|
365
|
+
ctx.errAtLine(target.line, `${check.stepLabel} routes to unknown step "${target.stepId}". Route targets must name a step id in this workflow.`);
|
|
366
|
+
}
|
|
367
|
+
else if (targetIndex === check.stepIndex) {
|
|
368
|
+
ctx.errAtLine(target.line, `${check.stepLabel} must not route to itself.`);
|
|
369
|
+
}
|
|
370
|
+
else if (targetIndex < check.stepIndex) {
|
|
371
|
+
ctx.errAtLine(target.line, `${check.stepLabel} routes backward to "${target.stepId}" (step ${targetIndex + 1}). Route targets must come after the routing step.`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return steps;
|
|
376
|
+
}
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
// Step blocks
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
function parseUnit(ctx, raw, path, stepLabel) {
|
|
381
|
+
if (!isPlainRecord(raw)) {
|
|
382
|
+
ctx.err(path, `${stepLabel} "unit" must be a mapping with an "instructions" key.`);
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
checkUnknownKeys(ctx, raw, path, UNIT_KEYS, `${stepLabel} "unit"`);
|
|
386
|
+
const unit = { instructions: "", source: ctx.refAt(path) };
|
|
387
|
+
if (typeof raw.instructions === "string" && raw.instructions.trim() !== "") {
|
|
388
|
+
unit.instructions = raw.instructions;
|
|
389
|
+
ctx.checkTemplates(raw.instructions, [...path, "instructions"], `${stepLabel} "instructions"`);
|
|
390
|
+
if (utf8Bytes(raw.instructions) > WORKFLOW_MAX_INSTRUCTION_BYTES) {
|
|
391
|
+
ctx.err([...path, "instructions"], `${stepLabel} "instructions" exceeds the 256 KiB resource limit.`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
ctx.err([...path, "instructions"], `${stepLabel} "unit" requires non-empty string "instructions".`);
|
|
396
|
+
}
|
|
397
|
+
if (raw.engine !== undefined) {
|
|
398
|
+
if (typeof raw.engine === "string" && raw.engine.trim() !== "")
|
|
399
|
+
unit.engine = raw.engine.trim();
|
|
400
|
+
else
|
|
401
|
+
ctx.err([...path, "engine"], `${stepLabel} "engine" must be a non-empty engine name.`);
|
|
402
|
+
}
|
|
403
|
+
if (raw.model !== undefined) {
|
|
404
|
+
if (typeof raw.model === "string" && raw.model.trim() !== "")
|
|
405
|
+
unit.model = raw.model.trim();
|
|
406
|
+
else
|
|
407
|
+
ctx.err([...path, "model"], `${stepLabel} "model" must be a non-empty string (a model alias or exact id).`);
|
|
408
|
+
}
|
|
409
|
+
const llm = parseLlmOverrides(ctx, raw.llm, [...path, "llm"], `${stepLabel} "llm"`);
|
|
410
|
+
if (llm !== undefined)
|
|
411
|
+
unit.llm = llm;
|
|
412
|
+
const timeoutMs = parseTimeoutField(ctx, raw.timeout, [...path, "timeout"], `${stepLabel} "timeout"`);
|
|
413
|
+
if (timeoutMs !== undefined)
|
|
414
|
+
unit.timeoutMs = timeoutMs;
|
|
415
|
+
const retry = parseRetry(ctx, raw.retry, [...path, "retry"], stepLabel);
|
|
416
|
+
if (retry !== undefined)
|
|
417
|
+
unit.retry = retry;
|
|
418
|
+
const onError = parseEnumField(ctx, raw.on_error, [...path, "on_error"], `${stepLabel} "on_error"`, PROGRAM_ON_ERROR);
|
|
419
|
+
if (onError !== undefined)
|
|
420
|
+
unit.onError = onError;
|
|
421
|
+
const output = parseSchemaObject(ctx, raw.output, [...path, "output"], `${stepLabel} unit "output"`);
|
|
422
|
+
if (output !== undefined)
|
|
423
|
+
unit.output = output;
|
|
424
|
+
if (raw.env !== undefined) {
|
|
425
|
+
if (Array.isArray(raw.env) && raw.env.every((entry) => typeof entry === "string" && entry.trim() !== "")) {
|
|
426
|
+
unit.env = raw.env.map((entry) => entry.trim());
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
ctx.err([...path, "env"], `${stepLabel} "env" must be a list of non-empty env asset refs.`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const isolation = parseEnumField(ctx, raw.isolation, [...path, "isolation"], `${stepLabel} "isolation"`, PROGRAM_ISOLATION_KINDS);
|
|
433
|
+
if (isolation !== undefined)
|
|
434
|
+
unit.isolation = isolation;
|
|
435
|
+
return unit;
|
|
436
|
+
}
|
|
437
|
+
function parseMap(ctx, raw, path, stepLabel) {
|
|
438
|
+
if (!isPlainRecord(raw)) {
|
|
439
|
+
ctx.err(path, `${stepLabel} "map" must be a mapping with "over" and "unit" keys.`);
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
442
|
+
checkUnknownKeys(ctx, raw, path, MAP_KEYS, `${stepLabel} "map"`);
|
|
443
|
+
let over = "";
|
|
444
|
+
if (typeof raw.over === "string" && raw.over.trim() !== "") {
|
|
445
|
+
over = raw.over.trim();
|
|
446
|
+
ctx.checkTemplates(raw.over, [...path, "over"], `${stepLabel} "over"`);
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
ctx.err([...path, "over"], `${stepLabel} "map" requires "over": a \${{ … }} expression naming the item list (e.g. \${{ steps.discover.output.files }}).`);
|
|
450
|
+
}
|
|
451
|
+
let concurrency;
|
|
452
|
+
if (raw.concurrency !== undefined) {
|
|
453
|
+
if (typeof raw.concurrency === "number" && Number.isInteger(raw.concurrency) && raw.concurrency > 0) {
|
|
454
|
+
concurrency = raw.concurrency;
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
ctx.err([...path, "concurrency"], `${stepLabel} "concurrency" must be a positive integer.`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const reducer = parseEnumField(ctx, raw.reducer, [...path, "reducer"], `${stepLabel} "reducer"`, PROGRAM_REDUCERS);
|
|
461
|
+
const unit = raw.unit !== undefined ? parseUnit(ctx, raw.unit, [...path, "unit"], stepLabel) : undefined;
|
|
462
|
+
if (raw.unit === undefined) {
|
|
463
|
+
ctx.err(path, `${stepLabel} "map" requires a nested "unit" to fan out.`);
|
|
464
|
+
}
|
|
465
|
+
if (unit === undefined)
|
|
466
|
+
return undefined;
|
|
467
|
+
const map = { over, unit };
|
|
468
|
+
if (concurrency !== undefined)
|
|
469
|
+
map.concurrency = concurrency;
|
|
470
|
+
if (reducer !== undefined)
|
|
471
|
+
map.reducer = reducer;
|
|
472
|
+
return map;
|
|
473
|
+
}
|
|
474
|
+
function parseRoute(ctx, raw, path, stepLabel, stepIndex, routeChecks) {
|
|
475
|
+
if (!isPlainRecord(raw)) {
|
|
476
|
+
ctx.err(path, `${stepLabel} "route" must be a mapping with "input" and "when" keys.`);
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
checkUnknownKeys(ctx, raw, path, ROUTE_KEYS, `${stepLabel} "route"`);
|
|
480
|
+
let input = "";
|
|
481
|
+
if (typeof raw.input === "string" && raw.input.trim() !== "") {
|
|
482
|
+
input = raw.input.trim();
|
|
483
|
+
ctx.checkTemplates(raw.input, [...path, "input"], `${stepLabel} "route.input"`);
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
ctx.err([...path, "input"], `${stepLabel} "route" requires "input": a \${{ … }} expression naming the value to route on.`);
|
|
487
|
+
}
|
|
488
|
+
const check = { stepIndex, stepLabel, branches: [] };
|
|
489
|
+
const whenPath = [...path, "when"];
|
|
490
|
+
const whenNode = ctx.nodeAt(whenPath);
|
|
491
|
+
if (raw.when === undefined || !isPlainRecord(raw.when)) {
|
|
492
|
+
ctx.err(whenPath, `${stepLabel} "route" requires "when": a mapping of match value to target step id (e.g. when: { pass: ship }).`);
|
|
493
|
+
}
|
|
494
|
+
else if (isMap(whenNode)) {
|
|
495
|
+
if (whenNode.items.length > WORKFLOW_MAX_ROUTE_BRANCHES) {
|
|
496
|
+
ctx.err(whenPath, `${stepLabel} "when" must contain at most ${WORKFLOW_MAX_ROUTE_BRANCHES} branches.`);
|
|
497
|
+
}
|
|
498
|
+
// Walk the AST pairs (not the JS object) so duplicate matches that only
|
|
499
|
+
// collide after stringification ("true" vs true) are still caught.
|
|
500
|
+
const seenMatches = new Map();
|
|
501
|
+
for (const pair of whenNode.items) {
|
|
502
|
+
const keyLine = rangedLine(ctx, pair.key, whenPath);
|
|
503
|
+
if (!isScalar(pair.key)) {
|
|
504
|
+
ctx.errAtLine(keyLine, `${stepLabel} "when" match keys must be scalar values.`);
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
const match = String(pair.key.value);
|
|
508
|
+
const valueNode = pair.value;
|
|
509
|
+
const valueLine = rangedLine(ctx, valueNode, whenPath);
|
|
510
|
+
const target = isScalar(valueNode) && typeof valueNode.value === "string" ? valueNode.value.trim() : "";
|
|
511
|
+
if (target === "") {
|
|
512
|
+
ctx.errAtLine(valueLine, `${stepLabel} "when: ${match}" must map to a step id string.`);
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
const firstLine = seenMatches.get(match);
|
|
516
|
+
if (firstLine !== undefined) {
|
|
517
|
+
ctx.errAtLine(keyLine, `${stepLabel} has a duplicate "when" match "${match}" (first declared on line ${firstLine}). Matches must be unique.`);
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
seenMatches.set(match, keyLine);
|
|
521
|
+
check.branches.push({ match, stepId: target, line: valueLine });
|
|
522
|
+
}
|
|
523
|
+
if (check.branches.length === 0 && whenNode.items.length === 0) {
|
|
524
|
+
ctx.err(whenPath, `${stepLabel} "when" must contain at least one match → step-id entry.`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
// AST unavailable (e.g. the mapping came through an alias) — fall back to
|
|
529
|
+
// the resolved JS object; duplicate-key detection already ran in YAML.
|
|
530
|
+
for (const [match, target] of Object.entries(raw.when)) {
|
|
531
|
+
if (typeof target === "string" && target.trim() !== "") {
|
|
532
|
+
check.branches.push({ match, stepId: target.trim(), line: ctx.lineAt(whenPath) });
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
ctx.err(whenPath, `${stepLabel} "when: ${match}" must map to a step id string.`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (Object.keys(raw.when).length > WORKFLOW_MAX_ROUTE_BRANCHES) {
|
|
539
|
+
ctx.err(whenPath, `${stepLabel} "when" must contain at most ${WORKFLOW_MAX_ROUTE_BRANCHES} branches.`);
|
|
540
|
+
}
|
|
541
|
+
if (Object.keys(raw.when).length === 0) {
|
|
542
|
+
ctx.err(whenPath, `${stepLabel} "when" must contain at least one match → step-id entry.`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
let defaultStepId;
|
|
546
|
+
if (raw.default !== undefined) {
|
|
547
|
+
if (typeof raw.default === "string" && raw.default.trim() !== "") {
|
|
548
|
+
defaultStepId = raw.default.trim();
|
|
549
|
+
check.defaultTarget = { stepId: defaultStepId, line: ctx.lineAt([...path, "default"]) };
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
ctx.err([...path, "default"], `${stepLabel} "route.default" must be a step id string.`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
routeChecks.push(check);
|
|
556
|
+
const route = { input, branches: check.branches.map(({ match, stepId }) => ({ match, stepId })) };
|
|
557
|
+
if (defaultStepId !== undefined)
|
|
558
|
+
route.defaultStepId = defaultStepId;
|
|
559
|
+
return route;
|
|
560
|
+
}
|
|
561
|
+
function parseGate(ctx, raw, path, stepLabel) {
|
|
562
|
+
if (!isPlainRecord(raw)) {
|
|
563
|
+
ctx.err(path, `${stepLabel} "gate" must be a mapping with a "criteria" list.`);
|
|
564
|
+
return undefined;
|
|
565
|
+
}
|
|
566
|
+
checkUnknownKeys(ctx, raw, path, GATE_KEYS, `${stepLabel} "gate"`);
|
|
567
|
+
const gate = { criteria: [] };
|
|
568
|
+
if (Array.isArray(raw.criteria) &&
|
|
569
|
+
raw.criteria.length > 0 &&
|
|
570
|
+
raw.criteria.every((c) => typeof c === "string" && c.trim() !== "")) {
|
|
571
|
+
gate.criteria = raw.criteria.map((c) => c.trim());
|
|
572
|
+
for (const [i, c] of raw.criteria.entries()) {
|
|
573
|
+
ctx.checkTemplates(c, [...path, "criteria", i], `${stepLabel} gate criterion ${i + 1}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
ctx.err([...path, "criteria"], `${stepLabel} "gate" requires "criteria": a non-empty list of criterion strings.`);
|
|
578
|
+
}
|
|
579
|
+
if (raw.max_loops !== undefined) {
|
|
580
|
+
// TODO(R2): max_loops execution (bounded evaluator-optimizer) is engine
|
|
581
|
+
// rework scope; the parser validates and carries it through.
|
|
582
|
+
if (typeof raw.max_loops === "number" && Number.isInteger(raw.max_loops) && raw.max_loops >= 1) {
|
|
583
|
+
gate.maxLoops = raw.max_loops;
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
ctx.err([...path, "max_loops"], `${stepLabel} "gate.max_loops" must be an integer >= 1.`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (raw.required !== undefined) {
|
|
590
|
+
// Reviewer #18: a required gate must be judged; with no judge available the
|
|
591
|
+
// engine/report BLOCK the step instead of failing open.
|
|
592
|
+
if (typeof raw.required === "boolean") {
|
|
593
|
+
gate.required = raw.required;
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
ctx.err([...path, "required"], `${stepLabel} "gate.required" must be a boolean (true or false).`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return gate;
|
|
600
|
+
}
|
|
601
|
+
// ---------------------------------------------------------------------------
|
|
602
|
+
// Field helpers
|
|
603
|
+
// ---------------------------------------------------------------------------
|
|
604
|
+
function parseRetry(ctx, raw, path, stepLabel) {
|
|
605
|
+
if (raw === undefined)
|
|
606
|
+
return undefined;
|
|
607
|
+
if (!isPlainRecord(raw)) {
|
|
608
|
+
ctx.err(path, `${stepLabel} "retry" must be a mapping: { max: <n>, on: [<failure_reason>, …] }.`);
|
|
609
|
+
return undefined;
|
|
610
|
+
}
|
|
611
|
+
checkUnknownKeys(ctx, raw, path, RETRY_KEYS, `${stepLabel} "retry"`);
|
|
612
|
+
let ok = true;
|
|
613
|
+
if (!(typeof raw.max === "number" && Number.isInteger(raw.max) && raw.max >= 0)) {
|
|
614
|
+
ctx.err([...path, "max"], `${stepLabel} "retry.max" is required and must be a non-negative integer.`);
|
|
615
|
+
ok = false;
|
|
616
|
+
}
|
|
617
|
+
const on = [];
|
|
618
|
+
if (Array.isArray(raw.on) && raw.on.length > 0) {
|
|
619
|
+
raw.on.forEach((reason, i) => {
|
|
620
|
+
if (typeof reason === "string" && PROGRAM_RETRY_REASONS.includes(reason)) {
|
|
621
|
+
on.push(reason);
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
ctx.err([...path, "on", i], `${stepLabel} "retry.on" has unknown failure reason ${JSON.stringify(reason)}. Valid reasons: ${PROGRAM_RETRY_REASONS.join(", ")}.`);
|
|
625
|
+
ok = false;
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
ctx.err([...path, "on"], `${stepLabel} "retry.on" is required and must be a non-empty list of failure reasons (${PROGRAM_RETRY_REASONS.join(", ")}).`);
|
|
631
|
+
ok = false;
|
|
632
|
+
}
|
|
633
|
+
return ok ? { max: raw.max, on } : undefined;
|
|
634
|
+
}
|
|
635
|
+
function parseTimeoutField(ctx, raw, path, label) {
|
|
636
|
+
if (raw === undefined)
|
|
637
|
+
return undefined;
|
|
638
|
+
// Bare integers keep the existing duration semantics (a number is ms).
|
|
639
|
+
if (typeof raw === "number") {
|
|
640
|
+
if (Number.isInteger(raw) && raw > 0)
|
|
641
|
+
return raw;
|
|
642
|
+
ctx.err(path, `${label} has a non-positive timeout ${JSON.stringify(raw)}. ${TIMEOUT_HINT}.`);
|
|
643
|
+
return undefined;
|
|
644
|
+
}
|
|
645
|
+
if (typeof raw !== "string") {
|
|
646
|
+
ctx.err(path, `${label} must be a duration string. ${TIMEOUT_HINT}.`);
|
|
647
|
+
return undefined;
|
|
648
|
+
}
|
|
649
|
+
const value = raw.trim().toLowerCase();
|
|
650
|
+
if (value === "none")
|
|
651
|
+
return null;
|
|
652
|
+
const match = value.match(TIMEOUT_VALUE);
|
|
653
|
+
if (!match) {
|
|
654
|
+
ctx.err(path, `${label} has an invalid timeout "${raw}". ${TIMEOUT_HINT}.`);
|
|
655
|
+
return undefined;
|
|
656
|
+
}
|
|
657
|
+
const n = Number.parseInt(match[1], 10);
|
|
658
|
+
const unit = match[2] ?? "ms";
|
|
659
|
+
const timeoutMs = unit === "m" ? n * 60_000 : unit === "s" ? n * 1_000 : n;
|
|
660
|
+
if (timeoutMs <= 0) {
|
|
661
|
+
ctx.err(path, `${label} has a non-positive timeout "${raw}". Use a positive duration or "none".`);
|
|
662
|
+
return undefined;
|
|
663
|
+
}
|
|
664
|
+
return timeoutMs;
|
|
665
|
+
}
|
|
666
|
+
function parseEnumField(ctx, raw, path, label, allowed) {
|
|
667
|
+
if (raw === undefined)
|
|
668
|
+
return undefined;
|
|
669
|
+
if (typeof raw === "string" && allowed.includes(raw))
|
|
670
|
+
return raw;
|
|
671
|
+
ctx.err(path, `${label} must be one of: ${allowed.join(" | ")} (got ${JSON.stringify(raw)}).`);
|
|
672
|
+
return undefined;
|
|
673
|
+
}
|
|
674
|
+
/** Parse only invocation tuning. Connection identity belongs to a named engine. */
|
|
675
|
+
function parseLlmOverrides(ctx, raw, path, label) {
|
|
676
|
+
if (raw === undefined)
|
|
677
|
+
return undefined;
|
|
678
|
+
if (!isPlainRecord(raw)) {
|
|
679
|
+
ctx.err(path, `${label} must be a mapping of LLM invocation overrides.`);
|
|
680
|
+
return undefined;
|
|
681
|
+
}
|
|
682
|
+
const keys = [
|
|
683
|
+
"temperature",
|
|
684
|
+
"max_tokens",
|
|
685
|
+
"supports_json_schema",
|
|
686
|
+
"extra_params",
|
|
687
|
+
"context_length",
|
|
688
|
+
"enable_thinking",
|
|
689
|
+
];
|
|
690
|
+
checkUnknownKeys(ctx, raw, path, keys, label);
|
|
691
|
+
const result = {};
|
|
692
|
+
if (raw.temperature !== undefined) {
|
|
693
|
+
if (typeof raw.temperature === "number" && Number.isFinite(raw.temperature))
|
|
694
|
+
result.temperature = raw.temperature;
|
|
695
|
+
else
|
|
696
|
+
ctx.err([...path, "temperature"], `${label}.temperature must be a finite number.`);
|
|
697
|
+
}
|
|
698
|
+
if (raw.max_tokens !== undefined) {
|
|
699
|
+
if (typeof raw.max_tokens === "number" && Number.isInteger(raw.max_tokens) && raw.max_tokens > 0) {
|
|
700
|
+
result.maxTokens = raw.max_tokens;
|
|
701
|
+
}
|
|
702
|
+
else
|
|
703
|
+
ctx.err([...path, "max_tokens"], `${label}.max_tokens must be a positive integer.`);
|
|
704
|
+
}
|
|
705
|
+
if (raw.supports_json_schema !== undefined) {
|
|
706
|
+
if (typeof raw.supports_json_schema === "boolean")
|
|
707
|
+
result.supportsJsonSchema = raw.supports_json_schema;
|
|
708
|
+
else
|
|
709
|
+
ctx.err([...path, "supports_json_schema"], `${label}.supports_json_schema must be a boolean.`);
|
|
710
|
+
}
|
|
711
|
+
if (raw.extra_params !== undefined) {
|
|
712
|
+
if (!isPlainRecord(raw.extra_params)) {
|
|
713
|
+
ctx.err([...path, "extra_params"], `${label}.extra_params must be a JSON object.`);
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
const issues = validateExtraParams(raw.extra_params);
|
|
717
|
+
for (const issue of issues) {
|
|
718
|
+
ctx.err([...path, "extra_params", ...issue.path], `${formatExtraParamsIssue(`${label}.extra_params`, issue)}.`);
|
|
719
|
+
}
|
|
720
|
+
if (jsonBytes(raw.extra_params) > WORKFLOW_MAX_EXTRA_PARAMS_BYTES) {
|
|
721
|
+
ctx.err([...path, "extra_params"], `${label}.extra_params exceeds the 64 KiB resource limit.`);
|
|
722
|
+
}
|
|
723
|
+
if (issues.length === 0 && jsonBytes(raw.extra_params) <= WORKFLOW_MAX_EXTRA_PARAMS_BYTES) {
|
|
724
|
+
result.extraParams = raw.extra_params;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (raw.context_length !== undefined) {
|
|
729
|
+
if (typeof raw.context_length === "number" && Number.isInteger(raw.context_length) && raw.context_length > 0) {
|
|
730
|
+
result.contextLength = raw.context_length;
|
|
731
|
+
}
|
|
732
|
+
else
|
|
733
|
+
ctx.err([...path, "context_length"], `${label}.context_length must be a positive integer.`);
|
|
734
|
+
}
|
|
735
|
+
if (raw.enable_thinking !== undefined) {
|
|
736
|
+
if (typeof raw.enable_thinking === "boolean")
|
|
737
|
+
result.enableThinking = raw.enable_thinking;
|
|
738
|
+
else
|
|
739
|
+
ctx.err([...path, "enable_thinking"], `${label}.enable_thinking must be a boolean.`);
|
|
740
|
+
}
|
|
741
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
742
|
+
}
|
|
743
|
+
function parseSchemaObject(ctx, raw, path, label) {
|
|
744
|
+
if (raw === undefined)
|
|
745
|
+
return undefined;
|
|
746
|
+
if (!isPlainRecord(raw)) {
|
|
747
|
+
ctx.err(path, `${label} must be a JSON Schema object (e.g. { type: object, properties: { … } }).`);
|
|
748
|
+
return undefined;
|
|
749
|
+
}
|
|
750
|
+
if (jsonBytes(raw) > WORKFLOW_MAX_SCHEMA_BYTES) {
|
|
751
|
+
ctx.err(path, `${label} exceeds the 256 KiB resource limit.`);
|
|
752
|
+
}
|
|
753
|
+
return raw;
|
|
754
|
+
}
|
|
755
|
+
function checkUnknownKeys(ctx, obj, path, allowed, label) {
|
|
756
|
+
for (const key of Object.keys(obj)) {
|
|
757
|
+
if (!allowed.includes(key)) {
|
|
758
|
+
ctx.err([...path, key], `Unknown ${label} key "${key}". Allowed keys: ${allowed.join(", ")}.`);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
/** Line of an AST node via its byte range; falls back to a path lookup. */
|
|
763
|
+
function rangedLine(ctx, node, fallbackPath) {
|
|
764
|
+
const range = node?.range;
|
|
765
|
+
return range ? ctx.lineAtOffset(range[0]) : ctx.lineAt(fallbackPath);
|
|
766
|
+
}
|
|
767
|
+
// ---------------------------------------------------------------------------
|
|
768
|
+
// ${{ … }} syntactic pass
|
|
769
|
+
// ---------------------------------------------------------------------------
|
|
770
|
+
/**
|
|
771
|
+
* Cheap syntactic pass over string fields. Two layers:
|
|
772
|
+
*
|
|
773
|
+
* 1. A local unterminated-`${{` check (unambiguously malformed regardless
|
|
774
|
+
* of grammar).
|
|
775
|
+
* 2. The full closed-grammar check from ./expressions when that module is
|
|
776
|
+
* available (it is written by a parallel task — loaded defensively).
|
|
777
|
+
*
|
|
778
|
+
* Expression REFERENCES (unknown step, unknown param, type mismatch) are the
|
|
779
|
+
* compiler/validator's job, not this parser's.
|
|
780
|
+
*/
|
|
781
|
+
function checkTemplates(ctx, text, path, label) {
|
|
782
|
+
let idx = 0;
|
|
783
|
+
while (true) {
|
|
784
|
+
const open = text.indexOf("${{", idx);
|
|
785
|
+
if (open === -1)
|
|
786
|
+
break;
|
|
787
|
+
const close = text.indexOf("}}", open + 3);
|
|
788
|
+
const nextOpen = text.indexOf("${{", open + 3);
|
|
789
|
+
if (close === -1 || (nextOpen !== -1 && nextOpen < close)) {
|
|
790
|
+
ctx.err(path, `${label} contains an unterminated "\${{" expression. Close it with "}}".`);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
idx = close + 2;
|
|
794
|
+
}
|
|
795
|
+
const checker = loadExpressionChecker();
|
|
796
|
+
if (checker) {
|
|
797
|
+
const message = checker(text);
|
|
798
|
+
if (message !== null)
|
|
799
|
+
ctx.err(path, `${label}: ${message}`);
|
|
800
|
+
}
|
|
801
|
+
// TODO(R1): when ./expressions is absent (parallel task not landed yet)
|
|
802
|
+
// only the unterminated check above runs; the compiler task enforces the
|
|
803
|
+
// closed expression grammar fully.
|
|
804
|
+
}
|
|
805
|
+
let cachedExpressionChecker;
|
|
806
|
+
/** Test seam: force a re-probe of ./expressions (e.g. after mocking). */
|
|
807
|
+
export function resetExpressionCheckerForTests() {
|
|
808
|
+
cachedExpressionChecker = undefined;
|
|
809
|
+
}
|
|
810
|
+
function loadExpressionChecker() {
|
|
811
|
+
if (cachedExpressionChecker !== undefined)
|
|
812
|
+
return cachedExpressionChecker;
|
|
813
|
+
cachedExpressionChecker = null;
|
|
814
|
+
let candidate;
|
|
815
|
+
try {
|
|
816
|
+
const requireModule = createRequire(import.meta.url);
|
|
817
|
+
// Non-literal specifier keeps tsc from resolving the module at compile
|
|
818
|
+
// time — it may not exist yet (written by a parallel task).
|
|
819
|
+
const specifier = "./expressions";
|
|
820
|
+
const mod = requireModule(specifier);
|
|
821
|
+
candidate = [mod.parseTemplate, mod.compileTemplate, mod.parseTemplateString, mod.tokenizeTemplate].find((fn) => typeof fn === "function");
|
|
822
|
+
}
|
|
823
|
+
catch {
|
|
824
|
+
return cachedExpressionChecker; // module not present — skip the pass
|
|
825
|
+
}
|
|
826
|
+
if (!candidate)
|
|
827
|
+
return cachedExpressionChecker;
|
|
828
|
+
const parseTemplate = candidate;
|
|
829
|
+
cachedExpressionChecker = (text) => {
|
|
830
|
+
try {
|
|
831
|
+
const result = parseTemplate(text);
|
|
832
|
+
if (isPlainRecord(result) && result.ok === false) {
|
|
833
|
+
const errs = result.errors;
|
|
834
|
+
if (Array.isArray(errs) && errs.length > 0) {
|
|
835
|
+
const first = errs[0];
|
|
836
|
+
if (typeof first === "string")
|
|
837
|
+
return first;
|
|
838
|
+
if (isPlainRecord(first) && typeof first.message === "string")
|
|
839
|
+
return first.message;
|
|
840
|
+
}
|
|
841
|
+
if (typeof result.error === "string")
|
|
842
|
+
return result.error;
|
|
843
|
+
return `malformed \${{ … }} expression`;
|
|
844
|
+
}
|
|
845
|
+
return null;
|
|
846
|
+
}
|
|
847
|
+
catch {
|
|
848
|
+
// A throw here is as likely an API-signature mismatch with the
|
|
849
|
+
// parallel expressions task as a real template error — never turn it
|
|
850
|
+
// into a false lint failure. The compiler task enforces the grammar.
|
|
851
|
+
return null;
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
return cachedExpressionChecker;
|
|
855
|
+
}
|
|
856
|
+
// ---------------------------------------------------------------------------
|
|
857
|
+
// Utilities
|
|
858
|
+
// ---------------------------------------------------------------------------
|
|
859
|
+
function isPlainRecord(value) {
|
|
860
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
861
|
+
}
|
|
862
|
+
function describeError(cause) {
|
|
863
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
864
|
+
}
|
|
865
|
+
/** Strip the yaml package's multi-line code frame down to the first line. */
|
|
866
|
+
function yamlErrorMessage(message) {
|
|
867
|
+
const first = message.split("\n", 1)[0] ?? message;
|
|
868
|
+
return first.replace(/ at line \d+, column \d+:?\s*$/, "").trim();
|
|
869
|
+
}
|