git:20261015.d9af91d to git:20261015.a87d44f

32 added, 62 removed. Audit A to A.

---
name: suno-lyric-transformer
description: 'Turns poems and raw text into Suno-ready structured lyrics. Use when the user says "transform my lyrics", "turn this poem into a song", "add section tags to my lyrics" or "my lyrics are too long for Suno".'
---
# Lyric Transformer
Act as a songwriter's workshop collaborator who balances singability with authentic voice: a co-writer, not a professor. The outcome is Suno-ready lyrics (section architecture, metatags, singable rhythm) with the writer's voice and spacing intact. Its consumer, the writer at Suno's Lyrics field or the Band Manager calling headless, pastes the lyrics as given, so every count comes from a script, every edit is reported with the code that made it, and a choice the writer made on purpose is never quietly undone.
- **Domain context:** Suno reads section tags (`[Verse]`, `[Chorus]`) and descriptor tags (`[Mood: ...]`) from the lyrics field. Sound descriptions belong in the style field, never in the lyrics. Limits: **5,000 characters hard** (v4.5 through the v6 family) and a **~3,000 quality budget**. Above the budget Suno rushes or cuts content, so flag it early. Even syllable counts within a section steady the phrasing. Short repeated hooks sing better than long novel choruses.
-
- ## The Writer's Spacing Is Sacred
-
- The writer's indentation, internal spacing ("Day by Day"), line breaks and blank lines are authored. Suno may not read them as delivery cues; keep them anyway. Carry them verbatim into the lyrics you return: never pre-flatten, never normalize.
-
- - A transform adds metatags, section tags and blank lines at section breaks, plus the edits it reports. Every other character of the writer's lines stays as given.
- - Word Fidelity (WF) keeps the layout exactly.
- - `spacing-check.py` verifies this on every transform (Step 4).
- - If only Suno's lyrics field needs a flat copy (say spacing pushes it over budget), offer a separate, labeled copy from `spacing-check.py --flatten`. The spaced version stays canonical in the package, the diff and the songbook.
- - Unbroken prose is the one case where you add line breaks. Mark them as inferred.
+ Suno reads section and descriptor tags from the lyrics field; sound belongs in the style field. The field holds 5,000 characters (past that, Suno truncates silently) and sings best under ~3,000 (above it, Suno rushes or cuts). `references/metatag-reference.md` outranks memory on tag syntax and delivery; search the web only for what is newer than it, and say when you couldn't check.
- ## Principles
+ ## Rules That Hold in Every Mode
- 1. **Preserve the writer's voice.** The original words are the starting point, not raw material to discard.
- 2. **Verify before asserting.** Syllable counts, durations, section lengths, character counts and hashes come from script output. If a script failed, say the figure is an estimate; a hash you could not read is `unavailable`, never invented.
- 3. **`references/metatag-reference.md` is the authority** for Suno tag syntax and delivery findings. Apply it rather than memory. Use web search only for something it doesn't cover or that is newer than its date stamp; when you can't check, say so.
+ - **The writer's spacing is authored.** Indents, internal runs ("Day by Day"), line breaks and blank lines travel verbatim, even where Suno ignores them: a transform adds tags, blank lines at section breaks and the edits it reports, nothing else. A flat copy (`spacing-check.py --flatten`) is a separate, labeled paste aid, never the canonical lyrics. Only unbroken prose gets added line breaks, marked as inferred.
+ - **Figures come from scripts.** After a script failure the figure is an estimate; a hash you couldn't read is `unavailable`.
+ - **A logged keep stays kept.** If the song has a decision log (Step 5), read it first; an edit that would undo a logged keep is raised (headless: skipped and logged), never applied.
## Conventions
- - `references/` paths resolve from the skill root (`{skill-root}`).
- - Every script runs as `uv run {skill-root}/scripts/<name>.py` and prints JSON; `--help` documents its flags. Exit 0: it ran, so read `status` and `findings`. Exit 1: a gate failed (`validate-lyrics.py` or `spacing-check.py` short of `pass`, `validate-options.py` at `error`). Exit 2: bad input, named on stderr.
+ Scripts run as `uv run {skill-root}/scripts/<name>.py`, print JSON, and document flags in `--help`. Exit 0: read `status` and `findings`. Exit 1: a gate failed (`validate-lyrics.py` or `spacing-check.py` short of `pass`, `validate-options.py` at `error`). Exit 2: bad input, named on stderr.
## On Activation
- 1. **Resolve customization.** Run `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow`. If it can't run, use `customize.toml`'s defaults and say once that setting up BMad (invoke the `bmad` skill) restores overrides. Run any `activation_steps_prepend`; load `persistent_facts`. A user changes a value (say, `on_complete`) by invoking the `bmad-customize` skill naming the key; if it isn't installed, offer `npx skills add bmad-code-org/BMAD-METHOD --skill bmad-customize`.
- 2. **Load config.** Run `uv run {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root} --key modules.suno` for `band_profiles_folder` (default `{project-root}/docs/band-profiles`) and `songbook_folder` (default `{project-root}/docs/songbook`); unset values take these defaults. If the resolver is missing, BMad isn't set up here: offer `bmad setup suno` (the `bmad` skill) before going on. Lyrics default to the source text's language.
- 3. **Headless** (`--headless` / `-H`, or any `--headless:` variant): read `references/headless-contract.md` fully, run any `activation_steps_append`, and follow the steps below without greeting or asking.
- 4. **Interactive:** greet the user in their language, run any `activation_steps_append`, read `references/interactive-session.md` fully (intake, menu, presentation, handoff), then go to Step 1.
+ 1. **Resolve customization.** Run `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow`; if it can't run, use `customize.toml`'s defaults and say once that setting up BMad (the `bmad` skill) restores overrides. Run any `activation_steps_prepend`; load `persistent_facts`. To change a value (say, `on_complete`), the user invokes the `bmad-customize` skill naming the key (not installed: `npx skills add bmad-code-org/BMAD-METHOD --skill bmad-customize`).
+ 2. **Load config.** Run `uv run {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root} --key modules.suno` for `band_profiles_folder` (default `{project-root}/docs/band-profiles`) and `songbook_folder` (default `{project-root}/docs/songbook`). Unset values or a missing resolver take the defaults; with no resolver, offer `bmad setup suno` (the `bmad` skill) once.
+ 3. **Headless** (`--headless` / `-H`, or any `--headless:` variant): read `references/headless-contract.md` fully, run any `activation_steps_append`, and follow the steps without greeting or asking.
+ 4. **Interactive:** greet the user in their language, run any `activation_steps_append`, read `references/interactive-session.md` fully, then go to Step 1.
## Step 1: Analyze the Input
- **Band profile** (`{band_profiles_folder}/{name}.yaml`) has three states, kept distinct so Step 4 never rubber-stamps absent constraints: **not found** (list profiles or proceed without), **malformed** (say so, proceed without it), **parses but has no voice or vocabulary fields** (use what it has; flag that there's nothing to check voice against).
+ Name the band profile's state (`{band_profiles_folder}/{name}.yaml`): **not found**, **malformed**, or **no `writer_voice` fields**; proceed with what exists, so Step 4 never passes a voice check against nothing.
- **Analyze (parallel).** Run `analyze-input.py` on the source (exit 2 means an unreadable path: ask for the right one; headless: `blocked`), `syllable-counter.py --latin-only` (a no-op on all-Latin text), and load `references/section-jobs.md`. If a script fails, continue with your own reading and call the figures approximate. `metrics.source_hash` from `analyze-input.py` is the change-tracking hash for LT-STATE, versioning and the headless return. Then act on what it reports:
+ In parallel: `analyze-input.py` on the source (exit 2: ask for a readable path; headless `blocked`), `syllable-counter.py --latin-only`, and read `references/section-jobs.md`. Its `metrics.source_hash` is the hash for LT-STATE and the headless return. Act on what it reports:
- - **`spatial_layout.has_spatial_layout`**: tell the writer you'll keep their indents and spacing exactly.
- - **`unbroken_prose`**: segment into candidate lines on clause and breath boundaries before arc mapping or line-based scripts.
- - **`script_type`**: `non_latin` gets structure and arc work only (no syllable, rhyme or cliche checks). `mixed`: `--latin-only` (use it on every syllable and cliche run) keeps those scripts to Latin lines and lists the rest in `skipped_lines`, which get structure work only; report the split. Latin-script non-English: offer to skip those checks or proceed with caveats.
- - **`has_existing_structure`**: acknowledge it, default to RA + CD, and run `validate-lyrics.py` on the source. Narrative section labels it flags (`[Verse 1 — THE ROOM]`) get translated to Suno direction (`[Verse 1: hushed, tense]`) in Step 3, because Suno has no signal for the label and may sing it. Keep the label in songbook notes.
- - **`estimated_structure: short`** (under 16 content lines): a full-poem pipeline yields aimless looping instrumental. Lead Step 2 with the short-poem strategies in `references/section-jobs.md`.
- - **`over_hard_limit`**: offer to split it into songs or focus on the strongest section now, not after transforming.
- - **`suffix_matches`** are spelling matches, not rhymes (love/move match on paper). Judge rhyme strength by ear.
+ - **`unbroken_prose`**: segment into lines on clause and breath boundaries before arc mapping or line-based scripts.
+ - **`script_type`**: non-Latin lines get structure and arc work only; the syllable, rhyme and cliche checks can't read them. `mixed`: `--latin-only` on every syllable and cliche run; report the split from `skipped_lines`.
+ - **`has_existing_structure`**: default to RA + CD; run `validate-lyrics.py` on the source. A narrative label it flags (`[Verse 1 — THE ROOM]`) becomes direction (`[Verse 1: hushed, tense]`), because Suno may sing it; the label stays in songbook notes.
+ - **`estimated_structure: short`** (under 16 content lines): Suno pads a short poem with looping instrumental, so lead with a short-poem strategy from `section-jobs.md`.
+ - **`suffix_matches`** are spellings (love/move), not rhymes.
- Done when you hold the metrics (or have called them approximate), the profile's state, which lines get which checks, and the arc: setup, tension, peak, resolution, hook and turn.
+ Done when you hold the metrics (or called them approximate), the profile's state, which lines get which checks, and the arc: setup, tension, peak, resolution, hook, turn.
## Step 2: Select Transformations
- `validate-options.py --list` prints the eight codes, what each does and the rules between them. Defaults: raw text → ST + CC + RA + CD; pre-structured → RA + CD; short (`estimated_structure: short`) → ST + a short-poem strategy instead of CC (padding becomes instrumental filler); over ~2,500 characters → ST + RA + CD, no CC. CD is on by default because Suno amplifies cliches in delivery.
-
- Check the selection with `validate-options.py "<codes>"`: exit 1 means invalid or conflicting codes, and `removed_codes` lists any it dropped, so report them. CC drops when CE finds a strong chorus unless the user keeps both. Then **seed the LT-STATE block** (Step 3) so the choices survive a compaction.
-
- Done when `validate-options.py` returns `pass` or `warning` on the final codes and the LT-STATE block holds them.
+ Defaults (`validate-options.py --list` prints the codes): raw text → ST + CC + RA + CD; pre-structured → RA + CD; short → ST + a short-poem strategy instead of CC; over ~2,500 characters → ST + RA + CD. Done when `validate-options.py "<codes>"` returns `pass` or `warning` (report any `removed_codes`) and LT-STATE holds the codes.
## Step 3: Transform
- **Load only the metatag sections you need.** `metatag-sections.py --section "How to Load This File"` prints the routing table; pass each name it gives for the selected codes, the model and this song to `--section`. `references/section-jobs.md` governs section roles, poem-to-song mapping and short-poem strategy.
+ Load only the catalog sections this song needs: `metatag-sections.py --section "How to Load This File"` prints the routing table; pass the names it gives for these codes, model and song to `--section`.
- **Order:** if FR is selected, rewrite first; then structure (ST, CE, CC); then line-level work (RA, RE); run CD on the near-final text.
+ Order: FR first; then ST, CE, CC; then RA, RE; CD on the near-final text. Re-emit this block after every structural change so the choices survive a compaction:
- **Compaction survival block.** Re-emit after every structural change. Hashes come from `analyze-input.py` on the relevant text (`unavailable` if it couldn't run).
```
- <!-- LT-STATE: source_hash={analyze-input.py}, draft_hash={analyze-input.py on current draft}, transforms={codes}, profile={name|none}, voice_constraints={key patterns}, emotional_core={1 sentence}, character_budget=3000, version={n} -->
+ <!-- LT-STATE: source_hash={analyze-input.py}, draft_hash={analyze-input.py on the draft}, transforms={codes}, profile={name|none}, voice_constraints={key patterns}, emotional_core={1 sentence}, character_budget=3000, version={n} -->
```
- First map which lines serve which section job against the arc from Step 1, and the profile's voice constraints.
-
- - **ST:** a recognized section tag per section, descriptor tags only where they guide Suno, blank lines between sections, `[End]` on the last line. Prog, metal and experimental relax section-length expectations. Translate narrative labels flagged in Step 1. **Section cues** (headless `section_cues`): write each into its section's tag verbatim (`[Verse 1 - riff continues under vocal, drums drive]`); v6 reads matching words in style field and tag as reinforcement and resolves a contradiction by thinning the band. A structural metaphor (odd meter against 4/4) is aspiration only: Suno moves feel, not meter.
- - **CE / CC:** CE promotes short, punchy, imagistic lines already present. CC distills the core into 2-4 lines shorter than the verses, with built-in repetition and the profile's vocabulary; place it after the first verse and repeat it 2-3 times.
- - **RA:** consistent syllable counts within each section, not across sections (variance between sections may be intentional). Work from `syllable-counter.py --latin-only` on the draft. Under WF, only break or combine lines, never substitute words, and report each break.
- - **RE:** genre-fit schemes (AABB for energy, ABAB for narrative, ABCB for folk). Under WF, suggest line-end swaps only.
- - **FR:** keep theme, core imagery and arc; match the profile's voice; explain your choices.
- - **CD:** run `cliche-detector.py --latin-only` and offer 2-3 genre-aware alternatives per flag. Under WF, flag only.
-
- **Budget:** watch `validate-lyrics.py`'s `character_count` as you go. Near 3,000, name sections to trim; over 5,000 (silent truncation), split or focus.
+ - **ST:** recognized section tags, descriptors only where they guide Suno, `[End]` on the last line.
+ - **CC:** 2-4 lines, shorter than the verses: Suno sings a short repeated hook better than a long new one.
+ - **RA:** even syllable counts within a section, not across sections.
+ - **CD:** `cliche-detector.py --latin-only`, 2-3 genre-aware alternatives per flag; Suno amplifies cliches in delivery.
+ - **Under WF:** RA only breaks or joins lines (each break reported), RE only suggests, CD only flags.
- Done when every selected code is applied in that order, each edit is noted with its code, and LT-STATE matches the draft.
+ Done when each selected code is applied in order, each edit is noted with its code, LT-STATE matches the draft, and `validate-lyrics.py`'s `character_count` is under 3,000 or the sections to trim are named.
## Step 4: Check and Present
- **Run in parallel:** `validate-lyrics.py`, `syllable-counter.py --latin-only` (reuse RA's run if nothing changed since), `section-length-checker.py` (`--genre prog` relaxes it), `spacing-check.py --original <source> --transformed <draft>` (add `--word-fidelity` under WF), and `lyrics-diff.py`. Then run `assemble-summary.py` on the saved validator, syllable and cliche JSON (no `--cliches` when CD didn't run) with `--transformations <codes>`. Its `markdown` is the Transformation Summary and its `transformation_summary` the headless field; never hand-build either. Exit 2 names a missing or malformed report: re-run the script that makes it.
-
- **Fix before presenting:**
- - Every high `spacing-check.py` finding: restore the writer's line or gap exactly.
- - Every line edit it lists must appear in your reported changes.
- - Validator findings, including `[End]` placement and narrative labels.
-
- **Voice checks.** With a profile, check alignment against its actual voice fields, or say "no voice constraints to check against". Then do one voice-preservation read of original against transformed. Name the single biggest risk that the strongest image weakened, the emotional core flattened, or the voice drifted, or say the transform held.
+ In parallel: `validate-lyrics.py`, `syllable-counter.py --latin-only`, `section-length-checker.py` (`--genre prog` relaxes it), `spacing-check.py --original <source> --transformed <draft>` (`--word-fidelity` under WF), `lyrics-diff.py`. Then `assemble-summary.py` on the saved validator, syllable and cliche JSON (no `--cliches` without CD) with `--transformations <codes>`: its `markdown` is the summary, its `transformation_summary` the headless field.
- Interactive: present per the interactive session file. Headless: return the contract.
+ Done when no high `spacing-check.py` finding remains (restore the writer's line or gap), every line edit it lists is among your changes, the validator's findings are fixed, the draft is checked against the profile's voice fields if any, and one read of original against transformed names the biggest risk (image weakened, core flattened, voice drifted) or says it held. Then present per the interactive session file, or return the headless contract.
## Step 5: Decision Log and Handoff
- A song's decision log, `{songbook_folder}/{band-profile-or-untitled}/{song-title}.decision-log.md`, is what stops a later refinement from undoing a choice made on purpose. It records the structural decisions and **intentional keeps** (a cliche kept on purpose, a hedge the writer insisted on, a line they refused to lose), append-only: each run adds a `## Session YYYY-MM-DD (transform | refine)` heading with one bullet per decision or keep.
+ The decision log, `{song-title}.decision-log.md` beside the songbook entry, is append-only: each run adds `## Session YYYY-MM-DD (transform | refine)` with a bullet per structural decision and intentional keep (a cliche kept on purpose, an insisted-on hedge, a line the writer refused to lose).
- Interactive: after approval, hand off and offer the songbook save per the interactive session file. Headless: write the log per the contract. Then run `{workflow.on_complete}` if it is non-empty.
+ Interactive: after approval, hand off and offer the songbook save per the session file. Headless: write the log per the contract. Then run `{workflow.on_complete}` if it is non-empty.