@tagma/sdk 0.1.9 → 0.2.1

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 CHANGED
@@ -117,7 +117,7 @@ Options:
117
117
  - `pipeline_start` — pipeline began; includes `states: ReadonlyMap<taskId, TaskState>` (initial snapshot of all tasks at `waiting`)
118
118
  - `task_status_change` — a task changed status; includes `state: TaskState` (complete snapshot at the time of change: `startedAt` is populated before the `running` event; `result` and `finishedAt` are populated before any terminal-status event)
119
119
  - `pipeline_end` — pipeline finished; includes `success: boolean`
120
- - `maxLogRuns` -- number of per-run log directories to keep under `<workDir>/logs/` (default: 20)
120
+ - `maxLogRuns` -- number of per-run log directories to keep under `<workDir>/.tagma/logs/` (default: 20)
121
121
 
122
122
  ### `PipelineRunner`
123
123
 
@@ -260,4 +260,4 @@ Use `buildDag` instead when you have a fully resolved `PipelineConfig` and need
260
260
 
261
261
  ## License
262
262
 
263
- MIT
263
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tagma/sdk",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  "plugins/*"
package/src/config-ops.ts CHANGED
@@ -140,6 +140,12 @@ export function removeTask(
140
140
 
141
141
  const qualId = `${trackId}.${taskId}`;
142
142
 
143
+ // After deletion, can a bare ref "taskId" still resolve to some other task globally?
144
+ // It can if any track in the post-deletion config still contains a task with that bare id.
145
+ const bareIdSurvivesGlobally = withoutTask.tracks.some(t =>
146
+ t.tasks.some(tk => tk.id === taskId),
147
+ );
148
+
143
149
  return {
144
150
  ...withoutTask,
145
151
  tracks: withoutTask.tracks.map(t => {
@@ -149,15 +155,19 @@ export function removeTask(
149
155
 
150
156
  // Resolve whether a ref in THIS track points to the deleted task:
151
157
  // - Fully-qualified ref ("trackId.taskId") — always points to the deleted task.
152
- // - Bare ref ("taskId") from the same track always pointed to the deleted task
153
- // (same-track lookup takes priority, and the task was in this track).
154
- // - Bare ref from a different track — points to the deleted task only if this
155
- // track has no task with that same id (no local task to shadow it).
158
+ // - Bare ref ("taskId") from the SAME track as the deleted task always pointed
159
+ // to the deleted task (same-track lookup takes priority).
160
+ // - Bare ref from a DIFFERENT track:
161
+ // 1. If this track has a local task with that id ref resolves locally, not removed.
162
+ // 2. Else if some other track still has a task with that id → ref will resolve
163
+ // there after deletion, not removed.
164
+ // 3. Else → ref is dangling, remove it.
156
165
  const isRemovedFrom = (ref: string): boolean => {
157
166
  if (ref === qualId) return true;
158
167
  if (ref === taskId) {
159
168
  if (t.id === trackId) return true; // same track — was pointing here
160
- return !remainingIds.has(taskId); // cross-track only if no local override
169
+ if (remainingIds.has(taskId)) return false; // local task shadows ref is fine
170
+ return !bareIdSurvivesGlobally; // remove only if truly dangling
161
171
  }
162
172
  return false;
163
173
  };
@@ -236,4 +246,4 @@ export function transferTask(
236
246
  };
237
247
  if (!task) return config;
238
248
  return upsertTask(afterRemove, toTrackId, task);
239
- }
249
+ }
package/src/engine.ts CHANGED
@@ -29,7 +29,10 @@ function preflight(config: PipelineConfig, dag: Dag): void {
29
29
  const track = node.track;
30
30
  const driverName = task.driver ?? track.driver ?? config.driver ?? 'claude-code';
31
31
 
32
- if (!hasHandler('drivers', driverName)) {
32
+ // Pure command tasks don't use a driver — skip driver registration check.
33
+ const isCommandOnly = task.command && !task.prompt;
34
+
35
+ if (!isCommandOnly && !hasHandler('drivers', driverName)) {
33
36
  errors.push(`Task "${node.taskId}": driver "${driverName}" not registered`);
34
37
  }
35
38
 
@@ -117,7 +120,7 @@ export type PipelineEvent =
117
120
  export interface RunPipelineOptions {
118
121
  readonly approvalGateway?: ApprovalGateway;
119
122
  /**
120
- * Maximum number of per-run log directories to retain under `<workDir>/logs/`.
123
+ * Maximum number of per-run log directories to retain under `<workDir>/.tagma/logs/`.
121
124
  * Oldest directories are deleted after each run. Defaults to 20. Set to 0 to disable cleanup.
122
125
  */
