3DMK

Overview

Systems

Games

Code/Schema

3DMKv2.1.3

Return

Getting started in agentic development

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.

“Deterministic vs. Probabilistic AI: The Enterprise Guide to Building Workflows That Scale,” Elementum, March 2026.

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.

Download

  • Markdown

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.

  1. OverviewWhat it is
  2. VisionOptional
  3. ProductOptional
  4. TechnicalHow it is built
  5. MagiWhat runs
  6. PlanningIn what order
  7. ScrollsThe queue
Each one approved, then checked against the one before

Each document fixes one layer of the project.

DocumentWhat it fixes
OverviewWhat the project is, its current state, its stack, its libraries, and its document index
VisionPurpose, users, goals, non-goals, and key constraints
ProductFeatures, user flows, interface behavior, and what is out of scope
TechnicalArchitecture, processing, data model, contracts, and publishing
MagiA machine-readable graph of every running part and every boundary between parts
PlanningAgent 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.

Download

  • Markdown

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.

Session 1 2 3 Foundation Repository Schema Bootstrap API Trails Readings Summaries Daemons Feed daemon Summary daemon Website Trail pages Map Release Checks Tag Development Production
WorkSent backEpic closesA stop for the developer
One way Trailcast could run, in three sessions

A schedule for Trailcast

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.

StateMeaning
CompletedDone and closed in Scrolls
In progressStarted and not yet finished
ReadyEvery dependency complete, and not yet started
BlockedWaiting 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
  1. BriefFrom Scrolls
  2. BuildSubagent
  3. TestsWhole suite
  4. CoverageNear 100%
  5. AcceptanceCriteria met
  6. BoundsAtlas check
  7. CloseScrolls, commit
  8. Sent back
Until every check passes
One releasePublishing
  1. ChecksEvery test
  2. TagVersion bump
  3. ImageBuilt once
  4. DevelopmentDeploy
  5. VerifyEnd to end
  6. ApprovalDeveloper
  7. ProductionSame image
  8. 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.

KindCoversRuns
UnitEach function and class on its own, with the feed and the database stubbedAfter every item
IntegrationThe API and both daemons against a test databaseAfter every item
End to endThe site in a browser against a running deployAfter 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.

NeedWhat the Atlas shows
Where a failure startsThe file and line of each class and function the failing test reaches
What a change addedNew files, classes, functions, and imports, against the previous snapshot
What depends on a moduleEvery import of that module across the repository
Whether a model matchesThe 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.

Download

  • Markdown

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.

ModeBehavior
ReviewRuns every phase, writes nothing, and reports every change it would make
ReconcileRuns every phase, writes the corrections to the documents, and reports what it fixed and what remains
RefactorRuns 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.

JobEntry pointTrusted firstWhen
Documentstechnical-inTechnical, reconciled to the configuration filesThe default, for a project in active development
Documentsoverview-outOverview, reconciled to the configuration filesA new project, or one just after a pivot
Codecode-inThe code, through the AtlasThe default, after active development
Codetechnical-outTechnicalTechnical 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.

ScoreMeaning
alignedTechnical describes what the Atlas shows
driftTechnical is partly correct, but outdated or incomplete
missingTechnical has no coverage of something the Atlas shows
contradictedTechnical 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.

Download

  • Markdown

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.

PartJob
Graph engineServes reads from an in-memory copy of the graph
Write gateValidates each batch and writes it to PostgreSQL and the in-memory graph
ImporterSplits Markdown files into nodes and edges for the write gate
ID minterIssues 6-character base62 identifiers from a counter
Consistency checkCompares 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.

RelationMeaningRead behavior
hasPartA parent holds a childLoads every descendant
requiresA node depends on another nodeLoads the target, one hop only
referencesA node points to another nodeReturns 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.

SystemWhat passes between them
Core MCPThe seele_search, seele_read, seele_write, and seele_import tools, forwarded by the Core MCP relay
PostgreSQLNodes, edges, and version records
core-authAccess tokens, checked on every call except health checks
core-dbSchema patches, applied at deploy time
ConsulService registration, and a fresh address lookup for each relay call
VaultDatabase and service credentials at runtime

Download

  • Markdown

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.

