Validating Changes to the Required Standard
SkillMediaDefines the verification bar every HOT-Step CPP change must clear before being called done — per-tier checks (TypeScript, C++ engine, UI, audio, end-to-end smoke generation) and honest result reporting. Use when finishing any code change, deciding whether work is "done", running a smoke/e2e test, or reporting test/validation results.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Validating Changes to the Required Standard skill
What this skill tells your AI
The instructions your AI receives, as published by scragnog/hot-step-cpp in .claude/skills/validating-changes/SKILL.md and read by ahel’s review.
HOT-Step CPP is a 3-tier app: a C++17/CUDA/GGML engine (engine/, spawned as
ace-server.exe on port 8085), a Node/TypeScript/Express server (server/src/,
port 3001), and a React 19 UI (ui/src/, Vite dev server on port 3000). A change
is NOT done until it clears the bar for every tier it touches. This skill defines
those bars, how to run a headless end-to-end smoke generation, and how to report
results honestly.
All commands are Windows PowerShell (; as separator, never &&).
Deep API/log detail lives in reference.md in this folder.
When to use this skill
- You just finished a code change and need to decide whether it is "done".
- You need to verify a C++ engine change, a server/UI TypeScript change, or a Lua plugin change actually works.
- You need to run an end-to-end generation without the UI (headless smoke test).
- You are about to report validation results to the user.
Golden rules (hard constraints — each prevents expensive damage)
-
ONLY the human can judge audio quality, by ear. Never claim audio "sounds fine" or "is broken" from metrics, file size, duration, RMS, spectrograms, log silence, or the pipeline's quality-evaluator scores (stored in the
quality_scoresDB column — advisory telemetry only). Say: "generation completed mechanically; audio at<path>awaits your ear." WHY: metrics have repeatedly failed to predict what the user hears; false verdicts destroy trust and mislead debugging. -
PRESERVE EXPERIMENT ARTIFACTS. Never delete generated test audio, latents, or other outputs because you predict they are bad. The human verifies by ear and has lost artifacts to premature cleanup before. WHY: a generation costs GPU time and a seed; a deleted artifact may be unreproducible evidence.
-
Report outcomes faithfully. Failed tests are reported as FAILED with the actual output/error pasted. Skipped steps are reported as SKIPPED with the reason. Never write hedged "should work" / "probably fine". Distinguish what you ran and saw vs what you inferred vs what you did not verify. WHY: the user makes release and debugging decisions off your report.
-
Rebuild C++ only via
dev-rebuild.batat repo root — neverengine\build.cmddirectly, under any circumstances. WHY: you cannot reliably tell whether the app is running; the Node server auto-respawns ace-server 3 s after a non-clean exit (server/src/index.ts:284-308), and the respawned exe file-locks the linker output — infinite loop.dev-rebuild.batis safe even when the app is stopped (its shutdown request is a suppressed no-op). -
Never push a
v*tag to "test" a build. WHY: any pushedv*tag triggers the full multi-platform Release workflow and drafts a GitHub Release. Throwaway compile checks use a-CI-Testsuffix; any push still requires explicit user approval. -
Never
cmake --build . --clean-firstunless the GGML/CUDA layer itself changed. WHY: CUDA kernel recompilation takes 20+ minutes. For stale-.objsymptoms delete onlyengine/build/acestep-core.dir/andengine/build/Release/acestep-core.lib. -
No
npm run buildduring dev. Type-check withtsconly (§ below). WHY: builds are slow and unnecessary until the user tests a prod (LAUNCH.bat) flow. -
Never visually verify UI with a browser agent — ask the human; they provide screenshots. Browser/
Invoke-RestMethodIS fine for non-visual API checks. WHY: the browser agent is too slow/unreliable in this environment. -
A feature is not done until a USER can run it. Anything the app resolves at runtime — model weights, a data file, a binary — must be reachable by someone who downloaded a release, not just present on this machine. Run
node server/scripts/check-release-prereqs.mjsbefore any push (§ Tier 6). WHY: this has shipped broken twice. MM3 training asked for two GGUFs that existed only on the dev box (#137), and the MM3 caption corpus was committed but never copied into the archives (#139). Both passed tsc, both built green, both were dead on arrival for every user. -
Don't clobber the user's generation queue. Jobs run one at a time on a single GPU. Check
GET /api/generate/queuebefore submitting a smoke generation, and never callPOST /api/generate/reset-queue(force-drains everything) without asking. WHY: it cancels the user's in-flight work.
Tier 1 — TypeScript (server and UI)
Bar: type-check clean. Zero errors.
Node 18–22 LTS only — Node 24+ breaks dependencies (
engines: >=18.0.0 <24.0.0inserver/package.json). If tsc/tsx/npm fails oddly, checknode --versionBEFORE blaming the change — a wrong Node version produces false validation verdicts.
cd D:\Ace-Step-Latest\hot-step-cpp\server; npx tsc --noEmit
cd D:\Ace-Step-Latest\hot-step-cpp\ui; npx tsc -b
server/package.jsondefines"typecheck": "tsc --noEmit". The UI uses project references (ui/tsconfig.json→tsconfig.app.json+tsconfig.node.json, bothnoEmit-safe), sonpx tsc -btype-checks without emitting JS.- In dev mode (
dev.bat) the server runs undertsx watch(server/package.json"dev": "tsx watch src/index.ts") — it auto-restarts on save. Server-side bar = passing tsc plus a clean restart visible in the newestlogs/<session>/node_console.log(no stack trace after the restart).
Tier 2 — C++ engine
Bar: rebuild compiles AND the engine starts clean after relaunch (log check).
-
Recompile immediately after editing any
engine/src/orengine/tools/file — don't wait to be asked:D:\Ace-Step-Latest\hot-step-cpp\dev-rebuild.batWhat it does (verified in the script): POSTs
http://localhost:3001/api/shutdown(kills ace-server, Vite, and the Node server cleanly), waits up to 10 s forace-server.exeto exit, force-kills at 10 s, aborts at 15 s, then runsengine\build.cmd. -
The rebuild does NOT restart the app. Relaunch (
dev.batfor dev,LAUNCH.batfor prod) before any runtime verification. -
Clean-start check — read the newest
logs/<session>/ace_engine.logand confirm:- model registry scan ran (
[Server] Scanning models in .../[Registry] ...lines), [Server] Listening on 127.0.0.1:8085appears,- no crash/exception before the Listening line,
node_console.loghas no[ace-server] Restarting in 3 seconds...(that exact string = crash respawn,server/src/index.ts:304).
$sess = (Get-ChildItem D:\Ace-Step-Latest\hot-step-cpp\logs | Sort-Object Name -Descending | Select-Object -First 1).FullName Select-String -Path "$sess\ace_engine.log" -Pattern 'Listening on' Select-String -Path "$sess\node_console.log" -Pattern 'Restarting in 3 seconds' - model registry scan ran (
-
After any upstream sync (the engine is a patched fork of acestep.cpp):
powershell -File D:\Ace-Step-Latest\hot-step-cpp\engine\verify-hooks.ps1Exit 0 = good. Note: CLAUDE.md says "3 hook files" but the script checks 5 things (the script is the truth):
pipeline-synth-ops.cpp → hot-step-sampler.h(loss is SILENT — compiles fine but all solvers/guidance/schedulers go dead),model-store.h → hot-step-params.h,dit.h → adapter-merge.h+adapter-runtime.h,tools/hot-step-server.cpp → hot-step-params.h, and a linker sentinelhotstep_sampler_linked_inhot-step-sampler.h. -
Lua plugins (
engine/plugins/) need no rebuild, but they load at engine start — relaunch the app for a new/edited plugin to appear.
Tier 3 — UI visuals
Bar: npx tsc -b clean + Vite HMR applies the change without console errors +
the human confirms the visuals. You cannot close this tier yourself. Ask the
user to look and wait for their screenshot/feedback.
Tier 4 — Audio quality
Bar: the human's ear. There is no mechanical substitute — see Golden rules 1–2.
Your job ends at: generation succeeded mechanically, here is the file path
(server/data/audio/<uuid>.wav or http://localhost:3001/audio/<uuid>.wav), here
are the seed and params for reproduction. Then wait.
Tier 5 — End-to-end headless smoke generation
Bar: job reaches status: "succeeded", a WAV exists on disk, and the per-generation
log ends with GENERATION COMPLETED. (Mechanical success only — quality is Tier 4.)
Full request/response/log detail: reference.md.
-
Pre-flight health — require
engine.ready -eq $true:Invoke-RestMethod http://localhost:3001/api/health -
Check the queue is free (don't queue behind/ahead of the user silently):
Invoke-RestMethod http://localhost:3001/api/generate/queue -
Get an auth token (
POST /api/generatereturns 401 without a Bearer token; tokens are in-memory and reset on server restart):$auth = Invoke-RestMethod http://localhost:3001/api/auth/auto $hdr = @{ Authorization = "Bearer $($auth.token)" } -
Submit a fast smoke request —
skipLm: $trueskips the language-model phase (the "LM" that writes audio codes/metadata before the DiT diffusion transformer runs). Always passdurationexplicitly: with skipLm the server defaults an unset duration to 120 s (generate.ts:222).$body = @{ prompt = 'minimal ambient techno, warm pads' instrumental = $true skipLm = $true duration = 30 inferenceSteps = 8 seed = 42 # fixed seed = reproducible; or randomSeed = $true } | ConvertTo-Json $job = Invoke-RestMethod -Method Post -Uri http://localhost:3001/api/generate -Headers $hdr -ContentType 'application/json' -Body $body $job.jobIdOmit
skipLmif your change touches the LM phase — then the smoke must exercise it. -
Poll to completion (status values:
pending | lm_running | synth_running | saving | succeeded | failed | cancelled):do { Start-Sleep 3; $s = Invoke-RestMethod "http://localhost:3001/api/generate/status/$($job.jobId)"; "{0} {1}% {2}" -f $s.status, $s.progress, $s.stage } while ($s.status -notin 'succeeded','failed','cancelled') $s | ConvertTo-Json -Depth 5Built-in watchdogs (
pollUntilDone,generate.ts:102-170): 2 min with no stage/progress change ⇒ "Generation stalled"; wall-clock default 45 min. A first-ever TensorRT run legitimately showsBuilding TRT engine ... (first run only, ~5-10 min)— do not panic-cancel it. There is 1 automatic retry with a randomized seed on transient failure. -
Confirm outputs. On success
$s.result.audioUrlsis like["/audio/<uuid>.wav"]. The live data dir isserver\data, NOT the repo-rootdata\(verified:config.data.dirresolves relative toserver/src,server/src/config.ts:191-199; rootdata/is stale/legacy — never "verify" outputs there). Files land inserver\data\audio\; DB row in thesongstable ofserver\data\hotstep.dbwith the full request ingeneration_params. -
Confirm the log. Per-generation logs are buffered in memory and flushed only at finish/fail — mid-run, tail
node_console.log(lines prefixed[Gen:<jobId8>]) orace_engine.loginstead.$sess = (Get-ChildItem D:\Ace-Step-Latest\hot-step-cpp\logs | Sort-Object Name -Descending | Select-Object -First 1).FullName Get-Content (Get-ChildItem "$sess\generations" | Sort-Object LastWriteTime -Descending | Select-Object -First 1).FullName -Tail 40Success ends
GENERATION COMPLETED.; failure endsGENERATION FAILED: <error>(server/src/services/logger.ts:145,167). -
Hand off to the human (Tier 4): file path + seed + params. Do not delete the artifacts.
Tier 6 — Distributability (does it work for someone who is not you?)
Bar: node server/scripts/check-release-prereqs.mjs exits 0.
Applies to any change that adds a file the app looks for at runtime — a new model, a new data file, a new registry entry, a new packaged binary. This tier is invisible to every other bar: the path is resolved at runtime, so tsc passes, the build is green, and the feature is dead in every download.
node D:\Ace-Step-Latest\hot-step-cpp\server\scripts\check-release-prereqs.mjs
# --offline to skip the Hugging Face half
What it checks:
- Weights — every entry in
server/src/data/model-registry.jsonexists in the Hugging Face repo it names, at the size the registry claims, in a repo that is public. Companion files (LICENSE) too. - Packs — no pack references a file id that does not exist (that is how the Model Manager blanked in #120).
- Runtime data — everything in
server/src/data/is packaged by.github/workflows/release.yml, which copies the directory wholesale for all three platforms. Put new runtime data there and it ships automatically; put it anywhere else and it will not.
What it CANNOT check, so do it by hand:
- A model resolved by prefix scan rather than by registry id.
resolveMm3TrainModelsinserver/src/services/training/mm3Train.tstakes the newestmm3-rvq-*.ggufon disk — the registry has to publish something that matches the prefix, and only a human knows that.fs.existsSyncor a directory scan against a models dir is the smell; grep for those when adding a feature. - Anything gated behind an HF token or a licence the user has to accept.
- Weights you converted locally. Uploading is a deliberate act:
huggingface_hubis installed and the token is in~/.cache/huggingface/token, but ask before pushing anything to a public repo, and credit the original author in the model card if the weights are not ours.
Key files
| Path | Role |
|---|---|
dev-rebuild.bat | The ONLY sanctioned C++ rebuild entry point (shutdown → build) |
engine/verify-hooks.ps1 | Post-upstream-sync check of the 5 fork hooks |
server/src/routes/generate.ts | Generation queue, POST /api/generate, status/queue/cancel endpoints, watchdogs |
server/src/routes/health.ts | GET /api/health — engine readiness |
server/src/routes/auth.ts | GET /api/auth/auto — free Bearer token; getUserId guard |
server/src/services/generation/translateParams.ts | UI param names → engine AceRequest names |
server/src/services/aceClient.ts | HTTP client to ace-server :8085 (single-threaded httplib caveat) |
server/src/services/logger.ts | Session log folders + buffered per-generation logs |
server/src/config.ts | Ports, paths; config.data.dir = server/data |
logs/YYYY-MM-DD_HH-MM-SS/ | Per-session logs: ace_engine.log, node_console.log, generations/gen_<jobId>_<task>.log |
server/data/audio/ | Where generated audio actually lands |
Failure signatures
| Symptom | Cause → fix |
|---|---|
503 Engine not ready: ... on POST /generate | ace-server still bootstrapping — wait; check /api/health engine.bootStatus |
| 401 Unauthorized | missing/stale Bearer token (in-memory, resets on Node restart) — re-hit /api/auth/auto |
Generation stalled — no progress for Ns | 2-min watchdog fired; engine wedged or emitting unrecognized progress — check ace_engine.log tail |
Generation failed on ace-server | engine-side failure — the real error is in ace_engine.log, not the Node response |
[ace-server] Restarting in 3 seconds... in node_console.log | engine crashed; repeated ⇒ missing DLL or a bad C++ change |
/api/health bootStatus Engine crashed N times — check logs for missing DLLs | crash limiter gave up (index.ts:296-302) — fix the crash, relaunch |
| Solvers/schedulers/guidance silently ignored after upstream sync (no error!) | lost hot-step-sampler.h hook — run engine/verify-hooks.ps1 |
| Adapter/sideband params vanish between LM and synth phases | LM echo drops ServerFields-only params; synth requests are rebuilt from the current request + LM fields only (generate.ts:312-328) — new sideband params must flow through that reconstruction, never a whitelist |
First run stuck on Building TRT engine 5–10 min | normal one-time TensorRT compile, not a hang |
| HTTP timeouts against :8085 during a generation | normal — ace-server is single-threaded httplib and can't answer mid-DiT-step (aceClient.ts:6-8) |
Output file "missing" from repo-root data\ | wrong directory — live outputs are in server\data\audio\ |
Institutional knowledge
- VALIDATED (ground truth): PRESERVE EXPERIMENT ARTIFACTS — never delete generated test audio/outputs on your own prediction that they are bad; the human verifies by ear and has lost artifacts to premature cleanup before.
- VALIDATED (ground truth): report outcomes faithfully — failed = FAILED with output pasted; skipped = SKIPPED with reason; no hedged "should work".
- VALIDATED: audio quality is human-ear-only;
quality_scorestelemetry is advisory, never a verdict. - VALIDATED: live data dir is
server\data, rootdata\is stale (config.ts:191-199+ runtime log confirm). - VALIDATED:
verify-hooks.ps1checks 5 hooks, not the 3 CLAUDE.md lists — the script is the truth. - VALIDATED (code):
adapter_section_isolationstill exists on the wire (aceClient.ts,translateParams.ts) but the feature was reverted (commitee041e1, broke musical continuity) — don't "fix" it back in or count it as active. - UNVALIDATED: basin re-base params
rebase_source/rebase_betaexist intranslateParams.tsbut the cross-base fix is unvalidated — never report it working. - DELIBERATE, not a bug: draft-LM speculative decoding is disabled by a code
comment in
server/src/config.ts(~line 117). - VALIDATED (twice, in production): local presence is not distribution.
MM3 training required
mm3-rvq-*.gguf+mm3-enc-*.ggufthat had never been uploaded (#137, fixed 96d442fb);mm3-corpus.jsonwas committed but not copied into release archives (#139). Tier 6 exists because of these. - VALIDATED: smoke generations queue globally one-at-a-time behind the user's
jobs — check
GET /api/generate/queuefirst;reset-queueis destructive.
Deeper reading
- reference.md — same folder: full API shapes, output-file table, log anatomy, all generate endpoints.
CLAUDE.md(repo root) — orientation map, build/git rules.engine/docs/ARCHITECTURE.md— engine internals, request JSON, generation modes.docs/PLUGINS.md— Lua plugin authoring (plugins hot-load, no rebuild).docs/RELEASING.md— release process (any pushedv*tag triggers a full multi-platform CI build — never push casualv*tags).docs/plans/— internal design/investigation docs. Gitignored, local-only — may be absent on a fresh clone.
Signals
- GitHub stars
- 137
- Forks
- 20
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
validating-changes- Source
- github.com/scragnog/hot-step-cpp