@calmo/task-runner 4.0.4 → 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/.github/workflows/codeql.yml +1 -1
- package/.github/workflows/release-please.yml +2 -2
- package/.jules/nexus.md +10 -0
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +3 -0
- package/CHANGELOG.md +46 -0
- package/CODE_OF_CONDUCT.md +131 -0
- package/CONTRIBUTING.md +89 -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/TaskResult.d.ts +9 -0
- package/dist/TaskRunner.d.ts +8 -1
- package/dist/TaskRunner.js +64 -41
- package/dist/TaskRunner.js.map +1 -1
- package/dist/TaskStateManager.d.ts +22 -6
- package/dist/TaskStateManager.js +105 -45
- package/dist/TaskStateManager.js.map +1 -1
- package/dist/WorkflowExecutor.js +36 -20
- 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.d.ts +1 -1
- package/dist/strategies/DryRunExecutionStrategy.js +2 -4
- package/dist/strategies/DryRunExecutionStrategy.js.map +1 -1
- package/dist/utils/PriorityQueue.d.ts +13 -0
- package/dist/utils/PriorityQueue.js +82 -0
- package/dist/utils/PriorityQueue.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/proposal.md +18 -0
- package/openspec/changes/add-resource-concurrency/specs/task-runner/spec.md +25 -0
- package/openspec/changes/add-resource-concurrency/tasks.md +10 -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/{feat-task-metrics → archive/2026-01-22-feat-task-metrics}/proposal.md +1 -1
- package/openspec/changes/archive/2026-01-22-feat-task-metrics/tasks.md +6 -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-conditional-retries/proposal.md +18 -0
- package/openspec/changes/feat-conditional-retries/specs/task-runner/spec.md +23 -0
- package/openspec/changes/feat-conditional-retries/tasks.md +37 -0
- package/openspec/changes/feat-state-persistence/specs/task-runner/spec.md +47 -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/openspec/specs/release-pr/spec.md +31 -0
- package/openspec/specs/task-runner/spec.md +12 -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/TaskResult.ts +9 -0
- package/src/TaskRunner.ts +78 -46
- package/src/TaskStateManager.ts +118 -46
- package/src/WorkflowExecutor.ts +45 -22
- package/src/contracts/Plugin.ts +32 -0
- package/src/strategies/DryRunExecutionStrategy.ts +2 -3
- package/src/utils/PriorityQueue.ts +101 -0
- package/openspec/changes/feat-task-metrics/tasks.md +0 -6
- /package/openspec/changes/{adopt-release-pr → archive/2026-01-22-adopt-release-pr}/design.md +0 -0
- /package/openspec/changes/{adopt-release-pr → archive/2026-01-22-adopt-release-pr}/proposal.md +0 -0
- /package/openspec/changes/{adopt-release-pr → archive/2026-01-22-adopt-release-pr}/specs/release-pr/spec.md +0 -0
- /package/openspec/changes/{adopt-release-pr → archive/2026-01-22-adopt-release-pr}/tasks.md +0 -0
- /package/openspec/changes/{feat-task-metrics → archive/2026-01-22-feat-task-metrics}/specs/001-generic-task-runner/spec.md +0 -0
|
@@ -12,5 +12,5 @@ export declare class DryRunExecutionStrategy<TContext> implements IExecutionStra
|
|
|
12
12
|
* @param signal Optional abort signal (ignored).
|
|
13
13
|
* @returns A promise resolving to a success result.
|
|
14
14
|
*/
|
|
15
|
-
execute(
|
|
15
|
+
execute(step: TaskStep<TContext>, _context: TContext, _signal?: AbortSignal): Promise<TaskResult>;
|
|
16
16
|
}
|
|
@@ -9,16 +9,14 @@ export class DryRunExecutionStrategy {
|
|
|
9
9
|
* @param signal Optional abort signal (ignored).
|
|
10
10
|
* @returns A promise resolving to a success result.
|
|
11
11
|
*/
|
|
12
|
-
async execute(
|
|
13
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
14
|
-
_step,
|
|
12
|
+
async execute(step,
|
|
15
13
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
16
14
|
_context,
|
|
17
15
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
18
16
|
_signal) {
|
|
19
17
|
return Promise.resolve({
|
|
20
18
|
status: "success",
|
|
21
|
-
message: "Dry run: simulated success",
|
|
19
|
+
message: "Dry run: simulated success " + step.name,
|
|
22
20
|
});
|
|
23
21
|
}
|
|
24
22
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DryRunExecutionStrategy.js","sourceRoot":"","sources":["../../src/strategies/DryRunExecutionStrategy.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,OAAO,uBAAuB;IAGlC;;;;;;OAMG;IACH,KAAK,CAAC,OAAO
|
|
1
|
+
{"version":3,"file":"DryRunExecutionStrategy.js","sourceRoot":"","sources":["../../src/strategies/DryRunExecutionStrategy.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,OAAO,uBAAuB;IAGlC;;;;;;OAMG;IACH,KAAK,CAAC,OAAO,CACX,IAAwB;IACxB,6DAA6D;IAC7D,QAAkB;IAClB,6DAA6D;IAC7D,OAAqB;QAErB,OAAO,OAAO,CAAC,OAAO,CAAC;YACrB,MAAM,EAAE,SAAS;YACjB,OAAO,EAAE,6BAA6B,GAAG,IAAI,CAAC,IAAI;SACnD,CAAC,CAAC;IACL,CAAC;CACF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare class PriorityQueue<T> {
|
|
2
|
+
private heap;
|
|
3
|
+
private sequenceCounter;
|
|
4
|
+
push(item: T, priority: number): void;
|
|
5
|
+
pop(): T | undefined;
|
|
6
|
+
peek(): T | undefined;
|
|
7
|
+
size(): number;
|
|
8
|
+
isEmpty(): boolean;
|
|
9
|
+
clear(): void;
|
|
10
|
+
private bubbleUp;
|
|
11
|
+
private sinkDown;
|
|
12
|
+
private compare;
|
|
13
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export class PriorityQueue {
|
|
2
|
+
heap = [];
|
|
3
|
+
sequenceCounter = 0;
|
|
4
|
+
push(item, priority) {
|
|
5
|
+
const node = { item, priority, sequenceId: this.sequenceCounter++ };
|
|
6
|
+
this.heap.push(node);
|
|
7
|
+
this.bubbleUp();
|
|
8
|
+
}
|
|
9
|
+
pop() {
|
|
10
|
+
if (this.heap.length === 0)
|
|
11
|
+
return undefined;
|
|
12
|
+
if (this.heap.length === 1)
|
|
13
|
+
return this.heap.pop().item;
|
|
14
|
+
const top = this.heap[0];
|
|
15
|
+
this.heap[0] = this.heap.pop();
|
|
16
|
+
this.sinkDown();
|
|
17
|
+
return top.item;
|
|
18
|
+
}
|
|
19
|
+
peek() {
|
|
20
|
+
return this.heap[0]?.item;
|
|
21
|
+
}
|
|
22
|
+
size() {
|
|
23
|
+
return this.heap.length;
|
|
24
|
+
}
|
|
25
|
+
isEmpty() {
|
|
26
|
+
return this.heap.length === 0;
|
|
27
|
+
}
|
|
28
|
+
clear() {
|
|
29
|
+
this.heap = [];
|
|
30
|
+
this.sequenceCounter = 0;
|
|
31
|
+
}
|
|
32
|
+
bubbleUp() {
|
|
33
|
+
let index = this.heap.length - 1;
|
|
34
|
+
const element = this.heap[index];
|
|
35
|
+
while (index > 0) {
|
|
36
|
+
const parentIndex = Math.floor((index - 1) / 2);
|
|
37
|
+
const parent = this.heap[parentIndex];
|
|
38
|
+
if (this.compare(element, parent) <= 0)
|
|
39
|
+
break;
|
|
40
|
+
this.heap[index] = parent;
|
|
41
|
+
this.heap[parentIndex] = element;
|
|
42
|
+
index = parentIndex;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
sinkDown() {
|
|
46
|
+
let index = 0;
|
|
47
|
+
const length = this.heap.length;
|
|
48
|
+
const element = this.heap[0];
|
|
49
|
+
while (true) {
|
|
50
|
+
const leftChildIndex = 2 * index + 1;
|
|
51
|
+
const rightChildIndex = 2 * index + 2;
|
|
52
|
+
let swapIndex = null;
|
|
53
|
+
if (leftChildIndex < length) {
|
|
54
|
+
if (this.compare(this.heap[leftChildIndex], element) > 0) {
|
|
55
|
+
swapIndex = leftChildIndex;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (rightChildIndex < length) {
|
|
59
|
+
const rightChild = this.heap[rightChildIndex];
|
|
60
|
+
const leftChild = this.heap[leftChildIndex];
|
|
61
|
+
if ((swapIndex === null && this.compare(rightChild, element) > 0) ||
|
|
62
|
+
(swapIndex !== null && this.compare(rightChild, leftChild) > 0)) {
|
|
63
|
+
swapIndex = rightChildIndex;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (swapIndex === null)
|
|
67
|
+
break;
|
|
68
|
+
this.heap[index] = this.heap[swapIndex];
|
|
69
|
+
this.heap[swapIndex] = element;
|
|
70
|
+
index = swapIndex;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Returns positive if a > b (a should come before b)
|
|
74
|
+
compare(a, b) {
|
|
75
|
+
if (a.priority !== b.priority) {
|
|
76
|
+
return a.priority - b.priority;
|
|
77
|
+
}
|
|
78
|
+
// Lower sequenceId means earlier insertion, so it has higher priority
|
|
79
|
+
return b.sequenceId - a.sequenceId;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=PriorityQueue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PriorityQueue.js","sourceRoot":"","sources":["../../src/utils/PriorityQueue.ts"],"names":[],"mappings":"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"}
|
|
@@ -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).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Change: Add Resource-Based Concurrency Control
|
|
2
|
+
|
|
3
|
+
## Why
|
|
4
|
+
|
|
5
|
+
Global concurrency limits (`concurrency: 5`) are too blunt for workflows accessing heterogeneous resources. A workflow might bottleneck on a slow API (e.g., Jira) while underutilizing a fast one (e.g., Redis). Users need to define concurrency limits *per resource type* (e.g., "max 2 Jira requests", "max 50 DB writes") to optimize throughput without overloading specific downstream services.
|
|
6
|
+
|
|
7
|
+
## What Changes
|
|
8
|
+
|
|
9
|
+
- **TaskStep Interface**: Add `resources` property (e.g., `{ cpu: 1, github_api: 1 }`).
|
|
10
|
+
- **TaskRunnerExecutionConfig**: Add `resourceLimits` property (e.g., `{ github_api: 5 }`).
|
|
11
|
+
- **WorkflowExecutor**: Update the `processLoop` to check if *both* global concurrency AND specific resource limits are satisfied before starting a task.
|
|
12
|
+
- **State Management**: Track currently consumed resources in `WorkflowExecutor`.
|
|
13
|
+
|
|
14
|
+
## Impact
|
|
15
|
+
|
|
16
|
+
- **Affected Specs**: `task-runner`
|
|
17
|
+
- **Affected Code**: `src/TaskStep.ts`, `src/TaskRunnerExecutionConfig.ts`, `src/WorkflowExecutor.ts`.
|
|
18
|
+
- **Breaking Changes**: None. Optional properties added.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: Resource-Based Concurrency Control
|
|
4
|
+
|
|
5
|
+
The system SHALL support limiting concurrency based on abstract resources defined by tasks.
|
|
6
|
+
|
|
7
|
+
#### Scenario: Task defines resource usage
|
|
8
|
+
- **GIVEN** a `TaskStep` with `resources: { "api_call": 1 }`
|
|
9
|
+
- **WHEN** the task is executed
|
|
10
|
+
- **THEN** the system SHALL account for 1 unit of "api_call" consumption.
|
|
11
|
+
|
|
12
|
+
#### Scenario: Execution limited by resource availability
|
|
13
|
+
- **GIVEN** `resourceLimits` is configured with `{ "db_connection": 2 }`
|
|
14
|
+
- **AND** 3 tasks are ready, each requiring 1 "db_connection"
|
|
15
|
+
- **WHEN** execution proceeds
|
|
16
|
+
- **THEN** only 2 tasks SHALL execute concurrently.
|
|
17
|
+
- **AND** the 3rd task SHALL wait until resources are released.
|
|
18
|
+
|
|
19
|
+
#### Scenario: Global and Resource limits combined
|
|
20
|
+
- **GIVEN** `concurrency` is set to 5
|
|
21
|
+
- **AND** `resourceLimits` is `{ "heavy_job": 2 }`
|
|
22
|
+
- **AND** 10 tasks are ready: 5 "heavy_job" tasks and 5 "light" tasks (no resources)
|
|
23
|
+
- **WHEN** execution proceeds
|
|
24
|
+
- **THEN** at most 2 "heavy_job" tasks SHALL run.
|
|
25
|
+
- **AND** up to 3 "light" tasks MAY run concurrently (totaling 5).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
## 1. Implementation
|
|
2
|
+
|
|
3
|
+
- [ ] 1.1 Update `TaskStep` interface in `src/TaskStep.ts` to include `resources?: Record<string, number>`.
|
|
4
|
+
- [ ] 1.2 Update `TaskRunnerExecutionConfig` in `src/TaskRunnerExecutionConfig.ts` to include `resourceLimits?: Record<string, number>`.
|
|
5
|
+
- [ ] 1.3 Update `WorkflowExecutor` to track active resource usage.
|
|
6
|
+
- [ ] 1.4 Update `WorkflowExecutor.processLoop` to validate resource availability before scheduling tasks.
|
|
7
|
+
- [ ] 1.5 Update `WorkflowExecutor.executeTaskStep` (or equivalent completion logic) to release resources when a task finishes.
|
|
8
|
+
- [ ] 1.6 Add unit tests for resource limiting in `tests/WorkflowExecutor.test.ts`.
|
|
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 -->
|
package/openspec/changes/{feat-task-metrics → archive/2026-01-22-feat-task-metrics}/proposal.md
RENAMED
|
@@ -7,7 +7,7 @@ Users currently lack visibility into the performance of individual tasks within
|
|
|
7
7
|
## What Changes
|
|
8
8
|
|
|
9
9
|
- Update `TaskResult` interface to include an optional `metrics` property containing `startTime`, `endTime`, and `duration`.
|
|
10
|
-
- Update `WorkflowExecutor` to capture these timestamps during task execution and populate the `metrics` property.
|
|
10
|
+
- Update `WorkflowExecutor` to capture these timestamps using `performance.now()` for high-precision timing during task execution and populate the `metrics` property.
|
|
11
11
|
- Ensure these metrics are available in the final `TaskResult` map returned by `TaskRunner.execute`.
|
|
12
12
|
|
|
13
13
|
## Impact
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
## 1. Implementation
|
|
2
|
+
|
|
3
|
+
- [x] 1.1 Update `TaskResult` interface in `src/TaskResult.ts` to include `metrics`.
|
|
4
|
+
- [x] 1.2 Update `WorkflowExecutor.ts` to capture start/end times using `performance.now()` and calculate duration.
|
|
5
|
+
- [x] 1.3 Update `WorkflowExecutor.ts` to inject metrics into the `TaskResult`.
|
|
6
|
+
- [x] 1.4 Add unit tests in `tests/TaskMetrics.test.ts` to verify metrics are present and correct.
|
|
@@ -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,18 @@
|
|
|
1
|
+
# Change: Conditional Retries
|
|
2
|
+
|
|
3
|
+
## Why
|
|
4
|
+
|
|
5
|
+
Currently, the `RetryingExecutionStrategy` treats all task failures equally. If a task has retry attempts configured, it will blindly retry even if the error is permanent (e.g., syntax error, invalid configuration) or logic-based (e.g., validation failure). This wastes resources and execution time. Users need a way to specify *which* errors should trigger a retry, allowing them to fail fast on critical errors while retrying on transient ones (e.g., network timeouts).
|
|
6
|
+
|
|
7
|
+
## What Changes
|
|
8
|
+
|
|
9
|
+
- Update `TaskRetryConfig` interface in `src/contracts/TaskRetryConfig.ts` to include an optional `shouldRetry` predicate.
|
|
10
|
+
- Update `RetryingExecutionStrategy` in `src/strategies/RetryingExecutionStrategy.ts` to evaluate this predicate (if present) before deciding to retry.
|
|
11
|
+
- If `shouldRetry` returns `false`, the retry loop is broken immediately, and the failure result is returned.
|
|
12
|
+
- If `shouldRetry` is undefined, the existing behavior (retry on any failure) is preserved.
|
|
13
|
+
|
|
14
|
+
## Impact
|
|
15
|
+
|
|
16
|
+
- **Affected specs**: `001-generic-task-runner` (or simply `task-runner`)
|
|
17
|
+
- **Affected code**: `src/contracts/TaskRetryConfig.ts`, `src/strategies/RetryingExecutionStrategy.ts`
|
|
18
|
+
- **Non-breaking change**: The `shouldRetry` property is optional. Existing retry configurations will continue to work as before.
|