builderman 1.3.0 → 1.5.0

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
@@ -10,28 +10,30 @@ It is designed for monorepos, long-running development processes, and CI/CD pipe
10
10
 
11
11
  ## Table of Contents
12
12
 
13
- - [Key Features](#key-features)
14
- - [Installation](#installation)
15
- - [Quick Start](#quick-start)
16
- - [Core Concepts](#core-concepts)
17
- - [Tasks](#tasks)
18
- - [Commands & Modes](#commands--modes)
19
- - [Dependencies](#dependencies)
20
- - [Pipelines](#pipelines)
21
- - [Pipeline Composition](#pipeline-composition)
22
- - [Error Handling Guarantees](#error-handling-guarantees)
23
- - [Cancellation](#cancellation)
24
- - [Teardown](#teardown)
25
- - [Basic Teardown](#basic-teardown)
26
- - [Teardown Callbacks](#teardown-callbacks)
27
- - [Teardown Execution Rules](#teardown-execution-rules)
28
- - [Skipping Tasks](#skipping-tasks)
29
- - [Strict Mode](#strict-mode)
30
- - [Task-Level Skip Override](#task-level-skip-override)
31
- - [Execution Statistics](#execution-statistics)
32
- - [Pipeline Statistics](#pipeline-statistics)
33
- - [Task Statistics](#task-statistics)
34
- - [When Should I Use builderman?](#when-should-i-use-builderman)
13
+ > - [Key Features](#key-features)
14
+ > - [Installation](#installation)
15
+ > - [Quick Start](#quick-start)
16
+ > - [Core Concepts](#core-concepts)
17
+ > - [Tasks](#tasks)
18
+ > - [Commands & Modes](#commands--modes)
19
+ > - [Environment Variables](#environment-variables)
20
+ > - [Dependencies](#dependencies)
21
+ > - [Pipelines](#pipelines)
22
+ > - [Concurrency Control](#concurrency-control)
23
+ > - [Pipeline Composition](#pipeline-composition)
24
+ > - [Error Handling Guarantees](#error-handling-guarantees)
25
+ > - [Cancellation](#cancellation)
26
+ > - [Teardown](#teardown)
27
+ > - [Basic Teardown](#basic-teardown)
28
+ > - [Teardown Callbacks](#teardown-callbacks)
29
+ > - [Teardown Execution Rules](#teardown-execution-rules)
30
+ > - [Skipping Tasks](#skipping-tasks)
31
+ > - [Strict Mode](#strict-mode)
32
+ > - [Task-Level Skip Override](#task-level-skip-override)
33
+ > - [Execution Statistics](#execution-statistics)
34
+ > - [Pipeline Statistics](#pipeline-statistics)
35
+ > - [Task Statistics](#task-statistics)
36
+ > - [When Should I Use builderman?](#when-should-i-use-builderman)
35
37
 
36
38
  ## Key Features
37
39
 
@@ -62,17 +64,19 @@ import { task, pipeline } from "builderman"
62
64
  const build = task({
63
65
  name: "build",
64
66
  commands: { build: "tsc" },
65
- cwd: ".",
67
+ cwd: "packages/my-package", // Optional: defaults to "."
66
68
  })
67
69
 
68
70
  const test = task({
69
71
  name: "test",
70
72
  commands: { build: "npm test" },
71
- cwd: ".",
72
73
  dependencies: [build],
74
+ cwd: "packages/my-package",
73
75
  })
74
76
 
75
- const result = await pipeline([build, test]).run()
77
+ const result = await pipeline([build, test]).run({
78
+ command: "build",
79
+ })
76
80
 
77
81
  if (!result.ok) {
78
82
  console.error("Pipeline failed:", result.error.message)
@@ -93,6 +97,7 @@ A **task** represents a unit of work. Each task:
93
97
  - Defines commands for one or more modes
94
98
  - May depend on other tasks
95
99
  - May register teardown logic
100
+ - Has an optional working directory (`cwd`, defaults to `"."`)
96
101
 
97
102
  ```ts
98
103
  import { task } from "builderman"
@@ -130,6 +135,89 @@ Commands may be:
130
135
  - `run`: the command to execute
131
136
  - `readyWhen`: a predicate that marks the task as ready
132
137
  - `teardown`: cleanup logic to run after completion
138
+ - `env`: environment variables specific to this command
139
+
140
+ ---
141
+
142
+ ### Environment Variables
143
+
144
+ Environment variables can be provided at multiple levels, with more specific levels overriding less specific ones:
145
+
146
+ **Precedence order (highest to lowest):**
147
+
148
+ 1. Command-level `env` (in command config)
149
+ 2. Task-level `env` (in task config)
150
+ 3. Pipeline-level `env` (in `pipeline.run()`)
151
+ 4. Process environment variables
152
+
153
+ #### Command-Level Environment Variables
154
+
155
+ ```ts
156
+ const apiTask = task({
157
+ name: "api",
158
+ commands: {
159
+ dev: {
160
+ run: "npm run dev",
161
+ env: {
162
+ PORT: "3000",
163
+ NODE_ENV: "development",
164
+ },
165
+ },
166
+ },
167
+ })
168
+ ```
169
+
170
+ #### Task-Level Environment Variables
171
+
172
+ ```ts
173
+ const apiTask = task({
174
+ name: "api",
175
+ commands: {
176
+ dev: "npm run dev",
177
+ build: "npm run build",
178
+ },
179
+ env: {
180
+ API_URL: "http://localhost:3000",
181
+ LOG_LEVEL: "debug",
182
+ },
183
+ })
184
+ ```
185
+
186
+ #### Pipeline-Level Environment Variables
187
+
188
+ ```ts
189
+ const result = await pipeline([apiTask]).run({
190
+ env: {
191
+ DATABASE_URL: "postgres://localhost/mydb",
192
+ REDIS_URL: "redis://localhost:6379",
193
+ },
194
+ })
195
+ ```
196
+
197
+ #### Nested Pipeline Environment Variables
198
+
199
+ When converting a pipeline to a task, you can provide environment variables that will be merged with the outer pipeline's environment:
200
+
201
+ ```ts
202
+ const innerPipeline = pipeline([
203
+ /* ... */
204
+ ])
205
+ const innerTask = innerPipeline.toTask({
206
+ name: "inner",
207
+ env: {
208
+ INNER_VAR: "inner-value",
209
+ },
210
+ })
211
+
212
+ const outerPipeline = pipeline([innerTask])
213
+ const result = await outerPipeline.run({
214
+ env: {
215
+ OUTER_VAR: "outer-value",
216
+ },
217
+ })
218
+ ```
219
+
220
+ In this example, tasks in `innerPipeline` will receive both `INNER_VAR` and `OUTER_VAR`, with `INNER_VAR` taking precedence if there's a conflict.
133
221
 
134
222
  ---
135
223
 
@@ -169,6 +257,31 @@ const result = await pipeline([libTask, consumerTask]).run({
169
257
  })
170
258
  ```
171
259
 
260
+ #### Concurrency Control
261
+
262
+ By default, pipelines run as many tasks concurrently as possible (limited only by dependencies). You can limit concurrent execution using `maxConcurrency`:
263
+
264
+ ```ts
265
+ const result = await pipeline([task1, task2, task3, task4, task5]).run({
266
+ maxConcurrency: 2, // At most 2 tasks will run simultaneously
267
+ })
268
+ ```
269
+
270
+ When `maxConcurrency` is set:
271
+
272
+ - Tasks that are ready to run (dependencies satisfied) will start up to the limit
273
+ - As tasks complete, new ready tasks will start to maintain the concurrency limit
274
+ - Dependencies are still respected — a task won't start until its dependencies complete
275
+
276
+ This is useful for:
277
+
278
+ - Limiting resource usage (CPU, memory, network)
279
+ - Controlling database connection pools
280
+ - Managing API rate limits
281
+ - Reducing system load in CI environments
282
+
283
+ If `maxConcurrency` is not specified, there is no limit (tasks run concurrently as dependencies allow).
284
+
172
285
  ---
173
286
 
174
287
  ### Pipeline Composition
@@ -187,7 +300,11 @@ const deploy = pipeline([
187
300
  ])
188
301
 
189
302
  const buildTask = build.toTask({ name: "build" })
190
- const testTask = test.toTask({ name: "test", dependencies: [buildTask] })
303
+ const testTask = test.toTask({
304
+ name: "test",
305
+ dependencies: [buildTask],
306
+ env: { TEST_ENV: "test-value" }, // Optional: env for nested pipeline
307
+ })
191
308
  const deployTask = deploy.toTask({ name: "deploy", dependencies: [testTask] })
192
309
 
193
310
  const ci = pipeline([buildTask, testTask, deployTask])
@@ -215,16 +332,19 @@ if (!result.ok) {
215
332
  console.error("Pipeline was cancelled")
216
333
  break
217
334
  case PipelineError.TaskFailed:
218
- console.error(`Task failed: ${result.error.message}`)
335
+ console.error("Task failed:", result.error.message)
336
+ break
337
+ case PipelineError.TaskReadyTimeout:
338
+ console.error("Task was not ready in time:", result.error.message)
339
+ break
340
+ case PipelineError.TaskCompletedTimeout:
341
+ console.error("Task did not complete in time:", result.error.message)
219
342
  break
220
343
  case PipelineError.ProcessTerminated:
221
- console.error("Process was terminated")
344
+ console.error("Process terminated:", result.error.message)
222
345
  break
223
346
  case PipelineError.InvalidTask:
224
- console.error(`Invalid task configuration: ${result.error.message}`)
225
- break
226
- case PipelineError.InvalidSignal:
227
- console.error("Invalid abort signal")
347
+ console.error("Invalid task configuration:", result.error.message)
228
348
  break
229
349
  }
230
350
  }
@@ -278,7 +398,6 @@ const dbTask = task({
278
398
  },
279
399
  build: "echo build",
280
400
  },
281
- cwd: ".",
282
401
  })
283
402
  ```
284
403
 
@@ -336,7 +455,6 @@ const dbTask = task({
336
455
  commands: {
337
456
  dev: "docker-compose up",
338
457
  },
339
- cwd: ".",
340
458
  })
341
459
 
342
460
  const apiTask = task({
@@ -345,7 +463,6 @@ const apiTask = task({
345
463
  dev: "npm run dev",
346
464
  build: "npm run build",
347
465
  },
348
- cwd: ".",
349
466
  dependencies: [dbTask],
350
467
  })
351
468
 
@@ -386,7 +503,6 @@ const dbTask = task({
386
503
  commands: {
387
504
  dev: "docker-compose up",
388
505
  },
389
- cwd: ".",
390
506
  allowSkip: true,
391
507
  })
392
508
 
@@ -0,0 +1,12 @@
1
+ export type PipelineErrorCode = typeof PipelineError.Aborted | typeof PipelineError.ProcessTerminated | typeof PipelineError.TaskFailed | typeof PipelineError.TaskCompletedTimeout | typeof PipelineError.TaskReadyTimeout | typeof PipelineError.InvalidTask;
2
+ export declare class PipelineError extends Error {
3
+ readonly code: PipelineErrorCode;
4
+ readonly taskName?: string;
5
+ constructor(message: string, code: PipelineErrorCode, taskName?: string);
6
+ static Aborted: "aborted";
7
+ static ProcessTerminated: "process-terminated";
8
+ static TaskFailed: "task-failed";
9
+ static TaskReadyTimeout: "task-ready-timeout";
10
+ static TaskCompletedTimeout: "task-completed-timeout";
11
+ static InvalidTask: "invalid-task";
12
+ }
@@ -22,29 +22,35 @@ Object.defineProperty(PipelineError, "Aborted", {
22
22
  enumerable: true,
23
23
  configurable: true,
24
24
  writable: true,
25
- value: 0
25
+ value: "aborted"
26
26
  });
27
27
  Object.defineProperty(PipelineError, "ProcessTerminated", {
28
28
  enumerable: true,
29
29
  configurable: true,
30
30
  writable: true,
31
- value: 1
31
+ value: "process-terminated"
32
32
  });
33
33
  Object.defineProperty(PipelineError, "TaskFailed", {
34
34
  enumerable: true,
35
35
  configurable: true,
36
36
  writable: true,
37
- value: 2
37
+ value: "task-failed"
38
38
  });
39
- Object.defineProperty(PipelineError, "InvalidSignal", {
39
+ Object.defineProperty(PipelineError, "TaskReadyTimeout", {
40
40
  enumerable: true,
41
41
  configurable: true,
42
42
  writable: true,
43
- value: 3
43
+ value: "task-ready-timeout"
44
+ });
45
+ Object.defineProperty(PipelineError, "TaskCompletedTimeout", {
46
+ enumerable: true,
47
+ configurable: true,
48
+ writable: true,
49
+ value: "task-completed-timeout"
44
50
  });
45
51
  Object.defineProperty(PipelineError, "InvalidTask", {
46
52
  enumerable: true,
47
53
  configurable: true,
48
54
  writable: true,
49
- value: 4
55
+ value: "invalid-task"
50
56
  });
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { task } from "./task.js";
2
2
  export { pipeline } from "./pipeline.js";
3
- export { PipelineError, type PipelineErrorCode } from "./pipeline-error.js";
3
+ export { PipelineError, type PipelineErrorCode } from "./errors.js";
4
4
  export type { Task, Pipeline, TaskConfig, Command, CommandConfig, Commands, PipelineRunConfig, PipelineTaskConfig, RunResult, PipelineStats, TaskStats, TaskStatus, } from "./types.js";
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  export { task } from "./task.js";
2
2
  export { pipeline } from "./pipeline.js";
3
- export { PipelineError } from "./pipeline-error.js";
3
+ export { PipelineError } from "./errors.js";
@@ -0,0 +1,2 @@
1
+ export declare const $TASK_INTERNAL: unique symbol;
2
+ export declare const $PIPELINE_INTERNAL: unique symbol;
@@ -0,0 +1,2 @@
1
+ export const $TASK_INTERNAL = Symbol("task-internal");
2
+ export const $PIPELINE_INTERNAL = Symbol("pipeline-internal");
@@ -0,0 +1,33 @@
1
+ import type { PipelineRunConfig, TaskStats } from "../types.js";
2
+ import type { TeardownManager } from "./teardown-manager.js";
3
+ import type { TimeoutManager } from "./timeout-manager.js";
4
+ import type { QueueManager } from "./queue-manager.js";
5
+ /**
6
+ * Represents a task execution in progress
7
+ */
8
+ export interface TaskExecution {
9
+ taskId: string;
10
+ taskName: string;
11
+ process?: import("node:child_process").ChildProcess;
12
+ startedAt: number;
13
+ readyAt?: number;
14
+ }
15
+ /**
16
+ * Centralized execution context that replaces scattered configuration parameters
17
+ * and provides unified access to execution state and helper methods
18
+ */
19
+ export interface ExecutionContext {
20
+ config: PipelineRunConfig;
21
+ signal?: AbortSignal;
22
+ spawn: typeof import("node:child_process").spawn;
23
+ teardownManager: TeardownManager;
24
+ timeoutManager: TimeoutManager;
25
+ queueManager: QueueManager;
26
+ taskStats: Map<string, TaskStats>;
27
+ updateTaskStatus: (taskId: string, updates: Partial<TaskStats>) => void;
28
+ isAborted: () => boolean;
29
+ }
30
+ /**
31
+ * Creates an execution context for pipeline execution
32
+ */
33
+ export declare function createExecutionContext(config: PipelineRunConfig, teardownManager: TeardownManager, timeoutManager: TimeoutManager, queueManager: QueueManager, taskStats: Map<string, TaskStats>): ExecutionContext;
@@ -0,0 +1,30 @@
1
+ import { spawn } from "node:child_process";
2
+ /**
3
+ * Creates an execution context for pipeline execution
4
+ */
5
+ export function createExecutionContext(config, teardownManager, timeoutManager, queueManager, taskStats) {
6
+ // Use dynamic import only if spawn is not provided
7
+ const spawnFn = config.spawn ?? spawn;
8
+ return {
9
+ config,
10
+ signal: config.signal,
11
+ spawn: spawnFn,
12
+ teardownManager,
13
+ timeoutManager,
14
+ queueManager,
15
+ taskStats,
16
+ updateTaskStatus(taskId, updates) {
17
+ const currentStats = taskStats.get(taskId);
18
+ const updatedStats = { ...currentStats, ...updates };
19
+ // Calculate duration if both start and finish times are available
20
+ if (updatedStats.startedAt && updatedStats.finishedAt) {
21
+ updatedStats.durationMs =
22
+ updatedStats.finishedAt - updatedStats.startedAt;
23
+ }
24
+ taskStats.set(taskId, updatedStats);
25
+ },
26
+ isAborted() {
27
+ return config.signal?.aborted ?? false;
28
+ },
29
+ };
30
+ }
@@ -1,2 +1,2 @@
1
- import type { TaskGraph, Task } from "./types.js";
1
+ import type { TaskGraph, Task } from "../types.js";
2
2
  export declare function createTaskGraph(tasks: Task[]): TaskGraph;
@@ -0,0 +1,24 @@
1
+ import type { TaskGraph } from "../types.js";
2
+ import type { TaskExecution } from "./execution-context.js";
3
+ /**
4
+ * Queue manager interface returned by createQueueManager
5
+ */
6
+ export interface QueueManager {
7
+ getNextReadyTask(): string | null;
8
+ markRunningTaskReady(taskId: string): void;
9
+ markTaskComplete(taskId: string): void;
10
+ markTaskFailed(taskId: string): void;
11
+ markTaskSkipped(taskId: string): void;
12
+ markTaskRunning(taskId: string, execution: TaskExecution): void;
13
+ canExecuteMore(): boolean;
14
+ isComplete(): boolean;
15
+ hasFailed(): boolean;
16
+ getRunningTasks(): Map<string, TaskExecution>;
17
+ clearQueues(): void;
18
+ abortAllRunningTasks(): void;
19
+ }
20
+ /**
21
+ * Creates a queue manager that replaces the generator-based scheduler
22
+ * with explicit queue-based execution state management
23
+ */
24
+ export declare function createQueueManager(graph: TaskGraph, maxConcurrency?: number): QueueManager;
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Creates a queue manager that replaces the generator-based scheduler
3
+ * with explicit queue-based execution state management
4
+ */
5
+ export function createQueueManager(graph, maxConcurrency) {
6
+ const readyQueue = [];
7
+ const waitingQueue = new Map();
8
+ const runningTasks = new Map();
9
+ const completedTasks = new Set();
10
+ const failedTasks = new Set();
11
+ const skippedTasks = new Set();
12
+ const maxConcurrencyLimit = maxConcurrency ?? Infinity;
13
+ let status = "running";
14
+ for (const [taskId, node] of graph.nodes) {
15
+ const depCount = node.dependencies.size;
16
+ if (depCount === 0) {
17
+ readyQueue.push(taskId);
18
+ }
19
+ else {
20
+ waitingQueue.set(taskId, depCount);
21
+ }
22
+ }
23
+ const updateDependentTasks = (completedTaskId) => {
24
+ const node = graph.nodes.get(completedTaskId);
25
+ if (!node)
26
+ return;
27
+ for (const dependentId of node.dependents) {
28
+ const currentCount = waitingQueue.get(dependentId);
29
+ if (currentCount === undefined)
30
+ continue;
31
+ const newCount = currentCount - 1;
32
+ if (newCount > 0) {
33
+ waitingQueue.set(dependentId, newCount);
34
+ continue;
35
+ }
36
+ waitingQueue.delete(dependentId);
37
+ readyQueue.push(dependentId);
38
+ }
39
+ };
40
+ const allTasksFinished = () => {
41
+ const totalTasks = graph.nodes.size;
42
+ const finishedTasks = completedTasks.size + failedTasks.size + skippedTasks.size;
43
+ return finishedTasks === totalTasks;
44
+ };
45
+ /**
46
+ * Update execution status based on current queue state
47
+ */
48
+ const updateExecutionStatus = () => {
49
+ if (status === "failed" || status === "aborted") {
50
+ return; // Don't change from terminal states
51
+ }
52
+ if (allTasksFinished()) {
53
+ status = "completed";
54
+ }
55
+ };
56
+ return {
57
+ /**
58
+ * Get the next ready task for execution, respecting concurrency limits
59
+ * Returns null if pipeline has failed or been aborted
60
+ */
61
+ getNextReadyTask() {
62
+ // Don't return tasks if pipeline has failed or been aborted
63
+ if (status === "failed" || status === "aborted") {
64
+ return null;
65
+ }
66
+ if (readyQueue.length === 0)
67
+ return null;
68
+ if (runningTasks.size >= maxConcurrencyLimit)
69
+ return null;
70
+ // Additional check: if we have failed tasks, don't process new ones
71
+ if (failedTasks.size > 0) {
72
+ return null;
73
+ }
74
+ const taskId = readyQueue.shift() ?? null;
75
+ if (taskId) {
76
+ // Final check before returning - prevent race conditions
77
+ // Check failed tasks to prevent race conditions (status check already done above)
78
+ if (failedTasks.size > 0) {
79
+ // Put it back if we detected failure
80
+ readyQueue.unshift(taskId);
81
+ return null;
82
+ }
83
+ }
84
+ return taskId;
85
+ },
86
+ /**
87
+ * Mark a running task as ready (via readyWhen) and update dependent tasks
88
+ */
89
+ markRunningTaskReady(taskId) {
90
+ if (runningTasks.has(taskId)) {
91
+ updateDependentTasks(taskId);
92
+ }
93
+ },
94
+ /**
95
+ * Mark a task as complete and update dependent tasks
96
+ */
97
+ markTaskComplete(taskId) {
98
+ runningTasks.delete(taskId);
99
+ completedTasks.add(taskId);
100
+ updateDependentTasks(taskId);
101
+ updateExecutionStatus();
102
+ },
103
+ /**
104
+ * Mark a task as failed
105
+ * When a task fails, we clear the ready queue immediately to prevent dependent tasks from starting.
106
+ * Note: This clears only the ready queue. The full cleanup (including waiting queue) happens
107
+ * in failPipeline via clearQueues(). This immediate ready queue clearing is critical to prevent
108
+ * race conditions where dependent tasks might be in the ready queue when a dependency fails.
109
+ * Failed tasks don't update dependents as they block the pipeline.
110
+ */
111
+ markTaskFailed(taskId) {
112
+ runningTasks.delete(taskId);
113
+ failedTasks.add(taskId);
114
+ status = "failed";
115
+ // Clear ready queue immediately to prevent any dependent tasks from starting
116
+ // This is critical to prevent race conditions where dependent tasks might
117
+ // be in the ready queue when a dependency fails
118
+ readyQueue.length = 0;
119
+ },
120
+ /**
121
+ * Mark a task as skipped and update dependent tasks
122
+ */
123
+ markTaskSkipped(taskId) {
124
+ runningTasks.delete(taskId);
125
+ skippedTasks.add(taskId);
126
+ updateDependentTasks(taskId);
127
+ updateExecutionStatus();
128
+ },
129
+ /**
130
+ * Mark a task as running
131
+ */
132
+ markTaskRunning(taskId, execution) {
133
+ runningTasks.set(taskId, execution);
134
+ },
135
+ /**
136
+ * Check if there are more tasks that can be executed
137
+ */
138
+ canExecuteMore() {
139
+ return readyQueue.length > 0 && runningTasks.size < maxConcurrencyLimit;
140
+ },
141
+ /**
142
+ * Check if all tasks are complete (either completed, failed, or skipped)
143
+ */
144
+ isComplete: allTasksFinished,
145
+ /**
146
+ * Check if any tasks have failed
147
+ */
148
+ hasFailed() {
149
+ return failedTasks.size > 0;
150
+ },
151
+ /**
152
+ * Get current running tasks
153
+ */
154
+ getRunningTasks() {
155
+ return new Map(runningTasks);
156
+ },
157
+ /**
158
+ * Clear all queues (used during cancellation)
159
+ * This prevents any pending tasks from starting, but preserves their status
160
+ * as "pending" in the task stats (they are not marked as aborted)
161
+ */
162
+ clearQueues() {
163
+ readyQueue.length = 0;
164
+ waitingQueue.clear();
165
+ // Keep running tasks for cleanup, but mark them for termination
166
+ // Note: Tasks in waitingQueue remain with "pending" status - they are not
167
+ // moved to failed/aborted state since they never started
168
+ },
169
+ /**
170
+ * Mark all running tasks as aborted and clear them from running tasks
171
+ * Used during cancellation to ensure proper state consistency
172
+ */
173
+ abortAllRunningTasks() {
174
+ // Move all running tasks to failed state
175
+ for (const taskId of runningTasks.keys()) {
176
+ failedTasks.add(taskId);
177
+ }
178
+ runningTasks.clear();
179
+ status = "aborted";
180
+ },
181
+ };
182
+ }
@@ -21,9 +21,12 @@ export function createSignalHandler({ abortSignal, onAborted, onProcessTerminate
21
21
  // Handle abort signal if provided
22
22
  let signalCleanup = null;
23
23
  if (abortSignal) {
24
- abortSignal.addEventListener("abort", onAborted);
24
+ const handleAbort = () => {
25
+ onAborted();
26
+ };
27
+ abortSignal.addEventListener("abort", handleAbort);
25
28
  signalCleanup = () => {
26
- abortSignal.removeEventListener("abort", onAborted);
29
+ abortSignal.removeEventListener("abort", handleAbort);
27
30
  };
28
31
  }
29
32
  return {
@@ -0,0 +1,33 @@
1
+ import type { Task } from "../types.js";
2
+ import type { ExecutionContext } from "./execution-context.js";
3
+ /**
4
+ * Callbacks for task execution events that replace scheduler coordination.
5
+ * These callbacks are invoked by the task executor to notify the queue manager
6
+ * of task state changes, enabling queue-based execution flow.
7
+ */
8
+ export interface TaskExecutionCallbacks {
9
+ /**
10
+ * Called when a task becomes ready (e.g., via readyWhen condition).
11
+ * This allows dependent tasks to start executing.
12
+ */
13
+ onTaskReady: (taskId: string) => void;
14
+ /**
15
+ * Called when a task completes successfully.
16
+ * Updates dependent task dependency counts and moves ready tasks to execution queue.
17
+ */
18
+ onTaskComplete: (taskId: string) => void;
19
+ /**
20
+ * Called when a task fails.
21
+ * Triggers pipeline failure and cleanup.
22
+ */
23
+ onTaskFailed: (taskId: string, error: Error) => void;
24
+ /**
25
+ * Called when a task is skipped (e.g., missing command in non-strict mode).
26
+ * Treated similarly to completion for dependency resolution.
27
+ */
28
+ onTaskSkipped: (taskId: string) => void;
29
+ }
30
+ /**
31
+ * Executes a task (either a regular task or a nested pipeline).
32
+ */
33
+ export declare function executeTask(task: Task, context: ExecutionContext, callbacks: TaskExecutionCallbacks): void;