atproto git client
atgc plan
1 folder · 36 files

README.md

Plan #

What atgc is building, and roughly in what order. One file per epic. milestones.md is the other view of the same work: releases worth naming, in prose, without reference to any of this.

An epic is a line of work that takes many pull requests and has an exit criterion somebody could check. It is not a task list — the tasks live inside it, and in the code and its doc comments, which are where the reasoning behind a decision belongs.

Two facts shape all of it #

Everything is somebody's record. A pull request, an issue, an SSH key and a repo's description are records in the PDS of whoever wrote them, not rows in a forge's database. Opening a pull needs no permission on the target repo; a pull's state is a separate newest-wins log rather than a field; pushing a branch never updates anything. docs/architecture.md is the long version, and nearly every surprise in this register follows from it.

No index is trusted by default. The one thing a PDS cannot answer is "everything anybody wrote about this repo", and the index that can — Bobbin — stalls for hours at a time and answers about a stale world in the shape of a fresh answer. So every index is opted into, every listing says what it could not see, and indexes is an epic rather than a footnote.

The id is the commit scope #

Each epic's filename is its id, and that id never changes. plan/complete/agent.md is agent, so its commits read feat(agent): … and fix(agent): …. That is the whole reason the ids have no number prefix, and the reason there is no order field either: a filename or a field that has to be rewritten when something else moves is not an identifier. Archiving moves the file between directories and never renames it, for the same reason.

plan is a valid scope too, for changes to this register.

Not every scope in git log is an epic id, and that is not enforced by anything today. The ids were chosen to match the scopes already in use where one existed — stack reads as stacks and pr spans four epics — so adopting the rule is a decision about the commit hook, not a rewrite of this directory.

Order is advisory, and unnumbered #

The Open table below reads roughly top to bottom, and that is the whole of the ordering. There is no order field and no numbered column, because a total order over thirty epics is a lie that costs something to maintain: inserting one at the front would renumber every file beneath it and every row here, so two branches each adding an epic conflict by construction, over a number neither of them cares about.

What actually constrains the work is dependsOn, which says what genuinely cannot start first, and status, which says what is blocked. Those are per epic, they are true independently of what anybody else is doing, and moving an epic is moving one line.

The sequence the tables read in comes from order.txt, which is a list of ids and nothing else. It is advisory and it is optional: an epic it does not name still appears, alphabetically, at the end of whichever table it belongs to.

Status #

shipped — the exit criterion is met. The file stays, because how something was built and what it cost to learn is worth more after it works than before. A shipped epic with nothing open left moves to complete/; one with loose ends stays here until they close.

open — being worked on or ready to be.

blocked — cannot start until something in dependsOn lands.

continuous — no exit criterion and no completion date. Worked whenever adjacent code is open. Output contracts and documentation are like this: they are never done, and pretending otherwise just produces an epic that is permanently 80% complete.

declined — decided against. Not blocked, which is waiting: the answer is no rather than not yet. The file stays, because a decision that is not written down gets made again.

The tables are generated #

The five tables below are output, not source. scripts/gen-plan-readme.py reads every epic's frontmatter and rewrites what sits between the <!-- generated: … --> fences. Everything around them is hand-written and is passed through untouched, so prose can go anywhere except inside a fence. The plan-register prek hook runs the script with --check, which means a row that disagrees with the file it points at fails the commit, and plan-shape checks the epics themselves.

Adding an epic is therefore one file and one command: write plan/<id>.md with the frontmatter keys every other epic carries, run scripts/gen-plan-readme.py, and stage both. Nothing here is typed by hand — that is what stops two branches each adding an epic from conflicting over rows neither of them cared about.

Complete #

Nothing open, nothing left to decide. These live in complete/ so the list below stays the list of things somebody might work on. They are not deleted, and their ids stay valid commit scopes, because a follow-up fix to a finished epic is still that epic's commit.

