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.