@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.
- package/.jules/nexus.md +5 -0
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +2 -0
- package/CHANGELOG.md +51 -0
- package/README.md +34 -0
- package/conductor/code_styleguides/general.md +23 -0
- package/conductor/code_styleguides/javascript.md +51 -0
- package/conductor/code_styleguides/typescript.md +43 -0
- package/conductor/product-guidelines.md +14 -0
- package/conductor/product.md +16 -0
- package/conductor/setup_state.json +1 -0
- package/conductor/tech-stack.md +19 -0
- package/conductor/workflow.md +334 -0
- package/dist/EventBus.js +19 -18
- package/dist/EventBus.js.map +1 -1
- package/dist/PluginManager.d.ts +22 -0
- package/dist/PluginManager.js +40 -0
- package/dist/PluginManager.js.map +1 -0
- package/dist/TaskGraphValidator.d.ts +1 -1
- package/dist/TaskGraphValidator.js +16 -21
- package/dist/TaskGraphValidator.js.map +1 -1
- package/dist/TaskRunner.d.ts +8 -1
- package/dist/TaskRunner.js +37 -25
- package/dist/TaskRunner.js.map +1 -1
- package/dist/TaskStateManager.d.ts +1 -0
- package/dist/TaskStateManager.js +22 -6
- package/dist/TaskStateManager.js.map +1 -1
- package/dist/TaskStep.d.ts +12 -0
- package/dist/WorkflowExecutor.js +19 -10
- package/dist/WorkflowExecutor.js.map +1 -1
- package/dist/contracts/Plugin.d.ts +30 -0
- package/dist/contracts/Plugin.js +2 -0
- package/dist/contracts/Plugin.js.map +1 -0
- package/dist/strategies/DryRunExecutionStrategy.js +2 -2
- package/dist/strategies/DryRunExecutionStrategy.js.map +1 -1
- package/dist/strategies/StandardExecutionStrategy.js +43 -1
- package/dist/strategies/StandardExecutionStrategy.js.map +1 -1
- package/openspec/changes/add-middleware-support/proposal.md +19 -0
- package/openspec/changes/add-middleware-support/specs/task-runner/spec.md +34 -0
- package/openspec/changes/add-middleware-support/tasks.md +9 -0
- package/openspec/changes/add-resource-concurrency/tasks.md +1 -0
- package/openspec/changes/allow-plugin-hooks/design.md +51 -0
- package/openspec/changes/allow-plugin-hooks/proposal.md +12 -0
- package/openspec/changes/allow-plugin-hooks/specs/post-task/spec.md +21 -0
- package/openspec/changes/allow-plugin-hooks/specs/pre-task/spec.md +32 -0
- package/openspec/changes/allow-plugin-hooks/tasks.md +7 -0
- package/openspec/changes/archive/2026-02-15-implement-plugin-system/design.md +45 -0
- package/openspec/changes/archive/2026-02-15-implement-plugin-system/proposal.md +13 -0
- package/openspec/changes/archive/2026-02-15-implement-plugin-system/specs/plugin-context/spec.md +17 -0
- package/openspec/changes/archive/2026-02-15-implement-plugin-system/specs/plugin-loading/spec.md +27 -0
- package/openspec/changes/archive/2026-02-15-implement-plugin-system/tasks.md +7 -0
- package/openspec/changes/feat-completion-dependencies/proposal.md +58 -0
- package/openspec/changes/feat-completion-dependencies/tasks.md +46 -0
- package/openspec/changes/feat-task-caching/design.md +34 -0
- package/openspec/changes/feat-task-caching/proposal.md +18 -0
- package/openspec/changes/feat-task-caching/specs/task-runner/spec.md +58 -0
- package/openspec/changes/feat-task-caching/tasks.md +24 -0
- package/openspec/changes/feat-task-loop/proposal.md +22 -0
- package/openspec/changes/feat-task-loop/specs/task-runner/spec.md +34 -0
- package/openspec/changes/feat-task-loop/tasks.md +8 -0
- package/openspec/proposals/feat-matrix-execution/proposal.md +23 -0
- package/openspec/proposals/feat-matrix-execution/specs/task-runner/spec.md +47 -0
- package/openspec/proposals/feat-matrix-execution/tasks.md +11 -0
- package/openspec/proposals/feat-task-observability/proposal.md +16 -0
- package/openspec/proposals/feat-task-observability/specs/task-runner/spec.md +14 -0
- package/openspec/proposals/feat-task-observability/tasks.md +7 -0
- package/openspec/specs/plugin-context/spec.md +19 -0
- package/openspec/specs/plugin-loading/spec.md +29 -0
- package/package.json +1 -1
- package/src/EventBus.ts +11 -15
- package/src/PluginManager.ts +43 -0
- package/src/TaskGraphValidator.ts +22 -24
- package/src/TaskRunner.ts +45 -28
- package/src/TaskStateManager.ts +20 -6
- package/src/TaskStep.ts +14 -0
- package/src/WorkflowExecutor.ts +24 -11
- package/src/contracts/Plugin.ts +32 -0
- package/src/strategies/DryRunExecutionStrategy.ts +2 -2
- package/src/strategies/StandardExecutionStrategy.ts +48 -1
- /package/openspec/changes/{feat-continue-on-error → archive/2026-02-18-feat-continue-on-error}/proposal.md +0 -0
- /package/openspec/changes/{feat-continue-on-error → archive/2026-02-18-feat-continue-on-error}/tasks.md +0 -0
- /package/openspec/changes/{feat-per-task-timeout → archive/2026-02-25-feat-per-task-timeout}/proposal.md +0 -0
- /package/openspec/changes/{feat-per-task-timeout → archive/2026-02-25-feat-per-task-timeout}/specs/task-runner/spec.md +0 -0
- /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.
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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]
|
|
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
|
|
61
|
+
// We use queueMicrotask() to schedule the listener on the microtask queue,
|
|
65
62
|
// ensuring the emit method remains non-blocking.
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
|
26
|
-
const
|
|
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,
|
|
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
|
|
57
|
+
private buildTaskMapAndCheckDuplicates(
|
|
58
58
|
taskGraph: TaskGraph,
|
|
59
59
|
errors: ValidationError[]
|
|
60
|
-
):
|
|
61
|
-
const
|
|
60
|
+
): Map<string, Task> {
|
|
61
|
+
const taskMap = new Map<string, Task>();
|
|
62
62
|
for (const task of taskGraph.tasks) {
|
|
63
|
-
if (
|
|
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
|
-
|
|
70
|
+
taskMap.set(task.id, task);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
-
return
|
|
73
|
+
return taskMap;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
private checkMissingDependencies(
|
|
77
77
|
taskGraph: TaskGraph,
|
|
78
|
-
|
|
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 (!
|
|
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(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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:
|
|
171
|
+
dependencies: taskMap.get(dependenceId)!.dependencies,
|
|
174
172
|
});
|
|
175
173
|
}
|
|
176
174
|
} else {
|