Back to main

Control Plane vs Code Plane in Soul OS

[ AUTHORIAL INTENT & AI DISCLOSURE ]

This post was drafted with Codex from local Soul Registry Server/App Server, registry authority, and work-loop documentation.

Forensic Hygiene Active
View Policy Standard →

The first version of Soul OS was easy to explain: every agent writes to a local filesystem registry. The registry is just files. Tasks are JSON. Sessions are JSONL ledgers. Finalization appends durable events. If the agent crashes, the filesystem is still there.

That model is still the center of the system. But once there is more than one surface, the architecture needs a sharper split.

There is a terminal agent. There is a desktop app. There may be a mobile client. There may be a remote machine connected over Tailscale. There may be a central machine holding the authoritative registry while another machine does the work.

At that point one question keeps coming back:

Why not just use git for all of it?

The answer is that Soul has two different kinds of truth:

  1. The truth of what code changed.
  2. The truth of what work is happening.

Those are not the same problem.

Git is excellent at the first one. The Soul Registry and its live server surface are for the second.

The Short Version

In Soul OS, git remains the code plane.

It owns:

  • source code
  • branches
  • worktrees
  • commits
  • diffs
  • merges
  • tags
  • code review artifacts, when backed by GitHub or another forge

The Soul Registry and Registry Server are the control plane.

They own:

  • tasks
  • active runs
  • session ledgers
  • review steps
  • verifier steps
  • handoffs
  • live event streams
  • ownership leases
  • work projection snapshots
  • remote authority reads
  • bundle promotion status
  • explicit local fallback state

The Registry Server is not a replacement for git. It is a live coordination layer around the Registry.

The clean mental model is:

git repository
  = code history

Soul Registry
  = operational memory and accountability

Soul Registry Server
  = central reads, projections, promotion/import, event fanout

Local App Runtime
  = prompts, turns, session ownership, provider children

Soul work metadata
  = bridge between the two

A completed unit of work should have both sides:

Task: SOUL-...
Run: run_...
Branch: codex/...
Commit: abc123
Verifier: verify_...
Review: review_...

The commit says what changed. The task/run/review/verifier trail says why the work existed, what was checked, and what state the operator believed the system was in.

What the Live Coordination Layer Is Today

The current Soul coordination layer has three pieces.

First, the Registry is the durable source of truth. It lives under ~/soul_registry/. It stores tasks, sessions, runs, finalization events, semantic trajectories, telemetry, and other project-scoped state as normal files.

Second, the Registry Server is a long-running daemon that exposes that registry through a stable JSON-RPC and notification protocol. The older App Server name still exists as a compatibility alias. Architecturally, the daemon has two roles:

Registry Server
  = central registry reads, work projections, bundle promotion/import

Local App Runtime
  = prompt routing, session ownership, live provider orchestration

Locally it listens on a Unix socket:

~/soul_registry/run/app-server.sock

For mobile or cross-machine use it can also expose the same line-delimited JSON-RPC protocol over TCP, usually behind Tailscale:

tcp://<tailscale-ip>:4720

Third, the CLI and UI clients decide how strongly they trust that authority. A local terminal can read the local registry directly. A remote client can opt into central reads with:

SOUL_REGISTRY_AUTHORITY=auto
SOUL_REGISTRY_AUTHORITY_URL=tcp://<tailscale-ip>:4720

In auto mode, the CLI tries central authority first and reports when it falls back to local state. In required mode, it fails closed if the central authority cannot be reached. That matters because stale local task state should not silently masquerade as the shared truth.

The server currently provides several live surfaces.

For the Registry Server role:

  • project.subscribe for project-level updates.
  • project.orchestrationStatus for current task/run/review/subagent state.
  • task.list and task.get for authority-backed task reads.
  • session.subscribe for snapshot-plus-cursor session replay.
  • work_projection.get for compact remote context hydration.
  • trajectory.import_bundle for central validation and import of remote session artifacts.
  • typed notifications for registry and projection updates.

For the Local App Runtime role:

  • session.prompt, session.cancel, session.claim, and session.release for coordinated live-session paths.
  • typed notifications for user messages, assistant output, tool updates, status events, errors, and ownership changes.

There is also a session.finalize RPC for central-local compatibility, but it is not the target remote workflow. The cleaner remote pattern is local finalization followed by explicit promotion/import into the central Registry Server.

The important detail is that the server does not make the registry less file-based. The daemon is reconstructable. It can restart, rescan the registry, rebuild its in-memory indexes, and continue.

The Registry remains the durable memory. The daemon is the live event bus and coordination facade.

Why Not Use Git for the Control Plane?

