@yasserkhanorg/e2e-agents 1.8.5 → 1.9.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.
- package/README.md +95 -8
- package/dist/adapters/cypress.d.ts +10 -0
- package/dist/adapters/cypress.d.ts.map +1 -0
- package/dist/adapters/cypress.js +86 -0
- package/dist/adapters/framework_adapter.d.ts +41 -0
- package/dist/adapters/framework_adapter.d.ts.map +1 -0
- package/dist/adapters/framework_adapter.js +152 -0
- package/dist/adapters/playwright.d.ts +10 -0
- package/dist/adapters/playwright.d.ts.map +1 -0
- package/dist/adapters/playwright.js +86 -0
- package/dist/adapters/pytest.d.ts +10 -0
- package/dist/adapters/pytest.d.ts.map +1 -0
- package/dist/adapters/pytest.js +96 -0
- package/dist/adapters/supertest.d.ts +12 -0
- package/dist/adapters/supertest.d.ts.map +1 -0
- package/dist/adapters/supertest.js +85 -0
- package/dist/agent/config.d.ts +1 -1
- package/dist/agent/config.d.ts.map +1 -1
- package/dist/agent/git.d.ts +1 -0
- package/dist/agent/git.d.ts.map +1 -1
- package/dist/agent/git.js +3 -0
- package/dist/agentic/fix_loop.d.ts.map +1 -1
- package/dist/agentic/fix_loop.js +5 -4
- package/dist/agentic/runner.d.ts +2 -0
- package/dist/agentic/runner.d.ts.map +1 -1
- package/dist/agentic/runner.js +15 -12
- package/dist/agents/cross-impact.d.ts.map +1 -1
- package/dist/agents/cross-impact.js +6 -1
- package/dist/agents/executor.d.ts.map +1 -1
- package/dist/agents/executor.js +6 -1
- package/dist/agents/strategist.d.ts.map +1 -1
- package/dist/agents/strategist.js +6 -1
- package/dist/agents/test-designer.d.ts.map +1 -1
- package/dist/agents/test-designer.js +6 -1
- package/dist/anthropic_provider.d.ts.map +1 -1
- package/dist/anthropic_provider.js +1 -0
- package/dist/base_provider.d.ts +56 -0
- package/dist/base_provider.d.ts.map +1 -1
- package/dist/base_provider.js +123 -1
- package/dist/budget_ledger.d.ts +28 -0
- package/dist/budget_ledger.d.ts.map +1 -0
- package/dist/budget_ledger.js +62 -0
- package/dist/cache/cached_provider.d.ts +45 -0
- package/dist/cache/cached_provider.d.ts.map +1 -0
- package/dist/cache/cached_provider.js +88 -0
- package/dist/cache/response_cache.d.ts +79 -0
- package/dist/cache/response_cache.d.ts.map +1 -0
- package/dist/cache/response_cache.js +177 -0
- package/dist/cli/commands/bootstrap.d.ts +3 -0
- package/dist/cli/commands/bootstrap.d.ts.map +1 -0
- package/dist/cli/commands/bootstrap.js +109 -0
- package/dist/cli/commands/cost_report.d.ts +3 -0
- package/dist/cli/commands/cost_report.d.ts.map +1 -0
- package/dist/cli/commands/cost_report.js +115 -0
- package/dist/cli/commands/crew.d.ts.map +1 -1
- package/dist/cli/commands/crew.js +118 -1
- package/dist/cli/commands/gate.d.ts +3 -0
- package/dist/cli/commands/gate.d.ts.map +1 -0
- package/dist/cli/commands/gate.js +86 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +7 -62
- package/dist/cli/commands/train.d.ts.map +1 -1
- package/dist/cli/commands/train.js +16 -21
- package/dist/cli/defaults.d.ts +35 -0
- package/dist/cli/defaults.d.ts.map +1 -0
- package/dist/cli/defaults.js +125 -0
- package/dist/cli/errors.d.ts +27 -0
- package/dist/cli/errors.d.ts.map +1 -0
- package/dist/cli/errors.js +57 -0
- package/dist/cli/parse_args.d.ts.map +1 -1
- package/dist/cli/parse_args.js +24 -2
- package/dist/cli/types.d.ts +7 -1
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli.js +47 -2
- package/dist/crew/context.d.ts +15 -0
- package/dist/crew/context.d.ts.map +1 -1
- package/dist/crew/orchestrator.d.ts +14 -0
- package/dist/crew/orchestrator.d.ts.map +1 -1
- package/dist/crew/orchestrator.js +162 -4
- package/dist/crew/protocol.d.ts +13 -0
- package/dist/crew/protocol.d.ts.map +1 -1
- package/dist/crew/provider.d.ts +15 -1
- package/dist/crew/provider.d.ts.map +1 -1
- package/dist/crew/provider.js +24 -4
- package/dist/custom_provider.d.ts.map +1 -1
- package/dist/custom_provider.js +1 -0
- package/dist/engine/diff_loader.d.ts.map +1 -1
- package/dist/engine/diff_loader.js +3 -14
- package/dist/engine/impact_engine.d.ts.map +1 -1
- package/dist/engine/impact_engine.js +9 -23
- package/dist/esm/adapters/cypress.js +49 -0
- package/dist/esm/adapters/framework_adapter.js +114 -0
- package/dist/esm/adapters/playwright.js +49 -0
- package/dist/esm/adapters/pytest.js +59 -0
- package/dist/esm/adapters/supertest.js +48 -0
- package/dist/esm/agent/git.js +3 -1
- package/dist/esm/agentic/fix_loop.js +5 -4
- package/dist/esm/agentic/runner.js +15 -12
- package/dist/esm/agents/cross-impact.js +6 -1
- package/dist/esm/agents/executor.js +6 -1
- package/dist/esm/agents/strategist.js +6 -1
- package/dist/esm/agents/test-designer.js +6 -1
- package/dist/esm/anthropic_provider.js +1 -0
- package/dist/esm/base_provider.js +121 -0
- package/dist/esm/budget_ledger.js +58 -0
- package/dist/esm/cache/cached_provider.js +82 -0
- package/dist/esm/cache/response_cache.js +140 -0
- package/dist/esm/cli/commands/bootstrap.js +106 -0
- package/dist/esm/cli/commands/cost_report.js +112 -0
- package/dist/esm/cli/commands/crew.js +118 -1
- package/dist/esm/cli/commands/gate.js +83 -0
- package/dist/esm/cli/commands/init.js +3 -58
- package/dist/esm/cli/commands/train.js +16 -21
- package/dist/esm/cli/defaults.js +118 -0
- package/dist/esm/cli/errors.js +52 -0
- package/dist/esm/cli/parse_args.js +24 -2
- package/dist/esm/cli.js +47 -2
- package/dist/esm/crew/orchestrator.js +162 -4
- package/dist/esm/crew/provider.js +24 -4
- package/dist/esm/custom_provider.js +1 -0
- package/dist/esm/engine/diff_loader.js +1 -12
- package/dist/esm/engine/impact_engine.js +9 -23
- package/dist/esm/index.js +21 -0
- package/dist/esm/knowledge/cluster_utils.js +60 -0
- package/dist/esm/knowledge/kg_bridge.js +381 -0
- package/dist/esm/knowledge/kg_types.js +3 -0
- package/dist/esm/knowledge/route_families.js +89 -0
- package/dist/esm/mcp-server.js +2 -4
- package/dist/esm/metrics/prometheus.js +149 -0
- package/dist/esm/model_router.js +59 -0
- package/dist/esm/ollama_provider.js +1 -0
- package/dist/esm/openai_provider.js +1 -0
- package/dist/esm/pipeline/orchestrator.js +6 -12
- package/dist/esm/pipeline/stage0_preprocess.js +12 -19
- package/dist/esm/pipeline/stage2_coverage.js +1 -0
- package/dist/esm/pipeline/stage3_generation.js +1 -0
- package/dist/esm/progress.js +112 -0
- package/dist/esm/prompts/coverage.js +7 -24
- package/dist/esm/prompts/cross-impact.js +3 -21
- package/dist/esm/prompts/generation.js +158 -36
- package/dist/esm/prompts/generation_profile.js +147 -0
- package/dist/esm/prompts/heal.js +33 -15
- package/dist/esm/prompts/impact.js +3 -22
- package/dist/esm/prompts/json_extract.js +36 -0
- package/dist/esm/prompts/strategist.js +2 -20
- package/dist/esm/prompts/test-designer.js +6 -21
- package/dist/esm/provider_factory.js +6 -4
- package/dist/esm/reporters/junit.js +86 -0
- package/dist/esm/reporters/reporter.js +3 -0
- package/dist/esm/reporters/sarif.js +131 -0
- package/dist/esm/resilience/circuit_breaker.js +78 -0
- package/dist/esm/resilience/retry.js +56 -0
- package/dist/esm/sanitize.js +66 -0
- package/dist/esm/training/kg_scanner.js +115 -0
- package/dist/esm/training/scanner.js +27 -34
- package/dist/esm/version.js +33 -0
- package/dist/index.d.ts +21 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +45 -1
- package/dist/knowledge/cluster_utils.d.ts +28 -0
- package/dist/knowledge/cluster_utils.d.ts.map +1 -0
- package/dist/knowledge/cluster_utils.js +67 -0
- package/dist/knowledge/kg_bridge.d.ts +31 -0
- package/dist/knowledge/kg_bridge.d.ts.map +1 -0
- package/dist/knowledge/kg_bridge.js +388 -0
- package/dist/knowledge/kg_types.d.ts +75 -0
- package/dist/knowledge/kg_types.d.ts.map +1 -0
- package/dist/knowledge/kg_types.js +4 -0
- package/dist/knowledge/route_families.d.ts +18 -0
- package/dist/knowledge/route_families.d.ts.map +1 -1
- package/dist/knowledge/route_families.js +91 -0
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +2 -4
- package/dist/metrics/prometheus.d.ts +37 -0
- package/dist/metrics/prometheus.d.ts.map +1 -0
- package/dist/metrics/prometheus.js +153 -0
- package/dist/model_router.d.ts +28 -0
- package/dist/model_router.d.ts.map +1 -0
- package/dist/model_router.js +63 -0
- package/dist/ollama_provider.d.ts.map +1 -1
- package/dist/ollama_provider.js +1 -0
- package/dist/openai_provider.d.ts.map +1 -1
- package/dist/openai_provider.js +1 -0
- package/dist/pipeline/orchestrator.d.ts +2 -0
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +6 -12
- package/dist/pipeline/stage0_preprocess.d.ts.map +1 -1
- package/dist/pipeline/stage0_preprocess.js +11 -18
- package/dist/pipeline/stage2_coverage.d.ts +2 -0
- package/dist/pipeline/stage2_coverage.d.ts.map +1 -1
- package/dist/pipeline/stage2_coverage.js +1 -0
- package/dist/pipeline/stage3_generation.d.ts +2 -0
- package/dist/pipeline/stage3_generation.d.ts.map +1 -1
- package/dist/pipeline/stage3_generation.js +1 -0
- package/dist/pipeline/stage4_heal.d.ts +2 -0
- package/dist/pipeline/stage4_heal.d.ts.map +1 -1
- package/dist/progress.d.ts +22 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +116 -0
- package/dist/prompts/coverage.d.ts +2 -0
- package/dist/prompts/coverage.d.ts.map +1 -1
- package/dist/prompts/coverage.js +7 -24
- package/dist/prompts/cross-impact.d.ts +1 -0
- package/dist/prompts/cross-impact.d.ts.map +1 -1
- package/dist/prompts/cross-impact.js +3 -21
- package/dist/prompts/generation.d.ts +3 -1
- package/dist/prompts/generation.d.ts.map +1 -1
- package/dist/prompts/generation.js +158 -36
- package/dist/prompts/generation_profile.d.ts +29 -0
- package/dist/prompts/generation_profile.d.ts.map +1 -0
- package/dist/prompts/generation_profile.js +151 -0
- package/dist/prompts/heal.d.ts +3 -1
- package/dist/prompts/heal.d.ts.map +1 -1
- package/dist/prompts/heal.js +33 -15
- package/dist/prompts/impact.d.ts +1 -0
- package/dist/prompts/impact.d.ts.map +1 -1
- package/dist/prompts/impact.js +3 -22
- package/dist/prompts/json_extract.d.ts +14 -0
- package/dist/prompts/json_extract.d.ts.map +1 -0
- package/dist/prompts/json_extract.js +39 -0
- package/dist/prompts/strategist.d.ts.map +1 -1
- package/dist/prompts/strategist.js +2 -20
- package/dist/prompts/test-designer.d.ts +2 -0
- package/dist/prompts/test-designer.d.ts.map +1 -1
- package/dist/prompts/test-designer.js +6 -21
- package/dist/provider_factory.d.ts.map +1 -1
- package/dist/provider_factory.js +6 -4
- package/dist/reporters/junit.d.ts +6 -0
- package/dist/reporters/junit.d.ts.map +1 -0
- package/dist/reporters/junit.js +89 -0
- package/dist/reporters/reporter.d.ts +42 -0
- package/dist/reporters/reporter.d.ts.map +1 -0
- package/dist/reporters/reporter.js +4 -0
- package/dist/reporters/sarif.d.ts +7 -0
- package/dist/reporters/sarif.d.ts.map +1 -0
- package/dist/reporters/sarif.js +134 -0
- package/dist/resilience/circuit_breaker.d.ts +36 -0
- package/dist/resilience/circuit_breaker.d.ts.map +1 -0
- package/dist/resilience/circuit_breaker.js +82 -0
- package/dist/resilience/retry.d.ts +11 -0
- package/dist/resilience/retry.d.ts.map +1 -0
- package/dist/resilience/retry.js +59 -0
- package/dist/sanitize.d.ts +15 -0
- package/dist/sanitize.d.ts.map +1 -0
- package/dist/sanitize.js +71 -0
- package/dist/training/kg_scanner.d.ts +13 -0
- package/dist/training/kg_scanner.d.ts.map +1 -0
- package/dist/training/kg_scanner.js +118 -0
- package/dist/training/scanner.d.ts +7 -2
- package/dist/training/scanner.d.ts.map +1 -1
- package/dist/training/scanner.js +27 -34
- package/dist/version.d.ts +6 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +36 -0
- package/package.json +7 -2
- package/schemas/route-families.schema.json +31 -1
|
@@ -5,13 +5,16 @@ import { existsSync, mkdirSync, renameSync, writeFileSync } from 'fs';
|
|
|
5
5
|
import { dirname, join, resolve } from 'path';
|
|
6
6
|
import * as readline from 'readline';
|
|
7
7
|
import { resolveConfig } from '../../agent/config.js';
|
|
8
|
-
import { loadRouteFamilyManifest } from '../../knowledge/route_families.js';
|
|
8
|
+
import { loadRouteFamilyManifest, serializeManifest } from '../../knowledge/route_families.js';
|
|
9
9
|
import { LLMProviderFactory } from '../../provider_factory.js';
|
|
10
10
|
import { logger, LogLevel } from '../../logger.js';
|
|
11
|
+
import { getVersion } from '../../version.js';
|
|
11
12
|
import { scanProject } from '../../training/scanner.js';
|
|
13
|
+
import { scanFromKnowledgeGraph } from '../../training/kg_scanner.js';
|
|
12
14
|
import { mergeFamilies, detectStaleFamilies } from '../../training/merger.js';
|
|
13
15
|
import { enrichFamilies } from '../../training/enricher.js';
|
|
14
16
|
import { getCommitFiles, validateCommit, buildValidationReport, formatValidationReport } from '../../training/validator.js';
|
|
17
|
+
import { loadKnowledgeGraph } from '../../knowledge/kg_bridge.js';
|
|
15
18
|
class TrainError extends Error {
|
|
16
19
|
constructor(message) {
|
|
17
20
|
super(message);
|
|
@@ -121,24 +124,6 @@ function ask(rl, question, defaultValue) {
|
|
|
121
124
|
});
|
|
122
125
|
});
|
|
123
126
|
}
|
|
124
|
-
function serializeManifest(manifest) {
|
|
125
|
-
const output = {
|
|
126
|
-
families: manifest.families.map((f) => {
|
|
127
|
-
// Remove undefined/empty optional fields for clean JSON
|
|
128
|
-
const cleaned = { ...f };
|
|
129
|
-
const optionalArrays = ['pageObjects', 'components', 'webappPaths', 'serverPaths', 'specDirs', 'cypressSpecDirs', 'tags', 'userFlows', 'features'];
|
|
130
|
-
for (const key of optionalArrays) {
|
|
131
|
-
if (!cleaned[key] || (Array.isArray(cleaned[key]) && cleaned[key].length === 0)) {
|
|
132
|
-
delete cleaned[key];
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
if (!cleaned.priority)
|
|
136
|
-
delete cleaned.priority;
|
|
137
|
-
return cleaned;
|
|
138
|
-
}),
|
|
139
|
-
};
|
|
140
|
-
return JSON.stringify(output, null, 2) + '\n';
|
|
141
|
-
}
|
|
142
127
|
export async function runTrainCommand(args, autoConfig) {
|
|
143
128
|
const opts = resolveTrainOptions(args, autoConfig);
|
|
144
129
|
const totalTimer = logger.timer('train-total');
|
|
@@ -151,12 +136,22 @@ export async function runTrainCommand(args, autoConfig) {
|
|
|
151
136
|
logger.info('e2e-ai-agents train');
|
|
152
137
|
logger.info('===================');
|
|
153
138
|
// ---------- Phase 1: Deterministic scan ----------
|
|
139
|
+
// Prefer knowledge graph when available
|
|
140
|
+
const kg = loadKnowledgeGraph(opts.appPath);
|
|
154
141
|
logger.info('Scanning project structure...');
|
|
155
142
|
if (opts.serverRoot) {
|
|
156
143
|
logger.info(`Server root: ${opts.serverRoot}`);
|
|
157
144
|
}
|
|
158
145
|
const scanTimer = logger.timer('scan');
|
|
159
|
-
|
|
146
|
+
let scanResult;
|
|
147
|
+
if (kg) {
|
|
148
|
+
logger.info('Using knowledge graph for scanning (found .understand-anything/knowledge-graph.json)');
|
|
149
|
+
scanResult = scanFromKnowledgeGraph(kg);
|
|
150
|
+
logger.info(`KG: ${kg.nodes.length} nodes, ${kg.edges.length} edges`);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
scanResult = scanProject(opts.appPath, opts.testsRoot !== opts.appPath ? opts.testsRoot : undefined, opts.serverRoot, opts.gitRepoRoot);
|
|
154
|
+
}
|
|
160
155
|
timings.scan = scanTimer.end();
|
|
161
156
|
logger.info(`Found ${scanResult.stats.totalSourceFiles} source files, ${scanResult.stats.totalTestFiles} test files`);
|
|
162
157
|
logger.info(`Discovered ${scanResult.families.length} candidate families`);
|
|
@@ -332,7 +327,7 @@ export async function runTrainCommand(args, autoConfig) {
|
|
|
332
327
|
const reportDir = dirname(opts.outputPath);
|
|
333
328
|
const trainReport = {
|
|
334
329
|
timestamp: new Date().toISOString(),
|
|
335
|
-
version:
|
|
330
|
+
version: getVersion(),
|
|
336
331
|
timings,
|
|
337
332
|
families: {
|
|
338
333
|
total: mergeResult.manifest.families.length,
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
import { existsSync, readFileSync } from 'fs';
|
|
4
|
+
import { execFileSync } from 'child_process';
|
|
5
|
+
import { join, resolve } from 'path';
|
|
6
|
+
/**
|
|
7
|
+
* Detect the test framework from package.json dependencies.
|
|
8
|
+
*/
|
|
9
|
+
export function detectFramework(appPath) {
|
|
10
|
+
const resolvedPath = resolve(appPath);
|
|
11
|
+
const pkgPath = join(resolvedPath, 'package.json');
|
|
12
|
+
if (!existsSync(pkgPath)) {
|
|
13
|
+
return 'auto';
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
17
|
+
const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
18
|
+
if (allDeps['@playwright/test'] || allDeps.playwright) {
|
|
19
|
+
return 'playwright';
|
|
20
|
+
}
|
|
21
|
+
if (allDeps.cypress) {
|
|
22
|
+
return 'cypress';
|
|
23
|
+
}
|
|
24
|
+
if (allDeps['selenium-webdriver'] || allDeps.webdriverio) {
|
|
25
|
+
return 'selenium';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// ignore malformed package.json
|
|
30
|
+
}
|
|
31
|
+
return 'auto';
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Detect the tests root directory by scanning common conventions.
|
|
35
|
+
*/
|
|
36
|
+
export function detectTestsRoot(appPath) {
|
|
37
|
+
const resolvedPath = resolve(appPath);
|
|
38
|
+
const candidates = [
|
|
39
|
+
'e2e-tests/playwright',
|
|
40
|
+
'e2e-tests',
|
|
41
|
+
'e2e',
|
|
42
|
+
'tests/e2e',
|
|
43
|
+
'test/e2e',
|
|
44
|
+
'tests',
|
|
45
|
+
'test',
|
|
46
|
+
'specs',
|
|
47
|
+
'playwright',
|
|
48
|
+
'cypress',
|
|
49
|
+
];
|
|
50
|
+
for (const candidate of candidates) {
|
|
51
|
+
if (existsSync(join(resolvedPath, candidate))) {
|
|
52
|
+
return candidate;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Detect the git default branch for diffing.
|
|
59
|
+
* Returns origin/<branch> format.
|
|
60
|
+
*/
|
|
61
|
+
export function detectGitDefaultBranch(appPath) {
|
|
62
|
+
try {
|
|
63
|
+
// Try to find the remote HEAD branch first
|
|
64
|
+
const remoteInfo = execFileSync('git', ['remote', 'show', 'origin'], {
|
|
65
|
+
cwd: resolve(appPath),
|
|
66
|
+
encoding: 'utf-8',
|
|
67
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
68
|
+
timeout: 5000,
|
|
69
|
+
});
|
|
70
|
+
const headMatch = remoteInfo.match(/HEAD branch:\s*(.+)/);
|
|
71
|
+
if (headMatch) {
|
|
72
|
+
return `origin/${headMatch[1].trim()}`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// fallback to current branch
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const result = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
80
|
+
cwd: resolve(appPath),
|
|
81
|
+
encoding: 'utf-8',
|
|
82
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
83
|
+
timeout: 5000,
|
|
84
|
+
}).trim();
|
|
85
|
+
return `origin/${result}`;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return 'origin/main';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Detect the project root by walking up to find package.json or .git.
|
|
93
|
+
*/
|
|
94
|
+
export function detectProjectRoot(startDir) {
|
|
95
|
+
let current = resolve(startDir);
|
|
96
|
+
while (true) {
|
|
97
|
+
if (existsSync(join(current, 'package.json')) || existsSync(join(current, '.git'))) {
|
|
98
|
+
return current;
|
|
99
|
+
}
|
|
100
|
+
const parent = resolve(current, '..');
|
|
101
|
+
if (parent === current) {
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
current = parent;
|
|
105
|
+
}
|
|
106
|
+
return startDir;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Resolve defaults for CLI commands that need path/testsRoot/framework/since.
|
|
110
|
+
* Explicit values from CLI flags take precedence over detected values.
|
|
111
|
+
*/
|
|
112
|
+
export function resolveDefaults(explicit) {
|
|
113
|
+
const path = explicit.path || detectProjectRoot(process.cwd());
|
|
114
|
+
const testsRoot = explicit.testsRoot || detectTestsRoot(path) || '.';
|
|
115
|
+
const framework = explicit.framework || detectFramework(path);
|
|
116
|
+
const since = explicit.gitSince || detectGitDefaultBranch(path);
|
|
117
|
+
return { path, testsRoot, framework, since };
|
|
118
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
/**
|
|
4
|
+
* CLI Error types with structured exit codes.
|
|
5
|
+
*
|
|
6
|
+
* Exit codes:
|
|
7
|
+
* 0 = success
|
|
8
|
+
* 1 = general/user error (bad args, missing config, invalid input)
|
|
9
|
+
* 2 = budget exceeded
|
|
10
|
+
* 3 = LLM provider unavailable (API down, auth failure)
|
|
11
|
+
* 4 = invalid manifest or config file
|
|
12
|
+
*/
|
|
13
|
+
export const EXIT_CODES = {
|
|
14
|
+
SUCCESS: 0,
|
|
15
|
+
GENERAL_ERROR: 1,
|
|
16
|
+
BUDGET_EXCEEDED: 2,
|
|
17
|
+
PROVIDER_UNAVAILABLE: 3,
|
|
18
|
+
INVALID_CONFIG: 4,
|
|
19
|
+
};
|
|
20
|
+
export class CliError extends Error {
|
|
21
|
+
constructor(message, exitCode = EXIT_CODES.GENERAL_ERROR) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.exitCode = exitCode;
|
|
24
|
+
this.name = 'CliError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Classify an unknown error into the appropriate exit code.
|
|
29
|
+
*/
|
|
30
|
+
export function classifyError(error) {
|
|
31
|
+
if (error instanceof CliError)
|
|
32
|
+
return error.exitCode;
|
|
33
|
+
const msg = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
34
|
+
// Budget errors
|
|
35
|
+
if (msg.includes('budget exceeded') || msg.includes('budget limit')) {
|
|
36
|
+
return EXIT_CODES.BUDGET_EXCEEDED;
|
|
37
|
+
}
|
|
38
|
+
// Provider/auth errors
|
|
39
|
+
if (msg.includes('api key') || msg.includes('authentication') ||
|
|
40
|
+
msg.includes('unauthorized') || msg.includes('403') ||
|
|
41
|
+
(msg.includes('provider') && msg.includes('unavailable')) ||
|
|
42
|
+
msg.includes('econnrefused') || msg.includes('econnreset')) {
|
|
43
|
+
return EXIT_CODES.PROVIDER_UNAVAILABLE;
|
|
44
|
+
}
|
|
45
|
+
// Config/manifest errors
|
|
46
|
+
if ((msg.includes('manifest') && (msg.includes('invalid') || msg.includes('not found') || msg.includes('parse'))) ||
|
|
47
|
+
(msg.includes('config') && msg.includes('invalid')) ||
|
|
48
|
+
(msg.includes('route-families') && msg.includes('invalid'))) {
|
|
49
|
+
return EXIT_CODES.INVALID_CONFIG;
|
|
50
|
+
}
|
|
51
|
+
return EXIT_CODES.GENERAL_ERROR;
|
|
52
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// See LICENSE.txt for license information.
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
4
|
import { dirname, join, resolve } from 'path';
|
|
5
|
+
import { logger } from '../logger.js';
|
|
5
6
|
export const CONFIG_CANDIDATES = ['e2e-ai-agents.config.json', '.e2e-ai-agents.config.json'];
|
|
6
7
|
export function findConfigUpwards(startDir) {
|
|
7
8
|
if (!startDir) {
|
|
@@ -62,6 +63,7 @@ const FLAGS = {
|
|
|
62
63
|
'--generate': { key: 'analyzeGenerate', type: 'boolean' },
|
|
63
64
|
'--heal': { key: 'analyzeHeal', type: 'boolean' },
|
|
64
65
|
'--no-ai': { key: 'noAi', type: 'boolean' },
|
|
66
|
+
'--degraded-mode': { key: 'degradedMode', type: 'boolean' },
|
|
65
67
|
'--enrich': { key: 'trainEnrich', type: 'boolean' },
|
|
66
68
|
'--no-enrich': { key: 'trainEnrich', type: 'boolean-false' },
|
|
67
69
|
'--validate': { key: 'trainValidate', type: 'boolean' },
|
|
@@ -131,6 +133,13 @@ const FLAGS = {
|
|
|
131
133
|
type: 'csv',
|
|
132
134
|
transform: (v) => csvSplit(v).filter((s) => s === 'run-now' || s === 'must-add-tests' || s === 'safe-to-merge'),
|
|
133
135
|
},
|
|
136
|
+
// -- gate command --
|
|
137
|
+
'--threshold': { key: 'gateThreshold', type: 'number' },
|
|
138
|
+
// -- bootstrap command --
|
|
139
|
+
'--kg-path': { key: 'bootstrapKgPath', type: 'string' },
|
|
140
|
+
'--scaffold-framework': { key: 'bootstrapScaffoldFramework', type: 'boolean' },
|
|
141
|
+
'--test-mode': { key: 'bootstrapTestMode', type: 'enum', enumValues: ['ui', 'api', 'both'] },
|
|
142
|
+
'--max-families': { key: 'bootstrapMaxFamilies', type: 'number' },
|
|
134
143
|
};
|
|
135
144
|
// Build a lookup from alias -> canonical flag name
|
|
136
145
|
const ALIAS_MAP = {};
|
|
@@ -146,7 +155,8 @@ const COMMANDS = new Set([
|
|
|
146
155
|
'init', 'impact', 'plan', 'heal', 'suggest', 'generate',
|
|
147
156
|
'finalize-generated-tests', 'feedback',
|
|
148
157
|
'traceability-capture', 'traceability-ingest',
|
|
149
|
-
'analyze', 'llm-health', 'train', 'crew',
|
|
158
|
+
'analyze', 'llm-health', 'train', 'crew', 'cost-report', 'gate',
|
|
159
|
+
'bootstrap',
|
|
150
160
|
]);
|
|
151
161
|
// ---------------------------------------------------------------------------
|
|
152
162
|
// Parser
|
|
@@ -168,6 +178,9 @@ export function parseArgs(argv) {
|
|
|
168
178
|
const arg = argv[i];
|
|
169
179
|
const canonical = ALIAS_MAP[arg];
|
|
170
180
|
if (!canonical) {
|
|
181
|
+
if (arg.startsWith('--')) {
|
|
182
|
+
logger.warn(`Unknown flag "${arg}" (ignored)`);
|
|
183
|
+
}
|
|
171
184
|
continue;
|
|
172
185
|
}
|
|
173
186
|
const def = FLAGS[canonical];
|
|
@@ -196,7 +209,16 @@ export function parseArgs(argv) {
|
|
|
196
209
|
break;
|
|
197
210
|
case 'number-raw':
|
|
198
211
|
if (next) {
|
|
199
|
-
|
|
212
|
+
const rawValue = def.transform ? def.transform(next) : Number(next);
|
|
213
|
+
// Allow non-number transforms through; reject NaN/Infinity for numbers
|
|
214
|
+
if (typeof rawValue === 'number') {
|
|
215
|
+
if (Number.isFinite(rawValue)) {
|
|
216
|
+
setField(parsed, def.key, rawValue);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
setField(parsed, def.key, rawValue);
|
|
221
|
+
}
|
|
200
222
|
i += 1;
|
|
201
223
|
}
|
|
202
224
|
break;
|
package/dist/esm/cli.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// See LICENSE.txt for license information.
|
|
4
4
|
import { resolveConfig } from './agent/config.js';
|
|
5
5
|
import { parseArgs, resolveAutoConfig } from './cli/parse_args.js';
|
|
6
|
+
import { resolveDefaults } from './cli/defaults.js';
|
|
6
7
|
import { printUsage } from './cli/usage.js';
|
|
7
8
|
import { runLlmHealth } from './cli/commands/llm_health.js';
|
|
8
9
|
import { runAnalyzeCommand } from './cli/commands/analyze.js';
|
|
@@ -16,14 +17,42 @@ import { runGenerateCommand } from './cli/commands/generate.js';
|
|
|
16
17
|
import { runInitCommand } from './cli/commands/init.js';
|
|
17
18
|
import { runTrainCommand } from './cli/commands/train.js';
|
|
18
19
|
import { runCrewCommand } from './cli/commands/crew.js';
|
|
20
|
+
import { runCostReportCommand } from './cli/commands/cost_report.js';
|
|
21
|
+
import { runGateCommand } from './cli/commands/gate.js';
|
|
22
|
+
import { runBootstrapCommand } from './cli/commands/bootstrap.js';
|
|
23
|
+
import { classifyError, EXIT_CODES } from './cli/errors.js';
|
|
24
|
+
// Commands that skip default resolution (they handle their own setup)
|
|
25
|
+
const SKIP_DEFAULTS_COMMANDS = new Set(['init', 'llm-health', 'cost-report', 'bootstrap']);
|
|
26
|
+
// Commands that need path/testsRoot/framework/since
|
|
27
|
+
const NEEDS_DEFAULTS_COMMANDS = new Set([
|
|
28
|
+
'impact', 'plan', 'suggest', 'crew', 'generate', 'heal', 'analyze', 'train',
|
|
29
|
+
'feedback', 'traceability-capture', 'traceability-ingest', 'finalize-generated-tests',
|
|
30
|
+
]);
|
|
19
31
|
async function main() {
|
|
20
32
|
const args = parseArgs(process.argv.slice(2));
|
|
21
33
|
const autoConfig = resolveAutoConfig(args);
|
|
34
|
+
// Auto-detect defaults for commands that need them (when no config file found)
|
|
35
|
+
if (args.command && NEEDS_DEFAULTS_COMMANDS.has(args.command) && !SKIP_DEFAULTS_COMMANDS.has(args.command)) {
|
|
36
|
+
const defaults = resolveDefaults({
|
|
37
|
+
path: args.path,
|
|
38
|
+
testsRoot: args.testsRoot,
|
|
39
|
+
framework: args.framework,
|
|
40
|
+
gitSince: args.gitSince,
|
|
41
|
+
});
|
|
42
|
+
args.path = args.path || defaults.path;
|
|
43
|
+
args.testsRoot = args.testsRoot || defaults.testsRoot;
|
|
44
|
+
args.framework = args.framework || defaults.framework;
|
|
45
|
+
args.gitSince = args.gitSince || defaults.since;
|
|
46
|
+
}
|
|
22
47
|
if (args.command === 'init') {
|
|
23
48
|
const hasYes = process.argv.includes('--yes') || process.argv.includes('-y');
|
|
24
49
|
await runInitCommand(hasYes);
|
|
25
50
|
return;
|
|
26
51
|
}
|
|
52
|
+
if (args.command === 'bootstrap') {
|
|
53
|
+
await runBootstrapCommand(args);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
27
56
|
if (args.command === 'train') {
|
|
28
57
|
await runTrainCommand(args, autoConfig);
|
|
29
58
|
return;
|
|
@@ -64,6 +93,14 @@ async function main() {
|
|
|
64
93
|
await runCrewCommand(args, autoConfig);
|
|
65
94
|
return;
|
|
66
95
|
}
|
|
96
|
+
if (args.command === 'cost-report') {
|
|
97
|
+
runCostReportCommand(args);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (args.command === 'gate') {
|
|
101
|
+
await runGateCommand(args, autoConfig);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
67
104
|
if (!args.path && !autoConfig) {
|
|
68
105
|
console.error('Error: --path is required (or provide a config file with path set)');
|
|
69
106
|
printUsage();
|
|
@@ -138,6 +175,14 @@ async function main() {
|
|
|
138
175
|
process.exit(1);
|
|
139
176
|
}
|
|
140
177
|
main().catch((error) => {
|
|
141
|
-
|
|
142
|
-
|
|
178
|
+
const exitCode = classifyError(error);
|
|
179
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
180
|
+
console.error(message);
|
|
181
|
+
if (exitCode === EXIT_CODES.BUDGET_EXCEEDED) {
|
|
182
|
+
console.error('Hint: Increase --budget or use --degraded-mode to skip AI features.');
|
|
183
|
+
}
|
|
184
|
+
else if (exitCode === EXIT_CODES.PROVIDER_UNAVAILABLE) {
|
|
185
|
+
console.error('Hint: Check API key or use --degraded-mode for deterministic analysis only.');
|
|
186
|
+
}
|
|
187
|
+
process.exit(exitCode);
|
|
143
188
|
});
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import { getChangedFiles, isTestFile } from '../agent/git.js';
|
|
7
7
|
import { preprocess } from '../pipeline/stage0_preprocess.js';
|
|
8
8
|
import { logger } from '../logger.js';
|
|
9
|
+
import { BudgetExceededError } from '../base_provider.js';
|
|
10
|
+
import { BudgetLedger } from '../budget_ledger.js';
|
|
9
11
|
import { createEmptyUsageStats, mergeUsageStats } from './context.js';
|
|
10
12
|
import { WORKFLOWS } from './workflows.js';
|
|
11
13
|
export class CrewOrchestrator {
|
|
@@ -15,10 +17,61 @@ export class CrewOrchestrator {
|
|
|
15
17
|
registerAgent(agent) {
|
|
16
18
|
this.agents.set(agent.role, agent);
|
|
17
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Load and register plugins from file paths.
|
|
22
|
+
* Each module must default-export an object satisfying AgentPlugin.
|
|
23
|
+
*/
|
|
24
|
+
async loadPlugins(pluginPaths) {
|
|
25
|
+
const loaded = [];
|
|
26
|
+
for (const pluginPath of pluginPaths) {
|
|
27
|
+
try {
|
|
28
|
+
// Security: Only allow relative paths (starting with . or ..) to prevent loading arbitrary modules.
|
|
29
|
+
// Absolute paths, URLs, and node_modules references are rejected.
|
|
30
|
+
if (!pluginPath.startsWith('.')) {
|
|
31
|
+
logger.warn(`Plugin path must be relative (start with ./): ${pluginPath} — skipped`);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const resolved = new URL(pluginPath, `file://${process.cwd()}/`).href;
|
|
35
|
+
// Security: reject paths that resolve outside the workspace (e.g., ../../etc/evil.js)
|
|
36
|
+
const cwd = `file://${process.cwd()}/`;
|
|
37
|
+
if (!resolved.startsWith(cwd)) {
|
|
38
|
+
logger.warn(`Plugin path '${pluginPath}' resolves outside workspace — skipped`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const mod = await import(resolved);
|
|
42
|
+
const plugin = mod.default || mod;
|
|
43
|
+
if (!plugin.role || typeof plugin.execute !== 'function') {
|
|
44
|
+
logger.warn(`Plugin at ${pluginPath} missing required role/execute — skipped`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
// Warn if plugin overrides a built-in agent
|
|
48
|
+
if (this.agents.has(plugin.role)) {
|
|
49
|
+
logger.warn(`Plugin '${plugin.role}' overrides built-in agent — ensure this is intentional`);
|
|
50
|
+
}
|
|
51
|
+
this.agents.set(plugin.role, plugin);
|
|
52
|
+
loaded.push(plugin.role);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
56
|
+
logger.warn(`Failed to load plugin ${pluginPath}: ${msg}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return loaded;
|
|
60
|
+
}
|
|
18
61
|
async run(config) {
|
|
19
62
|
const workflow = WORKFLOWS[config.workflow || 'full-qa'];
|
|
20
63
|
const timings = {};
|
|
21
64
|
const warnings = [];
|
|
65
|
+
// Load plugins if configured, then inject them into workflow phases
|
|
66
|
+
const pluginRoles = [];
|
|
67
|
+
if (config.plugins && config.plugins.length > 0) {
|
|
68
|
+
const loaded = await this.loadPlugins(config.plugins);
|
|
69
|
+
pluginRoles.push(...loaded);
|
|
70
|
+
if (loaded.length > 0) {
|
|
71
|
+
logger.info(`Loaded ${loaded.length} plugins: ${loaded.join(', ')}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const effectivePhases = this.injectPluginsIntoPhases(workflow.phases, pluginRoles);
|
|
22
75
|
// Step 1: Get changed files
|
|
23
76
|
const gitResult = getChangedFiles(config.appPath, config.gitSince, {
|
|
24
77
|
includeUncommitted: config.gitIncludeUncommitted,
|
|
@@ -32,6 +85,8 @@ export class CrewOrchestrator {
|
|
|
32
85
|
if (changedFiles.length === 0) {
|
|
33
86
|
warnings.push('No changed application files detected.');
|
|
34
87
|
}
|
|
88
|
+
// Create shared budget ledger for aggregate cost tracking across all agents
|
|
89
|
+
const budgetLedger = config.budgetUSD ? new BudgetLedger(config.budgetUSD) : undefined;
|
|
35
90
|
// Initialize context (will be populated during preprocess phase)
|
|
36
91
|
const ctx = {
|
|
37
92
|
changedFiles,
|
|
@@ -46,6 +101,8 @@ export class CrewOrchestrator {
|
|
|
46
101
|
testsRoot: config.testsRoot,
|
|
47
102
|
gitSince: config.gitSince,
|
|
48
103
|
providerOverride: config.providerOverride,
|
|
104
|
+
budgetUSD: config.budgetUSD,
|
|
105
|
+
budgetLedger,
|
|
49
106
|
impactedFlows: [],
|
|
50
107
|
strategyEntries: [],
|
|
51
108
|
testDesigns: [],
|
|
@@ -54,14 +111,21 @@ export class CrewOrchestrator {
|
|
|
54
111
|
findings: [],
|
|
55
112
|
generatedSpecs: [],
|
|
56
113
|
usage: createEmptyUsageStats(),
|
|
114
|
+
agentUsage: [],
|
|
57
115
|
messages: [],
|
|
58
116
|
warnings,
|
|
59
117
|
};
|
|
60
118
|
// Execute each phase
|
|
61
|
-
for (const phase of
|
|
119
|
+
for (const phase of effectivePhases) {
|
|
62
120
|
const timer = logger.timer(`crew:${phase.name}`);
|
|
63
121
|
if (phase.handler === 'built-in') {
|
|
64
122
|
await this.runBuiltInPhase(phase.name, ctx, config);
|
|
123
|
+
// Dry-run: after preprocess, return summary without running agents
|
|
124
|
+
if (config.dryRun && phase.name === 'preprocess') {
|
|
125
|
+
timings[phase.name] = timer.end();
|
|
126
|
+
ctx.warnings.push('Dry run — no LLM calls were made.');
|
|
127
|
+
return { context: ctx, warnings, timings, dryRun: true };
|
|
128
|
+
}
|
|
65
129
|
}
|
|
66
130
|
else if (phase.parallel && phase.parallel.length > 0) {
|
|
67
131
|
await this.runParallel(phase.parallel, phase.name, ctx);
|
|
@@ -73,9 +137,10 @@ export class CrewOrchestrator {
|
|
|
73
137
|
warnings.push(`Phase '${phase.name}' has no handler, parallel, or sequential agents — skipped.`);
|
|
74
138
|
}
|
|
75
139
|
timings[phase.name] = timer.end();
|
|
76
|
-
// Budget check
|
|
77
|
-
|
|
78
|
-
|
|
140
|
+
// Budget check — prefer ledger (aggregate across all providers) over ctx.usage
|
|
141
|
+
const currentCost = budgetLedger ? budgetLedger.totalCost : ctx.usage.totalCost;
|
|
142
|
+
if (config.budgetUSD && currentCost >= config.budgetUSD) {
|
|
143
|
+
warnings.push(`Budget limit reached ($${currentCost.toFixed(4)} >= $${config.budgetUSD}). Stopping workflow.`);
|
|
79
144
|
break;
|
|
80
145
|
}
|
|
81
146
|
}
|
|
@@ -92,10 +157,19 @@ export class CrewOrchestrator {
|
|
|
92
157
|
};
|
|
93
158
|
}
|
|
94
159
|
const task = { role, action, input: null };
|
|
160
|
+
const startMs = Date.now();
|
|
95
161
|
try {
|
|
96
162
|
const result = await agent.execute(task, ctx);
|
|
163
|
+
const durationMs = Date.now() - startMs;
|
|
97
164
|
if (result.usage) {
|
|
98
165
|
mergeUsageStats(ctx.usage, result.usage);
|
|
166
|
+
ctx.agentUsage.push({
|
|
167
|
+
agent: role,
|
|
168
|
+
inputTokens: result.usage.totalInputTokens,
|
|
169
|
+
outputTokens: result.usage.totalOutputTokens,
|
|
170
|
+
cost: result.usage.totalCost,
|
|
171
|
+
durationMs,
|
|
172
|
+
});
|
|
99
173
|
}
|
|
100
174
|
if (result.warnings && result.warnings.length > 0) {
|
|
101
175
|
ctx.warnings.push(...result.warnings);
|
|
@@ -103,6 +177,10 @@ export class CrewOrchestrator {
|
|
|
103
177
|
return result;
|
|
104
178
|
}
|
|
105
179
|
catch (error) {
|
|
180
|
+
if (error instanceof BudgetExceededError) {
|
|
181
|
+
ctx.warnings.push(`Budget exceeded ($${error.currentCost.toFixed(4)} >= $${error.budgetUSD}). Agent '${role}' skipped.`);
|
|
182
|
+
return { role, status: 'failed', output: null, warnings: [error.message] };
|
|
183
|
+
}
|
|
106
184
|
const message = error instanceof Error ? error.message : String(error);
|
|
107
185
|
ctx.warnings.push(`Agent '${role}' failed: ${message}`);
|
|
108
186
|
return { role, status: 'failed', output: null, warnings: [message] };
|
|
@@ -158,6 +236,86 @@ export class CrewOrchestrator {
|
|
|
158
236
|
}
|
|
159
237
|
this.checkPhaseResults(phaseName, results, ctx);
|
|
160
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* Inject loaded plugins into workflow phases based on their `phase` and `runAfter` fields.
|
|
241
|
+
* Plugins with `runAfter` dependencies are appended to the sequential list of the matching phase;
|
|
242
|
+
* plugins without `runAfter` are appended to the parallel list.
|
|
243
|
+
* Returns a new array of phases (does not mutate the original workflow definition).
|
|
244
|
+
*/
|
|
245
|
+
injectPluginsIntoPhases(phases, pluginRoles) {
|
|
246
|
+
if (pluginRoles.length === 0)
|
|
247
|
+
return phases;
|
|
248
|
+
// Build mutable copies keyed by phase name
|
|
249
|
+
const phaseMap = new Map();
|
|
250
|
+
const ordered = [];
|
|
251
|
+
for (const p of phases) {
|
|
252
|
+
ordered.push(p.name);
|
|
253
|
+
if (p.handler === 'built-in') {
|
|
254
|
+
phaseMap.set(p.name, { handler: 'built-in' });
|
|
255
|
+
}
|
|
256
|
+
else if (p.parallel) {
|
|
257
|
+
phaseMap.set(p.name, { parallel: [...p.parallel] });
|
|
258
|
+
}
|
|
259
|
+
else if (p.sequential) {
|
|
260
|
+
phaseMap.set(p.name, { sequential: [...p.sequential] });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
for (const role of pluginRoles) {
|
|
264
|
+
const agent = this.agents.get(role);
|
|
265
|
+
if (!agent)
|
|
266
|
+
continue;
|
|
267
|
+
const plugin = agent;
|
|
268
|
+
if (!plugin.phase)
|
|
269
|
+
continue;
|
|
270
|
+
const target = phaseMap.get(plugin.phase);
|
|
271
|
+
if (!target) {
|
|
272
|
+
logger.warn(`Plugin '${role}' targets phase '${plugin.phase}' which does not exist in workflow — skipped`);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (target.handler === 'built-in') {
|
|
276
|
+
logger.warn(`Plugin '${role}' targets built-in phase '${plugin.phase}' — not supported, skipped`);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const pluginRole = role;
|
|
280
|
+
if (plugin.runAfter && plugin.runAfter.length > 0) {
|
|
281
|
+
// Validate that runAfter dependencies are either in this phase or a prior phase
|
|
282
|
+
const phaseRoles = target.parallel || target.sequential || [];
|
|
283
|
+
const missingDeps = plugin.runAfter.filter((dep) => !phaseRoles.includes(dep) && !this.agents.has(dep));
|
|
284
|
+
if (missingDeps.length > 0) {
|
|
285
|
+
logger.warn(`Plugin '${role}' has unresolved runAfter deps [${missingDeps.join(', ')}] — injecting anyway`);
|
|
286
|
+
}
|
|
287
|
+
// Plugin has dependencies — must run sequentially
|
|
288
|
+
if (target.sequential) {
|
|
289
|
+
target.sequential.push(pluginRole);
|
|
290
|
+
}
|
|
291
|
+
else if (target.parallel) {
|
|
292
|
+
// Convert to sequential to respect dependency ordering
|
|
293
|
+
target.sequential = [...target.parallel, pluginRole];
|
|
294
|
+
delete target.parallel;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
if (target.parallel) {
|
|
299
|
+
target.parallel.push(pluginRole);
|
|
300
|
+
}
|
|
301
|
+
else if (target.sequential) {
|
|
302
|
+
target.sequential.push(pluginRole);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
logger.info(`Plugin '${role}' injected into phase '${plugin.phase}'`);
|
|
306
|
+
}
|
|
307
|
+
// Rebuild WorkflowPhase array
|
|
308
|
+
return ordered.map((name) => {
|
|
309
|
+
const entry = phaseMap.get(name);
|
|
310
|
+
if (entry.handler === 'built-in')
|
|
311
|
+
return { name, handler: 'built-in' };
|
|
312
|
+
if (entry.parallel)
|
|
313
|
+
return { name, parallel: entry.parallel };
|
|
314
|
+
if (entry.sequential)
|
|
315
|
+
return { name, sequential: entry.sequential };
|
|
316
|
+
throw new Error(`Phase '${name}' has no handler, parallel, or sequential agents after plugin injection`);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
161
319
|
checkPhaseResults(phaseName, results, ctx) {
|
|
162
320
|
const allFailed = results.length > 0 && results.every((r) => r.status === 'failed');
|
|
163
321
|
if (allFailed) {
|
|
@@ -5,9 +5,29 @@
|
|
|
5
5
|
* instantiation and prevents usage stats fragmentation.
|
|
6
6
|
*/
|
|
7
7
|
import { LLMProviderFactory } from '../provider_factory.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
import { BaseProvider } from '../base_provider.js';
|
|
9
|
+
import { ModelRouter } from '../model_router.js';
|
|
10
|
+
export async function getCrewProvider(providerOverride, budgetUSD, opts) {
|
|
11
|
+
let effectiveOverride = providerOverride;
|
|
12
|
+
// Apply model routing if configured and agent role is provided
|
|
13
|
+
if (opts?.agentRole && opts?.modelRoutingProviderType) {
|
|
14
|
+
const router = new ModelRouter(opts.modelRoutingProviderType, opts.modelRoutingOverrides);
|
|
15
|
+
const model = router.getModel(opts.agentRole);
|
|
16
|
+
if (model) {
|
|
17
|
+
// Override uses provider:model format (e.g., "anthropic:claude-haiku-4-5-20251001")
|
|
18
|
+
effectiveOverride = `${opts.modelRoutingProviderType}:${model}`;
|
|
19
|
+
}
|
|
11
20
|
}
|
|
12
|
-
|
|
21
|
+
const provider = effectiveOverride
|
|
22
|
+
? await LLMProviderFactory.createFromString(effectiveOverride)
|
|
23
|
+
: await LLMProviderFactory.createFromEnv();
|
|
24
|
+
if (provider instanceof BaseProvider) {
|
|
25
|
+
if (opts?.budgetLedger) {
|
|
26
|
+
provider.setBudgetLedger(opts.budgetLedger);
|
|
27
|
+
}
|
|
28
|
+
else if (budgetUSD !== undefined) {
|
|
29
|
+
provider.setBudget(budgetUSD);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return provider;
|
|
13
33
|
}
|