| name | voicenter-bot-json-assembler |
| description | Assembles a fully-detailed Voicenter Agent Spec into Bot JSON wire format — the final mechanical step in the three-skill pipeline. Use this skill when an Agent Spec exists with all section 5 entries marked `[detailed]` and the user wants the deployable JSON. Trigger phrases include "run Skill 3", "assemble the JSON", "emit the bot JSON", "publish the bot", "build the wire-format", "Skill 3 (JSON Assembler)", or any direct continuation from Skill 2's completion handoff. Produces a single `bot-<name>-<date>.json` file plus a banner identifying every fail-loud sentinel and any drift between spec section 6 and what Skill 3 regenerated. Refuses to assemble if any intent is still `[structural]` or `[detailed-revisit]`, or if the spec deviates from the strict template (Doc 2 §3.7). Runs the §15.4 cross-reference pass — 23 checks (8 §15.4 + 3 Compass + 3 botIntents-role + 1 duplicate-global-intent + 8 field-placement doctrine), checks 1–7, 11–13, 15, and 16–21 blocking. Does NOT author any text content (Skills 1 and 2 only). Does NOT make creative decisions, interpret deviations, fix violations, or invoke other skills (it reports routing recommendations; the user invokes the relevant skill). |
Language. Reply in the user's language: detect what they write — Hebrew→Hebrew, English→English — and mirror it, switching if they switch mid-conversation. This shapes your prose, your questions, and your AskUserQuestion option labels only. It does not change the artifacts you produce — identifiers, JSON keys, BCP-47 language codes, API field names, and other data stay exactly as specified.
Opening. Your first message greets bilingually so the user knows both languages are available — e.g. "נוכל להמשיך בעברית או באנגלית — מה נוח לך? / We can continue in Hebrew or English — whichever you prefer." Then mirror whatever language the user replies in.
One question per turn. Ask exactly one question per message and wait for the answer before asking the next — never present multiple questions in a single turn. When the answer is a closed set (pick-one / yes-no / pick-from-list), use the AskUserQuestion tool rather than plain text; it automatically adds an "Other" free-text escape, so don't hand-roll one. Reserve plain free-text questions for genuinely open inputs (names, descriptions, URLs, numbers).
Skill 3 — JSON Assembler & Publish
This skill produces the deployable Bot JSON by mechanically projecting a fully-detailed Agent Spec into Voicenter wire-format. It is the third and final skill in the Voicenter Bot generation pipeline:
- Skill 1 (Agent Spec Designer): structural design via interview → fills sections 1, 2, 3, 4, 4.5; creates section 5 stubs marked
[structural].
- Skill 2 (Intent Detail Author): language-heavy per-intent content → fills section 5 entries, marks them
[detailed].
- Skill 3 (this skill): mechanical assembly of spec → wire-format JSON.
Operating principle: pure parser, not interpreter. Skill 3 makes no creative decisions. It does not best-effort interpret ambiguous spec content; if the spec deviates from the strict template, Skill 3 reports a structured parse error and refuses to assemble. The entire skill architecture depends on Skill 3 being deterministic — if Skill 3 interprets, "what JSON does this spec produce?" depends on Skill 3's mood, and the source-of-truth contract dies. Discipline is the design.
The risk vector for this skill is doing too much: filling in plausible-looking values for unknowns, smoothing over template deviations, auto-fixing cross-reference violations, deciding RT-specific defaults the spec didn't specify. The anti-list (§8) is the longest and most opinionated section — read it before doing anything else.
1. Required reading at invocation
Before touching the spec, load context from these references.
| Read | Why |
|---|
| Doc 1 §4 — Bot top-level wrapper | Mapping for spec section 1 root fields |
Doc 1 §5 — ActiveVersionInfo envelope | Mapping for version-level fields |
Doc 1 §6 — The two AIModelConfig objects | Top-level vs version-level + created payload duplication |
| Doc 1 §7 — Crosswalk: training-doc → JSON paths | Field-name reconciliation reference |
Doc 1 §8 — intentList six parallel collections | The bulk of assembly |
Doc 1 §9 — intents[] 17-field skeleton | Per-intent shape |
Doc 1 §10 — IntentParameters slot definitions | Per-slot shape |
Doc 1 §11 — ResponseTypeId reference (RT=1/2/3/4) | RT-specific Configuration assembly (§4.4) |
| Doc 1 §11.2 — RT=2 pairing rule | Cross-reference check 5 + 6 |
Doc 1 §12 — ParameterTypeId catalog | Slot type emission |
| Doc 1 §13 — Mustache + variable categories | Cross-reference check 7 |
| Doc 1 §15.3 — ID placeholder strategy (Option A) | §4.1 allocation |
| Doc 1 §15.4 — Cross-reference pass spec | §6 — the cross-reference checks |
| Doc 1 §16 — Schema quirks summary | §4.5 + Appendix A |
| Doc 2 §3.7 — Strict-template enforcement | §3 parse rules |
| Doc 2 §6 — Skill 3 architecture | Everything in this file implements this |
| Doc 2 §7.5 — Routing failures back | Appendix B |
locked-decisions.md decision B | Sentinel strategy |
locked-decisions.md decision M | Section 4.5 inventory drives Mustache check |
../../references/voice-prompt-doctrine.md | Compass doctrine — 13 rules; Skill 3 owns checks 8 (token budget — rule 1), 9 (session resumption — rule 2), 10 (model-config doctrine — rule 12), and the banner sentinels (rule 13) |
../../references/field-placement-doctrine.md | Field-placement doctrine (v1.14.0) — FP-1…FP-13; Skill 3 owns cross-reference checks 16–23 |
Also load this file from Skill 1's package:
skills/voicenter-bot-spec-designer/model-catalog.md — required for resolving named catalog entries to AIModelConfigID / AIModelTypeId and provider model string (§4.2.3).
Skill 3 does not load Skill 2's conversation-routines-style-guide.md. The style of validationPrompt and intentInstructions text is Skill 2's concern; Skill 3 emits the text verbatim from the spec, regardless of style.
2. Setup
2.1 Detect runtime
| Signal | Runtime |
|---|
Conversation in claude.ai or mobile app, no workspace file system, no agent-spec.md accessible | Single-conversation |
Workspace file system available (Claude Code), agent-spec.md readable as a file | Claude Code |
State the detected runtime. The user can correct.
2.2 Read the spec
Single-conversation: read backward through the conversation context to find the most recent spec emission. The spec is identifiable by its ## 1. Bot Identity header and ## 7. Generation Metadata footer. If both Skill 1 and Skill 2 ran in this conversation, take the most recent (Skill 2's output).
Claude Code: read agent-spec.md from the workspace (or whatever filename the user references).
No spec found: abort with: "No Agent Spec found. Skill 3 requires a fully-detailed spec produced by Skill 1 → Skill 2. Invoke Skill 1 (Agent Spec Designer) first."
2.3 Pre-flight gates
Three gates run before any assembly work. All are blocking. Refusal at any gate emits a clear message and halts; no JSON is produced.
Gate A — Completeness
Walk section 5. Count entries with status [structural] or [detailed-revisit]. If the count is greater than zero, refuse:
Skill 3 will not assemble an incomplete spec. Section 5 has [N] intents still pending: [list with status per intent]. Run Skill 2 (Intent Detail Author) to detail them, then re-invoke Skill 3.
The list shows identifier + status (e.g., validate_customer_address [structural], confirm_appointment [detailed-revisit]), not detail level.
Cross-check against section 7.5 (which Skill 2 maintains). If 7.5 says zero pending but section 5 has pending entries, that's a Skill 2 bookkeeping bug — surface it: "Spec inconsistency: section 7.5 reports 0 pending, but section 5 has [N] intents in non-detailed state. Re-run Skill 2 once to refresh, then re-invoke Skill 3."
Gate B — Parseability
Run the strict-template parser (§3) over the spec. The first deviation halts parsing and produces a structured error. No partial assembly.
Parseability is checked before completeness in cases where the file is malformed at the section-header level (e.g., section headers missing entirely) — in that case, Skill 3 cannot even tell which intents are pending. Practical order: try a quick scan for the seven ## N. section headers first; if they're missing, Gate B fires first. If headers are present, Gate A fires first.
Gate C — RT=2 verification
For every intent whose section-4 Response Type is 2 (RT=2 / API Call), verify a matching entry exists in spec section 7.6 (the RT=2 API verification log). If any RT=2 intent has no 7.6 entry, refuse:
Skill 3 will not assemble an RT=2 intent whose API was never verified. Intent(s) missing a section 7.6 verification record: [list]. Re-run Skill 2 (Intent Detail Author) on each — it hard-verifies the live API (real curl, 2xx + every declared response path present) and writes the 7.6 record. There is no waiver.
This is a backstop: a hard-verified spec reaches Skill 3 with every RT=2 intent already [detailed] (Gate A) and logged in 7.6. Gate C catches a hand-edited spec that flipped an intent to [detailed] without verifying.
3. Strict-template parsing
3.1 The deterministic parse principle
The Agent Spec template is documented in Doc 2 §3 and codified in Skill 1's spec-skeleton.md. Skill 3 reads it as a fixed grammar — no synonyms, no flexibility, no creative tolerance.
Specifically, the parser expects:
- Section headers exact:
## 1. Bot Identity, ## 2. Persona Bundle, ## 3. Caller Silence Behavior, ## 4. Intent List (Structural), ## 4.5 Available Variables, ## 4.6 Global/System Catalog Intents, ## 5. Intent Details, ## 6. Cross-References, ## 7. Generation Metadata. Exact strings, exact numbering, exact punctuation. ## 1: Bot Identity is a parse error. ## Bot Identity is a parse error.
- Section 4.6 (optional): either the literal
[none], or one or more ### Catalog Intent: <IntentId> — <Name> blocks, each with **Wiring:** silence-forward only|triggerable global and a **Definition:** fenced ```json block. The JSON block must parse and carry a positive-integer IntentId and an IntentCategoryId. A malformed block or a non-positive IntentId is a parse error (§3.2) — Skill 3 does NOT repair it.
- Field labels exact:
**Bot Name:**, **Identifier:**, **Description:**, **Account ID:**, **Primary Language:**, **Channels Active:**, **Voice Name:**, **AI Model Config:**. Bold markdown around the colon-terminated label, exactly as written.
- Section 1 optional limit fields (v1.13.0; maxDurationLayerId default revised v1.14.0):
**Daily limit:** (int), **Daily limit layer:** (int), **Max duration layer:** (int), **Daily limit sentence:** (free text), **Max duration sentence:** (free text), **IVRLayerSelect_2:** (int). All optional; absence parses to defaults 600 / 3 / 0 / production-default sentence / production-default Hebrew sentence / 3 (see §4.2.2). A non-integer where an int is expected is a parse error.
- Status markers exact:
[structural], [detailed], [detailed-revisit]. No synonyms (e.g., [done], [in progress]).
- Unknown markers exact:
<UNKNOWN: <description>>, <INCOMPLETE: <description>>, [not configured]. The angle-bracket format is not optional; (UNKNOWN: ...) is a parse error.
- Intent header in section 4:
### Intent N: <identifier> where N is the 1-based ordinal and identifier is snake_case. The number determines section 4 ordering (used for first-intent start-marker logic in botIntents[]).
- Bot-intent role in section 4:
**Bot-intent role:** <value> where <value> is exactly one of entry, global, chained. The field is optional; absence is parsed as chained. Any other value (e.g. start, escalation, a list) is a parse error per §3.2. This field drives §4.3.3 botIntents membership/type.
- Staggering fields in section 4 (v1.13.0, optional):
**Captures answer to:** (free text) and **Asks next:** (free text, or the literal [none — terminal]). Absence ⇒ the staggering-dependent checks skip for that intent.
- Terminal outcome in section 4 (v1.13.0, optional; RT=1 only):
**Terminal outcome:** <slot_name> = <value-part>. Two-mode grammar: a double-quoted <value-part> ⇒ fixed mode (the exact pinned string); an unquoted free-text <value-part> ⇒ captured/dynamic mode (a description of how the value is captured or composed). A line without <slot_name> = is a parse error. <slot_name> must be snake_case and is cross-checked against the intent's slot list in §6 check 20.
- Sensitive in section 4 (v1.13.0, optional):
**Sensitive:** true|false only. Absence parses as false. Any other value is a parse error.
- IsSilenceIntent in section 4 (v1.14.0, optional):
**IsSilenceIntent:** true|false only. Absence parses as false. Any other value is a parse error. Drives the intent-root IsSilenceIntent integer (§4.3.1 row 13).
- Intent header in section 5:
### Intent: <identifier> (no ordinal). Identifier matches a section 4 entry.
- Section 4.5.5 (v1.13.0, optional): header exact
### 4.5.5 CustomData keys (per-call payload) under ## 4.5 Available Variables; entries - \{{key}}` — `. Absence ⇒ empty CustomData key list (check 7 then allows only 4.5.1–4.5.4 references).
- Slot lines in section 4: numbered list under
**Slots:** heading, format [slot_name] — \ParameterTypeId` [N], Required [`true`|`false`], Order [N], OptionList [if ENUM]`.
- Transition lines in section 4: numbered list under
**Transitions out:** heading, each item is a target intent identifier optionally followed by a parenthetical role label (e.g., 1. get_available_slots (success path)).
- RT-specific sub-labels in section 4: for RT=1 intents,
**Layer:** followed by an integer. For RT=2 intents, **URL:**, **Method:**, **Headers:**, **Body:**, and **API silence behavior:** (the silence block has six sub-bullets exact: silence_duration:, silence_loops:, silence_sentence:, silence_ending_sentence:, silence_instructions:, fallback intent:). For RT=3 intents, the RT-specific block is empty (no sub-bullets). For RT=4 intents, **Dial source:** (parameter | static), then either **Parameter phone:** (slot identifier, when dial-source=parameter) or **Phone1:** / **Phone2:** / **Phone3:** (when dial-source=static); plus **selectdial_option:**, **NEXT_VO_ID:**, **MAX_DIAL_DURATION:**, **Record:**, optional **Announcement:** / **Loading announcement:** / **Post-execution intent instructions:**, and **Response success:** (object with instructions key).
3.2 Parse error format
When a deviation is detected, halt and emit:
Skill 3 parse error.
Location: line <N> in <spec source>
Section: <section number, e.g., 4>
Expected: <pattern>
Found: <actual content, truncated to one line>
Fix: <one-line hint about the fix>
Skill 3 will not assemble. Re-run Skill 1 patch mode (if the spec was hand-edited or structurally invalid) or fix the deviation manually, then re-invoke Skill 3.
The <spec source> is the conversation message reference (single-conv) or the file path (Claude Code). Line numbers are within that source.
Skill 3 does not attempt to interpret around the deviation. It does not emit a partial JSON. It does not flag and continue. One deviation, one error, one halt.
3.3 Common deviations and example messages
These are illustrative — the parser is grammar-driven, not pattern-matched, so anything off-grammar surfaces. The examples here are the most common shapes the user will see.
| Deviation | Example error |
|---|
| Missing section header | Expected: '## 4. Intent List (Structural)'. Found: '## Intent List'. Fix: restore the section number and exact heading. |
| Bold field label punctuation off | Expected: '**Bot Name:** <value>'. Found: 'Bot Name: <value>'. Fix: wrap the label in bold markdown. |
| Unknown marker shape wrong | Expected: '<UNKNOWN: <description>>'. Found: '(UNKNOWN: ...)'. Fix: use angle brackets and the literal token UNKNOWN. |
| Status marker synonym | Expected: one of '[structural]', '[detailed]', '[detailed-revisit]'. Found: '[done]'. Fix: re-run Skill 2 to set the canonical marker. |
| Intent identifier in section 5 has no match in section 4 | Section 5 entry 'verify_caller_id' has no matching intent in section 4. Fix: re-run Skill 1 patch mode to add the intent or remove the orphan section 5 entry. |
| Section 4 reference to undeclared transition target | Intent 'validate_customer_address' transitions to 'get_slots', but no intent 'get_slots' exists in section 4 (closest match: 'get_available_slots'). Fix: re-run Skill 1 patch mode to correct the transition target. |
| Spec ends mid-intent (truncated upload) | Section 5 entry 'confirm_appointment' has no closing structure (no following section 6 header). Fix: re-attach the complete spec. |
| RT-specific sub-label punctuation off | Expected: '**URL:** <value>'. Found: 'URL: <value>'. Fix: wrap the sub-label in bold markdown. |
| Bot-intent role value off-grammar | Expected: '**Bot-intent role:** entry|global|chained'. Found: '**Bot-intent role:** start'. Fix: use one of the three canonical role values (or omit for chained). |
| Terminal outcome missing slot assignment (v1.13.0) | Expected: '**Terminal outcome:** <slot_name> = "<fixed value>"' or '**Terminal outcome:** <slot_name> = <capture/compose description>'. Found: '**Terminal outcome:** הלקוח אישר הכל'. Fix: name the owning slot and use '=' (quote the value only when it is a fixed pinned string). |
| Sensitive value off-grammar (v1.13.0) | Expected: '**Sensitive:** true|false'. Found: '**Sensitive:** yes'. Fix: use lowercase true or false (or omit for false). |
| IsSilenceIntent value off-grammar (v1.14.0) | Expected: '**IsSilenceIntent:** true|false'. Found: '**IsSilenceIntent:** 1'. Fix: use lowercase true or false (or omit for false). |
The transition-target check (last two rows) blurs into cross-reference territory — it's caught at parse time because it's a dangling identifier discoverable from sections 4-5 alone, and Skill 3 already has the data. Treating it as a parse error rather than waiting for §15.4 lets the user fix one thing at a time.
4. Spec-to-wire-format assembly
Run only if all three pre-flight gates pass and the parser succeeds. Assembly happens in memory; nothing is emitted until §6 (cross-reference pass) also passes.
4.1 ID placeholder allocation
Per Doc 1 §15.3 Option A and Doc 2 §6.5: sequential negative integers, range-coded so the kind of ID is identifiable at a glance.
| ID kind | Placeholder range | Allocation rule |
|---|
BotID | -1 | Single value |
BotVersionId | -2 | Single value |
IntentCategoryId | -3 | Single default category |
IntentId | -10, -11, -12, ... | One per intent in section 4 ordering |
BotIntentId | -100, -101, -102, ... | One per emitted botIntents[] entry (entry + global intents only — see §4.3.3), in section-4 order. Chained intents get no BotIntentId. |
ParameterId | -1000, -1001, -1002, ... | One per slot, walked intent-by-intent then slot-by-slot |
IntentRelatedID | -2000, -2001, ... | v1.5.0: one per intentRelations[] row (no longer mirrors NextIntentID) |
IntentConditionGroupID | -3000, -3001, ... | One per emitted botIntents[] entry (subset — §4.3.3) + one per intentRelations[] row. |
IntentSourceID | -4000, -4001, ... | v1.5.0: one per intent when voice channel is active |
IntentConditionRelationID does not need a new range. It mirrors BotIntentId (when inside botIntents[]) or IntentRelatedID (when inside intentRelations[]) — the production export pattern. Skill 3 fills it from the matching parent value.
AccountID is user-supplied (spec section 1) or -999 sentinel if <UNKNOWN: Account ID>. Used at the bot top-level wrapper AND echoed into each intents[].AccountId and intentCategories[].AccountId (production pattern — v1.5.0).
AIModelConfigID and AIModel (= AIModelTypeId) come from the model catalog (model-catalog.md) per the spec section 1 entry. Skill 3 looks both up at emission time. -999 sentinels if catalog has TODO or spec marks unknown.
Allocation procedure:
- Walk section 4 in order. For each intent: assign
IntentId from the -10 series (every intent gets one). Assign BotIntentId from the -100 series only to intents whose **Bot-intent role:** is entry or global (the botIntents[] subset); chained intents get an IntentId but no BotIntentId. Cache <identifier> → IntentId for all, and <identifier> → BotIntentId for the subset.
- Within each intent's section 5 entry, walk slots in
Order value. For each slot: assign ParameterId from the -1000 series. Cache the mapping <intent identifier>.<slot name> → ParameterId.
- Emit
BotID = -1, BotVersionId = -2, IntentCategoryId = -3 as fixed values.
The cached mappings are used in §4.3 wherever an ID is referenced (transition rows, parameter parent-ID, api-silence relations, botIntents references).
Catalog intents (section 4.6) bypass placeholder allocation entirely. Their IntentId, IntentCategoryId, ParameterId, IntentScriptId, and any nested IDs are real platform-assigned positives and are copied through verbatim. Do NOT assign them -10/-1000/-3 placeholders and do NOT add them to the cached <identifier> → IntentId map used for the bot's own intents (they are referenced by real IntentId, not identifier).
The numerical ranges are wide so a human reading the JSON can identify what kind of ID a placeholder represents at a glance. Real platform-assigned IDs after import will be positive integers, so there's no collision risk on re-export.
4.2 Top-level wrapper and version envelope
4.2.1 Top-level fields (spec section 1 → root)
Emit fields in this order (matches production export — v1.5.0):
| Order | Spec field | Wire-format path | Source |
|---|
| 1 | Bot Name | <root>.Name | Direct copy |
| 2 | (allocated) | <root>.BotID | -1 |
| 3 | Account ID | <root>.AccountID | Direct copy, or -999 sentinel if <UNKNOWN> |
| 4 | (assembled) | <root>.intentList | §4.3 below |
| 5 | (constant) | <root>.BotStatusId | 1 (per Doc 1 §4) |
| 6 | (generated) | <root>.CreatedDate | ISO timestamp at assembly time, format "YYYY-MM-DD HH:MM:SS" |
| 7 | Description | <root>.Description | Direct copy |
| 8 | (constant) | <root>.BotLanguages | [] (preserved per §16) |
| 9 | (constant) | <root>.ModifiedDate | null |
| 10 | (resolved) | <root>.AiModelConfig | §4.2.3 below |
| 11 | (assembled) | <root>.ActiveVersionInfo | §4.2.2 below |
v1.5.0 wire-format correction. Prior baseline emitted intentList last and Description near the top. Production export places intentList at position #4 (right after AccountID). Skill 3 v1.5.0+ matches the production order.
4.2.2 ActiveVersionInfo envelope
Emit fields in this order (matches production — v1.5.0):
| Order | Wire-format path | Value |
|---|
| 1 | IsActive | 1 |
| 2 | CreatedDate | Same ISO timestamp as root |
| 3 | Description | "" (matches production samples) |
| 4 | BotVersionId | -2 (placeholder) |
| 5 | ModifiedDate | null |
| 6 | SystemPrompt | "" (preserved per §16; NOT the bot's actual system prompt, which lives in AIModelConfig.prompts) |
| 7 | AIModelConfig | §4.2.3 + 4.2.4 + 4.2.5 below |
| 8 | VersionNumber | "0.0.1" (per Doc 1 §5; v1 always emits this) |
| 9 | AIModelConfigId | Same value as <root>.AiModelConfig.AIModelConfigID (mirror) |
| 10 | BotVersionStatusId | 3 (per Doc 1 §5) |
v1.5.0 wire-format correction. Field order revised to match production. Prior baseline had BotVersionId first; production has IsActive first.
4.2.3 The two AIModelConfig objects
Doc 1 §6 defines two distinct objects with confusingly similar names. Both must be emitted. v1.5.0 wire-format correction: the prior "both created payloads must be identical" rule is replaced — production exports show the top-level catalog reference carries a much leaner created than the version-level. See below.
Top-level <root>.AiModelConfig (catalog reference; production export shape):
Emit fields in this order:
| Order | Field | Source |
|---|
| 1 | Name | From model-catalog.md "Display name" (e.g., "Gemini 3.1 - Voice driven" for AIModelConfigID=139) |
| 2 | ApiKey | {} (empty object, v1 default-public path) |
| 3 | AIModel | From model-catalog.md entry's AIModelTypeId (e.g., 18 for Gemini 3.1) — production exports denormalize this here under the field name AIModel |
| 4 | IsActive | 1 |
| 5 | AccountId | 0 (the reuse-existing-config switch per Appendix D §D.1; v1 always emits 0) |
| 6 | ModifiedBy | null |
| 7 | CreatedDate | ISO timestamp at assembly time |
| 8 | ModifiedDate | ISO timestamp at assembly time |
| 9 | AIModelConfig | Nested object (capital I, distinct from the lowercase-i parent name) containing only { "created": { "model": "<provider model string from model-catalog.md>" } } |
| 10 | AIModelConfigID | From model-catalog.md entry (e.g., 139); -999 sentinel if <UNKNOWN> |
v1.5.0 fields removed from the prior baseline: Description, BaseUrl, Type object, AIModelTypeId (the integer was kept but renamed to AIModel per production), full created payload (lives in the version-level object now).
Version-level <root>.ActiveVersionInfo.AIModelConfig (runtime config; production export shape):
Emit fields in this order:
| Order | Field | Source |
|---|
| 1 | max_duration | Spec section 1 **Max call duration:** (integer seconds; default 1200) |
| 2 | daily_limit | Spec section 1 **Daily limit:** (integer; default 600) — v1.13.0, golden-export field |
| 3 | dailyLimitLayerId | Spec section 1 **Daily limit layer:** (integer layer ID; default 3) — v1.13.0 |
| 4 | maxDurationLayerId | Spec section 1 **Max duration layer:** (integer layer ID; default 0 — v1.14.0, was 3; Skill 1 asks via the MCP layer list when connected) |
| 5 | daily_limit_sentence | Spec section 1 **Daily limit sentence:**; default (production-derived): "Sorry, but reached daily limit of calls duration, please try again later or contact the copany's support" — v1.13.0 |
| 6 | max_duration_sentence | Spec section 1 **Max duration sentence:**; default (production-derived, v1.14.0 — verbatim incl. trailing space): "נראה שהגענו לזמן שיחה מקסימלי, אנא נסה שנית " |
| 7 | IVRLayerSelect_2 | Spec section 1 **IVRLayerSelect_2:** (integer; default 3) — v1.13.0 |
| 8 | prompts | §4.2.4 below |
| 9 | recordAgentCalls | Spec section 1 **Record agent calls:** emitted as the STRING "false" / "true" (production format — not a JSON boolean) |
| 10 | silence_behaviour | §4.2.5 below; omitted entirely if spec section 3 is [not configured] |
| 11 | created | §4.2.4 below (the lean payload — voice + realtime input only) |
v1.5.0 fields removed from the prior baseline: tools: [] and instructions: "" at this level (production does not carry them). Reorder to match production.
v1.13.0 fields added (golden export בוט שיקוף – קבוצת קלי v0.0.17): daily_limit, dailyLimitLayerId, maxDurationLayerId, daily_limit_sentence, max_duration_sentence, IVRLayerSelect_2 — all siblings of max_duration at this level (NOT inside created). When a default is applied (spec field absent), list it in the banner's DEFAULTS APPLIED section. Layer-target defaults are split since v1.14.0: dailyLimitLayerId and IVRLayerSelect_2 stay at the golden-derived 3; maxDurationLayerId defaults to 0 and is user-chosen via the MCP layer list when connected (Skill 1 step 13). All are account-specific — the banner note lets the operator re-check them post-import.
4.2.4 The created payload (lean) and prompts bundle
created payload at the version level (ActiveVersionInfo.AIModelConfig.created) — v1.5.0 lean shape:
{
"realtimeInputConfig": {
"automaticActivityDetection": {
"disabled": "true"
}
},
"generationConfig": {
"speechConfig": {
"voiceConfig": {
"prebuiltVoiceConfig": {
"voiceName": "<voice from spec section 1>"
}
}
}
}
}
| Path | Source |
|---|
realtimeInputConfig.automaticActivityDetection.disabled | Always the literal string "true" (production format — not a JSON boolean) |
generationConfig.speechConfig.voiceConfig.prebuiltVoiceConfig.voiceName | Spec section 1 **Voice Name:** direct copy |
If no voice channel is active in section 1: omit the voiceConfig object entirely — keep only realtimeInputConfig. (No production sample for chat-only bots; this is the safest default.)
created payload at the top level (AiModelConfig.AIModelConfig.created) — even leaner:
{
"model": "<provider model string from model-catalog.md>"
}
Just the model string. No generation config, no system instruction, no voice config (the voice config lives in the version-level created).
v1.5.0 wire-format correction. Prior baseline emitted both created payloads as identical full Gemini Live setup objects (model + full generationConfig + systemInstruction + tools). Production exports show the two created payloads serve different purposes — the catalog reference carries only the model string; the runtime config carries only the realtime + voice. Both prior fields temperature, topP, topK, responseModalities, proactivity, thinkingConfig, systemInstruction, tools are dropped from emission.
Note on Compass doctrine rule 12 / check 10 interaction: the dropped fields are exactly the ones check 10 used to validate. Check 10 is rewritten in §6.2 to validate that no removed fields are re-added, rather than positively asserting them present. See §6.2 check 10 v1.5.0 description for the inverted regression-catching rule.
prompts bundle (ActiveVersionInfo.AIModelConfig.prompts) — unchanged from prior:
| Wire-format path | Spec source |
|---|
prompts.persona | Section 2.1 verbatim |
prompts.voiceInstructions | Section 2.2 verbatim |
prompts.chatInstructions | Section 2.3 verbatim |
prompts.intentInstructions | Section 2.4 verbatim (bot-level opening behavior — NOT per-intent) |
prompts.openingAnnouncement | Section 2.5 verbatim |
If the spec marks any prompts field <UNKNOWN>, emit "" and add the field path to the banner.
4.2.5 silence_behaviour (spec section 3, conditional)
If section 3 reads [not configured]: omit the entire silence_behaviour field from ActiveVersionInfo.AIModelConfig. Do not emit it as null, do not emit it as {}. Refua's production sample omits it entirely; Skill 3 follows that pattern.
If section 3 has its fields populated: emit them direct field-to-field.
| Wire-format path | Spec source |
|---|
silence_behaviour.intent | The IntentId of section 3's silence failover intent: (the intent to jump to when caller-silence loops are exhausted). Emit as the first key of the object (matches production shape). IMPORT LIMITATION (empirically confirmed 2026-06-23, Matan bot, AccountID 15832): the Voicenter import procedure remaps negative placeholders in intents[] / botIntents[] / intentRelations[] to real positive IDs, but it does NOT remap silence_behaviour.intent — a placeholder survives verbatim into the imported bot and the silence forward is blank in the UI until set by hand. Resolution rules (v1.14.0), in priority order: (1) if the failover names a section-4.6 catalog intent (user-supplied), emit its real positive IntentId verbatim — it survives import unchanged. (2) the normal case — the failover names a bot-own intent (the dedicated silence-forwarding intent Skill 1 always creates, or the user-chosen existing flow intent): emit its negative placeholder from the cached ID map, and add the MANDATORY banner line: silence_behaviour.intent is a pre-import placeholder the import procedure does NOT remap — after import, set the silence forward to <display name> in the UI (the target intent is identifiable by IsSilenceIntent: 1). (3) -999 + banner only if section 3 is unresolvable. Never emit as a string identifier; never omit when silence_behaviour is emitted. (v1.14.0 removed the pre-v1.14 "substitute canonical system global 19" mechanism — silence forwarding always targets a real, bot-own intent the user chose an outcome for.) Production proof of the real-id form post-import: the operator/משרד-התחבורה export carries silence_behaviour.intent: 7518. |
silence_behaviour.silence_duration | Section 3 silence_duration |
silence_behaviour.silence_loops | Section 3 silence_loops |
silence_behaviour.silence_sentence | Section 3 silence_sentence |
silence_behaviour.silence_ending_sentence | Section 3 silence_ending_sentence |
The silence_behaviour.intent failover is bot-level (caller silence regardless of active intent), distinct from each RT=2 intent's api_silence_behaviour.intent (API silence). Both are structural intent failovers; silenceRelations[] stays [] (the bot-level failover lives in this field, not a relations row).
4.3 intentList assembly (sections 4 + 5 → six parallel collections)
Per Doc 1 §8, intentList has six parallel collections wired by integer IDs. Skill 3 builds them from the cached ID map (§4.1) and section 4-5 content.
4.3.1 intents[]
For each section 4 intent (in order), build a 17-field entry per the v1.5.0 production-aligned shape. Emit fields in this order (matches production export):
| Order | Wire-format field | Spec source (or default) |
|---|
| 1 | Name | Section 4 "Display name" |
| 2 | IntentId | Cached <identifier> → IntentId placeholder |
| 3 | IsActive | 1 (always — v1.5.0 restored at intent root) |
| 4 | Priority | 1 (per Doc 1 §9.0) |
| 5 | AccountId | Spec section 1 **Account ID:** — same value as <root>.AccountID (v1.5.0 added) |
| 6 | Description | Section 4 "Description" |
| 7 | MaxAttempts | Section 4 explicit value if set; else 3 |
| 8 | IntentConfig | { prompts: { llmDescription: "", validationPrompt: <section 5 verbatim> }, additional: { max_turns: <see below>, sensitive: <section 4 or false>, max_turns_sentence: <see below> } } (v1.13.0 — additional block; llmDescription unchanged, always "") |
| 9 | IntentScripts | [] (empty array — per §16 quirk 8) |
| 10 | IntentSources | v1.5.0: per spec section 1 Channels Active. Voice active → [{ "SourceID": 1, "SourceName": "VOICE", "IntentSourceID": <placeholder from -4000 range> }]. Chat-only → []. Both channels → emit voice entry only for v1 (chat-only sample missing). Note: the production fixture shows mixed distribution — most intents in the transport-planner have IntentSources: [] even though voice is active, while one intent has the populated voice entry. v1.5.0 design decision 14 standardizes to populated voice entry for every intent on voice-active bots (the design intent of "channel-per-intent" semantics). |
| 11 | IntentToolName | Section 4 "Tool name" (= identifier) |
| 12 | IntentResponces | §4.4 below — invariant outer shape { IsActive: 1, ResponseTypeId, Configuration } |
| 13 | IsSilenceIntent | Integer 0/1. 0 by default; 1 if spec section 4 sets **IsSilenceIntent:** true (v1.14.0 — the dedicated silence-forwarding intent) |
| 14 | IntentCategoryId | -3 (the single default category placeholder) |
| 15 | IntentParameters | §4.3.2 below |
| 16 | ValidationTimeout | 30 (per Doc 1 §9.0) |
| 17 | HandlingInstructions | null (preserved per §16) |
IntentConfig.additional emission rules (v1.13.0 — replaces the pre-v1.13 sibling max_turns/max_turns_sentence placement):
IntentConfig.additional is emitted on EVERY bot-own intent (golden export בוט שיקוף – קבוצת קלי v0.0.17 carries it on every real intent) with three keys:
| Key | Default | Spec override |
|---|
max_turns | 5 for ALL RTs (v1.14.0 — replaces the pre-v1.14 RT=2 15 rule; both production reference exports carry 5 everywhere except conversation-heavy intents at 10) | Section 4 **Max turns:** (Skill 1 sets 10 autonomously on conversation-heavy intents — never user-prompted) |
sensitive | false (JSON boolean) | Section 4 **Sensitive:** (v1.14.0: true only on sensitive-collecting intents per Skill 1 §3.4.3) |
max_turns_sentence | "מתנצל אבל נראה שיש לי בעיה מסויימת, אנא נסה שנית מאוחר יותר" for ALL RTs (v1.14.0 masculine fallback — Skill 2 normally authors the gender-matched sentence into section 4) | Section 4 **Max turns sentence:** |
Shape: "additional": { "max_turns": 5, "sensitive": false, "max_turns_sentence": "" } — a sibling of prompts inside IntentConfig. Never emit max_turns / max_turns_sentence as direct siblings of prompts (the pre-v1.13 shape) — they live inside additional. When a default is applied, list it once in the banner DEFAULTS APPLIED section (aggregated, not per-intent).
Note on production distribution (v1.14.0 — replaces the v1.5.0 "RT=2 default 15" design decision 6). Both v1.14.0 reference exports (transport route-planner; supermarket cart agent) carry additional.max_turns: 5 on nearly every intent regardless of RT, with 10 only on conversation-heavy intents (product search; credit-card collection). The pipeline therefore standardizes to a uniform default of 5; the 10 upgrade is Skill 1's autonomous call via section 4 **Max turns:** — see spec-skeleton.md §4.
v1.5.0 changes from prior 14-field baseline: Reordered to match production. Added intent-root IsActive (always 1). Added intent-root AccountId. IsSilenceIntent now integer (was boolean). IntentSources shape includes SourceName and IntentSourceID (was [{ SourceID: 1 }]). max_turns / max_turns_sentence added with RT-conditional defaults (relocated into IntentConfig.additional in v1.13.0; RT-conditional defaults replaced by uniform 5 in v1.14.0).
v1.5.0 fields removed from prior baseline: intent-root IsDeleted (production never had it; the v1.4.1 correction removed it correctly — kept removed).
Catalog-intent injection (v1.11.0). After emitting the bot's own intents (above), append each section-4.6 catalog intent's **Definition:** JSON object to intents[] verbatim, in section-4.6 declaration order. No field is rewritten, reordered, or renumbered. (These intents already carry real IDs and a complete shape; Skill 3 is a pure conduit for them.)
4.3.2 IntentParameters[] (per intent, slot list)
For each slot in section 5, build a parameter entry. Emit fields in this order (matches production):
| Order | Wire-format field | Spec source (or default) |
|---|
| 1 | Name | Slot name |
| 2 | Schema | null (preserved literal — production constant) |
| 3 | IntentId | Cached <intent> → IntentId placeholder (parent backreference) |
| 4 | IsActive | 1 (integer) |
| 5 | CreatedBy | Spec section 1 **Created by:** value, or "" if not set |
| 6 | IsRequired | Integer 0/1. 1 if slot.IsRequired; else 0. Production format — not boolean. |
| 7 | ModifiedBy | " " (single literal space — production constant per parameter row) |
| 8 | OptionList | For ENUM: array of { Value, Label } pairs from spec. For non-ENUM: null (NOT [] — v1.5.0 correction) |
| 9 | CreatedDate | ISO timestamp at assembly time |
| 10 | Description | Section 5 slot description |
| 11 | ParameterId | Cached <intent>.<slot> → ParameterId placeholder |
| 12 | DefaultValue | Slot default if set; else "" (NOT null — v1.5.0 correction) |
| 13 | ModifiedDate | ISO timestamp at assembly time |
| 14 | ParameterType | Full nested object — see table below |
| 15 | CollectionOrder | Slot order (1-indexed) |
| 16 | ParameterTypeId | 1 / 10 / 16 / 19 per Doc 1 §12 |
| 17 | ValidationRules | {} (preserved per §16) |
ParameterType nested object — frozen constants per ParameterTypeId (v1.5.0):
{
"Name": "STRING",
"IsActive": 1,
"CreatedBy": "SYSTEM",
"ModifiedBy": null,
"CreatedDate": "2025-01-21 11:25:25",
"Description": "Basic text input",
"ModifiedDate": null,
"ParameterTypeId": 1,
"ValidationPattern": null,
"IsCustomValidationAllowed": 1
}
Per-type system-dictionary values (v1.13.0 — captured VERBATIM from production exports; never re-authored. STRING/BOOLEAN/ENUM/INTEGER/JSON verified against the golden export בוט שיקוף – קבוצת קלי v0.0.17; the pre-v1.13 BOOLEAN/ENUM rows were wrong "extrapolated" guesses and are corrected below):
| ParameterTypeId | Name | Description | ValidationPattern | IsCustomValidationAllowed | CreatedDate |
|---|
| 1 | "STRING" | "Basic text input" | null | 1 | "2025-01-21 11:25:25" |
| 4 | "INTEGER" | "Whole number input" | "^[0-9]+$" | 1 | "2025-01-21 11:25:25" |
| 10 | "PHONE" | "Phone number" | unverified | unverified | "2025-01-21 11:25:25" |
| 16 | "BOOLEAN" | "Yes/No input" | "^(true|false|yes|no)$" | 0 | "2025-01-21 11:25:25" |
| 19 | "ENUM" | "Selection from predefined options" | null | 0 | "2025-01-21 11:25:25" |
| 20 | "JSON" | "json schema" | null | 0 | "2025-04-10 09:50:42" |
Shared constants across all types: IsActive: 1, CreatedBy: "SYSTEM", ModifiedBy: null, ModifiedDate: null.
PHONE (10) is unverified — no production export in hand carries it. Until its dictionary row is captured from a real export or ParameterType.Data.sql, emit ValidationPattern: null, IsCustomValidationAllowed: 1 (the pre-v1.13 values) AND add a banner line: ParameterType PHONE block unverified against system dictionary — verify after import. Check 21 (§6) byte-matches every emitted ParameterType block against this table; unverified PHONE downgrades to the banner note instead of failing.
v1.5.0 fields removed from prior baseline: parameter-root IsDeleted (production doesn't carry it) and parameter-root ValidationPattern (it lives inside ParameterType now).
4.3.3 botIntents[]
Selective membership (v1.8.0). Emit an entry only for intents whose **Bot-intent role:** is entry or global. Skip chained intents entirely (default role; they are reached via intentRelations[]). Walk section 4 in order; emit the subset in that order. Emit fields in this order (matches production):
| Order | Wire-format field | Value |
|---|
| 1 | BotId | -1 (mirror of root; lowercase d per production casing) |
| 2 | DTMFList | [] (always emitted, never omitted) |
| 3 | IntentId | Cached <identifier> → IntentId placeholder (lowercase d per production) |
| 4 | IsActive | 1 (integer) |
| 5 | SortOrder | 0-based ordinal within the emitted subset, in section-4 order (first emitted → 0, second emitted → 1, …). Chained intents are skipped and do not consume an index. |
| 6 | BotIntentId | Cached <identifier> → BotIntentId placeholder (lowercase d) |
| 7 | BotVersionId | -2 (mirror of ActiveVersionInfo.BotVersionId; v1.5.0 added) |
| 8 | BotIntentTypeID | Role discriminator (Doc 1 §8.2 / G-10): entry → 1, global → 2. |
| 9 | ConditionGroupList | Populated by default with single entry (see below). v1.5.0 default reversed from prior []. |
Default ConditionGroupList content (emitted for every botIntents[] row):
[
{
"Order": 1,
"IntentConditionList": [],
"IntentConditionName": "",
"IntentConditionGroupID": "<placeholder from -3000 range, allocated for this botIntents row>",
"IntentConditionGroupType": 1,
"IntentConditionRelationID": "<same as this row's BotIntentId — mirror>",
"IntentConditionRelationType": 1,
"IntentConditionGroupTypeName": "tool",
"IntentConditionRelationTypeName": "BotIntentID"
}
]
v1.5.0 changes from prior baseline: BotID/IntentID capital-D casing changed to lowercase BotId/IntentId per production. DTMFList: [] added. BotVersionId: -2 added. SortOrder switched to 0-based. ConditionGroupList populated by default with the structural entry above.
v1.8.0 worked example (Noa). 9 intents, roles: handle_who_are_you/collect_inquiry_basics/handle_out_of_scope = entry, transfer_to_human = global, the other 5 = chained. botIntents[] emits 4 entries — SortOrder 0/1/2/3 over (9214 t1, 9217 t1, 9229 t2, 9235 t1) — and omits the 5 chained intents. See references/test-artifacts/bot-noa-2026-06-01.json.
Catalog-intent wiring (v1.11.0). A section-4.6 catalog intent wired silence-forward only emits NO botIntents[] row (free-floating). A catalog intent wired triggerable global emits a botIntents[] row with BotIntentTypeID 2, using its real BotIntentId if the 4.6 definition supplies one (else a -100-series placeholder). Like any authored global, it is reachable from anywhere via that type-2 registration — Skill 3 generates NO per-intent intentRelations[] edges to it (v1.12.0).
4.3.4 intentRelations[]
For each section 4 row's "Transitions out" list, build the set of (origin, next) pairs from the authored transitions only. No global fan-out (v1.12.0 — the v1.8.0 D4/D5 fan-out was removed): a global intent is reachable from anywhere by virtue of its botIntents[] type-2 registration (§4.3.3), so Skill 3 does not append per-intent edges to global intents (hangup, transfer-to-human, etc.). Emit only the transitions the author listed (an author may still list an explicit hand-off to a global; that authored edge is kept). Deduplicate by (OriginIntentID, NextIntentID) before emission, keeping the lowest Order value (the DB unique key forbids duplicates). Order is the 0-based position in the final deduped list for that origin.
Emit fields in this order (matches production):
| Order | Wire-format field | Source |
|---|
| 1 | Order | 0-based position in the transitions list (after dedup) |
| 2 | DTMFList | [] (always emitted) |
| 3 | NextIntentID | Cached <target identifier> → IntentId |
| 4 | OriginIntentID | Cached <origin identifier> → IntentId (capital-D casing per production) |
| 5 | IntentRelatedID | Unique row PK from placeholder range -2000, -2001, … (v1.5.0 — no longer mirrors NextIntentID) |
| 6 | ConditionGroupList | Populated by default with single entry (see below) |
Default ConditionGroupList content for intentRelations[]:
[
{
"Order": 0,
"IntentConditionList": [],
"IntentConditionName": "",
"IntentConditionGroupID": "<placeholder from -3000 range, allocated for this intentRelations row>",
"IntentConditionGroupType": 1,
"IntentConditionRelationID": "<same as this row's IntentRelatedID — mirror>",
"IntentConditionRelationType": 2,
"IntentConditionGroupTypeName": "tool",
"IntentConditionRelationTypeName": "RelatedIntentID"
}
]
Note the differences from botIntents[] ConditionGroupList: Order: 0 (vs 1), IntentConditionRelationType: 2 (vs 1), IntentConditionRelationTypeName: "RelatedIntentID" (vs "BotIntentID").
v1.5.0 changes: IntentRelatedID is now a unique row PK with its own placeholder range (was mirror of NextIntentID). Order is 0-based (was 1-based). DTMFList: [] always emitted. ConditionGroupList populated by default.
Section 4.7 pass-through rule (unchanged from prior). Section 4.7 opt-in lets the spec author override the default condition_groups and dtmf_list content. If present, Skill 3 lifts the YAML-style blocks verbatim into the corresponding JSON fields. If absent, the v1.5.0 defaults above apply.
Catalog-intent transitions (v1.12.0). A triggerable global catalog intent is reachable from anywhere via its botIntents[] type-2 registration, exactly like an authored global — Skill 3 generates NO per-intent intentRelations[] edges to it. A silence-forward only catalog intent participates in NO intentRelations[] rows.
4.3.5 intentCategories[]
Single default category, all intents reference it. Emit fields in this order:
| Order | Wire-format field | Value |
|---|
| 1 | Name | Spec section 1 **Bot Name:** value (v1.12.0 — was the hardcoded literal "Default Category"; each bot's own category now carries the bot's name so bots don't all collide on one "Default Category" entry in the account) |
| 2 | IsActive | 1 |
| 3 | AccountId | Spec section 1 **Account ID:** value — same as <root>.AccountID (v1.5.0 added) |
| 4 | PriorityId | 1 (v1.5.0 correction — was 2 in prior baseline; production has 1) |
| 5 | Description | Same as Name (production observation — v1.5.0 added) |
| 6 | IntentCategoryId | -3 (placeholder) |
v1.5.0 changes: BotID removed (production doesn't carry it). IsActive, AccountId, Description added. PriorityId corrected from 2 to 1.
Catalog-intent category merge (v1.11.0). For each section-4.6 catalog intent, add its category row (the full object: Name, IsActive, AccountId — typically 0, PriorityId, Description, IntentCategoryId) to intentCategories[], de-duplicated by IntentCategoryId. The bot's own -3 category (now named after the bot, v1.12.0) is always emitted; catalog categories (e.g. system category 22 "Sales intents", AccountId 0) ride alongside it. If the catalog intent's **Definition:** does not embed its category object, the author must supply it in the 4.6 block — Skill 3 does not synthesize category metadata.
4.3.6 silenceRelations[]
[] (per Doc 1 §8.5 + §16; v1 always empty).
4.3.7 apiSilenceRelations[]
For each RT=2 intent in section 4 ordering, emit one entry. The Configuration is the full content of the parent intent's IntentResponces.Configuration (v1.5.0 — was just the six silence_* fields in prior baseline).
| Wire-format field | Source |
|---|
Configuration | Deep copy of the parent RT=2 intent's IntentResponces.Configuration (every field: url, method, headers, body if any, fail_output, announcement, function_output, response_success, intentInstructions, intentLoadingAnnouncement, api_silence_behaviour) |
OriginIntentID | Cached <RT=2 intent identifier> → IntentId |
ApiSilenceIntentID | Cached <fallback intent from spec section 5> → IntentId |
v1.5.0 wire-format correction. Prior baseline emitted only the six silence_* fields here. Production shows the entire parent Configuration is copied — including url, method, body, the API-specific announcement, function_output, response_success, intentInstructions, etc. Skill 3 v1.5.0+ does a deep copy.
Cross-reference check 6 (§6.2) now validates full Configuration deep equality, not just the six fields. Since Skill 3 emits both from the same spec source, they match by construction; check 6 catches emission bugs.
If a non-RT=2 intent has API silence behavior in its section 5 entry, that's a Skill 2 bug — Skill 3 ignores it (RT determines whether the entry is emitted).
4.4 RT-specific IntentResponces.Configuration
Per intent, branch on Response Type (section 4) to assemble the correct Configuration shape. Doc 1 §11 has the per-RT field tables; the rules below codify Skill 3's behavior including unknowns.
IntentResponces outer shape — invariant across all RTs. Every IntentResponces object has the same four top-level keys in this order: IsActive (always 1), Configuration, ResponseTypeId, SuccessCondition (always the empty string "" on bot-own intents; §4.6 catalog blocks pass through verbatim). The per-RT tables below define Configuration's contents only — the IsActive and ResponseTypeId rows are repeated in each RT table as a reminder.
RT=1 — Layer Transfer (terminal)
| Wire-format field | Source |
|---|
ResponseTypeId | 1 |
IsActive | 1 (per §16 quirk #15 — required inside every IntentResponces) |
Configuration.layer | Section 5 "Layer" — the real layer number (fetched from the MCP during Skill 1, §2.4.A). Defaults to 0 (root layer) when the spec omits it. No -999 sentinel for layer (v1.12.0 — 0 is a valid landing layer, a deliberate exception to fail-loud; see anti-list §"Suppress fail-loud sentinels"). |
Configuration.announcement | Always omitted (v1.14.0 hard rule). RT=1 never carries an announcement key — the farewell lives in the PREVIOUS intent's intentInstructions (FP-8 farewell trigger rule; production: every layer-transfer intent has only intentLoadingAnnouncement). If a legacy spec supplies one, that is a check-20 failure, not an emission choice. |
Configuration.intentLoadingAnnouncement | Section 5 "Loading announcement" verbatim. Always emitted for RT=1 — the terminal's only utterance, a short "יום טוב"-style line. |
RT=1 intents do not emit intentInstructions (post-execution behavior on a terminal intent has no meaning per Doc 1 §11.5).
Terminal doctrine (FP-8, v1.14.0) — one RT=1 terminal per outcome, owning its outcome slot, no terminal→anything relations, no announcement (farewell on the predecessor) — is validated by cross-reference check 20 (§6).
RT=2 — API Call
| Wire-format field | Source |
|---|
ResponseTypeId | 2 |
IsActive | 1 (per §16 quirk #15 — required inside every IntentResponces) |
Configuration.url | Section 5 "URL"; <USER_TO_FILL: webhook_url> if <UNKNOWN> |
Configuration.method | Section 5 "Method" ("POST" or "GET") |
Configuration.headers | Section 5 "Headers" object; {} if not specified |
Configuration.fail_output | Section 5 verbatim |
Configuration.announcement | Section 5 "Announcement (after API success)" verbatim. v1.5.0 — renamed from apiResponseAnnouncement in prior baseline. |
Configuration.function_output | Section 5 "Fail-output fallback map" — object shape { "default": "<fallback string>" } (v1.5.0 — was a string of LLM guidance in prior baseline). User may extend with per-code keys; Skill 3 passes the object through verbatim. |
Configuration.response_success | Section 5 "Response success instructions" — object shape { "instructions": "<string>" } (v1.5.0 — was bare string in prior baseline). |
Configuration.intentInstructions | Section 5 "Post-Execution Intent Instructions" verbatim |
Configuration.api_silence_behaviour | Spec section 5 "API silence behavior" — the six-key object defined in §4.4.1 below (the intent key is the resolved failover IntentId — mandatory, never omit). Same content as apiSilenceRelations[].Configuration.api_silence_behaviour (cross-reference check 6 validates). |
Configuration.intentLoadingAnnouncement | Section 5 "Loading announcement" verbatim |
4.4.1 The api_silence_behaviour object — exact shape
Every RT=2 intent's Configuration.api_silence_behaviour is an object with these six keys (emit all six; this object is the intent-failover contract — if intent is missing the call has no fallback when the caller goes silent during the webhook):
| Key | Source | Notes |
|---|
intent | Cached <fallback intent from spec section 5 "API silence behavior"> → IntentId (integer) | The failover intent. Resolve the spec's fallback intent: identifier to its IntentId, exactly as apiSilenceRelations[].ApiSilenceIntentID is resolved (§4.3.7) — the two MUST be the same integer. -999 sentinel if the fallback intent is <UNKNOWN>. Never emit as a string identifier; never omit. |
silence_loops | Section 5 silence_loops: (integer) | |
silence_duration | Section 5 silence_duration: (integer seconds) | |
silence_sentence | Section 5 silence_sentence: verbatim | |
silence_instructions | Section 5 silence_instructions: verbatim ("" if empty) | |
silence_ending_sentence | Section 5 silence_ending_sentence: verbatim | |
Because api_silence_behaviour.intent and apiSilenceRelations[].ApiSilenceIntentID are both resolved from the same spec fallback intent: field, they are equal by construction. Cross-reference check 6 (full Configuration deep equality) catches any drift between the inline copy and the registry copy; cross-reference check 3 confirms the resolved ApiSilenceIntentID endpoint exists in intents[].
v1.5.0 wire-format corrections (RT=2):
apiResponseAnnouncement → renamed announcement (production field name).
function_output → object { "default": "<string>" } instead of bare string.
response_success → object { "instructions": "<string>" } instead of bare string.
IntentLoadingAnnouncement (capital I) — removed. Prior baseline emitted both lowercase and capital-I as a "casing-bug pair." Production exports of Gemini 3.1 Voice driven bots carry only the lowercase form. v1.5.0 emits only intentLoadingAnnouncement.
- Empty-string runtime tolerance (voice-agent-llm v1.0.3+):
announcement may be empty at runtime — the service substitutes [START THE CONVERSATION] as an LLM instruction (bot opens from persona; the literal string is not spoken aloud). Skill 3 emits whatever the spec contains verbatim. No new validation rule — Skill 2's Check 10 still requires authored text upstream.
The api_silence_behaviour sub-object inside Configuration and the corresponding apiSilenceRelations[].Configuration (now a deep copy of the entire Configuration, not just the six fields) must be content-identical — Skill 3 emits both from the same spec source.
RT=3 — Continue
| Wire-format field | Source |
|---|
ResponseTypeId | 3 |
IsActive | 1 (per §16 quirk #15 — required inside every IntentResponces) |
Configuration.announcement | Section 5 "Announcement" verbatim (may be the empty string when the spec logs the FP-3 intentional-empty exception) |
Configuration.response_success | Section 5 "Response success instructions" — object shape { "instructions": "<string>" } (v1.5.0 — was bare string). |
Configuration.intentInstructions | Section 5 "Post-Execution Intent Instructions" verbatim |
Configuration.intentLoadingAnnouncement | Section 5 "Loading announcement" verbatim — always emitted for RT=3 (v1.13.0, FP-7). Skill 2 check 12 guarantees it is authored non-empty; Skill 3 check 17 backstops (an unset value would produce the default "." SAY directive bug at runtime). |
v1.5.0 wire-format correction (RT=3): response_success is now an object { "instructions": "<text>" }, not a bare string.
v1.13.0 wire-format addition (RT=3): intentLoadingAnnouncement added (golden-export field; key order per the golden export: announcement, response_success, intentInstructions, intentLoadingAnnouncement).
RT=4 — Dial-Out
RT=4 has two operating modes selected by section 4 **Dial source:**. Both modes emit the same Configuration shape; specific fields are populated or left empty per mode.
| Wire-format field | Source |
|---|
ResponseTypeId | 4 |
IsActive | 1 (per §16 quirk #15 — required inside every IntentResponces) |
Configuration.phone1 | Section 4 **Phone1:** (E.164 with leading +) when dial-source=static; "" when dial-source=parameter |
Configuration.phone2 | Section 4 **Phone2:** when dial-source=static; "" when dial-source=parameter |
Configuration.phone3 | Section 4 **Phone3:** when dial-source=static; "" when dial-source=parameter; <USER_TO_FILL: phone3> if static and <UNKNOWN> |
Configuration.parameter_phone | Section 4 **Parameter phone:** (slot identifier) when dial-source=parameter; key omitted when dial-source=static |
Configuration.selectdial_option | Section 4 **selectdial_option:** — literal string "Parameter" when dial-source=parameter; key omitted (or set to user's literal value) when dial-source=static |
Configuration.NEXT_VO_ID | Section 4 **NEXT_VO_ID:** (int); -999 sentinel if <UNKNOWN> |
Configuration.MAX_DIAL_DURATION | Section 4 **MAX_DIAL_DURATION:** (int seconds) |
Configuration.record | Section 4 **Record:** (boolean) |
Configuration.announcement | Section 4 **Announcement:** verbatim; key omitted if absent in spec |
Configuration.intentLoadingAnnouncement | Section 4 **Loading announcement:** verbatim; key omitted if absent |
Configuration.intentInstructions | Section 4 **Post-execution intent instructions:** verbatim; emit "" if absent (parallel to RT=2/RT=3 §16 convention) |
Configuration.response_success | Section 4 **Response success:** object (e.g., { "instructions": "<text>" }); emit {} if absent |
Empty-phone handling. A spec entry of "" for Phone1, Phone2, or Phone3 is preserved as "" in the JSON — the dialer's runtime contract is "try in order, skip empties." Do not coerce empty phones to null and do not collapse the keys.
4.5 Quirk preservation
Walk Appendix A. For every quirk in the table, ensure the assembled wire structure has the exact form prescribed. This is a verification pass against the in-memory structure — if any quirk is absent or mis-emitted, that's a Skill 3 implementation bug, halt and report.
In normal operation, §4.2-4.4 already produce all quirks correctly. §4.5 is the verification gate that catches drift between the emission code and the §16 contract.
The full checklist is in Appendix A (rows 2, 5, 6, 7 marked REMOVED/CORRECTED; rows 16-19 added in v1.5.0; rows 20-23 added in v1.13.0; row 24 added in v1.14.0).
4.6 Sentinel emission for unknowns
Walk spec section 7.4. For each unknown marker, the corresponding wire-format field has already received a sentinel during §4.2-4.4. §4.6 is the bookkeeping pass:
- Build the sentinel inventory: for each
<UNKNOWN: ...> marker in section 7.4, identify the wire-format JSON path that received the sentinel and the sentinel value emitted.
- Build the disagreement list: any
<UNKNOWN: ...> in section 7.4 that did not produce a sentinel (Skill 1/2 staged the unknown but Skill 3 didn't find a wire-format slot for it), or any sentinel emitted at §4.2-4.4 that is not in section 7.4 (Skill 3 found an unknown the spec didn't track).
- Both lists feed the banner (§7.2). The sentinel inventory is the user's pre-import checklist; the disagreement list is a soft warning about spec/skill drift.
Sentinel format reference:
| Spec marker shape | Wire-format emission |
|---|
<UNKNOWN: webhook_url> (string field) | "<USER_TO_FILL: webhook_url>" |
<UNKNOWN: NEXT_VO_ID> (integer field) | -999 |
<UNKNOWN: phone destination> (string) | "<USER_TO_FILL: phone3>" |
<UNKNOWN: Account ID> (integer ID) | -999 |
<UNKNOWN: AIModelConfigID> (integer ID) | -999 |
<UNKNOWN: AIModelTypeId> (integer ID) | -999 |
<UNKNOWN: AI Model Config> (string name → triggers all model fields unknown) | <USER_TO_FILL: ...> for strings, -999 for IDs across the whole AiModelConfig block |
<UNKNOWN: <some object field>> (object) | {} plus a banner note |
<INCOMPLETE: ...> (section partial) | Section emitted with available content; banner notes incompleteness |
[not configured] (whole-section omission) | Section omitted from JSON entirely |
The sentinel value carries the field role (webhook_url, phone3, model_config_name) inside the placeholder text, so the banner's path-plus-value listing is self-documenting.
5. Section 6 regeneration sanity check
After §4 assembly completes and before §6 cross-reference pass: regenerate spec section 6 (subsections 6.1–6.5) from sections 4-5 fully. Compare to the spec's existing section 6.
| Subsection | Regeneration source |
|---|
| 6.1 Mustache variable usage | Walk every text field across sections 2 and 5 + section 4 RT=2 body; for each Mustache reference, record (reference, location, resolution source). |
| 6.2 Intent transition graph | Flatten section 4 transitions out → list of (origin → next) pairs, deduped. Authored transitions only (v1.12.0 — no global fan-out); this matches the emitted intentRelations[] and Skill 1's section 6.2, so no spurious drift is reported. |
| 6.3 RT=2 API silence pairings | For each RT=2 intent, the apiSilenceRelations[] registry entry that pairs with its embedded api_silence_behaviour. |
| 6.4 Escalation paths | For each non-terminal intent, the transition row that points to escalation. |
| 6.5 ID assignments | The <identifier> → IntentId mapping built in §4.1. |
Comparison logic: subsection-by-subsection diff. Differences are normalized for whitespace and ordering before comparison — section 6 in spec is allowed to list entries in any order, since it's derivative.
Drift handling: soft warning, not blocking. Section 6 is derivative; sections 4-5 are authoritative. If the regenerated 6 differs from the spec's 6, that's a signal that either (a) sections 4-5 were edited inconsistently after section 6 was generated (e.g., manual edits between Skill 1/2 invocations), or (b) Skill 1/2's section 6 update logic has a bug. Skill 3 doesn't auto-fix and doesn't block emission. It records the drift in the banner and section 7.3 generation log.
The banner section "DRIFT NOTES" lists each subsection that drifted, with a one-line summary (e.g., 6.1: regenerated had 3 references the spec missed; 6.2: spec had 1 transition that no longer exists in section 4).
If the user cares enough about the drift to fix it, they invoke Skill 1 patch mode (which regenerates section 6 cleanly). Otherwise the JSON is still emitted from the authoritative sections 4-5.
6. The §15.4 cross-reference pass
After §4 assembly and §5 sanity check: run all twenty-three checks — eight per Doc 1 §15.4, three from Compass doctrine integration per ../../references/voice-prompt-doctrine.md, three botIntents-role integrity checks (11–13, v1.8.0; the v1.8.0 fan-out-completeness check was removed in v1.12.0 when global fan-out was dropped), one duplicate-global-intent check (15, v1.12.0), and eight field-placement doctrine checks (16–23, v1.13.0/v1.14.0) per ../../references/field-placement-doctrine.md. Checks 1–7, 11–13, 15, and 16–21 are blocking; 8 is advisory/blocking by token band; 9 advisory; 10 blocking on mismatch; 14, 22, and 23 are non-blocking advisory. (The 1–7 blocking status is per locked decision C.) Failure of any blocking check halts emission.
6.1 Order, timing, what each check operates on
The pass operates on the assembled in-memory wire structure, not on the spec (checks 16–23 additionally consult the spec's section-4 staggering/terminal/role fields, the 4.5 variable inventory, and — for check 23 — the persona text). Sentinel values (-999, <USER_TO_FILL: ...>) are present at this point — they are not treated as missing references for the ID-resolution checks (1-4). The ID-resolution checks operate on placeholder integers (the negative-integer cache), which are internally consistent by construction; sentinel -999 only appears in user-supplied ID fields (AccountID, layer, NEXT_VO_ID), which are not the subject of any §15.4 check.
Run order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 11 → 12 → 13 → 15 → 16 → 17 → 18 → 19 → 20 → 21 → 22 → 23 → 8 → 9 → 10 → 14. All checks run unconditionally (no short-circuit on first failure) so the user gets a complete failure report rather than fixing one issue at a time. Checks 11–13, 15, and 16–23 are model-agnostic (unlike 8–10, which gate on the Gemini 3.1 model). Checks 8, 9, 10 are gated on AiModelConfig.created.model being models/gemini-3.1-flash-live-preview; if the model is different they skip silently (one-time per-spec log entry to section 7.3).
6.2 The checks
| # | Check | What it validates | Detection |
|---|
| 1 | botIntents[].IntentID resolves | Every botIntents[].IntentID matches an intents[].IntentId. | Build the set of intents[].IntentId values; for each botIntents[i].IntentID, verify membership. |
| 2 | intentRelations[] resolves (both endpoints) | Every OriginIntentID and NextIntentID matches an intents[].IntentId. | Same set; verify membership for both endpoint fields. IntentRelatedID is not checked separately (it's a unique row PK from the -2000 placeholder range per §4.1; verified by the placeholder allocator). |
| 3 | apiSilenceRelations[] resolves (both endpoints) | Every OriginIntentID and ApiSilenceIntentID matches an intents[].IntentId. | Same set; verify membership. |
| 4 | intents[].IntentCategoryId resolves | Every intents[].IntentCategoryId matches an intentCategories[].IntentCategoryId. | v1 has a single category (-3); check is trivial but explicit. |
| 5 | RT=2 has apiSilenceRelations[] pairing and an inline failover intent | Every intent with IntentResponces.ResponseTypeId = 2 has (a) a corresponding apiSilenceRelations[] entry where OriginIntentID matches the intent's IntentId, and (b) a Configuration.api_silence_behaviour.intent that is a present, non-null integer equal to that entry's ApiSilenceIntentID. | Walk RT=2 intents; for each, verify the row exists AND the inline intent failover key is present and matches ApiSilenceIntentID. A missing/null/string intent is a blocking failure — the intent has no failover. |
| 6 | IntentResponces.Configuration matches apiSilenceRelations[].Configuration | For each RT=2 intent, the full content of IntentResponces.Configuration equals the corresponding apiSilenceRelations[].Configuration content (v1.5.0 — was just the six silence_* sub-fields in prior baseline). | Deep equality across every key in the parent intent's Configuration: url, method, headers, body (if any), fail_output, announcement, function_output, response_success, intentInstructions, intentLoadingAnnouncement, AND the nested api_silence_behaviour sub-object (all six keys: the failover intent plus silence_loops, silence_duration, silence_sentence, silence_instructions, silence_ending_sentence). |
| 7 | Mustache resolvability | Every Mustache reference (in any text field across the assembled structure) resolves: (a) collected by the same intent that uses it, OR (b) in 4.5.1+4.5.2 whitelist (call-context or env), OR (c) in 4.5.3 collected by an intent that is upstream of the using intent in the transition graph, OR (d) in 4.5.4 declared for the same RT=2 intent or an upstream RT=2 intent, OR (e) in the 4.5.5 CustomData key list (v1.13.0, FP-11). | Walk every text field; extract Mustache tokens; for each, classify against (a)-(e). Failure message appends: CustomData keys are never invented — if {{<name>}} is a real per-call key, add it to spec 4.5.5 via Skill 1 patch mode. |
| 8 | Assembled-prompt token budget (Compass rule 1) | Estimated token count of the assembled systemInstruction-equivalent text (bot-level prompts + per-intent validationPrompt + per-intent post-exec intentInstructions, excluding openingAnnouncement) is below the doctrine thresholds. Advisory at 1,500–4,999; blocking at ≥ 5,000 (forced decomposition at ≥ 6,000). Applies only when AiModelConfig.created.model is gemini-3.1-flash-live-preview (gating per Compass rule 1). | Run the char-based token estimate per references/voice-prompt-doctrine.md §2. Banner-report the count and band. Halt on ≥ 5,000. |
| 9 | Session-resumption ceiling (Compass rule 2) | If spec section 1 declares cross-session continuity is required, the assembled systemInstruction is under 200 tok. Advisory. Same gating as check 8. | Same token estimate as check 8. Banner-only if continuity not required. |
| 10 | Model-config doctrine (Compass rule 12 — v1.5.0 inversion) | When <root>.AiModelConfig.AIModelConfig.created.model is models/gemini-3.1-flash-live-preview: validate that the version-level AIModelConfig.created does NOT contain any of the dropped fields (temperature, topP, topK, responseModalities, proactivity, thinkingConfig, systemInstruction, tools, affectiveDialog, proactiveAudio, thinkingConfig.thinkingLevel != "minimal"). The lean payload from §4.2.4 has none of these by construction; check 10 catches future regressions. Blocking on any presence. | Inspect the assembled in-memory ActiveVersionInfo.AIModelConfig.created. The expected keys are exactly realtimeInputConfig and (when voice active) generationConfig.speechConfig.voiceConfig.prebuiltVoiceConfig.voiceName. Any other key under generationConfig is a failure. |
| 11 | Global registered as type-2 (C-a) | Every intent with role global has a botIntents[] entry with BotIntentTypeID = 2. | Build the set of global identifiers from section 4; for each, verify a botIntents[] entry exists with that IntentId and BotIntentTypeID = 2. |
| 12 | No chained intent in botIntents (C-c) | No intent with role chained appears in botIntents[]. | For each botIntents[] entry, verify its source intent's role is entry or global. |
| 13 | Start point exists (C-d) | At least one entry or global intent exists (otherwise the bot has no top-level trigger). | Assert botIntents[] is non-empty and contains ≥1 type-1 or type-2 entry. |
| 14 | Section-4.6 catalog intents resolve | Every catalog intent referenced by section 3 (silence_behaviour.intent) or any structural failover field is present in the emitted intents[] by real IntentId, AND its IntentCategoryId is present in intentCategories[]. Non-blocking advisory (the §4.6 parse already guaranteed structure; this catches a reference to an undeclared catalog id). | Walk every failover intent field that resolves to a catalog IntentId; verify presence in intents[] and intentCategories[] by real ID. |
| 15 | No duplicate global intents by tool name (C-e) | For each unique IntentToolName value across section 4, at most one intent with that tool name may have role global (registered in botIntents[] with BotIntentTypeID = 2). Multiple global intents with the same tool name cause duplicates in the UI. Blocking check. | Build a map { IntentToolName → [intent1, intent2, ...] } for all intents with role global (those in botIntents[] with type 2). For each tool name with count > 1, it's a blocking failure listing the duplicate intent identifiers. |
| 16 | validationPrompt speech-free (v1.13.0, FP-5) | No IntentConfig.prompts.validationPrompt contains imperative speech content — scripts, questions to the caller, greetings, or turn-taking guards. The Intent Agent is the only consumer; anything written to be spoken there is never spoken. Blocking. | Per validationPrompt, per line: (i) imperative-speech regex `(?im)^\s*\W*(say |
| 17 | RT=3 intentLoadingAnnouncement present (v1.13.0, FP-7) | Every RT=3 intent's Configuration.intentLoadingAnnouncement is present, non-empty, not whitespace-only, and not the literal "." (the default SAY-directive bug). Blocking. | Walk RT=3 intents; test the field. |
| 18 | Own-parameter references (v1.13.0, FP-8) | No intent's validationPrompt, Configuration.announcement, or Configuration.intentInstructions references a parameter name that belongs to a DIFFERENT intent — an intent can only set its own IntentParameters; foreign references (e.g., a gate "setting" a terminal's status slot) are un-executable. Blocking. | Build the bot-wide set of all IntentParameters[].Name values with their owning IntentIds. For each intent, scan the three fields for word-boundary matches of any slot name; any match whose owner is a different intent fails, reporting intent, field, matched name, and the owning intent. |
| 19 | No duplicate speak-obligation (v1.13.0, FP-6) | No normalized speech obligation appears in two or more obligation sites — the diagnosed mechanism of double-speech bugs (e.g., a farewell in both a terminal's announcement and another field). Blocking. | Extract mandated-speech strings: sentences of every announcement, every intentLoadingAnnouncement, sentences of prompts.openingAnnouncement, and FP-4 quoted lines (: "<...>") inside per-intent Configuration.intentInstructions, prompts.intentInstructions, and prompts.persona. Normalize each (trim; strip punctuation and niqqud; collapse whitespace). Any normalized string ≥ 12 characters appearing in 2+ sites fails, reporting both JSON paths. |
| 20 | Terminal shape (v1.13.0, FP-8; announcement clause added v1.14.0) | Every RT=1 terminal: has Configuration.layer (0 allowed — banner-noted); carries NO Configuration.announcement key (v1.14.0 — the farewell lives in the predecessor's intentInstructions; a spec-supplied RT=1 announcement is a failure, not an emission choice); when the spec declares **Terminal outcome:**, the named slot exists in that intent's IntentParameters AND the validationPrompt implements the declared value mode (fixed mode ⇒ the exact pinned string appears verbatim; captured/dynamic ⇒ a save/compose instruction naming the slot exists); and NO intentRelations[] row has an RT=1 intent as OriginIntentID (no terminal→anything chains, incl. finalize→end_call). Blocking. | Walk RT=1 intents against the spec section-4/5 fields and the relations array; assert the announcement key is absent from every RT=1 Configuration. |
| 21 | ParameterType dictionary byte-match (v1.13.0) | Every emitted ParameterType object on bot-own intents matches the §4.3.2 system-dictionary table field-for-field (Name, Description, ParameterTypeId, ValidationPattern, IsCustomValidationAllowed, IsActive, CreatedBy, CreatedDate, ModifiedBy, ModifiedDate). System dictionary rows are copied verbatim, never re-authored. Blocking — a mismatch is a Skill 3 emission bug. Carve-outs: §4.6 catalog-intent blocks are verbatim pass-through (excluded); unverified PHONE downgrades to a banner note. | Compare each emitted block against the §4.3.2 table. |
| 22 | No authored edges into type-2 globals (v1.13.0, FP-9) | Advisory. intentRelations[] rows whose NextIntentID is a type-2 global are legal but usually redundant — globals are reachable from anywhere by construction, and extra edges enlarge the tool-routing surface. | List any relation targeting a botIntents type-2 IntentId; banner line recommending removal via Skill 1 patch mode. |
| 23 | Off-topic global present (v1.14.0, FP-6) | Advisory. Every bot should carry the mandatory off-topic handling pair: (a) at least one RT=1 intent registered type-2 in botIntents[] whose Description/Name marks it as the off-topic/unrelated-topic terminal, AND (b) a prompts.persona off-topic section (forbid + deflect + N-loop ending) that references that intent's Description. Missing either half means the bot has no escape hatch when a caller won't return to the flow. | Scan botIntents[] type-2 entries' source intents (RT=1) for off-topic semantics in Description/Name (e.g., "unrelated", "לא קשור"); scan prompts.persona for an off-topic rule and match its routing target against that Description. On miss: banner line routing to Skill 1 patch mode (§3.2.5 elicitation). |
Check 7 specifics — the dotted-path validation depth:
| Mustache shape | How resolution works |
|---|
{{slot_name}} | Match against 4.5.1, 4.5.2, or 4.5.3. For 4.5.3 (slot variables), the slot must be collected by the same intent OR by an intent upstream in the transition graph. Cousin intents (no path either way) are violations. |
{{response.foo.bar}} or {{available_slots.N.field}} | Match against 4.5.4 dotted-path declarations. The owning RT=2 intent must be the using intent itself OR an upstream RT=2 intent in the transition graph. Downstream or cousin = violation. |