Comprehensive guide for conda-forge recipe development...
Mission: Autonomously manage the entire lifecycle of a conda-forge recipe โ from creation to PR submission โ with maximum correctness, security, and quality.
These principles govern all behavior. They integrate the CLAUDE.md Karpathy guidelines with the using-agent-skills non-negotiables. Apply them unconditionally.
Don't assume. Surface tradeoffs and assumptions before any action.
idea-refine: produce a HMW framing + "Not Doing" list before calling generate_recipe_from_pypi.PLAN:
1. generate_recipe_from_pypi โ verify: recipe.yaml created
2. validate_recipe โ verify: no schema errors
3. scan_for_vulnerabilities โ verify: no Critical/High CVEs
โ Executing unless you redirect.
Minimum recipe that solves the problem. Nothing speculative.
build.sh complexity when pip install . suffices.code-simplification).Touch only what the task requires.
Define success criteria. Loop until verified. Transform every task into a verifiable goal:
validate_recipe and optimize_recipe pass with no warnings; scan_for_vulnerabilities finds no Critical CVEs"analyze_build_failure; write the fix; confirm a clean get_build_summary"debugging-and-error-recovery)When a build fails or unexpected behavior occurs: stop, preserve the error log, diagnose systematically, fix the root cause โ never apply a workaround. Resume only after a verified clean build.
source-driven-development)Conda-forge standards evolve rapidly. Before implementing a pattern:
bq show table stats) or carry a "verified $DATE" marker. Live integration tests are load-bearing for quantitative claims; mocked unit tests cover code correctness but cannot find server-side caps, rate limits, response truncation, or quota walls. Numerical claims decay silently โ the 2026-06-12 BigQuery invoice surprise traced to a 2016 napkin number ("~30 GB scanned per query, within free tier") copied through the v8.1.0 spec, code docstring, CHANGELOG, three reference docs, and the quickref cheatsheet without anyone re-verifying. Real cost (verified 2026-06-12 via live dry-run preflight): reference/atlas-phase-engineering.md ยง 13 for the canonical application of this discipline.conda-forge-metadata 0.16.x renamed autotick_bot โ conda_forge_bot), grep the entire skill workspace โ scripts/ + tests/ + tools/ + project-level Python โ not just the script you observed failing. The v8.16.1 fix patched 2 sites (Phase C atlas + update-mapping-cache CLI โ the call sites the failing bootstrap-data run exercised). v8.16.3 had to follow up after pixi run test surfaced 3 more errors in 2 missed sites (name_resolver.py Tier-2 fallback + tests/conftest.py:stub_metadata_api fixture). The discovery procedure (pkgutil.iter_modules to enumerate installed submodules) was correct; the application scope was too narrow because it was limited to "what the runtime entry-point hits", excluding rare-path fallbacks and test infrastructure. Apply the try-new-fallback-to-old import pattern at every grep hit at once; ship one PATCH, not three.These are non-negotiable rules that override all other guidance.
meta.yaml and recipe.yaml cannot coexist in the same build run. The tooling will reject it. If both files exist (e.g., after migrate_to_v1), remove meta.yaml only after validating and successfully building the new recipe.yaml.
stdlib is Required for All Compiled RecipesEvery recipe that uses a C-ABI compiler (c, cxx, rust, fortran, cuda, go-cgo) must include stdlib. Omitting it causes automatic rejection by conda-forge CI.
Exception: go-nocgo (pure Go, no CGO) does not require stdlib("c"). Legacy compiler("go") is treated as go-nocgo.
# recipe.yaml (v1)
requirements:
build:
- ${{ compiler("c") }}
- ${{ stdlib("c") }} # REQUIRED โ never omit
# meta.yaml (v0)
requirements:
build:
- {{ compiler('c') }}
- {{ stdlib('c') }} # REQUIRED โ never omit
Go compiler macros (use the correct name โ compiler("go") is deprecated):
# Pure Go (no CGO) โ no stdlib needed
- ${{ compiler("go-nocgo") }}
# Go with CGO โ requires c compiler + stdlib
- ${{ compiler("c") }}
- ${{ stdlib("c") }}
- ${{ compiler("go-cgo") }}
Do not pin compiler versions manually. Use the compiler() macro โ the global pins (GCC 14 on Linux, Clang 19 on macOS) resolve automatically. compiler_stack is deprecated and should be removed from any existing conda-forge.yml.
3.11Python 3.10 was dropped from the conda-forge build matrix on 2026-09-02 (conda-forge-pinning 2026.09.02.08.53.43); 3.9 went in August 2025. The floor is 3.11. Never set python_min below 3.11 for new recipes. See Python Version Policy for full rules.
Never hardcode the floor โ read it. It has now moved twice. The authority is
conda-forge-pinning's own conda_build_config.yaml, and the local copy of it is
.pixi/envs/local-recipes/conda_build_config.yaml:
grep -A 4 '^python_min:' .pixi/envs/local-recipes/conda_build_config.yaml
Upstream source of record:
https://github.com/conda-forge/conda-forge-pinning-feedstock/blob/main/recipe/conda_build_config.yaml.
The repo-root conda_build_config.yaml is a verbatim local-testing copy of that file โ re-sync
it (never hand-edit) when it drifts. tests/meta/test_no_redundant_python_min.py reads the floor
dynamically from the installed pinning, so it follows the ecosystem on its own; a hardcoded floor
in prose or in a recipe does not.
source.url Must Use the pypi.org/packages/... PatternRecipe source.url: for PyPI artifacts must route through https://pypi.org/packages/..., never https://files.pythonhosted.org/packages/<hash>/.... The hashed files.pythonhosted.org URL is what PyPI's JSON API returns and what grayskull historically emitted, but it bypasses standard JFrog Artifactory PyPI Remote Repository proxies in air-gapped corporate environments.
Canonical 2026 shape โ package.name is the literal distribution name; context: carries only version (+ optional python_min override); the URL's path segments are literal and only ${{ version }} interpolates. This is what current grayskull emits and what conda-forge reviewers expect:
context:
version: "1.0.0"
package:
name: my-package
version: ${{ version }}
source:
url: https://pypi.org/packages/source/m/my-package/my-package-${{ version }}.tar.gz
For sdist filenames that use underscores instead of hyphens (e.g. litellm_proxy_extras-0.4.69.tar.gz for project litellm-proxy-extras, py_yaml12-0.1.0.tar.gz for py-yaml12), keep the path-2 segment hyphenated (distribution name) and the filename stem underscored:
source:
url: https://pypi.org/packages/source/l/litellm-proxy-extras/litellm_proxy_extras-${{ version }}.tar.gz
Wheel (LAST resort โ only when no usable sdist AND no GitHub source archive exist; see G54/G55 โ a wheel-only PyPI package often still has source on GitHub, and many sdists are metadata-only/broken):
source:
url: https://pypi.org/packages/<py-tag>/m/my-package/my-package-${{ version }}-<py-tag>-none-any.whl
where <py-tag> is the wheel's Python tag (py3, py2.py3, cp310, โฆ). The <py-tag> segment is upstream-specific and may change on a version bump โ flag this with a recipe comment.
Why fully literal? The older ${{ name[0] }} / ${{ name }} / ${{ name | replace("-", "_") }} chain was a holdover from context.name-style recipes. Now that the distribution name lives only in package.name, the URL is just a path โ write it as a path. Renames are rare, version bumps are common, and the literal form is more legible and removes a class of jinja-typo bugs.
The recipe-generator.py script emits this shape automatically; manual edits and recipe reviews must enforce the same form. See docs/enterprise-deployment.md ยง3 for the full proxy rationale.
build.bat Must call Every .cmd Shim (pnpm/npm/yarn)On Windows, pnpm, npm, yarn, npx, and similar tools ship as .cmd wrappers. Invoking a .cmd from a .bat without call causes cmd.exe to transfer control to the shim โ when it returns, the parent script terminates with exit 0 instead of continuing. The build appears to succeed but later steps never run, and rattler-build emits misleading errors like ร No license files were copied.
:: WRONG โ script silently terminates after pnpm returns
pnpm --version || exit /b 1
cargo --version || exit /b 1 :: never runs
:: RIGHT โ `call` recurses and returns control to the parent
call pnpm --version
if errorlevel 1 exit /b 1
cargo --version
if errorlevel 1 exit /b 1
call is harmless on real .exe (node.exe, cargo.exe, rustc.exe); when in doubt, add it. See guides/ci-troubleshooting.md ยง "Silent build.bat Termination".
_path_guardAny surface that accepts a caller-supplied path or recipe slug โ an MCP tool
argument, a CLI positional โ must confine it to the recipes/ subtree via
scripts/_path_guard.py, never a hand-rolled check:
from _path_guard import (
REPO_ROOT, recipes_root,
validate_recipe_name, # a flat slug: rejects "/", "\", "..", control bytes
resolve_under_recipes, # any path: resolves symlinks, requires containment
validate_recipe_file_path, # the above + a .yaml/.yml suffix check
)
Three surfaces skipped it and each was a real escape (AUD-CFE-001/002/006):
submit_pr.prepare_branch joined a slug onto recipes/ and copytree'd the
result into a public fork; recipe_editor.execute_actions validated only the
file suffix, so it would rewrite any .yaml/.yml in the repo (pixi.toml's
siblings, .github/workflows/*.yml); trigger_build would build any recipe path
on the filesystem. A suffix check is not a location check.
Two properties worth preserving if you touch the helper:
.resolve() before comparing, so a symlink inside recipes/ that points
out is also rejected โ and use is_relative_to, not a string prefix, or
recipes-evil/ passes a recipes/ root.CFE_RECIPES_ROOT, else <repo>/recipes).
Capturing it at import would make the test suite's override silently
ineffective in an already-imported module โ the fixture copies live in
tmp_path, outside the real tree.tests/unit/test_path_guard.py covers the escapes; add a case there rather than
re-deriving the rules at a new call site.
_paths, Never a Hand-Rolled WalkAny new script needing the skill-scoped data directory (.claude/data/conda-forge-expert/)
or the monorepo root must import scripts/_paths.py:
from _paths import get_data_dir, get_repo_root
A Rule-2 retro (Story 5.5) found ~30 hand-rolled copies of a 1-line _get_data_dir()
across scripts/*.py, in three subtly different shapes โ with and without
.resolve() on __file__, and two files (feedstock_context.py,
feedstock_lookup.py) at the wrong depth entirely, resolving
.claude/skills/data/conda-forge-expert instead of .claude/data/conda-forge-expert.
That divergence was live, not theoretical: the wrong directory had actually been
created on disk and .gitignore'd rather than root-caused. A second, independent
divergence hit REPO_ROOT the same way โ bootstrap_data.py walked one level too
far (parents[5], landing above the real repo root) and recipe_optimizer.py's
_read_conda_forge_python_floor() walked one level too shallow (parents[3],
landing on .claude/ itself), which meant it could never find the real
.pixi/envs/local-recipes/conda_build_config.yaml pinning file and silently
fell back to its hardcoded default on every call since SEL-004 shipped.
All four confirmed-wrong call sites now import _paths; the other ~26 duplicated copies
were not mass-migrated in that pass (disproportionate blast radius for a
closing-retrospective commit) โ migrate them opportunistically when you're already
touching that file. They are duplicated-and-depth-correct, not clean: 35 files under
scripts/ still contain at least one un-.resolve()d Path(__file__).parent walk
(grep -lE 'Path\(__file__\)\.parent' scripts/*.py | wc -l), so a large share of the
deferred population is latently wrong by this constraint's own rule. .resolve() matters even when the depth is right: without it, a parent-walk
over a symlinked invocation path can land one directory off from the real location. If
you touch one of these files for any reason, at minimum add .resolve().
get_repo_root()/get_data_dir() compute lazily and return None (never raise) on
(IndexError, OSError) โ a resolution failure surfaces only to a caller that actually
calls the function, not as an import-time crash for every script that merely imports
_paths. None is a real return value and nothing downstream catches it for you:
binding it straight into a module-scope _DIR / "name" is a TypeError at import โ
the same crash the lazy contract exists to avoid, one file over, and one that the
except ImportError guards wrapping sibling-module loads elsewhere in this skill do
not catch. Handle it explicitly at the call site: degrade (disable an optional
cache, fall back to a documented default) or exit with a diagnostic.
_http.auth_headers_for used to attach JFROG_API_KEY / JFROG_USERNAME+PASSWORD
to every outbound request whenever the env var was set, regardless of the target
host โ a cross-resolver credential leak (surfaced 2026-05-10, partially mitigated by
the v8.14.0 skip_auth call-site opt-out). A Rule-2 retro (Story 5.5) landed the fix:
the JFrog branches are gated on _configured_enterprise_hosts(), a set derived
(never hardcoded) from every currently-set *_BASE_URL env var's host and the
operator's pixi config (_pixi_configured_hosts() โ mirrors/default-channels/
pypi-config.index-url+extra-index-urls, deliberately excluding those resolvers'
own public-default fallbacks). Both sources matter:
docs/reference/pixi-config-jfrog.example.toml documents project-local
.pixi/config.toml โ no env vars at all โ as this repo's recommended enterprise
setup, so an env-var-only allowlist would have silently dropped JFrog auth for the
documented path, a functional regression the same-pass adversarial review caught
before it shipped. The gate is host-level, not repo-scoped or resolver-scoped โ a
JFrog instance fronting several ecosystems under one credential is expected to receive
that credential for all of them โ but it closes the leak that mattered: a public
fallback host the operator never configured anything for used to receive the header
whenever the key was merely set. Closing that without an SSRF-style private-IP
denylist matters because a denylist would wrongly block the very enterprise mirrors
this routing exists to reach. skip_auth=True still short-circuits everything and
remains the right call for a known-public endpoint regardless of configuration; the
host gate is defense in depth for every other call site, not a replacement for it. A
malformed *_BASE_URL/pixi-config URL value (e.g. an unclosed IPv6-bracket literal)
contributes no host rather than crashing the whole allowlist derivation โ urlparse()
raises ValueError on those, and the derivation catches it per-entry.
github.com/api.github.com are always excluded from the JFrog gate, regardless
of _configured_enterprise_hosts() โ the JFrog/GitHub-token logic shares one
if/elif chain, and without this exclusion a *_BASE_URL that happened to resolve to
either host would shadow GITHUB_TOKEN with an unrelated JFrog credential.
Derive every host with urlparse(url).hostname, never netloc.split(":")[0]. The
allowlist decides whether a credential is withheld or handed over, so a sloppy parse
fails in both directions at once. netloc.split(":")[0] returns the username for
https://svc:tok@artifactory.corp/api/conda/cf โ a routine Artifactory form, whose
real mirror then never enters the allowlist and silently stops receiving its own
credential โ and it collapses every IPv6 literal to [2001, so an unrelated
address sharing that prefix matches and receives the credential. .hostname
strips userinfo, unwraps the brackets, and still strips the port (the property the
comparison needs so a *_BASE_URL with an explicit port matches a request URL
without one).
Public default hosts can never be allowlisted. _public_default_hosts() derives
them from this module's own _DEFAULT_* fallback globals โ never a hand-kept list โ and
both halves of the allowlist subtract them. The coverage is automatic only for a
resolver that declares its fallbacks in a _DEFAULT_* global; one that inlines them
as f-string literals in its own body (resolve_anaconda_channel_urls does) is covered
only if some other resolver's global happens to name the same host. Declare your
fallbacks in a _DEFAULT_* global, or your resolver's public host can be allowlisted. Otherwise a merely redundant PYPI_BASE_URL=https://pypi.org/simple
marks the public host "configured" and re-opens the leak.
The fourth review pass found that gap was not hypothetical: anaconda.org,
files.pythonhosted.org, repo.anaconda.com and dev.azure.com are all hosts this
module requests and none is named by any _DEFAULT_* global, so each was reproduced
receiving JFROG_API_KEY under a *_BASE_URL naming it. _PUBLIC_HOST_FLOOR now sits
under the derivation for exactly those three escape classes (URL built inline;
CDN/redirect target; a vendor's second domain) โ a floor beneath the derivation, never
a replacement for it: keep declaring fallbacks in globals, and the floor stays short.
Two further rules fall out of that pass. A set you SUBTRACT must never be cached
empty โ _public_default_hosts() recomputes rather than freezing an empty result,
because an empty subtrahend silently re-opens the gate for the life of the process, and
empty can only mean a load-order bug (the globals are declared below the function).
And a hand-kept mirror of a derived set drifts toward the leak: inventory_channel.py's
no-_http floor sat at 9 hosts against _http's 18 while its own docstring claimed it
was never wider โ a test now pins the containment. Non-*_BASE_URL mirror
vars count too when a resolver honours them: resolve_npm_urls reads npm's own
npm_config_registry/NPM_CONFIG_REGISTRY, so those are in the scan
(_EXTRA_MIRROR_ENV_VARS). Adding a resolver that reads a new non-_BASE_URL var
means adding it there, or that operator's mirror 401s.
The gate is skipped entirely when no JFrog credential is set โ it gates nothing
else, and deriving it costs an os.environ walk plus a stat + TOML parse of the pixi
config chain on a path every make_request() takes.
A test of the gate must clear the ambient environment first. The allowlist comes
from whatever *_BASE_URL vars the shell happens to export, so an exact-set assertion
depends on who runs it. A Claude Code session exports ANTHROPIC_BASE_URL, which put
api.anthropic.com into the set and failed a local pr-preflight (2026-09-29) while
CI, with no such var, stayed green. Host-gate test modules opt into the shared
clean_mirror_env fixture (tests/conftest.py), which removes every *_BASE_URL
plus _EXTRA_MIRROR_ENV_VARS. A test that asserts the whole allowlist also stubs
read_pixi_config, the gate's other source.
This gate HAD TWO independent copies elsewhere in this skill โ
dependency-checker.py's _auth_headers (its own implementation, broader alias
vocabulary, never delegated to _http.py) and inventory_channel.py's _make_request
no-_http fallback. Both were fixed in the Story 5.5 pass. Before adding a THIRD
standalone auth-header builder anywhere in this skill: check whether
_http.auth_headers_for/make_request can be used directly first; a new independent
copy is exactly how this leak survived one full "durable fix" landing undetected โ and
each copy then has to re-earn every property the original gained. Both copies needed a
second and a third pass to reach parity: inventory_channel.py's fallback shipped
without the public-default subtraction, so it was wider than _http in exactly the
direction that leaks while its docstring claimed it was deliberately narrower. When
you copy a gate, copy its exclusions too, and test the copy against the same
scenarios โ a divergence here is silent by construction.
Gate the credential KIND, not just the host. "May this host receive a credential?"
and "may it receive this credential?" are different questions, and answering only the
first leaks. dependency-checker.py authorizes the channel the operator NAMED, and
that channel is routinely public (--channel https://conda.anaconda.org/myprivateorg
is a supported setup) โ so host-level authorization alone sent the JFrog API key to
anaconda.org and, because the JFrog branch is checked first, shadowed the
CONDA_TOKEN that channel actually needs. Splitting by kind fixes both at once: a
named public host gets its channel token and nothing else; a non-public configured host
keeps the original priority. A credential's destination is a property of the
credential, not only of the request.
Scope an authorization set to the call that built it. _EXPLICIT_CHANNEL_HOSTS is
cleared at the top of get_configured_channels, not accumulated. A module-level set
that only ever grows is a per-process credential allowlist assembled from caller
arguments: a host named by one call's --channel stays authorized for every later
call in that process.
Be exact about which process, though โ the fourth review pass found this rule's own
first write-up asserting a reproduction it never had. The MCP server does not run
these scripts in-process. conda_forge_server.py::_run_script is
subprocess.run([sys.executable, script_path, *args]), so dependency-checker.py
gets a fresh interpreter per tool call and no module-level state survives it. The
accumulation is real in-process and the per-call clear is the right contract, but the
production trigger described here โ and in the matching sys.path-growth note below โ
does not exist on the MCP path. When you justify a fix with a failure mode, name the
entry point you traced it through; "a long-lived server" is an assumption about
process lifetime, and in this skill it happens to be the wrong one.
A host gate must admit the hosts the tool was pointed at, not only *_BASE_URL
ones. dependency-checker.py takes its channel from --channel /
CONDA_CHANNEL_URL / the enterprise config โ none of which is a *_BASE_URL โ so
gating on the shared allowlist alone withheld the credential from the operator's own
Artifactory channel (a silent 401/404 on the one channel they configured, and the
reason CONDA_TOKEN, an anaconda.org channel token rather than a JFrog one, reached
nothing at all). It now unions _EXPLICIT_CHANNEL_HOSTS, recorded by
get_configured_channels on its explicit branches only โ never on the step-4 public
fallback. When tightening a credential path, enumerate every way the operator can name
a destination before assuming one env-var family covers them.
Migrate every host-parse in the file, not only the ones on the gate's path.
netrc_credentials was the third netloc.split(":")[0] in _http.py and the one the
first two passes did not touch, so a userinfo-bearing Artifactory URL matched no
machine line and fell through to any default entry โ sending an unrelated
credential to the mirror the operator had a correct entry for. Grep the whole file for
the banned form when you fix one instance of it.
โฆbut check what else the canonical parse changes. That migration then broke netrc
matching in the opposite direction: _host_of is urlparse().hostname, which
LOWERCASES, while netrc.authenticators is an exact dict lookup over the machine
tokens as spelled in the file and folds nothing. A legal machine ARTIFACTORY.CORP.COM
stopped matching and fell through to default โ the same wrong-credential-to-the-mirror
failure the migration was made to fix, re-entered by the fix itself. Machine lines are
matched case-insensitively now. A shared helper carries all of its normalizations to
every call site, not only the one you wanted; before reusing one on a new path, list
what it normalizes and check each against that path's own matching rules.
Guard Path.home() on any path a request can reach. It raises RuntimeError โ not
OSError, so an except OSError does not catch it โ when neither HOME nor a passwd
entry resolves, which is ordinary in rootless / arbitrary-UID containers. This has now
bitten twice in the same feature: once via read_pixi_config in the allowlist
derivation, once via netrc_credentials when gating made that branch reachable. Both
degrade to "no credential" and log; neither takes the process down.
Closing one credential branch makes the next one reachable. Before the gate, a set
JFROG_API_KEY matched auth_headers_for's first if unconditionally, so the generic
.netrc fallback at the end of the chain never ran in that state. It does now: an
operator with both a JFrog key and a default entry in .netrc sends those Basic
credentials to unconfigured hosts. That is default's documented netrc semantics and so
is the operator's own declared intent โ but it is a behaviour change, and it is the
general shape to check for. When you add a guard to a branch in an if/elif chain,
work out which branch now receives the traffic and whether that is acceptable.
Every recipe.yaml (v1 format) must start with these two lines verbatim โ no blank line before them:
# yaml-language-server: $schema=https://raw.githubusercontent.com/prefix-dev/recipe-format/main/schema.json
schema_version: 1
The comment drives editor schema validation (VS Code, Helix, Zed, IntelliJ); schema_version: 1 flips rattler-build into the v1 parser. Build tooling treats v1 as implicit when the file is named recipe.yaml, so omitting either line is not a build error.
Empirical adoption: only 1 in 30 recent merged staged-recipes PRs (AprโMay 2026) carries the comment header; conda-forge reviewers do not block PRs that omit it. This is a local-recipes repo convention, enforced by the tests/meta/test_recipe_yaml_schema_header.py meta-test and by the optimizer's SCHEMA-001 check โ both written specifically for this repo so editor validation is on by default for every recipe under recipes/.
recipe-generator.py enforces this on all three v1 generation paths (PyPI/grayskull, rattler-generate for CRAN/CPAN/LuaRocks, npm). The optimize_recipe check SCHEMA-001 flags any user-edited recipe.yaml that drops them.
meta.yaml (v0 format) does not use the schema header โ it predates the prefix-dev schema. Only apply this rule to recipe.yaml.
# CFE comments BlockThe recipe body must stay free of CFE/Claude/agent-authored comments. Do not scatter rationale, gotcha tags, or explanatory notes (# G25 flatten, # load-bearing under pip_check, # license pattern 2, # python_min override becauseโฆ, # BFP fix:โฆ, build-env explanations, etc.) inline through requirements:, build:, tests:, about:, or any other body section. This keeps the local recipe.yaml a clean copy-paste source into a feedstock / staged-recipes PR, and keeps submitted conda-forge recipes free of AI comment noise.
Two distinct comment classes โ handle them differently:
Existing human / upstream-feedstock comments (already in the source recipe or on the conda-forge feedstock โ e.g. # Node.js build environment, # pnpm package manager) โ LEAVE in the body verbatim. Never remove or relocate them; the local recipe must stay a faithful mirror of the feedstock. Removing them is a defect.
New comments the agent wants to add (any rationale the agent generates) โ never inline. Write them ONLY in the bottom # CFE comments block, organized by the recipe location they refer to. A human later curates โ copying up into the body only the notes worth keeping in the submitted recipe.
The only comment that stays at the top of the body is the functional schema-header line (# yaml-language-server: $schema=โฆ) โ it's a directive, not an annotation.
Canonical layout (see recipes/ironcalc/recipe.yaml โ the reference exemplar). In extra:, after recipe-maintainers: + a blank line. The #### / # CFE โฆ lines are YAML comments at column 0; the cfe-* keys are real YAML indented 2 spaces under extra::
extra:
recipe-maintainers:
- <handle>
#### CFE metadata AND comments
# CFE metadata
cfe-conda-name: <name>
cfe-upstream-registry: <pypi|npm|cargo|maven|cran|cpan|luarocks|golang|github>
cfe-upstream-name: <name in that registry>
cfe-purls:
- pkg:conda/<name>@${{ version }}?channel=conda-forge
- pkg:<registry>/<upstream>@${{ version }}
cfe-upstream-repo: <url>
cfe-upstream-homepage: <url>
cfe-import-names: [<top-level python import(s)>]
cfe-source-kind: <pypi-sdist | pypi-wheel | github-tag | github-commit | github-release-binary>
cfe-noarch: <python | generic | compiled>
cfe-pip-check: <true | false:<reason-code>>
cfe-on-conda-forge-status: <confirmed-on-conda-forge | pending-submission-to-conda-forge | pending-approval-on-conda-forge | blocked-pending-prerequisites | pypi-only | archived-on-conda-forge>
cfe-on-conda-forge-feedstock: <feedstock-url | none>
cfe-forge-recipe-updates-needed: <none | list>
cfe-forge-blocker-list: [] # or a YAML list of blockers
cfe-last-checked: <ISO-8601 UTC>
cfe-generated-by-version: <ver>
cfe-generated-at-datetime: <ISO-8601 UTC>
cfe-local-build-status: <success | build-clean-test-blocked | failed | not-attempted>
cfe-local-build-datetime: <ISO-8601 UTC | none>
cfe-local-build-platform: <host subdir the build ran on, e.g. linux-64 | none>
cfe-local-build-tool: <rattler-build | conda-build | none>
####
# CFE comments
# Header:
# # <general / provenance notes the agent would have put at the top>
# build:
# script:
# env:
# # <the note that would have been inline in build.script.env>
# context:
# # <โฆ>
# host:
# # <โฆ>
# run:
# # <โฆ>
####
cfe-upstream-registry must name the registry the package actually publishes to โ it is not decoration: bmad_suite_metapackage.py resolves suite pins through it ([npm+floor] vs [github+floor]), github_updater.py / the MCP update_recipe* tools route through it, and a wrong value fails silently as "upstream unknown". Live case: recipes/bmad-method carried cfe-upstream-registry: pypi (+ a pkg:pypi/bmad-method purl) for a package that only exists on npm; the generator's generate failed rc=1 note sat in the recipe's CFE comments for weeks because the PyPI lookup 404'd. Check the purl and the registry agree with source.url before stamping the block (corrected to npm 2026-09-05).
The # CFE comments block mirrors the recipe's structure (location keys build / context / host / run / requirements / about / tests) so each parked note shows where it would belong if promoted. Both the # CFE metadata and # CFE comments sections are CFE-local-only and are stripped before any push (along with extra.cfe-* keys). recipe-generator.py must emit new rationale into this block, never inline.
The 4 identity/decision fields are the cached "hard-won" answers (added v8.37.0; from the 4-analyst deep-analysis synthesis, 2026-06-19). They sit at the end of the identity/upstream block (after cfe-upstream-homepage, before cfe-on-conda-forge-status). Each caches a value that authoring would otherwise have to recompute โ and that a regen (grayskull / recipe-generator.py re-running over a version bump) would re-guess, possibly wrong. Value semantics:
cfe-import-names โ the verified top-level Python import name(s) โ the names the CFEP-25 test imports: use. This caches the G7/G10 divergence: when the import name does NOT match the distribution name (altk, OpenDsStar, pymilvus.model, ibm_boto3, baidubce, data_diff), grayskull re-guesses it wrong on every regen and the import test breaks. With the verified value cached, a regen restores the correct import instead of re-deriving it from the sdist. This is the single highest-frequency authoring recompute, and is especially load-bearing for the feedstock-refresh effort (256 regens). Non-Python recipes: [].cfe-source-kind โ which artifact the recipe actually sources: pypi-sdist, pypi-wheel, github-tag, github-commit, or github-release-binary (a prebuilt single-file release binary that is repackaged rather than built โ e.g. a self-contained .NET CLI, see G44). When a non-PyPI source was chosen deliberately (because the PyPI sdist re-trips a known gotcha), append the reason: github-tag:pypi-sdist-strips-headers-G5. This prevents a version bump from silently reverting to a PyPI sdist that re-trips G4 / G5 / G9 / G16. Keep this field in sync with the actual source: block: when you switch the source (wheelโsdist/github), UPDATE cfe-source-kind โ it is the cached decision a regen reads back, and a stale value (e.g. pypi-wheel on a recipe already sourcing a GitHub tag) mis-routes the next regen and hides the recipe from the G54 retroactive wheel-sweep. Live miss: pybase62 was switched to a GitHub tag archive but left cfe-source-kind: pypi-wheel.cfe-noarch โ the build shape: python (noarch:python), generic (noarch:generic), or compiled (a per-arch / per-Python compiled build). Drives the build matrix and the per-Python prerequisite fan-out (G38 / G40). Per G42 it can flip across versions โ verify the current version's artifact shape (e.g. milvus-lite 3.0 went C++-compiled โ pure-Python), don't trust the older version's reputation.cfe-pip-check โ whether pip_check: true is in effect. When it is intentionally off, record false:<reason-code> so the temporary external-bug waiver and its revert obligation (G24 / G26 / G28 / G36 โ e.g. an upstream dep's poisoned wheel METADATA / dist-info version) is not silently lost on strip-before-push. The reason code names the blocking package so the waiver can be re-checked and revoked when the upstream is fixed.The cfe-local-build-* fields are the LOCAL-BUILD verification record (added v8.36.0). They capture "does this recipe build locally?" โ a verified fact โ and are deliberately separate from cfe-on-conda-forge-status, which tracks "is it on / submittable to conda-forge?". The two are orthogonal: a recipe with cfe-on-conda-forge-status: blocked-pending-prerequisites may have built perfectly locally (green against the local channel) and is "blocked" only because a prerequisite isn't on conda-forge yet โ cfe-local-build-status: success records that the recipe itself is sound. Always set cfe-local-build-status from the actual outcome of the last local build, never inferred from the cf-submission status.
cfe-local-build-status value semantics:
success โ build EXIT=0 and the import test and pip_check all pass; the artifact landed in noarch/ (or linux-64/ for arch builds). The recipe is fully verified locally.build-clean-test-blocked โ the build phase was clean (the .conda was written) but the test-env solve failed on a missing / local-only / not-yet-on-conda-forge dependency (rattler-build quarantines the artifact to broken/). The recipe itself built fine; the block is a prerequisite gap, not a recipe defect. (Common in the langflow-closure class of multi-layer recursion.)failed โ the build phase itself failed (compile / packaging error). The recipe is not yet sound.not-attempted โ recipe authored but not built yet. This is the default the recipe-generator emits.The companion fields are none until a build runs: cfe-local-build-datetime (ISO-8601 UTC of the build, else none), cfe-local-build-platform (the host subdir the build ran on, e.g. linux-64, osx-arm64, else none), and cfe-local-build-tool (rattler-build for v1 / conda-build for v0, else none). Like every cfe-* key, all four are stripped before any push.
Recording the first build on a recipe that has no cfe-* block? Add the full canonical block โ identity + the four decision fields (cfe-import-names / cfe-source-kind / cfe-noarch / cfe-pip-check) + cfe-on-conda-forge-status + all four cfe-local-build-* fields โ never a cfe-local-build-*-only stub. Older pre-convention recipes (e.g. firecrawl-py, mem0ai) often carry only extra.recipe-maintainers; recording their first local build means adding the complete block, not appending a six-field fragment under a bare #### CFE metadata header (the failure mode caught in local-recipes PR #24's first draft).
cfe- schema design principle* (from the 4-analyst deep-analysis synthesis, 2026-06-19). What earns a cfe-* field is governed by three rules:
cfe-* field is justified only when it stores something stable that authoring would otherwise have to recompute โ an identity fact (cfe-conda-name, cfe-import-names, cfe-upstream-name) or a hard-won decision (cfe-source-kind, cfe-pip-check, cfe-on-conda-forge-status). Volatile signals โ CVE counts, download numbers, feedstock-health, adoption-stage, who-depends โ are NEVER cached: read them live from the atlas at the moment they're needed. Caching a volatile metric manufactures staleness โ the recipe ships a number that was true once and is wrong now.cfe-last-checked as a hint, never ground truth. A few fields (e.g. cfe-on-conda-forge-status) are stable enough to cache but can still drift. They carry cfe-last-checked so a reader knows the value is a hint with an age, not an authoritative fact โ re-verify before relying on it for a decision.cfe-* are stripped before any push. Every cfe-* key (and both # CFE โฆ comment blocks) is local-recipes-only and removed before the recipe is copied into a feedstock / staged-recipes PR. They exist to drive CFE/admin/maintainer tooling, never to ship.The 2026-06-19 deep-analysis disqualified three previously-floated SBOM/security placeholders โ none was ever encoded, and none should be: cfe-cpe (the CPE is syft-derived from the package name โ recompute, don't cache), cfe-sbom-hash (the SBOM's home is conda-meta/ per unratified CEP #127, not recipe extra:), and cfe-syft-ref (attestation lives in an external Sigstore bundle). None of the three reaches a tool from recipe extra:, so none earns a cfe-* field.
Tier-2 orchestration fields are PLANNED โ they land with the feedstock-refresh effort (where they get populated), not before:
cfe-feedstock-version + cfe-upstream-latest-version โ the two cached legs of the behind-upstream triple (the third, the live-deployed feedstock version, is read live). Stamped-hint fields (carry cfe-last-checked).cfe-platforms-shipping / cfe-platforms-target โ the feedstock's current vs. desired platform coverage (drives platform-expansion PRs).cfe-submission-pr-state โ the open/merged/closed state of the PR named by the existing cfe-submission-pr, completing that field.noarch: python RecipesThe canonical test for a noarch: python library recipe is the CFEP-25 triad:
tests:
- python:
imports:
- <top_level_module>
pip_check: true
python_version:
- ${{ python_min }}.*
- "*"
This is what conda-forge's web-service review (the post-merge linter) expects on every noarch:python recipe. A recipe that uses package_contents.site_packages: instead โ even when the artifact is otherwise clean โ will be flagged by the web service with the canonical hint:
noarch: python recipes should usually follow the syntax in our documentation for specifying the Python version. For the
tests[].python.python_versionortests[].requirements.runsection of the recipe, you should usually use the pinpython_version: ${{ python_min }}.*orpython ${{ python_min }}.*for the python_version or python entry.
conda-smithy lint (the local linter the skill's validate_recipe runs) does not fire this; it's a web-service-only check. So a recipe can pass every local gate, build cleanly, install correctly โ and still get a review comment from the web service.
Escape hatch โ when package_contents.site_packages: is permitted:
Only when the test environment genuinely cannot exercise python.imports:
import myapp.<module> raises ImproperlyConfigured without DJANGO_SETTINGS_MODULE, and the test env's solver can't resolve the 40+ transitive Django-plugin chain even if settings were configured.mediapipe, etc.) that the test env can't install.noarch: python recipe (legacy grayskull-emitted pattern) where the ABI-specific .so won't match the test env's Python.In these cases, document the reason directly above the tests: block with an inline comment whose first token is # CFEP-25-justified::
# CFEP-25-justified: Django web-app backend โ import fails without
# DJANGO_SETTINGS_MODULE configured.
tests:
- package_contents:
site_packages:
- <module>
Before settling for package_contents, try to make the import work (v8.78.0). The escape hatch above verifies only that files landed โ it proves nothing about importability, which is the thing the test existed to check. For the Django class specifically, the blocker is usually just that some module reads settings.<X> at import scope, and a two-line settings.configure() is enough to get a real import. Prefer a script: test that configures settings and then imports for real:
# CFEP-25-justified: Django app โ a bare `import <pkg>` raises ImproperlyConfigured
# because <pkg>.config reads settings.DEBUG at module scope. These script tests
# configure settings first and then import for real, on python_min and newest.
tests:
- script:
- python -c "from django.conf import settings; settings.configure(DEBUG=False); import <pkg>; print(<pkg>.__version__)"
- pip check
requirements:
run:
- pip
- python ${{ python_min }}.*
- script:
- python -c "from django.conf import settings; settings.configure(DEBUG=False); import <pkg>; print(<pkg>.__version__)"
- pip check
requirements:
run:
- pip
- python
Two blocks, not one, so the python_min and newest-Python legs are both covered โ that is what the CFEP-25 triad's python_version: [${{ python_min }}.*, "*"] buys, and a script test cannot express it in a single block. Pin python explicitly in each block's requirements.run: or the solver silently resolves only the newest interpreter. pip check is invoked directly (a script test has no pip_check: key), which keeps the dependency-graph validation the escape hatch would otherwise discard โ including any flattened extras (G25).
Determine the minimum viable configuration empirically, in the failed build's surviving test_env (the G27 probe technique) rather than guessing how much Django setup is needed:
TE=$(ls -d build_artifacts/<cfg>/test/test_<name>*/test_env | head -1)
"$TE/bin/python" -c "from django.conf import settings; settings.configure(DEBUG=False); import <pkg>; print('OK')"
Escalate only as far as the probe demands: bare settings.configure(DEBUG=False) โ configure(INSTALLED_APPS=[...], DATABASES={}) + django.setup(). Stop at the first form that works; the fuller form drags in app-registry checks and their warnings for no verification gain. Fall back to package_contents-only when even a configured import can't run (the ML-benchmark / unsolvable-test-env classes above), and note that optimize_recipe's TEST-003 is satisfied by the # CFEP-25-justified: comment either way โ so the comment is required for the script-test form too.
The recipe_optimizer check TEST-003 flags any noarch:python recipe missing both python.imports: AND the # CFEP-25-justified: comment. This catches the same drift that a web-service review would catch โ before submission.
recipe-generator.py always emits the canonical CFEP-25 triad on the PyPI / grayskull path. Manual edits and remediation passes must not silently substitute package_contents without the justification comment.
Per conda-forge.org/docs/maintainer/adding_pkgs/ โ "License files":
The license should only be shipped along with the recipe if there is no license file in the downloaded archive. If there is a license file in the archive, please set
license_fileto the path of the license file in the archive.
This produces three patterns in order of preference:
license_file: LICENSE and let rattler-build pick it up from the extracted source tarball. No file in the recipe directory, no secondary source.LICENSE file directly into recipes/<name>/ alongside recipe.yaml. Use license_file: LICENSE. rattler-build resolves the path relative to the recipe directory when the file is not found in the extracted source. Also notify the upstream developers that the license file is missing โ shipping LICENSE in the recipe is a workaround, not a long-term solution.source.url fetching LICENSE from GitHub (the pattern this skill used pre-v8.10.0) โ works but is non-canonical. Adds a brittle commit-pin + sha256 maintenance burden and looks unusual to reviewers. Convert to pattern (2) when you encounter it.When pattern (2) is used, ship the LICENSE in-recipe and remove the stale "upstream archive ships no LICENSE; secondary source pulled..." comment from source:. The source: block can return to its flat single-URL form.
An sdist that vendors node_modules/ โ ship the licenses of what SHIPS, not of what's vendored (v8.78.0). Some Python sdists vendor an entire src/js/node_modules/ tree (hundreds of LICENSE files) to make their JS build hermetic. Enumerating them all into license_file: is wrong in both directions: it bloats the recipe, and it pollutes about.license with SPDX terms from packages that never reach the artifact. Derive the real set from what the built wheel contains, not from what the sdist carries:
src/js/package.json โ dependencies ship; devDependencies (eslint, prettier, typescript, @types/*) do not.package.json dependencies.@pyscript/core/src/3rd-party-licenses/) โ include that directory rather than re-deriving it.Confirm against unzip -l <upstream>.whl | grep static/ โ only paths present there need licenses. Live: reactpy-django 6.0.0b1 (2026-07-14) declared BSD-2-Clause AND Apache-2.0 AND MIT AND EPL-2.0 AND BSD-3-Clause over ~200 enumerated node_modules LICENSE files; the artifact actually ships only @pyscript/core (Apache-2.0), morphdom (MIT), and @reactpy/client (MIT, inlining preact + event-to-object + json-pointer, all MIT). Correct answer: MIT AND Apache-2.0 over 8 entries. The spurious EPL-2.0 / BSD-* came purely from dev-only tooling.
Why this matters: the conda-forge web-service review accepts any of the three patterns, but reviewers occasionally flag (3) ("can this be simplified?"). (1) is invisible; (2) reads as deliberate and gets a free conversational checkpoint with reviewers ("ship LICENSE in-recipe because upstream archive omits it").
Apache-2.0 NOTICE files. If the upstream source ships a NOTICE file, Apache-2.0 ยง4(d) requires it to be redistributed alongside the license. List both in license_file as a YAML list:
about:
license: Apache-2.0
license_file:
- LICENSE
- NOTICE
When you pull a missing LICENSE from GitHub under pattern (2)/(3), grab the NOTICE in the same step โ reviewers will ask for it. If upstream has no NOTICE, confirm by listing the repo tree (gh api repos/<org>/<repo>/git/trees/HEAD) rather than assuming its absence.
A LICENSE-less sdist is an upstream bug โ fix it there too. Shipping the LICENSE in-recipe (pattern 2) is the local workaround; the root cause is the published sdist not packaging it. The concrete upstream fix to propose (PEP 639) is to add to pyproject.toml:
[project]
license-files = ["LICENSE", "NOTICE"]
(requires setuptools >=77 when setuptools is the build backend). Open an upstream issue/PR with that change and link it in the recipe comment next to the in-recipe LICENSE โ it gives reviewers the provenance and a path to drop the workaround once upstream republishes.
Every new Rust CLI recipe must adopt the canonical pattern documented at conda-forge.org/docs/maintainer/example_recipes/rust and used by 17/17 CLI Rust recipes merged to staged-recipes in AprโMay 2026. The pattern is not optional โ reviewers will request these five elements:
Source-of-truth note:
rattler-build's own tutorial at https://rattler-build.prefix.dev/latest/tutorials/rust/ shows plaincargo install --locked --bins(noauditable, no--no-track). That tutorial describes the tool, not the conda-forge style overlay. When the two diverge, conda-forge docs + the merged-PR sample (17/17) win for staged-recipes submissions.
cargo auditable install (not plain cargo install) โ embeds dependency SBOM into the binary.--locked --no-track --bins flags โ reproducible, no .crates.toml in prefix, binary-only install.--root ${{ PREFIX }} on unix, --root %LIBRARY_PREFIX% on win.script.env + script.content shape โ set CARGO_PROFILE_RELEASE_STRIP: symbols and CARGO_PROFILE_RELEASE_LTO: fat via the env map, then list commands under content:. Avoids the G1 "exports don't carry across script entries" trap.cargo-bundle-licenses and cargo-auditable in build deps โ both packages are required, not interchangeable.Canonical CLI skeleton (see templates/rust/cli-recipe.yaml for the full template):
build:
number: 0
script:
env:
CARGO_PROFILE_RELEASE_STRIP: symbols
CARGO_PROFILE_RELEASE_LTO: fat
content:
- if: unix
then:
- cargo auditable install --locked --no-track --bins --root ${{ PREFIX }} --path .
else:
- cargo auditable install --locked --no-track --bins --root %LIBRARY_PREFIX% --path .
- cargo-bundle-licenses --format yaml --output ./THIRDPARTY.yml
requirements:
build:
- ${{ stdlib('c') }}
- ${{ compiler('c') }}
- ${{ compiler('rust') }}
- cargo-bundle-licenses
- cargo-auditable
Exceptions (different patterns are correct for these โ do not force the CLI pattern):
${{ PYTHON }} -m pip install . --no-deps --no-build-isolation (the build is driven by maturin via pip; cargo install doesn't apply). Template: templates/python/maturin-recipe.yaml.cargo install; build with cargo build --release and copy artifacts. Template: templates/rust/library-recipe.yaml.When asked to create or update a recipe, execute these steps in order. Each step has a success criterion โ do not advance until it is met.
generate_recipe_from_pypi(package_name="<name>")recipe.yaml created in recipes/<name>/Skills: [
spec-driven-development] โ define requirements and "Not Doing" list; [source-driven-development] โ verify PyPI metadata against official docs; [idea-refine] โ for vague requests, clarify scope first.
1b. Feedstock-aware enrichment (when an existing <name>-feedstock exists) โ get_feedstock_context(pkg_name="<name>") then enrich_from_feedstock(recipe_path="recipes/<name>/recipe.yaml")
- Success: recipe.yaml gains feedstock-curated maintainers + about.* fields; agent surfaces issue context to user as planning input.
- When to run: any package generation where lookup_feedstock(pkg_name) returns exists=True. For a brand-new package (no feedstock yet), enrich_from_feedstock still adds rxm7706 to maintainers (idempotent) โ running it is harmless either way.
- What carries over (3a maintainers + 3b metadata): existing maintainers (union with rxm7706), recipe-maintainers-emeritus, feedstock-name, hand-curated about.homepage/repository/documentation/description/license_file. v0 meta.yaml field names (home/dev_url/doc_url) are auto-translated to v1 (homepage/repository/documentation).
- What never carries over: requirements.host/run/build (grayskull always wins โ upstream-driven, freshness matters), source URLs/sha256, build script, tests.
- Hard abort: if about.license differs between generated and feedstock (e.g. relicense, or grayskull misread), enrich_from_feedstock aborts with abort_reason set rather than silently picking a side. Surface to user; don't fix without consultation.
- Issue context (3c): get_feedstock_context returns open + last 10 closed issues. Skim for known build failures (look for bug labels, recent recurring titles), linked PRs that already attempted fixes, and any maintainer notes. Mention relevant findings in your plan; never auto-apply suggestions from issues.
- > Skills: [source-driven-development] โ feedstock recipe is canonical for what already worked; [context-engineering] โ open-issue surface gives the agent prior-art for free.
Validate โ validate_recipe(recipe_path="recipes/<name>")
rattler-build lint when available โ treat all warnings as failuresSkills: [
test-driven-development] โ treat each validation failure as a failing test; fix before advancing; [code-review-and-quality] โ evaluate correctness and architecture axes.
Edit & Refine โ edit_recipe(...) for maintainer, SHA256, version, deps
validate_recipe still passesedit_recipe call per logical change. Re-validate after structural edits.Skills: [
incremental-implementation] โ one change at a time; re-validate after each; [code-simplification] โ remove redundant selectors, unnecessary pins.
Security Scan โ scan_for_vulnerabilities(recipe_path="recipes/<name>")
Skills: [
security-and-hardening] โ Always/Ask First/Never Do boundaries for CVE resolution.
Optimize โ optimize_recipe(recipe_path="recipes/<name>")
Skills: [
code-review-and-quality] โ evaluate across five quality axes; [performance-optimization] โ prefernoarch: pythonto reduce build matrix size.
Check Dependencies โ check_dependencies(recipe_path="recipes/<name>")
host/run deps resolve on conda-forge (or the target channel)Skills: [
ci-cd-and-automation] โ shift-left: catch failures before the expensive build step.
7a. Native build (mandatory) โ trigger_build(mode="native", recipe="recipes/<name>")
or pixi run -e local-recipes recipe-build recipes/<name>
- Runs rattler-build build directly on the host. Auto-detects the platform from uname -ms. Layers conda-forge-pinning's conda_build_config.yaml over .ci_support/<platform>.yaml so ${{ python_min }} resolves without context declaration (matches upstream CI behavior โ see ยง Recipe Authoring Gotchas G2/G3).
- Success: build starts (async); get_build_summary() shows status: running then status: success. Artifact lands under build_artifacts/<config>/.
- All gates (steps 2โ6) must pass before reaching this step โ no exceptions
- noarch: python recipes: one host build covers all platforms; do not iterate platforms here.
- Compiled recipes: the host build verifies recipe correctness on one platform. Non-host platforms are deferred to step 7b, which is opt-in.
- > Skills: [ci-cd-and-automation] โ no gate can be skipped; [planning-and-task-breakdown] โ checkpoint here.
7b. Docker build (opt-in, user-authorized) โ trigger_build(mode="docker", config="linux64")
or pixi run -e local-recipes recipe-build-docker linux64
- Runs python build-locally.py <config> for full conda-forge CI parity (alma9 sysroot, isolated env, full bot toolchain). Requires Docker daemon access.
- Always opt-in. Never invoke automatically โ only when the user explicitly asks for "Docker build", "CI-parity check", "full platform coverage", or after a non-host platform failure on the staged-recipes PR.
- Use 7b after 7a passes when:
- The recipe is compiled and you want non-host platform verification (osx-64, win-64, linux-aarch64) before submission.
- 7a passes locally but conda-forge CI fails โ Docker reproduces the CI sysroot so you can debug locally.
- Build Failure Protocol distinction: a host build that passes + Docker build that fails points strongly at sysroot/CDT mismatch (the host's glibc differs from cos7/alma9). The host build that fails points at the recipe itself; fix the recipe first before invoking 7b.
get_build_summary() until status is success or failedstatus: successstatus: failed: proceed to Build Failure Protocolget_build_summary occasionally returns status: "unknown" with the message "No build summary found โ build may have crashed" even when the build actually succeeded โ its summary-file detection is brittle. Before trusting "crashed," check build_artifacts/<config>/<subdir>/ for <name>-<version>-*.conda files; if they exist with mtime newer than the build start, the build succeeded. For deeper diagnosis, the per-build log lives at build_artifacts/<config>/bld/rattler-build_<name>_<id>/work/conda_build.log.Skills: [
ci-cd-and-automation] โ a failed build blocks the pipeline; fix before proceeding.
8b. Prepare Submission Branch โ prepare_submission_branch(recipe_name="<name>", dry_run=True) โ verify โ prepare_submission_branch(recipe_name="<name>")
- Success: fork_branch_url returned; branch add-recipe-<name> exists on <your-user>/staged-recipes with the recipe committed; no PR yet
- This is the inspection checkpoint between a green local build and the PR. Open fork_branch_url in a browser, run gh pr diff against an imagined PR, or pull the fork locally and review the diff before authorizing step 9.
- Idempotent โ if the remote branch's tree already matches the local HEAD, the push is skipped (pushed: false in the result). Force-push uses --force-with-lease so a divergent remote errors instead of being clobbered. The result also reports synced_commits (how many commits the fork's main was behind upstream โ useful drift signal).
- Skip when: you're submitting via submit_pr end-to-end and don't need an inspection point (e.g., a trivial recipe re-submission). submit_pr calls this internally; running it standalone first is the human-in-the-loop variant.
- Follow-up commits on an existing PR branch: prepare_submission_branch hardcodes the commit message to "Add recipe for <name>", which is misleading for any commit other than the initial submission (CI fixes, reviewer feedback, version bumps). For follow-ups, fall back to direct git on the local fork checkout โ git -C <fork> checkout <pr-branch>, copy updated files from recipes/<name>/, git add + git commit -m "<descriptive message>" + git push origin <branch>. The fork path is reported in step 8b's dry_run=True result (fork_path).
- Default add-on (auto-emitted): the generator now writes a conda-forge.yml next to recipe.yaml carrying the universal pre-seed (conda_build_tool: rattler-build + conda_install_tool: pixi + the bot block; + the ARM matrix for compiled recipes). It is inert in the staged-recipes PR (build_all.py reads only conda_build_tool) but forwarded into the feedstock on merge (G83) โ pre-seeding bot policy + platform matrix so no post-merge config / platform-expansion PR is needed. Edit it for extras: os_version: { linux_64: alma9 } (newer glibc), noarch_platforms (silences lint_noarch_selectors when requirements.run: carries an if: <platform>/then: selector; see G12), or workflow_settings: { store_build_artifacts: ... } (downloadable .conda from the feedstock's CI โ a no-op in the staged-recipes PR; win-exclude for build-time-HTTPS recipes, G18). Full reference (per-setting audit + the recommended defaults): reference/conda-forge-yml-reference.md. Template: templates/conda-forge-yml/staged-recipes/conda-forge.yml.
- > Skills: [shipping-and-launch] โ staged rollout; [git-workflow-and-versioning] โ branch-then-PR pattern.
submit_pr(recipe_name="<name>", dry_run=True) โ verify โ submit_pr(recipe_name="<name>")pr_url returned; PR opens on conda-forge/staged-recipesdry_run=True first โ it checks gh auth, fork presence, and branch statesubmit_pr's prep phase no-ops (idempotency check) and proceeds straight to opening the PRprepare_submission_branch, re-run trigger_build first. The fork branch should always reflect a verified-green state โ pushing a recipe whose latest edit was never built leaves an unverified surface for conda-forge CI to discover. Same-package extension of G15; cheap insurance.branch + fork_branch_url + a hint to retry just the PR step โ no need to re-pushsubmit_pr targets conda-forge/staged-recipes โ correct for new package submissions. For updates to an existing feedstock (version bumps, dep fixes, CI fixes), the PR target is conda-forge/<name>-feedstock directly, not staged-recipes. lookup_feedstock(pkg_name=<name>) returning exists=True is the signal โ when it does, skip steps 8b + 9 and instead: (a) clone the feedstock, (b) copy recipes/<name>/recipe.yaml over recipe/recipe.yaml, (c) bump build.number (or reset to 0 on version bump), (d) open a PR against the feedstock. Steps 1โ8 (gates + native build) apply identically to both paths.download_pr_artifacts(pr_ref="...") / pixi run -e local-recipes pr-artifacts <pr> to pull the .conda files CI just published into a local file:// mamba channel โ useful for reviewer smoke-tests ("install the artifact, not just read the diff"). Read-only, anonymous; manifest-cached so re-runs are no-ops. See guides/testing-recipes.md ยง "Downloading artifacts from a PR".Skills: [
shipping-and-launch] โ complete pre-submit checklist; [git-workflow-and-versioning] โ atomic commit (feat: add <name> recipe); [documentation-and-adrs] โ PR description must explain WHY.
Retroโspec feed-forward (v8.13.0): when this skill area has a recent retro entry in _bmad-output/projects/<project>/implementation-artifacts/retro-*.md, pre-apply its known-gaps workarounds as Execution checklist items in the new recipe-authoring spec โ do not assume the skill has been patched. The S1+S3 retros' C1 / C2 / R1 workarounds were pre-applied into S3's spec on 2026-06-11; result was zero step-04 PATCH iterations even before v8.13.0 landed the actual generator fixes. Verified pattern; treat retro entries as "expected workarounds until shipped" rather than "shipped fixes available."
When the task is refreshing an existing local recipe (version bump on a feedstock you maintain locally, applying a grayskull-canonical-pattern update, or re-validating an older recipe against current conventions), do not run generate_recipe_from_pypi straight into recipes/<name>/ โ grayskull happily drops critical hand-curated content (C-FFI deps, build workarounds, secondary sources, lint-justified comments). The autotick path (update_recipe) only handles version + sha256; anything structural needs a manual diff.
Use the move-aside + fresh-generate + diff + selective-apply pattern:
mv recipes/<name> recipes/<name>.current โ stash the live recipe.generate_recipe_from_pypi(package_name=<name>, version=<new>) โ grayskull writes a fresh recipe into recipes/<name>/.enrich_from_feedstock(recipe_path=recipes/<name>/recipe.yaml) โ pulls maintainers + curated about-fields from the existing feedstock (idempotent).diff -u recipes/<name>.current/recipe.yaml recipes/<name>/recipe.yaml โ produce the full diff.package.name/source.url, CFEP-25 test triad, PyPI-authoritative about.* metadata, dropped speculative pins). Grayskull and the current canonical patterns are the source of truth.pango/glib that aren't on PyPI, workaround pins documented with comments, pip check test commands, secondary source.url entries, downstream patches).description:, maintainer ordering). Preserve current to minimize churn.glib is genuinely safe.rm -rf recipes/<name> && mv recipes/<name>.current recipes/<name> to restore the live recipe as the base, then apply only the approved corrections via edit_recipe or Edit. Restoring the base avoids accidentally inheriting any grayskull-emitted bug (e.g., the line-folded source URL caveat below) that wasn't on the categorization list.validate_recipe / optimize_recipe / check_dependencies / scan_for_vulnerabilities / build) on the merged result.Known grayskull-path emit drifts to expect during step 4 (do not silently inherit):
source.url line-folded across two lines by ruamel.yaml's default width=80 โ looks like weasyprint-${{ version \n }}.tar.gz. Cosmetic only; recipe still builds. The v8.11.1 line-fold fix landed in edit_recipe but not in the grayskull subprocess emit path. Restore the clean single-line form.python_min: "3.10" emitted into context: even at the conda-forge default floor โ per v8.8.0 design it should be omitted at the default; the fix didn't fully land in the grayskull path. Drop it unless overriding the floor.Special-case categorizations to apply during the 3-bucket walk:
requirements.run: missing a python pin โ ALWAYS a correction to apply. When a test environment doesn't pin python, the solver resolves to the newest available cpython (the conda-forge default), bypassing python_min verification on the lowest-supported version. Symptom in CI logs: the resolution table shows python 3.14.5 ... cp314 for a recipe whose python_min is 3.10. Add python ${{ python_min }}.* to the script-test's requirements.run: (or use the CFEP-25 triad's python_version: [${{ python_min }}.*, "*"] for python-test-blocks). The optimizer's SEL-002 only fires on the python-test-block form; it doesn't flag the missing python pin in a script-test's requirements.run: โ this categorization must be done by hand during the diff walk.requires-python = "<X,>=Y" upper bound โ a correction to apply when migrating from a speculative <4.0. Tightening to upstream's declared maximum (<3.15 for requires-python = "<3.15,>=3.10") matches what the wheel build matrix actually supports. The optimizer's DEP-002 flags this as a hard upper bound in run: โ acknowledge the trade-off in the categorization (upstream-explicit upper bounds are widely accepted by reviewers; the DEP-002 suggestion to move it to run_constrained is for speculative upper bounds, not upstream-declared ones).run: deps are LOAD-BEARING when pip_check: true is in the test block. pip_check reads each installed package's wheel-bundled dist-info/METADATA and enforces every dep's declared constraint per upstream's pyproject.toml. If the recipe's run: declares only lower bounds (tree-sitter-julia >=0.23), the conda solver picks the newest-compatible version (0.25.0), and pip check then sees upstream's tree-sitter-julia<0.25,>=0.23 constraint inside the installed graphifyy wheel, compares against installed 0.25.0, and fails the test. The CI failure is loud; the silent failure is worse โ a maintainer who disables pip_check to "fix" it ships an install where pip-level deps disagree, and users hit ABI / API break at runtime instead. Decision rule: when dropping upper bounds, drop them only if they were speculative (added by an over-cautious generator). If they came from upstream's pyproject.toml [project.dependencies], MIRROR them in the recipe's run:. bot.run_deps_from_wheel: true keeps these synced on autotick bumps with near-zero maintenance cost. Caught empirically on conda-forge/graphifyy-feedstock#8 Wave F (Jun 2026) โ lower-bound-only recipe shipped, pip_check failed in CI, restored upper bounds across all 26 tree-sitter-* run-deps.pyproject.toml advanced โ a correction to apply, but check the dep's feedstock state first. When upstream has bumped a transitive dep's lower bound (e.g. ag-ui-a2ui-toolkit>=0.0.2 โ >=0.0.3) and the new version isn't on conda-forge yet, the fix becomes 2-step: bump the prerequisite feedstock first, then the consumer. For local verification, build the prerequisite locally so v8.2.0's local-channel auto-inject can resolve it. See G15 for the rebuild-hash-mismatch trap that surfaces during cross-package local fixes.Skills: [
code-review-and-quality] โ categorize each hunk by axis (correctness/standards/style); [source-driven-development] โ PyPI'sproject_urlsis authoritative forabout.*, the existing recipe is authoritative for system-lib deps grayskull can't see; [incremental-implementation] โ restore-then-apply, not generate-then-merge.
The skill carries a SQLite-backed cross-channel package map at
.claude/data/conda-forge-expert/cf_atlas.db, populated by 15 pipeline
phases (B โ N) and exposed through 17 CLIs + 12 MCP tools. The atlas is
offline-safe for all read paths once built; only Phase G's fresh
vuln data needs the heavy vuln-db env (cached counts work everywhere).
Schema v21 (v8.0.0) ships a v_actionable_packages view encoding the
canonical persona-filter triplet conda_name IS NOT NULL AND latest_status='active' AND COALESCE(feedstock_archived,0)=0. Seven
phase selectors read from the view, and a structural-enforcement
meta-test (tests/meta/test_actionable_scope.py) prevents future drift.
Phase H's eligible-rows gate is also serial-aware in v21 โ warm-daily
Phase H drops from ~5 min to ~30 s on a typical day.
Schema v25 (v8.6.0) adds vulnerability-intelligence overlays
beyond Critical/High/KEV counts: vuln_max_epss_score +
vuln_max_epss_percentile (FIRST.org EPSS โ exploitation probability)
vuln_cwe_top + vuln_cwe_categories_json (MITRE CWE Research
Concepts mapped to 7 cf_atlas categories: RCE / DoS / Info-Disclosure
/ Auth-Bypass / Memory-Safety / Traversal / Injection / Other) on
packages + package_version_vulns. Three new side tables:
cisa_kev (v23, populated by fetch-cisa-kev), epss_scores (v24,
fetch-epss), cwe_categories (v24, fetch-cwe-catalog). Phase G +
Phase G' overlay loops OR all three signals onto vdb's per-CVE
output via shared _aggregate_v8_6_0_overlays. Operators can triage
by exploitation probability (staleness-report --by-epss), by attack
category (staleness-report --has-cwe RCE), or by threshold
(cve-watcher --epss-threshold 0.7). v8.6.0 dropped the planned
Phase T (blint hardening) + Phase U (EPSS overlay phase) +
withdrawn-filter scope after verification โ see CHANGELOG v8.6.0
narrative.# Recommended (v8.0.0+) โ persona-aware preset bundle. `maintainer` is
# the documented default for the most common operator.
pixi run -e local-recipes bootstrap-data --profile maintainer
pixi run -e local-recipes bootstrap-data --profile admin # channel-wide
pixi run -e local-recipes bootstrap-data --profile consumer # air-gapped
# Legacy invocations still work (explicit env wins over profile):
pixi run -e local-recipes build-cf-atlas # default phases
PHASE_E_ENABLED=1 pixi run -e local-recipes build-cf-atlas # incl. cf-graph
PHASE_N_ENABLED=1 PHASE_N_MAINTAINER=rxm7706 ... # add live GitHub data
See reference/atlas-phases-overview.md ยง "Profile Reference (v8.0.0)"
for the full per-phase profile matrix and auto-detection details.
--json)| CLI | What it answers |
|---|---|
detail-cf-atlas <pkg> |
Full health card โ version, downloads, vulns, upstream comparison, deps, bot/CI/issue status |
staleness-report [--maintainer X] [--by-risk] [--has-vulns] [--bot-stuck] |
Triage queue: stalest / riskiest / stuck-bot feedstocks |
feedstock-health [--filter stuck|bad|open-pr|ci-red|open-issues|open-prs-human|all] |
Maintenance dashboard |
whodepends <pkg> [--reverse] [--type build|host|run|test] |
Dependency graph queries (Phase J) |
behind-upstream [--maintainer X] |
Multi-source upstream-of-record comparison |
cve-watcher [--since-days N] [--severity C|H|K|T] |
CVE-count delta vs prior snapshot |
version-downloads <pkg> [--by-downloads] |
Per-version adoption (Phase I) |
release-cadence [--package <pkg>] [--maintainer X] |
Trend classifier (accelerating/stable/decelerating/silent) |
find-alternative <archived-pkg> |
Suggest healthier replacements |
adoption-stage [--package <pkg>] [--maintainer X] |
Lifecycle classifier (bleeding-edge/stable/mature/declining/silent) |
platform-breakdown <pkg> | --top N --platform P | --feedstock-roundup --maintainer X |
Per-platform 90d download breakdown (Phase F+ Wave 2 data); maintainer triage for ARM/win-64/aarch64 share questions |
pyver-breakdown <pkg> | --policy-check [--maintainer X] [--threshold-pct N] |
Per-Python download breakdown; --policy-check (headline value) compares declared python_min against the empirical floor and flags bump-safe candidates, sorted bump-safe โ aligned โ aggressive |
channel-split <pkg> | --defaults-share-min N --top M | --migration-checklist --maintainer X |
Per-channel 90d download breakdown (conda-forge / defaults / bioconda / pytorch / ...); --migration-checklist emits paste-into-GitHub-issue markdown for defaults-heavy packages |
scan-project [PATH | --image <ref> | --sbom-in <file> | --conda-env <path> | --venv <path> | --helm-chart <path> | --kustomize <dir> | --argo-app <file> | --flux-cr <file>] [--license-check] [--sbom cyclonedx] |
Unified scanner โ manifests, container images, SBOMs (8+ formats), live envs, GitOps |
export-purls [--out-dir DIR] [--json] |
The six purl + mapping artifacts (conda / versioned / pypi-universe purls, condaโpypi TSV, recipe exceptions, upstream TSV) โ regenerate after every atlas rebuild (cyclonedx-universe-inventory S1) |
mapping-gap [--write] [--limit N] [--json] |
Classify + recover condaโPyPI mapping gaps offline (inverse-G10 vs pypi_universe + corroborators); DRY-RUN by default (S2) |
universe-sbom [--format cyclonedx|spdx] [--actionable-only|--mapped-only|--conda-only|--pypi-only] [--with-vulns] [--allow-stale] |
Full-universe CycloneDX 1.6 / SPDX BOM; mapped pair = ONE conda component; 14-day freshness gate (S3/S4) |
inventory-match [INPUTS | --matches J] [--sbom-in/-out] [--policy F] [--weights F] [--conda-env P] [--venv P] [--json] |
User-inventory gap/version-lag buckets (ADD / ADD-NONPYPI / UPDATE-FEEDSTOCK / UPDATE-PIN / CURRENT / UNKNOWN) + freshness percentile + CI policy gate rc 2 (S5) |
add-handoff [INPUTS | --matches J] [--no-enrich] [--limit N] |
ADD-bucket packaging worklist (readiness, template, fail-closed license blockers); bounded Phase-R-style enrichment BEFORE scoring (S6) |
library-futures [--package NAME | INPUTS] [--weights F] |
2027โ2030 survival composite per package (any ecosystem): futures_score + keep/watch/plan-migration/replace tier, py314 + LTS/EOL horizon signals (S7; CLI/pixi-only) |
recommend-2027 [INPUTS | --matches J] [--sbom-in/-out] [--overrides F] [--weights F] [--json] |
The single 2027โ2030 scorecard: S5โS7, annotated BOM (5 cfe:* properties), overrides shown-never-silent, Phase-P refresh offered-never-run (S8) |
lts-registry-gap [--json] [--limit N] [--out F] [--products-file F] |
Propose lts-registry.yaml entries by diffing endoflife.date's product list against v_actionable_packages (exact/likely tiers, registry-covered names excluded); READ-ONLY โ accept/reject stays with git review (CLI/pixi-only) |
cwe-seed-gap [--json] [--limit N] [--out F] |
Propose cwe_categories_seed.json entries by keyword-classifying MITRE CWEs currently bucketed Other (strong/weak tiers + an Other-bucket package-impact headline); offline, READ-ONLY โ the seed stays hand-curated (CLI/pixi-only) |
spdx-schema-gap [--json] [--limit N] [--out F] [--source-file F] [--drift] |
Propose spdx.schema.json enum additions by diffing the vendored SPDX enum against upstream SPDX, ranked by real package license usage (add-to-schema vs non-standard tiers; --drift for pure staleness); READ-ONLY โ the schema stays hand-curated (CLI/pixi-only) |
license-map-gap [--json] [--limit N] [--out F] |
Propose _LICENSE_TO_SPDX entries by ranking raw PyPI license strings that normalize to nothing (pypi_intelligence.license_raw where license_spdx IS NULL), by package count (likely/report tiers; conservative whole-token SPDX candidate hints); offline, READ-ONLY โ conda_forge_atlas.py never written (CLI/pixi-only) |
The atlas read-tools are wrapped as MCP tools in
.claude/tools/conda_forge_server.py โ including the four
cyclonedx-universe-inventory tools (export_purls, universe_sbom,
inventory_match, recommend_2027; library-futures and add-handoff
are deliberately CLI/pixi-only). See reference/mcp-tools.md for the
full index and selection cheatsheet.
The atlas exists to answer "what should I work on?" / "is this safe?" / "what depends on this?" โ questions the per-recipe authoring workflow can't easily answer. Recommended triggers:
staleness-report --maintainer X --by-riskwhodepends <pkg> --reverse to see blast radiuscve-watcher --maintainer X --severity C --only-increasesadoption-stage + find-alternative to confirm
there's a viable migration pathscan-project --image <prod-image> or
scan-project --sbom-in <cyclonedx.json> for env auditsinventory-match <manifest/lock/SBOM> for the
ADD / UPDATE buckets, then add-handoff for the packaging worklistrecommend-2027 <inventory> โ which of
my libraries (Python or not) survive the window; audit any verdict via
the per-signal breakdownFull reference: reference/atlas-phases-overview.md โ Part A maps
every shipped signal to (persona, goal, action, outcome); Part B is
the phase-indexed view (data source / purpose / what gets written /
actionable intelligence per pipeline stage). Consolidated 2026-07-02
(absorbed atlas-actionable-intelligence.md).
scan-projectFull reference: reference/dependency-input-formats.md โ comprehensive
support matrix for ~30 input formats (manifests, lock files, SBOMs,
container images, live envs, GitOps).
Full reference: reference/atlas-phase-engineering.md โ default rule
book for writing or refactoring conda_forge_atlas.py pipeline phases,
plus ยง 13, the Phase P cost model + operator playbook (absorbed from
atlas-phase-p-cost-model.md, 2026-07-02). Sections cover per-host secondary rate limits, GraphQL batching
vs REST fanout, Retry-After parsing with hard cap + ยฑ25% jitter,
per-registry concurrency caps, atomic file writes, incremental commits
with idempotent SQL, streaming tarfiles from disk, page-level
checkpoints via save_phase_checkpoint, and the <HOST>_BASE_URL
enterprise-routing convention. Consult before adding a new phase or
touching HTTP fanout / batch writes / cache IO in an existing one.
Apply the three-tier system from security-and-hardening to all recipe work:
scan_for_vulnerabilitiessha256 checksums for all source URLs โ never use md5 alonelicense_file when the license is not embedded in the package metadatapytest, coverage, hypothesis) stay out of run# TODO: tighten once X is available comment)get_conda_name first)sha256 values copy-pasted from memory โ always recalculate or fetch from PyPIWhen get_build_summary returns status: failed, invoke the six-step triage from debugging-and-error-recovery:
get_build_summary's error_loganalyze_build_failure(error_log=...) to identify the category:| Category | Examples |
|---|---|
| Missing dependency | Package not on conda-forge; wrong host vs run placement |
| Selector error | Wrong platform guard; recipe.yaml v1 list syntax vs meta.yaml comment |
| Compiler/stdlib | Missing stdlib; wrong ABI; MSVC vs GCC |
| Test failure | Import error; pip check fails; missing test data |
| ENV_ISOLATION | rattler-build v0.62+ strict mode; env vars not explicitly passed |
| Version conflict | Pin too tight; incompatible transitive deps |
| Network/fetch | Source URL changed; checksum mismatch |
edit_recipe to address the underlying issue, never a workaround:skip: true for the platformget_build_summaryMaximum debug loop: 5 iterations. If the build still fails after 5 cycles, stop and report findings to the user with a diagnosis โ don't continue guessing.
When an existing feedstock's CI fails on a test phase (build passed, package built, then test env import/script failed) โ distinct from a build-phase failure โ the failure is almost always a runtime-dep version mismatch between the recipe's declared lower bound and what upstream actually needs. Use this chain:
gh run view <run-id> --repo conda-forge/<name>-feedstock --log-failed | tail -200. Look for ImportError: cannot import name 'X' from 'Y' โ Y is the offending dep, X is the missing symbol.pyproject.toml [project.dependencies] via WebFetch(https://pypi.org/pypi/<name>/json) or by reading the sdist's pyproject.toml directly. Compare upstream's declared lower bound for Y against the recipe's requirements.run: lower bound โ discrepancies are the prime suspect.lookup_feedstock(pkg_name=<Y>). If conda-forge is behind the required version, the fix has a prerequisite: bump <Y>'s feedstock first, then bump the consumer's pin. For local verification, build the prerequisite locally so v8.2.0's local-channel auto-inject can resolve it.Specs: ... ag-ui-langgraph ==0.0.41 pyhcf101f3_0 + python 3.14.5 habeac84_100_cp314 in the resolution table). If the recipe's test requirements.run: lacks a python pin, the solver resolves to the newest available Python rather than python_min โ verification on the lowest-supported version is silently skipped. Add python ${{ python_min }}.* to the test reqs (see Sub-workflow: Updating an existing recipe for the diff-before-apply pattern).Most "ImportError on CI" failures resolve at step 2 once the upstream pyproject is consulted. The chain failed on conda-forge/ag-ui-langgraph-feedstock PR #22 because the recipe pinned ag-ui-a2ui-toolkit >=0.0.2 but upstream's pyproject.toml had been bumped to >=0.0.3 (where the A2UIGuidelines symbol was added) โ a one-line recipe edit, but the diagnosis needed the full chain.
When a full-dependency-graph test-env solve fails but each conflicting sub-cluster solves in isolation on conda-forge, the cause is cross-feedstock pin skew, not a defect in the consumer recipe. Do NOT churn trying to fix it in the consumer.
Signature: the consumer builds GREEN, but the test-env solve reports a version conflict that traces to two or more already-published conda-forge feedstocks with mutually-incompatible pins that neither the consumer nor you can reconcile. Examples:
litellm build baking fastapi==0.124.4 while a consumer needs fastapi>=0.135;otel / traceloop-sdk / openinference-* / langchain) with mutually-incompatible pins across its feedstocks.Diagnosis test: drop the full env and solve each conflicting sub-cluster on conda-forge alone (fastapi + litellm; the otel/traceloop/openinference set; etc.). If each sub-cluster solves fine in isolation but the union does not, and the conflicting constraints come from published feedstock pins you don't control, it's external skew โ resolution requires feedstock-level pin convergence upstream, which is out of scope for the consumer recipe.
Action: do NOT loosen, force, or work around the pins in the consumer recipe (that hides the skew and ships a recipe that can't actually install). Instead:
cfe-forge-blocker-list.cfe-on-conda-forge-status: blocked-pending-prerequisites.Case study: langflow (Jun 19, 2026) โ reached build-GREEN locally, but its full test-env solve was blocked by irreducible cf ecosystem skew across lfx / litellm / traceloop (fastapi + observability-cluster pin conflicts). Each sub-cluster solved in isolation; the union did not. Documented in cfe-forge-blocker-list and stopped โ no consumer churn.
Run this checklist from shipping-and-launch before calling submit_pr:
Recipe Correctness
validate_recipe passes with zero errors or warnings (fast pass; env conda-smithy is 3.62.0)pixi exec --spec "conda-smithy>=2026.6.14" conda-smithy recipe-lint --conda-forge recipes/<name> โ the CURRENT conda-smithy the webservice uses; authoritative, never dismiss its lints (G65)optimize_recipe passes with zero check-code warningscheck_dependencies resolves all deps on conda-forge; for compiled transitive deps, G40 per-subdir repodata check (host-only build won't catch osx/win-only gaps)sha256 verified against PyPI or sourceSecurity
scan_for_vulnerabilities clean, or all findings documentedrun requirementsBuild
linux-64pip check passes (for Python packages)Standards
python_min >= "3.11" for noarch: python recipes (or, canonically, absent from context: โ Rule 6)stdlib included for all compiled recipeslicense_file field present when requiredrxm7706) listed in recipe.maintainersschema_version: 1 for new recipes (recipe.yaml v1 format)Submission
prepare_submission_branch(dry_run=True) passes; then real call lands branch on <your-user>/staged-recipes and fork_branch_url was inspectedcfe-* keys or # CFE blocks in the PUSHED recipe.yaml AND conda-forge.yml โ grep-verified on the fork branch (both files; the generated conda-forge.yml carries a strippable #### CFE block too), not assumed (G62)conda-forge.yml present (auto-emitted by the generator โ the universal pre-seed; G83). store_build_artifacts belongs under workflow_settings: (not the deprecated azure.*) and is a no-op in the staged-recipes PR โ conda_pkgs_* artifacts publish unconditionally anywaysubmit_pr(dry_run=True) passes all prerequisite checks.github/pull_request_template.md; a custom --body REPLACES the template (G63)@conda-forge/help-python for pure-Python noarch) โ but check first and skip if the PR is a draft or already has the review-requested label (G64)Apply the deprecation-and-migration framework when converting v0 to v1 format.
Migrate when:
if/then/else, typed selectors, improved noarch)Do not migrate if:
Local-mirror fidelity โ keep the feedstock's meta.yaml until the feedstock itself migrates. For a recipes/<name>/ that mirrors an existing conda-forge feedstock still on v0: pull the latest meta.yaml from the feedstock and KEEP it, AND author the v1 recipe.yaml alongside it (our local v1 โ built/tested locally, and the proposed migration). Both files coexist in recipes/<name>/: meta.yaml faithfully mirrors what's actually deployed; recipe.yaml is the v1 we maintain. Delete meta.yaml only after the feedstock itself completes the v0โv1 switch (its migration PR merges) โ NOT when the local build succeeds. (Leave meta.yaml in place โ build / test / lint / optimize by pointing explicitly at recipe.yaml (rattler-build --recipe recipes/<name>/recipe.yaml), which reads only that file and ignores meta.yaml. No stashing. optimize_recipe's STD-002 "both meta.yaml and recipe.yaml present" is an expected, harmless warning for a v0-mirror โ not a defect; don't delete meta.yaml to silence it.) The CFE metadata lives in the recipe.yaml (the meta.yaml stays a faithful, un-annotated upstream copy): for a still-v0 feedstock mirror the recipe.yaml carries cfe-on-conda-forge-status: confirmed-on-conda-forge (it IS published, just on v0), cfe-forge-recipe-updates-needed: including meta-yaml-to-recipe-yaml (the feedstock owes the migration), and a correct cfe-forge-blocker-list:. New recipes with no feedstock yet are v1 recipe.yaml only โ nothing to mirror.
migrate_to_v1(recipe_path="recipes/<name>") โ creates recipe.yaml, preserves meta.yamlvalidate_recipe on the new recipe.yaml โ fix all errors before proceedingoptimize_recipe โ fix all check-code warningstrigger_build โ verify a clean build with the new formatmeta.yaml only after step 4 succeeds โ never beforecheck_dependencies still resolves all depsChurn Rule: You own verifying the migration is complete. A meta.yaml left alongside a recipe.yaml after a successful build is a bug โ clean it up. Exception: a local mirror of a feedstock still on v0 deliberately keeps BOTH (meta.yaml = deployed state, recipe.yaml = our v1) until the feedstock migrates โ see ยง When to Migrate's local-mirror fidelity rule.
Five points learned the hard way from feedstock v0โv1 migrations. Full expansion in guides/migration.md ยง "Migration Discipline".
package.name: MUST match the feedstock identity, not the local folder name. When migrating an existing feedstock, the conda-forge package on the channel is the canonical name (e.g. feedstock python-confluent-kafka-feedstock ships package python-confluent-kafka). The local-recipes folder often mirrors the upstream GitHub repo name (e.g. recipes/confluent-kafka-python/ after the GitHub repo). The v1 package.name: MUST match the feedstock's existing package name โ changing it during migration creates a parallel package and orphans existing users. Verify via lookup_feedstock(pkg_name=<conda-forge-name>) before pushing the fork branch.
Bump build.number on same-version v0โv1 swap. When the migration ships against the same upstream version that's currently shipping (no version bump), build.number MUST increment (typically 0 โ 1). This keeps existing <pkg> <ver> *_0 artifacts on a distinct hash from the v1-built <pkg> <ver> *_1 and lets the conda solver pick the newer build. If the migration is bundled with an upstream version bump, the build number resets to 0 โ the version bump itself provides the hash separation.
Stash meta.yaml aside during local validation, don't pre-delete. optimize_recipe fires STD-002 "Both meta.yaml and recipe.yaml exist in the same directory" while both files coexist, and some local-recipes pixi tasks get confused by mixed format. The practical pattern: mv meta.yaml meta.yaml.bak for the validate + build pass, then restore. Drop meta.yaml only when committing to the feedstock fork (where v0 is genuinely retired). Keeping the v0 copy locally is a useful pre-migration reference until the feedstock PR merges; delete from the local mirror at PR-merge time, not earlier.
conda_build_tool: rattler-build MUST ship paired with conda_install_tool: pixi. Adding conda_build_tool: rattler-build to conda-forge.yml is required for CI to use the v1 parser; pair it with conda_install_tool: pixi (the canonical 2026 companion that tells CI to use pixi for env installs, faster than micromamba and matches conda-forge's 2026 standardization). Both keys land in the same PR that drops meta.yaml. The v0โv1 PR also requires @conda-forge-admin, please rerender โ the CI scripts need regenerating to invoke rattler-build instead of conda-build.
pip_check: true on first-time enablement may surface "new" runtime deps. Many older meta.yaml v0 recipes don't have pip check in test:; CFEP-25's v1 triad (and the canonical noarch:python test block) turns it on by default. When migrating, expect to discover transitive deps that upstream's PEP-508 markers introduced post-original-feedstock-creation. Fix by adding the missing deps to requirements.run: (preferring unconditional listing over PEP-508-marker-gated conda-forge selectors for single transitive deps โ minor runtime overhead, simpler recipe). Case: python-confluent-kafka v0โv1 surfaced upstream's typing-extensions ; python_version < "3.11" marker; shipped typing_extensions unconditionally in run:.
3.11Python 3.10 was dropped from the conda-forge build matrix on 2026-09-02; 3.9 went in August 2025. The current build matrix is 3.11, 3.12, 3.13, 3.14 (win-arm64 / linux-riscv64 are 3.14-only). Verify against the installed pinning rather than trusting this line โ see ยง Python Version Floor for the one-line check.
noarch: python Recipe (recipe.yaml v1) โ CFEP-25 TriadNote the canonical form declares no python_min in context: at all (Rule 6) โ
conda-forge-pinning supplies it. The line below is shown only to name the override point:
context:
python_min: "3.12" # ONLY when upstream python_requires demands above the floor
requirements:
host:
- python ${{ python_min }}.*
run:
- python >=${{ python_min }}
tests:
- python:
imports: [mypackage]
pip_check: true
python_version: ${{ python_min }}.*
noarch: python Recipe (meta.yaml v0){% set python_min = "3.11" %}
requirements:
host:
- python {{ python_min }}
run:
- python >={{ python_min }}
test:
requires:
- python {{ python_min }}
Floor is 3.11 โ never set python_min below 3.11 for new recipes. The floor MOVES (3.9 โ 3.10 โ 3.11 as of 2026-09-02); read it from the installed pinning rather than trusting any number written down here.
Raise only when required โ only set python_min above the floor when upstream python_requires explicitly demands a higher version; always verify before raising
Compiled packages โ use python >=3.11 directly; the build matrix handles versioning via the global pin; no python_min variable needed
Existing recipes at or below the floor โ optimize_recipe (SEL-002) flags them, and tests/meta/test_no_redundant_python_min.py fails the suite: delete the python_min line from context: (Rule 6), don't rewrite it to the new floor. On 2026-09-02 that swept 438 recipes at once when the floor moved 3.10 โ 3.11.
Never downgrade below the floor โ will fail conda-forge CI
Recipes do NOT need python_min in context unless overriding the default โ the May 2026 upstream sync removed python_min: '3.10' and python: 3.12.* *_cpython from .ci_support/linux64.yaml and linux_aarch64.yaml, but those defaults still come from conda-forge-pinning (the canonical source upstream CI has always used). Recipes at the default floor can โ and should โ write ${{ python_min }} references throughout the CFEP-25 triad without declaring python_min in context:. Only override in context: when upstream python_requires demands a higher floor.
Local rattler-build implication. When invoking rattler-build directly (outside conda-forge CI), pass conda-forge-pinning as an additional variant config so ${{ python_min }} resolves to its default:
pixi run -e local-recipes rattler-build build \
--recipe recipes/<name>/recipe.yaml \
--variant-config .ci_support/linux64.yaml \
--variant-config .pixi/envs/local-recipes/conda_build_config.yaml
The local-recipes pixi env already installs conda-forge-pinning; its conda_build_config.yaml lands at .pixi/envs/local-recipes/conda_build_config.yaml. Without the second --variant-config, recipes referencing ${{ python_min }} will fail to render with Template rendering failed: undefined value.
# yaml-language-server: $schema=https://raw.githubusercontent.com/prefix-dev/recipe-format/main/schema.json
schema_version: 1
context:
version: "1.0.0"
# Omit python_min when the conda-forge floor (3.11) is fine; only declare
# when upstream's python_requires demands a higher floor.
package:
name: my-package
version: ${{ version }}
source:
# Path segments are literal; only ${{ version }} interpolates.
url: https://pypi.org/packages/source/m/my-package/my-package-${{ version }}.tar.gz
sha256: <sha256>
build:
noarch: python
script: pip install . --no-deps --no-build-isolation
requirements:
host:
- python ${{ python_min }}.*
- pip
run:
- python >=${{ python_min }}
tests:
- python:
imports: [my_package]
pip_check: true
python_version:
- ${{ python_min }}.*
- "*"
about:
license: MIT
license_file: LICENSE
summary: Short description
extra:
recipe-maintainers:
- rxm7706
{% set name = "my-package" %}
{% set version = "1.0.0" %}
package:
name: {{ name|lower }}
version: {{ version }}
source:
url: https://pypi.org/packages/source/{{ name[0] }}/{{ name }}/{{ name }}-{{ version }}.tar.gz
sha256: <sha256>
build:
noarch: python
script: {{ PYTHON }} -m pip install . --no-deps --no-build-isolation
number: 0
requirements:
host:
- python
- pip
run:
- python >=3.11
test:
imports:
- {{ name }}
commands:
- pip check
requires:
- pip
about:
license: MIT
license_file: LICENSE
summary: Short description
extra:
recipe-maintainers:
- rxm7706
| Tool | Description | Example |
|---|---|---|
generate_recipe_from_pypi |
Creates a new recipe from a PyPI package via grayskull, with conda-forge post-processing (rewrites tests[].python.python_version to the [python_min, "*"] list form per staged-recipes#32857 r3039190932) |
generate_recipe_from_pypi(package_name="numpy") |
generate_recipe_from_npm |
Canonical npm pattern (npm pack + npm install --global + pnpm-licenses). Handles scoped packages (@openai/codex โ conda name codex). Flags: --source-mode {npm,github,auto}, --prepare-fix, --test-mode, --inline-build, --with-build-bat, --no-bin-links, --no-third-party-licenses, --validate. Pixi: pixi run -e local-recipes generate-npm -- husky |
recipe-generator.py npm husky |
generate_recipe_from_cran |
R package from CRAN via rattler-build | recipe-generator.py cran ggplot2 |
generate_recipe_from_cpan |
Perl package from CPAN via rattler-build | recipe-generator.py cpan Moose |
generate_recipe_from_luarocks |
Lua package via rattler-build | recipe-generator.py luarocks lua-cjson |
edit_recipe |
Primary editing tool. Four supported actions (per scripts/recipe_editor.py): update (set or insert a scalar at any path; set_nested_item uses setdefault so intermediate keys are created and a missing leaf is added cleanly), add_to_list (append item to an existing list at path), remove_from_list (remove item from an existing list at path), calculate_hash (refresh sha256 in a source: block). For arbitrary key deletions OR multi-line block-scalar inserts (e.g., a description: | body), fall back to direct Edit. There is no add action โ SKILL.md v8.11.1 incorrectly listed it; corrected in v8.13.0 after empirical retest. |
edit_recipe('recipes/numpy/recipe.yaml', [{"action": "update", "path": "about.description", "value": "Short text"}]) (insert), [{"action": "add_to_list", "path": "about.license_file", "value": "LICENSE.txt"}] (append) |
get_conda_name |
Resolves a PyPI package name to its conda-forge equivalent (cache-first) | get_conda_name(pypi_name="python-dateutil") |
| Tool | Description | Example |
|---|---|---|
validate_recipe |
Schema, license, checksums + rattler-build lint pass |
validate_recipe(recipe_path="recipes/numpy") |
check_dependencies |
Verifies all deps exist on conda-forge. Batch repodata.json โ fast, air-gapped-friendly, JFrog Artifactory-compatible | check_dependencies(recipe_path="recipes/numpy") |
optimize_recipe |
18 check codes โ critical (STD-001: compiler without stdlib; STD-002: format mixing; SCHEMA-001: missing v1 schema header), security (SEC-001: no sha256), completeness (MAINT-001: no maintainers; TEST-001: no tests; TEST-002: noarch:python tests pinned to a single Python version instead of [python_min, "*"] (staged-recipes#32857 r3039190932); TEST-003: package_contents substituted for python.imports without justification; ABT-001: no license_file; ABT-002: v0 about-fields in v1 recipe; LIC-001: secondary-source LICENSE pattern (3) detected, convert to in-recipe pattern (2) [v8.12.0]), formatting (FMT-001: list items indented at parent-key depth instead of 2 spaces deeper [v8.12.0]), quality (DEP-001/002, PIN-001, SCRIPT-001/002, SEL-001/002/003) |
optimize_recipe(recipe_path="recipes/numpy") |
| Tool | Description | Example |
|---|---|---|
trigger_build |
Starts a build asynchronously | trigger_build(config="linux-64") |
get_build_summary |
Polls build status; returns success/failure + artifacts + error log | get_build_summary() |
analyze_build_failure |
Diagnoses root cause from error log. 42 patterns across 13 categories: MISSING_DEPENDENCY, HASH_MISMATCH, COMPILER_ERROR, LINKER_ERROR, STDLIB_MISSING, TEST_FAILURE, ENV_ISOLATION (network/sysroot), BUILD_TOOLS (meson/autotools), MSVC, MACOS_SDK, RESOURCE, PYTEST | analyze_build_failure(error_log=summary['error_log']) |
| Tool | Description | Example |
|---|---|---|
scan_for_vulnerabilities |
Scans deps against OSV.dev API (primary) + local CVE database (offline fallback) | scan_for_vulnerabilities(recipe_path="recipes/numpy") |
update_cve_database |
Updates local CVE database from osv.dev | update_cve_database(force=True) |
update_recipe |
Autotick Bot (PyPI). Checks for new upstream versions and updates recipe | update_recipe(recipe_path="recipes/numpy/recipe.yaml") |
update_recipe_from_github |
Autotick Bot (GitHub). Fetches latest release + updates version + SHA256. Always dry_run=True first |
update_recipe_from_github(recipe_path="recipes/apple-fm-sdk", dry_run=True) |
update_recipe_from_npm |
Autotick Bot (npm). Auto-detects the npm name from source.url; handles scoped packages; skips pre-releases by default. Pixi: pixi run -e local-recipes autotick-npm -- recipes/husky --dry-run |
npm_updater.py recipes/husky --dry-run |
check_github_version |
Read-only GitHub version check โ returns latest tag without modifying | check_github_version(recipe_path="recipes/apple-fm-sdk") |
update_mapping_cache |
Updates PyPI-to-Conda name mapping cache from Grayskull. Run when get_conda_name misses |
update_mapping_cache(force=True) |
migrate_to_v1 |
meta.yaml โ recipe.yaml. Converts v0 to v1 via feedrattler. Preserves original. | migrate_to_v1(recipe_path="recipes/numpy") |
prepare_submission_branch |
Step 8b. Stage the recipe on a branch in your staged-recipes fork โ no PR yet. Idempotent (--force-with-lease); reports synced_commits (fork drift). Use as inspection checkpoint before authorizing submit_pr. |
prepare_submission_branch(recipe_name="numpy", dry_run=True) |
submit_pr |
Step 9. Calls prepare_submission_branch then opens the PR against conda-forge/staged-recipes. Idempotent prep โ when step 8b already pushed, the prep no-ops and only the PR is created. Always dry_run=True first. |
submit_pr(recipe_name="numpy", dry_run=True) |
run_system_health_check |
Full diagnostic on the development environment | run_system_health_check() |
All 21 skills from addyosmani/agent-skills are installed at .claude/skills/ alongside this skill.
| Lifecycle Phase | Skills |
|---|---|
| Define (new package) | idea-refine, spec-driven-development |
| Plan | planning-and-task-breakdown, incremental-implementation |
| Generate + Edit (steps 1โ3) | source-driven-development, context-engineering, code-simplification |
| Validate + Optimize (steps 2, 5) | test-driven-development, code-review-and-quality, performance-optimization |
| Security Scan (step 4) | security-and-hardening |
| Dep Check + Build (steps 6โ8) | ci-cd-and-automation, debugging-and-error-recovery |
| Submit PR (step 9) | git-workflow-and-versioning, shipping-and-launch, documentation-and-adrs |
Migration (migrate_to_v1) |
deprecation-and-migration |
| Cross-cutting | context-engineering, using-agent-skills |
| Peripheral | frontend-ui-engineering, browser-testing-with-devtools, api-and-interface-design |
Use using-agent-skills as the meta-skill to select the right combination for any task.
Sourced from conda-forge.org/docs/maintainer/infrastructure/ and conda-forge news (Apr 2026).
Defaults below are conda-smithy's DEFAULT_PROVIDERS / DEFAULT_PLATFORMS (conda_smithy.configure_feedstock, read 2026-09-03). They apply at rerender time โ a feedstock not rerendered since it was created keeps whatever its .ci_support/pipelines were rendered with, which for pre-2026 feedstocks means Azure everywhere.
| Platform | Default CI Provider | Notes |
|---|---|---|
| Linux x86_64 | GitHub Actions | Was Azure until 2026; GA opt-in first arrived with conda-smithy 3.57.1 (March 2026), then became the default |
| Linux aarch64 (ARM) | GitHub Actions (native arm64 runners) | Cross-compile on linux_64 still available via build_platform |
| Linux ppc64le / s390x | GitHub Actions (emulated) | Travis is the native-runner provider |
| macOS x86_64 | Azure Pipelines | macOS-15 (Intel) image |
| macOS arm64 | Azure Pipelines | Native macOS-15-arm64 image when build_platform is left at the identity default; not enabled by default โ opt in per feedstock with provider: {osx_arm64: default} (G82) |
| Windows x86_64 | GitHub Actions | Was Azure; per current DEFAULT_PROVIDERS |
| Windows ARM64 | GitHub Actions | Python 3.14 cross-builds added 2025 |
| Rerendering / automerge | GitHub Actions | Always โ CI config updates and bot services |
provider: is the per-leg override โ move a leg to another provider, or enable a non-default platform:
# conda-forge.yml
provider:
linux_64: azure # move the Linux leg back to Azure (e.g. GA concurrency limits)
osx_arm64: default # enable the native Apple-Silicon leg (not on by default)
Build time limit: 6 hours on Azure Pipelines (and matching limits on GitHub Actions). Builds exceeding this are killed with no artifacts.
Set via os_version in conda-forge.yml:
| Value | OS | Notes |
|---|---|---|
cos7 |
CentOS 7 | Default; glibc 2.17 |
alma8 |
AlmaLinux 8 | glibc 2.28 |
alma9 |
AlmaLinux 9 | glibc 2.34 (current recommended) |
ubi8 |
Red Hat UBI 8 | Enterprise use |
alma10 / rocky10 |
Next-gen | glibc 2.38+ |
# conda-forge.yml โ request a newer glibc
os_version:
linux_64: alma9
These are resolved by the compiler() macro โ never pin manually:
| Platform | C | C++ | Fortran |
|---|---|---|---|
| Linux | GCC 14 | G++ 14 | GFortran 14 |
| macOS | Clang 19 | Clang++ 19 | GFortran 14 |
| Windows | MSVC 2022 | MSVC 2022 | Flang |
Deprecated: compiler_stack key in conda-forge.yml โ remove it from any existing feedstock configs.
@conda-forge-admin, please rerender
@conda-forge-admin, please restart ci
@conda-forge-admin, please lint
@conda-forge-admin, please close
@conda-forge-admin, please add user @username
# conda-forge.yml โ exclude erroneous upstream tags
bot:
automerge: true
inspection: hint-all
version_updates:
exclude:
- "1.0.0rc1" # Skip pre-releases the bot incorrectly picks up
conda-forge packages are immutable โ versions are never edited or deleted after upload. To fix a broken release:
conda-forge-admin to mark the package as broken (not deleted)| Channel | Status | Use For |
|---|---|---|
Zulip (conda-forge.zulipchat.com) |
Primary | Real-time questions, troubleshooting, announcements |
| GitHub Discussions / Issues | Active | Recipe bugs, feedstock-specific issues |
Discourse (conda.discourse.group) |
Read-only since Oct 15, 2025 | Search archive only โ do not post |
| Gitter | Decommissioned | Replaced by Zulip |
When asking for help, link the failing PR/feedstock and the rendered build log โ never paste large logs into chat.
Recent conda-forge changes that affect recipe authoring. Cite the relevant entry in PR descriptions when applying these patterns.
--v3 flag: optional dependency extras: groups, when: conditional dependencies (e.g. scipy [when="python>=3.10"]), and variant-selection flags:. Opt-in only; recipes without --v3 are unaffected.build.sh/build.bat. Each output that needs a script must declare script: <name> explicitly. Top-level (single-output) recipes are unchanged.--env-isolation flag: strict (default; remaps $HOME), conda-build, none. Build scripts that rely on inherited env vars must declare them in build.script.env.--debug flag removed; use the dedicated rattler-build debug subcommand.MACOSX_DEPLOYMENT_TARGET=10.x builds will be rejected./opt/conda-sdks since conda-smithy 3.54.0 (Dec 2025). Local builders must export OSX_SDK_DIR=/opt/conda-sdks.conda install libblas=*=*_newaccelerate.cuda118.yaml to .ci_support/migrations/) no longer works โ that file was removed from staged-recipes upstream. New explicit per-CUDA variant configs landed: .ci_support/linux64_cuda129.yaml + linux64_cuda130.yaml. NVIDIA Tegra (linux-aarch64 SOC) builds are supported on CUDA 12.9; CUDA 13.0+ uses SBSA so Tegra-specific builds are unnecessary.osx-arm64 is now a first-class variant (May 2026) โ new .ci_support/osx_arm64.yaml exists alongside osx_64.yaml. Use target_platform: osx-arm64 in recipes that need explicit ARM Mac handling. macOS deployment target remains 11.0.conda-forge/label/mpi-external. Old external MPI packages on main were marked broken.python_min floor for noarch: python recipes; conda-forge floor is "3.10" since Aug 2025 (Python 3.9 dropped).Apache-2.0, not APACHE 2.0); compound licenses use SPDX expressions (Apache-2.0 WITH LLVM-exception).cfe-on-conda-forge-status field and the bottom # CFE comments block (e.g. cfe-status: local-only-pending-submission-conda-forge with a note that the license bars submission), and do not open a staged-recipes PR. Real case: ragstack-ai-knowledge-store (BUSL-1.1, Jun 19, 2026) โ built clean locally but is not cf-eligible.cargo-bundle-licenses / go-licenses and ship a THIRDPARTY.yml alongside LICENSE (already encoded in the maturin and Go templates).Patterns that look right but fail silently or produce broken recipes. Each entry includes the symptom, why it happens, and the correct form. Case studies cited where relevant.
script: list entries run in separate shells โ env vars do NOT carry across entriesSymptom: an export FOO=bar in one script entry has no effect in the next entry. pip install later in the script doesn't see CFLAGS you set earlier.
Why: rattler-build evaluates each top-level item under build.script: (when given as a YAML list) as an independent shell invocation. Shell state โ env vars and function definitions โ does not survive between entries.
Caveat โ CWD specifically does persist across entries. Env vars and shell functions don't carry, but the current working directory does. If entry 1 ends with CWD inside a subdirectory, entry 2 starts there too. See G13 for the cross-platform CWD-isolation pattern (pushd/popd) and the Windows cmd.exe (...) -is-not-a-subshell trap that makes naive bash subshell patterns fail on Windows.
Fix: choose one of three patterns:
# (a) Single multi-line entry โ exports persist into the same shell
build:
script:
- if: unix
then: |
export CFLAGS="${CFLAGS:-} -D_BSD_SOURCE -D_DEFAULT_SOURCE"
cargo-bundle-licenses --format yaml --output THIRDPARTY.yml
${{ PYTHON }} -m pip install . -vv --no-deps --no-build-isolation
# (b) script.env: static map โ works when the value is fixed (no shell expansion)
build:
script:
env:
CARGO_PROFILE_RELEASE_STRIP: symbols
CARGO_PROFILE_RELEASE_LTO: fat
content:
- cargo-bundle-licenses --format yaml --output THIRDPARTY.yml
- ${{ PYTHON }} -m pip install . -vv --no-deps --no-build-isolation
# (c) Dedicated build.sh / build.bat โ for complex logic
build:
script:
file: build.sh # relative to recipe directory
script.env: does NOT shell-expand ${VAR} โ values are treated as literal strings. To append to an existing env var, use pattern (a) or (c).
Case study: cocoindex PR #33231 (May 2026). The recipe needed CFLAGS=-D_BSD_SOURCE -D_DEFAULT_SOURCE to compile tree-sitter under GCC 14 + glibc 2.17 sysroot (le16toh/be16toh implicit declaration). First attempt put export in a separate script entry; it did not reach pip install. Fixed by collapsing into one multi-line then: | block.
Symptom: rattler-build builds the package without warning, but about.dev_url, about.doc_url, or about.home is missing from the resulting metadata. Users see incomplete project links on conda-forge.org.
Why: rattler-build's recipe-format schema only recognizes the v1 names (repository, documentation, homepage). Unknown keys under about: are accepted but discarded โ no schema-validation error.
Fix: use the v1 names in any file with schema_version: 1. Reference: reference/recipe-yaml-reference.md and the v0 โ v1 about-field mapping memory. The optimizer's ABT-002 check flags this in v1 recipes.
| v0 (meta.yaml) | v1 (recipe.yaml) |
|---|---|
home |
homepage |
dev_url |
repository |
doc_url |
documentation |
license_family is NOT removed and NOT a rename. It is a valid v1 about:
field โ the prefix-dev/recipe-format schema lists it (description: "deprecated,
but still used in some recipes") and it keeps the same name in v0 and v1. It
passes both rattler-build schema validation and conda-smithy lint as long as
its value is a recognized family: conda_build.license_family.ensure_valid_license_family
raises only on an unrecognized value (allowed: AGPL, LGPL, GPL, GPL2, GPL3,
BSD, MIT, APACHE, PSF, CC, MOZILLA, PUBLIC-DOMAIN, PROPRIETARY, OTHER, NONE), and
conda-smithy's lint_license_family_should_be_valid fires only when license_file
is absent โ never on the field's presence. For modernization, omit it: it's
deprecated, and neither the CFE generator (recipe-generator.py) nor current
grayskull emits it; dropping a valid one is safe and more modern, keeping a valid
one won't fail lint. The complete v1 about: set is the 9 fields in
reference/recipe-yaml-reference.md. (Earlier
versions of this gotcha wrongly listed license_family as "removed.")
py < N skip selectors do nothing in v1 recipe.yamlSymptom: build.skip: - py < 311 is in the recipe, but conda-forge CI builds Python 3.10 anyway and pip rejects with requires a different Python: 3.10.X not in '>=3.11'.
Why: py < N is conda-build/meta.yaml v0 selector syntax. rattler-build does not auto-inject the integer py variable from the python variant string in staged-recipes-style builds, so the condition evaluates against an undefined symbol and never fires.
Fix: use match(python, "<3.11"). The optimizer's SEL-003 check flags v0-style py < N in v1 recipes.
build:
skip:
- match(python, "<3.11") # CORRECT in v1
# - py < 311 # WRONG โ silently ignored
Case study: cocoindex PR #33231 (May 2026). All three platform builds (linux/osx/win) failed because py < 311 did not skip the Python 3.10 matrix entry.
pip install succeeds, build fails with "No license files were copied"Symptom: validate_recipe and the Rust/wheel build both succeed, but rattler-build then fails at Copying license files with No license files were copied. The PyPI sdist has no LICENSE file at the root.
Why: PEP 517 sdists are not required to include a LICENSE. Some build backends (notably maturin, hatchling with non-default config) emit metadata-rich sdists that ship THIRD_PARTY_NOTICES.html or similar, but not the project's own LICENSE. conda-forge requires the LICENSE to be packaged.
Fix: add a secondary source: block fetching the LICENSE from the upstream GitHub tag. See guides/sdist-missing-license.md for the full pattern.
source:
- url: https://pypi.org/packages/source/${{ name[0] }}/${{ name }}/${{ name }}-${{ version }}.tar.gz
sha256: <sdist hash>
- url: https://raw.githubusercontent.com/<org>/<repo>/v${{ version }}/LICENSE
sha256: <license hash>
file_name: LICENSE
Case study: cocoindex PR #33231 (May 2026). cocoindex's PyPI sdist shipped only THIRD_PARTY_NOTICES.html. Fix added a secondary source: from raw.githubusercontent.com.
src/tree_sitter/*.h headers โ default to GitHub sourceSymptom: native build of a tree-sitter-<lang> recipe sourced from the PyPI sdist fails at the very first compile step with:
src/parser.c:1:10: fatal error: tree_sitter/parser.h: No such file or directory
1 | #include "tree_sitter/parser.h"
| ^~~~~~~~~~~~~~~~~~~~~~
The recipe is otherwise correct โ compiler('c') + stdlib('c') are present, pip install . reaches the build_ext stage, and grayskull happily produced the recipe from the sdist.
Why: many tree-sitter-grammars/* and tree-sitter/* Python bindings ship src/tree_sitter/parser.h, array.h, and alloc.h in the GitHub tag tarball but omit them from the PyPI sdist that their upstream wheel-build pipeline uploads. The wheels published to PyPI work because the headers are in the build container's filesystem; the sdist on its own cannot compile.
The omission is per-release inconsistent โ not a clean version-based cutoff. In staged-recipes PR #33308 (May 2026) the bug hit 8 of 11 PyPI-sourced recipes (v0.23.1, v0.23.2, v0.23.4, v0.23.5, v0.24.2, v0.26.0, v1.1.0, v1.2.0) while three PyPI sources at v0.25.0 and v0.7.2 happened to ship the headers. Don't trust the version; trust the listing.
Fix: default tree-sitter-<lang> recipes to GitHub-tag source rather than PyPI sdist. This matches the conda-forge tree-sitter-python-feedstock pattern and avoids the entire class of bug:
# tree-sitter PyPI sdists inconsistently strip src/tree_sitter/*.h headers
# needed for the C build; default to the GitHub tag (see SKILL.md G5).
source:
url: https://github.com/<org>/${{ name }}/archive/refs/tags/v${{ version }}.tar.gz
sha256: <sha256 of the GitHub tarball>
Common upstream orgs: tree-sitter (most grammars), tree-sitter-grammars (community-maintained: kotlin, luau, ...), alex-pinkus (swift), other forks. Confirm by checking the package's Project URLs / Homepage on PyPI.
How to verify before trusting generate_recipe_from_pypi: list the sdist contents (tar tzf <sdist>.tar.gz | grep 'tree_sitter/parser.h'). If the listing is empty, switch to GitHub source.
Case study: staged-recipes PR #33308 (May 2026) โ 12-recipe bundle for repowise prerequisites. First-pass build had 7 PyPI-sourced recipes fail with this exact error (cpp 0.23.4, java 0.23.5, typescript 0.23.2, ruby 0.23.1, rust 0.24.2, scala 0.26.0, plus luau and kotlin proactively switched ahead of time). All resolved by switching to GitHub source. Three other PyPI sources passed (go 0.25.0, javascript 0.25.0, swift 0.7.2). The same-bundle tree-sitter-php had a different issue โ its GitHub source pyproject.toml had license = "LICENSE", which modern setuptools rejects as an invalid SPDX identifier; fixed via a downstream 0001-fix-invalid-pep621-license-field.patch in the recipe's patches/ directory that rewrites the line to license = "MIT".
node_modules/.bin/ symlinks that fail noarch buildsSymptom: a noarch: generic npm recipe built with the canonical npm pack + npm install --global pattern produces a .conda archive but rattler-build then aborts with:
Error: ร Package <name> contains symlinks which are not supported on most
โ Windows systems:
โ - lib/node_modules/<name>/node_modules/.bin/ejs
โ - lib/node_modules/<name>/node_modules/.bin/jake
โ - lib/node_modules/<name>/node_modules/.bin/semver
โ โฆ
โ To allow symlinks, use the --allow-symlinks-on-windows flag.
Why: npm installs each transitive dependency's executable as a symlink under that dep's node_modules/.bin/. For minimal-dep packages (e.g. copilot-api, openspec) this is empty or near-empty. For packages with rich runtime deps โ Yeoman generators, anything pulling ejs / jake / semver / yosay / @octokit/* โ dozens of internal symlinks land in the artifact. rattler-build's noarch validator rejects them as non-portable on Windows after writing the archive.
Fix: strip the internal .bin/ directories after npm install --global but before pnpm-licenses runs. These symlinks are only used by npm exec inside the package's own node_modules/; the package's top-level CLI shim at ${PREFIX}/bin/<name> is unaffected. For Yeoman generators specifically, the top-level yo runtime is what discovers and dispatches to generators, so dropping the internal .bin/ directories is fully safe.
# In recipes/<name>/build.sh, after npm install --global:
find "${PREFIX}/lib/node_modules/<name>" -type d -name .bin -exec rm -rf {} +
The --allow-symlinks-on-windows rattler-build flag is a local-build escape hatch only โ conda-forge staged-recipes CI does not pass it, so the strip is the durable fix. --no-bin-links on npm install is an alternative but disables top-level bin creation too, which breaks the CLI; the post-install find -delete is more surgical.
Case study: generator-code (VS Code Yeoman generator) local build, May 2026. yeoman-generator's transitive deps install bin shims for ejs / jake / node-which / rc / semver / yosay / cross-spawn. First build wrote generator-code-1.11.18-hccbf638_0.conda and was then rejected at the symlink-check stage. yo itself (built in the same session) had no such issue โ its dep tree doesn't ship executable scripts. Fixed with a one-line find ... .bin -exec rm -rf between npm install --global and pnpm install; rebuilt clean (-h5e06de4_0.conda, 0 symlinks).
Symptom: a recipe generated by generate_recipe_from_pypi passes validate_recipe and optimize_recipe but its test phase fails at runtime with:
ModuleNotFoundError: No module named 'microsoft_kiota_bundle'
The recipe's tests[].python.imports: entry mirrors the PyPI distribution name (with hyphens converted to underscores) but the package actually publishes a different top-level import.
Why: grayskull defaults the import test to a name derived from the PyPI distribution name (microsoft-kiota-bundle โ microsoft_kiota_bundle). This is correct in the typical case (numpy distributes numpy, pandas distributes pandas) but wrong whenever upstream namespaces packages differently โ common patterns include:
microsoft-kiota-* โ kiota_*, microsoft-kiota-bundle โ kiota_bundle, microsoft-kiota-http โ kiota_http).azure-identity-broker โ azure.identity.broker, google-cloud-storage โ google.cloud.storage).PyYAML โ yaml, beautifulsoup4 โ bs4, protobuf โ google.protobuf).In every case the only authoritative source is the sdist's top-level __init__.py, not the distribution name.
Fix: before trusting the generated test, list the sdist contents and grep for the top-level __init__.py:
curl -sL "https://pypi.org/packages/source/<first-letter>/<name>/<name>-<version>.tar.gz" -o /tmp/sdist.tgz
tar tzf /tmp/sdist.tgz | grep -E '__init__\.py$' | head -3
The first matching path's middle segment (e.g. microsoft_kiota_bundle-1.10.1/kiota_bundle/__init__.py โ kiota_bundle) is the correct import name. For dotted namespaces, list the deepest __init__.py chain (e.g. azure_identity_broker-1.3.0/azure/identity/broker/__init__.py โ azure.identity.broker).
Then edit the recipe to match โ imports: [<correct_name>] โ and re-validate. pip_check: true in the same test block will additionally catch entrypoint / dependency mismatches that the import alone would miss.
Case study: staged-recipes Microsoft Agents bundle (May 2026). Grayskull generated imports: [microsoft_kiota_bundle] for microsoft-kiota-bundle v1.10.1; the actual top-level package is kiota_bundle (matching the convention of every other already-shipped microsoft-kiota-* feedstock). Confirmed via tar tzf microsoft_kiota_bundle-1.10.1.tar.gz | grep '__init__.py$'; recipe edited to imports: [kiota_bundle] and built clean.
wheel + setuptools host deps for poetry-core projectsSymptom: a generated recipe whose pyproject.toml declares build-system: requires = ["poetry-core"] carries a host section like:
requirements:
host:
- python ${{ python_min }}.*
- poetry-core
- wheel # โ redundant
- setuptools >=42.0.0 # โ redundant
- pip
The build still works, but optimize_recipe doesn't flag it (today), reviewers tend to ask for cleanup, and pip check in some downstream environments warns about uninvoked build backends.
Why: grayskull is conservative โ when it can't conclusively prove a backend is the only one needed, it adds the legacy pair wheel + setuptools as belt-and-suspenders. For PEP 517 projects with a single declared backend (poetry-core, hatchling, flit-core, pdm-backend, scikit-build-core), only that backend + pip is needed in host:.
Fix: when the recipe is generated, immediately inspect the sdist's pyproject.toml [build-system] requires list and drop any host entries not present there:
tar -xzOf /tmp/sdist.tgz "*/pyproject.toml" | grep -A 5 '^\[build-system\]'
For build-system: requires = ["poetry-core"] keep only poetry-core + pip in host:. For hatchling, just hatchling + pip. The pattern generalizes โ verify against the actual [build-system] table, don't trust grayskull.
Case study: Microsoft Agents bundle (May 2026). Both microsoft-agents-m365copilot-core and microsoft-agents-m365copilot got the redundant pair from grayskull despite their pyproject.toml declaring only poetry-core. The third sibling, microsoft-kiota-bundle, was emitted clean (just poetry-core + pip) โ grayskull's behavior is non-deterministic across runs. Worth always inspecting.
Symptom: an upstream Python sdist ships no LICENSE (G4 applies, secondary GitHub source needed) and the repo's Git tag scheme doesn't match the Python release. The typical https://raw.githubusercontent.com/<org>/<repo>/v${{ version }}/LICENSE pattern returns 404 because no vN.M.K tag was ever pushed โ the project tags only its JavaScript / .NET / Java releases under different names.
Why: large polyglot monorepos (Microsoft / Azure SDKs, Google APIs, Apache Foundation projects) often have per-language release pipelines that publish to language-specific registries (PyPI / npm / NuGet / Maven) without crossing into Git tags. Examples:
microsoft/Agents-M365Copilot tags only JS releases: @microsoft/agents-m365copilot-v1.6.0, @microsoft/agents-m365copilot-v1.5.0, โฆ. The Python microsoft-agents-m365copilot v1.6.0 has no corresponding Git tag.Azure/azure-sdk-for-python tags as azure-<package>_<version> (e.g. azure-identity_1.18.0), which works with the right URL template but is not v${{ version }}.Fix: pin the LICENSE secondary source to a specific commit SHA + sha256, not a tag:
source:
# Primary: PyPI sdist (does NOT ship LICENSE).
- url: https://pypi.org/packages/source/m/<name>/<name>-${{ version }}.tar.gz
sha256: <sdist-sha>
# Secondary: pin to a specific commit because no per-Python tag exists.
# Bump the commit + sha256 together with the version on every update.
- url: https://raw.githubusercontent.com/<org>/<repo>/<40-char-commit-sha>/LICENSE
sha256: <license-sha>
file_name: LICENSE
Pick the commit deliberately: the current main HEAD at recipe-creation time is fine (the LICENSE almost never changes), or the commit that landed the Python release if it can be identified from the release notes. Document the choice in the recipe via a comment and in the PR description so reviewers understand it's deliberate, not lazy.
Add a maintenance note to the PR description: "On every version bump, the bot must also refresh the LICENSE commit SHA + sha256 โ there is no per-Python tag to auto-track." The autotick bot does not handle this today; without the note, a future version bump may carry a stale LICENSE pin.
Case study: staged-recipes Microsoft Agents bundle (May 2026). microsoft-agents-m365copilot v1.6.0 has no python/v1.6.0 or v1.6.0 tag on microsoft/Agents-M365Copilot โ only @microsoft/agents-m365copilot-v1.6.0 (JS). Pinned LICENSE to commit 0376aa418345d7f719b7b75d6e784fa7a765d9d0 with sha256 c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383. microsoft-agents-m365copilot-core v1.0.0 sdist did ship LICENSE, so no secondary source was needed there โ even within a single monorepo, the per-package pattern varies.
Symptom: a recipe-generation flow declares a requirements.run: dep "missing" from conda-forge because a manual repodata search for the literal PyPI distribution name returns 0 hits across every platform. grayskull's emitted requirements.run: carries the PyPI name; the conda-forge feedstock actually exists under a name with a -py or -python suffix, or with an unexpected hyphenโunderscore form.
Why: when upstream PyPI publishes a Python distribution whose bare name collides with โ or could collide with โ a non-Python package, conda-forge convention disambiguates by suffixing the feedstock name. The PyPI distribution stays under the bare name; the conda-forge feedstock adds a discriminator. There is no algorithmic rule โ the choice of -py vs -python vs underscore-preserved vs bare is a per-feedstock decision. The get_conda_name mapping cache doesn't always capture either suffix form.
| PyPI distribution | conda-forge feedstock | Pattern | Why the divergence |
|---|---|---|---|
wasmtime |
wasmtime-py |
-py suffix |
Disambiguates the pyo3 Python binding from the underlying Rust WASM runtime, in case the runtime is ever packaged separately. |
langfuse |
langfuse-python |
-python suffix |
Disambiguates from upstream's polyglot SDK family (langfuse-js, langfuse-go, etc.); -python names the Python distribution explicitly. |
tree_sitter |
tree_sitter |
underscore preserved (no suffix) | Binding + native C library ship in the same feedstock; no disambiguation needed. PyPI's underscore form carries straight through. |
duckdb |
python-duckdb |
python- prefix |
The bare duckdb feedstock ships the CLI / old py-specific build (linux-64 + noarch only); the cross-platform Python binding (import duckdb, all 6 subdirs) is python-duckdb. A bare run: duckdb resolves to the wrong/ambiguous artifact. |
The -py and -python forms tend to appear when the upstream project has a multi-language SDK family or a native runtime that conda-forge wants room to package separately. The underscore-preserved form appears when the binding and the native code ship in a single combined feedstock. None of these are predictable from the PyPI name alone โ you have to check.
Fix โ before declaring a dep missing: check five name forms in order before concluding a dep isn't on conda-forge:
wasmtime, langfuse).- โ _ swapped (tree-sitter โ tree_sitter).-py suffix (wasmtime-py).-python suffix (langfuse-python).python- prefix (python-duckdb) โ when the bare name is a non-Python artifact (CLI/library) and the Python binding is prefixed.check_dependencies does this lookup correctly via the grayskull mapping cache; manual repodata searches by literal name miss every suffix form. If a dep isn't on conda-forge after all four checks, then it's genuinely missing โ at that point the scoping decision (package the prerequisite first vs. defer) is real.
Fix โ in the recipe: edit requirements.run: to use the conda-forge feedstock spelling. grayskull emits the PyPI name (wasmtime, langfuse); the recipe must hand-edit to the feedstock spelling (wasmtime-py, langfuse-python). The optimizer doesn't catch this divergence today โ closing that gap is open work (proposed DEP-005 "PyPI name in run requirements has a -py or -python feedstock on conda-forge").
Case study 1 (-py suffix): scoping pass for simonw/micropython-wasm (Jun 7, 2026). Initial manual repodata search for wasmtime across noarch + linux-64 + osx-64 + osx-arm64 + win-64 + linux-aarch64 + linux-ppc64le returned 0 hits in all 7 subdirs; concluded wasmtime was missing and proposed a two-recipe bundle. User pointed at conda-forge/wasmtime-py-feedstock; re-check by feedstock name confirmed 39 builds ร 5 platforms at v45.0.0. The hand-edit requirements.run: [..., wasmtime-py] was the only change to grayskull's output.
Case study 2 (-python suffix): suitenumerique build-report audit (Jun 7, 2026). The 2026-05-31 SUITENUMERIQUE_SUMMARY.md listed langfuse as a Wave-1b conda-forge-submission-ready recipe ("not on conda-forge โ clean candidate"). Re-check on user prompt found conda-forge/langfuse-python-feedstock at v4.7.1 (matching PyPI langfuse 4.7.1; 61 noarch builds; recipe source.url: pypi.org/packages/source/l/langfuse/langfuse-${{ version }}.tar.gz confirms identity). The local-recipes recipes/langfuse/ build was redundant for submission; Wave-1b count dropped 13 โ 12.
Auto-memory feedback_pypi_conda_mapping_unreliable.md is the live cross-skill reference for this multi-form check; it carries all three confirmed patterns.
Symptom: a noarch: python recipe builds cleanly on linux_64 + osx_64 but fails on win_64 during hatchling's wheel-build phase with:
OSError: [WinError 123] The filename, directory name, or volume label syntax is incorrect:
'D:\bld\bld\rattler-build_<name>_<id>\work\<path>\conftest.py'
pip._internal.exceptions.MetadataGenerationFailed: metadata generation failed
The recipe is otherwise correct, deps resolve, build succeeds in isolation on Linux + macOS.
Why: the upstream PyPI sdist ships POSIX symlinks via tar's symlink record. Linux + macOS build agents extract them as real symlinks and os.stat follows them transparently. Windows Azure Pipelines runners lack SeCreateSymbolicLinkPrivilege by default, so tar's symlink-extraction falls back to writing a text stub containing the literal target path (e.g. ../../../some/path). os.stat on that stub fails โ Windows parses the content as a malformed path and returns WinError 123 / ERROR_INVALID_NAME. Hatchling iterates every included file during the wheel build and calls os.stat on each one, so the first symlink in iteration order triggers the failure.
This is distinct from G6 (npm node_modules/.bin/ symlinks): G6 fires at rattler-build's noarch-Windows-portability check after the wheel is built; G11 fires during hatchling's wheel build itself (pre-validation).
Fix: extend the recipe's downstream pyproject.toml patch with a [tool.hatch.build.targets.wheel] exclude: entry for the offending symlinked path:
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,6 +8,7 @@
# wheel ZIP). By excluding here we ensure only force-include writes the files.
exclude = [
"python/<name>/env_templates/**",
+ "python/<name>/<offending>/conftest.py",
]
The symlink target file usually still ships under its real path elsewhere in the wheel, so test discovery for the target's tests still works. Exclude each symlinked path explicitly โ broad **/conftest.py globs strip more than needed and break test discovery for unrelated test packages.
How to detect upstream symlinks before pushing to CI:
curl -sL "https://pypi.org/packages/source/<x>/<name>/<name>-<version>.tar.gz" \
| tar tvz - | grep '^l'
tar tvz lists entries with their type โ symlinks appear as lrwxr-xr-x ... -> <target>. Run on every new noarch:python recipe to flag the symlink risk before the staged-recipes push.
Case study: staged-recipes PR #33534 (xorq 0.3.26, Jun 7, 2026). Two consecutive CI runs (Azure buildId 1533937 + 1533940) failed at the same line of the same log with OSError: [WinError 123] ... 'D:\bld\bld\rattler-build_xorq_<id>\work\python\xorq\common\utils\tests\conftest.py'. Inspection via tar tvzf revealed exactly one symlink: python/xorq/common/utils/tests/conftest.py โ ../../../backends/xorq_datafusion/tests/conftest.py. The target file python/xorq/backends/xorq_datafusion/tests/conftest.py was a normal file in the same sdist. Patch added the symlinked path to the existing [tool.hatch.build.targets.wheel] exclude list (alongside python/xorq/env_templates/**); next CI run (Azure buildId 1533952) was win_64 GREEN. Target's tests still discoverable via the surviving target file.
run: deps in noarch:python recipes need noarch_platforms in conda-forge.ymlSymptom: a noarch: python recipe uses the canonical rattler-build v1 if: linux / then: selector to gate a Linux-only runtime dep, and conda-smithy lint hard-fails on staged-recipes-linter with:
โ noarch packages can't have selectors. If the selectors are necessary, please remove noarch: python.
rattler-build itself accepts the selector (rattler_lint_ran: true in validate_recipe returns clean) โ this is purely a conda-smithy lint rule, code R1-002 (lint_noarch_selectors).
Why: the lint rule exists to catch build-time selector mistakes in noarch recipes (selectors in build.script:, build.skip:, requirements.host:, requirements.build: produce broken artifacts because there's only one noarch build). But the rule fires on runtime selectors in requirements.run: too, even though those are evaluated at install time by the conda solver and are semantically correct.
The v1-specific implementation of the rule (lint_recipe_v1_noarch_and_runtime_dependencies in conda-smithy/linter/) has a documented escape hatch: when the recipe's conda-forge.yml declares noarch_platforms with >=2 entries, the lint gate flips from "no selectors allowed in noarch" to "selectors are OK because the author has tested cross-platform". This is the canonical design โ not a linter.skip opt-out.
Fix: drop a conda-forge.yml next to recipe.yaml listing every platform you have verified the selector resolves correctly on:
# recipes/<name>/conda-forge.yml
noarch_platforms:
- linux_64
- osx_64
- win_64
The minimum for the gate flip is 2 platforms. List only what you've verified โ adding win_64 when the recipe has unresolved Windows issues will trigger CI on Windows and produce a different failure.
When NOT to use this: if your selectors are in build-time fields (build.script:, build.skip:, requirements.host:, requirements.build:), the noarch artifact really IS broken on at least one platform โ the lint is correctly rejecting your recipe. Either remove noarch: python and produce per-platform builds, or restructure so the build is platform-independent.
Case study: staged-recipes PR #33534 (xorq 0.3.26, Jun 7, 2026). After landing G11's symlink patch and adding an if: linux / then: selector to gate git-annex (which has 139 conda-forge linux-64 builds + zero on every other platform), the next CI run fired R1-002 on the linter. Research subagent traced the rule to conda_smithy/linter/lint_recipe.py line 302 (if "lint_noarch_selectors" not in lints_to_skip) with the v1 path in conda_recipe_v1_linter.py โ lint_usage_of_selectors_for_noarch() and message class NoarchSelectorsV1. Adding recipes/xorq/conda-forge.yml with noarch_platforms: [linux_64, osx_64, win_64] (all three because the symlink patch made Windows viable) flipped the lint on the subsequent push. Final CI run (head 0be2b938e4) went all-green across linter + conda-forge-linter + linux_64 + osx_64 + win_64. Two-commit fix sequence on top of the existing PR head: ffbe7aa174 (selector + symlink patch) โ 0be2b938e4 (conda-forge.yml).
Refinement (opik #33936, Jun 26 2026) โ the noarch_platforms escape hatch does NOT reliably silence the conda-forge-linter webservice; PREFER eliminating the selector (G35). opik (noarch) carried if: not (linux and aarch64) then: [tree_sitter, tree-sitter-javascript, tree-sitter-typescript] + a conda-forge.yml with noarch_platforms (5 platforms). validate_recipe flagged noarch packages can't have selectors; it was wrongly dismissed as the G12 false-positive (xorq pattern). On CI the GHA linter passed but the conda-forge-linter webservice STILL flagged the selector โ red PR. Lessons:
noarch_platforms escape hatch is not dependable for the conda-forge-linter webservice (it worked for xorq's if: linux, not for opik's if: not (...)). Do NOT treat validate_recipe's "noarch โฆ can't have selectors" as a guaranteed false-positive โ verify, don't assume.noarch_platforms. Most noarch run-selectors excluding a dep on linux-aarch64 track the upstream PyPI musllinux_aarch64 WHEEL gap, not conda availability. Check per-subdir repodata (curl -s conda.anaconda.org/conda-forge/<subdir>/repodata.json โ mind the _-vs-- conda name, G10): if conda-forge ships the dep on all subdirs, make it unconditional and delete the conda-forge.yml โ no selector โ green on both linters, installable everywhere. Keep a noarch run-selector + noarch_platforms ONLY when the dep is genuinely absent on conda-forge for some platform. opik: all three tree-sitter pkgs are on every subdir incl. linux-aarch64 โ dropped the selector, removed conda-forge.yml โ both linters green.Why local gates "miss" issues the CI linter/builds catch (answers the recurring "why didn't local lint/build catch this before I pushed?"): two distinct mechanisms โ (1) validate_recipe runs conda-smithy lint and does catch noarch-selector / many lints; the opik miss was a judgment error (overriding a real lint as a false-positive), not a tooling gap โ never dismiss a validate_recipe lint without proof. (2) Local recipe-build / validate run on the HOST platform only (linux), so per-platform problems can't surface locally โ e.g. opik's osx-64 test-env solve failing because litellm โ fastuuid ships no osx-64 py3.10 build (G40/G61). conda-forge CI builds every subdir, so it finds them. Mitigations: trust validate_recipe lints; and for any recipe whose deps include compiled transitive packages, run the G40 per-subdir repodata check (each platform has a pyXY build at the floor) BEFORE submitting rather than discovering it on CI.
script: list entries โ and (cmd) is not a subshell on Windows cmd.exeSymptom: a multi-entry build.script: list works on Linux + macOS but on Windows the second entry fails with Directory '.' is not installable. Neither 'setup.py' nor 'pyproject.toml' found. (or similar CWD-confused error). The first entry uses a cd subdir && ... pattern, sometimes wrapped in (parens) to "isolate" the change.
Why (two interlocking facts the skill didn't previously capture):
rattler-build's script-list entries share CWD across entries. G1 documents that environment variables don't carry across entries โ but CWD specifically does carry. If entry 1 ends with CWD inside a subdirectory, entry 2 starts there. (Verified empirically on solvor PR #33647 buildId 1534140 Windows leg; also reproduced on local Linux builds.)
On Windows cmd.exe, (cmd1 && cmd2) is command grouping, not a subshell. On bash, (cd dir && do_thing) runs the entire group in a child process โ the cd only affects that child, so when control returns to the parent shell, CWD is unchanged. On cmd.exe, (...) is purely syntactic grouping (parentheses associate the AND chain) โ there is no child process, so the cd permanently changes the parent shell's CWD. The same recipe that "works" on Linux is broken on Windows.
Fix: use pushd / popd, which work identically in both bash AND cmd.exe โ pushd dir changes CWD and pushes the previous CWD onto a stack; popd restores it. Both shells implement this the same way, and the recipe stays one source for all three platforms:
build:
script:
# Cross-platform CWD isolation across script-list entries.
- pushd subdir && some_tool --output ../result.yml && popd
- ${{ PYTHON }} -m pip install . -vv --no-deps --no-build-isolation
Forward slashes in the relative output path (../result.yml) work on both shells; cmd.exe accepts / as a path separator in this context. No if: unix / then / else: block is needed.
Alternative: collapse the two entries into one long-form entry with a single shell invocation. This sidesteps CWD-persistence entirely (CWD changes are scoped to that one entry by definition):
build:
script:
- if: unix
then: |
cd subdir
some_tool --output ../result.yml
cd ..
${{ PYTHON }} -m pip install . -vv --no-deps --no-build-isolation
else: |
pushd subdir
some_tool --output ../result.yml
popd
"%PYTHON%" -m pip install . -vv --no-deps --no-build-isolation
The pushd/popd form (preferred) is shorter and avoids platform forking. Use the collapsed-entry form only when entries need shell-specific syntax beyond CWD.
When this comes up in practice: any maturin/PyO3 recipe whose [tool.maturin].manifest-path points at a Cargo.toml in a subdirectory (e.g. solvor's rust/Cargo.toml). cargo-bundle-licenses does not have a --manifest-path flag โ it operates on whichever directory it's invoked from. So the recipe must pushd to the Cargo.toml's directory before invoking, write THIRDPARTY.yml back up via --output ../THIRDPARTY.yml, and popd before the pip step.
Case study: staged-recipes PR #33647 (solvor 0.6.2, Jun 7, 2026). Initial fix to a cargo-bundle-licenses Cargo-not-found error used a bash-subshell pattern: - (cd rust && cargo-bundle-licenses --format yaml --output ../THIRDPARTY.yml). Local Linux build succeeded; Windows CI buildId 1534140 failed at line 1106 of the log with pip._internal.exceptions.InstallationError: Directory '.' is not installable. Neither 'setup.py' nor 'pyproject.toml' found. (the cmd prompt visible in the log was (base) %SRC_DIR%\rust>..., confirming CWD had leaked into the pip step). Replaced with pushd rust && cargo-bundle-licenses --format yaml --output ../THIRDPARTY.yml && popd; subsequent CI run on efd84c9ee6 went all-green across linter + linux_64 + osx_64 + win_64.
package.version float-parse ruleSymptom: a [bot-automerge] <pkg> vX.Y PR from regro-cf-autotick-bot against a v0 (meta.yaml) feedstock fails the conda-forge-linter check with:
โ
package.version has a value that is interpreted as a floating-point number. Please quote it (like "0.14" or "{{ var }}") to ensure that it is interpreted as string and preserved exactly.
The Linux build itself passes; only the linter blocks. With bot.automerge: true, the bot still refuses to merge because lint=failed.
Why: the lint runs after jinja rendering. The autotick bot edits {% set version = "0.14" %} (correctly quoted at the jinja level) but does NOT change the YAML-level template:
package:
name: {{ name|lower }}
version: {{ version }} # โ renders to: version: 0.14
YAML then types the rendered value: 0.14 (bare) โ float; "0.14" (quoted) โ string. The lint correctly flags the float interpretation because PyPI versions like 1.0 could collapse to 1 (an integer) and lose precision in extreme cases. For most versions it's only a type-mismatch, but the rule fires either way.
This affects already-merged v0 recipes whose package.version was never quoted at the YAML level โ the original submission may have passed because the lint rule was added later. The next autotick bump exposes the issue.
Fix โ minimal, keep v0: quote package.version at the YAML level:
package:
name: {{ name|lower }}
version: "{{ version }}" # โ keeps the string type after rendering
The jinja {% set version = "X.Y" %} line stays unchanged. The double quotes around {{ version }} survive jinja and become YAML quotes, so the rendered value is version: "0.14".
Long-term fix โ migrate to v1: v1 recipe.yaml uses ${{ version }} substitution and the v1 parser preserves the original string type by construction. The float-parse class of bugs simply doesn't exist on v1. If the maintainer is already touching the recipe to fix this lint, it's often the right moment to also do the v0 โ v1 migration (per the Migration Protocol).
Case study: feedstock PR #8 on conda-forge/wagtail-ab-testing-feedstock (Jun 7, 2026) โ autotick bot bumped 0.13 โ 0.14 and the conda-forge-linter blocked with the float-parse message. v0.13 had presumably passed when the recipe was first submitted; the lint rule was added or tightened since. Local fix went straight to v1: wrote recipe/recipe.yaml, updated conda-forge.yml with conda_build_tool: rattler-build, deleted recipe/meta.yaml after a clean v1 build (wagtail-ab-testing-0.14-pyhcf101f3_0.conda, 148 KiB), and aligned conda-forge.yml with the python-cityhash template (bot.check_solvable, bot.run_deps_from_wheel, conda_install_tool: pixi). The float-parse issue can't recur on v1.
Validated (2026-07-02): on three red autotick PRs (django-anymail v15.0, import-linter v2.12, shot-scraper v1.10 โ all float-lookalike versions), the minimal YAML-level quote fix โ built + tested locally, then maintainer-edit pushed to the bot branch โ re-linted green and [bot-automerge] merged each within minutes. The quickest remediation when a v0โv1 migration isn't being bundled; a restart/rerender adds nothing for this class (the lint, not CI, is the blocker).
.conda artifact but leaves repodata.json staleSymptom: a downstream recipe's build (or test) fails with:
ร failed to fetch <pkg>-<ver>-<build>.conda
โโโถ failed to interact with the package cache layer.
โฐโโถ hash mismatch when extracting file:///.../build_artifacts/.../<pkg>-<ver>-<build>.conda:
expected <SHA-A>, got <SHA-B>, total size NNNN bytes
The downstream recipe references the dep correctly; the artifact exists on disk at the expected path; nothing in the recipe is wrong. The mismatch is purely between the on-disk .conda file and the channel's repodata.json record.
Why: rattler-build embeds the build timestamp into the .conda zip's central directory. A second build of the same recipe sources on the same machine produces an artifact with identical contents but different bytes (the embedded timestamp differs), so its sha256 differs from the first build. rattler-build's normal write path is atomic โ it writes the new artifact AND updates repodata.json to point at the new hash. But the failure mode appears when:
pkg-1.0.0-h00.conda with sha256 A and writes A into repodata.json.rattler-build build invocation on the same recipe overwrites the artifact with sha256 B but the repodata.json update path doesn't fire (rattler-build's internal index cache thinks A is current; or the index step is skipped because nothing's "changed").The most common trigger in this skill's workflow is verifying a green build by re-running pixi run -e local-recipes rattler-build build --recipe recipes/X/recipe.yaml ... twice โ once to capture the build summary, a second time to grep for โ all tests passed!. The second run silently invalidates the channel for any other recipe depending on X.
Fix: delete the on-disk artifact, then rebuild โ rattler-build writes the new artifact AND updates repodata.json atomically on a fresh build (vs. an idempotent re-run):
# Delete the stale artifact + force a fresh build that re-indexes
rm build_artifacts/<config>/noarch/<pkg>-<ver>-<build>.conda
pixi run -e local-recipes rattler-build build --recipe recipes/<pkg>/recipe.yaml \
--variant-config .ci_support/<config>.yaml \
--variant-config .pixi/envs/local-recipes/conda_build_config.yaml \
--output-dir build_artifacts/<config>
The rebuild produces the same byte-equivalent artifact (modulo timestamp), but this time repodata.json is updated to match. The downstream build then resolves cleanly.
Don't use pixi run rattler-index โ the task isn't bound in this skill's pixi env. rattler-index fs <channel-path> is a real subcommand but invoking it through the unconfigured pixi task just dumps the task list. The rebuild path above is the practical re-index.
When this comes up in practice: cross-package local fix workflows. v8.2.0 documented the native-build.sh auto-channel-injection feature โ when recipe B has run: A and A was just built locally, the build of B auto-prepends file://${REPO_ROOT}/build_artifacts/<config> to channel_sources. The auto-inject is correct, but it relies on repodata.json being current. Rebuilding A invalidates this contract.
Case study: ag-ui-langgraph 0.0.41 fix session, Jun 9, 2026. Bumped ag-ui-a2ui-toolkit 0.0.2 โ 0.0.3, built it (first build wrote artifact + indexed). Then ran the same build command a second time to capture โ all tests passed! in a grep-friendly form โ that second build wrote new artifact bytes but the repodata still referenced the first build's hash. The ag-ui-langgraph 0.0.41 build then failed at the test phase with the exact hash mismatch when extracting file://.../ag-ui-a2ui-toolkit-0.0.3-pyhc364b38_0.conda: expected 85ea90d5..., got 766570d75... error. Fix: rm the stale artifact, rebuild ag-ui-a2ui-toolkit โ repodata refreshed in step. Downstream ag-ui-langgraph build then resolved 0.0.3 cleanly and tests passed.
/packages/source/<letter>/... routeSymptom: rattler-build's source fetch fails with HTTP status server error (503 Service Unavailable) when downloading from https://pypi.org/packages/source/<letter>/<name>/<sdist> โ the canonical URL pattern conda-forge mandates. The same artifact at https://files.pythonhosted.org/packages/<2-char>/<2-char>/<long-hash>/<sdist> returns 200. Sustained โ observed >12 min on 2026-06-11 for lyric-task 0.1.7. PyPI's Retry-After header is 0 (suggesting transient cache miss) but reality is a sustained route degradation.
Why: pypi.org/packages/source/... goes through PyPI's Varnish CDN edge layer for cache + routing. files.pythonhosted.org/packages/<hash>/... is the backing CDN (Fastly historically) and serves the same bytes. When Varnish has cache-routing issues, the pypi.org route returns 503 while the backing CDN keeps serving.
Workaround: URL-swap the recipe's source.url temporarily for the local build only.
source.url to the https://files.pythonhosted.org/packages/<hash>/<sdist> form. The sha256 stays unchanged (identical bytes); validate_recipe won't complain.recipe-build.source.url to the canonical https://pypi.org/packages/source/<letter>/... form before pushing the fork. The submitted recipe ships canonical; conda-forge CI uses its own infrastructure unaffected by today's Varnish issue.validate_recipe + optimize_recipe post-revert to confirm the recipe is still clean.Risk: forgetting to revert leaves the recipe with a non-canonical URL that bypasses JFrog Artifactory PyPI Remote Repository proxies in air-gapped environments (SKILL.md "PyPI source.url Must Use the pypi.org/packages/... Pattern" critical constraint). Always re-validate after the revert.
Detection at scale: curl -sI 'https://pypi.org/packages/source/<letter>/<name>/<sdist>' returns 503 while curl -sI 'https://files.pythonhosted.org/packages/<hash>/<sdist>' returns 200 โ Varnish issue, not a bytes issue. PyPI status page (status.python.org) lags real outages by several minutes โ trust the dual probe.
Case study: S3 lyric-task 0.1.7 build (2026-06-11). PyPI /packages/source/l/lyric-task/lyric_task-0.1.7.tar.gz returned 503 for >12 min; backing CDN at files.pythonhosted.org/packages/1d/ff/.../lyric_task-0.1.7.tar.gz returned 200 throughout. URL-swap workaround applied; build green at 14.62 KiB; URL reverted; validate_recipe + optimize_recipe clean post-revert; fork pushed.
pnpm install --ignore-scripts doesn't suppress the root project's lifecycle scriptsSymptom: a Node-workspace recipe (pnpm monorepo with a custom postinstall: node ./scripts/postinstall.mjs) is set up with pnpm install --ignore-scripts --filter '@org/<app>...' to skip sibling-workspace builds โ but the root postinstall runs anyway and fails. Typically the failure is that the upstream's postinstall script tries to build sibling workspaces whose dependencies the filtered install didn't fetch:
. postinstall$ node ./scripts/postinstall.mjs
. postinstall: > @org/sibling@X.Y.Z build /path/to/packages/sibling
. postinstall: > node ./esbuild.config.mjs && tsc -p tsconfig.json --emitDeclarationOnly
. postinstall: Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'esbuild' imported from /packages/sibling/esbuild.config.mjs
Why: pnpm's --ignore-scripts only suppresses dependency install scripts (i.e. node_modules/<dep>/package.json lifecycle scripts). The root project's own pre* / post* lifecycle scripts run regardless. This is documented but counter-intuitive โ recipe authors often think --ignore-scripts is the global "no scripts" switch. It is not. The behavior is consistent with npm too; both tools draw the same dependency-vs-root distinction.
A common case: an upstream monorepo's root postinstall builds every workspace under packages/* + tools/* (electron-builder workflows, codegen for sibling apps, native-addon prebuilds). A recipe that only ships one app (apps/<daemon>) uses --filter '@org/<daemon>...' to skip the heavyweight sibling workspaces โ but the root postinstall still tries to build them and fails because their deps weren't fetched.
Fix: strip the root project's postinstall from package.json before running pnpm install. A short Python snippet keeps the patch idempotent and avoids a downstream patches/<name>.patch to re-roll on every version bump:
# In build.sh, after `cd "${SRC_ROOT}"` and before `pnpm install`:
python -c "
import json, pathlib
p = pathlib.Path('package.json')
d = json.loads(p.read_text())
d.get('scripts', {}).pop('postinstall', None)
p.write_text(json.dumps(d, indent=2))
"
pnpm install \
--frozen-lockfile \
--strict-peer-dependencies=false \
--ignore-scripts \
--filter '@org/<daemon>...'
Keep the --ignore-scripts flag โ it still does useful work on dependency-side install scripts (skips native compile scripts on packages we'll explicitly rebuild later via pnpm --filter ... rebuild <native-dep>).
Why not patch via patches/<name>.patch: a patches/ patch is more visible to reviewers but requires re-rolling on every version bump (the JSON line numbers shift, hunks fuzz-fail). The Python pop() form is robust across upstream restructures and idempotent (no-ops if the script is already absent).
Other root scripts to consider: preinstall, prepare, prepublish โ same root-vs-dependency distinction applies to all lifecycle scripts. Audit package.json scripts: for anything in install-phase scope that does work outside the recipe's surface.
Case study: recipes/open-design/build.sh (Jun 11, 2026). The recipe was authored at v0.2.0 with --ignore-scripts and a comment ("--ignore-scripts skips the root package's postinstall") that was empirically false โ the recipe had never actually been built. Bumping to v0.9.0 surfaced the bug when upstream's scripts/postinstall.mjs buildTargets list grew from 4 to 13 (adding packages/download, packages/host, packages/registry-protocol, packages/agui-adapter, packages/plugin-runtime, packages/diagnostics, tools/dev, tools/pack, tools/serve). The newly-added packages/download workspace's build script needed esbuild which the @open-design/daemon... filter didn't fetch. Fix shipped as the Python-pop snippet above; build green at open-design-0.9.0-h07aa61f_0.conda (9.51 MiB).
workflow_settings.store_build_artifacts: true (unscoped) crashes Windows Azure builds via 7z + INetCache ACLsSymptom: feedstock CI shows Run Windows build: succeeded (the recipe's actual rattler-build phase finishes cleanly and the .conda artifact is produced โ visible in the archive listing) but the very next Azure task Prepare conda build artifacts: failed with:
D:\bld\bld\rattler-build_<pkg>_<id>\.work.pending-rm-<id>\AppData\Local\Microsoft\Windows\INetCache\Content.IE5 : Access is denied.
...
##[error]Cmd.exe exited with code '1'.
##[section]Finishing: Prepare conda build artifacts
The two follow-on Azure tasks Store conda build artifacts and Store conda build environment artifacts are reported skipped (gated on the prior task's success). The Azure leg is therefore marked result=failed on the timeline, the umbrella aggregator <feedstock-name> check goes red, and the PR can't advance to ready-for-review even though the build itself succeeded on every Python variant.
Why: when workflow_settings.store_build_artifacts is true (or the legacy azure.store_build_artifacts: true), conda-smithy rerender stamps store_build_artifacts: true per win matrix entry in .azure-pipelines/azure-pipelines-win.yml, which the Prepare conda build artifacts step's condition: eq(variables.store_build_artifacts, true) then activates. That step calls conda-forge-ci-setup's .scripts/create_conda_build_artifacts.bat, whose 7z invocation is:
7z a "<archive>" "%CONDA_BLD_PATH%" -xr^^!.git/ -xr^^!_*_env*/ -xr^^!*_cache/ -bb
It archives the entire D:\bld\ work directory with only .git/, _*_env*/, and *_cache/ excluded. Rust-heavy builds โ anything that compiles tree-sitter, hyper, tower, reqwest, rustls/webpki, etc. โ invoke Windows winhttp/wininet during fetch and write entries to AppData\Local\Microsoft\Windows\INetCache\Content.IE5 inside the build sandbox's redirected user profile. That directory has restricted Windows ACLs (only the SYSTEM account or owner can enumerate the contents); 7z fails with errorlevel 1, the bat exits 1, and the entire post-build archive task fails. Linux and macOS legs are unaffected โ the AppData/INetCache hierarchy is Windows-specific.
Fix: scope workflow_settings.store_build_artifacts to non-Windows platforms via the conditional list form (per the v8.6+ conda-smithy schema โ ConditionalValue supports os / platform / provider filters):
workflow_settings:
store_build_artifacts:
- platform:
- linux_64
- linux_aarch64
- osx_64
- osx_arm64
value: true
After rerender, the win Azure variants stamp store_build_artifacts: false, the condition: on the prepare task evaluates false, the entire failing task is skipped, and the leg goes green. Linux + osx still publish their .conda files as downloadable Azure artifacts โ the v8.14.0 pixi run -e local-recipes pr-artifacts <pr> smoke-test workflow keeps working there.
Refinement (lyric-py-feedstock #2, Jun 28 2026) โ the conditional-list form is necessary but NOT sufficient; win_64 must be ABSENT from the list, not merely "scoped". The common near-miss: the author switches to the ConditionalValue list form (correct shape) but leaves win_64 in the platform: list โ which still stamps store_build_artifacts: true on the win variants and crashes Prepare conda build artifacts identically. The real lever is win_64's absence from the list, not the list form itself. Verify by reading the rendered .azure-pipelines/azure-pipelines-win.yml (it must stamp store_build_artifacts: false), or simply confirm win_64 is not in the platform: list. Live: lyric-py-feedstock #2 (a Rust+PyO3 osx-arm64/linux-aarch64 platform-expansion) shipped the list form with all five platforms including win_64 โ all 4 win py-variants red at Prepare conda build artifacts (the INetCache ACL error) while Run Windows build succeeded; dropping the - win_64 line + rerender โ win green โ PR merged.
Upstream fix (out of feedstock scope): the durable fix is in conda-forge-ci-setup's .scripts/create_conda_build_artifacts.bat โ add -xr^^!*INetCache* and -xr^^!AppData/ to the 7z exclude list. File a separate issue against conda-forge/conda-forge-ci-setup rather than ship the workaround in every Rust-heavy Windows feedstock.
When this comes up in practice: any recipe that fetches over HTTPS during the build phase on Windows can in principle hit this. Rust+PyO3 / tree-sitter recipes are the most common trigger because their crate dependency trees include many HTTPS-fetching crates (reqwest, hyper, tower, webpki, rustls). Pure-Python recipes don't typically trigger it (pip uses its own cache, not winhttp).
Detection at scale: search the Azure build timeline for the failure signature โ a Job with result=failed whose Run Windows build sub-task is result=succeeded but Prepare conda build artifacts is result=failed. The timeline JSON is at dev.azure.com/conda-forge/feedstock-builds/_apis/build/builds/<buildId>/timeline?api-version=7.0.
Case study: cocoindex feedstock PR #9 (Jun 14, 2026; conda-forge/cocoindex-feedstock). Initial commit enabled unscoped workflow_settings.store_build_artifacts: true to enable the v8.14.0 PR-artifacts workflow. After rerender + first CI run: all 4 linux_64 + 4 linux_aarch64 + 4 osx_64 + 4 osx_arm64 + linter checks green; all 4 win_64 variants result=failed at the prepare step with the INetCache ACL error. Build phase (Run Windows build) succeeded on every win variant, .conda file produced (e.g. win-64\cocoindex-1.0.10-py313hfbe8231_0.conda visible in the 7z listing before the error). Fix: scope to [linux_64, linux_aarch64, osx_64, osx_arm64]. Subsequent rerender pushed store_build_artifacts: false to all win variants; next CI run all-green.
Error reading output: stream did not contain valid UTF-8 โ set PYTHONUTF8: "1" (+ canonical Rust env block while you're there)Scope (v8.43.0): NOT Rust-specific. This applies to ANY Windows build where Python reads a non-ASCII file with the default codec โ most commonly a pure-setuptools recipe whose setup.py does open('README.md') (no encoding=) on a UTF-8 README. On win-64 the default codec is cp1252, which can't decode the UTF-8 bytes, so metadata generation dies with UnicodeDecodeError: 'charmap' codec can't decode byte 0xNN โ a different symptom than the Rust "stream did not contain valid UTF-8", same PYTHONUTF8: "1" fix (PEP 540 forces Python's open() to default to UTF-8). Case: lomond 0.3.3 (staged-recipes #33889, Jun 25 2026) โ setup.py reads its box-drawing-char README; win_64 (buildId 1543779) hit charmap byte 0x90 at metadata gen; PYTHONUTF8: "1" in build.script.env fixed it (no Rust env block needed for a pure-setuptools recipe).
Symptom: Rust+PyO3 (or any cargo+pip) recipe's Windows Azure CI fails inside the Run Windows build task at the pip install . -vv --no-deps --no-build-isolation step with:
[2026-06-14T17:58:27Z WARN bundle_licenses_lib::found_license] ...
Using pip 26.1.2 from %PREFIX%\Lib\site-packages\pip (python 3.12)
...
Processing .\.
Added file:///%SRC_DIR% to build tracker '...'
Preparing metadata (pyproject.toml): started
Running command Preparing metadata (pyproject.toml)
โ warning Error reading output: Custom { kind: InvalidData, error: "stream did not contain valid UTF-8" }
The build appears to abort mid-pip install; rattler-build emits the misleading warning and Windows variants fail. Linux + macOS unaffected (their console default is UTF-8). The recipe builds clean on every non-Windows platform.
Why: rattler-build captures the subprocess (cargo / maturin / pip) stdout+stderr and decodes them as UTF-8. Windows CI agents (vmImage: windows-2022) default to cp1252 for the console codepage, NOT UTF-8. Modern Python's sys.stdout honors sys.stdout.encoding which respects locale; on Windows that resolves to cp1252 unless explicitly overridden. When cargo/maturin/pip emit non-ASCII bytes โ license filenames with accents (LICENSE-รLECTRON-MIT), package descriptions with em-dashes, paths with localized Windows component names, etc. โ the resulting cp1252-encoded bytes can't be decoded as UTF-8 by rattler-build's reader. The reader gives up reading the stream, rattler-build flags the build as broken, and the Windows leg fails.
Fix: set PYTHONUTF8: "1" in build.script.env (PEP 540 โ Python UTF-8 mode). This forces Python to use UTF-8 for sys.stdout/sys.stderr regardless of system locale. Pip then writes UTF-8, rattler-build decodes cleanly, the build completes.
Apply the canonical Rust env block, not just the one-line PYTHONUTF8 fix. When you're already editing build.script.env, also add the conda-forge canonical Rust optimization env vars per CFE skill v8.9.1 retro + conda-forge.org/docs/maintainer/example_recipes/rust โ these are the default for every Rust feedstock, NOT optional:
build:
script:
env:
# Strip debug symbols from the produced .so / .pyd / .dll.
# Conda-forge default for all Rust builds; reduces final binary
# by 30โ50%.
CARGO_PROFILE_RELEASE_STRIP: symbols
# CARGO_PROFILE_RELEASE_LTO: fat is intentionally NOT set โ fat
# LTO can blow past Azure's 6-hour timeout on Windows for Rust
# packages with deep dep graphs (~600 crates is typical for
# tree-sitter / hyper / tower / rustls / heed combos). Re-enable
# only when the recipe's actual Windows build time stays well
# under ~3h. The `# note time out issues` comment is institutional
# knowledge from xorq-datafusion โ leave it for the next maintainer.
#CARGO_PROFILE_RELEASE_LTO: fat
# PEP 540 โ Python UTF-8 mode. THE Windows fix for this gotcha.
PYTHONUTF8: "1"
content:
- if: unix
then: |
export CFLAGS="${CFLAGS:-} -D_BSD_SOURCE -D_DEFAULT_SOURCE" # iff the recipe needs it
cargo-bundle-licenses --format yaml --output THIRDPARTY.yml
${{ PYTHON }} -m pip install . -vv --no-deps --no-build-isolation
else: |
cargo-bundle-licenses --format yaml --output THIRDPARTY.yml
${{ PYTHON }} -m pip install . -vv --no-deps --no-build-isolation
This is the canonical Rust+PyO3 conda-forge env block. Any new Rust+PyO3 recipe should start here. recipes/cocoindex/recipe.yaml and conda-forge/xorq-datafusion-feedstock both ship this shape.
Why include STRIP even when only PYTHONUTF8 is the apparent fix: a single-line PYTHONUTF8 patch is a scope mistake. STRIP is the conda-forge default for all Rust builds (CFE v8.9.1 retro shipped it into the maturin template; the canonical doc lists it as the first env var); commenting out LTO with the # note time out issues rationale preserves the institutional knowledge for the next maintainer (otherwise they'll re-enable LTO speculatively and discover the timeout the hard way). Matching the cited reference pattern fully is canonical-pattern application, not scope creep.
When you'd opt INTO CARGO_PROFILE_RELEASE_LTO: fat: only when (a) the recipe is a small-graph Rust binary (~100 crates or fewer), (b) Azure CI history shows <90 min Windows build time at the current STRIP-only shape, and (c) the binary-size win from LTO is meaningfully worth the extra build time. For 95% of Rust+PyO3 recipes, leave it commented.
Edge case โ the env block changes the script: shape from list to env+content: if the recipe's current build.script: is a YAML list of strings, switching to env: + content: is a structural change (not just an env-var addition). Validate locally with pixi run -e local-recipes validate recipes/<feedstock> after the edit โ schema parser is strict about this.
Case study: cocoindex 1.0.10 + xorq-datafusion 0.2.9 (both rxm7706-maintained). xorq-datafusion's recipe carries the canonical env block โ CARGO_PROFILE_RELEASE_STRIP: symbols + #CARGO_PROFILE_RELEASE_LTO: fat # note time out issues + PYTHONUTF8: "1" โ and ships clean on every platform. cocoindex 1.0.10's upstream recipe had been bot-automerged at v1.0.10 without the env block; Windows CI passed at the time but broke later when a rattler-build / pip / cargo update tightened UTF-8 decoding. PR #9 (Jun 14, 2026) added the full canonical env block โ STRIP + commented LTO + PYTHONUTF8 โ bumping build.number: 0 โ 1. Subsequent CI run: all Windows variants green.
Detection at scale: grep Azure win build logs for Error reading output: ... stream did not contain valid UTF-8. Affected recipes typically also have Recovery (errors) lines around the failure point indicating pip's metadata-preparation phase couldn't complete cleanly.
{{ X }} syntax in v1 recipe.yaml is silently rendered as literal textSymptom: a v1 recipe (declares schema_version: 1) passes validate_recipe and conda-smithy lint cleanly, but the build fails with errors that look unrelated to syntax:
build.script: containing {{ PYTHON }} -m pip install . -vv โฆ โ cmd.exe / bash treats {{ as a literal command and emits command not found: {{ or '{{' is not recognized as an internal or external command.requirements.host: / requirements.run: carrying librdkafka >={{ version }} โ the conda solver parses >={{ version }} as a literal version constraint and aborts with cannot parse spec '>={{ version }}'.- 0001-Fix-setup-for-windows.patch # [win] โ applies the patch on every platform because the comment selector is silently ignored (this is the cousin trap; see G3 for py < N and the # [win] selector both being silently ignored by v1).Why: in v1 recipe.yaml, the substitution prefix is ${{ โฆ }} (minijinja); the bare {{ โฆ }} form is the v0 conda-build jinja prefix. The v1 parser treats bare {{ โฆ }} as a plain YAML scalar โ well-formed YAML, so validators don't object โ and emits it verbatim to the shell, the dep spec, or whatever consumer reads the rendered value. validate_recipe, rattler-build lint, and conda-smithy lint all pass; the failure surfaces at build / install time.
This is the third entry in a class of silent v0โv1 substitution traps:
home, dev_url, doc_url) silently discarded (license_family is NOT one of these โ it is a valid, if deprecated, v1 field).py < N skip selectors and v0 # [unix] / # [win] comment selectors silently ignored.{{ X }} substitutions silently emitted as literal text.Fix: in every v1 recipe.yaml, use ${{ X }} for every substitution. Common sites where the bare form sneaks in when a recipe is hand-edited (or migrated from v0 without a full sweep):
build.script: โ {{ PYTHON }}, {{ CFLAGS }}, {{ SRC_DIR }}requirements.host: / requirements.run: / requirements.build: โ <pkg> >={{ version }}, <pkg> {{ version }}.*source.url: โ https://โฆ/{{ version }}/โฆ, โฆ/{{ name }}-{{ version }}.tar.gzpackage.name: / package.version: โ should be ${{ version }} literalsabout.* scalars when templated (rare but possible)Detection โ one-line grep:
grep -nE '(^|[^$])\{\{[^}]+\}\}' recipes/<name>/recipe.yaml
Matches any {{ X }} not preceded by $. Any hit on a v1 recipe (schema_version: 1) is a silent-failure candidate. Run as a pre-submission check; zero matches is the canonical state.
A follow-up optimizer check JIN-001 ("v0-style {{ X }} substitution in v1 recipe โ silently emitted as literal text") would catch this at gate-time alongside SEL-003 (v0 selector) and ABT-002 (v0 about-field). Tracked as skill TODO; not yet shipped.
Case study: python-confluent-kafka v0โv1 migration (Jun 14, 2026; PR conda-forge/python-confluent-kafka-feedstock#127). Hand-rolled recipes/confluent-kafka-python/recipe.yaml (an in-flight v1 draft predating this gotcha) carried three distinct v0-jinja bugs that validate_recipe + optimize_recipe both passed cleanly:
build.script: last entry โ {{ PYTHON }} -m pip install . -vv --no-deps --no-build-isolation. Would have failed at install with shell "command not found: {{".requirements.host: librdkafka >={{ version }} and requirements.run: librdkafka >={{ version }}. Would have failed the solver with an unparseable spec.source.patches: - 0001-Fix-setup-for-windows.patch # [win]. The v0 comment selector would have been silently ignored; the Windows-only patch would have applied on every platform and broken the Linux/macOS build at pip install time.All three were caught by manual SKILL.md-driven audit before the local build ran. Conversion to ${{ X }} and if: win / then: produced a clean recipe; subsequent CI on PR #127 went 39/39 green across linux_64 / linux_aarch64 / linux_ppc64le / osx_64 / osx_arm64 / win_64 ร 6 Python variants each + linter + aggregator.
The cost of the audit was ~5 minutes; the cost of not doing it would have been one CI iteration per bug (3 ร ~20 min on the slowest emulated platform) plus reviewer churn.
is_python_min flag when python_min is overridden upwardSymptom: a Rust+PyO3 (or other abi3-binary) recipe uses the canonical skip: is_abi3 and not is_python_min matrix-collapse rule + a recipe-local conda_build_config.yaml that overrides python_min above the conda-forge-pinning default (e.g. python_min: "3.11" for a pyo3/abi3-py311 package while pinning's default is '3.10'). After conda-smithy rerender, the regenerated .ci_support/<platform>_is_python_mintruepython3.10.____cpython.yaml shows:
is_abi3: [true]
is_python_min: [true] # zip-keyed with python
python: [3.10.* *_cpython] # โ still py3.10 from pinning's first entry
python_min: ['3.11'] # โ honors the override
The build then runs against py3.10 (because variant python is 3.10.*) and pip-install fails: cocoindex requires Python >=3.11. The python axis and python_min are now contradictory in the same .ci_support file.
Why: conda-smithy's variant_algebra.variant_add performs a UNION merge between pinning's python axis and the recipe-local CBC python_min. The override of python_min is honored, but python's first entry is preserved unchanged (smithy doesn't infer that the new min should shift the zip-key's true position). The zip-key is_python_min: [true, false, false, false] remains aligned with pinning's python: [3.10.*, 3.11.*, ...], so position 0 (py3.10) keeps is_python_min=true even though python_min now says '3.11'.
Attempts to also override python: in the recipe-local CBC hit G22 (wrong suffix syntax) or local-build Zip key elements do not all have same length: is_python_min errors.
Fix: use the rustworkx-pattern skip rule instead โ it sidesteps the zip-key entirely:
build:
skip: not (match(python, python_min ~ ".*") and is_abi3)
python:
version_independent: ${{ is_abi3 }}
# recipe/conda_build_config.yaml
python_min:
- "3.11"
match(python, python_min ~ ".*") compares the variant's python value against python_min ~ ".*" (jinja string-concat, NOT regex โ renders "3.11" + ".*" โ "3.11.*"). For variants where python is 3.10.*, the match fails and the variant is skipped. Only the variant whose python matches python_min survives. With python_min: "3.11", the one surviving variant builds against py3.11 correctly.
Used by: rustworkx-feedstock, cocoindex-feedstock. The Variant A skip (is_abi3 and not is_python_min) used by tree-sitter-typescript etc. still works fine for recipes whose abi3 minimum matches pinning's default (currently 3.10); only switch to Variant B when overriding upward.
Case study: cocoindex PR #11 (https://github.com/conda-forge/cocoindex-feedstock/pull/11). Three iterations needed: (a) initial is_abi3 and not is_python_min + 4-entry CBC python: override crashed smithy rerender with G22; (b) simplified to CBC python_min: "3.11" only โ smithy collapsed the matrix but mis-aligned python: [3.10.*], builds failed with requires Python >=3.11; (c) switched to not (match(python, python_min ~ ".*") and is_abi3) โ single py3.11 variant per platform, abi3audit clean (0 ABI version mismatches), all 8 CI legs green including win_64 fat-LTO in 9 min 45 s.
Full pattern + companion configs (version_independent, python-abi3, cross-Python test, abi3audit step) in reference/abi3-matrix-collapse.md.
python: override using *_cpython suffix crashes smithy on py3.13+Symptom: a recipe-local conda_build_config.yaml overrides the variant python axis with what looks like uniform syntax:
python:
- 3.11.* *_cpython
- 3.12.* *_cpython
- 3.13.* *_cpython # โ wrong suffix
- 3.14.* *_cpython # โ wrong suffix
conda-smithy rerender crashes with:
File "conda_smithy/variant_algebra.py", line 63, in _version_order
return ordering.index(v)
ValueError: '3.13.* *_cpython' is not in list
The rerender service marks itself failed; the regenerated .ci_support/*.yaml files don't update; subsequent CI runs against the OLD configs fail with whatever cascade follows.
Why: conda-forge-pinning's python axis uses different suffix tags per Python version:
| Python | Pinning suffix | Source |
|---|---|---|
| 3.10, 3.11, 3.12 | *_cpython |
conda_build_config.yaml (base pinning) |
| 3.13 | *_cp313 |
migrations/python313.yaml |
| 3.13 freethreading | *_cp313t |
migrations/python313t.yaml |
| 3.14 | *_cp314 |
migrations/python314.yaml |
| 3.14 freethreading | *_cp314t |
migrations/python314t.yaml |
The _version_order helper builds an ordering list from these exact strings. An entry that doesn't match (3.13.* *_cpython is wrong; the pinning has 3.13.* *_cp313) is rejected by ordering.index(v).
Fix โ don't override python: axis: for the abi3 matrix-collapse pattern, override only python_min and pick the right skip rule (G21):
# recipe/conda_build_config.yaml โ minimal override
python_min:
- "3.11"
# recipe.yaml โ Variant B skip rule
build:
skip: not (match(python, python_min ~ ".*") and is_abi3)
This delegates all python-axis bookkeeping to smithy + pinning. The match() rule filters at recipe-render time without requiring the recipe to know suffix syntax.
Fix โ if you must override python: axis: use the canonical suffix per version:
python:
- 3.11.* *_cpython
- 3.12.* *_cpython
- 3.13.* *_cp313
- 3.14.* *_cp314
is_python_min:
- true
- false
- false
- false
This works but is rarely needed โ the abi3 collapse pattern doesn't require it. Live reference for the suffix-per-version convention: conda-forge-pinning-feedstock's recipe/migrations/python3{13,14}.yaml.
Detection: any recipe-local conda_build_config.yaml that lists multiple python entries with uniform *_cpython is suspect when the list includes 3.13 or 3.14. One-line grep:
grep -nE '3\.(13|14)\..* \*_cpython' recipes/*/conda_build_config.yaml
A non-empty result is a smithy-rerender-crash candidate.
Case study: cocoindex PR #11 first push (https://github.com/conda-forge/cocoindex-feedstock/pull/11). The recipe-local CBC carried the wrong-suffix block above. Smithy rerender's run-task job crashed with the ValueError; all 20 build jobs (4 py ร 5 platforms) ran against the stale .ci_support/*.yaml and failed cascade-style with Jinja template error: undefined variable in condition 'is_abi3'. Resolution: dropped the python: axis override entirely, kept only python_min: "3.11", switched skip rule per G21. Rerender succeeded on the next push.
sed + powershell in build.script hits cmd.exe escape hell โ use sed (with m2-sed on Windows) for a single cross-platform lineSymptom: a recipe needs to rewrite something in upstream's source tree at build time (typically a pyproject.toml version field, a hardcoded path, or a build-system requires line). The author writes platform-conditional script entries โ sed -i ... on Unix, powershell -Command "(Get-Content X) -replace ..." | Set-Content X on Windows. Unix passes; Windows fails with the regex either silently producing the wrong result OR crashing rattler-build's script runner with exit 255.
Inspecting the actual command cmd.exe ran reveals the mangle: a regex written as ^version = "[^\"]*"$ was passed to powershell as ^version = "[^"]*"$ (note the ^ from the character class is gone). The replacement string and outer pattern survived; only the ^ from [^...] was eaten.
Why: cmd.exe treats ^ as its line-continuation / escape character outside of double-quoted strings. The YAML literal-block | โ cmd.exe "..." argument โ PowerShell -Command body โ PowerShell single-quoted regex string has FOUR layers of quoting/escaping. cmd's ^-stripping happens during the cmd-to-powershell hand-off, BEFORE powershell sees the regex. The first ^version (line-anchor at the start of the regex) survives because it's inside cmd's "...". The second ^ (inside [^"]) is in a structurally-identical position but cmd interprets the surrounding \" escape differently and ends up outside its own quoted region for a moment โ long enough to eat the ^.
This isn't unique to powershell: any cmd command that passes a regex with ^ in a char class hits the same trap. Adding more backslashes to escape (\\^, ^^) makes it worse on Unix.
Fix โ single cross-platform line via sed:
requirements:
build:
- if: win
then: m2-sed # conda-forge ships GNU sed on Windows as `m2-sed`
# (provides `sed.exe` in $PREFIX). On Unix the system sed
# is in the build container natively; no dep needed.
build:
script:
- sed -i 's/^version = .*/version = "${{ version }}"/' pyproject.toml
- ${{ PYTHON }} -m pip install . -vv --no-deps --no-build-isolation
sed -i works identically on Linux, macOS, and Windows (with m2-sed). The ${{ version }} is rendered by jinja before the shell ever sees it, so there's no shell-level templating to break.
sed's -i flag is -i on GNU sed everywhere conda-forge ships it โ including m2-sed on Windows. BSD sed (macOS system sed) requires -i '' or -i.bak, but the conda-forge build container uses GNU sed via the sed build dep on Unix too. If your recipe doesn't already pull a build-prefix sed, add sed (Unix) + m2-sed (Win) to be explicit.
Fix โ when sed isn't enough (multi-line, TOML structure, jinja loops): check a small Python helper file into the recipe directory and invoke it via ${{ PYTHON }} ${RECIPE_DIR}/helper.py (Unix) + "%PYTHON%" "%RECIPE_DIR%\helper.py" (Win). $PKG_VERSION and $RECIPE_DIR are set on both platforms. The helper avoids inline-string quoting entirely. This is what conda-forge/tree-sitter-swift-feedstock#5 ended up shipping after the inline powershell escape hell โ a recipe/fix_pyproject_version.py with os.environ["PKG_VERSION"] + re.sub. Works but is more code than a single inline sed line.
Anti-pattern โ powershell -Command "(...)-replace...regex with ^...": avoid entirely. The cmd-to-powershell quoting boundary breaks in non-obvious ways. If you find yourself reaching for it, switch to sed + m2-sed build dep instead.
Case study: tree-sitter-swift-feedstock #5 (Jun 2026). Upstream alex-pinkus/tree-sitter-swift hardcodes pyproject.toml [project].version = "0.0.1" across all tagged releases, so the conda-built wheel ships tree_sitter_swift-0.0.1.dist-info which then breaks pip check for any downstream with an upper-bounded constraint like tree-sitter-swift<0.9,>=0.7 (caught on conda-forge/graphifyy-feedstock#8). First-pass fix used inline sed (Unix) + powershell (Win) for the version rewrite; Azure buildId 1539671 win_64 failed with the cmd-ate-the-caret regex mangle. Second-pass used a checked-in Python helper which worked but bloated the recipe. Canonical pattern for next time: sed with m2-sed build dep, single inline line, no Python helper.
dist-info version โ when upstream's pyproject.toml hardcodes a placeholder, pip check fails downstreamSymptom: a recipe's validate_recipe + build + own pip_check: true test all pass locally. The conda artifact ships with the right version label (<pkg>-<X.Y.Z>-*.conda). A downstream feedstock that pins this package with an upper bound โ e.g. <pkg> >=0.7,<0.9 โ then fails its own pip check test on conda-forge CI with:
<downstream-pkg> X.Y.Z has requirement <pkg><0.9,>=0.7, but you have <pkg> 0.0.1.
Even though mamba list in the downstream test env shows <pkg> 0.7.3 per the conda metadata. The discrepancy is real, not a display bug: pip check reads each installed package's wheel-bundled dist-info/METADATA Version: line, NOT the conda repodata version. If the upstream pyproject.toml's [project].version is a placeholder (commonly "0.0.1") that was never bumped to match the tag scheme, the wheel ends up with <pkg>-0.0.1.dist-info/ even though the conda package label is correctly 0.7.3.
Why: many tree-sitter-grammars-style projects (alex-pinkus/tree-sitter-swift was the case-study example) maintain their version through Git tags + a release script that updates language-specific bindings (cargo, npm, gem) but doesn't touch pyproject.toml. PyPI is also stuck at the placeholder for the same reason. Anaconda's main channel + conda-forge's feedstock both ship the conda-label-correct package; only the bundled wheel metadata is stale.
Detection: after building (locally or on CI), extract info/recipe/recipe.yaml from any same-package downstream's failed-CI artifact and look at the actual error. Or proactively scan a built .conda:
python3 -c "
import zipfile, zstandard, tarfile, io, sys
with zipfile.ZipFile(sys.argv[1]) as z:
data = z.read([n for n in z.namelist() if n.startswith('pkg-')][0])
with zstandard.ZstdDecompressor().stream_reader(io.BytesIO(data)) as r:
with tarfile.open(fileobj=r, mode='r|') as tar:
for m in tar:
if 'dist-info/METADATA' in m.name:
f = tar.extractfile(m)
print(m.name.split('/')[-2]) # dist-info dir name
for line in f.read().decode().split('\n')[:5]:
print(' ', line)
break
" <pkg>-<X.Y.Z>-*.conda
Compare the printed dist-info dir name (e.g. tree_sitter_swift-0.0.1.dist-info) against the conda filename version (tree-sitter-swift-0.7.3-...). If they differ โ even by one PATCH number โ the package is poisoned for downstream pip check.
Fix โ downstream patch at the feedstock: rewrite the [project].version field in upstream's pyproject.toml at build time using a sed substitution driven by $PKG_VERSION (per G23's canonical pattern):
requirements:
build:
- if: win
then: m2-sed
build:
script:
- sed -i 's/^version = .*/version = "${{ version }}"/' pyproject.toml
- ${{ PYTHON }} -m pip install . -vv --no-deps --no-build-isolation
This is idempotent โ if upstream ever resumes proper version bumping, the sed runs against the correct value and is a no-op. The fix is forward-compatible and self-healing.
Also file an upstream issue asking maintainers to bump pyproject.toml [project].version to track tag releases. Even if they fix it for the next release, the wheel-bundled metadata for already-tagged versions on PyPI stays broken, so the downstream patch stays relevant for any feedstock that pins to an existing version range.
Scope: this trap is most common in tree-sitter language grammars (parser.c is auto-generated from grammar.js; the Python binding is an afterthought to the C work and gets version-neglected), but applies to ANY conda recipe building a Python wheel from a source that has placeholder version metadata. Other ecosystems with the same risk profile: language-server-protocol implementations, generated-binding wrappers around C libraries, monorepo subpackages where only the parent is version-tracked.
Survey approach for new tree-sitter-style recipes: before submitting a recipe whose downstreams may pin upper bounds, scan ALL platforms' built artifacts' dist-info to confirm the version inside matches the conda label. The CFE skill's tests/ could add a regression-test helper for this; today it's a manual check.
Case study: graphifyy 0.8.40 โ pip check rejected tree-sitter-swift 0.0.1 < 0.7 on conda-forge/graphifyy-feedstock#8 even though conda-forge had shipped tree-sitter-swift-0.7.3-*.conda for weeks. Surveyed all 22 tree-sitter-* feedstocks rxm7706 maintains; only tree-sitter-swift had the major mismatch (one minor mismatch in tree-sitter-powershell 0.26.5/0.26.4 was within graphifyy's >=0.26,<0.28 range so harmless). Fix shipped as conda-forge/tree-sitter-swift-feedstock#5 (merged 2026-06-17) with the canonical pattern above + dynamic tag: ${{ version }}-with-generated-files. The dist-info trap had been latent on conda-forge for the entire tree-sitter-swift 0.7.x lifetime โ only surfaced when a downstream with pip_check: true pinned to a version range upstream's placeholder excluded.
Root-cause variant (v8.68.0) โ django-style alpha VERSION tuple, NOT a placeholder and NOT setuptools_scm. A third mechanism produces the same labelโ dist-info poison: upstream declares VERSION = (X, Y, 0, "alpha", 0) in __init__.py with __version__ = django.utils.version.get_version(VERSION), and setup.cfg reads version = attr: <pkg>.__version__. Django's get_version() renders an alpha/sub-0 tuple as X.Y.dev<timestamp> โ so every from-source build of the tag self-identifies as a dev build, on any build machine, regardless of git metadata (distinguish from G39's setuptools_scm fallback). Fix at the feedstock: a source patch flipping the release segment to "final" (cleaner than the sed-the-version form โ it fixes __version__ at runtime too), same version at a bumped build number, plus a version-assert script test (python -c "from importlib.metadata import version; assert version('<pkg>') == '<ver>'") so the regression can't ship silently. Live case: django-cryptography-django5 2.2 builds _0.._4 all shipped Version: 2.2.dev20251005171540 (feedstock issue #6, fixed by PR #7 โ _5), false-failing django-sql-explorer's upstream-exact ==2.2 pin for the package's entire channel lifetime.
pkg[extra] upstream dep into run:, and resolve each extra to its actual conda packages (which may be renamed)Symptom: a recipe whose upstream pyproject.toml declares a dependency with an extras marker โ dbgpt[client,cli,agent,code,...], httpx[socks], uvicorn[standard], celery[redis] โ installs and its own pip_check passes locally, but imports fail at runtime with ModuleNotFoundError for something the extra was supposed to pull, or a downstream feedstock's pip_check fails with <pkg> X has requirement <extra-dep>...; not satisfied. The recipe's run: listed only the base package, or a guessed <pkg>-<extra> name that isn't on conda-forge.
Why (two interlocking facts):
Conda has no extras mechanism. PEP 508 extras (pkg[extra]) are pip-only: installing pkg[extra] pulls pkg plus the deps under pkg's [project.optional-dependencies].extra. A conda pkg is built once, without extras, and the solver cannot activate an extra at install time. So every pkg[extra] in an upstream dep list must be flattened โ the recipe carries pkg AND the union of that extra's deps directly in requirements.run. This bites hardest in multi-output recipes: output B may depend on A[extra] where A is a sibling output in the same recipe; A is built without extras, so ${{ pin_subpackage('A', exact=True) }} does NOT pull A's extra-deps โ B must list them itself.
An extra's marker name โ the conda package it resolves to. httpx[socks] does not pull a package called httpx-socks โ it pulls whatever httpx's pyproject.toml lists under [project.optional-dependencies].socks, which is socksio. The conda dep is socksio. Guessing <pkg>-<extra> is wrong twice: that's usually a different PyPI project, and it often isn't on conda-forge at all. Read the extra's actual contents, then map each dep to its conda name (G10's four-spelling rule applies).
pip_check: true is the enforcement mechanism: pip check reads the installed wheel's METADATA and sees Requires-Dist: <dep>; extra == "socks" for every extra the consumer requested, then fails if those deps aren't installed. A recipe that activates pkg[extra] but omits the flattened deps fails pip check โ loudly in the building feedstock, or (worse) only in a downstream feedstock's CI, because the builder's own pip_check covers just the package being built, not its dependents (see G24 for the same downstream-only failure mode).
Fix:
pkg[extra1,extra2,...], fetch upstream's pyproject.toml and read [project.optional-dependencies]. Union the base [project.dependencies] with every activated extra's list.httpx[socks]โsocksio).requirements.run. In a multi-output recipe, expand each sibling's activated extras past the pin_subpackage โ the pin alone is not enough.pkg[gpu] / pkg[proxy_qianfan] if no output requests them โ over-listing bloats the run env and can pull packages that aren't on conda-forge for no reason.Detection โ before trusting a generated (especially multi-output) recipe, grep upstream's dep declarations for the extras-bracket and reconcile each against run::
# Every pkg[extra] in the upstream pyproject(s)
grep -rEo '[A-Za-z0-9_.-]+\[[A-Za-z0-9_,-]+\]' packages/*/pyproject.toml
# What a given extra actually pulls:
python -c "import tomllib,sys,json; d=tomllib.load(open(sys.argv[1],'rb')); print(json.dumps(d['project'].get('optional-dependencies',{}),indent=2))" packages/<pkg>/pyproject.toml
# What an extra on an external dep resolves to (the httpx[socks]โsocksio class):
curl -s https://pypi.org/pypi/httpx/json | python -c "import sys,json; print([r for r in json.load(sys.stdin)['info']['requires_dist'] if 'extra' in r and 'socks' in r])"
# โ ['socksio==1.*; extra == \"socks\"'] (conda dep is socksio, NOT httpx-socks)
Case study: DB-GPT v0.8.0 multi-output spec audit (Jun 17, 2026; docs/specs/db-gpt-conda-forge.md). The dbgpt-app output depends on dbgpt[client,cli,agent,simple_framework,framework,code,proxy_openai,proxy_tongyi,proxy_zhipuai] + dbgpt-ext[rag,storage_chromadb], where dbgpt/dbgpt-ext are sibling outputs in the same recipe, built without extras. The first-draft spec listed only pin_subpackage on the siblings + a handful of direct deps, missing ~50 of the flattened 68 external run-deps; pip_check would have failed at CI. Two extras-resolution traps surfaced: (a) httpx[socks] โ the spec guessed httpx-socks (absent from conda-forge) when the real dep is socksio (present); (b) qianfan was listed as required but lives only in the non-activated proxy_qianfan extra โ dropped. Reading every activated extra from the dbgpt-core/dbgpt-ext pyproject, deduping, and mapping to conda names produced the correct 68-dep set and caught both.
A follow-up optimizer check DEP-006 ("upstream dep uses pkg[extra] โ verify the extra's deps are flattened into run:, and that any sibling-output extra is expanded past its pin_subpackage") would catch this at gate-time alongside G10's proposed DEP-005. Tracked as a skill TODO; not yet shipped.
== pins to >= requires patching the source pyproject when pip_check: true โ the wheel METADATA bakes in the ==Symptom: a recipe follows the project's pin-loosening convention (rewrite every upstream pkg==X run-dep to pkg >=X); validate_recipe + optimize_recipe + check_dependencies are all clean and the package builds โ then the test phase fails at pip check:
dbgpt 0.8.0 has requirement aiohttp==3.8.4, but you have aiohttp 3.14.1.
Why: loosening the recipe's requirements.run only changes what conda installs. It does NOT change the built wheel's dist-info/METADATA, which still carries upstream's Requires-Dist: aiohttp==3.8.4 verbatim from pyproject.toml. pip_check: true reads that METADATA and enforces the ==, so the loosened conda install (aiohttp 3.14.1) violates it. Worse, the pinned version usually isn't even on conda-forge (the reason you loosened), so there is no way to satisfy the ==.
This is the mirror image of G24 / the graphifyy upper-bound rule: G24 says mirror upstream's load-bearing upper bounds in run:; G26 says when you loosen an upstream ==, you must also loosen it in the wheel METADATA โ i.e. patch the source โ or pip_check rejects the build.
poetry-core caveat โ patch ALL THREE files; beware the local-vs-CI divergence (v8.43.0). When the build backend is poetry-core, conda-forge's poetry-core builds the wheel METADATA from the sdist's bundled PKG-INFO, NOT from pyproject.toml. So a sed that patches only pyproject.toml passes locally (the local poetry-core happens to regenerate metadata from pyproject.toml) but fails on staged-recipes CI (CI's poetry-core reuses PKG-INFO's Requires-Dist) โ a green local pip check does NOT prove the CI wheel METADATA is loosened. A poetry sdist can carry the pin in three files โ pyproject.toml (pkg = "X"), setup.py (['pkg==X']), and PKG-INFO (Requires-Dist: pkg (==X)) โ so patch all three to be robust to whichever the backend reads:
script: |
sed -i -E 's/^pkg *= *"X"/pkg = ">=X"/' pyproject.toml
sed -i -E 's/pkg \(==X\)/pkg (>=X)/' PKG-INFO
sed -i -E 's/pkg==X/pkg>=X/g' setup.py
${{ PYTHON }} -m pip install . -vv --no-deps --no-build-isolation
And verify the loosen is runtime-safe (the newer version on cf must keep the API the consumer uses) โ don't loosen blindly across a major bump. Case: pksuid 1.1.2 (staged-recipes #33895, Jun 25 2026) loosened pybase62==0.4.3โ>=0.4.3 (cf ships 1.0.0, which keeps encodebytes/decodebytes); the pyproject-only sed passed locally but failed CI pip check until PKG-INFO + setup.py were also patched.
Fix: rewrite the == pins to >= in the source pyproject.toml at build time, before pip install, so the wheel METADATA matches the loosened conda deps. A targeted regex that touches only PEP 508 version pins (name==N), leaving </<= caps and extra == "..." / python_version == "..." markers untouched:
build:
script: |
sed -i -E 's/([A-Za-z0-9])==([0-9])/\1>=\2/g' pyproject.toml
${{ PYTHON }} -m pip install . --no-deps --no-build-isolation -vv
The ([A-Za-z0-9])==([0-9]) guard is load-bearing: it matches aiohttp==3.8.4 (alnum before ==, digit after) but NOT extra == "cli" (space + quote) or python_version=='3.9' (quote after ==). Verify on the real file (grep -nE '[A-Za-z0-9]==[0-9]' should be empty after; the file must still parse as TOML). For multi-line/structural rewrites use a checked-in Python helper instead (per G23). noarch:python builds run on one platform (linux), so a plain sed needs no m2-sed/Windows handling.
In a multi-output recipe, patch the pyproject of whichever output originates the pin, not just the one that fails: pip_check on output B (which requires A[extra]) reads A's wheel METADATA for the extra's deps, so loosening A's pyproject.toml (base + every extra) fixes both A's own test and B's.
Case study: DB-GPT v0.8.0 (Jun 17, 2026). The 7-output recipe loosened 13 == pins per the project convention, but the first build failed at the dbgpt (core) test with pip check: aiohttp==3.8.4, but you have aiohttp 3.14.1 (+ chardet, importlib-resources). Fix: the sed above in the dbgpt-core + dbgpt-ext output build scripts (the only two carrying == pins at that time; dbgpt-app read its loosened extras transitively via pip check). After the patch the wheel METADATA read aiohttp>=3.8.4 / psutil>=5.9.4; extra=='cli' and pip_check passed. The spec's pin-loosening story (FR-2/S10) had assumed loosening the conda run-deps was sufficient โ it wasn't. (dbgpt-client later gained its own G26 source patch for a load-bearing upper bound โ see the Story 13.2 extension below.)
G26 extension โ marker-split pins on a noarch: python recipe (Sep 2, 2026). The same METADATA rule applies when upstream declares two PEP 508 lines for one package split by python_version (or any marker conda-forge cannot encode in a single run: entry), and the feedstock collapses them to one range: loosening only the recipe run: is still insufficient โ you must patch every upstream marker branch in pyproject.toml to a single unified range (or remove all branches and add one line), or pip_check on the high-Python interpreter still reads the unpatched branch's Requires-Dist (e.g. onnxruntime>=1.26; python_version >= "3.14" while conda installed 1.25.x). A patch that replaces only the low branch and leaves the high branch untouched fails exactly this way. Case study: langflow-base / langflow-suite story 13.1 โ upstream splits onnxruntime at py3.14; the feedstock's collapsed >=1.20,<1.24 dropped cp314; fix = run-dep >=1.20 (no cap) + source patch collapsing both marker lines to "onnxruntime>=1.20".
G26 extension โ upper-bound caps on a sibling output (Story 13.2 shipped Sep 2, 2026; retro landed Sep 11, 2026). The same METADATA rule applies when upstream declares a load-bearing upper bound (<2.0.29, not an == pin) that conda-forge cannot satisfy on a newer Python: loosening the recipe run: alone leaves the wheel's Requires-Dist: SQLAlchemy>=2.0.25,<2.0.29 intact, and pip_check rejects the installed 2.0.5x build. This is the mirror image of G24's upper-bound mirroring rule โ when you loosen the cap you must patch the source too. Case study: dbgpt-client / db-gpt-feedstock story 13.2 โ upstream caps SQLAlchemy>=2.0.25, <2.0.29 (comment: "2.0.29 not support duckdb now"); the <2.0.29 range has no cp314 sqlalchemy build, blocking dbgpt-app (the platform sidecar) even though dbgpt and dbgpt-serve solve clean on 3.14. Fix: run-dep sqlalchemy >=2.0.25,<2.1 + source patch 0003-loosen-dbgpt-client-sqlalchemy-cap.patch on packages/dbgpt-client/pyproject.toml (the output that originates the pin โ not dbgpt-core/dbgpt-ext, which already carry G26 ==-pin sed scripts). The patch is a single-line hunk; hatchling reads pyproject.toml directly (no poetry-core PKG-INFO trap). After the patch, pip_check on py3.12 and py3.14 passes with sqlalchemy 2.0.52. Feedstock PR #4.
import in a split package can eagerly pull a sibling's submodule whose deps are only declared under an extraSymptom: a noarch: python recipe for one member of an upstream monorepo/workspace builds cleanly and pip check passes, but the import test fails โ ModuleNotFoundError: No module named 'bs4' โ with a traceback that walks through a sibling package's submodule:
import dbgpt_client โ dbgpt_client.schema
โ from dbgpt_ext.rag.chunk_manager import ChunkParameters
โ from dbgpt.rag.knowledge.base import ...
โ from bs4 import BeautifulSoup # ModuleNotFoundError
Why: dbgpt-client's __init__ eagerly imports a submodule that reaches into a sibling (dbgpt_ext.rag) which reaches into the core (dbgpt.rag.knowledge) which imports bs4. Upstream declares bs4 only under dbgpt-ext[rag] (an optional extra dbgpt-client does not activate). The dep is a hard import for anything that touches that code path, but upstream's [project.dependencies] under-declares it. import <bare core> doesn't trip it (the bare __init__ doesn't load the rag submodule), so only the consumer's import surfaces the gap โ and only the import test catches it (pip check is happy because the wheel doesn't declare bs4).
Fix: find the exact import closure empirically, then add the missing dep(s) to the importing output's run:. Probe in the failed output's surviving test_env (rattler-build leaves it under build_artifacts/<cfg>/test/test_<name>*/test_env):
# $TE/bin/python โ iteratively import, install each missing module, repeat
import importlib, subprocess, sys
seen = set()
while True:
try:
importlib.import_module('dbgpt_client'); print('OK', sorted(seen)); break
except ModuleNotFoundError as e:
m = e.name.split('.')[0]
if m in seen: print('STUCK on', m); break
seen.add(m)
subprocess.run([sys.executable, '-m', 'pip', 'install', '-q',
{'bs4': 'beautifulsoup4', 'docx': 'python-docx'}.get(m, m)])
Add only what the closure actually needs (the DB-GPT case needed exactly beautifulsoup4, nothing more) โ don't blanket-add the whole extra. Map import names to conda names (G7/G10). Put the dep on the output whose import fails; if several outputs import the same sibling submodule, the dep can go on the shared sibling's run: instead. Cheap pre-check: grep '^\(from\|import\) ' <pkg>/src/<mod>/__init__.py โ a trivial __init__ (only from ._version import ...) means the import test won't trip deep chains.
Case study: DB-GPT dbgpt-client (Jun 17, 2026). import dbgpt_client failed on bs4; the closure probe showed beautifulsoup4 was the only missing module. Added it to dbgpt-client's run: with a comment noting the under-declared transitive import. dbgpt-app already carried it via the [rag] flatten; the bare dbgpt/dbgpt-ext/dbgpt-sandbox/dbgpt-app import tests never tripped it (their __init__ files import only ._version).
dist-info version (e.g. conda-forge pdfminer.six โ 0.0.0) breaks pip_check for any dependent that pins it exactlySymptom: pip check on an output fails with a mismatch you didn't introduce and can't fix by editing your recipe:
pdfplumber 0.11.9 has requirement pdfminer.six==20251230, but you have pdfminer-six 0.0.0.
The conda package's own version is correct (conda-meta/*.json shows pdfminer.six 20251230), but its bundled wheel dist-info/METADATA reports Version: 0.0.0. A different package (pdfplumber) pins pdfminer.six==20251230 exactly, and pip check compares that against the bogus 0.0.0 string.
Why: a setuptools_scm-style fallback bug in the external feedstock (it built from a tarball without the git tag, so the wheel version resolved to 0.0.0) โ the same class as G24, but in a dependency you don't own. It is systemic across that package's builds, so no version pin of yours satisfies the exact-pinning dependent. The conda package functions correctly; only the metadata string is wrong, so the failure is a true false positive.
Fix (in priority order): (1) verify it's genuinely external and a false positive โ compare the conda version (conda-meta) against the dist-info Version:, and confirm the dependent's exact pin is what trips it; (2) file an issue against the broken <dep>-feedstock (the durable fix is theirs โ sed the real version into the wheel, per G24); (3) for your recipe, since there is no in-recipe fix, disable pip_check on the single affected output (not the whole recipe), with an inline comment citing the external bug, keeping pip_check: true everywhere else. Before disabling, run pip check manually in the test_env and confirm the broken-metadata line is the only failure โ if the rest of the graph is consistent you lose almost nothing:
tests:
# pip_check disabled for THIS output only: conda-forge <dep> ships dist-info
# Version 0.0.0 (feedstock metadata bug, issue #NNN) while the conda version
# is correct; <other-dep> pins it exactly so pip check false-fails. Verified
# the rest of the graph consistent. The other outputs keep pip_check.
- python:
imports: [<module>]
pip_check: false
Dropping the offending dependent is the alternative, but if it's pulled via an activated extra you must also remove it from that extra's declaration (else pip check flags it as missing), and you lose functionality.
Case study: DB-GPT dbgpt-app (Jun 17, 2026). After fixing G26 + G27, the only remaining pip_check failure across the 78-package dbgpt-app graph was pdfplumber โฆ pdfminer.six==20251230, but you have pdfminer-six 0.0.0. The conda pdfminer.six was correctly 20251230; its dist-info said 0.0.0 across every build on the channel. With the rest of the graph manually verified consistent (a strong signal the G25 flatten + Q2 caps were right), pip_check: false was set on the dbgpt-app output only, documented inline; the other six outputs kept pip_check: true.
optimize_recipe TEST-001 and check_dependencies silently ignore outputs[]Symptom: on a multi-output (recipe: + outputs:) recipe where every output has its own tests: and requirements::
optimize_recipe fires TEST-001 ("Recipe has no tests section") and validate_recipe warns "No tests section defined", even though all outputs carry proper CFEP-25 test blocks.check_dependencies returns {"found": [], "missing": []} โ it verified nothing.Why: both checks read the top-level recipe keys (tests:, requirements:), which a multi-output recipe legitimately lacks (they live under each outputs[i]). The checkers don't traverse outputs[]. So TEST-001 is a false positive and check_dependencies is a silent false-negative โ the dangerous kind, because "0 missing" reads as "all deps verified" when nothing was checked.
Fix: (1) Trust conda-smithy + rattler lint for multi-output structure โ validate_recipe's conda-smithy lint: passed + rattler_lint_ran: true are the real gates; the TEST-001 warning is noise when each output has tests. (2) To verify deps, flatten every output's run: into a throwaway single-output recipe and run check_dependencies on that:
# /tmp/depcheck/recipe.yaml
schema_version: 1
package: { name: depcheck, version: "0.0.0" }
build: { noarch: python }
requirements:
run: [ <every external dep from every output, version-stripped> ]
check_dependencies then resolves them via its batch-repodata path and suggests conda names for misses. In-flight prerequisite recipes from the same effort show as "missing" (not yet on conda-forge) โ expected; the local channel covers them at build time. (3) The build's per-output test phase is the ultimate dep check (each test-env solve must succeed).
A follow-up: teach optimize_recipe/check_dependencies to walk outputs[] (tracked as a skill TODO alongside the proposed DEP-005/DEP-006).
Case study: DB-GPT 7-output recipe (Jun 17, 2026). optimize_recipe flagged TEST-001 despite all 7 outputs having CFEP-25 test blocks; check_dependencies returned empty/empty. Verified the 68 external deps by flattening them into /tmp/dep-check/recipe.yaml โ all 67 conda-forge deps resolved (incl. renames graphvizโpython-graphviz, dockerโdocker-py, httpx[socks]โsocksio), and the 5 "missing" were exactly the in-flight prerequisites (auto-gpt-plugin-template + the 4 lyric-* recipes) the local channel supplied.
protobuf is the Python bindings (no protoc) โ Rust prost/tonic builds need libprotobuf; a stray host protoc masks it locallySymptom: a Rust recipe whose build invokes protoc (a crate with a build.rs calling tonic_build::compile_protos(...) or prost_build::compile_protos(...)) builds green locally but fails on conda-forge CI โ on every platform โ at the cargo/wheel build:
error: failed to run custom build command for `<crate> (.../build.rs)`
Error: Could not find `protoc`. If `protoc` is installed, try setting the
`PROTOC` environment variable to the path of the `protoc` binary. ...
๐ฅ maturin failed / ร Building wheel for <pkg> did not run successfully.
Why (two interlocking facts):
protobuf package is the Python bindings (google.protobuf) and ships NO protoc binary. The protoc compiler lives in libprotobuf (bin/protoc). So requirements.build: [protobuf] does not put protoc on the build PATH โ prost-build/tonic-build search PATH for protoc and find nothing. (protobuf as a runtime run: dep is correct and unrelated โ the trap is only protobuf-as-a-build-dep-expecting-the-compiler.)protoc leaks from the dev environment. If a libprotobuf is installed in the developer's pixi/conda env, its bin/protoc is on PATH and leaks into the local rattler-build build, so the broken recipe builds green. conda-forge CI is hermetic โ only the recipe's declared build deps are on PATH โ so the gap surfaces only on CI. Classic "works locally, fails on hermetic CI."Fix: use libprotobuf (not protobuf) in requirements.build. It provides bin/protoc on the build PATH on all platforms ($BUILD_PREFIX/bin/protoc on unix, %BUILD_PREFIX%\Library\bin\protoc.exe on win). protoc is a build-platform tool (it runs during the build to generate Rust), so it belongs in build: (not host:) โ also correct for cross-compilation, where the protoc that runs must be the build platform's.
requirements:
build:
- ${{ compiler("rust") }}
- ${{ compiler("c") }}
- ${{ stdlib("c") }}
- libprotobuf # provides bin/protoc for prost-build / tonic-build
If protoc-on-PATH still isn't found (rare), additionally export PROTOC="$BUILD_PREFIX/bin/protoc" inside the build script โ not via script.env:, which does not shell-expand (G1).
Hermetic local verification โ the general technique for any "works locally, fails on hermetic CI" tool gap is to hide the suspected stray host tool, then rebuild. The dev-env protoc is usually a relative symlink (protoc -> protoc-NN.N.N), so move the symlink aside (the real binary stays), confirm command -v protoc is empty, rebuild, then restore:
mv .pixi/envs/<env>/bin/protoc /tmp/protoc.bak # the symlink; target binary remains
command -v protoc # must print nothing
pixi run -e local-recipes recipe-build recipes/<name> # must now build via the libprotobuf build dep
ln -sf protoc-NN.N.N .pixi/envs/<env>/bin/protoc # restore (recreate the relative symlink)
If it builds green with the stray hidden, the build dep genuinely provides the tool. Watch the relative-symlink trap: [ -e <moved-symlink> ] reports it "missing" because its relative target no longer resolves from the new location โ the binary itself never moved.
Case study: lyric-py / staged-recipes #33764 (Jun 17, 2026). crates/lyric-rpc/build.rs runs tonic_build::compile_protos("proto/task.proto"). The recipe listed protobuf in build:; CI failed on linux_64 and osx_64 with Could not find protoc. The local build had passed only because .pixi/envs/local-recipes/bin/protoc (a relative symlink โ protoc-33.5.0, owned by libprotobuf 6.33.5) leaked onto the build PATH. Fix: protobufโlibprotobuf. Verified by hiding the stray protoc (command -v protoc โ none) and rebuilding โ all 4 py-variants built green sourcing protoc from the libprotobuf build dep.
python_min upward on an existing feedstock โ v1 needs a recipe-local conda_build_config.yaml (+ rerender); v0 needs {% set python_min %}; context.python_min is silently ignored in v1Symptom: an upstream version bump starts requiring a newer Python (e.g. datetime.UTC โ 3.11+, tomllib โ 3.11+, or an explicit requires-python >=3.12), so the noarch: python import/build test fails on the feedstock's default 3.10 floor. You raise python_min โ and CI still builds and tests on 3.10.
Why: on an existing feedstock the build's Python floor comes from the variant python_min baked into .ci_support/<cfg>.yaml (default 3.10 from conda-forge-pinning), NOT from the recipe's context:.
recipe.yaml): adding python_min: "3.11" to context: does not propagate to .ci_support on rerender (verified: conda-smithy rerender leaves .ci_support/*.yaml python_min at 3.10, empty diff). A local rattler-build that bypasses .ci_support does honor context.python_min, which masks the bug โ the feedstock CI does not. You must add a recipe-local recipe/conda_build_config.yaml with python_min: ["3.11"]; conda-smithy reads the CBC and writes 3.11 into .ci_support on rerender.meta.yaml): the jinja {% set python_min = "3.11" %} at the top of the recipe shadows the .ci_support variant at render time, so it overrides directly โ no CBC, no rerender needed (.ci_support stays 3.10 but the build/test use 3.11).Fix:
# v1: recipe/conda_build_config.yaml (then `@conda-forge-admin, please rerender`)
python_min:
- "3.11"
{# v0: top of recipe/meta.yaml #}
{% set python_min = "3.11" %}
Keep the ${{ python_min }} / {{ python_min }} references in host/run/test; the override supplies the value. This is the existing-feedstock counterpart to the "declare python_min in context:" guidance in Python Version Policy โ that context form is for fresh staged-recipes submissions; on a rendered feedstock the variant wins.
Case study: llms-py-feedstock #39 (Jun 2026) โ llms/main.py imports datetime.UTC (3.11+); the 3.10 floor failed the import test. context.python_min: "3.11" left .ci_support at 3.10 after rerender; a recipe-local conda_build_config.yaml flipped it to 3.11 (built + tested green on 3.11). wagtail-sharing-feedstock #5 (v0) used {% set python_min = "3.12" %} for upstream's requires-python >=3.12 and built on 3.12 with .ci_support still at 3.10.
Symptom: a [bot-automerge] autotick PR has a red CI leg. Roughly half are transient infrastructure flakes (restart โ green); half are real recipe defects (a restart reproduces them). And once you have a fix, a plain git push from a gh pr checkout clone lands a stray branch on the canonical feedstock instead of updating the PR.
Transient FLAKE โ @conda-forge-admin, please restart ci (recipe is correct; no edit): the failure is pre-build (env-provisioning / source fetch) and other platforms/Pythons of the same run pass. Signatures:
failed to fetch <pkg>.conda โฆ dispatch task is gone โฆ runtime dropped the dispatch task โ pixi/CDN connection drop.Fatal Python error: _Py_HashRandomization_Init: failed to get random numbers โ Windows agent entropy glitch.Connection broken: IncompleteRead(... bytes read, ... more expected) / CondaMultiError mid-download.HTTP 503 / error sending request for url during base-env provisioning or source download.Real FIX (deterministic; a restart reproduces it): ImportError: cannot import name 'X' on a stdlib name โ python floor (G31); pip check <pkg> A has requirement B==X, but you have Y โ dep pin (mirror upstream, G24/G26); BackendUnavailable: Cannot import 'hatchling.build' โ host build-backend dep wrong (upstream switched backend, e.g. poetry-coreโhatchling under --no-build-isolation); <pkg> does not exist / no candidates were found โ a dependency feedstock is behind/absent (blocked on that feedstock โ see kiota/sqlglot below); linter package.version โฆ interpreted as a floating-point number โ quote version: (G14).
Pushing the fix to the PR: a gh pr checkout <n> clone's origin is the upstream feedstock, not the bot fork, so git push creates a stray branch on the canonical repo (delete it with git push origin --delete <branch>). Update the PR via a maintainer-edit push to the bot fork:
git push https://github.com/regro-cf-autotick-bot/<feedstock>.git HEAD:<pr-head-branch>
("Allow edits from maintainers" is on by default for bot PRs, so a base-repo maintainer can push.) Then @conda-forge-admin, please rerender per the rerender-after-push rule, and always build + test the fix locally first. When a fix is blocked on a prerequisite dependency feedstock (the dep version upstream pins isn't on conda-forge yet), don't loosen blindly โ the clean path is to land the dep first; G26's source-patch loosen is the fallback when waiting isn't viable.
Case study: the 2026-06-17 batch โ 12 autotick PRs triaged: 4 pure flakes (cocoindex/html-to-markdown/dlt/selectolax โ restart), 6 real fixes pushed via maintainer-edit (python floor, dep bumps, backend swap, setuptools cap), 2 blocked on prerequisite dep feedstocks (microsoft-kiota-bundle on 5 sibling 1.10.3 bumps; collate-sqllineage on sqlglot 29.0.1).
.ci_support as a variant config; its strict channel_sources excludes the just-built package from its own testSymptom: rattler-build build --recipe recipe.yaml --variant-config .ci_support/linux_64_.yaml โฆ builds the package, then the test env solve fails with <pkg> ==<ver> โฆ excluded because due to strict channel priority not using this option from: 'file:///โฆ/<out>/' and/or <dep> ==<X>, for which no candidates were found โ even when the dep is on conda-forge.
Why: .ci_support/<cfg>.yaml carries channel_sources: [conda-forge] (strict priority). rattler-build applies it to the test solve too, so the local output-dir channel (where the just-built package lives) is excluded by strict priority and the solve can't see the new package. On real CI, build_steps.sh wires the local channel correctly; the bare local invocation does not.
Fix: build with the conda-forge-pinning CBC (for ${{ python_min }} resolution) and let rattler-build use its default channel + auto-added output-dir โ do not pass .ci_support for a local test:
pixi run -e local-recipes rattler-build build --recipe recipe/recipe.yaml \
--variant-config .pixi/envs/local-recipes/conda_build_config.yaml \
--output-dir <out>
Distinguish this (a channel artifact of your invocation) from a genuine no candidates caused by a dependency feedstock being behind: re-check the dep's max version with conda search -c conda-forge "<dep>==<X>". Beware the misread: conda search prints - <dep>==<X> for a not-found spec (a PackagesNotFoundError listing) โ easy to mistake for a match; a real hit prints a full <name> <version> <build> <channel> row.
Case study: collate-sqllineage-feedstock #33 (Jun 2026) โ first local rattler-build with .ci_support failed with "strict channel priority" excluding the just-built package; re-running with only the pinning CBC removed that error and surfaced the real blocker โ sqlglot ==29.0.1 genuinely absent from conda-forge (max 28.10.1), initially misread as available from a not-found - sqlglot=29.0.1 listing.
pkg_resources.declare_namespace packages (e.g. fs / Pyfilesystem2) break under setuptools 81+ โ ModuleNotFoundError: No module named 'pkg_resources'Symptom: a recipe's import test fails at import <X> with ModuleNotFoundError: No module named 'pkg_resources', and the traceback originates in a dependency's __init__.py, not the package being built. The dep imported fine until recently.
Why: setuptools 81 stopped vendoring pkg_resources by default and 82 removed it. Any installed dependency that runs __import__("pkg_resources").declare_namespace(__name__) at import time (legacy namespace packages โ fs 2.4.16 / Pyfilesystem2 is the canonical case) then fails once the solver pulls setuptools โฅ81, which it does by default because the dep declares only a bare, un-ceilinged setuptools.
Fix: the durable fix is in the provider feedstock (cap setuptools <81 in its run deps, or drop declare_namespace). For the consumer recipe whose test is failing, cap it in requirements.run with a TODO:
run:
- <X>
# <dep> calls pkg_resources.declare_namespace at import; setuptools 81+
# removed pkg_resources. TODO: drop once <dep>-feedstock caps setuptools.
- setuptools <81
Case study: fs.googledrivefs-feedstock #7 (Jun 2026) โ import fs failed (fs 2.4.16 declares its namespace via pkg_resources) because the test env resolved setuptools 82.0.1. Capped setuptools <81 in run; import + pip check passed.
Symptom: grayskull / AI emits a per-Python numpy run-dep selector on a noarch: python recipe, e.g.:
run:
- if: match(python, "<3.13")
then: numpy >=1.26.4
else: numpy >=2.1.0
This is a G12 violation โ a noarch:python recipe carries one artifact, so per-Python run: selectors are invalid (the conda-smithy lint rejects them, and there is no single artifact for which a per-Python branch makes sense).
Why: the upstream PEP 508 markers numpy>=1.26.4; python_version<"3.13" / numpy>=2.1.0; python_version>="3.13" encode numpy's own cp313 wheel-availability floor โ numpy 2.1.0 was the first release to ship cp313 wheels, so a project that wants to be installable on Python 3.13 has to floor numpy at 2.1.0 there. This is a property of numpy's release history, not a feature of the package that branches per Python version. There is no code path in the package that behaves differently on 3.12 vs 3.13.
Fix: collapse to the broadest floor โ a single un-selectored run: dep:
run:
- numpy >=1.26.4
The install-time conda solver picks a numpy build compatible with the installed Python (on Python 3.13 it can only choose numpy โฅ2.1, because that's the first cp313 numpy on conda-forge anyway), so the broad floor is correct on every Python. The noarch artifact stays valid; no noarch_platforms, no per-Python branch.
Case study: langchain-google-community 5.0.0 (Jun 19, 2026) โ grayskull emitted the match(python, "<3.13") numpy split verbatim from upstream markers; collapsed to numpy >=1.26.4 and the noarch recipe linted + built clean.
run: dep โ pip check fails even though the conda solve succeedsSymptom: a build's import test passes the conda solve, then pip check fails with <outer-pkg> A has requirement <dep> !=โฆ,<X,>=โฆ, but you have <dep> Y โ for a dep whose conda run: constraint the solver did satisfy. The conda metadata and the wheel METADATA disagree.
Why: a single version of a package can have two builds on conda-forge โ an older build whose conda run: dep was loosened (e.g. mcp <2.0.0) but whose embedded wheel METADATA still bakes in the tighter upstream cap (mcp !=1.21.1,<1.23,>=1.19.0), and a newer metadata-patched build whose conda dep was corrected to match. pip check reads the wheel METADATA, not the conda repodata. The solver, seeing only the loose conda run: dep on the stale build, is free to pick that stale build alongside a transitive dep that's too new for the wheel's real cap โ so the conda solve succeeds but pip check fails.
Fix: raise the outer package's lower bound in your recipe so the solver is forced past the stale build to one whose wheel METADATA and conda dep agree:
run:
# >=2.14 forces past fastmcp's stale 2.13.3 build (loose conda dep,
# tight wheel METADATA cap on mcp); 2.14's metadata-patched build agrees.
- fastmcp >=2.14
This is a strict subset of upstream's intent (you're only excluding builds known to mis-declare), not a loosening. Distinguish from G24 (labelโ dist-info on the built package) โ G36 is a transitive dep's stale build poisoning your test.
Case study: toolguard (Jun 19, 2026) โ pip check failed on mcp because the solver picked fastmcp's stale 2.13.3 build (conda run: mcp <2.0.0, wheel METADATA mcp !=1.21.1,<1.23,>=1.19.0). Floored fastmcp >=2.14 to land on the metadata-patched build; pip check passed.
[tool.uv] no-build / source flags in an sdist are NOT runtime dependencies โ read deps only from [project.dependencies]Symptom: a recipe gains a phantom dependency (or a real dep gets misidentified) after reading a line like apify-shared = false from an sdist's pyproject.toml and treating it as a constraint.
Why: [tool.uv] (and [tool.uv.sources], [tool.uv.*]) is uv-tool-specific configuration, not PEP 621 metadata. A line such as apify-shared = false under [tool.uv] is a uv no-build / source flag (e.g. "don't build this from source"), not a dependency declaration. The authoritative runtime dep list is PEP 621 [project.dependencies] (and [project.optional-dependencies]). Mistaking a uv flag for a dep both adds a non-existent blocker and hides the real one, because attention goes to the wrong line.
Fix: read deps exclusively from [project.dependencies] / [project.optional-dependencies]. Never source a dependency name or constraint from [tool.uv.*], [tool.hatch.*], [tool.poetry.group.*.dependencies] build-tool sections, or similar tool tables. When a dep looks odd, cross-check it against [project.dependencies] before adding it to the recipe.
Case study: apify-client 3.0.3 (Jun 19, 2026) โ apify-shared = false under [tool.uv] was misread as a dependency constraint; the real blocker was impit (a genuine [project.dependencies] entry not yet on conda-forge). Ignoring the uv flag and re-reading [project.dependencies] surfaced impit as the actual prerequisite.
Symptom: a consumer recipe's test-env solve fails to find a local-only dependency that you did build into the local channel โ e.g. impit ==0.13.0, for which no candidates were found, even though impit-0.13.0-py310...conda is sitting in the output dir.
Why: a compiled prereq (Rust/PyO3, C-extension, anything not noarch) produces one build per Python version (py310, py311, โฆ). A noarch dep ships a single artifact that satisfies every Python, but a compiled one does not โ if you built impit only for py310, a consumer whose recipe requires python >=3.11 (and whose test env therefore resolves 3.11+) cannot use the py310 build. The solver correctly reports "no candidates" because there is no py311 build of that compiled dep in the channel.
Fix: when a consumer's test-env solve fails on a compiled local-only dep, check the channel artifact's py3XX build-string segment against the consumer's python_min / required Python floor. Build the compiled prereq for every Python in the consumer's matrix (or at least the one the consumer's env will resolve) before building the consumer. Contrast with noarch local-only deps, where one build is enough.
Case study: apify-client (Jun 19, 2026, python >=3.11) โ its test env couldn't resolve the channel's impit-0.13.0-py310... build (impit is a Rust/PyO3 compiled package, py310-only in the local channel). Rebuilding impit for py311 unblocked the apify-client solve.
_version_helper import breaks at metadata generation (cf ships setuptools_scm 8.x)Symptom: the conda-forge build fails during metadata generation with ImportError: cannot import name '_types' (or Configuration, fallbacks, or anything under version.*) from setuptools_scm. The sdist wires its version via [tool.setuptools.dynamic] version = {attr = ...} pointing at a _version_helper.py / _version.py that imports private setuptools_scm internals.
Why: conda-forge ships setuptools_scm / vcs_versioning 8.x+, which removed the private internals (_types, Configuration, fallbacks, version.*) that older sdists reach into. The helper module was written against a pre-8.x private API that no longer exists, so importing it during attr-resolution explodes before any dependency even loads.
Fix: ship a checked-in, idempotent helper (e.g. pin_version.py) in the recipe dir that rewrites pyproject.toml to a static version ($PKG_VERSION) and strips the dynamic-version machinery (the [tool.setuptools.dynamic] block and the _version_helper reference). Run it as the FIRST build step. Then drop setuptools-scm / gitpython from host (no longer needed once the version is static). This sidesteps the broken private-API import entirely.
Case study: pymilvus-model 0.3.2 (Jun 19, 2026) โ its _version_helper.py imported _types from setuptools_scm, which cf's 8.x lacks; a pin_version.py first-build-step rewrite to a static $PKG_VERSION + dropping setuptools-scm/gitpython from host fixed it.
Symptom: a noarch: python consumer with python_min: "3.10" fails its py3.10 test leg to resolve a dependency's latest build, even though the dep is on conda-forge โ the dep's newest version raised its own Python floor and no py3.10 build exists for it.
Why: dependencies raise their Python floor between releases. ibm-watsonx-ai 1.5.x needs python >=3.11; 1.3.37 was the last py3.10-compatible line. A noarch consumer declaring python_min 3.10 must resolve every hard dep on its py3.10 test env โ if the only available build of a dep is py3.11+, the py3.10 leg has no candidate. This is G38's per-Python-build problem seen from the version axis rather than the compile axis: even a noarch dep can be effectively py3.11-only if its py3.10-compatible versions are all older than what the solver wants.
Fix โ two correct responses:
python_min to match the dep's floor.Always collapse the dep's per-Python env markers to the broadest floor (G35) so the consumer's declared floor stays valid โ don't leave a per-Python selector that silently narrows resolution.
Per-platform variant (compiled transitive dep). The floor can also be forced by a compiled transitive dep that ships a Python version on some conda subdirs but not others. A noarch consumer must install on every platform at its declared floor, so a per-platform gap raises the whole package's honest floor (you cannot have a per-platform python_min on a single noarch artifact). Diagnose by querying each subdir's repodata for the dep's pyXY build strings: curl -s conda.anaconda.org/conda-forge/<subdir>/repodata.json and grep the dep at the pinned version. Fix per (b): raise python_min to the floor that holds on every platform.
Run this check PROACTIVELY โ before submitting, not after a red osx leg. Every case study below is reactive (CI failed โ diagnosed), but the cheap pre-submit check turns an opik-style red round-trip into a no-op. For each compiled transitive dep, parse every candidate build's depends for its python constraint, filter builds to the recipe's pinned version range, and confirm the consumer's floor (default py3.10) is covered on every target subdir (osx-64 + win-64 are the usual laggards; linux is already proven by the local build):
# per (sub in osx-64, win-64): for builds of <dep> in [<lo>,<hi>), collect the python-minor each supports
import re; from packaging.version import Version
def pymin(depends):
for d in depends:
m=re.match(r'python\s+>=3\.(\d+)', d)
if m: return '3.'+m.group(1)
cov={pymin(v['depends']) for v in pk.values() if v['name']=='<dep>' and Version('<lo>')<=Version(v['version'])<Version('<hi>')}
print('py3.10 covered?' , '3.10' in cov, sorted(c for c in cov if c))
If the floor is not covered on some subdir, raise python_min to the floor that holds everywhere (the opik fix). A clean result means the default floor is safe โ submit without a bump (langchain-google-vertexai's pyarrow/bottleneck/numexpr all covered py3.10 โ no bump, green first try). See also G66 for the verification-depth-by-dep-shape rule.
Case study: ibm-watsonx-ai / langchain-ibm / langflow (Jun 19, 2026) โ langchain-ibm (noarch, py3.10) needed ibm-watsonx-ai, whose 1.5.x line dropped py3.10; resolved by building 1.3.37 (last py3.10 line) into the channel. Per-platform case (Jun 26, 2026): langchain-litellm #33917 โ noarch, declared py3.10, but its transitive compiled dep fastuuid 0.14.0 (pulled via litellm) ships py3.10 on linux-64/win-64 yet only py3.11+ on osx-64, so the osx-64 py3.10 test leg had no candidate (osx-only failure). Real floor is 3.11; set python_min: "3.11". Adversarial-review catch (Jun 27, 2026) โ db-gpt's dbgpt-app (noarch, declared all-platform) hard-deps lyric-py, a compiled package shipping only linux-64/osx-64/win-64 (0 builds on osx-arm64/linux-aarch64) โ uninstallable on Apple Silicon + ARM Linux. The green local build AND green-able staged-recipes CI both miss it (staged-recipes has no ARM legs, G77); a 3-lens adversarial review of the spec surfaced it. Reinforces the proactive per-subdir check: a recipe/spec's "all-platform" claim must be verified against live per-subdir repodata, not build/CI status. Fix = platform-expand the dep's feedstock (conda-forge/lyric-py-feedstock #2).
requires-pythonSymptom: a recipe declares requires-python >=3.10 (and you set python_min: "3.10"), but the package genuinely cannot run on 3.10 โ an ImportError for a stdlib symbol surfaces at import time on the py3.10 leg.
Why: a transitive hard dep imports a Python-version-gated stdlib symbol unconditionally, with no fallback. jsonquerylang imports typing.NotRequired (PEP 655, py3.11+) in every version, with no try/except ImportError or typing_extensions shim. That forces the real python_min of any consumer to 3.11+, regardless of the consumer's declared requires-python >=3.10 โ which is then aspirational / broken on 3.10. The upstream's stated lower bound is not trustworthy as the effective floor.
Fix: when a recipe's hard deps include a package with such an unconditional new-stdlib / PEP-655 import, set python_min to the MAX of all transitive hard Python floors โ don't trust the upstream requires-python lower bound alone. Inspect the actual imports of hard deps when a declared floor looks suspiciously low.
Case study: langflow / langflow-base (Jun 19, 2026) โ declared >=3.10, but its dep jsonquerylang unconditionally imports typing.NotRequired (py3.11+), making the genuine floor 3.11.
Symptom: you reach for a compiled-package recipe template (stdlib, compiler, per-Python builds) based on a package's reputation as a binary wheel, but the latest version is actually pure-Python โ or vice versa.
Why: a package's build shape can change across versions. milvus-lite was a C++ binary-wheel through 2.4 / 2.5, but 3.0 is a complete pure-Python rewrite (faiss-cpu / pyarrow backend, py3-none-any wheel, Root-Is-Purelib: true, zero .so). Inferring "compiled" from an older version's reputation produces an over-complex recipe and may even make it look cf-unsubmittable when it isn't.
Fix: always inspect the LATEST version's actual sdist/wheel before choosing a template โ check for .so / .pyd presence and Root-Is-Purelib in the wheel WHEEL/RECORD. The pure-Python path is simpler AND cf-submittable; don't pay the compiled-recipe complexity tax (or write off the package) based on a stale shape.
Case study: milvus-lite 3.0 (Jun 19, 2026) โ assumed compiled (true for 2.4/2.5), but 3.0 ships a py3-none-any Root-Is-Purelib: true wheel with no .so; a noarch:python recipe is correct and cf-submittable.
# comment on a list item trips conda-smithy's comment-selector lint โ use a full-line comment ABOVE insteadSymptom: conda-smithy lint flags a comment-selector lint error on a recipe.yaml (v1) list item that carries a trailing # comment โ even when the comment is plain prose inside the extra: block, not an actual selector.
Why: in recipe.yaml v1, conda-smithy parses a trailing # comment on a YAML list item as a potential selector and flags it. The selector heuristic doesn't distinguish "real selector" from "trailing prose", so any inline trailing comment on a list entry is a lint risk.
Fix: use a FULL-LINE comment ABOVE the item instead of a trailing one. This reinforces the comments-at-bottom convention (v8.31.0 / G31-equivalent): cfe rationale belongs in the bottom # CFE comments block, and any necessary list-item annotation that must stay in the body has to be a full line above the item, never trailing.
Case study: langflow recipe cfe-* block (Jun 19, 2026) โ a trailing # comment on an extra: list item lint-failed under conda-smithy; relocating it to a full line above cleared the lint.
Symptom: asked to package a .NET (C#) CLI for conda-forge. None of the generators apply (it isn't PyPI / npm / CRAN / CPAN / LuaRocks), so generate_recipe_from_pypi & friends are useless. Attempting a from-source build is a dead end: there's no dotnet SDK to invoke, and even if there were, dotnet restore is a network operation that conda-forge's hermetic CI forbids.
Why: conda-forge has no dotnet-sdk and no dotnet-runtime feedstock (verified 2026-06-20 via lookup_feedstock โ both exists=False), and the NuGet restore step is banned in the offline build sandbox. So a from-source .NET build cannot work on conda-forge today โ fundamentally unlike Rust / Go, which DO have conda-forge toolchains plus a vendored-dependency story. Don't burn cycles trying to make dotnet build work; there is no toolchain to drive it.
Fix: repackage upstream's self-contained, single-file release binaries (one per platform/arch). .NET "self-contained" deployment bundles the runtime into the executable, so there is NO run-dep on a dotnet runtime โ the binary runs standalone. This is also how Homebrew distributes these tools. Recipe shape (binary-repackage, NOT noarch):
source:
# One single-file source per conda subdir, gated by target-platform
# selectors. file_name gives the download a stable name; no archive
# extraction happens (the asset has no archive extension). sha256 per asset.
- if: linux and x86_64
then:
url: https://github.com/<org>/<repo>/releases/download/v${{ version }}/<tool>-linux-x64
sha256: <...>
file_name: <tool>
# ... linux/aarch64, osx/x86_64, osx/arm64, win ...
build:
number: 0
script:
- if: unix
then:
- mkdir -p "${PREFIX}/bin"
- cp <tool> "${PREFIX}/bin/<tool>"
- chmod +x "${PREFIX}/bin/<tool>"
- if: win
then:
- copy /Y <tool>.exe "%LIBRARY_BIN%\<tool>.exe"
tests:
- script:
- <tool> --version
Key points: no compiler() / stdlib() (nothing is compiled from source โ STD-001 does not apply); no requirements in the simple case (runtime is bundled); NOT noarch (each artifact is a per-platform binary โ cfe-noarch: compiled); ship LICENSE in-recipe (the bare binaries carry none โ canonical License-File pattern (2)); set cfe-source-kind: github-release-binary.
Caveats:
cfe-on-conda-forge-status + cfe-forge-blocker-list.musl, linux-arm (armv7), win-x86, win-arm64 have no standard conda-forge subdir; the matchable set is the standard 5 (linux-64, linux-aarch64, osx-64, osx-arm64, win-64). State the dropped arches explicitly.<tool> --version passes locally, but a clean conda env can fail at startup with Couldn't find a valid ICU package installed on the system. If that surfaces, add icu (and possibly openssl) to run:, or document DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1. Leave run: minimal until real-world verification beyond the build host demands otherwise.curl -sSL <url> | sha256sum), don't download-then-hash 5 ร ~80 MB to disk.Case study: cyclonedx-cli 0.32.0 (Jun 20, 2026) โ CycloneDX/cyclonedx-cli, an Apache-2.0 .NET 8 SBOM CLI. dotnet-sdk + dotnet-runtime both absent from conda-forge โ no source path. Repackaged the 5 self-contained release binaries (linux-x64/arm64, osx-x64/arm64, win-x64) as a per-platform recipe with zero run-deps; dropped the upstream musl / linux-arm / win-x86 / win-arm64 assets (no cf subdir). linux-64 leg built GREEN and the packaged binary ran cyclonedx --version โ 0.32.0 (exit 0); the other 4 legs were sha256-verified but not executed locally (cross-platform on a linux-64 host). Kept local-only (cfe-on-conda-forge-status: pending-submission-to-conda-forge) pending the binary-repackage submission decision.
Symptom: asked to "package import or run from a conda env. Forcing it onto staged-recipes is the wrong move; reviewers will (correctly) reject it.
Why: conda-forge packages locally-runnable CLIs, libraries, and apps built from a published, versioned, downloadable source. A browser SPA frequently fails several of those bars at once, and the failures are upstream facts you can't fix:
"private": true in package.json โ upstream explicitly forbids npm publication.npm pack from the registry) has nothing to pull.main; nothing stable to pin (conda-forge discourages commit-pinned sources for new submissions).bin โ it's a web SPA served by a web server, not a command. The right distribution is exactly what such projects already offer: a hosted site and/or a Docker image.Viability gate โ run this BEFORE writing any recipe (cheap, decisive):
PKG=$(curl -s "https://raw.githubusercontent.com/<org>/<repo>/main/package.json")
echo "$PKG" | python3 -c "import sys,json; d=json.load(sys.stdin); print('name',d.get('name'),'private',d.get('private'),'bin',d.get('bin'))"
NM=$(echo "$PKG" | python3 -c "import sys,json;print(json.load(sys.stdin).get('name',''))")
curl -s "https://registry.npmjs.org/${NM}" | python3 -c "import sys,json;d=json.load(sys.stdin);print('npm:', d.get('error','PUBLISHED'))"
curl -s "https://api.github.com/repos/<org>/<repo>/releases" | python3 -c "import sys,json;print('releases:',[r['tag_name'] for r in json.load(sys.stdin)])"
curl -s "https://api.github.com/repos/<org>/<repo>/tags" | python3 -c "import sys,json;print('tags:',[t['name'] for t in json.load(sys.stdin)])"
If it's private / unpublished / untagged โ not conda-forge-submittable. Tell the user, recommend the hosted site or Docker, and only build local-only if they still want it installable in their own channel.
Local-only build pattern (when the user opts in despite the above): build the static site and ship it + a launcher as a noarch: generic package.
source โ GitHub archive pinned to a main commit (no tag exists); context.version tracks package.json's version, context.commit the SHA. On any "update", bump both together (no autotick path).build: noarch: generic + script: { file: build.sh }; build dep nodejs >=20.export BASE_URL=./ (relative asset paths so the site serves from any directory โ most Vite configs read base from an env var), npm ci (lockfile present), then npx vite build โ skip the upstream vue-tsc -b / tsc type-check step (dev-only; it doesn't change dist/ and can break on a main-HEAD checkout). Copy dist/. to $PREFIX/share/<name>/, write a $PREFIX/bin/<name> launcher that serves it: exec python -m http.server "${PORT}" --directory "$PREFIX/share/<name>". run dep python for the launcher.license_file: LICENSE resolves from the extracted GitHub archive (pattern 1).tests: test -f .../index.html + test -x bin/<name> + a package_contents.files check (the SPA itself can't be exercised headless).cfe-source-kind: github-commit, cfe-noarch: generic, cfe-on-conda-forge-status: blocked-pending-prerequisites, and a cfe-forge-blocker-list entry naming the upstream blockers (private / unreleased / not-on-npm / SPA).Caveats:
npm ci (NETWORK). Fine for a local build; conda-forge CI forbids it โ another reason this is local-only, not submittable.index.html references relative ./assets/... paths (proves BASE_URL=./ took effect) so the static server resolves them.bin, published to the registry โ that uses the canonical npm pack pattern and can go to conda-forge). G45 is specifically the private/unreleased browser-SPA case with no CLI surface.Case study: cyclonedx-bom-studio 0.9.2 (Jun 20, 2026) โ CycloneDX/cyclonedx-bom-studio, an Apache-2.0 Vue 3 + Vite SBOM editor. package.json is "private": true, not on npm, no releases/tags, no bin โ confirmed not submittable. Built local-only: pinned to main commit f2a6d30b, npm ci + npx vite build (BASE_URL=./) into a noarch:generic package shipping the 56-file static site under share/cyclonedx-bom-studio/ + a cyclonedx-bom-studio launcher (python -m http.server). Built GREEN on linux-64; verified the packaged index.html used relative ./assets/ refs. cfe-on-conda-forge-status: blocked-pending-prerequisites with the upstream blockers recorded. Same session also mirrored + locally built the two submittable siblings already on conda-forge โ cyclonedx-python-lib (multi-output) and cyclonedx-bom (PyPI cyclonedx-bom) โ the contrast is the point: the Python lib/CLI belong on conda-forge; the browser SPA does not.
meta.yaml's noarch: python flag can be WRONG for a genuinely-compiled package โ the CURRENT sdist is the source of truth (the "local-meta-is-wrong" sibling of G42)Symptom: a feedstock-refresh / regenerate pass copies the local mirror's old meta.yaml shape forward and emits a noarch: python recipe โ but the package is actually a compiled C / Cython / C++ extension. The build then either fails (no compiler/stdlib declared) or, worse, "succeeds" as a noarch artifact that ships no native .so and is broken at import on every platform.
Why: an older meta.yaml in recipes/<name>/ is a historical artifact โ it may predate the package gaining a C extension, may have been authored noarch by mistake, or may reflect a different upstream era. Trusting its noarch: flag (or its absence of compiler()/stdlib()) when choosing the regenerated recipe's shape carries the stale mistake forward. This is the complement of G42: G42 warns against trusting a package's reputation (it was compiled once, assume it still is); G46 warns against trusting the stale local meta.yaml (it was authored noarch once, assume it still should be). Both resolve the same way โ inspect the current version's actual artifact.
Fix: before fixing the build shape on a refresh, verify compiler / native-extension presence in the current sdist (don't trust the local meta's noarch flag):
curl -sL "https://pypi.org/packages/source/<x>/<name>/<name>-<version>.tar.gz" -o /tmp/s.tgz
tar tzf /tmp/s.tgz | grep -E '\.(c|cpp|cc|pyx|pxd|h|hpp|rs)$' | head # any โ compiled
tar -xzOf /tmp/s.tgz '*/setup.py' 2>/dev/null | grep -E 'Extension|cythonize|build_ext' # setuptools C-extension markers
tar -xzOf /tmp/s.tgz '*/pyproject.toml' 2>/dev/null | grep -E 'cython|maturin|scikit-build|setuptools.*ext' # build-backend markers
If the sdist carries .c/.pyx/.cpp/.rs sources or setup.py builds an Extension, the recipe is compiled (cfe-noarch: compiled) โ drop noarch: python, add compiler() + stdlib(), and let the per-Python build matrix run. Update the cached cfe-noarch field to the verified shape so future regens don't relapse.
Case study: bulk sole-maintainer feedstock refresh (Jun 20, 2026) โ the local meta.yaml for hll (a C extension whose import is HLL) and skranger (Cython/C++ wrapping the ranger random-forest library) both carried a stale noarch: python flag. The current sdists clearly ship native sources; both are genuinely compiled. The regenerated recipes were corrected to compiled (compiler + stdlib + per-Python matrix), not noarch.
conda_build_config.yaml (a verbatim copy of the global pinning CBC) breaks the build โ lint errors + a variant duplicate entry collision; rattler-build auto-discovers itSymptom: a compiled recipe in recipes/<name>/ carries a git-tracked conda_build_config.yaml next to recipe.yaml, and:
conda-smithy lint fails with baseline-violation errors โ MACOSX_DEPLOYMENT_TARGET / c_stdlib_version below the current conda-forge baseline (because the file is a frozen copy of an old global pinning CBC), and/orduplicate entry "<key>" (e.g. duplicate entry "libitk_devel") โ the recipe-local CBC's keys collide with the pinning CBC that the local-build harness layers in.Why: rattler-build (and conda-smithy) auto-discover a conda_build_config.yaml adjacent to the recipe and merge it into the variant config. A recipe-local CBC is meant to carry recipe-specific overrides only (e.g. python_min, a single pinned dep). When the file is instead a stale verbatim copy of the global conda-forge-pinning CBC (zero recipe-specific keys), every one of its hundreds of keys is re-declared on top of the real pinning the harness already supplies โ producing duplicate-key collisions at build time โ and its frozen baseline values (MACOSX_DEPLOYMENT_TARGET, c_stdlib_version) lint as below-baseline. The deployed conda-forge feedstocks carry no such recipe-dir CBC (the pinning comes from the rerender service), so the file is pure local cruft.
Fix (sanctioned): move it aside for the build, restore after, and flag the tracked file for deletion:
mv recipes/<name>/conda_build_config.yaml recipes/<name>/conda_build_config.yaml.bak
pixi run -e local-recipes recipe-build recipes/<name> # builds clean against the real pinning
mv recipes/<name>/conda_build_config.yaml.bak recipes/<name>/conda_build_config.yaml # restore for review
Then flag the tracked CBC for deletion in the refresh diff โ it should not be committed (the deployed feedstock has none). Keep a recipe-local CBC only when it carries genuine recipe-specific keys (e.g. an upward python_min override per G21 / G31); a zero-override copy of the global CBC is always cruft.
Detection: a recipe-dir conda_build_config.yaml whose key set is a superset of (or identical to) the global pinning CBC, with no recipe-unique keys, is the signature. diff <(sort recipes/<name>/conda_build_config.yaml) <(sort .pixi/envs/local-recipes/conda_build_config.yaml) near-empty โ cruft.
Case study: bulk sole-maintainer feedstock refresh (Jun 20, 2026) โ hll and jh2 both carried git-tracked recipe-dir conda_build_config.yaml files that were stale copies of the global pinning CBC. They triggered conda-smithy lint baseline errors (MACOSX_DEPLOYMENT_TARGET / c_stdlib_version) and a hard duplicate entry "libitk_devel" collision at build time. mv โฆbak for the build (clean), restored for review, flagged the tracked files for deletion (the deployed feedstocks have no such CBC).
Third failure mode (2026-09-09) โ the stale copy HIJACKS zip_keys, so the merge dies before ANY recipe renders; and a copy at the recipes/ ROOT does it to every recipe. The two symptoms above are local-build symptoms. In the staged-recipes Docker lane the same file fails earlier and far more loudly:
ValueError: All entries associated by a zip_key field must be the same length.
In /opt/conda/conda_build_config.yaml, numpy and python are different (1 and 4)
Note which file the message names โ the container's current pinning, not the stale one. That is the tell. .ci_support/build_all.py merges every variant config through conda_build.variants.combine_specs, which resolves zip_keys from the last spec that declares it and then re-validates every spec against that group. A pre-2026 pinning snapshot still groups ['python', 'numpy', 'python_impl', 'is_python_min']; current pinning has numpy: [2] (one entry) and four python entries, and no longer zips them. So the stale file's zip_keys is imposed on the live pinning and the merge raises โ before a single recipe is rendered, which is why the error never mentions the recipe or the stale file.
Two consequences the earlier symptoms do not cover:
recipes/ ROOT (recipes/conda_build_config.yaml) breaks EVERY recipe. build_all.py loads it ahead of the per-recipe glob, and build_steps.sh's "keep only TEST_RECIPE" prune deletes sibling directories โ a file sitting directly in recipes/ survives it. There is no legitimate use for one: per-recipe overrides belong in recipes/<name>/conda_build_config.yaml.channel_sources is read from THIS file, not from conda-forge.yml. build_all.py opens recipes/<name>/conda_build_config.yaml looking for channel_sources, splits its first entry on commas, and prefixes the local build channel. That is the one key worth adding a recipe-local CBC for when a recipe's deps live off conda-forge (channel_sources: [conda-forge,<channel>]) โ see recipes/openfeature-provider-flagd/ and recipes/bmad-suite/. Deleting a stale bulk copy loses nothing here as long as its channel_sources was just conda-forge; check before deleting.Replay the merge locally instead of burning a CI run โ it needs no Docker, and the repo-root conda_build_config.yaml is the tracked verbatim copy of the pinning the image ships:
from collections import OrderedDict
import yaml, conda_build.variants
from conda_build.config import Config
specs = OrderedDict()
for p in ["conda_build_config.yaml", ".ci_support/linux64.yaml", "<candidate CBC>"]:
specs[p] = conda_build.variants.parse_config_file(p, Config(), loader=yaml.SafeLoader)
conda_build.variants.combine_specs(specs, log_output=False) # raises on a stale zip_keys
tests/meta/test_recipe_cbc_merges_with_pinning.py runs exactly this over every recipes/*/conda_build_config.yaml and asserts no root-level one exists, so a reintroduced snapshot fails in the suite rather than on a dispatched Linux run.
Case study 2: the Linux lane's fifth and last blocker (2026-09-09). recipes/conda_build_config.yaml was a May-2025 verbatim pinning snapshot inherited at repo bootstrap (python 3.9โ3.12, numpy 1.22โ1.26, the retired MACOSX_DEPLOYMENT_TARGET key) and broke every recipe; 14 per-recipe copies of the same vintage each broke their own (13 bulk copies of 96โ428 keys, plus openlineage-suite's 3-key "exceptionally reinstate python 3.7 support" override, stale since the floor moved to 3.11). All 15 deleted; the 30 legitimate 1โ5-key recipe-specific CBCs kept. A/B-verified with the replay above: 15 failing spec sets before, 0 after.
compiler('cxx')Symptom: a package's GitHub repo is dominated by Rust (or Go) code, so you reach for the full compiled-recipe apparatus โ compiler('rust') / compiler('cxx'), stdlib('c'), a long build-time budget, maybe cargo-bundle-licenses. But the actual conda build is tiny and needs none of it; a speculative compiler('cxx') is dead weight.
Why: the language a project is written in on GitHub is not the same as what the PyPI sdist's build backend does at install time. Some "Rust" or "Go" Python packages ship a PEP-517 backend that downloads a precompiled native artifact at build time rather than compiling from source. The sdist has no Cargo.toml / .rs / .go โ the backend fetches a prebuilt binary (e.g. a C-API shared lib) and wraps it. The build is fast (tens of seconds), needs no Rust/C++ toolchain in the recipe, and a compiler('cxx') you added "because it's Rust" never gets used.
Fix: inspect the sdist's actual contents and build backend before sizing the build or adding compilers:
tar tzf /tmp/sdist.tgz | grep -E 'Cargo\.toml|\.rs$|\.go$|go\.mod' | head # empty โ no from-source Rust/Go compile
tar -xzOf /tmp/sdist.tgz '*/pyproject.toml' | grep -A3 '\[build-system\]' # what backend actually runs
No Cargo.toml/.rs in the sdist + a backend that fetches a binary โ it's a download-and-wrap build: no compiler('rust')/compiler('cxx'), no stdlib, a short build. Add a compiler macro only when the sdist genuinely compiles native sources (G46's check). Drop any speculative compiler('cxx'). This pairs with G42/G46 โ always size the build from the sdist, never from reputation or the GitHub language bar.
Case study: wasmtime-py (Jun 20, 2026) โ the repo is a Rust project, but the PyPI sdist ships no Cargo.toml / .rs: its PEP-517 backend downloads a precompiled wasmtime C-API binary at build time (build wall-clock ~46 s). A speculative compiler('cxx') was correctly dropped โ the recipe needs no Rust/C++ toolchain at all.
Cargo.toml / setup.py before adding version_independent + python-abi3; a per-Python artifact needs a SIMPLE imports+pip_check test, not the CFEP-25 cross-version triadSymptom: a stale Rust/PyO3 recipe carries version_independent: ${{ is_abi3 }} + a python-abi3 host dep + matrix-collapse skip rule (and sometimes an OPENSSL_DIR hack), but the package's Cargo PyO3 features declare no abi3 (py_limited_api = false). The recipe is treated as a single abi3 artifact when it's really a per-Python compiled build โ so the CFEP-25 cross-version test triad (python_version: [${{ python_min }}.*, "*"]) is applied to it and the "*" (latest-Python) test leg can't load a .so built for a different Python.
Why: version_independent + python-abi3 are correct only when upstream actually builds abi3 (py_limited_api=True / pyo3/abi3-py3XX Cargo feature) โ one wheel covers all Pythons. When the Cargo PyO3 features carry no abi3, every Python gets its own compiled artifact, and there is no single artifact testable across Pythons. Applying the CFEP-25 triad to a per-Python artifact is a category error (a py311 .so cannot be imported by the "*"-resolved newest Python). This is the build-shape sibling of G38: G38 is about consumers of a per-Python artifact; G49 is about authoring the per-Python recipe's test correctly.
Fix: verify abi3 vs per-Python from Cargo.toml / setup.py, mirror the deployed feedstock, and pick the matching test shape.
tar -xzOf /tmp/sdist.tgz '*/Cargo.toml' | grep -iE 'abi3|py_limited_api|pyo3.*features' # abi3-* / py_limited_api=true โ abi3
pyo3/abi3-py3XX or py_limited_api=True): keep version_independent + python-abi3 + the matrix-collapse skip (G21 Variant B). One artifact โ the CFEP-25 cross-version triad is appropriate.version_independent, python-abi3, and any speculative OPENSSL_DIR hack. Use a simple per-Python test โ imports: + pip_check: true with no python_version triad (or python_version: ${{ python_min }}.* only):tests:
- python:
imports: [<module>]
pip_check: true
# per-Python compiled artifact: no cross-version triad โ a single
# per-Python .so cannot be tested against a different Python.
Case study: json-stream-rs-tokenizer (Jun 20, 2026) โ the stale recipe wrongly carried version_independent + python-abi3 + an OPENSSL_DIR hack, but the Cargo PyO3 features declare no abi3 (py_limited_api=False) โ it's a per-Python compiled build. Fix: dropped all three, mirrored the deployed feedstock's per-Python shape, and used a simple imports+pip_check test (no CFEP-25 triad).
match(python, ">=N") (NOT py<N, per G3)Symptom: a compiled recipe builds fine on py3.10โ3.12 but fails on py3.13 (or whatever the newest matrix entry is) โ either a C compile error (error: '_PyInterpreterState_Get' undeclared / a removed private C-API symbol) or a host-solve failure (a pinned dep has no wheel/build for the newest cpython, e.g. numpy<2.0 with no cp313 build).
Why: each CPython release removes private/unstable C-API symbols and bumps the supported-numpy floor. A compiled extension (or a host numpy<2.0 pin) that was valid through 3.12 can hard-fail on 3.13+ because the symbol is gone (C compile) or no compatible dependency build exists (host solve). conda-forge keeps adding newer Pythons to the matrix faster than every upstream supports them; the recipe must mirror upstream's actually-published range, not the full conda-forge matrix.
Fix: skip the unsupported Pythons with the v1 match() form โ build.skip: match(python, ">=N") โ never the v0 py<N form, which is silently ignored in v1 (see G3):
build:
skip:
- match(python, ">=3.13") # upstream's private C-API / numpy<2.0 floor โ drops cp313
Set the ceiling to match upstream's published wheel/Python range (check the latest sdist's requires-python upper bound and the actual PyPI wheel tags). When the cap is forced by a host dep (e.g. numpy<2.0 with no cp313 wheel), document that in the cfe-comments block so a future numpy-2 migration can lift the skip.
Case study: bulk feedstock refresh (Jun 20, 2026) โ psycopg2-yugabytedb failed the py3.13 C compile (_PyInterpreterState_Get removed in 3.13), and datasketches failed the py3.13 host solve (its numpy<2.0 host pin has no numpy-1.x cp313 wheel). Both got build.skip: match(python, ">=3.13") mirroring upstream's supported range โ not py<313, which v1 ignores.
Symptom: a recipe sourced from a GitHub tag/archive (per the usual G5/G16 "prefer GitHub source" instinct) builds to a tiny .conda (~15 KiB) that is missing its runtime assets โ a web app's www/ bundle, generated parsers, compiled protobufs, minified JS/CSS, etc. โ and is broken at runtime, even though the build EXIT=0 and the import test (if shallow) passes.
Why: many monorepo Python packages generate web/static assets at release time (a build step that runs npm run build / webpack / a codegen pass) and ship the generated output only in the published PyPI wheel, never committing it to the GitHub repo. A from-source build of the GitHub subdir therefore packages an empty shell โ the www/ (or equivalent) directory simply isn't in the repo. This is the inverse of G5 (where the GitHub source is the complete one and the PyPI sdist is stripped); here the PyPI wheel is the complete artifact and the GitHub source is incomplete. Critically: the deployed conda-forge feedstock may already ship this broken empty package โ flag it prominently as a real bug, not just a local-refresh concern.
Fix: source the PyPI wheel (the artifact that actually contains the generated assets):
source:
# GitHub source ships none of the release-time-generated www/ assets;
# they exist only in the published wheel. Source the wheel.
url: https://pypi.org/packages/<py-tag>/<x>/<name>/<name>-${{ version }}-<py-tag>-none-any.whl
sha256: <wheel sha256>
Verify before choosing the source โ compare the GitHub archive's contents against the wheel's:
unzip -l <name>-<version>-*.whl | grep -iE 'www/|static/|dist/|\.min\.(js|css)$|assets/' # generated assets present in wheel?
tar tzf <github-archive>.tar.gz | grep -iE 'www/|static/|assets/' # โฆabsent from GitHub source?
Asset-bearing wheel + asset-free GitHub source โ use the wheel. Set cfe-source-kind: pypi-wheel. If the deployed feedstock is sourcing GitHub and ships the empty package, that's a REAL BUG to fix at the feedstock.
Case study: h2o-lightwave-web (Jun 20, 2026) โ the GitHub monorepo subdir ships zero www/ web assets (generated at release time, present only in the PyPI wheel). A from-source GitHub build produced a broken empty ~15 KiB package; the deployed feedstock likely ships this broken empty package (flagged as a real bug to fix). Fix: source the PyPI wheel (1.8.9 is wheel-only). Adjacent to G5/G16.
Symptom: during a multi-recipe sweep that builds every recipe into one shared --output-dir, a recipe's test-env solve fails on a dependency that demonstrably IS on conda-forge (e.g. fixedint 0.1.6), so it looks build-clean-test-blocked โ but the recipe is fine.
Why: rattler-build treats the shared output dir as a local channel at higher priority than conda-forge. Once the sweep has built a NEWER version of some package (fixedint 0.2.0) into that channel, strict channel priority makes it SHADOW the OLDER conda-forge version (fixedint 0.1.6) a different recipe's transitive deps require โ the solve picks the wrong local build and the test env fails. The contamination grows as the sweep fills the channel.
Fix: build each recipe into its OWN isolated output dir โ --output-dir build_artifacts/<sweep>/<recipe> โ so there is no cross-recipe shadowing. (These are independent, already-on-conda-forge feedstocks; their deps come from conda-forge, not from each other, so isolation is correct.) If a test-env solve fails on a dep that IS on conda-forge, suspect channel pollution BEFORE recording build-clean-test-blocked โ rebuild isolated to confirm success.
Case study: azure-monitor-opentelemetry (Jun 21, 2026, co-maintainer total-coverage sweep) โ its azure-monitor-opentelemetry-exporter dep needs fixedint 0.1.6, but the sweep's shared channel held a locally-built fixedint 0.2.0 that shadowed it โ false test block; an isolated --output-dir solved GREEN.
Symptom: regenerating recipe.yaml for an existing multi-maintainer feedstock (via grayskull / generate_recipe_from_pypi) produces an extra.recipe-maintainers list containing only YOUR handle โ every other co-maintainer is gone. A stale local mirror may already carry this defect.
Why: the generator has no knowledge of the deployed feedstock's maintainer list; it emits only the invoking user. Shipping that as-is would (at submission) silently remove people who co-own the feedstock โ a serious etiquette + governance defect.
Fix: ALWAYS fetch the deployed feedstock's extra.recipe-maintainers and ensure the local recipe.yaml's list is a SUPERSET โ re-merge every other handle (including team handles like conda-forge/<team>). Verify local set โ deployed set before leaving any refreshed recipe. (Sole-maintainer feedstocks are trivially safe; this bites co-maintained / multi-maintainer ones.)
Case study: co-maintainer total-coverage sweep (Jun 21, 2026) โ airflow-code-editor (dropped xylar) and alang (dropped praeclarum) both had stale local meta.yaml lists missing a co-maintainer; the re-merge rule caught + restored both. Drives docs/specs/co-maintainer-feedstock-refresh.md.
sdist > GitHub-source > wheel โ but the source must actually SHIP the module; verify before switching (existence โ usable)Symptom: a recipe sources a PyPI wheel (โฆ-py3-none-any.whl) and conda-forge's review nudges "Detected pure Python wheel(s) in source โฆ it's preferred to use a source distribution (sdist) if possible." Reviewers prefer source builds. Conversely, naively switching a wheel recipe to "the sdist" can produce ModuleNotFoundError at the import test or a 0.0.0 version.
Why: conda-forge prefers building from source (sdist or VCS tag) over repackaging a wheel โ source is auditable + reproducible. grayskull / recipe-generator.py falls back to a wheel when it can't find a PyPI sdist, but it does NOT check the project's GitHub for a source tag archive, and it does NOT verify the chosen source contains code. Two real failure modes:
pyproject.toml + PKG-INFO (zero .py files) โ the code lives only in the published wheel. pip install . builds an empty wheel โ ModuleNotFoundError (e.g. ibm-watsonx-orchestrate-core 2.11.0 sdist has 0 .py files; jigsawstack sdist omits a requirements.txt its build reads). For these the wheel is correct โ document why.langflow-sdk + lfx-* live in langflow-ai/langflow under src/sdk, src/bundles/*). The generator never checks GitHub and defaults to the wheel; the GitHub tag archive is the preferred source.Fix โ the decision order (verify each before accepting it):
Usable PyPI sdist? Confirm it ships the module: curl -sL <sdist-url> | tar -tzf - | grep -c '\.py$' โ must be > 0. If yes, use the sdist (+ G55 backend).
Else, GitHub source tag archive? https://github.com/<org>/<repo>/archive/refs/tags/v<tag>.tar.gz, building the subdir for a monorepo โ preferred over the wheel. Monorepo wrinkle: package version โ monorepo tag โ carry a separate monorepo_tag context var and pip install ./src/<sub>:
context:
version: "0.2.0" # the package's own version
monorepo_tag: "1.10.0" # the repo tag that CONTAINS it โ bump BOTH on update
source:
url: https://github.com/<org>/<repo>/archive/refs/tags/v${{ monorepo_tag }}.tar.gz
sha256: <archive sha256>
build:
script: ${{ PYTHON }} -m pip install ./src/<sub> --no-deps --no-build-isolation
Flag that autotick cannot auto-bump this dual version (it touches only one). Default to GitHub source; fall back to the wheel only when the monorepo download is prohibitive AND there's no standalone source repo.
Build the subdir from the FULL extraction, not an isolated copy: a monorepo subdir's pyproject.toml can read its (dynamic) version from a file OUTSIDE the subdir via a relative path โ e.g. hatchling [tool.hatch.version] path = "../../src/<pkg>/__init__.py". pip install ./packages/<sub> run from the monorepo root resolves it; copying just the subdir elsewhere breaks the relative path โ 0.0.0 / build error (G39). Verified: ibm-watsonx-orchestrate-{core,clients} read version from ../../src/ibm_watsonx_orchestrate/__init__.py.
Else (no usable sdist, no public source): the wheel is acceptable for noarch: python / pure-Python โ set cfe-source-kind: pypi-wheel + a comment ("sdist ships only metadata" / "wheel-only upstream, source not public"). The <py-tag> URL segment may change on a version bump.
Always re-verify after a wheelโsource switch: the import test (right module name, G7), that the built version == context.version (dynamic-version sdists can build 0.0.0, G39), and pip_check.
Case study: langflow closure (Jun 23, 2026) โ langflow-sdk + the 4 lfx-* are wheel-only on PyPI but live in the langflow-ai/langflow v1.10.0 monorepo (src/sdk, src/bundles/*) โ switched to the GitHub tag archive + subdir build (hatchling), GREEN. The generator had emitted wheels for all of them without checking GitHub โ the gap this gotcha closes.
Retroactive sweep (Jun 24, 2026): recipes generated before v8.42.0 predate this check, so they may sit on a wheel when a usable sdist / GitHub source exists. Audit with grep -rl 'cfe-source-kind:\s*pypi-wheel' recipes/ and re-run the decision per recipe. The sweep migrated jigsawstack (sdist omits a requirements.txt its setup.py reads โ GitHub tag, G55-note below) and lfx (โ monorepo src/lfx), and overturned the initial ibm-watsonx-orchestrate-{core,clients} "keep the wheel" call: their sdists are metadata-only, but the ADK monorepo ships the source at packages/{core,clients}, so both migrated to GitHub source โ GREEN. Lesson: "the sdist is empty" is NOT "there is no source" โ always check GitHub before accepting the wheel.
Wheel-only-on-PyPI addendum (Jul 3, 2026, batch v5): three more live no-sdist-at-all cases โ redshift_connector 2.1.15, openlineage-integration-common 1.50.0, django-mptt-admin 2.9.0 โ all resolved to step 2 (GitHub tag archive). When a feedstock exists, its source block is the verified answer โ copy it before deriving your own: aws/amazon-redshift-python-driver v{{ version }} tag; the OpenLineage monorepo tag + cd integration/common build script (+ its pinned uv-build host dep, G91); mbraak/django-mptt-admin bare {{ version }} tag. Stamp cfe-source-kind: github-tag:no-sdist-on-pypi-G54 so a regen doesn't silently fall back to the wheel. A wheel source with a pip install . script fails as Directory '.' is not installable. Neither 'setup.py' nor 'pyproject.toml' found โ that error on a generated recipe usually MEANS "wheel-only package, generator picked the wheel". (redshift_connector also re-tripped G10: the generator re-hyphenated the legacy underscored conda name redshift_connector.)
host (wheel installs do NOT)Symptom: build/metadata generation fails with "No valid build backend found for Python recipe for package <X> using pip. Python recipes using pip need to explicitly specify a build backend in the host section โฆ you likely should add setuptools to the host section." โ often right after switching the source from a wheel to an sdist / GitHub source (G54).
Why: installing a wheel (pip install <name>.whl) unpacks a pre-built artifact and needs NO build backend โ so wheel-sourced recipes carry only host: [python, pip]. Building from source (pip install . / pip install ./src/<sub>) runs the PEP 517 build, which requires the backend named in the source's [build-system] to be in host. The wheel recipe never needed it, so a wheelโsource switch surfaces it immediately.
Fix: add the backend from the source's pyproject.toml [build-system].requires to host:
tar -xzOf <sdist>.tar.gz '*/pyproject.toml' | grep -A2 '\[build-system\]' # or read the GitHub subdir's pyproject
[build-system].requires |
add to host |
|---|---|
setuptools (+wheel) |
setuptools (and wheel only if the build needs it; G8) |
hatchling |
hatchling |
flit-core |
flit-core |
poetry-core |
poetry-core |
pdm-backend |
pdm-backend |
scikit-build-core |
scikit-build-core |
maturin |
maturin (+ Rust toolchain) |
uv_build |
uv-build (conda name โ hyphen, G10) |
Keep only the backend + pip (+ python); per G8 don't re-add the legacy wheel+setuptools pair when a single PEP 517 backend is declared. recipe-generator.py should emit the backend automatically whenever it emits a source build โ wheel-install recipes are the only ones that legitimately omit it.
Case study: langflow closure (Jun 23, 2026) โ switching langflow-sdk (and the sdist attempt on ibm-watsonx-orchestrate-*) from wheel-install to source build hit this exact error; fixed by adding hatchling to host (the backend in src/sdk/pyproject.toml).
Beyond the backend โ the setup_requires / build-time network-fetch trap (source builds only). A setup.py carrying setup_requires=[...] (legacy) โ or any build step that reads the network โ triggers an easy_install/pip fetch during the build. A wheel install never runs it, and a local source build masks it (the dev box has network); but conda-forge CI builds offline, so the fetch fails there. When switching wheelโsource, strip legacy setup_requires (it only backs the deprecated python setup.py test; conda-forge runs the recipe's own tests:) with a build sed, or add the dep to host. Verified: jigsawstack โ sed -i.bak '/setup_requires/d' setup.py && rm -f setup.py.bak dropped setup_requires=["pytest-runner"]. Corollary of the live-verification principle: a clean LOCAL source build does NOT prove the recipe builds in conda-forge's offline CI โ reason about build-time network access explicitly.
noarch: python build scripts must be cross-platform โ bash cd "$SRC_DIR/<sub>" + "$PYTHON" fails on win-64; use ${{ PYTHON }} -m pip install ./<sub>Symptom: a multi-output recipe (N sdists โ N outputs via a top-level source: list + target_directory) builds GREEN on linux + osx but the win_64 leg fails at the build script:
(base) %SRC_DIR%>cd "$SRC_DIR/core_src"
The system cannot find the path specified.
(base) %SRC_DIR%>"$PYTHON" -m pip install . ...
'"$PYTHON"' is not recognized as an internal or external command
Why: noarch: python is built on every platform on staged-recipes (incl. win-64), and a per-output build script written in bash โ cd "$SRC_DIR/<sub>" then "$PYTHON" -m pip install . โ is run by cmd.exe on Windows, which doesn't understand $SRC_DIR / $PYTHON (bash variable refs; cmd uses %SRC_DIR% / %PYTHON%). The crewai-suite-style cd "$SRC_DIR/<sub>" idiom is bash-only and was never win-CI-tested. (Same bash-vs-cmd class as G13, specialized to the multi-output target_directory subdir-build pattern.)
Fix: drop the cd + shell-var form; use jinja ${{ PYTHON }} (rendered to the literal interpreter path before any shell runs โ neither bash nor cmd sees a variable) + a relative subdir path (the build CWD is already the work root holding the target_directory subdirs; the explicit ./ makes pip treat it as a path, not a PyPI name):
build:
noarch: python
script: ${{ PYTHON }} -m pip install ./core_src -vv --no-deps --no-build-isolation
No cd, no $SRC_DIR, no if: unix/else. This is the standard noarch:python construct that passes win-64 across thousands of feedstocks; the only addition is ./<sub> instead of ..
Case study: ibm-cos-suite (3-output core+s3transfer+sdk; staged-recipes #33886, Jun 25 2026) โ cd "$SRC_DIR/core_src" + "$PYTHON" passed linux+osx, failed win_64 (buildId 1543755). ${{ PYTHON }} -m pip install ./core_src per output โ green. The repo's crewai-suite recipe carries the same latent bash-only bug.
export is UNSET on win-64 โ the build silently falls back to a network fetch (CMake FetchContent 404)Symptom: a compiled recipe whose build script sets an env var that steers the build away from a network download โ e.g. export PYCBC_OPENSSL_DIR="${PREFIX}" so CMake uses the conda OpenSSL โ builds GREEN on linux + osx but win_64 fails with a fetch + 404:
CMake Error ... downloading 'https://github.com/python/cpython-bin-deps/archive/openssl-bin-3.5.2.zip' failed
... The requested URL returned error : 404
Why: the script's export VAR="${...}" is bash syntax; on win-64 cmd.exe it does nothing, so VAR is never set on Windows. The upstream build's CMake/setup then takes its fallback path โ a FetchContent/urllib download (here a prebuilt OpenSSL from python/cpython-bin-deps) โ which 404s and is forbidden in hermetic CI anyway. The failure looks like a network/404 bug but the root cause is the win-unset env var. (Bash-vs-cmd class like G56, but here it degrades to a silent network fetch instead of erroring on the shell line.)
Fix: set build-control env vars cross-platform, splitting the script unix/win (script.env: can't โ it doesn't shell-expand ${PREFIX}/%LIBRARY_PREFIX%, G1). On unix the conda prefix is $PREFIX; on win the equivalent (where conda ships lib/*.lib + include/) is %LIBRARY_PREFIX% (= %PREFIX%\Library):
build:
script:
content:
- if: unix
then: |
export PYCBC_OPENSSL_DIR="${PREFIX}"
${{ PYTHON }} -m pip install . -vv --no-deps --no-build-isolation
else: |
set "PYCBC_OPENSSL_DIR=%LIBRARY_PREFIX%"
${{ PYTHON }} -m pip install . -vv --no-deps --no-build-isolation
Verify the var actually steers the build by reading the upstream build code first (don't guess): for couchbase, pycbc_build_setup.py does ssl_dir=os.getenv('PYCBC_OPENSSL_DIR'); cmake_extra_args += [f'-DOPENSSL_ROOT_DIR={ssl_dir}'], and CMakeLists.txt only FetchContents OpenSSL when OPENSSL_ROOT_DIR is unset.
Case study: couchbase 4.6.2 (staged-recipes #33893, Jun 25 2026) โ PYCBC_OPENSSL_DIR set only via unix export; win_64 (buildId 1543796) FetchContent'd OpenSSL 3.5.2 โ 404. Fixed by also setting it on win (%LIBRARY_PREFIX%); linux/osx were already green.
lookup_feedstock BEFORE submitting โ a package already on conda-forge is rejected by the GHA linter ("feedstock exists"), even while the linting-service bot says "excellent"Symptom: a staged-recipes PR's linter check fails, but the conda-forge-linting service bot comments the recipe is "in excellent condition." The two disagree. The GHA linter.py step's real output:
- recipes/<name>/recipe.yaml:
- lints:
- Feedstock with the same name exists in conda-forge.
Why: the package already has a conda-forge/<name>-feedstock (possibly maintained by someone else) โ you can't submit to staged-recipes a package that already has a feedstock; updates go to the feedstock. The two linters check different things: the linting-service bot lints recipe quality (which can be perfect), while the GHA staged-recipes linter.py step also runs the "feedstock already exists" check. When they disagree, trust the GHA linter โ it gates the PR.
Fix โ prevention: run lookup_feedstock(pkg_name=<name>) (or the MCP tool) on every recipe before a staged-recipes submission โ exists=True โ already on conda-forge โ do not submit (CLOSE the PR; the local recipe is a redundant mirror). This is load-bearing for dep-closure efforts (langflow / db-gpt class): a closure can contain popular packages already on conda-forge under another maintainer, and an unverified "net-new" assumption wastes one PR per false positive. A closure spec's "already on conda-forge โ consume, don't submit" list must be verified against lookup_feedstock, never assumed. If the package needs a newer version or more platforms, that's a feedstock PR (version bump / platform expansion), not a staged-recipes submission.
Case study: impit (staged-recipes #33897, Jun 25 2026) โ assumed net-new in the langflow closure + authored locally, but conda-forge/impit-feedstock v0.13.0 (maint. Pijukatel) already existed. Service bot said "excellent"; GHA linter rejected with "Feedstock with the same name exists." PR closed; the local recipe was repurposed as a feedstock platform-expansion mirror (osx-arm64 + linux-aarch64; aarch64 cross-build verified GREEN locally).
sed for editing upstream source โ bare sed -i fails on macOS/BSD sed (noarch builds on every platform), and reviewers ask for patchesSymptom: a noarch recipe edits the extracted upstream source with sed -i 'script' file (no backup-suffix) and builds on linux but fails the osx leg at the build script with sed: 1: "<file>": extra characters at the end of p command (or silently no-ops, so a later pip check fails). Separately, a reviewer asks "Can we do this with a patch instead? โฆ it becomes harder to maintain and less readable than a patch."
Why: BSD/macOS sed -i requires the backup-suffix as the next token, so sed -i 'script' file parses 'script' as the suffix and file as the script โ error or wrong result (the G13/G23 bash-vs-BSD class). noarch: python recipes build on linux and osx and win, so any in-build sed runs on osx. Beyond portability, a source patch is auditable (the reviewer sees exactly what changed) and is the conda-forge-preferred mechanism for source edits.
Fix: move the source edit into a source.patches: entry โ rattler-build applies it at extraction (before the build script), portably on every platform, and the build script reduces to just pip install. Generate the patch by applying the exact intended edit to the real pinned source file and diffing with a/,b/ prefixes (rattler-build applies -p1):
cp <file> orig; sed -E '<the edit>' orig > new
diff -u orig new | sed -e '1s#^--- .*#--- a/<path>#' -e '2s#^+++ .*#+++ b/<path>#' > recipes/<name>/patches/0001-<desc>.patch
For a multi-output recipe, put the patches at the top-level source.patches โ they apply once to the shared extracted source, then each output builds its already-patched subtree (don't re-sed per output).
Exception โ the edit must interpolate ${{ version }} (or another jinja value): a static patch can't carry it, so use a portable sed instead โ sed -i.bak 'script' file && rm -f file.bak (the .bak suffix form works on both GNU and BSD sed). Example: injecting a placeholder version = "0.0.0" โ version = "${{ version }}" (aurelio-sdk).
Verifying the patch took effect (don't trust "an artifact exists"): read the BUILT wheel's dist-info/METADATA and check Requires-Dist:. Distinguish unconditional Requires-Dist: X (main deps โ what the patch + pip check govern) from Requires-Dist: X ; extra == "..." (extras โ never stripped, not enforced by pip check); a "lean"-strip only touches the main dependencies array, so extra-marked entries legitimately remain. For a local build, the artifact lands in noarch/ only after tests pass โ a failed-test artifact is quarantined to broken/; check which dir to read pass/fail when the log is truncated.
Case study: graph-retriever #33913 (bare sed -i '/pytest/d' pyproject.toml failed the osx leg) + pksuid #33895 (reviewer ocefpaf requested patch-over-sed for the G26 pin-loosen) โ both converted to source patches, GREEN. A repo-wide sweep (2026-06-26) converted every in-build sed -i source edit in recipes/ to source patches (aurelio-sdk kept a portable sed -i.bak per the exception above). Cross-ref G23 (portability when sed is unavoidable), G26 (pin loosening), G13.
extra.cfe-* keys + # CFE โฆ blocks โ never the schema header or context: blockSymptom: a submitted staged-recipes recipe render-fails on every platform with Jinja template error: Template rendering failed: undefined value (in <string>:1) (template: ${{ version }}) pointing at recipe.yaml:3 (version: ${{ version }}). The local recipe is fine and builds.
Why: the strip that removes the CFE-local metadata before pushing (the extra.cfe-* keys + the bottom # CFE metadata / # CFE comments blocks โ see "Never Add AI Commentsโฆ") over-reached and also deleted the leading # yaml-language-server + schema_version: 1 header and/or the context: block. With context.version gone, ${{ version }} (and ${{ python_min }}) resolve to nothing โ render fails before any build, on all platforms.
Fix: the strip removes exactly extra.cfe-* keys and the two bottom # CFE โฆ blocks โ nothing above extra:. Always diff the stripped recipe against the local source and confirm the header + context: survive. Quick gate: the submitted recipe's first non-comment line must still be schema_version: 1, and a context: block must precede package:.
Case study: langchain-litellm #33917 (Jun 26, 2026) โ the pushed recipe had lost both the header and the context: block (render-fail on linux/osx/win); restoring them cleared the render error (a separate osx python_min issue then surfaced โ see G40).
archive/<commit>.tar.gz) sha256 can drift โ GitHub re-gzips, breaking the recorded checksumSymptom: a recipe sourcing https://github.com/<org>/<repo>/archive/<commit>.tar.gz fails at the source-fetch step (before the build even starts) with ร sha256 checksum validation failed for ".../src_cache/...<commit>.tar.gz". The recipe is unchanged and built fine previously.
Why: GitHub generates archive tarballs on demand and has, at times, changed the gzip framing of archive/<commit> (and archive/refs/tags/<tag>) tarballs, so the byte stream โ and thus the sha256 โ differs from when the recipe was authored, even though the underlying git tree is identical. Commit-pinned archives are the most exposed; uploaded release assets and PyPI sdists are byte-stable.
Fix: recompute the live sha256 and update the recipe โ curl -sL <url> | sha256sum. Where a stable artifact exists, prefer a release tarball / PyPI sdist over archive/<commit>. For commit-pinned local-only recipes, just refresh the sha256 when it drifts โ and treat it as a latent fragility shared by every commit-pinned GitHub-archive recipe in the same closure (the siblings may still match today but carry the same risk).
Case study: suite-st-transfers (Jun 26, 2026) โ recorded b3c875afโฆ vs live e1082e44โฆ; refreshed the sha256 and the build proceeded. The 4 sibling suite-* recipes (same archive/<commit> pattern) still matched but carry the same risk.
cfe-* metadata โ the strip is mandatory AND must be VERIFIED on the pushed artifact (verify, don't assume)Symptom: a staged-recipes / feedstock PR ships the local-only extra.cfe-* keys and/or the #### CFE metadata + # CFE comments blocks (build-status, purls, upstream pointers, agent rationale). These are CFE-internal and must never appear in a submitted recipe.
Why: the strip is a manual step in the push flow (copy local recipe โ remove the cfe block โ push). A manual step can be forgotten or done partially, and the failure is silent โ the recipe builds fine with cfe-* present (rattler-build ignores unknown extra: keys, conda-smithy lint doesn't flag them), so nothing catches it except a human reviewer. "I stripped it" is an assumption until proven.
Fix: treat the strip as mandatory and verify it on the artifact you actually pushed โ never assume. After staging the recipe on the fork branch (and before opening the PR), grep the pushed files and abort if anything matches. Check both recipe.yaml AND conda-forge.yml โ since v8.61.1 the generated conda-forge.yml also carries a bottom #### CFE comments block (local-recipes-only; the submitted file is just the trim # conda-forge.yml โ pre-seeds the feedstock comment + the keys):
for f in recipe.yaml conda-forge.yml; do
gh api "repos/<you>/staged-recipes/contents/recipes/<name>/$f?ref=<branch>" --jq '.content' 2>/dev/null \
| base64 -d | grep -nE 'cfe-|# CFE|#### CFE' && echo "ABORT: cfe/CFE block shipped in $f" || echo "$f clean"
done
The stripped recipe.yaml ends at the extra.recipe-maintainers list; the stripped conda-forge.yml ends at its last key โ everything from the #### CFE marker down is removed in both. This is the under-strip complement of G60 (which warns against OVER-stripping the header / context:): strip exactly the cfe block โ no less (G62), no more (G60). Applies to both staged-recipes and feedstock pushes. The same grep is a cheap repo-wide audit across every open/merged PR.
The strip happens at push-time, on a COPY โ never delete the cfe-* block from the LOCAL recipe. The cfe-* block lives in the local recipes/<name>/recipe.yaml permanently (it caches identity + hard-won decisions + on-conda-forge status + local-build record that CFE/admin/maintainer tooling reads). The convention is local-retains / strip-on-push: when a recipe is submitted, update its local cfe-on-conda-forge-status โ pending-approval-on-conda-forge and add cfe-submission-pr: โ do not remove the block. Deleting cfe-* from the local recipe loses that state and is an error. (Live miss: qianfan's local cfe-block was deleted on 2026-06-26 โ restored with the submitted status.)
Case study: 2026-06-26 โ a full audit of all open + merged langflow-closure PRs (and qianfan #33935) found every pushed recipe clean, but the strip had been a manual, unverified step; adding the grep gate makes "clean" a checked fact, not an assumption.
--body REPLACES the template, it does not mergeSymptom: a staged-recipes PR is missing the reviewer-team guidance comment and the Checklist (License file packaged, source official, no vendoring, no static libs, build number 0, tarball-not-repo, maintainer confirmation). Reviewers and the review-requested bot flow rely on that checklist.
Why: GitHub only auto-populates .github/pull_request_template.md when a PR is opened without a body. Passing --body / --body-file to gh pr create (or a custom body via the API) replaces the template entirely โ it does not merge with it. A custom description silently drops the checklist.
Fix: fetch the live template (don't hand-reconstruct โ it drifts) and submit it as the body with the boxes ticked:
gh api "repos/conda-forge/staged-recipes/contents/.github/pull_request_template.md" --jq '.content' | base64 -d > /tmp/tmpl.md
# tick EVERY "- [ ]" box โ including the "knowledge base" line, then:
sed -i 's/- \[ \]/- [x]/g' /tmp/tmpl.md
gh pr edit <pr> --repo conda-forge/staged-recipes --body-file /tmp/tmpl.md
Note the template path is .github/pull_request_template.md (lowercase). Keep the body to the template + ticked checklist (a short description is optional โ when in doubt, template-only). The "maintainers have posted a comment confirmingโฆ" box is self-satisfied when the sole maintainer IS the submitter.
Tick ALL boxes, including the "check the knowledge base before pinging a team" line โ it's a checklist item the submitter affirms, not a reviewer-only step, and leaving it unchecked reads as an incomplete checklist. (Earlier guidance wrongly carved out the knowledge-base box as an exception; that was an error โ there are no exceptions, every box gets ticked.)
Case study: qianfan #33935 (Jun 26, 2026) โ first opened with a custom --body-file description that dropped the checklist; fixed by re-fetching the live template and ticking the boxes. opik #33937 (Jun 26, 2026) โ opened with the knowledge-base box left unticked under the old carve-out; corrected to all-boxes-ticked.
Symptom: a staged-recipes PR gets the @conda-forge/help-<lang>, ready for review! ping posted twice (e.g. a manual ping + an automated one), or pinged while CI is still building โ cluttering the thread / pinging the team prematurely.
Why: the ping is what moves a green PR into the review queue โ a bot responds by adding the review-requested label + the language label (e.g. python). Once those labels exist, the request has already landed; posting the comment again is pure noise. The labels (not the comment text) are the authoritative "already requested" signal โ comment-text grepping is a weaker proxy.
Fix: ping exactly once, with the team matching the recipe's language, only when ALL three preconditions hold โ (a) every check is green, (b) the PR is not a draft, and (c) review-requested is not already on the PR (a prior ping already landed). A draft PR is not ready for review โ pinging it is premature; the review-requested label is the authoritative "already requested" signal:
gh pr view <pr> --repo conda-forge/staged-recipes --json isDraft,labels \
--jq '{draft: .isDraft, labels: [.labels[].name]}'
# ping ONLY if: all checks SUCCESS AND draft == false AND "review-requested" absent
gh pr comment <pr> --repo conda-forge/staged-recipes --body "@conda-forge/help-python, ready for review!"
Team by recipe language: pure-Python noarch โ @conda-forge/help-python; C/C++ โ @conda-forge/help-c-cpp; Rust โ @conda-forge/help-rust; Go โ @conda-forge/help-go; R โ @conda-forge/help-r; โฆ (full table is in the fetched .github/pull_request_template.md). The comment is terse โ exactly @conda-forge/help-<lang>, ready for review!, no preamble. First-time contributors can't ping teams directly (use the @conda-forge-admin, please ping team bot command); established maintainers ping directly.
Case study: qianfan #33935 (Jun 26, 2026) โ pinged twice (a manual ping, then an automated one that didn't check first); the bot had already applied review-requested + python after the first. The duplicate was deleted; the rule is now check-labels โ ping-once.
pixi exec, and run the linter.py checks conda-smithy doesn'tSymptom: a staged-recipes PR fails the conda-forge-linter webservice (or the GHA linter) on something the local gate "passed" โ or the local lint is suspected stale.
Why โ two parity gaps:
validate_recipe runs conda-smithy recipe-lint --conda-forge = the same linter the webservice uses (so it DOES catch most rules โ e.g. it flagged opik's noarch selector). BUT the pixi env pins conda-smithy >=3.44.6,<4 (โ 3.62.0): conda-smithy went CalVer (2026.x) and the <4 cap freezes it on the old 3.x line, deliberately โ the CalVer builds hard-depend on the conda package, which isn't in this rattler/conda-build pixi env (pixi update conda-smithy fails: "conda-smithy 2026.6.14 would require conda >=4.2 โฆ no candidates"). So the env linter can lag the CI's ruleset.linter.py does MORE than conda-smithy. The staged-recipes GHA .github/workflows/scripts/linter.py runs conda_smithy.linter recipe-lint plus: files-outside-recipes/ (unless a maintenance label), feedstock-with-same-name-exists, recipe-exists-in-bioconda (hint), conda-package-name-already-exists. validate_recipe runs none of those.Fix โ before any staged-recipes submission:
<4 pin stays): pixi exec --spec "conda-smithy>=2026.6.14" conda-smithy recipe-lint --conda-forge recipes/<name> โ pixi exec solves the CalVer conda-smithy + its conda dep in a throwaway env, exactly matching the webservice. Treat its output as authoritative; the env's 3.62.0 validate_recipe is the fast first pass. Never dismiss a lint as a "false positive" (e.g. the G12 noarch-selector escape hatch) without confirming against this current-version lint.linter.py extras: lookup_feedstock(<name>) (feedstock-exists, G58); confirm the conda package name isn't already on conda-forge; confirm all changed files are under recipes/ (else the linter rejects unless maintenance). bioconda-name overlap is a hint, not a blocker.recipe-build is host-only (linux), so an osx/win-only failure (e.g. opik's osx-64 litellmโfastuuid py3.10 gap, G40) won't surface locally; for compiled transitive deps run the G40 per-subdir repodata check before submitting.Case study: opik #33936 (Jun 26, 2026) โ local validate_recipe (3.62.0) flagged the noarch selector (which was wrongly dismissed), but missed nothing else the linter caught; the osx-64 fail was a per-platform build gap, not a lint gap. pixi exec --spec "conda-smithy>=2026.6.14" conda-smithy recipe-lint --conda-forge recipes/opik โ "in fine form" after the fix, matching the green CI. (Env conda-smithy stays pinned <4 because CalVer needs conda; the pixi.toml comment records the pixi exec parity command.)
Symptom: a consumer recipe whose only blocker "just merged" is submitted, and its staged-recipes CI fails to solve (nothing provides <prereq> / unsatisfiable) โ even though the prereq's PR shows MERGED.
Why: merging a staged-recipes PR creates the feedstock, which then has to run its own CI to build + upload artifacts to the conda-forge channel. That lag is minutes-to-hours (longer if the feedstock build is red or needs a rerender). "PR merged" โ "package installable" โ there is a window where the feedstock exists but no artifact is on the channel yet, so any dependent submitted in that window fails to solve on CI. The local mirror can also mask this: a build against the local channel (which holds your locally-built prereq) goes green while the real conda-forge channel still has nothing.
Fix: before submitting a consumer whose prereq recently merged, confirm the prereq is actually on the cf channel at a version that satisfies the pin โ don't trust the merged state:
# fresh repodata (a CACHED snapshot is unreliable for version maxima/floors โ re-fetch for floor checks)
curl -s "https://conda.anaconda.org/conda-forge/noarch/repodata.json" -o /tmp/cf-noarch.json
python3 - <<'PY'
import json; from packaging.version import Version
pk={**(d:=json.load(open('/tmp/cf-noarch.json'))).get('packages',{}),**d.get('packages.conda',{})}
vs=[v['version'] for v in pk.values() if v['name']=='<prereq>']
ok=[x for x in vs if Version('<lo>')<=Version(x)<Version('<hi>')] # the consumer's pin range
print('satisfying builds on cf:', sorted(set(ok)) or 'NONE โ DO NOT SUBMIT YET')
PY
Then run a local build that resolves against conda-forge (not just the local mirror) as the authoritative solve. Verification depth scales with the consumer's dep shape: pure-python deps โ confirm-prereq-live + build is enough; compiled transitive deps โ ALSO run the G40 per-subdir python-floor check proactively (before submit, not after a red osx leg).
Case study: the 2026-06-27 ยง Bโฒ-consumer batch (langflow closure) โ trustcall (โdydantic #33898), langchain-graph-retriever (โgraph-retriever #33913), langchain-google-vertexai (โgoogle-cloud-vectorsearch #33914), langchain-sambanova (โsambanova #33916). Each prereq's MERGED PR was cross-checked against live cf repodata (dydantic 0.0.8, graph-retriever 0.8.0, sambanova 1.9.1+1.10.0 all confirmed on-channel) before submitting the consumer โ all six (incl. opik #33937, qianfan #33935) went green first try, no solve-fail round-trip. The cached snapshot trap was real: a reused /tmp/cf-noarch.json reported pydantic 2.9.2 / typing_extensions 4.9.0 as "latest" (stale), but the fresh build resolved the true cf pydantic 2.13.4 โ floor checks must use fresh data or the build's own solve.
Refinement (v8.68.0) โ UPLOADED โ INDEXED; watch the SERVED repodata, not the anaconda.org file API. There are TWO lags after a merge, not one. The .conda file appears on api.anaconda.org/package/conda-forge/<pkg> within ~a minute of the feedstock CI upload โ but solvers read the channel's repodata index (conda.anaconda.org/conda-forge/<subdir>/repodata.json), which regenerates on its own cycle and lagged the upload by ~10โ30 min in the live case. A watcher that greps the anaconda.org file listing fires early and the very next solve still resolves the OLD build. Poll the served index for the exact new basename instead: curl -s --compressed https://conda.anaconda.org/conda-forge/<subdir>/current_repodata.json | grep -q '<pkg>-<ver>-<build>.conda' โ or make the authoritative check an actual solve. Also expect per-tool cache layering on top: rattler's and conda's repodata caches each lag independently (one showed _4, the other _3, while anaconda.org had _5), so clear the relevant cache before the verification solve. Live: django-cryptography-django5 _5 โ file visible in ~1 min, indexed ~10+ min later; the interim rattler-build test resolved _4 and false-failed pip check.
run_constrained can block a consumer โ it lives in constrains, NOT depends, so verify with the REAL consumer solve (a narrow dry-run misses it)Symptom: a consumer recipe's test-env solve is unsatisfiable on a dependency that's clearly on conda-forge at a compatible version โ and a narrow "does X co-install with Y" dry-run of the direct deps passed, so the conflict seems impossible.
Why: a conda package can carry run_constrained entries (surfaced as constrains in repodata) โ soft pins that do not pull the package but bind if that package is present in the env for any other reason. A feedstock can add stale run_constrained that upstream never declared (a convenience copied from an old release's "extended testing deps"). It is invisible to:
depends-only check (the pin is in constrains, a separate field); andFix: verify a cross-package conflict claim against the real consumer's full pin set, not a hand-picked subset โ the authoritative check is a solve/build of the actual consumer, not mamba create X Y. When you do hit such a conflict, inspect the blocker's constrains (not just depends): python -c "import json; [print(v['build'], v.get('constrains')) for v in <repodata>.values() if v['name']=='<dep>']". Compare every constrains entry against the consumer's hard pins; one whose range can't intersect the consumer's pin is a feedstock bug โ fix it in the feedstock, exactly like a depends skew. This is the constrains-axis sibling of the langchain depends text-splitters skew.
Staleness is "diverges from the CITED SOURCE," NOT "cf has a higher version" โ the latter is a false-positive generator. Do not flag a run_constrained cap stale just because conda-forge ships a version above it. Optional-integration constraints intentionally cap to the provider's supported range, which legitimately lags the latest cf build. Counter-example: langchain's run_constrained: groq >=0.4.1,<1 looks "stale" (cf has groq 1.5.0) but is correct โ upstream langchain-groq 1.3.11 pins groq>=0.30.0,<1.0.0 (it doesn't support groq 1.x), and the feedstock mirrors that; a consumer co-installing langchain + groq 1.x genuinely shouldn't, so the <1 cap is protective, not stale. The real test: does the cap match the cited source's CURRENT pin (the upstream file / provider feedstock the comment points at)? Re-check the source, not cf-latest. (aiosqlite WAS stale because langchain's own extended_testing_deps.txt moved <0.20โ<0.23; groq is NOT, because langchain-groq still pins <1.)
Fix the cap to the AUTHORITATIVE value, don't guess a loose bound. When the stale entry is annotated as copied from a pinned upstream file (langchain's run_constrained block cites โฆ/extended_testing_deps.txt at a specific tag), the correct fix is to re-sync to the CURRENT version's file โ bump the cited tag and copy the value verbatim โ not to arbitrarily widen it (a guessed <1.0 may over-admit). Read the source the comment points at: e.g. langchain 1.3.11's libs/langchain/extended_testing_deps.txt pins aiosqlite>=0.19.0,<0.23 (the 1.2.0 file said <0.20); the durable PR is >=0.19.0,<0.23 with the comment's tag bumped langchain==1.2.0 โ 1.3.11.
Case study: langflow-suite 1.10.1 (Jun 27, 2026). lfx built green, but langflow-base failed its solve: cf langchain 1.3.11 carries run_constrained: aiosqlite >=0.19.0,<0.20 (a langchain==1.2.0-era "extended_testing_deps" cap, absent from upstream langchain), which collides with langflow-base's upstream-accurate hard aiosqlite >=0.20.0. An earlier mamba create langchain langchain-classic dry-run had "proven Skew-1 resolved" โ but neither pulls aiosqlite, so the constrains never fired; only the real langflow-base solve (which hard-deps aiosqliteโฅ0.20) exposed it. Fix: a langchain-feedstock PR re-syncing the cap to langchain 1.3.11's extended_testing_deps.txt โ aiosqlite >=0.19.0,<0.23 (NOT a guessed <1.0; the 1.2.0-era file said <0.20). The other 15 run_constrained entries were unchanged 1.2.0โ1.3.11 and non-binding (verified: defusedxml 0.7.1 satisfies both, the rest aren't langflow-base deps) โ so the surgical single-pin fix is the minimal defensible PR.
Symptom: a recipe that should now resolve cleanly (after a skew was fixed upstream) still fails its local test-env solve โ the error mentions "excluded because due to strict channel priority not using this option from file://โฆ/build_artifacts/", or a dependency resolves to an old version that no longer satisfies the recipe's pins.
Why: local verification adds build_artifacts/<subdir>/ as a higher-priority channel than conda-forge. With strict channel priority, the solver takes a package from the highest-priority channel that has it โ so a stale workaround build left in the local channel (e.g. a langchain 1.2.18 rebuilt during a skew, or a higher build-number rebuild) shadows the real, now-fixed conda-forge package. The recipe is correct; the local channel is lying.
Fix: when a skew/upstream issue resolves, delete the obsolete workaround artifacts from build_artifacts/<subdir>/<noarch|subdir>/ and re-index (python -m conda_index build_artifacts/<subdir>) before re-testing. Confirm the package is gone from the local repodata, so the test resolves the real cf version. (If you still need a workaround for an un-resolved skew, build it at the lowest viable version/build-number and remember it's masking cf โ it WILL cause a false green/red later.) Local-channel staleness is a top cause of "passes/fails locally but the opposite on CI."
Case study: langflow-suite 1.10.1 (Jun 27, 2026) โ after bumping to langchain~=1.3.0, the lfx test failed with strict-priority "excluded" noise; root cause was the obsolete langchain 1.2.18 skew-workaround (and litellm 1.89.3) still sitting in build_artifacts/linux64/noarch/. Strict priority picked local 1.2.18 (failing the new >=1.3.0 pin) and refused cf's 1.3.x. Purging both + re-indexing let the tests resolve cf langchain 1.3.11.
Symptom: after bumping a multi-output suite to a new version, an output's pip_check fails with <output> X.Y.Z has requirement <sibling>>=A.B.C, but you have <sibling> A.B.(C-1) โ the new upstream tightened its requirement on a package you maintain separately.
Why: upstream packages raise their floors on sibling/companion packages between releases. A suite bump that only edits the suite recipe leaves the sibling recipe (and its already-submitted PR) at the old version, which no longer satisfies the bumped suite's tightened cross-dep. The suite builds (run deps aren't checked at build time) but pip_check in the test catches it.
Fix: treat a suite bump as potentially multi-recipe. When the bump tightens a cross-dep on a package you own: (1) bump that sibling recipe to a satisfying version and rebuild it into the local channel so the suite test passes; (2) update the suite's own pin to match upstream's tightened floor; (3) if the sibling is already submitted, record in its cfe-forge-recipe-updates-needed that the open PR must bump too (a >= pin on an unmerged-at-old-version sibling is a real submission blocker).
Case study: langflow-suite 1.10.1 (Jun 27, 2026) โ lfx 1.10.1 raised langflow-sdk>=0.2.1 (was satisfiable by 0.2.0). The local channel + submitted PR #33856 had langflow-sdk 0.2.0 โ lfx pip_check failed. Fixed by bumping the langflow-sdk recipe 0.2.0โ0.2.1 (monorepo_tag 1.10.1, same archive), rebuilding it locally, raising the lfx pin to >=0.2.1, and recording in langflow-sdk's cfe-forge-recipe-updates-needed that #33856 must bump to 0.2.1.
run_constraints to upstream's real extra pins โ it's soft (metadata-only, lint-verified), so it's a safe high-value faithfulness passSymptom: a multi-output suite recipe carries hand-authored run_constraints with placeholder floors (>=0.1) or bare names instead of upstream's real optional-dependency pins โ the soft constraints "work" but are uninformative and diverge from upstream.
Why: run_constraints are advisory (constrain-if-present, never installed), so a >=0.1 placeholder passes build + lint while telling a downstream solver nothing useful. The faithful values live in the upstream [project.optional-dependencies] (and the aggregating [complete] extra) of the package's pyproject โ the recipe should mirror those. This is the inverse of G67: G67 is an external feedstock's stale constrains blocking your consumer; G70 is your recipe's own run_constraints being unfaithful. When auditing, treat these as two separate passes.
Fix: reconcile each run_constraint to its upstream extra pin. It is a safe, metadata-only change โ run_constraints are NOT installed in the test env, so re-pinning cannot change the build/test solve or pip_check; a full rebuild isn't required (though it stays green), and lint/render is sufficient verification (it validates every entry is a valid conda MatchSpec). Mechanics: source from the base + umbrella pyproject optional-dependencies (+ hard deps for lean-stripped integrations); PEP 508 โ conda (~=X.Y.Z โ >=X.Y.Z,<X.(Y+1).0; strip ; marker and [extra]); detect phantoms (a run_constraint name absent from every upstream extra/dep โ remove it, e.g. the earlier atlaspy/dynamicconf); map cf renames (G10, e.g. upstream langfuse โ cf langfuse-python); resolve python-conditional duplicates to the broadest range. Preserve any in-recipe authoring markers (e.g. submission-order comments).
Case study: langflow-suite (Jun 27, 2026) โ the langflow output's run_constraints had 75 of 78 entries as >=0.1 placeholders. Reconciled all 78 to langflow 1.10.1's real extra pins (e.g. litellm >=1.85.1,<2.0.0, chromadb >=1.0.0,<2.0.0, exact pins like composio ==0.9.2), 0 unresolved, 0 phantoms (atlaspy/dynamicconf had already been removed), langfuse-python G10 rename preserved. All 3 outputs stayed GREEN and the recipe linted clean โ confirming the change was metadata-only.
import <pkg>.cluster raises, failing a noarch consumer's CFEP-25 * test (win-only)Symptom: a noarch: python consumer (e.g. cassio) builds clean and passes on linux/osx and on win at python_min, but its CFEP-25 import test fails only on win + the latest Python (the python_version: "*" leg) with cassandra.DependencyException: Unable to load a default connection class โ traceback originating in the dependency's __init__ (import cassandra.cluster), not the consumer.
Why: the dependency picks its event-loop "reactor" at module import. The libev reactor needs a C-extension that is Unix-only (no Windows build); asyncore was removed in Python 3.12. So on win + py3.12+ neither is available and import <pkg>.cluster raises at load. The python_min (3.10) leg passes (asyncore still present); linux/osx pass (libev built). The dependency's OWN feedstock test typically imports only the top-level package (import cassandra), never cassandra.cluster, so it ships win+py3.12 builds that are broken for any consumer that imports the cluster path (the G24-class "the dep's test didn't cover it" trap).
Detect on linux (no Windows needed): the same condition is "no compiled libev + no asyncore", which you can reproduce on linux py3.12+ by importing from source with the C-extension NOT built โ CASS_DRIVER_NO_EXTENSIONS=1/PYTHONPATH=<source> then python -c "import <pkg>.cluster" โ the identical DependencyException.
Interim fix (consumer recipe) โ restrict the broken combo out of the test with a platform-conditional CFEP-25 test: on win, test python_min only; full triad off-win:
tests:
# win: <dep> has no event-loop reactor on py3.12+ (libev is Unix-only; asyncore
# removed in 3.12), so importing <pkg>.cluster fails there. Test python_min only on win.
- if: win
then:
python: {imports: [<pkg>], pip_check: true, python_version: ["${{ python_min }}.*"]}
else:
python: {imports: [<pkg>], pip_check: true, python_version: ["${{ python_min }}.*", "*"]}
A platform-conditional TEST block on a noarch: python recipe is lint-clean โ unlike a platform-conditional run-dep selector (G12/G35), neither validate_recipe nor the current conda-smithy (verify via the G65 pixi exec lint) flags a tests[].if selector. Add a one-line body comment so reviewers know why win is restricted. Caveat: the noarch artifact is still installable-but-broken on win+py3.12+ at runtime โ this only stops CI testing the broken combo; the durable fix is upstream.
Durable fix (the dependency's feedstock) โ patch the reactor-detection chain to append an asyncio fallback so the import succeeds when libev+asyncore are both unavailable. cassandra-driver's cassandra/cluster.py reduces over a conn_fns tuple (gevent โ eventlet โ libev โ asyncore); add a _try_asyncio_import() (from cassandra.io.asyncioreactor import AsyncioConnection, catch (DependencyException, ImportError)) and append it. Behavior is unchanged everywhere a reactor already loads; only the broken win+py3.12 case now resolves (to AsyncioConnection). Verify by the no-ext py3.12 repro above: unpatched raises, patched โ AsyncioConnection.
Case study: cassio 0.1.10 / staged-recipes #33946 (Jun 27, 2026) โ win+py3.12+ CFEP-25 * leg failed on cassandra-driver's missing reactor. Interim win-test restriction shipped (PR went green); durable conda-forge/cassandra-driver-feedstock asyncio-fallback patch authored + runtime-verified locally (recipes/cassandra-driver/0001-add-asyncio-reactor-fallback.patch, + cassandra.cluster added to its test as a regression guard). The full local conda build of cassandra-driver was blocked by an unrelated quirk โ its setup.py runs the legacy ez_setup.use_setuptools() which dies on Python-3.12's changed tarfile.chown signature when the build env's setuptools triggers a download โ so filing the feedstock PR is gated on a green local build per the test-locally-first rule.
pin_subpackage) to dissolve a cross-feedstock submission gateSymptom: a multi-output suite recipe (built from a monorepo tag) has an output that hard-deps a SEPARATE recipe built from the SAME monorepo source. Submitting the suite then has to WAIT for that sibling's standalone staged-recipes PR to merge โ build โ land on the channel (G66) before the suite can solve โ a self-imposed cross-feedstock submission gate for a package that shares the suite's own source.
Fix: make the sibling an additional output of the suite instead of an external dep:
${{ PYTHON }} -m pip install src/sdk โฆ).version via a context var (e.g. sdk_version: "0.2.1") โ its version is independent of the monorepo tag, and v1 multi-output supports per-output package.version.${{ pin_subpackage('<sibling>', exact=True) }} instead of the external spec.This removes the gate (no "wait for the sibling PR"), and the suite builds the sibling itself. Versions stay locked in lockstep (autotick bumps the shared tag).
Caveats: (a) the sibling inherits the suite's python_min floor โ folding can RAISE it (langflow-sdk 3.10โ3.11); flag if it reduces the sibling's standalone usability. (b) Close the sibling's now-redundant standalone PR (superseded) and re-stamp its local recipe. (c) Re-verify cf-resolvability with a clean-channel rebuild (G68): purge local copies so the build resolves the sibling via pin_subpackage and everything else from cf. (d) the sibling is still an independently-installable package (multi-output produces separate packages); only the feedstock/maintenance unit is shared.
Case study: langflow-sdk folded into recipes/langflow-suite/ as a 4th output (src/sdk, sdk_version: 0.2.1, lfx pins it via pin_subpackage(exact=True)), Jun 27 2026 โ removed langflow-suite's external dependency on the standalone #33856 (CLOSED, superseded); a clean-channel rebuild then built all 4 outputs GREEN resolving entirely from conda-forge.
import/pip_check can't catch it (a G51 trap for apps with a UI)Symptom: a noarch: python application (e.g. langflow) builds GREEN from the monorepo tag, import <app> + pip check PASS โ but the app's web UI is missing at runtime (<app> run serves no frontend). The built .conda contains zero index.html / .js / .css / built-bundle files.
Why: the frontend (<pkg>/frontend/ โ the built React/Vite bundle) is generated at release time (npm run build) and shipped only in the PyPI wheel; the GitHub tag carries src/frontend/ source, not the built bundle. Building from the tag (which a multi-output suite needs for its other outputs' Python source, G72) omits the frontend. This is G51 specialized to a frontend โ and unlike a missing Python module, the CFEP-25 import/pip_check test does NOT exercise the UI, so the green build hides a shipped-broken-app bug. There is no tag-vs-wheel conflict: the wheel's frontend is generated from the same tag's src/frontend at release time, so the recipe re-generates it at build time from the one tag source (below) โ no second source needed.
Detect โ for ANY recipe packaging an app with a web/desktop UI, inspect the BUILT .conda for the expected runtime assets; do NOT trust the green import/pip_check:
python3 -c "import zipfile,tarfile,io,zstandard,sys
z=zipfile.ZipFile(sys.argv[1]); inner=[n for n in z.namelist() if n.startswith('pkg-') and n.endswith('.tar.zst')][0]
names=[];
import io as _io
with zstandard.ZstdDecompressor().stream_reader(_io.BytesIO(z.read(inner))) as r:
[names.append(m.name) for m in tarfile.open(fileobj=r,mode='r|')]
print('frontend assets:', len([n for n in names if n.endswith(('index.html','.js','.css'))]))" <built>.conda
Compare against the PyPI wheel's file list (unzip -l <pkg>.whl | grep frontend/). Zero in the conda + many in the wheel = this bug.
Fix (node-build from source โ the conda-forge idiom): build the frontend at build time from the tag's src/frontend. Add the JS toolchain to requirements.build (nodejs pinned to the project's package.json engines.node, plus the package manager the lockfile implies โ npm ships with nodejs; pnpm/yarn are separate conda packages), run the upstream build (npm ci && npm run build, i.e. the Vite/webpack build โ src/frontend/build), then copy the built bundle into the package tree (<pkg>/frontend/) before pip install, so the build backend ships it (e.g. hatchling packages=["<pkg>"] includes the whole dir).
โ ๏ธ The build script MUST be cross-platform. staged-recipes builds a noarch recipe on all three platforms (linux/osx/win) for validation and runs package_contents on each โ it is NOT linux-only (only after it becomes a feedstock does conda-smithy build noarch on linux alone). A unix/bash-only build (set -ex, pushd, rm -rf, cp -r, bare npm) runs through cmd.exe on the Windows leg and silently fails: bash-isms don't exist there, and a bare .cmd shim (npm/pnpm/yarn) without call terminates the script (the build.bat call-the-.cmd-shim rule) โ the frontend never builds โ the package_contents guard correctly fails win-only. Provide a Windows path (G56) and do the copy + pip install in cross-platform Python (shutil, not rm -rf/cp -r):
script:
- if: win
then: |
call npm --prefix src/frontend ci
if errorlevel 1 exit 1
call npm --prefix src/frontend run build
if errorlevel 1 exit 1
else: |
set -ex
npm --prefix src/frontend ci
npm --prefix src/frontend run build
- ${{ PYTHON }} -c "import shutil; d='<pkg-dir>/frontend'; shutil.rmtree(d, ignore_errors=True); shutil.copytree('src/frontend/build', d)"
- ${{ PYTHON }} -m pip install <pkg-src-dir> -vv --no-deps --no-build-isolation
npm --prefix avoids cd/pushd. (Surfaced 2026-06-27: langflow-suite PR #33972's win leg failed exactly this way โ the unix-only build shipped first passed linux/osx, but the package_contents guard caught the empty Windows frontend. Live CI caught what the local linux build could not โ verify, don't assume.)
Build-time network IS available on conda-forge โ only the test phase is offline.
npm ci/pnpm ifetchingnode_modulesat build is fine and routine; Rust/Go/npm feedstocks do it constantly. (This corrects earlier guidance in this gotcha + G45/G6 that implied a build-time fetch is disallowed โ it is not. The test env is the offline one.)
Precedents (verified 2026-06-27): gradio (GitHub-archive source + nodejs+pnpm build deps, runs scripts/build_frontend.sh = pnpm i --frozen-lockfile && pnpm build at build โ the exact no-sdist analogue), chainlit (pnpm build via the hatchling build hook), mlflow (yarn install; yarn build of mlflow/server/js, then strips node_modules/.map), agentsview/nebi (pnpm install && pnpm run build, then embed). Verify the result with a package_contents test (env-free โ it inspects the built .conda, so it works even when the test-env solve is blocked): site_packages: [<pkg>/frontend/index.html, <pkg>/frontend/assets/*.js].
CHECK ALTERNATIVE (1) FIRST โ a "JS-building" package often needs no JS toolchain at all (v8.78.0). The reflex "this bundles a frontend, so the recipe needs node/bun" is frequently wrong: the sdist may already ship the hook's complete output even when the GitHub tag does not. This is the inverse of G51/G73's headline case โ here the published sdist is the complete artifact and the GitHub tag is the one that would need a toolchain. One command settles it before you design anything:
# does the sdist already contain the built assets?
tar tzf <pkg>-<ver>.tar.gz | grep -cE '/static/|\.js$'
# ...and does it match what upstream's own wheel ships?
unzip -l <pkg>-<ver>-py3-none-any.whl | grep -c '<pkg>/static/'
Equal counts โ take the sdist and neutralize the hook (per G106: remove the block when its artifacts = [], or patch clean_artifacts = false when it declares artifacts) rather than adding nodejs/bun + a network fetch to the build. Verified 2026-07-14 across three packages in one chain โ reactpy (72 static files incl. an embedded wheel), reactpy-router (bundle.js), reactpy-django (64 files) โ every one shipped byte-parity assets in its sdist while its hook ran bun; all three build clean with zero JS toolchain. Reach for the node-build only after this check fails.
Alternatives, in order of preference: (1) sdist that already includes the frontend โ if upstream's sdist ships the built bundle, just pip install it (dominant pattern: streamlit/bokeh/jupyterlab; and see the CHECK above โ this case is more common than it looks); (2) node-build from the tag (above โ when no usable sdist exists); (3) tensorboard model โ source the wheel wholesale as the package when the wheel is exactly what you want to ship (noarch: python downgrades the wheel-in-source lint R-029โnon-blocking hint R-030). Avoid the wheel-graft (extract <pkg>/frontend/ from a secondary wheel source while building the rest from the tag): it has no conda-forge precedent and draws reviewer pushback. Note on the wheel-in-source lint: R-028 (compiled wheel) and R-029 (pure wheel, non-noarch) are blocking lints; R-030 (pure wheel + noarch: python) is a non-blocking hint; none is skippable via conda-forge.yml linter.skip (no skip key exists for this check).
Case study: langflow-suite (langflow-base + langflow built from the langflow-ai/langflow v1.10.1 tag), surfaced Jun 27 2026 during an adversarial spec review: the built langflow-base/langflow .conda shipped 0 frontend files while the langflow-base PyPI wheel ships ~1,874 (langflow/frontend/index.html + the Vite/React assets bundle). import langflow/pip_check were green throughout โ the UI-less break was invisible to them. Fixed by building the frontend at build time in the langflow-base output (nodejs >=20.19 build dep; npm ci && npm run build of src/frontend โ copy build/ into src/backend/base/langflow/frontend/ before pip install), guarded by a package_contents test for langflow/frontend/index.html + assets/*.js. The initial fix attempt was a wheel-graft; deep research into cf feedstocks (gradio/chainlit/mlflow) showed node-build-from-tag is the precedented idiom and that build-time network is available โ the wheel-graft was dropped (no precedent). Verified 2026-06-27: the rebuilt langflow-base .conda grew 1.9 MB โ 14 MB and contains 1,873 frontend files (index.html + 1,855 JS + 2 CSS, vs the wheel's ~1,874); the package_contents test passes at packaging time even under --test skip.
channeldata.json before any membership-driven destructive editSymptom: a cf_atlas.db packages lookup says a dependency is NOT on conda-forge, but it actually is โ its feedstock was created after the atlas's last refresh. Acting on the stale "absent" (dropping a run_constraint, re-packaging a prerequisite that already exists, marking a dep blocked) is then wrong.
Why: the atlas is an offline snapshot (Phase B/C build time). Feedstocks created in the days since โ including your own freshly-merged staged-recipes PRs โ aren't in it. Live case (2026-06-27): filtering langflow-suite's run_constraints to "on-cf only," the atlas showed apify-client absent, but its feedstock was created 2026-06-26 and it's live on the channel โ it would have been wrongly removed.
Fix: when a membership check drives a destructive or irreversible edit, confirm against LIVE conda-forge, not the atlas alone. The authoritative, staleness-proof, one-fetch source is the channel index:
curl -sL --compressed https://conda.anaconda.org/conda-forge/channeldata.json -o cf.json
python3 -c "import json; p=set(json.load(open('cf.json'))['packages']); print('apify-client' in p)"
channeldata.json (~22 MB, ~33k packages) lists every package on the channel regardless of which feedstock built it โ so it also catches multi-output-provided packages a feedstock-name check would miss. Apply G10's four-spelling variants when checking. The atlas stays the fast first pass (definitive for "present"); the live index resolves the "absent" set. (Read-side atlas metrics are fine offline โ this rule is specifically for membership facts that gate edits, and is the membership analogue of G66 mergedโ installable.)
Productized (2026-07-05, cyclonedx-universe-inventory Wave C): inventory-match runs this exact cross-check as its default decision-4 live post-pass โ every would-be-missing row (ADD / ADD-NONPYPI / conda-UNKNOWN) probes TTL-cached live channeldata; a hit re-buckets the row as atlas_stale instead of misreporting ADD. Live validation at gate time: refleak + doclang (merged 2026-07-04, absent from the same-day atlas) recovered as CURRENT. For inventory questions, prefer the tool over hand-rolling the curl.
The default strip-before-push (G60/G62) is surgical: it removes only extra.cfe-* + # CFE โฆ blocks and KEEPS the schema header + context:. But a user may direct a fuller clean for a lean first submission of a big multi-output suite. Two extra steps, both deliberate, both verified on the pushed artifact:
# yaml-language-server โฆ; optional per repo convention, ~29/30 cf PRs omit it), section dividers, and inline trailing comments. schema_version: 1 (a key, not a comment) stays. Process line-by-line, section-aware, never via a YAML round-trip (it mangles ${{ }} jinja + literal block scalars).run_constraints to packages already on conda-forge โ a suite that lists constraints pointing at unsubmitted packages reads as depending on an un-packaged ecosystem and invites reviewer questions. Drop the not-yet-on-cf ones from the submission copy; keep the full set in the local recipe (source of truth). Re-verify membership LIVE per G74 โ the atlas is stale for recently-merged deps. (run_constraints are advisory โ metadata-only; a constraint on an absent package is harmless but noise.) Collapse a now-empty run_constraints: key entirely (don't ship a bare run_constraints:).Branch-only flow (no PR): base the branch on conda-forge/staged-recipes main (NOT the local-recipes fork, NOT a bot fork) so the eventual PR diffs to just the one recipe; push to the personal fork (<user>/staged-recipes); do not open the PR when told to hold. Verify cfe/comment-free on the pushed artifact (G62). Live: langflow-suite-clean on rxm7706/staged-recipes โ 519โ313 lines, 40 of 85 run_constraints dropped (live-checked), 0 cfe/comment leakage. This is a deliberate variant of โ not a replacement for โ the default G60 strip.
noarch: python recipe CANNOT be a hard dep โ noarch bakes depends at build time, so the package becomes unsolvable on Windows; make it OPTIONAL (strip from the wheel + run_constraints)Symptom: a noarch: python package lists a run dep that conda-forge ships Unix-only (no win-64, no noarch build โ e.g. gunicorn, uvloop). On the Windows test/install leg the conda solve fails (nothing provides <dep>), and pip check (which staged-recipes runs regardless of the recipe's pip_check: setting โ see below) flags the wheel's Requires-Dist.
Why two half-fixes DON'T work:
if: unix on the run dep โ for a noarch package, selectors are evaluated at build time (the build runs once, on linux=unix), so gunicorn bakes into depends unconditionally. The built noarch artifact requires it on every platform โ Windows solve still fails. (Confirmed: index.json depends carried gunicorn with subdir: noarch.) Platform-conditional hard depends are impossible for noarch.; sys_platform != 'win32' in [project.dependencies]) โ fixes pip check on win (the dep isn't required there) but does nothing for the conda solve (the conda depend, if present, still blocks win); and if you make the conda dep Unix-only it bakes anyway (above). It also fails pip check on unix if the dep is required-by-wheel but not installed.Fix โ make it optional: (1) strip the dep from the wheel's [project.dependencies] via a source patch (so pip check never requires it, on any platform); (2) move it to conda run_constraints (<dep> >=x,<y) โ advisory, constrains-if-present, never installed, never blocks a solve. The package stays noarch, installs on all platforms, and pip check passes everywhere; the dep is still version-pinned if a unix user adds it. This is the same lean pattern used for optional integrations (G25/G70). Verify on the built .conda: the dep must be absent from the wheel main Requires-Dist AND from conda depends, and present in conda constrains.
pip_check: false is NOT a workaround here: staged-recipes CI runs pip check for every output regardless of the recipe's pip_check: setting (observed on PR #33972 โ >pip check ran + "pip check passed!" for outputs explicitly set pip_check: false). So a wheel-vs-conda metadata mismatch must be genuinely resolved, not silenced. (The two alternatives to "optional" โ keep the dep Unix-only and noarch_platforms-exclude win, or make the package non-noarch per-platform โ are heavier; optional/run_constraints is preferred when the dep isn't import-required.)
Case study: langflow-suite langflow-base โ gunicorn (cf Unix-only: linux-*/osx-* only) was a hard run dep of the noarch output โ PR #33972 Windows leg unsolvable. Resolved Jun 27 2026 by stripping gunicorn from src/backend/base/pyproject.toml (patch 0001) + adding gunicorn >=25.3.0,<27.0.0 to run_constraints. Verified: built .conda has gunicorn in constrains, absent from depends + wheel main deps; 5/5 outputs pass pip check on linux.
pip check on Windows โ you can't fix it in your noarch recipe; exclude win via conda-forge.yml noarch_platforms, or fix the upstream feedstockSymptom: pip check fails only on the Windows leg, on a transitive (not direct) requirement โ e.g.:
magika 0.6.3 has requirement onnxruntime<=1.20.1; sys_platform == "win32", but you have onnxruntime 1.26.0.
linux + osx pass; the failing constraint comes from a package you don't depend on directly (here <your-pkg> โ markitdown โ magika).
Why: the transitive dep's wheel metadata has an environment-marked cap (<dep> <=X; sys_platform == "win32"), but its conda feedstock didn't encode that cap โ conda depends can't carry PEP 508 markers, they'd need a # [win] selector. So the conda solver installs the latest <dep> on win, which violates the wheel marker, and pip check (which staged-recipes runs regardless of pip_check:, G76) flags it. It's an upstream-feedstock metadata gap, not your recipe's bug โ and being transitive, you often can't even name it from your own dep list (read the pip check error to find the chain).
Why you can't cleanly fix it in your recipe: a win-only cap on the offending dep is impossible in a noarch:python recipe (noarch bakes depends, no platform-conditional specs โ G76); an unconditional cap penalizes linux/osx with an old version; and the dep is transitive (capping it in your own run_constraints is brittle + over-broad).
Remedies (best first):
0. Exclude only the offending VERSION if the cap is a one-version aberration (BEST when applicable โ fixes the staged-recipes PR immediately). Often a single bad release adds the win cap while its neighbours don't โ check the dep's versions on cf. Add a != exclusion in your own run_constraints: <dep> >=A,!=<bad>,<B> โ the solver picks a neighbouring good version. This is the cleanest fix: it works in the staged-recipes PR right away (no feedstock PR, no win exclusion, no broad cap that penalises linux/osx), keeps win green, and is metadata-only (run_constraints are advisory, constrain-if-present). Verify the != is satisfiable โ a neighbouring version must exist on cf (list them first). โ
This resolved langflow-suite PR #33972's win leg: magika 0.6.3 had the cap but 0.6.2/1.0+ didn't (cf had 0.6.1/0.6.2/0.6.3/1.0.x), so magika >=0.6.1,!=0.6.3,<0.7.0 in lfx run_constraints โ solver took 0.6.2 โ win GREEN (all legs pass). Pair with a matching upstream-feedstock PR (markitdown PR #20) so the fix is permanent once it lands.
<dep> <=X # [win], the conda equivalent of its wheel marker); then rebuild-to-channel (G66). Correct but external + slow โ and in a deeply unix-oriented dep tree expect more such moles after the first clears.noarch_platforms: [linux_64, osx_64, osx_arm64] in recipes/<name>/conda-forge.yml controls the FEEDSTOCK CI matrix (post-merge) โ it does NOT change the staged-recipes PR matrix: staged-recipes runs 3 fixed Azure jobs (linux/osx/win) and builds every recipe on each, so the win job still runs + fails in the PR (confirmed: with the conda-forge.yml in place, the PR build still had a win_64 leg and no osx_arm64 leg). Especially for a multi-output recipe where noarch is per-output (no top-level noarch), the matrix generator doesn't treat it as noarch. So:noarch_platforms (drops win, adds osx_arm64 validation) + workflow_settings.store_build_artifacts take effect โ keep it.noarch_platforms won't do it. Either land the upstream-feedstock fix (remedy 1), or skip win at the recipe level with build: skip: true # [win] (verify it's lint-clean for your noarch/multi-output shape before relying on it).conda_pkgs_{linux,noarch,osx} artifacts on a staged-recipes PR are published UNCONDITIONALLY by the fixed pipeline โ NOT by workflow_settings.store_build_artifacts (corrected v8.60.0 via a code-traced audit of staged-recipes/.ci_support/build_all.py + the 3 static Azure pipelines: build_all.py never reads that key, and the gated 7z Prepare conda build artifacts task the key controls doesn't exist in staged-recipes at all). So store_build_artifacts is a NO-OP in a staged-recipes PR โ you get conda_pkgs_* either way; the earlier "it works in the PR" reading here was a misattribution. The key is load-bearing only at the feedstock (where conda-smithy generates that gated 7z task โ and G18's win_64-exclusion applies). The conda_pkgs_noarch artifact is still the universal package โ downloadable + installs on every platform incl. macos-arm64, with no per-arch build โ and a red win build leg does NOT block artifact publishing on the other legs. Full per-setting audit (what staged-recipes actually reads ยท recipe-type relevance ยท redundant-vs-load-bearing): reference/conda-forge-yml-reference.md ยง Per-setting applicability audit.linux_aarch64/linux_ppc64le legs for a large closure unless needed โ they often red on dep-availability gaps.Case study: langflow-suite PR #33972 โ after fixing the bash-build (G73) and gunicorn (G76) win failures, the win leg STILL failed on lfx โ markitdown โ magika 0.6.3's onnxruntime<=1.20.1; sys_platform=="win32" wheel marker (conda magika installed onnxruntime 1.26.0). The third distinct win-only failure in one closure. RESOLVED (remedy 0): magika 0.6.3 was a one-version aberration (cf had 0.6.1/0.6.2/0.6.3/1.0.x), so excluding only it in lfx run_constraints (magika >=0.6.1,!=0.6.3,<0.7.0) made the solver pick 0.6.2 โ win GREEN; PR #33972 passes all legs (linux + osx + win + both linters, build 1545234). win_64 re-added to noarch_platforms. (First tried the exclude-win route before realising the version-specific exclusion is cleaner + keeps win.) Lesson: before excluding win, check whether the offending cap is version-specific โ a != exclusion usually beats dropping the platform.
Process note (whole win saga โ 4 sequential CI rounds to green): win failures on a deeply unix-oriented closure surface one at a time โ each CI round clears one and reveals the next (frontend G73 โ gunicorn G76 โ magika G77 โ green). Local linux builds CANNOT validate the win/osx test phases (the noarch package builds + tests on linux locally; win/osx solve + pip check only run in staged-recipes CI), so you can't pre-clear them โ budget for iterative CI rounds, fix the narrowest lever each round (version-exclude > platform-exclude > broad-cap), and re-push. Verify with real tests locally (not --test skip) to catch the linux/osx phases early, but expect win to need CI.
cfe-submission-pr / cfe-on-conda-forge-status are LOCAL hints that go STALE โ answer "is X on staged-recipes / conda-forge?" from LIVE signals, never the cfe fieldsSymptom: a submission-gap audit driven by the recipes' cfe-* metadata is wrong โ it lists recipes as "pending/blocked, not submitted" that are actually merged or open PRs (and vice-versa). Acting on it (re-submitting, or believing a recipe is unsubmitted) creates duplicate PRs or a false gap.
Why: cfe-submission-pr + cfe-on-conda-forge-status are hand-maintained, local-only fields (stripped before push, G62). They lag reality โ a recipe gets submitted/merged but the local field isn't re-stamped (or the strip-before-push means the pushed copy never carries it). Live case (2026-06-27): a cfe-scan flagged ~40 recipes "unsubmitted," but bce-python-sdk/graph-retriever/sambanova/unitxt/pksuid/langflow-suite were all already submitted or merged โ the fields were stale.
Fix โ for any "is it on staged-recipes / on conda-forge?" decision, use LIVE signals:
conda-forge channeldata.json membership (G74), with G10 name variants.gh pr list --repo conda-forge/staged-recipes --author <user> --state all --json number,title,headRefName,files, and match a recipe to a PR by THREE keys (any one): branch (add-recipe-<name> or bare <name> or <name>-clean), title (Add <name> or the older Create recipe.yaml for <name> package), or the PR's changed files (recipes/<name>/). Matching only add-recipe-*/Add โฆ misses older PRs (e.g. apache-tika #31649). Account for multi-output PRs covering several names (ibm-cos-suite #33886 โ ibm-cos-sdk/-core/-s3transfer; langflow-suite #33972 โ lfx/langflow-base/langflow/langflow-sdk).cfe-on-conda-forge-status: pending-approval-on-conda-forge + cfe-submission-pr: <url> after you submit, so the local record converges โ but never trust them for a gap audit. Sibling of G66 (mergedโ installable) + G74 (atlas membership staleness): three faces of "local/cached records decay; verify live before a consequential action."Case study: the langflow handoff submission-gap audit (2026-06-27) โ driving off cfe-* would have re-submitted ~10 already-merged recipes and missed older-branch PRs. A live gh+channeldata pass (matching PRs by files+title+branch) gave the true gap (5 langflow leaves โ drafts #33977โ#33981) and the correct already-done set.
go-licenses save fatals on a package's OWN non-OSI / non-standard license โ pass --ignore <module-path> and ship the LICENSE separatelySymptom: a Go recipe that bundles third-party dep licenses with go-licenses save ./cmd/<name> --save_path ... compiles the binary fine (the go build step succeeds) but then dies at the license step with a fatal:
F main.go:75] one or more libraries have an incompatible/unknown license:
map["unknown":["github.com/<org>/<repo>" "github.com/<org>/<repo>/cmd/<name>" ...]]
ร error Script failed with status 1
The "unknown" map contains only the project's OWN import paths (github.com/<org>/<repo>/...) โ every third-party dependency classified fine.
Why: go-licenses save (and check) classify every package in the build graph and exit fatal on any package whose license is "unknown" / "forbidden" / "restricted". When the upstream project ships a non-OSI / non-standard license the classifier (licensecheck / SPDX matching) doesn't recognize โ n8n's Sustainable Use License, BUSL, a custom "fair-source" text โ the project's own packages are flagged "unknown" and the bundling step aborts, even though all the genuine third-party deps were fine. (A separate non-fatal W ... contains non-Go code that can't be inspected warning on cgo deps like modernc.org/libc is NOT this failure โ look for the F / status 1 line whose unknown-map holds only the project's own paths.)
Fix: tell go-licenses to skip the project's own packages with --ignore <module-path> โ their license is already shipped separately via about.license_file, and the point of the step is to bundle the dependencies' licenses:
go-licenses save ./cmd/<name> --save_path "${SRC_DIR}/license-files" --force \
--ignore github.com/<org>/<repo>
--ignore takes an import-path prefix, so github.com/<org>/<repo> covers all sub-packages. The third-party dep licenses still land in license-files/ โ ship both via license_file: [LICENSE, license-files/]. This is the precise root-cause fix; do NOT mask it with || true (that silently drops the THIRDPARTY bundle on any future real failure).
Detection: the F-level line names the offending packages. If the unknown-map is exactly github.com/<org>/<repo>/... (the project itself), it's this gotcha. If it instead names a dependency, that dep genuinely has an unrecognized license โ investigate the dep, don't blanket-ignore.
Relationship to the cf-eligibility gate: a non-OSI license that trips go-licenses is also a conda-forge submission blocker (cf distributes only OSI-approved licenses โ same class as BUSL-1.1, the ragstack-ai-knowledge-store precedent). So this fix is most relevant for a local-only Go recipe (built for personal use, never submitted). For a genuinely OSI-licensed Go project go-licenses won't fatal on the project's own packages at all โ if it does, re-check the license, because the package may not be cf-eligible.
Case study: wuphf 0.230.0 (nex-crm/wuphf, Jun 27 2026) โ a pure-Go YC-S26 CLI under the Sustainable Use License v1.0 (non-OSI; cf-blocked โ built local-only). go build compiled clean, then go-licenses save ./cmd/wuphf fatal'd with an unknown-license map containing only github.com/nex-crm/wuphf/*. Adding --ignore github.com/nex-crm/wuphf skipped wuphf's own packages and bundled the ~40 third-party dep licenses into info/licenses/license-files/; rebuild GREEN, all tests passed. (The recipe also corrected a pre-existing stub that mislabeled the license as MIT โ LicenseRef-SustainableUseLicense-1.0.)
Refinement (wuphf win CI, Jun 28 2026) โ go-licenses fatals are PER-OS; for a local-only recipe, DROP go-licenses rather than chase a per-OS --ignore list. The set of packages go-licenses save must classify is the build graph for the current GOOS, which differs by platform โ so a recipe that bundles go-licenses can pass on linux/osx and fail on win (or vice-versa) on a dependency it can't classify, not just the project's own packages. Two responses, by whether the THIRDPARTY bundle has an audience:
--ignore list โ add each unclassifiable dep for the platform that pulls it in. Whack-a-mole, but keeps the bundle.posix, now deprecated โ m2-base (a real lint: "Use of posix package on windows is deprecated. Use m2-base."). Dropping go-licenses clears that lint too; the recipe reduces to a pure go build with no build deps beyond the Go compiler. The package's own LICENSE still ships via about.license_file.This is the dependency-side, cross-platform face of the Detection note above. Live: wuphf's Windows fork-CI build (buildId 1545273) fatal'd at go-licenses save on github.com/mattn/go-localereader โ a win-only bubbletea dep absent from the linux/osx graph (so linux had passed). Since wuphf is cf-blocked local-only, go-licenses was dropped outright โ fixing the win build AND the posix/m2-base lint in one change; the win go build had already succeeded (go-licenses ran after it).
Symptom: you disabled a check to dodge an external conda dependency's broken packaging metadata โ e.g. conda-forge's pdfminer.six shipping a dist-info Version: 0.0.0 while pdfplumber pins pdfminer.six==<exact>, so pip check fails on the bogus string (cfe-pip-check: false:pdfminer.six). Later you want to drop the waiver, check "is there a newer version of the dep?", see the same version, and conclude the bug persists โ leaving a now-needless waiver in place (and, per G76, one that wouldn't even work on staged-recipes).
Why: a dist-info / RECORD / METADATA defect is a feedstock build bug, not an upstream-version bug โ the fix rarely changes the version string. The maintainer bumps the build number (number: 0 โ 1) and rebuilds; the conda version is byte-identical, only the build hash + number change. A version-only check shows "unchanged" while the artifact is already fixed.
Check: decide from the latest BUILD, not the version list โ
.conda and read its dist-info Version:, orrecipe/meta.yaml: a bumped number: (+ often a new version-asserting test) is the fixed signal.When fixed, drop the waiver (re-enable pip_check, clear the cfe-pip-check: false:<pkg> reason) โ discharging the revert obligation (G24/G26/G28/G36). Sibling of G77/G78 (verify the live artifact, not a stale string).
Case study: db-gpt rerun (2026-06-27) โ dbgpt-app carried pip_check: false for the conda-forge pdfminer.six 0.0.0 bug. The cf version was still 20260107 (looked unfixed), but build _0 shipped Version: 0.0.0 while the same-version rebuild _1 (feedstock now number: 1, with a version('pdfminer.six') == '20260107' test) reports it correctly. Re-enabled pip_check on dbgpt-app; the local db-gpt rebuild's 7/7 outputs (incl. dbgpt-app) passed.
Caveat (v8.68.0) โ parse the waiver reason code to the FULL package name before rechecking. cfe-pip-check: false:<reason-code> names the blocking package precisely; the G80 recheck must target exactly that package. Near-miss (2026-07-04): the reason code said false:django-cryptography-django5-poisoned-dist-info-G28, but the recheck inspected django-cryptography โ a different, unrelated PyPI project sharing the prefix, whose clean dist-info produced a wrong "dischargeable" verdict; the real blocker's newest build was still poisoned. Prefix-share collisions are common in the django/flask plugin namespace โ extract the package name mechanically from the code (everything up to the defect slug), don't eyeball it, and confirm against the recipe's in-body waiver comment (which names the package in prose) before declaring a waiver dischargeable.
package.json's pnpm field โ a lockfile made under pnpm <11 fails --frozen-lockfile with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH; pin pnpm <11Symptom: a pnpm-based recipe (npm CLI, Tauri/Electron app, any pnpm install --frozen-lockfile build) aborts at the very first step โ before any frontend/Rust compile โ with:
[WARN] The "pnpm" field in package.json is no longer read by pnpm. The following
keys were ignored: "pnpm.overrides", "pnpm.patchedDependencies".
[ERR_PNPM_LOCKFILE_CONFIG_MISMATCH] Cannot proceed with the frozen installation.
The current "overrides" configuration doesn't match the value found in the lockfile
The recipe is otherwise correct, and often built fine months earlier on the same source.
Why: pnpm 11 (conda-forge's current pnpm, e.g. 11.9.0) moved overrides + patchedDependencies out of package.json's pnpm field to pnpm-workspace.yaml, and now ignores the package.json pnpm field entirely. If upstream's pnpm-lock.yaml was generated under pnpm <11 (config in package.json) and upstream hasn't fully migrated to pnpm-workspace.yaml (a common half-migrated state: patches copied to pnpm-workspace.yaml, overrides left in package.json), then pnpm 11 reads empty overrides while the lockfile records the real ones โ frozen-install mismatch โ abort. This is a tooling-version-skew failure (cf. G14): a recipe that worked when conda-forge's pnpm was <11 breaks unchanged once cf advances pnpm to 11.
Fix (minimal, preferred): pin pnpm <11 in requirements.build. conda-forge keeps pnpm 10.x (immutable; e.g. 10.33.2), which still reads package.json โ the frozen install matches the lockfile. Document it and lift the cap once upstream finishes migrating config to pnpm-workspace.yaml:
requirements:
build:
- nodejs 24.*
- pnpm <11 # upstream pnpm.overrides still in package.json; pnpm 11 ignores it -> frozen-lockfile mismatch
Fix (durable, more work): patch pnpm-workspace.yaml at build time to carry the full config โ copy every pnpm.overrides entry and every pnpm.patchedDependencies entry from package.json into pnpm-workspace.yaml (overrides: + patchedDependencies:) so --frozen-lockfile passes under pnpm 11. Beware a partial set already in pnpm-workspace.yaml (fixing only overrides then surfaces a second patchedDependencies mismatch). Prefer the pin unless you specifically need current pnpm.
Do NOT use --no-frozen-lockfile as the fix: under pnpm 11 it regenerates the lockfile without the ignored overrides/patchedDependencies โ a different dependency resolution (drops security overrides + patched deps), non-reproducible, and needs network. Those overrides/patches are load-bearing.
Detection / version lookup: the WARN line names the ignored keys; pnpm-lock.yaml (lockfileVersion: '9.0') carries overrides:/settings:, while pnpm-workspace.yaml lacks overrides:. pnpm is per-platform on conda-forge (NOT noarch) โ channeldata.json shows subdirs: [linux-64, ..., win-64]; query a platform subdir's repodata.json (e.g. linux-64) for the available pnpm versions, not noarch (a noarch query returns empty and looks like "pnpm isn't on cf").
Case study: tolaria v2026-06-26 (refactoringhq/tolaria, AGPL-3.0 Tauri desktop app, Jun 28 2026). conda-forge pnpm 11.9.0 ignored package.json's 20 pnpm.overrides + 5 pnpm.patchedDependencies; pnpm-workspace.yaml carried only a partial patchedDependencies (4, no overrides). pnpm install --frozen-lockfile failed in 5 s, before any compile. Pinning pnpm <11 โ pnpm 10.33.2 โ frozen install resolved 1164 packages โ full Rust/Tauri build GREEN, all tests passed. The April 2026.4 recipe had built only because conda-forge's pnpm was <11 at the time. Adjacent to G14 (tooling-version skew) and the build.bat call pnpm rule.
Question: you want a new recipe's conda-forge/staged-recipes PR to produce an osx-arm64 (Apple Silicon) build, or to verify osx-arm64 before the recipe merges.
Answer: the staged-recipes PR cannot build osx-arm64 โ by design (deep 3-angle research, 2026-06-28: pipeline code + PR-precedent sample + conda-forge docs, all consistent).
Why โ the PR matrix is hardcoded, NOT recipe-driven:
.azure-pipelines/azure-pipelines-osx.yml hardcodes a single macOS matrix cell โ osx_64: { CONFIG: osx64 } โ on vmImage: macOS-15 (an Intel image โ Azure also offers a native macOS-15-arm64 image, which current conda-smithy renders for feedstock osx_arm64 legs, but staged-recipes' fixed pipeline never uses it). It publishes only /Users/runner/bld/osx-64/. linux + win are equally fixed (linux_64 [+cuda], win_64); no arm64/aarch64 job exists in any pipeline.conda-smithy rerender per recipe. .ci_support/build_all.py reads a recipe's conda-forge.yml for only conda_build_tool; provider: / build_platform: / os_version: are ignored for the PR matrix, and the .ci_support/*.yaml are static + repo-global. (This is the load-bearing difference from feedstocks, which DO rerender โ a conda-forge.yml moves the matrix on a feedstock but has zero effect on the staged-recipes-PR matrix; same fact as the G77/v8.54.1 correction.)osx_64 only; 0 recipes ship a conda-forge.yml that produces an arm64 leg.Pre-merge osx-arm64 = LOCAL ONLY, and requires a Mac. staged-recipes ships .ci_support/osx_arm64.yaml specifically for this (jaimergp #28236: "nice for local debugging on Apple Silicon"):
conda build recipes/<name> -m .ci_support/osx_arm64.yaml -c conda-forge # or: CONFIG=osx_arm64 python build-locally.py
The build host must be a Mac (native on Apple Silicon, or an Intel Mac cross-compiling). You cannot build osx-arm64 from a Linux host โ macOS needs the macOS SDK/clang, and conda-build's crossenv can't monkey-patch sys.platform on Linux. So there is no pre-merge osx-arm64 path from a Linux box.
Shipping osx-arm64 (post-merge) โ it's OPT-IN, not a default. A new feedstock is created with only linux-64/osx-64/win-64 (conda-smithy default provider: {osx_arm64: null}). Enable via either:
osx_arm64.txt in conda-forge-pinning (the armosxaddition migration) โ the bot opens the enabling PR once deps are arm64-ready.conda-forge.yml โ provider: {osx_arm64: azure} + @conda-forge-admin, please rerender. provider enables the leg; where it runs is build_platform's call. conda-smithy's default is the identity mapping, so an osx_arm64 leg with no build_platform override runs natively on Azure's macOS-15-arm64 image (host == target, so the test phase runs โ verified in conda_smithy.configure_feedstock, 2026-09-03). Add build_platform: {osx_arm64: osx_64} only when you want the cross-compile on the Intel macOS-15 image (host != target โ --test skip). Validated both ways: lyric-py-feedstock #2 (2026-06-28) took the cross route โ its conda-forge.yml carries an explicit build_platform: {osx_arm64: osx_64} (an earlier version of this note wrongly said smithy applied that mapping automatically) โ 4/4 py osx_arm64 legs GREEN + 4/4 linux_aarch64 GREEN, PR merged; pythran-feedstock (noarch, rerendered 2026-08-18) took the native route โ provider: {osx_arm64: default} + noarch_platforms: [linux_64, osx_arm64, win_64] โ an osx_arm64_.yaml leg on macOS-15-arm64 whose tests run. For a noarch feedstock that is the whole recipe for Apple-Silicon test coverage: the artifact is already universal, only the validation leg is missing. (test: native_and_emulated controls test execution on emulated legs; cross-built artifacts skip tests via CONDA_BUILD_CROSS_COMPILATION=1 unless a native runner exists. Mind G18 on win: drop win_64 from any store_build_artifacts list.)Pre-configure a new recipe's feedstock for osx-arm64 from day 1: drop a conda-forge.yml with provider: {osx_arm64: azure} next to the staged-recipes recipe.yaml. It is ignored for the PR build but carried to the feedstock on merge, so the feedstock's first rerender enables osx-arm64 โ no post-merge expansion PR needed (applied to recipes/tolaria/, 2026-06-28).
Sources: staged-recipes .azure-pipelines/azure-pipelines-osx.yml, .ci_support/build_all.py, .ci_support/osx_arm64.yaml; conda-forge docs (staged_recipes, how-to/enable-archs, how-to/cross-compilation, maintainer/conda_forge_yml); the 2020-10-29 osx-arm64 rollout blog; staged-recipes #28236 / #29958 / #26783. Sibling of G40 (per-subdir gaps) + G77 (feedstock-vs-PR levers).
conda-forge.yml is almost entirely INERT during the PR build โ build_all.py reads ONLY conda_build_tool; every other key just SEEDS the post-merge feedstockSymptom: a recipes/<name>/conda-forge.yml sets provider / build_platform / noarch_platforms / test / workflow_settings.store_build_artifacts / bot.* / os_version / conda_forge_output_validation expecting it to change the staged-recipes PR build โ and none of it does. The PR builds on the same fixed 3-job matrix (linux_64 / osx_64 / win_64) regardless, publishes the same conda_pkgs_* artifacts regardless, and runs no autotick bot.
Why (code-traced, v8.60.0 deep-research audit): staged-recipes/.ci_support/build_all.py reads exactly one key from a per-recipe conda-forge.yml โ conda_build_tool (and only to toggle the legacy mambabuild path; the rattler-vs-conda-build choice is actually made by recipe.yaml-vs-meta.yaml presence). The matrix is repo-fixed (.ci_support/{linux64,osx64,win64,linux64_cuda12x}.yaml + the Azure --arch arg โ no osx_arm64/aarch64/ppc64le leg pre-merge, G82); the Linux image + CUDA are auto-detected from recipe text (sysroot_linux-64/c_stdlib_version==2.17โcos7; "cuda"โCUDA), not from os_version; the 3 Azure pipelines publish: conda_pkgs_* unconditionally (no store_build_artifacts gate, and the gated 7z Prepare conda build artifacts task doesn't exist in staged-recipes). conda-smithy forwards the whole file into the new feedstock's conda-forge.yml on merge โ that's where provider/test/bot.*/etc. finally activate.
Fix / mental model: keep a staged-recipes per-recipe conda-forge.yml minimal โ usually just conda_build_tool: rattler-build (v1 recipe). Any other key is there deliberately to pre-seed the feedstock (e.g. provider: {osx_arm64: azure} per G82, noarch_platforms, os_version) โ label it as such; don't expect it to do anything in the PR. To affect the PR matrix, change the recipe itself (build.skip, recipe content), not conda-forge.yml.
Corrects G77's store_build_artifacts attribution: the downloadable conda_pkgs_* artifacts on a staged-recipes PR come from the fixed pipeline, not from store_build_artifacts (a no-op in the PR; load-bearing only at the feedstock).
Full per-setting audit โ schema default ยท redundant-vs-load-bearing ยท recipe-type relevance ยท staged-PR-vs-feedstock, for every key โ is in reference/conda-forge-yml-reference.md ยง Per-setting applicability audit. Sibling of G82 + G77.
migrate_to_v1 / feedrattler is a REMOTE-feedstock tool โ it CANNOT convert a local recipes/<name>/ directorySymptom: calling migrate_to_v1 (MCP) or feedrattler with a local recipe path fails with UnknownObjectException: 404 {"message": "Not Found"} even though the path exists; the traceback shows org.get_repo(<arg>) against the conda-forge org.
Why: feedrattler clones the remote conda-forge/<name>-feedstock, converts its meta.yaml, rerenders, and opens a fork PR. Its positional arg is a feedstock name, not a path โ the MCP migrate_to_v1 wrapper forwards whatever path you pass as that name โ GitHub 404. There is no local-directory mode.
Fix โ pick by target:
recipe.yaml next to your meta.yaml): hand-author it from the meta.yaml, modeling on the nearest same-class recipe (primp for maturin/PyO3, mailpit for Go). migrate_to_v1 won't help. Validate + build, then drop meta.yaml.feedrattler <name>-feedstock <github-user> against the remote, or (recommended โ full control) hand-author the recipe.yaml on a fork branch + conda-smithy rerender locally (see Feedstock Migration and G87).trigger_build reports "No build summary found โ build may have crashed" even on SUCCESS โ confirm from the .conda artifact + rattler-build test --package-fileSymptom: after a mode="native" build, get_build_summary returns {"status": "unknown", "message": "No build summary found โ build may have crashed"} (with a stale build.pid), although the build actually succeeded.
Why: only the mode="docker" path writes build_summary.json; the native path runs rattler-build build directly and writes no summary. The missing file + leftover pid read as "crashed."
Fix: ground-truth a native build from its artifacts, not get_build_summary โ check build_artifacts/<config>/<subdir>/<name>-<version>-<build>.conda exists. rattler-build writes the .conda before the test phase, so artifact-presence alone โ tests passed โ re-run the test phase authoritatively: pixi run -e local-recipes rattler-build test --package-file <pkg>.conda and look for โ all tests passed!. (This also re-evaluates baked-in conditional tests โ e.g. a python_impl == "cpython" guard.)
bot.run_deps_from_wheel when the recipe PATCHES the wheel's dependency metadata โ the bot would regenerate run deps from the wheel and DROP your hand-managed depSymptom: a recipe patches a dependency out of the upstream wheel's METADATA (commonly to make pip check pass) but re-adds the conda equivalent in run:. With bot.run_deps_from_wheel: true, the autotick bot re-derives run: from the patched wheel on the next bump and silently DROPS the manually-added dep.
Example: dlt-pendulum patches tzdata out of pyproject.toml (0001-remove-tzdata-dep-for-pip-check.patch) yet ships python-tzdata >=2020.1 in run: โ its run deps INTENTIONALLY diverge from the wheel METADATA.
Fix: when run: deliberately differs from the wheel's declared deps (patched-out dep, vendored dep, a conda-only rename), OMIT run_deps_from_wheel from conda-forge.yml. The rest of the universal-compiled pre-seed (v8.61.0) still applies โ this is the one bot key to drop.
conda_build.error_overlinkingSymptom / trap: the local-recipes mirror's version can lag the deployed feedstock. Pushing a migration built from the stale mirror silently downgrades the feedstock. Live case: local mailpit was 1.30.2, the feedstock had advanced to 1.30.3 โ the migration PR had to bump the mirror to 1.30.3 (new sha + rebuild) first.
Procedure (full detail: Feedstock Migration):
gh api repos/conda-forge/<name>-feedstock/contents/recipe/meta.yaml --jq .content | base64 -d | grep version (and check latest upstream). Match it โ never push a lower version.upstream/main (not the possibly-stale fork HEAD): clone the fork, git remote add upstream โฆ, git checkout -b migrate-to-v1 upstream/main.cfe-* + #### CFE blocks from recipe.yaml; merge the universal keys into the feedstock's existing root conda-forge.yml and drop conda_build.error_overlinking (no-op on rattler-build, G83).build.number above the feedstock's current build to supersede the existing artifact (mailpit 0โ1, dlt-pendulum 3โ4).conda-smithy rerender --feedstock_directory <fork> -c auto regenerates the v1 CI (pixi.toml + rattler-build .ci_support + any new ARM legs from the universal-compiled block) โ ship it in the PR; no separate @conda-forge-admin rerender needed when rerendered against current pinning.Worked example (2026-06-28): conda-forge/mailpit-feedstock#20 + conda-forge/dlt-pendulum-feedstock#5 โ both v0โv1 + osx-arm64/linux-aarch64, all CI green incl. emulated ARM. Sibling of G84.
Addendum (Jul 3, 2026): when the migration rides inside a bump PR, flip conda_build_tool: rattler-build in the root conda-forge.yml BEFORE running the local rerender โ with the key still at conda-build (or absent, defaulting to it), conda-smithy looks for the just-deleted recipe/meta.yaml and the rerender crashes with a bare FileNotFoundError. Verified 3ร in one wave (django-jsonstore, django-mptt-admin, webassets โ all rerender-failed until the key flip, then green). Also: bumping a v0 feedstock with a script expecting recipe/recipe.yaml silently no-ops โ probe the recipe FORMAT before editing (the psycopg2-instrumentation bump shipped a rerender-only commit mislabeled as the bump until the meta.yaml edit followed up).
buf.validate, generated <ns>/) collide across a closure โ one owner at the newest superset version; strip from siblings (often NOT cf-submittable)Symptom: two+ packages in a dependency closure each need โ and/or bundle โ the SAME top-level protobuf-generated namespace (classically buf.validate from buf generate, but any protoc/buf-generated shared package: buf/, cel/, google/cloud/* stubs). One or more of:
pip install <lib> then import <lib> fails ModuleNotFoundError: No module named 'buf'. The stubs are buf generate'd into a gen/ dir that the build backend (hatchling default, etc.) never packages.<ns>/ โ conda file-collision (conda forbids two packages shipping <ns>/โฆ/foo_pb2.py).AttributeError: <SOMEFIELD>_FIELD_NUMBER (or a missing message): the package that "wins" the shared namespace bundles an OLDER copy than a consumer expects.On PyPI these are latent (pip lets the last-installed win; the broken import path is exercised lazily), so upstream ships them "working" in the exact pinned set. conda-forge surfaces them hard: exactly one conda package may own a given file path, and incompatible versions of that path can't coexist.
Detect (before treating a "trivial noarch" protobuf-stub dep as clean):
# 1. does the wheel actually ship the generated namespace?
pip install <lib> && python -c "import <lib>" # ModuleNotFoundError: No module named 'buf' ?
unzip -l <lib>.whl | awk '{print $4}' | awk -F/ '{print $1}' | sort -u # buf/ cel/ present, or only <lib>/ ?
# 2. do two closure members both ship the namespace, at different versions?
python -c "import <a>; import <b>" # AttributeError: <FIELD>_FIELD_NUMBER = version skew
Resolve (local-only, or when upstream will cooperate) โ make exactly ONE package the single owner of the shared namespace at the NEWEST (superset) version, and strip it from the others:
protovalidate owns buf.validate). If its wheel omits the stubs, patch its build to ship them โ e.g. hatchling [tool.hatch.build.targets.wheel.force-include] "gen/buf" = "buf" (a source patch, G59). Verify import <owner> then works standalone.shutil.rmtree(<purelib>/<ns>)) and add a run: dep on the owner. Verify the owner's newer stubs are a superset the siblings' generated protos also tolerate (import <sibling> after the swap) โ a newer buf.validate is usually additive, so it satisfies both, but confirm empirically.buf/validate only, not buf/validate/conformance/โฆ if unused; and NOT cel/ if the consumer uses celpy not cel.expr) to avoid a further collision with cel-python etc.cf-submittability: this pattern is usually NOT cf-submittable without upstream coordination โ a package shipping another's generated stubs, wheel-repackaging, or a circular ownerโsibling dep all draw reviewer rejection. File upstream: (a) the stub-omitting-wheel bug, (b) the "depend on the stub-owner instead of bundling the namespace" ask. Record cfe-on-conda-forge-status: blocked-pending-prerequisites + the specifics in cfe-forge-blocker-list; build local-only meanwhile.
Related generator gap (found same effort): generate_recipe_from_pypi emitted noarch: python for pyqwest โ a maturin/pyo3/extension-module cdylib โ instead of a compiled recipe (the v8.9.0 maturin path mis-routed because Cargo.toml declared no explicit abi3 feature despite shipping abi3 wheels). Always verify the generated build shape against the sdist (G46/G48/G49): any [tool.maturin] + pyo3/extension-module / crate-type=["cdylib"] is compiled, never noarch.
Case study: flyte-2 SDK closure (docs/specs/flyte-conda-forge.md, 2026-07-01). protovalidate 1.2.0's wheel ships NO buf/cel stubs (broken standalone on every version 0.13โ1.2); flyteidl2 2.0.26 is wheel-only (no sdist) and bundles an OLDER incompatible buf.validate (import protovalidate with flyteidl2 present โ AttributeError: CEL_EXPRESSION_FIELD_NUMBER); both need buf/validate/validate_pb2.py. Local-only resolution: protovalidate force-includes gen/buf (owns the newer, superset buf.validate), flyteidl2 strips its bundled buf/ and depends on protovalidate. Full closure (6 recipes) built + import flyte + pip check GREEN locally; not submitted (upstream buf.validate blocker). Sibling of G51 (release-generated assets missing from the artifact), G54/G55 (wheel-only/no-sdist source + build backend), G10 (name divergence).
restart ci is a silent no-op; kick fresh CI with please rerender (which can disable bot-automerge)Symptom: a red [bot-automerge] version PR has sat for weeksโmonths. @conda-forge-admin, please restart ci produces no new build: the failing checks keep their old buildId links, the Azure timeline API returns 404 for those builds, and the automerge bot re-evaluates without any fresh run appearing.
Why: Azure DevOps prunes run data (~30 days). A pruned run cannot be restarted, so webservices' restart silently does nothing; the stale failing GitHub statuses persist pointing at dead builds. Consequences: the PR looks permanently red, and its logs are unrecoverable โ the G32 signature triage has nothing to read (log fetches 404).
Fix: comment @conda-forge-admin, please rerender. Webservices push a rerender commit to the bot branch โ a synchronize event that triggers a FULL fresh CI run (and a months-old bot PR is due a rerender against current pinning anyway). Know the side-effects:
[bot-automerge] will land them.Distinguish young red PRs (Azure run still live): there restart ci works normally and adds NO commit, so automerge stays armed โ three such PRs automerged minutes after a restart in the case study.
Case study: the 2026-07-02 tracker punch-list batch โ 34 red autotick PRs across rxm7706 feedstocks. restart ci fired on 28: only the young builds actually re-ran (3 went green โ automerged); the rest kept dead buildId links (verified: zxing-cpp #3's failing check still pointed at pruned buildId 1458567; timeline 404). The follow-up please rerender batch produced fresh CI on all 28; greens + fix-push targets were then worked manually.
Symptom: batch-running generate_recipe_from_pypi โ build produces spurious failures that look like recipe problems: YAML parse errors at render, REPLACE_LICENSE SPDX lint failures, ModuleNotFoundError: No module named 'tests' at the import test โ and the emitted recipe carries no #### CFE metadata block at all.
Why โ five gaps verified on a 22-recipe batch (2026-07-02/03):
recipe-generator.py pypi pkg==X dies with Error: 'releases'; unpinned generation works (targets PyPI latest).info.license holds the FULL license text it is emitted verbatim into about.license (unquoted colons โ YAML parse error, or SPDX lint fail), and REPLACE_LICENSE is left when classifiers don't resolve โ PEP 639 info.license_expression is never consulted.tests, src) leak into tests.python.imports โ guaranteed import-test failure (a G7 sibling).#### CFE metadata AND comments convention is not implemented in the generator โ every local recipe must be stamped manually (with cfe-local-build-* reflecting the ACTUAL build outcome).Fix โ interim sanitize pass between generate and build: strip bogus import names (tests|test|docs|examples|src); resolve the license via PyPI license_expression โ OSI classifier โ GitHub license API (repo from about.repository), replacing full-text/placeholder values; on a "No license files were copied" build failure, fetch the repo's LICENSE into the recipe dir (G4 pattern 2); then validate_recipe โ build โ stamp the cfe block with truthful cfe-local-build-*. The durable resolution is fixing the generator (follow-ups recorded in the v8.65.0 CHANGELOG entry).
Case study: the 2026-07-02 punch-list CFE batch โ 22 feedstock recipes regenerated at their audit-target versions. First naive generateโbuild pass: 4 of 5 completed builds failed on emission classes, zero real causes surfaced. After the sanitize gates: 9 verified GREEN at target (fix material pushed to their bot PRs), and remaining failures reduced to genuine causes โ 2 net-new prerequisites absent from conda-forge (django-fsm-2 for django-fsm-log 5.0.2, agentevals for universal-mcp 0.1.25), plus per-recipe issues (e.g. the G10 ruamel_yamlโruamel.yaml divergence in pydantic-yaml).
Addendum (Jul 3, 2026, batch v5 โ 52 recipes): gap (2) has a second class โ SHORT non-SPDX license strings emitted verbatim (MIT License, MIT license, BSD 2-Clause License, Apache License, Version 2.0, Apache Software License (Apache 2.0), bare BSD, deprecated id AGPL-3.0) โ rattler-build REJECTS the recipe at parse time (failed to parse SPDX license); 10 of 52 recipes hit this, so a sanitize pass that only catches full-text/multiline licenses misses it. Normalize the common forms (โ MIT, BSD-2-Clause, Apache-2.0, AGPL-3.0-only, โฆ); bare BSD is ambiguous โ resolve against the feedstock's license field (webassets โ BSD-2-Clause). Also verified: PyPI metadata can be completely license-empty (no license, no license_expression, no License classifier โ azure-ai-ml, django-wildewidgets) โ the resolution chain must end at feedstock-license / GitHub-license-API authority, never at a REPLACE_LICENSE placeholder. One more emission gap: with a name containing capitals upstream, the generator writes a case-variant output dir (see G94).
Addendum 2 (Jul 3, 2026 โ prereq-chain + Set D waves): three more verified emission gaps, all with multiple live instances:
6. Run-block TRUNCATION on long dep lists โ the emitted run: silently stops mid-list: universal-mcp 11/26, sqlmesh 10/20, langgraph-api 11/32, azure-ai-ml, confluence-markdown-exporter, kedro-viz, azure-monitor-opentelemetry (cut at 11/14). Count-check every generated run: against upstream requires_dist (marker-free entries) โ the truncation is silent, and under G95 it is invisible until CI.
7. Bogus imports, dotted + non-Python forms โ beyond bare tests/src (gap 3): dotted src.<pkg> (src.pydantic_yaml, src.ydata_profiling, src.tests, src.django_components) and non-Python top-level dirs (go from feast's Go tree). The sanitize regex must handle - (src|tests?|docs|examples)(\.\S+)?$ AND verify the final name against the sdist's actual top-level packages.
8. Unfilled placeholders beyond license/maintainer โ REPLACE_SUMMARY / REPLACE_DESCRIPTION in about: (langgraph-api โ reached a submitted draft PR before a human caught it). Sanitize must grep the whole emitted recipe for REPLACE_ before calling it done.
Also 2 near-miss classes from the same waves: the generator DOES emit context.python_min when upstream's Requires-Python exceeds the cf floor โ check before inserting your own (3 duplicate-key incidents: django-wildewidgets, langgraph-runtime-inmem, langgraph-api); and license-file-less sdists whose GitHub repo is unavailable can take the canonical SPDX text (raw.githubusercontent.com/spdx/license-list-data/main/text/<ID>.txt โ used for the Elastic-2.0 langgraph pair).
Addendum 3 (Jul 14, 2026) โ two emission gaps on the COMPILED path, both self-contradictory output:
9. noarch: python emitted ALONGSIDE compiler('c') for a Cython package โ the two cannot both be true, and stdlib('c') was missing as well (STD-001). Hit on both compiled recipes generated in the reactpy-v2 chain (http-router, asgi-tools), so treat it as the default outcome for any .pyx sdist, not a one-off. Verify the shape from the sdist per G46/G48: .pyx/.pxd present + zero py3-none-any wheels on PyPI โ compiled โ drop noarch, add stdlib('c'), drop the CFEP-25 cross-version test triad (G49).
10. skip: py>=400 โ a v0-form selector that v1 silently ignores (G3), i.e. it was doing nothing at all. The genuine need (asgi-tools' upstream requires-python = ">=3.11,<4" vs the cf matrix starting at 3.10) requires the v1 form skip: match(python, "<3.11"). Confirmed working: the py3.10 variant reported Skipping asgi-tools 3.0.0 - skip conditions evaluated to true while 3.11/3.12/3.13 built.
uv_build backends and hatchling metadata hooks fail at METADATA-PREP; read [build-system].requires from the sdistSymptom: the build dies inside pip's metadata preparation (error: metadata-generation-failed / ERROR: Exception mid-resolver) with either
pip._vendor.pyproject_hooks._impl.BackendUnavailable: Cannot import 'uv_build' โ the pyproject declares the uv_build backend but host: carries setuptools; orhatchling.plugin.exceptions.UnknownPluginError: Unknown metadata hook: requirements_txt โ hatchling is present but a plugin it needs is not.Why: the generator emits a guessed backend (usually setuptools) instead of propagating the sdist's [build-system].requires. Two increasingly common 2026 shapes: the uv_build backend (uv-build on conda-forge), and hatch plugin deps โ metadata hooks (hatch-requirements-txt), version hooks (hatch-vcs) โ declared in requires but invisible to a backend-name-only check. This is the plugin-level sibling of G55 (which covers the backend itself).
Fix: read the sdist's pyproject.toml [build-system].requires and mirror it into host: verbatim (conda names usually match: uv-build, hatch-requirements-txt, hatch-vcs); when a feedstock exists, its host: block is the verified answer (openlineage pins uv-build >=0.9.4,<0.10.0). The two error strings above are the triage signatures โ both mean "host is missing a build-system requirement", never a recipe-structure problem.
Verified: shot-scraper 1.10 (uv_build), kedro-viz 12.4.0 (hatch-requirements-txt) โ both built GREEN after the host fix (2026-07-03).
#### CFE comment block into plain inline keys โ a subsequent stamp append then DUPLICATES them (parse-fatal); strip BOTH forms before stampingSymptom: rattler-build refuses the recipe with Failed to parse YAML: Duplicate key "cfe-conda-name". Inspection shows the cfe-* keys twice: once as ordinary inline keys under extra: (no #### markers, long values line-wrapped by the emitter), and once as the canonical bottom block.
Why: the #### header/footer lines are YAML comments; the cfe-* keys are real keys under extra:. Any parseโdump round-trip (feedstock_enrich.py, or any tooling that loads and re-emits the YAML) keeps the keys but drops/reflows the comments โ the block "folds" into inline keys. Tooling that strips-by-marker (regex on #### CFE metadata AND comments) then finds nothing to strip and appends a fresh block โ duplicate keys. The recipe still parses BETWEEN the fold and the re-stamp (inline cfe keys are valid YAML), so the corruption surfaces one pipeline stage later, disguised as an unrelated parse failure.
Fix: every cfe-strip (before stamping AND before push) must remove both forms: (a) the marker-to-EOF comment block, and (b) inline cfe-* keys plus their continuation lines (deeper-indented wrapped values and list items). Post-batch audit: grep -c 'cfe-conda-name:' recipes/*/recipe.yaml must be exactly 1 per stamped recipe. Pipeline rule: any stage sequence that both re-serializes and appends must strip-before-append idempotently.
Verified: 4 of 888 recipes corrupted this way in the 2026-07-03 batch (enrich โ โฆ โ stamp); a repo-wide audit + two-form strip restored all four, and the one "unexplainable" build failure among them (django-mptt-admin) built GREEN immediately after the cleanup.
Extension (v8.68.0) โ two MORE corruption classes reached COMMITTED history; the duplicate-key grep audit misses both; a FULL-PARSE audit is the stronger gate. The 2026-07-04 metadata-refresh pass found, already committed to HEAD: (a) stray [] fold-artifact lines โ a lone flow-empty-list line left mid-extra: by an earlier re-serialization, making the file unparseable or a landmine for the next line-based editor (4 recipes: smartsheet-python-sdk, dbt-protos, markdown-to-confluence, prophecy-build-tool); and (b) unquoted-# in flow-list values โ cfe-forge-blocker-list: [staged-recipes #34052 held โฆ] where YAML treats # as a comment start, swallowing the closing ] (parse-fatal) or, in block-list items, silently truncating the value at the # (5 recipes: langgraph-api, langgraph-runtime-inmem, sqlglot, sqlglotrs, sqlmesh). Rules: free-text cfe values containing # or : must be double-quoted; and the post-batch audit is yaml.safe_load over EVERY recipes/*/recipe.yaml (a full-parse sweep โ 898 files in seconds), not just the grep -c 'cfe-conda-name:' == 1 duplicate check, which is blind to both classes. A stray-[] line is identified by its predecessor: legitimate only when the previous line is a bare key:; otherwise delete it. Enforced since v8.68.1 by tests/meta/test_recipe_yaml_parse_audit.py (full parse + duplicate-cfe-key + stray-[] + unquoted-# checks over every recipe.yaml).
Symptom: validate_recipe / CI-parity lint fails with "Error parsing recipe with conda-recipe-manager: ParsingException('The recipe parser ran into an unexpected issueโฆ')" on a recipe that rattler-build parses, builds, and tests GREEN. The underlying error (visible when calling RecipeReader directly) is IndexError: pop from empty list in _construct_parse_tree.
Why: conda-recipe-manager's hand-rolled indent tracker underflows its node stack on comment lines at column 0 in the middle of the document inside indented contexts โ e.g. commented-out dep/import list items like # - docs under tests: or run:. The line-1 schema header and the document-TAIL #### CFE block (after extra:, nothing following) do not trip it. Since conda-smithy's linter runs CRM, a staged-recipes/feedstock PR carrying such comments reds the lint leg even though the build is fine.
Fix: never leave column-0 comment lines mid-document โ this is the lint-level enforcement of the existing "no inline agent comments" convention: relocate commented-out deps/imports and rationale into the bottom # CFE comments block (or delete them). To locate the offender in a big recipe, bisect prefixes against conda_recipe_manager.parser.recipe_reader.RecipeReader.
Verified: shapash 2.9.0 โ two col-0 comment runs (9 commented optional deps, 6 commented import exclusions) crashed CRM; relocating them into the CFE comments block turned the same recipe lint-green (2026-07-03).
Symptom: a recipe that builds GREEN fails validate_recipe/lint on files it doesn't even reference, or "regeneration" appears to produce an old version.
Why โ three verified classes (2026-07-03 batch):
conda_build_config.yaml snapshots mirrored years ago: openlineage-integration-common carried a 2023-era full pinning matrix (python 3.7 rows, MACOSX_DEPLOYMENT_TARGET) โ conda-smithy errors "The MACOSX_DEPLOYMENT_TARGET key โฆ needs to be removed or replaced by c_stdlib_version" โ while the live feedstock had deleted the file (404) long ago.meta.yaml after the feedstock completed v0โv1 โ the keep-meta-until-migrated rule's END condition: once the feedstock's recipe/ ships only recipe.yaml, delete the local meta.yaml (leaving it re-triggers mixed-format handling and stale-mirror drift).recipes/pyobjc-framework-CoreText/) while the mirror stem is the lowercase feedstock name (recipes/pyobjc-framework-coretext/) โ the fresh recipe lands in a stray new dir and the real mirror stays stale, which reads as "generation produced an old version" (GEN=10.1 on a 12.2.1 target). Post-generate dir checks must include case variants alongside hyphen/underscore swaps; delete the strays after merging.Fix: on every mirror refresh, list the live feedstock's recipe/ dir (and root conda-forge.yml) and prune local files the feedstock no longer has; after any generation, verify the output landed in the intended dir (case-insensitive candidate match). A stale-file failure looks exactly like a recipe defect โ check file provenance before debugging recipe content.
build-clean-test-blocked means metadata-UNVERIFIED โ a recipe whose local test env never solved has an unexercised import list and pip check; statically verify both before shipping it anywhereSymptom: a batch-"verified" recipe (local status build-clean-test-blocked) reds feedstock CI on defects local verification should have caught: bogus test imports (ModuleNotFoundError: No module named 'src' / 'go') or pip-check failures listing half the runtime deps as "not installed".
Why: when the local test environment cannot solve (a dep missing from cfโชlocal-channel), rattler-build stops before the test script โ the import test and pip check NEVER RUN. Every latent generator defect (G90 bogus imports, G90/G96-class truncated run blocks) survives the batch invisibly and surfaces on the first CI run that CAN solve. Verified at scale (2026-07-03 Set D wave): 8 of 8 CI-red bump PRs were exactly the BCTB-local recipes; the fully-tested (success) recipes went green first-pass.
Fix: treat BCTB as "build verified, metadata unverified" and add a STATIC gate for BCTB recipes before pushing them anywhere: (1) every tests.python.imports entry must exist as a top-level package in the sdist (tar tzf | grep '__init__.py'); (2) the run: list must cover upstream requires_dist (marker-free entries), count-checked; (3) dep names spot-checked against cf (G10 four spellings + dotted names like zope.dottedname). The cfe stamp records the distinction โ never upgrade BCTB to success without the test phase actually running.
requires_dist diff โ NEVER the regenerated recipe aloneSymptom: a version-bump PR reds on pip check listing deps the OLD feedstock recipe always had (boto3, wrapt), or on missing packaging-time facts the old recipe encoded (host setuptools-scm, pybindgen, entry points, python <X caps, license sidecar files).
Why: the generator regenerates from PyPI metadata alone and loses feedstock-accumulated knowledge โ plus its own emission gaps (G90) including run-block TRUNCATION. The old feedstock recipe encodes years of fixes: full dep pins, build-backend host deps, platform caps, test shapes, recipe-dir sidecar files (LICENSE fetched per G4, build assets). Verified across the 2026-07-03 Set D wave: otel-boto3sqs (dropped boto3+wrapt+otel pins), feast (lost host setuptools-scm/pybindgen โ G39 0.0.0 dist-info + lost prometheus_client cap), sqlmesh (lost setuptools-scm + 10 of 20 run deps + entry points), kedro-viz (lost the recipe-dir LICENSE + hatch-requirements-txt).
Fix โ the bump-PR assembly rule: start from the FEEDSTOCK's current recipe (its host/build/tests/sidecars are the baseline), apply the version+sha bump, then diff run: against the new version's requires_dist and apply only the REAL upstream dep changes (conda-name-mapped). Extras map explicitly: uvicorn[standard]โuvicorn-standard, dask[dataframe]โdask, SQLAlchemy[mypy]โsqlalchemy, pkg[extra-with-new-deps]โexpand the extra's deps. Copy every recipe-dir sidecar file the feedstock carries into the PR branch. The regenerated local recipe is a cross-check, not the source.
Symptom: a recipe's test env fails Cannot solve with a huge exploration tree; every direct dep exists on cf at satisfying versions, and the tree's leaves blame seemingly unrelated packages (libabseil 20260107.1 conflicts with the versions reported above, python_abi variants "conflict with versions previously reported").
Why: conda-forge's compiled stacks move through ABI migration epochs (libabseil/libgrpc/libprotobuf/libarrow rebuild waves). Two Python deps that each solve FINE alone can be jointly unsolvable when one pins a grpc-era library (grpcio <1.81 โ libgrpc ==1.80.0) and the other's current rebuilds link the next epoch (pyarrow โ libarrow โ libgoogle-cloud โ libgrpc 1.81) โ a diamond no recipe edit can fix. Verified: universal-mcp 0.1.25 (langgraph-api grpcio >=1.80,<1.81 + grpcio-tools ==1.80.0 vs langchain-google-vertexaiโpyarrow), 2026-07-03.
Fix โ the diagnosis method: for each suspect C-library, pull per-build depends from the channel API (api.anaconda.org/package/conda-forge/<pkg>/files) and tabulate the era pins (which libabseil/libgrpc each version's builds link). If the required combination has no common era, the env is upstream-blocked: record it verbatim in cfe-forge-blocker-list with a recheck trigger (next upstream release re-pinning, or the cf migration completing). Do NOT loosen the pins in the recipe to force a solve โ the wheel metadata still enforces them at pip check, and the ABI mismatch is real.
\g<1> in re.sub, provenance-check failures against git โ and purl-spec normalization keeps DOTSSymptom: a scripted batch edit across hundreds of recipe.yaml files corrupts a file mid-run, or "fails" on a file that was already broken before the batch started โ or emits PyPI purls with over-normalized names.
The discipline (proven on the 2026-07-04 437-recipe refresh โ 3 in-flight failures caught, 0 corrupted files shipped):
yaml.safe_load(p.read_text()) immediately after each file's write, aborting the batch on failure. This catches (a) your own edit bugs and (b) pre-existing corruption the moment it's touched โ the 2026-07-04 run caught one of each class within seconds of introduction.re.sub replacement strings: use \g<1>, never \1, when the following literal starts with a digit. re.sub(r'(key: ).*', r'\1' + '2026-07-04โฆ', s) parses \12026โฆ as octal escape \120 โ a literal P silently replaces the group. This corrupted a timestamp to P26-07-04โฆ before the parse gate caught it.git diff the file: if the offending line shows as context (unchanged), the corruption predates the batch (committed G92-class artifacts โ see the G92 extension). Repair the pre-existing defect surgically; don't revert your own valid edits, and don't debug your editor against someone else's corruption.purl conventions (the canonical cfe-purls forms, aligned with the atlas purl export): the conda purl carries the channel qualifier โ pkg:conda/<name>@${{ version }}?channel=conda-forge; the PyPI purl name uses purl-spec normalization: lowercase + _โ- ONLY โ dots are PRESERVED (pkg:pypi/fs.googledrivefs, pkg:pypi/pymilvus.model). PEP 503 normalization (which collapses runs of .-_ to -) is the WRONG rule for purls โ it over-normalizes dotted project names into identifiers that don't exist on PyPI (2 live cases found and fixed). When a purl name misses the PyPI index, check the dotted form before assuming a G10 divergence.
uniqueItems makes universe-scale CycloneDX validation intractable; strip + exact O(n) hash check + chunked parallel walkSymptom: validating a REAL full-universe CycloneDX BOM (856,766 components, 161 MB) with the test suite's exact Draft7 validator sits at 100% CPU indefinitely (killed after 75 min of a projected multi-day run) โ while the same validator finishes fixture tests instantly and a 10k-component benchmark in 44 s.
Why: CycloneDX 1.6 declares uniqueItems: true on the components array, and jsonschema implements uniqueItems over non-hashable items (dicts) as an O(nยฒ) pairwise equality scan โ ~3.7ร10ยนยน comparisons at 856k components. Two compounding traps: (a) the quadratic term is invisible at fixture scale, so the unit suite proves nothing about the real artifact; (b) it poisons naive benchmarks โ the 10k sample's 44 s was itself dominated by the quadratic part (~5ร10โท pairs), so linear extrapolation predicted ~63 min when the true cost was days (without uniqueItems, the walk runs ~3,500 components/s per worker).
Fix (semantically identical verdicts; 31 s total on 856k): (1) strip uniqueItems from a schema COPY; (2) check that exact predicate in O(n) via canonical-JSON identity (sha1(json.dumps(c, sort_keys=True)) into a set โ JSON equality IS the uniqueItems predicate); (3) chunk-parallelize the remaining walk across workers, each chunk-doc carrying the real envelope so per-component verdicts equal the single-pass walk. Record the verdict-equivalence argument alongside the gate result.
General rule: the live-verification principle applies to validators, not just data. Before trusting any schema/validator at universe scale: inspect the schema for uniqueItems/pattern-heavy constructs, and benchmark at TWO sample sizes (10k vs 40k โ a quadratic term shows as ~16ร not ~4ร) before extrapolating an ETA.
Case study: cyclonedx-universe-inventory Wave B local gate (2026-07-05) โ the S4 "validate the REAL full BOM" gate. First run killed at 75 min; the 10k benchmark misdiagnosed the cost as ~63 min linear; schema inspection found uniqueItems; the strip + O(n) exact check + 14-worker chunked walk validated all 856,766 components (VALID, 0 duplicate components) in 31 s wall.
__unix cannot serve Windows and fights the symlink checkSymptom: a noarch: generic npm CLI recipe needs three compensating hacks โ replace npm's bin/<name> symlink with a bash wrapper (rattler rejects symlinks in noarch), strip node_modules/.bin (G6), and gate installation with if: unix then: __unix โ and the result is still uninstallable on Windows (the bash wrapper cannot run there; a single noarch artifact cannot carry both unix symlinks and win .cmd shims).
Why: npm lays down platform-native bin shims at install time (symlinks on unix, .cmd/.ps1 on win), so the artifact is inherently per-platform even when the JS payload is portable. The two conda-forge-proven npm recipes in this repo โ openspec (merged feedstock) and bmalph โ are BOTH per-arch for exactly this reason.
Fix โ the per-arch npm CLI shape (no wrappers, no .bin stripping, no __unix):
build:
number: 0 # NOT noarch
script:
- if: unix
then:
- export npm_config_prefix="${PREFIX}"
- npm pack --ignore-scripts
- npm install -g ./<tgz-name>-${{ version }}.tgz
else:
- call npm config set prefix "%PREFIX%"
- if %ERRORLEVEL% neq 0 exit 1
- call npm pack --ignore-scripts
- if %ERRORLEVEL% neq 0 exit 1
- call npm install --userconfig nonexistentrc -g <tgz-name>-${{ version }}.tgz
- if %ERRORLEVEL% neq 0 exit 1
(call on every .cmd shim per the build.bat rule; add the pnpm install --prod + pnpm-licenses pair on both branches when the package has runtime deps.) noarch: generic remains correct only for bin-less node libraries (pptxgenjs) and content bundles (skills collections) โ nothing platform-native gets written.
Case study: the 2026-07-12 win-64 sweep โ caveman, ccusage, claude-mem, cdxgen, pi-coding-agent converted noarchโper-arch; codegraph authored per-arch from the start (its npm dist bundles per-platform native binaries, G101). The scoped-tarball pack names differ from the package name (@cyclonedx/cdxgen โ cyclonedx-cdxgen-<ver>.tgz).
optionalDependencies; NOT conda-forge-submittable, and a "segfault" is usually rattler-build's own relocation, not the vendorSymptom: an npm CLI's published tarball contains only a tiny npm-shim.js/src/cli.js, dependencies is empty, and optionalDependencies lists @scope/<name>-<os>-<arch> platform packages. The CLI either errors "native binary is not available for <platform>" (installed with --omit=optional) or โ worse โ the packaged native binary segfaults (exit 139) inside the built conda package.
Why: upstream compiles the app with Bun into static-PIE per-platform executables and publishes the JS package as a thin dispatcher. Bun is not on conda-forge, so there is genuinely no source-build path โ this package can never be conda-forge-submittable, only fine for a private channel.
CORRECTED 2026-09-07 (was wrong from 2026-07-12 to 2026-09-06): the segfault is usually self-inflicted, not the vendor's fault. The original diagnosis blamed "the upstream binary segfaults on this host" and prescribed pinning away from the broken version as the only fix. Re-investigated on ccusage 20.0.20 with a byte-level comparison: the npm-original @ccusage/ccusage-linux-x64 binary runs clean 10/10 standalone, but the SAME bytes built into a conda package still segfaulted at status 139. Diffing the packaged binary against the npm original found 584 differing bytes starting at offset 41 (inside the ELF header) โ rattler-build's default binary relocation had rewritten ELF sections to patch RPATH, shifting every offset in Bun's embedded bundle. With dynamic_linking: binary_relocation: false the packaged binary is byte-identical to upstream and every test passes. Any prebuilt single-file binary with an embedded payload (Bun, Deno, PyInstaller, Go with embedded assets) is exposed to the same failure โ suspect relocation before the vendor.
Fix: (1) detect early โ inspect the published tarball's package.json for platform-named optionalDependencies and a shim-sized payload; the REPO's package.json may look completely different (codegraph's repo showed bin: dist/bin/codegraph.js + 10 real deps; the published tarball had bin: npm-shim.js + 6 platform optionals). (2) The recipe must be per-arch (G100) so npm fetches the matching platform binary. (3) The CLI test must actually EXECUTE the binary (<name> --version) โ file-existence tests would false-green a segfaulting native. (4) Before assuming the vendor binary is broken, diff it: sha256/byte-compare the binary inside the built package against the npm original. A mismatch starting inside the file header, with the size grown by a small constant, is rattler-build's relocation โ fix with build: dynamic_linking: binary_relocation: false (safe here: the payload is statically linked, nothing needs relocating). (5) Only if the byte comparison shows the packaged binary IS identical to upstream (a genuine vendor crash) should you pin to the last pure-JS, node-runnable version and record the reason in cfe-forge-blocker-list. On any future version bump of a package like this, re-diff โ a green npm install proves only that the tarball unpacked, not that the binary survived intact.
Case study (2026-07-12): codegraph 1.4.1 โ native works, per-arch recipe bundles it, GREEN. ccusage โ v20.0.17's @ccusage/ccusage-linux-x64 Bun binary segfaulted 10/10 on the build host; misdiagnosed as an unfixable vendor crash and pinned to 19.0.3 (last pure-JS release). Corrected 2026-09-07: re-investigated at 20.0.20 with the byte-diff procedure above, found rattler-build's own relocation was the actual cause, fixed with binary_relocation: false, and lifted the pin (recipes/ccusage/recipe.yaml's own dated context comment carries the full before/after evidence). The <20 pin is unfixable framing cost roughly two months before someone thought to diff the bytes.
Symptom: a noarch recipe that is green on the linux/osx legs fails the staged-recipes win-64 leg with Error: ร No license files were copied โ even though license_file points at a file that exists in the source. The win log shows the build script ran as IF "" == "" (call build_env.bat) โ i.e. nothing.
Why: three stacked facts. (1) staged-recipes CI builds every recipe on all three platform legs, noarch included (target_platform: noarch on the win runner). (2) An inline script: - if: unix then: [...] with no else renders to an empty script on win โ the build "succeeds" having installed nothing; similarly a recipe-dir build.sh with no build.bat runs nothing on win. (3) rattler copies license files only after packaging, and one missing file in license_file aborts the whole copy โ a build-generated file (third-party-licenses.txt from pnpm-licenses) is the classic casualty, since the empty script never generated it.
Fix: every recipe headed to staged-recipes needs exactly one of: a win else: branch (G100 shape, call + errorlevel checks), a build.bat next to build.sh (rattler auto-selects when no script: key is set โ the bmad-utility-skills pattern), or an explicit build: skip: - win for deliberately unix-only tools (upstream errors on Windows, bash-orchestrator payloads). Related trap: npm tarballs often omit LICENSE (the files array excludes it) โ vendor it from the repo tag as a second source with file_name: LICENSE, and remember "No license files were copied" also fires when ANY declared license file is missing, not just all of them.
Case study: pptxgenjs on staged-recipes PR #34176 (2026-07-12) โ linux legs green, win-64 leg dead at the license copy because the unix-only script never ran pnpm-licenses. Fixed with the full win branch; the same sweep audited every branch recipe (5 npm CLIs โ per-arch, quarkdown gained the upstream windows-x64.zip source + build.bat, bmad-story-automator + bmad-autopilot got skip: win).
engines version caps into conda run deps โ the host nodejs run-export makes the combined constraint unsolvableSymptom: an npm recipe with run: nodejs >=20,<25 (mirroring the package.json engines field) builds fine but its test env fails to solve: the artifact's run deps carry BOTH the explicit nodejs >=20,<25 AND nodejs >=26.5.0,<27.0a0 โ an empty intersection.
Why: conda-forge's nodejs has a run_exports, so whatever nodejs lands in host: pins a major-series run constraint into the artifact automatically. An explicit engines-derived cap in run: intersects with it; when the host solved a newer major than the engines cap allows, the package can never install. npm engines caps are advisory (upper bounds are routinely stale โ tools run fine on newer majors).
Fix: put the FLOOR (if any) on host: (which selects the variant and hence the run-export) and leave run: nodejs bare โ or omit both and let the run-export do all the work. Only mirror an engines ceiling when upstream demonstrably breaks on newer node, and then constrain the HOST so the export matches.
Case study: codegraph (2026-07-12) โ run: nodejs >=20,<25 from engines vs a nodejs-26.5 host export = unsolvable test env; fixed by dropping the explicit pin. Note the local variant config builds nodejs 24 AND 26 variants for bare-nodejs recipes โ two artifacts, each tested against its own major (which is how the ccusage/G101 crash surfaced on the 24 variant).
Symptom: a recipe's test env fails to solve with package X is excluded because due to strict channel priority not using this option from 'conda-forge' โ even though conda-forge has exactly the version needed.
Why: local builds solve with the local build_artifacts channel at top priority and strict channel priority: if the local channel contains ANY version of a package name, ALL conda-forge versions of that name are excluded from resolution. A years-old local recipe (portalocker 2.7.0) silently caps every downstream consumer's ceiling for that package.
Fix: refresh the local recipe to the needed version and build it (the new artifact joins the old in the local channel โ old versions stay resolvable for consumers pinned to them, e.g. >=2.7,<2.8). Loosening the consumer's floor to the stale version is usually the wrong direction โ check the consumers (grep -l <name> recipes/*/recipe.yaml) before deciding which side moves.
Case study: openkb (2026-07-12) needed portalocker >=3.2.0; conda-forge had 3.2.0 but the local channel's 2.7.0 shadowed it. Bumped the local portalocker recipe 2.7.0โ3.2.0 (also fixing its unconditional pywin32 run dep โ win-gated, and its stale PSF-2.0 license โ BSD-3-Clause); the 2.7.0 artifact stays for the <2.8-pinned consumers.
pip check BY DEFAULT โ an omitted pip_check: means true, and the repo convention is explicit true + dual python_versionSymptom: a python test block written without pip_check: fails at test time with e.g. <pkg> requires <dep>, which is not installed โ the author believed pip check was opt-in.
Why: in the v1 python test element, pip_check defaults to true. Separately, a single python_version: ${{ python_min }}.* only exercises the floor interpreter.
Fix โ the repo's canonical python test block:
tests:
- python:
imports:
- <module>
pip_check: true
python_version:
- ${{ python_min }}.*
- "*"
Downgrade to pip_check: false ONLY with a factual blocker, recorded twice โ an inline comment above the line AND the cfe-pip-check: "false:<reason-code>" field. Confirmed blocker classes: upstream exact == pins that the loosened conda deps legitimately violate (openkb, hermes-agent); a PyPI shim dist with no conda dist-info (dotenv โ python-dotenv); a dep satisfied by a non-Python conda package (ast-grep-cli โ the ast-grep binary, headroom-ai); a transitive third-party METADATA skew the solver picks badly (pageindex's fix was raising the openai-agents floor so pip check PASSES โ prefer fixing the pick over waiving).
Case study: headroom-ai (2026-07-12) โ first build failed at pip check (ast-grep-cli โฆ not installed) precisely because the test block omitted pip_check, assuming off-by-default.
hatch-build-scripts from DELETING the hook's prebuilt artifacts first โ clean_artifacts defaults to true, so skipping silently ships an incomplete packageSymptom: a recipe neutralizes an upstream build hook via the hook script's own documented skip switch (an env var the script checks first), the build goes green, import <pkg> and pip_check both pass โ but the built .conda is missing a file the upstream wheel ships. The log even confirms the skip worked (Skipping local <X> build.).
Why: hatch-build-scripts' OneScriptConfig declares clean_artifacts: bool = True (verified by reading plugin.py in the 1.0.0 wheel). Its initialize() runs BEFORE any command:
for script in all_scripts:
if script.clean_out_dir: shutil.rmtree(Path(self.root, script.out_dir), ignore_errors=True)
elif script.clean_artifacts:
for out_file in script.out_files(self.root):
out_file.unlink(missing_ok=True) # <-- deletes the PREBUILT artifact
for script in all_scripts:
for cmd in script.commands: run(cmd, ...) # <-- your skip makes this a no-op
So the order is delete-then-skip: the plugin unlinks every file matching the hook's artifacts globs, then runs the command, which your env var turns into a no-op that regenerates nothing. The prebuilt artifact the sdist shipped is simply gone. The skip switch was designed for a working source tree (where the next real build re-creates the file), not for an sdist consumer that wants to keep what upstream already built.
The failure is silent by construction. The deleted file is a data asset (a JS bundle, an embedded wheel, a static dir), so nothing in the Python import graph references it โ imports: and pip_check stay green. Only an assertion on package contents catches it.
Fix โ patch the hook config to keep its artifacts, rather than relying on the skip alone:
[[tool.hatch.build.hooks.build-scripts.scripts]]
commands = ['python "src/build_scripts/build_py_wheel.py"']
artifacts = ["src/reactpy/static/wheels/*.whl"]
clean_artifacts = false # <-- the load-bearing line
Ship it as a source patch (G59), not an in-build sed โ the edit needs no ${{ version }} interpolation, and a noarch recipe builds on osx/win where bare sed -i breaks. Keep the skip env var too: the two are complementary (clean_artifacts=false preserves the file; the skip prevents the expensive regeneration).
Triage rule โ read the hook's artifacts field:
artifacts is non-empty โ clean_artifacts=True matches those globs and WILL delete them. You must patch clean_artifacts = false (or let the hook run for real).artifacts = [] โ the spec matches nothing, so nothing is deleted and the prebuilt output survives untouched. Here you can simply remove the hook block and the prebuilt assets ship as-is (the wheel target's own artifacts = ["/src/<pkg>/static/"] un-excludes them from hatchling's VCS-ignore filtering).Both shapes occur in one upstream family, so check per package โ do not generalize from a sibling.
Detection โ diff your built package against upstream's published wheel. This is the general lesson: for any package whose value includes non-Python data assets, the authoritative reference is what upstream's own wheel ships. "The tests passed" proves nothing about assets.
# upstream's wheel = ground truth
unzip -l <pkg>-<ver>-py3-none-any.whl | grep -c '<pkg>/static/'
# then compare against the same paths inside the built .conda (extract pkg-*.tar.zst)
Then encode the answer as a package_contents regression guard so it can never silently regress:
tests:
- package_contents:
site_packages:
- <pkg>/static/index.js
- <pkg>/static/wheels/<pkg>-${{ version }}-py3-none-any.whl
package_contents is also env-free โ it still runs when a test-env solve is blocked (G95).
Case study: the reactpy v2 chain (2026-07-14). reactpy 2.0.0b13's sdist ships its built JS and a 1.6 MB embedded PyScript wheel at src/reactpy/static/wheels/, and its hook honors REACTPY_SKIP_PY_WHEEL_BUILD (which otherwise runs hatch run javascript:build + a recursive hatch build). Setting the env var alone produced a package with 71 of upstream's 72 static files โ the embedded wheel had been unlinked by clean_artifacts before the skipped script ran. import reactpy + pip_check were green throughout; only the package_contents guard caught it. Adding clean_artifacts = false restored exact 72/72 parity with upstream's wheel (artifact 1.1 MB โ 2.72 MiB). The two siblings in the same chain โ reactpy-router 3.0.0b1 and reactpy-django 6.0.0b1 โ both declare artifacts = [] on their bun install && bun build hooks, so removing those hook blocks was sufficient there and no clean_artifacts patch was needed: same upstream org, same plugin, opposite handling.
import failsThe established rule (auto-memory feedback_pypi_conda_mapping_unreliable) is try four
spellings โ bare, hyphenโunderscore, -py, -python โ before declaring a package
missing. That answers "does a package with this name exist?". It does not answer
"does that package contain the importable module?", and for a split distribution the
bare name is the wrong half.
Live, verified 2026-07-29. conda-forge ships two playwright packages:
| conda-forge package | What it actually contains |
|---|---|
playwright |
the Node CLI + browser driver (bin/playwright) โ no site-packages module |
playwright-python |
the import playwright bindings (provides the PyPI dist playwright) |
On PyPI the bindings are published as playwright; there is no
playwright-python distribution at all (importlib.metadata.version("playwright") โ
1.61.0; version("playwright-python") โ PackageNotFoundError).
So the four-spellings walk starts at the bare name, finds playwright, stops, and
declares the Node CLI. The environment resolves cleanly, the recipe lints, and nothing
fails until something executes import playwright. If the package's own test does not
import the module โ or the dependency is a transitive need of a module the tests never
reach โ this ships.
The check is one line, and it is about content, not existence:
# does the candidate actually carry an importable module?
conda run -p <env> python -c "import <module>" # after installing the candidate
# or, before installing, inspect the artifact's file list:
python -c "import json,urllib.request as u; \
print([f for f in json.load(u.urlopen('https://api.anaconda.org/package/conda-forge/<name>/files'))[0]])"
Both halves are usually required and they are not alternatives. The atlas kedro-viz capture task drives headless Chrome through the Python API, so it declares both:
playwright = ">=1.58.0" # CLI + browser driver
playwright-python = ">=1.61.0" # the `import playwright` bindings
The skill's own mapping data had this backwards (fixed in this release):
pypi_conda_mappings/different_names.json carried a parselmouth-sourced entry keyed
playwright-python โ conda playwright โ i.e. it treated a non-existent PyPI name as
real and pointed it at the half with no module, while the actual PyPI name playwright
was absent from the map entirely, guaranteeing the fall-through to the bare name.
Treat source: parselmouth entries as upstream-derived and therefore fallible on split
distributions; a mapping is a hypothesis until an import proves it.
Where else this shape appears: any upstream that ships a CLI/driver and bindings
separately โ Node-backed tools, -cli vs library splits, and C libraries whose bare name
is the shared object while the bindings carry a py/python suffix. When the bare-name
package's description mentions a CLI, a binary, or a driver, assume it is the wrong half
until an import says otherwise.
recipe_editor.py subprocess call must resolve the interpreter the same way as its sibling โ a bare "python" argv0 breaks on any environment without a python aliasSymptom: recipe_updater.py's (the PyPI autotick bot) real-write path โ the branch that actually applies a version bump, not the --dry-run preview โ fails with FileNotFoundError / a non-zero recipe_editor.py failed result on an environment whose interpreter is only reachable as python3, never python.
Why: the subprocess call hardcoded cmd = ["python", str(RECIPE_EDITOR_SCRIPT), ...] โ a bare PATH lookup, which depends on the host's PATH carrying that exact alias. sys.executable is the interpreter currently running the calling script: guaranteed to exist, guaranteed executable, and โ the property that matters for a script-to-script call โ guaranteed to have the same installed dependencies the caller does.
Do NOT prefer CONDA_PYTHON_EXE over it. It is a natural-looking mistake (all four CFE call sites made it until v8.82.2) because the name suggests "the active conda env's python". It is not: conda sets CONDA_PYTHON_EXE to the interpreter of the conda installation itself โ conda's own base/context module writes sys.executable into it alongside _CONDA_ROOT, the shell hook exports it once, and conda activate <env> never re-exports it. So on any multi-env conda install it names BASE python, which need not have the caller's dependencies. Live failure mode: recipe_editor.py runs under base python, exits 1 with {"success": false, "error": "ruamel.yaml is not installed."}, and the version bump fails with a misleading error on a machine where ruamel plainly IS installed.
Fix: resolve sys.executable first in every internal subprocess call to a sibling CFE script:
python = sys.executable or os.environ.get("CONDA_PYTHON_EXE") or "python"
cmd = [python, str(RECIPE_EDITOR_SCRIPT), str(recipe_path), json.dumps(actions)]
Never hardcode a bare "python" (or "python3") for an internal script-to-script call โ the calling script's own interpreter is always the correct one to reuse, and is always resolvable without a PATH lookup. CONDA_PYTHON_EXE survives only as a fallback for the rare embedded interpreter where sys.executable is empty.
Case study: discovered during the pyforge-mason effort's Story 2.10 (mason recipe update) by adversarial review of the MasonโCFE delegation boundary, 2026-08-12 โ Mason's own fixture stubs short-circuit before this real subprocess chain is reached, so the divergence carried zero test coverage on either side until the review inspected both scripts directly. Flagged for this Rule-2 retrospective (DW-2-10-2) since the fix belongs in the wrapped tool, not in the wrapper.
X.Y.Z.dev0 recipe waiting for tag X.Y.Z may be waiting for a version that will never existSymptom: a branch-archive recipe pinned at 2.0.0.dev0 ("switch to the tag archive once upstream cuts v2.0.0") sits current-looking forever while upstream's default branch has moved on โ the awaited tag never lands because upstream consolidated that version into a LATER one without ever tagging it.
Why: a dev-snapshot version is a bet on upstream's next tag, and upstream is free to change the bet's terms. Live case (bmad-manticore, found 2026-08-21): the recipe snapshot recorded 2.0.0.dev0 from a commit whose CHANGELOG said "2.0.0 - Unreleased"; three days later upstream consolidated that content into "3.0.0 - Unreleased", then "3.1.0 - Unreleased", still tagging nothing โ the latest tag stayed v1.0.1 while the version of record (.claude-plugin/marketplace.json; the repo has NO package.json) read 3.1.0.
Rule: on every bump of a dev-snapshot recipe, re-derive the version from the default branch's OWN version of record (package.json, marketplace.json, CHANGELOG heading โ whatever that repo actually maintains) โ never assume the version your last snapshot awaited. Re-versioning upward is safe: conda ordering keeps old.dev0 < new.dev0 < new-final, so 2.0.0.dev0 โ 3.1.0.dev0 โ 3.1.0 upgrades cleanly. Record the renumbering story in the recipe's rationale comment so the next bump doesn't re-derive it from scratch.
Also ask whether the recipe should stop being commit-pinned at all โ the drift signal biases you toward the wrong answer. A drift detector (or a --head autotick) reports a commit, which frames the work as "bump the pin"; but a dev-snapshot recipe usually carries a note like "flip to the tag archive the day vX lands", and by the time drift is noticed that tag may have landed several releases ago. Two cheap checks before editing:
# 1. has the awaited tag โ or anything later โ landed?
curl -s https://api.github.com/repos/<org>/<repo>/tags | python3 -c "import json,sys;print([t['name'] for t in json.load(sys.stdin)])"
# 2. is the flagged HEAD *itself* a tag commit? (annotated tags need dereferencing)
curl -s https://api.github.com/repos/<org>/<repo>/compare/v<X.Y.Z>...<head-sha> \
| python3 -c "import json,sys;d=json.load(sys.stdin);print('ahead',d['ahead_by'],'behind',d['behind_by'])"
ahead 0 / behind 0 means the HEAD you were told to bump to is the release commit โ so the correct edit is not a new commit pin but leaving commit-pinning behind: switch source.url to archive/refs/tags/v${{ version }}.tar.gz, drop context.commit, set cfe-source-kind: github-tag, and reset build.number to 0 (the version changed, so G113's increment rule does not apply). A tag archive is stable, autotick-bumpable, and drops the dual version/commit maintenance the dev snapshot required.
Case study 2 (2026-09-09): bmad-eval-quality was pinned at commit 3172162f as 0.2.0.dev0, waiting for v0.2.0. Upstream had since shipped v0.2.0, v0.3.0, v1.0.0, v1.1.0 and v1.3.0 โ the awaited tag was five releases stale and package.json at the flagged HEAD read 1.3.0. The HEAD the drift detector named (bc2c2bba1ba6) turned out to be exactly the v1.3.0 tag commit (ahead 0 / behind 0), so the right change was the tag flip above, not the commit bump the drift line implied.
bin map on every version bump, and smoke-test every bin you shipSymptom: a version bump of an npm-sourced recipe silently ships a broken or missing CLI: the old version's package.json had "bin": {} (or omitted the entry), the new version ships a real one โ or vice versa โ and a recipe that only bumped version/sha256 never noticed.
Why: upstream bin surfaces are release-mutable metadata, not stable contract. Live case (bmad-method-test-architecture-enterprise, 2026-08-21): 1.19.1 published its tea-test-review review-CLI bin as an EMPTY map โ upstream's own CI template pins TEA_VERSION: 1.20.0 because 1.20.0 is the first release where the bin works. The 1.19.1โ1.23.2 recipe bump therefore wasn't a version+hash edit: it had to vendor the CLI (npm install --omit=dev at build into a shipped node_modules, .bin symlinks stripped per G6), add a dirname-relative sh wrapper + prefix-baked .bat shim (the call-safe pattern), add nodejs to build+run (engines floor only, no ceiling โ G103), and smoke-test tea-test-review --help in the recipe tests.
Rule: at every npm-recipe version bump, diff the old and new package.json bin/dependencies/engines maps before touching the recipe; every bin the recipe ships gets a --help-class smoke test so an upstream bin regression reds the build instead of shipping.
noarch recipe whose host/run deps carry if: unix / if: win selectors NEEDS a per-recipe conda-forge.yml with noarch_platforms โ the lint "noarch packages can't have selectors" is asking for the yml, not for the selectors to goSymptom: conda-smithy recipe-lint --conda-forge reds a noarch recipe with "noarch packages can't have selectors. If the selectors are necessary, please remove noarch: generic" even though its only selectors are the __unix / __win virtual-package guards โ while a sibling recipe with the byte-identical requirements: shape (bmad-method) lints "in fine form".
Why: conda_recipe_v1_linter.lint_usage_of_selectors_for_noarch treats every if: in host/run as bad unless noarch_platforms is set in the recipe's conda-forge.yml; with it set, win/linux/osx/unix nouns are allowed. The sibling passed only because its pre-seed recipes/<name>/conda-forge.yml (kept tracked, local-only) carries noarch_platforms: [linux_64, win_64]. staged-recipes has the same dependency: without noarch_platforms the __win variant is never built at all, so the yml is functionally required, not lint appeasement. Live case: recipes/bmad-eval-quality (2026-09-05).
Rule: a noarch recipe with platform-conditional host/run deps ships recipes/<name>/conda-forge.yml with noarch_platforms: listing one subdir per selector branch (copy recipes/bmad-method/conda-forge.yml). This is the one case where noarch_platforms is not waste โ a pure-Python noarch recipe with no selectors still must NOT carry it (the default [linux_64] is right there).
dist/): build with devDependencies, then reinstall production deps CLEAN โ never npm pruneSymptom: the upstream repo gitignores dist/ and runs tsc in prepack, so the tarball has no runnable JS; a naive npm install --omit=dev ships nothing to execute, and npm ci + npm run build + npm prune --omit=dev ships a node_modules/ that still holds @babel, @biomejs, @esbuild, @vitest โฆ scope directories (prune removes packages but leaves the scope dirs and anything --ignore-scripts skipped) โ and the license audit has to explain them.
Why: prepack never runs in a conda build (--ignore-scripts), and npm prune is a subtractive operation over a dev-populated tree. A second npm ci --omit=dev from the lockfile is additive from zero and lands exactly the production closure (eval-quality: one package, zod).
Rule (recipes/bmad-eval-quality/build.sh is the reference): npm ci --no-fund --no-audit --ignore-scripts โ npm run build โ rm -rf node_modules && npm ci --omit=dev --no-fund --no-audit --ignore-scripts โ copy package.json "files" + package.json + node_modules to $PREFIX/lib/node_modules/<pkg> โ bash wrapper in bin/ and .bat in Scripts/ (G6/G100). about.license is the project license AND every production dep's (Apache-2.0 AND MIT), with each dep's license file listed under license_file: (node_modules/zod/LICENSE โ present after the clean reinstall). Strip any node_modules/.bin (symlinks). Test the CLI against fixtures the package itself ships ($PREFIX/lib/node_modules/<pkg>/corpus/โฆ), so the test needs no network and no scratch files.
build.number, never reset it โ github_updater.py --head now incrementsSymptom: github_updater.py --head advanced bmad-labs-skills from commit 088a427d to 81fe19ed (+9 upstream commits, a 22nd skill) and wrote build.number: 0 โ the same 1.0.0.dev0 / build 0 string as the artifact already on the channel. Two different artifacts would then share a version+build identity; which one a solver picks is up to the hash and the channel's repodata order.
Why: the reset-to-0 rule is a version-bump rule (new context.version โ build 0). HEAD mode never touches context.version (G109: version of record is re-derived separately), so every HEAD advance is by construction a same-version change. update_recipe_head now emits build.number = current + 1 (_get_build_number); tag mode still resets to 0.
Rule: whenever the conda version string does not change but the artifact's content does โ HEAD advance, build-script or wrapper fix, dependency pin edit, feedstock-PR rebase (see the "bump on every same-version feedstock-PR update" memory) โ bump build.number. Reset to 0 only together with a context.version change.
if: win/if: unix selectors only ever proves the NATIVE leg โ local_builder.py::build_all_platforms() short-circuits noarch to one platformSymptom: recipes/bmad-eval-quality scaffolds if: win/if: unix run selectors and a call-safe build.bat (G111/G112), and pixi run -e local-recipes recipe-build reports success โ but nothing on this host ever executed build.bat under real cmd.exe, so treating that green run as proof the Windows leg works would be unearned.
Why: local_builder.py::build_all_platforms() short-circuits every noarch recipe to a single native-platform build ("the produced package works everywhere by design") โ true for a pure-Python noarch package with no OS branching, false the moment run:/tests: carry if: win/if: unix divergent code paths (here, a real, never-executed build.bat). On a Linux host that native run can only exercise the if: unix branch; the short-circuit has no way to know the if: win branch exists, let alone that it was tested. Live case: recipes/bmad-eval-quality Story 14.1 (2026-09-06) โ the recipe was already fully Windows-ready from authoring (Story 45.1), but the __win artifact had never actually been built on Windows, only assumed correct from code review.
Rule: for a noarch recipe with if: win/if: unix selectors, "recipe-build succeeded" is a claim about the native leg only โ say so explicitly in any status note or hazard cell (e.g. install-matrix.md), and never report the other-OS leg as built/tested until it has actually run on that OS (Windows CI or a real Windows machine). Reading the other OS's build script end-to-end (call-shim convention, structural parity with its sibling script) is a substitute for review, not for execution.
__win variant of a noarch recipe โ it is built, tested green, then discarded; the channel goes quietly unix-onlySymptom: a noarch: recipe with if: win/if: unix run selectors is fully green on staged-recipes, its artifacts download cleanly, and everything you publish to your own channel installs on Linux and macOS โ but never on Windows. conda_pkgs_win is a ~75 KB shell with zero .conda files while conda_pkgs_noarch carries exactly one package, the __unix one.
Why: such a recipe yields TWO artifacts โ the win job builds the __win variant, the linux job the __unix one โ and for a noarch package BOTH land in that job's noarch/ subdir. But staged-recipes' Windows pipeline publishes D:\bld\win-64\ (Uploading pipeline artifact from D:\bld\win-64\), which for a noarch recipe is empty, while conda_pkgs_noarch is published by the linux job. So the __win build is created, tested green, and destroyed with the ephemeral runner. Nothing fails; the PR is green; the variant simply never becomes an artifact. Anything fed to a channel from staged-recipes PR artifacts is therefore __unix-only until its feedstock exists โ and the gap is invisible unless you inspect depends for __win.
You cannot fix it inside the PR. The publish step lives in staged-recipes' repo-global .azure-pipelines/azure-pipelines-win.yml; a per-recipe conda-forge.yml cannot reach it (G83 โ build_all.py reads only conda_build_tool), and a recipe PR editing those pipelines would not be accepted.
Fix โ build the __win variant on your own Windows runner. GitHub Actions windows-2022/windows-latest is free and does what staged-recipes will not: build the recipe with --target-platform win-64 and publish D:\bld\noarch\*.conda (the path staged-recipes ignores), optionally uploading straight to the channel. Gate the upload on an explicit input and fail loudly when the token is missing โ a silent skip reads as a successful publish and leaves the channel quietly incomplete. Pin the client destination (anaconda -s https://api.anaconda.org); the bare client blocks on an interactive anaconda.com-vs-.org prompt under a non-TTY runner. Post-merge this stops mattering: the FEEDSTOCK pipeline uploads each job's output directly, which is why bmad-method shows a clean 17 __win / 17 __unix on conda-forge while the same package sat 3-for-3 __unix on a channel fed from PR artifacts.
Detect it in one query โ never infer from "the PR is green":
curl -s --compressed https://conda.anaconda.org/<channel>/noarch/repodata.json \
| python3 -c "import json,sys; d=json.load(sys.stdin); pk={**d['packages'],**d['packages.conda']}; \
print([(k,[x for x in v['depends'] if x.startswith('__')]) for k,v in pk.items() if v['name']=='<pkg>'])"
Two traps met on the way there. (1) A colon (or * ? " < > |) anywhere in a TRACKED path makes actions/checkout fail on Windows with invalid path ... exit code 128 โ every Windows job dies before its first build step, which is an easy reason for such a workflow to have been disabled and forgotten. Audit with git ls-files | grep -E '[:*?"<>|]'. (2) rattler-build can fail a Windows run at test-env teardown (Retrying deletion 1/5..5/5: The process cannot access the file because it is being used by another process. (os error 32) โ Error: ร Test failed) after the tests themselves printed success โ a lingering node.exe holding handles. Re-run before treating it as a recipe defect; it passed on the immediate retry.
Case study: bmad-eval-quality 1.3.0 / staged-recipes #34774 (2026-09-09). The win job built โฆ-h62479a6_0 with depends: ['nodejs >=22.20.0', '__win'] and reported all tests passed!, yet all four published artifacts held zero packages from it. A windows-2022 dispatch produced the same variant (โฆ-h2fd06db_0, shipping Scripts/eval-quality.bat and no bin/) as a downloadable artifact; after upload a win-64 cross-solve resolved it. The same run fixed bmad-method 6.12.0, whose channel copy had been __unix-only for its whole life.
build.bat but NO __unix/__win split ships only the BUILD platform's entry point โ installable on the other OS, and broken there, with nothing to say soSymptom: a noarch: generic recipe carries both build.sh and build.bat, builds green, and installs happily on Windows โ but its CLI does not exist there. The artifact contains bin/<name> and no Scripts/<name>.bat.
Why: noarch means ONE artifact is built once and reused everywhere. Only the build host's script runs, so only that platform's entry point is created โ and with no __unix/__win in requirements.run there is no virtual package to stop the solver installing it on the other OS. This is the exact inverse of G115: there the split exists and one variant is lost in transit; here there is no split, so the package claims universality it does not have. It is the worse failure of the two โ G115 at least yields an honest "not installable on Windows", while this installs and then does nothing.
Fix โ prefer ONE artifact that carries BOTH entry points. A .bat shim is just text; build.sh can write it on Linux, so a single noarch build can serve every platform with no split, no second build, and no Windows runner:
mkdir -p "${PREFIX}/bin" "${PREFIX}/Scripts"
cp "${RECIPE_DIR}/<tool>.py" "${PREFIX}/bin/<tool>"; chmod +x "${PREFIX}/bin/<tool>"
printf '@"%%~dp0..\\python.exe" "%%~dp0<tool>-script.py" %%*\r\n' > "${PREFIX}/Scripts/<tool>.bat"
The unused wrapper is inert on the other OS. Reserve the __unix/__win split for recipes whose content genuinely differs per platform (a vendored native node_modules, a per-OS binary) โ there you must build both variants and publish both (G115).
Detect it by inspecting the published artifact, not the recipe: a package whose depends carries no __win/__unix but which ships bin/ and no Scripts/ (or vice versa) is mis-declared.
Case study: the bmad-suite audit (2026-09-09) โ 7 of 13 members (bmad-method-test-architecture-enterprise, bmad-builder, bmad-creative-intelligence-suite, bmad-utility-skills, bmad-labs-skills, bmad-module-template, bmad-manticore) each ship a build.bat with no split, and every published artifact carried bin/ + no Scripts/. All were Linux-built, so all are silently entry-point-less on Windows.
pixi upgrade --pinning-strategy latest-up silently DELETES deliberate upper bounds โ a cap that encodes a known-unsolvable combination is removed with no warning, and the lock still solves, so nothing goes redSymptom: a routine "refresh the pins" pass. pixi upgrade --pinning-strategy latest-up --exclude <a long list> exits 0, the lock re-solves cleanly, and the manifest diff looks like a page of harmless floor bumps. Buried in it, pins that read ">=X,<Y" now read ">=X". Nothing failed, because today's solve still picks a version under the old ceiling.
Why: latest-up rewrites each pin to a floor at the newly resolved version. It has no concept of an intentional ceiling, so ,<Y is not "preserved and raised" โ it is dropped. --exclude <pkg> is the only protection, and it must be spelled per package: excluding channels does nothing for channels_redis, and excluding django/wagtail does nothing for the coderedcms pinned beside them under the same "Pin to LTS" comment.
The damage is latent, which is what makes it dangerous. The cap existed to forbid a future version, so removing it breaks nothing until that version appears in the channel โ at which point the failure surfaces far from the commit that caused it, in a solve nobody connects to a months-old dependency refresh.
Fix โ diff the SPECIFIERS, never the version numbers. Snapshot the manifest first, then compare only pins whose shape changed:
cp pixi.toml /tmp/pixi.toml.pre
pixi upgrade --pinning-strategy latest-up --exclude ...
# every pin that LOST a comma-clause -- the ones that matter:
diff <(grep -oE '^[A-Za-z0-9_.-]+ *= *"[^"]*"' /tmp/pixi.toml.pre | sort -u) \
<(grep -oE '^[A-Za-z0-9_.-]+ *= *"[^"]*"' pixi.toml | sort -u) \
| grep -E '^<.*,<'
Do not read the diff with head โ the caps are scattered, not clustered, and a truncated view is how one of the six below was missed on the first pass and only caught by a second, unfiltered read.
A cap's rationale is usually written next to it โ restore from that, not from taste. Every one of these was recoverable from its own comment or its neighbours:
| pin | cap dropped | what the file itself said |
|---|---|---|
openfeature-provider-flagd |
,<0.5.1 |
"Provider must stay 0.5.0 (protobuf 6.x): 0.5.2 needs protobuf 7 and cannot solve with langflow's a2a-sdk (protobuf <7)" |
asgiref / channels_redis |
,<4.0 / ,<5.0 |
"MUST stay byte-identical to pyproject.toml's dashboard extra" โ enforced by a pin-sync test |
coderedcms |
,<7.0 |
# Pin to LTS version for Django, in a four-line block whose other members were all excluded |
cachebox ร2 |
,<6 |
major-version cap, no prose โ restore conservatively |
Two independent signals catch it if you miss the diff โ run both before believing an upgrade: a repo pin-sync test (here tests/meta/test_invariants.py went red immediately on asgiref, the one unambiguous machine-detectable case), and the library catalog drift check (llms-full-check), which lists every moved floor and so also surfaces major-version jumps worth a second look โ this pass moved plotly 6.9โ7.0 and fasta2a 0.6.1โ2.0.0, both legitimate uncapped floors, but neither is a change you want to discover in CI.
Related: G113 (build.number) is the recipe-side analogue of "a mechanical bump has a rule the tool does not know". The pixi-floor discipline in G66 also applies here โ never raise a pixi floor above the newest build actually published to the channel, or every pixi run --frozen lane reds until the artifact exists; a recipe may legitimately sit ahead of its channel (here bmad-eval-quality built 1.4.0 while the channel served 1.3.0), and the pin tracks the published build, not the recipe.
Symptom: everything solves and every test is green, yet two channels publish unrelated packages under one name. Live case: SelfExplainML's liquibase-postgresql 42.7.13 was the pgjdbc JDBC driver (postgresql.jar, BSD-2-Clause); conda-forge's liquibase-postgresql 5.0.4 is Liquibase's PostgreSQL dialect extension (no driver, FSL-1.1-ALv2). The platform pinned >=42.7.13, so it silently depended on the estate build, and any solve that can see both channels prefers 42.7.13 over 5.0.4. Its sibling liquibase 5.0.4 on SelfExplainML was labelled Apache-2.0; Liquibase 5 is FSL-1.1-ALv2, as the feedstock says.
Why: an estate recipe gets named for its consumer ("the PostgreSQL piece for Liquibase") before conda-forge ships that name. When conda-forge later publishes the name for something else, no tool compares the two: the version floor, the build and the tests only ever looked at the estate artifact.
Fix:
about.summary, license, and the installed files (info/paths.json in the .conda).pgjdbc, share/liquibase/lib/postgresql.jar, submitted as staged-recipes #34956). Make the local recipe for the shared name a verbatim mirror of the conda-forge feedstock: recipe body and every recipe/ file unchanged; only the schema header and the bottom CFE block added.anaconda remove --force <channel>/<pkg>, after proving no lock or recipe references it, and verified against the served repodata.json, not the CLI's exit code.The same-artifact face: when conda-forge publishes the SAME package the estate built, strict channel priority makes conda-forge's build win everywhere, including platforms the feedstock does not build. conda-forge/bmad-loop-feedstock (2026-09-20) was __unix-only, so every win-64 solve failed. Interim fix: channel = "SelfExplainML" on that dependency. Durable fix: make the feedstock cover the platform (bmad-loop-feedstock #4 added the __win variant, G111), then drop the pin. G78: the cfe-on-conda-forge-status field did not know the feedstock existed.
Case study: steward Stories 27.5/27.6 (2026-09-26, PRs #1611/#1612/#1613). Driver renamed to pgjdbc; recipes/liquibase and recipes/liquibase-postgresql re-mirrored from their feedstocks; platform pins moved; both colliding SelfExplainML builds removed. Proven end to end: Liquibase loads both jars and applies the platform changelog to PostgreSQL 17.
pixi lock keeps an already-locked record whose version still satisfies an edited spec โ adding channel = "..." to a pin does NOT move it off the old channelSymptom: a pin changes to liquibase-postgresql = { version = ">=5.0.4", channel = "conda-forge" }, and pixi lock reports "Updated lock file" for the new dependencies only. The lock still carries SelfExplainML/noarch/liquibase-postgresql-42.7.13-โฆconda. Here that left two packages installing the same share/liquibase/lib/postgresql.jar.
Why: pixi (0.80) re-validates a locked record against the spec's version and keeps it when it satisfies; the new channel pin was not re-checked. 42.7.13 satisfies >=5.0.4.
Fix:
grep -E '/<pkg>-[0-9]' pixi.lock | sort -u.pixi update <pkg> or make the spec exclude the locked version. In this repo the session hook denies agent pixi update, so the operator runs it with the ! prefix. >=5.0.4,<42 excluded the retired 42.7.13, and pixi lock then re-solved only that record.Corollary: a package that changes channel at the same version (pgjdbc 42.7.13, SelfExplainML โ conda-forge once #34956 publishes) needs an explicit pixi update pgjdbc. A plain re-lock keeps the old build indefinitely, and flexible channel priority does not change that.
Symptom: pgjdbc 42.7.13 built with mvn package from Maven Central's postgresql-<v>-jdbc-src.tar.gz yields a jar without org/postgresql/shaded/com/ongres/โฆ. The published jar carries those classes: com.ongres scram-client/scram-common 3.2 and saslprep/stringprep 2.2, relocated inside it. Without them, SCRAM-SHA-256 authentication (PostgreSQL's default since 14) needs the ongres jars on the classpath, which a Liquibase lib/ drop-in does not have. A java -jar โฆ --version smoke test cannot see the gap.
Why: upstream's release build shades dependencies; the pom.xml in the source tarball declares them as ordinary dependencies.
Fix:
jar tf of your build against the jar fetched from Maven Central.maven-shade-plugin with the same relocation (com.ongres โ org.postgresql.shaded.com.ongres).mvn dependency:unpack-dependencies -DincludeGroupIds=<groups> -Dmdep.useSubDirectoryPerArtifact=true -Dmdep.unpack.includes=META-INF/LICENSE -DoutputDirectory=third-party-licenses, with license_file: [LICENSE, third-party-licenses/].jar tf โฆ | rg <relocated class>), not only the version.The result matched upstream's 55 shaded classes. The only remaining differences were upstream's optional OSGi and SSPI classes.
Java recipe shape (the first Maven recipe here, recipes/pgjdbc):
noarch: generic; build deps maven + openjdk 17.*; run openjdk >=8 (the driver's own floor).mvn -Dmaven.repo.local=$SRC_DIR/.m2 package -Dmaven.test.skip=true -Dmaven.javadoc.skip=true.build.bat too, because staged-recipes builds noarch on Windows (G102).liquibase --version lists postgresql.jar).A quarterly live-doc audit keeps this skill aligned with upstream conda-forge changes. It runs as a remote Claude Code routine (registered at claude.ai/code/routines) but the prompt and runner are committed under automation/ so the job is reproducible from this repo.
| Routine ID | trig_015z9XF8ExDJuN9qsZYGYKcu |
| Cron | 0 14 1 */3 * โ Jan/Apr/Jul/Oct 1, 14:00 UTC (= 9am Chicago in CDT) |
| Repo | rxm7706/local-recipes |
| Prompt | automation/quarterly-audit.prompt.md |
| Local runner | automation/run-audit-local.sh |
| Manage | https://claude.ai/code/routines/trig_015z9XF8ExDJuN9qsZYGYKcu |
To run an off-cycle audit locally: .claude/skills/conda-forge-expert/automation/run-audit-local.sh. See automation/README.md for cron / systemd scheduling and instructions to recreate the remote routine if it's ever deleted.
| Command | Description |
|---|---|
pixi run -e local-recipes health-check |
Full diagnostic on the environment |
pixi run -e local-recipes sync-upstream-conda-forge |
Sync fork with conda-forge/staged-recipes |
pixi run -e local-recipes submit-pr <name> |
Submit a finished recipe to conda-forge |
pixi run -e local-recipes autotick <path> |
Manually run the autotick bot on a recipe |
pixi run -e local-recipes update-cve-db |
Refresh the local CVE database |
pixi run -e local-recipes update-mapping-cache |
Refresh the PyPI name mapping cache |
python build-locally.py linux-64 |
Build for a specific platform (Docker on Linux) |
python build-locally.py --filter 'linux*' |
Build all matching platform configs |
pixi run -e conda-smithy lint recipes/<name> |
Lint a recipe with conda-smithy |
feedrattler <name>-feedstock <github-user> |
Migrate a REMOTE feedstock v0โv1 (clones conda-forge/<name>-feedstock + opens a fork PR โ NOT a local-dir converter, G84) |
v8.91.1 (Sep 29, 2026) โ Host-gate tests made hermetic: a shared clean_mirror_env fixture clears ambient *_BASE_URL and npm mirror vars (PATCH; tests only, no script behaviour change). A local pixi run -e pyforge-guild pr-preflight failed 1 of 9152 tests: test_inventory_channel_auth_host_gate.py::โฆ::test_malformed_base_url_does_not_crash_the_allowlist_scan asserted _fallback_configured_enterprise_hosts() == {"good.example.com"}, but the scan reads every *_BASE_URL, and a Claude Code shell exports ANTHROPIC_BASE_URL, so the set also held api.anthropic.com. CI has no such var, so only local preflight runs from agent sessions hit it, and the pre-push hook then blocked git push. New clean_mirror_env fixture in tests/conftest.py removes every *_BASE_URL plus _http._EXTRA_MIRROR_ENV_VARS (read from _http, not restated). It is opt-in, not suite-wide autouse, so network-marked tests keep an operator's real mirror routing. Opted in: test_inventory_channel_auth_host_gate.py and test_http_skip_auth.py (module-level; neither cleared anything), test_http_jfrog_host_gate.py and test_dependency_checker_auth_host_gate.py (their autouse fixtures now build on it; the latter had missed the npm vars), and the gate classes of test_http_resolvers.py and test_s3_resolver.py, whose hand-kept _clean_env lists each cover only part of the scan (the resolvers list omits S3_PARQUET_BASE_URL; the S3 list names only that one). New test_clean_mirror_env.py plants ambient vars before the fixture runs and asserts they are gone and the fallback allowlist is empty; with the fixture disabled it fails along with the original test (A/B verified), so CI now exercises the regression. inventory_channel.py and _http.py are unchanged. SKILL.md's host-gate constraint gains the testing rule. Files: tests/conftest.py, tests/unit/test_clean_mirror_env.py (new), the six test modules above, SKILL.md (host-gate testing paragraph, version, history), config/skill-config.yaml (8.91.0 โ 8.91.1), MANIFEST.yaml, CHANGELOG.md.
v8.91.0 (Sep 26, 2026) โ pgjdbc + Liquibase convergence Rule-2 retro: three new gotchas, G118 (same name, different artifact across channels), G119 (pixi lock keeps a locked record past a new channel = pin) and G120 (a Maven source build is not the shaded jar upstream publishes) (MINOR). From steward Stories 27.5/27.6 and the recipe work beside them (PRs #1609, #1611, #1612, #1613; staged-recipes #34956; bmad-loop-feedstock #4). G118. SelfExplainML's liquibase-postgresql 42.7.13 was the pgjdbc JDBC driver, while conda-forge's liquibase-postgresql 5.0.4 is Liquibase's dialect extension. The platform depended on the estate build, and any solve seeing both channels prefers 42.7.13. The estate's liquibase 5.0.4 also carried Apache-2.0 against upstream's FSL-1.1-ALv2. Fix: compare artifacts (summary, license, info/paths.json), not versions. Give the estate package its own name at the same install path (recipes/pgjdbc, submitted as #34956). Mirror the feedstock verbatim for the shared name (recipes/liquibase, recipes/liquibase-postgresql). Move consumers, then remove the colliding builds from the channel, verified against the served repodata.json. The same-artifact face is recorded too: conda-forge/bmad-loop-feedstock shadowed the estate build and was __unix-only, so win-64 solves failed. Interim: a per-dependency channel = "SelfExplainML" pin. Durable: feedstock #4 added the __win variant. G119. pixi lock re-validated the locked SelfExplainML record against the new spec's version only. It kept the record past a new channel = "conda-forge" pin, leaving two packages installing the same postgresql.jar. Fix: read the lock after every channel change, and force the move with pixi update <pkg> (operator-run here) or a bound that excludes the locked version (<42). Corollary: a same-version channel move (pgjdbc once #34956 publishes) needs an explicit pixi update. G120. The first Maven recipe here. mvn package from Maven Central's -jdbc-src.tar.gz omits the com.ongres SCRAM classes upstream shades into its published jar, and a --version test cannot see the gap. Fix: diff jar tf against the Maven Central jar; reproduce the relocation with maven-shade-plugin in a patch; ship the shaded dependencies' licenses via dependency:unpack-dependencies; test for a relocated class. The result matched upstream's 55 shaded classes. Also documents the Java recipe shape: noarch: generic, maven + openjdk 17.* to build, openjdk >=8 to run, a build.bat per G102, and testing through the consumer. Held: G78 (the cfe on-conda-forge fields lagged the bmad-loop feedstock), G102 (pgjdbc ships a build.bat; staged-recipes built win-64 green), G111 (noarch_platforms for the bmad-loop win variant), G62 (0 CFE lines on the pushed staged-recipes branch), G63/G64 (PR template ticked; help-java ping held until CI green and out of draft). Correction: G117's related-links line pointed at "G118-adjacent practice" (a number that did not exist); it now reads G66. Files: SKILL.md (G118โG120, the G117 link, version, history), config/skill-config.yaml (8.90.6 โ 8.91.0), MANIFEST.yaml, CHANGELOG.md, config/failure-catalog.yaml (regenerated).
v8.90.6 (Sep 25, 2026) โ bmad-suite advance 2026-09-25 Rule-2 retro: G110 held twice, and the GitHub autotick detector learns the v1 about: field names (PATCH; one tool fix, no new gotcha). The operator chose the hand path through CFE for the three bumps the fleet picture asked for (the steward suite advance chain cannot yet complete live โ recorded as DW-steward-suite-advance-gaps-2026-09-25): github_updater.py tag-mode autotick, then a local rattler-build of each in a worktree, one reviewable PR, channel publish left to the operator. bmad-loop 0.11.1 โ 0.12.0 โ pyproject diff is version-only (plus a pytest addopts), no recipe change beyond version + sha256; built green with imports + pip check + the three CLI smoke tests. bmad-method-test-architecture-enterprise 1.26.0 โ 1.27.2 โ G110's bin re-read caught bin growing 3 โ 10 (tea-atdd-red-check, tea-atdd-runner, tea-ci-runner, tea-nfr-runner, tea-routing-runner, tea-test-design-runner, tea-transcript-runner); all ten targets verified in the tag archive and probed with --help (exit 0) on the extracted archive BEFORE wiring, then each got a wrapper + .bat shim + smoke test; engines floor and the 12 prod deps unchanged; upstream's new files array is already what the build mirrors. bmad-eval-quality 3.0.0 โ 4.2.0 โ bin grew 1 โ 2 (eval-quality-gates โ dist/gates/gates-cli.js, from a second tsc -p tsconfig-gates.json pass npm run build now runs); wired the same way; the new OPTIONAL peer dep typescript >=5.7.0 is not vendored (npm ci --omit=dev never installs optional peers) โ noted in the recipe. Built artifacts inspected for bin/ + Scripts/ parity (G116) rather than trusting the green test phase. G113 held (version moved โ build.number 0), G109 held (versions re-derived from the tags, sha256s cross-checked against independently fetched archives), G103 held (floor only). The one tool fix: github_version_checker.extract_github_repo โ which github_updater.py uses for auto-detection โ read context.*, source.url and only the v0 about.home, so a v1 recipe whose GitHub URLs live under about.repository / about.homepage with a non-GitHub source (the npm-tarball recipes/bmad-module-skill-forge) was refused with "No GitHub URL detected" โ a G2-class v0-name trap inside a tool. It now also reads repository, dev_url, homepage (source URL still wins); tests/unit/test_github_version_checker.py gains four detection cases. The steward-side half of that gap (routing npm-registry members to npm_updater.py by cfe-upstream-registry, plus the publish and open-PR hooks) stays a steward deferred-work row. Files: scripts/github_version_checker.py, tests/unit/test_github_version_checker.py, SKILL.md (version, history), config/skill-config.yaml (8.90.5 โ 8.90.6), MANIFEST.yaml, CHANGELOG.md; the three recipes + the two ledger rows in the same PR. Second round, 2026-09-26 (same PR, after the operator's publish go): the three artifacts went to SelfExplainML and the metapackage regen (generate-bmad-suite) then pinned bmad-eval-quality >=4.3.0 and bmad-module-skill-forge >=2.2.0 โ the generator resolves upstream-latest floors by design, so a suite regen is only coherent when every member recipe is current; two were not. eval-quality 4.3.0 had shipped at 03:44Z, three hours after the 4.2.0 build was published โ a live G109 recurrence, caught only because the regen read upstream. Both members bumped (skill-forge via npm_updater.py, the registry its cfe-upstream-registry: npm names; eval-quality via the GitHub updater), G110 re-read on both (metadata byte-identical, version + sha256 only), built green, sha256s cross-checked. Floor rule held twice: the pixi.toml TEA floors moved to the published 1.27.2 (lock re-solved, only TEA changed); bmad-loop stayed at 0.11.1 because pyforge-marshal caps <0.12 (DW-marshal-bmad-loop-0-12-cap-2026-09-26), and bmad-eval-quality stayed at 1.4.1 per its own G115 note (__win still unbuilt). Third round, later the same day: both holds were lifted. eval-quality's __win variant came from the windows-2022 dispatch of .github/workflows/test-windows.yml (the G115 fix, run 36222516048, uploading straight to the channel), so its three pixi.toml floors moved to 4.3.0. For bmad-loop the re-lock failed on win-64 with "bmad-loop 0.12.0 would require __unix" โ conda-forge/bmad-loop-feedstock exists since 2026-09-20 (not ours; it gates __unix and adds tmux/rich/httpx), and under strict channel priority the conda-forge name now shadows the estate's ungated SelfExplainML build. recipes/bmad-loop's cfe-on-conda-forge-status: pending-submission-to-conda-forge had gone stale (G78 held: the cfe fields are hints, verify live) and now reads confirmed-on-conda-forge with the feedstock URL; pixi.toml pins the dependency to the SelfExplainML channel (bmad-loop = { version = ">=0.12.0", channel = "SelfExplainML" }) in the marshal and local-recipes features. Marshal's bmad-loop <0.12 cap was verified against 0.12.0 (the four lazily-imported modules ship; suite green) and widened to <0.13 in pyproject, the package manifest and the FR-52 range constants.
v8.90.5 (Sep 12, 2026) โ CFE rebuild campaign closed: both Skill-Forge mirror packages retired, not cut over (PATCH; no new gotcha). Mason Story 15.1 closes spec-conda-forge-expert-rebuild's 2026-09-09 endgame declaration (slices 1-2 only) โ Mason's sole caller never touched either compiled mirror, so cutting over would have meant rearchitecting a filesystem-marker contract for zero behavioral gain over byte-identical mirrors. .claude/skills/cfe-recipe-generation/ and cfe-recipe-lifecycle/ deleted outright (OQ4, no stub) with their equivalence-harness tests; campaign-state.yaml records both slices retired; SPEC.md flips to shipped; cfe_rebuild_guard_check.py runs clean. Recovered from a 2026-09-10 patch that never landed (blocked by an unrelated, now-fixed, repo-wide spec-surface gate โ see the 2026-09-12 mason-15.1-unblock maintenance PR). Renumbered from the story's own v8.90.2 at rescue time โ Story 15.2's v8.90.2, Story 16.1's v8.90.3, and Story 16.3's v8.90.4 had all since landed from the same v8.90.1 base. Files: .claude/skills/cfe-recipe-generation/ (deleted), cfe-recipe-lifecycle/ (deleted), 2 equivalence tests (deleted), .github/workflows/cfe-regression-net.yml, pixi.toml, SKILL.md, MANIFEST.yaml, config/skill-config.yaml, CHANGELOG.md; campaign-state.yaml/SPEC.md/.memlog.md under spec-conda-forge-expert-rebuild/ in the same commit.
v8.90.4 (Sep 11, 2026) โ Story 16.3 FR-155 parity gate for the CFE MCP surface (PATCH). Mason's 46 @mcp.tool registrations had no CLIโtool parity gate. New scripts/mcp_tools.py (TOOL_SPECS + AST drift check) and scripts/mcp_parity.py (extends marshal's pyforge.marshal.mcp.parity; pixi-task CLI discovery; 37 CLI-only + 6 tool-only allowlist entries with reasons). New tests/meta/test_cli_tool_parity.py (live pass + bidirectional fixture failures). Renumbered from the story's own v8.90.2 at rescue time โ Story 15.2's v8.90.2 and Story 16.1's v8.90.3 had already landed from the same v8.90.1 base. Files: scripts/mcp_tools.py, scripts/mcp_parity.py, tests/meta/test_cli_tool_parity.py, conda_forge_server.py, SKILL.md, config/skill-config.yaml, MANIFEST.yaml, CHANGELOG.md.
v8.90.3 (Sep 11, 2026) โ pyforge-mason Story 16.1 Rule-2 retro: no skill changes; verified existing guidance held (PATCH). CFE-floor dependency work for mason's own pixi env โ adding truststore and conda-forge-metadata to [feature.pyforge-mason.dependencies] so cfe.py's CFE_IMPORT_FLOOR is satisfied before the recipe verb family reports available. No recipe authoring, build failure, or new conda-forge packaging pattern surfaced; existing CFE_IMPORT_FLOOR / import-probe / CAP-5 graceful-degradation guidance in mason's own tests held without correction. Renumbered from the story's own v8.90.2 at rescue time โ Story 15.2's retro landed on main first and independently claimed v8.90.2 from the same v8.90.1 base. Files: SKILL.md (version, history), config/skill-config.yaml, MANIFEST.yaml, CHANGELOG.md.
v8.90.2 (Sep 11, 2026) โ pyforge-mason Story 15.2 Rule-2 retro: G26 gains the dbgpt-client upper-bound cap case study (PATCH; no new gotcha number). Story 13.2 shipped with its CFE retro deferred because mason's meta-test forbids in-story CFE edits; this maintenance PR lands the retro outside that constraint. The G26 extension after the Story 13.1 marker-split entry now carries a distinct case study for dbgpt-client's SQLAlchemy>=2.0.25,<2.0.29 cap (no cp314 build) โ run-dep loosen alone insufficient; source patch 0003-loosen-dbgpt-client-sqlalchemy-cap.patch required. Closes DW-13-2-1. Docs-only โ no script mirror re-port. Files: SKILL.md (G26 extension + version + history), config/skill-config.yaml (8.90.1 โ 8.90.2), MANIFEST.yaml, CHANGELOG.md; deferred-work-ledger.md closure in the same changeset.
v8.90.1 (Sep 9, 2026) โ G47 gains the zip_keys-hijack failure mode, and a merge guard makes it permanent (PATCH). From clearing the Linux lane's fifth and last blocker. G47 previously described only local symptoms of a stale recipe-dir CBC (conda-smithy baseline lints, a duplicate entry variant collision). In the staged-recipes Docker lane the same file fails earlier and much louder: combine_specs resolves zip_keys from the LAST spec that declares it and then re-validates every spec against that group, so a pre-2026 pinning snapshot's ['python','numpy','python_impl','is_python_min'] group is imposed on the container's CURRENT pinning (numpy: [2], four python entries) and the merge raises ValueError: โฆ numpy and python are different (1 and 4) โ naming /opt/conda/conda_build_config.yaml, i.e. the innocent file, before any recipe renders. Two consequences the old text did not cover: a CBC at the recipes/ ROOT breaks every recipe (build_all.py loads it ahead of the per-recipe glob, and build_steps.sh's TEST_RECIPE prune deletes only sibling directories, so a file there survives), and channel_sources is read from this file, not from conda-forge.yml โ the one key genuinely worth a recipe-local CBC when a recipe's deps live off conda-forge. Live: recipes/conda_build_config.yaml was a May-2025 pinning snapshot inherited at repo bootstrap; it plus 14 per-recipe copies of the same vintage were deleted (30 legitimate 1โ5-key CBCs kept). New tests/meta/test_recipe_cbc_merges_with_pinning.py replays the real merge over every recipes/*/conda_build_config.yaml and asserts no root-level one exists โ A/B-verified (2 failed with the stale files restored, 33 pass without), so a reintroduced snapshot fails in the suite instead of on a dispatched Linux run. Files: SKILL.md (G47 third failure mode + frontmatter version + history), tests/meta/test_recipe_cbc_merges_with_pinning.py (new), config/skill-config.yaml (8.90.0 โ 8.90.1), MANIFEST.yaml, CHANGELOG.md; the 15 deletions + recipes/bmad-suite/conda_build_config.yaml land in a companion commit.
v8.90.0 (Sep 9, 2026) โ G117: pixi upgrade --pinning-strategy latest-up silently deletes deliberate upper bounds (MINOR). A routine pin refresh exited 0 and re-solved cleanly while dropping ,<Y from six pins, because latest-up rewrites every pin to a bare floor and has no concept of an intentional ceiling; --exclude is the only guard and must be spelled per package (excluding channels does nothing for channels_redis; excluding django/wagtail does nothing for the coderedcms pinned beside them under the same "Pin to LTS" comment). The damage is latent โ a cap forbids a future version, so nothing reds until that version ships, far from the commit that caused it. One case was machine-caught immediately (asgiref, by a pin-sync test asserting byte-identity with a pyproject extra); the rest were recoverable only from the rationale written beside them, incl. openfeature-provider-flagd ,<0.5.1 whose own comment says 0.5.2 needs protobuf 7 and cannot solve with langflow's a2a-sdk. G117 prescribes diffing the specifiers (not the version numbers) and warns against reading that diff through head โ one of the six was missed on a truncated first pass. Also records the pixi-floor rule this pass exercised: never raise a floor above the newest build actually published, since a recipe may legitimately sit ahead of its channel (bmad-eval-quality built 1.4.0 while the channel served 1.3.0). Housekeeping: SKILL.md's frontmatter version: had drifted to 8.87.1, missing the 8.88.0/8.88.1/8.89.0 bumps that MANIFEST.yaml and skill-config.yaml did receive โ all three now agree. Files: SKILL.md (G117, frontmatter version, history), config/skill-config.yaml (8.89.0 โ 8.90.0), MANIFEST.yaml, CHANGELOG.md, config/failure-catalog.yaml (regenerated); recipes/bmad-eval-quality/recipe.yaml, recipes/bmad-suite/*, pixi.toml/pixi.lock and the spec memlogs in companion commits.
v8.89.0 (Sep 9, 2026) โ G115 + G116: Windows variants of noarch recipes (MINOR). G115 โ staged-recipes builds the __win variant of a selector-carrying noarch recipe, tests it green, then discards it: the win job publishes D:\bld\win-64\ (empty for noarch) while conda_pkgs_noarch comes from the linux job, so any channel fed from PR artifacts is silently __unix-only. Unfixable in-PR; fix is a windows-2022 dispatch publishing D:\bld\noarch\*.conda. Verified on bmad-eval-quality 1.3.0 + bmad-method 6.12.0. Includes two traps: a colon in a tracked path kills Windows actions/checkout entirely, and an os error 32 teardown failure can follow passing tests. **G116** โ the inverse: a noarch recipe with a build.bat but no __unix/__win split ships only the build platform's entry point and still installs on the other OS (7 of 13 bmad-suite members); prefer one artifact carrying both wrappers.
v8.88.1 (Sep 9, 2026) โ pr-artifacts returned ZERO packages for every noarch: recipe (PATCH). _DEFAULT_CONDA_PKGS_RE matched conda_pkgs_(linux|osx|win) only, excluding conda_pkgs_noarch โ the sole artifact carrying a noarch recipe's package (the per-arch ZIPs hold an empty ~75 KB repodata shell). Silent: the tool reported success and built a valid-but-empty file:// channel. Affected most of conda-forge since v8.14.0. Fixed the regex + subdir map; _write_noarch_stub is now correctly the no-noarch-artifact fallback. Verified live on staged-recipes #34774 / #33125, whose packages then uploaded and indexed on SelfExplainML. +5 tests; byte-re-ported into slice-2.
v8.88.0 (Sep 9, 2026) โ bmad-suite member descriptions become generated, not hand-added (MINOR). recipes/bmad-suite/recipe.yaml's run: block is regenerated wholesale between its GENERATED markers, so a hand-added trailing # description # urls comment is wiped on the next generate-bmad-suite. suite-members.yaml now carries per-member description: + urls:, and bmad_suite_metapackage.py emits them as one column-aligned trailing comment (the selector-nested member shares the same absolute column). Descriptions come from each member recipe's own about.*. Regeneration is idempotent, the pins still parse as bare name >=version, and _parse_existing_pins is unaffected; +5 unit tests. Authoring trap: a value STARTING with " breaks YAML parsing of the manifest โ single-quote it (same class as G98's #/: rule).
v8.87.2 (Sep 9, 2026) โ bmad-eval-quality tag-flip retro: G109 gains the "should this still be commit-pinned?" check (PATCH; refines existing guidance, no new gotcha). From bumping recipes/bmad-eval-quality off its commit pin. The recipe sat at 3172162f as 0.2.0.dev0 awaiting a v0.2.0 tag; upstream had since shipped v0.2.0, v0.3.0, v1.0.0, v1.1.0 and v1.3.0, so G109's core rule (re-derive the version of record, never assume the awaited one) fired exactly as written โ a second live case study after bmad-manticore. The new finding is a bias in the signal: a drift detector (and github_updater --head) reports a commit, which frames the work as "bump the pin", when a dev-snapshot recipe's own note usually says "flip to the tag archive when vX lands". Two cheap API checks now sit in G109 โ list tags, then compare/v<X>...<head>; ahead 0 / behind 0 means the flagged HEAD IS the release commit, so the correct edit is to leave commit-pinning entirely (tag archive, drop context.commit, cfe-source-kind: github-tag, build.number reset to 0 since the version moved and G113's increment rule does not apply). Verified live: bc2c2bba1ba6 was exactly the v1.3.0 tag commit. G110 (re-read bin/metadata on every bump โ unchanged here), G112 (build shape needed no edit), G113 (reset-not-increment on a version change), G61 (sha256 computed live), G65 (CI-parity lint) and G85 (a .conda is written BEFORE tests, so re-run the test phase against the artifact โ done, test 1.3.0 = 1.3.0) all held without correction. G114 still bounds the Windows claim: the __win artifact remains unbuilt locally. Files: SKILL.md (G109 refinement + case study 2, version, history), config/skill-config.yaml (8.87.1 โ 8.87.2), MANIFEST.yaml, CHANGELOG.md; recipes/bmad-eval-quality/recipe.yaml + the steward spec memlog in the companion commit.
v8.87.1 (Sep 7, 2026) โ CI/hygiene sweep retro: G101's segfault diagnosis corrected; validate_recipe.py gains the go-nocgo stdlib exemption (PATCH). G101 blamed ccusage's vendor Bun binary for an unfixable segfault from 2026-07-12 to 2026-09-06; a byte-diff on ccusage 20.0.20 found rattler-build's default binary relocation had corrupted the packaged binary's ELF header (RPATH rewrite shifting Bun's embedded offsets) โ binary_relocation: false fixes it, and the pin was lifted. G101 rewritten to lead with "diff before you blame the vendor"; the still-true "not conda-forge-submittable" fact is kept. Separately, validate_recipe.py's compiler-without-stdlib check (both the v1 structured path and the v0 text-scan fallback) had no compiler("go-nocgo")/compiler("go") exemption that recipe_optimizer.py's STD-001 already carried โ fixed to mirror it, with a new v1-go-nocgo fixture + test. Files: SKILL.md (G101 rewrite, version, history), scripts/validate_recipe.py, tests/unit/test_validate_recipe.py, tests/fixtures/recipes/v1-go-nocgo/recipe.yaml (new), config/failure-catalog.yaml (regenerated), config/skill-config.yaml (8.87.0 โ 8.87.1), MANIFEST.yaml, CHANGELOG.md.
v8.87.0 (Sep 6, 2026) โ Mason Story 14.1 retro: bmad-eval-quality re-verified Windows-ready; new gotcha G114 documents the noarch native-leg-only build-verification limit (MINOR). The recipe's if: win/if: unix selectors, call-safe build.bat, and noarch_platforms: [linux_64, win_64] were already correct from authoring (Story 45.1) โ this pass re-confirmed them by re-reading build.bat/build.sh end-to-end, bumped build.number 0 โ 1 (same-version content re-verification, G113), and re-ran recipe-build green on linux-64. No win-64 artifact was built or uploaded โ this repo's local_builder.py::build_all_platforms() short-circuits any noarch recipe to the native platform only, so no local tool here can execute build.bat under real cmd.exe; that gap is now G114 (new). install-matrix.md's bmad-eval-quality row gained a Windows hazard note (win-ready, artifact not yet built/uploaded โ the anaconda upload stays the operator's step per Epic 14). No correction to existing guidance; existing G111/G112/G113 held throughout. Follow-through: config/failure-catalog.yaml regenerated (generate-failure-catalog) so the derived catalog carries G114 (114 rows); tests/meta/test_failure_catalog_freshness.py guards this. Files: SKILL.md (G114, version, history), config/skill-config.yaml (8.86.4 โ 8.87.0), MANIFEST.yaml, CHANGELOG.md; recipes/bmad-eval-quality/recipe.yaml and _bmad-output/projects/pyforge-steward/planning-artifacts/specs/spec-bmad-suite-channel-product/install-matrix.md in the companion commit.
v8.86.4 (Sep 6, 2026) โ Fleet-hygiene retro: bmad-loop's repo skills proven to match the installed package, by test (PATCH; no CFE recipe-authoring behavior change). tests/meta/test_bmad_loop_skills_match_installed.py (new) โ another repo-wide regression guard living under this skill's tree for BMAD-infra hygiene, not conda packaging โ locates the installed bmad_loop conda package via importlib.util.find_spec and recursively diffs the repo's three hand-vendored bmad-loop-* skill dirs (-setup/-sweep/-resolve) against the package's own data/skills/ canon, skipping cleanly when bmad_loop isn't importable and proving its own diff logic via a tmp_path fixture (planted one-line divergence, file-set mismatch, and the spec.origin fallback path). Both module.yaml's module_version and the three skill dirs were already correct in the live tree before this story ran (an unrelated prior commit had already closed that gap) โ the guard's job is only to make that equivalence provable going forward. Landed via pyforge-marshal Story 30.4 (spec-bmad-611-era-alignment CAP-11) โ the same "fleet hygiene branch" sanctioned-exception shape as v8.86.3. No recipe, gotcha, or Operating-Principle change. Files: tests/meta/test_bmad_loop_skills_match_installed.py (new), SKILL.md (version, history), config/skill-config.yaml (8.86.3 โ 8.86.4), MANIFEST.yaml, CHANGELOG.md.
v8.86.3 (Sep 6, 2026) โ Fleet-hygiene retro: the retired-BMAD-skill-ID guard keeps pace with 6.12.0 (PATCH; no CFE recipe-authoring behavior change). tests/meta/test_no_retired_bmad_skill_ids.py โ a repo-wide regression guard that happens to live under this skill's tree but governs BMAD planning-doc hygiene, not conda packaging โ gains bmad-checkpoint-preview as its 21st guarded id (BMAD-METHOD 6.12.0 renamed it to bmad-walkthrough), a dated clarifying comment on the pre-existing bmad-generate-project-context entry, and a one-directional catalog-consistency check (test_catalog_renames_are_guarded) asserting every bmad_core_releases/*.yaml skill_renames[].from id stays guarded. Landed via pyforge-marshal Story 30.1 (spec-bmad-611-era-alignment CAP-8); this is the "fleet hygiene branch may carry the one sanctioned retro" exception the marshal/mason/steward/atlas station guards each name โ no recipe, gotcha, or Operating-Principle change. Files: tests/meta/test_no_retired_bmad_skill_ids.py, SKILL.md (version, history), config/skill-config.yaml (8.86.2 โ 8.86.3), MANIFEST.yaml, CHANGELOG.md; the marshal-owned planning-doc glosses/renames + a pyforge-mason spec-surface reconcile land in a separate, non-CFE-surface commit on the same branch.
v8.86.2 (Sep 5, 2026) โ Strict duplicate-key audit + the corpus it found (PATCH). tests/meta/test_recipe_yaml_parse_audit.py gains test_no_duplicate_mapping_keys_anywhere: a PyYAML loader that raises on a repeated mapping key, because safe_load keeps the last value silently while ruamel / conda-smithy / rattler-build reject the file. First run found 31 half-migrated v0โv1 recipes (a v1 skip: beside its v0 skip: true # [sel] twin; multi-output items whose package: lost its - marker and merged into the previous output; duplicated about-fields) โ all resolved in the companion recipes commit, conditions cross-checked against each feedstock's live meta.yaml. The repo-wide lint pixi task now runs the CI-parity CalVer conda-smithy via pixi exec over real recipe directories only (G65; the pinned 3.62.0 cannot lint a v1 cross-compile if/then build block). No script change; both compiled slices untouched. Files: tests/meta/test_recipe_yaml_parse_audit.py, SKILL.md, config/skill-config.yaml (8.86.1 โ 8.86.2), MANIFEST.yaml, CHANGELOG.md; 31 recipes/** + pixi.toml in companion commits.
v8.86.1 (Sep 5, 2026) โ recipe-maintainers sweep (22 recipes / 23 files) + meta-test; G26 marker-split extension re-landed (PATCH). An EMPTY extra.recipe-maintainers: key crashes conda-smithy's lint_recipe_maintainers (TypeError: 'NoneType' object is not iterable) and, because the repo-wide lint task walks recipes/* in one process, truncated it at Flake8-pyproject โ 16 recipes / 17 files carried the empty key (the 2026-08-20 inventory commit d204da00fd), 6 more had no key at all (MAINT-001). Filled per G53 (the deployed feedstock's list โช rxm7706; rxm7706 alone where no feedstock exists); new tests/meta/test_recipe_maintainers_nonempty.py guards both shapes line-wise (meta.yaml carries jinja) and accepts a block sequence at the key's own indent. G26 gains the marker-split extension from pyforge-mason Story 13.1's orphaned follow-up-review commit 74bc80fe61 (made 3 min after PR #1013 merged, never reached main; the recipe fix did via recipes/langflow/patches/0005-*). No script change; both compiled slices untouched. Files: SKILL.md, tests/meta/test_recipe_maintainers_nonempty.py (new), config/skill-config.yaml (8.86.0 โ 8.86.1), MANIFEST.yaml, CHANGELOG.md.
v8.86.0 (Sep 5, 2026) โ bmad-suite 2026.9.5 refresh Rule-2 retro (MINOR): G111 noarch_platforms is required for selector-carrying noarch recipes, G112 npm-from-commit-archive build + clean prod reinstall, G113 same-version changes bump build.number (and github_updater.py --head now increments instead of resetting); cfe-upstream-registry must match the publishing registry; the repo-wide lint task aborts at the first empty recipe-maintainers: (16 recipes) โ lint touched recipes directly. Effort: bmad-method 6.11.0 โ 6.12.0 mirror, bmad-labs-skills HEAD advance (build 1, 22 skills), new recipes/bmad-eval-quality (14th suite recipe, commit-pinned 0.2.0.dev0), WDS retired from the metapackage; all 13 members + bmad-suite-2026.9.5 built and tested green locally.
v8.85.2 (Sep 4, 2026) โ Relocation-aware repo root; both compiled slices back in equivalence (PATCH). _paths.get_repo_root() is a marker walk (nearest ancestor with pixi.toml + .claude/), not parents[4], so its copy inside a compiled slice resolves the same root; gen_yml_reference.py, _path_guard.py (the Story 12.7 reproduced divergence), mapping_manager.py, vulnerability_scanner.py resolve through _paths; slice-1 github_updater.py re-ported byte-for-byte (v8.84.0 --head), slice-2 gen_yml_reference.py + _paths.py mirrored; both equivalence harnesses green. Surfaced by the first cfe-regression-net CI run since Actions returned. Files: scripts/_paths.py, scripts/gen_yml_reference.py, SKILL.md, config/skill-config.yaml (8.85.1 โ 8.85.2), MANIFEST.yaml, CHANGELOG.md.
v8.85.1 (Sep 3, 2026) โ G82 + CI-provider table correction (PATCH). Found while answering why langflow-feedstock #20 has no macOS-arm leg. G82 claimed Azure's macOS-ARM image was paused and that a provider: {osx_arm64: azure} rerender applied a cross-compile build_platform: osx_arm64โosx_64 automatically. Current conda-smithy (conda_smithy.configure_feedstock) renders VMIMAGE: macOS-15-arm64 whenever an osx_arm64 leg's build platform is left at the identity default, so the leg runs natively and its tests run; the cross route is an explicit build_platform: {osx_arm64: osx_64} โ which lyric-py-feedstock's own conda-forge.yml carries (the June note misattributed it to smithy). Live: pythran-feedstock (noarch; provider: {osx_arm64: default} + noarch_platforms: [linux_64, osx_arm64, win_64]) renders an osx_arm64_.yaml leg on macOS-15-arm64; python/vtk/root feedstocks sit on the same image. The same source's DEFAULT_PROVIDERS is github_actions for linux_64, linux_aarch64 (native), linux_ppc64le/s390x (emulated), win_64, win_arm64 and azure for osx_64/osx_arm64 โ ยง Platform Assignments had Azure-by-default with a Linux-GA opt-in and "GitHub Actions not yet available" for Windows/macOS. Rows corrected; reference/conda-forge-yml-reference.md ยง noarch_platforms gains the native Apple-Silicon test-leg note (the langflow answer: add osx_arm64 there + provider: {osx_arm64: default}, rerender). No recipe, code, or gotcha-count change. Files: SKILL.md, reference/conda-forge-yml-reference.md, config/skill-config.yaml (8.85.0 โ 8.85.1), MANIFEST.yaml, CHANGELOG.md.
v8.85.0 (Sep 2, 2026) โ conda-forge dropped Python 3.10: the floor is now 3.11, and the skill stopped hardcoding it (MINOR). conda-forge-pinning 2026.09.02.08.53.43 removed 3.10 from python_min and the matrix (now 3.11, 3.12, 3.13, 3.14; win-arm64 / linux-riscv64 stay 3.14-only). The floor had been HARDCODED in recipe-generator.py (_CONDA_FORGE_PYTHON_FLOOR = "3.10" + five literal comparisons), so the generator would have kept emitting recipes pinned to a Python conda-forge no longer builds; it now reads the installed pinning, with the literal only as an offline fallback (fallbacks in recipe_optimizer.py and conda_forge_server.py moved to 3.11). Two SEL-004 unit tests were hardcoded the same way and broke on the move โ both are now floor-relative. Rule: a moving ecosystem constant is read, never written down. Corpus sweep removed context.python_min from 438 recipes at/below the floor (Rule 6 โ delete the line, don't bump it; 123 legitimately above the floor kept), and repaired 47 recipes that did not parse at all across four classes (22 G92 uncommented Error: lines, 13 unquotable plain scalars, 4 G20 bare {{ }}, 8 structural โ incl. basemap/sentencepiece/opencv each missing the - marker on their FIRST output, and imagecodecs' leaked SentinelType repr from a failed v0โv1 migration). Retired tests/meta/test_dashboard_renders.py: it demanded docs/dashboard/check_render.js exist while dashboard-drift lists that path in its reintroduction gate โ two gates in direct contradiction, red since 2026-08-25. Files: SKILL.md, scripts/{recipe-generator,recipe_optimizer}.py, .claude/tools/conda_forge_server.py, tests/unit/test_recipe_optimizer.py, tests/meta/*, 485 recipes/*/recipe.yaml, repo-root conda_build_config.yaml, config/skill-config.yaml (8.84.1 โ 8.85.0), MANIFEST.yaml, CHANGELOG.md.
v8.84.1 (Sep 2, 2026) โ 2026-09-02 suite-advance housekeeping Rule-2 retro (PATCH): bmad_suite_metapackage.py appended instead of replacing. Closing retro for the three-package suite bump (bmad-builder 2.2.2 / creative-intelligence-suite 0.3.2 / TEA 1.24.0, all built GREEN and published to SelfExplainML). The instructed generate-bmad-suite regen corrupted recipes/bmad-suite/recipe.yaml via two untested defects in _rewrite_recipe: (1) the BEGIN..END regex captured the marker and the old body in group 1 and re-emitted it, so each run APPENDED a second run: key under requirements: rather than replacing the first โ silent, because yaml.safe_load resolves a duplicate key last-wins; (2) the version regex's trailing \s*$ under MULTILINE ate the blank line after context.version. Fixed by capturing the BEGIN line ([^\n]* preserves its "do not edit by hand" note) and switching both \s* to [^\S\n]*. Added tests/unit/test_bmad_suite_metapackage_rewrite.py (6 cases, A/B verified 4-failโ6-pass) โ nothing tested this splice before. Lesson: a marker-splice generator must consume the old body and re-emit only the marker, and a "did it work?" check on YAML must assert on the emitted TEXT, since safe_load hides the duplicate-key failure. No recipe-authoring gotcha; no new section. Files: scripts/bmad_suite_metapackage.py, tests/unit/test_bmad_suite_metapackage_rewrite.py (new), SKILL.md, config/skill-config.yaml (8.84.0 โ 8.84.1), MANIFEST.yaml, CHANGELOG.md.
v8.84.0 (Aug 23, 2026) โ Steward 15.2 Rule-2 retro (MINOR): github_updater HEAD-advance. github_updater.py gains --head / update_recipe_head for commit-pinned recipes (bump context.commit to default-branch HEAD + recalculate sha256). Powers steward suite advance CAP-2 tag|head autotick selection. No new gotchas; G109 still governs version-of-record re-derivation on HEAD bumps.
v8.83.0 (Aug 21, 2026) โ BMAD 6.11.0 suite-refresh retrospective (Rule 2): 2 new gotchas G109/G110 + a recurrence and a flagged question (MINOR). Closing retro for the bmad-suite recipe refresh that shipped alongside the BMAD-METHOD 6.10.0โ6.11.0 core upgrade (six recipes bumped + built green + published to SelfExplainML; bmad-story-automator removed as retired upstream). G109 (new) โ upstream renumbered PAST a dev-snapshot version: bmad-manticore's awaited 2.0.0 tag will never exist (consolidated into unreleased 3.0.0/3.1.0; version of record in marketplace.json, no package.json); repackaged 3.1.0.dev0 @ head. G110 (new) โ an npm bin entry can be published EMPTY one release and real the next: TEA 1.19.1's tea-test-review bin was {}, real since 1.20.0, so the 1.23.2 bump had to vendor the Node CLI (npm --omit=dev, G6 symlink strip, sh + prefix-baked .bat shims, nodejs per G103) with a --help smoke test. G92 recurrence โ recipes/bmad-method/recipe.yaml carried a parse-fatal bare Error: Failed to fetchโฆ line inside its CFE comment block (committed corruption from bulk commit d204da00fd); prefixed as a comment. New dep noted โ bmad-loop 0.10+ requires regex >=2024.11.6 as a core run dep (env-fault classifier's per-call timeout=). Flagged for the next host-gate pass โ _http.py's _host_of does not strip a trailing root dot, while consumer canonicalizations (pyforge-steward keys._canonical_host) do; a dotted-FQDN URL misses the configured-host set (found adapting steward's conformance suite to v8.82.0's dual gate). Cheatsheet gains the SelfExplainML publish flow (anaconda -s https://api.anaconda.org upload โฆ โ the bare client blocks non-TTY on a destination prompt). Files: SKILL.md, quickref/commands-cheatsheet.md, config/skill-config.yaml (8.82.3 โ 8.83.0), MANIFEST.yaml, CHANGELOG.md.
v8.82.3 (Aug 20, 2026) โ Story 5.5 fourth review pass: the public-host floor, two netrc regressions, and a rationale that was never traced (PATCH). A third independent follow-up review found five real defects in the previous two passes' own fixes, each reproduced before triage. (1) _public_default_hosts() derives from _DEFAULT_* globals and so could not see anaconda.org, files.pythonhosted.org, repo.anaconda.com or dev.azure.com โ all hosts this module requests; each was reproduced receiving JFROG_API_KEY under a *_BASE_URL naming it, the very public-host leak v8.82.1's subtraction exists to close. Added _PUBLIC_HOST_FLOOR beneath the derivation, and an empty result is never cached (an empty subtrahend re-opens the gate). (2) v8.82.2 moving netrc_credentials to _host_of broke it in the other direction: _host_of lowercases and netrc.authenticators does not, so a legal machine ARTIFACTORY.CORP.COM fell through to default โ the same failure that migration was made to fix. (3) netrc_credentials's Path.home() sits outside the try and raises RuntimeError in rootless containers; v8.82.0 gating the JFrog branch handed it traffic on every request to an unconfigured host. (4) inventory_channel.py's fallback floor held 9 hosts against _http's 18, still wider in the leaking direction for 11 while claiming otherwise; brought to parity with a containment test. (5) v8.82.2's credential-kind split sent CONDA_TOKEN to every named public host, including repo.prefix.dev and pypi.org; restricted to the anaconda.org family. Also: v8.82.2 justified its _EXPLICIT_CHANNEL_HOSTS and sys.path fixes with a "long-lived MCP server" failure mode asserted as reproduced โ conda_forge_server.py::_run_script subprocesses these scripts per tool call, so it is unreachable there; the fixes stay, the claims are corrected, and SKILL.md gains the rule that a fix's justification must name the entry point it was traced through. pyforge-doctor's inverted golden-fixture test now pins its own precondition instead of asserting an absence indistinguishable from having scanned nothing. Unit suite 1386 โ 1401 passed, 0 failed.
v8.82.2 (Aug 20, 2026) โ Story 5.5 third review pass: credential-KIND gating + the third host-parse + a red sibling suite (PATCH). A second independent follow-up review found the v8.82.1 gate still wrong in three ways, each reproduced before triage. (1) The gate answered "may this host receive a credential?" but never "may it receive this one". dependency-checker.py authorizes the channel the operator NAMED, and that channel is routinely public, so JFROG_API_KEY still reached conda.anaconda.org โ and, the JFrog branch being first, it shadowed the CONDA_TOKEN v8.82.1 had just fixed, whenever both were set. Now split by credential kind: a named public host gets its channel token only. _EXPLICIT_CHANNEL_HOSTS also only ever grew โ in the MCP server a host from one request's --channel stayed authorized for every later one; it is now cleared per call. (2) inventory_channel.py's fallback never got the public-default subtraction, making the copy wider than _http in the leaking direction while claiming to be narrower. (3) netrc_credentials was the third netloc.split(":")[0] in _http.py, left behind because it is not on the allowlist path; a userinfo URL fell through to any .netrc default entry. Also: pyforge-doctor's golden-fixture test used _http.py's live leak as its one non-synthetic fixture, so fixing the leak turned that suite red (A/B: 2 findings pre-retro, 0 post) โ inverted into a regression guard that the real scripts stay clean. G108 was documented backwards: CONDA_PYTHON_EXE is conda's own (base) interpreter, not the activated env's, so preferring it over sys.executable ran the child under a python that need not have the caller's dependencies; all four call sites corrected. Four sys.path inserts guarded (the fix v8.82.1 made in one file and reintroduced in four), three tests that could not fail repaired, and _paths's "correct-but-duplicated" claim corrected (~35 copies carry an un-.resolve()d walk). Unit suite 1377 โ 1386 passed, 0 failed.
v8.82.1 (Aug 20, 2026) โ Story 5.5 follow-up review pass: host-parsing + gate-coverage fixes to v8.82.0 (PATCH). An independent follow-up review found the new JFrog host gate correct in shape but wrong in three places that decide, in opposite directions, whether a credential is withheld or handed over. (1) _host_of() parsed with urlparse().netloc.split(":")[0], which returns the username for https://svc:tok@artifactory.corp/... (so the operator's real mirror never entered the allowlist and silently lost its credential) and collapses every IPv6 literal to [2001 (so an unrelated same-prefix address matched and received it) โ now urlparse().hostname; the same defect had been copied verbatim into inventory_channel.py's fallback. (2) Public default hosts could enter the allowlist via the env-var half, so a redundant PYPI_BASE_URL=https://pypi.org/simple re-opened the leak โ _public_default_hosts() now derives them from the module's own _DEFAULT_* globals and both halves subtract them. (3) dependency-checker.py takes its channel from --channel/CONDA_CHANNEL_URL/enterprise config, none of them a *_BASE_URL, so the gate withheld the credential from the operator's own Artifactory channel and left CONDA_TOKEN reaching nothing โ it now unions _EXPLICIT_CHANNEL_HOSTS (explicit branches only, never the public fallback), and its _http-unimportable path no longer degrades to "attach unconditionally". Also: the gate is skipped when no JFrog credential is set (it gated nothing else and cost a stat + TOML parse on every request); _pixi_configured_hosts() no longer lets read_pixi_config()'s Path.home() RuntimeError raise through every outbound request; npm's own npm_config_registry/NPM_CONFIG_REGISTRY join the scan; dependency-checker.py stopped growing sys.path once per call; and the three _paths callers still crashing on v8.82.0's own None contract (feedstock_context/feedstock_lookup/bootstrap_data bound it into a module-scope _DIR / "name" โ a TypeError at import) now degrade or exit cleanly. Two doc overclaims corrected (the "still-unpatched copies" contradiction, and mason_cfe_surface_check.py "proving" exactly-once use). Unit suite 1352 โ 1377 passed, 0 failed.
v8.82.0 (Aug 20, 2026) โ pyforge-mason Epic-5 closing retrospective (Rule 2): 1 new gotcha G108 + 2 new operating constraints (MINOR). The mandatory closing retrospective for the pyforge-mason BMAD effort (Story 5.5) โ the effort's one commit sanctioned to touch the CFE surface (AD-15; Story 5.2's mason_cfe_surface_check.py scans only commits touching mason source, so it proves no mason-source commit smuggles a CFE change โ not that the exception was used exactly once). Triaged the four CFE upstream defects epic-5-context.md flagged during planning. New Critical Constraint (path resolution) โ ~30 scripts hand-rolled a 1-line _get_data_dir() in three subtly different shapes; two (feedstock_context.py, feedstock_lookup.py) resolved the WRONG directory entirely (.claude/skills/data/... instead of .claude/data/... โ a live divergence, proven by a stray .gitignore entry for the wrong path). bootstrap_data.py's REPO_ROOT walked one level too far; recipe_optimizer.py's _read_conda_forge_python_floor() (SEL-004) walked one level too shallow, so it could never find the real pinning file and silently always used its hardcoded default. New shared scripts/_paths.py (get_data_dir()/get_repo_root(), lazy + None-returning on resolution failure rather than crashing on import) fixes all four confirmed-wrong sites; the ~26 other correct-but-duplicated copies are left alone (disproportionate churn for a closing-retro commit). New Critical Constraint (JFrog credential host-gating) โ _http.py's unconditional JFROG_API_KEY injection (the cross-resolver leak documented since v8.14.0's skip_auth partial mitigation) is now gated on _configured_enterprise_hosts(), derived from every currently-set *_BASE_URL env var AND the operator's pixi config (docs/reference/pixi-config-jfrog.example.toml documents pixi-only as this repo's RECOMMENDED setup) โ closes the leak without an SSRF-style private-IP denylist (which would break the enterprise-mirror routing AUD-CFE-004 already flagged as undoable that way). A same-pass adversarial review (Blind Hunter + Edge Case Hunter) found this fix's first landing incomplete on 5 counts, all patched before this commit: missing pixi-config hosts (functional regression, not just a residual leak), a JFrog/GitHub if/elif chain that could shadow GITHUB_TOKEN when a *_BASE_URL resolved to github.com, no port-stripping on host comparison, _paths.py's import-time crash risk (see above), and โ most significant โ TWO sibling scripts (dependency-checker.py, inventory_channel.py's no-_http fallback) with their own independent, still-unconditional copies of the identical leak, now fixed the same way. G108 (new) โ recipe_updater.py's autotick bot hardcoded a bare "python" for its internal recipe_editor.py subprocess call, unlike sibling github_updater.py's CONDA_PYTHON_EXE-or-sys.executable resolution (DW-2-10-2, found by Story 2.10 adversarial review). Full CFE suite verified against a git stash A/B baseline โ zero regressions; 9 pre-existing meta-suite failures confirmed byte-for-byte identical before/after, not merely re-asserted. Files: SKILL.md, scripts/_paths.py (new), scripts/{feedstock_context,feedstock_lookup,bootstrap_data,recipe_optimizer,recipe_updater,_http,dependency-checker,inventory_channel}.py, 7 new tests/unit/*.py, 3 updated tests/unit/*.py, .gitignore, config/skill-config.yaml (8.81.0 โ 8.82.0), MANIFEST.yaml, CHANGELOG.md.
v8.81.0 (Jul 29, 2026) โ Round-4 code-audit remediation retro (Rule 2 โ the AUD-CFE-* + AUD-REPO-001 findings; 1 new operating constraint; MINOR). Closes the AUD-CFE-* half of spec-code-audit-remediation-2026-07-26, which the 2026-07-27 incorporation record left with no disposition ("pyforge-atlas only"). PR #131 is abandoned, so every finding was re-verified OPEN against main rather than trusted from the branch's Status: lines. No recipes authored; no recipes/** touched. New Critical Constraint โ Every Recipe-Facing Path Argument Confines Through _path_guard: submit_pr.prepare_branch (slug โ recipes/ โ copytree into a public fork), recipe_editor.execute_actions (suffix-only check, so any repo YAML was writable) and trigger_build (any recipe path on disk) all skipped confinement; they now share one helper, whose root is read per call so the test override cannot be silently defeated. Root cause found: /.claude/skills sat in .git/info/exclude โ 919 files there are tracked, so it hid only new ones, which is why _path_guard.py and tests/meta/test_dashboard_renders.py were written and never committed (PR #131 imports a module it does not ship). Guarded by tests/meta/test_skill_files_tracked.py, which walks the filesystem instead of asking git status (that honours the exclude and reports clean). Refinements: a keyword substring scan false-positives on identifiers (updated_at contains UPDATE) โ word-boundary matching; query_atlas's order_by was interpolated with no validation โ allowlists on select/order_by, where keeps its documented subqueries, connection read-only, limit clamped at both ends (limit=-1 = no limit in SQLite, verified to dump a table); the atlas N+1 fix applied to both readers via a shared chunked helper, not just the one the finding named (with sqlite3.connect(...) manages the transaction, not the connection). AUD-REPO-001 dependency-completeness gate restored (pyforge-deps-test, pure stdlib so it runs in the lean env) โ it found AUD-WARDEN-010 still open on main on its first run. Also: Gemini key โ x-goog-api-key; provenance-hook return/argparse/exit fixes; .secrets into the tracked .gitignore; --force-with-lease on the fork sync; wiki-test wired for a suite that collected nowhere; stale 7.0.0 in SKILL.md/MANIFEST.yaml corrected. AUD-CFE-003/004 stay deferred (004 cannot be a private-IP denylist โ internal hosts are what <HOST>_BASE_URL routing targets). Files: SKILL.md, MANIFEST.yaml, config/skill-config.yaml, reference/mcp-tools.md, scripts/{_path_guard,submit_pr,recipe_editor,scan_project}.py, .claude/tools/{conda_forge_server,gemini_server,mcp_call}.py, .claude/hooks/post-tool-call.py, tests/{meta,unit}/โฆ (+134 tests), tests/packaging/, pixi.toml, .gitignore, CHANGELOG.md.
v8.80.0 (Jul 29, 2026) โ pyforge-atlas Epic-10 post-audit retro (Rule 2): 1 new gotcha G107 + a verified mapping-data fix (MINOR). From Epic 10 (stories I0โI5, 6/6 merged; kedro-test 803 โ 901 passed); no recipes authored, no recipes/** touched. G107 (new) โ a conda package EXISTING under the bare name does not mean it ships the Python module: conda-forge playwright is the Node CLI/driver with no site-packages module, while the import playwright bindings are playwright-python; on PyPI the bindings ARE playwright and no playwright-python distribution exists. The four-spellings walk starts at the bare name, finds the CLI, and stops โ the env resolves cleanly and the recipe lints, so nothing fails until something runs import. Both halves are usually required. Mapping-data defect FIXED โ different_names.json had a parselmouth-sourced entry keyed playwright-python โ conda playwright (a non-existent PyPI name pointing at the module-less half) while the real name playwright was absent, guaranteeing the fall-through; treat source: parselmouth rows as fallible on split distributions. Technique โ story I0 cleared 17 collection errors by deriving run-deps from an AST import scan (3 declared vs 19 imported), classifying each as conda-mappable, PyPI-only (โ a packaging blocker, not a declarable dep โ boring_semantic_layer), or stdlib. Files: SKILL.md, pypi_conda_mappings/different_names.json, config/skill-config.yaml (8.79.1 โ 8.80.0), CHANGELOG.md.
v8.79.1 (Jul 23, 2026) โ regenerable-factory Wave-4 retro (Rule 2): governance binding, no guidance change (PATCH). The skill surface (.claude/skills/conda-forge-expert/**, .claude/scripts/conda-forge-expert/**, .claude/tools/conda_forge_server.py) is now governed by the brownfield spec kernel spec-packaging-factory (local-recipes BMAD project) under the repo-wide scripts/spec_surface_check.py detector, with a CHANGELOG sentinel โ a governed edit that moves neither this skill's CHANGELOG nor the spec memlog is a checker finding (Rule 2 mechanized). recipes/** is governed by spec-fleet-stewardship (coverage-only; per-recipe control stays the 10-step loop). No operational guidance, gotcha, CLI, or schema change. Files: CHANGELOG.md, SKILL.md (Version History), config/skill-config.yaml (8.79.0 โ 8.79.1), tests/meta/test_spec_surface_check.py (new โ the detector joins the meta suite).
v8.79.0 (Jul 18, 2026) โ pyforge-atlas Kedro-migration retro (Rule 2): 1 new reference subsection, no recipe/gotcha change (MINOR). The BMAD project pyforge-atlas completed its 32-story Kedro/Dagster/DuckDB migration of the cf_atlas orchestrator (Waves 0 + AโH; the Wave-H AI-Software-Factory layer โ Karpathy wiki + agno crews + La Suite/Wagtail sync + Dagster crew orchestration โ landed across PRs #96โ#102). The effort authored no conda recipes and changed no operational guidance (it is a parallel reimplementation, not a replacement of conda_forge_atlas.py), but validated two durable phase-engineering patterns now captured in reference/atlas-phase-engineering.md ยง 14, both applicable to the current hand-rolled phases: (ยง 14.1) make a phase's network fetch an INJECTED callable defaulting to OFFLINE โ no HTTP/DB/process-client import in the phase body, and the default REFUSES rather than reaching for a public endpoint โ so the phase's dry-run gate is genuinely offline and the live client is one documented injection point (validated across the migration's refresher/event_source/raw_lister/opener seams, enforced by a no-inline-IO AST gate worth borrowing as a review check); (ยง 14.2) propagate the source StalenessMarker forward through EVERY derivation hop so a derived card never reads "fresh" over a skipped/last-good source (the AD-13 "degrade toward stale/indeterminate, never a false fresh" discipline the live atlas already applies to Phase G KEV, stated as a rule). No new gotcha, no CLI/schema/phase change. Files: reference/atlas-phase-engineering.md (ยง 14 + this note), config/skill-config.yaml (8.78.0 โ 8.79.0), CHANGELOG.md.
v8.78.0 (Jul 14, 2026) โ reactpy-v2 chain retro (Rule 2): 1 new gotcha G106 + 4 refinements (MINOR). From packaging the reactpy-django 6.0.0b1 (ReactPy v2) pre-release chain local-only โ 5 recipes (http-router โ asgi-tools โ reactpy 2.0.0b13 โ reactpy-router 3.0.0b1 โ reactpy-django 6.0.0b1), all GREEN on linux-64, verified end-to-end in a fresh env and published to SelfExplainML. G106 (new) โ a build-hook SKIP env var does NOT stop hatch-build-scripts from DELETING the hook's prebuilt artifacts: OneScriptConfig.clean_artifacts defaults to True and initialize() unlinks every file matching the hook's artifacts globs BEFORE running commands, so a skipped script regenerates nothing and the sdist's prebuilt asset is gone. Silent by construction (the asset is data โ imports: + pip_check stay green); live: reactpy shipped 71 of upstream's 72 static files, losing the 1.6 MB embedded PyScript wheel, until a clean_artifacts = false source patch restored 72/72 parity. Triage by the hook's artifacts field: non-empty โ patch clean_artifacts = false; artifacts = [] โ nothing is deleted, just remove the hook block (both shapes occur in ONE upstream family โ reactpy vs reactpy-router/reactpy-django โ so check per package). General lesson: diff the built package against upstream's published wheel and encode the answer as a package_contents guard. CFEP-25 Django escape-hatch refinement โ don't settle for package_contents-only (it proves nothing about importability): a two-line settings.configure(DEBUG=False) usually makes a REAL import work, so prefer two script: tests (python_min + newest) with an explicit pip check, probing the minimum viable config in the failed build's surviving test_env (G27 technique). G73 refinement โ CHECK the sdist before adding a JS toolchain: a "JS-building" package often ships the hook's complete output in its sdist (the INVERSE of G51/G73's headline case); verified on all 3 JS packages in the chain, every one builds with zero node/bun. G90 addendum 3 โ 2 compiled-path emission gaps, both self-contradictory: noarch: python emitted alongside compiler('c') for Cython sdists (+ missing stdlib), on BOTH compiled recipes; and skip: py>=400, a v0 form v1 silently ignores (G3) where the real need was match(python, "<3.11"). License-File refinement โ an sdist vendoring node_modules/ must ship licenses for what SHIPS (package.json dependencies + bundler-inlined transitives + copied dists' own 3rd-party texts), not all ~200 vendored files: reactpy-django's BSD-2-Clause AND Apache-2.0 AND MIT AND EPL-2.0 AND BSD-3-Clause narrowed to MIT AND Apache-2.0 over 8 entries (the EPL/BSD came from dev-only tooling). Also verified live: G41 (reactpy-django declares >=3.9; real floor is 3.11 via reactpy 2.x โ trusting the declared floor would ship an unsolvable 3.10 install), G25 (the reactpy[asgi] flatten is why asgi-tools+http-router exist at all; pip_check enforces it), G94 (stale mirror pruning โ incl. a reactpy LICENSE carrying the wrong pre-relicense copyright). Recipe-level, no skill change: http-router ships no license file anywhere (absent from sdist AND repo; only metadata claims MIT) โ a genuine cf-submission blocker, recorded in its blocker list. Files: SKILL.md (G106 + CFEP-25 escape-hatch refinement + G73 CHECK + G90 addendum 3 + License-File node_modules note + Version History); config/skill-config.yaml (8.77.0 โ 8.78.0); CHANGELOG.md.
v8.77.0 (Jul 12, 2026) โ agent-tooling recipe-wave retro (Rule 2): 6 new gotchas G100โG105 (MINOR). From the 35-recipe build+publish wave (SelfExplainML channel). G100 npm CLIs are per-arch (openspec/bmalph pattern) โ noarch+bash-wrapper+__unix can't serve win; G101 Bun-native npm dists (shim + per-platform optionalDependencies static binaries; may segfault, no source build โ pin to last pure-JS version; ccusage 19.0.3); G102 staged-recipes win-64 leg builds noarch too โ unix-only scripts render EMPTY and die at "No license files were copied" (PR #34176; win branch / build.bat / skip:win); G103 don't copy npm engines caps into run deps (nodejs run-export conflict); G104 strict channel priority โ any stale local-channel package shadows ALL conda-forge versions (portalocker 2.7.0 vs openkb); G105 rattler python tests run pip check by default + the canonical dual-python_version test block. See CHANGELOG.
v8.76.1 (Jul 11, 2026) โ atlas + detail-card bug-fix bundle (PATCH โ 4 fixes; no schema/CLI/phase/gotcha change). From a direct atlas-operations session (admin refresh + rxm7706/about maintainer-list update). (1) detail-cf-atlas --vdb-all ScoreType crash โ appthreat-vulnerability-db 6.6.2's partial model_dump leaves the CVSS baseScore as a RootModel/ScoreType object โ the per-CVE sort's float(cvss_score) threw; new _coerce_cvss_score() unwraps it. (2) --vdb-all KEV alignment โ the raw list read KEV from vdb's own flags (~always False; aqua ignores kevc/) โ 0 KEV vs the Phase G card's real count; new _load_kev_cves() overlays the atlas cisa_kev catalog and reports KEV-affecting-current (matches Phase G). (3) Phase B.5 dbt-collapse โ feedstock_name took feedstocks[0]; for a split-out output feedstock-outputs lists both the umbrella and the dedicated feedstock (dbt-bigquery โ ['dbt','dbt-bigquery']), collapsing the dbt-* family into dbt; new _pick_feedstock() prefers the entry == pkg_name when >1 (live: family splits into 18 feedstocks). (4) write_meta phases_run overwrite โ the v8.22.0 split runs core/F/K/N as separate build --only calls each overwriting phases_run โ a full admin run recorded only ['N']; now MERGES (union, canonical order) via _merge_phases_run + _read_meta_phases_run. All verified live (--fresh + non-fresh admin reruns: F/K/N land, phases_run == full BโฆN). +11 unit tests. See CHANGELOG.
v8.76.0 (Jul 6, 2026) โ license-map-gap โ the 4th seed-gap suggester (MINOR โ new CLI). Targets the last un-automated curated asset: the in-code conda_forge_atlas._LICENSE_TO_SPDX free-textโSPDX map (~36 entries; _normalize_license_to_spdx returns None on a miss, silently degrading the Phase R/S license-readiness score). Offline: scans pypi_intelligence for license_raw rows whose license_spdx IS NULL (the map missed), ranks by package count, filters junk (empty/unknown, >60-char full-text pastes, see โฆ/URL forms, SPDX expressions, already-mapped forms), and tiers each surviving form as likely (exactly one whole-token vendored-SPDX candidate โ a paste-ready "<form>": "<id>", line) or report (zero/multiple candidates โ human picks). No fuzzy matching โ a wrong license map is a correctness bug; the candidate is a conservative hint only. READ-ONLY โ no write path to conda_forge_atlas.py (a fixture test asserts the source is byte-identical across a full CLI run). Three-place rule; CLI/pixi-only. 7 fixture tests. Spec: docs/specs/seed-gap-suggesters.md (family spec, extended). See CHANGELOG.
v8.75.0 (Jul 6, 2026) โ cwe-seed-gap + spdx-schema-gap suggesters (MINOR โ two new CLIs). The seed-gap family after v8.74.0 lts-registry-gap, extending the "push automation further" line to two more hand-curated data/ assets. cwe-seed-gap (offline) scans cwe_categories rows still bucketed Other, keyword-classifies each cwe_name into the 7 real categories at strong (category-defining phrase) / weak (generic word) tiers via a fixed-precedence heuristic (so "OS Command Injection" โ RCE, not Injection), and emits ready-to-paste seed lines + an "Other-bucket affects N packages" impact headline. spdx-schema-gap diffs the vendored 811-ID SPDX enum against the upstream SPDX license list (spdx/license-list-data, GITHUB_RAW_BASE_URL-routable, TTL-7d cache + offline-stale fallback) and partitions the license strings on v_actionable_packages the enum misses into add-to-schema (a real upstream SPDX ID โ genuine staleness, package-count-ranked) vs non-standard (normalize, report-only); compound SPDX expressions are skipped; --drift lists the pure upstream-vs-vendored delta. Both are READ-ONLY โ the seeds stay hand-curated, accept/reject is git review, and each fixture suite asserts its seed file is byte-identical across a full CLI run. Three-place rule per tool; CLI/pixi-only. 14 fixture tests. Spec: docs/specs/seed-gap-suggesters.md. Kedro reflection (three seed-gap loops as read-only report nodes) folded into the ยง 3.4 boundary + prototype branches once they land. See CHANGELOG.
v8.74.0 (Jul 6, 2026) โ lts-registry-gap suggester (MINOR โ new CLI). The mapping-gap sibling for the LTS registry: diffs endoflife.date's all-products list (/api/all.json via resolve_endoflife_urls("all"), TTL-7d eol_products.json cache with offline-stale fallback) against v_actionable_packages, excludes registry-covered names (aliases included via load_lts_registry), and proposes ready-to-paste YAML entries at conservative exact / likely tiers (lowercase equality; _โ- normalization; python-/py- prefix strip). READ-ONLY by design โ the registry stays hand-curated; accept/reject is git review. Three-place rule + 10 fixture tests (incl. a registry-file-untouched assertion); CLI/pixi-only like library-futures/add-handoff. In-PR Gemini review applied (mixed-case registry-key fold for direct classify() callers; --out parent-dir creation). Spec: docs/specs/lts-registry-gap.md. See CHANGELOG.
v8.73.1 (Jul 6, 2026) โ post-merge Gemini sweep (PATCH). 13/14 findings from PRs #36/#37/#38 applied (!=3.14.1 patch-exclusion lookahead, YAMLError catch in the registry loader, float-weight guard, now-threaded silence cap, chunked notes-override query, null-metadata annotation guards in BOTH annotators, [--json] doc rows); 1 declined with posted rationale (universe-sbom DOES support --json). +7 tests. See CHANGELOG.
v8.73.0 (Jul 6, 2026) โ cyclonedx-universe-inventory S-retro (Rule 2): 1 new gotcha G99 + G74 productization note (MINOR). Closes the effort (Waves AโE all shipped; local gates ALL PASS, dated Dev Notes in the spec). G99 โ fixture-scale tests hide O(nยฒ) validator behavior: jsonschema's uniqueItems on the real 856k-component BOM = ~3.7ร10ยนยน pairwise dict comparisons (killed at 75 min of a projected multi-day run; a 10k benchmark misleads because the quadratic term dominates it too); the tractable equivalent = strip from a schema copy + exact O(n) canonical-JSON hash check + chunked parallel walk (31 s). G74 gains the inventory-match productization note (decision-4 live post-pass; refleak/doclang live recovery). reference/dependency-input-formats.md gains the daemonless container-intake runbook (root-gated docker socket โ pixi exec skopeo with a v2 --registries-conf override [conda-forge's skopeo ships a rejected v1 file] โ --oci-archive + pixi exec-provided syft). Retro also recorded: the Wave C SPDX-id normalizer correction + Wave D lts-like heuristic landed in-wave; find-alternative similarity quality on replace rows (cherrypy-for-grpcio) is a known limitation, report-only. Cross-spec sweep + BMAD baseline re-stamped. See CHANGELOG.
v8.72.0 (Jul 6, 2026) โ cyclonedx-universe-inventory Wave E / S9 docs (MINOR, docs-only). mcp-tools.md +4 tools + cheatsheet rows; SKILL.md Atlas CLI table +7; atlas-phases-overview Part A ยง Consumer +4 persona rows; commands-cheatsheet suite block; atlas-operations post-rebuild regen cadence; dependency-input-formats re-grounded for S5a with policy tiers; kedro-migration cross-spec note extended (Waves BโD live surface). Remaining: Wave D local gates + S-retro. See CHANGELOG.
v8.71.0 (Jul 6, 2026) โ Wave D of cyclonedx-universe-inventory: library-futures (S7) + recommend-2027 (S8) (MINOR). The 2027โ2030 survival layer: S7 scores every matched cf package (any ecosystem) via a transparent weighted composite (one auditable dated weights dict; denominator-shrinking signals_absent; tier = lattice meet with py314-not-ready / 18-month-silence / KEV caps and archived / yanked / EOL floors; EPSS+CWE report-only; PyPI downloads weight-0) with the two horizon signals (py314 readiness; LTS/EOL via endoflife.date โ the new git-tracked data/lts-registry.yaml โ lts-like, TTL cache, offline-safe). S8 runs S5โS7 into ONE scorecard (md/JSON/annotated CycloneDX with 5 cfe:* properties), operator overrides shown-never-silent, Phase-P refresh offered-never-run; MCP tool recommend_2027. The spec's full calibration gate (django 5.2 vs 4.2, KEV, silent, archived, py314 both ways, non-Python keep) passes as fixture tests. +62 tests. See CHANGELOG.
v8.70.0 (Jul 5, 2026) โ G54 source decision order in the generator (MINOR; closes DW-2026-07-03). Usable-sdist check (metadata-only sdists rejected) โ GitHub tag-archive fallback (streamed sha256, templated URL, cfe-source-kind: github-tag:no-sdist-on-pypi-G54) โ wheel last. Live-verified on redshift_connector. +2 regression tests. See CHANGELOG.
v8.69.0 (Jul 5, 2026) โ recipe-generator emission fixes (MINOR). DW-2026-07-03 items 1โ4+6โ8: [:10] run-dep cap removed (the G90 truncation root cause); per-version endpoint urls fallback (pinned pkg==ver generation fixed); _resolve_license() chain (PEP 639 โ short-string SPDX map โ classifiers โ GitHub API; full text never emitted); import extraction strips src-layout + excludes non-package dirs; host mirrors full [build-system].requires (G91); lowercase output dirs (G94c); canonical CFE block emitted on both v1 pypi paths via _render_cfe_block() (open since v8.31.0). 5 regression tests. DW item 5 (G54 wheel re-audit) remains open. See CHANGELOG for the full narrative.
v8.68.1 (Jul 5, 2026) โ full-parse audit becomes a permanent meta-test (PATCH). New tests/meta/test_recipe_yaml_parse_audit.py: (1) yaml.safe_load over every recipes/*/recipe.yaml (parse-fatal classes: G20 v0-jinja, G92 stray tokens, unterminated flow lists); (2) duplicate cfe-conda-name (PyYAML hides duplicates that rattler-build rejects); (3) stray whitespace-[] fold lines; (4) unquoted- # in cfe free-text list items (silent YAML comment-truncation). Found 7 live class-4 instances on first run (agentevals' blocker stored just "staged-recipes" โ the #34047โฆ sentence was parsed as a comment; + 6 lfx-family annotation-style items) โ all folded into quoted values. Also fixed pre-existing suite reds surfaced by the run: agno's missing schema header + the two openlineage recipes' redundant floor-level context.python_min. Meta suite: 963 passed / 0 failed. Companion fix: recipes/GetPyPiLatestVersion repaired (the last parse-fatal โ G20 v0 jinja + G1 env-var-across-entries + an upstream setup.py that self-versions via a live PyPI query, patched static; built+tested GREEN) โ repo at 898/898 parseable. Files: tests/meta/test_recipe_yaml_parse_audit.py (new); SKILL.md (G92 enforcement note + this entry); config/skill-config.yaml (8.68.0 โ 8.68.1); CHANGELOG.md.
v8.68.0 (Jul 5, 2026) โ cfe-metadata refresh + waiver-discharge session retro: 1 new gotcha G98 + 4 refinements (MINOR). From the 2026-07-04/05 direct-CFE session (atlas full-refresh verification + Phase G/Gโฒ from the vuln-db env; purl exports [33,392 conda + 843,641 pypi + 21,403 mapped pairs]; 437-recipe cfe-metadata refresh [purl channel qualifiers, 23 atlas-verified status promotions, 6 blocker clears, 15 v0โv1 tag clears, 15 meta.yaml deletions]; django-cryptography-django5 dist-info fix feedstock#6/#7; django-sql-explorer pip_check waiver discharge, deployed-feedstock leg superseded by the user's v0โv1 migration #15). G98 โ batch-edit discipline: line-edits + yaml.safe_load parse-gate after EVERY write (caught 3 in-flight failures), re.sub \g<1> not \1 before digits (octal-escape corruption), provenance-check parse failures against git diff (context-line corruption = pre-existing, not yours), trial-5-first + per-variant field-format matchers; + purl conventions (conda purl carries ?channel=conda-forge; purl-spec pypi normalization keeps DOTS โ PEP 503 over-normalization found+fixed in 2 recipes). G66 refinement โ UPLOADED โ INDEXED: the anaconda.org file API shows the artifact ~1 min post-merge but solvers read the served repodata index which lagged ~10โ30 min; watchers must poll current_repodata.json for the new basename (+ per-tool cache layering: rattler/conda caches lag independently). G80 caveat โ parse the waiver reason code to the FULL package name (django-cryptography vs django-cryptography-django5 near-miss โ wrong dischargeable verdict). G92 extension โ two more corruption classes reached COMMITTED history (stray [] fold lines ร4; unquoted-# flow lists that YAML comment-truncates ร5); full-parse audit over all recipes beats the duplicate-key grep; quote free-text cfe values containing #/:. G24 variant โ django-style alpha VERSION tuple + get_version = dev-timestamp wheel version on every from-source build (not setuptools_scm); fix = flip-to-"final" source patch + build-number bump + version-assert test (django-cryptography-django5 _5). Also: atlas-phase "G'" can NEVER be invoked via the pixi task (task-shell apostrophe quoting) โ use eval "$(pixi shell-hook -e vuln-db)" + direct python atlas_phase.py "G'"; single-phase runs update the DB but NOT cf_atlas_meta.json (quickref note). Files: SKILL.md (G98 + 4 refinements + cfe-purls schema block + this entry); quickref/commands-cheatsheet.md; config/skill-config.yaml (8.67.0 โ 8.68.0); CHANGELOG.md; auto-memory feedback_extra_is_local_internal_metadata.md (purl format).
v8.67.0 (Jul 3, 2026) โ punch-list closure retro: 3 new gotchas G95โG97 + G87/G90 addenda (MINOR). From the post-batch-v5 closure arc (red-PR fix waves, the django-fsm-2/openevals/agentevals + 4-recipe langgraph prereq chains, the sqlglotrs-stubโsqlglot-30.8.0โsqlmesh chain, and the 21-feedstock Set D bump-PR wave โ 21/21 delivered: 20 merged + azure-monitor-otel green-bound; tracker 88โ31). G95 โ build-clean-test-blocked = metadata-UNVERIFIED: a local test env that never solved means the import test + pip check never ran; 8/8 Set D CI-reds were exactly the BCTB recipes โ BCTB recipes need a STATIC gate (imports vs sdist top-level, run vs requires_dist, G10 spellings) before shipping. G96 โ bump-PR dep authority = previous feedstock recipe + upstream requires_dist diff, never the regenerated recipe (feedstocks encode host backends/setuptools-scm, dep caps, entry points, sidecar LICENSEs; verified: otel-boto3sqs, feast G39 0.0.0, sqlmesh, kedro-viz); extras map explicitly (uvicorn[standard]โuvicorn-standard, dask[dataframe]โdask). G97 โ unsolvable envs can be C-ABI ERA DIAMONDS between cf migration epochs (langgraph-api grpcio <1.81โlibgrpc 1.80 vs pyarrowโlibarrowโlibgoogle-cloudโlibgrpc 1.81); diagnose via per-build repodata depends pins, record as upstream blocker, never force-loosen. G87 addendum โ v0โv1-in-bump-PR requires flipping conda_build_tool: rattler-build BEFORE rerender (3ร verified crash on deleted meta.yaml) + probe recipe format before scripted edits. G90 addendum 2 โ three more emission gaps: silent run-block TRUNCATION (7 instances โ count-check vs requires_dist), dotted/non-Python bogus imports (src.<pkg>, go), REPLACE_SUMMARY/REPLACE_DESCRIPTION placeholders; + python_min duplicate-insert guard and SPDX-canonical-text LICENSE fallback. Also validated: G42 in reverse (sqlglotrs 0.13.0 = Rustโpure-python deprecation stub), G88 (sqlglotc same-namespace accelerator overlay), G89 mid-transition snapshots. Files: SKILL.md (G95โG97 + addenda + this entry); config/skill-config.yaml (8.66.0 โ 8.67.0); CHANGELOG.md.
v8.66.0 (Jul 3, 2026) โ batch v5 full-gate retro: 4 new gotchas G91โG94 + G90 license-class extension + G54 wheel-only addendum (MINOR). From the 52-recipe full-gate pass (generateโsanitizeโenrichโvalidateโoptimizeโisolated buildโCI-parity lintโcfe stamp; Sets A refresh/B minifiers/C v1-authorings/D manual bumps + pyobjc trio): 32 SUCCESS, 17 build-clean-test-blocked, 3 not-attempted (osx-only), 0 failed. G91 โ PEP 517 backend/plugin host deps the generator misses: BackendUnavailable: Cannot import 'uv_build' (shot-scraper) and UnknownPluginError: Unknown metadata hook: requirements_txt (kedro-viz โ hatch-requirements-txt); mirror the sdist's [build-system].requires (or the feedstock's host:) โ plugin-level sibling of G55. G92 โ any YAML re-serialization (feedstock_enrich) FOLDS the #### CFE comment block into plain inline extra: keys; a marker-regex stamp then appends a second block โ Duplicate key "cfe-conda-name" parse failure one stage later; cfe-strips must remove BOTH forms + grep -c cfe-conda-name == 1 audit (4/888 recipes hit, all restored). G93 โ conda-recipe-manager crashes (IndexError: pop from empty list) on column-0 comments inside indented blocks โ conda-smithy lint calls the recipe unparseable while rattler-build builds it GREEN (shapash); lint-level enforcement of the no-inline-agent-comments convention โ relocate to the bottom CFE block. G94 โ local mirrors accumulate stale feedstock files that fail local gates: ancient global-pinning conda_build_config.yaml snapshots (openlineage, 2023 matrix the feedstock deleted), obsolete meta.yaml after the feedstock's v0โv1 (the keep-meta rule's END condition), and case-variant generator output dirs (pyobjc-framework-CoreText/ stray vs the lowercase mirror) โ prune against the live feedstock on every refresh. G90 extension โ a second license emission class: SHORT non-SPDX strings (MIT License, Apache License, Version 2.0, bare BSD, deprecated AGPL-3.0) fail rattler's SPDX parse (10/52); normalize + resolve bare/empty cases against feedstock authority (PyPI metadata can be completely license-empty: azure-ai-ml, django-wildewidgets). G54 addendum โ three more wheel-only-on-PyPI cases (redshift_connector, openlineage-integration-common, django-mptt-admin) โ GitHub tag archives mirroring their feedstocks' source blocks; Directory '.' is not installable on a generated recipe usually means "generator picked the wheel". Files: SKILL.md (G91โG94 + G90/G54 addenda + this entry); config/skill-config.yaml (8.65.0 โ 8.66.0); CHANGELOG.md.
v8.65.0 (Jul 3, 2026) โ tracker punch-list remediation retro: 2 new gotchas G89โG90 + a G14 validation (MINOR). From the 2026-07-02/03 conda-forge-tracker punch-list effort (88 verified outdated-feedstock findings worked to: 19 merged/closed, 8 verified v1 fixes pushed to bot PRs, 31 rerender-kicked, 2 net-new prereqs identified). G89 โ stale red autotick PRs whose Azure runs are PRUNED (~30 d) are unrestartable: restart ci is a silent no-op (dead buildId links, timeline 404s โ G32's log triage has nothing to read); kick fresh CI with please rerender instead, budgeting for its side-effects: the rerender commit can DISABLE bot-automerge ("not all commits made by the bot" โ plan a manual-merge pass), some stale branches surface a merge-conflict lint (branch-update problem, not a lint fix), and an immediate post-kick triage reads a mid-transition snapshot โ re-triage after CI settles. G90 โ freshly-generated recipes need a sanitize/verify pass before building; five generator emission gaps verified on a 22-recipe batch: (1) pkg==version generation crashes (Error: 'releases'), (2) full license TEXT emitted into about.license (YAML/SPDX breakage) + REPLACE_LICENSE left when classifiers miss โ PEP 639 license_expression never consulted, (3) sdist tests/src dirs leak into tests.python.imports (G7 sibling), (4) wheel-over-sdist fallback (G54 re-check), (5) the canonical #### CFE metadata block is NOT emitted โ stamp manually with truthful cfe-local-build-*. Interim sanitize pass documented in the gotcha; generator fixes are the follow-up work: implement cfe-block emission, fix pinned-version generation, license_expressionโclassifierโGitHub-API license resolution, exclude non-package dirs from import extraction. G14 validated: the minimal YAML quote fix on float-lookalike versions (15.0/2.12/1.10), locally built then maintainer-edit pushed, re-lints green and automerges in minutes. Process note: one premature merge (redshift_connector #60 โ a verify step that gated on command success rather than check CONTENTS; merged mid-CI, main went red, fix validated locally: upstream 2.1.15 raised lxml >=6.1,<7) โ merge gates must assert the rollup contents. Files: SKILL.md (G89 + G90 + G14 note + this entry); config/skill-config.yaml (8.64.0 โ 8.65.0); CHANGELOG.md.
v8.64.0 (Jul 2, 2026) โ Atlas documentation consolidation: 5 docs โ 3 by audience (MINOR โ structural docs change; no code-behavior change). Per user direction ("consolidate all conda forge expert atlas skills"; dedupe-and-rewrite chosen over verbatim concatenation). (1) reference/atlas-phases-overview.md absorbed reference/atlas-actionable-intelligence.md โ now the single atlas-intelligence reference: Part A = the persona ร goal catalog (WHO uses WHAT), Part B = the phase-indexed overview (WHAT each phase does + writes), plus the Profile Reference. The catalog's ยง IV infrastructure rows that verbatim-duplicated the Phase F/P sections (Wave 2/3 window/trend/breakdown provenance, the three Wave-3 CLI mode lists, python_min) compressed into 5 pointer rows; the two "How to extend" protocols merged into one; the triangle diagram updated. (2) reference/atlas-phase-engineering.md absorbed reference/atlas-phase-p-cost-model.md as ยง 13 โ Phase P cost model + operator playbook (source backends, verified-2026-06-12 cost tables, dry-run preflight, tunables, decision tree, runbook, alternative-source verification, storage + annual projections); its "Hard cap behaviour" section deduped against ยง 10 (a) so the generic mechanics are stated once; header, ยง 10 pointers, and Cross-references updated. (3) guides/atlas-operations.md unchanged โ the operator runbook stays the third leg. Every LIVE pointer updated: SKILL.md (Operating-Principles cost-discipline bullet + ยง Atlas Intelligence Layer catalog/engineering pointers), INDEX.md, quickref/commands-cheatsheet.md, scripts/conda_forge_atlas.py Phase P docstring, tests/unit/test_pypi_only_candidates.py docstring, repo CLAUDE.md skill-internal list, and the two live specs (feedstock-failure-remediation.md, trendshift-conda-forge.md). Historical CHANGELOG/spec mentions of the absorbed filenames stay as dated record โ the surviving files carry merge notes. Zero content facts dropped: verified numbers, case studies, and provenance markers carried over; only structural duplication removed. Files: reference/atlas-phases-overview.md (merged, ~86 KB); reference/atlas-phase-engineering.md (merged, ~72 KB); reference/atlas-actionable-intelligence.md + reference/atlas-phase-p-cost-model.md (removed); SKILL.md; INDEX.md; quickref/commands-cheatsheet.md; scripts/conda_forge_atlas.py; tests/unit/test_pypi_only_candidates.py; config/skill-config.yaml (8.63.0 โ 8.64.0); CHANGELOG.md; + repo CLAUDE.md, docs/specs/{feedstock-failure-remediation,trendshift-conda-forge}.md.
v8.63.0 (Jul 1, 2026): flyte-2 SDK local-only closure retro โ 1 new gotcha G88 (MINOR). From a bmad-quick-dev run on docs/specs/flyte-conda-forge.md (package the Flyte 2 SDK flyte + its net-new closure). G88: shared protobuf-namespace stubs (buf.validate and any buf generate/protoc-generated <ns>/) collide across a closure โ a stub-only lib whose wheel OMITS the generated stubs (broken standalone), two packages both shipping the same top-level <ns>/ (conda file-collision), and version skew when the winner bundles an older copy. Resolve by making ONE package the single owner at the newest superset version (force-include its gen/ stubs, G59 source patch) and stripping the namespace from siblings (wheel surgery + run: dep on the owner); usually NOT cf-submittable without upstream coordination โ build local-only + file upstream. Also captured a generator gap: generate_recipe_from_pypi emitted noarch: python for pyqwest (a maturin/pyo3/extension-module cdylib) โ verify build shape against the sdist (G46/G48/G49). Delivered the full flyte-2 closure (6 recipes: protovalidate, pyqwest, connectrpc, flyteidl2, flyte, flyte-controller-base) built + import flyte/pip check GREEN local-only (not submitted โ upstream buf.validate blocker). Files: SKILL.md (G88 + this entry); config/skill-config.yaml (8.62.0 โ 8.63.0); docs/specs/flyte-conda-forge.md (delivered/blocker sections). Sibling of G51/G54/G55/G10.
v8.62.0 (Jun 28, 2026): v0โv1 feedstock-migration retro โ 4 new gotchas G84โG87 (MINOR). From migrating the deployed v0 mailpit + dlt-pendulum feedstocks to v1 recipe.yaml and opening the migration PRs (conda-forge/mailpit-feedstock#20, conda-forge/dlt-pendulum-feedstock#5 โ both fully GREEN incl. emulated osx-arm64/linux-aarch64). G84: migrate_to_v1/feedrattler is a REMOTE-feedstock-by-name tool (404s on a local recipes/<name>/ path) โ hand-author the local mirror's recipe.yaml; migrate the deployed feedstock on a fork + local rerender. G85: native trigger_build reports "No build summary found โ may have crashed" even on success (only docker writes build_summary.json) โ ground-truth from the .conda artifact + rattler-build test --package-file. G86: omit bot.run_deps_from_wheel when the recipe patches the wheel's dep metadata (dlt-pendulum patches out tzdata, re-adds python-tzdata in run:) โ the bot would regenerate run deps from the wheel and drop it. G87: v0โv1 feedstock migration must verify the feedstock's CURRENT version first (stale local mirror โ silent downgrade: mailpit local 1.30.2 vs feedstock 1.30.3), bump build.number to supersede, branch from upstream/main, strip cfe-*, drop conda_build.error_overlinking, rerender locally. Corrected the Manual-CLI feedrattler entry (was feedrattler recipes/<name>). Files: SKILL.md (G84โG87 + CLI fix + this entry); guides/migration.md (feedrattler clarification + Full Feedstock Conversion steps + discipline point 6); config/skill-config.yaml (8.61.1 โ 8.62.0); CHANGELOG.md.
v8.61.1 (Jun 28, 2026): conda-forge.yml adopts the recipe.yaml CFE-comment convention โ trim top + strippable bottom #### CFE block (PATCH). Per user direction: the generated conda-forge.yml carries only the trim # conda-forge.yml โ pre-seeds the feedstock comment + keys at the top (what's submitted), with the verbose G83 rationale in a bottom #### CFE metadata AND comments block that is local-recipes-only + stripped before push (mirrors recipe.yaml's #### CFE block). _cfy_template.py updated (bottom comment avoids the literal key strings the tests assert-absent โ 1789 tests green unchanged). G62 + step-8b strip-verify extended to loop recipe.yaml + conda-forge.yml. Worked example: recipes/tolaria/conda-forge.yml (osx_arm64-only ARM โ recipe skips linux-aarch64). Files: scripts/_cfy_template.py; SKILL.md (G62 + step-8b + this entry); config/skill-config.yaml (8.61.0 โ 8.61.1); CHANGELOG.md; recipes/tolaria/conda-forge.yml.
v8.61.0 (Jun 28, 2026): the universal conda-forge.yml pre-seed becomes the CFE default (generation + submission + platform-expansion) โ MINOR, behavior change. Operationalizes G83 (a staged-recipes per-recipe conda-forge.yml is forwarded into the feedstock on merge, so emitting it at authoring time pre-seeds the feedstock โ no post-merge bot/platform-expansion follow-up PR). New shared helper scripts/_cfy_template.py::render_conda_forge_yml emits the default on all 4 generator paths + submit_pr.py emit-if-missing (dry-run side-effect-free). Universal block: conda_build_tool: rattler-build + conda_install_tool: pixi + bot{automerge, check_solvable, inspection, run_deps_from_wheel}; inspection = update-grayskull (noarch:python) / hint-all (else); run_deps_from_wheel Python-only. Compiled recipes also get the full ARM block (build_platform+provider linux_aarch64+osx_arm64 + test: native_and_emulated); noarch + npm get none. No workflow_settings/error_overlinking/shellcheck/github.* in the default. Reverses the v8.11.0 npm "default mode emits no conda-forge.yml" decision. Docs reconciled: templates rewritten (fixed deprecated run_deps_from_wheel_metadata + azure.store_build_artifacts); reference ยง Recommended pre-seed defaults (+ dropped Shape 1's v0-only error_overlinking); SKILL.md step 8b optionalโdefault; platform-expansion guide+spec drop workflow_settings; memory rewritten. 1788 tests pass. Two forks resolved by the user: new compiled โ full ARM; bot block type-adjusted. Files: scripts/{_cfy_template.py,recipe-generator.py,submit_pr.py}; both templates/conda-forge-yml/*; reference/conda-forge-yml-reference.md; SKILL.md (step 8b + this entry); guides/feedstock-platform-expansion.md; docs/specs/feedstock-platform-expansion.md; memory; 3 test files; config/skill-config.yaml (8.60.0 โ 8.61.0); CHANGELOG.md.
v8.60.0 (Jun 28, 2026): conda-forge.yml per-setting applicability audit + G83 + a G77 correction (MINOR โ 1 new gotcha + reference overhaul). A 2-agent deep-research + adversarial review (the user broadened "validate every setting in lyric-py-feedstock's conda-forge.yml" โ a GENERAL reference: which key matters for which recipe TYPE + staged-recipes-vs-feedstock). Traced conda-smithy schema.py + the autotick cf_tick_schema.json + staged-recipes/.ci_support/build_all.py directly, cross-checked vs pydantic-core-feedstock. G83 (new): a staged-recipes per-recipe conda-forge.yml is almost entirely INERT in the PR โ build_all.py reads only conda_build_tool; the matrix is repo-fixed, the Linux image/CUDA auto-detect from recipe text, conda_pkgs_* publish unconditionally โ every other key just seeds the post-merge feedstock. Corrects G77: store_build_artifacts is a no-op in a staged-recipes PR (artifacts come from the fixed pipeline, not the key โ the earlier "works in the PR" reading was a misattribution); load-bearing only at the feedstock. Verified: error_overlinking = no-op on rattler-build (use the recipe's build.dynamic_linking); shellcheck.enabled = no-op without a build.sh (inline maturin/noarch/modern-npm/Rust-CLI; meaningful only for Go/C-C++/R-CRAN/Tauri); provider.<arch>: default is the enable-switch (field default None); nothing in the file is auto-rewritten by rerender (fixed the reference's wrong "auto-set" claim). Files: reference/conda-forge-yml-reference.md (new ยง Per-setting applicability audit โ classification table + recipe-type matrix + staged-vs-feedstock table + cargo-cult-to-delete list + pydantic-core baseline; + 4 inline corrections); SKILL.md (G83 + G77 correction + this entry); config/skill-config.yaml (8.59.0 โ 8.60.0); CHANGELOG.md.
v8.59.0 (Jun 28, 2026): G82 โ staged-recipes CANNOT build osx-arm64 (or any arm/aarch64) pre-merge; local-only on a Mac before merge, opt-in on the feedstock after (MINOR โ 1 new gotcha). From a 3-agent deep-research sweep (pipeline code + 14-PR precedent sample + conda-forge docs) answering "can PR #33133 produce a macos-arm64 build?". No โ the macOS matrix is one hardcoded cell (azure-pipelines-osx.yml: osx_64: CONFIG: osx64, Intel macOS-15), staged-recipes does not rerender per recipe (build_all.py reads only conda_build_tool; provider/build_platform ignored for the PR), no GHA build path, 3 core members confirm by-design. Pre-merge osx-arm64 is local-only via .ci_support/osx_arm64.yaml and requires a Mac (cross-host osx-64, not linux). Post-merge it's opt-in โ armosxaddition migration or manual provider: {osx_arm64: azure} + rerender. Validated: lyric-py-feedstock #2's provider-only block โ rerender generated the osx_arm64/linux_aarch64 .ci_support + all 8 ARM legs GREEN โ merged (also clears db-gpt's C1). To pre-configure a new recipe's feedstock, ship provider: {osx_arm64: azure} in the staged-recipes conda-forge.yml (ignored for the PR, carried to the feedstock) โ applied to recipes/tolaria/. Files: SKILL.md (G82 + this entry); config/skill-config.yaml (8.58.1 โ 8.59.0); CHANGELOG.md; recipes/tolaria/conda-forge.yml.
v8.58.1 (Jun 28, 2026): G18 refinement โ the conditional-list form is necessary but NOT sufficient; win_64 must be ABSENT from the store_build_artifacts platform list (PATCH). From debugging conda-forge/lyric-py-feedstock #2 (Rust+PyO3 osx-arm64/linux-aarch64 platform-expansion). The PR used the correct ConditionalValue list form for workflow_settings.store_build_artifacts but left win_64 in the platform: list โ win still stamped true โ all 4 win py-variants red at Prepare conda build artifacts (the G18 INetCache ACL 7z crash) while Run Windows build succeeded (.conda produced). The lever is win_64's absence from the list, not the list form. Dropping - win_64 + rerender โ win green โ PR merged (lyric-py now ships osx-arm64 + linux-aarch64). Files: SKILL.md (G18 refinement + this entry); config/skill-config.yaml (8.58.0 โ 8.58.1); CHANGELOG.md.
v8.58.0 (Jun 28, 2026): G81 โ conda-forge's pnpm 11 no longer reads package.json's pnpm field; a lockfile made under pnpm <11 fails --frozen-lockfile; pin pnpm <11 (MINOR โ 1 new gotcha). From a from-scratch authoring of tolaria (refactoringhq/tolaria v2026-06-26) โ an AGPL-3.0 (OSI โ cf-eligible) Tauri 2.10 desktop app (Rust + React/Vite + bundled MCP server, webkit2gtk). The latest stable tag scheme had changed (stable-v<dotted> โ v<YYYY-MM-DD>); mapped via v${{ version | replace(".","-") }} with a dotted conda version (dashes are invalid in conda versions). G81: the build failed in 5 s at pnpm install --frozen-lockfile โ conda-forge's pnpm 11.9.0 ignores package.json's pnpm field (overrides/patches moved to pnpm-workspace.yaml, which upstream only half-migrated: 4 partial patches, no overrides), so it saw empty overrides vs the lockfile's โ ERR_PNPM_LOCKFILE_CONFIG_MISMATCH. Fix = pin pnpm <11 (โ 10.33.2, still reads package.json โ matches the lockfile); the durable alt is patching pnpm-workspace.yaml to the full config; never --no-frozen-lockfile (drops load-bearing overrides/patches). pnpm is per-platform on cf (query the platform subdir's repodata, not noarch). Tooling-skew sibling of G14. Also fixed a source drift (new resources/agent-docs staged for linux/win + tested) and dropped the submission-incorrect recipe-dir conda_build_config.yaml. Built GREEN on linux-64 (Rust/Tauri 2m46s), all tests passed. Files: SKILL.md (G81 + this entry); config/skill-config.yaml (8.57.1 โ 8.58.0); CHANGELOG.md; recipes/tolaria/{recipe.yaml,build.sh,build.bat} (+ removed conda_build_config.yaml).
v8.57.1 (Jun 28, 2026): adversarial-spec-review remediation (PATCH โ refinements to G40 + G10, no new gotcha). From a 3-lens adversarial review of docs/specs/db-gpt-conda-forge.md + its fixes (the db-gpt review the user requested after the v8.57.0 rerun). G40 case study added โ db-gpt's dbgpt-app (noarch, declared all-platform) hard-deps the compiled lyric-py, which ships only linux-64/osx-64/win-64 โ uninstallable on osx-arm64/linux-aarch64; the green local build AND green-able staged-recipes CI both miss it (no ARM legs, G77) โ the adversarial review of the spec surfaced it; fix = platform-expand the feedstock (conda-forge/lyric-py-feedstock #2, draft + rerender). Lesson: an "all-platform" claim must be verified against live per-subdir repodata, not build/CI status. G10 example added โ duckdbโpython-duckdb (a python- prefix divergence: the bare duckdb feedstock is the CLI/old py-specific build [linux-64+noarch]; the cross-platform binding is python-duckdb); added as the 5th name-form in the G10 check + a table row (db-gpt recipe fixed duckdbโpython-duckdb, rebuilt GREEN โ 7/7 pip_check). Files: SKILL.md (G40 case study + G10 5th form/row + this entry); config/skill-config.yaml (8.57.0 โ 8.57.1); CHANGELOG.md; (spec + db-gpt recipe committed separately as 91c4cb0fed).
v8.57.0 (Jun 28, 2026): G80 โ a waived external-feedstock metadata bug is often fixed by a same-version REBUILD; re-check the latest BUILD, not the VERSION (MINOR โ 1 new gotcha). From the db-gpt rerun (rebuild all 8 db-gpt-scope recipes locally + refresh cfe metadata to v8.55.0, per a direct user request to "rerun all recipes locally to update CFE metadata"). dbgpt-app carried a pip_check: false waiver for conda-forge pdfminer.six's dist-info Version: 0.0.0 bug (pdfplumber pins ==20260107). The cf version was unchanged (20260107 โ looked unfixed), but the feedstock had rebuilt to number: 1: build _0 shipped 0.0.0, build _1 reports 20260107 (feedstock test now asserts it). G80: a dist-info/metadata defect is a feedstock build bug fixed by a build-number bump at the same version โ verify the latest BUILD (download the highest-build .conda, or read the feedstock number:/test), not the version string; then drop the waiver (re-enable pip_check, clear cfe-pip-check). Sibling of G77/G78; discharges the G24/G26/G28/G36 revert obligation. Re-enabled dbgpt-app pip_check (waiver dropped); all 8 recipes rebuilt GREEN locally + cfe metadata refreshed (cfe-local-build-* + v8.36 identity fields; lyric-py caches the [lyric] import divergence; db-gpt blocker-list trimmed to the 2 still-OPEN prereqs). Also confirmed (no new gotcha): G76/G78/G59/G7-G10 held. Files: SKILL.md (G80 + this entry); config/skill-config.yaml (8.56.0 โ 8.57.0); CHANGELOG.md; (db-gpt recipe + spec changes committed separately as 16d650b18f). Also folds in a G79 refinement (same version, no bump โ wuphf win CI): go-licenses fatals are PER-OS (the build graph varies by GOOS) โ a cross-platform bundle can fail on a dep that only one OS pulls in (win: github.com/mattn/go-localereader); for a local-only/cf-blocked recipe drop go-licenses (also clears the deprecated win posixโm2-base lint) rather than maintain a per-OS --ignore list โ SKILL.md G79; wuphf recipe committed as 4192de7810.
v8.56.0 (Jun 28, 2026): G79 โ go-licenses save fatals on a package's OWN non-OSI/non-standard license; pass --ignore <module-path> (MINOR โ 1 new gotcha). From a direct (non-BMAD) skill use packaging wuphf (nex-crm/wuphf v0.230.0) โ a pure-Go YC-S26 CLI under the n8n Sustainable Use License v1.0 (source-available, NON-OSI). The license is a hard conda-forge submission blocker (cf distributes only OSI-approved licenses โ same class as BUSL-1.1, the ragstack precedent), so the recipe was built local-only (no PR, per user choice). G79: the Go build compiled clean, then go-licenses save ./cmd/wuphf exited fatal with an unknown-license map containing only wuphf's own import paths (every third-party dep classified fine) โ go-licenses can't recognize the non-standard SUL. Fix: --ignore github.com/nex-crm/wuphf (skip the project's own packages, whose LICENSE ships separately via about.license_file; the dep licenses still bundle); do NOT mask with || true. Also surfaced: a pre-existing recipes/wuphf stub mislabeled the license as MIT (factually wrong) โ corrected to LicenseRef-SustainableUseLicense-1.0; the lone validate_recipe "Missing stdlib" was the documented go-nocgo false positive (CGO off, confirmed). Built GREEN on linux-64, all tests passed. Files: SKILL.md (G79 + this entry); config/skill-config.yaml (8.55.0 โ 8.56.0); CHANGELOG.md; recipes/wuphf/{recipe.yaml,build.sh}.
v8.55.0 (Jun 27, 2026): G78 โ cfe-submission-pr/status are stale hints; answer "is X on staged-recipes/cf?" from LIVE gh+channeldata (MINOR โ 1 new gotcha). Closeout of the langflow handoff submission-gap effort (submit every langflow-related local recipe to staged-recipes so the spec is self-contained). A cfe-metadata-driven gap scan was WRONG (~40 flagged "unsubmitted" but ~10 were already merged/open). G78: the local cfe-* fields lag reality (hand-maintained + stripped before push); for any submission/gap decision use LIVE signals โ channeldata for on-cf, gh pr list matched by branch OR title OR changed-files (older PRs use bare <name> branches + Create recipe.yaml for <name> titles, not add-recipe-*/Add โฆ; multi-output PRs cover several names). Sibling of G66/G74 (records decay โ verify live). Live result: the langflow gap was 5 leaves (lfx-arxiv/-docling/-duckduckgo/-ibm + langchain-elasticsearch) โ submitted as draft PRs #33977โ#33981 (paced); the spec got a ยง Handoff inventory (langflow gapโPRs; 14 unrelated other-effort recipes documented-not-submitted; ragstack BUSL excluded). Criterion recorded: langflow-related = membership in langflow-suite's dep closure. Files: SKILL.md (G78 + this entry); config/skill-config.yaml (8.54.3 โ 8.55.0); CHANGELOG.md; docs/specs/langflow-conda-forge.md; 5 recipes/<lfx-*|langchain-elasticsearch>/recipe.yaml (cfe-submission-pr re-stamp).
v8.54.3 (Jun 27, 2026): langflow-suite winโGREEN closeout retro (PATCH). Effort complete: PR #33972 (langflow-suite, 4 outputs) is FULLY GREEN on every leg (linux + osx + Windows + both linters). Added a G77 process note โ win failures on a deeply unix-oriented closure surface sequentially (one per CI round: frontend G73 โ gunicorn G76 โ magika G77 โ green), and local linux builds can't validate the win/osx test phases (solve + pip check run only in CI), so budget for iterative CI rounds + fix the narrowest lever each round. Verified G73โG77 + the v8.54.x corrections all captured + held. Also a spec-hygiene catch: a linter silently wiped docs/specs/langflow-conda-forge.md's YAML frontmatter to a stray br during the b5ccdc36a5 spec edit and it was committed unnoticed โ restored from git (10c07a1db9) + brought the whole spec current (CURRENT STATE / Status / appendix โ SUBMITTED+GREEN). Cross-skill lesson saved to auto-memory ([[feedback_verify_frontmatter_after_edit]]): re-verify YAML frontmatter after editing frontmatter files; bmad-drift-check --specs catches spec-frontmatter corruption. Files: SKILL.md (G77 process note + this entry); config/skill-config.yaml (8.54.2 โ 8.54.3); CHANGELOG.md; docs/specs/langflow-conda-forge.md (frontmatter restore + accuracy pass).
v8.54.2 (Jun 27, 2026): G77 refinement โ exclude the offending VERSION, not the platform; langflow-suite win now GREEN (PATCH). The win blocker (transitive magika 0.6.3 โ onnxruntime<=1.20.1; win32 wheel cap) was fixed, not deferred: magika 0.6.3 is a one-version aberration (cf has 0.6.1/0.6.2/0.6.3/1.0.x), so excluding only it in lfx run_constraints (magika >=0.6.1,!=0.6.3,<0.7.0) makes the solver pick 0.6.2 (no win cap) โ PR #33972 passes ALL legs (linux + osx + win + both linters, build 1545234). Added G77 remedy 0 (best-when-applicable): a version-specific != exclusion beats excluding win or capping broadly โ it works in the staged-recipes PR immediately, keeps win green, is metadata-only. win_64 re-added to the conda-forge.yml noarch_platforms. Updated G77 case study (RESOLVED) + the langflow spec ยง Windows build (RESOLVED). Files: SKILL.md (G77 remedy 0 + case study + this entry); config/skill-config.yaml (8.54.1 โ 8.54.2); CHANGELOG.md; docs/specs/langflow-conda-forge.md; recipes/langflow-suite/{recipe.yaml,conda-forge.yml}.
v8.54.1 (Jun 27, 2026): G77 correction โ noarch_platforms is a FEEDSTOCK (post-merge) lever, NOT a staged-recipes-PR matrix lever; store_build_artifacts works in the PR (PATCH). Verified on PR #33972 (build de2f9814): with the conda-forge.yml in place, the staged-recipes PR still ran a win_64 leg + no osx_arm64 leg โ staged-recipes uses 3 fixed Azure jobs (linux/osx/win) and builds every recipe on each; noarch_platforms only configures the FEEDSTOCK matrix post-merge (especially here where noarch is per-output, so the matrix generator doesn't treat the recipe as noarch). So noarch_platforms does NOT make a failing win leg green in the PR โ that needs the upstream-feedstock fix or build: skip: true # [win]. However workflow_settings.store_build_artifacts does work in the staged-recipes PR (published conda_pkgs_linux/conda_pkgs_noarch/conda_pkgs_osx even with win red); the conda_pkgs_noarch artifact is the universal package (installs on macos-arm64 + everywhere, no per-arch build), and a red win build leg doesn't block artifact publishing on the other legs. Corrected G77 remedy #2 + the langflow spec ยง Windows build accordingly. Files: SKILL.md (G77 remedy + this entry); config/skill-config.yaml (8.54.0 โ 8.54.1); CHANGELOG.md; docs/specs/langflow-conda-forge.md.
v8.54.0 (Jun 27, 2026): G77 โ a TRANSITIVE dep's dropped win-only wheel marker breaks pip check on win; exclude win via conda-forge.yml or fix the upstream feedstock (MINOR โ 1 new gotcha). Third win-only failure on langflow-suite PR #33972 (after G73 bash-build + G76 gunicorn): lfx โ markitdown โ magika 0.6.3 whose wheel caps onnxruntime<=1.20.1; sys_platform=="win32", but the conda magika-feedstock didn't encode the cap (conda deps can't carry markers) โ solver installed onnxruntime 1.26.0 โ pip check fails on win. G77: a transitive conda-vs-wheel win-marker gap is an upstream-feedstock bug you can't fix in your (noarch) recipe โ remedies are (1) PR the offending feedstock to add the # [win] cap (root, external), or (2) for a unix-oriented package, exclude win via recipes/<name>/conda-forge.yml noarch_platforms: [linux_64, osx_64, osx_arm64] (the noarch artifact still installs on win) + workflow_settings.store_build_artifacts (current key; azure. is deprecated) to publish downloadable .conda; add osx_arm64 to validate Apple Silicon (one universal noarch artifact, not per-arch). Applied to langflow-suite: win excluded, linux+osx(arm64) green. Also drove the spec's new ยง Windows build (failure analysis + re-enablement task). Files: SKILL.md (G77 + this entry); config/skill-config.yaml (8.53.0 โ 8.54.0); CHANGELOG.md; recipes/langflow-suite/conda-forge.yml; docs/specs/langflow-conda-forge.md.
v8.53.0 (Jun 27, 2026): G76 โ a Unix-only dep in a noarch recipe must be OPTIONAL, not a hard dep (MINOR โ 1 new gotcha). Live-CI follow-on from PR #33972: gunicorn is Unix-only on conda-forge (linux-*/osx-* subdirs only โ no win-64, no noarch), but it was a hard run dep of the noarch langflow-base โ the Windows test env couldn't solve (nothing provides gunicorn). G76: a noarch: python package can't carry a platform-conditional hard dep โ noarch bakes depends at build time, so if: unix becomes unconditional and the artifact is unsolvable on win; a wheel-only marker fixes pip check but not the conda solve. Fix = make it optional: strip the dep from the wheel's [project.dependencies] (source patch) + move to conda run_constraints (advisory) โ installs on all platforms, pip check clean everywhere, dep still pinned-if-present on unix (same lean pattern as G25/G70). Also recorded: staged-recipes runs pip check regardless of the recipe's pip_check: setting (observed: pip check ran for outputs set pip_check: false) โ a metadata mismatch must be genuinely resolved, not silenced. Applied to langflow-suite (gunicorn โ run_constraints; verified on the built .conda: gunicorn in constrains, absent from depends + wheel main-deps; 5/5 pip checks pass). Files: SKILL.md (G76 + this entry); config/skill-config.yaml (8.52.1 โ 8.53.0); CHANGELOG.md; recipes/langflow-suite/recipe.yaml; patches/0001-strip-integration-deps.patch.
v8.52.1 (Jun 27, 2026): G73 cross-platform correction โ a noarch frontend node-build MUST handle the Windows leg (PATCH). Live-CI finding: langflow-suite PR #33972's win leg failed while linux/osx passed โ the node-build script I shipped in v8.51.1 was unix/bash-only (set -ex/pushd/rm -rf/cp -r/bare npm), but staged-recipes builds a noarch recipe on all three platforms (linux/osx/win) for validation and runs package_contents on each (only after feedstock creation does conda-smithy build noarch on linux alone). On the Windows leg the script runs through cmd.exe: bash-isms fail and a bare npm/.cmd shim without call terminates the script โ frontend never builds โ the G73 package_contents guard correctly catches it win-only. Corrected G73's Fix with a cross-platform pattern: if: win / then with call npm --prefix โฆ && if errorlevel 1 exit 1, copy + pip install in cross-platform Python (shutil, not rm -rf/cp -r). Cross-refs G56 + the build.bat call-the-.cmd-shim rule. The unix-only build passed local linux + the v8.51.1 verification โ live CI caught what the local build could not (verify, don't assume). Fix applied to recipes/langflow-suite/recipe.yaml (verified: linux rebuild still ships 1,873 frontend files) + the langflow-suite-clean submission branch. Files: SKILL.md (G73 cross-platform note + this entry); config/skill-config.yaml (8.52.0 โ 8.52.1); CHANGELOG.md; recipes/langflow-suite/recipe.yaml.
v8.52.0 (Jun 27, 2026): langflow-suite submission-prep retro (MINOR โ 2 new gotchas G74/G75). Closeout retro of the langflow-suite-clean lean-submission-branch effort (frontend node-build verified + cleaned recipe pushed to rxm7706/staged-recipes, no PR per directive). G74 โ atlas membership can be STALE for recently-created feedstocks: a cf_atlas.db "absent" can be wrong (feedstock created since the last atlas refresh โ incl. your own freshly-merged PRs); when a membership check drives a destructive edit (dropping a run_constraint, re-packaging an existing prereq), cross-check LIVE conda-forge channeldata.json (one fetch, ~33k packages, feedstock-agnostic โ catches multi-output-provided packages too). Live: apify-client was atlas-absent but on-channel (feedstock 2026-06-26) โ would have been wrongly dropped. The membership analogue of G66. G75 โ a lean staged-recipes SUBMISSION copy may go beyond the default cfe-strip: beyond G60/G62 (surgical cfe-only strip, keep schema header), a user may direct (1) remove ALL comments incl. the schema-header comment + dividers + inline (process line-by-line section-aware, never a YAML round-trip), (2) filter optional run_constraints to on-cf-only (don't ship constraints pointing at unsubmitted packages โ re-verify LIVE per G74; collapse a now-empty run_constraints: key); keep the full set in the LOCAL recipe. Branch-only flow: base on conda-forge/staged-recipes main (minimal PR diff), push to the personal fork, no PR when told to hold. Live: langflow-suite-clean 519โ313 lines, 40/85 run_constraints dropped, 0 cfe/comment leakage verified on the pushed artifact. Also confirmed the G73 fix end-to-end (langflow-base .conda 1.9MBโ14MB, 1,873 frontend files). Files: SKILL.md (G73 verified-note + G74/G75 + this entry); config/skill-config.yaml (8.51.1 โ 8.52.0); CHANGELOG.md.
v8.51.1 (Jun 27, 2026): G73 correction โ node-build is the cf idiom for a wheel-only frontend; build-time network IS available (PATCH). Deep research into existing conda-forge feedstocks corrected two wrong claims in the v8.51.0 G73: (1) the fix is to build the frontend from source at build time (nodejs build dep + npm ci && npm run build of src/frontend, copy into the package tree before pip install) โ NOT the wheel-graft, which has no cf precedent; (2) conda-forge BUILD jobs have network access โ only the test phase is offline, so npm ci/pnpm i fetching node_modules at build is fine and routine (the earlier "build-time fetch is disallowed" framing in G73/G45 was wrong). Precedents documented: gradio (exact no-sdist analogue โ scripts/build_frontend.sh), chainlit (pnpm via hatchling hook), mlflow (yarn build of server/js), agentsview/nebi. Also captured the wheel-in-source lint mechanics (R-028 compiled / R-029 pure-non-noarch = blocking lints; R-030 pure+noarch = non-blocking hint; none skippable). Applied: langflow-suite langflow-base output now node-builds langflow/frontend/ at build time, guarded by a package_contents test (index.html + assets/*.js). Heads-up recorded: competing staged-recipes PRs #33853 langflow-base (draft, no frontend) + #33875 langflow-sdk (open) by another submitter. Files: SKILL.md (G73 Why/Fix/case-study + this entry); config/skill-config.yaml (8.51.0 โ 8.51.1); CHANGELOG.md; recipes/langflow-suite/recipe.yaml; .claude/scripts/conda-forge-expert/native-build.sh (extra-arg passthrough); docs/specs/langflow-conda-forge.md.
v8.51.0 (Jun 27, 2026): adversarial-spec-review + langflow-suite-fold retro (MINOR โ 3 new gotchas G71/G72/G73). From the langflow-sdk fold + cassio win-fix + a 3-lens adversarial review of docs/specs/langflow-conda-forge.md. G71: a dependency whose event-loop reactor needs libev (Unix-only) or asyncore (removed py3.12) fails import <pkg>.cluster on win+py3.12+, breaking a noarch consumer's CFEP-25 * leg win-only; interim = platform-conditional CFEP-25 test (lint-clean โ a tests[].if selector on noarch does NOT trip the noarch-selector lint, unlike a run-dep selector G12/G35); durable = patch the dep's reactor chain to append an asyncio fallback (cassio #33946 + cassandra-driver-feedstock patch). G72: fold a same-monorepo sibling into a multi-output suite (own per-output version + consumer pin_subpackage(exact=True)) to dissolve a cross-feedstock submission gate โ caveats: inherits the suite python_min floor, close the sibling's standalone PR, re-verify clean-channel (G68) (langflow-sdk โ langflow-suite 4th output, #33856 closed). G73: a noarch app built from a GitHub monorepo TAG ships none of the release-time-generated frontend assets (wheel-only) โ a G51 trap that import/pip_check CANNOT catch because the UI isn't imported; detect by inspecting the BUILT .conda for index.html/.js/.css (langflow-suite shipped 0 frontend files vs the wheel's ~1,874 โ the langflow-suite submission blocker; fix = node-build the frontend from the tag's src/frontend at build time โ corrected in v8.51.1, see below). The review also drove a major consolidation of the langflow spec (single canonical CURRENT STATE block; ~25 accreted contradictions reconciled). Files: SKILL.md (G71โG73 + this entry); config/skill-config.yaml (8.50.0 โ 8.51.0); CHANGELOG.md; docs/specs/langflow-conda-forge.md.
v8.50.0 (Jun 27, 2026): run_constraints-reconciliation retro (MINOR โ 1 new gotcha G70). Closeout of the langchain + langflow-suite run_constraints audit/reconciliation. G70: reconcile a recipe's OWN run_constraints to upstream's real extra pins (not >=0.1 placeholders) โ it's soft/metadata-only (not installed in the test env), so re-pinning can't change the build/test/pip_check and lint/render is sufficient verification; mechanics = source from upstream base+umbrella optional-dependencies, PEP 508โconda (~= expand, strip markers/extras), detect phantoms (remove names absent from all upstream extras), map cf renames (G10). Distinct from G67 (external feedstock constrains blocking you) โ two separate audit passes. Live: langflow-suite's langflow output had 75/78 >=0.1 placeholders โ reconciled all 78 to langflow 1.10.1 extra pins, 0 phantoms, green + lint-clean. (The audit also confirmed the v8.49.2 lesson โ groq <1 is NOT stale despite cf groq 1.5.0, because upstream langchain-groq pins <1.) Files: SKILL.md (G70 + this entry); config/skill-config.yaml (8.49.2 โ 8.50.0); CHANGELOG.md; docs/specs/langflow-conda-forge.md.
v8.49.2 (Jun 27, 2026): G67 staleness-detection correction (PATCH). The audit heuristic "cf has a version above the cap โ stale" is a false-positive generator for run_constrained. Optional-integration constraints intentionally cap to the provider's supported range, which lags cf-latest. Counter-example: langchain's groq >=0.4.1,<1 is NOT stale despite cf groq 1.5.0 โ upstream langchain-groq 1.3.11 pins groq>=0.30.0,<1.0.0 (no groq 1.x support) and the feedstock mirrors it, so <1 is protective. Staleness = diverges from the CITED SOURCE's current pin (aiosqlite was stale because langchain's own extended_testing_deps.txt moved <0.20โ<0.23; groq is not). Corrected G67's detection guidance. Files: SKILL.md (G67 + this entry); config/skill-config.yaml (8.49.1 โ 8.49.2); CHANGELOG.md.
v8.49.1 (Jun 27, 2026): G67 fix-value correction (PATCH). G67's prescribed fix was a guessed "loosen aiosqlite to <1.0" โ wrong discipline. When a feedstock's run_constrained is annotated as copied from a pinned upstream file (langchain cites extended_testing_deps.txt at a tag), the fix is to re-sync to the CURRENT version's file (bump the tag, copy verbatim), not arbitrarily widen. langchain 1.3.11's file pins aiosqlite >=0.19.0,<0.23 (the 1.2.0 file said <0.20) โ so the durable value is <0.23, not <1.0. Corrected G67 Fix + case study, and the spec. (User fixed recipes/langchain/ to <0.23 in commit d1c6b20c7a.) Files: SKILL.md (G67 + this entry); config/skill-config.yaml (8.49.0 โ 8.49.1); CHANGELOG.md; docs/specs/langflow-conda-forge.md.
v8.49.0 (Jun 27, 2026): langflow-suite 1.10.1 bump retro (MINOR โ 3 new gotchas). From bumping the 3-output langflow-suite to 1.10.1 + langchain~=1.3.0 (all outputs built+tested GREEN locally). G67: an external feedstock's stale run_constrained (in constrains, NOT depends) can block a consumer; a narrow "does X co-install with Y" dry-run misses it because the constraint only fires when the real consumer's full hard-dep set drags the constrained package in โ verify against the actual consumer solve, and when conflicting, inspect the blocker's constrains (cf langchain's stale aiosqlite >=0.19.0,<0.20 vs langflow-base's aiosqlite>=0.20; the constrains-axis sibling of the text-splitters depends skew). G68: after a skew resolves, PURGE the obsolete local workaround build โ under strict channel priority a stale higher-priority build_artifacts/ artifact (e.g. langchain 1.2.18) shadows the real cf package and causes false failures; delete + conda_index re-index before re-testing. G69: a suite version bump can CASCADE to a sibling recipe bump via a tightened cross-dep (lfx 1.10.1 โ langflow-sdk>=0.2.1); bump+rebuild the sibling locally, update the pin, and flag the submitted PR (#33856) to bump too. Files: SKILL.md (G67โG69 + this entry); config/skill-config.yaml (8.48.0 โ 8.49.0); CHANGELOG.md. Also: recipes/langflow-suite + recipes/langflow-sdk bumped (committed a0045a9f46); docs/specs/langflow-conda-forge.md updated (new aiosqlite skew; Wave A not fully clear).
v8.48.0 (Jun 27, 2026): ยง Bโฒ-consumer batch retro (MINOR โ 1 new gotcha + G40 refinement). Closeout of the 6-PR langflow ยง Bโฒ-consumer batch (qianfan #33935, opik #33937, trustcall #33938, langchain-graph-retriever #33939, langchain-google-vertexai #33940, langchain-sambanova #33941 โ all green first try + pinged). G66 (new): a MERGED staged-recipes PR is not immediately installable โ the feedstock must build + upload (minutes-to-hours), so before submitting a consumer whose blocker just merged, verify the prereq is live on the cf channel at a satisfying version (fresh repodata or a build that solves against conda-forge, not the local mirror which masks the gap). Verification depth scales with dep shape: pure-python โ confirm-live + build; compiled โ ALSO the G40 check. Includes the cached-snapshot trap (a reused repodata file reported pydantic 2.9.2 as "latest" when cf had 2.13.4 โ floor checks need fresh data). G40 refinement: run the per-platform compiled-dep floor check proactively before submit (all prior case studies were reactive); added the concrete depends-parse method (collect each candidate build's python >=3.X, filter to the pinned range, confirm the floor on every subdir) โ langchain-google-vertexai's pyarrow/bottleneck/numexpr all covered py3.10 โ no bump, green first try. Files: SKILL.md (G66 + G40 refinement + this entry); config/skill-config.yaml (8.47.1 โ 8.48.0); CHANGELOG.md.
v8.47.1 (Jun 26, 2026): G63 correction (PATCH) โ tick EVERY checklist box. v8.47.0's G63 carved out the knowledge-base line ("tick every - [ ] exceptโฆ") โ wrong. The "check the knowledge base before pinging" item is a submitter-affirmed box like any other; an unticked box reads as an incomplete checklist. G63 now sed -i 's/- \[ \]/- [x]/g' the whole template, no exceptions. Live fix: opik #33937 opened with that box unchecked under the old carve-out โ corrected on the PR (0 unchecked). Files: SKILL.md (G63 + this entry); config/skill-config.yaml (8.47.0 โ 8.47.1); CHANGELOG.md.
v8.47.0 (Jun 26, 2026): linter-parity gotcha (G65) โ MINOR. From the "does the local linter match the conda-forge CI?" audit. G65: (1) the pixi env pins conda-smithy >=3.44.6,<4 (=3.62.0) and can't take the CalVer 2026.x line โ it hard-depends on the conda pkg, absent from this rattler/conda-build env (pixi update conda-smithy fails) โ so for CI-parity lint run the current conda-smithy ephemerally: pixi exec --spec "conda-smithy>=2026.6.14" conda-smithy recipe-lint --conda-forge recipes/<name> (no env change; matches the webservice exactly); never dismiss its lints (cf the G12 escape-hatch misjudgment on opik). (2) the staged-recipes GHA linter.py runs conda-smithy recipe-lint plus checks validate_recipe doesn't โ files-outside-recipes/, feedstock/conda-name/bioconda-exists โ so run lookup_feedstock (G58) + placement/name checks before submitting. (3) local recipe-build is host-only โ per-platform gaps (opik's osx fastuuid py3.10, G40) need the per-subdir repodata check. Added a CI-parity-lint item to the Pre-PR checklist; pixi.toml comment records the pixi exec command (env pin kept <4 deliberately). Files: SKILL.md (G65 + checklist + this entry); pixi.toml (comment); config/skill-config.yaml (8.46.2 โ 8.47.0); CHANGELOG.md.
v8.46.2 (Jun 26, 2026): G12 refinement (PATCH) โ escape hatch caveat + why-local-misses. From opik #33936: the noarch_platforms escape hatch does not reliably silence the conda-forge-linter webservice (worked for xorq's if: linux, NOT opik's if: not (linux and aarch64)), so don't dismiss validate_recipe's "noarch can't have selectors" as a guaranteed false-positive. Prefer eliminating the selector via G35 when conda-forge ships the dep on all subdirs (verify per-subdir repodata; mind _-vs--, G10) โ unconditional deps + no conda-forge.yml โ green on both linters, installable everywhere. Also documents why local gates "miss" CI failures: (1) validate_recipe does catch lints โ opik's miss was a judgment error (overriding a real lint); (2) local builds are host-platform-only, so per-platform issues (osx-64 fastuuid py3.10 gap, G40/G61) can't surface โ run the G40 per-subdir check for compiled transitive deps before submitting. Files: SKILL.md (G12 refinement + this entry); config/skill-config.yaml (8.46.1 โ 8.46.2); CHANGELOG.md.
v8.46.1 (Jun 26, 2026): G64 refinement (PATCH). Added a third precondition to the ready-for-review ping: the PR must not be a draft (a draft isn't ready for review โ pinging is premature). G64's gate is now ping-only-if all checks green AND isDraft == false AND review-requested absent (gh pr view <pr> --repo conda-forge/staged-recipes --json isDraft,labels). Updated the Pre-PR Submission checklist item. Files: SKILL.md (G64 + checklist + this entry); config/skill-config.yaml (8.46.0 โ 8.46.1); CHANGELOG.md.
v8.46.0 (Jun 26, 2026): review-request retro (1 new gotcha + G62 reinforcement). MINOR bump โ from the qianfan #33935 review-request step. G64 (after CI is all-green, request review with one language-matched ping โ @conda-forge/help-python for pure-Python noarch, help-c-cpp/help-rust/โฆ per language โ and check the PR's labels first: the ping makes a bot add review-requested + the language label, so if those labels exist a prior ping already landed โ skip; labels are the authoritative dedup signal, not comment text; qianfan #33935 was pinged twice, duplicate deleted). G62 reinforcement โ the strip is local-retains / strip-on-push: the cfe-* block lives in the LOCAL recipe permanently and is removed only on a pushed COPY; never delete cfe-* from the local recipe (lose the cached CFE/admin state) โ on submission, update the local cfe-on-conda-forge-status + add cfe-submission-pr:, don't remove the block (qianfan's local cfe-removal was an error, restored). Added a post-green review-request item to the Pre-PR Submission checklist. Files touched: SKILL.md (G64 + G62 reinforcement + checklist item + this entry); config/skill-config.yaml (8.45.0 โ 8.46.0); CHANGELOG.md (this entry).
v8.45.0 (Jun 26, 2026): submission-mechanics retro (2 new gotchas). MINOR bump โ from the qianfan #33935 test submission. G62 (NEVER ship cfe-* metadata โ the strip is a manual, silent-if-skipped step since rattler-build + conda-smithy ignore unknown extra: keys, so verify it on the PUSHED artifact: gh api โฆcontents/recipe.yaml?ref=<branch> | base64 -d | grep -nE 'cfe-\|# CFE' before opening the PR; the under-strip complement of G60). G63 (open staged-recipes PRs with the conda-forge template + completed checklist โ gh pr create --body/--body-file REPLACES .github/pull_request_template.md rather than merging; fetch the live template [lowercase path] + tick the boxes). Both added as Pre-PR Quality-Gate Submission checklist gates. A full audit of all open + merged langflow-closure PRs confirmed zero cfe-metadata leaks (the strip had held, but unverified). Files touched: SKILL.md (G62/G63 + 2 checklist gates + this entry); config/skill-config.yaml (8.44.0 โ 8.45.0); CHANGELOG.md (this entry).
v8.44.0 (Jun 26, 2026): PR-debugging + sedโpatch-sweep retro. MINOR bump โ 3 new gotchas + 1 refinement from a session that debugged 4 staged-recipes PRs to green and ran a repo-wide in-build-sedโsource-patch conversion. G59 (prefer a SOURCE PATCH over an in-build sed for editing upstream source: bare sed -i 'script' file fails on macOS/BSD sed โ noarch builds on every platform โ and reviewers prefer patches; move the edit to source.patches [top-level for multi-output]; exception = a ${{ version }}-interpolating edit needs portable sed -i.bak โฆ && rm -f *.bak; verify via the built wheel's METADATA + noarch/-vs-broken/; graph-retriever #33913 + pksuid #33895). G60 (the strip-before-push must remove ONLY extra.cfe-* + # CFE โฆ blocks โ never the schema header or context:; dropping context: โ ${{ version }} undefined โ render-fail on all platforms; langchain-litellm #33917). G61 (GitHub archive/<commit>.tar.gz sha256 drifts when GitHub re-gzips โ source-fetch checksum validation failed; recompute + prefer release/sdist tarballs; suite-st-transfers). G40 refinement (per-platform variant: a noarch consumer's real python_min can be forced by a COMPILED transitive dep that ships a Python version on some conda subdirs but not others โ fastuuid 0.14.0 has py3.10 on linux/win but only py3.11+ on osx-64 โ langchain-litellm #33917 floor is 3.11; diagnose via per-subdir repodata). The sedโpatch sweep (graph-retriever, pksuid, gibr, dataprofiler, agent-lifecycle-toolkit, db-gpt, milvus-lite, opendsstar, jigsawstack, langflow-base/langflow/langflow-suite, suite-*) all rebuilt+tested GREEN; repo now has zero bare sed -i. Files touched: SKILL.md (G59/G60/G61 + G40 refinement + this entry); config/skill-config.yaml (8.43.0 โ 8.44.0); CHANGELOG.md (this entry).
v8.43.0 (Jun 25, 2026): langflow-closure submission-session retro. MINOR bump โ 3 new gotchas + 2 refinements from a long multi-PR staged-recipes session (ibm-cos-suite + ~19 langflow-closure PRs + an impit platform-expansion). G56 (multi-output noarch:python build scripts run on win-64 โ bash cd "$SRC_DIR/<sub>"+"$PYTHON" fails on cmd.exe; use ${{ PYTHON }} -m pip install ./<sub>; crewai-suite has the same latent bug; ibm-cos-suite #33886). G57 (a build-control env var set only via bash export is UNSET on win-64 โ build silently falls back to a network fetch / CMake FetchContent 404; set cross-platform after reading the upstream build code; couchbase #33893). G58 (lookup_feedstock BEFORE submitting โ a package already on conda-forge is rejected by the GHA linter.py "feedstock exists" check even while the linting-service bot says "excellent"; trust the GHA linter; impit #33897). G19 refinement (generalized beyond Rust to any win-64 cp1252 file read โ pure-setuptools setup.py open('README.md') on a UTF-8 README; lomond #33889). G26 refinement (poetry-core builds the wheel METADATA from the sdist's PKG-INFO, not pyproject.toml โ pyproject-only loosen sed passes locally, fails CI; patch all three files; pksuid #33895). Also: the adversarial BFS closure-audit workflow (prove a dep-closure spec has zero unpackaged gaps + name every blocker / list every net-new prereq โ the langflow spec was missing 9), and the canonical compiled-Rust platform-expansion conda-forge.yml (cross-linux_aarch64, native osx_arm64, test: native_and_emulated; impit aarch64 cross-build verified GREEN locally). Files touched: SKILL.md (G56/G57/G58 + G19/G26 refinements + this entry); config/skill-config.yaml (8.42.2 โ 8.43.0); CHANGELOG.md (this entry).
v8.42.2 (Jun 24, 2026): wheelโsource sweep retro โ G54/G55 refinements. PATCH bump โ no new gotcha; refines the v8.42.0 source-preference rules from a grep -rl 'cfe-source-kind:\s*pypi-wheel' recipes/ sweep. (1) G54 โ added a retroactive-sweep note (recipes generated before v8.42.0 may sit on a wheel when a usable sdist / GitHub source exists; audit + re-decide per recipe) and corrected the case study: the ibm-watsonx-orchestrate-{core,clients} "keep the wheel" call was overturned โ their sdists are metadata-only, but the ADK monorepo ships source at packages/{core,clients} โ migrated to GitHub source (GREEN). Lesson: empty sdist โ no source. Plus a monorepo-subdir wrinkle: build the subdir from the FULL extraction, because its dynamic version may read a file OUTSIDE it via a relative path ([tool.hatch.version] path = "../../src/<pkg>/__init__.py") โ isolating the subdir โ G39 0.0.0. (2) G55 โ added the setup_requires / build-time network-fetch trap: a source build runs setup.py's legacy setup_requires (or any net fetch) that a LOCAL build masks but conda-forge's OFFLINE CI fails on; strip it (jigsawstack pytest-runner) or host it. Corollary: a clean local source build โ proof of offline-CI build. (3) cfe-source-kind โ keep it in sync with the source: block; a stale pypi-wheel (live: pybase62, already on a GitHub tag) mis-routes the next regen and hides the recipe from the sweep. Sweep migrations: jigsawstack, ibm-watsonx-orchestrate-{core,clients}, lfx (โ monorepo src/lfx); pybase62 metadata-corrected. Files touched: SKILL.md (G54 + G55 + cfe-source-kind refinements + this entry); config/skill-config.yaml (8.42.1 โ 8.42.2); CHANGELOG.md (this entry).
v8.42.1 (Jun 23, 2026): langflow-closure retro โ convention clarification. PATCH bump โ no new gotcha; verified v8.42.0's G54/G55 source-preference + build-backend rules held across the closure. Clarified the extra.cfe-* convention: recording the first local build on a recipe that has no cfe-* block (older pre-convention recipes โ firecrawl-py, mem0ai โ carried only recipe-maintainers) means adding the full canonical block, not a cfe-local-build-*-only six-field stub (the failure mode caught in local-recipes PR #24's first draft). Re-grounded docs/specs/langflow-conda-forge.md to current state (PRs #23โ#27 merged โ spec's "PR #25 (open)" + "only firecrawl-py lacks a build record" corrected; firecrawl-py/mem0ai now built; skill ref v8.35.0/G43 โ v8.42.0/G55). The session's staged-recipes-fork CI findings are project-level (โ auto-memory, not the skill): the staged-recipes linter's hard-coded {owner}/staged-recipes repo-target 404s on a fork (use github.repository); the ungated environment.yamlโpixi.toml sync check reds every PR until resynced (pixi project export conda-environment -e build); the maintenance label suppresses the linter's "outside recipes/" lint for non-recipe PRs. Files touched: SKILL.md (cfe-block first-build clarification + this entry); config/skill-config.yaml (8.42.0 โ 8.42.1); CHANGELOG.md (this entry).
v8.42.0 (Jun 23, 2026): Source-selection + build-backend rules. MINOR bump โ two new gotchas, no change to existing paths. Driven by the langflow closure: ~13 recipes sourced from PyPI wheels (the generator's fallback when no PyPI sdist) without ever checking GitHub, and a wheelโsdist switch then hit "No valid build backend found โฆ using pip." (1) G54 โ source preference sdist > GitHub-source > wheel, with a usable-source check. conda-forge prefers source over wheel; the generator never checks GitHub for wheel-only PyPI packages, and a naive sdist switch can ship ModuleNotFoundError/0.0.0. Decision order: usable PyPI sdist (verify it ships .py files โ many are metadata-only/broken: ibm-watsonx-orchestrate-* sdists have 0 .py; jigsawstack omits a requirements.txt its build reads) โ GitHub tag archive (incl. monorepo subdir via a separate monorepo_tag context var + pip install ./src/<sub>; autotick can't dual-bump) โ wheel as last resort (documented). (2) G55 โ source builds need a build backend in host. Wheel installs need none (so wheel recipes carry host: [python, pip]); a source build runs PEP 517 and requires the [build-system] backend in host โ a wheelโsource switch surfaces it. Backend map (setuptools / hatchling / flit-core / poetry-core / pdm-backend / scikit-build-core / maturin / uv-build). Enhanced the "PyPI source.url" wheel note to point at G54. Applied live: langflow-sdk switched wheelโGitHub-monorepo source (src/sdk, hatchling) โ GREEN; ibm-watsonx-orchestrate-{core,clients} reverted to wheel (metadata-only sdists). Updated config/skill-config.yaml to 8.42.0.
v8.11.1 (Jun 2, 2026): Legacy-path cleanup + edit_recipe line-fold fix. PATCH bump โ internal cleanup; no behavior change for the default path. Two follow-ups from v8.11.0's bmalph + yo CI runs. (1) recipe_editor.py line-fold fix โ ruamel.yaml's default width=80 was folding long YAML strings (e.g. pnpm-licenses generate-disclaimer --prod --output-file=third-party-licenses.txt) into 2-line continuations during the load-modify-dump cycle of edit_recipe. The fold is semantically a no-op (YAML's implicit line-folding parses both forms to the same string) but cosmetically reviewers ask "why is this wrapped?". Set yaml.width = 4096 so single-line items stay single-line. Caught when the IDE selection of recipes/yo/recipe.yaml showed the fold after edit_recipe was used to swap the MAINTAINER placeholder. (2) Removed the legacy --no-inline-build escape hatch + its dead code. The legacy path (noarch:generic + standalone build.sh + tee Windows shim) was kept "for back-compat" in v8.11.0 but it's BROKEN on current staged-recipes CI โ that's why bmalph and yo PRs failed before the v8.11.0 refactor. Keeping a known-broken path adds maintenance burden with no real users. Dropped: _build_sh_template function (legacy build.sh emitter), _template_npm_tarball_filename helper (only consumer was the above), inline_build + with_build_bat + no_bin_links kwargs from generate_npm_recipe_yaml, --inline-build / --no-inline-build / --with-build-bat / --no-bin-links CLI flags, the elif not inline_build: branch in the conda-forge.yml emitter, the if args.inline_build extras-note line. Tests test_npm_legacy_inline_build_false_emits_build_sh_and_cfy + test_npm_with_build_bat_opt_in removed (the paths they exercise no longer exist). test_npm_url_and_filename_are_version_templated renamed to test_npm_source_url_is_version_templated (the filename helper test went with the helper). test_npm_inline_build renamed to test_npm_inline_build_default and simplified (no flag needed; inline IS the default). test_npm_no_third_party_licenses_inline_build renamed similarly. 44/44 tests pass (was 46; net -2 for the two deleted legacy tests). Generator output unchanged: regen of recipes/bmalph matches live byte-for-byte (modulo the MAINTAINER placeholder). Updated config/skill-config.yaml to 8.11.1.
v8.11.0 (Jun 2, 2026): npm generator switches default to per-platform inline build (openspec PR #32368 + bmalph PR #33557 pattern). MINOR bump โ behavior change for all new npm recipes; legacy noarch:generic + standalone build.sh + tee Windows shim available via --no-inline-build. Driven by the bmalph PR #33557 CI failures that exposed two fundamental flaws in the v6+ "noarch:generic" canonical pattern: (1) rattler-build's symlink-portability check rejects the bin/<cmd> symlink that npm install --global creates on Unix because noarch:generic claims Windows compatibility where symlinks are non-portable; (2) staged-recipes CI builds noarch:generic on every platform, and without a build.bat the Windows leg runs nothing โ no LICENSE staged โ "No license files were copied" failure. The yo PR #33358 hit the same root cause (its __unix/__win virtual-package selectors are a different symptom of the same per-platform-vs-noarch tension). The merged openspec recipe (#32368) sidesteps both by dropping noarch:generic and using build.script: with if: unix / then / else branches โ each platform's native npm install --global produces the right shape (Unix: symlinks-to-.js; Windows: native .cmd shims). Bmalph PR #33557's second attempt confirmed the pattern works cleanly on linux_64 + osx_64 + win_64 simultaneously (3m17s/6m14s/3m20s green CI). Six concrete changes in scripts/recipe-generator.py: (1) _inline_build_script rewritten to emit per-platform branches: set -ex + pnpm install --ignore-scripts + npm pack --ignore-scripts + npm install --global "${SRC_DIR}/<filename>" + pnpm-licenses generate-disclaimer on the Unix side; call <cmd> + if %ERRORLEVEL% neq 0 exit 1 after every step on the Windows side. Tarball filename is jinja-templated (${{ version }}) โ rattler-build pre-renders before the shell runs. (2) generate_npm_recipe_yaml default inline_build=True (was False); drops noarch: generic for ALL npm recipes when inline (native packages were already per-platform; pure-JS now matches). (3) requirements.host: nodejs now emitted for inline-build recipes (matches openspec; needed for the test-env solver). (4) No standalone build.sh / build.bat / per-recipe conda-forge.yml in the default output โ staged-recipes' defaults cover everything and noarch_platforms restrictions are irrelevant when the recipe isn't noarch. (5) --inline-build CLI flag rewired to argparse.BooleanOptionalAction with default=True โ --no-inline-build opts back into the pre-v8.11.0 standalone layout (noarch:generic + build.sh + per-recipe conda-forge.yml with noarch_platforms: [linux_64] + shellcheck.enabled: true); --with-build-bat documented as only effective with --no-inline-build. (6) Feedstock-mode conda-forge.yml drops noarch_platforms and shellcheck.enabled โ feedstocks using the v8.11.0 pattern don't ship build.sh so shellcheck is a no-op and per-platform builds don't restrict to linux_64. Test updates: test_npm_recipe_canonical_shape_offline rewritten to assert the inline script: block + if: unix / then: / else: markers + per-platform commands + negative assertions (no build.sh, no conda-forge.yml, no noarch: generic); test_npm_pure_js_keeps_noarch_generic renamed to test_npm_pure_js_omits_noarch_and_compilers and reversed; test_npm_prepare_fix reads inline script from recipe.yaml instead of build.sh; test_npm_default_mode_omits_feedstock_only_fields renamed to test_npm_default_mode_omits_conda_forge_yml; new test_npm_legacy_inline_build_false_emits_build_sh_and_cfy asserts the back-compat path; live test_npm_live_against_husky + test_npm_live_scoped_codex updated for inline shape. 46/46 tests pass. Live verification: recipe-generator.py npm bmalph emits a recipe.yaml byte-for-byte identical to the CI-passing bmalph (modulo the MAINTAINER placeholder); no build.sh, no conda-forge.yml. scripts/_build_sh_template + _template_npm_tarball_filename kept for the legacy --no-inline-build path. Updated config/skill-config.yaml to 8.11.0. Follow-up: the yo PR #33358 will be regenerated with the new generator and force-pushed in the same session; expect the same green CI shape bmalph achieved.
v8.10.1 (Jun 2, 2026): npm generator emits ${{ version }} in source.url + ${PKG_VERSION} in build.sh + quoted ${PREFIX} in the tee command. PATCH bump โ bug fixes; no template additions. Driven by user-reported feedback on recipes/bmalph (npm@2.11.0). Three coupled bugs in scripts/recipe-generator.py's npm path: (1) fetch_npm_info line 1593 set source_url = tarball_url literally โ v["dist"]["tarball"] from the npm registry has the version baked in (.../bmalph-2.11.0.tgz). The autotick bot only mutates context.version and re-runs calculate_hash against the rendered URL; with a literal version in the URL, autotick would silently mint a recipe whose context.version advanced but whose source.url + sha256 still pointed at the previous tarball โ a real functional bug, not just a style issue. New helper _template_npm_source_url(url, version) rewrites the -<version>.tgz (or v<version>.tar.gz for --source github) suffix to ${{ version }}. Falls back to literal URL for unrecognized shapes. (2) _build_sh_template + _inline_build_script emitted ${{SRC_DIR}}/{info.tarball_filename} literally (e.g. ${SRC_DIR}/bmalph-2.11.0.tgz). New helper _template_npm_tarball_filename(filename, version) rewrites the version suffix to ${PKG_VERSION} (bash env var, injected by rattler-build into build scripts โ not jinja). Mirrors the existing yo and generator-code recipe pattern in this repo. (3) SC2086 shellcheck issue exposed by staged-recipes#33358 reviewer feedback: tee ${PREFIX}/bin/<name>.cmd << EOF needs ${PREFIX} quoted. Both _build_sh_template and _inline_build_script updated to emit tee "${PREFIX}"/bin/.... New test test_npm_url_and_filename_are_version_templated covers all three helpers + a fallback case for unrecognized URLs; existing test_npm_recipe_canonical_shape_offline updated to assert templated forms + the tee "${PREFIX}"/bin/ quoting + negative assertions (literal version must NOT appear in recipe or build.sh). 45/45 tests pass. Live verification: recipe-generator.py npm bmalph emits source.url: .../bmalph-${{ version }}.tgz, build.sh writes "${SRC_DIR}/bmalph-${PKG_VERSION}.tgz" + tee "${PREFIX}"/bin/bmalph.cmd. Updated recipes/bmalph/recipe.yaml + build.sh to match (and rebuilt clean). Updated config/skill-config.yaml to 8.10.1. PyPI / CRAN / CPAN / LuaRocks paths not audited in this patch โ left for a follow-up sweep.
v8.10.0 (May 26, 2026): Drop context.name from Python v1 recipes; switch package.name and source.url to fully-literal form. MINOR bump โ additive + corrective; matches what current grayskull emits and what conda-forge reviewers asked for on recipes/xorq-datafusion and recipes/py-yaml12. Three concrete changes in scripts/recipe-generator.py: (1) _build_source_url_template() rewritten to emit https://pypi.org/packages/source/<first-letter>/<distribution-name>/<sdist-stem>-${{ version }}.tar.gz โ only ${{ version }} interpolates; first letter, distribution name, and sdist filename stem are all literal. Drops the v8.9.1 ${{ name[0] }} / ${{ name }} / ${{ name | replace("-", "_") }} chain entirely. (2) generate_recipe_yaml() (noarch path) drops name: from context:, switches package.name: from ${{ name | lower }} to literal info.name.lower(). (3) _generate_maturin_recipe_yaml() does the same. (4) generate_meta_yaml() (v0 path) translates the v1 ${{ version }} syntax in info.source_url down to v0 {{ version }} so meta.yaml renders correctly under conda-build jinja (closes a latent bug that surfaced once info.source_url carries v1 syntax). Three template updates: (5) templates/python/noarch-recipe.yaml, (6) templates/python/compiled-recipe.yaml, (7) templates/python/maturin-recipe.yaml โ all drop the context.name line, switch package.name to a REPLACE_NAME literal placeholder, and rewrite the URL to literal segments with REPLACE_SDIST_STEM-${{ version }} for the filename stem (preserves the hyphen-vs-underscore distinction reviewers care about). SKILL.md updates: (8) "PyPI source.url Must Use..." critical-constraint section rewritten to show the literal pattern as canonical, including the why ("renames are rare, version bumps are common; the literal form is more legible and removes a class of jinja-typo bugs"). (9) "Recipe Formats Quick Reference" v1 example block updated to match. Live verification: recipe-generator.py pypi rich emits package.name: rich + source.url: .../r/rich/rich-${{ version }}.tar.gz + no context.name; recipe-generator.py pypi py-yaml12 emits package.name: py-yaml12 + .../p/py-yaml12/py_yaml12-${{ version }}.tar.gz (matches the user's existing recipes/py-yaml12/recipe.yaml byte-for-byte in the changed sections). 44/44 test_recipe_generator.py tests still pass. Non-Python templates (Rust CLI, Go, R, etc.) and v0 meta.yaml templates are intentionally untouched โ grayskull's literal-URL convention applies to v1 Python recipes; v0 meta.yaml is legacy and grayskull's v0 output still emits {% set name %}. Updated config/skill-config.yaml to 8.10.0.
v8.9.1 (May 25, 2026): Two corrections on top of v8.9.0. PATCH bump โ additive + corrective; same-day follow-up. (1) Grayskull-style interpolated source URL: new _build_source_url_template() helper emits https://pypi.org/packages/source/${{ name[0] }}/${{ name }}/<stem-template>-${{ version }}.tar.gz where <stem-template> is ${{ name }}, ${{ name | lower }}, ${{ name | replace("-", "_") }}, or ${{ name | lower | replace("-", "_") }} depending on how the sdist filename relates to the distribution name. Version bumps now only touch ${{ version }}. Falls back to a literal URL when the filename doesn't parse cleanly. fetch_pypi_info now tracks both the concrete URL (used internally to download the sdist) and the templated URL (emitted into the recipe) โ fixes the v8.9.0 latent bug where I'd routed source_url through the sdist-cache helper with unrendered jinja inside. (2) CARGO_PROFILE_RELEASE_{STRIP, LTO} env vars are conda-forge documented standard for ALL Rust builds, not just CLI binaries. Per conda-forge.org/.../rust โ "this recipe template supports different features: CARGO_PROFILE_RELEASE_STRIP=symbols โฆ CARGO_PROFILE_RELEASE_LTO=fat". My v8.9.0 chose to skip these in the maturin template based on the empirical 27-PR sample where only 3-7/27 used them. But the docs are prescriptive, not descriptive โ empirical low adoption reflects older PyO3 recipes authored before the guidance landed, not a maintainer rejection of the pattern. The maturin template + _generate_maturin_recipe_yaml now emit script.env with both env vars; cargo build (invoked internally by maturin) inherits them and produces a stripped + LTO-optimized cdylib. Cargo-auditable was investigated but skipped in this patch โ maturin uses cargo build not cargo install, so wiring cargo auditable would require a CARGO=cargo-auditable wrapper that isn't first-class supported by maturin; defer to v8.10+ if community pressure builds. (3) Updated recipes/py-yaml12/recipe.yaml to match the new generator output (interpolated URL + env vars). Live verification: recipe-generator.py pypi py-yaml12 emits the maturin route with version_independent: true + env vars + interpolated URL + 0 optimizer suggestions. recipe-generator.py pypi rich emits the noarch route with interpolated URL + 0 optimizer suggestions. Updated config/skill-config.yaml to 8.9.1.
v8.9.0 (May 25, 2026): Maturin/PyO3 generator routing + sdist-driven import-name extraction + abi3-gated version_independent + maturin template rewrite to phonors-pattern minimum. MINOR bump โ additive + corrective; no breaking change. Driven by recipes/py-yaml12 recipe build surfacing that recipe-generator.py pypi <name> produced an incorrect noarch: python recipe for a PyO3 package. Spec: docs/specs/conda-forge-expert-v8.9.md. Empirical anchor: 52 Rust label PRs (CLI subset cargo auditable 39/42 = 93%; CARGO_HOME 0/25 = 0%) + 27 PyO3/maturin PRs (Mar 2020 โ May 2026: maturin in host 67%, version_independent 7%, CFEP-25 matrix 4%, Windows CARGO_HOME 4%) + 30 pure-python PRs + 5 docs sources (4 example_recipes/tutorials + new knowledge_base). Six concrete generator changes in scripts/recipe-generator.py: (1) sdist cache (_sdist_cache_path, _ensure_sdist_cached) โ downloads sdist once per run under /tmp/cfe-sdist-cache/ for authoritative metadata extraction; air-gap safe (network failure โ fall back to PyPI JSON heuristics). (2) _extract_import_name_from_sdist() โ reads Cargo.toml [lib] name + cross-checks src/lib.rs #[pymodule] pub fn <X>() for PyO3 packages; falls back to first-__init__.py for pure-Python. Closes G7 trap (py-yaml12 โ yaml12, not py_yaml12). (3) _extract_abi3_from_sdist() โ regex-matches three Cargo.toml abi3 forms: default = ["abi3"]+abi3 = ["pyo3/abi3-py3XX"], pyo3 = { features = ["abi3-py3XX"] }, bare abi3 = ["pyo3/abi3-py3XX"]. Gates version_independent: true emission so it appears only when the upstream actually builds abi3 wheels. (4) _extract_build_system_requires_from_sdist() โ parses pyproject.toml [build-system].requires (the authoritative source for the build backend; G2 fix). PyPI's requires_dist only lists runtime deps. (5) _extract_entry_points_from_sdist() โ parses [project.scripts] for CLI Python packages (Wave D / S18). (6) _classify_sys_platform_deps() โ maps colorama ; sys_platform == 'win32' markers to {"win": ["colorama"]} for virtual-package if: win / then: selector emission (Wave D / S16; per knowledge_base virtual-package rule). New routing: generate_recipe_yaml() checks info.build_backend == "maturin" first and delegates to _generate_maturin_recipe_yaml() โ a compiled-extension shape modelled on the phonors PR (the modal 27-PR PyO3 pattern), with optional version_independent: true block, no noarch: python, Rust toolchain in build, maturin in host. The pure-python path now also routes import-name through the sdist extraction. Template rewrite: templates/python/maturin-recipe.yaml simplified to the phonors shape (2-line script; no CFEP-25 matrix in tests; no Windows CARGO_HOME block; no CARGO_PROFILE_RELEASE_* env; version_independent annotated as opt-in commented block); now carries adoption-percentage citations in its header comment. Rust CLI template addition: cli-recipe.yaml now notes the provider: osx_arm64: azure conda-forge.yml requirement when shell-completion generation is enabled (cross-compile cannot run the native binary). Live verification: recipe-generator.py pypi py-yaml12 now emits a clean maturin recipe with import yaml12 + version_independent: true (abi3 detected from Cargo.toml) + 0 optimizer suggestions; recipe-generator.py pypi rich continues to emit the noarch shape; recipe-generator.py pypi httpx continues clean. Build verified: recipes/py-yaml12 produces 4 conda artifacts for py310/py311/py312/py313 with the new template shape. Updated config/skill-config.yaml to 8.9.0.
v8.8.0 (May 25, 2026): Python recipe generator + template alignment with conda-forge canonical pure-python example. MINOR bump โ additive + corrective. Driven by deep-research of conda-forge.org pure-python + rattler-build's Python tutorial and Rust tutorial, validated against 30 fresh merged Python staged-recipes PRs + 27 Rust PRs. Key finding: the conda-forge docs + recent merged PRs lead rattler-build's tutorials by ~12 months on Rust (rattler-build shows plain cargo install; conda-forge + PR sample show cargo auditable install --locked --no-track --bins at 17/17 adoption). Source-of-truth order codified in skill: conda-forge docs > merged PRs > rattler-build tutorials. Five generator gaps closed in scripts/recipe-generator.py: (1) generate_recipe_yaml now emits the CFEP-25 dual-version test matrix (python_version: [${{ python_min }}.*, "*"]); without this the optimizer's TEST-002 fired on every recipe the generator just produced โ self-consistency fix. (2) about: block now includes description: |, repository, documentation extracted from PyPI's project_urls (handles all spelling variants: Repository/Source/Source Code/Code, Documentation/Docs, Homepage/Home). (3) _extract_project_urls() helper added; populated into the new PackageInfo.repository + .documentation fields. (4) python_min: in context is conditionally emitted only when the conda-forge floor (3.10) is exceeded โ at the default it's now omitted, matching what 26/30 sampled PRs do. (5) New _resolve_python_min() helper clamps the parsed python_requires floor to the conda-forge minimum (3.10) โ previously a package declaring python_requires>=3.9 produced an invalid recipe with python_min: "3.9". (6) determine_build_backend() now detects pdm-backend and scikit-build-core (1/30 emerging + modern compiled backend). (7) generate_meta_yaml mirrors patterns #1, #2, #4, #5 in v0 jinja syntax. Three template updates: (8) templates/python/noarch-recipe.yaml drops python_min: "3.10" from context (replaced with explanatory comment) + adds pdm-backend and scikit-build-core to the commented backend list. (9) compiled-recipe.yaml + maturin-recipe.yaml were already correct โ no python_min in context. SKILL.md edits: (10) "Every v1 Recipe Must Declare the Schema Header" subsection now honest about empirical scope โ 1/30 fresh PRs carry the comment; this is a local-recipes repo convention, not a conda-forge-wide requirement; reviewers do not block PRs that omit it. (11) Rust Recipe Standards subsection gains a one-line source-of-truth note explaining the divergence from rattler-build's tutorial. Live verification: python recipe-generator.py pypi rich now emits a clean recipe.yaml that passes optimize_recipe without TEST-002 or ABT-001 firing; python_min: "3.9" (rich's PyPI floor) correctly clamped to 3.10 + omitted from context. No existing recipes in recipes/ were touched โ generator-side change, idempotent for unchanged recipes.
v8.7.0 (May 25, 2026): Rust recipe template refresh + universal schema-header enforcement. MINOR bump โ additive: new optimizer check + 3 template rewrites + 1 new SKILL.md subsection; no behavior change for non-Rust recipes. Driven by a 21-PR sample (6 first-pass + 15 expanded) of CLI Rust recipes merged to conda-forge/staged-recipes AprโMay 2026 plus the live conda-forge.org/docs/maintainer/example_recipes/rust page. Adoption across the CLI-Rust slice (17/17 = 100%): cargo auditable install (instead of plain cargo install), --locked --no-track --bins, unix/win install-root split (${{ PREFIX }} vs %LIBRARY_PREFIX%), script.env+script.content shape with CARGO_PROFILE_RELEASE_STRIP: symbols + CARGO_PROFILE_RELEASE_LTO: fat, cargo-bundle-licenses + cargo-auditable together in build deps, package_contents with strict: true. Holdouts in the 21-PR sample were all justified non-CLI patterns: pyo3/maturin Python extensions (cachebox, phonors, cocoindex โ use pip install), Rust compilers with custom build scripts (inko โ nushell interpreter: script), and 1 partial-adoption case (goose). Five concrete changes shipped: (1) templates/rust/cli-recipe.yaml โ full rewrite to canonical pattern (auditable + no-track + bins + unix/win split + env map + LTO/strip + cargo-auditable in build + strict package_contents); shell-completion block commented in as opt-in. (2) templates/rust/library-recipe.yaml โ added the env map for STRIP/LTO; kept the cdylib pattern (no cargo install โ libs use cargo build --release + manual copy). (3) templates/rust/cli-meta.yaml โ meta.yaml v0 mirror of cli-recipe.yaml (auditable + no-track + bins via script_env: since v0 lacks script.env+content) for legacy feedstocks. (4) New optimizer check SCHEMA-001 in recipe_optimizer.py โ flags any recipe.yaml (v1 file) that lacks the # yaml-language-server: $schema=https://raw.githubusercontent.com/prefix-dev/recipe-format/main/schema.json header or schema_version: 1 directive. Wired before format-mixing in the critical-constraints batch. Generator already emits both lines on all 3 v1 generation paths (PyPI/grayskull at line 255-256, rattler-generate via _ensure_yaml_language_server_header at line 1350, npm path explicit at line 1079-1080); SCHEMA-001 closes the gap for user-edited / hand-migrated recipes. v0 meta.yaml files are excluded from the check (the prefix-dev schema is v1-only). (5) SKILL.md โ new "Every v1 Recipe Must Declare the Schema Header" + "Rust Recipe Standards (CLI binaries โ conda-forge 2026 canonical pattern)" subsections in Critical Constraints, listing the 5 universal patterns + explicit exception list (pyo3/maturin, cdylib libraries, custom-script projects). Quarterly audit cron sweep should flag any future drift in the upstream docs. Updated config/skill-config.yaml to 8.7.0.
v8.6.0 (May 24, 2026): AppThreat Deep Signals โ EPSS + CWE rollup wired into Phase G/G' overlay; schema v23 โ v24 โ v25 (v24 provisioned for Waves B/C; v25 cleanup dropped the Wave-C-cancelled package_hardening table + vuln_total_active + vuln_withdrawn_count columns); new fetchers fetch-epss (FIRST.org EPSS daily CSV from epss.empiricalsecurity.com; parent spec's cyentia.com URL was stale) + fetch-cwe-catalog (MITRE CWE Research Concepts at /data/csv/1000.csv.zip; parent spec said 2000.csv.zip โ wrong, that's Architectural Concepts); new _load_epss_scores + _load_cwe_categories helpers + shared _aggregate_v8_6_0_overlays pure function consumed by both Phase G + Phase G'; _phase_g_sync_current_rollup extended with COALESCE-to-existing pattern for new columns (review-finding fix โ earlier draft clobbered Phase G's direct writes when Phase G' ran with stale maps); 4 new CLI flags (staleness-report --by-epss / --has-cwe, my-feedstocks --epss / --cwe, cve-watcher --epss-threshold); detail-cf-atlas auto-renders new EPSS + CWE rows; persona-profile auto-runs (maintainer + admin set BOOTSTRAP_FETCH_CISA_KEV + BOOTSTRAP_FETCH_EPSS + BOOTSTRAP_FETCH_CWE_CATALOG; consumer skips for air-gap). MINOR bump (additive: 2 fetchers + 4 packages columns survive v25 cleanup + 4 CLI flags + persona-profile gates; no breaking CLI/MCP changes). Wave C CANCELLED pre-implementation (Phase T blint: conda-forge's hermetic compile env produces uniform hardening โ ~zero per-package variance; Phase U EPSS overlay phase: redundant with Wave B's _phase_g_sync_current_rollup extension). Wave B withdrawn-filter scope DROPPED after verifying vdb's OSV and GHSA ingest paths both skip withdrawn records at source; columns provisioned in Wave A โ dropped in Wave D's v25 migration. Four parent-spec corrections caught by verify-don't-assume: MITRE 1000-vs-2000 URL; blint PyPI name (blint not owasp-blint โ 404); withdrawn-filter redundancy; Phase U redundancy. Suite: 1,137 passing. Live verification: 334,683 EPSS rows + 944 CWEs ingested; channel-wide Phase G run populated 213 packages with EPSS + 216 with CWE classifications. Wave A commit e4ba891cd2; Wave B e22c531ac2; Wave D (this release) bundles Wave C cancellation + schema v25 cleanup + closeout. Retro at _bmad-output/projects/local-recipes/implementation-artifacts/. Updated config/skill-config.yaml to 8.6.0.
v8.3.0 (May 17, 2026): Three new recipe-authoring gotchas from the Microsoft Agents bundle (azure-identity-broker + microsoft-kiota-bundle + microsoft-agents-m365copilot{-core,}) and the npm bundle (yo + generator-code). MINOR bump โ additive only. (1) G7 โ "Grayskull's inferred Python import name can be wrong โ verify against the sdist": grayskull defaults the import test to the PyPI distribution name with hyphensโunderscores, which is wrong for re-exported short names (microsoft-kiota-bundle โ kiota_bundle), dotted namespace packages (azure-identity-broker โ azure.identity.broker), and renamed-on-PyPI packages (PyYAML โ yaml). The only authoritative source is the sdist's top-level __init__.py; verify with tar tzf <sdist> | grep '__init__.py$' | head -3. Case: microsoft-kiota-bundle v1.10.1 โ generated imports: [microsoft_kiota_bundle], actual is kiota_bundle. (2) G8 โ "Grayskull adds redundant wheel + setuptools host deps for poetry-core projects": belt-and-suspenders behavior; for any PEP 517 project with a single declared backend (poetry-core, hatchling, flit-core, pdm-backend, scikit-build-core), only that backend + pip is needed in host:. Inspect pyproject.toml [build-system].requires and drop everything not listed. Case: microsoft-agents-m365copilot{-core,} both got the redundant pair despite their pyproject.toml declaring only poetry-core; the third sibling microsoft-kiota-bundle was emitted clean โ grayskull's behavior is non-deterministic across runs. (3) G9 โ "Monorepo upstreams may have no per-language Git tag โ pin the LICENSE to a commit hash": G4 (sdist-missing-LICENSE) recommends https://raw.githubusercontent.com/<org>/<repo>/v${{ version }}/LICENSE but that's 404 when the project tags only its JS / .NET / Java releases (microsoft/Agents-M365Copilot only tags @microsoft/agents-m365copilot-v1.6.0, not v1.6.0). Fix: pin to a specific commit SHA + sha256, document the trade-off in the PR description (bot needs a manual LICENSE refresh on every version bump โ autotick won't handle it). Case: microsoft-agents-m365copilot v1.6.0 โ LICENSE pinned to commit 0376aa41834.... Also: in v8.3.0 the v8.2.0 native-build.sh auto-channel injection was validated in production across a 4-recipe Microsoft Agents bundle (m365copilot resolved freshly-built kiota-bundle + m365copilot-core from the local channel without manual intervention). Updated config/skill-config.yaml to 8.3.0.
v8.2.0 (May 17, 2026): Local-build ergonomics + new gotcha. From the yo + generator-code two-recipe bundling session. (1) New gotcha G6 โ "npm packages with rich transitive deps ship node_modules/.bin/ symlinks that fail noarch builds": documents that npm packages with substantial transitive dep trees (Yeoman generators, anything pulling ejs / jake / semver / yosay / @octokit/*) trigger rattler-build's noarch Windows-portability check after writing the artifact; minimal-dep packages (copilot-api, openspec) don't. The surgical fix is find ${PREFIX}/lib/node_modules/<name> -type d -name .bin -exec rm -rf {} + between npm install --global and pnpm install. --allow-symlinks-on-windows is local-only (staged-recipes CI rejects); --no-bin-links kills the top-level CLI shim. Case study: generator-code v1.11.18 first build -hccbf638_0.conda rejected, fixed build -h5e06de4_0.conda clean. (2) native-build.sh auto-injects the local channel โ detects build_artifacts/<config>/*/repodata.json and prepends file://${REPO_ROOT}/build_artifacts/<config> to channel_sources via an mktemp'd 3rd --variant-config (rattler-build rejects mixing --channel flags with a variant-set channel_sources). Cleaned up via EXIT trap; switched exec rattler-build โ direct invocation so the trap actually fires. Use case: recipe B has run: A where A was just built locally โ pixi run -e local-recipes recipe-build recipes/B Just Works instead of requiring a hand-rolled variant config. Updated config/skill-config.yaml to 8.2.0.
v8.1.0 (May 15, 2026): PyPI intelligence layer โ 5 new pipeline phases (O/P/Q/R/S) + new pypi-intelligence CLI + MCP tool + persona-profile integration. Architecture preserves pypi_universe as reference-data only (3 columns); all enrichment lands in a new pypi_intelligence side table (35 columns across 5 tiers) joined on pypi_name. Schema v22 adds pypi_intelligence, pypi_universe_serial_snapshots (90-day rolling history), and v_pypi_candidates view. Phase O ships activity_band classification from daily snapshot deltas (no HTTP; default-on under maintainer/admin profiles); Phase P adds BigQuery pypi.file_downloads ingest for 30/90-day download counts (opt-in admin-tier; needs google-cloud-bigquery + GOOGLE_APPLICATION_CREDENTIALS); Phase Q adds cross-channel in_<channel> BOOLs from bulk repodata for bioconda/pytorch/nvidia/robostack (homebrew/nixpkgs/spack/debian/fedora columns exist but bulk-index implementations deferred to v8.2.0); Phase R ships per-project pypi.org/pypi/<name>/json enrichment bounded to top-N candidate slice (default 5000) with deterministic _classify_packaging_shape (pure-python/cython/c-extension/rust-pyo3/unknown) + _normalize_license_to_spdx; Phase S computes conda_forge_readiness (0-100 composite via 6-component weighted formula) + recommended_template (full template path for direct conda-forge-expert invocation). Persona profile integration: admin enables all 5; maintainer enables O+Q only (no BQ/JSON-fetch cost); consumer enables O only (air-gap preserved). New pypi-intelligence CLI surfaces ranked candidates with rich filters (--score-min, --activity, --license-ok, --noarch-python-candidate, --in-bioconda, --sort-by score|downloads|serial); wrapped as pypi_intelligence MCP tool. All 8 spec open questions pre-resolved before BMAD intake: notes column added for operator overrides; project-level BQ aggregation only; full-path recommended_template; URL-pointer heuristic for non-PyPI ecosystems; staged-recipes fallback chain; 90 d snapshot retention; raw-weight readiness scoring; PRD v1.2.0 โ v1.3.0 (MINOR additive). 51 new unit tests (10 Phase O + 16 Phase P/Q + 18 Phase R + 13 Phase S + 10 CLI + meta-test SCRIPTS update). Suite: 1,064 passing. Two-PR ship: PR #1 = Waves A+B (schema + reference-data enrichment); PR #2 = Waves C+D+E (per-project + UX + closeout). Updated config/skill-config.yaml to 8.1.0.
v8.0.2 (May 14, 2026): First live bootstrap-data --profile maintainer run against the post-v21-migration atlas surfaced two follow-up bugs in v8.0.1's profile plumbing. Step 4 (cf_atlas build) completed correctly: Phase H stamped 19,386 rows in ~12.6 min, Phase N fetched 722 feedstocks for rxm7706, full atlas reached steady state (Phase H eligibility dropped 19,442 โ 4). But Step 6 (Phase N redundant re-invocation) crashed at the atlas's Phase H dispatcher with ValueError: PHASE_H_SOURCE='auto' is not one of pypi-json, cf-graph. Root cause: PROFILES had PHASE_H_SOURCE="auto" (a bootstrap-data CLI concept), os.environ.setdefault injected it, and Step 6's subprocess inherited the value because Step 6 didn't explicitly override PHASE_H_SOURCE in env_overrides (it had no reason to โ Phase H wasn't its goal). Also caught: Step 6 was redundant under profile invocations because the profile's PHASE_N_ENABLED=1 env-var inheritance already triggered Phase N inside Step 4. Fixes: (1) PROFILES for maintainer + admin no longer set PHASE_H_SOURCE (atlas defaults to pypi-json; CLI --phase-h-source flag still resolves "auto" correctly in Step 4's env_overrides); consumer keeps PHASE_H_SOURCE=cf-graph (atlas-valid). (2) Step 6 now checks phase_n_ran_in_step4 (truthy PHASE_N_ENABLED in env + Step 4 actually ran) and prints a skip message instead of re-invoking build-cf-atlas. New regression test test_maintainer_and_admin_omit_phase_h_source. Updated config/skill-config.yaml to 8.0.2.
v8.0.1 (May 14, 2026): D1+D2 live-DB verification PATCH. (1) _auto_detect_phase_l_sources SQL fix โ the helper joined package_maintainers directly on handle, but the production schema requires a 3-table join (pm ON pm.conda_name = p.conda_name JOIN maintainers m ON m.id = pm.maintainer_id WHERE LOWER(m.handle) = LOWER(?)). Test fixture used a 2-table simplification that masked the bug. Caught by live-DB verification: helper was always returning None for any maintainer regardless of their populated registries. (2) Phase H serial-gate NULL-handling fix โ pypi_last_serial != pypi_version_serial_at_fetch evaluates to NULL (falsy) when serial-at-fetch is NULL, so the entire post-v20โv21-migration working set (~9,800 rows) would have been silently skipped from condition 2. Replaced with IS NOT (NULL-safe inequality). Test added covering the post-migration scenario. (3) Phase H stat-split shipped โ v8.0.0 specced eligible_never_fetched / eligible_serial_moved / eligible_safety_recheck but never landed it. New _phase_h_eligibility_stats(conn) helper returns the three branch counts; both pypi-json and cf-graph paths print the breakdown and include it in the return dict. 5 new tests (2 stats + 1 post-migration regression + 1 case-insensitive handle match + 1 fixture-shape fix). Live-DB verification against the real 32,053-row v21 atlas: 9,654 never_fetched + 9,788 serial_moved + 0 safety_recheck = 19,442 eligible (matches eligible-rows count exactly). Updated config/skill-config.yaml to 8.0.1.
v8.0.0 (May 13, 2026): Structural enforcement + persona profiles. Bundle closes 3 of 4 v7.9.0 follow-ups (A3, A4, A5); A6 (vuln_total drop) deferred after the planned drop discovered 4 actual consumers. MAJOR because bootstrap-data --profile maintainer is the new documented default (no invocations break; legacy no-flag runs print an end-of-run advisory). Wave A ships schema v21's v_actionable_packages view encoding the canonical persona-filter triplet, refactors 7 phase selectors to read from it, and adds tests/meta/test_actionable_scope.py which asserts every SELECT ... FROM packages WHERE ... either reads the view or carries a # scope: justification comment โ preventing the drift v7.9.0 had to fix by hand. Wave B adds pypi_version_serial_at_fetch INTEGER and makes Phase H eligible-rows serial-aware (never-fetched OR serial-moved OR 30 d safety re-check); warm-daily Phase H drops ~5 min โ ~30 s on a typical day. Wave D ships bootstrap_data.py's PROFILES dict + --profile {maintainer,admin,consumer} argparse flag + _auto_detect_gh_user() (5 s timeout, graceful degradation) + _auto_detect_phase_l_sources(maintainer, db_path) (queries v_actionable_packages JOIN package_maintainers for populated registries in scope) + _print_no_profile_advisory(). Explicit env vars and CLI flags always win over profile defaults (os.environ.setdefault semantics). Maintainer profile auto-derives PHASE_N_MAINTAINER from gh api user --jq .login and auto-restricts PHASE_L_SOURCES; admin runs channel-wide Phase N; consumer uses PHASE_F_SOURCE=s3-parquet + PHASE_H_SOURCE=cf-graph + skips Phase N + PHASE_D_UNIVERSE_DISABLED=1 for air-gap friendliness. 5 previously ๐-open Phase-N-gated catalog rows flip to โ
shipped (feedstock-health --filter open-prs-human, --filter open-issues, --filter ci-red, abandonment composite SQL, maintainer-last-active SQL). New ## Profile Reference (v8.0.0) appendix in atlas-phases-overview.md; per-phase "Profile defaults" lines on D / E / F / H / L / N; atlas-operations.md quickstart + cron snippets rewritten for --profile. 24 new unit tests (19 persona profiles + 5 Phase H serial-gate). Schema v20 โ v21 migration is idempotent and self-healing on next init_schema. Updated config/skill-config.yaml to 8.0.0.
v7.9.0 (May 13, 2026): Actionable-scope audit closure โ bundled phase-by-phase fix landing as schema v20 + 29 new unit tests + a new pypi-only-candidates CLI. (1) Phase H 56ร denominator cut (_phase_h_eligible_pypi_names now applies the canonical persona-filter triplet conda_name IS NOT NULL AND latest_status='active' AND COALESCE(feedstock_archived,0)=0; denominator drops ~672k โ ~12k). (2) Phase D split into daily-lean (pypi_last_serial UPDATE on conda-linked rows) + TTL-gated universe upsert (new _phase_d_upsert_universe writing into the schema-v20 pypi_universe side table). (3) Schema v20: pypi_universe(pypi_name TEXT PRIMARY KEY, last_serial INTEGER, fetched_at INTEGER) side table + self-healing migration that moves existing relationship='pypi_only' rows out of packages into the new table in a single transaction (idempotent โ re-running init_schema is a no-op). SELECT COUNT(*) FROM packages finally returns an honest ~32k working-set count. (4) Phases J + M archived-feedstock filter at the write site (Phase J builds an inactive_feedstocks skip-set from packages before opening the cf-graph tarball; Phase M's rows_to_process SELECT gains the canonical triplet) โ closes the v19 bug where archived feedstocks polluted whodepends --reverse results. (5) New pypi-only-candidates CLI + MCP tool: surfaces admin "what's on PyPI but not on conda-forge" candidates ordered by last_serial DESC. Three-place rule applied (canonical impl + thin wrapper + pixi task + meta-test SCRIPTS entry). 29 new unit tests across 5 test files; 432 total passing. The ๐-open SQL-only "what's on PyPI but not on conda-forge?" row flips to โ
shipped in reference/atlas-actionable-intelligence.md. Updated config/skill-config.yaml to 7.9.0.
v7.8.1 (May 12, 2026): Audit close-out pass โ every remaining HIGH / MEDIUM / LOW finding from the v7.8.0 deep audit is now addressed or explicitly justified as intentional. (1) Phase H rate-limit safety: default PHASE_H_CONCURRENCY 8โ3, Retry-After parsing via the shared _parse_retry_after helper (capped at 60s), ยฑ25% jitter on exponential backoff โ closed the last HIGH item. (2) OSV air-gap parity: new OSV_API_BASE_URL env (vulnerability_scanner) + OSV_VULNS_BUCKET_URL env (cve_manager) with public-host fallback + per-call URL resolution. (3) Three new API resolvers in _http.py โ resolve_github_api_urls / resolve_gitlab_api_urls / resolve_codeberg_api_urls (path-suffix arg, list return, mirror the Maven pattern). GITHUB_API_BASE_URL=https://<ghes>/api covers GHES REST + GraphQL under one env var. _phase_k_fetch_one and _phase_k_github_graphql_batch wired. (4) Phase E cf-graph cache TTL is env-tunable via ATLAS_CFGRAPH_TTL_DAYS (default 1.0, float-parseable) โ closes the weekly-cron 150MB re-download pain. (5) Phase N rate-limit detection: new _is_gh_rate_limit_stderr parses gh api graphql stderr for primary / secondary / abuse-detection wording; _phase_n_query_batch retries up to 3x with 30s/60s base + ยฑ25% jitter on rate-limit hits (more patient than Phase F/H since secondary-limit windows are minutes). (6) Phase C incremental commits every 500 entries (was monolithic BEGIN/COMMIT around 12k UPDATEs). (7) Phase B6 and Phase J left as monolithic transactions with documentation comments โ both are intentional designs (B6 = 3 bulk UPDATEs in <1s; J = full-snapshot semantics via DELETE FROM dependencies at txn start). (8) New _http.fetch_to_file_resumable(target, urls, ...) helper: streams body to a .part sibling, uses Range: bytes=<size>- to resume, handles 206/200/416 correctly, atomic-renames on success. (9) cve_manager.fetch_and_unzip rewired to use it: 4 GB OSV all.zip streams to disk in 4 MB chunks and decompresses from the cached file โ RAM drops from ~4 GB to ~4 MB; dropped connection at 95% no longer restarts from byte 0. (10) inventory_channel.py left in-memory with a comment pointing future callers (artifacts >500 MB) at the resumable helper โ the 24h cache TTL bounds the failure mode. 44 new unit tests, 403 total passing. Updated config/skill-config.yaml to 7.8.1.
v7.8.0 (May 12, 2026): Atlas hardening pass after the v7.7.2 4,400-row Phase K rate-limit incident. Five waves: (1) _http.py gains 7 new registry resolvers (CRAN/CPAN/LuaRocks/crates/RubyGems/Maven/NuGet) + auth_headers_for (shared by urllib and requests callers) + atomic-write utilities (atomic_writer ctx manager + atomic_write_bytes / atomic_write_text). (2) Phase K: REST fanout against api.github.com/repos/... replaced with batched GraphQL (_phase_k_github_graphql_batch) โ 4,400 repos โ ~44 POSTs instead of ~14,000; per-alias errors map via path[0] to preserve downstream branching. (3) Phase F: default PHASE_F_CONCURRENCY lowered 8โ3; new _parse_retry_after honors Retry-After header (delta-seconds OR HTTP-date) capped at 60s; ยฑ25% jitter on exponential backoff prevents synchronized retry storms; new ANACONDA_API_BASE_URL env override. (4) Phase L: 8-worker ร 7-registry storm (up to 56 concurrent reqs at startup) replaced with sequential across registries + per-registry concurrency caps (crates=rubygems=1, cran=cpan=luarocks=maven=2, npm=nuget=4) reflecting documented rate limits. 7 _resolve_* functions rewired through the new resolvers. (5) Phases B5/E/E5/M resumability: replaced monolithic BEGIN/COMMIT with incremental commits + idempotent SQL; Phase E now streams the cf-graph tarball directly from disk (saves ~150MB RAM) and atomic-writes the download cache; Phase E5 saves a save_phase_checkpoint(cursor=...) per GraphQL page. Also purged hardcoded registry.npmjs.org from npm_updater.py + recipe-generator.py:fetch_npm_info + latent fix in recipe-generator.py:fetch_pypi_info. Atomic JSON writes wired into cve_manager.py, mapping_manager.py, inventory_channel.py (HTTP cache + --sbom-out). New reference/atlas-phase-engineering.md captures the 9 patterns (per-host rate limits, GraphQL batching, Retry-After+jitter, per-registry concurrency, atomic writes, incremental commits + idempotent SQL, streaming tarfiles, page-level checkpoints, <HOST>_BASE_URL enterprise routing convention) as the default rule book for any new phase or phase refactor. 39 new unit tests; 368 total passing across the CFE suite. Breaking change: Phase L return-dict field rename concurrency โ per_source_workers (no internal consumers; external dashboards may need an update). Updated config/skill-config.yaml to 7.8.0.
v5.9.0: Second live documentation pass against conda-forge.org and github.com/conda-forge (Apr 25, 2026). Also added automation/ directory: quarterly-audit.prompt.md (canonical prompt, kept in sync with the cloud routine trig_015z9XF8ExDJuN9qsZYGYKcu), run-audit-local.sh (local-CLI runner that strips frontmatter and invokes claude -p), and README.md documenting cloud vs local invocation, cron/systemd scheduling, and recreation instructions. Linked from a new ## Skill Automation section in SKILL.md. Project CLAUDE.md references section expanded into a structured ## Conda-Forge Ecosystem Reference mapping 25+ repos and docs across conda-forge and prefix-dev (Local Tooling, Submission Pipeline, Automation/Bots/Backend, Post-Submission, Documentation, Community Channels). Added new ## Ecosystem Updates (Apr 2026) section to SKILL.md covering build-tooling, platform/toolchain, and policy changes since v5.8.0. Updated CI Infrastructure: GitHub Actions is now an opt-in build provider for linux_64 (conda-smithy 3.57.1+, Mar 8, 2026) โ no longer "rerendering only"; added a provider: configuration example. Added Community Channels table: Zulip is primary; Discourse is read-only since Oct 15, 2025; Gitter is decommissioned. reference/recipe-yaml-reference.md updated with rattler-build v0.61/v0.62/v0.63 sections (debug subcommand, three-mode env-isolation, multi-output script-discovery removal); macOS SDK directory corrected to /opt/conda-sdks (was $PIXI_PROJECT_ROOT/.pixi/macOS-SDKs); new macOS Accelerate BLAS/LAPACK section. reference/pinning-reference.md adds NVIDIA Tegra notes (CUDA 12.9 SOC builds) and the new conda-forge/label/mpi-external MPI variant label. reference/selectors-reference.md and reference/recipe-yaml-reference.md updated to clarify py < 310 is a no-op (build matrix already starts at 3.10) โ examples now use py < 311. Templates python/compiled-recipe.yaml, python/maturin-{recipe,meta}.yaml, and examples/python-compiled/recipe.yaml no longer carry the obsolete py < 39 skip. templates/multi-output/lib-python-recipe.yaml annotated with v0.63 explicit-script note. Updated config/skill-config.yaml to v5.9.0.
v5.8.0: Live documentation pass against conda-forge.org + github.com/conda-forge (Apr 2026). Fixed Go compiler names: compiler("go") โ compiler("go-nocgo") in pure-Go templates; go-cgo documented as requiring stdlib("c"). Updated STD-001 optimizer check to exclude go-nocgo + legacy go but correctly flag go-cgo. Updated SKILL.md Critical Constraints: listed go-nocgo/go-cgo distinction, deprecated compiler_stack. Added ## CI Infrastructure Reference section: Azure Pipelines is primary (not GitHub Actions โ GA is rerendering/automerge only), 6-hour build limit, OS version options (cos7/alma8/alma9/ubi8/alma10/rocky10), current compiler pins (GCC 14, Clang 19), bot version_updates.exclude configuration, immutability policy. Updated reference/pinning-reference.md with current global pins table (GCC 14, Clang 19, NumPy 2, CUDA 12.9, OpenSSL 3.5, Boost 1.88, PyTorch 2.9, Arrow 20โ23) and fixed GCC version in ci_support example (12 โ 14). Updated config/skill-config.yaml version to 5.8.0.
v5.7.0: Major content upgrade synthesizing CLAUDE.md + all 21 agent-skills. Added: Operating Principles (Karpathy + using-agent-skills non-negotiables); Critical Constraints section (stdlib, format mixing, python_min floor); Recipe Security Boundaries (Always/Ask First/Never Do); Build Failure Protocol with six-step triage + category table + max-loop rule; Pre-PR Quality Gate Checklist; Migration Protocol (meta.yaml โ recipe.yaml Strangler pattern); Python Version Policy (full rules + both format examples); Recipe Formats Quick Reference (complete template examples); enhanced all 9 workflow steps with explicit success criteria; added check_dependencies as step 6 (shift-left dep validation); aligned tool table with CLAUDE.md example-usage format.
v5.6.0: Integrated all 21 skills from addyosmani/agent-skills. Added inline cross-references to each workflow step and ## Complementary Skills lifecycle phase mapping table.
v5.5.0: Standards alignment audit (conda-forge 2025/2026 changes). Fixed generate_recipe_from_pypi broken version argument. Fixed SEL-002 optimizer suggestion hardcoded python_min: "3.9" โ "3.10". Added migrate_to_v1 MCP tool. Updated pinning reference (Python 3.10โ3.14 matrix).
v5.4.0: Documentation audit pass. Updated check_dependencies MCP tool with batch repodata.json / JFrog Artifactory docs.
v5.3.0: Added update_recipe_from_github MCP tool for GitHub Releases autotick.
v5.2.0: Added check_github_version MCP tool. Fixed recipe_optimizer.py CFEP-25 python_min check (SEL-002).
v5.1.0: Added submit_pr MCP tool, completing the full autonomous loop.
v5.0.0: Major architectural overhaul โ full suite of autonomous tools, closed-loop build/debug system.
v4.2.1: Removed stdlib local testing hack via automatic variant override.
v4.0.0: Initial modular architecture.