@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 +2 -2
- package/package.json +1 -1
- package/src/config-ops.ts +16 -6
- package/src/engine.ts +23 -4
- package/src/pipeline-runner.ts +18 -3
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
|
|
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
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
|
|
153
|
-
// (same-track lookup takes priority
|
|
154
|
-
// - Bare ref from a
|
|
155
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|
package/src/pipeline-runner.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|