Identities for entities did.bot
agent llm did
README.md

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.

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.