@calmo/task-runner 2.0.0 → 3.1.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/AGENTS.md +49 -0
- package/CHANGELOG.md +33 -0
- package/README.md +1 -1
- package/coverage/coverage-final.json +9 -4
- package/coverage/index.html +24 -9
- package/coverage/lcov-report/index.html +24 -9
- package/coverage/lcov-report/src/EventBus.ts.html +8 -8
- package/coverage/lcov-report/src/TaskGraphValidator.ts.html +53 -53
- package/coverage/lcov-report/src/TaskRunner.ts.html +377 -20
- package/coverage/lcov-report/src/TaskRunnerBuilder.ts.html +313 -0
- package/coverage/lcov-report/src/TaskRunnerExecutionConfig.ts.html +139 -0
- package/coverage/lcov-report/src/TaskStateManager.ts.html +511 -0
- package/coverage/lcov-report/src/WorkflowExecutor.ts.html +119 -137
- 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 +56 -11
- package/coverage/lcov-report/src/strategies/DryRunExecutionStrategy.ts.html +178 -0
- package/coverage/lcov-report/src/strategies/StandardExecutionStrategy.ts.html +190 -0
- package/coverage/lcov-report/src/strategies/index.html +131 -0
- package/coverage/lcov.info +419 -204
- package/coverage/src/EventBus.ts.html +8 -8
- package/coverage/src/TaskGraphValidator.ts.html +53 -53
- package/coverage/src/TaskRunner.ts.html +377 -20
- package/coverage/src/TaskRunnerBuilder.ts.html +313 -0
- package/coverage/src/TaskRunnerExecutionConfig.ts.html +139 -0
- package/coverage/src/TaskStateManager.ts.html +511 -0
- package/coverage/src/WorkflowExecutor.ts.html +119 -137
- package/coverage/src/contracts/RunnerEvents.ts.html +1 -1
- package/coverage/src/contracts/index.html +1 -1
- package/coverage/src/index.html +56 -11
- package/coverage/src/strategies/DryRunExecutionStrategy.ts.html +178 -0
- package/coverage/src/strategies/StandardExecutionStrategy.ts.html +190 -0
- package/coverage/src/strategies/index.html +131 -0
- package/dist/TaskRunner.d.ts +24 -2
- package/dist/TaskRunner.js +99 -3
- package/dist/TaskRunner.js.map +1 -1
- package/dist/TaskRunnerBuilder.d.ts +33 -0
- package/dist/TaskRunnerBuilder.js +54 -0
- package/dist/TaskRunnerBuilder.js.map +1 -0
- package/dist/TaskRunnerExecutionConfig.d.ts +18 -0
- package/dist/TaskRunnerExecutionConfig.js +2 -0
- package/dist/TaskRunnerExecutionConfig.js.map +1 -0
- package/dist/TaskStateManager.d.ts +54 -0
- package/dist/TaskStateManager.js +130 -0
- package/dist/TaskStateManager.js.map +1 -0
- package/dist/TaskStatus.d.ts +1 -1
- package/dist/TaskStep.d.ts +2 -1
- package/dist/WorkflowExecutor.d.ts +11 -17
- package/dist/WorkflowExecutor.js +67 -69
- package/dist/WorkflowExecutor.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/strategies/DryRunExecutionStrategy.d.ts +16 -0
- package/dist/strategies/DryRunExecutionStrategy.js +25 -0
- package/dist/strategies/DryRunExecutionStrategy.js.map +1 -0
- package/dist/strategies/IExecutionStrategy.d.ts +16 -0
- package/dist/strategies/IExecutionStrategy.js +2 -0
- package/dist/strategies/IExecutionStrategy.js.map +1 -0
- package/dist/strategies/StandardExecutionStrategy.d.ts +9 -0
- package/dist/strategies/StandardExecutionStrategy.js +25 -0
- package/dist/strategies/StandardExecutionStrategy.js.map +1 -0
- package/openspec/changes/add-concurrency-control/proposal.md +14 -0
- package/openspec/changes/add-concurrency-control/tasks.md +9 -0
- package/openspec/changes/add-task-retry-policy/proposal.md +16 -0
- package/openspec/changes/add-task-retry-policy/tasks.md +10 -0
- package/openspec/changes/add-workflow-preview/proposal.md +13 -0
- package/openspec/changes/add-workflow-preview/tasks.md +7 -0
- package/openspec/changes/archive/2026-01-18-add-external-task-cancellation/tasks.md +10 -0
- package/openspec/changes/archive/2026-01-18-add-integration-tests/proposal.md +18 -0
- package/openspec/changes/archive/2026-01-18-add-integration-tests/tasks.md +17 -0
- package/openspec/changes/archive/2026-01-18-refactor-core-architecture/proposal.md +14 -0
- package/openspec/changes/archive/2026-01-18-refactor-core-architecture/tasks.md +8 -0
- package/openspec/changes/feat-per-task-timeout/proposal.md +25 -0
- package/openspec/changes/feat-per-task-timeout/tasks.md +27 -0
- package/package.json +1 -1
- package/src/TaskRunner.ts +123 -4
- package/src/TaskRunnerBuilder.ts +76 -0
- package/src/TaskRunnerExecutionConfig.ts +18 -0
- package/src/TaskStateManager.ts +142 -0
- package/src/TaskStatus.ts +1 -1
- package/src/TaskStep.ts +2 -1
- package/src/WorkflowExecutor.ts +77 -83
- package/src/index.ts +4 -0
- package/src/strategies/DryRunExecutionStrategy.ts +31 -0
- package/src/strategies/IExecutionStrategy.ts +21 -0
- package/src/strategies/StandardExecutionStrategy.ts +35 -0
- package/test-report.xml +139 -45
- package/GEMINI.md +0 -48
- package/openspec/changes/add-external-task-cancellation/tasks.md +0 -10
- /package/openspec/changes/{add-external-task-cancellation → archive/2026-01-18-add-external-task-cancellation}/proposal.md +0 -0
package/dist/WorkflowExecutor.js
CHANGED
|
@@ -5,97 +5,95 @@
|
|
|
5
5
|
export class WorkflowExecutor {
|
|
6
6
|
context;
|
|
7
7
|
eventBus;
|
|
8
|
-
|
|
8
|
+
stateManager;
|
|
9
|
+
strategy;
|
|
9
10
|
/**
|
|
10
11
|
* @param context The shared context object.
|
|
11
12
|
* @param eventBus The event bus to emit events.
|
|
13
|
+
* @param stateManager Manages execution state.
|
|
14
|
+
* @param strategy Execution strategy.
|
|
12
15
|
*/
|
|
13
|
-
constructor(context, eventBus) {
|
|
16
|
+
constructor(context, eventBus, stateManager, strategy) {
|
|
14
17
|
this.context = context;
|
|
15
18
|
this.eventBus = eventBus;
|
|
19
|
+
this.stateManager = stateManager;
|
|
20
|
+
this.strategy = strategy;
|
|
16
21
|
}
|
|
17
22
|
/**
|
|
18
23
|
* Executes the given steps.
|
|
19
24
|
* @param steps The list of steps to execute.
|
|
25
|
+
* @param signal Optional AbortSignal for cancellation.
|
|
20
26
|
* @returns A Promise that resolves to a map of task results.
|
|
21
27
|
*/
|
|
22
|
-
async execute(steps) {
|
|
28
|
+
async execute(steps, signal) {
|
|
23
29
|
this.eventBus.emit("workflowStart", { context: this.context, steps });
|
|
24
|
-
|
|
30
|
+
this.stateManager.initialize(steps);
|
|
31
|
+
// Check if already aborted
|
|
32
|
+
if (signal?.aborted) {
|
|
33
|
+
this.stateManager.cancelAllPending("Workflow cancelled before execution started.");
|
|
34
|
+
const results = this.stateManager.getResults();
|
|
35
|
+
this.eventBus.emit("workflowEnd", { context: this.context, results });
|
|
36
|
+
return results;
|
|
37
|
+
}
|
|
25
38
|
const executingPromises = new Set();
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
this.processQueue(steps, results, executingPromises);
|
|
39
|
+
const onAbort = () => {
|
|
40
|
+
// Mark all pending tasks as cancelled
|
|
41
|
+
this.stateManager.cancelAllPending("Workflow cancelled.");
|
|
42
|
+
};
|
|
43
|
+
if (signal) {
|
|
44
|
+
signal.addEventListener("abort", onAbort);
|
|
33
45
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
try {
|
|
47
|
+
// Initial pass
|
|
48
|
+
this.processLoop(executingPromises, signal);
|
|
49
|
+
while (this.stateManager.hasPendingTasks() ||
|
|
50
|
+
this.stateManager.hasRunningTasks()) {
|
|
51
|
+
// Safety check: if no tasks are running and we still have pending tasks,
|
|
52
|
+
// it means we are stuck (e.g. cycle or unhandled dependency).
|
|
53
|
+
// Since valid graphs shouldn't have this, we break to avoid infinite loop.
|
|
54
|
+
if (executingPromises.size === 0) {
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
// Wait for the next task to finish
|
|
59
|
+
await Promise.race(executingPromises);
|
|
60
|
+
}
|
|
61
|
+
if (signal?.aborted) {
|
|
62
|
+
this.stateManager.cancelAllPending("Workflow cancelled.");
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// After a task finishes, check for new work
|
|
66
|
+
this.processLoop(executingPromises, signal);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Ensure everything is accounted for (e.g. if loop exited early)
|
|
70
|
+
this.stateManager.cancelAllPending("Workflow cancelled.");
|
|
71
|
+
const results = this.stateManager.getResults();
|
|
72
|
+
this.eventBus.emit("workflowEnd", { context: this.context, results });
|
|
73
|
+
return results;
|
|
48
74
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
*/
|
|
53
|
-
handleSkippedTasks(steps, results) {
|
|
54
|
-
const pendingSteps = steps.filter((step) => !results.has(step.name) && !this.running.has(step.name));
|
|
55
|
-
for (const step of pendingSteps) {
|
|
56
|
-
const deps = step.dependencies ?? [];
|
|
57
|
-
const failedDep = deps.find((dep) => results.has(dep) && results.get(dep)?.status !== "success");
|
|
58
|
-
if (failedDep) {
|
|
59
|
-
const result = {
|
|
60
|
-
status: "skipped",
|
|
61
|
-
message: `Skipped due to failed dependency: ${failedDep}`,
|
|
62
|
-
};
|
|
63
|
-
results.set(step.name, result);
|
|
64
|
-
this.eventBus.emit("taskSkipped", { step, result });
|
|
75
|
+
finally {
|
|
76
|
+
if (signal) {
|
|
77
|
+
signal.removeEventListener("abort", onAbort);
|
|
65
78
|
}
|
|
66
79
|
}
|
|
67
80
|
}
|
|
68
81
|
/**
|
|
69
|
-
*
|
|
70
|
-
*/
|
|
71
|
-
getReadySteps(steps, results) {
|
|
72
|
-
return steps.filter((step) => {
|
|
73
|
-
if (results.has(step.name) || this.running.has(step.name))
|
|
74
|
-
return false;
|
|
75
|
-
const deps = step.dependencies ?? [];
|
|
76
|
-
return deps.every((dep) => results.has(dep) && results.get(dep)?.status === "success");
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Handles the lifecycle of a single task execution.
|
|
82
|
+
* Logic to identify tasks that can be started and run them.
|
|
81
83
|
*/
|
|
82
|
-
|
|
83
|
-
this.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
84
|
+
processLoop(executingPromises, signal) {
|
|
85
|
+
const toRun = this.stateManager.processDependencies();
|
|
86
|
+
// Execute ready tasks
|
|
87
|
+
for (const step of toRun) {
|
|
88
|
+
this.stateManager.markRunning(step);
|
|
89
|
+
const taskPromise = this.strategy.execute(step, this.context, signal)
|
|
90
|
+
.then((result) => {
|
|
91
|
+
this.stateManager.markCompleted(step, result);
|
|
92
|
+
})
|
|
93
|
+
.finally(() => {
|
|
94
|
+
executingPromises.delete(taskPromise);
|
|
93
95
|
});
|
|
94
|
-
|
|
95
|
-
finally {
|
|
96
|
-
this.running.delete(step.name);
|
|
97
|
-
const result = results.get(step.name);
|
|
98
|
-
this.eventBus.emit("taskEnd", { step, result });
|
|
96
|
+
executingPromises.add(taskPromise);
|
|
99
97
|
}
|
|
100
98
|
}
|
|
101
99
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WorkflowExecutor.js","sourceRoot":"","sources":["../src/WorkflowExecutor.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"WorkflowExecutor.js","sourceRoot":"","sources":["../src/WorkflowExecutor.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export { TaskRunner } from "./TaskRunner.js";
|
|
2
|
+
export { TaskRunnerBuilder } from "./TaskRunnerBuilder.js";
|
|
3
|
+
export { TaskStateManager } from "./TaskStateManager.js";
|
|
4
|
+
export { StandardExecutionStrategy } from "./strategies/StandardExecutionStrategy.js";
|
|
5
|
+
export type { IExecutionStrategy } from "./strategies/IExecutionStrategy.js";
|
|
2
6
|
export type { TaskStep } from "./TaskStep.js";
|
|
3
7
|
export type { TaskResult } from "./TaskResult.js";
|
|
4
8
|
export type { TaskStatus } from "./TaskStatus.js";
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
export { TaskRunner } from "./TaskRunner.js";
|
|
2
|
+
export { TaskRunnerBuilder } from "./TaskRunnerBuilder.js";
|
|
3
|
+
export { TaskStateManager } from "./TaskStateManager.js";
|
|
4
|
+
export { StandardExecutionStrategy } from "./strategies/StandardExecutionStrategy.js";
|
|
2
5
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,yBAAyB,EAAE,MAAM,2CAA2C,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { IExecutionStrategy } from "./IExecutionStrategy.js";
|
|
2
|
+
import { TaskStep } from "../TaskStep.js";
|
|
3
|
+
import { TaskResult } from "../TaskResult.js";
|
|
4
|
+
/**
|
|
5
|
+
* Execution strategy that simulates task execution without running the actual logic.
|
|
6
|
+
*/
|
|
7
|
+
export declare class DryRunExecutionStrategy<TContext> implements IExecutionStrategy<TContext> {
|
|
8
|
+
/**
|
|
9
|
+
* Simulates execution by returning a success result immediately.
|
|
10
|
+
* @param step The task step (ignored).
|
|
11
|
+
* @param context The shared context (ignored).
|
|
12
|
+
* @param signal Optional abort signal (ignored).
|
|
13
|
+
* @returns A promise resolving to a success result.
|
|
14
|
+
*/
|
|
15
|
+
execute(_step: TaskStep<TContext>, _context: TContext, _signal?: AbortSignal): Promise<TaskResult>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execution strategy that simulates task execution without running the actual logic.
|
|
3
|
+
*/
|
|
4
|
+
export class DryRunExecutionStrategy {
|
|
5
|
+
/**
|
|
6
|
+
* Simulates execution by returning a success result immediately.
|
|
7
|
+
* @param step The task step (ignored).
|
|
8
|
+
* @param context The shared context (ignored).
|
|
9
|
+
* @param signal Optional abort signal (ignored).
|
|
10
|
+
* @returns A promise resolving to a success result.
|
|
11
|
+
*/
|
|
12
|
+
async execute(
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
14
|
+
_step,
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
16
|
+
_context,
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
18
|
+
_signal) {
|
|
19
|
+
return Promise.resolve({
|
|
20
|
+
status: "success",
|
|
21
|
+
message: "Dry run: simulated success",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=DryRunExecutionStrategy.js.map
|
|
@@ -0,0 +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;IACX,6DAA6D;IAC7D,KAAyB;IACzB,6DAA6D;IAC7D,QAAkB;IAClB,6DAA6D;IAC7D,OAAqB;QAErB,OAAO,OAAO,CAAC,OAAO,CAAC;YACrB,MAAM,EAAE,SAAS;YACjB,OAAO,EAAE,4BAA4B;SACtC,CAAC,CAAC;IACL,CAAC;CACF"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { TaskStep } from "../TaskStep.js";
|
|
2
|
+
import { TaskResult } from "../TaskResult.js";
|
|
3
|
+
/**
|
|
4
|
+
* Interface for task execution strategies.
|
|
5
|
+
* Allows different execution behaviors (e.g., standard, dry-run, debugging).
|
|
6
|
+
*/
|
|
7
|
+
export interface IExecutionStrategy<TContext> {
|
|
8
|
+
/**
|
|
9
|
+
* Executes a single task step.
|
|
10
|
+
* @param step The task step to execute.
|
|
11
|
+
* @param context The shared context.
|
|
12
|
+
* @param signal Optional abort signal.
|
|
13
|
+
* @returns A promise resolving to the task result.
|
|
14
|
+
*/
|
|
15
|
+
execute(step: TaskStep<TContext>, context: TContext, signal?: AbortSignal): Promise<TaskResult>;
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IExecutionStrategy.js","sourceRoot":"","sources":["../../src/strategies/IExecutionStrategy.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { IExecutionStrategy } from "./IExecutionStrategy.js";
|
|
2
|
+
import { TaskStep } from "../TaskStep.js";
|
|
3
|
+
import { TaskResult } from "../TaskResult.js";
|
|
4
|
+
/**
|
|
5
|
+
* Standard execution strategy that runs the task's run method.
|
|
6
|
+
*/
|
|
7
|
+
export declare class StandardExecutionStrategy<TContext> implements IExecutionStrategy<TContext> {
|
|
8
|
+
execute(step: TaskStep<TContext>, context: TContext, signal?: AbortSignal): Promise<TaskResult>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard execution strategy that runs the task's run method.
|
|
3
|
+
*/
|
|
4
|
+
export class StandardExecutionStrategy {
|
|
5
|
+
async execute(step, context, signal) {
|
|
6
|
+
try {
|
|
7
|
+
return await step.run(context, signal);
|
|
8
|
+
}
|
|
9
|
+
catch (e) {
|
|
10
|
+
// Check if error is due to abort
|
|
11
|
+
if (signal?.aborted &&
|
|
12
|
+
(e instanceof Error && e.name === "AbortError" || signal.reason === e)) {
|
|
13
|
+
return {
|
|
14
|
+
status: "cancelled",
|
|
15
|
+
message: "Task cancelled during execution",
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
status: "failure",
|
|
20
|
+
error: e instanceof Error ? e.message : String(e),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=StandardExecutionStrategy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StandardExecutionStrategy.js","sourceRoot":"","sources":["../../src/strategies/StandardExecutionStrategy.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,OAAO,yBAAyB;IAGpC,KAAK,CAAC,OAAO,CACX,IAAwB,EACxB,OAAiB,EACjB,MAAoB;QAEpB,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACzC,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,iCAAiC;YACjC,IACE,MAAM,EAAE,OAAO;gBACf,CAAC,CAAC,YAAY,KAAK,IAAI,CAAC,CAAC,IAAI,KAAK,YAAY,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,EACtE,CAAC;gBACD,OAAO;oBACL,MAAM,EAAE,WAAW;oBACnB,OAAO,EAAE,iCAAiC;iBAC3C,CAAC;YACJ,CAAC;YACD,OAAO;gBACL,MAAM,EAAE,SAAS;gBACjB,KAAK,EAAE,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;aAClD,CAAC;QACJ,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Change: Add Concurrency Control
|
|
2
|
+
|
|
3
|
+
## Why
|
|
4
|
+
The current implementation executes *all* independent tasks in parallel. In large graphs with many independent nodes, this could overwhelm system resources (CPU, memory) or trigger external API rate limits.
|
|
5
|
+
|
|
6
|
+
## What Changes
|
|
7
|
+
- Add `concurrency: number` optional property to `WorkflowExecutor`.
|
|
8
|
+
- Modify `WorkflowExecutor.processLoop` to respect the concurrency limit.
|
|
9
|
+
- Track the active promise count.
|
|
10
|
+
- Only start new tasks from the ready queue if `active < concurrency`.
|
|
11
|
+
- Continue to defer ready tasks until slots open up.
|
|
12
|
+
|
|
13
|
+
## Impact
|
|
14
|
+
- **Affected Components**: `WorkflowExecutor`
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## Implementation
|
|
2
|
+
- [ ] 1.1 Update `TaskRunnerExecutionConfig` to include an optional `concurrency` property.
|
|
3
|
+
- [ ] 1.2 Update `WorkflowExecutor` to accept the `concurrency` limit.
|
|
4
|
+
- [ ] 1.3 Implement a task queueing mechanism in `WorkflowExecutor` to manage pending tasks.
|
|
5
|
+
- [ ] 1.4 Update execution logic to check available concurrency slots before starting a task.
|
|
6
|
+
- [ ] 1.5 Ensure task completion triggers the execution of queued tasks.
|
|
7
|
+
- [ ] 1.6 Verify that unlimited concurrency (default behavior) is preserved when no limit is set.
|
|
8
|
+
- [ ] 1.7 Add unit tests for concurrency limits (e.g., ensure no more than N tasks run at once).
|
|
9
|
+
- [ ] 1.8 Add integration tests with mixed dependencies and concurrency limits.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Change: Add Task Retry Policy
|
|
2
|
+
|
|
3
|
+
## Why
|
|
4
|
+
Tasks currently run once and fail immediately if they throw an error or return a failure status. Network glitches or transient issues can therefore cause an entire workflow to fail unnecessarily.
|
|
5
|
+
|
|
6
|
+
## What Changes
|
|
7
|
+
- Add `TaskRetryConfig` interface to define retry behavior (attempts, delay, backoff).
|
|
8
|
+
- Update `TaskStep` interface to include optional `retry: TaskRetryConfig`.
|
|
9
|
+
- Implement `RetryingExecutionStrategy` which decorates any `IExecutionStrategy`.
|
|
10
|
+
- It catches failures from the inner strategy.
|
|
11
|
+
- It checks the retry policy.
|
|
12
|
+
- It waits and re-executes the step if applicable.
|
|
13
|
+
|
|
14
|
+
## Impact
|
|
15
|
+
- **New Components**: `RetryingExecutionStrategy`, `TaskRetryConfig`
|
|
16
|
+
- **Affected Components**: `TaskStep` (interface update)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
## Implementation
|
|
2
|
+
- [ ] 1.1 Create `TaskRetryConfig` interface with `attempts`, `delay`, and `backoff`.
|
|
3
|
+
- [ ] 1.2 Update `TaskStep` interface to include optional `retry: TaskRetryConfig`.
|
|
4
|
+
- [ ] 1.3 Update execution logic to catch task failures.
|
|
5
|
+
- [ ] 1.4 Implement retry loop/recursion checking `attempts` count.
|
|
6
|
+
- [ ] 1.5 Implement delay logic with support for `fixed` and `exponential` backoff.
|
|
7
|
+
- [ ] 1.6 Ensure `TaskResult` reflects the final status after retries (success if eventually succeeds, failure if all attempts fail).
|
|
8
|
+
- [ ] 1.7 Add unit tests for successful retry after failure.
|
|
9
|
+
- [ ] 1.8 Add unit tests for exhaustion of retry attempts (final failure).
|
|
10
|
+
- [ ] 1.9 Add unit tests for backoff timing (mock timers).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Change: Add Workflow Preview
|
|
2
|
+
|
|
3
|
+
## Why
|
|
4
|
+
It can be difficult to understand the execution flow of complex dependency graphs just by looking at the code. Users also currently cannot easily verify the execution plan without running the side effects, which carries risk.
|
|
5
|
+
|
|
6
|
+
## What Changes
|
|
7
|
+
- Add a `DryRunExecutionStrategy` which implements `IExecutionStrategy`. This allows `WorkflowExecutor` to simulate execution without side effects.
|
|
8
|
+
- Add a standalone utility `generateMermaidGraph(steps: TaskStep[])` to generate a Mermaid.js diagram of the dependency graph.
|
|
9
|
+
- Expose these features via the main `TaskRunner` facade if applicable, or as separate utilities.
|
|
10
|
+
|
|
11
|
+
## Impact
|
|
12
|
+
- **New Components**: `DryRunExecutionStrategy`, `generateMermaidGraph`
|
|
13
|
+
- **Affected Components**: `WorkflowExecutor` (indirectly, via strategy injection)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
## Implementation
|
|
2
|
+
- [ ] 1.1 Update `TaskRunnerExecutionConfig` to include an optional `dryRun: boolean` property.
|
|
3
|
+
- [ ] 1.2 Implement `dryRun` logic in `WorkflowExecutor` (traverse graph, validate order, skip `step.run()`, return `skipped` or `success` pseudo-status).
|
|
4
|
+
- [ ] 1.3 Implement `getMermaidGraph(steps: TaskStep[])` method (can be static or instance method on `TaskRunner`).
|
|
5
|
+
- [ ] 1.4 Ensure `dryRun` respects other configs like `concurrency` (if applicable) to simulate actual timing/order if possible, or just strict topological order.
|
|
6
|
+
- [ ] 1.5 Add unit tests for `dryRun` ensuring no side effects occur.
|
|
7
|
+
- [ ] 1.6 Add unit tests for `getMermaidGraph` output correctness (nodes and edges).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
## Implementation
|
|
2
|
+
- [x] 1.1 Update `TaskRunner.execute` signature to accept an optional config object.
|
|
3
|
+
- [x] 1.2 Implement logic within `TaskRunner` to listen for `AbortSignal` and initiate cancellation.
|
|
4
|
+
- [x] 1.3 Implement global timeout mechanism using `AbortController` and `setTimeout`.
|
|
5
|
+
- [x] 1.4 Propagate `AbortSignal` to individual `TaskStep` executions.
|
|
6
|
+
- [x] 1.5 Ensure `TaskRunner` correctly handles tasks that are already aborted before execution.
|
|
7
|
+
- [x] 1.6 Update `TaskResult` and `RunnerEvents` to reflect cancellation status.
|
|
8
|
+
- [x] 1.7 Add unit tests for `AbortSignal` cancellation scenarios.
|
|
9
|
+
- [x] 1.8 Add unit tests for global timeout scenarios.
|
|
10
|
+
- [x] 1.9 Add integration tests covering both `AbortSignal` and timeout interactions.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Change: Add Comprehensive Integration Tests
|
|
2
|
+
|
|
3
|
+
## Why
|
|
4
|
+
The current test suite relies heavily on unit tests and mocks. To ensure robust behavior in real-world scenarios, we need comprehensive integration tests that execute full task graphs without mocks, validating complex configurations and interactions.
|
|
5
|
+
|
|
6
|
+
## What Changes
|
|
7
|
+
- Create a dedicated `tests/integration-tests/` directory.
|
|
8
|
+
- Implement 10-20 integration test scenarios covering:
|
|
9
|
+
- Linear and branching dependencies.
|
|
10
|
+
- Failure handling and propagation.
|
|
11
|
+
- Context mutation and sharing.
|
|
12
|
+
- Timing and concurrency (real execution).
|
|
13
|
+
- Cancellation and timeouts.
|
|
14
|
+
- Error recovery (if retry policy is implemented, otherwise standard error states).
|
|
15
|
+
|
|
16
|
+
## Impact
|
|
17
|
+
- Affected specs: `task-runner` (no functional changes to the runtime, but validates existing specs)
|
|
18
|
+
- Affected code: `tests/integration-tests/*`
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
## Implementation
|
|
2
|
+
- [x] 1.1 Setup `tests/integration-tests/` directory and test runner config if needed.
|
|
3
|
+
- [x] 1.2 Implement Scenario 1: Basic linear workflow (A -> B -> C) success.
|
|
4
|
+
- [x] 1.3 Implement Scenario 2: Branching workflow (A -> [B, C] -> D) success.
|
|
5
|
+
- [x] 1.4 Implement Scenario 3: Task failure and downstream skipping (A -> B(fail) -> C(skip)).
|
|
6
|
+
- [x] 1.5 Implement Scenario 4: Shared context mutation (A writes, B reads).
|
|
7
|
+
- [x] 1.6 Implement Scenario 5: Large graph execution (e.g., 20+ nodes).
|
|
8
|
+
- [x] 1.7 Implement Scenario 6: Mixed duration tasks (verifying parallel efficiency).
|
|
9
|
+
- [x] 1.8 Implement Scenario 7: Cancellation via AbortSignal in mid-execution.
|
|
10
|
+
- [x] 1.9 Implement Scenario 8: Global timeout interrupting long tasks.
|
|
11
|
+
- [x] 1.10 Implement Scenario 9: Dynamic context validation (tasks validating context state).
|
|
12
|
+
- [x] 1.11 Implement Scenario 10: Circular dependency detection (at runtime validation).
|
|
13
|
+
- [x] 1.12 Implement Scenario 11: Missing dependency handling.
|
|
14
|
+
- [x] 1.13 Implement Scenario 12: Complex "Diamond" dependency graph.
|
|
15
|
+
- [x] 1.14 Implement Scenario 13: Tasks with side-effects (e.g., file I/O or simulated network).
|
|
16
|
+
- [x] 1.15 Implement Scenario 14: Zero-dependency parallel burst (all run at once).
|
|
17
|
+
- [x] 1.16 Verification: Ensure all integration tests pass consistently.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Change: Refactor Core Architecture
|
|
2
|
+
|
|
3
|
+
## Why
|
|
4
|
+
When multiple developers work on the project, conflicts arise due to tight coupling and poor separation of concerns. Large classes like `WorkflowExecutor` are taking on too many responsibilities, and there is duplicated logic around graph traversal and state management.
|
|
5
|
+
|
|
6
|
+
## What Changes
|
|
7
|
+
- Decouple `WorkflowExecutor` from `EventBus` (pass a listener interface or use a mediating controller).
|
|
8
|
+
- Extract `TaskExecutionStrategy` to allow pluggable execution modes (e.g., standard, dry-run, debug).
|
|
9
|
+
- Centralize state management for task results and context, moving it out of the executor loop.
|
|
10
|
+
- Standardize error handling and logging (QoL improvements).
|
|
11
|
+
|
|
12
|
+
## Impact
|
|
13
|
+
- Affected specs: `task-runner` (no behavior change, but structural refactor)
|
|
14
|
+
- Affected code: `src/WorkflowExecutor.ts`, `src/TaskRunner.ts`, new files for extracted logic.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
## Implementation
|
|
2
|
+
- [x] 1.1 Extract `TaskStateManager` to handle `TaskResult` storage, updates, and context mutations.
|
|
3
|
+
- [x] 1.2 Define `IExecutionStrategy` interface for running tasks (Strategy Pattern).
|
|
4
|
+
- [x] 1.3 Refactor `WorkflowExecutor` to use `TaskStateManager` and `IExecutionStrategy`.
|
|
5
|
+
- [x] 1.4 Move explicit event emission out of the core loop into the `TaskStateManager` or a dedicated `ExecutionObserver`.
|
|
6
|
+
- [x] 1.5 Create a factory or builder for `TaskRunner` to simplify configuration for developers.
|
|
7
|
+
- [x] 1.6 Verify that all existing tests pass with the new structure.
|
|
8
|
+
- [x] 1.7 Add "Quality of Life" improvements (e.g., better error messages with specific task context).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Feature: Per-Task Timeout
|
|
2
|
+
|
|
3
|
+
## 🎯 User Story
|
|
4
|
+
"As a developer, I want to define a maximum execution time for specific tasks so that a single hung task (e.g., a stalled network request) fails fast without blocking the rest of the independent tasks or waiting for the global workflow timeout."
|
|
5
|
+
|
|
6
|
+
## ❓ Why
|
|
7
|
+
Currently, the `TaskRunner` allows a **global** timeout for the entire `execute()` call. However, this is insufficient for granular control:
|
|
8
|
+
1. **Varying Latency**: Some tasks are expected to be fast (local validation), others slow (data fetching). A global timeout of 30s is too loose for the fast ones.
|
|
9
|
+
2. **Boilerplate**: Developers currently have to manually implement `setTimeout`, `Promise.race`, and `AbortController` logic inside every `run()` method to handle timeouts properly.
|
|
10
|
+
3. **Resilience**: A single "zombie" task can hold up the entire pipeline until the global timeout kills everything. Per-task timeouts allow failing that specific task (and skipping its dependents) while letting other independent tasks continue.
|
|
11
|
+
|
|
12
|
+
## 🛠️ What Changes
|
|
13
|
+
1. **Interface Update**: Update `TaskStep<T>` to accept an optional `timeout` property (in milliseconds).
|
|
14
|
+
2. **Execution Strategy**: Update `StandardExecutionStrategy` to:
|
|
15
|
+
- Create a local timeout timer for the task.
|
|
16
|
+
- Create a combined `AbortSignal` (merging the workflow's signal and the local timeout).
|
|
17
|
+
- Race the task execution against the timer.
|
|
18
|
+
- Return a specific failure result if the timeout wins.
|
|
19
|
+
|
|
20
|
+
## ✅ Acceptance Criteria
|
|
21
|
+
- [ ] A task with `timeout: 100` must fail if the `run` method takes > 100ms.
|
|
22
|
+
- [ ] The error message for a timed-out task should clearly state "Task timed out after 100ms".
|
|
23
|
+
- [ ] The `AbortSignal` passed to the task's `run` method must be triggered when the timeout occurs.
|
|
24
|
+
- [ ] If the Global Workflow is cancelled *before* the task times out, the task should receive the cancellation signal immediately.
|
|
25
|
+
- [ ] A task completing *before* the timeout should clear the timer to prevent open handles.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Engineering Tasks
|
|
2
|
+
|
|
3
|
+
- [ ] **Task 1: Update Interface**
|
|
4
|
+
- Modify `src/TaskStep.ts` to add `timeout?: number;` to the `TaskStep` interface.
|
|
5
|
+
- Document the property (milliseconds).
|
|
6
|
+
|
|
7
|
+
- [ ] **Task 2: Add Timeout Logic to StandardExecutionStrategy**
|
|
8
|
+
- Modify `src/strategies/StandardExecutionStrategy.ts`.
|
|
9
|
+
- Inside `execute`:
|
|
10
|
+
- Check if `step.timeout` is defined.
|
|
11
|
+
- If yes, create an `AbortController`.
|
|
12
|
+
- Set a `setTimeout` to trigger the controller.
|
|
13
|
+
- Use `Promise.race` (or simply pass the new signal and wait) to handle the timeout.
|
|
14
|
+
- **Crucial**: Ensure the new signal respects the *parent* `signal` (if global cancel happens, local signal must also abort).
|
|
15
|
+
- **Crucial**: Clean up the timer (`clearTimeout`) in a `finally` block.
|
|
16
|
+
|
|
17
|
+
- [ ] **Task 3: Unit Tests**
|
|
18
|
+
- Create `tests/strategies/StandardExecutionStrategy.timeout.test.ts`.
|
|
19
|
+
- Test case: Task finishes before timeout (success).
|
|
20
|
+
- Test case: Task runs longer than timeout (failure/error).
|
|
21
|
+
- Test case: Task receives AbortSignal on timeout.
|
|
22
|
+
- Test case: Global cancellation overrides local timeout.
|
|
23
|
+
|
|
24
|
+
- [ ] **Task 4: Integration Test**
|
|
25
|
+
- Update `tests/TaskRunner.test.ts` or create `tests/timeouts.test.ts`.
|
|
26
|
+
- Define a workflow with a slow task and a short timeout.
|
|
27
|
+
- Verify that the slow task fails, dependents are skipped, and independent tasks still complete (if any).
|