Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ cargo test --locked

This crate holds only the *music* part of the agent. The ACP server, session/turn loop, approval, workspace tools (`read_file`, `write_file`, `run_command`, ...), terminal UI and MCP layer all come from the Onde Agent Platform crates (`ed-acp`, `ed-acp-tui`, `ed-mcp`, from the `onde-ed` workspace). Look there for anything protocol- or session-related; the README says it is checked out next to this repo.

- `src/profile.rs`: `SplitFire` implements `ed_acp::Profile`. It is the single wiring point: identity (`INFO`, env prefix `SPLITFIRE`), system prompt, toolsets (`WorkspaceTools` + `MusicTools`), slash commands, and the lazily started built-in `demucs --mcp` server (`SharedServer`; a client-supplied MCP server named `demucs` wins).
- `src/profile.rs`: `SplitFire` implements `ed_acp::Profile`. It is the single wiring point: identity (`INFO`, env prefix `SPLITFIRE`), system prompt, toolsets (`WorkspaceTools` + `MusicTools`), slash commands, and the lazily started built-in `demucs --mcp` server (`SharedServer`; a client-supplied MCP server named `demucs` wins). Embedding apps use that: the separator passes its own `--mcp` server, and KaroKowe Jam wraps `SplitFire` and returns no built-in servers.
- `src/prompt.rs`: system prompt and slash commands (`/analyze`, `/chords`, `/practice`, `/stems` only when separation is available).
- `src/tools/`: `MusicTools` is one `Toolset` exposing six `theory_*` tools, `analyze_audio` and `list_separations`. All are read-only and never prompt for approval. New tools need an entry in `definitions()`, `handles()` and `describe()`.
- `src/theory/` (pitch, interval, scale, chord, key, tempo) and `src/audio/` (symphonia decode, loudness/tempo/key analysis) are the pure computation behind the tools. The design principle is that the model calls these instead of recalling answers, so results must be exact (correct enharmonic spelling, e.g. C# major has E# and B#).
Expand Down
7 changes: 7 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Link the MSVC C runtime statically on Windows. Without this the binary
# imports VCRUNTIME140.dll and the UCRT api-ms-win-crt-*.dll set, and on a
# machine without the Visual C++ Redistributable it dies at load time with
# STATUS_DLL_NOT_FOUND (0xC0000135) before main runs. winget ships splitfire-agent as a
# portable exe, so there is no installer to bring the runtime along.
[target.'cfg(all(windows, target_env = "msvc"))']
rustflags = ["-C", "target-feature=+crt-static"]
311 changes: 311 additions & 0 deletions .github/workflows/release-winget.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
name: winget Release

# Dispatched manually once the GitHub release and its checksums.txt exist.
#
# winget manifests live in microsoft/winget-pkgs, and `wingetcreate update`
# only works on a package that is already there. So this workflow branches:
#
# - package already in winget-pkgs -> `wingetcreate update`, which rewrites
# the URL and re-hashes the installer for us.
# - package not there yet -> render the manifests in packaging/winget/ and
# `wingetcreate submit` them as a new package.
#
# The second path only runs once, but keeping it here means a first release
# does not need someone to sit at a Windows box running `wingetcreate new`
# interactively.
#
# splitfire-agent ships a single Windows x64 zip (splitfire-agent-windows-x64.zip).
# The SHA256 is extracted from the combined checksums.txt that the release
# workflow uploads after all build jobs finish.
on:
workflow_dispatch:
inputs:
tag:
description: "Release tag (e.g. v1.1.1)"
required: true

concurrency:
# Queue a re-dispatched tag behind the original instead of cancelling it:
# a cancelled mid-publish run can leave a channel half-updated, whereas a
# queued duplicate just hits the "already published" check and exits clean.
group: ${{ github.workflow }}-${{ github.event.inputs.tag || github.ref_name }}
cancel-in-progress: false

permissions:
contents: read

env:
REPO: SplitFireAI/splitfire-agent
PACKAGE_IDENTIFIER: SplitFireAI.splitfire-agent

jobs:
submit-winget-manifest:
name: Submit winget manifest
# wingetcreate is a Windows-only tool.
runs-on: windows-latest

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Resolve tag, version and release date
id: release
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="${{ github.event.inputs.tag }}"
VERSION="${TAG#v}"
# wingetcreate wants ReleaseDate as YYYY-MM-DD.
DATE=$(gh release view "$TAG" --repo "$REPO" --json publishedAt --jq '.publishedAt[0:10]')

echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "date=${DATE}" >> "$GITHUB_OUTPUT"

- name: Extract SHA256 from checksums.txt
id: sha
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="${{ steps.release.outputs.tag }}"
mkdir -p artifacts
gh release download "$TAG" --repo "$REPO" --pattern "checksums.txt" --dir artifacts/

# checksums.txt lines: "<hash> <filename>"
# The schema wants 64 uppercase hex characters.
SHA_X64=$(grep 'splitfire-agent-windows-x64\.zip' artifacts/checksums.txt \
| awk '{print $1}' | tr '[:lower:]' '[:upper:]')

if [ -z "$SHA_X64" ]; then
echo "::error::Could not find splitfire-agent-windows-x64.zip in checksums.txt"
exit 1
fi

echo "sha_x64=${SHA_X64}" >> "$GITHUB_OUTPUT"

- name: Check whether the package is already in winget-pkgs
id: exists
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
# Manifests are filed under the lowercased first letter of the
# publisher, e.g. manifests/s/SplitFireAI/splitfire-agent.
PUBLISHER="${PACKAGE_IDENTIFIER%%.*}"
NAME="${PACKAGE_IDENTIFIER#*.}"
FIRST=$(echo "${PUBLISHER:0:1}" | tr '[:upper:]' '[:lower:]')
PATH_IN_REPO="manifests/${FIRST}/${PUBLISHER}/${NAME}"

if gh api "repos/microsoft/winget-pkgs/contents/${PATH_IN_REPO}" >/dev/null 2>&1; then
echo "Found ${PACKAGE_IDENTIFIER} at ${PATH_IN_REPO}; submitting an update."
echo "found=true" >> "$GITHUB_OUTPUT"
else
echo "No ${PACKAGE_IDENTIFIER} at ${PATH_IN_REPO}; submitting it as a new package."
echo "found=false" >> "$GITHUB_OUTPUT"
fi

- name: Render manifests for a first submission
if: steps.exists.outputs.found == 'false'
shell: bash
run: |
TAG="${{ steps.release.outputs.tag }}"
VERSION="${{ steps.release.outputs.version }}"
SHA_X64="${{ steps.sha.outputs.sha_x64 }}"
RELEASE_DATE="${{ steps.release.outputs.date }}"

mkdir -p manifests

for template in packaging/winget/*.yaml.in; do
out="manifests/$(basename "$template" .in)"
sed \
-e "s|@VERSION@|${VERSION}|g" \
-e "s|@TAG@|${TAG}|g" \
-e "s|@REPO@|${REPO}|g" \
-e "s|@SHA_X64@|${SHA_X64}|g" \
-e "s|@RELEASE_DATE@|${RELEASE_DATE}|g" \
"$template" > "$out"

echo "--- ${out} ---"
cat "$out"
done

- name: Download wingetcreate
shell: pwsh
run: Invoke-WebRequest -Uri "https://aka.ms/wingetcreate/latest" -OutFile wingetcreate.exe

# winget-pkgs convention: file the "please add/update this package"
# Package Request issue *first*, then open the manifest PR that fulfils
# it with "Resolves #N". The other way round reads as a request and its
# fix posted seconds apart.
#
# The issue and the PR are authored by two separate PATs, both with
# public_repo scope on microsoft/winget-pkgs (fork + open issues/PRs):
# WINGET_ISSUE_TOKEN files the Package Request issue.
# WINGET_PR_TOKEN opens the manifest PR. It also needs read:org and
# read:discussion: the "Set the winget PR body" step
# calls `gh pr edit`, whose GraphQL query resolves
# org/team and discussion fields and fails with a
# scope error without them.
- name: File the winget Package Request issue
Comment thread
setoelkahfi marked this conversation as resolved.
id: issue
shell: bash
env:
GH_TOKEN: ${{ secrets.WINGET_ISSUE_TOKEN }}
run: |
set -euo pipefail

if [ -z "${GH_TOKEN:-}" ]; then
echo "::error::WINGET_ISSUE_TOKEN is not set. It needs a PAT (public_repo) for the account that should author the Package Request issue."
exit 1
fi

VERSION="${{ steps.release.outputs.version }}"
TAG="${{ steps.release.outputs.tag }}"
INSTALLER="https://github.com/${REPO}/releases/download/${TAG}/splitfire-agent-windows-x64.zip"

# winget-pkgs' Package Request is an issue *form*
# (.github/ISSUE_TEMPLATE/package_request.yml): the title must be
# "[Package Request]: ..." and the body has to carry the labelled
# fields below (Installer Url especially). A free-text body lands as
# Needs-Triage and can't be acted on.
ISSUE_BODY=$(cat <<EOF
Download Page Url: https://github.com/${REPO}/releases/tag/${TAG}
Publisher: SplitFireAI
Package Name: SplitFire Agent
Description: Music-domain ACP agent for theory, production, industry, culture and live shows.
Package Version: ${VERSION}
Installer Url: ${INSTALLER}
EOF
)

ISSUE_URL=$(gh issue create \
--repo microsoft/winget-pkgs \
--title "[Package Request]: ${PACKAGE_IDENTIFIER} ${VERSION}" \
--body "$ISSUE_BODY")

echo "Opened issue ${ISSUE_URL}"
echo "issue_url=${ISSUE_URL}" >> "$GITHUB_OUTPUT"
echo "issue_number=${ISSUE_URL##*/}" >> "$GITHUB_OUTPUT"

