@webpresso/agent-kit 0.21.3 → 0.21.5

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 (99) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +105 -41
  4. package/catalog/AGENTS.md.tpl +3 -1
  5. package/catalog/agent/rules/changeset-release.md +13 -16
  6. package/catalog/agent/skills/plan-refine/SKILL.md +5 -4
  7. package/catalog/base-kit/commitlint.config.ts.tmpl +1 -3
  8. package/catalog/base-kit/e2e/fixtures/smoke.html.tmpl +13 -0
  9. package/catalog/base-kit/e2e/smoke.spec.ts.tmpl +13 -0
  10. package/catalog/base-kit/oxlint.config.ts.tmpl +26 -0
  11. package/catalog/base-kit/playwright.config.ts.tmpl +10 -0
  12. package/catalog/base-kit/src/quality-sample.test.ts.tmpl +19 -0
  13. package/catalog/base-kit/src/quality-sample.ts.tmpl +11 -0
  14. package/catalog/base-kit/stryker.config.ts.tmpl +14 -0
  15. package/catalog/base-kit/tsconfig.json.tmpl +9 -0
  16. package/catalog/base-kit/vitest.config.ts.tmpl +10 -0
  17. package/catalog/docs/templates/adr.md +1 -1
  18. package/catalog/docs/templates/blueprint.md +1 -0
  19. package/catalog/docs/templates/blueprint.yaml +6 -3
  20. package/catalog/docs/templates/guide.md +1 -1
  21. package/catalog/docs/templates/postmortem.md +1 -1
  22. package/catalog/docs/templates/research.md +1 -1
  23. package/catalog/docs/templates/runbook.md +1 -1
  24. package/catalog/docs/templates/system.md +12 -3
  25. package/catalog/docs/templates/tech-debt.md +1 -0
  26. package/commands/blueprint.md +37 -4
  27. package/dist/esm/audit/resolve-audit-script.d.ts +24 -0
  28. package/dist/esm/audit/resolve-audit-script.js +27 -0
  29. package/dist/esm/blueprint/db/enums.d.ts +1 -1
  30. package/dist/esm/blueprint/index.d.ts +0 -1
  31. package/dist/esm/blueprint/index.js +0 -2
  32. package/dist/esm/blueprint/local.d.ts +0 -3
  33. package/dist/esm/blueprint/local.js +0 -2
  34. package/dist/esm/blueprint/service/BlueprintCreationService.js +5 -2
  35. package/dist/esm/blueprint/utils/package-assets.d.ts +11 -0
  36. package/dist/esm/blueprint/utils/package-assets.js +33 -4
  37. package/dist/esm/build/sync-catalog-doc-templates.d.ts +23 -0
  38. package/dist/esm/build/sync-catalog-doc-templates.js +93 -0
  39. package/dist/esm/cli/commands/audit.js +2 -7
  40. package/dist/esm/cli/commands/blueprint/router.js +5 -2
  41. package/dist/esm/cli/commands/blueprint/template-resolver.js +8 -4
  42. package/dist/esm/cli/commands/init/host-visibility.js +4 -2
  43. package/dist/esm/cli/commands/init/index.js +46 -7
  44. package/dist/esm/cli/commands/init/scaffold-base-kit.d.ts +12 -0
  45. package/dist/esm/cli/commands/init/scaffold-base-kit.js +141 -6
  46. package/dist/esm/cli/commands/typecheck.js +10 -4
  47. package/dist/esm/e2e/command-builder.js +26 -7
  48. package/dist/esm/e2e/execution.js +4 -0
  49. package/dist/esm/e2e/run-planner.js +1 -0
  50. package/dist/esm/e2e/types.d.ts +1 -0
  51. package/dist/esm/format/index.js +7 -1
  52. package/dist/esm/lint/index.js +3 -1
  53. package/dist/esm/mcp/blueprint-server.js +361 -66
  54. package/dist/esm/mcp/tools/audit.js +2 -8
  55. package/dist/esm/mcp/tools/e2e.d.ts +1 -1
  56. package/dist/esm/package.json +3 -0
  57. package/dist/esm/secret-gate/runner.js +4 -0
  58. package/dist/esm/test/command-builder.d.ts +1 -0
  59. package/dist/esm/test/command-builder.js +8 -2
  60. package/dist/esm/test-helpers/hermetic-env.d.ts +25 -0
  61. package/dist/esm/test-helpers/hermetic-env.js +31 -0
  62. package/dist/esm/tool-runtime/index.d.ts +5 -0
  63. package/dist/esm/tool-runtime/index.js +23 -0
  64. package/dist/esm/tool-runtime/resolve-runner.d.ts +13 -0
  65. package/dist/esm/tool-runtime/resolve-runner.js +40 -0
  66. package/package.json +12 -18
  67. package/skills/plan-refine/SKILL.md +5 -4
  68. package/dist/esm/blueprint/dag/cycle-detector.d.ts +0 -12
  69. package/dist/esm/blueprint/dag/cycle-detector.js +0 -46
  70. package/dist/esm/blueprint/dag/executor.d.ts +0 -140
  71. package/dist/esm/blueprint/dag/executor.js +0 -292
  72. package/dist/esm/blueprint/dag/index.d.ts +0 -20
  73. package/dist/esm/blueprint/dag/index.js +0 -17
  74. package/dist/esm/blueprint/dag/interfaces.d.ts +0 -56
  75. package/dist/esm/blueprint/dag/interfaces.js +0 -13
  76. package/dist/esm/blueprint/dag/local/independence.d.ts +0 -107
  77. package/dist/esm/blueprint/dag/local/independence.js +0 -231
  78. package/dist/esm/blueprint/dag/local/index.d.ts +0 -14
  79. package/dist/esm/blueprint/dag/local/index.js +0 -14
  80. package/dist/esm/blueprint/dag/local/package-graph.d.ts +0 -66
  81. package/dist/esm/blueprint/dag/local/package-graph.js +0 -148
  82. package/dist/esm/blueprint/dag/plan-parser.d.ts +0 -54
  83. package/dist/esm/blueprint/dag/plan-parser.js +0 -236
  84. package/dist/esm/blueprint/dag/task-graph-algorithms.d.ts +0 -13
  85. package/dist/esm/blueprint/dag/task-graph-algorithms.js +0 -236
  86. package/dist/esm/blueprint/dag/task-graph.d.ts +0 -171
  87. package/dist/esm/blueprint/dag/task-graph.js +0 -370
  88. package/dist/esm/blueprint/dag/types.d.ts +0 -17
  89. package/dist/esm/blueprint/dag/types.js +0 -2
  90. package/dist/esm/blueprint/graph/index.d.ts +0 -5
  91. package/dist/esm/blueprint/graph/index.js +0 -5
  92. package/dist/esm/blueprint/graph/mermaid-parser.d.ts +0 -3
  93. package/dist/esm/blueprint/graph/mermaid-parser.js +0 -93
  94. package/dist/esm/blueprint/graph/mermaid-serializer.d.ts +0 -3
  95. package/dist/esm/blueprint/graph/mermaid-serializer.js +0 -20
  96. package/dist/esm/blueprint/graph/schema.d.ts +0 -89
  97. package/dist/esm/blueprint/graph/schema.js +0 -104
  98. package/dist/esm/blueprint/graph/task-graph-adapter.d.ts +0 -6
  99. package/dist/esm/blueprint/graph/task-graph-adapter.js +0 -30
