Skip to content
SHC Docs

Next-wave plans

Implementation plans produced by the read-only design pass (workflow weat4nfq3, 2026-06-22). Companion to DRIFT-REGISTER.md (the backlog) and the ADRs. Most waves have since landed and are collapsed to a status tombstone (heading + what shipped); the still-open waves keep their full plan. Pick an open wave, re-verify the sites against HEAD, and land serially.


Wave: Structural context ownership (subsystem loops) — ✅ LANDED 2026-06-22

Section titled “Wave: Structural context ownership (subsystem loops) — ✅ LANDED 2026-06-22”

Status: Done (commit 1b746f6a7). The daemon-lifetime ctx is threaded through each loop subsystem’s DepsBuilder (DaemonCtx field); backup_mount.go’s active reaper-loop bug + the latent landmines (collectors/docker_events/container_recorder)

  • the node_self_register/config_loader/unix_socket Background→DaemonCtx migrations all landed, plus the OnLeader-ctx bug class (schedule_dispatcher + swarm_recorder) the judge caught. Fitness Check 7 (subsystem-loop-start-ctx) enforces it.

Wave: Phase B — launcher injection — ✅ LANDED (#106)

Section titled “Wave: Phase B — launcher injection — ✅ LANDED (#106)”

Status: Done, but via a deliberately different design than originally planned — the docker-label approach was rejected. What shipped: the launcher is pure scoped config (the launcher key, resolved deployment > stack > environment > tenant > global, then the app.yaml launcher: default). The launcher *bool state hybrid (models/types/repository/CLI --launcher/--no-launcher) was deleted and the deployment.launcher column retired. Injection does NOT emit docker labels — it rides the shc-oidc gating seam: writeIngressRouteFile defines a per-deployment shc-launcher-<project> rewritebody middleware in the file-provider route file (cross-node safe under exposedByDefault: false), and MaterializeIngress re-resolves per sweep via launcherForDeployment. Live in modules/app/launcher.go (launcherForDeployment), modules/app/inject_traefik.go (shc-launcher-<project> + auth.RewritebodyMiddleware()), and auth.EffectiveLauncher (now a scoped-config resolver).


Remaining Runtime-singleton families (the broader “no legacy” backlog)

Section titled “Remaining Runtime-singleton families (the broader “no legacy” backlog)”

The *Service ambient-pull locator is fully gone (di-singletons 0, hard). But func SetRuntimeService was only ONE family — the broader func SetRuntime* grep now finds 8 files (fitness Check 1b di-runtime-families, ratchet ceiling 8, falling). app.RuntimeStackRepo was swept + deleted (commit 4dac5314c, 2026-06-22) and stack.RuntimeState was swept by the ADR-0002 / B1 state-model collapse (2026-07-04 — the wired stack *State now lives solely on the composition root, Services.StackState(); ceiling fell 9→8). The rest:

  • Deliberately RETAINED bridges (do not naively delete):
    • ca.RuntimeStore (modules/ca/register.go) — the shc ca … CLI runs in-process with NO composition root and falls back through GetRuntimeStore. Killing it needs the CLI to route CA ops over HTTP (a feature change). NOTE: keycloakMasterRootFn (services.go) is one of its daemon readers but is the keycloak-backchannel master-CA trust seam — OFF-LIMITS, don’t convert it.
    • stack.RuntimeState (modules/stack/state.go)SWEPT (ADR-0002 / B1, 2026-07-04). Set/GetRuntimeState is retired; the wired stack *State is on the composition root (Services.StackState()), and stack/router.go + app/router.go resolve it there. No longer a retained bridge.
  • ✅ SWEPT (2026-06-23, parallel-worktree generation + serial merge): internal_stacks.SetRuntime (dead-deleted), job.Enqueuer, backup.RecoveryActions, backup.Scheduler, hud.Handler — all onto the composition root (accessor + consumer-seam resolution + singleton deleted). backup.MountSubprocService: daemon HTTP path converted to the root (dual-write); singleton RETAINED as the shc backup mount CLI in-process bridge (and it’s SetMountSubprocService, not SetRuntime*, so outside the ratchet). di-runtime-families ratchet: 13 → 9, then 9 → 8 after the stack.RuntimeState sweep above (current ceiling 8).
  • FOUNDATIONAL — keep (legitimately process-wide infra, NOT legacy): core/operations/store.go (async-op SSE event registry, read cross-package via EnsureRuntime), subsystems/impl/ent_client.go (THE single DB-connection publish seam), modules/vault/state.go RuntimeFactory (fires during config.Load before the root exists), modules/app/commands/preset.go (stateless CLI-only). Deleting these would be wrong — the ~9 ratchet floor IS these + the bridges, a desirable floor not 0.
  • Deferred (cascade-heavy / low ROI): modules/storage/register.go ConfigService (a self-contained dead web — register funcs + a dead daemon file + koanf store + wireStorageProductionDrivers + an os/exec keep-alive), modules/backup/mount/manager.go.
  • Retained bridges: modules/ca/register.go (CLI), modules/test/register.go (test-runner harness). (modules/stack/state.go was here but is now SWEPT — see above.)

Also out of this family but same shape: ~15 SetXResolver push-resolver package globals (SetDNSResolver, SetVaultResolver, SetScopeResolver, …) — setter injection via a package var. Milder than ambient-pull; ADR-0006 partially endorses “inject a resolver”. Convert to threaded Deps only where it’s cheap.

Wave: App-module DI sweep (the final 2 singletons) — ✅ DONE 2026-06-22

Section titled “Wave: App-module DI sweep (the final 2 singletons) — ✅ DONE 2026-06-22”

Status: Complete. di-singletons and di-test-fixtures ratchets are both at 0 (the whole DI legacy sweep is done). Landed in 4 commits (4b611d6fa recorder threading, 01373968f state-mirror + cmdAdapter, e7fabf0d7 daemon/subsystem/CLI readers, d6e540757 delete the singletons). The state_mirror.go block was lifted on explicit user instruction (“do it anyway”) and converted with a pure reader-mechanism swap (resolveAppService, zero logic change). Adversarially verified (recorder-regression / behavior-equivalence / completeness — zero findings). KEY non-finding: the design’s “router gets nil d.Services” blocker was a FALSE POSITIVE on the dead startAPIServer shim — the live router sites (unix_socket.go, internal_api.go) thread the root, now guarded by tests/unit/cli/cli_test.go’s router-deps checks. NOTE: app’s Set/GetRuntimeStackRepo + SetIngressIntentRecorder are SEPARATE singleton families (not SetRuntimeService-named, not counted) and remain by design.

Wave: State-model collapse (ADR-0002) — DONE (B1, 2026-07-04)

Section titled “Wave: State-model collapse (ADR-0002) — DONE (B1, 2026-07-04)”

Implemented. state_mirror.go is deleted; the app Service routes every stack read/write through one active deploymentStore (the DB deployment table on the daemon via SetStackRepo, the in-memory app.State offline when repo==nil — NOT a read-through cache, the ADR’s ratified Option A). Node canonicalisation moved to the DB write path (dbStore.stampNode); the mirror funcs (mirrorStackRow/mirrorStackRowErr/mirrorStackDelete/MirrorAllToRepo/ nodeIDResolver/SetNodeIDResolver) plus the memory→DB boot backfill and the 5s disk-reconcile ticker are gone. ReconcileDiskStacks is demoted to a one-shot, readiness-gated ImportOrphanManifests boot import (tombstone branch dropped — delete is atomic, no resurrection window). RegisterRecoveredStack is KEPT and rewired to a direct store write (the cross-node node+runtime recording regression guard, HANDOFF fix 25617e486); the swarm-only node-pin gate in services_seams_clone.go is preserved. shc.stack.status has one owner (app/router.go). Step 0/1 (manifest.json rename + legacy state.json read-fallback) shipped earlier under T17.

Still owed: a fresh 3-node proxmox clone/recover/move + shc ls/status re-prove on real hardware (the final field gate).


Wave: Duplicate-impl dedup (UDS dialer → daemonclient) — ✅ LANDED

Section titled “Wave: Duplicate-impl dedup (UDS dialer → daemonclient) — ✅ LANDED”

Status: Done. The canonical helpers UDSTransport / UDSClient / UDSBaseURL exist in core/daemonclient/client.go (~194-248) and the inline dialer copies were repointed onto them: the uds-dialer-copies ratchet is now at 0 (non-test, non-daemonclient DialContext(ctx, "unix", …) copies = 0), and the arch-fitness.sh ceiling is a hard 0.


Squash all ent migrations back to ONE baseline SQL migration. There was a prior squash (squashes.json folds 2 pre-squash migrations into 20260612043922_auto.sql) — extend it to a single file. ce’s make migrate is generate-only (atlas hash + diff); runtime apply is a separate startup runner. CAUTION (memory migrations_squashed): a squash means pre-squash DBs fail daemon startup until wiped — fine pre-1.0 but the squash must be a clean baseline that a fresh DB applies in one shot.

Wave: Test relocation (extractable test tree)

Section titled “Wave: Test relocation (extractable test tree)”

Move tests so they can later be pulled into a SEPARATE git repo (tests may not be open-sourced). Go constraint: white-box tests (package foo, accessing unexported symbols) MUST live next to the code; only black-box (package foo_test) + integration

  • e2e + bats can relocate cleanly. The recon must classify: which tests are white-box (stay, or convert to black-box) vs already-relocatable, and design the layout (tests/unit// already exists for some). The arch-fitness di-test-fixtures ratchet + the per-package test runs assume current locations — update both.

Wave: CLI performance (max responsiveness)

Section titled “Wave: CLI performance (max responsiveness)”

Goal: make shc CLI commands much faster. CONSTRAINTS (user): NO aggressive lazy-loading that risks state desync; safe caching OK; maintainable; the biggest win is likely eliminating redundant round-trips + repeated pre-flight checks (auth / capability / version checked again and again) + N+1 daemon-side queries. Trace the hot commands end-to-end (CLI process init → UDS/auth handshake → per-command daemon calls → handler DB/docker cost) and produce a prioritized, safe optimization plan.