voice-agent-factory — cortextOS agent -> ElevenLabs voice agent
Run this skill against a target agent and it produces a live, policy-gated voice
surface: a zero-dependency Node gateway exposing the agent's real capabilities as
ElevenLabs tools, a widget page, a generated test harness, provisioning scripts,
and an auditable discovery record.
ALL CODE IS WRITTEN DYNAMICALLY PER RUN. There is NO pre-built script library
or template to copy. The agent running this skill reads the bundled resources/
docs (verified ElevenLabs API surface), then writes every file — gateway, tools,
tests, provisioning calls — fresh, fit to the target agent's actual capabilities
as discovered. Two agents with different jobs get completely different tool
catalogs from the same skill.
Prerequisites
- A cortextOS install with the target agent onboarded (
<agentDir> = the agent's
workspace, e.g. orgs/<org>/agents/<name>/), and the cortextos CLI on PATH.
- Node >= 18 (the generated gateway and tests are zero-dependency
node: builtins).
- An ElevenLabs account +
ELEVENLABS_API_KEY in your org secrets file. Any paid
tier works; the skill tier-detects and picks the right provisioning path. Voice
minutes are billed by ElevenLabs — see resources/GAPS.md pricing table.
- For remote reach:
cloudflared (quick tunnel). Localhost-only runs skip it, but
then ElevenLabs webhook tools cannot reach the gateway — server-tools mode
REQUIRES a public HTTPS URL. A reverse proxy or other tunnel works equally well.
- A process manager (pm2 or equivalent) for the gateway. The quick tunnel itself
must NOT be process-managed (lesson 8).
- Browser page: the generated widget page loads the EL embed from unpkg.com at
runtime (the one external browser dependency). If you need a no-CDN deployment,
vendor
@elevenlabs/convai-widget-embed locally and point the script tag at it.
How to run
Tell the agent running this skill: the target agent name + org, the operator
contact, and (optionally) a voice id. Then follow the phases in order. Minimal
invocation: "Run voice-agent-factory against in ; I'm the operator."
What this writes / creates
- Files:
<agentDir>/voice/ (gateway, tools, tests, page, provisioning scripts,
.env chmod 600 + gitignored, voice-profile.json) and an append-only audit
log under the agent's docs. Nothing outside the target agent's workspace.
- Local: one listening port you choose (track one port per agent), a process-manager
entry
<agent>-voice-gateway, optionally a quick-tunnel process.
- External (ONLY after the Phase 5 gate): an ElevenLabs agent, one EL webhook tool
per gateway tool (or an MCP server attachment), and EL voice-minute usage.
Read resources/REPORT.md before any ElevenLabs API call: EL is mid-rename
(ConvAI -> ElevenAgents), doc URLs drift, and the API surface in REPORT is the
verified one. resources/GAPS.md has the server-tool webhook schema + pricing.
resources/SOURCES.md lists primary sources.
Non-negotiable lessons (each one was hit live during the reference builds)
- PROBE, never map from docs. Every CLI's JSON output shape MUST be captured
by running a real safe command before writing a field mapping. (An email tool
shipped with empty from/subject because the CLI nests
{headers:{from,...}, message:{snippet}}; a calendar create wraps its response as {event:{id}}
not {id}.)
- Verb-set completeness. For every NOUN a tool touches, generate the full
verb set — list/get/create/update/delete — or record an explicit exclusion
with a reason. (A build shipped calendar WRITE with no READ; the operator hit
the gap in his first minute of talking.)
- The agent states its limits truthfully — make the truth complete. The
prompt carries an auto-generated "I cannot X from voice" section built from
the exclusion list, each with a relay path (usually create_task to the real
agent).
- Tier-detect before MCP — and probe the POST, not the GET. Some tiers
return
convai_mcp_servers_disabled on POST /v1/convai/mcp-servers while
the GET still 200s. The POST is the gate; fall back to webhook server tools
(same gateway, /tool/<name> REST skin) without redesign.
- Compact returns. Voice latency stacks per tool round-trip. Returns carry
ids + summaries, bodies capped (
1500 chars), lists capped (8-15 items), and
an as_of UTC stamp on anything time-sensitive (the voice LLM does not
reliably know today's date).
- Timezone offsets are required in every time input (description-level
rule); the server parses RFC3339 and rejects unparseable times.
- Hard invariants live server-side, not in the prompt. Draft-only mail (no
send path in the code at all), pinned resource ids (not parameters),
append-only updates, recipient whitelists. Verbal confirmation is a persona
rule; safety is code.
- Tunnel URL is load-bearing. EL webhook tool URLs embed the tunnel
hostname. Never process-manage or casually restart a quick tunnel; on
rotation, re-register tools with the new URL and delete the orphans.
- Signed URLs expire (~15 min). The page mints on load; document the
refresh-if-idle quirk to the user.
- Secrets discipline. API key in org secrets + gateway
.env (chmod 600,
gitignored — verify with git check-ignore); the browser only ever sees
signed URLs; the gateway bearer secret is generated per deployment
(openssl rand -hex 24).
- simulate-conversation MOCKS tools — never use it as E2E proof. EL's
POST /v1/convai/agents/<id>/simulate-conversation returns "Tool Called."
for every tool (ToolMockConfig default) and the sim LLM then FABRICATES
plausible results — it spoke a member count 2.4x the real number with zero
gateway hits in the audit log. Use it for persona/flow checks only. Real
verification = text-only WebSocket session (lesson 12).
- Real headless E2E = text-only WS session on the signed URL. Mint
/api/signed-url, connect a WebSocket client, send
{"type":"conversation_initiation_client_data","conversation_config_override": {"conversation":{"text_only":true}}}, then {"type":"user_message","text":...};
answer ping events with {"type":"pong","event_id":...}. Watch for
agent_tool_response events AND assert the gateway audit log recorded each
call with real args. Spoken numbers must match the live data exactly.
- Targets may already have a voice scaffold — coexist, never clobber. Check
<agentDir>/voice/ first. If occupied, pick new filenames + a free port,
reuse existing modules by import only, and append to the existing README
rather than overwriting. Also probe --json claims in agent docs: some
commands emit JSON with no flag at all, some documented flags don't exist.
- Mock and live must share the param-handling path. Apply filters and
transforms AFTER the fixture-or-file load fork so mock tests exercise the
same logic (a keyword filter shipped broken in mock because the mock
returned early). If a run is halted, write a discovery record (probe shapes,
planned catalog, exclusions) before pivoting — it makes the run resumable
without re-mining.
Phase 0 — Inputs and account
- Target agent name + org. Operator contact for the ASK phase.
ELEVENLABS_API_KEY present (org secrets). Verify: GET /v1/user -> tier.
Log the tier; it picks the provisioning path (lesson 4).
- Voice: operator picks, or default
21m00Tcm4TlvDq8ikWAM (Rachel) — give each
agent in a fleet a DISTINCT voice; swappable later via agent PATCH.
Phase 1 — DISCOVER (the tool engine, part 1: inventory)
FIRST detect the target's runtime from its config.json (runtime field).
The discovery paths below are for claude-code agents. For codex-app-server
agents, substitute: skills under the agent's plugin skills dir (e.g.
plugins/cortextos-agent-skills/skills/), identity from AGENTS/IDENTITY/SOUL/
TOOLS files, and the runtime's own session/transcript store if one exists. If no
transcript source can be found for the runtime, STOP with an explicit
unsupported-runtime error naming what was looked for — do not silently
under-mine the agent (frequency ranking and anchor workflows would be fiction).
Mine four sources, in this order, into voice-profile.json candidates:
- Skills. Read every
<agentDir>/.claude/skills/*/SKILL.md (+ org-level
skills the agent's CLAUDE.md references). Extract per skill:
- bash blocks -> exact CLI invocations with flags = ground-truth param usage
- frontmatter description/triggers -> tool description language
- numbered step sequences -> prompt procedure candidates (Phase 3)
- CLIs. The agent's TOOLS.md registry + every binary the skills invoke. For
each command:
--help parse -> flags, required/optional, enums. Record the
account/instance flags the agent actually uses.
- MCPs.
<agentDir>/.mcp.json and live-connected servers. tools/list
each: schemas are ready-made; mark as PROXY backend (the gateway forwards
through the same policy gate — never attach an external MCP directly to EL).
- Transcripts + memory. Last
14 days of the agent's JSONL transcripts
(`/.claude/projects//*.jsonl`, top-level sessions) + memory files.
Count tool/command frequency -> rank candidates by REAL usage; extract the
top 3 recurring workflows verbatim (they become the anchor chains in Phase
4/6). Also: agent config.json (recipients, accounts, data paths, crons).
Fan discovery out to parallel subagents (skills / transcripts / CLI surface) —
each returns structured raw data, not prose.
Phase 2 — ASK (only what discovery cannot answer)
3-5 questions max to the operator: top workflows confirm (from transcript
ranking), what requires verbal confirmation vs auto, anything that must be
EXCLUDED from voice entirely, voice choice, reach (localhost vs tunnel).
Propose defaults from discovery so the operator can answer with one word — and
if the operator is unavailable, proceed on stated defaults for DISCOVERY and
GENERATION only. Provisioning never proceeds on defaults (Phase 5 gate).
Phase 3 — GENERATE (the tool engine, part 2: synthesis)
Per candidate tool, in dependency order:
- Backend classify: bus-command | CLI-exec | file-access | MCP-proxy.
- Schema derive from Phase-1 ground truth (flags + skill usage examples).
- PROBE (mandatory, lesson 1): run the real command (safe reads live;
writes in a fixture/dry-run mode) with JSON output; capture the response
shape; generate the field mapping + compact-return transform from the
CAPTURED shape. File-backed tools: capture the actual file structure.
- Verb-set matrix (lesson 2): per noun, fill read/list/get/create/update/
delete or record exclusion + reason in the profile.
- Policy class: read | write_safe | write_confirm | excluded. Generate the
server-side invariant for every write (pin, whitelist, append-only,
draft-only, priority caps).
- Description-as-trigger: when-to-use phrasing + id-threading guidance
("use the exact id returned by X") — the EL description does the job of a
skill trigger for the voice LLM.
System prompt construction:
- Persona core from the agent's identity (CLAUDE.md role, SOUL/identity files).
- Per relevant skill: DISTILL (never paste) into a procedure section — trigger
phrase, numbered steps referencing generated tool names, confirm rules.
- Tool discipline block (chain reads silently, carry ids exactly, readback
rules, never round numbers).
- Truthful-limits section auto-generated from exclusions (lesson 3) with relay
paths.
- Keep lean; 2MB is the EL cap but hundreds of lines is the practical target.
Generate a new voice scaffold (never copy a prior build's files) under an empty
<agentDir>/voice/; if that directory already exists, coexist per lesson 13 —
new filenames + a free port inside it, or a voice/<deployment-name>/ subtree,
and append to existing docs rather than overwriting. The generated tree: voice.config.js (identity + persona + PINNED data paths),
src/tools/* (written per tool, shaped by its backend type and probe captures),
mcp-server.js (written for this agent's tool registry + policy classes),
src/policy.js, src/session-store.js (append-only audit JSONL), tests/
(Phase 4), public/index.html, .env (lesson 10), el-provision.js +
el-server-tools.js.
Phase 4 — TEST (all auto-generated, all must pass before provisioning)
- MCP protocol suite (initialize / tools/list count / unknown-method) — write it
fresh against the generated server.
- Policy matrix: every tool x every class, whitelist negatives, unknown-tool
reject, plus a no-orphans check (every policy entry has a catalog tool and
vice versa).
- Per-tool units on mock fixtures (fixtures generated from the Phase-3 probe
captures — the mock IS the captured real shape; lesson 14 on shared paths).
- Anchor chains: the agent's top transcript workflows end-to-end on mocks with
id threading asserted.
- Gateway HTTP: bearer 401 negative, page serves, signed-url clean pre-key state.
- Then LIVE smokes: every read tool against real data locally before any
provisioning; writes as create-then-cleanup.
Phase 5 — PROVISION
OPERATOR GATE (hard stop — do not pass on defaults). Before ANY
api.elevenlabs.io provisioning call, process-manager start, tunnel, or link
sharing, present the operator with: the discovered tool catalog + policy classes,
the verb-matrix exclusions, expected EL cost/tier implications (minutes billing,
concurrency), the tunnel-exposure surface (public HTTPS URL fronting the
gateway), and every write tool with its server-side invariant. Get explicit
approval. This is the conscious human checkpoint between "code generated locally"
and "external paid resources exist + a voice surface is reachable".
- Tier probe (lesson 4, POST not GET) -> MCP path or server-tools path.
- Start the gateway under a process manager (
<agent>-voice-gateway); tunnel
via a detached quick tunnel (lesson 8); verify tunnel -> gateway health.
- Create agent:
POST /v1/convai/agents/create — a current Claude model,
generated prompt, voice. PATCH path for updates.
- Tools: server-tools path =
POST /v1/convai/tools per tool (url = tunnel
/tool/<name>, bearer header, request_body_schema from the generated
schema), then PATCH agent tool_ids (GET current ids first, append — never
overwrite blind). MCP path = POST /v1/convai/mcp-servers with
require_approval_per_tool — this only CREATES the MCP server config; the
agent does not use it until you PATCH the agent's prompt config to include
the returned MCP server id (conversation_config.agent.prompt.mcp_server_ids
— GET current ids first and append, preserving existing entries, exactly as
with tool_ids).
- Live read smokes through the tunnel; write smokes create-then-cleanup.
Phase 6 — VERIFY (before any link ships)
- Drive a REAL text-only WebSocket session per top-3 workflow (lesson 12 — NOT
simulate-conversation, which mocks tools and fabricates results, lesson 11):
assert the expected tools fired via
agent_tool_response events, the gateway
audit log recorded each call with real args, AND the spoken answer matches
the live data exactly.
- Asymmetry audit (executable lesson-2 checklist): diff the verb-set matrix
against the shipped catalog; any silent gap = ship-blocker.
- Page check over the tunnel; THEN send the operator the link with: known
quirks (15-min signed-url refresh, what voice can never do), the audit-log
location, and the recovery runbook (tunnel rotation procedure).
Outputs
<agentDir>/voice/ live gateway + page + tests, process-managed.
voice-profile.json — the full discovery record (tools, probe shapes, policy
classes, verb matrix with exclusions, EL ids, verification evidence). This is
the auditable artifact.
- Operator link + quirks note + recovery runbook.
1---2name: voice-agent-factory3description: Turn ANY cortextOS agent into a live ElevenLabs voice agent: mine its skills, CLIs, MCPs, and transcripts into a tool catalog, distill its skills into a voice persona, dynamically generate a policy-gated gateway and all code per target (NO pre-built scripts), test exhaustively on probe-shaped fixtures, provision on ElevenLabs with automatic tier fallback, and verify end-to-end with real conversations before shipping a link.4---56# voice-agent-factory — cortextOS agent -> ElevenLabs voice agent78Run this skill against a target agent and it produces a live, policy-gated voice9surface: a zero-dependency Node gateway exposing the agent's real capabilities as10ElevenLabs tools, a widget page, a generated test harness, provisioning scripts,11and an auditable discovery record.1213**ALL CODE IS WRITTEN DYNAMICALLY PER RUN.** There is NO pre-built script library14or template to copy. The agent running this skill reads the bundled `resources/`15docs (verified ElevenLabs API surface), then writes every file — gateway, tools,16tests, provisioning calls — fresh, fit to the target agent's actual capabilities17as discovered. Two agents with different jobs get completely different tool18catalogs from the same skill.1920## Prerequisites2122- A cortextOS install with the target agent onboarded (`<agentDir>` = the agent's23 workspace, e.g. `orgs/<org>/agents/<name>/`), and the `cortextos` CLI on PATH.24- Node >= 18 (the generated gateway and tests are zero-dependency `node:` builtins).25- An ElevenLabs account + `ELEVENLABS_API_KEY` in your org secrets file. Any paid26 tier works; the skill tier-detects and picks the right provisioning path. Voice27 minutes are billed by ElevenLabs — see `resources/GAPS.md` pricing table.28- For remote reach: `cloudflared` (quick tunnel). Localhost-only runs skip it, but29 then ElevenLabs webhook tools cannot reach the gateway — server-tools mode30 REQUIRES a public HTTPS URL. A reverse proxy or other tunnel works equally well.31- A process manager (pm2 or equivalent) for the gateway. The quick tunnel itself32 must NOT be process-managed (lesson 8).33- Browser page: the generated widget page loads the EL embed from unpkg.com at34 runtime (the one external browser dependency). If you need a no-CDN deployment,35 vendor `@elevenlabs/convai-widget-embed` locally and point the script tag at it.3637## How to run3839Tell the agent running this skill: the target agent name + org, the operator40contact, and (optionally) a voice id. Then follow the phases in order. Minimal41invocation: "Run voice-agent-factory against <agent> in <org>; I'm the operator."4243## What this writes / creates4445- Files: `<agentDir>/voice/` (gateway, tools, tests, page, provisioning scripts,46 `.env` chmod 600 + gitignored, `voice-profile.json`) and an append-only audit47 log under the agent's docs. Nothing outside the target agent's workspace.48- Local: one listening port you choose (track one port per agent), a process-manager49 entry `<agent>-voice-gateway`, optionally a quick-tunnel process.50- External (ONLY after the Phase 5 gate): an ElevenLabs agent, one EL webhook tool51 per gateway tool (or an MCP server attachment), and EL voice-minute usage.5253Read `resources/REPORT.md` before any ElevenLabs API call: EL is mid-rename54(ConvAI -> ElevenAgents), doc URLs drift, and the API surface in REPORT is the55verified one. `resources/GAPS.md` has the server-tool webhook schema + pricing.56`resources/SOURCES.md` lists primary sources.5758## Non-negotiable lessons (each one was hit live during the reference builds)59601. **PROBE, never map from docs.** Every CLI's JSON output shape MUST be captured61 by running a real safe command before writing a field mapping. (An email tool62 shipped with empty from/subject because the CLI nests `{headers:{from,...},63 message:{snippet}}`; a calendar create wraps its response as `{event:{id}}`64 not `{id}`.)652. **Verb-set completeness.** For every NOUN a tool touches, generate the full66 verb set — list/get/create/update/delete — or record an explicit exclusion67 with a reason. (A build shipped calendar WRITE with no READ; the operator hit68 the gap in his first minute of talking.)693. **The agent states its limits truthfully — make the truth complete.** The70 prompt carries an auto-generated "I cannot X from voice" section built from71 the exclusion list, each with a relay path (usually create_task to the real72 agent).734. **Tier-detect before MCP — and probe the POST, not the GET.** Some tiers74 return `convai_mcp_servers_disabled` on `POST /v1/convai/mcp-servers` while75 the GET still 200s. The POST is the gate; fall back to webhook server tools76 (same gateway, `/tool/<name>` REST skin) without redesign.775. **Compact returns.** Voice latency stacks per tool round-trip. Returns carry78 ids + summaries, bodies capped (~1500 chars), lists capped (~8-15 items), and79 an `as_of` UTC stamp on anything time-sensitive (the voice LLM does not80 reliably know today's date).816. **Timezone offsets are required in every time input** (description-level82 rule); the server parses RFC3339 and rejects unparseable times.837. **Hard invariants live server-side, not in the prompt.** Draft-only mail (no84 send path in the code at all), pinned resource ids (not parameters),85 append-only updates, recipient whitelists. Verbal confirmation is a persona86 rule; safety is code.878. **Tunnel URL is load-bearing.** EL webhook tool URLs embed the tunnel88 hostname. Never process-manage or casually restart a quick tunnel; on89 rotation, re-register tools with the new URL and delete the orphans.909. **Signed URLs expire (~15 min).** The page mints on load; document the91 refresh-if-idle quirk to the user.9210. **Secrets discipline.** API key in org secrets + gateway `.env` (chmod 600,93 gitignored — verify with `git check-ignore`); the browser only ever sees94 signed URLs; the gateway bearer secret is generated per deployment95 (`openssl rand -hex 24`).9611. **simulate-conversation MOCKS tools — never use it as E2E proof.** EL's97 `POST /v1/convai/agents/<id>/simulate-conversation` returns `"Tool Called."`98 for every tool (ToolMockConfig default) and the sim LLM then FABRICATES99 plausible results — it spoke a member count 2.4x the real number with zero100 gateway hits in the audit log. Use it for persona/flow checks only. Real101 verification = text-only WebSocket session (lesson 12).10212. **Real headless E2E = text-only WS session on the signed URL.** Mint103 `/api/signed-url`, connect a WebSocket client, send104 `{"type":"conversation_initiation_client_data","conversation_config_override":105 {"conversation":{"text_only":true}}}`, then `{"type":"user_message","text":...}`;106 answer `ping` events with `{"type":"pong","event_id":...}`. Watch for107 `agent_tool_response` events AND assert the gateway audit log recorded each108 call with real args. Spoken numbers must match the live data exactly.10913. **Targets may already have a voice scaffold — coexist, never clobber.** Check110 `<agentDir>/voice/` first. If occupied, pick new filenames + a free port,111 reuse existing modules by import only, and append to the existing README112 rather than overwriting. Also probe `--json` claims in agent docs: some113 commands emit JSON with no flag at all, some documented flags don't exist.11414. **Mock and live must share the param-handling path.** Apply filters and115 transforms AFTER the fixture-or-file load fork so mock tests exercise the116 same logic (a keyword filter shipped broken in mock because the mock117 returned early). If a run is halted, write a discovery record (probe shapes,118 planned catalog, exclusions) before pivoting — it makes the run resumable119 without re-mining.120121## Phase 0 — Inputs and account122123- Target agent name + org. Operator contact for the ASK phase.124- `ELEVENLABS_API_KEY` present (org secrets). Verify: `GET /v1/user` -> tier.125 Log the tier; it picks the provisioning path (lesson 4).126- Voice: operator picks, or default `21m00Tcm4TlvDq8ikWAM` (Rachel) — give each127 agent in a fleet a DISTINCT voice; swappable later via agent PATCH.128129## Phase 1 — DISCOVER (the tool engine, part 1: inventory)130131FIRST detect the target's runtime from its `config.json` (`runtime` field).132The discovery paths below are for `claude-code` agents. For `codex-app-server`133agents, substitute: skills under the agent's plugin skills dir (e.g.134`plugins/cortextos-agent-skills/skills/`), identity from AGENTS/IDENTITY/SOUL/135TOOLS files, and the runtime's own session/transcript store if one exists. If no136transcript source can be found for the runtime, STOP with an explicit137unsupported-runtime error naming what was looked for — do not silently138under-mine the agent (frequency ranking and anchor workflows would be fiction).139140Mine four sources, in this order, into `voice-profile.json` candidates:1411421. **Skills.** Read every `<agentDir>/.claude/skills/*/SKILL.md` (+ org-level143 skills the agent's CLAUDE.md references). Extract per skill:144 - bash blocks -> exact CLI invocations with flags = ground-truth param usage145 - frontmatter description/triggers -> tool description language146 - numbered step sequences -> prompt procedure candidates (Phase 3)1472. **CLIs.** The agent's TOOLS.md registry + every binary the skills invoke. For148 each command: `--help` parse -> flags, required/optional, enums. Record the149 account/instance flags the agent actually uses.1503. **MCPs.** `<agentDir>/.mcp.json` and live-connected servers. `tools/list`151 each: schemas are ready-made; mark as PROXY backend (the gateway forwards152 through the same policy gate — never attach an external MCP directly to EL).1534. **Transcripts + memory.** Last ~14 days of the agent's JSONL transcripts154 (`~/.claude/projects/<wd-slug>/*.jsonl`, top-level sessions) + memory files.155 Count tool/command frequency -> rank candidates by REAL usage; extract the156 top 3 recurring workflows verbatim (they become the anchor chains in Phase157 4/6). Also: agent config.json (recipients, accounts, data paths, crons).158159Fan discovery out to parallel subagents (skills / transcripts / CLI surface) —160each returns structured raw data, not prose.161162## Phase 2 — ASK (only what discovery cannot answer)1631643-5 questions max to the operator: top workflows confirm (from transcript165ranking), what requires verbal confirmation vs auto, anything that must be166EXCLUDED from voice entirely, voice choice, reach (localhost vs tunnel).167Propose defaults from discovery so the operator can answer with one word — and168if the operator is unavailable, proceed on stated defaults for DISCOVERY and169GENERATION only. Provisioning never proceeds on defaults (Phase 5 gate).170171## Phase 3 — GENERATE (the tool engine, part 2: synthesis)172173Per candidate tool, in dependency order:1741751. **Backend classify**: bus-command | CLI-exec | file-access | MCP-proxy.1762. **Schema derive** from Phase-1 ground truth (flags + skill usage examples).1773. **PROBE (mandatory, lesson 1)**: run the real command (safe reads live;178 writes in a fixture/dry-run mode) with JSON output; capture the response179 shape; generate the field mapping + compact-return transform from the180 CAPTURED shape. File-backed tools: capture the actual file structure.1814. **Verb-set matrix (lesson 2)**: per noun, fill read/list/get/create/update/182 delete or record exclusion + reason in the profile.1835. **Policy class**: read | write_safe | write_confirm | excluded. Generate the184 server-side invariant for every write (pin, whitelist, append-only,185 draft-only, priority caps).1866. **Description-as-trigger**: when-to-use phrasing + id-threading guidance187 ("use the exact id returned by X") — the EL description does the job of a188 skill trigger for the voice LLM.189190**System prompt construction:**191- Persona core from the agent's identity (CLAUDE.md role, SOUL/identity files).192- Per relevant skill: DISTILL (never paste) into a procedure section — trigger193 phrase, numbered steps referencing generated tool names, confirm rules.194- Tool discipline block (chain reads silently, carry ids exactly, readback195 rules, never round numbers).196- Truthful-limits section auto-generated from exclusions (lesson 3) with relay197 paths.198- Keep lean; 2MB is the EL cap but hundreds of lines is the practical target.199200Generate a new voice scaffold (never copy a prior build's files) under an empty201`<agentDir>/voice/`; if that directory already exists, coexist per lesson 13 —202new filenames + a free port inside it, or a `voice/<deployment-name>/` subtree,203and append to existing docs rather than overwriting. The generated tree: `voice.config.js` (identity + persona + PINNED data paths),204`src/tools/*` (written per tool, shaped by its backend type and probe captures),205`mcp-server.js` (written for this agent's tool registry + policy classes),206`src/policy.js`, `src/session-store.js` (append-only audit JSONL), `tests/`207(Phase 4), `public/index.html`, `.env` (lesson 10), `el-provision.js` +208`el-server-tools.js`.209210## Phase 4 — TEST (all auto-generated, all must pass before provisioning)211212- MCP protocol suite (initialize / tools/list count / unknown-method) — write it213 fresh against the generated server.214- Policy matrix: every tool x every class, whitelist negatives, unknown-tool215 reject, plus a no-orphans check (every policy entry has a catalog tool and216 vice versa).217- Per-tool units on mock fixtures (fixtures generated from the Phase-3 probe218 captures — the mock IS the captured real shape; lesson 14 on shared paths).219- Anchor chains: the agent's top transcript workflows end-to-end on mocks with220 id threading asserted.221- Gateway HTTP: bearer 401 negative, page serves, signed-url clean pre-key state.222- Then LIVE smokes: every read tool against real data locally before any223 provisioning; writes as create-then-cleanup.224225## Phase 5 — PROVISION226227**OPERATOR GATE (hard stop — do not pass on defaults).** Before ANY228`api.elevenlabs.io` provisioning call, process-manager start, tunnel, or link229sharing, present the operator with: the discovered tool catalog + policy classes,230the verb-matrix exclusions, expected EL cost/tier implications (minutes billing,231concurrency), the tunnel-exposure surface (public HTTPS URL fronting the232gateway), and every write tool with its server-side invariant. Get explicit233approval. This is the conscious human checkpoint between "code generated locally"234and "external paid resources exist + a voice surface is reachable".2352361. Tier probe (lesson 4, POST not GET) -> MCP path or server-tools path.2372. Start the gateway under a process manager (`<agent>-voice-gateway`); tunnel238 via a detached quick tunnel (lesson 8); verify tunnel -> gateway health.2393. Create agent: `POST /v1/convai/agents/create` — a current Claude model,240 generated prompt, voice. PATCH path for updates.2414. Tools: server-tools path = `POST /v1/convai/tools` per tool (url = tunnel242 `/tool/<name>`, bearer header, request_body_schema from the generated243 schema), then PATCH agent `tool_ids` (GET current ids first, append — never244 overwrite blind). MCP path = `POST /v1/convai/mcp-servers` with245 `require_approval_per_tool` — this only CREATES the MCP server config; the246 agent does not use it until you PATCH the agent's prompt config to include247 the returned MCP server id (`conversation_config.agent.prompt.mcp_server_ids`248 — GET current ids first and append, preserving existing entries, exactly as249 with `tool_ids`).2505. Live read smokes through the tunnel; write smokes create-then-cleanup.251252## Phase 6 — VERIFY (before any link ships)2532541. Drive a REAL text-only WebSocket session per top-3 workflow (lesson 12 — NOT255 simulate-conversation, which mocks tools and fabricates results, lesson 11):256 assert the expected tools fired via `agent_tool_response` events, the gateway257 audit log recorded each call with real args, AND the spoken answer matches258 the live data exactly.2592. Asymmetry audit (executable lesson-2 checklist): diff the verb-set matrix260 against the shipped catalog; any silent gap = ship-blocker.2613. Page check over the tunnel; THEN send the operator the link with: known262 quirks (15-min signed-url refresh, what voice can never do), the audit-log263 location, and the recovery runbook (tunnel rotation procedure).264265## Outputs266267- `<agentDir>/voice/` live gateway + page + tests, process-managed.268- `voice-profile.json` — the full discovery record (tools, probe shapes, policy269 classes, verb matrix with exclusions, EL ids, verification evidence). This is270 the auditable artifact.271- Operator link + quirks note + recovery runbook.