@tagma/sdk 0.4.14 → 0.4.16
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/LICENSE +21 -21
- package/README.md +569 -569
- package/dist/dag.d.ts.map +1 -1
- package/dist/dag.js +58 -69
- package/dist/dag.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +63 -37
- package/dist/engine.js.map +1 -1
- package/dist/middlewares/static-context.d.ts.map +1 -1
- package/dist/middlewares/static-context.js +7 -3
- package/dist/middlewares/static-context.js.map +1 -1
- package/dist/prompt-doc.d.ts +36 -0
- package/dist/prompt-doc.d.ts.map +1 -0
- package/dist/prompt-doc.js +44 -0
- package/dist/prompt-doc.js.map +1 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +11 -0
- package/dist/registry.js.map +1 -1
- package/dist/sdk.d.ts +3 -0
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +4 -0
- package/dist/sdk.js.map +1 -1
- package/dist/task-ref.d.ts +55 -0
- package/dist/task-ref.d.ts.map +1 -0
- package/dist/task-ref.js +101 -0
- package/dist/task-ref.js.map +1 -0
- package/dist/task-ref.test.d.ts +2 -0
- package/dist/task-ref.test.d.ts.map +1 -0
- package/dist/task-ref.test.js +364 -0
- package/dist/task-ref.test.js.map +1 -0
- package/dist/templates.d.ts +20 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +93 -0
- package/dist/templates.js.map +1 -0
- package/dist/validate-raw.d.ts.map +1 -1
- package/dist/validate-raw.js +27 -53
- package/dist/validate-raw.js.map +1 -1
- package/package.json +2 -2
- package/scripts/preinstall.js +31 -31
- package/src/adapters/stdin-approval.ts +106 -106
- package/src/adapters/websocket-approval.ts +224 -224
- package/src/approval.ts +131 -131
- package/src/bootstrap.ts +37 -37
- package/src/completions/exit-code.ts +34 -34
- package/src/completions/file-exists.ts +66 -66
- package/src/completions/output-check.ts +86 -86
- package/src/config-ops.ts +307 -307
- package/src/dag.ts +61 -67
- package/src/drivers/claude-code.ts +250 -250
- package/src/engine.ts +1137 -1098
- package/src/hooks.ts +187 -187
- package/src/logger.ts +182 -182
- package/src/middlewares/static-context.ts +49 -45
- package/src/pipeline-runner.ts +156 -156
- package/src/prompt-doc.ts +49 -0
- package/src/registry.ts +255 -242
- package/src/runner.ts +395 -395
- package/src/schema.test.ts +101 -101
- package/src/schema.ts +338 -338
- package/src/sdk.ts +111 -92
- package/src/task-ref.test.ts +401 -0
- package/src/task-ref.ts +120 -0
- package/src/triggers/file.ts +164 -164
- package/src/triggers/manual.ts +86 -86
- package/src/types.ts +18 -18
- package/src/utils.ts +203 -203
- package/src/validate-raw.ts +412 -442
package/src/dag.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
TaskConfig,
|
|
6
6
|
TrackConfig,
|
|
7
7
|
} from './types';
|
|
8
|
+
import { buildTaskIndex, qualifyTaskId, resolveTaskRef } from './task-ref';
|
|
8
9
|
|
|
9
10
|
export interface DagNode {
|
|
10
11
|
readonly taskId: string; // fully qualified: track_id.task_id or just task_id
|
|
@@ -27,33 +28,17 @@ export interface Dag {
|
|
|
27
28
|
readonly sorted: readonly string[]; // topological order
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
// Build a global task ID: for cross-track refs we use "track_id.task_id"
|
|
31
|
-
// Within a track, bare "task_id" is also valid
|
|
32
|
-
function qualifyId(trackId: string, taskId: string): string {
|
|
33
|
-
return `${trackId}.${taskId}`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
31
|
export function buildDag(config: PipelineConfig): Dag {
|
|
37
32
|
const nodes = new Map<string, DagNode>();
|
|
38
|
-
// Map bare task IDs to qualified IDs (for resolving unqualified refs)
|
|
39
|
-
const bareToQualified = new Map<string, string>();
|
|
40
33
|
|
|
41
|
-
// 1. Register all nodes
|
|
34
|
+
// 1. Register all nodes. Duplicates throw — same-track task-id collisions
|
|
35
|
+
// would otherwise silently overwrite one another in the DAG.
|
|
42
36
|
for (const track of config.tracks) {
|
|
43
37
|
for (const task of track.tasks) {
|
|
44
|
-
const qid =
|
|
45
|
-
|
|
38
|
+
const qid = qualifyTaskId(track.id, task.id);
|
|
46
39
|
if (nodes.has(qid)) {
|
|
47
40
|
throw new Error(`Duplicate task ID: "${qid}"`);
|
|
48
41
|
}
|
|
49
|
-
|
|
50
|
-
// Track bare ID → qualified. If same bare ID in multiple tracks, mark ambiguous
|
|
51
|
-
if (bareToQualified.has(task.id)) {
|
|
52
|
-
bareToQualified.set(task.id, '__ambiguous__');
|
|
53
|
-
} else {
|
|
54
|
-
bareToQualified.set(task.id, qid);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
42
|
nodes.set(qid, {
|
|
58
43
|
taskId: qid,
|
|
59
44
|
task,
|
|
@@ -63,34 +48,27 @@ export function buildDag(config: PipelineConfig): Dag {
|
|
|
63
48
|
}
|
|
64
49
|
}
|
|
65
50
|
|
|
66
|
-
//
|
|
51
|
+
// Shared index for ref resolution — same code path validate-raw uses.
|
|
52
|
+
const index = buildTaskIndex(config);
|
|
53
|
+
|
|
67
54
|
function resolveRef(ref: string, fromTrackId: string): string {
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
if (!nodes.has(ref)) {
|
|
71
|
-
throw new Error(`Task reference "${ref}" not found`);
|
|
72
|
-
}
|
|
73
|
-
return ref;
|
|
74
|
-
}
|
|
75
|
-
// Try within same track first
|
|
76
|
-
const sameTrack = qualifyId(fromTrackId, ref);
|
|
77
|
-
if (nodes.has(sameTrack)) return sameTrack;
|
|
78
|
-
// Try global bare lookup
|
|
79
|
-
const global = bareToQualified.get(ref);
|
|
80
|
-
if (global && global !== '__ambiguous__') return global;
|
|
81
|
-
if (global === '__ambiguous__') {
|
|
55
|
+
const result = resolveTaskRef(ref, fromTrackId, index);
|
|
56
|
+
if (result.kind === 'ambiguous') {
|
|
82
57
|
throw new Error(
|
|
83
58
|
`Ambiguous task reference "${ref}" exists in multiple tracks. ` +
|
|
84
59
|
`Use "track_id.task_id" format.`,
|
|
85
60
|
);
|
|
86
61
|
}
|
|
87
|
-
|
|
62
|
+
if (result.kind === 'not_found') {
|
|
63
|
+
throw new Error(`Task reference "${ref}" not found`);
|
|
64
|
+
}
|
|
65
|
+
return result.qid;
|
|
88
66
|
}
|
|
89
67
|
|
|
90
68
|
// 2. Resolve depends_on and continue_from to qualified IDs
|
|
91
69
|
for (const track of config.tracks) {
|
|
92
70
|
for (const task of track.tasks) {
|
|
93
|
-
const qid =
|
|
71
|
+
const qid = qualifyTaskId(track.id, task.id);
|
|
94
72
|
const deps: string[] = [];
|
|
95
73
|
let resolvedContinueFrom: string | undefined;
|
|
96
74
|
|
|
@@ -100,18 +78,27 @@ export function buildDag(config: PipelineConfig): Dag {
|
|
|
100
78
|
}
|
|
101
79
|
}
|
|
102
80
|
if (task.continue_from) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
81
|
+
// Preserve the ambiguous-vs-not-found distinction in the user-facing
|
|
82
|
+
// error: rewording "ambiguous" as "no such task found" (the previous
|
|
83
|
+
// behavior) hid the real problem and sent users searching for a
|
|
84
|
+
// missing task that actually existed in two places.
|
|
85
|
+
const result = resolveTaskRef(task.continue_from, track.id, index);
|
|
86
|
+
if (result.kind === 'ambiguous') {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Task "${qid}": continue_from "${task.continue_from}" is ambiguous — ` +
|
|
89
|
+
`multiple tracks have a task with this id. Use the fully-qualified ` +
|
|
90
|
+
`form "trackId.${task.continue_from}".`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (result.kind === 'not_found') {
|
|
107
94
|
throw new Error(
|
|
108
95
|
`Task "${qid}": continue_from "${task.continue_from}" — no such task found. ` +
|
|
109
96
|
`Use a fully-qualified reference (trackId.taskId) or ensure the target task exists.`,
|
|
110
97
|
);
|
|
111
98
|
}
|
|
112
|
-
resolvedContinueFrom =
|
|
113
|
-
if (!deps.includes(
|
|
114
|
-
deps.push(
|
|
99
|
+
resolvedContinueFrom = result.qid;
|
|
100
|
+
if (!deps.includes(result.qid)) {
|
|
101
|
+
deps.push(result.qid); // continue_from implies dependency
|
|
115
102
|
}
|
|
116
103
|
}
|
|
117
104
|
|
|
@@ -137,22 +124,37 @@ export function buildDag(config: PipelineConfig): Dag {
|
|
|
137
124
|
}
|
|
138
125
|
}
|
|
139
126
|
|
|
140
|
-
|
|
127
|
+
// D20: deterministic topo order. Kahn's algorithm dequeues in insertion
|
|
128
|
+
// order by default, which depends on map iteration order — itself a
|
|
129
|
+
// function of the (track, task) order in the YAML. Two pipelines that
|
|
130
|
+
// are DAG-equivalent but written in a different order produced different
|
|
131
|
+
// `sorted` arrays, leading to subtle run-to-run non-determinism for
|
|
132
|
+
// parallel tasks with side effects (writing the same file, touching the
|
|
133
|
+
// same repo). Break the tie by qualified id so identical DAG shapes
|
|
134
|
+
// always yield identical schedules across machines and across YAML
|
|
135
|
+
// round-trips.
|
|
136
|
+
const ready: string[] = [];
|
|
141
137
|
for (const [id, degree] of inDegree) {
|
|
142
|
-
if (degree === 0)
|
|
138
|
+
if (degree === 0) ready.push(id);
|
|
143
139
|
}
|
|
140
|
+
ready.sort();
|
|
144
141
|
|
|
145
142
|
const sorted: string[] = [];
|
|
146
|
-
// Use an index pointer instead of shift() to avoid O(n) per dequeue.
|
|
147
143
|
let qi = 0;
|
|
148
|
-
while (qi <
|
|
149
|
-
const current =
|
|
144
|
+
while (qi < ready.length) {
|
|
145
|
+
const current = ready[qi++]!;
|
|
150
146
|
sorted.push(current);
|
|
147
|
+
// Collect children whose in-degree hits zero in this step, then push
|
|
148
|
+
// them into the ready bucket in sorted order — keeps each "wave" of
|
|
149
|
+
// parallel-eligible tasks ordered by qid.
|
|
150
|
+
const newlyReady: string[] = [];
|
|
151
151
|
for (const child of adjacency.get(current)!) {
|
|
152
152
|
const newDegree = inDegree.get(child)! - 1;
|
|
153
153
|
inDegree.set(child, newDegree);
|
|
154
|
-
if (newDegree === 0)
|
|
154
|
+
if (newDegree === 0) newlyReady.push(child);
|
|
155
155
|
}
|
|
156
|
+
if (newlyReady.length > 1) newlyReady.sort();
|
|
157
|
+
for (const child of newlyReady) ready.push(child);
|
|
156
158
|
}
|
|
157
159
|
|
|
158
160
|
if (sorted.length !== nodes.size) {
|
|
@@ -193,38 +195,30 @@ export interface RawDag {
|
|
|
193
195
|
*/
|
|
194
196
|
export function buildRawDag(config: RawPipelineConfig): RawDag {
|
|
195
197
|
const nodes = new Map<string, RawDagNode>();
|
|
196
|
-
const bareToQualified = new Map<string, string>();
|
|
197
198
|
|
|
198
|
-
// 1. Register all concrete tasks
|
|
199
|
+
// 1. Register all concrete tasks. Duplicates are skipped (not thrown) so
|
|
200
|
+
// partially-typed editor state doesn't produce a hard error.
|
|
199
201
|
for (const track of config.tracks) {
|
|
200
202
|
for (const task of track.tasks) {
|
|
201
|
-
const qid =
|
|
202
|
-
if (nodes.has(qid)) continue;
|
|
203
|
-
|
|
204
|
-
if (bareToQualified.has(task.id)) {
|
|
205
|
-
bareToQualified.set(task.id, '__ambiguous__');
|
|
206
|
-
} else {
|
|
207
|
-
bareToQualified.set(task.id, qid);
|
|
208
|
-
}
|
|
203
|
+
const qid = qualifyTaskId(track.id, task.id);
|
|
204
|
+
if (nodes.has(qid)) continue;
|
|
209
205
|
nodes.set(qid, { taskId: qid, trackId: track.id, rawTask: task, dependsOn: [] });
|
|
210
206
|
}
|
|
211
207
|
}
|
|
212
208
|
|
|
213
|
-
|
|
209
|
+
const index = buildTaskIndex(config);
|
|
210
|
+
|
|
214
211
|
function tryResolve(ref: string, fromTrackId: string): string | null {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (nodes.has(sameTrack)) return sameTrack;
|
|
218
|
-
const global = bareToQualified.get(ref);
|
|
219
|
-
if (global && global !== '__ambiguous__') return global;
|
|
220
|
-
return null;
|
|
212
|
+
const result = resolveTaskRef(ref, fromTrackId, index);
|
|
213
|
+
return result.kind === 'resolved' ? result.qid : null;
|
|
221
214
|
}
|
|
222
215
|
|
|
216
|
+
// 2. Resolve dependency refs leniently (missing / ambiguous refs are skipped)
|
|
223
217
|
const edges: { from: string; to: string }[] = [];
|
|
224
218
|
|
|
225
219
|
for (const track of config.tracks) {
|
|
226
220
|
for (const task of track.tasks) {
|
|
227
|
-
const qid =
|
|
221
|
+
const qid = qualifyTaskId(track.id, task.id);
|
|
228
222
|
const deps: string[] = [];
|
|
229
223
|
|
|
230
224
|
for (const ref of task.depends_on ?? []) {
|