--- 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 Five terminals, each script 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. `cargo install` of the hook binary reuses what the tests just built and costs a second or so, which is what its comment in the script claims. 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 **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 refuses. The others hold nothing and a restart costs the build; that one holds every account, record and name hold, and without `--data` all of it is in memory, so a watch there would factory reset the deployment on every save and the first symptom would be sessions whose accounts had 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] **An integration test harness: a server and a fake agent, in process.** `crates/didbot/tests/hook_to_record.rs` drives a real listener, the real hook handler and the real scrobble host: a session opens, a payload becomes a stamped call, the call becomes a record, the record reads back over HTTP, and the session ends taking its accounts with it. It needs a socket where the other suites do not, because the hook and the scrobble host are HTTP *clients* and substituting the transport would skip exactly the code that has to agree. - [x] **A session that never reached a live server** is covered by the same harness: one opens with nothing listening, gets no account, and scrobbles anyway once a server exists. - [x] `dev-pds.sh` and `dev-mcp.sh` stop a previous run by pidfile rather than by command-line pattern. `pkill -f "didbot-dev --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`, `dev-mcp.sh` and `dev-site.sh`, over the shared `dev-profile.sh`/`dev-pidfile.sh`/`dev-watch.sh` helpers. This used to read "five dev scripts — server, index, query, swarm, web": the index, query and web scripts went with their crates to vibescrobble.com, and `docs/running-locally.md` now says two terminals are enough. - [x] A swarm that drives the stack with simulated agents and scrobbles. - [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.