@calmo/task-runner 4.1.0 → 4.2.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 (60) 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 +27 -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 +16 -16
  15. package/dist/EventBus.js.map +1 -1
  16. package/dist/PluginManager.d.ts +22 -0
  17. package/dist/PluginManager.js +39 -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 +29 -19
  24. package/dist/TaskRunner.js.map +1 -1
  25. package/dist/TaskStateManager.js +9 -5
  26. package/dist/TaskStateManager.js.map +1 -1
  27. package/dist/WorkflowExecutor.js +19 -10
  28. package/dist/WorkflowExecutor.js.map +1 -1
  29. package/dist/contracts/Plugin.d.ts +30 -0
  30. package/dist/contracts/Plugin.js +2 -0
  31. package/dist/contracts/Plugin.js.map +1 -0
  32. package/openspec/changes/add-middleware-support/proposal.md +19 -0
  33. package/openspec/changes/add-middleware-support/specs/task-runner/spec.md +34 -0
  34. package/openspec/changes/add-middleware-support/tasks.md +9 -0
  35. package/openspec/changes/add-resource-concurrency/tasks.md +1 -0
  36. package/openspec/changes/allow-plugin-hooks/design.md +51 -0
  37. package/openspec/changes/allow-plugin-hooks/proposal.md +12 -0
  38. package/openspec/changes/allow-plugin-hooks/specs/post-task/spec.md +21 -0
  39. package/openspec/changes/allow-plugin-hooks/specs/pre-task/spec.md +32 -0
  40. package/openspec/changes/allow-plugin-hooks/tasks.md +7 -0
  41. package/openspec/changes/archive/2026-02-15-implement-plugin-system/design.md +45 -0
  42. package/openspec/changes/archive/2026-02-15-implement-plugin-system/proposal.md +13 -0
  43. package/openspec/changes/archive/2026-02-15-implement-plugin-system/specs/plugin-context/spec.md +17 -0
  44. package/openspec/changes/archive/2026-02-15-implement-plugin-system/specs/plugin-loading/spec.md +27 -0
  45. package/openspec/changes/archive/2026-02-15-implement-plugin-system/tasks.md +7 -0
  46. package/openspec/changes/feat-completion-dependencies/proposal.md +58 -0
  47. package/openspec/changes/feat-completion-dependencies/tasks.md +46 -0
  48. package/openspec/changes/feat-task-loop/proposal.md +22 -0
  49. package/openspec/changes/feat-task-loop/specs/task-runner/spec.md +34 -0
  50. package/openspec/changes/feat-task-loop/tasks.md +8 -0
  51. package/openspec/specs/plugin-context/spec.md +19 -0
  52. package/openspec/specs/plugin-loading/spec.md +29 -0
  53. package/package.json +1 -1
  54. package/src/EventBus.ts +7 -8
  55. package/src/PluginManager.ts +41 -0
  56. package/src/TaskGraphValidator.ts +22 -24
  57. package/src/TaskRunner.ts +36 -23
  58. package/src/TaskStateManager.ts +9 -5
  59. package/src/WorkflowExecutor.ts +24 -11
  60. package/src/contracts/Plugin.ts +32 -0
