| name | bepinex-mono-mod-quality-check |
| description | Review or create a BepInEx Mono mod for C# project and module structure, plugin identity, game compatibility, dependency boundaries, repository-family alignment, GitHub CI and release automation, package readiness, release metadata, and verification. Use when implementing, reviewing, preparing a release for, diagnosing quality gaps in, or applying a minimal-difference peer rollout to a BepInEx 5 Mono plugin or its build and release workflow. |
BepInEx Mono Mod Quality Check
Goals
- Preserve a loader-visible plugin identity that agrees with the assembly, generated metadata, and package metadata.
- Keep C# project and module boundaries aligned with gameplay policy, framework integration, and composition
responsibilities.
- Keep game and BepInEx compatibility explicit, evidence-backed, and scoped to what was actually tested.
- Produce reproducible builds and installable release packages without committing local game or build output.
- Keep GitHub Actions and release automation pinned, least-privilege, and traceable to the built artifact.
- Report findings as actionable evidence, not assumed conventions from another mod.
Documentation responsibility
Use software-documentation-maintenance to create and maintain the required docs/domain/, docs/architecture/, and
docs/operations/ map, its indexes, canonical ownership, migrations, and links. This Skill remains responsible for the
BepInEx-specific content and correctness placed in that map:
domain/ records the exact tested game, loader, Harmony, Unity, networking, serialized-asset, and package-host facts
needed by the integration.
architecture/ records the mod's Core/Interop boundaries, callback and authority policy, state lifetime,
transactions, failure behavior, and product-selected integration choices.
operations/ records the verified restore, build, runtime validation, packaging, release, and Thunderstore procedures
selected by the repository.
Apply prose-quality-check only after ownership and technical evidence are settled. Do not move a mod-selected policy
into reusable domain knowledge, and do not treat a well-structured document as proof that its game or release claim is
correct.
Use unity-game-analyze to establish version-specific game-code and serialized-asset evidence: the concrete call path,
effective prefab/scene values, object/load reachability, lifecycle timing, authority, and unresolved runtime inputs.
This Skill consumes that evidence to select and verify hooks, adapters, identity mappings, compatibility claims, and
tests. It remains responsible for the mod decision; unity-game-analyze must not approve a Harmony target merely
because a method exists.
Required Template Baseline
Use the quality baseline in this Skill for a repository that does not already have stricter, equivalent rules.
Repository files establish target-specific facts such as game build, loader version, package host, and release
namespace; they do not replace this Skill's quality bar.
- Commit an SDK-style project,
nuget.config with explicit sources and package source mapping, global.json selecting
the release SDK, one packages.lock.json per resolving project, a narrow .gitignore, Markdown lint configuration,
and contributor documentation.
- Keep the contributor agreement and the pull-request confirmation internally complete. Adopt the bundled CLA contract
only after the maintainer explicitly selects the project license and contribution terms; otherwise use
license-neutral contributor guidance without a CLA checkbox. When adopting the bundled contract, deploy
repository-contributing and github-pull-request-template together; never infer that a CLA checkbox is unsupported
without inspecting its referenced CONTRIBUTING.md section.
- For C# changes, require locked restore, format verification without restore, and a no-restore build. For Markdown,
workflow, shell, APM, package, or release changes, apply the corresponding checks in
repository-quality-template.md.
- Use event-owned CI entry workflows: pull requests (and merge queues when used) validate proposed source with a
lint
job (and a test job when the repository has tests), while the integration branch re-runs those jobs on its exact
commit and directly gates build, retained edge artifacts, and release through needs. Keep lint-source as a
Composite Action name, not a catch-all job name. When version or publication state must be resolved, use a read-only
plan job and make build depend directly on lint, optional test, and plan; have release consume the
verified build artifact and any needed plan output. Here, read-only means that planning does not modify tracked
source/package files, tags, releases, or other GitHub state and has no write-capable token. It may fetch remote refs
needed to classify the current version. Do not add manual dispatch or polling jobs without a documented
operator/recovery need.
- When the target adopts a bundled CI or publishing contract, copy its files exactly from this Skill's canonical
assets/ and verify them from the installed Skill during authoring and review. Consumer CI must run only committed
repository-owned actions and scripts; it must not execute .agents/skills/ or require this Skill at workflow
runtime. Follow
canonical-templates.md for selection, synchronization, validation, and rollout;
do not fork a template silently.
- Treat a missing required file, unpinned external executable input, unreviewed package source, absent lockfile, or
unverified final archive as a finding; do not downgrade it to a repository preference.
- Provide a global
Enabled configuration setting by default. It must disable the mod's declared functionality without
implying that BepInEx unloads the plugin. Omit it only when the mod's size, lifecycle, or implementation constraints
make a coherent global gate impractical; record that concrete constraint and the available narrower controls.
- Allow a target repository to add stricter checks. Record a concrete compatibility or host constraint before omitting a
baseline item.
Workflow
Follow implementation-runbook.md in order for every setup, alignment,
implementation, or release-readiness task. It defines the required evidence ledger, file inventory, change order,
conditional branches, verification matrix, and report format. Do not replace that procedure with an informal checklist.
- Establish the repository contract before proposing changes.
- Read
AGENTS.md, README.md, CONTRIBUTING.md, the project file, lockfile, package assets, and relevant CI or
release scripts to find target-specific facts and existing stricter rules. Do not use their absence as a reason to
omit this Skill's baseline.
- Create the evidence ledger from the runbook before selecting a target framework, loader references, plugin
identity, package metadata, archive layout, or publish destination. Mark an unavailable fact
blocked; never fill
it from another BepInEx repository.
- When the maintainer designates peer repositories or requests horizontal rollout, follow
repository-family-alignment.md before applying the abstract baseline.
Require a disposition for every missing, extra, changed, or newline-different target path. Reuse portable shared
content exactly unless a concrete product, runtime, test, package-host, repository-visibility, or maintenance
constraint requires the smallest possible difference.
- Treat a requested game-version change as a compatibility alignment. Record the old and target version, Steam
manifest/build identifier, matching managed-code and serialized-asset evidence handoff hashes, game-reference
package version, every compatibility claim, and the required build and runtime checks. Do not claim the target
version when code and asset evidence come from different builds.
- Classify the request as
setup, alignment, implementation, release-readiness, or plan-only. Execute only
the runbook branches enabled by the evidence ledger and request type; record every disabled branch and its concrete
reason.
- For a new repository or a template-alignment review, read
repository-quality-template.md after the runbook inventory. It is the
normative file-by-file standard, not permission to fork a designated repository family's portable conventions.
- If the evidence ledger enables a contract represented by a bundled template, read
canonical-templates.md, select only the applicable template IDs, and use the
sync script for application and authoring-time drift checks. Keep target-specific workflow values in the caller,
not in a copied action. Do not make consumer CI depend on the installed Skill.
- When a patch, reflection target, serialized name/value, Unity object, game lifecycle, or build-compatibility claim
lacks a closed target-build trace, use
unity-game-analyze and record its evidence handoff. Do not reconstruct the
Unity code/asset procedure informally inside the mod review.
- Check C# project and module structure.
- Start from the repository's existing layout. Do not introduce projects, layers, or folder taxonomy solely to
resemble another mod.
- Keep the loader entry point and composition root small. It may create the controller, adapters, state, and use
cases, but it must not become the location for gameplay policy or framework callbacks.
- Put gameplay policy, state transitions, result values, and use cases in framework-free Core modules when the mod
has enough behavior to benefit from a boundary. Core may depend on ports it defines, but not on BepInEx, Harmony,
Unity, or game-specific types.
- Put BepInEx configuration/logging, Harmony callbacks, Unity/game-object access, networking, and concrete port
adapters in Interop modules. Keep callbacks thin: validate or guard the framework boundary, translate data, and
delegate to Core-facing controller methods.
- Introduce a Core port and an Interop implementation when a use case needs game or framework access. Do not hide
that access behind static singletons in Core.
- When one callback or update supplies multiple outputs, capture live game/framework state once in Interop as a
framework-free observation, derive conditions once in Core, and pass one immutable frame or result to every
presenter. Do not let HUD, world rendering, logging, or other presentation paths independently resample the same
live state.
- Keep direct observations, derived conditions, and intentionally incomplete diagnostic proxies distinct in names and
types. Represent values that become available together as one optional aggregate or result variant instead of
correlated nullable properties, and preserve
unknown or unreadable separately from observed false or absence.
- Do not add persistent Core state merely because a value type belongs in Core. Keep an update frame transient unless
history, smoothing, change detection, save/restore, or another explicit cross-callback rule requires a store.
- Keep a module cohesive around one responsibility. Split a module when it mixes policy with framework access or when
its lifecycle/state ownership cannot be stated clearly; do not split it merely by file type or pre-emptive reuse.
- Keep one plugin project when it enforces the needed boundary. Add separate C# projects only when a concrete test,
reuse, build, or dependency-isolation need outweighs the added target-framework and packaging complexity.
- Keep repeated build properties in project files until at least two projects actually share them and the repository
family uses a common props file. Do not add
Directory.Build.props merely to centralize a small solution.
global.json has a separate SDK-selection role and remains required when the family build uses it.
- Check the plugin boundary.
- Ensure the assembly name, plugin GUID, plugin name, and loader-facing version have one documented source of truth
or a verified synchronization path.
- Confirm the plugin entry point uses BepInEx metadata and that generated plugin-info values, if used, agree with the
project configuration. For a game-specific plugin with a stable tested executable name, declare
BepInProcess and
verify it in the built assembly.
- Keep Harmony or equivalent patch registration deliberate: use a stable identifier, a bounded assembly/patch scope,
and lifecycle behavior that does not duplicate registration.
- Make every patch/callback boundary fail-safe according to its declared behavior. Guard the primary callback and its
diagnostic/error-reporting path independently so a logging, serialization, or reporting failure cannot escape into
the game callback.
- Gate callback-driven state changes, scans, UI work, and network sends on the mod's declared execution role
(
client, host, or server) using an explicit role check at the Interop boundary. Treat unavailable network state
as not authorized for that role; do not rely on incidental RPC stage or prior state to suppress work.
- For practice, debug, cheat-like, or other client-visible features that can substantially change game balance,
default non-host use to denied. Require an authoritative host consent path before enabling the feature for a
guest. A local guest setting never grants consent. Use the host's installation of the same mod as the consent
signal only when the documented feature cannot affect host security or another protected trust boundary;
otherwise require an explicit host-controlled allow/deny policy. Document the consent model, denial behavior,
and any deliberately different policy. Read guest-feature-authorization.md
before selecting the authorization transport, settings, lifetime, or verification matrix.
- Bind the global
Enabled setting at the Interop/configuration boundary and gate the mod's declared work before
expensive observation, mutation, presentation, or network sends. Keep the plugin loaded so lifecycle cleanup and
safe callbacks remain valid. If a global gate is impractical, document the concrete scale or lifecycle constraint
and verify that the narrower controls cover the intended operational need.
- When an observation must be all-or-nothing, enumerate every prerequisite needed to classify each entry. A
missing/destroyed entry, required metadata object, or required classification field fails the whole observation;
never return a partial count merely because one nullable layer was skipped.
- For transactional changes to base-game state, separate prepare, commit, and rollback. Start the restoration scope
before the first destructive mutation; keep every fallible synchronization, spawn, RPC, and commit operation inside
it; and restore the exact original values plus remove prepared/spawned residue on failure. A swallowed outer
exception is not a substitute for rollback.
- Treat resource acquisition as a transaction too. If allocation or instantiation succeeds and later validation in
the same prepare step fails, return an owned cleanup handle or clean up internally before throwing. Fault tests
must cover failure after each acquired resource, not only before a stage begins.
- Before patching a networked method, record its boundary ledger for every relevant path: execution role,
send/server/receive stage, authoritative arguments, synchronously mutated fields, and deferred RPC effects. Select
the hook and state source from that ledger; never read a mirrored field in a postfix unless the call graph proves
it is updated before the hook. Cover host-local and remote-client paths when both exist.
- For a shared RPC/event boundary, enumerate every baseline caller and relevant mod-extensible caller class. Preserve
an explicit transaction-origin signal or paired hook so a state synchronization callback is not treated as proof
that the intended domain action occurred. Negative provenance paths must leave base-game state unchanged.
- Validate the real Harmony boundary, not only extracted Core policy. Invoke the actual prefix/postfix entry points
in the target version's call order for every host and remote path. Use deliberately different method-argument,
pre-call field, post-call field, and deferred-receive values so an incorrect authority source or provenance window
cannot pass through accidental equality.
- Make the boundary harness reproduce the base method's actual mutations and omissions. Do not manually update a fake
field at a point where the inspected game method leaves it stale. Assert each intermediate sentinel before invoking
the patch so the test proves timing rather than merely reaching the expected final value.
- Preserve protocol indexes, catalog/list indexes, network IDs, and stable domain IDs as separately named or typed
values through preparation and application. Hash, persist, log, or serialize only the identity named by the product
contract; never relabel a collection index as a domain ID.
- Choose prefix or postfix from lifecycle semantics. Observe completion-dependent state in a postfix unless pre-call
mutation is required; if the base method is skipped or throws, do not report or persist completion that did not
occur.
- For every lifecycle predicate, enumerate the named positive states and immediately adjacent negative transitions
such as loading, departing, travelling, resetting, and unavailable state. Build and verify a truth table rather
than using a broad proxy such as
not in orbit when the product names a narrower state.
- Treat game API calls, patches, serialized names, and timing assumptions as compatibility-sensitive. Preserve the
exact tested game baseline and request runtime evidence when a code-only review cannot establish it.
- Check build and dependency boundaries.
- Match the target framework and language version to the repository's documented BepInEx and game runtime
compatibility; do not upgrade either opportunistically.
- Keep game and loader references compile-only when the release package relies on the player's installed runtime.
Keep development analyzers and code generators private to the project.
- When updating a game-specific compile reference, update its resolved package and lockfile to the target game
version, then reconcile every version-bearing project, package, changelog, and compatibility document field. A
successful compilation proves only API resolution; it does not establish runtime compatibility.
- Require a committed lockfile for every resolving project and run
dotnet restore --locked-mode. After restore, run
dotnet format --no-restore --verify-no-changes and a no-restore build. A missing lockfile or unlocked restore is
a reproducibility finding.
- When a change adds or modifies automated tests, run the repository's documented test command after the locked
restore and no-restore build. A separate Core or test project is justified only by its stated test, reuse, build,
or dependency-isolation benefit, and must meet the same lockfile and source-mapping rules as every other resolving
project.
- Require
nuget.config to clear unreviewed default sources and map direct and transitive package patterns to their
intended source. Review every new feed, package, lockfile change, CI action, container, or downloaded tool for
canonical source, publisher, immutable version or digest, content hash where available, license, transitive
effects, and seven-day release age before adoption.
- Do not commit game installations, mod-manager profiles, generated plugin metadata,
bin/, obj/, or machine-local
paths.
- Check package and release consistency.
- Trace the release artifact from the built DLL to its archive. Confirm that the archive layout and every required
file match the package host and repository documentation.
- Keep a versioned, repository-owned archive contract. Its host-neutral part must define the allowed root paths,
exact plugin-DLL count, prohibited contents, and archive path-safety rules; add a host-specific extension only from
the selected host's authoritative contract. Do not substitute another host's manifest or layout.
- Reconcile package manifest identity, version, dependencies, compatibility claims, README, changelog, icon, and
any explicitly selected license with the release intent. Do not create, infer, or package a license until the
maintainer selects one.
- When compatibility is repeated across the package README, developer
changelog, and package changelog, inspect same-role entries in designated
peer repositories before writing. Preserve the established hierarchy,
dates, and reference links, while retaining each artifact's role: README
compatibility sections may show the target and test environment; changelog
Notes entries should state concise compatibility facts. Keep detailed
evidence status in the evidence ledger, developer documentation, or PR
notes unless it changes the release's user-facing compatibility meaning.
Do not infer that an incomplete runtime result belongs in a package
changelog or README merely because it is recorded in developer evidence;
add it only when the same-role convention or maintainer selects it.
Do not add a Changed entry that merely says the documentation was
"clarified"; record the resulting compatibility state under Notes.
- Separate package readiness from publication authorization. When the evidence ledger confirms a distribution host,
keep the repository family's portable manifest, package README, user-facing changelog, editable and rendered icon,
final-archive validation, inert publisher tooling, and a license only when explicitly selected complete even when
an external upload is not yet enabled. If the host is blocked or explicitly none, do not invent or borrow
host-specific metadata. Gate the publication side effect on explicit host, namespace, category, credential,
runtime, and release-mode approval.
- Verify version synchronization from produced outputs, not duplicated source literals. Generate loader-facing
metadata from the project version or inspect the built assembly and compare project, assembly, loader-facing,
manifest, archive, and tag versions that are enabled for the release.
- Decode BepInEx loader metadata from the built assembly's actual custom attributes. String presence in DLL bytes
does not prove
BepInPlugin, BepInProcess, or dependency attributes carry the expected constructor values.
- Verify identity, supported game/loader baseline, dependencies, and release version on the exact metadata and
documentation files copied into the final archive. Repository-root prose does not satisfy a package-facing claim
when it is not distributed.
- When a package-facing README embeds screenshots or diagrams, verify that every reference resolves and renders in
the package host or its documented renderer. Keep renderer-specific fallback files separate from editable
developer sources, and do not treat root-README rendering as package validation. Package an image only when the
host contract requires it; otherwise a stable repository-hosted fallback may avoid duplicating an asset whose
authoring lifecycle belongs in developer documentation.
- Keep developer changelog entries and user-facing release notes in their repository-defined roles. Either package a
distinct publication-facing source or explicitly declare the canonical changelog dual-purpose; do not describe a
derivation step that packaging does not perform. Do not claim untested game compatibility or publish a version
already represented by an immutable release.
- Reserve
0.0.0 for unpublished edge or development builds; do not use it
as a released changelog version. Derive every other release version from
the project source instead of hardcoding a specific public version in
workflows. Keep pending work under Unreleased until a real release
version and date are selected.
- A negative package fixture proves only the first production guard it reaches. Derive it from the passing fixture,
preserve every earlier invariant, mutate the intended property, and assert the intended branch-specific diagnostic
or result. A generic exception or nonzero exit does not prove the advertised rejection branch.
- When the project derives manifest or package versions in CI, verify that the project version, generated version,
and loader-compatible version are deliberately handled for stable, prerelease, and edge builds.
- Check GitHub repository settings, CI, and release automation when the repository uses GitHub Actions or GitHub
Releases.
- Require the repository or organization Actions setting that enforces full-length commit-SHA pins. Independently
verify every third-party
uses: reference has a full commit SHA and an accurate version comment; inspect reusable
workflows, container digests, and downloaded-tool checksums too.
- When the repository publishes GitHub Releases, require repository-level immutable releases where GitHub makes the
setting available. Automation must attach every asset before publishing the release and must fail rather than
replace an existing tag, release, or asset. If the setting is unavailable, record the residual risk and require an
explicit fail-on-existing-release/tag/asset path instead of silently treating releases as immutable.
- Keep checksum material used to verify the build-to-release handoff inside the workflow artifact by default.
Publish only the package archive unless the repository has an explicit user-facing checksum-asset contract.
- Trace one release from its source commit through locked restore, build, archived artifact, and release asset.
Publish only the artifact produced by that build; do not rebuild a separately checked-out revision in the release
job. Create and verify an artifact digest across the build and release jobs.
- Install the exact SDK selected by
global.json in CI with a full-SHA-pinned setup action or another pinned,
verified mechanism before restore. Do not assume a hosted runner already contains the release-critical SDK.
- Separate validation artifacts, prereleases, and stable publishing according to the repository's version rules. Gate
external package-host publication on the intended stable release mode and require exactly one reviewed package
artifact. Keep a separate committed publication-authorization input disabled until the maintainer explicitly
approves the side effect; a stable version alone is not authorization.
Do not introduce a separate release mode merely because a numeric public
release remains beta: when the repository classifies it as stable release
mode, express beta status in its release metadata and notes.
- Retain every integration-branch edge build as a workflow artifact, with its source commit and digest visible in the
workflow summary, even when the edge version is deliberately not published.
- Keep archive creation CI-owned. A locally callable validator is useful, but a second repository-local production
packager,
release/ helper tree, or custom approval schema needs a distinct consumer and lifecycle. Never remove the
stable release path while consolidating packaging ownership.
- Default workflow permissions to read-only. Scope
contents: write and publishing secrets to the release job that
needs them, and never expose a publish credential to pull-request validation.
- Run the narrowest relevant checks, then widen for the changed surface.
- For C# or project changes, run locked restore, format verification, and no-restore build. Run the documented tests
when automated tests are present or changed. Use the solution or project path required by the repository layout.
- For documentation or package text, run Markdown lint over every committed Markdown file using the checked-in
configuration.
- For workflows, composite actions, or shell scripts, run ShellCheck,
actionlint, and
pinact run --check --min-age 7; check full-SHA action pins, container digests, downloaded-tool checksums,
permissions, concurrency, and secret scope.
- Reconcile declared tooling with execution. Every committed lint/check configuration and every command promised in
README or CONTRIBUTING must have a runnable documented command and an enabled CI invocation, or be removed with the
documented reason.
- For APM changes, run
apm lock, review locked refs and hashes, run apm install --frozen, then apm audit --ci.
- For release or compatibility changes, inspect the final archive and perform runtime validation. State exactly what
environment, game version, loader version, and mod set were tested; record missing runtime evidence as a blocker
rather than guessing.
- When the product requires structured validation logging, check evidence quality as well as event coverage. A record
that proves a state transition must identify the observed execution role, boundary/source, result, and bounded
non-sensitive before/after values needed to distinguish successful application from callback reachability. Derive
authority at the event boundary; use a neutral role when unavailable. Every exception swallowed inside a patch
callback, including an inner transaction catch, emits the bounded callback-exception event in addition to domain
restoration evidence. Preserve the product's privacy exclusions.
- If the maintainer marks content for history rewriting, correct the current tree, inspect every reachable branch and
tag for the exact unwanted text, and rewrite only the authorized repository before publication or coordinated
force-push. Re-scan after rewriting. Do not disclose the removed private text in a public commit or pull request.
Use
commit-message-quality-check for reconstructed commits so required contribution attribution is preserved.
- Preserve repository visibility. Creating a repository, changing its visibility, or enabling public package or
release publication requires an explicit maintainer instruction; a migration request alone does not authorize it.
- Report the result in this order: scope and evidence consulted; findings grouped by
structure/plugin/build/CI/package/runtime; checks passed or skipped; and remaining compatibility or release risks.
State every omitted baseline item and its concrete constraint; do not report a quality pass based only on an existing
repository convention.
Reference Baseline
Read repository-derived-baseline.md when starting a new BepInEx Mono
project or when the repository does not already document its structure and quality gates. It records transferable
practices; it is a baseline, not a replacement for the target repository's contract.
Read repository-quality-template.md when creating or aligning a repository.
It turns that baseline into a checklist for ignore rules, APM, NuGet, Markdown, CI, composite actions, and conditional
Thunderstore publishing.
Read repository-family-alignment.md when the maintainer designates peer
repositories or asks for a minimal-difference rollout. It defines the delta ledger, privacy boundary, canonical-first
improvement rule, newline review, and independent convergence loop.
Read canonical-templates.md when a target can adopt an exact bundled Composite
Action or script. It defines the canonical assets, synchronization command, drift gate, and multi-repository maintenance
procedure.