PartRole
core-scrolls-apiPython and FastAPI service, and the only reader and writer of the Scrolls database
PostgreSQLStores every theme, work item, agent, and lookup value
Schema repositoryOwns the database patches, apart from the service code
Core MCP toolsA 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.

LevelHoldsScope
ThemeInitiativesA product domain
InitiativeEpics, stories, tasks, and bugsA cross-cutting objective within a theme
EpicStories, tasks, and bugsA cohesive chunk of work within an initiative
StorySubtasksDocumentation, process, or design work that fits one conversation
TaskSubtasksCode, configuration, or infrastructure work that fits one conversation
BugSubtasksOne isolated, reproducible defect
SubtaskNoneOne 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.

SystemWhat passes between them
Core MCPAgent calls, forwarded to the service after a fresh Consul lookup on each call
PostgreSQLAll Scrolls records
SeelePlanning documents, which an agent turns into initiatives, epics, and work items
NervHQInitiative progress, roadmap views, and work-item detail on the dashboard

Download

  • Markdown

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.

PartJob
Registry storeHolds every record in memory and serves every read
Write lockLets one change run at a time
ID minterIssues 6-character base62 identifiers from a counter
Git clientCommits each change and pushes the history to GitHub
Reconciliation workerPolls GitHub for the latest commit of each source
Library workerRebuilds a source’s dependency list when a manifest changes
Atlas runnerParses 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.

RecordWhat it holds
ThemeA top-level grouping such as core, with a description
ProjectName, display name, context tag, project type, and owning theme
SourceThe one repository of a project: language, GitHub URL, default branch, development host and port, Consul service name, and release tags
HostOperating system, Docker endpoint, remote Core MCP endpoint, and machine role
LibrariesThe dependencies parsed from a source’s manifest files
Atlas snapshotThe 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.

SystemWhat passes between them
Core MCPReads and writes through the registry_* tools, plus a commit event after each Git commit that it makes
Publishing command lineA deploy event that sets a source’s development or production tag
GitHubCommit metadata for each source’s last code update
core-authAccess tokens that authorize writes and events
ConsulService registration and overrides for the reconciliation schedule

Download

  • Markdown

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.

PartJob
API routersAccept submissions and serve results, artifacts, status, and health
EngineAssigns request IDs, holds the queue, and tracks each request’s lifecycle
Worker poolRuns queued requests, each in its own isolated browser context
ChromiumLoads each page and runs the requested actions
Data volumeStores 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.

OutputReturned as
titleAn inline field
textAn inline field, capped at 20,000 characters
htmlAn artifact that holds the full page
screenshotAn 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.

SystemWhat passes between them
ConsulService registration and the pool size setting
VaultService credentials, read at startup
NexusPython packages for the image build
core-authAccess tokens for service-to-service calls
PlaywrightThe browser engine, pinned to one version with a matching Chromium build

Download

  • Markdown

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.

PartMemberJob
set-versionCLIBumps, commits, tags, and pushes a release
deploy-dev and deploy-prodCLIDeploy one release tag to core-docker-1 or core-docker-aws-1
publish-dev and publish-prodCLISend the new files of an assets release to its content site
view-dev and view-prodCLIList each host’s containers with their image versions
API serverServiceRegisters manifests and runs stack operations
Deploy workerServiceRuns queued deploy and destroy jobs one at a time
Health monitor and drift detectorServiceCheck 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.

RecordHolds
ManifestA repository’s registered manifest, its version, and its validation result
StackOne repository in one environment, with its state, health, and lock
DeploymentOne operation with its commit, release tag, image tag, status, and duration
Drift eventA 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.

Download

  • Markdown

NervHQ and its API

https://core-nervhq.service.consul · https://core-nervhq-api.service.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.

PartJob
Collector daemonsPoll GitHub, infrastructure health, and Nexus
SchedulerFires stored jobs and sends each task to an idle daemon with the matching capability
Database workerWrites and reads PostgreSQL records
Cache workerUpdates Redis and adds each change to a 60-second event buffer
Session ingesterSums token usage per session, model, and agent every minute
RoutersServe 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.

GroupTablesContentsExpiry
Database-backed24GitHub activity, sessions, projects, action items, settings, and the daily logNone
Infrastructure10Health and package data from the collectors300 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.
  • GitHub: Sync daemons collect repositories, branches, tags, commits, and changed files.
  • 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.

