@hyperdrive.bot/paseo-protocol 0.3.42 → 0.3.44

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.
@@ -0,0 +1,172 @@
1
+ import { defaultLoopParams, validateLoopParams } from "../fleet/params.js";
2
+ import { createWorkflowTokenPattern, renderWorkflowInputValue, SUBSTITUTABLE_STEP_FIELDS, validateWorkflowDefinition, WorkflowDefinitionError, } from "./definition.js";
3
+ /**
4
+ * Compile one definition against one set of input values.
5
+ *
6
+ * Validates BEFORE it emits, and reports EVERY issue in one throw rather than stopping
7
+ * at the first: a definition can carry a bad input value, an undeclared token and an
8
+ * unknown agent name simultaneously, and three sequential throws would hide the second
9
+ * and third until the first is fixed. On any issue nothing is returned and no partial
10
+ * graph is observable.
11
+ *
12
+ * @throws {WorkflowDefinitionError} carrying the union of the value issues, the
13
+ * definition issues, and the unresolved-reference issues this module adds.
14
+ */
15
+ export function compileWorkflowDefinition(args) {
16
+ const { definition, session, values } = args;
17
+ // Defaults on the LEFT so a supplied value always wins over a declared default.
18
+ // `defaultLoopParams` only emits a key when the field's `default` parses as a
19
+ // `LoopParamValue`, so a field with no default contributes nothing here - which is
20
+ // exactly the gap the declared-but-unresolved issue below closes.
21
+ const merged = {
22
+ ...defaultLoopParams({ fields: definition.inputs }),
23
+ ...values,
24
+ };
25
+ const declaredKeys = new Set(definition.inputs.map((field) => field.key));
26
+ const valueIssues = validateLoopParams({ fields: definition.inputs }, merged);
27
+ // Each of the three failure shapes has exactly ONE reporter. A required input that is
28
+ // missing is already located at `inputs[<key>]`, so the reference pass below stays
29
+ // silent about it rather than restating the same root cause at every step that
30
+ // mentions it.
31
+ const reportedKeys = new Set(valueIssues.map((issue) => issue.key));
32
+ const rendered = definition.steps.map((step) => renderStepFields({ step, values: merged, declaredKeys, reportedKeys }));
33
+ const issues = [
34
+ ...valueIssues.map((issue) => ({
35
+ path: `inputs[${issue.key}]`,
36
+ message: issue.message,
37
+ })),
38
+ // Deliberately duplicated with `parseWorkflowDefinition`, and it stays: the two
39
+ // guard different entry points - a CLI that parsed a file, versus any caller
40
+ // invoking this compiler directly with a hand-built object.
41
+ ...validateWorkflowDefinition(definition),
42
+ ...rendered.flatMap((entry) => entry.issues),
43
+ ];
44
+ if (issues.length > 0) {
45
+ throw new WorkflowDefinitionError(issues);
46
+ }
47
+ const graph = {
48
+ // What the manager's `title ?? graph.goal` default reads, so `--title` stays optional.
49
+ goal: definition.name,
50
+ // Ported ballast: the CLI's reusable master-prompt template. Zero readers on
51
+ // 25fe57635 (only the TS interface and the Zod schema mention it).
52
+ masterPrompt: "",
53
+ metadata: {
54
+ // Emitted EMPTY on purpose. `resolveExecutionLayers` honours a declared list only
55
+ // when non-empty, then falls through to Kahn layering over `dependencies`. A
56
+ // precomputed list here would be a second derivation that can disagree with `needs`.
57
+ executionLayers: [],
58
+ // Upper bound, not a cap - nothing enforces it. Zero readers.
59
+ maxParallelism: definition.steps.length,
60
+ totalTasks: definition.steps.length,
61
+ // Unknown. Zero readers.
62
+ estimatedDuration: 0,
63
+ // Per-file task generation is a CLI concept with no paseo reader.
64
+ perFileMode: false,
65
+ // `storyFormat` and `totalFiles` are omitted, not defaulted: both are optional and
66
+ // neither has a definition-side vocabulary, so `false`/`0` would be a claim where
67
+ // an absent key is honestly "not specified".
68
+ },
69
+ // Ambient run identity, supplied by the caller so this stays a pure function.
70
+ // `outputDirectory` has zero readers on 25fe57635 but is schema-required.
71
+ session,
72
+ tasks: rendered.map(buildTask),
73
+ };
74
+ // Shallow copy: a caller mutating the returned presets must not reach back into the
75
+ // definition. Populating them onto the wire is Story 3.3's job.
76
+ return { graph, agentPresets: { ...definition.agents } };
77
+ }
78
+ /** Substitute every token in one field's text, iterating Story 3.1's scanner. */
79
+ function substituteTokens(args) {
80
+ const issues = [];
81
+ // A FRESH pattern per call. A module-level `/g` regex carries `lastIndex` between
82
+ // calls, which would make the result depend on scan order.
83
+ const text = args.text.replace(createWorkflowTokenPattern(), (raw, body) => {
84
+ const name = body.trim();
85
+ if (!args.declaredKeys.has(name)) {
86
+ // Undeclared, or malformed (a body that is not a bare identifier is never a
87
+ // declared key). `validateWorkflowDefinition` already reports both, located at
88
+ // this same path, so reporting again here would double-count one mistake. The
89
+ // raw token is returned unrendered; the union above is non-empty, so the caller
90
+ // throws and this interim string is never observable.
91
+ return raw;
92
+ }
93
+ const value = args.values[name];
94
+ if (value === undefined) {
95
+ if (args.reportedKeys.has(name)) {
96
+ // Already located at `inputs[<name>]` by the value pass (required and missing).
97
+ return raw;
98
+ }
99
+ // Declared, referenced, and has no value. `validateLoopParams` cannot catch this
100
+ // (a non-required field with no value is legal to it) and Story 3.1 cannot
101
+ // (the input IS declared). Only the compiler knows the token is referenced.
102
+ // Reporting it is what keeps the token from rendering as "" or "undefined".
103
+ issues.push({
104
+ path: args.path,
105
+ message: `step "${args.stepId}" references ${raw}, which is a declared input with no value: supply --input ${name}=... or give the field a default`,
106
+ });
107
+ return raw;
108
+ }
109
+ return renderWorkflowInputValue(value);
110
+ });
111
+ return { text, issues };
112
+ }
113
+ /**
114
+ * Render every substitutable field of one step.
115
+ *
116
+ * Iterates `SUBSTITUTABLE_STEP_FIELDS` rather than a second hard-coded list, so this
117
+ * module and Story 3.1's validator cannot disagree about which fields accept a token.
118
+ * `id` and `needs` are absent from that constant on purpose: a topology that depends on
119
+ * runtime values could not be validated at authoring time.
120
+ */
121
+ function renderStepFields(args) {
122
+ const fields = {};
123
+ const issues = [];
124
+ for (const field of SUBSTITUTABLE_STEP_FIELDS) {
125
+ const text = args.step[field];
126
+ if (text === undefined) {
127
+ continue;
128
+ }
129
+ const result = substituteTokens({
130
+ text,
131
+ values: args.values,
132
+ declaredKeys: args.declaredKeys,
133
+ reportedKeys: args.reportedKeys,
134
+ path: `steps[${args.step.id}].${field}`,
135
+ stepId: args.step.id,
136
+ });
137
+ fields[field] = result.text;
138
+ issues.push(...result.issues);
139
+ }
140
+ return { step: args.step, fields, issues };
141
+ }
142
+ /** One task node per step, in definition order. */
143
+ function buildTask(rendered) {
144
+ const { fields, step } = rendered;
145
+ // `step.id` never carries a token, so a fallback that came from it needs no rendering.
146
+ const title = fields.title ?? step.id;
147
+ return {
148
+ id: step.id, // never substituted
149
+ dependencies: step.needs, // never substituted
150
+ // Falls back through the SUBSTITUTED title before the id, so a token in the title
151
+ // reaches the description rather than the description inheriting an unrendered one.
152
+ description: fields.description ?? title,
153
+ // `prompt` is required by `WorkflowStepSchema`, so `renderStepFields` always renders
154
+ // it; the fallback exists only to keep the expression total.
155
+ prompt: fields.prompt ?? step.prompt,
156
+ title,
157
+ // `""` is the schema-satisfying empty, and `outputFile` has no reader anywhere.
158
+ outputFile: fields.outputFile ?? "",
159
+ // Ported ballast from the bmad-workflow CLI. Verified zero readers on 25fe57635:
160
+ // the only occurrences of `parallelizable` in the tree are its two declarations.
161
+ parallelizable: true,
162
+ // Same: no reader. The definition has no duration vocabulary and this epic does not
163
+ // invent one.
164
+ estimatedMinutes: 0,
165
+ // Conditional spread so the key is ABSENT rather than present-with-`undefined`: the
166
+ // schema says optional, and absent is the honest encoding of "this step names no agent".
167
+ ...(step.agent === undefined ? {} : { agentType: step.agent }),
168
+ // `targetFiles` is never emitted: the definition has no vocabulary for it and this
169
+ // epic does not invent one.
170
+ };
171
+ }
172
+ //# sourceMappingURL=compile.js.map
@@ -0,0 +1,134 @@
1
+ import { z } from "zod";
2
+ import { type LoopParamValue } from "../fleet/params.js";
3
+ /** Step fields that accept `{{VAR}}` substitution. NEVER `id` and NEVER `needs`: a
4
+ * topology that depends on runtime values could not be validated at parse time. */
5
+ export declare const SUBSTITUTABLE_STEP_FIELDS: readonly ["prompt", "title", "description", "outputFile"];
6
+ export type SubstitutableStepField = (typeof SUBSTITUTABLE_STEP_FIELDS)[number];
7
+ export interface WorkflowDefinitionIssue {
8
+ /** `steps[<id>].<field>` | `steps[#<index>]` | `inputs[<key>]` | `agents[<name>]` | `steps` | `version` */
9
+ path: string;
10
+ message: string;
11
+ }
12
+ export declare class WorkflowDefinitionError extends Error {
13
+ readonly issues: WorkflowDefinitionIssue[];
14
+ constructor(issues: WorkflowDefinitionIssue[]);
15
+ }
16
+ /**
17
+ * One step. `.strict()` rather than Zod's default strip: a key this version does not
18
+ * support must be REJECTED by name, because a silently stripped `forEach:` reads as
19
+ * configured and behaves as absent.
20
+ */
21
+ export declare const WorkflowStepSchema: z.ZodObject<{
22
+ id: z.ZodString;
23
+ needs: z.ZodDefault<z.ZodArray<z.ZodString>>;
24
+ agent: z.ZodOptional<z.ZodString>;
25
+ prompt: z.ZodString;
26
+ title: z.ZodOptional<z.ZodString>;
27
+ description: z.ZodOptional<z.ZodString>;
28
+ outputFile: z.ZodOptional<z.ZodString>;
29
+ }, z.core.$strict>;
30
+ /**
31
+ * The whole definition. `inputs` reuses the Loops parameter vocabulary rather than
32
+ * declaring a second one; a new input type is added in ../fleet/params.ts, never here.
33
+ */
34
+ export declare const WorkflowDefinitionSchema: z.ZodObject<{
35
+ version: z.ZodLiteral<1>;
36
+ name: z.ZodString;
37
+ inputs: z.ZodDefault<z.ZodArray<z.ZodObject<{
38
+ key: z.ZodString;
39
+ label: z.ZodString;
40
+ type: z.ZodEnum<{
41
+ string: "string";
42
+ number: "number";
43
+ boolean: "boolean";
44
+ enum: "enum";
45
+ "string-list": "string-list";
46
+ }>;
47
+ description: z.ZodOptional<z.ZodString>;
48
+ required: z.ZodDefault<z.ZodBoolean>;
49
+ options: z.ZodOptional<z.ZodArray<z.ZodString>>;
50
+ min: z.ZodOptional<z.ZodNumber>;
51
+ max: z.ZodOptional<z.ZodNumber>;
52
+ default: z.ZodOptional<z.ZodUnknown>;
53
+ }, z.core.$strip>>>;
54
+ agents: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
55
+ provider: z.ZodOptional<z.ZodString>;
56
+ model: z.ZodOptional<z.ZodString>;
57
+ modeId: z.ZodOptional<z.ZodString>;
58
+ systemPrompt: z.ZodOptional<z.ZodString>;
59
+ thinkingOptionId: z.ZodOptional<z.ZodString>;
60
+ approvalPolicy: z.ZodOptional<z.ZodString>;
61
+ sandboxMode: z.ZodOptional<z.ZodString>;
62
+ networkAccess: z.ZodOptional<z.ZodBoolean>;
63
+ webSearch: z.ZodOptional<z.ZodBoolean>;
64
+ }, z.core.$strict>>>;
65
+ steps: z.ZodArray<z.ZodObject<{
66
+ id: z.ZodString;
67
+ needs: z.ZodDefault<z.ZodArray<z.ZodString>>;
68
+ agent: z.ZodOptional<z.ZodString>;
69
+ prompt: z.ZodString;
70
+ title: z.ZodOptional<z.ZodString>;
71
+ description: z.ZodOptional<z.ZodString>;
72
+ outputFile: z.ZodOptional<z.ZodString>;
73
+ }, z.core.$strict>>;
74
+ }, z.core.$strict>;
75
+ export type WorkflowStep = z.infer<typeof WorkflowStepSchema>;
76
+ export type WorkflowDefinition = z.infer<typeof WorkflowDefinitionSchema>;
77
+ /** A `{{ ... }}` occurrence, matched LOOSELY on purpose: a malformed token must be
78
+ * reported, not silently ignored and passed through into the rendered prompt.
79
+ * Fresh instance per call so `lastIndex` is never shared. */
80
+ export declare function createWorkflowTokenPattern(): RegExp;
81
+ export interface WorkflowTokenMatch {
82
+ /** The full literal token as written, e.g. `{{ PRD }}`. */
83
+ raw: string;
84
+ /** The trimmed body, e.g. `PRD`. */
85
+ name: string;
86
+ /** True when `name` is a bare identifier, i.e. inside the closed grammar. */
87
+ wellFormed: boolean;
88
+ }
89
+ /**
90
+ * Every `{{ ... }}` occurrence in `text`, in source order.
91
+ *
92
+ * The grammar is closed: `{{`, optional whitespace, ONE identifier, optional
93
+ * whitespace, `}}`. No dotted paths, no filters, no expressions, no nesting. Anything
94
+ * else comes back with `wellFormed: false` so the caller can report it rather than let
95
+ * it survive into a rendered prompt as literal text.
96
+ */
97
+ export declare function scanWorkflowTokens(text: string): WorkflowTokenMatch[];
98
+ /**
99
+ * Render one input value for interpolation into a PROMPT.
100
+ *
101
+ * Deliberately NOT `loopParamsToEnv`: booleans render as "true"/"false" here and
102
+ * "1"/"0" there, because the consumer here is an LLM prompt and there a bash env
103
+ * var. The two renderers share no consumer, so the divergence is safe; the spec
104
+ * asserts them side by side so it stays deliberate.
105
+ */
106
+ export declare function renderWorkflowInputValue(value: LoopParamValue): string;
107
+ /**
108
+ * Semantic checks Zod cannot see: ids, edges, cycles, agent names and tokens.
109
+ *
110
+ * Takes an already-parsed definition, returns EVERY issue, never throws and never
111
+ * returns early. Each pass is separate so the issue order is stable and each rule is
112
+ * readable on its own.
113
+ */
114
+ export declare function validateWorkflowDefinition(definition: WorkflowDefinition): WorkflowDefinitionIssue[];
115
+ export type WorkflowDefinitionParseResult = {
116
+ ok: true;
117
+ definition: WorkflowDefinition;
118
+ } | {
119
+ ok: false;
120
+ issues: WorkflowDefinitionIssue[];
121
+ };
122
+ /**
123
+ * Parse an untrusted object into a definition.
124
+ *
125
+ * Never throws. `WorkflowDefinitionError` exists for callers that want a throwing
126
+ * boundary; this function returns a discriminated result instead.
127
+ *
128
+ * `version` is checked BY HAND before the Zod parse. A file declaring a version this
129
+ * build does not know has a shape this build does not know, so parsing it against the
130
+ * v1 schema buries the one useful fact under a cascade of field errors. `z.literal(1)`
131
+ * stays in the schema as the structural guard for direct `.parse` callers.
132
+ */
133
+ export declare function parseWorkflowDefinition(raw: unknown): WorkflowDefinitionParseResult;
134
+ //# sourceMappingURL=definition.d.ts.map
@@ -0,0 +1,362 @@
1
+ import { z } from "zod";
2
+ import { LoopParamFieldSchema } from "../fleet/params.js";
3
+ import { WorkflowAgentPresetSchema } from "../messages.js";
4
+ /**
5
+ * A workflow DEFINITION: the authored, versioned description of a pipeline, as opposed
6
+ * to a `TaskGraph`, which is one pre-built run of one.
7
+ *
8
+ * The point of this module is that a definition is either provably valid or carries a
9
+ * list of located errors. Every check reports EVERY issue in one pass, mirroring
10
+ * `validateLoopParams` in ../fleet/params.ts, so an author fixes a file once instead of
11
+ * resubmitting to discover the next mistake.
12
+ *
13
+ * Pure by construction: no I/O, no `node:` imports, no clock, no RNG. The same input
14
+ * always produces the same issue list in the same order.
15
+ */
16
+ // Same rule as `LoopParamFieldSchema.key` in ../fleet/params.ts. Duplicated (not
17
+ // imported) because that module does not export the pattern and this epic's
18
+ // compatibility requirements forbid editing it. The drift guard is a test that runs
19
+ // one sample table through both schemas and asserts identical accept/reject.
20
+ const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
21
+ /** The only definition version this build understands. */
22
+ const SUPPORTED_VERSION = 1;
23
+ /** Step fields that accept `{{VAR}}` substitution. NEVER `id` and NEVER `needs`: a
24
+ * topology that depends on runtime values could not be validated at parse time. */
25
+ export const SUBSTITUTABLE_STEP_FIELDS = ["prompt", "title", "description", "outputFile"];
26
+ export class WorkflowDefinitionError extends Error {
27
+ constructor(issues) {
28
+ super(`Workflow definition is invalid: ${issues.map((i) => `${i.path}: ${i.message}`).join("; ")}`);
29
+ this.issues = issues;
30
+ this.name = "WorkflowDefinitionError";
31
+ }
32
+ }
33
+ /**
34
+ * One step. `.strict()` rather than Zod's default strip: a key this version does not
35
+ * support must be REJECTED by name, because a silently stripped `forEach:` reads as
36
+ * configured and behaves as absent.
37
+ */
38
+ export const WorkflowStepSchema = z
39
+ .object({
40
+ id: z.string().trim().min(1).regex(IDENTIFIER, "step id must be a valid identifier"),
41
+ needs: z.array(z.string()).default([]),
42
+ agent: z.string().optional(),
43
+ prompt: z.string().min(1),
44
+ title: z.string().optional(),
45
+ description: z.string().optional(),
46
+ outputFile: z.string().optional(),
47
+ })
48
+ .strict();
49
+ /**
50
+ * The whole definition. `inputs` reuses the Loops parameter vocabulary rather than
51
+ * declaring a second one; a new input type is added in ../fleet/params.ts, never here.
52
+ */
53
+ export const WorkflowDefinitionSchema = z
54
+ .object({
55
+ version: z.literal(1),
56
+ name: z.string().trim().min(1),
57
+ inputs: z.array(LoopParamFieldSchema).default([]),
58
+ agents: z.record(z.string(), WorkflowAgentPresetSchema).default({}),
59
+ steps: z.array(WorkflowStepSchema).min(1),
60
+ })
61
+ .strict();
62
+ /** A `{{ ... }}` occurrence, matched LOOSELY on purpose: a malformed token must be
63
+ * reported, not silently ignored and passed through into the rendered prompt.
64
+ * Fresh instance per call so `lastIndex` is never shared. */
65
+ export function createWorkflowTokenPattern() {
66
+ return /\{\{([^}]*)\}\}/g;
67
+ }
68
+ /**
69
+ * Every `{{ ... }}` occurrence in `text`, in source order.
70
+ *
71
+ * The grammar is closed: `{{`, optional whitespace, ONE identifier, optional
72
+ * whitespace, `}}`. No dotted paths, no filters, no expressions, no nesting. Anything
73
+ * else comes back with `wellFormed: false` so the caller can report it rather than let
74
+ * it survive into a rendered prompt as literal text.
75
+ */
76
+ export function scanWorkflowTokens(text) {
77
+ const matches = [];
78
+ for (const match of text.matchAll(createWorkflowTokenPattern())) {
79
+ const name = (match[1] ?? "").trim();
80
+ matches.push({ raw: match[0], name, wellFormed: IDENTIFIER.test(name) });
81
+ }
82
+ return matches;
83
+ }
84
+ /**
85
+ * Render one input value for interpolation into a PROMPT.
86
+ *
87
+ * Deliberately NOT `loopParamsToEnv`: booleans render as "true"/"false" here and
88
+ * "1"/"0" there, because the consumer here is an LLM prompt and there a bash env
89
+ * var. The two renderers share no consumer, so the divergence is safe; the spec
90
+ * asserts them side by side so it stays deliberate.
91
+ */
92
+ export function renderWorkflowInputValue(value) {
93
+ if (Array.isArray(value)) {
94
+ return value.join(",");
95
+ }
96
+ if (typeof value === "boolean") {
97
+ return value ? "true" : "false";
98
+ }
99
+ return String(value);
100
+ }
101
+ /**
102
+ * Semantic checks Zod cannot see: ids, edges, cycles, agent names and tokens.
103
+ *
104
+ * Takes an already-parsed definition, returns EVERY issue, never throws and never
105
+ * returns early. Each pass is separate so the issue order is stable and each rule is
106
+ * readable on its own.
107
+ */
108
+ export function validateWorkflowDefinition(definition) {
109
+ return [
110
+ ...collectDuplicateStepIds(definition.steps),
111
+ ...collectUnknownNeeds(definition.steps),
112
+ ...collectCycle(definition.steps),
113
+ ...collectUnknownAgents(definition),
114
+ ...collectTokenIssues(definition),
115
+ ];
116
+ }
117
+ /** One issue per repeated id, not one per extra occurrence. */
118
+ function collectDuplicateStepIds(steps) {
119
+ const seen = new Set();
120
+ const reported = new Set();
121
+ const issues = [];
122
+ for (const step of steps) {
123
+ if (!seen.has(step.id)) {
124
+ seen.add(step.id);
125
+ continue;
126
+ }
127
+ if (reported.has(step.id)) {
128
+ continue;
129
+ }
130
+ reported.add(step.id);
131
+ issues.push({ path: "steps", message: `duplicate step id: "${step.id}"` });
132
+ }
133
+ return issues;
134
+ }
135
+ function collectUnknownNeeds(steps) {
136
+ const known = new Set(steps.map((step) => step.id));
137
+ const issues = [];
138
+ for (const step of steps) {
139
+ for (const need of step.needs) {
140
+ if (known.has(need)) {
141
+ continue;
142
+ }
143
+ issues.push({
144
+ path: `steps[${step.id}].needs`,
145
+ message: `step "${step.id}" needs "${need}", which is not a step in this definition`,
146
+ });
147
+ }
148
+ }
149
+ return issues;
150
+ }
151
+ /**
152
+ * Kahn-style layering, mirroring `computeLayersFromDependencies`
153
+ * (packages/server/src/server/workflow/workflow-progress.ts:255-283) including its rule
154
+ * that a dependency outside the graph counts as already satisfied. Honouring that rule
155
+ * is what keeps an unknown `needs` target from being double-reported as a cycle.
156
+ *
157
+ * Collects an issue where the server function throws, because this pass must report
158
+ * every issue in one run.
159
+ */
160
+ function collectCycle(steps) {
161
+ const byId = new Map(steps.map((step) => [step.id, step]));
162
+ const resolved = new Set();
163
+ let remaining = steps.map((step) => step.id);
164
+ while (remaining.length > 0) {
165
+ const layer = remaining.filter((id) => {
166
+ const node = byId.get(id);
167
+ if (!node) {
168
+ return false;
169
+ }
170
+ // A dependency outside the graph is treated as already satisfied.
171
+ return node.needs.every((dep) => resolved.has(dep) || !byId.has(dep));
172
+ });
173
+ if (layer.length === 0) {
174
+ const unresolved = [...new Set(remaining)].sort();
175
+ return [
176
+ {
177
+ path: "steps",
178
+ message: `steps form a dependency cycle: ${unresolved.join(", ")}`,
179
+ },
180
+ ];
181
+ }
182
+ for (const id of layer) {
183
+ resolved.add(id);
184
+ }
185
+ remaining = remaining.filter((id) => !resolved.has(id));
186
+ }
187
+ return [];
188
+ }
189
+ function collectUnknownAgents(definition) {
190
+ const declared = Object.keys(definition.agents).sort();
191
+ const known = declared.length === 0 ? "no agents are declared" : `declared agents: ${declared.join(", ")}`;
192
+ const issues = [];
193
+ for (const step of definition.steps) {
194
+ const agent = step.agent;
195
+ if (agent === undefined || Object.hasOwn(definition.agents, agent)) {
196
+ continue;
197
+ }
198
+ issues.push({
199
+ path: `steps[${step.id}].agent`,
200
+ message: `step "${step.id}" names agent "${agent}", which is not declared (${known})`,
201
+ });
202
+ }
203
+ return issues;
204
+ }
205
+ function collectTokenIssues(definition) {
206
+ const declared = new Set(definition.inputs.map((field) => field.key));
207
+ const issues = [];
208
+ for (const step of definition.steps) {
209
+ for (const field of SUBSTITUTABLE_STEP_FIELDS) {
210
+ const text = step[field];
211
+ if (text === undefined) {
212
+ continue;
213
+ }
214
+ issues.push(...collectFieldTokenIssues(step.id, field, text, declared));
215
+ }
216
+ }
217
+ return issues;
218
+ }
219
+ function collectFieldTokenIssues(stepId, field, text, declared) {
220
+ const path = `steps[${stepId}].${field}`;
221
+ const issues = [];
222
+ for (const token of scanWorkflowTokens(text)) {
223
+ if (!token.wellFormed) {
224
+ issues.push({
225
+ path,
226
+ message: `step "${stepId}" has a malformed token ${token.raw} in ${field}: only {{NAME}} naming a declared input is supported`,
227
+ });
228
+ continue;
229
+ }
230
+ if (declared.has(token.name)) {
231
+ continue;
232
+ }
233
+ issues.push({
234
+ path,
235
+ message: `step "${stepId}" references ${token.raw} in ${field}, which is not a declared input`,
236
+ });
237
+ }
238
+ return issues;
239
+ }
240
+ /**
241
+ * Parse an untrusted object into a definition.
242
+ *
243
+ * Never throws. `WorkflowDefinitionError` exists for callers that want a throwing
244
+ * boundary; this function returns a discriminated result instead.
245
+ *
246
+ * `version` is checked BY HAND before the Zod parse. A file declaring a version this
247
+ * build does not know has a shape this build does not know, so parsing it against the
248
+ * v1 schema buries the one useful fact under a cascade of field errors. `z.literal(1)`
249
+ * stays in the schema as the structural guard for direct `.parse` callers.
250
+ */
251
+ export function parseWorkflowDefinition(raw) {
252
+ const versionIssue = checkVersionGate(raw);
253
+ if (versionIssue) {
254
+ return { ok: false, issues: [versionIssue] };
255
+ }
256
+ const parsed = WorkflowDefinitionSchema.safeParse(raw);
257
+ if (!parsed.success) {
258
+ const issues = parsed.error.issues.flatMap((issue) => {
259
+ const unrecognizedKeys = issue.code === "unrecognized_keys" ? issue.keys : [];
260
+ return locateZodIssue({ path: issue.path, message: issue.message, unrecognizedKeys }, raw);
261
+ });
262
+ return { ok: false, issues };
263
+ }
264
+ const issues = validateWorkflowDefinition(parsed.data);
265
+ if (issues.length > 0) {
266
+ return { ok: false, issues };
267
+ }
268
+ return { ok: true, definition: parsed.data };
269
+ }
270
+ function checkVersionGate(raw) {
271
+ const record = RawObjectSchema.safeParse(raw);
272
+ if (!record.success) {
273
+ return { path: "version", message: "expected a workflow definition object" };
274
+ }
275
+ if (!Object.hasOwn(record.data, "version")) {
276
+ return {
277
+ path: "version",
278
+ message: 'missing "version": a file without "version" is treated as a task graph, not a workflow definition',
279
+ };
280
+ }
281
+ const version = record.data.version;
282
+ if (version !== SUPPORTED_VERSION) {
283
+ return {
284
+ path: "version",
285
+ message: `unsupported definition version: ${String(version)} (this build supports version ${SUPPORTED_VERSION})`,
286
+ };
287
+ }
288
+ return null;
289
+ }
290
+ const RawObjectSchema = z.record(z.string(), z.unknown());
291
+ /** Loose view of the raw input, used only to recover the ids Zod's paths do not carry. */
292
+ const RawLocatorSchema = z.object({
293
+ steps: z.array(z.unknown()).optional(),
294
+ inputs: z.array(z.unknown()).optional(),
295
+ });
296
+ const RawStepIdSchema = z.object({ id: z.string().trim().min(1) });
297
+ const RawInputKeySchema = z.object({ key: z.string().trim().min(1) });
298
+ function locateZodIssue(issue, raw) {
299
+ if (issue.unrecognizedKeys.length > 0) {
300
+ return issue.unrecognizedKeys.map((key) => describeUnrecognizedKey(key, issue.path, raw));
301
+ }
302
+ return [{ path: locatePath(issue.path, raw), message: issue.message }];
303
+ }
304
+ /**
305
+ * One issue PER unrecognized key, not one lumped issue, and with a purpose-built
306
+ * message that names the version so the author knows the key is unsupported here rather
307
+ * than misspelled.
308
+ */
309
+ function describeUnrecognizedKey(key, path, raw) {
310
+ const index = path[1];
311
+ if (path[0] !== "steps" || typeof index !== "number") {
312
+ return {
313
+ path: key,
314
+ message: `"${key}" is not a supported definition key in version: ${SUPPORTED_VERSION}`,
315
+ };
316
+ }
317
+ const stepId = readRawStepId(raw, index);
318
+ const located = stepId === null ? `steps[#${index}]` : `steps[${stepId}]`;
319
+ const owner = stepId === null ? `step #${index}` : `step "${stepId}"`;
320
+ return {
321
+ path: located,
322
+ message: `${owner}: "${key}" is not a supported step key in version: ${SUPPORTED_VERSION}`,
323
+ };
324
+ }
325
+ /**
326
+ * Zod reports `steps[2]`, which names the array index. An author reads step ids, so
327
+ * recover the id from the raw input and degrade to `steps[#<index>]` only when the step
328
+ * cannot name itself.
329
+ */
330
+ function locatePath(path, raw) {
331
+ const [head, index, field] = path;
332
+ if (head === "steps" && typeof index === "number") {
333
+ const stepId = readRawStepId(raw, index);
334
+ if (stepId === null) {
335
+ return `steps[#${index}]`;
336
+ }
337
+ return field === undefined ? `steps[${stepId}]` : `steps[${stepId}].${String(field)}`;
338
+ }
339
+ if (head === "inputs" && typeof index === "number") {
340
+ const key = readRawInputKey(raw, index);
341
+ return key === null ? `inputs[#${index}]` : `inputs[${key}]`;
342
+ }
343
+ const joined = path.map((segment) => String(segment)).join(".");
344
+ return joined.length > 0 ? joined : "definition";
345
+ }
346
+ function readRawStepId(raw, index) {
347
+ const view = RawLocatorSchema.safeParse(raw);
348
+ if (!view.success) {
349
+ return null;
350
+ }
351
+ const step = RawStepIdSchema.safeParse(view.data.steps?.[index]);
352
+ return step.success ? step.data.id : null;
353
+ }
354
+ function readRawInputKey(raw, index) {
355
+ const view = RawLocatorSchema.safeParse(raw);
356
+ if (!view.success) {
357
+ return null;
358
+ }
359
+ const input = RawInputKeySchema.safeParse(view.data.inputs?.[index]);
360
+ return input.success ? input.data.key : null;
361
+ }
362
+ //# sourceMappingURL=definition.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperdrive.bot/paseo-protocol",
3
- "version": "0.3.42",
3
+ "version": "0.3.44",
4
4
  "description": "Paseo shared protocol schemas and wire types",
5
5
  "files": [
6
6
  "dist",