# Who owns what Every bug that cost a day in this pipeline has had the same shape: two systems both setting the same thing, and whichever ran last silently won. This file says who owns each concern so the next person deletes the loser instead of adding a third writer. All four examples below are real, from 2026-08-06. ## Computer name - the PPKG owns it The provisioning package declares: ```xml F%SERIAL% ``` so a bay comes up as `F`. `run-enrollment.ps1` used to also run `Rename-Computer -NewName "E$serial"`. Both are pending renames; last writer wins at reboot. The script ran twice, and its second run landed after the package had already queued `F579C144`, so the bay came up `E579C144` with the package reporting no errors at all. **Rule:** nothing in this repo renames a machine. If the naming convention changes, it changes in the package. ## Drive letters - PESetup owns them PESetup hardcodes `W:` in nine places - every copy destination, both DISM offline sessions, `bcdboot`, `reagentc` - and creates it during its own disk preparation. `startnet.cmd` briefly had a volume finder that scanned for "the applied Windows volume" and `diskpart`-assigned it to `W:`. On a re-image it found the *previous* install, relabelled a partition PESetup was about to erase, and every staging copy failed into a volume that no longer existed. **Rule:** wait for `W:\Windows\System32\config\system` - the hive only exists once the WIM apply has written it. Never run `diskpart` while PESetup is running. See `docs/PESETUP-INTERNALS.md`. ## Enrollment - the PPKG owns it, the orchestrator drives reboots The SFLD package joins Entra using the BPRT token in `0__Accounts_Azure.provxml`. A human then assigns the device category in Intune. The vendor's `Start-BulkEnrollOrchestrator.ps1` has two branches. The normal one aborts the package's own reboot, registers `AutoSecondReboot`, and drives the Entra join to completion. The `-ManualFallback` one runs `sysprep /oobe /reboot` - it is an interactive escape hatch for handing a machine back to OOBE, meant to be triggered by a person. The shopfloor unattend registered `-ManualFallback` as an at-logon scheduled task. So OOBE completed, autologon fired, and four seconds later the machine syspreped itself back to OOBE, losing the deployment chain permanently. **Rule:** shopfloor bays enrol. Never wire `-ManualFallback` to anything automatic. ## Kiosk URLs - GE-Enforce owns them `plugins/geenforce/seed_display_scope.py` in shopdb-flask is authoritative: | display-type.txt | route | |---|---| | `Dashboard` | `/shopdb/shopfloor` | | `Lobby` | `/shopdb/tv` | | `3DPrintRoom` | `/shopdb/parts-kiosk` | Its dispatcher prefers the server-side role from Settings > Dashboard Defaults (resolved by device IP), falls back to `C:\Enrollment\display-type.txt`, writes the Startup shortcut itself, and **sweeps** any shortcut matching `shopfloor-dashboard` or `/shopdb/`. `site-config.json` also carries `edgeHomepage` per display type. Those are a backstop for the window before the kiosk installer runs - a stale value there gets deleted on the next enforce cycle rather than honoured. **Rule:** if a kiosk points at the wrong page, fix the GE-Enforce scope first. Keep `site-config.json` correct, but do not expect it to win. ## Repo vs share The share is production; the repo is meant to describe it. Drift runs both ways - live hand-edits nobody committed, and repo fixes never deployed. `scripts/share-drift.py` classifies every mapped pair as `git-owned` (repo wins, safe to push) or `unreconciled` (diverged, nobody has decided). It reports and never writes. Run it before a build day; `scripts/preflight.py` covers the rest. The unattends are `unreconciled` on purpose: the live copies are ~17 KB and the repo copies ~12 KB, so pushing the repo would regress production.