Download

  • Markdown

Consul and Vault

https://consul.service.consul · https://vault.service.consul

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.

TierServicesConfiguration sourceSecret source
FoundationConsul, Vault, and NexusFixed settings at deploy timeNot applicable
InfrastructureRedis and PostgreSQLConsulVault
ApplicationEvery other serviceConsulVault

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.

Download

  • Markdown

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 kindHoldsUsed by
GroupOne address that joins the proxy and hosted repositoriesEvery project build
ProxyCached copies of packages from a public registryThe group and the vulnerability checks
HostedInternal packages built in the stackThe 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.

SystemWhat passes
Project buildsPackage requests go to the group. Packages return from the hosted or proxy repository.
PublishingRelease builds install JavaScript packages through the group.
ConsulNexus registers as a service. The bootstrap stores the repository addresses as settings.
VaultVault holds the credentials for uploads to hosted repositories.
NervHQA collector lists the components. It checks proxy packages against OSV data. Redis caches the results for the dashboard.
SeeleAn agent job scans the proxy cache against OSV. It records the blocked versions for each language in Seele.

Download

  • Markdown

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.

PartWhat it does
postgres-coreHolds the general application databases on core-docker-1, on the stock PostgreSQL image
Seele instanceHolds Seele’s nodes and edges, with the pgvector extension for embedding similarity
core-dbApplies schema patches, refreshes grants, and provisions service roles
Project bootstrapHands each project’s patches to core-db at every deploy
Production instanceRuns 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.

SystemWhat passes
ConsulEach instance registers as a service. Services and core-db find the instances by name.
VaultVault holds the password of every role.
PublishingRelease deploys hand each project’s patches to core-db. Production runs no core-db, so there the deploy applies the same patches directly.
ApplicationsPython services connect through the database pool in the shared Core runtime library.
RedisRedis caches data whose durable copy lives in Postgres. NervHQ reloads its cache tables from Postgres after a Redis restart.
Seele, Scrolls, and NervHQEach keeps its records in its own database.

Download

  • Markdown

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.

SystemWhat passes
ConsulServices find Redis by name. The bootstrap reads the Redis settings from Consul.
VaultVault holds the Redis password.
PostgresPostgres keeps the durable copy. NervHQ reloads its cache tables from Postgres after a restart.
NervHQThe API caches dashboard data and infrastructure snapshots. Database writes update Redis, and WebSocket events refresh the dashboard.
PublishingPublishing keeps its deploy queue and cached health results in Redis.
Game servicesGame APIs keep limit counters and other short-lived state in Redis.

Download

  • Markdown

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.

ViewWhat it shows
OverviewEvery design project as a card, with names from the Registry and usage notes from Seele
DesignsA file tree, the design frame at preset device widths, and an element inspector
ArtifactsThe same tools over artifacts that Claude built in its web app
ArtboardA vector board with the core tools and shortcuts of Adobe Illustrator
DocsA 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.

SystemWhat passes
SeeleThe site reads and searches the Design knowledge base. It never writes to Seele.
RegistryThe Overview reads display names, themes, and repository links. It never writes to the Registry.
Core FetchCore Fetch captures the rendered result at real viewport sizes for screenshot review.
Claude sessionsClaude writes design files and artifacts to disk. Copy SVG puts a sketch on the clipboard as text that pastes into the chat.
ConsulThe site registers as a service when it deploys.

Download

  • Markdown

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.

SystemWhat passes
PostgresAccounts and saved creations
RedisShort-lived session data
ConsulService registration and lookup by name
VaultCredentials for each service
NexusPackages for every image build
PublishingRelease tags, deploys, and content publishes
SeeleThe project’s documents and decisions

The Rock Crawler, a Bricks creation, in gray. The Bulwark, a stock Bricks tank, in gray. An elevation tile of the Moab world, eight kilometers a side, floating over its shadow. An elevation tile of the Mt. Lemmon world, eight kilometers a side, floating over its shadow.

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.

SystemWhat passes
PostgresAccounts, races, and results
RedisShort-lived counters
ConsulService registration and lookup by name
VaultCredentials for each service
NexusPackages for every image build
PublishingRelease tags, deploys, and content publishes
StudioBackground music and interface sounds
SeeleThe project’s documents and decisions