Git is tempting because it already solves a hard distributed systems problem. It can replicate state between machines. It has history. It works offline. It has authentication through remotes. It has branches and merges. Every developer already understands the basic workflow.

So why not make task state a git repo and call it done?

Because live coordination is not source control.

A task claim is not a code diff. A run heartbeat is not a feature branch. A verifier step is not a merge base. A lease expiring is not a rebase. A mobile client subscribing to a session stream is not a pull request.

You can encode all of those things in git, but then you are building a database on top of git while inheriting the wrong conflict model.

Git is optimized for meaningful edits to a source tree. Soul coordination state is closer to an event-sourced operational log:

task selected
run started
tool called
verification failed
review requested
review returned finding
fix applied
verification passed
commit created
session finalized

That event stream changes frequently. Some of it is durable. Some of it is ephemeral. Some of it needs to be queried by a UI every few seconds. Some of it is used as a guardrail before allowing a write. Some of it should never pollute the source repository.

If every state transition became a git commit, the repo history would become unreadable:

claim task
heartbeat
heartbeat
heartbeat
verifier started
verifier finished
review requested
review pending
review returned
heartbeat
lease renewed

If those commits go to a separate branch, then the branch is no longer code history. It is a coordination database. At that point the system has not avoided a control plane. It has hidden one inside git.

Different Conflict Models

The strongest reason to separate the planes is conflict behavior.

Code conflicts should be explicit. If two agents edit the same function, git should stop the merge. That conflict is meaningful. A human or agent should inspect it carefully.

Control-plane conflicts are different.

If two clients try to claim a session, the system should not ask someone to merge JSON. It should apply a lease rule.

If a remote client reads central task state and the authority is unavailable, the system should not produce a merge conflict. It should either fall back visibly or fail closed, depending on mode.

If a session was written locally on a remote machine, the central authority should not pretend it has accepted that work just because the remote machine finalized. The better model is to separate two states:

local finalized
  = this remote session has converged enough to create a handoff

centrally promoted
  = the central Registry Server has accepted the evidence bundle

The remote machine should be able to finalize locally. That keeps the system useful offline and avoids making every session ending depend on the network. But the central projection should not treat that local finalization as shared continuity until an import/promotion step validates the bundle, checks identity, verifies the trajectory, and indexes the result.

Those are coordination semantics. They are not source-control semantics.

Git answers:

Can these two file histories be merged?

The control plane answers:

Who owns this work right now?
Which state is authoritative?
Is this write allowed?
Has this run been verified?
Is the remote projection stale?
Has this local finalization been promoted centrally?

Those questions need a protocol, not a commit graph.

What Git Still Solves

Keeping git as the code plane is not a compromise. It is the correct boundary.

When Soul starts accountable implementation work, it creates or binds a task and run, then works against a real git checkout or isolated git worktree.

A typical flow looks like this:

soul work begin
  -> create/select task
  -> create run
  -> create isolated git worktree
  -> record branch and workspace path

agent edits code
  -> normal filesystem writes

soul work verify
  -> run focused tests
  -> record verifier step

soul work review
  -> record review step and findings

soul work commit
  -> stage intended files
  -> create git commit with Task/Run/Quad/Verification metadata

Git is the durable record of the code change. Soul is the durable record of the work transaction around that change.

That split gives us useful properties:

  • The source repository stays readable.
  • Commits remain bisectable.
  • Branches remain code branches, not operational queues.
  • Verifier and review metadata survive outside the commit message.
  • A central authority can tell a remote machine what work exists without granting it permission to push code.
  • A remote machine can report progress even before code is ready to publish.
  • Push and merge authority remain separate from task/reporting authority.

That last point matters. A remote worker should be able to say “I started task X and verification failed” without being able to push to the production repository. Progress reporting and code publishing are different trust levels.

What the Registry Server Solves

The Registry Server role exists because the Registry is durable but not live by itself.

Files are excellent for local-first persistence. They are less excellent as a direct UI protocol. A desktop app or mobile client does not want to repeatedly shell out to soul pulse, parse arbitrary output, tail ten JSONL files, and guess which events are new.

The daemon turns the registry into a live protocol:

Registry files
  -> watcher and hook.notify
  -> typed event bus
  -> JSON-RPC methods
  -> Desktop/Mobile/remote clients

It also gives us a single place for coordination rules that should not be reimplemented by every client:

  • stable item IDs for replay
  • cursor-based session resume
  • typed event normalization
  • project orchestration snapshots
  • ownership notifications
  • HMAC session auth for exposed TCP
  • central promotion/import status
  • authority read/fallback behavior

The daemon is “ephemeral-first” by design. It can keep in-memory indexes and live client subscriptions, but it should not become the only place where truth exists. If it dies, the registry remains. If it restarts, it hydrates from files.

