Modules: internal/state, internal/lock, internal/orchestrate, internal/hooks, internal/doctor · Milestone: M0 (lock+state) → M2/M6 · Effort: spread across M0/M2/M6
Build the lock + state store FIRST (M0). Everything that mutates shared state goes through them from the first commit. Retrofitting concurrency safety is the single most expensive mistake available here. This spec addresses the #1 architectural risk: concurrent invocations corrupting machine-global shared state.
Own the machine-global ledger, serialize all mutations across processes, and make multi-phase operations crash-safe and self-healing.
modernc.org/sqlite(pure-Go, preserves the static binary), under the XDG data/state dir, keyed by Docker context (the value of the activedocker context/DOCKER_HOST) so WSL2's two daemons (Desktop vs in-distrodockerd) don't share a ledger and mis-count.- Opened with WAL +
busy_timeout=5s+foreign_keys=ON. - Versioned migrations table, forward-only within a major, backup
state.dbbefore migrating. - Rolling event log — every shared-service start/stop and ref change is recorded with a reason + timestamp, so "my DB disappeared" becomes answerable (
service X stopped because project Y went down at T). doctor --rebuild-statereconstructs the entire ledger from live Docker labels + on-disk config (the recovery path whenstate.dbis lost/corrupted — likely on 9p/WSL2 or akill -9mid-write).
docker_context(id, name, endpoint)
shared_service(ctx, name, engine, major_version, status, started_at)
service_ref(ctx, project, service, shared_service) -- ref count = COUNT(*)
port_alloc(ctx, owner, purpose, port UNIQUE)
provisioned(ctx, project, kind, name, created_at) -- db/role/bucket ownership ledger
hook_run(ctx, project, hook, scope_key) -- idempotency for firstRun etc. (spec 11)
saga_phase(ctx, workspace, scope, phase, status, fingerprint)-- up-saga resumability (spec 09)
event_log(ctx, ts, kind, subject, reason)
schema_version(version)
- A coarse
gofrs/flockadvisory lock on a file underXDG_RUNTIME_DIR(fallbackXDG_STATE_HOME), taken around every operation that mutates the ledger or the shared stack. Single writer; reads are lock-free snapshots. - Covers the four races that otherwise happen daily: network-ensure TOCTOU (inspect→create), port allocation, ref-count rows,
CREATE ROLE. doctordetects 9p/networked state dirs (where SQLite/flock locking is unreliable) and warns; refuse/mnt/*working dirs entirely.
- Allocate from a configurable base range, persist immediately (eliminates the inter-invocation TOCTOU).
- Bind-test (
net.Listen 127.0.0.1:port) is advisory only; union it with ports already published by Docker (SDKContainerListport bindings) so Docker-Desktop-VM-published ports are detected (a host bind-test doesn't reflect the VM proxy). - Prefer not publishing host ports at all (DNS-over-shared-network default) — allocate only on explicit host-reachability requests.
The up saga (internal/orchestrate) — see spec 09
up runs ~10 named phases (preflight → clone → network ensure → shared services → provision DB/bucket → resolve secrets → CA/trust → generate → compose up → hooks; the canonical list with per-phase lock/skip/compensation rules is in spec 09). A failure mid-way (e.g. provisioning fails after the shared stack started and ref rows were inserted) must not leave a half-up workspace + lying ledger.
- Model as a saga: each phase is named, idempotent, and resumable, with durable phase-state and a compensating action on failure (e.g. roll back the ref row if the project stack never came up).
- Re-running
upskips satisfied phases (powers one-command onboarding, feature #1). - Live bubbletea checklist when TTY; plain/
--jsonfallback otherwise. - On unexplained inconsistency,
doctor --rebuild-state+shared doctorrepair.
Lifecycle hooks (internal/hooks) — see spec 11
- Declarative
preUp/postUp/preDown/firstRun/postPull, run on the host or viacompose exec, with the same${ref}/secret interpolation. firstRunidempotency tracked in the state DB (keyed per provisioned data volume) so it survives restarts — this is what correctly replaces Postgresinitdb.d(which never re-runs on an existing shared volume). Canonical uses: DB migrations, seeding, app-key generation, dependency install.
doctor capability matrix (internal/doctor) — see spec 13
Runs the real branch logic (not docs) for: docker daemon reachable + correct context, docker compose ≥ v2.20, git ≥ 2.30, disk for volumes, shared-network health, port conflicts, CA trust state (host + Firefox/NSS + Windows-on-WSL2), secrets-provider reachability, stale ref rows vs live containers, *.localhost/resolver per platform, 9p state-dir warning, keyring presence. Every failure → one-line remediation; --fix where safe.
Teardown (workspace destroy / uninstall) — see spec 13
Reverse all machine-global artifacts, with explicit data-loss confirmation: external network · shared containers + named volumes (Postgres data!) · the SQLite ledger · root CA from host + Firefox + Windows stores · alias symlinks · cloudflared creds · keyring entries · template cache. Orphaning a CA (a security artifact) or dangling symlinks is a defect.
- pure-Go SQLite is not immune to
database is lockedunder concurrent writers — WAL +busy_timeout+ the flock are all required together. - Reconcile correctness depends on the label-filter rules (spec 03):
All=true, excludeoneoff=true, correct normalized project name (or filter on your own label), correct Docker context. - A
kill -9mid-write is the expected failure on WSL2/9p —doctor --rebuild-stateis the safety net, not an afterthought.
- Two concurrent
upinvocations serialize on the flock — nodatabase is locked, no duplicate port, norole already exists. -
kill -9during phase 4 ofup→ next command reconciles to a consistent ledger; no orphan ref rows;shared statusis truthful. - Deleting
state.dbthendoctor --rebuild-state→ ledger reconstructed from live Docker labels + config. - A version-change self-update runs the state migration (with backup) on next launch; no schema mismatch.
-
firstRunhook runs exactly once per provisioned volume, even across restarts. -
workspace destroyleaves no network, volume, CA (any store), symlink, keyring entry, or cache behind. -
doctoron a/mnt/cworking dir refuses with a clear message; on a 9p state dir, warns.
The spine consumed by internal/workspace, internal/provision, internal/secrets (cache), and internal/cli. Owns the durable truth that makes the shared-services differentiator safe under real-world concurrent use.