Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 27 additions & 24 deletions packages/agent-data/DECISIONS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
Non-obvious decisions only, grouped by business-logic flow. Anything not listed is left
to the implementer's judgment. Flag conflicts instead of silently deviating.
to the implementer's judgment. Flag conflicts instead of silently deviating. Keep
outdated decisions (no history).

A bullet is a person's pick, and says what it was picked over. What the code does belongs
in SPEC.md; a choice made while implementing is the implementer's judgment, not a
Expand All @@ -10,22 +11,23 @@ decision. An AI proposes a bullet and asks; it never adds or rewrites one.
import each other.
- Named for the `agent-data` branch, but no function hardcodes it: every one that touches
a branch takes it as an argument.
- `.branches/<branch>` holds a project's persistent checkouts, one per branch: the agents'
own (made by other packages) and the data branch's (made here). The directory name is
exported. Dotted so a `*` glob skips it: each checkout is a full copy, so a tool that
descends does N times the work.
- Hidden through the common git dir's `info/exclude`, never a committed `.gitignore`: the
library must not touch tracked files; a per-worktree `info/exclude` is never read, and
one line there, written once, covers every checkout. Best-effort: the checkout stands
even when the rule could not be written.
- Every git call has a time budget; a call that outruns it fails as a timeout, its own
error kind, never as a plain failure: a killed `push` may have half landed.
- `.branches/` holds a project's persistent checkouts, one per branch at
`.branches/<branch>`: the agents' own (made by other packages) and the data branch's
(made here). The directory name is exported. Dotted so a `*` glob skips it: each
checkout is a full copy, so a tool that descends does N times the work.
- `.branches/` is hidden through the common git dir's `info/exclude`, never a committed
`.gitignore`: the library must not touch tracked files; a per-worktree `info/exclude` is
never read, and one line there, written once, covers every checkout. Best-effort: the
checkout stands even when the rule could not be written.
- Every git call through the package's own runner has a time budget; a call that outruns
it fails as a timeout, its own error kind, never as a plain failure: a killed `push` may
have half landed.

## The branch
- A branch of the project's repository holds the agents' data (tickets, the queue) the way
`gh-pages` holds a site; code branches hold only code. Every write that changes
something pushes, and a pull is a cycle of its own, so a machine that writes nothing
still gets what the others pushed.
something pushes when there is a remote, and a pull is a cycle of its own, so a machine
that writes nothing still gets what the others pushed.
- One branch for all skills, each with its own folder or file. Not one per skill: every
extra branch needs its own checkout and its own sync failure to report.
- Missing locally, it is adopted from origin's copy; missing there too, it is born an
Expand All @@ -52,17 +54,18 @@ Fetch what others pushed → make the change → commit → push.
resets it, new files included. Both try the push twice; the command's write then throws
with nothing left to retry, the process's never throws: its callers are background
ticks.
- A write is a re-runnable function, not a finished commit: a lost race winds the
attempt's commit back and runs the function again on the new files, so the change lands
once. The message is the caller's: fixed, or a function run after the change, since a
batch only knows what it did once done. Never a force push. After two failed pushes the
process's write reports the failure and the commit stays local in its checkout; the next
write or pull rebases it onto the remote and pushes it with the new one. When that
rebase conflicts the checkout is reset to origin's tip: the remote wins, every unpushed
commit is dropped unreported, only the current change runs again.
- An op is handed a directory and writes into it. Parent directories are created: git
keeps no empty directory, so a skill's folder vanishes with its last file and is absent
on a new branch.
- A write is a re-runnable function, not a finished commit: a push that fails, a lost race
or anything else, winds the attempt's commit back and runs the function again on the new
files, so the change lands once. The message is the caller's: fixed, or a function run
after the change, since a batch only knows what it did once done; the library's own are
`create the <branch> branch` for the birth and `sync` for the pull. Never a force push.
After two failed pushes the process's write reports the failure and the commit stays
local in its checkout; the next write or pull rebases it onto the remote and pushes it
with the new one. When that rebase fails, a conflict or anything else, the checkout is
reset to origin's tip: the remote wins, every unpushed commit is dropped unreported,
only the current change runs again.
- An op is handed a directory and writes into it; git keeps no empty directory, so a
skill's folder vanishes with its last file and is absent on a new branch.
- The remote is always `origin`; a repository without one is remote-less whatever other
remotes it has. Then the process's write commits locally and reports no error, a
command's write refuses (an outcome, not a throw), and the pull reports an error:
Expand Down
16 changes: 8 additions & 8 deletions packages/agent-data/src/file-branch.SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ A branch used as a file store: a branch of the project's repository holding file
- **One branch, holding files nobody edits by hand** - the caller names it; nothing on it is anyone's working tree, so it can be pushed and pulled eagerly, which is what gives every machine and every cloud session the same view.
- **Born parentless, or adopted from origin** - a branch origin already has is adopted; a branch that has to be created here starts from an empty commit with no parent, so no code commit is ever an ancestor of the file history.
- **The persistent checkout, hidden from the project's git** - the branch is checked out at `.branches/<branch>`, named after its branch like every other checkout there, and that directory is hidden from the project's git.
- **One funnel, serialized per branch** - every write a long-lived process makes goes through one cycle — sync, run the operation, commit, push — and cycles for the same branch never interleave.
- **One funnel, serialized per branch** - every write a long-lived process makes goes through one cycle — sync, run the operation, commit, push — and cycles for the same branch never interleave. Reads do not wait for a cycle: a read during one may see the operation's files before they are committed, or none at all while a failed cycle is being put back.
- **A write is an intent, not a commit** - when a push loses a race, the cycle syncs again and re-runs the operation against the fresher state instead of force-fitting a stale commit.
- **A push is owed until it lands** - a commit that could not be pushed stays local, and the next cycle carries it out, even when that cycle writes nothing of its own.
- **Conflicts resolve toward origin** - the checkout is nobody's working tree, so origin always wins and the local intent is re-applied on top.
Expand All @@ -29,7 +29,7 @@ A branch used as a file store: a branch of the project's repository holding file
- **A repository with no remote** - no `origin`, whatever other remotes it has: fine for a funneled write, an error for the pull, a refusal for a detached write.
- **Reading from anywhere in the repository** - the persistent checkout when this location has one, else the branch's ref, else origin's copy of it, so a reader holds no copy of the files; a read can ask for a fresh copy instead.
- **A reader that fetches once** - a reader opening many files fetches up front, picks one ref, and takes every read off it.
- **A detached one-shot write** - a command in any clone writes through a throwaway worktree on origin's tip and pushes straight to the branch, never touching the persistent checkout.
- **A detached one-shot write** - a command in any clone writes through a throwaway worktree on origin's tip and pushes straight to the branch, never touching the persistent checkout. On its way out it prunes the repository's stale worktree registrations, whatever branch they were for, as does the creation of the persistent checkout.
- **The files an operation writes** - reading, writing, deleting and listing the files under the directory an operation is handed, with parent directories created on write.

