@tagma/sdk 0.7.4 → 0.7.6

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 (191) hide show
  1. package/README.md +60 -53
  2. package/dist/completions/file-exists.js +1 -1
  3. package/dist/completions/file-exists.js.map +1 -1
  4. package/dist/completions/output-check.d.ts.map +1 -1
  5. package/dist/completions/output-check.js +17 -4
  6. package/dist/completions/output-check.js.map +1 -1
  7. package/dist/config.d.ts +4 -4
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +2 -2
  10. package/dist/config.js.map +1 -1
  11. package/dist/dataflow.d.ts +3 -0
  12. package/dist/dataflow.d.ts.map +1 -0
  13. package/dist/dataflow.js +2 -0
  14. package/dist/dataflow.js.map +1 -0
  15. package/dist/drivers/opencode.d.ts.map +1 -1
  16. package/dist/drivers/opencode.js +23 -71
  17. package/dist/drivers/opencode.js.map +1 -1
  18. package/dist/middlewares/static-context.d.ts.map +1 -1
  19. package/dist/middlewares/static-context.js +1 -2
  20. package/dist/middlewares/static-context.js.map +1 -1
  21. package/dist/pipeline-runner.d.ts.map +1 -1
  22. package/dist/pipeline-runner.js +2 -2
  23. package/dist/pipeline-runner.js.map +1 -1
  24. package/dist/schema.d.ts.map +1 -1
  25. package/dist/schema.js +3 -4
  26. package/dist/schema.js.map +1 -1
  27. package/dist/triggers/file.d.ts.map +1 -1
  28. package/dist/triggers/file.js +1 -2
  29. package/dist/triggers/file.js.map +1 -1
  30. package/dist/triggers/manual.d.ts.map +1 -1
  31. package/dist/triggers/manual.js +1 -2
  32. package/dist/triggers/manual.js.map +1 -1
  33. package/dist/types.d.ts +1 -2
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/types.js +1 -12
  36. package/dist/types.js.map +1 -1
  37. package/dist/utils-api.d.ts +1 -1
  38. package/dist/utils-api.d.ts.map +1 -1
  39. package/dist/utils-api.js +1 -1
  40. package/dist/utils-api.js.map +1 -1
  41. package/dist/validate-raw.d.ts +4 -4
  42. package/dist/validate-raw.d.ts.map +1 -1
  43. package/dist/validate-raw.js +45 -52
  44. package/dist/validate-raw.js.map +1 -1
  45. package/package.json +11 -24
  46. package/dist/adapters/stdin-approval.d.ts +0 -2
  47. package/dist/adapters/stdin-approval.d.ts.map +0 -1
  48. package/dist/adapters/stdin-approval.js +0 -2
  49. package/dist/adapters/stdin-approval.js.map +0 -1
  50. package/dist/adapters/websocket-approval.d.ts +0 -2
  51. package/dist/adapters/websocket-approval.d.ts.map +0 -1
  52. package/dist/adapters/websocket-approval.js +0 -2
  53. package/dist/adapters/websocket-approval.js.map +0 -1
  54. package/dist/core/dataflow.d.ts +0 -23
  55. package/dist/core/dataflow.d.ts.map +0 -1
  56. package/dist/core/dataflow.js +0 -99
  57. package/dist/core/dataflow.js.map +0 -1
  58. package/dist/core/log-prune.d.ts +0 -16
  59. package/dist/core/log-prune.d.ts.map +0 -1
  60. package/dist/core/log-prune.js +0 -34
  61. package/dist/core/log-prune.js.map +0 -1
  62. package/dist/core/preflight.d.ts +0 -13
  63. package/dist/core/preflight.d.ts.map +0 -1
  64. package/dist/core/preflight.js +0 -61
  65. package/dist/core/preflight.js.map +0 -1
  66. package/dist/core/run-context.d.ts +0 -55
  67. package/dist/core/run-context.d.ts.map +0 -1
  68. package/dist/core/run-context.js +0 -158
  69. package/dist/core/run-context.js.map +0 -1
  70. package/dist/core/run-state.d.ts +0 -25
  71. package/dist/core/run-state.d.ts.map +0 -1
  72. package/dist/core/run-state.js +0 -93
  73. package/dist/core/run-state.js.map +0 -1
  74. package/dist/core/scheduler.d.ts +0 -13
  75. package/dist/core/scheduler.d.ts.map +0 -1
  76. package/dist/core/scheduler.js +0 -35
  77. package/dist/core/scheduler.js.map +0 -1
  78. package/dist/core/task-executor.d.ts +0 -13
  79. package/dist/core/task-executor.d.ts.map +0 -1
  80. package/dist/core/task-executor.js +0 -610
  81. package/dist/core/task-executor.js.map +0 -1
  82. package/dist/core/trigger-errors.d.ts +0 -9
  83. package/dist/core/trigger-errors.d.ts.map +0 -1
  84. package/dist/core/trigger-errors.js +0 -15
  85. package/dist/core/trigger-errors.js.map +0 -1
  86. package/dist/dag.d.ts +0 -45
  87. package/dist/dag.d.ts.map +0 -1
  88. package/dist/dag.js +0 -177
  89. package/dist/dag.js.map +0 -1
  90. package/dist/hooks.d.ts +0 -73
  91. package/dist/hooks.d.ts.map +0 -1
  92. package/dist/hooks.js +0 -106
  93. package/dist/hooks.js.map +0 -1
  94. package/dist/pipeline-definition.d.ts +0 -3
  95. package/dist/pipeline-definition.d.ts.map +0 -1
  96. package/dist/pipeline-definition.js +0 -4
  97. package/dist/pipeline-definition.js.map +0 -1
  98. package/dist/ports.d.ts +0 -196
  99. package/dist/ports.d.ts.map +0 -1
  100. package/dist/ports.js +0 -688
  101. package/dist/ports.js.map +0 -1
  102. package/dist/prompt-doc.d.ts +0 -70
  103. package/dist/prompt-doc.d.ts.map +0 -1
  104. package/dist/prompt-doc.js +0 -154
  105. package/dist/prompt-doc.js.map +0 -1
  106. package/dist/registry.d.ts +0 -3
  107. package/dist/registry.d.ts.map +0 -1
  108. package/dist/registry.js +0 -2
  109. package/dist/registry.js.map +0 -1
  110. package/dist/task-ref.d.ts +0 -55
  111. package/dist/task-ref.d.ts.map +0 -1
  112. package/dist/task-ref.js +0 -103
  113. package/dist/task-ref.js.map +0 -1
  114. package/dist/utils.d.ts +0 -13
  115. package/dist/utils.d.ts.map +0 -1
  116. package/dist/utils.js +0 -177
  117. package/dist/utils.js.map +0 -1
  118. package/src/adapters/stdin-approval.ts +0 -1
  119. package/src/adapters/websocket-approval.ts +0 -1
  120. package/src/approval.ts +0 -9
  121. package/src/bootstrap.ts +0 -55
  122. package/src/completions/exit-code.ts +0 -34
  123. package/src/completions/file-exists.ts +0 -66
  124. package/src/completions/output-check.test.ts +0 -50
  125. package/src/completions/output-check.ts +0 -92
  126. package/src/config-ops.test.ts +0 -70
  127. package/src/config-ops.ts +0 -328
  128. package/src/config.ts +0 -26
  129. package/src/core/dataflow.test.ts +0 -166
  130. package/src/core/dataflow.ts +0 -161
  131. package/src/core/log-prune.test.ts +0 -58
  132. package/src/core/log-prune.ts +0 -43
  133. package/src/core/preflight.test.ts +0 -49
  134. package/src/core/preflight.ts +0 -89
  135. package/src/core/run-context.test.ts +0 -291
  136. package/src/core/run-context.ts +0 -211
  137. package/src/core/run-state.test.ts +0 -98
  138. package/src/core/run-state.ts +0 -122
  139. package/src/core/scheduler.test.ts +0 -83
  140. package/src/core/scheduler.ts +0 -42
  141. package/src/core/task-executor.ts +0 -752
  142. package/src/core/trigger-errors.ts +0 -15
  143. package/src/dag.test.ts +0 -56
  144. package/src/dag.ts +0 -245
  145. package/src/drivers/opencode.ts +0 -410
  146. package/src/engine-ports-mixed.test.ts +0 -182
  147. package/src/engine-ports.test.ts +0 -210
  148. package/src/engine-task-type.test.ts +0 -56
  149. package/src/engine.ts +0 -32
  150. package/src/hooks.ts +0 -193
  151. package/src/index.ts +0 -31
  152. package/src/logger.ts +0 -2
  153. package/src/middlewares/static-context.ts +0 -49
  154. package/src/package-split.test.ts +0 -15
  155. package/src/pipeline-definition.ts +0 -5
  156. package/src/pipeline-runner.test.ts +0 -144
  157. package/src/pipeline-runner.ts +0 -194
  158. package/src/plugin-registry.test.ts +0 -448
  159. package/src/plugins.ts +0 -21
  160. package/src/ports.test.ts +0 -678
  161. package/src/ports.ts +0 -925
  162. package/src/prompt-doc.test.ts +0 -174
  163. package/src/prompt-doc.ts +0 -169
  164. package/src/registry.ts +0 -7
  165. package/src/runner.test.ts +0 -142
  166. package/src/runner.ts +0 -1
  167. package/src/runtime/adapters/stdin-approval.ts +0 -1
  168. package/src/runtime/adapters/websocket-approval.ts +0 -1
  169. package/src/runtime/bun-process-runner.ts +0 -1
  170. package/src/runtime-adapters.test.ts +0 -10
  171. package/src/runtime.ts +0 -12
  172. package/src/schema-ports.test.ts +0 -172
  173. package/src/schema.test.ts +0 -213
  174. package/src/schema.ts +0 -379
  175. package/src/tagma.test.ts +0 -317
  176. package/src/tagma.ts +0 -67
  177. package/src/task-ref.test.ts +0 -401
  178. package/src/task-ref.ts +0 -121
  179. package/src/triggers/file.test.ts +0 -79
  180. package/src/triggers/file.ts +0 -131
  181. package/src/triggers/manual.ts +0 -86
  182. package/src/types.ts +0 -18
  183. package/src/utils-api.ts +0 -8
  184. package/src/utils.test.ts +0 -28
  185. package/src/utils.ts +0 -203
  186. package/src/validate-raw-plugin-types.test.ts +0 -60
  187. package/src/validate-raw-ports.test.ts +0 -136
  188. package/src/validate-raw.ts +0 -852
  189. package/src/yaml-compiler.test.ts +0 -108
  190. package/src/yaml-compiler.ts +0 -110
  191. package/src/yaml.ts +0 -11
