What we test
Every package the factory builds passes the same gate before it is evidence, and a group of packages — a desktop app, a prebuilt binary — must do more. This page is that list, written once: the agents that draft and audit recipes read exactly this text as their skills, the gate enforces what a program can enforce, and a maintainer decides last, on the evidence of both.
Why we test the way we test
Two kinds of checks work on every package, and they are not the same kind of thing.
The gate is deterministic. vet_package, in the build worker, runs the same checks on every package in the same order and writes vet.json: same input, same verdict, and a failure is a fact a maintainer can read in the log. It cannot be argued with and it cannot hallucinate; it can only be incomplete.
The agents are not. The agent that drafts a recipe and the second agent that audits a contributor's build can write a wrong recipe or wave a wrong one through. The skills on this page make them more accurate — a finding names the check the gate has, a recipe follows the conventions the gate enforces — but a skill mitigates, it does not solve. That is why an agent's output is evidence for a person, never a decision.
So a lesson goes to both places. When a build fails for a reason we did not test for, or a package ships with a fault the gate did not see, the lesson becomes a rule in a skill — so the agents stop making the mistake — and, whenever a program can check it, a check in the gate — so nobody can. The skills carry the knowledge; the gate carries the proof. Over time the gate grows, the agents' work gets more accurate, and what reaches the rings has fewer surprises in it. What we learned, at the end of this page, is the log.
Every package
What the pool checks on every package before it is evidence, and what a recipe must do to pass. The gate (vet_package, in the build worker) runs these checks deterministically, in this order, and writes vet.json; the name in brackets is the check's name there. An agent drafting or auditing a recipe follows the same list — it is the one list.
Sources and checksums [checksums]. Every download is the upstream release — the project's own tag or release asset, never a fork, a mirror or a paste — and every checksum is real. SKIP is allowed only for a VCS source pinned to a commit or tag. A -bin package downloads the asset of each architecture separately (source_x86_64, source_aarch64) with its own checksum.
The recipe reads cleanly [shellcheck, namcap-pkgbuild]. shellcheck finds no error in the PKGBUILD (the three codes makepkg makes false are excluded: unused variables it consumes, $pkgdir it defines, cd under -e). namcap finds no error in it: url=, license=, a pkgdesc that does not repeat the name, depends that exist.
The built package reads cleanly [namcap-package]. namcap scans what was built: every shared library an ELF file links is provided by depends (glibc excepted: it cannot be uninstalled); no file is world-writable or setuid unless the recipe says why; nothing from $srcdir leaks into the package; ELF files live in /usr — or in /opt, which the gate allows for a self-contained application (see Desktop apps). Warnings are weighed, errors fail.
Files where pacman expects them [files]. Only /usr, /etc and /opt; never /usr/local, /bin, /lib, /home, /tmp or a tmp directory under /opt/<name>; no libtool .la archives; a package that installs no file fails.
Metadata that says what it is [metadata]. pkgdesc and license are set — the licence an SPDX identifier — and url= points at the project. A licence Arch does not ship as a common one is installed under /usr/share/licenses/<pkgname>/.
Tests, or the reason there are none [check]. check() runs the upstream tests when they run offline and in reasonable time; otherwise the recipe omits it with a one-line comment saying why (# no check(): …). A missing check() with no reason is a warning the maintainer sees.
It installs, and it starts [smoke]. The gate installs the package with a real pacman in the build container and starts every executable the package puts in /usr/bin with --version (then --help). A binary that cannot start — a shared library missing, a wrapper pointing at a path that does not exist, exit 126 or 127 — fails. An application that needs a display is checked differently: see Desktop apps.
Dependencies are Arch package names. depends is what the program needs at run time (the libraries it links, the tools it calls); makedepends the toolchain and headers; optdepends what unlocks a feature, with a reason after the colon. Nothing base-devel provides is listed as makedepends. The runtime is named the way Arch names it since gcc-libs became a meta-package (February 2026): glibc and libgcc for a binary that links libgcc_s (a Rust binary does), libstdc++ for C++, libgomp for OpenMP — as Arch's own recipes do (ripgrep, fd, ninja). gcc-libs in depends is namcap's dependency-implicitly-satisfied, a warning. A dependency the build log mentions and the recipe omits is a finding.
Provenance in the recipe. The file begins with # Maintainer: omarchy-pool factory <https://github.com/firemanxbr/omarchy-pool> and # Requested from: <project url>; every non-obvious choice has a short comment above it. No sudo, no network beyond source=, no prompts, no strip by hand, "$pkgdir" and "$srcdir" always quoted.
What an auditor writes. Concrete findings only — the line, why it matters, what to change — with the check's name when the gate has one, so the maintainer sees the recipe and the evidence side by side. A sound recipe gets a short ok, not invented problems.
Desktop apps
Applies when the package ships a graphical application: it installs a .desktop file under /usr/share/applications, or its main executable links a toolkit (GTK, Qt, Electron/Chromium — libX11, libwayland-client, libgtk-3, libgtk-4, libQt5Gui, libQt6Gui). Most of these are repackaged releases — Electron apps above all — so read Prebuilt binaries too.
Where it lives. A self-contained application (Electron, a JetBrains IDE, anything shipping its own runtime) goes whole into /opt/<name>/ or /usr/lib/<name>/ — one directory, nothing scattered. The gate allows ELF files under /opt for this reason and nowhere else. An application that follows the FHS on its own (a GTK or Qt program built from source with --prefix=/usr) installs normally under /usr.
The launcher in /usr/bin. A small shell wrapper, never a symlink to the real binary:
#!/bin/sh
exec /opt/<name>/<binary> --ozone-platform-hint=auto "$@"
The wrapper passes "$@" through, adds nothing a user would not want, and never --no-sandbox: an Electron app that needs it is packaged wrong, not worked around. (--ozone-platform-hint=auto lets Chromium pick Wayland or X11 at run time; it is harmless on either.)
The desktop entry and the icons. /usr/share/applications/<name>.desktop with Exec=<name> %U (the wrapper, by name, not an absolute path into /opt), Icon=<name>, a Categories= line; the icons under /usr/share/icons/hicolor/<size>x<size>/apps/<name>.png (or scalable/apps/<name>.svg), as symlinks into the application's own directory when it ships them. The gate validates the entry with desktop-file-validate.
Electron and Chromium. chrome-sandbox inside the application directory is 4755 (root-owned setuid) — that is the one setuid file the gate accepts, and the recipe says so in a comment. depends lists what the bundled Chromium needs at run time on Arch: gtk3, nss, alsa-lib, libxss, libnotify, libxtst, xdg-utils, at-spi2-core, libcups (namcap reports what the ELF scan finds; the log of a failed start says the rest). The licence file the app ships is installed under /usr/share/licenses/<pkgname>/.
What the gate does instead of starting it. A graphical application cannot start in the build container — no display, no D-Bus — so the gate does not count its exit code. It checks what a start would have proved: the real executable behind the wrapper exists and is executable; ldd on it resolves every shared library (a not found fails); the .desktop entry is valid and its Exec= names something on the path. --version is still tried, and a version printed is noted; a crash for want of a display is not a failure.
What an auditor looks for. A symlink where the wrapper should be; a --no-sandbox; an Exec= with an absolute path into /opt; icons missing so the entry shows a blank; chrome-sandbox without setuid (the app will refuse to start) or setuid on anything else; libraries the ELF scan needs that depends does not name; a .desktop the upstream ships that the recipe forgot to install.
Prebuilt binaries
Applies when the recipe repackages a release the project already built — a tarball, an AppImage's contents, a .deb, a .zip — instead of building from source. The package is named <name>-bin unless the project ships only binaries.
One asset per architecture. source_x86_64 and source_aarch64, each the release asset for that architecture with its own checksum; a project that ships one architecture gets arch=('x86_64') and nothing pretended. Never latest in a URL — the tag, so the checksum can stay true.
Unpack, do not rebuild. package() extracts the asset into the application directory (bsdtar --no-same-owner -xf … -C "$pkgdir/opt/<name>" or /usr/lib/<name>), installs the launcher, the desktop entry, the icons and the licence, and fixes what needs fixing: chmod the executables the archive lost, chrome-sandbox to 4755, Exec= in a bundled .desktop rewritten to the wrapper. Nothing is compiled; makedepends is empty or names only the tool that unpacks.
Nothing to debug, nothing to strip. Always options=('!debug'): the recipe compiled nothing, and a debug option turned on would only make a -debug split of dangling build-id symlinks, which the gate fails [prebuilt-debug] (the pool builds with !debug; it serves no debug package). Add !strip when the vendor's binaries carry their own symbols or signatures, and say why. An unstripped ELF is a namcap warning, not an error.
Libraries. The gate runs ldd on the real executable: every shared library resolves, or the package fails. What resolves through the system goes in depends; what the archive bundles (its own lib/ directory) is found through the application's own rpath or LD_LIBRARY_PATH set by the wrapper — never by copying files into /usr/lib.
What an auditor looks for. A .deb unpacked with its /usr/local, /etc/init.d or postinst scripts; a checksum that is SKIP on a downloaded binary; an asset from a fork or a CDN the project does not name; a version in the URL that does not match pkgver; files the archive scattered outside the application directory.
Who does what
Two people are behind every package the factory ships, and neither does the other's part.
The contributor — anyone signed in, a maintainer included — requests the package on the record (the project, the licence, the source) from the dashboard or through the API, and brings a build that passes the gate on their own worker: the recipe, the log, the manifest, the gate's verdict and the second agent's audit are the evidence, staged in their workspace. A build that fails, or fails the gate, is the contributor's to fix; it never reaches a maintainer's queue. When the evidence is complete the build is ready for a maintainer, and only then is a maintainer's time asked for.
The maintainer never uses the contributor's bytes. They read the evidence, have the project build the recipe again on a trusted worker with the project's agent, watch that build pass the same gate, see a real pacman install it from the lab (the trial), and decide — approve with a note, or reject with the reason, which sends it back to the contributor. A maintainer who brought the package is its contributor: nobody decides on their own package, whatever their role, so two people are always between a recipe and the rings.
Until the maintainer decides, the project's build sits in the lab: pinned, tried, visible on the package's page under From the factory, never promised and never promoted. Approved, it enters edge and earns rc and stable on the same evidence as every synced package. A maintainer can still block it later — the reason on the record, another maintainer lifts it — and it leaves every ring at once.
When a build does not pass
A build that failed, or failed the gate, or was blocked by the audit, is the contributor's to fix — and the page says how, on the row of that architecture (each architecture is built on a worker of its own, so one can be ready while the other failed). The tools, in the order to try them:
- Read the evidence. The log says what stopped it; the gate's log names each failed check, and this page explains it; the audit's report lists its findings with a fix for each; the PKGBUILD the agent wrote is beside them.
- Build again, with the lesson. The next build of that architecture starts from the last build's PKGBUILD and what stopped it — the log, the gate's verdict, the audit's report — whether that build failed, was rejected or was stopped at the gate: the drafter corrects instead of starting from nothing. A hint the contributor writes in the Build dialog goes with it: the binary's name, a build flag, a dependency, what the recipe should do differently. Inside one build the agent gets three attempts, each from the last log. (A package whose repository ships its own PKGBUILD is built as it is, with no drafting: the fix is made there, tagged, and the request renewed with the tag.)
- Choose the worker. A request lands in the shared queue the moment its record is written — every contributor's shared worker takes from it, the best idle one of the architecture first (native before emulated, then the most cores), the others after three minutes; the page says where a build stands ("3 of 7"). The Build dialog offers the queue or one of the contributor's own workers, which takes it at once; a queued build can be taken out and put back from the same dialog — nothing puts it back by itself. A build that ran emulated (x86_64 under qemu on an aarch64 host) may need nothing but a native worker: a toolchain that cannot start there fails the build as soon as it is installed, with the reason — before any correction turn of the drafter. Revoking a worker frees the builds asked for it.
- Build it at home first. The same image runs on any machine with the contributor's own agent key (Workers in the docs): what passes there is what they queue here.
A maintainer chooses the same way for the project's build: which of the project's workers — one that builds and whose agent answers, native or emulated — and the note they write is the hint the project's agent drafts with.
An approval can be withdrawn by any maintainer, the one who gave it included: one that broke the rule (a package approved by the person who brought it, as the first package was during the bootstrap), or one a maintainer no longer stands behind. The approval stays on the record and is void from then on; the package leaves every ring it reached through it; the chain is evidence again and waits for another maintainer's decision. The reason is a signed decision and a journal line, and the contributor sees it on their build's page.
The score
Every chain — a contributor's build, the project's build of it, the decision — earns points, fifty for each half, from what the pool recorded:
| The contributor's half | points | The maintainer's half | points |
|---|---|---|---|
| A request on the record: licence and source named, as the form asks today (2 when it predates the checklist — renew it) | 5 | The project built it again (−3 per extra attempt, at least 6) | 15 |
| A build that succeeds (−3 per extra attempt, at least 4) | 15 | The project's gate: clean 10, with warnings 7 | 10 |
| The gate: clean 15, with warnings 10, failed 0 | 15 | The trial installed it | 15 |
| The audit: ok 15, warn 10 (5 with a high finding), block 0 | 15 | A decision with a note (3 without) | 5 |
| The category settled | 5 |
A chain is ready for a maintainer when the contributor's half is complete: a build that succeeded, the gate passed, the audit answered — and the request as the form would take it today, naming the version the build is of. A request the pool wrote from a registration made before the form existed confirmed nothing; the contributor renews it from their page (the same form, filled from the record) and the build is ready again. The request judges what is still to be decided: a bump — the pool's own build of a new release from the approved recipe — and a chain already decided are not scored against it. Each architecture is its own chain: one can be ready while the other failed, and a person's page shows them one by one.
The class is the score today: A from 90, B from 75, C from 55, D below. Beside it the dashboard shows the class the chain reaches with the maintainer's half green — what a maintainer is told before starting. A package's class is that of its latest approved chain (or its latest chain, before any decision); the same rules rank every package in the factory, and nothing here decides anything: the number ranks, people approve. The rules are worker/src/score.ts, one function, tested.
How this page grows
A skill is a markdown file in the repository: factory/skills/general/ for what applies to every package, factory/skills/groups/<group>.md for a group — its first paragraph says when it applies. A pull request adds or changes one; with the next release it is on this page, in the worker image beside the prompts, and in front of every agent that drafts or audits a recipe. Nothing is written twice.
When a lesson can be a check, it also goes to the gate — vet_package in factory/worker/omarchy-build-worker.sh — with a name, and the skill names that check in brackets so the two stay one list. A check that needs a tool the build container lacks adds the tool to prepare_container.
To propose a skill or a check: a pull request, with the failure that taught it — the task, the log line, the package — in the description. A maintainer merges it like any other change to the process.
What we learned
- 2026-09-16 — desktop apps (Electron). Twelve of fourteen of the project's rebuilds failed the gate on the same two checks while the contributors' builds of the same packages passed. namcap-package reported ELF files under
/opt: the gate meant to allow that for self-contained applications, but its exemption named namcap's rule id and namcap was run without-m, so the exemption never matched. smoke started the application with--versionin a container with no display and no D-Bus, where Chromium dies (exit 139) whatever the package. The gate now runs namcap with-m, and for a desktop application checks what a start would have proved instead — the executable behind the launcher exists,lddresolves every library, the.desktopentry is valid — while a command-line binary must still start. The Desktop apps and Prebuilt binaries skills were written from the recipe that passed. - 2026-09-17 — a prebuilt binary's debug split, and the version a draft is of. omarchy-cli's first x86_64 build (a
-binrecipe from the release tarball) built and failed the gate on namcap's dangling-symlink inomarchy-cli-bin-debug: makepkg's defaultdebugoption makes a-debugsplit of build-id symlinks for a package that compiled nothing. The agent read the symptom, not the cause. The gate now names the rule —options=('!debug')for a recipe withoutbuild()[prebuilt-debug] — and the skill says so first. The same day the drafter was found building GitHub's latest tag instead of the release the request names (asked at v0.0.168, drafted at 0.0.175): a drafted build now carries the request's version. - 2026-09-17 — rustc under emulation. Four x86_64 builds of felix failed the same way on the Studio's emulated worker, three drafter attempts each, the agent "correcting" a PKGBUILD that was never the problem:
rustccannot start there. The host's kernel (Asahi) uses 16 KB pages; qemu-user can only place a file mapping on a 16 KB boundary, andlibedit.so.0— pulled by rustc through libLLVM — asks for its data segment at a 4 KB one (mmap … = EFAULT, "failed to map segment"). gcc, python, git, bsdtar and cargo itself start; rustc and rustup's own toolchain do not. Two things followed: an emulated worker now probes every toolchain a recipe installs and fails the build at once when one cannot start, and the shared queue hands a build to a native worker first whenever one is idle. - 2026-09-17 — the request predates the form. The first packages were registered before the request form existed; the pool wrote their records from what it had, with an empty checklist and often an unknown version, and the score gave them the full five points for "a request on the record" — so a package could read ready for a maintainer on a request nobody had confirmed. The request is now checked the way the form checks it, one function for the story, for Review and for the page: an incomplete one earns two points, is not ready, and says what to put right; the contributor renews it from their page. A package's status was also one word for two architectures, each built on a worker of its own — registered after a failed x86_64 build hid an aarch64 build waiting for a maintainer. A person's page now shows each architecture as its own chain.
- 2026-09-17 — a worker that died without a word. omarchy-cli's aarch64 build was claimed five times by the Studio's community worker and reported nothing five times: each lease expired half an hour later, the pool queued the build again, the same worker took it and died the same way — a day lost to a package that built fine on the first native worker of another contributor. The drafter had named the package
omarchy-cli-bin(a prebuilt binary, as the skill says) while the task was namedomarchy-cli; the worker looked for its package withlsand a glob that matched nothing, and undernullglobthe pattern vanished,lslisted the working directory instead, andattempt.logbecame the "package" —taron it ended the shell with status 2, before any call to the pool. Two things followed: the worker takes the task's own package from the globs themselves (<name>-…, then<name>-bin-…, then whatever makepkg wrote, or fails the build when there is none), and the shell has last words — whatever ends it while it holds a task is reported to the pool at once, with the command that did it, so a death costs a minute on the dashboard, not a day of leases. - 2026-09-18 — one package, two architectures, two namcaps. Two drafts of omarchy-cli 0.0.168, neither naming
depends=, met the gate the same morning: the aarch64 one passed with a warning, the x86_64 one — the first build to reach the gate on a native x86_64 worker — failed on namcap-packagedependency-detected-not-included libgcc, on its third attempt: the first had not linked (Arch's x86_64 makepkg.conf turnsltoon and a crate's assembly came out as bitcode — the drafter wrote!lto, as Arch's recipes do), the second had failed the gate on the-debugsplit makepkg'sdebugoption makes there and not on Arch Linux ARM, whose build-id symlinks point into the main package that namcap resolves against the installed one — and the gate runs before the install. The pool builds with!debugnow: it serves no debug package, and one recipe builds the same set on both architectures. Thenlibgcc. Arch had splitgcc-libsinto one package per library in February 2026 (gcc 15.2.1:libgcc,libstdc++,libgomp…) and left the name as a meta-package that owns no file, solibgcc_s.so.1islibgcc's — and the gate's exemption, written in September, namedgcc-libs: a name checked against memory, not againstpacman -Qoon a live system. The aarch64 pass was not a pass: namcap 3.6.0 finds a library's owner throughldconfig -pand takes as 64-bit only the lines taggedlibc6,x86-64; on aarch64 the tag islibc6,AArch64, no library finds its map, and the scan warnslibrary-no-package-associatedabout libc itself instead of naming a missing dependency — the same package, checked with namcap taught that tag (a fix namcap's master has carried since January, unreleased), reportsglibcandlibgccexactly as x86_64 does. Three things followed: the gate exempts what Arch's guideline exempts,glibc, and takes every other name namcap finds as a dependency to list —libgccandlibstdc++as Arch's own recipes list them,libgompand the rest that the old name used to cover; the build container teaches namcap that the aarch64 tag is 64-bit before the gate runs, and says namcap-libmap beside the warnings when a libc has no package, so a blind scan reads as one — a warning, and thinner evidence costs the score five points until the worker is fixed; and the exemption for an ELF under/opt, which named the id namcap prints as information (elffile-not-in-allowed-dirs) and never the one it prints as the error (elffile-in-questionable-dirs), now names both. And the one warning every x86_64 binary carried —unused-sodependon the dynamic loader, which the linker names NEEDED andldd -unever sees used — is no longer weighed: it cost every Rust package five points of the gate's fifteen and no recipe could clear it. The skill and the prompt name the runtime the way Arch does, and the drafter's own tool writesglibcandlibgccinto a Rust recipe before any model reads it.