Agent skill

Lua Plugin Authoring

by scragnog in scragnog/HOT-Step-CPP

Explains how to write, test, and debug HOT-Step Lua plugins (solvers, schedulers, guidance modes, postprocess) including the apg()/poststep() API and UI param flow.

MITAuto-check passedTesting & QA

Install Lua Plugin Authoring

skills CLI
$ npx skills add scragnog/HOT-Step-CPP --skill lua-plugin-authoring -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install scragnog/HOT-Step-CPP lua-plugin-authoring --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/scragnog/HOT-Step-CPP.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/lua-plugin-authoring .claude/skills/lua-plugin-authoring && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
lua-plugin-authoring
GitHub stars
171
Token cost
~7k tokens
SKILL.md length
2,939 words
Files
2
Skills in repo
18
Repo updated
First seen
Licence
MIT

At a glance

Explains how to write, test, and debug HOT-Step Lua plugins (solvers, schedulers, guidance modes, postprocess) including the apg()/poststep() API and UI param flow.

  • Works in 4 steps: Solver — plugins/solvers/ → Scheduler — plugins/schedulers/ → Guidance — plugins/guidance/ → …
  • Modifying a sampling solver
  • SKILL.md covers Golden rules, Where plugins live and how…, Procedure: write and test a… and The four plugin types —…, plus 7 more sections
  • Calls cmake

What it does

Lua Plugin Authoring is an agent skill from scragnog/HOT-Step-CPP. Explains how to write, test, and debug HOT-Step Lua plugins (solvers, schedulers, guidance modes, postprocess) including the apg()/poststep() API and UI param flow. Use when adding or modifying a sampling solver, noise scheduler, CFG/guidance mode, or VAE-decode postprocess plugin, or when a plugin fails to load, has no effect, or produces noise.

Its SKILL.md is about 7k tokens, which your agent loads only when the skill is triggered. The skill folder holds 1 other file (for example `reference.md`).

It sits in Testing & QA, covering Test generation. It works with Lua and C++. The repository describes itself as: Turn dials. Summon bangers! NOW WITH MORE C++! Local AI music generation powered by GGML. The licence is MIT.

When your agent uses it

  • Modifying a sampling solver
  • Noise scheduler
  • CFG/guidance mode
  • VAE-decode postprocess plugin

Example prompts

  • “Use the lua-plugin-authoring skill to explain how to write, test, and debug HOT-Step Lua plugins (solvers, schedulers, guidance modes, postprocess)…”
  • “/lua-plugin-authoring”

Workflow steps

4 steps, taken from the step headings in SKILL.md.

  1. Solver — plugins/solvers/
  2. Scheduler — plugins/schedulers/
  3. Guidance — plugins/guidance/
  4. Postprocess — plugins/postprocess/ (replaces tiled VAE decode)

What it can do on your machine

Read from SKILL.md and the folder at commit 91e92a8. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • cmake

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    No URLs in SKILL.md.

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Lua Plugin Authoring loads about 7k tokens when it runs. Until then it costs about 93 tokens; SKILL.md has 2,939 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~93
When it runs · the whole SKILL.md, loaded when a task matches
~7k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from scragnog/HOT-Step-CPP at commit 91e92a8, republished under its MIT licence (© scragnog). 2,939 words, ~6,970 tokens.

