@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.
Files changed (95) hide show
  1. package/.github/dependabot.yml +7 -7
  2. package/.github/workflows/ci.yml +4 -4
  3. package/.jules/backlog_maniac.md +1 -0
  4. package/.jules/nexus.md +1 -0
  5. package/.jules/sentinel.md +1 -0
  6. package/.releaserc.json +2 -7
  7. package/AGENTS.md +21 -16
  8. package/CHANGELOG.md +192 -174
  9. package/README.md +95 -88
  10. package/coverage/coverage-final.json +9 -9
  11. package/coverage/index.html +9 -9
  12. package/coverage/lcov-report/index.html +9 -9
  13. package/coverage/lcov-report/src/EventBus.ts.html +30 -24
  14. package/coverage/lcov-report/src/TaskGraphValidationError.ts.html +12 -3
  15. package/coverage/lcov-report/src/TaskGraphValidator.ts.html +152 -137
  16. package/coverage/lcov-report/src/TaskRunner.ts.html +48 -45
  17. package/coverage/lcov-report/src/TaskRunnerBuilder.ts.html +29 -5
  18. package/coverage/lcov-report/src/TaskRunnerExecutionConfig.ts.html +1 -1
  19. package/coverage/lcov-report/src/TaskStateManager.ts.html +82 -52
  20. package/coverage/lcov-report/src/WorkflowExecutor.ts.html +210 -66
  21. package/coverage/lcov-report/src/contracts/RunnerEvents.ts.html +1 -1
  22. package/coverage/lcov-report/src/contracts/index.html +1 -1
  23. package/coverage/lcov-report/src/index.html +16 -16
  24. package/coverage/lcov-report/src/strategies/DryRunExecutionStrategy.ts.html +4 -4
  25. package/coverage/lcov-report/src/strategies/RetryingExecutionStrategy.ts.html +29 -11
  26. package/coverage/lcov-report/src/strategies/StandardExecutionStrategy.ts.html +7 -7
  27. package/coverage/lcov-report/src/strategies/index.html +1 -1
  28. package/coverage/lcov.info +426 -383
  29. package/coverage/src/EventBus.ts.html +30 -24
  30. package/coverage/src/TaskGraphValidationError.ts.html +12 -3
  31. package/coverage/src/TaskGraphValidator.ts.html +152 -137
  32. package/coverage/src/TaskRunner.ts.html +48 -45
  33. package/coverage/src/TaskRunnerBuilder.ts.html +29 -5
  34. package/coverage/src/TaskRunnerExecutionConfig.ts.html +1 -1
  35. package/coverage/src/TaskStateManager.ts.html +82 -52
  36. package/coverage/src/WorkflowExecutor.ts.html +210 -66
  37. package/coverage/src/contracts/RunnerEvents.ts.html +1 -1
  38. package/coverage/src/contracts/index.html +1 -1
  39. package/coverage/src/index.html +16 -16
  40. package/coverage/src/strategies/DryRunExecutionStrategy.ts.html +4 -4
  41. package/coverage/src/strategies/RetryingExecutionStrategy.ts.html +29 -11
  42. package/coverage/src/strategies/StandardExecutionStrategy.ts.html +7 -7
  43. package/coverage/src/strategies/index.html +1 -1
  44. package/dist/EventBus.js +13 -11
  45. package/dist/EventBus.js.map +1 -1
  46. package/dist/TaskGraphValidationError.js.map +1 -1
  47. package/dist/TaskGraphValidator.js +9 -9
  48. package/dist/TaskGraphValidator.js.map +1 -1
  49. package/dist/TaskRunner.js.map +1 -1
  50. package/dist/TaskRunnerBuilder.js.map +1 -1
  51. package/dist/TaskStateManager.d.ts +6 -0
  52. package/dist/TaskStateManager.js +11 -2
  53. package/dist/TaskStateManager.js.map +1 -1
  54. package/dist/TaskStep.d.ts +5 -0
  55. package/dist/WorkflowExecutor.js +49 -7
  56. package/dist/WorkflowExecutor.js.map +1 -1
  57. package/dist/strategies/RetryingExecutionStrategy.js +3 -1
  58. package/dist/strategies/RetryingExecutionStrategy.js.map +1 -1
  59. package/dist/strategies/StandardExecutionStrategy.js +1 -1
  60. package/dist/strategies/StandardExecutionStrategy.js.map +1 -1
  61. package/openspec/AGENTS.md +81 -15
  62. package/openspec/changes/archive/2026-01-18-add-concurrency-control/proposal.md +7 -4
  63. package/openspec/changes/archive/2026-01-18-add-concurrency-control/tasks.md +1 -0
  64. package/openspec/changes/archive/2026-01-18-add-external-task-cancellation/proposal.md +4 -1
  65. package/openspec/changes/archive/2026-01-18-add-external-task-cancellation/tasks.md +2 -1
  66. package/openspec/changes/archive/2026-01-18-add-integration-tests/proposal.md +3 -0
  67. package/openspec/changes/archive/2026-01-18-add-integration-tests/tasks.md +1 -0
  68. package/openspec/changes/archive/2026-01-18-add-task-retry-policy/proposal.md +3 -0
  69. package/openspec/changes/archive/2026-01-18-add-task-retry-policy/tasks.md +1 -0
  70. package/openspec/changes/archive/2026-01-18-add-workflow-preview/proposal.md +3 -0
  71. package/openspec/changes/archive/2026-01-18-add-workflow-preview/tasks.md +1 -0
  72. package/openspec/changes/archive/2026-01-18-feat-conditional-execution/proposal.md +35 -0
  73. package/openspec/changes/archive/2026-01-18-feat-conditional-execution/tasks.md +32 -0
  74. package/openspec/changes/archive/2026-01-18-refactor-core-architecture/proposal.md +3 -0
  75. package/openspec/changes/archive/2026-01-18-refactor-core-architecture/tasks.md +1 -0
  76. package/openspec/changes/feat-per-task-timeout/proposal.md +11 -6
  77. package/openspec/changes/feat-per-task-timeout/tasks.md +1 -1
  78. package/openspec/project.md +21 -15
  79. package/package.json +2 -1
  80. package/src/EventBus.ts +18 -16
  81. package/src/TaskGraph.ts +8 -8
  82. package/src/TaskGraphValidationError.ts +4 -1
  83. package/src/TaskGraphValidator.ts +148 -143
  84. package/src/TaskRunner.ts +42 -41
  85. package/src/TaskRunnerBuilder.ts +11 -3
  86. package/src/TaskStateManager.ts +12 -2
  87. package/src/TaskStep.ts +6 -0
  88. package/src/WorkflowExecutor.ts +63 -15
  89. package/src/contracts/ITaskGraphValidator.ts +12 -12
  90. package/src/contracts/ValidationError.ts +6 -6
  91. package/src/contracts/ValidationResult.ts +4 -4
  92. package/src/strategies/DryRunExecutionStrategy.ts +3 -3
  93. package/src/strategies/RetryingExecutionStrategy.ts +15 -9
  94. package/src/strategies/StandardExecutionStrategy.ts +4 -4
  95. package/test-report.xml +132 -108
