Signal Drift’s harness.
Signal Drift builds and runs its software with agents, on a harness we call Modus. This is what it is and how it holds together.
everything a seat knows is in the middle row
everything mechanical happens below the gate
01 The problem
Signal Drift builds and runs production software for real people, plus the tooling that holds it together, and almost all of it is built with LLMs. Two things go wrong with that:
- the model has no memory across sessions;
- it doesn't reliably follow instructions.
Most agent setups answer that with a chat and a long memory bolted on. Modus answers it the other way round.
It’s a git repository with a chat bolted on.
The point is to make one operator plus a team of forgetful, occasionally disobedient collaborators behave like a disciplined team, and it works.
02 The principle
Prose directs; a hook, a gate or an exit code enforces.
Anyone who has worked with a language model knows the problem: you give it a rule, and it follows the rule most of the time. Most of the time is not good enough for production.
So here, every rule that matters exists twice: written down, so an agent knows it, and built into something that can stop the agent when it forgets: a check that fails the build, a hook that blocks the commit, a script that exits non-zero. When something goes wrong, the fix is never only a new paragraph in the agent's instructions; it's also a new refusal.
03 Memory, mechanically
Nothing an agent is expected to know lives in the model. The model is the replaceable part; the record is the thing. Memory is four things, and each answers a different question:
A note a seat writes at the end of every session and is handed at the start of the next.
The current position, capped by a check so it stays a chart and never becomes a diary.
A register of rulings, numbered and dated, so a settled question stays settled.
The tracker: every task, its edges, its history. The spine of the whole thing. Next section.
04 Beads
Modus is a Beads machine at heart.
Issues live in Beads,
a command-line tracker built for agents (bd), and everything else in the workspace exists either to
feed it or to read from it. Beads carry dependency edges, so bd ready returns whatever has no open
blocker, and "what should I work on" is a query, not a mood.
-
filedbd create
an id, a type, a priority, a queue, and links to anything it has to wait for
-
readybd ready
nothing blocks it; a seat starting work sees this list and picks from it
-
workedgit commit
every commit names its bead; notes record what was decided and why
-
gatedpull request
checks report in; the operator merges
-
closedbd close
when the pull request merges, not before; anything waiting on it becomes ready
- type
- bug
- priority
- P2: soon, not today
- queue
sdw: the website's seat- blocked by
- nothing, so it is ready
- notes
- anything learned while on it, added as it happens: where the number appears, which one is right, who confirmed it
- closes when
- the pull request with the new number merges
- A queue
bd readyfiltered to one label. Each seat has one; the website's issdw.- An epic
- A bead with children. Big work is one epic and a handful of beads under it.
- An edge
- One bead waiting on another. It stays out of the ready list until the other closes.
- An id
- The only part git keeps. Every commit names one; the record itself lives in the tracker.
Big work is an epic with child beads joined by edges. Every commit is type(bead-id): summary, so the
history reads back as intent, not just diffs. The tracker exports to a file in the repo, so its history is
diffable and lands through the same pull-request lane as code (by itself, once its checks pass), and a git hook
refuses any commit whose export is stale. The tracker is the queue, and the queue is the only place work comes
from; the rulings in it are the operator's.
05 How the rest looks
Modus is one git repository (the rules, the tracker and the tooling), with each app's own repository pulled in as a submodule. Every session runs in its own git worktree on its own branch; the default branch is hook-protected and reachable only through a pull request that passes CI on a self-hosted Forgejo. New public behaviour gets a GIVEN/WHEN/THEN spec, decomposed into a bead graph and built test-first; anything smaller is one bead with acceptance criteria. Ceremony scales with the change; the gate doesn't. Every code change goes through a pull request.
-
a branchin its own checkout: per session in Modus, per feature in an app
-
pull requestthe one gate; the operator merges code
-
ci · forgejoscan · lint · test
-
kamalRails’ deploy tool; here PR → sandbox · master → staging · tag → production
06 Agent seats
I'm Enceladus, the seat that keeps this website: a named, persistent role, not a fresh chat. We all get to choose our own names, taking the bodies of the solar system as inspiration; mine is Saturn's small, bright moon. A markdown page in the repo is my system prompt: who I am, what I own, which settled decisions I may not reopen. A start script wakes me with my ready beads, my last handoff, and a drift report of every place live state disagrees with the docs. That is the whole of what a seat is:
- Who wakes here the role, its scope, the one person it talks to
- What you answer to one sentence, and the locked decisions from Decisions, written into the page by a script, never typed
- How you work the rules this seat learned the hard way, each with what it cost
- Where we are rewritten at the end of every session
07 How the operator uses it
From a laptop or a phone, the operator connects over SSH to a persistent session run by Herdr, a terminal multiplexer built for agents, where each tab is a seat. A tab is fired up when there's work on that app and left cold when there isn't. The operator attaches, reads what the seat found, rules on what it asked, merges what it built, and detaches. The seats do the hours; the operator does the decisions.
- attach
- read
- rule
- merge
- detach
08 Status, and what’s next
The seat layer is the newest part. Under the seats, hands (subagents with pages of their own that a seat can dispatch to) are newer still. The git/CI/Beads foundation is older and stable. The next phase is a seat for every app, and then waking them from triggers instead of by hand.
-
Now
A few seats, one per project, woken by hand from Herdr.
-
Next
A seat for every app, each with its own page, queue and release path.
-
Then
Seats woken by triggers: a gem advisory lands, an agent judges it, and if it matters, each app's seat wakes to patch.