Agent kit for working on canonical/chisel-releases. Cross-agent skills + scripts for authoring and reviewing chisel slice definition files.
73
91%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
You author slices against canonical/chisel-releases.
Scope: author + test + commit slices locally. Do NOT open PRs -- the user opens the PR themselves.
You run autonomously. There is no human in the loop mid-task. Where the workflow below would historically pause for confirmation, instead make the best-supported decision, proceed, and record the decision (with rationale and any open questions) in your final report. Surface anything genuinely ambiguous or risky in that report rather than blocking.
Existing slices are append-only. Only modify a published slice if strictly necessary (e.g. fixing a bug, adding a missing dependency, or accommodating an upstream packaging change). Never reorganise, rename, or remove paths from existing slices without a concrete reason -- downstream consumers depend on the current layout. When in doubt, create a new slice rather than changing an existing one. If you do change an existing SDF, run scripts/check-diff.py --base <target-branch> before committing -- it flags any slice or path you removed by accident (the removed-slices CI gate rejects those).
Prerequisites: run scripts/orientation <package> first -- it reports your working dir, the skill dir, and the target release + manifest format (from chisel.yaml) deterministically. Then read shared/CHISEL.md for chisel/SDF format reference, branch model, schema versions, and canonical naming conventions. This command focuses on the workflow of writing slices.
When this prompt and the repo disagree, trust the repo. Read slices/bash.yaml or slices/base-files.yaml on the target branch as canonical reference.
Follow these steps in order. Do NOT skip steps.
scripts/deb-list.py <pkg> (or apt-cache show <pkg> where apt is available); do not assume existence from the name. If the package is not found, stop and report it -- do not author anything.ubuntu-24.04). This determines which chisel-releases branch to target.chisel.yaml on the target branch: maintenance.end-of-life must be in the future.format: version in chisel.yaml. This gates available features (see shared/CHISEL.md schema versions table). Do not use v2+/v3+ features on older formats.slices/<pkg>.yaml does not already exist on the target branch. If it does, stop and report it.Before designing anything, check whether the package already has slices on other release branches. Existing SDFs inform the design and are required context for forward-porting.
List all live release branches.
git ls-remote --heads https://github.com/canonical/chisel-releases.git 'ubuntu-*' \
| awk '{print $2}' | sed 's|refs/heads/||'Check which branches already have an SDF for the target package.
# For each branch:
curl -fsSL -o /dev/null -w "%{http_code}" \
https://raw.githubusercontent.com/canonical/chisel-releases/<branch>/slices/<pkg>.yamlFetch and study existing SDFs. If the package has slices on any branch, download them:
curl -fsSL https://raw.githubusercontent.com/canonical/chisel-releases/<branch>/slices/<pkg>.yamlNote the structural decisions: slice names, grouping approach (by-type vs by-function), dependency choices, mutate: patterns. Carry these forward unless there is a concrete reason to diverge.
Compare .deb contents across releases. Run scripts/deb-list.py for the target release and for each release that already has an SDF. Look for cross-release differences (see shared/CHISEL.md Cross-Release Differences table):
/bin/ -> /usr/bin/)libssl3 -> libssl3t64)Record the differences in your final report. Note what needs adaptation when writing the SDF and when forward-porting.
Carry forward structural decisions from existing SDFs. Consistency across releases matters for forward-port reviewability. Diverge only when:
.deb contents changed enough to require a different structureIf no existing SDFs are found on any branch, this is a net-new package -- proceed to Step 3.
If the fetches fail (offline / egress-restricted environment), don't stall: treat the package as net-new, derive everything from the local checkout + deb-list.py, and note the skipped cross-release check in your final report.
Before inspecting or designing anything, build the complete dependency tree. Dependencies MUST be sliced before the target package.
apt-cache depends --recurse --no-recommends --no-suggests --no-conflicts --no-breaks --no-replaces --no-enhances <package> to resolve all transitive Depends:. Alternatively, run scripts/deb-list.py <package> to get direct Depends: and recurse manually.ls slices/ | sed 's/\.yaml$//' or chisel info --release <release> <dep> ...).IMPORTANT: Slice dependencies bottom-up. A package cannot reference slices that do not exist. Work from the leaves of the dependency tree toward the root.
Note: only Depends: matter. Not Recommends: or Suggests:. Including Recommends: is rejected by reviewers.
For EACH package that needs slicing (starting from leaf dependencies), inspect it using the bundled deb-list.py script:
scripts/deb-list.py <package> [arch] [--scripts]This fetches the .deb straight from the ubuntu mirror (reading the suite from chisel.yaml) and prints:
Depends: line (feeds directly into essential: entries)[x] executable, [f] regular file, [l] symlink (with target)--scripts to print full bodies)Reading the output:
[l] path -> target means the deb ships that symlink -- use a bare path entry, no explicit symlink: needed.[x] marks executables (go in bins); [f] marks regular files.mode: to a slice entry only when the permission is non-standard (not 0644/0755/0777). Never on a glob path -- wildcard entries accept only until:/arch: (anything else is a parse error); name the file explicitly instead.--scripts shows postinst calling update-alternatives, ldconfig, or update-mime-database, those side-effects don't run in a chisel rootfs -- either drop the dep or write a mutate: equivalent.deb-list.py libfoo amd64, then deb-list.py libfoo arm64)./usr/share/doc/**, changelogs, examples. Those are excluded by convention (see "Exclude by Default" in shared/CHISEL.md) -- under /usr/share/doc/ ship only legal files (copyright, and NOTICE/LICENSE-type notices where present).Requires dpkg-deb + network to the mirror (archive.ubuntu.com / ports.ubuntu.com). No sudo or apt cache needed.
With this output, analyse:
Understand what the package ships: binaries, libraries, config files, data files, scripts, headers, etc. Note architecture-specific paths.
Chisel does not run maintainer scripts. Whatever postinst/preinst do (create symlinks, generate files, register alternatives), you must reproduce via:
contents declarations for simple cases (symlinks, directories)mutate: scripts for logicNo explicit symlink: if the deb already ships it. Chisel preserves deb symlinks. Manual symlink: only for paths the deb doesn't ship (e.g. those created by maintainer scripts).
For ELF binaries, determine shared library dependencies (via ldd / lddtree output). Cross-reference against the dependency tree to catch transitive runtime deps. List each provider explicitly in essential: even when it would come in transitively -- reviewers run lddtree (on emulated arches too) and reject a bins/libs slice missing any of them. The usual suspects: libc6_libs, libgcc-s1_libs, libstdc++6_libs.
Use the source to:
Before designing new slices, study existing SDFs on the target branch.
Read representative SDFs for similar packages. Use slices/bash.yaml, slices/base-files.yaml, slices/openssl.yaml, slices/dpkg.yaml as references.
Follow naming conventions from shared/CHISEL.md (Canonical Slice Names table). Use libs not lib, bins not bin (the table notes the rare base-files-style exceptions).
Check shared dependencies. If the target package depends on packages with multiple slices (e.g. libc6_libs, libc6_config), determine which specific slice is needed. Do not over-depend.
Verify no path conflicts. Multiple slices from different packages can declare the same path ONLY if:
{make: true}, {text: ...}) and the inline definitions match exactlySearch existing slices: grep -r "/path/you/want" slices/
Respect the append-only principle. Removing files from existing published slices is a regression. If you need a slimmer variant, create a new slice (core, minimal, etc.) rather than removing from an existing one.
Choose the approach that fits the package best.
Best for most packages. Group files by their type. See the Canonical Slice Names table in shared/CHISEL.md.
Typical structure:
copyright slice (mandatory, always present)bins for executableslibs for shared objectsconfig for configuration filesdata, scripts, var, etc. as neededBest for complex packages with distinct functional subsets (e.g. Python standard library, large runtime frameworks).
Typical structure:
core -- minimum-functional subsetstandard -- fuller-featured above corefile-formats, networking, crypto)Do NOT mix approaches arbitrarily within a single SDF.
Start from a draft rather than a blank file:
scripts/deb-list.py <package> --sdf > slices/<package>.yamlIt groups the deb's files into bins/libs/config/headers/var/copyright, drops clutter (man pages, completions, docs), globs multiarch lib dirs (*-linux-*), wires the copyright slice + global essential (handling shared-copyright doc-dir symlinks), and sorts contents -- so check-slice.py passes on it out of the box. Ambiguous /usr/lib and /usr/share files are left as # unplaced comments for you to place. Then do the judgement the draft can't: add each slice's cross-package essential: deps (from Step 3), place the # unplaced files into the right slice (data/scripts/var/...) or drop them, reproduce maintainer-script effects, and restructure into functional slices (core/standard/...) for complex packages. The target shape (list-form essential: -- v1/v2 branches):
package: <package-name>
essential:
- <package-name>_copyright
slices:
bins:
essential:
- <dep-package>_libs
- <package-name>_config
contents:
/usr/bin/<binary>:
config:
contents:
/etc/<package>/config-file:
copyright:
contents:
/usr/share/doc/<package-name>/copyright:On a v3 branch (ubuntu-26.04) every essential: must be a map, not a list -- chisel cut rejects the list form with "essential expects a map". Same shape, map keys:
essential:
<package-name>_copyright:
slices:
bins:
essential:
<dep-package>_libs:
<package-name>_config:Key rules:
package: must match the filename stemessential: lists <pkg>_copyright so every slice transitively ships itcopyright slice placed last by conventionlicense / notice slicesUpstream LICENSE.txt, NOTICE, ThirdPartyNotices.txt are not the deb copyright. They get separate license: / notice: slices that depend on <pkg>_copyright.
These are mandatory. CI and reviewers reject non-conforming SDFs.
After writing the SDF, run the bundled deterministic linter -- do not eyeball these rules:
scripts/check-slice.py slices/<package>.yamlIt reads format: from ./chisel.yaml automatically (or pass --format N / --branch ubuntu-XX.XX). It reports block (fix before commit -- CI/parse failure), warn (reviewers reject), info (nit / skipped). Fix every block and every warn you can't justify before moving on.
The script mechanically owns: sorting (contents paths and essential entries, bytewise ASCII -- CI checks with LC_COLLATE=C sort -C), slice-name validity, absolute paths, duplicate contents keys, arch names (list order there is a nit, not a gate), clutter exclusions, copyright presence, the version-gated fields (hint/prefer/v3-essential/essential-as-map/essential-as-list), and hint: length + style. Don't restate its work -- run it.
The rules it can't judge -- these are on you:
essential (global) at the top of the file, right after package:.copyright slice at the bottom of the slices: block.*-linux-*, not explicit triples. E.g. /usr/lib/*-linux-*/libfoo.so.1:.* for single-version sonames: libfoo.so.1: not libfoo.so.1*:./usr/src/rustc-1.93.*/**, /usr/lib/perl5/*/. Patch-level pins break on the next package update.** or a bare *.pm can collide with hundreds of other packages' paths. Add another path level to scope it (.../perl5/*/auto/DBI/DBI.so:), and grep -r "/shared/path" slices/ before declaring a path more than one package could own./path: {arch: [amd64, arm64]}./usr/bin/foo: # Symlink to ../lib/foo/foo..github/yamllint.yaml): 2-space indent, lines <= 100 chars, at most one consecutive blank line, comments aligned to content, at most one space inside { }/[ ].Testing is mandatory. Every package gets a spread test at tests/spread/integration/<package>/task.yaml -- upstream ships one even for pure-library and data-only packages (ca-certificates, base-passwd, fontconfig, ...). The classification below controls test depth only, never whether the file exists.
Testing blocks commit. Do NOT proceed to Step 11 (commit) without tests landed. A feat: slice and its test: tests form one series -- both must exist before you stop. If tests aren't feasible, leave the slice uncommitted and report why; do not commit the slice alone.
Use the bundled try-cut helper to verify the cut succeeds without managing the temp root manually (NOTE: it removes the rootfs on exit -- installability only):
scripts/try-cut [--arch ARCH] <package>_<slice>When you need to poke around the resulting rootfs (chroot in, inspect files), cut manually instead:
mkdir rootfs/
chisel cut --release ./ --root rootfs/ <package>_<slice>Start from the scaffold rather than a blank file:
scripts/scaffold-test.py slices/<package>.yaml > tests/spread/integration/<package>/task.yamlIt emits one fresh rootfs per slice and a chroot line for every declared binary, so coverage is complete by construction. Then do the real work per package kind:
libssl3): verify .so files exist and are valid ELF (head -c4 | grep ELF-style). Minimal depth acceptable -- but the task.yaml still exists.grep, sed): --version + one representative functional test.python3, nginx, curl, git): thorough suite. Research first: read the package docs (features, flags, config), study the upstream test suite (test*/, tests/, t/ in the source package), identify key functional areas (each gets at least one test), and check runtime lookups (/etc/passwd, /tmp, timezone/locale data).The shape the scaffold produces:
summary: Integration tests for <package>
execute: |
# <package>_bins: fresh rootfs so a missing dep can't hide behind another test.
rootfs="$(install-slices <package>_bins)"
chroot "$rootfs" <command> --version # replace with a real functional check
# (for an http client: fetch a URL; for an interpreter: import core modules)Test design principles:
bins slice must prove its binaries actually work.bins and scripts, both need tests.install-slices afresh for each test rather than reusing one rootfs -- leftover slices from an earlier test mask a missing dependency in a later one. This is a standard reviewer request.bins slice must be exercised. Reviewers reject untested binaries. A binary you can't drive fully still gets a skeleton test proving the dynamic linker resolves it -- run it and grep for its own usage/error text, e.g. chroot "$rootfs" /usr/lib/foo/helper 2>&1 | grep -Fiq "usage"./dev/null, /bin/sh, or DNS, fix the environment per the "Chroot environment patterns" table in shared/CHISEL.md -- do not retreat to file-existence checks.https://example.com) is accepted upstream -- copy resolv.conf in per the chroot patterns table.Test hygiene (recurring review nits):
--arch "$chisel_arch" from install-slices on 26.04 -- it was a v2-era workaround, not needed there. Older branches may still want it."$rootfs" (no trailing-slash/brace noise), quote every variable, and use bash arrays rather than string-joined args.grep -Fiq (-F literal, -i case-insensitive, -q quiet).sleeps or unbounded retry loops -- bound every wait with a timeout so spread can't hang.trap cleanup EXIT to umount anything you bind-mounted (/dev, /proc). Some tools need /proc mounted in the chroot (see systemd/test_standard.sh).Run with: spread lxd:tests/spread/integration/<package>
Then check coverage deterministically:
scripts/check-test.py slices/<package>.yamlIt warns when there's no test, or a test that exercises none of the binaries -- fix those before committing. It reports partial coverage as info with the list of untested binaries: review that list and add a check (or at least a linker-resolves skeleton) for each you reasonably can. Full coverage isn't demanded -- alternatives symlinks, multi-call binaries, and big suites are fine tested representatively.
Before committing, cross-reference the authored SDFs against the official chisel documentation (the authoritative source of truth). Fix discrepancies now -- landing a commit and then finding it diverged means an avoidable amend.
curl -fsSL https://raw.githubusercontent.com/canonical/chisel-docs/main/docs/how-to/slice-a-package.md
curl -fsSL https://raw.githubusercontent.com/canonical/chisel-docs/main/docs/reference/chisel-releases/slice-definitions.md
curl -fsSL https://raw.githubusercontent.com/canonical/chisel-docs/main/docs/reference/chisel-releases/chisel.yaml.md(rendered at https://documentation.ubuntu.com/chisel/latest/<path>/ if you prefer. If these fetches fail -- offline / egress-restricted environment -- skip this step, rely on shared/CHISEL.md + check-slice.py, and note the skipped verification in your final report.)
Check: does the SDF use any undocumented fields or patterns? Does the design match documented recommendations? Is the format: version compatible with all features used? Fix any discrepancy before committing; note deliberate divergence in your final report.
If tool behaviour diverged from the docs during chisel cut (a field ignored, a wildcard that didn't match, mutate running differently), note it -- the tool source at https://raw.githubusercontent.com/canonical/chisel/main/internal/setup/setup.go is the ultimate arbiter.
Precondition: scripts/check-slice.py slices/<pkg>.yaml reports no block findings, scripts/check-test.py slices/<pkg>.yaml reports no warn (a test exists and exercises the binaries), tests/spread/integration/<pkg>/task.yaml exists and passes (spread lxd:tests/spread/integration/<pkg>), and Step 10 surfaced no unresolved discrepancy. If the linter blocks, the test is missing or exercises no binaries, or tests fail, stop -- do not commit a feat: slice with lint blocks or without working tests.
Commit in two steps (one category per commit): the feat: slice first, then the test: tests. Both must land before you stop.
git -C <repo> commit -m "feat(<pkg>): add <slice-list> slices" # the SDF(s)
git -C <repo> commit -m "test(<pkg>): add integration tests" # the spread test(s)Follow conventional commits: feat:, fix:, test:, ci:, chore:, docs:. Subject lowercase, imperative, <=50 chars, no trailing period. Body wrap 72.
Stop here. The user opens the PR themselves.
Reminder: all PRs must be forward-ported oldest -> newest across all maintained release branches. Note any required forward-ports in your final report.
Return a concise summary to the caller (this is your output -- it is not shown to the user as chat). Include:
Do NOT dump full file contents or raw command output -- summarise.
Read live SDFs from the checkout -- they are canonical and can't go stale (Step 5 already mandates this):
slices/vim-tiny.yaml (bins + config + copyright)slices/libc6.yaml (multiarch lib globs, arch-gated entries)slices/dpkg.yaml (bins/config/tables/var split)grep -l "mutate:" slices/*.yaml (e.g. slices/apt.yaml, slices/libpam-runtime.yaml)The one non-obvious shape worth showing -- the mutate: + until: mutate + mutable: triad for a file that must be generated from shipped inputs (schematic, not a real package):
package: foo
essential:
- foo_copyright
slices:
data:
contents:
/etc/foo/merged.conf: {text: FIXME, mutable: true} # placeholder, rewritten by mutate
/usr/share/foo/conf.d/: {until: mutate} # inputs: present for the script,
/usr/share/foo/conf.d/**: {until: mutate} # removed from the final rootfs
mutate: |
dir = "/usr/share/foo/conf.d/"
parts = [content.read(dir + p) for p in content.list(dir)]
content.write("/etc/foo/merged.conf", "".join(parts))
copyright:
contents:
/usr/share/doc/foo/copyright:(The rest of this file already covers dependency order, transitive deps, naming, globs, copyright, maintainer scripts, clutter, and testing -- these are the ones with no step of their own.)
{arch: ...} for arch-specific paths -- inspect each target arch with deb-list.py <pkg> <arch> before assuming one layout.logrotate drop-in when logrotate isn't sliced) is dead weight -- drop it.