package/README.md CHANGED
@@ -13,30 +13,24 @@ A lightweight, type-safe, and domain-agnostic task orchestration engine. It reso
13
13
  - **Type-Safe Context**: Fully typed shared state using TypeScript Generics.
14
14
  - **Parallel Execution**: Automatically identifies and runs independent steps concurrently.
15
15
  - **Dependency Management**: Enforces execution order based on dependencies.
16
- - **Error Handling & Skipping**: robustly handles failures and automatically skips dependent steps.
17
- - **Event System**: Subscribe to lifecycle events (`workflowStart`, `taskStart`, `taskEnd`, etc.) for logging or monitoring.
18
- - **Runtime Validation**: Automatically detects circular dependencies and missing dependencies before execution loops.
19
-
20
- ## Event System
21
-
22
- The `TaskRunner` implements an Observer Pattern, allowing you to subscribe to various lifecycle events.
23
-
24
- ```typescript
25
- runner.on("taskStart", ({ step }) => {
26
- console.log(`Starting step: ${step.name}`);
27
- });
28
-
29
- runner.on("taskEnd", ({ step, result }) => {
30
- console.log(`Step ${step.name} finished with status: ${result.status}`);
31
- });
32
- ```
16
+ - **Automatic Retries**: Configurable retry logic for flaky tasks.
17
+ - **Dry Run Mode**: Simulate workflow execution to verify dependency graphs.
18
+ - **Visualization**: Generate Mermaid.js diagrams of your task graph.
19
+ - **Event System**: Subscribe to lifecycle events for logging or monitoring.
20
+ - **Runtime Validation**: Automatically detects circular dependencies and missing dependencies.
21
+ - **Conditional Execution**: Skip tasks dynamically based on context state.
33
22
 
