@async/pipeline 0.2.4 → 0.4.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.
@@ -1,10 +1,14 @@
1
1
  import { assertCacheStore, defaultPipelineCache, defineCache, isCacheDirective, mergeWithDefaultCacheStores, parseCacheRef } from "./cache.js";
2
+ import { assertSupportedDeclaration, brandDeclaration, hasDeclarationKind, readDeclaration } from "./declaration.js";
2
3
  import { pipelineError } from "./errors.js";
3
4
  export * from "./cache.js";
5
+ export * from "./declaration.js";
4
6
  export * from "./errors.js";
7
+ export const DEFAULT_PIPELINE_CONFIG_FILES = ["pipeline.ts", "pipeline.js", "pipeline.mjs", "pipeline.mts"];
8
+ const SOURCE_PIPELINE_DEFAULTED = Symbol.for("@async/pipeline.source.pipeline.defaulted");
5
9
  export function sh(first, ...values) {
6
10
  if (typeof first === "function") {
7
- return { kind: "deferred-shell", command: first };
11
+ return brandDeclaration({ kind: "deferred-shell", command: first }, "deferred-shell");
8
12
  }
9
13
  let command = "";
10
14
  for (let index = 0; index < first.length; index += 1) {
@@ -13,9 +17,11 @@ export function sh(first, ...values) {
13
17
  command += String(values[index]);
14
18
  }
15
19
  }
16
- return { kind: "shell", command };
20
+ return brandDeclaration({ kind: "shell", command }, "shell");
17
21
  }
18
22
  const AGENT_STEP_FIELDS = new Set(["use", "prompt", "model", "stdoutTo"]);
23
+ const DECLARED_AGENT_STEP_FIELDS = new Set(["kind", ...AGENT_STEP_FIELDS]);
24
+ const SHELL_STEP_FIELDS = new Set(["kind", "command"]);
19
25
  export function agent(options) {
20
26
  rejectUnknownFields(AGENT_STEP_FIELDS, options, "agent() step");
21
27
  const use = options.use;
@@ -33,7 +39,7 @@ export function agent(options) {
33
39
  throw pipelineError("ASYNC_PIPELINE_AGENT_INVALID", 'agent() "stdoutTo" must be a relative path inside the task\'s working directory; absolute paths and ".." segments are rejected.');
34
40
  }
35
41
  }
36
- const step = { kind: "agent", use, prompt: options.prompt };
42
+ const step = brandDeclaration({ kind: "agent", use, prompt: options.prompt }, "agent");
37
43
  if (options.model !== undefined)
38
44
  step.model = options.model;
39
45
  if (options.stdoutTo !== undefined)
@@ -50,138 +56,159 @@ export function task(definition, run) {
50
56
  if (definition.run !== undefined && run !== undefined) {
51
57
  throw pipelineError("ASYNC_PIPELINE_TASK_ARGUMENT_CONFLICT", "Do not pass a second task argument when config.run is defined.");
52
58
  }
53
- return run === undefined ? definition : { ...definition, run };
59
+ return brandDeclaration(run === undefined ? definition : { ...definition, run }, "task");
54
60
  }
55
61
  export function job(definition) {
56
- return definition;
62
+ return brandDeclaration(definition, "job");
57
63
  }
58
64
  function envVar(name, valuesOrOptions, options = {}) {
59
65
  if (!valuesOrOptions)
60
- return { kind: "async-pipeline.env.var", name };
66
+ return brandDeclaration({ kind: "async-pipeline.env.var", name }, "env.var");
61
67
  if (isDefaultOnlyEnvOptions(valuesOrOptions) && Object.keys(valuesOrOptions).length === 1) {
62
- return { kind: "async-pipeline.env.var", name, default: valuesOrOptions.default };
68
+ return brandDeclaration({ kind: "async-pipeline.env.var", name, default: valuesOrOptions.default }, "env.var");
63
69
  }
64
- return {
70
+ return brandDeclaration({
65
71
  kind: "async-pipeline.env.var",
66
72
  name,
67
73
  values: { ...valuesOrOptions },
68
74
  default: options.default
69
- };
75
+ }, "env.var");
70
76
  }
