@tagma/sdk 0.4.12 → 0.4.14
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 +569 -566
- package/dist/adapters/websocket-approval.d.ts.map +1 -1
- package/dist/adapters/websocket-approval.js +3 -1
- package/dist/adapters/websocket-approval.js.map +1 -1
- package/dist/approval.d.ts.map +1 -1
- package/dist/approval.js.map +1 -1
- package/dist/completions/exit-code.d.ts.map +1 -1
- package/dist/completions/exit-code.js.map +1 -1
- package/dist/completions/file-exists.d.ts.map +1 -1
- package/dist/completions/file-exists.js.map +1 -1
- package/dist/completions/output-check.js +2 -7
- package/dist/completions/output-check.js.map +1 -1
- package/dist/config-ops.d.ts.map +1 -1
- package/dist/config-ops.js +24 -26
- package/dist/config-ops.js.map +1 -1
- package/dist/dag.d.ts.map +1 -1
- package/dist/dag.js +1 -1
- package/dist/dag.js.map +1 -1
- package/dist/drivers/claude-code.d.ts.map +1 -1
- package/dist/drivers/claude-code.js +10 -5
- package/dist/drivers/claude-code.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +54 -27
- package/dist/engine.js.map +1 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +1 -3
- package/dist/hooks.js.map +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +4 -2
- package/dist/logger.js.map +1 -1
- package/dist/pipeline-runner.d.ts.map +1 -1
- package/dist/pipeline-runner.js +10 -4
- package/dist/pipeline-runner.js.map +1 -1
- package/dist/registry.d.ts +11 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +28 -3
- package/dist/registry.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +18 -13
- package/dist/runner.js.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +14 -14
- package/dist/schema.js.map +1 -1
- package/dist/schema.test.js +5 -1
- package/dist/schema.test.js.map +1 -1
- package/dist/sdk.d.ts +2 -2
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +1 -1
- package/dist/sdk.js.map +1 -1
- package/dist/triggers/file.d.ts.map +1 -1
- package/dist/triggers/file.js +11 -4
- package/dist/triggers/file.js.map +1 -1
- package/dist/triggers/manual.d.ts.map +1 -1
- package/dist/triggers/manual.js +2 -1
- package/dist/triggers/manual.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +13 -6
- package/dist/utils.js.map +1 -1
- package/dist/validate-raw.d.ts.map +1 -1
- package/dist/validate-raw.js +40 -11
- package/dist/validate-raw.js.map +1 -1
- package/package.json +2 -2
- package/scripts/preinstall.js +1 -1
- package/src/adapters/stdin-approval.ts +106 -106
- package/src/adapters/websocket-approval.ts +224 -220
- package/src/approval.ts +131 -125
- package/src/bootstrap.ts +37 -37
- package/src/completions/exit-code.ts +34 -30
- package/src/completions/file-exists.ts +66 -60
- package/src/completions/output-check.ts +86 -86
- package/src/config-ops.ts +307 -322
- package/src/dag.ts +234 -228
- package/src/drivers/claude-code.ts +250 -240
- package/src/engine.ts +1098 -935
- package/src/hooks.ts +187 -179
- package/src/logger.ts +182 -178
- package/src/middlewares/static-context.ts +45 -45
- package/src/pipeline-runner.ts +156 -150
- package/src/registry.ts +51 -23
- package/src/runner.ts +395 -397
- package/src/schema.test.ts +5 -1
- package/src/schema.ts +338 -328
- package/src/sdk.ts +91 -81
- package/src/triggers/file.ts +33 -14
- package/src/triggers/manual.ts +86 -81
- package/src/types.ts +18 -18
- package/src/utils.ts +202 -191
- package/src/validate-raw.ts +442 -409
package/src/schema.ts
CHANGED
|
@@ -1,328 +1,338 @@
|
|
|
1
|
-
import yaml from 'js-yaml';
|
|
2
|
-
import { relative } from 'path';
|
|
3
|
-
import type {
|
|
4
|
-
PipelineConfig,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
): boolean {
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
>(
|
|
203
|
-
return
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
...(
|
|
257
|
-
...(task.
|
|
258
|
-
...(task.
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
...(task.
|
|
262
|
-
...(
|
|
263
|
-
...(task.
|
|
264
|
-
...(task.
|
|
265
|
-
? {
|
|
266
|
-
: {}),
|
|
267
|
-
...(task.
|
|
268
|
-
...(task.
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
...(track.
|
|
284
|
-
...(
|
|
285
|
-
...(track.
|
|
286
|
-
...(track.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
1
|
+
import yaml from 'js-yaml';
|
|
2
|
+
import { relative } from 'path';
|
|
3
|
+
import type {
|
|
4
|
+
PipelineConfig,
|
|
5
|
+
RawPipelineConfig,
|
|
6
|
+
RawTrackConfig,
|
|
7
|
+
RawTaskConfig,
|
|
8
|
+
TrackConfig,
|
|
9
|
+
TaskConfig,
|
|
10
|
+
Permissions,
|
|
11
|
+
CompletionConfig,
|
|
12
|
+
} from './types';
|
|
13
|
+
import { truncateForName, validatePath } from './utils';
|
|
14
|
+
import { DEFAULT_PERMISSIONS } from './types';
|
|
15
|
+
import { buildDag } from './dag';
|
|
16
|
+
|
|
17
|
+
// ═══ YAML Parsing ═══
|
|
18
|
+
|
|
19
|
+
export function parseYaml(content: string): RawPipelineConfig {
|
|
20
|
+
const doc = yaml.load(content) as { pipeline?: RawPipelineConfig };
|
|
21
|
+
if (!doc?.pipeline) {
|
|
22
|
+
throw new Error('YAML must contain a top-level "pipeline" key');
|
|
23
|
+
}
|
|
24
|
+
const p = doc.pipeline;
|
|
25
|
+
if (!p.name) throw new Error('pipeline.name is required');
|
|
26
|
+
if (!p.tracks || p.tracks.length === 0) throw new Error('pipeline.tracks must be non-empty');
|
|
27
|
+
|
|
28
|
+
// D14: Detect duplicate track IDs before per-track validation so the error
|
|
29
|
+
// message is clear ("Duplicate track id") rather than a confusing DAG error
|
|
30
|
+
// ("Duplicate task ID: track.task_x") that only surfaces at runPipeline time.
|
|
31
|
+
const seenTrackIds = new Set<string>();
|
|
32
|
+
for (const track of p.tracks) {
|
|
33
|
+
if (track.id) {
|
|
34
|
+
if (seenTrackIds.has(track.id)) {
|
|
35
|
+
throw new Error(`Duplicate track id "${track.id}": each track must have a unique id.`);
|
|
36
|
+
}
|
|
37
|
+
seenTrackIds.add(track.id);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const track of p.tracks) {
|
|
42
|
+
validateRawTrack(track);
|
|
43
|
+
}
|
|
44
|
+
return p;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// D8: IDs must start with a letter or underscore and contain only
|
|
48
|
+
// alphanumerics, underscores, and hyphens. Dots are forbidden because
|
|
49
|
+
// the engine uses "trackId.taskId" as the qualified separator — a dot in
|
|
50
|
+
// either part creates an ambiguous qualified ID and breaks resolveRef.
|
|
51
|
+
const ID_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
52
|
+
|
|
53
|
+
function assertValidId(id: string, label: string): void {
|
|
54
|
+
if (!ID_RE.test(id)) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`${label}: id "${id}" is invalid. IDs must match /^[A-Za-z_][A-Za-z0-9_-]*$/ ` +
|
|
57
|
+
`(letters, digits, underscores, hyphens; no dots or spaces; must start with letter/underscore).`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function validateRawTrack(track: RawTrackConfig): void {
|
|
63
|
+
if (!track.id) throw new Error('track.id is required');
|
|
64
|
+
assertValidId(track.id, `track "${track.id}"`);
|
|
65
|
+
if (!track.name) throw new Error(`track "${track.id}": name is required`);
|
|
66
|
+
if (!track.tasks || track.tasks.length === 0) {
|
|
67
|
+
throw new Error(`track "${track.id}": tasks must be non-empty`);
|
|
68
|
+
}
|
|
69
|
+
for (const task of track.tasks) {
|
|
70
|
+
validateRawTask(task, track.id);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function validateRawTask(task: RawTaskConfig, trackId: string): void {
|
|
75
|
+
if (!task.id) throw new Error(`track "${trackId}": task.id is required`);
|
|
76
|
+
assertValidId(task.id, `task "${task.id}" in track "${trackId}"`);
|
|
77
|
+
|
|
78
|
+
const hasPromptKey = typeof task.prompt === 'string';
|
|
79
|
+
const hasCommandKey = typeof task.command === 'string';
|
|
80
|
+
if (!hasPromptKey && !hasCommandKey) {
|
|
81
|
+
throw new Error(`task "${task.id}": must have either "prompt" or "command"`);
|
|
82
|
+
}
|
|
83
|
+
if (hasPromptKey && hasCommandKey) {
|
|
84
|
+
throw new Error(`task "${task.id}": cannot have both "prompt" and "command"`);
|
|
85
|
+
}
|
|
86
|
+
// Empty-content tasks (e.g. `prompt: ''`) are allowed at parse time and
|
|
87
|
+
// flagged as non-fatal validation errors by validate-raw.ts.
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ═══ Config Inheritance Resolution ═══
|
|
91
|
+
|
|
92
|
+
export function resolveConfig(raw: RawPipelineConfig, workDir: string): PipelineConfig {
|
|
93
|
+
// Build qualified ID set for resolving bare continue_from references
|
|
94
|
+
const allQualifiedIds = new Set<string>();
|
|
95
|
+
for (const t of raw.tracks) {
|
|
96
|
+
if (!t.id) continue;
|
|
97
|
+
for (const tk of t.tasks ?? []) {
|
|
98
|
+
if (tk.id) allQualifiedIds.add(`${t.id}.${tk.id}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function qualifyContinueFrom(ref: string, trackId: string): string {
|
|
103
|
+
// Already qualified
|
|
104
|
+
if (allQualifiedIds.has(ref)) return ref;
|
|
105
|
+
// Same-track shorthand
|
|
106
|
+
const sameTrack = `${trackId}.${ref}`;
|
|
107
|
+
if (allQualifiedIds.has(sameTrack)) return sameTrack;
|
|
108
|
+
// Cross-track bare lookup — must be unambiguous
|
|
109
|
+
let match: string | null = null;
|
|
110
|
+
for (const qid of allQualifiedIds) {
|
|
111
|
+
if (qid.endsWith(`.${ref}`)) {
|
|
112
|
+
if (match !== null) return ref; // ambiguous — leave as-is
|
|
113
|
+
match = qid;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return match ?? ref; // not found — leave as-is (validated elsewhere)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const tracks: TrackConfig[] = raw.tracks.map((rawTrack) => {
|
|
120
|
+
const trackDriver = rawTrack.driver ?? raw.driver;
|
|
121
|
+
// validatePath enforces no .. traversal and no absolute paths escaping workDir.
|
|
122
|
+
const trackCwd = rawTrack.cwd ? validatePath(rawTrack.cwd, workDir) : workDir;
|
|
123
|
+
|
|
124
|
+
const tasks: TaskConfig[] = rawTrack.tasks.map((rawTask) => {
|
|
125
|
+
const name =
|
|
126
|
+
rawTask.name ??
|
|
127
|
+
(rawTask.prompt ? truncateForName(rawTask.prompt) : (rawTask.command ?? rawTask.id));
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
id: rawTask.id,
|
|
131
|
+
name,
|
|
132
|
+
prompt: rawTask.prompt,
|
|
133
|
+
command: rawTask.command,
|
|
134
|
+
depends_on: rawTask.depends_on,
|
|
135
|
+
trigger: rawTask.trigger,
|
|
136
|
+
continue_from: rawTask.continue_from
|
|
137
|
+
? qualifyContinueFrom(rawTask.continue_from, rawTrack.id)
|
|
138
|
+
: undefined,
|
|
139
|
+
// Inheritance: Task > Track > Pipeline
|
|
140
|
+
model: rawTask.model ?? rawTrack.model ?? raw.model,
|
|
141
|
+
reasoning_effort:
|
|
142
|
+
rawTask.reasoning_effort ?? rawTrack.reasoning_effort ?? raw.reasoning_effort,
|
|
143
|
+
permissions: rawTask.permissions ?? rawTrack.permissions ?? DEFAULT_PERMISSIONS,
|
|
144
|
+
driver: rawTask.driver ?? trackDriver ?? 'claude-code',
|
|
145
|
+
timeout: rawTask.timeout,
|
|
146
|
+
// Middleware: Task-level overrides Track (including [] to disable)
|
|
147
|
+
middlewares: rawTask.middlewares !== undefined ? rawTask.middlewares : rawTrack.middlewares,
|
|
148
|
+
completion: rawTask.completion,
|
|
149
|
+
agent_profile: rawTask.agent_profile ?? rawTrack.agent_profile,
|
|
150
|
+
cwd: rawTask.cwd ? validatePath(rawTask.cwd, workDir) : trackCwd,
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
id: rawTrack.id,
|
|
156
|
+
name: rawTrack.name,
|
|
157
|
+
color: rawTrack.color,
|
|
158
|
+
agent_profile: rawTrack.agent_profile,
|
|
159
|
+
model: rawTrack.model ?? raw.model,
|
|
160
|
+
reasoning_effort: rawTrack.reasoning_effort ?? raw.reasoning_effort,
|
|
161
|
+
permissions: rawTrack.permissions ?? DEFAULT_PERMISSIONS,
|
|
162
|
+
driver: trackDriver ?? 'claude-code',
|
|
163
|
+
cwd: trackCwd,
|
|
164
|
+
middlewares: rawTrack.middlewares,
|
|
165
|
+
on_failure: rawTrack.on_failure ?? 'skip_downstream',
|
|
166
|
+
tasks,
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
name: raw.name,
|
|
172
|
+
driver: raw.driver,
|
|
173
|
+
model: raw.model,
|
|
174
|
+
reasoning_effort: raw.reasoning_effort,
|
|
175
|
+
timeout: raw.timeout,
|
|
176
|
+
plugins: raw.plugins,
|
|
177
|
+
hooks: raw.hooks,
|
|
178
|
+
tracks,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Field-by-field permissions comparison — avoids relying on JSON.stringify key order.
|
|
183
|
+
function permissionsEqual(a: Permissions | undefined, b: Permissions | undefined): boolean {
|
|
184
|
+
if (a === b) return true;
|
|
185
|
+
if (!a || !b) return false;
|
|
186
|
+
return a.read === b.read && a.write === b.write && a.execute === b.execute;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function isDefaultExitCodeCompletion(completion: CompletionConfig | undefined): boolean {
|
|
190
|
+
if (!completion || completion.type !== 'exit_code') return false;
|
|
191
|
+
const {
|
|
192
|
+
type: _type,
|
|
193
|
+
expect,
|
|
194
|
+
...rest
|
|
195
|
+
} = completion as CompletionConfig & {
|
|
196
|
+
expect?: unknown;
|
|
197
|
+
};
|
|
198
|
+
if (Object.keys(rest).length > 0) return false;
|
|
199
|
+
return expect === undefined || expect === 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function stripDefaultTaskCompletion<T extends { completion?: CompletionConfig }>(task: T): T {
|
|
203
|
+
if (!isDefaultExitCodeCompletion(task.completion)) return task;
|
|
204
|
+
const { completion: _completion, ...rest } = task;
|
|
205
|
+
return rest as T;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function stripDefaultCompletionsForSerialization<T extends PipelineConfig | RawPipelineConfig>(
|
|
209
|
+
config: T,
|
|
210
|
+
): T {
|
|
211
|
+
return {
|
|
212
|
+
...config,
|
|
213
|
+
tracks: config.tracks.map((track) => ({
|
|
214
|
+
...track,
|
|
215
|
+
tasks: track.tasks.map((task) => stripDefaultTaskCompletion(task)),
|
|
216
|
+
})),
|
|
217
|
+
} as T;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ═══ YAML Serialization ═══
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Serialize a pipeline config back to YAML string.
|
|
224
|
+
* Wraps the config under the top-level `pipeline` key as expected by parseYaml.
|
|
225
|
+
*/
|
|
226
|
+
export function serializePipeline(config: PipelineConfig | RawPipelineConfig): string {
|
|
227
|
+
return yaml.dump(
|
|
228
|
+
{ pipeline: stripDefaultCompletionsForSerialization(config) },
|
|
229
|
+
{ lineWidth: 120, indent: 2 },
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Convert a resolved PipelineConfig back to a RawPipelineConfig for serialization.
|
|
235
|
+
* Strips injected defaults and converts absolute cwd paths back to relative so the
|
|
236
|
+
* resulting YAML is portable across machines.
|
|
237
|
+
*
|
|
238
|
+
* Use this when you need to save a config that was previously loaded via
|
|
239
|
+
* loadPipeline(). For a pure load→edit→save cycle on raw YAML, prefer
|
|
240
|
+
* parseYaml() → edit RawPipelineConfig → serializePipeline().
|
|
241
|
+
*/
|
|
242
|
+
export function deresolvePipeline(config: PipelineConfig, workDir: string): RawPipelineConfig {
|
|
243
|
+
const tracks: RawTrackConfig[] = config.tracks.map((track) => {
|
|
244
|
+
const trackCwdRel =
|
|
245
|
+
track.cwd && track.cwd !== workDir ? relative(workDir, track.cwd) : undefined;
|
|
246
|
+
const effectiveTrackDriver = track.driver ?? config.driver ?? 'claude-code';
|
|
247
|
+
const effectiveTrackModel = track.model ?? config.model;
|
|
248
|
+
const effectiveTrackReasoning = track.reasoning_effort ?? config.reasoning_effort;
|
|
249
|
+
|
|
250
|
+
const tasks: RawTaskConfig[] = track.tasks.map((task) => {
|
|
251
|
+
const taskCwdRel =
|
|
252
|
+
task.cwd && task.cwd !== track.cwd ? relative(workDir, task.cwd) : undefined;
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
id: task.id,
|
|
256
|
+
...(task.name ? { name: task.name } : {}),
|
|
257
|
+
...(task.prompt !== undefined ? { prompt: task.prompt } : {}),
|
|
258
|
+
...(task.command !== undefined ? { command: task.command } : {}),
|
|
259
|
+
...(task.depends_on?.length ? { depends_on: task.depends_on } : {}),
|
|
260
|
+
...(task.trigger ? { trigger: task.trigger } : {}),
|
|
261
|
+
...(task.continue_from ? { continue_from: task.continue_from } : {}),
|
|
262
|
+
...(taskCwdRel ? { cwd: taskCwdRel } : {}),
|
|
263
|
+
...(task.model && task.model !== effectiveTrackModel ? { model: task.model } : {}),
|
|
264
|
+
...(task.reasoning_effort && task.reasoning_effort !== effectiveTrackReasoning
|
|
265
|
+
? { reasoning_effort: task.reasoning_effort }
|
|
266
|
+
: {}),
|
|
267
|
+
...(task.driver && task.driver !== effectiveTrackDriver ? { driver: task.driver } : {}),
|
|
268
|
+
...(task.timeout ? { timeout: task.timeout } : {}),
|
|
269
|
+
...(task.middlewares !== undefined ? { middlewares: task.middlewares } : {}),
|
|
270
|
+
...(task.completion && !isDefaultExitCodeCompletion(task.completion)
|
|
271
|
+
? { completion: task.completion }
|
|
272
|
+
: {}),
|
|
273
|
+
...(task.agent_profile ? { agent_profile: task.agent_profile } : {}),
|
|
274
|
+
...(task.permissions && !permissionsEqual(task.permissions, track.permissions)
|
|
275
|
+
? { permissions: task.permissions }
|
|
276
|
+
: {}),
|
|
277
|
+
};
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
id: track.id,
|
|
282
|
+
name: track.name,
|
|
283
|
+
...(track.color ? { color: track.color } : {}),
|
|
284
|
+
...(track.agent_profile ? { agent_profile: track.agent_profile } : {}),
|
|
285
|
+
...(track.model && track.model !== config.model ? { model: track.model } : {}),
|
|
286
|
+
...(track.reasoning_effort && track.reasoning_effort !== config.reasoning_effort
|
|
287
|
+
? { reasoning_effort: track.reasoning_effort }
|
|
288
|
+
: {}),
|
|
289
|
+
...(track.driver && track.driver !== (config.driver ?? 'claude-code')
|
|
290
|
+
? { driver: track.driver }
|
|
291
|
+
: {}),
|
|
292
|
+
...(trackCwdRel ? { cwd: trackCwdRel } : {}),
|
|
293
|
+
...(track.middlewares?.length ? { middlewares: track.middlewares } : {}),
|
|
294
|
+
...(track.on_failure && track.on_failure !== 'skip_downstream'
|
|
295
|
+
? { on_failure: track.on_failure }
|
|
296
|
+
: {}),
|
|
297
|
+
...(track.permissions && !permissionsEqual(track.permissions, DEFAULT_PERMISSIONS)
|
|
298
|
+
? { permissions: track.permissions }
|
|
299
|
+
: {}),
|
|
300
|
+
tasks,
|
|
301
|
+
};
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
name: config.name,
|
|
306
|
+
...(config.driver ? { driver: config.driver } : {}),
|
|
307
|
+
...(config.model ? { model: config.model } : {}),
|
|
308
|
+
...(config.reasoning_effort ? { reasoning_effort: config.reasoning_effort } : {}),
|
|
309
|
+
...(config.timeout ? { timeout: config.timeout } : {}),
|
|
310
|
+
...(config.plugins?.length ? { plugins: config.plugins } : {}),
|
|
311
|
+
...(config.hooks ? { hooks: config.hooks } : {}),
|
|
312
|
+
tracks,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ═══ Offline Validation ═══
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Validate a pipeline config without executing it.
|
|
320
|
+
* Only checks structural/DAG correctness — does not check plugin registration.
|
|
321
|
+
* Returns an array of error messages (empty = valid).
|
|
322
|
+
*/
|
|
323
|
+
export function validateConfig(config: PipelineConfig): string[] {
|
|
324
|
+
const errors: string[] = [];
|
|
325
|
+
try {
|
|
326
|
+
buildDag(config);
|
|
327
|
+
} catch (err) {
|
|
328
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
329
|
+
}
|
|
330
|
+
return errors;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ═══ Full Parse Pipeline ═══
|
|
334
|
+
|
|
335
|
+
export async function loadPipeline(yamlContent: string, workDir: string): Promise<PipelineConfig> {
|
|
336
|
+
const raw = parseYaml(yamlContent);
|
|
337
|
+
return resolveConfig(raw, workDir);
|
|
338
|
+
}
|