@tagma/sdk 0.7.9 → 0.7.11
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/README.md +16 -16
- package/dist/completions/output-check.d.ts.map +1 -1
- package/dist/completions/output-check.js +19 -10
- package/dist/completions/output-check.js.map +1 -1
- package/dist/drivers/opencode.d.ts.map +1 -1
- package/dist/drivers/opencode.js +7 -118
- package/dist/drivers/opencode.js.map +1 -1
- package/dist/duration.d.ts +2 -0
- package/dist/duration.d.ts.map +1 -0
- package/dist/duration.js +5 -0
- package/dist/duration.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/middlewares/static-context.d.ts.map +1 -1
- package/dist/middlewares/static-context.js +20 -1
- package/dist/middlewares/static-context.js.map +1 -1
- package/dist/pipeline-runner.d.ts +1 -1
- package/dist/pipeline-runner.d.ts.map +1 -1
- package/dist/pipeline-runner.js +15 -3
- package/dist/pipeline-runner.js.map +1 -1
- package/dist/plugins.d.ts +1 -1
- package/dist/plugins.d.ts.map +1 -1
- package/dist/plugins.js +1 -1
- package/dist/plugins.js.map +1 -1
- package/dist/schema.d.ts +1 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +112 -8
- package/dist/schema.js.map +1 -1
- package/dist/tagma.d.ts.map +1 -1
- package/dist/tagma.js +7 -3
- package/dist/tagma.js.map +1 -1
- package/dist/triggers/file.d.ts.map +1 -1
- package/dist/triggers/file.js +13 -10
- package/dist/triggers/file.js.map +1 -1
- package/dist/triggers/manual.d.ts.map +1 -1
- package/dist/triggers/manual.js +11 -9
- package/dist/triggers/manual.js.map +1 -1
- package/dist/validate-raw.d.ts.map +1 -1
- package/dist/validate-raw.js +322 -80
- package/dist/validate-raw.js.map +1 -1
- package/dist/yaml-compiler.js.map +1 -1
- package/package.json +4 -4
package/dist/validate-raw.js
CHANGED
|
@@ -11,12 +11,16 @@ import { extractInputReferences } from '@tagma/core';
|
|
|
11
11
|
function buildQidIndex(config) {
|
|
12
12
|
const idx = new Map();
|
|
13
13
|
for (const track of config.tracks ?? []) {
|
|
14
|
-
if (!track
|
|
14
|
+
if (!track || typeof track !== 'object')
|
|
15
|
+
continue;
|
|
16
|
+
if (!isValidTaskId(track.id))
|
|
15
17
|
continue;
|
|
16
18
|
if (!Array.isArray(track.tasks))
|
|
17
19
|
continue;
|
|
18
20
|
for (const task of track.tasks ?? []) {
|
|
19
|
-
if (!task
|
|
21
|
+
if (!task || typeof task !== 'object')
|
|
22
|
+
continue;
|
|
23
|
+
if (!isValidTaskId(task.id))
|
|
20
24
|
continue;
|
|
21
25
|
idx.set(qualifyTaskId(track.id, task.id), { track, task });
|
|
22
26
|
}
|
|
@@ -24,8 +28,40 @@ function buildQidIndex(config) {
|
|
|
24
28
|
return idx;
|
|
25
29
|
}
|
|
26
30
|
const DURATION_RE = /^(\d*\.?\d+)\s*(s|m|h|d)$/;
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
const MAX_TIMER_DURATION_MS = 2_147_483_647;
|
|
32
|
+
function validateDuration(input) {
|
|
33
|
+
if (typeof input !== 'string')
|
|
34
|
+
return { ok: false, reason: 'format' };
|
|
35
|
+
const match = DURATION_RE.exec(input.trim());
|
|
36
|
+
if (!match)
|
|
37
|
+
return { ok: false, reason: 'format' };
|
|
38
|
+
const value = parseFloat(match[1]);
|
|
39
|
+
const unit = match[2];
|
|
40
|
+
const ms = (() => {
|
|
41
|
+
switch (unit) {
|
|
42
|
+
case 's':
|
|
43
|
+
return value * 1000;
|
|
44
|
+
case 'm':
|
|
45
|
+
return value * 60_000;
|
|
46
|
+
case 'h':
|
|
47
|
+
return value * 3_600_000;
|
|
48
|
+
case 'd':
|
|
49
|
+
return value * 86_400_000;
|
|
50
|
+
default:
|
|
51
|
+
return Number.NaN;
|
|
52
|
+
}
|
|
53
|
+
})();
|
|
54
|
+
if (!Number.isFinite(ms) || ms > MAX_TIMER_DURATION_MS) {
|
|
55
|
+
return { ok: false, reason: 'range' };
|
|
56
|
+
}
|
|
57
|
+
return { ok: true };
|
|
58
|
+
}
|
|
59
|
+
function durationErrorMessage(input, validation) {
|
|
60
|
+
const label = String(input);
|
|
61
|
+
if (validation.reason === 'range') {
|
|
62
|
+
return `Duration "${label}" exceeds maximum supported timeout of ${MAX_TIMER_DURATION_MS}ms.`;
|
|
63
|
+
}
|
|
64
|
+
return `Invalid duration format "${label}". Expected e.g. "30s", "5m", "1h".`;
|
|
29
65
|
}
|
|
30
66
|
// D8: IDs may only contain letters, digits, underscores, and hyphens, and must
|
|
31
67
|
// start with a letter or underscore. Dots are explicitly forbidden because the
|
|
@@ -35,7 +71,6 @@ function isValidDuration(input) {
|
|
|
35
71
|
// engine.ts, editor) stays in lockstep with what we accept here.
|
|
36
72
|
const isValidId = isValidTaskId;
|
|
37
73
|
const VALID_ON_FAILURE = new Set(['skip_downstream', 'stop_all', 'ignore']);
|
|
38
|
-
const VALID_REASONING_EFFORT = new Set(['low', 'medium', 'high']);
|
|
39
74
|
const VALID_PIPELINE_MODES = new Set(['trusted', 'safe']);
|
|
40
75
|
const PERMISSION_FIELDS = ['read', 'write', 'execute'];
|
|
41
76
|
// Built-in plugin types always known to the SDK core, regardless of which
|
|
@@ -50,6 +85,168 @@ const BUILTIN_COMPLETION_TYPES = new Set([
|
|
|
50
85
|
]);
|
|
51
86
|
const BUILTIN_MIDDLEWARE_TYPES = new Set(['static_context']);
|
|
52
87
|
const BUILTIN_DRIVER_TYPES = new Set(['opencode']);
|
|
88
|
+
function commandConfigKind(value) {
|
|
89
|
+
if (typeof value === 'string')
|
|
90
|
+
return 'shell';
|
|
91
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
92
|
+
return null;
|
|
93
|
+
const raw = value;
|
|
94
|
+
const hasShell = 'shell' in raw;
|
|
95
|
+
const hasArgv = 'argv' in raw;
|
|
96
|
+
if (hasShell === hasArgv)
|
|
97
|
+
return null;
|
|
98
|
+
if (hasShell)
|
|
99
|
+
return typeof raw.shell === 'string' ? 'shell' : null;
|
|
100
|
+
return Array.isArray(raw.argv) && raw.argv.every((arg) => typeof arg === 'string')
|
|
101
|
+
? 'argv'
|
|
102
|
+
: null;
|
|
103
|
+
}
|
|
104
|
+
function validateCommandConfig(value, path, label, errors) {
|
|
105
|
+
const kind = commandConfigKind(value);
|
|
106
|
+
if (kind === null) {
|
|
107
|
+
errors.push({
|
|
108
|
+
path,
|
|
109
|
+
message: `${label} must be a non-empty shell string, { shell: string }, or { argv: string[] }`,
|
|
110
|
+
});
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
if (typeof value === 'string') {
|
|
114
|
+
if (value.trim().length === 0) {
|
|
115
|
+
errors.push({ path, message: `${label} shell string must not be empty` });
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
const raw = value;
|
|
121
|
+
if (kind === 'shell') {
|
|
122
|
+
if (!raw.shell || raw.shell.trim().length === 0) {
|
|
123
|
+
errors.push({ path: `${path}.shell`, message: `${label}.shell must not be empty` });
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
if (!raw.argv || raw.argv.length === 0) {
|
|
129
|
+
errors.push({ path: `${path}.argv`, message: `${label}.argv must contain at least one argument` });
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
raw.argv.forEach((arg, index) => {
|
|
133
|
+
if (arg.length === 0) {
|
|
134
|
+
errors.push({ path: `${path}.argv[${index}]`, message: `${label}.argv entries must not be empty` });
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
function commandInputReferences(command) {
|
|
140
|
+
if (typeof command === 'string')
|
|
141
|
+
return extractInputReferences(command);
|
|
142
|
+
if ('shell' in command)
|
|
143
|
+
return extractInputReferences(command.shell);
|
|
144
|
+
const refs = new Set();
|
|
145
|
+
for (const arg of command.argv) {
|
|
146
|
+
for (const ref of extractInputReferences(arg))
|
|
147
|
+
refs.add(ref);
|
|
148
|
+
}
|
|
149
|
+
return [...refs];
|
|
150
|
+
}
|
|
151
|
+
function isRecord(value) {
|
|
152
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
153
|
+
}
|
|
154
|
+
function isNonEmptyString(value) {
|
|
155
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
156
|
+
}
|
|
157
|
+
function validateStringList(value, path, label, errors) {
|
|
158
|
+
if (value === undefined)
|
|
159
|
+
return [];
|
|
160
|
+
if (!Array.isArray(value)) {
|
|
161
|
+
errors.push({ path, message: `${label} must be an array of strings` });
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
const refs = [];
|
|
165
|
+
for (let i = 0; i < value.length; i++) {
|
|
166
|
+
const item = value[i];
|
|
167
|
+
if (typeof item !== 'string' || item.trim().length === 0) {
|
|
168
|
+
errors.push({ path: `${path}[${i}]`, message: `${label} entries must be non-empty strings` });
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
refs.push(item);
|
|
172
|
+
}
|
|
173
|
+
return refs;
|
|
174
|
+
}
|
|
175
|
+
function dependencyRefs(task) {
|
|
176
|
+
return Array.isArray(task.depends_on)
|
|
177
|
+
? task.depends_on.filter((dep) => typeof dep === 'string' && dep.length > 0)
|
|
178
|
+
: [];
|
|
179
|
+
}
|
|
180
|
+
function validatePluginRef(value, path, label, errors) {
|
|
181
|
+
if (value === undefined)
|
|
182
|
+
return null;
|
|
183
|
+
if (!isRecord(value)) {
|
|
184
|
+
errors.push({ path, message: `${label} must be an object with a non-empty type` });
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
if (!isNonEmptyString(value.type)) {
|
|
188
|
+
errors.push({ path: `${path}.type`, message: `${label}.type must be a non-empty string` });
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return value.type;
|
|
192
|
+
}
|
|
193
|
+
function validateMiddlewareList(value, path, errors) {
|
|
194
|
+
if (value === undefined)
|
|
195
|
+
return [];
|
|
196
|
+
if (!Array.isArray(value)) {
|
|
197
|
+
errors.push({ path, message: 'middlewares must be an array of objects' });
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
const types = [];
|
|
201
|
+
for (let i = 0; i < value.length; i++) {
|
|
202
|
+
const type = validatePluginRef(value[i], `${path}[${i}]`, 'middleware', errors);
|
|
203
|
+
if (type !== null)
|
|
204
|
+
types.push({ index: i, type });
|
|
205
|
+
}
|
|
206
|
+
return types;
|
|
207
|
+
}
|
|
208
|
+
const HOOK_FIELDS = [
|
|
209
|
+
'pipeline_start',
|
|
210
|
+
'task_start',
|
|
211
|
+
'task_success',
|
|
212
|
+
'task_failure',
|
|
213
|
+
'pipeline_complete',
|
|
214
|
+
'pipeline_error',
|
|
215
|
+
];
|
|
216
|
+
function validateHooks(value, errors) {
|
|
217
|
+
if (value === undefined)
|
|
218
|
+
return;
|
|
219
|
+
if (!isRecord(value)) {
|
|
220
|
+
errors.push({ path: 'hooks', message: 'hooks must be an object map' });
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
for (const field of HOOK_FIELDS) {
|
|
224
|
+
if (!(field in value))
|
|
225
|
+
continue;
|
|
226
|
+
const command = value[field];
|
|
227
|
+
if (!Array.isArray(command)) {
|
|
228
|
+
validateCommandConfig(command, `hooks.${field}`, `hooks.${field}`, errors);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (command.length === 0) {
|
|
232
|
+
errors.push({ path: `hooks.${field}`, message: `hooks.${field} must not be empty` });
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
command.forEach((entry, index) => {
|
|
236
|
+
validateCommandConfig(entry, `hooks.${field}[${index}]`, `hooks.${field}[${index}]`, errors);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function validateReasoningEffort(value, path, errors) {
|
|
241
|
+
if (value === undefined)
|
|
242
|
+
return;
|
|
243
|
+
if (!isNonEmptyString(value)) {
|
|
244
|
+
errors.push({
|
|
245
|
+
path,
|
|
246
|
+
message: 'reasoning_effort must be a non-empty string',
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
53
250
|
/**
|
|
54
251
|
* Validate a raw pipeline config.
|
|
55
252
|
* Checks structure, required fields, prompt/command exclusivity,
|
|
@@ -77,7 +274,7 @@ export function validateRaw(config, knownTypes) {
|
|
|
77
274
|
? new Set([...BUILTIN_MIDDLEWARE_TYPES, ...(knownTypes.middlewares ?? [])])
|
|
78
275
|
: null;
|
|
79
276
|
// Top level
|
|
80
|
-
if (!config.name
|
|
277
|
+
if (!isNonEmptyString(config.name)) {
|
|
81
278
|
errors.push({ path: 'name', message: 'Pipeline name is required' });
|
|
82
279
|
}
|
|
83
280
|
if (config.mode && !VALID_PIPELINE_MODES.has(config.mode)) {
|
|
@@ -86,12 +283,26 @@ export function validateRaw(config, knownTypes) {
|
|
|
86
283
|
message: `Invalid mode "${config.mode}". Expected "trusted" or "safe".`,
|
|
87
284
|
});
|
|
88
285
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
286
|
+
validateReasoningEffort(config.reasoning_effort, 'reasoning_effort', errors);
|
|
287
|
+
if (config.timeout !== undefined) {
|
|
288
|
+
const validation = validateDuration(config.timeout);
|
|
289
|
+
if (!validation.ok) {
|
|
290
|
+
errors.push({
|
|
291
|
+
path: 'timeout',
|
|
292
|
+
message: durationErrorMessage(config.timeout, validation),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (config.max_concurrency !== undefined) {
|
|
297
|
+
if (!Number.isInteger(config.max_concurrency) || config.max_concurrency < 1) {
|
|
298
|
+
errors.push({
|
|
299
|
+
path: 'max_concurrency',
|
|
300
|
+
message: 'max_concurrency must be a positive integer',
|
|
301
|
+
});
|
|
302
|
+
}
|
|
94
303
|
}
|
|
304
|
+
validateStringList(config.plugins, 'plugins', 'plugins', errors);
|
|
305
|
+
validateHooks(config.hooks, errors);
|
|
95
306
|
if (knownDrivers && config.driver && !knownDrivers.has(config.driver)) {
|
|
96
307
|
errors.push({
|
|
97
308
|
path: 'driver',
|
|
@@ -126,7 +337,7 @@ export function validateRaw(config, knownTypes) {
|
|
|
126
337
|
continue;
|
|
127
338
|
}
|
|
128
339
|
const track = maybeTrack;
|
|
129
|
-
if (!track.id
|
|
340
|
+
if (!isNonEmptyString(track.id)) {
|
|
130
341
|
errors.push({ path: `${trackPath}.id`, message: 'Track id is required' });
|
|
131
342
|
}
|
|
132
343
|
else if (!isValidId(track.id)) {
|
|
@@ -141,7 +352,7 @@ export function validateRaw(config, knownTypes) {
|
|
|
141
352
|
else {
|
|
142
353
|
seenTrackIds.add(track.id);
|
|
143
354
|
}
|
|
144
|
-
if (!track.name
|
|
355
|
+
if (!isNonEmptyString(track.name)) {
|
|
145
356
|
errors.push({ path: `${trackPath}.name`, message: 'Track name is required' });
|
|
146
357
|
}
|
|
147
358
|
if (track.on_failure && !VALID_ON_FAILURE.has(track.on_failure)) {
|
|
@@ -150,12 +361,7 @@ export function validateRaw(config, knownTypes) {
|
|
|
150
361
|
message: `Invalid on_failure value "${track.on_failure}". Expected "skip_downstream", "stop_all", or "ignore".`,
|
|
151
362
|
});
|
|
152
363
|
}
|
|
153
|
-
|
|
154
|
-
errors.push({
|
|
155
|
-
path: `${trackPath}.reasoning_effort`,
|
|
156
|
-
message: `Invalid reasoning_effort "${track.reasoning_effort}". Expected "low", "medium", or "high".`,
|
|
157
|
-
});
|
|
158
|
-
}
|
|
364
|
+
validateReasoningEffort(track.reasoning_effort, `${trackPath}.reasoning_effort`, errors);
|
|
159
365
|
if (knownDrivers && track.driver && !knownDrivers.has(track.driver)) {
|
|
160
366
|
errors.push({
|
|
161
367
|
path: `${trackPath}.driver`,
|
|
@@ -167,13 +373,13 @@ export function validateRaw(config, knownTypes) {
|
|
|
167
373
|
// Track-level middlewares can reference a plugin that was uninstalled
|
|
168
374
|
// after the YAML was written - surface a warning so the user notices
|
|
169
375
|
// before hitting Run.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (
|
|
376
|
+
const trackMiddlewareTypes = validateMiddlewareList(track.middlewares, `${trackPath}.middlewares`, errors);
|
|
377
|
+
if (knownMiddlewares) {
|
|
378
|
+
for (const { index: mi, type } of trackMiddlewareTypes) {
|
|
379
|
+
if (!knownMiddlewares.has(type)) {
|
|
174
380
|
errors.push({
|
|
175
381
|
path: `${trackPath}.middlewares[${mi}].type`,
|
|
176
|
-
message: `Middleware type "${
|
|
382
|
+
message: `Middleware type "${type}" is not registered. Install the plugin (e.g. @tagma/middleware-${type}) or remove the reference - the pipeline will fail at run time.`,
|
|
177
383
|
severity: 'warning',
|
|
178
384
|
});
|
|
179
385
|
}
|
|
@@ -196,9 +402,14 @@ export function validateRaw(config, knownTypes) {
|
|
|
196
402
|
// Per-task validation
|
|
197
403
|
const seenTaskIds = new Set();
|
|
198
404
|
for (let ki = 0; ki < track.tasks.length; ki++) {
|
|
199
|
-
const task = track.tasks[ki];
|
|
200
405
|
const taskPath = `${trackPath}.tasks[${ki}]`;
|
|
201
|
-
|
|
406
|
+
const maybeTask = track.tasks[ki];
|
|
407
|
+
if (!isRecord(maybeTask)) {
|
|
408
|
+
errors.push({ path: taskPath, message: `Task ${ki} must be an object` });
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
const task = maybeTask;
|
|
412
|
+
if (!isNonEmptyString(task.id)) {
|
|
202
413
|
errors.push({ path: `${taskPath}.id`, message: 'Task id is required' });
|
|
203
414
|
continue; // Can't check further without an id
|
|
204
415
|
}
|
|
@@ -216,46 +427,44 @@ export function validateRaw(config, knownTypes) {
|
|
|
216
427
|
}
|
|
217
428
|
seenTaskIds.add(task.id);
|
|
218
429
|
const hasPromptKey = typeof task.prompt === 'string';
|
|
219
|
-
const
|
|
430
|
+
const hasCommandField = task.command !== undefined;
|
|
431
|
+
const hasCommandKey = commandConfigKind(task.command) !== null;
|
|
220
432
|
const promptEmpty = hasPromptKey && task.prompt.trim().length === 0;
|
|
221
|
-
const commandEmpty = hasCommandKey && task.command.trim().length === 0;
|
|
222
433
|
if (hasPromptKey && hasCommandKey) {
|
|
223
434
|
errors.push({
|
|
224
435
|
path: taskPath,
|
|
225
436
|
message: `Task "${task.id}": cannot have both "prompt" and "command"`,
|
|
226
437
|
});
|
|
227
438
|
}
|
|
228
|
-
else if (!hasPromptKey && !
|
|
439
|
+
else if (!hasPromptKey && !hasCommandField) {
|
|
229
440
|
errors.push({
|
|
230
441
|
path: taskPath,
|
|
231
442
|
message: `Task "${task.id}": must have "prompt" or "command"`,
|
|
232
443
|
});
|
|
233
444
|
}
|
|
445
|
+
else if (hasCommandField && !hasCommandKey) {
|
|
446
|
+
validateCommandConfig(task.command, `${taskPath}.command`, `Task "${task.id}" command`, errors);
|
|
447
|
+
}
|
|
234
448
|
else if (promptEmpty) {
|
|
235
449
|
errors.push({
|
|
236
450
|
path: taskPath,
|
|
237
451
|
message: `Task "${task.id}": prompt content cannot be empty`,
|
|
238
452
|
});
|
|
239
453
|
}
|
|
240
|
-
else if (
|
|
241
|
-
|
|
242
|
-
path: taskPath,
|
|
243
|
-
message: `Task "${task.id}": command content cannot be empty`,
|
|
244
|
-
});
|
|
454
|
+
else if (task.command !== undefined) {
|
|
455
|
+
validateCommandConfig(task.command, `${taskPath}.command`, `Task "${task.id}" command`, errors);
|
|
245
456
|
}
|
|
246
457
|
// Field-level validations
|
|
247
|
-
if (task.timeout
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
path: `${taskPath}.reasoning_effort`,
|
|
256
|
-
message: `Invalid reasoning_effort "${task.reasoning_effort}". Expected "low", "medium", or "high".`,
|
|
257
|
-
});
|
|
458
|
+
if (task.timeout !== undefined) {
|
|
459
|
+
const validation = validateDuration(task.timeout);
|
|
460
|
+
if (!validation.ok) {
|
|
461
|
+
errors.push({
|
|
462
|
+
path: `${taskPath}.timeout`,
|
|
463
|
+
message: durationErrorMessage(task.timeout, validation),
|
|
464
|
+
});
|
|
465
|
+
}
|
|
258
466
|
}
|
|
467
|
+
validateReasoningEffort(task.reasoning_effort, `${taskPath}.reasoning_effort`, errors);
|
|
259
468
|
if (knownDrivers && task.driver && !knownDrivers.has(task.driver)) {
|
|
260
469
|
errors.push({
|
|
261
470
|
path: `${taskPath}.driver`,
|
|
@@ -268,39 +477,46 @@ export function validateRaw(config, knownTypes) {
|
|
|
268
477
|
// Only fire when the host supplied a `knownTypes` snapshot, so offline
|
|
269
478
|
// validation stays quiet. The messages deliberately name the npm
|
|
270
479
|
// scope so users can copy-paste the install command.
|
|
271
|
-
|
|
480
|
+
const triggerType = validatePluginRef(task.trigger, `${taskPath}.trigger`, 'trigger', errors);
|
|
481
|
+
const completionType = validatePluginRef(task.completion, `${taskPath}.completion`, 'completion', errors);
|
|
482
|
+
const taskMiddlewareTypes = validateMiddlewareList(task.middlewares, `${taskPath}.middlewares`, errors);
|
|
483
|
+
if (knownTriggers && triggerType !== null && !knownTriggers.has(triggerType)) {
|
|
272
484
|
errors.push({
|
|
273
485
|
path: `${taskPath}.trigger.type`,
|
|
274
|
-
message: `Trigger type "${
|
|
486
|
+
message: `Trigger type "${triggerType}" is not registered. Install the plugin (e.g. @tagma/trigger-${triggerType}) or the task will fail at run time.`,
|
|
275
487
|
severity: 'warning',
|
|
276
488
|
});
|
|
277
489
|
}
|
|
278
|
-
if (knownCompletions &&
|
|
279
|
-
task.completion?.type &&
|
|
280
|
-
!knownCompletions.has(task.completion.type)) {
|
|
490
|
+
if (knownCompletions && completionType !== null && !knownCompletions.has(completionType)) {
|
|
281
491
|
errors.push({
|
|
282
492
|
path: `${taskPath}.completion.type`,
|
|
283
|
-
message: `Completion type "${
|
|
493
|
+
message: `Completion type "${completionType}" is not registered. Install the plugin (e.g. @tagma/completion-${completionType}) or the task will fail at run time.`,
|
|
284
494
|
severity: 'warning',
|
|
285
495
|
});
|
|
286
496
|
}
|
|
287
|
-
if (knownMiddlewares
|
|
288
|
-
for (
|
|
289
|
-
|
|
290
|
-
if (mw?.type && !knownMiddlewares.has(mw.type)) {
|
|
497
|
+
if (knownMiddlewares) {
|
|
498
|
+
for (const { index: mi, type } of taskMiddlewareTypes) {
|
|
499
|
+
if (!knownMiddlewares.has(type)) {
|
|
291
500
|
errors.push({
|
|
292
501
|
path: `${taskPath}.middlewares[${mi}].type`,
|
|
293
|
-
message: `Middleware type "${
|
|
502
|
+
message: `Middleware type "${type}" is not registered. Install the plugin (e.g. @tagma/middleware-${type}) or remove the reference - the pipeline will fail at run time.`,
|
|
294
503
|
severity: 'warning',
|
|
295
504
|
});
|
|
296
505
|
}
|
|
297
506
|
}
|
|
298
507
|
}
|
|
299
508
|
// Port declaration checks
|
|
509
|
+
const deps = validateStringList(task.depends_on, `${taskPath}.depends_on`, 'task.depends_on', errors);
|
|
510
|
+
if (task.continue_from !== undefined && !isNonEmptyString(task.continue_from)) {
|
|
511
|
+
errors.push({
|
|
512
|
+
path: `${taskPath}.continue_from`,
|
|
513
|
+
message: 'task.continue_from must be a non-empty string',
|
|
514
|
+
});
|
|
515
|
+
}
|
|
300
516
|
validateTaskPorts(task, track.id, taskPath, qidIndex, index, errors);
|
|
301
517
|
// depends_on reference checks
|
|
302
|
-
if (
|
|
303
|
-
for (const dep of
|
|
518
|
+
if (deps.length > 0) {
|
|
519
|
+
for (const dep of deps) {
|
|
304
520
|
const resolved = resolveTaskRef(dep, track.id, index);
|
|
305
521
|
if (resolved.kind === 'not_found') {
|
|
306
522
|
errors.push({
|
|
@@ -317,7 +533,7 @@ export function validateRaw(config, knownTypes) {
|
|
|
317
533
|
}
|
|
318
534
|
}
|
|
319
535
|
// continue_from reference check
|
|
320
|
-
if (task.continue_from) {
|
|
536
|
+
if (isNonEmptyString(task.continue_from)) {
|
|
321
537
|
const resolved = resolveTaskRef(task.continue_from, track.id, index);
|
|
322
538
|
if (resolved.kind === 'not_found') {
|
|
323
539
|
errors.push({
|
|
@@ -331,8 +547,8 @@ export function validateRaw(config, knownTypes) {
|
|
|
331
547
|
message: `Task "${task.id}": continue_from "${task.continue_from}" is ambiguous - multiple tracks have a task with this id. Use the fully-qualified form "trackId.${task.continue_from}".`,
|
|
332
548
|
});
|
|
333
549
|
}
|
|
334
|
-
else if (
|
|
335
|
-
!
|
|
550
|
+
else if (deps.length === 0 ||
|
|
551
|
+
!deps.some((dep) => {
|
|
336
552
|
const depResolved = resolveTaskRef(dep, track.id, index);
|
|
337
553
|
return depResolved.kind === 'resolved' && depResolved.qid === resolved.qid;
|
|
338
554
|
})) {
|
|
@@ -472,7 +688,7 @@ function validateInputBindingSources(task, trackId, taskPath, index, errors) {
|
|
|
472
688
|
const upstreamId = bindingSourceTaskId(source);
|
|
473
689
|
if (!upstreamId)
|
|
474
690
|
continue;
|
|
475
|
-
const deps = task
|
|
691
|
+
const deps = dependencyRefs(task);
|
|
476
692
|
const isDirectDep = deps.some((dep) => {
|
|
477
693
|
const resolved = resolveTaskRef(dep, trackId, index);
|
|
478
694
|
return resolved.kind === 'resolved' && resolved.qid === upstreamId;
|
|
@@ -499,7 +715,7 @@ function bindingSourceTaskId(source) {
|
|
|
499
715
|
return null;
|
|
500
716
|
}
|
|
501
717
|
function validateTaskPorts(task, trackId, taskPath, qidIndex, index, errors) {
|
|
502
|
-
const isPromptTask = typeof task.prompt === 'string' &&
|
|
718
|
+
const isPromptTask = typeof task.prompt === 'string' && commandConfigKind(task.command) === null;
|
|
503
719
|
validateBindingMap(task.inputs, `${taskPath}.inputs`, 'inputs', errors);
|
|
504
720
|
validateBindingMap(task.outputs, `${taskPath}.outputs`, 'outputs', errors);
|
|
505
721
|
validateInputBindingSources(task, trackId, taskPath, index, errors);
|
|
@@ -513,8 +729,8 @@ function validateTaskPorts(task, trackId, taskPath, qidIndex, index, errors) {
|
|
|
513
729
|
for (const n of extractInputReferences(task.prompt))
|
|
514
730
|
referenced.add(n);
|
|
515
731
|
}
|
|
516
|
-
if (
|
|
517
|
-
for (const n of
|
|
732
|
+
if (commandConfigKind(task.command) !== null) {
|
|
733
|
+
for (const n of commandInputReferences(task.command))
|
|
518
734
|
referenced.add(n);
|
|
519
735
|
}
|
|
520
736
|
let availableInputs;
|
|
@@ -559,7 +775,7 @@ function validateTaskPorts(task, trackId, taskPath, qidIndex, index, errors) {
|
|
|
559
775
|
*/
|
|
560
776
|
function collectUpstreamCommandOutputNames(task, trackId, qidIndex, index) {
|
|
561
777
|
const names = new Set();
|
|
562
|
-
for (const dep of task
|
|
778
|
+
for (const dep of dependencyRefs(task)) {
|
|
563
779
|
const r = resolveTaskRef(dep, trackId, index);
|
|
564
780
|
if (r.kind !== 'resolved')
|
|
565
781
|
continue;
|
|
@@ -567,7 +783,7 @@ function collectUpstreamCommandOutputNames(task, trackId, qidIndex, index) {
|
|
|
567
783
|
if (!entry)
|
|
568
784
|
continue;
|
|
569
785
|
// Only Command tasks contribute - Prompt upstreams pass free text.
|
|
570
|
-
if (
|
|
786
|
+
if (commandConfigKind(entry.task.command) === null)
|
|
571
787
|
continue;
|
|
572
788
|
const outputs = entry.task.outputs;
|
|
573
789
|
if (!outputs || typeof outputs !== 'object' || Array.isArray(outputs))
|
|
@@ -594,12 +810,12 @@ function collectUpstreamCommandOutputNames(task, trackId, qidIndex, index) {
|
|
|
594
810
|
function validateInferredPromptPortConflicts(task, trackId, taskPath, qidIndex, index, errors) {
|
|
595
811
|
// Input collision
|
|
596
812
|
const producersByName = new Map();
|
|
597
|
-
for (const dep of task
|
|
813
|
+
for (const dep of dependencyRefs(task)) {
|
|
598
814
|
const r = resolveTaskRef(dep, trackId, index);
|
|
599
815
|
if (r.kind !== 'resolved')
|
|
600
816
|
continue;
|
|
601
817
|
const entry = qidIndex.get(r.qid);
|
|
602
|
-
if (!entry ||
|
|
818
|
+
if (!entry || commandConfigKind(entry.task.command) === null)
|
|
603
819
|
continue;
|
|
604
820
|
const outputs = entry.task.outputs;
|
|
605
821
|
if (!outputs || typeof outputs !== 'object' || Array.isArray(outputs))
|
|
@@ -632,9 +848,9 @@ function validateInferredPromptPortConflicts(task, trackId, taskPath, qidIndex,
|
|
|
632
848
|
for (const [downstreamQid, entry] of qidIndex) {
|
|
633
849
|
if (downstreamQid === taskQid)
|
|
634
850
|
continue;
|
|
635
|
-
if (
|
|
851
|
+
if (commandConfigKind(entry.task.command) === null)
|
|
636
852
|
continue; // only downstream Commands contribute
|
|
637
|
-
const deps = entry.task
|
|
853
|
+
const deps = dependencyRefs(entry.task);
|
|
638
854
|
let dependsOnUs = false;
|
|
639
855
|
for (const d of deps) {
|
|
640
856
|
const r = resolveTaskRef(d, entry.track.id, index);
|
|
@@ -651,24 +867,50 @@ function validateInferredPromptPortConflicts(task, trackId, taskPath, qidIndex,
|
|
|
651
867
|
for (const [inputName, binding] of Object.entries(inputs)) {
|
|
652
868
|
if (!binding || typeof binding !== 'object' || Array.isArray(binding))
|
|
653
869
|
continue;
|
|
870
|
+
const outputName = inferredPromptOutputName(inputName, binding, taskQid);
|
|
871
|
+
if (outputName === null)
|
|
872
|
+
continue;
|
|
654
873
|
const shape = bindingShapeKey(binding);
|
|
655
|
-
const prior = consumerShapeByName.get(
|
|
874
|
+
const prior = consumerShapeByName.get(outputName);
|
|
656
875
|
if (!prior) {
|
|
657
|
-
consumerShapeByName.set(
|
|
876
|
+
consumerShapeByName.set(outputName, { shape, firstConsumer: downstreamQid });
|
|
658
877
|
continue;
|
|
659
878
|
}
|
|
660
|
-
if (prior.shape !== shape && !reported.has(
|
|
661
|
-
reported.add(
|
|
879
|
+
if (prior.shape !== shape && !reported.has(outputName)) {
|
|
880
|
+
reported.add(outputName);
|
|
662
881
|
errors.push({
|
|
663
882
|
path: taskPath,
|
|
664
883
|
message: `Task "${task.id}": downstream Commands ${prior.firstConsumer} and ` +
|
|
665
|
-
`${downstreamQid} disagree on the shape of inferred output "${
|
|
884
|
+
`${downstreamQid} disagree on the shape of inferred output "${outputName}" - ` +
|
|
666
885
|
`a single LLM emission cannot satisfy both. Rename on one side.`,
|
|
667
886
|
});
|
|
668
887
|
}
|
|
669
888
|
}
|
|
670
889
|
}
|
|
671
890
|
}
|
|
891
|
+
function inferredPromptOutputName(inputName, binding, promptTaskId) {
|
|
892
|
+
if (typeof binding.from !== 'string' || binding.from.length === 0)
|
|
893
|
+
return inputName;
|
|
894
|
+
const source = binding.from;
|
|
895
|
+
if (source.startsWith('outputs.'))
|
|
896
|
+
return source.slice('outputs.'.length);
|
|
897
|
+
const outputMarker = '.outputs.';
|
|
898
|
+
const outputIdx = source.lastIndexOf(outputMarker);
|
|
899
|
+
if (outputIdx > 0) {
|
|
900
|
+
const sourceTaskId = source.slice(0, outputIdx);
|
|
901
|
+
if (sourceTaskId !== promptTaskId)
|
|
902
|
+
return null;
|
|
903
|
+
return source.slice(outputIdx + outputMarker.length);
|
|
904
|
+
}
|
|
905
|
+
const dot = source.lastIndexOf('.');
|
|
906
|
+
if (dot > 0) {
|
|
907
|
+
const sourceTaskId = source.slice(0, dot);
|
|
908
|
+
if (sourceTaskId !== promptTaskId)
|
|
909
|
+
return null;
|
|
910
|
+
return source.slice(dot + 1);
|
|
911
|
+
}
|
|
912
|
+
return source;
|
|
913
|
+
}
|
|
672
914
|
function bindingShapeKey(port) {
|
|
673
915
|
if ((port.type ?? 'json') !== 'enum')
|
|
674
916
|
return String(port.type ?? 'json');
|
|
@@ -679,21 +921,21 @@ function detectCycles(config, index) {
|
|
|
679
921
|
// Build adjacency: qualifiedId ->[resolved dep qualifiedIds]
|
|
680
922
|
const adj = new Map();
|
|
681
923
|
for (const track of config.tracks) {
|
|
682
|
-
if (!track.id)
|
|
924
|
+
if (!track || typeof track !== 'object' || !isValidTaskId(track.id))
|
|
683
925
|
continue;
|
|
684
926
|
if (!Array.isArray(track.tasks))
|
|
685
927
|
continue;
|
|
686
928
|
for (const task of track.tasks ?? []) {
|
|
687
|
-
if (!task.id)
|
|
929
|
+
if (!task || typeof task !== 'object' || !isValidTaskId(task.id))
|
|
688
930
|
continue;
|
|
689
931
|
const qid = qualifyTaskId(track.id, task.id);
|
|
690
932
|
const deps = [];
|
|
691
|
-
for (const dep of task
|
|
933
|
+
for (const dep of dependencyRefs(task)) {
|
|
692
934
|
const resolved = resolveTaskRef(dep, track.id, index);
|
|
693
935
|
if (resolved.kind === 'resolved')
|
|
694
936
|
deps.push(resolved.qid);
|
|
695
937
|
}
|
|
696
|
-
if (task.continue_from) {
|
|
938
|
+
if (isNonEmptyString(task.continue_from)) {
|
|
697
939
|
const resolved = resolveTaskRef(task.continue_from, track.id, index);
|
|
698
940
|
if (resolved.kind === 'resolved' && !deps.includes(resolved.qid))
|
|
699
941
|
deps.push(resolved.qid);
|