{
  "name": "bridge_cto_bot",
  "nodes": [
    {
      "parameters": {
        "updates": [
          "message"
        ],
        "additionalFields": {}
      },
      "id": "node_trigger_tg_cto",
      "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 */4 * * *"
            }
          ]
        }
      },
      "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 11 * * 1"
            }
          ]
        }
      },
      "id": "node_trigger_weekly",
      "name": "Schedule: weekly",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        240,
        600
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "bridge-cto-invoke",
        "responseMode": "lastNode",
        "options": {}
      },
      "id": "node_trigger_webhook_cto",
      "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: 'cto',\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: \"cto\",\n    task,\n    user_text,\n    chat_id,\n    from_agent,\n    now_iso: new Date().toISOString(),\n    daily_spec: [{\"hour\": 9, \"minute\": 0, \"task_name\": \"daily_project_pulse\"}],\n    weekly_spec: [{\"weekday\": 1, \"hour\": 10, \"minute\": 0, \"task_name\": \"weekly_project_review\"}],\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 COALESCE((SELECT (value = 'true') FROM bridge.system_limits WHERE key = 'cto_bot_enabled'), true) AS 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\nFROM (\n  SELECT memo_id, from_agent, memo_type, priority, subject, body_json, created_at,\n         CASE priority WHEN 'urgent' THEN 0 WHEN 'high' THEN 1 WHEN 'normal' THEN 2 ELSE 3 END AS priority_rank\n  FROM bridge.agent_memos\n  WHERE to_agent IN ('cto', 'all') AND status = 'open'\n  LIMIT 20\n) 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    'pending_reviews', (\n        SELECT COALESCE(jsonb_agg(row_to_json(m)), '[]'::jsonb)\n        FROM (\n            SELECT memo_id, from_agent, subject, body_json, created_at\n            FROM bridge.agent_memos\n            WHERE to_agent = 'cto'\n              AND memo_type IN ('cto_review_request','architecture_request','feasibility_request')\n              AND status = 'open'\n            ORDER BY created_at DESC LIMIT 10\n        ) m\n    ),\n    'recent_cro_approvals', (\n        SELECT COALESCE(jsonb_agg(row_to_json(c)), '[]'::jsonb)\n        FROM (\n            SELECT project_name, cro_score, recommendation, created_at\n            FROM bridge.cro_evaluations\n            WHERE recommendation = 'APPROVE' AND created_at >= NOW() - INTERVAL '7d'\n            ORDER BY created_at DESC LIMIT 5\n        ) c\n    ),\n    'project_memory_recent', (\n        SELECT COALESCE(jsonb_agg(row_to_json(pm)), '[]'::jsonb)\n        FROM (\n            SELECT project_name, outcome, cro_score, key_insights, updated_at\n            FROM bridge.project_memory\n            WHERE updated_at >= NOW() - INTERVAL '30d'\n            ORDER BY updated_at DESC LIMIT 8\n        ) pm\n    ),\n    'stalled_builds', (\n        SELECT COALESCE(jsonb_agg(row_to_json(s)), '[]'::jsonb)\n        FROM (\n            SELECT memo_id, from_agent, subject, created_at\n            FROM bridge.agent_memos\n            WHERE to_agent = 'cto'\n              AND status = 'open'\n              AND created_at < NOW() - INTERVAL '3 days'\n            ORDER BY created_at ASC LIMIT 5\n        ) s\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// \u2500\u2500 Structured brief normalisation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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// When user_text is a short, unstructured brief (< 60 words), wrap it in a\n// structured envelope so downstream agents (Website bot, Programmer bot) get\n// well-formed input rather than raw free text. This prevents 'had trouble\n// producing a structured response' failures.\nfunction normaliseBrief(text) {\n    if (!text) return text;\n    const words = text.trim().split(/\\s+/);\n    if (words.length >= 60) return text; // already verbose enough\n    const lc = text.toLowerCase();\n    // Detect website/landing-page briefs\n    if (lc.includes('landing page') || lc.includes('website') || lc.includes('site')) {\n        return JSON.stringify({\n            type: 'website_brief',\n            raw_brief: text,\n            instructions: 'Parse the raw_brief and fill in as many fields as possible: niche, location, goal, sections, colour_scheme, call_tracking, deadline. Then pass the structured payload to the Website bot.'\n        });\n    }\n    return text;\n}\nconst normalised_user_text = normaliseBrief(task.user_text);\n\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_CTO_BOT \u2014 Chief Technology Officer of Bridge Digital Solutions.\\\\n\\\\nROLE\\\\nYou are the technical brain of the organization. You own ALL technical decisions, architecture, and system efficiency. You sit in the DECISION LAYER and PRE-BUILD LAYER \u2014 you evaluate feasibility before anything is built.\\\\n\\\\nPOSITION IN THE DECISION FLOW\\\\nBizDev/Research \u2192 CEO evaluates opportunity \u2192 COS orchestrates validation \u2192 [CRO + Finance + Security + CTO in parallel] \u2192 APPROVED/REJECTED\\\\nAfter approval: COS \u2192 PM \u2192 CTO defines architecture \u2192 PM assigns builders\\\\n\\\\nYou receive memos of type 'cto_review_request' from COS or PM. You MUST respond with memo_type='cto_review_complete' containing your full technical assessment.\\\\n\\\\nPRIMARY RESPONSIBILITIES\\\\n1. TECHNICAL FEASIBILITY \u2014 Can this be built? What stack, APIs, tools. Scalability. Blockers.\\\\n2. ARCHITECTURE DESIGN \u2014 Define system structure, choose frameworks, design n8n workflows, GitHub deployments, container vs static decisions.\\\\n3. BUILD STRATEGY \u2014 Decide: template vs custom, Lovable vs GitHub, container vs static. Optimize speed and cost. Reuse before creating new.\\\\n4. COST OPTIMIZATION \u2014 Recommend model selection (Haiku for simple, Sonnet for complex). Minimize API calls. Estimate token budget.\\\\n5. RESOURCE MANAGEMENT \u2014 Control live site count, container usage. Coordinate with Finance.\\\\n6. PERFORMANCE & RELIABILITY \u2014 Ensure stability, detect inefficiencies, propose improvements.\\\\n\\\\nREQUIRED OUTPUT FOR cto_review_request:\\\\nReply with memo_type='cto_review_complete' to the requesting agent (pm or chief_of_staff):\\\\nProvide in body_json: project_name, feasibility (high/medium/low), architecture_plan (stack, tools, deployment, estimated_build_time_days), cost_strategy (model_recommendation haiku/sonnet/mixed, estimated_token_budget, api_calls_per_day), efficiency_plan (reuse_opportunities, optimizations list), risks (technical list, scalability list), recommendation (PROCEED/MODIFY/REJECT), modification_notes if MODIFY or REJECT.\\\\n\\\\nFEASIBILITY SCORING:\\\\n- high: current stack, no new deps, < 5 days\\\\n- medium: new integration needed, 5-14 days, manageable risk\\\\n- low: significant unknowns, new infrastructure, > 14 days or high failure risk\\\\n\\\\nBEFORE EVERY EVALUATION:\\\\n1. Check bot_context for similar past projects and outcomes\\\\n2. Check if CRO already approved \u2014 tech plan must align with the revenue model\\\\n3. REUSE before creating new \u2014 check if existing infrastructure covers the need\\\\n\\\\nSCHEDULED REVIEWS (daily 10:30 UTC, weekly Monday 11:00 UTC):\\\\n- Scan for stalled builds (projects with cto_review_complete but no PM progress)\\\\n- Flag performance regressions or efficiency issues\\\\n- Report to COS on technical health\\\\n\\\\nAUTHORITY\\\\nYou MAY: reject technically infeasible projects, recommend model/stack changes, define architecture, request COS override for blocked builds.\\\\nYou MAY NOT: override CRO scoring, approve budget spend (Finance authority), deploy to production without PM + Security clearance.\\\\n\\\\nESCALATE TO COS WHEN: architecture decision requires CEO input, budget exceeds Finance threshold, security risk discovered during technical review, build is stalled > 3 days.\\\\n\\\\nOUTPUT STYLE\\\\nTelegram replies: use \\\\\\\"\u2699\ufe0f CTO REPORT\\\\\\\" header. Be technical but concise. Surface risks clearly. Never oversell feasibility.\\\\n\\\\nSUCCESS DEFINITION\\\\nYou are successful when projects are built efficiently, with no overengineering, within budget, and with zero avoidable technical failures.\\n\"\\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${normalised_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}': ${normalised_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": "// Passthrough \u2014 bot-engine already parsed, validated, and risk-annotated the response.\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}\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\udda5\ufe0f *Chief Technology Officer*\\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, '  \ud83d\udd27 ')                        // 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, '\ud83d\udd25 $&')     // flag errors\n    .replace(/SUCCESS|COMPLETE|DONE|APPROVED/gi, '\u2705 $&')     // flag successes\n    .replace(/ACTION:|action:/g, '\ud83d\ude80 *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_\u2699\ufe0f CTO \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:  $json.reply_text  || '',\n    actions:     Array.isArray($json.actions) ? $json.actions : [],\n    model_used:  $json.model_used  || 'unknown',\n    parse_error: $json.parse_error || null,\n    bot_name:    upstream.bot_name,\n    user_chat_id: upstream.user_chat_id,\n    task_kind:   upstream.task_kind,\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_CTO_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_pm_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_CTO_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 ('cto', '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_CTO_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"
  }
}