# Leave a gap so the issue and the PR aren't posted seconds apart.
- name: Sebat dulu ...
if: steps.issue.outputs.issue_number != ''
shell: bash
run: sleep $(( RANDOM % 61 + 150 ))

- name: Submit new package
id: submit_new
if: steps.exists.outputs.found == 'false'
shell: pwsh
env:
WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_PR_TOKEN }}
Comment thread
setoelkahfi marked this conversation as resolved.
run: |
if (-not $env:WINGET_CREATE_GITHUB_TOKEN) {
Write-Error "WINGET_PR_TOKEN is not set. It needs a PAT with public_repo, read:org and read:discussion scopes so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR."
}

$version = "${{ steps.release.outputs.version }}"

# Tee the output so we can scrape the PR url wingetcreate prints on success.
# --no-open because the runner has no browser to open the PR in.
.\wingetcreate.exe submit manifests `
--prtitle "New package: $env:PACKAGE_IDENTIFIER version $version" `
--no-open 2>&1 | Tee-Object -Variable output

if ($LASTEXITCODE -ne 0) {
Write-Error "wingetcreate submit failed with exit code $LASTEXITCODE"
}

$pr = ($output | Select-String -Pattern 'https://github\.com/microsoft/winget-pkgs/pull/\d+' `
| Select-Object -First 1).Matches.Value
if ($pr) {
$number = $pr -replace '.*/pull/',''
"pr_url=$pr" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
"pr_number=$number" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
Write-Host "Opened $pr"
} else {
Write-Warning "Could not scrape a PR url from wingetcreate output."
}

- name: Submit manifest update
id: submit_update
if: steps.exists.outputs.found == 'true'
shell: pwsh
env:
WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_PR_TOKEN }}
run: |
if (-not $env:WINGET_CREATE_GITHUB_TOKEN) {
Write-Error "WINGET_PR_TOKEN is not set. It needs a PAT with public_repo, read:org and read:discussion scopes so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR."
}

$tag = "${{ steps.release.outputs.tag }}"
$version = "${{ steps.release.outputs.version }}"
$base = "https://github.com/${env:REPO}/releases/download/$tag"

# Single x64 zip installer — no arch suffix needed in the URL hint.
# Tee the output so we can scrape the PR url wingetcreate prints on success.
.\wingetcreate.exe update $env:PACKAGE_IDENTIFIER `
--version $version `
--urls "$base/splitfire-agent-windows-x64.zip|x64" `
--release-notes-url "https://github.com/${env:REPO}/releases/tag/$tag" `
--release-date "${{ steps.release.outputs.date }}" `
--prtitle "Update $env:PACKAGE_IDENTIFIER to version $version" `
--submit `
--no-open 2>&1 | Tee-Object -Variable output

if ($LASTEXITCODE -ne 0) {
Write-Error "wingetcreate failed with exit code $LASTEXITCODE"
}

$pr = ($output | Select-String -Pattern 'https://github\.com/microsoft/winget-pkgs/pull/\d+' `
| Select-Object -First 1).Matches.Value
if ($pr) {
$number = $pr -replace '.*/pull/',''
"pr_url=$pr" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
"pr_number=$number" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
Write-Host "Opened $pr"
} else {
Write-Warning "Could not scrape a PR url from wingetcreate output."
}

# Tie the manifest PR back to the tracking issue and tick the submission
# checklist winget-pkgs asks for. Runs under WINGET_PR_TOKEN so the comment/edit
# is attributed to the same identity that opened the PR.
- name: Set the winget PR body
id: link
if: steps.issue.outputs.issue_number != '' && (steps.submit_new.outputs.pr_number != '' || steps.submit_update.outputs.pr_number != '')
shell: bash
env:
GH_TOKEN: ${{ secrets.WINGET_PR_TOKEN }}
run: |
set -euo pipefail

if [ -z "${GH_TOKEN:-}" ]; then
echo "::error::WINGET_PR_TOKEN is not set. It needs a PAT that can edit PRs on microsoft/winget-pkgs."
exit 1
fi

ISSUE_NUMBER="${{ steps.issue.outputs.issue_number }}"
PR_NUMBER="${{ steps.submit_new.outputs.pr_number }}${{ steps.submit_update.outputs.pr_number }}"
VERSION="${{ steps.release.outputs.version }}"

# printf, not an indented heredoc: GitHub dedents only the common leading
# whitespace of the run block, which would leave a leading-space-prefixed
# closing EOF that bash would not recognise as the terminator.
BODY=$(printf '%s\n' \
"Resolves #${ISSUE_NUMBER}" \
"" \
"${PACKAGE_IDENTIFIER} version ${VERSION}." \
"" \
"- [x] Have you signed the [Contributor License Agreement](https://cla.opensource.microsoft.com/microsoft/winget-pkgs)?" \
"- [x] Have you checked that there aren't other open [pull requests](https://github.com/microsoft/winget-pkgs/pulls) for the same manifest update/change?" \
"- [x] Have you validated your manifest locally with \`winget validate --manifest <path>\`?" \
"- [x] Have you tested your manifest locally with \`winget install --manifest <path>\`?" \
"- [x] Does your manifest conform to the [1.12 schema](https://github.com/microsoft/winget-pkgs/tree/master/doc/manifest/schema/1.12.0)?" \
"- [x] This PR is linked to an issue.")

gh pr edit "$PR_NUMBER" \
--repo microsoft/winget-pkgs \
--body "$BODY"
39 changes: 37 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,19 @@ jobs:
with:
targets: ${{ matrix.target }}
- run: cargo build --release --locked --bin splitfire-agent --target ${{ matrix.target }}
# The exe must not import the VC++ runtime DLLs (see .cargo/config.toml), or it fails to
# load on a machine without the redistributable.
- name: Check static CRT
if: matrix.os == 'windows-latest'
shell: pwsh
run: ./packaging/windows/check-static-crt.ps1 -Path target/${{ matrix.target }}/release/splitfire-agent.exe
- name: Package archive
shell: bash
run: |
BIN=target/${{ matrix.target }}/release/splitfire-agent
ARCHIVE=splitfire-agent-${{ matrix.name }}
if [ "${{ matrix.os }}" = "windows-latest" ]; then
7z a "$ARCHIVE.zip" "$BIN.exe"
# exe at the zip root, matching RelativeFilePath in the winget manifest
(cd "target/${{ matrix.target }}/release" && 7z a "$GITHUB_WORKSPACE/$ARCHIVE.zip" splitfire-agent.exe)
echo "asset=$ARCHIVE.zip" >> "$GITHUB_ENV"
else
tar -czf "$ARCHIVE.tar.gz" -C "target/${{ matrix.target }}/release" splitfire-agent
Expand All @@ -48,3 +54,32 @@ jobs:
gh release create "$GITHUB_REF_NAME" --verify-tag --generate-notes \
|| gh release view "$GITHUB_REF_NAME" >/dev/null
gh release upload "$GITHUB_REF_NAME" "$asset" --clobber

checksums:
name: Publish checksums.txt
needs: build
runs-on: ubuntu-latest
steps:
- name: Download all release assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p artifacts
gh release download "$GITHUB_REF_NAME" \
--repo "$GITHUB_REPOSITORY" \
--pattern "splitfire-agent-*" \
--dir artifacts/

- name: Generate checksums.txt
run: |
cd artifacts
sha256sum splitfire-agent-* > checksums.txt
Comment thread
setoelkahfi marked this conversation as resolved.
cat checksums.txt

- name: Upload checksums.txt
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload "$GITHUB_REF_NAME" \
--repo "$GITHUB_REPOSITORY" \
artifacts/checksums.txt --clobber
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ prompt tells you how large the download is. If `demucs` is missing or fails to s
logs one line and carries on without it. An MCP server named `demucs` supplied by your editor
takes precedence. `SPLITFIRE_MCP_DEMUCS=off` disables it.

Apps that embed SplitFire decide what stems mean the same way. They pass a `demucs` MCP server of
their own in `session/new`, or return no built-in servers from their `Profile` and write their own
stems guidance. The SplitFire AI Audio Separator supplies its own executable in `--mcp` mode on
its on-device engine; KaroKowe Jam supplies none and points users at its in-app flow. The stem
hooks only check the tool name `mcp__demucs__separate_stems` and the `list_models` and
`separate_stems` result shapes (`structuredContent.models[]{id,size_mb,cached}` and
`structuredContent.stems[].path`).

Separate only music you have the right to work with.

### Other MCP servers
Expand Down
Loading
Loading