@@ -0,0 +1,19 @@
1
+ # Change: Add Middleware Support
2
+
3
+ ## Why
4
+
5
+ Developers currently duplicate code for cross-cutting concerns like logging, error handling policies, context validation, and metrics across every task definition. This leads to code duplication, inconsistent behavior, and maintenance burden. There is no standard way to inject logic "around" task execution globally.
6
+
7
+ ## What Changes
8
+
9
+ - **Middleware Interface**: Introduce a `Middleware<T>` type representing a function that wraps task execution.
10
+ - **TaskRunnerBuilder**: Add a `.use(middleware)` method to register middleware functions.
11
+ - **TaskRunner**:
12
+ - Store the chain of middleware.
13
+ - During execution, wrap the `Strategy.execute` call with the middleware chain (onion model).
14
+ - Ensure middleware runs *before* the task starts and *after* it finishes (or fails).
15
+
16
+ ## Impact
17
+
18
+ - Affected specs: `task-runner`
19
+ - Affected code: `TaskRunner.ts`, `TaskRunnerBuilder.ts`, `WorkflowExecutor.ts`, `contracts/Middleware.ts`
@@ -0,0 +1,34 @@
1
+ ## ADDED Requirements
2
+
3
+ ### Requirement: Global Middleware Support
4
+
5
+ The system SHALL support registering global middleware functions that intercept and wrap the execution of every `TaskStep`.
6
+
7
+ #### Scenario: Middleware Execution Order
8
+
9
+ - **GIVEN** a `TaskRunner` with two middleware functions: `A` (outer) and `B` (inner)
10
+ - **WHEN** a task is executed
11
+ - **THEN** `A` runs before `B`
12
+ - **AND** `B` runs before the task strategy
13
+ - **AND** the task strategy runs
14
+ - **AND** `B` runs after the task strategy
15
+ - **AND** `A` runs after `B`.
16
+
17
+ #### Scenario: Modifying Task Result
18
+
19
+ - **GIVEN** a middleware that modifies the returned result
20
+ - **WHEN** a task completes successfully
21
+ - **THEN** the final result stored in the runner MUST match the result returned by the middleware.
22
+
23
+ #### Scenario: Blocking Execution
24
+
25
+ - **GIVEN** a middleware that returns a result *without* calling `next()`
26
+ - **WHEN** the task is scheduled
27
+ - **THEN** the task strategy SHALL NOT be executed
28
+ - **AND** the task result SHALL be the one returned by the middleware.
29
+
30
+ #### Scenario: Context Access
31
+
32
+ - **GIVEN** a middleware function
33
+ - **WHEN** it is invoked
34
+ - **THEN** it SHALL have access to the current `TaskStep` and shared `context`.
@@ -0,0 +1,9 @@
1
+ ## 1. Implementation
2
+
3
+ - [ ] 1.1 Define `Middleware` and `MiddlewareNext` types in `src/contracts/Middleware.ts`.
4
+ - [ ] 1.2 Update `TaskRunner` class to store a list of middleware functions.
5
+ - [ ] 1.3 Implement `composeMiddleware` utility to create the onion chain.
6
+ - [ ] 1.4 Update `WorkflowExecutor` or `TaskRunner` (wherever strategy is invoked) to wrap the strategy execution with composed middleware.
7
+ - [ ] 1.5 Update `TaskRunnerBuilder` to expose `.use(middleware)` method.
8
+ - [ ] 1.6 Add unit tests for Middleware composition and execution order.
9
+ - [ ] 1.7 Add integration test demonstrating a Logging Middleware and a Policy Middleware (skipping task).
@@ -7,3 +7,4 @@
7
7
  - [ ] 1.5 Update `WorkflowExecutor.executeTaskStep` (or equivalent completion logic) to release resources when a task finishes.
8
8
  - [ ] 1.6 Add unit tests for resource limiting in `tests/WorkflowExecutor.test.ts`.
9
9
  - [ ] 1.7 Add integration test for mixed resource usage in `tests/integration/resource-concurrency.test.ts`.