@@ -0,0 +1,23 @@
1
+ import { resolveRunner, } from './resolve-runner.js';
2
+ const runtimeCache = new Map();
3
+ function cacheKey(tool, options) {
4
+ return JSON.stringify({
5
+ tool,
6
+ filterOutput: options.filterOutput ?? true,
7
+ fallbackCommand: options.fallbackCommand ?? null,
8
+ fallbackArgs: options.fallbackArgs ?? [],
9
+ });
10
+ }
11
+ export function getManagedRunner(tool, options = {}) {
12
+ const key = cacheKey(tool, options);
13
+ const cached = runtimeCache.get(key);
14
+ if (cached)
15
+ return cached;
16
+ const resolved = resolveRunner(tool, options);
17
+ runtimeCache.set(key, resolved);
18
+ return resolved;
19
+ }
20
+ export function clearManagedRunnerCache() {
21
+ runtimeCache.clear();
22
+ }
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,13 @@
1
+ export interface ManagedRunnerResolution {
2
+ readonly tool: string;
3
+ readonly command: string;
4
+ readonly args: readonly string[];
5
+ readonly source: 'managed' | 'fallback';
6
+ }
7
+ export interface ResolveRunnerOptions {
8
+ readonly fallbackCommand?: string;
9
+ readonly fallbackArgs?: readonly string[];
10
+ readonly filterOutput?: boolean;
11
+ }
12
+ export declare function resolveRunner(tool: string, options?: ResolveRunnerOptions): ManagedRunnerResolution;
13
+ //# sourceMappingURL=resolve-runner.d.ts.map
@@ -0,0 +1,40 @@
1
+ const MANAGED_TOOL_PREFIX = {
2
+ playwright: { command: 'vp', args: ['exec', 'playwright'] },
3
+ vitest: { command: 'vp', args: ['exec', 'vitest'] },
4
+ vp: { command: 'vp', args: [] },
5
+ };
6
+ function withOptionalRtk(resolution, filterOutput) {
7
+ if (!filterOutput)
8
+ return resolution;
9
+ return {
10
+ ...resolution,
11
+ command: 'rtk',
12
+ args: [resolution.command, ...resolution.args],
13
+ };
14
+ }
15
+ export function resolveRunner(tool, options = {}) {
16
+ const normalized = tool.trim();
17
+ if (!normalized) {
18
+ throw new Error('tool runtime resolution requires a non-empty tool name');
19
+ }
20
+ const filterOutput = options.filterOutput ?? true;
21
+ const managed = MANAGED_TOOL_PREFIX[normalized];
22
+ if (managed) {
23
+ return withOptionalRtk({
24
+ tool: normalized,
25
+ command: managed.command,
26
+ args: [...managed.args],
27
+ source: 'managed',
28
+ }, filterOutput);
29
+ }
30
+ if (options.fallbackCommand) {
31
+ return withOptionalRtk({
32
+ tool: normalized,
33
+ command: options.fallbackCommand,
34
+ args: [...(options.fallbackArgs ?? [])],
35
+ source: 'fallback',
36
+ }, filterOutput);
37
+ }
38
+ throw new Error(`No managed runtime runner is defined for tool "${normalized}"`);
39
+ }
40
+ //# sourceMappingURL=resolve-runner.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpresso/agent-kit",
3
- "version": "0.21.3",
3
+ "version": "0.21.5",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,7 +14,7 @@
14
14
  "registry": "https://registry.npmjs.org/",
