@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
package/dist/tools.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { StreamDefinitionSchema } from './types.js';
|
|
3
|
-
import { analyzeError } from './intelligence/
|
|
3
|
+
import { analyzeError, evaluateCompletion, DEFAULT_THRESHOLDS, categorizeStream, getRecommendedLimit, getThresholds, createDetector, generateAllSuggestions, getSuggestionsSummary, parsePlanDocument, getSectionsAtLevel, isUnpopulatedTemplate, createDiagnostics, detectOwnershipConflicts, autoMergeOwnershipConflicts, extractDeliverables, processDeliverables, formatDeliverablesReport, buildDependencyGraph, formatDependencyReport, generateStreams, formatStreamsForReview, toInitFormat, extractPrerequisites, } from './intelligence/index.js';
|
|
4
4
|
import { initOrchestration, loadManifest, saveManifest, updateStreamStatus, addStream, archiveOrchestration, manifestExists, recoverStuckStreams, } from './manifest.js';
|
|
5
5
|
import { createLogger } from './logger.js';
|
|
6
6
|
import { createLogger as createPinoLogger } from './logging.js';
|
|
@@ -11,19 +11,11 @@ import { createExecutor, getProviderStatus } from './executors/index.js';
|
|
|
11
11
|
import { broadcaster } from './execution-broadcaster.js';
|
|
12
12
|
import { calculateRunCost } from './cost.js';
|
|
13
13
|
import { circuitBreakers } from './executors/circuit-breaker.js';
|
|
14
|
-
import { getTier } from './tiers.js';
|
|
14
|
+
import { getTier, getEffectiveLimits, TRIAL_WINDOW_DAYS } from './tiers.js';
|
|
15
15
|
import { checkTierLimits, getWaveCountWarning } from './tier-gating.js';
|
|
16
|
-
import { loadConfig, validateCloudConfig, getConfiguredProvider } from './config.js';
|
|
17
|
-
import { DEFAULT_THRESHOLDS, categorizeStream, getRecommendedLimit, getThresholds, } from './intelligence/learning-engine.js';
|
|
18
|
-
import { createDetector, } from './intelligence/anti-pattern-detector.js';
|
|
19
|
-
import { generateAllSuggestions, getSuggestionsSummary, } from './intelligence/split-suggester.js';
|
|
20
|
-
// Phase 8B-B: Plan-to-Streams imports
|
|
21
|
-
import { parsePlanDocument, getSectionsAtLevel, isUnpopulatedTemplate } from './intelligence/plan-parser.js';
|
|
22
|
-
import { createDiagnostics, detectOwnershipConflicts } from './intelligence/diagnostics.js';
|
|
23
|
-
import { extractDeliverables, processDeliverables, formatDeliverablesReport, } from './intelligence/deliverable-extractor.js';
|
|
24
|
-
import { buildDependencyGraph, formatDependencyReport, } from './intelligence/dependency-inferrer.js';
|
|
25
|
-
import { generateStreams, formatStreamsForReview, toInitFormat, extractPrerequisites, } from './intelligence/stream-generator.js';
|
|
16
|
+
import { loadConfig, validateCloudConfig, validateCloudProvider, getConfiguredProvider, getProviderApiKey } from './config.js';
|
|
26
17
|
import * as fs from 'fs/promises';
|
|
18
|
+
import * as path from 'path';
|
|
27
19
|
/**
|
|
28
20
|
* Analyze stream definitions for potential budget issues.
|
|
29
21
|
* Uses learned thresholds (LE.3) to warn about streams that may exceed limits.
|
|
@@ -161,24 +153,57 @@ async function withToolLogging(toolName, params, fn) {
|
|
|
161
153
|
return fail(`Tool "${toolName}" failed: ${err.message ?? String(err)}`, { code: 'TOOL_INTERNAL_ERROR', retryable: true, suggestion: 'Retry the operation or check execution logs.' });
|
|
162
154
|
}
|
|
163
155
|
}
|
|
156
|
+
/** Resolve promotion-aware tier limits for the current user. */
|
|
157
|
+
async function resolveUserTier(context) {
|
|
158
|
+
// Cloud mode: server is authoritative, use existing promotion-based resolution
|
|
159
|
+
if (context?.promotionStore) {
|
|
160
|
+
const tierId = context.userTier ?? 'free';
|
|
161
|
+
const promotion = await context.promotionStore.getActiveByUserId(context.userId);
|
|
162
|
+
return getEffectiveLimits(tierId, promotion);
|
|
163
|
+
}
|
|
164
|
+
// Local mode: load config for tier info when no ToolContext
|
|
165
|
+
let configTier = context?.userTier ?? 'free';
|
|
166
|
+
let configCreatedAt = context?.userCreatedAt;
|
|
167
|
+
let configTrialRuns = 0;
|
|
168
|
+
if (!context) {
|
|
169
|
+
try {
|
|
170
|
+
const { loadConfig } = await import('./config.js');
|
|
171
|
+
const config = await loadConfig();
|
|
172
|
+
configTier = config.tier ?? 'free';
|
|
173
|
+
configCreatedAt = config.accountCreatedAt;
|
|
174
|
+
configTrialRuns = config.trialRunsRemaining ?? 0;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// Config load failed — fall through with defaults
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Try JWT entitlement first, fall back to config tier
|
|
181
|
+
try {
|
|
182
|
+
const { resolveEntitledTier, loadPublicKey } = await import('./entitlements/resolve.js');
|
|
183
|
+
const publicKey = await loadPublicKey();
|
|
184
|
+
return resolveEntitledTier({
|
|
185
|
+
tier: configTier,
|
|
186
|
+
accountCreatedAt: configCreatedAt,
|
|
187
|
+
trialRunsRemaining: configTrialRuns,
|
|
188
|
+
}, publicKey);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return getTier(configTier);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
164
194
|
const FREE_LEARN_LIMIT = 3;
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
// Paid tiers with smartPlanning enabled — always allowed
|
|
195
|
+
async function checkLearnAccess(ctx) {
|
|
196
|
+
const tier = await resolveUserTier(ctx);
|
|
197
|
+
// Paid tiers (including trial users who resolve to 'pro') — always allowed
|
|
169
198
|
if (tier.smartPlanning === 'full') {
|
|
170
199
|
return { allowed: true };
|
|
171
200
|
}
|
|
172
|
-
// Free tier (
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
suggestion: 'Upgrade to Pro ($19/mo) for unlimited learn calls: /pricing',
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
return { allowed: true };
|
|
201
|
+
// Free tier (post-trial or never-trial) — not allowed
|
|
202
|
+
return {
|
|
203
|
+
allowed: false,
|
|
204
|
+
error: 'Learn requires a Pro or higher plan.',
|
|
205
|
+
suggestion: 'Upgrade to Pro ($19/mo) for learn and smart planning: /pricing',
|
|
206
|
+
};
|
|
182
207
|
}
|
|
183
208
|
// ============================================================================
|
|
184
209
|
// Plan Template Generator
|
|
@@ -313,6 +338,7 @@ export function registerTools(server, executor, context) {
|
|
|
313
338
|
server.registerTool('init', {
|
|
314
339
|
title: 'Initialize Orchestration',
|
|
315
340
|
description: 'Initialize a new orchestration for a feature. Creates .orchex/active/manifest.yaml with streams.',
|
|
341
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
316
342
|
inputSchema: {
|
|
317
343
|
feature: z.string().describe('Feature name (e.g., "user-authentication")'),
|
|
318
344
|
streams: z
|
|
@@ -324,7 +350,7 @@ export function registerTools(server, executor, context) {
|
|
|
324
350
|
return withToolLogging('init', { feature, streamCount: Object.keys(streams).length }, async () => {
|
|
325
351
|
const projectDir = resolveProjectDir(project_dir);
|
|
326
352
|
// Use cloud tier if available, otherwise default to 'free' (local MCP mode)
|
|
327
|
-
const tier =
|
|
353
|
+
const tier = await resolveUserTier(context);
|
|
328
354
|
// Enforce tier limits (waves, providers, etc) for this orchestration
|
|
329
355
|
const tierLimitResult = checkTierLimits({
|
|
330
356
|
tier,
|
|
@@ -404,6 +430,7 @@ export function registerTools(server, executor, context) {
|
|
|
404
430
|
server.registerTool('add_stream', {
|
|
405
431
|
title: 'Add Stream',
|
|
406
432
|
description: 'Add a new stream to the active orchestration.',
|
|
433
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
407
434
|
inputSchema: {
|
|
408
435
|
id: z.string().describe('Unique stream ID'),
|
|
409
436
|
name: z.string().describe('Human-readable stream name'),
|
|
@@ -471,6 +498,7 @@ export function registerTools(server, executor, context) {
|
|
|
471
498
|
server.registerTool('status', {
|
|
472
499
|
title: 'Orchestration Status',
|
|
473
500
|
description: 'Get the current orchestration status and progress.',
|
|
501
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
474
502
|
inputSchema: {
|
|
475
503
|
project_dir: z.string().optional().describe('Project directory'),
|
|
476
504
|
},
|
|
@@ -478,6 +506,33 @@ export function registerTools(server, executor, context) {
|
|
|
478
506
|
return withToolLogging('status', {}, async () => {
|
|
479
507
|
const projectDir = resolveProjectDir(project_dir);
|
|
480
508
|
if (!(await manifestExists(projectDir))) {
|
|
509
|
+
// Check if auto-plan generation is in progress
|
|
510
|
+
try {
|
|
511
|
+
const autoPlanPath = path.join(projectDir, '.orchex', 'active', 'auto-plan-result.json');
|
|
512
|
+
const raw = await fs.readFile(autoPlanPath, 'utf-8');
|
|
513
|
+
const autoPlanResult = JSON.parse(raw);
|
|
514
|
+
if (autoPlanResult.status === 'generating') {
|
|
515
|
+
return ok('Auto-plan generation in progress.', {
|
|
516
|
+
autoPlan: { status: 'generating', intent: autoPlanResult.intent, startedAt: autoPlanResult.startedAt },
|
|
517
|
+
hint: 'Call auto again to check if the plan is ready.',
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
if (autoPlanResult.status === 'complete') {
|
|
521
|
+
return ok('Auto-plan ready. Call auto again to review or approve.', {
|
|
522
|
+
autoPlan: { status: 'complete', message: autoPlanResult.message },
|
|
523
|
+
hint: 'Call auto with approve: true to execute.',
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
if (autoPlanResult.status === 'failed') {
|
|
527
|
+
return fail(`Auto-plan generation failed: ${autoPlanResult.error}`, {
|
|
528
|
+
code: 'AUTO_PLAN_FAILED', retryable: true,
|
|
529
|
+
suggestion: 'Call auto again to retry.',
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
// No auto-plan file — normal "no orchestration" case
|
|
535
|
+
}
|
|
481
536
|
return fail('No active orchestration found.', {
|
|
482
537
|
code: 'NO_ACTIVE_ORCHESTRATION', retryable: false, suggestion: 'Run init to create an orchestration first.',
|
|
483
538
|
});
|
|
@@ -571,8 +626,8 @@ export function registerTools(server, executor, context) {
|
|
|
571
626
|
}
|
|
572
627
|
}
|
|
573
628
|
// Use cloud tier if available, otherwise default to 'free' (local MCP mode)
|
|
574
|
-
const tier =
|
|
575
|
-
|
|
629
|
+
const tier = await resolveUserTier(context);
|
|
630
|
+
const responseData = {
|
|
576
631
|
feature: manifest.feature,
|
|
577
632
|
status: manifest.status,
|
|
578
633
|
progress: { completed, failed, total, percentage: Math.round((completed / total) * 100) },
|
|
@@ -597,7 +652,14 @@ export function registerTools(server, executor, context) {
|
|
|
597
652
|
maxWaves: tier.maxWaves,
|
|
598
653
|
maxProviders: tier.maxProviders,
|
|
599
654
|
},
|
|
600
|
-
}
|
|
655
|
+
};
|
|
656
|
+
if (manifest.status === 'circuit_breaker') {
|
|
657
|
+
responseData.circuitBreaker = {
|
|
658
|
+
status: 'paused',
|
|
659
|
+
hint: 'Fix the root cause, then run recover with include_failed: true to resume.',
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
return ok(`Orchestration: "${manifest.feature}"`, responseData);
|
|
601
663
|
});
|
|
602
664
|
});
|
|
603
665
|
// --------------------------------------------------------------------------
|
|
@@ -608,6 +670,7 @@ export function registerTools(server, executor, context) {
|
|
|
608
670
|
description: 'Run the orchestration. Each call executes one wave of streams in parallel via LLM API, ' +
|
|
609
671
|
'applies artifacts to the codebase, and runs verification commands. ' +
|
|
610
672
|
'Call repeatedly for each wave, or use auto mode.',
|
|
673
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
611
674
|
inputSchema: {
|
|
612
675
|
mode: z.enum(['auto', 'wave']).optional().default('wave')
|
|
613
676
|
.describe('auto: execute all waves sequentially. wave: execute one wave and return.'),
|
|
@@ -617,9 +680,13 @@ export function registerTools(server, executor, context) {
|
|
|
617
680
|
.describe('Preview without executing (default: false)'),
|
|
618
681
|
model: z.string().optional()
|
|
619
682
|
.describe('LLM model override (auto-detected based on provider)'),
|
|
683
|
+
iterate: z.boolean().optional().default(false)
|
|
684
|
+
.describe('When true with mode=auto, re-plan and re-execute if failures remain. Max 3 iterations.'),
|
|
685
|
+
max_iterations: z.number().optional().default(3)
|
|
686
|
+
.describe('Maximum iterations for iterative mode (default: 3, max: 5).'),
|
|
620
687
|
project_dir: z.string().optional().describe('Project directory'),
|
|
621
688
|
},
|
|
622
|
-
}, async ({ mode, wave: waveNum, dry_run, model, project_dir }) => {
|
|
689
|
+
}, async ({ mode, wave: waveNum, dry_run, model, iterate, max_iterations, project_dir }) => {
|
|
623
690
|
return withToolLogging('execute', { mode, wave: waveNum, dry_run }, async () => {
|
|
624
691
|
const projectDir = resolveProjectDir(project_dir);
|
|
625
692
|
if (!(await manifestExists(projectDir))) {
|
|
@@ -627,6 +694,22 @@ export function registerTools(server, executor, context) {
|
|
|
627
694
|
code: 'NO_ACTIVE_ORCHESTRATION', retryable: false, suggestion: 'Run init to create an orchestration first.',
|
|
628
695
|
});
|
|
629
696
|
}
|
|
697
|
+
const executeManifest = await loadManifest(projectDir);
|
|
698
|
+
if (executeManifest.status === 'circuit_breaker') {
|
|
699
|
+
return {
|
|
700
|
+
content: [{
|
|
701
|
+
type: 'text',
|
|
702
|
+
text: JSON.stringify({
|
|
703
|
+
success: false,
|
|
704
|
+
message: 'Orchestration paused by circuit breaker. Fix the root cause, then run recover with include_failed: true to resume.',
|
|
705
|
+
data: {
|
|
706
|
+
status: 'circuit_breaker',
|
|
707
|
+
hint: 'Use orchex.recover(include_failed: true) after fixing the issue.',
|
|
708
|
+
},
|
|
709
|
+
}),
|
|
710
|
+
}],
|
|
711
|
+
};
|
|
712
|
+
}
|
|
630
713
|
// Create executor (use injected one if provided, otherwise check config)
|
|
631
714
|
let exec;
|
|
632
715
|
if (executor) {
|
|
@@ -643,6 +726,13 @@ export function registerTools(server, executor, context) {
|
|
|
643
726
|
suggestion: 'Set your API key: orchex config --staging --api-key <key>',
|
|
644
727
|
});
|
|
645
728
|
}
|
|
729
|
+
const providerError = validateCloudProvider(config);
|
|
730
|
+
if (providerError) {
|
|
731
|
+
return fail(providerError, {
|
|
732
|
+
code: 'CLOUD_PROVIDER_UNSUPPORTED', retryable: false,
|
|
733
|
+
suggestion: 'Switch to Anthropic: orchex config --provider anthropic',
|
|
734
|
+
});
|
|
735
|
+
}
|
|
646
736
|
try {
|
|
647
737
|
const { CloudExecutor } = await import('./cloud-executor.js');
|
|
648
738
|
exec = new CloudExecutor(config.apiUrl, config.apiKey);
|
|
@@ -684,16 +774,23 @@ export function registerTools(server, executor, context) {
|
|
|
684
774
|
}
|
|
685
775
|
}
|
|
686
776
|
else {
|
|
687
|
-
// Local mode: auto-detect provider from environment variables
|
|
777
|
+
// Local mode: auto-detect provider from environment variables,
|
|
778
|
+
// falling back to cached BYOK keys from login sync
|
|
688
779
|
try {
|
|
689
780
|
const provider = getConfiguredProvider(config);
|
|
690
|
-
|
|
781
|
+
let apiKey = getProviderApiKey(provider);
|
|
782
|
+
// Fallback: cached BYOK key
|
|
783
|
+
if (!apiKey && provider !== 'ollama' && provider !== 'bedrock') {
|
|
784
|
+
const { getCachedApiKey } = await import('./key-cache.js');
|
|
785
|
+
apiKey = await getCachedApiKey(provider);
|
|
786
|
+
}
|
|
787
|
+
exec = createExecutor({ provider, ...(apiKey ? { apiKey } : {}) });
|
|
691
788
|
}
|
|
692
789
|
catch (error) {
|
|
693
790
|
const status = getProviderStatus();
|
|
694
791
|
return fail(`Failed to create executor. ${status.message}`, {
|
|
695
792
|
code: 'EXECUTOR_CREATE_FAILED', retryable: false,
|
|
696
|
-
suggestion: 'Set an API key for a supported provider (ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY, GEMINI_API_KEY, or OLLAMA_HOST).',
|
|
793
|
+
suggestion: 'Set an API key for a supported provider (ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY, GEMINI_API_KEY, or OLLAMA_HOST), or store a key at orchex.dev/dashboard/keys and run `orchex login`.',
|
|
697
794
|
});
|
|
698
795
|
}
|
|
699
796
|
}
|
|
@@ -783,8 +880,69 @@ export function registerTools(server, executor, context) {
|
|
|
783
880
|
// Continue past failures — stop only when no runnable streams remain
|
|
784
881
|
done = result.done || !result.next;
|
|
785
882
|
}
|
|
883
|
+
// Iterative execution: re-plan and retry if failures remain
|
|
884
|
+
if (iterate && totalFailed > 0) {
|
|
885
|
+
const maxIter = Math.min(max_iterations ?? 3, 5);
|
|
886
|
+
let iteration = 1;
|
|
887
|
+
while (iteration < maxIter) {
|
|
888
|
+
const summaries = [...allStreamResults.values()].map(s => ({
|
|
889
|
+
id: s.id,
|
|
890
|
+
status: (s.status === 'complete' ? 'complete' : 'failed'),
|
|
891
|
+
verifyPassed: s.status === 'complete',
|
|
892
|
+
error: s.error,
|
|
893
|
+
}));
|
|
894
|
+
const decision = evaluateCompletion({
|
|
895
|
+
intent: manifest.feature,
|
|
896
|
+
streamResults: summaries,
|
|
897
|
+
iterationNumber: iteration,
|
|
898
|
+
maxIterations: maxIter,
|
|
899
|
+
});
|
|
900
|
+
if (decision.complete)
|
|
901
|
+
break;
|
|
902
|
+
// Re-plan based on failures (uses auto-planner from P-AUTO)
|
|
903
|
+
// This is a placeholder — requires P-AUTO's generatePlan() to be implemented
|
|
904
|
+
iteration++;
|
|
905
|
+
toolLog.info({ iteration, nextIntent: decision.nextIntent?.slice(0, 100) }, 'iteration_replanning');
|
|
906
|
+
}
|
|
907
|
+
}
|
|
786
908
|
broadcaster.unsubscribe(orchestrationId, progressListener);
|
|
787
|
-
//
|
|
909
|
+
// Generate execution report first (so we have runId for history save)
|
|
910
|
+
let report;
|
|
911
|
+
try {
|
|
912
|
+
const { generateReport, saveReportLocally } = await import('./intelligence/index.js');
|
|
913
|
+
const { generateLearningSummary } = await import('./intelligence/index.js');
|
|
914
|
+
const reportManifest = await loadManifest(projectDir);
|
|
915
|
+
report = generateReport(reportManifest, [{
|
|
916
|
+
success: totalFailed === 0,
|
|
917
|
+
wave: { number: wavesCompleted, totalWaves: wavesCompleted },
|
|
918
|
+
streams: [...allStreamResults.values()],
|
|
919
|
+
verify: { passed: allStreamResults.size - totalFailed, failed: totalFailed },
|
|
920
|
+
done: true,
|
|
921
|
+
}]);
|
|
922
|
+
await saveReportLocally(projectDir, report);
|
|
923
|
+
// Sync to cloud if locally authenticated (no ToolContext historyStore)
|
|
924
|
+
if (!context?.historyStore) {
|
|
925
|
+
try {
|
|
926
|
+
const { loadConfig } = await import('./config.js');
|
|
927
|
+
const { syncReportToCloud } = await import('./cloud-sync.js');
|
|
928
|
+
const syncConfig = await loadConfig();
|
|
929
|
+
await syncReportToCloud(syncConfig, report);
|
|
930
|
+
}
|
|
931
|
+
catch {
|
|
932
|
+
// Sync is best-effort
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
const summary = generateLearningSummary(report);
|
|
936
|
+
await logger.info('execution_report_saved', {
|
|
937
|
+
runId: report.runId,
|
|
938
|
+
qualityScore: report.planQualityScore,
|
|
939
|
+
learningSummary: summary,
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
catch {
|
|
943
|
+
// Report generation is best-effort
|
|
944
|
+
}
|
|
945
|
+
// Persist run to history store (after report for runId)
|
|
788
946
|
if (context?.historyStore) {
|
|
789
947
|
try {
|
|
790
948
|
const finalManifest = await loadManifest(projectDir);
|
|
@@ -805,6 +963,7 @@ export function registerTools(server, executor, context) {
|
|
|
805
963
|
await context.historyStore.saveRun({
|
|
806
964
|
userId: context.userId,
|
|
807
965
|
organizationId: context.organizationId,
|
|
966
|
+
runId: report?.runId,
|
|
808
967
|
feature: finalManifest.feature,
|
|
809
968
|
status: totalFailed === 0 ? 'complete' : 'failed',
|
|
810
969
|
streamCount: streamRecords.length,
|
|
@@ -879,38 +1038,67 @@ export function registerTools(server, executor, context) {
|
|
|
879
1038
|
await logger.info('wave_cost', { wave: result.wave.number, costUsd: waveCost });
|
|
880
1039
|
}
|
|
881
1040
|
broadcaster.unsubscribe(orchestrationId, progressListener);
|
|
882
|
-
//
|
|
883
|
-
if (
|
|
1041
|
+
// Generate execution report first (for runId)
|
|
1042
|
+
if (result.done || !result.next) {
|
|
1043
|
+
let report;
|
|
884
1044
|
try {
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1045
|
+
const { generateReport, saveReportLocally } = await import('./intelligence/index.js');
|
|
1046
|
+
const { generateLearningSummary } = await import('./intelligence/index.js');
|
|
1047
|
+
const reportManifest = await loadManifest(projectDir);
|
|
1048
|
+
report = generateReport(reportManifest, [result]);
|
|
1049
|
+
await saveReportLocally(projectDir, report);
|
|
1050
|
+
if (!context?.historyStore) {
|
|
1051
|
+
try {
|
|
1052
|
+
const { loadConfig } = await import('./config.js');
|
|
1053
|
+
const { syncReportToCloud } = await import('./cloud-sync.js');
|
|
1054
|
+
const syncConfig = await loadConfig();
|
|
1055
|
+
await syncReportToCloud(syncConfig, report);
|
|
1056
|
+
}
|
|
1057
|
+
catch { /* Sync is best-effort */ }
|
|
1058
|
+
}
|
|
1059
|
+
const summary = generateLearningSummary(report);
|
|
1060
|
+
await logger.info('execution_report_saved', {
|
|
1061
|
+
runId: report.runId,
|
|
1062
|
+
qualityScore: report.planQualityScore,
|
|
1063
|
+
learningSummary: summary,
|
|
901
1064
|
});
|
|
902
|
-
await context.historyStore.saveRun({
|
|
903
|
-
userId: context.userId,
|
|
904
|
-
organizationId: context.organizationId,
|
|
905
|
-
feature: finalManifest.feature,
|
|
906
|
-
status: result.verify.failed === 0 ? 'complete' : 'failed',
|
|
907
|
-
streamCount: streamRecords.length,
|
|
908
|
-
waveCount: currentWave.number,
|
|
909
|
-
startedAt: new Date().toISOString(),
|
|
910
|
-
}, streamRecords);
|
|
911
1065
|
}
|
|
912
|
-
catch
|
|
913
|
-
|
|
1066
|
+
catch {
|
|
1067
|
+
// Report generation is best-effort
|
|
1068
|
+
}
|
|
1069
|
+
// Persist to history store (after report for runId)
|
|
1070
|
+
if (context?.historyStore) {
|
|
1071
|
+
try {
|
|
1072
|
+
const finalManifest = await loadManifest(projectDir);
|
|
1073
|
+
const waveResultMap = new Map(result.streams.map(sr => [sr.id, sr]));
|
|
1074
|
+
const streamRecords = Object.entries(finalManifest.streams).map(([sid, s]) => {
|
|
1075
|
+
const execResult = waveResultMap.get(sid);
|
|
1076
|
+
return {
|
|
1077
|
+
streamId: sid,
|
|
1078
|
+
name: s.name,
|
|
1079
|
+
status: s.status,
|
|
1080
|
+
waveNumber: finalManifest.waveAssignments?.[sid] ?? 0,
|
|
1081
|
+
filesChanged: s.owns ?? [],
|
|
1082
|
+
tokensInput: execResult?.tokensUsed?.input ?? 0,
|
|
1083
|
+
tokensOutput: execResult?.tokensUsed?.output ?? 0,
|
|
1084
|
+
provider: execResult?.provider,
|
|
1085
|
+
model: execResult?.model,
|
|
1086
|
+
};
|
|
1087
|
+
});
|
|
1088
|
+
await context.historyStore.saveRun({
|
|
1089
|
+
userId: context.userId,
|
|
1090
|
+
organizationId: context.organizationId,
|
|
1091
|
+
runId: report?.runId,
|
|
1092
|
+
feature: finalManifest.feature,
|
|
1093
|
+
status: result.verify.failed === 0 ? 'complete' : 'failed',
|
|
1094
|
+
streamCount: streamRecords.length,
|
|
1095
|
+
waveCount: currentWave.number,
|
|
1096
|
+
startedAt: new Date().toISOString(),
|
|
1097
|
+
}, streamRecords);
|
|
1098
|
+
}
|
|
1099
|
+
catch (err) {
|
|
1100
|
+
toolLog.warn({ err }, 'history_save_failed');
|
|
1101
|
+
}
|
|
914
1102
|
}
|
|
915
1103
|
}
|
|
916
1104
|
await logger.info('background_wave_completed', {
|
|
@@ -962,6 +1150,7 @@ export function registerTools(server, executor, context) {
|
|
|
962
1150
|
server.registerTool('complete', {
|
|
963
1151
|
title: 'Complete',
|
|
964
1152
|
description: 'Mark a stream as complete, or archive the entire orchestration.',
|
|
1153
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
965
1154
|
inputSchema: {
|
|
966
1155
|
stream_id: z.string().optional().describe('Stream ID to mark complete'),
|
|
967
1156
|
archive: z.boolean().optional().default(false).describe('Archive the orchestration'),
|
|
@@ -1014,6 +1203,7 @@ export function registerTools(server, executor, context) {
|
|
|
1014
1203
|
server.registerTool('recover', {
|
|
1015
1204
|
title: 'Recover Stuck Streams',
|
|
1016
1205
|
description: 'Recover streams stuck in in_progress state or failed state. Auto-detects stuck streams and resets them to pending for retry, or skips them.',
|
|
1206
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1017
1207
|
inputSchema: {
|
|
1018
1208
|
mode: z.enum(['retry', 'skip']).optional().default('retry')
|
|
1019
1209
|
.describe('retry: reset stuck streams to pending. skip: mark them as skipped.'),
|
|
@@ -1061,6 +1251,12 @@ export function registerTools(server, executor, context) {
|
|
|
1061
1251
|
await updateStreamStatus(projectDir, stream_id, 'skipped');
|
|
1062
1252
|
newStatus = 'skipped';
|
|
1063
1253
|
}
|
|
1254
|
+
// Reset circuit_breaker status on recovery
|
|
1255
|
+
const updatedManifest = await loadManifest(projectDir);
|
|
1256
|
+
if (updatedManifest.status === 'circuit_breaker') {
|
|
1257
|
+
updatedManifest.status = 'pending';
|
|
1258
|
+
await saveManifest(projectDir, updatedManifest);
|
|
1259
|
+
}
|
|
1064
1260
|
return ok(`Stream '${stream_id}' recovered (set to ${newStatus})`, {
|
|
1065
1261
|
stream_id,
|
|
1066
1262
|
status: newStatus,
|
|
@@ -1083,6 +1279,12 @@ export function registerTools(server, executor, context) {
|
|
|
1083
1279
|
}
|
|
1084
1280
|
changedIds = recoveredIds;
|
|
1085
1281
|
}
|
|
1282
|
+
// Reset circuit_breaker status on recovery
|
|
1283
|
+
const updatedManifest = await loadManifest(projectDir);
|
|
1284
|
+
if (updatedManifest.status === 'circuit_breaker') {
|
|
1285
|
+
updatedManifest.status = 'pending';
|
|
1286
|
+
await saveManifest(projectDir, updatedManifest);
|
|
1287
|
+
}
|
|
1086
1288
|
return ok(`Recovered ${changedIds.length} stuck stream(s): [${changedIds.join(', ')}]${mode === 'skip' ? ' (set to skipped)' : ''}`, {
|
|
1087
1289
|
recovered: changedIds,
|
|
1088
1290
|
status: mode === 'skip' ? 'skipped' : 'pending',
|
|
@@ -1095,7 +1297,8 @@ export function registerTools(server, executor, context) {
|
|
|
1095
1297
|
// --------------------------------------------------------------------------
|
|
1096
1298
|
server.registerTool('learn', {
|
|
1097
1299
|
title: 'Learn from Plan',
|
|
1098
|
-
description: 'Parse a
|
|
1300
|
+
description: 'Parse a structured implementation plan into stream definitions. Internal pipeline stage — use auto for most workflows. Expects markdown with code deliverables, not PRDs or specs.',
|
|
1301
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1099
1302
|
inputSchema: {
|
|
1100
1303
|
document_path: z.string().describe('Path to the markdown planning document'),
|
|
1101
1304
|
document_content: z.string().optional().describe('Plan document contents as string. When provided, used instead of reading document_path from disk. Useful for cloud mode where the server cannot access local files.'),
|
|
@@ -1106,28 +1309,26 @@ export function registerTools(server, executor, context) {
|
|
|
1106
1309
|
},
|
|
1107
1310
|
}, async ({ document_path, document_content, deliverable_level, prefix, validate_antipatterns, project_dir }) => {
|
|
1108
1311
|
return withToolLogging('learn', { document_path }, async () => {
|
|
1109
|
-
//
|
|
1312
|
+
// Tier gating (both local and cloud modes)
|
|
1110
1313
|
let learnCallsRemaining;
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1314
|
+
const access = await checkLearnAccess(context);
|
|
1315
|
+
if (!access.allowed) {
|
|
1316
|
+
return fail(access.error, {
|
|
1317
|
+
code: 'LEARN_TRIAL_EXPIRED',
|
|
1318
|
+
retryable: false,
|
|
1319
|
+
suggestion: access.suggestion,
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
if (context?.userTier === 'free') {
|
|
1323
|
+
const user = await context.userStore.getUserById(context.userId);
|
|
1324
|
+
if (user && user.learnCallsCount >= FREE_LEARN_LIMIT) {
|
|
1325
|
+
return fail(`Free trial limit reached. You have used all ${FREE_LEARN_LIMIT} free learn calls.`, {
|
|
1326
|
+
code: 'LEARN_QUOTA_EXCEEDED',
|
|
1116
1327
|
retryable: false,
|
|
1117
|
-
suggestion:
|
|
1328
|
+
suggestion: 'Upgrade to Pro ($19/mo) for unlimited learn calls: /pricing',
|
|
1118
1329
|
});
|
|
1119
1330
|
}
|
|
1120
|
-
|
|
1121
|
-
const user = await context.userStore.getUserById(context.userId);
|
|
1122
|
-
if (user && user.learnCallsCount >= FREE_LEARN_LIMIT) {
|
|
1123
|
-
return fail(`Free trial limit reached. You have used all ${FREE_LEARN_LIMIT} free learn calls.`, {
|
|
1124
|
-
code: 'LEARN_QUOTA_EXCEEDED',
|
|
1125
|
-
retryable: false,
|
|
1126
|
-
suggestion: 'Upgrade to Pro ($19/mo) for unlimited learn calls: /pricing',
|
|
1127
|
-
});
|
|
1128
|
-
}
|
|
1129
|
-
learnCallsRemaining = user ? FREE_LEARN_LIMIT - user.learnCallsCount - 1 : undefined;
|
|
1130
|
-
}
|
|
1331
|
+
learnCallsRemaining = user ? FREE_LEARN_LIMIT - user.learnCallsCount - 1 : undefined;
|
|
1131
1332
|
}
|
|
1132
1333
|
const projectDir = resolveProjectDir(project_dir);
|
|
1133
1334
|
const diagnostics = createDiagnostics();
|
|
@@ -1217,6 +1418,14 @@ export function registerTools(server, executor, context) {
|
|
|
1217
1418
|
projectDir,
|
|
1218
1419
|
diagnostics,
|
|
1219
1420
|
});
|
|
1421
|
+
// Sequential edit diagnostics (P2.13)
|
|
1422
|
+
const { detectSequentialEdits } = await import('./intelligence/index.js');
|
|
1423
|
+
const seqDiagnostics = detectSequentialEdits(result.streams);
|
|
1424
|
+
if (seqDiagnostics.length > 0) {
|
|
1425
|
+
for (const diag of seqDiagnostics) {
|
|
1426
|
+
diagnostics.warnings.push(`Sequential edit conflict: ${diag.suggestion}`);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1220
1429
|
// Calculate waves from the generated streams
|
|
1221
1430
|
const streamDefs = Object.fromEntries(Object.entries(result.streams).map(([id, s]) => [
|
|
1222
1431
|
id,
|
|
@@ -1234,22 +1443,27 @@ export function registerTools(server, executor, context) {
|
|
|
1234
1443
|
]));
|
|
1235
1444
|
const { waves } = calculateWaves({ feature: plan.title, streams: streamDefs, status: 'pending', created: new Date().toISOString() });
|
|
1236
1445
|
// Warn if wave count approaches or exceeds tier limit
|
|
1237
|
-
|
|
1238
|
-
const tier =
|
|
1446
|
+
{
|
|
1447
|
+
const tier = await resolveUserTier(context);
|
|
1239
1448
|
const waveWarning = getWaveCountWarning(waves.length, tier);
|
|
1240
1449
|
if (waveWarning) {
|
|
1241
1450
|
diagnostics.warnings.push(waveWarning);
|
|
1242
1451
|
}
|
|
1243
1452
|
}
|
|
1244
|
-
//
|
|
1245
|
-
const
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
for (const
|
|
1250
|
-
diagnostics.warnings.push(`
|
|
1453
|
+
// Auto-merge ownership conflicts BEFORE toInitFormat
|
|
1454
|
+
const ownershipMerge = autoMergeOwnershipConflicts(result.streams);
|
|
1455
|
+
if (ownershipMerge.merges.length > 0) {
|
|
1456
|
+
result.streams = ownershipMerge.streams;
|
|
1457
|
+
result.count = Object.keys(ownershipMerge.streams).length;
|
|
1458
|
+
for (const msg of ownershipMerge.merges) {
|
|
1459
|
+
diagnostics.warnings.push(`Auto-merged: ${msg}`);
|
|
1251
1460
|
}
|
|
1461
|
+
diagnostics.ownershipConflicts = [];
|
|
1462
|
+
}
|
|
1463
|
+
else {
|
|
1464
|
+
diagnostics.ownershipConflicts = detectOwnershipConflicts(result.streams);
|
|
1252
1465
|
}
|
|
1466
|
+
const initFormatStreams = toInitFormat(result);
|
|
1253
1467
|
const prerequisites = extractPrerequisites(initFormatStreams);
|
|
1254
1468
|
// Build summary message
|
|
1255
1469
|
let message = `Generated ${result.count} streams from "${plan.title}"`;
|
|
@@ -1303,13 +1517,14 @@ export function registerTools(server, executor, context) {
|
|
|
1303
1517
|
learnTrial: learnCallsRemaining !== undefined ? {
|
|
1304
1518
|
remaining: learnCallsRemaining,
|
|
1305
1519
|
limit: FREE_LEARN_LIMIT,
|
|
1306
|
-
trialDays:
|
|
1520
|
+
trialDays: TRIAL_WINDOW_DAYS,
|
|
1307
1521
|
} : undefined,
|
|
1308
1522
|
reports: {
|
|
1309
1523
|
deliverables: formatDeliverablesReport(rawDeliverables),
|
|
1310
1524
|
dependencies: formatDependencyReport(graph),
|
|
1311
1525
|
streams: formatStreamsForReview(result),
|
|
1312
1526
|
},
|
|
1527
|
+
sequentialEditConflicts: seqDiagnostics.length > 0 ? seqDiagnostics : undefined,
|
|
1313
1528
|
diagnostics,
|
|
1314
1529
|
});
|
|
1315
1530
|
});
|
|
@@ -1319,7 +1534,8 @@ export function registerTools(server, executor, context) {
|
|
|
1319
1534
|
// --------------------------------------------------------------------------
|
|
1320
1535
|
server.registerTool('init-plan', {
|
|
1321
1536
|
title: 'Generate Plan Template',
|
|
1322
|
-
description: 'Generate an annotated
|
|
1537
|
+
description: 'Generate an annotated plan template for manual editing. Advanced workflow — use auto for most cases.',
|
|
1538
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1323
1539
|
inputSchema: {
|
|
1324
1540
|
feature_name: z.string().optional().default('my-feature').describe('Feature name for the plan (used in title and example stream IDs)'),
|
|
1325
1541
|
output_path: z.string().optional().describe('Output file path. Defaults to docs/plans/YYYY-MM-DD-<feature>-plan.md'),
|
|
@@ -1358,11 +1574,552 @@ export function registerTools(server, executor, context) {
|
|
|
1358
1574
|
});
|
|
1359
1575
|
});
|
|
1360
1576
|
// --------------------------------------------------------------------------
|
|
1361
|
-
//
|
|
1577
|
+
// 10. auto — One-shot orchestration from intent (P-AUTO)
|
|
1578
|
+
// --------------------------------------------------------------------------
|
|
1579
|
+
/** Heartbeat interval during plan generation to keep MCP transport alive. */
|
|
1580
|
+
const AUTO_HEARTBEAT_MS = 5_000;
|
|
1581
|
+
/** File name for async auto-plan results in .orchex/active/ */
|
|
1582
|
+
const AUTO_PLAN_RESULT_FILE = 'auto-plan-result.json';
|
|
1583
|
+
server.registerTool('auto', {
|
|
1584
|
+
title: 'Auto-Orchestrate',
|
|
1585
|
+
description: 'Start here. Takes any input (intent, PRD, spec, bug report), generates an implementation plan, previews streams, and optionally executes. ' +
|
|
1586
|
+
'The primary entry point for all orchestrations.',
|
|
1587
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
1588
|
+
inputSchema: {
|
|
1589
|
+
intent: z.string().describe('What you want to build (e.g., "Add user authentication with JWT")'),
|
|
1590
|
+
project_dir: z.string().optional().describe('Project directory (defaults to cwd)'),
|
|
1591
|
+
provider: z.string().optional().describe('LLM provider override'),
|
|
1592
|
+
model: z.string().optional().describe('LLM model override'),
|
|
1593
|
+
approve: z.boolean().optional().default(false).describe('If true, execute immediately after preview. If false (default), return preview only.'),
|
|
1594
|
+
approved_streams: z.array(z.string()).optional().describe('Stream IDs to approve for execution. Non-listed pending streams will be skipped. ' +
|
|
1595
|
+
'Omit this parameter to approve all streams (default behavior).'),
|
|
1596
|
+
},
|
|
1597
|
+
}, async ({ intent, project_dir, provider: providerOverride, model: modelOverride, approve, approved_streams }) => {
|
|
1598
|
+
return withToolLogging('auto', { intent, approve }, async () => {
|
|
1599
|
+
const projectDir = resolveProjectDir(project_dir);
|
|
1600
|
+
// 1. Create executor
|
|
1601
|
+
let exec;
|
|
1602
|
+
if (executor) {
|
|
1603
|
+
exec = executor;
|
|
1604
|
+
}
|
|
1605
|
+
else {
|
|
1606
|
+
const config = await loadConfig();
|
|
1607
|
+
if (config.mode === 'cloud') {
|
|
1608
|
+
const validationError = validateCloudConfig(config);
|
|
1609
|
+
if (validationError) {
|
|
1610
|
+
return fail(validationError, { code: 'CLOUD_CONFIG_INVALID', retryable: false });
|
|
1611
|
+
}
|
|
1612
|
+
const providerError = validateCloudProvider(config);
|
|
1613
|
+
if (providerError) {
|
|
1614
|
+
return fail(providerError, {
|
|
1615
|
+
code: 'CLOUD_PROVIDER_UNSUPPORTED', retryable: false,
|
|
1616
|
+
suggestion: 'Switch to Anthropic: orchex config --provider anthropic',
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
try {
|
|
1620
|
+
const { CloudExecutor } = await import('./cloud-executor.js');
|
|
1621
|
+
exec = new CloudExecutor(config.apiUrl, config.apiKey);
|
|
1622
|
+
}
|
|
1623
|
+
catch {
|
|
1624
|
+
return fail('Cloud mode requires the orchex server. Visit orchex.dev for details.', {
|
|
1625
|
+
code: 'CLOUD_NOT_AVAILABLE', retryable: false,
|
|
1626
|
+
suggestion: 'Cloud execution is not available in the local MCP package. Sign up at orchex.dev for cloud features, or use local mode with your own API key.',
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
else {
|
|
1631
|
+
const resolvedProvider = providerOverride
|
|
1632
|
+
? providerOverride
|
|
1633
|
+
: getConfiguredProvider(await loadConfig());
|
|
1634
|
+
let autoApiKey = getProviderApiKey(resolvedProvider);
|
|
1635
|
+
// Fallback: cached BYOK key
|
|
1636
|
+
if (!autoApiKey && resolvedProvider !== 'ollama' && resolvedProvider !== 'bedrock') {
|
|
1637
|
+
const { getCachedApiKey } = await import('./key-cache.js');
|
|
1638
|
+
autoApiKey = await getCachedApiKey(resolvedProvider);
|
|
1639
|
+
}
|
|
1640
|
+
exec = createExecutor({ provider: resolvedProvider, ...(autoApiKey ? { apiKey: autoApiKey } : {}) });
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
// 1a. First-run detection
|
|
1644
|
+
const { isFirstRun, getFirstRunSuggestion, markFirstRunComplete } = await import('./intelligence/index.js');
|
|
1645
|
+
const firstRun = await isFirstRun(projectDir);
|
|
1646
|
+
let firstRunHint = '';
|
|
1647
|
+
if (firstRun) {
|
|
1648
|
+
firstRunHint = '\n\n---\n' + getFirstRunSuggestion();
|
|
1649
|
+
}
|
|
1650
|
+
// 1b. Load pattern hints from execution history (L2)
|
|
1651
|
+
const { loadPatterns, patternsToPromptHints } = await import('./intelligence/index.js');
|
|
1652
|
+
const patterns = await loadPatterns(projectDir);
|
|
1653
|
+
const patternHints = patternsToPromptHints(patterns);
|
|
1654
|
+
// 2. Check for existing async plan result
|
|
1655
|
+
const autoPlanPath = path.join(projectDir, '.orchex', 'active', AUTO_PLAN_RESULT_FILE);
|
|
1656
|
+
let cachedPlan = null;
|
|
1657
|
+
try {
|
|
1658
|
+
const raw = await fs.readFile(autoPlanPath, 'utf-8');
|
|
1659
|
+
cachedPlan = JSON.parse(raw);
|
|
1660
|
+
}
|
|
1661
|
+
catch {
|
|
1662
|
+
// No cached plan — first call or already cleaned up
|
|
1663
|
+
}
|
|
1664
|
+
// If plan is still generating, return status
|
|
1665
|
+
if (cachedPlan?.status === 'generating') {
|
|
1666
|
+
// Stale detection: if older than 10 minutes, treat as failed
|
|
1667
|
+
const startedAt = cachedPlan.startedAt;
|
|
1668
|
+
if (startedAt && Date.now() - new Date(startedAt).getTime() > 600_000) {
|
|
1669
|
+
await fs.unlink(autoPlanPath).catch(() => { });
|
|
1670
|
+
// Fall through to start fresh generation
|
|
1671
|
+
cachedPlan = null;
|
|
1672
|
+
}
|
|
1673
|
+
else {
|
|
1674
|
+
return ok('Auto-plan generation in progress. Call status to check, or wait and call auto again.', {
|
|
1675
|
+
status: 'generating',
|
|
1676
|
+
intent: cachedPlan.intent,
|
|
1677
|
+
startedAt: cachedPlan.startedAt,
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
// If plan is complete and not approved, return cached result
|
|
1682
|
+
if (cachedPlan?.status === 'complete' && !approve) {
|
|
1683
|
+
return ok(cachedPlan.message, {
|
|
1684
|
+
...cachedPlan,
|
|
1685
|
+
hint: 'Call auto again with approve: true to execute this plan.',
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
// If plan failed, clear and allow retry
|
|
1689
|
+
if (cachedPlan?.status === 'failed' && !approve) {
|
|
1690
|
+
await fs.unlink(autoPlanPath).catch(() => { });
|
|
1691
|
+
cachedPlan = null;
|
|
1692
|
+
}
|
|
1693
|
+
// Resolve tier BEFORE plan generation so we can pass stream budget to the LLM
|
|
1694
|
+
const autoTier = await resolveUserTier(context);
|
|
1695
|
+
const autoMaxStreams = autoTier.maxParallelAgents === -1 ? undefined : autoTier.maxParallelAgents;
|
|
1696
|
+
// 3. Preview mode (approve: false) — generate plan asynchronously
|
|
1697
|
+
if (!approve) {
|
|
1698
|
+
// Ensure .orchex/active/ directory exists
|
|
1699
|
+
await fs.mkdir(path.join(projectDir, '.orchex', 'active'), { recursive: true });
|
|
1700
|
+
// Write generating status
|
|
1701
|
+
await fs.writeFile(autoPlanPath, JSON.stringify({
|
|
1702
|
+
status: 'generating',
|
|
1703
|
+
intent,
|
|
1704
|
+
startedAt: new Date().toISOString(),
|
|
1705
|
+
}, null, 2));
|
|
1706
|
+
// Fire-and-forget: plan generation runs in background
|
|
1707
|
+
(async () => {
|
|
1708
|
+
try {
|
|
1709
|
+
// Heartbeat for logging (informational, does not reset client timeout)
|
|
1710
|
+
const heartbeatInterval = setInterval(() => {
|
|
1711
|
+
try {
|
|
1712
|
+
server.server.sendLoggingMessage({
|
|
1713
|
+
level: 'info',
|
|
1714
|
+
data: { phase: 'plan-generation', status: 'in_progress', intent: intent.substring(0, 100) },
|
|
1715
|
+
logger: 'orchex-auto',
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
catch { /* ignore */ }
|
|
1719
|
+
}, AUTO_HEARTBEAT_MS);
|
|
1720
|
+
const { generatePlan: autoGeneratePlan } = await import('./intelligence/index.js');
|
|
1721
|
+
let planResult;
|
|
1722
|
+
try {
|
|
1723
|
+
planResult = await autoGeneratePlan(intent, projectDir, exec, {
|
|
1724
|
+
model: modelOverride,
|
|
1725
|
+
provider: exec.provider,
|
|
1726
|
+
maxStreams: autoMaxStreams,
|
|
1727
|
+
extraContext: patternHints || undefined,
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
finally {
|
|
1731
|
+
clearInterval(heartbeatInterval);
|
|
1732
|
+
}
|
|
1733
|
+
// Learn pipeline
|
|
1734
|
+
const diagnostics = createDiagnostics();
|
|
1735
|
+
const plan = parsePlanDocument(planResult.planMarkdown);
|
|
1736
|
+
const rawDeliverables = extractDeliverables(plan, { diagnostics });
|
|
1737
|
+
const deliverables = processDeliverables(rawDeliverables);
|
|
1738
|
+
diagnostics.splitCount = deliverables.length - rawDeliverables.length;
|
|
1739
|
+
if (deliverables.length === 0) {
|
|
1740
|
+
await fs.writeFile(autoPlanPath, JSON.stringify({
|
|
1741
|
+
status: 'failed',
|
|
1742
|
+
error: 'Auto-plan produced no deliverables. Try a more specific intent.',
|
|
1743
|
+
}, null, 2));
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
const graph = buildDependencyGraph(deliverables, { diagnostics });
|
|
1747
|
+
const genResult = generateStreams(deliverables, graph, {
|
|
1748
|
+
validateAntiPatterns: true, projectDir, diagnostics,
|
|
1749
|
+
});
|
|
1750
|
+
const { detectSequentialEdits: detectSeq } = await import('./intelligence/index.js');
|
|
1751
|
+
const seqDiagnostics = detectSeq(genResult.streams);
|
|
1752
|
+
const { classifyTask, suggestProvider } = await import('./intelligence/index.js');
|
|
1753
|
+
const rawInitStreams = toInitFormat(genResult);
|
|
1754
|
+
// Auto-merge ownership conflicts BEFORE model routing
|
|
1755
|
+
const mergeResult = autoMergeOwnershipConflicts(rawInitStreams);
|
|
1756
|
+
const initStreams = mergeResult.streams;
|
|
1757
|
+
if (mergeResult.merges.length > 0) {
|
|
1758
|
+
for (const msg of mergeResult.merges) {
|
|
1759
|
+
diagnostics.warnings.push(`Auto-merged: ${msg}`);
|
|
1760
|
+
}
|
|
1761
|
+
try {
|
|
1762
|
+
server.server.sendLoggingMessage({
|
|
1763
|
+
level: 'info',
|
|
1764
|
+
data: { phase: 'ownership-merge', merges: mergeResult.merges },
|
|
1765
|
+
logger: 'orchex-auto',
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
catch { /* ignore */ }
|
|
1769
|
+
}
|
|
1770
|
+
for (const [, stream] of Object.entries(initStreams)) {
|
|
1771
|
+
const owns = stream.owns ?? [];
|
|
1772
|
+
let totalOwnedLines = 0;
|
|
1773
|
+
for (const fp of owns) {
|
|
1774
|
+
try {
|
|
1775
|
+
const content = await fs.readFile(path.join(projectDir, fp), 'utf-8');
|
|
1776
|
+
totalOwnedLines += content.split('\n').length;
|
|
1777
|
+
}
|
|
1778
|
+
catch {
|
|
1779
|
+
totalOwnedLines += 100;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
const chars = classifyTask(stream.plan ?? '', owns, totalOwnedLines);
|
|
1783
|
+
const sug = suggestProvider(chars);
|
|
1784
|
+
if (sug.provider && sug.confidence >= 0.5) {
|
|
1785
|
+
stream.suggestedProvider = sug.provider;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
const streamDefs = Object.fromEntries(Object.entries(initStreams).map(([id, s]) => [id, {
|
|
1789
|
+
name: s.name, deps: s.deps || [], owns: s.owns || [],
|
|
1790
|
+
reads: s.reads || [], setup: [], plan: s.plan,
|
|
1791
|
+
verify: s.verify || [], status: 'pending', attempts: 0,
|
|
1792
|
+
}]));
|
|
1793
|
+
const { waves } = calculateWaves({
|
|
1794
|
+
feature: plan.title, streams: streamDefs,
|
|
1795
|
+
status: 'pending', created: new Date().toISOString(),
|
|
1796
|
+
});
|
|
1797
|
+
const { formatPlanPreview, estimatePlannedCost } = await import('./intelligence/index.js');
|
|
1798
|
+
const waveList = waves.map(w => ({ number: w.number, streams: w.streams.map(s => s.id) }));
|
|
1799
|
+
const allWarnings = [...genResult.warnings, ...diagnostics.warnings];
|
|
1800
|
+
// Validate models for each stream and build model decisions map
|
|
1801
|
+
const { resolveModel } = await import('./model-validator.js');
|
|
1802
|
+
const { resolveApiUrl, loadConfig, DEFAULT_MODELS } = await import('./config.js');
|
|
1803
|
+
const toolConfig = await loadConfig();
|
|
1804
|
+
const toolApiUrl = toolConfig.mode === 'cloud' && toolConfig.apiKey ? await resolveApiUrl() : undefined;
|
|
1805
|
+
const modelDecisions = {};
|
|
1806
|
+
for (const [id, stream] of Object.entries(initStreams)) {
|
|
1807
|
+
const suggestedProvider = stream.suggestedProvider ?? exec.provider;
|
|
1808
|
+
const suggestedModel = modelOverride ?? DEFAULT_MODELS[suggestedProvider] ?? suggestedProvider;
|
|
1809
|
+
const validation = await resolveModel(suggestedProvider, suggestedModel, toolApiUrl);
|
|
1810
|
+
// Estimate cost
|
|
1811
|
+
const ownedLines = (stream.owns ?? []).length * 100;
|
|
1812
|
+
const costEstimate = estimatePlannedCost(ownedLines * 4, validation.resolvedModel);
|
|
1813
|
+
modelDecisions[id] = {
|
|
1814
|
+
provider: suggestedProvider,
|
|
1815
|
+
model: validation.resolvedModel,
|
|
1816
|
+
estimatedCostUsd: costEstimate.finalCost,
|
|
1817
|
+
fallback: validation.wasFallback ? `${validation.originalModel} → ${validation.resolvedModel}` : undefined,
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
const preview = formatPlanPreview(initStreams, waveList, allWarnings, seqDiagnostics, modelDecisions);
|
|
1821
|
+
const message = `Auto-plan generated: "${plan.title}" — ${preview.summary.streamCount} streams in ${preview.summary.waveCount} waves. Call auto again with approve: true to execute.`;
|
|
1822
|
+
await fs.writeFile(autoPlanPath, JSON.stringify({
|
|
1823
|
+
status: 'complete',
|
|
1824
|
+
message,
|
|
1825
|
+
planMarkdown: planResult.planMarkdown,
|
|
1826
|
+
planTokensUsed: planResult.tokensUsed,
|
|
1827
|
+
preview,
|
|
1828
|
+
streams: initStreams,
|
|
1829
|
+
waves: waveList,
|
|
1830
|
+
warnings: allWarnings.length > 0 ? allWarnings : undefined,
|
|
1831
|
+
sequentialEditConflicts: seqDiagnostics.length > 0 ? seqDiagnostics : undefined,
|
|
1832
|
+
diagnostics,
|
|
1833
|
+
}, null, 2));
|
|
1834
|
+
// Notify completion
|
|
1835
|
+
try {
|
|
1836
|
+
server.server.sendLoggingMessage({
|
|
1837
|
+
level: 'info',
|
|
1838
|
+
data: { phase: 'plan-generation', status: 'complete', title: plan.title, streams: preview.summary.streamCount },
|
|
1839
|
+
logger: 'orchex-auto',
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
catch { /* ignore */ }
|
|
1843
|
+
}
|
|
1844
|
+
catch (err) {
|
|
1845
|
+
await fs.writeFile(autoPlanPath, JSON.stringify({
|
|
1846
|
+
status: 'failed',
|
|
1847
|
+
error: err.message,
|
|
1848
|
+
}, null, 2)).catch(() => { });
|
|
1849
|
+
}
|
|
1850
|
+
})();
|
|
1851
|
+
return ok('Auto-plan generation started in background. Call auto again (or use status) to check progress.', {
|
|
1852
|
+
status: 'generating',
|
|
1853
|
+
intent,
|
|
1854
|
+
hint: 'Plan generation takes 10-60s depending on project size. Call auto again to get the result.',
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
// 4. Approve mode — load cached plan or generate synchronously
|
|
1858
|
+
let planTitle;
|
|
1859
|
+
let planMarkdown;
|
|
1860
|
+
let initStreams;
|
|
1861
|
+
let streamDefs;
|
|
1862
|
+
if (cachedPlan?.status === 'complete') {
|
|
1863
|
+
// Use cached plan from async generation
|
|
1864
|
+
planTitle = parsePlanDocument(cachedPlan.planMarkdown).title;
|
|
1865
|
+
planMarkdown = cachedPlan.planMarkdown;
|
|
1866
|
+
initStreams = cachedPlan.streams;
|
|
1867
|
+
// Auto-merge ownership conflicts from cached plan
|
|
1868
|
+
const mergeResult = autoMergeOwnershipConflicts(initStreams);
|
|
1869
|
+
initStreams = mergeResult.streams;
|
|
1870
|
+
streamDefs = Object.fromEntries(Object.entries(initStreams).map(([id, s]) => [id, {
|
|
1871
|
+
name: s.name, deps: s.deps || [], owns: s.owns || [],
|
|
1872
|
+
reads: s.reads || [], setup: [], plan: s.plan ?? '',
|
|
1873
|
+
verify: s.verify || [], status: 'pending', attempts: 0,
|
|
1874
|
+
}]));
|
|
1875
|
+
// Clean up cached result
|
|
1876
|
+
await fs.unlink(autoPlanPath).catch(() => { });
|
|
1877
|
+
}
|
|
1878
|
+
else {
|
|
1879
|
+
// No cached plan — generate synchronously (direct approve without preview)
|
|
1880
|
+
const heartbeatInterval = setInterval(() => {
|
|
1881
|
+
try {
|
|
1882
|
+
server.server.sendLoggingMessage({
|
|
1883
|
+
level: 'info',
|
|
1884
|
+
data: { phase: 'plan-generation', status: 'in_progress', intent: intent.substring(0, 100) },
|
|
1885
|
+
logger: 'orchex-auto',
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
catch { /* ignore */ }
|
|
1889
|
+
}, AUTO_HEARTBEAT_MS);
|
|
1890
|
+
const { generatePlan: autoGeneratePlan } = await import('./intelligence/index.js');
|
|
1891
|
+
let planResult;
|
|
1892
|
+
try {
|
|
1893
|
+
planResult = await autoGeneratePlan(intent, projectDir, exec, {
|
|
1894
|
+
model: modelOverride,
|
|
1895
|
+
provider: exec.provider,
|
|
1896
|
+
maxStreams: autoMaxStreams,
|
|
1897
|
+
extraContext: patternHints || undefined,
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
catch (err) {
|
|
1901
|
+
return fail(`Auto-plan generation failed: ${err.message}`, {
|
|
1902
|
+
code: 'AUTO_PLAN_FAILED', retryable: true,
|
|
1903
|
+
suggestion: 'Check your LLM provider configuration and try again.',
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1906
|
+
finally {
|
|
1907
|
+
clearInterval(heartbeatInterval);
|
|
1908
|
+
}
|
|
1909
|
+
const diagnostics = createDiagnostics();
|
|
1910
|
+
const plan = parsePlanDocument(planResult.planMarkdown);
|
|
1911
|
+
planTitle = plan.title;
|
|
1912
|
+
planMarkdown = planResult.planMarkdown;
|
|
1913
|
+
const rawDeliverables = extractDeliverables(plan, { diagnostics });
|
|
1914
|
+
const deliverables = processDeliverables(rawDeliverables);
|
|
1915
|
+
diagnostics.splitCount = deliverables.length - rawDeliverables.length;
|
|
1916
|
+
if (deliverables.length === 0) {
|
|
1917
|
+
return fail('Auto-plan produced no deliverables. Try a more specific intent.', {
|
|
1918
|
+
code: 'NO_DELIVERABLES', retryable: true,
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
const graph = buildDependencyGraph(deliverables, { diagnostics });
|
|
1922
|
+
const genResult = generateStreams(deliverables, graph, {
|
|
1923
|
+
validateAntiPatterns: true, projectDir, diagnostics,
|
|
1924
|
+
});
|
|
1925
|
+
const { detectSequentialEdits: detectSeq, autoFixSequentialEdits: autoFixSeq } = await import('./intelligence/index.js');
|
|
1926
|
+
const seqDiagnostics = detectSeq(genResult.streams);
|
|
1927
|
+
if (seqDiagnostics.length > 0)
|
|
1928
|
+
autoFixSeq(genResult.streams);
|
|
1929
|
+
initStreams = toInitFormat(genResult);
|
|
1930
|
+
const syncMergeResult = autoMergeOwnershipConflicts(initStreams);
|
|
1931
|
+
initStreams = syncMergeResult.streams;
|
|
1932
|
+
streamDefs = Object.fromEntries(Object.entries(initStreams).map(([id, s]) => [id, {
|
|
1933
|
+
name: s.name, deps: s.deps || [], owns: s.owns || [],
|
|
1934
|
+
reads: s.reads || [], setup: [], plan: s.plan ?? '',
|
|
1935
|
+
verify: s.verify || [], status: 'pending', attempts: 0,
|
|
1936
|
+
}]));
|
|
1937
|
+
}
|
|
1938
|
+
// 5. Approved — init and execute all waves
|
|
1939
|
+
if (await manifestExists(projectDir)) {
|
|
1940
|
+
return fail('An active orchestration already exists. Complete or archive it first.', {
|
|
1941
|
+
code: 'ACTIVE_ORCHESTRATION_EXISTS', retryable: false,
|
|
1942
|
+
suggestion: 'Run orchex.complete(archive: true) to archive, then retry.',
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1945
|
+
// Tier checks
|
|
1946
|
+
{
|
|
1947
|
+
const tier = await resolveUserTier(context);
|
|
1948
|
+
const limits = checkTierLimits({
|
|
1949
|
+
tier,
|
|
1950
|
+
streams: streamDefs,
|
|
1951
|
+
projectDir,
|
|
1952
|
+
mode: tier.cloudOrchestrations !== 0 ? 'cloud' : 'local',
|
|
1953
|
+
});
|
|
1954
|
+
if (!limits.allowed) {
|
|
1955
|
+
return fail(limits.error, {
|
|
1956
|
+
code: limits.code || 'TIER_LIMIT_EXCEEDED', retryable: false,
|
|
1957
|
+
suggestion: limits.suggestion,
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
// Init orchestration
|
|
1962
|
+
await initOrchestration(projectDir, planTitle, initStreams);
|
|
1963
|
+
// Apply partial approval if approved_streams is specified
|
|
1964
|
+
if (approved_streams !== undefined) {
|
|
1965
|
+
const { applyPartialApproval } = await import('./intelligence/index.js');
|
|
1966
|
+
await applyPartialApproval(projectDir, approved_streams);
|
|
1967
|
+
}
|
|
1968
|
+
// Execute all waves
|
|
1969
|
+
const { generateReport, saveReportLocally } = await import('./intelligence/index.js');
|
|
1970
|
+
const { generateLearningSummary } = await import('./intelligence/index.js');
|
|
1971
|
+
const allResponses = [];
|
|
1972
|
+
let done = false;
|
|
1973
|
+
while (!done) {
|
|
1974
|
+
const response = await executeWave(projectDir, exec, {
|
|
1975
|
+
model: modelOverride,
|
|
1976
|
+
});
|
|
1977
|
+
allResponses.push(response);
|
|
1978
|
+
done = response.done;
|
|
1979
|
+
}
|
|
1980
|
+
// Generate execution report
|
|
1981
|
+
const manifest = await loadManifest(projectDir);
|
|
1982
|
+
const report = generateReport(manifest, allResponses);
|
|
1983
|
+
await saveReportLocally(projectDir, report);
|
|
1984
|
+
// Persist run to history store (server-side)
|
|
1985
|
+
if (context?.historyStore) {
|
|
1986
|
+
try {
|
|
1987
|
+
const allStreamResultsMap = new Map(allResponses.flatMap(r => r.streams).map(sr => [sr.id, sr]));
|
|
1988
|
+
const streamRecords = Object.entries(manifest.streams).map(([sid, s]) => {
|
|
1989
|
+
const execResult = allStreamResultsMap.get(sid);
|
|
1990
|
+
return {
|
|
1991
|
+
streamId: sid,
|
|
1992
|
+
name: s.name,
|
|
1993
|
+
status: s.status,
|
|
1994
|
+
waveNumber: manifest.waveAssignments?.[sid] ?? 0,
|
|
1995
|
+
filesChanged: s.owns ?? [],
|
|
1996
|
+
tokensInput: execResult?.tokensUsed?.input ?? 0,
|
|
1997
|
+
tokensOutput: execResult?.tokensUsed?.output ?? 0,
|
|
1998
|
+
provider: execResult?.provider,
|
|
1999
|
+
model: execResult?.model,
|
|
2000
|
+
};
|
|
2001
|
+
});
|
|
2002
|
+
const allComplete = Object.values(manifest.streams).every(s => s.status === 'complete');
|
|
2003
|
+
await context.historyStore.saveRun({
|
|
2004
|
+
userId: context.userId,
|
|
2005
|
+
organizationId: context.organizationId,
|
|
2006
|
+
runId: report.runId,
|
|
2007
|
+
feature: manifest.feature,
|
|
2008
|
+
status: allComplete ? 'complete' : 'failed',
|
|
2009
|
+
streamCount: streamRecords.length,
|
|
2010
|
+
waveCount: allResponses.length,
|
|
2011
|
+
startedAt: report.timestamp,
|
|
2012
|
+
}, streamRecords);
|
|
2013
|
+
}
|
|
2014
|
+
catch (err) {
|
|
2015
|
+
toolLog.warn({ err }, 'history_save_failed');
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
// Sync to cloud if locally authenticated
|
|
2019
|
+
if (!context?.historyStore) {
|
|
2020
|
+
try {
|
|
2021
|
+
const { loadConfig } = await import('./config.js');
|
|
2022
|
+
const { syncReportToCloud } = await import('./cloud-sync.js');
|
|
2023
|
+
const syncConfig = await loadConfig();
|
|
2024
|
+
await syncReportToCloud(syncConfig, report);
|
|
2025
|
+
}
|
|
2026
|
+
catch { /* Sync is best-effort */ }
|
|
2027
|
+
}
|
|
2028
|
+
const summary = generateLearningSummary(report);
|
|
2029
|
+
if (firstRun) {
|
|
2030
|
+
await markFirstRunComplete(projectDir);
|
|
2031
|
+
}
|
|
2032
|
+
return ok(`Orchestration complete: "${planTitle}"${firstRunHint}`, {
|
|
2033
|
+
planMarkdown,
|
|
2034
|
+
report: {
|
|
2035
|
+
runId: report.runId,
|
|
2036
|
+
planQualityScore: report.planQualityScore,
|
|
2037
|
+
totalStreams: report.totalStreams,
|
|
2038
|
+
totalWaves: report.totalWaves,
|
|
2039
|
+
tokenUsage: report.tokenUsage,
|
|
2040
|
+
waveEfficiency: report.waveEfficiency,
|
|
2041
|
+
failurePatterns: report.failurePatterns,
|
|
2042
|
+
},
|
|
2043
|
+
learningSummary: summary,
|
|
2044
|
+
responses: allResponses,
|
|
2045
|
+
});
|
|
2046
|
+
});
|
|
2047
|
+
});
|
|
2048
|
+
// --------------------------------------------------------------------------
|
|
2049
|
+
// 11. reset-learning — Reset learning data (A11)
|
|
2050
|
+
// --------------------------------------------------------------------------
|
|
2051
|
+
server.registerTool('reset-learning', {
|
|
2052
|
+
title: 'Reset Learning',
|
|
2053
|
+
description: 'Reset learning data: thresholds, events, patterns, and/or reports.',
|
|
2054
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
2055
|
+
inputSchema: {
|
|
2056
|
+
confirm: z.boolean().describe('Must be true to proceed'),
|
|
2057
|
+
patterns_only: z.boolean().optional().default(false).describe('Only reset patterns'),
|
|
2058
|
+
reports_only: z.boolean().optional().default(false).describe('Only reset reports'),
|
|
2059
|
+
project_dir: z.string().optional().describe('Project directory'),
|
|
2060
|
+
},
|
|
2061
|
+
}, async ({ confirm, patterns_only, reports_only, project_dir }) => {
|
|
2062
|
+
return withToolLogging('reset-learning', { confirm, patterns_only, reports_only }, async () => {
|
|
2063
|
+
const projectDir = resolveProjectDir(project_dir);
|
|
2064
|
+
const { resetLearning } = await import('./intelligence/index.js');
|
|
2065
|
+
try {
|
|
2066
|
+
const result = await resetLearning(projectDir, {
|
|
2067
|
+
confirm,
|
|
2068
|
+
patternsOnly: patterns_only,
|
|
2069
|
+
reportsOnly: reports_only,
|
|
2070
|
+
});
|
|
2071
|
+
return ok(result.message, {
|
|
2072
|
+
deleted: result.deleted,
|
|
2073
|
+
totalDeleted: result.totalDeleted,
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
catch (err) {
|
|
2077
|
+
return fail(err.message, {
|
|
2078
|
+
code: 'RESET_LEARNING_ERROR',
|
|
2079
|
+
retryable: false,
|
|
2080
|
+
suggestion: 'Use confirm: true to proceed.',
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
});
|
|
2084
|
+
});
|
|
2085
|
+
// --------------------------------------------------------------------------
|
|
2086
|
+
// 12. rollback-stream — Revert file changes from a stream (P2.8)
|
|
2087
|
+
// --------------------------------------------------------------------------
|
|
2088
|
+
server.registerTool('rollback-stream', {
|
|
2089
|
+
title: 'Rollback Stream',
|
|
2090
|
+
description: 'Revert file changes made by a specific stream using git.',
|
|
2091
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
2092
|
+
inputSchema: {
|
|
2093
|
+
stream_id: z.string().describe('Stream ID to rollback'),
|
|
2094
|
+
project_dir: z.string().optional().describe('Project directory'),
|
|
2095
|
+
},
|
|
2096
|
+
}, async ({ stream_id, project_dir }) => {
|
|
2097
|
+
return withToolLogging('rollback-stream', { stream_id }, async () => {
|
|
2098
|
+
const projectDir = resolveProjectDir(project_dir);
|
|
2099
|
+
const manifest = await loadManifest(projectDir);
|
|
2100
|
+
const stream = manifest.streams[stream_id];
|
|
2101
|
+
if (!stream) {
|
|
2102
|
+
return fail(`Stream '${stream_id}' not found`, {
|
|
2103
|
+
code: 'STREAM_NOT_FOUND', retryable: false,
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
2106
|
+
const { rollbackStream } = await import('./intelligence/index.js');
|
|
2107
|
+
const result = await rollbackStream(projectDir, stream_id, stream.owns ?? []);
|
|
2108
|
+
if (result.errors.length > 0) {
|
|
2109
|
+
return fail(`Rollback partial: ${result.errors.join('; ')}`, {
|
|
2110
|
+
code: 'ROLLBACK_PARTIAL', retryable: false,
|
|
2111
|
+
});
|
|
2112
|
+
}
|
|
2113
|
+
return ok(`Rolled back ${result.reverted.length} file(s) for stream "${stream_id}"`, { reverted: result.reverted });
|
|
2114
|
+
});
|
|
2115
|
+
});
|
|
2116
|
+
// --------------------------------------------------------------------------
|
|
2117
|
+
// 13. reload — Restart MCP server to pick up rebuilt code
|
|
1362
2118
|
// --------------------------------------------------------------------------
|
|
1363
2119
|
server.registerTool('reload', {
|
|
1364
2120
|
title: 'Reload MCP Server',
|
|
1365
2121
|
description: 'Restart the MCP server to pick up rebuilt code. MCP client will reconnect automatically.',
|
|
2122
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1366
2123
|
inputSchema: {},
|
|
1367
2124
|
}, async () => {
|
|
1368
2125
|
setTimeout(() => process.exit(0), 100);
|