34
23
  ## Usage Example
35
24
 
36
- Here is a simple example showing how to define a context, create steps, and execute them.
25
+ The library now provides a fluent `TaskRunnerBuilder` for easy configuration.
37
26
 
38
27
  ```typescript
39
- import { TaskRunner, TaskStep } from './src';
28
+ import {
29
+ TaskRunnerBuilder,
30
+ TaskStep,
31
+ RetryingExecutionStrategy,
32
+ StandardExecutionStrategy
33
+ } from "@calmo/task-runner";
40
34
 
41
35
  // 1. Define your domain-specific context
42
36
  interface ValidationContext {
@@ -49,48 +43,49 @@ interface ValidationContext {
49
43
 
50
44
  // 2. Define your steps
51
45
  const UrlFormatStep: TaskStep<ValidationContext> = {
52
- name: 'UrlFormatStep',
46
+ name: "UrlFormatStep",
53
47
  run: async (ctx) => {
54
- const valid = ctx.issueBody.includes('github.com');
55
- return valid
56
- ? { status: 'success' }
57
- : { status: 'failure', error: 'Invalid URL' };
48
+ if (!ctx.issueBody.includes("github.com")) {
49
+ return { status: "failure", error: "Invalid URL" };
50
+ }
51
+ return { status: "success" };
58
52
  },
59
53
  };
60
54
 
61
55
  const DataLoaderStep: TaskStep<ValidationContext> = {
62
- name: 'DataLoaderStep',
63
- dependencies: ['UrlFormatStep'],
64
- run: async (ctx) => {
65
- // Simulate API call
66
- ctx.prData = { additions: 20, ciStatus: 'success' };
67
- return { status: 'success', message: 'Data fetched' };
56
+ name: "DataLoaderStep",
57
+ dependencies: ["UrlFormatStep"],
58
+ retry: {
59
+ attempts: 3,
60
+ delay: 1000,
61
+ backoff: "exponential"
68
62
  },
69
- };
70
-
71
- const MaxChangesStep: TaskStep<ValidationContext> = {
72
- name: 'MaxChangesStep',
73
- dependencies: ['DataLoaderStep'],
74
63
  run: async (ctx) => {
75
- // Safe access because dependencies ensured execution order
76
- if (!ctx.prData) return { status: 'failure', error: 'Missing PR Data' };
77
-
78
- return ctx.prData.additions < 50
79
- ? { status: 'success' }
80
- : { status: 'failure', error: 'Too many changes' };
64
+ // Simulate API call
65
+ ctx.prData = { additions: 20, ciStatus: "success" };
66
+ return { status: "success", message: "Data fetched" };
81
67
  },
82
68
  };
83
69
 
84
- // 3. Execute the runner
70
+ // 3. Configure and Build the Runner
85
71
  async function main() {
86
72
  const context: ValidationContext = {
87
- issueBody: 'https://github.com/org/repo/pull/1',
73
+ issueBody: "https://github.com/org/repo/pull/1",
88
74
  };
89
75
 
90
- const runner = new TaskRunner(context);
76
+ const runner = new TaskRunnerBuilder(context)
77
+ .useStrategy(new RetryingExecutionStrategy(new StandardExecutionStrategy()))
78
+ .on("taskStart", ({ step }) => console.log(`Starting: ${step.name}`))
79
+ .on("taskEnd", ({ step, result }) => console.log(`Finished: ${step.name} -> ${result.status}`))
80
+ .build();
91
81
 
92
- const steps = [UrlFormatStep, DataLoaderStep, MaxChangesStep];
93
- const results = await runner.execute(steps);
82
+ const steps = [UrlFormatStep, DataLoaderStep];
83
+
84
+ // 4. Execute with options
85
+ const results = await runner.execute(steps, {
86
+ concurrency: 5, // Run up to 5 tasks in parallel
87
+ timeout: 30000 // 30s timeout for the whole workflow
88
+ });
94
89
 
95
90
  console.table(Object.fromEntries(results));
96
91
  }
@@ -98,67 +93,79 @@ async function main() {
98
93
  main();
99
94
  ```