15
15
  "access": "public"
16
16
  },
17
- "description": "One command scaffolds a repo so every AI coding agent (Claude Code, Codex CLI, Cursor, Windsurf, Gemini, OpenCode) shares the same context, hooks, and quality gates. The `wp` CLI: blueprints, skills catalog, lore commits, tech-debt lifecycle, audit composite.",
17
+ "description": "TypeScript infrastructure for AI-agent-driven development: the `wp` CLI + MCP server give agents planning (blueprints), tests, mutation, e2e, CI, docs, and tech-debt summary-first so they keep context, and enforced as pre-commit/CI contracts. Manages AGENTS.md / CLAUDE.md and per-agent surfaces.",
18
18
  "keywords": [
19
19
  "agent",
20
20
  "webpresso",
@@ -113,6 +113,9 @@
113
113
  "#mcp/*": "./src/mcp/*.ts",
114
114
  "#content/*.js": "./src/content/*.ts",
115
115
  "#content/*": "./src/content/*.ts",
116
+ "#tool-runtime": "./src/tool-runtime/index.ts",
117
+ "#tool-runtime/*.js": "./src/tool-runtime/*.ts",
118
+ "#tool-runtime/*": "./src/tool-runtime/*.ts",
116
119
  "#output-transforms/*.js": "./src/output-transforms/*.ts",
117
120
  "#output-transforms/*": "./src/output-transforms/*.ts",
118
121
  "#lint/*.js": "./src/lint/*.ts",
@@ -167,18 +170,6 @@
167
170
  "default": "./dist/esm/blueprint/index.js"
168
171
  }
169
172
  },