An elevation tile of the Pikes Peak hill climb, four by ten kilometers, floating over its shadow. The 1970s open-wheel car, in gray. The 2020s open-wheel car, in gray. Two trees and two houses from the Tail of the Dragon kit, in gray. Two trees and two houses from the Japanese mountain pass kit, in gray.

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.

SystemWhat passes
PublishingRelease tags and deploys to both hosts
NexusPackages for the image build
ConsulRegistration of the development service
StudioThe game’s sounds
FetchHeadless bot games for testing

The prop plane from Skyharbor, in gray. The turboprop plane from Skyharbor, in gray. The narrowbody plane from Skyharbor, in gray. The widebody plane from Skyharbor, in gray. The heavy plane from Skyharbor, in gray. The site plan of a Skyharbor airstrip, the first airport level, on a card slab. The site plan of a Skyharbor macro hub, the fourth airport level, on a card slab.

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.

RecordWhat it holds
ThemeA top-level grouping of projects, with a name and a description
ProjectOne unit of work inside a theme, with a type and a short context tag
SourceThe one repository of a project: language, repository URL, default branch, release tags, and development host
LibrariesThe dependencies read from a source’s manifest files, each with a version and a scope
HostA machine in the environment, with its operating system, address, and role
MetaThe 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.

registry schema
{
  "meta": {
    "dev_base": "<string> 1-120 character ASCII",
    "last_updated": "<ISO 8601> UTC",
    "schema_version": "1.2"
  },
  "theme": [
    {
      "theme_id": "<base62> 6 character",
      "theme_name": "<string> 2-20 character a-z0-9",
      "theme_display_name": "<string> 2-30 character ASCII",
      "description": "<string> 0-256 character ASCII",
      "theme_base": "<string> 1-20 character ASCII",
      "status": "<enum> active/inactive"
    }
  ],
  "project": [
    {
      "theme_id": "FROM theme.theme_id",
      "project_id": "<base62> 6 character",
      "project_name": "<string> 2-30 character a-z0-9-",
      "project_display_name": "<string> 2-30 character ASCII",
      "context": "<string> 2-20 character a-z0-9",
      "project_type": "<enum> service/library/application/tool/plugin/infrastructure/documentation/assets/website-static/website-dynamic",
      "status": "<enum> active/inactive"
    }
  ],
  "source": [
    {
      "project_id": "FROM project.project_id",
      "source_id": "<base62> 6 character",
      "source_base": "<string> 1-20 character ASCII",
      "source_name": "<string> 1-30 character a-z0-9-",
      "source_type": "<enum> python/typescript/rust/go/swift/kotlin/csharp/lua/documentation/data/assets/infra/template",
      "provider": "<enum> github",
      "url": "<string> 0-256 character ASCII",
      "tag_dev": "<string|null> 1-40 character ASCII",
      "tag_prod": "<string|null> 1-40 character ASCII",
      "default_branch": "<string> 1-20 character a-zA-Z0-9-",
      "last_code_update": "<ISO 8601> UTC",
      "infra_dev_host": "<string|null> FROM host.host_name",
      "infra_dev_port": "<int|null>",
      "infra_service_name": "<string> 1-40 character a-zA-Z0-9-",
      "status": "<enum> active/inactive"
    }
  ],
  "libraries": [
    {
      "libraries_id": "<base62> 6 character",
      "source_id": "FROM source.source_id",
      "language": "<enum> python/typescript/rust/go/swift/kotlin/csharp/lua/documentation/data/assets/infra/template",
      "dependencies": [
        {
          "name": "<string> 1-128 character ASCII",
          "version": "<string> 1-40 character ASCII",
          "scope": "<enum> runtime/dev/prod"
        }
      ]
    }
  ],
  "host": [
    {
      "host_id": "<base62> 6 character",
      "host_name": "<string> 2-30 character a-z0-9-",
      "display_name": "<string> 2-30 character ASCII",
      "ip": "<string> IPv4 dotted quad",
      "os": "<enum> windows/macos/linux",
      "is_developer": "<bool>",
      "is_docker": "<bool>",
      "docker_url": "<string|null> tcp://<host>:<port>, required when is_docker",
      "remote_mcp_url": "<string|null> remote MCP server URL",
      "status": "<enum> active/inactive"
    }
  ]
}

