Skip to content
A-Team

Documentation

Live platform reference

Served directly from the public A-Team MCP (mcp.ateam-ai.com), so it always matches what your assistant sees. Your AI assistant reads the same content through the MCP tools.

Platform overview

ateam_get_spec(overview)
{
  "service": "@adas/skill-validator",
  "version": "1.0.0",
  "description": "A-Team External Agent API — learn, build, validate, and deploy A-Team multi-agent solutions",
  "getting_started": [
    "1. GET /spec/skill — read the skill specification (note the auto_expand section for minimal definitions)",
    "2. GET /spec/examples/skill — study a complete working example",
    "3. Build your skill definition — define only what is unique (problem, tools, guardrails). The platform auto-generates intents, workflows, scenarios, and role when you validate or deploy.",
    "4. POST /validate/skill — validate and fix errors",
    "5. GET /spec/solution — read the solution specification when ready to compose skills",
    "6. POST /validate/solution — validate the full solution",
    "7. POST /deploy/solution — deploy everything to A-Team Core (the Skill Builder auto-generates MCP servers from your tool definitions — no slug or Python code needed)",
    "8. GET /deploy/solutions/:id/definition — read back the deployed solution to verify",
    "9. GET /deploy/solutions/:id/skills/:skillId — read back individual skills to verify",
    "10. PATCH /deploy/solutions/:id/skills/:skillId — update skills incrementally (tools_push, tools_delete, etc.) without re-deploying everything",
    "11. POST /deploy/solutions/:id/skills/:skillId/redeploy — after PATCH, redeploy just that skill (regenerates MCP server, pushes to A-Team Core)",
    "--- Operate & Debug ---",
    "12. POST /deploy/solutions/:id/skills/:skillId/test — test a skill (sync: wait for result, or async: true to get job_id immediately)",
    "13. POST /deploy/solutions/:id/skills/:skillId/test-pipeline — test decision pipeline only (intent + planning, NO tool execution). Returns intent classification, first planned action, and timing.",
    "14. GET /deploy/solutions/:id/skills/:skillId/test/:jobId — poll async test progress (iteration, steps, result)",
    "15. DELETE /deploy/solutions/:id/skills/:skillId/test/:jobId — abort a running test",
    "16. GET /deploy/solutions/:id/logs — view execution logs (job traces, tool calls, errors)",
    "17. GET /deploy/solutions/:id/metrics — analyze execution metrics (timing, bottlenecks, signals)",
    "18. GET /deploy/solutions/:id/diff — compare Builder definitions vs what is deployed in Core",
    "19. GET /deploy/solutions/:id/connectors/:connectorId/source — inspect connector source code",
    "--- Voice Testing ---",
    "20. POST /deploy/voice-test — simulate a voice conversation (text-based E2E test). Send { messages: [\"Hello\", \"Acme\", \"Check vehicle 7\"], phone_number?: \"+14155551234\" }. Returns full conversation with verification status, tool calls, and skill results.",
    "--- GitHub Version Control (dev/main branching) ---",
    "21. Every successful deploy auto-pushes to a \"dev\" branch in GitHub (tenant--solution-id) with a date-based version tag (dev-YYYY-MM-DD-NNN). GitHub push is ASYNC — deploy responds immediately, push happens in background.",
    "22. The \"main\" branch is production. Promote tested dev versions to main via POST /deploy/solutions/:id/promote. This gives you manual control over production releases.",
    "23. GET /deploy/solutions/:id/github/status — check repo existence, latest commit, verify background push completed",
    "24. GET /deploy/solutions/:id/github/log — view commit history",
    "25. GET /deploy/solutions/:id/github/read?path=connectors/my-mcp/server.js — read a file from repo",
    "26. PATCH /deploy/solutions/:id/github/patch — edit files in repo (single or multi-file). Body: { files: [{ path, content }] } or { path, content } for single file",
    "27. POST /deploy/solutions/:id/github/pull-connectors — pull connector source from GitHub repo as mcp_store format (for github-first deploys)",
    "--- Version Management ---",
    "28. GET /deploy/solutions/:id/versions/dev — list all available dev versions (tags) with dates and commit SHAs",
    "29. POST /deploy/solutions/:id/promote — promote latest (or specific) dev version to main. Creates prod-YYYY-MM-DD-NNN tag on main.",
    "30. POST /deploy/solutions/:id/rollback — rollback main to a previous production tag. Body: { target: \"prod-YYYY-MM-DD-001\" } (also accepts commit SHA or legacy safe-* tag). ADDITIVE — creates a new commit on top of main with the target tree; full history preserved.",
    "--- GitHub-First Iteration Loop ---",
    "31. After the first deploy, iterate on connector code via GitHub: PATCH /deploy/solutions/:id/github/patch to edit code → POST /deploy/solution with github:true to redeploy from repo (no inline mcp_store needed)",
    "32. IMPORTANT: Each connector has INDEPENDENT source code. To add a feature to multiple connectors, read and patch EACH connector separately. Use GET /deploy/solutions/:id/connectors/:connectorId/source to read current code before patching."
  ],
  "endpoints": {
    "/spec/enums": {
      "method": "GET",
      "description": "All A-Team enum values in a flat lookup (phases, data types, classifications, tones, etc.)"
    },
    "/spec/skill": {
      "method": "GET",
      "description": "Complete A-Team skill specification: schema, validation rules, system tools, agent guide, and template"
    },
    "/spec/solution": {
      "method": "GET",
      "description": "Complete A-Team solution specification: multi-skill architecture, grant economy, handoffs, routing, security contracts, agent guide, and template"
    },
    "/spec/workflows": {
      "method": "GET",
      "description": "Builder workflows — the step-by-step state machines for building skills and solutions. Use this to guide users through the build process."
    },
    "/spec/mobile-connector": {
      "method": "GET",
      "description": "Mobile connector specification — build functional connectors (background services) for ateam-mobile. Access device capabilities via Native Bridge SDK."
    },
    "/spec/ui-plugins": {
      "method": "GET",
      "description": "UI Plugins specification — build interactive dashboards for web (iframe) and mobile (React Native). Covers render modes, plugin SDK, bundle pipeline, and deployment."
    },
    "/spec/host-contract": {
      "method": "GET",
      "description": "Host contract — normative boundary between any host shell (mobile app, web shell, kiosk, watch) and the solutions it renders. Defines the ownership matrix, forbidden host behaviors, host capability allow-list, and validator drift check. Use this when reviewing a host implementation or when designing a portable solution. Companion to surface field on ui_plugins."
    },
    "/spec/multi-user-connector": {
      "method": "GET",
      "description": "Multi-user connector guide — how to build connectors that isolate data per user (actor). Covers HTTP vs stdio transport, actor context propagation, and complete code examples."
    },
    "/spec/platform-connectors": {
      "method": "GET",
      "description": "Platform connector catalog — live tool schemas for all built-in connectors (memory, browser, gmail, whatsapp, etc.). Includes inter-connector calling pattern with complete code example."
    },
    "/spec/sdk": {
      "method": "GET",
      "description": "@ateam/sdk runtime API reference — platform, context, memory, progress, log, llm modules. Used by custom connectors and skill code to access platform capabilities."
    },
    "/spec/python_helpers": {
      "method": "GET",
      "description": "Python orchestration helpers — the adas.* namespace auto-injected into run_python_script. Read this when designing a persona that orchestrates logic via Python (read state → call tool → checkpoint → status). Without these helpers, agents emit 5-10x bigger / brittler scripts that hand-roll JSON parsing and tool delegation."
    },
    "/spec/widgets": {
      "method": "GET",
      "description": "Widgets framework — what a widget is (UI plugin exposed by an MCP connector), how identity / render / capabilities / uiActions / surface fit together, and the three wiring routes (connector-bundled, solution-declared, skill-declared HTML). Read this when picking ready-to-use widgets for a solution or wiring a custom one. Companion to /spec/ui-plugins (every-field reference) and /spec/host-contract (host/solution boundary)."
    },
    "/spec/examples": {
      "method": "GET",
      "description": "Index of complete, runnable examples (skill, connector, connector-ui, solution)"
    },
    "/spec/examples/skill": {
      "method": "GET",
      "description": "Complete \"Order Support Agent\" skill that passes all 5 validation stages"
    },
    "/spec/examples/connector": {
      "method": "GET",
      "description": "Standard stdio MCP connector for order management"
    },
    "/spec/examples/connector-ui": {
      "method": "GET",
      "description": "UI-capable connector with dashboard plugins (ui_capable: true)"
    },
    "/spec/examples/solution": {
      "method": "GET",
      "description": "Full \"E-Commerce Customer Service\" solution — 3 skills, grants, handoffs, routing"
    }
  },
  "also_available": {
    "POST /validate/skill": "Validate a single skill definition (5-stage pipeline)",
    "POST /validate/solution": "Validate a solution (cross-skill contracts + LLM quality scoring)",
    "POST /deploy/mcp-store/:connectorId": "Pre-upload large connector source files (e.g., dist bundles). They are auto-merged into the next POST /deploy/solution. Body: { files: [{ path, content }] }",
    "GET /deploy/mcp-store": "List all pre-staged connector files",
    "DELETE /deploy/mcp-store/:connectorId": "Remove pre-staged files for a connector",
    "POST /deploy/connector": "Deploy a connector via Skill Builder → A-Team Core",
    "POST /deploy/skill": "Deploy a single skill via Skill Builder (requires solution_id)",
    "POST /deploy/solution": "Deploy a full solution via Skill Builder → A-Team Core (identity + connectors + skills). No slug or Python MCP code needed.",
    "GET /deploy/solutions": "List all solutions stored in the Skill Builder",
    "GET /deploy/status/:solutionId": "Get aggregated deploy status — skills, connectors, A-Team Core health",
    "DELETE /deploy/solutions/:solutionId": "Remove a solution from the Skill Builder",
    "GET /deploy/solutions/:solutionId/definition": "Read back the full solution definition (identity, grants, handoffs, routing)",
    "GET /deploy/solutions/:solutionId/skills": "List skills in a solution (summaries with original and internal IDs)",
    "GET /deploy/solutions/:solutionId/skills/:skillId": "Read back a full skill definition (accepts original or internal skill ID)",
    "PATCH /deploy/solutions/:solutionId": "Update solution definition incrementally (grants, handoffs, routing, identity)",
    "PATCH /deploy/solutions/:solutionId/skills/:skillId": "Update a skill incrementally (tools, intents, policy, engine — accepts original or internal ID)",
    "POST /deploy/solutions/:solutionId/skills/:skillId/redeploy": "Re-deploy a single skill after PATCH — regenerates MCP server and pushes to A-Team Core",
    "DELETE /deploy/solutions/:solutionId/skills/:skillId": "Remove a single skill from a solution (accepts original or internal ID)",
    "DELETE /deploy/solutions/:solutionId/connectors/:connectorId": "Remove a connector from a solution — stops + deletes from Core, removes from solution definition (grants, platform_connectors), removes from skill connectors arrays, and cleans mcp-store files",
    "GET /deploy/solutions/:solutionId/validate": "Re-validate solution from stored state (structural + cross-skill checks)",
    "GET /deploy/solutions/:solutionId/skills/:skillId/validate": "Re-validate a single skill from stored state",
    "GET /deploy/solutions/:solutionId/connectors/health": "Connector health — status, discovered tools, errors from A-Team Core",
    "GET /deploy/solutions/:solutionId/skills/:skillId/conversation": "Skill conversation history — returns chat messages, optional ?limit=N",
    "GET /deploy/solutions/:solutionId/health": "Live health check — cross-checks definition vs A-Team Core (skills deployed, connectors connected, issues)",
    "POST /deploy/solutions/:solutionId/chat": "Send a message to the Solution Bot — returns AI response with state updates and suggested focus",
    "POST /deploy/solutions/:solutionId/redeploy": "Re-deploy ALL skills at once — regenerates MCP servers and pushes to A-Team Core",
    "POST /deploy/solutions/:solutionId/skills": "Add a new skill to an existing solution — creates, links, and updates solution topology",
    "GET /deploy/solutions/:solutionId/export": "Export solution as a JSON bundle — compatible with POST /deploy/solution for re-import",
    "GET /deploy/solutions/:solutionId/logs": "Execution logs — recent jobs with step traces, tool calls, errors, timing. Query: ?skill_id=X&limit=10&job_id=X",
    "POST /deploy/solutions/:solutionId/skills/:skillId/test": "Test a skill — sync (wait for result) or async (Body: { message, async: true } returns job_id immediately)",
    "POST /deploy/solutions/:solutionId/skills/:skillId/test-pipeline": "Test decision pipeline only — runs intent detection + first planner iteration WITHOUT executing tools. Returns intent classification, planned action, and timing. Body: { message }",
    "GET /deploy/solutions/:solutionId/skills/:skillId/test/:jobId": "Poll async test progress — iteration, steps, status, result, elapsed_ms",
    "DELETE /deploy/solutions/:solutionId/skills/:skillId/test/:jobId": "Abort a running test",
    "GET /deploy/solutions/:solutionId/metrics": "Execution metrics — timing, bottlenecks, tool stats, signals. Query: ?job_id=X or ?skill_id=X",
    "GET /deploy/solutions/:solutionId/connectors/:connectorId/source": "Connector source code — read the MCP server files",
    "GET /deploy/solutions/:solutionId/diff": "Diff Builder vs Core — shows undeployed, orphaned, or changed skills. Query: ?skill_id=X",
    "GET /deploy/solutions/:solutionId/github/status": "GitHub repo status — exists, latest commit, URL. Use to verify async background push completed.",
    "GET /deploy/solutions/:solutionId/github/log": "GitHub commit history — recent commits with message, author, date",
    "GET /deploy/solutions/:solutionId/github/read": "Read a file from GitHub repo — query: ?path=connectors/my-mcp/server.js",
    "POST /deploy/solutions/:solutionId/github/push": "Force-push current solution state to GitHub (normally auto-pushed on deploy)",
    "PATCH /deploy/solutions/:solutionId/github/patch": "Edit files in GitHub repo — single file { path, content } or multi-file { files: [{ path, content }] }",
    "POST /deploy/solutions/:solutionId/github/pull": "Pull full solution from GitHub and re-deploy (full round-trip)",
    "POST /deploy/solutions/:solutionId/github/pull-connectors": "Pull ONLY connector source files from GitHub as mcp_store format — used by github-first deploys",
    "GET /deploy/solutions/:solutionId/versions/dev": "List all dev versions — tags with dates, counters, and commit SHAs",
    "POST /deploy/solutions/:solutionId/promote": "Promote dev → main (production). Body: { tag?: \"dev-2026-03-11-005\" } — omit tag to promote latest. Creates prod-YYYY-MM-DD-NNN tag.",
    "POST /deploy/solutions/:solutionId/rollback": "Rollback main to previous production tag. Body: { target: \"prod-YYYY-MM-DD-001\" } (also accepts commit SHA / legacy safe-* tag). ADDITIVE — preserves history.",
    "GET /health": "Health check"
  },
  "deploy_guide": {
    "_note": "All deploy routes proxy through the Skill Builder backend, which stores everything (visible in Skill Builder UI), auto-generates Python MCP servers from skill tool definitions, and pushes to A-Team Core.",
    "POST /deploy/solution": {
      "description": "Deploy a complete solution — the recommended way to deploy. The Skill Builder handles slug generation, MCP server creation, and A-Team Core registration.",
      "body": {
        "solution": {
          "_note": "Solution architecture — identity, grants, handoffs, routing",
          "id": "ecom-customer-service",
          "name": "E-Commerce Customer Service",
          "description": "...",
          "identity": {
            "actor_types": [
              "..."
            ],
            "default_actor_type": "..."
          },
          "skills": [
            {
              "id": "skill-id",
              "name": "...",
              "role": "gateway|worker"
            }
          ],
          "grants": [
            "..."
          ],
          "handoffs": [
            "..."
          ],
          "routing": {}
        },
        "skills": [
          {
            "_note": "Full skill definitions — same format as POST /validate/skill",
            "id": "order-support",
            "name": "Order Support Agent",
            "tools": [
              "... tool definitions with inputs, outputs, source ..."
            ],
            "role": "...",
            "connectors": [
              "orders-mcp"
            ]
          }
        ],
        "connectors": [
          {
            "_note": "Connector metadata — how to connect to MCP servers",
            "id": "orders-mcp",
            "name": "Orders MCP",
            "transport": "stdio",
            "command": "node",
            "args": [
              "/mcp-store/orders-mcp/server.js"
            ]
          }
        ],
        "mcp_store": {
          "_note": "Optional but RECOMMENDED for stdio connectors: connector source code files. Key = connector id, value = { path: content } map. Without mcp_store, stdio connectors will fail to start if the server code is not pre-installed on A-Team Core. The deploy response includes validation_warnings if connectors are missing code.",
          "orders-mcp": [
            {
              "path": "server.js",
              "content": "..."
            },
            {
              "path": "package.json",
              "content": "..."
            }
          ]
        }
      }
    },
    "POST /deploy/connector": {
      "description": "Deploy a single connector. Registers it in the Skill Builder catalog and connects it in A-Team Core.",
      "body": {
        "connector": {
          "id": "orders-mcp",
          "name": "Orders MCP",
          "transport": "stdio",
          "command": "node",
          "args": []
        }
      }
    },
    "POST /deploy/skill": {
      "description": "Deploy a single skill into an existing solution. Requires solution_id.",
      "body": {
        "skill": {
          "id": "order-support",
          "name": "Order Support Agent",
          "tools": [
            "..."
          ]
        },
        "solution_id": "<existing-solution-id>"
      }
    },
    "PATCH /deploy/solutions/:solutionId/skills/:skillId": {
      "description": "Update a deployed skill incrementally. Accepts original skill ID or internal ID. Supports dot notation for scalar fields, and _push/_delete/_update/_rename for array fields.",
      "body": {
        "updates": {
          "_note": "All operations are optional. Combine as many as needed.",
          "problem.statement": "New problem statement (dot notation for scalar fields)",
          "tools_push": {
            "name": "new-tool",
            "description": "A new tool",
            "inputs": [
              "..."
            ],
            "output": {},
            "source": {},
            "policy": {},
            "security": {}
          },
          "tools_update": {
            "name": "existing-tool",
            "description": "Updated description"
          },
          "tools_delete": "tool-to-remove",
          "tools_rename": {
            "from": "old-name",
            "to": "new-name"
          },
          "intents.supported_push": {
            "id": "new-intent",
            "description": "...",
            "examples": [
              "..."
            ]
          },
          "policy.guardrails.always_push": "New guardrail rule",
          "engine.temperature": 0.5
        }
      },
      "protected_arrays": [
        "tools",
        "meta_tools",
        "intents.supported",
        "policy.guardrails.always",
        "policy.guardrails.never"
      ],
      "protected_note": "These arrays cannot be replaced directly — use _push/_delete/_update instead to prevent accidental data loss."
    },
    "PATCH /deploy/solutions/:solutionId": {
      "description": "Update a deployed solution incrementally. Supports dot notation and _push/_delete/_update for arrays.",
      "body": {
        "state_update": {
          "_note": "All operations are optional.",
          "phase": "DEPLOYED",
          "identity.actor_types_push": {
            "key": "new-role",
            "label": "New Role",
            "description": "..."
          },
          "grants_push": {
            "key": "ns.grant",
            "description": "...",
            "issued_by": [
              "skill-a"
            ],
            "consumed_by": [
              "skill-b"
            ],
            "issued_via": "grant_mapping"
          },
          "handoffs_push": {
            "id": "a-to-b",
            "from": "skill-a",
            "to": "skill-b",
            "trigger": "...",
            "grants_passed": [
              "ns.grant"
            ],
            "mechanism": "handoff-controller-mcp"
          },
          "routing.api": {
            "default_skill": "skill-a",
            "description": "API routing"
          }
        }
      }
    },
    "POST /deploy/solutions/:solutionId/redeploy": {
      "description": "Re-deploy ALL skills in a solution at once. Iterates every linked skill, regenerates MCP servers, pushes to A-Team Core. ASYNC SUPPORTED — pass { async: true } in the body to run in background and avoid the upstream 100s Cloudflare 524 timeout on large solutions.",
      "body": {
        "async": "optional boolean — true returns job_id immediately, false (default) blocks until done"
      },
      "returns_sync": {
        "ok": true,
        "solution_id": "solution-id",
        "deployed": 2,
        "failed": 0,
        "total": 2,
        "skills": [
          {
            "skill_id": "skill-1",
            "ok": true,
            "skillSlug": "...",
            "mcpUri": "..."
          }
        ]
      },
      "returns_async": {
        "ok": true,
        "async": true,
        "job_id": "redeploy-bulk-<solid>-<timestamp>",
        "poll_url": "/deploy/jobs/<job_id>",
        "message": "redeploy-bulk started in background. Poll job_id for status."
      },
      "polling": "GET /deploy/jobs/:job_id — returns the job entry. status field cycles in_progress → done | failed. The full result is merged into the entry on completion (same shape as the sync response)."
    },
    "POST /deploy/solutions/:solutionId/skills": {
      "description": "Add a new skill to an existing deployed solution. Creates the skill, populates it with the provided definition, and links it to the solution.",
      "body": {
        "skill": {
          "id": "new-skill-id",
          "name": "New Skill",
          "description": "What this skill does",
          "role": "worker",
          "_note": "Full skill definition — same format as in POST /deploy/solution skills array"
        }
      },
      "returns": {
        "ok": true,
        "skill_id": "new-skill-id",
        "internal_id": "dom_xxx"
      }
    },
    "GET /deploy/solutions/:solutionId/export": {
      "description": "Export solution as a JSON bundle. Returns solution + all skill definitions + connector metadata in a format compatible with POST /deploy/solution for re-import.",
      "returns": {
        "solution": {
          "id": "...",
          "name": "...",
          "identity": {},
          "skills": [],
          "grants": [],
          "handoffs": [],
          "routing": {}
        },
        "skills": [
          "full skill definitions"
        ],
        "connectors": [
          "connector metadata"
        ],
        "exported_at": "ISO timestamp"
      }
    },
    "POST /deploy/solutions/:solutionId/skills/:skillId/redeploy": {
      "description": "Re-deploy a single skill after PATCH updates. Reads stored definition, regenerates MCP server, pushes to A-Team Core. Accepts original or internal skill ID.",
      "body": {
        "_note": "No body required — reads from stored state. Just POST with empty body."
      },
      "returns": {
        "ok": true,
        "skill_id": "original-skill-id",
        "internal_id": "dom_xxx (if different from skill_id)",
        "status": "deployed",
        "skillSlug": "the-slug-used",
        "mcpUri": "tcp://localhost:PORT",
        "message": "Skill deployed successfully"
      },
      "typical_workflow": [
        "1. PATCH /deploy/solutions/:id/skills/:skillId — update tools, policy, etc.",
        "2. POST /deploy/solutions/:id/skills/:skillId/redeploy — push changes to A-Team Core",
        "3. GET /deploy/status/:id — verify new status"
      ]
    }
  },
  "id_remapping_guide": {
    "_note": "ID remapping is now AUTOMATIC. After deploying skills, the pipeline auto-remaps original IDs to internal dom_xxx IDs in grants, handoffs, routing, and security_contracts. You do NOT need to PATCH manually.",
    "how_it_works": "When POST /deploy/solution deploys skills, internal IDs (e.g., dom_abc123) are assigned. The deploy pipeline then automatically deep-replaces all original IDs in the solution definition (grants, handoffs, routing, security_contracts) with internal IDs.",
    "what_you_get": "The deploy response includes import.skills[] with both originalId and id (internal). The health check (GET /deploy/solutions/:id/health) accepts both original and internal IDs.",
    "manual_override": "If you need to manually update IDs (e.g., after adding a new skill), use PATCH /deploy/solutions/:id with the remapped references.",
    "example_response_fragment": {
      "import.skills[0]": {
        "id": "dom_04f014ac",
        "originalId": "identity-assurance",
        "name": "Identity Assurance Manager",
        "status": "imported"
      },
      "import.skills[1]": {
        "id": "dom_492a7855",
        "originalId": "support-tier-1",
        "name": "Customer Support Tier 1",
        "status": "imported"
      }
    },
    "tip": "Use human-readable IDs (e.g., identity-assurance) in your source files. The deploy pipeline handles remapping automatically."
  }
}

