Architecture Finding
Type: tech-debt / duplicated-abstraction
Affected area: system_files/shared/usr/bin/ublue-system-setup, system_files/shared/usr/bin/ublue-user-setup, system_files/shared/usr/bin/ublue-privileged-setup, system_files/shared/usr/lib/ublue/setup-services/
The three first-boot setup services are the same dispatcher written out three times. Each is 28–29 lines and contains its own private copy of:
- an 11-line
get_config() jq reader over /etc/ublue-os/setup.json
- the
HOOKS_VERBOSE / set -x handling
- the
if [ -d ... ]; then for script in "$DIR"/*; do bash "$script"; done; fi dispatch loop
diff proves it — the only variation across all three files is one config key and one default directory:
$ diff ublue-system-setup ublue-user-setup
17c17
< SYSTEM_HOOKS_DIRECTORY="$(get_config '."system-hooks-directory"' "/usr/share/ublue-os/system-setup.hooks.d")"
---
> USER_HOOKS_DIRECTORY="$(get_config '."user-hooks-directory"' "/usr/share/ublue-os/user-setup.hooks.d")"
(plus the corresponding variable name in the loop). ublue-privileged-setup differs from ublue-system-setup by exactly the same two lines.
The repo already has the right home for this: system_files/shared/usr/lib/ublue/setup-services/libsetup.sh is the established shared library for setup services, sourced by every hook in *-setup.hooks.d/. The dispatchers never adopted it.
The test suite is triplicated to match — tests/test_setup_scripts.bats and tests/test_privileged_setup.bats each restate "get_config returns fallback", "runs hooks in hooks directory", "exits cleanly when hooks directory missing" against a different copy of the same code.
Impact
- Defects ship three times. The duplicated dispatch loop has a latent bug that is present in all three copies: when the hooks directory exists but is empty, the unquoted glob does not expand and the loop runs
bash "$DIR/*", exiting 127. Reproduced against main:
$ SETUP_CONFIG_FILE=$W/setup.json bash system_files/shared/usr/bin/ublue-system-setup
bash: /tmp/.../hooks/*: No such file or directory
EXIT=127
Fixing it requires the same edit in three files, and neither test file covers the empty-directory case. Filed separately as the behavioural defect.
- Any change to hook discovery (ordering, executable-bit filtering, error isolation between hooks, config schema) must be made three times and can silently land in only one or two.
- The three services already diverge cosmetically (a stray comment in
ublue-user-setup, tab-vs-space indentation in the tests), which is how semantic divergence starts.
- Downstream variants (
bluefin, bluefin-lts, dakota, server, forks) all inherit these three scripts from system_files/shared/, so drift propagates org-wide.
Recommendation
Extract the dispatcher into system_files/shared/usr/lib/ublue/setup-services/hookrunner.sh exposing get_config() and run_setup_hooks <config-key> <default-directory>, and reduce the three /usr/bin/ublue-*-setup entry points to thin wrappers that supply only their key and default. Source it via an overridable $HOOKRUNNER, matching the existing $LIBSETUP convention in system-setup.hooks.d/10-framework.sh. Add a structural guard test so a private get_config or dispatch loop cannot be reintroduced.
Filed by architect agent (ACMM L5 — hold-gated mode)
🐝 Hive Agent: architect | Instance: hosted-projectbluefin-knuckle-gjvq | SHA: unknown
— hive: agent=architect backend=copilot model=claude-opus-5
Architecture Finding
Type: tech-debt / duplicated-abstraction
Affected area:
system_files/shared/usr/bin/ublue-system-setup,system_files/shared/usr/bin/ublue-user-setup,system_files/shared/usr/bin/ublue-privileged-setup,system_files/shared/usr/lib/ublue/setup-services/The three first-boot setup services are the same dispatcher written out three times. Each is 28–29 lines and contains its own private copy of:
get_config()jq reader over/etc/ublue-os/setup.jsonHOOKS_VERBOSE/set -xhandlingif [ -d ... ]; then for script in "$DIR"/*; do bash "$script"; done; fidispatch loopdiffproves it — the only variation across all three files is one config key and one default directory:(plus the corresponding variable name in the loop).
ublue-privileged-setupdiffers fromublue-system-setupby exactly the same two lines.The repo already has the right home for this:
system_files/shared/usr/lib/ublue/setup-services/libsetup.shis the established shared library for setup services, sourced by every hook in*-setup.hooks.d/. The dispatchers never adopted it.The test suite is triplicated to match —
tests/test_setup_scripts.batsandtests/test_privileged_setup.batseach restate "get_config returns fallback", "runs hooks in hooks directory", "exits cleanly when hooks directory missing" against a different copy of the same code.Impact
bash "$DIR/*", exiting 127. Reproduced againstmain:ublue-user-setup, tab-vs-space indentation in the tests), which is how semantic divergence starts.bluefin,bluefin-lts,dakota,server, forks) all inherit these three scripts fromsystem_files/shared/, so drift propagates org-wide.Recommendation
Extract the dispatcher into
system_files/shared/usr/lib/ublue/setup-services/hookrunner.shexposingget_config()andrun_setup_hooks <config-key> <default-directory>, and reduce the three/usr/bin/ublue-*-setupentry points to thin wrappers that supply only their key and default. Source it via an overridable$HOOKRUNNER, matching the existing$LIBSETUPconvention insystem-setup.hooks.d/10-framework.sh. Add a structural guard test so a privateget_configor dispatch loop cannot be reintroduced.Filed by architect agent (ACMM L5 — hold-gated mode)
🐝 Hive Agent:
architect| Instance:hosted-projectbluefin-knuckle-gjvq| SHA:unknown— hive: agent=architect backend=copilot model=claude-opus-5