@calmo/task-runner 4.1.0 → 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/.jules/nexus.md +5 -0
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +2 -0
- package/CHANGELOG.md +27 -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/TaskRunner.d.ts +8 -1
- package/dist/TaskRunner.js +29 -19
- package/dist/TaskRunner.js.map +1 -1
- package/dist/TaskStateManager.js +9 -5
- package/dist/TaskStateManager.js.map +1 -1
- package/dist/WorkflowExecutor.js +19 -10
- 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/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/tasks.md +1 -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/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-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/package.json +1 -1
- package/src/EventBus.ts +7 -8
- package/src/PluginManager.ts +41 -0
- package/src/TaskGraphValidator.ts +22 -24
- package/src/TaskRunner.ts +36 -23
- package/src/TaskStateManager.ts +9 -5
- package/src/WorkflowExecutor.ts +24 -11
- package/src/contracts/Plugin.ts +32 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ITaskGraphValidator } from "./contracts/ITaskGraphValidator.js";
|
|
2
2
|
import { ValidationResult } from "./contracts/ValidationResult.js";
|
|
3
3
|
import { ValidationError } from "./contracts/ValidationError.js";
|
|
4
|
-
import { TaskGraph } from "./TaskGraph.js";
|
|
4
|
+
import { TaskGraph, Task } from "./TaskGraph.js";
|
|
5
5
|
import {
|
|
6
6
|
ERROR_CYCLE,
|
|
7
7
|
ERROR_DUPLICATE_TASK,
|
|
@@ -22,11 +22,11 @@ export class TaskGraphValidator implements ITaskGraphValidator {
|
|
|
22
22
|
validate(taskGraph: TaskGraph): ValidationResult {
|
|
23
23
|
const errors: ValidationError[] = [];
|
|
24
24
|
|
|
25
|
-
// 1. Check
|
|
26
|
-
const
|
|
25
|
+
// 1. Build Map and Check Duplicates (Single Pass)
|
|
26
|
+
const taskMap = this.buildTaskMapAndCheckDuplicates(taskGraph, errors);
|
|
27
27
|
|
|
28
28
|
// 2. Check for missing dependencies
|
|
29
|
-
this.checkMissingDependencies(taskGraph,
|
|
29
|
+
this.checkMissingDependencies(taskGraph, taskMap, errors);
|
|
30
30
|
|
|
31
31
|
// 3. Check for cycles
|
|
32
32
|
// Only run cycle detection if there are no missing dependencies, otherwise we might chase non-existent nodes.
|
|
@@ -35,7 +35,7 @@ export class TaskGraphValidator implements ITaskGraphValidator {
|
|
|
35
35
|
);
|
|
36
36
|
|
|
37
37
|
if (!hasMissingDependencies) {
|
|
38
|
-
this.checkCycles(taskGraph, errors);
|
|
38
|
+
this.checkCycles(taskGraph, taskMap, errors);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
return {
|
|
@@ -54,33 +54,33 @@ export class TaskGraphValidator implements ITaskGraphValidator {
|
|
|
54
54
|
return `Task graph validation failed: ${errorDetails.join("; ")}`;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
private
|
|
57
|
+
private buildTaskMapAndCheckDuplicates(
|
|
58
58
|
taskGraph: TaskGraph,
|
|
59
59
|
errors: ValidationError[]
|
|
60
|
-
):
|
|
61
|
-
const
|
|
60
|
+
): Map<string, Task> {
|
|
61
|
+
const taskMap = new Map<string, Task>();
|
|
62
62
|
for (const task of taskGraph.tasks) {
|
|
63
|
-
if (
|
|
63
|
+
if (taskMap.has(task.id)) {
|
|
64
64
|
errors.push({
|
|
65
65
|
type: ERROR_DUPLICATE_TASK,
|
|
66
66
|
message: `Duplicate task detected with ID: ${task.id}`,
|
|
67
67
|
details: { taskId: task.id },
|
|
68
68
|
});
|
|
69
69
|
} else {
|
|
70
|
-
|
|
70
|
+
taskMap.set(task.id, task);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
-
return
|
|
73
|
+
return taskMap;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
private checkMissingDependencies(
|
|
77
77
|
taskGraph: TaskGraph,
|
|
78
|
-
|
|
78
|
+
taskMap: Map<string, Task>,
|
|
79
79
|
errors: ValidationError[]
|
|
80
80
|
): void {
|
|
81
81
|
for (const task of taskGraph.tasks) {
|
|
82
82
|
for (const dependenceId of task.dependencies) {
|
|
83
|
-
if (!
|
|
83
|
+
if (!taskMap.has(dependenceId)) {
|
|
84
84
|
errors.push({
|
|
85
85
|
type: ERROR_MISSING_DEPENDENCY,
|
|
86
86
|
message: `Task '${task.id}' depends on missing task '${dependenceId}'`,
|
|
@@ -91,13 +91,11 @@ export class TaskGraphValidator implements ITaskGraphValidator {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
private checkCycles(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
94
|
+
private checkCycles(
|
|
95
|
+
taskGraph: TaskGraph,
|
|
96
|
+
taskMap: Map<string, Task>,
|
|
97
|
+
errors: ValidationError[]
|
|
98
|
+
): void {
|
|
101
99
|
const visited = new Set<string>();
|
|
102
100
|
const recursionStack = new Set<string>();
|
|
103
101
|
|
|
@@ -108,7 +106,7 @@ export class TaskGraphValidator implements ITaskGraphValidator {
|
|
|
108
106
|
|
|
109
107
|
const path: string[] = [];
|
|
110
108
|
if (
|
|
111
|
-
this.detectCycle(task.id, path, visited, recursionStack,
|
|
109
|
+
this.detectCycle(task.id, path, visited, recursionStack, taskMap)
|
|
112
110
|
) {
|
|
113
111
|
// Extract the actual cycle from the path
|
|
114
112
|
// The path might look like A -> B -> C -> B (if we started at A and found cycle B-C-B)
|
|
@@ -132,7 +130,7 @@ export class TaskGraphValidator implements ITaskGraphValidator {
|
|
|
132
130
|
path: string[],
|
|
133
131
|
visited: Set<string>,
|
|
134
132
|
recursionStack: Set<string>,
|
|
135
|
-
|
|
133
|
+
taskMap: Map<string, Task>
|
|
136
134
|
): boolean {
|
|
137
135
|
// Use an explicit stack to avoid maximum call stack size exceeded errors
|
|
138
136
|
const stack: { taskId: string; index: number; dependencies: string[] }[] =
|
|
@@ -145,7 +143,7 @@ export class TaskGraphValidator implements ITaskGraphValidator {
|
|
|
145
143
|
stack.push({
|
|
146
144
|
taskId: startTaskId,
|
|
147
145
|
index: 0,
|
|
148
|
-
dependencies:
|
|
146
|
+
dependencies: taskMap.get(startTaskId)!.dependencies,
|
|
149
147
|
});
|
|
150
148
|
|
|
151
149
|
while (stack.length > 0) {
|
|
@@ -170,7 +168,7 @@ export class TaskGraphValidator implements ITaskGraphValidator {
|
|
|
170
168
|
stack.push({
|
|
171
169
|
taskId: dependenceId,
|
|
172
170
|
index: 0,
|
|
173
|
-
dependencies:
|
|
171
|
+
dependencies: taskMap.get(dependenceId)!.dependencies,
|
|
174
172
|
});
|
|
175
173
|
}
|
|
176
174
|
} else {
|
package/src/TaskRunner.ts
CHANGED
|
@@ -14,11 +14,10 @@ import { TaskGraphValidationError } from "./TaskGraphValidationError.js";
|
|
|
14
14
|
import { IExecutionStrategy } from "./strategies/IExecutionStrategy.js";
|
|
15
15
|
import { StandardExecutionStrategy } from "./strategies/StandardExecutionStrategy.js";
|
|
16
16
|
import { RetryingExecutionStrategy } from "./strategies/RetryingExecutionStrategy.js";
|
|
17
|
+
import { Plugin } from "./contracts/Plugin.js";
|
|
18
|
+
import { PluginManager } from "./PluginManager.js";
|
|
17
19
|
import { DryRunExecutionStrategy } from "./strategies/DryRunExecutionStrategy.js";
|
|
18
20
|
|
|
19
|
-
// Re-export types for backward compatibility
|
|
20
|
-
export { RunnerEventPayloads, RunnerEventListener, TaskRunnerExecutionConfig };
|
|
21
|
-
|
|
22
21
|
/**
|
|
23
22
|
* The main class that orchestrates the execution of a list of tasks
|
|
24
23
|
* based on their dependencies, with support for parallel execution.
|
|
@@ -30,10 +29,14 @@ export class TaskRunner<TContext> {
|
|
|
30
29
|
private executionStrategy: IExecutionStrategy<TContext> =
|
|
31
30
|
new RetryingExecutionStrategy(new StandardExecutionStrategy());
|
|
32
31
|
|
|
32
|
+
private pluginManager: PluginManager<TContext>;
|
|
33
|
+
|
|
33
34
|
/**
|
|
34
35
|
* @param context The shared context object to be passed to each task.
|
|
35
36
|
*/
|
|
36
|
-
constructor(private context: TContext) {
|
|
37
|
+
constructor(private context: TContext) {
|
|
38
|
+
this.pluginManager = new PluginManager({ events: this.eventBus });
|
|
39
|
+
}
|
|
37
40
|
|
|
38
41
|
/**
|
|
39
42
|
* Subscribe to an event.
|
|
@@ -56,17 +59,25 @@ export class TaskRunner<TContext> {
|
|
|
56
59
|
event: K,
|
|
57
60
|
callback: RunnerEventListener<TContext, K>
|
|
58
61
|
): void {
|
|
59
|
-
/* v8 ignore next 1 */
|
|
60
62
|
this.eventBus.off(event, callback);
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Registers a plugin.
|
|
67
|
+
* @param plugin The plugin to register.
|
|
68
|
+
* @returns The TaskRunner instance for chaining.
|
|
69
|
+
*/
|
|
70
|
+
public use(plugin: Plugin<TContext>): this {
|
|
71
|
+
this.pluginManager.use(plugin);
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
|
|
63
75
|
/**
|
|
64
76
|
* Sets the execution strategy to be used.
|
|
65
77
|
* @param strategy The execution strategy.
|
|
66
78
|
* @returns The TaskRunner instance for chaining.
|
|
67
79
|
*/
|
|
68
80
|
public setExecutionStrategy(strategy: IExecutionStrategy<TContext>): this {
|
|
69
|
-
/* v8 ignore next 2 */
|
|
70
81
|
this.executionStrategy = strategy;
|
|
71
82
|
return this;
|
|
72
83
|
}
|
|
@@ -77,14 +88,15 @@ export class TaskRunner<TContext> {
|
|
|
77
88
|
* @returns A string containing the Mermaid graph definition.
|
|
78
89
|
*/
|
|
79
90
|
public static getMermaidGraph<T>(steps: TaskStep<T>[]): string {
|
|
80
|
-
const graphLines = ["graph TD"];
|
|
91
|
+
const graphLines = new Set<string>(["graph TD"]);
|
|
81
92
|
const idMap = new Map<string, string>();
|
|
82
93
|
const usedIds = new Set<string>();
|
|
83
94
|
const baseIdCounters = new Map<string, number>();
|
|
84
95
|
|
|
85
96
|
const getUniqueId = (name: string) => {
|
|
86
|
-
|
|
87
|
-
|
|
97
|
+
const existingId = idMap.get(name);
|
|
98
|
+
if (existingId !== undefined) {
|
|
99
|
+
return existingId;
|
|
88
100
|
}
|
|
89
101
|
|
|
90
102
|
const sanitized = this.sanitizeMermaidId(name);
|
|
@@ -112,17 +124,15 @@ export class TaskRunner<TContext> {
|
|
|
112
124
|
return uniqueId;
|
|
113
125
|
};
|
|
114
126
|
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
// However, we must process all step NAMES first.
|
|
119
|
-
for (const step of steps) {
|
|
120
|
-
getUniqueId(step.name);
|
|
121
|
-
}
|
|
122
|
-
|
|
127
|
+
// We process nodes in input order to ensure deterministic ID generation
|
|
128
|
+
// (getUniqueId is called for each unique step name in order, populating the cache).
|
|
129
|
+
const processedNamesForNodes = new Set<string>();
|
|
123
130
|
for (const step of steps) {
|
|
124
|
-
|
|
125
|
-
|
|
131
|
+
if (!processedNamesForNodes.has(step.name)) {
|
|
132
|
+
const stepId = getUniqueId(step.name);
|
|
133
|
+
graphLines.add(` ${stepId}[${JSON.stringify(step.name)}]`);
|
|
134
|
+
processedNamesForNodes.add(step.name);
|
|
135
|
+
}
|
|
126
136
|
}
|
|
127
137
|
|
|
128
138
|
for (const step of steps) {
|
|
@@ -130,12 +140,12 @@ export class TaskRunner<TContext> {
|
|
|
130
140
|
const stepId = getUniqueId(step.name);
|
|
131
141
|
for (const dep of step.dependencies) {
|
|
132
142
|
const depId = getUniqueId(dep);
|
|
133
|
-
graphLines.
|
|
143
|
+
graphLines.add(` ${depId} --> ${stepId}`);
|
|
134
144
|
}
|
|
135
145
|
}
|
|
136
146
|
}
|
|
137
147
|
|
|
138
|
-
return [...
|
|
148
|
+
return [...graphLines].join("\n");
|
|
139
149
|
}
|
|
140
150
|
|
|
141
151
|
/**
|
|
@@ -159,6 +169,9 @@ export class TaskRunner<TContext> {
|
|
|
159
169
|
steps: TaskStep<TContext>[],
|
|
160
170
|
config?: TaskRunnerExecutionConfig
|
|
161
171
|
): Promise<Map<string, TaskResult>> {
|
|
172
|
+
// Initialize plugins
|
|
173
|
+
await this.pluginManager.initialize();
|
|
174
|
+
|
|
162
175
|
// Validate the task graph before execution
|
|
163
176
|
const taskGraph: TaskGraph = {
|
|
164
177
|
tasks: steps.map((step) => ({
|
|
@@ -197,9 +210,9 @@ export class TaskRunner<TContext> {
|
|
|
197
210
|
config.timeout,
|
|
198
211
|
config.signal
|
|
199
212
|
);
|
|
200
|
-
} else {
|
|
201
|
-
return executor.execute(steps, config?.signal);
|
|
202
213
|
}
|
|
214
|
+
|
|
215
|
+
return executor.execute(steps, config?.signal);
|
|
203
216
|
}
|
|
204
217
|
|
|
205
218
|
/**
|
package/src/TaskStateManager.ts
CHANGED
|
@@ -39,10 +39,12 @@ export class TaskStateManager<TContext> {
|
|
|
39
39
|
this.readyQueue.push(step);
|
|
40
40
|
} else {
|
|
41
41
|
for (const dep of deps) {
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
let dependents = this.dependencyGraph.get(dep);
|
|
43
|
+
if (dependents === undefined) {
|
|
44
|
+
dependents = [];
|
|
45
|
+
this.dependencyGraph.set(dep, dependents);
|
|
44
46
|
}
|
|
45
|
-
|
|
47
|
+
dependents.push(step);
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
50
|
}
|
|
@@ -188,11 +190,13 @@ export class TaskStateManager<TContext> {
|
|
|
188
190
|
*/
|
|
189
191
|
private cascadeFailure(failedStepName: string): void {
|
|
190
192
|
const queue = [failedStepName];
|
|
193
|
+
// Optimization: Use index pointer instead of shift() to avoid O(N) array re-indexing
|
|
194
|
+
let head = 0;
|
|
191
195
|
// Use a set to track visited nodes in this cascade pass to avoid redundant processing,
|
|
192
196
|
// although checking results.has() in internalMarkSkipped also prevents it.
|
|
193
197
|
|
|
194
|
-
while (queue.length
|
|
195
|
-
const currentName = queue
|
|
198
|
+
while (head < queue.length) {
|
|
199
|
+
const currentName = queue[head++];
|
|
196
200
|
const dependents = this.dependencyGraph.get(currentName);
|
|
197
201
|
|
|
198
202
|
if (!dependents) continue;
|
package/src/WorkflowExecutor.ts
CHANGED
|
@@ -63,8 +63,22 @@ export class WorkflowExecutor<TContext> {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
try {
|
|
66
|
+
// Create a signal mechanism to wait for any task completion
|
|
67
|
+
let signalResolver!: () => void;
|
|
68
|
+
let signalPromise = new Promise<void>((resolve) => {
|
|
69
|
+
signalResolver = resolve;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const signalWork = () => {
|
|
73
|
+
signalResolver();
|
|
74
|
+
// Reset the promise for the next wait
|
|
75
|
+
signalPromise = new Promise<void>((resolve) => {
|
|
76
|
+
signalResolver = resolve;
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
66
80
|
// Initial pass
|
|
67
|
-
this.processLoop(executingPromises, signal);
|
|
81
|
+
this.processLoop(executingPromises, signalWork, signal);
|
|
68
82
|
|
|
69
83
|
while (
|
|
70
84
|
this.stateManager.hasPendingTasks() ||
|
|
@@ -78,16 +92,13 @@ export class WorkflowExecutor<TContext> {
|
|
|
78
92
|
break;
|
|
79
93
|
} else {
|
|
80
94
|
// Wait for the next task to finish
|
|
81
|
-
await
|
|
95
|
+
await signalPromise;
|
|
82
96
|
}
|
|
83
97
|
|
|
84
98
|
if (signal?.aborted) {
|
|
85
99
|
this.stateManager.cancelAllPending(
|
|
86
100
|
ExecutionConstants.WORKFLOW_CANCELLED
|
|
87
101
|
);
|
|
88
|
-
} else {
|
|
89
|
-
// After a task finishes, check for new work
|
|
90
|
-
this.processLoop(executingPromises, signal);
|
|
91
102
|
}
|
|
92
103
|
}
|
|
93
104
|
|
|
@@ -109,6 +120,7 @@ export class WorkflowExecutor<TContext> {
|
|
|
109
120
|
*/
|
|
110
121
|
private processLoop(
|
|
111
122
|
executingPromises: Set<Promise<void>>,
|
|
123
|
+
signalWork: () => void,
|
|
112
124
|
signal?: AbortSignal
|
|
113
125
|
): void {
|
|
114
126
|
const newlyReady = this.stateManager.processDependencies();
|
|
@@ -129,12 +141,13 @@ export class WorkflowExecutor<TContext> {
|
|
|
129
141
|
|
|
130
142
|
const step = this.readyQueue.pop()!;
|
|
131
143
|
|
|
132
|
-
const taskPromise = this.executeTaskStep(step, signal)
|
|
133
|
-
.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
144
|
+
const taskPromise = this.executeTaskStep(step, signal).finally(() => {
|
|
145
|
+
executingPromises.delete(taskPromise);
|
|
146
|
+
// When a task finishes, we try to run more
|
|
147
|
+
this.processLoop(executingPromises, signalWork, signal);
|
|
148
|
+
// Signal that a task has completed
|
|
149
|
+
signalWork();
|
|
150
|
+
});
|
|
138
151
|
|
|
139
152
|
executingPromises.add(taskPromise);
|
|
140
153
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { EventBus } from "../EventBus.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Context provided to a plugin during installation.
|
|
5
|
+
* exposes capabilities to hook into the runner lifecycle and context.
|
|
6
|
+
*/
|
|
7
|
+
export interface PluginContext<TContext> {
|
|
8
|
+
/**
|
|
9
|
+
* The event bus instance to subscribe to runner events.
|
|
10
|
+
*/
|
|
11
|
+
events: EventBus<TContext>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Interface that all plugins must implement.
|
|
16
|
+
*/
|
|
17
|
+
export interface Plugin<TContext> {
|
|
18
|
+
/**
|
|
19
|
+
* Unique name of the plugin.
|
|
20
|
+
*/
|
|
21
|
+
name: string;
|
|
22
|
+
/**
|
|
23
|
+
* Semantic version of the plugin.
|
|
24
|
+
*/
|
|
25
|
+
version: string;
|
|
26
|
+
/**
|
|
27
|
+
* Called when the plugin is installed into the runner.
|
|
28
|
+
* This is where the plugin should subscribe to events or perform setup.
|
|
29
|
+
* @param context The plugin context exposing runner capabilities.
|
|
30
|
+
*/
|
|
31
|
+
install(context: PluginContext<TContext>): void | Promise<void>;
|
|
32
|
+
}
|