Agent skill

Build Afm Nightly Publish

by scouzi1966 in scouzi1966/maclocal-api

Build, test, and publish an afm-next nightly release — full from-scratch build, user testing pause, GitHub release, and Homebrew tap update.

MITAuto-check passedMobile

Install Build Afm Nightly Publish

skills CLI
$ npx skills add scouzi1966/maclocal-api --skill build-afm-nightly-publish -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install scouzi1966/maclocal-api build-afm-nightly-publish --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/scouzi1966/maclocal-api.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/build-afm-nightly-publish .claude/skills/build-afm-nightly-publish && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
build-afm-nightly-publish
GitHub stars
346
Token cost
~8.8k tokens
SKILL.md length
2,967 words
Files
1
Skills in repo
12
Repo updated
First seen
Licence
MIT

At a glance

Build, test, and publish an afm-next nightly release — full from-scratch build, user testing pause, GitHub release, and Homebrew tap update.

  • Works in 5 steps: Validate Environment → Build from Scratch (True Clean Build) → Present Binary and Enter… → …
  • User types /build-afm-nightly-publish
  • SKILL.md covers Usage, Prerequisites and Instructions
  • Calls git, gh and swift; reaches kruks.ai

What it does

Build Afm Nightly Publish is an agent skill from scouzi1966/maclocal-api. Build, test, and publish an afm-next nightly release — full from-scratch build, user testing pause, GitHub release, and Homebrew tap update. Use when user types /build-afm-nightly-publish or asks to publish a nightly build.

Its SKILL.md is about 8.8k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.

It sits in Mobile, covering iOS development and User research. It works with GitHub, Homebrew and macOS. The repository describes itself as: 'afm' command cli: macOS server and single prompt mode that exposes Apple's Foundation and MLX Models and other APIs running on your Mac through a single aggregated…. The licence is MIT.

When your agent uses it

  • User types /build-afm-nightly-publish
  • Asks to publish a nightly build

Example prompts

  • “/build-afm-nightly-publish”

Requirements

  • Python 3
  • Node.js

Workflow steps

5 steps, taken from the step headings in SKILL.md.

  1. Validate Environment
  2. Build from Scratch (True Clean Build)
  3. Present Binary and Enter Test/Fix/Rebuild Loop
  4. Publish Release
  5. Verify & Report

What it can do on your machine

Read from SKILL.md and the folder at commit 138ca5d. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • git
    • gh
    • swift
    • npm
    • python3
    • brew
    • pip
    • node
    • npx

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    Hosts in commands or code, which the agent is likely to contact:

    • kruks.ai

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Build Afm Nightly Publish loads about 8.8k tokens when it runs. Until then it costs about 62 tokens; SKILL.md has 2,967 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~62
When it runs · the whole SKILL.md, loaded when a task matches
~8.8k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from scouzi1966/maclocal-api at commit 138ca5d, republished under its MIT licence (© scouzi1966). 2,967 words, ~8,824 tokens.