71
77
  export const env = {
72
78
  secret(name) {
73
- return { kind: "async-pipeline.env.secret", name };
79
+ return brandDeclaration({ kind: "async-pipeline.env.secret", name }, "env.secret");
74
80
  },
75
81
  var: envVar
76
82
  };
77
83
  export const sandbox = {
78
84
  host() {
79
- return { kind: "host" };
85
+ return brandDeclaration({ kind: "host" }, "sandbox.host");
80
86
  },
81
87
  lima(options = {}) {
82
- return { kind: "lima", vm: options.vm };
88
+ return brandDeclaration({ kind: "lima", vm: options.vm }, "sandbox.lima");
83
89
  },
84
90
  docker(options) {
85
- return {
91
+ return brandDeclaration({
86
92
  kind: "docker",
87
93
  image: options.image,
88
94
  workdir: options.workdir,
89
95
  volumes: options.volumes ? options.volumes.map((volume) => ({ ...volume })) : undefined
90
- };
96
+ }, "sandbox.docker");
91
97
  },
92
98
  container(options) {
93
- return {
99
+ return brandDeclaration({
94
100
  kind: "container",
95
101
  image: options.image,
96
102
  workdir: options.workdir,
97
103
  volumes: options.volumes ? options.volumes.map((volume) => ({ ...volume })) : undefined
98
- };
104
+ }, "sandbox.container");
99
105
  }
100
106
  };
101
107
  export const execution = {
102
108
  local(options = {}) {
103
- return {
109
+ return brandDeclaration({
104
110
  kind: "local",
105
111
  sandbox: options.sandbox,
106
112
  provider: options.provider
107
- };
113
+ }, "execution.local");
108
114
  },
109
115
  github(options) {
110
- return {
116
+ return brandDeclaration({
111
117
  kind: "github",
112
118
  sandbox: options.sandbox,
113
119
  provider: options.provider,
114
120
  runsOn: options.runsOn ? cloneRunsOnEntry(options.runsOn) : undefined,
115
121
  runsOnMatrix: options.runsOnMatrix ? options.runsOnMatrix.map(cloneRunsOnEntry) : undefined
116
- };
122
+ }, "execution.github");
117
123
  }
118
124
  };
119
125
  export const command = {
120
126
  policy(options = {}) {
121
- return {
127
+ return brandDeclaration({
122
128
  rules: options.rules ? options.rules.map(cloneCommandRule) : [],
123
129
  fallback: options.fallback ? cloneCommandAction(options.fallback) : undefined,
124
130
  record: options.record,
125
131
  output: options.output ? { ...options.output } : undefined,
126
132
  shims: options.shims ? [...options.shims] : undefined
127
- };
133
+ }, "command.policy");
128
134
  },
129
135
  rule(options) {
130
- return cloneCommandRule(options);
136
+ return brandDeclaration(cloneCommandRule(options), "command.rule");
131
137
  },
132
138
  allow(options = {}) {
133
- return { kind: "async-pipeline.command.allow", output: options.output ? { ...options.output } : undefined };
139
+ return brandDeclaration({ kind: "async-pipeline.command.allow", output: options.output ? { ...options.output } : undefined }, "command.allow");
134
140
  },
135
141
  deny(options = {}) {
136
- return { kind: "async-pipeline.command.deny", message: options.message, output: options.output ? { ...options.output } : undefined };
142
+ return brandDeclaration({ kind: "async-pipeline.command.deny", message: options.message, output: options.output ? { ...options.output } : undefined }, "command.deny");
137
143
  },
138
144
  mock(options = {}) {
139
- return {
145
+ return brandDeclaration({
140
146
  kind: "async-pipeline.command.mock",
141
147
  code: options.code,
142
148
  stdout: options.stdout,
143
149
  stderr: options.stderr,
144
150
  output: options.output ? { ...options.output } : undefined
145
- };
151
+ }, "command.mock");
146
152
  },
147
153
  requireApproval(options = {}) {
148
- return { kind: "async-pipeline.command.requireApproval", message: options.message, output: options.output ? { ...options.output } : undefined };
154
+ return brandDeclaration({ kind: "async-pipeline.command.requireApproval", message: options.message, output: options.output ? { ...options.output } : undefined }, "command.requireApproval");
149
155
  },
150
156
  requireEnvironment(options) {
151
- return { kind: "async-pipeline.command.requireEnvironment", name: options.name, output: options.output ? { ...options.output } : undefined };
157
+ return brandDeclaration({ kind: "async-pipeline.command.requireEnvironment", name: options.name, output: options.output ? { ...options.output } : undefined }, "command.requireEnvironment");
152
158
  }
153
159
  };
