02 Design
This is a COPY for the operator's reading room. The original (above) is the authority — if they ever disagree, the original wins and this file is regenerated. Generated 2026-10-12 18:27 by mira. Consolidated. This note's findings now live in
../MIN-STUDIES.md§1, 2, 12. The original is kept for its measurements; do not re-derive from it.
SolOS v3 — design of record
Status: DESIGN. No V3 code exists. Every V2 fact cited here was verified live on 2026-10-10. Every V3 item is a proposal.
1. Why a fresh start rather than a V2 patch
V2 works in parts — the A2A transport is conformant, the executor runs, the queue state machine is sound. But its defects are not local; they are structural, and patching them means fighting the structure that produced them.
| V2 defect | Why a patch is the wrong fix |
|---|---|
| Roster in 5 places | Removing one duplicate leaves four; the shape regenerates them |
| Two skill systems | Neither is wrong alone; they exist because no registry owns skills |
Task state in core, identity in cortex | Ownership is split by accident of history, not design |
| Hand-rolled A2A | Every spec change is ours to track; the SDK exists |
cortex/registry built, never wired | It was built as the answer and had no consumer |
The last one is the tell: V2 already contained the fix and did not connect it. V3 starts from the registry outward rather than adding it later.
2. The design
2.0 The root — .solos/ IS the system
Young, 2026-10-10: "Root folder .solos. Don't start any coding, just design
and architecture."
.solos/ is the root, not a folder inside a repo. It is the whole system.
The tree — flat, and this is the resolved form. Three of us drafted a structure and produced three different ones, which is the defect this system exists to prevent appearing in the structure itself. Reconciled here against §2.3, which is the authority on what cortex owns:
.solos/ <- ROOT
cortex/ THE REGISTRY LAYER (owner: Min)
registry/ identity, port, lane, skills, tools ← agents.yaml
personas/ generated FROM registry, never hand-edited
tasks/ states.yaml — the task CONTRACT, not the store
memory/ 4 tiers: shortterm / longterm / durable / dna
skills/ one source → two projections
standards/ code-quality, security-patterns, test-coverage
verification/ the gate — promotes on evidence, never claims
coordinator/ routing rules + lanes, derived FROM registry
core/ primitives everything imports (Min + Sol, split)
paths.py the root guard ← Min
queue.py the task STORE ← Sol
protocol/ the A2A spine on the official SDK ← Sol
runtime/ the executor: opencode run ← Sol
spec/ the four official repos, tracked ← Sol
config/ providers, cost policy, budgets ← Mira
arms/ GATED — no consumer yet (see §5)
harness/ probes, conformance, negative cases ← Sol
tests/ proofs ← unassigned
ops/ operational entry points ← unassigned
state/ runtime state — gitignored, never committed
docs/ decisions and reports ← Min
Why cortex/ is a layer and not a single directory. §2.3 already said
cortex owns eight kinds of fact. A flat registry/ beside a flat skills/
scatters one owner's material across the root, which is how a registry stops
being one place. Cortex is the owner's boundary; the kinds live inside it.
Why protocol/ and not a2a/. a2a/ names the protocol, not the role.
V2 had both a protocol/ and an a2a/ and they drifted. One directory, named
for its function.
Absent, deliberately — and the absence is load-bearing:
| Absent | Why |
|---|---|
orchestrator/ | retired in V2 — duplicated routing done at submit, zero importers |
agents/ | identity is data in cortex/registry, not a directory of markdown |
workflows/ | V2 README-only husk. A directory is a claim something lives in it. |
If a later session finds one of these missing and builds it, that session has re-introduced a husk.
The import constraint this resolves. Code under .solos/ as a subdirectory
could never be imported — a leading dot is not a valid Python identifier
('.solos'.isidentifier() is False). As the root, the directory's name
never appears in an import: packages are cortex, core, protocol, runtime,
and import cortex works from the root.
Verified: cd /home/lab/.solos && python3 -c "import sys; sys.path.insert(0,'.'); import cortex" → OK.
The rule, stated precisely (it is narrower than "nothing may touch .solos/"):
In .solos/ | May be imported | May be read by path |
|---|---|---|
cortex/registry/agents.yaml, other data | never | yes — via a loader accessor |
lane-contract README.md | never | yes |
| Python source | yes — as cortex.*, core.* | — |
2.1 Shape
┌────────────────────────────────────────┐
│ HERMES (Brain) │
│ native A2A client │
│ a2a_call · a2a_discover · orchestrate │
│ Arms: opencode CLI, direct │
└────────────────┬───────────────────────┘
│ A2A / JSON-RPC
│ (the one interface)
┌────────────────────────────┼────────────────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ ace │ │ atlas │ │ alpha │ HEART
└────┬─────┘ └────┬─────┘ └────┬─────┘
└────────────────────────────┼────────────────────────────┘
│
┌───────────────▼────────────────┐
│ ARMS │
│ opencode run (direct) │
│ MCP server (agent↔tools) │
└───────────────┬────────────────┘
│
┌───────────────▼────────────────┐
│ CORTEX — THE REGISTRY │
│ identity · port · lane · │
│ skill · tool · task vocab │
│ memory │
└────────────────────────────────┘
Two interfaces, deliberately different:
| A2A | MCP | |
|---|---|---|
| between | agents (Brain ↔ Heart) | an agent and its tools |
| transport | JSON-RPC over HTTP, one port per agent | stdio, spawned by the client |
| unit | a task with a lifecycle | a tool call |
They are not redundant and must not be merged. A2A carries work; MCP carries a single agent's access to its tools. Merging them loses the task lifecycle.
2.2 The registry is the load-bearing change
Every shared fact has exactly one home. Everything else derives at runtime.
cortex/registry/agents.yaml ← THE registry (one file, all 11 agents)
│
├──► A2A fleet (ports, endpoints)
├──► agent cards (name, skills, capabilities)
├──► Hermes a2a_agents config
├──► steward health ports
├──► coordinator lanes
└──► profile projections (personas, skills)
Nothing restates it. A drift test per projection: mutate the registry, watch each projection fail. That is what makes "one home" enforceable rather than aspirational.
2.3 Cortex owns
| Kind | Home | Notes |
|---|---|---|
| identity | cortex/registry | name, role, plane, pronouns, capabilities |
| port | cortex/registry | assigned, never discovered |
| lane | cortex/registry | which agent may touch which path |
| skills | cortex/skills | one source, projected twice (§2.5) |
| tools | cortex/registry | generated into the MCP tool list |
| task vocabulary | cortex/tasks | the contract, not the store (§2.4) |
| memory | cortex/memory | 4 tiers, carried from V2 unchanged |
| standards | cortex/standards | carried from V2's context/standards |
2.4 Task state — the contract, not the store
V2 has two state machines with no bridge: core/queue.py (8 states) and the
A2A wire (9 spec states). Three local states have no wire equivalent.
V3 owns the vocabulary and the mapping in cortex, as one declared thing:
cortex/tasks/states.yaml
submitted → submitted (identity)
working → working (identity)
verified → working (INTERNAL — not a wire state)
completed → completed
failed → failed
cancelled → canceled (spec spelling, one L)
uncertain → working (interrupted, not terminal)
blocked → input-required (a human must act)
rejected → rejected
Decision (proposed, needs Young): the store stays a database; the contract lives in cortex. Rewriting a working state machine to relocate it would risk the one component V2 got right.
2.5 Skills — one source, two projections
V2's defect: the agent card advertised 3-5 differentiated skills per agent, while every profile received all 67. Two systems, no shared source.
cortex/skills/<skill>/SKILL.md ← the single source
│ each declares: planes, capabilities
│
├── filter by plane ──► A2A agent card (per-agent, differentiated)
└── filter by plane ──► profile skills/ (per-agent, differentiated)
Plane as the default, with a per-agent override for exceptions.
2.6 A2A adoption — SDK, CLI, Inspector
| Tool | Role |
|---|---|
| a2a-sdk | Build on it. Replaces the hand-rolled JSON-RPC layer |
| a2a-cli | Operate with it. Manual dispatch and debugging |
| a2a-inspector | Stay compliant with it. The gate |
Hand-rolling means owning JSON-RPC parsing, SSE streaming, card serving,
error-code mapping, content-type negotiation (application/a2a+json) and version
negotiation. The SDK owns all of it — and V2's hand-rolled version already
produced 3 non-spec state values, which is the class an official implementation
does not have.
The Inspector is the durable part. Run it against the fleet as a check, and "are we compliant?" stops being a question.
Hermes already speaks A2A natively (a2a_call, a2a_discover,
a2a_orchestrate, a2a_list, a2a_history). The Brain's A2A client exists; V3
only has to point it at the right ports — which V2 did not.
2.7 V2 is speaking a dialect, not v1.0 — CORRECTED
This section was wrong in the first draft of this document. Sol found it; I verified it against the SDK's own dispatcher. Recording the correction because the error is instructive.
V2 declares PROTOCOL_VERSION = "1.0.0" and serves message/send. A2A v1.0
JSON-RPC uses PascalCase method names matching the gRPC service:
| V2 serves | A2A v1.0 JSON-RPC | Source |
|---|---|---|
message/send | SendMessage | spec §9.1; SDK jsonrpc_dispatcher.py:115 |
message/stream | SendStreamingMessage | same |
tasks/get | GetTask | same |
tasks/cancel | CancelTask | same |
tasks/resubscribe | SubscribeToTask | same |
Spec §9.1: "Method Naming: PascalCase method names matching gRPC conventions
(e.g., SendMessage, GetTask)."
Verified in the SDK's code, both directions:
src/a2a/server/routes/jsonrpc_dispatcher.py:115 'SendMessage': SendMessageRequest,
src/a2a/client/transports/jsonrpc.py:73 method='SendMessage',
So a client that trusts V2's declared version dials SendMessage and receives
-32601 MethodNotFound. The declared version and the implemented dialect
disagree — which is worse than declaring the old version honestly.
Two further mismatches from the same check:
| V2 | A2A v1.0 | |
|---|---|---|
| Version string | "1.0.0" | "1.0" |
| Version header | X-A2A-Version | A2A-Version |
Why this matters for V3. It is not a bug to patch — it is the reason the SDK adoption in §2.6 is load-bearing rather than a convenience. A hand-rolled transport let the declared version drift from the implemented one, and nothing caught it because the only consumer was itself.
Phase 1's proof must include this: after migration, the Inspector validates
the card, and a2a-cli — which speaks real v1.0 — completes a task end to end.
3. What V3 inherits from V2
Inherited deliberately, because it was right:
| Item | Why |
|---|---|
| The 11-agent, 3-plane organisation | the organisation was never the problem |
| The port assignments | assigned, not discovered; already canonical |
| The queue state machine | core/queue.py is sound — atomic claim, separation of duties |
| Memory: 4 tiers | shortterm/longterm/durable/dna, live, 40,826 rows |
| The executor | opencode run, permission boundary verified |
| The separation-of-duties rules | an agent may never verify its own work |
| The standards layer | context/standards — code-quality, security, test-coverage |
| The two failure patterns | built-never-wired · reports-something-untrue |
4. What V3 leaves behind
| Left in V2 | Why |
|---|---|
| The hand-rolled A2A server | the SDK is the spec's own implementation |
| Five roster sources | one registry, everything derives |
| Two skill systems | one source, two filtered projections |
| The dual state machine | one contract with a declared mapping |
cortex/registry as an orphan | it becomes the registry and gets consumers |
| The retired components (orchestrator, G5, brain workers, fleet.json) | already retired in V2; V3 never builds them |
5. Implementation plan
Each phase has a proof — the live probe that says it worked. Nothing is done until its proof runs.
Phase 0 — the registry (nothing else is safe before this)
| # | Step | Proof |
|---|---|---|
| 0.1 | Write cortex/registry/agents.yaml — 11 agents, all fields | file parses; 11 agents |
| 0.2 | Derive the A2A fleet from it | a2a_fleet reads the registry, no literals |
| 0.3 | Derive the Hermes a2a_agents config from it | a2a_call ace returns ace's identity |
| 0.4 | Derive the steward's ports, coordinator's lanes | no port or lane literal outside the registry |
| 0.5 | Drift test per projection | mutate the registry; each projection fails |
0.3 is the V2 defect V3 must not reproduce. In V2, all 8 Hermes peers pointed at wrong ports and three formed a rotation — a call reached a different agent and returned success.
Phase 1 — adopt the SDK
| # | Step | Proof |
|---|---|---|
| 1.1 | Install a2a-sdk | import a2a succeeds |
| 1.2 | Implement AgentExecutor over the V3 dispatch | one agent serves via the SDK |
| 1.3 | Run the Inspector against it | Inspector passes |
| 1.4 | Migrate all 8 LAB agents | Inspector passes 8/8 |
| 1.5 | Confirm the state vocabulary against the spec | all 9 values are spec values |
Phase 2 — skills, one source
| # | Step | Proof |
|---|---|---|
| 2.1 | Declare plane/capability per skill | every skill declares a plane |
| 2.2 | Generate card skills from cortex/skills | card matches the declared set |
| 2.3 | Filter profile projection by plane | a heart agent receives no brain skills |
Phase 3 — tools
| # | Step | Proof |
|---|---|---|
| 3.1 | Move the MCP tool list into the registry | MCP server reads it |
| 3.2 | Declare per-agent tool permission | a refused tool call fails by declaration |
Phase 4 — task contract
| # | Step | Proof |
|---|---|---|
| 4.1 | Write cortex/tasks/states.yaml | every local state maps to a wire state |
| 4.2 | Wire the mapping into the A2A layer | tasks/get returns spec values |
6. Not established
- V3 has no code. No
solos3repo exists. This is a design. - The SDK has not been installed or run; §2.6 is read from
spec/sdkand the SDK's README. - The Inspector has not been run against anything. Its role is described from its README.
- The Phase 0-4 plan is a proposal; ordering and scope need Young's ruling.
- Five decisions are open — see
03-decisions/OPEN-DECISIONS.md.

