@calmo/task-runner 3.4.0 → 3.5.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/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 +21 -16
- package/CHANGELOG.md +192 -174
- package/README.md +95 -88
- package/coverage/coverage-final.json +9 -9
- package/coverage/index.html +9 -9
- package/coverage/lcov-report/index.html +9 -9
- package/coverage/lcov-report/src/EventBus.ts.html +30 -24
- 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 +82 -52
- package/coverage/lcov-report/src/WorkflowExecutor.ts.html +210 -66
- 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 +16 -16
- 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 +7 -7
- package/coverage/lcov-report/src/strategies/index.html +1 -1
- package/coverage/lcov.info +426 -383
- package/coverage/src/EventBus.ts.html +30 -24
- 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 +82 -52
- package/coverage/src/WorkflowExecutor.ts.html +210 -66
- package/coverage/src/contracts/RunnerEvents.ts.html +1 -1
- package/coverage/src/contracts/index.html +1 -1
- package/coverage/src/index.html +16 -16
- 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 +7 -7
- 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/TaskStateManager.d.ts +6 -0
- package/dist/TaskStateManager.js +11 -2
- package/dist/TaskStateManager.js.map +1 -1
- package/dist/TaskStep.d.ts +5 -0
- package/dist/WorkflowExecutor.js +49 -7
- 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-feat-conditional-execution/proposal.md +35 -0
- package/openspec/changes/archive/2026-01-18-feat-conditional-execution/tasks.md +32 -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 +2 -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/TaskStateManager.ts +12 -2
- package/src/TaskStep.ts +6 -0
- package/src/WorkflowExecutor.ts +63 -15
- 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 +132 -108
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 {
|
|
@@ -64,7 +66,8 @@ export class WorkflowExecutor<TContext> {
|
|
|
64
66
|
|
|
65
67
|
while (
|
|
66
68
|
this.stateManager.hasPendingTasks() ||
|
|
67
|
-
this.stateManager.hasRunningTasks()
|
|
69
|
+
this.stateManager.hasRunningTasks() ||
|
|
70
|
+
executingPromises.size > 0
|
|
68
71
|
) {
|
|
69
72
|
// Safety check: if no tasks are running and we still have pending tasks,
|
|
70
73
|
// it means we are stuck (e.g. cycle or unhandled dependency).
|
|
@@ -77,10 +80,10 @@ export class WorkflowExecutor<TContext> {
|
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
if (signal?.aborted) {
|
|
80
|
-
|
|
83
|
+
this.stateManager.cancelAllPending("Workflow cancelled.");
|
|
81
84
|
} else {
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
// After a task finishes, check for new work
|
|
86
|
+
this.processLoop(executingPromises, signal);
|
|
84
87
|
}
|
|
85
88
|
}
|
|
86
89
|
|
|
@@ -122,17 +125,62 @@ export class WorkflowExecutor<TContext> {
|
|
|
122
125
|
|
|
123
126
|
const step = this.readyQueue.shift()!;
|
|
124
127
|
|
|
125
|
-
|
|
128
|
+
const taskPromise = (async () => {
|
|
129
|
+
try {
|
|
130
|
+
if (step.condition) {
|
|
131
|
+
const check = step.condition(this.context);
|
|
132
|
+
const shouldRun = check instanceof Promise ? await check : check;
|
|
133
|
+
|
|
134
|
+
if (signal?.aborted) {
|
|
135
|
+
this.stateManager.markCompleted(step, {
|
|
136
|
+
status: "cancelled",
|
|
137
|
+
message: "Cancelled during condition evaluation.",
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!shouldRun) {
|
|
143
|
+
const result: TaskResult = {
|
|
144
|
+
status: "skipped",
|
|
145
|
+
message: "Skipped by condition evaluation.",
|
|
146
|
+
};
|
|
147
|
+
this.stateManager.markSkipped(step, result);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} catch (error) {
|
|
152
|
+
const result: TaskResult = {
|
|
153
|
+
status: "failure",
|
|
154
|
+
message:
|
|
155
|
+
error instanceof Error
|
|
156
|
+
? error.message
|
|
157
|
+
: "Condition evaluation failed",
|
|
158
|
+
error: error instanceof Error ? error.message : String(error),
|
|
159
|
+
};
|
|
160
|
+
this.stateManager.markCompleted(step, result);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (signal?.aborted) {
|
|
165
|
+
this.stateManager.markCompleted(step, {
|
|
166
|
+
status: "cancelled",
|
|
167
|
+
message: "Cancelled before execution started.",
|
|
168
|
+
});
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.stateManager.markRunning(step);
|
|
126
173
|
|
|
127
|
-
|
|
128
|
-
|
|
174
|
+
await this.strategy
|
|
175
|
+
.execute(step, this.context, signal)
|
|
176
|
+
.then((result) => {
|
|
129
177
|
this.stateManager.markCompleted(step, result);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
178
|
+
});
|
|
179
|
+
})().finally(() => {
|
|
180
|
+
executingPromises.delete(taskPromise);
|
|
181
|
+
// When a task finishes, we try to run more
|
|
182
|
+
this.processLoop(executingPromises, signal);
|
|
183
|
+
});
|
|
136
184
|
|
|
137
185
|
executingPromises.add(taskPromise);
|
|
138
186
|
}
|
|
@@ -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",
|