100
95
 
101
- ## Skip Propagation
96
+ ## Advanced Configuration
102
97
 
103
- If a task fails or is skipped, the `TaskRunner` automatically marks all subsequent tasks that depend on it as `skipped`. This ensures that your pipeline doesn't attempt to run steps with missing prerequisites, saving resources and preventing cascading errors.
98
+ ### Execution Strategies
104
99
 
105
- ## Context Hydration
100
+ The `TaskRunner` is built on top of composable execution strategies.
106
101
 
107
- One nice thing to do is to avoid optional parameters and excessive use of ```!``` operator, with task dependencies we can chain our steps and context usages to make sure steps are executed only when pre requisites are met.
102
+ - **StandardExecutionStrategy**: The default strategy that simply runs the task.
103
+ - **RetryingExecutionStrategy**: Wraps another strategy to add retry logic. Configured via the `retry` property on `TaskStep`.
104
+ - **DryRunExecutionStrategy**: Simulates execution without running the actual task logic. Useful for validating your graph or testing conditions.
108
105
 
109
- This decouples **Data Loading** from **Business Logic**.
106
+ You can set a strategy globally using the `TaskRunnerBuilder`:
110
107
 
111
- ### Scenario: User Profile Validation
108
+ ```typescript
109
+ runnerBuilder.useStrategy(new DryRunExecutionStrategy());
110
+ ```
111
+
112
+ ### Execution Options
112
113
 
113
- Imagine you need to validate if a user is a "Pro" member. You shouldn't mix the API fetching logic with the validation logic.
114
+ When calling `execute`, you can provide a configuration object:
114
115
 
115
- 1. **Initial State**: The context starts with only a `userId`.
116
- 2. **Hydration Step**: A `UserLoaderStep` runs first. It fetches data from an API and attaches it to the context.
117
- 3. **Logic Step**: A `PremiumCheckStep` runs next. It doesn't need to know _how_ the data was fetched; it simply checks the `isPro` flag in the context.
116
+ - **concurrency**: Limits the number of tasks running in parallel. Defaults to unlimited.
117
+ - **timeout**: Sets a maximum execution time for the entire workflow.
118
+ - **signal**: Accepts an `AbortSignal` to cancel the workflow programmatically.
119
+ - **dryRun**: Overrides the current strategy with `DryRunExecutionStrategy` for this execution.
118
120
 
