wogiflow 1.0.0
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/.workflow/agents/reviewer.md +81 -0
- package/.workflow/agents/security.md +94 -0
- package/.workflow/agents/story-writer.md +58 -0
- package/.workflow/bridges/base-bridge.js +395 -0
- package/.workflow/bridges/claude-bridge.js +434 -0
- package/.workflow/bridges/index.js +130 -0
- package/.workflow/lib/assumption-detector.js +481 -0
- package/.workflow/lib/config-substitution.js +371 -0
- package/.workflow/lib/failure-categories.js +478 -0
- package/.workflow/state/app-map.md.template +15 -0
- package/.workflow/state/architecture.md.template +24 -0
- package/.workflow/state/component-index.json.template +5 -0
- package/.workflow/state/decisions.md.template +15 -0
- package/.workflow/state/feedback-patterns.md.template +9 -0
- package/.workflow/state/knowledge-sync.json.template +6 -0
- package/.workflow/state/progress.md.template +14 -0
- package/.workflow/state/ready.json.template +7 -0
- package/.workflow/state/request-log.md.template +14 -0
- package/.workflow/state/session-state.json.template +11 -0
- package/.workflow/state/stack.md.template +33 -0
- package/.workflow/state/testing.md.template +36 -0
- package/.workflow/templates/claude-md.hbs +257 -0
- package/.workflow/templates/correction-report.md +67 -0
- package/.workflow/templates/gemini-md.hbs +52 -0
- package/README.md +1802 -0
- package/bin/flow +205 -0
- package/lib/index.js +33 -0
- package/lib/installer.js +467 -0
- package/lib/release-channel.js +269 -0
- package/lib/skill-registry.js +526 -0
- package/lib/upgrader.js +401 -0
- package/lib/utils.js +305 -0
- package/package.json +64 -0
- package/scripts/flow +985 -0
- package/scripts/flow-adaptive-learning.js +1259 -0
- package/scripts/flow-aggregate.js +488 -0
- package/scripts/flow-archive +133 -0
- package/scripts/flow-auto-context.js +1015 -0
- package/scripts/flow-auto-learn.js +615 -0
- package/scripts/flow-bridge.js +223 -0
- package/scripts/flow-browser-suggest.js +316 -0
- package/scripts/flow-bug.js +247 -0
- package/scripts/flow-cascade.js +711 -0
- package/scripts/flow-changelog +85 -0
- package/scripts/flow-checkpoint.js +483 -0
- package/scripts/flow-cli.js +403 -0
- package/scripts/flow-code-intelligence.js +760 -0
- package/scripts/flow-complexity.js +502 -0
- package/scripts/flow-config-set.js +152 -0
- package/scripts/flow-constants.js +157 -0
- package/scripts/flow-context +152 -0
- package/scripts/flow-context-init.js +482 -0
- package/scripts/flow-context-monitor.js +384 -0
- package/scripts/flow-context-scoring.js +886 -0
- package/scripts/flow-correct.js +458 -0
- package/scripts/flow-damage-control.js +985 -0
- package/scripts/flow-deps +101 -0
- package/scripts/flow-diff.js +700 -0
- package/scripts/flow-done +151 -0
- package/scripts/flow-done.js +489 -0
- package/scripts/flow-durable-session.js +1541 -0
- package/scripts/flow-entropy-monitor.js +345 -0
- package/scripts/flow-export-profile +349 -0
- package/scripts/flow-export-scanner.js +1046 -0
- package/scripts/flow-figma-confirm.js +400 -0
- package/scripts/flow-figma-extract.js +496 -0
- package/scripts/flow-figma-generate.js +683 -0
- package/scripts/flow-figma-index.js +909 -0
- package/scripts/flow-figma-match.js +617 -0
- package/scripts/flow-figma-mcp-server.js +518 -0
- package/scripts/flow-figma-pipeline.js +414 -0
- package/scripts/flow-file-ops.js +301 -0
- package/scripts/flow-gate-confidence.js +825 -0
- package/scripts/flow-guided-edit.js +659 -0
- package/scripts/flow-health +185 -0
- package/scripts/flow-health.js +413 -0
- package/scripts/flow-hooks.js +556 -0
- package/scripts/flow-http-client.js +249 -0
- package/scripts/flow-hybrid-detect.js +167 -0
- package/scripts/flow-hybrid-interactive.js +591 -0
- package/scripts/flow-hybrid-test.js +152 -0
- package/scripts/flow-import-profile +439 -0
- package/scripts/flow-init +253 -0
- package/scripts/flow-instruction-richness.js +827 -0
- package/scripts/flow-jira-integration.js +579 -0
- package/scripts/flow-knowledge-router.js +522 -0
- package/scripts/flow-knowledge-sync.js +589 -0
- package/scripts/flow-linear-integration.js +631 -0
- package/scripts/flow-links.js +774 -0
- package/scripts/flow-log-manager.js +559 -0
- package/scripts/flow-loop-enforcer.js +1246 -0
- package/scripts/flow-loop-retry-learning.js +630 -0
- package/scripts/flow-lsp.js +923 -0
- package/scripts/flow-map-index +348 -0
- package/scripts/flow-map-sync +201 -0
- package/scripts/flow-memory-blocks.js +668 -0
- package/scripts/flow-memory-compactor.js +350 -0
- package/scripts/flow-memory-db.js +1110 -0
- package/scripts/flow-memory-sync.js +484 -0
- package/scripts/flow-metrics.js +353 -0
- package/scripts/flow-migrate-ids.js +370 -0
- package/scripts/flow-model-adapter.js +802 -0
- package/scripts/flow-model-router.js +884 -0
- package/scripts/flow-models.js +1231 -0
- package/scripts/flow-morning.js +517 -0
- package/scripts/flow-multi-approach.js +660 -0
- package/scripts/flow-new-feature +86 -0
- package/scripts/flow-onboard +1042 -0
- package/scripts/flow-orchestrate-llm.js +459 -0
- package/scripts/flow-orchestrate.js +3592 -0
- package/scripts/flow-output.js +123 -0
- package/scripts/flow-parallel-detector.js +399 -0
- package/scripts/flow-parallel-dispatch.js +987 -0
- package/scripts/flow-parallel.js +428 -0
- package/scripts/flow-pattern-enforcer.js +600 -0
- package/scripts/flow-prd-manager.js +282 -0
- package/scripts/flow-progress.js +323 -0
- package/scripts/flow-project-analyzer.js +975 -0
- package/scripts/flow-prompt-composer.js +487 -0
- package/scripts/flow-providers.js +1381 -0
- package/scripts/flow-queue.js +308 -0
- package/scripts/flow-ready +82 -0
- package/scripts/flow-ready.js +189 -0
- package/scripts/flow-regression.js +396 -0
- package/scripts/flow-response-parser.js +450 -0
- package/scripts/flow-resume.js +284 -0
- package/scripts/flow-rules-sync.js +439 -0
- package/scripts/flow-run-trace.js +718 -0
- package/scripts/flow-safety.js +587 -0
- package/scripts/flow-search +104 -0
- package/scripts/flow-security.js +481 -0
- package/scripts/flow-session-end +106 -0
- package/scripts/flow-session-end.js +437 -0
- package/scripts/flow-session-state.js +671 -0
- package/scripts/flow-setup-hooks +216 -0
- package/scripts/flow-setup-hooks.js +377 -0
- package/scripts/flow-skill-create.js +329 -0
- package/scripts/flow-skill-creator.js +572 -0
- package/scripts/flow-skill-generator.js +1046 -0
- package/scripts/flow-skill-learn.js +880 -0
- package/scripts/flow-skill-matcher.js +578 -0
- package/scripts/flow-spec-generator.js +820 -0
- package/scripts/flow-stack-wizard.js +895 -0
- package/scripts/flow-standup +162 -0
- package/scripts/flow-start +74 -0
- package/scripts/flow-start.js +235 -0
- package/scripts/flow-status +110 -0
- package/scripts/flow-status.js +301 -0
- package/scripts/flow-step-browser.js +83 -0
- package/scripts/flow-step-changelog.js +217 -0
- package/scripts/flow-step-comments.js +306 -0
- package/scripts/flow-step-complexity.js +234 -0
- package/scripts/flow-step-coverage.js +218 -0
- package/scripts/flow-step-knowledge.js +193 -0
- package/scripts/flow-step-pr-tests.js +364 -0
- package/scripts/flow-step-regression.js +89 -0
- package/scripts/flow-step-review.js +516 -0
- package/scripts/flow-step-security.js +162 -0
- package/scripts/flow-step-silent-failures.js +290 -0
- package/scripts/flow-step-simplifier.js +346 -0
- package/scripts/flow-story +105 -0
- package/scripts/flow-story.js +500 -0
- package/scripts/flow-suspend.js +252 -0
- package/scripts/flow-sync-daemon.js +654 -0
- package/scripts/flow-task-analyzer.js +606 -0
- package/scripts/flow-team-dashboard.js +748 -0
- package/scripts/flow-team-sync.js +752 -0
- package/scripts/flow-team.js +977 -0
- package/scripts/flow-tech-options.js +528 -0
- package/scripts/flow-templates.js +812 -0
- package/scripts/flow-tiered-learning.js +728 -0
- package/scripts/flow-trace +204 -0
- package/scripts/flow-transcript-chunking.js +1106 -0
- package/scripts/flow-transcript-digest.js +7918 -0
- package/scripts/flow-transcript-language.js +465 -0
- package/scripts/flow-transcript-parsing.js +1085 -0
- package/scripts/flow-transcript-stories.js +2194 -0
- package/scripts/flow-update-map +224 -0
- package/scripts/flow-utils.js +2242 -0
- package/scripts/flow-verification.js +644 -0
- package/scripts/flow-verify.js +1177 -0
- package/scripts/flow-voice-input.js +638 -0
- package/scripts/flow-watch +168 -0
- package/scripts/flow-workflow-steps.js +521 -0
- package/scripts/flow-workflow.js +1029 -0
- package/scripts/flow-worktree.js +489 -0
- package/scripts/hooks/adapters/base-adapter.js +102 -0
- package/scripts/hooks/adapters/claude-code.js +359 -0
- package/scripts/hooks/adapters/index.js +79 -0
- package/scripts/hooks/core/component-check.js +341 -0
- package/scripts/hooks/core/index.js +35 -0
- package/scripts/hooks/core/loop-check.js +241 -0
- package/scripts/hooks/core/session-context.js +294 -0
- package/scripts/hooks/core/task-gate.js +177 -0
- package/scripts/hooks/core/validation.js +230 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
- package/scripts/hooks/entry/claude-code/session-end.js +87 -0
- package/scripts/hooks/entry/claude-code/session-start.js +46 -0
- package/scripts/hooks/entry/claude-code/stop.js +43 -0
- package/scripts/postinstall.js +139 -0
- package/templates/browser-test-flow.json +56 -0
- package/templates/bug-report.md +43 -0
- package/templates/component-detail.md +42 -0
- package/templates/component.stories.tsx +49 -0
- package/templates/context/constraints.md +83 -0
- package/templates/context/conventions.md +177 -0
- package/templates/context/stack.md +60 -0
- package/templates/correction-report.md +90 -0
- package/templates/feature-proposal.md +35 -0
- package/templates/hybrid/_base.md +254 -0
- package/templates/hybrid/_patterns.md +45 -0
- package/templates/hybrid/create-component.md +127 -0
- package/templates/hybrid/create-file.md +56 -0
- package/templates/hybrid/create-hook.md +145 -0
- package/templates/hybrid/create-service.md +70 -0
- package/templates/hybrid/fix-bug.md +33 -0
- package/templates/hybrid/modify-file.md +55 -0
- package/templates/story.md +68 -0
- package/templates/task.json +56 -0
- package/templates/trace.md +69 -0
|
@@ -0,0 +1,3592 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Hybrid Mode Orchestrator
|
|
5
|
+
*
|
|
6
|
+
* Executes plans created by Claude using a local LLM.
|
|
7
|
+
* Updates all Wogi Flow state files after each step.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* flow-orchestrate <plan.json> # Execute a plan
|
|
11
|
+
* flow-orchestrate --resume # Resume from checkpoint
|
|
12
|
+
* flow-orchestrate --rollback # Rollback last execution
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { execSync, execFileSync, spawn } = require('child_process');
|
|
18
|
+
const readline = require('readline');
|
|
19
|
+
const { validatePathWithinProject } = require('./flow-security');
|
|
20
|
+
|
|
21
|
+
// Import LLM clients (extracted for modularity)
|
|
22
|
+
const { LocalLLM, CloudExecutor, createExecutor } = require('./flow-orchestrate-llm');
|
|
23
|
+
|
|
24
|
+
// Import complexity assessment module
|
|
25
|
+
const {
|
|
26
|
+
assessTaskComplexity,
|
|
27
|
+
TOKEN_BUDGETS,
|
|
28
|
+
getDefaultTokens,
|
|
29
|
+
clampTokens
|
|
30
|
+
} = require('./flow-complexity');
|
|
31
|
+
|
|
32
|
+
// Import instruction richness module
|
|
33
|
+
const {
|
|
34
|
+
getInstructionRichness,
|
|
35
|
+
getVerbosityGuidance,
|
|
36
|
+
loadProjectContext: loadRichnessContext,
|
|
37
|
+
loadPatterns,
|
|
38
|
+
loadRelevantTypes,
|
|
39
|
+
loadRelatedCode
|
|
40
|
+
} = require('./flow-instruction-richness');
|
|
41
|
+
|
|
42
|
+
// Import export scanner module
|
|
43
|
+
const {
|
|
44
|
+
buildExportMap,
|
|
45
|
+
loadCachedExportMap,
|
|
46
|
+
saveExportMapCache,
|
|
47
|
+
formatExportMapForTemplate,
|
|
48
|
+
validateComponentUsage,
|
|
49
|
+
formatComponentWithUsage,
|
|
50
|
+
setProjectRoot: setExportScannerRoot
|
|
51
|
+
} = require('./flow-export-scanner');
|
|
52
|
+
|
|
53
|
+
// Import utilities for consistent project root, colors, and config
|
|
54
|
+
const { getProjectRoot, colors, getConfig, writeJson } = require('./flow-utils');
|
|
55
|
+
const { getPromptAdjustments, recordModelResult } = require('./flow-model-adapter');
|
|
56
|
+
|
|
57
|
+
// Import provider infrastructure for cloud executors
|
|
58
|
+
const {
|
|
59
|
+
createExecutorFromConfig,
|
|
60
|
+
getExecutorConfig,
|
|
61
|
+
MODEL_CAPABILITIES,
|
|
62
|
+
getModelContextLimit
|
|
63
|
+
} = require('./flow-providers');
|
|
64
|
+
|
|
65
|
+
// Import response parser for error recovery
|
|
66
|
+
const { parseOnRetry, cleanCodeBlock } = require('./flow-response-parser');
|
|
67
|
+
|
|
68
|
+
// Import adaptive learning for smart retries and model improvement
|
|
69
|
+
const {
|
|
70
|
+
analyzeFailure,
|
|
71
|
+
refinePromptForRetry,
|
|
72
|
+
recordSuccessfulRecovery,
|
|
73
|
+
ERROR_CATEGORIES
|
|
74
|
+
} = require('./flow-adaptive-learning');
|
|
75
|
+
|
|
76
|
+
// Import pattern enforcer for active learning enforcement
|
|
77
|
+
const {
|
|
78
|
+
injectPatterns,
|
|
79
|
+
extractRelevantPatterns,
|
|
80
|
+
validateAgainstPatterns,
|
|
81
|
+
generateSessionSummary
|
|
82
|
+
} = require('./flow-pattern-enforcer');
|
|
83
|
+
|
|
84
|
+
// v2.0: Import durable session for unified step tracking
|
|
85
|
+
const durableSession = require('./flow-durable-session');
|
|
86
|
+
|
|
87
|
+
// ============================================================
|
|
88
|
+
// Configuration
|
|
89
|
+
// ============================================================
|
|
90
|
+
|
|
91
|
+
const PROJECT_ROOT = getProjectRoot();
|
|
92
|
+
|
|
93
|
+
// Set export scanner project root to match orchestrator's
|
|
94
|
+
setExportScannerRoot(PROJECT_ROOT);
|
|
95
|
+
const WORKFLOW_DIR = path.join(PROJECT_ROOT, '.workflow');
|
|
96
|
+
const STATE_DIR = path.join(WORKFLOW_DIR, 'state');
|
|
97
|
+
const TEMPLATES_DIR = path.join(PROJECT_ROOT, 'templates', 'hybrid');
|
|
98
|
+
|
|
99
|
+
function log(color, ...args) {
|
|
100
|
+
console.log(colors[color] + args.join(' ') + colors.reset);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ============================================================
|
|
104
|
+
// Structured Failure Output
|
|
105
|
+
// ============================================================
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Save structured failure info for retry context
|
|
109
|
+
* This helps the AI understand what failed and how to fix it
|
|
110
|
+
*/
|
|
111
|
+
function saveStructuredFailure(step, errorHistory, attempts, config) {
|
|
112
|
+
const failurePath = path.join(STATE_DIR, 'last-failure.json');
|
|
113
|
+
|
|
114
|
+
const failureInfo = {
|
|
115
|
+
timestamp: new Date().toISOString(),
|
|
116
|
+
taskId: step.taskId || step.description || 'unknown',
|
|
117
|
+
stepAction: step.action || 'unknown',
|
|
118
|
+
targetFile: step.file || null,
|
|
119
|
+
attempts: attempts,
|
|
120
|
+
maxRetries: config.maxRetries,
|
|
121
|
+
model: config.model,
|
|
122
|
+
errors: errorHistory.slice(-5).map(e => ({
|
|
123
|
+
category: e.category,
|
|
124
|
+
signature: e.signature,
|
|
125
|
+
message: err.message?.slice(0, 500) || ''
|
|
126
|
+
})),
|
|
127
|
+
suggestion: generateFixSuggestion(errorHistory),
|
|
128
|
+
lastErrorCategory: errorHistory[errorHistory.length - 1]?.category || 'unknown'
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
fs.writeFileSync(failurePath, JSON.stringify(failureInfo, null, 2));
|
|
133
|
+
log('dim', ` 📝 Failure context saved to ${failurePath}`);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
log('dim', ` ⚠️ Could not save failure context: ${err.message}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return failureInfo;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Generate a fix suggestion based on error history
|
|
143
|
+
*/
|
|
144
|
+
function generateFixSuggestion(errorHistory) {
|
|
145
|
+
if (!errorHistory || errorHistory.length === 0) {
|
|
146
|
+
return 'Review the task requirements and try again';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const lastError = errorHistory[errorHistory.length - 1];
|
|
150
|
+
const errorCounts = {};
|
|
151
|
+
|
|
152
|
+
for (const e of errorHistory) {
|
|
153
|
+
errorCounts[e.category] = (errorCounts[e.category] || 0) + 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const mostCommon = Object.entries(errorCounts)
|
|
157
|
+
.sort((a, b) => b[1] - a[1])[0];
|
|
158
|
+
|
|
159
|
+
const suggestions = {
|
|
160
|
+
import: 'Check import paths match the Available Imports section exactly',
|
|
161
|
+
type: 'Verify prop types match the component definitions',
|
|
162
|
+
syntax: 'Ensure output is pure code without markdown or explanations',
|
|
163
|
+
runtime: 'Check for null/undefined handling and async/await usage',
|
|
164
|
+
unknown: 'Review the error message for specific guidance'
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
return suggestions[mostCommon?.[0]] || suggestions.unknown;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ============================================================
|
|
171
|
+
// Config Loader (uses centralized getConfig from flow-utils)
|
|
172
|
+
// ============================================================
|
|
173
|
+
|
|
174
|
+
function loadHybridConfig() {
|
|
175
|
+
const config = getConfig();
|
|
176
|
+
const hybrid = config.hybrid || {};
|
|
177
|
+
|
|
178
|
+
if (!hybrid.enabled) {
|
|
179
|
+
throw new Error('Hybrid mode is not enabled. Run /wogi-hybrid first.');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Use getExecutorConfig to normalize legacy vs new config format
|
|
183
|
+
const executorConfig = getExecutorConfig(hybrid);
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
// Executor identification (new format)
|
|
187
|
+
executorType: executorConfig.type || 'local', // 'local' or 'cloud'
|
|
188
|
+
provider: executorConfig.provider || 'ollama',
|
|
189
|
+
endpoint: executorConfig.endpoint || 'http://localhost:11434',
|
|
190
|
+
model: executorConfig.model || '',
|
|
191
|
+
apiKey: executorConfig.apiKey || null, // For cloud providers
|
|
192
|
+
|
|
193
|
+
// Planner settings
|
|
194
|
+
adaptToExecutor: hybrid.planner?.adaptToExecutor ?? true,
|
|
195
|
+
useAdapterKnowledge: hybrid.planner?.useAdapterKnowledge ?? true,
|
|
196
|
+
|
|
197
|
+
// Execution settings
|
|
198
|
+
temperature: hybrid.settings?.temperature ?? 0.7,
|
|
199
|
+
// Cloud models may have different token limits
|
|
200
|
+
maxTokens: hybrid.settings?.maxTokens ?? (executorConfig.type === 'cloud' ? 4096 : 16384),
|
|
201
|
+
maxRetries: hybrid.settings?.maxRetries ?? 20,
|
|
202
|
+
timeout: hybrid.settings?.timeout ?? (executorConfig.type === 'cloud' ? 60000 : 120000),
|
|
203
|
+
autoExecute: hybrid.settings?.autoExecute ?? false,
|
|
204
|
+
// Context window can be overridden in config, otherwise auto-detected from model
|
|
205
|
+
contextWindow: hybrid.settings?.contextWindow || null,
|
|
206
|
+
// Instruction richness settings
|
|
207
|
+
instructionRichness: hybrid.settings?.instructionRichness || {},
|
|
208
|
+
|
|
209
|
+
// Cloud provider reference (for model selection in setup wizard)
|
|
210
|
+
cloudProviders: hybrid.cloudProviders || config.hybrid?.cloudProviders || {}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ============================================================
|
|
215
|
+
// Code Extraction
|
|
216
|
+
// ============================================================
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Extracts clean code from LLM response.
|
|
220
|
+
* Handles:
|
|
221
|
+
* - Thinking/reasoning preamble
|
|
222
|
+
* - </think> tags (from models that use thinking tokens)
|
|
223
|
+
* - Markdown code blocks
|
|
224
|
+
* - Trailing explanations
|
|
225
|
+
* - Model-specific artifacts (Llama, Qwen, DeepSeek, etc.)
|
|
226
|
+
* - JSON wrapper responses
|
|
227
|
+
* - Multiple code blocks (selects largest/most relevant)
|
|
228
|
+
*/
|
|
229
|
+
function extractCodeFromResponse(response, modelName = '') {
|
|
230
|
+
if (!response || typeof response !== 'string') {
|
|
231
|
+
return response;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const rawResponse = response;
|
|
235
|
+
let code = response;
|
|
236
|
+
|
|
237
|
+
// 0. Handle JSON wrapper responses (some models wrap code in JSON)
|
|
238
|
+
try {
|
|
239
|
+
const jsonMatch = code.match(/^\s*\{[\s\S]*"code"\s*:\s*"([\s\S]*)"[\s\S]*\}\s*$/);
|
|
240
|
+
if (jsonMatch) {
|
|
241
|
+
code = JSON.parse(`"${jsonMatch[1]}"`); // Unescape JSON string
|
|
242
|
+
}
|
|
243
|
+
} catch { /* not JSON wrapped */ }
|
|
244
|
+
|
|
245
|
+
// 1. Remove model-specific thinking tags and artifacts
|
|
246
|
+
const thinkingPatterns = [
|
|
247
|
+
// Standard thinking tags
|
|
248
|
+
/<think>[\s\S]*?<\/think>/gi,
|
|
249
|
+
/<thinking>[\s\S]*?<\/thinking>/gi,
|
|
250
|
+
/<reasoning>[\s\S]*?<\/reasoning>/gi,
|
|
251
|
+
/<analysis>[\s\S]*?<\/analysis>/gi,
|
|
252
|
+
|
|
253
|
+
// Qwen-specific
|
|
254
|
+
/<\|im_start\|>[\s\S]*?<\|im_end\|>/gi,
|
|
255
|
+
|
|
256
|
+
// DeepSeek-specific artifacts
|
|
257
|
+
/^<\|begin_of_sentence\|>/gm,
|
|
258
|
+
/<\|end_of_sentence\|>$/gm,
|
|
259
|
+
|
|
260
|
+
// Llama-specific
|
|
261
|
+
/\[INST\][\s\S]*?\[\/INST\]/gi,
|
|
262
|
+
/<<SYS>>[\s\S]*?<<\/SYS>>/gi,
|
|
263
|
+
|
|
264
|
+
// Generic assistant markers
|
|
265
|
+
/^Assistant:\s*/gim,
|
|
266
|
+
/^AI:\s*/gim,
|
|
267
|
+
/^Response:\s*/gim,
|
|
268
|
+
/^Output:\s*/gim,
|
|
269
|
+
/^Answer:\s*/gim,
|
|
270
|
+
/^Code:\s*/gim,
|
|
271
|
+
|
|
272
|
+
// Model-specific trailing signatures
|
|
273
|
+
/---\s*End of (response|code|file)[\s\S]*$/gi,
|
|
274
|
+
/\n\nPlease let me know[\s\S]*$/gi,
|
|
275
|
+
/\n\nIs there anything[\s\S]*$/gi,
|
|
276
|
+
/\n\nFeel free to[\s\S]*$/gi,
|
|
277
|
+
/\n\nLet me know if[\s\S]*$/gi,
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
for (const pattern of thinkingPatterns) {
|
|
281
|
+
code = code.replace(pattern, '');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 2. Handle </think> tag (if partial tag remains)
|
|
285
|
+
const thinkEndMatch = code.match(/<\/think>\s*/i);
|
|
286
|
+
if (thinkEndMatch) {
|
|
287
|
+
code = code.slice(thinkEndMatch.index + thinkEndMatch[0].length);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 3. Extract from markdown code blocks
|
|
291
|
+
// Find all code blocks and pick the best one
|
|
292
|
+
const codeBlocks = [...code.matchAll(/```(?:typescript|tsx|ts|javascript|jsx|js|plaintext)?\s*\n([\s\S]*?)```/g)];
|
|
293
|
+
|
|
294
|
+
if (codeBlocks.length > 0) {
|
|
295
|
+
// Score each block and pick the best one
|
|
296
|
+
let bestBlock = codeBlocks[0][1];
|
|
297
|
+
let bestScore = scoreCodeBlock(bestBlock);
|
|
298
|
+
|
|
299
|
+
for (let i = 1; i < codeBlocks.length; i++) {
|
|
300
|
+
const blockContent = codeBlocks[i][1];
|
|
301
|
+
const score = scoreCodeBlock(blockContent);
|
|
302
|
+
if (score > bestScore) {
|
|
303
|
+
bestScore = score;
|
|
304
|
+
bestBlock = blockContent;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
code = bestBlock;
|
|
308
|
+
} else {
|
|
309
|
+
// Also try to remove any remaining markdown code block markers
|
|
310
|
+
code = code.replace(/^```(?:typescript|tsx|javascript|jsx|ts|js|plaintext)?\n/gm, '');
|
|
311
|
+
code = code.replace(/\n```$/gm, '');
|
|
312
|
+
code = code.replace(/^```$/gm, '');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 4. Find first valid TypeScript/JavaScript line
|
|
316
|
+
const validStartPatterns = [
|
|
317
|
+
/^import\s/m,
|
|
318
|
+
/^export\s/m,
|
|
319
|
+
/^const\s/m,
|
|
320
|
+
/^let\s/m,
|
|
321
|
+
/^var\s/m,
|
|
322
|
+
/^function\s/m,
|
|
323
|
+
/^async\s+function\s/m,
|
|
324
|
+
/^class\s/m,
|
|
325
|
+
/^interface\s/m,
|
|
326
|
+
/^type\s/m,
|
|
327
|
+
/^enum\s/m,
|
|
328
|
+
/^declare\s/m,
|
|
329
|
+
/^module\s/m,
|
|
330
|
+
/^namespace\s/m,
|
|
331
|
+
/^\/\*\*/m, // JSDoc comment
|
|
332
|
+
/^\/\*[^*]/m, // Block comment
|
|
333
|
+
/^\/\//m, // Single line comment at start
|
|
334
|
+
/^'use /m, // 'use strict' or 'use client'
|
|
335
|
+
/^"use /m,
|
|
336
|
+
/^@/m, // Decorators
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
let earliestMatch = -1;
|
|
340
|
+
for (const pattern of validStartPatterns) {
|
|
341
|
+
const match = code.search(pattern);
|
|
342
|
+
if (match !== -1 && (earliestMatch === -1 || match < earliestMatch)) {
|
|
343
|
+
earliestMatch = match;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (earliestMatch > 0) {
|
|
348
|
+
code = code.slice(earliestMatch);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 5. Remove trailing explanations and prose
|
|
352
|
+
const trailingPatterns = [
|
|
353
|
+
// Standard prose after code
|
|
354
|
+
/(\}|\;)\s*\n\s*\n+[A-Z][a-z]/,
|
|
355
|
+
// Numbered explanations
|
|
356
|
+
/(\}|\;)\s*\n\s*\n+\d+\.\s+/,
|
|
357
|
+
// Bullet points
|
|
358
|
+
/(\}|\;)\s*\n\s*\n+[-*•]\s+/,
|
|
359
|
+
// Notes/explanations
|
|
360
|
+
/(\}|\;)\s*\n\s*\n+(?:Note:|Explanation:|Summary:|Key |Important:)/i,
|
|
361
|
+
];
|
|
362
|
+
|
|
363
|
+
for (const pattern of trailingPatterns) {
|
|
364
|
+
const match = code.match(pattern);
|
|
365
|
+
if (match) {
|
|
366
|
+
code = code.slice(0, match.index + 1);
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 6. Clean up common artifacts
|
|
372
|
+
code = code
|
|
373
|
+
// Remove zero-width characters
|
|
374
|
+
.replace(/[\u200B-\u200D\uFEFF]/g, '')
|
|
375
|
+
// Normalize line endings
|
|
376
|
+
.replace(/\r\n/g, '\n')
|
|
377
|
+
.replace(/\r/g, '\n')
|
|
378
|
+
// Remove trailing whitespace on each line
|
|
379
|
+
.replace(/[ \t]+$/gm, '')
|
|
380
|
+
// Collapse multiple blank lines to max 2
|
|
381
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
382
|
+
.trim();
|
|
383
|
+
|
|
384
|
+
// Debug logging
|
|
385
|
+
if (process.env.DEBUG_HYBRID) {
|
|
386
|
+
console.log('\n--- RAW LLM RESPONSE (first 500 chars) ---');
|
|
387
|
+
console.log(rawResponse.slice(0, 500));
|
|
388
|
+
console.log('\n--- EXTRACTED CODE (first 500 chars) ---');
|
|
389
|
+
console.log(code.slice(0, 500));
|
|
390
|
+
console.log('---\n');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return code;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Score a code block to determine which is most likely the actual code
|
|
398
|
+
* Higher score = more likely to be the real code
|
|
399
|
+
*/
|
|
400
|
+
function scoreCodeBlock(block) {
|
|
401
|
+
if (!block) return 0;
|
|
402
|
+
|
|
403
|
+
let score = 0;
|
|
404
|
+
|
|
405
|
+
// Length bonus (longer is usually better, but cap it)
|
|
406
|
+
score += Math.min(block.length / 100, 50);
|
|
407
|
+
|
|
408
|
+
// Valid code patterns
|
|
409
|
+
if (/^import\s/m.test(block)) score += 20;
|
|
410
|
+
if (/^export\s/m.test(block)) score += 20;
|
|
411
|
+
if (/^const\s/m.test(block)) score += 10;
|
|
412
|
+
if (/^function\s/m.test(block)) score += 10;
|
|
413
|
+
if (/^class\s/m.test(block)) score += 10;
|
|
414
|
+
if (/^interface\s/m.test(block)) score += 15;
|
|
415
|
+
if (/^type\s/m.test(block)) score += 10;
|
|
416
|
+
|
|
417
|
+
// Code structure indicators
|
|
418
|
+
score += (block.match(/\{/g) || []).length * 2;
|
|
419
|
+
score += (block.match(/\}/g) || []).length * 2;
|
|
420
|
+
score += (block.match(/=>/g) || []).length * 3;
|
|
421
|
+
score += (block.match(/return\s/g) || []).length * 3;
|
|
422
|
+
|
|
423
|
+
// Penalties for prose/non-code
|
|
424
|
+
if (/^[A-Z][a-z]+\s+[a-z]+/m.test(block)) score -= 10; // Starts with prose
|
|
425
|
+
if (/\.$/.test(block.trim())) score -= 5; // Ends with period (prose)
|
|
426
|
+
|
|
427
|
+
return score;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Validates if the extracted code looks like valid TypeScript/JavaScript.
|
|
432
|
+
* Returns { valid: boolean, reason?: string }
|
|
433
|
+
*/
|
|
434
|
+
function isValidCode(code) {
|
|
435
|
+
if (!code) {
|
|
436
|
+
return { valid: false, reason: 'Empty output' };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (code.length < 10) {
|
|
440
|
+
return { valid: false, reason: 'Output too short' };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const trimmed = code.trim();
|
|
444
|
+
|
|
445
|
+
// Check for common LLM prose patterns that indicate thinking/explanation
|
|
446
|
+
const prosePatterns = [
|
|
447
|
+
/^(We need|Let's|The |I |You |This |Maybe|Probably|Actually|But |So |Thus |Given |Here|Now |First|To |In order)/i,
|
|
448
|
+
/^(Looking at|Based on|According to|As you can|Note that|Remember|Consider|Thinking|Output:)/i,
|
|
449
|
+
/^(```|~~~)/, // Markdown code fence at start means extraction failed
|
|
450
|
+
/<think>|<\/think>/i, // Thinking tags leaked through
|
|
451
|
+
];
|
|
452
|
+
|
|
453
|
+
for (const pattern of prosePatterns) {
|
|
454
|
+
if (pattern.test(trimmed)) {
|
|
455
|
+
return { valid: false, reason: `Starts with prose/thinking: "${trimmed.slice(0, 50)}..."` };
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Must start with valid TS/JS syntax
|
|
460
|
+
const validStartPatterns = /^(import|export|const|let|var|function|async|class|interface|type|enum|declare|module|namespace|\/\*\*|\/\*|\/\/|'use |"use |@)/;
|
|
461
|
+
|
|
462
|
+
if (!validStartPatterns.test(trimmed)) {
|
|
463
|
+
return { valid: false, reason: `Invalid start: "${trimmed.slice(0, 50)}..."` };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Additional sanity checks
|
|
467
|
+
// Should have some code-like structure (braces, semicolons, etc.)
|
|
468
|
+
const hasCodeStructure = /[{};=()]/.test(code);
|
|
469
|
+
if (!hasCodeStructure && code.length > 100) {
|
|
470
|
+
return { valid: false, reason: 'No code structure detected (missing braces/semicolons)' };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return { valid: true };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ============================================================
|
|
477
|
+
// Semantic Output Validation
|
|
478
|
+
// ============================================================
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Validates that the output semantically matches what was requested.
|
|
482
|
+
* This catches cases where the code is syntactically valid but implements
|
|
483
|
+
* the wrong thing (e.g., creating ApprovalChain instead of Button).
|
|
484
|
+
*
|
|
485
|
+
* @param {string} code - The generated code
|
|
486
|
+
* @param {Object} step - The step definition containing type and params
|
|
487
|
+
* @returns {{ valid: boolean, reason?: string, confidence: number }}
|
|
488
|
+
*/
|
|
489
|
+
function validateOutputMatchesTask(code, step) {
|
|
490
|
+
if (!code || !step) {
|
|
491
|
+
return { valid: true, confidence: 0 }; // Can't validate without info
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const stepType = step.type;
|
|
495
|
+
const expectedName = step.params?.name || step.params?.componentName || '';
|
|
496
|
+
const targetPath = step.params?.path || '';
|
|
497
|
+
const codeLower = code.toLowerCase();
|
|
498
|
+
const issues = [];
|
|
499
|
+
let confidence = 100;
|
|
500
|
+
|
|
501
|
+
// Extract the expected filename/component name from path
|
|
502
|
+
const fileBaseName = targetPath
|
|
503
|
+
? path.basename(targetPath, path.extname(targetPath))
|
|
504
|
+
: expectedName;
|
|
505
|
+
|
|
506
|
+
// 1. Check if expected name appears in the code
|
|
507
|
+
if (fileBaseName && fileBaseName.length > 2) {
|
|
508
|
+
const namePattern = new RegExp(`\\b${escapeRegex(fileBaseName)}\\b`, 'i');
|
|
509
|
+
if (!namePattern.test(code)) {
|
|
510
|
+
issues.push(`Expected "${fileBaseName}" not found in output`);
|
|
511
|
+
confidence -= 40;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// 2. Check step-type specific patterns
|
|
516
|
+
switch (stepType) {
|
|
517
|
+
case 'create-component':
|
|
518
|
+
// Should have a function/const that exports a component
|
|
519
|
+
if (!/export\s+(default\s+)?function|export\s+(default\s+)?const/.test(code)) {
|
|
520
|
+
issues.push('No exported function/const found for component');
|
|
521
|
+
confidence -= 30;
|
|
522
|
+
}
|
|
523
|
+
// Should have JSX (tsx file)
|
|
524
|
+
if (targetPath.endsWith('.tsx') && !/<[A-Z]|<[a-z]+\s|<\//.test(code)) {
|
|
525
|
+
issues.push('No JSX found in .tsx component');
|
|
526
|
+
confidence -= 20;
|
|
527
|
+
}
|
|
528
|
+
break;
|
|
529
|
+
|
|
530
|
+
case 'create-hook':
|
|
531
|
+
// Should have a use* function
|
|
532
|
+
if (!/function\s+use[A-Z]|const\s+use[A-Z]/.test(code)) {
|
|
533
|
+
issues.push('No use* hook function found');
|
|
534
|
+
confidence -= 50;
|
|
535
|
+
}
|
|
536
|
+
break;
|
|
537
|
+
|
|
538
|
+
case 'create-service':
|
|
539
|
+
// Should have exports (functions or class)
|
|
540
|
+
if (!/export\s+(const|function|class|async)/.test(code)) {
|
|
541
|
+
issues.push('No exports found in service');
|
|
542
|
+
confidence -= 30;
|
|
543
|
+
}
|
|
544
|
+
break;
|
|
545
|
+
|
|
546
|
+
case 'modify-file':
|
|
547
|
+
// For modifications, the expected changes should be present
|
|
548
|
+
// This is harder to validate without more context
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// 3. Check for common "wrong thing" patterns
|
|
553
|
+
// If the code exports something completely different from expected name
|
|
554
|
+
const exportMatches = code.match(/export\s+(?:default\s+)?(?:function|const|class)\s+(\w+)/g) || [];
|
|
555
|
+
if (exportMatches.length > 0 && fileBaseName) {
|
|
556
|
+
const exportNames = exportMatches.map(m => {
|
|
557
|
+
const parts = m.split(/\s+/);
|
|
558
|
+
return parts[parts.length - 1];
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// Check if any export is similar to expected name
|
|
562
|
+
const hasMatchingExport = exportNames.some(name =>
|
|
563
|
+
name.toLowerCase().includes(fileBaseName.toLowerCase()) ||
|
|
564
|
+
fileBaseName.toLowerCase().includes(name.toLowerCase())
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
if (!hasMatchingExport && exportNames.length > 0) {
|
|
568
|
+
issues.push(`Exports [${exportNames.join(', ')}] but expected "${fileBaseName}"`);
|
|
569
|
+
confidence -= 30;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Validation result
|
|
574
|
+
const valid = confidence >= 50;
|
|
575
|
+
return {
|
|
576
|
+
valid,
|
|
577
|
+
reason: issues.length > 0 ? issues.join('; ') : undefined,
|
|
578
|
+
confidence,
|
|
579
|
+
issues
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Escapes special regex characters in a string
|
|
585
|
+
*/
|
|
586
|
+
function escapeRegex(string) {
|
|
587
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ============================================================
|
|
591
|
+
// Import Validation (Config-Driven)
|
|
592
|
+
// ============================================================
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Validates imports in generated code against the export map.
|
|
596
|
+
* Uses the cached export map for accurate import validation.
|
|
597
|
+
*
|
|
598
|
+
* @param {string} code - The generated code
|
|
599
|
+
* @param {Object} exportMap - The export map (or null to load from cache)
|
|
600
|
+
* @returns {{ valid: boolean, errors: string[], warnings: string[] }}
|
|
601
|
+
*/
|
|
602
|
+
function validateImports(code, exportMap = null) {
|
|
603
|
+
const errors = [];
|
|
604
|
+
const warnings = [];
|
|
605
|
+
|
|
606
|
+
// Load export map if not provided
|
|
607
|
+
if (!exportMap) {
|
|
608
|
+
exportMap = loadCachedExportMap();
|
|
609
|
+
if (!exportMap) {
|
|
610
|
+
// No export map available, can't validate
|
|
611
|
+
return { valid: true, errors: [], warnings: ['No export map available for validation'] };
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Load doNotImport from config
|
|
616
|
+
let doNotImport = ['React']; // Default
|
|
617
|
+
try {
|
|
618
|
+
const config = getConfig();
|
|
619
|
+
doNotImport = config.hybrid?.projectContext?.doNotImport || ['React'];
|
|
620
|
+
} catch {}
|
|
621
|
+
|
|
622
|
+
// Build a lookup map for all exports by import path
|
|
623
|
+
const exportsByPath = new Map();
|
|
624
|
+
|
|
625
|
+
// Add all exports from the map
|
|
626
|
+
for (const [category, items] of Object.entries(exportMap)) {
|
|
627
|
+
if (category === '_meta') continue;
|
|
628
|
+
|
|
629
|
+
for (const [name, info] of Object.entries(items)) {
|
|
630
|
+
if (!info.importPath) continue;
|
|
631
|
+
|
|
632
|
+
const exports = [];
|
|
633
|
+
if (info.exports?.length > 0) exports.push(...info.exports);
|
|
634
|
+
if (info.types?.length > 0) exports.push(...info.types);
|
|
635
|
+
if (info.defaultExport) exports.push(info.defaultExport);
|
|
636
|
+
|
|
637
|
+
exportsByPath.set(info.importPath, {
|
|
638
|
+
name,
|
|
639
|
+
exports,
|
|
640
|
+
defaultExport: info.defaultExport,
|
|
641
|
+
category
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Extract imports from code
|
|
647
|
+
const importMatches = code.match(/import\s+(?:type\s+)?(?:{[^}]*}|[\w*]+)?\s*(?:,\s*{[^}]*})?\s*from\s+['"]([^'"]+)['"]/g) || [];
|
|
648
|
+
|
|
649
|
+
for (const importLine of importMatches) {
|
|
650
|
+
// Extract the import path
|
|
651
|
+
const pathMatch = importLine.match(/from\s+['"]([^'"]+)['"]/);
|
|
652
|
+
if (!pathMatch) continue;
|
|
653
|
+
|
|
654
|
+
const importPath = pathMatch[1];
|
|
655
|
+
|
|
656
|
+
// Skip external packages
|
|
657
|
+
if (!importPath.startsWith('@/') && !importPath.startsWith('./') && !importPath.startsWith('../')) {
|
|
658
|
+
// Check doNotImport for external packages
|
|
659
|
+
for (const forbidden of doNotImport) {
|
|
660
|
+
if (importLine.includes(`import ${forbidden} `) ||
|
|
661
|
+
importLine.includes(`import ${forbidden},`) ||
|
|
662
|
+
importLine.includes(`import * as ${forbidden}`)) {
|
|
663
|
+
errors.push(`Forbidden import detected: "import ${forbidden}" - use named imports instead`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Check if import path exists in our export map
|
|
670
|
+
const knownExports = exportsByPath.get(importPath);
|
|
671
|
+
|
|
672
|
+
if (!knownExports) {
|
|
673
|
+
// Path not in export map - might be a relative import or unknown path
|
|
674
|
+
if (importPath.startsWith('@/')) {
|
|
675
|
+
warnings.push(`Import path "${importPath}" not found in export map - verify it exists`);
|
|
676
|
+
}
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Extract what's being imported
|
|
681
|
+
const namedImportsMatch = importLine.match(/{([^}]+)}/);
|
|
682
|
+
if (namedImportsMatch) {
|
|
683
|
+
const importedNames = namedImportsMatch[1]
|
|
684
|
+
.split(',')
|
|
685
|
+
.map(n => n.trim().split(/\s+as\s+/)[0].trim()) // Handle "X as Y"
|
|
686
|
+
.filter(n => n && n !== 'type'); // Filter out 'type' keyword
|
|
687
|
+
|
|
688
|
+
const availableExports = knownExports.exports || [];
|
|
689
|
+
|
|
690
|
+
for (const importedName of importedNames) {
|
|
691
|
+
if (importedName && !availableExports.includes(importedName)) {
|
|
692
|
+
const suggestions = availableExports.slice(0, 5).join(', ');
|
|
693
|
+
errors.push(`"${importedName}" is not exported by "${importPath}" - available: ${suggestions}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Check default import
|
|
699
|
+
const defaultImportMatch = importLine.match(/import\s+(\w+)\s*(?:,|from)/);
|
|
700
|
+
if (defaultImportMatch) {
|
|
701
|
+
const defaultImportName = defaultImportMatch[1];
|
|
702
|
+
if (defaultImportName !== 'type' && !knownExports.defaultExport) {
|
|
703
|
+
// Check if they might want a named export
|
|
704
|
+
if (knownExports.exports.includes(defaultImportName)) {
|
|
705
|
+
warnings.push(`"${defaultImportName}" is a named export, not default - use: import { ${defaultImportName} } from '${importPath}'`);
|
|
706
|
+
} else {
|
|
707
|
+
errors.push(`"${importPath}" has no default export - use named imports instead`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
return {
|
|
714
|
+
valid: errors.length === 0,
|
|
715
|
+
errors,
|
|
716
|
+
warnings
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// ============================================================
|
|
721
|
+
// Auto-Correction for Common LLM Mistakes
|
|
722
|
+
// ============================================================
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Gets project context from config for auto-correction and templates.
|
|
726
|
+
* Returns the projectContext section from config.json hybrid settings.
|
|
727
|
+
*/
|
|
728
|
+
function getProjectContext() {
|
|
729
|
+
try {
|
|
730
|
+
const config = getConfig();
|
|
731
|
+
return config.hybrid?.projectContext || {};
|
|
732
|
+
} catch (err) {
|
|
733
|
+
return {};
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Auto-corrects common LLM mistakes in generated code.
|
|
739
|
+
* Runs before file write to fix predictable errors.
|
|
740
|
+
*
|
|
741
|
+
* Uses config.json → hybrid.projectContext for project-specific corrections.
|
|
742
|
+
* Falls back to sensible defaults if no config exists.
|
|
743
|
+
*/
|
|
744
|
+
function autoCorrectCode(code, filePath, projectConfig = null) {
|
|
745
|
+
if (!code || typeof code !== 'string') {
|
|
746
|
+
return { corrected: code, corrections: [] };
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Load project context from config if not provided
|
|
750
|
+
const ctx = projectConfig?.projectContext || getProjectContext();
|
|
751
|
+
|
|
752
|
+
let corrected = code;
|
|
753
|
+
const corrections = [];
|
|
754
|
+
|
|
755
|
+
// 1. Remove forbidden imports (from config, defaults to ['React'])
|
|
756
|
+
const doNotImport = ctx.doNotImport || ['React'];
|
|
757
|
+
for (const forbidden of doNotImport) {
|
|
758
|
+
// Case A: Default import - "import X from '...'"
|
|
759
|
+
const defaultImportRegex = new RegExp(`^import ${forbidden} from ['"][^'"]+['"];?\\s*\\n?`, 'gm');
|
|
760
|
+
if (defaultImportRegex.test(corrected)) {
|
|
761
|
+
corrected = corrected.replace(defaultImportRegex, '');
|
|
762
|
+
corrections.push(`Removed forbidden import: ${forbidden}`);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Case B: Combined with named imports - "import X, { y, z } from '...'"
|
|
766
|
+
const combinedImportRegex = new RegExp(`^import ${forbidden},\\s*(\\{[^}]+\\})\\s+from\\s+(['"][^'"]+['"])`, 'gm');
|
|
767
|
+
if (combinedImportRegex.test(corrected)) {
|
|
768
|
+
corrected = corrected.replace(combinedImportRegex, 'import $1 from $2');
|
|
769
|
+
corrections.push(`Removed ${forbidden} from combined import`);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Case C: Namespace import - "import * as X from '...'"
|
|
773
|
+
const namespaceImportRegex = new RegExp(`^import \\* as ${forbidden} from ['"][^'"]+['"];?\\s*\\n?`, 'gm');
|
|
774
|
+
if (namespaceImportRegex.test(corrected)) {
|
|
775
|
+
corrected = corrected.replace(namespaceImportRegex, '');
|
|
776
|
+
corrections.push(`Removed namespace import: ${forbidden}`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// 2. Fix component paths based on config mappings
|
|
781
|
+
const componentPaths = ctx.componentPaths || {};
|
|
782
|
+
|
|
783
|
+
// Build reverse mapping from shadcn-style to project paths
|
|
784
|
+
// @/components/ui/button → project's Button path
|
|
785
|
+
const shadcnPattern = /@\/components\/ui\/(\w+)/g;
|
|
786
|
+
corrected = corrected.replace(shadcnPattern, (match, component) => {
|
|
787
|
+
const capitalName = component.charAt(0).toUpperCase() + component.slice(1);
|
|
788
|
+
const configPath = componentPaths[capitalName];
|
|
789
|
+
if (configPath) {
|
|
790
|
+
corrections.push(`Fixed import: ${match} → ${configPath}`);
|
|
791
|
+
return configPath;
|
|
792
|
+
}
|
|
793
|
+
return match; // Leave as-is if no mapping
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
// 3. Fix type paths for features (from config)
|
|
797
|
+
const typePaths = ctx.typePaths || { features: '../api/types' };
|
|
798
|
+
if (filePath && filePath.includes('/features/') && typePaths.features) {
|
|
799
|
+
const wrongPaths = ["'../types'", '"../types"', "'./types'", '"./types"'];
|
|
800
|
+
for (const wrong of wrongPaths) {
|
|
801
|
+
if (corrected.includes(wrong)) {
|
|
802
|
+
corrected = corrected.replace(new RegExp(wrong.replace(/['"]/g, '[\'"]'), 'g'), `'${typePaths.features}'`);
|
|
803
|
+
corrections.push('Fixed type import path');
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// 4. Remove external utils if configured (noExternalUtils: true)
|
|
809
|
+
if (ctx.noExternalUtils && corrected.includes('@/lib/utils')) {
|
|
810
|
+
const hadFormatCurrency = corrected.includes('formatCurrency');
|
|
811
|
+
const hadCn = corrected.includes(' cn(') || corrected.includes(' cn`');
|
|
812
|
+
|
|
813
|
+
// Remove the import
|
|
814
|
+
corrected = corrected.replace(/^import.*from ['"]@\/lib\/utils['"];?\s*\n?/gm, '');
|
|
815
|
+
corrections.push('Removed @/lib/utils import');
|
|
816
|
+
|
|
817
|
+
// Inline formatCurrency if it was used
|
|
818
|
+
if (hadFormatCurrency) {
|
|
819
|
+
const formatCurrencyFn = `\nconst formatCurrency = (amount: number) =>\n new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount);\n`;
|
|
820
|
+
// Insert after imports
|
|
821
|
+
const lastImportMatch = corrected.match(/^import[^;]+;?\s*\n/gm);
|
|
822
|
+
if (lastImportMatch) {
|
|
823
|
+
const lastImport = lastImportMatch[lastImportMatch.length - 1];
|
|
824
|
+
const insertPos = corrected.lastIndexOf(lastImport) + lastImport.length;
|
|
825
|
+
corrected = corrected.slice(0, insertPos) + formatCurrencyFn + corrected.slice(insertPos);
|
|
826
|
+
}
|
|
827
|
+
corrections.push('Inlined formatCurrency');
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Remove cn() usage - just use template literals or className directly
|
|
831
|
+
if (hadCn) {
|
|
832
|
+
corrected = corrected.replace(/cn\((['"`][^'"`]+['"`])\)/g, '$1');
|
|
833
|
+
corrections.push('Removed cn() wrapper');
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// 5. Fix double-quoted imports to single quotes (style consistency)
|
|
838
|
+
const singleQuoteCount = (corrected.match(/from '/g) || []).length;
|
|
839
|
+
const doubleQuoteCount = (corrected.match(/from "/g) || []).length;
|
|
840
|
+
if (singleQuoteCount > doubleQuoteCount && doubleQuoteCount > 0) {
|
|
841
|
+
corrected = corrected.replace(/from "([^"]+)"/g, "from '$1'");
|
|
842
|
+
corrections.push('Normalized import quotes to single quotes');
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// 6. Remove empty import statements (artifact of removing imports)
|
|
846
|
+
corrected = corrected.replace(/^import\s*\{\s*\}\s*from\s*['"][^'"]+['"];?\s*\n?/gm, '');
|
|
847
|
+
|
|
848
|
+
// 7. Fix multiple consecutive blank lines (cleanup)
|
|
849
|
+
corrected = corrected.replace(/\n{3,}/g, '\n\n');
|
|
850
|
+
|
|
851
|
+
// Log corrections if any
|
|
852
|
+
if (corrections.length > 0 && typeof log === 'function') {
|
|
853
|
+
log('dim', ` 🔧 Auto-corrected: ${corrections.join(', ')}`);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
return { corrected: corrected.trim(), corrections };
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// ============================================================
|
|
860
|
+
// Project Auto-Detection (for wogi-init/wogi-onboard)
|
|
861
|
+
// ============================================================
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Detects the UI framework used in the project by checking dependencies.
|
|
865
|
+
* @param {string} projectRoot - Root directory of the project
|
|
866
|
+
* @returns {string} - Framework name: 'styled-components', 'shadcn', 'mui', 'chakra', 'antd', or 'react'
|
|
867
|
+
*/
|
|
868
|
+
function detectUIFramework(projectRoot = PROJECT_ROOT) {
|
|
869
|
+
try {
|
|
870
|
+
const pkgJsonPath = path.join(projectRoot, 'package.json');
|
|
871
|
+
if (!fs.existsSync(pkgJsonPath)) {
|
|
872
|
+
return 'react';
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
876
|
+
const deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
|
|
877
|
+
|
|
878
|
+
// Check in priority order
|
|
879
|
+
if (deps['styled-components']) return 'styled-components';
|
|
880
|
+
if (deps['@shadcn/ui'] || deps['@radix-ui/react-slot']) return 'shadcn';
|
|
881
|
+
if (deps['@mui/material']) return 'mui';
|
|
882
|
+
if (deps['@chakra-ui/react']) return 'chakra';
|
|
883
|
+
if (deps['antd']) return 'antd';
|
|
884
|
+
if (deps['tailwindcss']) return 'tailwind';
|
|
885
|
+
|
|
886
|
+
return 'react'; // vanilla
|
|
887
|
+
} catch (err) {
|
|
888
|
+
return 'react';
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Scans the components directory and builds a mapping of component names to import paths.
|
|
894
|
+
* @param {string} projectRoot - Root directory of the project
|
|
895
|
+
* @param {string[]} componentDirs - Directories to scan (relative to projectRoot)
|
|
896
|
+
* @returns {Object} - Mapping of ComponentName → import path
|
|
897
|
+
*/
|
|
898
|
+
function scanComponentPaths(projectRoot = PROJECT_ROOT, componentDirs = ['src/components']) {
|
|
899
|
+
const componentPaths = {};
|
|
900
|
+
|
|
901
|
+
for (const dir of componentDirs) {
|
|
902
|
+
const fullDir = path.join(projectRoot, dir);
|
|
903
|
+
if (!fs.existsSync(fullDir)) continue;
|
|
904
|
+
|
|
905
|
+
try {
|
|
906
|
+
const scanDir = (dirPath, aliasPath) => {
|
|
907
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
908
|
+
|
|
909
|
+
for (const entry of entries) {
|
|
910
|
+
if (entry.name.startsWith('.') || entry.name.startsWith('_')) continue;
|
|
911
|
+
if (entry.name.includes('.test.') || entry.name.includes('.spec.') || entry.name.includes('.stories.')) continue;
|
|
912
|
+
|
|
913
|
+
const entryPath = path.join(dirPath, entry.name);
|
|
914
|
+
|
|
915
|
+
if (entry.isDirectory()) {
|
|
916
|
+
// Check for index file or component file with same name
|
|
917
|
+
const indexFile = ['index.tsx', 'index.ts', 'index.jsx', 'index.js'].find(f =>
|
|
918
|
+
fs.existsSync(path.join(entryPath, f))
|
|
919
|
+
);
|
|
920
|
+
|
|
921
|
+
const componentFile = ['.tsx', '.ts', '.jsx', '.js'].find(ext =>
|
|
922
|
+
fs.existsSync(path.join(entryPath, entry.name + ext))
|
|
923
|
+
);
|
|
924
|
+
|
|
925
|
+
if (indexFile || componentFile) {
|
|
926
|
+
// This is a component directory
|
|
927
|
+
const componentName = entry.name;
|
|
928
|
+
const importPath = `${aliasPath}/${entry.name}`;
|
|
929
|
+
componentPaths[componentName] = importPath;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Recurse into subdirectories
|
|
933
|
+
scanDir(entryPath, `${aliasPath}/${entry.name}`);
|
|
934
|
+
} else if (entry.isFile()) {
|
|
935
|
+
// Direct component file
|
|
936
|
+
const ext = path.extname(entry.name);
|
|
937
|
+
if (['.tsx', '.ts', '.jsx', '.js'].includes(ext)) {
|
|
938
|
+
const componentName = path.basename(entry.name, ext);
|
|
939
|
+
// Skip index files and lowercase filenames (likely utilities)
|
|
940
|
+
if (componentName === 'index' || componentName[0] === componentName[0].toLowerCase()) continue;
|
|
941
|
+
|
|
942
|
+
const importPath = `${aliasPath}/${componentName}`;
|
|
943
|
+
componentPaths[componentName] = importPath;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
// Determine alias path (@/components or relative)
|
|
950
|
+
const aliasPath = dir.startsWith('src/') ? `@/${dir.slice(4)}` : `@/${dir}`;
|
|
951
|
+
scanDir(fullDir, aliasPath);
|
|
952
|
+
} catch (err) {
|
|
953
|
+
log('dim', ` ⚠️ Error scanning ${dir}: ${err.message}`);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
return componentPaths;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Generates a full projectContext configuration by auto-detecting project settings.
|
|
962
|
+
* Can be called during wogi-init or wogi-onboard.
|
|
963
|
+
* @param {string} projectRoot - Root directory of the project
|
|
964
|
+
* @returns {Object} - projectContext configuration
|
|
965
|
+
*/
|
|
966
|
+
function generateProjectContext(projectRoot = PROJECT_ROOT) {
|
|
967
|
+
const uiFramework = detectUIFramework(projectRoot);
|
|
968
|
+
|
|
969
|
+
// Scan standard component directories
|
|
970
|
+
const componentDirs = ['src/components', 'components', 'src/shared', 'shared'];
|
|
971
|
+
const componentPaths = scanComponentPaths(projectRoot, componentDirs);
|
|
972
|
+
|
|
973
|
+
// Default type paths
|
|
974
|
+
const typePaths = {
|
|
975
|
+
features: '../api/types',
|
|
976
|
+
shared: '@/types'
|
|
977
|
+
};
|
|
978
|
+
|
|
979
|
+
// Default forbidden imports (React for React 17+)
|
|
980
|
+
const doNotImport = ['React'];
|
|
981
|
+
|
|
982
|
+
// NoExternalUtils depends on framework
|
|
983
|
+
const noExternalUtils = uiFramework !== 'shadcn';
|
|
984
|
+
|
|
985
|
+
return {
|
|
986
|
+
uiFramework,
|
|
987
|
+
componentPaths,
|
|
988
|
+
typePaths,
|
|
989
|
+
doNotImport,
|
|
990
|
+
noExternalUtils
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// Export for CLI usage
|
|
995
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
996
|
+
module.exports = {
|
|
997
|
+
detectUIFramework,
|
|
998
|
+
scanComponentPaths,
|
|
999
|
+
generateProjectContext,
|
|
1000
|
+
autoCorrectCode,
|
|
1001
|
+
extractCodeFromResponse,
|
|
1002
|
+
isValidCode
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// ============================================================
|
|
1007
|
+
// Project Context Generator - Claude creates once, Local LLM reuses
|
|
1008
|
+
// ============================================================
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Generates and caches a comprehensive project context document.
|
|
1012
|
+
* This context is generated once (expensive) and reused for all steps (free).
|
|
1013
|
+
*
|
|
1014
|
+
* The context includes:
|
|
1015
|
+
* - Type definitions from the project
|
|
1016
|
+
* - Theme structure and correct access paths
|
|
1017
|
+
* - Component patterns from existing code
|
|
1018
|
+
* - Available components list
|
|
1019
|
+
* - Critical rules and conventions
|
|
1020
|
+
*/
|
|
1021
|
+
class ProjectContextGenerator {
|
|
1022
|
+
constructor(projectRoot = PROJECT_ROOT) {
|
|
1023
|
+
this.projectRoot = projectRoot;
|
|
1024
|
+
this.contextPath = path.join(projectRoot, '.workflow/state/hybrid-context.md');
|
|
1025
|
+
this.cacheMaxAge = 60 * 60 * 1000; // 1 hour
|
|
1026
|
+
|
|
1027
|
+
// Load config for project-specific settings
|
|
1028
|
+
this.config = this.loadProjectConfig();
|
|
1029
|
+
|
|
1030
|
+
// Export map (loaded lazily)
|
|
1031
|
+
this._exportMap = null;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Get or build the export map (with caching)
|
|
1036
|
+
*/
|
|
1037
|
+
getExportMap() {
|
|
1038
|
+
if (this._exportMap) return this._exportMap;
|
|
1039
|
+
|
|
1040
|
+
// Try cached first
|
|
1041
|
+
this._exportMap = loadCachedExportMap();
|
|
1042
|
+
if (this._exportMap) return this._exportMap;
|
|
1043
|
+
|
|
1044
|
+
// Build fresh export map
|
|
1045
|
+
const fullConfig = { hybrid: { projectContext: this.config } };
|
|
1046
|
+
this._exportMap = buildExportMap(fullConfig);
|
|
1047
|
+
saveExportMapCache(this._exportMap);
|
|
1048
|
+
|
|
1049
|
+
return this._exportMap;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Load project-specific settings from config.json
|
|
1054
|
+
*/
|
|
1055
|
+
loadProjectConfig() {
|
|
1056
|
+
try {
|
|
1057
|
+
const config = getConfig();
|
|
1058
|
+
return config.hybrid?.projectContext || {};
|
|
1059
|
+
} catch {
|
|
1060
|
+
return {};
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* Check if we have a valid cached context (less than 1 hour old)
|
|
1066
|
+
*/
|
|
1067
|
+
hasValidCache() {
|
|
1068
|
+
try {
|
|
1069
|
+
if (!fs.existsSync(this.contextPath)) return false;
|
|
1070
|
+
const stats = fs.statSync(this.contextPath);
|
|
1071
|
+
const ageMs = Date.now() - stats.mtimeMs;
|
|
1072
|
+
return ageMs < this.cacheMaxAge;
|
|
1073
|
+
} catch {
|
|
1074
|
+
return false;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Get cached context or null
|
|
1080
|
+
*/
|
|
1081
|
+
getCachedContext() {
|
|
1082
|
+
if (!this.hasValidCache()) return null;
|
|
1083
|
+
try {
|
|
1084
|
+
return fs.readFileSync(this.contextPath, 'utf-8');
|
|
1085
|
+
} catch {
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Save generated context to cache
|
|
1092
|
+
*/
|
|
1093
|
+
saveContext(context) {
|
|
1094
|
+
try {
|
|
1095
|
+
const dir = path.dirname(this.contextPath);
|
|
1096
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
1097
|
+
fs.writeFileSync(this.contextPath, context);
|
|
1098
|
+
} catch (err) {
|
|
1099
|
+
log('yellow', ` ⚠️ Could not cache context: ${err.message}`);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
/**
|
|
1104
|
+
* Simple glob implementation using fs
|
|
1105
|
+
*/
|
|
1106
|
+
globSync(pattern) {
|
|
1107
|
+
const results = [];
|
|
1108
|
+
const basePath = this.projectRoot;
|
|
1109
|
+
|
|
1110
|
+
const parts = pattern.split('/');
|
|
1111
|
+
const searchDir = (currentPath, remainingParts) => {
|
|
1112
|
+
if (remainingParts.length === 0) {
|
|
1113
|
+
if (fs.existsSync(currentPath)) results.push(currentPath);
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
const [current, ...rest] = remainingParts;
|
|
1118
|
+
|
|
1119
|
+
if (current === '*' || current === '**') {
|
|
1120
|
+
try {
|
|
1121
|
+
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
1122
|
+
for (const entry of entries) {
|
|
1123
|
+
if (entry.isDirectory()) {
|
|
1124
|
+
searchDir(path.join(currentPath, entry.name), rest);
|
|
1125
|
+
if (current === '**') {
|
|
1126
|
+
searchDir(path.join(currentPath, entry.name), remainingParts);
|
|
1127
|
+
}
|
|
1128
|
+
} else if (rest.length === 0) {
|
|
1129
|
+
results.push(path.join(currentPath, entry.name));
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
} catch {}
|
|
1133
|
+
} else if (current.includes('*')) {
|
|
1134
|
+
try {
|
|
1135
|
+
const regex = new RegExp('^' + current.replace(/\*/g, '.*') + '$');
|
|
1136
|
+
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
1137
|
+
for (const entry of entries) {
|
|
1138
|
+
if (regex.test(entry.name)) {
|
|
1139
|
+
if (entry.isDirectory()) {
|
|
1140
|
+
searchDir(path.join(currentPath, entry.name), rest);
|
|
1141
|
+
} else if (rest.length === 0) {
|
|
1142
|
+
results.push(path.join(currentPath, entry.name));
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
} catch {}
|
|
1147
|
+
} else {
|
|
1148
|
+
const nextPath = path.join(currentPath, current);
|
|
1149
|
+
if (fs.existsSync(nextPath)) {
|
|
1150
|
+
searchDir(nextPath, rest);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
|
|
1155
|
+
searchDir(basePath, parts);
|
|
1156
|
+
return results.map(p => path.relative(basePath, p));
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* Read file with line limit
|
|
1161
|
+
*/
|
|
1162
|
+
readFile(filePath, maxLines = 100) {
|
|
1163
|
+
try {
|
|
1164
|
+
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(this.projectRoot, filePath);
|
|
1165
|
+
if (!fs.existsSync(fullPath)) return null;
|
|
1166
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
1167
|
+
return content.split('\n').slice(0, maxLines).join('\n');
|
|
1168
|
+
} catch {
|
|
1169
|
+
return null;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* Check if a path should be excluded based on config
|
|
1175
|
+
*/
|
|
1176
|
+
shouldExcludePath(filePath) {
|
|
1177
|
+
const excludeDirs = this.config.excludeDirectories || ['__tests__', '__mocks__', 'node_modules', '.git'];
|
|
1178
|
+
return excludeDirs.some(dir => filePath.includes(`/${dir}/`) || filePath.includes(`\\${dir}\\`));
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* Check if a type definition should be excluded based on config patterns
|
|
1183
|
+
*/
|
|
1184
|
+
shouldExcludeType(typeName) {
|
|
1185
|
+
const excludePatterns = this.config.excludeTypePatterns || [];
|
|
1186
|
+
if (excludePatterns.length === 0) return false;
|
|
1187
|
+
|
|
1188
|
+
return excludePatterns.some(pattern => {
|
|
1189
|
+
try {
|
|
1190
|
+
const regex = new RegExp(pattern, 'i');
|
|
1191
|
+
return regex.test(typeName);
|
|
1192
|
+
} catch {
|
|
1193
|
+
return typeName.toLowerCase().includes(pattern.toLowerCase());
|
|
1194
|
+
}
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* Filter type content to exclude irrelevant types
|
|
1200
|
+
*/
|
|
1201
|
+
filterTypesContent(content, filePath) {
|
|
1202
|
+
if (this.shouldExcludePath(filePath)) return null;
|
|
1203
|
+
|
|
1204
|
+
const lines = content.split('\n');
|
|
1205
|
+
const filtered = [];
|
|
1206
|
+
let skipBlock = false;
|
|
1207
|
+
let braceCount = 0;
|
|
1208
|
+
|
|
1209
|
+
for (const line of lines) {
|
|
1210
|
+
// Check if this line starts a type we want to exclude
|
|
1211
|
+
const typeMatch = line.match(/(?:export\s+)?(?:interface|type)\s+(\w+)/);
|
|
1212
|
+
if (typeMatch && this.shouldExcludeType(typeMatch[1])) {
|
|
1213
|
+
skipBlock = true;
|
|
1214
|
+
braceCount = 0;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
if (skipBlock) {
|
|
1218
|
+
braceCount += (line.match(/{/g) || []).length;
|
|
1219
|
+
braceCount -= (line.match(/}/g) || []).length;
|
|
1220
|
+
if (braceCount <= 0 && line.includes('}')) {
|
|
1221
|
+
skipBlock = false;
|
|
1222
|
+
}
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
filtered.push(line);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
const result = filtered.join('\n').trim();
|
|
1230
|
+
return result.length > 10 ? result : null;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* Scan a directory for components and their exports
|
|
1235
|
+
*/
|
|
1236
|
+
scanComponentExports(componentDir) {
|
|
1237
|
+
const components = {};
|
|
1238
|
+
const fullDir = path.join(this.projectRoot, componentDir);
|
|
1239
|
+
|
|
1240
|
+
if (!fs.existsSync(fullDir)) return components;
|
|
1241
|
+
|
|
1242
|
+
try {
|
|
1243
|
+
const entries = fs.readdirSync(fullDir, { withFileTypes: true });
|
|
1244
|
+
|
|
1245
|
+
for (const entry of entries) {
|
|
1246
|
+
if (!entry.isDirectory()) continue;
|
|
1247
|
+
|
|
1248
|
+
const compPath = path.join(fullDir, entry.name);
|
|
1249
|
+
const indexPath = path.join(compPath, 'index.ts');
|
|
1250
|
+
const indexTsxPath = path.join(compPath, 'index.tsx');
|
|
1251
|
+
const mainFile = path.join(compPath, `${entry.name}.tsx`);
|
|
1252
|
+
|
|
1253
|
+
let exports = [];
|
|
1254
|
+
let importPath = `@/components/${entry.name}`;
|
|
1255
|
+
|
|
1256
|
+
// Try to find exports from index file
|
|
1257
|
+
for (const indexFile of [indexPath, indexTsxPath]) {
|
|
1258
|
+
if (fs.existsSync(indexFile)) {
|
|
1259
|
+
const content = fs.readFileSync(indexFile, 'utf-8');
|
|
1260
|
+
const exportMatches = content.match(/export\s+{\s*([^}]+)\s*}/g);
|
|
1261
|
+
if (exportMatches) {
|
|
1262
|
+
for (const match of exportMatches) {
|
|
1263
|
+
const names = match.replace(/export\s*{\s*/, '').replace(/\s*}/, '').split(',');
|
|
1264
|
+
exports.push(...names.map(n => n.trim()).filter(n => n && !n.includes(' as ')));
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
// Also check for named exports
|
|
1268
|
+
const namedExports = content.match(/export\s+(?:const|function|class)\s+(\w+)/g);
|
|
1269
|
+
if (namedExports) {
|
|
1270
|
+
for (const match of namedExports) {
|
|
1271
|
+
const name = match.split(/\s+/).pop();
|
|
1272
|
+
if (name && !exports.includes(name)) exports.push(name);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
break;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// If no index, try main file
|
|
1280
|
+
if (exports.length === 0 && fs.existsSync(mainFile)) {
|
|
1281
|
+
const content = fs.readFileSync(mainFile, 'utf-8');
|
|
1282
|
+
const namedExports = content.match(/export\s+(?:const|function|class)\s+(\w+)/g);
|
|
1283
|
+
if (namedExports) {
|
|
1284
|
+
for (const match of namedExports) {
|
|
1285
|
+
const name = match.split(/\s+/).pop();
|
|
1286
|
+
if (name) exports.push(name);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
if (exports.length > 0) {
|
|
1292
|
+
components[entry.name] = {
|
|
1293
|
+
exports: [...new Set(exports)],
|
|
1294
|
+
importPath
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
} catch (err) {
|
|
1299
|
+
// Ignore scan errors
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
return components;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/**
|
|
1306
|
+
* Get default type patterns based on common project structures
|
|
1307
|
+
*/
|
|
1308
|
+
getDefaultTypePatterns() {
|
|
1309
|
+
return [
|
|
1310
|
+
'src/types/*.ts',
|
|
1311
|
+
'src/types/index.ts',
|
|
1312
|
+
'src/*/types.ts',
|
|
1313
|
+
'src/features/*/api/types.ts',
|
|
1314
|
+
'src/**/types/*.ts',
|
|
1315
|
+
'apps/*/src/types/*.ts',
|
|
1316
|
+
'apps/*/src/features/*/api/types.ts',
|
|
1317
|
+
];
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Get default component patterns based on common project structures
|
|
1322
|
+
*/
|
|
1323
|
+
getDefaultComponentDirs() {
|
|
1324
|
+
const possibleDirs = [
|
|
1325
|
+
'src/components',
|
|
1326
|
+
'components',
|
|
1327
|
+
'apps/web/src/components',
|
|
1328
|
+
'src/shared/components',
|
|
1329
|
+
];
|
|
1330
|
+
|
|
1331
|
+
return possibleDirs.filter(dir => {
|
|
1332
|
+
const fullPath = path.join(this.projectRoot, dir);
|
|
1333
|
+
return fs.existsSync(fullPath);
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* Gather project files for context generation (config-driven)
|
|
1339
|
+
*/
|
|
1340
|
+
gatherProjectFiles() {
|
|
1341
|
+
const files = {};
|
|
1342
|
+
|
|
1343
|
+
// 1. Use config type directories or detect them
|
|
1344
|
+
const typeDirs = this.config.typeDirs?.length > 0
|
|
1345
|
+
? this.config.typeDirs
|
|
1346
|
+
: this.getDefaultTypePatterns();
|
|
1347
|
+
|
|
1348
|
+
for (const pattern of typeDirs) {
|
|
1349
|
+
const matches = this.globSync(pattern);
|
|
1350
|
+
for (const match of matches.slice(0, 5)) {
|
|
1351
|
+
if (this.shouldExcludePath(match)) continue;
|
|
1352
|
+
const content = this.readFile(match, 150);
|
|
1353
|
+
if (content) {
|
|
1354
|
+
const filtered = this.filterTypesContent(content, match);
|
|
1355
|
+
if (filtered) files[match] = filtered;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// 2. Use config component directories or detect them
|
|
1361
|
+
const componentDirs = this.config.componentDirs?.length > 0
|
|
1362
|
+
? this.config.componentDirs
|
|
1363
|
+
: this.getDefaultComponentDirs();
|
|
1364
|
+
|
|
1365
|
+
// Read sample components (2-3 examples)
|
|
1366
|
+
let componentCount = 0;
|
|
1367
|
+
for (const dir of componentDirs) {
|
|
1368
|
+
if (componentCount >= 3) break;
|
|
1369
|
+
const pattern = `${dir}/**/*.tsx`;
|
|
1370
|
+
const matches = this.globSync(pattern)
|
|
1371
|
+
.filter(f => !f.includes('.spec') && !f.includes('.test') && !f.includes('index') && !this.shouldExcludePath(f));
|
|
1372
|
+
for (const match of matches.slice(0, 2)) {
|
|
1373
|
+
const content = this.readFile(match, 80);
|
|
1374
|
+
if (content) {
|
|
1375
|
+
files[match] = content;
|
|
1376
|
+
componentCount++;
|
|
1377
|
+
}
|
|
1378
|
+
if (componentCount >= 3) break;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// 3. Read component index files
|
|
1383
|
+
for (const dir of componentDirs) {
|
|
1384
|
+
const indexPath = `${dir}/index.ts`;
|
|
1385
|
+
const content = this.readFile(indexPath, 50);
|
|
1386
|
+
if (content) files[indexPath] = content;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
return files;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
/**
|
|
1393
|
+
* Generate available imports section from export map
|
|
1394
|
+
* Now includes components with usage examples, hooks, services, types, and utils
|
|
1395
|
+
*/
|
|
1396
|
+
generateAvailableImportsSection() {
|
|
1397
|
+
let section = '## Available Imports\n\n';
|
|
1398
|
+
section += '**CRITICAL:** Only use imports listed below. DO NOT guess import paths.\n';
|
|
1399
|
+
section += '**CRITICAL:** Use string literals for variant/size props, NOT object access.\n\n';
|
|
1400
|
+
|
|
1401
|
+
const exportMap = this.getExportMap();
|
|
1402
|
+
|
|
1403
|
+
// Components - with usage examples and warnings
|
|
1404
|
+
if (Object.keys(exportMap.components).length > 0) {
|
|
1405
|
+
section += '### Components\n\n';
|
|
1406
|
+
|
|
1407
|
+
for (const [name, info] of Object.entries(exportMap.components)) {
|
|
1408
|
+
// Use the formatComponentWithUsage helper if component has details
|
|
1409
|
+
const hasDetails = info.usageExample ||
|
|
1410
|
+
(info.props && Object.keys(info.props).length > 0) ||
|
|
1411
|
+
(info.arrayExports && info.arrayExports.length > 0);
|
|
1412
|
+
|
|
1413
|
+
if (hasDetails) {
|
|
1414
|
+
section += formatComponentWithUsage(name, info);
|
|
1415
|
+
} else {
|
|
1416
|
+
// Fallback to simple format
|
|
1417
|
+
section += `#### ${name}\n\n`;
|
|
1418
|
+
section += '```typescript\n';
|
|
1419
|
+
if (info.exports.length > 0) {
|
|
1420
|
+
section += `import { ${info.exports.join(', ')} } from '${info.importPath}';\n`;
|
|
1421
|
+
} else if (info.defaultExport) {
|
|
1422
|
+
section += `import ${info.defaultExport} from '${info.importPath}';\n`;
|
|
1423
|
+
}
|
|
1424
|
+
section += '```\n\n';
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// Collect all array exports for global warning
|
|
1429
|
+
const allArrayExports = [];
|
|
1430
|
+
for (const [name, info] of Object.entries(exportMap.components)) {
|
|
1431
|
+
if (info.arrayExports && info.arrayExports.length > 0) {
|
|
1432
|
+
allArrayExports.push(...info.arrayExports);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
if (allArrayExports.length > 0) {
|
|
1437
|
+
section += '#### ⚠️ CRITICAL: Array Exports Warning\n\n';
|
|
1438
|
+
section += `The following exports are **ARRAYS** (for iteration), **NOT objects**:\n`;
|
|
1439
|
+
section += `\`${allArrayExports.join('`, `')}\`\n\n`;
|
|
1440
|
+
section += '**WRONG:** `variant={cardVariants.default}` ❌\n';
|
|
1441
|
+
section += '**CORRECT:** `variant="default"` ✅\n\n';
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// Hooks - with file name vs export name warning
|
|
1446
|
+
if (Object.keys(exportMap.hooks).length > 0) {
|
|
1447
|
+
section += '### Hooks\n\n';
|
|
1448
|
+
section += '**IMPORTANT:** Use exact hook names shown below. File names may differ from export names.\n\n';
|
|
1449
|
+
|
|
1450
|
+
for (const [fileName, info] of Object.entries(exportMap.hooks)) {
|
|
1451
|
+
section += `#### ${fileName}\n`;
|
|
1452
|
+
section += '```typescript\n';
|
|
1453
|
+
if (info.exports.length > 0) {
|
|
1454
|
+
section += `// File: ${fileName}.ts\n`;
|
|
1455
|
+
section += `import { ${info.exports.join(', ')} } from '${info.importPath}';\n`;
|
|
1456
|
+
}
|
|
1457
|
+
section += '```\n\n';
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
section += '**Common Hook Mistakes:**\n';
|
|
1461
|
+
section += '- ❌ `useAuthStore()` → Check actual export (might be `useAuthState()`)\n';
|
|
1462
|
+
section += '- ❌ Using file name as function name → Use the actual exported function name\n\n';
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// Services
|
|
1466
|
+
if (Object.keys(exportMap.services).length > 0) {
|
|
1467
|
+
section += '### Services\n\n';
|
|
1468
|
+
section += '```typescript\n';
|
|
1469
|
+
for (const [name, info] of Object.entries(exportMap.services)) {
|
|
1470
|
+
if (info.exports.length > 0) {
|
|
1471
|
+
section += `import { ${info.exports.join(', ')} } from '${info.importPath}';\n`;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
section += '```\n\n';
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// Types
|
|
1478
|
+
if (Object.keys(exportMap.types).length > 0) {
|
|
1479
|
+
section += '### Types\n\n';
|
|
1480
|
+
section += '```typescript\n';
|
|
1481
|
+
for (const [name, info] of Object.entries(exportMap.types)) {
|
|
1482
|
+
if (info.types && info.types.length > 0) {
|
|
1483
|
+
section += `import type { ${info.types.join(', ')} } from '${info.importPath}';\n`;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
section += '```\n\n';
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// Utils
|
|
1490
|
+
if (Object.keys(exportMap.utils).length > 0) {
|
|
1491
|
+
section += '### Utilities\n\n';
|
|
1492
|
+
section += '```typescript\n';
|
|
1493
|
+
for (const [name, info] of Object.entries(exportMap.utils)) {
|
|
1494
|
+
if (info.exports.length > 0) {
|
|
1495
|
+
section += `import { ${info.exports.join(', ')} } from '${info.importPath}';\n`;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
section += '```\n\n';
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// Check if we found anything
|
|
1502
|
+
const totalExports = Object.keys(exportMap.components).length +
|
|
1503
|
+
Object.keys(exportMap.hooks).length +
|
|
1504
|
+
Object.keys(exportMap.services).length +
|
|
1505
|
+
Object.keys(exportMap.types).length +
|
|
1506
|
+
Object.keys(exportMap.utils).length;
|
|
1507
|
+
|
|
1508
|
+
if (totalExports === 0) {
|
|
1509
|
+
section += '_No exports found. Define imports inline or use TODO comments._\n\n';
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
return section;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
/**
|
|
1516
|
+
* @deprecated Use generateAvailableImportsSection instead
|
|
1517
|
+
*/
|
|
1518
|
+
generateAvailableComponentsSection() {
|
|
1519
|
+
return this.generateAvailableImportsSection();
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
/**
|
|
1523
|
+
* Generate project-specific warnings from config
|
|
1524
|
+
*/
|
|
1525
|
+
generateWarningsSection() {
|
|
1526
|
+
const warnings = this.config.projectWarnings || [];
|
|
1527
|
+
const doNotImport = this.config.doNotImport || ['React'];
|
|
1528
|
+
|
|
1529
|
+
if (warnings.length === 0 && doNotImport.length <= 1) return '';
|
|
1530
|
+
|
|
1531
|
+
let section = '## Project-Specific Warnings\n\n';
|
|
1532
|
+
|
|
1533
|
+
if (doNotImport.length > 0) {
|
|
1534
|
+
section += '**DO NOT import these:**\n';
|
|
1535
|
+
for (const item of doNotImport) {
|
|
1536
|
+
section += `- ❌ \`${item}\`\n`;
|
|
1537
|
+
}
|
|
1538
|
+
section += '\n';
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
if (warnings.length > 0) {
|
|
1542
|
+
section += '**Additional warnings:**\n';
|
|
1543
|
+
for (const warning of warnings) {
|
|
1544
|
+
section += `- ⚠️ ${warning}\n`;
|
|
1545
|
+
}
|
|
1546
|
+
section += '\n';
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
return section;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
/**
|
|
1553
|
+
* Generate type locations section from config
|
|
1554
|
+
*/
|
|
1555
|
+
generateTypeLocationsSection() {
|
|
1556
|
+
const typeLocations = this.config.typeLocations || {};
|
|
1557
|
+
|
|
1558
|
+
if (Object.keys(typeLocations).length === 0) return '';
|
|
1559
|
+
|
|
1560
|
+
let section = '## Type Import Paths\n\n';
|
|
1561
|
+
section += '| Context | Import From |\n';
|
|
1562
|
+
section += '|---------|-------------|\n';
|
|
1563
|
+
|
|
1564
|
+
for (const [context, importPath] of Object.entries(typeLocations)) {
|
|
1565
|
+
section += `| ${context} | \`${importPath}\` |\n`;
|
|
1566
|
+
}
|
|
1567
|
+
section += '\n';
|
|
1568
|
+
|
|
1569
|
+
return section;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
/**
|
|
1573
|
+
* Generate custom rules section from config
|
|
1574
|
+
*/
|
|
1575
|
+
generateCustomRulesSection() {
|
|
1576
|
+
const rules = this.config.customRules || [];
|
|
1577
|
+
|
|
1578
|
+
if (rules.length === 0) return '';
|
|
1579
|
+
|
|
1580
|
+
let section = '## Project Coding Rules\n\n';
|
|
1581
|
+
for (const rule of rules) {
|
|
1582
|
+
section += `- ${rule}\n`;
|
|
1583
|
+
}
|
|
1584
|
+
section += '\n';
|
|
1585
|
+
|
|
1586
|
+
return section;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/**
|
|
1590
|
+
* Generate dynamic context based on detected UI framework
|
|
1591
|
+
*/
|
|
1592
|
+
generateFrameworkGuidance() {
|
|
1593
|
+
const uiFramework = this.config.uiFramework;
|
|
1594
|
+
const stylingApproach = this.config.stylingApproach;
|
|
1595
|
+
|
|
1596
|
+
if (!uiFramework && !stylingApproach) return '';
|
|
1597
|
+
|
|
1598
|
+
let section = '## Framework & Styling\n\n';
|
|
1599
|
+
|
|
1600
|
+
if (uiFramework) {
|
|
1601
|
+
section += `**UI Framework:** ${uiFramework}\n\n`;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
if (stylingApproach) {
|
|
1605
|
+
section += `**Styling Approach:** ${stylingApproach}\n\n`;
|
|
1606
|
+
|
|
1607
|
+
// Add framework-specific guidance
|
|
1608
|
+
switch (stylingApproach.toLowerCase()) {
|
|
1609
|
+
case 'styled-components':
|
|
1610
|
+
section += `### Styled Components Patterns
|
|
1611
|
+
- Use transient props: \`$active\`, \`$variant\`, \`$size\` (prefix with $)
|
|
1612
|
+
- Theme access: \`\${({ theme }) => theme.colors.X}\`
|
|
1613
|
+
- Add displayName: \`Component.displayName = 'Component'\`
|
|
1614
|
+
\n`;
|
|
1615
|
+
break;
|
|
1616
|
+
case 'tailwind':
|
|
1617
|
+
case 'tailwindcss':
|
|
1618
|
+
section += `### Tailwind Patterns
|
|
1619
|
+
- Use className for styling
|
|
1620
|
+
- Use cn() utility if available for conditional classes
|
|
1621
|
+
- Follow project's class naming conventions
|
|
1622
|
+
\n`;
|
|
1623
|
+
break;
|
|
1624
|
+
case 'css-modules':
|
|
1625
|
+
section += `### CSS Modules Patterns
|
|
1626
|
+
- Import styles: \`import styles from './Component.module.css'\`
|
|
1627
|
+
- Use: \`className={styles.container}\`
|
|
1628
|
+
\n`;
|
|
1629
|
+
break;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
return section;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
/**
|
|
1637
|
+
* Generate smart context from project files (config-driven)
|
|
1638
|
+
*/
|
|
1639
|
+
generateSmartContext(projectFiles) {
|
|
1640
|
+
let context = '# Project Context for Code Generation\n\n';
|
|
1641
|
+
context += '> This context is auto-generated from your project configuration.\n';
|
|
1642
|
+
context += '> Local LLM: Use this as your primary reference.\n\n';
|
|
1643
|
+
|
|
1644
|
+
// 1. Available components (FIRST - most important for imports)
|
|
1645
|
+
context += this.generateAvailableComponentsSection();
|
|
1646
|
+
|
|
1647
|
+
// 2. Framework/styling guidance
|
|
1648
|
+
context += this.generateFrameworkGuidance();
|
|
1649
|
+
|
|
1650
|
+
// 3. Type locations
|
|
1651
|
+
context += this.generateTypeLocationsSection();
|
|
1652
|
+
|
|
1653
|
+
// 4. Project-specific warnings
|
|
1654
|
+
context += this.generateWarningsSection();
|
|
1655
|
+
|
|
1656
|
+
// 5. Custom rules
|
|
1657
|
+
context += this.generateCustomRulesSection();
|
|
1658
|
+
|
|
1659
|
+
// 6. Type Definitions (filtered)
|
|
1660
|
+
context += '## Type Definitions\n\n';
|
|
1661
|
+
let hasTypes = false;
|
|
1662
|
+
for (const [filePath, content] of Object.entries(projectFiles)) {
|
|
1663
|
+
if (filePath.includes('types')) {
|
|
1664
|
+
context += `### From \`${filePath}\`\n\`\`\`typescript\n${content}\n\`\`\`\n\n`;
|
|
1665
|
+
hasTypes = true;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
if (!hasTypes) {
|
|
1669
|
+
context += '_No type files found. Define types inline if needed._\n\n';
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
// 7. Component patterns (sample)
|
|
1673
|
+
context += '## Component Patterns\n\n';
|
|
1674
|
+
let sampleShown = false;
|
|
1675
|
+
for (const [filePath, content] of Object.entries(projectFiles)) {
|
|
1676
|
+
if (filePath.includes('components/') && filePath.endsWith('.tsx') && !sampleShown) {
|
|
1677
|
+
context += `### Sample Pattern (from \`${filePath}\`)\n`;
|
|
1678
|
+
context += 'Follow this pattern for new components:\n';
|
|
1679
|
+
context += '```typescript\n' + content + '\n```\n\n';
|
|
1680
|
+
sampleShown = true;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
if (!sampleShown) {
|
|
1684
|
+
context += '_No sample components found._\n\n';
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// 8. Universal rules
|
|
1688
|
+
context += `## Universal Rules
|
|
1689
|
+
|
|
1690
|
+
### Import Rules
|
|
1691
|
+
- ❌ NEVER: \`import React from 'react'\` (causes TS6133 error in React 17+)
|
|
1692
|
+
- ✅ CORRECT: \`import { useState, useCallback } from 'react'\`
|
|
1693
|
+
- ❌ NEVER invent import paths - use only what's listed above
|
|
1694
|
+
- ✅ If unsure, define types inline or use TODO comment
|
|
1695
|
+
|
|
1696
|
+
### Export Rules
|
|
1697
|
+
- ✅ Named exports: \`export function ComponentName() {}\`
|
|
1698
|
+
- ✅ Props interface: \`interface ComponentNameProps {}\`
|
|
1699
|
+
|
|
1700
|
+
---
|
|
1701
|
+
|
|
1702
|
+
**Remember:** If you're unsure about an import path, DON'T GUESS. Use inline code or a TODO comment.
|
|
1703
|
+
|
|
1704
|
+
`;
|
|
1705
|
+
|
|
1706
|
+
return context;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
/**
|
|
1710
|
+
* Minimal context fallback when no project files found
|
|
1711
|
+
*/
|
|
1712
|
+
getMinimalContext() {
|
|
1713
|
+
let context = `# Project Context for Code Generation
|
|
1714
|
+
|
|
1715
|
+
## Critical Rules
|
|
1716
|
+
|
|
1717
|
+
### Imports
|
|
1718
|
+
- ❌ NEVER: \`import React from 'react'\` - causes TS6133 unused variable error
|
|
1719
|
+
- ✅ CORRECT: \`import { useState, useCallback } from 'react'\`
|
|
1720
|
+
- ❌ NEVER invent import paths - only import what you know exists
|
|
1721
|
+
|
|
1722
|
+
### Exports
|
|
1723
|
+
- ✅ Use named exports: \`export function ComponentName\`
|
|
1724
|
+
- ✅ Define Props interface: \`interface ComponentNameProps {}\`
|
|
1725
|
+
|
|
1726
|
+
`;
|
|
1727
|
+
|
|
1728
|
+
// Add any configured warnings even in minimal mode
|
|
1729
|
+
context += this.generateWarningsSection();
|
|
1730
|
+
context += this.generateCustomRulesSection();
|
|
1731
|
+
|
|
1732
|
+
return context;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
/**
|
|
1736
|
+
* Generate or retrieve project context
|
|
1737
|
+
*/
|
|
1738
|
+
getOrGenerateContext() {
|
|
1739
|
+
// Check cache first
|
|
1740
|
+
const cached = this.getCachedContext();
|
|
1741
|
+
if (cached) {
|
|
1742
|
+
return { context: cached, fromCache: true };
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// Gather project files
|
|
1746
|
+
const projectFiles = this.gatherProjectFiles();
|
|
1747
|
+
|
|
1748
|
+
if (Object.keys(projectFiles).length === 0) {
|
|
1749
|
+
const minimal = this.getMinimalContext();
|
|
1750
|
+
return { context: minimal, fromCache: false };
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// Generate context from files
|
|
1754
|
+
const context = this.generateSmartContext(projectFiles);
|
|
1755
|
+
|
|
1756
|
+
// Cache it
|
|
1757
|
+
this.saveContext(context);
|
|
1758
|
+
|
|
1759
|
+
return { context, fromCache: false };
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
/**
|
|
1763
|
+
* Force regenerate context (bypass cache)
|
|
1764
|
+
*/
|
|
1765
|
+
regenerateContext() {
|
|
1766
|
+
const projectFiles = this.gatherProjectFiles();
|
|
1767
|
+
const context = Object.keys(projectFiles).length > 0
|
|
1768
|
+
? this.generateSmartContext(projectFiles)
|
|
1769
|
+
: this.getMinimalContext();
|
|
1770
|
+
|
|
1771
|
+
this.saveContext(context);
|
|
1772
|
+
return context;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// ============================================================
|
|
1777
|
+
// Hybrid Metrics Logging
|
|
1778
|
+
// ============================================================
|
|
1779
|
+
|
|
1780
|
+
/**
|
|
1781
|
+
* Logs token estimation metrics for accuracy tracking.
|
|
1782
|
+
* Saves to .workflow/state/hybrid-metrics.json
|
|
1783
|
+
*
|
|
1784
|
+
* @param {Object} plan - The executed plan
|
|
1785
|
+
* @param {Object} executionResult - Result of execution
|
|
1786
|
+
* @param {Object} complexity - Complexity assessment
|
|
1787
|
+
*/
|
|
1788
|
+
function logTokenMetrics(plan, executionResult, complexity) {
|
|
1789
|
+
const config = getConfig();
|
|
1790
|
+
const logMetrics = config.hybrid?.settings?.tokenEstimation?.logMetrics;
|
|
1791
|
+
|
|
1792
|
+
if (!logMetrics) return;
|
|
1793
|
+
|
|
1794
|
+
const metricsPath = path.join(STATE_DIR, 'hybrid-metrics.json');
|
|
1795
|
+
|
|
1796
|
+
// Load existing metrics or create new array
|
|
1797
|
+
let metrics = [];
|
|
1798
|
+
if (fs.existsSync(metricsPath)) {
|
|
1799
|
+
try {
|
|
1800
|
+
metrics = JSON.parse(fs.readFileSync(metricsPath, 'utf-8'));
|
|
1801
|
+
} catch {
|
|
1802
|
+
metrics = [];
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// Add new metric entry
|
|
1807
|
+
const entry = {
|
|
1808
|
+
timestamp: new Date().toISOString(),
|
|
1809
|
+
planId: plan.planId || 'unknown',
|
|
1810
|
+
task: plan.task || 'unknown',
|
|
1811
|
+
complexity: {
|
|
1812
|
+
level: complexity?.level || 'unknown',
|
|
1813
|
+
estimatedTokens: complexity?.estimatedTokens || 0,
|
|
1814
|
+
reasoning: complexity?.reasoning || ''
|
|
1815
|
+
},
|
|
1816
|
+
execution: {
|
|
1817
|
+
success: executionResult.success,
|
|
1818
|
+
stepsCompleted: executionResult.steps?.filter(s => s.success).length || 0,
|
|
1819
|
+
stepsTotal: executionResult.steps?.length || 0,
|
|
1820
|
+
escalated: executionResult.escalateToCloud?.length > 0,
|
|
1821
|
+
escalatedSteps: executionResult.escalateToCloud?.map(s => s.id) || []
|
|
1822
|
+
}
|
|
1823
|
+
};
|
|
1824
|
+
|
|
1825
|
+
metrics.push(entry);
|
|
1826
|
+
|
|
1827
|
+
// Keep only last 100 entries to prevent file bloat
|
|
1828
|
+
if (metrics.length > 100) {
|
|
1829
|
+
metrics = metrics.slice(-100);
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
// Save metrics
|
|
1833
|
+
try {
|
|
1834
|
+
fs.writeFileSync(metricsPath, JSON.stringify(metrics, null, 2));
|
|
1835
|
+
} catch (err) {
|
|
1836
|
+
log('yellow', ` ⚠️ Could not save metrics: ${err.message}`);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
/**
|
|
1841
|
+
* Displays complexity assessment to the user
|
|
1842
|
+
*/
|
|
1843
|
+
function displayComplexityAssessment(complexity) {
|
|
1844
|
+
log('white', '\n' + '─'.repeat(60));
|
|
1845
|
+
log('cyan', ' COMPLEXITY ASSESSMENT');
|
|
1846
|
+
log('white', '─'.repeat(60));
|
|
1847
|
+
|
|
1848
|
+
const levelColors = {
|
|
1849
|
+
small: 'green',
|
|
1850
|
+
medium: 'yellow',
|
|
1851
|
+
large: 'yellow',
|
|
1852
|
+
xl: 'red'
|
|
1853
|
+
};
|
|
1854
|
+
|
|
1855
|
+
log(levelColors[complexity.level] || 'white', `\n Level: ${complexity.level.toUpperCase()}`);
|
|
1856
|
+
log('white', ` Estimated Tokens: ${complexity.estimatedTokens.toLocaleString()}`);
|
|
1857
|
+
log('dim', ` Range: ${complexity.budget.min.toLocaleString()} - ${complexity.budget.max.toLocaleString()}`);
|
|
1858
|
+
log('dim', `\n Reasoning: ${complexity.reasoning}`);
|
|
1859
|
+
|
|
1860
|
+
// Show key factors
|
|
1861
|
+
if (complexity.factors.complexityKeywords?.length > 0) {
|
|
1862
|
+
log('dim', ` Keywords: ${complexity.factors.complexityKeywords.slice(0, 5).join(', ')}`);
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
log('white', '');
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
/**
|
|
1869
|
+
* Displays instruction richness settings to the user
|
|
1870
|
+
*/
|
|
1871
|
+
function displayInstructionRichness(richness) {
|
|
1872
|
+
log('white', '─'.repeat(60));
|
|
1873
|
+
log('cyan', ' INSTRUCTION RICHNESS');
|
|
1874
|
+
log('white', '─'.repeat(60));
|
|
1875
|
+
|
|
1876
|
+
const levelColors = {
|
|
1877
|
+
minimal: 'green',
|
|
1878
|
+
standard: 'yellow',
|
|
1879
|
+
rich: 'yellow',
|
|
1880
|
+
maximum: 'red'
|
|
1881
|
+
};
|
|
1882
|
+
|
|
1883
|
+
log(levelColors[richness.level] || 'white', `\n Level: ${richness.level.toUpperCase()}`);
|
|
1884
|
+
log('white', ` Verbosity: ${richness.templateVerbosity}`);
|
|
1885
|
+
log('dim', ` Claude Token Budget: ~${richness.claudeTokenBudget.toLocaleString()}`);
|
|
1886
|
+
|
|
1887
|
+
// Show what will be included
|
|
1888
|
+
const includes = [];
|
|
1889
|
+
if (richness.includeProjectContext) includes.push('Project Context');
|
|
1890
|
+
if (richness.includeTypeDefinitions) includes.push('Types');
|
|
1891
|
+
if (richness.includeRelatedCode) includes.push('Related Code');
|
|
1892
|
+
if (richness.includeExamples) includes.push('Examples');
|
|
1893
|
+
if (richness.includePatterns) includes.push('Patterns');
|
|
1894
|
+
if (richness.includeFullFileContents) includes.push('Full Files');
|
|
1895
|
+
|
|
1896
|
+
log('dim', ` Includes: ${includes.join(', ') || 'Minimal context only'}`);
|
|
1897
|
+
log('dim', `\n ${richness.description}`);
|
|
1898
|
+
log('white', '');
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
/**
|
|
1902
|
+
* Gets token estimation settings from config
|
|
1903
|
+
*/
|
|
1904
|
+
function getTokenEstimationSettings() {
|
|
1905
|
+
try {
|
|
1906
|
+
const config = getConfig();
|
|
1907
|
+
return {
|
|
1908
|
+
enabled: config.hybrid?.settings?.tokenEstimation?.enabled ?? true,
|
|
1909
|
+
minTokens: config.hybrid?.settings?.tokenEstimation?.minTokens ?? 1000,
|
|
1910
|
+
maxTokens: config.hybrid?.settings?.tokenEstimation?.maxTokens ?? 8000,
|
|
1911
|
+
defaultLevel: config.hybrid?.settings?.tokenEstimation?.defaultLevel ?? 'medium',
|
|
1912
|
+
logMetrics: config.hybrid?.settings?.tokenEstimation?.logMetrics ?? true
|
|
1913
|
+
};
|
|
1914
|
+
} catch {
|
|
1915
|
+
return {
|
|
1916
|
+
enabled: true,
|
|
1917
|
+
minTokens: 1000,
|
|
1918
|
+
maxTokens: 8000,
|
|
1919
|
+
defaultLevel: 'medium',
|
|
1920
|
+
logMetrics: true
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
// ============================================================
|
|
1926
|
+
// Context Management & Auto-Compaction
|
|
1927
|
+
// ============================================================
|
|
1928
|
+
|
|
1929
|
+
/**
|
|
1930
|
+
* Estimates token count from text.
|
|
1931
|
+
* Uses ~4 characters per token as a rough estimate.
|
|
1932
|
+
* This is conservative - actual tokenization varies by model.
|
|
1933
|
+
*/
|
|
1934
|
+
function estimateTokens(text) {
|
|
1935
|
+
if (!text) return 0;
|
|
1936
|
+
// Rough estimate: ~4 chars per token for English text/code
|
|
1937
|
+
// Add extra for whitespace and special characters
|
|
1938
|
+
return Math.ceil(text.length / 3.5);
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
/**
|
|
1942
|
+
* Calculates context usage percentage
|
|
1943
|
+
*/
|
|
1944
|
+
function getContextUsage(promptTokens, contextWindow) {
|
|
1945
|
+
if (!contextWindow) return 0;
|
|
1946
|
+
return Math.round((promptTokens / contextWindow) * 100);
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
/**
|
|
1950
|
+
* Smart prompt compaction strategies
|
|
1951
|
+
*/
|
|
1952
|
+
const compactionStrategies = {
|
|
1953
|
+
/**
|
|
1954
|
+
* Truncate file content to relevant sections
|
|
1955
|
+
* Keeps imports, target area, and exports
|
|
1956
|
+
*/
|
|
1957
|
+
truncateFileContent(content, maxLines = 200) {
|
|
1958
|
+
const lines = content.split('\n');
|
|
1959
|
+
if (lines.length <= maxLines) return content;
|
|
1960
|
+
|
|
1961
|
+
const imports = [];
|
|
1962
|
+
const exports = [];
|
|
1963
|
+
const middle = [];
|
|
1964
|
+
let inImports = true;
|
|
1965
|
+
|
|
1966
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1967
|
+
const line = lines[i];
|
|
1968
|
+
if (inImports && (line.startsWith('import ') || line.startsWith('from ') || line.trim() === '')) {
|
|
1969
|
+
imports.push(line);
|
|
1970
|
+
} else {
|
|
1971
|
+
inImports = false;
|
|
1972
|
+
if (line.startsWith('export ') && i > lines.length - 50) {
|
|
1973
|
+
exports.push(line);
|
|
1974
|
+
} else {
|
|
1975
|
+
middle.push(line);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
// Keep imports + first/last portions of middle + exports
|
|
1981
|
+
const keepFromMiddle = maxLines - imports.length - exports.length;
|
|
1982
|
+
const halfKeep = Math.floor(keepFromMiddle / 2);
|
|
1983
|
+
|
|
1984
|
+
const truncatedMiddle = [
|
|
1985
|
+
...middle.slice(0, halfKeep),
|
|
1986
|
+
'',
|
|
1987
|
+
`// ... ${middle.length - keepFromMiddle} lines truncated for context ...`,
|
|
1988
|
+
'',
|
|
1989
|
+
...middle.slice(-halfKeep)
|
|
1990
|
+
];
|
|
1991
|
+
|
|
1992
|
+
return [...imports, ...truncatedMiddle, ...exports].join('\n');
|
|
1993
|
+
},
|
|
1994
|
+
|
|
1995
|
+
/**
|
|
1996
|
+
* Remove previous errors from retry prompt, keep only the latest
|
|
1997
|
+
*/
|
|
1998
|
+
trimRetryErrors(prompt) {
|
|
1999
|
+
const errorSections = prompt.split('## PREVIOUS ERROR');
|
|
2000
|
+
if (errorSections.length <= 2) return prompt;
|
|
2001
|
+
|
|
2002
|
+
// Keep base prompt + only the latest error
|
|
2003
|
+
return errorSections[0] + '## PREVIOUS ERROR' + errorSections[errorSections.length - 1];
|
|
2004
|
+
},
|
|
2005
|
+
|
|
2006
|
+
/**
|
|
2007
|
+
* Remove verbose template sections
|
|
2008
|
+
*/
|
|
2009
|
+
trimTemplateVerbosity(prompt) {
|
|
2010
|
+
// Remove example sections if prompt is too long
|
|
2011
|
+
let trimmed = prompt.replace(/## Examples[\s\S]*?(?=##|$)/gi, '');
|
|
2012
|
+
// Remove detailed explanations
|
|
2013
|
+
trimmed = trimmed.replace(/\*\*Note:\*\*[\s\S]*?(?=\n\n|$)/gi, '');
|
|
2014
|
+
return trimmed;
|
|
2015
|
+
},
|
|
2016
|
+
|
|
2017
|
+
/**
|
|
2018
|
+
* Truncate search results array to prevent context overflow
|
|
2019
|
+
* @param {Array} results - Array of search results with optional content
|
|
2020
|
+
* @param {number} maxResults - Maximum number of results to keep
|
|
2021
|
+
* @param {number} maxLinesPerResult - Maximum lines per result content
|
|
2022
|
+
*/
|
|
2023
|
+
truncateSearchResults(results, maxResults = 10, maxLinesPerResult = 30) {
|
|
2024
|
+
if (!Array.isArray(results)) return results;
|
|
2025
|
+
|
|
2026
|
+
const truncated = results.slice(0, maxResults).map(r => {
|
|
2027
|
+
// If result has content, truncate it
|
|
2028
|
+
if (r.content && typeof r.content === 'string') {
|
|
2029
|
+
const lines = r.content.split('\n');
|
|
2030
|
+
if (lines.length > maxLinesPerResult) {
|
|
2031
|
+
return {
|
|
2032
|
+
...r,
|
|
2033
|
+
content: [
|
|
2034
|
+
...lines.slice(0, maxLinesPerResult),
|
|
2035
|
+
`... ${lines.length - maxLinesPerResult} more lines truncated ...`
|
|
2036
|
+
].join('\n')
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
return r;
|
|
2041
|
+
});
|
|
2042
|
+
|
|
2043
|
+
// Add truncation notice if we cut results
|
|
2044
|
+
if (results.length > maxResults) {
|
|
2045
|
+
truncated.push({
|
|
2046
|
+
_notice: true,
|
|
2047
|
+
message: `... and ${results.length - maxResults} more results (truncated to save context)`
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
return truncated;
|
|
2052
|
+
}
|
|
2053
|
+
};
|
|
2054
|
+
|
|
2055
|
+
/**
|
|
2056
|
+
* Auto-compacts a prompt to fit within context window.
|
|
2057
|
+
* Returns { prompt, wasCompacted, originalTokens, finalTokens }
|
|
2058
|
+
*/
|
|
2059
|
+
function autoCompactPrompt(prompt, contextWindow, reserveForOutput = 2048) {
|
|
2060
|
+
// Sanity check: never reserve more than 50% of context window
|
|
2061
|
+
// This prevents the bug where maxTokens == contextWindow causing availableTokens = 0
|
|
2062
|
+
const maxReserve = Math.floor(contextWindow / 2);
|
|
2063
|
+
if (reserveForOutput > maxReserve) {
|
|
2064
|
+
log('dim', ` 📊 Capping output reserve from ${reserveForOutput} to ${maxReserve} tokens`);
|
|
2065
|
+
reserveForOutput = maxReserve;
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
const availableTokens = contextWindow - reserveForOutput;
|
|
2069
|
+
|
|
2070
|
+
// Another sanity check: ensure we have at least 1024 tokens for the prompt
|
|
2071
|
+
if (availableTokens < 1024) {
|
|
2072
|
+
log('yellow', ` ⚠️ Warning: Very low available tokens (${availableTokens}). Context: ${contextWindow}, Reserve: ${reserveForOutput}`);
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
const originalTokens = estimateTokens(prompt);
|
|
2076
|
+
|
|
2077
|
+
if (originalTokens <= availableTokens) {
|
|
2078
|
+
return {
|
|
2079
|
+
prompt,
|
|
2080
|
+
wasCompacted: false,
|
|
2081
|
+
originalTokens,
|
|
2082
|
+
finalTokens: originalTokens,
|
|
2083
|
+
usage: getContextUsage(originalTokens, contextWindow)
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
log('yellow', ` ⚠️ Prompt too large (${originalTokens.toLocaleString()} tokens), compacting...`);
|
|
2088
|
+
|
|
2089
|
+
let compacted = prompt;
|
|
2090
|
+
|
|
2091
|
+
// Strategy 1: Trim retry errors
|
|
2092
|
+
compacted = compactionStrategies.trimRetryErrors(compacted);
|
|
2093
|
+
let tokens = estimateTokens(compacted);
|
|
2094
|
+
if (tokens <= availableTokens) {
|
|
2095
|
+
log('dim', ` 📦 Trimmed retry errors: ${tokens.toLocaleString()} tokens`);
|
|
2096
|
+
return { prompt: compacted, wasCompacted: true, originalTokens, finalTokens: tokens, usage: getContextUsage(tokens, contextWindow) };
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
// Strategy 2: Trim template verbosity
|
|
2100
|
+
compacted = compactionStrategies.trimTemplateVerbosity(compacted);
|
|
2101
|
+
tokens = estimateTokens(compacted);
|
|
2102
|
+
if (tokens <= availableTokens) {
|
|
2103
|
+
log('dim', ` 📦 Trimmed template verbosity: ${tokens.toLocaleString()} tokens`);
|
|
2104
|
+
return { prompt: compacted, wasCompacted: true, originalTokens, finalTokens: tokens, usage: getContextUsage(tokens, contextWindow) };
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
// Strategy 3: Truncate file content in the prompt
|
|
2108
|
+
// Find content between ``` markers and truncate
|
|
2109
|
+
const codeBlockRegex = /```[\s\S]*?```/g;
|
|
2110
|
+
compacted = compacted.replace(codeBlockRegex, (match) => {
|
|
2111
|
+
const content = match.slice(3, -3); // Remove ``` markers
|
|
2112
|
+
if (content.split('\n').length > 100) {
|
|
2113
|
+
const truncated = compactionStrategies.truncateFileContent(content, 100);
|
|
2114
|
+
return '```' + truncated + '```';
|
|
2115
|
+
}
|
|
2116
|
+
return match;
|
|
2117
|
+
});
|
|
2118
|
+
|
|
2119
|
+
// Also check for {{currentContent}} style blocks
|
|
2120
|
+
const currentContentMatch = compacted.match(/{{currentContent}}[\s\S]*?(?=##|$)/);
|
|
2121
|
+
if (currentContentMatch && currentContentMatch[0].length > 5000) {
|
|
2122
|
+
const lines = currentContentMatch[0].split('\n');
|
|
2123
|
+
const truncated = compactionStrategies.truncateFileContent(lines.slice(1).join('\n'), 150);
|
|
2124
|
+
compacted = compacted.replace(currentContentMatch[0], '{{currentContent}}\n' + truncated + '\n\n');
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
tokens = estimateTokens(compacted);
|
|
2128
|
+
log('dim', ` 📦 Truncated file content: ${tokens.toLocaleString()} tokens`);
|
|
2129
|
+
|
|
2130
|
+
// If still too large, do aggressive truncation
|
|
2131
|
+
if (tokens > availableTokens) {
|
|
2132
|
+
const ratio = availableTokens / tokens;
|
|
2133
|
+
const targetLength = Math.floor(compacted.length * ratio * 0.9); // 10% safety margin
|
|
2134
|
+
compacted = compacted.slice(0, targetLength) + '\n\n[Content truncated to fit context window]';
|
|
2135
|
+
tokens = estimateTokens(compacted);
|
|
2136
|
+
log('yellow', ` ⚠️ Aggressive truncation: ${tokens.toLocaleString()} tokens`);
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
return {
|
|
2140
|
+
prompt: compacted,
|
|
2141
|
+
wasCompacted: true,
|
|
2142
|
+
originalTokens,
|
|
2143
|
+
finalTokens: tokens,
|
|
2144
|
+
usage: getContextUsage(tokens, contextWindow)
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
// ============================================================
|
|
2149
|
+
// Template Engine
|
|
2150
|
+
// ============================================================
|
|
2151
|
+
|
|
2152
|
+
class TemplateEngine {
|
|
2153
|
+
constructor(templatesDir) {
|
|
2154
|
+
this.templatesDir = templatesDir;
|
|
2155
|
+
this.cache = new Map();
|
|
2156
|
+
this.richness = null; // Instruction richness settings
|
|
2157
|
+
this.projectRoot = PROJECT_ROOT;
|
|
2158
|
+
this.projectContext = this.loadProjectContext();
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
/**
|
|
2162
|
+
* Load project context from config for template rendering
|
|
2163
|
+
*/
|
|
2164
|
+
loadProjectContext() {
|
|
2165
|
+
try {
|
|
2166
|
+
const config = getConfig();
|
|
2167
|
+
const ctx = config.hybrid?.projectContext || {};
|
|
2168
|
+
|
|
2169
|
+
// Format availableComponents for template display
|
|
2170
|
+
let formattedComponents = '';
|
|
2171
|
+
if (ctx.availableComponents && Object.keys(ctx.availableComponents).length > 0) {
|
|
2172
|
+
formattedComponents = '```typescript\n';
|
|
2173
|
+
for (const [name, info] of Object.entries(ctx.availableComponents)) {
|
|
2174
|
+
const exports = Array.isArray(info.exports) ? info.exports.join(', ') : info.exports || name;
|
|
2175
|
+
const importPath = info.importPath || `@/components/${name}`;
|
|
2176
|
+
formattedComponents += `// ${name}\nimport { ${exports} } from '${importPath}'\n`;
|
|
2177
|
+
}
|
|
2178
|
+
formattedComponents += '```';
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
// Format typeLocations for template display
|
|
2182
|
+
let formattedTypeLocations = '';
|
|
2183
|
+
if (ctx.typeLocations && Object.keys(ctx.typeLocations).length > 0) {
|
|
2184
|
+
formattedTypeLocations = '| Context | Import Path |\n|---------|-------------|\n';
|
|
2185
|
+
for (const [context, importPath] of Object.entries(ctx.typeLocations)) {
|
|
2186
|
+
formattedTypeLocations += `| ${context} | \`${importPath}\` |\n`;
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
// Format warnings
|
|
2191
|
+
let formattedWarnings = '';
|
|
2192
|
+
if (ctx.projectWarnings && ctx.projectWarnings.length > 0) {
|
|
2193
|
+
formattedWarnings = ctx.projectWarnings.map(w => `- ⚠️ ${w}`).join('\n');
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
// Format custom rules
|
|
2197
|
+
let formattedRules = '';
|
|
2198
|
+
if (ctx.customRules && ctx.customRules.length > 0) {
|
|
2199
|
+
formattedRules = ctx.customRules.map(r => `- ${r}`).join('\n');
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
// Format doNotImport
|
|
2203
|
+
let formattedDoNotImport = '';
|
|
2204
|
+
if (ctx.doNotImport && ctx.doNotImport.length > 0) {
|
|
2205
|
+
formattedDoNotImport = ctx.doNotImport.map(i => `\`${i}\``).join(', ');
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
return {
|
|
2209
|
+
uiFramework: ctx.uiFramework,
|
|
2210
|
+
stylingApproach: ctx.stylingApproach,
|
|
2211
|
+
availableComponents: formattedComponents,
|
|
2212
|
+
typeLocations: formattedTypeLocations,
|
|
2213
|
+
projectWarnings: formattedWarnings,
|
|
2214
|
+
customRules: formattedRules,
|
|
2215
|
+
doNotImport: formattedDoNotImport,
|
|
2216
|
+
// Keep raw values too for programmatic use
|
|
2217
|
+
_raw: ctx
|
|
2218
|
+
};
|
|
2219
|
+
} catch {
|
|
2220
|
+
return {};
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
/**
|
|
2225
|
+
* Set instruction richness level for context-aware rendering
|
|
2226
|
+
*/
|
|
2227
|
+
setRichness(richnessConfig) {
|
|
2228
|
+
this.richness = richnessConfig;
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
loadTemplate(name) {
|
|
2232
|
+
if (this.cache.has(name)) {
|
|
2233
|
+
return this.cache.get(name);
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
const templatePath = path.join(this.templatesDir, `${name}.md`);
|
|
2237
|
+
if (!fs.existsSync(templatePath)) {
|
|
2238
|
+
throw new Error(`Template not found: ${name}`);
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
let template = fs.readFileSync(templatePath, 'utf-8');
|
|
2242
|
+
|
|
2243
|
+
// Include base template
|
|
2244
|
+
const basePath = path.join(this.templatesDir, '_base.md');
|
|
2245
|
+
if (fs.existsSync(basePath)) {
|
|
2246
|
+
const base = fs.readFileSync(basePath, 'utf-8');
|
|
2247
|
+
template = template.replace('{{include _base.md}}', base);
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
// Include patterns
|
|
2251
|
+
const patternsPath = path.join(this.templatesDir, '_patterns.md');
|
|
2252
|
+
if (fs.existsSync(patternsPath)) {
|
|
2253
|
+
const patterns = fs.readFileSync(patternsPath, 'utf-8');
|
|
2254
|
+
template = template.replace('{{include _patterns.md}}', patterns);
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
this.cache.set(name, template);
|
|
2258
|
+
return template;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
/**
|
|
2262
|
+
* Loads additional context based on richness settings
|
|
2263
|
+
*/
|
|
2264
|
+
loadRichnessContext(params) {
|
|
2265
|
+
if (!this.richness) return {};
|
|
2266
|
+
|
|
2267
|
+
const context = {};
|
|
2268
|
+
const filePath = params.path;
|
|
2269
|
+
|
|
2270
|
+
// Load patterns from decisions.md
|
|
2271
|
+
if (this.richness.includePatterns) {
|
|
2272
|
+
const patterns = loadPatterns(this.projectRoot);
|
|
2273
|
+
if (patterns) {
|
|
2274
|
+
context.decisionsPatterns = patterns;
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
// Load relevant type definitions
|
|
2279
|
+
if (this.richness.includeTypeDefinitions && filePath) {
|
|
2280
|
+
const types = loadRelevantTypes(this.projectRoot, filePath);
|
|
2281
|
+
if (types) {
|
|
2282
|
+
context.relevantTypes = types;
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
// Load related code snippets
|
|
2287
|
+
if (this.richness.includeRelatedCode && filePath) {
|
|
2288
|
+
const related = loadRelatedCode(this.projectRoot, filePath, params.type);
|
|
2289
|
+
if (related) {
|
|
2290
|
+
context.relatedCodeExamples = related;
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
// Add verbosity guidance
|
|
2295
|
+
context.verbosityGuidance = getVerbosityGuidance(this.richness.templateVerbosity);
|
|
2296
|
+
context.richnessLevel = this.richness.level;
|
|
2297
|
+
context.templateVerbosity = this.richness.templateVerbosity;
|
|
2298
|
+
|
|
2299
|
+
return context;
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
render(templateName, params) {
|
|
2303
|
+
let template = this.loadTemplate(templateName);
|
|
2304
|
+
|
|
2305
|
+
// Load richness-based context and merge with params
|
|
2306
|
+
const richnessContext = this.loadRichnessContext(params);
|
|
2307
|
+
|
|
2308
|
+
// Merge: params override projectContext, richnessContext adds more
|
|
2309
|
+
const augmentedParams = { ...this.projectContext, ...params, ...richnessContext };
|
|
2310
|
+
|
|
2311
|
+
// Simple variable substitution
|
|
2312
|
+
const substitute = (str, obj, prefix = '') => {
|
|
2313
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
2314
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
2315
|
+
|
|
2316
|
+
if (value === null || value === undefined) {
|
|
2317
|
+
str = str.replace(new RegExp(`{{${fullKey}}}`, 'g'), '');
|
|
2318
|
+
} else if (typeof value === 'object' && !Array.isArray(value)) {
|
|
2319
|
+
str = substitute(str, value, fullKey);
|
|
2320
|
+
} else if (Array.isArray(value)) {
|
|
2321
|
+
const arrayStr = value.map(v => {
|
|
2322
|
+
if (typeof v === 'object') {
|
|
2323
|
+
return JSON.stringify(v, null, 2);
|
|
2324
|
+
}
|
|
2325
|
+
return `- ${v}`;
|
|
2326
|
+
}).join('\n');
|
|
2327
|
+
str = str.replace(new RegExp(`{{${fullKey}}}`, 'g'), arrayStr);
|
|
2328
|
+
} else {
|
|
2329
|
+
str = str.replace(new RegExp(`{{${fullKey}}}`, 'g'), String(value));
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
return str;
|
|
2333
|
+
};
|
|
2334
|
+
|
|
2335
|
+
let result = substitute(template, augmentedParams);
|
|
2336
|
+
|
|
2337
|
+
// Process conditionals: {{#if var}}content{{/if}}
|
|
2338
|
+
// Supports nested object access: {{#if obj.prop}}
|
|
2339
|
+
result = result.replace(/\{\{#if\s+([\w.]+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (match, varPath, content) => {
|
|
2340
|
+
// Support dot notation for nested access
|
|
2341
|
+
const value = varPath.split('.').reduce((obj, key) => obj?.[key], augmentedParams);
|
|
2342
|
+
return value ? content : '';
|
|
2343
|
+
});
|
|
2344
|
+
|
|
2345
|
+
// Clean up any remaining unprocessed conditionals (variables not in params)
|
|
2346
|
+
result = result.replace(/\{\{#if\s+[\w.]+\}\}[\s\S]*?\{\{\/if\}\}/g, '');
|
|
2347
|
+
|
|
2348
|
+
// Add richness-specific sections if available
|
|
2349
|
+
if (this.richness && (this.richness.includePatterns || this.richness.includeTypeDefinitions || this.richness.includeRelatedCode)) {
|
|
2350
|
+
let additionalContext = '\n\n## Additional Context (Based on Task Complexity)\n\n';
|
|
2351
|
+
let hasContent = false;
|
|
2352
|
+
|
|
2353
|
+
if (richnessContext.decisionsPatterns) {
|
|
2354
|
+
additionalContext += '### Project Patterns\n' + richnessContext.decisionsPatterns + '\n\n';
|
|
2355
|
+
hasContent = true;
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
if (richnessContext.relevantTypes) {
|
|
2359
|
+
additionalContext += '### Relevant Type Definitions\n```typescript\n' + richnessContext.relevantTypes + '\n```\n\n';
|
|
2360
|
+
hasContent = true;
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
if (richnessContext.relatedCodeExamples) {
|
|
2364
|
+
additionalContext += '### Related Code Examples\n' + richnessContext.relatedCodeExamples + '\n\n';
|
|
2365
|
+
hasContent = true;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
if (hasContent) {
|
|
2369
|
+
result += additionalContext;
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
return result;
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
// ============================================================
|
|
2378
|
+
// Validator
|
|
2379
|
+
// ============================================================
|
|
2380
|
+
|
|
2381
|
+
class Validator {
|
|
2382
|
+
static fileExists(filePath) {
|
|
2383
|
+
if (fs.existsSync(filePath)) {
|
|
2384
|
+
return { success: true, message: 'File exists' };
|
|
2385
|
+
}
|
|
2386
|
+
return { success: false, message: `File not found: ${filePath}` };
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
/**
|
|
2390
|
+
* Finds the nearest directory containing a tsconfig.json.
|
|
2391
|
+
* Walks up from the file's directory to find the right TypeScript project root.
|
|
2392
|
+
* Essential for monorepos where tsconfig is in apps/web/, apps/api/, etc.
|
|
2393
|
+
*/
|
|
2394
|
+
static findTsConfigDir(filePath) {
|
|
2395
|
+
if (!filePath) return PROJECT_ROOT;
|
|
2396
|
+
|
|
2397
|
+
let dir = path.dirname(filePath);
|
|
2398
|
+
while (dir && dir !== path.dirname(dir)) { // Stop at filesystem root
|
|
2399
|
+
const tsconfig = path.join(dir, 'tsconfig.json');
|
|
2400
|
+
if (fs.existsSync(tsconfig)) {
|
|
2401
|
+
return dir;
|
|
2402
|
+
}
|
|
2403
|
+
// Also check for package.json as fallback (workspace root)
|
|
2404
|
+
const packageJson = path.join(dir, 'package.json');
|
|
2405
|
+
if (fs.existsSync(packageJson)) {
|
|
2406
|
+
// If this package has a tsconfig, use it
|
|
2407
|
+
if (fs.existsSync(path.join(dir, 'tsconfig.json'))) {
|
|
2408
|
+
return dir;
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
dir = path.dirname(dir);
|
|
2412
|
+
}
|
|
2413
|
+
return PROJECT_ROOT;
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
static typescriptCheck(filePath) {
|
|
2417
|
+
try {
|
|
2418
|
+
// Find the nearest tsconfig directory (for monorepo support)
|
|
2419
|
+
const cwd = this.findTsConfigDir(filePath);
|
|
2420
|
+
const tsconfigPath = path.join(cwd, 'tsconfig.json');
|
|
2421
|
+
|
|
2422
|
+
// Check if tsconfig exists in this directory
|
|
2423
|
+
if (!fs.existsSync(tsconfigPath)) {
|
|
2424
|
+
log('dim', ` ⚠️ No tsconfig.json found, skipping TypeScript check`);
|
|
2425
|
+
return { success: true, message: 'TypeScript check skipped (no tsconfig.json)' };
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
if (cwd !== PROJECT_ROOT) {
|
|
2429
|
+
log('dim', ` 📁 Running tsc from: ${path.relative(PROJECT_ROOT, cwd) || '.'}`);
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
// Use execFileSync with array args for safety
|
|
2433
|
+
execFileSync('npx', ['tsc', '--noEmit'], {
|
|
2434
|
+
encoding: 'utf-8',
|
|
2435
|
+
cwd,
|
|
2436
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
2437
|
+
});
|
|
2438
|
+
return { success: true, message: 'TypeScript check passed' };
|
|
2439
|
+
} catch (err) {
|
|
2440
|
+
const stderr = e.stderr || e.stdout || err.message;
|
|
2441
|
+
|
|
2442
|
+
// Filter out help text (indicates no tsconfig found)
|
|
2443
|
+
if (stderr.includes('COMMON COMMANDS') || stderr.includes('tsc: The TypeScript Compiler')) {
|
|
2444
|
+
return { success: true, message: 'TypeScript check skipped (tsc could not find project)' };
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
// CRITICAL: Filter errors to only include the file we're validating
|
|
2448
|
+
// This prevents pre-existing errors in other files from failing validation
|
|
2449
|
+
if (filePath) {
|
|
2450
|
+
const cwd = this.findTsConfigDir(filePath);
|
|
2451
|
+
const relativeFile = path.relative(cwd, filePath);
|
|
2452
|
+
const fileName = path.basename(filePath);
|
|
2453
|
+
const lines = stderr.split('\n');
|
|
2454
|
+
|
|
2455
|
+
// Find errors that mention our file (by relative path or just filename)
|
|
2456
|
+
const relevantErrors = lines.filter(line => {
|
|
2457
|
+
// Match lines that contain our file path
|
|
2458
|
+
return line.includes(relativeFile) ||
|
|
2459
|
+
line.includes(fileName) ||
|
|
2460
|
+
// Also include "error TS" lines that follow a file match (context)
|
|
2461
|
+
(line.trim().startsWith('error TS') && lines[lines.indexOf(line) - 1]?.includes(fileName));
|
|
2462
|
+
});
|
|
2463
|
+
|
|
2464
|
+
if (relevantErrors.length === 0) {
|
|
2465
|
+
// Errors exist but not in our file - pass validation
|
|
2466
|
+
const errorCount = (stderr.match(/error TS/g) || []).length;
|
|
2467
|
+
log('dim', ` ⚠️ ${errorCount} pre-existing error(s) in other files, ${fileName} is clean`);
|
|
2468
|
+
return { success: true, message: 'TypeScript check passed (file-specific)' };
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
// Errors in our file - fail with relevant errors only
|
|
2472
|
+
return {
|
|
2473
|
+
success: false,
|
|
2474
|
+
message: relevantErrors.slice(0, 10).join('\n')
|
|
2475
|
+
};
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
return {
|
|
2479
|
+
success: false,
|
|
2480
|
+
message: stderr.split('\n').slice(0, 10).join('\n')
|
|
2481
|
+
};
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
static eslintCheck(filePath) {
|
|
2486
|
+
try {
|
|
2487
|
+
// Also find the right directory for eslint config
|
|
2488
|
+
const cwd = this.findTsConfigDir(filePath);
|
|
2489
|
+
// Use execFileSync with array args to prevent shell injection
|
|
2490
|
+
execFileSync('npx', ['eslint', filePath, '--fix'], {
|
|
2491
|
+
encoding: 'utf-8',
|
|
2492
|
+
cwd,
|
|
2493
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
2494
|
+
});
|
|
2495
|
+
return { success: true, message: 'ESLint check passed' };
|
|
2496
|
+
} catch (err) {
|
|
2497
|
+
const stderr = e.stderr || e.stdout || err.message;
|
|
2498
|
+
return {
|
|
2499
|
+
success: false,
|
|
2500
|
+
message: stderr.split('\n').slice(0, 10).join('\n')
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
static runChecks(checks, filePath) {
|
|
2506
|
+
const results = [];
|
|
2507
|
+
|
|
2508
|
+
for (const check of checks) {
|
|
2509
|
+
let result;
|
|
2510
|
+
switch (check) {
|
|
2511
|
+
case 'file-exists':
|
|
2512
|
+
result = this.fileExists(filePath);
|
|
2513
|
+
break;
|
|
2514
|
+
case 'typescript-check':
|
|
2515
|
+
result = this.typescriptCheck(filePath); // Now passes filePath
|
|
2516
|
+
break;
|
|
2517
|
+
case 'eslint-check':
|
|
2518
|
+
result = this.eslintCheck(filePath);
|
|
2519
|
+
break;
|
|
2520
|
+
default:
|
|
2521
|
+
result = { success: true, message: `Unknown check: ${check}` };
|
|
2522
|
+
}
|
|
2523
|
+
results.push({ check, ...result });
|
|
2524
|
+
|
|
2525
|
+
if (!result.success) break;
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
return results;
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
// ============================================================
|
|
2533
|
+
// Rollback Manager
|
|
2534
|
+
// ============================================================
|
|
2535
|
+
|
|
2536
|
+
class RollbackManager {
|
|
2537
|
+
constructor() {
|
|
2538
|
+
this.createdFiles = [];
|
|
2539
|
+
this.modifiedFiles = [];
|
|
2540
|
+
this.checkpointPath = path.join(STATE_DIR, 'rollback-checkpoint.json');
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
trackCreation(filePath) {
|
|
2544
|
+
this.createdFiles.push(filePath);
|
|
2545
|
+
this.saveCheckpoint();
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
trackModification(filePath) {
|
|
2549
|
+
if (fs.existsSync(filePath)) {
|
|
2550
|
+
const original = fs.readFileSync(filePath, 'utf-8');
|
|
2551
|
+
this.modifiedFiles.push({ path: filePath, original });
|
|
2552
|
+
this.saveCheckpoint();
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
saveCheckpoint() {
|
|
2557
|
+
const checkpoint = {
|
|
2558
|
+
createdFiles: this.createdFiles,
|
|
2559
|
+
modifiedFiles: this.modifiedFiles,
|
|
2560
|
+
timestamp: new Date().toISOString()
|
|
2561
|
+
};
|
|
2562
|
+
fs.writeFileSync(this.checkpointPath, JSON.stringify(checkpoint, null, 2));
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
loadCheckpoint() {
|
|
2566
|
+
if (fs.existsSync(this.checkpointPath)) {
|
|
2567
|
+
const checkpoint = JSON.parse(fs.readFileSync(this.checkpointPath, 'utf-8'));
|
|
2568
|
+
this.createdFiles = checkpoint.createdFiles || [];
|
|
2569
|
+
this.modifiedFiles = checkpoint.modifiedFiles || [];
|
|
2570
|
+
return true;
|
|
2571
|
+
}
|
|
2572
|
+
return false;
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
rollback() {
|
|
2576
|
+
log('yellow', '\n🔙 Rolling back changes...\n');
|
|
2577
|
+
|
|
2578
|
+
for (const filePath of this.createdFiles) {
|
|
2579
|
+
if (fs.existsSync(filePath)) {
|
|
2580
|
+
fs.unlinkSync(filePath);
|
|
2581
|
+
log('dim', ` 🗑️ Deleted: ${filePath}`);
|
|
2582
|
+
|
|
2583
|
+
let dir = path.dirname(filePath);
|
|
2584
|
+
while (dir !== PROJECT_ROOT && fs.existsSync(dir)) {
|
|
2585
|
+
const files = fs.readdirSync(dir);
|
|
2586
|
+
if (files.length === 0) {
|
|
2587
|
+
fs.rmdirSync(dir);
|
|
2588
|
+
log('dim', ` 📁 Removed empty: ${dir}`);
|
|
2589
|
+
dir = path.dirname(dir);
|
|
2590
|
+
} else {
|
|
2591
|
+
break;
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
for (const { path: filePath, original } of this.modifiedFiles) {
|
|
2598
|
+
fs.writeFileSync(filePath, original);
|
|
2599
|
+
log('dim', ` ↩️ Restored: ${filePath}`);
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
if (fs.existsSync(this.checkpointPath)) {
|
|
2603
|
+
fs.unlinkSync(this.checkpointPath);
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
this.createdFiles = [];
|
|
2607
|
+
this.modifiedFiles = [];
|
|
2608
|
+
|
|
2609
|
+
log('green', '\n✅ Rollback complete\n');
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
clearCheckpoint() {
|
|
2613
|
+
if (fs.existsSync(this.checkpointPath)) {
|
|
2614
|
+
fs.unlinkSync(this.checkpointPath);
|
|
2615
|
+
}
|
|
2616
|
+
this.createdFiles = [];
|
|
2617
|
+
this.modifiedFiles = [];
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
// ============================================================
|
|
2622
|
+
// State Manager
|
|
2623
|
+
// ============================================================
|
|
2624
|
+
|
|
2625
|
+
class StateManager {
|
|
2626
|
+
updateRequestLog(step, status, mode = 'hybrid', executor = '') {
|
|
2627
|
+
const logPath = path.join(STATE_DIR, 'request-log.md');
|
|
2628
|
+
const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 16);
|
|
2629
|
+
|
|
2630
|
+
const entry = `
|
|
2631
|
+
## ${timestamp} - ${step.title}
|
|
2632
|
+
|
|
2633
|
+
**Status:** ${status}
|
|
2634
|
+
**Type:** ${step.type}
|
|
2635
|
+
**Mode:** ${mode}${executor ? ` (${executor})` : ''}
|
|
2636
|
+
${step.params?.path ? `**File:** \`${step.params.path}\`` : ''}
|
|
2637
|
+
|
|
2638
|
+
${step.description || ''}
|
|
2639
|
+
|
|
2640
|
+
---
|
|
2641
|
+
`;
|
|
2642
|
+
|
|
2643
|
+
if (fs.existsSync(logPath)) {
|
|
2644
|
+
fs.appendFileSync(logPath, entry);
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
updateAppMap(update) {
|
|
2649
|
+
if (!update) return;
|
|
2650
|
+
|
|
2651
|
+
const mapPath = path.join(STATE_DIR, 'app-map.md');
|
|
2652
|
+
if (!fs.existsSync(mapPath)) return;
|
|
2653
|
+
|
|
2654
|
+
let content = fs.readFileSync(mapPath, 'utf-8');
|
|
2655
|
+
const { section, entry } = update;
|
|
2656
|
+
|
|
2657
|
+
const sectionRegex = new RegExp(`(## ${section}[\\s\\S]*?)(\n## |$)`);
|
|
2658
|
+
const match = content.match(sectionRegex);
|
|
2659
|
+
|
|
2660
|
+
if (match) {
|
|
2661
|
+
const [, sectionContent, nextSection] = match;
|
|
2662
|
+
const newSection = sectionContent.trimEnd() + `\n- ${entry}\n\n`;
|
|
2663
|
+
content = content.replace(sectionRegex, newSection + (nextSection === '\n## ' ? '\n## ' : ''));
|
|
2664
|
+
fs.writeFileSync(mapPath, content);
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
/**
|
|
2669
|
+
* Update hybrid session state
|
|
2670
|
+
* v2.0: Delegates to durable session when enabled
|
|
2671
|
+
*/
|
|
2672
|
+
updateHybridSession(data) {
|
|
2673
|
+
const config = getConfig();
|
|
2674
|
+
|
|
2675
|
+
// v2.0: Use durable session if enabled
|
|
2676
|
+
if (config.durableSteps?.enabled !== false) {
|
|
2677
|
+
// Update durable session with hybrid-specific data
|
|
2678
|
+
const dsSession = durableSession.loadDurableSession();
|
|
2679
|
+
if (dsSession) {
|
|
2680
|
+
// Track tokens saved
|
|
2681
|
+
if (data.totalTokensSaved) {
|
|
2682
|
+
durableSession.addTokensSaved(data.totalTokensSaved - (dsSession.metrics.tokensSaved || 0));
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
// If executedSteps changed, mark corresponding steps as completed
|
|
2686
|
+
if (data.executedSteps) {
|
|
2687
|
+
for (const stepId of data.executedSteps) {
|
|
2688
|
+
const step = dsSession.steps.find(s => s.id === stepId || s.description?.includes(stepId));
|
|
2689
|
+
if (step && step.status !== durableSession.STEP_STATUS.COMPLETED) {
|
|
2690
|
+
durableSession.markStepCompleted(step.id, 'Executed by orchestrator');
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
// If failedSteps changed, mark corresponding steps as failed
|
|
2696
|
+
if (data.failedSteps) {
|
|
2697
|
+
for (const stepId of data.failedSteps) {
|
|
2698
|
+
const step = dsSession.steps.find(s => s.id === stepId || s.description?.includes(stepId));
|
|
2699
|
+
if (step && step.status !== durableSession.STEP_STATUS.FAILED) {
|
|
2700
|
+
durableSession.markStepFailed(step.id, 'Failed in orchestrator');
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
return durableSession.getHybridSession();
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
// Legacy fallback: write to hybrid-session.json directly
|
|
2710
|
+
// DEPRECATED: This path is kept for backward compatibility but will be removed
|
|
2711
|
+
// Enable durableSteps in config.json to use the modern session management
|
|
2712
|
+
console.warn('[DEPRECATED] Using legacy hybrid-session.json - enable durableSteps.enabled in config.json');
|
|
2713
|
+
const sessionPath = path.join(STATE_DIR, 'hybrid-session.json');
|
|
2714
|
+
|
|
2715
|
+
let session = {
|
|
2716
|
+
sessionId: `sess-${Date.now()}`,
|
|
2717
|
+
startedAt: new Date().toISOString(),
|
|
2718
|
+
autoExecute: false,
|
|
2719
|
+
currentPlan: null,
|
|
2720
|
+
executedSteps: [],
|
|
2721
|
+
failedSteps: [],
|
|
2722
|
+
pendingSteps: [],
|
|
2723
|
+
totalTokensSaved: 0
|
|
2724
|
+
};
|
|
2725
|
+
|
|
2726
|
+
if (fs.existsSync(sessionPath)) {
|
|
2727
|
+
session = { ...session, ...JSON.parse(fs.readFileSync(sessionPath, 'utf-8')) };
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
Object.assign(session, data);
|
|
2731
|
+
session.updatedAt = new Date().toISOString();
|
|
2732
|
+
|
|
2733
|
+
// Use atomic writeJson to prevent data corruption
|
|
2734
|
+
writeJson(sessionPath, session);
|
|
2735
|
+
return session;
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
/**
|
|
2739
|
+
* Get hybrid session state
|
|
2740
|
+
* v2.0: Returns durable session in hybrid format when enabled
|
|
2741
|
+
*/
|
|
2742
|
+
getHybridSession() {
|
|
2743
|
+
const config = getConfig();
|
|
2744
|
+
|
|
2745
|
+
// v2.0: Use durable session if enabled
|
|
2746
|
+
if (config.durableSteps?.enabled !== false) {
|
|
2747
|
+
return durableSession.getHybridSession();
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
// Legacy fallback - DEPRECATED
|
|
2751
|
+
const sessionPath = path.join(STATE_DIR, 'hybrid-session.json');
|
|
2752
|
+
if (fs.existsSync(sessionPath)) {
|
|
2753
|
+
console.warn('[DEPRECATED] Reading legacy hybrid-session.json - enable durableSteps.enabled in config.json');
|
|
2754
|
+
return JSON.parse(fs.readFileSync(sessionPath, 'utf-8'));
|
|
2755
|
+
}
|
|
2756
|
+
return null;
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
saveResults(results) {
|
|
2760
|
+
const resultsPath = path.join(STATE_DIR, 'hybrid-results.json');
|
|
2761
|
+
fs.writeFileSync(resultsPath, JSON.stringify(results, null, 2));
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
/**
|
|
2765
|
+
* Loads project context from config.json, export map, and app-map.md.
|
|
2766
|
+
* Returns context that can be used in templates.
|
|
2767
|
+
*
|
|
2768
|
+
* Reads from:
|
|
2769
|
+
* - config.json → hybrid.projectContext (primary source)
|
|
2770
|
+
* - export-map.json (scanned exports)
|
|
2771
|
+
* - app-map.md (supplemental component info)
|
|
2772
|
+
*/
|
|
2773
|
+
loadProjectContext() {
|
|
2774
|
+
const context = {
|
|
2775
|
+
importPatterns: '',
|
|
2776
|
+
availableComponents: '',
|
|
2777
|
+
availableHooks: '',
|
|
2778
|
+
availableServices: '',
|
|
2779
|
+
availableTypes: '',
|
|
2780
|
+
availableUtils: '',
|
|
2781
|
+
typeLocations: '',
|
|
2782
|
+
uiFramework: 'react',
|
|
2783
|
+
stylingApproach: '',
|
|
2784
|
+
doNotImport: '',
|
|
2785
|
+
projectWarnings: '',
|
|
2786
|
+
customRules: '',
|
|
2787
|
+
projectContext: null,
|
|
2788
|
+
exportMap: null
|
|
2789
|
+
};
|
|
2790
|
+
|
|
2791
|
+
// Try to load from config (primary source)
|
|
2792
|
+
const configPath = path.join(WORKFLOW_DIR, 'config.json');
|
|
2793
|
+
if (fs.existsSync(configPath)) {
|
|
2794
|
+
try {
|
|
2795
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
2796
|
+
const projectCtx = config.hybrid?.projectContext || {};
|
|
2797
|
+
|
|
2798
|
+
// Store raw project context for auto-correction
|
|
2799
|
+
context.projectContext = projectCtx;
|
|
2800
|
+
|
|
2801
|
+
// UI Framework
|
|
2802
|
+
if (projectCtx.uiFramework) {
|
|
2803
|
+
context.uiFramework = projectCtx.uiFramework;
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
// Styling approach
|
|
2807
|
+
if (projectCtx.stylingApproach) {
|
|
2808
|
+
context.stylingApproach = projectCtx.stylingApproach;
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
// Format forbidden imports
|
|
2812
|
+
if (projectCtx.doNotImport?.length > 0) {
|
|
2813
|
+
context.doNotImport = projectCtx.doNotImport.join(', ');
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
// Format project warnings
|
|
2817
|
+
if (projectCtx.projectWarnings?.length > 0) {
|
|
2818
|
+
context.projectWarnings = projectCtx.projectWarnings.map(w => `- ⚠️ ${w}`).join('\n');
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
// Format custom rules
|
|
2822
|
+
if (projectCtx.customRules?.length > 0) {
|
|
2823
|
+
context.customRules = projectCtx.customRules.map(r => `- ${r}`).join('\n');
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
// Format type locations
|
|
2827
|
+
if (projectCtx.typeLocations && Object.keys(projectCtx.typeLocations).length > 0) {
|
|
2828
|
+
context.typeLocations = Object.entries(projectCtx.typeLocations)
|
|
2829
|
+
.map(([scope, importPath]) => `- In ${scope}: \`import type { X } from '${importPath}'\``)
|
|
2830
|
+
.join('\n');
|
|
2831
|
+
}
|
|
2832
|
+
} catch (err) {
|
|
2833
|
+
log('dim', ` ⚠️ Could not parse config.json: ${err.message}`);
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
// Load export map for accurate imports
|
|
2838
|
+
const exportMap = loadCachedExportMap();
|
|
2839
|
+
if (exportMap) {
|
|
2840
|
+
context.exportMap = exportMap;
|
|
2841
|
+
|
|
2842
|
+
// Format components
|
|
2843
|
+
if (Object.keys(exportMap.components).length > 0) {
|
|
2844
|
+
context.availableComponents = Object.entries(exportMap.components)
|
|
2845
|
+
.map(([name, info]) => {
|
|
2846
|
+
if (info.exports.length > 0) {
|
|
2847
|
+
return `import { ${info.exports.join(', ')} } from '${info.importPath}';`;
|
|
2848
|
+
} else if (info.defaultExport) {
|
|
2849
|
+
return `import ${info.defaultExport} from '${info.importPath}';`;
|
|
2850
|
+
}
|
|
2851
|
+
return null;
|
|
2852
|
+
})
|
|
2853
|
+
.filter(Boolean)
|
|
2854
|
+
.join('\n');
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
// Format hooks
|
|
2858
|
+
if (Object.keys(exportMap.hooks).length > 0) {
|
|
2859
|
+
context.availableHooks = Object.entries(exportMap.hooks)
|
|
2860
|
+
.map(([name, info]) => info.exports.length > 0
|
|
2861
|
+
? `import { ${info.exports.join(', ')} } from '${info.importPath}';`
|
|
2862
|
+
: null)
|
|
2863
|
+
.filter(Boolean)
|
|
2864
|
+
.join('\n');
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
// Format services
|
|
2868
|
+
if (Object.keys(exportMap.services).length > 0) {
|
|
2869
|
+
context.availableServices = Object.entries(exportMap.services)
|
|
2870
|
+
.map(([name, info]) => info.exports.length > 0
|
|
2871
|
+
? `import { ${info.exports.join(', ')} } from '${info.importPath}';`
|
|
2872
|
+
: null)
|
|
2873
|
+
.filter(Boolean)
|
|
2874
|
+
.join('\n');
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
// Format types
|
|
2878
|
+
if (Object.keys(exportMap.types).length > 0) {
|
|
2879
|
+
context.availableTypes = Object.entries(exportMap.types)
|
|
2880
|
+
.map(([name, info]) => info.types?.length > 0
|
|
2881
|
+
? `import type { ${info.types.join(', ')} } from '${info.importPath}';`
|
|
2882
|
+
: null)
|
|
2883
|
+
.filter(Boolean)
|
|
2884
|
+
.join('\n');
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
// Format utils
|
|
2888
|
+
if (Object.keys(exportMap.utils).length > 0) {
|
|
2889
|
+
context.availableUtils = Object.entries(exportMap.utils)
|
|
2890
|
+
.map(([name, info]) => info.exports.length > 0
|
|
2891
|
+
? `import { ${info.exports.join(', ')} } from '${info.importPath}';`
|
|
2892
|
+
: null)
|
|
2893
|
+
.filter(Boolean)
|
|
2894
|
+
.join('\n');
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
// Supplement with app-map.md if no exports found
|
|
2899
|
+
const appMapPath = path.join(STATE_DIR, 'app-map.md');
|
|
2900
|
+
if (fs.existsSync(appMapPath) && !context.availableComponents) {
|
|
2901
|
+
try {
|
|
2902
|
+
const appMap = fs.readFileSync(appMapPath, 'utf-8');
|
|
2903
|
+
|
|
2904
|
+
// Extract component sections
|
|
2905
|
+
const componentMatch = appMap.match(/## Components[\s\S]*?(?=##|$)/i);
|
|
2906
|
+
if (componentMatch) {
|
|
2907
|
+
context.availableComponents = componentMatch[0].trim();
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
// Extract screens/features
|
|
2911
|
+
const screensMatch = appMap.match(/## Screens[\s\S]*?(?=##|$)/i);
|
|
2912
|
+
if (screensMatch) {
|
|
2913
|
+
context.availableComponents += '\n\n' + screensMatch[0].trim();
|
|
2914
|
+
}
|
|
2915
|
+
} catch (err) {
|
|
2916
|
+
log('dim', ` ⚠️ Could not parse app-map.md: ${err.message}`);
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
return context;
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
// ============================================================
|
|
2925
|
+
// Orchestrator
|
|
2926
|
+
// ============================================================
|
|
2927
|
+
|
|
2928
|
+
class Orchestrator {
|
|
2929
|
+
constructor() {
|
|
2930
|
+
this.config = loadHybridConfig();
|
|
2931
|
+
// Use factory to create appropriate executor (local or cloud)
|
|
2932
|
+
this.llm = createExecutor(this.config);
|
|
2933
|
+
this.templates = new TemplateEngine(TEMPLATES_DIR);
|
|
2934
|
+
this.rollback = new RollbackManager();
|
|
2935
|
+
this.state = new StateManager();
|
|
2936
|
+
this.completedSteps = new Set();
|
|
2937
|
+
|
|
2938
|
+
// Project context generator - generates once, reuses for all steps
|
|
2939
|
+
this.contextGenerator = new ProjectContextGenerator(PROJECT_ROOT);
|
|
2940
|
+
this.projectContext = null;
|
|
2941
|
+
|
|
2942
|
+
// Complexity assessment for the current plan
|
|
2943
|
+
this.planComplexity = null;
|
|
2944
|
+
|
|
2945
|
+
// Instruction richness settings (set per-plan based on complexity)
|
|
2946
|
+
this.instructionRichness = null;
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
/**
|
|
2950
|
+
* Ensures project context is loaded (from cache or generated)
|
|
2951
|
+
* Called once before executing any steps - local LLM tokens are FREE
|
|
2952
|
+
*/
|
|
2953
|
+
async ensureProjectContext() {
|
|
2954
|
+
const { context, fromCache } = this.contextGenerator.getOrGenerateContext();
|
|
2955
|
+
this.projectContext = context;
|
|
2956
|
+
|
|
2957
|
+
if (fromCache) {
|
|
2958
|
+
log('dim', '📋 Using cached project context');
|
|
2959
|
+
} else {
|
|
2960
|
+
log('green', '✅ Generated and cached project context');
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
const contextTokens = estimateTokens(context);
|
|
2964
|
+
log('dim', ` Context size: ~${contextTokens.toLocaleString()} tokens (prepended to each step - FREE)`);
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
async executePlan(plan) {
|
|
2968
|
+
const results = {
|
|
2969
|
+
planId: plan.planId,
|
|
2970
|
+
task: plan.task,
|
|
2971
|
+
success: true,
|
|
2972
|
+
startedAt: new Date().toISOString(),
|
|
2973
|
+
steps: [],
|
|
2974
|
+
failedSteps: [],
|
|
2975
|
+
escalateToCloud: [],
|
|
2976
|
+
tokensSaved: plan.estimatedTokensSaved || 0
|
|
2977
|
+
};
|
|
2978
|
+
|
|
2979
|
+
// Assess task complexity for token estimation
|
|
2980
|
+
const tokenSettings = getTokenEstimationSettings();
|
|
2981
|
+
if (tokenSettings.enabled) {
|
|
2982
|
+
this.planComplexity = assessTaskComplexity({
|
|
2983
|
+
title: plan.task,
|
|
2984
|
+
description: plan.description || plan.task,
|
|
2985
|
+
// Include step info in complexity assessment
|
|
2986
|
+
technicalNotes: plan.steps?.map(s => s.title || s.type).join(', ')
|
|
2987
|
+
});
|
|
2988
|
+
|
|
2989
|
+
// Display complexity assessment
|
|
2990
|
+
displayComplexityAssessment(this.planComplexity);
|
|
2991
|
+
|
|
2992
|
+
// Warn if task might be too complex for hybrid mode
|
|
2993
|
+
if (this.planComplexity.level === 'xl') {
|
|
2994
|
+
log('yellow', ' ⚠️ This task is very complex. Consider breaking into smaller tasks.');
|
|
2995
|
+
log('yellow', ' Proceeding with maximum token budget...\n');
|
|
2996
|
+
}
|
|
2997
|
+
} else {
|
|
2998
|
+
log('dim', ' Token estimation disabled, using default budget');
|
|
2999
|
+
this.planComplexity = {
|
|
3000
|
+
level: tokenSettings.defaultLevel,
|
|
3001
|
+
estimatedTokens: getDefaultTokens(tokenSettings.defaultLevel),
|
|
3002
|
+
reasoning: 'Token estimation disabled'
|
|
3003
|
+
};
|
|
3004
|
+
}
|
|
3005
|
+
|
|
3006
|
+
// Get instruction richness based on complexity
|
|
3007
|
+
this.instructionRichness = getInstructionRichness(
|
|
3008
|
+
this.planComplexity.level,
|
|
3009
|
+
this.config.instructionRichness || {}
|
|
3010
|
+
);
|
|
3011
|
+
|
|
3012
|
+
// Set richness on template engine for context-aware rendering
|
|
3013
|
+
this.templates.setRichness(this.instructionRichness);
|
|
3014
|
+
|
|
3015
|
+
// Display richness settings
|
|
3016
|
+
displayInstructionRichness(this.instructionRichness);
|
|
3017
|
+
|
|
3018
|
+
// Generate project context ONCE before executing any steps
|
|
3019
|
+
// This context is prepended to each step's prompt (local LLM tokens are FREE)
|
|
3020
|
+
await this.ensureProjectContext();
|
|
3021
|
+
|
|
3022
|
+
this.state.updateHybridSession({
|
|
3023
|
+
currentPlan: plan.planId,
|
|
3024
|
+
pendingSteps: plan.steps.map(s => s.id)
|
|
3025
|
+
});
|
|
3026
|
+
|
|
3027
|
+
log('cyan', '\n' + '═'.repeat(60));
|
|
3028
|
+
log('cyan', ' EXECUTING PLAN');
|
|
3029
|
+
log('cyan', '═'.repeat(60));
|
|
3030
|
+
log('white', `\nTask: ${plan.task}`);
|
|
3031
|
+
log('white', `Steps: ${plan.steps.length}`);
|
|
3032
|
+
// Show executor type (local or cloud)
|
|
3033
|
+
const executorLabel = this.config.executorType === 'cloud'
|
|
3034
|
+
? `☁️ ${this.config.provider} / ${this.config.model}`
|
|
3035
|
+
: `🖥️ ${this.config.provider} / ${this.config.model}`;
|
|
3036
|
+
log('white', `Executor: ${executorLabel}`);
|
|
3037
|
+
log('dim', `Token Budget: ${this.planComplexity.estimatedTokens.toLocaleString()} (${this.planComplexity.level})\n`);
|
|
3038
|
+
|
|
3039
|
+
const steps = plan.steps;
|
|
3040
|
+
|
|
3041
|
+
while (this.completedSteps.size < steps.length) {
|
|
3042
|
+
const readySteps = steps.filter(step => {
|
|
3043
|
+
if (this.completedSteps.has(step.id)) return false;
|
|
3044
|
+
if (results.failedSteps.includes(step.id)) return false;
|
|
3045
|
+
|
|
3046
|
+
const deps = step.dependsOn || [];
|
|
3047
|
+
return deps.every(d => this.completedSteps.has(d));
|
|
3048
|
+
});
|
|
3049
|
+
|
|
3050
|
+
if (readySteps.length === 0) {
|
|
3051
|
+
if (this.completedSteps.size + results.failedSteps.length < steps.length) {
|
|
3052
|
+
log('red', '\n⚠️ Some steps cannot be executed due to failed dependencies');
|
|
3053
|
+
results.success = false;
|
|
3054
|
+
}
|
|
3055
|
+
break;
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
const parallelSteps = readySteps.filter(s => s.canParallelize !== false);
|
|
3059
|
+
const sequentialSteps = readySteps.filter(s => s.canParallelize === false);
|
|
3060
|
+
|
|
3061
|
+
// Execute parallel steps (includes single step case - Promise.all works fine)
|
|
3062
|
+
if (parallelSteps.length >= 1) {
|
|
3063
|
+
if (parallelSteps.length > 1) {
|
|
3064
|
+
log('cyan', `\n⚡ Executing ${parallelSteps.length} steps in parallel...\n`);
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
const parallelResults = await Promise.all(
|
|
3068
|
+
parallelSteps.map(step => this.executeStep(step, plan.context))
|
|
3069
|
+
);
|
|
3070
|
+
|
|
3071
|
+
for (let i = 0; i < parallelResults.length; i++) {
|
|
3072
|
+
const stepResult = parallelResults[i];
|
|
3073
|
+
const step = parallelSteps[i];
|
|
3074
|
+
|
|
3075
|
+
results.steps.push(stepResult);
|
|
3076
|
+
|
|
3077
|
+
if (stepResult.success) {
|
|
3078
|
+
this.completedSteps.add(step.id);
|
|
3079
|
+
} else {
|
|
3080
|
+
results.failedSteps.push(step.id);
|
|
3081
|
+
if (stepResult.escalate) {
|
|
3082
|
+
results.escalateToCloud.push(step);
|
|
3083
|
+
}
|
|
3084
|
+
results.success = false;
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
for (const step of sequentialSteps) {
|
|
3090
|
+
const stepResult = await this.executeStep(step, plan.context);
|
|
3091
|
+
results.steps.push(stepResult);
|
|
3092
|
+
|
|
3093
|
+
if (stepResult.success) {
|
|
3094
|
+
this.completedSteps.add(step.id);
|
|
3095
|
+
} else {
|
|
3096
|
+
results.failedSteps.push(step.id);
|
|
3097
|
+
if (stepResult.escalate) {
|
|
3098
|
+
results.escalateToCloud.push(step);
|
|
3099
|
+
}
|
|
3100
|
+
results.success = false;
|
|
3101
|
+
break;
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
results.completedAt = new Date().toISOString();
|
|
3107
|
+
|
|
3108
|
+
this.state.updateHybridSession({
|
|
3109
|
+
executedSteps: Array.from(this.completedSteps),
|
|
3110
|
+
failedSteps: results.failedSteps,
|
|
3111
|
+
pendingSteps: [],
|
|
3112
|
+
totalTokensSaved: results.tokensSaved
|
|
3113
|
+
});
|
|
3114
|
+
|
|
3115
|
+
this.state.saveResults(results);
|
|
3116
|
+
|
|
3117
|
+
// Log metrics for accuracy tracking
|
|
3118
|
+
logTokenMetrics(plan, results, this.planComplexity);
|
|
3119
|
+
|
|
3120
|
+
if (results.success) {
|
|
3121
|
+
this.rollback.clearCheckpoint();
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
return results;
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
async executeStep(step, context) {
|
|
3128
|
+
const result = {
|
|
3129
|
+
stepId: step.id,
|
|
3130
|
+
title: step.title,
|
|
3131
|
+
success: false,
|
|
3132
|
+
attempts: 0,
|
|
3133
|
+
errors: [],
|
|
3134
|
+
escalate: false
|
|
3135
|
+
};
|
|
3136
|
+
|
|
3137
|
+
log('white', '\n' + '─'.repeat(60));
|
|
3138
|
+
log('cyan', `📋 Step ${step.id}: ${step.title}`);
|
|
3139
|
+
log('dim', ` Type: ${step.type}`);
|
|
3140
|
+
if (step.params?.path) {
|
|
3141
|
+
log('dim', ` Path: ${step.params.path}`);
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
const templateName = step.template || step.type;
|
|
3145
|
+
|
|
3146
|
+
// Load project-specific context from app-map and config
|
|
3147
|
+
const projectContext = this.state.loadProjectContext();
|
|
3148
|
+
|
|
3149
|
+
let params = { ...step.params, ...context, ...projectContext };
|
|
3150
|
+
|
|
3151
|
+
if (step.type === 'modify-file' && step.params?.path) {
|
|
3152
|
+
const filePath = step.params.path;
|
|
3153
|
+
if (fs.existsSync(filePath)) {
|
|
3154
|
+
params.currentContent = fs.readFileSync(filePath, 'utf-8');
|
|
3155
|
+
this.rollback.trackModification(filePath);
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
let prompt;
|
|
3160
|
+
try {
|
|
3161
|
+
prompt = this.templates.render(templateName, params);
|
|
3162
|
+
} catch (err) {
|
|
3163
|
+
result.errors.push(`Template error: ${err.message}`);
|
|
3164
|
+
log('red', ` ❌ Template error: ${err.message}`);
|
|
3165
|
+
return result;
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
// INJECT ACTIVE PATTERNS from decisions.md, app-map.md, and skills
|
|
3169
|
+
// This ensures learned patterns are prominently displayed and enforced
|
|
3170
|
+
const taskContext = {
|
|
3171
|
+
description: step.description || params.task || '',
|
|
3172
|
+
file: step.params?.path || step.file || '',
|
|
3173
|
+
action: step.action || templateName
|
|
3174
|
+
};
|
|
3175
|
+
prompt = injectPatterns(prompt, taskContext, PROJECT_ROOT);
|
|
3176
|
+
|
|
3177
|
+
// PREPEND PROJECT CONTEXT - Local LLM tokens are FREE
|
|
3178
|
+
// This gives the LLM comprehensive knowledge about types, theme, patterns
|
|
3179
|
+
if (this.projectContext) {
|
|
3180
|
+
prompt = this.projectContext + '\n\n---\n\n# Step Instructions\n\n' + prompt;
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
// Add model-specific guidance (weaknesses to avoid, patterns that work)
|
|
3184
|
+
const modelAdjustments = getPromptAdjustments(this.config.model);
|
|
3185
|
+
if (modelAdjustments.guidance) {
|
|
3186
|
+
prompt = `## Model-Specific Guidance\n\n${modelAdjustments.guidance}\n\n---\n\n${prompt}`;
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
// Show initial context info
|
|
3190
|
+
const initialTokens = estimateTokens(prompt);
|
|
3191
|
+
log('dim', ` Prompt size: ~${initialTokens.toLocaleString()} tokens (includes project context - FREE)`);
|
|
3192
|
+
|
|
3193
|
+
// ADAPTIVE LEARNING: Save original prompt for refinement during retries
|
|
3194
|
+
const originalPrompt = prompt;
|
|
3195
|
+
|
|
3196
|
+
// Smart retry tracking - detect stuck loops and progress
|
|
3197
|
+
const errorHistory = [];
|
|
3198
|
+
const errorSignatures = new Map(); // Track how many times we see each error pattern
|
|
3199
|
+
let consecutiveSameError = 0;
|
|
3200
|
+
let lastErrorSignature = null;
|
|
3201
|
+
|
|
3202
|
+
/**
|
|
3203
|
+
* Extract a signature from an error message for comparison
|
|
3204
|
+
* Normalizes variable parts (line numbers, specific values) to detect same error type
|
|
3205
|
+
*/
|
|
3206
|
+
const getErrorSignature = (errorMsg) => {
|
|
3207
|
+
if (!errorMsg) return 'unknown';
|
|
3208
|
+
return errorMsg
|
|
3209
|
+
.replace(/line \d+/gi, 'line N')
|
|
3210
|
+
.replace(/:\d+:\d+/g, ':N:N')
|
|
3211
|
+
.replace(/'[^']+'/g, "'X'")
|
|
3212
|
+
.replace(/"[^"]+"/g, '"X"')
|
|
3213
|
+
.replace(/\d+/g, 'N')
|
|
3214
|
+
.substring(0, 100);
|
|
3215
|
+
};
|
|
3216
|
+
|
|
3217
|
+
/**
|
|
3218
|
+
* Categorize error type for targeted fix strategies
|
|
3219
|
+
*/
|
|
3220
|
+
const categorizeError = (errorMsg) => {
|
|
3221
|
+
if (!errorMsg) return 'unknown';
|
|
3222
|
+
const msg = errorMsg.toLowerCase();
|
|
3223
|
+
if (msg.includes('cannot find module') || msg.includes('import')) return 'import';
|
|
3224
|
+
if (msg.includes('type') && (msg.includes('not assignable') || msg.includes('missing'))) return 'type';
|
|
3225
|
+
if (msg.includes('syntax') || msg.includes('unexpected token')) return 'syntax';
|
|
3226
|
+
if (msg.includes('eslint') || msg.includes('prettier')) return 'lint';
|
|
3227
|
+
if (msg.includes('semantic') || msg.includes('confidence')) return 'semantic';
|
|
3228
|
+
return 'other';
|
|
3229
|
+
};
|
|
3230
|
+
|
|
3231
|
+
for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
|
|
3232
|
+
result.attempts = attempt + 1;
|
|
3233
|
+
|
|
3234
|
+
// Smart retry: Check if we're stuck in a loop
|
|
3235
|
+
if (consecutiveSameError >= 3) {
|
|
3236
|
+
log('red', ` ⚠️ Same error repeated ${consecutiveSameError} times - escalating`);
|
|
3237
|
+
result.errors.push(`Stuck on error: ${lastErrorSignature}`);
|
|
3238
|
+
result.escalate = true;
|
|
3239
|
+
break;
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
// Smart retry: If we've seen 5+ different errors, we might be thrashing
|
|
3243
|
+
if (errorHistory.length >= 5 && new Set(errorHistory.map(e => e.category)).size >= 4) {
|
|
3244
|
+
log('yellow', ` ⚠️ Multiple error types encountered - may need different approach`);
|
|
3245
|
+
}
|
|
3246
|
+
|
|
3247
|
+
log('dim', ` Attempt ${attempt + 1}/${this.config.maxRetries + 1}...`);
|
|
3248
|
+
|
|
3249
|
+
try {
|
|
3250
|
+
// Auto-compact prompt if needed
|
|
3251
|
+
const contextWindow = this.llm.contextWindow || 4096;
|
|
3252
|
+
// Reserve 30% of context for output, but cap at 2048 tokens
|
|
3253
|
+
const reserveForOutput = Math.min(2048, Math.floor(contextWindow * 0.3));
|
|
3254
|
+
const { prompt: compactedPrompt, wasCompacted, usage } = autoCompactPrompt(
|
|
3255
|
+
prompt,
|
|
3256
|
+
contextWindow,
|
|
3257
|
+
reserveForOutput
|
|
3258
|
+
);
|
|
3259
|
+
|
|
3260
|
+
if (wasCompacted) {
|
|
3261
|
+
prompt = compactedPrompt;
|
|
3262
|
+
}
|
|
3263
|
+
|
|
3264
|
+
// Log context usage
|
|
3265
|
+
if (usage > 80) {
|
|
3266
|
+
log('yellow', ` ⚠️ Context usage: ${usage}%`);
|
|
3267
|
+
} else if (process.env.DEBUG_HYBRID) {
|
|
3268
|
+
log('dim', ` Context usage: ${usage}%`);
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3271
|
+
const startTime = Date.now();
|
|
3272
|
+
const output = await this.llm.generate(prompt);
|
|
3273
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
3274
|
+
log('dim', ` Generated in ${duration}s`);
|
|
3275
|
+
|
|
3276
|
+
let cleanOutput = this.cleanOutput(output);
|
|
3277
|
+
|
|
3278
|
+
const outputPath = step.params?.path;
|
|
3279
|
+
|
|
3280
|
+
// Auto-correct common LLM mistakes (React imports, paths, etc.)
|
|
3281
|
+
const { corrected: autoFixed } = autoCorrectCode(cleanOutput, outputPath);
|
|
3282
|
+
cleanOutput = autoFixed;
|
|
3283
|
+
|
|
3284
|
+
// CRITICAL: Validate code BEFORE writing to prevent file corruption
|
|
3285
|
+
const codeValidation = isValidCode(cleanOutput);
|
|
3286
|
+
if (!codeValidation.valid) {
|
|
3287
|
+
log('red', ` ❌ Invalid code output: ${codeValidation.reason}`);
|
|
3288
|
+
result.errors.push(`Invalid code: ${codeValidation.reason}`);
|
|
3289
|
+
|
|
3290
|
+
// Add error context for retry
|
|
3291
|
+
prompt += `\n\n## PREVIOUS ERROR\n\nYour output was not valid code. ${codeValidation.reason}\n\nOutput ONLY valid TypeScript/JavaScript code. No explanations, no markdown, no thinking.`;
|
|
3292
|
+
continue; // Skip file write, retry
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
// Semantic validation: check if output matches what was requested
|
|
3296
|
+
const semanticValidation = validateOutputMatchesTask(cleanOutput, step);
|
|
3297
|
+
if (!semanticValidation.valid) {
|
|
3298
|
+
log('yellow', ` ⚠️ Semantic mismatch (confidence: ${semanticValidation.confidence}%): ${semanticValidation.reason}`);
|
|
3299
|
+
|
|
3300
|
+
// If confidence is very low, treat as error and retry
|
|
3301
|
+
if (semanticValidation.confidence < 30) {
|
|
3302
|
+
log('red', ` ❌ Output doesn't match task - retrying with clarification`);
|
|
3303
|
+
result.errors.push(`Semantic mismatch: ${semanticValidation.reason}`);
|
|
3304
|
+
|
|
3305
|
+
// Add clarification for retry
|
|
3306
|
+
const expectedName = step.params?.name || path.basename(step.params?.path || '', path.extname(step.params?.path || ''));
|
|
3307
|
+
prompt += `\n\n## PREVIOUS ERROR - WRONG OUTPUT\n\nYour output did not match the task. ${semanticValidation.reason}\n\n**CRITICAL**: You must create "${expectedName}", not something else.\nLook at the "YOUR TASK" section and implement EXACTLY what is requested.`;
|
|
3308
|
+
continue; // Retry with clarification
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
// Medium confidence - warn but proceed
|
|
3312
|
+
log('dim', ` Proceeding despite semantic concerns`);
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
// Import validation: check against available components from config
|
|
3316
|
+
const importValidation = validateImports(cleanOutput);
|
|
3317
|
+
if (!importValidation.valid) {
|
|
3318
|
+
log('red', ` ❌ Import errors: ${importValidation.errors.join(', ')}`);
|
|
3319
|
+
result.errors.push(`Import errors: ${importValidation.errors.join('; ')}`);
|
|
3320
|
+
|
|
3321
|
+
// Add hint to prompt for retry
|
|
3322
|
+
prompt += `\n\n## PREVIOUS ERROR - IMPORT ISSUES\n\nYour code has invalid imports:\n${importValidation.errors.map(e => `- ${e}`).join('\n')}\n\nCheck the "Available Components" section and use ONLY those exact imports.\nDO NOT guess import paths or exports.`;
|
|
3323
|
+
continue; // Retry with corrected hints
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
// Log warnings but don't fail
|
|
3327
|
+
if (importValidation.warnings.length > 0) {
|
|
3328
|
+
for (const warning of importValidation.warnings) {
|
|
3329
|
+
log('yellow', ` ⚠️ ${warning}`);
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
if (outputPath) {
|
|
3334
|
+
const dir = path.dirname(outputPath);
|
|
3335
|
+
if (!fs.existsSync(dir)) {
|
|
3336
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
const isNew = !fs.existsSync(outputPath);
|
|
3340
|
+
|
|
3341
|
+
// For modify-file, do a sanity check: new content shouldn't be drastically smaller
|
|
3342
|
+
if (!isNew && step.type === 'modify-file') {
|
|
3343
|
+
const existingContent = fs.readFileSync(outputPath, 'utf-8');
|
|
3344
|
+
const sizeRatio = cleanOutput.length / existingContent.length;
|
|
3345
|
+
if (sizeRatio < 0.3 && existingContent.length > 100) {
|
|
3346
|
+
log('red', ` ❌ Output suspiciously small (${Math.round(sizeRatio * 100)}% of original)`);
|
|
3347
|
+
result.errors.push('Output file size too small - likely incomplete');
|
|
3348
|
+
prompt += `\n\n## PREVIOUS ERROR\n\nYour output was only ${Math.round(sizeRatio * 100)}% the size of the original file. You must output the COMPLETE file, not a partial snippet.`;
|
|
3349
|
+
continue; // Skip write, retry
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3353
|
+
fs.writeFileSync(outputPath, cleanOutput);
|
|
3354
|
+
|
|
3355
|
+
if (isNew) {
|
|
3356
|
+
this.rollback.trackCreation(outputPath);
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
const checks = step.validation?.checks || ['file-exists', 'typescript-check'];
|
|
3361
|
+
const validationResults = Validator.runChecks(checks, outputPath);
|
|
3362
|
+
|
|
3363
|
+
const allPassed = validationResults.every(r => r.success);
|
|
3364
|
+
|
|
3365
|
+
if (allPassed) {
|
|
3366
|
+
result.success = true;
|
|
3367
|
+
|
|
3368
|
+
this.state.updateRequestLog(step, 'completed', 'hybrid', this.config.model);
|
|
3369
|
+
|
|
3370
|
+
if (step.stateUpdates?.appMap) {
|
|
3371
|
+
this.state.updateAppMap(step.stateUpdates.appMap);
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
// Record success for model learning
|
|
3375
|
+
recordModelResult(this.config.model, {
|
|
3376
|
+
taskType: step.action || 'unknown',
|
|
3377
|
+
success: true
|
|
3378
|
+
});
|
|
3379
|
+
|
|
3380
|
+
// ADAPTIVE LEARNING: If we had failures before success, record what we learned
|
|
3381
|
+
if (errorHistory.length > 0) {
|
|
3382
|
+
// Use cached failure analyses from errorHistory (already analyzed during retry loop)
|
|
3383
|
+
const adaptiveFailures = errorHistory
|
|
3384
|
+
.map(e => e.analysis)
|
|
3385
|
+
.filter(Boolean);
|
|
3386
|
+
|
|
3387
|
+
if (adaptiveFailures.length > 0) {
|
|
3388
|
+
recordSuccessfulRecovery(this.config.model, adaptiveFailures, {
|
|
3389
|
+
taskId: step.id || step.description,
|
|
3390
|
+
attemptsTaken: result.attempts,
|
|
3391
|
+
taskType: step.action
|
|
3392
|
+
});
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3396
|
+
log('green', ` ✅ Step completed`);
|
|
3397
|
+
return result;
|
|
3398
|
+
} else {
|
|
3399
|
+
const failedCheck = validationResults.find(r => !r.success);
|
|
3400
|
+
result.errors.push(failedCheck.message);
|
|
3401
|
+
log('yellow', ` ⚠️ Validation failed: ${failedCheck.check}`);
|
|
3402
|
+
log('dim', ` ${failedCheck.message.slice(0, 100)}`);
|
|
3403
|
+
|
|
3404
|
+
// Smart retry: Track this error
|
|
3405
|
+
const errorSig = getErrorSignature(failedCheck.message);
|
|
3406
|
+
const errorCat = categorizeError(failedCheck.message);
|
|
3407
|
+
errorHistory.push({ message: failedCheck.message, signature: errorSig, category: errorCat });
|
|
3408
|
+
|
|
3409
|
+
if (errorSig === lastErrorSignature) {
|
|
3410
|
+
consecutiveSameError++;
|
|
3411
|
+
log('dim', ` (Same error ${consecutiveSameError}x)`);
|
|
3412
|
+
} else {
|
|
3413
|
+
consecutiveSameError = 1;
|
|
3414
|
+
lastErrorSignature = errorSig;
|
|
3415
|
+
// Progress! Different error means we fixed something
|
|
3416
|
+
if (errorHistory.length > 1) {
|
|
3417
|
+
log('dim', ` (Different error - making progress)`);
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
|
|
3421
|
+
// ADAPTIVE LEARNING: Use smart prompt refinement based on failure analysis
|
|
3422
|
+
const failureAnalysis = analyzeFailure(failedCheck.message, null, {
|
|
3423
|
+
taskType: step.action,
|
|
3424
|
+
targetFile: step.params?.path
|
|
3425
|
+
});
|
|
3426
|
+
|
|
3427
|
+
// Store analysis in errorHistory for later use (avoid duplicate analysis)
|
|
3428
|
+
errorHistory[errorHistory.length - 1].analysis = failureAnalysis;
|
|
3429
|
+
|
|
3430
|
+
// Use cached analyses from previous errors
|
|
3431
|
+
const previousFailures = errorHistory.slice(0, -1)
|
|
3432
|
+
.map(e => e.analysis)
|
|
3433
|
+
.filter(Boolean);
|
|
3434
|
+
|
|
3435
|
+
const refined = refinePromptForRetry(originalPrompt, failureAnalysis, previousFailures);
|
|
3436
|
+
prompt = refined.prompt;
|
|
3437
|
+
log('dim', ` 📝 Applying ${refined.strategy} refinement strategy`);
|
|
3438
|
+
}
|
|
3439
|
+
} catch (err) {
|
|
3440
|
+
result.errors.push(err.message);
|
|
3441
|
+
log('red', ` ❌ Error: ${err.message}`);
|
|
3442
|
+
|
|
3443
|
+
// Smart retry: Track catch errors too
|
|
3444
|
+
const errorSig = getErrorSignature(err.message);
|
|
3445
|
+
const errorCat = categorizeError(err.message);
|
|
3446
|
+
errorHistory.push({ message: err.message, signature: errorSig, category: errorCat });
|
|
3447
|
+
|
|
3448
|
+
if (errorSig === lastErrorSignature) {
|
|
3449
|
+
consecutiveSameError++;
|
|
3450
|
+
} else {
|
|
3451
|
+
consecutiveSameError = 1;
|
|
3452
|
+
lastErrorSignature = errorSig;
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3457
|
+
result.escalate = true;
|
|
3458
|
+
this.state.updateRequestLog(step, 'failed - needs escalation', 'hybrid', this.config.model);
|
|
3459
|
+
log('red', ` ❌ Step failed after ${result.attempts} attempts`);
|
|
3460
|
+
if (errorHistory.length > 0) {
|
|
3461
|
+
const errorTypes = [...new Set(errorHistory.map(e => e.category))];
|
|
3462
|
+
log('dim', ` Error types encountered: ${errorTypes.join(', ')}`);
|
|
3463
|
+
}
|
|
3464
|
+
log('yellow', ` ⬆️ Flagged for escalation to Claude`);
|
|
3465
|
+
|
|
3466
|
+
// Record failure for model learning
|
|
3467
|
+
recordModelResult(this.config.model, {
|
|
3468
|
+
taskType: step.action || 'unknown',
|
|
3469
|
+
success: false,
|
|
3470
|
+
errorType: errorHistory[0]?.category || 'unknown',
|
|
3471
|
+
errorContext: errorHistory[0]?.message?.slice(0, 200) || null
|
|
3472
|
+
});
|
|
3473
|
+
|
|
3474
|
+
// Save structured failure info for retry context
|
|
3475
|
+
saveStructuredFailure(step, errorHistory, result.attempts, this.config);
|
|
3476
|
+
|
|
3477
|
+
return result;
|
|
3478
|
+
}
|
|
3479
|
+
|
|
3480
|
+
cleanOutput(output, error = null) {
|
|
3481
|
+
// Use the comprehensive extraction function first
|
|
3482
|
+
let extracted = extractCodeFromResponse(output, this.config.model);
|
|
3483
|
+
|
|
3484
|
+
// If there was an error and extraction didn't help much, try response parser
|
|
3485
|
+
if (error && extracted && extracted.length < 20) {
|
|
3486
|
+
const parsed = parseOnRetry(output, error);
|
|
3487
|
+
if (parsed.shouldRetry && parsed.content) {
|
|
3488
|
+
log('dim', ' Using response parser fallback');
|
|
3489
|
+
extracted = cleanCodeBlock(parsed.content);
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3493
|
+
return extracted;
|
|
3494
|
+
}
|
|
3495
|
+
|
|
3496
|
+
printSummary(results) {
|
|
3497
|
+
log('white', '\n' + '═'.repeat(60));
|
|
3498
|
+
log('cyan', ' EXECUTION SUMMARY');
|
|
3499
|
+
log('white', '═'.repeat(60));
|
|
3500
|
+
|
|
3501
|
+
const successCount = results.steps.filter(s => s.success).length;
|
|
3502
|
+
const totalCount = results.steps.length;
|
|
3503
|
+
|
|
3504
|
+
if (results.success) {
|
|
3505
|
+
log('green', `\n✅ Plan executed successfully!`);
|
|
3506
|
+
} else {
|
|
3507
|
+
log('red', `\n❌ Plan execution failed`);
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3510
|
+
log('white', `\nSteps completed: ${successCount}/${totalCount}`);
|
|
3511
|
+
log('white', `Tokens saved: ~${results.tokensSaved.toLocaleString()}`);
|
|
3512
|
+
|
|
3513
|
+
if (results.escalateToCloud.length > 0) {
|
|
3514
|
+
log('yellow', `\n⚠️ Steps requiring Claude escalation:`);
|
|
3515
|
+
for (const step of results.escalateToCloud) {
|
|
3516
|
+
log('yellow', ` • Step ${step.id}: ${step.title}`);
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3520
|
+
log('dim', `\nResults saved to: .workflow/state/hybrid-results.json`);
|
|
3521
|
+
log('white', '');
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
// ============================================================
|
|
3526
|
+
// Main CLI
|
|
3527
|
+
// ============================================================
|
|
3528
|
+
|
|
3529
|
+
async function main() {
|
|
3530
|
+
const args = process.argv.slice(2);
|
|
3531
|
+
|
|
3532
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
3533
|
+
console.log(`
|
|
3534
|
+
Wogi Flow Hybrid Orchestrator
|
|
3535
|
+
|
|
3536
|
+
Usage:
|
|
3537
|
+
flow-orchestrate <plan.json> Execute a plan file
|
|
3538
|
+
flow-orchestrate --resume Resume from checkpoint
|
|
3539
|
+
flow-orchestrate --rollback Rollback last execution
|
|
3540
|
+
flow-orchestrate --help Show this help
|
|
3541
|
+
|
|
3542
|
+
Examples:
|
|
3543
|
+
./scripts/flow-orchestrate /tmp/plan.json
|
|
3544
|
+
./scripts/flow-orchestrate --rollback
|
|
3545
|
+
`);
|
|
3546
|
+
process.exit(0);
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3549
|
+
if (args.includes('--rollback')) {
|
|
3550
|
+
const rollback = new RollbackManager();
|
|
3551
|
+
if (rollback.loadCheckpoint()) {
|
|
3552
|
+
rollback.rollback();
|
|
3553
|
+
} else {
|
|
3554
|
+
log('yellow', 'No rollback checkpoint found.');
|
|
3555
|
+
}
|
|
3556
|
+
process.exit(0);
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3559
|
+
if (args.includes('--resume')) {
|
|
3560
|
+
log('yellow', 'Resume not yet implemented');
|
|
3561
|
+
process.exit(1);
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
const planPath = args[0];
|
|
3565
|
+
if (!planPath) {
|
|
3566
|
+
console.error('Usage: flow-orchestrate <plan.json>');
|
|
3567
|
+
process.exit(1);
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3570
|
+
if (!fs.existsSync(planPath)) {
|
|
3571
|
+
console.error(`Plan file not found: ${planPath}`);
|
|
3572
|
+
process.exit(1);
|
|
3573
|
+
}
|
|
3574
|
+
|
|
3575
|
+
const plan = JSON.parse(fs.readFileSync(planPath, 'utf-8'));
|
|
3576
|
+
|
|
3577
|
+
try {
|
|
3578
|
+
const orchestrator = new Orchestrator();
|
|
3579
|
+
const results = await orchestrator.executePlan(plan);
|
|
3580
|
+
orchestrator.printSummary(results);
|
|
3581
|
+
|
|
3582
|
+
process.exit(results.success ? 0 : 1);
|
|
3583
|
+
} catch (err) {
|
|
3584
|
+
log('red', `\n❌ Orchestrator error: ${err.message}`);
|
|
3585
|
+
process.exit(1);
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3589
|
+
main().catch(err => {
|
|
3590
|
+
console.error(`\x1b[31mFatal error: ${err.message}\x1b[0m`);
|
|
3591
|
+
process.exit(1);
|
|
3592
|
+
});
|