--- 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/workflows/ci.yml` runs it on a push to `main` and on each pull request round. Tangled's CI is spindle, and a spindle is self-hosted: one has to be attached to this repository from its settings before the workflow runs at all. - [ ] **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 operator'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.** `scripts/dev-pds.sh` defaults `--data` to `$XDG_STATE_HOME/didbot/pds`, beside the configuration and the corpus this project already keeps under those directories, so a restart does not sign out every agent. 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. `DIDBOT_PDS_DATA=` is how a run asks for a clean slate, which is why `scripts/dev-profile.sh` spells that one default `${NAME-value}` rather than `${NAME:-value}`: the second treats an emptied variable as an unset one and hands the default straight back. - [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, so it sits at `$XDG_CACHE_HOME/didbot/mst-test-suite`, beside the configuration and state this project already keeps under the same directories. A clone left in a checkout's own `.cache/` is moved rather than re-downloaded. One clone is shared by every checkout, so a plain run that fast-forwarded it would move the corpus under whatever else is reading it. A corpus already there is left alone and reported, and `--update` is what changes it. - [x] **No test asserts the depth of a record's path through the Merkle search tree.** 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. `a_proof_missing_a_node_does_not_verify` needs no particular depth. - [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. 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. The stop tolerates its own signal: these scripts run under `set -e`, and a `wait` on a process the loop just killed reports the signal. A stale pidfile is harmless, because `exec` replaces the shell and takes any cleanup trap with it. - [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. A build reads the layout it writes and the one before it, through that layout's reader rather than a converter — see [deploy](deploy.md) — and refuses anything older. In development the answer to a refusal 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 generates only line tables.** 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: 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. - [x] `dev-pds.sh` stops a previous run by pidfile rather than by command-line pattern, which would match anything whose arguments contained the port string, including another developer's server and the shell running the script. `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`, the one place the port, the zone and the state directory are written down, 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] The swarm signs agents in over OAuth (PAR, decision record, approve, token exchange) rather than only using the raw credential `createAccount` hands back, so `crates/didbot-swarm/tests/decision_bounds.rs` can drive the decision store's per-account and total bounds under concurrent load. - [x] The swarm's sign-in presents real RFC 9449 proofs over a key it generates per agent, so `dev-swarm.sh --sign-in` signs agents in against `dev-pds.sh` and writes their records with the tokens that yields. - [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.