{
  "name": "bridge_researcher_bot",
  "nodes": [
    {
      "parameters": {
        "updates": [
          "message"
        ],
        "additionalFields": {}
      },
      "id": "node_trigger_tg_researcher",
      "name": "Telegram: user DM",
      "type": "n8n-nodes-base.telegramTrigger",
      "typeVersion": 1.1,
      "position": [
        240,
        200
      ],
      "disabled": true
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 30 */6 * * *"
            }
          ]
        }
      },
      "id": "node_trigger_daily",
      "name": "Schedule: daily",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        240,
        400
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 0 10 * * 2"
            }
          ]
        }
      },
      "id": "node_trigger_weekly",
      "name": "Schedule: weekly",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        240,
        600
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "bridge-researcher-invoke",
        "responseMode": "lastNode",
        "options": {}
      },
      "id": "node_trigger_webhook_researcher",
      "name": "Webhook: invoke",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        240,
        800
      ]
    },
    {
      "parameters": {
        "jsCode": "// \u2500\u2500 V2 Task Envelope: task_id propagation \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 _env = (typeof $input !== 'undefined' && $input.all()[0].json) || {};\nconst _envBody = _env.body || {};\nconst _inherited_task_id = _envBody.task_id || null;\nconst _gen_task_id = () => 'tid-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 9);\nconst _task_id = _inherited_task_id || _gen_task_id();\n// \u2500\u2500 End V2 Task Envelope \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst src = $input.all()[0];\n// \u2500\u2500 Loop prevention guard \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\u2500\u2500\u2500\nconst _incomingBody = (src.json && src.json.body) ? src.json.body : {};\nconst _attemptCount = (_incomingBody.attempt_count !== undefined)\n    ? parseInt(_incomingBody.attempt_count, 10) || 0 : 0;\nif (_attemptCount >= 1) {\n    return [{ json: { loop_detected: true, original_task: _incomingBody,\n        bot_name: 'researcher',\n        task: 'loop_escalation', user_text: null, chat_id: null, from_agent: 'loop_guard',\n        now_iso: new Date().toISOString(), daily_spec: [], weekly_spec: [] } }];\n}\n// \u2500\u2500 End loop prevention \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\u2500\u2500\u2500\u2500\u2500\n\n// \u2500\u2500 Deduplication guard \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\u2500\u2500\u2500\u2500\nconst messageId = (src.json && src.json.message && src.json.message.message_id)\n    ? String(src.json.message.message_id)\n    : null;\n\n\n// Detect trigger source by inspecting the item shape.\nlet task = 'scheduled_tick';\nlet user_text = null;\nlet chat_id = null;\nlet from_agent = 'system';\n\nif (src.json && src.json.message && src.json.message.chat) {\n    // Telegram trigger\n    task = 'user_dm';\n    let _utext = src.json.message.text || '';\n    // Strip leading slash from Telegram commands so Super Agent does not classify them as shell commands.\n    if (_utext.startsWith('/')) {\n        const parts = _utext.slice(1).split(/\\s+/);\n        const cmd = (parts[0] || '').toLowerCase();\n        const rest = parts.slice(1).join(' ');\n        if (cmd === 'start' || cmd === 'help') { _utext = 'Hello - please introduce yourself, your role in Bridge Digital, and what you can help with right now.'; }\n        else if (cmd === 'status') { _utext = 'Give me a brief status update on your current focus and any open items in your inbox.'; }\n        else { _utext = (cmd + ' ' + rest).trim(); }\n    }\n    user_text = _utext;\n    chat_id = src.json.message.chat.id;\n    from_agent = 'user';\n} else if (src.json && src.json.body && src.json.body.task) {\n    // Webhook trigger\n    task = src.json.body.task;\n    user_text = src.json.body.message || null;\n    from_agent = src.json.body.from_agent || 'system';\n    chat_id = null; // webhooks do not carry a telegram chat\n} else if (src.json && src.json.timestamp) {\n    // Schedule trigger\n    task = 'scheduled_tick';\n    user_text = null;\n    from_agent = 'system';\n}\n\nreturn [{ json: { message_id: messageId,\n    bot_name: \"researcher\",\n    task,\n    user_text,\n    chat_id,\n    from_agent,\n    now_iso: new Date().toISOString(),\n    daily_spec: [{\"hour\": 7, \"minute\": 30, \"task_name\": \"morning_intel_cycle\"}],\n    weekly_spec: [{\"weekday\": 2, \"hour\": 10, \"minute\": 0, \"task_name\": \"weekly_opportunity_deep_dive\"}],\n    task_id: _task_id,\n} }];"
      },
      "id": "node_build_task",
      "name": "Build task payload",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        400
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT (value = 'true') AS enabled FROM bridge.system_limits WHERE key = 'researcher_bot_enabled';",
        "options": {}
      },
      "id": "node_read_enabled",
      "name": "Read enabled flag",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        680,
        400
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "enabled_check",
              "leftValue": "={{ $json.enabled }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "node_if_enabled",
      "name": "If enabled",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        900,
        400
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT COALESCE(jsonb_agg(row_to_json(m) ORDER BY m.priority_rank, m.created_at), '[]'::jsonb)        AS open_inbox FROM (   SELECT memo_id, from_agent, memo_type, priority, subject, body_json,          created_at, related_lead_id,          CASE priority WHEN 'urgent' THEN 0 WHEN 'high' THEN 1                        WHEN 'normal' THEN 2 ELSE 3 END AS priority_rank   FROM bridge.agent_memos   WHERE to_agent IN ('researcher', 'all')     AND status = 'open'   LIMIT 20 ) m;",
        "options": {}
      },
      "id": "node_fetch_inbox",
      "name": "Fetch open inbox",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1120,
        400
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT jsonb_build_object(\n    'now_utc', NOW(),\n    'leads_last_24h', (\n        SELECT COALESCE(jsonb_agg(row_to_json(l)), '[]'::jsonb)\n        FROM (\n            SELECT lead_id, category AS niche, city, country,\n                   research_status, website_status,\n                   lead_score, website_presence_confidence, website_quality_score\n            FROM bridge.leads\n            WHERE created_at >= NOW() - INTERVAL '24 hours'\n            ORDER BY COALESCE(lead_score, 0) DESC\n            LIMIT 30\n        ) l\n    ),\n    'niche_stats_last_7d', (\n        SELECT COALESCE(jsonb_agg(row_to_json(s)), '[]'::jsonb)\n        FROM (\n            SELECT category AS niche, city,\n                   COUNT(*)::int                                   AS total,\n                   COUNT(*) FILTER (WHERE research_status IN\n                     ('Qualified','Ready for Website Team'))::int  AS qualified,\n                   COUNT(*) FILTER (WHERE marketing_status='Interested')::int AS interested,\n                   ROUND(AVG(COALESCE(lead_score,0))::numeric, 2)  AS avg_score\n            FROM bridge.leads\n            WHERE created_at >= NOW() - INTERVAL '7 days'\n            GROUP BY category, city\n            ORDER BY qualified DESC NULLS LAST, total DESC\n            LIMIT 20\n        ) s\n    ),\n    'active_campaigns', (\n        SELECT COALESCE(jsonb_agg(row_to_json(c)), '[]'::jsonb)\n        FROM (\n            SELECT campaign_target_id, niche, city, country, priority,\n                   daily_lead_target, daily_website_limit\n            FROM bridge.campaign_targets\n            WHERE active_flag = TRUE\n            ORDER BY priority DESC\n            LIMIT 15\n        ) c\n    ),\n    'my_open_memos_count', (\n        SELECT COUNT(*)::int FROM bridge.agent_memos\n        WHERE to_agent IN ('researcher','all') AND status='open'\n    )\n) AS context;",
        "options": {}
      },
      "id": "node_fetch_context",
      "name": "Fetch bot context",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1120,
        600
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const _buildTask = $('Build task payload').first().json;\nif (_buildTask.loop_detected) {\n    return [{ json: {\n        message: JSON.stringify({ reply_text: '', actions: [{ type: 'no_op', payload: {\n            reason: 'loop_detected', attempt_count: (_buildTask.original_task && _buildTask.original_task.attempt_count) || 1\n        }}]}),\n        session_id: 'bridge-loop-guard-' + Date.now(),\n        task_kind: 'loop_escalation', user_chat_id: null,\n        bot_name: _buildTask.bot_name || 'unknown', _pre_formed: true\n    }}];\n}\n\nconst task = $('Build task payload').first().json;\n// \u2500\u2500 Memory auto-block check (non-blocking) \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\nlet _autoBlockActive = false;\nconst _querySubject = task.user_text || task.subject || '';\nif (_querySubject && _querySubject.length > 5) {\n    try {\n        const _memCheck = await $http.post(\n            `${$env.SUPER_AGENT_URL || 'https://super-agent-production.up.railway.app'}/webhook/memory-query`,\n            { project_name: _querySubject.slice(0, 100), api_key: $env.N8N_API_KEY || '' }\n        );\n        if (_memCheck && _memCheck.data && _memCheck.data.auto_block === true) {\n            _autoBlockActive = true;\n            return [{ json: {\n                message: JSON.stringify({\n                    reply_text: `\ud83d\udeab AUTO-BLOCK: Project has been rejected ${_memCheck.data.rejection_count} time(s). Route to CRO then CEO for override.`,\n                    actions: [{ type: 'no_op', payload: {\n                        reason: 'auto_block_repeated_rejection',\n                        rejection_count: _memCheck.data.rejection_count,\n                        similar_projects: _memCheck.data.similar_projects\n                    }}]\n                }),\n                session_id: `bridge-auto-block-${Date.now()}`,\n                task_kind: 'auto_block', user_chat_id: task.chat_id,\n                bot_name: task.bot_name, _pre_formed: true\n            }}];\n        }\n    } catch(e) { /* non-blocking */ }\n}\n\n\nconst inboxRow = $('Fetch open inbox').first().json || {};\nconst inbox = Array.isArray(inboxRow.open_inbox) ? inboxRow.open_inbox : [];\nconst ctxRow = $('Fetch bot context').first().json || {};\nconst ctx = ctxRow.context ? [ctxRow.context] : [ctxRow];\n\n\nconst globalProtocol = `\n\nGLOBAL OPERATING PROTOCOL (non-negotiable):\n1. ALWAYS respond in valid JSON only. No prose, no markdown outside JSON strings.\n2. EXACT response structure: {\"reply_text\": \"...\", \"actions\": [...]}\n3. reply_text: human-readable Telegram message. Brief, role-appropriate.\n4. actions: array of {type, payload} objects. Empty array if no actions.\n5. NEVER include PLACEHOLDER, PASTE_, TBD, INSERT_HERE values in any field.\n6. NEVER mention internal tools (n8n, shell, SQL, workflow IDs) in reply_text.\n7. If required data is missing \u2192 return no_op with status \"need_input\" + list exact missing fields.\n8. If blocked \u2192 escalate to Chief of Staff. NEVER retry autonomously.\n9. MAX_RETRIES = 1. If attempt_count >= 1 in incoming body_json \u2192 return no_op immediately.\n`;\n\nconst system = \"You are Bridge_Researcher_Bot, the enterprise research and intelligence super-agent inside Bridge Digital Solutions.\\n\\nROLE\\nYou are the company's discovery, qualification, enrichment, and opportunity-intelligence brain. You research markets, businesses, competitors, niches, public signals, and recurring pain points. You do not merely collect data; you convert raw information into actionable business opportunities.\\n\\nPRIMARY OBJECTIVES\\n1. Discover and qualify profitable leads.\\n2. Enrich businesses with structured commercial intelligence.\\n3. Detect businesses with weak, outdated, or missing digital infrastructure.\\n4. Identify recurring pain points that could become productized services.\\n5. Propose new monetization ideas to Finance and leadership.\\n6. Reduce wasted effort by filtering weak opportunities before downstream departments act.\\n\\nSECONDARY OBJECTIVES\\n1. Detect niche trends, underserved segments, and local market gaps.\\n2. Identify upsell indicators and repeatable offer opportunities.\\n3. Improve targeting quality over time using prior outcomes and feedback loops.\\n\\nCORE FUNCTIONS\\nLead discovery, enrichment, qualification, duplicate prevention, market-gap detection, opportunity-proposal generation, competitor/niche intelligence, strategic research support.\\n\\nWHAT YOU MUST PRODUCE\\nFor each researched lead or market: structured lead profile, lead quality score, website presence assessment, business maturity estimate, likely services offered, contact completeness score, recommended next step, key risks or missing data, upsell potential, niche/profit relevance.\\nFor each discovered business opportunity: opportunity title, problem statement, target niche, evidence signals, proposed service/product concept, expected value hypothesis, recommendation for Finance review.\\n\\nDECISION PRINCIPLES\\n- Be conservative with uncertainty. Never invent facts.\\n- Distinguish clearly between observed facts, likely inferences, and missing data.\\n- Optimize for usefulness, commercial value, and downstream actionability.\\n- Prioritize profitable and repeatable opportunities over noisy or low-value data.\\n- Flag ambiguity instead of pretending certainty.\\n\\nQUALITY STANDARDS: Structured, commercially relevant, operationally useful, duplication-aware, evidence-based, action-oriented.\\n\\nINTERACTION RULES\\nYou collaborate with Bridge_Chief_Of_Staff_bot for execution alignment, Bridge_CEO_BOT for strategic insight, Finance-related agents via opportunity proposals, Marketing/Website teams via lead and niche intelligence.\\n\\nESCALATE WHEN: data is too uncertain for qualification; compliance risk suspected; pattern suggests a high-value new service opportunity; a niche shows unusually strong profit potential; a lead appears strategically valuable; multiple teams would benefit.\\n\\nOUTPUT STYLE\\nReturn concise but structured outputs. Separate facts, inferences, recommendations, missing data. When making proposals to CEO or CoS, attach evidence and a value hypothesis.\\n\\nSUCCESS DEFINITION\\nYou are successful when the enterprise spends less effort on weak opportunities and more effort on high-value, high-probability, high-profit opportunities.\\n\";\n\nconst contextBlock = JSON.stringify({\n    now: task.now_iso,\n    bot: task.bot_name,\n    task_kind: task.task,\n    open_inbox: inbox,\n    bot_context: ctx,\n    daily_cadence: task.daily_spec,\n    weekly_cadence: task.weekly_spec,\n}, null, 2);\n\nconst taskBlock = task.task === 'user_dm'\n    ? `The user sent you this message on Telegram:\\n${task.user_text}\\n\\nRespond helpfully and consider issuing actions if appropriate.`\n    : task.task === 'scheduled_tick'\n      ? `This is a scheduled cadence run. Decide what to do given the current context, open memos, and your role.`\n      : `An inter-agent invocation arrived from '${task.from_agent}': ${task.user_text || 'no message body'}. Decide how to respond.`;\n\nconst outputGuard = `\\n\\n[OUTPUT FORMAT]\\nReturn ONLY a JSON object (no prose, no markdown fences):\\n{\\n  \"reply_text\": \"<what to send back via Telegram; empty string when a scheduled run should stay silent>\",\\n  \"actions\": [\\n    {\"type\": \"memo\",     \"payload\": {\"to_agent\": \"researcher|chief_of_staff|cso|ceo|cleaner|all\", \"memo_type\": \"status|proposal|directive|question|decision\", \"priority\": \"urgent|high|normal|low\", \"subject\": \"...\", \"body_json\": {...}}},\\n    {\"type\": \"archive\",  \"payload\": {\"memo_id\": \"<uuid-from-open-inbox>\", \"reason\": \"...\"}},\\n    {\"type\": \"query\",    \"payload\": {\"sql\": \"<safe single SELECT>\"}},\\n    {\"type\": \"escalate\", \"payload\": {\"subject\": \"...\", \"body_json\": {...}}},\\n    {\"type\": \"cleanup\",  \"payload\": {\"slug\": \"...\", \"reason\": \"...\"}},\\n    {\"type\": \"no_op\",    \"payload\": {\"reason\": \"...\"}}\\n  ]\\n}\\nKeep actions <= 5. Never include non-whitelisted action types. User-facing commentary belongs in reply_text, not in alert actions.`;\n\nconst fullMessage = `${system}${globalProtocol}\\n\\n[CONTEXT]\\n${contextBlock}\\n\\n[TASK]\\n${taskBlock}${outputGuard}`;\n\nreturn [{ json: {\n    message: fullMessage,\n    session_id: `bridge-${task.bot_name}-${task.now_iso.slice(0,16).replace(/[:T-]/g,'')}`,\n    task_kind: task.task,\n    user_chat_id: task.chat_id,\n    bot_name: task.bot_name,\n} }];"
      },
      "id": "node_assemble_prompt",
      "name": "Assemble prompt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1340,
        400
      ]
    },
    {
      "id": "node_chat_direct",
      "name": "super-agent /webhook/bot-engine",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1560,
        400
      ],
      "parameters": {
        "method": "POST",
        "url": "={{ $env.SUPER_AGENT_URL || 'https://super-agent-production.up.railway.app' }}/webhook/bot-engine",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={ \"bot_name\": {{ JSON.stringify($json.bot_name || \"unknown\") }}, \"task_block\": {{ JSON.stringify($json.message || \"\") }}, \"session_id\": {{ JSON.stringify($json.session_id || \"default\") }}, \"task_kind\": {{ JSON.stringify($json.task || \"agent_invoke\") }}, \"api_key\": {{ JSON.stringify($env.N8N_API_KEY || \"\") }} }",
        "options": {
          "timeout": 180000,
          "response": {
            "response": {
              "neverError": true
            }
          },
          "retryOnFail": true,
          "maxTries": 3,
          "waitBetweenTries": 4000
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// \u2500\u2500 Pre-formed short-circuit (loop guard) \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 _upstream = $('Assemble prompt').first().json;\nif (_upstream && _upstream._pre_formed) {\n    let _pfActions;\n    try { _pfActions = JSON.parse(_upstream.message).actions; } catch(e) { _pfActions = []; }\n    return [{ json: {\n        reply_text: '',\n        actions: [{ type: 'no_op', risk: 'low',\n            payload: (_pfActions[0] && _pfActions[0].payload) || { reason: 'loop_guard' } }],\n        bot_name: _upstream.bot_name, user_chat_id: _upstream.user_chat_id,\n        task_kind: 'loop_escalation', model_used: 'loop_guard', parse_error: null\n    }}];\n}\n\n// \u2500\u2500 Standard parse + risk + validation \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 raw = $json.response || '';\nconst riskMap = { \"memo\": \"low\", \"archive\": \"low\", \"no_op\": \"low\",\n                  \"escalate\": \"low\", \"query\": \"medium\", \"cleanup\": \"high\" };\nconst defaultRisk = 'low';\n\nlet parsed = null, parseError = null;\ntry {\n    let cleaned = raw.trim().replace(/^```(?:json)?/, '').replace(/```$/, '').trim();\n    parsed = JSON.parse(cleaned);\n} catch (e) { parseError = e.message; }\nif (!parsed) {\n    try { const m = raw.match(/\\{[\\s\\S]*\\}/); if (m) parsed = JSON.parse(m[0]); } catch (e) {}\n}\n\nlet reply_text = '', actions = [];\nif (parsed && typeof parsed === 'object' && parsed.reply_text !== undefined) {\n    reply_text = typeof parsed.reply_text === 'string' ? parsed.reply_text : '';\n    actions = Array.isArray(parsed.actions) ? parsed.actions.slice(0, 5) : [];\n} else {\n    reply_text = '';\n    actions = [{ type: 'no_op', payload: {\n        reason: 'json_parse_failed',\n        parse_error: parseError || 'no structured response from model',\n        raw_preview: String(raw).slice(0, 200)\n    }}];\n}\n\n// \u2500\u2500 Placeholder validation \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\u2500\nconst BANNED = ['PASTE_', 'TBD', 'PLACEHOLDER', 'INSERT_HERE', 'YOUR_URL'];\nconst hasPlaceholder = (str) => BANNED.some(p => String(str).includes(p));\nif (hasPlaceholder(reply_text) || actions.some(a => hasPlaceholder(JSON.stringify(a)))) {\n    reply_text = '[Output validation failed \u2014 response contained placeholder values. Please provide the actual value.]';\n    actions = [{ type: 'no_op', payload: { reason: 'placeholder_detected' }}];\n}\n\n// \u2500\u2500 Internal tool leakage scrubbing (non-blocking) \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 leakageMap = {\n    'n8n workflow creation': 'automation system modification',\n    'n8n workflow modification': 'automation system modification',\n    'shell command (destructive)': 'system operation',\n    'shell command': 'system operation',\n    'SQL query': 'data operation',\n};\nfor (const [internal, safe] of Object.entries(leakageMap)) {\n    if (reply_text.toLowerCase().includes(internal.toLowerCase()))\n        reply_text = reply_text.replace(new RegExp(internal, 'gi'), safe);\n}\n\n// \u2500\u2500 Risk annotation \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst annotated = actions.map(a => {\n    const type = (a && typeof a.type === 'string') ? a.type : 'no_op';\n    let risk = riskMap[type] !== undefined ? riskMap[type] : defaultRisk;\n    return { type, risk, payload: a.payload || {} };\n});\n\nconst upstream = $('Assemble prompt').first().json;\n\n// \u2500\u2500 Telegram personality formatting \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 _formatTgMsg(raw, taskKind) {\n  if (!raw || raw.length < 3) return raw;\n\n  // Parse ISO timestamp for the footer\n  const _ts = new Date().toLocaleTimeString('en-GB', {hour:'2-digit',minute:'2-digit',timeZone:'UTC'}) + ' UTC';\n\n  // Build structured header\n  const _kind = (taskKind || 'report').replace(/_/g,' ');\n  let header = '\ud83d\udd2c *Researcher*\\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\\n';\n\n  // Transform the body: bold lines that look like labels, add section icons\n  let body = raw\n    .replace(/\\*\\*/g, '*')           // normalise bold markers\n    .replace(/^(#+)\\s+(.+)$/gm, (_, h, t) => '*' + t + '*')   // ## heading \u2192 bold\n    .replace(/^[-\u2022]\\s+/gm, '  \ud83e\udde9 ')                        // bullet \u2192 section icon\n    .replace(/^(\\w[^:\\n]{2,40}):\\s*(.+)$/gm, '*$1:* $2')    // Key: val \u2192 bold key\n    .replace(/ERROR|FAILED|CRITICAL|BLOCKED/g, '\u2757 $&')     // flag errors\n    .replace(/SUCCESS|COMPLETE|DONE|APPROVED/gi, '\u2705 $&')     // flag successes\n    .replace(/ACTION:|action:/g, '\ud83d\udca1 *ACTION:*')          // highlight actions\n    ;\n\n  // Escape Markdown special chars NOT inside existing bold spans\n  // (Telegram Markdown v1: escape [ ] only)\n  body = body.replace(/([\\[\\]])/g, '\\\\$1');\n\n  const footer = '\\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\\n_\ud83d\udcda Research \u00b7 Bridge OS_ \u00b7 _' + _ts + '_';\n\n  return header + body + footer;\n}\n\n// Apply formatting to reply_text if there is one\nif ($json && $json.reply_text && $json.reply_text.length > 3) {\n  const _taskKind = $json.task_kind || upstream && upstream.task_kind || '';\n  $json.reply_text = _formatTgMsg($json.reply_text, _taskKind);\n}\n\nreturn [{ json: {\n    reply_text, actions: annotated,\n    bot_name: upstream.bot_name, user_chat_id: upstream.user_chat_id,\n    task_kind: upstream.task_kind, model_used: $json.model_used || 'unknown',\n    parse_error: parseError\n}}];"
      },
      "id": "node_parse_response",
      "name": "Parse response + risk tag",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1780,
        400
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "has_reply",
              "leftValue": "={{ ($json.reply_text || '').trim().length }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "node_if_has_reply",
      "name": "If has reply",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2000,
        200
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://api.telegram.org/bot{{$env.Bridge_Researcher_bot || $env.Bridge_CEO_BOT}}/sendMessage",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json; charset=utf-8"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={ \"chat_id\": {{ $('Parse response + risk tag').first().json.user_chat_id || $env.BRIDGE_ADMIN_TELEGRAM_CHAT_ID }}, \"text\": \"{{ $('Parse response + risk tag').first().json.reply_text || '(no reply)' }}\" , \"parse_mode\": \"Markdown\" }",
        "options": {
          "timeout": 15000,
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      },
      "id": "node_reply_tg",
      "name": "Reply on Telegram",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2880,
        400
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://super-agent-production.up.railway.app/memory/ingest",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-Memory-Secret",
              "value": "={{$env.MEMORY_INGEST_SECRET}}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={ \"memories\": [ { \"content\": {{ JSON.stringify(($('Parse response + risk tag').first().json.reply_text || '(no reply)') + ' [actions=' + (($('Parse response + risk tag').first().json.actions || []).map(a => a.type).join(',')) + ']') }}, \"memory_type\": \"decision\", \"importance\": 3, \"source\": \"bridge_researcher_bot\"}]}",
        "options": {
          "timeout": 15000,
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      },
      "id": "node_memory_ingest",
      "name": "Memory ingest",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3100,
        400
      ]
    },
    {
      "parameters": {
        "jsCode": "const p = $json;\nconst out = [];\nfor (const a of (p.actions || [])) {\n    out.push({ json: {\n        action_type: a.type,\n        action_risk: a.risk,\n        action_payload: a.payload,\n        bot_name: p.bot_name,\n        user_chat_id: p.user_chat_id,\n        task_kind: p.task_kind,\n        model_used: p.model_used,\n        reply_text: p.reply_text,\n    }});\n}\n// Always emit at least one item so the reply path runs even when no actions.\nif (out.length === 0) {\n    out.push({ json: {\n        action_type: 'no_op',\n        action_risk: 'low',\n        action_payload: { reason: 'no actions from LLM' },\n        bot_name: p.bot_name,\n        user_chat_id: p.user_chat_id,\n        task_kind: p.task_kind,\n        model_used: p.model_used,\n        reply_text: p.reply_text,\n    }});\n}\nreturn out;"
      },
      "id": "node_split_actions",
      "name": "Split actions",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2000,
        400
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.action_risk }}",
                    "rightValue": "low",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "outputKey": "low"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.action_risk }}",
                    "rightValue": "medium",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "outputKey": "medium"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.action_risk }}",
                    "rightValue": "high",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "outputKey": "high"
            }
          ]
        },
        "options": {
          "allMatchingOutputs": false,
          "fallbackOutput": 2
        }
      },
      "id": "node_switch_risk",
      "name": "Switch by risk",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        2220,
        400
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "\nWITH input AS (\n  SELECT $1::jsonb AS p\n),\nmemo_insert AS (\n  INSERT INTO bridge.agent_memos\n    (from_agent, to_agent, memo_type, priority, subject, body_json, related_lead_id)\n  SELECT\n    $2,\n    COALESCE(p->>'to_agent','all'),\n    COALESCE(p->>'memo_type','status'),\n    COALESCE(p->>'priority','normal'),\n    COALESCE(p->>'subject','(no subject)'),\n    COALESCE(p->'body_json','{}'::jsonb) || '{\"attempt_count\": 0}'::jsonb,\n    NULLIF(p->>'related_lead_id','')::uuid\n  FROM input\n  WHERE $3 = 'memo'\n  RETURNING memo_id\n),\narchive_memo AS (\n  UPDATE bridge.agent_memos\n  SET status = 'resolved', resolved_at = NOW(), resolved_by = $2,\n      resolution_notes = COALESCE((SELECT p->>'reason' FROM input), 'archived by bot')\n  WHERE $3 = 'archive'\n    AND memo_id = (SELECT NULLIF((p->>'memo_id'),'')::uuid FROM input)\n  RETURNING memo_id\n),\nevent_log AS (\n  INSERT INTO bridge.workflow_events\n    (workflow_name, event_type, details_json)\n  SELECT\n    $2 || '_bot',\n    CASE $3\n      WHEN 'memo'    THEN 'memo_created'\n      WHEN 'archive' THEN 'memo_archived'\n      WHEN 'no_op'   THEN 'no_op'\n      ELSE 'other_low_risk'\n    END,\n    (SELECT p FROM input)\n  RETURNING event_id\n)\nSELECT\n  (SELECT memo_id FROM memo_insert)  AS memo_created,\n  (SELECT memo_id FROM archive_memo) AS memo_archived,\n  (SELECT event_id FROM event_log)   AS event_logged;\n",
        "options": {
          "queryReplacement": "={{ JSON.stringify($json.action_payload) }},{{ $json.bot_name }},{{ $json.action_type }}"
        }
      },
      "id": "node_exec_low_risk",
      "name": "Execute low-risk action",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        2440,
        300
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://api.telegram.org/bot{{$env.Bridge_Researcher_bot || $env.Bridge_CEO_BOT}}/sendMessage",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json; charset=utf-8"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={ \"chat_id\": {{$env.BRIDGE_ADMIN_TELEGRAM_CHAT_ID}}, \"text\": \"\\u26a0\\ufe0f  *Approval request from {{$json.bot_name}}*\\n\\n  Action:  `{{$json.action_type}}`\\n  Risk:    medium\\n  Payload: {{ JSON.stringify($json.action_payload).replace(/\"/g, \"'\").slice(0,600) }}\\n\\nReply *APPROVE {{$json.bot_name}} {{$json.action_type}}* to proceed, or ignore to reject.\\n\\u2014 Bridge Agents\", \"parse_mode\": \"Markdown\" }",
        "options": {
          "timeout": 15000,
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      },
      "id": "node_medium_approval",
      "name": "Medium-risk: approval DM",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2440,
        500
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO bridge.agent_memos (from_agent, to_agent, memo_type, priority, subject, body_json) VALUES ($1, 'chief_of_staff', 'approval_request', 'high',         'Pending medium-risk action awaiting user approval', $2::jsonb) RETURNING memo_id;",
        "options": {
          "queryReplacement": "={{ $json.bot_name }},{{ JSON.stringify({action_type: $json.action_type, action_payload: $json.action_payload}) }}"
        }
      },
      "id": "node_medium_memo",
      "name": "Medium-risk: log pending",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        2660,
        500
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO bridge.agent_memos (from_agent, to_agent, memo_type, priority, subject, body_json) VALUES ('researcher', 'ceo', 'escalation', 'urgent',         'HIGH-RISK action requires manual execution', $1::jsonb) RETURNING memo_id;",
        "options": {
          "queryReplacement": "={{ JSON.stringify({action_type: $json.action_type, action_payload: $json.action_payload}) }}"
        }
      },
      "id": "node_high_escalate",
      "name": "High-risk: escalate to CEO",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        2440,
        700
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://api.telegram.org/bot{{$env.Bridge_Researcher_bot || $env.Bridge_CEO_BOT}}/sendMessage",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json; charset=utf-8"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={ \"chat_id\": {{$env.BRIDGE_ADMIN_TELEGRAM_CHAT_ID}}, \"text\": \"\\ud83d\\udea8  *HIGH-RISK action blocked (escalated to CEO)*\\n\\n  From:    {{$json.bot_name}}\\n  Action:  `{{$json.action_type}}`\\n  Payload: {{ JSON.stringify($json.action_payload).slice(0,600) }}\\n\\nThis action was NOT executed. Review the agent_memos table for the escalation.\\n\\u2014 Bridge Agents\", \"parse_mode\": \"Markdown\" }",
        "options": {
          "timeout": 15000,
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      },
      "id": "node_high_alert",
      "name": "High-risk: urgent DM",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2660,
        700
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={\"ok\": true, \"bot\": {{JSON.stringify($(\"Parse response + risk tag\").first().json.bot_name)}}}",
        "options": {}
      },
      "id": "node_respond",
      "name": "Respond OK",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        3320,
        400
      ]
    }
  ],
  "connections": {
    "Telegram: user DM": {
      "main": [
        [
          {
            "node": "Build task payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule: daily": {
      "main": [
        [
          {
            "node": "Build task payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule: weekly": {
      "main": [
        [
          {
            "node": "Build task payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook: invoke": {
      "main": [
        [
          {
            "node": "Build task payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build task payload": {
      "main": [
        [
          {
            "node": "Read enabled flag",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read enabled flag": {
      "main": [
        [
          {
            "node": "If enabled",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If enabled": {
      "main": [
        [
          {
            "node": "Fetch open inbox",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Fetch open inbox": {
      "main": [
        [
          {
            "node": "Fetch bot context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch bot context": {
      "main": [
        [
          {
            "node": "Assemble prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Assemble prompt": {
      "main": [
        [
          {
            "node": "super-agent /webhook/bot-engine",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse response + risk tag": {
      "main": [
        [
          {
            "node": "If has reply",
            "type": "main",
            "index": 0
          },
          {
            "node": "Memory ingest",
            "type": "main",
            "index": 0
          },
          {
            "node": "Split actions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If has reply": {
      "main": [
        [
          {
            "node": "Reply on Telegram",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Reply on Telegram": {
      "main": [
        [
          {
            "node": "Respond OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Memory ingest": {
      "main": [
        [
          {
            "node": "Respond OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split actions": {
      "main": [
        [
          {
            "node": "Switch by risk",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Switch by risk": {
      "main": [
        [
          {
            "node": "Execute low-risk action",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Medium-risk: approval DM",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "High-risk: escalate to CEO",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execute low-risk action": {
      "main": [
        [
          {
            "node": "Respond OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Medium-risk: approval DM": {
      "main": [
        [
          {
            "node": "Medium-risk: log pending",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Medium-risk: log pending": {
      "main": [
        [
          {
            "node": "Respond OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "High-risk: escalate to CEO": {
      "main": [
        [
          {
            "node": "High-risk: urgent DM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "High-risk: urgent DM": {
      "main": [
        [
          {
            "node": "Respond OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "super-agent /webhook/bot-engine": {
      "main": [
        [
          {
            "node": "Parse response + risk tag",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}