@tagma/sdk 0.4.11 → 0.4.13

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 (88) hide show
  1. package/README.md +572 -566
  2. package/dist/adapters/websocket-approval.d.ts.map +1 -1
  3. package/dist/adapters/websocket-approval.js +3 -1
  4. package/dist/adapters/websocket-approval.js.map +1 -1
  5. package/dist/approval.d.ts.map +1 -1
  6. package/dist/approval.js.map +1 -1
  7. package/dist/completions/exit-code.d.ts.map +1 -1
  8. package/dist/completions/exit-code.js.map +1 -1
  9. package/dist/completions/file-exists.d.ts.map +1 -1
  10. package/dist/completions/file-exists.js.map +1 -1
  11. package/dist/completions/output-check.js +2 -7
  12. package/dist/completions/output-check.js.map +1 -1
  13. package/dist/config-ops.d.ts.map +1 -1
  14. package/dist/config-ops.js +24 -26
  15. package/dist/config-ops.js.map +1 -1
  16. package/dist/dag.d.ts.map +1 -1
  17. package/dist/dag.js +1 -1
  18. package/dist/dag.js.map +1 -1
  19. package/dist/drivers/claude-code.d.ts.map +1 -1
  20. package/dist/drivers/claude-code.js +10 -5
  21. package/dist/drivers/claude-code.js.map +1 -1
  22. package/dist/engine.d.ts.map +1 -1
  23. package/dist/engine.js +63 -29
  24. package/dist/engine.js.map +1 -1
  25. package/dist/hooks.d.ts.map +1 -1
  26. package/dist/hooks.js +1 -3
  27. package/dist/hooks.js.map +1 -1
  28. package/dist/logger.d.ts.map +1 -1
  29. package/dist/logger.js +4 -2
  30. package/dist/logger.js.map +1 -1
  31. package/dist/pipeline-runner.d.ts.map +1 -1
  32. package/dist/pipeline-runner.js +10 -4
  33. package/dist/pipeline-runner.js.map +1 -1
  34. package/dist/registry.d.ts +11 -1
  35. package/dist/registry.d.ts.map +1 -1
  36. package/dist/registry.js +28 -3
  37. package/dist/registry.js.map +1 -1
  38. package/dist/runner.d.ts.map +1 -1
  39. package/dist/runner.js +18 -13
  40. package/dist/runner.js.map +1 -1
  41. package/dist/schema.d.ts.map +1 -1
  42. package/dist/schema.js +39 -14
  43. package/dist/schema.js.map +1 -1
  44. package/dist/schema.test.js +5 -1
  45. package/dist/schema.test.js.map +1 -1
  46. package/dist/sdk.d.ts +2 -2
  47. package/dist/sdk.d.ts.map +1 -1
  48. package/dist/sdk.js +1 -1
  49. package/dist/sdk.js.map +1 -1
  50. package/dist/triggers/file.d.ts.map +1 -1
  51. package/dist/triggers/file.js +11 -4
  52. package/dist/triggers/file.js.map +1 -1
  53. package/dist/triggers/manual.d.ts.map +1 -1
  54. package/dist/triggers/manual.js +2 -1
  55. package/dist/triggers/manual.js.map +1 -1
  56. package/dist/utils.d.ts.map +1 -1
  57. package/dist/utils.js +63 -8
  58. package/dist/utils.js.map +1 -1
  59. package/dist/validate-raw.d.ts.map +1 -1
  60. package/dist/validate-raw.js +60 -11
  61. package/dist/validate-raw.js.map +1 -1
  62. package/package.json +2 -2
  63. package/scripts/preinstall.js +1 -1
  64. package/src/adapters/stdin-approval.ts +106 -106
  65. package/src/adapters/websocket-approval.ts +224 -220
  66. package/src/approval.ts +131 -125
  67. package/src/bootstrap.ts +37 -37
  68. package/src/completions/exit-code.ts +34 -30
  69. package/src/completions/file-exists.ts +66 -60
  70. package/src/completions/output-check.ts +86 -86
  71. package/src/config-ops.ts +307 -322
  72. package/src/dag.ts +234 -228
  73. package/src/drivers/claude-code.ts +250 -240
  74. package/src/engine.ts +1098 -928
  75. package/src/hooks.ts +187 -179
  76. package/src/logger.ts +182 -178
  77. package/src/middlewares/static-context.ts +45 -45
  78. package/src/pipeline-runner.ts +156 -150
  79. package/src/registry.ts +51 -23
  80. package/src/runner.ts +395 -397
  81. package/src/schema.test.ts +5 -1
  82. package/src/schema.ts +338 -298
  83. package/src/sdk.ts +91 -81
  84. package/src/triggers/file.ts +33 -14
  85. package/src/triggers/manual.ts +86 -81
  86. package/src/types.ts +18 -18
  87. package/src/utils.ts +202 -140
  88. package/src/validate-raw.ts +442 -389
