@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.
Files changed (98) hide show
  1. package/README.md +59 -18
  2. package/dist/cloud-executor.d.ts +71 -0
  3. package/dist/cloud-executor.js +335 -0
  4. package/dist/cloud-sync.d.ts +8 -0
  5. package/dist/cloud-sync.js +52 -0
  6. package/dist/config.d.ts +30 -4
  7. package/dist/config.js +61 -2
  8. package/dist/context-builder.d.ts +2 -0
  9. package/dist/context-builder.js +11 -3
  10. package/dist/cost.js +1 -1
  11. package/dist/entitlements/jwt.d.ts +7 -0
  12. package/dist/entitlements/jwt.js +78 -0
  13. package/dist/entitlements/resolve.d.ts +17 -0
  14. package/dist/entitlements/resolve.js +49 -0
  15. package/dist/entitlements/types.d.ts +21 -0
  16. package/dist/entitlements/types.js +4 -0
  17. package/dist/executors/base.d.ts +1 -1
  18. package/dist/executors/bedrock-executor.d.ts +39 -0
  19. package/dist/executors/bedrock-executor.js +197 -0
  20. package/dist/executors/index.d.ts +1 -0
  21. package/dist/executors/index.js +24 -1
  22. package/dist/index.js +468 -23
  23. package/dist/intelligence/index.d.ts +44 -0
  24. package/dist/intelligence/index.js +160 -0
  25. package/dist/key-cache.d.ts +31 -0
  26. package/dist/key-cache.js +84 -0
  27. package/dist/login-helpers.d.ts +25 -0
  28. package/dist/login-helpers.js +54 -0
  29. package/dist/manifest.js +18 -1
  30. package/dist/mcp-instructions.d.ts +1 -0
  31. package/dist/mcp-instructions.js +84 -0
  32. package/dist/mcp-resources.d.ts +8 -0
  33. package/dist/mcp-resources.js +420 -0
  34. package/dist/model-cache.d.ts +18 -0
  35. package/dist/model-cache.js +62 -0
  36. package/dist/model-validator.d.ts +20 -0
  37. package/dist/model-validator.js +125 -0
  38. package/dist/orchestrator.d.ts +14 -0
  39. package/dist/orchestrator.js +191 -32
  40. package/dist/setup/ide-registry.d.ts +13 -0
  41. package/dist/setup/ide-registry.js +51 -0
  42. package/dist/setup/index.d.ts +1 -0
  43. package/dist/setup/index.js +111 -0
  44. package/dist/tier-gating.js +0 -16
  45. package/dist/tiers.d.ts +35 -5
  46. package/dist/tiers.js +39 -3
  47. package/dist/tools.d.ts +6 -1
  48. package/dist/tools.js +852 -95
  49. package/dist/types.d.ts +71 -60
  50. package/dist/types.js +3 -0
  51. package/dist/waves.d.ts +1 -1
  52. package/dist/waves.js +29 -2
  53. package/package.json +41 -5
  54. package/src/entitlements/public-key.pem +9 -0
  55. package/dist/intelligence/anti-pattern-detector.d.ts +0 -117
  56. package/dist/intelligence/anti-pattern-detector.js +0 -327
  57. package/dist/intelligence/budget-enforcer.d.ts +0 -119
  58. package/dist/intelligence/budget-enforcer.js +0 -226
  59. package/dist/intelligence/context-optimizer.d.ts +0 -111
  60. package/dist/intelligence/context-optimizer.js +0 -282
  61. package/dist/intelligence/cost-tracker.d.ts +0 -114
  62. package/dist/intelligence/cost-tracker.js +0 -183
  63. package/dist/intelligence/deliverable-extractor.d.ts +0 -134
  64. package/dist/intelligence/deliverable-extractor.js +0 -909
  65. package/dist/intelligence/dependency-inferrer.d.ts +0 -87
  66. package/dist/intelligence/dependency-inferrer.js +0 -403
  67. package/dist/intelligence/diagnostics.d.ts +0 -33
  68. package/dist/intelligence/diagnostics.js +0 -64
  69. package/dist/intelligence/error-analyzer.d.ts +0 -7
  70. package/dist/intelligence/error-analyzer.js +0 -76
  71. package/dist/intelligence/file-chunker.d.ts +0 -15
  72. package/dist/intelligence/file-chunker.js +0 -64
  73. package/dist/intelligence/fix-stream-manager.d.ts +0 -59
  74. package/dist/intelligence/fix-stream-manager.js +0 -212
  75. package/dist/intelligence/heuristics.d.ts +0 -23
  76. package/dist/intelligence/heuristics.js +0 -124
  77. package/dist/intelligence/learning-engine.d.ts +0 -157
  78. package/dist/intelligence/learning-engine.js +0 -433
  79. package/dist/intelligence/learning-feedback.d.ts +0 -96
  80. package/dist/intelligence/learning-feedback.js +0 -202
  81. package/dist/intelligence/pattern-analyzer.d.ts +0 -35
  82. package/dist/intelligence/pattern-analyzer.js +0 -189
  83. package/dist/intelligence/plan-parser.d.ts +0 -124
  84. package/dist/intelligence/plan-parser.js +0 -498
  85. package/dist/intelligence/planner.d.ts +0 -29
  86. package/dist/intelligence/planner.js +0 -86
  87. package/dist/intelligence/self-healer.d.ts +0 -16
  88. package/dist/intelligence/self-healer.js +0 -84
  89. package/dist/intelligence/slicing-metrics.d.ts +0 -62
  90. package/dist/intelligence/slicing-metrics.js +0 -202
  91. package/dist/intelligence/slicing-templates.d.ts +0 -81
  92. package/dist/intelligence/slicing-templates.js +0 -420
  93. package/dist/intelligence/split-suggester.d.ts +0 -69
  94. package/dist/intelligence/split-suggester.js +0 -176
  95. package/dist/intelligence/stream-generator.d.ts +0 -90
  96. package/dist/intelligence/stream-generator.js +0 -452
  97. package/dist/telemetry/telemetry-types.d.ts +0 -85
  98. 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/error-analyzer.js';
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
- const FREE_LEARN_TRIAL_DAYS = 30;
166
- function checkLearnAccess(ctx) {
167
- const tier = getTier(ctx.userTier);
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 (smartPlanning === 'none') — apply trial logic
173
- const accountAgeDays = (Date.now() - new Date(ctx.userCreatedAt).getTime()) / (1000 * 60 * 60 * 24);
174
- if (accountAgeDays > FREE_LEARN_TRIAL_DAYS) {
175
- return {
176
- allowed: false,
177
- error: `Free trial expired. Your account is ${Math.floor(accountAgeDays)} days old (trial: ${FREE_LEARN_TRIAL_DAYS} days).`,
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 = getTier(context?.userTier ?? 'free');
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 = getTier(context?.userTier ?? 'free');
575
- return ok(`Orchestration: "${manifest.feature}"`, {
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
- exec = createExecutor({ provider });
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
- // Persist run to history store
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
- // Persist run to history store on final wave or failure
883
- if (context?.historyStore && (result.done || !result.next)) {
1041
+ // Generate execution report first (for runId)
1042
+ if (result.done || !result.next) {
1043
+ let report;
884
1044
  try {
885
- const finalManifest = await loadManifest(projectDir);
886
- // Build lookup from current wave's execution results
887
- const waveResultMap = new Map(result.streams.map(sr => [sr.id, sr]));
888
- const streamRecords = Object.entries(finalManifest.streams).map(([sid, s]) => {
889
- const execResult = waveResultMap.get(sid);
890
- return {
891
- streamId: sid,
892
- name: s.name,
893
- status: s.status,
894
- waveNumber: finalManifest.waveAssignments?.[sid] ?? 0,
895
- filesChanged: s.owns ?? [],
896
- tokensInput: execResult?.tokensUsed?.input ?? 0,
897
- tokensOutput: execResult?.tokensUsed?.output ?? 0,
898
- provider: execResult?.provider,
899
- model: execResult?.model,
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 (err) {
913
- toolLog.warn({ err }, 'history_save_failed');
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 planning document and generate stream definitions. Returns proposed streams for review before initialization.',
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
- // Free trial gating (cloud mode only)
1312
+ // Tier gating (both local and cloud modes)
1110
1313
  let learnCallsRemaining;
1111
- if (context) {
1112
- const access = checkLearnAccess(context);
1113
- if (!access.allowed) {
1114
- return fail(access.error, {
1115
- code: 'LEARN_TRIAL_EXPIRED',
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: access.suggestion,
1328
+ suggestion: 'Upgrade to Pro ($19/mo) for unlimited learn calls: /pricing',
1118
1329
  });
1119
1330
  }
1120
- if (context.userTier === 'free') {
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
- if (context) {
1238
- const tier = getTier(context.userTier);
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
- // Extract prerequisites (e.g., package installs) from plan text
1245
- const initFormatStreams = toInitFormat(result);
1246
- // Check for cross-stream ownership conflicts
1247
- diagnostics.ownershipConflicts = detectOwnershipConflicts(result.streams);
1248
- if (diagnostics.ownershipConflicts.length > 0) {
1249
- for (const conflict of diagnostics.ownershipConflicts) {
1250
- diagnostics.warnings.push(`Ownership conflict: ${conflict}`);
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: FREE_LEARN_TRIAL_DAYS,
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 markdown plan template that teaches users how to structure documents for orchex learn. Creates a starter plan file with inline comments explaining what the learn pipeline parses.',
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
- // 9. reloadRestart MCP server to pick up rebuilt code
1577
+ // 10. autoOne-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);