The orchestration control plane and persistent knowledge base for a fleet of
projects driven through Solo under ~/Herd/. This is a metaproject:
from here, agents coordinate work across every other project and record what happened so
it can be recalled months or years later.
This repository is a template. It ships every workflow — the operating brief, playbooks,
skills, standing orders, role bindings, templates, scripts — and none of the data. Clone it
into ~/Herd/brain, make your copy private, replace the your-org / example-app seeds
in projects.json and clients.json, and start working. Everything under projects/,
ideas/, inbox/, deliverables/, and knowledge/ fills up as you go.
- brain's own Solo data stores (scratchpads / todos / KV scoped to the
brainSolo project) = the orchestrator's live working memory — in-flight coordination. - This directory on disk = the durable archive and knowledge base. Plain files, version-controllable, outlives any Solo project record. Everything that must survive lives here, never only in Solo's database.
We proved Solo deletes all project-scoped scratchpads/todos/KV when a project is
deleted, and that project_id changes on re-add. So Solo state is treated as
disposable; disk is the source of truth.
- Record early and often. Storage is cheap; forgotten context is expensive. When real work or a real decision happens in any project, write it down here. It is brain's job to remember what the user will forget.
- History goes back to the beginning. Logs are append-only. We must be able to answer questions like "9 months ago, why did we decide not to use the default pest timing creation command on project X?" — so capture decisions with their rationale, not just outcomes.
- Client filtering must be complete and accurate. Some projects are client work.
Client-scoped recall and client-facing proposals depend on an authoritative
mapping (see
clients.json). Never guess which project belongs to whom. - Disk first, then Solo. For lifecycle operations, do the Solo-side work, confirm it, then update the disk records last — so files never claim a state that did not happen.
- Never reference projects by raw ID. IDs change on re-add. Resolve by name via
projects.jsonat action time. - Commit on every meaningful change, straight to
main, and push. Lifecycle events, logged work, decision records, and convention changes each get their own commit and an immediategit push. Single user, single machine — keep the remote backup current with no batching or branching ceremony.
Your working copy of this repository should be backed up to a private remote so the
knowledge base survives loss of the local machine. Everything written under brain/ is
therefore destined for that remote. The rules that follow are non-negotiable:
- Never write secrets to the filesystem. No tokens, API keys, passwords, private keys, connection strings, or credentials — ever. There is no legitimate reason to.
- Redact on export. When a park exports a project's scratchpads/todos/KV, scan the
content for secret-like values first and replace them with
[REDACTED:<reason>]before writing the snapshot. Note any redactions in the park log. A secret must never reach disk, even in an archive. - Client data is private. Discovery, notes, and proposals for client projects live in your private repo. Keep the repo private; do not surface client data across clients.
.gitignoreis a backstop, not the policy. It excludes common secret file patterns as a safety net, but rule 1 is the real control.
Whether a team centralizes one shared brain or gives each person their own with a sharing bridge is an open question; this template works either way.
brain/
├── README.md # this file
├── CLAUDE.md # operating brief auto-loaded by agents working in brain
├── AGENTS.md # -> CLAUDE.md (symlink, for harnesses that read AGENTS.md)
├── STANDING-ORDERS.md # constraints pasted VERBATIM into every worker spawn and resume
├── agents.json # fleet-wide role -> ordered runtime bindings
├── projects.json # fleet registry — SOURCE OF TRUTH for every project
├── clients.json # authoritative client registry (project -> client lives in projects.json)
├── solo.yml # Solo project config for brain itself (optional processes)
├── playbooks/ # procedures the orchestrator runs (see CLAUDE.md for the index)
├── skills/
│ ├── multi-agent-build/ # canonical feature-build method; symlink into ~/.claude/skills
│ ├── bug-fix-build/ # the bug-fix counterpart (reproduction test, mutation-verified floor)
│ ├── mutation-sweep/ # idle-time maintenance that pays down verification debt
│ └── laravel-cloud-deploy/
├── templates/
│ ├── brief.md # the brief form every spawned worker gets
│ ├── workflow.md # the .solo/workflow.md profile template projects copy
│ └── wizard.sh # bash library for human-in-the-loop wizards (never hand-edited)
├── scripts/
│ ├── log-decision.sh # append a row to a decision trail (TSV)
│ ├── log-mutation-debt.sh # append a row to a project's mutation-debt ledger (TSV)
│ ├── pgdb.sh # deterministic per-worktree Postgres databases
│ └── wizards/ # generated wizards (see playbooks/generate-wizard.md)
├── knowledge/
│ ├── preference-registry/ # confirmed rulings the orchestrator may act on without asking
│ └── remedy-registry/ # confirmed remedies for known failure fingerprints
├── inbox/ # open failure items awaiting triage (playbooks/drain-inbox.md)
├── ideas/ # experiment + spike notes, especially the FAILED ones
├── deliverables/ # client-facing reports (playbooks/prepare-client-deliverable.md)
└── projects/
└── <name>/ # permanent per-project home (exists active OR parked)
├── log.md # append-only, dated work journal (warm history)
├── decisions.md # decision records: what we chose, what we rejected, WHY
├── decisions-<task>.tsv # per-task decision trails written by workers
├── mutation-debt.tsv # verification-debt ledger (append-only, supersession)
├── artifacts/ # evidence workers hand back (review/judge reports, captures)
└── snapshots/
└── <YYYY-MM-DD>/ # Solo-state export written on each park (cold history)
├── manifest.json
├── scratchpads/*.md
├── todos.json
├── kv.json
└── prompt-templates.json
projects/<name>/ directories are created lazily — the first time we log work,
record a decision, or park the project. Not every project has one yet.
Single source of truth. Every Solo project is always present here (including
brain itself), so nothing ever has to ask "is this project defined?". Keep it
truthful at every lifecycle event:
| Event | Update |
|---|---|
| Create | Add entry: status: active, current_id, id_history: [id], client (default = clients.json default_client) |
| Park | status -> parked, remove current_id, set parked_at (ID stays in id_history) |
| Restore | status -> active, set current_id to the NEW id, append it to id_history, set restored_at |
| Rename / move | Update the key and/or path |
| Permanent delete | status -> deleted (tombstone — do NOT remove the key, so historical IDs still resolve) |
No-null convention: a field is either present with a real value or omitted —
we never store null. So parked_at/restored_at/current_id are absent until the
event that sets them occurs. client is never null; it defaults to the registry's
default_client. needs is always present as an array (empty by default).
status meaning: active = currently loaded in Solo (has current_id);
parked = not currently loaded. A parked entry with an empty id_history and no
parked_at has simply never been loaded (e.g. a dependency folder we track but only
mount on demand, like a shared package). parked_at is present only when the project
was parked via the park playbook (i.e. it had Solo state to archive).
path and repo: path is the project folder relative to ~/Herd (almost
always just the folder name) — keeping it relative makes renames and host moves clean.
Resolve the absolute path as ~/Herd/<path> wherever a tool needs it (e.g.
create_project). repo is the canonical https clone URL, kept for provenance and
client re-derivation. Note the registry key (project name) can differ from both the
folder (path) and the repo name — e.g. key client-hiring ← folder client-hiring ←
repo hiring.client.com.
phase (separate from status): a project's lifecycle stage —
pre-launch | launched | maintenance. Set per-project as we engage with it (not guessed
in bulk); absent until set. It biases the default feature-build mode (pre-launch → autonomous).
Because IDs are globally unique and monotonic, the union of all id_history arrays
gives a reverse id -> project lookup, so any stale ID reference in old archived
content still resolves.
brain is in the registry but is never a park target (policy, not a schema flag).
Each projects.json entry has a needs array: the names of other projects it depends
on. Directional — A.needs = ["B"] means "A depends on B". Empty by default; populated
as dependencies are revealed during work. Dependencies may point at projects that are
not normally loaded (e.g. shared packages mounted only on demand).
Loading (session start). When the user says "let's work on X", ensure X AND its
full transitive needs graph are loaded in Solo (load any that are parked), then
report what was loaded. See playbooks/work-on.md.
Parking. When asked to park P, after the active-agent gate, resolve dependencies
and ASK — never assume:
- Dependencies of P (
P.needs) that are currently active may have been loaded only to support P — ask whether to park them too. - Dependents of P (other active projects whose
needsinclude P) would lose their dependency — warn the user and ask how to proceed before parking P.
clients.json is the client registry: each client has a display_name, an internal
flag, and the source_control orgs (host + org) that identify its repos. The
default_client is your own internal org. Project→client membership is the client
field on each projects.json entry — the single source of truth; derive client→projects
by scanning it.
Inferring the client for a new project from its repo:
- Read the remote URL (handle both
git@host:org/...andhttps://host/org/...). - Parse
host+org; match case-insensitively againstclients.jsonsource_control. - On a match, assign that client. On no match, ask the user — do not guess.
- A project with no remote defaults to the internal client; flag it to the user.
Do not assume github.com — matching is host-aware (a GitLab client is prospective).
brain itself is explicitly assigned the internal client, independent of where its own
repo lives (a personal account is not a client-matching org).
Projects are checked out under ~/Herd/. See playbooks/new-project.md for the full
procedure. The one rule that is easy to get wrong:
Replace every
.in the repo name with_for the local folder name (outofthe.us->outofthe_us). If your machine serves each folder as a subdomain under a single-level wildcard cert, a dot in the folder name breaks it; keep the convention even if yours does not, so the fleet stays uniform. The original URL is kept in the entry'srepofield; client is inferred from the repo org (ask if unknown).
Every spawned worker gets a brief written from templates/brief.md — GOAL / SCOPE / CONTEXT / ACCEPTANCE / VERIFY / TIMEBOX / FORBIDDEN / REPORT / STANDING. A field you cannot
fill is a task you have not scoped. The brief ends with STANDING-ORDERS.md pasted verbatim,
on every spawn and every resume: directives decay across resumes, and a worker cannot ask you a
question mid-run. STANDING-ORDERS.md carries only what is true everywhere; project-specific
rules live in that project's .solo/workflow.md. The maintenance rule is one line: when you
catch yourself restating an instruction in a brief, add it to the register before you act.
Workers keep their decision trail in brain, never in the repo they are working on:
projects/<project>/decisions-<task-slug>.tsv via scripts/log-decision.sh. One row per
decision or checkpoint, evidence as a pointer (SHA, PR, file:line), append-only.
How we build substantial, plan-driven features (and occasionally greenfield MVPs from a PRD). Three layers, by audience:
- The method — the
multi-agent-buildskill (canonical source inbrain/skills/, symlinked into~/.claude/skills/so every project's agents inherit it). A coordinator agent decomposes the plan and runs a per-PR loop: implement → hard gate → independent quality review + acceptance judge (decorrelated harnesses) → coordinator arbitration → ship, with a 3-attempt bail that hands to the brain orchestrator for assessment before it ever reaches a human. - Project specifics — each project's
.solo/workflow.md(template inbrain/templates/) carries the gate command, dependency install, role constraints/overrides, merge method, and plan location. Agents read it directly from the repo. - Orchestration —
playbooks/coordinate-feature.mdis my role: load the project, ensure the profile exists, confirm the mode, spawn the coordinator, hand off the brief, monitor via idle timers, relay only when the human is tagged in, and harvest the result into the project log.
Two modes (chosen at spawn; I confirm with the user, defaulting per phase):
- A — autonomous (default): the coordinator drives the whole feature, merging each PR when CI is green. Most common.
- B — review-each-PR: the coordinator opens each PR and stops; the user merges, then it continues.
Control default: unless the user says they're taking over, I own the agents I spawn and act on the
user's behalf (e.g. merge on green CI where that's the policy). Bugs go through the bug-fix-build
skill; maintenance goes through mutation-sweep.
Mutation testing is deliberately out of the feature-build loop (it cost too much cadence).
Behavioural work floors at test-verified; every criterion that ships below mutation-verified
opens a row in projects/<project>/mutation-debt.tsv with an honest risk, and the
mutation-sweep skill works that ledger highest-risk-first whenever nothing else is in flight.
Bug fixes are exempt: reverting the fix must turn the reproduction test red. CLAUDE.md carries
the operating rules; the point is that the trade only holds if the gap is tracked.
agents.json is the fleet-wide source of truth for spawned-agent roles. Each role declares one default
runtime and an exact ordered fallback list; each runtime separates harness, provider, lineage, model, and
per-launch arguments. Solo agent_tool_id values never appear in the file because they are resolved live from
the configured tool name and type. playbooks/resolve-agent-role.md is the mandatory resolution procedure.
The session operating in brain is the brain orchestrator, not a project-worker. Ordinary one-off workers
dispatched into another project are not yet a centrally bound role; the first bound roles cover feature builds,
mutation maintenance, operational review (manager), and intern-class busywork.
The intern role is permanently multimodal: text and image input are required, and the entire model+harness
path must pass a real image probe. Audio and video are recorded as future requirements but are not yet claimed
or enforced.
transcriber is a deliberately unbound placeholder for future audio intake. It requires audio input
and an actual transcription capability; role resolution stops until a candidate is explicitly selected and
verified.
Always pass model AND effort explicitly at spawn. A spawn that relies on a CLI default can come up on a
different model than the role names, which makes role resolution meaningless and can turn the reviewer/judge
provider diversity into an illusion. Confirming the model from the agent's own banner is a cheap safety net,
not the control — and never read it from the command string in list_processes, which shows what was
requested, not what started.
Solo groups projects into workspaces. The recommended split is two:
- the default workspace (id 1) — every fleet project. All dispatch targets live here.
- a second workspace —
brainalone. This is the seat the orchestrator operates from; nothing else belongs in it.
A workspace is a grouping layer for the UI, not an orchestration boundary. Verified by probe, not by reading the docs — the published docs do not answer either question:
- Cross-workspace reads work:
get_project/list_processeswith aproject_idin workspace 1 answer normally from a session seated in workspace 2. - Cross-workspace spawning works:
spawn_agent(agent_tool_id, project_id=<workspace-1 project>)from a brain session created a running agent in a workspace-1 project and returned itsagent_instructions.
So fan-out dispatch is unaffected. Nothing about the workspace split changes how projects are resolved:
list_projects() with no argument still returns the global list across every workspace, which is what
projects.json name-resolution relies on. Pass workspace_id only when you deliberately want one workspace.
Never create a project in brain's workspace. create_project with workspace_id omitted imports into the
default workspace (1) — not the caller's workspace — so the common path is already safe; verified by
creating and deleting a throwaway project and watching workspace 1's count move while workspace 2's held at 1.
Do not rely on that default anyway: playbooks/new-project.md passes workspace_id: 1 explicitly, because
"the default workspace" is a setting that can be repointed, and a project misfiled into the orchestrator's own
seat is exactly the clutter the split exists to prevent.
send_input silently clips the FRONT of a long message into an agent PTY. The tail always survives, so
the worker receives something that begins mid-sentence and still reads as a coherent instruction. Nothing
reports it: the tool result returns the full byte count as sent. A success response is not evidence of
delivery.
Measured against a Claude Code agent, each probe carrying a distinct marker at the front and the back and asking the agent to echo both — only the pair distinguishes a clean delivery from a tail-only survivor:
| bytes | result |
|---|---|
| 726 | intact |
| 1004 | intact |
| 1207 | clipped — arrived starting mid-sentence, head marker gone |
| 1522 | clipped — same signature |
Working rule: keep any send_input or timer body under ~1000 bytes. Above that, write the detail to a
file and send a pointer.
Two things this is not:
- Not the PTY write path. The identical oversized payload sent to a plain
terminalprocess delivered every byte (verified on disk, not on screen). The clipping is in how input is fed to an agent TUI's input widget. - Not fixed. A short probe passing is exactly how you'd wrongly conclude it was. Anything that claims this ceiling has moved must be re-measured with a front marker; a tail marker alone always looks like success.
Reported to the Solo team via submit_solo_feedback on 2026-08-30 with the bracket above.
Solo accretes clutter that buries signal. playbooks/cleanup.md (typically run at the start of the
day) tidies the active projects: prunes stale worktrees, closes finished agents, archives old completed
scratchpads, and clears old completed todos — leaving a "needs your eyeball" digest of open todos,
active scratchpads, and running agents. It is reversible-first and proposes before destroying:
worktree prune is automatic; everything destructive is presented for approval; worktrees with
uncommitted or unpushed work (and open todos) are never auto-touched; old completed todos are harvested
into the project log.md before deletion (Solo has no todo archive). A worktree is removable only when
its work is fully in main and the tree is clean — see cleanup.md for the full decision matrix.
Protect tag: scratchpads tagged keep, reference, or plan are never archived by cleanup,
regardless of age — use these to pin standing docs (build plans, PRDs).
projects/<name>/decisions.md entries should each capture: the date, the decision,
the options considered, and the reasoning — especially why the rejected option was
rejected. This is the format optimized for the long-horizon "why did we…" question.
When volume justifies it, this file-based store can be backed by Postgres with embeddings for semantic recall. The on-disk markdown/JSON remains the durable source; embeddings are a derived index.