@calmo/task-runner 3.4.0 → 3.4.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.
Files changed (87) hide show
  1. package/.github/dependabot.yml +7 -7
  2. package/.github/workflows/ci.yml +4 -4
  3. package/.jules/backlog_maniac.md +1 -0
  4. package/.jules/nexus.md +1 -0
  5. package/.jules/sentinel.md +1 -0
  6. package/.releaserc.json +2 -7
  7. package/AGENTS.md +8 -2
  8. package/CHANGELOG.md +178 -174
  9. package/README.md +23 -23
  10. package/coverage/coverage-final.json +8 -8
  11. package/coverage/index.html +7 -7
  12. package/coverage/lcov-report/index.html +7 -7
  13. package/coverage/lcov-report/src/EventBus.ts.html +27 -21
  14. package/coverage/lcov-report/src/TaskGraphValidationError.ts.html +12 -3
  15. package/coverage/lcov-report/src/TaskGraphValidator.ts.html +152 -137
  16. package/coverage/lcov-report/src/TaskRunner.ts.html +48 -45
  17. package/coverage/lcov-report/src/TaskRunnerBuilder.ts.html +29 -5
  18. package/coverage/lcov-report/src/TaskRunnerExecutionConfig.ts.html +1 -1
  19. package/coverage/lcov-report/src/TaskStateManager.ts.html +1 -1
  20. package/coverage/lcov-report/src/WorkflowExecutor.ts.html +21 -12
  21. package/coverage/lcov-report/src/contracts/RunnerEvents.ts.html +1 -1
  22. package/coverage/lcov-report/src/contracts/index.html +1 -1
  23. package/coverage/lcov-report/src/index.html +8 -8
  24. package/coverage/lcov-report/src/strategies/DryRunExecutionStrategy.ts.html +4 -4
  25. package/coverage/lcov-report/src/strategies/RetryingExecutionStrategy.ts.html +29 -11
  26. package/coverage/lcov-report/src/strategies/StandardExecutionStrategy.ts.html +5 -5
  27. package/coverage/lcov-report/src/strategies/index.html +1 -1
  28. package/coverage/lcov.info +266 -262
  29. package/coverage/src/EventBus.ts.html +27 -21
  30. package/coverage/src/TaskGraphValidationError.ts.html +12 -3
  31. package/coverage/src/TaskGraphValidator.ts.html +152 -137
  32. package/coverage/src/TaskRunner.ts.html +48 -45
  33. package/coverage/src/TaskRunnerBuilder.ts.html +29 -5
  34. package/coverage/src/TaskRunnerExecutionConfig.ts.html +1 -1
  35. package/coverage/src/TaskStateManager.ts.html +1 -1
  36. package/coverage/src/WorkflowExecutor.ts.html +21 -12
  37. package/coverage/src/contracts/RunnerEvents.ts.html +1 -1
  38. package/coverage/src/contracts/index.html +1 -1
  39. package/coverage/src/index.html +8 -8
  40. package/coverage/src/strategies/DryRunExecutionStrategy.ts.html +4 -4
  41. package/coverage/src/strategies/RetryingExecutionStrategy.ts.html +29 -11
  42. package/coverage/src/strategies/StandardExecutionStrategy.ts.html +5 -5
  43. package/coverage/src/strategies/index.html +1 -1
  44. package/dist/EventBus.js +13 -11
  45. package/dist/EventBus.js.map +1 -1
  46. package/dist/TaskGraphValidationError.js.map +1 -1
  47. package/dist/TaskGraphValidator.js +9 -9
  48. package/dist/TaskGraphValidator.js.map +1 -1
  49. package/dist/TaskRunner.js.map +1 -1
  50. package/dist/TaskRunnerBuilder.js.map +1 -1
  51. package/dist/WorkflowExecutor.js +2 -1
  52. package/dist/WorkflowExecutor.js.map +1 -1
  53. package/dist/strategies/RetryingExecutionStrategy.js +3 -1
  54. package/dist/strategies/RetryingExecutionStrategy.js.map +1 -1
  55. package/dist/strategies/StandardExecutionStrategy.js +1 -1
  56. package/dist/strategies/StandardExecutionStrategy.js.map +1 -1
  57. package/openspec/AGENTS.md +81 -15
  58. package/openspec/changes/archive/2026-01-18-add-concurrency-control/proposal.md +7 -4
  59. package/openspec/changes/archive/2026-01-18-add-concurrency-control/tasks.md +1 -0
  60. package/openspec/changes/archive/2026-01-18-add-external-task-cancellation/proposal.md +4 -1
  61. package/openspec/changes/archive/2026-01-18-add-external-task-cancellation/tasks.md +2 -1
  62. package/openspec/changes/archive/2026-01-18-add-integration-tests/proposal.md +3 -0
  63. package/openspec/changes/archive/2026-01-18-add-integration-tests/tasks.md +1 -0
  64. package/openspec/changes/archive/2026-01-18-add-task-retry-policy/proposal.md +3 -0
  65. package/openspec/changes/archive/2026-01-18-add-task-retry-policy/tasks.md +1 -0
  66. package/openspec/changes/archive/2026-01-18-add-workflow-preview/proposal.md +3 -0
  67. package/openspec/changes/archive/2026-01-18-add-workflow-preview/tasks.md +1 -0
  68. package/openspec/changes/archive/2026-01-18-refactor-core-architecture/proposal.md +3 -0
  69. package/openspec/changes/archive/2026-01-18-refactor-core-architecture/tasks.md +1 -0
  70. package/openspec/changes/feat-per-task-timeout/proposal.md +11 -6
  71. package/openspec/changes/feat-per-task-timeout/tasks.md +1 -1
  72. package/openspec/project.md +21 -15
  73. package/package.json +1 -1
  74. package/src/EventBus.ts +18 -16
  75. package/src/TaskGraph.ts +8 -8
  76. package/src/TaskGraphValidationError.ts +4 -1
  77. package/src/TaskGraphValidator.ts +148 -143
  78. package/src/TaskRunner.ts +42 -41
  79. package/src/TaskRunnerBuilder.ts +11 -3
  80. package/src/WorkflowExecutor.ts +13 -10
  81. package/src/contracts/ITaskGraphValidator.ts +12 -12
  82. package/src/contracts/ValidationError.ts +6 -6
  83. package/src/contracts/ValidationResult.ts +4 -4
  84. package/src/strategies/DryRunExecutionStrategy.ts +3 -3
  85. package/src/strategies/RetryingExecutionStrategy.ts +15 -9
  86. package/src/strategies/StandardExecutionStrategy.ts +4 -4
  87. package/test-report.xml +108 -108
