@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.
Files changed (67) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +569 -569
  3. package/dist/dag.d.ts.map +1 -1
  4. package/dist/dag.js +58 -69
  5. package/dist/dag.js.map +1 -1
  6. package/dist/engine.d.ts.map +1 -1
  7. package/dist/engine.js +63 -37
  8. package/dist/engine.js.map +1 -1
  9. package/dist/middlewares/static-context.d.ts.map +1 -1
  10. package/dist/middlewares/static-context.js +7 -3
  11. package/dist/middlewares/static-context.js.map +1 -1
  12. package/dist/prompt-doc.d.ts +36 -0
  13. package/dist/prompt-doc.d.ts.map +1 -0
  14. package/dist/prompt-doc.js +44 -0
  15. package/dist/prompt-doc.js.map +1 -0
  16. package/dist/registry.d.ts.map +1 -1
  17. package/dist/registry.js +11 -0
  18. package/dist/registry.js.map +1 -1
  19. package/dist/sdk.d.ts +3 -0
  20. package/dist/sdk.d.ts.map +1 -1
  21. package/dist/sdk.js +4 -0
  22. package/dist/sdk.js.map +1 -1
  23. package/dist/task-ref.d.ts +55 -0
  24. package/dist/task-ref.d.ts.map +1 -0
  25. package/dist/task-ref.js +101 -0
  26. package/dist/task-ref.js.map +1 -0
  27. package/dist/task-ref.test.d.ts +2 -0
  28. package/dist/task-ref.test.d.ts.map +1 -0
  29. package/dist/task-ref.test.js +364 -0
  30. package/dist/task-ref.test.js.map +1 -0
  31. package/dist/templates.d.ts +20 -0
  32. package/dist/templates.d.ts.map +1 -0
  33. package/dist/templates.js +93 -0
  34. package/dist/templates.js.map +1 -0
  35. package/dist/validate-raw.d.ts.map +1 -1
  36. package/dist/validate-raw.js +27 -53
  37. package/dist/validate-raw.js.map +1 -1
  38. package/package.json +2 -2
  39. package/scripts/preinstall.js +31 -31
  40. package/src/adapters/stdin-approval.ts +106 -106
  41. package/src/adapters/websocket-approval.ts +224 -224
  42. package/src/approval.ts +131 -131
  43. package/src/bootstrap.ts +37 -37
  44. package/src/completions/exit-code.ts +34 -34
  45. package/src/completions/file-exists.ts +66 -66
  46. package/src/completions/output-check.ts +86 -86
  47. package/src/config-ops.ts +307 -307
  48. package/src/dag.ts +61 -67
  49. package/src/drivers/claude-code.ts +250 -250
  50. package/src/engine.ts +1137 -1098
  51. package/src/hooks.ts +187 -187
  52. package/src/logger.ts +182 -182
  53. package/src/middlewares/static-context.ts +49 -45
  54. package/src/pipeline-runner.ts +156 -156
  55. package/src/prompt-doc.ts +49 -0
  56. package/src/registry.ts +255 -242
  57. package/src/runner.ts +395 -395
  58. package/src/schema.test.ts +101 -101
  59. package/src/schema.ts +338 -338
  60. package/src/sdk.ts +111 -92
  61. package/src/task-ref.test.ts +401 -0
  62. package/src/task-ref.ts +120 -0
  63. package/src/triggers/file.ts +164 -164
  64. package/src/triggers/manual.ts +86 -86
  65. package/src/types.ts +18 -18
  66. package/src/utils.ts +203 -203
  67. 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 = qualifyId(track.id, task.id);
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
- // Helper to resolve a dependency ref to a qualified ID
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
- // 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__') {
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
- throw new Error(`Task reference "${ref}" not found`);
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 = qualifyId(track.id, task.id);
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
- let resolved: string;
104
- try {
105
- resolved = resolveRef(task.continue_from, track.id);
106
- } catch {
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 = resolved;
113
- if (!deps.includes(resolved)) {
114
- deps.push(resolved); // continue_from implies dependency
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
- const queue: string[] = [];
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) queue.push(id);
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 < queue.length) {
149
- const current = queue[qi++]!;
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) queue.push(child);
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 = `${track.id}.${task.id}`;
202
- if (nodes.has(qid)) continue; // skip duplicates silently
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
- // 2. Resolve dependency refs leniently (missing / ambiguous refs are skipped)
209
+ const index = buildTaskIndex(config);
210
+
214
211
  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;
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 = `${track.id}.${task.id}`;
221
+ const qid = qualifyTaskId(track.id, task.id);
228
222
  const deps: string[] = [];
229
223
 
230
224
  for (const ref of task.depends_on ?? []) {