@calmo/task-runner 4.1.0 → 4.3.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.
Files changed (84) hide show
  1. package/.jules/nexus.md +5 -0
  2. package/.release-please-manifest.json +1 -1
  3. package/AGENTS.md +2 -0
  4. package/CHANGELOG.md +51 -0
  5. package/README.md +34 -0
  6. package/conductor/code_styleguides/general.md +23 -0
  7. package/conductor/code_styleguides/javascript.md +51 -0
  8. package/conductor/code_styleguides/typescript.md +43 -0
  9. package/conductor/product-guidelines.md +14 -0
  10. package/conductor/product.md +16 -0
  11. package/conductor/setup_state.json +1 -0
  12. package/conductor/tech-stack.md +19 -0
  13. package/conductor/workflow.md +334 -0
  14. package/dist/EventBus.js +19 -18
  15. package/dist/EventBus.js.map +1 -1
  16. package/dist/PluginManager.d.ts +22 -0
  17. package/dist/PluginManager.js +40 -0
  18. package/dist/PluginManager.js.map +1 -0
  19. package/dist/TaskGraphValidator.d.ts +1 -1
  20. package/dist/TaskGraphValidator.js +16 -21
  21. package/dist/TaskGraphValidator.js.map +1 -1
  22. package/dist/TaskRunner.d.ts +8 -1
  23. package/dist/TaskRunner.js +37 -25
  24. package/dist/TaskRunner.js.map +1 -1
  25. package/dist/TaskStateManager.d.ts +1 -0
  26. package/dist/TaskStateManager.js +22 -6
  27. package/dist/TaskStateManager.js.map +1 -1
  28. package/dist/TaskStep.d.ts +12 -0
  29. package/dist/WorkflowExecutor.js +19 -10
  30. package/dist/WorkflowExecutor.js.map +1 -1
  31. package/dist/contracts/Plugin.d.ts +30 -0
  32. package/dist/contracts/Plugin.js +2 -0
  33. package/dist/contracts/Plugin.js.map +1 -0
  34. package/dist/strategies/DryRunExecutionStrategy.js +2 -2
  35. package/dist/strategies/DryRunExecutionStrategy.js.map +1 -1
  36. package/dist/strategies/StandardExecutionStrategy.js +43 -1
  37. package/dist/strategies/StandardExecutionStrategy.js.map +1 -1
  38. package/openspec/changes/add-middleware-support/proposal.md +19 -0
  39. package/openspec/changes/add-middleware-support/specs/task-runner/spec.md +34 -0
  40. package/openspec/changes/add-middleware-support/tasks.md +9 -0
  41. package/openspec/changes/add-resource-concurrency/tasks.md +1 -0
  42. package/openspec/changes/allow-plugin-hooks/design.md +51 -0
  43. package/openspec/changes/allow-plugin-hooks/proposal.md +12 -0
  44. package/openspec/changes/allow-plugin-hooks/specs/post-task/spec.md +21 -0
  45. package/openspec/changes/allow-plugin-hooks/specs/pre-task/spec.md +32 -0
  46. package/openspec/changes/allow-plugin-hooks/tasks.md +7 -0
  47. package/openspec/changes/archive/2026-02-15-implement-plugin-system/design.md +45 -0
  48. package/openspec/changes/archive/2026-02-15-implement-plugin-system/proposal.md +13 -0
  49. package/openspec/changes/archive/2026-02-15-implement-plugin-system/specs/plugin-context/spec.md +17 -0
  50. package/openspec/changes/archive/2026-02-15-implement-plugin-system/specs/plugin-loading/spec.md +27 -0
  51. package/openspec/changes/archive/2026-02-15-implement-plugin-system/tasks.md +7 -0
  52. package/openspec/changes/feat-completion-dependencies/proposal.md +58 -0
  53. package/openspec/changes/feat-completion-dependencies/tasks.md +46 -0
  54. package/openspec/changes/feat-task-caching/design.md +34 -0
  55. package/openspec/changes/feat-task-caching/proposal.md +18 -0
  56. package/openspec/changes/feat-task-caching/specs/task-runner/spec.md +58 -0
  57. package/openspec/changes/feat-task-caching/tasks.md +24 -0
  58. package/openspec/changes/feat-task-loop/proposal.md +22 -0
  59. package/openspec/changes/feat-task-loop/specs/task-runner/spec.md +34 -0
  60. package/openspec/changes/feat-task-loop/tasks.md +8 -0
  61. package/openspec/proposals/feat-matrix-execution/proposal.md +23 -0
  62. package/openspec/proposals/feat-matrix-execution/specs/task-runner/spec.md +47 -0
  63. package/openspec/proposals/feat-matrix-execution/tasks.md +11 -0
  64. package/openspec/proposals/feat-task-observability/proposal.md +16 -0
  65. package/openspec/proposals/feat-task-observability/specs/task-runner/spec.md +14 -0
  66. package/openspec/proposals/feat-task-observability/tasks.md +7 -0
  67. package/openspec/specs/plugin-context/spec.md +19 -0
  68. package/openspec/specs/plugin-loading/spec.md +29 -0
  69. package/package.json +1 -1
  70. package/src/EventBus.ts +11 -15
  71. package/src/PluginManager.ts +43 -0
  72. package/src/TaskGraphValidator.ts +22 -24
  73. package/src/TaskRunner.ts +45 -28
  74. package/src/TaskStateManager.ts +20 -6
  75. package/src/TaskStep.ts +14 -0
  76. package/src/WorkflowExecutor.ts +24 -11
  77. package/src/contracts/Plugin.ts +32 -0
  78. package/src/strategies/DryRunExecutionStrategy.ts +2 -2
  79. package/src/strategies/StandardExecutionStrategy.ts +48 -1
  80. /package/openspec/changes/{feat-continue-on-error → archive/2026-02-18-feat-continue-on-error}/proposal.md +0 -0
  81. /package/openspec/changes/{feat-continue-on-error → archive/2026-02-18-feat-continue-on-error}/tasks.md +0 -0
  82. /package/openspec/changes/{feat-per-task-timeout → archive/2026-02-25-feat-per-task-timeout}/proposal.md +0 -0
  83. /package/openspec/changes/{feat-per-task-timeout → archive/2026-02-25-feat-per-task-timeout}/specs/task-runner/spec.md +0 -0
  84. /package/openspec/changes/{feat-per-task-timeout → archive/2026-02-25-feat-per-task-timeout}/tasks.md +0 -0
