@calmo/task-runner 3.4.0 → 3.4.1
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/dependabot.yml +7 -7
- package/.github/workflows/ci.yml +4 -4
- package/.jules/backlog_maniac.md +1 -0
- package/.jules/nexus.md +1 -0
- package/.jules/sentinel.md +1 -0
- package/.releaserc.json +2 -7
- package/AGENTS.md +8 -2
- package/CHANGELOG.md +178 -174
- package/README.md +23 -23
- package/coverage/coverage-final.json +8 -8
- package/coverage/index.html +7 -7
- package/coverage/lcov-report/index.html +7 -7
- package/coverage/lcov-report/src/EventBus.ts.html +27 -21
- package/coverage/lcov-report/src/TaskGraphValidationError.ts.html +12 -3
- package/coverage/lcov-report/src/TaskGraphValidator.ts.html +152 -137
- package/coverage/lcov-report/src/TaskRunner.ts.html +48 -45
- package/coverage/lcov-report/src/TaskRunnerBuilder.ts.html +29 -5
- package/coverage/lcov-report/src/TaskRunnerExecutionConfig.ts.html +1 -1
- package/coverage/lcov-report/src/TaskStateManager.ts.html +1 -1
- package/coverage/lcov-report/src/WorkflowExecutor.ts.html +21 -12
- package/coverage/lcov-report/src/contracts/RunnerEvents.ts.html +1 -1
- package/coverage/lcov-report/src/contracts/index.html +1 -1
- package/coverage/lcov-report/src/index.html +8 -8
- package/coverage/lcov-report/src/strategies/DryRunExecutionStrategy.ts.html +4 -4
- package/coverage/lcov-report/src/strategies/RetryingExecutionStrategy.ts.html +29 -11
- package/coverage/lcov-report/src/strategies/StandardExecutionStrategy.ts.html +5 -5
- package/coverage/lcov-report/src/strategies/index.html +1 -1
- package/coverage/lcov.info +266 -262
- package/coverage/src/EventBus.ts.html +27 -21
- package/coverage/src/TaskGraphValidationError.ts.html +12 -3
- package/coverage/src/TaskGraphValidator.ts.html +152 -137
- package/coverage/src/TaskRunner.ts.html +48 -45
- package/coverage/src/TaskRunnerBuilder.ts.html +29 -5
- package/coverage/src/TaskRunnerExecutionConfig.ts.html +1 -1
- package/coverage/src/TaskStateManager.ts.html +1 -1
- package/coverage/src/WorkflowExecutor.ts.html +21 -12
- package/coverage/src/contracts/RunnerEvents.ts.html +1 -1
- package/coverage/src/contracts/index.html +1 -1
- package/coverage/src/index.html +8 -8
- package/coverage/src/strategies/DryRunExecutionStrategy.ts.html +4 -4
- package/coverage/src/strategies/RetryingExecutionStrategy.ts.html +29 -11
- package/coverage/src/strategies/StandardExecutionStrategy.ts.html +5 -5
- package/coverage/src/strategies/index.html +1 -1
- package/dist/EventBus.js +13 -11
- package/dist/EventBus.js.map +1 -1
- package/dist/TaskGraphValidationError.js.map +1 -1
- package/dist/TaskGraphValidator.js +9 -9
- package/dist/TaskGraphValidator.js.map +1 -1
- package/dist/TaskRunner.js.map +1 -1
- package/dist/TaskRunnerBuilder.js.map +1 -1
- package/dist/WorkflowExecutor.js +2 -1
- package/dist/WorkflowExecutor.js.map +1 -1
- package/dist/strategies/RetryingExecutionStrategy.js +3 -1
- package/dist/strategies/RetryingExecutionStrategy.js.map +1 -1
- package/dist/strategies/StandardExecutionStrategy.js +1 -1
- package/dist/strategies/StandardExecutionStrategy.js.map +1 -1
- package/openspec/AGENTS.md +81 -15
- package/openspec/changes/archive/2026-01-18-add-concurrency-control/proposal.md +7 -4
- package/openspec/changes/archive/2026-01-18-add-concurrency-control/tasks.md +1 -0
- package/openspec/changes/archive/2026-01-18-add-external-task-cancellation/proposal.md +4 -1
- package/openspec/changes/archive/2026-01-18-add-external-task-cancellation/tasks.md +2 -1
- package/openspec/changes/archive/2026-01-18-add-integration-tests/proposal.md +3 -0
- package/openspec/changes/archive/2026-01-18-add-integration-tests/tasks.md +1 -0
- package/openspec/changes/archive/2026-01-18-add-task-retry-policy/proposal.md +3 -0
- package/openspec/changes/archive/2026-01-18-add-task-retry-policy/tasks.md +1 -0
- package/openspec/changes/archive/2026-01-18-add-workflow-preview/proposal.md +3 -0
- package/openspec/changes/archive/2026-01-18-add-workflow-preview/tasks.md +1 -0
- package/openspec/changes/archive/2026-01-18-refactor-core-architecture/proposal.md +3 -0
- package/openspec/changes/archive/2026-01-18-refactor-core-architecture/tasks.md +1 -0
- package/openspec/changes/feat-per-task-timeout/proposal.md +11 -6
- package/openspec/changes/feat-per-task-timeout/tasks.md +1 -1
- package/openspec/project.md +21 -15
- package/package.json +1 -1
- package/src/EventBus.ts +18 -16
- package/src/TaskGraph.ts +8 -8
- package/src/TaskGraphValidationError.ts +4 -1
- package/src/TaskGraphValidator.ts +148 -143
- package/src/TaskRunner.ts +42 -41
- package/src/TaskRunnerBuilder.ts +11 -3
- package/src/WorkflowExecutor.ts +13 -10
- package/src/contracts/ITaskGraphValidator.ts +12 -12
- package/src/contracts/ValidationError.ts +6 -6
- package/src/contracts/ValidationResult.ts +4 -4
- package/src/strategies/DryRunExecutionStrategy.ts +3 -3
- package/src/strategies/RetryingExecutionStrategy.ts +15 -9
- package/src/strategies/StandardExecutionStrategy.ts +4 -4
- package/test-report.xml +108 -108
package/src/TaskRunner.ts
CHANGED
|
@@ -2,7 +2,10 @@ import { TaskStep } from "./TaskStep.js";
|
|
|
2
2
|
import { TaskResult } from "./TaskResult.js";
|
|
3
3
|
import { TaskGraphValidator } from "./TaskGraphValidator.js";
|
|
4
4
|
import { TaskGraph } from "./TaskGraph.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
RunnerEventPayloads,
|
|
7
|
+
RunnerEventListener,
|
|
8
|
+
} from "./contracts/RunnerEvents.js";
|
|
6
9
|
import { EventBus } from "./EventBus.js";
|
|
7
10
|
import { WorkflowExecutor } from "./WorkflowExecutor.js";
|
|
8
11
|
import { TaskRunnerExecutionConfig } from "./TaskRunnerExecutionConfig.js";
|
|
@@ -24,9 +27,8 @@ export { RunnerEventPayloads, RunnerEventListener, TaskRunnerExecutionConfig };
|
|
|
24
27
|
export class TaskRunner<TContext> {
|
|
25
28
|
private eventBus = new EventBus<TContext>();
|
|
26
29
|
private validator = new TaskGraphValidator();
|
|
27
|
-
private executionStrategy: IExecutionStrategy<TContext> =
|
|
28
|
-
new StandardExecutionStrategy()
|
|
29
|
-
);
|
|
30
|
+
private executionStrategy: IExecutionStrategy<TContext> =
|
|
31
|
+
new RetryingExecutionStrategy(new StandardExecutionStrategy());
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
34
|
* @param context The shared context object to be passed to each task.
|
|
@@ -85,7 +87,6 @@ export class TaskRunner<TContext> {
|
|
|
85
87
|
const safeId = (name: string) => JSON.stringify(name);
|
|
86
88
|
const sanitize = (name: string) => this.sanitizeMermaidId(name);
|
|
87
89
|
|
|
88
|
-
|
|
89
90
|
// Add all nodes first to ensure they exist
|
|
90
91
|
for (const step of steps) {
|
|
91
92
|
// Using the name as both ID and Label for simplicity
|
|
@@ -98,9 +99,7 @@ export class TaskRunner<TContext> {
|
|
|
98
99
|
for (const step of steps) {
|
|
99
100
|
if (step.dependencies) {
|
|
100
101
|
for (const dep of step.dependencies) {
|
|
101
|
-
graphLines.push(
|
|
102
|
-
` ${sanitize(dep)} --> ${sanitize(step.name)}`
|
|
103
|
-
);
|
|
102
|
+
graphLines.push(` ${sanitize(dep)} --> ${sanitize(step.name)}`);
|
|
104
103
|
}
|
|
105
104
|
}
|
|
106
105
|
}
|
|
@@ -162,40 +161,42 @@ export class TaskRunner<TContext> {
|
|
|
162
161
|
|
|
163
162
|
// We need to handle the timeout cleanup properly.
|
|
164
163
|
if (config?.timeout !== undefined) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
164
|
+
const controller = new AbortController();
|
|
165
|
+
const timeoutId = setTimeout(() => {
|
|
166
|
+
controller.abort(
|
|
167
|
+
new Error(`Workflow timed out after ${config.timeout}ms`)
|
|
168
|
+
);
|
|
169
|
+
}, config.timeout);
|
|
170
|
+
|
|
171
|
+
let effectiveSignal = controller.signal;
|
|
172
|
+
let onAbort: (() => void) | undefined;
|
|
173
|
+
|
|
174
|
+
// Handle combination of signals if user provided one
|
|
175
|
+
if (config.signal) {
|
|
176
|
+
if (config.signal.aborted) {
|
|
177
|
+
// If already aborted, use it directly (WorkflowExecutor handles early abort)
|
|
178
|
+
// We can cancel timeout immediately
|
|
179
|
+
clearTimeout(timeoutId);
|
|
180
|
+
effectiveSignal = config.signal;
|
|
181
|
+
} else {
|
|
182
|
+
// Listen to user signal to abort our controller
|
|
183
|
+
onAbort = () => {
|
|
184
|
+
controller.abort(config.signal?.reason);
|
|
185
|
+
};
|
|
186
|
+
config.signal.addEventListener("abort", onAbort);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
return await executor.execute(steps, effectiveSignal);
|
|
192
|
+
} finally {
|
|
193
|
+
clearTimeout(timeoutId);
|
|
194
|
+
if (config.signal && onAbort) {
|
|
195
|
+
config.signal.removeEventListener("abort", onAbort);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
197
198
|
} else {
|
|
198
|
-
|
|
199
|
+
return executor.execute(steps, config?.signal);
|
|
199
200
|
}
|
|
200
201
|
}
|
|
201
202
|
}
|
package/src/TaskRunnerBuilder.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { TaskRunner } from "./TaskRunner.js";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
RunnerEventPayloads,
|
|
4
|
+
RunnerEventListener,
|
|
5
|
+
} from "./contracts/RunnerEvents.js";
|
|
3
6
|
import { IExecutionStrategy } from "./strategies/IExecutionStrategy.js";
|
|
4
7
|
|
|
5
8
|
/**
|
|
@@ -9,7 +12,10 @@ export class TaskRunnerBuilder<TContext> {
|
|
|
9
12
|
private context: TContext;
|
|
10
13
|
private strategy?: IExecutionStrategy<TContext>;
|
|
11
14
|
private listeners: {
|
|
12
|
-
[K in keyof RunnerEventPayloads<TContext>]?: RunnerEventListener<
|
|
15
|
+
[K in keyof RunnerEventPayloads<TContext>]?: RunnerEventListener<
|
|
16
|
+
TContext,
|
|
17
|
+
K
|
|
18
|
+
>[];
|
|
13
19
|
} = {};
|
|
14
20
|
|
|
15
21
|
/**
|
|
@@ -57,7 +63,9 @@ export class TaskRunnerBuilder<TContext> {
|
|
|
57
63
|
runner.setExecutionStrategy(this.strategy);
|
|
58
64
|
}
|
|
59
65
|
|
|
60
|
-
(
|
|
66
|
+
(
|
|
67
|
+
Object.keys(this.listeners) as Array<keyof RunnerEventPayloads<TContext>>
|
|
68
|
+
).forEach((event) => {
|
|
61
69
|
const callbacks = this.listeners[event];
|
|
62
70
|
// callbacks is always defined because we are iterating keys of the object
|
|
63
71
|
callbacks!.forEach((callback) =>
|
package/src/WorkflowExecutor.ts
CHANGED
|
@@ -41,7 +41,9 @@ export class WorkflowExecutor<TContext> {
|
|
|
41
41
|
|
|
42
42
|
// Check if already aborted
|
|
43
43
|
if (signal?.aborted) {
|
|
44
|
-
this.stateManager.cancelAllPending(
|
|
44
|
+
this.stateManager.cancelAllPending(
|
|
45
|
+
"Workflow cancelled before execution started."
|
|
46
|
+
);
|
|
45
47
|
const results = this.stateManager.getResults();
|
|
46
48
|
this.eventBus.emit("workflowEnd", { context: this.context, results });
|
|
47
49
|
return results;
|
|
@@ -55,7 +57,7 @@ export class WorkflowExecutor<TContext> {
|
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
if (signal) {
|
|
58
|
-
|
|
60
|
+
signal.addEventListener("abort", onAbort);
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
try {
|
|
@@ -77,10 +79,10 @@ export class WorkflowExecutor<TContext> {
|
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
if (signal?.aborted) {
|
|
80
|
-
|
|
82
|
+
this.stateManager.cancelAllPending("Workflow cancelled.");
|
|
81
83
|
} else {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
// After a task finishes, check for new work
|
|
85
|
+
this.processLoop(executingPromises, signal);
|
|
84
86
|
}
|
|
85
87
|
}
|
|
86
88
|
|
|
@@ -124,14 +126,15 @@ export class WorkflowExecutor<TContext> {
|
|
|
124
126
|
|
|
125
127
|
this.stateManager.markRunning(step);
|
|
126
128
|
|
|
127
|
-
const taskPromise = this.strategy
|
|
129
|
+
const taskPromise = this.strategy
|
|
130
|
+
.execute(step, this.context, signal)
|
|
128
131
|
.then((result) => {
|
|
129
|
-
|
|
132
|
+
this.stateManager.markCompleted(step, result);
|
|
130
133
|
})
|
|
131
134
|
.finally(() => {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
+
executingPromises.delete(taskPromise);
|
|
136
|
+
// When a task finishes, we try to run more
|
|
137
|
+
this.processLoop(executingPromises, signal);
|
|
135
138
|
});
|
|
136
139
|
|
|
137
140
|
executingPromises.add(taskPromise);
|
|
@@ -5,17 +5,17 @@ import { ValidationResult } from "./ValidationResult.js";
|
|
|
5
5
|
* Defines the interface for a task graph validator.
|
|
6
6
|
*/
|
|
7
7
|
export interface ITaskGraphValidator {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Validates a given task graph for structural integrity.
|
|
10
|
+
* @param taskGraph The task graph to validate.
|
|
11
|
+
* @returns A ValidationResult object indicating the outcome of the validation.
|
|
12
|
+
*/
|
|
13
|
+
validate(taskGraph: TaskGraph): ValidationResult;
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Creates a human-readable error message from a validation result.
|
|
17
|
+
* @param result The validation result containing errors.
|
|
18
|
+
* @returns A formatted error string.
|
|
19
|
+
*/
|
|
20
|
+
createErrorMessage(result: ValidationResult): string;
|
|
21
21
|
}
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Describes a specific validation error found in the task graph.
|
|
3
3
|
*/
|
|
4
4
|
export interface ValidationError {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
/** The type of validation error. */
|
|
6
|
+
type: "cycle" | "missing_dependency" | "duplicate_task";
|
|
7
|
+
/** A human-readable message describing the error. */
|
|
8
|
+
message: string;
|
|
9
|
+
/** Optional detailed information about the error, e.g., the cycle path, or the task with a missing dependency. */
|
|
10
|
+
details?: unknown;
|
|
11
11
|
}
|
|
@@ -4,8 +4,8 @@ import { ValidationError } from "./ValidationError.js";
|
|
|
4
4
|
* The result of a task graph validation operation.
|
|
5
5
|
*/
|
|
6
6
|
export interface ValidationResult {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
/** True if the graph is valid, false otherwise. */
|
|
8
|
+
isValid: boolean;
|
|
9
|
+
/** An array of ValidationError objects if the graph is not valid. Empty if isValid is true. */
|
|
10
|
+
errors: ValidationError[];
|
|
11
11
|
}
|
|
@@ -5,9 +5,9 @@ import { TaskResult } from "../TaskResult.js";
|
|
|
5
5
|
/**
|
|
6
6
|
* Execution strategy that simulates task execution without running the actual logic.
|
|
7
7
|
*/
|
|
8
|
-
export class DryRunExecutionStrategy<
|
|
9
|
-
|
|
10
|
-
{
|
|
8
|
+
export class DryRunExecutionStrategy<
|
|
9
|
+
TContext,
|
|
10
|
+
> implements IExecutionStrategy<TContext> {
|
|
11
11
|
/**
|
|
12
12
|
* Simulates execution by returning a success result immediately.
|
|
13
13
|
* @param step The task step (ignored).
|
|
@@ -5,7 +5,9 @@ import { TaskResult } from "../TaskResult.js";
|
|
|
5
5
|
/**
|
|
6
6
|
* Execution strategy that retries tasks upon failure based on their retry configuration.
|
|
7
7
|
*/
|
|
8
|
-
export class RetryingExecutionStrategy<
|
|
8
|
+
export class RetryingExecutionStrategy<
|
|
9
|
+
TContext,
|
|
10
|
+
> implements IExecutionStrategy<TContext> {
|
|
9
11
|
constructor(private innerStrategy: IExecutionStrategy<TContext>) {}
|
|
10
12
|
|
|
11
13
|
async execute(
|
|
@@ -30,7 +32,11 @@ export class RetryingExecutionStrategy<TContext> implements IExecutionStrategy<T
|
|
|
30
32
|
|
|
31
33
|
const result = await this.innerStrategy.execute(step, context, signal);
|
|
32
34
|
|
|
33
|
-
if (
|
|
35
|
+
if (
|
|
36
|
+
result.status === "success" ||
|
|
37
|
+
result.status === "cancelled" ||
|
|
38
|
+
result.status === "skipped"
|
|
39
|
+
) {
|
|
34
40
|
return result;
|
|
35
41
|
}
|
|
36
42
|
|
|
@@ -51,13 +57,13 @@ export class RetryingExecutionStrategy<TContext> implements IExecutionStrategy<T
|
|
|
51
57
|
try {
|
|
52
58
|
await this.sleep(delay, signal);
|
|
53
59
|
} catch (e) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
if (signal?.aborted) {
|
|
61
|
+
return {
|
|
62
|
+
status: "cancelled",
|
|
63
|
+
message: "Task cancelled during retry delay",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
throw e;
|
|
61
67
|
}
|
|
62
68
|
}
|
|
63
69
|
}
|
|
@@ -5,9 +5,9 @@ import { TaskResult } from "../TaskResult.js";
|
|
|
5
5
|
/**
|
|
6
6
|
* Standard execution strategy that runs the task's run method.
|
|
7
7
|
*/
|
|
8
|
-
export class StandardExecutionStrategy<
|
|
9
|
-
|
|
10
|
-
{
|
|
8
|
+
export class StandardExecutionStrategy<
|
|
9
|
+
TContext,
|
|
10
|
+
> implements IExecutionStrategy<TContext> {
|
|
11
11
|
async execute(
|
|
12
12
|
step: TaskStep<TContext>,
|
|
13
13
|
context: TContext,
|
|
@@ -19,7 +19,7 @@ export class StandardExecutionStrategy<TContext>
|
|
|
19
19
|
// Check if error is due to abort
|
|
20
20
|
if (
|
|
21
21
|
signal?.aborted &&
|
|
22
|
-
(e instanceof Error && e.name === "AbortError" || signal.reason === e)
|
|
22
|
+
((e instanceof Error && e.name === "AbortError") || signal.reason === e)
|
|
23
23
|
) {
|
|
24
24
|
return {
|
|
25
25
|
status: "cancelled",
|