123
126
  readonly maxLogRuns?: number;
@@ -207,7 +210,7 @@ export async function runPipeline(
207
210
  runId,
208
211
  logPath: log.path,
209
212
  summary: { total: dag.nodes.size, success: 0, failed: 0, skipped: 0, timeout: 0, blocked: 0 },
210
- states,
213
+ states: freezeStates(states),
211
214
  };
212
215
  }
213
216
 
@@ -697,7 +700,7 @@ export async function runPipeline(
697
700
  console.log(` Log: ${log.path}`);
698
701
 
699
702
  emit({ type: 'pipeline_end', runId, success: allSuccess });
700
- return { success: allSuccess, runId, logPath: log.path, summary, states };
703
+ return { success: allSuccess, runId, logPath: log.path, summary, states: freezeStates(states) };
701
704
 
702
705
  } finally {
703
706
  // Prune old per-run log directories on every exit path (normal, blocked, or thrown).
@@ -741,3 +744,19 @@ function isTerminal(status: TaskStatus): boolean {
741
744
  return status === 'success' || status === 'failed' || status === 'timeout'
742
745
  || status === 'skipped' || status === 'blocked';
743
746
  }
747
+
748
+ /** Return a deep-copied, caller-safe snapshot of the states map. */
749
+ function freezeStates(states: Map<string, TaskState>): ReadonlyMap<string, TaskState> {
750
+ const copy = new Map<string, TaskState>();
751
+ for (const [id, s] of states) {
752
+ copy.set(id, {
753
+ config: { ...s.config },
754
+ trackConfig: { ...s.trackConfig },
755
+ status: s.status,
756
+ result: s.result ? { ...s.result } : null,
757
+ startedAt: s.startedAt,
758
+ finishedAt: s.finishedAt,
759
+ });
760
+ }
761
+ return copy;
762
+ }
@@ -108,9 +108,8 @@ export class PipelineRunner {
108
108
  * Returns null only if the pipeline has never started.
109
109
  */
110
110
  getStates(): ReadonlyMap<string, TaskState> | null {
111
- if (this._states) return this._states;
112
- // Return a snapshot copy so callers cannot mutate SDK-internal state.
113
- if (this._statesMirror.size > 0) return new Map([...this._statesMirror]);
111
+ if (this._states) return snapshotStates(this._states);
112
+ if (this._statesMirror.size > 0) return snapshotStates(this._statesMirror);
114
113
  return null;
115
114
  }
116
115
 
@@ -124,3 +123,19 @@ export class PipelineRunner {
124
123
  return () => this._handlers.delete(handler);
125
124
  }
126
125
  }
126
+
127
+ /** Deep-copy a states map so callers cannot mutate SDK internals. */
128
+ function snapshotStates(src: ReadonlyMap<string, TaskState>): ReadonlyMap<string, TaskState> {
129
+ const copy = new Map<string, TaskState>();
130
+ for (const [id, s] of src) {
131
+ copy.set(id, {
132
+ config: { ...s.config },
133
+ trackConfig: { ...s.trackConfig },
134
+ status: s.status,
135
+ result: s.result ? { ...s.result } : null,
136
+ startedAt: s.startedAt,
137
+ finishedAt: s.finishedAt,
138
+ });
139
+ }
140
+ return copy;
141
+ }