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.
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.
$ npx skills add scragnog/HOT-Step-CPP --skill lua-plugin-authoring -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install scragnog/HOT-Step-CPP lua-plugin-authoring --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ 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-srcUse ~/.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/
Install the "lua-plugin-authoring" agent skill from https://github.com/scragnog/HOT-Step-CPP/tree/master/.claude/skills/lua-plugin-authoring into .claude/skills/lua-plugin-authoring/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "lua-plugin-authoring", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/scragnog/HOT-Step-CPP/tree/master/.claude/skills/lua-plugin-authoringType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add scragnog/HOT-Step-CPP --skill lua-plugin-authoring -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install scragnog/HOT-Step-CPP lua-plugin-authoring --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/scragnog/HOT-Step-CPP.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.claude/skills/lua-plugin-authoring .agents/skills/lua-plugin-authoring && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "lua-plugin-authoring" agent skill from https://github.com/scragnog/HOT-Step-CPP/tree/master/.claude/skills/lua-plugin-authoring into .agents/skills/lua-plugin-authoring/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "lua-plugin-authoring", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add scragnog/HOT-Step-CPP --skill lua-plugin-authoring -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install scragnog/HOT-Step-CPP lua-plugin-authoring --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/scragnog/HOT-Step-CPP.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.claude/skills/lua-plugin-authoring .cursor/skills/lua-plugin-authoring && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "lua-plugin-authoring" agent skill from https://github.com/scragnog/HOT-Step-CPP/tree/master/.claude/skills/lua-plugin-authoring into .cursor/skills/lua-plugin-authoring/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "lua-plugin-authoring", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/scragnog/HOT-Step-CPP.git --path .claude/skills/lua-plugin-authoring--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add scragnog/HOT-Step-CPP --skill lua-plugin-authoring -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install scragnog/HOT-Step-CPP lua-plugin-authoring --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/scragnog/HOT-Step-CPP.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.claude/skills/lua-plugin-authoring .gemini/skills/lua-plugin-authoring && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "lua-plugin-authoring" agent skill from https://github.com/scragnog/HOT-Step-CPP/tree/master/.claude/skills/lua-plugin-authoring into .gemini/skills/lua-plugin-authoring/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "lua-plugin-authoring", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install scragnog/HOT-Step-CPP lua-plugin-authoringInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add scragnog/HOT-Step-CPP --skill lua-plugin-authoring -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/scragnog/HOT-Step-CPP.git skills-src && mkdir -p .github/skills && cp -r skills-src/.claude/skills/lua-plugin-authoring .github/skills/lua-plugin-authoring && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "lua-plugin-authoring" agent skill from https://github.com/scragnog/HOT-Step-CPP/tree/master/.claude/skills/lua-plugin-authoring into .github/skills/lua-plugin-authoring/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "lua-plugin-authoring", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add scragnog/HOT-Step-CPP --skill lua-plugin-authoring -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install scragnog/HOT-Step-CPP lua-plugin-authoring --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/scragnog/HOT-Step-CPP.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.claude/skills/lua-plugin-authoring .opencode/skills/lua-plugin-authoring && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "lua-plugin-authoring" agent skill from https://github.com/scragnog/HOT-Step-CPP/tree/master/.claude/skills/lua-plugin-authoring into .opencode/skills/lua-plugin-authoring/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "lua-plugin-authoring", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
lua-plugin-authoringExplains 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. 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.
4 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit 91e92a8. It shows what the files ask for, not the result of running them.
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.
Shell commands in SKILL.md call:
cmakeFrom the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
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.
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.
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.
The full file from scragnog/HOT-Step-CPP at commit 91e92a8, republished under its MIT licence (© scragnog). 2,939 words, ~6,970 tokens.
.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.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):
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.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.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).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.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).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.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).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)./api/plugins) are fine.Scanned at startup, in order (lua-plugin-registry.h:35-55):
engine/plugins/{solvers,schedulers,guidance,postprocess}/ — built-ins<repo-root>/plugins/{solvers,schedulers,guidance,postprocess}/ — user/community drop-insMissing 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):
.lua files; sorted for deterministic order._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).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.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).
plugins\<type>\my_thing.lua (repo root). Start by copying the closest template (see the table in "Worked templates" below).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.$s = Get-ChildItem logs | Sort-Object Name -Descending | Select-Object -First 1
Select-String -Path "$($s.FullName)\ace_engine.log" -Pattern '\[Plugins\]'[Plugins] ERROR loading <path>: <lua error> here.Invoke-RestMethod http://localhost:3000/api/plugins | ConvertTo-Json -Depth 6http://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.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)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):Select-String -Path "$($s.FullName)\ace_engine.log" -Pattern '\[Plugins\] ERROR'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.
plugins/solvers/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
endMulti-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).
plugins/schedulers/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
endshift 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).
plugins/guidance/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)
endCalled 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.
plugins/postprocess/ (replaces tiled VAE decode)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
endSelected 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.
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.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:local v = true
if params and params.rms_servo ~= nil then v = params.rms_servo endname, not filename. Renaming the plugin silently orphans users' stored values.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).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.| Want to write… | Copy from |
|---|---|
| Solver, single-eval | engine/plugins/solvers/euler.lua (21 lines, canonical) |
| Solver, multi-eval / stateful | engine/plugins/solvers/unipc.lua (needs_model=true, history reset on step_index==0) |
| Solver, full-loop / stochastic / heavy params | plugins/solvers/md_pingpong_simple.lua (owns_loop, pure-Lua RNG, hoisted scratch buffers; but see the toggle-bug note above) |
| Scheduler, simple | engine/plugins/schedulers/linear.lua |
Scheduler with companion require() | engine/plugins/schedulers/beta57.lua + beta_math.lua |
| Guidance, scale-modifying | engine/plugins/guidance/cfg_pp.lua (21 lines) |
Guidance with post_step() | engine/plugins/guidance/cfg_mp.lua |
| Postprocess | plugins/postprocess/md_audio_tiled.lua + _core |
| Path | Role |
|---|---|
engine/src/lua-plugin.h | The plugin API source of truth: FloatArray, sandbox, all call contracts, param extraction |
engine/src/lua-plugin-registry.h | Scan dirs, companion exclusion, name lookup + aliases, JSON for GET /plugins |
engine/src/hot-step-sampler.h | Sampling loop: solver/guidance dispatch, final-step x0, vt snapshot, post_step gating, linker sentinel |
engine/src/sampler-schedule.h | Scheduler dispatch, shift back-calculation, composite schedulers |
engine/src/pipeline-synth-ops.cpp | Postprocess plugin caller + fallback (ops_vae_decode_postprocess, ~line 1721) |
engine/src/hot-step-params.h | plugin_params map + legacy sideband params |
engine/tools/hot-step-server.cpp | Registry init (~2676), GET /plugins (~2716), plugin_params JSON parse (~771) |
server/src/routes/plugins.ts | Node proxy /api/plugins + 60 s cache + /reload |
server/src/services/generation/translateParams.ts | UI params → engine request JSON (plugin_params, postprocess_plugin) |
ui/src/components/global-bar/PluginControls.tsx | Renders declared params; accent map |
ui/src/stores/globalParamsStore.ts | hs-pluginParams localStorage persistence |
engine/plugins/ + plugins/ | Built-in and user plugin tiers |
docs/dev/plugins-authoring.md | Committed authoring guide (mostly accurate; see caveats in reference.md) |
plugins/README.md | Community plugins folder README: points at the authoring guide; its full-loop solver section is accurate |
| Symptom | Cause → fix |
|---|---|
| Plugin absent from UI dropdown | Lua 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 lists | Node 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-only | Wrote to vt / pred_cond / pred_uncond. Write to xt / result / scratch tables |
attempt to call a nil value (global 'apg') at load | Called 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 Euler | Expected the original velocity after calling model_fn — read original from the snapshot arg vt, fresh results from vt_buf |
| First step of 2nd generation explodes | Stateful 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 nothing | Key mismatch vs schema key; edited apg.lua (native bypass); or values persisted under an old plugin name |
Guidance post_step never fires | guidance_scale <= 1 (no CFG), final step, or past the cfg_cutoff_ratio step (CFG turned off) |
| Every solver/scheduler/guidance dead after upstream sync | pipeline-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 guidance | Each eval_cond/eval_uncond in post_step is a full forward pass — budget them |
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:
| Sampler | Call sites | Notes |
|---|---|---|
| ACE-Step DiT | hot-step-sampler.h | the original; time-major [T][Oc], t descends 1→0 |
| MiniMax-Music3 flow DiT | minimax/mm3-plugins.h | sigma ascends, latents channel-major — the bridge flips both |
| StableStep / SA3 refine | sa3-refine.h | t 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.diff = 0 and pass through unchanged; only
plugins doing cond-side work have any effect.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.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.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.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").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).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.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.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
SKILL.md and 1 other file in .claude/skills/lua-plugin-authoring of scragnog/HOT-Step-CPP.
Open the folder on GitHubat commit 91e92a8
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.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Lua Plugin Authoring this skillscragnog/HOT-Step-CPP | 171 | — | ~7k | Automated safety check: Pass | MIT | |
| OpenROAD Module Test AdderThe-OpenROAD-Project/OpenROAD | 3.2k | — | ~1.8k | Automated safety check: Pass | BSD-3-Clause | |
| Minitest Ebt Luakokusenz/deltaview.nvim | 127 | — | ~4k | Automated safety check: Pass | MIT | |
| Nvim TestS1M0N38/love2d.nvim | 215 | — | ~770 | Automated safety check: Pass | MIT | |
| Test Case ReducerArabelaTso/Skills-4-SE | 253 | — | ~2.5k | Automated safety check: Pass | Apache-2.0 | |
| Symbolic Execution AssistantArabelaTso/Skills-4-SE | 253 | — | ~3.5k | Automated safety check: Pass | Apache-2.0 |
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.
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.
S1M0N38/love2d.nvim
Execute tests and diagnose failures for love2d.nvim. An agent skill from S1M0N38/love2d.nvim.
ArabelaTso/Skills-4-SE
Automatically reduces bug-triggering test cases to minimal form while preserving the failure.
ArabelaTso/Skills-4-SE
Performs symbolic execution to detect potential errors by exploring execution paths, solving path constraints, and generating test inputs.
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.
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…
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.
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.
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.
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.
scragnog/HOT-Step-CPP
Safely pulls upstream acestep.cpp changes into the HOT-Step engine fork without destroying its integration hooks.
Categories
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.
Lua Plugin Authoring fits situations like: modifying a sampling solver; noise scheduler; CFG/guidance mode; VAE-decode postprocess plugin.
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.
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.
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.
Going by SKILL.md and its folder, Lua Plugin Authoring needs the command-line tools its instructions call (cmake).
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.
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.
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.
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.
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.
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.