@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.
- package/.jules/nexus.md +5 -0
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +2 -0
- package/CHANGELOG.md +27 -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 +16 -16
- package/dist/EventBus.js.map +1 -1
- package/dist/PluginManager.d.ts +22 -0
- package/dist/PluginManager.js +39 -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 +29 -19
- package/dist/TaskRunner.js.map +1 -1
- package/dist/TaskStateManager.js +9 -5
- package/dist/TaskStateManager.js.map +1 -1
- 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/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-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/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 +7 -8
- package/src/PluginManager.ts +41 -0
- package/src/TaskGraphValidator.ts +22 -24
- package/src/TaskRunner.ts +36 -23
- package/src/TaskStateManager.ts +9 -5
- package/src/WorkflowExecutor.ts +24 -11
- 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.).
|
package/openspec/changes/archive/2026-02-15-implement-plugin-system/specs/plugin-context/spec.md
ADDED
|
@@ -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)
|
package/openspec/changes/archive/2026-02-15-implement-plugin-system/specs/plugin-loading/spec.md
ADDED
|
@@ -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.
|
|
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
|
|
64
|
+
// We use queueMicrotask() to schedule the listener on the microtask queue,
|
|
65
65
|
// ensuring the emit method remains non-blocking.
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
+
}
|