@@ -0,0 +1,34 @@
1
+ ## Context
2
+
3
+ Currently, the task runner always re-executes tasks, even if inputs (context) have not changed. This is inefficient for workflows with expensive steps like builds or data processing.
4
+
5
+ ## Goals / Non-Goals
6
+
7
+ - **Goals**:
8
+ - Avoid redundant execution of expensive tasks.
9
+ - Support pluggable caching mechanisms (defaulting to in-memory).
10
+ - Allow restoration of context side effects when skipping execution.
11
+ - **Non-Goals**:
12
+ - Distributed caching (out of scope for now).
13
+ - Automatic dependency hashing (cache key must be provided by the user).
14
+ - Persistent file system caching (can be added later via plugin or custom provider).
15
+
16
+ ## Decisions
17
+
18
+ - **Decision**: Use `ICacheProvider` interface.
19
+ - **Rationale**: Allows users to swap the caching backend (e.g., Redis, FS) without changing core logic.
20
+ - **Decision**: Explicit `restore` callback.
21
+ - **Rationale**: Since context is mutable and side-effect driven, simply returning a cached result is insufficient. The task must explicitly define how to re-apply its changes to the context based on the cached result.
22
+ - **Decision**: Wrap execution strategy.
23
+ - **Rationale**: Follows the existing decorator pattern (like `RetryingExecutionStrategy`), keeping concerns separated.
24
+
25
+ ## Risks / Trade-offs
26
+
27
+ - **Risk**: Stale cache data if keys are not unique enough.
28
+ - **Mitigation**: Documentation must emphasize the importance of including all relevant inputs in the cache key.
29
+ - **Risk**: Context inconsistency if `restore` is implemented incorrectly.
30
+ - **Mitigation**: Provide clear examples and potentially validate context changes in debug mode.
31
+
32
+ ## Migration Plan
33
+
34
+ - This is an additive change. Existing tasks without `cache` config will work as before. No migration required.
@@ -0,0 +1,18 @@
1
+ # Change: Task Output Caching
2
+
3
+ ## Why
4
+
5
+ Currently, the task runner executes every task on every run, regardless of whether inputs or context have changed. For complex workflows involving expensive operations (e.g., builds, data processing), this leads to redundant execution and slower feedback loops. Implementing a caching mechanism will significantly improve performance by skipping tasks that have already been successfully executed with the same inputs.
6
+
7
+ ## What Changes
8
+
9
+ - **Task Configuration**: Add `cache` configuration to `TaskStep` interface, allowing tasks to define a cache key and a restoration logic.
10
+ - **Execution Strategy**: Introduce `CachingExecutionStrategy` that wraps other strategies. It checks for a cached result before execution and stores the result after successful execution.
11
+ - **Cache Provider**: Define an `ICacheProvider` interface with a default in-memory implementation (`MemoryCacheProvider`), allowing for future extension (e.g., file system or remote cache).
12
+ - **Task Result**: Ensure `TaskResult` is serializable and contains necessary metadata for caching.
13
+
14
+ ## Impact
15
+
16
+ - **Affected specs**: `task-runner`
17
+ - **Affected code**: `TaskStep.ts`, `TaskRunner.ts`, new strategy `CachingExecutionStrategy.ts`, new contract `ICacheProvider.ts`.
18
+ - **Performance**: Significant reduction in execution time for repeated workflows.
@@ -0,0 +1,58 @@
1
+ ## ADDED Requirements
2
+
3
+ ### Requirement: Task Caching Configuration
4
+
5
+ The `TaskStep` interface SHALL support an optional `cache` property of type `TaskCacheConfig`.
6
+
7
+ #### Scenario: Cache Config Structure
8
+
9
+ - **GIVEN** a `TaskCacheConfig` object
10
+ - **THEN** it SHALL support:
11
+ - `key`: A function returning a unique string key based on the context.
12
+ - `ttl`: Optional time-to-live in milliseconds.
13
+ - `restore`: Optional function to restore context side effects from a cached result.
14
+
15
+ ### Requirement: Caching Execution Strategy
16
+
17
+ The system SHALL provide a `CachingExecutionStrategy` that implements `IExecutionStrategy` and wraps another `IExecutionStrategy`.
18
+
19
+ #### Scenario: Cache Miss Execution
20
+
21
+ - **WHEN** the `CachingExecutionStrategy` executes a task with a cache key that is NOT present in the cache provider
22
+ - **THEN** it SHALL execute the task using the inner strategy.
23
+ - **AND** it SHALL store the result in the cache provider if execution is successful.
24
+ - **AND** it SHALL return the result.
25
+
26
+ #### Scenario: Cache Hit Execution
27
+
28
+ - **WHEN** the `CachingExecutionStrategy` executes a task with a cache key that IS present in the cache provider
29
+ - **THEN** it SHALL NOT execute the inner strategy.
30
+ - **AND** it SHALL invoke the `restore` function (if provided) with the current context and the cached result.
31
+ - **AND** it SHALL return the cached result.
32
+
33
+ #### Scenario: Cache Expiration
34
+
35
+ - **WHEN** a cached item's TTL has expired
36
+ - **THEN** the cache provider SHALL NOT return the item.
37
+ - **AND** the strategy SHALL proceed as a cache miss.
38
+
39
+ ### Requirement: Cache Provider Interface
40
+
41
+ The system SHALL define an `ICacheProvider` interface for pluggable caching backends.
42
+
43
+ #### Scenario: Interface Methods
44
+
45
+ - **GIVEN** an `ICacheProvider` implementation
46
+ - **THEN** it SHALL support:
47
+ - `get(key: string): Promise<TaskResult | undefined>`
48
+ - `set(key: string, value: TaskResult, ttl?: number): Promise<void>`
49
+ - `delete(key: string): Promise<void>`
50
+
51
+ ### Requirement: Default Memory Cache
52
+
53
+ The system SHALL provide a `MemoryCacheProvider` as the default implementation of `ICacheProvider`.
54
+
55
+ #### Scenario: In-Memory Storage
56
+
57
+ - **WHEN** items are set in `MemoryCacheProvider`
58
+ - **THEN** they are stored in memory and retrieved correctly until process termination or expiration.
@@ -0,0 +1,24 @@
1
+ ## 1. Implementation
2
+
3
+ - [ ] 1.1 Define `ICacheProvider` interface in `src/contracts/ICacheProvider.ts` with `get(key)`, `set(key, result, ttl)`, and `delete(key)` methods.
4
+ - [ ] 1.2 Implement `MemoryCacheProvider` in `src/utils/MemoryCacheProvider.ts` as the default in-memory cache implementation.
5
+ - [ ] 1.3 Update `TaskStep` interface in `src/TaskStep.ts` to include optional `cache` configuration:
6
+ - `key`: `(context: TContext) => string | Promise<string>`
7
+ - `ttl`: `number` (optional, default to infinite)
8
+ - `restore`: `(context: TContext, cachedResult: TaskResult) => void | Promise<void>` (optional, to re-apply context side effects)
9
+ - [ ] 1.4 Create `CachingExecutionStrategy` in `src/strategies/CachingExecutionStrategy.ts`.
10
+ - It should implement `IExecutionStrategy`.
11
+ - It should accept an inner `IExecutionStrategy` and an `ICacheProvider`.
12
+ - In `execute`:
13
+ - Calculate cache key using `step.cache.key(context)`.
14
+ - Check cache provider. If hit:
15
+ - Execute `step.cache.restore(context, result)` if provided.
16
+ - Return cached result with status `skipped` (or a new status `cached`).
17
+ - If miss:
18
+ - Execute inner strategy.
19
+ - If successful, store result in cache provider using `ttl`.
20
+ - Return result.
21
+ - [ ] 1.5 Update `TaskRunner.ts` to support configuring the cache provider and wrapping the execution strategy with `CachingExecutionStrategy` if caching is enabled.
22
+ - [ ] 1.6 Add unit tests for `MemoryCacheProvider`.
23
+ - [ ] 1.7 Add unit tests for `CachingExecutionStrategy`, verifying cache hits, misses, and restoration of context.
24
+ - [ ] 1.8 Add integration tests in `tests/TaskRunnerCaching.test.ts` to verify end-to-end caching behavior with context updates.
@@ -0,0 +1,22 @@
1
+ # Change: feat-task-loop
2
+
3
+ ## Why
4
+
5
+ Users often need to orchestrate tasks that involve waiting for a condition to be met, such as polling an API for a deployment status or waiting for a database to be healthy. Currently, this logic must be implemented imperatively within the `run` method of a task, which obscures the intent and mixes orchestration concerns with business logic.
6
+
7
+ ## What Changes
8
+
9
+ - Add a `loop` configuration to the `TaskStep` interface.
10
+ - Introduce a `LoopingExecutionStrategy` that wraps other strategies to handle conditional re-execution.
11
+ - The `loop` configuration will support:
12
+ - `interval`: Time in milliseconds to wait between iterations.
13
+ - `maxIterations`: Maximum number of times to run the task.
14
+ - `until`: A predicate function `(context, result) => boolean` that determines when the loop should stop.
15
+
16
+ ## Impact
17
+
18
+ - **Affected Specs**: `task-runner`
19
+ - **Affected Code**:
20
+ - `src/TaskStep.ts`: Update interface.
21
+ - `src/strategies/LoopingExecutionStrategy.ts`: New file.
22
+ - `src/TaskRunnerBuilder.ts`: Integrate the new strategy.
@@ -0,0 +1,34 @@
1
+ ## ADDED Requirements
2
+
3
+ ### Requirement: Task Looping
4
+
5
+ The `TaskStep` interface SHALL support an optional `loop` property of type `TaskLoopConfig` to enable conditional re-execution of a task (polling).
6
+
7
+ #### Scenario: Loop until condition met
8
+
9
+ - **GIVEN** a task with a `loop` configuration
10
+ - **AND** the `loop.until` predicate returns `false`
11
+ - **THEN** the task SHALL re-execute after the specified `interval`.
12
+ - **WHEN** the `loop.until` predicate returns `true`
13
+ - **THEN** the task SHALL complete successfully with the last result.
14
+
15
+ #### Scenario: Max iterations reached
16
+
17
+ - **GIVEN** a task with a `loop` configuration
18
+ - **AND** the task has re-executed `maxIterations` times without the predicate returning `true`
19
+ - **THEN** the task SHALL fail with an error indicating the loop limit was reached.
20
+
21
+ #### Scenario: Integration with Retries
22
+
23
+ - **GIVEN** a task with both `retry` and `loop` configurations
24
+ - **WHEN** the task fails (throws an error)
25
+ - **THEN** the `RetryingExecutionStrategy` SHALL handle the retry logic first.
26
+ - **AND** the loop iteration SHALL only count once the task executes successfully (or fails after retries).
27
+
28
+ #### Scenario: Loop Config Structure
29
+
30
+ - **GIVEN** a `TaskLoopConfig` object
31
+ - **THEN** it SHALL support:
32
+ - `interval`: Time in milliseconds to wait between iterations (default: 0).
33
+ - `maxIterations`: Maximum number of iterations (default: 1).
34
+ - `until`: A predicate function `(context: TContext, result: TaskResult) => boolean`.
@@ -0,0 +1,8 @@
1
+ ## 1. Implementation
2
+
3
+ - [ ] 1.1 Define `TaskLoopConfig` interface in `src/contracts/TaskLoopConfig.ts`.
4
+ - [ ] 1.2 Update `TaskStep` interface in `src/TaskStep.ts` to include optional `loop: TaskLoopConfig`.
5
+ - [ ] 1.3 Implement `LoopingExecutionStrategy` in `src/strategies/LoopingExecutionStrategy.ts`.
6
+ - [ ] 1.4 Update `TaskRunnerBuilder` in `src/TaskRunnerBuilder.ts` to include `LoopingExecutionStrategy`.
7
+ - [ ] 1.5 Add unit tests for `LoopingExecutionStrategy`.
8
+ - [ ] 1.6 Verify integration with `RetryingExecutionStrategy` (looping should happen outside retries).
@@ -0,0 +1,23 @@
1
+ # Change: Add Matrix Execution Support
2
+
3
+ ## Why
4
+
5
+ Task orchestration tools in the industry (such as Nx, GitHub Actions, GitLab CI, and Turborepo) commonly support Matrix Execution. This allows a single task definition to run multiple times automatically across a set of variable combinations. In `task-runner`, defining identical tasks to process multiple permutations manually increases boilerplate and violates the DRY (Don't Repeat Yourself) principle. Introducing `matrix` configurations directly on the `TaskStep` allows developers to run a task concurrently across different configurations (e.g., Node.js versions, OS platforms, target environments) efficiently.
6
+
7
+ ## What Changes
8
+
9
+ - Introduce a `matrix` property to the `TaskStep` configuration.
10
+ - The `matrix` property should support defining variables as an object of arrays (e.g., `matrix: { node: [18, 20], os: ['ubuntu', 'windows'] }`).
11
+ - The engine will dynamically generate and execute independent child tasks for each permutation of the matrix.
12
+ - Ensure the `MermaidGraph` utility correctly visualizes these dynamic nodes.
13
+ - Expose the current permutation variables to the execution context of each child task.
14
+
15
+ ## Impact
16
+
17
+ - Affected specs: `task-runner` (Task Configuration, Core Orchestration)
18
+ - Affected code:
19
+ - `src/core/TaskStep.ts`
20
+ - `src/core/TaskRunner.ts`
21
+ - `src/core/TaskGraph.ts`
22
+ - `src/core/TaskStateManager.ts`
23
+ - `src/core/WorkflowExecutor.ts`
@@ -0,0 +1,47 @@
1
+ ## ADDED Requirements
2
+
3
+ ### Requirement: Matrix Execution Configuration
4
+
5
+ The `TaskStep` interface SHALL support an optional `matrix` configuration property to enable parameterized execution.
6
+
7
+ #### Scenario: Matrix configuration structure
8
+
9
+ - **GIVEN** a `TaskStep` definition
10
+ - **THEN** it SHALL support a `matrix` property defined as a mapping of variable names to arrays of values (e.g., `Record<string, any[]>`).
11
+
12
+ ### Requirement: Matrix Permutation Expansion
13
+
14
+ The `TaskRunner` engine SHALL dynamically expand a task with a `matrix` configuration into multiple independent task instances.
15
+
16
+ #### Scenario: Single dimension matrix
17
+
18
+ - **WHEN** a task defines a single dimension matrix (e.g., `matrix: { env: ['dev', 'prod'] }`)
19
+ - **THEN** the engine SHALL generate independent task instances for each value.
20
+
21
+ #### Scenario: Multi-dimensional matrix
22
+
23
+ - **WHEN** a task defines a multi-dimensional matrix (e.g., `matrix: { os: ['linux', 'windows'], node: [18, 20] }`)
24
+ - **THEN** the engine SHALL generate independent task instances for every combination (Cartesian product) of the matrix dimensions.
25
+
26
+ ### Requirement: Contextual Matrix Variables
27
+
28
+ The `TaskRunner` SHALL provide the matrix permutation values to the execution context of the generated child tasks.
29
+
30
+ #### Scenario: Accessing matrix variables
31
+
32
+ - **WHEN** a child matrix task is executed for a permutation like `{ os: 'linux', node: 18 }`
33
+ - **THEN** its `run` function SHALL be able to access the permutation values via the context (e.g., `context.matrix.os` would be `'linux'` and `context.matrix.node` would be `18`).
34
+
35
+ ### Requirement: Matrix Dependency Resolution
36
+
37
+ The `TaskRunner` SHALL correctly resolve dependencies involving matrix tasks.
38
+
39
+ #### Scenario: Depending on a matrix task
40
+
41
+ - **WHEN** a standard task depends on a matrix task
42
+ - **THEN** the standard task SHALL wait for all child tasks of the matrix to complete successfully before executing.
43
+
44
+ #### Scenario: Matrix task dependencies
45
+
46
+ - **WHEN** a matrix task depends on another task
47
+ - **THEN** all generated child tasks of the matrix SHALL wait for the parent task to complete before executing.
@@ -0,0 +1,11 @@
1
+ ## 1. Implementation
2
+
3
+ - [ ] 1.1 Update `TaskStep.ts` interface to include an optional `matrix` property (e.g., `Record<string, unknown[]>`).
4
+ - [ ] 1.2 Update the `TaskRunnerBuilder` and `TaskGraph` validation to accept and validate the `matrix` property.
5
+ - [ ] 1.3 Implement a matrix permutation generator utility to compute Cartesian products of matrix dimensions.
6
+ - [ ] 1.4 Update `TaskStateManager.ts` or `TaskGraph.ts` to dynamically expand matrix steps into individual nodes during initialization, resolving dependencies appropriately.
7
+ - [ ] 1.5 Update the task execution context to provide the current matrix permutation variables to the `run` function.
8
+ - [ ] 1.6 Update the Mermaid Graph utility in `TaskRunner.ts` to correctly output nodes and edges for the dynamically generated matrix steps.
9
+ - [ ] 1.7 Add unit tests covering matrix generation, execution, failure handling, and context passing.
10
+ - [ ] 1.8 Add benchmarks for graph generation and state initialization with large matrix sets to ensure performance targets are met.
11
+ - [ ] 1.9 Add user-facing documentation for the `matrix` feature, including examples.
@@ -0,0 +1,16 @@
1
+ # Change: Workflow Observability and Logging Enhancements
2
+
3
+ ## Why
4
+
5
+ As workflows scale in complexity and run in diverse environments (CI/CD, containerized workers, local development), tracking their execution becomes difficult. Currently, users have to manually hook into the `EventBus` to build their own loggers or metrics exporters. Providing built-in, standardized observability tools (like a structured logger or CLI output formatter) will significantly improve the Developer Experience (DX) and reduce boilerplate for debugging complex task graphs.
6
+
7
+ ## What Changes
8
+
9
+ - Introduce a standardized `LoggerPlugin` (or similar logging abstraction) that hooks into the existing `EventBus`.
10
+ - Support multiple output formats: `text` (human-readable for CLI) and `json` (structured logging for ingestion by tools like Datadog, ELK, CloudWatch).
11
+ - Capture and format key metrics automatically: task duration, error stack traces, and workflow summaries.
12
+
13
+ ## Impact
14
+
15
+ - Affected specs: `task-runner` (adding observability capabilities)
16
+ - Affected code: `src/plugins/LoggerPlugin.ts` (new), `TaskRunnerBuilder` (optional `.withLogger()` integration).
@@ -0,0 +1,14 @@
1
+ ## ADDED Requirements
2
+
3
+ ### Requirement: Built-in Observability Logging
4
+
5
+ The system SHALL provide a standardized mechanism to log workflow and task execution events in both human-readable and structured formats.
6
+
7
+ #### Scenario: CLI Text Logging
8
+ - **WHEN** a workflow is executed with the text logger enabled
9
+ - **THEN** it SHALL output human-readable lifecycle events (start, success, failure, skip) to the console.
10
+
11
+ #### Scenario: Structured JSON Logging
12
+ - **WHEN** a workflow is executed with the JSON logger enabled
13
+ - **THEN** it SHALL output structured JSON objects for each lifecycle event, suitable for machine ingestion.
14
+ - **AND** the JSON object SHALL contain task name, timestamp, duration, and status.
@@ -0,0 +1,7 @@
1
+ ## 1. Implementation
2
+
3
+ - [ ] 1.1 Create `LoggerPlugin` implementing the `Plugin` interface.
4
+ - [ ] 1.2 Implement human-readable text formatting for CLI.
5
+ - [ ] 1.3 Implement structured JSON formatting for log ingestion.
6
+ - [ ] 1.4 Add a fluent method to `TaskRunnerBuilder` (e.g., `.withLogger()`) to easily attach the plugin.
7
+ - [ ] 1.5 Write unit tests for the logging outputs and integration.
@@ -0,0 +1,19 @@
1
+ # plugin-context Specification
2
+
3
+ ## Purpose
4
+ TBD - created by archiving change implement-plugin-system. Update Purpose after archive.
5
+ ## Requirements
6
+ ### Requirement: Expose EventBus to plugins
7
+
8
+ The `PluginContext` passed to `install` MUST expose the `EventBus` (or equivalent API) to allow listening to events.
9
+
10
+ #### Scenario: Listening to task events
11
+ Given a plugin is being installed
12
+ When it accesses `context.events`
13
+ Then it can subscribe to `taskStart` and `taskEnd` events
14
+
15
+ #### Scenario: Emitting custom events (Future)
16
+ Given a plugin
17
+ When it has access to the event bus
18
+ Then it should be able to emit events (if allowed by design)
19
+
@@ -0,0 +1,29 @@
1
+ # plugin-loading Specification
2
+
3
+ ## Purpose
4
+ TBD - created by archiving change implement-plugin-system. Update Purpose after archive.
5
+ ## Requirements
6
+ ### Requirement: Support registering plugins
7
+
8
+ The `TaskRunner` MUST allow registering plugins via a `use` method or configuration.
9
+
10
+ #### Scenario: Registering a valid plugin
11
+ Given a `TaskRunner` instance
12
+ When I call `use` with a valid plugin object
13
+ Then the plugin is added to the internal plugin list
14
+
15
+ #### Scenario: Registering an invalid plugin
16
+ Given a `TaskRunner` instance
17
+ When I call `use` with an invalid object (missing install method)
18
+ Then it should throw an error or reject the plugin
19
+
20
+ ### Requirement: Initialize plugins before execution
21
+
22
+ Plugins MUST be initialized (have their `install` method called) before the workflow starts.
23
+
24
+ #### Scenario: Plugin initialization
25
+ Given a registered plugin
26
+ When `execute` is called on the `TaskRunner`
27
+ Then the plugin's `install` method is called with the plugin context
28
+ And the workflow execution proceeds only after `install` completes
29
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calmo/task-runner",
3
- "version": "4.1.0",
3
+ "version": "4.3.0",
4
4
  "description": "A lightweight, type-safe, and domain-agnostic task orchestration engine. It resolves a Directed Acyclic Graph (DAG) of steps, executes independent tasks in parallel, and manages a shared context across the pipeline.",
5
5
  "repository": {
6
6
  "type": "git",
package/src/EventBus.ts CHANGED
@@ -40,10 +40,9 @@ export class EventBus<TContext> {
40
40
  event: K,
41
41
  callback: RunnerEventListener<TContext, K>
42
42
  ): void {
43
- if (this.listeners[event]) {
44
- (this.listeners[event] as Set<RunnerEventListener<TContext, K>>).delete(
45
- callback
46
- );
43
+ const listeners = this.listeners[event];
44
+ if (listeners) {
45
+ listeners.delete(callback);
47
46
  }
48
47
  }
49
48
 
@@ -56,16 +55,13 @@ export class EventBus<TContext> {
56
55
  event: K,
57
56
  data: RunnerEventPayloads<TContext>[K]
58
57
  ): void {
59
- const listeners = this.listeners[event] as
60
- | Set<RunnerEventListener<TContext, K>>
61
- | undefined;
58
+ const listeners = this.listeners[event];
62
59
  if (listeners) {
63
60
  for (const listener of listeners) {
64
- // We use Promise.resolve().then() to schedule the listener on the microtask queue,
61
+ // We use queueMicrotask() to schedule the listener on the microtask queue,
65
62
  // ensuring the emit method remains non-blocking.
66
- // The final .catch() ensures that any errors in the promise infrastructure itself are logged.
67
- Promise.resolve()
68
- .then(() => {
63
+ queueMicrotask(() => {
64
+ try {
69
65
  try {
70
66
  const result = listener(data);
71
67
  if (result instanceof Promise) {
@@ -84,14 +80,14 @@ export class EventBus<TContext> {
84
80
  error
85
81
  );
86
82
  }
87
- })
88
- .catch((error) => {
89
- // detailed handling for the promise chain itself
83
+ } catch (error) {
84
+ // detailed handling for the microtask execution itself
90
85
  console.error(
91
86
  `Unexpected error in event bus execution for ${String(event)}:`,
92
87
  error
93
88
  );
94
- });
89
+ }
90
+ });
95
91
  }
96
92
  }
97
93
  }
@@ -0,0 +1,43 @@
1
+ import { Plugin, PluginContext } from "./contracts/Plugin.js";
2
+
3
+ /**
4
+ * Manages the lifecycle of plugins.
5
+ */
6
+ export class PluginManager<TContext> {
7
+ private plugins: Plugin<TContext>[] = [];
8
+
9
+ constructor(private context: PluginContext<TContext>) {}
10
+
11
+ /**
12
+ * Registers a plugin.
13
+ * @param plugin The plugin to register.
14
+ */
15
+ public use(plugin: Plugin<TContext>): void {
16
+ // Check if plugin is already registered
17
+ if (this.plugins.some((p) => p.name === plugin.name)) {
18
+ // For now, we allow overwriting or just warn?
19
+ // Let's just allow it but maybe log it if we had a logger.
20
+ // Strict check: don't allow duplicate names.
21
+ throw new Error(`Plugin with name '${plugin.name}' is already registered.`);
22
+ }
23
+ this.plugins.push(plugin);
24
+ }
25
+
26
+ /**
27
+ * Initializes all registered plugins in parallel.
28
+ */
29
+ public async initialize(): Promise<void> {
30
+ const installPromises = this.plugins
31
+ .map((plugin) => plugin.install(this.context))
32
+ .filter((result): result is Promise<void> => result instanceof Promise);
33
+
34
+ await Promise.all(installPromises);
35
+ }
36
+
37
+ /**
38
+ * Returns the list of registered plugins.
39
+ */
40
+ public getPlugins(): ReadonlyArray<Plugin<TContext>> {
41
+ return this.plugins;
42
+ }
43
+ }
@@ -1,7 +1,7 @@
1
1
  import { ITaskGraphValidator } from "./contracts/ITaskGraphValidator.js";
2
2
  import { ValidationResult } from "./contracts/ValidationResult.js";
3
3
  import { ValidationError } from "./contracts/ValidationError.js";
4
- import { TaskGraph } from "./TaskGraph.js";
4
+ import { TaskGraph, Task } from "./TaskGraph.js";
5
5
  import {
6
6
  ERROR_CYCLE,
7
7
  ERROR_DUPLICATE_TASK,
@@ -22,11 +22,11 @@ export class TaskGraphValidator implements ITaskGraphValidator {
22
22
  validate(taskGraph: TaskGraph): ValidationResult {
23
23
  const errors: ValidationError[] = [];
24
24
 
25
- // 1. Check for duplicate tasks
26
- const taskIds = this.checkDuplicateTasks(taskGraph, errors);
25
+ // 1. Build Map and Check Duplicates (Single Pass)
26
+ const taskMap = this.buildTaskMapAndCheckDuplicates(taskGraph, errors);
27
27
 
28
28
  // 2. Check for missing dependencies
29
- this.checkMissingDependencies(taskGraph, taskIds, errors);
29
+ this.checkMissingDependencies(taskGraph, taskMap, errors);
30
30
 
31
31
  // 3. Check for cycles
32
32
  // Only run cycle detection if there are no missing dependencies, otherwise we might chase non-existent nodes.
@@ -35,7 +35,7 @@ export class TaskGraphValidator implements ITaskGraphValidator {
35
35
  );
36
36
 
37
37
  if (!hasMissingDependencies) {
38
- this.checkCycles(taskGraph, errors);
38
+ this.checkCycles(taskGraph, taskMap, errors);
39
39
  }
40
40
 
41
41
  return {
@@ -54,33 +54,33 @@ export class TaskGraphValidator implements ITaskGraphValidator {
54
54
  return `Task graph validation failed: ${errorDetails.join("; ")}`;
55
55
  }
56
56
 
57
- private checkDuplicateTasks(
57
+ private buildTaskMapAndCheckDuplicates(
58
58
  taskGraph: TaskGraph,
59
59
  errors: ValidationError[]
60
- ): Set<string> {
61
- const taskIds = new Set<string>();
60
+ ): Map<string, Task> {
61
+ const taskMap = new Map<string, Task>();
62
62
  for (const task of taskGraph.tasks) {
63
- if (taskIds.has(task.id)) {
63
+ if (taskMap.has(task.id)) {
64
64
  errors.push({
65
65
  type: ERROR_DUPLICATE_TASK,
66
66
  message: `Duplicate task detected with ID: ${task.id}`,
67
67
  details: { taskId: task.id },
68
68
  });
69
69
  } else {
70
- taskIds.add(task.id);
70
+ taskMap.set(task.id, task);
71
71
  }
72
72
  }
73
- return taskIds;
73
+ return taskMap;
74
74
  }
75
75
 
76
76
  private checkMissingDependencies(
77
77
  taskGraph: TaskGraph,
78
- taskIds: Set<string>,
78
+ taskMap: Map<string, Task>,
79
79
  errors: ValidationError[]
80
80
  ): void {
81
81
  for (const task of taskGraph.tasks) {
82
82
  for (const dependenceId of task.dependencies) {
83
- if (!taskIds.has(dependenceId)) {
83
+ if (!taskMap.has(dependenceId)) {
84
84
  errors.push({
85
85
  type: ERROR_MISSING_DEPENDENCY,
86
86
  message: `Task '${task.id}' depends on missing task '${dependenceId}'`,
@@ -91,13 +91,11 @@ export class TaskGraphValidator implements ITaskGraphValidator {
91
91
  }
92
92
  }
93
93
 
94
- private checkCycles(taskGraph: TaskGraph, errors: ValidationError[]): void {
95
- // Build adjacency list
96
- const adjacencyList = new Map<string, string[]>();
97
- for (const task of taskGraph.tasks) {
98
- adjacencyList.set(task.id, task.dependencies);
99
- }
100
-
94
+ private checkCycles(
95
+ taskGraph: TaskGraph,
96
+ taskMap: Map<string, Task>,
97
+ errors: ValidationError[]
98
+ ): void {
101
99
  const visited = new Set<string>();
102
100
  const recursionStack = new Set<string>();
103
101
 
@@ -108,7 +106,7 @@ export class TaskGraphValidator implements ITaskGraphValidator {
108
106
 
109
107
  const path: string[] = [];
110
108
  if (
111
- this.detectCycle(task.id, path, visited, recursionStack, adjacencyList)
109
+ this.detectCycle(task.id, path, visited, recursionStack, taskMap)
112
110
  ) {
113
111
  // Extract the actual cycle from the path
114
112
  // The path might look like A -> B -> C -> B (if we started at A and found cycle B-C-B)
@@ -132,7 +130,7 @@ export class TaskGraphValidator implements ITaskGraphValidator {
132
130
  path: string[],
133
131
  visited: Set<string>,
134
132
  recursionStack: Set<string>,
135
- adjacencyList: Map<string, string[]>
133
+ taskMap: Map<string, Task>
136
134
  ): boolean {
137
135
  // Use an explicit stack to avoid maximum call stack size exceeded errors
138
136
  const stack: { taskId: string; index: number; dependencies: string[] }[] =
@@ -145,7 +143,7 @@ export class TaskGraphValidator implements ITaskGraphValidator {
145
143
  stack.push({
146
144
  taskId: startTaskId,
147
145
  index: 0,
148
- dependencies: adjacencyList.get(startTaskId)!,
146
+ dependencies: taskMap.get(startTaskId)!.dependencies,
149
147
  });
150
148
 
151
149
  while (stack.length > 0) {
@@ -170,7 +168,7 @@ export class TaskGraphValidator implements ITaskGraphValidator {
170
168
  stack.push({
171
169
  taskId: dependenceId,
172
170
  index: 0,
173
- dependencies: adjacencyList.get(dependenceId)!,
171
+ dependencies: taskMap.get(dependenceId)!.dependencies,
174
172
  });
175
173
  }
176
174
  } else {