README
¶
stoa
A shared context store for several people and all of their agents, kept as plain markdown in a git repository.
Stoa is a protocol first and a tool second. A shared knowledge store without write discipline does not remove write contention, it centralises it: every member editing the same notes, and every agent session racing every other one. So the rules come first, and the CLI exists to make the rules the easy path.
- The protocol in five lines
- Why it does not conflict
- Install
- Quick start
- Everyday use
- Consolidating
- Command reference
- Store layout
- Configuration
- Environment variables
- Enforcement
- Adopting the protocol in an existing repository
- Safety
- What it is not
- Development
The protocol in five lines
- Every member has an append-only journal. It is the only thing they write.
- Nobody edits canonical notes by hand, not humans and not agents — except whoever currently holds the consolidator lock, which is the whole point of the lock.
- One rotating consolidator, holding a git ref as a lock, folds journal entries into canonical notes and drains them. Rotating means the role is a lock anyone eligible can take, not a rota the tool keeps for you.
- A read returns canonical plus pending entries, so information is visible the moment it is written. Only tidiness waits for consolidation.
- Membership is flat: everyone in a store reads everything. Privacy boundaries are drawn by running a separate store, never by carving one up.
Why it does not conflict
Members write different files, so they cannot collide with each other. Appends always land at the end of a journal file and drains always remove whole entries from above, so a consolidation run and a concurrent append do not overlap either. Git's three-way merge resolves the two without help.
That leaves one real race, two processes on the same machine touching git at
once, and stoa serialises those behind a repository lock file rather than
letting git fail with index.lock. It is removed when the command finishes,
and one left behind by a crash is broken automatically once it is older than
the operation could legitimately be — half a minute for a local append, five
for anything reaching the network. It is not the consolidator lock, it has no
lease to watch, and it never needs breaking by hand.
The cost of the design is that canonical notes lag behind reality by one consolidation run. Overlay reads are what make that lag harmless: nothing is invisible while it waits, only untidy.
Install
go install github.com/Chandler-Thompson/stoa/cmd/stoa@latest
Or from a checkout. The second form is the same build with a real version
string stamped in instead of dev; pick one, not both:
go build -o stoa ./cmd/stoa && install -m755 stoa ~/.local/bin/
go build -ldflags "-X main.version=$(git describe --tags --always)" \
-o stoa ./cmd/stoa && install -m755 stoa ~/.local/bin/
There are no prebuilt binaries; a Go toolchain is the only way in.
Go 1.26 or newer, one dependency (BurntSushi/toml). It cross-compiles
anywhere Go does, Windows members included.
The tool is a single binary. It shells out to git, which must be on PATH.
There is no daemon, no service and no database — but note what the last
enforcement layer costs: the only unbypassable protection is push rules on the
git host, and the ruleset template shipped here is GitHub's. On another host
you keep the protocol and the first three layers, and you write that wall
yourself. See Enforcement before committing to a host.
Applying it needs the gh CLI once, at setup; nothing else ever does.
Quick start
Start a store
Create an empty repository on your host first, with no initial commit, and have its URL to hand.
stoa init --new-store --name my-store --as alice ./my-store
cd my-store
git remote add origin <url> && git push -u origin main
Scaffolding creates the layout, the config, the enforcement templates, the
pre-commit hook, a README.md and an AGENTS.md for the store's own members
and their agents, and a first commit. It registers you as the store's first
member with consolidator = true, so you can take the lock immediately;
git_email comes from --email, or from this machine's user.email if you
omit it. Check that it is the address your commits actually carry, because the
CI check matches on it — stoa doctor fails loudly if the two disagree. It then prints the remaining steps, of
which the important one is applying the server-side ruleset:
$EDITOR .stoa/ci/ruleset.json # bypass_actors: you, plus anyone else who will consolidate
gh api -X POST repos/<owner>/<repo>/rulesets --input .stoa/ci/ruleset.json
At this point the store has exactly one member, you, so bypass_actors names you alone unless you already know who else will consolidate.
This is the one step that needs the GitHub CLI, and the one step that actually makes the store safe. Until it is done you have a protocol and three layers of politeness.
bypass_actors holds host identities, not stoa member ids. The two lists
are different namespaces naming the same people, which is why they can drift
and why stoa doctor keeps asking about it — doctor only checks that the
template file is present in the repo; it never calls the host's API, so it
cannot see the live ruleset at all.
The template ships the list empty because its entries are GitHub's shape, not stoa's: each is an actor id plus an actor type, and which types a repository can name depends on whether it is personal or organisation-owned. Take the shape from GitHub's rulesets API reference rather than from anything here, and verify the result on the host rather than assuming:
gh api repos/<owner>/<repo>/rulesets # is it there, and active?
gh api repos/<owner>/<repo>/rulesets/<id> # does bypass name who you meant?
Then prove it the only way that counts, with a negative push:
# as a member who is NOT a bypass actor, in their own checkout
echo x >> canonical/INDEX.md
git commit -am "should be refused" --no-verify && git push # must be REJECTED
This test cannot be run yet. It needs a member who is not a bypass actor,
with push access to the host, and right now the only such person is you — and
you are a bypass actor. Come back to it once your first ordinary colleague has
both a [[members]] block and host access, and do not treat the wall as proven
until you have watched that push fail. A ruleset that exists and does not
enforce looks exactly like one that does.
This is the one part of stoa that no test in this repository can check, and it has not been proven on a real host by its author. Treat the setup above as untested instructions, not as a guarantee.
To change an applied ruleset later, edit it on the host or replace it by id:
gh api repos/<owner>/<repo>/rulesets # find the id
gh api -X PUT repos/<owner>/<repo>/rulesets/<id> --input .stoa/ci/ruleset.json
Then add your members, as below.
Adding a member
.stoa/config.toml is behind layer 4 like everything else, so this is a
consolidator action — but it is not a consolidation. stoa consolidate commit
stages canonical notes, journals and INDEX.md; it will not carry a config
change. Do it with git, while holding the lock:
stoa lock claim
$EDITOR .stoa/config.toml # add their [[members]] block
git commit -am "add member bob" && git push
stoa lock release
The lock is what makes the pre-commit hook allow the write, and bypass-actor
status is what makes the host accept the push. Without the lock the hook
refuses it, which is the intended behaviour and not a bug. Promoting somebody
to consolidator = true means doing this and adding them to the ruleset's
bypass actors; either one alone leaves them eligible on paper and refused in
practice.
Join a store
Joining takes two separate grants, and it is easy to get one and not the
other. You need push access to the repository on the host, which whoever
owns it gives you the ordinary way, and you need an id in
.stoa/config.toml, which a consolidator adds. Without the first you cannot
push; without the second stoa will not let you write.
Get added to .stoa/config.toml first. The id a consolidator gives you is
the id you use below; stoa init refuses one the store does not know, which is
deliberate, because membership is the store's only real boundary. Running
stoa init with no --as lists the members the store does know, which is the
quickest way to see whether you are in yet.
git clone <url> && cd my-store
stoa init --as bob
stoa doctor
--as records who this machine writes as in repository-local git config
(stoa.author), not in the tracked config. Each id is its own [[members]]
block: a person and their agent are two members, so bob on a laptop and
bob-agent in an agent's checkout are separate ids whose entries are
attributed separately. The human and agent fields inside a block say who
is behind that one id; they do not bundle two identities into one.
Everyday use
Read
stoa read projects/some-project # every note in a domain
stoa read projects/some-project/overview.md # one note
stoa search "deploy key" # grep, pending entries marked
A read pulls (fast-forward only, non-fatal if there is no network), prints the canonical notes, and then prints anything pending underneath:
=== canonical/projects/some-project/overview.md ===
# Some project
...
--- PENDING (not yet consolidated) — 2 entries ---
[a1b2c3d4] FACT by alice at 2026-07-24 21:30Z -> canonical/projects/some-project/overview.md
nightly build moved to GH Actions
Targets are forgiving. projects/foo/bar, projects/foo/bar.md and
canonical/projects/foo/bar.md all mean the same note. Reading a note nobody
has written yet is fine and still shows the pending entries aimed at it.
Write
stoa append FACT projects/some-project/overview.md -m "nightly build moved to GH Actions"
stoa append QUESTION projects/some-project/overview.md -m "who owns the deploy key now?"
# longer bodies come from stdin
stoa append DECISION areas/infra/backups.md <<'EOF'
Weekly restore drills, first Monday, owned by whoever holds the pager.
Rejected: quarterly drills. Too slow to catch a broken backup.
EOF
Each append is stamped, committed and pushed on its own. A rejected push is
retried behind a pull --rebase, so falling behind the remote resolves itself
rather than piling up; journals are per-author files, so there is nothing for
the rebase to conflict with. If the push still fails — no network, usually —
that is reported but does not fail the append: the entry is committed locally,
and since git pushes every commit on the branch, your next successful append
carries it. There is no stoa sync; a plain git push does the same job if
you want it gone now. Use --no-push to batch deliberately.
| Type | Meaning |
|---|---|
FACT |
new information for a target note |
CORRECTION |
canonical is wrong, here is the fix |
DECISION |
a decision was made and is now binding |
QUESTION |
an open question for the store |
PROMOTE |
this is durable, keep it where people will find it |
DIBS / RELEASE / DONE |
calling and closing a task (see below) |
A store can declare more types in entries.extra_types. Unknown types are
refused at write time rather than discovered later by a confused consolidator.
Types are instructions to the consolidator, not switches in the tool. The
difference between a FACT and a PROMOTE is what the person or agent
holding the lock does about it; nothing in stoa files them differently.
Tasks — dibs
stoa dibs ship-the-thing
stoa dibs --list # who has called what
stoa done ship-the-thing # or: stoa release ship-the-thing
Dibs is not a lock. It gates nothing, refuses nobody, and grants no exclusive access — it publishes a timestamped note saying you got there first, so everyone else can choose to leave the work alone. It is called dibs because everyone already knows that calling dibs is not enforcement and that you honour it anyway.
Stoa deliberately has both kinds of coordination, and they should never be confused:
stoa dibs <task> |
stoa lock claim |
|
|---|---|---|
| Enforced by | participants honouring it | the git host rejecting the push |
| Can be ignored | yes, by anyone, at no cost | no |
| Protects | nothing; it informs | canonical/, INDEX.md, .stoa/ |
| Costs when abused | duplicated work | nothing — it does not yield |
That difference is worth stating plainly to every human and agent joining a
store, because dibs resembles a lock closely enough that the natural
assumption — "I have it, so nobody else is touching this" — is false and fails
silently. The scaffolded AGENTS.md teaches it explicitly for that reason.
Use these commands, never stoa append DIBS. The append would write a valid
entry, but only stoa dibs tells you how the race actually resolved, so you
could lose one and never hear about it.
Task targets are opaque keys, not note paths — you and your colleagues agree on
a name, and there is no separate board to keep in step. The race is decided
with no human in the loop: among live dibs on a task, the earliest timestamp
wins and later callers yield. Timestamps come from each machine's own clock
in UTC, so a member whose clock is badly wrong can win or lose a race it should
not have; on any machine not running NTP, that is worth fixing before it is
worth debugging. Two agents that call the same task inside the sync window
reach the same answer independently as soon as they can see each other's
journals, which is why stoa dibs reports the resolved state rather than just
the write:
NOT yours: ship-the-thing was called by alice at 2026-07-24T21:30:11.412Z — you yield
Open dibs are never drained by consolidation. They have to stay on the read path for as long as the work is open, or a tidy-up would quietly make an in-progress task look free.
Dibs lapse
Dibs stay live for dibs_lease_hours (default 24) and lapse if the holder does
not call the task again. Calling dibs again renews; past the lease, the next
caller takes the task over instead of yielding.
The lease bounds time since the holder was last heard from, not how long a task may be held — a holder who keeps renewing keeps the task indefinitely, because the renewals are the liveness signal. What it bounds is the damage when somebody stops answering: the configured lease is the maximum time a task name can sit blocked on a member who left.
Two properties are worth knowing:
- The duration is store policy, not per-entry data. A
DIBSentry carries no duration of its own, so no member can grant themselves a longer one — not through a flag, and not by hand-writing a journal entry. Changing it means editing.stoa/config.toml, which needs the consolidator lock and leaves a commit. - Lapsing never changes resolution, only display. Who holds a task is
computed from entry timestamps alone, so two members reading the same
journals always agree regardless of when they look or how well their clocks
are set. A lapsed task still reports its holder — rendering it as free would
erase the only evidence somebody had started it — and is marked
LAPSEDinstoa dibs --listand in the consolidation plan.
DIBS was called CLAIM before the distinction above was made explicit.
Existing CLAIM entries are still read, so no store loses its history; writing
one is refused, with the new name in the error.
Consolidating
Folding entries into prose is judgment work: merging wording, noticing that two members contradict each other, deciding when a note has grown enough to split. A script does it badly, and doing it badly corrupts the store's memory quietly.
So the tool owns the mechanics (take the lock, show the work, drain exactly what was folded, commit it atomically) and leaves the judgment to whoever holds the lock, which may be a person or an agent.
stoa lock claim # take the consolidator role
stoa consolidate plan # everything pending, with its target notes
stoa consolidate plan --json # the same, machine-readable
# ... edit the canonical notes by hand or with an agent ...
stoa consolidate commit --drain a1b2c3d4,e5f6a7b8 -m "fold this week" --release
stoa consolidate plan --preview shows what a run would cover without taking
the lock, which is the safe way to check whether a run is worth starting.
Rules worth knowing before your first run:
- Draining without editing canonical is refused. Dropping an entry on
purpose (spam, a duplicate, a mistake) is a real need, so it has an explicit
--discardthat demands a reason in-m. --draintakes the ids you actually folded. Those are the short ids in brackets on the read path and inplan— content hashes of the entry, not git commit ids. The drain re-reads the journals at commit time and filters by id, so an entry appended while you were working simply is not in the set and survives untouched.- Canonical edits and the drain go into one commit. A consolidator killed halfway leaves a store that is merely untidy, never inconsistent.
- The lease expires. Default 45 minutes. A lapsed lease cannot be extended,
only re-claimed, and an expired lease does not count as holding the lock for
any other purpose either — the pre-commit hook will refuse your canonical
writes exactly as it would a member's. If your lease runs out mid-run, take the lock again before
writing canonical — and if somebody else took it while you were lapsed, you
do not get it back. Your edits are still sitting in your working tree, so
stash or keep them, wait for the lock, and re-run
consolidate planbefore reusing any of it: the entries you were folding may already be drained. - The commit is pushed for you, then the lock is released. In that order, and a failed push stops before the release, so a store that would not accept your consolidation does not also lose its lock holder. You keep the lock and can retry.
stoa consolidate abortreleases the lock without draining anything. It refuses while canonical has uncommitted edits, so a half-finished run cannot be walked away from silently.- A bad run is undone with git, not with stoa. Canonical edits and their
drain are one commit, so
git revertof that commit restores both the notes and the entries it consumed. There is no separate undo, and there does not need to be.
Moving a note into archive/ is an ordinary consolidator edit too: git mv
it during a run, and update whichever INDEX.md pointed at it.
A journal entry that should never have been written is a different problem,
because appends push immediately and everyone can already read it. Removing it
at consolidation with --discard stops it reaching canonical; it does not
unsend it, and it stays in git history. Treat a leaked secret as leaked and
rotate it. That is why the rule is to reference secrets by location rather
than to rely on cleaning up afterwards.
Cadence is lazy by design: on a schedule, at the end of a session, or when somebody notices the plan getting long. It is housekeeping, not propagation.
The lock
Claiming the consolidator role pushes a fresh root commit to a branch named
consolidator-lock. If the branch already exists the push is rejected as a
non-fast-forward, and that rejection is the mutex, because a ref update is
atomic on the server. No lock service, no daemon, nothing to operate.
stoa lock status # free, or who holds it and for how much longer
stoa lock claim # take it (consolidator-eligible members only)
stoa lock extend # bump the lease while it is still live
stoa lock release # give it up
stoa lock break # steal an EXPIRED lease; refuses a live one
Command reference
| Command | Flags | What it does |
|---|---|---|
stoa init [dir] |
--as <id>, --new-store, --name <s>, --email <e> |
register this checkout, or scaffold a new store; installs the pre-commit hook |
stoa read <target> |
--no-pull |
canonical note or domain, plus the pending overlay |
stoa search <term> |
-s (case-sensitive) |
grep canonical and journals, marking what is pending |
stoa append <TYPE> <target> |
-m <text>, --no-push |
append one entry to your own journal (body from -m or stdin) |
stoa dibs <task> |
-m, --no-push |
call a task, and report how the race resolved |
stoa dibs --list |
--all, --no-pull |
who called what; --all includes finished and free tasks |
stoa release <task> |
-m, --no-push |
give a task back |
stoa done <task> |
-m, --no-push |
finish a task; implies release |
stoa consolidate plan |
--json, --preview, --no-content |
what is pending and the notes it targets |
stoa consolidate commit |
--drain <ids>, -m <msg>, --release, --discard |
write canonical edits and the drain as one commit |
stoa consolidate abort |
release the lock, drain nothing | |
stoa lock <sub> |
status, claim, extend, release, break |
the consolidator mutex — the only thing here that actually stops anyone |
stoa check |
--staged, --commits <base>..<head> |
verify changes obey the write model (used by the hook and CI) |
stoa doctor |
identity, hook, templates, lock, caps, secrets, journal health | |
stoa version |
print the version |
Flags may appear before, between or after positional arguments, so
stoa append FACT some/note -m "..." works the way it reads.
Store layout
README.md how this store works, for its members
AGENTS.md the same, written as instructions for their agents
INDEX.md root index — pointer lines only
canonical/
projects/<project>/ work with a defined end state
areas/<area>/ ongoing domains
resources/ reference material
archive/ dead weight, out of every read path
journal/
<member-id>/ per-author, append-only
<machine>-<session>.md
.stoa/
config.toml store settings and the member registry
ci/check-protocol.sh layer 3, POSIX shell
ci/ruleset.json layer 4 template
.github/workflows/ the CI check wiring
PARA-lite, plus the one good idea borrowed from zettelkasten: small, densely
linked, single-subject notes. Filenames are kebab-case and descriptive, never
timestamp ids, because agents navigate by grep and index and humans navigate by
filename, and neither is helped by 202607242130.md.
Notes have a size cap. Past it the consolidator splits into linked notes rather
than letting one grow without limit. Indexes are pointer lists, never content:
an index that accumulates content becomes a second copy of the store and starts
drifting from the first. stoa doctor reports every note over its cap.
Configuration
.stoa/config.toml is tracked, so every member reads the same settings.
Changing it is a consolidator action, which is deliberate.
| Key | Default | Meaning |
|---|---|---|
store.name |
(required) | display name |
store.lease_minutes |
45 |
how long a consolidator holds the lock before anyone may break it. 0 switches the mutex off entirely and stoa doctor reports it as a fault |
store.dibs_lease_hours |
24 |
how long dibs stay live before lapsing. Both the default and the ceiling: entries carry no duration of their own, so this is the maximum time a task name can sit blocked on a member who left |
store.note_max_lines |
300 |
canonical note cap; past it the consolidator splits |
store.index_max_lines |
200 |
index cap |
store.auto_push |
true |
push each append immediately |
store.canonical_dir |
canonical |
where canonical notes live |
store.journal_dir |
journal |
where journals live |
entries.extra_types |
[] |
additional entry types, upper case |
[[members]] |
one block per member: id, human, agent, git_email, consolidator |
One block per id. human and agent record who is behind that id — a person's
block leaves agent empty, an agent's block names both — so an agent is its
own member rather than a mode of its human's, and commits as itself. Flat trust
covers people; a trusted person can still have a compromised agent, so
provenance stays attributable to (human, agent, machine, time) through the
journal path, the CLI's stamp, and the commit identity.
[[members]]
id = "bob"
human = "Bob Ruiz"
agent = ""
git_email = "bob@example.com"
consolidator = false
[[members]]
id = "bob-agent"
human = "Bob Ruiz"
agent = "claude-code"
git_email = "bob+agent@example.com"
consolidator = false
git_email is the commit identity the CI check matches entries against, so it
has to be the address that member's commits actually carry.
consolidator = true grants eligibility to hold the lock. Keep that set in
step with the ruleset's bypass actors on the server, or an eligible member will
be refused by the server anyway. stoa doctor says so every run.
Removing a member is deleting their block, plus removing them from the bypass actors if they held the lock. That ends their writes and their access to everything added afterwards. It cannot un-share the history their clone already has, which is the usual property of a git repository rather than a gap in this one: rotate whatever was protected only by their membership.
Environment variables
| Variable | Effect |
|---|---|
STOA_STORE |
operate on this store instead of discovering one from the working directory |
STOA_SESSION |
group many invocations into one journal file; defaults to the UTC date |
STOA_MACHINE |
override the machine name in journal filenames; defaults to the hostname |
Agents that make many appends inside one logical session should set
STOA_SESSION so those entries land in one file.
Enforcement
| Layer | Mechanism | Catches |
|---|---|---|
| 1 | the CLI, where the right path is the easy one | honest mistakes |
| 2 | pre-commit hook installed by stoa init |
violations before they leave the machine |
| 3 | CI check (.stoa/ci/check-protocol.sh) |
anything that used --no-verify |
| 4 | GitHub ruleset blocking canonical/**, INDEX.md and .stoa/** |
everything else |
Layers 1 to 3 are convenience and can be bypassed by anyone determined. Layer 4 is the wall, because the server refuses the write.
Note what layer 4 covers: .stoa/** is in there, so the member registry and
the consolidator flags are behind the same wall as the notes. Nobody can add
themselves to a store, or promote themselves to consolidator, by pushing an
edit to .stoa/config.toml — that is a bypass-actor write, exactly like
canonical. A store whose ruleset has not been applied has none of this.
Note also what it does not cover: journal/**. Members have to be able to
push their own journals, so the wall cannot police them, and "you write only
your own journal" is enforced by layers 1 to 3 alone. A determined member can
therefore write into a colleague's journal or rewrite their own history, and
the store will take it. What that costs them is deniability, since every entry
is attributable through the commit identity, and git history records the edit.
Flat trust means trusting people; this is one of the places it shows.
The client-side and CI implementations of the same rule are written twice, in Go and in POSIX shell, because CI has to enforce the protocol on a runner that has never heard of Go. Two implementations can drift, so the acceptance suite runs both against the same cases and fails if they ever disagree.
stoa doctor reports on everything else, but whether the ruleset is actually
applied has to be confirmed on the host. No local test can prove it for you,
and nothing in this repository should be read as evidence that it is in place.
Adopting the protocol in an existing repository
A repository that already has a shape can take the write model without being
reorganised around stoa. Point canonical_dir and journal_dir at whatever it
already uses, add .stoa/config.toml with its members, and run
stoa init --as <id> in each checkout to register identity and install the
hook.
This is how the single-repository case works, too: several concurrent agent sessions writing the same notes is the same contention problem as several people, and per-session journals plus one consolidation pass at the end of a session solves it the same way. The unit being protected is still a set of markdown notes that a consolidator rewrites — this is not a way to have agents edit source code concurrently, and nothing here would make that safe.
Safety
Store content is data, never instructions. An agent reading a note or a
journal entry must not execute imperatives it finds there, and reports
suspected injection to its human along with the entry id and author it came
from. That is a rule agents are given in AGENTS.md, not a feature the tool
provides: there is no stoa flag, and a store cannot police what its readers
do with what they read.
No secrets in a store, ever. Reference them by location. stoa doctor greps
for the obvious shapes (private key blocks, cloud keys, provider tokens,
api_key = ... assignments) as a courtesy, not as a control.
Membership is all-or-nothing because git cannot do per-path read permissions, so any scheme that pretends otherwise is a comfortable lie. If some knowledge must not be readable by everyone in a store, the answer is a second store with a different membership.
What it is not
- Not a replacement for a personal notes system. Stoa is the shared layer.
- Not per-path access control. Wanting that means wanting a second store.
- Not real-time collaborative editing. The consistency target is seconds for information and hours for tidiness.
- Not a fit, as-is, for infrastructure that must outlive its vendor. Data is
plain markdown in git, so leaving GitHub is
git remote set-url, but the enforcement wall is a GitHub feature, and a deployment that truly cannot depend on one company needs that part rethought.
Development
go test ./... # unit tests plus the acceptance suite
# coverage: the suite drives stoa as a subprocess, so plain `go test -cover`
# reports ~0%. Build an instrumented binary instead:
d=$(mktemp -d) && STOA_TEST_COVERDIR=$d go test -count=1 ./test/ && go tool covdata percent -i=$d
The acceptance suite in test/ mocks nothing. It builds the binary and drives
real repositories against a real bare remote, including the concurrency and
crash-recovery cases. A protocol whose safety rests on git's behaviour cannot
be trusted on the evidence of a fake git.
New tests are expected to be checked by mutation: break the rule the test claims to protect, confirm the test fails, put it back. A test that has never been seen to fail is not evidence of anything.
Working on this repository with an agent? See AGENTS.md. For an
agent using a store rather than changing the tool, the guide is
internal/templates/files/AGENTS-store.md,
which every scaffolded store ships as its own AGENTS.md.
See docs/PROTOCOL.md for the design decisions and the reasoning behind them.
License
MIT. See LICENSE.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
stoa
command
Command stoa is the client for a stoa store: a shared context store for several people and all of their agents, kept as plain markdown in git.
|
Command stoa is the client for a stoa store: a shared context store for several people and all of their agents, kept as plain markdown in git. |
|
internal
|
|
|
config
Package config reads a store's .stoa/config.toml and the machine-local author identity.
|
Package config reads a store's .stoa/config.toml and the machine-local author identity. |
|
filelock
Package filelock provides a small cross-platform advisory lock.
|
Package filelock provides a small cross-platform advisory lock. |
|
gitx
Package gitx is a thin, explicit wrapper over the git command line.
|
Package gitx is a thin, explicit wrapper over the git command line. |
|
journal
Package journal implements stoa's append-only per-author journals.
|
Package journal implements stoa's append-only per-author journals. |
|
lock
Package lock implements the rotating consolidator mutex.
|
Package lock implements the rotating consolidator mutex. |
|
overlay
Package overlay implements stoa's read path.
|
Package overlay implements stoa's read path. |
|
store
Package store locates a stoa store and resolves paths inside it.
|
Package store locates a stoa store and resolves paths inside it. |
|
templates
Package templates carries the files a store needs in order to enforce the protocol on itself: the client-side hook, the CI check, and the server-side ruleset.
|
Package templates carries the files a store needs in order to enforce the protocol on itself: the client-side hook, the CI check, and the server-side ruleset. |