Reading the schema

Each value names its type first, then its limits.

NotationMeaning
<string>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
FROMA 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.

Download

  • Markdown

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.

The tables fall into five groups.

GroupTablesHolds
Lookupsscroll_status, scroll_priority, scroll_size, scroll_action, scroll_templateThe allowed values a theme, an initiative, or a work item names
Templatesscroll_template_epicThe standard epics an initiative receives under its template
Agentsscroll_agentThe agents that take work, each with its model and context limit
Hierarchyscroll_theme, scroll_initiative, scroll_epicThe containers, from a product domain down to a cohesive chunk of work
Work itemsscroll_work_ref, scroll_story, scroll_task, scroll_bug, scroll_subtaskThe units of work and the reference they share

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.

scroll_theme idPK status_idFK scroll_initiative idPK theme_idFK template_idFK status_idFK scroll_epic idPK initiative_idFK status_idFK scroll_work_ref idPK scroll_template idPK scroll_template_epic template_idFK scroll_story work_ref_idFK initiative_idFK epic_idFK status_idFK priority_idFK size_idFK agent_identifierFK action_idFK scroll_task work_ref_idFK initiative_idFK epic_idFK status_idFK priority_idFK size_idFK agent_identifierFK action_idFK scroll_bug work_ref_idFK initiative_idFK epic_idFK status_idFK priority_idFK agent_identifierFK action_idFK scroll_subtask work_ref_idFK parent_work_ref_idFK initiative_idFK status_idFK priority_idFK size_idFK agent_identifierFK action_idFK scroll_status idPK scroll_priority idPK scroll_size idPK scroll_agent idPK scroll_action idPK hierarchy and templates work items lookups and agents
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');

Download

  • Markdown
  • SQL

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.

TableHoldsKey
nodeOne row per node: identity, metadata, content, and vectorid, a six-character base62 text identifier
edgeOne row per relation between two nodesfrom_id, to_id, and relation together
node_versionOne snapshot per write of a nodeid, 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.

edge from_idFK to_idFK 2 other columns node idPK 12 other columns node_version node_idFK 5 other columns each edge joins two nodes each write adds one version
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);

Download

  • Markdown
  • SQL

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.

Claude MCP client Calls each tool by name Core MCP Local machine, proxy mode 107 tools in 11 groups Local tools run in process Remote tools forward to a relay Hosts come from the Registry Looked up on every call Proxy MCP one Service mode, first choice Seele and Scrolls Notepad and sessions Proxy MCP two Service mode, fallback Seele and Scrolls Notepad and sessions MCP over stdio Tried first On failover
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.

ModeWhere it runsJob
Proxy modeEach developer machine, started by ClaudeServes every tool over standard input and output, runs local tools, and forwards the rest
Service modeA container or a background service on a serverAccepts 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.

GroupToolsRunsWhat it does
Registry14Local, against the Registry serviceReads and changes the records for themes, projects, sources, and hosts, plus code snapshots
Global docs2Local, on the developer’s machineReads and writes documents in the documentation repositories, with front matter kept current
Seele4Forwarded through a proxy to SeeleSearches, reads, writes, and imports documentation nodes
Scrolls50Forwarded through a proxy to ScrollsCreates, reads, updates, closes, and deletes work items from themes down to subtasks, plus agents and progress views
Notepad6Forwarded through a proxy to NervHQKeeps short notes by scope and key, with bulk writes and cleanup
Session7Forwarded through a proxy to NervHQRegisters active sessions, takes heartbeats, closes stale sessions, and reads recorded ones
Git8Local, on the developer’s machineReads repository state and history, and stages, commits, and pushes changes
Code and files4Local, on the developer’s machineSearches file contents, lists directories, builds a structural code snapshot, and runs SQL against PostgreSQL
Ops6Local, except the session record, which goes through a proxy to NervHQRuns PowerShell, starts a bootstrap, sets up a repository, finds stale containers, and records a session
Publishing1Local, where the publishing command line is installedPasses any command to the publishing command line and returns its JSON result
Docker5Local, against any Docker host in the RegistryLists 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.

Download

  • Markdown