@tagma/sdk 0.7.0 → 0.7.3

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.
Files changed (72) hide show
  1. package/README.md +84 -44
  2. package/dist/bootstrap.d.ts +20 -0
  3. package/dist/bootstrap.d.ts.map +1 -1
  4. package/dist/bootstrap.js +21 -11
  5. package/dist/bootstrap.js.map +1 -1
  6. package/dist/core/dataflow.d.ts.map +1 -1
  7. package/dist/core/dataflow.js +45 -9
  8. package/dist/core/dataflow.js.map +1 -1
  9. package/dist/core/run-context.d.ts +3 -0
  10. package/dist/core/run-context.d.ts.map +1 -1
  11. package/dist/core/run-context.js +2 -0
  12. package/dist/core/run-context.js.map +1 -1
  13. package/dist/core/task-executor.d.ts.map +1 -1
  14. package/dist/core/task-executor.js +46 -84
  15. package/dist/core/task-executor.js.map +1 -1
  16. package/dist/engine.d.ts +6 -0
  17. package/dist/engine.d.ts.map +1 -1
  18. package/dist/engine.js +3 -0
  19. package/dist/engine.js.map +1 -1
  20. package/dist/index.d.ts +3 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/plugins.d.ts +2 -2
  25. package/dist/plugins.d.ts.map +1 -1
  26. package/dist/ports.d.ts +4 -0
  27. package/dist/ports.d.ts.map +1 -1
  28. package/dist/ports.js +27 -4
  29. package/dist/ports.js.map +1 -1
  30. package/dist/registry.d.ts +10 -4
  31. package/dist/registry.d.ts.map +1 -1
  32. package/dist/registry.js +64 -25
  33. package/dist/registry.js.map +1 -1
  34. package/dist/runtime.d.ts +9 -0
  35. package/dist/runtime.d.ts.map +1 -0
  36. package/dist/runtime.js +8 -0
  37. package/dist/runtime.js.map +1 -0
  38. package/dist/schema.d.ts.map +1 -1
  39. package/dist/schema.js +1 -7
  40. package/dist/schema.js.map +1 -1
  41. package/dist/tagma.d.ts +11 -1
  42. package/dist/tagma.d.ts.map +1 -1
  43. package/dist/tagma.js +6 -0
  44. package/dist/tagma.js.map +1 -1
  45. package/dist/validate-raw.d.ts +4 -4
  46. package/dist/validate-raw.d.ts.map +1 -1
  47. package/dist/validate-raw.js +89 -230
  48. package/dist/validate-raw.js.map +1 -1
  49. package/package.json +2 -2
  50. package/src/bootstrap.ts +23 -14
  51. package/src/core/dataflow.test.ts +8 -9
  52. package/src/core/dataflow.ts +57 -14
  53. package/src/core/run-context.test.ts +12 -0
  54. package/src/core/run-context.ts +4 -0
  55. package/src/core/task-executor.ts +75 -135
  56. package/src/engine-ports-mixed.test.ts +68 -411
  57. package/src/engine-ports.test.ts +37 -341
  58. package/src/engine.ts +8 -0
  59. package/src/index.ts +5 -0
  60. package/src/pipeline-runner.test.ts +5 -9
  61. package/src/plugin-registry.test.ts +138 -1
  62. package/src/plugins.ts +5 -2
  63. package/src/ports.test.ts +80 -0
  64. package/src/ports.ts +36 -4
  65. package/src/registry.ts +81 -26
  66. package/src/runtime.ts +20 -0
  67. package/src/schema-ports.test.ts +47 -197
  68. package/src/schema.ts +1 -7
  69. package/src/tagma.test.ts +72 -1
  70. package/src/tagma.ts +16 -1
  71. package/src/validate-raw-ports.test.ts +80 -393
  72. package/src/validate-raw.ts +90 -250
@@ -1,19 +1,12 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
2
  import yaml from 'js-yaml';
3
3
  import type { PipelineConfig, RawPipelineConfig } from './types';
4
- import {
5
- deresolvePipeline,
6
- parseYaml,
7
- resolveConfig,
8
- serializePipeline,
9
- } from './schema';
4
+ import { deresolvePipeline, parseYaml, resolveConfig, serializePipeline } from './schema';
10
5
 