id title
agent An agent arriving cold uses atgc correctly on the first try

Shipped, with loose ends #

The exit criterion is met and something is still open in the file.

id title
sign-in A person acts as their own ATProto account, from a terminal
credential-store The stored credentials survive a crash, and are nobody else's to read
identity One answer to what an identifier is, and who it names
keys A push from this machine is authorized before it is attempted
api Everything the lexicon has, reachable without a verb for it
http-bounds No stranger's response costs atgc unbounded time or memory
module-layout Where code goes is a compile error, not a convention
toolchain The build is cheap enough to run two of
brand atgc looks like one thing wherever it is drawn

Open #

id title status
pull-requests A pull request's whole life runs from the terminal open
branch-pulls A pull publishes the branch it claims open
pull-numbers A pull's number comes from an API, not from a page open
review Somebody else's pull can be read, compared and run here open
stacks A change that is several pull requests stays one chain open
issues An issue is filed, found and closed from here open
repos A repo is created, configured and taken apart from here open
search The index's full-text search is a command open
indexes An answer says which index it came from, and how far behind that index is open
doctor Whether the next command can work is one question open
logs What atgc did to somebody else's service is on disk afterwards open
report A bug reaches the tracker and feedback a board, from the terminal open
pipelines CI runs, and its runs are readable from here open
secrets A CI secret is set from the terminal, not from a web form blocked
artifacts A tag carries binaries somebody can download blocked
publishing atgc is installable by somebody who did not build it open
www Somebody who has not heard of atgc can find out what it is open
configuration Pointing atgc somewhere else is a setting, not a patch open
lexicons Record shapes come from the schema, not from reading somebody's JSON open
testing A defect in a sequence of commands is caught here, not in production open
dependencies What atgc ships is known, licensed, and not advised against open

The forge verbs read first because they are what somebody came here to run. The three that follow them — indexes, doctor, logs — sit in the middle rather than at the end because they are what makes a wrong answer from any of the others visible, and every one of them exists because an answer was wrong first. CI and everything downstream of a tag is one block near the bottom on purpose: it is the 1.0.0 arc, and it is deferred together rather than piecemeal. The tail is the tree's own hygiene, none of which is a feature and all of which is load-bearing for the next one.

Continuous #

id title
output Every command's output obeys the same five rules
docs Only what cannot be discovered by running the tool or reading the code

Not pursued #

Answered no. A file here is a list of answers rather than a line of work.

id title
declined What atgc is not building

Where the schemas are #

There is no lexicon epic in the sense of a schema track running ahead of the features. lexicons is about vendoring somebody else's: sh.tangled.* is Tangled's and app.userinput.* is userinput.app's, atgc authors none of them, and what that epic owns is keeping the generated types pinned to a fetched commit and honest about the records in the wild, which predate the schemas.

Every epic ends in Done #

Open work above, finished work as - [x] items under a trailing ## Done. An epic with nothing under Done says so.

That is what makes "is this finished" answerable by looking rather than by reading: an epic with no - [ ] left is a candidate for complete/.

Where TODO.md went #

This directory replaced it. TODO.md had become 3,400 lines of everything anybody had noticed, in fifteen sections that mapped to command families rather than to lines of work, with a decision record, a bug report, a rejected proposal and a one-line wish all wearing the same checkbox. Every entry in it now lives in the epic that covers it, verbatim where the wording was already right — which is nearly everywhere, because the entries were the good part.

Two entries did not survive the move, both because the work they describe had landed under a different entry: stack create's source question, answered by pushing the branch, and the three body readers, answered by term/body.rs. Both supersessions are recorded in the prose of the epic that took the rest (branch-pulls, module-layout), because an entry that quietly disappears is indistinguishable from one that was lost.

There is deliberately no general TODO file any more. Something worth writing down belongs in an epic — and if it belongs to no epic, that is the useful signal, not an excuse to start another list.