@wundam/orchex 1.0.0-rc.2 → 1.0.0-rc.21
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 +59 -18
- package/dist/cloud-executor.d.ts +71 -0
- package/dist/cloud-executor.js +335 -0
- package/dist/cloud-sync.d.ts +8 -0
- package/dist/cloud-sync.js +52 -0
- package/dist/config.d.ts +30 -4
- package/dist/config.js +61 -2
- package/dist/context-builder.d.ts +2 -0
- package/dist/context-builder.js +11 -3
- package/dist/cost.js +1 -1
- package/dist/entitlements/jwt.d.ts +7 -0
- package/dist/entitlements/jwt.js +78 -0
- package/dist/entitlements/resolve.d.ts +17 -0
- package/dist/entitlements/resolve.js +49 -0
- package/dist/entitlements/types.d.ts +21 -0
- package/dist/entitlements/types.js +4 -0
- package/dist/executors/base.d.ts +1 -1
- package/dist/executors/bedrock-executor.d.ts +39 -0
- package/dist/executors/bedrock-executor.js +197 -0
- package/dist/executors/index.d.ts +1 -0
- package/dist/executors/index.js +24 -1
- package/dist/index.js +468 -23
- package/dist/intelligence/index.d.ts +44 -0
- package/dist/intelligence/index.js +160 -0
- package/dist/key-cache.d.ts +31 -0
- package/dist/key-cache.js +84 -0
- package/dist/login-helpers.d.ts +25 -0
- package/dist/login-helpers.js +54 -0
- package/dist/manifest.js +18 -1
- package/dist/mcp-instructions.d.ts +1 -0
- package/dist/mcp-instructions.js +84 -0
- package/dist/mcp-resources.d.ts +8 -0
- package/dist/mcp-resources.js +420 -0
- package/dist/model-cache.d.ts +18 -0
- package/dist/model-cache.js +62 -0
- package/dist/model-validator.d.ts +20 -0
- package/dist/model-validator.js +125 -0
- package/dist/orchestrator.d.ts +14 -0
- package/dist/orchestrator.js +191 -32
- package/dist/setup/ide-registry.d.ts +13 -0
- package/dist/setup/ide-registry.js +51 -0
- package/dist/setup/index.d.ts +1 -0
- package/dist/setup/index.js +111 -0
- package/dist/tier-gating.js +0 -16
- package/dist/tiers.d.ts +35 -5
- package/dist/tiers.js +39 -3
- package/dist/tools.d.ts +6 -1
- package/dist/tools.js +852 -95
- package/dist/types.d.ts +71 -60
- package/dist/types.js +3 -0
- package/dist/waves.d.ts +1 -1
- package/dist/waves.js +29 -2
- package/package.json +41 -5
- package/src/entitlements/public-key.pem +9 -0
- package/dist/intelligence/anti-pattern-detector.d.ts +0 -117
- package/dist/intelligence/anti-pattern-detector.js +0 -327
- package/dist/intelligence/budget-enforcer.d.ts +0 -119
- package/dist/intelligence/budget-enforcer.js +0 -226
- package/dist/intelligence/context-optimizer.d.ts +0 -111
- package/dist/intelligence/context-optimizer.js +0 -282
- package/dist/intelligence/cost-tracker.d.ts +0 -114
- package/dist/intelligence/cost-tracker.js +0 -183
- package/dist/intelligence/deliverable-extractor.d.ts +0 -134
- package/dist/intelligence/deliverable-extractor.js +0 -909
- package/dist/intelligence/dependency-inferrer.d.ts +0 -87
- package/dist/intelligence/dependency-inferrer.js +0 -403
- package/dist/intelligence/diagnostics.d.ts +0 -33
- package/dist/intelligence/diagnostics.js +0 -64
- package/dist/intelligence/error-analyzer.d.ts +0 -7
- package/dist/intelligence/error-analyzer.js +0 -76
- package/dist/intelligence/file-chunker.d.ts +0 -15
- package/dist/intelligence/file-chunker.js +0 -64
- package/dist/intelligence/fix-stream-manager.d.ts +0 -59
- package/dist/intelligence/fix-stream-manager.js +0 -212
- package/dist/intelligence/heuristics.d.ts +0 -23
- package/dist/intelligence/heuristics.js +0 -124
- package/dist/intelligence/learning-engine.d.ts +0 -157
- package/dist/intelligence/learning-engine.js +0 -433
- package/dist/intelligence/learning-feedback.d.ts +0 -96
- package/dist/intelligence/learning-feedback.js +0 -202
- package/dist/intelligence/pattern-analyzer.d.ts +0 -35
- package/dist/intelligence/pattern-analyzer.js +0 -189
- package/dist/intelligence/plan-parser.d.ts +0 -124
- package/dist/intelligence/plan-parser.js +0 -498
- package/dist/intelligence/planner.d.ts +0 -29
- package/dist/intelligence/planner.js +0 -86
- package/dist/intelligence/self-healer.d.ts +0 -16
- package/dist/intelligence/self-healer.js +0 -84
- package/dist/intelligence/slicing-metrics.d.ts +0 -62
- package/dist/intelligence/slicing-metrics.js +0 -202
- package/dist/intelligence/slicing-templates.d.ts +0 -81
- package/dist/intelligence/slicing-templates.js +0 -420
- package/dist/intelligence/split-suggester.d.ts +0 -69
- package/dist/intelligence/split-suggester.js +0 -176
- package/dist/intelligence/stream-generator.d.ts +0 -90
- package/dist/intelligence/stream-generator.js +0 -452
- package/dist/telemetry/telemetry-types.d.ts +0 -85
- package/dist/telemetry/telemetry-types.js +0 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { readModelCache, fetchAndCacheModels } from './model-cache.js';
|
|
2
|
+
import { DEFAULT_MODELS, getProviderApiKey } from './config.js';
|
|
3
|
+
import { createLogger } from './logging.js';
|
|
4
|
+
const log = createLogger('model-validator');
|
|
5
|
+
/**
|
|
6
|
+
* Static fallback chains — used only when registry + cache are both unavailable.
|
|
7
|
+
*/
|
|
8
|
+
export const MODEL_FALLBACKS = {
|
|
9
|
+
anthropic: ['claude-sonnet-4-5-20250929', 'claude-3-5-sonnet-20241022', 'claude-3-haiku-20240307'],
|
|
10
|
+
openai: ['gpt-4.1', 'gpt-4o', 'gpt-4o-mini', 'gpt-3.5-turbo'],
|
|
11
|
+
gemini: ['gemini-2.5-pro', 'gemini-2.0-flash', 'gemini-1.5-flash'],
|
|
12
|
+
deepseek: ['deepseek-chat', 'deepseek-coder'],
|
|
13
|
+
bedrock: ['claude-3.5-sonnet', 'claude-3-haiku'],
|
|
14
|
+
ollama: ['llama3.3:70b', 'llama3.2:latest'],
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Query Ollama for installed models (local-only).
|
|
18
|
+
*/
|
|
19
|
+
async function queryOllamaModels() {
|
|
20
|
+
const baseUrl = process.env.OLLAMA_BASE_URL ?? process.env.OLLAMA_HOST ?? 'http://localhost:11434';
|
|
21
|
+
try {
|
|
22
|
+
const resp = await fetch(`${baseUrl}/api/tags`, { signal: AbortSignal.timeout(3000) });
|
|
23
|
+
if (!resp.ok)
|
|
24
|
+
return [];
|
|
25
|
+
const data = await resp.json();
|
|
26
|
+
return data.models.map(m => m.name);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a model to an available one using the registry/cache.
|
|
34
|
+
* Resolution chain: cache → server fetch → static fallback
|
|
35
|
+
*/
|
|
36
|
+
export async function resolveModel(provider, preferredModel, apiUrl) {
|
|
37
|
+
// Special case: Ollama models are local-only, query directly
|
|
38
|
+
if (provider === 'ollama') {
|
|
39
|
+
const ollamaModels = await queryOllamaModels();
|
|
40
|
+
if (ollamaModels.length > 0) {
|
|
41
|
+
if (ollamaModels.includes(preferredModel)) {
|
|
42
|
+
return { originalModel: preferredModel, resolvedModel: preferredModel, wasFallback: false };
|
|
43
|
+
}
|
|
44
|
+
// Find partial match (e.g., "llama3.3" matches "llama3.3:70b")
|
|
45
|
+
const partial = ollamaModels.find(m => m.startsWith(preferredModel) || preferredModel.startsWith(m.split(':')[0]));
|
|
46
|
+
if (partial) {
|
|
47
|
+
return {
|
|
48
|
+
originalModel: preferredModel,
|
|
49
|
+
resolvedModel: partial,
|
|
50
|
+
wasFallback: true,
|
|
51
|
+
reason: `Partial match: ${partial}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
originalModel: preferredModel,
|
|
56
|
+
resolvedModel: ollamaModels[0],
|
|
57
|
+
wasFallback: true,
|
|
58
|
+
reason: `${preferredModel} not installed, using ${ollamaModels[0]}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// Ollama not running — fall through to static fallback
|
|
62
|
+
}
|
|
63
|
+
// 1. Try cache
|
|
64
|
+
let cache = await readModelCache();
|
|
65
|
+
// 2. If cache miss/expired and we have a server URL, fetch fresh
|
|
66
|
+
if (!cache && apiUrl) {
|
|
67
|
+
cache = await fetchAndCacheModels(apiUrl);
|
|
68
|
+
}
|
|
69
|
+
// 3. If we have cached data, check if model exists
|
|
70
|
+
if (cache?.providers[provider]) {
|
|
71
|
+
const providerModels = cache.providers[provider];
|
|
72
|
+
const found = providerModels.some(m => m.modelId === preferredModel);
|
|
73
|
+
if (found) {
|
|
74
|
+
return { originalModel: preferredModel, resolvedModel: preferredModel, wasFallback: false };
|
|
75
|
+
}
|
|
76
|
+
// Model not in registry — find first available from fallback chain
|
|
77
|
+
const fallbacks = MODEL_FALLBACKS[provider] ?? [];
|
|
78
|
+
for (const fb of fallbacks) {
|
|
79
|
+
if (providerModels.some(m => m.modelId === fb)) {
|
|
80
|
+
log.warn({ provider, original: preferredModel, fallback: fb }, 'model_fallback');
|
|
81
|
+
return {
|
|
82
|
+
originalModel: preferredModel,
|
|
83
|
+
resolvedModel: fb,
|
|
84
|
+
wasFallback: true,
|
|
85
|
+
reason: `${preferredModel} not found in ${provider} model registry, using ${fb}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// None of the fallbacks exist in registry either — use first available from registry
|
|
90
|
+
if (providerModels.length > 0) {
|
|
91
|
+
const first = providerModels[0].modelId;
|
|
92
|
+
log.warn({ provider, original: preferredModel, fallback: first }, 'model_fallback_registry_first');
|
|
93
|
+
return {
|
|
94
|
+
originalModel: preferredModel,
|
|
95
|
+
resolvedModel: first,
|
|
96
|
+
wasFallback: true,
|
|
97
|
+
reason: `${preferredModel} not found, using first available: ${first}`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// 4. No cache, no server — use static fallback
|
|
102
|
+
const fallbacks = MODEL_FALLBACKS[provider] ?? [];
|
|
103
|
+
if (fallbacks.includes(preferredModel)) {
|
|
104
|
+
return { originalModel: preferredModel, resolvedModel: preferredModel, wasFallback: false };
|
|
105
|
+
}
|
|
106
|
+
const fallback = fallbacks[0] ?? DEFAULT_MODELS[provider] ?? preferredModel;
|
|
107
|
+
if (fallback !== preferredModel) {
|
|
108
|
+
log.warn({ provider, original: preferredModel, fallback }, 'model_fallback_static');
|
|
109
|
+
return {
|
|
110
|
+
originalModel: preferredModel,
|
|
111
|
+
resolvedModel: fallback,
|
|
112
|
+
wasFallback: true,
|
|
113
|
+
reason: `Offline — ${preferredModel} unverified, using safe default ${fallback}`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return { originalModel: preferredModel, resolvedModel: preferredModel, wasFallback: false };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Check if a provider has a valid API key configured.
|
|
120
|
+
*/
|
|
121
|
+
export function isProviderAvailable(provider) {
|
|
122
|
+
if (provider === 'ollama' || provider === 'bedrock')
|
|
123
|
+
return true; // No key needed / AWS creds
|
|
124
|
+
return !!getProviderApiKey(provider);
|
|
125
|
+
}
|
package/dist/orchestrator.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExecutorStrategy, ExecuteResponse, StreamResult, BudgetViolationType, FileContext } from './types.js';
|
|
2
|
+
import { type ErrorCategory } from './intelligence/index.js';
|
|
2
3
|
/**
|
|
3
4
|
* Extended stream result with context budget metrics (Orchex Learn).
|
|
4
5
|
*/
|
|
@@ -6,6 +7,8 @@ export interface StreamResultWithBudget extends StreamResult {
|
|
|
6
7
|
/** Context budget metrics */
|
|
7
8
|
contextMetrics?: {
|
|
8
9
|
estimatedTokens: number;
|
|
10
|
+
/** Actual input tokens from LLM API response */
|
|
11
|
+
actualTokens?: number;
|
|
9
12
|
budgetUtilization: number;
|
|
10
13
|
violationType: BudgetViolationType;
|
|
11
14
|
budgetLimit: number;
|
|
@@ -28,6 +31,17 @@ export declare function gatherStreamFileContext(projectDir: string, stream: {
|
|
|
28
31
|
owns?: string[];
|
|
29
32
|
reads?: string[];
|
|
30
33
|
}): Promise<FileContext[]>;
|
|
34
|
+
export interface CircuitBreakerResult {
|
|
35
|
+
tripped: boolean;
|
|
36
|
+
category?: ErrorCategory;
|
|
37
|
+
/** True if the tripped category is selfHealable — fix streams should still be generated. */
|
|
38
|
+
selfHealable?: boolean;
|
|
39
|
+
failedCount: number;
|
|
40
|
+
totalCount: number;
|
|
41
|
+
message?: string;
|
|
42
|
+
suggestion?: string;
|
|
43
|
+
}
|
|
44
|
+
export declare function checkWaveCircuitBreaker(waveResults: StreamResult[], threshold?: number): CircuitBreakerResult;
|
|
31
45
|
/**
|
|
32
46
|
* Execute a single wave of the orchestration.
|
|
33
47
|
* Steps: resolve → setup → context → execute → validate → apply → verify → report
|
package/dist/orchestrator.js
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
|
-
import { loadManifest, updateStreamStatus, addStream, recoverStuckStreams, cleanStaleLock } from './manifest.js';
|
|
1
|
+
import { loadManifest, saveManifest, updateStreamStatus, addStream, recoverStuckStreams, cleanStaleLock } from './manifest.js';
|
|
2
2
|
import { createLogger, clearExecutionLog } from './logger.js';
|
|
3
3
|
import { createLogger as createPinoLogger } from './logging.js';
|
|
4
4
|
const log = createPinoLogger('orchestrator');
|
|
5
|
-
import { generateFixStream } from './intelligence/
|
|
6
|
-
import { analyzeError } from './intelligence/error-analyzer.js';
|
|
7
|
-
import { cleanupOrphanFixStreams } from './intelligence/fix-stream-manager.js';
|
|
5
|
+
import { generateFixStream, analyzeError, cleanupOrphanFixStreams, streamResultToTelemetryEvent, appendLocalEvents, runLearningCycle, routeStream, loadRoutingRules, } from './intelligence/index.js';
|
|
8
6
|
import { calculateWaves } from './waves.js';
|
|
9
7
|
import { buildFullPrompt, buildFullPromptOptimized } from './context-builder.js';
|
|
10
|
-
import { applyArtifact, writeArtifact, validateArtifactSanity, createStreamBackup, withStreamIsolation, writeBackup, readAllBackups, deleteBackup, recoverFromIsolationCrash } from './artifacts.js';
|
|
8
|
+
import { applyArtifact, writeArtifact, validateArtifactSanity, createStreamBackup, revertStreamBackup, withStreamIsolation, writeBackup, readAllBackups, deleteBackup, recoverFromIsolationCrash } from './artifacts.js';
|
|
11
9
|
import { runCommands } from './commands.js';
|
|
12
10
|
import * as fs from 'fs/promises';
|
|
13
11
|
import * as path from 'path';
|
|
14
|
-
import { loadConfig, getConfiguredModel } from './config.js';
|
|
15
|
-
import {
|
|
12
|
+
import { loadConfig, getConfiguredModel, DEFAULT_MODELS } from './config.js';
|
|
13
|
+
import { getEffectiveTier, getEffectiveLimits } from './tiers.js';
|
|
16
14
|
import { broadcaster } from './execution-broadcaster.js';
|
|
17
|
-
import { streamResultToTelemetryEvent, appendLocalEvents, runLearningCycle, } from './intelligence/learning-engine.js';
|
|
18
15
|
const DEFAULT_MAX_TOKENS = 16384;
|
|
19
16
|
const LOCK_FILE = '.orchex/active/.lock';
|
|
20
17
|
/**
|
|
@@ -44,6 +41,35 @@ export async function gatherStreamFileContext(projectDir, stream) {
|
|
|
44
41
|
}
|
|
45
42
|
return context;
|
|
46
43
|
}
|
|
44
|
+
export function checkWaveCircuitBreaker(waveResults, threshold = 0.75) {
|
|
45
|
+
const totalCount = waveResults.length;
|
|
46
|
+
const failed = waveResults.filter(r => r.status === 'failed' && r.errorDetail);
|
|
47
|
+
const failedCount = failed.length;
|
|
48
|
+
if (failedCount === 0 || totalCount === 0) {
|
|
49
|
+
return { tripped: false, failedCount: 0, totalCount };
|
|
50
|
+
}
|
|
51
|
+
const ratio = failedCount / totalCount;
|
|
52
|
+
if (ratio < threshold) {
|
|
53
|
+
return { tripped: false, failedCount, totalCount };
|
|
54
|
+
}
|
|
55
|
+
const categories = new Set(failed.map(r => r.errorDetail.category));
|
|
56
|
+
if (categories.size !== 1) {
|
|
57
|
+
return { tripped: false, failedCount, totalCount };
|
|
58
|
+
}
|
|
59
|
+
const category = [...categories][0];
|
|
60
|
+
const suggestion = failed[0].errorDetail.suggestion;
|
|
61
|
+
// selfHealable is true only when ALL failed streams agree (any false → false)
|
|
62
|
+
const selfHealable = failed.every(r => r.errorDetail.selfHealable);
|
|
63
|
+
return {
|
|
64
|
+
tripped: true,
|
|
65
|
+
category,
|
|
66
|
+
selfHealable,
|
|
67
|
+
failedCount,
|
|
68
|
+
totalCount,
|
|
69
|
+
message: `${failedCount}/${totalCount} streams failed with ${category}`,
|
|
70
|
+
suggestion,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
47
73
|
/**
|
|
48
74
|
* Execute a single wave of the orchestration.
|
|
49
75
|
* Steps: resolve → setup → context → execute → validate → apply → verify → report
|
|
@@ -103,7 +129,7 @@ async function executeWaveInternal(projectDir, executor, options, logger) {
|
|
|
103
129
|
}
|
|
104
130
|
// 1. RESOLVE — load manifest, calculate waves, find target
|
|
105
131
|
const manifest = await loadManifest(projectDir);
|
|
106
|
-
const { waves, errors, blocked } = calculateWaves(manifest);
|
|
132
|
+
const { waves, errors, blocked } = calculateWaves(manifest, projectDir);
|
|
107
133
|
// Only fail when there are truly no runnable waves.
|
|
108
134
|
// Blocked stream messages are informational — other streams may still be runnable
|
|
109
135
|
// (e.g., fix streams whose deps are satisfied while the original's dependents are blocked).
|
|
@@ -145,7 +171,16 @@ async function executeWaveInternal(projectDir, executor, options, logger) {
|
|
|
145
171
|
};
|
|
146
172
|
}
|
|
147
173
|
const totalWaves = waves.length;
|
|
148
|
-
const
|
|
174
|
+
const baseModel = options.model ?? DEFAULT_MODELS[executor.provider] ?? getConfiguredModel(config);
|
|
175
|
+
// Validate base model before execution
|
|
176
|
+
const { resolveModel: resolveBaseModel } = await import('./model-validator.js');
|
|
177
|
+
const { resolveApiUrl: resolveBaseApiUrl } = await import('./config.js');
|
|
178
|
+
const baseApiUrl = config.mode === 'cloud' && config.apiKey ? await resolveBaseApiUrl() : undefined;
|
|
179
|
+
const baseValidation = await resolveBaseModel(executor.provider, baseModel, baseApiUrl);
|
|
180
|
+
const model = baseValidation.resolvedModel;
|
|
181
|
+
if (baseValidation.wasFallback) {
|
|
182
|
+
log.warn({ original: baseValidation.originalModel, resolved: model, reason: baseValidation.reason }, 'wave_model_fallback');
|
|
183
|
+
}
|
|
149
184
|
const maxTokens = options.maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
150
185
|
const waveLog = log.child({ orchestrationId: manifest.feature, waveNumber: targetWave.number });
|
|
151
186
|
const streamResults = [];
|
|
@@ -174,14 +209,14 @@ async function executeWaveInternal(projectDir, executor, options, logger) {
|
|
|
174
209
|
const failed = results.find((r) => !r.success);
|
|
175
210
|
if (failed) {
|
|
176
211
|
const setupError = `Setup command failed: ${failed.command}`;
|
|
177
|
-
const setupAnalysis = analyzeError(failed.error ?? failed.stderr ?? setupError);
|
|
212
|
+
const setupAnalysis = analyzeError(failed.error ?? failed.stderr ?? setupError, 'setup');
|
|
178
213
|
await updateStreamStatus(projectDir, stream.id, 'failed', `Setup failed: ${failed.error ?? failed.stderr}`);
|
|
179
214
|
streamResults.push({
|
|
180
215
|
id: stream.id,
|
|
181
216
|
name: stream.name,
|
|
182
217
|
status: 'failed',
|
|
183
218
|
error: setupError,
|
|
184
|
-
errorDetail: { category: setupAnalysis.category, retryable: setupAnalysis.retryable, suggestion: setupAnalysis.suggestion },
|
|
219
|
+
errorDetail: { category: setupAnalysis.category, retryable: setupAnalysis.retryable, selfHealable: setupAnalysis.selfHealable, suggestion: setupAnalysis.suggestion },
|
|
185
220
|
});
|
|
186
221
|
broadcaster.broadcast(manifest.feature, {
|
|
187
222
|
type: 'stream_failed',
|
|
@@ -237,6 +272,38 @@ async function executeWaveInternal(projectDir, executor, options, logger) {
|
|
|
237
272
|
// Track wave execution timing
|
|
238
273
|
const waveStartTime = Date.now();
|
|
239
274
|
const waveStartedAt = new Date().toISOString();
|
|
275
|
+
// Smart routing — opt-in via ~/.orchex/config.json routing rules
|
|
276
|
+
const routingRules = loadRoutingRules();
|
|
277
|
+
const routeDecisions = new Map();
|
|
278
|
+
if (routingRules.defaultProvider || routingRules.overrides) {
|
|
279
|
+
for (const stream of activeStreams) {
|
|
280
|
+
const streamDef = updatedManifest.streams[stream.id];
|
|
281
|
+
const decision = routeStream({ id: stream.id, name: stream.name, owns: stream.owns ?? [], reads: stream.reads ?? [], plan: streamDef?.plan }, routingRules);
|
|
282
|
+
routeDecisions.set(stream.id, decision);
|
|
283
|
+
if (decision.provider !== executor.provider) {
|
|
284
|
+
log.info({ streamId: stream.id, provider: decision.provider, model: decision.model, reasoning: decision.reasoning }, 'smart_route_applied');
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Validate and resolve models before execution
|
|
289
|
+
const { resolveModel } = await import('./model-validator.js');
|
|
290
|
+
const { resolveApiUrl } = await import('./config.js');
|
|
291
|
+
const apiUrl = config.mode === 'cloud' && config.apiKey ? await resolveApiUrl() : undefined;
|
|
292
|
+
const modelFallbacks = [];
|
|
293
|
+
for (const [streamId, decision] of routeDecisions) {
|
|
294
|
+
const validation = await resolveModel(decision.provider, decision.model, apiUrl);
|
|
295
|
+
if (validation.wasFallback) {
|
|
296
|
+
routeDecisions.set(streamId, {
|
|
297
|
+
...decision,
|
|
298
|
+
model: validation.resolvedModel,
|
|
299
|
+
reasoning: `${decision.reasoning} [fallback: ${validation.reason}]`,
|
|
300
|
+
});
|
|
301
|
+
modelFallbacks.push(`${streamId}: ${validation.originalModel} → ${validation.resolvedModel}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (modelFallbacks.length > 0) {
|
|
305
|
+
log.warn({ fallbacks: modelFallbacks }, 'model_fallbacks_applied');
|
|
306
|
+
}
|
|
240
307
|
const executePromises = activeStreams.map(async (stream) => {
|
|
241
308
|
const streamLog = waveLog.child({ streamId: stream.id });
|
|
242
309
|
const streamStartTime = Date.now();
|
|
@@ -247,6 +314,7 @@ async function executeWaveInternal(projectDir, executor, options, logger) {
|
|
|
247
314
|
const useOptimization = options.optimize !== false; // Default to true
|
|
248
315
|
let prompt;
|
|
249
316
|
let structuredPrompt;
|
|
317
|
+
const streamDef = updatedManifest.streams[stream.id];
|
|
250
318
|
if (useOptimization) {
|
|
251
319
|
// Use optimized builder with caching hints and budget checking
|
|
252
320
|
const promptOptions = {
|
|
@@ -254,7 +322,12 @@ async function executeWaveInternal(projectDir, executor, options, logger) {
|
|
|
254
322
|
model,
|
|
255
323
|
// Don't enforce hard limit here - just warn and continue
|
|
256
324
|
enforceHardLimit: false,
|
|
325
|
+
isFixStream: !!streamDef?.parentStreamId,
|
|
257
326
|
};
|
|
327
|
+
// Log model routing suggestion if present
|
|
328
|
+
if (streamDef?.suggestedProvider && !streamDef.provider) {
|
|
329
|
+
streamLog.info({ streamId: stream.id, suggestedProvider: streamDef.suggestedProvider }, 'model_routing_suggestion');
|
|
330
|
+
}
|
|
258
331
|
const optimized = await buildFullPromptOptimized(projectDir, stream.id, updatedManifest, promptOptions);
|
|
259
332
|
prompt = optimized.fullPrompt;
|
|
260
333
|
structuredPrompt = optimized;
|
|
@@ -291,11 +364,14 @@ async function executeWaveInternal(projectDir, executor, options, logger) {
|
|
|
291
364
|
};
|
|
292
365
|
}
|
|
293
366
|
// 4. EXECUTE — call LLM API
|
|
294
|
-
const
|
|
295
|
-
const
|
|
367
|
+
const routeDecision = routeDecisions.get(stream.id);
|
|
368
|
+
const streamModel = routeDecision?.model ?? model;
|
|
369
|
+
const isCloud = (config?.mode === 'cloud') || executor.provider === 'orchex-cloud';
|
|
370
|
+
const defaultTimeoutMs = isCloud ? 600_000 : 120_000; // 10min cloud, 2min local
|
|
371
|
+
const timeoutMs = streamDef.timeoutMs ?? defaultTimeoutMs;
|
|
296
372
|
const request = {
|
|
297
373
|
prompt,
|
|
298
|
-
model,
|
|
374
|
+
model: streamModel,
|
|
299
375
|
maxTokens,
|
|
300
376
|
streamId: stream.id,
|
|
301
377
|
structuredPrompt,
|
|
@@ -327,6 +403,10 @@ async function executeWaveInternal(projectDir, executor, options, logger) {
|
|
|
327
403
|
stopHeartbeat();
|
|
328
404
|
}
|
|
329
405
|
const executionTimeMs = Date.now() - streamStartTime;
|
|
406
|
+
// Enrich budgetCheck with actual token count from API response
|
|
407
|
+
if (budgetCheck && result.tokensUsed?.input) {
|
|
408
|
+
budgetCheck.actualTokens = result.tokensUsed.input;
|
|
409
|
+
}
|
|
330
410
|
if (!result.success || !result.artifact) {
|
|
331
411
|
// Log truncated rawResponse for debugging artifact extraction failures
|
|
332
412
|
const responsePreview = result.rawResponse
|
|
@@ -339,7 +419,7 @@ async function executeWaveInternal(projectDir, executor, options, logger) {
|
|
|
339
419
|
responsePreview,
|
|
340
420
|
});
|
|
341
421
|
const apiError = result.error ?? 'No artifact produced';
|
|
342
|
-
const apiAnalysis = analyzeError(apiError);
|
|
422
|
+
const apiAnalysis = analyzeError(apiError, 'transport');
|
|
343
423
|
broadcaster.broadcast(manifest.feature, {
|
|
344
424
|
type: 'stream_failed',
|
|
345
425
|
orchestrationId: manifest.feature,
|
|
@@ -351,7 +431,7 @@ async function executeWaveInternal(projectDir, executor, options, logger) {
|
|
|
351
431
|
name: stream.name,
|
|
352
432
|
status: 'failed',
|
|
353
433
|
error: apiError,
|
|
354
|
-
errorDetail: { category: apiAnalysis.category, retryable: apiAnalysis.retryable, suggestion: apiAnalysis.suggestion },
|
|
434
|
+
errorDetail: { category: apiAnalysis.category, retryable: apiAnalysis.retryable, selfHealable: apiAnalysis.selfHealable, suggestion: apiAnalysis.suggestion },
|
|
355
435
|
tokensUsed: result.tokensUsed,
|
|
356
436
|
provider: executor.provider,
|
|
357
437
|
model,
|
|
@@ -366,11 +446,23 @@ async function executeWaveInternal(projectDir, executor, options, logger) {
|
|
|
366
446
|
});
|
|
367
447
|
// 5. VALIDATE — artifact is already parsed by executor
|
|
368
448
|
const artifact = result.artifact;
|
|
449
|
+
// Check if stream was skipped by sibling fix completion while we were executing
|
|
450
|
+
const freshManifest = await loadManifest(projectDir);
|
|
451
|
+
const freshStatus = freshManifest.streams[stream.id]?.status;
|
|
452
|
+
if (freshStatus === 'skipped') {
|
|
453
|
+
log.info({ id: stream.id }, 'stream_skipped_by_sibling — discarding result');
|
|
454
|
+
return {
|
|
455
|
+
id: stream.id,
|
|
456
|
+
name: stream.name,
|
|
457
|
+
status: 'skipped',
|
|
458
|
+
summary: 'Skipped — sibling fix already resolved this stream',
|
|
459
|
+
};
|
|
460
|
+
}
|
|
369
461
|
// 5a. SANITY CHECK — reject degenerate outputs before applying
|
|
370
462
|
const sanity = validateArtifactSanity(artifact);
|
|
371
463
|
if (!sanity.passed) {
|
|
372
464
|
const sanityError = `Sanity check failed: ${sanity.violations.join('; ')}`;
|
|
373
|
-
const sanityAnalysis = analyzeError(sanityError);
|
|
465
|
+
const sanityAnalysis = analyzeError(sanityError, 'artifact');
|
|
374
466
|
await logger.error('sanity_check_failed', {
|
|
375
467
|
id: stream.id,
|
|
376
468
|
violations: sanity.violations,
|
|
@@ -380,7 +472,7 @@ async function executeWaveInternal(projectDir, executor, options, logger) {
|
|
|
380
472
|
name: stream.name,
|
|
381
473
|
status: 'failed',
|
|
382
474
|
error: sanityError,
|
|
383
|
-
errorDetail: { category: sanityAnalysis.category, retryable: sanityAnalysis.retryable, suggestion: sanityAnalysis.suggestion },
|
|
475
|
+
errorDetail: { category: sanityAnalysis.category, retryable: sanityAnalysis.retryable, selfHealable: sanityAnalysis.selfHealable, suggestion: sanityAnalysis.suggestion },
|
|
384
476
|
tokensUsed: result.tokensUsed,
|
|
385
477
|
provider: executor.provider,
|
|
386
478
|
model,
|
|
@@ -395,7 +487,7 @@ async function executeWaveInternal(projectDir, executor, options, logger) {
|
|
|
395
487
|
const applyResult = await applyArtifact(projectDir, stream.id, { force: false });
|
|
396
488
|
if (!applyResult.success) {
|
|
397
489
|
const applyError = applyResult.error ?? 'Apply failed';
|
|
398
|
-
const applyAnalysis = analyzeError(applyError);
|
|
490
|
+
const applyAnalysis = analyzeError(applyError, 'artifact');
|
|
399
491
|
await logger.error('stream_failed', {
|
|
400
492
|
id: stream.id,
|
|
401
493
|
phase: 'apply',
|
|
@@ -412,7 +504,7 @@ async function executeWaveInternal(projectDir, executor, options, logger) {
|
|
|
412
504
|
name: stream.name,
|
|
413
505
|
status: 'failed',
|
|
414
506
|
error: applyResult.error,
|
|
415
|
-
errorDetail: { category: applyAnalysis.category, retryable: applyAnalysis.retryable, suggestion: applyAnalysis.suggestion },
|
|
507
|
+
errorDetail: { category: applyAnalysis.category, retryable: applyAnalysis.retryable, selfHealable: applyAnalysis.selfHealable, suggestion: applyAnalysis.suggestion },
|
|
416
508
|
tokensUsed: result.tokensUsed,
|
|
417
509
|
provider: executor.provider,
|
|
418
510
|
model,
|
|
@@ -449,7 +541,7 @@ async function executeWaveInternal(projectDir, executor, options, logger) {
|
|
|
449
541
|
}
|
|
450
542
|
catch (error) {
|
|
451
543
|
const catchError = error.message;
|
|
452
|
-
const catchAnalysis = analyzeError(catchError);
|
|
544
|
+
const catchAnalysis = analyzeError(catchError, 'transport');
|
|
453
545
|
await logger.error('stream_failed', {
|
|
454
546
|
id: stream.id,
|
|
455
547
|
phase: 'execution',
|
|
@@ -466,7 +558,7 @@ async function executeWaveInternal(projectDir, executor, options, logger) {
|
|
|
466
558
|
name: stream.name,
|
|
467
559
|
status: 'failed',
|
|
468
560
|
error: catchError,
|
|
469
|
-
errorDetail: { category: catchAnalysis.category, retryable: catchAnalysis.retryable, suggestion: catchAnalysis.suggestion },
|
|
561
|
+
errorDetail: { category: catchAnalysis.category, retryable: catchAnalysis.retryable, selfHealable: catchAnalysis.selfHealable, suggestion: catchAnalysis.suggestion },
|
|
470
562
|
provider: executor.provider,
|
|
471
563
|
model,
|
|
472
564
|
executionTimeMs: Date.now() - streamStartTime,
|
|
@@ -481,7 +573,7 @@ async function executeWaveInternal(projectDir, executor, options, logger) {
|
|
|
481
573
|
name: activeStreams[i]?.name ?? 'unknown',
|
|
482
574
|
status: 'failed',
|
|
483
575
|
error: err.message ?? String(err),
|
|
484
|
-
errorDetail: { category: 'unknown', retryable: true, suggestion: 'Check orchestrator logs' },
|
|
576
|
+
errorDetail: { category: 'unknown', retryable: true, selfHealable: false, suggestion: 'Check orchestrator logs' },
|
|
485
577
|
tokensUsed: { input: 0, output: 0 },
|
|
486
578
|
})));
|
|
487
579
|
// Promise.allSettled — one failure doesn't kill the wave
|
|
@@ -496,13 +588,13 @@ async function executeWaveInternal(projectDir, executor, options, logger) {
|
|
|
496
588
|
}
|
|
497
589
|
else {
|
|
498
590
|
const rejectedError = result.reason?.message ?? 'Unknown error';
|
|
499
|
-
const rejectedAnalysis = analyzeError(rejectedError);
|
|
591
|
+
const rejectedAnalysis = analyzeError(rejectedError, 'transport');
|
|
500
592
|
streamResults.push({
|
|
501
593
|
id: 'unknown',
|
|
502
594
|
name: 'unknown',
|
|
503
595
|
status: 'failed',
|
|
504
596
|
error: rejectedError,
|
|
505
|
-
errorDetail: { category: rejectedAnalysis.category, retryable: rejectedAnalysis.retryable, suggestion: rejectedAnalysis.suggestion },
|
|
597
|
+
errorDetail: { category: rejectedAnalysis.category, retryable: rejectedAnalysis.retryable, selfHealable: rejectedAnalysis.selfHealable, suggestion: rejectedAnalysis.suggestion },
|
|
506
598
|
});
|
|
507
599
|
}
|
|
508
600
|
}
|
|
@@ -578,7 +670,7 @@ async function executeWaveInternal(projectDir, executor, options, logger) {
|
|
|
578
670
|
.join('\n')
|
|
579
671
|
|| 'unknown error';
|
|
580
672
|
const errorSummary = `Verify failed: ${failed.command}: ${verifyError.slice(0, 500)}`;
|
|
581
|
-
const verifyAnalysis = analyzeError(verifyError);
|
|
673
|
+
const verifyAnalysis = analyzeError(verifyError, 'verify');
|
|
582
674
|
verifyDetails.push(`${sr.id}: ${errorSummary}`);
|
|
583
675
|
await updateStreamStatus(projectDir, sr.id, 'failed', errorSummary);
|
|
584
676
|
await logger.error('verify_failed', {
|
|
@@ -588,9 +680,20 @@ async function executeWaveInternal(projectDir, executor, options, logger) {
|
|
|
588
680
|
error: verifyError.slice(0, 1000),
|
|
589
681
|
isolated: otherBackups.length > 0,
|
|
590
682
|
});
|
|
683
|
+
// Rollback file changes for failed verify — prevents leaving orphan files on disk
|
|
684
|
+
const streamBackup = successBackups.find(b => b.streamId === sr.id);
|
|
685
|
+
if (streamBackup) {
|
|
686
|
+
try {
|
|
687
|
+
await revertStreamBackup(projectDir, streamBackup);
|
|
688
|
+
await logger.info('verify_rollback', { stream: sr.id, files: streamBackup.entries.length + streamBackup.createdFiles.length });
|
|
689
|
+
}
|
|
690
|
+
catch (rollbackErr) {
|
|
691
|
+
await logger.warn('verify_rollback_failed', { stream: sr.id, error: rollbackErr.message });
|
|
692
|
+
}
|
|
693
|
+
}
|
|
591
694
|
sr.status = 'failed';
|
|
592
695
|
sr.error = errorSummary;
|
|
593
|
-
sr.errorDetail = { category: verifyAnalysis.category, retryable: verifyAnalysis.retryable, suggestion: verifyAnalysis.suggestion };
|
|
696
|
+
sr.errorDetail = { category: verifyAnalysis.category, retryable: verifyAnalysis.retryable, selfHealable: verifyAnalysis.selfHealable, suggestion: verifyAnalysis.suggestion };
|
|
594
697
|
}
|
|
595
698
|
else {
|
|
596
699
|
verifyPassed++;
|
|
@@ -621,15 +724,41 @@ async function executeWaveInternal(projectDir, executor, options, logger) {
|
|
|
621
724
|
});
|
|
622
725
|
}
|
|
623
726
|
}
|
|
727
|
+
// 7a. CIRCUIT BREAKER — detect systemic wave-level failures
|
|
728
|
+
const circuitBreaker = checkWaveCircuitBreaker(streamResults);
|
|
729
|
+
if (circuitBreaker.tripped) {
|
|
730
|
+
const firstFailedError = streamResults.find(sr => sr.status === 'failed')?.error;
|
|
731
|
+
log.warn({
|
|
732
|
+
category: circuitBreaker.category,
|
|
733
|
+
failedCount: circuitBreaker.failedCount,
|
|
734
|
+
totalCount: circuitBreaker.totalCount,
|
|
735
|
+
firstError: firstFailedError?.slice(0, 500),
|
|
736
|
+
}, 'circuit_breaker_tripped');
|
|
737
|
+
const cbManifest = await loadManifest(projectDir);
|
|
738
|
+
cbManifest.status = 'circuit_breaker';
|
|
739
|
+
await saveManifest(projectDir, cbManifest);
|
|
740
|
+
}
|
|
624
741
|
// 7a. SELF-HEAL — generate fix streams for failed streams (tier-gated)
|
|
625
742
|
const fixStreamsGenerated = [];
|
|
626
743
|
const currentManifest = await loadManifest(projectDir);
|
|
627
|
-
const
|
|
628
|
-
|
|
744
|
+
const effectiveTierId = config.mode === 'cloud' && config.apiKey
|
|
745
|
+
? getEffectiveTier({
|
|
746
|
+
tier: config.tier ?? 'free',
|
|
747
|
+
createdAt: config.accountCreatedAt ?? new Date().toISOString(),
|
|
748
|
+
trialRunsRemaining: config.trialRunsRemaining ?? 0,
|
|
749
|
+
})
|
|
750
|
+
: (config.tier ?? 'free');
|
|
751
|
+
const tier = getEffectiveLimits(effectiveTierId, null);
|
|
752
|
+
// Circuit breaker blocks self-healing only for non-selfHealable categories (infra failures).
|
|
753
|
+
// When all streams fail with a selfHealable category (code errors), fix streams are still useful.
|
|
754
|
+
const cbBlocksHealing = circuitBreaker.tripped && !(circuitBreaker.selfHealable ?? false);
|
|
755
|
+
if (tier.selfHealing === 'full' && !cbBlocksHealing) {
|
|
629
756
|
for (const sr of streamResults) {
|
|
630
757
|
if (sr.status !== 'failed' || sr.id === 'unknown')
|
|
631
758
|
continue;
|
|
632
|
-
|
|
759
|
+
// Pass the in-memory errorDetail (computed with origin hint) so generateFixStream
|
|
760
|
+
// can use the correct selfHealable flag rather than re-analyzing without origin.
|
|
761
|
+
const fixResult = generateFixStream(currentManifest, sr.id, sr.errorDetail);
|
|
633
762
|
if (fixResult) {
|
|
634
763
|
await addStream(projectDir, fixResult.fixStreamId, fixResult.fixStream);
|
|
635
764
|
fixStreamsGenerated.push(fixResult.fixStreamId);
|
|
@@ -662,7 +791,7 @@ async function executeWaveInternal(projectDir, executor, options, logger) {
|
|
|
662
791
|
}
|
|
663
792
|
// 8. REPORT — check for remaining waves
|
|
664
793
|
const finalManifest = await loadManifest(projectDir);
|
|
665
|
-
const { waves: remainingWaves } = calculateWaves(finalManifest);
|
|
794
|
+
const { waves: remainingWaves } = calculateWaves(finalManifest, projectDir);
|
|
666
795
|
const done = remainingWaves.length === 0;
|
|
667
796
|
const hasCompleted = streamResults.some((s) => s.status === 'complete');
|
|
668
797
|
const completedCount = streamResults.filter((s) => s.status === 'complete').length;
|
|
@@ -694,6 +823,36 @@ async function executeWaveInternal(projectDir, executor, options, logger) {
|
|
|
694
823
|
// Learning is best-effort
|
|
695
824
|
log.warn({ err }, 'learning_cycle_failed');
|
|
696
825
|
}
|
|
826
|
+
// 8c. PATTERN DETECTION (L2) — analyze reports every 5 runs
|
|
827
|
+
try {
|
|
828
|
+
const { detectPatterns, savePatterns, adaptReport } = await import('./intelligence/index.js');
|
|
829
|
+
const reportsDir = path.join(projectDir, '.orchex', 'reports');
|
|
830
|
+
const reportFiles = await fs.readdir(reportsDir).catch(() => []);
|
|
831
|
+
if (reportFiles.length > 0 && reportFiles.length % 5 === 0) {
|
|
832
|
+
const reports = [];
|
|
833
|
+
for (const file of reportFiles) {
|
|
834
|
+
if (!file.endsWith('.json'))
|
|
835
|
+
continue;
|
|
836
|
+
try {
|
|
837
|
+
const content = await fs.readFile(path.join(reportsDir, file), 'utf-8');
|
|
838
|
+
const canonical = JSON.parse(content);
|
|
839
|
+
reports.push(adaptReport(canonical));
|
|
840
|
+
}
|
|
841
|
+
catch { /* skip invalid */ }
|
|
842
|
+
}
|
|
843
|
+
if (reports.length >= 5) {
|
|
844
|
+
const patterns = detectPatterns(reports);
|
|
845
|
+
if (patterns.length > 0) {
|
|
846
|
+
await savePatterns(projectDir, patterns);
|
|
847
|
+
log.info({ patternCount: patterns.length }, 'patterns_detected');
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
catch (err) {
|
|
853
|
+
// Non-fatal — don't fail execution for pattern detection errors
|
|
854
|
+
log.warn({ err }, 'pattern_detection_error');
|
|
855
|
+
}
|
|
697
856
|
}
|
|
698
857
|
// Clean up disk backups after wave completion (all verify/isolation done)
|
|
699
858
|
for (const backup of successBackups) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface IdeConfig {
|
|
2
|
+
name: string;
|
|
3
|
+
slug: string;
|
|
4
|
+
detect: (projectDir: string) => boolean;
|
|
5
|
+
configPath: (projectDir: string) => string;
|
|
6
|
+
scope: 'project' | 'global';
|
|
7
|
+
}
|
|
8
|
+
export declare const IDE_REGISTRY: IdeConfig[];
|
|
9
|
+
export declare function detectIdes(projectDir: string): IdeConfig[];
|
|
10
|
+
export declare function findBySlug(slug: string): IdeConfig | undefined;
|
|
11
|
+
export declare function FALLBACK_CONFIG_PATH(projectDir: string): string;
|
|
12
|
+
export declare function generateMcpConfig(): Record<string, unknown>;
|
|
13
|
+
export declare function mergeConfig(existing: Record<string, unknown>, orchexEntry: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
export const IDE_REGISTRY = [
|
|
5
|
+
{
|
|
6
|
+
name: 'Cursor',
|
|
7
|
+
slug: 'cursor',
|
|
8
|
+
detect: (projectDir) => {
|
|
9
|
+
return Boolean(process.env.CURSOR_TRACE_DIR) || fs.existsSync(path.join(projectDir, '.cursor'));
|
|
10
|
+
},
|
|
11
|
+
configPath: (projectDir) => path.join(projectDir, '.cursor/mcp.json'),
|
|
12
|
+
scope: 'project'
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'Windsurf',
|
|
16
|
+
slug: 'windsurf',
|
|
17
|
+
detect: (projectDir) => {
|
|
18
|
+
return Boolean(process.env.WINDSURF_SESSION_ID) || fs.existsSync(path.join(os.homedir(), '.codeium'));
|
|
19
|
+
},
|
|
20
|
+
configPath: (projectDir) => path.join(os.homedir(), '.codeium/windsurf/mcp_config.json'),
|
|
21
|
+
scope: 'global'
|
|
22
|
+
}
|
|
23
|
+
];
|
|
24
|
+
export function detectIdes(projectDir) {
|
|
25
|
+
return IDE_REGISTRY.filter(ide => ide.detect(projectDir));
|
|
26
|
+
}
|
|
27
|
+
export function findBySlug(slug) {
|
|
28
|
+
return IDE_REGISTRY.find(ide => ide.slug === slug);
|
|
29
|
+
}
|
|
30
|
+
export function FALLBACK_CONFIG_PATH(projectDir) {
|
|
31
|
+
return path.join(projectDir, '.mcp.json');
|
|
32
|
+
}
|
|
33
|
+
export function generateMcpConfig() {
|
|
34
|
+
return {
|
|
35
|
+
mcpServers: {
|
|
36
|
+
orchex: {
|
|
37
|
+
command: 'npx',
|
|
38
|
+
args: ['-y', '@wundam/orchex']
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function mergeConfig(existing, orchexEntry) {
|
|
44
|
+
return {
|
|
45
|
+
...existing,
|
|
46
|
+
mcpServers: {
|
|
47
|
+
...(existing.mcpServers || {}),
|
|
48
|
+
orchex: orchexEntry
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function handleSetupCommand(args: string[], projectDir?: string): Promise<void>;
|