Skill specification

ateam_get_spec(skill)
{
  "_note": "A-Team spec with 12 sections. Content truncated — ask about specific sections for detail.",
  "sections": [
    "spec_version",
    "description",
    "changelog",
    "auto_expand",
    "schema",
    "system_tools",
    "platform_authentication",
    "python_helpers",
    "script_caching",
    "validation_rules",
    "template",
    "agent_guide"
  ],
  "spec_version": "1.6.0",
  "description": "Complete A-Team skill definition specification. A skill is an autonomous AI agent with tools, policies, and workflows.",
  "changelog": [
    {
      "version": "1.6.0",
      "changes": [
        "Added /spec/widgets — framework doc for ready-to-use platform widgets (UI plugins exposed by MCP connectors). Surfaces the identity / render / capabilities / uiActions / surface model and the three wiring routes (connector-bundled, solution-declared, skill-declared HTML) so external agents can wire widgets into a solution without crawling /spec/ui-plugins schema. Companion to /spec/ui-plugins (every-field reference) and /spec/host-contract (host/solution boundary)."
      ]
    },
    {
      "version": "1.5.0",
      "changes": [
        "Documented role.stages[] — Staged Skill Mode. Opt-in deterministic multi-step persona: a skill that declares role.stages[] runs as a finite state machine where the engine (not the LLM) decides stage transitions from each stage's reported status. Validator-enforced soundness rule: every `after` entry must reference an EARLIER stage id, which forbids forward refs + cycles by construction. Pre-existing CORE implementation (apps/backend/worker/stageMachine.js, validator validateStages, 2026-06-17) — just needed to be discoverable via the public spec so external agents can build staged skills."
      ]
    },
    {
      "version": "1.4.0",
      "changes": [
        "Added engine.loop_streak_threshold (number, optional, additive). Per-skill override for the generic loop-breaker streak threshold. CORE default 3, clamped to [1, 20] at runtime. Raise for multi-step workers whose stable script produces the same engine-signal fingerprint across iterations (Skill Factory workers are the motivating case). CORE read path: worker/mainloop.js → resolveLoopStreakThreshold."
      ]
    },
    {
      "version": "1.3.0",
      "changes": [
        "Documented engine.default_sub_job_seconds, engine.default_max_idle_seconds (both pre-existing in CORE but undocumented in Builder), and engine.default_max_delegation_depth (new — added to CORE sys.askAnySkill.js 2026-05-28). All three are per-skill ceiling overrides read by CORE's sys.askAnySkill at delegation time. Default-undeclared keeps CORE's built-in defaults (300s / 60s / depth 3). Set higher on long-running workers or deep-fan-out skills."
      ]
    },
    {
      "version": "1.2.0",
      "changes": [
        "Added engine.include_read_evidence_in_gate (boolean, optional, additive, default false). Opt-in to the finalization verifier's READ EVIDENCE channel. Solution-level inheritance supported via the new solution.engine block. Mirrors the CORE worker/finalizationGate.js flag introduced 2026-05-28 — see Docs/specs/VERIFIER_EVIDENCE_CHANNELS.md (in ai-dev-assistant) for the rollout policy."
      ]
    },
    {
      "version": "1.1.0",
      "changes": [
        "Added intents.fast_path.rules[].execution_contract.required_tools (optional, additive). CORE's finalization gate fails with REQUIRED_TOOL_NOT_EXECUTED if any listed tool did not run successfully. Use ONLY when the goal is producing a specific named artifact that one specific tool is the canonical producer of."
      ]
    },
    {
      "version": "1.0.0",
      "changes": [
        "Initial published spec."
      ]
    }
  ],
  "auto_expand": {
    "description": "A skill can be VERY small. The author writes prose + connector picks; the platform fills in the rest at deploy time. This is the §20 v2.3 schema strip — about 68% of what authors used to write is now platform-generated.",
    "minimal_required": [
      "id",
      "name",
      "role.persona",
      "connectors"
    ],
    "optional_but_useful": [
      "description",
      "handoff_when",
      "policy.guardrails"
    ],
    "auto_generated": [
      "tools  (Phase 2b — auto-imported from each connector's live tool inventory at deploy)",
      "ui_plugins  (Phase 5 — auto-discovered via each connector's ui.listPlugins + ui.getPlugin at deploy)",
      "handoff_when  (Phase 6b — LLM-synthesized from persona + tools + connectors when missing; cached by source hash)",
      "intents  (Phase 3 — synthesized from tools + persona; cached by source hash)",
      "scenarios  (auto-generated from tools + intents)",
      "role.* fields beyond persona  (auto-generated)",
      "engine config  (Phase 4 — resolved from preset name or default)",
      "policy.workflows / entities / access_policy  (auto-generated)",
      "security classification on tools  (Phase 2 — auto-classified destructive/read/etc)"
    ],
    "auto_orchestrator": {
      "description": "At the SOLUTION level, set routing_mode:\"auto\" and the platform generates an orchestrator skill at deploy time. Each worker skill's handoff_when (synthesized or author-written) becomes a routing rule. You do NOT write an orchestrator skill yourself.",
      "opt_in_field": "solution.routing_mode = \"auto\"",
      "opt_out": "Provide your own skill with role:\"orchestrator\", or omit routing_mode."
    },
    "rule": "If default behavior works — do not define extra blocks. Complexity is opt-in. REPLACE wins: any explicit field overrides the platform-generated equivalent.",
    "typical_minimal_skill": {
      "id": "memory-keeper",
      "name": "Memory Keeper",
      "version": "1.1.0",
      "description": "Persistent memory",
      "role": {
        "name": "Memory Keeper",
        "persona": "You are the user's persistent memory ..."
      },
      "connectors": [
        "memory-mcp"
      ],
      "handoff_when": "User wants to store, recall, update, or delete memories.",
      "policy": {
        "guardrails": {
          "always": [
            "confirm in 1 short sentence"
          ],
          "never": []
        }
      }
    }
  },
  "schema": {
    "id": {
      "type": "string",
      "required": true,
      "description": "Unique skill identifier (e.g., \"identity-assurance\")"
    },
    "name": {
      "type": "string",
      "required": true,
      "description": "Human-readable skill name"
    },
    "description": {
      "type": "string",
      "required": false,
      "description": "What this skill does"
    },
    "version": {
      "type": "string",
      "required": false,
      "description": "Semantic version (e.g., \"1.0.0\")"
    },
    "phase": {
      "type": "enum",
      "required": true,
      "values": [
        "PROBLEM_DISCOVERY",
        "SCENARIO_EXPLORATION",
        "INTENT_DEFINITION",
        "TOOLS_PROPOSAL",
        "TOOL_DEFINITION",
        "POLICY_DEFINITION",
        "MOCK_TESTING",
        "READY_TO_EXPORT",
        "EXPORTED",
        "DEPLOYED"
      ],
      "description": "Current development phase"
    },
    "created_at": {
      "type": "string",
      "required": false,
      "description": "ISO 8601 timestamp"
    },
    "updated_at": {
      "type": "string",
      "required": false,
      "description": "ISO 8601 timestamp"
    },
    "ui_capable": {
      "type": "boolean",
      "required": false,
      "description": "Whether this skill serves UI plugins"
    },
    "connectors": {
      "type": "string[]",
      "required": false,
      "description": "List of MCP connector IDs this skill depends on (e.g., [\"orders-mcp\", \"fulfillment-mcp\"])"
    },
    "problem": {
      "type": "object",
      "required": true,
      "description": "The problem this skill solves",
      "fields": {
        "statement": {
          "type": "string",
          "required": true,
          "description": "Problem statement (minimum 10 characters)"
        },
        "context": {
          "type": "string",
          "required": false,
          "description": "Additional context about the problem domain"
        },
        "goals": {
          "type": "string[]",
          "required": false,
          "description": "What the skill aims to achieve"
        }
      }
    },
    "scenarios": {
      "type": "array",
      "required": true,
      "min_items": 1,
      "auto_expandable": true,
      "description": "Concrete use cases that demonstrate the skill in action. Auto-generated from tools if omitted when sent to validate or deploy.",
      "item_schema": {
        "id": {
          "type": "string",
          "required": true,
          "description": "Unique scenario ID"
        },
        "title": {
          "type": "string",
          "required": true,
          "description": "Short scenario title"
        },
        "description": {
          "type": "string",
          "required": false,
          "description": "Detailed scenario description"
        },
        "steps": {
          "type": "string[]",
          "required": true,
          "description": "Ordered list of steps"
        },
        "expected_outcome": {
          "type": "string",
          "required": false,
          "description": "What should happen after the scenario completes"
        }
      }
    },
    "role": {
      "type": "object",
      "required": true,
      "auto_expandable": true,
      "description": "The agent persona and behavioral constraints. Auto-generated from problem + tools + guardrails if omitted when sent to validate or deploy.",
      "fields": {
        "name": {
          "type": "string",
          "required": true,
          "description": "Role name (e.g., \"Identity Assurance Manager\")"
        },
        "persona": {
          "type": "string",
          "required": true,
          "description": "Detailed persona description — how the agent should behave. In Staged Skill Mode (see role.stages below) persona stays as the FIXED shared text and each stage body is appended on top per stage."
        },
        "goals": {
          "type": "string[]",
          "required": false,
          "description": "What the agent tries to achieve"
        },
        "limitations": {
          "type": "string[]",
          "required": false,
          "description": "What the agent must NOT do"
        },
        "communication_style": {
          "type": "object",
          "required": false,
          "fields": {
            "tone": {
              "type": "enum",
              "values": [
                "formal",
                "casual",
                "technical",
                "warm"
              ],
              "description": "Communication tone"
            },
            "verbosity": {
              "type": "enum",
              "values": [
                "concise",
                "balanced",
                "detailed"
              ],
              "description": "Response detail level"
            }
          }
        },
        "stages": {
          "type": "array",
          "required": false,
          "description": "STAGED SKILL MODE — opt-in deterministic multi-step persona. A skill with role.stages[] runs as a finite state machine: role.persona stays the FIXED shared text; each stage body is the per-stage instruction appended on top. The engine (not the LLM) decides the next stage from the stage's reported status — so the sequence is reproducible across runs and never deadlocks by construction.\n\nWHEN TO USE:\n  • The skill has a known, ordered pipeline (plan → assemble → verify → deploy → report).\n  • You want each step to read a focused mini-persona, not a wall of text.\n  • You want replays and HLR jumps to be predictable — the same stage with the same inputs always produces the same body.\n\nWHEN NOT TO USE:\n  • Conversational skills where the next step genuinely depends on user input — leave stages omitted and use the standard single-persona mode.\n\nRUNTIME CONTRACT (engine-owned cursor, see apps/backend/worker/stageMachine.js):\n  • Cursor starts at stages[0].\n  • Each stage reports status: \"done\" | \"in_progress\" | \"not_ready\" (optionally + next).\n      done        → mark stage complete; cursor advances to the next stage in declaration order whose `after` prereqs are met. When all stages are done, the skill finalizes.\n      not_ready   → C1 GATE redirect. Cursor jumps to the stage's `next` (if reported and valid) or to the first unmet `after` prerequisite. Never forward-skips an unmet gate.\n      in_progress → stay on the current stage.\n  • HLR/HLP can request a jump to ANY stage (typically backward, e.g. \"re-run assemble after a tool change\"). The jump is gated: the target's `after` prereqs must be met. Jumping back invalidates the target and every later stage (they re-hold their gates until re-run).\n\nSOUNDNESS RULE (validator-enforced): every `after` entry MUST reference an EARLIER-declared stage id. That single constraint forbids forward refs AND cycles by construction, so a staged skill can never deadlock at runtime. Forward refs error STAGE_AFTER_NOT_EARLIER. Missing refs error STAGE_AFTER_UNKNOWN.\n\nVALIDATION ERRORS (see schemaValidator.validateStages):\n  • INVALID_STAGES — role.stages, when present, must be a non-empty array of { id, body }\n  • STAGE_MISSING_ID — stage[i] is missing a string id\n  • STAGE_DUP_ID — duplicate stage id\n  • STAGE_MISSING_BODY — stage is missing a non-empty body\n  • STAGE_AFTER_UNKNOWN — after references an unknown stage id\n  • STAGE_AFTER_NOT_EARLIER — after references a same/later stage (forbidden)\n\nSee Docs/WIP/STAGED_SKILL_MODE_2026-06-17.md (in ai-dev-assistant) for the full design + reference implementation: skill-factory-builder uses this with 7 stages (plan → tools → widgets → persona → assemble → verify → deploy).",
          "item_schema": {
            "id": {
              "type": "string",
              "required": true,
              "description": "Unique stage id within this skill (e.g. \"plan\", \"assemble\")."
            },
            "title": {
              "type": "string",
              "required": false,
              "description": "Optional display title. Defaults to id if omitted."
            },
            "after": {
              "type": "string[]",
              "required": false,
              "description": "Optional list of stage ids that must complete before this stage can be entered. Each entry MUST reference an EARLIER-declared stage id (soundness rule). Omit for the first stage or any stage with no prereqs."
            },
            "body": {
              "type": "string",
              "required": true,
              "description": "The per-stage persona instructions appended to role.persona while the cursor is on this stage. Make it focused — what this stage does, the tools it should call, the report shape it should emit ({status, next?})."
            }
          },
          "example": {
            "description": "Skill-Factory Builder uses 7 stages with a strict prereq DAG. Each stage emits status:\"done\" when its work is reported; \"not_ready\" if a prereq slot is empty (cursor backs off to the unmet gate).",
            "yaml": "role:\n  persona: \"You are the Skill Factory builder. Across all stages: never invent tool names; always cite cap_id.\"\n  stages:\n    - id: \"plan\"\n      body: \"Read the spec. Emit a plan with cap_ids and order. Report {status:\\\"done\\\"}.\"\n    - id: \"tools\"\n      after: [\"plan\"]\n      body: \"For each cap_id in the plan, delegate to skill-factory-tool-builder via adas.skills.ask. On its checkpoint, advance.\"\n    - id: \"widgets\"\n      after: [\"plan\"]\n      body: \"For each ui cap_id, delegate to skill-factory-ui-builder.\"\n    - id: \"persona\"\n      after: [\"plan\"]\n      body: \"Synthesize the candidate skill's persona from the spec + glossary.\"\n    - id: \"assemble\"\n      after: [\"tools\", \"widgets\", \"persona\"]\n      body: \"Call sb.assemble with generated_tools + generated_widgets + generated_persona. If any prereq slot is empty, report {status:\\\"not_ready\\\", next:\\\"<the empty one>\\\"}.\"\n    - id: \"verify\"\n      after: [\"assemble\"]\n      body: \"Delegate to skill-factory-qa. If qa surfaces blockers, report {status:\\\"not_ready\\\", next:\\\"assemble\\\"} so HLR can replan.\"\n    - id: \"deploy\"\n      after: [\"verify\"]\n      body: \"Call sb.deploy. Report {status:\\\"done\\\"} only on a successful deploy; on failure, jump back to assemble.\"",
            "behavior": "The cursor starts at plan, fans out across tools / widgets / persona (their `after` only requires plan), then converges at assemble (which gates on all three). A backward jump from verify to assemble re-invalidates assemble + deploy — they re-hold their gates until re-run.",
            "when_to_use_this_pattern": "Pipelines with deterministic order and convergence points. The fan-out gives the agent room to make independent progress; the convergence gate makes \"ready to assemble\" unambiguous."
          }
        }
      }
    },
    "glossary": {
      "type": "object",
      "required": false,
      "description": "Domain-specific term definitions (key-value pairs)"
    },
    "intents": {
      "type": "object",
      "required": true,
      "auto_expandable": true,
      "description": "User intent classification configuration. Auto-generated from tool names and descriptions if omitted when sent to validate or deploy. Only define explicitly when you need custom intent examples, entities, or disambiguation.",
      "fields": {
        "supported": {
          "type": "array",
          "required": true,
          "description": "List of intents the skill can handle",
          "item_schema": {
            "id": {
              "type": "string",
              "required": true,
              "description": "Unique intent ID"
            },
            "description": {
              "type": "string",
              "required": true,
              "description": "What this intent means"
            },
            "examples": {
              "type": "string[]",
              "required": true,
              "description": "Example user messages that match this intent"
            },
            "maps_to_workflow": {
              "type": "string",
              "required": false,
              "description": "Workflow ID to execute when this intent is detected"
            },
            "entities": {
              "type": "array",
              "required": false,
              "description": "Entities to extract from user message",
              "item_schema": {
                "name": {
                  "type": "string",
                  "required": true
                },
                "type": {
                  "type": "enum",
                  "values": [
                    "string",
                    "number",
                    "boolean",
                    "object",
                    "array",
                    "text"
                  ]
                },
                "required": {
                  "type": "boolean",
                  "required": false
                },
                "extract_from": {
                  "type": "enum",
                  "values": [
                    "message",
                    "context"
                  ]
                }
              }
            },
            "guardrails": {
              "type": "object",
              "required": false,
              "fields": {
                "pre_conditions": {
                  "type": "string[]"
                },
                "rate_limit": {
                  "type": "object",
                  "fields": {
                    "max_per_session": {
                      "type": "number"
                    },
                    "cooldown_seconds": {
                      "type": "number"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "thresholds": {
          "type": "object",
          "required": false,
          "description": "Confidence thresholds for intent matching",
          "fields": {
            "accept": {
              "type": "number",
              "description": "Confidence to proceed (default: 0.8)",
              "default": 0.8
            },
            "clarify": {
              "type": "number",
              "description": "Confidence to ask for clarification (default: 0.5)",
              "default": 0.5
            },
            "reject": {
              "type": "number",
              "description": "Confidence below which to reject (default: 0.3)",
              "default": 0.3
            }
          }
        },
        "out_of_domain": {
          "type": "object",
          "required": false,
          "description": "What to do when intent does not match this skill",
          "fields": {
            "action": {
              "type": "enum",
              "values": [
                "redirect",
                "reject",
                "escalate"
              ]
            },
            "message": {
              "type": "string",
              "description": "Message to show user"
            },
            "suggest_domains": {
              "type": "string[]",
              "description": "Other skill IDs that might handle this"
            }
          }
        },
        "fast_path": {
          "type": "object",
          "required": false,
          "description": "Regex-based zero-LLM intent shortcut. When a user message matches a rule's `pattern`, detectIntent skips the LLM classification and uses the rule's declared intent + execution_contract directly. Use sparingly — only for unambiguous phrasings the LLM might otherwise misclassify.",
          "fields": {
            "rules": {
              "type": "array",
              "required": false,
              "description": "Ordered list of regex shortcut rules. First match wins.",
              "item_schema": {
                "pattern": {
                  "type": "string",
                  "required": true,
                  "description": "JavaScript regex literal (e.g., \"/^(build|make|create) me .* widget/i\"). Tested against the raw user message."
                },
                "intent": {
                  "type": "string",
                  "required": true,
                  "description": "Intent ID to assign on match (must appear in intents.supported[].id)."
                },
                "mission_kind": {
                  "type": "enum",
                  "required": false,
                  "values": [
                    "generate",
                    "retrieve",
                    "execute",
                    "converse",
                    "configure"
                  ],
                  "description": "Hint to the planner about what kind of work this intent represents. Generates a \"specific named artifact\" → \"generate\"; calls a remote API → \"execute\"; etc."
                },
                "execution_contract": {
                  "type": "object",
                  "required": false,
                  "description": "Finalization-gate contract for this fast-path intent. CORE enforces these signals at the end of the chain; failure short-circuits the response with the matching gate error.",
                  "fields": {
                    "requires_external_effect": {
                      "type": "boolean",
                      "required": false,
                      "description": "When true, finalization is blocked unless at least one tool with side-effects (write/network/IO) ran successfully. Use for \"must do something in the world\" goals like sending an email or creating a calendar event."
                    },
                    "required_tools": {
                      "type": "string[]",
                      "required": false,
                      "description": "List of fully-qualified tool names (e.g. [\"acs.widget.store\"]) that MUST execute successfully for the goal to be considered satisfied. Use ONLY when the goal is producing a SPECIFIC NAMED ARTIFACT that one specific tool is the canonical producer of. Catches the class of bugs where the agent ran a tangentially-related tool (e.g. sys.trigger created a cron) but the goal actually demanded a different tool (e.g. acs.widget.store for a UI widget). Gate behavior: REQUIRED_TOOL_NOT_EXECUTED if any tool in this list is missing from the chain's successful execution set. Validator does NOT verify the tool names exist — CORE checks at runtime since the available tool surface depends on connector inventory at deploy time."
                    }
                  }
                }
              }
            }
          },
          "example": {
            "description": "Fast-path rule that pins widget-creation requests to a specific producer tool.",
            "yaml": "intents:\n  fast_path:\n    rules:\n      - pattern: \"/^(build|make|create) me .* widget/i\"\n        intent: \"create_widget\"\n        mission_kind: \"generate\"\n        execution_contract:\n          requires_external_effect: true\n          required_tools: [\"acs.widget.store\"]",
            "behavior": "When the user says \"build me a weather widget\", detectIntent skips the LLM and routes to create_widget. The chain runs as usual. At finalization time, the gate scans the successful-tool set: if acs.widget.store didn't run, the response is rejected with REQUIRED_TOOL_NOT_EXECUTED (the agent gets one retry to actually call the right tool).",
            "when_to_use": "A goal produces a specific named artifact, AND there is exactly one tool that is the canonical producer of that artifact, AND you have observed the agent silently running a different tool and claiming success.",
            "when_NOT_to_use": "General \"must do something external\" cases — use requires_external_effect: true alone for those. Multiple acceptable producer tools — required_tools is AND-logic; all listed tools must run."
          }
        }
      }
    },
    "tools": {
      "type": "array",
      "required": true,
      "min_items": 1,
      "description": "Available actions this skill can perform via MCP connectors",
      "item_schema": {
        "id": {
          "type": "string",
          "required": true,
          "description": "Unique tool ID (e.g., \"tool-orders-get\")"
        },
        "id_status": {
          "type": "enum",
          "values": [
            "temporary",
            "permanent"
          ],
          "description": "Whether this ID is provisional or finalized"
        },
        "name": {
          "type": "string",
          "required": true,
          "description": "Tool name matching MCP tool (e.g., \"orders.order.get\"). Supports wildcard: \"connector-id:*\" grants access to ALL tools from that connector (e.g., \"mobile-device-mcp:*\"). You can mix wildcards and individual tools in the same skill."
        },
        "description": {
          "type": "string",
          "required": true,
          "description": "What this tool does"
        },
        "inputs": {
          "type": "array",
          "required": true,
          "description": "Tool input parameters",
          "item_schema": {
            "name": {
              "type": "string",
              "required": true
            },
            "type": {
              "type": "enum",
              "values": [
                "string",
                "number",
                "boolean",
                "object",
                "array",
                "text"
              ],
              "required": true
            },
            "required": {
              "type": "boolean",
              "required": false
            },
            "description": {
              "type": "string",
              "required": true
            },
            "default": {
              "type": "any",
              "required": false
            },
            "enum": {
              "type": "string[]",
              "required": false,
              "description": "Allowed values"
            }
          }
        },
        "output": {
          "type": "object",
          "required": true,
          "fields": {
            "type": {
              "type": "enum",
              "values": [
                "string",
                "number",
                "boolean",
                "object",
                "array",
                "text"
              ]
            },
            "description": {
              "type": "string",
              "required": true
            },
            "schema": {
              "type": "object",
              "required": false,
              "description": "JSON Schema for output structure"
            }
          }
        },
        "source": {
          "type": "object",
          "required": false,
          "description": "Where this tool comes from",
          "fields": {
            "type": {
              "type": "enum",
              "values": [
                "mcp_bridge",
                "builtin",
                "custom"
              ]
            },
            "connection_id": {
              "type": "string",
              "description": "Connector ID for mcp_bridge tools"
            },
            "mcp_tool": {
              "type": "string",
              "description": "Tool name on the MCP server"
            }
          }
        },
        "policy": {
          "type": "object",
          "required": false,
          "description": "Tool-level access policy",
          "fields": {
            "allowed": {
              "type": "enum",
              "values": [
                "always",
                "conditional",
                "never"
              ]
            },
            "conditions": {
              "type": "array",
              "required": false,
              "item_schema": {
                "when": {
                  "type": "string",
                  "description": "Condition expression (e.g., \"amount > 500\")"
                },
                "action": {
                  "type": "enum",
                  "values": [
                    "allow",
                    "deny",
                    "escalate",
                    "require_approval"
                  ]
                },
                "message": {
                  "type": "string",
                  "required": false
                }
              }
            },
            "requires_approval": {
              "type": "enum",
              "values": [
                "always",
                "conditional",
                "never"
              ],
              "required": false
            },
            "rate_limit": {
              "type": "string",
              "required": false,
              "description": "e.g., \"100/minute\""
            }
          }
        },
        "script_cache": {
          "type": "object",
          "required": false,
          "description": "Opt-in for script-level JIT shortcuts (Level 2). When a fat tool interacts with a flaky external system — DOM scraping, version-rotating APIs, brittle HTML — setting `enabled: true` turns the tool into a first-class dispatchable function. The planner calls it directly like any other tool; the platform bakes the Python implementation on first call, caches it, and replays on subsequent calls. The LLM regenerates only when a cached run fails. ENABLE THIS when: the tool implementation is inherently unstable (browser automation, web scraping, screen reading, third-party API whose shape rotates). DO NOT enable for: pure-compute tools, deterministic API calls, MCP-bridged tools with stable schemas — they do not need caching.",
          "fields": {
            "enabled": {
              "type": "boolean",
              "required": true,
              "description": "Turn script caching on for this tool. Default: false. When true, the Core synthesizes a dispatcher for this tool — the planner can call it directly, no run_python_script boilerplate required."
            },
            "invalidate_on": {
              "type": "string[]",
              "required": false,
              "description": "Which failure classes trigger cache invalidation. Defaults to [\"execution\",\"domain_break\"]. \"logical\" is ALWAYS excluded (a bad password is a user problem, not a script problem — the cache should survive it).",
              "enum": [
                "execution",
                "domain_break"
              ]
            },
            "max_age_days": {
              "type": "number",
              "required": false,
              "description": "TTL in days. Cached scripts auto-expire this long after their last successful use (refreshed on every ok hit). Default: 30. Clamped to (0, 365].",
              "default": 30
            }
          },
          "failure_class_contract_AUTOMATIC": {
            "description": "The Python the baker generates MUST emit a failure_class field. The baker's system prompt enforces this automatically — you (the solution author) do NOT need to write the contract into the tool description or persona. The classes are:",
            "classes": {
              "ok": "Success. Platform refreshes cache TTL.",
              "logical": "External system gave a valid NO (invalid credentials, rate limited, moderation). Cache is PRESERVED.",
              "domain_break": "Script assumption was wrong (selector null, shape changed). Cache is INVALIDATED, rebaked next call."
            },
            "auto_classified_by_platform": "Python tracebacks, non-zero exits, timeouts → classified as execution automatically.",
            "default_when_missing": "ok:false without failure_class → treated as domain_break (conservative).",
            "solution_author_responsibility": "NONE. The baker enforces this contract. You just write the tool description."
          },
          "planner_awareness_AUTOMATIC": {
            "description": "At Level 2, the planner calls the tool DIRECTLY: linkedin.status({}). It does NOT need to know about run_python_script, tool_name, script_cache, or failure_class. The Core intercepts the call, resolves the synthetic dispatcher, and handles everything. Your skill persona does NOT need any rules about tool_name or run_python_script routing. The only thing the persona needs is the normal \"when to use this tool\" kind of guidance."
          },
          "when_to_use": [
            "Browser automation (DOM scraping, Playwright clicks, headless Chromium)",
            "Third-party APIs whose response shape changes between versions",
            "HTML parsing where the structure of the page rotates",
            "Any fat tool where you watched the LLM rewrite the same Python five times last week"
          ],
          "when_NOT_to_use": [
            "Deterministic compute — math, data transforms, JSON reshaping",
            "Tools that are already MCP-bridged with a stable schema",
            "Tools that run for milliseconds — caching overhead is not worth it",
            "Tools whose OUTPUT is supposed to depend on args you do not count as part of the shape"
          ],
          "example_config_MINIMAL": {
            "enabled": true
          },
          "example_config_FULL": {
            "enabled": true,
            "invalidate_on": [
              "execution",
              "domain_break"
            ],
            "max_age_days": 30
          },
          "design_doc": "Docs/WIP/SCRIPT-LEVEL-JIT-SHORTCUTS.md in ai-dev-assistant repo."
        },
        "script_hint": {
          "type": "string",
          "required": false,
          "description": "OPTIONAL Python snippet shown to the baker as a \"what you think the implementation should look like\" seed. Relevant only when script_cache.enabled is true. The baker uses the hint as a strong starting point but is NOT bound by it — if a selector is wrong or a step unnecessary, the baker will deviate. A good hint dramatically speeds the first bake and reduces LLM creativity; a bad hint is harmless (it gets thrown away on first failure). WHEN TO PROVIDE: if you know the site has a specific login button with a specific selector, put it in the hint. If you don't know the DOM, omit it entirely — the baker will figure it out from the tool description alone. FORMAT: raw Python source as a multi-line string. Use adas_call_tool(), adas_emit_progress(), adas_output_json() like you would in run_python_script directly. No imports needed (json, time, re are available). The baker will rewrite as needed — you do NOT have to emit failure_class; the baker handles that.\n\n⚠️ ANTI-PATTERN (seen in the wild): do NOT write `failure_class: \"domain_break\"` for a branch where a tool returned `ok: false`. A tool responding with a valid `{ok: false}` payload is `logical` (user cancelled / session expired / rate limit / no UI client attached). Using `domain_break` invalidates the cached script on every legitimate negative outcome, triggering bake_exhausted after 5 retries. Only use `domain_break` when the tool CONTRACT broke (tool not found, schema mismatch, selector returned null).",
          "example_when_you_know_the_site": "# Hint for a site with known selectors\nimport json\nnav = adas_call_tool('web.navigate', {'url': 'https://example.com/account'})\ncheck = adas_call_tool('web.evaluate', {'script': \"document.querySelector('.user-badge') ? 'in' : 'out'\"})\nadas_output_json({'logged_in': 'in' in str(check.get('result', ''))})",
          "example_when_you_DO_NOT_know_the_site": "# Omit script_hint entirely — let the baker discover the DOM on first run.",
          "example_with_dataStore_for_session_cookies": "# Use platform.dataStore for raw structured data (cookies, tokens).\n# NEVER use memory.store for raw JSON — it normalizes/summarizes.\nimport json\nstored = adas_call_tool('platform.dataStore.get', {'key': 'my_session_cookies'})\ncookies = None\nif stored.get('found') and stored.get('value'):\n    val = stored['value']\n    cookies = json.loads(val) if isinstance(val, str) else val\nif cookies:\n    adas_call_tool('web.cookies.set', {'cookies': cookies})\n# ... do work ...\n# Save fresh cookies with 24h TTL\nfresh = adas_call_tool('web.cookies.get', {'urls': 'https://example.com'})\nif fresh.get('cookies'):\n    adas_call_tool('platform.dataStore.set', {'key': 'my_session_cookies', 'value': json.dumps(fresh['cookies']), 'ttl_seconds': 86400})  # 24h TTL — caller decides; omit for no expiry",
          "best_practice_session_convention": {
            "description": "BEST PRACTICE: When multiple tools in a skill share session state (cookies, tokens, cached credentials), declare the convention ONCE in problem.context — not in each tool's script_hint. The baker reads problem.context when generating scripts for ANY tool, so all tools stay consistent automatically. Without this, updating one tool's storage path but forgetting another causes session breaks.",
            "example_problem_context": "\"Session convention: ALL tools in this skill store and restore session cookies from platform.dataStore under key 'my_session_cookies' (24h TTL). NEVER use memory.recall for cookies — memory-mcp normalizes/summarizes raw JSON. Restore pattern: platform.dataStore.get → json.loads → web.cookies.set. Save pattern: web.cookies.get → json.dumps → platform.dataStore.set with ttl_seconds=86400.\"",
            "why": "Prevents the class of bug where tool A writes to dataStore but tool B reads from memory.recall — a single line in problem.context keeps every baker-generated script consistent.",
            "future": "This is a solution-level best practice today. A future platform feature may formalize skill-level session config as a dedicated schema field with mechanical enforcement."
          }
        },
        "mock": {
          "type": "object",
          "required": false,
          "description": "Mock configuration for testing without real MCP",
          "fields": {
            "enabled": {
              "type": "boolean"
            },
            "mode": {
              "type": "enum",
              "values": [
                "examples",
                "llm",
                "hybrid"
              ]
            },
            "examples": {
              "type": "array",
              "item_schema": {
                "id": {
                  "type": "string"
                },
                "input": {
                  "type": "object"
                },
                "output": {
                  "type": "any"
                },
                "description": {
                  "type": "string",
                  "required": false
                }
              }
            }
          }
        },
        "mock_status": {
          "type": "enum",
          "values": [
            "untested",
            "tested",
            "skipped"
          ]
        },
        "security": {
          "type": "object",
          "required": true,
          "description": "Tool security metadata",
          "fields": {
            "classification": {
              "type": "enum",
              "values": [
                "public",
                "internal",
                "pii_read",
                "pii_write",
                "financial",
                "destructive"
              ],
              "required": true,
              "description": "Security classification. High-risk tools (pii_write, financial, destructive) require access policies."
            },
            "risk": {
              "type": "enum",
              "values": [
                "low",
                "medium",
                "high",
                "critical"
              ],
              "required": false
            },
            "data_owner_field": {
              "type": "string",
              "required": false,
              "description": "Input field that identifies data owner (for constrain policies)"
            }
          }
        }
      }
    },
    "meta_tools": {
      "type": "array",
      "required": false,
      "description": "Auto-generated tool compositions (created by DAL, not manually)",
      "item_schema": {
        "id": {
          "type": "string"
        },
        "name": {
          "type": "string"
        },
        "description": {
          "type": "string"
        },
        "composes": {
          "type": "string[]",
          "description": "Tool names this meta tool combines"
        },
        "logic": {
          "type": "string",
          "description": "How the tools are combined"
        },
        "status": {
          "type": "enum",
          "values": [
            "pending",
            "approved",
            "rejected"
          ]
        }
      }
    },
    "bootstrap_tools": {
      "type": "string[]",
      "required": false,
      "max_items": 3,
      "description": "Up to 3 tool names that are always available to the planner (pinned in tool selection). These are NOT auto-executed — they simply guarantee the planner can always see and choose these tools, even when LLM-based tool ranking would otherwise exclude them. Useful for core domain tools like identity lookup or order retrieval that the planner needs on almost every request. Values must be valid tool names from the tools array.",
      "example": [
        "identity.customer.lookup",
        "orders.list",
        "account.status"
      ]
    },
    "prefetch_tools": {
      "type": "string[]",
      "required": false,
      "description": "Tool names to pre-load into the planner context at job start (iteration 0). Unlike bootstrap_tools which PIN tools for every iteration, prefetch_tools loads them ONCE at the start so the planner has their outputs available immediately without needing sys.askForContextAndTools. Use for tools the skill almost always needs on first iteration — e.g., memory.recall, identity.lookup. The tools execute automatically at job start and their results are injected into the planner context.",
      "example": [
        "memory.recall",
        "memory.rules.match",
        "identity.customer.lookup"
      ],
      "vs_bootstrap": "bootstrap_tools = always VISIBLE to planner (pinned in tool selection). prefetch_tools = automatically EXECUTED at job start (results pre-loaded into context). Use bootstrap for tools the planner should always be able to CHOOSE. Use prefetch for tools whose RESULTS the planner needs from the start."
    },
    "exclude_bootstrap_tools": {
      "type": "string[]",
      "required": false,
      "description": "System bootstrap tools to UN-PIN for this skill. By default the platform force-pins 9 system tools (readFile, getCurrentProjectPath, getChatTranscript, sys.callAiWithTools, sys.step, sys.handoffToSkill, run_python_script, sys.askForContextAndTools, sys.finalizePlan). Three are MANDATORY and cannot be excluded: run_python_script, sys.askForContextAndTools, sys.finalizePlan. The remaining 6 CAN be excluded here. Excluded tools are NOT removed — they stay in the tool catalog and the LLM can still select them via tool ranking. They just won't be force-pinned. Use this to free up planner token budget for skills that don't need certain system tools. If also set at the solution level, both lists are merged (union).",
      "excludable_tools": [
        "readFile",
        "getCurrentProjectPath",
        "getChatTranscript",
        "sys.callAiWithTools",
        "sys.step",
        "sys.handoffToSkill",
        "sys.askAnySkill",
        "sys.findCapability",
        "sys.listSkills"
      ],
      "mandatory_tools_note": "These 3 are ALWAYS pinned and CANNOT be excluded: run_python_script, sys.askForContextAndTools, sys.finalizePlan",
      "example": [
        "readFile",
        "getCurrentProjectPath",
        "getChatTranscript"
      ],
      "tip": "For single-skill solutions, exclude sys.handoffToSkill, sys.askAnySkill, sys.findCapability, and sys.listSkills to save planner token budget — these are only useful in multi-skill solutions."
    },
    "triggers": {
      "type": "array",
      "required": false,
      "description": "Automation triggers that activate this skill periodically or on events. Triggers are the ONLY way a skill can act proactively (without a user message). The trigger-runner service fires them on schedule or in response to events, creating a job where the skill executes the trigger prompt autonomously using all its linked tools. These are STATIC triggers — defined at build time. For DYNAMIC triggers created at runtime by the agent (e.g., user says \"remind me at 9 AM\"), see sys.trigger system tool in the solution spec dynamic_triggers section.",
      "guide": {
        "how_it_works": [
          "1. The trigger-runner service checks enabled triggers on their schedule (every) or listens for events (event)",
          "2. When a trigger fires, it creates a new job for the skill with the trigger prompt as the goal",
          "3. The skill executes autonomously — it can use ALL its linked connector tools (read data, send messages, update records)",
          "4. concurrency=1 (default) means only one instance runs at a time — next fire waits for current to finish",
          "5. The trigger prompt should be specific: what to check, what action to take, what to report"
        ],
        "scope_guide": {
          "mental_model": "scope controls WHO the trigger runs for. \"system\" = one job, no user context. \"per_actor\" = one job PER active user, each with full user context (memory, preferences, identity).",
          "system": "Default. The trigger fires once per schedule. The job runs as the system actor with no user-specific context. Use for global housekeeping: cleanup, aggregation, health checks, system-wide reports.",
          "per_actor": "The trigger fires once per schedule, then fans out — creating a SEPARATE job for each active (non-deactivated) user. Each job runs with that user's full actor context: memory.userProfile returns THEIR profile, memory.recall searches THEIR history, connectors receive THEIR actor ID. Use for personalized proactive features: daily briefings, reminders, inbox digests, proactive recommendations.",
          "how_fan_out_works": [
            "1. Trigger becomes due (same schedule logic as system scope)",
            "2. Platform loads all active actors (status != deactivated) from the tenant",
            "3. For EACH actor: checks isDue and canStart independently (per-actor state tracking)",
            "4. Creates a separate job per actor with their actorId in both the HTTP header and triggerContext",
            "5. Each job runs in full ALS (AsyncLocalStorage) context — getCurrentActorId() returns the correct user",
            "6. All memory tools, connector calls, and storage queries automatically scope to that actor"
          ],
          "concurrency_note": "With per_actor scope, concurrency is enforced PER ACTOR, not globally. concurrency=1 means each user can have at most 1 running instance of this trigger — other users are unaffected.",
          "state_isolation": "Trigger state (lastRunAt, checkpoint, runningJobIds) is tracked per (skillSlug, triggerId, actorId). Actor A's trigger can fire while Actor B's is still running."
        },
        "dynamic_triggers_note": "For triggers created at RUNTIME by the AI agent (not at build time), use the sys.trigger system tool. It supports cron expressions, ISO 8601 intervals, and one-shot datetime schedules. Add sys.trigger to the skill's bootstrap_tools to make it available. See solution spec dynamic_triggers section for full documentation.",
        "schedule_examples": {
          "PT1M": "Every 1 minute (use sparingly — only for time-critical monitoring)

(Response truncated from 154,611 chars. Use more specific queries to get smaller results.)

Solution specification

ateam_get_spec(solution)
{
  "_note": "A-Team spec with 10 sections. Content truncated — ask about specific sections for detail.",
  "sections": [
    "spec_version",
    "description",
    "auto_expand",
    "schema",
    "host_contract",
    "functional_connectors",
    "models",
    "validation_rules",
    "template",
    "agent_guide"
  ],
  "spec_version": "1.0.0",
  "description": "Complete A-Team solution definition specification. A solution orchestrates multiple skills into a cohesive multi-agent system with shared grants, handoffs, and routing.",
  "auto_expand": {
    "description": "A solution can be very small. Author writes: skill picks (id list), routing_mode, optional style. Platform generates: orchestrator skill, handoff rules, ui_plugins, default routing channels, validator manifest.",
    "minimal_required": [
      "id",
      "name",
      "linked_skills"
    ],
    "optional_but_useful": [
      "description",
      "routing_mode",
      "style",
      "platform_connectors"
    ],
    "auto_generated": [
      "orchestrator skill  (Phase 6 — generated when routing_mode:\"auto\" AND no orchestrator-role skill exists. Persona is built from each worker's handoff_when triggers.)",
      "handoffs[]  (Phase 6 — orchestrator → each worker, auto_generated:true marker)",
      "routing.{voice,chat,api}.default_skill  (set to the generated orchestrator when not specified)",
      "ui_plugins[]  (Phase 5 — MCP introspection at deploy time, calls ui.listPlugins + ui.getPlugin on each ui_capable connector)",
      "style cascade  (Phase 1 — solution-level style prepended to every skill persona)"
    ],
    "auto_orchestrator": "Set routing_mode:\"auto\" and the platform builds the orchestrator. Each worker's handoff_when becomes a routing rule. The orchestrator is a real, deployed skill (id \"auto-orchestrator\", role_type \"orchestrator\", _auto_generated:true marker for safe regeneration).",
    "rule": "REPLACE wins: any explicit field overrides the platform-generated equivalent. Author keeps explicit fields when overriding inferred behavior; deletes them to opt back into automation.",
    "typical_minimal_solution": {
      "id": "personal-adas",
      "name": "Personal Assistant",
      "description": "Situation-aware personal assistant",
      "routing_mode": "auto",
      "style": "mobile",
      "linked_skills": [
        "memory-keeper",
        "daily-intel",
        "home-control",
        "messaging-agent",
        "mycoach",
        "my-docs",
        "travel-agent",
        "teach-this",
        "notification-triage",
        "life-manager"
      ]
    }
  },
  "schema": {
    "id": {
      "type": "string",
      "required": true,
      "description": "Unique solution identifier"
    },
    "name": {
      "type": "string",
      "required": true,
      "description": "Solution display name"
    },
    "version": {
      "type": "string",
      "required": false,
      "description": "Semantic version"
    },
    "description": {
      "type": "string",
      "required": false,
      "description": "What this multi-agent solution does"
    },
    "bootstrap_tools": {
      "type": "string[]",
      "required": false,
      "max_items": 3,
      "description": "Up to 3 tool names that are ALWAYS pinned for the planner across ALL skills in this solution. Merged with each skill's own bootstrap_tools (union, deduplicated). Use this to ensure critical tools like memory.userProfile run on every message regardless of which skill handles it. This also suppresses the greeting fast-path — when bootstrap_tools are present, even \"hi\" goes through the full planner so bootstrap tools execute.",
      "example": [
        "memory.userProfile"
      ]
    },
    "exclude_bootstrap_tools": {
      "type": "string[]",
      "required": false,
      "description": "System bootstrap tools to UN-PIN across ALL skills in this solution. By default the platform force-pins system tools for every skill. Three are MANDATORY (run_python_script, sys.askForContextAndTools, sys.finalizePlan) and cannot be excluded. The rest CAN be excluded here. Excluded tools are NOT removed from the tool catalog — the LLM tool finder can still select them. They just won't be force-pinned. This applies to ALL skills. Individual skills can add more excludes via their own exclude_bootstrap_tools field (lists are merged).",
      "excludable_tools": [
        "readFile",
        "getCurrentProjectPath",
        "getChatTranscript",
        "sys.callAiWithTools",
        "sys.step",
        "sys.handoffToSkill",
        "sys.askAnySkill",
        "sys.findCapability",
        "sys.listSkills"
      ],
      "mandatory_tools_note": "These 3 are ALWAYS pinned and CANNOT be excluded: run_python_script, sys.askForContextAndTools, sys.finalizePlan",
      "example": [
        "readFile",
        "getCurrentProjectPath"
      ]
    },
    "identity": {
      "type": "object",
      "required": false,
      "description": "Actor types and access control for the solution",
      "fields": {
        "actor_types": {
          "type": "array",
          "required": true,
          "description": "Define who can use this solution",
          "item_schema": {
            "key": {
              "type": "string",
              "required": true,
              "description": "Actor type identifier (e.g., \"customer\", \"agent\")"
            },
            "label": {
              "type": "string",
              "required": true,
              "description": "Display name"
            },
            "description": {
              "type": "string",
              "required": false
            },
            "default_channel": {
              "type": "string",
              "required": false,
              "description": "Default entry channel for this actor type"
            }
          }
        },
        "default_actor_type": {
          "type": "string",
          "description": "Default actor type if not specified. Must match an actor_types[].key"
        },
        "admin_roles": {
          "type": "string[]",
          "description": "Actor type keys that have admin privileges"
        }
      }
    },
    "skills": {
      "type": "array",
      "required": true,
      "min_items": 1,
      "description": "The autonomous agents in this solution",
      "item_schema": {
        "id": {
          "type": "string",
          "required": true,
          "description": "Unique skill ID (must match the skill definition id)"
        },
        "name": {
          "type": "string",
          "required": true
        },
        "role": {
          "type": "enum",
          "required": true,
          "values": [
            "gateway",
            "worker",
            "orchestrator",
            "approval"
          ],
          "description": "gateway = entry point (identity/routing), worker = does the work, orchestrator = coordinates, approval = authorizes"
        },
        "description": {
          "type": "string",
          "required": true
        },
        "entry_channels": {
          "type": "string[]",
          "required": false,
          "description": "Channels where external actors can reach this skill"
        },
        "connectors": {
          "type": "string[]",
          "required": false,
          "description": "MCP connector IDs this skill uses"
        },
        "ui_capable": {
          "type": "boolean",
          "required": false,
          "description": "Whether this skill serves UI plugins"
        },
        "prompt": {
          "type": "string",
          "required": false,
          "description": "System prompt for this skill in the solution context"
        },
        "example_conversations": {
          "type": "array",
          "required": false,
          "item_schema": {
            "title": {
              "type": "string"
            },
            "messages": {
              "type": "array",
              "item_schema": {
                "role": {
                  "type": "string"
                },
                "content": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "grants": {
      "type": "array",
      "required": false,
      "description": "Verified claims that flow between skills. The grant economy is the security backbone of the solution.",
      "item_schema": {
        "key": {
          "type": "string",
          "required": true,
          "description": "Grant identifier with namespace (e.g., \"ecom.customer_id\")"
        },
        "description": {
          "type": "string",
          "required": true
        },
        "values": {
          "type": "string[]",
          "required": false,
          "description": "Allowed values for enum grants (e.g., [\"L0\", \"L1\", \"L2\"])"
        },
        "issued_by": {
          "type": "string[]",
          "required": true,
          "description": "Skill IDs that can issue this grant"
        },
        "consumed_by": {
          "type": "string[]",
          "required": true,
          "description": "Skill IDs that need this grant"
        },
        "issued_via": {
          "type": "enum",
          "values": [
            "grant_mapping",
            "handoff",
            "platform"
          ]
        },
        "source_tool": {
          "type": "string",
          "required": false,
          "description": "Tool whose output creates this grant"
        },
        "source_field": {
          "type": "string",
          "required": false,
          "description": "JSON path into tool result (e.g., \"$.candidates[0].customer_id\")"
        },
        "ttl_seconds": {
          "type": "number",
          "required": false,
          "description": "How long the grant lives (omit for permanent)"
        },
        "internal": {
          "type": "boolean",
          "required": false,
          "description": "If true, grant is not visible outside issuing skill"
        }
      }
    },
    "handoffs": {
      "type": "array",
      "required": false,
      "description": "Skill-to-skill conversation transfers with grant propagation",
      "item_schema": {
        "id": {
          "type": "string",
          "required": true,
          "description": "Unique handoff ID"
        },
        "from": {
          "type": "string",
          "required": true,
          "description": "Source skill ID"
        },
        "to": {
          "type": "string",
          "required": true,
          "description": "Destination skill ID"
        },
        "trigger": {
          "type": "string",
          "required": true,
          "description": "When this handoff happens (human-readable)"
        },
        "grants_passed": {
          "type": "string[]",
          "required": true,
          "description": "Grant keys to transfer to target skill"
        },
        "grants_dropped": {
          "type": "string[]",
          "required": false,
          "description": "Grant keys to revoke after handoff"
        },
        "mechanism": {
          "type": "enum",
          "values": [
            "handoff-controller-mcp",
            "internal-message"
          ],
          "description": "handoff-controller-mcp = live conversation transfer, internal-message = async skill-to-skill"
        },
        "ttl_seconds": {
          "type": "number",
          "required": false,
          "description": "How long the handoff session lives"
        }
      }
    },
    "routing": {
      "type": "object",
      "required": false,
      "description": "Channel-to-skill routing. Maps each channel to its default entry skill. Users talk to the SOLUTION, not to individual skills — routing determines which skill handles each message.",
      "note": "Each key is a channel name (telegram, email, api, dashboard, etc.). Value is an object with default_skill and description. The \"api\" channel covers REST API, mobile apps, and web dashboard — always set it for multi-skill solutions.",
      "important": "Clients do NOT need to specify a skillSlug when sending messages. The platform resolves the skill automatically using this priority: 1. Conversation continuity (actor's last active skill) → 2. Channel routing (routing.<channel>.default_skill) → 3. Global default (policies.default_skill_slug) → 4. Auto-detect (gateway/orchestrator role → first deployed skill). For single-skill solutions, routing is optional. For multi-skill solutions, always define routing.api.default_skill to set the entry point.",
      "example": {
        "telegram": {
          "default_skill": "<gateway-skill-id>",
          "description": "All Telegram messages go to identity verification first"
        },
        "api": {
          "default_skill": "<gateway-skill-id>",
          "description": "Mobile app, web dashboard, and API calls go to gateway"
        },
        "email": {
          "default_skill": "<gateway-skill-id>",
          "description": "All emails go to gateway first"
        }
      }
    },
    "engine": {
      "type": "object",
      "required": false,
      "description": "Solution-wide engine/finalization-gate overrides. Skills inherit fields unless they declare the same field at the skill level. Use sparingly — most engine tuning belongs on the skill, not the solution. The intended use is tenant-wide rollouts of verifier-evolution flags (e.g. include_read_evidence_in_gate).",
      "fields": {
        "include_read_evidence_in_gate": {
          "type": "boolean",
          "required": false,
          "description": "Tenant-wide opt-in to the READ EVIDENCE channel of the finalization verifier (default: false). When set, every skill in this solution inherits the flag UNLESS the skill overrides it with its own engine.include_read_evidence_in_gate. See the skill-level field for the full description of behavior. Use this for tenant-wide rollout once per-skill A/B observation completes (see Docs/specs/VERIFIER_EVIDENCE_CHANNELS.md in ai-dev-assistant for the rollout policy)."
        }
      }
    },
    "platform_connectors": {
      "type": "array",
      "required": false,
      "description": "MCP connectors the solution depends on — both platform-shared (default) and solution-owned (source:\"solution\"). Solution-owned connectors have their source code in connectors/<id>/ inside the solution's GitHub repo; platform-shared connectors are deployed and maintained by the A-Team platform.",
      "item_schema": {
        "id": {
          "type": "string",
          "required": true,
          "description": "Connector ID"
        },
        "source": {
          "type": "enum",
          "required": false,
          "values": [
            "platform",
            "solution"
          ],
          "default": "platform",
          "description": "Ownership: \"platform\" = shared infra, source lives in A-Team images; \"solution\" = owned by this solution, source lives in connectors/<id>/ of the solution repo. Default \"platform\" for back-compat. Tools that classify (e.g. the auto-generated CLAUDE.md generator) should read this field rather than guessing from folder presence."
        },
        "required": {
          "type": "boolean",
          "description": "Is this connector required for the solution to work?"
        },
        "description": {
          "type": "string"
        },
        "used_by": {
          "type": "string[]",
          "required": false,
          "description": "Which skill IDs use this connector"
        },
        "ui_capable": {
          "type": "boolean",
          "required": false
        }
      }
    },
    "memory_engine": {
      "in_one_sentence": "The memory connector is the user's brain — your skills READ from it, the platform WRITES to it, and you focus on doing the actual work.",
      "headline": "Every solution gets a persistent cognitive memory layer for each user — already built, already deployed, already isolated. You don't wire it. You don't deploy it. You just read from it.",
      "what_it_is": "A long-term per-user brain. Each user has their own private memory island, fully isolated from other users even within the same solution. The platform provides the connector (memory-mcp) ready to use — add memory-mcp to your skill's connectors[] array and the read tools are available.",
      "why_it_matters": "Without it, your solution starts every conversation from zero. With it, your solution behaves like a friend who remembers — preferences, facts, routines, taught rules, past situations.",
      "memory_types": {
        "semantic": {
          "holds": "Facts about the user and their world",
          "example": "\"Lives in Tel Aviv\", \"Allergic to peanuts\", \"Prefers window seats\""
        },
        "procedural": {
          "holds": "Rules and learned behaviors",
          "example": "\"When user says good morning → turn on lights\""
        },
        "episodic": {
          "holds": "Things that happened",
          "example": "Past conversations, completed jobs"
        },
        "working": {
          "holds": "Short-lived state with TTL",
          "example": "\"User is running late\", \"Current plan in progress\""
        },
        "meta": {
          "holds": "Provenance / audit trail",
          "example": "When/why a memory was stored, who taught it"
        }
      },
      "read_vs_write": {
        "rule": "Skills are READ-ONLY consumers. The platform handles ALL writes.",
        "why": "Memory writes happen in two places, neither involves your skill: (1) the engine auto-extracts facts from conversations in the background, (2) the platform's teach handler captures explicit \"remember that...\" / \"from now on...\" statements as structured rules. Your skill never needs to call a write tool.",
        "consequence": "Don't hardcode user knowledge. Don't try to \"remember things yourself.\" Don't build your own teach skill. Trust the engine — it's already learning."
      },
      "read_tools": {
        "memory.userProfile()": "Call this at the START of every interaction. One call returns the user's name, timezone, preferences, facts, instructions, and active rules. Use it to personalize replies.",
        "memory.recall(query, type?, limit?)": "Search memories by query. Example: \"what did the user tell me about their diet?\"",
        "memory.list(type?, limit?, offset?)": "List all memories of a given type",
        "memory.rules.match(situation)": "Check if any user-taught rules apply to the current situation. Call this BEFORE taking an action that might conflict with a rule.",
        "memory.rules.count()": "Count active rules",
        "memory.semantic.search(query)": "Semantic search across all memories with similarity scoring",
        "memory.summarize(type?)": "\"What do you know about this user?\" — high-level summary",
        "memory.explain(id)": "\"Why is this remembered? Where did it come from?\" — provenance trail",
        "memory.audit(filter?)": "Inspect the memory audit log",
        "context.read(type)": "Read short-lived working state (situations, plans)"
      },
      "write_tools_note": "Write operations (memory.store, memory.update, memory.delete, memory.userProfile.set, context.store, context.resolve, context.clear) are RESERVED for the platform. Solution skills must NOT call them. They happen automatically via the auto-learning pipeline and the platform's teach handler.",
      "how_auto_learning_works": {
        "intro": "The engine listens to every conversation and extracts memorable things in three passes:",
        "passes": {
          "cheap_pass": {
            "when": "Every interaction",
            "cost": "Free",
            "method": "Regex to catch obvious facts",
            "examples": [
              "\"I live in...\"",
              "\"my name is...\"",
              "\"I'm allergic to...\""
            ]
          },
          "smart_pass": {
            "when": "Daily batch per user",
            "cost": "Cheap (Haiku)",
            "method": "LLM extraction of subtler facts and preferences"
          },
          "deep_pass": {
            "when": "Weekly batch per user",
            "cost": "Medium",
            "method": "Pattern detection across history",
            "example": "\"User always asks about Hebrew news in the morning\""
          }
        },
        "tagging": "Auto-learned memories are tagged source: \"auto\" with confidence 0.5–0.8 and decay over time. Explicitly taught memories are tagged source: \"taught\" with confidence 1.0 and don't decay. Users can review and correct everything via a dedicated UI."
      },
      "intelligence": [
        "Normalizes input on write — \"i prefer dark mode plz\" becomes \"Prefers dark mode UI\"",
        "Deduplicates semantically — repeating yourself doesn't create 5 entries",
        "Detects contradictions — \"lives in Tel Aviv\" + later \"moved to Berlin\" → old marked superseded, new stored",
        "Recalls semantically — \"what does the user like in the morning?\" matches even if no memory contains \"morning\"",
        "Decays stale memories — facts that haven't been touched in months get lower priority",
        "Compacts in the background — clusters similar memories into clean summaries",
        "Tracks provenance — every memory has an audit trail: where it came from, why it's there, confidence"
      ],
      "skill_design_rules": {
        "do": [
          "Call memory.userProfile at the START of every interaction — let the engine give you the real picture of who you're talking to.",
          "Call memory.rules.match BEFORE taking an action — the user may have taught a behavior that conflicts with what you're about to do.",
          "Use memory.recall when you need context-specific facts (\"what did the user say about their diet?\")",
          "Trust the engine — it learns automatically, you don't need to remind it."
        ],
        "dont": [
          "Don't hardcode user knowledge — always read it from the engine.",
          "Don't try to remember things yourself — auto-extraction is already running.",
          "Don't call write tools (memory.store, memory.update, etc.) — they're platform-only.",
          "Don't build your own teach skill — the platform handles \"remember that...\" and \"from now on...\".",
          "Don't cache memory results in your skill state — re-read each interaction (the engine may have learned new things since)."
        ]
      },
      "actor_isolation": "You don't pass an actor ID — the platform injects it automatically based on who's chatting. You don't pass a tenant — handled too. Memory is per-tenant + per-actor. Cross-user data leaks are impossible by construction.",
      "cross_skill": "The same memory works across every skill in your solution. What one skill learns (via the platform's extraction), all skills see.",
      "forward_compatibility": "Your skill calls don't change when the engine gets smarter (better embeddings, new dedup strategies, smarter extraction). Same tool names, smarter results.",
      "what_you_dont_do": [
        "Set up a database",
        "Design a memory schema",
        "Enforce per-user isolation",
        "Worry about cross-user data leaks",
        "Manage embeddings, vector search, or LLM integration",
        "Implement background compaction or decay logic",
        "Build a \"memory review\" UI (already exists)",
        "Build a \"teach my assistant\" skill (platform handles it)",
        "Wire memory tools into every new skill (just add memory-mcp to connectors)",
        "Migrate data when your solution evolves",
        "Optimize cost on the LLM extraction pipeline"
      ],
      "when_to_use": [
        "Personalization — adapt behavior to who you're talking to",
        "Preferences — remember how the user likes things done",
        "Rules and automations — react to user-taught behaviors",
        "Routine detection — notice patterns and proactively help",
        "Context continuation — pick up where the conversation left off",
        "Reducing repetition — don't ask the user the same question twice"
      ],
      "when_NOT_to_use": [
        "Application state that belongs in your own database",
        "Tenant-wide configuration (use solution settings instead)",
        "Logs / audit data (use a logging system)",
        "Large blobs (files, images, transcripts — store separately and reference them)",
        "Anything sensitive the user didn't intend to share — memory is not a surveillance log"
      ],
      "the_promise": "Every solution on this platform inherits a real, persistent, intelligent memory layer for its users — without writing a single line of memory code. Use it, and your solution feels like it knows the user. Skip it, and you're building a goldfish.",
      "how_to_enable": "Add \"memory-mcp\" to your skill's connectors[] array. Then add the READ tools you need to your skill's tools[] (e.g. memory.userProfile, memory.recall, memory.rules.match). The platform handles writes, isolation, extraction, and everything else."
    },
    "platform_connectors_reference": {
      "description": "Pre-built connectors managed at the platform level. These run as Docker containers and are shared across all tenants. Solution developers USE them (add to skill.connectors[]) but do NOT create or modify them. Do NOT include their source code in mcp_store or GitHub — they are platform infrastructure.",
      "important": "Platform connectors cannot be modified via ateam_github_patch or ateam_upload_connector. Changes must be made in the ai-dev-assistant repo and the container restarted.",
      "connectors": {
        "memory-mcp": {
          "transport": "http",
          "port": 7306,
          "description": "Persistent cognitive memory engine — every solution gets a built-in long-term brain per user. Semantic recall, deduplication, contradiction detection, automatic extraction, decay, compaction. Per-user isolated. See memory_engine spec section for full capabilities.",
          "tools": [
            {
              "name": "memory.store",
              "description": "Store a long-term HUMAN/AGENT CONTEXT memory — facts, preferences, taught rules, user profile fields. Content must be concise, durable, and recallable. Types: preference, fact, instruction, pattern, rule, user_model; the engine assigns the CoALA kind automatically (semantic / procedural). DO NOT use for: session cookies, OAuth tokens, API keys, or any credentials (use platform.dataStore). DO NOT use for: raw tool-output dumps, large JSON blobs, or cached API responses (use platform.dataStore). The engine runs quality guards, dedup, normalize, and conflict detection; rejections include a `guidance` field telling you the correct tool/path. Returns { memory, conflicts, processing }.",
              "inputs": [
                {
                  "name": "type",
                  "type": "string",
                  "description": "Memory type",
                  "required": true
                },
                {
                  "name": "content",
                  "type": "string",
                  "description": "Memory content (natural language or JSON for structured types)",
                  "required": true
                },
                {
                  "name": "tags",
                  "type": "array",
                  "description": "",
                  "required": false
                },
                {
                  "name": "context",
                  "type": "string",
                  "description": "",
                  "required": false
                },
                {
                  "name": "source",
                  "type": "string",
                  "description": "Provenance (default: taught)",
                  "required": false
                },
                {
                  "name": "confidence",
                  "type": "number",
                  "description": "",
                  "required": false
                }
              ]
            },
            {
              "name": "memory.recall",
              "description": "Recall memories by keyword query. Searches content + raw_content + tags. When embeddings come online this transparently becomes semantic.",
              "inputs": [
                {
                  "name": "query",
                  "type": "string",
                  "description": "",
                  "required": true
                },
                {
                  "name": "type",
                  "type": "string",
                  "description": "",
                  "required": false
                },
                {
                  "name": "kind",
                  "type": "string",
                  "description": "",
                  "required": false
                },
                {
                  "name": "limit",
                  "type": "number",
                  "description": "",
                  "required": false
                }
              ]
            },
            {
              "name": "memory.list",
              "description": "List stored memories with optional type/kind filter and pagination.",
              "inputs": [
                {
                  "name": "type",
                  "type": "string",
                  "description": "",
                  "required": false
                },
                {
                  "name": "kind",
                  "type": "string",
                  "description": "",
                  "required": false
                },
                {
                  "name": "limit",
                  "type": "number",
                  "description": "",
                  "required": false
                },
                {
                  "name": "offset",
                  "type": "number",
                  "description": "",
                  "required": false
                }
              ]
            },
            {
              "name": "memory.update",
              "description": "Update an existing memory by ID.",
              "inputs": [
                {
                  "name": "id",
                  "type": [
                    "string",
                    "number"
                  ],
                  "description": "",
                  "required": true
                },
                {
                  "name": "content",
                  "type": "string",
                  "description": "",
                  "required": false
                },
                {
                  "name": "tags",
                  "type": "array",
                  "description": "",
                  "required": false
                },
                {
                  "name": "context",
                  "type": "string",
                  "description": "",
                  "required": false
                }
              ]
            },
            {
              "name": "memory.delete",
              "description": "Delete a memory by ID.",
              "inputs": [
                {
                  "name": "id",
                  "type": [
                    "string",
                    "number"
                  ],
                  "description": "",
                  "required": true
                }
              ]
            },
            {
              "name": "memory.rules.match",
              "description": "Match active taught rules against a situation. Returns ranked rules. Used by Core fast_path / detectIntent to decide whether to short-circuit.",
              "inputs": [
                {
                  "name": "situation",
                  "type": "string",
                  "description": "",
                  "required": true
                },
                {
                  "name": "limit",
                  "type": "number",
                  "description": "",
                  "required": false
                }
              ]
            },
            {
              "name": "memory.rules.count",
              "description": "Count active vs inactive taught rules for the current actor.",
              "inputs": []
            },
            {
              "name": "context.store",
              "description": "Store an ephemeral context (situation or plan). Auto-expires via Mongo TTL index.",
              "inputs": [
                {
                  "name": "type",
                  "type": "string",
                  "description": "",
                  "required": true
                },
                {
                  "name": "data",
                  "type": "string",
                  "description": "JSON string",
                  "required": true
                },
                {
                  "name": "ttl_minutes",
                  "type": "number",
                  "description": "",
                  "required": false
                }
              ]
            },
            {
              "name": "context.read",
              "description": "Read active ephemeral context — current situations and plans.",
              "inputs": [
                {
                  "name": "type",
                  "type": "string",
                  "description": "",
                  "required": false
                },
                {
                  "name": "limit",
                  "type": "number",
                  "description": "",
                  "required": false
                }
              ]
            },
            {
              "name": "context.resolve",
              "description": "Mark an ephemeral context as resolved or abandoned.",
              "inputs": [
                {
                  "name": "id",
                  "type": [
                    "string",
                    "number"
                  ],
                  "description": "",
                  "required": true
                },
                {
                  "name": "status",
                  "type": "string",
                  "description": "",
                  "required": false
                }
              ]
            },
            {
              "name": "context.clear",
              "description": "Clear all active ephemeral context for the actor (optionally filtered by type).",
              "inputs": [
                {
                  "name": "type",
                  "type": "string",
                  "description": "",
                  "required": false
                }
              ]
            },
            {
              "name": "memory.userProfile",
              "description": "Get the complete user profile in one call. Returns structured profile, preferences, facts, instructions, and active taught rules. Call at the start of every interaction.",
              "inputs": []
            },
            {
              "name": "memory.userProfile.set",
              "description": "Set a structured profile field (name, timezone, location, etc). Creates or updates a user_model memory.",
              "inputs": [
                {
                  "name": "field",
                  "type": "string",
                  "description": "",
                  "required": true
                },
                {
                  "name": "value",
                  "type": "string",
                  "description": "",
                  "required": true
                }
              ]
            },
            {
              "name": "memory.semantic.search",
              "description": "Semantic search over memories. Returns matches with similarity scores. Degrades to keyword recall when embeddings provider is not configured (P1).",
              "inputs": [
                {
                  "name": "query",
                  "type": "string",
                  "description": "",
                  "required": true
                },
                {
                  "name": "kind",
                  "type": "string",
                  "description": "",
                  "required": false
                },
                {
                  "name": "limit",
                  "type": "number",
                  "description": "",
                  "required": false
                }
              ]
            },
            {
              "name": "memory.explain",
              "description": "Explain why a memory is remembered: returns the memory + its full meta trail.",
              "inputs": [
                {
                  "name": "id",
                  "type": [
                    "string",
                    "number"
                  ],
                  "description": "",
                  "required": true
                }
              ]
            },
            {
              "name": "memory.summarize",
              "description": "Structured snapshot of what's known about the current actor. P1 returns counts + structured profile. LLM narrative summary will be added in P6.",
              "inputs": []
            },
            {
              "name": "memory.compact",
              "description": "Run a compaction sweep — decay stale auto-learned memories and surface duplicate clusters. Does not auto-merge in P1; returns candidates for review.",
              "inputs": []
            },
            {
              "name": "memory.confidence",
              "description": "Read or set the confidence score on a memory.",
              "inputs": [
                {
                  "name": "id",
                  "type": [
                    "string",
                    "number"
                  ],
                  "description": "",
                  "required": true
                },
                {
                  "name": "confidence",
                  "type": "number",
                  "description": "Omit to read, provide to set",
                  "required": false
                }
              ]
            },
            {
              "name": "memory.supersede",
              "description": "Mark old memory as superseded by new memory. Old becomes inactive, link recorded.",
              "inputs": [
                {
                  "name": "old_id",
                  "type": [
                    "string",
                    "number"
                  ],
                  "description": "",
                  "required": true
                },
                {
                  "name": "new_id",
                  "type": "string",
                  "description": "",
                  "required": true
                }
              ]
            },
            {
              "name": "memory.audit",
              "description": "Read meta trail for a single memory or recent events for the actor.",
              "inputs": [
                {
                  "name": "id",
                  "type": [
                    "string",
                    "number"
                  ],
                  "description": "Memory ID — if omitted, returns recent actor events",
                  "required": false
                },
                {
                  "name": "limit",
                  "type": "number",
                  "description": "",
                  "required": false
                }
              ]
            },
            {
              "name": "memory.embed",
              "description": "Generate an embedding vector for arbitrary text. Returns null when no embedding provider is configured (P1 default).",
              "inputs": [
                {
                  "name": "text",
                  "type": "string",
                  "description": "",
                  "required": true
                }
              ]
            },
            {
              "name": "memory.extract",
              "description": "Run end-of-job auto-extraction over a transcript. Cheap regex pass always runs; smart LLM pass runs if smart=true and Core LLM is reachable. Writes extracted memories with source='auto' and lower confidence.",
              "inputs": [
                {
                  "name": "transcript",
                  "type": "array",
                  "description": "Conversation turns",
                  "required": true
                },
                {
                  "name": "smart",
                  "type": "boolean",
                  "description": "Also run LLM pass (default false in P1)",
                  "required": false
                }
              ]
            }
          ],
          "status": "connected"
        },
        "whatsapp-mcp": {
          "transport": "http",
          "port": 7305,
          "description": "WhatsApp Business messaging — send/receive messages, check connection, pair devices. Per-actor sessions.",
          "ui_plugins": [
            "whatsapp-setup"
          ],
          "tools": [
            {
              "name": "whatsapp.status",
              "description": "Check WhatsApp connection status. Returns whether connected, pending pairing, or disconnected.",
              "inputs": []
            },
            {
              "name": "whatsapp.setup",
              "description": "Connect to WhatsApp. Provide the user's phone number (with country code) to get a pairing code — the user enters it in WhatsApp > Linked Devices > Link with phone number. If session already exists, reconnects automatically. Falls back to QR code if no phone number provided.",
              "inputs": [
                {
                  "name": "phone_number",
                  "type": "string",
                  "description": "User's phone number with country code, digits only (e.g., '14155551234'). Required for pairing code flow (mobile). Omit for QR code flow (web).",
                  "required": false
                }
              ]
            },
            {
              "name": "whatsapp.send",
              "description": "Send a WhatsApp message to a contact. Provide a phone number (with country code like +1...) or a contact name (matched against recent messages). ALWAYS confirm with the user before calling this tool.",
              "inputs": [
                {
                  "name": "to",
                  "type": "string",
                  "description": "Phone number (with country code, e.g., '+14155551234') or contact name",
                  "required": true
                },
                {
                  "name": "message",
                  "type": "string",
                  "description": "Message text to send",
                  "required": true
                }
              ]
            },
            {
              "name": "whatsapp.unread",
              "description": "Get unread WhatsApp chats with their unread counts. Includes both 1:1 and group chats by default. Reads authoritative unread counts from WhatsApp's own chat state — not just the local message buffer.",
              "inputs": [
                {
                  "name": "limit",
                  "type": "number",
                  "description": "Max chats to return (default 30, max 100)",
                  "required": false
                },
                {
                  "name": "include_groups",
                  "type": "boolean",
                  "description": "Include group chats (default true)",
                  "required": false
                }
              ]
            },
            {
              "name": "whatsapp.recent",
              "description": "Get recent WhatsApp conversations, newest first. Reads from the chat store populated by WhatsApp's chat state sync, not just messages received this session.",
              "inputs": [
                {
                  "name": "limit",
                  "type": "number",
                  "description": "Max conversations to return (default 10, max 50)",
                  "required": false
                },
                {
                  "name": "include_groups",
                  "type": "boolean",
                  "description": "Include group chats (default true)",
                  "required": false
                }
              ]
            },
            {
              "name": "whatsapp.search",
              "description": "Search WhatsApp messages by contact name, phone number, or message content.",
              "inputs": [
                {
                  "name": "query",
                  "type": "string",
                  "description": "Search query — matches contact name, phone, or message text",
                  "required": true
                },
                {
                  "name": "limit",
                  "type": "number",
                  "description": "Max results (default 20, max 50)",
                  "required": false
                }
              ]
            }
          ],
          "status": "connected"
        },
        "telegram-mcp": {
          "transport": "http",
          "port": 7302,
          "description": "Telegram messaging — send messages, manage webhooks for inbound.",
          "tools": [
            {
              "name": "send_message",
              "description": "Send a text message to a Telegram chat. Pass `reply_markup` to attach an inline keyboard (e.g. widget-link buttons emitted by notif-router).",
              "inputs": [
                {
                  "name": "chat_id",
                  "type": [
                    "string",
                    "number"
                  ],
                  "description": "Telegram chat ID",
                  "required": true
                },
                {
                  "name": "text",
                  "type": "string",
                  "description": "Message text",
                  "required": true
                },
                {
                  "name": "reply_to_message_id",
                  "type": "number",
                  "description": "Message ID to reply to (for threading)",
                  "required": false
                },
                {
                  "name": "parse_mode",
                  "type": "string",
                  "description": "Text formatting mode",
                  "required": false
                },
                {
                  "name": "reply_markup",
                  "type": "object",
                  "description": "Inline keyboard / reply markup, forwarded verbatim to Telegram",
                  "required": false
                },
                {
                  "name": "_bot_token",
                  "type": "string",
                  "description": "Per-tenant bot token override",
                  "required": false
                }
              ]
            },
            {
              "name": "set_webhook",
              "description": "Register a webhook URL with Telegram for receiving updates.",
              "inputs": [
                {
                  "name": "url",
                  "type": "string",
                  "description": "HTTPS URL for Telegram to send updates to",
                  "required": true
                },
                {
                  "name": "_bot_token",
                  "type": "string",
                  "description": "Per-tenant bot token override",
                  "required": false
                }
              ]
            },
            {
              "name": "get_me",
              "description": "Get information about the bot. Useful for verifying the token and connectivity.",
              "inputs": [
                {
                  "name": "_bot_token",
                  "type": "string",
                  "description": "Per-tenant bot token override",
                  "required": false
                }
              ]
            },
            {
              "name": "get_updates",
              "description": "Long-poll for new messages from Telegram. Returns an array of Update objects.",
              "inputs": [
                {
                  "name": "offset",
                  "type": "number",
                  "description": "ID of first update to return",
                  "required": false
                },
                {
                  "name": "limit",
                  "type": "number",
                  "description": "Max number of updates (1-100)",
                  "required": false
                },
                {
                  "name": "timeout",
                  "type": "number",
                  "description": "Long-polling timeout in seconds (0-50)",
                  "required": false
                },
                {
                  "name": "_bot_token",
                  "type": "string",
                  "description": "Per-tenant bot token override",
                  "required": false
                }
              ]
            },
            {
              "name": "delete_webhook",
              "description": "Remove any active webhook so getUpdates works.",
              "inputs": [
                {
                  "name": "_bot_token",
                  "type": "string",
                  "description": "Per-tenant bot token override",
                  "required": false
                }
              ]
            }
          ],
          "status": "connected"
        },
        "gmail-mcp": {
          "transport": "http",
          "port": 7301,
          "description": "Gmail integration — send, read, search, archive, trash, label emails. OAuth-based.",
          "tools": [
            {
              "name": "auth.listServices",
              "description": "Internal — returns the auth services this connector handles, for platform.auth self-registration.",
              "inputs": []
            },
            {
              "name": "gmail.status",
              "description": "Internal — liveness probe for p

(Response truncated from 244,746 chars. Use more specific queries to get smaller results.)

Builder workflows

ateam_get_workflows()
{
  "description": "Builder workflows — the step-by-step state machines the Skill Builder uses internally. Use these to guide users through building skills and solutions conversationally, replicating the Skill Builder experience.",
  "usage": "When a user wants to build a skill or solution, read this workflow and follow it step by step. At each phase, ask the user the right questions, collect their answers, build the definition incrementally, validate with adas_validate_skill / adas_validate_solution, and deploy with adas_deploy_solution.",
  "skill_workflow": {
    "description": "State machine for building a single A-Team skill (autonomous AI agent)",
    "phases": [
      {
        "id": "PROBLEM_DISCOVERY",
        "order": 1,
        "label": "Problem Discovery",
        "goal": "Understand what problem this skill solves",
        "what_to_ask": [
          "What problem should this AI agent solve?",
          "Who are the users? What domain is this in?",
          "What systems or data does it need access to?"
        ],
        "what_to_build": {
          "problem.statement": "Clear problem description (min 10 chars)",
          "problem.context": "Domain context and background",
          "problem.goals": "What the skill aims to achieve"
        },
        "exit_criteria": "problem.statement is defined and >= 10 characters",
        "tips": [
          "Always suggest a concrete problem statement — don't just ask",
          "Include the domain context to help with later phases"
        ]
      },
      {
        "id": "SCENARIO_EXPLORATION",
        "order": 2,
        "label": "Scenario Exploration",
        "goal": "Define concrete use cases that demonstrate the skill in action",
        "what_to_ask": [
          "What are the most common situations this agent will handle?",
          "Walk me through a typical interaction step by step",
          "What edge cases should the agent handle?"
        ],
        "what_to_build": {
          "scenarios[]": "Array of scenarios, each with id, title, description, steps, expected_outcome"
        },
        "exit_criteria": "At least 1 scenario with title, description, and steps",
        "tips": [
          "Suggest 2-3 scenarios based on the problem statement",
          "Each scenario should map to a different user intent",
          "Include both happy path and edge cases"
        ]
      },
      {
        "id": "INTENT_DEFINITION",
        "order": 3,
        "label": "Intent Definition",
        "goal": "Define the user intents this skill can handle",
        "what_to_ask": [
          "What distinct requests can users make?",
          "For each intent, what are example phrases a user might say?",
          "What should happen when an unrecognized request comes in?"
        ],
        "what_to_build": {
          "intents.supported[]": "Array of intents, each with id, description, and examples (diverse phrasings)",
          "intents.thresholds": "Confidence thresholds for accept/clarify/reject",
          "intents.out_of_domain": "What to do with unrecognized requests"
        },
        "exit_criteria": "At least 1 intent with at least 1 example",
        "tips": [
          "Derive intents from the scenarios — each scenario usually maps to 1-2 intents",
          "Example phrases should be diverse, not repetitive",
          "Set out_of_domain to redirect for multi-skill solutions"
        ]
      },
      {
        "id": "TOOLS_PROPOSAL",
        "order": 4,
        "label": "Tools Proposal",
        "goal": "Propose the tools (actions) this agent needs",
        "what_to_ask": [
          "What actions does the agent need to perform?",
          "What data does it need to read or write?",
          "Which external systems does it connect to?"
        ],
        "what_to_build": {
          "tools[]": "Initial tool definitions with name, description — details come in next phase",
          "connectors": "Which MCP connectors provide these tools"
        },
        "exit_criteria": "At least 1 tool defined",
        "tips": [
          "Map each scenario step to a tool",
          "Use naming convention: connector-prefix.resource.action (e.g., orders.order.get)",
          "Don't forget system tools (sys.askUser, sys.emitUserMessage, sys.handoffToSkill, sys.askAnySkill, sys.findCapability, sys.listSkills, sys.focusUiPlugin, sys.trigger) for user interaction, multi-agent routing, UI control, and dynamic trigger management"
        ]
      },
      {
        "id": "TOOL_DEFINITION",
        "order": 5,
        "label": "Tool Definition",
        "goal": "Fully define each tool with inputs, outputs, source, and security",
        "what_to_ask": [
          "For each tool: what inputs does it need? What does it return?",
          "What security classification? (public, internal, pii_read, pii_write, financial, destructive)",
          "Are there any conditions for when the tool should be blocked or require approval?"
        ],
        "what_to_build": {
          "tools[].inputs": "Input parameters with name, type, required, description",
          "tools[].output": "Output type and description",
          "tools[].source": "Source connector and MCP tool name",
          "tools[].security": "Classification (required) and risk level",
          "tools[].policy": "Access conditions, approval requirements"
        },
        "exit_criteria": "All tools have inputs defined and output.description",
        "tips": [
          "Every tool needs security.classification — this is the most common validation error",
          "High-risk tools (pii_write, financial, destructive) need access_policy rules",
          "If the skill has >= 3 tools, suggest setting bootstrap_tools (up to 3 core tools always available to the planner)",
          "Add mock examples for each tool — needed for testing"
        ]
      },
      {
        "id": "POLICY_DEFINITION",
        "order": 6,
        "label": "Policy Definition",
        "goal": "Define guardrails, workflows, and approval rules",
        "what_to_ask": [
          "What should this agent NEVER do?",
          "What should it ALWAYS do?",
          "Are there specific sequences of steps that must be followed?",
          "Which actions need human approval?"
        ],
        "what_to_build": {
          "policy.guardrails.never": "Things the agent must never do",
          "policy.guardrails.always": "Things the agent must always do",
          "policy.workflows": "Named step sequences triggered by intents",
          "policy.approvals": "Tools that need human approval",
          "policy.escalation": "When and where to escalate"
        },
        "exit_criteria": "At least one guardrail (never or always) defined",
        "tips": [
          "Guardrails should be specific and actionable",
          "Workflow steps use tool NAMES not IDs",
          "System tools (sys.askUser, sys.emitUserMessage, sys.handoffToSkill, sys.askAnySkill, sys.findCapability, sys.focusUiPlugin, sys.trigger) are valid workflow steps"
        ]
      },
      {
        "id": "TRIGGERS_SETUP",
        "order": 7,
        "label": "Triggers & Proactive Messaging (Optional)",
        "goal": "Set up schedule or event triggers for proactive skill execution — sending notifications, monitoring state, running periodic checks. Two approaches: STATIC triggers (defined here, fixed at deploy) and DYNAMIC triggers (agent creates at runtime via sys.trigger).",
        "what_to_ask": [
          "Does this skill need to act proactively (without a user message)?",
          "What should it check, and how often? (e.g., stale tasks every 15min, daily summary)",
          "Should it send notifications (Telegram, email)? To whom (chat_id, email address)?",
          "What conditions should trigger a notification vs. doing nothing?",
          "Should users be able to create triggers dynamically? (e.g., \"remind me at 9 AM\", \"check X every 5 minutes\") — if yes, add sys.trigger to bootstrap_tools"
        ],
        "what_to_build": {
          "triggers[]": "STATIC: Schedule or event triggers with specific prompts, intervals, and concurrency settings (fixed at deploy time)",
          "bootstrap_tools": "DYNAMIC: Add \"sys.trigger\" to bootstrap_tools if the agent should create triggers at runtime (user-requested schedules, reminders, monitoring)",
          "connectors": "Add messaging connectors (telegram-mcp, gmail-mcp) to skill.connectors[] if sending notifications",
          "tools": "Add send tools (telegram.send_message, gmail.send_email) mapped to messaging connectors via source.connection_id"
        },
        "exit_criteria": "Triggers are defined with specific prompts, or explicitly skipped (most skills don't need triggers). If dynamic triggers are needed, sys.trigger is in bootstrap_tools.",
        "tips": [
          "Most skills do NOT need triggers — only add them for proactive behavior",
          "The trigger prompt is the most important part — it tells the skill exactly what to do when triggered",
          "STATIC vs DYNAMIC: use static triggers for predictable, always-on schedules (e.g., \"check orders every 5 min\"). Use dynamic (sys.trigger) for user-requested schedules (\"remind me tomorrow\", \"turn on AC at 3 PM daily\")",
          "Always include idempotency logic in the prompt (e.g., \"skip tasks that already have a NOTIFIED comment\")",
          "For Telegram notifications: you need the target chat_id and telegram-mcp connector linked to the skill",
          "For email: you need gmail-mcp connector linked and the target email address",
          "Test the trigger prompt with ateam_test_skill first — send it as a test message to verify the skill does the right thing before enabling the schedule",
          "The proactive_messaging pattern: connector (telegram-mcp) + tool (send_message) + trigger (schedule with prompt) = outbound notifications"
        ]
      },
      {
        "id": "MOCK_TESTING",
        "order": 8,
        "label": "Mock Testing",
        "goal": "Add mock data for tools and test scenarios without real backends",
        "what_to_ask": [
          "What should each tool return in a typical case?",
          "What error cases should we test?"
        ],
        "what_to_build": {
          "tools[].mock": "Mock configuration with mode and example input/output pairs",
          "tools[].mock_status": "Set to \"tested\" or \"skipped\" for each tool"
        },
        "exit_criteria": "All tools have mock_status != \"untested\"",
        "tips": [
          "Mock examples should cover both success and error cases",
          "Set mock_status to \"skipped\" for tools that don't need mock testing"
        ]
      },
      {
        "id": "READY_TO_EXPORT",
        "order": 9,
        "label": "Ready to Export",
        "goal": "Final validation gate — all checks must pass",
        "what_to_do": [
          "Run adas_validate_skill to check for errors",
          "Fix any validation errors",
          "Review the complete skill definition"
        ],
        "what_to_build": {
          "role": "Ensure role.name and role.persona are set",
          "engine": "Set model, temperature, rv2, hlr, autonomy",
          "grant_mappings": "If this skill issues grants for other skills",
          "access_policy": "If this skill consumes grants from other skills"
        },
        "exit_criteria": "Validation returns ready_to_export = true with no errors",
        "tips": [
          "Use adas_validate_skill to get the full validation report",
          "The engine section needs the full structure (model, temperature, rv2, hlr, autonomy) — not just shortcuts"
        ]
      },
      {
        "id": "EXPORTED",
        "order": 10,
        "label": "Exported / Deployed",
        "goal": "Skill is deployed and running",
        "what_to_do": [
          "Deploy via adas_deploy_solution (recommended) or adas_deploy_skill",
          "Verify deployment with adas_get_solution (view: \"health\")"
        ]
      },
      {
        "id": "GITHUB_ITERATION",
        "order": 11,
        "label": "GitHub Iteration (Dev/Main Workflow)",
        "goal": "Iterate on connector code using GitHub dev branch, promote tested versions to main (production)",
        "what_to_do": [
          "Every deploy auto-pushes to \"dev\" branch with a version tag (dev-YYYY-MM-DD-NNN). The push is ASYNC — deploy responds immediately.",
          "The \"main\" branch is production. Only promoted versions go there.",
          "To verify background push: use ateam_github_status(solution_id)",
          "To edit connector code: use ateam_github_patch (edits files in GitHub directly)",
          "To redeploy from GitHub: use ateam_build_and_run with github:true",
          "To update skill definitions: use ateam_patch (definitions live in the Builder, not GitHub)",
          "To test: use ateam_test_skill or ateam_test_pipeline"
        ],
        "iteration_loop": [
          "1. Read current connector code → ateam_get_connector_source(solution_id, connector_id) — ALWAYS read before editing",
          "2. Edit connector code → ateam_github_patch({ files: [{ path: \"connectors/my-mcp/server.js\", content: \"...\" }] })",
          "3. Redeploy from GitHub → ateam_build_and_run({ solution, skills, github: true }) — auto-pushes new version to dev",
          "4. Test → ateam_test_skill({ message: \"test query\" })",
          "5. Fix & repeat until working",
          "6. When ready → promote dev to main: POST /deploy/solutions/:id/promote"
        ],
        "per_connector_patching": {
          "_IMPORTANT": "Each connector has INDEPENDENT source code. Patching one connector does NOT change others.",
          "workflow": [
            "1. Identify which connectors need changes — use ateam_get_solution(solution_id, \"status\") to see all connectors and their tool counts",
            "2. For EACH connector that needs changes:",
            "   a. Read its current code: ateam_get_connector_source(solution_id, connector_id)",
            "   b. Patch its files: ateam_github_patch({ files: [{ path: \"connectors/<connector-id>/server.js\", content: \"...\" }] })",
            "3. After all connectors are patched, redeploy ONCE: ateam_build_and_run({ github: true })",
            "4. Test each connector's tools to verify: ateam_test_skill with messages that exercise each connector"
          ],
          "example": "If home-assistant-mcp supports \"set_brightness\" but hue-mcp and tuya-mcp do not, you need to: (1) read hue-mcp server.js, (2) add set_brightness tool to hue-mcp, (3) read tuya-mcp server.js, (4) add set_brightness tool to tuya-mcp, (5) redeploy once with github:true."
        },
        "version_management": {
          "branches": {
            "dev": "Staging — receives every successful deploy automatically. Last 10 versions kept.",
            "main": "Production — only changes via explicit promote. Rollback available."
          },
          "tags": {
            "dev": "dev-YYYY-MM-DD-NNN (e.g., dev-2026-03-11-003) — auto-created on each deploy",
            "prod": "prod-YYYY-MM-DD-NNN (e.g., prod-2026-03-11-001) — created on promote"
          },
          "commands": {
            "list_versions": "GET /deploy/solutions/:id/versions/dev",
            "promote": "POST /deploy/solutions/:id/promote — merges dev → main",
            "rollback": "POST /deploy/solutions/:id/rollback — reverts main to a previous prod tag. Body: { target: \"prod-...\" }. ADDITIVE (creates a new commit on top; history preserved).",
            "verify_push": "GET /deploy/solutions/:id/github/status — check if background push completed"
          }
        },
        "when_to_use_what": {
          "ateam_github_patch": "Edit connector source code (server.js, utils, package.json, UI assets). Supports search+replace for large files.",
          "ateam_github_write": "Write a new file to GitHub (one file per call). Use for creating new connector files.",
          "ateam_patch(target:\"skill\")": "Edit ANY skill field surgically — problem, role, intents, tools, policy, engine, scenarios, glossary. Supports dot notation and array ops (_push, _delete, _update). Deploys the single skill automatically.",
          "ateam_patch(target:\"solution\")": "Edit solution-level fields — linked_skills, platform_connectors, ui_plugins, grants, handoffs.",
          "ateam_upload_connector(github:true)": "Update connector code from GitHub and restart. Fast — deploys only the one connector, not the whole solution.",
          "ateam_build_and_run(github:true)": "Full solution redeploy from GitHub. Use for first deploy or when multiple skills+connectors changed. Auto-falls back to async mode if it times out.",
          "ateam_build_and_run(mcp_store)": "First deploy only — pass connector code inline to create the GitHub repo.",
          "ateam_redeploy(solution_id, skill_id)": "Re-deploy a single skill without changing its definition. Use after connector changes that affect a skill.",
          "ateam_verify_consistency": "Read-only check: do Builder FS and the GitHub repo agree for this solution? Returns drift list. Use after any unusual error to confirm state — much faster than scrolling commit logs."
        },
        "gh_write_semantics": {
          "description": "Whether each tool's GitHub push is synchronous (await commit SHA before responding) or async (fire-and-forget; verify separately).",
          "synchronous_trust_response": {
            "tools": [
              "ateam_patch",
              "ateam_github_patch",
              "ateam_github_write",
              "ateam_github_promote",
              "ateam_github_rollback"
            ],
            "guarantee": "When the response phase says github_write:done (or returns a commit_sha), the commit is on GitHub. The wrapper awaits GitHub's Contents API 200 response before resolving. F3 PR-6 also mirrors the change into Builder FS in the same call.",
            "if_failed": "The wrapper throws and you see {ok:false, phase:\"github_write\", error:\"...\"}. There is no silent failure mode here."
          },
          "async_verify_separately": {
            "tools": [
              "ateam_build_and_run"
            ],
            "behavior": "Validates + deploys to A-Team Core synchronously, returns the deploy result, THEN runs the GH push in the background with 15s timeouts + 2x retries.",
            "verify_with": "ateam_github_status(solution_id) to confirm the push landed, OR ateam_verify_consistency(solution_id) for a full FS↔GH drift report.",
            "why": "The full deploy bundle can be large; making the GH push synchronous would push response time over the upstream Cloudflare 100s limit on big solutions."
          },
          "no_gh_write": {
            "tools": [
              "ateam_redeploy",
              "ateam_test_skill",
              "ateam_test_pipeline",
              "ateam_get_solution",
              "ateam_status_all",
              "ateam_sync_all"
            ],
            "behavior": "These tools do not write to GitHub at all. They only deploy to Core, run tests, or read state."
          },
          "self_check": "Always available: ateam_verify_consistency(solution_id) returns {consistent:bool, drifts:[{path, kind}]}. If consistent:true, FS and GH agree on every byte (after JSON canonicalization + ephemeral-field stripping). Use this any time you suspect drift instead of grepping commit logs."
        },
        "large_solution_strategy": {
          "description": "Solutions with 5+ skills may timeout on full build_and_run (Cloudflare 100s limit). Use incremental tools instead.",
          "rules": [
            "NEVER use build_and_run for routine changes on large solutions — it deploys everything and may timeout.",
            "For skill definition changes: use ateam_patch(target:\"skill\", skill_id, updates) — deploys only that skill.",
            "For connector code changes: use ateam_github_patch to edit, then ateam_upload_connector(solution_id, connector_id, github:true) to deploy.",
            "If build_and_run times out, it auto-retries in async mode with polling — no action needed, just wait.",
            "If ateam_patch fails with \"not found\": skill is missing from Builder storage. Use ateam_github_patch to edit the skill JSON on GitHub (path: skills/<skill-id>/skill.json), then ask the platform operator to deploy."
          ]
        },
        "patch_examples": {
          "Change persona": "ateam_patch(target:\"skill\", skill_id:\"my-skill\", updates:{ \"role.persona\": \"You are a helpful assistant\" })",
          "Update problem statement": "ateam_patch(target:\"skill\", skill_id:\"my-skill\", updates:{ \"problem.statement\": \"Help users manage X\" })",
          "Add a guardrail": "ateam_patch(target:\"skill\", skill_id:\"my-skill\", updates:{ \"policy.guardrails.never_push\": [\"Never do X\"] })",
          "Add a tool": "ateam_patch(target:\"skill\", skill_id:\"my-skill\", updates:{ \"tools_push\": [{ name:\"conn.tool\", description:\"...\", inputs:[], output:{} }] })",
          "Delete a tool": "ateam_patch(target:\"skill\", skill_id:\"my-skill\", updates:{ \"tools_delete\": [\"old_tool_name\"] })",
          "Update intent": "ateam_patch(target:\"skill\", skill_id:\"my-skill\", updates:{ \"intents.supported_update\": [{ id:\"i1\", description:\"new description\" }] })",
          "Change engine model": "ateam_patch(target:\"skill\", skill_id:\"my-skill\", updates:{ \"engine.model\": \"claude-sonnet-4-5-20250514\" })",
          "Force redeploy": "ateam_patch(target:\"solution\", updates:{ \"_force_redeploy\": true })"
        },
        "tips": [
          "The first deploy MUST include mcp_store (inline connector code) — this creates the GitHub repo.",
          "After first deploy, use incremental tools: ateam_patch for skill changes, ateam_upload_connector for connector changes.",
          "For large solutions (5+ skills): NEVER use build_and_run for routine changes. Use ateam_patch and ateam_upload_connector instead.",
          "build_and_run auto-falls back to async mode on timeout — it returns a job_id and polls for completion (up to 10 min).",
          "Each connector has independent source code under connectors/{id}/server.js — read each one separately before patching.",
          "To add a feature across multiple connectors, you must patch each connector's server.js individually.",
          "GitHub push is ASYNC — the deploy responds immediately. Use ateam_github_status() to verify the push landed.",
          "Promotion: ateam_github_promote merges dev → main and auto-tags prod-YYYY-MM-DD-NNN. Rollback with ateam_github_rollback(target: \"prod-...\") — additive, preserves history."
        ]
      }
    ],
    "completeness_fields": [
      "problem — problem.statement >= 10 chars",
      "scenarios — at least 1 scenario with title and description",
      "role — role.name and role.persona defined",
      "intents — at least 1 intent with description and examples",
      "tools — at least 1 tool with name, description, and output",
      "policy — guardrails section with at least 1 never or always rule",
      "engine — model and temperature set",
      "mocks_tested — all tools have mock_status != \"untested\""
    ]
  },
  "solution_workflow": {
    "description": "State machine for building a multi-skill A-Team solution (the architecture layer that connects skills)",
    "note": "Build each skill individually first using the skill workflow. Then use this workflow to define how skills work together.",
    "phases": [
      {
        "id": "SOLUTION_DISCOVERY",
        "order": 1,
        "label": "Solution Discovery",
        "goal": "Understand the overall solution shape",
        "what_to_ask": [
          "What problem does this multi-agent solution solve?",
          "How many skills/agents will it need?",
          "What types of users interact with it? (customers, admins, operators)",
          "What channels does it serve? (chat, email, API, scheduled tasks)",
          "Is there an identity/security gateway?"
        ],
        "what_to_build": {
          "id": "Solution identifier",
          "name": "Solution display name",
          "description": "What this multi-agent solution does"
        },
        "exit_criteria": "Basic solution shape is understood — name, description, rough skill count",
        "tips": [
          "Suggest a solution pattern based on the domain (e-commerce, helpdesk, HR)",
          "Most solutions have: gateway (identity) → worker(s) → optional approval skill"
        ]
      },
      {
        "id": "IDENTITY_DESIGN",
        "order": 2,
        "label": "Identity Design (Users & Roles)",
        "goal": "Define who uses this solution — user types, admin roles, defaults",
        "what_to_ask": [
          "What types of users will interact with this solution?",
          "Which roles should have admin privileges?",
          "What is the default user type for unknown/anonymous users?"
        ],
        "what_to_build": {
          "identity.actor_types[]": "Each with key, label, description",
          "identity.admin_roles": "Keys that get admin access",
          "identity.default_actor_type": "Default for new users"
        },
        "exit_criteria": "At least 2 actor types, admin_roles set, default_actor_type set",
        "tips": [
          "Always suggest concrete user types with examples — don't just ask",
          "Common patterns: customer + admin, patient + doctor, employee + manager"
        ]
      },
      {
        "id": "SKILL_TOPOLOGY",
        "order": 3,
        "label": "Skill Topology",
        "goal": "Define each skill with its role in the solution",
        "what_to_ask": [
          "Which skill handles the entry point (gateway)?",
          "Which skills do the actual work (workers)?",
          "Do you need an orchestrator or approval skill?"
        ],
        "what_to_build": {
          "skills[]": "Each with id, name, role (gateway/worker/orchestrator/approval), description, entry_channels, connectors"
        },
        "exit_criteria": "At least 2 skills defined with roles",
        "tips": [
          "Roles: gateway = entry point + identity, worker = does the work, orchestrator = coordinates, approval = human-in-the-loop",
          "Each skill should have a clear, single responsibility"
        ]
      },
      {
        "id": "GRANT_ECONOMY",
        "order": 4,
        "label": "Grant Economy",
        "goal": "Define the verified claims that flow between skills",
        "what_to_ask": [
          "What information does each worker skill need from the gateway?",
          "What verified claims should flow between skills? (e.g., customer_id, assurance_level)",
          "How are grants issued? (from tool responses via grant_mapping, or via handoff)"
        ],
        "what_to_build": {
          "grants[]": "Each with key, description, issued_by, consumed_by, issued_via, source_tool, source_field"
        },
        "exit_criteria": "At least 1 grant defined",
        "tips": [
          "Grants are the security backbone — they ensure skills only act on verified data",
          "Common grants: customer_id, assurance_level, employee_id, session_token",
          "Use namespace.name format (e.g., ecom.customer_id)"
        ]
      },
      {
        "id": "HANDOFF_DESIGN",
        "order": 5,
        "label": "Handoff Design",
        "goal": "Define how conversations transfer between skills and which grants propagate",
        "what_to_ask": [
          "When should the gateway hand off to a worker? What triggers it?",
          "Which grants should be passed to the target skill?",
          "Should any grants be revoked after handoff?"
        ],
        "what_to_build": {
          "handoffs[]": "Each with id, from, to, trigger, grants_passed, grants_dropped, mechanism"
        },
        "exit_criteria": "All inter-skill flows have handoff definitions",
        "tips": [
          "handoff-controller-mcp = live conversation transfer — the platform provides sys.handoffToSkill as a built-in tool, no connector wiring needed",
          "internal-message = async skill-to-skill (background coordination)",
          "Every handoff needs a unique id",
          "Add sys.handoffToSkill to the skill's bootstrap_tools to pin it for the planner — ensures the LLM always sees and can use it"
        ]
      },
      {
        "id": "ROUTING_CONFIG",
        "order": 6,
        "label": "Routing Configuration",
        "goal": "Map each channel to its default entry skill",
        "what_to_ask": [
          "Which skill should answer Telegram messages?",
          "Which skill should handle emails?",
          "Which skill handles API/webhook calls?"
        ],
        "what_to_build": {
          "routing": "Object mapping channel names to { default_skill, description }"
        },
        "exit_criteria": "All declared channels have routing configured",
        "tips": [
          "Usually all channels route to the gateway skill first",
          "API/webhook channels might go directly to an orchestrator"
        ]
      },
      {
        "id": "SECURITY_CONTRACTS",
        "order": 7,
        "label": "Security Contracts",
        "goal": "Define cross-skill grant requirements for tools",
        "what_to_ask": [
          "Which tools in worker skills need verified grants before they can run?",
          "Which gateway skill provides those grants?",
          "What grant values are required? (e.g., assurance_level must be L1 or L2)"
        ],
        "what_to_build": {
          "security_contracts[]": "Each with name, consumer, provider, requires_grants, required_values, for_tools, validation"
        },
        "exit_criteria": "At least 1 security contract for the main consumer skill",
        "tips": [
          "Security contracts formalize the grant economy into enforceable rules",
          "consumer = skill being constrained, provider = skill that issues the grants"
        ]
      },
      {
        "id": "UI_PLUGINS",
        "order": 8,
        "label": "UI Plugins (Consumer Surfaces)",
        "goal": "For each interactive UI plugin, decide WHERE it renders in the host shell and WHO can summon it. Skip this phase entirely if the solution has no UI plugins.",
        "when_to_run": "Only when the solution declares one or more ui_plugins (either at solution-level or any skill-level). Skip otherwise.",
        "what_to_ask_per_plugin": [
          "Where should this render? Suggest one of: drawer (slide-up panel — default for \"show me my X\"), fullscreen (edge-to-edge takeover — for voice mode, capture flows, auth), card (inline tile — for dashboards / empty-state suggestions), header (small ambient pill in the top bar — for status indicators like battery/weather/DND), ambient (always-visible mini widget), nudge (proactive notification card injected into chat — e.g. \"Sarah replied — draft a response?\").",
          "Who can open it? — user (default; user taps an icon, slash command, or menu), always (mounted persistently — only sensible for header / ambient), or engine (HIDDEN; only mounted when a skill emits sys.focusUiPlugin / sys.showSurface — typical for camera capture, auth flows, one-shot result panels).",
          "Optional: does this handle sensitive data? (privacy: true → host shows a lock pill in the surface chrome).",
          "Optional: pick an emoji icon (e.g. 🥗, 📅, 💬) and a one-line title shown in the surface chrome."
        ],
        "what_to_build": {
          "ui_plugins[].surface.type": "Required when surface block is present. One of: drawer | fullscreen | card | header | ambient | nudge.",
          "ui_plugins[].surface.visibility": "Optional, defaults to \"user\". One of: always | user | engine.",
          "ui_plugins[].surface.icon": "Optional emoji or short symbol (e.g. \"🥗\").",
          "ui_plugins[].surface.title": "Optional title (defaults to plugin.name).",
          "ui_plugins[].surface.subtitle": "Optional secondary text.",
          "ui_plugins[].surface.privacy": "Optional boolean — show a lock pill in the chrome."
        },
        "suggested_defaults_by_intent": {
          "Show me my X / a dashboard": "{ type: \"drawer\", visibility: \"user\" }",
          "Camera / photo capture / scan flow": "{ type: \"fullscreen\", visibility: \"engine\" }",
          "OAuth sign-in / login / connect-account": "{ type: \"fullscreen\", visibility: \"engine\" }",
          "Voice mode / fullscreen takeover": "{ type: \"fullscreen\", visibility: \"engine\" }",
          "Status indicator (battery, weather, DND)": "{ type: \"header\", visibility: \"always\", render.mode: \"react-native\" }",
          "Persistent mini widget (e.g. now-playing)": "{ type: \"ambient\", visibility: \"always\", render.mode: \"react-native\" }",
          "Proactive chat-injected card (\"Sarah replied — draft?\")": "{ type: \"nudge\", visibility: \"engine\" }",
          "One-shot result panel after a skill action": "{ type: \"drawer\", visibility: \"engine\" }"
        },
        "composite_rules": [
          "header / ambient with render.mode \"iframe\" → validator warns. These surfaces are tiny + always-mounted; iframe overhead breaks the UX. Prefer render.mode \"react-native\" or \"adaptive\".",
          "visibility \"engine\" + featured:true → validator errors. Engine-only plugins are not user-discoverable by definition.",
          "No surface block at all → plugin renders in the legacy plugin-tab grid (full back-compat — no error)."
        ],
        "engine_contract": "When a skill response wants to PROACTIVELY surface a plugin (e.g. open the camera, show a result drawer), it emits sys.focusUiPlugin(pluginId, props?) — the host activates the surface declared in the manifest. This works regardless of visibility, including engine-only plugins. The skill decides per response which \"tier\" it uses: chat text (default) → block message (future) → dedicated surface (sys.focusUiPlugin).",
        "exit_criteria": "Every ui_plugins[] entry has either no surface field (legacy tab placement, OK) or a surface.type from the enum.",
        "tips": [
          "Always SUGGEST the surface — don't make the user remember the enum. Use the suggested_defaults_by_intent table.",
          "If the user describes a \"show me X\" experience → drawer + user is almost always right.",
          "If the user describes anything full-screen (camera, voice, auth) → fullscreen + engine.",
          "If they don't care, default to drawer + user — it's the safest fallback.",
          "Skip this phase entirely if there are no ui_plugins. Don't invent surfaces."
        ],
        "spec_reference": "GET /spec/solution → schema.ui_plugins.item_schema.surface (full schema + examples). Reference impl: ateam-mobile/src/plugin-sdk/surfaces/."
      },
      {
        "id": "VALIDATION",
        "order": 9,
        "label": "Validation",
        "goal": "Run validation and fix any issues",
        "what_to_do": [
          "Run adas_validate_solution with the full solution + all skill definitions",
          "Fix any errors (warnings are OK)",
          "Review the complete solution architecture"
        ],
        "exit_criteria": "No validation errors",
        "tips": [
          "Common errors: orphan skills, broken handoff chains, missing grant providers",
          "After validation passes, deploy with adas_deploy_solution"
        ]
      }
    ]
  },
  "github_workflow": {
    "description": "GitHub-first development workflow — how to iterate on solutions after the first deploy",
    "repo_structure": {
      "_note": "Every solution gets a GitHub repo named {tenant}--{solution-id} (e.g., main--ecommerce-solution)",
      "layout": {
        "solution.json": "Full solution definition (identity, grants, handoffs, routing)",
        "skills/{skill-id}/skill.json": "Individual skill definitions",
        "connectors/{connector-id}/server.js": "Connector MCP server source code",
        "connectors/{connector-id}/package.json": "Connector dependencies",
        "connectors/{connector-id}/ui-dist/": "UI plugin assets (if ui_capable)"
      }
    },
    "first_deploy": {
      "description": "The first deploy creates the GitHub repo automatically",
      "steps": [
        "1. Build solution + skills + connector code",
        "2. Call ateam_build_and_run with mcp_store containing connector source files",
        "3. On success, the solution is auto-pushed to GitHub (Phase 5 of deploy)",
        "4. The repo is now the source of truth for connector code"
      ]
    },
    "iteration_loop": {
      "description": "After the first deploy, iterate using GitHub as the code source",
      "code_changes": [
        "1. Edit connector code: ateam_github_patch({ files: [{ path: \"connectors/my-mcp/server.js\", content: \"...\" }] })",
        "2. Redeploy: ateam_build_and_run({ solution, skills, github: true })",
        "3. Test: ateam_test_skill({ message: \"...\" })"
      ],
      "definition_changes": [
        "1. Edit skill/solution definitions: ateam_patch({ target: \"skill\", updates: { ... } })",
        "2. No GitHub needed — definitions live in the Builder, not in connector code"
      ]
    },
    "tools_reference": {
      "ateam_github_patch": "Edit connector files in GitHub repo. Use for server.js, package.json, UI assets.",
      "ateam_github_status": "Check if repo exists and get latest commit info.",
      "ateam_github_log": "View commit history to track changes.",
      "ateam_github_read": "Read a specific file from the repo (e.g., to review current connector code before editing).",
      "ateam_github_promote": "Merge dev → main and auto-tag prod-YYYY-MM-DD-NNN. Optional flags: dry_run (preview without merging), label (annotate the tag), skip_tag.",
      "ateam_github_list_versions": "List all available prod-* and legacy safe-* checkpoints with dates and commit SHAs. See version history before rolling back.",
      "ateam_github_rollback": "Rollback main to a previous prod-* tag, safe-* tag, or commit SHA. ADDITIVE — creates a new commit on top of main; history is preserved.",
      "ateam_build_and_run(github:true)": "Deploy pulling connector code from GitHub instead of inline mcp_store."
    }
  },
  "conversation_guide": {
    "description": "How to drive the build conversation as a hosting AI",
    "principles": [
      "Follow the phase order — don't skip ahead",
      "At each phase, suggest concrete examples based on the domain — never ask bare questions",
      "Build the definition incrementally — add to it after each user response",
      "Validate frequently — run adas_validate_skill or adas_validate_solution after major changes",
      "One topic at a time — complete one phase before moving to the next",
      "Show progress — tell the user which phase they're in and what's next"
    ],
    "opening_message": "When the user wants to build something, start with: \"I'm an A-Team Solution Architect. I'll help you design and deploy a multi-agent AI system. Let's start — what problem do you want to solve?\"",
    "tools_to_use": {
      "adas_get_spec": "Fetch the full schema when you need field details",
      "adas_get_examples": "Show the user a working example for reference",
      "adas_validate_skill": "Validate after completing tools, policy, and engine phases",
      "adas_validate_solution": "Validate the full solution before deploy",
      "adas_deploy_solution": "Deploy when validation passes",
      "adas_get_solution": "Verify deployment health after deploy"
    }
  }
}

Working examples

ateam_get_examples(skill)
{
  "_note": "This is the comprehensive example (every field shown) — useful as a REFERENCE when overriding platform defaults. For most new skills you do NOT need most of these blocks. See GET /spec/skill → auto_expand for what the platform fills in automatically.",
  "_strip_summary": {
    "author_writes": [
      "id, name, version, description",
      "role.persona (the agent's instructions in prose)",
      "connectors[] (list of MCP connector ids this skill uses)",
      "handoff_when (one-sentence routing trigger — OPTIONAL, the platform LLM-synthesizes one if omitted)",
      "policy.guardrails (optional always/never rules)"
    ],
    "platform_generates_at_deploy": [
      "tools[] (Phase 2b — fetched from each connector's live /api/connectors/:id/tools)",
      "ui_plugins[] (Phase 5 — MCP introspection via ui.listPlugins + ui.getPlugin)",
      "intents (Phase 3 — LLM-synthesized from tools + persona)",
      "scenarios, role.goals/limitations/communication_style (auto)",
      "engine (Phase 4 — resolved from skill.engine string or default preset)",
      "security classifications on every tool (Phase 2)",
      "access_policy defaults"
    ],
    "see_also": "GET /spec/skill — auto_expand block has the full list + typical_minimal_skill"
  },
  "id": "order-support",
  "name": "Order Support Agent",
  "description": "Handles customer order inquiries — status checks, order lookups, and issue escalation for an e-commerce platform.",
  "version": "1.0.0",
  "phase": "TOOL_DEFINITION",
  "connectors": [
    "orders-mcp",
    "telegram-mcp"
  ],
  "problem": {
    "statement": "Customers need quick, accurate answers about their orders without waiting for a human support agent.",
    "context": "E-commerce platform processing 10,000+ daily orders across multiple fulfillment centers.",
    "goals": [
      "Resolve order status inquiries in under 30 seconds",
      "Reduce human support queue by handling routine order questions",
      "Escalate complex issues (refunds, disputes) to human agents"
    ]
  },
  "scenarios": [
    {
      "id": "check-order-status",
      "title": "Customer checks order status",
      "description": "A customer provides their order ID and wants to know where their package is.",
      "steps": [
        "Customer asks \"Where is my order #12345?\"",
        "Agent extracts order_id from message",
        "Agent calls orders.order.get to fetch order details",
        "Agent responds with order status, tracking info, and estimated delivery"
      ],
      "expected_outcome": "Customer receives current order status with tracking link."
    },
    {
      "id": "search-by-email",
      "title": "Customer searches orders by email",
      "description": "A customer wants to find their recent orders using their email address.",
      "steps": [
        "Customer says \"I need to find my recent orders, my email is jane@example.com\"",
        "Agent extracts email from message",
        "Agent calls orders.order.search to find matching orders",
        "Agent presents a summary list of matching orders"
      ],
      "expected_outcome": "Customer sees a list of their recent orders with status for each."
    },
    {
      "id": "escalate-refund",
      "title": "Customer requests a refund",
      "description": "A customer wants a refund — agent cannot process refunds, must escalate.",
      "steps": [
        "Customer says \"I want a refund for order #12345\"",
        "Agent looks up the order to verify it exists",
        "Agent explains that refund requests require human review",
        "Agent escalates to the returns support queue"
      ],
      "expected_outcome": "Customer is informed and the case is escalated to a human agent."
    }
  ],
  "role": {
    "name": "Order Support Specialist",
    "persona": "You are a helpful, efficient order support specialist for an e-commerce platform. You quickly resolve order inquiries using available tools. You are transparent about what you can and cannot do — if a request requires human intervention (refunds, disputes, address changes), you escalate promptly rather than making promises you cannot keep.",
    "goals": [
      "Answer order status questions accurately and quickly",
      "Help customers find their orders by ID or email",
      "Escalate complex issues to human agents when appropriate"
    ],
    "limitations": [
      "Cannot process refunds or issue credits",
      "Cannot modify order details (address, items, payment)",
      "Cannot access payment or billing information"
    ],
    "communication_style": {
      "tone": "casual",
      "verbosity": "concise"
    }
  },
  "intents": {
    "supported": [
      {
        "id": "check_order_status",
        "description": "Customer wants to know the status of a specific order",
        "examples": [
          "Where is my order?",
          "What is the status of order #12345?",
          "Track my package",
          "Has my order shipped yet?",
          "When will my order arrive?"
        ],
        "maps_to_workflow": "order_lookup_flow",
        "entities": [
          {
            "name": "order_id",
            "type": "string",
            "required": false,
            "extract_from": "message"
          }
        ]
      },
      {
        "id": "search_orders",
        "description": "Customer wants to find orders by email or other criteria",
        "examples": [
          "Find my orders",
          "I need to look up my recent orders",
          "Can you search for orders under jane@example.com?",
          "Show me all my orders from last month"
        ],
        "maps_to_workflow": "order_search_flow",
        "entities": [
          {
            "name": "email",
            "type": "string",
            "required": false,
            "extract_from": "message"
          }
        ]
      },
      {
        "id": "request_refund",
        "description": "Customer wants a refund or return — must be escalated",
        "examples": [
          "I want a refund",
          "Can I return this item?",
          "I need my money back for order #12345",
          "This product is defective, I want a refund"
        ]
      }
    ],
    "thresholds": {
      "accept": 0.85,
      "clarify": 0.6,
      "reject": 0.4
    },
    "out_of_domain": {
      "action": "redirect",
      "message": "I can only help with order inquiries. Let me connect you with the right team.",
      "suggest_domains": []
    }
  },
  "tools": [
    {
      "id": "tool-orders-get",
      "id_status": "permanent",
      "name": "orders.order.get",
      "description": "Retrieve full order details by order ID, including status, items, shipping info, and tracking.",
      "inputs": [
        {
          "name": "order_id",
          "type": "string",
          "required": true,
          "description": "The order ID to look up (e.g., \"ORD-12345\")"
        }
      ],
      "output": {
        "type": "object",
        "description": "Complete order object with status, line_items, shipping_address, tracking, and timestamps"
      },
      "source": {
        "type": "mcp_bridge",
        "connection_id": "orders-mcp",
        "mcp_tool": "order.get"
      },
      "policy": {
        "allowed": "always"
      },
      "mock": {
        "enabled": true,
        "mode": "examples",
        "examples": [
          {
            "id": "shipped-order",
            "input": {
              "order_id": "ORD-12345"
            },
            "output": {
              "order_id": "ORD-12345",
              "status": "shipped",
              "customer_email": "jane@example.com",
              "items": [
                {
                  "name": "Wireless Mouse",
                  "qty": 1,
                  "price": 29.99
                }
              ],
              "tracking": {
                "carrier": "UPS",
                "tracking_number": "1Z999AA10123456784",
                "url": "https://ups.com/track?num=1Z999AA10123456784"
              },
              "created_at": "2026-02-10T14:30:00Z",
              "estimated_delivery": "2026-02-14"
            },
            "description": "A shipped order with tracking info"
          }
        ]
      },
      "security": {
        "classification": "pii_read"
      }
    },
    {
      "id": "tool-orders-search",
      "id_status": "permanent",
      "name": "orders.order.search",
      "description": "Search orders by customer email, date range, or status. Returns a paginated list of order summaries.",
      "inputs": [
        {
          "name": "email",
          "type": "string",
          "required": false,
          "description": "Customer email to filter by"
        },
        {
          "name": "status",
          "type": "string",
          "required": false,
          "description": "Order status filter (pending, shipped, delivered, cancelled)"
        },
        {
          "name": "limit",
          "type": "number",
          "required": false,
          "description": "Max results to return (default: 10)"
        }
      ],
      "output": {
        "type": "object",
        "description": "Paginated list of order summaries with order_id, status, total, and created_at"
      },
      "source": {
        "type": "mcp_bridge",
        "connection_id": "orders-mcp",
        "mcp_tool": "order.search"
      },
      "policy": {
        "allowed": "always"
      },
      "mock": {
        "enabled": true,
        "mode": "examples",
        "examples": [
          {
            "id": "search-by-email",
            "input": {
              "email": "jane@example.com"
            },
            "output": {
              "orders": [
                {
                  "order_id": "ORD-12345",
                  "status": "shipped",
                  "total": 29.99,
                  "created_at": "2026-02-10T14:30:00Z"
                },
                {
                  "order_id": "ORD-12300",
                  "status": "delivered",
                  "total": 59.98,
                  "created_at": "2026-02-05T09:15:00Z"
                }
              ],
              "total_count": 2
            },
            "description": "Search results for a customer email"
          }
        ]
      },
      "security": {
        "classification": "pii_read"
      }
    },
    {
      "id": "tool-customers-lookup",
      "id_status": "permanent",
      "name": "orders.customer.lookup",
      "description": "Look up customer profile by email. Returns verified customer ID and basic profile.",
      "inputs": [
        {
          "name": "email",
          "type": "string",
          "required": true,
          "description": "Customer email address"
        }
      ],
      "output": {
        "type": "object",
        "description": "Customer profile with customer_id, name, email, and account_status"
      },
      "source": {
        "type": "mcp_bridge",
        "connection_id": "orders-mcp",
        "mcp_tool": "customer.lookup"
      },
      "policy": {
        "allowed": "always"
      },
      "mock": {
        "enabled": true,
        "mode": "examples",
        "examples": [
          {
            "id": "found-customer",
            "input": {
              "email": "jane@example.com"
            },
            "output": {
              "customer_id": "CUST-9876",
              "name": "Jane Doe",
              "email": "jane@example.com",
              "account_status": "active"
            },
            "description": "Customer found by email"
          }
        ]
      },
      "security": {
        "classification": "pii_read"
      }
    },
    {
      "id": "tool-telegram-send",
      "id_status": "permanent",
      "name": "telegram.send_message",
      "description": "Send a Telegram message for proactive notifications. Use during trigger-driven execution for important updates (stale orders, daily summaries). Do NOT use during normal user conversations — replies are handled automatically by the platform.",
      "inputs": [
        {
          "name": "chat_id",
          "type": "number",
          "required": true,
          "description": "Telegram chat ID of the recipient"
        },
        {
          "name": "text",
          "type": "string",
          "required": true,
          "description": "Message text (supports Markdown formatting)"
        },
        {
          "name": "parse_mode",
          "type": "string",
          "required": false,
          "description": "Message format: \"Markdown\" or \"HTML\" (default: plain text)"
        }
      ],
      "output": {
        "type": "object",
        "description": "Telegram API response with ok status and message_id"
      },
      "source": {
        "type": "mcp_bridge",
        "connection_id": "telegram-mcp",
        "mcp_tool": "send_message"
      },
      "policy": {
        "allowed": "always"
      },
      "security": {
        "classification": "public"
      }
    }
  ],
  "policy": {
    "guardrails": {
      "never": [
        "Process refunds or issue credits directly",
        "Share one customer's order information with another customer",
        "Reveal internal system IDs, database details, or API endpoints",
        "Make delivery promises beyond what tracking data shows"
      ],
      "always": [
        "Verify order details before sharing with the customer",
        "Offer to escalate if the request is beyond your capabilities",
        "Include tracking links when available"
      ]
    },
    "workflows": [
      {
        "id": "order_lookup_flow",
        "name": "Order Lookup",
        "description": "Look up a specific order by ID and present details to customer",
        "trigger": "check_order_status",
        "steps": [
          "orders.order.get"
        ],
        "required": true,
        "on_deviation": "warn"
      },
      {
        "id": "order_search_flow",
        "name": "Order Search",
        "description": "Search for customer orders by email and present results",
        "trigger": "search_orders",
        "steps": [
          "orders.customer.lookup",
          "orders.order.search"
        ],
        "required": false,
        "on_deviation": "warn"
      }
    ],
    "approvals": [],
    "escalation": {
      "enabled": true,
      "conditions": [
        "Customer requests refund",
        "Customer reports missing package after delivery"
      ],
      "target": "returns-support"
    }
  },
  "engine": {
    "model": "claude-sonnet-4-20250514",
    "temperature": 0.3,
    "rv2": {
      "max_iterations": 8,
      "iteration_timeout_ms": 30000,
      "allow_parallel_tools": false
    },
    "hlr": {
      "enabled": true,
      "critic": {
        "enabled": false,
        "strictness": "medium"
      },
      "reflection": {
        "enabled": false,
        "depth": "shallow"
      }
    },
    "autonomy": {
      "level": "autonomous"
    }
  },
  "grant_mappings": [
    {
      "tool": "orders.customer.lookup",
      "on_success": true,
      "grants": [
        {
          "key": "ecom.customer_id",
          "value_from": "$.customer_id",
          "condition": "$.account_status == \"active\"",
          "ttl_seconds": 3600
        }
      ]
    }
  ],
  "access_policy": {
    "rules": [
      {
        "tools": [
          "*"
        ],
        "effect": "allow"
      }
    ]
  },
  "response_filters": [
    {
      "id": "strip-sensitive-fields",
      "strip_fields": [
        "customer.payment_methods",
        "customer.ssn"
      ],
      "mask_fields": [
        "customer.email"
      ]
    }
  ],
  "triggers": [
    {
      "id": "check-stale-orders",
      "type": "schedule",
      "enabled": true,
      "concurrency": 1,
      "prompt": "Check for orders that may need attention:\n\n1. Call orders.order.search with status=\"pending\" to find pending orders\n2. For each order older than 2 hours: send a Telegram notification to chat_id 12345678 with the order ID, customer email, and how long it has been pending\n3. Use Markdown parse_mode for Telegram messages. Keep messages concise — 2-3 lines per order.\n4. If no stale orders found, do nothing — do NOT send a \"no issues\" message.",
      "every": "PT15M"
    },
    {
      "id": "daily-order-summary",
      "type": "schedule",
      "enabled": true,
      "concurrency": 1,
      "prompt": "Generate a daily summary of order activity:\n\n1. Call orders.order.search for each status (pending, shipped, delivered, cancelled) to get counts\n2. Send ONE Telegram message to chat_id 12345678 with the summary: total orders by status, any notable issues\n3. Use Markdown parse_mode. Format as a clean summary with emoji status indicators.\n4. Keep it brief — max 10 lines.",
      "every": "P1D"
    }
  ]
}

Connector example

ateam_get_examples(connector)
{
  "_note": "A standard stdio MCP connector. This is what A-Team Core manages as a child process.",
  "_entry_point_resolution": {
    "_note": "command and args are OPTIONAL when deploying with mcp_store. The system auto-detects the entry point from uploaded files.",
    "auto_detection_priority": [
      "1. package.json \"main\" field",
      "2. server.js",
      "3. index.js",
      "4. server.py → python3",
      "5. main.py → python3",
      "6. server.ts → npx tsx"
    ],
    "explicit_override": "You can always provide command + args to override auto-detection."
  },
  "_storage": {
    "_note": "A-Team Core automatically provides a DATA_DIR environment variable to every stdio connector. Use it to store persistent data (SQLite databases, files, etc.).",
    "how_to_use": "const DATA_DIR = process.env.DATA_DIR || \"./data\"; // A-Team Core auto-sets DATA_DIR per tenant+connector",
    "resolves_to": "/tenants/<tenant>/connector-data/<connector-id>/",
    "isolation": "Each connector gets its own directory. Data is tenant-scoped and persisted across restarts."
  },
  "_multi_user_data_isolation": {
    "_note": "CRITICAL: A-Team Core is multi-user. Multiple users share the same tenant. If your connector stores or retrieves per-user data, you MUST scope it by actorId. Without this, User A sees User B's data — a security violation.",
    "how_it_works": {
      "_step_1": "A-Team Core passes two headers on EVERY tool call to your connector:",
      "headers": {
        "X-ADAS-TENANT": "Tenant ID — identifies the organization/workspace. Always present.",
        "X-ADAS-ACTOR": "Actor ID — identifies the specific user making the request. Present when user is authenticated (which is almost always)."
      },
      "_step_2": "Your connector reads these headers and uses them as keys for data storage/retrieval.",
      "_step_3": "Data queries MUST include actorId in the filter/key so users only see their own data."
    },
    "when_you_need_this": [
      "Your connector stores per-user preferences, history, or state",
      "Your connector reads data from an external API scoped to a user (email, calendar, CRM contacts)",
      "Your connector has any notion of \"my data\" vs \"all data\"",
      "Your connector talks to a database, relay, or external service that holds user-specific records"
    ],
    "when_you_do_NOT_need_this": [
      "Your connector is stateless (e.g., a calculator, a weather API)",
      "Your connector only reads shared/global data (e.g., product catalog, company policies)",
      "All users in a tenant should see exactly the same data"
    ],
    "stdio_pattern": {
      "_note": "A-Team Core injects the caller identity onto EVERY tool call as _adas_* args. Read the actor per-call from args — NOT from env (process.env.ADAS_ACTOR_ID is NEVER set; only ADAS_TENANT is a stable process-level env).",
      "reading_context": "// How to get tenant and actor in a stdio connector — per tool call:\nfunction getScope(args) {\n  const tenant = args?._adas_tenant || process.env.ADAS_TENANT;\n  const actorId = args?._adas_actor;\n  if (!actorId) {\n    // NEVER fall back to a hardcoded id ('default', 'main', ...) — that\n    // silently pools every user's data into one shared bucket.\n    throw new Error(\"actor context missing — call is not actor-scoped\");\n  }\n  return { tenant, actorId };\n}",
      "storage_example_actorstore": "// ═══ PREFERRED — actorStore (per-actor SQL, scoped by construction) ═══\n// The platform's actorstore-mcp gives every (tenant, actor, skill) its OWN\n// private SQLite db. You run raw SQL; the actor is injected by Core — your\n// SQL physically CANNOT reach another user's data. No actor_id column, no\n// WHERE filter to forget. Call actorStore.help for the full API.\n\n// Per-user data (default — scoped to the calling user):\nawait coreCall(\"actorStore.exec\", {\n  sql: \"CREATE TABLE IF NOT EXISTS prefs(key TEXT PRIMARY KEY, value TEXT)\",\n});\nawait coreCall(\"actorStore.exec\", {\n  sql: \"INSERT INTO prefs(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value\",\n  params: [\"theme\", \"dark\"],\n});\n\n// Deliberately SHARED data (one store for ALL users in the tenant —\n// household devices, team board, org KB). Explicit opt-in:\nawait coreCall(\"actorStore.exec\", {\n  scope: \"tenant\",\n  sql: \"INSERT INTO house_devices(id, name) VALUES(?, ?)\",\n  params: [deviceId, name],\n});",
      "storage_example_sqlite": "// ═══ Self-managed SQLite (only if actorStore doesn't fit) — scope by actorId ═══\nimport Database from \"better-sqlite3\";\n\nconst db = new Database(path.join(DATA_DIR, \"connector.db\"));\n\n// Create table with actorId column\ndb.exec(`CREATE TABLE IF NOT EXISTS user_preferences (\n  actor_id TEXT NOT NULL,\n  key TEXT NOT NULL,\n  value TEXT,\n  updated_at TEXT DEFAULT CURRENT_TIMESTAMP,\n  PRIMARY KEY (actor_id, key)\n)`);\n\n// ✅ CORRECT — always filter by actorId\nfunction getPreference(actorId, key) {\n  return db.prepare(\"SELECT value FROM user_preferences WHERE actor_id = ? AND key = ?\")\n    .get(actorId, key);\n}\n\n// ❌ WRONG — returns ALL users' data (security violation!)\nfunction getPreferenceBroken(key) {\n  return db.prepare(\"SELECT value FROM user_preferences WHERE key = ?\").get(key);\n}",
      "storage_example_json": "// ═══ JSON file — scope data by actorId ═══\nimport { readFileSync, writeFileSync, mkdirSync } from \"fs\";\nimport path from \"path\";\n\nconst DATA_DIR = process.env.DATA_DIR || \"./data\";\n\n// Each actor gets their own directory\nfunction actorDir(actorId) {\n  const dir = path.join(DATA_DIR, \"actors\", actorId);\n  mkdirSync(dir, { recursive: true });\n  return dir;\n}\n\n// ✅ CORRECT — each user's data is physically separated\nfunction saveUserData(actorId, data) {\n  writeFileSync(path.join(actorDir(actorId), \"data.json\"), JSON.stringify(data));\n}\n\nfunction loadUserData(actorId) {\n  try {\n    return JSON.parse(readFileSync(path.join(actorDir(actorId), \"data.json\"), \"utf8\"));\n  } catch { return null; }\n}",
      "storage_example_external_api": "// ═══ External API — pass actorId to scope queries ═══\n\n// If your connector talks to an external service (relay, database, SaaS API),\n// pass actorId as a query param, header, or body field.\n\nasync function fetchUserCalendar(actorId) {\n  const res = await fetch(\n    `${RELAY_URL}/api/device/${deviceId}/data/calendar?actorId=${encodeURIComponent(actorId)}`,\n    { headers: { \"X-ADAS-ACTOR\": actorId } }\n  );\n  return res.json(); // Returns ONLY this user's calendar\n}\n\n// ❌ WRONG — no actorId = returns data for ALL users or wrong user\nasync function fetchCalendarBroken() {\n  const res = await fetch(`${RELAY_URL}/api/device/${deviceId}/data/calendar`);\n  return res.json(); // Whose calendar is this??\n}"
    },
    "http_pattern": {
      "_note": "HTTP connectors (Streamable HTTP transport) receive headers directly on every request.",
      "reading_headers": "// Express/Node.js HTTP connector:\napp.post(\"/mcp\", (req, res) => {\n  const tenant = req.headers[\"x-adas-tenant\"];   // Always present\n  const actorId = req.headers[\"x-adas-actor\"];   // Present when user is authenticated\n\n  // Use AsyncLocalStorage to propagate context through your handler chain:\n  requestContext.run({ tenant, actorId }, () => {\n    handleMcpRequest(req, res);\n  });\n});\n\n// In your tool handler, retrieve context:\nfunction getCurrentActorId() {\n  return requestContext.getStore()?.actorId || null;\n}"
    },
    "complete_example": {
      "_note": "A complete, working MCP connector that stores per-user todo lists. Copy this as a starting template for any multi-user connector.",
      "source": "import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\nimport { readFileSync, writeFileSync, mkdirSync } from \"fs\";\nimport path from \"path\";\n\nconst DATA_DIR = process.env.DATA_DIR || \"./data\";\nconst server = new McpServer({ name: \"todo-mcp\", version: \"1.0.0\" });\n\n// ── Per-actor storage ──\nfunction actorFile(actorId) {\n  const dir = path.join(DATA_DIR, \"actors\", actorId);\n  mkdirSync(dir, { recursive: true });\n  return path.join(dir, \"todos.json\");\n}\n\nfunction loadTodos(actorId) {\n  try { return JSON.parse(readFileSync(actorFile(actorId), \"utf8\")); }\n  catch { return []; }\n}\n\nfunction saveTodos(actorId, todos) {\n  writeFileSync(actorFile(actorId), JSON.stringify(todos, null, 2));\n}\n\n// ── Get current actor from environment ──\n// A-Team Core sets ADAS_ACTOR_ID on every tool call.\n// For Streamable HTTP transport, read from X-ADAS-ACTOR header instead.\nfunction getActorId() {\n  return process.env.ADAS_ACTOR_ID || \"default\";\n}\n\n// ── Tools ──\nserver.tool(\n  \"todo.list\",\n  \"List all todos for the current user\",\n  {},\n  async () => {\n    const actorId = getActorId();\n    const todos = loadTodos(actorId);\n    return {\n      content: [{ type: \"text\", text: JSON.stringify({ ok: true, actor: actorId, count: todos.length, todos }) }]\n    };\n  }\n);\n\nserver.tool(\n  \"todo.add\",\n  \"Add a todo for the current user\",\n  { title: z.string(), priority: z.enum([\"high\", \"medium\", \"low\"]).optional() },\n  async ({ title, priority }) => {\n    const actorId = getActorId();\n    const todos = loadTodos(actorId);\n    const todo = { id: Date.now().toString(36), title, priority: priority || \"medium\", done: false, created: new Date().toISOString() };\n    todos.push(todo);\n    saveTodos(actorId, todos);\n    return {\n      content: [{ type: \"text\", text: JSON.stringify({ ok: true, actor: actorId, todo }) }]\n    };\n  }\n);\n\nserver.tool(\n  \"todo.complete\",\n  \"Mark a todo as done for the current user\",\n  { id: z.string() },\n  async ({ id }) => {\n    const actorId = getActorId();\n    const todos = loadTodos(actorId);\n    const todo = todos.find(t => t.id === id);\n    if (!todo) return { content: [{ type: \"text\", text: JSON.stringify({ ok: false, error: \"Todo not found\" }) }], isError: true };\n    todo.done = true;\n    saveTodos(actorId, todos);\n    return {\n      content: [{ type: \"text\", text: JSON.stringify({ ok: true, actor: actorId, todo }) }]\n    };\n  }\n);\n\n// Start — A-Team Core manages the lifecycle\nconst transport = new StdioServerTransport();\nawait server.connect(transport);"
    },
    "rules": [
      "ALWAYS read X-ADAS-ACTOR header (HTTP) or ADAS_ACTOR_ID env (stdio) in tool handlers",
      "NEVER store user data without actorId as part of the key/path",
      "NEVER return data from one actor to another — always filter queries by actorId",
      "If actorId is missing, use \"default\" as fallback — but log a warning (it means auth was skipped)",
      "Tool schemas do NOT include actorId as a parameter — it flows transparently via headers, not via LLM args",
      "The LLM/planner NEVER decides which actor to query — the platform enforces it automatically",
      "Tenant isolation (X-ADAS-TENANT) and actor isolation (X-ADAS-ACTOR) are independent layers — respect both"
    ]
  },
  "_runtime_compatibility": {
    "_note": "A-Team Core runs connectors on Node.js 22.x with full ESM support. The @modelcontextprotocol/sdk works great.",
    "recommended_approach": "Use the official @modelcontextprotocol/sdk with StdioServerTransport. This is the simplest and most reliable approach.",
    "boilerplate": "import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\n\nconst server = new McpServer({\n  name: \"my-mcp\",\n  version: \"1.0.0\",\n});\n\nserver.tool(\"my.tool\", \"Does something\", { arg1: z.string() }, async ({ arg1 }) => {\n  return { content: [{ type: \"text\", text: JSON.stringify({ ok: true, arg1 }) }] };\n});\n\nconst transport = new StdioServerTransport();\nawait server.connect(transport);",
    "package_json_note": "Use \"type\": \"module\" in package.json when using ESM imports. List @modelcontextprotocol/sdk and zod as dependencies.",
    "alternative_approach": "You can also implement raw JSON-RPC over stdio without the SDK — useful if you want zero dependencies.",
    "raw_boilerplate": "import { createInterface } from \"readline\";\nconst rl = createInterface({ input: process.stdin });\n\nfunction send(msg) { process.stdout.write(JSON.stringify(msg) + \"\\n\"); }\nfunction result(id, data) { send({ jsonrpc: \"2.0\", id, result: data }); }\n\nconst TOOLS = [\n  { name: \"my.tool\", description: \"Does something\", inputSchema: { type: \"object\", properties: { arg1: { type: \"string\" } } } }\n];\n\nasync function handleCall(name, args) {\n  switch (name) {\n    case \"my.tool\": return { content: [{ type: \"text\", text: JSON.stringify({ ok: true }) }] };\n    default: throw new Error(\"Unknown tool: \" + name);\n  }\n}\n\nrl.on(\"line\", async (line) => {\n  const req = JSON.parse(line);\n  if (req.method === \"initialize\") return result(req.id, { protocolVersion: \"2024-11-05\", capabilities: { tools: {} }, serverInfo: { name: \"my-mcp\", version: \"1.0.0\" } });\n  if (req.method === \"tools/list\") return result(req.id, { tools: TOOLS });\n  if (req.method === \"tools/call\") {\n    try { return result(req.id, await handleCall(req.params.name, req.params.arguments || {})); }\n    catch (e) { return result(req.id, { content: [{ type: \"text\", text: JSON.stringify({ error: e.message }) }], isError: true }); }\n  }\n});"
  },
  "_common_mistakes": {
    "_note": "CRITICAL: These are the most common mistakes agents make when building connectors. Avoid them all.",
    "1_FATAL_web_server": {
      "mistake": "Starting an HTTP server (express, fastify, http.createServer) inside a stdio connector",
      "why_it_fails": "A-Team Core spawns your connector as a child process and communicates via stdin/stdout JSON-RPC. If your code calls app.listen(PORT) or http.createServer(), it will crash with EADDRINUSE because ADAS Core already uses those ports. Even if the port were free, ADAS Core cannot communicate with your connector over HTTP — it ONLY reads your stdout.",
      "wrong_code": "// ❌ WRONG — DO NOT DO THIS\nconst express = require(\"express\");\nconst app = express();\napp.post(\"/tools/call\", (req, res) => { ... });\napp.listen(4000); // CRASHES — port 4000 is ADAS Core's server",
      "correct_code": "// ✅ CORRECT — Use stdio transport\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nconst transport = new StdioServerTransport();\nawait server.connect(transport);",
      "deploy_check": "The deploy pipeline now detects express(), app.listen(), http.createServer() and BLOCKS deployment with an actionable error."
    },
    "2_FATAL_http_transport": {
      "mistake": "Using HttpServerTransport, SSEServerTransport, or StreamableHTTPServerTransport from the MCP SDK",
      "why_it_fails": "A-Team Core ONLY supports StdioServerTransport for connectors. HTTP-based MCP transports start a web server and bind to a port, which crashes in the A-Team runtime.",
      "wrong_code": "// ❌ WRONG\nimport { SSEServerTransport } from \"@modelcontextprotocol/sdk/server/sse.js\";",
      "correct_code": "// ✅ CORRECT\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";"
    },
    "3_WARNING_missing_package_json": {
      "mistake": "Uploading ESM code (.js files with import/export) without a package.json",
      "why_it_fails": "Without package.json, Node.js defaults to CommonJS mode. import/export syntax will fail with ERR_REQUIRE_ESM. Also, npm install cannot run, so dependencies are missing.",
      "fix": "Always include a package.json with \"type\": \"module\" (for ESM) and list all dependencies."
    },
    "4_WARNING_no_error_handling": {
      "mistake": "Not handling JSON-RPC errors in tool handlers",
      "why_it_fails": "Unhandled exceptions crash the process. ADAS Core will retry 5 times, then mark the connector as permanently failed.",
      "fix": "Wrap all tool handlers in try/catch and return MCP error responses: { content: [{ type: \"text\", text: JSON.stringify({ error: e.message }) }], isError: true }"
    },
    "5_WARNING_process_exit": {
      "mistake": "Calling process.exit() in connector code",
      "why_it_fails": "MCP servers must stay alive and continuously process messages from stdin. Calling process.exit() kills the connector.",
      "fix": "Never call process.exit(). Let the ADAS Core runtime manage the connector lifecycle."
    },
    "6_WARNING_console_log": {
      "mistake": "Using console.log() for debugging in a stdio connector",
      "why_it_fails": "console.log() writes to stdout, which is the JSON-RPC communication channel. Debug output mixed with JSON-RPC messages will corrupt the protocol.",
      "fix": "Use console.error() or process.stderr.write() for debug output. These go to stderr and are captured in connector diagnostics."
    }
  },
  "id": "orders-mcp",
  "name": "Orders MCP",
  "description": "E-commerce order management — CRUD operations for orders, customers, shipments, and returns tracking.",
  "transport": "stdio",
  "env": {
    "ORDERS_DB_URL": "postgresql://orders:secret@db:5432/orders",
    "NODE_ENV": "production"
  },
  "requiresAuth": true,
  "authInstructions": "You need a PostgreSQL connection string for the orders database.",
  "envRequired": [
    "ORDERS_DB_URL"
  ],
  "envHelp": {
    "ORDERS_DB_URL": {
      "label": "Database URL",
      "placeholder": "postgresql://user:pass@host:5432/dbname",
      "hint": "PostgreSQL connection string for the orders database"
    }
  },
  "category": "ecommerce",
  "layer": "tenant",
  "ui_capable": false,
  "_tools_reference": [
    {
      "name": "order.get",
      "description": "Get order by ID"
    },
    {
      "name": "order.search",
      "description": "Search orders by criteria"
    },
    {
      "name": "order.list",
      "description": "List orders with pagination"
    },
    {
      "name": "customer.lookup",
      "description": "Look up customer by email"
    },
    {
      "name": "customer.get",
      "description": "Get customer by ID"
    },
    {
      "name": "shipment.track",
      "description": "Get tracking info for a shipment"
    }
  ]
}

UI widget catalog

ateam_get_widget_catalog()
Authentication required — call ateam_auth first.

No authentication found.

Please ask the user to:
1. Get their API key at: https://mcp.ateam-ai.com/get-api-key
2. Then call: ateam_auth(api_key: "<their key>")

The key format is: adas_<tenant>_<32hex> — the tenant is auto-extracted.
This prevents accidental operations on the wrong tenant from pre-configured env vars.