154
160
  export const trigger = {
155
161
  manual() {
156
- return { type: "manual" };
162
+ return brandDeclaration({ type: "manual" }, "trigger.manual");
157
163
  },
158
164
  github(options) {
159
- return {
165
+ return brandDeclaration({
160
166
  type: "github",
161
167
  events: [...options.events],
162
168
  branches: options.branches ? [...options.branches] : undefined,
163
169
  paths: options.paths ? [...options.paths] : undefined,
164
170
  tags: options.tags ? [...options.tags] : undefined
165
- };
171
+ }, "trigger.github");
166
172
  },
167
173
  cron(cron, options = {}) {
168
- return { type: "schedule", cron, timezone: options.timezone };
174
+ return brandDeclaration({ type: "schedule", cron, timezone: options.timezone }, "trigger.cron");
169
175
  },
170
176
  schedule(cron) {
171
- return { type: "schedule", cron };
177
+ return brandDeclaration({ type: "schedule", cron }, "trigger.schedule");
172
178
  }
173
179
  };
174
180
  export function dependsOn(...taskIds) {
175
- return { kind: "async-pipeline.directive.dependsOn", taskIds };
181
+ return brandDeclaration({ kind: "async-pipeline.directive.dependsOn", taskIds }, "directive.dependsOn");
176
182
  }
177
183
  export const source = {
178
184
  git(definition) {
179
- return { ...definition, type: "git" };
185
+ return brandDeclaration({ ...definition, type: "git" }, "source.git");
180
186
  },
181
187
  path(definition) {
182
- return { ...definition, type: "path" };
188
+ return brandDeclaration({ ...definition, type: "path" }, "source.path");
183
189
  }
184
190
  };
191
+ export function tasks(definitions) {
192
+ return brandDeclaration(definitions, "section.tasks");
193
+ }
194
+ export function jobs(definitions) {
195
+ return brandDeclaration(definitions, "section.jobs");
196
+ }
197
+ export function triggers(definitions) {
198
+ return brandDeclaration(definitions, "section.triggers");
199
+ }
200
+ export function sources(definitions) {
201
+ return brandDeclaration(definitions, "section.sources");
202
+ }
203
+ export function taskDefaults(definitions) {
204
+ return brandDeclaration(definitions, "section.taskDefaults");
205
+ }
206
+ export function agents(definitions) {
207
+ return brandDeclaration(definitions, "section.agents");
208
+ }
209
+ export function sandboxes(definitions) {
210
+ return brandDeclaration(definitions, "section.sandboxes");
211
+ }
185
212
  const PIPELINE_FIELDS = new Set(["name", "env", "commands", "agents", "sandboxes", "execution", "cache", "namedInputs", "taskDefaults", "triggers", "sync", "sources", "tasks", "jobs"]);
186
213
  const AGENT_PROFILE_FIELDS = new Set(["command", "model"]);
