{
  "name": "Triple Pendulum Pipeline Orchestrator",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "triple-pendulum-pipeline",
        "responseMode": "onReceived",
        "options": {}
      },
      "id": "a1b2c3d4-0001-0001-0001-000000000001",
      "name": "Training Complete",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "language": "javaScript",
        "jsCode": "// ---- CONFIG ----\nconst PIPELINE_SECRET   = 'YOUR_PIPELINE_SECRET';\nconst LAUNCHER_SECRET   = 'YOUR_LAUNCHER_SECRET';\nconst LAUNCHER_URL      = 'http://10.1.4.232:8765/launch';\nconst TELEGRAM_BOT_URL  = 'https://api.telegram.org/botYOUR_TELEGRAM_BOT_TOKEN/sendMessage';\nconst TELEGRAM_CHAT_ID  = 935389847;\n\n// ---- STAGE DEFINITIONS ----\nconst STAGES = {\n  M2: {\n    pass_criteria: { ep7_success_rate: 0.80 },\n    pass: { stage: 'M3b', config: 'training/configs/m3b_all_eps_tqc.yaml', module: 'training.train_m3_all_eps' },\n    fail: { stage: 'HUMAN_REVIEW', note: 'M2 failed \u2014 manual intervention required' }\n  },\n  M3: {\n    pass_criteria: { overall_success_rate: 0.75 },\n    pass: { stage: 'M3b', config: 'training/configs/m3b_all_eps_tqc.yaml', module: 'training.train_m3_all_eps' },\n    fail: { stage: 'M3b', config: 'training/configs/m3b_all_eps_tqc.yaml', module: 'training.train_m3_all_eps' }\n  },\n  M3b: {\n    pass_criteria: { overall_success_rate: 0.75 },\n    pass: { stage: 'M4', config: 'training/configs/m4_transitions_tqc.yaml', module: 'training.train_m4_transitions', note: 'Set pretrained_policy in config before launch' },\n    fail: { stage: 'M3c', config: 'training/configs/m3c_all_eps_tqc.yaml', module: 'training.train_m3_all_eps' }\n  },\n  M3c: {\n    pass_criteria: { overall_success_rate: 0.75 },\n    pass: { stage: 'M4', config: 'training/configs/m4_transitions_tqc.yaml', module: 'training.train_m4_transitions', note: 'Set pretrained_policy in config before launch' },\n    fail: { stage: 'HUMAN_REVIEW', note: 'M3c failed \u2014 hyperparameter tuning needed' }\n  },\n  M4: {\n    pass_criteria: { overall_success_rate: 0.80 },\n    pass: { stage: 'HUMAN_REVIEW', note: 'M4 passed (\u2265 80% over 56 transitions) \u2014 proceed to M5 domain randomization' },\n    fail: { stage: 'HUMAN_REVIEW', note: 'M4 failed \u2014 extend budget or tune curriculum' }\n  }\n};\n\n// ---- VALIDATE SECRET (constant-time, n8n disallows crypto module) ----\nconst _raw = $input.first().json;\nconst body = _raw.body || _raw;\nconst incomingSecret = String(body.pipeline_secret || '');\nfunction safeEqual(a, b) {\n  if (a.length !== b.length) return false;\n  let mismatch = 0;\n  for (let i = 0; i < a.length; i++) mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);\n  return mismatch === 0;\n}\nif (!safeEqual(incomingSecret, PIPELINE_SECRET)) {\n  throw new Error('Invalid pipeline secret');\n}\n\n// ---- PARSE ----\nconst milestone = body.milestone || 'UNKNOWN';\nconst metrics   = body.metrics   || {};\nconst runName   = body.run_name  || 'unknown';\nconst runId     = body.run_id    || 'unknown';\n\n// ---- EVALUATE ----\nconst stageDef = STAGES[milestone];\nlet passed = false;\nlet next   = { stage: 'HUMAN_REVIEW', note: 'Stage not defined' };\nlet reasons = [];\n\nif (stageDef) {\n  const criteria = stageDef.pass_criteria;\n  const checks = Object.entries(criteria).map(([k, threshold]) => {\n    const actual = typeof metrics[k] === 'number' ? metrics[k] : 0;\n    const ok = actual >= threshold;\n    reasons.push(`${k}: ${(actual*100).toFixed(0)}% (need ${(threshold*100).toFixed(0)}%) ${ok ? '\\u2705' : '\\u274c'}`);\n    return ok;\n  });\n  passed = checks.every(Boolean);\n  next   = passed ? stageDef.pass : stageDef.fail;\n} else {\n  reasons.push(`unknown milestone: ${milestone} \\u274c`);\n  next = { stage: 'HUMAN_REVIEW', note: `Unknown milestone '${milestone}' \u2014 add it to STAGES.` };\n}\n\n// ---- LAUNCH PAYLOAD ----\nconst canLaunch = next.stage && next.stage !== 'HUMAN_REVIEW' && next.module && next.config;\nconst launchPayload = canLaunch ? { secret: LAUNCHER_SECRET, module: next.module, config: next.config } : null;\n\n// ---- TELEGRAM MESSAGE ----\nconst bars = [0,1,2,3,4,5,6,7].map(i => {\n  const sr = metrics['ep' + i + '_success_rate'];\n  const b = typeof sr === 'number' ? (sr >= 0.8 ? '\\uD83D\\uDFE2' : sr >= 0.5 ? '\\uD83D\\uDFE1' : '\\uD83D\\uDD34') : '\\u25A1';\n  return 'EP' + i + ': ' + b + ' ' + (typeof sr === 'number' ? (sr*100).toFixed(0) + '%' : 'n/a');\n}).join('  ');\n\nconst icon = passed ? '\\u2705' : '\\u26A0\\uFE0F';\nconst lines = [\n  '*Triple Pendulum \u2014 ' + milestone + ' ' + icon + '*',\n  'Run: `' + runName + '`',\n  '',\n  '*Criteria:*',\n  reasons.join('\\n'),\n  '',\n  '*Per-EP success:*',\n  bars,\n  '',\n  canLaunch\n    ? '*Next: launching ' + next.stage + '*\\n`' + next.config + '`'\n    : '*Next: ' + next.stage + '*\\n' + (next.note || '')\n];\nconst message = lines.join('\\n');\n\nreturn [{ json: {\n  milestone, runName, runId, passed, next, launchPayload, hasNextStage: canLaunch, message,\n  telegramBotUrl: TELEGRAM_BOT_URL,\n  telegramChatId: TELEGRAM_CHAT_ID,\n  launcherUrl: LAUNCHER_URL,\n  metrics\n} }];\n"
      },
      "id": "a1b2c3d4-0002-0002-0002-000000000002",
      "name": "Evaluate & Route",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        480,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "cond-launch",
              "leftValue": "={{ $json.hasNextStage }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "a1b2c3d4-0003-0003-0003-000000000003",
      "name": "Has Next Stage?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        720,
        300
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $('Evaluate & Route').first().json.launcherUrl }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($('Evaluate & Route').first().json.launchPayload) }}",
        "options": {
          "timeout": 10000
        }
      },
      "id": "a1b2c3d4-0004-0004-0004-000000000004",
      "name": "Launch Next Training",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        960,
        160
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $('Evaluate & Route').first().json.telegramBotUrl }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ chat_id: $('Evaluate & Route').first().json.telegramChatId, text: $('Evaluate & Route').first().json.message, parse_mode: 'Markdown', disable_web_page_preview: true }) }}",
        "options": {
          "timeout": 10000
        }
      },
      "id": "a1b2c3d4-0005-0005-0005-000000000005",
      "name": "Telegram Notify",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1200,
        300
      ]
    }
  ],
  "connections": {
    "Training Complete": {
      "main": [
        [
          {
            "node": "Evaluate & Route",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Evaluate & Route": {
      "main": [
        [
          {
            "node": "Has Next Stage?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Next Stage?": {
      "main": [
        [
          {
            "node": "Launch Next Training",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Telegram Notify",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Launch Next Training": {
      "main": [
        [
          {
            "node": "Telegram Notify",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Telegram Notify": {
      "main": [
        []
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "callerPolicy": "workflowsFromSameOwner",
    "availableInMCP": false
  }
}