119
121
  ```typescript
120
- interface MyProjectContext {
121
- rawInput: string;
122
- }
122
+ await runner.execute(steps, {
123
+ concurrency: 2,
124
+ dryRun: true
125
+ });
126
+ ```
123
127
 
124
- interface MyProjectFullContext extends MyProjectContext {
125
- apiData: {
126
- user: string;
127
- isPro: boolean;
128
- };
129
- }
128
+ ## Visualization
130
129
 
131
- // Step 1: Hydrate the context
132
- class UserLoaderStep implements TaskStep<MyProjectContext> {
133
- name = 'UserLoaderStep';
134
- async run(ctx: MyProjectContext & Partial<MyProjectFullContext>) {
135
- // Fetch data and update context
136
- ctx.apiData = { user: 'john_doe', isPro: true };
137
- return { status: 'success' };
138
- }
139
- }
130
+ You can generate a [Mermaid.js](https://mermaid.js.org/) diagram to visualize your task dependencies.
140
131
 
141
- // Step 2: Use the hydrated data
142
- class PremiumCheckStep implements TaskStep<MyProjectContext> {
143
- name = 'PremiumCheckStep';
144
- dependencies = ['UserLoaderStep']; // Ensures data is ready
132
+ ```typescript
133
+ import { TaskRunner } from "@calmo/task-runner";
145
134
 
146
- async run(ctx: MyProjectFullContext) {
147
- return ctx.apiData.isPro
148
- ? { status: 'success' }
149
- : { status: 'failure', error: 'User is not a Pro member' };
150
- }
151
- }
135
+ const graph = TaskRunner.getMermaidGraph(steps);
136
+ console.log(graph);
137
+ // Output: graph TD; A-->B; ...
152
138
  ```
153
139
 
154
- This approach allows `PremiumCheckStep` to be easily tested with mock data, as it doesn't depend on the actual API loader.
140
+ ## Skip Propagation
141
+
142
+ If a task fails or is skipped, the `TaskRunner` automatically marks all subsequent tasks that depend on it as `skipped`. This ensures that your pipeline doesn't attempt to run steps with missing prerequisites.
143
+
144
+ ## Conditional Execution
145
+
146
+ You can define a `condition` function for a task. If it returns `false`, the task is marked as `skipped`, and its dependencies are also skipped.
147
+
148
+ ```typescript
149
+ const deployStep: TaskStep<MyContext> = {
150
+ name: "deploy",
151
+ condition: (ctx) => ctx.env === "production",
152
+ run: async () => {
153
+ // Deploy logic
154
+ return { status: "success" };
155
+ }
156
+ };
157
+ ```
155
158
 
156
159
  ## Why I did this?
157
160
 
158
- In my company I have a Github Issue validation engine that checks **a lot** of stuff and I wanted to make a package that encapsulates the "validation engine" logic for use outside that niche case. I don't know if someone will find it useful but here it is.
161
+ In my current job I have a Github Issue validation engine that checks **a lot** of stuff and I wanted to make a package that encapsulates the "validation engine" logic for use outside that use case. I also wanted to try to make a package that is not tied to a specific scenario. I don't know if someone will find it useful but here it is.
162
+
163
+ ## AI Usage
164
+
165
+ One of the reasons this project exists is to test 'vibe coding' tools, so yes, this is vibe coded (like, A LOT, I've added myself only specs and some conflict resolutions). This repository serves as a testbed for these tools. It's a way to create a real life scenario showcasing the capabilities of agentic AI development.
159
166
 
160
- ## What is .gemini and .specify
167
+ ## Contributing and General Usage
161
168
 
162
- One of the reasons this project exists is to test 'code vibing' tools, so yes, this is vibe coded. My goal is to not touch the code and see if works.
169
+ Feel free to open issues and PRs. I'm open to feedback and suggestions. I can't promise to act on them but I'll try my best. If you want to play with it, feel free to fork it, change it and use it in your own projects.
163
170
 
164
171
  ---