package/src/dag.ts CHANGED
@@ -1,183 +1,189 @@
1
- import type { PipelineConfig, RawPipelineConfig, RawTaskConfig, TaskConfig, TrackConfig } from './types';
2
-
3
- export interface DagNode {
4
- readonly taskId: string; // fully qualified: track_id.task_id or just task_id
5
- readonly task: TaskConfig;
6
- readonly track: TrackConfig;
7
- readonly dependsOn: readonly string[];
8
- /**
9
- * H1: `task.continue_from` may be written by users as a bare task id
10
- * (e.g. `review`) or a same-track shorthand. The driver needs the
11
- * fully-qualified upstream id to look up output/session/normalized maps
12
- * deterministically — bare lookups race when two tracks happen to share
13
- * a task name. dag.ts performs the qualification once, here, so the
14
- * engine never has to.
15
- */
16
- readonly resolvedContinueFrom?: string;
17
- }
18
-
19
- export interface Dag {
20
- readonly nodes: ReadonlyMap<string, DagNode>;
21
- readonly sorted: readonly string[]; // topological order
22
- }
23
-
24
- // Build a global task ID: for cross-track refs we use "track_id.task_id"
25
- // Within a track, bare "task_id" is also valid
26
- function qualifyId(trackId: string, taskId: string): string {
27
- return `${trackId}.${taskId}`;
28
- }
29
-
30
- export function buildDag(config: PipelineConfig): Dag {
31
- const nodes = new Map<string, DagNode>();
32
- // Map bare task IDs to qualified IDs (for resolving unqualified refs)
33
- const bareToQualified = new Map<string, string>();
34
-
35
- // 1. Register all nodes
36
- for (const track of config.tracks) {
37
- for (const task of track.tasks) {
38
- const qid = qualifyId(track.id, task.id);
39
-
40
- if (nodes.has(qid)) {
41
- throw new Error(`Duplicate task ID: "${qid}"`);
42
- }
43
-
44
- // Track bare ID → qualified. If same bare ID in multiple tracks, mark ambiguous
45
- if (bareToQualified.has(task.id)) {
46
- bareToQualified.set(task.id, '__ambiguous__');
47
- } else {
48
- bareToQualified.set(task.id, qid);
49
- }
50
-
51
- nodes.set(qid, {
52
- taskId: qid,
53
- task,
54
- track,
55
- dependsOn: [], // filled below
56
- });
57
- }
58
- }
59
-
60
- // Helper to resolve a dependency ref to a qualified ID
61
- function resolveRef(ref: string, fromTrackId: string): string {
62
- // Already qualified (contains dot)
63
- if (ref.includes('.')) {
64
- if (!nodes.has(ref)) {
65
- throw new Error(`Task reference "${ref}" not found`);
66
- }
67
- return ref;
68
- }
69
- // Try within same track first
70
- const sameTrack = qualifyId(fromTrackId, ref);
71
- if (nodes.has(sameTrack)) return sameTrack;
72
- // Try global bare lookup
73
- const global = bareToQualified.get(ref);
74
- if (global && global !== '__ambiguous__') return global;
75
- if (global === '__ambiguous__') {
76
- throw new Error(
77
- `Ambiguous task reference "${ref}" exists in multiple tracks. ` +
78
- `Use "track_id.task_id" format.`
79
- );
80
- }
81
- throw new Error(`Task reference "${ref}" not found`);
82
- }
83
-
84
- // 2. Resolve depends_on and continue_from to qualified IDs
85
- for (const track of config.tracks) {
86
- for (const task of track.tasks) {
87
- const qid = qualifyId(track.id, task.id);
88
- const deps: string[] = [];
89
- let resolvedContinueFrom: string | undefined;
90
-
91
- if (task.depends_on) {
92
- for (const dep of task.depends_on) {
93
- deps.push(resolveRef(dep, track.id));
94
- }
95
- }
96
- if (task.continue_from) {
97
- let resolved: string;
98
- try {
99
- resolved = resolveRef(task.continue_from, track.id);
100
- } catch {
101
- throw new Error(
102
- `Task "${qid}": continue_from "${task.continue_from}" — no such task found. ` +
103
- `Use a fully-qualified reference (trackId.taskId) or ensure the target task exists.`
104
- );
105
- }
106
- resolvedContinueFrom = resolved;
107
- if (!deps.includes(resolved)) {
108
- deps.push(resolved); // continue_from implies dependency
109
- }
110
- }
111
-
112
- // Replace node with resolved deps + qualified continue_from.
113
- const node = nodes.get(qid)!;
114
- nodes.set(qid, { ...node, dependsOn: deps, resolvedContinueFrom });
115
- }
116
- }
117
-
118
- // 3. Topological sort + cycle detection (Kahn's algorithm)
119
- const inDegree = new Map<string, number>();
120
- const adjacency = new Map<string, string[]>(); // parent → children
121
-
122
- for (const [id] of nodes) {
123
- inDegree.set(id, 0);
124
- adjacency.set(id, []);
125
- }
126
-
127
- for (const [id, node] of nodes) {
128
- for (const dep of node.dependsOn) {
129
- adjacency.get(dep)!.push(id);
130
- inDegree.set(id, (inDegree.get(id) ?? 0) + 1);
131
- }
132
- }
133
-
134
- const queue: string[] = [];
135
- for (const [id, degree] of inDegree) {
136
- if (degree === 0) queue.push(id);
137
- }
138
-
139
- const sorted: string[] = [];
140
- // Use an index pointer instead of shift() to avoid O(n) per dequeue.
141
- let qi = 0;
142
- while (qi < queue.length) {
143
- const current = queue[qi++]!;
144
- sorted.push(current);
145
- for (const child of adjacency.get(current)!) {
146
- const newDegree = inDegree.get(child)! - 1;
147
- inDegree.set(child, newDegree);
148
- if (newDegree === 0) queue.push(child);
149
- }
150
- }
151
-
152
- if (sorted.length !== nodes.size) {
153
- // Only report nodes that are actually part of cycles (in-degree > 0
154
- // after Kahn's algorithm), not their downstream dependents.
155
- const sortedSet = new Set(sorted);
156
- const cycleMembers = [...nodes.keys()].filter(id =>
157
- !sortedSet.has(id) && (inDegree.get(id) ?? 0) > 0
158
- );
159
- throw new Error(`Circular dependency detected involving tasks: ${cycleMembers.join(', ')}`);
160
- }
161
-
162
- return { nodes, sorted };
163
- }
164
-
165
- // ═══ Raw DAG (for visual editor no workDir required) ═══
166
-
167
- export interface RawDagNode {
168
- readonly taskId: string; // fully qualified: track_id.task_id
169
- readonly trackId: string;
170
- readonly rawTask: RawTaskConfig;
171
- readonly dependsOn: readonly string[]; // fully qualified IDs, best-effort resolved
172
- }
173
-
174
- export interface RawDag {
175
- readonly nodes: ReadonlyMap<string, RawDagNode>;
176
- /** Directed edges: from → to means "from must complete before to starts" */
177
- readonly edges: readonly { readonly from: string; readonly to: string }[];
178
- }
179
-
180
- /**
1
+ import type {
2
+ PipelineConfig,
3
+ RawPipelineConfig,
4
+ RawTaskConfig,
5
+ TaskConfig,
6
+ TrackConfig,
7
+ } from './types';
8
+
9
+ export interface DagNode {
10
+ readonly taskId: string; // fully qualified: track_id.task_id or just task_id
11
+ readonly task: TaskConfig;
12
+ readonly track: TrackConfig;
13
+ readonly dependsOn: readonly string[];
14
+ /**
15
+ * H1: `task.continue_from` may be written by users as a bare task id
16
+ * (e.g. `review`) or a same-track shorthand. The driver needs the
17
+ * fully-qualified upstream id to look up output/session/normalized maps
18
+ * deterministically — bare lookups race when two tracks happen to share
19
+ * a task name. dag.ts performs the qualification once, here, so the
20
+ * engine never has to.
21
+ */
22
+ readonly resolvedContinueFrom?: string;
23
+ }
24
+
25
+ export interface Dag {
26
+ readonly nodes: ReadonlyMap<string, DagNode>;
27
+ readonly sorted: readonly string[]; // topological order
28
+ }
29
+
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
+ export function buildDag(config: PipelineConfig): Dag {
37
+ 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
+
41
+ // 1. Register all nodes
42
+ for (const track of config.tracks) {
43
+ for (const task of track.tasks) {
44
+ const qid = qualifyId(track.id, task.id);
45
+
46
+ if (nodes.has(qid)) {
47
+ throw new Error(`Duplicate task ID: "${qid}"`);
48
+ }
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
+ nodes.set(qid, {
58
+ taskId: qid,
59
+ task,
60
+ track,
61
+ dependsOn: [], // filled below
62
+ });
63
+ }
64
+ }
65
+
66
+ // Helper to resolve a dependency ref to a qualified ID
67
+ function resolveRef(ref: string, fromTrackId: string): string {
68
+ // Already qualified (contains dot)
69
+ if (ref.includes('.')) {
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__') {
82
+ throw new Error(
83
+ `Ambiguous task reference "${ref}" exists in multiple tracks. ` +
84
+ `Use "track_id.task_id" format.`,
85
+ );
86
+ }
87
+ throw new Error(`Task reference "${ref}" not found`);
88
+ }
89
+
90
+ // 2. Resolve depends_on and continue_from to qualified IDs
91
+ for (const track of config.tracks) {
92
+ for (const task of track.tasks) {
93
+ const qid = qualifyId(track.id, task.id);
94
+ const deps: string[] = [];
95
+ let resolvedContinueFrom: string | undefined;
96
+
97
+ if (task.depends_on) {
98
+ for (const dep of task.depends_on) {
99
+ deps.push(resolveRef(dep, track.id));
100
+ }
101
+ }
102
+ if (task.continue_from) {
103
+ let resolved: string;
104
+ try {
105
+ resolved = resolveRef(task.continue_from, track.id);
106
+ } catch {
107
+ throw new Error(
108
+ `Task "${qid}": continue_from "${task.continue_from}" — no such task found. ` +
109
+ `Use a fully-qualified reference (trackId.taskId) or ensure the target task exists.`,
110
+ );
111
+ }
112
+ resolvedContinueFrom = resolved;
113
+ if (!deps.includes(resolved)) {
114
+ deps.push(resolved); // continue_from implies dependency
115
+ }
116
+ }
117
+
118
+ // Replace node with resolved deps + qualified continue_from.
119
+ const node = nodes.get(qid)!;
120
+ nodes.set(qid, { ...node, dependsOn: deps, resolvedContinueFrom });
121
+ }
122
+ }
123
+
124
+ // 3. Topological sort + cycle detection (Kahn's algorithm)
125
+ const inDegree = new Map<string, number>();
126
+ const adjacency = new Map<string, string[]>(); // parent → children
127
+
128
+ for (const [id] of nodes) {
129
+ inDegree.set(id, 0);
130
+ adjacency.set(id, []);
131
+ }
132
+
133
+ for (const [id, node] of nodes) {
134
+ for (const dep of node.dependsOn) {
135
+ adjacency.get(dep)!.push(id);
136
+ inDegree.set(id, (inDegree.get(id) ?? 0) + 1);
137
+ }
138
+ }
139
+
140
+ const queue: string[] = [];
141
+ for (const [id, degree] of inDegree) {
142
+ if (degree === 0) queue.push(id);
143
+ }
144
+
145
+ const sorted: string[] = [];
146
+ // Use an index pointer instead of shift() to avoid O(n) per dequeue.
147
+ let qi = 0;
148
+ while (qi < queue.length) {
149
+ const current = queue[qi++]!;
150
+ sorted.push(current);
151
+ for (const child of adjacency.get(current)!) {
152
+ const newDegree = inDegree.get(child)! - 1;
153
+ inDegree.set(child, newDegree);
154
+ if (newDegree === 0) queue.push(child);
155
+ }
156
+ }
157
+
158
+ if (sorted.length !== nodes.size) {
159
+ // Only report nodes that are actually part of cycles (in-degree > 0
160
+ // after Kahn's algorithm), not their downstream dependents.
161
+ const sortedSet = new Set(sorted);
162
+ const cycleMembers = [...nodes.keys()].filter(
163
+ (id) => !sortedSet.has(id) && (inDegree.get(id) ?? 0) > 0,
164
+ );
165
+ throw new Error(`Circular dependency detected involving tasks: ${cycleMembers.join(', ')}`);
166
+ }
167
+
168
+ return { nodes, sorted };
169
+ }
170
+
171
+ // ═══ Raw DAG (for visual editor no workDir required) ═══
172
+
173
+ export interface RawDagNode {
174
+ readonly taskId: string; // fully qualified: track_id.task_id
175
+ readonly trackId: string;
176
+ readonly rawTask: RawTaskConfig;
177
+ readonly dependsOn: readonly string[]; // fully qualified IDs, best-effort resolved
178
+ }
179
+
180
+ export interface RawDag {
181
+ readonly nodes: ReadonlyMap<string, RawDagNode>;
182
+ /** Directed edges: from → to means "from must complete before to starts" */
183
+ readonly edges: readonly { readonly from: string; readonly to: string }[];
184
+ }
185
+
186
+ /**
181
187
  * Build a lightweight DAG from a raw (unresolved) pipeline config.
182
188
  * Unlike buildDag, this function:
183
189
  * - Does not require a workDir or resolved PipelineConfig
@@ -186,60 +192,60 @@ export interface RawDag {
186
192
  * Intended for the visual editor to render the flow graph before a pipeline is run.
187
193
  */
