| name | release |
| description | Cut a versioned per-target release of Oedipa. m4l publishes to GitHub Releases (tag + asset + notes); vst is local-only since the paid pivot (CMakeLists VERSION bump + `make release-vst` to produce signed/notarized dmg AND pkg in dist/, no tag, no GH release — upload to the paid distribution platform happens out of band). Verifies repo state, bumps semver, runs the build (vst), drafts notes (m4l), and walks each step with explicit user approval. `none` bump (vst only) keeps the current version and regenerates artifacts — for doc-only fixes to bundled files. |
| argument-hint | <m4l|vst> [major|minor|patch|none] |
| allowed-tools | Read, Write, Edit, Bash(git *), Bash(gh *), Bash(stat *), Bash(ls *), Bash(rm /tmp/oedipa-*), Bash(make release-vst), Bash(make release-m4l*), Bash(xcrun stapler validate *) |
Release Oedipa
Cut a versioned per-target release. The first $ARGUMENT selects the
target (m4l or vst); the second is the bump (major / minor /
patch / none, default patch). none is vst-only — keep the
current version and regenerate artifacts only.
m4l publishes to GitHub Releases with tags namespaced as
m4l-vX.Y.Z. vst is local-only since the paid pivot — no tag, no GH
release; the in-tree vst/CMakeLists.txt project(... VERSION …)
is the version source of record, and the dmg is uploaded to the paid
platform out of band.
The legacy v0.1.0 tag (m4l-only, pre-vst introduction) is retained
as historical. The first per-target release of m4l is m4l-v0.1.1
(or later); vst's vst-v0.1.0 / v0.1.1 / v0.1.2 tags remain in
the repo as source history with no GH release entries.
The release asset is target-specific, and the filename always
embeds the version (-vX.Y.Z) so multiple builds can coexist in
dist/ and the Save As default name in Max already carries the
version. dist/ only ever holds frozen / signed-and-notarized
artefacts.
- m4l →
dist/Oedipa-vX.Y.Z.amxd — frozen .amxd. The skill
bakes + stages an un-frozen versioned copy at
m4l/Oedipa-vX.Y.Z.amxd (gitignored) in Step 1.6; the user opens
that file in Max, Freezes, and Save-As's into dist/ (default
filename is already correct). See
ADR 007.
- vst →
dist/Oedipa-vX.Y.Z.pkg (recommended installer) and
dist/Oedipa-vX.Y.Z.dmg (drag-to-install fallback) — both signed /
notarized / stapled, built in lockstep by make release-vst. The
build scripts read the version from vst/CMakeLists.txt.
Distribution is via the paid platform (out of band, not GitHub).
Both artifacts are uploaded. See
ADR 009.
⚠️ vst paid pivot (2026-05-11; Polar selected 2026-05-16). vst does
not publish to GitHub. No tag is created, no GH release is created, no
asset is uploaded. /release vst ends locally at Step 1.6: a
signed/notarized/stapled dist/Oedipa-vX.Y.Z.pkg and
dist/Oedipa-vX.Y.Z.dmg in dist/ + (when bumping) a CMakeLists
VERSION bump committed to main. The build itself (make release-vst)
runs inside the skill at Step 1.6. Uploading both artifacts to Polar
and writing the listing copy happens out of band, outside this skill —
the skill does not draft release notes for vst.
Existing vst-v0.1.0 / v0.1.1 / v0.1.2 tags remain in the repo as
source history; their GH release entries (which previously carried the
dmg) are already deleted. No new vst tags will be created going forward.
m4l remains a free GitHub Releases distribution (tag + release + asset).
Pre-flight checks (do these BEFORE the publish step)
Once a release ships (m4l: tag pushed + GH release; vst: dmg
uploaded to the paid platform), it is harder to undo cleanly. Run
all checks; STOP and ask the user if any fail.
Check 1 — Working tree is clean
git status --porcelain must be empty. Uncommitted changes leak
into the release context. Halt if dirty.
Check 2 — main is synced with origin
git fetch origin --quiet
git rev-list --count main..origin/main
git rev-list --count origin/main..main
If origin is ahead, git pull. If local is ahead, push first
(via /commit for any unstaged work, then a normal push). Then
re-run.
Check 3 — CI is green on HEAD
gh run list --branch main --limit 5 --json conclusion,headSha,workflowName
The most recent completed run for the current HEAD SHA must have
conclusion: "success". If still in progress or failed, halt and
ask. Don't ship distribution artifacts past a red gate — chasing
"probably just CI flake" has historically masked real regressions.
(For m4l, the asset freshness gate now lives in Step 1.6 — the
bake + stage + freeze flow runs after Step 1 picks the version, so
the dist filename dist/Oedipa-vX.Y.Z.amxd isn't known yet at
pre-flight time. For vst, the build is run by the skill itself
in Step 1.6 and the artifact freshness check lives there too.)
Drafting
After pre-flight passes:
Step 1 — Determine next version
The second $ARGUMENT is the bump (default patch). none is
vst-only and means "keep the current version, just regenerate
artifacts" — useful when only bundled docs / readmes change and the
plug-in binary doesn't actually need a new version.
For m4l, parse the highest m4l-v* tag and bump per the second
$ARGUMENT:
git tag -l 'm4l-v*' | sort -V | tail -1
(none is rejected for m4l — m4l version metadata isn't in-tree, so
re-freezing at the same version would just produce a duplicate tag,
which the skill blocks.)
For vst, no tags are created anymore — the in-tree
project(Oedipa VERSION X.Y.Z) line in vst/CMakeLists.txt is the
authoritative previous version. Bump from there, or keep with none:
grep '^project(Oedipa VERSION' vst/CMakeLists.txt
If no prior version exists for this target, propose m4l-v0.1.0 /
0.1.0.
Show the proposed version to the user and confirm before
proceeding. The user can override.
Step 1.5 — Bump version metadata (vst only; skipped on none)
If Step 1 resolved to a new version (bump): edit
vst/CMakeLists.txt so project(Oedipa VERSION X.Y.Z) matches the
target version. Also prompt the user to add a Changelog entry to
vst/scripts/README.txt describing what changed in this release —
that file ships inside the dmg and is the only user-visible
changelog (vst has no GH release notes since the paid pivot). Wait
for the user to write the entry; do not auto-generate from git log.
Then commit and push to main BEFORE the build runs in Step 1.6. The
plist version reported to the DAW and the v… label drawn in the
editor header both come from the CMakeLists line; the __VERSION__
placeholder in welcome / conclusion / INSTALL / README files is
substituted at build time by build-pkg.sh / build-dmg.sh (do not
hard-code the version in those files).
git add vst/CMakeLists.txt vst/scripts/README.txt
git commit -m "chore(vst): bump version to X.Y.Z"
git push origin main
If Step 1 was none (regen at same version): skip this step
entirely — nothing to commit, nothing to push. Proceed to Step 1.6
so the build picks up whatever other source changes triggered the
regen (e.g. bundled README / INSTALL.txt fixes).
For m4l this step is skipped — m4l version metadata isn't
in-tree (the freeze captures whatever is on disk).
Step 1.6 — Build / stage and verify (target-specific)
For vst, run make release-vst to produce both artifacts
(codesign + notarize + staple + dmg + pkg are wrapped in the script
chain):
make release-vst
This takes a few minutes — notarization is the long leg (waits on
Apple's service). Requires DEVELOPER_TEAM_ID env var +
im9-notary keychain profile + Developer ID Application and
Developer ID Installer certs in the login keychain. If
make release-vst fails, halt and surface the error; do not retry
blindly — notarization rejections, expired certs, and missing env
all need investigation before re-run.
After build, verify both artifacts are present, fresh, and have a
stapled notarization ticket (filenames carry the version parsed by
the build scripts from vst/CMakeLists.txt):
ls -la dist/Oedipa-vX.Y.Z.pkg dist/Oedipa-vX.Y.Z.dmg
stat -f '%m' dist/Oedipa-vX.Y.Z.pkg
stat -f '%m' dist/Oedipa-vX.Y.Z.dmg
git log -1 --format=%ct -- vst/Source/ \
vst/CMakeLists.txt \
vst/scripts/ \
vst/tests/
xcrun stapler validate dist/Oedipa-vX.Y.Z.pkg
xcrun stapler validate dist/Oedipa-vX.Y.Z.dmg
Both dist/Oedipa-vX.Y.Z.pkg AND dist/Oedipa-vX.Y.Z.dmg mtimes
must be >= the latest vst-source commit time, and stapler validate must succeed on both. If either check fails, halt —
something went wrong in build or notarization.
Manual host smoke (Logic AU MIDI FX + Bitwig VST3 MIDI fx) is
recommended before handing the artifacts off.
vst stops here. Steps 2-5 are m4l-only. Upload both artifacts to
the paid distribution platform and write the listing copy out of
band — the skill does not interact with the platform directly.
For m4l, run make release-m4l with the version from Step 1
to bake the host bundle and stage a versioned, un-frozen copy of
the baked .amxd next to the source bake target. dist/ is for
frozen artefacts only — the un-frozen staging file lives in m4l/
(gitignored as m4l/Oedipa-v*.amxd) so the Save As dialog opens
with the versioned filename pre-filled when the user freezes.
make release-m4l VERSION=X.Y.Z
Output: m4l/Oedipa-vX.Y.Z.amxd (un-frozen). Then prompt the user:
Open m4l/Oedipa-vX.Y.Z.amxd in Max → click the snowflake
(Freeze) button in the patcher toolbar → File → Save As →
navigate to dist/ and save (the default filename
Oedipa-vX.Y.Z.amxd is already correct — just confirm the
location).
Wait for the user to confirm freeze + Save As is complete
before continuing. Then verify the saved .amxd exists in dist/
and its mtime is fresher than the latest m4l-source commit:
stat -f '%m' dist/Oedipa-vX.Y.Z.amxd
git log -1 --format=%ct -- m4l/Oedipa.maxpat \
m4l/cellstrip-renderer.js \
m4l/lattice-renderer.js \
m4l/separator-renderer.js \
m4l/oedipa-host.entry.mjs \
m4l/engine m4l/host
If dist/Oedipa-vX.Y.Z.amxd is missing or older, halt and re-prompt
— the user didn't actually Save As into dist/.
Manual smoke test in a fresh Live track is recommended before
tagging — drag dist/Oedipa-vX.Y.Z.amxd onto a new MIDI track,
confirm it loads, plays, slot rehydrate works. CI does not (and
cannot) cover this.
Step 2 — Draft release notes (m4l only)
For vst, skip this step — the skill does not draft listing copy.
Write the product / release description for the paid distribution
platform out of band.
For m4l, compute the previous-release boundary (highest m4l-v*
tag) and generate the changelog from the commit log between it and
HEAD:
PREV=$(git tag -l 'm4l-v*' | sort -V | tail -1)
git log "${PREV:-}"..HEAD --pretty=format:'- %s' -- m4l/
If $PREV is empty (first m4l release), use the project root as the
lower bound.
Categorize commits by their type: prefix into sections:
- Features —
feat:
- Fixes —
fix:
- Docs / housekeeping —
docs: / chore: / style: / refactor:
- CI / build —
ci:
Drop the Co-Authored-By lines and trailing housekeeping noise.
Keep the section short — release notes are for users, not
contributors; detailed history is in git log.
For the very first m4l release, use a project-intro template
("What it does" / "Install" / "Requirements") instead of a changelog.
Write the draft to /tmp/oedipa-m4l-vX.Y.Z-notes.md and show it to
the user. Wait for explicit "ok" or edit instructions before
continuing. The file is fed to gh release create --notes-file in
Step 3.
Step 3 — Tag, push, create release (m4l only)
For vst, skip this step entirely — see the paid-pivot block at
the top. vst's flow already ended at Step 1.6 with both artifacts in
dist/ and (when bumping) the CMakeLists bump committed; paid-
platform upload happens out of band.
TAG=m4l-vX.Y.Z
ASSET=dist/Oedipa-vX.Y.Z.amxd
TITLE="Oedipa m4l vX.Y.Z"
git tag "$TAG"
git push origin "$TAG"
gh release create "$TAG" "$ASSET" \
--title "$TITLE" \
--notes-file "/tmp/oedipa-$TAG-notes.md"
Step 4 — Verify (m4l only)
gh release view "$TAG" --json name,tagName,assets,url
Confirm:
assets[0].name is Oedipa-vX.Y.Z.amxd.
assets[0].size > 0 and matches the local file's size.
- The release URL is reachable.
Show the release URL to the user.
Step 5 — Cleanup (m4l only)
rm "/tmp/oedipa-m4l-vX.Y.Z-notes.md"
Rules
- Asset is target-specific. m4l →
dist/Oedipa-vX.Y.Z.amxd
(frozen); vst → both dist/Oedipa-vX.Y.Z.pkg
(signed/notarized/stapled installer) and dist/Oedipa-vX.Y.Z.dmg
(signed/notarized/stapled drag-to-install). Never mix.
- Filenames embed the version. vst build scripts read the
version from
vst/CMakeLists.txt (single source of truth). m4l's
make release-m4l takes VERSION=X.Y.Z from the skill (m4l
version metadata isn't in-tree) and copies the baked .amxd to
m4l/Oedipa-vX.Y.Z.amxd (un-frozen, gitignored). The user opens
that in Max, Freezes, and Save-As's into dist/ — the Save As
dialog pre-fills the versioned filename, so they just confirm.
dist/ is for frozen / shipped artefacts only. Un-frozen
staging files live in m4l/ (gitignored) for m4l, and signed +
notarized bundles + dmg + pkg land directly in dist/ for vst.
Never write un-frozen .amxd into dist/.
- m4l publishes to GitHub; vst does not. m4l tags
m4l-vX.Y.Z,
creates a GH release, attaches the .amxd. vst skips Step 3/4
entirely — no tag, no GH release, no asset upload. vst's dmg is
handed off to the paid platform out of band.
- vst version source of record = CMakeLists. Since vst has no
tags going forward,
project(Oedipa VERSION X.Y.Z) in
vst/CMakeLists.txt is the single authoritative version. Bump at
Step 1.5 (skipped on none) and commit + push before Step 1.6
builds the artifacts.
- Manual Freeze required for m4l. Max has no CLI freeze; this
skill does not automate it.
make release-vst runs inside the skill (Step 1.6). Both the
dmg and the pkg are built + signed + notarized + stapled by the
script chain (codesign.sh → notarize.sh → build-dmg.sh →
build-pkg.sh). The skill invokes it after Step 1.5 — the user
doesn't run it manually. Requires DEVELOPER_TEAM_ID +
im9-notary keychain profile + signing certs in the login
keychain (see ADR 009).
none bump is vst-only. A vst regen at the same version is
for doc-only fixes (bundled README / INSTALL.txt) where the
plug-in binary doesn't need a version change. Step 1.5 is skipped;
Step 1.6 still rebuilds artifacts so the new bundled files land in
dmg + pkg.
- Tag once, never re-tag (m4l). If an
m4l-v* tag for the
proposed version already exists, bump again rather than
overwrite. Force-deleting a tag that was already pushed is messy
and breaks anyone who pulled it.
- Notes via
--notes-file, not --notes (m4l). The temp-file
flow lets the user edit before publish. vst has no notes-drafting
step — write the paid-platform listing copy out of band.
- Halt on any pre-flight failure. Don't release past a red
gate.
- Halt on any user-confirmation gate. Steps 1 (version) and 2
(notes) require explicit "ok" — don't proceed silently.