11
6
  const WORK_DIR = process.platform === 'win32' ? 'D:\\fake-work' : '/fake-work';
12
7
 
13
- // ─── resolveConfig preserves ports ───────────────────────────────────
14
-
15
- describe('resolveConfig — ports passthrough', () => {
16
- test('raw lightweight bindings survive onto the resolved task', () => {
8
+ describe('schema unified bindings passthrough', () => {
9
+ test('typed inputs and outputs survive onto the resolved task', () => {
17
10
  const raw: RawPipelineConfig = {
18
11
  name: 'p',
19
12
  tracks: [
@@ -24,94 +17,19 @@ describe('resolveConfig — ports passthrough', () => {
24
17
  {
25
18
  id: 'a',
26
19
  command: 'echo "{{inputs.city}}"',
27
- inputs: {
28
- city: { from: 't.plan.outputs.city', required: true },
29
- },
30
- outputs: {
31
- report: { from: 'json.reportPath' },
32
- },
20
+ inputs: { city: { from: 't.plan.outputs.city', type: 'string', required: true } },
21
+ outputs: { report: { from: 'json.reportPath', type: 'string' } },
33
22
  },
34
23
  ],
35
24
  },
36
25
  ],
37
26
  };
38
- const resolved = resolveConfig(raw, WORK_DIR);
39
- const task = resolved.tracks[0]!.tasks[0]!;
27
+ const task = resolveConfig(raw, WORK_DIR).tracks[0]!.tasks[0]!;
40
28
  expect(task.inputs).toEqual(raw.tracks[0]!.tasks[0]!.inputs!);
41
29
  expect(task.outputs).toEqual(raw.tracks[0]!.tasks[0]!.outputs!);
42
30
  });
43
31
 
44
- test('raw ports survive onto the resolved task', () => {
45
- const raw: RawPipelineConfig = {
46
- name: 'p',
47
- tracks: [
48
- {
49
- id: 't',
50
- name: 'T',
51
- tasks: [
52
- {
53
- id: 'a',
54
- prompt: 'do it',
55
- ports: {
56
- inputs: [{ name: 'city', type: 'string', required: true }],
57
- outputs: [{ name: 'temp', type: 'number', description: 'Celsius' }],
58
- },
59
- },
60
- ],
61
- },
62
- ],
63
- };
64
- const resolved = resolveConfig(raw, WORK_DIR);
65
- const task = resolved.tracks[0]!.tasks[0]!;
66
- expect(task.ports).toBeDefined();
67
- expect(task.ports!.inputs).toEqual([
68
- { name: 'city', type: 'string', required: true },
69
- ]);
70
- expect(task.ports!.outputs).toEqual([
71
- { name: 'temp', type: 'number', description: 'Celsius' },
72
- ]);
73
- });
74
-
75
- test('tasks without ports still resolve with ports === undefined', () => {
76
- const raw: RawPipelineConfig = {
77
- name: 'p',
78
- tracks: [
79
- { id: 't', name: 'T', tasks: [{ id: 'a', prompt: 'do it' }] },
80
- ],
81
- };
82
- const resolved = resolveConfig(raw, WORK_DIR);
83
- expect(resolved.tracks[0]!.tasks[0]!.ports).toBeUndefined();
84
- });
85
-
86
- test('ports is not inherited from track or pipeline', () => {
87
- // Ports describe a per-task I/O contract. If we accidentally pulled
88
- // them from track defaults, two tasks in the same track would share
89
- // input ports and downstream data-flow would be ambiguous. Test that
90
- // a track with an unrelated `middlewares` default doesn't spread
91
- // anywhere unexpected — purely a regression guard for the no-inherit
92
- // invariant.
93
- const raw: RawPipelineConfig = {
94
- name: 'p',
95
- tracks: [
96
- {
97
- id: 't',
98
- name: 'T',
99
- middlewares: [{ type: 'static_context', file: './x' }],
100
- tasks: [{ id: 'a', prompt: 'x' }, { id: 'b', prompt: 'y' }],
101
- },
102
- ],
103
- };
104
- const resolved = resolveConfig(raw, WORK_DIR);
105
- for (const task of resolved.tracks[0]!.tasks) {
106
- expect(task.ports).toBeUndefined();
107
- }
108
- });
109
- });
110
-
111
- // ─── deresolvePipeline preserves ports ───────────────────────────────
112
-
113
- describe('deresolvePipeline — ports round-trip', () => {
114
- test('lightweight bindings round-trip', () => {
32
+ test('typed inputs and outputs round-trip through deresolve', () => {
115
33
  const raw: RawPipelineConfig = {
116
34
  name: 'p',
117
35
  tracks: [
@@ -123,25 +41,26 @@ describe('deresolvePipeline — ports round-trip', () => {
123
41
  id: 'a',
124
42
  command: 'echo "{{inputs.city}}"',
125
43
  inputs: {
126
- city: { from: 't.plan.outputs.city', required: true },
127
- mode: { default: 'quick' },
128
- },
129
- outputs: {
130
- raw: { from: 'stdout' },
44
+ city: {
45
+ from: 't.plan.outputs.city',
46
+ type: 'enum',
47
+ enum: ['Shanghai', 'Paris'],
48
+ required: true,
49
+ },
131
50
  },
51
+ outputs: { raw: { from: 'stdout' } },
132
52
  },
133
53
  ],
134
54
  },
135
55
  ],
136
56
  };
137
- const resolved = resolveConfig(raw, WORK_DIR);
138
- const back = deresolvePipeline(resolved, WORK_DIR);
57
+ const back = deresolvePipeline(resolveConfig(raw, WORK_DIR), WORK_DIR);
139
58
  expect(back.tracks[0]!.tasks[0]!.inputs).toEqual(raw.tracks[0]!.tasks[0]!.inputs!);
140
59
  expect(back.tracks[0]!.tasks[0]!.outputs).toEqual(raw.tracks[0]!.tasks[0]!.outputs!);
141
60
  });
142
61
 
143
- test('ports with both inputs and outputs round-trip', () => {
144
- const raw: RawPipelineConfig = {
62
+ test('empty binding maps are dropped on deresolve', () => {
63
+ const resolved: PipelineConfig = {
145
64
  name: 'p',
146
65
  tracks: [
147
66
  {
@@ -150,22 +69,21 @@ describe('deresolvePipeline — ports round-trip', () => {
150
69
  tasks: [
151
70
  {
152
71
  id: 'a',
72
+ name: 'a',
153
73
  prompt: 'hi',
154
- ports: {
155
- inputs: [{ name: 'city', type: 'string', required: true }],
156
- outputs: [{ name: 'temp', type: 'number' }],
157
- },
74
+ inputs: {},
75
+ outputs: {},
158
76
  },
159
77
  ],
160
78
  },
161
79
  ],
162
80
  };
163
- const resolved = resolveConfig(raw, WORK_DIR);
164
81
  const back = deresolvePipeline(resolved, WORK_DIR);
165
- expect(back.tracks[0]!.tasks[0]!.ports).toEqual(raw.tracks[0]!.tasks[0]!.ports!);
82
+ expect(back.tracks[0]!.tasks[0]!.inputs).toBeUndefined();
83
+ expect(back.tracks[0]!.tasks[0]!.outputs).toBeUndefined();
166
84
  });
167
85
 
168
- test('ports with only outputs round-trip', () => {
86
+ test('legacy ports are not carried through resolve or deresolve', () => {
169
87
  const raw: RawPipelineConfig = {
170
88
  name: 'p',
171
89
  tracks: [
@@ -175,51 +93,20 @@ describe('deresolvePipeline — ports round-trip', () => {
175
93
  tasks: [
176
94
  {
177
95
  id: 'a',
178
- command: 'echo hi',
179
- ports: { outputs: [{ name: 'x', type: 'string' }] },
96
+ command: 'echo ok',
97
+ ports: { outputs: [{ name: 'old', type: 'string' }] },
180
98
  },
181
99
  ],
182
100
  },
183
101
  ],
184
102
  };
185
103
  const resolved = resolveConfig(raw, WORK_DIR);
186
- const back = deresolvePipeline(resolved, WORK_DIR);
187
- expect(back.tracks[0]!.tasks[0]!.ports).toEqual({
188
- outputs: [{ name: 'x', type: 'string' }],
189
- });
190
- });
191
-
192
- test('empty ports ({}) is dropped on deresolve', () => {
193
- // YAML round-trip prefers field absence over `ports: {}` so a task
194
- // that once declared a port but had it cleared in the editor
195
- // doesn't persist a useless empty object in the file.
196
- const resolved: PipelineConfig = {
197
- name: 'p',
198
- tracks: [
199
- {
200
- id: 't',
201
- name: 'T',
202
- driver: 'opencode',
203
- permissions: { read: true, write: false, execute: false },
204
- on_failure: 'skip_downstream',
205
- tasks: [
206
- {
207
- id: 'a',
208
- name: 'a',
209
- prompt: 'hi',
210
- permissions: { read: true, write: false, execute: false },
211
- driver: 'opencode',
212
- ports: {},
213
- },
214
- ],
215
- },
216
- ],
217
- };
104
+ expect(resolved.tracks[0]!.tasks[0]!.ports).toBeUndefined();
218
105
  const back = deresolvePipeline(resolved, WORK_DIR);
219
106
  expect(back.tracks[0]!.tasks[0]!.ports).toBeUndefined();
220
107
  });
221
108
 
222
- test('YAML round-trip via serializePipeline preserves the full ports shape', () => {
109
+ test('YAML round-trip preserves typed unified binding shape', () => {
223
110
  const raw: RawPipelineConfig = {
224
111
  name: 'p',
225
112
  tracks: [
@@ -230,18 +117,13 @@ describe('deresolvePipeline — ports round-trip', () => {
230
117
  {
231
118
  id: 'classify',
232
119
  prompt: 'pick a bucket',
233
- ports: {
234
- inputs: [
235
- { name: 'doc', type: 'string', required: true, description: 'Full text' },
236
- ],
237
- outputs: [
238
- {
239
- name: 'bucket',
240
- type: 'enum',
241
- enum: ['spam', 'ham'],
242
- description: 'Classification',
243
- },
244
- ],
120
+ inputs: { doc: { type: 'string', required: true, description: 'Full text' } },
121
+ outputs: {
122
+ bucket: {
123
+ type: 'enum',
124
+ enum: ['spam', 'ham'],
125
+ description: 'Classification',
126
+ },
245
127
  },
246
128
  },
247
129
  ],
@@ -250,14 +132,11 @@ describe('deresolvePipeline — ports round-trip', () => {
250
132
  };
251
133
  const yamlText = serializePipeline(raw);
252
134
  const parsed = (yaml.load(yamlText) as { pipeline: RawPipelineConfig }).pipeline;
253
- expect(parsed.tracks[0]!.tasks[0]!.ports).toEqual(raw.tracks[0]!.tasks[0]!.ports!);
135
+ expect(parsed.tracks[0]!.tasks[0]!.inputs).toEqual(raw.tracks[0]!.tasks[0]!.inputs!);
136
+ expect(parsed.tracks[0]!.tasks[0]!.outputs).toEqual(raw.tracks[0]!.tasks[0]!.outputs!);
254
137
  });
255
- });
256
-
257
- // ─── parseYaml accepts ports ─────────────────────────────────────────
258
138
 
259
- describe('parseYaml accepts ports declarations', () => {
260
- test('real-world YAML with lightweight bindings parses cleanly', () => {
139
+ test('real-world YAML with typed bindings parses cleanly', () => {
261
140
  const text = `pipeline:
262
141
  name: demo
263
142
  tracks:
@@ -267,56 +146,27 @@ describe('parseYaml — accepts ports declarations', () => {
267
146
  - id: build
268
147
  command: bun run build
269
148
  outputs:
270
- bundlePath: { from: json.bundlePath }
149
+ bundlePath:
150
+ from: json.bundlePath
151
+ type: string
271
152
  - id: test
272
153
  depends_on: [build]
273
154
  command: 'bun test "{{inputs.bundlePath}}"'
274
155
  inputs:
275
156
  bundlePath:
276
157
  from: t.build.outputs.bundlePath
158
+ type: string
277
159
  required: true
278
160
  `;
279
161
  const config = parseYaml(text);
280
- const build = config.tracks[0]!.tasks[0]!;
281
- const testTask = config.tracks[0]!.tasks[1]!;
282
- expect(build.outputs!.bundlePath).toEqual({ from: 'json.bundlePath' });
283
- expect(testTask.inputs!.bundlePath).toEqual({
162
+ expect(config.tracks[0]!.tasks[0]!.outputs!.bundlePath).toEqual({
163
+ from: 'json.bundlePath',
164
+ type: 'string',
165
+ });
166
+ expect(config.tracks[0]!.tasks[1]!.inputs!.bundlePath).toEqual({
284
167
  from: 't.build.outputs.bundlePath',
168
+ type: 'string',
285
169
  required: true,
286
170
  });
287
171
  });
288
-
289
- test('real-world YAML with ports parses cleanly', () => {
290
- const text = `pipeline:
291
- name: demo
292
- tracks:
293
- - id: t
294
- name: Main
295
- tasks:
296
- - id: plan
297
- prompt: Pick a city and id
298
- ports:
299
- outputs:
300
- - name: city
301
- type: string
302
- description: Target city
303
- - name: id
304
- type: number
305
- - id: fetch
306
- depends_on: [plan]
307
- command: 'weather.sh --city "{{inputs.city}}" --id {{inputs.id}}'
308
- ports:
309
- inputs:
310
- - { name: city, type: string, required: true }
311
- - { name: id, type: number, required: true }
312
- outputs:
313
- - { name: temp, type: number }
314
- `;
315
- const config = parseYaml(text);
316
- const plan = config.tracks[0]!.tasks[0]!;
317
- const fetch = config.tracks[0]!.tasks[1]!;
318
- expect(plan.ports!.outputs!.map((p) => p.name)).toEqual(['city', 'id']);
319
- expect(fetch.ports!.inputs!.map((p) => p.name)).toEqual(['city', 'id']);
320
- expect(fetch.ports!.outputs!.map((p) => p.name)).toEqual(['temp']);
321
- });
322
172
  });
package/src/schema.ts CHANGED
@@ -161,11 +161,10 @@ export function resolveConfig(raw: RawPipelineConfig, workDir: string): Pipeline
161
161
  completion: rawTask.completion,
162
162
  agent_profile: rawTask.agent_profile ?? rawTrack.agent_profile,
163
163
  cwd: rawTask.cwd ? validatePath(rawTask.cwd, workDir) : trackCwd,
164
- // Lightweight bindings and ports: no inheritance they describe
164
+ // Unified bindings have no inheritance; they describe
165
165
  // per-task data flow, not cross-task defaults.
166
166
  inputs: rawTask.inputs,
167
167
  outputs: rawTask.outputs,
168
- ports: rawTask.ports,
169
168
  };
170
169
  });
171
170
 
@@ -313,11 +312,6 @@ export function deresolvePipeline(config: PipelineConfig, workDir: string): RawP
313
312
  : {}),
314
313
  ...(task.inputs && Object.keys(task.inputs).length > 0 ? { inputs: task.inputs } : {}),
315
314
  ...(task.outputs && Object.keys(task.outputs).length > 0 ? { outputs: task.outputs } : {}),
316
- ...(task.ports &&
317
- ((task.ports.inputs && task.ports.inputs.length > 0) ||
318
- (task.ports.outputs && task.ports.outputs.length > 0))
319
- ? { ports: task.ports }
320
- : {}),
321
315
  };
322
316
  });
323
317
 
package/src/tagma.test.ts CHANGED
@@ -3,7 +3,8 @@ import { mkdtempSync, rmSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { createTagma } from './tagma';
6
- import type { DriverPlugin, PipelineConfig } from './types';
6
+ import type { DriverPlugin, TagmaPlugin, TaskResult } from './types';
7
+ import type { TagmaRuntime } from './runtime';
7
8
 
8
9
  function makeDir(prefix: string): string {
9
10
  return mkdtempSync(join(tmpdir(), prefix));
@@ -21,6 +22,76 @@ function makeDriver(name: string, marker: string[]): DriverPlugin {
21
22
  }
22
23
 
23
24
  describe('createTagma', () => {
25
+ test('runs command tasks through the configured runtime', async () => {
26
+ const calls: string[] = [];
27
+ const taskResult: TaskResult = {
28
+ exitCode: 0,
29
+ stdout: 'runtime-ok',
30
+ stderr: '',
31
+ stdoutPath: null,
32
+ stderrPath: null,
33
+ stdoutBytes: 10,
34
+ stderrBytes: 0,
35
+ durationMs: 1,
36
+ sessionId: null,
37
+ normalizedOutput: null,
38
+ failureKind: null,
39
+ };
40
+ const runtime: TagmaRuntime = {
41
+ async runCommand(command, cwd) {
42
+ calls.push(`${cwd}:${command}`);
43
+ return taskResult;
44
+ },
45
+ async runSpawn() {
46
+ throw new Error('runSpawn should not be called for command tasks');
47
+ },
48
+ };
49
+ const tagma = createTagma({ builtins: false, runtime });
50
+ const dir = makeDir('tagma-runtime-run-');
51
+ try {
52
+ const result = await tagma.run(
53
+ {
54
+ name: 'runtime-run',
55
+ tracks: [
56
+ {
57
+ id: 't',
58
+ name: 'T',
59
+ tasks: [{ id: 'cmd', name: 'cmd', command: 'fake-only-command' }],
60
+ },
61
+ ],
62
+ },
63
+ {
64
+ cwd: dir,
65
+ skipPluginLoading: true,
66
+ },
67
+ );
68
+
69
+ expect(result.success).toBe(true);
70
+ expect(calls).toEqual([`${dir}:fake-only-command`]);
71
+ expect(result.states.get('t.cmd')?.result?.stdout).toBe('runtime-ok');
72
+ } finally {
73
+ rmSync(dir, { recursive: true, force: true });
74
+ }
75
+ });
76
+
77
+ test('registers capability plugins passed to options', () => {
78
+ const seen: string[] = [];
79
+ const driver = makeDriver('driver-plugin', seen);
80
+ const plugin: TagmaPlugin = {
81
+ name: 'tagma-plugin-local',
82
+ capabilities: {
83
+ drivers: {
84
+ mock: driver,
85
+ },
86
+ },
87
+ };
88
+
89
+ const tagma = createTagma({ builtins: false, plugins: [plugin] });
90
+
91
+ expect(tagma.registry.getHandler<DriverPlugin>('drivers', 'mock')).toBe(driver);
92
+ expect(seen).toEqual([]);
93
+ });
94
+
24
95
  test('instances own isolated plugin registries', () => {
25
96
  const seenA: string[] = [];
26
97
  const seenB: string[] = [];
package/src/tagma.ts CHANGED
@@ -2,7 +2,8 @@ import { runPipeline, type EngineResult, type RunPipelineOptions } from './engin
2
2
  import { bootstrapBuiltins } from './bootstrap';
3
3
  import { PluginRegistry } from './registry';
4
4
  import { validateConfig } from './schema';
5
- import type { PipelineConfig } from './types';
5
+ import { bunRuntime, type TagmaRuntime } from './runtime';
6
+ import type { PipelineConfig, TagmaPlugin } from './types';
6
7
 
7
8
  export interface CreateTagmaOptions {
8
9
  /**
@@ -14,6 +15,15 @@ export interface CreateTagmaOptions {
14
15
  * instance registry. Defaults to true.
15
16
  */
16
17
  readonly builtins?: boolean;
18
+ /**
19
+ * Package-level capability plugins to register into this SDK instance.
20
+ */
21
+ readonly plugins?: readonly TagmaPlugin[];
22
+ /**
23
+ * Runtime implementation used for command and driver process execution.
24
+ * Defaults to the SDK's Bun runtime.
25
+ */
26
+ readonly runtime?: TagmaRuntime;
17
27
  }
18
28
 
19
29
  export interface TagmaRunOptions extends Omit<RunPipelineOptions, 'registry'> {
@@ -28,9 +38,13 @@ export interface Tagma {
28
38
 
29
39
  export function createTagma(options: CreateTagmaOptions = {}): Tagma {
30
40
  const registry = options.registry ?? new PluginRegistry();
41
+ const runtime = options.runtime ?? bunRuntime();
31
42
  if (options.builtins !== false) {
32
43
  bootstrapBuiltins(registry);
33
44
  }
45
+ for (const plugin of options.plugins ?? []) {
46
+ registry.registerTagmaPlugin(plugin);
47
+ }
34
48
 
35
49
  return {
36
50
  registry,
@@ -38,6 +52,7 @@ export function createTagma(options: CreateTagmaOptions = {}): Tagma {
38
52
  return runPipeline(config, cwd, {
39
53
  ...runOptions,
40
54
  registry,
55
+ runtime,
41
56
  });
42
57
  },
43
58
  validate(config) {