170
- "./blueprint/dag": {
171
- "import": {
172
- "types": "./dist/esm/blueprint/dag/index.d.ts",
173
- "default": "./dist/esm/blueprint/dag/index.js"
174
- }
175
- },
176
- "./blueprint/dag/local": {
177
- "import": {
178
- "types": "./dist/esm/blueprint/dag/local/index.d.ts",
179
- "default": "./dist/esm/blueprint/dag/local/index.js"
180
- }
181
- },
182
173
  "./blueprint/local": {
183
174
  "import": {
184
175
  "types": "./dist/esm/blueprint/local.d.ts",
@@ -514,17 +505,21 @@
514
505
  "scripts": {
515
506
  "setup:agent": "wp setup",
516
507
  "build": "tshy && vp run chmod-bins && vp run link-self-bins",
517
- "postbuild": "vp run generate-skills",
508
+ "postbuild": "vp run generate-skills && vp run sync-doc-templates",
518
509
  "chmod-bins": "bun scripts/chmod-bins.ts",
519
510
  "link-self-bins": "bun scripts/link-self-bins.ts",
520
511
  "dev:link": "bun scripts/link-edge-local.ts",
521
512
  "prepack": "bun src/build/package-manifest.ts prepare",
522
513
  "postpack": "bun src/build/package-manifest.ts restore",
523
514
  "generate-skills": "bun src/build/generate-skills-dir.ts",
515
+ "sync-doc-templates": "bun src/build/sync-catalog-doc-templates.ts",
524
516
  "sync-marketplace-version": "bun scripts/sync-marketplace-version.ts",
525
517
  "version": "changeset version && vp run sync-marketplace-version",
518
+ "release:publish": "bun scripts/release-publish.ts",
526
519
  "typecheck": "tsc --noEmit",
527
- "test": "vitest run",
520
+ "test": "vitest run --exclude '**/*.integration.test.ts' && vitest run --no-file-parallelism .integration.test.ts",
521
+ "test:unit": "vitest run --exclude '**/*.integration.test.ts'",
522
+ "test:integration": "vitest run --no-file-parallelism .integration.test.ts",
528
523
  "test:mutation": "bun stryker run stryker.config.ts",
529
524
  "eval": "bun src/runners/evals/index.ts",
530
525
  "lint": "vp lint",
@@ -539,6 +534,7 @@
539
534
  "imports:check": "WP_SKIP_UPDATE_CHECK=1 wp audit no-relative-parent-imports",
540
535
  "verify:secrets": "bun scripts/check-no-dev-vars.ts",
541
536
  "audit:secret-provider-quarantine": "bun scripts/audit-secret-provider-quarantine.ts",
537
+ "public:consumer-smoke": "bun scripts/public-consumer-smoke.ts",
542
538
  "public:readiness": "bun scripts/public-readiness.ts",
543
539
  "audits:check": "WP_SKIP_UPDATE_CHECK=1 wp audit guardrails && vp run hooks:doctor:ci",
544
540
  "hooks:doctor": "WP_SKIP_UPDATE_CHECK=1 wp hooks doctor",
@@ -607,8 +603,6 @@
607
603
  "exports": {
608
604
  ".": "./src/index.ts",
609
605
  "./blueprint": "./src/blueprint/index.ts",
610
- "./blueprint/dag": "./src/blueprint/dag/index.ts",
611
- "./blueprint/dag/local": "./src/blueprint/dag/local/index.ts",
612
606
  "./blueprint/local": "./src/blueprint/local.ts",
613
607
  "./blueprint/tech-debt": "./src/blueprint/tech-debt/index.ts",
614
608
  "./blueprint/test-utils": "./src/blueprint/test-utils/blueprint-mocks.ts",
@@ -16,10 +16,11 @@ description: Methodology for refining, fact-checking, and hardening implementati
16
16
  >
17
17
  > Blueprint hardening and lifecycle infrastructure are production-ready:
18
18
  >
19
- > - Kahn's Algorithm DAG analysis (fully tested)
20
- > - `ParallelExecutor` concurrency engine with abort, timeout, per-type limits
21
- > - Blueprint dependency format and validation
22
- > - Failed tasks block dependents automatically
19
+ > - Blueprint dependency parsing, format validation, and the lifecycle state
20
+ > machine with evidence-gated task completion (fully tested, in-package)
21
+ > - Parallel execution is delegated to the `/pll` runtime adapter (OMX), which
22
+ > batches independent tasks by dependency order and blocks dependents of
23
+ > failed tasks
23
24
  >
24
25
  > **Remaining gaps:** execution docs and wrappers must stay aligned with the currently shipped runtime and CLI surface.
25
26
 
@@ -1,12 +0,0 @@
1
- export declare class CycleDetector {
2
- private edges;
3
- private visited;
4
- private recStack;
5
- private path;
6
- private cycles;
7
- constructor(edges: Map<string, Set<string>>);
8
- detect(nodes: IterableIterator<string>): string[][] | null;
9
- private dfs;
10
- private processNeighbor;
11
- }
12
- //# sourceMappingURL=cycle-detector.d.ts.map
@@ -1,46 +0,0 @@
1
- export class CycleDetector {
2
- edges;
3
- visited = new Set();
4
- recStack = new Set();
5
- path = [];
6
- cycles = [];
7
- constructor(edges) {
8
- this.edges = edges;
9
- }
10
- detect(nodes) {
11
- for (const nodeId of nodes) {
12
- if (!this.visited.has(nodeId) && this.dfs(nodeId)) {
13
- return this.cycles;
14
- }
15
- }
16
- return this.cycles.length > 0 ? this.cycles : null;
17
- }
18
- dfs(nodeId) {
19
- this.visited.add(nodeId);
20
- this.recStack.add(nodeId);
21
- this.path.push(nodeId);
22
- const edges = this.edges.get(nodeId);
23
- if (edges) {
24
- for (const neighbor of edges) {
25
- if (this.processNeighbor(neighbor))
26
- return true;
27
- }
28
- }
29
- this.path.pop();
30
- this.recStack.delete(nodeId);
31
- return false;
32
- }
33
- processNeighbor(neighbor) {
34
- if (!this.visited.has(neighbor)) {
35
- if (this.dfs(neighbor))
36
- return true;
37
- }
38
- else if (this.recStack.has(neighbor)) {
39
- const cycleStart = this.path.indexOf(neighbor);
40
- this.cycles.push([...this.path.slice(cycleStart), neighbor]);
41
- return true;
42
- }
43
- return false;
44
- }
45
- }
46
- //# sourceMappingURL=cycle-detector.js.map
@@ -1,140 +0,0 @@
1
- import type { IClock } from './interfaces.js';
2
- import type { Task } from './types.js';
3
- import { TaskGraph } from './task-graph.js';
4
- /**
5
- * Task execution result
6
- */
7
- export interface TaskResult<T = unknown> {
8
- taskId: string;
9
- status: 'completed' | 'failed' | 'skipped';
10
- output?: T;
11
- error?: Error;
12
- durationMs: number;
13
- startedAt: number;
14
- completedAt: number;
15
- }
16
- /**
17
- * Concurrency configuration by task type
18
- */
19
- export interface ConcurrencyConfig {
20
- /** Global max concurrent tasks across all types */
21
- global?: number;
22
- /** Default max concurrent tasks per type (when no type-specific limit) */
23
- default: number;
24
- /** Per-type limits (overrides default) */
25
- byType?: Record<string, number>;
26
- }
27
- /**
28
- * Task executor function signature
29
- * Receives task and returns result (or throws)
30
- */
31
- export type TaskExecutorFn<T, R> = (task: Task<T>) => Promise<R>;
32
- /**
33
- * Execution progress callback
34
- */
35
- export interface ExecutionProgress<T = unknown> {
36
- totalTasks: number;
37
- completedTasks: number;
38
- failedTasks: number;
39
- runningTasks: string[];
40
- pendingTasks: number;
41
- currentWave: number;
42
- totalWaves: number;
43
- latestResult?: TaskResult<T>;
44
- }
45
- export type ProgressCallback<T = unknown> = (progress: ExecutionProgress<T>) => void;
46
- /**
47
- * Executor options
48
- */
49
- export interface ExecutorOptions<R> {
50
- concurrency?: ConcurrencyConfig;
51
- onProgress?: ProgressCallback<R>;
52
- /** Clock for time operations (injectable for testing) */
53
- clock?: IClock;
54
- /** Skip tasks whose dependencies failed */
55
- skipOnFailedDependency?: boolean;
56
- /** Timeout for individual tasks in milliseconds (0 = no timeout) */
57
- taskTimeoutMs?: number;
58
- /** AbortSignal for cancellation */
59
- signal?: AbortSignal;
60
- }
61
- /**
62
- * Parallel DAG executor with concurrency controls.
63
- *
64
- * Uses Kahn's algorithm for topological execution order.
65
- * Supports per-task-type concurrency limits.
66
- *
67
- * Runtime-agnostic - works in Node.js, Bun, Deno, Cloudflare Workers.
68
- */
69
- export declare class ParallelExecutor<T = unknown, R = unknown> {
70
- private graph;
71
- private concurrency;
72
- private executorFn;
73
- private onProgress?;
74
- private clock;
75
- private skipOnFailedDependency;
76
- private taskTimeoutMs;
77
- private completed;
78
- private failed;
79
- private skipped;
80
- private running;
81
- private results;
82
- private signal?;
83
- private abortController?;
84
- constructor(graph: TaskGraph<T>, executorFn: TaskExecutorFn<T, R>, options?: ExecutorOptions<R>);
85
- /**
86
- * Execute all tasks in parallel, respecting dependencies and concurrency limits.
87
- * Returns results in completion order.
88
- * @throws {Error} If graph is invalid or execution is aborted
89
- */
90
- execute(): Promise<TaskResult<R>[]>;
91
- /**
92
- * Check if execution has been aborted and throw if so.
93
- * Checks both internal controller and external signal.
94
- */
95
- private throwIfAborted;
96
- /**
97
- * Get current execution state.
98
- */
99
- getState(): {
100
- completed: string[];
101
- failed: string[];
102
- skipped: string[];
103
- running: string[];
104
- };
105
- private executeWave;
106
- private startPendingTasks;
107
- private shouldSkipTask;
108
- private skipTask;
109
- private canStartTask;
110
- private getTaskType;
111
- /**
112
- * Count running tasks of a specific type.
113
- * FIX: Now properly tracks task types.
114
- */
115
- private countRunningByType;
116
- private startTask;
117
- /**
118
- * Execute task with optional timeout support.
119
- * State-of-the-art: proper timeout handling with AbortController pattern.
120
- */
121
- private executeWithTimeout;
122
- private createTimeoutPromise;
123
- private emitTaskStart;
124
- private waitForAny;
125
- private emitProgress;
126
- }
127
- /**
128
- * Create executor from task array with dependencies.
129
- * Convenience function for common use case.
130
- */
131
- export declare function createExecutor<T, R>(tasks: Array<{
132
- task: Task<T>;
133
- dependsOn?: string[];
134
- }>, executorFn: TaskExecutorFn<T, R>, options?: ExecutorOptions<R>): ParallelExecutor<T, R>;
135
- /**
136
- * Create executor directly from tasks using their dependencies arrays.
137
- * Most convenient for LLM agents.
138
- */
139
- export declare function createExecutorFromTasks<T, R>(tasks: Task<T>[], executorFn: TaskExecutorFn<T, R>, options?: ExecutorOptions<R>): ParallelExecutor<T, R>;
140
- //# sourceMappingURL=executor.d.ts.map
@@ -1,292 +0,0 @@
1
- import { realClock } from './interfaces.js';
2
- import { TaskGraph } from './task-graph.js';
3
- /**
4
- * Parallel DAG executor with concurrency controls.
5
- *
6
- * Uses Kahn's algorithm for topological execution order.
7
- * Supports per-task-type concurrency limits.
8
- *
9
- * Runtime-agnostic - works in Node.js, Bun, Deno, Cloudflare Workers.
10
- */
11
- export class ParallelExecutor {
12
- graph;
13
- concurrency;
14
- executorFn;
15
- onProgress;
16
- clock;
17
- skipOnFailedDependency;
18
- taskTimeoutMs;
19
- // Execution state
20
- completed = new Set();
21
- failed = new Set();
22
- skipped = new Set();
23
- running = new Map();
24
- results = [];
25
- signal;
26
- abortController;
27
- constructor(graph, executorFn, options = {}) {
28
- this.graph = graph;
29
- this.executorFn = executorFn;
30
- this.concurrency = options.concurrency ?? { default: 6 };
31
- this.onProgress = options.onProgress;
32
- this.clock = options.clock ?? realClock;
33
- this.skipOnFailedDependency = options.skipOnFailedDependency ?? true;
34
- this.taskTimeoutMs = options.taskTimeoutMs ?? 0;
35
- this.signal = options.signal;
36
- // Create internal abort controller for cleanup
37
- this.abortController = new AbortController();
38
- // Link external signal if provided
39
- if (options.signal) {
40
- options.signal.addEventListener('abort', () => {
41
- this.abortController?.abort(options.signal?.reason);
42
- });
43
- }
44
- }
45
- /**
46
- * Execute all tasks in parallel, respecting dependencies and concurrency limits.
47
- * Returns results in completion order.
48
- * @throws {Error} If graph is invalid or execution is aborted
49
- */
50
- async execute() {
51
- // Check for abort before starting
52
- this.throwIfAborted();
53
- // Validate graph first
54
- const validation = this.graph.validate();
55
- if (!validation.valid) {
56
- throw new Error(`Cannot execute invalid graph: ${validation.errors.join('; ')}`);
57
- }
58
- const waves = this.graph.getWaves();
59
- const totalTasks = waves.flat().length;
60
- for (let waveIdx = 0; waveIdx < waves.length; waveIdx++) {
61
- this.throwIfAborted();
62
- const wave = waves[waveIdx];
63
- if (!wave)
64
- continue;
65
- await this.executeWave(wave, waveIdx + 1, waves.length, totalTasks);
66
- }
67
- return this.results;
68
- }
69
- /**
70
- * Check if execution has been aborted and throw if so.
71
- * Checks both internal controller and external signal.
72
- */
73
- throwIfAborted() {
74
- // Check external signal first (passed in via options)
75
- if (this.signal?.aborted) {
76
- throw new Error(`Execution aborted: ${this.signal.reason || 'user request'}`);
77
- }
78
- // Check internal controller
79
- if (this.abortController?.signal.aborted) {
80
- throw new Error(`Execution aborted: ${this.abortController.signal.reason || 'user request'}`);
81
- }
82
- }
83
- /**
84
- * Get current execution state.
85
- */
86
- getState() {
87
- return {
88
- completed: Array.from(this.completed),
89
- failed: Array.from(this.failed),
90
- skipped: Array.from(this.skipped),
91
- running: Array.from(this.running.keys()),
92
- };
93
- }
94
- async executeWave(tasks, waveNum, totalWaves, totalTasks) {
95
- const pending = [...tasks];
96
- while (pending.length > 0 || this.running.size > 0) {
97
- this.startPendingTasks(pending, waveNum, totalWaves, totalTasks);
98
- if (this.running.size === 0)
99
- break;
100
- const result = await this.waitForAny();
101
- this.results.push(result);
102
- this.emitProgress(waveNum, totalWaves, totalTasks, result);
103
- }
104
- }
105
- startPendingTasks(pending, waveNum, totalWaves, totalTasks) {
106
- while (pending[0] && this.canStartTask(pending[0])) {
107
- const task = pending.shift();
108
- if (!task)
109
- break;
110
- if (this.shouldSkipTask(task)) {
111
- this.skipTask(task, waveNum, totalWaves, totalTasks);
112
- }
113
- else {
114
- this.startTask(task);
115
- }
116
- }
117
- }
118
- shouldSkipTask(task) {
119
- if (!this.skipOnFailedDependency)
120
- return false;
121
- // Check if any dependency failed
122
- const deps = this.graph.getDependencies(task.id);
123
- for (const dep of deps) {
124
- if (this.failed.has(dep) || this.skipped.has(dep)) {
125
- return true;
126
- }
127
- }
128
- return false;
129
- }
130
- skipTask(task, waveNum, totalWaves, totalTasks) {
131
- const now = this.clock.now();
132
- this.skipped.add(task.id);
133
- const result = {
134
- taskId: task.id,
135
- status: 'skipped',
136
- durationMs: 0,
137
- startedAt: now,
138
- completedAt: now,
139
- error: new Error('Skipped due to failed dependency'),
140
- };
141
- this.results.push(result);
142
- this.emitProgress(waveNum, totalWaves, totalTasks, result);
143
- }
144
- canStartTask(task) {
145
- const taskType = this.getTaskType(task);
146
- const typeLimit = this.concurrency.byType?.[taskType] ?? this.concurrency.default;
147
- const currentOfType = this.countRunningByType(taskType);
148
- // Check type-specific limit
149
- if (currentOfType >= typeLimit)
150
- return false;
151
- // Check global limit (defaults to default if not specified)
152
- const globalLimit = this.concurrency.global ?? this.concurrency.default;
153
- if (this.running.size >= globalLimit)
154
- return false;
155
- return true;
156
- }
157
- getTaskType(task) {
158
- // Extract type from task metadata if available
159
- const meta = task.data;
160
- const typeValue = meta?.type;
161
- // Validate type is a string
162
- if (typeof typeValue === 'string' && typeValue.length > 0) {
163
- return typeValue;
164
- }
165
- return 'default';
166
- }
167
- /**
168
- * Count running tasks of a specific type.
169
- * FIX: Now properly tracks task types.
170
- */
171
- countRunningByType(type) {
172
- let count = 0;
173
- for (const info of this.running.values()) {
174
- if (info.taskType === type) {
175
- count++;
176
- }
177
- }
178
- return count;
179
- }
180
- startTask(task) {
181
- const startTime = this.clock.now();
182
- const taskType = this.getTaskType(task);
183
- // Emit task start via progress callback (state-of-the-art observability)
184
- this.emitTaskStart(task.id, taskType);
185
- const promise = this.executeWithTimeout(task, startTime)
186
- .then((output) => ({
187
- taskId: task.id,
188
- status: 'completed',
189
- output,
190
- durationMs: this.clock.now() - startTime,
191
- startedAt: startTime,
192
- completedAt: this.clock.now(),
193
- }))
194
- .catch((error) => ({
195
- taskId: task.id,
196
- status: 'failed',
197
- error: error instanceof Error ? error : new Error(String(error)),
198
- durationMs: this.clock.now() - startTime,
199
- startedAt: startTime,
200
- completedAt: this.clock.now(),
201
- }))
202
- .finally(() => {
203
- this.running.delete(task.id);
204
- });
205
- // Track completion status (fire-and-forget)
206
- void promise.then((r) => {
207
- if (r.status === 'completed') {
208
- this.completed.add(task.id);
209
- }
210
- else {
211
- this.failed.add(task.id);
212
- }
213
- return;
214
- });
215
- this.running.set(task.id, {
216
- taskId: task.id,
217
- taskType,
218
- promise,
219
- });
220
- }
221
- /**
222
- * Execute task with optional timeout support.
223
- * State-of-the-art: proper timeout handling with AbortController pattern.
224
- */
225
- executeWithTimeout(task, _startTime) {
226
- if (this.taskTimeoutMs <= 0) {
227
- return this.executorFn(task);
228
- }
229
- return Promise.race([this.executorFn(task), this.createTimeoutPromise(task.id)]);
230
- }
231
- createTimeoutPromise(taskId) {
232
- return new Promise((resolve) => {
233
- setTimeout(() => resolve(), this.taskTimeoutMs);
234
- }).then(() => {
235
- throw new Error(`Task "${taskId}" timed out after ${this.taskTimeoutMs}ms`);
236
- });
237
- }
238
- emitTaskStart(_taskId, _taskType) {
239
- if (!this.onProgress)
240
- return;
241
- // Progress callback gets task start info via runningTasks update
242
- // This is called before the task is added to running, so it appears in next progress update
243
- }
244
- waitForAny() {
245
- const promises = Array.from(this.running.values()).map((info) => info.promise);
246
- return Promise.race(promises);
247
- }
248
- emitProgress(currentWave, totalWaves, totalTasks, latestResult) {
249
- if (!this.onProgress)
250
- return;
251
- this.onProgress({
252
- totalTasks,
253
- completedTasks: this.completed.size,
254
- failedTasks: this.failed.size,
255
- runningTasks: Array.from(this.running.keys()),
256
- pendingTasks: totalTasks - this.completed.size - this.failed.size - this.skipped.size - this.running.size,
257
- currentWave,
258
- totalWaves,
259
- latestResult,
260
- });
261
- }
262
- }
263
- /**
264
- * Create executor from task array with dependencies.
265
- * Convenience function for common use case.
266
- */
267
- export function createExecutor(tasks, executorFn, options) {
268
- const graph = new TaskGraph();
269
- // Add all tasks first
270
- for (const { task } of tasks) {
271
- graph.addTask(task);
272
- }
273
- // Add dependencies
274
- for (const { task, dependsOn } of tasks) {
275
- if (dependsOn) {
276
- for (const dep of dependsOn) {
277
- graph.addDependency(dep, task.id);
278
- }
279
- }
280
- }
281
- return new ParallelExecutor(graph, executorFn, options);
282
- }
283
- /**
284
- * Create executor directly from tasks using their dependencies arrays.
285
- * Most convenient for LLM agents.
286
- */
287
- export function createExecutorFromTasks(tasks, executorFn, options) {
288
- const graph = new TaskGraph();
289
- graph.addTasksWithDependencies(tasks);
290
- return new ParallelExecutor(graph, executorFn, options);
291
- }
292
- //# sourceMappingURL=executor.js.map