## Business logic
Expand Down Expand Up @@ -60,7 +60,7 @@ A long-lived process reads these files on every tick and writes them many times

The branch is checked out at `.branches/<branch>` — under the project, named after its branch, exactly like the agent checkouts beside it. The directory is hidden from the project's git through the repository's own exclude file, so no sweeping `git add -A` can commit it onto a code branch and no tracked file changes to achieve that; this may well be the first checkout the project ever gets.

Making sure the branch and its checkout exist is idempotent and cheap once they do — one read of the checkout's branch. A registration left behind by a directory someone deleted by hand is pruned first, so it cannot block the checkout being made again. Nothing here fails outright: a project this cannot be set up in reports why and is otherwise left alone.
Making sure the branch and its checkout exist is idempotent and cheap once they do — one read of the checkout's branch. A registration left behind by a directory someone deleted by hand is pruned first, so it cannot block the checkout being made again; the prune covers the whole repository, so a stale registration of any other checkout goes with it. Nothing here fails outright: a project this cannot be set up in reports why and is otherwise left alone.

### One funnel, serialized per branch

Expand All @@ -70,7 +70,7 @@ Two background jobs write the same files in the same moment. Neither may see, or

#### Business logic

Every write a long-lived process makes goes through one cycle: make sure the branch and its checkout exist, sync with origin, run the operation against the checkout, commit whatever it changed, push. Cycles are serialized per repository and branch, so two of them can never interleave — the eager pull included, since it is the same cycle applying no change.
Every write a long-lived process makes goes through one cycle: make sure the branch and its checkout exist, sync with origin, run the operation against the checkout, commit whatever it changed, push. Cycles are serialized per repository and branch — the repository as the caller spells its path, so two spellings of one path are two independent chains — so two of them can never interleave — the eager pull included, since it is the same cycle applying no change. Reads are not part of the chain: a read during a cycle may see the operation's files before they are committed, or none at all while a failed cycle is being put back.

A cycle never throws, because its callers run on background ticks with nobody to catch a failure. It reports one of three outcomes: it succeeded, saying whether anything changed and whether the push landed; or it failed with the change committed locally, meaning only the push failed and the next cycle will carry it; or it failed with nothing committed at all.

Expand All @@ -84,7 +84,7 @@ Another machine pushes to the branch in the instant between this machine's sync

#### Business logic

The change is expressed as an operation that can be run again, not as a fixed commit. When the push loses the race, the cycle syncs again and runs the operation against the fresher state rather than force-fitting a stale commit — the operation is the intent, the commit is only its serialization. It tries twice; a push that still fails, most likely for want of a network, keeps the commit locally and reports that it did, so the caller knows the change survived even though it is not yet shared.
The change is expressed as an operation that can be run again, not as a fixed commit. When the push is rejected, for a lost race or any other reason, the cycle syncs again and runs the operation against the fresher state rather than force-fitting a stale commit — the operation is the intent, the commit is only its serialization. It tries twice; a push that still fails, most likely for want of a network, keeps the commit locally and reports that it did, so the caller knows the change survived even though it is not yet shared.