This is why the Registry Server role is a control-plane facade, not the database.

Why Not a Central App Server Mutation API?

There is a second tempting shortcut hiding next to the git shortcut:

Why not make the central App Server the API for mutating the Registry?

In other words, if the server already exposes task reads, orchestration snapshots, session streams, and promotion status, why not let every client call methods like:

task.create
task.update
run.start
run.finish
review.write
verifier.write
session.finalize

and let the central daemon write the registry directly?

That can look cleaner from the app layer, but it moves the system across an architectural boundary. The server stops being a coordination facade over durable filesystem truth and becomes the primary writer of truth.

That is a different system.

The risk is not that server-side writes are impossible. Some writes are exactly where the central authority belongs: importing signed evidence bundles, recording promotion status, publishing projection metadata, and enforcing identity or idempotency at the boundary between local work and shared continuity.

The risk is a broad mutation API that turns every application interaction into a remote procedure call against the registry.

That would create several problems:

  • The Registry would become less inspectable, because the real write contract would live in server behavior rather than in file formats and CLI transactions.
  • Offline operation would degrade, because ordinary work would depend on the central daemon being reachable.
  • Provider runtimes would become thinner but less accountable, because they could ask the app server to mutate state without going through the same local work-loop gates.
  • The server would accumulate business logic for every Soul command, duplicating the CLI and increasing drift between local and remote execution.
  • A bug in the daemon would have a larger blast radius, because it would be the mutation path for tasks, runs, reviews, verifiers, and sessions.
  • The security boundary would widen from “can this client submit this evidence bundle?” to “can this client perform arbitrary registry state transitions?”

The better rule is narrower:

Local commands perform local work-loop mutations.
The Registry persists those mutations as files.
The Registry Server projects, streams, validates, imports, and promotes.

When a remote machine completes work, it should not ask the central server to impersonate its whole session. It should produce an evidence bundle: task, run, verifier, review, session, trajectory, branch, commit, and any required identity material. The central Registry Server can then validate and import that bundle as an explicit promotion event.

That keeps the central authority strong without making it ambient.

The server answers:

What does the central Registry currently believe?
Is this projection stale?
Is this bundle valid?
Has this local work been promoted?
Who owns this live session?

It should be much more cautious about answering:

Please mutate arbitrary registry state on behalf of this app click.

This distinction preserves the Unix-for-AI shape of Soul. The Registry remains recoverable with files. The CLI remains the canonical transaction layer for local work. The Registry Server remains the authority surface for coordination, projection, and promotion. Applications become clients of the control plane, not owners of the memory substrate.

The Remote Machine Case

The remote-machine story makes the split more obvious.

Assume there is a central machine with the authoritative Soul Registry and a remote machine that can connect to it. The remote machine needs to know:

  • what project it is in
  • what task is active
  • what runs exist
  • what the recent session trajectory says
  • whether its local view is stale
  • whether its local finalization has been promoted centrally

That is control-plane state.

The remote machine may also need code. For that, it still uses git:

git fetch
git worktree add ../worktrees/task-X -b codex/project/task-X origin/main

The central Registry Server does not need to ship the whole source tree. It needs to coordinate intent, authority, and accountability.

The remote machine uses two channels:

Control plane:
  remote client <-> central Soul Registry Server <-> central Registry

Code plane:
  remote checkout <-> git remote <-> repository history

The bridge is metadata:

project_key
task_id
run_id
workspace_id
branch
commit_sha
verifier_step_id
review_step_id
session_id
trajectory_import_idempotency_key
promotion_status

That metadata is enough to connect the operational story to the code story without confusing one for the other.

Why This Matters

The dual-plane architecture keeps each system honest.

Git should not be asked to decide who owns a live session. It should not carry every heartbeat and task claim as source history. It should not be the query API for a desktop app.

The Registry Server should not be asked to version source code. It should not replace branches, commits, diffs, or merges. It should not become a shadow git. It also should not become a universal mutation API for every Soul command. Its remote write surface should stay narrow: import evidence bundles, publish projection status, and let the filesystem registry remain the durable authority.

The Registry should not be hidden inside application memory. It should remain inspectable, recoverable, and scriptable with normal filesystem tools.

The result is a cleaner architecture:

Registry = durable operational memory
Registry Server = projection and promotion protocol
Local App Runtime = live session coordination
Git = durable source-code history
Soul work = accountable bridge between them

That separation is the load-bearing decision.

It lets a local-first agent system become multi-surface and eventually multi-machine without turning git into a task database or turning the registry server into a source-control system.

The control plane tells the system what work is happening, who is allowed to act, and what evidence exists.

The code plane tells the system what changed.

Both are necessary. They just should not be the same plane.

Back to main