188
194
  export function buildRawDag(config: RawPipelineConfig): RawDag {
189
- const nodes = new Map<string, RawDagNode>();
190
- const bareToQualified = new Map<string, string>();
191
-
195
+ const nodes = new Map<string, RawDagNode>();
196
+ const bareToQualified = new Map<string, string>();
197
+
192
198
  // 1. Register all concrete tasks
193
199
  for (const track of config.tracks) {
194
200
  for (const task of track.tasks) {
195
201
  const qid = `${track.id}.${task.id}`;
196
202
  if (nodes.has(qid)) continue; // skip duplicates silently
197
-
198
- if (bareToQualified.has(task.id)) {
199
- bareToQualified.set(task.id, '__ambiguous__');
200
- } else {
201
- bareToQualified.set(task.id, qid);
202
- }
203
- nodes.set(qid, { taskId: qid, trackId: track.id, rawTask: task, dependsOn: [] });
204
- }
205
- }
206
-
207
- // 2. Resolve dependency refs leniently (missing / ambiguous refs are skipped)
208
- function tryResolve(ref: string, fromTrackId: string): string | null {
209
- if (ref.includes('.')) return nodes.has(ref) ? ref : null;
210
- const sameTrack = `${fromTrackId}.${ref}`;
211
- if (nodes.has(sameTrack)) return sameTrack;
212
- const global = bareToQualified.get(ref);
213
- if (global && global !== '__ambiguous__') return global;
214
- return null;
215
- }
216
-
217
- const edges: { from: string; to: string }[] = [];
218
-
203
+
204
+ if (bareToQualified.has(task.id)) {
205
+ bareToQualified.set(task.id, '__ambiguous__');
206
+ } else {
207
+ bareToQualified.set(task.id, qid);
208
+ }
209
+ nodes.set(qid, { taskId: qid, trackId: track.id, rawTask: task, dependsOn: [] });
210
+ }
211
+ }
212
+
213
+ // 2. Resolve dependency refs leniently (missing / ambiguous refs are skipped)
214
+ function tryResolve(ref: string, fromTrackId: string): string | null {
215
+ if (ref.includes('.')) return nodes.has(ref) ? ref : null;
216
+ const sameTrack = `${fromTrackId}.${ref}`;
217
+ if (nodes.has(sameTrack)) return sameTrack;
218
+ const global = bareToQualified.get(ref);
219
+ if (global && global !== '__ambiguous__') return global;
220
+ return null;
221
+ }
222
+
223
+ const edges: { from: string; to: string }[] = [];
224
+
219
225
  for (const track of config.tracks) {
220
226
  for (const task of track.tasks) {
221
227
  const qid = `${track.id}.${task.id}`;
222
228
  const deps: string[] = [];
223
-
224
- for (const ref of task.depends_on ?? []) {
225
- const resolved = tryResolve(ref, track.id);
226
- if (resolved && !deps.includes(resolved)) {
227
- deps.push(resolved);
228
- edges.push({ from: resolved, to: qid });
229
- }
230
- }
231
- if (task.continue_from) {
232
- const resolved = tryResolve(task.continue_from, track.id);
233
- if (resolved && !deps.includes(resolved)) {
234
- deps.push(resolved);
235
- edges.push({ from: resolved, to: qid });
236
- }
237
- }
238
-
239
- const node = nodes.get(qid)!;
240
- nodes.set(qid, { ...node, dependsOn: deps });
241
- }
242
- }
243
-
244
- return { nodes, edges };
245
- }
229
+
230
+ for (const ref of task.depends_on ?? []) {
231
+ const resolved = tryResolve(ref, track.id);
232
+ if (resolved && !deps.includes(resolved)) {
233
+ deps.push(resolved);
234
+ edges.push({ from: resolved, to: qid });
235
+ }
236
+ }
237
+ if (task.continue_from) {
238
+ const resolved = tryResolve(task.continue_from, track.id);
239
+ if (resolved && !deps.includes(resolved)) {
240
+ deps.push(resolved);
241
+ edges.push({ from: resolved, to: qid });
242
+ }
243
+ }
244
+
245
+ const node = nodes.get(qid)!;
246
+ nodes.set(qid, { ...node, dependsOn: deps });
247
+ }
248
+ }
249
+
250
+ return { nodes, edges };
251
+ }