package/src/TaskRunner.ts CHANGED
@@ -2,7 +2,10 @@ import { TaskStep } from "./TaskStep.js";
2
2
  import { TaskResult } from "./TaskResult.js";
3
3
  import { TaskGraphValidator } from "./TaskGraphValidator.js";
4
4
  import { TaskGraph } from "./TaskGraph.js";
5
- import { RunnerEventPayloads, RunnerEventListener } from "./contracts/RunnerEvents.js";
5
+ import {
6
+ RunnerEventPayloads,
7
+ RunnerEventListener,
8
+ } from "./contracts/RunnerEvents.js";
6
9
  import { EventBus } from "./EventBus.js";
7
10
  import { WorkflowExecutor } from "./WorkflowExecutor.js";
8
11
  import { TaskRunnerExecutionConfig } from "./TaskRunnerExecutionConfig.js";
@@ -24,9 +27,8 @@ export { RunnerEventPayloads, RunnerEventListener, TaskRunnerExecutionConfig };
24
27
  export class TaskRunner<TContext> {
25
28
  private eventBus = new EventBus<TContext>();
26
29
  private validator = new TaskGraphValidator();
27
- private executionStrategy: IExecutionStrategy<TContext> = new RetryingExecutionStrategy(
28
- new StandardExecutionStrategy()
29
- );
30
+ private executionStrategy: IExecutionStrategy<TContext> =
31
+ new RetryingExecutionStrategy(new StandardExecutionStrategy());
30
32
 
31
33
  /**
32
34
  * @param context The shared context object to be passed to each task.
@@ -85,7 +87,6 @@ export class TaskRunner<TContext> {
85
87
  const safeId = (name: string) => JSON.stringify(name);
86
88
  const sanitize = (name: string) => this.sanitizeMermaidId(name);
87
89
 
88
-
89
90
  // Add all nodes first to ensure they exist
90
91
  for (const step of steps) {
91
92
  // Using the name as both ID and Label for simplicity
@@ -98,9 +99,7 @@ export class TaskRunner<TContext> {
98
99
  for (const step of steps) {
99
100
  if (step.dependencies) {
100
101
  for (const dep of step.dependencies) {
101
- graphLines.push(
102
- ` ${sanitize(dep)} --> ${sanitize(step.name)}`
103
- );
102
+ graphLines.push(` ${sanitize(dep)} --> ${sanitize(step.name)}`);
104
103
  }
105
104
  }
106
105
  }
@@ -162,40 +161,42 @@ export class TaskRunner<TContext> {
162
161
 
163
162
  // We need to handle the timeout cleanup properly.
164
163
  if (config?.timeout !== undefined) {
165
- const controller = new AbortController();
166
- const timeoutId = setTimeout(() => {
167
- controller.abort(new Error(`Workflow timed out after ${config.timeout}ms`));
168
- }, config.timeout);
169
-
170
- let effectiveSignal = controller.signal;
171
- let onAbort: (() => void) | undefined;
172
-
173
- // Handle combination of signals if user provided one
174
- if (config.signal) {
175
- if (config.signal.aborted) {
176
- // If already aborted, use it directly (WorkflowExecutor handles early abort)
177
- // We can cancel timeout immediately
178
- clearTimeout(timeoutId);
179
- effectiveSignal = config.signal;
180
- } else {
181
- // Listen to user signal to abort our controller
182
- onAbort = () => {
183
- controller.abort(config.signal?.reason);
184
- };
185
- config.signal.addEventListener("abort", onAbort);
186
- }
187
- }
188
-
189
- try {
190
- return await executor.execute(steps, effectiveSignal);
191
- } finally {
192
- clearTimeout(timeoutId);
193
- if (config.signal && onAbort) {
194
- config.signal.removeEventListener("abort", onAbort);
195
- }
196
- }
164
+ const controller = new AbortController();
165
+ const timeoutId = setTimeout(() => {
166
+ controller.abort(
167
+ new Error(`Workflow timed out after ${config.timeout}ms`)
168
+ );
169
+ }, config.timeout);
170
+
171
+ let effectiveSignal = controller.signal;
172
+ let onAbort: (() => void) | undefined;
173
+
174
+ // Handle combination of signals if user provided one
175
+ if (config.signal) {
176
+ if (config.signal.aborted) {
177
+ // If already aborted, use it directly (WorkflowExecutor handles early abort)
178
+ // We can cancel timeout immediately
179
+ clearTimeout(timeoutId);
180
+ effectiveSignal = config.signal;
181
+ } else {
182
+ // Listen to user signal to abort our controller
183
+ onAbort = () => {
184
+ controller.abort(config.signal?.reason);
185
+ };
186
+ config.signal.addEventListener("abort", onAbort);
187
+ }
188
+ }
189
+
190
+ try {
191
+ return await executor.execute(steps, effectiveSignal);
192
+ } finally {
193
+ clearTimeout(timeoutId);
194
+ if (config.signal && onAbort) {
195
+ config.signal.removeEventListener("abort", onAbort);
196
+ }
197
+ }
197
198
  } else {
198
- return executor.execute(steps, config?.signal);
199
+ return executor.execute(steps, config?.signal);
199
200
  }
200
201
  }
201
202
  }
@@ -1,5 +1,8 @@
1
1
  import { TaskRunner } from "./TaskRunner.js";
2
- import { RunnerEventPayloads, RunnerEventListener } from "./contracts/RunnerEvents.js";
2
+ import {
3
+ RunnerEventPayloads,
4
+ RunnerEventListener,
5
+ } from "./contracts/RunnerEvents.js";
3
6
  import { IExecutionStrategy } from "./strategies/IExecutionStrategy.js";
4
7
 
5
8
  /**
@@ -9,7 +12,10 @@ export class TaskRunnerBuilder<TContext> {
9
12
  private context: TContext;
10
13
  private strategy?: IExecutionStrategy<TContext>;
11
14
  private listeners: {
12
- [K in keyof RunnerEventPayloads<TContext>]?: RunnerEventListener<TContext, K>[];
15
+ [K in keyof RunnerEventPayloads<TContext>]?: RunnerEventListener<
16
+ TContext,
17
+ K
18
+ >[];
13
19
  } = {};
14
20
 
15
21
  /**
@@ -57,7 +63,9 @@ export class TaskRunnerBuilder<TContext> {
57
63
  runner.setExecutionStrategy(this.strategy);
58
64
  }
59
65
 
60
- (Object.keys(this.listeners) as Array<keyof RunnerEventPayloads<TContext>>).forEach((event) => {
66
+ (
67
+ Object.keys(this.listeners) as Array<keyof RunnerEventPayloads<TContext>>
68
+ ).forEach((event) => {
61
69
  const callbacks = this.listeners[event];
62
70
  // callbacks is always defined because we are iterating keys of the object
63
71
  callbacks!.forEach((callback) =>
@@ -41,7 +41,9 @@ export class WorkflowExecutor<TContext> {
41
41
 
42
42
  // Check if already aborted
43
43
  if (signal?.aborted) {
44
- this.stateManager.cancelAllPending("Workflow cancelled before execution started.");
44
+ this.stateManager.cancelAllPending(
45
+ "Workflow cancelled before execution started."
46
+ );
45
47
  const results = this.stateManager.getResults();
46
48
  this.eventBus.emit("workflowEnd", { context: this.context, results });
47
49
  return results;
@@ -55,7 +57,7 @@ export class WorkflowExecutor<TContext> {
55
57
  };
56
58
 
57
59
  if (signal) {
58
- signal.addEventListener("abort", onAbort);
60
+ signal.addEventListener("abort", onAbort);
59
61
  }
60
62
 
61
63
  try {
@@ -77,10 +79,10 @@ export class WorkflowExecutor<TContext> {
77
79
  }
78
80
 
79
81
  if (signal?.aborted) {
80
- this.stateManager.cancelAllPending("Workflow cancelled.");
82
+ this.stateManager.cancelAllPending("Workflow cancelled.");
81
83
  } else {
82
- // After a task finishes, check for new work
83
- this.processLoop(executingPromises, signal);
84
+ // After a task finishes, check for new work
85
+ this.processLoop(executingPromises, signal);
84
86
  }
85
87
  }
86
88
 
@@ -124,14 +126,15 @@ export class WorkflowExecutor<TContext> {
124
126
 
125
127
  this.stateManager.markRunning(step);
126
128
 
127
- const taskPromise = this.strategy.execute(step, this.context, signal)
129
+ const taskPromise = this.strategy
130
+ .execute(step, this.context, signal)
128
131
  .then((result) => {
129
- this.stateManager.markCompleted(step, result);
132
+ this.stateManager.markCompleted(step, result);
130
133
  })
131
134
  .finally(() => {
132
- executingPromises.delete(taskPromise);
133
- // When a task finishes, we try to run more
134
- this.processLoop(executingPromises, signal);
135
+ executingPromises.delete(taskPromise);
136
+ // When a task finishes, we try to run more
137
+ this.processLoop(executingPromises, signal);
135
138
  });
136
139
 
137
140
  executingPromises.add(taskPromise);
@@ -5,17 +5,17 @@ import { ValidationResult } from "./ValidationResult.js";
5
5
  * Defines the interface for a task graph validator.
6
6
  */
7
7
  export interface ITaskGraphValidator {
8
- /**
9
- * Validates a given task graph for structural integrity.
10
- * @param taskGraph The task graph to validate.
11
- * @returns A ValidationResult object indicating the outcome of the validation.
12
- */
13
- validate(taskGraph: TaskGraph): ValidationResult;
8
+ /**
9
+ * Validates a given task graph for structural integrity.
10
+ * @param taskGraph The task graph to validate.
11
+ * @returns A ValidationResult object indicating the outcome of the validation.
12
+ */
13
+ validate(taskGraph: TaskGraph): ValidationResult;
14
14
 
15
- /**
16
- * Creates a human-readable error message from a validation result.
17
- * @param result The validation result containing errors.
18
- * @returns A formatted error string.
19
- */
20
- createErrorMessage(result: ValidationResult): string;
15
+ /**
16
+ * Creates a human-readable error message from a validation result.
17
+ * @param result The validation result containing errors.
18
+ * @returns A formatted error string.
19
+ */
20
+ createErrorMessage(result: ValidationResult): string;
21
21
  }
@@ -2,10 +2,10 @@
2
2
  * Describes a specific validation error found in the task graph.
3
3
  */
4
4
  export interface ValidationError {
5
- /** The type of validation error. */
6
- type: "cycle" | "missing_dependency" | "duplicate_task";
7
- /** A human-readable message describing the error. */
8
- message: string;
9
- /** Optional detailed information about the error, e.g., the cycle path, or the task with a missing dependency. */
10
- details?: unknown;
5
+ /** The type of validation error. */
6
+ type: "cycle" | "missing_dependency" | "duplicate_task";
7
+ /** A human-readable message describing the error. */
8
+ message: string;
9
+ /** Optional detailed information about the error, e.g., the cycle path, or the task with a missing dependency. */
10
+ details?: unknown;
11
11
  }
@@ -4,8 +4,8 @@ import { ValidationError } from "./ValidationError.js";
4
4
  * The result of a task graph validation operation.
5
5
  */
6
6
  export interface ValidationResult {
7
- /** True if the graph is valid, false otherwise. */
8
- isValid: boolean;
9
- /** An array of ValidationError objects if the graph is not valid. Empty if isValid is true. */
10
- errors: ValidationError[];
7
+ /** True if the graph is valid, false otherwise. */
8
+ isValid: boolean;
9
+ /** An array of ValidationError objects if the graph is not valid. Empty if isValid is true. */
10
+ errors: ValidationError[];
11
11
  }
@@ -5,9 +5,9 @@ import { TaskResult } from "../TaskResult.js";
5
5
  /**
6
6
  * Execution strategy that simulates task execution without running the actual logic.
7
7
  */
8
- export class DryRunExecutionStrategy<TContext>
9
- implements IExecutionStrategy<TContext>
10
- {
8
+ export class DryRunExecutionStrategy<
9
+ TContext,
10
+ > implements IExecutionStrategy<TContext> {
11
11
  /**
12
12
  * Simulates execution by returning a success result immediately.
13
13
  * @param step The task step (ignored).
@@ -5,7 +5,9 @@ import { TaskResult } from "../TaskResult.js";
5
5
  /**
6
6
  * Execution strategy that retries tasks upon failure based on their retry configuration.
7
7
  */
8
- export class RetryingExecutionStrategy<TContext> implements IExecutionStrategy<TContext> {
8
+ export class RetryingExecutionStrategy<
9
+ TContext,
10
+ > implements IExecutionStrategy<TContext> {
9
11
  constructor(private innerStrategy: IExecutionStrategy<TContext>) {}
10
12
 
11
13
  async execute(
@@ -30,7 +32,11 @@ export class RetryingExecutionStrategy<TContext> implements IExecutionStrategy<T
30
32
 
31
33
  const result = await this.innerStrategy.execute(step, context, signal);
32
34
 
33
- if (result.status === "success" || result.status === "cancelled" || result.status === "skipped") {
35
+ if (
36
+ result.status === "success" ||
37
+ result.status === "cancelled" ||
38
+ result.status === "skipped"
39
+ ) {
34
40
  return result;
35
41
  }
36
42
 
@@ -51,13 +57,13 @@ export class RetryingExecutionStrategy<TContext> implements IExecutionStrategy<T
51
57
  try {
52
58
  await this.sleep(delay, signal);
53
59
  } catch (e) {
54
- if (signal?.aborted) {
55
- return {
56
- status: "cancelled",
57
- message: "Task cancelled during retry delay",
58
- };
59
- }
60
- throw e;
60
+ if (signal?.aborted) {
61
+ return {
62
+ status: "cancelled",
63
+ message: "Task cancelled during retry delay",
64
+ };
65
+ }
66
+ throw e;
61
67
  }
62
68
  }
63
69
  }
@@ -5,9 +5,9 @@ import { TaskResult } from "../TaskResult.js";
5
5
  /**
6
6
  * Standard execution strategy that runs the task's run method.
7
7
  */
8
- export class StandardExecutionStrategy<TContext>
9
- implements IExecutionStrategy<TContext>
10
- {
8
+ export class StandardExecutionStrategy<
9
+ TContext,
10
+ > implements IExecutionStrategy<TContext> {
11
11
  async execute(
12
12
  step: TaskStep<TContext>,
13
13
  context: TContext,
@@ -19,7 +19,7 @@ export class StandardExecutionStrategy<TContext>
19
19
  // Check if error is due to abort
20
20
  if (
21
21
  signal?.aborted &&
22
- (e instanceof Error && e.name === "AbortError" || signal.reason === e)
22
+ ((e instanceof Error && e.name === "AbortError") || signal.reason === e)
23
23
  ) {
24
24
  return {
25
25
  status: "cancelled",