* MUL-6479 feat(dev): make a local environment a named object with one verb per lifecycle step
Local development had six entry points, thirty-four make targets and no way to
see or delete what they created. `make up` / `status` / `list` / `down` /
`destroy` / `gc` / `env-exec` replace that with one set of verbs, shared by
humans and agents, with `C=api,web,daemon,desktop` selecting components.
Three defects made the old flow untrustworthy rather than merely verbose.
1. The database the tooling created was not the one the application reached.
`ensure-postgres.sh` creates through `docker exec`; when a native PostgreSQL
owns 5432 the container never binds the host port, so the create lands in a
server the backend never talks to. It printed "✓ PostgreSQL ready" and the
next step died with `SQLSTATE 3D000`. On the machine this was written on the
two servers had already diverged: 94 same-named databases, 69 more in the
container with no counterpart. `up` now creates through DATABASE_URL and
treats a successful `migrate up` — which pings that same string — as the only
proof; failure diagnoses the port owner instead of reporting a missing
database.
2. Identity was computed, not allocated. Ports, database names and CLI profiles
all came from `cksum($PWD) % 1000`, and a collision was silent: the loser
fails to bind while the winner keeps answering 200. Allocation now happens
under a lock, probing from the same path hash so a checkout keeps its
familiar numbers, and requires the slot to be both unregistered and free of a
live listener. The manifest in ~/.multica/dev/envs/<name>/ is what every
later command reads.
3. Creating an environment left no record of how to destroy it, so there was no
destroy — only archaeology. `down` stops processes and keeps the data;
`destroy` consumes the manifest: processes, database, profile, slot. `list`
makes leaks visible and `gc` collects environments whose checkout is gone or
whose TTL passed.
/health now reports pid, commit and started_at. A 200 alone proves something is
listening, which is exactly what let a failed restart keep serving the previous
build; `up` compares started_at against its own launch and refuses an answer
that predates it.
Two constraints are enforced rather than documented: the daemon is started from
a built server/bin/multica, never `go run` (the daemon re-execs its own path for
every task, and `go run` deletes that binary when the launcher exits), and
`C=daemon` under a .multica/daemon_task_context.json marker stops with that
explanation up front instead of spending a login on a refusal.
scripts/dev-env.test.sh covers the registry verbs against a throwaway
MULTICA_DEV_HOME with no services running, and pins the regression that made
`down` exit 1 after reporting success: on bash 3.2 a command substitution whose
function ends in a failing command aborts the script under `set -e`, and "no
process is listening" is that function's normal answer.
`make dev` is unchanged and still runs in the foreground.
Co-authored-by: multica-agent <[email protected]>
* fix(dev): close local environment lifecycle gaps
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Lambda <[email protected]>
Co-authored-by: multica-agent <[email protected]>