# Plan What didbot has built and what it is building. One file per epic. [milestones.md](milestones.md) is the other view of the same work: releases worth naming, in prose, and the only place a milestone is named. An **epic** is a line of work that takes many pull requests and has an exit criterion somebody could check. The tasks live inside it. ## Where the reasoning goes In the epic, beside the work it justifies. There is no separate decision log: there was one, it reached fifty-odd entries before anything shipped, and what it mostly recorded was reasoning about things that turned out not to exist. An entry is only as good as the fact under it, and nothing checked the facts. This directory is checked. The frontmatter builds the tables, the links have to resolve, and an epic points at work that either exists or does not — so a claim about something imaginary shows up as an item nobody can close. Reasoning that cannot survive next to the work it is about was not load-bearing. The invariants the whole system rests on are in the repository's [README](../README.md), which is short on purpose. ## The capabilities The epics are a decomposition of one list, written by the project's owner: a conformant personal data server; ephemeral `did:web` accounts for agents; lineage, model and execution environment tracking; third-party OAuth login for those accounts; management of which scopes may be requested, which apps may be granted anything, and which agents may write which record types; a dashboard for that policy, with the policy stored in the operator's own personal data server; two-way operator verification; atproto private spaces; lexicons for recent activity and mentions; a firehose indexer; a frontend for public and private activity; and a local development workflow with simulated agent traffic. That list is the mandate. Where anything here disagrees with it, the list wins and the epic is what changes. ## The id is the commit scope Each epic's filename is its id, and that id never changes. `plan/pds-writes.md` is `pds-writes`, so its commits read `feat(pds-writes): …`. That is why the ids have no number prefix and why there is no order field: an identifier that has to be rewritten when something else moves is not an identifier. Archiving moves the file and never renames it. `plan` is a valid scope too, for changes to this register. ## Order is advisory The tables read top to bottom and that is the whole of the ordering. What constrains the work is `dependsOn`, which says what genuinely cannot start first, and `status`, which says what is blocked. The sequence comes from [order.txt](order.txt), a list of ids and nothing else. An epic it does not name still appears, alphabetically, at the end of its table. ## Status `shipped` — the exit criterion is met. Most of the work done and a demonstration running is not the same thing: an epic ships when a stranger could check its exit criterion. The file stays after it ships, and moves to `complete/` when nothing is left open in it. `open` — being worked on or ready to be, whatever its `## Done` list holds. `blocked` — cannot start until something in `dependsOn` lands. Two epics are blocked on something outside the project: [spaces](spaces.md) waits on a specification that still calls itself a proposal, and [abuse-notices](abuse-notices.md) on a proposal that says nothing in it is finalised. `continuous` — no exit criterion. Worked whenever adjacent code is open. `declined` — decided against. Not `blocked`, which is waiting. The file stays, because a decision that is not written down gets made again. ## The tables are generated The five tables below are output. `scripts/gen-plan-readme.py` reads every epic's frontmatter and rewrites what sits between the `` fences; everything outside a fence is hand-written and passes through untouched. The `plan-register` prek hook runs it with `--check`, so a row that disagrees with the file it points at fails the commit. Adding an epic is one file and one command: write `plan/.md` with the same frontmatter keys, run `scripts/gen-plan-readme.py`, and stage both. The structure and both scripts come from lance.blue's `headquarters` repository. The one difference is that an epic names `crates` rather than `repos`. ## Complete Nothing open, nothing left to decide. These live in `complete/`, which does not exist yet. | id | title | |---|---| ## Shipped, with loose ends The exit criterion is met and something is still open in the file. | id | title | |---|---| | [cli](cli.md) | One `didbot` command, and a binary for each place the code runs | | [operator-ledger](operator-ledger.md) | The ledger says which operator ended an app's logins or an account's tokens | ## Open | id | title | status | |---|---|---| | [provenance](provenance.md) | A record says which agent, which model, and what spawned it | open | | [pds-writes](pds-writes.md) | A record is a signed commit in a repository | open | | [agent-accounts](agent-accounts.md) | An agent context becomes an account, and stops being one | open | | [account-types](account-types.md) | Not every account is a session | open | | [write-policy](write-policy.md) | Which agents may write which record types | open | | [auth-types](auth-types.md) | Every credential this server accepts, and what each one may do | open | | [oauth](oauth.md) | A third-party app signs in as an agent, with nobody at the consent screen | open | | [pds-xrpc](pds-xrpc.md) | A client nobody here wrote can talk to this server | open | | [federation](federation.md) | An off-the-shelf relay and an off-the-shelf app, not just the protocol | open | | [node](node.md) | A host holds one key, and creates an agent for every context on it | open | | [subagents](subagents.md) | A subagent is a context, so it gets an account | open | | [scope-policy](scope-policy.md) | An agent cannot be granted what its operator has not allowed | open | | [ownership](ownership.md) | The human names the agents and the agents name the human | open | | [vouch](vouch.md) | What an operator vouches, what an agent vouches, and what the server vouches | open | | [labels](labels.md) | What is true about an agent arrives as a label an app already draws | open | | [agent-sites](agent-sites.md) | An agent publishes a page of its own, at a name the zone already serves | open | | [policy-store](policy-store.md) | Policy is the operator's record, in the operator's repository | open | | [ai-preference](ai-preference.md) | A stranger's declared AI preference is a ceiling on what our agents may do | open | | [app-allowlist](app-allowlist.md) | An app an operator has not admitted gets nothing | open | | [e-stop](e-stop.md) | An operator can halt the swarm when nothing else is working | open | | [policy-dashboard](policy-dashboard.md) | The operator onboards a server and writes policy from a site this server does not serve | open | | [ops-dashboard](ops-dashboard.md) | The operator watches a running server and stops it, from a page nobody else can operate | blocked | | [analytics-dashboard](analytics-dashboard.md) | What this deployment's agents are doing, on a page the operator may make public | open | | [mentions](mentions.md) | One agent addresses another, and the other finds out | blocked | | [spaces](spaces.md) | Activity that is not public, without inventing a private tier | blocked | | [repo-scale](repo-scale.md) | A write costs the same in an old repository as in a new one | open | | [store-scale](store-scale.md) | The state is larger than the process holding it | open | | [zone-scale](zone-scale.md) | The zone runs out of names before it runs out of accounts | open | | [capacity](capacity.md) | A deployment stops minting before somebody else's quota does | open | | [dev-setup](dev-setup.md) | One command takes a new machine to a working stack | open | | [config](config.md) | How the server runs is one file; what an agent may do is never in it | open | | [deploy](deploy.md) | A named version runs somewhere that is not a laptop | open | | [alerts](alerts.md) | The server says when it is failing, somewhere its operator will see it | open | | [dns-providers](dns-providers.md) | A deployment publishes its zone somewhere other than Route53 | open | | [tls-sources](tls-sources.md) | A certificate comes from somewhere other than this server's own ACME client | open | | [services](services.md) | The component that can mint a hostname is not the component serving agent data | open | | [fleet](fleet.md) | One operator runs ten personal data servers and can see and stop all of them | open | | [adversarial](adversarial.md) | Integration tests at the seams, not inside the components that already pass | open | | [license](license.md) | The project has no license, and three different things need one | open | | [cost](cost.md) | What a deployment costs its operator, broken out by service | open | | [aws-deploy](aws-deploy.md) | A one-shot Terraform module an operator can consume without cloning this | open | | [cred-delivery](cred-delivery.md) | A write is bound to the context that made it, and the model holds nothing | open | | [abuse-notices](abuse-notices.md) | A stranger's moderation service can tell this server one of its agents is doing harm | blocked | | [account-data](account-data.md) | Every byte an account produces has one answer for when it goes | open | | [account-lifecycle](account-lifecycle.md) | An account's whole life is one reviewed state machine | open | | [alpha-exit](alpha-exit.md) | What an alpha deployment on the public internet must refuse or survive | open | | [blob-storage-tiers](blob-storage-tiers.md) | A blob lives on disk, in a bucket, or in both, and a deployment picks | open | | [browser-binding](browser-binding.md) | A web sign-in's code reaches only the agent's own browser | open | | [cedar-engine](cedar-engine.md) | Cedar is the second engine, and it never permits by accident | open | | [credential-rotation](credential-rotation.md) | An account's credentials change after it is created, and a replaced key stops working at once | open | | [dedupe-audit](dedupe-audit.md) | A workspace-wide sweep for drifted duplicate logic, dead code, and stragglers from removed backends | open | | [didjson-archive](didjson-archive.md) | The identity layer has a copy that is not the running server | open | | [handshake](handshake.md) | A server and its operator establish each other, with no shared secret | open | | [heartbeat](heartbeat.md) | One line an operator reads at a glance, that cannot grow | open | | [infra-review](infra-review.md) | What a first read of infra/ found, and what has to change before an apply | open | | [name-pools](name-pools.md) | A zone is a pool with a role, and names leave it only when its role allows | open | | [onboarding](onboarding.md) | An operator establishes a server before the server can establish anyone else | open | | [onboarding-policies](onboarding-policies.md) | A new operator is shown policies worth having, not an empty ruleset | open | | [periodic-backups](periodic-backups.md) | The server backs up its own data, and something reads it back | open | | [policy](policy.md) | What an agent may do is a set of denials, evaluated at the write, from three sources | open | | [policy-exemptions](policy-exemptions.md) | A policy can leave someone out without anyone being able to let themselves in | open | | [relay-sync](relay-sync.md) | A boot tells relays only what changed, and a restore tells them what it lost | open | | [site](site.md) | A stranger can read what this project is, without cloning it | open | | [tombstone-serving](tombstone-serving.md) | An account outlives the server that answered for it | open | | [updates](updates.md) | The project says what it has done, where the network can read it | open | | [web-launch](web-launch.md) | Checkers run by somebody else pass against the live deployment | open | | [witness](witness.md) | A stranger can check a `bot.did.registration` claim against something the server does not control | open | ## Continuous | id | title | |---|---| | [local-dev](local-dev.md) | The whole stack runs on one machine, with traffic that looks real | ## Not pursued Answered no. | id | title | |---|---| | [index](index.md) | One service watches every vouched server and says what changed | | [canvas](canvas.md) | A person can watch a swarm work | ## Where the lexicons are There is no lexicon epic. A `bot.did.*` schema is a deliverable of whichever epic needs the record. Doing it the other way round publishes a collection name nothing ends up using, and a collection name cannot be taken back once records exist in other people's repositories.