@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.
- package/README.md +60 -53
- package/dist/completions/file-exists.js +1 -1
- package/dist/completions/file-exists.js.map +1 -1
- package/dist/completions/output-check.d.ts.map +1 -1
- package/dist/completions/output-check.js +17 -4
- package/dist/completions/output-check.js.map +1 -1
- package/dist/config.d.ts +4 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/dataflow.d.ts +3 -0
- package/dist/dataflow.d.ts.map +1 -0
- package/dist/dataflow.js +2 -0
- package/dist/dataflow.js.map +1 -0
- package/dist/drivers/opencode.d.ts.map +1 -1
- package/dist/drivers/opencode.js +23 -71
- package/dist/drivers/opencode.js.map +1 -1
- package/dist/middlewares/static-context.d.ts.map +1 -1
- package/dist/middlewares/static-context.js +1 -2
- package/dist/middlewares/static-context.js.map +1 -1
- package/dist/pipeline-runner.d.ts.map +1 -1
- package/dist/pipeline-runner.js +2 -2
- package/dist/pipeline-runner.js.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +3 -4
- package/dist/schema.js.map +1 -1
- package/dist/triggers/file.d.ts.map +1 -1
- package/dist/triggers/file.js +1 -2
- package/dist/triggers/file.js.map +1 -1
- package/dist/triggers/manual.d.ts.map +1 -1
- package/dist/triggers/manual.js +1 -2
- package/dist/triggers/manual.js.map +1 -1
- package/dist/types.d.ts +1 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -12
- package/dist/types.js.map +1 -1
- package/dist/utils-api.d.ts +1 -1
- package/dist/utils-api.d.ts.map +1 -1
- package/dist/utils-api.js +1 -1
- package/dist/utils-api.js.map +1 -1
- package/dist/validate-raw.d.ts +4 -4
- package/dist/validate-raw.d.ts.map +1 -1
- package/dist/validate-raw.js +45 -52
- package/dist/validate-raw.js.map +1 -1
- package/package.json +11 -24
- package/dist/adapters/stdin-approval.d.ts +0 -2
- package/dist/adapters/stdin-approval.d.ts.map +0 -1
- package/dist/adapters/stdin-approval.js +0 -2
- package/dist/adapters/stdin-approval.js.map +0 -1
- package/dist/adapters/websocket-approval.d.ts +0 -2
- package/dist/adapters/websocket-approval.d.ts.map +0 -1
- package/dist/adapters/websocket-approval.js +0 -2
- package/dist/adapters/websocket-approval.js.map +0 -1
- package/dist/core/dataflow.d.ts +0 -23
- package/dist/core/dataflow.d.ts.map +0 -1
- package/dist/core/dataflow.js +0 -99
- package/dist/core/dataflow.js.map +0 -1
- package/dist/core/log-prune.d.ts +0 -16
- package/dist/core/log-prune.d.ts.map +0 -1
- package/dist/core/log-prune.js +0 -34
- package/dist/core/log-prune.js.map +0 -1
- package/dist/core/preflight.d.ts +0 -13
- package/dist/core/preflight.d.ts.map +0 -1
- package/dist/core/preflight.js +0 -61
- package/dist/core/preflight.js.map +0 -1
- package/dist/core/run-context.d.ts +0 -55
- package/dist/core/run-context.d.ts.map +0 -1
- package/dist/core/run-context.js +0 -158
- package/dist/core/run-context.js.map +0 -1
- package/dist/core/run-state.d.ts +0 -25
- package/dist/core/run-state.d.ts.map +0 -1
- package/dist/core/run-state.js +0 -93
- package/dist/core/run-state.js.map +0 -1
- package/dist/core/scheduler.d.ts +0 -13
- package/dist/core/scheduler.d.ts.map +0 -1
- package/dist/core/scheduler.js +0 -35
- package/dist/core/scheduler.js.map +0 -1
- package/dist/core/task-executor.d.ts +0 -13
- package/dist/core/task-executor.d.ts.map +0 -1
- package/dist/core/task-executor.js +0 -610
- package/dist/core/task-executor.js.map +0 -1
- package/dist/core/trigger-errors.d.ts +0 -9
- package/dist/core/trigger-errors.d.ts.map +0 -1
- package/dist/core/trigger-errors.js +0 -15
- package/dist/core/trigger-errors.js.map +0 -1
- package/dist/dag.d.ts +0 -45
- package/dist/dag.d.ts.map +0 -1
- package/dist/dag.js +0 -177
- package/dist/dag.js.map +0 -1
- package/dist/hooks.d.ts +0 -73
- package/dist/hooks.d.ts.map +0 -1
- package/dist/hooks.js +0 -106
- package/dist/hooks.js.map +0 -1
- package/dist/pipeline-definition.d.ts +0 -3
- package/dist/pipeline-definition.d.ts.map +0 -1
- package/dist/pipeline-definition.js +0 -4
- package/dist/pipeline-definition.js.map +0 -1
- package/dist/ports.d.ts +0 -196
- package/dist/ports.d.ts.map +0 -1
- package/dist/ports.js +0 -688
- package/dist/ports.js.map +0 -1
- package/dist/prompt-doc.d.ts +0 -70
- package/dist/prompt-doc.d.ts.map +0 -1
- package/dist/prompt-doc.js +0 -154
- package/dist/prompt-doc.js.map +0 -1
- package/dist/registry.d.ts +0 -3
- package/dist/registry.d.ts.map +0 -1
- package/dist/registry.js +0 -2
- package/dist/registry.js.map +0 -1
- package/dist/task-ref.d.ts +0 -55
- package/dist/task-ref.d.ts.map +0 -1
- package/dist/task-ref.js +0 -103
- package/dist/task-ref.js.map +0 -1
- package/dist/utils.d.ts +0 -13
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -177
- package/dist/utils.js.map +0 -1
- package/src/adapters/stdin-approval.ts +0 -1
- package/src/adapters/websocket-approval.ts +0 -1
- package/src/approval.ts +0 -9
- package/src/bootstrap.ts +0 -55
- package/src/completions/exit-code.ts +0 -34
- package/src/completions/file-exists.ts +0 -66
- package/src/completions/output-check.test.ts +0 -50
- package/src/completions/output-check.ts +0 -92
- package/src/config-ops.test.ts +0 -70
- package/src/config-ops.ts +0 -328
- package/src/config.ts +0 -26
- package/src/core/dataflow.test.ts +0 -166
- package/src/core/dataflow.ts +0 -161
- package/src/core/log-prune.test.ts +0 -58
- package/src/core/log-prune.ts +0 -43
- package/src/core/preflight.test.ts +0 -49
- package/src/core/preflight.ts +0 -89
- package/src/core/run-context.test.ts +0 -291
- package/src/core/run-context.ts +0 -211
- package/src/core/run-state.test.ts +0 -98
- package/src/core/run-state.ts +0 -122
- package/src/core/scheduler.test.ts +0 -83
- package/src/core/scheduler.ts +0 -42
- package/src/core/task-executor.ts +0 -752
- package/src/core/trigger-errors.ts +0 -15
- package/src/dag.test.ts +0 -56
- package/src/dag.ts +0 -245
- package/src/drivers/opencode.ts +0 -410
- package/src/engine-ports-mixed.test.ts +0 -182
- package/src/engine-ports.test.ts +0 -210
- package/src/engine-task-type.test.ts +0 -56
- package/src/engine.ts +0 -32
- package/src/hooks.ts +0 -193
- package/src/index.ts +0 -31
- package/src/logger.ts +0 -2
- package/src/middlewares/static-context.ts +0 -49
- package/src/package-split.test.ts +0 -15
- package/src/pipeline-definition.ts +0 -5
- package/src/pipeline-runner.test.ts +0 -144
- package/src/pipeline-runner.ts +0 -194
- package/src/plugin-registry.test.ts +0 -448
- package/src/plugins.ts +0 -21
- package/src/ports.test.ts +0 -678
- package/src/ports.ts +0 -925
- package/src/prompt-doc.test.ts +0 -174
- package/src/prompt-doc.ts +0 -169
- package/src/registry.ts +0 -7
- package/src/runner.test.ts +0 -142
- package/src/runner.ts +0 -1
- package/src/runtime/adapters/stdin-approval.ts +0 -1
- package/src/runtime/adapters/websocket-approval.ts +0 -1
- package/src/runtime/bun-process-runner.ts +0 -1
- package/src/runtime-adapters.test.ts +0 -10
- package/src/runtime.ts +0 -12
- package/src/schema-ports.test.ts +0 -172
- package/src/schema.test.ts +0 -213
- package/src/schema.ts +0 -379
- package/src/tagma.test.ts +0 -317
- package/src/tagma.ts +0 -67
- package/src/task-ref.test.ts +0 -401
- package/src/task-ref.ts +0 -121
- package/src/triggers/file.test.ts +0 -79
- package/src/triggers/file.ts +0 -131
- package/src/triggers/manual.ts +0 -86
- package/src/types.ts +0 -18
- package/src/utils-api.ts +0 -8
- package/src/utils.test.ts +0 -28
- package/src/utils.ts +0 -203
- package/src/validate-raw-plugin-types.test.ts +0 -60
- package/src/validate-raw-ports.test.ts +0 -136
- package/src/validate-raw.ts +0 -852
- package/src/yaml-compiler.test.ts +0 -108
- package/src/yaml-compiler.ts +0 -110
- 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
|
-
});
|