package/src/schema.ts DELETED
@@ -1,379 +0,0 @@
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?: unknown };
21
- if (!doc?.pipeline) {
22
- throw new Error('YAML must contain a top-level "pipeline" key');
23
- }
24
- if (typeof doc.pipeline !== 'object' || Array.isArray(doc.pipeline)) {
25
- throw new Error('pipeline must be an object');
26
- }
27
- const p = doc.pipeline as RawPipelineConfig;
28
- if (!p.name) throw new Error('pipeline.name is required');
29
- if (!Array.isArray(p.tracks)) throw new Error('pipeline.tracks must be an array');
30
- if (p.tracks.length === 0) throw new Error('pipeline.tracks must be non-empty');
31
-
32
- // D14: Detect duplicate track IDs before per-track validation so the error
33
- // message is clear ("Duplicate track id") rather than a confusing DAG error
34
- // ("Duplicate task ID: track.task_x") that only surfaces at runPipeline time.
35
- const seenTrackIds = new Set<string>();
36
- for (const track of p.tracks) {
37
- if (track.id) {
38
- if (seenTrackIds.has(track.id)) {
39
- throw new Error(`Duplicate track id "${track.id}": each track must have a unique id.`);
40
- }
41
- seenTrackIds.add(track.id);
42
- }
43
- }
44
-
45
- for (const track of p.tracks) {
46
- validateRawTrack(track);
47
- }
48
- return p;
49
- }
50
-
51
- // D8: IDs must start with a letter or underscore and contain only
52
- // alphanumerics, underscores, and hyphens. Dots are forbidden because
53
- // the engine uses "trackId.taskId" as the qualified separator — a dot in
54
- // either part creates an ambiguous qualified ID and breaks resolveRef.
55
- const ID_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
56
-
57
- function assertValidId(id: string, label: string): void {
58
- if (!ID_RE.test(id)) {
59
- throw new Error(
60
- `${label}: id "${id}" is invalid. IDs must match /^[A-Za-z_][A-Za-z0-9_-]*$/ ` +
61
- `(letters, digits, underscores, hyphens; no dots or spaces; must start with letter/underscore).`,
62
- );
63
- }
64
- }
65
-
66
- function validateRawTrack(track: RawTrackConfig): void {
67
- if (!track || typeof track !== 'object' || Array.isArray(track)) {
68
- throw new Error('track must be an object');
69
- }
70
- if (!track.id) throw new Error('track.id is required');
71
- assertValidId(track.id, `track "${track.id}"`);
72
- if (!track.name) throw new Error(`track "${track.id}": name is required`);
73
- if (!Array.isArray(track.tasks)) {
74
- throw new Error(`track "${track.id}": tasks must be an array`);
75
- }
76
- if (track.tasks.length === 0) {
77
- throw new Error(`track "${track.id}": tasks must be non-empty`);
78
- }
79
- for (const task of track.tasks) {
80
- validateRawTask(task, track.id);
81
- }
82
- }
83
-
84
- function validateRawTask(task: RawTaskConfig, trackId: string): void {
85
- if (!task || typeof task !== 'object' || Array.isArray(task)) {
86
- throw new Error(`track "${trackId}": task must be an object`);
87
- }
88
- if (!task.id) throw new Error(`track "${trackId}": task.id is required`);
89
- assertValidId(task.id, `task "${task.id}" in track "${trackId}"`);
90
-
91
- const hasPromptKey = typeof task.prompt === 'string';
92
- const hasCommandKey = typeof task.command === 'string';
93
- if (!hasPromptKey && !hasCommandKey) {
94
- throw new Error(`task "${task.id}": must have either "prompt" or "command"`);
95
- }
96
- if (hasPromptKey && hasCommandKey) {
97
- throw new Error(`task "${task.id}": cannot have both "prompt" and "command"`);
98
- }
99
- // Empty-content tasks (e.g. `prompt: ''`) are allowed at parse time and
100
- // flagged as hard validation errors by validate-raw.ts.
101
- }
102
-
103
- // ═══ Config Inheritance Resolution ═══
104
-
105
- export function resolveConfig(raw: RawPipelineConfig, workDir: string): PipelineConfig {
106
- // Build qualified ID set for resolving bare continue_from references
107
- const allQualifiedIds = new Set<string>();
108
- for (const t of raw.tracks) {
109
- if (!t.id) continue;
110
- for (const tk of t.tasks ?? []) {
111
- if (tk.id) allQualifiedIds.add(`${t.id}.${tk.id}`);
112
- }
113
- }
114
-
115
- function qualifyContinueFrom(ref: string, trackId: string): string {
116
- // Already qualified
117
- if (allQualifiedIds.has(ref)) return ref;
118
- // Same-track shorthand
119
- const sameTrack = `${trackId}.${ref}`;
120
- if (allQualifiedIds.has(sameTrack)) return sameTrack;
121
- // Cross-track bare lookup — must be unambiguous
122
- let match: string | null = null;
123
- for (const qid of allQualifiedIds) {
124
- if (qid.endsWith(`.${ref}`)) {
125
- if (match !== null) return ref; // ambiguous — leave as-is
126
- match = qid;
127
- }
128
- }
129
- return match ?? ref; // not found — leave as-is (validated elsewhere)
130
- }
131
-
132
- const tracks: TrackConfig[] = raw.tracks.map((rawTrack) => {
133
- const trackDriver = rawTrack.driver ?? raw.driver;
134
- // validatePath enforces no .. traversal and no absolute paths escaping workDir.
135
- const trackCwd = rawTrack.cwd ? validatePath(rawTrack.cwd, workDir) : workDir;
136
-
137
- const tasks: TaskConfig[] = rawTrack.tasks.map((rawTask) => {
138
- const name =
139
- rawTask.name ??
140
- (rawTask.prompt ? truncateForName(rawTask.prompt) : (rawTask.command ?? rawTask.id));
141
-
142
- return {
143
- id: rawTask.id,
144
- name,
145
- prompt: rawTask.prompt,
146
- command: rawTask.command,
147
- depends_on: rawTask.depends_on,
148
- trigger: rawTask.trigger,
149
- continue_from: rawTask.continue_from
150
- ? qualifyContinueFrom(rawTask.continue_from, rawTrack.id)
151
- : undefined,
152
- // Inheritance: Task > Track > Pipeline
153
- model: rawTask.model ?? rawTrack.model ?? raw.model,
154
- reasoning_effort:
155
- rawTask.reasoning_effort ?? rawTrack.reasoning_effort ?? raw.reasoning_effort,
156
- permissions: rawTask.permissions ?? rawTrack.permissions ?? raw.permissions ?? DEFAULT_PERMISSIONS,
157
- driver: rawTask.driver ?? trackDriver ?? 'opencode',
158
- timeout: rawTask.timeout,
159
- // Middleware: Task-level overrides Track (including [] to disable)
160
- middlewares: rawTask.middlewares !== undefined ? rawTask.middlewares : rawTrack.middlewares,
161
- completion: rawTask.completion,
162
- agent_profile: rawTask.agent_profile ?? rawTrack.agent_profile,
163
- cwd: rawTask.cwd ? validatePath(rawTask.cwd, workDir) : trackCwd,
164
- // Unified bindings have no inheritance; they describe
165
- // per-task data flow, not cross-task defaults.
166
- inputs: rawTask.inputs,
167
- outputs: rawTask.outputs,
168
- };
169
- });
170
-
171
- return {
172
- id: rawTrack.id,
173
- name: rawTrack.name,
174
- color: rawTrack.color,
175
- agent_profile: rawTrack.agent_profile,
176
- model: rawTrack.model ?? raw.model,
177
- reasoning_effort: rawTrack.reasoning_effort ?? raw.reasoning_effort,
178
- permissions: rawTrack.permissions ?? raw.permissions ?? DEFAULT_PERMISSIONS,
179
- driver: trackDriver ?? 'opencode',
180
- cwd: trackCwd,
181
- middlewares: rawTrack.middlewares,
182
- on_failure: rawTrack.on_failure ?? 'skip_downstream',
183
- tasks,
184
- };
185
- });
186
-
187
- return {
188
- name: raw.name,
189
- driver: raw.driver,
190
- model: raw.model,
191
- reasoning_effort: raw.reasoning_effort,
192
- permissions: raw.permissions,
193
- timeout: raw.timeout,
194
- plugins: raw.plugins,
195
- hooks: raw.hooks,
196
- tracks,
197
- };
198
- }
199
-
200
- // Field-by-field permissions comparison — avoids relying on JSON.stringify key order.
201
- function permissionsEqual(a: Permissions | undefined, b: Permissions | undefined): boolean {
202
- if (a === b) return true;
203
- if (!a || !b) return false;
204
- return a.read === b.read && a.write === b.write && a.execute === b.execute;
205
- }
206
-
207
- function isDefaultExitCodeCompletion(completion: CompletionConfig | undefined): boolean {
208
- if (!completion || completion.type !== 'exit_code') return false;
209
- const {
210
- type: _type,
211
- expect,
212
- ...rest
213
- } = completion as CompletionConfig & {
214
- expect?: unknown;
215
- };
216
- if (Object.keys(rest).length > 0) return false;
217
- return expect === undefined || expect === 0;
218
- }
219
-
220
- function stripDefaultTaskCompletion<T extends { completion?: CompletionConfig }>(task: T): T {
221
- if (!isDefaultExitCodeCompletion(task.completion)) return task;
222
- const { completion: _completion, ...rest } = task;
223
- return rest as T;
224
- }
225
-
226
- // `continue_from` is a prompt-only field — it tells AI drivers with
227
- // session-resume capability to thread off an upstream prompt task's context.
228
- // A command task runs as a plain shell subprocess and has no session to
229
- // resume, so any `continue_from` on a command task is dead weight. Drop it
230
- // at serialization time so YAML on disk never carries the stale field after
231
- // a user toggles task mode from prompt → command. The tagma-yaml agent's
232
- // system prompt (apps/editor/server/opencode-seed.ts) documents this
233
- // stripping — keep them in sync.
234
- function stripPromptOnlyFieldsFromCommandTask<
235
- T extends { command?: string; continue_from?: string },
236
- >(task: T): T {
237
- if (task.command === undefined || task.continue_from === undefined) return task;
238
- const { continue_from: _cf, ...rest } = task;
239
- return rest as T;
240
- }
241
-
242
- function stripForSerialization<T extends PipelineConfig | RawPipelineConfig>(
243
- config: T,
244
- ): T {
245
- return {
246
- ...config,
247
- tracks: config.tracks.map((track) => ({
248
- ...track,
249
- tasks: track.tasks.map((task) =>
250
- stripPromptOnlyFieldsFromCommandTask(stripDefaultTaskCompletion(task)),
251
- ),
252
- })),
253
- } as T;
254
- }
255
-
256
- // ═══ YAML Serialization ═══
257
-
258
- /**
259
- * Serialize a pipeline config back to YAML string.
260
- * Wraps the config under the top-level `pipeline` key as expected by parseYaml.
261
- */
262
- export function serializePipeline(config: PipelineConfig | RawPipelineConfig): string {
263
- return yaml.dump(
264
- { pipeline: stripForSerialization(config) },
265
- { lineWidth: 120, indent: 2 },
266
- );
267
- }
268
-
269
- /**
270
- * Convert a resolved PipelineConfig back to a RawPipelineConfig for serialization.
271
- * Strips injected defaults and converts absolute cwd paths back to relative so the
272
- * resulting YAML is portable across machines.
273
- *
274
- * Use this when you need to save a config that was previously loaded via
275
- * loadPipeline(). For a pure load→edit→save cycle on raw YAML, prefer
276
- * parseYaml() → edit RawPipelineConfig → serializePipeline().
277
- */
278
- export function deresolvePipeline(config: PipelineConfig, workDir: string): RawPipelineConfig {
279
- const tracks: RawTrackConfig[] = config.tracks.map((track) => {
280
- const trackCwdRel =
281
- track.cwd && track.cwd !== workDir ? relative(workDir, track.cwd) : undefined;
282
- const effectiveTrackDriver = track.driver ?? config.driver ?? 'opencode';
283
- const effectiveTrackModel = track.model ?? config.model;
284
- const effectiveTrackReasoning = track.reasoning_effort ?? config.reasoning_effort;
285
-
286
- const tasks: RawTaskConfig[] = track.tasks.map((task) => {
287
- const taskCwdRel =
288
- task.cwd && task.cwd !== track.cwd ? relative(workDir, task.cwd) : undefined;
289
-
290
- return {
291
- id: task.id,
292
- ...(task.name ? { name: task.name } : {}),
293
- ...(task.prompt !== undefined ? { prompt: task.prompt } : {}),
294
- ...(task.command !== undefined ? { command: task.command } : {}),
295
- ...(task.depends_on?.length ? { depends_on: task.depends_on } : {}),
296
- ...(task.trigger ? { trigger: task.trigger } : {}),
297
- ...(task.continue_from ? { continue_from: task.continue_from } : {}),
298
- ...(taskCwdRel ? { cwd: taskCwdRel } : {}),
299
- ...(task.model && task.model !== effectiveTrackModel ? { model: task.model } : {}),
300
- ...(task.reasoning_effort && task.reasoning_effort !== effectiveTrackReasoning
301
- ? { reasoning_effort: task.reasoning_effort }
302
- : {}),
303
- ...(task.driver && task.driver !== effectiveTrackDriver ? { driver: task.driver } : {}),
304
- ...(task.timeout ? { timeout: task.timeout } : {}),
305
- ...(task.middlewares !== undefined ? { middlewares: task.middlewares } : {}),
306
- ...(task.completion && !isDefaultExitCodeCompletion(task.completion)
307
- ? { completion: task.completion }
308
- : {}),
309
- ...(task.agent_profile ? { agent_profile: task.agent_profile } : {}),
310
- ...(task.permissions && !permissionsEqual(task.permissions, track.permissions)
311
- ? { permissions: task.permissions }
312
- : {}),
313
- ...(task.inputs && Object.keys(task.inputs).length > 0 ? { inputs: task.inputs } : {}),
314
- ...(task.outputs && Object.keys(task.outputs).length > 0 ? { outputs: task.outputs } : {}),
315
- };
316
- });
317
-
318
- return {
319
- id: track.id,
320
- name: track.name,
321
- ...(track.color ? { color: track.color } : {}),
322
- ...(track.agent_profile ? { agent_profile: track.agent_profile } : {}),
323
- ...(track.model && track.model !== config.model ? { model: track.model } : {}),
324
- ...(track.reasoning_effort && track.reasoning_effort !== config.reasoning_effort
325
- ? { reasoning_effort: track.reasoning_effort }
326
- : {}),
327
- ...(track.driver && track.driver !== (config.driver ?? 'opencode')
328
- ? { driver: track.driver }
329
- : {}),
330
- ...(trackCwdRel ? { cwd: trackCwdRel } : {}),
331
- ...(track.middlewares?.length ? { middlewares: track.middlewares } : {}),
332
- ...(track.on_failure && track.on_failure !== 'skip_downstream'
333
- ? { on_failure: track.on_failure }
334
- : {}),
335
- ...(track.permissions && !permissionsEqual(track.permissions, config.permissions ?? DEFAULT_PERMISSIONS)
336
- ? { permissions: track.permissions }
337
- : {}),
338
- tasks,
339
- };
340
- });
341
-
342
- return {
343
- name: config.name,
344
- ...(config.driver ? { driver: config.driver } : {}),
345
- ...(config.model ? { model: config.model } : {}),
346
- ...(config.reasoning_effort ? { reasoning_effort: config.reasoning_effort } : {}),
347
- ...(config.permissions && !permissionsEqual(config.permissions, DEFAULT_PERMISSIONS)
348
- ? { permissions: config.permissions }
349
- : {}),
350
- ...(config.timeout ? { timeout: config.timeout } : {}),
351
- ...(config.plugins?.length ? { plugins: config.plugins } : {}),
352
- ...(config.hooks ? { hooks: config.hooks } : {}),
353
- tracks,
354
- };
355
- }
356
-
357
- // ═══ Offline Validation ═══
358
-
359
- /**
360
- * Validate a pipeline config without executing it.
361
- * Only checks structural/DAG correctness — does not check plugin registration.
362
- * Returns an array of error messages (empty = valid).
363
- */
364
- export function validateConfig(config: PipelineConfig): string[] {
365
- const errors: string[] = [];
366
- try {
367
- buildDag(config);
368
- } catch (err) {
369
- errors.push(err instanceof Error ? err.message : String(err));
370
- }
371
- return errors;
372
- }
373
-
374
- // ═══ Full Parse Pipeline ═══
375
-
376
- export async function loadPipeline(yamlContent: string, workDir: string): Promise<PipelineConfig> {
377
- const raw = parseYaml(yamlContent);
378
- return resolveConfig(raw, workDir);
379
- }
package/src/tagma.test.ts DELETED
@@ -1,317 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { mkdtempSync, rmSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
- import { createTagma } from './tagma';
6
- import type { DriverPlugin, TagmaPlugin, TaskResult } from './types';
7
- import type { TagmaRuntime } from './runtime';
8
-
9
- function makeDir(prefix: string): string {
10
- return mkdtempSync(join(tmpdir(), prefix));
11
- }
12
-
13
- function makeDriver(name: string, marker: string[]): DriverPlugin {
14
- return {
15
- name,
16
- capabilities: { sessionResume: false, systemPrompt: false, outputFormat: false },
17
- async buildCommand() {
18
- marker.push(name);
19
- return { args: ['echo', name] };
20
- },
21
- };
22
- }
23
-
24
- function memoryLogStore() {
25
- return {
26
- openRunLog({ runId }: { runId: string }) {
27
- return {
28
- path: `mem://${runId}/pipeline.log`,
29
- dir: `mem://${runId}`,
30
- append() {
31
- /* memory sink */
32
- },
33
- close() {
34
- /* memory sink */
35
- },
36
- };
37
- },
38
- taskOutputPath({
39
- runId,
40
- taskId,
41
- stream,
42
- }: {
43
- runId: string;
44
- taskId: string;
45
- stream: 'stdout' | 'stderr';
46
- }) {
47
- return `mem://${runId}/${taskId}.${stream}`;
48
- },
49
- logsDir(workDir: string) {
50
- return `mem://${workDir}/logs`;
51
- },
52
- async prune() {
53
- /* memory sink */
54
- },
55
- };
56
- }
57
-
58
- describe('createTagma', () => {
59
- test('runs command tasks through the configured runtime', async () => {
60
- const calls: string[] = [];
61
- const taskResult: TaskResult = {
62
- exitCode: 0,
63
- stdout: 'runtime-ok',
64
- stderr: '',
65
- stdoutPath: null,
66
- stderrPath: null,
67
- stdoutBytes: 10,
68
- stderrBytes: 0,
69
- durationMs: 1,
70
- sessionId: null,
71
- normalizedOutput: null,
72
- failureKind: null,
73
- };
74
- const runtime: TagmaRuntime = {
75
- async runCommand(command, cwd) {
76
- calls.push(`${cwd}:${command}`);
77
- return taskResult;
78
- },
79
- async runSpawn() {
80
- throw new Error('runSpawn should not be called for command tasks');
81
- },
82
- async ensureDir() {
83
- /* no-op */
84
- },
85
- async fileExists() {
86
- return false;
87
- },
88
- async *watch() {
89
- /* no-op */
90
- },
91
- logStore: memoryLogStore(),
92
- now() {
93
- return new Date('2026-04-26T00:00:00.000Z');
94
- },
95
- sleep() {
96
- return Promise.resolve();
97
- },
98
- };
99
- const tagma = createTagma({ builtins: false, runtime });
100
- const dir = makeDir('tagma-runtime-run-');
101
- try {
102
- const result = await tagma.run(
103
- {
104
- name: 'runtime-run',
105
- tracks: [
106
- {
107
- id: 't',
108
- name: 'T',
109
- tasks: [{ id: 'cmd', name: 'cmd', command: 'fake-only-command' }],
110
- },
111
- ],
112
- },
113
- {
114
- cwd: dir,
115
- skipPluginLoading: true,
116
- },
117
- );
118
-
119
- expect(result.success).toBe(true);
120
- expect(calls).toEqual([`${dir}:fake-only-command`]);
121
- expect(result.states.get('t.cmd')?.result?.stdout).toBe('runtime-ok');
122
- } finally {
123
- rmSync(dir, { recursive: true, force: true });
124
- }
125
- });
126
-
127
- test('routes run logs and task output artifacts through the runtime log store', async () => {
128
- const calls: string[] = [];
129
- let stdoutPath: string | undefined;
130
- let stderrPath: string | undefined;
131
-
132
- const runtime = {
133
- async runCommand(_command: string, _cwd: string, options?: { stdoutPath?: string; stderrPath?: string }) {
134
- stdoutPath = options?.stdoutPath;
135
- stderrPath = options?.stderrPath;
136
- return {
137
- exitCode: 0,
138
- stdout: 'runtime-log-ok',
139
- stderr: '',
140
- stdoutPath: options?.stdoutPath ?? null,
141
- stderrPath: options?.stderrPath ?? null,
142
- stdoutBytes: 14,
143
- stderrBytes: 0,
144
- durationMs: 1,
145
- sessionId: null,
146
- normalizedOutput: null,
147
- failureKind: null,
148
- } satisfies TaskResult;
149
- },
150
- async runSpawn() {
151
- throw new Error('runSpawn should not be called for command tasks');
152
- },
153
- async ensureDir(path: string) {
154
- calls.push(`ensure:${path}`);
155
- },
156
- async fileExists(path: string) {
157
- calls.push(`exists:${path}`);
158
- return false;
159
- },
160
- async *watch(path: string) {
161
- calls.push(`watch:${path}`);
162
- },
163
- now() {
164
- return new Date('2026-04-26T00:00:00.000Z');
165
- },
166
- sleep(ms: number) {
167
- calls.push(`sleep:${ms}`);
168
- return Promise.resolve();
169
- },
170
- logStore: {
171
- openRunLog({ runId, header }: { runId: string; header: string }) {
172
- calls.push(`open:${runId}:${header.includes(runId)}`);
173
- return {
174
- path: `mem://${runId}/pipeline.log`,
175
- dir: `mem://${runId}`,
176
- append(line: string) {
177
- calls.push(`append:${line.length > 0}`);
178
- },
179
- close() {
180
- calls.push(`close:${runId}`);
181
- },
182
- };
183
- },
184
- taskOutputPath({
185
- runId,
186
- taskId,
187
- stream,
188
- }: {
189
- runId: string;
190
- taskId: string;
191
- stream: 'stdout' | 'stderr';
192
- }) {
193
- calls.push(`task-output:${taskId}:${stream}`);
194
- return `mem://${runId}/${taskId}.${stream}`;
195
- },
196
- logsDir(workDir: string) {
197
- calls.push(`logs-dir:${workDir}`);
198
- return `mem://${workDir}/logs`;
199
- },
200
- async prune({ keep, excludeRunId }: { keep: number; excludeRunId: string }) {
201
- calls.push(`prune:${keep}:${excludeRunId}`);
202
- },
203
- },
204
- } as unknown as TagmaRuntime;
205
-
206
- const tagma = createTagma({ builtins: false, runtime });
207
- const dir = makeDir('tagma-runtime-log-store-');
208
- try {
209
- const result = await tagma.run(
210
- {
211
- name: 'runtime-log-store',
212
- tracks: [
213
- {
214
- id: 't',
215
- name: 'T',
216
- tasks: [{ id: 'cmd', name: 'cmd', command: 'fake-only-command' }],
217
- },
218
- ],
219
- },
220
- {
221
- cwd: dir,
222
- skipPluginLoading: true,
223
- },
224
- );
225
-
226
- expect(result.success).toBe(true);
227
- expect(result.logPath).toMatch(/^mem:\/\/run_.+\/pipeline\.log$/);
228
- expect(stdoutPath).toMatch(/^mem:\/\/run_.+\/t\.cmd\.stdout$/);
229
- expect(stderrPath).toMatch(/^mem:\/\/run_.+\/t\.cmd\.stderr$/);
230
- expect(calls.some((call) => call.startsWith('open:run_'))).toBe(true);
231
- expect(calls).toContain('task-output:t.cmd:stdout');
232
- expect(calls).toContain('task-output:t.cmd:stderr');
233
- expect(calls.some((call) => call.startsWith('prune:20:run_'))).toBe(true);
234
- } finally {
235
- rmSync(dir, { recursive: true, force: true });
236
- }
237
- });
238
-
239
- test('registers capability plugins passed to options', () => {
240
- const seen: string[] = [];
241
- const driver = makeDriver('driver-plugin', seen);
242
- const plugin: TagmaPlugin = {
243
- name: 'tagma-plugin-local',
244
- capabilities: {
245
- drivers: {
246
- mock: driver,
247
- },
248
- },
249
- };
250
-
251
- const tagma = createTagma({ builtins: false, plugins: [plugin] });
252
-
253
- expect(tagma.registry.getHandler<DriverPlugin>('drivers', 'mock')).toBe(driver);
254
- expect(seen).toEqual([]);
255
- });
256
-
257
- test('instances own isolated plugin registries', () => {
258
- const seenA: string[] = [];
259
- const seenB: string[] = [];
260
- const tagmaA = createTagma({ builtins: false });
261
- const tagmaB = createTagma({ builtins: false });
262
-
263
- tagmaA.registry.registerPlugin('drivers', 'mock', makeDriver('driver-a', seenA));
264
- tagmaB.registry.registerPlugin('drivers', 'mock', makeDriver('driver-b', seenB));
265
-
266
- expect(tagmaA.registry.getHandler<DriverPlugin>('drivers', 'mock').name).toBe('driver-a');
267
- expect(tagmaB.registry.getHandler<DriverPlugin>('drivers', 'mock').name).toBe('driver-b');
268
- expect(seenA).toEqual([]);
269
- expect(seenB).toEqual([]);
270
- });
271
-
272
- test('run uses only the instance registry', async () => {
273
- const tagma = createTagma({ builtins: false });
274
- const dir = makeDir('tagma-instance-run-');
275
- try {
276
- await expect(
277
- tagma.run(
278
- {
279
- name: 'instance-run',
280
- tracks: [
281
- {
282
- id: 't',
283
- name: 'T',
284
- tasks: [{ id: 'prompt', name: 'prompt', prompt: 'hello' }],
285
- },
286
- ],
287
- },
288
- {
289
- cwd: dir,
290
- skipPluginLoading: true,
291
- },
292
- ),
293
- ).rejects.toThrow(/driver "opencode" not registered/);
294
- } finally {
295
- rmSync(dir, { recursive: true, force: true });
296
- }
297
- });
298
-
299
- test('validate returns structural pipeline errors without running tasks', () => {
300
- const tagma = createTagma({ builtins: false });
301
-
302
- expect(
303
- tagma.validate({
304
- name: 'invalid',
305
- tracks: [
306
- {
307
- id: 't',
308
- name: 'T',
309
- tasks: [
310
- { id: 'a', name: 'A', command: 'echo a', depends_on: ['missing'] },
311
- ],
312
- },
313
- ],
314
- }),
315
- ).toEqual(['Task reference "missing" not found']);
316
- });
317
- });