Download SKILL.mdSave it as .claude/skills/lua-plugin-authoring/SKILL.md (or your agent's skills folder). This skill also uses 1 other file; get the full folder from GitHub.
name
lua-plugin-authoring
description
Explains how to write, test, and debug HOT-Step Lua plugins (solvers, schedulers, guidance modes, postprocess) including the apg()/post_step() API and UI param flow. Use when adding or modifying a sampling solver, noise scheduler, CFG/guidance mode, or VAE-decode postprocess plugin, or when a plugin fails to load, has no effect, or produces noise.

Writing Lua plugins for HOT-Step CPP

Solvers, schedulers, guidance modes, and postprocess (VAE-decode replacement) are Lua plugins (LuaJIT 2.1) loaded by the C++ engine at startup. Adding one = drop a .lua file in the right directory and restart the app. No C++ rebuild.

Glossary (used throughout):

  • DiT — the diffusion transformer that denoises audio latents over N steps. A solver decides how the latent xt advances each step given the model's predicted velocity vt. A scheduler decides the timestep values. A guidance mode combines the conditional and unconditional model predictions (classifier-free guidance, CFG). A postprocess plugin replaces the built-in tiled VAE (variational autoencoder) latent-to-audio decode.
  • FloatArray — a zero-copy Lua userdata view over a raw C++ float*. It is 0-indexed (xt[0] … xt[n-1]), unlike normal Lua tables. #xt returns its length. Defined in engine/src/lua-plugin.h:32-87.
  • params — a Lua global table injected before every plugin call, holding the values the user set in the UI for this plugin's declared parameters.

Golden rules

  1. The old approach of editing engine/src/dit-sampler.h is OBSOLETE. All sampling routes through engine/src/hot-step-sampler.h (included by upstream pipeline-synth-ops.cpp). Adding a solver/scheduler/guidance = write a .lua plugin. WHY: C++ edits to the old sampler are dead code and waste a rebuild cycle; losing the hot-step-sampler.h include during an upstream sync kills every plugin (a linker sentinel hotstep_sampler_linked_ at hot-step-sampler.h:1309-1314 turns that into a link error; engine/verify-hooks.ps1 also checks it).
  2. "Hot-loadable" means no rebuild, NOT live reload. Plugins are scanned once at ace-server startup (engine/tools/hot-step-server.cpp:2676). After editing a .lua file you must restart the app. POST /api/plugins/reload only clears the Node server's 60-second cache of the plugin list (server/src/routes/plugins.ts:31-35) — it does not re-read files.
  3. Never kill ace-server.exe directly. The Node server auto-respawns it on crash, causing an infinite respawn + file-lock loop. The working restart is Invoke-RestMethod -Method Post http://localhost:3000/api/shutdown/restart (the shutdown router is mounted at /api/shutdown — server/src/index.ts:77 — so plain /api/restart 404s; the loop wrapper relaunches Node, which respawns ace-server and rescans plugins). Alternatively dev-rebuild.bat (clean shutdown; its compile is a no-op for Lua-only changes) then start again with dev.bat — note dev-rebuild.bat does NOT relaunch, and re-running dev.bat while the app is still up just spawns port-conflicting duplicates. If plugin work escalates into editing any engine/src/ C++ file (e.g. a new bridge function in lua-plugin.h), rebuild via dev-rebuild.bat immediately — never engine/build.cmd directly, and never cmake --clean-first (20+ min CUDA recompile; for stale .obj issues delete only engine/build/acestep-core.dir/ and engine/build/Release/acestep-core.lib).
  4. FloatArrays are 0-indexed; Lua tables are 1-indexed. An out-of-range index raises a Lua error which aborts the call — the engine prints the error and continues, so xt never advances and the "successful" generation is pure noise. WHY: this is the single most common plugin bug and it fails silently from the user's perspective.
  5. Put your plugins in repo-root plugins/<type>/, not engine/plugins/<type>/. Both are scanned (engine tier first, then repo root — engine/src/lua-plugin-registry.h:38-50); the repo-root tier keeps built-ins clean. Duplicate names: first loaded wins (so you cannot shadow a built-in).
  6. In guidance plugins, always route the base combine through apg(). Raw uncond + w*(cond-uncond) produces audible artifacts; the native apg() bridge adds momentum smoothing, perpendicular projection, and norm thresholding. Customize the scale or post-process result instead (see engine/plugins/guidance/cfg_pp.lua).
  7. Do not visually verify the UI with a browser agent — ask the user. API checks (/api/plugins) are fine.
  8. Never delete generated test audio, even output you believe is noise or broken — the user verifies plugin results by ear and compares control vs. plugin runs. Leave every test generation in place and ask the user to listen.

Where plugins live and how they load

Scanned at startup, in order (lua-plugin-registry.h:35-55):

  1. engine/plugins/{solvers,schedulers,guidance,postprocess}/ — built-ins
  2. <repo-root>/plugins/{solvers,schedulers,guidance,postprocess}/ — user/community drop-ins

Missing directories are skipped silently (e.g. engine/plugins/postprocess/ does not exist — the only shipped postprocess plugin is plugins/postprocess/md_audio_tiled.lua).

Loader rules (lua-plugin-registry.h:157-226):

  • Only .lua files; sorted for deterministic order.
  • Companion-file exclusion: filename stems containing _constants, _math, or _data are never loaded as plugins — they are require() targets (e.g. beta_math.lua, stork4_constants.lua). Stems ending _core are skipped only if a sibling without _core exists in the same dir (md_audio_tiled_core is skipped; storm_sampler_core loads as a plugin because no storm_sampler.lua exists).
  • Plugin type is detected by which global table the file defines: solver, scheduler, guidance, or postprocess (lua-plugin.h:348-386). Wrong table for the directory → "declares wrong type, skipping". Empty/missing name → skipped. Lua syntax error → [Plugins] ERROR loading <path>: <message> on stderr, plugin absent.
  • Startup log (in logs/<session>/ace_engine.log): one line per plugin, then [Plugins] Loaded N solvers, N schedulers, N guidance, N postprocess.

Sandbox (lua-plugin.h:180-195, 316-343): math, string, table, print, pairs, ipairs, tonumber, tostring, require are available. os, io, debug, dofile, loadfile are removed. require() searches ONLY the plugin's own directory; C modules are blocked. Each plugin gets its own lua_State that lives for the whole process — file-level local variables persist across steps AND across generations (that is how stateful solvers work, and why they must self-reset; see Golden rule of state below).

Procedure: write and test a plugin

  1. Create plugins\<type>\my_thing.lua (repo root). Start by copying the closest template (see the table in "Worked templates" below).
  2. Restart the app. If it is running: Invoke-RestMethod -Method Post http://localhost:3000/api/restart (relaunches Node → respawns ace-server → rescans plugins). If it is fully stopped: start with dev.bat. Do NOT re-run dev.bat over a running app (port conflicts; the old ace-server keeps serving the stale plugin list), and never kill ace-server.exe yourself.
  3. Confirm it loaded:
    powershell
    $s = Get-ChildItem logs | Sort-Object Name -Descending | Select-Object -First 1
    Select-String -Path "$($s.FullName)\ace_engine.log" -Pattern '\[Plugins\]'
    Expect a per-plugin line naming yours. A load failure prints [Plugins] ERROR loading <path>: <lua error> here.
  4. Confirm the API sees it:
    powershell
    Invoke-RestMethod http://localhost:3000/api/plugins | ConvertTo-Json -Depth 6
    (or engine-direct http://localhost:8085/plugins). If you edited metadata and the list looks stale, bust the Node cache: Invoke-RestMethod -Method Post http://localhost:3000/api/plugins/reload — cache only; file changes still need an app restart.
  5. Generate once with known-good settings (solver euler + scheduler linear) as a control, then swap in your plugin. A generation selects plugins via request JSON fields infer_method (solver — note the non-obvious name), scheduler, guidance_mode, and postprocess_plugin (UI params inferMethod / scheduler / guidanceMode / postprocessPlugin, mapped in translateParams.ts:58-60). Runtime confirmation lines in ace_engine.log:
    • [DiT] Solver: <display> (<name>, N NFE/step, order K) (hot-step-sampler.h:503)
    • [DiT] Guidance: <display> (<name>) [native APG] [post_step] (hot-step-sampler.h:525)
    • [DiT] Custom schedule: <display> (<name>), shift=X (engine/src/sampler-schedule.h:151)
    • [Postprocess] Using plugin '<name>' for VAE decode (pipeline-synth-ops.cpp:1764)
  6. Debug with print() — it goes to engine stdout, captured in ace_engine.log and the matching logs/<session>/generations/gen_*.log. Runtime Lua errors surface as [Plugins] ERROR in solver '<name>' step(): <traceback> (also schedule()/guide()/post_step()/process() variants):
    powershell
    Select-String -Path "$($s.FullName)\ace_engine.log" -Pattern '\[Plugins\] ERROR'

The four plugin types — contracts

All contracts below are verified against engine/src/lua-plugin.h (the single source of truth). Full detail including full-loop solvers, post_step(), and postprocess internals: reference.md.

1. Solver — plugins/solvers/
lua
solver = {
    name = "my_solver", display = "My Solver", description = "...",
    nfe = 1, order = 1,          -- informational (shown in logs/UI)
    needs_model = false,          -- true => step() gets model_fn + vt_buf
    stateful = false, stochastic = false,  -- informational
    -- owns_loop = true          -- advanced: define sample() instead of step(); see reference.md
    params = { ... },             -- optional; see "Declared UI params"
}

-- single-eval (needs_model = false): 5 args
function step(xt, vt, t_curr, t_prev, n)
    -- xt: mutable FloatArray (modify IN PLACE); vt: READ-ONLY FloatArray
    -- t_curr = current t; t_prev = the NEXT (lower) t despite the name
    local dt = t_curr - t_prev   -- positive
    for i = 0, n - 1 do xt[i] = xt[i] - vt[i] * dt end
end

Multi-eval (needs_model = true) gets 7 args: step(xt, vt, t_curr, t_prev, n, model_fn, vt_buf). model_fn(xt_arr, t_val) runs a full CFG'd forward pass and writes guided velocity into vt_buf (a live-memory FloatArray). The vt arg is a snapshot taken before your step — read the original velocity from vt, read fresh model results from vt_buf (hot-step-sampler.h:1179-1195; this separation fixed the historical "Heun silently becomes Euler" bug).

Globals injected per call: step_index (0-based), batch_n, n_per (elements per batch item), params (lua-plugin.h:451-457). n = whole flattened batch (batch_n * n_per).

Engine invariants: the final step never calls step() — the engine computes output = xt - vt * t_curr itself (hot-step-sampler.h:1171-1175). DCW correction and repaint injection are applied after your step by the engine — do not reimplement them.

Stateful solvers must self-reset: lua_State persists across generations, so reset file-locals when step_index == 0 (see engine/plugins/solvers/unipc.lua:153-158). Checking only "did n change" is insufficient — two same-length generations back-to-back will bleed state (plugins/solvers/md_pingpong_simple.lua:242-258 documents the explosion this causes).

2. Scheduler — plugins/schedulers/
lua
scheduler = { name = "my_sched", display = "My Sched", description = "...", params = {...} }

function schedule(output, num_steps, shift)
    -- write num_steps DESCENDING t values (1.0 -> ~0.0) into output (FloatArray)
    -- do NOT append a trailing 0 — the engine handles the final x0 step
    for i = 0, num_steps - 1 do output[i] = 1.0 - i / num_steps end
    -- apply the standard shift warp yourself (every shipped scheduler does):
    if shift ~= 1.0 then
        for i = 0, num_steps - 1 do
            local t = output[i]
            output[i] = shift * t / (1.0 + (shift - 1.0) * t)
        end
    end
end

shift is NOT taken from the UI — it is back-calculated from the upstream schedule's second timestep and clamped (<0.5 → 1.0, >10 → 3.0) at sampler-schedule.h:61-74. Your plugin only runs when the request names a scheduler; empty = upstream default shifted-linear. A custom_timesteps CSV in the request overrides all schedulers (hot-step-sampler.h:91-103). Composite syntax composite:A+B:crossover:split blends two scheduler plugins engine-side (sampler-schedule.h:79-133). Name aliases: karras → sgm_uniform; power:4.00 falls back to prefix power (lua-plugin-registry.h:72-91).

3. Guidance — plugins/guidance/
lua
guidance = { name = "my_guide", display = "My Guide", description = "...", params = {...} }

function guide(pred_cond, pred_uncond, guidance_scale, result, Oc, T, norm_threshold)
    -- pred_cond / pred_uncond: READ-ONLY FloatArrays [Oc*T]; result: mutable [Oc*T]
    -- customize the scale, then ALWAYS combine via apg():
    apg(pred_cond, pred_uncond, guidance_scale, result, Oc, T, norm_threshold)
end

Called per batch element per model eval. Globals: step_idx (note: guidance uses step_idx; solvers use step_index), total_steps, dt, t_curr, params.

apg() is a C bridge over native apg_forward() (lua-plugin.h:643-671), registered lazily on the first guide() call (lua-plugin.h:697-704). Calling it at file top level therefore fails at load with Lua's attempt to call a nil value (global 'apg') and the plugin is skipped. Calling it from post_step() does NOT error — the _apg_mbuf global set before each guide() call (lua-plugin.h:688-689) is never cleared, so it silently reuses the momentum buffer of whichever batch element was guided last (wrong/stale state). Either way: only call apg() inside guide().

Native bypass gotcha: when the selected guidance is named exactly "apg", the engine takes the native C++ path and never calls Lua guide() (use_apg_native, hot-step-sampler.h:524). Editing apg.lua's body does nothing; it is a documented fallback (apg.lua:4-5). To experiment, copy it under a new name.

Advanced: define a global post_step(xt, t, n, eval_cond, eval_uncond, vt_cond, vt_uncond) and the engine calls it after every solver step except the last, only while CFG is active. Each eval_cond/eval_uncond call is a full model forward pass — expensive. Details + gating conditions: reference.md. Real user: engine/plugins/guidance/cfg_mp.lua:47.

4. Postprocess — plugins/postprocess/ (replaces tiled VAE decode)
lua
postprocess = { name = "my_pp", display = "My PP", params = {...} }

function process(latents, B, C_lat, W, C_aud, final_samples, upscale_factor, vae_decode_fn)
    -- latents: plain Lua TABLE (1-indexed, NOT a FloatArray), channel-major [C_lat=64, W]
    -- B is always 1 (engine iterates batch items); upscale_factor = 1920 samples/latent frame
    -- vae_decode_fn(latent_table, T_latent) -> audio_table, T_audio
    -- MUST return: audio_table (1-indexed, [2 * T_audio]), T_audio
end

Selected per request via JSON field postprocess_plugin (engine/src/request.cpp:177-178; UI param postprocessPlugin → server/src/services/generation/translateParams.ts:181-183). Unknown name or T_audio <= 0 → warning + automatic fallback to the built-in tiled decoder (pipeline-synth-ops.cpp:1730-1734, 1792-1804). This path uses Lua tables with transposes on both sides — deliberately not zero-copy. Reference implementation: plugins/postprocess/md_audio_tiled.lua + md_audio_tiled_core.lua (the require()-a-_core-module pattern). Full contract: reference.md.

Declared UI params and how values reach your plugin

Schema (extracted at lua-plugin.h:230-291) — four types: slider (default/min/max/step), select (default + options as {value=,label=} tables or bare strings), toggle (bool default), text (string default). Common fields: key, label, hint, visible_when = { key = "...", equals = "..." } (string-compared against a sibling param's current value). Field reference + JSON shape: reference.md.

Flow: engine GET /plugins → Node proxy GET /api/plugins (60 s cache, empty lists on engine failure) → UI PluginControls (ui/src/components/global-bar/PluginControls.tsx) stores flat strings in { "pluginName:paramKey": "value" }, persisted under localStorage key hs-pluginParams (ui/src/stores/globalParamsStore.ts:149) → request field plugin_params (translateParams.ts:176-178) → engine parses ([DIAG] Parsed N plugin_params in the log) → before every Lua call, lua_inject_params (lua-plugin.h:410-436) filters by "<plugin.name>:" prefix and sets a fresh params global. Coercion: numeric string → number, "true"/"false" → boolean, else string.

Param traps:

  • params and any key may be nil (untouched params are absent from the map). Always default: local x = (params and params.x) or 0.5.
  • The or idiom is WRONG for toggles defaulting true: (params and params.rms_servo) or true is true even when the user turned it OFF (false or true == true). md_pingpong_simple.lua:267 (rms_servo_on) carries exactly this latent bug — do not copy it. Correct:
    lua
    local v = true
    if params and params.rms_servo ~= nil then v = params.rms_servo end
  • Keys are namespaced by plugin name, not filename. Renaming the plugin silently orphans users' stored values.
  • Do not rely on the transform schema field — it is extracted and serialized but PluginControls.tsx never applies it; values are sent verbatim. docs/dev/plugins-authoring.md's claim that the UI transforms values is not implemented.
  • accent (UI colorway) must be one of: amber, cyan (default), blue, teal, green, emerald, purple, indigo, orange, pink, rose, sky, violet (PluginControls.tsx:20-35).
  • Fields like stork_substeps, beat_stability, apg_momentum are a separate legacy sideband channel (hot-step-params.h:97-100, translateParams.ts:147-155), not plugin_params. New plugins must use declared params only.
Show full SKILL.md (1,107 more words)Show less

Worked templates (all real, in-repo)

Want to write…Copy from
Solver, single-evalengine/plugins/solvers/euler.lua (21 lines, canonical)
Solver, multi-eval / statefulengine/plugins/solvers/unipc.lua (needs_model=true, history reset on step_index==0)
Solver, full-loop / stochastic / heavy paramsplugins/solvers/md_pingpong_simple.lua (owns_loop, pure-Lua RNG, hoisted scratch buffers; but see the toggle-bug note above)
Scheduler, simpleengine/plugins/schedulers/linear.lua
Scheduler with companion require()engine/plugins/schedulers/beta57.lua + beta_math.lua
Guidance, scale-modifyingengine/plugins/guidance/cfg_pp.lua (21 lines)
Guidance with post_step()engine/plugins/guidance/cfg_mp.lua
Postprocessplugins/postprocess/md_audio_tiled.lua + _core

Key files

PathRole
engine/src/lua-plugin.hThe plugin API source of truth: FloatArray, sandbox, all call contracts, param extraction
engine/src/lua-plugin-registry.hScan dirs, companion exclusion, name lookup + aliases, JSON for GET /plugins
engine/src/hot-step-sampler.hSampling loop: solver/guidance dispatch, final-step x0, vt snapshot, post_step gating, linker sentinel
engine/src/sampler-schedule.hScheduler dispatch, shift back-calculation, composite schedulers
engine/src/pipeline-synth-ops.cppPostprocess plugin caller + fallback (ops_vae_decode_postprocess, ~line 1721)
engine/src/hot-step-params.hplugin_params map + legacy sideband params
engine/tools/hot-step-server.cppRegistry init (~2676), GET /plugins (~2716), plugin_params JSON parse (~771)
server/src/routes/plugins.tsNode proxy /api/plugins + 60 s cache + /reload
server/src/services/generation/translateParams.tsUI params → engine request JSON (plugin_params, postprocess_plugin)
ui/src/components/global-bar/PluginControls.tsxRenders declared params; accent map
ui/src/stores/globalParamsStore.tshs-pluginParams localStorage persistence
engine/plugins/ + plugins/Built-in and user plugin tiers
docs/dev/plugins-authoring.mdCommitted authoring guide (mostly accurate; see caveats in reference.md)
plugins/README.mdCommunity plugins folder README: points at the authoring guide; its full-loop solver section is accurate

Failure signatures

SymptomCause → fix
Plugin absent from UI dropdownLua syntax error at load (grep [Plugins] ERROR loading in ace_engine.log); wrong global table for its directory; empty name; duplicate name (first wins); or filename matched companion exclusion (_constants/_math/_data, or _core with a non-core sibling)
All dropdowns empty / fallback listsNode couldn't reach the engine — /api/plugins returned empty lists (plugins.ts:26-28). Check engine is up on :8085
Output pure noise, generation "succeeds"step() raised (index out of range, nil arithmetic) — error printed each step, xt never advanced. Grep [Plugins] ERROR in
FloatArray is read-onlyWrote to vt / pred_cond / pred_uncond. Write to xt / result / scratch tables
attempt to call a nil value (global 'apg') at loadCalled apg() at file top level — it is only registered lazily inside guide() dispatch
apg() from post_step() behaves oddly (no error)Silently reuses the stale momentum buffer from the last guide() call — never call apg() outside guide()
Multi-eval solver quietly acts like EulerExpected the original velocity after calling model_fn — read original from the snapshot arg vt, fresh results from vt_buf
First step of 2nd generation explodesStateful solver didn't reset file-locals on step_index == 0 (an n-change check alone misses same-length runs)
Toggle "can't be turned off"(params and params.key) or true idiom — see Param traps
Param changes do nothingKey mismatch vs schema key; edited apg.lua (native bypass); or values persisted under an old plugin name
Guidance post_step never firesguidance_scale <= 1 (no CFG), final step, or past the cfg_cutoff_ratio step (CFG turned off)
Every solver/scheduler/guidance dead after upstream syncpipeline-synth-ops.cpp lost the hot-step-sampler.h include — now a link error via the hotstep_sampler_linked_ sentinel. Run engine/verify-hooks.ps1
Generation very slow with custom guidanceEach eval_cond/eval_uncond in post_step is a full forward pass — budget them

Three samplers run these plugins, not one

The plugin layer never had an ACE dependency — every lua_call_* takes raw float*, element counts and a param map. What was ACE-specific was the sampler. Three now dispatch into it, and a plugin change affects all three:

SamplerCall sitesNotes
ACE-Step DiThot-step-sampler.hthe original; time-major [T][Oc], t descends 1→0
MiniMax-Music3 flow DiTminimax/mm3-plugins.hsigma ascends, latents channel-major — the bridge flips both
StableStep / SA3 refinesa3-refine.ht already descends like ACE; latents channel-major

The layout trap, twice learned: apg_project() (guidance/apg-core.h) normalises per channel over time and indexes [t*Oc + c] — it is hard-wired to ACE's time-major memory. MM3 and SA3 both store latents channel-major, so both bridges transpose into the ACE view before any guidance plugin sees a buffer, and back after. Skip that and APG silently normalises scrambled mixtures rather than channels — it compiles, it runs, it sounds subtly wrong. Solvers are exempt: lua_call_solver_step is handed only a flat n, and lua_call_solver_loop uses T/Oc solely to publish n_per, so no solver can index across channels.

Per-backend caveats worth knowing before you assume a plugin "works everywhere":

  • owns_loop solvers: fine on ACE and SA3, refused on MM3 — a full-loop solver bypasses MM3's per-step window-overlap blend and breaks every seam.
  • Guidance on SA3 is near-decorative. SA3 was trained at cfg=1 and has no unconditional branch, so the engine passes the cond velocity as both predictions. APG-family modes see diff = 0 and pass through unchanged; only plugins doing cond-side work have any effect.
  • Schedulers on SA3 get rescaled. Schedulers emit sigma_max = 1.0 for full denoising; the SA3 refine is SDEdit and starts at strength (~0.3), so the returned curve is linearly rescaled onto [strength → 0] — spacing preserved, starting point not. Note the override array is steps long while the loop indexes sigmas[i+1], so the terminal 0.0 must be appended (getting this wrong is an out-of-bounds read, not a compile error).
  • sampler_build_scheduler_override() reads process-global g_hotstep_params, not its arguments. /sa3-refine therefore saves and restores solver_name/scheduler/plugin_params around the call — without that, a refine leaks its picks into the next ACE generation on the same worker.

Institutional knowledge

  • VALIDATED: hot-step-sampler.h replaced dit-sampler.h as the sampling path; the include lives in upstream pipeline-synth-ops.cpp and its loss during a sync used to be silent (everything compiled, all plugins dead). The linker sentinel now makes it a link error. Always run engine/verify-hooks.ps1 after touching upstream files.
  • VALIDATED: the engine snapshots vt before multi-eval solver steps (hot-step-sampler.h:1179-1195) because sharing one buffer between "original velocity" and "model_fn output" silently degraded Heun to Euler.
  • VALIDATED: stateful plugins must reset on step_index == 0; persisting lua_States bleed state across generations (documented in-code at md_pingpong_simple.lua:242-246 — "explosive velocity on step 1").
  • VALIDATED: philox_randn is NOT exposed to Lua. sde.lua:3-4 mentions it as a required C helper, but it is not registered; the SDE stochastic path is handled C++-side for that specific plugin. Pure-Lua stochastic plugins must roll their own RNG (see the LCG + Box-Muller in md_pingpong_simple.lua).
  • The outdated module-return examples that used to live in plugins/README.md were removed on 2026-09-25; the README now points at docs/dev/plugins-authoring.md. Trust that guide and engine/src/lua-plugin.h.
  • UNVERIFIED: whether the TensorRT sampler variant (hot-step-sampler-trt.h) covers solver/guidance plugins identically — it calls the same scheduler override, but its plugin dispatch was not audited. Check before relying on plugins under the TensorRT backend.

Deeper reading

  • reference.md (this folder) — full-loop solver contract, post_step() details, postprocess internals, param schema JSON shape, docs-vs-code discrepancy list.
  • docs/dev/plugins-authoring.md — committed authoring guide. Known inaccuracies: says shift comes "from UI" (actually back-calculated); documents transform as applied by the UI (it is not); omits owns_loop/sample() and the postprocess type; uses step_idx naming loosely (solvers get step_index, guidance gets step_idx).
  • engine/docs/ARCHITECTURE.md — engine internals, request JSON.
  • docs/plans/ — internal design docs, gitignored and local-only (may be absent on a fresh clone).

© scragnog, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

SKILL.md and 1 other file in .claude/skills/lua-plugin-authoring of scragnog/HOT-Step-CPP.

  • SKILL.md
  • reference.md

Open the folder on GitHubat commit 91e92a8

Compare with similar skills

Lua Plugin Authoring next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Lua Plugin Authoring compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Lua Plugin Authoring this skillscragnog/HOT-Step-CPP171—~7kAutomated safety check: PassMIT
OpenROAD Module Test AdderThe-OpenROAD-Project/OpenROAD3.2k—~1.8kAutomated safety check: PassBSD-3-Clause
Minitest Ebt Luakokusenz/deltaview.nvim127—~4kAutomated safety check: PassMIT
Nvim TestS1M0N38/love2d.nvim215—~770Automated safety check: PassMIT
Test Case ReducerArabelaTso/Skills-4-SE253—~2.5kAutomated safety check: PassApache-2.0
Symbolic Execution AssistantArabelaTso/Skills-4-SE253—~3.5kAutomated safety check: PassApache-2.0

Similar skills

  • OpenROAD Module Test Adder

    The-OpenROAD-Project/OpenROAD

    Adds integration or unit tests to an OpenROAD module: writes the Tcl test, generates golden files and registers it in both CMake and Bazel.

    3.2k GitHub stars~1.8k tokensUpdated today
    Testing & QAAuto-check passed
  • Minitest Ebt Lua

    kokusenz/deltaview.nvim

    This skill should be used when the user asks to write example-based tests, unit tests, or EBT for a Neovim plugin using the MiniTest (mini.test) framework in Lua.

    127 GitHub stars~4k tokensUpdated 1 mo ago
    Testing & QAAuto-check passed
  • Nvim Test

    S1M0N38/love2d.nvim

    Execute tests and diagnose failures for love2d.nvim. An agent skill from S1M0N38/love2d.nvim.

    215 GitHub stars~770 tokensUpdated 3 mo ago
    Testing & QAAuto-check passed
  • Test Case Reducer

    ArabelaTso/Skills-4-SE

    Automatically reduces bug-triggering test cases to minimal form while preserving the failure.

    253 GitHub stars~2.5k tokensUpdated 1 mo ago
    Testing & QAAuto-check passed
  • Symbolic Execution Assistant

    ArabelaTso/Skills-4-SE

    Performs symbolic execution to detect potential errors by exploring execution paths, solving path constraints, and generating test inputs.

    253 GitHub stars~3.5k tokensUpdated 1 mo ago
    Testing & QAAuto-check passed
  • OpenROAD Bug Fixer

    The-OpenROAD-Project/OpenROAD

    Fixes an OpenROAD bug from a GitHub issue or error code: finds the root cause, implements the fix, adds a regression test and prepares a signed-off commit.

    3.2k GitHub stars~784 tokensUpdated today
    DevelopmentAuto-check passed

More from scragnog/HOT-Step-CPP

All 18 skills in this repo
  • Ear Test Scoresheet

    scragnog/HOT-Step-CPP

    The standard way to run a listening test in HOT-Step - a local HTML score sheet next to the renders where Rob plays each track, scores it 1-5 on named criteria, and the page charts the two score…

    171 GitHub stars~1.9k tokensUpdated yesterday
    Auto-check passed
  • Engine Performance

    scragnog/HOT-Step-CPP

    Explains where HOT-Step generation time goes (LM/DiT/VAE), how the TensorRT paths activate, how to benchmark from logs, and which knobs trade quality for speed.

    171 GitHub stars~4.9k tokensUpdated yesterday
    Auto-check passed
  • Mm3 Backend

    scragnog/HOT-Step-CPP

    Maps HOT-Step's native MiniMax-Music3 backend — engine port modules, endpoints, server/UI integration, parity/fixture infrastructure, and the hard-won trap list.

    171 GitHub stars~4.5k tokensUpdated yesterday
    Auto-check passed
  • Mm3 Lm Adapter Training

    scragnog/HOT-Step-CPP

    The validated recipe for training MiniMax-Music3 planner-LM style adapters (artist/album clones) with ace-train mm3-lm-train and the Training Studio.

    171 GitHub stars~4k tokensUpdated yesterday
    Auto-check passed
  • Release Process

    scragnog/HOT-Step-CPP

    Runbook for cutting and publishing a HOT-Step CPP release via a v git tag that triggers the multi-platform CI build and drafts a GitHub Release.

    171 GitHub stars~5.1k tokensUpdated yesterday
    Auto-check passed
  • Upstream Sync

    scragnog/HOT-Step-CPP

    Safely pulls upstream acestep.cpp changes into the HOT-Step engine fork without destroying its integration hooks.

    171 GitHub stars~5k tokensUpdated yesterday
    Auto-check passed

Works with

Categories

Questions about Lua Plugin Authoring

What does Lua Plugin Authoring do?

Explains how to write, test, and debug HOT-Step Lua plugins (solvers, schedulers, guidance modes, postprocess) including the apg()/poststep() API and UI param flow. Lua Plugin Authoring is an agent skill from scragnog/HOT-Step-CPP. Explains how to write, test, and debug HOT-Step Lua plugins (solvers, schedulers, guidance modes, postprocess) including the apg()/poststep() API and UI param flow.

When should I use Lua Plugin Authoring?

Lua Plugin Authoring fits situations like: modifying a sampling solver; noise scheduler; CFG/guidance mode; VAE-decode postprocess plugin.

How do I install Lua Plugin Authoring in Claude Code?

Run `npx skills add scragnog/HOT-Step-CPP --skill lua-plugin-authoring -a claude-code`. Or copy the skill folder (.claude/skills/lua-plugin-authoring in scragnog/HOT-Step-CPP) into .claude/skills/lua-plugin-authoring in your project. Claude Code loads it when a task matches its description.

How do I install Lua Plugin Authoring in Codex?

Run `npx skills add scragnog/HOT-Step-CPP --skill lua-plugin-authoring -a codex`. Or copy the skill folder (.claude/skills/lua-plugin-authoring in scragnog/HOT-Step-CPP) into .agents/skills/lua-plugin-authoring in your project. Codex loads it when a task matches its description.

Can I use Lua Plugin Authoring in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add scragnog/HOT-Step-CPP --skill lua-plugin-authoring -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/lua-plugin-authoring, .gemini/skills/lua-plugin-authoring, .github/skills/lua-plugin-authoring and .opencode/skills/lua-plugin-authoring in your project.

What does Lua Plugin Authoring need to run?

Going by SKILL.md and its folder, Lua Plugin Authoring needs the command-line tools its instructions call (cmake).

Does Lua Plugin Authoring access the network?

SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.

Is Lua Plugin Authoring safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Lua Plugin Authoring use?

Lua Plugin Authoring is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Lua Plugin Authoring use?

About 7k tokens (SKILL.md is roughly 28k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Lua Plugin Authoring?

Skills that share tags, products or a category with Lua Plugin Authoring: OpenROAD Module Test Adder (The-OpenROAD-Project/OpenROAD, 3.2k stars), Minitest Ebt Lua (kokusenz/deltaview.nvim, 127 stars), Nvim Test (S1M0N38/love2d.nvim, 215 stars) and Test Case Reducer (ArabelaTso/Skills-4-SE, 253 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Lua Plugin Authoring?

scragnog (a GitHub user) maintains it in scragnog/HOT-Step-CPP, which has 171 GitHub stars. The repository holds 18 skills in this directory. The repository was last updated on October 7, 2026.

Source: scragnog/HOT-Step-CPP on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.