Download SKILL.mdSave it as .claude/skills/build-afm-nightly-publish/SKILL.md (or your agent's skills folder).
name
build-afm-nightly-publish
description
Build, test, and publish an afm-next nightly release — full from-scratch build, user testing pause, GitHub release, and Homebrew tap update. Use when user types /build-afm-nightly-publish or asks to publish a nightly build.
user_invocable
true

Build & Publish AFM Nightly

Build afm from scratch (works from a fresh clone), let the user test it, then publish a GitHub pre-release and update the Homebrew tap.

Usage

  • /build-afm-nightly-publish — full pipeline: build + test + publish
  • /build-afm-nightly-publish --skip-build — skip build, use existing release binary

Prerequisites

The publish script (Scripts/publish-next.sh) requires:

  • gh CLI authenticated with push access to scouzi1966/maclocal-api
  • homebrew-afm repo at ../homebrew-afm (relative to repo root) or TAP_DIR env var — auto-cloned if missing
  • vesta-mac repo at ../vesta-mac (relative to repo root) or VESTA_DIR env var — required for PEP 503 wheel index update. Auto-cloned if missing.
  • All build prerequisites from /build-afm (Xcode, Swift, Node.js, etc.)
  • promptfoo CLI (npm install -g promptfoo or npx promptfoo) — required for the promptfoo agentic eval suite
  • wrangler CLI (npm install -g wrangler) — required for Cloudflare Pages deploy of wheel index

Instructions

Step 1: Validate Environment

Run these checks and present results to the user:

bash
# Build prerequisites
uname -m                    # must be arm64
sw_vers -productVersion     # must be 26.0+
xcode-select -p             # must point to Xcode.app
swift --version             # Swift 5.9+
git --version
node --version              # Node 18+
npm --version

# Promptfoo (required for agentic eval suite)
command -v promptfoo && promptfoo --version 2>/dev/null | head -1
# Must be installed (npm install -g promptfoo)

# Publish prerequisites
gh auth status              # must be authenticated

# CRITICAL: Verify the user is the repo owner (scouzi1966)
# This prevents non-owners from accidentally overwriting releases or the brew tap.
GH_USER=$(gh api user -q .login)
echo "GitHub user: $GH_USER"
# Must be "scouzi1966"

# Verify push (write) access to both repos
gh api repos/scouzi1966/maclocal-api -q '.permissions.push'    # must be true
gh api repos/scouzi1966/homebrew-afm -q '.permissions.push'    # must be true

# Tap repo — auto-clone if missing
TAP_DIR="${TAP_DIR:-$(cd "$(git rev-parse --show-toplevel)/.." && pwd)/homebrew-afm}"
if [ ! -f "$TAP_DIR/afm-next.rb" ]; then
  echo "Tap repo missing at $TAP_DIR — cloning..."
  gh repo clone scouzi1966/homebrew-afm "$TAP_DIR"
fi
test -f "$TAP_DIR/afm-next.rb" && echo "Tap OK: $TAP_DIR" || echo "FAILED to clone tap repo"

# vesta-mac repo — required for wheel index update. Auto-clone if missing.
VESTA_DIR="${VESTA_DIR:-$(cd "$(git rev-parse --show-toplevel)/.." && pwd)/vesta-mac}"
if [ ! -d "$VESTA_DIR/.git" ]; then
  echo "vesta-mac repo missing at $VESTA_DIR — cloning..."
  gh repo clone scouzi1966/vesta-mac "$VESTA_DIR"
fi
test -d "$VESTA_DIR/.git" && echo "vesta-mac OK: $VESTA_DIR" || echo "FAILED to clone vesta-mac"

Present as a checklist. If the GitHub user is not scouzi1966 or push access is false for either repo, STOP immediately and tell the user:

This skill publishes releases and updates the Homebrew tap for scouzi1966/maclocal-api.
Only the repository owner (scouzi1966) can run it. You are authenticated as: <username>

Do NOT proceed unless: (1) all build checks pass, (2) GitHub user has push access to BOTH repos, (3) tap repo is available.

Step 2: Build from Scratch (True Clean Build)

A nightly release must be built from a completely clean state. swift package clean is NOT sufficient — it leaves behind cached modules, package resolution state, and precompiled headers that can mask stale code:

Cached artifactLocationWhat swift package clean does
Compiled .o/.swiftmodule.build/arm64-apple-macosx/release/Removes
Module cache (PCM/PCH).build/arm64-apple-macosx/release/ModuleCache/Keeps (~400MB)
Cloned SPM dependencies.build/repositories/Keeps (~300MB)
Package resolution lock.build/workspace-state.jsonKeeps
Xcode DerivedData~/Library/Developer/Xcode/DerivedData/*maclocal*Keeps (if exists)

Before running the build script, nuke all cached state:

bash
# 1. Remove entire SPM build directory (modules, cache, resolution state — everything)
rm -rf .build

# 2. Remove Xcode DerivedData for this project (if anyone opened it in Xcode)
rm -rf ~/Library/Developer/Xcode/DerivedData/*maclocal* \
       ~/Library/Developer/Xcode/DerivedData/*MacLocal* \
       ~/Library/Developer/Xcode/DerivedData/*afm* 2>/dev/null || true

# 3. Verify clean state
test -d .build && echo "FAIL: .build still exists" || echo "OK: .build removed"

Then run the full build:

bash
./Scripts/build-from-scratch.sh

IMPORTANT: Never add --skip-submodules, --skip-patches, or --skip-webui. This is a release build — everything must be from scratch.

Why this matters: Stale ModuleCache can cause the compiler to use old .swiftmodule files from a previous build, meaning your patches compile but the binary links against the cached (unpatched) version. Stale workspace-state.json can resolve a different version of MLX Swift than what the pin specifies. Both failures are silent — the build succeeds, the binary runs, but behavior is wrong.

If the user passed --skip-build, skip the clean and build steps, but still run all Step 2b verification checks against the existing binary:

bash
test -x .build/arm64-apple-macosx/release/afm || test -x .build/release/afm
Step 2b: Post-Build Verification ("What Could Go Wrong")

The build script reports success, but do not trust its output alone. Independently verify every critical artifact. The build script could succeed (exit 0) while:

  • Patches silently failed to apply (vendor reverted by git submodule update)
  • xgrammar compiled but wasn't linked (missing from Package.swift targets)
  • MLX Swift resolved a wrong version (pin not applied to Package.swift)
  • Metallib bundle missing (Metal shaders won't load at runtime → crash)
  • WebUI assets missing (llama.cpp web interface won't serve)
  • BuildInfo.swift not restored (leaves dirty working tree)

Run all of these checks. Present results as a table. If ANY check fails, STOP and investigate before proceeding.

Check 1: Patches byte-identical to vendor targets

The patch script says "Applied" but git submodule update can silently revert files. Verify every patch file is byte-for-byte identical to its vendor target using the actual arrays from Scripts/apply-mlx-patches.sh:

python
python3 -c "
import os
# These arrays MUST match Scripts/apply-mlx-patches.sh — if they drift, the check is wrong.
# Read them from the script itself to stay in sync.
patches = [
  ('Qwen3VL.swift','Libraries/MLXVLM/Models/Qwen3VL.swift'),
  ('Qwen3Next.swift','Libraries/MLXLLM/Models/Qwen3Next.swift'),
  # ... all 20 entries from PATCH_FILES/TARGET_PATHS arrays ...
]
ok = fail = 0
for pf, tp in patches:
    src, tgt = f'Scripts/patches/{pf}', f'vendor/mlx-swift-lm/{tp}'
    if not os.path.exists(tgt):
        print(f'MISSING:   {pf} -> {tp}'); fail += 1
    else:
        with open(src,'rb') as a, open(tgt,'rb') as b:
            if a.read() == b.read():
                print(f'MATCH:     {pf}'); ok += 1
            else:
                print(f'MISMATCH:  {pf}'); fail += 1
print(f'\n{ok}/{ok+fail} patches verified')
"

Why this matters: If even one patch is stale, the compiled binary has upstream code instead of our optimized/fixed version. This has happened when git submodule update --init --recursive runs AFTER apply-mlx-patches.sh — it silently reverts patches.

Check 2: MLX Swift pinned AND resolved to exact version
bash
# Check the pin in source
grep 'mlx-swift.*exact' vendor/mlx-swift-lm/Package.swift
# Must show: exact: "0.30.3"
# 0.30.4+ has SDPA NaN regression — if this shows any other version, STOP.

# Check what SPM actually resolved (the pin could say 0.30.3 but resolution used a cached different version)
python3 -c "
import json
d = json.load(open('Package.resolved'))
for p in d.get('pins', []):
    if 'mlx' in p.get('identity','').lower():
        print(f'{p[\"identity\"]}: {p[\"state\"].get(\"version\",\"?\")}')
"
# Must show: mlx-swift: 0.30.3
# If version differs from pin, the resolution is stale — this is exactly what rm -rf .build prevents.

Why this matters: The pin in Package.swift is a request, but Package.resolved is what was actually fetched and compiled against. A stale workspace-state.json or Package.resolved from a previous build can cause SPM to use a cached resolution even after the pin changes. Nuking .build/ in Step 2 prevents this, but verify anyway.

Check 3: xgrammar submodule present and at expected version
bash
git submodule status vendor/xgrammar
# Must show a commit hash, NOT a '-' prefix (which means uninitialized)
cd vendor/xgrammar && git describe --tags --always && cd -
# Must show v0.1.32 or the expected pinned tag

Why this matters: xgrammar is a C++ library compiled from source. If the submodule is missing or at the wrong version, the EBNF grammar constraint feature either doesn't exist or has different behavior.

Check 4: xgrammar symbols linked into the binary
bash
# Verify xgrammar C++ was compiled and linked (not just present as source)
strings .build/arm64-apple-macosx/release/afm | grep -c 'xgrammar/cpp/'
# Must be > 0 (typically 10+)

# Verify our Swift XGrammarService wrapper is in the binary
strings .build/arm64-apple-macosx/release/afm | grep 'XGrammarService'
# Must show: XGrammarService, _TtC11MacLocalAPI15XGrammarService, etc.

# Verify xgrammar C++ symbols are actually linked
nm -a .build/arm64-apple-macosx/release/afm 2>/dev/null | grep -c 'xgrammar'
# Must be > 0 (typically 30+)

Why this matters: xgrammar could be in the source tree but excluded from the Swift Package Manager target graph. The binary would build fine but grammar-constrained decoding would silently fail at runtime.

Check 5: Metallib bundle present
bash
METALLIB=".build/arm64-apple-macosx/release/AFMKit_AFMKitMLX.bundle/default.metallib"
test -f "$METALLIB" && echo "OK: metallib $(du -h "$METALLIB" | cut -f1)" || echo "FAIL: metallib missing"
# Must exist and be > 1MB (typically ~3.7MB)

Why this matters: Without the metallib, MLX GPU kernels can't load. The server starts but crashes on first inference. The build script checks this, but verify independently.

Check 6: WebUI assets present
bash
test -f "Resources/webui/index.html.gz" && echo "OK: webui assets" || echo "FAIL: webui missing"

Why this matters: The llama.cpp web UI is served at / — without it, browser access shows nothing.

Check 7: BuildInfo.swift is clean (not left with injected SHA)
bash
grep 'static let version' Sources/AFMKit/BuildInfo.swift
# Must show the base version like: static let version: String? = "v0.9.7"
# Must NOT show a commit SHA like: static let version: String? = "v0.9.7-3d71b40"
git diff Sources/AFMKit/BuildInfo.swift
# Must show no diff (file restored to committed state)

Why this matters: The build script injects the git SHA into BuildInfo.swift during compilation then restores it. If restore fails, the working tree is dirty and the next git commit could accidentally commit the injected version.

Check 8: Binary is stripped and reasonable size
bash
ls -lh .build/arm64-apple-macosx/release/afm
# Size should be 30-50MB for a stripped release binary
# If > 100MB, it's likely unstripped (debug symbols included)
nm -gU .build/arm64-apple-macosx/release/afm 2>/dev/null | wc -l
# Stripped binary has minimal external symbols (< 500 typically)
# Unstripped has thousands
Check 9: Relocated binary does NOT crash (pip install simulation)

This is the most critical distribution check. SPM auto-generates resource_bundle_accessor.swift with a hardcoded absolute build path. If any code path calls Bundle.module, the binary will fatalError when installed via pip or Homebrew (because the build path no longer exists). This has shipped broken nightlies before.

bash
# Simulate pip install: copy binary + loose metallib to a temp dir (NO SPM bundle directory)
TMPDIR=$(mktemp -d)
cp .build/arm64-apple-macosx/release/afm "$TMPDIR/"
cp .build/arm64-apple-macosx/release/AFMKit_AFMKitMLX.bundle/default.metallib "$TMPDIR/"

# Must NOT crash with "could not load resource bundle" fatalError
MACAFM_MLX_MODEL_CACHE=/Volumes/edata/models/vesta-test-cache \
  "$TMPDIR/afm" mlx -m mlx-community/Qwen3.5-35B-A3B-4bit -s "hello" --max-tokens 5 2>&1 | head -3
EXIT_CODE=${PIPESTATUS[0]}
rm -rf "$TMPDIR"

if [ "$EXIT_CODE" -ne 0 ]; then
  echo "FAIL: Relocated binary crashed (exit $EXIT_CODE)"
  echo "This means Bundle.module fatalError is still reachable."
  echo "Check MLXMetalLibrary.swift — it must NOT call Bundle.module."
else
  echo "PASS: Relocated binary works"
fi

Why this matters: This is the exact layout pip creates: macafm_next/bin/afm + macafm_next/bin/default.metallib. If this test fails, every pip user gets a crash on first run. This check is non-negotiable — if it fails, do NOT publish.

Root cause if it fails: Someone added a Bundle.module call somewhere in the codebase. Search for it:

bash
grep -r 'Bundle\.module' Sources/ --include='*.swift'
# Must return ZERO results (only comments allowed)
Check 10: No Bundle.module calls in source code
bash
# Bundle.module uses SPM's auto-generated accessor which fatalError's on relocated binaries.
# It must NEVER be called from our code. Comments referencing it are OK.
HITS=$(grep -r 'Bundle\.module' Sources/ --include='*.swift' | grep -v '//' | grep -v '^\s*//' | wc -l)
if [ "$HITS" -gt 0 ]; then
  echo "FAIL: Found $HITS Bundle.module call(s) in source code"
  grep -rn 'Bundle\.module' Sources/ --include='*.swift' | grep -v '//'
  echo "This WILL crash when installed via pip or Homebrew."
else
  echo "PASS: No Bundle.module calls"
fi

Why this matters: Even a single Bundle.module call anywhere in the code path triggers the auto-generated fatalError. This is a regression guard — any future code that adds Bundle.module will be caught here before it ships.

Check 11: Info.plist embedded with privacy usage descriptions

macOS 26 SIGABRTs any process that requests privacy-sensitive APIs (Speech Recognition, microphone, camera, contacts, etc.) without a matching *UsageDescription key in its Info.plist. Currently required for PR #107's Apple Speech feature (afm speech, POST /v1/audio/transcriptions, chat input_audio content parts). Any future privacy-API integration needs its key added here too.

bash
BIN=.build/arm64-apple-macosx/release/afm

# Verify __TEXT,__info_plist section exists (embedded via Package.swift linker flags)
if otool -l "$BIN" | grep -q '__info_plist'; then
  PLIST_SIZE=$(otool -l "$BIN" | grep -A4 __info_plist | grep 'size' | awk '{print $2}')
  echo "PASS: __info_plist section present ($PLIST_SIZE bytes)"
else
  echo "FAIL: Missing __TEXT,__info_plist section"
  echo "Check Package.swift linker flags (-Xlinker -sectcreate ...) and Sources/AFMCLI/Info.plist"
fi

# Verify NSSpeechRecognitionUsageDescription key is present
if strings "$BIN" | grep -q 'NSSpeechRecognitionUsageDescription'; then
  echo "PASS: NSSpeechRecognitionUsageDescription key embedded"
else
  echo "FAIL: NSSpeechRecognitionUsageDescription missing from embedded plist"
  echo "afm speech / /v1/audio/transcriptions will SIGABRT on macOS 26"
fi

# Verify plist structure is parseable (not corrupted during build)
plutil -lint Sources/AFMCLI/Info.plist

Why this matters: Without the embedded plist, running afm speech -f foo.wav (or any endpoint that calls SFSpeechRecognizer) crashes before returning any output. The build script Scripts/build-from-scratch.sh already enforces this — but also check here so the publish flow fails loudly if someone bypasses the build script or if Package.swift's linker flags get reverted in a merge.

Root cause if it fails:

  1. Sources/AFMCLI/Info.plist was deleted or renamed
  2. Package.swift's linkerSettings lost the -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist -Xlinker … flags
  3. Someone added a new privacy-API usage (microphone, camera) without adding the corresponding *UsageDescription key to Info.plist

Full plist must also include CFBundleIdentifier, CFBundleName, CFBundleExecutable — these establish TCC identity. Changing CFBundleIdentifier later would force existing users to re-grant Speech Recognition permission.

Check 12: Report all vendor/submodule pin levels

Present the exact version of every submodule and SPM dependency so the user can verify the build reproduces the expected dependency tree. Also fetch the latest release tag from each upstream repo to show if we're behind.

bash
echo "=== Git Submodules ==="
git submodule status

echo "=== SPM Resolved Versions ==="
python3 -c "
import json
d = json.load(open('Package.resolved'))
for p in sorted(d.get('pins', []), key=lambda x: x.get('identity','')):
    v = p['state'].get('version') or p['state'].get('revision','?')[:12]
    print(f'  {p[\"identity\"]}: {v}')
"

echo "=== Upstream Latest Releases ==="
for repo in ml-explore/mlx-swift ml-explore/mlx-swift-lm mlc-ai/xgrammar ggml-org/llama.cpp huggingface/swift-transformers huggingface/swift-huggingface; do
  tag=$(gh api "repos/$repo/releases/latest" -q '.tag_name' 2>/dev/null || echo "?")
  echo "  $repo: $tag"
done

This is informational — no pass/fail. But if a resolved version is unexpected (e.g., mlx-swift != 0.30.3), STOP.

Present verification results
#CheckWhat could go wrongResult
1Patches byte-identical (N/N)submodule update reverted patchesPASS/FAIL
2MLX Swift pin + resolved 0.30.3stale resolution → SDPA NaN crashesPASS/FAIL
3xgrammar at expected tagmissing submodule → no grammar constraintsPASS/FAIL
4xgrammar linked in binarycompiled but not linked → silent runtime failurePASS/FAIL
5Metallib bundle presentmissing → crash on first inferencePASS/FAIL
6WebUI assets presentmissing → no browser UIPASS/FAIL
7BuildInfo.swift cleandirty working tree → accidental commitPASS/FAIL
8Binary stripped, reasonable sizeunstripped → bloated downloadPASS/FAIL
9Relocated binary works (pip sim)Bundle.module fatalError → crash on pip installPASS/FAIL
10No Bundle.module in sourceregression guard → future crash on relocated binaryPASS/FAIL
11Info.plist embedded + NSSpeechRecognitionUsageDescriptionmacOS 26 SIGABRTs Speech Recognition without UsageDescription keyPASS/FAIL

Then present two separate tables for vendor pins:

Git Submodules:

SubmoduleSourcePinned CommitUpstream LatestNotes
vendor/mlx-swift-lmSubmodulegit submodule status hash + taggh api repos/.../releases/latestOur patched fork
vendor/xgrammarSubmodulegit submodule status hash + taggh api repos/.../releases/latestC++ grammar engine
vendor/llama.cppSubmodulegit submodule status hash + taggh api repos/.../releases/latestWebUI only

SPM Dependencies (from Package.resolved):

PackageSourceResolved VersionUpstream LatestNotes
mlx-swiftSPM (exact pin)Package.resolved versiongh api repos/.../releases/latest0.30.4+ has SDPA NaN — pinned to exact 0.30.3
swift-transformersSPM (from)Package.resolved versiongh api repos/.../releases/latestTokenizer/chat templates
swift-huggingfaceSPM (from)Package.resolved versiongh api repos/.../releases/latestHF hub downloads
swift-jinjaSPM (transitive)Package.resolved version—Jinja2 template engine
vaporSPM (from)Package.resolved version—HTTP framework

Populate the "Upstream Latest" column by querying gh api repos/OWNER/REPO/releases/latest -q '.tag_name'. This lets the user see at a glance if we're behind upstream on any dependency.

If ANY check fails, STOP. Do not proceed to user testing or publishing.

Step 3: Present Binary and Enter Test/Fix/Rebuild Loop

After all verification checks pass, get the binary path and version:

bash
BIN=".build/arm64-apple-macosx/release/afm"
[ -x "$BIN" ] || BIN=".build/release/afm"
echo "Binary: $(cd "$(dirname "$BIN")" && pwd)/$(basename "$BIN")"
$BIN --version

Report to the user:

  • Binary path (absolute)
  • Version string
  • Verification results table (from Step 2b)

Then use AskUserQuestion to pause and let the user decide what to do next:

Question: "The build is verified. What would you like to do?"

Options:

  1. "Publish as-is" — Skip testing, go straight to GitHub release and tap update
  2. "Run tests" — Run automated tests, then decide (see test scope question below)
  3. "I'll test manually" — Pause here while the user tests the binary themselves
  4. "Cancel" — Abort without publishing
If user selects "Run tests"

First, list available models in the cache and let the user pick:

bash
MACAFM_MLX_MODEL_CACHE=/Volumes/edata/models/vesta-test-cache ./Scripts/list-models.sh

Question: "Which model to test with?"

Present the available models as options (show model name and size). The user picks one.

Then ask the test scope:

Question: "Which tests to run?"

Options:

  1. "Assertions only (all tiers including unit)" — Run /test-afm-assertions with full tier (deterministic pass/fail tests, ~15 min/model)
  2. "Comprehensive only" — Run /test-macafm smart analysis (AI-scored quality evaluation)
  3. "Both" — Run assertions first, then comprehensive (most thorough, ~30 min/model)
  4. "Full nightly suite" — Run assertions + comprehensive + promptfoo agentic evals (most complete, ~60 min)

Invoke the appropriate skill(s) with the selected model. Do NOT re-ask the model question — pass it through to the test skill(s).

Show full SKILL.md (1,266 more words)Show less
If test scope includes promptfoo

Run the full promptfoo agentic eval suite after assertions/comprehensive complete:

bash
AFM_MODEL=MODEL \
AFM_BINARY=.build/arm64-apple-macosx/release/afm \
MACAFM_MLX_MODEL_CACHE=/Volumes/edata/models/vesta-test-cache \
./Scripts/feature-promptfoo-agentic/run-promptfoo-agentic.sh all

This manages its own server lifecycle (starts/stops across 8 server profiles) and runs ~137 test cases across 16 configs:

  • Structured (6+4 tests): json_schema response format and stress tests
  • Tool calling (7 tests × 3 profiles): default, adaptive-xml, adaptive-xml-grammar
  • Tool call quality (6 tests × 3 profiles): BFCL-inspired when-to-call decisions
  • Grammar constraints (17 tests × 8 server phases): schema/tools enforcement, concurrent, prefix-cache, mixed-strict, header assertions
  • Agentic (4 tests × 3 profiles): Multi-turn coding workflows
  • Frameworks (8 tests × 3 profiles): Agent framework tool schemas (OpenCode, Pi, OpenClaw, Hermes shapes)
  • OpenCode (37 tests × 3 profiles): Primary-source OpenCode built-in tools
  • PI (20 tests × 3 profiles): Pi coding-agent tools
  • OpenClaw (12 tests × 3 profiles): OpenClaw tool coverage
  • Hermes (12 tests × 3 profiles): Hermes agentic framework tools

Output: JSON reports in $AFM_PROMPTFOO_OUT_DIR (default: /Volumes/edata/promptfoo/data/maclocal-api/current/).

Interpreting promptfoo results:

  • Server-side features (structured, toolcall, grammar schema/tools/headers): Should be 100% pass. Failures here indicate server bugs.
  • Concurrent grammar failures: Known race condition in --concurrent 2 grammar path — not a release blocker.
  • OpenCode/PI/agentic failures: Model quality at task complexity — the model can't always pick the right tool or produce correct arguments for complex multi-tool scenarios. Not server bugs.
  • adaptive-xml profile failures scoring lower than default: The adaptive-xml parser produces slightly different formatting that quality judges may score lower. Compare with default profile to confirm it's not a regression.

To extract pass/fail summary from all result files:

bash
python3 -c "
import json, os, glob
files = sorted(glob.glob('$AFM_PROMPTFOO_OUT_DIR/*MODEL_SLUG*.json'))
total_pass = total_fail = 0
for f in files:
    name = os.path.basename(f).replace('-MODEL_SLUG.json','')
    d = json.load(open(f))
    stats = d.get('results', d).get('stats', {})
    p, fa = stats.get('successes', 0), stats.get('failures', 0)
    total_pass += p; total_fail += fa
    status = 'PASS' if fa == 0 else f'FAIL ({fa})'
    print(f'{name}: {p}/{p+fa} — {status}')
print(f'\nTOTAL: {total_pass}/{total_pass+total_fail}')
"

After tests complete, present results and ask:

Question: "Tests complete. What next?"

Options:

  1. "Publish" — Results are acceptable, proceed to release
  2. "Fix and rebuild" — There are issues to fix before releasing
  3. "Cancel" — Abort
If user selects "I'll test manually"

Wait for the user to come back. When they do, ask:

Question: "Ready to proceed?"

Options:

  1. "Publish" — Testing passed, proceed to release
  2. "Fix and rebuild" — There are issues to fix before releasing
  3. "Cancel" — Abort
If user selects "Fix and rebuild" (from any path above)

The user will make code changes (or ask you to). After changes are made:

  1. Re-run Step 2 (full clean build: rm -rf .build + ./Scripts/build-from-scratch.sh)
  2. Re-run Step 2b (all 8 verification checks)
  3. Return to Step 3 (present binary and ask again)

This loop repeats until the user selects "Publish" or "Cancel". Each iteration is a full clean rebuild — never do an incremental build for a release.

Version and changelog selection

Before publishing, ask the user two questions via AskUserQuestion:

Question 1 — Version: Determine the suggested version by reading Sources/AFMKit/BuildInfo.swift and extracting the version (strip leading v). Present it to the user:

"Release version? The base version from BuildInfo.swift is X.Y.Z. The full nightly version will be X.Y.Z-next.<sha>.<date>."

Options:

  1. "X.Y.Z (from BuildInfo.swift)" — Use the version from BuildInfo.swift (recommended)
  2. "Custom version" — Enter a different base version

Question 2 — Changelog since: Show both the last nightly tag AND the last stable release tag so the user can choose the right baseline:

bash
# Find the last nightly tag
LAST_NIGHTLY=$(git tag -l 'nightly-*' --sort=-creatordate | head -1)
if [ -n "$LAST_NIGHTLY" ]; then
  NIGHTLY_DATE=$(git log -1 --format='%ci' "$LAST_NIGHTLY" 2>/dev/null | cut -d' ' -f1)
  NIGHTLY_COUNT=$(git rev-list "${LAST_NIGHTLY}..HEAD" --count 2>/dev/null)
  echo "Last nightly: $LAST_NIGHTLY ($NIGHTLY_DATE) — $NIGHTLY_COUNT commits since"
fi

# Find the last stable release tag (v*.*.* without -next or nightly)
LAST_STABLE=$(git tag -l 'v*' --sort=-version:refname | grep -v 'nightly\|next' | head -1)
if [ -n "$LAST_STABLE" ]; then
  STABLE_DATE=$(git log -1 --format='%ci' "$LAST_STABLE" 2>/dev/null | cut -d' ' -f1)
  STABLE_COUNT=$(git rev-list "${LAST_STABLE}..HEAD" --count 2>/dev/null)
  echo "Last stable:  $LAST_STABLE ($STABLE_DATE) — $STABLE_COUNT commits since"
fi

# Show commit log from the more recent of the two
echo "--- Commits since last nightly ---"
git log --oneline "${LAST_NIGHTLY}..HEAD" 2>/dev/null

Present both reference points and ask:

"Generate changelog from which point?"

Options:

  1. "Since last nightly <tag> (N commits)" — Default for routine nightlies (incremental changelog)
  2. "Since last stable release <tag> (N commits)" — Use for the first nightly after a stable release, or when you want the full delta since the last official version
  3. "Custom commit SHA" — Enter a specific commit SHA

Guidance for which to pick:

  • Routine nightly (there have been nightlies since the last stable release): use "since last nightly" — the changelog shows only what's new since the previous nightly
  • First nightly after a stable release (no nightlies since last v* tag): use "since last stable release" — the changelog shows everything new in this development cycle
  • No previous tags at all: use "Custom commit SHA" or omit --since entirely (the script will include all commits)

Changelog filtering — exclude reverted/superseded commits: When reviewing the commit list for the changelog, omit commits whose work was later removed or fully replaced. For example, if a commit adds a Python bridge and a later commit removes it, neither should appear in the release notes — the net effect is zero. Only include commits that contribute to the final state of the codebase at HEAD. The publish-next.sh script includes all commits mechanically; you should review and curate the changelog before presenting it to the user.

If the user selects "since last stable release", pass --since <stable-tag-sha> to the publish script. If the user provides a custom SHA, pass --since <sha>. If the user selects "since last nightly", no --since flag is needed (the script defaults to this).

Do NOT proceed to Step 4 unless the user selects "Publish".

Step 4: Publish Release

Run the publish script with --skip-build (already built in Step 2), the confirmed version, and optional --since:

bash
# Without custom since (uses last nightly tag):
./Scripts/publish-next.sh --skip-build --version <confirmed-version>

# With custom since:
./Scripts/publish-next.sh --skip-build --version <confirmed-version> --since <commit-sha>

This script handles everything:

  1. Packages the binary + metallib bundle + webui into afm-next-arm64.tar.gz
  2. Generates changelog from commits since the last nightly-* release (with both Homebrew and pip install instructions in the release notes)
  3. Creates a GitHub pre-release tagged nightly-YYYYMMDD-SHORTSHA
  4. Updates the nightly tag to point to HEAD
  5. Updates afm-next.rb in the homebrew-afm tap (url, version, sha256)
  6. Commits and pushes the tap update
  7. Builds a nightly wheel (macafm-next) via Scripts/build-nightly-wheel.sh
  8. Uploads the wheel to the GitHub release and updates the PEP 503 index on kruks.ai via Scripts/update-wheel-index.sh (requires vesta-mac repo at ../vesta-mac and wrangler for Cloudflare Pages deploy)

After publishing, update both nightly references in README.md. There are two — update both or one goes stale:

(a) Release-notes link (Install table row):

bash
# | **Release notes** | [v0.9.6](...) | [v0.9.7-next](https://github.com/scouzi1966/maclocal-api/releases/tag/nightly-YYYYMMDD-SHORTSHA) |
  1. Find the nightly release-notes link in the Install table.
  2. Replace the old nightly-* tag in the URL with the just-published tag (e.g., nightly-20260614-a92a0fe).
  3. If the base version changed, also update the link text (e.g., v0.9.7-next → v0.9.8-next).

(b) Pinned pip wheel example (the "Install a previous version" / pip block):

bash
#   macafm-next==0.9.13.dev20260613           # pinned nightly
  1. Bump the pinned wheel to the one just built: macafm-next==<base>.dev<YYYYMMDD> (e.g., 0.9.13.dev20260614). The wheel version is <base>.dev<YYYYMMDD> — same base as the release, date = build date. (The unpinned pip install … macafm-next command always resolves to latest and needs no change — only the pinned example lags.)

Then verify nothing stale remains, commit, and push:

bash
grep -nE 'dev2026[0-9]{4}|nightly-2026[0-9]{4}' README.md   # all should show the NEW date/tag
  1. Commit (e.g., Update README nightly references to YYYYMMDD-SHORTSHA) and push to remote.

Do not skip this step. The README is the main page users see — both the release link and the pinned wheel example must point to the latest nightly.

Step 5: Verify & Report

After the publish script completes, verify and report:

bash
# Verify GitHub release exists
SHORT_SHA=$(git rev-parse --short HEAD)
DATE=$(date -u +%Y%m%d)
RELEASE_TAG="nightly-${DATE}-${SHORT_SHA}"
gh release view "$RELEASE_TAG" --repo scouzi1966/maclocal-api --json tagName,url,assets -q '.url'

# Verify tap was updated
TAP_DIR="${TAP_DIR:-$(cd "$(git rev-parse --show-toplevel)/.." && pwd)/homebrew-afm}"
grep 'version "' "$TAP_DIR/afm-next.rb"

Report to the user:

  • Release URL (link to the GitHub release)
  • Release tag name
  • Changelog (what changed since last nightly)
  • Install commands (both methods):
    # Homebrew
    brew tap scouzi1966/afm
    brew install scouzi1966/afm/afm-next    # fresh install
    brew upgrade afm-next                    # upgrade
    
    # pip
    pip install --extra-index-url https://kruks.ai/afm/wheels/simple/ macafm-next
Step 5b: Archive Test Results

Copy all test reports from this nightly run to the versioned nightly archive:

bash
DATE=$(date +%Y-%m-%d)
mkdir -p test-reports/nightly/$DATE
cp test-reports/assertions-report-*.html test-reports/assertions-report-*.jsonl \
   test-reports/multi-assertions-report-*.html test-reports/multi-assertions-report-*.jsonl \
   test-reports/nightly/$DATE/ 2>/dev/null || true

# Also copy promptfoo results if they were run
AFM_PROMPTFOO_OUT_DIR="${AFM_PROMPTFOO_OUT_DIR:-/Volumes/edata/promptfoo/data/maclocal-api/current}"
cp "$AFM_PROMPTFOO_OUT_DIR"/*.json test-reports/nightly/$DATE/ 2>/dev/null || true

# Also copy smart analysis if it was run
cp test-reports/smart-analysis-*.md test-reports/nightly/$DATE/ 2>/dev/null || true

git add test-reports/nightly/$DATE/
git commit -m "Add nightly test results for $DATE ($(git rev-parse --short HEAD))"
git push

This maintains the test history at test-reports/nightly/YYYY-MM-DD/ for cross-nightly comparison.

Error Handling
  • Build failure: Show error output, suggest running /build-afm first to diagnose
  • Verification failure (Step 2b): Do not proceed. Investigate the specific check that failed. If patches are stale, re-run ./Scripts/apply-mlx-patches.sh. If resolution is wrong, rm -rf .build and rebuild.
  • Test failures (Step 3): Present the failures and let the user decide: fix and rebuild, publish anyway, or cancel. Do NOT automatically fix test failures — that's the user's decision.
  • gh release create failure: Check gh auth status, check if tag already exists (gh release view <tag>)
  • Tap push failure: Check if ../homebrew-afm is on the right branch and has no uncommitted changes
  • User cancels at any point: Clean exit, no publish. The built binary remains available for manual use.

© scouzi1966, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in .claude/skills/build-afm-nightly-publish of scouzi1966/maclocal-api.

Open the folder on GitHubat commit 138ca5d

Compare with similar skills

Build Afm Nightly Publish next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Build Afm Nightly Publish compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Build Afm Nightly Publish this skillscouzi1966/maclocal-api346—~8.8kAutomated safety check: PassMIT
Install Mimi Remotegaixianggeng/mimi-remote104—~2.8kAutomated safety check: PassGPL-3.0
Releasevaayne/mori303—~1.2kAutomated safety check: PassMIT
Tusk ReleaseShape-Machine/tusk-macos127—~822Automated safety check: PassCustom licence
Releasenikships/droidproxy122—~1.1kAutomated safety check: PassMIT
Swift Developmentaiskillstore/marketplace433—~1.8kAutomated safety check: PassNone

Similar skills

  • Install Mimi Remote

    gaixianggeng/mimi-remote

    安装、配置、配对、迁移、升级、诊断、回滚或卸载 Mimi Remote;在 macOS 上安装和维护 Mimi Remote Mac 菜单栏 App / DMG,或通过 Homebrew、Linux user-systemd 部署 agentd;从源码构建 iPhone/iPad App;配置 Codex 主通道和可选 Claude Code 实验 Runtime。用户提出“安装 Mimi…

    104 GitHub stars~2.8k tokensUpdated yesterday
    MobileAuto-check passed
  • Release

    vaayne/mori

    Release workflow for Mori macOS workspace terminal and MoriRemote iOS app.

    303 GitHub stars~1.2k tokensUpdated 2 mo ago
    MobileAuto-check passed
  • Tusk Release

    Shape-Machine/tusk-macos

    A skill your agent uses when preparing a full Tusk macOS release: bump the app version, regenerate the Xcode project, build and package the DMG, publish the GitHub release, and update README links.

    127 GitHub stars~822 tokensUpdated 4 mo ago
    MobileAuto-check passed
  • Release

    nikships/droidproxy

    Build, sign, notarize, and publish a new DroidProxy release.

    122 GitHub stars~1.1k tokensUpdated yesterday
    MobileAuto-check passed
  • Swift Development

    aiskillstore/marketplace

    Comprehensive Swift development for building, testing, and deploying iOS/macOS applications.

    433 GitHub stars~1.8k tokensUpdated yesterday
    MobileAuto-check passed
  • Deploy

    kangraemin/claude-inspector

    Claude Inspector macOS 배포 스킬. An agent skill from kangraemin/claude-inspector.

    131 GitHub stars~739 tokensUpdated 3 days ago
    AI & LLM EngineeringAuto-check: notes

More from scouzi1966/maclocal-api

All 12 skills in this repo
  • Afm

    scouzi1966/maclocal-api

    Maintain and extend AFM (maclocal-api), a Swift OpenAI-compatible local LLM server and CLI for Apple Foundation Models, MLX models, API gateway proxying, and Vision OCR.

    346 GitHub stars~1.2k tokensUpdated yesterday
    Auto-check passed
  • Build Afm

    scouzi1966/maclocal-api

    Build AFM from scratch — submodules, patches, webui, and Swift build.

    346 GitHub stars~1.8k tokensUpdated yesterday
    Auto-check: notes
  • Codex Promptfoo Agentic Eval

    scouzi1966/maclocal-api

    Run and review the Promptfoo-based AFM agentic evaluation suite.

    346 GitHub stars~1.8k tokensUpdated yesterday
    Auto-check passed
  • Test Afm Binary

    scouzi1966/maclocal-api

    Test a pre-built afm binary at any path — runs pre-flight safety checks, then any combination of unit tests, assertions, smart analysis, promptfoo evals, batch validation, OpenAI compat, GPU…

    346 GitHub stars~3.8k tokensUpdated yesterday
    Auto-check passed
  • Afm Release Wheel

    scouzi1966/maclocal-api

    A skill your agent uses when user wants to build a PyPI wheel from an existing compiled afm binary and publish to PyPI.

    346 GitHub stars~1.5k tokensUpdated yesterday
    Auto-check: warnings
  • Test Macafm

    scouzi1966/maclocal-api

    Run the maclocal-api (AFM/MLX) test suite — automated assertions and smart analysis.

    346 GitHub stars~7k tokensUpdated yesterday
    Auto-check passed

Questions about Build Afm Nightly Publish

What does Build Afm Nightly Publish do?

Build, test, and publish an afm-next nightly release — full from-scratch build, user testing pause, GitHub release, and Homebrew tap update. Build Afm Nightly Publish is an agent skill from scouzi1966/maclocal-api. Build, test, and publish an afm-next nightly release — full from-scratch build, user testing pause, GitHub release, and Homebrew tap update.

When should I use Build Afm Nightly Publish?

Build Afm Nightly Publish fits situations like: user types /build-afm-nightly-publish; asks to publish a nightly build.

How do I install Build Afm Nightly Publish in Claude Code?

Run `npx skills add scouzi1966/maclocal-api --skill build-afm-nightly-publish -a claude-code`. Or copy the skill folder (.claude/skills/build-afm-nightly-publish in scouzi1966/maclocal-api) into .claude/skills/build-afm-nightly-publish in your project. Claude Code loads it when a task matches its description.

How do I install Build Afm Nightly Publish in Codex?

Run `npx skills add scouzi1966/maclocal-api --skill build-afm-nightly-publish -a codex`. Or copy the skill folder (.claude/skills/build-afm-nightly-publish in scouzi1966/maclocal-api) into .agents/skills/build-afm-nightly-publish in your project. Codex loads it when a task matches its description.

Can I use Build Afm Nightly Publish in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add scouzi1966/maclocal-api --skill build-afm-nightly-publish -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/build-afm-nightly-publish, .gemini/skills/build-afm-nightly-publish, .github/skills/build-afm-nightly-publish and .opencode/skills/build-afm-nightly-publish in your project.

What does Build Afm Nightly Publish need to run?

Going by SKILL.md and its folder, Build Afm Nightly Publish needs the command-line tools its instructions call (git, gh, swift, npm, python3 and brew). Our summary lists: Python 3; Node.js.

Does Build Afm Nightly Publish access the network?

SKILL.md names 1 domain. In commands or code: kruks.ai; the agent is likely to contact it when it follows the instructions. This is read from the text; nothing was executed.

Is Build Afm Nightly Publish safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Build Afm Nightly Publish use?

Build Afm Nightly Publish is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Build Afm Nightly Publish use?

About 8.8k tokens (SKILL.md is roughly 35k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Build Afm Nightly Publish?

Skills that share tags, products or a category with Build Afm Nightly Publish: Install Mimi Remote (gaixianggeng/mimi-remote, 104 stars), Release (vaayne/mori, 303 stars), Tusk Release (Shape-Machine/tusk-macos, 127 stars) and Release (nikships/droidproxy, 122 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Build Afm Nightly Publish?

scouzi1966 (a GitHub user) maintains it in scouzi1966/maclocal-api, which has 346 GitHub stars. The repository holds 12 skills in this directory. The repository was last updated on October 10, 2026.

Source: scouzi1966/maclocal-api on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.