Port Allocation
Port Allocation (#197)
Section titled “Port Allocation (#197)”How BoB assigns dev ports across the fleet. Band convention, the two-tier source-of-truth model, backfill, and inspection. Extracted from the dev lifecycle reference.
Port Allocation (#197)
Section titled “Port Allocation (#197)”Every BoB-provisioned project owns a deterministic port band so multiple projects can run their dev servers simultaneously without collisions or unpredictable auto-increment. Port assignment is declarative, version-controlled, reproducible, and owned by exactly one source of truth — per the prime directive.
Band convention
Section titled “Band convention”base = 5200 + slot*10 # contiguous 10-port band per project base + 0 Vite dev (dev.json server.port) base + 1 Wrangler dev base + 2 cds dashboard base + 3 docs-site dev (scripts/docs-site/dev.sh) base + 4..9 reserved / extensible (other long-running dev services)slot is derived from a deterministic FNV-1a hash of the project name, so a
fresh clone proposes the same band before the ledger has any record of the
project. Bands overlapping well-known services (PostgreSQL 5432, VNC 5900) are
skipped; extend via the ledger’s reserved_bases.
Two-tier source-of-truth model
Section titled “Two-tier source-of-truth model”- Authoritative ledger —
BOB_SOURCE/provisions/ports.json. The single source of truth, version-controlled (validated byschemas/ports.schema.json). It is the only tier with the global view needed for collision detection. Managed byscripts/port-ledger.js. The ledger lives in BOB_SOURCE — not machine-local BOB_HOME — exactly likeprovisions/<project>.json. - Derived projection —
dev.json+ vite/wrangler configs.cdiwrites the concrete ports into a marked, regeneratable block (_bob_portsindev.json; anexport const BOB_PORTS = {…}sentinel invite.config.*; a comment sentinel inwrangler.toml). Never hand-edited — regenerating from the ledger is lossless and idempotent (re-runningcdiyields zero diff). - Name-hash = default proposal, not authority. The allocator tries the hash-derived band first; the ledger only records and arbitrates the rare collision (deterministically stepping to the next free band).
Accepted tradeoff: a project’s port lives in the bigbrain repo, not the
project’s own repo. Accepted because port allocation is inherently cross-project
and BoB already owns per-project state in provisions/.
Retrofit / backfill
Section titled “Retrofit / backfill”When cdi runs on a project with an explicit, non-default pinned port
(e.g. nanaawards 5180, pinned to coexist with a file:../-linked sibling),
that pin seeds the ledger as a preserved pinned entry and is never
reshuffled. Framework defaults are not treated as pins — otherwise every
SvelteKit project would pin to 5173 and perpetually collide. The recognised
defaults are the FRAMEWORK_DEFAULT_PORTS set in scripts/port-project-sync.js:
Vite 5173/5174, Wrangler 8787/8788, Next/Express 3000/3001, Astro 4321,
Angular 4200, Hugo 1313, Django 8000. A default missing from that set is a
collision waiting to happen — the first project of that framework is recorded
as pinned to it and the second fails make port-migrate (#1919, Astro).
Astro needs the port on the command line. Unlike Vite and Wrangler, the
sync writes no Astro config block, so an assigned band only takes effect when
dev.json server.command passes it — astro dev --port <base>. Check the
command after a band is assigned or migrated.
Inspecting bands (observable)
Section titled “Inspecting bands (observable)”cdb --ports # port-band table for all projectscds --ports # same, from the dashboard CLInode ~/.claude/scripts/port-ledger.js list # raw JSONnode ~/.claude/scripts/port-ledger.js path # ledger locationThe legacy machine-local ~/.claude/dashboard-ports.json (the old cds
3333–3400 registry) is folded into the ledger on first use and retired — no
parallel registry remains.
Onboarding the existing fleet (#198)
Section titled “Onboarding the existing fleet (#198)”cdi only allocates the project it is run on. Projects provisioned before
#197 land with no band. scripts/port-fleet-migrate.js is the one-shot
migration that backfills the whole fleet at once:
make port-migrate-dry # review the planned project → ports table (zero writes)make port-migrate # apply: backfill every un-allocated projectDiscovery source is BOB_SOURCE/provisions/*.json (minus the ledger, the
_default template, and manifests with no resolvable _meta.path). The
project name fed to the allocator is basename(_meta.path) — identical to
cdi’s PORT_PROJECT_NAME — so the band a project gets here is exactly the
band cdi would assign. Properties:
- Idempotent & stable — a project with an existing ledger band is never reassigned; a second run is a verified no-op (no ledger or file diff).
- Loud on collision — a pinned band overlapping a reserved range or another project’s band is reported and the run aborts with exit 1 before any write; never silently double-assigned.
- Confined writes — per-project changes go only through the lossless sentinel-block projector; content outside the marked blocks is preserved.
- Graceful skips — projects with no vite/Wrangler/
dev.jsonstill get a ledger band (reserved) and are listed as ledger-only; missing-on-disk and template manifests are listed as skipped with a reason.
deploy.sh invokes it idempotently and non-fatally on every deploy (honouring
the deploy --dry-run flag); the loud exit-1 path is make port-migrate. The
end-of-run summary reports assigned / ledger-only / already-allocated /
skipped / collisions. Tests: make test-port-fleet-migrate.