Project Planning Conventions
Context
Every VM type or cross-cutting concern gets a project plan in
docs/projects/<date>-<seq>-<name>/project_plan.md. Plans are the contract
between planning and implementation. Without structure, milestones lack
verify criteria, rollback procedures, and dependency tracking — leading to
"it works on my machine" outcomes.
Rules
- Every milestone MUST include three sections: tasks (checkbox items), verify (inline assertions to add to molecule), and rollback (how to undo the milestone).
- Every milestone MUST declare its dependency status: self-contained (no external blockers) or blocked on (lists the blocking project/milestone).
- Self-contained milestones come before blocked milestones in the ordering. Work that can ship now ships first.
- Milestone 0 of any project that introduces new testing patterns MUST establish the test infrastructure before feature work begins.
- Feature milestones MUST reference the relevant skills by name so implementers know which skills to load.
- The project plan MUST reference the architectural decisions that were made and WHY, using the tree diagram format from
docs/architecture/overview.md.
- NEVER defer all testing to a final milestone. Each milestone owns its own assertions.
- Blocked milestones SHOULD still be fully specified — they're ready to implement the moment the blocker is resolved.
- Every feature milestone MUST include an implementation pattern note specifying: which task file to create, which plays to add to
site.yml, which tags to use, and which molecule scenario to create. NEVER leave "how it integrates" as an open question.
- Per the project's "Bake, don't configure at runtime" principle (
project-structure.mdc): every package belongs in the image build, NOT at runtime. If a plan proposes opkg install or apt install during converge, reject it. Configure roles only do host-specific topology changes.
- Rollback MUST fully restore the baseline state, including auth credentials and connection methods. If a milestone changes how Ansible connects to a target (e.g., password → key auth), the rollback MUST reverse the connection method too.
- When a milestone changes the auth/connection method for a dynamic group, the plan MUST specify how subsequent plays and per-feature scenarios detect and adapt to the new auth method.
- Every plan MUST include a Milestone Dependency Graph (ASCII tree) showing the ordering and blocking relationships at a glance.
- Before execution, ALWAYS run the plan through the Plan Review Checklist (below) against all referenced skills. Previous bug: the OpenWrt router plan had two critical issues (SSH auth transition, dynamic group persistence) that would have broken implementation — both caught only by cross-referencing skills during review.
Template
# <Service Name>
## Overview
2-3 sentences: what this is, current state, what this project adds.
## Type
VM (KVM/QEMU) | LXC container | Cross-cutting infrastructure
## Resources (for VM/LXC projects)
- Cores, RAM, Disk, Network, PCI, VMID
## Startup (for VM/LXC projects)
- Auto-start, boot priority, dependencies
## Build Profiles
Which build profiles include this service.
## Prerequisites
What must exist before this project starts.
## Skills
Table of relevant skills with when-to-use descriptions.
---
## Architectural Decisions
Tree diagram of decisions with rationale (leaf nodes).
---
## Milestones
### Milestone 0: <Foundation Work>
_Self-contained._
Description of infrastructure/scaffolding this project needs.
- [ ] Task items as checkboxes
**Verify:**
- [ ] Assertions to add to molecule verify
**Rollback:**
How to undo this milestone.
### Milestone N: <Feature>
_Self-contained._ or _Blocked on: <project/milestone>._
Description.
See: `<skill-name>` skill.
**Implementation pattern:**
- Task file: `roles/<type>_configure/tasks/<feature>.yml`
- site.yml plays: (1) configure on `<dynamic_group>`, tag `<feature>` (2) deploy_stamp on `<flavor_group>`, tag `<feature>`
- Molecule scenario: `molecule/<type>-<feature>/`
- [ ] Task items
**Verify:**
- [ ] Assertions
**Rollback (`--tags <feature>-rollback`):**
Steps to reverse the feature (including auth/credential state if changed).
Milestone dependency graph
Include an ASCII tree showing milestone ordering at the top of the
Milestones section:
## Milestone dependency graph
M0 (test infra)
├── M1 (security) ← self-contained
├── M2 (VLANs) ← self-contained
├── M3 (encrypted DNS) ← self-contained
├── M4 (mesh) ← self-contained
├── M5 (pihole DNS) ← blocked on Pi-hole LXC project
├── M6 (syslog) ← blocked on Netdata project
├── M7 (monitoring) ← blocked on Netdata project
└── M8 (docs + integration)
Dependency tracking
Mark each milestone clearly:
_Self-contained. No external dependencies._
_Blocked on: Pi-hole LXC project (2026-03-09-03). Cannot test DNS
forwarding chain without a running Pi-hole instance._
Blocked milestones appear after all self-contained milestones. Within
each group, order by logical dependency (security before VLANs before
DNS, since VLANs may affect DNS zone config).
When implementing a project, blocked milestones SHOULD be moved to
their downstream projects rather than kept as stubs. Stubs in
site.yml, cleanup.yml, and task files create dead code that
confuses future maintainers. Instead, document the integration point
in the architecture docs (e.g., "Pi-hole DNS forwarding play will be
added by the Pi-hole LXC project"). The downstream project owns both
its own infrastructure AND the OpenWrt integration plays.
Previous bug: M5-M7 stubs (pihole_dns, syslog, monitoring) were
implemented as task files + site.yml plays + cleanup.yml rollback
plays, then had to be removed entirely because they belonged in their
respective downstream projects.
Task ordering within milestones
Every milestone's task list MUST follow dependency order. Walk through each
task and ask: "What must already exist for this to succeed?"
Canonical ordering for a configure milestone:
- Fix system baseline state (broken packages, missing modules)
- Install packages
- Generate keys/credentials (requires package tools like
wg genkey)
- Template configuration files (requires generated keys)
- Start/enable services (requires config files)
- Configure runtime state (firewall rules, sysctl, NAT — requires services)
- Persist runtime state (save iptables rules, write generated env file)
For a provisioning milestone:
- Load host-side kernel modules (LXC shares host kernel)
- Upload images/templates
- Create VM/container
- Configure auto-start
- Start VM/container
- Clean template baseline (fix broken packages in LXC)
- Register in dynamic inventory
NEVER put key generation before package installation. NEVER put service
start before configuration. NEVER install packages before fixing broken
system state.
See: task-ordering rule for the full ordering reference and common mistakes.
Secret generation in milestones
When a milestone generates secrets (keys, tokens, PSKs):
- Document which env vars are auto-generated and which require user input
- Specify the generated file:
test.env.generated (test) or
.env.generated (production), auto-detected via env_generated_path
- Include a verify assertion checking the generated file exists and
contains the expected keys
- Include cleanup of the generated file in rollback
See: secret-generation rule for the full pattern.
Milestone sizing
Each milestone should be completable in a single focused session
(2-4 hours). If a milestone has more than 8-10 checkbox items, split
it. If it has fewer than 3, merge it with an adjacent milestone.
Plan review checklist
Before considering a plan ready for execution, verify each item.
Structural checks
- Dynamic group persistence: Do any plays target dynamic groups
(
openwrt, pihole, etc.)? If so, every entry point that runs as a
separate ansible-playbook invocation (per-feature converge, verify,
cleanup/rollback) MUST reconstruct the group. add_host is ephemeral.
- Auth transitions: Does any milestone change how Ansible connects
to a target (password → key, add SSH key, disable password)? If so:
- The milestone MUST specify the exact ordering (deploy → verify → lock)
- The milestone MUST re-register the host via
add_host with new args
- Subsequent milestones MUST detect which auth method is active
- Rollback MUST restore the original auth method
- Rollback completeness: Does each rollback section undo EVERYTHING
the milestone changed? Check: UCI config, packages, files, auth state,
cron jobs, service enablement. A partial rollback leaves the system in
an undefined state.
- Skill rule compliance: For each referenced skill, scan its Rules
section. Every applicable NEVER/ALWAYS constraint should be reflected
in the task items (e.g., retry/delay for opkg, pipefail for shell tasks,
detached scripts for firewall restarts).
- Implementation pattern: Does every feature milestone specify the
task file name,
site.yml play structure (target group + tags), and
deploy_stamp pairing? Ambiguity here causes inconsistent implementations.
- Molecule scenario: Does every feature milestone that adds testable
behavior also create a per-feature molecule scenario?
- No fallback paths: Does the plan introduce any "try X, fall back to Y"
logic? If so, reject it. One tested path per feature. Missing prerequisites
should fail with an actionable error message, not silently degrade.
- Verify from the right host: Do verify assertions run on the Proxmox
host (via
qm, shell commands) or inside the VM (via dynamic group)?
If inside the VM, the verify needs group reconstruction too.
Container/VM capability checks
- LXC features and capabilities: If the plan provisions an LXC
container, verify that required features are declared:
nesting=1: needed for iptables/nftables inside unprivileged
containers AND for systemd services that use sandboxing directives
(LogNamespace, ProtectSystem, ProtectHome, ProtectControlGroups,
BindReadOnlyPaths). Most Debian-packaged services use these.
mount=cgroup: needed for cgroup mounts (systemd containers)
keyctl=1: needed for kernel key management
- If the service uses systemd sandboxing (most Debian services), the
plan MUST also include a systemd drop-in override baked into the image
via
build-images.sh. NEVER deploy the override via the configure
role — it's base system config (identical on every container) and
each pct_remote task adds 15-60s overhead. nesting=1 alone does
not fix all directives (e.g., LogNamespace still fails).
- If no special features are needed, the plan MUST explicitly state
"no special LXC features required" so reviewers don't wonder.
Previous bug: WireGuard plan omitted
nesting=1. The iptables -t nat MASQUERADE command in M2 would have failed with "Permission denied"
at runtime.
Previous bug: Netdata LXC exited 226/NAMESPACE even as privileged.
Root cause: LogNamespace=netdata in the systemd unit requires
CLONE_NEWNS, forbidden without nesting=1. Fix: nesting=1 +
systemd override clearing LogNamespace.
- Bake, don't configure at runtime (per
project-structure.mdc): if
the plan mentions runtime package installation (opkg install,
apt install), reject it. Packages AND base configuration belong in
the image build. Configure roles only apply host-specific topology
(IPs, bridges, subnets, forwarding targets). If software is already
in the base OS (e.g., rsyslog in Debian), the image build should
pre-configure it — the configure role should not set up listeners,
spool directories, or logrotate from scratch.
- Kernel module host-side loading: If the service needs kernel modules
(WireGuard, VFIO, GPU drivers), verify the plan loads them on the
Proxmox HOST (not inside the container — LXC shares the host kernel).
Include persistence via
/etc/modules-load.d/ and cleanup removal of
both the config file and modprobe -r.
- WiFi PHY namespace move: If the plan provisions an LXC container
that needs WiFi access, verify:
- Container is privileged (
unprivileged: false) — namespace moves
require CAP_NET_ADMIN
--ostype unmanaged for OpenWrt containers (Proxmox can't auto-detect)
lxc_ct_skip_debian_cleanup: true for non-Debian containers
- WiFi driver loading on the host before PHY detection
- Hard-fail if no WiFi PHY found (all wifi_nodes must have WiFi)
- Hookscript for PHY persistence across container restarts
proxmox_pci_passthrough must clean stale vfio-pci bindings on
non-router hosts before the mesh role runs
Previous bug: mesh1 WiFi was invisible because stale vfio-pci.conf
and blacklist-wifi.conf from a prior run kept iwlwifi blacklisted
and the device bound to vfio-pci.
Image build checks
- Image build milestone: Does the plan include an image build
milestone (M0) with a
build-images.sh section? Every service needs
a purpose-built image, even if the software is pre-installed in the
base OS. The image build bakes in default configuration, spool
directories, and service-specific setup. If the plan says "pre-installed
on Debian, no build needed", reject it — the IMAGE may not need extra
packages, but it DOES need pre-configuration.
Previous bug: rsyslog plan skipped M0 because "rsyslog is pre-installed
on Debian." But the configure role then had to create spool directories,
enable TCP modules, and set up logrotate at runtime — all of which
belong in the image.
- Image template variables: Does the plan define custom template
variables (
<type>_lxc_template, <type>_lxc_template_path) in
group_vars/all.yml? Using proxmox_lxc_default_template directly
bypasses the image verification gate. Every service should reference
its own template variable so the provision role can hard-fail if the
custom image is missing.
Cross-reference checks
- Prerequisite verification: Grep the codebase to confirm claimed
prerequisites actually exist: VMIDs in
group_vars/all.yml, flavor
groups in inventory/hosts.yml, dynamic groups in inventory, platform
groups in molecule/default/molecule.yml.
- site.yml play ordering: Verify the proposed play position doesn't
conflict with existing plays. Count the actual play numbers. Every
service play runs during normal converge — NEVER use Ansible's
never
tag. Tags are for selective invocation via --tags, not for exclusion.
- Tag collision: Verify proposed tags don't collide with existing
tags in
site.yml or cleanup.yml.
- Shared tags: If the plan uses a tag shared with another service
(e.g.,
[monitoring] for rsyslog + netdata), document this is
intentional and explain the implication: you cannot deploy just one
of the services via tag once both exist in site.yml.
- Cleanup parity: Verify that files deployed by the new roles are
added to BOTH
molecule/default/cleanup.yml AND playbooks/cleanup.yml.
Also verify playbooks/cleanup.yml gets rollback tags if the service
supports per-feature rollback.
- Architecture doc consistency: Verify the plan's plays, tags, and
resource allocations match
overview.md's target site.yml and VM
table. Flag any discrepancies.
Completeness checks
- Gitignore coverage: If the plan creates new generated or state
files (e.g.,
.env.generated, .state/), verify they are already in
.gitignore. Don't add redundant task items for files already covered.
- Predictable IP for consumers: If the service will be consumed by
other services (routing, DNS forwarding, log collection), verify the
plan addresses how consumers discover a stable IP (static lease, DNS,
etc.). Document this even if the solution is deferred to a downstream
project.
- Future integration notes: If the service introduces a new pattern
(e.g.,
.env.generated accumulation, NAT routing), add a "Future
Integration Considerations" section documenting how downstream
projects should interact with it.
- Network topology assumption: Does the plan document which host
topologies the service supports? If the flavor group could include
hosts both behind OpenWrt (LAN) and directly on WAN, the plan MUST
specify the topology branching strategy (bridge, subnet, gateway,
DNS). A "Network topology assumption" section is required for all
LXC container and VM plans.
Previous bug: rsyslog plan only said "static IP on LAN bridge" but
monitoring_nodes appears in ALL build profiles including Gaming
Rig (no OpenWrt). WAN-connected hosts need different bridge, gateway,
and DNS settings.
- Container IP offset allocation: If the service uses static IPs
computed from an offset, verify the offset is defined in
group_vars/all.yml and doesn't collide with existing allocations.
Current allocations: WireGuard 3–6, Pi-hole 10, rsyslog 12, Netdata
13, HA 14, Jellyfin 15, MeshWiFi 20. WAN offsets add +200. Check WAN
offset against physical host IPs on the supernet.
- Milestone consolidation: Are any milestones redundant? Provisioning
and site.yml integration should be in the SAME milestone (not split).
Per-feature rollback plays in cleanup.yml should be in the testing
milestone (where per-feature scenarios are defined), not a separate
milestone.
- Deferred work ownership: If the plan contains milestones that are
"blocked on" downstream projects, verify they are explicitly deferred
to those projects with a clear integration point. Stubs in site.yml,
cleanup.yml, and task files create dead code.
Previous bug: OpenWrt M5-M7 stubs (pihole_dns, syslog, monitoring)
were implemented then removed because they belonged downstream.
- Testing Strategy section: Every plan MUST include a "Testing
Strategy" section with: (a) parallelism in
molecule/default, (b)
per-feature scenario hierarchy, (c) day-to-day workflow (bash
commands), (d) teardown table showing what each scenario creates and
destroys and its baseline impact.
- Documented exceptions to bake principle: Three documented
exceptions exist. Plans using these MUST explicitly state the
exception and rationale:
- Docker pull of pinned image tag: deterministic, versioned,
idempotent (e.g., Home Assistant pre-pulls HA container image)
- Desktop LXC via build-images.sh: rootfs tarball with all packages
(KDE, GNOME, KasmVNC) baked in; GPU drivers selected at build time
- Windows VMs via ISO + autounattend.xml: install-from-ISO IS
the bake approach for Windows — deterministic, unattended, with
drivers pre-injected
Any OTHER runtime package installation is still rejected.
- Cross-cutting milestone ownership: If the plan deploys shared
infrastructure (e.g., display-exclusive hookscript), identify which
project OWNS the deployment and which projects only ATTACH. Only one
project deploys; others reference it. Document the owning project
explicitly.
Previous bug: Kodi, Moonlight, and Desktop LXC plans all had tasks to
deploy the display-exclusive hookscript. Consolidated ownership to
the Kiosk project.
- Separate hardware topology: If the service runs on separate
physical hardware (e.g., Gaming Rig), the plan MUST document the
hardware topology separately. Separate hardware may lack OpenWrt,
may use a different build profile, and may require hardware-dependent
testing strategies (skip when hardware unavailable).
- VA-API driver portability: Image builds for services that use
iGPU (Jellyfin, Kodi, Moonlight) SHOULD include BOTH Intel and AMD
VA-API driver packages. At runtime, only the matching driver loads.
This avoids rebuilding images when hardware changes.
- Configure role task budget: For LXC services, count the tasks in
the proposed configure role. Each
pct_remote task adds 15-60s of
overhead. If a task deploys config that is identical across all
containers (systemd overrides, base config files, logrotate), move it
to the image build. Only host-specific config (IPs, keys, optional
features) belongs in the configure role.
Previous bug: Netdata configure role had 3 extra tasks for systemd
override (mkdir + copy + daemon_reload). Across 4 nodes, this added
~3 minutes per test run. Moving to the image saved 38% of test time.
- LXC disk sizing: Verify the planned rootfs disk size accommodates
the EXTRACTED template, not just the compressed size. Compressed
templates can be 3-5x smaller. Minimum 2GB for services with databases
or monitoring data.
Cross-references
- Reference skills inline:
See: rollback-patterns skill.
- Reference architecture docs:
See: docs/architecture/overview.md, Network Topology.
- Reference other project plans by their directory name:
Blocked on: 2026-03-09-00-shared-infrastructure, Milestone 1.
1---2name: project-planning3description: Template and conventions for vm_builds project plans. Use when creating, reviewing, or updating project plans in docs/projects/, when adding a new VM or service type, or when structuring milestones with verify/rollback sections.4---56# Project Planning Conventions78## Context910Every VM type or cross-cutting concern gets a project plan in11`docs/projects/<date>-<seq>-<name>/project_plan.md`. Plans are the contract12between planning and implementation. Without structure, milestones lack13verify criteria, rollback procedures, and dependency tracking — leading to14"it works on my machine" outcomes.1516## Rules17181. Every milestone MUST include three sections: **tasks** (checkbox items), **verify** (inline assertions to add to molecule), and **rollback** (how to undo the milestone).192. Every milestone MUST declare its dependency status: **self-contained** (no external blockers) or **blocked on** (lists the blocking project/milestone).203. Self-contained milestones come before blocked milestones in the ordering. Work that can ship now ships first.214. Milestone 0 of any project that introduces new testing patterns MUST establish the test infrastructure before feature work begins.225. Feature milestones MUST reference the relevant skills by name so implementers know which skills to load.236. The project plan MUST reference the architectural decisions that were made and WHY, using the tree diagram format from `docs/architecture/overview.md`.247. NEVER defer all testing to a final milestone. Each milestone owns its own assertions.258. Blocked milestones SHOULD still be fully specified — they're ready to implement the moment the blocker is resolved.269. Every feature milestone MUST include an **implementation pattern** note specifying: which task file to create, which plays to add to `site.yml`, which tags to use, and which molecule scenario to create. NEVER leave "how it integrates" as an open question.2710. Per the project's "Bake, don't configure at runtime" principle (`project-structure.mdc`): every package belongs in the image build, NOT at runtime. If a plan proposes `opkg install` or `apt install` during converge, reject it. Configure roles only do host-specific topology changes.2811. Rollback MUST fully restore the baseline state, including auth credentials and connection methods. If a milestone changes how Ansible connects to a target (e.g., password → key auth), the rollback MUST reverse the connection method too.2912. When a milestone changes the auth/connection method for a dynamic group, the plan MUST specify how subsequent plays and per-feature scenarios detect and adapt to the new auth method.3013. Every plan MUST include a **Milestone Dependency Graph** (ASCII tree) showing the ordering and blocking relationships at a glance.3114. Before execution, ALWAYS run the plan through the **Plan Review Checklist** (below) against all referenced skills. Previous bug: the OpenWrt router plan had two critical issues (SSH auth transition, dynamic group persistence) that would have broken implementation — both caught only by cross-referencing skills during review.3233## Template3435```markdown36# <Service Name>3738## Overview392-3 sentences: what this is, current state, what this project adds.4041## Type42VM (KVM/QEMU) | LXC container | Cross-cutting infrastructure4344## Resources (for VM/LXC projects)45- Cores, RAM, Disk, Network, PCI, VMID4647## Startup (for VM/LXC projects)48- Auto-start, boot priority, dependencies4950## Build Profiles51Which build profiles include this service.5253## Prerequisites54What must exist before this project starts.5556## Skills57Table of relevant skills with when-to-use descriptions.5859---6061## Architectural Decisions62Tree diagram of decisions with rationale (leaf nodes).6364---6566## Milestones6768### Milestone 0: <Foundation Work>69_Self-contained._70Description of infrastructure/scaffolding this project needs.7172- [ ] Task items as checkboxes7374**Verify:**75- [ ] Assertions to add to molecule verify7677**Rollback:**78How to undo this milestone.7980### Milestone N: <Feature>81_Self-contained._ or _Blocked on: <project/milestone>._82Description.8384See: `<skill-name>` skill.8586**Implementation pattern:**87- Task file: `roles/<type>_configure/tasks/<feature>.yml`88- site.yml plays: (1) configure on `<dynamic_group>`, tag `<feature>` (2) deploy_stamp on `<flavor_group>`, tag `<feature>`89- Molecule scenario: `molecule/<type>-<feature>/`9091- [ ] Task items9293**Verify:**94- [ ] Assertions9596**Rollback (`--tags <feature>-rollback`):**97Steps to reverse the feature (including auth/credential state if changed).98```99100## Milestone dependency graph101102Include an ASCII tree showing milestone ordering at the top of the103Milestones section:104105```markdown106## Milestone dependency graph107M0 (test infra)108├── M1 (security) ← self-contained109├── M2 (VLANs) ← self-contained110├── M3 (encrypted DNS) ← self-contained111├── M4 (mesh) ← self-contained112├── M5 (pihole DNS) ← blocked on Pi-hole LXC project113├── M6 (syslog) ← blocked on Netdata project114├── M7 (monitoring) ← blocked on Netdata project115└── M8 (docs + integration)116```117118## Dependency tracking119120Mark each milestone clearly:121122```markdown123_Self-contained. No external dependencies._124```125126```markdown127_Blocked on: Pi-hole LXC project (2026-03-09-03). Cannot test DNS128forwarding chain without a running Pi-hole instance._129```130131Blocked milestones appear after all self-contained milestones. Within132each group, order by logical dependency (security before VLANs before133DNS, since VLANs may affect DNS zone config).134135When implementing a project, blocked milestones SHOULD be moved to136their downstream projects rather than kept as stubs. Stubs in137`site.yml`, `cleanup.yml`, and task files create dead code that138confuses future maintainers. Instead, document the integration point139in the architecture docs (e.g., "Pi-hole DNS forwarding play will be140added by the Pi-hole LXC project"). The downstream project owns both141its own infrastructure AND the OpenWrt integration plays.142143Previous bug: M5-M7 stubs (pihole_dns, syslog, monitoring) were144implemented as task files + site.yml plays + cleanup.yml rollback145plays, then had to be removed entirely because they belonged in their146respective downstream projects.147148## Task ordering within milestones149150Every milestone's task list MUST follow dependency order. Walk through each151task and ask: "What must already exist for this to succeed?"152153Canonical ordering for a configure milestone:1541. Fix system baseline state (broken packages, missing modules)1552. Install packages1563. Generate keys/credentials (requires package tools like `wg genkey`)1574. Template configuration files (requires generated keys)1585. Start/enable services (requires config files)1596. Configure runtime state (firewall rules, sysctl, NAT — requires services)1607. Persist runtime state (save iptables rules, write generated env file)161162For a provisioning milestone:1631. Load host-side kernel modules (LXC shares host kernel)1642. Upload images/templates1653. Create VM/container1664. Configure auto-start1675. Start VM/container1686. Clean template baseline (fix broken packages in LXC)1697. Register in dynamic inventory170171NEVER put key generation before package installation. NEVER put service172start before configuration. NEVER install packages before fixing broken173system state.174175See: `task-ordering` rule for the full ordering reference and common mistakes.176177## Secret generation in milestones178179When a milestone generates secrets (keys, tokens, PSKs):1801. Document which env vars are auto-generated and which require user input1812. Specify the generated file: `test.env.generated` (test) or182 `.env.generated` (production), auto-detected via `env_generated_path`1833. Include a verify assertion checking the generated file exists and184 contains the expected keys1854. Include cleanup of the generated file in rollback186187See: `secret-generation` rule for the full pattern.188189## Milestone sizing190191Each milestone should be completable in a single focused session192(2-4 hours). If a milestone has more than 8-10 checkbox items, split193it. If it has fewer than 3, merge it with an adjacent milestone.194195## Plan review checklist196197Before considering a plan ready for execution, verify each item.198199### Structural checks2002011. **Dynamic group persistence**: Do any plays target dynamic groups202 (`openwrt`, `pihole`, etc.)? If so, every entry point that runs as a203 separate `ansible-playbook` invocation (per-feature converge, verify,204 cleanup/rollback) MUST reconstruct the group. `add_host` is ephemeral.2052. **Auth transitions**: Does any milestone change how Ansible connects206 to a target (password → key, add SSH key, disable password)? If so:207 - The milestone MUST specify the exact ordering (deploy → verify → lock)208 - The milestone MUST re-register the host via `add_host` with new args209 - Subsequent milestones MUST detect which auth method is active210 - Rollback MUST restore the original auth method2113. **Rollback completeness**: Does each rollback section undo EVERYTHING212 the milestone changed? Check: UCI config, packages, files, auth state,213 cron jobs, service enablement. A partial rollback leaves the system in214 an undefined state.2154. **Skill rule compliance**: For each referenced skill, scan its Rules216 section. Every applicable NEVER/ALWAYS constraint should be reflected217 in the task items (e.g., retry/delay for opkg, pipefail for shell tasks,218 detached scripts for firewall restarts).2195. **Implementation pattern**: Does every feature milestone specify the220 task file name, `site.yml` play structure (target group + tags), and221 `deploy_stamp` pairing? Ambiguity here causes inconsistent implementations.2226. **Molecule scenario**: Does every feature milestone that adds testable223 behavior also create a per-feature molecule scenario?2247. **No fallback paths**: Does the plan introduce any "try X, fall back to Y"225 logic? If so, reject it. One tested path per feature. Missing prerequisites226 should fail with an actionable error message, not silently degrade.2278. **Verify from the right host**: Do verify assertions run on the Proxmox228 host (via `qm`, shell commands) or inside the VM (via dynamic group)?229 If inside the VM, the verify needs group reconstruction too.230231### Container/VM capability checks2322339. **LXC features and capabilities**: If the plan provisions an LXC234 container, verify that required features are declared:235 - `nesting=1`: needed for iptables/nftables inside unprivileged236 containers AND for systemd services that use sandboxing directives237 (`LogNamespace`, `ProtectSystem`, `ProtectHome`, `ProtectControlGroups`,238 `BindReadOnlyPaths`). Most Debian-packaged services use these.239 - `mount=cgroup`: needed for cgroup mounts (systemd containers)240 - `keyctl=1`: needed for kernel key management241 - If the service uses systemd sandboxing (most Debian services), the242 plan MUST also include a systemd drop-in override baked into the image243 via `build-images.sh`. NEVER deploy the override via the configure244 role — it's base system config (identical on every container) and245 each `pct_remote` task adds 15-60s overhead. `nesting=1` alone does246 not fix all directives (e.g., `LogNamespace` still fails).247 - If no special features are needed, the plan MUST explicitly state248 "no special LXC features required" so reviewers don't wonder.249 Previous bug: WireGuard plan omitted `nesting=1`. The `iptables -t nat250 MASQUERADE` command in M2 would have failed with "Permission denied"251 at runtime.252 Previous bug: Netdata LXC exited 226/NAMESPACE even as privileged.253 Root cause: `LogNamespace=netdata` in the systemd unit requires254 `CLONE_NEWNS`, forbidden without `nesting=1`. Fix: `nesting=1` +255 systemd override clearing `LogNamespace`.25610. **Bake, don't configure at runtime** (per `project-structure.mdc`): if257 the plan mentions runtime package installation (`opkg install`,258 `apt install`), reject it. Packages AND base configuration belong in259 the image build. Configure roles only apply host-specific topology260 (IPs, bridges, subnets, forwarding targets). If software is already261 in the base OS (e.g., rsyslog in Debian), the image build should262 pre-configure it — the configure role should not set up listeners,263 spool directories, or logrotate from scratch.26411. **Kernel module host-side loading**: If the service needs kernel modules265 (WireGuard, VFIO, GPU drivers), verify the plan loads them on the266 Proxmox HOST (not inside the container — LXC shares the host kernel).267 Include persistence via `/etc/modules-load.d/` and cleanup removal of268 both the config file and `modprobe -r`.26912. **WiFi PHY namespace move**: If the plan provisions an LXC container270 that needs WiFi access, verify:271 - Container is privileged (`unprivileged: false`) — namespace moves272 require CAP_NET_ADMIN273 - `--ostype unmanaged` for OpenWrt containers (Proxmox can't auto-detect)274 - `lxc_ct_skip_debian_cleanup: true` for non-Debian containers275 - WiFi driver loading on the host before PHY detection276 - Hard-fail if no WiFi PHY found (all wifi_nodes must have WiFi)277 - Hookscript for PHY persistence across container restarts278 - `proxmox_pci_passthrough` must clean stale vfio-pci bindings on279 non-router hosts before the mesh role runs280 Previous bug: mesh1 WiFi was invisible because stale vfio-pci.conf281 and blacklist-wifi.conf from a prior run kept iwlwifi blacklisted282 and the device bound to vfio-pci.283284### Image build checks28528613. **Image build milestone**: Does the plan include an image build287 milestone (M0) with a `build-images.sh` section? Every service needs288 a purpose-built image, even if the software is pre-installed in the289 base OS. The image build bakes in default configuration, spool290 directories, and service-specific setup. If the plan says "pre-installed291 on Debian, no build needed", reject it — the IMAGE may not need extra292 packages, but it DOES need pre-configuration.293 Previous bug: rsyslog plan skipped M0 because "rsyslog is pre-installed294 on Debian." But the configure role then had to create spool directories,295 enable TCP modules, and set up logrotate at runtime — all of which296 belong in the image.29714. **Image template variables**: Does the plan define custom template298 variables (`<type>_lxc_template`, `<type>_lxc_template_path`) in299 `group_vars/all.yml`? Using `proxmox_lxc_default_template` directly300 bypasses the image verification gate. Every service should reference301 its own template variable so the provision role can hard-fail if the302 custom image is missing.303304### Cross-reference checks30530615. **Prerequisite verification**: Grep the codebase to confirm claimed307 prerequisites actually exist: VMIDs in `group_vars/all.yml`, flavor308 groups in `inventory/hosts.yml`, dynamic groups in inventory, platform309 groups in `molecule/default/molecule.yml`.31016. **site.yml play ordering**: Verify the proposed play position doesn't311 conflict with existing plays. Count the actual play numbers. Every312 service play runs during normal converge — NEVER use Ansible's `never`313 tag. Tags are for selective invocation via `--tags`, not for exclusion.31417. **Tag collision**: Verify proposed tags don't collide with existing315 tags in `site.yml` or `cleanup.yml`.31618. **Shared tags**: If the plan uses a tag shared with another service317 (e.g., `[monitoring]` for rsyslog + netdata), document this is318 intentional and explain the implication: you cannot deploy just one319 of the services via tag once both exist in `site.yml`.32019. **Cleanup parity**: Verify that files deployed by the new roles are321 added to BOTH `molecule/default/cleanup.yml` AND `playbooks/cleanup.yml`.322 Also verify `playbooks/cleanup.yml` gets rollback tags if the service323 supports per-feature rollback.32420. **Architecture doc consistency**: Verify the plan's plays, tags, and325 resource allocations match `overview.md`'s target site.yml and VM326 table. Flag any discrepancies.327328### Completeness checks32933021. **Gitignore coverage**: If the plan creates new generated or state331 files (e.g., `.env.generated`, `.state/`), verify they are already in332 `.gitignore`. Don't add redundant task items for files already covered.33322. **Predictable IP for consumers**: If the service will be consumed by334 other services (routing, DNS forwarding, log collection), verify the335 plan addresses how consumers discover a stable IP (static lease, DNS,336 etc.). Document this even if the solution is deferred to a downstream337 project.33823. **Future integration notes**: If the service introduces a new pattern339 (e.g., `.env.generated` accumulation, NAT routing), add a "Future340 Integration Considerations" section documenting how downstream341 projects should interact with it.34224. **Network topology assumption**: Does the plan document which host343 topologies the service supports? If the flavor group could include344 hosts both behind OpenWrt (LAN) and directly on WAN, the plan MUST345 specify the topology branching strategy (bridge, subnet, gateway,346 DNS). A "Network topology assumption" section is required for all347 LXC container and VM plans.348 Previous bug: rsyslog plan only said "static IP on LAN bridge" but349 `monitoring_nodes` appears in ALL build profiles including Gaming350 Rig (no OpenWrt). WAN-connected hosts need different bridge, gateway,351 and DNS settings.35225. **Container IP offset allocation**: If the service uses static IPs353 computed from an offset, verify the offset is defined in354 `group_vars/all.yml` and doesn't collide with existing allocations.355 Current allocations: WireGuard 3–6, Pi-hole 10, rsyslog 12, Netdata356 13, HA 14, Jellyfin 15, MeshWiFi 20. WAN offsets add +200. Check WAN357 offset against physical host IPs on the supernet.35826. **Milestone consolidation**: Are any milestones redundant? Provisioning359 and site.yml integration should be in the SAME milestone (not split).360 Per-feature rollback plays in cleanup.yml should be in the testing361 milestone (where per-feature scenarios are defined), not a separate362 milestone.36327. **Deferred work ownership**: If the plan contains milestones that are364 "blocked on" downstream projects, verify they are explicitly deferred365 to those projects with a clear integration point. Stubs in site.yml,366 cleanup.yml, and task files create dead code.367 Previous bug: OpenWrt M5-M7 stubs (pihole_dns, syslog, monitoring)368 were implemented then removed because they belonged downstream.36928. **Testing Strategy section**: Every plan MUST include a "Testing370 Strategy" section with: (a) parallelism in `molecule/default`, (b)371 per-feature scenario hierarchy, (c) day-to-day workflow (bash372 commands), (d) teardown table showing what each scenario creates and373 destroys and its baseline impact.37429. **Documented exceptions to bake principle**: Three documented375 exceptions exist. Plans using these MUST explicitly state the376 exception and rationale:377 - **Docker pull of pinned image tag**: deterministic, versioned,378 idempotent (e.g., Home Assistant pre-pulls HA container image)379 - **Desktop LXC via build-images.sh**: rootfs tarball with all packages380 (KDE, GNOME, KasmVNC) baked in; GPU drivers selected at build time381 - **Windows VMs via ISO + autounattend.xml**: install-from-ISO IS382 the bake approach for Windows — deterministic, unattended, with383 drivers pre-injected384 Any OTHER runtime package installation is still rejected.38530. **Cross-cutting milestone ownership**: If the plan deploys shared386 infrastructure (e.g., display-exclusive hookscript), identify which387 project OWNS the deployment and which projects only ATTACH. Only one388 project deploys; others reference it. Document the owning project389 explicitly.390 Previous bug: Kodi, Moonlight, and Desktop LXC plans all had tasks to391 deploy the display-exclusive hookscript. Consolidated ownership to392 the Kiosk project.39331. **Separate hardware topology**: If the service runs on separate394 physical hardware (e.g., Gaming Rig), the plan MUST document the395 hardware topology separately. Separate hardware may lack OpenWrt,396 may use a different build profile, and may require hardware-dependent397 testing strategies (skip when hardware unavailable).39832. **VA-API driver portability**: Image builds for services that use399 iGPU (Jellyfin, Kodi, Moonlight) SHOULD include BOTH Intel and AMD400 VA-API driver packages. At runtime, only the matching driver loads.401 This avoids rebuilding images when hardware changes.40233. **Configure role task budget**: For LXC services, count the tasks in403 the proposed configure role. Each `pct_remote` task adds 15-60s of404 overhead. If a task deploys config that is identical across all405 containers (systemd overrides, base config files, logrotate), move it406 to the image build. Only host-specific config (IPs, keys, optional407 features) belongs in the configure role.408 Previous bug: Netdata configure role had 3 extra tasks for systemd409 override (mkdir + copy + daemon_reload). Across 4 nodes, this added410 ~3 minutes per test run. Moving to the image saved 38% of test time.41134. **LXC disk sizing**: Verify the planned rootfs disk size accommodates412 the EXTRACTED template, not just the compressed size. Compressed413 templates can be 3-5x smaller. Minimum 2GB for services with databases414 or monitoring data.415416## Cross-references417418- Reference skills inline: `See: rollback-patterns skill.`419- Reference architecture docs: `See: docs/architecture/overview.md, Network Topology.`420- Reference other project plans by their directory name:421 `Blocked on: 2026-03-09-00-shared-infrastructure, Milestone 1.`