{
  "updatedAt": "2026-03-19T05:34:06.197Z",
  "createdAt": "2026-03-17T03:35:25.543Z",
  "id": "PNOMGkCjzFGxf52E",
  "name": "eek-Go v2 (Batch-Then-Review)",
  "description": null,
  "active": true,
  "isArchived": false,
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "coding-agent",
        "responseMode": "lastNode",
        "options": {}
      },
      "id": "wh-trigger",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        250,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const query = $json.query || {};\nconst body = $json.body || $json;\nconst message = query.message || body.message || body.goal || '';\nconst projectId = query.project_id || body.project_id || ('proj-' + Date.now());\nconst reference_url = query.reference_url || body.reference_url || null;\nconst image_data = body.image_data || null;\nreturn [{ json: { message, project_id: projectId, operation: 'init', goal: message, reference_url, image_data } }];"
      },
      "id": "extract-input",
      "name": "Extract Input",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        450,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const operation = $json.operation || 'get';\nconst projectId = $json.project_id;\nif (!projectId) return [{ json: { error: 'project_id required' } }];\nconst store = $getWorkflowStaticData('global');\nif (operation === 'init') {\n  if (store[projectId]) return [{ json: { ...store[projectId], already_exists: true } }];\n  store[projectId] = {\n    project_id: projectId,\n    goal: $json.goal || '',\n    status: 'active',\n    completed_tasks: [],\n    pending_tasks: [],\n    files: [],\n    created_at: new Date().toISOString(),\n    last_updated: new Date().toISOString()\n  };\n  return [{ json: store[projectId] }];\n}\nif (operation === 'get') {\n  return [{ json: store[projectId] || { project_id: projectId, exists: false } }];\n}\nif (operation === 'set') {\n  const existing = store[projectId] || {};\n  store[projectId] = { ...existing, ...$json.state, project_id: projectId, last_updated: new Date().toISOString() };\n  return [{ json: store[projectId] }];\n}\nreturn [{ json: { error: `Unknown operation: ${operation}` } }];"
      },
      "id": "init-memory",
      "name": "Init Memory",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        650,
        300
      ]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ ($env.FILE_API_URL || 'http://file-api:3456') + '/projects/' + $json.project_id + '/files-content' }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + ($env.FILE_API_TOKEN || '') }}"
            }
          ]
        },
        "options": {}
      },
      "id": "fetch-project-files",
      "name": "Fetch Project Files",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        850,
        300
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// MODIFIED: Absorbs Research: Build Project Context\n// Caches _allFileContents, _researchDocs, _projectFileList in static data\n// so Phase 2 chunks don't need to re-fetch or re-compute.\nconst memory = $('Init Memory').first().json;\nconst message = $('Extract Input').first().json.message;\nconst allFiles = $json.files || [];\nconst staticData = $getWorkflowStaticData('global');\n\n// Cache all file contents for Phase 2\nstaticData._allFileContents = allFiles;\n\nconst fileList = allFiles.map(f => f.path);\n\n// Build API surface (exports from each file)\nconst apiSurface = [];\nfor (const f of allFiles) {\n  if (!f.path.match(/\\.(ts|tsx|js|jsx)$/) || f.path.includes('node_modules')) continue;\n  const lines = (f.content || '').split('\\n');\n  const exports = [];\n  for (const line of lines) {\n    const trimmed = line.trim();\n    if (/^export\\s+default\\s/.test(trimmed)) {\n      exports.push(trimmed.replace(/\\{[\\s\\S]*$/, '{...}').substring(0, 120));\n    } else if (/^export\\s+(const|let|var|function|class|interface|type|enum)\\s/.test(trimmed)) {\n      exports.push(trimmed.replace(/\\{[\\s\\S]*$/, '{...}').replace(/=>[\\s\\S]*$/, '=> ...').substring(0, 150));\n    } else if (/^export\\s+\\{/.test(trimmed)) {\n      exports.push(trimmed.substring(0, 150));\n    }\n  }\n  if (exports.length > 0) {\n    apiSurface.push(`## ${f.path}\\n${exports.join('\\n')}`);\n  }\n}\n\n// Extract dependencies from package.json\nlet dependencies = {};\nlet devDependencies = {};\nlet depNames = [];\nfor (const f of allFiles) {\n  if (f.path === 'package.json' || f.path.endsWith('/package.json')) {\n    try {\n      const pkg = JSON.parse(f.content);\n      dependencies = { ...dependencies, ...(pkg.dependencies || {}) };\n      devDependencies = { ...devDependencies, ...(pkg.devDependencies || {}) };\n      depNames = [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.devDependencies || {})];\n    } catch(e) {}\n  }\n}\n\n// Build research docs string\nconst depList = Object.entries(dependencies).map(([k,v]) => `  ${k}: ${v}`).join('\\n');\nconst devDepList = Object.entries(devDependencies).map(([k,v]) => `  ${k}: ${v}`).join('\\n');\nlet researchDocs = '';\nif (depList || devDepList) {\n  researchDocs += '## Dependency Manifest\\nOnly use packages listed here.\\n';\n  if (depList) researchDocs += `dependencies:\\n${depList}\\n`;\n  if (devDepList) researchDocs += `devDependencies:\\n${devDepList}\\n`;\n}\nif (apiSurface.length > 0) {\n  researchDocs += '\\n## Project API Surface\\nMatch these import/export signatures exactly when importing from project files.\\n';\n  researchDocs += apiSurface.join('\\n\\n');\n}\n\n// Cache for all downstream phases\nstaticData._researchDocs = researchDocs;\nstaticData._projectFileList = fileList;\nstaticData._projectApiSummary = apiSurface.join('\\n');\nstaticData._projectDeps = depNames;\n\nreturn [{ json: {\n  message,\n  project_goal: memory.goal || message,\n  project_id: memory.project_id,\n  existing_files: fileList,\n  api_summary: apiSurface.join('\\n'),\n  installed_packages: depNames\n} }];"
      },
      "id": "prepare-planner",
      "name": "Prepare Planner Input",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1050,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const message = $json.message || '';\nconst existingFiles = $json.existing_files || [];\nconst apiSummary = $json.api_summary || '';\nconst installedPkgs = $json.installed_packages || [];\nconst scrapeData = $json._scrapeData;\n\nlet projectContext = '';\nif (existingFiles.length > 0) {\n  projectContext += `\\n\\nEXISTING PROJECT FILES (only reference files from this list):\\n${existingFiles.join('\\n')}\\n`;\n}\nif (apiSummary) {\n  projectContext += `\\nAPI SURFACE (exports from each file):\\n${apiSummary}\\n`;\n}\nif (installedPkgs.length > 0) {\n  projectContext += `\\nINSTALLED PACKAGES (only reference packages from this list):\\n${installedPkgs.join(', ')}\\n`;\n}\n\nconst plannerPrompt = `You are a Senior Software Architect and UI/UX Design Expert. Your job is to break a coding request into MULTIPLE structured tasks with detailed visual specifications.\n\nDESIGN PRINCIPLES (apply to ALL task descriptions):\n- Main interaction element must be centered and dominant (40-60% of viewport height)\n- Use vertical single-column layouts for apps/games \u2014 never side-by-side grids unless explicitly requested\n- Content hierarchy: hero/main action at top \u2192 stats/feedback \u2192 secondary actions (shop, settings) at bottom\n- Mobile-first: everything fits in 100vh, no page-level scrollbars, internal scrolling only for lists\n- Dark backgrounds with bright accent colors. Use gradients for depth (purple-900 to black, not flat colors)\n- Every click/tap must produce visible feedback (scale animation, color flash, particle effect)\n- Numbers should animate when they change (bounce, pulse, scale). Use tabular-nums for counters\n- Cards: rounded-xl (12-16px), subtle shadows, semi-transparent backgrounds with backdrop-blur\n- Buttons: press animation (scale 0.95), hover glow, disabled state with reduced opacity\n- Color-code affordability: gold/amber = available, red/gray = locked, green = owned/success\n- Typography: big bold numbers (text-4xl+), clear hierarchy via weight not just size\n- Spacing: generous padding, never cramped. Mobile touch targets minimum 44px\n\nWHEN A REFERENCE IMAGE IS PROVIDED:\n- Describe the EXACT layout structure you see: column vs row, spacing ratios, component sizes\n- Specify exact colors, gradients, border styles, and shadows visible in the image\n- Note the visual hierarchy: what's biggest, what's brightest, what draws the eye first\n- Describe animations or interactive states implied by the design (hover effects, active states)\n- Include specific dimensions: \"toilet button should be 150px diameter\" not just \"large button\"\n- The coder CANNOT see the image \u2014 your description is their ONLY reference Each task will be handled by a separate coder with 128K context who can write up to 6 files at once.\\n\\nOutput a JSON object with ONE field:\\n\\n\"tasks\": array of task objects. IMPORTANT: You MUST create multiple tasks. Group files by concern (e.g. styles in one task, frontend components in another, config in another). Each task has:\\n- task_id: string like \"TASK-001\"\\n- description: detailed actionable description of what to implement, including specific requirements, color values, component names, API endpoints, and behavior. The coder cannot see the original request \u2014 your description is all they get.\\n- files: array of exact file paths to create or modify. List EVERY file this task needs \u2014 the coder can ONLY write files listed here. For existing projects, prefer modifying files from the EXISTING PROJECT FILES list. For new projects or missing functionality, CREATE new file paths as needed.\\n- dependencies: array of task_id strings this task depends on (empty if none)\\n- complexity: \"low\", \"medium\", or \"high\"\n- needs_concept: boolean \u2014 set to true if this task involves UI/visual work that has NO reference image from the user. Set to false if the user provided a reference image that covers this task's visual design, or if the task is purely logic/config with no visual component.\\n\\nRULES:\\n- Always split work across multiple tasks by concern: styles, components, routes, config, etc.\\n- Each task can touch up to 12-15 files. The coder has 128K context and currently uses less than 2% of it \u2014 give it MORE work per task. Aim for 2-3 LARGE tasks instead of many small ones. A single task can contain an entire feature (components + hooks + styles + config). Fewer tasks = faster pipeline\\n- The description must be SELF-CONTAINED \u2014 include ALL details the coder needs\\n- For existing projects, reference files from the EXISTING PROJECT FILES list when modifying. You MAY create new files that do not exist yet.\n- CRITICAL: For new projects (empty file list), you MUST include a setup task that creates: package.json (with ALL dependencies), index.html, vite.config.js (or next.config.js), tsconfig.json (if TypeScript), tailwind.config.js + postcss.config.js (if using Tailwind), and the main entry point (src/main.jsx or app/layout.tsx). The project MUST be buildable.\\n- Detect the project type from the files and packages. If the project is a React/Vite frontend (no Express or backend framework in INSTALLED PACKAGES), focus all tasks on frontend files. Only create server/middleware/API handler tasks if Express or a similar backend framework is in INSTALLED PACKAGES.\\n- In each task description, specify the EXACT import/export style each file should use (named vs default) based on the API SURFACE above.\\n- NEVER create a task whose only purpose is deleting files. The coder cannot delete files \u2014 it can only create or modify. If dead files exist, ignore them. They do not affect the build.\n- The FIRST task should always include project configuration files (package.json, config files, index.html) if they do not already exist. The project MUST compile and run after all tasks complete.\n- Output ONLY the raw JSON object: {\"tasks\": [...]}\\n- No markdown, no explanation \u2014 ONLY the JSON with tasks`;\n\nlet messageContent;\nif (scrapeData && scrapeData.screenshot_b64) {\n  const cssTokensStr = JSON.stringify(scrapeData.css_tokens, null, 2);\n  const domStr = scrapeData.dom_summary || '';\n  messageContent = [\n    { type: 'image_url', image_url: { url: `data:image/png;base64,${scrapeData.screenshot_b64}` } },\n    { type: 'text', text: `${plannerPrompt}\\n\\nREFERENCE DESIGN (screenshot above):\\nCSS Tokens:\\n${cssTokensStr}\\n\\nDOM Structure:\\n${domStr}\\n\\nRequest: ${message}${projectContext}\\n\\n(Match the visual design, color palette, typography, and layout from the screenshot)` }\n  ];\n} else if ($('Extract Input').first().json.image_data) {\n  const userImage = $('Extract Input').first().json.image_data;\n  messageContent = [\n    { type: 'image_url', image_url: { url: 'data:image/png;base64,' + userImage } },\n    { type: 'text', text: plannerPrompt + '\\n\\nREFERENCE IMAGE (uploaded by user \u2014 replicate this design):\\n\\nRequest: ' + message + projectContext }\n  ];\n} else {\n  messageContent = `${plannerPrompt}\\n\\nRequest: ${message}${projectContext}`;\n}\n\nreturn [{\n  json: {\n    model: $env.PLANNER_MODEL || 'qwen3.5-27b@q4_k_m',\n    messages: [{ role: 'user', content: messageContent }],\n    temperature: 1.0,\n    top_p: 0.95,\n    top_k: 20,\n    min_p: 0.0,\n    presence_penalty: 0.0,\n    max_tokens: 8192,\n    chat_template_kwargs: { enable_thinking: true, max_thinking_tokens: 4096 }\n  }\n}];"
      },
      "id": "planner-build",
      "name": "Planner: Build Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1250,
        300
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.LM_STUDIO_URL || 'http://10.0.0.100:1234/v1/chat/completions' }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json) }}",
        "options": {
          "timeout": 600000
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + $env.LLM_API_KEY }}"
            }
          ]
        }
      },
      "id": "planner-llm",
      "name": "Planner: Call LM Studio",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1450,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const raw = $json.choices[0].message.content || $json.choices[0].message.reasoning_content || '';\nlet content = raw.replace(/<think>[\\s\\S]*?<\\/think>/g, '').trim();\nif (content.includes('</think>')) content = content.split('</think>').pop().trim();\ncontent = content.replace(/<\\/?task>/g, '').trim();\ncontent = content.replace(/\\n+(?:Reasoning|Note|Explanation):[\\s\\S]*/i, '').trim();\ncontent = content.replace(/^```(?:json)?\\n?/, '').replace(/\\n?```$/, '').trim();\n\nlet tasks, planDocument = '';\ntry {\n  const parsed = JSON.parse(content);\n  if (Array.isArray(parsed)) {\n    tasks = parsed;\n  } else {\n    tasks = parsed.tasks || [parsed];\n    planDocument = parsed.plan_document || '';\n  }\n} catch (e) {\n  // Truncated JSON recovery: extract complete task objects\n  const tasksMatch = content.match(/\"tasks\"\\s*:\\s*\\[/);\n  if (tasksMatch) {\n    const arrStart = content.indexOf('[', tasksMatch.index);\n    let bracketDepth = 0;\n    let lastCompleteObj = -1;\n    for (let i = arrStart; i < content.length; i++) {\n      if (content[i] === '{') bracketDepth++;\n      if (content[i] === '}') {\n        bracketDepth--;\n        if (bracketDepth === 0) lastCompleteObj = i;\n      }\n    }\n    if (lastCompleteObj > arrStart) {\n      const recoveredArr = content.substring(arrStart, lastCompleteObj + 1) + ']';\n      try {\n        tasks = JSON.parse(recoveredArr);\n      } catch (e2) {\n        tasks = [{ task_id: 'TASK-FALLBACK-' + Date.now(), description: content, files: [], dependencies: [], complexity: 'high' }];\n      }\n    } else {\n      tasks = [{ task_id: 'TASK-FALLBACK-' + Date.now(), description: content, files: [], dependencies: [], complexity: 'high' }];\n    }\n  } else {\n    tasks = [{ task_id: 'TASK-FALLBACK-' + Date.now(), description: content, files: [], dependencies: [], complexity: 'high' }];\n  }\n}\n\ntasks = tasks.map(t => ({\n  ...t,\n  files: t.files || [],\n  dependencies: t.dependencies || [],\n  complexity: t.complexity || 'medium'\n}));\n\nreturn [{ json: { tasks, plan_document: planDocument } }];"
      },
      "id": "planner-parse",
      "name": "Planner: Parse Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1650,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// MODIFIED: Absorbs Split Into Chunks. Builds flat (task, chunk) queue.\n// Queue is passed through data pipeline (not static data) to avoid stale reads after HTTP nodes.\nconst tasks = $json.tasks || [];\nconst planDocument = $json.plan_document || '';\nconst plannerInput = $('Prepare Planner Input').first().json;\nconst projectId = plannerInput.project_id;\nconst projectGoal = plannerInput.project_goal;\n\n// Topological sort\nconst taskMap = new Map();\ntasks.forEach(t => taskMap.set(t.task_id, t));\nconst inDegree = new Map();\nconst dependents = new Map();\ntasks.forEach(t => {\n  inDegree.set(t.task_id, 0);\n  dependents.set(t.task_id, []);\n});\ntasks.forEach(t => {\n  const deps = (t.dependencies || []).filter(d => taskMap.has(d));\n  inDegree.set(t.task_id, deps.length);\n  deps.forEach(d => dependents.get(d).push(t.task_id));\n});\nconst topoQueue = [];\ntasks.forEach(t => {\n  if (inDegree.get(t.task_id) === 0) topoQueue.push(t.task_id);\n});\nconst sorted = [];\nwhile (topoQueue.length > 0) {\n  const id = topoQueue.shift();\n  sorted.push(taskMap.get(id));\n  for (const dep of (dependents.get(id) || [])) {\n    inDegree.set(dep, inDegree.get(dep) - 1);\n    if (inDegree.get(dep) === 0) topoQueue.push(dep);\n  }\n}\nif (sorted.length < tasks.length) {\n  const sortedIds = new Set(sorted.map(t => t.task_id));\n  tasks.forEach(t => { if (!sortedIds.has(t.task_id)) sorted.push(t); });\n}\n\n// Flatten into (task, chunk) queue \u2014 2 files per chunk\nconst chunkSize = 15;\nconst queue = [];\nfor (const task of sorted) {\n  const taskFiles = task.files || [];\n  const chunks = [];\n  for (let i = 0; i < taskFiles.length; i += chunkSize) {\n    chunks.push(taskFiles.slice(i, i + chunkSize));\n  }\n  if (taskFiles.length === 0) continue;  // skip tasks with no files assigned\n  for (const chunkFiles of chunks) {\n    queue.push({\n      task,\n      chunk_files: chunkFiles,\n      project_id: projectId,\n      project_goal: projectGoal,\n      plan_document: planDocument\n    });\n  }\n}\n\nconst staticData = $getWorkflowStaticData('global');\nstaticData.p2Results = [];\nstaticData.allTasks = sorted;\nstaticData._queueTotal = queue.length;\nstaticData._queueDone = 0;\n\nif (queue.length === 0) {\n  return [{ json: { task: { task_id: 'NONE', description: 'No tasks', files: [] }, chunk_files: [], project_id: projectId, project_goal: projectGoal, plan_document: planDocument, _p2Queue: [] } }];\n}\n// Pass remaining queue items through data pipeline instead of static data\nconst first = queue[0];\nfirst._p2Queue = queue.slice(1);\nreturn [{ json: first }];"
      },
      "id": "spread-tasks",
      "name": "Spread Tasks",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1850,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// P2: Stash Context \u2014 passes through all fields including _p2Queue for loop control\nconst staticData = $getWorkflowStaticData('global');\nstaticData._currentProjectId = $json.project_id;\nstaticData._currentTaskId = ($json.task || {}).task_id || '';\nstaticData._currentTask = $json.task || null;\nstaticData._currentProjectGoal = $json.project_goal || '';\nstaticData._currentQueue = $json._p2Queue || [];  // snapshot queue for this iteration\nreturn [{ json: {\n  task: $json.task,\n  chunk_files: $json.chunk_files,\n  project_id: $json.project_id,\n  project_goal: $json.project_goal,\n  plan_document: $json.plan_document,\n  _p2Queue: $json._p2Queue || []\n} }];"
      },
      "id": "p2-stash",
      "name": "P2: Stash Context",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2050,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// Builds coder input \u2014 task files in full, other files as compact summaries\nconst staticData = $getWorkflowStaticData('global');\nconst stashed = $json;\nconst taskFiles = (stashed.task || {}).files || stashed.chunk_files || [];\nconst allFiles = staticData._allFileContents || [];\n\n// Full content for files in THIS task + closely related files (imports)\nconst taskFileSet = new Set(taskFiles.map(f => typeof f === 'string' ? f : f.path));\n\n// Also include files that task files import from\nfor (const file of allFiles) {\n  if (taskFileSet.has(file.path)) {\n    // Find imports in this file\n    const importMatches = (file.content || '').matchAll(/from\\s+['\"]([^'\"]+)['\"]/g);\n    for (const m of importMatches) {\n      const importPath = m[1];\n      if (importPath.startsWith('.')) {\n        // Resolve relative import to find the actual file\n        const dir = file.path.replace(/\\/[^\\/]+$/, '');\n        const resolved = importPath.replace(/^\\.\\//,'').replace(/^\\.\\.\\//, '');\n        for (const f of allFiles) {\n          if (f.path.includes(resolved) || f.path.replace(/\\.(ts|tsx|js|jsx)$/, '').endsWith(resolved)) {\n            taskFileSet.add(f.path);\n          }\n        }\n      }\n    }\n  }\n}\n\n// Always include package.json and main config files (small, critical for context)\nconst alwaysInclude = ['package.json', 'tsconfig.json', 'vite.config.js', 'tailwind.config.js', 'postcss.config.js', 'index.html'];\nfor (const f of alwaysInclude) {\n  const match = allFiles.find(a => a.path === f || a.path.endsWith('/' + f));\n  if (match) taskFileSet.add(match.path);\n}\n\n// Split: full content for task files, compact for everything else\nconst fullFiles = [];\nconst summaryFiles = [];\nfor (const file of allFiles) {\n  if (file.path.includes('node_modules')) continue;\n  if (taskFileSet.has(file.path)) {\n    fullFiles.push(file);\n  } else if (file.path.match(/\\.(ts|tsx|js|jsx|css|json|html)$/)) {\n    // Compact summary: path + first line (exports) + size\n    const firstLines = (file.content || '').split('\\n').slice(0, 3).join('\\n');\n    const exports = (file.content || '').match(/export\\s+(default\\s+)?(?:function|const|class)\\s+(\\w+)/g) || [];\n    summaryFiles.push({\n      path: file.path,\n      summary: exports.join(', ') || firstLines.substring(0, 100),\n      size: (file.content || '').length\n    });\n  }\n}\n\nconst existingFiles = fullFiles;\n\nconst task = { ...(stashed.task || {}) };\nif (taskFiles.length > 0) task.files = taskFiles;\n\n// Extract design tokens from reference HTML files (e.g., Stitch concepts)\nconst http = require('http');\nconst projectId = stashed.project_id;\nlet designSystem = staticData._stitchDesignSystem || '';\nlet referenceStyles = staticData._stitchStyles || '';\n\n// Only fetch if not already cached this run AND task involves UI/visual work\nconst taskDesc = ((stashed.task || {}).description || '').toLowerCase();\nconst isVisualTask = taskDesc.includes('design') || taskDesc.includes('style') || taskDesc.includes('layout') || taskDesc.includes('ui') || taskDesc.includes('color') || taskDesc.includes('component') || taskDesc.includes('css') || taskDesc.includes('tailwind') || taskDesc.includes('visual') || taskDesc.includes('mockup') || taskDesc.includes('reference');\n\nif (!designSystem && isVisualTask && projectId) {\n  try {\n    // Fetch reference files list\n    const refFiles = await new Promise((resolve) => {\n      const req = http.request({\n        hostname: 'file-api', port: 3456, method: 'GET',\n        path: '/projects/' + projectId + '/files',\n        headers: { 'Authorization': 'Bearer ' + ($env.FILE_API_TOKEN || '') },\n        timeout: 10000\n      }, res => {\n        let d = ''; res.on('data', c => d += c);\n        res.on('end', () => { try { resolve(JSON.parse(d).files || []); } catch { resolve([]); } });\n      });\n      req.on('error', () => resolve([]));\n      req.end();\n    });\n\n    // Find HTML files in references/\n    const htmlRefs = refFiles.filter(f => f.path.startsWith('references/') && f.path.endsWith('.html'));\n    \n    for (const ref of htmlRefs.slice(0, 2)) {\n      const content = ref.content || '';\n      if (content.length < 100) continue;\n      \n      // Extract Tailwind config\n      const configMatch = content.match(/tailwind\\.config\\s*=\\s*(\\{[\\s\\S]*?\\})\\s*<\\/script>/);\n      if (configMatch && !designSystem) {\n        designSystem = configMatch[1].substring(0, 4000);\n        staticData._stitchDesignSystem = designSystem;\n          \n          // Extract common component patterns from HTML and generate @apply classes\n          const classPatterns = [];\n          \n          // Find repeated class combinations in the HTML (buttons, cards, badges, etc.)\n          const classMatches = content.match(/class=\"([^\"]{30,})\"/g) || [];\n          const classCounts = {};\n          for (const match of classMatches) {\n            const classes = match.replace('class=\"', '').replace('\"', '').trim();\n            // Normalize whitespace\n            const normalized = classes.replace(/\\s+/g, ' ').trim();\n            classCounts[normalized] = (classCounts[normalized] || 0) + 1;\n          }\n          \n          // Find patterns used 2+ times \u2014 these are component-worthy\n          const componentClasses = [];\n          for (const [classes, count] of Object.entries(classCounts)) {\n            if (count >= 2 && classes.length > 20 && classes.length < 200) {\n              // Generate a semantic name from the classes\n              let name = 'component';\n              if (classes.includes('rounded-full') && classes.includes('px-')) name = 'pill';\n              else if (classes.includes('rounded-') && classes.includes('border')) name = 'card';\n              else if (classes.includes('font-bold') && classes.includes('text-')) name = 'heading';\n              else if (classes.includes('flex') && classes.includes('items-center')) name = 'row';\n              else if (classes.includes('grid')) name = 'grid';\n              else if (classes.includes('btn') || (classes.includes('cursor-pointer') && classes.includes('px-'))) name = 'btn';\n              else if (classes.includes('backdrop-blur')) name = 'glass';\n              \n              componentClasses.push({ name: name + '-' + componentClasses.length, classes, count });\n            }\n          }\n          \n          if (componentClasses.length > 0) {\n            // Generate @apply CSS\n            let applyCSS = '@layer components {\\n';\n            for (const comp of componentClasses.slice(0, 15)) {\n              applyCSS += '  .' + comp.name + ' {\\n    @apply ' + comp.classes.substring(0, 150) + ';\\n  }\\n';\n            }\n            applyCSS += '}';\n            staticData._stitchComponentClasses = applyCSS;\n          }\n      }\n      \n      // Extract style blocks\n      const styleMatches = content.match(/<style>([\\s\\S]*?)<\\/style>/g);\n      if (styleMatches && !referenceStyles) {\n        referenceStyles = styleMatches.join('\\n').substring(0, 2000);\n        staticData._stitchStyles = referenceStyles;\n      }\n    }\n  } catch(e) {}\n}\n\nreturn [{ json: {\n  task,\n  existing_files: existingFiles,\n  other_files_summary: summaryFiles,\n  plan_document: stashed.plan_document || '',\n  project_goal: stashed.project_goal || '',\n  project_id: stashed.project_id,\n  research_docs: staticData._researchDocs || '',\n  design_system: designSystem,\n  reference_styles: referenceStyles\n} }];"
      },
      "id": "p2-build-input",
      "name": "P2: Build Code Input",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2250,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// CW: Prepare Message \u2014 restructured for instruction priority\n// Order: Task FIRST \u2192 Assets \u2192 Design System \u2192 Images \u2192 Files \u2192 Research (last)\nconst input = $json;\nconst planDocument = input.plan_document || '';\n\nlet taskDescription = input.task?.description || input.description || '';\nif (!taskDescription && input.task?.raw_output) {\n  const raw = input.task.raw_output;\n  const cleaned = raw.replace(/<\\/?task>/g, '').replace(/\\n+(?:Reasoning|Note):[\\s\\S]*/i, '').trim();\n  try {\n    const parsed = JSON.parse(cleaned);\n    const list = Array.isArray(parsed) ? parsed : (parsed.tasks || []);\n    taskDescription = list.map(t => `${t.task_id}: ${t.description}`).join('\\n\\n');\n  } catch(e) { taskDescription = raw; }\n}\n\nconst taskFiles = (input.task || {}).files || [];\nconst filesConstraint = taskFiles.length > 0\n  ? '\\n\\nFILES TO MODIFY (only output these files):\\n' + taskFiles.join('\\n')\n  : '';\n\nconst existingFiles = input.existing_files || [];\nconst otherFilesSummary = input.other_files_summary || [];\nconst existingFilesSection = existingFiles.length > 0\n  ? '\\n\\nEXISTING PROJECT FILES:\\n' +\n    (() => { let total = 0; const MAX = 40000; return existingFiles.filter(f => { const size = f.path.length + (f.content||'').length + 20; if (total + size > MAX) return false; total += size; return true; }).map(f => `### ${f.path}\\n\\`\\`\\`\\n${f.content}\\n\\`\\`\\``).join('\\n\\n'); })() + (otherFilesSummary.length > 0 ? '\\n\\nOTHER PROJECT FILES (not shown in full):\\n' + otherFilesSummary.map(f => `- ${f.path} (${f.size} chars): ${f.summary}`).join('\\n') : '')\n  : '';\n\nconst researchDocs = input.research_docs || '';\n\n// Detect image assets in public/assets/\nconst assetFiles = existingFiles\n  .filter(f => f.path.match(/^public\\/assets\\/.*\\.(png|jpg|jpeg|svg|gif|webp)$/i))\n  .map(f => f.path);\nconst assetWarning = assetFiles.length > 0\n  ? '\\n\\nAVAILABLE ASSETS (use <img src> for these, do NOT recreate them):\\n' +\n    assetFiles.map(a => `- ${a} \u2192 <img src=\"/${a.replace('public/', '')}\" />`).join('\\n')\n  : '';\n\n// Design system tokens from reference HTML\nconst designSystem = input.design_system || $getWorkflowStaticData('global')._stitchDesignSystem || '';\nconst stitchStyles = input.reference_styles || $getWorkflowStaticData('global')._stitchStyles || '';\n\n// Load reference images\nconst http = require('http');\nconst projectId = input.project_id || $('Extract Input').first().json.project_id || '';\nconst FILE_API = ($env.FILE_API_URL || 'http://file-api:3456').replace('http://', '');\nconst [apiHost, apiPort] = FILE_API.split(':');\nconst refImages = await new Promise((resolve) => {\n  const req = http.request({\n    hostname: apiHost, port: parseInt(apiPort) || 3456,\n    path: `/projects/${projectId}/references`,\n    method: 'GET',\n    headers: { 'Authorization': 'Bearer ' + ($env.FILE_API_TOKEN || '') },\n    timeout: 10000\n  }, (res) => {\n    let data = '';\n    res.on('data', d => data += d);\n    res.on('end', () => { try { resolve(JSON.parse(data).references || []); } catch { resolve([]); } });\n  });\n  req.on('error', () => resolve([]));\n  req.on('timeout', () => { req.destroy(); resolve([]); });\n  req.end();\n});\n\nconst webhookImage = $('Extract Input').first().json.image_data || null;\nconst taskId = (input.task || {}).task_id || '';\nconst taskConcepts = $getWorkflowStaticData('global')._taskConcepts || {};\nconst taskConcept = taskConcepts[taskId] || null;\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// BUILD THE PROMPT \u2014 instruction-first order\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n// SYSTEM: Short, focused rules only\nconst systemMessage = `You are a Senior Full-Stack Developer. Output ONLY code file blocks.\n\nFORMAT \u2014 for each file output EXACTLY:\n### path/to/file.ext\n\\`\\`\\`ext\n[complete file content]\n\\`\\`\\`\n\nRULES:\n- Output complete file content, not diffs\n- Only output files listed in FILES TO MODIFY\n- No explanations, no prose \u2014 ONLY file blocks\n- Files with JSX MUST use .tsx extension\n- NEVER remove @tailwind directives from CSS files\n- NEVER remove existing imports that are still used\n- When modifying a file, preserve everything that works \u2014 only change what the task asks for`;\n\n// USER: Task first, then supporting context\nconst content = [];\n\n// \u2500\u2500 1. TASK DESCRIPTION (highest priority \u2014 read this first) \u2500\u2500\ncontent.push({ type: 'text', text: `YOUR TASK:\\n${taskDescription}${assetWarning}${filesConstraint}` });\n\n// \u2500\u2500 2. DESIGN SYSTEM (if available \u2014 use these tokens) \u2500\u2500\nif (designSystem) {\n  content.push({ type: 'text', text: 'DESIGN SYSTEM (follow these design principles, color palette, font choices, and spacing patterns):\\n' + designSystem.substring(0, 3000) });\n}\nif (stitchStyles) {\n  content.push({ type: 'text', text: 'REFERENCE CSS PATTERNS:\\n' + stitchStyles.substring(0, 1500) });\n}\nconst componentClasses = $getWorkflowStaticData('global')._stitchComponentClasses || '';\nif (componentClasses) {\n  content.push({ type: 'text', text: 'COMPONENT CLASSES (add these to your CSS file and use the semantic class names instead of repeating utility classes):\\n' + componentClasses });\n}\n\n// \u2500\u2500 3. REFERENCE IMAGES (visual target) \u2500\u2500\nif (refImages.length > 0) {\n  for (const ref of refImages) {\n    content.push({ type: 'image_url', image_url: { url: 'data:image/png;base64,' + ref.base64 } });\n    const label = ref.filename.replace(/\\.(png|jpg|jpeg|gif|webp)$/i, '').replace(/[_-]/g, ' ');\n    content.push({ type: 'text', text: 'REFERENCE (' + label + '):' });\n  }\n} else if (webhookImage) {\n  content.push({ type: 'image_url', image_url: { url: 'data:image/png;base64,' + webhookImage } });\n  content.push({ type: 'text', text: 'REFERENCE DESIGN:' });\n}\nif (taskConcept) {\n  content.push({ type: 'image_url', image_url: { url: 'data:image/png;base64,' + taskConcept } });\n  content.push({ type: 'text', text: 'TASK CONCEPT (visual guide for this task):' });\n}\n\n// \u2500\u2500 4. EXISTING FILES (context for the coder) \u2500\u2500\nif (existingFilesSection) {\n  content.push({ type: 'text', text: existingFilesSection });\n}\n\n// \u2500\u2500 5. ARCHITECTURE + RESEARCH (lowest priority \u2014 background context) \u2500\u2500\nif (planDocument) {\n  content.push({ type: 'text', text: 'ARCHITECTURE:\\n' + planDocument.substring(0, 3000) });\n}\nif (researchDocs) {\n  content.push({ type: 'text', text: 'LIBRARY DOCS:\\n' + researchDocs.substring(0, 6000) });\n}\n\nconst messageContent = content.length > 1 ? content : content[0].text;\n\nreturn [{ json: {\n  model: $env.CODER_MODEL || 'qwen3.5-27b@q4_k_m',\n  messages: [\n    { role: 'system', content: systemMessage },\n    { role: 'user', content: messageContent }\n  ],\n  temperature: 0.6,\n  chat_template_kwargs: { enable_thinking: false },\n  top_p: 0.95,\n    top_k: 20,\n    min_p: 0.0,\n    presence_penalty: 0.0,\n  max_tokens: 16384\n} }];"
      },
      "id": "cw-prepare",
      "name": "CW: Prepare Message",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2450,
        300
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.LM_STUDIO_URL || 'http://10.0.0.100:1234/v1/chat/completions' }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json) }}",
        "options": {
          "timeout": 600000
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + $env.LLM_API_KEY }}"
            }
          ]
        }
      },
      "id": "cw-llm",
      "name": "CW: Call LM Studio",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2650,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const msg = $json.choices[0].message || {};\nlet raw = msg.content || '';\nconst reasoning = msg.reasoning_content || '';\n\n// Qwen3.5-27B reasoning model may put ALL output in reasoning_content or <think> tags\n// Try content first, then reasoning_content\nlet content = raw.replace(/<think>[\\s\\S]*?<\\/think>/g, '').trim();\nif (content.includes('</think>')) {\n  content = content.split('</think>').pop().trim();\n}\n\n// If content is empty or too short (binary garbage from images), use reasoning_content\nif (content.length < 50 && reasoning.length > 50) {\n  content = reasoning.replace(/<think>[\\s\\S]*?<\\/think>/g, '').trim();\n  if (content.includes('</think>')) {\n    content = content.split('</think>').pop().trim();\n  }\n}\n\n// If still empty, try the full raw content (maybe think tags are unclosed)\nif (content.length < 50 && raw.length > 50) {\n  // Strip just the opening <think> and try to find file blocks\n  content = raw.replace(/<think>/g, '').replace(/<\\/think>/g, '').trim();\n}\n\nconst files = [];\nconst seen = new Set();\nconst re = /###\\s+((?:[\\w.-]+\\/)*(?:\\.[\\w][\\w.-]*|[\\w.-]+\\.(?:ts|tsx|js|jsx|json|md|yml|yaml|env|prisma|css|html|sh|txt|lock|toml|cfg|ini)|Dockerfile|Makefile|LICENSE|CHANGELOG))\\s*\\n```[\\w]*\\n([\\s\\S]*?)```/g;\nlet m;\nwhile ((m = re.exec(content)) !== null) {\n  const path = m[1].trim();\n  const fileContent = m[2];\n  if (path && !seen.has(path)) {\n    seen.add(path);\n    files.push({ path, content: fileContent });\n  }\n}\nconst staticData = $getWorkflowStaticData('global');\nconst projectId = staticData._currentProjectId || $('Extract Input').first().json.project_id || 'unknown';\nconst task = staticData._currentTask || {};\nconst projectGoal = staticData._currentProjectGoal || $('Extract Input').first().json.message || '';\nreturn [{ json: { files, task, project_id: projectId, project_goal: projectGoal } }];"
      },
      "id": "cw-parse",
      "name": "CW: Parse Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2850,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// NEW: Replaces Chunk: Prepare Write\n// Simplified write prep + updates _allFileContents cache so later chunks see fresh content\nconst stashed = $('P2: Stash Context').first().json;\nconst chunkFiles = stashed.chunk_files || [];\nconst newFiles = $json.files || [];\nconst staticData2 = $getWorkflowStaticData('global');\nconst projectId = $json.project_id || staticData2._currentProjectId || stashed.project_id;\n\n// Include all files the Code Writer produced\nlet filteredFiles = newFiles;\n\n// Handle package.json merge: preserve existing deps, add new ones\nconst staticData = $getWorkflowStaticData('global');\nconst allFiles = staticData._allFileContents || [];\nfor (const newFile of filteredFiles) {\n  if (newFile.path === 'package.json' || newFile.path.endsWith('/package.json')) {\n    const existing = allFiles.find(f => f.path === newFile.path);\n    if (existing) {\n      try {\n        const existingPkg = JSON.parse(existing.content);\n        const newPkg = JSON.parse(newFile.content);\n        newPkg.dependencies = { ...(existingPkg.dependencies || {}), ...(newPkg.dependencies || {}) };\n        newPkg.devDependencies = { ...(existingPkg.devDependencies || {}), ...(newPkg.devDependencies || {}) };\n        newFile.content = JSON.stringify(newPkg, null, 2);\n      } catch(e) {}\n    }\n  }\n}\n\n// Update the in-memory file cache so later chunks see fresh content\nfor (const newFile of filteredFiles) {\n  const idx = allFiles.findIndex(f => f.path === newFile.path);\n  if (idx >= 0) {\n    allFiles[idx] = { path: newFile.path, content: newFile.content };\n  } else {\n    allFiles.push({ path: newFile.path, content: newFile.content });\n  }\n}\nstaticData._allFileContents = allFiles;\n\nif (filteredFiles.length === 0) {\n  return [{ json: { project_id: projectId, files: [], _skipWrite: true } }];\n}\n\nreturn [{ json: { project_id: projectId, files: filteredFiles } }];"
      },
      "id": "p2-prepare-write",
      "name": "P2: Prepare Write",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3050,
        300
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ ($env.FILE_API_URL || 'http://file-api:3456') + '/projects/' + $json.project_id + '/files-batch' }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + ($env.FILE_API_TOKEN || '') }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ files: $json.files }) }}",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      },
      "id": "p2-write-files",
      "name": "P2: Write Files",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3250,
        300
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// P2: Store Result \u2014 uses $json like all other Code nodes\nconst staticData = $getWorkflowStaticData('global');\nstaticData._queueDone = (staticData._queueDone || 0) + 1;\nconst total = staticData._queueTotal || 0;\nconst done = staticData._queueDone;\nconst remaining = total - done;\n\nconst taskId = staticData._currentTaskId || 'unknown';\nconst taskFiles = (staticData._currentTask || {}).files || [];\nconst writtenFiles = ($json.files_written || $json.files || []).map(f => typeof f === 'string' ? f : (f.path || JSON.stringify(f)));\nif (!staticData.p2Results) staticData.p2Results = [];\nstaticData.p2Results.push({ task_id: taskId, files_written: writtenFiles });\n\n\n// Status callback to Forge \u2014 fire and forget\ntry {\n  const http = require('http');\n  const cbBody = JSON.stringify({\n    event: 'task_written',\n    project_id: staticData._currentProjectId || 'unknown',\n    data: { task_id: taskId, files: writtenFiles.length > 0 ? writtenFiles : taskFiles }\n  });\n  const cbReq = http.request({\n    hostname: 'forge', port: 3500, path: '/api/status-callback',\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(cbBody) }\n  });\n  cbReq.on('error', () => {});\n  cbReq.write(cbBody);\n  cbReq.end();\n} catch(e) {}\n\nconst remainingQueue = staticData._currentQueue || [];\nconst isDone = remaining <= 0 || remainingQueue.length === 0;\n\nlet nextItem = null;\nif (!isDone) {\n  nextItem = JSON.parse(JSON.stringify(remainingQueue[0]));\n  nextItem._p2Queue = remainingQueue.slice(1);\n}\n\nreturn [{ json: {\n  _done: isDone,\n  _nextItem: nextItem,\n  _remaining: remaining,\n  task_id: taskId,\n  files_written: writtenFiles\n} }];"
      },
      "id": "p2-store",
      "name": "P2: Store Result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3450,
        300
      ]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ ($env.FILE_API_URL || 'http://file-api:3456') + '/projects/' + $('Prepare Planner Input').first().json.project_id + '/files-content' }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + ($env.FILE_API_TOKEN || '') }}"
            }
          ]
        },
        "options": {}
      },
      "id": "p3-refetch",
      "name": "P3: Re-fetch All Files",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3850,
        500
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// Full project review with VL model \u2014 sees code AND screenshot\nconst allFiles = $json.files || [];\n// Store files for the code review pass\nconst __staticData = $getWorkflowStaticData('global');\n__staticData._codeReviewFiles = allFiles;\n__staticData._codeReviewBuildResult = $getWorkflowStaticData('global')._buildResult || {};\nconst staticData = $getWorkflowStaticData('global');\nconst screenshots = staticData._screenshots || {};\nconst interactionResult = staticData._interactionTest || {};\nconst playtestScreenshots = staticData._playtestScreenshots || [];\nconst playtestReport = staticData._playtestReport || '';\nconst plannerInput = $('Prepare Planner Input').first().json;\nconst projectGoal = plannerInput.project_goal || '';\nconst deps = staticData._projectDeps || [];\nconst buildResult = staticData._buildResult || { success: true };\nconst screenshot_b64 = staticData._screenshot_b64 || null;\n// Load reference images from project's references/ folder (persisted across runs)\nconst http = require('http');\nconst projectId = $('Extract Input').first().json.project_id;\nconst FILE_API = ($env.FILE_API_URL || 'http://file-api:3456').replace('http://', '');\nconst [apiHost, apiPort] = FILE_API.split(':');\nconst refImages = await new Promise((resolve) => {\n  const req = http.request({\n    hostname: apiHost, port: parseInt(apiPort) || 3456,\n    path: `/projects/${projectId}/references`,\n    method: 'GET',\n    headers: { 'Authorization': 'Bearer ' + ($env.FILE_API_TOKEN || '') },\n    timeout: 15000\n  }, (res) => {\n    let data = '';\n    res.on('data', d => data += d);\n    res.on('end', () => {\n      try { resolve(JSON.parse(data).references || []); } catch { resolve([]); }\n    });\n  });\n  req.on('error', () => resolve([]));\n  req.on('timeout', () => { req.destroy(); resolve([]); });\n  req.end();\n});\n// Fallback to webhook image_data if no saved references\nconst webhookImage = $('Extract Input').first().json.image_data || null;\n\nconst allFilesFormatted = allFiles\n  .filter(f => f.path.match(/\\.(ts|tsx|js|jsx|json|css|html)$/) && !f.path.includes('node_modules'))\n  .map(f => '### ' + f.path + '\\n```\\n' + f.content + '\\n```')\n  .join('\\n\\n');\n\n// Browser inspection data from Playwright MCP\nconst consoleErrors = staticData._consoleErrors || [];\nconst layoutInfo = staticData._layoutInfo || null;\nconst networkErrors = staticData._networkErrors || [];\nconst domSnapshot = staticData._domSnapshot || '';\n\nlet browserSection = '';\nif (interactionResult && !interactionResult.error) {\n  browserSection += '\\nINTERACTION TEST: Clicked ' + (interactionResult.clicked || 'main button') + \n    ' \u2014 state ' + (interactionResult.stateChanged ? 'CHANGED (interactive element works)' : 'DID NOT CHANGE (possible bug: click handler may be broken)') + '\\n';\n}\nif (Object.keys(screenshots).length > 1) {\n  browserSection += '\\nMULTI-VIEWPORT TEST: Screenshots taken at mobile (390px), tablet (768px), desktop (1440px).\\n';\n  browserSection += 'Check: Does the layout adapt? Any overflow? Elements hidden on smaller screens?\\n';\n}\nif (consoleErrors.length > 0) {\n  browserSection += '\\nCONSOLE ERRORS (from browser \u2014 these are REAL runtime errors):\\n' + consoleErrors.join('\\n') + '\\n';\n}\nif (networkErrors.length > 0) {\n  browserSection += '\\nNETWORK ERRORS (failed requests):\\n' + networkErrors.join('\\n') + '\\n';\n}\nif (layoutInfo) {\n  browserSection += '\\nLAYOUT INSPECTION (computed CSS from running app):\\n' + JSON.stringify(layoutInfo, null, 2) + '\\n';\n  browserSection += 'Check: Are elements overlapping? Is the layout centered? Are widths/heights reasonable for mobile (390x844)?\\n';\n}\nif (domSnapshot) {\n  browserSection += '\\nDOM SNAPSHOT (accessibility tree from browser):\\n' + domSnapshot + '\\n';\n}\n\nconst buildSection = buildResult.success\n  ? '\\nBUILD STATUS: \u2705 Project builds successfully.\\n'\n  : '\\nBUILD STATUS: \u274c BUILD FAILED \u2014 THIS IS THE HIGHEST PRIORITY FIX:\\n' +\n    'Stage: ' + (buildResult.stage || 'unknown') + '\\n' +\n    'Error: ' + (buildResult.error || 'unknown') + '\\n' +\n    'Output:\\n' + (buildResult.output || '') + '\\n' +\n    'You MUST flag this as a critical severity fix.\\n';\n\nconst textPrompt = 'You are a Senior Code Reviewer with UI/UX expertise performing a FULL PROJECT REVIEW.\\n' +\n  'You are reviewing ALL files AND the visual output of the project.\\n\\n' +\n  'Your job is to find issues across code AND visuals:\\n' +\n  '1. IMPORT/EXPORT MISMATCHES: imports that reference exports that don\\'t exist, wrong import style (named vs default)\\n' +\n  '2. TYPE MISMATCHES: function signatures that don\\'t match their call sites\\n' +\n  '3. DEPENDENCY ISSUES: imports of packages not in package.json\\n' +\n  '4. MISSING FILES: components or modules imported but never created\\n' +\n  '5. CRITICAL BUGS: null reference risks, unhandled promise rejections\\n' +\n  '6. ASSET/STYLE LOADING: verify entry point imports stylesheet, Tailwind classes are compiled\\n' +\n  '7. BUILD CHAIN: configs use consistent module format, all deps listed\\n' +\n  '8. DEAD FILES: duplicate entry points, orphaned files\\n' +\n  '9. FILE SCOPE: ONLY flag issues in files that exist in the ALL PROJECT FILES section above. Do NOT suggest creating new files. If functionality is missing, suggest adding it to an EXISTING file.\\n10. UNUSED ASSETS: If public/assets/ contains PNG/SVG images but the code uses inline SVGs or emoji instead, flag this as a BLOCKER. The project has real game assets \u2014 they MUST be used via <img src> tags.\\n10. VISUAL ISSUES: compare the screenshot with the design intent \u2014 is the layout correct? Are components visible? Colors applied? Anything broken visually?\\n\\n' +\n  'Project Goal: ' + projectGoal + '\\n' +\n  buildSection + '\\n' +\n  'ALL PROJECT FILES:\\n' + allFilesFormatted + '\\n\\n' +\n  'CRITICAL: Output the JSON response IMMEDIATELY \u2014 do NOT write analysis or reasoning before the JSON. Start your response with the opening { bracket. Return ONLY valid JSON:\\n' +\n  '{\\n  \"overall_quality\": number (0-100),\\n  \"cross_file_consistent\": boolean,\\n' +\n  '  \"visual_quality\": number (0-100),\\n' +\n  '  \"fixes_needed\": [\\n    {\\n      \"file\": \"path\",\\n      \"severity\": \"critical|high|medium\",\\n' +\n  '      \"issue\": \"description\",\\n      \"problem\": \"specific fix\",\\n      \"related_files\": [\"path\"]\\n    }\\n  ],\\n' +\n  '  \"visual_issues\": [\"specific visual problem 1\", \"specific visual problem 2\"],\\n' +\n  '  \"summary\": \"overall assessment including visual review\"\\n}';\n\n// Build multimodal content array\nconst content = [];\n// Include all reference images from the project\nif (refImages.length > 0) {\n  for (const ref of refImages) { // All refs \u2014 96K context can handle it\n    content.push({ type: 'image_url', image_url: { url: 'data:image/png;base64,' + ref.base64 } });\n    const label = ref.filename.replace(/\\.(png|jpg|jpeg|gif|webp)$/i, '').replace(/[_-]/g, ' ');\n    content.push({ type: 'text', text: 'REFERENCE: ' + label + ' (what the user wants):' });\n  }\n} else if (webhookImage) {\n  content.push({ type: 'image_url', image_url: { url: 'data:image/png;base64,' + webhookImage } });\n  content.push({ type: 'text', text: 'REFERENCE IMAGE (what the user wants it to look like):' });\n}\nif (screenshot_b64) {\n  content.push({ type: 'image_url', image_url: { url: 'data:image/png;base64,' + screenshot_b64 } });\n  content.push({ type: 'text', text: 'ACTUAL SCREENSHOT of the built application:' });\n}\n// Tablet/desktop screenshots removed to save context\n// Add playtest screenshots \u2014 reviewer sees the game being played\nif (playtestScreenshots.length > 0) {\n  for (const ps of playtestScreenshots.slice(0, 2)) { // Limit to 2 playtest screenshots\n    content.push({ type: 'image_url', image_url: { url: 'data:image/png;base64,' + ps.b64 } });\n    content.push({ type: 'text', text: 'PLAYTEST: ' + ps.label.replace(/_/g, ' ') });\n  }\n}\nif (playtestReport) {\n  browserSection += '\\nPLAYTEST OBSERVATIONS:\\n' + playtestReport + '\\n';\n}\ncontent.push({ type: 'text', text: textPrompt + browserSection });\n\n// Use multimodal array if we have images, plain text if not\nconst messageContent = content.length > 1 ? content : textPrompt;\n\nreturn [{\n  json: {\n    model: $env.REVIEWER_MODEL || 'qwen3.5-27b@q4_k_m',\n    messages: [{ role: 'user', content: messageContent }],\n    temperature: 0.7,\n    top_p: 0.8,\n    top_k: 20,\n    min_p: 0.0,\n    presence_penalty: 1.5,\n    max_tokens: 8192\n  }\n}];"
      },
      "id": "p3-review-build",
      "name": "P3: Full Review Build",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4050,
        500
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.LM_STUDIO_URL || 'http://10.0.0.100:1234/v1/chat/completions' }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json) }}",
        "options": {
          "timeout": 600000
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + $env.LLM_API_KEY }}"
            }
          ]
        }
      },
      "id": "p3-review-llm",
      "name": "P3: Review LLM",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        4250,
        500
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// Parses cross-file review format with fixes_needed[]\n// Merge visual review + code review\nconst staticData2 = $getWorkflowStaticData('global');\nconst visualReview = staticData2._visualReview || '';\n\n// Code review from current LLM call\nconst codeMsg = ($json.choices && $json.choices[0] && $json.choices[0].message) || {};\nconst codeContent = codeMsg.content || '';\nconst codeReasoning = codeMsg.reasoning_content || '';\n// Try content first, then reasoning for code review JSON\nconst codeReview = (codeContent && codeContent.includes('code_quality')) ? codeContent : codeReasoning;\n\n// Use visual review as primary (has overall quality), code review adds issues\nlet raw = visualReview;\n\n// Try to extract code issues and merge them\nlet codeIssues = [];\ntry {\n  const codeJson = JSON.parse(codeReview.match(/\\{[\\s\\S]*\\}/)?.[0] || '{}');\n  codeIssues = codeJson.issues || [];\n} catch {}\nlet content = raw.replace(/<think>[\\s\\S]*?<\\/think>/g, '').trim();\nif (content.includes('</think>')) content = content.split('</think>').pop().trim();\ncontent = content.replace(/^```(?:json)?\\n?/, '').replace(/\\n?```$/, '').trim();\nconst jsonMatch = content.match(/\\{[\\s\\S]*\\}/);\nlet review;\ntry {\n  review = JSON.parse(jsonMatch ? jsonMatch[0] : content);\n} catch (e) {\n  review = { overall_quality: 0, cross_file_consistent: true, fixes_needed: [], summary: 'Review parse failed: ' + content.substring(0, 200) };\n}\n\nconst fixes = [...(review.fixes_needed || []), ...codeIssues.map(i => ({ file: i.file, severity: i.severity, issue: i.issue, problem: i.problem || i.issue }))];\n// If visual review failed but code review produced issues, use code review quality\nlet codeQuality = 0;\ntry {\n  const codeJson = JSON.parse((codeReview.match(/\\{[\\s\\S]*\\}/) || ['{}'])[0]);\n  codeQuality = codeJson.code_quality || 0;\n} catch {}\n\n// Use whichever quality is higher (non-zero)\nif (review.overall_quality === 0 && codeQuality > 0) {\n  review.overall_quality = codeQuality;\n}\n\nconst criticalFixCount = fixes.filter(f => f.severity === 'critical' || f.severity === 'high').length;\nconst visualQuality = review.visual_quality || 0;\nconst visualIssues = review.visual_issues || [];\nconst projectId = $('Prepare Planner Input').first().json.project_id;\n\nconst staticData = $getWorkflowStaticData('global');\nstaticData._reviewResult = review;\n\nreturn [{ json: {\n  review,\n  critical_fix_count: criticalFixCount,\n  fixes_needed: fixes,\n  project_id: projectId\n} }];"
      },
      "id": "p3-review-parse",
      "name": "P3: Review Parse",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4450,
        500
      ]
    },
    {
      "parameters": {
        "jsCode": "// Determine if fixes are needed \u2014 adds _needsFix flag\nconst fixes = $json.fixes_needed || [];\nconst criticalCount = $json.critical_fix_count || 0;\nconst quality = ($json.review || {}).overall_quality || 0;\n\n// Only fix if: real fixes exist AND review actually produced valid output\nconst needsFix = fixes.length > 0 && criticalCount > 0 && quality > 0;\n\nreturn [{ json: { ...$json, _needsFix: needsFix } }];"
      },
      "id": "p3-needs-fix",
      "name": "P3: Route Decision",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4650,
        500
      ]
    },
    {
      "parameters": {
        "jsCode": "// Visual-aware fix: sees screenshot + reference images, not just text\nconst http = require('http');\nconst fixes = $json.fixes_needed || [];\nconst visualIssues = $json.visual_issues || [];\nconst projectId = $json.project_id;\nconst staticData = $getWorkflowStaticData('global');\nconst allFiles = staticData._allFileContents || [];\nconst screenshot_b64 = staticData._screenshot_b64 || null;\n\n// Load reference images from project references/ folder\nconst FILE_API = ($env.FILE_API_URL || 'http://file-api:3456').replace('http://', '');\nconst [apiHost, apiPort] = FILE_API.split(':');\nconst refImages = await new Promise((resolve) => {\n  const req = http.request({\n    hostname: apiHost, port: parseInt(apiPort) || 3456,\n    path: `/projects/${projectId}/references`,\n    method: 'GET',\n    headers: { 'Authorization': 'Bearer ' + ($env.FILE_API_TOKEN || '') },\n    timeout: 15000\n  }, (res) => {\n    let data = '';\n    res.on('data', d => data += d);\n    res.on('end', () => {\n      try { resolve(JSON.parse(data).references || []); } catch { resolve([]); }\n    });\n  });\n  req.on('error', () => resolve([]));\n  req.on('timeout', () => { req.destroy(); resolve([]); });\n  req.end();\n});\n\n// Collect files to fix + related context files\n// Only fix files that actually exist in the project\nconst existingPaths = new Set(allFiles.map(f => f.path));\nconst filesToFix = new Set(fixes.map(f => f.file).filter(f => existingPaths.has(f)));\nconst contextFiles = new Set();\nfixes.forEach(f => (f.related_files || []).forEach(rf => contextFiles.add(rf)));\n\n// Build fix instructions\nconst fixInstructions = fixes.map(f =>\n  `FILE: ${f.file}\\nSEVERITY: ${f.severity}\\nISSUE: ${f.issue}\\nFIX: ${f.fix_instruction}`\n).join('\\n\\n');\n\nconst visualSection = visualIssues.length > 0\n  ? '\\n\\nVISUAL ISSUES (from comparing screenshot to reference):\\n' + visualIssues.map((v,i) => `${i+1}. ${v}`).join('\\n')\n  : '';\n\n// Build existing files section\nconst relevantPaths = new Set([...filesToFix, ...contextFiles]);\nconst existingFilesSection = allFiles\n  .filter(f => relevantPaths.has(f.path))\n  .map(f => {\n    const marker = filesToFix.has(f.path) ? ' (NEEDS FIX)' : ' (CONTEXT ONLY - read but do not output)';\n    return '### ' + f.path + marker + '\\n```\\n' + f.content + '\\n```';\n  })\n  .join('\\n\\n');\n\nconst filesToOutput = [...filesToFix];\n\nlet systemMessage = 'You are a Senior Full-Stack Developer with strong UI/UX skills. You are fixing specific issues found during code review.\\n\\nYou can SEE the actual application screenshot and the reference design image. Fix ALL issues \u2014 both code bugs AND visual/layout problems.\\n\\nFor EACH file you fix, output EXACTLY this format:\\n### path/to/file.ts\\n```ts\\n[complete file content]\\n```\\n\\nRULES:\\n- Only output files marked as NEEDS FIX\\n- Only modify files that already exist in the CURRENT FILE CONTENTS section below. These are the only files you can write to.\\n- Output complete file content (not diffs)\\n- No explanations, no prose \u2014 ONLY the file blocks\\n- CRITICAL: Only use packages found in the Dependency Manifest below.\\n- CRITICAL: Match import styles to the source module exactly.\\n- CRITICAL: Files containing JSX MUST use .tsx extension, NOT .ts.';\n\nconst researchDocs = staticData._researchDocs || '';\nif (researchDocs) {\n  systemMessage += '\\n\\nPROJECT CONTEXT:\\n' + researchDocs;\n}\n\n// Detect assets the fixer should use\nconst assetFiles = allFiles\n  .filter(f => f.path.match(/^public\\/assets\\/.*\\.(png|jpg|jpeg|svg|gif|webp)$/i))\n  .map(f => f.path);\nconst assetReminder = assetFiles.length > 0\n  ? '\\n\\n\u26a0\ufe0f AVAILABLE ASSETS (use <img src> for these, do NOT use SVGs or emoji):\\n' +\n    assetFiles.map(a => '- ' + a + ' \u2192 <img src=\"/' + a.replace('public/', '') + '\" />').join('\\n')\n  : '';\n\nconst userText = 'Fix the following issues found during code review:\\n\\n' + fixInstructions + visualSection + '\\n\\nFILES TO FIX (only output these):\\n' + filesToOutput.join('\\n') + '\\n\\nCURRENT FILE CONTENTS:\\n' + existingFilesSection;\n\n// Build multimodal content array \u2014 fixer SEES the app + reference\nconst content = [];\nif (refImages.length > 0) {\n  for (const ref of refImages) {\n    content.push({ type: 'image_url', image_url: { url: 'data:image/png;base64,' + ref.base64 } });\n    const label = ref.filename.replace(/\\.(png|jpg|jpeg|gif|webp)$/i, '').replace(/[_-]/g, ' ');\n    content.push({ type: 'text', text: 'REFERENCE DESIGN (' + label + ') \u2014 the app should look like this:' });\n  }\n}\nif (screenshot_b64) {\n  content.push({ type: 'image_url', image_url: { url: 'data:image/png;base64,' + screenshot_b64 } });\n  content.push({ type: 'text', text: 'ACTUAL APP SCREENSHOT \u2014 this is what the app currently looks like. Compare to the reference above and fix ALL visual differences:' });\n}\n// Add browser inspection data\nconst consoleErrors = staticData._consoleErrors || [];\nconst layoutInfo = staticData._layoutInfo || null;\n\nlet browserData = '';\nif (consoleErrors.length > 0) {\n  browserData += '\\n\\nCONSOLE ERRORS (real runtime errors from browser):\\n' + consoleErrors.join('\\n');\n}\nif (layoutInfo) {\n  browserData += '\\n\\nLAYOUT INSPECTION (actual computed CSS \u2014 element positions and sizes):\\n' + JSON.stringify(layoutInfo, null, 2);\n  browserData += '\\nUse this data to verify your fixes produce correct layout. Elements should be centered, not overlapping, and sized for mobile (390x844 viewport).';\n}\n\ncontent.push({ type: 'text', text: userText + assetReminder + browserData });\n\nconst messageContent = content.length > 1 ? content : userText;\n\nreturn [{ json: {\n  model: $env.FIXER_MODEL || 'qwen3.5-27b@q4_k_m',\n  messages: [\n    { role: 'system', content: systemMessage },\n    { role: 'user', content: messageContent }\n  ],\n  temperature: 0.6,\n  top_p: 0.95,\n    top_k: 20,\n    min_p: 0.0,\n    presence_penalty: 0.0,\n  max_tokens: 16384,\n  _project_id: projectId\n} }];"
      },
      "id": "p4-fix-build",
      "name": "P4: Fix Build",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4850,
        400
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.LM_STUDIO_URL || 'http://10.0.0.100:1234/v1/chat/completions' }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json) }}",
        "options": {
          "timeout": 600000
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + $env.LLM_API_KEY }}"
            }
          ]
        }
      },
      "id": "p4-fix-llm",
      "name": "P4: Fix LLM",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        5050,
        400
      ]
    },
    {
      "parameters": {
        "jsCode": "// NEW: Parse fix response, update cache, prepare for write\nconst raw = $json.choices[0].message.content || $json.choices[0].message.reasoning_content || '';\nlet content = raw.replace(/<think>[\\s\\S]*?<\\/think>/g, '').trim();\nif (content.includes('</think>')) {\n  content = content.split('</think>').pop().trim();\n}\nconst files = [];\nconst seen = new Set();\nconst re = /###\\s+((?:[\\w.-]+\\/)*(?:\\.\\w[\\w.-]*|[\\w.-]+\\.(?:ts|tsx|js|jsx|json|md|yml|yaml|env|prisma|css|html|sh|txt|lock|toml|cfg|ini)|Dockerfile|Makefile|LICENSE|CHANGELOG))\\s*\\n```[\\w]*\\n([\\s\\S]*?)```/g;\nlet m;\nwhile ((m = re.exec(content)) !== null) {\n  const path = m[1].trim();\n  const fileContent = m[2];\n  if (path && !seen.has(path)) {\n    seen.add(path);\n    files.push({ path, content: fileContent });\n  }\n}\n\n// Update in-memory cache\nconst staticData = $getWorkflowStaticData('global');\nconst allFiles = staticData._allFileContents || [];\n\n// FILTER: Only allow fixes to files that already exist in the project\nconst existingPaths = new Set(allFiles.map(f => f.path));\nconst filteredFiles = files.filter(f => existingPaths.has(f.path));\nconst rejectedFiles = files.filter(f => !existingPaths.has(f.path));\nif (rejectedFiles.length > 0) {\n  console.log('Fixer tried to create new files (rejected):', rejectedFiles.map(f => f.path));\n}\nfor (const newFile of filteredFiles) {\n  const idx = allFiles.findIndex(f => f.path === newFile.path);\n  if (idx >= 0) {\n    allFiles[idx] = { path: newFile.path, content: newFile.content };\n  } else {\n    allFiles.push({ path: newFile.path, content: newFile.content });\n  }\n}\nstaticData._allFileContents = allFiles;\nstaticData._fixResults = filteredFiles.map(f => f.path);\n\nconst projectId = $('Prepare Planner Input').first().json.project_id;\n\nif (filteredFiles.length === 0) {\n  return [{ json: { project_id: projectId, files: [], _skipWrite: true } }];\n}\nreturn [{ json: { project_id: projectId, files: filteredFiles } }];"
      },
      "id": "p4-fix-parse",
      "name": "P4: Fix Parse",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        5250,
        400
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ ($env.FILE_API_URL || 'http://file-api:3456') + '/projects/' + $json.project_id + '/files-batch' }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + ($env.FILE_API_TOKEN || '') }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ files: $json.files }) }}",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      },
      "id": "p4-fix-write",
      "name": "P4: Fix Write",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        5450,
        400
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// MODIFIED: Includes review + fix results from Phase 3 & 4\nconst staticData = $getWorkflowStaticData('global');\nconst plannerInput = $('Prepare Planner Input').first().json;\nconst p2Results = staticData.p2Results || [];\nconst reviewResult = staticData._reviewResult || {};\nconst fixResults = staticData._fixResults || [];\nconst finalReview = staticData._finalReview || {};\nconst allTasks = staticData.allTasks || [];\n\n// Build task-level results from p2Results\nconst taskResults = new Map();\nfor (const r of p2Results) {\n  if (!taskResults.has(r.task_id)) {\n    const task = allTasks.find(t => t.task_id === r.task_id);\n    taskResults.set(r.task_id, {\n      task_id: r.task_id,\n      description: task ? task.description.substring(0, 200) : '',\n      files_written: [],\n      chunks_processed: 0\n    });\n  }\n  const entry = taskResults.get(r.task_id);\n  entry.files_written.push(...(r.files_written || []));\n  entry.chunks_processed++;\n}\n\nconst allFilesWritten = p2Results.flatMap(r => r.files_written || []);\n\nreturn [{ json: {\n  status: 'completed',\n  project_id: plannerInput.project_id,\n  project_goal: plannerInput.project_goal,\n  tasks_completed: taskResults.size,\n  task_results: [...taskResults.values()],\n  review: {\n    overall_quality: reviewResult.overall_quality || 0,\n    cross_file_consistent: reviewResult.cross_file_consistent || false,\n    fixes_applied: fixResults,\n    summary: reviewResult.summary || ''\n  },\n  files_written: [...new Set(allFilesWritten)],\n  final_review: {\n    quality: finalReview.final_quality || 0,\n    summary: finalReview.summary || '',\n    suggestions: finalReview.suggestions || []\n  }\n} }];"
      },
      "id": "aggregate",
      "name": "Aggregate All Results",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        5650,
        500
      ]
    },
    {
      "parameters": {
        "jsCode": "const result = $json;\nreturn [{ json: {\n  operation: 'set',\n  project_id: result.project_id,\n  state: {\n    status: 'completed',\n    completed_tasks: result.task_results.map(t => t.task_id),\n    files: result.files_written\n  }\n} }];"
      },
      "id": "prepare-mem-update",
      "name": "Prepare Memory Update",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        5850,
        500
      ]
    },
    {
      "parameters": {
        "jsCode": "const op = $json;\nconst store = $getWorkflowStaticData('global');\nconst projectId = op.project_id;\nif (op.operation === 'set') {\n  const existing = store[projectId] || {};\n  store[projectId] = { ...existing, ...op.state, project_id: projectId, last_updated: new Date().toISOString() };\n}\nreturn [{ json: store[projectId] || { project_id: projectId } }];"
      },
      "id": "update-memory",
      "name": "Update Memory",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        6050,
        500
      ]
    },
    {
      "parameters": {
        "jsCode": "const aggregate = $('Aggregate All Results').first().json;\nreturn [{ json: aggregate }];"
      },
      "id": "build-response",
      "name": "Build Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        6250,
        500
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "p0-ref-url-check",
              "leftValue": "={{ $('Extract Input').first().json.reference_url }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEmpty"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "p0-has-reference-url",
      "name": "P0: Has Reference URL?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1050,
        500
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ ($env.FILE_API_URL || 'http://file-api:3456') + '/scrape' }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + ($env.FILE_API_TOKEN || '') }}"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ url: $('Extract Input').first().json.reference_url }) }}",
        "options": {
          "timeout": 30000
        }
      },
      "id": "p0-scrape-url",
      "name": "P0: Scrape URL",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1250,
        600
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "const scrapeData = $json;\nconst staticData = $getWorkflowStaticData('global');\nif (scrapeData && scrapeData.screenshot_b64) {\n  staticData._scrapeData = scrapeData;\n} else {\n  staticData._scrapeData = null;\n}\nreturn [{ json: $('Prepare Planner Input').first().json }];"
      },
      "id": "p0-merge-scrape-data",
      "name": "P0: Merge Scrape Data",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1450,
        600
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ ($env.LM_STUDIO_HOST || 'http://10.0.0.100:1234') + '/api/v1/models/load' }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: $env.PLANNER_MODEL || 'qwen3.5-27b@q4_k_m', context_length: parseInt($env.PLANNER_CTX) || 32768 }) }}",
        "options": {
          "timeout": 120000
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + ($env.LLM_API_KEY || '') }}"
            }
          ]
        }
      },
      "id": "load-planner-model",
      "name": "Load Planner Model",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1150,
        100
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "const loadResp = $json;\nif (loadResp.status !== 'loaded') throw new Error('Load Planner Model failed: ' + JSON.stringify(loadResp));\nconst plannerInput = $('Prepare Planner Input').first().json;\nconst staticData = $getWorkflowStaticData('global');\nif (!$('Extract Input').first().json.reference_url) {\n  staticData._scrapeData = null;\n}\nreturn [{ json: { ...plannerInput, _scrapeData: staticData._scrapeData || null } }];"
      },
      "id": "restore-planner-input",
      "name": "Restore: Planner Input",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1350,
        100
      ]
    },
    {
      "parameters": {
        "jsCode": "const http = require('http');\nfunction lmsRequest(method, path, body) {\n  return new Promise((resolve, reject) => {\n    const lmsHost = ($env.LM_STUDIO_HOST || 'http://10.0.0.100:1234').replace(/^https?:\\/\\//, '');\n    const [hostname, portStr] = lmsHost.split(':');\n    const bodyStr = body ? JSON.stringify(body) : null;\n    const req = http.request({\n      hostname, port: parseInt(portStr) || 1234, path, method,\n      headers: {\n        Authorization: 'Bearer ' + ($env.LLM_API_KEY || ''),\n        'Content-Type': 'application/json',\n        ...(bodyStr ? { 'Content-Length': Buffer.byteLength(bodyStr) } : {})\n      }\n    }, res => {\n      let d = ''; res.on('data', c => d += c);\n      res.on('end', () => { try { resolve(JSON.parse(d)); } catch(e) { resolve({}); } });\n    });\n    req.on('error', reject);\n    if (bodyStr) req.write(bodyStr);\n    req.end();\n  });\n}\n\nconst modelKey = $env.PLANNER_MODEL || '';\nconst staticData = $getWorkflowStaticData('global');\ntry {\n  const resp = await lmsRequest('GET', '/api/v1/models');\n  for (const m of (resp.models || [])) {\n    if (m.key === modelKey) {\n      for (const inst of (m.loaded_instances || [])) {\n        try { await lmsRequest('POST', '/api/v1/models/unload', { instance_id: inst.id }); } catch(e) {}\n      }\n    }\n  }\n} catch(e) { console.error('Unload error:', e.message); }\nstaticData._plannerInstanceId = null;\nreturn [{ json: $('Planner: Parse Response').first().json }];"
      },
      "id": "unload-planner-model",
      "name": "Unload Planner Model",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1750,
        100
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ ($env.LM_STUDIO_HOST || 'http://10.0.0.100:1234') + '/api/v1/models/load' }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: $env.CODER_MODEL || 'qwen/qwen3-coder-next', context_length: parseInt($env.CODER_CTX) || 131072 }) }}",
        "options": {
          "timeout": 120000
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + ($env.LLM_API_KEY || '') }}"
            }
          ]
        }
      },
      "id": "load-coder-model",
      "name": "Load Coder Model",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1950,
        100
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "const loadResp = $json;\nif (loadResp.status !== 'loaded') throw new Error('Load Coder Model failed: ' + JSON.stringify(loadResp));\nconst staticData = $getWorkflowStaticData('global');\nstaticData._coderInstanceId = loadResp.instance_id;\nreturn [{ json: $('Planner: Parse Response').first().json }];"
      },
      "id": "restore-after-load-coder",
      "name": "Restore: After Load Coder",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2150,
        100
      ]
    },
    {
      "parameters": {
        "jsCode": "const http = require('http');\nfunction lmsRequest(method, path, body) {\n  return new Promise((resolve, reject) => {\n    const lmsHost = ($env.LM_STUDIO_HOST || 'http://10.0.0.100:1234').replace(/^https?:\\/\\//, '');\n    const [hostname, portStr] = lmsHost.split(':');\n    const bodyStr = body ? JSON.stringify(body) : null;\n    const req = http.request({\n      hostname, port: parseInt(portStr) || 1234, path, method,\n      headers: {\n        Authorization: 'Bearer ' + ($env.LLM_API_KEY || ''),\n        'Content-Type': 'application/json',\n        ...(bodyStr ? { 'Content-Length': Buffer.byteLength(bodyStr) } : {})\n      }\n    }, res => {\n      let d = ''; res.on('data', c => d += c);\n      res.on('end', () => { try { resolve(JSON.parse(d)); } catch(e) { resolve({}); } });\n    });\n    req.on('error', reject);\n    if (bodyStr) req.write(bodyStr);\n    req.end();\n  });\n}\n\nconst modelKey = $env.CODER_MODEL || '';\nconst staticData = $getWorkflowStaticData('global');\ntry {\n  const resp = await lmsRequest('GET', '/api/v1/models');\n  for (const m of (resp.models || [])) {\n    if (m.key === modelKey) {\n      for (const inst of (m.loaded_instances || [])) {\n        try { await lmsRequest('POST', '/api/v1/models/unload', { instance_id: inst.id }); } catch(e) {}\n      }\n    }\n  }\n} catch(e) { console.error('Unload error:', e.message); }\nstaticData._coderInstanceId = null;\nreturn [{ json: $json }];"
      },
      "id": "unload-coder-model",
      "name": "Unload Coder Model",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3750,
        100
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ ($env.LM_STUDIO_HOST || 'http://10.0.0.100:1234') + '/api/v1/models/load' }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: $env.REVIEWER_MODEL || 'qwen3.5-27b@q4_k_m', context_length: parseInt($env.REVIEWER_CTX) || 32768 }) }}",
        "options": {
          "timeout": 120000
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + ($env.LLM_API_KEY || '') }}"
            }
          ]
        }
      },
      "id": "load-reviewer-model",
      "name": "Load Reviewer Model",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3950,
        100
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "const loadResp = $json;\nif (loadResp.status !== 'loaded') throw new Error('Load Reviewer Model failed: ' + JSON.stringify(loadResp));\nconst staticData = $getWorkflowStaticData('global');\nstaticData._reviewerInstanceId = loadResp.instance_id;\nreturn [{ json: { project_id: $('Extract Input').first().json.project_id } }];"
      },
      "id": "restore-after-load-reviewer",
      "name": "Restore: After Load Reviewer",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4150,
        100
      ]
    },
    {
      "parameters": {
        "jsCode": "const http = require('http');\nfunction lmsRequest(method, path, body) {\n  return new Promise((resolve, reject) => {\n    const lmsHost = ($env.LM_STUDIO_HOST || 'http://10.0.0.100:1234').replace(/^https?:\\/\\//, '');\n    const [hostname, portStr] = lmsHost.split(':');\n    const bodyStr = body ? JSON.stringify(body) : null;\n    const req = http.request({\n      hostname, port: parseInt(portStr) || 1234, path, method,\n      headers: {\n        Authorization: 'Bearer ' + ($env.LLM_API_KEY || ''),\n        'Content-Type': 'application/json',\n        ...(bodyStr ? { 'Content-Length': Buffer.byteLength(bodyStr) } : {})\n      }\n    }, res => {\n      let d = ''; res.on('data', c => d += c);\n      res.on('end', () => { try { resolve(JSON.parse(d)); } catch(e) { resolve({}); } });\n    });\n    req.on('error', reject);\n    if (bodyStr) req.write(bodyStr);\n    req.end();\n  });\n}\n\nconst modelKey = $env.REVIEWER_MODEL || '';\nconst staticData = $getWorkflowStaticData('global');\ntry {\n  const resp = await lmsRequest('GET', '/api/v1/models');\n  for (const m of (resp.models || [])) {\n    if (m.key === modelKey) {\n      for (const inst of (m.loaded_instances || [])) {\n        try { await lmsRequest('POST', '/api/v1/models/unload', { instance_id: inst.id }); } catch(e) {}\n      }\n    }\n  }\n} catch(e) { console.error('Unload error:', e.message); }\nstaticData._reviewerInstanceId = null;\nreturn [{ json: $('P3: Review Parse').first().json }];"
      },
      "id": "unload-reviewer-model",
      "name": "Unload Reviewer Model",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4350,
        100
      ]
    },
    {
      "parameters": {
        "jsCode": "return [{ json: $('P3: Review Parse').first().json }];"
      },
      "id": "restore-after-unload-reviewer",
      "name": "Restore: After Unload Reviewer",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4550,
        100
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ ($env.LM_STUDIO_HOST || 'http://10.0.0.100:1234') + '/api/v1/models/load' }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: $env.FIXER_MODEL || 'qwen3.5-27b@q4_k_m', context_length: parseInt($env.FIXER_CTX) || 32768 }) }}",
        "options": {
          "timeout": 120000
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + ($env.LLM_API_KEY || '') }}"
            }
          ]
        }
      },
      "id": "load-fixer-model",
      "name": "Load Fixer Model",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        4750,
        100
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "const loadResp = $json;\nif (loadResp.status !== 'loaded') throw new Error('Load Fixer Model failed: ' + JSON.stringify(loadResp));\nconst staticData = $getWorkflowStaticData('global');\nstaticData._fixerInstanceId = loadResp.instance_id;\nreturn [{ json: $('P3: Route Decision').first().json }];"
      },
      "id": "restore-after-load-fixer",
      "name": "Restore: After Load Fixer",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4950,
        100
      ]
    },
    {
      "parameters": {
        "jsCode": "const http = require('http');\nfunction lmsRequest(method, path, body) {\n  return new Promise((resolve, reject) => {\n    const lmsHost = ($env.LM_STUDIO_HOST || 'http://10.0.0.100:1234').replace(/^https?:\\/\\//, '');\n    const [hostname, portStr] = lmsHost.split(':');\n    const bodyStr = body ? JSON.stringify(body) : null;\n    const req = http.request({\n      hostname, port: parseInt(portStr) || 1234, path, method,\n      headers: {\n        Authorization: 'Bearer ' + ($env.LLM_API_KEY || ''),\n        'Content-Type': 'application/json',\n        ...(bodyStr ? { 'Content-Length': Buffer.byteLength(bodyStr) } : {})\n      }\n    }, res => {\n      let d = ''; res.on('data', c => d += c);\n      res.on('end', () => { try { resolve(JSON.parse(d)); } catch(e) { resolve({}); } });\n    });\n    req.on('error', reject);\n    if (bodyStr) req.write(bodyStr);\n    req.end();\n  });\n}\n\nconst modelKey = $env.FIXER_MODEL || '';\nconst staticData = $getWorkflowStaticData('global');\ntry {\n  const resp = await lmsRequest('GET', '/api/v1/models');\n  for (const m of (resp.models || [])) {\n    if (m.key === modelKey) {\n      for (const inst of (m.loaded_instances || [])) {\n        try { await lmsRequest('POST', '/api/v1/models/unload', { instance_id: inst.id }); } catch(e) {}\n      }\n    }\n  }\n} catch(e) { console.error('Unload error:', e.message); }\nstaticData._fixerInstanceId = null;\nreturn [{ json: $('P4: Fix Write').first().json }];"
      },
      "id": "unload-fixer-model",
      "name": "Unload Fixer Model",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        5550,
        100
      ]
    },
    {
      "parameters": {
        "jsCode": "return [{ json: $('P4: Fix Write').first().json }];"
      },
      "id": "restore-after-unload-fixer",
      "name": "Restore: After Unload Fixer",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        5700,
        100
      ]
    },
    {
      "parameters": {
        "jsCode": "// Startup: Unload all loaded models before beginning pipeline\n// Prevents leftover models from crashed/cancelled runs eating VRAM\nconst http = require('http');\n\nfunction httpRequest(options, body) {\n  return new Promise((resolve, reject) => {\n    const req = http.request(options, (res) => {\n      let data = '';\n      res.on('data', chunk => data += chunk);\n      res.on('end', () => {\n        try { resolve(JSON.parse(data)); }\n        catch(e) { resolve(data); }\n      });\n    });\n    req.on('error', reject);\n    if (body) req.write(body);\n    req.end();\n  });\n}\n\nconst host = '10.0.0.100';\nconst port = 1234;\nconst apiKey = $env.LLM_API_KEY || '';\n\n// Get all loaded instances\nconst modelsResp = await httpRequest({\n  host, port,\n  path: '/api/v1/models',\n  method: 'GET',\n  headers: { 'Authorization': 'Bearer ' + apiKey }\n});\n\nconst unloaded = [];\nfor (const model of (modelsResp.models || [])) {\n  for (const inst of (model.loaded_instances || [])) {\n    const body = JSON.stringify({ instance_id: inst.id });\n    await httpRequest({\n      host, port,\n      path: '/api/v1/models/unload',\n      method: 'POST',\n      headers: {\n        'Authorization': 'Bearer ' + apiKey,\n        'Content-Type': 'application/json',\n        'Content-Length': Buffer.byteLength(body)\n      }\n    }, body);\n    unloaded.push(inst.id);\n  }\n}\n\nreturn [{ json: { ...$json, _startupUnloaded: unloaded } }];"
      },
      "id": "startup-unload-all",
      "name": "Startup: Unload All Models",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        950,
        100
      ]
    },
    {
      "id": "p2-continue-gate",
      "name": "P2: Continue Gate",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3650,
        200
      ],
      "parameters": {
        "jsCode": "// Gate: pass through next item if NOT done, return [] if done\nif ($json._done === true || !$json._nextItem) {\n  return [];\n}\nreturn [{ json: $json._nextItem }];"
      }
    },
    {
      "id": "p2-exit-gate",
      "name": "P2: Exit Gate",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3650,
        400
      ],
      "parameters": {
        "jsCode": "// Gate: pass through if done, return [] if NOT done\nif ($json._done !== true) {\n  return [];\n}\nreturn [{ json: { _remaining: 0, task_id: $json.task_id, files_written: $json.files_written } }];"
      }
    },
    {
      "id": "cb-pipeline-started",
      "name": "CB: Pipeline Started",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        300,
        500
      ],
      "parameters": {
        "jsCode": "// Status callback to Forge \u2014 fire and forget\nconst http = require('http');\nconst event = 'pipeline_started';\nconst projectId = $json.project_id || $getWorkflowStaticData('global')._currentProjectId || 'unknown';\nconst callbackData = { project_id: projectId };\n\ntry {\n  const body = JSON.stringify({ event, project_id: projectId, data: callbackData });\n  const req = http.request({\n    hostname: 'forge', port: 3500, path: '/api/status-callback',\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }\n  });\n  req.on('error', () => {});  // ignore errors \u2014 pipeline must not stop\n  req.write(body);\n  req.end();\n} catch(e) {}\n\nreturn [$input.all()[0]];  // pass through unchanged"
      }
    },
    {
      "id": "cb-planning-complete",
      "name": "CB: Planning Complete",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1650,
        500
      ],
      "parameters": {
        "jsCode": "// Status callback to Forge \u2014 fire and forget\nconst http = require('http');\nconst event = 'planning_complete';\nconst projectId = $json.project_id || $getWorkflowStaticData('global')._currentProjectId || 'unknown';\nconst callbackData = { task_count: ($json.tasks || []).length, project_id: projectId };\n\ntry {\n  const body = JSON.stringify({ event, project_id: projectId, data: callbackData });\n  const req = http.request({\n    hostname: 'forge', port: 3500, path: '/api/status-callback',\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }\n  });\n  req.on('error', () => {});  // ignore errors \u2014 pipeline must not stop\n  req.write(body);\n  req.end();\n} catch(e) {}\n\n// Stash plan for retrieval after Wait node\nconst staticData2 = $getWorkflowStaticData('global');\nstaticData2._pendingPlan = $input.all()[0].json;\n\nreturn [$input.all()[0]];  // pass through unchanged"
      }
    },
    {
      "id": "cb-review-complete",
      "name": "CB: Review Complete",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        5050,
        500
      ],
      "parameters": {
        "jsCode": "// Status callback to Forge \u2014 fire and forget\nconst http = require('http');\nconst event = 'review_complete';\nconst projectId = $json.project_id || $getWorkflowStaticData('global')._currentProjectId || 'unknown';\nconst callbackData = { quality: $json.review?.overall_quality || 0, summary: ($json.review?.summary || '').substring(0, 1000), project_id: projectId };\n\ntry {\n  const body = JSON.stringify({ event, project_id: projectId, data: callbackData });\n  const req = http.request({\n    hostname: 'forge', port: 3500, path: '/api/status-callback',\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }\n  });\n  req.on('error', () => {});  // ignore errors \u2014 pipeline must not stop\n  req.write(body);\n  req.end();\n} catch(e) {}\n\nreturn [$input.all()[0]];  // pass through unchanged"
      }
    },
    {
      "id": "cb-pipeline-complete",
      "name": "CB: Pipeline Complete",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        6250,
        500
      ],
      "parameters": {
        "jsCode": "// Status callback to Forge \u2014 fire and forget\nconst http = require('http');\nconst event = 'pipeline_complete';\nconst projectId = $json.project_id || $getWorkflowStaticData('global')._currentProjectId || 'unknown';\nconst callbackData = { tasks_completed: $json.tasks_completed || 0, files_written: $json.files_written || [], summary: ($json.final_review?.summary || $json.review?.summary || '').substring(0, 1000), suggestions: $json.final_review?.suggestions || [], final_quality: $json.final_review?.quality || 0, project_id: projectId };\n\ntry {\n  const body = JSON.stringify({ event, project_id: projectId, data: callbackData });\n  const req = http.request({\n    hostname: 'forge', port: 3500, path: '/api/status-callback',\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }\n  });\n  req.on('error', () => {});  // ignore errors \u2014 pipeline must not stop\n  req.write(body);\n  req.end();\n} catch(e) {}\n\nreturn [$input.all()[0]];  // pass through unchanged"
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ ($env.LM_STUDIO_HOST || 'http://10.0.0.100:1234') + '/api/v1/models/load' }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: $env.FINAL_REVIEWER_MODEL || 'deepseek-r1-distill-qwen-32b', context_length: parseInt($env.FINAL_REVIEWER_CTX) || 131072 }) }}",
        "options": {
          "timeout": 120000
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + ($env.LLM_API_KEY || '') }}"
            }
          ]
        }
      },
      "id": "load-final-reviewer",
      "name": "Load Final Reviewer",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        5700,
        300
      ],
      "onError": "continueRegularOutput"
    },
    {
      "id": "restore-final-reviewer",
      "name": "Restore: Final Reviewer",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        5900,
        300
      ],
      "parameters": {
        "jsCode": "const loadResp = $json;\nif (loadResp.status !== 'loaded') throw new Error('Load Final Reviewer failed: ' + JSON.stringify(loadResp));\nreturn [{ json: { project_id: $('Prepare Planner Input').first().json.project_id } }];"
      }
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ ($env.FILE_API_URL || 'http://file-api:3456') + '/projects/' + $('Prepare Planner Input').first().json.project_id + '/files-content' }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + ($env.FILE_API_TOKEN || '') }}"
            }
          ]
        },
        "options": {}
      },
      "id": "final-refetch",
      "name": "Final: Re-fetch Files",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        6100,
        300
      ],
      "onError": "continueRegularOutput"
    },
    {
      "id": "final-review-build",
      "name": "Final: Review Build",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        6300,
        300
      ],
      "parameters": {
        "jsCode": "const http = require('http');\nconst allFiles = $json.files || [];\nconst staticData = $getWorkflowStaticData('global');\nconst plannerInput = $('Prepare Planner Input').first().json;\nconst projectGoal = plannerInput.project_goal || '';\nconst projectId = $('Extract Input').first().json.project_id;\nconst initialReview = staticData._reviewResult || {};\n\n// Run actual build check before reviewing\nconst FILE_API = ($env.FILE_API_URL || 'http://file-api:3456').replace('http://', '');\nconst [apiHost, apiPort] = FILE_API.split(':');\nconst buildResult = await new Promise((resolve) => {\n  const postData = JSON.stringify({});\n  const req = http.request({\n    hostname: apiHost, port: parseInt(apiPort) || 3456,\n    path: `/projects/${projectId}/build-check`,\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n      'Authorization': 'Bearer ' + ($env.FILE_API_TOKEN || ''),\n      'Content-Length': Buffer.byteLength(postData)\n    },\n    timeout: 120000\n  }, (res) => {\n    let data = '';\n    res.on('data', d => data += d);\n    res.on('end', () => {\n      try { resolve(JSON.parse(data)); } catch { resolve({ success: false, error: 'parse error' }); }\n    });\n  });\n  req.on('error', (e) => resolve({ success: false, error: e.message }));\n  req.on('timeout', () => { req.destroy(); resolve({ success: false, error: 'timeout' }); });\n  req.write(postData);\n  req.end();\n});\n\nconst buildStatus = buildResult.success\n  ? 'BUILD PASSES (vite build succeeded)'\n  : `BUILD FAILS: ${buildResult.error || 'unknown error'}${buildResult.stage ? ' (stage: ' + buildResult.stage + ')' : ''}`;\n\nconst allFilesFormatted = allFiles\n  .filter(f => f.path.match(/\\.(ts|tsx|js|jsx|json|css|html)$/) && !f.path.includes('node_modules'))\n  .map(f => '### ' + f.path + '\\n```\\n' + f.content + '\\n```')\n  .join('\\n\\n');\n\nconst prompt = `You are a Senior Code Reviewer performing a FINAL REVIEW of a completed project.\n\nProject Goal: ${projectGoal}\n\nACTUAL BUILD STATUS (from running vite build): ${buildStatus}\n\nInitial Review Score: ${initialReview.overall_quality || 'N/A'}/100\nInitial Issues Found: ${(initialReview.fixes_needed || []).length}\nFixes Were Applied: Yes\n\nALL PROJECT FILES (after fixes):\n${allFilesFormatted}\n\nReturn ONLY valid JSON (no markdown):\n{\n  \"final_quality\": number (0-100),\n  \"builds\": ${buildResult.success ? 'true' : 'false'},\n  \"summary\": \"2-3 sentence assessment of the project in its current state\",\n  \"suggestions\": [\n    {\n      \"preview\": \"Short 1-sentence title describing the scope of this suggestion\",\n      \"detail\": \"The full detailed engineering brief \u2014 10-20 sentences with every file path, function name, prop name, and exact change described\"\n    }\n  ]\n}\n\nIMPORTANT: The build status above is REAL \u2014 it was just run. If the build FAILS, your first suggestion MUST be a CRITICAL FIXES suggestion that addresses EVERY build error. Do NOT score above 40 if the build fails. Do NOT say \"the project builds successfully\" if BUILD FAILS appears above.\n\nRULES FOR SUGGESTIONS:\n\nProduce exactly 2-3 suggestions. Each suggestion has a \"preview\" (1 sentence, ~15 words) and a \"detail\" (full engineering brief).\n\nThe \"detail\" field is what gets sent to the coding pipeline. The pipeline has a 128K context coder that handles 6-8 files per task. Each suggestion's detail should generate 2-4 large tasks.\n\nSuggestion categories (PRIORITY ORDER \u2014 address higher categories first):\\n1. VISUAL/LAYOUT PROBLEMS: Broken layouts visible in screenshots \u2014 containers not wrapping content, elements overlapping, bare backgrounds showing, misaligned spacing, content hidden behind nav bars, duplicate elements. These are the HIGHEST priority. If you see layout issues in the screenshots, ALL suggestions should address them.\\n2. WIRING/FUNCTIONALITY: Features that exist visually but don't actually work \u2014 buttons that do nothing, hardcoded data that should be dynamic, missing click handlers, unconnected state. Describe what's broken and which file needs the fix.\\n3. POLISH (only if layout and wiring are solid): Animations, loading states, accessibility, responsive tweaks. Do NOT suggest polish if there are layout or wiring issues.\n\nEach \"detail\" should be a DENSE paragraph of 10-20 sentences. It should read like a complete engineering brief \u2014 a developer can execute every change from the description alone. Reference specific file paths, function names, prop names, state variables, and CSS classes throughout.\n\nIMPORTANT: Base your suggestions on what you SEE in the screenshots, not what you imagine could be added. If the screenshot shows broken layout, that is your #1 suggestion \u2014 not new features.\\n\\nIf the project is in great shape (90+ AND builds), focus suggestions on enhancements: new features, dark mode, localStorage persistence, accessibility, performance optimization.`;\n\nreturn [{\n  json: {\n    model: $env.FINAL_REVIEWER_MODEL || 'deepseek-r1-distill-qwen-32b',\n    messages: [{ role: 'user', content: prompt }],\n    temperature: parseFloat($env.FINAL_REVIEWER_TEMP) || 0.3,\n    top_p: parseFloat($env.FINAL_REVIEWER_TOP_P) || 0.7,\n    max_tokens: 16384\n  }\n}];"
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.LM_STUDIO_URL || 'http://10.0.0.100:1234/v1/chat/completions' }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json) }}",
        "options": {
          "timeout": 600000
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + $env.LLM_API_KEY }}"
            }
          ]
        }
      },
      "id": "final-review-llm",
      "name": "Final: Review LLM",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        6500,
        300
      ],
      "onError": "continueRegularOutput"
    },
    {
      "id": "final-review-parse",
      "name": "Final: Review Parse",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        6700,
        300
      ],
      "parameters": {
        "jsCode": "// Parse final review \u2014 robust JSON extraction\nconst msg = ($json.choices && $json.choices[0] && $json.choices[0].message) || {};\nlet raw = msg.content || '';\n\n// If content is empty/whitespace, try reasoning_content\nif (!raw.trim()) raw = msg.reasoning_content || '';\n\n// Strip thinking blocks\nlet content = raw.replace(/<think>[\\s\\S]*?<\\/think>/g, '').trim();\nif (content.includes('</think>')) content = content.split('</think>').pop().trim();\n\n// Strip markdown code fences\ncontent = content.replace(/^```(?:json)?\\n?/, '').replace(/\\n?```$/, '').trim();\n\n// Try multiple parse strategies\nlet review;\ntry {\n  // Strategy 1: direct parse (content is pure JSON)\n  review = JSON.parse(content);\n} catch(e1) {\n  try {\n    // Strategy 2: extract JSON object with balanced braces\n    let depth = 0, start = -1, end = -1;\n    for (let i = 0; i < content.length; i++) {\n      if (content[i] === '{') { if (depth === 0) start = i; depth++; }\n      if (content[i] === '}') { depth--; if (depth === 0) { end = i + 1; break; } }\n    }\n    if (start >= 0 && end > start) {\n      review = JSON.parse(content.substring(start, end));\n    } else {\n      throw new Error('No balanced JSON found');\n    }\n  } catch(e2) {\n    review = { final_quality: 0, summary: 'Final review parse failed: ' + e2.message, suggestions: [] };\n  }\n}\n\n// Normalize suggestions format (handle both string[] and {preview, detail}[] )\nif (review.suggestions) {\n  review.suggestions = review.suggestions.map(s => {\n    if (typeof s === 'string') return s;\n    if (s && s.preview) return s;\n    return String(s);\n  });\n}\n\nconst staticData = $getWorkflowStaticData('global');\nstaticData._finalReview = review;\n\n// Send callback to Forge\ntry {\n  const http = require('http');\n  const projectId = $('Prepare Planner Input').first().json.project_id || 'unknown';\n  const cbBody = JSON.stringify({\n    event: 'final_review_complete',\n    project_id: projectId,\n    data: { quality: review.final_quality, summary: review.summary, suggestions: review.suggestions }\n  });\n  const req = http.request({\n    hostname: 'forge', port: 3500, path: '/api/status-callback',\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(cbBody) }\n  });\n  req.on('error', () => {});\n  req.write(cbBody);\n  req.end();\n} catch(e) {}\n\nreturn [{ json: review }];"
      }
    },
    {
      "id": "unload-final-reviewer",
      "name": "Unload Final Reviewer",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        6900,
        300
      ],
      "parameters": {
        "jsCode": "const http = require('http');\nfunction lmsRequest(method, path, body) {\n  return new Promise((resolve, reject) => {\n    const lmsHost = ($env.LM_STUDIO_HOST || 'http://10.0.0.100:1234').replace(/^https?:\\/\\//, '');\n    const [hostname, portStr] = lmsHost.split(':');\n    const bodyStr = body ? JSON.stringify(body) : null;\n    const req = http.request({\n      hostname, port: parseInt(portStr) || 1234, path, method,\n      headers: {\n        Authorization: 'Bearer ' + ($env.LLM_API_KEY || ''),\n        'Content-Type': 'application/json',\n        ...(bodyStr ? { 'Content-Length': Buffer.byteLength(bodyStr) } : {})\n      }\n    }, res => {\n      let d = ''; res.on('data', c => d += c);\n      res.on('end', () => { try { resolve(JSON.parse(d)); } catch(e) { resolve({}); } });\n    });\n    req.on('error', reject);\n    if (bodyStr) req.write(bodyStr);\n    req.end();\n  });\n}\nconst modelKey = $env.FINAL_REVIEWER_MODEL || '';\ntry {\n  const resp = await lmsRequest('GET', '/api/v1/models');\n  for (const m of (resp.models || [])) {\n    if (m.key === modelKey) {\n      for (const inst of (m.loaded_instances || [])) {\n        try { await lmsRequest('POST', '/api/v1/models/unload', { instance_id: inst.id }); } catch(e) {}\n      }\n    }\n  }\n} catch(e) {}\nreturn [{ json: $('Final: Review Parse').first().json }];"
      }
    },
    {
      "id": "cb-fix-applied",
      "name": "CB: Fix Applied",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        5500,
        100
      ],
      "parameters": {
        "jsCode": "// Status callback to Forge \u2014 fire and forget\nconst http = require('http');\nconst staticData = $getWorkflowStaticData('global');\nconst projectId = staticData._currentProjectId || $('Prepare Planner Input').first().json.project_id || 'unknown';\nconst fixResults = staticData._fixResults || [];\n\ntry {\n  const body = JSON.stringify({\n    event: 'fix_applied',\n    project_id: projectId,\n    data: { files_fixed: fixResults, fix_count: fixResults.length }\n  });\n  const req = http.request({\n    hostname: 'forge', port: 3500, path: '/api/status-callback',\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }\n  });\n  req.on('error', () => {});\n  req.write(body);\n  req.end();\n} catch(e) {}\n\nreturn [$input.all()[0]];"
      }
    },
    {
      "id": "research-fetch-docs",
      "name": "Research: Fetch Docs",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2050,
        300
      ],
      "parameters": {
        "jsCode": "// Research: Fetch library docs \u2014 Context7 \u2192 Exa fallback \u2192 Magic UI\nconst http = require('http');\nconst https = require('https');\nconst staticData = $getWorkflowStaticData('global');\nconst allFiles = staticData._allFileContents || [];\nconst tasks = $json.tasks || [];\n\n// \u2500\u2500\u2500 1. Extract libraries from package.json + task text \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst libs = new Set();\nconst pkgFile = allFiles.find(f => f.path === 'package.json');\nif (pkgFile) {\n  try {\n    const pkg = JSON.parse(pkgFile.content);\n    for (const dep of Object.keys(pkg.dependencies || {})) libs.add(dep);\n    for (const dep of Object.keys(pkg.devDependencies || {})) libs.add(dep);\n  } catch(e) {}\n}\nconst taskText = tasks.map(t => t.description || '').join(' ').toLowerCase();\nconst knownLibs = ['react', 'next', 'nextjs', 'vite', 'tailwindcss', 'tailwind', 'express', 'prisma', 'framer-motion', 'heroui', 'zustand', 'zod', 'three', 'react-three', 'r3f', 'drei', '@react-three/fiber', '@react-three/drei'];\nfor (const lib of knownLibs) {\n  if (taskText.includes(lib)) {\n    const mapped = {'nextjs':'next','tailwind':'tailwindcss','react-three':'@react-three/fiber','r3f':'@react-three/fiber','drei':'@react-three/drei'};\n    libs.add(mapped[lib] || lib);\n  }\n}\nif (allFiles.some(f => f.path.match(/\\.(jsx|tsx)$/))) libs.add('react');\nconst skipList = new Set(['autoprefixer', 'postcss', 'typescript', 'vite', '@vitejs/plugin-react', '@types/react', '@types/node', '@types/react-dom', 'eslint', 'prettier']);\nconst toFetch = [...libs].filter(l => !skipList.has(l)).slice(0, 7);\n\nif (toFetch.length === 0) {\n  staticData._researchDocs = '';\n  return [{ json: $json }];\n}\n\n// \u2500\u2500\u2500 2. MCP helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction mcpRequest(sessionId, method, params) {\n  return new Promise((resolve, reject) => {\n    const body = JSON.stringify({ jsonrpc: '2.0', method, params, id: Date.now() });\n    const req = http.request({\n      hostname: 'docky', port: 8811, path: '/mcp', method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        'Accept': 'application/json, text/event-stream',\n        'Content-Length': Buffer.byteLength(body),\n        ...(sessionId ? { 'Mcp-Session-Id': sessionId } : {})\n      }\n    }, res => {\n      let data = '';\n      res.on('data', c => data += c);\n      res.on('end', () => resolve({ data, sessionId: res.headers['mcp-session-id'] }));\n    });\n    req.on('error', reject);\n    req.setTimeout(20000, () => { req.destroy(); reject(new Error('timeout')); });\n    req.write(body); req.end();\n  });\n}\n\n// Exa MCP helper (HTTPS to mcp.exa.ai)\nfunction exaRequest(sessionId, method, params) {\n  return new Promise((resolve, reject) => {\n    const body = JSON.stringify({ jsonrpc: '2.0', method, params, id: Date.now() });\n    const headers = {\n      'Content-Type': 'application/json',\n      'Accept': 'application/json, text/event-stream',\n      'Content-Length': Buffer.byteLength(body),\n    };\n    if (sessionId) headers['Mcp-Session-Id'] = sessionId;\n    const req = https.request({\n      hostname: 'mcp.exa.ai', port: 443, path: '/mcp', method: 'POST', headers\n    }, res => {\n      let data = '';\n      if (!sessionId && res.headers['mcp-session-id']) {\n        sessionId = res.headers['mcp-session-id'];\n      }\n      res.on('data', c => data += c);\n      res.on('end', () => resolve({ data, sessionId }));\n    });\n    req.on('error', reject);\n    req.setTimeout(30000, () => { req.destroy(); reject(new Error('exa timeout')); });\n    req.write(body); req.end();\n  });\n}\n\nfunction parseSSE(raw) {\n  for (const line of raw.split('\\n')) {\n    if (line.startsWith('data: ') || line.startsWith('data:')) {\n      const payload = line.startsWith('data: ') ? line.slice(6) : line.slice(5);\n      try { return JSON.parse(payload); } catch(e) {}\n    }\n  }\n  try { return JSON.parse(raw); } catch(e) {}\n  return {};\n}\n\nconst docs = [];\nconst errors = [];\nlet docSource = 'none';\n\n// \u2500\u2500\u2500 3. Try Context7 first \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ntry {\n  const init = await mcpRequest(null, 'initialize', {\n    protocolVersion: '2024-11-05', capabilities: {},\n    clientInfo: { name: 'eek-go-research', version: '1.0' }\n  });\n  const sid = init.sessionId;\n  if (!sid) throw new Error('No MCP session ID');\n\n  for (const lib of toFetch) {\n    try {\n      const resolve = await mcpRequest(sid, 'tools/call', {\n        name: 'resolve-library-id', arguments: { libraryName: lib }\n      });\n      const resolveData = parseSSE(resolve.data);\n      const resolveText = (resolveData.result?.content || []).map(c => c.text || '').join('');\n      if (resolveText.includes('Failed to retrieve')) { errors.push(lib + ': ctx7 failed'); continue; }\n      const idMatch = resolveText.match(/Context7-compatible library ID:\\s*(\\/[\\w.-]+\\/[\\w.-]+)/);\n      if (!idMatch) { errors.push(lib + ': no ID'); continue; }\n\n      const docsResp = await mcpRequest(sid, 'tools/call', {\n        name: 'get-library-docs',\n        arguments: { context7CompatibleLibraryID: idMatch[1], tokens: 5000, topic: 'setup components hooks API examples' }\n      });\n      const docsData = parseSSE(docsResp.data);\n      const docText = (docsData.result?.content || []).map(c => c.text || '').join('');\n      if (docText.length > 100 && !docText.includes('Failed to retrieve')) {\n        docs.push('## ' + lib + '\\n' + docText.substring(0, 8000));\n        docSource = 'context7';\n      }\n    } catch(e) { errors.push(lib + ': ' + e.message); }\n  }\n} catch(e) {\n  errors.push('ctx7 init: ' + e.message);\n}\n\n// \u2500\u2500\u2500 4. Exa fallback \u2014 if Context7 returned 0 docs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nif (docs.length === 0) {\n  try {\n    const exaInit = await exaRequest(null, 'initialize', {\n      protocolVersion: '2024-11-05', capabilities: {},\n      clientInfo: { name: 'eek-go-research', version: '1.0' }\n    });\n    const exaSid = exaInit.sessionId;\n\n    // Batch libraries into 2-3 targeted queries instead of one per lib\n    const mainLibs = toFetch.filter(l => !l.startsWith('@types'));\n    const queries = [];\n    // Group into max 3 queries\n    if (mainLibs.length <= 3) {\n      for (const lib of mainLibs) {\n        queries.push(lib + ' API documentation examples hooks components');\n      }\n    } else {\n      // Combine into fewer queries\n      const chunk1 = mainLibs.slice(0, 3).join(' ');\n      const chunk2 = mainLibs.slice(3).join(' ');\n      queries.push(chunk1 + ' API documentation examples');\n      if (chunk2) queries.push(chunk2 + ' API documentation examples');\n    }\n\n    for (const query of queries.slice(0, 3)) {\n      try {\n        const resp = await exaRequest(exaSid, 'tools/call', {\n          name: 'get_code_context_exa',\n          arguments: { query, num_results: 2 }\n        });\n        const respData = parseSSE(resp.data);\n        const text = (respData.result?.content || []).map(c => c.text || '').join('');\n        if (text.length > 200) {\n          // Trim to 6K chars per query to keep total context reasonable\n          docs.push('## Docs: ' + query.split(' API ')[0] + '\\n' + text.substring(0, 6000));\n          docSource = 'exa';\n        }\n      } catch(e) { errors.push('exa: ' + e.message); }\n    }\n  } catch(e) {\n    errors.push('exa init: ' + e.message);\n  }\n}\n\n// \u2500\u2500\u2500 5. Design guide (always included) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst designGuide = `## UI/UX Design Principles for Web Applications\n\n### Layout\n- Main interaction element must be centered and visually dominant (40-60% of viewport)\n- Use vertical single-column layouts for game/app UIs \u2014 never side-by-side grids unless it's a dashboard\n- Content hierarchy: hero/main action \u2192 stats/feedback \u2192 secondary actions (shop, settings)\n- Mobile-first: everything should work on a 375px wide screen and scale up\n- Scrollable secondary content (shops, lists) should never push the main interaction off-screen\n\n### Visual Feedback\n- Every user interaction (click, purchase, hover) MUST produce visible feedback\n- Number changes should animate (bounce, scale pulse, color flash)\n- Buttons: press animation (scale 0.95), hover glow/lift, disabled state with reduced opacity\n- Success actions: green flash, checkmark, particle burst\n- Use CSS transitions (200-300ms) on all interactive elements\n\n### Color & Contrast\n- Dark backgrounds with bright accent colors for maximum contrast\n- Use gradients over flat colors for depth (e.g., purple-900 to indigo-950)\n- Interactive elements should be the brightest items on screen\n- Text must have sufficient contrast \u2014 white/yellow on dark, with text-shadow for readability\n\n### Typography\n- Big, bold numbers for scores/stats (text-4xl to text-6xl)\n- Clear hierarchy: title (bold, large) \u2192 subtitle (medium) \u2192 body (regular, smaller)\n- Monospace or tabular numbers for counters that change frequently\n\n### Animation\n- Idle animations (slow pulse, float, rotate) make the UI feel alive\n- Click animations should be fast (100-200ms) and snappy\n- Spawn/death animations for appearing/disappearing elements\n- Use CSS will-change on animated elements for performance\n\n### Cards & Containers\n- Rounded corners (border-radius: 12-16px)\n- Subtle shadows for depth (shadow-lg, shadow-xl)\n- Semi-transparent backgrounds with backdrop-blur for overlay panels\n- Hover: lift (translateY -2 to -4px) + shadow increase`;\n\n// \u2500\u2500\u2500 6. Magic UI component examples \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet magicDocs = '';\ntry {\n  const uiKeywords = [];\n  const taskDescs = tasks.map(t => (t.description || '').toLowerCase()).join(' ');\n  const uiPatterns = ['button', 'card', 'shop', 'form', 'input', 'modal', 'dialog', 'nav', 'header', 'footer', 'sidebar', 'menu', 'table', 'list', 'grid', 'dashboard', 'counter', 'score', 'game', 'animation', 'toggle', 'dropdown'];\n  for (const p of uiPatterns) {\n    if (taskDescs.includes(p)) uiKeywords.push(p);\n  }\n  if (uiKeywords.length > 0) {\n    const magicInit = await new Promise((resolve, reject) => {\n      const body = JSON.stringify({ jsonrpc: '2.0', method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'eek-go-magic', version: '1.0' } }, id: Date.now() });\n      const req = http.request({\n        hostname: 'docky', port: 8811, path: '/mcp', method: 'POST',\n        headers: { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream', 'Content-Length': Buffer.byteLength(body) }\n      }, res => {\n        let data = ''; res.on('data', c => data += c);\n        res.on('end', () => resolve({ data, sessionId: res.headers['mcp-session-id'] }));\n      });\n      req.on('error', reject);\n      req.setTimeout(10000, () => { req.destroy(); reject(new Error('timeout')); });\n      req.write(body); req.end();\n    });\n    const magicSid = magicInit.sessionId;\n    if (magicSid) {\n      const searchTerms = uiKeywords.slice(0, 3).map(k => k + ' component');\n      for (const query of searchTerms) {\n        try {\n          const inspResp = await mcpRequest(magicSid, 'tools/call', {\n            name: '21st_magic_component_inspiration',\n            arguments: { message: 'I need a modern ' + query + ' for a web app', searchQuery: query }\n          });\n          const inspData = parseSSE(inspResp.data);\n          const inspText = (inspData.result?.content || []).map(c => c.text || '').join('');\n          if (inspText.length > 200) {\n            try {\n              const components = JSON.parse(inspText);\n              if (Array.isArray(components)) {\n                const examples = components.slice(0, 2).map(c => {\n                  const code = c.demoCode || c.code || '';\n                  const name = c.demoName || c.name || query;\n                  return '### ' + name + '\\n```tsx\\n' + code + '\\n```';\n                }).join('\\n\\n');\n                if (examples.length > 100) magicDocs += '\\n\\n## Magic UI: ' + query + '\\n' + examples;\n              }\n            } catch(e) {\n              if (inspText.length > 100) magicDocs += '\\n\\n## Magic UI: ' + query + '\\n' + inspText.substring(0, 4000);\n            }\n          }\n        } catch(e) {}\n      }\n    }\n  }\n} catch(e) {}\n\nif (magicDocs) docs.push('## UI Component Examples (from 21st.dev Magic)\\n' + magicDocs);\n\n// \u2500\u2500\u2500 7. Assemble final research docs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst sourceLabel = docSource === 'exa' ? 'Exa Web Search' : docSource === 'context7' ? 'Context7' : 'Design Guide Only';\nstaticData._researchDocs = docs.length > 0\n  ? designGuide + '\\n\\n---\\n\\n## Library Documentation (from ' + sourceLabel + ')\\n\\n' + docs.join('\\n\\n---\\n\\n')\n  : designGuide;\n\n// \u2500\u2500\u2500 8. Callback to Forge \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ntry {\n  const projectId = $('Extract Input').first().json.project_id || staticData._currentProjectId || 'unknown';\n  const cbBody = JSON.stringify({\n    event: 'research_complete', project_id: projectId,\n    data: { libraries_fetched: toFetch, doc_count: docs.length, doc_chars: docs.reduce((s,d) => s+d.length, 0), source: docSource, errors: errors.length ? errors : undefined }\n  });\n  const req = http.request({\n    hostname: 'forge', port: 3500, path: '/api/status-callback', method: 'POST',\n    headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Content-Length': Buffer.byteLength(cbBody) }\n  });\n  req.on('error', () => {});\n  req.write(cbBody); req.end();\n} catch(e) {}\n\nreturn [{ json: $json }];\n"
      }
    },
    {
      "id": "build-check",
      "name": "Build Check",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3950,
        300
      ],
      "parameters": {
        "jsCode": "const http = require('http');\n\n// \u2500\u2500\u2500 Playwright MCP helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Calls Playwright MCP tools through the docky gateway for full browser inspection\nconst MCP_URL_HOST = ($env.MCP_GATEWAY_URL || 'http://docky:8811/mcp').replace('http://', '').replace('/mcp', '');\nconst [mcpHost, mcpPortStr] = MCP_URL_HOST.split(':');\nconst mcpPort = parseInt(mcpPortStr) || 8811;\nlet _mcpSessionId = null;\n\nfunction mcpCall(method, params) {\n  return new Promise((resolve) => {\n    const body = JSON.stringify({ jsonrpc: '2.0', method, params, id: Date.now() });\n    const headers = {\n      'Content-Type': 'application/json',\n      'Accept': 'application/json, text/event-stream',\n      'Content-Length': Buffer.byteLength(body),\n    };\n    if (_mcpSessionId) headers['Mcp-Session-Id'] = _mcpSessionId;\n    const req = http.request({ hostname: mcpHost, port: mcpPort, path: '/mcp', method: 'POST', headers, timeout: 60000 }, (res) => {\n      if (!_mcpSessionId && res.headers['mcp-session-id']) _mcpSessionId = res.headers['mcp-session-id'];\n      let data = '';\n      res.on('data', c => data += c);\n      res.on('end', () => {\n        // Parse SSE format\n        for (const line of data.split('\\n')) {\n          if (line.startsWith('data:')) {\n            try { return resolve(JSON.parse(line.slice(5))); } catch {}\n          }\n        }\n        try { resolve(JSON.parse(data)); } catch { resolve({ error: data }); }\n      });\n    });\n    req.on('error', e => resolve({ error: e.message }));\n    req.on('timeout', () => { req.destroy(); resolve({ error: 'timeout' }); });\n    req.write(body);\n    req.end();\n  });\n}\n\nasync function mcpInit() {\n  await mcpCall('initialize', {\n    protocolVersion: '2024-11-05',\n    capabilities: {},\n    clientInfo: { name: 'eek-go-pipeline', version: '1.0' }\n  });\n}\n\nasync function browserTool(name, args) {\n  const resp = await mcpCall('tools/call', { name, arguments: args });\n  const content = resp?.result?.content || [];\n  return content.map(c => c.text || '').join('\\n');\n}\n\n// \u2500\u2500\u2500 Build Check: build + Playwright MCP inspection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst staticData = $getWorkflowStaticData('global');\nconst projectId = $('Extract Input').first().json.project_id || 'unknown';\n\nlet buildResult = { success: true, output: '' };\n\n// Step 1: Run vite build via file-api\ntry {\n  const result = await new Promise((resolve, reject) => {\n    const req = http.request({\n      hostname: 'file-api', port: 3456, method: 'POST',\n      path: `/projects/${projectId}/build-check`,\n      headers: { 'Authorization': 'Bearer ' + ($env.FILE_API_TOKEN || ''), 'Content-Type': 'application/json', 'Content-Length': 0 }\n    }, res => {\n      let data = '';\n      res.on('data', c => data += c);\n      res.on('end', () => { try { resolve(JSON.parse(data)); } catch { resolve({ success: false, error: data }); } });\n    });\n    req.on('error', e => resolve({ success: false, error: e.message }));\n    req.setTimeout(180000, () => { req.destroy(); resolve({ success: false, error: 'Build check timed out' }); });\n    req.end();\n  });\n  buildResult = result;\n} catch(e) {\n  buildResult = { success: false, error: e.message };\n}\n\nstaticData._buildResult = buildResult;\nstaticData._screenshot_b64 = null;\nstaticData._consoleErrors = [];\nstaticData._layoutInfo = null;\nstaticData._networkErrors = [];\n\n// Step 2: If build passed, start preview + full Playwright MCP inspection\nif (buildResult.success) {\n  try {\n    // Start preview server\n    await new Promise((resolve) => {\n      const req = http.request({\n        hostname: 'file-api', port: 3456, method: 'POST',\n        path: '/projects/' + projectId + '/preview/start',\n        headers: { 'Authorization': 'Bearer ' + ($env.FILE_API_TOKEN || ''), 'Content-Type': 'application/json', 'Content-Length': 0 }\n      }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => resolve(d)); });\n      req.on('error', () => resolve(''));\n      req.setTimeout(60000, () => { req.destroy(); resolve(''); });\n      req.end();\n    });\n\n    // Wait for dev server to be ready\n    await new Promise(r => setTimeout(r, 4000));\n\n    // Initialize Playwright MCP session\n    await mcpInit();\n\n    // Navigate to the app (mobile viewport)\n    await browserTool('browser_resize', { width: 390, height: 844 });\n    const navResult = await browserTool('browser_navigate', { url: 'http://10.0.0.100:4000' });\n\n    // Wait for app to render\n    await new Promise(r => setTimeout(r, 2000));\n\n    // \u2500\u2500\u2500 Multi-viewport screenshots (mobile, tablet, desktop) \u2500\u2500\u2500\n    const viewports = [\n      { name: 'mobile', width: 390, height: 844 },\n      { name: 'tablet', width: 768, height: 1024 },\n      { name: 'desktop', width: 1440, height: 900 }\n    ];\n    staticData._screenshots = {};\n    \n    for (const vp of viewports) {\n      try {\n        await browserTool('browser_resize', { width: vp.width, height: vp.height });\n        await new Promise(r => setTimeout(r, 500));\n        \n        // Fallback: use file-api scrape for reliable base64\n        const scrapeBody = JSON.stringify({ url: 'http://10.0.0.100:4000', viewport: { width: vp.width, height: vp.height } });\n        const scrapeResult = await new Promise((resolve) => {\n          const req = http.request({\n            hostname: 'file-api', port: 3456, method: 'POST', path: '/scrape',\n            headers: { 'Authorization': 'Bearer ' + ($env.FILE_API_TOKEN || ''), 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(scrapeBody) }\n          }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch { resolve({}); } }); });\n          req.on('error', () => resolve({}));\n          req.write(scrapeBody); req.end();\n        });\n        if (scrapeResult.screenshot_b64) {\n          staticData._screenshots[vp.name] = scrapeResult.screenshot_b64;\n        }\n      } catch(e) {}\n    }\n    // Primary screenshot for reviewer (mobile \u2014 the target viewport)\n    staticData._screenshot_b64 = staticData._screenshots.mobile || null;\n\n    // \u2500\u2500\u2500 Interaction testing \u2014 click the main button, verify state \u2500\u2500\u2500\n    try {\n      await browserTool('browser_resize', { width: 390, height: 844 });\n      await new Promise(r => setTimeout(r, 500));\n      \n      // Try clicking the most prominent button/interactive element\n      const snapshot = await browserTool('browser_snapshot', {});\n      const buttonMatch = snapshot.match(/button.*?\\[ref=(\\w+)\\].*?cursor=pointer/i);\n      if (buttonMatch) {\n        const beforeText = await browserTool('browser_evaluate', {\n          function: \"() => document.body.innerText.substring(0, 200)\"\n        });\n        await browserTool('browser_click', { ref: buttonMatch[1] });\n        await new Promise(r => setTimeout(r, 500));\n        const afterText = await browserTool('browser_evaluate', {\n          function: \"() => document.body.innerText.substring(0, 200)\"\n        });\n        staticData._interactionTest = {\n          clicked: buttonMatch[0].substring(0, 60),\n          stateChanged: beforeText !== afterText,\n          before: (beforeText || '').substring(0, 100),\n          after: (afterText || '').substring(0, 100)\n        };\n      }\n    } catch(e) {\n      staticData._interactionTest = { error: e.message };\n    }\n\n    // Get console errors\n    const consoleOutput = await browserTool('browser_console_messages', {});\n    const errorLines = consoleOutput.split('\\n').filter(l => l.includes('[ERROR]') || l.includes('[WARN]'));\n    staticData._consoleErrors = errorLines.slice(0, 20);\n\n    // Evaluate layout CSS \u2014 check key elements\n    const layoutResult = await browserTool('browser_evaluate', {\n      function: \"() => { try { var root = document.getElementById('root'); if (!root || !root.firstElementChild) return JSON.stringify({error: 'no root'}); var el = root.firstElementChild; var cs = getComputedStyle(el); var body = getComputedStyle(document.body); var children = Array.from(el.querySelectorAll('*')).slice(0,30).map(function(c){ var r = c.getBoundingClientRect(); var s = getComputedStyle(c); return { tag: c.tagName, class: (c.className||'').toString().substring(0,60), x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height), display: s.display, overflow: s.overflow !== 'visible' ? s.overflow : undefined }; }); return JSON.stringify({ viewport: {w: window.innerWidth, h: window.innerHeight}, root: {w: root.offsetWidth, h: root.offsetHeight, display: cs.display, flexDir: cs.flexDirection, maxW: cs.maxWidth, overflow: cs.overflow}, bodyBg: body.backgroundColor, elements: children }); } catch(e) { return JSON.stringify({error: e.message}); } }\"\n    });\n\n    // Parse layout result\n    try {\n      const layoutMatch = layoutResult.match(/### Result\\n\"(.+)\"/s);\n      if (layoutMatch) {\n        const unescaped = layoutMatch[1].replace(/\\\\\"/g, '\"').replace(/\\\\\\\\/g, '\\\\');\n        staticData._layoutInfo = JSON.parse(unescaped);\n      }\n    } catch {}\n\n    // Get network errors (failed requests, 404s)\n    const networkOutput = await browserTool('browser_network_requests', {});\n    const failedRequests = networkOutput.split('\\n').filter(l => l.includes('failed') || l.includes('404') || l.includes('ERR_'));\n    staticData._networkErrors = failedRequests.slice(0, 10);\n\n    // Get DOM snapshot for structural analysis\n    const snapshot = await browserTool('browser_snapshot', {});\n    staticData._domSnapshot = (snapshot || '').substring(0, 3000);\n\n    // Stop preview\n    await new Promise((resolve) => {\n      const req = http.request({\n        hostname: 'file-api', port: 3456, method: 'POST',\n        path: '/projects/' + projectId + '/preview/stop',\n        headers: { 'Authorization': 'Bearer ' + ($env.FILE_API_TOKEN || ''), 'Content-Length': 0 }\n      }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => resolve(d)); });\n      req.on('error', () => resolve(''));\n      req.end();\n    });\n\n  } catch(e) {\n    // Inspection failed \u2014 continue with whatever we got\n  }\n}\n\n// Send callback to Forge\ntry {\n  const cbBody = JSON.stringify({\n    event: buildResult.success ? 'build_check_passed' : 'build_check_failed',\n    project_id: projectId,\n    data: {\n      success: buildResult.success,\n      error: buildResult.error || null,\n      stage: buildResult.stage || null,\n      console_errors: (staticData._consoleErrors || []).length,\n      has_screenshot: !!staticData._screenshot_b64,\n      has_layout: !!staticData._layoutInfo,\n    }\n  });\n  const req = http.request({\n    hostname: 'forge', port: 3500, path: '/api/status-callback', method: 'POST',\n    headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Content-Length': Buffer.byteLength(cbBody) }\n  });\n  req.on('error', () => {});\n  req.write(cbBody); req.end();\n} catch {}\n\nreturn [{ json: $json }];\n"
      }
    },
    {
      "parameters": {
        "jsCode": "const http = require('http');\n\n// \u2500\u2500\u2500 Playwright MCP helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Calls Playwright MCP tools through the docky gateway for full browser inspection\nconst MCP_URL_HOST = ($env.MCP_GATEWAY_URL || 'http://docky:8811/mcp').replace('http://', '').replace('/mcp', '');\nconst [mcpHost, mcpPortStr] = MCP_URL_HOST.split(':');\nconst mcpPort = parseInt(mcpPortStr) || 8811;\nlet _mcpSessionId = null;\n\nfunction mcpCall(method, params) {\n  return new Promise((resolve) => {\n    const body = JSON.stringify({ jsonrpc: '2.0', method, params, id: Date.now() });\n    const headers = {\n      'Content-Type': 'application/json',\n      'Accept': 'application/json, text/event-stream',\n      'Content-Length': Buffer.byteLength(body),\n    };\n    if (_mcpSessionId) headers['Mcp-Session-Id'] = _mcpSessionId;\n    const req = http.request({ hostname: mcpHost, port: mcpPort, path: '/mcp', method: 'POST', headers, timeout: 60000 }, (res) => {\n      if (!_mcpSessionId && res.headers['mcp-session-id']) _mcpSessionId = res.headers['mcp-session-id'];\n      let data = '';\n      res.on('data', c => data += c);\n      res.on('end', () => {\n        // Parse SSE format\n        for (const line of data.split('\\n')) {\n          if (line.startsWith('data:')) {\n            try { return resolve(JSON.parse(line.slice(5))); } catch {}\n          }\n        }\n        try { resolve(JSON.parse(data)); } catch { resolve({ error: data }); }\n      });\n    });\n    req.on('error', e => resolve({ error: e.message }));\n    req.on('timeout', () => { req.destroy(); resolve({ error: 'timeout' }); });\n    req.write(body);\n    req.end();\n  });\n}\n\nasync function mcpInit() {\n  await mcpCall('initialize', {\n    protocolVersion: '2024-11-05',\n    capabilities: {},\n    clientInfo: { name: 'eek-go-pipeline', version: '1.0' }\n  });\n}\n\nasync function browserTool(name, args) {\n  const resp = await mcpCall('tools/call', { name, arguments: args });\n  const content = resp?.result?.content || [];\n  return content.map(c => c.text || '').join('\\n');\n}\n\n// \u2500\u2500\u2500 Post-Fix Build Check: rebuild + Playwright MCP re-inspection \u2500\u2500\u2500\u2500\nconst staticData = $getWorkflowStaticData('global');\nconst projectId = $('Extract Input').first().json.project_id || 'unknown';\nconst iteration = (staticData._visualFixIteration || 0);\n\n// Run build check\nconst buildResult = await new Promise((resolve) => {\n  const req = http.request({\n    hostname: 'file-api', port: 3456, method: 'POST',\n    path: `/projects/${projectId}/build-check`,\n    headers: { 'Authorization': 'Bearer ' + ($env.FILE_API_TOKEN || ''), 'Content-Type': 'application/json', 'Content-Length': 0 },\n    timeout: 180000\n  }, res => {\n    let data = '';\n    res.on('data', c => data += c);\n    res.on('end', () => { try { resolve(JSON.parse(data)); } catch { resolve({ success: false, error: 'parse error' }); } });\n  });\n  req.on('error', e => resolve({ success: false, error: e.message }));\n  req.end();\n});\n\nstaticData._buildResult = buildResult;\nstaticData._screenshot_b64 = null;\nstaticData._consoleErrors = [];\nstaticData._layoutInfo = null;\n\nif (buildResult.success) {\n  try {\n    // Start preview\n    await new Promise((resolve) => {\n      const req = http.request({\n        hostname: 'file-api', port: 3456, method: 'POST',\n        path: '/projects/' + projectId + '/preview/start',\n        headers: { 'Authorization': 'Bearer ' + ($env.FILE_API_TOKEN || ''), 'Content-Type': 'application/json', 'Content-Length': 0 }\n      }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => resolve(d)); });\n      req.on('error', () => resolve(''));\n      req.setTimeout(60000, () => { req.destroy(); resolve(''); });\n      req.end();\n    });\n\n    await new Promise(r => setTimeout(r, 4000));\n\n    // Playwright MCP inspection\n    await mcpInit();\n    await browserTool('browser_resize', { width: 390, height: 844 });\n    await browserTool('browser_navigate', { url: 'http://10.0.0.100:4000' });\n    await new Promise(r => setTimeout(r, 2000));\n\n    // Screenshot (fallback to file-api scrape)\n    const screenshotText = await browserTool('browser_take_screenshot', {});\n    const b64Match = screenshotText.match(/\\[data:image\\/[^;]+;base64,([^\\]]+)\\]/);\n    if (b64Match) {\n      staticData._screenshot_b64 = b64Match[1];\n    } else {\n      const scrapeBody = JSON.stringify({ url: 'http://10.0.0.100:4000', viewport: { width: 390, height: 844 } });\n      const scrapeResult = await new Promise((resolve) => {\n        const req = http.request({\n          hostname: 'file-api', port: 3456, method: 'POST', path: '/scrape',\n          headers: { 'Authorization': 'Bearer ' + ($env.FILE_API_TOKEN || ''), 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(scrapeBody) }\n        }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch { resolve({}); } }); });\n        req.on('error', () => resolve({}));\n        req.write(scrapeBody); req.end();\n      });\n      staticData._screenshot_b64 = scrapeResult.screenshot_b64 || null;\n    }\n\n    // Console errors\n    const consoleOutput = await browserTool('browser_console_messages', {});\n    staticData._consoleErrors = consoleOutput.split('\\n').filter(l => l.includes('[ERROR]') || l.includes('[WARN]')).slice(0, 20);\n\n    // Layout CSS check\n    const layoutResult = await browserTool('browser_evaluate', {\n      function: \"() => { try { var root = document.getElementById('root'); if (!root || !root.firstElementChild) return JSON.stringify({error: 'no root'}); var el = root.firstElementChild; var cs = getComputedStyle(el); var children = Array.from(el.querySelectorAll('*')).slice(0,30).map(function(c){ var r = c.getBoundingClientRect(); return { tag: c.tagName, class: (c.className||'').toString().substring(0,60), x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height), display: getComputedStyle(c).display }; }); return JSON.stringify({ viewport: {w: window.innerWidth, h: window.innerHeight}, root: {w: root.offsetWidth, h: root.offsetHeight, display: cs.display, maxW: cs.maxWidth}, elements: children }); } catch(e) { return JSON.stringify({error: e.message}); } }\"\n    });\n    try {\n      const layoutMatch = layoutResult.match(/### Result\\n\"(.+)\"/s);\n      if (layoutMatch) {\n        staticData._layoutInfo = JSON.parse(layoutMatch[1].replace(/\\\\\"/g, '\"').replace(/\\\\\\\\/g, '\\\\'));\n      }\n    } catch {}\n\n    // Stop preview\n    await new Promise((resolve) => {\n      const req = http.request({\n        hostname: 'file-api', port: 3456, method: 'POST',\n        path: '/projects/' + projectId + '/preview/stop',\n        headers: { 'Authorization': 'Bearer ' + ($env.FILE_API_TOKEN || ''), 'Content-Length': 0 }\n      }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => resolve(d)); });\n      req.on('error', () => resolve(''));\n      req.end();\n    });\n  } catch(e) { /* inspection failed, continue */ }\n}\n\nreturn [{ json: {\n  project_id: projectId,\n  build_success: buildResult.success,\n  has_screenshot: !!staticData._screenshot_b64,\n  iteration: iteration,\n  build_error: buildResult.error || null,\n  console_errors: (staticData._consoleErrors || []).length,\n  has_layout: !!staticData._layoutInfo,\n} }];\n"
      },
      "id": "post-fix-build-check",
      "name": "Post-Fix Build Check",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3150,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// Visual Fix Gate: if we have a screenshot and iteration < 2, check visual quality\n// If the build failed or looks wrong, loop back to the fixer\nconst staticData = $getWorkflowStaticData('global');\nconst iteration = staticData._visualFixIteration || 0;\nconst buildSuccess = $json.build_success;\nconst hasScreenshot = $json.has_screenshot;\nconst projectId = $json.project_id;\nconst MAX_VISUAL_ITERATIONS = 2;\n\n// If build failed and we haven't exceeded max iterations, try to fix again\nif (!buildSuccess && iteration < MAX_VISUAL_ITERATIONS) {\n  staticData._visualFixIteration = iteration + 1;\n  // Create a synthetic review result pointing at the build error\n  return [{\n    json: {\n      _loopBack: true,\n      project_id: projectId,\n      fixes_needed: [{\n        file: 'build',\n        severity: 'critical',\n        issue: 'Build failed after fix: ' + ($json.build_error || 'unknown'),\n        fix_instruction: 'Fix the build error. Check all imports, exports, and file extensions.',\n        related_files: []\n      }],\n      visual_issues: [],\n      critical_fix_count: 1\n    }\n  }];\n}\n\n// If we have a screenshot and haven't maxed iterations, the fixer already saw it\n// (the next fix pass will include the updated screenshot automatically)\n// For now, continue to final review\nstaticData._visualFixIteration = 0; // reset for next pipeline run\nreturn [{\n  json: {\n    _loopBack: false,\n    project_id: projectId\n  }\n}];"
      },
      "id": "visual-fix-gate",
      "name": "Visual Fix Gate",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3350,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// Continue to CB: Fix Applied (only when NOT looping back)\nif ($json._loopBack) return [];\nreturn [{ json: $json }];"
      },
      "id": "visual-continue-gate",
      "name": "Visual: Continue",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3550,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// Loop back to fixer (only when looping)\nif (!$json._loopBack) return [];\n// Re-fetch all files for the fixer\nconst http = require('http');\nconst projectId = $json.project_id;\nconst allFiles = await new Promise((resolve) => {\n  const req = http.request({\n    hostname: 'file-api', port: 3456, method: 'GET',\n    path: '/projects/' + projectId + '/files',\n    headers: { 'Authorization': 'Bearer ' + ($env.FILE_API_TOKEN || '') },\n    timeout: 30000\n  }, res => {\n    let data = '';\n    res.on('data', c => data += c);\n    res.on('end', () => { try { resolve(JSON.parse(data).files || []); } catch { resolve([]); } });\n  });\n  req.on('error', () => resolve([]));\n  req.end();\n});\n\nconst staticData = $getWorkflowStaticData('global');\nstaticData._allFileContents = allFiles;\n\nreturn [{ json: $json }];"
      },
      "id": "visual-loop-gate",
      "name": "Visual: Loop Back",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3550,
        500
      ]
    },
    {
      "parameters": {
        "jsCode": "const http = require('http');\nconst staticData = $getWorkflowStaticData('global');\nconst projectId = $('Extract Input').first().json.project_id || 'unknown';\n\n// Skip if build failed\nif (!staticData._buildResult?.success) {\n  staticData._playtestReport = 'Build failed \u2014 skipping playtest.';\n  return [{ json: $json }];\n}\n\n// \u2500\u2500\u2500 Playwright MCP helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst MCP_HOST = ($env.MCP_GATEWAY_URL || 'http://docky:8811/mcp').replace('http://', '').replace('/mcp', '');\nconst [mcpHost, mcpPort] = MCP_HOST.split(':');\nlet _sid = null;\n\nfunction mcpCall(method, params) {\n  return new Promise((resolve) => {\n    const body = JSON.stringify({ jsonrpc: '2.0', method, params, id: Date.now() });\n    const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream', 'Content-Length': Buffer.byteLength(body) };\n    if (_sid) headers['Mcp-Session-Id'] = _sid;\n    const req = http.request({ hostname: mcpHost, port: parseInt(mcpPort) || 8811, path: '/mcp', method: 'POST', headers, timeout: 30000 }, (res) => {\n      if (!_sid && res.headers['mcp-session-id']) _sid = res.headers['mcp-session-id'];\n      let data = '';\n      res.on('data', c => data += c);\n      res.on('end', () => {\n        for (const line of data.split('\\n')) {\n          if (line.startsWith('data:')) {\n            try { return resolve(JSON.parse(line.slice(5).trim())); } catch {}\n          }\n        }\n        try { resolve(JSON.parse(data)); } catch { resolve({ error: data }); }\n      });\n    });\n    req.on('error', e => resolve({ error: e.message }));\n    req.on('timeout', () => { req.destroy(); resolve({ error: 'timeout' }); });\n    req.write(body); req.end();\n  });\n}\n\nasync function browserTool(name, args) {\n  const resp = await mcpCall('tools/call', { name, arguments: args || {} });\n  return (resp?.result?.content || []).map(c => c.text || '').join('\\n');\n}\n\n// \u2500\u2500\u2500 Start preview server \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nawait new Promise((resolve) => {\n  const req = http.request({\n    hostname: 'file-api', port: 3456, method: 'POST',\n    path: '/projects/' + projectId + '/preview/start',\n    headers: { 'Authorization': 'Bearer ' + ($env.FILE_API_TOKEN || ''), 'Content-Type': 'application/json', 'Content-Length': 0 }\n  }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => resolve(d)); });\n  req.on('error', () => resolve(''));\n  req.setTimeout(60000, () => { req.destroy(); resolve(''); });\n  req.end();\n});\nawait new Promise(r => setTimeout(r, 4000));\n\n// \u2500\u2500\u2500 Initialize Playwright MCP \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nawait mcpCall('initialize', {\n  protocolVersion: '2024-11-05', capabilities: {},\n  clientInfo: { name: 'eek-go-playtest', version: '1.0' }\n});\n\n// \u2500\u2500\u2500 PLAYTEST SESSION \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst screenshots = [];\nconst observations = [];\n\ntry {\n  // Step 1: Open the game at mobile viewport\n  await browserTool('browser_resize', { width: 390, height: 844 });\n  await browserTool('browser_navigate', { url: 'http://10.0.0.100:4000' });\n  await new Promise(r => setTimeout(r, 2000));\n\n  // Screenshot: Initial state\n  const scrape1 = await new Promise((resolve) => {\n    const body = JSON.stringify({ url: 'http://10.0.0.100:4000', viewport: { width: 390, height: 844 } });\n    const req = http.request({\n      hostname: 'file-api', port: 3456, method: 'POST', path: '/scrape',\n      headers: { 'Authorization': 'Bearer ' + ($env.FILE_API_TOKEN || ''), 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }\n    }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch { resolve({}); } }); });\n    req.on('error', () => resolve({}));\n    req.write(body); req.end();\n  });\n  if (scrape1.screenshot_b64) screenshots.push({ label: 'initial_state', b64: scrape1.screenshot_b64 });\n\n  // Get DOM snapshot to find interactive elements\n  const snapshot = await browserTool('browser_snapshot', {});\n  observations.push('DOM: ' + snapshot.substring(0, 1500));\n\n  // Get console errors\n  const consoleOutput = await browserTool('browser_console_messages', {});\n  const errors = consoleOutput.split('\\n').filter(l => l.includes('[ERROR]'));\n  if (errors.length > 0) observations.push('CONSOLE ERRORS: ' + errors.slice(0, 5).join('; '));\n\n  // Step 2: Click the main game button 5 times\n  const buttonRef = snapshot.match(/button[^[]*\\[ref=(\\w+)\\].*?cursor=pointer/i);\n  if (buttonRef) {\n    // Read counter before clicks\n    const before = await browserTool('browser_evaluate', {\n      function: \"() => { try { return document.body.innerText.match(/\\\\d+/)?.[0] || '0'; } catch { return '?'; } }\"\n    });\n\n    for (let i = 0; i < 5; i++) {\n      await browserTool('browser_click', { ref: buttonRef[1] });\n      await new Promise(r => setTimeout(r, 200));\n    }\n    await new Promise(r => setTimeout(r, 500));\n\n    // Read counter after clicks\n    const after = await browserTool('browser_evaluate', {\n      function: \"() => { try { return document.body.innerText.match(/\\\\d+/)?.[0] || '0'; } catch { return '?'; } }\"\n    });\n    observations.push('CLICK TEST: Counter before=' + before + ' after=' + after + ' (clicked 5 times)');\n\n    // Screenshot: After clicking\n    const scrape2 = await new Promise((resolve) => {\n      const body = JSON.stringify({ url: 'http://10.0.0.100:4000', viewport: { width: 390, height: 844 } });\n      const req = http.request({\n        hostname: 'file-api', port: 3456, method: 'POST', path: '/scrape',\n        headers: { 'Authorization': 'Bearer ' + ($env.FILE_API_TOKEN || ''), 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }\n      }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch { resolve({}); } }); });\n      req.on('error', () => resolve({}));\n      req.write(body); req.end();\n    });\n    if (scrape2.screenshot_b64) screenshots.push({ label: 'after_clicking', b64: scrape2.screenshot_b64 });\n  } else {\n    observations.push('CLICK TEST: Could not find main clickable button in DOM');\n  }\n\n  // Step 3: Try to find and click an upgrade/shop button\n  const shopButton = snapshot.match(/button[^[]*Upgrade[^[]*\\[ref=(\\w+)\\]/i) || snapshot.match(/Upgrade Shop[^[]*\\[ref=(\\w+)\\]/i);\n  if (shopButton) {\n    await browserTool('browser_click', { ref: shopButton[1] });\n    await new Promise(r => setTimeout(r, 1000));\n\n    // Screenshot: Shop open\n    const scrape3 = await new Promise((resolve) => {\n      const body = JSON.stringify({ url: 'http://10.0.0.100:4000', viewport: { width: 390, height: 844 } });\n      const req = http.request({\n        hostname: 'file-api', port: 3456, method: 'POST', path: '/scrape',\n        headers: { 'Authorization': 'Bearer ' + ($env.FILE_API_TOKEN || ''), 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }\n      }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch { resolve({}); } }); });\n      req.on('error', () => resolve({}));\n      req.write(body); req.end();\n    });\n    if (scrape3.screenshot_b64) screenshots.push({ label: 'shop_open', b64: scrape3.screenshot_b64 });\n\n    // Try to buy something\n    const buySnapshot = await browserTool('browser_snapshot', {});\n    const buyButton = buySnapshot.match(/button[^[]*(?:Buy|purchase)[^[]*\\[ref=(\\w+)\\]/i);\n    if (buyButton) {\n      await browserTool('browser_click', { ref: buyButton[1] });\n      await new Promise(r => setTimeout(r, 500));\n      observations.push('PURCHASE TEST: Clicked buy button');\n    }\n  }\n\n  // Step 4: Wait for idle income\n  await new Promise(r => setTimeout(r, 3000));\n  const afterIdle = await browserTool('browser_evaluate', {\n    function: \"() => { try { return document.body.innerText.match(/\\\\d+/)?.[0] || '0'; } catch { return '?'; } }\"\n  });\n  observations.push('IDLE TEST: Counter after 3s idle=' + afterIdle);\n\n  // Final screenshot\n  const scrapeFinal = await new Promise((resolve) => {\n    const body = JSON.stringify({ url: 'http://10.0.0.100:4000', viewport: { width: 390, height: 844 } });\n    const req = http.request({\n      hostname: 'file-api', port: 3456, method: 'POST', path: '/scrape',\n      headers: { 'Authorization': 'Bearer ' + ($env.FILE_API_TOKEN || ''), 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }\n    }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch { resolve({}); } }); });\n    req.on('error', () => resolve({}));\n    req.write(body); req.end();\n  });\n  if (scrapeFinal.screenshot_b64) screenshots.push({ label: 'final_state', b64: scrapeFinal.screenshot_b64 });\n\n} catch(e) {\n  observations.push('PLAYTEST ERROR: ' + e.message);\n}\n\n// Stop preview\ntry {\n  await new Promise((resolve) => {\n    const req = http.request({\n      hostname: 'file-api', port: 3456, method: 'POST',\n      path: '/projects/' + projectId + '/preview/stop',\n      headers: { 'Authorization': 'Bearer ' + ($env.FILE_API_TOKEN || ''), 'Content-Length': 0 }\n    }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => resolve(d)); });\n    req.on('error', () => resolve(''));\n    req.end();\n  });\n} catch {}\n\n// Store playtest data for the reviewer\nstaticData._playtestScreenshots = screenshots;\nstaticData._playtestObservations = observations;\nstaticData._playtestReport = observations.join('\\n');\n\n// Callback to Forge\ntry {\n  const cbBody = JSON.stringify({\n    event: 'playtest_complete',\n    project_id: projectId,\n    data: { screenshots: screenshots.length, observations: observations.length }\n  });\n  const req = http.request({\n    hostname: 'forge', port: 3500, path: '/api/status-callback',\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(cbBody) }\n  });\n  req.on('error', () => {});\n  req.write(cbBody); req.end();\n} catch {}\n\nreturn [{ json: $json }];\n"
      },
      "id": "playtest-review",
      "name": "Playtest: Play Game",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2100,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// Generate Stitch concept UI for each task using the \"Designer Flow\"\n// Step 1: Generate first screen \u2192 extract design context\n// Step 2: Use that context for all subsequent screens (consistent design system)\nconst https = require('https');\nconst staticData = $getWorkflowStaticData('global');\nconst tasks = $json.tasks || [];\nconst STITCH_API_KEY = $env.STITCH_API_KEY || '';\n\n// Only generate concepts for tasks that need them (planner decides per-task)\nconst tasksNeedingConcepts = tasks.filter(t => t.needs_concept === true);\n\nif (!STITCH_API_KEY || tasksNeedingConcepts.length === 0) {\n  staticData._taskConcepts = {};\n  return [{ json: $json }];\n}\n\nfunction stitchCall(params) {\n  return new Promise((resolve) => {\n    const body = JSON.stringify({ jsonrpc: '2.0', method: 'tools/call', id: Date.now(), params });\n    const req = https.request({\n      hostname: 'stitch.googleapis.com', port: 443, path: '/mcp', method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        'Accept': 'application/json, text/event-stream',\n        'X-Goog-Api-Key': STITCH_API_KEY,\n        'Content-Length': Buffer.byteLength(body)\n      },\n      timeout: 120000\n    }, (res) => {\n      let data = '';\n      res.on('data', c => data += c);\n      res.on('end', () => { try { resolve(JSON.parse(data)); } catch { resolve({ error: data }); } });\n    });\n    req.on('error', e => resolve({ error: e.message }));\n    req.on('timeout', () => { req.destroy(); resolve({ error: 'timeout' }); });\n    req.write(body); req.end();\n  });\n}\n\nfunction downloadImage(url) {\n  return new Promise((resolve) => {\n    https.get(url + '=s1024', { timeout: 30000 }, (res) => {\n      const chunks = [];\n      res.on('data', c => chunks.push(c));\n      res.on('end', () => resolve(Buffer.concat(chunks).toString('base64')));\n    }).on('error', () => resolve(null));\n  });\n}\n\nconst concepts = {};\nconst projectId = $('Extract Input').first().json.project_id || 'unknown';\nconst projectGoal = staticData._currentProjectGoal || $('Extract Input').first().json.message || '';\n\n// Step 1: Create Stitch project\nlet stitchProjectId = null;\ntry {\n  const createRes = await stitchCall({\n    name: 'create_project', arguments: { title: projectId + '-v' + Date.now() }\n  });\n  const createText = createRes?.result?.content?.[0]?.text || '';\n  const createInfo = JSON.parse(createText);\n  stitchProjectId = createInfo.name?.replace('projects/', '');\n} catch(e) {}\n\nif (!stitchProjectId) {\n  staticData._taskConcepts = {};\n  return [{ json: $json }];\n}\n\n// Step 2: Designer Flow \u2014 generate first task screen, then extract context for consistency\nlet designContext = null;\nconst tasksToGenerate = tasksNeedingConcepts.slice(0, 3); // Max 3 to avoid rate limits\n\nfor (let i = 0; i < tasksToGenerate.length; i++) {\n  const task = tasksToGenerate[i];\n  try {\n    // Transform technical task description into a visual design prompt for Stitch\n    const rawDesc = (task.description || '').substring(0, 400);\n    // Strip code blocks, technical details, import statements\n    const visualDesc = rawDesc\n      .replace(/```[\\s\\S]*?```/g, '')\n      .replace(/import\\s+\\{[^}]+\\}[^;]+;/g, '')\n      .replace(/const\\s+\\[.*\\].*useState/g, '')\n      .replace(/\\$\\{[^}]+\\}/g, '')\n      .replace(/`[^`]+`/g, '')\n      .replace(/\\n{2,}/g, '. ')\n      .trim();\n    const taskDesc = visualDesc.substring(0, 300);\n    let prompt;\n    \n    if (i === 0) {\n      // First task: generate with project context for the design system\n      prompt = 'Design a polished mobile game UI with dark purple theme and neon accents. The game is: ' + projectGoal.substring(0, 150) + '. This screen shows: ' + taskDesc;\n    } else if (designContext) {\n      // Subsequent tasks: use extracted design context for consistency\n      prompt = 'Using this existing design style: ' + designContext.substring(0, 200) + '. Design a new screen showing: ' + taskDesc + '. Match the visual style, colors, and layout patterns.';\n    } else {\n      prompt = 'Design a polished mobile game UI screen showing: ' + taskDesc + '. Use dark purple gradients, neon accents, rounded cards, and modern game aesthetics.';\n    }\n    \n    const genRes = await stitchCall({\n      name: 'generate_screen_from_text',\n      arguments: {\n        projectId: stitchProjectId,\n        prompt: prompt,\n        deviceType: 'MOBILE',\n        modelId: 'GEMINI_3_FLASH',\n      }\n    });\n    \n    const genText = genRes?.result?.content?.[0]?.text || '';\n    const genInfo = JSON.parse(genText);\n    const screens = genInfo.outputComponents?.[0]?.design?.screens || [];\n    const screenshotUrl = screens[0]?.screenshot?.downloadUrl;\n    \n    if (screenshotUrl) {\n      const b64 = await downloadImage(screenshotUrl);\n      if (b64 && b64.length > 100) {\n        concepts[task.task_id] = b64;\n      }\n    }\n    \n    // After first screen: extract design context for consistency\n    if (i === 0 && screens.length > 0) {\n      try {\n        const contextRes = await stitchCall({\n          name: 'get_design_context',\n          arguments: {\n            projectId: stitchProjectId,\n            screenName: screens[0]?.name || 'Screen 1'\n          }\n        });\n        const contextText = contextRes?.result?.content?.[0]?.text || '';\n        if (contextText.length > 50) {\n          designContext = contextText.substring(0, 500);\n        }\n      } catch(e) {\n        // Design context extraction failed \u2014 continue without it\n      }\n    }\n  } catch(e) {\n    // Skip this task's concept\n  }\n}\n\nstaticData._taskConcepts = concepts;\n\n// Callback\ntry {\n  const http = require('http');\n  const count = Object.keys(concepts).length;\n  const cbBody = JSON.stringify({\n    event: 'stitch_complete',\n    project_id: projectId,\n    data: { message: '\ud83c\udfa8 Generated ' + count + ' task concepts' + (designContext ? ' (consistent design system)' : ''), count }\n  });\n  const req = http.request({\n    hostname: 'forge', port: 3500, path: '/api/status-callback', method: 'POST',\n    headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(cbBody) }\n  });\n  req.on('error', () => {});\n  req.write(cbBody); req.end();\n} catch {}\n\nreturn [{ json: $json }];\n"
      },
      "id": "generate-task-concepts",
      "name": "Generate Task Concepts",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1600,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// Pass to fixer if fixes needed, empty otherwise\nif (!$json._needsFix) return [];\nreturn [{ json: $json }];"
      },
      "id": "p3-fix-gate",
      "name": "P3: Fix Gate",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2500,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// Pass to final review if NO fixes needed, empty otherwise\nif ($json._needsFix) return [];\nreturn [{ json: $json }];"
      },
      "id": "p3-skip-gate",
      "name": "P3: Skip Gate",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2500,
        500
      ]
    },
    {
      "parameters": {
        "jsCode": "// Pipeline Run Report \u2014 structured summary of the entire execution\nconst staticData = $getWorkflowStaticData('global');\nconst http = require('http');\n\nconst projectId = $('Extract Input').first().json.project_id || 'unknown';\nconst message = $('Extract Input').first().json.message || '';\n\n// Collect timing and results from each phase\nconst report = {\n  project_id: projectId,\n  timestamp: new Date().toISOString(),\n  prompt: message.substring(0, 200),\n  phases: []\n};\n\n// Helper to safely get node data\nconst getNodeData = (nodeName) => {\n  try { return $(nodeName).first().json; } catch { return null; }\n};\n\n// Planner\nconst plannerData = getNodeData('Planner: Parse Response');\nif (plannerData) {\n  const tasks = plannerData.tasks || [];\n  report.phases.push({\n    name: 'PLANNER',\n    status: tasks.length > 0 ? 'OK' : 'EMPTY',\n    detail: `${tasks.length} tasks: ${tasks.map(t => t.task_id).join(', ')}`,\n    files: tasks.flatMap(t => t.files || [])\n  });\n}\n\n// Research\nreport.phases.push({\n  name: 'RESEARCH',\n  status: (staticData._researchDocs || '').length > 100 ? 'OK' : 'EMPTY',\n  detail: `${((staticData._researchDocs || '').length / 1000).toFixed(1)}K chars`\n});\n\n// Stitch\nconst concepts = staticData._taskConcepts || {};\nconst conceptCount = Object.keys(concepts).length;\nreport.phases.push({\n  name: 'STITCH',\n  status: conceptCount > 0 ? 'OK' : 'SKIPPED',\n  detail: `${conceptCount} concepts generated`\n});\n\n// Coder \u2014 check results\nconst p2Results = staticData.p2Results || [];\nreport.phases.push({\n  name: 'CODER',\n  status: p2Results.length > 0 ? 'OK' : 'EMPTY',\n  detail: p2Results.map(r => `${r.task_id}: ${(r.files_written || []).length} files`).join(', ')\n});\n\n// Build Check\nconst buildResult = staticData._buildResult || {};\nreport.phases.push({\n  name: 'BUILD',\n  status: buildResult.success ? 'PASS' : 'FAIL',\n  detail: buildResult.success ? 'Build passed' : (buildResult.error || 'Unknown error').substring(0, 200)\n});\n\n// Reviewer\nconst reviewResult = staticData._reviewResult || {};\nconst reviewQuality = reviewResult.overall_quality || 0;\nconst fixesNeeded = (reviewResult.fixes_needed || []).length;\nreport.phases.push({\n  name: 'REVIEWER',\n  status: reviewQuality > 0 ? 'OK' : 'PARSE_FAILED',\n  detail: `quality=${reviewQuality}, fixes=${fixesNeeded}`,\n  fixes: (reviewResult.fixes_needed || []).map(f => `[${f.severity}] ${f.file}: ${(f.issue || f.problem || '').substring(0, 60)}`)\n});\n\n// Fixer\nconst fixResults = staticData._fixResults || [];\nreport.phases.push({\n  name: 'FIXER',\n  status: fixResults.length > 0 ? 'OK' : 'SKIPPED',\n  detail: fixResults.length > 0 ? `Fixed: ${fixResults.join(', ')}` : 'No fixes applied'\n});\n\n// Final Review\nconst finalResult = $json;\nconst finalQuality = finalResult.final_quality || finalResult.quality || 0;\nreport.phases.push({\n  name: 'FINAL_REVIEW',\n  status: 'OK',\n  detail: `quality=${finalQuality}`\n});\n\n// \u2550\u2550\u2550 ISSUE DETECTION (deterministic checks) \u2550\u2550\u2550\nconst issues = [];\n\n// Check: build failed but high score\nif (!buildResult.success && finalQuality > 50) {\n  issues.push(`BUG: Final review scored ${finalQuality} despite build failure`);\n}\n\n// Check: reviewer parse failed\nif (reviewQuality === 0 && fixesNeeded === 0) {\n  issues.push('Reviewer produced no valid JSON \u2014 parse failed');\n}\n\n// Check: fixer ran but produced no files\nif (fixResults.length === 0 && fixesNeeded > 0) {\n  issues.push(`Fixer should have fixed ${fixesNeeded} issues but produced 0 files`);\n}\n\n// Check: coder produced empty files\nif (p2Results.some(r => (r.files_written || []).length === 0)) {\n  issues.push('Coder produced 0 files for one or more tasks');\n}\n\n// Check: @tailwind directives\nconst allFiles = staticData._allFileContents || [];\nconst cssFiles = allFiles.filter(f => f.path.endsWith('.css'));\nfor (const css of cssFiles) {\n  if (css.content && !css.content.includes('@tailwind') && css.path.includes('index')) {\n    issues.push(`@tailwind directives missing from ${css.path}`);\n  }\n}\n\n// Check: phantom files created\nif (allFiles.some(f => f.path === 'src/App.tsx') && !allFiles.some(f => f.path === 'src/main.tsx' && (f.content || '').includes('App'))) {\n  // App.tsx exists but main.tsx doesn't import it\n}\n\nreport.issues = issues;\nreport.final_quality = finalQuality;\nreport.build_passed = !!buildResult.success;\n\n// \u2550\u2550\u2550 FORMAT REPORT \u2550\u2550\u2550\nlet reportText = `\\n${'\u2550'.repeat(50)}\\n`;\nreportText += `RUN | ${projectId} | ${new Date().toLocaleTimeString()}\\n`;\nreportText += `${'\u2550'.repeat(50)}\\n`;\n\nfor (const phase of report.phases) {\n  const icon = phase.status === 'OK' || phase.status === 'PASS' ? '\u2713' : phase.status === 'SKIPPED' ? '\u25cb' : '\u2717';\n  reportText += `${icon} ${phase.name.padEnd(14)} ${phase.detail}\\n`;\n  if (phase.fixes) {\n    for (const fix of phase.fixes) {\n      reportText += `    ${fix}\\n`;\n    }\n  }\n}\n\nif (issues.length > 0) {\n  reportText += `${'\u2500'.repeat(50)}\\n`;\n  reportText += `ISSUES DETECTED:\\n`;\n  for (const issue of issues) {\n    reportText += `  \u26a0 ${issue}\\n`;\n  }\n}\nreportText += `${'\u2550'.repeat(50)}\\n`;\n\n// Save report via callback to Forge\ntry {\n  const cbBody = JSON.stringify({\n    event: 'pipeline_report',\n    project_id: projectId,\n    data: { report: reportText, structured: report }\n  });\n  const req = http.request({\n    hostname: 'forge', port: 3500, path: '/api/status-callback', method: 'POST',\n    headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(cbBody) }\n  });\n  req.on('error', () => {});\n  req.write(cbBody); req.end();\n} catch {}\n\n// Also log to console for n8n execution data\nconsole.log(reportText);\n\n// Store in staticData for the complete callback\nstaticData._pipelineReport = report;\nstaticData._pipelineReportText = reportText;\n\nreturn [{ json: { ...($json), _report: reportText } }];\n"
      },
      "id": "pipeline-report",
      "name": "Pipeline Report",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4200,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// P3: Code Review \u2014 reads file contents, judges code quality (no images)\nconst staticData = $getWorkflowStaticData('global');\nconst allFiles = $getWorkflowStaticData('global')._codeReviewFiles || [];\nconst buildResult = $getWorkflowStaticData('global')._codeReviewBuildResult || {};\n\n// Get the visual review result from the previous LLM call\nconst visualReviewRaw = $json.choices?.[0]?.message || {};\nconst visualContent = visualReviewRaw.content || '';\nconst visualReasoning = visualReviewRaw.reasoning_content || '';\n// Content has the JSON answer, reasoning has thinking text \u2014 prefer content if it has JSON\nconst visualReview = (visualContent && visualContent.includes('overall_quality')) ? visualContent : visualReasoning;\n\n// Store visual review for later merge\nstaticData._visualReview = visualReview;\n\n// Build code review prompt \u2014 file contents only, no images\nconst buildSection = buildResult.success\n  ? 'BUILD STATUS: PASSED'\n  : 'BUILD STATUS: FAILED \u2014 ' + (buildResult.error || 'unknown').substring(0, 300);\n\nconst fileSummaries = allFiles\n  .filter(f => f.path.match(/\\.(ts|tsx|js|jsx|json|css|html)$/) && !f.path.includes('node_modules'))\n  .filter(f => !f.path.startsWith('references/'))\n  .map(f => '### ' + f.path + '\\n```\\n' + (f.content || '') + '\\n```')\n  .join('\\n\\n')\n  .substring(0, 80000);\n\nconst prompt = `You are a Senior Code Reviewer. Review this project's code for quality issues.\n\n${buildSection}\n\nPROJECT FILES:\n${fileSummaries}\n\nReturn ONLY valid JSON (no markdown, start with {):\n{\n  \"code_quality\": number (0-100),\n  \"issues\": [\n    {\n      \"file\": \"path/to/file\",\n      \"severity\": \"critical|high|medium\",\n      \"issue\": \"description\",\n      \"problem\": \"what's wrong and its impact\"\n    }\n  ]\n}\n\nFocus on: import/export mismatches, missing error handling, runtime crashes, type errors, broken API calls, state management bugs, performance issues. Max 5 issues. Only flag REAL problems, not style preferences.`;\n\nreturn [{ json: {\n  model: $env.REVIEWER_MODEL || 'qwen3.5-27b@q4_k_m',\n  messages: [{ role: 'user', content: prompt }],\n  temperature: 0.7,\n  top_p: 0.8,\n  top_k: 20,\n  presence_penalty: 1.5,\n  max_tokens: 4096\n} }];\n"
      },
      "id": "p3-code-review-build",
      "name": "P3: Code Review Build",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2800,
        300
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.LM_STUDIO_URL || 'http://10.0.0.100:1234/v1/chat/completions' }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + ($env.LLM_API_KEY || '') }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json) }}",
        "options": {
          "timeout": 600000
        }
      },
      "id": "p3-code-review-llm",
      "name": "P3: Code Review LLM",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3000,
        300
      ]
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Extract Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Input": {
      "main": [
        [
          {
            "node": "CB: Pipeline Started",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Init Memory": {
      "main": [
        [
          {
            "node": "Fetch Project Files",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Project Files": {
      "main": [
        [
          {
            "node": "Prepare Planner Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Planner Input": {
      "main": [
        [
          {
            "node": "P0: Has Reference URL?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "P0: Has Reference URL?": {
      "main": [
        [
          {
            "node": "P0: Scrape URL",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Startup: Unload All Models",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "P0: Scrape URL": {
      "main": [
        [
          {
            "node": "P0: Merge Scrape Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "P0: Merge Scrape Data": {
      "main": [
        [
          {
            "node": "Startup: Unload All Models",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load Planner Model": {
      "main": [
        [
          {
            "node": "Restore: Planner Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Restore: Planner Input": {
      "main": [
        [
          {
            "node": "Planner: Build Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Planner: Build Request": {
      "main": [
        [
          {
            "node": "Planner: Call LM Studio",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Planner: Call LM Studio": {
      "main": [
        [
          {
            "node": "Planner: Parse Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Planner: Parse Response": {
      "main": [
        [
          {
            "node": "CB: Planning Complete",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Unload Planner Model": {
      "main": [
        [
          {
            "node": "Research: Fetch Docs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load Coder Model": {
      "main": [
        [
          {
            "node": "Restore: After Load Coder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Restore: After Load Coder": {
      "main": [
        [
          {
            "node": "Spread Tasks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Spread Tasks": {
      "main": [
        [
          {
            "node": "P2: Stash Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "P2: Stash Context": {
      "main": [
        [
          {
            "node": "P2: Build Code Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "P2: Build Code Input": {
      "main": [
        [
          {
            "node": "CW: Prepare Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CW: Prepare Message": {
      "main": [
        [
          {
            "node": "CW: Call LM Studio",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CW: Call LM Studio": {
      "main": [
        [
          {
            "node": "CW: Parse Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CW: Parse Response": {
      "main": [
        [
          {
            "node": "P2: Prepare Write",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "P2: Prepare Write": {
      "main": [
        [
          {
            "node": "P2: Write Files",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "P2: Write Files": {
      "main": [
        [
          {
            "node": "P2: Store Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Unload Coder Model": {
      "main": [
        [
          {
            "node": "Build Check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load Reviewer Model": {
      "main": [
        [
          {
            "node": "Restore: After Load Reviewer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Restore: After Load Reviewer": {
      "main": [
        [
          {
            "node": "P3: Re-fetch All Files",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "P3: Re-fetch All Files": {
      "main": [
        [
          {
            "node": "P3: Full Review Build",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "P3: Full Review Build": {
      "main": [
        [
          {
            "node": "P3: Review LLM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "P3: Review LLM": {
      "main": [
        [
          {
            "node": "P3: Code Review Build",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "P3: Review Parse": {
      "main": [
        [
          {
            "node": "CB: Review Complete",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Unload Reviewer Model": {
      "main": [
        [
          {
            "node": "Restore: After Unload Reviewer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Restore: After Unload Reviewer": {
      "main": [
        [
          {
            "node": "P3: Route Decision",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load Fixer Model": {
      "main": [
        [
          {
            "node": "Restore: After Load Fixer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Restore: After Load Fixer": {
      "main": [
        [
          {
            "node": "P4: Fix Build",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "P4: Fix Build": {
      "main": [
        [
          {
            "node": "P4: Fix LLM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "P4: Fix LLM": {
      "main": [
        [
          {
            "node": "P4: Fix Parse",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "P4: Fix Parse": {
      "main": [
        [
          {
            "node": "P4: Fix Write",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "P4: Fix Write": {
      "main": [
        [
          {
            "node": "Post-Fix Build Check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Unload Fixer Model": {
      "main": [
        [
          {
            "node": "Restore: After Unload Fixer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Restore: After Unload Fixer": {
      "main": [
        [
          {
            "node": "Load Final Reviewer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate All Results": {
      "main": [
        [
          {
            "node": "Prepare Memory Update",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Memory Update": {
      "main": [
        [
          {
            "node": "Update Memory",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Memory": {
      "main": [
        [
          {
            "node": "Build Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Startup: Unload All Models": {
      "main": [
        [
          {
            "node": "Load Planner Model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "P2: Store Result": {
      "main": [
        [
          {
            "node": "P2: Continue Gate",
            "type": "main",
            "index": 0
          },
          {
            "node": "P2: Exit Gate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "P2: Continue Gate": {
      "main": [
        [
          {
            "node": "P2: Stash Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "P2: Exit Gate": {
      "main": [
        [
          {
            "node": "Unload Coder Model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CB: Pipeline Started": {
      "main": [
        [
          {
            "node": "Init Memory",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CB: Planning Complete": {
      "main": [
        [
          {
            "node": "Unload Planner Model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CB: Review Complete": {
      "main": [
        [
          {
            "node": "Unload Reviewer Model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Response": {
      "main": [
        [
          {
            "node": "CB: Pipeline Complete",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CB: Pipeline Complete": {
      "main": [
        []
      ]
    },
    "Load Final Reviewer": {
      "main": [
        [
          {
            "node": "Restore: Final Reviewer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Restore: Final Reviewer": {
      "main": [
        [
          {
            "node": "Final: Re-fetch Files",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Final: Re-fetch Files": {
      "main": [
        [
          {
            "node": "Final: Review Build",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Final: Review Build": {
      "main": [
        [
          {
            "node": "Final: Review LLM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Final: Review LLM": {
      "main": [
        [
          {
            "node": "Final: Review Parse",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Final: Review Parse": {
      "main": [
        [
          {
            "node": "Pipeline Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Unload Final Reviewer": {
      "main": [
        [
          {
            "node": "Aggregate All Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CB: Fix Applied": {
      "main": [
        [
          {
            "node": "Unload Fixer Model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Research: Fetch Docs": {
      "main": [
        [
          {
            "node": "Generate Task Concepts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Check": {
      "main": [
        [
          {
            "node": "Playtest: Play Game",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post-Fix Build Check": {
      "main": [
        [
          {
            "node": "Visual Fix Gate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Visual Fix Gate": {
      "main": [
        [
          {
            "node": "Visual: Continue",
            "type": "main",
            "index": 0
          },
          {
            "node": "Visual: Loop Back",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Visual: Continue": {
      "main": [
        [
          {
            "node": "CB: Fix Applied",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Visual: Loop Back": {
      "main": [
        [
          {
            "node": "P4: Fix Build",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Playtest: Play Game": {
      "main": [
        [
          {
            "node": "Load Reviewer Model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Task Concepts": {
      "main": [
        [
          {
            "node": "Load Coder Model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "P3: Route Decision": {
      "main": [
        [
          {
            "node": "P3: Fix Gate",
            "type": "main",
            "index": 0
          },
          {
            "node": "P3: Skip Gate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "P3: Fix Gate": {
      "main": [
        [
          {
            "node": "Load Fixer Model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "P3: Skip Gate": {
      "main": [
        [
          {
            "node": "Load Final Reviewer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pipeline Report": {
      "main": [
        [
          {
            "node": "Unload Final Reviewer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "P3: Code Review Build": {
      "main": [
        [
          {
            "node": "P3: Code Review LLM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "P3: Code Review LLM": {
      "main": [
        [
          {
            "node": "P3: Review Parse",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "callerPolicy": "workflowsFromSameOwner",
    "availableInMCP": false
  },
  "staticData": {
    "global": {
      "proj-1773719443026": {
        "project_id": "proj-1773719443026",
        "goal": "The app has broken imports and exports across files. Go through every file, make sure named vs default exports match their imports, function call arguments match definitions, and only use packages that are in package.json. The project should compile and run cleanly.",
        "status": "completed",
        "completed_tasks": [
          "TASK-001",
          "TASK-002",
          "TASK-003"
        ],
        "pending_tasks": [],
        "files": [
          "src/types/index.ts",
          "src/services/n8nApi.ts",
          "tailwind.config.ts",
          "src/hooks/useProjectManagement.ts",
          "src/store/projectStore.tsx",
          "src/hooks/useImagePaste.ts",
          "src/components/Layout/Sidebar.tsx",
          "src/components/Layout/Header.tsx",
          "src/App.tsx"
        ],
        "created_at": "2026-03-17T19:12:17.237Z",
        "last_updated": "2026-03-17T20:18:05.773Z"
      },
      "_allFileContents": [
        {
          "path": "index.html",
          "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/vite.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Toilet Clicker Game</title>\n  </head>\n  <body class=\"bg-[#0F0518] text-white\">\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
        },
        {
          "path": "package.json",
          "content": "{\n  \"name\": \"toilet-clicker-game\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^18.2.43\",\n    \"@types/react-dom\": \"^18.2.17\",\n    \"@vitejs/plugin-react\": \"^4.2.1\",\n    \"autoprefixer\": \"^10.4.17\",\n    \"postcss\": \"^8.4.33\",\n    \"tailwindcss\": \"^3.4.1\",\n    \"typescript\": \"^5.3.3\",\n    \"vite\": \"^5.0.8\"\n  }\n}"
        },
        {
          "path": "postcss.config.js",
          "content": "export default {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
        },
        {
          "path": "src/assets/icons/upgrade-icons.ts",
          "content": "import React from 'react';\n\n// Toilet Icon Component\nexport const ToiletIcon: React.FC<{ className?: string }> = ({ className }) => (\n  <svg \n    xmlns=\"http://www.w3.org/2000/svg\" \n    viewBox=\"0 0 24 24\" \n    fill=\"none\" \n    stroke=\"currentColor\" \n    strokeWidth=\"2\" \n    strokeLinecap=\"round\" \n    strokeLinejoin=\"round\" \n    className={className}\n  >\n    <path d=\"M19 7V4a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v3\" />\n    <path d=\"M4 10h16\" />\n    <path d=\"M5 21a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2\" />\n    <path d=\"M9 21v-8a3 3 0 0 1 6 0v8\" />\n    <path d=\"M12 10v4\" />\n    <path d=\"M10 12h4\" />\n  </svg>\n);\n\n// Pipe Icon Component\nexport const PipeIcon: React.FC<{ className?: string }> = ({ className }) => (\n  <svg \n    xmlns=\"http://www.w3.org/2000/svg\" \n    viewBox=\"0 0 24 24\" \n    fill=\"none\" \n    stroke=\"currentColor\" \n    strokeWidth=\"2\" \n    strokeLinecap=\"round\" \n    strokeLinejoin=\"round\" \n    className={className}\n  >\n    <path d=\"M6 19v-3\" />\n    <path d=\"M10 19v-3\" />\n    <path d=\"M14 19v-3\" />\n    <path d=\"M18 19v-3\" />\n    <path d=\"M2 7h20\" />\n    <path d=\"M6 7V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v3\" />\n    <path d=\"M6 14h12\" />\n    <path d=\"M6 10h12\" />\n  </svg>\n);\n\n// Pressure Gauge Icon Component\nexport const PressureGaugeIcon: React.FC<{ className?: string }> = ({ className }) => (\n  <svg \n    xmlns=\"http://www.w3.org/2000/svg\" \n    viewBox=\"0 0 24 24\" \n    fill=\"none\" \n    stroke=\"currentColor\" \n    strokeWidth=\"2\" \n    strokeLinecap=\"round\" \n    strokeLinejoin=\"round\" \n    className={className}\n  >\n    <circle cx=\"12\" cy=\"12\" r=\"10\" />\n    <path d=\"M12 12L16 8\" />\n    <path d=\"M12 12L16 16\" />\n    <path d=\"M12 12L8 16\" />\n    <path d=\"M12 12L8 8\" />\n    <circle cx=\"12\" cy=\"12\" r=\"3\" />\n  </svg>\n);\n\n// Water Drop Icon Component\nexport const WaterDropIcon: React.FC<{ className?: string }> = ({ className }) => (\n  <svg \n    xmlns=\"http://www.w3.org/2000/svg\" \n    viewBox=\"0 0 24 24\" \n    fill=\"none\" \n    stroke=\"currentColor\" \n    strokeWidth=\"2\" \n    strokeLinecap=\"round\" \n    strokeLinejoin=\"round\" \n    className={className}\n  >\n    <path d=\"M12 2.69l5.74 5.88a6 6 0 0 1-8.48 8.48A6 6 0 0 1 5.52 9.11L12 2.69z\" />\n    <path d=\"M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z\" />\n  </svg>\n);\n"
        },
        {
          "path": "src/components/GameScreen.tsx",
          "content": "import React, { useState, useEffect, useRef } from 'react';\nimport './styles/animations.css';\nimport { useGame } from '../context/GameContext';\n\ninterface PoopParticle {\n  id: number;\n  x: number;\n  y: number;\n  velocityX: number;\n  velocityY: number;\n  rotation: number;\n}\n\nconst GameScreen: React.FC = () => {\n  const { poopCount, clickPower, perSecond, handleClick } = useGame();\n  const [bounceAnimation, setBounceAnimation] = useState(false);\n  const [particles, setParticles] = useState<PoopParticle[]>([]);\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  // Initialize particles\n  useEffect(() => {\n    const newParticles: PoopParticle[] = [];\n    const particleCount = 15;\n\n    for (let i = 0; i < particleCount; i++) {\n      newParticles.push({\n        id: i,\n        x: Math.random() * 100, // percentage\n        y: Math.random() * 100, // percentage\n        velocityX: (Math.random() - 0.5) * 2, // random horizontal speed\n        velocityY: Math.random() * 2 + 1, // downward speed\n        rotation: Math.random() * 360,\n      });\n    }\n\n    setParticles(newParticles);\n  }, []);\n\n  // Update particles animation\n  useEffect(() => {\n    const interval = setInterval(() => {\n      setParticles((prevParticles) =>\n        prevParticles.map((particle) => {\n          let newX = particle.x + particle.velocityX * 0.2;\n          let newY = particle.y + particle.velocityY * 0.2;\n          let newRotation = particle.rotation + 2;\n\n          // Reset if out of bounds\n          if (newY > 110) {\n            newX = Math.random() * 100;\n            newY = -10;\n            newRotation = Math.random() * 360;\n          }\n\n          return {\n            ...particle,\n            x: newX,\n            y: newY,\n            rotation: newRotation,\n          };\n        })\n      );\n    }, 50);\n\n    return () => clearInterval(interval);\n  }, []);\n\n  const handleToiletClick = () => {\n    // Animate count with bounce\n    setBounceAnimation(true);\n    setTimeout(() => setBounceAnimation(false), 300);\n\n    // Add particles\n    const newParticles: PoopParticle[] = [];\n    for (let i = 0; i < 5; i++) {\n      newParticles.push({\n        id: Date.now() + i,\n        x: 50, // center of screen\n        y: 60, // near toilet\n        velocityX: (Math.random() - 0.5) * 4,\n        velocityY: Math.random() * 2 + 1,\n        rotation: Math.random() * 360,\n      });\n    }\n\n    setParticles((prev) => [...prev, ...newParticles]);\n\n    // Update count\n    handleClick();\n  };\n\n  return (\n    <div\n      ref={containerRef}\n      className=\"game-screen min-h-screen w-full flex flex-col items-center justify-start p-4 relative overflow-hidden\"\n      style={{\n        background: 'linear-gradient(to bottom, #4c008d, #000000)',\n      }}\n    >\n      {/* Hero Title */}\n      <div className=\"text-center z-10 mt-8 mb-6\">\n        <h1\n          className=\"text-5xl font-bold tracking-wide bg-gradient-to-r from-yellow-400 via-yellow-300 to-yellow-500 text-transparent bg-clip-text\"\n          style={{\n            textShadow: '0 2px 4px rgba(0,0,0,0.5)',\n          }}\n        >\n          Attack on Toilet\n        </h1>\n        <p className=\"text-white mt-3 text-lg max-w-md mx-auto\">\n          Collect poop, upgrade your flush, and dominate the bathroom!\n        </p>\n      </div>\n\n      {/* Click Power and Per Second Counters */}\n      <div className=\"z-10 mb-4 flex gap-6\">\n        <div className=\"text-center\">\n          <div className=\"text-sm text-gray-300\">Click Power</div>\n          <div\n            className=\"text-2xl font-bold\"\n            style={{\n              color: '#FBBF24',\n              textShadow: '0 0 10px rgba(251, 191, 36, 0.7)',\n            }}\n          >\n            {clickPower}\n          </div>\n        </div>\n        <div className=\"text-center\">\n          <div className=\"text-sm text-gray-300\">Per Second</div>\n          <div\n            className=\"text-2xl font-bold\"\n            style={{\n              color: '#FBBF24',\n              textShadow: '0 0 10px rgba(251, 191, 36, 0.7)',\n            }}\n          >\n            {perSecond}\n          </div>\n        </div>\n      </div>\n\n      {/* Poop Counter */}\n      <div className={`z-10 mb-8 ${bounceAnimation ? 'animate-bounce-slight' : ''}`}>\n        <div\n          className=\"text-6xl font-bold\"\n          style={{\n            color: '#FBBF24',\n            textShadow: '0 0 10px rgba(251, 191, 36, 0.7)',\n          }}\n        >\n          {poopCount} Poop Collected\n        </div>\n      </div>\n\n      {/* Toilet Button */}\n      <div className=\"relative z-10 mb-8 touch-target\">\n        <button\n          onClick={handleToiletClick}\n          className=\"toilet-button w-[150px] h-[150px] rounded-lg shadow-lg transition-transform active:scale-110 flex items-center justify-center\"\n          style={{\n            background: 'linear-gradient(135deg, #ffffff 0%, #f0f0f0 100%)',\n            boxShadow: '0 10px 25px rgba(0,0,0,0.5), inset 0 -5px 10px rgba(0,0,0,0.1)',\n          }}\n        >\n          <svg\n            width=\"120\"\n            height=\"90\"\n            viewBox=\"0 0 120 90\"\n            className=\"toilet-svg\"\n          >\n            {/* Toilet base */}\n            <rect x=\"10\" y=\"60\" width=\"100\" height=\"30\" rx=\"5\" fill=\"#f0f0f0\" stroke=\"#ccc\" strokeWidth=\"2\" />\n            {/* Toilet tank */}\n            <rect x=\"40\" y=\"10\" width=\"40\" height=\"50\" rx=\"5\" fill=\"#e0e0e0\" stroke=\"#ccc\" strokeWidth=\"2\" />\n            {/* Toilet seat */}\n            <rect x=\"20\" y=\"50\" width=\"80\" height=\"10\" rx=\"5\" fill=\"#d0d0d0\" stroke=\"#ccc\" strokeWidth=\"2\" />\n            {/* Toilet bowl */}\n            <ellipse cx=\"60\" cy=\"75\" rx=\"30\" ry=\"15\" fill=\"#ffffff\" stroke=\"#ccc\" strokeWidth=\"2\" />\n            {/* Water in bowl */}\n            <ellipse cx=\"60\" cy=\"80\" rx=\"25\" ry=\"8\" fill=\"#3b82f6\" opacity=\"0.7\" />\n          </svg>\n        </button>\n      </div>\n\n      {/* Animated Poop Particles */}\n      {particles.map((particle) => (\n        <div\n          key={particle.id}\n          className=\"poop-particle absolute z-0 text-2xl\"\n          style={{\n            left: `${particle.x}%`,\n            top: `${particle.y}%`,\n            transform: `rotate(${particle.rotation}deg)`,\n            transition: 'transform 0.1s linear',\n          }}\n        >\n          \ud83d\udca9\n        </div>\n      ))}\n\n      {/* Star trails for particles */}\n      <div className=\"absolute inset-0 pointer-events-none overflow-hidden\">\n        {particles.map((particle, index) => (\n          <div\n            key={`star-${index}`}\n            className=\"star-trail absolute w-1 h-1 bg-yellow-300 rounded-full opacity-70\"\n            style={{\n              left: `${particle.x}%`,\n              top: `${particle.y}%`,\n              transform: 'translate(-50%, -50%)',\n              boxShadow: '0 0 8px 2px rgba(255, 255, 0, 0.6)',\n            }}\n          />\n        ))}\n      </div>\n    </div>\n  );\n};\n\nexport default GameScreen;\n"
        },
        {
          "path": "src/components/UpgradeShop.tsx",
          "content": "import React, { useState } from 'react';\nimport {\n  ToiletIcon,\n  PipeIcon,\n  PressureGaugeIcon,\n  WaterDropIcon\n} from './assets/icons/upgrade-icons';\nimport { useGame } from '../context/GameContext';\nimport './styles/animations.css';\n\ninterface UpgradeCardProps {\n  title: string;\n  description: string;\n  benefit: string;\n  owned: boolean;\n  icon: React.ReactNode;\n  cost?: number;\n  onPurchase?: () => void;\n}\n\nconst UpgradeCard: React.FC<UpgradeCardProps> = ({ title, description, benefit, owned, icon, cost, onPurchase }) => {\n  const [purchaseAnimation, setPurchaseAnimation] = useState(false);\n\n  const handlePurchase = () => {\n    if (onPurchase) {\n      // Trigger gold glow animation\n      setPurchaseAnimation(true);\n      setTimeout(() => setPurchaseAnimation(false), 600);\n\n      onPurchase();\n    }\n  };\n\n  return (\n    <div\n      className={`\n        rounded-xl bg-gradient-to-br from-purple-900/80 to-black/80 backdrop-blur-sm\n        border-2 ${owned ? 'border-green-400 shadow-[0_0_15px_rgba(74,222,128,0.3)]' : 'border-purple-400/50 shadow-[0_0_10px_rgba(168,85,247,0.2)]'}\n        p-4 transition-all duration-300\n        ${owned ? 'opacity-100 scale-100' : 'opacity-90 hover:scale-[1.02] hover:shadow-[0_0_20px_rgba(168,85,247,0.4)]'}\n        active:scale-95\n        ${purchaseAnimation ? 'animate-gold-glow border-accent-gold' : ''}\n      `}\n    >\n      <div className=\"flex items-start gap-4\">\n        <div className={`\n          p-3 rounded-lg\n          ${owned ? 'bg-green-900/30' : 'bg-purple-900/30'}\n        `}>\n          {icon}\n        </div>\n\n        <div className=\"flex-1\">\n          <h3 className=\"text-lg font-bold text-white mb-1\">{title}</h3>\n          <p className=\"text-sm text-gray-300 mb-2\">{description}</p>\n\n          <div className={`\n            flex items-center gap-2 text-sm font-medium\n            ${owned ? 'text-green-400' : 'text-purple-300'}\n          `}>\n            {owned ? (\n              <>\n                <span className=\"text-green-400\">\u2713 Owned</span>\n                <span className=\"tabular-nums\">{benefit}</span>\n              </>\n            ) : (\n              <>\n                <span className=\"tabular-nums\">{benefit}</span>\n                <span className=\"text-gray-500 mx-1\">|</span>\n                <span className=\"text-accent-gold font-semibold\">{cost} Poop</span>\n              </>\n            )}\n          </div>\n\n          {!owned && (\n            <button\n              onClick={handlePurchase}\n              disabled={false}\n              className={`\n                mt-3 w-full py-2 px-4 rounded-lg text-sm font-bold transition-all duration-200\n                bg-gradient-to-r from-accent-gold to-accent-amber\n                text-black shadow-lg shadow-orange-500/20\n                hover:shadow-xl hover:shadow-orange-500/30 hover:scale-[1.02]\n                active:scale-95 active:shadow-sm touch-target\n              `}\n            >\n              Buy: {cost} Poop\n            </button>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nconst UpgradeShop: React.FC = () => {\n  const { poopCount, purchaseUpgrade, upgradeList } = useGame();\n\n  const upgrades = [\n    {\n      title: 'Enhanced Flush',\n      description: 'Increase your click power with improved flushing mechanics.',\n      benefit: '+2 Click',\n      owned: false,\n      cost: 50,\n      icon: <ToiletIcon className=\"w-6 h-6 text-purple-300\" />,\n    },\n    {\n      title: 'Slow Leak',\n      description: 'A controlled drip that generates poop over time.',\n      benefit: '+1/sec',\n      owned: true,\n      cost: 0,\n      icon: <WaterDropIcon className=\"w-6 h-6 text-green-400\" />,\n    },\n    {\n      title: 'Pressure Booster',\n      description: 'Amplify water pressure for more powerful flushes.',\n      benefit: '+5 Click',\n      owned: false,\n      cost: 150,\n      icon: <PressureGaugeIcon className=\"w-6 h-6 text-purple-300\" />,\n    },\n    {\n      title: 'Drip System',\n      description: 'Automated dripping system for passive poop generation.',\n      benefit: '+3/sec',\n      owned: false,\n      cost: 200,\n      icon: <WaterDropIcon className=\"w-6 h-6 text-purple-300\" />,\n    },\n  ];\n\n  return (\n    <div className=\"mt-8 w-full max-w-md mx-auto\">\n      {/* Main Shop Container */}\n      <div className=\"\n        rounded-xl bg-black/50 backdrop-blur-sm\n        border-2 border-purple-500 shadow-2xl\n        p-6\n      \">\n        {/* Shop Header */}\n        <div className=\"flex items-center justify-between mb-6\">\n          <div className=\"flex items-center gap-3\">\n            <div className=\"p-2 bg-purple-900/30 rounded-lg\">\n              <svg\n                xmlns=\"http://www.w3.org/2000/svg\"\n                className=\"w-6 h-6 text-purple-400\"\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                strokeWidth=\"2\"\n              >\n                <path d=\"M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4\" />\n              </svg>\n            </div>\n            <h2 className=\"text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-400\">\n              Upgrade Shop\n            </h2>\n          </div>\n          <div className=\"flex items-center gap-2 px-3 py-1 rounded-lg bg-green-900/20 border border-green-500/30\">\n            <span className=\"text-green-400 text-sm font-semibold\">Poop:</span>\n            <span className=\"text-green-300 font-bold tabular-nums\">{poopCount}</span>\n          </div>\n        </div>\n\n        {/* Upgrade Cards */}\n        <div className=\"flex flex-col gap-4\">\n          {upgrades.map((upgrade, index) => {\n            const upgradeState = upgradeList.find(u => u.id === `click-${index + 1}` || u.id === `auto-${index + 1}`);\n            const isOwned = upgrade.owned || (upgradeState ? upgradeState.owned : false);\n            const cost = upgrade.cost || (upgradeState ? upgradeState.cost : 0);\n\n            return (\n              <UpgradeCard\n                key={index}\n                title={upgrade.title}\n                description={upgrade.description}\n                benefit={upgrade.benefit}\n                owned={isOwned}\n                icon={upgrade.icon}\n                cost={cost}\n                onPurchase={() => purchaseUpgrade(upgradeState?.id || `click-${index + 1}`)}\n              />\n            );\n          })}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default UpgradeShop;\n"
        },
        {
          "path": "src/context/GameContext.ts",
          "content": "import { createContext, useContext, useState, useEffect, ReactNode } from 'react';\n\ninterface Upgrades {\n  clickPower: number;\n  perSecond: number;\n}\n\ninterface UpgradeState {\n  id: string;\n  title: string;\n  owned: boolean;\n  cost: number;\n  benefit: string;\n  type: 'clickPower' | 'perSecond';\n}\n\ninterface GameContextType {\n  poopCount: number;\n  clickPower: number;\n  perSecond: number;\n  upgrades: Upgrades;\n  upgradeList: UpgradeState[];\n  handleClick: () => void;\n  addUpgrade: (type: 'clickPower' | 'perSecond') => void;\n  purchaseUpgrade: (upgradeId: string) => void;\n}\n\nconst GameContext = createContext<GameContextType | undefined>(undefined);\n\n// Storage key for localStorage\nconst STORAGE_KEY = 'toilet-clicker-save';\n\nconst GameProvider: React.FC<{ children: ReactNode }> = ({ children }) => {\n  const [poopCount, setPoopCount] = useState(326);\n  const [clickPower, setClickPower] = useState(1);\n  const [perSecond, setPerSecond] = useState(0);\n  const [upgrades, setUpgrades] = useState<Upgrades>({\n    clickPower: 0,\n    perSecond: 0,\n  });\n\n  const [upgradeList, setUpgradeList] = useState<UpgradeState[]>([\n    {\n      id: 'click-1',\n      title: 'Enhanced Flush',\n      owned: false,\n      cost: 50,\n      benefit: '+2 Click',\n      type: 'clickPower',\n    },\n    {\n      id: 'auto-1',\n      title: 'Slow Leak',\n      owned: true,\n      cost: 0,\n      benefit: '+1/sec',\n      type: 'perSecond',\n    },\n    {\n      id: 'click-2',\n      title: 'Pressure Booster',\n      owned: false,\n      cost: 150,\n      benefit: '+5 Click',\n      type: 'clickPower',\n    },\n    {\n      id: 'auto-2',\n      title: 'Drip System',\n      owned: false,\n      cost: 200,\n      benefit: '+3/sec',\n      type: 'perSecond',\n    },\n  ]);\n\n  // Load saved game state on mount\n  useEffect(() => {\n    const loadGame = () => {\n      try {\n        const savedData = localStorage.getItem(STORAGE_KEY);\n        if (savedData) {\n          const parsedData = JSON.parse(savedData);\n\n          if (typeof parsedData.poopCount === 'number') setPoopCount(parsedData.poopCount);\n          if (typeof parsedData.clickPower === 'number') setClickPower(parsedData.clickPower);\n          if (typeof parsedData.perSecond === 'number') setPerSecond(parsedData.perSecond);\n\n          if (Array.isArray(parsedData.upgradeList)) {\n            setUpgradeList((prev) => {\n              const mergedUpgrades = prev.map((upgrade) => {\n                const savedUpgrade = parsedData.upgradeList.find((u: UpgradeState) => u.id === upgrade.id);\n                return savedUpgrade ? { ...upgrade, owned: savedUpgrade.owned } : upgrade;\n              });\n              return mergedUpgrades;\n            });\n          }\n        }\n      } catch (error) {\n        console.error('Failed to load game:', error);\n      }\n    };\n\n    loadGame();\n  }, []);\n\n  // Save game state whenever key values change\n  const saveGame = () => {\n    try {\n      const saveData = {\n        poopCount,\n        clickPower,\n        perSecond,\n        upgradeList: upgradeList.map(({ id, owned }) => ({ id, owned })),\n      };\n      localStorage.setItem(STORAGE_KEY, JSON.stringify(saveData));\n    } catch (error) {\n      console.error('Failed to save game:', error);\n    }\n  };\n\n  // Save on state changes\n  useEffect(() => {\n    saveGame();\n  }, [poopCount, clickPower, perSecond, upgradeList]);\n\n  const handleClick = () => {\n    setPoopCount((prev) => prev + clickPower);\n  };\n\n  const addUpgrade = (type: 'clickPower' | 'perSecond') => {\n    if (type === 'clickPower') {\n      setUpgrades((prev) => ({\n        ...prev,\n        clickPower: prev.clickPower + 1,\n      }));\n      setClickPower((prev) => prev + 1);\n    } else {\n      setUpgrades((prev) => ({\n        ...prev,\n        perSecond: prev.perSecond + 1,\n      }));\n      setPerSecond((prev) => prev + 1);\n    }\n  };\n\n  const purchaseUpgrade = (upgradeId: string) => {\n    setUpgradeList((prevUpgrades) => {\n      return prevUpgrades.map((upgrade) => {\n        if (upgrade.id === upgradeId && !upgrade.owned) {\n          if (poopCount >= upgrade.cost) {\n            setPoopCount((prev) => prev - upgrade.cost);\n\n            if (upgrade.type === 'clickPower') {\n              const powerIncrease = parseInt(upgrade.benefit.replace('+', '').replace(' Click', ''));\n              setClickPower((prev) => prev + powerIncrease);\n              setUpgrades((prev) => ({\n                ...prev,\n                clickPower: prev.clickPower + powerIncrease,\n              }));\n            } else {\n              const perSecondIncrease = parseInt(upgrade.benefit.replace('+', '').replace('/sec', ''));\n              setPerSecond((prev) => prev + perSecondIncrease);\n              setUpgrades((prev) => ({\n                ...prev,\n                perSecond: prev.perSecond + perSecondIncrease,\n              }));\n            }\n\n            return { ...upgrade, owned: true };\n          }\n        }\n        return upgrade;\n      });\n    });\n  };\n\n  return (\n    <GameContext.Provider value={{ poopCount, clickPower, perSecond, upgrades, upgradeList, handleClick, addUpgrade, purchaseUpgrade }}>\n      {children}\n    </GameContext.Provider>\n  );\n};\n\nexport const useGame = () => {\n  const context = useContext(GameContext);\n  if (context === undefined) {\n    throw new Error('useGame must be used within a GameProvider');\n  }\n  return context;\n};\n\nexport default GameContext;\n\nexport { GameProvider };\n"
        },
        {
          "path": "src/hooks/useGameLogic.ts",
          "content": "import { useEffect, useRef } from 'react';\nimport { useGame } from '../context/GameContext';\nimport { useState } from 'react';\n\ninterface Particle {\n  id: number;\n  x: number;\n  y: number;\n  velocityX: number;\n  velocityY: number;\n  rotation: number;\n}\n\nconst useGameLogic = () => {\n  const { poopCount, clickPower, perSecond, handleClick, addUpgrade } = useGame();\n  const [particles, setParticles] = useState<Particle[]>([]);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const lastPoopCountRef = useRef(poopCount);\n\n  // Initialize idle particles\n  useEffect(() => {\n    const newParticles: Particle[] = [];\n    const particleCount = 15;\n\n    for (let i = 0; i < particleCount; i++) {\n      newParticles.push({\n        id: i,\n        x: Math.random() * 100,\n        y: Math.random() * 100,\n        velocityX: (Math.random() - 0.5) * 2,\n        velocityY: Math.random() * 2 + 1,\n        rotation: Math.random() * 360,\n      });\n    }\n\n    setParticles(newParticles);\n  }, []);\n\n  // Update idle particles animation\n  useEffect(() => {\n    const interval = setInterval(() => {\n      setParticles((prevParticles) =>\n        prevParticles.map((particle) => {\n          let newX = particle.x + particle.velocityX * 0.2;\n          let newY = particle.y + particle.velocityY * 0.2;\n          let newRotation = particle.rotation + 2;\n\n          if (newY > 110) {\n            newX = Math.random() * 100;\n            newY = -10;\n            newRotation = Math.random() * 360;\n          }\n\n          return {\n            ...particle,\n            x: newX,\n            y: newY,\n            rotation: newRotation,\n          };\n        })\n      );\n    }, 50);\n\n    return () => clearInterval(interval);\n  }, []);\n\n  // Auto-collection effect\n  useEffect(() => {\n    const interval = setInterval(() => {\n      if (perSecond > 0) {\n        handleClick();\n      }\n    }, 1000);\n\n    return () => clearInterval(interval);\n  }, [perSecond]);\n\n  // Particle burst on click\n  const handleToiletClick = () => {\n    handleClick();\n\n    const newParticles: Particle[] = [];\n    for (let i = 0; i < 5; i++) {\n      newParticles.push({\n        id: Date.now() + i,\n        x: 50,\n        y: 60,\n        velocityX: (Math.random() - 0.5) * 4,\n        velocityY: Math.random() * 2 + 1,\n        rotation: Math.random() * 360,\n      });\n    }\n\n    setParticles((prev) => [...prev, ...newParticles]);\n  };\n\n  // Animate poop count on change\n  useEffect(() => {\n    if (poopCount !== lastPoopCountRef.current) {\n      const animation = setTimeout(() => {\n        lastPoopCountRef.current = poopCount;\n      }, 300);\n\n      return () => clearTimeout(animation);\n    }\n  }, [poopCount]);\n\n  return {\n    poopCount,\n    clickPower,\n    perSecond,\n    particles,\n    containerRef,\n    handleToiletClick,\n    addUpgrade,\n  };\n};\n\nexport default useGameLogic;\n"
        },
        {
          "path": "src/main.tsx",
          "content": "import React, { useState, useEffect } from 'react';\nimport ReactDOM from 'react-dom/client';\nimport { GameProvider } from './context/GameContext';\nimport GameScreen from './components/GameScreen';\nimport './styles/animations.css';\n\nconst LoadingScreen: React.FC<{ isLoading: boolean }> = ({ isLoading }) => {\n  return (\n    <div\n      className={`\n        fixed inset-0 z-50 flex items-center justify-center bg-gradient-to-b from-purple-900 to-black\n        transition-opacity duration-500 ease-in-out\n        ${isLoading ? 'opacity-100' : 'opacity-0 pointer-events-none'}\n      `}\n    >\n      <div className=\"text-center\">\n        <div className=\"mb-4 flex justify-center\">\n          <div className=\"h-12 w-12 animate-spin rounded-full border-4 border-purple-500/30 border-t-purple-500\"></div>\n        </div>\n        <p className=\"text-xl font-medium text-white\">Loading game...</p>\n      </div>\n    </div>\n  );\n};\n\nconst App: React.FC = () => {\n  const [isLoading, setIsLoading] = useState(true);\n\n  useEffect(() => {\n    // Simulate initial load delay for localStorage restoration\n    const timer = setTimeout(() => {\n      setIsLoading(false);\n    }, 800); // Slight delay to show loading state\n\n    return () => clearTimeout(timer);\n  }, []);\n\n  if (isLoading) {\n    return <LoadingScreen isLoading={isLoading} />;\n  }\n\n  return (\n    <GameProvider>\n      <GameScreen />\n    </GameProvider>\n  );\n};\n\nReactDOM.createRoot(document.getElementById('root')!).render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>,\n);\n"
        },
        {
          "path": "src/styles/animations.css",
          "content": "@keyframes fadeIn {\n  0% {\n    opacity: 0;\n  }\n  100% {\n    opacity: 1;\n  }\n}\n\n.fade-in {\n  animation: fadeIn 0.5s ease-in-out forwards;\n  opacity: 0;\n}\n\n@keyframes bounce-slight {\n  0%, 100% { transform: scale(1); }\n  50% { transform: scale(1.1); }\n}\n\n@keyframes gold-glow {\n  0% { box-shadow: 0 0 0 0 rgba(251, 191, 36, 0.7); }\n  70% { box-shadow: 0 0 0 20px rgba(251, 191, 36, 0); }\n  100% { box-shadow: 0 0 0 0 rgba(251, 191, 36, 0); }\n}\n\n@keyframes star-trail {\n  0% {\n    transform: translate(-50%, -50%) scale(1);\n    opacity: 1;\n  }\n  100% {\n    transform: translate(-50%, -150%) scale(0.2);\n    opacity: 0;\n  }\n}\n\n@keyframes poop-fall {\n  0% {\n    transform: translate(-50%, -50%) rotate(0deg);\n    opacity: 1;\n  }\n  100% {\n    transform: translate(-50%, 150%) scale(0.8) rotate(360deg);\n    opacity: 0;\n  }\n}\n\n@keyframes pulse-subtle {\n  0%, 100% { transform: scale(1); }\n  50% { transform: scale(1.02); }\n}\n\n.animate-bounce-slight {\n  animation: bounce-slight 0.3s ease-out;\n}\n\n.animate-gold-glow {\n  animation: gold-glow 0.6s ease-out;\n}\n\n.animate-star-trail {\n  animation: star-trail 1s ease-out forwards;\n}\n\n.animate-poop-fall {\n  animation: poop-fall 1.5s ease-in-out forwards;\n}\n\n.animate-pulse-subtle {\n  animation: pulse-subtle 0.8s ease-in-out infinite;\n}\n\n/* Particle styles */\n.poop-particle {\n  position: absolute;\n  z-index: 50;\n  font-size: 24px;\n  pointer-events: none;\n}\n\n.star-trail {\n  position: absolute;\n  width: 8px;\n  height: 8px;\n  background: linear-gradient(to top, #fbbf24, transparent);\n  border-radius: 50%;\n  z-index: 49;\n  pointer-events: none;\n}\n\n/* Touch target minimum size */\n.touch-target {\n  min-width: 44px;\n  min-height: 44px;\n}\n"
        },
        {
          "path": "src/styles/game.css",
          "content": ".game-screen {\n  position: relative;\n}\n\n.toilet-button {\n  position: relative;\n  cursor: pointer;\n  transition: transform 0.15s ease-out;\n}\n\n.toilet-button::before {\n  content: '';\n  position: absolute;\n  top: -20px;\n  left: 50%;\n  transform: translateX(-50%);\n  width: 100%;\n  height: 20px;\n  background: linear-gradient(to bottom, #e0e0e0, #c0c0c0);\n  border-radius: 10px 10px 0 0;\n}\n\n.toilet-button::after {\n  content: '';\n  position: absolute;\n  top: -30px;\n  left: 50%;\n  transform: translateX(-50%);\n  width: 60%;\n  height: 10px;\n  background: #a0a0a0;\n  border-radius: 5px;\n}\n\n.poop-particle {\n  position: absolute;\n  animation: flyIn 3s linear infinite;\n  z-index: 0;\n}\n\n@keyframes flyIn {\n  0% {\n    transform: rotate(0deg) scale(0.8);\n    opacity: 0;\n  }\n  10% {\n    opacity: 1;\n  }\n  90% {\n    opacity: 1;\n  }\n  100% {\n    transform: rotate(360deg) scale(1.2);\n    opacity: 0;\n  }\n}\n\n.star-trail {\n  animation: starTrail 2s ease-out infinite;\n}\n\n@keyframes starTrail {\n  0% {\n    transform: translate(-50%, -50%) scale(1);\n    opacity: 1;\n  }\n  100% {\n    transform: translate(-50%, -50%) scale(0.2);\n    opacity: 0;\n  }\n}\n\n/* Mobile-first responsive adjustments */\n@media (max-width: 480px) {\n  .game-screen {\n    padding: 16px;\n  }\n\n  h1 {\n    font-size: 2rem;\n  }\n\n  .poop-counter {\n    font-size: 3.5rem;\n  }\n\n  .toilet-button {\n    width: 120px;\n    height: 120px;\n  }\n}\n\n/* High contrast mode support */\n@media (prefers-contrast: high) {\n  .game-screen {\n    background: linear-gradient(to bottom, #3a006b, #000000);\n  }\n\n  h1 {\n    text-shadow: 0 0 5px rgba(255, 255, 0, 0.8);\n  }\n\n  .poop-counter {\n    text-shadow: 0 0 5px rgba(251, 191, 36, 0.8);\n  }\n}\n"
        },
        {
          "path": "tailwind.config.js",
          "content": "/** @type {import('tailwindcss').Config} */\nexport default {\n  content: [\n    \"./index.html\",\n    \"./src/**/*.{js,ts,jsx,tsx}\",\n  ],\n  darkMode: 'class',\n  theme: {\n    extend: {\n      colors: {\n        primary: {\n          DEFAULT: '#8B5CF6',\n          dark: '#6D28D9',\n          lighter: '#A78BFA',\n        },\n        accent: {\n          gold: '#FBBF24',\n          amber: '#F59E0B',\n        },\n        success: {\n          DEFAULT: '#10B981',\n          dark: '#059669',\n        },\n        locked: {\n          DEFAULT: '#6B7280',\n          dark: '#4B5563',\n        },\n        background: {\n          DEFAULT: '#0F0518',\n          gradient: 'linear-gradient(to bottom, #2E1065, #0F0518)',\n        },\n      },\n      animation: {\n        'pulse-fast': 'pulse 0.5s cubic-bezier(0.4, 0, 0.6, 1)',\n        'bounce-slight': 'bounce 0.3s',\n        'float': 'float 3s ease-in-out infinite',\n      },\n      keyframes: {\n        float: {\n          '0%, 100%': { transform: 'translateY(0)' },\n          '50%': { transform: 'translateY(-10px)' },\n        },\n      },\n    },\n  },\n  plugins: [],\n}\n"
        },
        {
          "path": "tsconfig.json",
          "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true\n  },\n  \"include\": [\"src\"]\n}\n"
        },
        {
          "path": "vite.config.js",
          "content": "import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\n\nexport default defineConfig({\n  plugins: [react()],\n  base: '/',\n  build: {\n    outDir: 'dist',\n    sourcemap: false\n  },\n  server: {\n    port: 3000,\n    open: true\n  }\n})\n"
        },
        {
          "path": "src/components/GameScreen.css",
          "content": ".game-screen {\n  position: relative;\n  background: linear-gradient(to bottom, #4c008d, #000000);\n}\n\n.toilet-button {\n  position: relative;\n  cursor: pointer;\n  transition: transform 0.15s ease-out;\n  animation: float 3s ease-in-out infinite;\n}\n\n.toilet-button:active {\n  transform: scale(0.95);\n}\n\n.toilet-button::before {\n  content: '';\n  position: absolute;\n  top: -20px;\n  left: 50%;\n  transform: translateX(-50%);\n  width: 100%;\n  height: 20px;\n  background: linear-gradient(to bottom, #e0e0e0, #c0c0c0);\n  border-radius: 10px 10px 0 0;\n}\n\n.toilet-button::after {\n  content: '';\n  position: absolute;\n  top: -30px;\n  left: 50%;\n  transform: translateX(-50%);\n  width: 60%;\n  height: 10px;\n  background: #a0a0a0;\n  border-radius: 5px;\n}\n\n.poop-particle {\n  position: absolute;\n  animation: flyIn 3s linear infinite;\n  z-index: 0;\n}\n\n@keyframes flyIn {\n  0% {\n    transform: rotate(0deg) scale(0.8);\n    opacity: 0;\n  }\n  10% {\n    opacity: 1;\n  }\n  90% {\n    opacity: 1;\n  }\n  100% {\n    transform: rotate(360deg) scale(1.2);\n    opacity: 0;\n  }\n}\n\n.star-trail {\n  animation: starTrail 2s ease-out infinite;\n}\n\n@keyframes starTrail {\n  0% {\n    transform: translate(-50%, -50%) scale(1);\n    opacity: 1;\n  }\n  100% {\n    transform: translate(-50%, -50%) scale(0.2);\n    opacity: 0;\n  }\n}\n\n/* Mobile-first responsive adjustments */\n@media (max-width: 480px) {\n  .game-screen {\n    padding: 16px;\n  }\n  \n  h1 {\n    font-size: 2rem;\n  }\n  \n  .poop-counter {\n    font-size: 3.5rem;\n  }\n  \n  .toilet-button {\n    width: 120px;\n    height: 120px;\n  }\n}\n\n/* High contrast mode support */\n@media (prefers-contrast: high) {\n  .game-screen {\n    background: linear-gradient(to bottom, #3a006b, #000000);\n  }\n  \n  h1 {\n    text-shadow: 0 0 5px rgba(255, 255, 0, 0.8);\n  }\n  \n  .poop-counter {\n    text-shadow: 0 0 5px rgba(251, 191, 36, 0.8);\n  }\n}\n"
        }
      ],
      "_researchDocs": "## UI/UX Design Principles for Web Applications\n\n### Layout\n- Main interaction element must be centered and visually dominant (40-60% of viewport)\n- Use vertical single-column layouts for game/app UIs \u2014 never side-by-side grids unless it's a dashboard\n- Content hierarchy: hero/main action \u2192 stats/feedback \u2192 secondary actions (shop, settings)\n- Mobile-first: everything should work on a 375px wide screen and scale up\n- Scrollable secondary content (shops, lists) should never push the main interaction off-screen\n\n### Visual Feedback\n- Every user interaction (click, purchase, hover) MUST produce visible feedback\n- Number changes should animate (bounce, scale pulse, color flash)\n- Buttons: press animation (scale 0.95), hover glow/lift, disabled state with reduced opacity\n- Success actions: green flash, checkmark, particle burst\n- Use CSS transitions (200-300ms) on all interactive elements\n\n### Color & Contrast\n- Dark backgrounds with bright accent colors for maximum contrast\n- Use gradients over flat colors for depth (e.g., purple-900 to indigo-950)\n- Interactive elements should be the brightest items on screen\n- Affordability: green = can buy, red/gray = locked, gold = premium\n- Text must have sufficient contrast \u2014 white/yellow on dark, with text-shadow for readability\n\n### Typography\n- Big, bold numbers for scores/stats (text-4xl to text-6xl)\n- Clear hierarchy: title (bold, large) \u2192 subtitle (medium) \u2192 body (regular, smaller)\n- Use font-weight differences, not just size, to create hierarchy\n- Monospace or tabular numbers for counters that change frequently\n\n### Animation\n- Idle animations (slow pulse, float, rotate) make the UI feel alive\n- Click animations should be fast (100-200ms) and snappy\n- Spawn/death animations for appearing/disappearing elements (scale 0\u21921, fade in/out)\n- Use CSS will-change on animated elements for performance\n- Stagger animations for lists (each item slightly delayed)\n\n### Cards & Containers\n- Rounded corners (border-radius: 12-16px)\n- Subtle shadows for depth (shadow-lg, shadow-xl)\n- Semi-transparent backgrounds with backdrop-blur for overlay panels\n- Hover: lift (translateY -2 to -4px) + shadow increase\n- Border: subtle (1px border with low-opacity white or accent color)\n\n---\n\n## Library Documentation (from Context7)\n\n## UI Component Examples (from 21st.dev Magic)\n\n\n## Magic UI: card component\n### Default\n```tsx\nimport { Card } from \"@/components/ui/card\"\n\nconst cardContent = {\n  title: \"Lorem ipsum dolor\",\n  description: \"Lorem ipsum dolor, sit amet consectetur adipisicing elit. Nostrum, hic ipsum! Qui dicta debitis aliquid quo molestias explicabo iure!\"\n}\n\nexport function DefaultCardDemo() {\n  return (\n    <Card {...cardContent} className=\"max-w-[400px] bg-background\" />\n  )\n}\n\nexport function DotsCardDemo() {\n  return (\n    <Card variant=\"dots\" {...cardContent} className=\"max-w-[400px] bg-background\" />\n  )\n}\n\nexport function GradientCardDemo() {\n  return (\n    <Card variant=\"gradient\" {...cardContent} className=\"max-w-[400px] bg-background\" />\n  )\n}\n\nexport function PlusCardDemo() {\n  return (\n    <Card variant=\"plus\" {...cardContent} className=\"max-w-[400px] bg-background\" />\n  )\n}\n\nexport function NeubrutalismCardDemo() {\n  return (\n    <Card variant=\"neubrutalism\" {...cardContent} className=\"max-w-[400px] bg-background\" />\n  )\n}\n\nexport function InnerCardDemo() {\n  return (\n    <Card variant=\"inner\" {...cardContent} className=\"max-w-[400px] bg-background\" />\n  )\n}\n\nexport function LiftedCardDemo() {\n  return (\n    <Card variant=\"lifted\" {...cardContent} className=\"max-w-[400px] bg-background\" />\n  )\n}\n\nexport function CornersCardDemo() {\n  return (\n    <Card variant=\"corners\" {...cardContent} className=\"max-w-[400px] bg-background\" />\n  )\n}\n```\n\n### Simple  Card.tsx\n```tsx\nimport React from \"react\";\n\n\nexport default function Example() {\n    return (\n        <div className=\"p-4 bg-white rounded-lg shadow max-w-80\">\n            <img className=\"rounded-md max-h-40 w-full object-cover\" src=\"https://images.unsplash.com/photo-1560264418-c4445382edbc?q=80&w=400\" alt=\"officeImage\" />\n            <p className=\"text-gray-900 text-xl font-semibold ml-2 mt-2\">Your Card Title</p>\n            <p className=\"text-gray-500 text-sm my-3 ml-2\">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore..</p>\n        </div>\n    );\n};\n\n\n```\n\n## Magic UI: game component\n### default.tsx\n```tsx\nimport React from 'react';\nimport { FinancialDashboard } from '@/components/ui/financial-dashboard';\n\n// Import Lucide icons for the demo\nimport {\n  ArrowLeftRight,\n  CreditCard,\n  Landmark,\n  LineChart,\n  ShieldCheck,\n  SwitchCamera,\n  Target,\n  TrendingUp,\n  Users,\n} from 'lucide-react';\n\n// --- Fallback Icon for Logo ---\nconst LogoIcon = ({\n  letter,\n  className,\n}: {\n  letter: string;\n  className?: string;\n}) => (\n  <div\n    className={`w-9 h-9 flex items-center justify-center rounded-full font-bold text-white text-sm ${className}`}\n  >\n    {letter}\n  </div>\n);\n\n// --- DEMO DATA ---\nconst quickActionsData = [\n  { icon: ArrowLeftRight, title: 'Transfer', description: 'Send Money' },\n  { icon: Landmark, title: 'Pay', description: 'Bills & Payments' },\n  { icon: TrendingUp, title: 'Invest', description: 'Grow Wealth' },\n  { icon: CreditCard, title: 'Cards', description: 'Manage Cards' },\n];\n\nconst recentActivityData = [\n  {\n    icon: <LogoIcon letter=\"N\" className=\"bg-red-600\" />,\n    title: 'Netflix Subscription',\n    time: '2 hours ago',\n    amount: -15.99,\n  },\n  {\n    icon: <LogoIcon letter=\"S\" className=\"bg-green-500\" />,\n    title: 'Salary Deposit',\n    time: '1 day ago',\n    amount: 3450.0,\n  },\n  {\n    icon: LineChart,\n    title: 'Investment Transfer',\n    time: '2 days ago',\n    amount: -500.0,\n  },\n];\n\nconst financialServicesData = [\n  {\n    icon: ShieldCheck,\n    title: 'Wealth Management',\n    description: 'Investment portfolios & advisory',\n    isPremium: true,\n  },\n  {\n    icon: Target,\n    title: 'Savings Goals',\n    description: 'Set & track financial goals',\n    hasAction: true,\n  },\n  {\n    icon: SwitchCamera,\n    title: 'Cash Flow',\n    description: 'Income & expense analysis',\n  },\n  {\n    icon: Users,\n    title: 'Joint Accounts',\n    description: 'Family & business accounts',\n  },\n];\n\n// --- DEMO COMPONENT ---\nexport default function FinancialDashboardDemo() {\n  return (\n    <div className=\"bg-background min-h-screen flex items-center justify-center p-4\">\n      <FinancialDashboard\n        quickActions={quickActionsData}\n        recentActivity={recentActivityData}\n        financialServices={financialServicesData}\n      />\n    </div>\n  );\n}\n```\n\n### default.tsx\n```tsx\nimport { Browser } from \"@/components/ui/browser-simulator\";\n\nexport default function DemoOne() {\n  return <Browser />;\n}\n\n```",
      "_projectFileList": [
        "index.html",
        "package.json",
        "postcss.config.js",
        "src/assets/icons/upgrade-icons.ts",
        "src/components/GameScreen.tsx",
        "src/components/UpgradeShop.tsx",
        "src/context/GameContext.ts",
        "src/hooks/useGameLogic.ts",
        "src/main.tsx",
        "src/styles/animations.css",
        "src/styles/game.css",
        "tailwind.config.js",
        "tsconfig.json",
        "vite.config.js"
      ],
      "_projectApiSummary": "## postcss.config.js\nexport default {...}\n## src/assets/icons/upgrade-icons.ts\nexport const ToiletIcon: React.FC<{...}\nexport const PipeIcon: React.FC<{...}\nexport const PressureGaugeIcon: React.FC<{...}\nexport const WaterDropIcon: React.FC<{...}\n## src/components/GameScreen.tsx\nexport default GameScreen;\n## src/components/UpgradeShop.tsx\nexport default UpgradeShop;\n## src/context/GameContext.ts\nexport const useGame = () => ...\nexport default GameContext;\nexport { GameProvider };\n## src/hooks/useGameLogic.ts\nexport default useGameLogic;\n## tailwind.config.js\nexport default {...}\n## vite.config.js\nexport default defineConfig({...}",
      "_projectDeps": [
        "react",
        "react-dom",
        "@types/react",
        "@types/react-dom",
        "@vitejs/plugin-react",
        "autoprefixer",
        "postcss",
        "tailwindcss",
        "typescript",
        "vite"
      ],
      "p2Results": [
        {
          "task_id": "TASK-001",
          "files_written": [
            "src/components/GameScreen.css"
          ]
        },
        {
          "task_id": "TASK-002",
          "files_written": [
            "src/context/GameContext.ts"
          ]
        },
        {
          "task_id": "TASK-003",
          "files_written": [
            "package.json",
            "vite.config.js"
          ]
        }
      ],
      "allTasks": [
        {
          "task_id": "TASK-001",
          "description": "Fix the import resolution error for './GameScreen.css' in GameScreen.tsx by creating the missing CSS file. The file must be named GameScreen.css and placed in src/components/. The CSS should include styles for the GameScreen component following the design principles: dark background gradient (purple-900 to black), rounded-xl cards with backdrop-blur, animated numbers using tabular-nums, and buttons with press animation (scale 0.95). Apply global styles for animations from src/styles/animations.css and game-specific styles from src/styles/game.css. Use Tailwind classes with custom utilities for gradients and shadows. The coder must ensure the file is created and correctly referenced in GameScreen.tsx.",
          "files": [
            "src/components/GameScreen.css"
          ],
          "dependencies": [],
          "complexity": "low"
        },
        {
          "task_id": "TASK-002",
          "description": "Fix the syntax error in GameContext.ts at line 179 where '>' is expected but 'value' is found. This is likely a JSX or TypeScript type error in the return statement of a component or context provider. The file contains a React context and uses JSX syntax. The error is in a return statement that likely has incorrect JSX structure or mismatched type annotations. The coder must examine the code around line 179 and correct the syntax, ensuring proper JSX closing tags, type definitions, or conditional rendering. The fix must preserve all existing state, logic, and export structure. The file should remain compatible with the GameProvider and useGame hook exports.",
          "files": [
            "src/context/GameContext.ts"
          ],
          "dependencies": [],
          "complexity": "medium"
        },
        {
          "task_id": "TASK-003",
          "description": "Ensure the project has a valid build configuration by verifying and updating vite.config.js and package.json. The project uses Vite with React, TypeScript, and Tailwind. Add any missing dependencies if required (e.g., @types/react, @types/react-dom). Ensure the Vite config includes plugins for React and Tailwind. Confirm the entry point is correctly set to src/main.tsx. The package.json must have correct scripts for dev, build, and preview. If missing, add these scripts. The coder must ensure the project compiles and runs without errors after this task.",
          "files": [
            "package.json",
            "vite.config.js"
          ],
          "dependencies": [],
          "complexity": "low"
        }
      ],
      "_reviewResult": {
        "overall_quality": 65,
        "cross_file_consistent": false,
        "visual_quality": 70,
        "fixes_needed": [
          {
            "file": "src/context/GameContext.ts",
            "severity": "critical",
            "issue": "Build failed due to syntax error: Expected '>' but found 'value' at line 179. This is caused by a JSX syntax error in the GameContext.Provider \u2014 the closing tag is missing or malformed.",
            "fix_instruction": "Fix the JSX syntax in the GameContext.Provider by ensuring the closing tag is properly formatted. The current line is: `<GameContext.Provider value={{ poopCount, clickPower, perSecond, upgrades, upgradeList, handleClick, addUpgrade, purchaseUpgrade }}>`. This is invalid because the JSX is not properly closed. The correct syntax should be: `<GameContext.Provider value={{ poopCount, clickPower, perSecond, upgrades, upgradeList, handleClick, addUpgrade, purchaseUpgrade }}>{children}</GameContext.Provider>`. The error is likely due to a missing closing tag or incorrect formatting in the JSX.",
            "related_files": [
              "src/main.tsx",
              "src/components/GameScreen.tsx"
            ]
          },
          {
            "file": "src/components/GameScreen.tsx",
            "severity": "high",
            "issue": "The GameScreen component imports '../styles/animations.css' but the file is actually located at 'src/styles/animations.css'. This causes a 404 during build, preventing styles from loading.",
            "fix_instruction": "Update the import statement in GameScreen.tsx to correctly reference the file: `import '../styles/animations.css';` \u2192 `import './styles/animations.css';`.",
            "related_files": [
              "src/styles/animations.css"
            ]
          },
          {
            "file": "src/components/UpgradeShop.tsx",
            "severity": "high",
            "issue": "The UpgradeShop component imports '../assets/icons/upgrade-icons.ts' but the file is located at 'src/assets/icons/upgrade-icons.ts'. The import path is incorrect, leading to a module not found error.",
            "fix_instruction": "Update the import path to: `import { ToiletIcon, PipeIcon, PressureGaugeIcon, WaterDropIcon } from './assets/icons/upgrade-icons';`.",
            "related_files": [
              "src/assets/icons/upgrade-icons.ts"
            ]
          },
          {
            "file": "src/hooks/useGameLogic.ts",
            "severity": "medium",
            "issue": "The hook uses `useGame` from '../context/GameContext', but the hook is not used in any component. This creates a dead code path and may cause unnecessary re-renders if used.",
            "fix_instruction": "Either remove the unused hook or use it in a component. If not needed, delete the file.",
            "related_files": [
              "src/components/GameScreen.tsx"
            ]
          },
          {
            "file": "src/styles/game.css",
            "severity": "medium",
            "issue": "The file 'src/styles/game.css' is duplicated in functionality with 'src/components/GameScreen.css'. Both define the same classes (e.g., .game-screen, .toilet-button, .poop-particle). This causes style conflicts and bloat.",
            "fix_instruction": "Remove 'src/styles/game.css' and ensure 'src/components/GameScreen.css' is properly imported in GameScreen.tsx. Alternatively, consolidate styles into a single file.",
            "related_files": [
              "src/components/GameScreen.css",
              "src/components/GameScreen.tsx"
            ]
          },
          {
            "file": "src/main.tsx",
            "severity": "medium",
            "issue": "The App component renders GameScreen directly under GameProvider, but the GameScreen component does not use any context. This is redundant and may cause unnecessary re-renders.",
            "fix_instruction": "Ensure GameScreen uses the GameContext via `useGame()` or remove the provider if not needed. Alternatively, refactor to use context only where necessary.",
            "related_files": [
              "src/context/GameContext.ts",
              "src/components/GameScreen.tsx"
            ]
          }
        ],
        "visual_issues": [
          "The toilet button animation is not visible due to the build failure.",
          "The poop particles are not animating because the build failed.",
          "The upgrade shop UI is not rendered due to the build failure.",
          "The background gradient is not applied correctly in the GameScreen component because of the missing CSS import.",
          "The star trails and particle effects are not visible due to the build failure."
        ],
        "summary": "The project has a critical build failure due to a JSX syntax error in GameContext.ts, preventing any visual output. Additionally, there are multiple import path mismatches (e.g., animations.css, upgrade-icons.ts) that will cause runtime errors if the build were to succeed. The project also contains duplicate CSS files and unused hooks, leading to code bloat. Once the critical build issue is fixed, the visual design is mostly consistent with the goal \u2014 featuring a toilet clicker with poop particles and an upgrade shop \u2014 but the current state prevents any visual validation. The UI components are well-structured with responsive design and animations, but they are currently inaccessible due to the build error."
      },
      "_fixResults": [
        "src/context/GameContext.ts",
        "src/components/GameScreen.tsx",
        "src/components/UpgradeShop.tsx",
        "src/hooks/useGameLogic.ts",
        "src/styles/game.css",
        "src/main.tsx"
      ],
      "5b33e465-8f00-4649-bdb2-c98931982f3d": {
        "project_id": "5b33e465-8f00-4649-bdb2-c98931982f3d",
        "goal": "Create a basic dashboard that monitors the health of zfs pool and nvme drives on my linux server, use react HeroUI elements and have it use this style:",
        "status": "completed",
        "completed_tasks": [
          "TASK-001",
          "TASK-002",
          "TASK-003",
          "TASK-004"
        ],
        "pending_tasks": [],
        "files": [
          "src/main.jsx",
          "src/index.css",
          "src/services/zfsService.js",
          "src/components/PoolHealthCard.jsx",
          "src/components/DriveHealthTable.jsx",
          "src/App.jsx"
        ],
        "created_at": "2026-03-17T21:39:25.947Z",
        "last_updated": "2026-03-17T21:44:17.364Z"
      },
      "my-website": {
        "project_id": "my-website",
        "goal": "Create a landing page using React frontend",
        "status": "active",
        "completed_tasks": [],
        "pending_tasks": [],
        "files": [],
        "created_at": "2026-03-18T03:04:35.279Z",
        "last_updated": "2026-03-18T03:04:35.279Z"
      },
      "_scrapeData": null,
      "_plannerInstanceId": null,
      "_coderInstanceId": null,
      "_currentProjectId": "attack-on-toilet-v3",
      "_currentTaskId": "TASK-003",
      "test-loop2": {
        "project_id": "test-loop2",
        "goal": "Build a simple hello world Next.js page with a heading",
        "status": "active",
        "completed_tasks": [],
        "pending_tasks": [],
        "files": [],
        "created_at": "2026-03-18T16:25:07.978Z",
        "last_updated": "2026-03-18T16:25:07.978Z"
      },
      "_queueTotal": 3,
      "_queueDone": 3,
      "_currentTask": {
        "task_id": "TASK-003",
        "description": "Ensure the project has a valid build configuration by verifying and updating vite.config.js and package.json. The project uses Vite with React, TypeScript, and Tailwind. Add any missing dependencies if required (e.g., @types/react, @types/react-dom). Ensure the Vite config includes plugins for React and Tailwind. Confirm the entry point is correctly set to src/main.tsx. The package.json must have correct scripts for dev, build, and preview. If missing, add these scripts. The coder must ensure the project compiles and runs without errors after this task.",
        "files": [
          "package.json",
          "vite.config.js"
        ],
        "dependencies": [],
        "complexity": "low"
      },
      "_currentProjectGoal": "a toilet clicker game where you collect poop as currency. poop comes in from edge of screen into toilet as you click and multiplies as you gain bonuses from the shop. Use the reference image here as inspiration:",
      "_currentQueue": [],
      "_reviewerInstanceId": null,
      "avalon-landing": {
        "project_id": "avalon-landing",
        "goal": "Build a modern landing page for Avalon Platforms\u2122 \u2014 a premium cloud infrastructure company. Include: hero section with bold headline \"Infrastructure Without Limits\", features grid (3 cards: Global Edge Network, Zero-Downtime Deploys, AI-Powered Scaling), pricing section (3 tiers: Starter $29/mo, Pro $99/mo, Enterprise custom), testimonials carousel, and a contact form. Use a dark theme with electric blue (#0066FF) accents. Tech stack: Next.js 14 App Router + Tailwind CSS.",
        "status": "completed",
        "completed_tasks": [
          "TASK-001",
          "TASK-002",
          "TASK-003",
          "TASK-004"
        ],
        "pending_tasks": [],
        "files": [
          {
            "path": "package.json",
            "bytes": 516,
            "success": true
          },
          {
            "path": "tsconfig.json",
            "bytes": 644,
            "success": true
          },
          {
            "path": "next.config.js",
            "bytes": 56,
            "success": true
          },
          {
            "path": "postcss.config.js",
            "bytes": 94,
            "success": true
          },
          {
            "path": "tailwind.config.ts",
            "bytes": 1014,
            "success": true
          },
          {
            "path": "app/globals.css",
            "bytes": 1136,
            "success": true
          },
          {
            "path": "components/ContactForm.tsx",
            "bytes": 4112,
            "success": true
          },
          {
            "path": "components/TestimonialsCarousel.tsx",
            "bytes": 5749,
            "success": true
          }
        ],
        "created_at": "2026-03-18T16:30:24.459Z",
        "last_updated": "2026-03-18T16:47:28.455Z"
      },
      "_fixerInstanceId": null,
      "avalon-v2": {
        "project_id": "avalon-v2",
        "goal": "Recreate this link-in-bio style landing page for Avalon Platforms\u2122. Match the visual design from the reference URL exactly: full-screen video/image background with dark overlay, centered vertical layout, glassmorphism frosted-glass style buttons with semi-transparent backgrounds and subtle borders, the Avalon Platforms\u2122 heading with tagline \"Insanely simple marketing technologies\", icon row, feature link buttons (Create stunning micro sites, Make incredible ads with AI, Fast & simple email marketing, Go to Avalon Platforms), Clone this site button, and email input with social icons at the bottom. Use the Geist font family. Dark/moody aesthetic with glass-effect UI elements. Tech stack: Next.js 14 App Router + Tailwind CSS.",
        "status": "completed",
        "completed_tasks": [
          "TASK-001",
          "TASK-002"
        ],
        "pending_tasks": [],
        "files": [
          {
            "path": "app/page.tsx",
            "bytes": 2137,
            "success": true
          },
          {
            "path": "app/globals.css",
            "bytes": 340,
            "success": true
          }
        ],
        "created_at": "2026-03-18T17:02:08.987Z",
        "last_updated": "2026-03-18T17:21:07.687Z"
      },
      "idle-clicker": {
        "project_id": "idle-clicker",
        "goal": "Create an \"idle clicker game\" similar to cookie clicker. You can choose the design and theme. Be as creative as you like. Use the reference photo as inspiration.",
        "status": "completed",
        "completed_tasks": [
          "TASK-001",
          "TASK-002"
        ],
        "pending_tasks": [],
        "files": [
          "src/index.css",
          "src/App.jsx",
          "src/components/GameHeader.jsx",
          "src/components/CookieButton.jsx",
          "src/components/FloatingNumber.jsx",
          "src/components/GameFooter.jsx"
        ],
        "created_at": "2026-03-18T18:39:08.933Z",
        "last_updated": "2026-03-18T20:28:08.854Z"
      },
      "_finalReview": {
        "final_quality": 87,
        "summary": "The toilet clicker game is well-implemented with solid state management and visual effects, but needs attention to accessibility and a few import fixes.",
        "suggestions": [
          {
            "preview": "Fix critical import issues in UpgradeShop component",
            "detail": "In src/components/UpgradeShop.tsx, the icon imports from './assets/icons/upgrade-icons' should be corrected to '../assets/icons/upgrade-icons' since it's one level up. The current path causes a broken import for ToiletIcon, PipeIcon, PressureGaugeIcon, and WaterDropIcon. Update all four icon components to use the correct relative path. Additionally, verify that these icons are properly exported from src/assets/icons/upgrade-icons.ts. In GameContext.ts, ensure the upgradeList state is properly initialized with all required fields for each upgrade object."
          },
          {
            "preview": "Enhance accessibility throughout the application",
            "detail": "Add ARIA labels to interactive elements in both GameScreen.tsx and UpgradeShop.tsx. For the toilet button in GameScreen.tsx, add aria-label='Click to collect poop'. In UpgradeShop.tsx, each upgrade card should have role='button' and proper aria-labels for screen readers. Implement keyboard navigation support - ensure the Enter key triggers clicks on interactive elements. Add focus states to all buttons using Tailwind's focus:ring-2 focus:ring-accent-gold classes. For the poop counter in GameScreen.tsx, add aria-live='polite' to announce changes to screen readers. In src/styles/animations.css, ensure animations don't cause accessibility issues by maintaining sufficient contrast during transitions."
          },
          {
            "preview": "Add sound effects and music preferences panel",
            "detail": "Create a new settings component at src/components/SettingsPanel.tsx that allows toggling of sound effects and background music. Add audio files for toilet flush sounds, poop collection, and upgrade purchases to src/assets/sounds/. In GameContext.ts, add state variables for soundEnabled and musicEnabled. Create useSound hook in src/hooks/useSound.ts to manage audio playback with proper volume control. Update handleClick function in GameScreen.tsx to play the flush sound when clicked. Add a settings button to the game screen that opens the settings panel with fade-in animation from src/styles/animations.css. Implement local storage persistence for sound preferences by adding them to the saveGame function in GameContext.ts."
          }
        ]
      },
      "test-webhook": {
        "project_id": "test-webhook",
        "goal": "test",
        "status": "active",
        "completed_tasks": [],
        "pending_tasks": [],
        "files": [],
        "created_at": "2026-03-18T22:14:12.561Z",
        "last_updated": "2026-03-18T22:14:12.561Z"
      },
      "_pendingPlan": {
        "tasks": [
          {
            "task_id": "TASK-001",
            "description": "Fix the import resolution error for './GameScreen.css' in GameScreen.tsx by creating the missing CSS file. The file must be named GameScreen.css and placed in src/components/. The CSS should include styles for the GameScreen component following the design principles: dark background gradient (purple-900 to black), rounded-xl cards with backdrop-blur, animated numbers using tabular-nums, and buttons with press animation (scale 0.95). Apply global styles for animations from src/styles/animations.css and game-specific styles from src/styles/game.css. Use Tailwind classes with custom utilities for gradients and shadows. The coder must ensure the file is created and correctly referenced in GameScreen.tsx.",
            "files": [
              "src/components/GameScreen.css"
            ],
            "dependencies": [],
            "complexity": "low"
          },
          {
            "task_id": "TASK-002",
            "description": "Fix the syntax error in GameContext.ts at line 179 where '>' is expected but 'value' is found. This is likely a JSX or TypeScript type error in the return statement of a component or context provider. The file contains a React context and uses JSX syntax. The error is in a return statement that likely has incorrect JSX structure or mismatched type annotations. The coder must examine the code around line 179 and correct the syntax, ensuring proper JSX closing tags, type definitions, or conditional rendering. The fix must preserve all existing state, logic, and export structure. The file should remain compatible with the GameProvider and useGame hook exports.",
            "files": [
              "src/context/GameContext.ts"
            ],
            "dependencies": [],
            "complexity": "medium"
          },
          {
            "task_id": "TASK-003",
            "description": "Ensure the project has a valid build configuration by verifying and updating vite.config.js and package.json. The project uses Vite with React, TypeScript, and Tailwind. Add any missing dependencies if required (e.g., @types/react, @types/react-dom). Ensure the Vite config includes plugins for React and Tailwind. Confirm the entry point is correctly set to src/main.tsx. The package.json must have correct scripts for dev, build, and preview. If missing, add these scripts. The coder must ensure the project compiles and runs without errors after this task.",
            "files": [
              "package.json",
              "vite.config.js"
            ],
            "dependencies": [],
            "complexity": "low"
          }
        ],
        "plan_document": ""
      },
      "attack-on-toilet": {
        "status": "completed",
        "completed_tasks": [
          "TASK-001",
          "TASK-002"
        ],
        "files": [
          "src/components/GameArea.tsx",
          "src/index.css"
        ],
        "project_id": "attack-on-toilet",
        "last_updated": "2026-03-19T03:25:50.239Z"
      },
      "test-research": {
        "project_id": "test-research",
        "goal": "Build a simple counter app with React and Tailwind",
        "status": "completed",
        "completed_tasks": [
          "TASK-001",
          "TASK-002"
        ],
        "pending_tasks": [],
        "files": [
          "package.json",
          "vite.config.js",
          "index.html",
          "tailwind.config.js",
          "postcss.config.js",
          "src/main.jsx",
          "src/App.jsx",
          "src/components/Counter.jsx",
          "src/styles/globals.css"
        ],
        "created_at": "2026-03-18T23:57:45.115Z",
        "last_updated": "2026-03-19T00:03:42.830Z"
      },
      "attack-on-toilet-v2": {
        "project_id": "attack-on-toilet-v2",
        "goal": "Build \"Attack on Toilet\" from scratch \u2014 a vertical mobile-style idle clicker game using React, Tailwind CSS, and Zustand for state management. The layout must be a single centered column like the reference image: main clickable element dominant in the center (50% of screen height), stats overlay at the top, and upgrade shop as a compact scrollable panel at the bottom. No horizontal layouts, no sidebars, no scrollbars on the main page \u2014 everything fits in 100vh with overflow-hidden on body.\n\nCore mechanics: Click the toilet emoji (\ud83d\udebd large, 120px+) to earn poop (\ud83d\udca9). Each click spawns poop emojis that fly IN from random screen edges toward the toilet like a vortex, shrinking and fading as they arrive. The number of emojis per click scales with click power: Math.floor(Math.log10(clickPower)) + 1, capped at 8. Floating +number text animates upward from the click point.\n\nStats panel at top: poop count, click power, per-second rate. Use a compact number formatter (2.8M, 1.2B, 340K) \u2014 never toLocaleString for large numbers. The poop count should bounce/pulse when it changes.\n\nUpgrade shop at bottom (max 35vh, scrolls internally): cards color-coded by affordability \u2014 gold/amber gradient for buyable, dark red for locked, green with checkmark for owned. Each upgrade has name, description, cost (formatted), and buy button. Include both click multipliers and auto-generators. Start with at least 8 upgrades scaling from 10 to 10,000 cost.\n\nState: Zustand with persist middleware to localStorage. Save poop, clickPower, autoRate, and unlockedUpgrades. Reconstruct non-serializable data on rehydrate. Include a reset game function.\n\nVisual style: dark purple-to-black gradient background, floating semi-transparent bubbles for ambient effect, yellow/gold accent text, glowing pulse on the toilet hover, satisfying squish animation on click (scale 0.9 \u2192 1). Every interaction must have visible feedback.\n\nMUST include: package.json with all deps, index.html, vite.config.js, tailwind.config.js, postcss.config.js. Project must build and run out of the box with npm install && npm run dev. Use image reference as a LAYOUT guide.",
        "status": "completed",
        "completed_tasks": [
          "TASK-001",
          "TASK-002",
          "TASK-003"
        ],
        "pending_tasks": [],
        "files": [
          "package.json",
          "vite.config.js",
          "tailwind.config.js",
          "postcss.config.js",
          "tsconfig.json",
          "index.html",
          "src/main.tsx",
          "src/App.tsx",
          "src/components/GameArea.tsx",
          "src/components/ScoreBoard.tsx",
          "src/components/UpgradeShop.tsx",
          "src/index.css",
          "src/store/gameStore.ts",
          "src/hooks/useGameLoop.ts",
          "src/data/upgrades.ts",
          "src/types/game.ts",
          "src/utils/formatNumber.ts"
        ],
        "created_at": "2026-03-19T03:49:42.323Z",
        "last_updated": "2026-03-19T06:36:08.113Z"
      },
      "_buildResult": {
        "success": false,
        "stage": "build",
        "error": "Build failed",
        "output": "vite v5.4.21 building for production...\ntransforming...\n\u2713 4 modules transformed.\nx Build failed in 119ms\nerror during build:\n[vite:esbuild] Transform failed with 1 error:\n/projects/attack-on-toilet-v3/src/context/GameContext.ts:179:26: ERROR: Expected \">\" but found \"value\"\nfile: /projects/attack-on-toilet-v3/src/context/GameContext.ts:179:26\n\nExpected \">\" but found \"value\"\n177|  \n178|    return (\n179|      <GameContext.Provider value={{ poopCount, clickPower, perSecond, upgrades, upgradeList, handleClick, addUpgrade, purchaseUpgrade }}>\n   |                            ^\n180|        {children}\n181|      </GameContext.Provider>\n\n    at failureErrorWithLog (/projects/attack-on-toilet-v3/node_modules/esbuild/lib/main.js:1472:15)\n    at /projects/attack-on-toilet-v3/node_modules/esbuild/lib/main.js:755:50\n    at responseCallbacks.<computed> (/projects/attack-on-toilet-v3/node_modules/esbuild/lib/main.js:622:9)\n    at handleIncomingPacket (/projects/attack-on-toilet-v3/node_modules/esbuild/lib/main.js:677:12)\n    at Socket.readFromStdout (/projects/attack-on-toilet-v3/node_modules/esbuild/lib/main.js:600:7)\n    at Socket.emit (node:events:524:28)\n    at addChunk (node:internal/streams/readable:561:12)\n    at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)\n    at Readable.push (node:internal/streams/readable:392:5)\n    at Pipe.onStreamRead (node:internal/stream_base_commons:191:23)\n"
      },
      "_screenshot_b64": null,
      "attack-on-toilet-v3": {
        "project_id": "attack-on-toilet-v3",
        "goal": "a toilet clicker game where you collect poop as currency. poop comes in from edge of screen into toilet as you click and multiplies as you gain bonuses from the shop. Use the reference image here as inspiration:",
        "status": "completed",
        "completed_tasks": [
          "TASK-001",
          "TASK-002",
          "TASK-003"
        ],
        "pending_tasks": [],
        "files": [
          "src/components/GameScreen.css",
          "src/context/GameContext.ts",
          "package.json",
          "vite.config.js"
        ],
        "created_at": "2026-03-19T06:43:15.275Z",
        "last_updated": "2026-03-19T14:38:15.124Z"
      }
    }
  },
  "meta": null,
  "versionId": "c4e7e3fc-3b1a-48ed-a2c7-e880d6123dd8",
  "activeVersionId": "c4e7e3fc-3b1a-48ed-a2c7-e880d6123dd8",
  "versionCounter": 391,
  "triggerCount": 1,
  "shared": [
    {
      "updatedAt": "2026-03-17T03:35:25.545Z",
      "createdAt": "2026-03-17T03:35:25.545Z",
      "role": "workflow:owner",
      "workflowId": "PNOMGkCjzFGxf52E",
      "projectId": "NOoEgHyQpyx29cuh",
      "project": {
        "updatedAt": "2026-03-08T03:53:52.951Z",
        "createdAt": "2026-03-08T03:52:59.888Z",
        "id": "NOoEgHyQpyx29cuh",
        "name": "Will Lovelock <eekanti@gmail.com>",
        "type": "personal",
        "icon": null,
        "description": null,
        "creatorId": "8a83cac8-2e54-48fc-8da6-ef4d02988d2f"
      }
    }
  ],
  "tags": [],
  "activeVersion": {
    "updatedAt": "2026-03-19T05:34:06.199Z",
    "createdAt": "2026-03-19T05:34:06.199Z",
    "versionId": "c4e7e3fc-3b1a-48ed-a2c7-e880d6123dd8",
    "workflowId": "PNOMGkCjzFGxf52E",
    "nodes": [
      {
        "parameters": {
          "httpMethod": "POST",
          "path": "coding-agent",
          "responseMode": "lastNode",
          "options": {}
        },
        "id": "wh-trigger",
        "name": "Webhook",
        "type": "n8n-nodes-base.webhook",
        "typeVersion": 2,
        "position": [
          250,
          300
        ],
        "webhookId": "b3f7a2d1-4e89-4c12-9f56-a1b2c3d4e5f6"
      },
      {
        "parameters": {
          "jsCode": "const query = $json.query || {};\nconst body = $json.body || $json;\nconst message = query.message || body.message || body.goal || '';\nconst projectId = query.project_id || body.project_id || ('proj-' + Date.now());\nconst reference_url = query.reference_url || body.reference_url || null;\nconst image_data = body.image_data || null;\nreturn [{ json: { message, project_id: projectId, operation: 'init', goal: message, reference_url, image_data } }];"
        },
        "id": "extract-input",
        "name": "Extract Input",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          450,
          300
        ]
      },
      {
        "parameters": {
          "jsCode": "const operation = $json.operation || 'get';\nconst projectId = $json.project_id;\nif (!projectId) return [{ json: { error: 'project_id required' } }];\nconst store = $getWorkflowStaticData('global');\nif (operation === 'init') {\n  if (store[projectId]) return [{ json: { ...store[projectId], already_exists: true } }];\n  store[projectId] = {\n    project_id: projectId,\n    goal: $json.goal || '',\n    status: 'active',\n    completed_tasks: [],\n    pending_tasks: [],\n    files: [],\n    created_at: new Date().toISOString(),\n    last_updated: new Date().toISOString()\n  };\n  return [{ json: store[projectId] }];\n}\nif (operation === 'get') {\n  return [{ json: store[projectId] || { project_id: projectId, exists: false } }];\n}\nif (operation === 'set') {\n  const existing = store[projectId] || {};\n  store[projectId] = { ...existing, ...$json.state, project_id: projectId, last_updated: new Date().toISOString() };\n  return [{ json: store[projectId] }];\n}\nreturn [{ json: { error: `Unknown operation: ${operation}` } }];"
        },
        "id": "init-memory",
        "name": "Init Memory",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          650,
          300
        ]
      },
      {
        "parameters": {
          "method": "GET",
          "url": "={{ ($env.FILE_API_URL || 'http://file-api:3456') + '/projects/' + $json.project_id + '/files-content' }}",
          "sendHeaders": true,
          "headerParameters": {
            "parameters": [
              {
                "name": "Authorization",
                "value": "={{ 'Bearer ' + ($env.FILE_API_TOKEN || '') }}"
              }
            ]
          },
          "options": {}
        },
        "id": "fetch-project-files",
        "name": "Fetch Project Files",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          850,
          300
        ],
        "onError": "continueRegularOutput"
      },
      {
        "parameters": {
          "jsCode": "// MODIFIED: Absorbs Research: Build Project Context\n// Caches _allFileContents, _researchDocs, _projectFileList in static data\n// so Phase 2 chunks don't need to re-fetch or re-compute.\nconst memory = $('Init Memory').first().json;\nconst message = $('Extract Input').first().json.message;\nconst allFiles = $json.files || [];\nconst staticData = $getWorkflowStaticData('global');\n\n// Cache all file contents for Phase 2\nstaticData._allFileContents = allFiles;\n\nconst fileList = allFiles.map(f => f.path);\n\n// Build API surface (exports from each file)\nconst apiSurface = [];\nfor (const f of allFiles) {\n  if (!f.path.match(/\\.(ts|tsx|js|jsx)$/) || f.path.includes('node_modules')) continue;\n  const lines = (f.content || '').split('\\n');\n  const exports = [];\n  for (const line of lines) {\n    const trimmed = line.trim();\n    if (/^export\\s+default\\s/.test(trimmed)) {\n      exports.push(trimmed.replace(/\\{[\\s\\S]*$/, '{...}').substring(0, 120));\n    } else if (/^export\\s+(const|let|var|function|class|interface|type|enum)\\s/.test(trimmed)) {\n      exports.push(trimmed.replace(/\\{[\\s\\S]*$/, '{...}').replace(/=>[\\s\\S]*$/, '=> ...').substring(0, 150));\n    } else if (/^export\\s+\\{/.test(trimmed)) {\n      exports.push(trimmed.substring(0, 150));\n    }\n  }\n  if (exports.length > 0) {\n    apiSurface.push(`## ${f.path}\\n${exports.join('\\n')}`);\n  }\n}\n\n// Extract dependencies from package.json\nlet dependencies = {};\nlet devDependencies = {};\nlet depNames = [];\nfor (const f of allFiles) {\n  if (f.path === 'package.json' || f.path.endsWith('/package.json')) {\n    try {\n      const pkg = JSON.parse(f.content);\n      dependencies = { ...dependencies, ...(pkg.dependencies || {}) };\n      devDependencies = { ...devDependencies, ...(pkg.devDependencies || {}) };\n      depNames = [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.devDependencies || {})];\n    } catch(e) {}\n  }\n}\n\n// Build research docs string\nconst depList = Object.entries(dependencies).map(([k,v]) => `  ${k}: ${v}`).join('\\n');\nconst devDepList = Object.entries(devDependencies).map(([k,v]) => `  ${k}: ${v}`).join('\\n');\nlet researchDocs = '';\nif (depList || devDepList) {\n  researchDocs += '## Dependency Manifest\\nOnly use packages listed here.\\n';\n  if (depList) researchDocs += `dependencies:\\n${depList}\\n`;\n  if (devDepList) researchDocs += `devDependencies:\\n${devDepList}\\n`;\n}\nif (apiSurface.length > 0) {\n  researchDocs += '\\n## Project API Surface\\nMatch these import/export signatures exactly when importing from project files.\\n';\n  researchDocs += apiSurface.join('\\n\\n');\n}\n\n// Cache for all downstream phases\nstaticData._researchDocs = researchDocs;\nstaticData._projectFileList = fileList;\nstaticData._projectApiSummary = apiSurface.join('\\n');\nstaticData._projectDeps = depNames;\n\nreturn [{ json: {\n  message,\n  project_goal: memory.goal || message,\n  project_id: memory.project_id,\n  existing_files: fileList,\n  api_summary: apiSurface.join('\\n'),\n  installed_packages: depNames\n} }];"
        },
        "id": "prepare-planner",
        "name": "Prepare Planner Input",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          1050,
          300
        ]
      },
      {
        "parameters": {
          "jsCode": "const message = $json.message || '';\nconst existingFiles = $json.existing_files || [];\nconst apiSummary = $json.api_summary || '';\nconst installedPkgs = $json.installed_packages || [];\nconst scrapeData = $json._scrapeData;\n\nlet projectContext = '';\nif (existingFiles.length > 0) {\n  projectContext += `\\n\\nEXISTING PROJECT FILES (only reference files from this list):\\n${existingFiles.join('\\n')}\\n`;\n}\nif (apiSummary) {\n  projectContext += `\\nAPI SURFACE (exports from each file):\\n${apiSummary}\\n`;\n}\nif (installedPkgs.length > 0) {\n  projectContext += `\\nINSTALLED PACKAGES (only reference packages from this list):\\n${installedPkgs.join(', ')}\\n`;\n}\n\nconst plannerPrompt = `You are a Senior Software Architect and UI/UX Design Expert. Your job is to break a coding request into MULTIPLE structured tasks with detailed visual specifications.\n\nDESIGN PRINCIPLES (apply to ALL task descriptions):\n- Main interaction element must be centered and dominant (40-60% of viewport height)\n- Use vertical single-column layouts for apps/games \u2014 never side-by-side grids unless explicitly requested\n- Content hierarchy: hero/main action at top \u2192 stats/feedback \u2192 secondary actions (shop, settings) at bottom\n- Mobile-first: everything fits in 100vh, no page-level scrollbars, internal scrolling only for lists\n- Dark backgrounds with bright accent colors. Use gradients for depth (purple-900 to black, not flat colors)\n- Every click/tap must produce visible feedback (scale animation, color flash, particle effect)\n- Numbers should animate when they change (bounce, pulse, scale). Use tabular-nums for counters\n- Cards: rounded-xl (12-16px), subtle shadows, semi-transparent backgrounds with backdrop-blur\n- Buttons: press animation (scale 0.95), hover glow, disabled state with reduced opacity\n- Color-code affordability: gold/amber = available, red/gray = locked, green = owned/success\n- Typography: big bold numbers (text-4xl+), clear hierarchy via weight not just size\n- Spacing: generous padding, never cramped. Mobile touch targets minimum 44px\n\nWHEN A REFERENCE IMAGE IS PROVIDED:\n- Describe the EXACT layout structure you see: column vs row, spacing ratios, component sizes\n- Specify exact colors, gradients, border styles, and shadows visible in the image\n- Note the visual hierarchy: what's biggest, what's brightest, what draws the eye first\n- Describe animations or interactive states implied by the design (hover effects, active states)\n- Include specific dimensions: \"toilet button should be 150px diameter\" not just \"large button\"\n- The coder CANNOT see the image \u2014 your description is their ONLY reference Each task will be handled by a separate coder with 128K context who can write up to 6 files at once.\\n\\nOutput a JSON object with ONE field:\\n\\n\"tasks\": array of task objects. IMPORTANT: You MUST create multiple tasks. Group files by concern (e.g. styles in one task, frontend components in another, config in another). Each task has:\\n- task_id: string like \"TASK-001\"\\n- description: detailed actionable description of what to implement, including specific requirements, color values, component names, API endpoints, and behavior. The coder cannot see the original request \u2014 your description is all they get.\\n- files: array of exact file paths to create or modify. List EVERY file this task needs \u2014 the coder can ONLY write files listed here. For existing projects, prefer modifying files from the EXISTING PROJECT FILES list. For new projects or missing functionality, CREATE new file paths as needed.\\n- dependencies: array of task_id strings this task depends on (empty if none)\\n- complexity: \"low\", \"medium\", or \"high\"\\n\\nRULES:\\n- Always split work across multiple tasks by concern: styles, components, routes, config, etc.\\n- Each task can touch up to 12-15 files. The coder has 128K context and currently uses less than 2% of it \u2014 give it MORE work per task. Aim for 2-3 LARGE tasks instead of many small ones. A single task can contain an entire feature (components + hooks + styles + config). Fewer tasks = faster pipeline\\n- The description must be SELF-CONTAINED \u2014 include ALL details the coder needs\\n- For existing projects, reference files from the EXISTING PROJECT FILES list when modifying. You MAY create new files that do not exist yet.\n- CRITICAL: For new projects (empty file list), you MUST include a setup task that creates: package.json (with ALL dependencies), index.html, vite.config.js (or next.config.js), tsconfig.json (if TypeScript), tailwind.config.js + postcss.config.js (if using Tailwind), and the main entry point (src/main.jsx or app/layout.tsx). The project MUST be buildable.\\n- Detect the project type from the files and packages. If the project is a React/Vite frontend (no Express or backend framework in INSTALLED PACKAGES), focus all tasks on frontend files. Only create server/middleware/API handler tasks if Express or a similar backend framework is in INSTALLED PACKAGES.\\n- In each task description, specify the EXACT import/export style each file should use (named vs default) based on the API SURFACE above.\\n- The FIRST task should always include project configuration files (package.json, config files, index.html) if they do not already exist. The project MUST compile and run after all tasks complete.\n- Output ONLY the raw JSON object: {\"tasks\": [...]}\\n- No markdown, no explanation \u2014 ONLY the JSON with tasks`;\n\nlet messageContent;\nif (scrapeData && scrapeData.screenshot_b64) {\n  const cssTokensStr = JSON.stringify(scrapeData.css_tokens, null, 2);\n  const domStr = scrapeData.dom_summary || '';\n  messageContent = [\n    { type: 'image_url', image_url: { url: `data:image/png;base64,${scrapeData.screenshot_b64}` } },\n    { type: 'text', text: `${plannerPrompt}\\n\\nREFERENCE DESIGN (screenshot above):\\nCSS Tokens:\\n${cssTokensStr}\\n\\nDOM Structure:\\n${domStr}\\n\\nRequest: ${message}${projectContext}\\n\\n(Match the visual design, color palette, typography, and layout from the screenshot)` }\n  ];\n} else if ($('Extract Input').first().json.image_data) {\n  const userImage = $('Extract Input').first().json.image_data;\n  messageContent = [\n    { type: 'image_url', image_url: { url: 'data:image/png;base64,' + userImage } },\n    { type: 'text', text: plannerPrompt + '\\n\\nREFERENCE IMAGE (uploaded by user \u2014 replicate this design):\\n\\nRequest: ' + message + projectContext }\n  ];\n} else {\n  messageContent = `${plannerPrompt}\\n\\nRequest: ${message}${projectContext}`;\n}\n\nreturn [{\n  json: {\n    model: $env.PLANNER_MODEL || 'qwen/qwen3-vl-32b',\n    messages: [{ role: 'user', content: messageContent }],\n    temperature: 0.7,\n    top_p: 0.9,\n    top_k: 20,\n    min_p: 0.0,\n    presence_penalty: 0.0,\n    max_tokens: 32768\n  }\n}];"
        },
        "id": "planner-build",
        "name": "Planner: Build Request",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          1250,
          300
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "={{ $env.LM_STUDIO_URL || 'http://10.0.0.100:1234/v1/chat/completions' }}",
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify($json) }}",
          "options": {
            "timeout": 600000
          },
          "sendHeaders": true,
          "headerParameters": {
            "parameters": [
              {
                "name": "Authorization",
                "value": "={{ 'Bearer ' + $env.LLM_API_KEY }}"
              }
            ]
          }
        },
        "id": "planner-llm",
        "name": "Planner: Call LM Studio",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          1450,
          300
        ]
      },
      {
        "parameters": {
          "jsCode": "const raw = $json.choices[0].message.content || $json.choices[0].message.reasoning_content || '';\nlet content = raw.replace(/<think>[\\s\\S]*?<\\/think>/g, '').trim();\nif (content.includes('</think>')) content = content.split('</think>').pop().trim();\ncontent = content.replace(/<\\/?task>/g, '').trim();\ncontent = content.replace(/\\n+(?:Reasoning|Note|Explanation):[\\s\\S]*/i, '').trim();\ncontent = content.replace(/^```(?:json)?\\n?/, '').replace(/\\n?```$/, '').trim();\n\nlet tasks, planDocument = '';\ntry {\n  const parsed = JSON.parse(content);\n  if (Array.isArray(parsed)) {\n    tasks = parsed;\n  } else {\n    tasks = parsed.tasks || [parsed];\n    planDocument = parsed.plan_document || '';\n  }\n} catch (e) {\n  // Truncated JSON recovery: extract complete task objects\n  const tasksMatch = content.match(/\"tasks\"\\s*:\\s*\\[/);\n  if (tasksMatch) {\n    const arrStart = content.indexOf('[', tasksMatch.index);\n    let bracketDepth = 0;\n    let lastCompleteObj = -1;\n    for (let i = arrStart; i < content.length; i++) {\n      if (content[i] === '{') bracketDepth++;\n      if (content[i] === '}') {\n        bracketDepth--;\n        if (bracketDepth === 0) lastCompleteObj = i;\n      }\n    }\n    if (lastCompleteObj > arrStart) {\n      const recoveredArr = content.substring(arrStart, lastCompleteObj + 1) + ']';\n      try {\n        tasks = JSON.parse(recoveredArr);\n      } catch (e2) {\n        tasks = [{ task_id: 'TASK-FALLBACK-' + Date.now(), description: content, files: [], dependencies: [], complexity: 'high' }];\n      }\n    } else {\n      tasks = [{ task_id: 'TASK-FALLBACK-' + Date.now(), description: content, files: [], dependencies: [], complexity: 'high' }];\n    }\n  } else {\n    tasks = [{ task_id: 'TASK-FALLBACK-' + Date.now(), description: content, files: [], dependencies: [], complexity: 'high' }];\n  }\n}\n\ntasks = tasks.map(t => ({\n  ...t,\n  files: t.files || [],\n  dependencies: t.dependencies || [],\n  complexity: t.complexity || 'medium'\n}));\n\nreturn [{ json: { tasks, plan_document: planDocument } }];"
        },
        "id": "planner-parse",
        "name": "Planner: Parse Response",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          1650,
          300
        ]
      },
      {
        "parameters": {
          "jsCode": "// MODIFIED: Absorbs Split Into Chunks. Builds flat (task, chunk) queue.\n// Queue is passed through data pipeline (not static data) to avoid stale reads after HTTP nodes.\nconst tasks = $json.tasks || [];\nconst planDocument = $json.plan_document || '';\nconst plannerInput = $('Prepare Planner Input').first().json;\nconst projectId = plannerInput.project_id;\nconst projectGoal = plannerInput.project_goal;\n\n// Topological sort\nconst taskMap = new Map();\ntasks.forEach(t => taskMap.set(t.task_id, t));\nconst inDegree = new Map();\nconst dependents = new Map();\ntasks.forEach(t => {\n  inDegree.set(t.task_id, 0);\n  dependents.set(t.task_id, []);\n});\ntasks.forEach(t => {\n  const deps = (t.dependencies || []).filter(d => taskMap.has(d));\n  inDegree.set(t.task_id, deps.length);\n  deps.forEach(d => dependents.get(d).push(t.task_id));\n});\nconst topoQueue = [];\ntasks.forEach(t => {\n  if (inDegree.get(t.task_id) === 0) topoQueue.push(t.task_id);\n});\nconst sorted = [];\nwhile (topoQueue.length > 0) {\n  const id = topoQueue.shift();\n  sorted.push(taskMap.get(id));\n  for (const dep of (dependents.get(id) || [])) {\n    inDegree.set(dep, inDegree.get(dep) - 1);\n    if (inDegree.get(dep) === 0) topoQueue.push(dep);\n  }\n}\nif (sorted.length < tasks.length) {\n  const sortedIds = new Set(sorted.map(t => t.task_id));\n  tasks.forEach(t => { if (!sortedIds.has(t.task_id)) sorted.push(t); });\n}\n\n// Flatten into (task, chunk) queue \u2014 2 files per chunk\nconst chunkSize = 15;\nconst queue = [];\nfor (const task of sorted) {\n  const taskFiles = task.files || [];\n  const chunks = [];\n  for (let i = 0; i < taskFiles.length; i += chunkSize) {\n    chunks.push(taskFiles.slice(i, i + chunkSize));\n  }\n  if (taskFiles.length === 0) continue;  // skip tasks with no files assigned\n  for (const chunkFiles of chunks) {\n    queue.push({\n      task,\n      chunk_files: chunkFiles,\n      project_id: projectId,\n      project_goal: projectGoal,\n      plan_document: planDocument\n    });\n  }\n}\n\nconst staticData = $getWorkflowStaticData('global');\nstaticData.p2Results = [];\nstaticData.allTasks = sorted;\nstaticData._queueTotal = queue.length;\nstaticData._queueDone = 0;\n\nif (queue.length === 0) {\n  return [{ json: { task: { task_id: 'NONE', description: 'No tasks', files: [] }, chunk_files: [], project_id: projectId, project_goal: projectGoal, plan_document: planDocument, _p2Queue: [] } }];\n}\n// Pass remaining queue items through data pipeline instead of static data\nconst first = queue[0];\nfirst._p2Queue = queue.slice(1);\nreturn [{ json: first }];"
        },
        "id": "spread-tasks",
        "name": "Spread Tasks",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          1850,
          300
        ]
      },
      {
        "parameters": {
          "jsCode": "// P2: Stash Context \u2014 passes through all fields including _p2Queue for loop control\nconst staticData = $getWorkflowStaticData('global');\nstaticData._currentProjectId = $json.project_id;\nstaticData._currentTaskId = ($json.task || {}).task_id || '';\nstaticData._currentTask = $json.task || null;\nstaticData._currentProjectGoal = $json.project_goal || '';\nstaticData._currentQueue = $json._p2Queue || [];  // snapshot queue for this iteration\nreturn [{ json: {\n  task: $json.task,\n  chunk_files: $json.chunk_files,\n  project_id: $json.project_id,\n  project_goal: $json.project_goal,\n  plan_document: $json.plan_document,\n  _p2Queue: $json._p2Queue || []\n} }];"
        },
        "id": "p2-stash",
        "name": "P2: Stash Context",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          2050,
          300
        ]
      },
      {
        "parameters": {
          "jsCode": "// Builds coder input with FULL project context (not just chunk files)\n// The coder has 128K context \u2014 feed it everything so it sees the whole picture\nconst staticData = $getWorkflowStaticData('global');\nconst stashed = $json;\nconst chunkFiles = stashed.chunk_files || [];\nconst allFiles = staticData._allFileContents || [];\n\n// Send ALL project files as context \u2014 the coder needs to see CSS classes,\n// imports, exports, and component props across the entire project\nconst existingFiles = allFiles;\n\n// Mark which files are in THIS task's chunk (so coder knows what to output)\nconst task = { ...(stashed.task || {}) };\nif (chunkFiles.length > 0) task.files = chunkFiles;\n\nreturn [{ json: {\n  task,\n  existing_files: existingFiles,\n  plan_document: stashed.plan_document || '',\n  project_goal: stashed.project_goal || '',\n  project_id: stashed.project_id,\n  research_docs: staticData._researchDocs || ''\n} }];"
        },
        "id": "p2-build-input",
        "name": "P2: Build Code Input",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          2250,
          300
        ]
      },
      {
        "parameters": {
          "jsCode": "// MODIFIED: Removed retry/feedback logic (that's Phase 4 now)\n// Removed hardcoded tech stack, uses positive language\nconst input = $json;\nconst planDocument = input.plan_document || '';\n\nlet taskDescription = input.task?.description || input.description || '';\nif (!taskDescription && input.task?.raw_output) {\n  const raw = input.task.raw_output;\n  const cleaned = raw.replace(/<\\/?task>/g, '').replace(/\\n+(?:Reasoning|Note):[\\s\\S]*/i, '').trim();\n  try {\n    const parsed = JSON.parse(cleaned);\n    const list = Array.isArray(parsed) ? parsed : (parsed.tasks || []);\n    taskDescription = list.map(t => `${t.task_id}: ${t.description}`).join('\\n\\n');\n  } catch(e) { taskDescription = raw; }\n}\n\nconst taskFiles = (input.task || {}).files || [];\nconst filesConstraint = taskFiles.length > 0\n  ? '\\n\\nFILES TO MODIFY (only output these files):\\n' + taskFiles.join('\\n')\n  : '';\n\nconst existingFiles = input.existing_files || [];\nconst existingFilesSection = existingFiles.length > 0\n  ? '\\n\\nFULL PROJECT CONTEXT (all files in the project \u2014 read these for context but ONLY output files listed in FILES TO MODIFY):\\n' +\n    existingFiles.map(f => `### ${f.path}\\n\\`\\`\\`\\n${f.content}\\n\\`\\`\\``).join('\\n\\n')\n  : '';\n\nconst researchDocs = input.research_docs || '';\nconst hasDependencyManifest = researchDocs.includes('## Dependency Manifest');\nconst depRule = hasDependencyManifest\n  ? '- CRITICAL: Only use packages found in the Dependency Manifest below. If you need functionality from a package not in the manifest, use built-in browser/Node APIs or the listed packages instead.'\n  : '- CRITICAL: No package.json exists yet. Only use packages that are explicitly named in the Task description above. For anything not named, use built-in browser/Node APIs instead.';\n\nconst userContent = `Task:\\n${taskDescription}${filesConstraint}${existingFilesSection}`;\n\nlet systemMessage = `You are a Senior Full-Stack Developer. Output ONLY the files that need to be created or changed. Skip files that are already correct.\\n\\nFor EACH file you need to create or modify, output EXACTLY this format:\\n### path/to/file.ts\\n\\`\\`\\`ts\\n[complete file content]\\n\\`\\`\\`\\n\\nRULES:\\n- Only output files that this task requires you to create or change\\n- Output complete file content (not diffs)\\n- No explanations, no numbered lists, no prose \u2014 ONLY the file blocks\\n- Only output files listed in the FILES TO MODIFY section\\n- CRITICAL: Implement EXACTLY what the Task specifies. Use the exact text strings, component names, props, and content listed. Never substitute generic placeholders for specified content.\\n- ${depRule.replace(/^- /, '')}\\n- CRITICAL: Match import styles to the source module. Use named imports ({ X }) for named exports, default imports for default exports. Check the EXISTING PROJECT FILES to confirm each module's export style.\\n- Preserve each file's existing export style (named vs default) when modifying it`;\n\nif (planDocument) {\n  systemMessage += `\\n\\nARCHITECTURE REFERENCE (follow this plan):\\n${planDocument}`;\n}\n\nif (researchDocs) {\n  systemMessage += `\\n\\nPROJECT CONTEXT (from research agent):\\n${researchDocs}`;\n}\n\nreturn [{ json: {\n  model: $env.CODER_MODEL || 'qwen/qwen3-coder-next',\n  messages: [\n    { role: 'system', content: systemMessage },\n    { role: 'user', content: userContent }\n  ],\n  temperature: 0.05,\n  top_p: 0.2,\n  top_k: 20,\n  min_p: 0.0,\n  presence_penalty: 0.0,\n  repeat_penalty: 1.05,\n  max_tokens: 16384\n} }];"
        },
        "id": "cw-prepare",
        "name": "CW: Prepare Message",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          2450,
          300
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "={{ $env.LM_STUDIO_URL || 'http://10.0.0.100:1234/v1/chat/completions' }}",
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify($json) }}",
          "options": {
            "timeout": 300000
          },
          "sendHeaders": true,
          "headerParameters": {
            "parameters": [
              {
                "name": "Authorization",
                "value": "={{ 'Bearer ' + $env.LLM_API_KEY }}"
              }
            ]
          }
        },
        "id": "cw-llm",
        "name": "CW: Call LM Studio",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          2650,
          300
        ]
      },
      {
        "parameters": {
          "jsCode": "const raw = $json.choices[0].message.content || $json.choices[0].message.reasoning_content || '';\nlet content = raw.replace(/<think>[\\s\\S]*?<\\/think>/g, '').trim();\nif (content.includes('</think>')) {\n  content = content.split('</think>').pop().trim();\n}\nconst files = [];\nconst seen = new Set();\nconst re = /###\\s+((?:[\\w.-]+\\/)*(?:\\.\\w[\\w.-]*|[\\w.-]+\\.(?:ts|tsx|js|jsx|json|md|yml|yaml|env|prisma|css|html|sh|txt|lock|toml|cfg|ini)|Dockerfile|Makefile|LICENSE|CHANGELOG))\\s*\\n```[\\w]*\\n([\\s\\S]*?)```/g;\nlet m;\nwhile ((m = re.exec(content)) !== null) {\n  const path = m[1].trim();\n  const fileContent = m[2];\n  if (path && !seen.has(path)) {\n    seen.add(path);\n    files.push({ path, content: fileContent });\n  }\n}\nconst staticData = $getWorkflowStaticData('global');\nconst projectId = staticData._currentProjectId || $('Extract Input').first().json.project_id || 'unknown';\nconst task = staticData._currentTask || {};\nconst projectGoal = staticData._currentProjectGoal || $('Extract Input').first().json.message || '';\nreturn [{ json: { files, task, project_id: projectId, project_goal: projectGoal } }];"
        },
        "id": "cw-parse",
        "name": "CW: Parse Response",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          2850,
          300
        ]
      },
      {
        "parameters": {
          "jsCode": "// NEW: Replaces Chunk: Prepare Write\n// Simplified write prep + updates _allFileContents cache so later chunks see fresh content\nconst stashed = $('P2: Stash Context').first().json;\nconst chunkFiles = stashed.chunk_files || [];\nconst newFiles = $json.files || [];\nconst staticData2 = $getWorkflowStaticData('global');\nconst projectId = $json.project_id || staticData2._currentProjectId || stashed.project_id;\n\n// Include all files the Code Writer produced\nlet filteredFiles = newFiles;\n\n// Handle package.json merge: preserve existing deps, add new ones\nconst staticData = $getWorkflowStaticData('global');\nconst allFiles = staticData._allFileContents || [];\nfor (const newFile of filteredFiles) {\n  if (newFile.path === 'package.json' || newFile.path.endsWith('/package.json')) {\n    const existing = allFiles.find(f => f.path === newFile.path);\n    if (existing) {\n      try {\n        const existingPkg = JSON.parse(existing.content);\n        const newPkg = JSON.parse(newFile.content);\n        newPkg.dependencies = { ...(existingPkg.dependencies || {}), ...(newPkg.dependencies || {}) };\n        newPkg.devDependencies = { ...(existingPkg.devDependencies || {}), ...(newPkg.devDependencies || {}) };\n        newFile.content = JSON.stringify(newPkg, null, 2);\n      } catch(e) {}\n    }\n  }\n}\n\n// Update the in-memory file cache so later chunks see fresh content\nfor (const newFile of filteredFiles) {\n  const idx = allFiles.findIndex(f => f.path === newFile.path);\n  if (idx >= 0) {\n    allFiles[idx] = { path: newFile.path, content: newFile.content };\n  } else {\n    allFiles.push({ path: newFile.path, content: newFile.content });\n  }\n}\nstaticData._allFileContents = allFiles;\n\nif (filteredFiles.length === 0) {\n  return [{ json: { project_id: projectId, files: [], _skipWrite: true } }];\n}\n\nreturn [{ json: { project_id: projectId, files: filteredFiles } }];"
        },
        "id": "p2-prepare-write",
        "name": "P2: Prepare Write",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          3050,
          300
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "={{ ($env.FILE_API_URL || 'http://file-api:3456') + '/projects/' + $json.project_id + '/files-batch' }}",
          "sendHeaders": true,
          "headerParameters": {
            "parameters": [
              {
                "name": "Authorization",
                "value": "={{ 'Bearer ' + ($env.FILE_API_TOKEN || '') }}"
              },
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          },
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify({ files: $json.files }) }}",
          "options": {
            "response": {
              "response": {
                "neverError": true
              }
            }
          }
        },
        "id": "p2-write-files",
        "name": "P2: Write Files",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          3250,
          300
        ],
        "onError": "continueRegularOutput"
      },
      {
        "parameters": {
          "jsCode": "// P2: Store Result \u2014 uses $json like all other Code nodes\nconst staticData = $getWorkflowStaticData('global');\nstaticData._queueDone = (staticData._queueDone || 0) + 1;\nconst total = staticData._queueTotal || 0;\nconst done = staticData._queueDone;\nconst remaining = total - done;\n\nconst taskId = staticData._currentTaskId || 'unknown';\nconst writtenFiles = ($json.files_written || $json.files || []).map(f => typeof f === 'string' ? f : (f.path || JSON.stringify(f)));\nif (!staticData.p2Results) staticData.p2Results = [];\nstaticData.p2Results.push({ task_id: taskId, files_written: writtenFiles });\n\n\n// Status callback to Forge \u2014 fire and forget\ntry {\n  const http = require('http');\n  const cbBody = JSON.stringify({\n    event: 'task_written',\n    project_id: staticData._currentProjectId || 'unknown',\n    data: { task_id: taskId, files: writtenFiles }\n  });\n  const cbReq = http.request({\n    hostname: 'forge', port: 3500, path: '/api/status-callback',\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(cbBody) }\n  });\n  cbReq.on('error', () => {});\n  cbReq.write(cbBody);\n  cbReq.end();\n} catch(e) {}\n\nconst remainingQueue = staticData._currentQueue || [];\nconst isDone = remaining <= 0 || remainingQueue.length === 0;\n\nlet nextItem = null;\nif (!isDone) {\n  nextItem = JSON.parse(JSON.stringify(remainingQueue[0]));\n  nextItem._p2Queue = remainingQueue.slice(1);\n}\n\nreturn [{ json: {\n  _done: isDone,\n  _nextItem: nextItem,\n  _remaining: remaining,\n  task_id: taskId,\n  files_written: writtenFiles\n} }];"
        },
        "id": "p2-store",
        "name": "P2: Store Result",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          3450,
          300
        ]
      },
      {
        "parameters": {
          "method": "GET",
          "url": "={{ ($env.FILE_API_URL || 'http://file-api:3456') + '/projects/' + $('Prepare Planner Input').first().json.project_id + '/files-content' }}",
          "sendHeaders": true,
          "headerParameters": {
            "parameters": [
              {
                "name": "Authorization",
                "value": "={{ 'Bearer ' + ($env.FILE_API_TOKEN || '') }}"
              }
            ]
          },
          "options": {}
        },
        "id": "p3-refetch",
        "name": "P3: Re-fetch All Files",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          3850,
          500
        ],
        "onError": "continueRegularOutput"
      },
      {
        "parameters": {
          "jsCode": "// Full project review with VL model \u2014 sees code AND screenshot\nconst allFiles = $json.files || [];\nconst staticData = $getWorkflowStaticData('global');\nconst plannerInput = $('Prepare Planner Input').first().json;\nconst projectGoal = plannerInput.project_goal || '';\nconst deps = staticData._projectDeps || [];\nconst buildResult = staticData._buildResult || { success: true };\nconst screenshot_b64 = staticData._screenshot_b64 || null;\nconst refImage = $('Extract Input').first().json.image_data || null;\n\nconst allFilesFormatted = allFiles\n  .filter(f => f.path.match(/\\.(ts|tsx|js|jsx|json|css|html)$/) && !f.path.includes('node_modules'))\n  .map(f => '### ' + f.path + '\\n```\\n' + f.content + '\\n```')\n  .join('\\n\\n');\n\nconst buildSection = buildResult.success\n  ? '\\nBUILD STATUS: \u2705 Project builds successfully.\\n'\n  : '\\nBUILD STATUS: \u274c BUILD FAILED \u2014 THIS IS THE HIGHEST PRIORITY FIX:\\n' +\n    'Stage: ' + (buildResult.stage || 'unknown') + '\\n' +\n    'Error: ' + (buildResult.error || 'unknown') + '\\n' +\n    'Output:\\n' + (buildResult.output || '') + '\\n' +\n    'You MUST flag this as a critical severity fix.\\n';\n\nconst textPrompt = 'You are a Senior Code Reviewer with UI/UX expertise performing a FULL PROJECT REVIEW.\\n' +\n  'You are reviewing ALL files AND the visual output of the project.\\n\\n' +\n  'Your job is to find issues across code AND visuals:\\n' +\n  '1. IMPORT/EXPORT MISMATCHES: imports that reference exports that don\\'t exist, wrong import style (named vs default)\\n' +\n  '2. TYPE MISMATCHES: function signatures that don\\'t match their call sites\\n' +\n  '3. DEPENDENCY ISSUES: imports of packages not in package.json\\n' +\n  '4. MISSING FILES: components or modules imported but never created\\n' +\n  '5. CRITICAL BUGS: null reference risks, unhandled promise rejections\\n' +\n  '6. ASSET/STYLE LOADING: verify entry point imports stylesheet, Tailwind classes are compiled\\n' +\n  '7. BUILD CHAIN: configs use consistent module format, all deps listed\\n' +\n  '8. DEAD FILES: duplicate entry points, orphaned files\\n' +\n  '9. VISUAL ISSUES: compare the screenshot with the design intent \u2014 is the layout correct? Are components visible? Colors applied? Anything broken visually?\\n\\n' +\n  'Project Goal: ' + projectGoal + '\\n' +\n  buildSection + '\\n' +\n  'ALL PROJECT FILES:\\n' + allFilesFormatted + '\\n\\n' +\n  'Return ONLY valid JSON:\\n' +\n  '{\\n  \"overall_quality\": number (0-100),\\n  \"cross_file_consistent\": boolean,\\n' +\n  '  \"visual_quality\": number (0-100),\\n' +\n  '  \"fixes_needed\": [\\n    {\\n      \"file\": \"path\",\\n      \"severity\": \"critical|high|medium\",\\n' +\n  '      \"issue\": \"description\",\\n      \"fix_instruction\": \"specific fix\",\\n      \"related_files\": [\"path\"]\\n    }\\n  ],\\n' +\n  '  \"visual_issues\": [\"specific visual problem 1\", \"specific visual problem 2\"],\\n' +\n  '  \"summary\": \"overall assessment including visual review\"\\n}';\n\n// Build multimodal content array\nconst content = [];\nif (refImage) {\n  content.push({ type: 'image_url', image_url: { url: 'data:image/png;base64,' + refImage } });\n  content.push({ type: 'text', text: 'REFERENCE IMAGE (what the user wants it to look like):' });\n}\nif (screenshot_b64) {\n  content.push({ type: 'image_url', image_url: { url: 'data:image/png;base64,' + screenshot_b64 } });\n  content.push({ type: 'text', text: 'ACTUAL SCREENSHOT of the built application:' });\n}\ncontent.push({ type: 'text', text: textPrompt });\n\n// Use multimodal array if we have images, plain text if not\nconst messageContent = content.length > 1 ? content : textPrompt;\n\nreturn [{\n  json: {\n    model: $env.PLANNER_MODEL || 'qwen/qwen3-vl-32b',\n    messages: [{ role: 'user', content: messageContent }],\n    temperature: 0.3,\n    top_p: 0.6,\n    max_tokens: 8192\n  }\n}];"
        },
        "id": "p3-review-build",
        "name": "P3: Full Review Build",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          4050,
          500
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "={{ $env.LM_STUDIO_URL || 'http://10.0.0.100:1234/v1/chat/completions' }}",
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify($json) }}",
          "options": {
            "timeout": 600000
          },
          "sendHeaders": true,
          "headerParameters": {
            "parameters": [
              {
                "name": "Authorization",
                "value": "={{ 'Bearer ' + $env.LLM_API_KEY }}"
              }
            ]
          }
        },
        "id": "p3-review-llm",
        "name": "P3: Review LLM",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          4250,
          500
        ],
        "onError": "continueRegularOutput"
      },
      {
        "parameters": {
          "jsCode": "// Parses cross-file review format with fixes_needed[]\nconst raw = ($json.choices && $json.choices[0] && $json.choices[0].message) ? ($json.choices[0].message.content || $json.choices[0].message.reasoning_content || '') : '';\nlet content = raw.replace(/<think>[\\s\\S]*?<\\/think>/g, '').trim();\nif (content.includes('</think>')) content = content.split('</think>').pop().trim();\ncontent = content.replace(/^```(?:json)?\\n?/, '').replace(/\\n?```$/, '').trim();\nconst jsonMatch = content.match(/\\{[\\s\\S]*\\}/);\nlet review;\ntry {\n  review = JSON.parse(jsonMatch ? jsonMatch[0] : content);\n} catch (e) {\n  review = { overall_quality: 0, cross_file_consistent: true, fixes_needed: [], summary: 'Review parse failed: ' + content.substring(0, 200) };\n}\n\nconst fixes = review.fixes_needed || [];\nconst criticalFixCount = fixes.filter(f => f.severity === 'critical' || f.severity === 'high').length;\nconst visualQuality = review.visual_quality || 0;\nconst visualIssues = review.visual_issues || [];\nconst projectId = $('Prepare Planner Input').first().json.project_id;\n\nconst staticData = $getWorkflowStaticData('global');\nstaticData._reviewResult = review;\n\nreturn [{ json: {\n  review,\n  critical_fix_count: criticalFixCount,\n  fixes_needed: fixes,\n  project_id: projectId\n} }];"
        },
        "id": "p3-review-parse",
        "name": "P3: Review Parse",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          4450,
          500
        ]
      },
      {
        "parameters": {
          "conditions": {
            "options": {
              "caseSensitive": true,
              "leftValue": "",
              "typeValidation": "strict"
            },
            "conditions": [
              {
                "id": "fix-check",
                "leftValue": "={{ $json.critical_fix_count }}",
                "rightValue": 0,
                "operator": {
                  "type": "number",
                  "operation": "gt"
                }
              }
            ]
          }
        },
        "id": "p3-needs-fix",
        "name": "P3: Needs Fix?",
        "type": "n8n-nodes-base.if",
        "typeVersion": 2,
        "position": [
          4650,
          500
        ]
      },
      {
        "parameters": {
          "jsCode": "// NEW: Builds targeted fix prompt from review feedback\n// Only files flagged in fixes_needed get sent to the coder\nconst fixes = $json.fixes_needed || [];\nconst projectId = $json.project_id;\nconst staticData = $getWorkflowStaticData('global');\nconst allFiles = staticData._allFileContents || [];\n\n// Collect files to fix + related context files\nconst filesToFix = new Set(fixes.map(f => f.file));\nconst contextFiles = new Set();\nfixes.forEach(f => (f.related_files || []).forEach(rf => contextFiles.add(rf)));\n\n// Build fix instructions\nconst fixInstructions = fixes.map(f =>\n  `FILE: ${f.file}\\nSEVERITY: ${f.severity}\\nISSUE: ${f.issue}\\nFIX: ${f.fix_instruction}`\n).join('\\n\\n');\n\n// Build existing files section\nconst relevantPaths = new Set([...filesToFix, ...contextFiles]);\nconst existingFilesSection = allFiles\n  .filter(f => relevantPaths.has(f.path))\n  .map(f => {\n    const marker = filesToFix.has(f.path) ? ' (NEEDS FIX)' : ' (CONTEXT ONLY - read but do not output)';\n    return `### ${f.path}${marker}\\n\\`\\`\\`\\n${f.content}\\n\\`\\`\\``;\n  })\n  .join('\\n\\n');\n\nconst filesToOutput = [...filesToFix];\n\nlet systemMessage = `You are a Senior Full-Stack Developer. You are fixing specific issues found during code review.\\n\\nFor EACH file you fix, output EXACTLY this format:\\n### path/to/file.ts\\n\\`\\`\\`ts\\n[complete file content]\\n\\`\\`\\`\\n\\nRULES:\\n- Only output files marked as NEEDS FIX\\n- Output complete file content (not diffs)\\n- No explanations, no prose \u2014 ONLY the file blocks\\n- CRITICAL: Only use packages found in the Dependency Manifest below.\\n- CRITICAL: Match import styles to the source module exactly.`;\n\nconst researchDocs = staticData._researchDocs || '';\nif (researchDocs) {\n  systemMessage += `\\n\\nPROJECT CONTEXT:\\n${researchDocs}`;\n}\n\nconst userContent = `Fix the following issues found during code review:\\n\\n${fixInstructions}\\n\\nFILES TO FIX (only output these):\\n${filesToOutput.join('\\n')}\\n\\nCURRENT FILE CONTENTS:\\n${existingFilesSection}`;\n\nreturn [{ json: {\n  model: $env.FIXER_MODEL || 'mistralai/devstral-small-2-2512',\n  messages: [\n    { role: 'system', content: systemMessage },\n    { role: 'user', content: userContent }\n  ],\n  temperature: 0.1,\n  top_p: 0.25,\n  top_k: 20,\n  min_p: 0.0,\n  presence_penalty: 0.0,\n  repeat_penalty: 1.05,\n  max_tokens: 16384,\n  _project_id: projectId\n} }];"
        },
        "id": "p4-fix-build",
        "name": "P4: Fix Build",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          4850,
          400
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "={{ $env.LM_STUDIO_URL || 'http://10.0.0.100:1234/v1/chat/completions' }}",
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify($json) }}",
          "options": {
            "timeout": 300000
          },
          "sendHeaders": true,
          "headerParameters": {
            "parameters": [
              {
                "name": "Authorization",
                "value": "={{ 'Bearer ' + $env.LLM_API_KEY }}"
              }
            ]
          }
        },
        "id": "p4-fix-llm",
        "name": "P4: Fix LLM",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          5050,
          400
        ]
      },
      {
        "parameters": {
          "jsCode": "// NEW: Parse fix response, update cache, prepare for write\nconst raw = $json.choices[0].message.content || $json.choices[0].message.reasoning_content || '';\nlet content = raw.replace(/<think>[\\s\\S]*?<\\/think>/g, '').trim();\nif (content.includes('</think>')) {\n  content = content.split('</think>').pop().trim();\n}\nconst files = [];\nconst seen = new Set();\nconst re = /###\\s+((?:[\\w.-]+\\/)*(?:\\.\\w[\\w.-]*|[\\w.-]+\\.(?:ts|tsx|js|jsx|json|md|yml|yaml|env|prisma|css|html|sh|txt|lock|toml|cfg|ini)|Dockerfile|Makefile|LICENSE|CHANGELOG))\\s*\\n```[\\w]*\\n([\\s\\S]*?)```/g;\nlet m;\nwhile ((m = re.exec(content)) !== null) {\n  const path = m[1].trim();\n  const fileContent = m[2];\n  if (path && !seen.has(path)) {\n    seen.add(path);\n    files.push({ path, content: fileContent });\n  }\n}\n\n// Update in-memory cache\nconst staticData = $getWorkflowStaticData('global');\nconst allFiles = staticData._allFileContents || [];\nfor (const newFile of files) {\n  const idx = allFiles.findIndex(f => f.path === newFile.path);\n  if (idx >= 0) {\n    allFiles[idx] = { path: newFile.path, content: newFile.content };\n  } else {\n    allFiles.push({ path: newFile.path, content: newFile.content });\n  }\n}\nstaticData._allFileContents = allFiles;\nstaticData._fixResults = files.map(f => f.path);\n\nconst projectId = $('Prepare Planner Input').first().json.project_id;\n\nif (files.length === 0) {\n  return [{ json: { project_id: projectId, files: [], _skipWrite: true } }];\n}\nreturn [{ json: { project_id: projectId, files } }];"
        },
        "id": "p4-fix-parse",
        "name": "P4: Fix Parse",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          5250,
          400
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "={{ ($env.FILE_API_URL || 'http://file-api:3456') + '/projects/' + $json.project_id + '/files-batch' }}",
          "sendHeaders": true,
          "headerParameters": {
            "parameters": [
              {
                "name": "Authorization",
                "value": "={{ 'Bearer ' + ($env.FILE_API_TOKEN || '') }}"
              },
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          },
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify({ files: $json.files }) }}",
          "options": {
            "response": {
              "response": {
                "neverError": true
              }
            }
          }
        },
        "id": "p4-fix-write",
        "name": "P4: Fix Write",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          5450,
          400
        ],
        "onError": "continueRegularOutput"
      },
      {
        "parameters": {
          "jsCode": "// MODIFIED: Includes review + fix results from Phase 3 & 4\nconst staticData = $getWorkflowStaticData('global');\nconst plannerInput = $('Prepare Planner Input').first().json;\nconst p2Results = staticData.p2Results || [];\nconst reviewResult = staticData._reviewResult || {};\nconst fixResults = staticData._fixResults || [];\nconst finalReview = staticData._finalReview || {};\nconst allTasks = staticData.allTasks || [];\n\n// Build task-level results from p2Results\nconst taskResults = new Map();\nfor (const r of p2Results) {\n  if (!taskResults.has(r.task_id)) {\n    const task = allTasks.find(t => t.task_id === r.task_id);\n    taskResults.set(r.task_id, {\n      task_id: r.task_id,\n      description: task ? task.description.substring(0, 200) : '',\n      files_written: [],\n      chunks_processed: 0\n    });\n  }\n  const entry = taskResults.get(r.task_id);\n  entry.files_written.push(...(r.files_written || []));\n  entry.chunks_processed++;\n}\n\nconst allFilesWritten = p2Results.flatMap(r => r.files_written || []);\n\nreturn [{ json: {\n  status: 'completed',\n  project_id: plannerInput.project_id,\n  project_goal: plannerInput.project_goal,\n  tasks_completed: taskResults.size,\n  task_results: [...taskResults.values()],\n  review: {\n    overall_quality: reviewResult.overall_quality || 0,\n    cross_file_consistent: reviewResult.cross_file_consistent || false,\n    fixes_applied: fixResults,\n    summary: reviewResult.summary || ''\n  },\n  files_written: [...new Set(allFilesWritten)],\n  final_review: {\n    quality: finalReview.final_quality || 0,\n    summary: finalReview.summary || '',\n    suggestions: finalReview.suggestions || []\n  }\n} }];"
        },
        "id": "aggregate",
        "name": "Aggregate All Results",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          5650,
          500
        ]
      },
      {
        "parameters": {
          "jsCode": "const result = $json;\nreturn [{ json: {\n  operation: 'set',\n  project_id: result.project_id,\n  state: {\n    status: 'completed',\n    completed_tasks: result.task_results.map(t => t.task_id),\n    files: result.files_written\n  }\n} }];"
        },
        "id": "prepare-mem-update",
        "name": "Prepare Memory Update",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          5850,
          500
        ]
      },
      {
        "parameters": {
          "jsCode": "const op = $json;\nconst store = $getWorkflowStaticData('global');\nconst projectId = op.project_id;\nif (op.operation === 'set') {\n  const existing = store[projectId] || {};\n  store[projectId] = { ...existing, ...op.state, project_id: projectId, last_updated: new Date().toISOString() };\n}\nreturn [{ json: store[projectId] || { project_id: projectId } }];"
        },
        "id": "update-memory",
        "name": "Update Memory",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          6050,
          500
        ]
      },
      {
        "parameters": {
          "jsCode": "const aggregate = $('Aggregate All Results').first().json;\nreturn [{ json: aggregate }];"
        },
        "id": "build-response",
        "name": "Build Response",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          6250,
          500
        ]
      },
      {
        "parameters": {
          "conditions": {
            "options": {
              "caseSensitive": true,
              "leftValue": "",
              "typeValidation": "strict"
            },
            "conditions": [
              {
                "id": "p0-ref-url-check",
                "leftValue": "={{ $('Extract Input').first().json.reference_url }}",
                "rightValue": "",
                "operator": {
                  "type": "string",
                  "operation": "notEmpty"
                }
              }
            ],
            "combinator": "and"
          },
          "options": {}
        },
        "id": "p0-has-reference-url",
        "name": "P0: Has Reference URL?",
        "type": "n8n-nodes-base.if",
        "typeVersion": 2,
        "position": [
          1050,
          500
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "={{ ($env.FILE_API_URL || 'http://file-api:3456') + '/scrape' }}",
          "sendHeaders": true,
          "headerParameters": {
            "parameters": [
              {
                "name": "Authorization",
                "value": "={{ 'Bearer ' + ($env.FILE_API_TOKEN || '') }}"
              }
            ]
          },
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify({ url: $('Extract Input').first().json.reference_url }) }}",
          "options": {
            "timeout": 30000
          }
        },
        "id": "p0-scrape-url",
        "name": "P0: Scrape URL",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          1250,
          600
        ],
        "onError": "continueRegularOutput"
      },
      {
        "parameters": {
          "jsCode": "const scrapeData = $json;\nconst staticData = $getWorkflowStaticData('global');\nif (scrapeData && scrapeData.screenshot_b64) {\n  staticData._scrapeData = scrapeData;\n} else {\n  staticData._scrapeData = null;\n}\nreturn [{ json: $('Prepare Planner Input').first().json }];"
        },
        "id": "p0-merge-scrape-data",
        "name": "P0: Merge Scrape Data",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          1450,
          600
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "={{ ($env.LM_STUDIO_HOST || 'http://10.0.0.100:1234') + '/api/v1/models/load' }}",
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify({ model: $env.PLANNER_MODEL || 'qwen/qwen3-vl-32b' , context_length: 32768 }) }}",
          "options": {
            "timeout": 120000
          },
          "sendHeaders": true,
          "headerParameters": {
            "parameters": [
              {
                "name": "Authorization",
                "value": "={{ 'Bearer ' + ($env.LLM_API_KEY || '') }}"
              }
            ]
          }
        },
        "id": "load-planner-model",
        "name": "Load Planner Model",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          1150,
          100
        ],
        "onError": "continueRegularOutput"
      },
      {
        "parameters": {
          "jsCode": "const loadResp = $json;\nif (loadResp.status !== 'loaded') throw new Error('Load Planner Model failed: ' + JSON.stringify(loadResp));\nconst plannerInput = $('Prepare Planner Input').first().json;\nconst staticData = $getWorkflowStaticData('global');\nif (!$('Extract Input').first().json.reference_url) {\n  staticData._scrapeData = null;\n}\nreturn [{ json: { ...plannerInput, _scrapeData: staticData._scrapeData || null } }];"
        },
        "id": "restore-planner-input",
        "name": "Restore: Planner Input",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          1350,
          100
        ]
      },
      {
        "parameters": {
          "jsCode": "const http = require('http');\nfunction lmsRequest(method, path, body) {\n  return new Promise((resolve, reject) => {\n    const lmsHost = ($env.LM_STUDIO_HOST || 'http://10.0.0.100:1234').replace(/^https?:\\/\\//, '');\n    const [hostname, portStr] = lmsHost.split(':');\n    const bodyStr = body ? JSON.stringify(body) : null;\n    const req = http.request({\n      hostname, port: parseInt(portStr) || 1234, path, method,\n      headers: {\n        Authorization: 'Bearer ' + ($env.LLM_API_KEY || ''),\n        'Content-Type': 'application/json',\n        ...(bodyStr ? { 'Content-Length': Buffer.byteLength(bodyStr) } : {})\n      }\n    }, res => {\n      let d = ''; res.on('data', c => d += c);\n      res.on('end', () => { try { resolve(JSON.parse(d)); } catch(e) { resolve({}); } });\n    });\n    req.on('error', reject);\n    if (bodyStr) req.write(bodyStr);\n    req.end();\n  });\n}\n\nconst modelKey = $env.PLANNER_MODEL || '';\nconst staticData = $getWorkflowStaticData('global');\ntry {\n  const resp = await lmsRequest('GET', '/api/v1/models');\n  for (const m of (resp.models || [])) {\n    if (m.key === modelKey) {\n      for (const inst of (m.loaded_instances || [])) {\n        try { await lmsRequest('POST', '/api/v1/models/unload', { instance_id: inst.id }); } catch(e) {}\n      }\n    }\n  }\n} catch(e) { console.error('Unload error:', e.message); }\nstaticData._plannerInstanceId = null;\nreturn [{ json: $('Planner: Parse Response').first().json }];"
        },
        "id": "unload-planner-model",
        "name": "Unload Planner Model",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          1750,
          100
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "={{ ($env.LM_STUDIO_HOST || 'http://10.0.0.100:1234') + '/api/v1/models/load' }}",
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify({ model: $env.CODER_MODEL || 'qwen/qwen3-coder-next' , context_length: 65536 }) }}",
          "options": {
            "timeout": 120000
          },
          "sendHeaders": true,
          "headerParameters": {
            "parameters": [
              {
                "name": "Authorization",
                "value": "={{ 'Bearer ' + ($env.LLM_API_KEY || '') }}"
              }
            ]
          }
        },
        "id": "load-coder-model",
        "name": "Load Coder Model",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          1950,
          100
        ],
        "onError": "continueRegularOutput"
      },
      {
        "parameters": {
          "jsCode": "const loadResp = $json;\nif (loadResp.status !== 'loaded') throw new Error('Load Coder Model failed: ' + JSON.stringify(loadResp));\nconst staticData = $getWorkflowStaticData('global');\nstaticData._coderInstanceId = loadResp.instance_id;\nreturn [{ json: $('Planner: Parse Response').first().json }];"
        },
        "id": "restore-after-load-coder",
        "name": "Restore: After Load Coder",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          2150,
          100
        ]
      },
      {
        "parameters": {
          "jsCode": "const http = require('http');\nfunction lmsRequest(method, path, body) {\n  return new Promise((resolve, reject) => {\n    const lmsHost = ($env.LM_STUDIO_HOST || 'http://10.0.0.100:1234').replace(/^https?:\\/\\//, '');\n    const [hostname, portStr] = lmsHost.split(':');\n    const bodyStr = body ? JSON.stringify(body) : null;\n    const req = http.request({\n      hostname, port: parseInt(portStr) || 1234, path, method,\n      headers: {\n        Authorization: 'Bearer ' + ($env.LLM_API_KEY || ''),\n        'Content-Type': 'application/json',\n        ...(bodyStr ? { 'Content-Length': Buffer.byteLength(bodyStr) } : {})\n      }\n    }, res => {\n      let d = ''; res.on('data', c => d += c);\n      res.on('end', () => { try { resolve(JSON.parse(d)); } catch(e) { resolve({}); } });\n    });\n    req.on('error', reject);\n    if (bodyStr) req.write(bodyStr);\n    req.end();\n  });\n}\n\nconst modelKey = $env.CODER_MODEL || '';\nconst staticData = $getWorkflowStaticData('global');\ntry {\n  const resp = await lmsRequest('GET', '/api/v1/models');\n  for (const m of (resp.models || [])) {\n    if (m.key === modelKey) {\n      for (const inst of (m.loaded_instances || [])) {\n        try { await lmsRequest('POST', '/api/v1/models/unload', { instance_id: inst.id }); } catch(e) {}\n      }\n    }\n  }\n} catch(e) { console.error('Unload error:', e.message); }\nstaticData._coderInstanceId = null;\nreturn [{ json: $json }];"
        },
        "id": "unload-coder-model",
        "name": "Unload Coder Model",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          3750,
          100
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "={{ ($env.LM_STUDIO_HOST || 'http://10.0.0.100:1234') + '/api/v1/models/load' }}",
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify({ model: $env.PLANNER_MODEL || 'qwen/qwen3-vl-32b' , context_length: 32768 }) }}",
          "options": {
            "timeout": 120000
          },
          "sendHeaders": true,
          "headerParameters": {
            "parameters": [
              {
                "name": "Authorization",
                "value": "={{ 'Bearer ' + ($env.LLM_API_KEY || '') }}"
              }
            ]
          }
        },
        "id": "load-reviewer-model",
        "name": "Load Reviewer Model",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          3950,
          100
        ],
        "onError": "continueRegularOutput"
      },
      {
        "parameters": {
          "jsCode": "const loadResp = $json;\nif (loadResp.status !== 'loaded') throw new Error('Load Reviewer Model failed: ' + JSON.stringify(loadResp));\nconst staticData = $getWorkflowStaticData('global');\nstaticData._reviewerInstanceId = loadResp.instance_id;\nreturn [{ json: { project_id: $('Extract Input').first().json.project_id } }];"
        },
        "id": "restore-after-load-reviewer",
        "name": "Restore: After Load Reviewer",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          4150,
          100
        ]
      },
      {
        "parameters": {
          "jsCode": "const http = require('http');\nfunction lmsRequest(method, path, body) {\n  return new Promise((resolve, reject) => {\n    const lmsHost = ($env.LM_STUDIO_HOST || 'http://10.0.0.100:1234').replace(/^https?:\\/\\//, '');\n    const [hostname, portStr] = lmsHost.split(':');\n    const bodyStr = body ? JSON.stringify(body) : null;\n    const req = http.request({\n      hostname, port: parseInt(portStr) || 1234, path, method,\n      headers: {\n        Authorization: 'Bearer ' + ($env.LLM_API_KEY || ''),\n        'Content-Type': 'application/json',\n        ...(bodyStr ? { 'Content-Length': Buffer.byteLength(bodyStr) } : {})\n      }\n    }, res => {\n      let d = ''; res.on('data', c => d += c);\n      res.on('end', () => { try { resolve(JSON.parse(d)); } catch(e) { resolve({}); } });\n    });\n    req.on('error', reject);\n    if (bodyStr) req.write(bodyStr);\n    req.end();\n  });\n}\n\nconst modelKey = $env.PLANNER_MODEL || '';\nconst staticData = $getWorkflowStaticData('global');\ntry {\n  const resp = await lmsRequest('GET', '/api/v1/models');\n  for (const m of (resp.models || [])) {\n    if (m.key === modelKey) {\n      for (const inst of (m.loaded_instances || [])) {\n        try { await lmsRequest('POST', '/api/v1/models/unload', { instance_id: inst.id }); } catch(e) {}\n      }\n    }\n  }\n} catch(e) { console.error('Unload error:', e.message); }\nstaticData._reviewerInstanceId = null;\nreturn [{ json: $('P3: Review Parse').first().json }];"
        },
        "id": "unload-reviewer-model",
        "name": "Unload Reviewer Model",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          4350,
          100
        ]
      },
      {
        "parameters": {
          "jsCode": "return [{ json: $('P3: Review Parse').first().json }];"
        },
        "id": "restore-after-unload-reviewer",
        "name": "Restore: After Unload Reviewer",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          4550,
          100
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "={{ ($env.LM_STUDIO_HOST || 'http://10.0.0.100:1234') + '/api/v1/models/load' }}",
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify({ model: $env.FIXER_MODEL || 'mistralai/devstral-small-2-2512' , context_length: 65536 }) }}",
          "options": {
            "timeout": 120000
          },
          "sendHeaders": true,
          "headerParameters": {
            "parameters": [
              {
                "name": "Authorization",
                "value": "={{ 'Bearer ' + ($env.LLM_API_KEY || '') }}"
              }
            ]
          }
        },
        "id": "load-fixer-model",
        "name": "Load Fixer Model",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          4750,
          100
        ],
        "onError": "continueRegularOutput"
      },
      {
        "parameters": {
          "jsCode": "const loadResp = $json;\nif (loadResp.status !== 'loaded') throw new Error('Load Fixer Model failed: ' + JSON.stringify(loadResp));\nconst staticData = $getWorkflowStaticData('global');\nstaticData._fixerInstanceId = loadResp.instance_id;\nreturn [{ json: $('P3: Needs Fix?').first().json }];"
        },
        "id": "restore-after-load-fixer",
        "name": "Restore: After Load Fixer",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          4950,
          100
        ]
      },
      {
        "parameters": {
          "jsCode": "const http = require('http');\nfunction lmsRequest(method, path, body) {\n  return new Promise((resolve, reject) => {\n    const lmsHost = ($env.LM_STUDIO_HOST || 'http://10.0.0.100:1234').replace(/^https?:\\/\\//, '');\n    const [hostname, portStr] = lmsHost.split(':');\n    const bodyStr = body ? JSON.stringify(body) : null;\n    const req = http.request({\n      hostname, port: parseInt(portStr) || 1234, path, method,\n      headers: {\n        Authorization: 'Bearer ' + ($env.LLM_API_KEY || ''),\n        'Content-Type': 'application/json',\n        ...(bodyStr ? { 'Content-Length': Buffer.byteLength(bodyStr) } : {})\n      }\n    }, res => {\n      let d = ''; res.on('data', c => d += c);\n      res.on('end', () => { try { resolve(JSON.parse(d)); } catch(e) { resolve({}); } });\n    });\n    req.on('error', reject);\n    if (bodyStr) req.write(bodyStr);\n    req.end();\n  });\n}\n\nconst modelKey = $env.FIXER_MODEL || '';\nconst staticData = $getWorkflowStaticData('global');\ntry {\n  const resp = await lmsRequest('GET', '/api/v1/models');\n  for (const m of (resp.models || [])) {\n    if (m.key === modelKey) {\n      for (const inst of (m.loaded_instances || [])) {\n        try { await lmsRequest('POST', '/api/v1/models/unload', { instance_id: inst.id }); } catch(e) {}\n      }\n    }\n  }\n} catch(e) { console.error('Unload error:', e.message); }\nstaticData._fixerInstanceId = null;\nreturn [{ json: $('P4: Fix Write').first().json }];"
        },
        "id": "unload-fixer-model",
        "name": "Unload Fixer Model",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          5550,
          100
        ]
      },
      {
        "parameters": {
          "jsCode": "return [{ json: $('P4: Fix Write').first().json }];"
        },
        "id": "restore-after-unload-fixer",
        "name": "Restore: After Unload Fixer",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          5700,
          100
        ]
      },
      {
        "parameters": {
          "jsCode": "// Startup: Unload all loaded models before beginning pipeline\n// Prevents leftover models from crashed/cancelled runs eating VRAM\nconst http = require('http');\n\nfunction httpRequest(options, body) {\n  return new Promise((resolve, reject) => {\n    const req = http.request(options, (res) => {\n      let data = '';\n      res.on('data', chunk => data += chunk);\n      res.on('end', () => {\n        try { resolve(JSON.parse(data)); }\n        catch(e) { resolve(data); }\n      });\n    });\n    req.on('error', reject);\n    if (body) req.write(body);\n    req.end();\n  });\n}\n\nconst host = '10.0.0.100';\nconst port = 1234;\nconst apiKey = $env.LLM_API_KEY || '';\n\n// Get all loaded instances\nconst modelsResp = await httpRequest({\n  host, port,\n  path: '/api/v1/models',\n  method: 'GET',\n  headers: { 'Authorization': 'Bearer ' + apiKey }\n});\n\nconst unloaded = [];\nfor (const model of (modelsResp.models || [])) {\n  for (const inst of (model.loaded_instances || [])) {\n    const body = JSON.stringify({ instance_id: inst.id });\n    await httpRequest({\n      host, port,\n      path: '/api/v1/models/unload',\n      method: 'POST',\n      headers: {\n        'Authorization': 'Bearer ' + apiKey,\n        'Content-Type': 'application/json',\n        'Content-Length': Buffer.byteLength(body)\n      }\n    }, body);\n    unloaded.push(inst.id);\n  }\n}\n\nreturn [{ json: { ...$json, _startupUnloaded: unloaded } }];"
        },
        "id": "startup-unload-all",
        "name": "Startup: Unload All Models",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          950,
          100
        ]
      },
      {
        "id": "p2-continue-gate",
        "name": "P2: Continue Gate",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          3650,
          200
        ],
        "parameters": {
          "jsCode": "// Gate: pass through next item if NOT done, return [] if done\nif ($json._done === true || !$json._nextItem) {\n  return [];\n}\nreturn [{ json: $json._nextItem }];"
        }
      },
      {
        "id": "p2-exit-gate",
        "name": "P2: Exit Gate",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          3650,
          400
        ],
        "parameters": {
          "jsCode": "// Gate: pass through if done, return [] if NOT done\nif ($json._done !== true) {\n  return [];\n}\nreturn [{ json: { _remaining: 0, task_id: $json.task_id, files_written: $json.files_written } }];"
        }
      },
      {
        "id": "cb-pipeline-started",
        "name": "CB: Pipeline Started",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          300,
          500
        ],
        "parameters": {
          "jsCode": "// Status callback to Forge \u2014 fire and forget\nconst http = require('http');\nconst event = 'pipeline_started';\nconst projectId = $json.project_id || $getWorkflowStaticData('global')._currentProjectId || 'unknown';\nconst callbackData = { project_id: projectId };\n\ntry {\n  const body = JSON.stringify({ event, project_id: projectId, data: callbackData });\n  const req = http.request({\n    hostname: 'forge', port: 3500, path: '/api/status-callback',\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }\n  });\n  req.on('error', () => {});  // ignore errors \u2014 pipeline must not stop\n  req.write(body);\n  req.end();\n} catch(e) {}\n\nreturn [$input.all()[0]];  // pass through unchanged"
        }
      },
      {
        "id": "cb-planning-complete",
        "name": "CB: Planning Complete",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          1650,
          500
        ],
        "parameters": {
          "jsCode": "// Status callback to Forge \u2014 fire and forget\nconst http = require('http');\nconst event = 'planning_complete';\nconst projectId = $json.project_id || $getWorkflowStaticData('global')._currentProjectId || 'unknown';\nconst callbackData = { task_count: ($json.tasks || []).length, project_id: projectId };\n\ntry {\n  const body = JSON.stringify({ event, project_id: projectId, data: callbackData });\n  const req = http.request({\n    hostname: 'forge', port: 3500, path: '/api/status-callback',\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }\n  });\n  req.on('error', () => {});  // ignore errors \u2014 pipeline must not stop\n  req.write(body);\n  req.end();\n} catch(e) {}\n\n// Stash plan for retrieval after Wait node\nconst staticData2 = $getWorkflowStaticData('global');\nstaticData2._pendingPlan = $input.all()[0].json;\n\nreturn [$input.all()[0]];  // pass through unchanged"
        }
      },
      {
        "id": "cb-review-complete",
        "name": "CB: Review Complete",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          5050,
          500
        ],
        "parameters": {
          "jsCode": "// Status callback to Forge \u2014 fire and forget\nconst http = require('http');\nconst event = 'review_complete';\nconst projectId = $json.project_id || $getWorkflowStaticData('global')._currentProjectId || 'unknown';\nconst callbackData = { quality: $json.review?.overall_quality || 0, summary: ($json.review?.summary || '').substring(0, 1000), project_id: projectId };\n\ntry {\n  const body = JSON.stringify({ event, project_id: projectId, data: callbackData });\n  const req = http.request({\n    hostname: 'forge', port: 3500, path: '/api/status-callback',\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }\n  });\n  req.on('error', () => {});  // ignore errors \u2014 pipeline must not stop\n  req.write(body);\n  req.end();\n} catch(e) {}\n\nreturn [$input.all()[0]];  // pass through unchanged"
        }
      },
      {
        "id": "cb-pipeline-complete",
        "name": "CB: Pipeline Complete",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          6250,
          500
        ],
        "parameters": {
          "jsCode": "// Status callback to Forge \u2014 fire and forget\nconst http = require('http');\nconst event = 'pipeline_complete';\nconst projectId = $json.project_id || $getWorkflowStaticData('global')._currentProjectId || 'unknown';\nconst callbackData = { tasks_completed: $json.tasks_completed || 0, files_written: $json.files_written || [], summary: ($json.final_review?.summary || $json.review?.summary || '').substring(0, 1000), suggestions: $json.final_review?.suggestions || [], final_quality: $json.final_review?.quality || 0, project_id: projectId };\n\ntry {\n  const body = JSON.stringify({ event, project_id: projectId, data: callbackData });\n  const req = http.request({\n    hostname: 'forge', port: 3500, path: '/api/status-callback',\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }\n  });\n  req.on('error', () => {});  // ignore errors \u2014 pipeline must not stop\n  req.write(body);\n  req.end();\n} catch(e) {}\n\nreturn [$input.all()[0]];  // pass through unchanged"
        }
      },
      {
        "parameters": {
          "method": "POST",
          "url": "={{ ($env.LM_STUDIO_HOST || 'http://10.0.0.100:1234') + '/api/v1/models/load' }}",
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify({ model: $env.FINAL_REVIEWER_MODEL || 'mistralai/magistral-small-2509' , context_length: 98304 }) }}",
          "options": {
            "timeout": 120000
          },
          "sendHeaders": true,
          "headerParameters": {
            "parameters": [
              {
                "name": "Authorization",
                "value": "={{ 'Bearer ' + ($env.LLM_API_KEY || '') }}"
              }
            ]
          }
        },
        "id": "load-final-reviewer",
        "name": "Load Final Reviewer",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          5700,
          300
        ],
        "onError": "continueRegularOutput"
      },
      {
        "id": "restore-final-reviewer",
        "name": "Restore: Final Reviewer",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          5900,
          300
        ],
        "parameters": {
          "jsCode": "const loadResp = $json;\nif (loadResp.status !== 'loaded') throw new Error('Load Final Reviewer failed: ' + JSON.stringify(loadResp));\nreturn [{ json: { project_id: $('Prepare Planner Input').first().json.project_id } }];"
        }
      },
      {
        "parameters": {
          "method": "GET",
          "url": "={{ ($env.FILE_API_URL || 'http://file-api:3456') + '/projects/' + $('Prepare Planner Input').first().json.project_id + '/files-content' }}",
          "sendHeaders": true,
          "headerParameters": {
            "parameters": [
              {
                "name": "Authorization",
                "value": "={{ 'Bearer ' + ($env.FILE_API_TOKEN || '') }}"
              }
            ]
          },
          "options": {}
        },
        "id": "final-refetch",
        "name": "Final: Re-fetch Files",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          6100,
          300
        ],
        "onError": "continueRegularOutput"
      },
      {
        "id": "final-review-build",
        "name": "Final: Review Build",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          6300,
          300
        ],
        "parameters": {
          "jsCode": "const allFiles = $json.files || [];\nconst staticData = $getWorkflowStaticData('global');\nconst plannerInput = $('Prepare Planner Input').first().json;\nconst projectGoal = plannerInput.project_goal || '';\nconst initialReview = staticData._reviewResult || {};\n\nconst allFilesFormatted = allFiles\n  .filter(f => f.path.match(/\\.(ts|tsx|js|jsx|json|css|html)$/) && !f.path.includes('node_modules'))\n  .map(f => '### ' + f.path + '\\n```\\n' + f.content + '\\n```')\n  .join('\\n\\n');\n\nconst prompt = `You are a Senior Code Reviewer performing a FINAL REVIEW of a completed project.\n\nProject Goal: ${projectGoal}\n\nInitial Review Score: ${initialReview.overall_quality || 'N/A'}/100\nInitial Issues Found: ${(initialReview.fixes_needed || []).length}\nFixes Were Applied: Yes\n\nALL PROJECT FILES (after fixes):\n${allFilesFormatted}\n\nReturn ONLY valid JSON (no markdown):\n{\n  \"final_quality\": number (0-100),\n  \"summary\": \"2-3 sentence assessment of the project in its current state\",\n  \"suggestions\": [\n    {\n      \"preview\": \"Short 1-sentence title describing the scope of this suggestion\",\n      \"detail\": \"The full detailed engineering brief \u2014 10-20 sentences with every file path, function name, prop name, and exact change described\"\n    }\n  ]\n}\n\nRULES FOR SUGGESTIONS:\n\nProduce exactly 2-3 suggestions. Each suggestion has a \"preview\" (1 sentence, ~15 words) and a \"detail\" (full engineering brief).\n\nThe \"detail\" field is what gets sent to the coding pipeline. The pipeline has a 128K context coder that handles 6-8 files per task. Each suggestion's detail should generate 2-4 large tasks.\n\nSuggestion categories (pick 2-3 that apply):\n1. CRITICAL FIXES: All bugs, broken imports, wrong props, missing wiring. For EACH broken file: state the path, what's wrong, and the exact fix. Include variable names, prop names, function signatures.\n2. MISSING FEATURES: Core functionality that was requested but not implemented. Describe each feature in detail \u2014 what component renders it, what state it needs, what hooks/handlers to add, what files to create.\n3. UI/UX POLISH & ENHANCEMENTS: Animations, responsive design, accessibility, visual improvements, error states, loading states. Reference specific CSS classes, component names, and design patterns.\n\nEach \"detail\" should be a DENSE paragraph of 10-20 sentences. It should read like a complete engineering brief \u2014 a developer can execute every change from the description alone. Reference specific file paths, function names, prop names, state variables, and CSS classes throughout.\n\nIf the project is in great shape (90+), focus suggestions on enhancements: new features, dark mode, localStorage persistence, accessibility, performance optimization.`;\n\nreturn [{\n  json: {\n    model: $env.FINAL_REVIEWER_MODEL || 'mistralai/magistral-small-2509',\n    messages: [{ role: 'user', content: prompt }],\n    temperature: 0.5,\n    top_p: 0.75,\n    max_tokens: 16384\n  }\n}];"
        }
      },
      {
        "parameters": {
          "method": "POST",
          "url": "={{ $env.LM_STUDIO_URL || 'http://10.0.0.100:1234/v1/chat/completions' }}",
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify($json) }}",
          "options": {
            "timeout": 600000
          },
          "sendHeaders": true,
          "headerParameters": {
            "parameters": [
              {
                "name": "Authorization",
                "value": "={{ 'Bearer ' + $env.LLM_API_KEY }}"
              }
            ]
          }
        },
        "id": "final-review-llm",
        "name": "Final: Review LLM",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          6500,
          300
        ],
        "onError": "continueRegularOutput"
      },
      {
        "id": "final-review-parse",
        "name": "Final: Review Parse",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          6700,
          300
        ],
        "parameters": {
          "jsCode": "// Parse final review \u2014 robust JSON extraction\nconst msg = ($json.choices && $json.choices[0] && $json.choices[0].message) || {};\nlet raw = msg.content || '';\n\n// If content is empty/whitespace, try reasoning_content\nif (!raw.trim()) raw = msg.reasoning_content || '';\n\n// Strip thinking blocks\nlet content = raw.replace(/<think>[\\s\\S]*?<\\/think>/g, '').trim();\nif (content.includes('</think>')) content = content.split('</think>').pop().trim();\n\n// Strip markdown code fences\ncontent = content.replace(/^```(?:json)?\\n?/, '').replace(/\\n?```$/, '').trim();\n\n// Try multiple parse strategies\nlet review;\ntry {\n  // Strategy 1: direct parse (content is pure JSON)\n  review = JSON.parse(content);\n} catch(e1) {\n  try {\n    // Strategy 2: extract JSON object with balanced braces\n    let depth = 0, start = -1, end = -1;\n    for (let i = 0; i < content.length; i++) {\n      if (content[i] === '{') { if (depth === 0) start = i; depth++; }\n      if (content[i] === '}') { depth--; if (depth === 0) { end = i + 1; break; } }\n    }\n    if (start >= 0 && end > start) {\n      review = JSON.parse(content.substring(start, end));\n    } else {\n      throw new Error('No balanced JSON found');\n    }\n  } catch(e2) {\n    review = { final_quality: 0, summary: 'Final review parse failed: ' + e2.message, suggestions: [] };\n  }\n}\n\n// Normalize suggestions format (handle both string[] and {preview, detail}[] )\nif (review.suggestions) {\n  review.suggestions = review.suggestions.map(s => {\n    if (typeof s === 'string') return s;\n    if (s && s.preview) return s;\n    return String(s);\n  });\n}\n\nconst staticData = $getWorkflowStaticData('global');\nstaticData._finalReview = review;\n\n// Send callback to Forge\ntry {\n  const http = require('http');\n  const projectId = $('Prepare Planner Input').first().json.project_id || 'unknown';\n  const cbBody = JSON.stringify({\n    event: 'final_review_complete',\n    project_id: projectId,\n    data: { quality: review.final_quality, summary: review.summary, suggestions: review.suggestions }\n  });\n  const req = http.request({\n    hostname: 'forge', port: 3500, path: '/api/status-callback',\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(cbBody) }\n  });\n  req.on('error', () => {});\n  req.write(cbBody);\n  req.end();\n} catch(e) {}\n\nreturn [{ json: review }];"
        }
      },
      {
        "id": "unload-final-reviewer",
        "name": "Unload Final Reviewer",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          6900,
          300
        ],
        "parameters": {
          "jsCode": "const http = require('http');\nfunction lmsRequest(method, path, body) {\n  return new Promise((resolve, reject) => {\n    const lmsHost = ($env.LM_STUDIO_HOST || 'http://10.0.0.100:1234').replace(/^https?:\\/\\//, '');\n    const [hostname, portStr] = lmsHost.split(':');\n    const bodyStr = body ? JSON.stringify(body) : null;\n    const req = http.request({\n      hostname, port: parseInt(portStr) || 1234, path, method,\n      headers: {\n        Authorization: 'Bearer ' + ($env.LLM_API_KEY || ''),\n        'Content-Type': 'application/json',\n        ...(bodyStr ? { 'Content-Length': Buffer.byteLength(bodyStr) } : {})\n      }\n    }, res => {\n      let d = ''; res.on('data', c => d += c);\n      res.on('end', () => { try { resolve(JSON.parse(d)); } catch(e) { resolve({}); } });\n    });\n    req.on('error', reject);\n    if (bodyStr) req.write(bodyStr);\n    req.end();\n  });\n}\nconst modelKey = $env.FINAL_REVIEWER_MODEL || '';\ntry {\n  const resp = await lmsRequest('GET', '/api/v1/models');\n  for (const m of (resp.models || [])) {\n    if (m.key === modelKey) {\n      for (const inst of (m.loaded_instances || [])) {\n        try { await lmsRequest('POST', '/api/v1/models/unload', { instance_id: inst.id }); } catch(e) {}\n      }\n    }\n  }\n} catch(e) {}\nreturn [{ json: $('Final: Review Parse').first().json }];"
        }
      },
      {
        "id": "cb-fix-applied",
        "name": "CB: Fix Applied",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          5500,
          100
        ],
        "parameters": {
          "jsCode": "// Status callback to Forge \u2014 fire and forget\nconst http = require('http');\nconst staticData = $getWorkflowStaticData('global');\nconst projectId = staticData._currentProjectId || $('Prepare Planner Input').first().json.project_id || 'unknown';\nconst fixResults = staticData._fixResults || [];\n\ntry {\n  const body = JSON.stringify({\n    event: 'fix_applied',\n    project_id: projectId,\n    data: { files_fixed: fixResults, fix_count: fixResults.length }\n  });\n  const req = http.request({\n    hostname: 'forge', port: 3500, path: '/api/status-callback',\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }\n  });\n  req.on('error', () => {});\n  req.write(body);\n  req.end();\n} catch(e) {}\n\nreturn [$input.all()[0]];"
        }
      },
      {
        "id": "research-fetch-docs",
        "name": "Research: Fetch Docs",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          2050,
          300
        ],
        "parameters": {
          "jsCode": "// Research: Fetch library docs from Context7 MCP\n// Uses require('http') with Accept header (required by Streamable HTTP MCP protocol)\nconst http = require('http');\nconst staticData = $getWorkflowStaticData('global');\nconst allFiles = staticData._allFileContents || [];\nconst tasks = $json.tasks || [];\n\n// 1. Extract libraries\nconst libs = new Set();\nconst pkgFile = allFiles.find(f => f.path === 'package.json');\nif (pkgFile) {\n  try {\n    const pkg = JSON.parse(pkgFile.content);\n    for (const dep of Object.keys(pkg.dependencies || {})) libs.add(dep);\n    for (const dep of Object.keys(pkg.devDependencies || {})) libs.add(dep);\n  } catch(e) {}\n}\nconst taskText = tasks.map(t => t.description || '').join(' ').toLowerCase();\nconst knownLibs = ['react', 'next', 'nextjs', 'vite', 'tailwindcss', 'tailwind', 'express', 'prisma', 'framer-motion', 'heroui', 'zustand', 'zod', 'three', 'react-three', 'r3f', 'drei', '@react-three/fiber', '@react-three/drei'];\nfor (const lib of knownLibs) {\n  if (taskText.includes(lib)) {\n      const mapped = {'nextjs':'next','tailwind':'tailwindcss','react-three':'@react-three/fiber','r3f':'@react-three/fiber','drei':'@react-three/drei'};\n      libs.add(mapped[lib] || lib);\n    }\n}\nif (allFiles.some(f => f.path.match(/\\.(jsx|tsx)$/))) libs.add('react');\n// No hardcoded priority \u2014 fetch docs for whatever the project actually uses\n// Skip trivial/internal packages that don't have useful docs\nconst skipList = new Set(['autoprefixer', 'postcss', 'typescript', 'vite', '@vitejs/plugin-react', '@types/react', '@types/node', '@types/react-dom', 'eslint', 'prettier']);\nconst toFetch = [...libs].filter(l => !skipList.has(l)).slice(0, 7);\n\nif (toFetch.length === 0) {\n  staticData._researchDocs = '';\n  return [{ json: $json }];\n}\n\n// 2. MCP HTTP helper\nfunction mcpRequest(sessionId, method, params) {\n  return new Promise((resolve, reject) => {\n    const body = JSON.stringify({ jsonrpc: '2.0', method, params, id: Date.now() });\n    const req = http.request({\n      hostname: 'docky', port: 8811, path: '/mcp', method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        'Accept': 'application/json, text/event-stream',\n        'Content-Length': Buffer.byteLength(body),\n        ...(sessionId ? { 'Mcp-Session-Id': sessionId } : {})\n      }\n    }, res => {\n      let data = '';\n      res.on('data', c => data += c);\n      res.on('end', () => resolve({ data, sessionId: res.headers['mcp-session-id'] }));\n    });\n    req.on('error', reject);\n    req.setTimeout(20000, () => { req.destroy(); reject(new Error('timeout')); });\n    req.write(body);\n    req.end();\n  });\n}\n\nfunction parseSSE(raw) {\n  for (const line of raw.split('\\n')) {\n    if (line.startsWith('data: ')) {\n      try { return JSON.parse(line.slice(6)); } catch(e) {}\n    }\n  }\n  return {};\n}\n\nconst docs = [];\nconst errors = [];\n\ntry {\n  // Initialize session\n  const init = await mcpRequest(null, 'initialize', {\n    protocolVersion: '2024-11-05', capabilities: {},\n    clientInfo: { name: 'eek-go-research', version: '1.0' }\n  });\n  const sid = init.sessionId;\n  if (!sid) throw new Error('No MCP session ID');\n\n  for (const lib of toFetch) {\n    try {\n      // Resolve library ID\n      const resolve = await mcpRequest(sid, 'tools/call', {\n        name: 'resolve-library-id', arguments: { libraryName: lib }\n      });\n      const resolveData = parseSSE(resolve.data);\n      const resolveText = (resolveData.result?.content || []).map(c => c.text || '').join('');\n      const idMatch = resolveText.match(/Context7-compatible library ID:\\s*(\\/[\\w.-]+\\/[\\w.-]+)/);\n      if (!idMatch) { errors.push(lib + ': no ID'); continue; }\n\n      // Get docs\n      const docsResp = await mcpRequest(sid, 'tools/call', {\n        name: 'get-library-docs',\n        arguments: { context7CompatibleLibraryID: idMatch[1], tokens: 5000, topic: 'setup components hooks API examples' }\n      });\n      const docsData = parseSSE(docsResp.data);\n      const docText = (docsData.result?.content || []).map(c => c.text || '').join('');\n      if (docText.length > 100) {\n        docs.push('## ' + lib + ' (' + idMatch[1] + ')\\n' + docText.substring(0, 8000));\n      }\n    } catch(e) { errors.push(lib + ': ' + e.message); }\n  }\n} catch(e) {\n  errors.push('init: ' + e.message);\n}\n\n// Prepend a universal design guide\nconst designGuide = `## UI/UX Design Principles for Web Applications\n\n### Layout\n- Main interaction element must be centered and visually dominant (40-60% of viewport)\n- Use vertical single-column layouts for game/app UIs \u2014 never side-by-side grids unless it's a dashboard\n- Content hierarchy: hero/main action \u2192 stats/feedback \u2192 secondary actions (shop, settings)\n- Mobile-first: everything should work on a 375px wide screen and scale up\n- Scrollable secondary content (shops, lists) should never push the main interaction off-screen\n\n### Visual Feedback\n- Every user interaction (click, purchase, hover) MUST produce visible feedback\n- Number changes should animate (bounce, scale pulse, color flash)\n- Buttons: press animation (scale 0.95), hover glow/lift, disabled state with reduced opacity\n- Success actions: green flash, checkmark, particle burst\n- Use CSS transitions (200-300ms) on all interactive elements\n\n### Color & Contrast\n- Dark backgrounds with bright accent colors for maximum contrast\n- Use gradients over flat colors for depth (e.g., purple-900 to indigo-950)\n- Interactive elements should be the brightest items on screen\n- Affordability: green = can buy, red/gray = locked, gold = premium\n- Text must have sufficient contrast \u2014 white/yellow on dark, with text-shadow for readability\n\n### Typography\n- Big, bold numbers for scores/stats (text-4xl to text-6xl)\n- Clear hierarchy: title (bold, large) \u2192 subtitle (medium) \u2192 body (regular, smaller)\n- Use font-weight differences, not just size, to create hierarchy\n- Monospace or tabular numbers for counters that change frequently\n\n### Animation\n- Idle animations (slow pulse, float, rotate) make the UI feel alive\n- Click animations should be fast (100-200ms) and snappy\n- Spawn/death animations for appearing/disappearing elements (scale 0\u21921, fade in/out)\n- Use CSS will-change on animated elements for performance\n- Stagger animations for lists (each item slightly delayed)\n\n### Cards & Containers\n- Rounded corners (border-radius: 12-16px)\n- Subtle shadows for depth (shadow-lg, shadow-xl)\n- Semi-transparent backgrounds with backdrop-blur for overlay panels\n- Hover: lift (translateY -2 to -4px) + shadow increase\n- Border: subtle (1px border with low-opacity white or accent color)`;\n\n// 3. Fetch UI component inspiration from Magic MCP (21st.dev)\nlet magicDocs = '';\ntry {\n  // Extract UI-related keywords from task descriptions for search\n  const uiKeywords = [];\n  const taskDescs = tasks.map(t => (t.description || '').toLowerCase()).join(' ');\n  const uiPatterns = ['button', 'card', 'shop', 'form', 'input', 'modal', 'dialog', 'nav', 'header', 'footer', 'sidebar', 'menu', 'table', 'list', 'grid', 'dashboard', 'counter', 'score', 'game', 'animation', 'toggle', 'dropdown'];\n  for (const p of uiPatterns) {\n    if (taskDescs.includes(p)) uiKeywords.push(p);\n  }\n\n  if (uiKeywords.length > 0) {\n    // Reuse the existing MCP session or create new one for magic\n    const magicInit = await new Promise((resolve, reject) => {\n      const body = JSON.stringify({ jsonrpc: '2.0', method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'eek-go-magic', version: '1.0' } }, id: Date.now() });\n      const req = http.request({\n        hostname: 'docky', port: 8811, path: '/mcp', method: 'POST',\n        headers: { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream', 'Content-Length': Buffer.byteLength(body) }\n      }, res => {\n        let data = ''; res.on('data', c => data += c);\n        res.on('end', () => resolve({ data, sessionId: res.headers['mcp-session-id'] }));\n      });\n      req.on('error', reject);\n      req.setTimeout(10000, () => { req.destroy(); reject(new Error('timeout')); });\n      req.write(body); req.end();\n    });\n\n    const magicSid = magicInit.sessionId;\n    if (magicSid) {\n      // Search for 2-3 relevant component types\n      const searchTerms = uiKeywords.slice(0, 3).map(k => k + ' component');\n      for (const query of searchTerms) {\n        try {\n          const inspResp = await mcpRequest(magicSid, 'tools/call', {\n            name: '21st_magic_component_inspiration',\n            arguments: { message: 'I need a modern ' + query + ' for a web app', searchQuery: query }\n          });\n          const inspData = parseSSE(inspResp.data);\n          const inspText = (inspData.result?.content || []).map(c => c.text || '').join('');\n          if (inspText.length > 200) {\n            // Extract just the code examples, not the full JSON\n            try {\n              const components = JSON.parse(inspText);\n              if (Array.isArray(components)) {\n                const examples = components.slice(0, 2).map(c => {\n                  const code = c.demoCode || c.code || '';\n                  const name = c.demoName || c.name || query;\n                  return '### ' + name + '\\n```tsx\\n' + code + '\\n```';\n                }).join('\\n\\n');\n                if (examples.length > 100) {\n                  magicDocs += '\\n\\n## Magic UI: ' + query + '\\n' + examples;\n                }\n              }\n            } catch(e) {\n              // Not JSON, use raw text (truncated)\n              if (inspText.length > 100) {\n                magicDocs += '\\n\\n## Magic UI: ' + query + '\\n' + inspText.substring(0, 4000);\n              }\n            }\n          }\n        } catch(e) { /* skip this query */ }\n      }\n    }\n  }\n} catch(e) {\n  // Magic MCP down \u2014 continue without it\n}\n\nif (magicDocs) {\n  docs.push('## UI Component Examples (from 21st.dev Magic)\\n' + magicDocs);\n}\n\nstaticData._researchDocs = docs.length > 0\n  ? designGuide + '\\n\\n---\\n\\n## Library Documentation (from Context7)\\n\\n' + docs.join('\\n\\n---\\n\\n')\n  : designGuide;\n\n// Callback\ntry {\n  const projectId = $('Extract Input').first().json.project_id || staticData._currentProjectId || 'unknown';\n  const cbBody = JSON.stringify({\n    event: 'research_complete', project_id: projectId,\n    data: { libraries_fetched: toFetch, doc_count: docs.length, doc_chars: docs.reduce((s,d) => s+d.length, 0), errors: errors.length ? errors : undefined }\n  });\n  const req = http.request({\n    hostname: 'forge', port: 3500, path: '/api/status-callback', method: 'POST',\n    headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Content-Length': Buffer.byteLength(cbBody) }\n  });\n  req.on('error', () => {});\n  req.write(cbBody);\n  req.end();\n} catch(e) {}\n\nreturn [{ json: $json }];"
        }
      },
      {
        "id": "build-check",
        "name": "Build Check",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          3950,
          300
        ],
        "parameters": {
          "jsCode": "// Build Check: run npm install + vite build, store errors for reviewer/fixer\nconst http = require('http');\nconst staticData = $getWorkflowStaticData('global');\nconst projectId = $('Extract Input').first().json.project_id || 'unknown';\n\nlet buildResult = { success: true, output: '' };\n\ntry {\n  const body = '';\n  const result = await new Promise((resolve, reject) => {\n    const req = http.request({\n      hostname: 'file-api', port: 3456, method: 'POST',\n      path: `/projects/${projectId}/build-check`,\n      headers: {\n        'Authorization': 'Bearer ' + ($env.FILE_API_TOKEN || ''),\n        'Content-Type': 'application/json',\n        'Content-Length': 0\n      }\n    }, res => {\n      let data = '';\n      res.on('data', c => data += c);\n      res.on('end', () => { try { resolve(JSON.parse(data)); } catch(e) { resolve({ success: false, error: data }); } });\n    });\n    req.on('error', e => resolve({ success: false, error: e.message }));\n    req.setTimeout(180000, () => { req.destroy(); resolve({ success: false, error: 'Build check timed out' }); });\n    req.end();\n  });\n  buildResult = result;\n} catch(e) {\n  buildResult = { success: false, error: e.message };\n}\n\n// Store build result for reviewer\nstaticData._buildResult = buildResult;\n\n// If build passed, start preview, screenshot, stop preview\nstaticData._screenshot_b64 = null;\nif (buildResult.success) {\n  try {\n    // Start preview\n    const startBody = '';\n    await new Promise((resolve, reject) => {\n      const req = http.request({\n        hostname: 'file-api', port: 3456, method: 'POST',\n        path: '/projects/' + projectId + '/preview/start',\n        headers: { 'Authorization': 'Bearer ' + ($env.FILE_API_TOKEN || ''), 'Content-Type': 'application/json', 'Content-Length': 0 }\n      }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => resolve(d)); });\n      req.on('error', reject);\n      req.setTimeout(120000, () => { req.destroy(); reject(new Error('timeout')); });\n      req.end();\n    });\n\n    // Screenshot via Playwright\n    const scrapeBody = JSON.stringify({ url: 'http://localhost:4000' });\n    const scrapeResult = await new Promise((resolve, reject) => {\n      const req = http.request({\n        hostname: 'file-api', port: 3456, method: 'POST', path: '/scrape',\n        headers: { 'Authorization': 'Bearer ' + ($env.FILE_API_TOKEN || ''), 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(scrapeBody) }\n      }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch(e) { resolve({}); } }); });\n      req.on('error', reject);\n      req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout')); });\n      req.write(scrapeBody); req.end();\n    });\n    staticData._screenshot_b64 = scrapeResult.screenshot_b64 || null;\n\n    // Stop preview\n    const stopReq = http.request({\n      hostname: 'file-api', port: 3456, method: 'POST',\n      path: '/projects/' + projectId + '/preview/stop',\n      headers: { 'Authorization': 'Bearer ' + ($env.FILE_API_TOKEN || ''), 'Content-Length': 0 }\n    });\n    stopReq.on('error', () => {});\n    stopReq.end();\n  } catch(e) { /* screenshot failed, continue without it */ }\n}\n\n// Send callback to Forge\ntry {\n  const cbBody = JSON.stringify({\n    event: buildResult.success ? 'build_check_passed' : 'build_check_failed',\n    project_id: projectId,\n    data: { success: buildResult.success, error: buildResult.error || null, stage: buildResult.stage || null }\n  });\n  const req = http.request({\n    hostname: 'forge', port: 3500, path: '/api/status-callback', method: 'POST',\n    headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Content-Length': Buffer.byteLength(cbBody) }\n  });\n  req.on('error', () => {});\n  req.write(cbBody);\n  req.end();\n} catch(e) {}\n\nreturn [{ json: $json }];"
        }
      }
    ],
    "connections": {
      "Webhook": {
        "main": [
          [
            {
              "node": "Extract Input",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Extract Input": {
        "main": [
          [
            {
              "node": "CB: Pipeline Started",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Init Memory": {
        "main": [
          [
            {
              "node": "Fetch Project Files",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Fetch Project Files": {
        "main": [
          [
            {
              "node": "Prepare Planner Input",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Prepare Planner Input": {
        "main": [
          [
            {
              "node": "P0: Has Reference URL?",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "P0: Has Reference URL?": {
        "main": [
          [
            {
              "node": "P0: Scrape URL",
              "type": "main",
              "index": 0
            }
          ],
          [
            {
              "node": "Startup: Unload All Models",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "P0: Scrape URL": {
        "main": [
          [
            {
              "node": "P0: Merge Scrape Data",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "P0: Merge Scrape Data": {
        "main": [
          [
            {
              "node": "Startup: Unload All Models",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Load Planner Model": {
        "main": [
          [
            {
              "node": "Restore: Planner Input",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Restore: Planner Input": {
        "main": [
          [
            {
              "node": "Planner: Build Request",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Planner: Build Request": {
        "main": [
          [
            {
              "node": "Planner: Call LM Studio",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Planner: Call LM Studio": {
        "main": [
          [
            {
              "node": "Planner: Parse Response",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Planner: Parse Response": {
        "main": [
          [
            {
              "node": "CB: Planning Complete",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Unload Planner Model": {
        "main": [
          [
            {
              "node": "Research: Fetch Docs",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Load Coder Model": {
        "main": [
          [
            {
              "node": "Restore: After Load Coder",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Restore: After Load Coder": {
        "main": [
          [
            {
              "node": "Spread Tasks",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Spread Tasks": {
        "main": [
          [
            {
              "node": "P2: Stash Context",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "P2: Stash Context": {
        "main": [
          [
            {
              "node": "P2: Build Code Input",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "P2: Build Code Input": {
        "main": [
          [
            {
              "node": "CW: Prepare Message",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "CW: Prepare Message": {
        "main": [
          [
            {
              "node": "CW: Call LM Studio",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "CW: Call LM Studio": {
        "main": [
          [
            {
              "node": "CW: Parse Response",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "CW: Parse Response": {
        "main": [
          [
            {
              "node": "P2: Prepare Write",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "P2: Prepare Write": {
        "main": [
          [
            {
              "node": "P2: Write Files",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "P2: Write Files": {
        "main": [
          [
            {
              "node": "P2: Store Result",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Unload Coder Model": {
        "main": [
          [
            {
              "node": "Build Check",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Load Reviewer Model": {
        "main": [
          [
            {
              "node": "Restore: After Load Reviewer",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Restore: After Load Reviewer": {
        "main": [
          [
            {
              "node": "P3: Re-fetch All Files",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "P3: Re-fetch All Files": {
        "main": [
          [
            {
              "node": "P3: Full Review Build",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "P3: Full Review Build": {
        "main": [
          [
            {
              "node": "P3: Review LLM",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "P3: Review LLM": {
        "main": [
          [
            {
              "node": "P3: Review Parse",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "P3: Review Parse": {
        "main": [
          [
            {
              "node": "CB: Review Complete",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Unload Reviewer Model": {
        "main": [
          [
            {
              "node": "Restore: After Unload Reviewer",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Restore: After Unload Reviewer": {
        "main": [
          [
            {
              "node": "P3: Needs Fix?",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "P3: Needs Fix?": {
        "main": [
          [
            {
              "node": "Load Fixer Model",
              "type": "main",
              "index": 0
            }
          ],
          [
            {
              "node": "Load Final Reviewer",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Load Fixer Model": {
        "main": [
          [
            {
              "node": "Restore: After Load Fixer",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Restore: After Load Fixer": {
        "main": [
          [
            {
              "node": "P4: Fix Build",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "P4: Fix Build": {
        "main": [
          [
            {
              "node": "P4: Fix LLM",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "P4: Fix LLM": {
        "main": [
          [
            {
              "node": "P4: Fix Parse",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "P4: Fix Parse": {
        "main": [
          [
            {
              "node": "P4: Fix Write",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "P4: Fix Write": {
        "main": [
          [
            {
              "node": "CB: Fix Applied",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Unload Fixer Model": {
        "main": [
          [
            {
              "node": "Restore: After Unload Fixer",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Restore: After Unload Fixer": {
        "main": [
          [
            {
              "node": "Load Final Reviewer",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Aggregate All Results": {
        "main": [
          [
            {
              "node": "Prepare Memory Update",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Prepare Memory Update": {
        "main": [
          [
            {
              "node": "Update Memory",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Update Memory": {
        "main": [
          [
            {
              "node": "Build Response",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Startup: Unload All Models": {
        "main": [
          [
            {
              "node": "Load Planner Model",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "P2: Store Result": {
        "main": [
          [
            {
              "node": "P2: Continue Gate",
              "type": "main",
              "index": 0
            },
            {
              "node": "P2: Exit Gate",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "P2: Continue Gate": {
        "main": [
          [
            {
              "node": "P2: Stash Context",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "P2: Exit Gate": {
        "main": [
          [
            {
              "node": "Unload Coder Model",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "CB: Pipeline Started": {
        "main": [
          [
            {
              "node": "Init Memory",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "CB: Planning Complete": {
        "main": [
          [
            {
              "node": "Unload Planner Model",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "CB: Review Complete": {
        "main": [
          [
            {
              "node": "Unload Reviewer Model",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Build Response": {
        "main": [
          [
            {
              "node": "CB: Pipeline Complete",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "CB: Pipeline Complete": {
        "main": [
          []
        ]
      },
      "Load Final Reviewer": {
        "main": [
          [
            {
              "node": "Restore: Final Reviewer",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Restore: Final Reviewer": {
        "main": [
          [
            {
              "node": "Final: Re-fetch Files",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Final: Re-fetch Files": {
        "main": [
          [
            {
              "node": "Final: Review Build",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Final: Review Build": {
        "main": [
          [
            {
              "node": "Final: Review LLM",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Final: Review LLM": {
        "main": [
          [
            {
              "node": "Final: Review Parse",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Final: Review Parse": {
        "main": [
          [
            {
              "node": "Unload Final Reviewer",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Unload Final Reviewer": {
        "main": [
          [
            {
              "node": "Aggregate All Results",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "CB: Fix Applied": {
        "main": [
          [
            {
              "node": "Unload Fixer Model",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Research: Fetch Docs": {
        "main": [
          [
            {
              "node": "Load Coder Model",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Build Check": {
        "main": [
          [
            {
              "node": "Load Reviewer Model",
              "type": "main",
              "index": 0
            }
          ]
        ]
      }
    },
    "authors": "Will Lovelock",
    "name": null,
    "description": null,
    "autosaved": false,
    "workflowPublishHistory": [
      {
        "createdAt": "2026-03-19T05:34:06.277Z",
        "id": 533,
        "workflowId": "PNOMGkCjzFGxf52E",
        "versionId": "c4e7e3fc-3b1a-48ed-a2c7-e880d6123dd8",
        "event": "activated",
        "userId": "8a83cac8-2e54-48fc-8da6-ef4d02988d2f"
      },
      {
        "createdAt": "2026-03-19T05:34:06.252Z",
        "id": 532,
        "workflowId": "PNOMGkCjzFGxf52E",
        "versionId": "c4e7e3fc-3b1a-48ed-a2c7-e880d6123dd8",
        "event": "deactivated",
        "userId": "8a83cac8-2e54-48fc-8da6-ef4d02988d2f"
      }
    ]
  }
}