@tagma/sdk 0.1.3 → 0.1.4
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 +139 -139
- package/package.json +4 -4
- package/src/adapters/stdin-approval.ts +117 -117
- package/src/adapters/websocket-approval.ts +144 -144
- package/src/completions/exit-code.ts +19 -19
- package/src/completions/file-exists.ts +39 -39
- package/src/completions/output-check.ts +57 -57
- package/src/dag.ts +137 -137
- package/src/drivers/claude-code.ts +207 -207
- package/src/engine.ts +637 -598
- package/src/hooks.ts +138 -138
- package/src/logger.ts +107 -100
- package/src/middlewares/static-context.ts +29 -29
- package/src/runner.ts +193 -193
- package/src/schema.ts +260 -260
- package/src/triggers/file.ts +94 -94
- package/src/triggers/manual.ts +61 -61
- package/src/utils.ts +147 -147
package/src/schema.ts
CHANGED
|
@@ -1,260 +1,260 @@
|
|
|
1
|
-
import yaml from 'js-yaml';
|
|
2
|
-
import { resolve } from 'path';
|
|
3
|
-
import type {
|
|
4
|
-
PipelineConfig, RawPipelineConfig, RawTrackConfig, RawTaskConfig,
|
|
5
|
-
TrackConfig, TaskConfig, Permissions, MiddlewareConfig,
|
|
6
|
-
TemplateConfig, TemplateParamDef,
|
|
7
|
-
} from './types';
|
|
8
|
-
import { truncateForName, validatePathParam } from './utils';
|
|
9
|
-
import { DEFAULT_PERMISSIONS } from './types';
|
|
10
|
-
|
|
11
|
-
// ═══ YAML Parsing ═══
|
|
12
|
-
|
|
13
|
-
export function parseYaml(content: string): RawPipelineConfig {
|
|
14
|
-
const doc = yaml.load(content) as { pipeline?: RawPipelineConfig };
|
|
15
|
-
if (!doc?.pipeline) {
|
|
16
|
-
throw new Error('YAML must contain a top-level "pipeline" key');
|
|
17
|
-
}
|
|
18
|
-
const p = doc.pipeline;
|
|
19
|
-
if (!p.name) throw new Error('pipeline.name is required');
|
|
20
|
-
if (!p.tracks || p.tracks.length === 0) throw new Error('pipeline.tracks must be non-empty');
|
|
21
|
-
|
|
22
|
-
for (const track of p.tracks) {
|
|
23
|
-
validateRawTrack(track);
|
|
24
|
-
}
|
|
25
|
-
return p;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function validateRawTrack(track: RawTrackConfig): void {
|
|
29
|
-
if (!track.id) throw new Error('track.id is required');
|
|
30
|
-
if (!track.name) throw new Error(`track "${track.id}": name is required`);
|
|
31
|
-
if (!track.tasks || track.tasks.length === 0) {
|
|
32
|
-
throw new Error(`track "${track.id}": tasks must be non-empty`);
|
|
33
|
-
}
|
|
34
|
-
for (const task of track.tasks) {
|
|
35
|
-
validateRawTask(task, track.id);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function validateRawTask(task: RawTaskConfig, trackId: string): void {
|
|
40
|
-
if (!task.id) throw new Error(`track "${trackId}": task.id is required`);
|
|
41
|
-
if (task.use) return; // template usage, validated later
|
|
42
|
-
|
|
43
|
-
const hasPrompt = typeof task.prompt === 'string' && task.prompt.length > 0;
|
|
44
|
-
const hasCommand = typeof task.command === 'string' && task.command.length > 0;
|
|
45
|
-
if (!hasPrompt && !hasCommand) {
|
|
46
|
-
throw new Error(`task "${task.id}": must have either "prompt" or "command"`);
|
|
47
|
-
}
|
|
48
|
-
if (hasPrompt && hasCommand) {
|
|
49
|
-
throw new Error(`task "${task.id}": cannot have both "prompt" and "command"`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ═══ Template Expansion ═══
|
|
54
|
-
|
|
55
|
-
export async function expandTemplates(
|
|
56
|
-
tasks: readonly RawTaskConfig[],
|
|
57
|
-
instancePrefix: string,
|
|
58
|
-
): Promise<RawTaskConfig[]> {
|
|
59
|
-
const result: RawTaskConfig[] = [];
|
|
60
|
-
|
|
61
|
-
for (const task of tasks) {
|
|
62
|
-
if (!task.use) {
|
|
63
|
-
result.push(task);
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const template = await loadTemplate(task.use);
|
|
68
|
-
const params = resolveTemplateParams(template, task.with ?? {}, task.id);
|
|
69
|
-
const expanded = expandTemplateTask(template, params, task.id, instancePrefix);
|
|
70
|
-
result.push(...expanded);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return result;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function loadTemplate(ref: string): Promise<TemplateConfig> {
|
|
77
|
-
// Strip version suffix for import
|
|
78
|
-
const moduleName = ref.replace(/@v\d+$/, '');
|
|
79
|
-
try {
|
|
80
|
-
const mod = await import(moduleName);
|
|
81
|
-
// Expect the module to export a template.yaml content or parsed object
|
|
82
|
-
if (mod.template) return mod.template as TemplateConfig;
|
|
83
|
-
|
|
84
|
-
// Try loading template.yaml from the package
|
|
85
|
-
const pkgPath = require.resolve(`${moduleName}/template.yaml`);
|
|
86
|
-
const content = await Bun.file(pkgPath).text();
|
|
87
|
-
const doc = yaml.load(content) as { template: TemplateConfig };
|
|
88
|
-
return doc.template;
|
|
89
|
-
} catch {
|
|
90
|
-
throw new Error(`Failed to load template: "${ref}". Is the package installed?`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function resolveTemplateParams(
|
|
95
|
-
template: TemplateConfig,
|
|
96
|
-
provided: Record<string, unknown>,
|
|
97
|
-
instanceId: string,
|
|
98
|
-
): Record<string, unknown> {
|
|
99
|
-
const params: Record<string, unknown> = {};
|
|
100
|
-
const defs = template.params ?? {};
|
|
101
|
-
|
|
102
|
-
for (const [key, def] of Object.entries(defs)) {
|
|
103
|
-
const value = provided[key] ?? def.default;
|
|
104
|
-
if (value === undefined) {
|
|
105
|
-
throw new Error(`Template "${template.name}" instance "${instanceId}": missing required param "${key}"`);
|
|
106
|
-
}
|
|
107
|
-
validateParamType(key, value, def, template.name, instanceId);
|
|
108
|
-
params[key] = value;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Warn about unknown params
|
|
112
|
-
for (const key of Object.keys(provided)) {
|
|
113
|
-
if (!(key in defs)) {
|
|
114
|
-
console.warn(`Template "${template.name}" instance "${instanceId}": unknown param "${key}"`);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return params;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function validateParamType(
|
|
122
|
-
key: string, value: unknown, def: TemplateParamDef,
|
|
123
|
-
templateName: string, instanceId: string,
|
|
124
|
-
): void {
|
|
125
|
-
const ctx = `Template "${templateName}" instance "${instanceId}" param "${key}"`;
|
|
126
|
-
const ptype = def.type ?? 'string';
|
|
127
|
-
|
|
128
|
-
switch (ptype) {
|
|
129
|
-
case 'string':
|
|
130
|
-
if (typeof value !== 'string') throw new Error(`${ctx}: expected string, got ${typeof value}`);
|
|
131
|
-
break;
|
|
132
|
-
case 'path':
|
|
133
|
-
if (typeof value !== 'string') throw new Error(`${ctx}: expected path string, got ${typeof value}`);
|
|
134
|
-
validatePathParam(value);
|
|
135
|
-
break;
|
|
136
|
-
case 'enum':
|
|
137
|
-
if (!def.enum?.includes(value as string)) {
|
|
138
|
-
throw new Error(`${ctx}: value "${value}" not in allowed values [${def.enum?.join(', ')}]`);
|
|
139
|
-
}
|
|
140
|
-
break;
|
|
141
|
-
case 'number':
|
|
142
|
-
if (typeof value !== 'number') throw new Error(`${ctx}: expected number, got ${typeof value}`);
|
|
143
|
-
if (def.min !== undefined && value < def.min) throw new Error(`${ctx}: ${value} < min ${def.min}`);
|
|
144
|
-
if (def.max !== undefined && value > def.max) throw new Error(`${ctx}: ${value} > max ${def.max}`);
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function expandTemplateTask(
|
|
150
|
-
template: TemplateConfig,
|
|
151
|
-
params: Record<string, unknown>,
|
|
152
|
-
instanceId: string,
|
|
153
|
-
instancePrefix: string,
|
|
154
|
-
): RawTaskConfig[] {
|
|
155
|
-
return template.tasks.map(task => {
|
|
156
|
-
const prefixedId = `${instanceId}.${task.id}`;
|
|
157
|
-
|
|
158
|
-
// Replace ${{ params.xxx }} in string fields
|
|
159
|
-
const interpolate = (s: string): string =>
|
|
160
|
-
s.replace(/\$\{\{\s*params\.(\w+)\s*\}\}/g, (_, key) => String(params[key] ?? ''));
|
|
161
|
-
|
|
162
|
-
const newTask: Record<string, unknown> = { ...task, id: prefixedId };
|
|
163
|
-
|
|
164
|
-
// Interpolate string fields
|
|
165
|
-
if (task.prompt) newTask.prompt = interpolate(task.prompt);
|
|
166
|
-
if (task.command) newTask.command = interpolate(task.command);
|
|
167
|
-
|
|
168
|
-
// Namespace depends_on
|
|
169
|
-
if (task.depends_on) {
|
|
170
|
-
newTask.depends_on = task.depends_on.map(dep => `${instanceId}.${dep}`);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Namespace continue_from
|
|
174
|
-
if (task.continue_from) {
|
|
175
|
-
newTask.continue_from = `${instanceId}.${task.continue_from}`;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Rewrite output path to instance namespace
|
|
179
|
-
if (task.output) {
|
|
180
|
-
const original = interpolate(task.output);
|
|
181
|
-
newTask.output = original.replace('./tmp/', `./tmp/${instanceId}/`);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return newTask as unknown as RawTaskConfig;
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// ═══ Config Inheritance Resolution ═══
|
|
189
|
-
|
|
190
|
-
export function resolveConfig(raw: RawPipelineConfig, workDir: string): PipelineConfig {
|
|
191
|
-
const tracks: TrackConfig[] = raw.tracks.map(rawTrack => {
|
|
192
|
-
const trackDriver = rawTrack.driver ?? raw.driver;
|
|
193
|
-
const trackCwd = rawTrack.cwd ? resolve(workDir, rawTrack.cwd) : workDir;
|
|
194
|
-
|
|
195
|
-
const tasks: TaskConfig[] = rawTrack.tasks.map(rawTask => {
|
|
196
|
-
const name = rawTask.name
|
|
197
|
-
?? (rawTask.prompt ? truncateForName(rawTask.prompt) : rawTask.command ?? rawTask.id);
|
|
198
|
-
|
|
199
|
-
return {
|
|
200
|
-
id: rawTask.id,
|
|
201
|
-
name,
|
|
202
|
-
prompt: rawTask.prompt,
|
|
203
|
-
command: rawTask.command,
|
|
204
|
-
depends_on: rawTask.depends_on,
|
|
205
|
-
trigger: rawTask.trigger,
|
|
206
|
-
continue_from: rawTask.continue_from,
|
|
207
|
-
output: rawTask.output,
|
|
208
|
-
// Inheritance: Task > Track
|
|
209
|
-
model_tier: rawTask.model_tier ?? rawTrack.model_tier ?? 'medium',
|
|
210
|
-
permissions: rawTask.permissions ?? rawTrack.permissions ?? DEFAULT_PERMISSIONS,
|
|
211
|
-
driver: rawTask.driver ?? trackDriver ?? 'claude-code',
|
|
212
|
-
timeout: rawTask.timeout,
|
|
213
|
-
// Middleware: Task-level overrides Track (including [] to disable)
|
|
214
|
-
middlewares: rawTask.middlewares !== undefined ? rawTask.middlewares : rawTrack.middlewares,
|
|
215
|
-
completion: rawTask.completion,
|
|
216
|
-
agent_profile: rawTask.agent_profile ?? rawTrack.agent_profile,
|
|
217
|
-
cwd: rawTask.cwd ? resolve(workDir, rawTask.cwd) : trackCwd,
|
|
218
|
-
};
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
return {
|
|
222
|
-
id: rawTrack.id,
|
|
223
|
-
name: rawTrack.name,
|
|
224
|
-
color: rawTrack.color,
|
|
225
|
-
agent_profile: rawTrack.agent_profile,
|
|
226
|
-
model_tier: rawTrack.model_tier ?? 'medium',
|
|
227
|
-
permissions: rawTrack.permissions ?? DEFAULT_PERMISSIONS,
|
|
228
|
-
driver: trackDriver ?? 'claude-code',
|
|
229
|
-
cwd: trackCwd,
|
|
230
|
-
middlewares: rawTrack.middlewares,
|
|
231
|
-
on_failure: rawTrack.on_failure ?? 'skip_downstream',
|
|
232
|
-
tasks,
|
|
233
|
-
};
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
return {
|
|
237
|
-
name: raw.name,
|
|
238
|
-
driver: raw.driver,
|
|
239
|
-
timeout: raw.timeout,
|
|
240
|
-
plugins: raw.plugins,
|
|
241
|
-
hooks: raw.hooks,
|
|
242
|
-
tracks,
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// ═══ Full Parse Pipeline ═══
|
|
247
|
-
|
|
248
|
-
export async function loadPipeline(yamlContent: string, workDir: string): Promise<PipelineConfig> {
|
|
249
|
-
const raw = parseYaml(yamlContent);
|
|
250
|
-
|
|
251
|
-
// Expand templates in each track
|
|
252
|
-
const expandedTracks: RawTrackConfig[] = [];
|
|
253
|
-
for (const track of raw.tracks) {
|
|
254
|
-
const expandedTasks = await expandTemplates(track.tasks, track.id);
|
|
255
|
-
expandedTracks.push({ ...track, tasks: expandedTasks });
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const expandedRaw: RawPipelineConfig = { ...raw, tracks: expandedTracks };
|
|
259
|
-
return resolveConfig(expandedRaw, workDir);
|
|
260
|
-
}
|
|
1
|
+
import yaml from 'js-yaml';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import type {
|
|
4
|
+
PipelineConfig, RawPipelineConfig, RawTrackConfig, RawTaskConfig,
|
|
5
|
+
TrackConfig, TaskConfig, Permissions, MiddlewareConfig,
|
|
6
|
+
TemplateConfig, TemplateParamDef,
|
|
7
|
+
} from './types';
|
|
8
|
+
import { truncateForName, validatePathParam } from './utils';
|
|
9
|
+
import { DEFAULT_PERMISSIONS } from './types';
|
|
10
|
+
|
|
11
|
+
// ═══ YAML Parsing ═══
|
|
12
|
+
|
|
13
|
+
export function parseYaml(content: string): RawPipelineConfig {
|
|
14
|
+
const doc = yaml.load(content) as { pipeline?: RawPipelineConfig };
|
|
15
|
+
if (!doc?.pipeline) {
|
|
16
|
+
throw new Error('YAML must contain a top-level "pipeline" key');
|
|
17
|
+
}
|
|
18
|
+
const p = doc.pipeline;
|
|
19
|
+
if (!p.name) throw new Error('pipeline.name is required');
|
|
20
|
+
if (!p.tracks || p.tracks.length === 0) throw new Error('pipeline.tracks must be non-empty');
|
|
21
|
+
|
|
22
|
+
for (const track of p.tracks) {
|
|
23
|
+
validateRawTrack(track);
|
|
24
|
+
}
|
|
25
|
+
return p;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validateRawTrack(track: RawTrackConfig): void {
|
|
29
|
+
if (!track.id) throw new Error('track.id is required');
|
|
30
|
+
if (!track.name) throw new Error(`track "${track.id}": name is required`);
|
|
31
|
+
if (!track.tasks || track.tasks.length === 0) {
|
|
32
|
+
throw new Error(`track "${track.id}": tasks must be non-empty`);
|
|
33
|
+
}
|
|
34
|
+
for (const task of track.tasks) {
|
|
35
|
+
validateRawTask(task, track.id);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function validateRawTask(task: RawTaskConfig, trackId: string): void {
|
|
40
|
+
if (!task.id) throw new Error(`track "${trackId}": task.id is required`);
|
|
41
|
+
if (task.use) return; // template usage, validated later
|
|
42
|
+
|
|
43
|
+
const hasPrompt = typeof task.prompt === 'string' && task.prompt.length > 0;
|
|
44
|
+
const hasCommand = typeof task.command === 'string' && task.command.length > 0;
|
|
45
|
+
if (!hasPrompt && !hasCommand) {
|
|
46
|
+
throw new Error(`task "${task.id}": must have either "prompt" or "command"`);
|
|
47
|
+
}
|
|
48
|
+
if (hasPrompt && hasCommand) {
|
|
49
|
+
throw new Error(`task "${task.id}": cannot have both "prompt" and "command"`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ═══ Template Expansion ═══
|
|
54
|
+
|
|
55
|
+
export async function expandTemplates(
|
|
56
|
+
tasks: readonly RawTaskConfig[],
|
|
57
|
+
instancePrefix: string,
|
|
58
|
+
): Promise<RawTaskConfig[]> {
|
|
59
|
+
const result: RawTaskConfig[] = [];
|
|
60
|
+
|
|
61
|
+
for (const task of tasks) {
|
|
62
|
+
if (!task.use) {
|
|
63
|
+
result.push(task);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const template = await loadTemplate(task.use);
|
|
68
|
+
const params = resolveTemplateParams(template, task.with ?? {}, task.id);
|
|
69
|
+
const expanded = expandTemplateTask(template, params, task.id, instancePrefix);
|
|
70
|
+
result.push(...expanded);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function loadTemplate(ref: string): Promise<TemplateConfig> {
|
|
77
|
+
// Strip version suffix for import
|
|
78
|
+
const moduleName = ref.replace(/@v\d+$/, '');
|
|
79
|
+
try {
|
|
80
|
+
const mod = await import(moduleName);
|
|
81
|
+
// Expect the module to export a template.yaml content or parsed object
|
|
82
|
+
if (mod.template) return mod.template as TemplateConfig;
|
|
83
|
+
|
|
84
|
+
// Try loading template.yaml from the package
|
|
85
|
+
const pkgPath = require.resolve(`${moduleName}/template.yaml`);
|
|
86
|
+
const content = await Bun.file(pkgPath).text();
|
|
87
|
+
const doc = yaml.load(content) as { template: TemplateConfig };
|
|
88
|
+
return doc.template;
|
|
89
|
+
} catch {
|
|
90
|
+
throw new Error(`Failed to load template: "${ref}". Is the package installed?`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveTemplateParams(
|
|
95
|
+
template: TemplateConfig,
|
|
96
|
+
provided: Record<string, unknown>,
|
|
97
|
+
instanceId: string,
|
|
98
|
+
): Record<string, unknown> {
|
|
99
|
+
const params: Record<string, unknown> = {};
|
|
100
|
+
const defs = template.params ?? {};
|
|
101
|
+
|
|
102
|
+
for (const [key, def] of Object.entries(defs)) {
|
|
103
|
+
const value = provided[key] ?? def.default;
|
|
104
|
+
if (value === undefined) {
|
|
105
|
+
throw new Error(`Template "${template.name}" instance "${instanceId}": missing required param "${key}"`);
|
|
106
|
+
}
|
|
107
|
+
validateParamType(key, value, def, template.name, instanceId);
|
|
108
|
+
params[key] = value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Warn about unknown params
|
|
112
|
+
for (const key of Object.keys(provided)) {
|
|
113
|
+
if (!(key in defs)) {
|
|
114
|
+
console.warn(`Template "${template.name}" instance "${instanceId}": unknown param "${key}"`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return params;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function validateParamType(
|
|
122
|
+
key: string, value: unknown, def: TemplateParamDef,
|
|
123
|
+
templateName: string, instanceId: string,
|
|
124
|
+
): void {
|
|
125
|
+
const ctx = `Template "${templateName}" instance "${instanceId}" param "${key}"`;
|
|
126
|
+
const ptype = def.type ?? 'string';
|
|
127
|
+
|
|
128
|
+
switch (ptype) {
|
|
129
|
+
case 'string':
|
|
130
|
+
if (typeof value !== 'string') throw new Error(`${ctx}: expected string, got ${typeof value}`);
|
|
131
|
+
break;
|
|
132
|
+
case 'path':
|
|
133
|
+
if (typeof value !== 'string') throw new Error(`${ctx}: expected path string, got ${typeof value}`);
|
|
134
|
+
validatePathParam(value);
|
|
135
|
+
break;
|
|
136
|
+
case 'enum':
|
|
137
|
+
if (!def.enum?.includes(value as string)) {
|
|
138
|
+
throw new Error(`${ctx}: value "${value}" not in allowed values [${def.enum?.join(', ')}]`);
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
case 'number':
|
|
142
|
+
if (typeof value !== 'number') throw new Error(`${ctx}: expected number, got ${typeof value}`);
|
|
143
|
+
if (def.min !== undefined && value < def.min) throw new Error(`${ctx}: ${value} < min ${def.min}`);
|
|
144
|
+
if (def.max !== undefined && value > def.max) throw new Error(`${ctx}: ${value} > max ${def.max}`);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function expandTemplateTask(
|
|
150
|
+
template: TemplateConfig,
|
|
151
|
+
params: Record<string, unknown>,
|
|
152
|
+
instanceId: string,
|
|
153
|
+
instancePrefix: string,
|
|
154
|
+
): RawTaskConfig[] {
|
|
155
|
+
return template.tasks.map(task => {
|
|
156
|
+
const prefixedId = `${instanceId}.${task.id}`;
|
|
157
|
+
|
|
158
|
+
// Replace ${{ params.xxx }} in string fields
|
|
159
|
+
const interpolate = (s: string): string =>
|
|
160
|
+
s.replace(/\$\{\{\s*params\.(\w+)\s*\}\}/g, (_, key) => String(params[key] ?? ''));
|
|
161
|
+
|
|
162
|
+
const newTask: Record<string, unknown> = { ...task, id: prefixedId };
|
|
163
|
+
|
|
164
|
+
// Interpolate string fields
|
|
165
|
+
if (task.prompt) newTask.prompt = interpolate(task.prompt);
|
|
166
|
+
if (task.command) newTask.command = interpolate(task.command);
|
|
167
|
+
|
|
168
|
+
// Namespace depends_on
|
|
169
|
+
if (task.depends_on) {
|
|
170
|
+
newTask.depends_on = task.depends_on.map(dep => `${instanceId}.${dep}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Namespace continue_from
|
|
174
|
+
if (task.continue_from) {
|
|
175
|
+
newTask.continue_from = `${instanceId}.${task.continue_from}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Rewrite output path to instance namespace
|
|
179
|
+
if (task.output) {
|
|
180
|
+
const original = interpolate(task.output);
|
|
181
|
+
newTask.output = original.replace('./tmp/', `./tmp/${instanceId}/`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return newTask as unknown as RawTaskConfig;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ═══ Config Inheritance Resolution ═══
|
|
189
|
+
|
|
190
|
+
export function resolveConfig(raw: RawPipelineConfig, workDir: string): PipelineConfig {
|
|
191
|
+
const tracks: TrackConfig[] = raw.tracks.map(rawTrack => {
|
|
192
|
+
const trackDriver = rawTrack.driver ?? raw.driver;
|
|
193
|
+
const trackCwd = rawTrack.cwd ? resolve(workDir, rawTrack.cwd) : workDir;
|
|
194
|
+
|
|
195
|
+
const tasks: TaskConfig[] = rawTrack.tasks.map(rawTask => {
|
|
196
|
+
const name = rawTask.name
|
|
197
|
+
?? (rawTask.prompt ? truncateForName(rawTask.prompt) : rawTask.command ?? rawTask.id);
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
id: rawTask.id,
|
|
201
|
+
name,
|
|
202
|
+
prompt: rawTask.prompt,
|
|
203
|
+
command: rawTask.command,
|
|
204
|
+
depends_on: rawTask.depends_on,
|
|
205
|
+
trigger: rawTask.trigger,
|
|
206
|
+
continue_from: rawTask.continue_from,
|
|
207
|
+
output: rawTask.output,
|
|
208
|
+
// Inheritance: Task > Track
|
|
209
|
+
model_tier: rawTask.model_tier ?? rawTrack.model_tier ?? 'medium',
|
|
210
|
+
permissions: rawTask.permissions ?? rawTrack.permissions ?? DEFAULT_PERMISSIONS,
|
|
211
|
+
driver: rawTask.driver ?? trackDriver ?? 'claude-code',
|
|
212
|
+
timeout: rawTask.timeout,
|
|
213
|
+
// Middleware: Task-level overrides Track (including [] to disable)
|
|
214
|
+
middlewares: rawTask.middlewares !== undefined ? rawTask.middlewares : rawTrack.middlewares,
|
|
215
|
+
completion: rawTask.completion,
|
|
216
|
+
agent_profile: rawTask.agent_profile ?? rawTrack.agent_profile,
|
|
217
|
+
cwd: rawTask.cwd ? resolve(workDir, rawTask.cwd) : trackCwd,
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
id: rawTrack.id,
|
|
223
|
+
name: rawTrack.name,
|
|
224
|
+
color: rawTrack.color,
|
|
225
|
+
agent_profile: rawTrack.agent_profile,
|
|
226
|
+
model_tier: rawTrack.model_tier ?? 'medium',
|
|
227
|
+
permissions: rawTrack.permissions ?? DEFAULT_PERMISSIONS,
|
|
228
|
+
driver: trackDriver ?? 'claude-code',
|
|
229
|
+
cwd: trackCwd,
|
|
230
|
+
middlewares: rawTrack.middlewares,
|
|
231
|
+
on_failure: rawTrack.on_failure ?? 'skip_downstream',
|
|
232
|
+
tasks,
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
name: raw.name,
|
|
238
|
+
driver: raw.driver,
|
|
239
|
+
timeout: raw.timeout,
|
|
240
|
+
plugins: raw.plugins,
|
|
241
|
+
hooks: raw.hooks,
|
|
242
|
+
tracks,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ═══ Full Parse Pipeline ═══
|
|
247
|
+
|
|
248
|
+
export async function loadPipeline(yamlContent: string, workDir: string): Promise<PipelineConfig> {
|
|
249
|
+
const raw = parseYaml(yamlContent);
|
|
250
|
+
|
|
251
|
+
// Expand templates in each track
|
|
252
|
+
const expandedTracks: RawTrackConfig[] = [];
|
|
253
|
+
for (const track of raw.tracks) {
|
|
254
|
+
const expandedTasks = await expandTemplates(track.tasks, track.id);
|
|
255
|
+
expandedTracks.push({ ...track, tasks: expandedTasks });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const expandedRaw: RawPipelineConfig = { ...raw, tracks: expandedTracks };
|
|
259
|
+
return resolveConfig(expandedRaw, workDir);
|
|
260
|
+
}
|