一键导入
working-with-dist-zilla
Use when working in a Perl repo containing a dist.ini file, or when the user mentions dzil, Dist::Zilla, or @Author::* PluginBundles.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when working in a Perl repo containing a dist.ini file, or when the user mentions dzil, Dist::Zilla, or @Author::* PluginBundles.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Streamlined code review workflow - gets SHAs and invokes a general-purpose code reviewer without permission prompts
Use when the user asks to "implement the design handoff", "build this design", "apply the design handoff", or otherwise wire a design-system / component-export spec (often a set of `*.card.html` cards plus component source) into an app's real templates and CSS. Also use when the user asks to write a GitHub issue that points an implementing agent at a design to build later.
Use when modernizing a Perl project's GitHub Actions CI — applies seven idempotent transforms (fail-fast flag, Perl 5.42 matrix, perl-tester image bump, default-branch push, concurrency cancel, setup-cpm + cpm install, drop pre-5.24 macOS/Windows cells) to Dist::Zilla-style workflows.
Use when reviewing or improving Perl code against project standards — reads changed .pm/.pl/.t files (or named paths) and flags violations of the rules in STANDARDS.md (quoting, core-module use, URL building, test hygiene, and more).
Use when adding, migrating to, or auditing `precious.toml` in a Perl repo (or any repo with a `typos.toml`). Generates the canonical config (perltidy + perlvars + omegasort + optional perlcritic + optional typos), consolidates `.perltidyrc`, edits `dist.ini` to drop Code::TidyAll, wires a CI lint job, and adds a self-installing `scripts/pre-commit` shell hook so `precious lint --staged` runs locally on commit. Idempotent across re-runs.
Use when the user asks for an "adversarial review", "review this adversarially", or wants two reviewers competing to find serious issues in code or other work. Enforces scope discipline so findings count doesn't inflate across rounds.
| name | working-with-dist-zilla |
| description | Use when working in a Perl repo containing a dist.ini file, or when the user mentions dzil, Dist::Zilla, or @Author::* PluginBundles. |
| version | 1.1.0 |
Dist::Zilla (dzil) is a meta-tool that builds CPAN distributions from a dist.ini declaration. Many of its behaviors are stable across projects but invisible to anyone who hasn't been bitten — there's no "the docs say so" moment, you just learn by getting them wrong. This skill captures those patterns so you apply them on the first pass instead of discovering each one in an implementer/reviewer loop.
Worked example that surfaced these patterns in sequence: oalders/lwp-consolelogger#56 (Code::TidyAll → precious migration).
Use this skill when:
dist.ini file at its rootdzil, Dist::Zilla, @Author::* bundles, cpanfile, or META.jsondzil build output is producing diff noise or unexpected warningsDon't use when:
ExtUtils::MakeMaker / Module::Build distribution with no dist.inilib/ source code without touching prereqs or build config)digraph dzil {
"Repo orientation" [shape=box];
"Editing prereqs?" [shape=diamond];
"Decide PluginRemover vs RemovePrereqs" [shape=box];
"Apply edits to dist.ini" [shape=box];
"Run dzil build" [shape=box];
"Decide commit vs revert" [shape=box];
"Run dzil test --release --author" [shape=box];
"Tests pass?" [shape=diamond];
"Fix warnings (e.g. .mailmap)" [shape=box];
"Commit" [shape=box];
"Repo orientation" -> "Editing prereqs?";
"Editing prereqs?" -> "Decide PluginRemover vs RemovePrereqs" [label="yes"];
"Editing prereqs?" -> "Run dzil build" [label="no"];
"Decide PluginRemover vs RemovePrereqs" -> "Apply edits to dist.ini";
"Apply edits to dist.ini" -> "Run dzil build";
"Run dzil build" -> "Decide commit vs revert";
"Decide commit vs revert" -> "Run dzil test --release --author";
"Run dzil test --release --author" -> "Tests pass?";
"Tests pass?" -> "Fix warnings (e.g. .mailmap)" [label="warnings"];
"Fix warnings (e.g. .mailmap)" -> "Run dzil build";
"Tests pass?" -> "Commit" [label="clean"];
}
dist.ini usually starts with a single [@Author::Foo] line. The bundle's plugin and prereq-block names are what you'll need to reference for -remove. Find the bundle source:
perldoc -lm Dist::Zilla::PluginBundle::Author::OALDERS
Read the configure / bundle_config sub. The strings you can -remove against come from there — both plugin instance names (e.g. Test::TidyAll) and named Prereqs blocks (e.g. 'Prereqs' => 'Modules for use with tidyall').
Dist::Zilla::Role::PluginBundle::PluginRemover matches -remove values against the plugin instance name or the expanded class. Class-name removes (-remove = Test::TidyAll) work reliably because they match Dist::Zilla::Plugin::Test::TidyAll — that string is stable. Instance-name removes against a named Prereqs block like:
[ 'Prereqs' => 'Modules for use with tidyall' => { ... } ]
…require you to know the exact moniker the bundle assigned. Bundle authors pick these names ad-hoc, so the same conceptual block is called 'Modules for use with tidyall' in @Author::OALDERS but might be 'TidyAll prereqs' in another bundle. The moniker may also include spaces, slashes, or the @Bundle/ prefix, none of which is documented anywhere except the bundle source. Prefer module-level removal via [RemovePrereqs] (next section) — it sidesteps the moniker-guessing problem entirely.
Decision tree:
| Target | Use |
|---|---|
A plugin (e.g. Test::TidyAll) | [@Author::Foo] with -remove = Test::TidyAll |
| Individual modules from a bundle's prereqs | [RemovePrereqs] (see below) |
A named [Prereqs / NAME] block | [RemovePrereqs] — -remove is unreliable |
[RemovePrereqs] syntax: remove =, not -remove =[RemovePrereqs] uses remove = (no leading dash). Using -remove = raises multiple values given for property -remove from Config::MVP. This contradicts the muscle-memory pattern from PluginBundle -remove. Correct usage:
[RemovePrereqs]
remove = Code::TidyAll
remove = Code::TidyAll::Plugin::SortLines::Naturally
remove = Parallel::ForkManager
remove = Test::Vars
[Prereqs / PHASE]After stripping bundle prereqs, re-add what you still need with a phase-scoped block. The slash syntax sets the phase + relationship in one shot:
[Prereqs / DevelopRequires]
App::perlvars = 0
[Prereqs / TestRequires]
Test::Deep = 0
Use RuntimeRequires, TestRequires, DevelopRequires, ConfigureRequires as appropriate.
CopyFilesFromBuild ruledzil build regenerates META.json, Makefile.PL, README.md (and sometimes others) via [CopyFilesFromBuild]. These files are tracked at the repo root but are essentially snapshots of the last release. When a feature PR touches dist.ini, the regenerated copies show up as ~100-line diffs of prereq lists, MakeMaker plugin version bumps, and dist-version increments — noise without semantic content.
Rule: For non-release PRs, commit ONLY dist.ini and cpanfile. Revert the rest and remove the build dir:
git checkout -- META.json Makefile.PL README.md
rm -rf <dist-name>-*/ <dist-name>-*.tar.gz
The next release commit regenerates them cleanly.
Caveat: If the PR IS a release (version bump in dist.ini + Changes update), then those files SHOULD ship in the commit.
cpanfile sync is automatic[CopyFilesFromBuild] copies whatever filenames the bundle explicitly lists — it doesn't intrinsically know about cpanfile. But most modern bundles configure [CopyFilesFromBuild] copy = cpanfile (often alongside META.json, Makefile.PL, etc.), so the cpanfile at the repo root is regenerated automatically after dzil build. You usually don't need to cp manually. Verify with:
diff -u cpanfile <Dist>-*/cpanfile
If they differ, run dzil build again — your edit hasn't been reflected yet.
Linter/formatter config and dev hook scripts matter only to contributors working in the repo; they add nothing to an installed dist. Unless excluded, [Git::GatherDir] (which gathers from git ls-files) sweeps them into the tarball and ships them to CPAN, where they are dead weight.
Canonical file list — the dev-only configs that should never reach the dist:
precious.toml.perltidyrc / perltidyrc.perlcriticrcperlimports.toml.tidyallrc / tidyall.ini (legacy Code::TidyAll)scripts/pre-commit)Two mechanisms — pick by whether the file should ever enter the manifest:
| Mechanism | Syntax | What it does | Reach for it when |
|---|---|---|---|
[Git::GatherDir] (or [GatherDir]) | exclude_filename = <name> | Keeps the file out of the gather step entirely — it never enters the manifest | Files that never need to be in the dist (the root config files above) |
[PruneFiles] | filename = <name> or match = <regex> | Drops a file that was already gathered | Tracked files contributors need locally but that must not ship (e.g. scripts/pre-commit, which contributors symlink as a git hook — see the tune-precious skill's T6) |
Practical caveat: exclude_filename only works on a [Git::GatherDir] block you actually control in dist.ini. When [Git::GatherDir] is supplied by an [@Author::*] PluginBundle (the common case), you usually can't pass exclude_filename to it from dist.ini — use the bundle's documented exclude passthrough if it has one, otherwise [PruneFiles] is the universal fallback that works regardless of who gathered the file.
exclude_filename matches the file's path relative to the dist root, not a bare basename — for the root-level configs above the path equals the name, so a single exclude_filename = precious.toml is correct. For path-bearing or pattern matches use exclude_match = <regex> (GatherDir) or [PruneFiles] match = <regex>.
[Git::GatherDir]
exclude_filename = precious.toml
exclude_filename = .perltidyrc
exclude_filename = perlimports.toml
[PruneFiles]
filename = scripts/pre-commit
Verify: after editing, dzil build --no-tgz exits 0 and find <Dist>-*/ -name <file> (or grep of the built MANIFEST) shows the excluded files are absent from the build output.
Git::Contributors → .mailmapdzil build emits warnings via @Author::OALDERS/Git::Contributors (or any bundle that runs the plugin) when commit history has duplicate author identities — usually case-different emails, or one person committing under multiple addresses. Fix with a .mailmap at the repo root:
Canonical Name <canonical@email> <duplicate@email>
Canonical Name <canonical@email> <Duplicate@Email>
Verify the warnings are gone by inspecting the deduplicated identity list:
git log --use-mailmap --format='%aN <%aE>' | sort -u
dzil test --release --authorAlways run author + release tests before committing dist.ini changes:
dzil test --release --author
This catches:
Test::Code::TidyAll left behind after removing the tidyall plugin)xt/release-*.t files generated by plugins you've since removed (delete them as part of the change)If the tests fail because a generated xt/release-*.t references a plugin you removed, delete the test file — it was a build artifact of the old bundle config.
Two dzil quirks bite inside restricted execution sandboxes:
dzil test --release --author needs network sockets and home-dir writes. It uses Net::EmptyPort and writes to paths like ~/.dataprinter. Sandboxes often block both. If the tests can't run inside the sandbox, run them outside and report results back.
dzil reads ~/.dzil (often a symlink into dotfiles). Granting read access to the symlink alone is not enough — the sandbox needs to follow the link to its target. For nono sandboxes specifically, that means adding a read grant for the resolved path:
--read /home/<user>/dot-files/dzil
(Adjust the path to wherever readlink ~/.dzil actually points.) Other sandboxes need the equivalent: Bubblewrap wants --ro-bind <target> <target>, Firejail wants --read-only=<target>, Docker needs the target mounted, etc. The principle is the same — grant access to the symlink TARGET, not just the link.
| Mistake | Fix |
|---|---|
Committing regenerated META.json / Makefile.PL / README.md in a non-release PR | Revert them; the next release commit regenerates cleanly |
Using -remove = Modules for use with tidyall on a named Prereqs block | Use [RemovePrereqs] with module-name remove = lines instead |
-remove = Code::TidyAll inside [RemovePrereqs] | It's remove = (no dash) inside [RemovePrereqs]; the dash form is only for PluginBundles |
Adding a prereq with [Prereqs] and wondering why it's a runtime dep | Use [Prereqs / DevelopRequires] (or TestRequires, etc.) to scope the phase |
Manually copying cpanfile from build dir | [CopyFilesFromBuild] already does it; just re-run dzil build |
Skipping dzil test --release --author | Author tests catch removed-prereq leftovers and stale xt/ tests |
Ignoring Git::Contributors warnings | Add a .mailmap; cheap, one-time fix |
| Guessing at bundle plugin names | perldoc -lm Dist::Zilla::PluginBundle::Author::Foo and read the source |
Shipping precious.toml / .perltidyrc / dev hooks to CPAN because they weren't excluded (§7) | Exclude root config via exclude_filename (or [PruneFiles] when the bundle owns [Git::GatherDir]); prune tracked-but-local files like scripts/pre-commit |
META.json / Makefile.PL churn → you forgot the revert step; this is build-output noisemultiple values given for property -remove → you used -remove = inside [RemovePrereqs]; drop the dash-remove against a named Prereqs block silently does nothing → use [RemovePrereqs] with module names insteaddzil test --release --author passes but dzil build warns about Git::Contributors → add a .mailmapxt/release-*.t failing for a plugin you removed → delete the stale test file, it was a build artifactCannot read ~/.dzil → grant read access to the symlink TARGET, not just ~/.dzilMANIFEST contains precious.toml, .perltidyrc, or scripts/pre-commit → the tooling-config exclusion (§7) was skippedkitchen-sink:tune-precious — generates precious.toml / .perltidyrc / scripts/pre-commit and applies the §7 exclusion rule for the files it writes.mailmap format reference