10
+ - [ ] 1.8 Update README.md to include docs and examples on resource concurrency configuration
@@ -0,0 +1,51 @@
1
+ # Plugin Hooks Design
2
+
3
+ ## Architecture
4
+
5
+ We will extend the `PluginManager` to manage a list of hooks. The `WorkflowExecutor` will call these hooks at appropriate times.
6
+
7
+ ### Hook Interfaces
8
+
9
+ ```typescript
10
+ export type PreTaskHookResult<TContext> =
11
+ | { action: "continue" }
12
+ | { action: "skip"; message?: string }
13
+ | { action: "fail"; error: Error };
14
+
15
+ export type PreTaskHook<TContext> = (
16
+ step: TaskStep<TContext>,
17
+ context: TContext
18
+ ) => Promise<PreTaskHookResult<TContext> | void> | void; // void implies continue
19
+
20
+ export type PostTaskHook<TContext> = (
21
+ step: TaskStep<TContext>,
22
+ context: TContext,
23
+ result: TaskResult
24
+ ) => Promise<TaskResult | void> | void; // void implies return original result
25
+ ```
26
+
27
+ ### PluginContext Extension
28
+
29
+ ```typescript
30
+ export interface PluginContext<TContext> {
31
+ events: EventBus<TContext>;
32
+ preTask(hook: PreTaskHook<TContext>): void;
33
+ postTask(hook: PostTaskHook<TContext>): void;
34
+ }
35
+ ```
36
+
37
+ ### Execution Flow
38
+
39
+ **In `WorkflowExecutor.executeTaskStep`:**
40
+
41
+ 1. **Run Pre-Hooks (Sequential)**
42
+ - Iterate through registered pre-hooks.
43
+ - If any returns `skip`, mark task skipped and return.
44
+ - If any returns `fail`, mark task failed and return.
45
+ - (Future: allow modifying context contextually? Context is mutable so yes).
46
+
47
+ 2. **Execute Task** (Original logic)
48
+
49
+ 3. **Run Post-Hooks (Sequential)**
50
+ - Pass the result to hooks.
51
+ - Hooks can return a new `TaskResult` to overwrite the current one.
@@ -0,0 +1,12 @@
1
+ # Allow Plugin hooks
2
+
3
+ ## Summary
4
+ Introduce pre-task and post-task hooks to the Plugin System, allowing plugins to intercept task execution, modify behavior, or transform results.
5
+
6
+ ## Motivation
7
+ Current plugins are limited to "observation" via read-only events. To enable advanced use cases like caching, validation, or policy enforcement, plugins need to be able to intervene in the execution flow.
8
+
9
+ ## Scope
10
+ - **Pre-Task Hooks**: Runs before a task. Can skip, fail, or modify task/context.
11
+ - **Post-Task Hooks**: Runs after a task. Can modify the result.
12
+ - **Hook Registration**: Add `registerPreHook` and `registerPostHook` to `PluginContext`.
@@ -0,0 +1,21 @@
1
+ # Post-Task Hooks Requirements
2
+
3
+ ## ADDED Requirements
4
+
5
+ ### Requirement: Execute Post-Task Hooks after task execution
6
+
7
+ The `WorkflowExecutor` MUST execute all registered post-task hooks after a task completes (success or failure).
8
+
9
+ #### Scenario: Running post hooks
10
+ Given registered post-task hooks
11
+ When a task finishes execution
12
+ Then hooks are executed sequentially with access to the task result
13
+
14
+ ### Requirement: Modify result via hook
15
+
16
+ The runner MUST allow post-task hooks to modify the task result.
17
+
18
+ #### Scenario: Modifying result status
19
+ Given a post-task hook that returns a new `TaskResult`
20
+ When a task finishes
21
+ Then the final result of the task is updated to the one returned by the hook
@@ -0,0 +1,32 @@
1
+ # Pre-Task Hooks Requirements
2
+
3
+ ## ADDED Requirements
4
+
5
+ ### Requirement: Execute Pre-Task Hooks before task execution
6
+
7
+ The `WorkflowExecutor` MUST execute all registered pre-task hooks before running a task.
8
+
9
+ #### Scenario: Running multiple hooks
10
+ Given multiple registered pre-task hooks
11
+ When a task is about to run
12
+ Then hooks are executed sequentially in registration order
13
+
14
+ ### Requirement: Skip task execution via hook
15
+
16
+ The runner MUST skip task execution if a pre-task hook returns a skip action.
17
+
18
+ #### Scenario: Skipping task
19
+ Given a pre-task hook that returns `{ action: "skip" }`
20
+ When a task is about to run
21
+ Then the task is marked as skipped
22
+ And execution proceeds to the next task
23
+
24
+ ### Requirement: Fail task execution via hook
25
+
26
+ The runner MUST fail task execution if a pre-task hook returns a fail action.
27
+
28
+ #### Scenario: Failing task
29
+ Given a pre-task hook that returns `{ action: "fail", error: Error("Reason") }`
30
+ When a task is about to run
31
+ Then the task is marked as failed with the provided error
32
+ And failure cascades to dependent tasks
@@ -0,0 +1,7 @@
1
+ - [-] Implement Plugin Hooks
2
+ - [ ] Define Hook Types and Interfaces <!-- id: 0 -->
3
+ - [ ] Update `PluginContext` to include `preTask` and `postTask` <!-- id: 1 -->
4
+ - [ ] Implement Hook Registry in `PluginManager` <!-- id: 2 -->
5
+ - [ ] Integrate Pre-Task Hooks in `WorkflowExecutor` <!-- id: 3 -->
6
+ - [ ] Integrate Post-Task Hooks in `WorkflowExecutor` <!-- id: 4 -->
7
+ - [ ] Add tests for hook execution and interruption <!-- id: 5 -->
@@ -0,0 +1,45 @@
1
+ # Plugin System Design
2
+
3
+ ## Architecture
4
+
5
+ The plugin system will resolve around a `PluginManager` that is owned by the `TaskRunner`.
6
+
7
+ ### Plugin Interface
8
+
9
+ A plugin is defined as:
10
+
11
+ ```typescript
12
+ export interface Plugin<TContext> {
13
+ name: string;
14
+ version: string;
15
+ install(context: PluginContext<TContext>): void | Promise<void>;
16
+ }
17
+ ```
18
+
19
+ ### Plugin Context
20
+
21
+ The `install` method receives a `PluginContext`, which exposes capabilities to the plugin:
22
+
23
+ ```typescript
24
+ export interface PluginContext<TContext> {
25
+ events: EventBus<TContext>; // Access to listen/emit events
26
+ // Potentially other APIs in the future, e.g., registerTask, logger, etc.
27
+ }
28
+ ```
29
+
30
+ ### Lifecycle
31
+
32
+ 1. **Registration**: Plugins are registered via `taskRunner.use(plugin)`.
33
+ 2. **Installation**: When `taskRunner.execute()` is called (or explicitly before), the `PluginManager` iterates over registered plugins and calls `install()`.
34
+ 3. **Execution**: Plugins listen to events or hooks during execution.
35
+
36
+ ### Error Handling
37
+
38
+ - Implementation should handle plugin failures during `install` gracefully (fail fast or log and continue, defined by config).
39
+ - Runtime errors in listeners should be caught to avoid crashing the runner, though `EventBus` current implementation might need review.
40
+
41
+ ## Components
42
+
43
+ - `src/contracts/Plugin.ts`: Interface definition.
44
+ - `src/PluginManager.ts`: Manages the list of plugins and their initialization.
45
+ - `src/TaskRunner.ts`: Modified to include `use()` method and delegate to `PluginManager`.
@@ -0,0 +1,13 @@
1
+ # Propose Plugin System
2
+
3
+ ## Summary
4
+ Introduce a plugin system to the `task-runner` that allows external modules to alter behavior, listen to events, and interact with the execution context via a defined API.
5
+
6
+ ## Motivation
7
+ Currently, extending the `task-runner` requires modifying the core codebase. To enable a more collaborative and decentralized development model, we need a way for developers to inject custom logic, middleware, or event listeners without touching the core.
8
+
9
+ ## Scope
10
+ - Define a strict `Plugin` interface.
11
+ - Implement a `PluginManager` to handle plugin lifecycle (registration, initialization).
12
+ - Expose an `EventBus` and `PluginContext` to plugins.
13
+ - Ensure plugins can intercept or react to workflow lifecycle events (`workflowStart`, `taskStart`, etc.).
@@ -0,0 +1,17 @@
1
+ # Plugin Context Requirements
2
+
3
+ ## ADDED Requirements
4
+
5
+ ### Requirement: Expose EventBus to plugins
6
+
7
+ The `PluginContext` passed to `install` MUST expose the `EventBus` (or equivalent API) to allow listening to events.
8
+
9
+ #### Scenario: Listening to task events
10
+ Given a plugin is being installed
11
+ When it accesses `context.events`
12
+ Then it can subscribe to `taskStart` and `taskEnd` events
13
+
14
+ #### Scenario: Emitting custom events (Future)
15
+ Given a plugin
16
+ When it has access to the event bus
17
+ Then it should be able to emit events (if allowed by design)
@@ -0,0 +1,27 @@
1
+ # Plugin Loading Requirements
2
+
3
+ ## ADDED Requirements
4
+
5
+ ### Requirement: Support registering plugins
6
+
7
+ The `TaskRunner` MUST allow registering plugins via a `use` method or configuration.
8
+
9
+ #### Scenario: Registering a valid plugin
10
+ Given a `TaskRunner` instance
11
+ When I call `use` with a valid plugin object
12
+ Then the plugin is added to the internal plugin list
13
+
14
+ #### Scenario: Registering an invalid plugin
15
+ Given a `TaskRunner` instance
16
+ When I call `use` with an invalid object (missing install method)
17
+ Then it should throw an error or reject the plugin
18
+
19
+ ### Requirement: Initialize plugins before execution
20
+
21
+ Plugins MUST be initialized (have their `install` method called) before the workflow starts.
22
+
23
+ #### Scenario: Plugin initialization
24
+ Given a registered plugin
25
+ When `execute` is called on the `TaskRunner`
26
+ Then the plugin's `install` method is called with the plugin context
27
+ And the workflow execution proceeds only after `install` completes
@@ -0,0 +1,7 @@
1
+ - [x] Implement Plugin System
2
+ - [x] Define `Plugin` and `PluginContext` interfaces <!-- id: 0 -->
3
+ - [x] Implement `PluginManager` class <!-- id: 1 -->
4
+ - [x] Update `TaskRunner` to support `use()` method <!-- id: 2 -->
5
+ - [x] Integrate `PluginManager` into `TaskRunner` lifecycle <!-- id: 3 -->
6
+ - [x] Verify `EventBus` exposure to plugins <!-- id: 4 -->
7
+ - [x] Add integration tests for a sample plugin <!-- id: 5 -->
@@ -0,0 +1,58 @@
1
+ # Feature: Completion Dependencies (Finally Tasks)
2
+
3
+ ## User Story
4
+ "As a developer, I want to define tasks that run even if their dependencies fail (e.g., cleanup/teardown), so that I can ensure resources are released and the system is left in a clean state regardless of workflow success."
5
+
6
+ ## The "Why"
7
+ Currently, the `TaskRunner` strictly propagates failures: if Task A fails, all tasks depending on A are automatically skipped. This behavior is correct for logical dependencies (e.g., "Build" -> "Deploy"), but strictly prohibits "Teardown" or "Compensation" patterns (e.g., "Provision" -> "Test" -> "Deprovision").
8
+
9
+ If "Test" fails, "Deprovision" is skipped, leaving expensive resources running.
10
+
11
+ While a `continueOnError` proposal exists, it marks a task as "Optional" (allowing *all* dependents to proceed). It does not support the case where:
12
+ 1. "Test" is CRITICAL (if it fails, the workflow should eventually fail).
13
+ 2. "Deprovision" MUST run after "Test".
14
+ 3. "Publish Results" (dependent on "Test") MUST skip if "Test" fails.
15
+
16
+ We need a way to define **Dependency Behavior** at the edge level.
17
+
18
+ ## The "What"
19
+ We will extend the `dependencies` property in `TaskStep` to support granular configuration.
20
+
21
+ ### Current
22
+ ```typescript
23
+ dependencies: ["TaskA", "TaskB"]
24
+ ```
25
+
26
+ ### Proposed
27
+ ```typescript
28
+ dependencies: [
29
+ "TaskA",
30
+ { step: "TaskB", runCondition: "always" } // or 'complete'
31
+ ]
32
+ ```
33
+
34
+ The `runCondition` determines when the dependent becomes ready:
35
+ - `success` (Default): Ready only if dependency succeeds.
36
+ - `always`: Ready if dependency succeeds OR fails. (If dependency is *skipped*, the dependent is still skipped, as the parent never ran).
37
+
38
+ ## Acceptance Criteria
39
+ - [ ] Support `dependencies` as an array of `string | DependencyConfig`.
40
+ - [ ] `DependencyConfig` schema: `{ step: string; runCondition?: 'success' | 'always' }`.
41
+ - [ ] **Scenario 1 (Success):** A -> B(always). A succeeds. B runs.
42
+ - [ ] **Scenario 2 (Failure):** A -> B(always). A fails. B runs.
43
+ - [ ] **Scenario 3 (Skip):** X(fail) -> A -> B(always). X fails, A skips. B skips (because A never ran).
44
+ - [ ] **Scenario 4 (Hybrid):** A(fail) -> B(always) -> C(standard).
45
+ - A fails.
46
+ - B runs (cleanup).
47
+ - C skips (because B succeeded, but C implicitly depends on the *chain*? No, standard DAG. C depends on B. If B succeeds, C runs. Wait.)
48
+
49
+ **Refining Scenario 4:**
50
+ If C depends on B, and B runs (cleaning up A), C runs.
51
+ This might not be desired if C assumes A succeeded.
52
+ *Constraint:* If C depends on B, it only cares about B. If C cares about A, it must depend on A explicitly.
53
+ *User Responsibility:* Users must ensure that if C depends on B (cleanup), C can handle the fact that A might have failed. Or, C should also depend on A (standard) if it needs A's success.
54
+
55
+ ## Constraints
56
+ - **Backward Compatibility:** Existing `string[]` syntax must work exactly as before.
57
+ - **Cycle Detection:** The validator must treat `{ step: "A" }` identical to `"A"` for cycle checks.
58
+ - **Type Safety:** The context passed to the task remains `TContext`. The task must safeguard against missing data if a dependency failed (e.g., checking `ctx.data` before use).
@@ -0,0 +1,46 @@
1
+ # Engineering Tasks: Completion Dependencies
2
+
3
+ ## 1. Contracts & Types
4
+ - [ ] **Modify `src/TaskStep.ts`**
5
+ - Define `export type TaskRunCondition = 'success' | 'always';`
6
+ - Define `export interface TaskDependencyConfig { step: string; runCondition?: TaskRunCondition; }`
7
+ - Update `TaskStep` interface: `dependencies?: (string | TaskDependencyConfig)[];`
8
+
9
+ ## 2. Validation Logic
10
+ - [ ] **Update `src/TaskGraphValidator.ts`**
11
+ - Update `validate` method to handle mixed string/object arrays.
12
+ - Extract the dependency name correctly for cycle detection and adjacency lists.
13
+ - Ensure `checkMissingDependencies` checks the `step` property of config objects.
14
+
15
+ ## 3. Core Logic (TaskStateManager)
16
+ - [ ] **Update `src/TaskStateManager.ts`**
17
+ - **Data Structures**:
18
+ - Change `dependencyGraph` to store metadata: `Map<string, { step: TaskStep<TContext>, condition: TaskRunCondition }[]>`.
19
+ - **Initialization**:
20
+ - Parse the mixed `dependencies` array during `initialize`.
21
+ - Store the `runCondition` (default 'success') in the graph.
22
+ - **Failure Handling (`cascadeFailure`)**:
23
+ - When a task fails (or is skipped? No, only Failures trigger 'always'. Skips should still cascade Skips):
24
+ - Iterate dependents.
25
+ - If dependent has `condition === 'always'`:
26
+ - Treat as "success" (call `handleSuccess` logic or equivalent: decrement count).
27
+ - Do NOT add to `cascade` queue.
28
+ - If dependent has `condition === 'success'`:
29
+ - Mark skipped (existing logic).
30
+ - Add to `cascade` queue.
31
+ - **Skip Handling**:
32
+ - If a task is SKIPPED, dependents with `runCondition: 'always'` should ALSO be skipped (because the parent never ran).
33
+ - Ensure `cascadeFailure` distinguishes between "Failed" vs "Skipped" when checking the condition.
34
+
35
+ ## 4. Testing
36
+ - [ ] **Unit Tests (`tests/TaskStateManager.test.ts`)**
37
+ - Test initialization with mixed types.
38
+ - Test failure propagation with 'always' condition (dependent runs).
39
+ - Test skip propagation with 'always' condition (dependent skips).
40
+ - [ ] **Integration Tests (`tests/TaskRunner.test.ts`)**
41
+ - Create a "Teardown" scenario:
42
+ - Step 1: Setup (Success)
43
+ - Step 2: Work (Failure) -> Depends on 1
44
+ - Step 3: Cleanup (Success) -> Depends on 2 (Always)
45
+ - Verify Step 3 runs.
46
+ - Verify final Workflow status (should be Failure because Step 2 failed).
@@ -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,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.2.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
@@ -61,11 +61,10 @@ export class EventBus<TContext> {
61
61
  | undefined;
62
62
  if (listeners) {
63
63
  for (const listener of listeners) {
64
- // We use Promise.resolve().then() to schedule the listener on the microtask queue,
64
+ // We use queueMicrotask() to schedule the listener on the microtask queue,
65
65
  // 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(() => {
66
+ queueMicrotask(() => {
67
+ try {
69
68
  try {
70
69
  const result = listener(data);
71
70
  if (result instanceof Promise) {
@@ -84,14 +83,14 @@ export class EventBus<TContext> {
84
83
  error
85
84
  );
86
85
  }
87
- })
88
- .catch((error) => {
89
- // detailed handling for the promise chain itself
86
+ } catch (error) {
87
+ // detailed handling for the microtask execution itself
90
88
  console.error(
91
89
  `Unexpected error in event bus execution for ${String(event)}:`,
92
90
  error
93
91
  );
94
- });
92
+ }
93
+ });
95
94
  }
96
95
  }
97
96
  }
@@ -0,0 +1,41 @@
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.
28
+ */
29
+ public async initialize(): Promise<void> {
30
+ for (const plugin of this.plugins) {
31
+ await plugin.install(this.context);
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Returns the list of registered plugins.
37
+ */
38
+ public getPlugins(): ReadonlyArray<Plugin<TContext>> {
39
+ return this.plugins;
40
+ }
41
+ }