build
Build the JRE-PA-Simulator Windows executable + staged distribution folder (no GitHub release). Embeds version in exe metadata.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Build the JRE-PA-Simulator Windows executable + staged distribution folder (no GitHub release). Embeds version in exe metadata.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | build |
| description | Build the JRE-PA-Simulator Windows executable + staged distribution folder (no GitHub release). Embeds version in exe metadata. |
| triggers | ["/build","build exe","build executable","build distribution"] |
Reproduce the PyInstaller build locally: produce a one-file exe with version metadata embedded, stage the distribution folder (dist-release/JRE-PA-Simulator/), and stop there. Zipping and GitHub release are separate, opt-in steps — release.ps1 is the all-in-one path; this skill is the "just build and let me test" path.
Version (e.g. 0.5.2, v0.5.2, 0.5.2b).
version_info.txt; the running app reads it back through app_paths.app_version() for the update check — so the version you stamp at build time IS the single source of truth, no separate constant.)a, b, c are sequential sub-revisions of the same patch (user's scheme). Do not treat b as "beta" and suppress it anywhere — it must survive into the exe metadata and filenames verbatim.v, then always re-add v in output filenames (see Step 3). So 0.5.2 and v0.5.2 both produce JRE-PA-Simulator-v0.5.2-distribution.zip.(major, minor, patch, sub) for the Windows version resource:
major.minor.patch from the numeric components (missing → 0).sub from the trailing letter: a→1, b→2, c→3, … (one-letter case ord(letter) - ord('a') + 1). No letter → sub = 0.0.5.2 → (0, 5, 2, 0); 0.5.2a → (0, 5, 2, 1); 0.5.2b → (0, 5, 2, 2); v0.5.2c → (0, 5, 2, 3).[a-z] trailing suffix: stop, ask the user to clarify — do not silently drop it.FileVersion, ProductVersion): preserve the full normalized string without the v prefix, letter intact. E.g. v0.5.2b → "0.5.2b".If not provided, ask: "What version should I tag this build? (e.g. 0.5.2)". Wait for answer before proceeding.
After version is confirmed, run all sub-steps in sequence without pausing. If any sub-step fails, surface the error and stop.
2a — Dep pre-flight
uv run _harness/check_deps.py
If non-zero, stop and surface the violations. See critical_lessons.md §3.
2b — Generate version_info.txt
Write a PyInstaller Windows version resource to the project root. Overwrite any existing file. Template:
VSVersionInfo(
ffi=FixedFileInfo(
filevers=(MAJOR, MINOR, PATCH, BUILD),
prodvers=(MAJOR, MINOR, PATCH, BUILD),
mask=0x3f,
flags=0x0,
OS=0x40004,
fileType=0x1,
subtype=0x0,
date=(0, 0)
),
kids=[
StringFileInfo(
[
StringTable(
u'040904B0',
[StringStruct(u'CompanyName', u''),
StringStruct(u'FileDescription', u'JR East PA Simulator'),
StringStruct(u'FileVersion', u'VERSION_STRING'),
StringStruct(u'InternalName', u'JRE-PA-Simulator'),
StringStruct(u'OriginalFilename', u'JRE-PA-Simulator.exe'),
StringStruct(u'ProductName', u'JRE-PA-Simulator'),
StringStruct(u'ProductVersion', u'VERSION_STRING')])
]),
VarFileInfo([VarStruct(u'Translation', [1033, 1200])])
]
)
Substitute MAJOR/MINOR/PATCH/BUILD (numbers, per the parsing rules above — BUILD comes from the subversion letter) and VERSION_STRING (the user's string, leading v stripped, trailing letter preserved).
The file lives at project root and is gitignored — it's a per-build artifact, not source. Overwrite freely.
2c — Clean & build
dist-release/JRE-PA-Simulator/audio may be a junction from a previous run (pointing at the project's real audio/). A naive Remove-Item -Recurse will follow the junction and delete your real audio files. Always break the junction first:
# Break audio junction if it exists (don't recurse into the real audio/!)
$audioJunction = "dist-release\JRE-PA-Simulator\audio"
if (Test-Path $audioJunction) {
$item = Get-Item $audioJunction -Force
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
[System.IO.Directory]::Delete($item.FullName, $false)
}
}
Remove-Item -Path "dist", "dist-release", "build" -Recurse -Force -ErrorAction SilentlyContinue
uv run --no-dev --group build pyinstaller --onefile --console --name "JRE-PA-Simulator" main.py --clean --noconfirm --version-file version_info.txt --collect-data plotly
--no-dev --group build isolates the build venv to prod deps + pyinstaller only — no librosa / ffmpeg-python / black / pyright visible to PyInstaller's static analysis. Defense-in-depth against accidental dev-dep bundling. The build dependency group is declared in pyproject.toml; if a future build step needs another tool (e.g. UPX), add it there.
--console is required — a console window is needed for error visibility on non-English Windows (where Japanese stdout requires PYTHONUTF8=1 or sys.stdout.reconfigure('utf-8'), and silent crashes are otherwise invisible). If the build fails, surface the pyinstaller error verbatim and stop.
--collect-data plotly ships plotly's package_data/ subdirectory — specifically plotly.min.js, the ~3MB JS bundle that fig.to_html(include_plotlyjs='inline') reads at runtime. PyInstaller's static import analysis bundles plotly's .py files but skips non-Python data files; without this flag, the Report ↓ button in the OCR debug panel silently breaks in release builds (lib loads, but its JS bundle is missing → render-time crash swallowed by the try/except in auto_input/driver.py:_render_report_async). Discovered by /review+fix Lens 1 on 2026-04-30 reviewing commit 51c7b07. If a future runtime-asset-shipping lib enters dependencies (matplotlib, bokeh, ...), add a sibling --collect-data <lib> here.
2d — Stage distribution folder (with audio junction for testing)
The shipped zip ships the audio folder populated with all real route data (excluding audio/_*/ — preserved-but-not-shipped). During smoke-test we want the staged folder to be immediately runnable without first copying ~600 MB of audio, so we use a junction: dist-release/JRE-PA-Simulator/audio points at the project's real audio/. At zip time, Step 3 breaks the junction and replaces it with a real directory containing the shippable subset.
Inclusion model — default-ship, not hand-picked. Stage every top-level project-root directory by default; maintain only an exclusion list. This solves the recurring "we forgot to add the new asset folder" class (2026-05-05 line_icons + ocr_templates) — new folders ship automatically; if a folder shouldn't ship, you add it to $shipExclude in a single visible action. The cost asymmetry is heavy in favor of over-shipping: missing-required-asset = release crash; extra-shipped-folder = a few MB in the zip.
New-Item -ItemType Directory -Force -Path "dist-release\JRE-PA-Simulator" | Out-Null
Copy-Item "dist\JRE-PA-Simulator.exe" "dist-release\JRE-PA-Simulator\"
# Default-ship every top-level directory at project root, excluding:
# - `_*` prefix (preserved-not-shipped: _archive, _mock, _dev_scripts, ...)
# - `.*` prefix (.git, .venv, .claude, .github, .vscode, .idea, ...)
# - Hard-listed dev / repo-only / build-output folders below.
$shipExclude = @(
'dist', 'dist-release', 'build', # build outputs (would self-recurse)
'displays', 'auto_input', 'setup_tims', # Python source — bundled INTO exe by PyInstaller, not alongside
'memory', 'lcd_references', 'tims_references', # repo-only / dev refs
'audio_src', 'assets' # dev tooling / repo-only
)
$shipDirs = Get-ChildItem -Path "." -Directory | Where-Object {
$_.Name -notmatch '^[_.]' -and $_.Name -notin $shipExclude
}
Write-Host "Shipping top-level directories:" -ForegroundColor Cyan
$shipDirs | ForEach-Object { Write-Host " $($_.Name)" }
foreach ($dir in $shipDirs) {
if ($dir.Name -eq 'audio') {
# audio/ — junction during smoke test (Step 3 breaks + replaces with real copy at zip time)
$projectAudio = (Resolve-Path "audio").Path
New-Item -ItemType Junction -Path "dist-release\JRE-PA-Simulator\audio" -Target $projectAudio | Out-Null
} else {
# Recursive copy, excluding `_*` harness subdirs (matches the audio/_*/ pattern)
$destDir = "dist-release\JRE-PA-Simulator\$($dir.Name)"
New-Item -ItemType Directory -Force -Path $destDir | Out-Null
Get-ChildItem -Path $dir.FullName | Where-Object { $_.Name -notmatch '^_' } | ForEach-Object {
Copy-Item -Path $_.FullName -Destination $destDir -Recurse -Force
}
}
}
$shipDirs is a soft guard — eyeball-confirm what's being staged at the start of every build. If something appears that shouldn't, add to $shipExclude (a deliberate, visible action) and re-run._visual_iter/, _recordings/, audio_src/) — convention is _* prefix or .* prefix; otherwise add to $shipExclude. New shipped folders need no skill edit at all._* filter applies recursively — data/_*, fonts/_*, ocr_templates/_* would all be excluded if added in future, matching the audio/_*/ Step 3 pattern.audio/ directory — Path(sys.executable).parent / "audio" / ... resolves through transparently.Remove-Item -Recurse the staged folder without breaking the junction first (see Step 2c's guard above).Compress-Archive follows the junction transparently. We still break + replace before zipping in Step 3 — both because we need to exclude audio/_*/ from the shipped zip (the junction would pull them in) and because junctions inside zips are messy on extraction.2e — Launch exe + HARD STOP for smoke test
Auto-launch the exe from the staged folder so the user doesn't have to hunt for it. Use Start-Process (non-blocking — it returns immediately; the exe runs in its own window and does not tie up this shell):
Start-Process -FilePath "dist-release\JRE-PA-Simulator\JRE-PA-Simulator.exe" `
-WorkingDirectory "dist-release\JRE-PA-Simulator"
Setting -WorkingDirectory matches what happens when the user double-clicks in Explorer. The exe itself uses sys.executable for path resolution, so CWD doesn't affect fonts/ / data/ / audio/ loading — but log files and crash dumps land next to the working directory.
Then report:
dist\JRE-PA-Simulator.exe)dist-release\JRE-PA-Simulator\ — audio/ is a junction to <project-root>/audio/, so real routes are testable)Do not proceed further until the user explicitly confirms the smoke test passed. Launching the exe ≠ verifying it works — fonts can fail to load, JSON can be missing, audio routes can misbehave, and you will not see any of that from here. Only the user can verify.
Do NOT:
Start-Process succeeding means the app is running correctly — it only means the OS accepted the launch request. Font loading, JSON path resolution, and mixer init all fail post-launch if they fail at all.Wait for an explicit "works / ok / ship it / zip it" from the user before Step 3.
The staged audio/ is a junction to the project's real audio/. We need to break it and replace with a real directory containing only the line folders that ship — excluding audio/_*/ (preserved-but-not-shipped: _archive/, _mock/).
# Break the audio junction (deletes the junction entry, NOT the target)
$audioJunction = "dist-release\JRE-PA-Simulator\audio"
$item = Get-Item $audioJunction -Force
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
[System.IO.Directory]::Delete($item.FullName, $false)
} else {
# Defensive: if it's not a junction, something is off — abort rather than risk deleting real audio
throw "Expected $audioJunction to be a junction, found $($item.Attributes). Aborting zip."
}
New-Item -ItemType Directory -Force -Path $audioJunction | Out-Null
# Copy each line folder under audio/ that is NOT `_`-prefixed (~600 MB at time of writing)
Get-ChildItem -Path "audio" -Directory | Where-Object { $_.Name -notmatch '^_' } | ForEach-Object {
Copy-Item -Path $_.FullName -Destination $audioJunction -Recurse -Force
}
# Remove smoke-test-generated runtime state (see note below) before zipping.
Remove-Item -Path "dist-release\JRE-PA-Simulator\settings.json" -Force -ErrorAction SilentlyContinue
# Zip
Compress-Archive -Path "dist-release\JRE-PA-Simulator" -DestinationPath "dist-release\JRE-PA-Simulator-v<VERSION>-distribution.zip" -Force
The _* exclusion is critical: _archive/ (working backups, Sobu reference recordings, etc.) and _mock/ (preview-only test catalog) must never reach end users — those are repo-internal scaffolding.
Smoke-test self-pollution — strip settings.json before zip. i18n.py writes settings.json to project_root(), which in the exe resolves to Path(sys.executable).parent = the staged folder. The Step 2e smoke-test launch therefore creates dist-release\JRE-PA-Simulator\settings.json (e.g. {"language": "en", "oobe_completed": true}) carrying the tester's language choice + a completed-OOBE flag. If zipped, end users skip the first-run language picker and inherit the tester's locale. The Remove-Item above deletes it pre-zip. Any future runtime-written user-state file at project root (logs, caches, crash dumps from the smoke test) needs the same treatment — they only appear after Step 2e, so the include-everything staging in Step 2d can't pre-empt them.
Never use Remove-Item -Recurse -Force $audioJunction — Remove-Item with -Recurse on a junction follows the reparse point and deletes the real audio directory. Use [System.IO.Directory]::Delete(path, false) instead, which removes only the junction entry.
Filename version: always v + the normalized numeric+letter string (e.g. v0.5.2, v0.5.2b). If the user typed v0.5.2, strip their v first and re-add one — never produce vv0.5.2.
After zipping: the staged folder now has a populated real audio/ directory (~600 MB), not the junction. If the user wants to keep iterating with the staged folder against live audio edits, re-run /build to recreate the junction in Step 2d. Mention zip size in the final report — typical ship: ~660 MB (exe + fonts + data + audio); GitHub release file limit is 2 GB so there's headroom.
After the user has confirmed the smoke test and zipped (Step 3), the publish flow continues in /release. That skill picks up here: pre-flights the build artifacts, drafts release_notes.md with the criteria below, tags the commit, and hands the gh release create command to the user.
Don't run /release automatically — wait for the user to invoke it. /build ends at "zip ready on disk."
gh release create from this skill. That's /release's job. If the user wants a release, point them at /release <version>.pyproject.toml or any other source. The version is a build-time label only._* folder convention (preserved-but-not-shipped)Folders prefixed with _ under audio/ (e.g. audio/_mock/, audio/_archive/) are preserved in the repo but must not ship to end users. Step 3 explicitly enforces this via the Where-Object { $_.Name -notmatch '^_' } filter when copying line folders into the staged audio directory. Same convention applies recursively to any future data/_*, fonts/_*, ocr_templates/_* — Step 2d's per-dir Get-ChildItem ... | Where-Object { $_.Name -notmatch '^_' } block already excludes them, so no skill edit is needed when new harness subdirs appear under shipped trees.
The smoke-test junction in Step 2d transparently includes _*/ folders — that's intentional. The user can preview-test against the mock catalog from inside the staged folder before the zip excludes them.
release.ps1 eventually runs)Even though this skill doesn't generate notes, when you do help assemble them (pre-writing release_notes.md before the user runs release.ps1), apply this rule:
Include a change in user-facing release notes iff the artifact it affects ships inside the distribution zip.
What ships: the exe, fonts/, data/*.json, audio/** (excluding audio/_*/). Anything that lands in dist-release/JRE-PA-Simulator/ qualifies.
What does not ship: README*.md (repo-only), CLAUDE.md, .claude/**, .github/**, memory/**, pyproject.toml, test/preview harnesses, the mock route catalog (audio/mock/**). Changes to these are invisible to end users of the exe → omit from notes.
Future: if manuals/guides are ever bundled into the distribution (e.g. dist-release/JRE-PA-Simulator/manual.pdf), they flip from "repo-only" to "shipped" and start qualifying.
Mixed commits (one commit touches both shipped and repo-only paths): report only the shipped-facing portion. The commit-hygiene skill (/commit) should catch cases where the shipped and repo-only pieces were bundled into one commit unnecessarily — read it for the reasonable-mixing vs. unrelated-mixing distinction.
JRE-PA-Simulator.exe) so the fonts/ + data/ folder layout stays canonical and any external scripts/shortcuts don't break per release. Version is discoverable via Windows' native Properties dialog.You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation.
Picks up after /build to publish a GitHub release. Pre-flights the staged artifacts, drafts release_notes.md from commits since the last tag, tags master, then hands the gh release create upload command to the user. Stops short of running the upload — that's user-driven.
Spawn a fresh-context Opus 4.7 agent to give an independent take when Claude and the user are talking past each other on a contested logic interpretation, OR to validate behavioral self-observations from /session-recap Step 0. User-triggered or claude self-proposed at impasse.
End-of-session recap — codify learnings synchronously into their canonical homes; daily logs hold narrative continuity only.
Review dirty code and fix iteratively using Ralph loop pattern. When user say to "loop to fix dirty" or "review+fix"
Review dirty code changes using Claude Code Agent tool. When user say to "review" or "review changes" or "review dirty code"