This website is not here to tell you how you should build things. It is a visual representation of how I have made agentic development work for me. If you saw something useful and want to learn more, please keep reading. I do my best to outline as much of my process as possible.
This article touches on a topic that many people may not be aware of in their own development efforts. While most of the industry has focused on telling AI what to build, a number of people have also been telling AI how to build. This entire harness is grounded in governance around how things are done. Here is what that means.
Deterministic code, probabilistic creative
DeterministicCode
SpecWhat to build
SchemaTables and types
ServiceCode and API
TestsCheck each change
ReleaseTagged build
Standards
Same input, same output
ProbabilisticCreative
PromptAn idea
SketchConcept
PaletteColor
MelodyMusic
PickBy taste
RefineAnother pass
KeepFinal piece
Again
Same input, new output
In my experience, trying to fit everything into one or the other leads to sadness. The concept of vibe coding lives in the probabilistic world. Without governance around what you are building, you give AI little to work with. You expect it to just “know” what to output. This works for creative work and ideas. It does not work as well for backend infrastructure that needs to scale, or for building things consistently so they are easy to secure.
This section of my site explains how I have solved this for myself, to some extent. It is not perfect, but it has worked for me so far. I hope you find it useful.
From concept to seeded work
Every project in this harness travels the same road from an idea to a queue of work. A chain of documents fixes each decision before any code exists. The developer approves each document, and the agent checks it against the document before it. The last step seeds Scrolls with work items that an agent can run without guessing.
The example on this page and the next two is Trailcast, a website for trail conditions. It has a public site, an API, and two daemons that run on a schedule.
The example
Trailcast shows the current state of each trail in a region. Its parts live in two repositories, and each repository gets its own document chain and its own graph:
trailcast-web: the public site, with a page for each trail and a map
trailcast-api: the API, plus a feed daemon and a summary daemon
The feed daemon pulls weather and closure reports from an outside feed every 15 minutes and stores each reading. Each night, the summary daemon rolls the day’s readings into one summary per trail. The API serves trails, readings, and summaries to the site.
Concept
A project starts as a conversation, not a document. The developer describes the idea in plain terms. The agent asks what the project must do, who uses it, and what it must never do. No code exists yet, and no document.
The concept ends when the developer runs trigger new project with a theme and a project name. A theme that does not exist yet comes first, through trigger new theme.
The document chain
The trigger loads its process from Seele and follows it step by step. It first searches for documents that already exist and shows the result as a table. It then stops until the developer confirms. Vision and Product are optional, and the developer decides whether the project needs each one.
OverviewWhat it is
VisionOptional
ProductOptional
TechnicalHow it is built
MagiWhat runs
PlanningIn what order
ScrollsThe queue
Each one approved, then checked against the one before
Each document fixes one layer of the project.
Document
What it fixes
Overview
What the project is, its current state, its stack, its libraries, and its document index
Vision
Purpose, users, goals, non-goals, and key constraints
Product
Features, user flows, interface behavior, and what is out of scope
Technical
Architecture, processing, data model, contracts, and publishing
Magi
A machine-readable graph of every running part and every boundary between parts
Planning
Agent routing, current state, the Scrolls seed, and the epics with their work items
Every document goes through the same loop. The agent walks through the document’s purpose and required sections with the developer. It gathers what it needs and writes a draft to Seele. Revisions continue until the developer approves. The agent then checks the draft against the document before it. A change there sends the check back up the chain until nothing moves.
Technical
Technical is where open choices end. Before it writes a line, the agent loads the standards for each of the project’s languages:
The language standard: runtime, async model, project structure, and build
The library matrix: approved libraries, blocked versions, and upgrade holds
The scaffold: the directory tree for the project’s type
The integration standard: how a service joins the shared platform
The draft follows them. Every dependency carries an exact version pin. Each design choice becomes an architecture decision with its rationale and a rule the code must follow. For Trailcast, the standards fix the API framework, the database driver, the log format, and the test layout. The daemons follow the same rules as the API.
The agent checks the finished draft twice: against the standards it loaded, and against Product, Vision, and Overview. The repository must exist in the Registry before the chain goes further. If it does not, trigger new repo registers and creates it first.
Magi
Magi restates Technical as a graph that a script can check. Each running part is a node with a type: a service, a process, an interface, a data store, an application, or a view. Each relationship is a typed edge. Every edge that carries data names the model that crosses it.
WebsiteApplication
calls
APIService
consumesTrail
Weather feedExternal
consumesReport
Feed daemonService
persists_toReading
Summary daemonService
persists_toSummary
DatabaseData store
Trails
Readings
Summaries
The trailcast-api graph
Every value in the graph uses the project’s own vocabulary: class paths, table names, method names, and routing keys. An agent that reads the graph can search the code for those exact strings. A shared system, such as the database, carries the same identifier in every graph that uses it. Two projects that touch the same table point at the same node.
The first graph comes from the approved Technical document. From then on, the code is the test. A node without running code leaves the graph, and planned work stays in Planning. That rule makes the graph a lock. It holds only what was defined and built, and every later change is measured against it.
Planning
With Technical and Magi approved, the agent updates the Overview with the final stack, libraries, and document index. Planning comes last. It turns the design into ordered work, and its Scrolls seed holds two tables: the initiative’s identity and an inventory of its epics.
Trailcast initiative
FoundationEpic 1, trailcast-api
Repository
Schema
Bootstrap
APIEpic 2, trailcast-api
Trails
Readings
Summaries
DaemonsEpic 3, trailcast-api
Feed daemon
Summary daemon
WebsiteEpic 4, trailcast-web
Trail pages
Map
The epics follow fixed rules. Foundation comes first: the repository, the schema, and the bootstrap. Each epic holds one concern and, where it can, one repository. Each epic also states its exit criteria, so done has a definition before work starts. Every work item in an epic’s table carries a type, a name, a title, a priority, a size, and a repository.
Seeding
The scrolls trigger, trigger scrolls, moves the plan into Scrolls. It loads Planning and Technical first. If either is missing or fails the documentation standards, the trigger stops and names the gap. A gap means an earlier step was skipped, and the trigger does not fill it with a guess.
The trigger then checks the seed against live Scrolls data. It presents a dry run: the initiative, the new epics, and a count of work items per epic. It creates nothing until the developer confirms. Each item then lands with what an agent needs to run it alone:
A type: a story for documentation and a task for code, never a mix
A short description for listings and the dashboard
An agent specification with file paths, signatures, configuration keys, and Technical references
The items that must finish before it starts
An acceptance-criteria subtask and a testing subtask on every task
Each seeded epic then moves from planning to ready. Trailcast now has a queue in place of an idea. The next page follows that queue to production.
From orchestration to production
Seeded work runs through one trigger and ships through one release route. The orchestration trigger reads an initiative’s state from Scrolls and agrees on a scope with the developer. It then plans the work, hands each item to a subagent, and checks every result before the item closes. Publishing turns the finished work into one tagged build that every environment runs. This page follows Trailcast from its first ready task to production.
The schedule shows one way the Trailcast initiative could run, in three sessions. Each session opens with a stop, where the developer confirms the scope and approves the plan. Foundation runs alone, because every later item depends on the repository, the schema, and the bootstrap.
The second session fans out. Once the bootstrap lands, the trails endpoint, the readings endpoint, and the feed daemon start together, each with its own subagent. As each item closes, the items that waited on it start. At the busiest point, four subagents run at once.
The hatched segments mark work that a check sent back. The readings endpoint failed its acceptance criteria once, and the feed daemon stepped out of bounds once. Each went back to a subagent and passed the second time. A point marks each epic’s close, where the graph gets checked. The release runs last, and production waits for the developer’s approval.
State
The trigger, trigger orchestration, takes an initiative name. If the initiative does not exist in Scrolls, the trigger stops. Otherwise it loads every epic and work item, each with its dependencies, its agent specification, and its status. It then sorts the items into four states.
State
Meaning
Completed
Done and closed in Scrolls
In progress
Started and not yet finished
Ready
Every dependency complete, and not yet started
Blocked
Waiting on a dependency that is not complete
For a fresh Trailcast initiative, the foundation tasks are ready. The API, daemon, and website tasks are blocked behind them. Each later session reads the same state and starts where the last one stopped.
Scope and plan
The agent presents that state, grouped by epic, and stops. The developer names where the work starts and where it stops for this session. Nothing runs until the developer confirms.
The agent then enters plan mode. From the confirmed scope, the Technical document, and each item’s agent specification, it writes a step-by-step plan. The plan orders the items by their dependencies and marks the ones that can run in parallel. It names the model for each item and the checks each item must pass. The developer approves the plan before any file changes.
The orchestrator
Once the plan is approved, the main agent becomes the orchestrator. It writes little of the code itself. It routes each item to a subagent, checks what comes back, and keeps Scrolls current.
Before the first item, the orchestrator checks that every epic in scope is ready or in progress. An epic in any other state stops the run. When the first item of an epic starts, the epic moves to in progress.
Each subagent starts from a brief, not from the conversation. The brief holds the item’s agent specification and the Technical sections it cites. It also holds the Magi nodes the item touches, with the item’s acceptance criteria and testing subtasks. The item’s size picks the model: Haiku for small, Sonnet for medium, and Opus for large.
Items that do not depend on each other run in parallel. An item waits for every item it depends on. Every item then runs the same loop, and every release runs the same route.
One itemOrchestrator
BriefFrom Scrolls
BuildSubagent
TestsWhole suite
CoverageNear 100%
AcceptanceCriteria met
BoundsAtlas check
CloseScrolls, commit
Sent back
Until every check passes
One releasePublishing
ChecksEvery test
TagVersion bump
ImageBuilt once
DevelopmentDeploy
VerifyEnd to end
ApprovalDeveloper
ProductionSame image
New version
One tag, every environment
A stop for the developer
Validation
A subagent’s report is a claim, not proof. The orchestrator runs every check itself before it closes an item. Every task carries two subtasks from the seed, and both must pass:
Acceptance criteria: the code does what the item says, under the conditions the item states
Testing: the tests the item calls for exist and pass
The two prove different things. Tests can pass while the work is still wrong. For the readings endpoint, the subagent’s tests passed, but the endpoint returned readings oldest first. The criteria asked for the last day of readings, newest first.
Any failed check sends the item back. The orchestrator returns it to a subagent with the failed check, its output, and the evidence of what is wrong. The item stays open until every check passes.
Only then does the orchestrator close the item and its subtasks in Scrolls. It commits and pushes the change before the next item starts. An epic closes when every item under it is completed or cancelled.
Automated tests
Each item adds unit tests for the code it adds. The goal is coverage as close to 100% as the code allows. The language standard sets the test framework, the file names, and the layout.
The whole suite runs after every item, not only the new tests. Each change proves that it did not break the work before it. A failure in an earlier module counts against the new item, and the item goes back.
For Trailcast, the suite holds three kinds of test.
Kind
Covers
Runs
Unit
Each function and class on its own, with the feed and the database stubbed
After every item
Integration
The API and both daemons against a test database
After every item
End to end
The site in a browser against a running deploy
After each deploy
Atlas in the loop
Atlas is how the orchestrator sees the code without reading it file by file. Each snapshot records every file, class, function, constant, and import at one commit. The orchestrator pulls it after each change, both to check the change and to trace a failure to its source.
A change is in bounds when it matches its agent specification and the Magi nodes it touches. A class the specification never named is out of bounds. So is an import the library matrix does not approve, or code that does a job the graph gives another node. The orchestrator sends that change back with the Atlas entry as evidence.
The feed daemon’s first pass shows both uses. A test failed because some readings arrived without a trail. The Atlas showed two report parsers, and the daemon imported an old one that an earlier attempt left behind. It also showed a function in the feed daemon that wrote summaries, a job the graph gives only to the summary daemon. The subagent removed both, and the next run passed.
The orchestrator reads these from the Atlas during testing.
Need
What the Atlas shows
Where a failure starts
The file and line of each class and function the failing test reaches
What a change added
New files, classes, functions, and imports, against the previous snapshot
What depends on a module
Every import of that module across the repository
Whether a model matches
The class fields, compared with the model on the Magi edge
Magi at each epic’s close
Atlas checks the detail inside the loop. Magi checks the shape at a higher level. When an epic closes, the orchestrator runs trigger magi against the code the epic produced. Every node needs running code, and every data edge needs a model that matches the class that carries it.
A gap at this point has one of two causes. Either the code drifted from the design, or the design missed something the work needed. The first goes back to a subagent as a fix. The second stops for the developer, because only an approved document changes the design.
Larger stretches of work end with the reconciliation jobs, which the reconciliation page describes. Between those runs, Atlas and Magi hold every item to the design that seeded it.
Tagging a release
A release starts only when every check passes. For Trailcast, that means the whole suite of the API and both daemons, plus the site’s page checks. A failed check stops the release, and the agent reports its output in place of a summary.
One command versions the repository. It bumps every file that declares the version, commits the staged work with it, tags the commit, and pushes. Major and minor versions always get a tag, and a patch gets one when it ships. The Trailcast site prints its version in the footer, so a check of the running page can read it.
The tag is the release. A deploy refuses a tag that is not on the remote. The build starts from an archive of the tag, never the working tree, so an uncommitted file never ships. The deploy specification and the compose file come from the tag too. The image carries the same version, and a version is never reused, so one version always names one build.
Development first
Every release runs at home before it reaches Amazon Web Services (AWS). The development deploy builds the image once from the tag and starts it on the development host. Each deploy runs a fixed series of named steps, from export and build through start, register, and retain. A failure names its step.
Guards protect running work. A deploy holds while users are still connected, and it fails before any change when the host lacks disk space. Database patches apply before the new containers start.
The agent then verifies the running release against what changed. For Trailcast, the footer shows the new version, and the health check reports the database and both daemons. The feed daemon logs its first pull. The end-to-end run drives the site in a browser against the development deploy.
A failure here never becomes a fix in place. The fix lands on the main branch as new work, and a new version takes the same route from the start.
The same build in production
Production waits for the developer’s approval, and one approval covers one deploy. The production deploy ships the image that development verified to the production host in AWS. Nothing rebuilds between the two.
Each environment supplies its own configuration when the container starts. The image stays the same, so what passed at home is what runs in production. The agent then repeats the checks against the public site.
Publishing records each deployment with its commit, tag, image, and status. The Registry records which release runs in each environment. Each host keeps its running release and the one before it. A rollback deploys the previous version, and the build and ship steps skip because the image is already there.
Database patches only move forward. A patch adds tables and columns and never renames or drops in the same release, so the previous release still runs on the new schema.
Infrastructure on demand
The production side does not wait on a console. An infrastructure service in Core speaks to seven AWS services directly, and every resource type answers the same five operations.
AWS on demand
EC2Hosts and networks
S3Buckets
Certificate ManagerCertificates
CloudFrontDistributions
Route 53Zones and records
LambdaFunctions
API GatewayAPIs and stages
Every typeFive operations
Apply
Describe
List
Discover
Destroy
One infrastructure service, seven AWS services
Apply creates a resource or updates it to match a request. Describe and list read what the service manages, and discover enumerates everything in the account, including resources made elsewhere. Destroy removes a resource. Publishing never calls AWS itself, and every resource it needs goes through this service.
The production host came from the same service: its instance, network, storage, and address. For the first Trailcast release in production, one work item asks for a DNS record for the site’s public name. The production edge then adds a route for the site and serves it over HTTPS.
A static site takes different resources: a bucket in Simple Storage Service (S3), a CloudFront distribution, a certificate, and an alias record. A serverless route takes Lambda functions behind API Gateway. Each one is a request to the same service.
Reconciliation and drift
Documents and code drift apart as soon as work lands. Two jobs pull them back together. The documentation job checks the five documents against each other and against the repository’s configuration files. The code job checks the code, through Atlas, against Technical, the Magi graph, and the standards. In review and reconcile modes, neither job changes a line of code. A refactoring mode runs the same jobs the other way, and the code changes to match the documents.
Drift
Drift is any gap between what a document says and what exists. A version pin moves, and the document keeps the old one. An endpoint lands without a row in the API table. A feature is cut, and Product still describes it in the present tense. Each gap is small. Together they turn the documents into guesses, and an agent that reads a guess builds on it.
In the Trailcast example, a later feature adds email alerts when a trail closes. Its work goes into Scrolls without a documentation update. Once that work lands, Technical lacks the subscription table, the two endpoints, and the alert daemon. Magi lacks their nodes. Reconciliation closes those gaps.
Two jobs
The two jobs split the work by what they read.
DocumentsJob 3
ReadsConfiguration files and the file tree
ComparesThe five documents with each other and with configuration
TrustsConfiguration over any document
WritesThe documents in Seele
RefactorConfiguration, to match the documents
CodeJob 4
ReadsThe Atlas of the source code
ComparesThe Atlas with Technical, Magi, and the standards
TrustsThe Atlas for what the code is
WritesTechnical in Seele and the Magi file
RefactorThe code, to match Technical and Magi
Outlined: refactoring mode only
The documentation job is the cheaper of the two, because it never reads source code. The code job goes deeper, because it reads the code through Atlas. A full audit of a repository runs the documentation job first, then the code job.
Modes and entry points
Each job takes a mode and an entry point. Both jobs share the same three modes.
Mode
Behavior
Review
Runs every phase, writes nothing, and reports every change it would make
Reconcile
Runs every phase, writes the corrections to the documents, and reports what it fixed and what remains
Refactor
Runs every phase with the documents trusted, changes the code to match them, and runs again to confirm
The entry point sets which side the job trusts first.
Job
Entry point
Trusted first
When
Documents
technical-in
Technical, reconciled to the configuration files
The default, for a project in active development
Documents
overview-out
Overview, reconciled to the configuration files
A new project, or one just after a pivot
Code
code-in
The code, through the Atlas
The default, after active development
Code
technical-out
Technical
Technical was just written or reconciled
A common pattern runs review first and discusses the findings with the developer. A reconcile run then applies the agreed changes, with the review’s findings as its input.
The documentation job
Job 3, job reconcile-docs, works on one repository at a time. It starts with an orientation pass that writes nothing. The pass resolves the repository in the Registry, detects the languages from their configuration files, and reads every document in the chain.
Three rules hold in every phase:
A deployment follows one of the allowed paths: the bootstrap to development, publishing to production, or a signed device app
Every project uses the latest shared libraries, and an older pin is a finding
Vision and Product are optional, and the Overview states which applies
Configuration files settle every dispute. When Technical and Overview disagree on a version, the dependency file wins, whatever the entry point. A claim about code behavior, such as an endpoint count, is outside this job. The job lists it as unverifiable and points to the code job.
The job is idempotent. A second run with no changes in between finds nothing. A finding on the second run means the first run missed it.
The code job
Job 4, job reconcile-code, starts by generating the Atlas. Atlas parses the source into syntax trees and records every file, class, function, constant, and import. The agent reads the Atlas, not the files. If Atlas fails, the job stops and does not fall back to reading files by hand.
Every finding cites its evidence: an Atlas file and line, a Technical section, a standards rule, or a Magi element ID. Phrases such as “appears to” or “likely” mark a guess, and the job records no guesses.
The comparisons run in four phases, each against the same read-only Atlas:
Atlas against Technical: structure, classes, endpoints, imports, and configuration
Atlas against the standards: naming, scaffold, libraries, logging, and tests
Atlas against Magi: nodes, models, edge contracts, and the shared registries
Shared library use: whether the code calls the shared wrappers that exist for its purpose
The phases share one input, so they run as parallel agents. The orchestrating agent gathers their results into one report, with an effort estimate for each finding. Each Technical section gets one of four scores.
Score
Meaning
aligned
Technical describes what the Atlas shows
drift
Technical is partly correct, but outdated or incomplete
missing
Technical has no coverage of something the Atlas shows
contradicted
Technical asserts something the Atlas disproves
What the graph locks
Magi is where a defined solution becomes a fixed one. Its contracts name actual methods, routing keys, and tables, so a check either finds them in the Atlas or reports them missing. Each data edge carries a model with its fields, and the job compares those fields with the class in the Atlas. A shared database or an outside service carries the same universally unique identifier (UUID) in every graph that uses it.
This is the deterministic half of the harness. A design starts as a probabilistic conversation. Once the developer approves it, it lives in Technical and Magi, and every later change is measured against them. Work that drifts from the graph produces a finding, not a new design.
For Trailcast, the code job finds the alert daemon in the Atlas with no node in the graph. It finds the subscription class with no model, and two endpoints missing from the API table. In reconcile mode, it writes all three into Technical and Magi. The second run finds nothing.
Refactoring mode
In review and reconcile modes, neither job edits source, configuration, dependency, or test files. When the code is the side that is wrong, the finding says what must change and how large the change is: trivial, moderate, or significant.
Refactoring mode acts on those findings. It runs the same phases with the approved documents trusted first, so every gap counts against the code. Each finding aimed at the code becomes a change, and the tests run after each one. A second run of the job confirms that nothing remains.
A refactor moves the code back inside the design. It never moves the design. A finding that needs a new design stops for the developer, because only an approved document changes Technical or Magi.
Seele
https://core-seele.service.consul
Seele is the documentation store for the development environment. It keeps standards, processes, project documents, and knowledge bases outside the conversation. An agent pulls only the nodes that a job needs. That leaves more of a limited context window for the task, and the answer stays tied to the real documentation.
Role
Seele is the default source for any document, standard, or reference. Agents reach it through Core Model Context Protocol (MCP) tools. A seele://<id> reference in a prompt points an agent at one node. The Design and Studio knowledge bases live here too. Each project also keeps its chain of documents here, from current state through planned work.
Two access paths meet at the same data. A deterministic read starts from a known node ID. It walks the graph from there. A search starts from a query. It returns ranked node handles without their content.
Parts
The core-seele service is one Python and FastAPI process. Five internal parts divide the work.
Part
Job
Graph engine
Serves reads from an in-memory copy of the graph
Write gate
Validates each batch and writes it to PostgreSQL and the in-memory graph
Importer
Splits Markdown files into nodes and edges for the write gate
ID minter
Issues 6-character base62 identifiers from a counter
Consistency check
Compares the in-memory graph with PostgreSQL
The write gate enforces three rules. Inline references become edges. Each filename stays unique. Each node has at most one parent. The gate flags an unknown vocabulary value with a warning in place of a refusal. One unresolved reference in an import batch stops the whole batch, so no partial write lands.
Data
PostgreSQL is the source of truth. Three tables hold current nodes, current edges, and version history. Every write stores a full snapshot of the node and its edges. Those snapshots rebuild any earlier state of the graph.
A node holds an identity, metadata, Markdown content, and an optional embedding vector. Content carries no headings, because the tree of nodes supplies the heading structure. Each node records its token count, so a caller sees the context cost before a read.
Each edge carries one of three relations. The relation sets how a read loads the target node.
Relation
Meaning
Read behavior
hasPart
A parent holds a child
Loads every descendant
requires
A node depends on another node
Loads the target, one hop only
references
A node points to another node
Returns the target ID without content
Search uses PostgreSQL full-text ranking with optional theme and subject filters. Vector similarity search is deferred.
Connections
Seele exchanges data with six other systems.
System
What passes between them
Core MCP
The seele_search, seele_read, seele_write, and seele_import tools, forwarded by the Core MCP relay
PostgreSQL
Nodes, edges, and version records
core-auth
Access tokens, checked on every call except health checks
core-db
Schema patches, applied at deploy time
Consul
Service registration, and a fresh address lookup for each relay call
Vault
Database and service credentials at runtime
Scrolls
https://core-scrolls-api.service.consul
Scrolls is the work-management system for the development environment. A large request is too vague to hand to an agent. Scrolls breaks it into initiatives, epics, stories, tasks, bugs, and subtasks. Each item carries its goal, context, order, and current state, so an agent receives a defined queue in place of an open request.
Role
Scrolls sits between planning and execution. A project’s planning document in Seele seeds the initiative and its work items. Agents reach Scrolls through Core Model Context Protocol (MCP) tools. Each agent takes items from its queue. It marks each item complete after it finishes the work. Work can pause, resume, and move between agents without loss of context.
Scrolls replaced an earlier Jira integration. Its hierarchy follows the Jira model with one change: An initiative can hold stories, tasks, and bugs directly when an epic adds no value. Its context model and agent controls are purpose-built.
Parts
Four parts make up the system.
Part
Role
core-scrolls-api
Python and FastAPI service, and the only reader and writer of the Scrolls database
PostgreSQL
Stores every theme, work item, agent, and lookup value
Schema repository
Owns the database patches, apart from the service code
Core MCP tools
A set of 50 typed operations for the full lifecycle of a work item
Inside the service, HTTP routing, business logic, and SQL sit in separate layers. Dataclasses mirror the database tables one to one. A write that spans several statements commits whole or not at all.
Data
The hierarchy runs from theme down to subtask. Each level has a defined scope.
Level
Holds
Scope
Theme
Initiatives
A product domain
Initiative
Epics, stories, tasks, and bugs
A cross-cutting objective within a theme
Epic
Stories, tasks, and bugs
A cohesive chunk of work within an initiative
Story
Subtasks
Documentation, process, or design work that fits one conversation
Task
Subtasks
Code, configuration, or infrastructure work that fits one conversation
Bug
Subtasks
One isolated, reproducible defect
Subtask
None
One atomic action inside a story, task, or bug
Every row has a permanent universally unique identifier (UUID) key and a kebab-case name. Work items also carry a readable identifier such as MCP-TASK-002-000003. Its segments give the initiative code, the item type, the epic sequence, and the item sequence.
Lookup tables fix the allowed status, priority, size, and action values. Each size maps to a default agent: small to Haiku, medium to Sonnet, and large to Opus. List operations return lightweight fields only. The agent specification, document references, dependencies, and subtasks come back only from a detail read.
Connections
Scrolls exchanges data with four systems.
System
What passes between them
Core MCP
Agent calls, forwarded to the service after a fresh Consul lookup on each call
PostgreSQL
All Scrolls records
Seele
Planning documents, which an agent turns into initiatives, epics, and work items
NervHQ
Initiative progress, roadmap views, and work-item detail on the dashboard
Registry
https://core-registry.service.consul
The Registry is the inventory of the development environment. It holds structured records for every theme, project, source repository, and host, plus a code snapshot of each repository. Agents read exact paths, hosts, and deployment details from it in place of a search. Documentation lives in Seele, not in the Registry.
Role
The Registry answers what exists and where it runs. Agents reach it through Core Model Context Protocol (MCP) tools. A repo:// or theme:// reference in a prompt resolves through it. The core-infra-bootstrap tool reads each source’s development host and port fields to find where the service deploys. Commit and deployment events keep each source’s metadata current.
Parts
The core-registry service is one Python and FastAPI process. Seven internal parts divide the work.
Part
Job
Registry store
Holds every record in memory and serves every read
Write lock
Lets one change run at a time
ID minter
Issues 6-character base62 identifiers from a counter
Git client
Commits each change and pushes the history to GitHub
Reconciliation worker
Polls GitHub for the latest commit of each source
Library worker
Rebuilds a source’s dependency list when a manifest changes
Atlas runner
Parses a repository’s syntax trees into a structural snapshot
Atlas reads Python, TypeScript, Rust, Swift, and Kotlin. Reconciliation runs hourly for a source with a commit in the last 24 hours, and daily otherwise.
Data
The records live in one JavaScript Object Notation (JSON) file in a dedicated Git repository. Each change rewrites the file atomically. A local commit then records the change. A background loop pushes those commits to GitHub every 60 seconds, and the Git history serves as the audit log.
Six kinds of record make up the inventory.
Record
What it holds
Theme
A top-level grouping such as core, with a description
Project
Name, display name, context tag, project type, and owning theme
Source
The one repository of a project: language, GitHub URL, default branch, development host and port, Consul service name, and release tags
Host
Operating system, Docker endpoint, remote Core MCP endpoint, and machine role
Libraries
The dependencies parsed from a source’s manifest files
Atlas snapshot
The modules and symbols of one repository at one commit
Projects take one of 10 fixed types, and sources take one of 13. A deactivated host stays readable. The Registry refuses to deactivate a host that an active source still deploys to.
Atlas snapshots sit in a separate local-only Git repository with one file per source. Each new snapshot is one commit, so a Git diff shows the structural change between two commits.
Connections
Five other systems exchange data with the Registry.
System
What passes between them
Core MCP
Reads and writes through the registry_* tools, plus a commit event after each Git commit that it makes
Publishing command line
A deploy event that sets a source’s development or production tag
GitHub
Commit metadata for each source’s last code update
core-auth
Access tokens that authorize writes and events
Consul
Service registration and overrides for the reconciliation schedule
Fetch
https://core-fetch.service.consul
Fetch is a browser automation service. Some pages need JavaScript, a click, a hover, or time to finish drawing before their content appears. Fetch opens such a page the way a browser does. It returns the text, HTML, screenshots, console messages, and page errors that the page produces.
Role
Agents, services, and scripts call Fetch when a task needs a rendered page. Design research uses it to save the full text of reference pages. Device emulation lets it test a phone layout with touch input and a high-density screen. One request can run a sequence of actions, with a screenshot at each state.
Fetch builds its queue, workers, and API around the upstream Playwright project. Playwright supplies the rendering. Fetch runs on the desktop machine for now. Its planned home is core-docker-1, after a phase that sizes memory per browser context.
Parts
The core-fetch service runs one headless Chromium browser inside a Python and FastAPI process. Five parts divide the work.
Part
Job
API routers
Accept submissions and serve results, artifacts, status, and health
Engine
Assigns request IDs, holds the queue, and tracks each request’s lifecycle
Worker pool
Runs queued requests, each in its own isolated browser context
Chromium
Loads each page and runs the requested actions
Data volume
Stores completed results and artifacts
A browser context is the unit of isolation and memory. The Consul key-value store holds the pool size, with a default of two concurrent contexts. Actions run in order after navigation: goto, click, hover, fill, press, waitFor, waitMs, evaluate, and screenshot.
Data
A request names a URL plus optional actions, emulation settings, a viewport, and outputs. The service answers at once with a request ID and the status queued. The caller polls the result until the status reads ok or error.
Each output returns in one of two ways.
Output
Returned as
title
An inline field
text
An inline field, capped at 20,000 characters
html
An artifact that holds the full page
screenshot
An image artifact of the full page by default
The result also carries the HTTP status, duration, console messages, page errors, and the return values of evaluate actions. The default timeout is 30 seconds for navigation and for each action. The default viewport is 1,280 by 900 pixels.
Queued and running requests live only in memory. A restart loses them. Completed results and artifacts persist on the data volume through a restart.
Connections
Fetch depends on four platform services and one upstream project.
System
What passes between them
Consul
Service registration and the pool size setting
Vault
Service credentials, read at startup
Nexus
Python packages for the image build
core-auth
Access tokens for service-to-service calls
Playwright
The browser engine, pinned to one version with a matching Chromium build
Publishing and its command line
https://core-publishing.service.consul
Publishing is the release route for the stack. It turns a version into a release tag, one image, and a deployment on each host. Two parts carry the work: the core-publishing service and its command-line interface (CLI), core-publishing-cli. The route exists so that every environment runs the same tested build.
Role
The CLI starts every release step from a developer machine. One command versions a repository in four steps: a version bump, a commit, a release tag for major and minor versions, and a push. The bump covers every file that declares the version.
A deploy takes a release tag, never the working tree. One image builds once on core-docker-1 for both hosts. A development deploy runs it there. A production deploy ships the same image to core-docker-aws-1. Each host supplies its own configuration and secrets.
Guards protect running work. A deploy holds while a running game server still has open sessions. A deploy also fails before any change when a host lacks free disk space.
The core-publishing service holds release state on core-docker-1. It registers each repository’s publishing manifest. It tracks one stack per environment. It records deployment history, health, and drift.
Parts
Each part owns one job.
Part
Member
Job
set-version
CLI
Bumps, commits, tags, and pushes a release
deploy-dev and deploy-prod
CLI
Deploy one release tag to core-docker-1 or core-docker-aws-1
publish-dev and publish-prod
CLI
Send the new files of an assets release to its content site
view-dev and view-prod
CLI
List each host’s containers with their image versions
API server
Service
Registers manifests and runs stack operations
Deploy worker
Service
Runs queued deploy and destroy jobs one at a time
Health monitor and drift detector
Service
Check stack health every 60 seconds and drift every 5 minutes
Data
The CLI keeps no state. Git holds the versions and release tags. Each target keeps its running release and the one before it.
The service stores its records in PostgreSQL. Redis holds its deploy queue and short-lived health results. The service keeps these main records.
Record
Holds
Manifest
A repository’s registered manifest, its version, and its validation result
Stack
One repository in one environment, with its state, health, and lock
Deployment
One operation with its commit, release tag, image tag, status, and duration
Drift event
A difference between a stack’s expected and actual configuration
A stack moves through the states planned, applying, running, stopped, failed, destroying, and destroyed.
Connections
The CLI hands each release to the Core bootstrap, which builds, ships, and starts it. The route exchanges data with these systems:
Registry: The CLI looks up each repository and the development host there.
GitHub: A release tag must exist locally and on the remote before a deploy starts.
Consul and Vault: The target host’s pair supplies the configuration and secrets for each release.
PostgreSQL: Each deploy applies the repository’s database patches before its containers start.
Model Context Protocol (MCP) gateway: It exposes the CLI as its publishing tool.
The service finds PostgreSQL, Redis, and Vault through Consul.
NervHQ is the operations dashboard for the stack. It shows machines, services, projects, agent sessions, work queues, code activity, and system health on one screen. The NervHQ API gathers that data from the rest of the stack. Together they show what runs and what needs attention without a query to each service.
Role
The dashboard, core-nervhq, is a React single-page application written in TypeScript. It runs as static files that nginx serves, with no server-side code.
The API, core-nervhq-api, is a FastAPI service written in Python. It coordinates collector daemons, scheduled jobs, PostgreSQL storage, Redis caching, and WebSocket event streams. It also records agent sessions. Both members run on core-docker-1.
Parts
The dashboard groups its views into five sections:
Dashboard: the daily log, sessions, and a Magi graph viewer across all themes
Project management: Scrolls initiatives, epics, stories, tasks, and bugs
Seele: a document reader, a searchable library, and a map of the corpus colored by theme
Themes: one dashboard per theme with sessions, GitHub activity, projects, Docker, and action items
Admin: system settings, theme management, and GitHub repository management
The API runs these parts inside one service.
Part
Job
Collector daemons
Poll GitHub, infrastructure health, and Nexus
Scheduler
Fires stored jobs and sends each task to an idle daemon with the matching capability
Database worker
Writes and reads PostgreSQL records
Cache worker
Updates Redis and adds each change to a 60-second event buffer
Session ingester
Sums token usage per session, model, and agent every minute
Routers
Serve the cache, sessions, action items, the daily log, notepad entries, and Magi graph files
The daemons hold no database access. Every result returns to the API over a WebSocket.
Data
The API stores durable records in PostgreSQL. A Redis cache serves the dashboard from two groups of tables.
Group
Tables
Contents
Expiry
Database-backed
24
GitHub activity, sessions, projects, action items, settings, and the daily log
None
Infrastructure
10
Health and package data from the collectors
300 seconds
Session data covers each session’s activity, summary, changed repositories, and token usage by model and agent. Infrastructure entries expire when the collectors stop reporting. The dashboard mirrors the same 34 tables in its own store.
Connections
The API exchanges data with these systems:
Model Context Protocol (MCP) gateway: It sends a typed close event at the end of each session.
Scrolls: The API passes each work-tracking request to the Scrolls service.
Registry: The dashboard shows each repository’s development host from the Registry.
Nexus: A collector inventories packages. It checks proxied packages against Open Source Vulnerabilities (OSV) data.
Consul and Vault: The health collector reads the Consul service catalog and Vault’s status.
One transaction records each closed session with its activity, summary, and changed repositories. Database writes then update Redis. WebSocket events refresh the dashboard without a full reload. Token usage reaches the dashboard within 6 minutes of each agent turn.
Consul and Vault are two foundation services that every other service in the stack depends on. Consul tracks where each service runs. It also holds shared settings. Vault stores the secrets that services need. Both are off-the-shelf HashiCorp products. Together they keep addresses and credentials out of source code, images, and prompts.
Role
Consul provides service discovery. Each service registers its name, location, tags, and health check when it starts. Clients resolve that name at call time through Domain Name System (DNS) or HTTP lookups. No service stores another service’s address. Name lookups work on every machine in the development environment.
Consul also keeps shared, non-secret configuration in a key-value store. Secrets never enter it.
Vault stores versioned key-value secrets. Each service authenticates to Vault with its own identity, and no two services share one. A policy limits each service to its own secrets. The service receives its credentials at runtime. It holds them in memory.
No project keeps secrets in source, environment files, plaintext configuration, or a local secret store. Configuration flows through Consul, and secrets flow through Vault.
Parts
Both Docker hosts, core-docker-1 and core-docker-aws-1, run their own Consul and Vault. Each pair runs under the same names, so one compose file deploys a service to either host.
The Core bootstrap brings up the stack in three tiers.
Tier
Services
Configuration source
Secret source
Foundation
Consul, Vault, and Nexus
Fixed settings at deploy time
Not applicable
Infrastructure
Redis and PostgreSQL
Consul
Vault
Application
Every other service
Consul
Vault
Consul starts first. Vault, Nexus, Redis, and PostgreSQL follow in that order. For each service, the Core bootstrap runs three steps: deploy, health check, and Consul registration. The bootstrap seeds Consul with non-secret values. It initializes the Vault secrets engine. It seeds approved values. It also creates the service policies.
Data
Consul keeps three kinds of data:
A catalog of registered services
Health check results for each registered service
Key-value entries of non-secret settings, grouped by service
Each consumer has its own Vault entry for each system that it uses. Each entry keeps its earlier versions. These entries hold service credentials, such as database role passwords. The Vault standard forbids printing a service credential or writing it to a file.
Connections
Every managed service uses both. The main exchanges follow:
Managed services: Each one registers with Consul. It finds its dependencies by name.
Vault: Vault registers in Consul, so services find Vault by name as well.
PostgreSQL and Redis: Both read settings from Consul and credentials from Vault.
Publishing: Each release reads its configuration and secrets from the target host’s Consul and Vault.
NervHQ: Its health collector reads the Consul service catalog and Vault’s status.
Agent tools: A tool uses a secret without showing it to the conversation.
Nexus
https://nexus.service.consul
Nexus is the package registry for every project in the stack. It runs Sonatype Nexus Repository, an off-the-shelf product. Every build resolves its dependencies through Nexus, and every internal library publishes to it. Builds draw from one known source instead of whatever the public registries serve at the time.
Role
Nexus stands between the builds and the public package registries. It hosts private packages and proxies public ones. A proxy repository keeps a local copy of each public package after its first request. No project reads from a public registry directly.
Nexus also feeds security monitoring. Two checks compare its cached packages against the Open Source Vulnerabilities (OSV) database. A flagged version marks a library to update or to remove from the build.
Parts
Each language gets three kinds of repository. Python, JavaScript, and Rust each have the full set. Other ecosystems follow the same pattern. Each kind of repository has one job.
Repository kind
Holds
Used by
Group
One address that joins the proxy and hosted repositories
Every project build
Proxy
Cached copies of packages from a public registry
The group and the vulnerability checks
Hosted
Internal packages built in the stack
The group and internal library uploads
Projects configure only the group. Machine-level settings send every package install through it. The Core bootstrap creates the default repositories for each language when Nexus starts. It records their addresses in Consul. It also registers Nexus as a service.
Data
Nexus stores package files and the metadata that describes them. Each component carries a name and a version. Hosted repositories keep the internal releases. Proxy repositories keep every public package that a build has requested. That cache is the set of components the security checks scan.
Connections
Nexus exchanges packages, addresses, and findings with the systems in this table.
System
What passes
Project builds
Package requests go to the group. Packages return from the hosted or proxy repository.
Publishing
Release builds install JavaScript packages through the group.
Consul
Nexus registers as a service. The bootstrap stores the repository addresses as settings.
Vault
Vault holds the credentials for uploads to hosted repositories.
NervHQ
A collector lists the components. It checks proxy packages against OSV data. Redis caches the results for the dashboard.
Seele
An agent job scans the proxy cache against OSV. It records the blocked versions for each language in Seele.
Postgres
https://core-db.service.consul
Postgres is the relational database for the stack. It runs PostgreSQL, an off-the-shelf open-source database. Postgres holds the structured records that must survive a restart: users, projects, work items, history, settings, and the relationships between them. Applications read and change these records while they run.
Role
Core manages the database for every project. A project does not create databases, run migrations, or manage roles for other services. It supplies numbered schema patches. It provisions its own application role through the Core bootstrap. A shared Core infrastructure repository owns the containers, versions, tuning, and extensions.
Parts
Postgres has five parts across the development and production hosts.
Part
What it does
postgres-core
Holds the general application databases on core-docker-1, on the stock PostgreSQL image
Seele instance
Holds Seele’s nodes and edges, with the pgvector extension for embedding similarity
core-db
Applies schema patches, refreshes grants, and provisions service roles
Project bootstrap
Hands each project’s patches to core-db at every deploy
Production instance
Runs on core-docker-aws-1 under the same name as at home, so services connect the same way in both places
core-db runs a discovery loop. Each cycle finds the Postgres instances in the Consul catalog. It reads their credentials from Vault. It then lists the databases on each instance. Patching never starts on its own. A deploy or an operator starts every run.
Data
Each project gets its own database. Each service that uses the database gets its own login role. A service role may read and write table data. No service role holds superuser rights.
A schema arrives as numbered SQL patch files, applied in order, each in its own transaction. Every database starts with a required first patch that is safe to run twice.
core-db stores a checksum for every patch it applies. A patch edited after it ran fails that check, and core-db applies nothing. Each schema therefore matches its patch files. After every successful run, core-db refreshes the grants of each service role, so every role reaches a new table at once.
Connections
Postgres exchanges addresses, credentials, patches, and records with the systems in this table.
System
What passes
Consul
Each instance registers as a service. Services and core-db find the instances by name.
Vault
Vault holds the password of every role.
Publishing
Release deploys hand each project’s patches to core-db. Production runs no core-db, so there the deploy applies the same patches directly.
Applications
Python services connect through the database pool in the shared Core runtime library.
Redis
Redis caches data whose durable copy lives in Postgres. NervHQ reloads its cache tables from Postgres after a Redis restart.
Seele, Scrolls, and NervHQ
Each keeps its records in its own database.
Redis
redis://redis.service.consul
Redis is the shared cache for the stack, an off-the-shelf data store that keeps its data in memory. Core runs it, and projects only consume it. Redis holds information that changes too fast to fetch from the main database each time. Durable records stay in Postgres, and secrets stay in Vault.
Role
Redis is the only cache in the stack. No project deploys its own cache or another caching technology. Services use Redis for caches, short-lived infrastructure snapshots, counters, queues, and live dashboard data. Every cached value is disposable, so a service must still work when the cache is empty.
Parts
Redis has three parts:
Development container: One Redis container runs on core-docker-1. The Core bootstrap starts it with settings from Consul and a password from Vault.
Production container: A matching container runs on core-docker-aws-1 under the same name. Publishing holds its configuration.
Client libraries: Shared Core libraries for Python and Rust each provide a Redis client. Python services must use it. They never manage a connection inline.
Data
Redis holds keys and values in memory. Each key starts with the owning service’s catalog name, then a domain, then the key itself: <service-id>:<domain>:<key>. Snapshot entries carry an expiry time. NervHQ’s infrastructure snapshots expire after 300 seconds, so stale entries clear when a collector stops reporting. Publishing’s cached health results expire after 120 seconds.
Neither host writes Redis data to disk. A restart empties Redis, and services rebuild what they cache. A full Redis refuses new writes with an error. It never drops keys silently.
Connections
Redis exchanges settings, credentials, and cached data with the systems in this table.
System
What passes
Consul
Services find Redis by name. The bootstrap reads the Redis settings from Consul.
Vault
Vault holds the Redis password.
Postgres
Postgres keeps the durable copy. NervHQ reloads its cache tables from Postgres after a restart.
NervHQ
The API caches dashboard data and infrastructure snapshots. Database writes update Redis, and WebSocket events refresh the dashboard.
Publishing
Publishing keeps its deploy queue and cached health results in Redis.
Game services
Game APIs keep limit counters and other short-lived state in Redis.
Design
https://core-design.service.consul
Design is the workspace for interface work before it becomes a product. It pairs the core-design review site with the Design knowledge base in Seele. Claude writes design files to disk, and the site shows each one in a real browser frame. Review happens on the rendered page.
Role
Design carries interface work from research to a reviewed page. Seele holds the research findings and the approved references. Research binds nothing, and standards follow from it. Concepts select references, and core-design turns those decisions into versioned browser artifacts and reusable style guides.
Each piece of work follows one loop: research → concept → source artifact → browser render → screenshot review → revision.
Parts
The site offers five views.
View
What it shows
Overview
Every design project as a card, with names from the Registry and usage notes from Seele
Designs
A file tree, the design frame at preset device widths, and an element inspector
Artifacts
The same tools over artifacts that Claude built in its web app
Artboard
A vector board with the core tools and shortcuts of Adobe Illustrator
Docs
A reader for the Design knowledge base, with full-text search
The frame reloads when any file in the open project changes. The inspector shows an element’s box, computed values, and matching style rules. A generator builds dated style-guide snapshots of other repositories’ tokens and components.
The application uses TypeScript, React, and Vite. The site runs on the workstation, because it serves the design folders from the working copy.
Data
The site has no backend. Each design is a plain HTML page with its own style sheets and scripts. Each top-level folder is one project. Its name states the kind: governance, style guide, theme, or repository.
Artifacts keep the file layout they were published with. The site wraps each artifact page in the same document skeleton that Claude’s web app adds, so the page renders the same way. The artboard keeps its sketch in browser storage. It exports Portable Network Graphics (PNG) files at up to 3× scale and Scalable Vector Graphics (SVG) files.
Seele holds the knowledge as linked nodes: research findings, references with source notes, studies of real sites, and concepts. Every node carries tags from a fixed vocabulary, and design work finds its sources by those tags. Token values live in code, never in Seele.
Connections
Design exchanges files, names, and renders with the systems in this table.
System
What passes
Seele
The site reads and searches the Design knowledge base. It never writes to Seele.
Registry
The Overview reads display names, themes, and repository links. It never writes to the Registry.
Core Fetch
Core Fetch captures the rendered result at real viewport sizes for screenshot review.
Claude sessions
Claude writes design files and artifacts to disk. Copy SVG puts a sketch on the clipboard as text that pastes into the chat.
Consul
The site registers as a service when it deploys.
Bricks
https://minigame-bricks-web.service.consul
Bricks is a toy-brick vehicle builder with live multiplayer driving, played in the browser. A player builds a vehicle from parts, then drives it with other players through 3D worlds built from real places.
In the harness
Bricks runs through the same harness as every other project. Its documents live in Seele, its work in Scrolls, and its repositories in the Registry. The game splits into a game client, a game service, a physics server, an admin service, and a content build. Each part has its own repository and its own document chain.
Releases deploy from version tags through Publishing, to the development host and then the production host. The worlds reuse world-building tools first written for DMK Racing.
Assets
The catalog includes a 2×4 brick, a seat, a steering wheel, an engine, and a wheel. The home page’s Extras show every drivable world on the globe, with its terrain.
The five parts are converted from the LDraw Parts Library, by the LDraw.org contributors, under the Creative Commons Attribution 4.0 license. The part authors are James Jessiman, Massimo Maso, Vincent Messenet, Donald Sutter, and Magnus Forsberg. Steffen, OrionP, Philo, guyvivan, and ejboer contributed edits. LDraw is a trademark owned and licensed by the Estate of James Jessiman. LEGO is a registered trademark of the LEGO Group, which does not sponsor, endorse, or authorize this game.
Connections
Bricks depends on these systems of the stack.
System
What passes
Postgres
Accounts and saved creations
Redis
Short-lived session data
Consul
Service registration and lookup by name
Vault
Credentials for each service
Nexus
Packages for every image build
Publishing
Release tags, deploys, and content publishes
Seele
The project’s documents and decisions
DMK Racing
https://dmkracing-web.service.consul
DMK Racing is a turn-based racing board game, played on a 3D board in the browser. Its tracks come from real circuits and roads around the world. People and bots race together in one race.
In the harness
DMK Racing runs through the same harness as every other project. Its documents live in Seele, its work in Scrolls, and its repositories in the Registry. The game splits into a game client, a game service, an admin service, and a content build. Each part has its own repository and its own document chain.
Releases leave from version tags through Publishing, to the development host and then the production host. The tracks, cars, and audio ship apart from the code images, as a separate content publish.
Assets
The game’s cars include a stock car, an open-wheel car, and a road car. Each car is a flat-panel model built in Blender. The home page’s Extras show every track on the globe, where it lies.
Connections
DMK Racing depends on these systems of the stack.
System
What passes
Postgres
Accounts, races, and results
Redis
Short-lived counters
Consul
Service registration and lookup by name
Vault
Credentials for each service
Nexus
Packages for every image build
Publishing
Release tags, deploys, and content publishes
Studio
Background music and interface sounds
Seele
The project’s documents and decisions
Skyharbor
https://minigame-skyharbor-web.service.consul
Skyharbor is a single-player airline game on a globe, played in the browser. The player connects real airports with routes, and the network grows until it can no longer keep up.
In the harness
Skyharbor runs through the same harness as every other project. Its documents live in Seele, its work in Scrolls, and its repositories in the Registry. The game is two repositories: one holds the rules, and one plays the game in the browser. A bot test in Fetch plays games headless to check each change.
The whole game runs in the browser, with no game server. Releases deploy from version tags through Publishing, to the development host and then the production host.
Assets
The aircraft ladder has five planes, from a prop to a heavy. Each plane is a Blender model in white card and ink. The home page’s Extras show every airport on the globe, with its runways.
Connections
Skyharbor depends on these systems of the stack.
System
What passes
Publishing
Release tags and deploys to both hosts
Nexus
Packages for the image build
Consul
Registration of the development service
Studio
The game’s sounds
Fetch
Headless bot games for testing
Registry schema
The Registry is the inventory of a development environment. It records the themes, projects, source repositories, dependencies, and machines that make up the environment. This page describes a new Registry with no records in it yet.
The data it manages
Five kinds of record fill the Registry. A meta block sits beside them and describes the data file itself.
Record
What it holds
Theme
A top-level grouping of projects, with a name and a description
Project
One unit of work inside a theme, with a type and a short context tag
Source
The one repository of a project: language, repository URL, default branch, release tags, and development host
Libraries
The dependencies read from a source’s manifest files, each with a version and a scope
Host
A machine in the environment, with its operating system, address, and role
Meta
The development base path, the time of the last change, and the schema version
A new Registry starts with every list empty. A theme comes first, because every project names one. Each new project then brings exactly one source with it.
Where it lives
The developer keeps the Registry’s source in GitHub. The data is available through an API, and every agent and tool reads and writes it there.
The schema
The schema mirrors the stored file. Each field holds a description of its value in place of data.
Text, with its length range and its allowed characters
<string|null>
Text, or null when the field has no value yet
<enum>
One value from the list that follows, with slashes between the choices
<base62>
A 6-character identifier that the Registry issues from a counter
<ISO 8601>
A date and time in Coordinated Universal Time (UTC)
<bool>
True or false
<int|null>
A whole number, or null
FROM
A reference to a field in another list, which must already exist
The references chain the lists together. A project names its theme, a source names its project, and a libraries record names its source. A source names its development host by the host’s name, not its identifier.
Themes, projects, sources, and hosts are never deleted. Their status moves to inactive instead. A theme that goes inactive takes its projects and their sources with it.
Scrolls schema
Scrolls keeps its work items in one PostgreSQL database of 15 tables. This page gives that schema as a new install receives it, with every table empty.
What the schema holds
Every table has a serial integer key for joins and a universally unique identifier (UUID) for references from outside the database. Each theme, initiative, epic, and work item has a kebab-case name, held to that form by a check constraint.
Stories, tasks, bugs, and subtasks each own one row in scroll_work_ref. A subtask names its parent through that row, so one column can point at a story, a task, or a bug.
Two triggers run on the tables that change. One stamps updated_on on every update. The other rejects a status whose applies_to list leaves out the row’s kind of entity.
The lookup tables start empty. Every theme and work item names a status, so the developer adds the lookup values before the first theme. An initiative names its template on insert, because an empty database holds no default template.
Downloads
Two files download from the top of the page. The Markdown file holds this description and every table’s definition. The SQL file holds the definitions alone, in dependency order, for an empty PostgreSQL 13 database or later.
Relationships
The diagram shows only the columns that link one table to another. Each line runs from a foreign key to the primary key it names. Lines that end at the same key merge before they reach it.
Linked columns only, each line from a foreign key to the key it names
Lines to the hierarchy leave a work item on its left. Lines to the lookups and agents leave on its right. The depends_on arrays hold the IDs of other work items and carry no foreign key.
Functions
The triggers call two shared functions, so this block runs first.
functions
CREATE FUNCTION scroll_set_updated_on()
RETURNS trigger AS $$
BEGIN
NEW.updated_on = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE FUNCTION scroll_status_applies_to_check()
RETURNS trigger AS $$
DECLARE
kind text := TG_ARGV[0];
nm text;
appl text[];
BEGIN
SELECT s.name, s.applies_to
INTO nm, appl
FROM scroll_status s
WHERE s.id = NEW.status_id;
IF nm IS NULL THEN
RAISE EXCEPTION
'unknown status_id % for %', NEW.status_id, kind
USING ERRCODE = 'foreign_key_violation';
END IF;
IF NOT (kind = ANY(appl)) THEN
RAISE EXCEPTION
'status % is not valid for %; allowed: %',
nm, kind, (
SELECT array_agg(s.name ORDER BY s.id)
FROM scroll_status s
WHERE kind = ANY(s.applies_to)
)
USING ERRCODE = 'check_violation';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Lookups and agents
These tables hold the values the rest of the schema names.
scroll_status
CREATE TABLE scroll_status (
id serial PRIMARY KEY,
key uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
name text NOT NULL UNIQUE,
applies_to text[] NOT NULL,
created_on timestamptz NOT NULL DEFAULT now()
);
scroll_priority
CREATE TABLE scroll_priority (
id serial PRIMARY KEY,
key uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
name text NOT NULL UNIQUE,
sort_order int NOT NULL,
created_on timestamptz NOT NULL DEFAULT now()
);
scroll_size
CREATE TABLE scroll_size (
id serial PRIMARY KEY,
key uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
name text NOT NULL UNIQUE,
agent_default text NOT NULL,
created_on timestamptz NOT NULL DEFAULT now()
);
scroll_action
CREATE TABLE scroll_action (
id serial PRIMARY KEY,
key uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
name text NOT NULL UNIQUE,
created_on timestamptz NOT NULL DEFAULT now()
);
scroll_template
CREATE TABLE scroll_template (
id serial PRIMARY KEY,
key uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
name text NOT NULL UNIQUE,
created_on timestamptz NOT NULL DEFAULT now()
);
scroll_template_epic
CREATE TABLE scroll_template_epic (
id serial PRIMARY KEY,
key uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
template_id int NOT NULL REFERENCES scroll_template(id),
name text NOT NULL,
title text NOT NULL,
sort_order int NOT NULL,
role text NOT NULL DEFAULT 'domain'
CHECK (role IN ('sentinel', 'domain')),
created_on timestamptz NOT NULL DEFAULT now(),
UNIQUE (template_id, name),
CHECK (name ~ '^[a-z][a-z0-9]*(-[a-z0-9]+)*$')
);
scroll_agent
CREATE TABLE scroll_agent (
id serial PRIMARY KEY,
key uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
name text NOT NULL UNIQUE,
model text NOT NULL,
status text NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'inactive')),
max_context int,
created_on timestamptz NOT NULL DEFAULT now(),
updated_on timestamptz NOT NULL DEFAULT now()
);
CREATE TRIGGER trg_updated_on
BEFORE UPDATE ON scroll_agent
FOR EACH ROW
EXECUTE FUNCTION scroll_set_updated_on();
Hierarchy
Each level holds the one beneath it, from theme to epic.
scroll_theme
CREATE TABLE scroll_theme (
id serial PRIMARY KEY,
key uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
name text NOT NULL UNIQUE
CHECK (name ~ '^[a-z][a-z0-9]*(-[a-z0-9]+)*$'),
display_name text NOT NULL,
description text,
status_id int NOT NULL REFERENCES scroll_status(id),
labels text[] DEFAULT '{}',
created_on timestamptz NOT NULL DEFAULT now(),
updated_on timestamptz NOT NULL DEFAULT now()
);
CREATE TRIGGER trg_updated_on
BEFORE UPDATE ON scroll_theme
FOR EACH ROW
EXECUTE FUNCTION scroll_set_updated_on();
CREATE TRIGGER trg_status_check
BEFORE INSERT OR UPDATE OF status_id ON scroll_theme
FOR EACH ROW
EXECUTE FUNCTION scroll_status_applies_to_check('theme');
scroll_initiative
CREATE TABLE scroll_initiative (
id serial PRIMARY KEY,
key uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
theme_id int NOT NULL REFERENCES scroll_theme(id) ON DELETE RESTRICT,
name text NOT NULL
CHECK (name ~ '^[a-z][a-z0-9]*(-[a-z0-9]+)*$'),
title text NOT NULL,
epic_ident text NOT NULL
CHECK (epic_ident ~ '^[A-Z][A-Z0-9]{1,7}$'),
description text,
status_id int NOT NULL REFERENCES scroll_status(id),
labels text[] DEFAULT '{}',
created_on timestamptz NOT NULL DEFAULT now(),
updated_on timestamptz NOT NULL DEFAULT now(),
template_id int NOT NULL REFERENCES scroll_template(id),
UNIQUE (theme_id, name),
UNIQUE (theme_id, epic_ident)
);
CREATE INDEX idx_scroll_initiative_theme_id
ON scroll_initiative (theme_id);
CREATE INDEX idx_scroll_initiative_status_id
ON scroll_initiative (status_id);
CREATE INDEX idx_scroll_initiative_template_id
ON scroll_initiative (template_id);
CREATE TRIGGER trg_updated_on
BEFORE UPDATE ON scroll_initiative
FOR EACH ROW
EXECUTE FUNCTION scroll_set_updated_on();
CREATE TRIGGER trg_status_check
BEFORE INSERT OR UPDATE OF status_id ON scroll_initiative
FOR EACH ROW
EXECUTE FUNCTION scroll_status_applies_to_check('initiative');
scroll_epic
CREATE TABLE scroll_epic (
id serial PRIMARY KEY,
key uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
initiative_id int NOT NULL REFERENCES scroll_initiative(id) ON DELETE RESTRICT,
identifier text NOT NULL UNIQUE
CHECK (identifier ~ '^[A-Z][A-Z0-9]{1,7}-EPIC-\d{3}$'),
name text NOT NULL
CHECK (name ~ '^[a-z][a-z0-9]*(-[a-z0-9]+)*$'),
title text NOT NULL,
description text,
status_id int NOT NULL REFERENCES scroll_status(id),
sort_order int NOT NULL DEFAULT 0,
labels text[] DEFAULT '{}',
created_on timestamptz NOT NULL DEFAULT now(),
updated_on timestamptz NOT NULL DEFAULT now(),
phase double precision,
rank text,
UNIQUE (initiative_id, name)
);
CREATE INDEX idx_scroll_epic_initiative_id
ON scroll_epic (initiative_id);
CREATE INDEX idx_scroll_epic_status_id
ON scroll_epic (status_id);
CREATE TRIGGER trg_updated_on
BEFORE UPDATE ON scroll_epic
FOR EACH ROW
EXECUTE FUNCTION scroll_set_updated_on();
CREATE TRIGGER trg_status_check
BEFORE INSERT OR UPDATE OF status_id ON scroll_epic
FOR EACH ROW
EXECUTE FUNCTION scroll_status_applies_to_check('epic');
Work items
Each work item table carries its own indexes and triggers after its definition.
scroll_work_ref
CREATE TABLE scroll_work_ref (
id serial PRIMARY KEY,
key uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
kind text NOT NULL
CHECK (kind IN ('story', 'task', 'bug', 'subtask')),
created_on timestamptz NOT NULL DEFAULT now()
);
scroll_story
CREATE TABLE scroll_story (
id serial PRIMARY KEY,
key uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
work_ref_id int NOT NULL UNIQUE REFERENCES scroll_work_ref(id) ON DELETE CASCADE,
initiative_id int NOT NULL REFERENCES scroll_initiative(id) ON DELETE RESTRICT,
epic_id int REFERENCES scroll_epic(id) ON DELETE SET NULL,
identifier text NOT NULL UNIQUE
CHECK (identifier ~ '^[A-Z][A-Z0-9]{1,7}-STORY-\d{3}-\d{6}$'),
name text NOT NULL
CHECK (name ~ '^[a-z][a-z0-9]*(-[a-z0-9]+)*$'),
title text NOT NULL,
description text NOT NULL,
agent_spec text,
status_id int NOT NULL REFERENCES scroll_status(id),
priority_id int NOT NULL REFERENCES scroll_priority(id),
size_id int NOT NULL REFERENCES scroll_size(id),
agent_identifier int REFERENCES scroll_agent(id),
labels text[] DEFAULT '{}',
doc_references jsonb,
standards_references text[] DEFAULT '{}',
action_id int REFERENCES scroll_action(id),
action_ref_id int,
action_extra text,
auto_start boolean NOT NULL DEFAULT false,
depends_on int[] DEFAULT '{}',
due_date date,
completed_on timestamptz,
created_on timestamptz NOT NULL DEFAULT now(),
updated_on timestamptz NOT NULL DEFAULT now(),
phase double precision
);
CREATE INDEX idx_scroll_story_initiative_id
ON scroll_story (initiative_id);
CREATE INDEX idx_scroll_story_epic_id
ON scroll_story (epic_id);
CREATE INDEX idx_scroll_story_status_id
ON scroll_story (status_id);
CREATE INDEX idx_scroll_story_priority_id
ON scroll_story (priority_id);
CREATE INDEX idx_scroll_story_agent_identifier
ON scroll_story (agent_identifier);
CREATE INDEX idx_scroll_story_status_priority
ON scroll_story (status_id, priority_id);
CREATE TRIGGER trg_updated_on
BEFORE UPDATE ON scroll_story
FOR EACH ROW
EXECUTE FUNCTION scroll_set_updated_on();
CREATE TRIGGER trg_status_check
BEFORE INSERT OR UPDATE OF status_id ON scroll_story
FOR EACH ROW
EXECUTE FUNCTION scroll_status_applies_to_check('story');
scroll_task
CREATE TABLE scroll_task (
id serial PRIMARY KEY,
key uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
work_ref_id int NOT NULL UNIQUE REFERENCES scroll_work_ref(id) ON DELETE CASCADE,
initiative_id int NOT NULL REFERENCES scroll_initiative(id) ON DELETE RESTRICT,
epic_id int REFERENCES scroll_epic(id) ON DELETE SET NULL,
identifier text NOT NULL UNIQUE
CHECK (identifier ~ '^[A-Z][A-Z0-9]{1,7}-TASK-\d{3}-\d{6}$'),
name text NOT NULL
CHECK (name ~ '^[a-z][a-z0-9]*(-[a-z0-9]+)*$'),
title text NOT NULL,
description text NOT NULL,
agent_spec text,
status_id int NOT NULL REFERENCES scroll_status(id),
priority_id int NOT NULL REFERENCES scroll_priority(id),
size_id int NOT NULL REFERENCES scroll_size(id),
agent_identifier int REFERENCES scroll_agent(id),
labels text[] DEFAULT '{}',
repo text,
action_id int REFERENCES scroll_action(id),
action_ref_id int,
action_extra text,
auto_start boolean NOT NULL DEFAULT false,
depends_on int[] DEFAULT '{}',
due_date date,
completed_on timestamptz,
created_on timestamptz NOT NULL DEFAULT now(),
updated_on timestamptz NOT NULL DEFAULT now(),
phase double precision
);
CREATE INDEX idx_scroll_task_initiative_id
ON scroll_task (initiative_id);
CREATE INDEX idx_scroll_task_epic_id
ON scroll_task (epic_id);
CREATE INDEX idx_scroll_task_status_id
ON scroll_task (status_id);
CREATE INDEX idx_scroll_task_priority_id
ON scroll_task (priority_id);
CREATE INDEX idx_scroll_task_agent_identifier
ON scroll_task (agent_identifier);
CREATE INDEX idx_scroll_task_repo
ON scroll_task (repo);
CREATE INDEX idx_scroll_task_status_priority
ON scroll_task (status_id, priority_id);
CREATE TRIGGER trg_updated_on
BEFORE UPDATE ON scroll_task
FOR EACH ROW
EXECUTE FUNCTION scroll_set_updated_on();
CREATE TRIGGER trg_status_check
BEFORE INSERT OR UPDATE OF status_id ON scroll_task
FOR EACH ROW
EXECUTE FUNCTION scroll_status_applies_to_check('task');
scroll_bug
CREATE TABLE scroll_bug (
id serial PRIMARY KEY,
key uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
work_ref_id int NOT NULL UNIQUE REFERENCES scroll_work_ref(id) ON DELETE CASCADE,
initiative_id int NOT NULL REFERENCES scroll_initiative(id) ON DELETE RESTRICT,
epic_id int REFERENCES scroll_epic(id) ON DELETE SET NULL,
identifier text NOT NULL UNIQUE
CHECK (identifier ~ '^[A-Z][A-Z0-9]{1,7}-BUG-\d{3}-\d{6}$'),
name text NOT NULL
CHECK (name ~ '^[a-z][a-z0-9]*(-[a-z0-9]+)*$'),
title text NOT NULL,
description text NOT NULL,
agent_spec text,
status_id int NOT NULL REFERENCES scroll_status(id),
priority_id int NOT NULL REFERENCES scroll_priority(id),
agent_identifier int REFERENCES scroll_agent(id),
labels text[] DEFAULT '{}',
repo text,
action_id int REFERENCES scroll_action(id),
action_ref_id int,
action_extra text,
auto_start boolean NOT NULL DEFAULT false,
depends_on int[] DEFAULT '{}',
completed_on timestamptz,
created_on timestamptz NOT NULL DEFAULT now(),
updated_on timestamptz NOT NULL DEFAULT now(),
phase double precision
);
CREATE INDEX idx_scroll_bug_initiative_id
ON scroll_bug (initiative_id);
CREATE INDEX idx_scroll_bug_epic_id
ON scroll_bug (epic_id);
CREATE INDEX idx_scroll_bug_status_id
ON scroll_bug (status_id);
CREATE INDEX idx_scroll_bug_priority_id
ON scroll_bug (priority_id);
CREATE INDEX idx_scroll_bug_agent_identifier
ON scroll_bug (agent_identifier);
CREATE INDEX idx_scroll_bug_repo
ON scroll_bug (repo);
CREATE TRIGGER trg_updated_on
BEFORE UPDATE ON scroll_bug
FOR EACH ROW
EXECUTE FUNCTION scroll_set_updated_on();
CREATE TRIGGER trg_status_check
BEFORE INSERT OR UPDATE OF status_id ON scroll_bug
FOR EACH ROW
EXECUTE FUNCTION scroll_status_applies_to_check('bug');
scroll_subtask
CREATE TABLE scroll_subtask (
id serial PRIMARY KEY,
key uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
work_ref_id int NOT NULL UNIQUE REFERENCES scroll_work_ref(id) ON DELETE CASCADE,
parent_work_ref_id int NOT NULL REFERENCES scroll_work_ref(id) ON DELETE CASCADE,
initiative_id int NOT NULL REFERENCES scroll_initiative(id) ON DELETE RESTRICT,
identifier text NOT NULL UNIQUE
CHECK (identifier ~ '^[A-Z][A-Z0-9]{1,7}-SUB-\d{3}-\d{6}$'),
name text NOT NULL
CHECK (name ~ '^[a-z][a-z0-9]*(-[a-z0-9]+)*$'),
title text NOT NULL,
description text,
agent_spec text,
status_id int NOT NULL REFERENCES scroll_status(id),
priority_id int REFERENCES scroll_priority(id),
size_id int REFERENCES scroll_size(id),
agent_identifier int REFERENCES scroll_agent(id),
labels text[] DEFAULT '{}',
repo text,
action_id int REFERENCES scroll_action(id),
action_ref_id int,
action_extra text,
auto_start boolean NOT NULL DEFAULT false,
depends_on int[] DEFAULT '{}',
sort_order int NOT NULL DEFAULT 0,
completed_on timestamptz,
created_on timestamptz NOT NULL DEFAULT now(),
updated_on timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX idx_scroll_subtask_parent_work_ref_id
ON scroll_subtask (parent_work_ref_id);
CREATE INDEX idx_scroll_subtask_initiative_id
ON scroll_subtask (initiative_id);
CREATE INDEX idx_scroll_subtask_status_id
ON scroll_subtask (status_id);
CREATE INDEX idx_scroll_subtask_agent_identifier
ON scroll_subtask (agent_identifier);
CREATE TRIGGER trg_updated_on
BEFORE UPDATE ON scroll_subtask
FOR EACH ROW
EXECUTE FUNCTION scroll_set_updated_on();
CREATE TRIGGER trg_status_check
BEFORE INSERT OR UPDATE OF status_id ON scroll_subtask
FOR EACH ROW
EXECUTE FUNCTION scroll_status_applies_to_check('subtask');
Seele schema
Seele stores documentation as a graph in PostgreSQL. Three tables hold it: the nodes, the edges between them, and a version history of every write. A new installation starts with all three tables empty.
What Seele holds
A node is one unit of documentation. It carries an identity, a title, classification fields, Markdown content, and an optional embedding vector. Each node also records its token count and its created and modified dates.
An edge joins two nodes with a named relation. The relation sets how a read loads the target node. An optional rank orders the children of one parent.
A version row is a full snapshot of one node and its edges, taken at each write. The snapshots rebuild any earlier state of the graph.
The three tables divide the data this way.
Table
Holds
Key
node
One row per node: identity, metadata, content, and vector
id, a six-character base62 text identifier
edge
One row per relation between two nodes
from_id, to_id, and relation together
node_version
One snapshot per write of a node
id, an integer that grows with each write
Three objects support the tables. The pgvector extension supplies the vector column type. A sequence feeds the ID minter, which turns each number into a base62 identifier. A small function lets the full-text index read the subject array.
The full-text index ranks matches in three weights. The title counts most, the subject tags next, and the content least.
The graph library
The Seele API uses rustworkx, a graph library for Python with its core written in Rust. At startup the API loads every node and edge into a rustworkx directed graph in memory.
Reads walk that in-memory graph, not the database. A tree read follows the hasPart edges down from a root, in rank order. Each write lands in PostgreSQL first and updates the graph after the commit. PostgreSQL stays the source of truth.
Downloads
Two files download from the top of the page. The Markdown file holds this description and the definition of every table. The SQL file holds the definitions alone, in dependency order. It runs on an empty PostgreSQL database with the pgvector extension available.
Relationships
Every link in the schema points at the node table. The diagram shows only the linked columns: the primary key of a node and the foreign keys that name it.
Seele tables, linked columns only
An edge row needs both of its nodes to exist first. A version row needs its node. A schema load therefore creates the node table before the other two.
Table definitions
Each block holds one table with its indexes. The setup block comes first, because the node table depends on it.
setup
CREATE EXTENSION IF NOT EXISTS vector;
CREATE SEQUENCE seele_id_seq START 1;
CREATE OR REPLACE FUNCTION immutable_array_to_string(text[], text)
RETURNS text
LANGUAGE SQL
IMMUTABLE
PARALLEL SAFE
AS $$ SELECT array_to_string($1, $2) $$;
node
CREATE TABLE node (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
type TEXT NOT NULL,
theme TEXT NOT NULL,
context TEXT,
qualifier TEXT,
subject TEXT[] NOT NULL DEFAULT '{}',
created DATE NOT NULL,
modified DATE NOT NULL,
tokens INTEGER,
filename TEXT UNIQUE,
content TEXT NOT NULL DEFAULT '',
vector vector
);
CREATE INDEX idx_node_filename ON node (filename);
CREATE INDEX idx_node_theme_type ON node (theme, type);
CREATE INDEX idx_node_fts ON node USING GIN ((
setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
setweight(to_tsvector('english', immutable_array_to_string(subject, ' ')), 'B') ||
setweight(to_tsvector('english', coalesce(content, '')), 'C')
));
edge
CREATE TABLE edge (
from_id TEXT NOT NULL REFERENCES node(id),
to_id TEXT NOT NULL REFERENCES node(id),
relation TEXT NOT NULL,
rank TEXT,
PRIMARY KEY (from_id, to_id, relation)
);
CREATE INDEX idx_edge_to ON edge (to_id, relation);
CREATE INDEX idx_edge_from_relation ON edge (from_id, relation);
node_version
CREATE TABLE node_version (
id BIGSERIAL PRIMARY KEY,
node_id TEXT NOT NULL REFERENCES node(id),
written_at TIMESTAMPTZ NOT NULL DEFAULT now(),
session_id TEXT NOT NULL,
node_data JSONB NOT NULL,
edge_data JSONB NOT NULL
);
CREATE INDEX idx_version_node_time ON node_version (node_id, written_at DESC);
Core MCP
Core Model Context Protocol (MCP) is the tool server that every Claude session in the development environment talks to. MCP is the standard way an agent calls outside tools. One Rust program serves 107 tools in 11 groups. Some run on the developer’s machine, and the rest forward to the services that own the data.
One local server, two relays in priority order
Role
Core MCP gives an agent one place to reach the whole environment. The agent sees a single list of tools and never needs to know where a tool runs. File search, Git, Docker, and PowerShell run beside the agent on the developer’s machine. Documentation, work tracking, notes, and session records live in services. Core MCP forwards those calls.
The program runs in two modes from the same build.
Mode
Where it runs
Job
Proxy mode
Each developer machine, started by Claude
Serves every tool over standard input and output, runs local tools, and forwards the rest
Service mode
A container or a background service on a server
Accepts forwarded calls over HTTP and passes each one to the service that owns it
Service mode runs no local tools. It has no file access, no Git, no shell, and no Docker access. It needs only the network, so its container needs no mounted volumes.
Proxy MCP servers
The two proxy MCP servers in the diagram are service mode instances on two different servers. The local Core MCP holds them as an ordered list. Each forwarded call goes to the first one that answers.
A proxy maps each tool name to one request on the service behind it. Seele tools go to Seele, and Scrolls tools go to Scrolls. Notepad and session tools go to the NervHQ service. The proxy looks up Seele and Scrolls in Consul on every call, so a moved service needs no redeploy.
How proxies are added
Two mechanisms add a proxy, and both leave the tool list unchanged:
Relay list: the local server reads an ordered list of proxy addresses when Claude starts it
Registered hosts: any host in the Registry with a Core MCP address becomes a target on the next call
The relay list sets failover. When a proxy fails to connect, Core MCP tries the next one and skips the failed proxy for 30 seconds. Calls return to the first proxy once it answers again. An error from a live proxy comes back as it is and never fails over.
Registered hosts need no restart. Core MCP reads the host from the Registry on each call that names one. A new server joins with one registry_host_add call that includes its Core MCP address. A call that names an unknown host returns the hosts that accept forwarded calls.
Running a tool on another host
Most tools take an optional remote_host argument. Without it, a tool runs where its group runs. With it, Core MCP looks up that host in the Registry and sends the call to the Core MCP address on the host’s record.
The named host answers only the tools its own Core MCP serves. A relay in service mode serves the forwarded groups, so a forwarded tool skips the relay list and goes to that host. Registry additions and bootstrap run only where the agent runs.
Tool groups
Each group covers one area of the environment. The table gives each group’s tool count, where its tools run, and what they do.
Group
Tools
Runs
What it does
Registry
14
Local, against the Registry service
Reads and changes the records for themes, projects, sources, and hosts, plus code snapshots
Global docs
2
Local, on the developer’s machine
Reads and writes documents in the documentation repositories, with front matter kept current
Seele
4
Forwarded through a proxy to Seele
Searches, reads, writes, and imports documentation nodes
Scrolls
50
Forwarded through a proxy to Scrolls
Creates, reads, updates, closes, and deletes work items from themes down to subtasks, plus agents and progress views
Notepad
6
Forwarded through a proxy to NervHQ
Keeps short notes by scope and key, with bulk writes and cleanup
Session
7
Forwarded through a proxy to NervHQ
Registers active sessions, takes heartbeats, closes stale sessions, and reads recorded ones
Git
8
Local, on the developer’s machine
Reads repository state and history, and stages, commits, and pushes changes
Code and files
4
Local, on the developer’s machine
Searches file contents, lists directories, builds a structural code snapshot, and runs SQL against PostgreSQL
Ops
6
Local, except the session record, which goes through a proxy to NervHQ
Runs PowerShell, starts a bootstrap, sets up a repository, finds stale containers, and records a session
Publishing
1
Local, where the publishing command line is installed
Passes any command to the publishing command line and returns its JSON result
Docker
5
Local, against any Docker host in the Registry
Lists and inspects containers, reads logs, runs lifecycle actions, and runs Compose commands
Tools load on demand by group, so a session carries only the groups its work needs. The publishing tool passes its arguments through unchanged, so a new publishing command needs no change to Core MCP.
Builds
The default build turns on every tool. The two document tools sit behind build features, so a build can drop either one. A dropped tool stays in the list and returns an error that names the missing feature.
A secondary development machine uses a build without the document tools. Every other tool stays on, and its forwarded tools reach the same proxies as the main machine.