### A push is owed until it lands

Expand All @@ -96,7 +96,7 @@ Whenever the local branch holds commits origin does not, the cycle pushes — ev

#### Business logic

Syncing fetches origin's copy of the branch and replays whatever local commits exist on top of it. When that cannot be done cleanly, the checkout is reset to origin's state outright. Nothing is lost by that, because the local intent is re-applied immediately afterwards by the cycle that is running — the whole design rests on the operation being re-runnable.
Syncing fetches origin's copy of the branch and replays whatever local commits exist on top of it. When that replay fails for any reason — a conflict, or a rebase that cannot start — the checkout is reset to origin's state outright. Nothing is lost by that, because the local intent is re-applied immediately afterwards by the cycle that is running — the whole design rests on the operation being re-runnable.

### A failed operation leaves nothing half-written

Expand Down Expand Up @@ -154,9 +154,9 @@ An agent runs a command in its own checkout that has to add to these files. It m

#### Business logic

A detached write fetches origin's tip of the branch, checks it out in a throwaway worktree outside the repository, runs the operation there, commits, pushes straight to the branch, and removes the worktree afterwards — registration and directory both, whether it succeeded or not.
A detached write fetches origin's tip of the branch, checks it out in a throwaway worktree outside the repository, runs the operation there, commits, pushes straight to the branch, and removes the worktree afterwards — registration and directory both, whether it succeeded or not, pruning the repository's stale worktree registrations with it, whatever branch they were for.

It follows the same intent rule as the funnel: a push that loses a race re-fetches, resets the worktree to origin's tip and runs the operation again, twice in all; a push that still fails is raised with git's own reason. A branch origin does not have yet is born by the write itself, parentless, exactly as it would be locally. An operation that writes nothing commits nothing and reports that it changed nothing. A repository with no remote is refused, and says so as its own outcome rather than as a failure.
It follows the same intent rule as the funnel: a push that loses a race re-fetches, resets the worktree to origin's tip and runs the operation again, twice in all; a push that still fails is raised with git's own reason, and so is anything else that fails — a worktree that cannot be made, a commit that fails, an operation that throws; only the missing remote is an outcome rather than a failure. A branch origin does not have yet is born by the write itself, parentless, exactly as it would be locally. An operation that writes nothing commits nothing and reports that it changed nothing. A repository with no remote is refused, and says so as its own outcome rather than as a failure.

It never touches the persistent checkout, and it never moves the local branch ref: the machine running a long-lived process converges on its own next pull.

Expand Down
4 changes: 3 additions & 1 deletion packages/agent-data/src/git.SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Running git on a caller's behalf: every command gets a time budget matched to it
## Business logic — TL;DR

- **Every git command gets a time budget matched to its cost** - reads get the shortest budget, ordinary local mutations a longer one, and anything touching the network or writing a whole checkout the longest.
- **A timeout is not a git failure** - a command killed for outrunning its budget is reported as a timeout, recognisable across package boundaries, so a caller can clean up after an interrupted checkout creation without mistaking git's own refusals for one.
- **A timeout is not a git failure** - a command killed for outrunning its budget is reported as a timeout, recognisable across package boundaries, so a caller can clean up after an interrupted checkout creation without mistaking git's own refusals for one. A command whose output exceeds 16 MB is killed too, but reported as a plain failure, not a timeout.
- **"Not a repo" is distinguishable from "git failed"** - a directory can be asked whether it sits inside a git working tree; anything unreadable reads as "not a repo".
- **The checkout from anywhere inside** - from any directory in a repo, the root of the checkout it is in can be read.
- **The line worth showing** - a failed invocation is reduced to git's own `fatal:` / `error:` / `remote:` line when there is one, else its first line.
Expand All @@ -27,6 +27,8 @@ Each git invocation is classified by its subcommand, ignoring any leading global

Removing or pruning worktrees counts as an ordinary local mutation; listing them counts as a read.

Every invocation's output is capped at 16 MB, enough for a large checkout's file listing; a command that exceeds it is killed and reported as a plain failure, never as a timeout.

#### Rationale

A single flat budget, sized for reads, once covered every git call. That made the two slowest operations — creating a worktree and pushing — routinely die mid-flight on large repos, which is precisely the case where being killed is most destructive.
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-data/src/index.SPEC.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
The package's main entry point: it gathers the two names, the git runner, the exclude rule and the branch used as a file store into one place for a caller to import. No business logic of its own. The two names are also reachable on their own (`names`), for code that runs in a browser and must not pull in git.
The package's main entry point: it gathers the two names, the git runner, the exclude rule and the branch used as a file store into one place for a caller to import. No business logic of its own. The git time budgets and the classification of a command stay inside the package: they are not exported. The two names are also reachable on their own (`names`), for code that runs in a browser and must not pull in git.

## Before modifying/creating SPEC.md files

Expand Down
Loading
Loading