From 10621e6371085d20d7a25e4358366bc17b3d335e Mon Sep 17 00:00:00 2001 From: Seto Date: Sun, 11 Oct 2026 19:04:35 +0200 Subject: [PATCH 1/5] Document how embedding apps supply their own stems Hosts decide what stems mean through a client-supplied demucs MCP server, or by returning no built-in servers from their Profile. The README and AGENTS.md now say so, and name the two result shapes the stem hooks read. Co-Authored-By: Claude Sonnet 5.5 --- .agents/AGENTS.md | 2 +- README.md | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md index 649da70..a7c870d 100644 --- a/.agents/AGENTS.md +++ b/.agents/AGENTS.md @@ -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#). diff --git a/README.md b/README.md index 6c156de..d710ec3 100644 --- a/README.md +++ b/README.md @@ -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 From 2f65da67ece5b0e716d2abbdb7135a16791bfc95 Mon Sep 17 00:00:00 2001 From: Seto Date: Sun, 11 Oct 2026 22:48:40 +0200 Subject: [PATCH 2/5] Add winget package manifests and release workflow - packaging/winget/SplitFireAI.splitfire-agent.{yaml,installer,locale.en-US}.yaml.in: manifest templates with @VERSION@/@SHA_X64@/@RELEASE_DATE@ placeholders - .github/workflows/release.yml: append checksums job that concatenates all per-platform SHA256 files into a combined checksums.txt and uploads it as a release asset (required by the winget workflow) - .github/workflows/release-winget.yml: manually triggered workflow that extracts the x64 SHA256 from checksums.txt, detects whether the package already exists in winget-pkgs, and either submits a new package via `wingetcreate submit` or updates an existing one via `wingetcreate update` Co-Authored-By: siGit Code v1.6.7-acp --- .github/workflows/release-winget.yml | 185 ++++++++++++++++++ .github/workflows/release.yml | 29 +++ ...itFireAI.splitfire-agent.installer.yaml.in | 17 ++ ...ireAI.splitfire-agent.locale.en-US.yaml.in | 32 +++ .../SplitFireAI.splitfire-agent.yaml.in | 6 + 5 files changed, 269 insertions(+) create mode 100644 .github/workflows/release-winget.yml create mode 100644 packaging/winget/SplitFireAI.splitfire-agent.installer.yaml.in create mode 100644 packaging/winget/SplitFireAI.splitfire-agent.locale.en-US.yaml.in create mode 100644 packaging/winget/SplitFireAI.splitfire-agent.yaml.in diff --git a/.github/workflows/release-winget.yml b/.github/workflows/release-winget.yml new file mode 100644 index 0000000..9973138 --- /dev/null +++ b/.github/workflows/release-winget.yml @@ -0,0 +1,185 @@ +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: " " + # 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 + + - name: Submit new package + if: steps.exists.outputs.found == 'false' + shell: pwsh + env: + WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_TOKEN }} + run: | + if (-not $env:WINGET_CREATE_GITHUB_TOKEN) { + Write-Error "WINGET_TOKEN is not set. It needs a PAT with public_repo scope so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR." + } + + $version = "${{ steps.release.outputs.version }}" + + # --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 + + if ($LASTEXITCODE -ne 0) { + Write-Error "wingetcreate submit failed with exit code $LASTEXITCODE" + } + + - name: Submit manifest update + if: steps.exists.outputs.found == 'true' + shell: pwsh + env: + WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_TOKEN }} + run: | + if (-not $env:WINGET_CREATE_GITHUB_TOKEN) { + Write-Error "WINGET_TOKEN is not set. It needs a PAT with public_repo scope 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. + .\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 + + if ($LASTEXITCODE -ne 0) { + Write-Error "wingetcreate failed with exit code $LASTEXITCODE" + } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0eb9c50..2cb0070 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,3 +48,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 + 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 diff --git a/packaging/winget/SplitFireAI.splitfire-agent.installer.yaml.in b/packaging/winget/SplitFireAI.splitfire-agent.installer.yaml.in new file mode 100644 index 0000000..1134bce --- /dev/null +++ b/packaging/winget/SplitFireAI.splitfire-agent.installer.yaml.in @@ -0,0 +1,17 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.6.0.schema.json +PackageIdentifier: SplitFireAI.splitfire-agent +PackageVersion: @VERSION@ +InstallerType: zip +# `NestedInstallerType` and `NestedInstallerFiles` tell winget where to find +# the exe inside the zip and what command shim to place on PATH. +NestedInstallerType: portable +NestedInstallerFiles: + - RelativeFilePath: splitfire-agent.exe + PortableCommandAlias: splitfire-agent +ReleaseDate: "@RELEASE_DATE@" +Installers: + - Architecture: x64 + InstallerUrl: https://github.com/@REPO@/releases/download/@TAG@/splitfire-agent-windows-x64.zip + InstallerSha256: @SHA_X64@ +ManifestType: installer +ManifestVersion: 1.6.0 diff --git a/packaging/winget/SplitFireAI.splitfire-agent.locale.en-US.yaml.in b/packaging/winget/SplitFireAI.splitfire-agent.locale.en-US.yaml.in new file mode 100644 index 0000000..5d4197a --- /dev/null +++ b/packaging/winget/SplitFireAI.splitfire-agent.locale.en-US.yaml.in @@ -0,0 +1,32 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.6.0.schema.json +PackageIdentifier: SplitFireAI.splitfire-agent +PackageVersion: @VERSION@ +PackageLocale: en-US +Publisher: SplitFireAI +PublisherUrl: https://github.com/SplitFireAI +PublisherSupportUrl: https://github.com/@REPO@/issues +PackageName: SplitFire Agent +PackageUrl: https://github.com/@REPO@ +License: MIT OR Apache-2.0 +LicenseUrl: https://github.com/@REPO@/blob/main/LICENSE +ShortDescription: Music-domain ACP agent for theory, production, industry, culture and live shows. +Description: |- + SplitFire Agent is a music-domain ACP-compatible coding agent backed by any + OpenAI-compatible chat completions API. It covers music theory, production, + industry knowledge, culture, and live shows, and integrates with editors such + as Zed and VS Code via the Agent Client Protocol (ACP). +Moniker: splitfire-agent +Tags: + - ai + - acp + - agent + - music + - inference + - llm + - splitfire +ReleaseNotesUrl: https://github.com/@REPO@/releases/tag/@TAG@ +Documentations: + - DocumentLabel: Readme + DocumentUrl: https://github.com/@REPO@/blob/main/README.md +ManifestType: defaultLocale +ManifestVersion: 1.6.0 diff --git a/packaging/winget/SplitFireAI.splitfire-agent.yaml.in b/packaging/winget/SplitFireAI.splitfire-agent.yaml.in new file mode 100644 index 0000000..c28807a --- /dev/null +++ b/packaging/winget/SplitFireAI.splitfire-agent.yaml.in @@ -0,0 +1,6 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.6.0.schema.json +PackageIdentifier: SplitFireAI.splitfire-agent +PackageVersion: @VERSION@ +DefaultLocale: en-US +ManifestType: version +ManifestVersion: 1.6.0 From 2c75971246439ebb0871d96b35fc6a178c7fa477 Mon Sep 17 00:00:00 2001 From: Seto Date: Mon, 12 Oct 2026 16:27:52 +0200 Subject: [PATCH 3/5] =?UTF-8?q?ci:=20convert=20winget=20release=20to=20dua?= =?UTF-8?q?l-token=20issue=E2=86=92PR=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split the single WINGET_TOKEN into WINGET_ISSUE_TOKEN (files the tracking issue under one identity) and WINGET_PR_TOKEN (opens and edits the manifest PR under another), matching the winget-pkgs submission convention. The new link step ties the PR back to the issue and ticks the submission checklist. Co-Authored-By: siGit Code v1.6.7-acp --- .github/workflows/release-winget.yml | 124 +++++++++++++++++++++++++-- 1 file changed, 118 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release-winget.yml b/.github/workflows/release-winget.yml index 9973138..a3871a3 100644 --- a/.github/workflows/release-winget.yml +++ b/.github/workflows/release-winget.yml @@ -135,35 +135,95 @@ jobs: shell: pwsh run: Invoke-WebRequest -Uri "https://aka.ms/wingetcreate/latest" -OutFile wingetcreate.exe + # File the tracking issue first, as a separate identity (WINGET_ISSUE_TOKEN), + # so the manifest PR that wingetcreate opens next can resolve it. winget-pkgs + # asks submitters to link an issue; filing it here means the "Set the winget + # PR body" step can tick "Linked to an issue" and add "Resolves #N". + - name: File the winget tracking issue + 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 that can open issues on microsoft/winget-pkgs." + exit 1 + fi + + VERSION="${{ steps.release.outputs.version }}" + TAG="${{ steps.release.outputs.tag }}" + BASE="https://github.com/${REPO}/releases/download/${TAG}" + + if [ "${{ steps.exists.outputs.found }}" = "false" ]; then + TITLE="New package: ${PACKAGE_IDENTIFIER} version ${VERSION}" + ACTION="Add" + else + TITLE="Update: ${PACKAGE_IDENTIFIER} to version ${VERSION}" + ACTION="Update" + fi + + BODY=$(cat <> "$GITHUB_OUTPUT" + - name: Submit new package + id: submit_new if: steps.exists.outputs.found == 'false' shell: pwsh env: - WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_TOKEN }} + WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_PR_TOKEN }} run: | if (-not $env:WINGET_CREATE_GITHUB_TOKEN) { - Write-Error "WINGET_TOKEN is not set. It needs a PAT with public_repo scope so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR." + Write-Error "WINGET_PR_TOKEN is not set. It needs a PAT with public_repo scope 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 + --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_TOKEN }} + WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_PR_TOKEN }} run: | if (-not $env:WINGET_CREATE_GITHUB_TOKEN) { - Write-Error "WINGET_TOKEN is not set. It needs a PAT with public_repo scope so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR." + Write-Error "WINGET_PR_TOKEN is not set. It needs a PAT with public_repo scope so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR." } $tag = "${{ steps.release.outputs.tag }}" @@ -171,6 +231,7 @@ jobs: $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" ` @@ -178,8 +239,59 @@ jobs: --release-date "${{ steps.release.outputs.date }}" ` --prtitle "Update $env:PACKAGE_IDENTIFIER to version $version" ` --submit ` - --no-open + --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 \`?" \ + "- [x] Have you tested your manifest locally with \`winget install --manifest \`?" \ + "- [x] Does your manifest conform to the [1.6 schema](https://github.com/microsoft/winget-pkgs/tree/master/doc/manifest/schema/1.6.0)?" \ + "- [x] This PR is linked to an issue.") + + gh pr edit "$PR_NUMBER" \ + --repo microsoft/winget-pkgs \ + --body "$BODY" From 64c738d114487d4344369b02d130da399be194e0 Mon Sep 17 00:00:00 2001 From: Seto Date: Tue, 13 Oct 2026 13:51:03 +0200 Subject: [PATCH 4/5] ci: link the C runtime statically and file a Package Request issue for winget winget's validator runs the bare exe on a console and expects exit 0, and the exe has to load on a machine without the VC++ redistributable. - Link the MSVC runtime statically on Windows and fail the release build if the exe still imports vcruntime or ucrt DLLs (packaging/windows/check-static-crt.ps1). - Put splitfire-agent.exe at the zip root so it matches RelativeFilePath. - Print the missing-API-key hint and exit 0 when the TUI is launched bare. - File the winget-pkgs issue from the Package Request form fields, wait a few minutes, then open the PR with "Resolves #N". WINGET_PR_TOKEN needs read:org and read:discussion for the PR body edit. - Move the manifests to schema 1.12.0 and set License to Apache-2.0. Co-Authored-By: Claude Sonnet 5.5 --- .cargo/config.toml | 7 ++ .github/workflows/release-winget.yml | 72 +++++++++++-------- .github/workflows/release.yml | 10 ++- packaging/windows/check-static-crt.ps1 | 35 +++++++++ ...itFireAI.splitfire-agent.installer.yaml.in | 4 +- ...ireAI.splitfire-agent.locale.en-US.yaml.in | 6 +- .../SplitFireAI.splitfire-agent.yaml.in | 4 +- src/main.rs | 4 +- 8 files changed, 103 insertions(+), 39 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 packaging/windows/check-static-crt.ps1 diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..360835a --- /dev/null +++ b/.cargo/config.toml @@ -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"] diff --git a/.github/workflows/release-winget.yml b/.github/workflows/release-winget.yml index a3871a3..e741b2b 100644 --- a/.github/workflows/release-winget.yml +++ b/.github/workflows/release-winget.yml @@ -135,11 +135,20 @@ jobs: shell: pwsh run: Invoke-WebRequest -Uri "https://aka.ms/wingetcreate/latest" -OutFile wingetcreate.exe - # File the tracking issue first, as a separate identity (WINGET_ISSUE_TOKEN), - # so the manifest PR that wingetcreate opens next can resolve it. winget-pkgs - # asks submitters to link an issue; filing it here means the "Set the winget - # PR body" step can tick "Linked to an issue" and add "Resolves #N". - - name: File the winget tracking issue + # 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 id: issue shell: bash env: @@ -148,38 +157,43 @@ jobs: set -euo pipefail if [ -z "${GH_TOKEN:-}" ]; then - echo "::error::WINGET_ISSUE_TOKEN is not set. It needs a PAT that can open issues on microsoft/winget-pkgs." + 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 }}" - BASE="https://github.com/${REPO}/releases/download/${TAG}" - - if [ "${{ steps.exists.outputs.found }}" = "false" ]; then - TITLE="New package: ${PACKAGE_IDENTIFIER} version ${VERSION}" - ACTION="Add" - else - TITLE="Update: ${PACKAGE_IDENTIFIER} to version ${VERSION}" - ACTION="Update" - fi - - BODY=$(cat <> "$GITHUB_OUTPUT" + echo "issue_number=${ISSUE_URL##*/}" >> "$GITHUB_OUTPUT" - echo "Filed ${ISSUE_URL}" - ISSUE_NUMBER="${ISSUE_URL##*/}" - echo "issue_number=${ISSUE_NUMBER}" >> "$GITHUB_OUTPUT" + # Leave a gap so the issue and the PR aren't posted seconds apart. + - name: Wait before opening the PR + if: steps.issue.outputs.issue_number != '' + shell: bash + run: sleep $(( RANDOM % 61 + 150 )) - name: Submit new package id: submit_new @@ -189,7 +203,7 @@ jobs: 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 scope so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR." + 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 }}" @@ -223,7 +237,7 @@ jobs: 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 scope so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR." + 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 }}" @@ -289,7 +303,7 @@ jobs: "- [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 \`?" \ "- [x] Have you tested your manifest locally with \`winget install --manifest \`?" \ - "- [x] Does your manifest conform to the [1.6 schema](https://github.com/microsoft/winget-pkgs/tree/master/doc/manifest/schema/1.6.0)?" \ + "- [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" \ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2cb0070..3a9b8ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/packaging/windows/check-static-crt.ps1 b/packaging/windows/check-static-crt.ps1 new file mode 100644 index 0000000..3c85988 --- /dev/null +++ b/packaging/windows/check-static-crt.ps1 @@ -0,0 +1,35 @@ +# Fails if a Windows binary depends on the Visual C++ runtime DLLs. +# +# splitfire-agent is linked with +crt-static (see .cargo/config.toml) so it runs on a +# clean Windows install. If RUSTFLAGS overrides that, or a native dependency +# drags in the DLL runtime, the exe still builds and passes tests on a runner +# that has the redistributable installed, then fails to load on users' +# machines. This check catches it before release. +param( + [Parameter(Mandatory = $true)] + [string]$Path +) + +$ErrorActionPreference = 'Stop' + +$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' +$dumpbin = & $vswhere -latest -products * -find 'VC\Tools\MSVC\*\bin\Hostx64\x64\dumpbin.exe' | + Select-Object -First 1 +if (-not $dumpbin) { + throw 'dumpbin.exe not found; is the MSVC toolset installed?' +} + +$output = & $dumpbin /nologo /dependents $Path +if ($LASTEXITCODE -ne 0) { + throw "dumpbin failed on $Path" +} +$output | Write-Host + +$runtime = $output | + Where-Object { $_ -match '^\s+(vcruntime|msvcp|concrt|ucrtbase|api-ms-win-crt-)\S*\.dll\s*$' } | + ForEach-Object { $_.Trim() } +if ($runtime) { + Write-Host "::error::$Path depends on the Visual C++ runtime: $($runtime -join ', ')" + exit 1 +} +Write-Host "$Path does not depend on the Visual C++ runtime." diff --git a/packaging/winget/SplitFireAI.splitfire-agent.installer.yaml.in b/packaging/winget/SplitFireAI.splitfire-agent.installer.yaml.in index 1134bce..8d728c5 100644 --- a/packaging/winget/SplitFireAI.splitfire-agent.installer.yaml.in +++ b/packaging/winget/SplitFireAI.splitfire-agent.installer.yaml.in @@ -1,4 +1,4 @@ -# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.6.0.schema.json +# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.12.0.schema.json PackageIdentifier: SplitFireAI.splitfire-agent PackageVersion: @VERSION@ InstallerType: zip @@ -14,4 +14,4 @@ Installers: InstallerUrl: https://github.com/@REPO@/releases/download/@TAG@/splitfire-agent-windows-x64.zip InstallerSha256: @SHA_X64@ ManifestType: installer -ManifestVersion: 1.6.0 +ManifestVersion: 1.12.0 diff --git a/packaging/winget/SplitFireAI.splitfire-agent.locale.en-US.yaml.in b/packaging/winget/SplitFireAI.splitfire-agent.locale.en-US.yaml.in index 5d4197a..46de6e0 100644 --- a/packaging/winget/SplitFireAI.splitfire-agent.locale.en-US.yaml.in +++ b/packaging/winget/SplitFireAI.splitfire-agent.locale.en-US.yaml.in @@ -1,4 +1,4 @@ -# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.6.0.schema.json +# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.12.0.schema.json PackageIdentifier: SplitFireAI.splitfire-agent PackageVersion: @VERSION@ PackageLocale: en-US @@ -7,7 +7,7 @@ PublisherUrl: https://github.com/SplitFireAI PublisherSupportUrl: https://github.com/@REPO@/issues PackageName: SplitFire Agent PackageUrl: https://github.com/@REPO@ -License: MIT OR Apache-2.0 +License: Apache-2.0 LicenseUrl: https://github.com/@REPO@/blob/main/LICENSE ShortDescription: Music-domain ACP agent for theory, production, industry, culture and live shows. Description: |- @@ -29,4 +29,4 @@ Documentations: - DocumentLabel: Readme DocumentUrl: https://github.com/@REPO@/blob/main/README.md ManifestType: defaultLocale -ManifestVersion: 1.6.0 +ManifestVersion: 1.12.0 diff --git a/packaging/winget/SplitFireAI.splitfire-agent.yaml.in b/packaging/winget/SplitFireAI.splitfire-agent.yaml.in index c28807a..0d8db63 100644 --- a/packaging/winget/SplitFireAI.splitfire-agent.yaml.in +++ b/packaging/winget/SplitFireAI.splitfire-agent.yaml.in @@ -1,6 +1,6 @@ -# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.6.0.schema.json +# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.12.0.schema.json PackageIdentifier: SplitFireAI.splitfire-agent PackageVersion: @VERSION@ DefaultLocale: en-US ManifestType: version -ManifestVersion: 1.6.0 +ManifestVersion: 1.12.0 diff --git a/src/main.rs b/src/main.rs index f85baa6..dc34b26 100644 --- a/src/main.rs +++ b/src/main.rs @@ -85,10 +85,12 @@ async fn main() -> anyhow::Result<()> { } else { let llm = LlmConfig::from_env(&INFO.env); if llm.api_key.is_none() { - anyhow::bail!( + // Exit 0: winget's validator launches the bare exe on a console and expects success. + eprintln!( "No Onde API key configured. Set ONDE_API_KEY or run `{} --setup`.", INFO.name ); + return Ok(()); } let mut tui = TuiConfig::current_exe(INFO.name, opts.yolo)?; tui.env.push((INFO.env.var("SURFACE"), "tui".to_string())); From 8f530e2c9643e3600739c530d19bebfefbb1207b Mon Sep 17 00:00:00 2001 From: Seto Date: Tue, 13 Oct 2026 14:05:06 +0200 Subject: [PATCH 5/5] ci: name the winget wait step "Sebat dulu ..." to match sigit Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/release-winget.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-winget.yml b/.github/workflows/release-winget.yml index e741b2b..71fefef 100644 --- a/.github/workflows/release-winget.yml +++ b/.github/workflows/release-winget.yml @@ -190,7 +190,7 @@ jobs: echo "issue_number=${ISSUE_URL##*/}" >> "$GITHUB_OUTPUT" # Leave a gap so the issue and the PR aren't posted seconds apart. - - name: Wait before opening the PR + - name: Sebat dulu ... if: steps.issue.outputs.issue_number != '' shell: bash run: sleep $(( RANDOM % 61 + 150 ))