didbot-setup #
Wires this machine's harness to a didbot stack, and says what is wrong with one that is already wired.
didbot-setup # what is wrong here
didbot-setup apply # show the changes, ask, make them
didbot-setup verify # provision, write, read back, clean up
didbot-setup undo # take back what was written here
What it is for #
The stack comes up from a checkout: five scripts, each finding the others by
default. What does not come up from a checkout is everything around it — a
binary on PATH, because the harness invokes the hook by name; hook entries in
a settings file that belongs to a person rather than to this project; a
scrobble host declared where the harness looks for it; and a restart, because
hook configuration is read once at session start.
That is the whole subject, and it is the part that differs on every machine.
One list, three commands #
check asks whether each change has been made. apply makes the ones that
have not. undo takes back the ones this crate made and nobody has edited
since. All three walk the same list, because a doctor and an installer that
state the same facts separately drift within a month — and the failure looks
like the doctor lying.
Idempotence and resumability are not features here, they are consequences: every change is probed before it is made, so running twice changes nothing and running after a failure continues from wherever it stopped. There is no checkpoint file, because the machine's own state is the checkpoint and a file would go stale the moment somebody edited a setting by hand.
Consent, and taking it back #
apply prints every change as a file, a path and a value, and asks once.
--yes skips the question; a run with no terminal and no --yes refuses
rather than assuming.
Putting didbot-hook on PATH is asked for separately, and --yes does
not cover it. --install does. The harness invokes the hook by name, so
settings written with no binary point at nothing — but one copy in
~/.cargo/bin is then run by every checkout on the machine, which is a bigger
thing to have agreed to in passing than a settings file.
Every change made is recorded in $XDG_STATE_HOME/didbot/setup-ledger.json
with the value that was at that location before it. JSON has no comments, so a
change cannot leave a marker saying who made it; without the ledger a teardown
would have to rewrite each file from a template and throw away everything else
in it. With it, undo is exact — and a location whose value has changed since
is reported and left alone, because somebody edited it and it is theirs now.
Scopes #
| scope | hooks and permissions | the scrobble host |
|---|---|---|
project |
.claude/settings.json |
.mcp.json |
local |
.claude/settings.local.json |
~/.claude.json under this directory |
user |
~/.claude/settings.json |
~/.claude.json |
Settings files merge: every hook in every scope runs. Wired in two scopes
means two accounts provisioned per session and one of them never torn down,
which has no other symptom worth recognising, so check reports it as a
conflict.
The bootstrap, and who runs this #
Hooks are captured at session start, so a session that runs apply is never
itself hooked — and neither are its subagents, which inherit the session's
captured configuration rather than re-reading disk.
So a new directory is set up from outside it, before a session starts there:
git worktree add -b claude/thing .claude/worktrees/thing origin/main
didbot-setup apply --dir .claude/worktrees/thing
--dir works because the harness keys per-directory configuration on the
absolute path, which is writable from anywhere.
Describing a second stack #
The three tables are hand-editable and always were, but a flag at a time is
harder to get wrong than a TOML table at a time — and nothing here writes a
configuration that check would then report as broken.
didbot-setup service add pds pds-beta --port 3500 --zone beta.agents.localhost
didbot-setup service add mcp mcp-beta --pds pds-beta
didbot-setup profile new beta --pds pds-beta --mcp mcp-beta
didbot-setup bind beta # this directory, or --dir another
didbot-setup show
A service given no --port lands just above whichever upstream it names, so
the run above puts mcp-beta on 3501 and a second stack reads as a block
rather than as five numbers scattered above whatever happened to be highest.
profile new --from default --mcp mcp-b is the two-hosts-one-server case:
copy everything and replace one role.
Writing the file discards comments — it is machine-written from here on. show
prints what it holds and what the current directory resolves to.
The development scripts read this too #
didbot-setup env prints the profile as shell assignments, and
scripts/dev-profile.sh — sourced by each dev-*.sh — evaluates them. So a
worktree bound to a second profile runs ./scripts/dev-pds.sh and gets that
profile's ports with nothing edited.
Each assignment is export NAME="${NAME:-value}", so a variable already in the
environment wins: DIDBOT_PDS_PORT=3100 ./scripts/dev-pds.sh still means
3100. A profile is a default, not an override — the same precedence the hook
uses. A machine with no configuration file is unaffected, because the builtin
profile is the ports those scripts already defaulted to, and a checkout with no
setup binary built yet is unaffected too, because the helper is silent when it
cannot find one.
Following one turn across three processes #
A scrobble crosses three of them: the hook stamps it, the scrobble host writes it, and the personal data server stores it. Each logs what it did, and the DID is the wrong grain to join those lines by — an agent that has been working for an hour has hundreds of lines under one DID.
So the hook stamps the harness's own tool_use_id into the call, the scrobble
host logs it and forwards it in a quernstone-turn header, and the server puts
it on the span every line of that request is emitted under:
hook stamped a scrobble for 9f1c4e2a turn=toolu_01ABC
mcp 🧵 tightening the MST invariants did=basalt-otter turn=toolu_01ABC
pds request{... turn=toolu_01ABC}: wrote record rkey=3mu4ck4rmiz3y
It is the harness's identifier rather than a fresh one, so the same string also finds the turn in the session transcript. It is never stored: a record is public and permanent, and an operational identifier does not belong in one.
didbot-setup debug on # every component, one switch
didbot-setup debug # what it is now
debug on writes debug = true into stack.toml, and env turns that into a
RUST_LOG the development scripts export. A running process keeps the filter
it started with, so the terminals need restarting; the hook is a fresh process
per event and needs nothing.
Which of three steps you are waiting on #
The scrobble tool's description is compiled in. Editing it needs a rebuild, and a restart of the host, and a new session — a harness reads a tool description once, when the session opens. Three steps, none of them visible, and a developer who edits the prompt and sees nothing change has no way to tell which one they are waiting on.
So the host reports a digest of what it serves, and check compares it against
what this checkout compiles:
ok scrobble host build mcp was built from 68efa7d and serves this checkout's tool description
WARN scrobble host build mcp was built from a1b2c3d and serves a different tool description
than this checkout compiles. Restart scripts/dev-mcp.sh…
check also compares the host's own idea of where it writes against the
profile's. A host started by hand with the wrong --pds, or left over from
another stack, produces a scrobble refused for an account the target has never
heard of, and nothing else says why.
Verify, rather than "the port answered" #
verify provisions an agent through the same client the hook uses, writes a
com.vibescrobble.scrobble under it, reads the record back, asks the query
service whether it arrived, and deletes the account. It is the only way to find
out whether a harness restart took effect, and its exit status is the answer.
The account is deleted whatever happens, including when a step fails: a check that strands an agent every time it runs is a check people stop running.
Where the disk went #
A target/ in this workspace is between eight and twenty gigabytes and there
is one per worktree. Four live worktrees have filled a machine mid-link more
than once, and the failure does not look like a full disk — it looks like the
linker crashing, which sends a developer reading a compiler bug tracker.
didbot-setup disk
didbot-setup disk --clean # orphans only
22.3G this worktree …/.claude/worktrees/dev-setup/target
10.1G in use …/.claude/worktrees/mst-diff-suite/target
8.6G idle …/didbot/target
41.0G in total
--clean removes exactly one category: a build directory whose worktree no
longer exists, which belongs to nothing and which nobody is building in. This
machine is shared between people and agents, so a directory touched in the last
hour is reported as in use and never offered, and one that is merely
idle is somebody else's to clear with cargo clean in it.
What it will not do #
Restart the harness, install a certificate authority, or bind a privileged port. Each is named as a step and stopped on, with the command to run, rather than attempted quietly.