187
214
  const TASK_FIELDS = new Set(["description", "dependsOn", "inputs", "outputs", "cache", "retry", "timeout", "requires", "run", "steps"]);
@@ -190,6 +217,15 @@ const EXECUTION_PROFILE_FIELDS = new Set(["kind", "sandbox", "provider", "runsOn
190
217
  const GITHUB_JOB_FIELDS = new Set(["environment", "permissions", "runsOn", "runsOnMatrix"]);
191
218
  const GITHUB_PERMISSION_FIELDS = new Set(["contents", "idToken", "issues", "packages", "pullRequests"]);
192
219
  const CONTAINER_PROVIDERS = new Set(["auto", "docker", "apple-container", "lima"]);
220
+ const SECTION_KINDS = {
221
+ agents: "section.agents",
222
+ sandboxes: "section.sandboxes",
223
+ taskDefaults: "section.taskDefaults",
224
+ triggers: "section.triggers",
225
+ sources: "section.sources",
226
+ tasks: "section.tasks",
227
+ jobs: "section.jobs"
228
+ };
193
229
  function rejectUnknownFields(known, value, where) {
194
230
  for (const key of Object.keys(value)) {
195
231
  if (!known.has(key)) {
@@ -215,9 +251,6 @@ function validateDefinitionShape(definition) {
215
251
  throw pipelineError("ASYNC_PIPELINE_AGENT_INVALID", `Agent profile "${id}" requires "model": a non-empty string or env.var(...). The model is the profile's cache identity; the command is deliberately not.`);
216
252
  }
217
253
  }
218
- for (const [id, taskDefinition] of Object.entries(definition.tasks ?? {})) {
219
- rejectUnknownFields(TASK_FIELDS, taskDefinition, `Task "${id}"`);
220
- }
221
254
  for (const [id, defaults] of Object.entries(definition.taskDefaults ?? {})) {
222
255
  rejectUnknownFields(TASK_FIELDS, defaults, `taskDefaults["${id}"]`);
223
256
  }
@@ -243,10 +276,112 @@ function validateDefinitionShape(definition) {
243
276
  }
244
277
  }
245
278
  }
279
+ function normalizePipelineSections(definition) {
280
+ return {
281
+ ...definition,
282
+ agents: normalizeSection("agents", definition.agents),
283
+ sandboxes: normalizeSection("sandboxes", definition.sandboxes),
284
+ taskDefaults: normalizeSection("taskDefaults", definition.taskDefaults),
285
+ triggers: normalizeSection("triggers", definition.triggers),
286
+ sources: normalizeSection("sources", definition.sources),
287
+ tasks: normalizeSection("tasks", definition.tasks),
288
+ jobs: normalizeSection("jobs", definition.jobs)
289
+ };
290
+ }
291
+ function normalizeSection(key, value) {
292
+ if (value === undefined)
293
+ return value;
294
+ if (!isObjectRecord(value)) {
295
+ throw pipelineError("ASYNC_PIPELINE_SECTION_INVALID", `Pipeline section "${key}" must be an object.`);
296
+ }
297
+ const expectedKind = SECTION_KINDS[key];
298
+ const metadata = assertSupportedDeclaration(value);
299
+ if (metadata) {
300
+ if (metadata.kind !== expectedKind) {
301
+ throw pipelineError("ASYNC_PIPELINE_SECTION_KIND_MISMATCH", `Pipeline section "${key}" expected declaration kind "${expectedKind}", received "${metadata.kind}".`);
302
+ }
303
+ return value;
304
+ }
305
+ return brandDeclaration(value, expectedKind);
306
+ }
307
+ function flattenTaskDefinitions(definitions) {
308
+ const tasks = [];
309
+ const seen = new Set();
310
+ function visit(node, path) {
311
+ for (const [key, value] of Object.entries(node)) {
312
+ validateTaskTreeKey(key, path);
313
+ if (!isObjectRecord(value)) {
314
+ throw pipelineError("ASYNC_PIPELINE_TASK_TREE_INVALID", `Task tree entry "${[...path, key].join(".")}" must be an object.`);
315
+ }
316
+ const isTask = isTaskDefinitionNode(value, path);
317
+ if (isTask) {
318
+ const id = taskTreeId(path, key);
319
+ validateLocalTaskId(id);
320
+ if (seen.has(id)) {
321
+ throw pipelineError("ASYNC_PIPELINE_TASK_ID_COLLISION", `Task group entry "${[...path, key].join(".")}" normalizes to duplicate task id "${id}".`);
322
+ }
323
+ seen.add(id);
324
+ rejectUnknownFields(TASK_FIELDS, value, `Task "${id}"`);
325
+ tasks.push({ id, definition: value, groupPath: path });
326
+ continue;
327
+ }
328
+ if (key.includes(".")) {
329
+ throw pipelineError("ASYNC_PIPELINE_TASK_GROUP_INVALID_KEY", `Task group key "${key}" cannot contain ".". Use nested objects instead.`);
330
+ }
331
+ visit(value, [...path, key]);
332
+ }
333
+ }
334
+ visit(definitions, []);
335
+ return tasks;
336
+ }
337
+ function validateTaskTreeKey(key, path) {
338
+ if (!key.trim()) {
339
+ throw pipelineError("ASYNC_PIPELINE_TASK_GROUP_INVALID_KEY", "Task group key cannot be empty.");
340
+ }
341
+ if (key.includes(":")) {
342
+ throw pipelineError("ASYNC_PIPELINE_TASK_GROUP_INVALID_KEY", `Task group key "${key}" cannot contain ":". Use source namespaces through dependsOn instead.`);
343
+ }
344
+ if (path.length > 0 && key.includes(".")) {
345
+ throw pipelineError("ASYNC_PIPELINE_TASK_GROUP_INVALID_KEY", `Nested task group key "${key}" cannot contain ".". Use nested objects instead.`);
346
+ }
347
+ }
348
+ function taskTreeId(path, key) {
349
+ const segments = path.length > 0 && (key === "default" || key === "index") ? path : [...path, key];
350
+ return segments.join(".");
351
+ }
352
+ function isTaskDefinitionNode(value, path) {
353
+ const metadata = assertSupportedDeclaration(value);
354
+ if (metadata?.kind === "task")
355
+ return true;
356
+ if (metadata?.kind.startsWith("section."))
357
+ return false;
358
+ if (metadata)
359
+ return false;
360
+ if (Object.keys(value).some((key) => TASK_FIELDS.has(key)))
361
+ return true;
362
+ return path.length === 0 && Object.keys(value).length === 0;
363
+ }
364
+ function resolveTaskDependencies(taskId, groupPath, dependencies, knownTaskIds) {
365
+ return dependencies.map((dependency) => resolveTaskDependency(taskId, groupPath, dependency, knownTaskIds));
366
+ }
367
+ function resolveTaskDependency(taskId, groupPath, dependency, knownTaskIds) {
368
+ if (isNamespacedTaskRef(dependency) || groupPath.length === 0)
369
+ return dependency;
370
+ const groupCandidate = [...groupPath, dependency].join(".");
371
+ const groupMatch = knownTaskIds.has(groupCandidate);
372
+ const rootMatch = knownTaskIds.has(dependency);
373
+ if (groupMatch && rootMatch && groupCandidate !== dependency) {
374
+ throw pipelineError("ASYNC_PIPELINE_TASK_DEPENDENCY_AMBIGUOUS", `Task "${taskId}" depends on ambiguous local task "${dependency}". Use "${groupCandidate}" or "${dependency}" explicitly.`);
375
+ }
376
+ if (groupMatch)
377
+ return groupCandidate;
378
+ return dependency;
379
+ }
246
380
  export function definePipeline(definition) {
247
381
  return normalizePipeline(definition);
248
382
  }
249
383
  export function normalizePipeline(definition) {
384
+ definition = normalizePipelineSections(definition);
250
385
  validateDefinitionShape(definition);
251
386
  const namedInputs = definition.namedInputs ?? {};
252
387
  const cacheRegistry = normalizeCacheRegistry(definition.cache);
@@ -256,16 +391,18 @@ export function normalizePipeline(definition) {
256
391
  sources[id] = normalizeSource(id, sourceDefinition);
257
392
  }
258
393
  const tasks = {};
259
- for (const [id, taskDefinition] of Object.entries(definition.tasks)) {
394
+ const flattenedTaskDefinitions = flattenTaskDefinitions(definition.tasks);
395
+ const knownTaskIds = new Set(flattenedTaskDefinitions.map((entry) => entry.id));
396
+ for (const { id, definition: taskDefinition, groupPath } of flattenedTaskDefinitions) {
260
397
  validateLocalTaskId(id);
261
398
  const defaults = definition.taskDefaults?.[id] ?? definition.taskDefaults?.[taskName(id)] ?? {};
262
399
  const merged = { ...defaults, ...taskDefinition };
263
400
  const runItems = merged.steps ? [...merged.steps] : runItemsFromDefinition(merged.run);
264
401
  const { steps, cacheDirectives, dependsOnDirectives } = partitionRunItems(runItems);
265
- const liftedDependsOn = uniqueTaskIds([
402
+ const liftedDependsOn = resolveTaskDependencies(id, groupPath, uniqueTaskIds([
266
403
  ...(merged.dependsOn ?? []),
267
404
  ...dependsOnDirectives.flatMap((directive) => directive.taskIds)
268
- ]);
405
+ ]), knownTaskIds);
269
406
  const cache = normalizeCache(merged.cache ?? cacheDirectives[0], cacheRegistry);
270
407
  const retry = normalizeRetry(merged.retry);
271
408
  const timeoutMs = normalizeTimeout(merged.timeout);
@@ -311,6 +448,12 @@ export function normalizePipeline(definition) {
311
448
  function isDefaultOnlyEnvOptions(value) {
312
449
  return typeof value.default === "string";
313
450
  }
451
+ function isObjectRecord(value) {
452
+ return typeof value === "object" && value !== null && !Array.isArray(value);
453
+ }
454
+ function isEnvVarRef(value) {
455
+ return isObjectRecord(value) && value.kind === "async-pipeline.env.var";
456
+ }
314
457
  function normalizeAgents(definitions = {}) {
315
458
  const normalized = {};
316
459
  for (const [id, profile] of Object.entries(definitions)) {
@@ -655,21 +798,31 @@ function collectRequiredTasks(pipeline, targets) {
655
798
  }
656
799
  function normalizeSource(id, sourceDefinition) {
657
800
  const prepare = [...(sourceDefinition.prepare ?? [])];
658
- const pipeline = sourceDefinition.pipeline ?? "pipeline.ts";
659
- if (sourceDefinition.type === "git") {
660
- return {
801
+ const pipeline = sourceDefinition.pipeline ?? DEFAULT_PIPELINE_CONFIG_FILES[0];
802
+ const defaulted = !sourceDefinition.pipeline;
803
+ const normalized = sourceDefinition.type === "git"
804
+ ? {
805
+ ...sourceDefinition,
806
+ id,
807
+ pipeline,
808
+ prepare
809
+ }
810
+ : {
661
811
  ...sourceDefinition,
662
812
  id,
663
813
  pipeline,
664
814
  prepare
665
815
  };
816
+ if (defaulted) {
817
+ Object.defineProperty(normalized, SOURCE_PIPELINE_DEFAULTED, {
818
+ value: true,
819
+ enumerable: false
820
+ });
666
821
  }
667
- return {
668
- ...sourceDefinition,
669
- id,
670
- pipeline,
671
- prepare
672
- };
822
+ return normalized;
823
+ }
824
+ export function sourceUsesDefaultPipelineConfig(sourceDefinition) {
825
+ return sourceDefinition[SOURCE_PIPELINE_DEFAULTED] === true;
673
826
  }
674
827
  function validateComposedPipeline(pipeline, loadedSources) {
675
828
  for (const taskDefinition of Object.values(pipeline.tasks)) {
@@ -908,7 +1061,8 @@ function partitionRunItems(items) {
908
1061
  const steps = [];
909
1062
  const cacheDirectives = [];
910
1063
  const dependsOnDirectives = [];
911
- for (const item of items) {
1064
+ for (const rawItem of items) {
1065
+ const item = normalizeDeclaredRunItem(rawItem);
912
1066
  if (isCacheDirective(item)) {
913
1067
  cacheDirectives.push(item);
914
1068
  continue;
@@ -924,10 +1078,49 @@ function partitionRunItems(items) {
924
1078
  }
925
1079
  return { steps, cacheDirectives, dependsOnDirectives };
926
1080
  }
1081
+ function normalizeDeclaredRunItem(item) {
1082
+ const metadata = assertSupportedDeclaration(item);
1083
+ if (!metadata || !isObjectRecord(item))
1084
+ return item;
1085
+ if (metadata.kind === "shell") {
1086
+ rejectUnknownFields(SHELL_STEP_FIELDS, item, "shell declaration");
1087
+ const command = item.command;
1088
+ if (typeof command !== "string") {
1089
+ throw pipelineError("ASYNC_PIPELINE_DECLARATION_INVALID", "Shell declaration requires a string command.");
1090
+ }
1091
+ return brandDeclaration({ kind: "shell", command }, "shell");
1092
+ }
1093
+ if (metadata.kind === "deferred-shell") {
1094
+ rejectUnknownFields(SHELL_STEP_FIELDS, item, "deferred shell declaration");
1095
+ const command = item.command;
1096
+ if (typeof command !== "function") {
1097
+ throw pipelineError("ASYNC_PIPELINE_DECLARATION_INVALID", "Deferred shell declaration requires a command function.");
1098
+ }
1099
+ return brandDeclaration({ kind: "deferred-shell", command: command }, "deferred-shell");
1100
+ }
1101
+ if (metadata.kind === "agent") {
1102
+ rejectUnknownFields(DECLARED_AGENT_STEP_FIELDS, item, "agent declaration");
1103
+ const use = item.use;
1104
+ const prompt = item.prompt;
1105
+ if ((typeof use !== "string" && !isEnvVarRef(use)) || (typeof use === "string" && use.length === 0)) {
1106
+ throw pipelineError("ASYNC_PIPELINE_AGENT_INVALID", 'agent declaration requires "use": a non-empty profile id or env.var(...).');
1107
+ }
1108
+ if (typeof prompt !== "string" || prompt.length === 0) {
1109
+ throw pipelineError("ASYNC_PIPELINE_AGENT_INVALID", 'agent declaration requires a non-empty "prompt" string.');
1110
+ }
1111
+ const step = brandDeclaration({ kind: "agent", use: use, prompt }, "agent");
1112
+ if (item.model !== undefined)
1113
+ step.model = item.model;
1114
+ if (item.stdoutTo !== undefined)
1115
+ step.stdoutTo = item.stdoutTo;
1116
+ return step;
1117
+ }
1118
+ return item;
1119
+ }
927
1120
  function isDependsOnDirective(value) {
928
1121
  return Boolean(value)
929
1122
  && typeof value === "object"
930
- && value.kind === "async-pipeline.directive.dependsOn";
1123
+ && (value.kind === "async-pipeline.directive.dependsOn" || hasDeclarationKind(value, "directive.dependsOn"));
931
1124
  }
932
1125
  function uniqueTaskIds(taskIds) {
933
1126
  return [...new Set(taskIds)];