The Dual Voice Architecture
Essay 7.3 — The Plugin Kit, Part 3 of 9.
Essay 7.2 opened the universal skeleton — CLAUDE.md, hooks, scripts — and named the PLUGIN-LOCK ceremony that gates every edit to them. This sub-essay opens the organ that almost every plugin doubles: voice.xml. The hooks-side and scripts-side surfaces share one XML schema across different audiences. Getting them confused is the most common new-user error in plugin authoring.
voice.xml × Two — The Dual Surface
This is the organ that confuses new users most, and the one where the relational anatomy matters most.
The two voice surfaces are different files with different audiences. hooks/voice.xml and scripts/voice.xml look nearly identical structurally (both XML, both with <coaching>, <block>, <info>, <entry> elements identified by id), but they serve different consumers. ⓘ
hooks/voice.xml — The Agent-Facing Surface
What it is. Strings the plugin emits at hook fire-time. Coaching voices that nudge the agent at a specific moment (entering a phase, crossing a context tier, having just dispatched a subagent). Blocks that refuse the agent when a guard fires. ⓘ
Who reads it. The LLM agent. Voices delivered via hooks land in the agent’s context window — either as soft injections via additionalContext JSON (coaching) or as exit-2 stderr refusals (blocks). The LLM sees the string mid-turn and reacts. ⓘ
Who writes it. Mostly CONDENSE step 4 (which consumes [VOICE-UPDATE] markers other phases emit). The historian subagent also updates voice files when the plugin’s evolution requires new coaching. PLUGIN-LOCK is the underlying gate for any edit. ⓘ
What it depends on. The shared lib/voice-helper/ helper (the get_voice function that loads voice.xml entries and substitutes {{var}} placeholders). ⓘ
scripts/voice.xml — The Operator-Facing Surface
What it is. Strings the plugin’s CLI prints to the operator’s terminal. When safe-lock.sh reverts a test failure, the operator sees a short status line in their terminal. The text is what the operator’s eyes read in the shell. ⓘ
Who reads it. The human operator. Terminal output. Not injected into the LLM’s context — printed to stderr or stdout of the shell, visible only to the person at the keyboard. ⓘ
Who writes it. Same ceremony as hooks/voice.xml.
What it depends on. Same voice-helper.
Why the split matters
Same intent — both surfaces carry the plugin’s voice. Different audiences — the LLM consumes structured paragraphs that frame the situation and propose action; the human consumes status lines that flag what happened. Wording often differs between the two for the same conceptual event. Auditor scripts that grep voice ids have to look in both files; auditing only one false-positives every id that lives in the other. ⓘ
Soft vs hard at the element level. Inside each voice.xml, the <coaching> element produces a context injection — probabilistic, the LLM may absorb or ignore it. The <block> element produces a refusal — exit-2 stderr that fails the agent’s tool call. The two element types coexist in the same file. The Lock-13 over-engineering veto says: new behavioral controls start as coaching; only when measurement shows coaching consistently fails does the control harden into a block. ⓘ
The yaml-injection pairing. When a job reaches stage 3 of maturation (multi-cycle with .yaml plan, covered in Essay 8), the .yaml file’s per-phase fields pair with voice ids by convention. The yaml field name maps to a voice id directly; voice-helper appends the matched value to the rendered voice text at phase entry. New yaml fields require no parser change — just add the matching voice id to the plugin’s hooks/voice.xml and the seed agent picks up the new pairing on the next phase entry. ⓘ
The new-plugin lens. When you guide your seed to add coaching for a new behavior, the seed writes the coaching string to hooks/voice.xml first (cheap, soft). If the operator later observes the coaching consistently fails to hold, the seed authors a <block> element in the same file for the hard variant — same id namespace, harder enforcement. The seed never invents a third voice surface. The split between LLM-facing and operator-facing voice surfaces is structural. ⓘ
A consulting firm’s seed agent could carry the same dual-voice split: hooks/voice.xml coaches the agent on deliverable-checklist enforcement; scripts/voice.xml prints terminal status to the consultant when deliverable.sh validate runs. Same schema, the agent-facing and operator-facing surfaces, one ceremony. ⓘ
The hooks-side and scripts-side surfaces share one schema. The LLM consumes the agent-facing surface; the operator consumes the CLI-facing surface. The next sub-essay opens the organ that almost every plugin needs but that no plugin lets anyone else touch — the private data.json state.
Essay 7.3 — The Plugin Kit, Part 3 of 9.
Previous: Essay 7.2 — Skeleton: CLAUDE.md, Hooks, and Scripts — the universal organs governed by PLUGIN-LOCK. Next: Essay 7.4 — data.json — The Hidden State — per-plugin private state, script-mediated.
Comments