--- id: local-dev title: The whole stack runs on one machine, with traffic that looks real status: continuous crates: [didbot-swarm, didbot-serve] dependsOn: [] exitCriterion: > None. A development workflow is never finished; it is worked whenever something adjacent to it is open. --- # local-dev A terminal per script, each finding the others by default, and a swarm that drives the stack with simulated agents. This is what makes every other epic workable without a deployment. - [ ] **A runner for [scripts/ci.sh](../scripts/ci.sh).** Tangled's CI is spindle, and a spindle is self-hosted: this repo has none attached, so the script is run by a person until one exists. - [ ] **Keep [docs/running-locally.md](../docs/running-locally.md) true.** - [ ] **Say where to run it, and what each posture costs.** Server and agents on one laptop is the development posture and the weakest: one compromise takes the keys, the zone credential and the owner's own logins. Server in the cloud with agents on a laptop moves the keys off the machine running capable code. Agents in cloud sandboxes as well makes browser isolation moot and unlocks instance-identity attestation. ## The inner loop, measured A one-crate incremental rebuild of the development server is about two seconds. A full `dev-pds.sh` cycle after a change to `didbot-pds` is about fourteen, of which four is running that half's tests and the rest is compiling and linking. So the compile is not where the time goes. What the build costs is *disk*, and that is what fills a machine: see the dev profile under Done. ## Done - [x] **The development server keeps its accounts by default.** It ran with no `--data` directory, so every restart took the accounts of whatever was talking to it: an agent that signed in this morning was signed in to a server that no longer knew it, and the symptom arrived far away from the one banner line that had said so at boot. `scripts/dev-pds.sh` now defaults to `$XDG_STATE_HOME/didbot/pds`, beside the configuration and the corpus this project already keeps under those directories. Per user rather than per checkout. The agents on a machine address one server on one port, and a second checkout on that port is refused by `pds.lock` before it can write — which is the existing single-writer refusal doing the job it was built for, not a new one. `DIDBOT_PDS_DATA=` is how a run asks for a clean slate, so `didbot-setup env` had to stop writing `${NAME:-value}`: that spelling treats an emptied variable as an unset one and handed the default straight back. `${NAME-value}` is what dev-profile.sh already claimed to emit. **Correction, not new work:** three items here were ticked against code that has left this repository — `didbot-replay`'s firehose capture and playback (no such binary, crate or script exists now), a canvas mock, and the index/query/web dev scripts. The index, the query service and the canvas moved to vibescrobble.com; the replay tool went with them. They are unticked rather than left standing. The surviving scripts are described accurately below. - [x] **The Merkle search tree corpus is cached per machine, not per checkout.** It is 72MB of immutable upstream test data that varies with nothing, and it used to live in each checkout's own `.cache/`: this machine carried two copies within a day of the suite landing, and every new worktree failed four tests until somebody fetched one. It now sits at `$XDG_CACHE_HOME/didbot/mst-test-suite`, beside the configuration and state this project already keeps under the same directories, and a clone left in a checkout is moved rather than re-downloaded. One clone shared by every checkout has a cost worth naming: a plain run that fast-forwarded it would move the corpus under whatever else is reading it. So a corpus already there is left alone and reported, and `--update` is what changes it. - [x] **A test that failed about once in twelve suite runs, found and fixed.** `a_proof_missing_a_node_does_not_verify` asserted that a record's path through the Merkle search tree was more than one node deep. A record key is a TID and a tree's shape comes from the hashes of its keys, so that depth varies run to run: it held nineteen times in twenty and failed the whole suite the other time. Nothing in the test needed the depth. It was measured at two failures in twenty-five runs before, and none in thirty after. It took three sightings to catch, because the first two were seen through a `cargo test` whose output had been sent to `/dev/null` — which is its own lesson about checking exit statuses without keeping what produced them. - [x] **Watch modes, and the one component that refuses.** `--watch` on any `dev-*.sh` reruns the whole script — rebuild, that half's tests, banner and all — whenever anything under `crates/` moves. It reruns the script rather than restarting the binary because the tests are the step most likely to have something to say about the change that triggered them. The personal data server watches too, now that it keeps its state by default. It refuses under `DIDBOT_PDS_DATA=`: the others hold nothing and a restart costs the build, while a server in memory rebuilds every account, record and name hold on every save, so a watch there factory resets the deployment and the first symptom is sessions whose accounts have silently stopped existing. It says so and names the two ways to mean it. Polling rather than `inotifywait`, which is not on every machine this runs on, and a second of latency against a rebuild that takes fourteen is not worth a dependency that has to be installed first. Two things had to be got right and were not, first time. The stop has to tolerate its own signal: these scripts run under `set -e`, and a `wait` on a process the loop just killed reports the signal, which took the watcher down with the thing it was restarting — a watcher that worked exactly once. And the pidfile's cleanup trap was dead code from the day it was written, because `exec` replaces the shell and takes the trap with it; a stale pidfile is harmless and is now documented as such rather than pretended about. - [x] **A `--data` directory says what wrote it.** Serde fills a defaulted field and ignores an unknown one, so a log entry can parse and mean something else, and the deployment starts up looking healthy. A `pds.layout` stamp is checked before the log is read, and a directory this binary would misread is refused by name. Nothing converts one layout to another, on purpose: a conversion this project has never needed is one nobody has tested, and in development the answer is to delete the directory. The stamp is only worth something if somebody bumps it, so the durability suite pins the on-disk shape against the number — the variants read out of the source, and the fields read off a log the test writes and decodes by hand rather than with the crate's own reader. A field rename fails it with the instruction to bump. - [x] **A dev profile that stops generating what nobody reads.** Debug information was most of what a build produced and almost none of it was used: nothing here is debugged by stepping, and a backtrace needs only line tables. Measured by building the development server from cold into a target directory of its own under each setting, so neither answer was polluted by the other's artefacts — 1.1 GB to 827 MB of target, 106 MB to 43 MB of binary, 59.6 s to 47.2 s of build. The workspace gains more than that share, because the facade crate's test binaries link every crate in it and there is one per suite. The saving arrives on the next `cargo clean`. Cargo keeps artefacts it has superseded, so a directory that has been built in for a while holds both shapes and briefly grows; the worktree this was written in had reached 31 GB and rebuilt from clean, whole workspace and every test binary, into 3.4 GB. - [x] **Simulate failure, not only the happy path.** A server that goes away mid-stream is now a test: `crates/didbot/tests/firehose.rs` cuts a subscriber's connection, keeps writing records it cannot see, and requires the frames it missed back in order with nothing repeated. It needs a proxy in front of the server, because aborting the task that owns the listener does not disconnect anyone — axum serves each accepted connection from its own task — and the first version of the test passed whether the resume worked or not. A torn log was already covered, in `didbot-pds`'s durability suite: a write interrupted halfway costs that write and nothing before it, and one torn inside the first entry replays to an empty deployment. A session that dies without a `SessionEnd` is covered by the hook harness. - [x] **A way to reclaim worktree build directories.** `didbot-setup disk` reports every `target/` under the checkout with what it costs and what it belongs to, and `--clean` removes exactly one category: the ones whose worktree no longer exists. A machine shared between people and agents cannot have a tool that sweeps: a directory touched in the last hour is reported as in use and never offered, and an idle one is somebody else's to clear. - [x] `dev-pds.sh` stops a previous run by pidfile rather than by command-line pattern. `pkill -f "didbot-pds --port ${PORT}"` matched anything whose arguments contained that string, including another developer's server and the shell running the script — which happened while this was being written. `scripts/dev-pidfile.sh` reads `/proc//exe` before signalling, so a reused pid is never a licence to kill whatever inherited the number. - [x] Each `dev-*.sh` sources `scripts/dev-profile.sh`, so a worktree bound to a second profile gets that profile's ports with nothing edited, and a variable already in the environment still wins. - [x] One command running the same checks as the commit hooks: [scripts/ci.sh](../scripts/ci.sh) runs prek over `prek.toml` rather than restating it, adds the commit-msg hook over the whole branch, and adds the test suite the hooks are too slow to run. A commit made with `--no-verify` is checked by it. - [x] Dev scripts that find each other by default: `dev-pds.sh`, `dev-swarm.sh` and `dev-site.sh`, over the shared `dev-profile.sh`/`dev-pidfile.sh`/`dev-watch.sh` helpers. The index, query and web scripts live with their crates in vibescrobble.com. - [x] A swarm that drives the stack with simulated agents and records. - [x] `--data`, so a development server keeps its state across a restart. - [x] Commit hooks: format, clippy, doc build, doc lint, lexicon tests, plan shape and register, and Conventional Commits. - [x] The crate scaffold, the namespace constant and the embedded lexicon loader. - [x] The Claude Code hook payload types.