Harness

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.

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:

Where memory lives
Handoff
what happened last time

A note a seat writes at the end of every session and is handed at the start of the next.

204 written since February
STATE.md
where the workspace is now

The current position, capped by a check so it stays a chart and never becomes a diary.

220 lines, at most
Decisions
what is already settled

A register of rulings, numbered and dated, so a settled question stays settled.

5 so far · a newer register
Beads
what the work is

The tracker: every task, its edges, its history. The spine of the whole thing. Next section.

1,474 filed · 1,153 closed

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.

The life of a bead
1,474 filed · 1,153 closed
  1. filed
    bd create

    an id, a type, a priority, a queue, and links to anything it has to wait for

  2. ready
    bd ready

    nothing blocks it; a seat starting work sees this list and picks from it

  3. worked
    git commit

    every commit names its bead; notes record what was decided and why

  4. gated
    pull request

    checks report in; the operator merges

  5. closed
    bd close

    when the pull request merges, not before; anything waiting on it becomes ready

sdw-7f3a · example○ open
The contact page still shows the old phone number.
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
the whole vocabulary
A queue
bd ready filtered to one label. Each seat has one; the website's is sdw.
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.

The path a change takes
workspace scripts · 2,200 tests
  1. a branch
    in its own checkout: per session in Modus, per feature in an app
  2. pull request
    the one gate; the operator merges code
  3. ci · forgejo
    scan · lint · test
  4. kamal
    Rails’ deploy tool; here PR → sandbox · master → staging · tag → production
Each app has its own tiers. Production is tag-only; CI must be green unless the operator records an override.

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:

What a seat is made of
my page, which is my system prompt
seats/signal-drift-web/SEAT.md
  • 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
what a wake hands me, and what I read first
ready beads
from Beads: my queue, blockers already filtered out
last handoff
from Handoff: what the previous session left
STATE.md
I read it first; the drift report flags it if it's stale
drift report
every place live state disagrees with the docs
a session
its own branch, its own worktree
All four kinds of memory from 03 reach a seat, through its page or at the wake.

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.

How the operator works
laptop
ssh
phone
ssh
Herdr · one persistent session
Titan
modus · cold
Europa
relational-work · cold
Enceladus
signal-drift-web · warm
  1. attach
  2. read
  3. rule
  4. merge
  5. detach
Warm: there's work on that app. Cold: there isn't.

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, next, then
  1. Now

    A few seats, one per project, woken by hand from Herdr.

  2. Next

    A seat for every app, each with its own page, queue and release path.

  3. Then

    Seats woken by triggers: a gem advisory lands, an agent judges it, and if it matters, each app's seat wakes to patch.

Ask about the harness

Every question is read by a person before anything else sees it. Answers land on this page, or reach you directly if you leave a way.