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.
Files changed (221) hide show
  1. package/.workflow/agents/reviewer.md +81 -0
  2. package/.workflow/agents/security.md +94 -0
  3. package/.workflow/agents/story-writer.md +58 -0
  4. package/.workflow/bridges/base-bridge.js +395 -0
  5. package/.workflow/bridges/claude-bridge.js +434 -0
  6. package/.workflow/bridges/index.js +130 -0
  7. package/.workflow/lib/assumption-detector.js +481 -0
  8. package/.workflow/lib/config-substitution.js +371 -0
  9. package/.workflow/lib/failure-categories.js +478 -0
  10. package/.workflow/state/app-map.md.template +15 -0
  11. package/.workflow/state/architecture.md.template +24 -0
  12. package/.workflow/state/component-index.json.template +5 -0
  13. package/.workflow/state/decisions.md.template +15 -0
  14. package/.workflow/state/feedback-patterns.md.template +9 -0
  15. package/.workflow/state/knowledge-sync.json.template +6 -0
  16. package/.workflow/state/progress.md.template +14 -0
  17. package/.workflow/state/ready.json.template +7 -0
  18. package/.workflow/state/request-log.md.template +14 -0
  19. package/.workflow/state/session-state.json.template +11 -0
  20. package/.workflow/state/stack.md.template +33 -0
  21. package/.workflow/state/testing.md.template +36 -0
  22. package/.workflow/templates/claude-md.hbs +257 -0
  23. package/.workflow/templates/correction-report.md +67 -0
  24. package/.workflow/templates/gemini-md.hbs +52 -0
  25. package/README.md +1802 -0
  26. package/bin/flow +205 -0
  27. package/lib/index.js +33 -0
  28. package/lib/installer.js +467 -0
  29. package/lib/release-channel.js +269 -0
  30. package/lib/skill-registry.js +526 -0
  31. package/lib/upgrader.js +401 -0
  32. package/lib/utils.js +305 -0
  33. package/package.json +64 -0
  34. package/scripts/flow +985 -0
  35. package/scripts/flow-adaptive-learning.js +1259 -0
  36. package/scripts/flow-aggregate.js +488 -0
  37. package/scripts/flow-archive +133 -0
  38. package/scripts/flow-auto-context.js +1015 -0
  39. package/scripts/flow-auto-learn.js +615 -0
  40. package/scripts/flow-bridge.js +223 -0
  41. package/scripts/flow-browser-suggest.js +316 -0
  42. package/scripts/flow-bug.js +247 -0
  43. package/scripts/flow-cascade.js +711 -0
  44. package/scripts/flow-changelog +85 -0
  45. package/scripts/flow-checkpoint.js +483 -0
  46. package/scripts/flow-cli.js +403 -0
  47. package/scripts/flow-code-intelligence.js +760 -0
  48. package/scripts/flow-complexity.js +502 -0
  49. package/scripts/flow-config-set.js +152 -0
  50. package/scripts/flow-constants.js +157 -0
  51. package/scripts/flow-context +152 -0
  52. package/scripts/flow-context-init.js +482 -0
  53. package/scripts/flow-context-monitor.js +384 -0
  54. package/scripts/flow-context-scoring.js +886 -0
  55. package/scripts/flow-correct.js +458 -0
  56. package/scripts/flow-damage-control.js +985 -0
  57. package/scripts/flow-deps +101 -0
  58. package/scripts/flow-diff.js +700 -0
  59. package/scripts/flow-done +151 -0
  60. package/scripts/flow-done.js +489 -0
  61. package/scripts/flow-durable-session.js +1541 -0
  62. package/scripts/flow-entropy-monitor.js +345 -0
  63. package/scripts/flow-export-profile +349 -0
  64. package/scripts/flow-export-scanner.js +1046 -0
  65. package/scripts/flow-figma-confirm.js +400 -0
  66. package/scripts/flow-figma-extract.js +496 -0
  67. package/scripts/flow-figma-generate.js +683 -0
  68. package/scripts/flow-figma-index.js +909 -0
  69. package/scripts/flow-figma-match.js +617 -0
  70. package/scripts/flow-figma-mcp-server.js +518 -0
  71. package/scripts/flow-figma-pipeline.js +414 -0
  72. package/scripts/flow-file-ops.js +301 -0
  73. package/scripts/flow-gate-confidence.js +825 -0
  74. package/scripts/flow-guided-edit.js +659 -0
  75. package/scripts/flow-health +185 -0
  76. package/scripts/flow-health.js +413 -0
  77. package/scripts/flow-hooks.js +556 -0
  78. package/scripts/flow-http-client.js +249 -0
  79. package/scripts/flow-hybrid-detect.js +167 -0
  80. package/scripts/flow-hybrid-interactive.js +591 -0
  81. package/scripts/flow-hybrid-test.js +152 -0
  82. package/scripts/flow-import-profile +439 -0
  83. package/scripts/flow-init +253 -0
  84. package/scripts/flow-instruction-richness.js +827 -0
  85. package/scripts/flow-jira-integration.js +579 -0
  86. package/scripts/flow-knowledge-router.js +522 -0
  87. package/scripts/flow-knowledge-sync.js +589 -0
  88. package/scripts/flow-linear-integration.js +631 -0
  89. package/scripts/flow-links.js +774 -0
  90. package/scripts/flow-log-manager.js +559 -0
  91. package/scripts/flow-loop-enforcer.js +1246 -0
  92. package/scripts/flow-loop-retry-learning.js +630 -0
  93. package/scripts/flow-lsp.js +923 -0
  94. package/scripts/flow-map-index +348 -0
  95. package/scripts/flow-map-sync +201 -0
  96. package/scripts/flow-memory-blocks.js +668 -0
  97. package/scripts/flow-memory-compactor.js +350 -0
  98. package/scripts/flow-memory-db.js +1110 -0
  99. package/scripts/flow-memory-sync.js +484 -0
  100. package/scripts/flow-metrics.js +353 -0
  101. package/scripts/flow-migrate-ids.js +370 -0
  102. package/scripts/flow-model-adapter.js +802 -0
  103. package/scripts/flow-model-router.js +884 -0
  104. package/scripts/flow-models.js +1231 -0
  105. package/scripts/flow-morning.js +517 -0
  106. package/scripts/flow-multi-approach.js +660 -0
  107. package/scripts/flow-new-feature +86 -0
  108. package/scripts/flow-onboard +1042 -0
  109. package/scripts/flow-orchestrate-llm.js +459 -0
  110. package/scripts/flow-orchestrate.js +3592 -0
  111. package/scripts/flow-output.js +123 -0
  112. package/scripts/flow-parallel-detector.js +399 -0
  113. package/scripts/flow-parallel-dispatch.js +987 -0
  114. package/scripts/flow-parallel.js +428 -0
  115. package/scripts/flow-pattern-enforcer.js +600 -0
  116. package/scripts/flow-prd-manager.js +282 -0
  117. package/scripts/flow-progress.js +323 -0
  118. package/scripts/flow-project-analyzer.js +975 -0
  119. package/scripts/flow-prompt-composer.js +487 -0
  120. package/scripts/flow-providers.js +1381 -0
  121. package/scripts/flow-queue.js +308 -0
  122. package/scripts/flow-ready +82 -0
  123. package/scripts/flow-ready.js +189 -0
  124. package/scripts/flow-regression.js +396 -0
  125. package/scripts/flow-response-parser.js +450 -0
  126. package/scripts/flow-resume.js +284 -0
  127. package/scripts/flow-rules-sync.js +439 -0
  128. package/scripts/flow-run-trace.js +718 -0
  129. package/scripts/flow-safety.js +587 -0
  130. package/scripts/flow-search +104 -0
  131. package/scripts/flow-security.js +481 -0
  132. package/scripts/flow-session-end +106 -0
  133. package/scripts/flow-session-end.js +437 -0
  134. package/scripts/flow-session-state.js +671 -0
  135. package/scripts/flow-setup-hooks +216 -0
  136. package/scripts/flow-setup-hooks.js +377 -0
  137. package/scripts/flow-skill-create.js +329 -0
  138. package/scripts/flow-skill-creator.js +572 -0
  139. package/scripts/flow-skill-generator.js +1046 -0
  140. package/scripts/flow-skill-learn.js +880 -0
  141. package/scripts/flow-skill-matcher.js +578 -0
  142. package/scripts/flow-spec-generator.js +820 -0
  143. package/scripts/flow-stack-wizard.js +895 -0
  144. package/scripts/flow-standup +162 -0
  145. package/scripts/flow-start +74 -0
  146. package/scripts/flow-start.js +235 -0
  147. package/scripts/flow-status +110 -0
  148. package/scripts/flow-status.js +301 -0
  149. package/scripts/flow-step-browser.js +83 -0
  150. package/scripts/flow-step-changelog.js +217 -0
  151. package/scripts/flow-step-comments.js +306 -0
  152. package/scripts/flow-step-complexity.js +234 -0
  153. package/scripts/flow-step-coverage.js +218 -0
  154. package/scripts/flow-step-knowledge.js +193 -0
  155. package/scripts/flow-step-pr-tests.js +364 -0
  156. package/scripts/flow-step-regression.js +89 -0
  157. package/scripts/flow-step-review.js +516 -0
  158. package/scripts/flow-step-security.js +162 -0
  159. package/scripts/flow-step-silent-failures.js +290 -0
  160. package/scripts/flow-step-simplifier.js +346 -0
  161. package/scripts/flow-story +105 -0
  162. package/scripts/flow-story.js +500 -0
  163. package/scripts/flow-suspend.js +252 -0
  164. package/scripts/flow-sync-daemon.js +654 -0
  165. package/scripts/flow-task-analyzer.js +606 -0
  166. package/scripts/flow-team-dashboard.js +748 -0
  167. package/scripts/flow-team-sync.js +752 -0
  168. package/scripts/flow-team.js +977 -0
  169. package/scripts/flow-tech-options.js +528 -0
  170. package/scripts/flow-templates.js +812 -0
  171. package/scripts/flow-tiered-learning.js +728 -0
  172. package/scripts/flow-trace +204 -0
  173. package/scripts/flow-transcript-chunking.js +1106 -0
  174. package/scripts/flow-transcript-digest.js +7918 -0
  175. package/scripts/flow-transcript-language.js +465 -0
  176. package/scripts/flow-transcript-parsing.js +1085 -0
  177. package/scripts/flow-transcript-stories.js +2194 -0
  178. package/scripts/flow-update-map +224 -0
  179. package/scripts/flow-utils.js +2242 -0
  180. package/scripts/flow-verification.js +644 -0
  181. package/scripts/flow-verify.js +1177 -0
  182. package/scripts/flow-voice-input.js +638 -0
  183. package/scripts/flow-watch +168 -0
  184. package/scripts/flow-workflow-steps.js +521 -0
  185. package/scripts/flow-workflow.js +1029 -0
  186. package/scripts/flow-worktree.js +489 -0
  187. package/scripts/hooks/adapters/base-adapter.js +102 -0
  188. package/scripts/hooks/adapters/claude-code.js +359 -0
  189. package/scripts/hooks/adapters/index.js +79 -0
  190. package/scripts/hooks/core/component-check.js +341 -0
  191. package/scripts/hooks/core/index.js +35 -0
  192. package/scripts/hooks/core/loop-check.js +241 -0
  193. package/scripts/hooks/core/session-context.js +294 -0
  194. package/scripts/hooks/core/task-gate.js +177 -0
  195. package/scripts/hooks/core/validation.js +230 -0
  196. package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
  197. package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
  198. package/scripts/hooks/entry/claude-code/session-end.js +87 -0
  199. package/scripts/hooks/entry/claude-code/session-start.js +46 -0
  200. package/scripts/hooks/entry/claude-code/stop.js +43 -0
  201. package/scripts/postinstall.js +139 -0
  202. package/templates/browser-test-flow.json +56 -0
  203. package/templates/bug-report.md +43 -0
  204. package/templates/component-detail.md +42 -0
  205. package/templates/component.stories.tsx +49 -0
  206. package/templates/context/constraints.md +83 -0
  207. package/templates/context/conventions.md +177 -0
  208. package/templates/context/stack.md +60 -0
  209. package/templates/correction-report.md +90 -0
  210. package/templates/feature-proposal.md +35 -0
  211. package/templates/hybrid/_base.md +254 -0
  212. package/templates/hybrid/_patterns.md +45 -0
  213. package/templates/hybrid/create-component.md +127 -0
  214. package/templates/hybrid/create-file.md +56 -0
  215. package/templates/hybrid/create-hook.md +145 -0
  216. package/templates/hybrid/create-service.md +70 -0
  217. package/templates/hybrid/fix-bug.md +33 -0
  218. package/templates/hybrid/modify-file.md +55 -0
  219. package/templates/story.md +68 -0
  220. package/templates/task.json +56 -0
  221. 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
+ });