# P2 · arrangement, render, agent P2 turns the loop into a thing that can be played, rendered and performed with an agent. It is delivered in three slices. ## P2a · the arrangement Four 2-bar sections (A–D) form an 8-bar loop. Each section is a full `Pattern` (5 rows × 16 steps, with per-row faces). The engine walks 64 steps, picks the section and the pattern step, and triggers the section's rows; the bar clock runs 1–8. The state carries `arrangement: [Pattern; 4]`, `active_section`, and `pattern` = the active section for the editors. Commands: `select_section { index }`, `clear_section { index }`, `copy_section { from, to }`, event `arrangement { sections, active }`. Pattern edits apply to the active section. Select is persisted but not undoable; clear/copy push history. Sections, the active index and their history are stored in SQLite (`arrangement`, `active_section` columns, migrated with `ensure_column`) and restored on load. Surface: the quarry gets a section strip (A–D with note counts, copy →, clear); the pattern grid edits the active section. The descent draws the 64-step road, bar labels 01–08, section seams and letters A–D, with the active section emphasized; the playhead follows the 8-bar clock. Verification: - `cargo test -p zhila-core arrangement`: sections independent, select/copy/clear, undo, material carries four sections, loop_bars = 8. - `node scripts/persistence-smoke.mjs`: imports, edits section B, copies A→C, cuts, restarts, verifies the arrangement, active B, B's step 0 = 0, C = A, and undo/redo. - `npm --prefix ui run arrangement-smoke`: four sections, select B, edit a step, switch to the nave, play, screenshot `preview-p2-arrangement.png`. Limits: exactly four sections of two bars; no insert/reorder/length; select is not undoable; no per-section mute. ## P2b · render `zhila_engine::render_arrangement(RenderRequest)` drives the same `AudioState` offline: no cpal device, the same voices, sample faces, gains, master and the 8-bar sections, plus a 1.5 s tail. `Command::render` is intercepted by the host, rendered in `spawn_blocking`, written with `hound` as 48 kHz stereo 32-bit float to `/renders/-.wav`, and announced with `Event::rendered { path, duration_sec }`; the command result is the unchanged state. Render is not a project edit and does not push history. Surface: `render` in the top rail; the wire logs the send and the `rendered` path; the button's title keeps the last path. Verification: `cargo test -p zhila-engine` (non-silent 16.5 s buffer, correct frame count); `node scripts/render-smoke.mjs` (status 16.5 s, peak 0.442, 6.3 MB, float 48 kHz stereo, event duration matches); `npm --prefix ui run render-ui-smoke` (button → wire path, screenshot `preview-p2-render.png`). Limits: WAV only, stereo float, fixed 48 kHz and 1.5 s tail; no stems, normalisation or section ranges. ## P2c · the choir `crates/zhila-daemon/src/agent.rs` is the performer. It receives the project state and live meter as text, a system prompt with the ZHILA rules, and an OpenAI-compatible tool list. Safe verbs (play, stop, strike, strike_face, render) execute immediately; project verbs (tempo, gains, pattern, faces, sections, cuts, undo/redo) are collected into one proposal stored with an id. The hand approves or rejects; approval applies every command with `Actor::Agent`, saves the project, and emits the normal events plus an agent confirmation. Wire: `agent_ask { text }`, `agent_approve { id }`, `agent_reject { id }`, `agent_reset`; events `agent_message { role, text }`, `agent_proposal { id, description, commands }`, `agent_resolved { id, applied }`; `agent_history { messages }` on hello. The host keeps the conversation and pending proposals in memory. Config: `ZHILA_AGENT_KEY` or `DEEPSEEK_API_KEY` enables the live provider (`ZHILA_AGENT_URL`, default `https://api.deepseek.com`; `ZHILA_AGENT_MODEL`, default `deepseek-chat`). No key, or `ZHILA_AGENT_MOCK=1`, uses the deterministic mock: "tempo" proposes 132 bpm, "listen" strikes the bass, "play"/"stop"/"render" perform, "bass" proposes a gain. Surface: the `choir` room (top rail) with the conversation, proposals (commands labelled, approve/reject), a listen button, an input, and a readout of sections, levels, bpm and cores. The agent's voice is Cormorant; the commands are mono. Verification: `node scripts/agent-smoke.mjs` (mock: ask → proposal → approve → tempo 132, resolved, history); `npm --prefix ui run agent-ui-smoke` (choir ask/approve, readout 132, screenshot `preview-p2-agent.png`); `cargo test -p zhila-daemon` (mock turn, tool mapping). Limits: in-app only, no MCP; one tool round per ask; the conversation is daemon memory; the mock is the default without a key; the host restart clears the chat (the project keeps the approved edits). ## P2 acceptance - Arrangement: four 2-bar sections, 8-bar playback, per-section editing, persisted and undoable. - Render: a valid 48 kHz stereo float WAV of the arrangement (16.5 s, non-silent, peak 0.442). - Agent: ask → proposal → approve → a project edit applied with `Actor::Agent`, on the same wire. - The native Tauri app hosts all of it in-process; the browser path uses the standalone daemon. - `./scripts/check.sh`, the persistence smoke and the three P2 smokes pass.