This is _bmad-output/planning-artifacts/architecture.md, the plan this mind was built from, published as it is written with
one change: the owner's name is replaced with "the owner". Every FR, AC, NFR, AR and story tag on this
site links to the place here that defines it.
stepsCompleted: [1, 2, 3, 4, 5, 6, 7, 8] lastStep: 8 status: 'complete' completedAt: '2026-09-21' inputDocuments:
- _bmad-output/planning-artifacts/prd.md
- _bmad-output/planning-artifacts/ux-design-specification.md
- _bmad-output/design-artifacts/roster-design.md
- _bmad-output/design-artifacts/ux-verification-spine.md
- _bmad-output/design-artifacts/agent-examples/amygdala.md
- _bmad-output/design-artifacts/agent-examples/daydreamer.md
- _bmad-output/design-artifacts/agent-examples/sunk-cost.md
- compliance/README.md
inputDocumentsSkimmed:
- _bmad-output/design-artifacts/roster.csv
- compliance/colophon.md
inputDocumentsExcluded:
- _archive/planning/prd-discovery-notes.md # archived 2026-09-21 before this workflow; absorbed into the PRD
- _archive/planning/prd-validation-report.md # archived 2026-09-21; findings fixed and folded into the PRD
- _bmad-output/design-artifacts/*.html # running mocks, not architecture inputs
workflowType: 'architecture' project_name: 'Human42' user_name: 'the owner' date: '2026-09-21'
Architecture Decision Document
_This document builds collaboratively through step-by-step discovery. Sections are appended as we work through each architectural decision together._
Project Context Analysis
Requirements Overview
Functional Requirements
165 requirements, verified by tools/check-prd.py rather than counted by hand. 8 are Growth-tagged (FR36, FR41, FR53, FR66, FR72, FR86, FR105, FR106), leaving 155 in the MVP, which the Minimal Launch Set divides cleanly into 93 at t0 and 62 after, with no overlap and nothing unclassified. Architecture derives the inventory from the document rather than from its summary, and the checker now holds both halves: one check that every MVP requirement has a build stage, another that every one is classified into the launch set or deferred.
Four requirements were added after this document was first written. FR153 defines fit -- how well a (model, provider) pair suits an agent -- which FR129 and FR131 had both spent and neither defined. FR154 spreads the roster across model families, because fit computed per agent and maximised independently sends the whole roster to one family and destroys the society the measurement exists to protect. FR155 lets a signed-in visitor holding credits speak to a committee in FR141's three conversation forms while never being seated on it, and FR156 draws that visitor as a bubble every channel of which is substrate-drawn. FR157 adds the implementer -- the substrate-side component that turns a graduated task into a diff, which FR28 funded with its own sub-cap and FR29 required output from while nothing produced it -- and FR158 bounds what it may spend and says what happens to a task it cannot carry. FR159 names the classes of idea that are refused outright and FR160 bars the person who sends one.
FR159 is a gate in three places rather than a filter in one, which is an architectural fact: internal/cage refuses the text at intake, refuses the idea again at graduation before cognition hands it to a backlog, and refuses the task a third time at the implementer, because a committee can assemble a prohibited idea out of parts that each passed moderation. The list and the ban list are cage items, so the mind can read both and edit neither -- which matters most in the case where the person it wanted barred was its critic.
The implementer makes the lineage rule five-way. A change is written, approved and then graded, and a family that did any two of those three is grading something of its own, so the implementer joins the agents, the external reviewer, the outcome judge and the render grader in mutual disjointness. The registry's 26 approved families are what keep that satisfiable.
FR156 adds a third party to a partition built for two, which is an architectural fact rather than a styling one. Cross-cutting concern 1 has three kinds of state field -- agent-written, substrate-derived, sealed. A visitor writes none of them: its bubble is entirely Derived, and its text reaches a page only through Quote() exactly as agent text does. The identity is carried by chroma, since an agent hue sits above an identity floor and a visitor's bubble sits below it in the framework's own band, so coloured means an agent's claim is held by a measurable property rather than by a convention someone has to remember.
| Group | FRs | Growth |
|---|---|---|
| Cognition and Workspace | 47 | 0 |
| Operations, Safety, and Persistence | 22 | 2 |
| Economy and Revenue | 20 | 1 |
| Observatory and Sharing | 20 | 1 |
| Thought Intake | 18 | 0 |
| Roster, Contracts, and Contribution | 12 | 1 |
| Self-Modification and Action | 11 | 1 |
| Outcome Judgment and Learning | 7 | 1 |
| Decisions and Verdicts | 6 | 1 |
Cognition is more than a quarter of the contract and carries the clock (FR134, FR135), the blind ballot (FR136-FR140), the model registry (FR125-FR133), the pairing layer (FR153, FR154) and the standing economy (FR112, FR116, FR143). It is the architectural centre of gravity.
Architecturally the 163 resolve into four obligations rather than nine groups:
- Advance a barrier-synchronised loop over ~100 agents where every agent acts once per sync and the barrier waits for the slowest.
- Spend money deliberately under a $500/month cap, where the number of ideas promoted "is derived from the remaining spend allowance, so the mind votes as often as it can afford and no more, reaching zero rather than overspending."
- Prove what happened - append-only events with per-event time, per-vote arrival times, seeded runs, recorded model calls, exact replay.
- Hold a boundary the system it runs cannot cross - the cage, enforced by absence of credentials, protected paths, containers and the broker.
The broker is where the fourth obligation concentrates, and its shape is already decided. Counting what the PRD makes it enforce per call: registry-only routing, capability matching, licence class, commercial-use ceilings, four-way model-family disjointness, two budgets, per-provider hourly request budgets, ordered fallback, retry with backoff, failover at "schema-failure rate above 10% over 10 ticks", and recording "every call's inputs and outputs and the times it was issued and returned, which is where a vote event's arrival time comes from."
It also holds every secret - which settles where it can run. It cannot live on the runner tier, because that is where "mind-authored and stranger-authored code actually executes". But the runner needs it: the render grader "can never run on hosted Actions, which hold no keys", and the runner reaches the inference host "through the job channel and the broker rather than by sharing a machine with either". The runner must therefore call the broker while never holding what it holds, which makes the broker an authenticated internal network service with callers on all three tiers, not a library linked into each component. It must attribute every call to the correct sub-cap whichever tier issued it, and it sits on the critical path of the tick.
Non-Functional Requirements
Nine categories, 84 rows, of which 83 carry one of four gate classes: launch gate (blocks t0), per release (blocks a merge), drill (weekly or quarterly, pages the owner), monitored (reported in the digest). The gate class is an architectural input: per-release gates need CI hardware, drills need scheduling and paging, monitored rows need substrate counters that exist from day one.
Five rows carry two classes each, so the class counts sum to 88 rather than to
- The one ungated row is Reliability's "Loss of the world is the only
unacceptable outage", which states a posture rather than a measurement and needs no gate.
The check suite therefore splits by determinism rather than by cost or convenience. The verification spine states the failure mode plainly - "Timing checks on shared CI runners flake and then get muted. Put deterministic checks on hosted Actions, timing-sensitive ones on the substrate runner, which is unchanging hardware." Anything measuring a duration or a frame rate belongs on hardware that does not change underneath the measurement; validation, unit tests, and secret and dependency scanning do not. Getting this backwards does not fail loudly - it produces a muted frame-rate gate, which is how a quality gate dies.
The NFRs that will decide the design, rather than merely be satisfied by it:
- Triage under 8 seconds for the full roster over the 2,000-idea pool, "and never the longest sync in its tick". At the 200-agent measurement ceiling that is 400,000 natural scores per triage sync. The shape is a dense (agents x features) by (features x ideas) product - but FR15 requires "integer end to end ... so no float enters the scoring path and replay stays exact", which rules out the obvious float BLAS path and makes the numeric type an architectural decision rather than an implementation detail.
- The event log budget - "under 5,000 events per hour in normal operation and under 25,000 per hour at the intake cap", achieved by logging "never full rule-score matrices". Triage output is therefore recomputed on replay rather than stored, which is what makes determinism load-bearing.
- Restore under 2 minutes with zero lost events, drilled weekly, plus a quarterly restore onto a fresh machine within 4 hours.
- 1,000 concurrent live observers, 10,000 crowded - and neither the host nor the uplink binds. At 10 diffs per second and a kilobyte apiece that is roughly 10 megabytes a second of upstream: about a tenth of the gigabit link already in place, before the 10 gigabit one replacing it. Stating the bound against the link already present means the load test can run now rather than waiting on hardware. The engine may serve the connections itself, and the off-host fan-out relay is "a resilience option rather than a capacity requirement" - it protects against the home connection being down, not against the load.
- Frame rate 5 minimum, 30 target, above 100 bubbles, measured with the largest committee lit, at widths from 360 CSS pixels to roughly 7680.
- WCAG 2.2 AA, with target size held at the AAA figure of 44 by 44 and stance order recoverable in greyscale and forced-colors.
Scale & Complexity
- Primary domain: web application over a persistent multi-agent engine; secondary api_backend and developer_tool
- Complexity level: high, per the PRD's own classification, with the drivers it names - agents build their own tools, graduated ideas become an autonomously executed task list, the system modifies its own code and roster, public thought injection is a designed input channel, and the launch roster is near 100 agents with internal growth and pruning
- Estimated architectural components: roughly 40, across four deployment tiers (engine, runner, inference, off-host relay) plus the browser
- 81 tunable parameters in 5 tables, which the PRD declares is already a build artifact: "This table is also the schema for the substrate config file that lives outside the repository's write path, so it is an artifact architecture builds against rather than documentation overhead."
- 49 acceptance criteria and 9 chaos scenarios inherited as tests
Technical Constraints & Dependencies
Fixed by the PRD and the UX specification, not open to architecture
- Three physically separate hosts, split by trust, all on standard desktop-class x86 hardware. The engine runs the tick loop, event store, triage, leaderboard and fan-out origin; the runner tier is "cheap, wipeable boards" where mind-authored and stranger-authored code executes; the inference host "dials the engine and pulls jobs" and opens no inbound port. None is the owner's personal machine, and none has a route to his personal network. The split is a containment decision and not a step toward cloud: "The blast radius of the worst case is hardware the owner is willing to lose."
- No inbound callbacks anywhere on the public path. Payment confirmation, repository status and inference jobs are all obtained by outbound polling or by the far side dialling in. This shapes the payment integration, CI, and the inference tier identically.
- Secrets never reach the mind. Everything external is called through the broker, which holds the keys.
- Two repositories, both private, and the cage is an absence of keys rather than a protection rule: the mind's bot identity "has no credentials for [the substrate repository] at all".
- Pinned container images from an allowlisted registry. Every tier runs the same architecture, so images are pinned once rather than built for two, and the rule "costs nothing beyond the pinning it already requires".
- One state model, two renderers, DOM first. The UX specification settles the build order and gives the reason: the DOM renderer "is the text view, the permalink, keyboard reachability, screen-reader parity and the fallback layout - five obligations discharged by one build." This inverts the intuitive order. The canvas is the second consumer of a state model already proven by the boring renderer, not the primary one with an accessible view bolted on.
- Apache 2.0, with contributor sign-off collected on the site.
Deferred to architecture, explicitly
| Decision | What the PRD says |
|---|---|
| Engine language | "Chosen in architecture from Python, Go, or Rust", on tick-loop and fan-out performance on standard desktop-class hardware, ease of the containerized runner, and the strength of the LLM and payments tooling |
| Front end framework | "Svelte or React, chosen in architecture", rendering the observatory with Canvas 2D under a 5-second first-view budget |
| Database, web framework, container runtime | "Architecture decides the specific database, web framework, and container runtime. The PRD constrains only behavior and budgets." |
| Wide layout breakpoint | "the width above which the thought panel docks beside the canvas instead of overlaying it" |
| WebGL threshold | "Canvas area above which WebGL replaces Canvas 2D - set from measurement on the wide desktop" |
| Shimmer period floor | "set from measurement at Stage 1 on the tracer bullet's seven bubbles" |
External dependencies, each of which "has a documented behavior when unavailable, and none of them can stop the tick": OpenRouter and a local model server (through the broker), Stripe Payment Links, Auth0, GitHub with branch protection and hosted Actions, Amazon S3, a CDN, an external heartbeat monitor, a tunnel, and the external reviewer's own provider, which sits outside the broker.
Cross-Cutting Concerns Identified
1. Provenance - and it originates in the state model, not in the UI. The requirement that substrate and agent output be distinguishable everywhere is usually read as a presentation rule with long arms. It is the reverse. The PRD builds it into the agent state schema: "the only presentation field an agent writes is its hue and the substrate derives size from standing, shimmer from activation, and seating from the committee record of the idea now being deliberated, so an agent holds no field that could seat it." Not a prohibition an agent might violate - an absence of the field in which it could. The stance is stricter still: it "belongs to the vote record rather than to agent state, and that record is sealed while the ballot is open: it is readable by no agent and served to no client until the voting period closes."
So there are three kinds of state field, and the kind is structural: agent-written, substrate-derived, and sealed-until-a-moment. Everything downstream is a consequence. On the canvas the partition is motion - FR145's "Claims are soft; facts are hard" - and the same partition is then required statically "because motion survives neither a still frame nor a screen reader nor a reduced-motion preference". At the render layer the verification spine makes it a type error: an agent paint context with "no fillText, no font, no drawImage, no handle to the other layers". At the text layer it is one quote() function through which every plain-text emission of agent text passes, where "the closing quote can only ever be emitted by the substrate - the agent cannot close the quote, as a bound parameter cannot close a SQL string." In the accessibility tree every text node carries an origin and "unknown fails". Carried by type in the state model, these become guarantees rather than checks someone has to remember to write.
2. Determinism, and replay as shared infrastructure. Seeded runs, every model call recorded with issue and return times, pinned feature-assessor and embedding model versions, integer-only scoring, rule scores recomputed rather than stored, and events re-emitted at their recorded times "so a committee that resolved raggedly replays raggedly".
The replay engine has three consumers, not one. It is a product feature (FR94, the researcher reading his trace), it is the reproducibility guarantee, and it is the deterministic stimulus for the entire UI suite - "it is not a recorded video: it is recorded telemetry, replayed." That third consumer also solves a staging problem, since a segment for a hundred agents can be synthesised while Stage 1 has only seven. Replay is therefore load-bearing shared infrastructure scheduled in Stage 1, not research tooling.
One caveat travels with it and is recorded verbatim: "A fully green suite is entirely compatible with a mind that has stopped thinking: the fixture replays a recorded stream, so the bubbles will breathe whether or not a tick ever fires." Liveness is held by the healthy-tick metric and the activity floor, never by the harness.
3. The cage. Invariants enforced regardless of agent behaviour, with a test that attempts each forbidden action and expects rejection. Enforced in four different ways - absent credentials, owner-protected repository paths, the broker refusing calls, and container isolation on separate hardware - which means the cage is not one module but a property several components must hold jointly. The verification suite joins it by construction: "a mind that can edit the test proving it is not impersonating the substrate has no such proof."
4. Budget enforcement. Every model call, every tick, every sub-cap, with a breaker at 90% "of the day's ceiling and of each sub-cap, never later", a sealed venture sub-cap, a 2% overshoot tolerance for calls in flight, and a five-rung degradation ladder ending in hibernation. Cost is a first-class input to the cognition loop rather than an operational afterthought, because the number of ideas promoted is derived from what remains.
5. Untrusted input, in more places than the obvious one. Visitor thoughts, yes - but also agent text read by other agents ("a blurb is untrusted input to everyone who reads it exactly as a visitor's thought is untrusted input to the roster"), and every field of a pull request, since "a pull request's title, description, rationale, and scorecard are all written by agents and are all untrusted input to whatever can merge them". Schema validation and score clamping are "load-bearing rather than defensive" because open-weights models follow schemas less reliably.
6. Read-only metrics and features. Agents read and never write, "with write attempts rejected and logged", proven by a test every release. The visitor-reception signals additionally split into targetable legibility signals and readable-but-never-targetable engagement signals, "and the substrate refuses a scorecard that does" name one.
7. Model-family lineage independence, enforced per call. A four-way disjointness constraint between the agent tiers, the outcome judge, the render grader and the external reviewer, whose family "is recorded in substrate configuration so the broker can refuse a call that would breach the rule". The registry's 26 approved families are what make this satisfiable.
8. Two clocks. Derived tick time for everything the mind is measured on, wall-clock for every safety bound, and never a conversion between them. The watchdog measures barrier progress rather than elapsed time "because a sync can legitimately outlast any fixed timeout - swapping a model on the local GPU is work, not a stall."
9. Substrate-owned copy. A frozen string registry no self-change can edit, covering the runway meter, hunger and crowded notices, purchase and tip prompts, and the invitation to suggest fixes. The verification spine wants the reserved-name list generated from it "so a new substrate concept cannot be added without an entry".
10. Schema and contract versioning - five axes, one restore path. The system carries five independently moving version numbers: the event schema version on every event, the agent contract version, the API major version, the named feature vocabulary, and the pinned model versions for the feature assessor and the embeddings. They do not move together, and each has its own migration obligation. FR52 re-validates "every active and archived agent" when the contract version changes, and revived agents are re-validated before returning. Adding, removing or redefining a thought feature "is a contract version change that re-validates every active and archived agent, and it is a cage change." Restore must migrate world state across event schema versions while re-applying redaction tombstones. Because origin is carried by type in the state model (concern 1), the schema version is also what guarantees that partition survives a restore - the two are one seam rather than two concerns.
Starter Template Evaluation
Primary Technology Domain
Not one domain. Human42 is a Go engine that is not primarily a web service
- a barrier-synchronised tick loop with a small HTTP and WebSocket surface
hung off it - plus a browser client that is a hand-written Canvas 2D renderer with a DOM renderer beside it, plus server-rendered public pages, deployed as several processes across three physically separated hosts.
No single starter addresses that shape, and the evaluation below concludes differently for the two halves.
Language and toolchain, decided with the owner: Go, pinned at 1.27.1 (released 2026-09-01). Chosen against the PRD's own criteria once the hardware correction removed the architecture penalty:
- LLM tooling - the owner named this the decider. OpenAI ships an official Go library (
github.com/openai/openai-go, v3); Rust's equivalent (async-openai) is community-maintained. The broker "speaks one OpenAI-compatible API to both tiers", so the surface is narrow either way, but first-party is first-party. - Payments tooling - the PRD's other named criterion. Stripe's official server-side SDKs are Ruby, Python, PHP, Java, Node, Go and .NET. There is no official Rust SDK. The MVP surface is small - hosted Payment Links confirmed by "outbound polling of the processor at least every 30 seconds", no webhooks by design - but again, first-party.
- Fluency - the owner builds in Go today and does not build in Rust. The PRD names "The owner as the sole builder, working with AI coding agents, part-time", which makes the learning curve a real cost rather than a preference.
- Supply-chain surface - the reason Python was excluded. Cargo executes build scripts at compile time, which is how three crates with roughly 245 million downloads between them were poisoned in August 2026;
go buildexecutes no dependency code. This cuts the opposite way from the intuition and makes the PRD's existing rule - "Package installation is limited to an allowlisted registry with pinned versions, because the mind adding a dependency is a supply-chain event" - cheaper to hold in Go than in Rust. - Concurrency -
sync.WaitGroupover goroutines is close to a literal statement of FR134's sync barrier, and the fan-out origin is idiomatic. - What Go does not cost us. Rust's strongest claim here was the type system, since the verification spine wants "a compile error before it is a test failure" for the agent paint context, and cross-cutting concern 1 needs three structurally distinct kinds of state field. Go discharges both with an interface exposing only the permitted methods over an unexported concrete type, and with package-boundary encapsulation - a sealed ballot lives in its own package with no exported accessor until the reveal. The guarantee survives; only its spelling changes.
Starter Options Considered
go-blueprint (github.com/Melkeydev/go-blueprint) - the most widely used Go scaffolder. Maintained, and it does what it advertises: a cmd/api and cmd/web layout, a choice of web framework, a database driver, Docker, GitHub Actions, WebSocket, and optionally React with Tailwind.
Rejected, on three specific conflicts rather than on taste:
- It imposes a web framework on a component that is not a web application. The engine is a tick loop; its HTTP surface is the observatory stream, injection, and a handful of reads. A framework chosen for routing and middleware ergonomics optimises the smallest part of this system.
- Its two-binary layout works against the containment boundary. The three tiers are a security split - "The blast radius of the worst case is hardware The owner is willing to lose" - so process and module boundaries should be drawn from the trust boundaries, not inherited from a scaffold whose defaults know nothing about them.
- Its front-end option contradicts the UX specification outright. That document chose "Custom, and it is the substrate's own vocabulary", and gave the reason: "An established system would supply a colour and type opinion that this spec has spent its length removing." Scaffolding Tailwind would import exactly the opinion the design spent its length removing.
golang-standards/project-layout - widely cited and widely contested, and in any case a directory convention rather than a generator. Its useful parts (cmd/, internal/) are adopted below without taking the rest.
create-vite 9.2.1 (Vite 8.3.0) - evaluated for the browser half and initially accepted, precisely because it scaffolds build tooling and nothing else: no design system, no component library, no colour or type opinion, no routing convention. Superseded in step 4, where the decision to server-render the public pages with SvelteKit made a bare Vite scaffold the wrong base. See Core Architectural Decisions.
Selected Starter
**Backend: none. go mod init, with the layout drawn from the tier topology.**
The rationale is the same one that rejected go-blueprint. Every starter worth using encodes decisions about the shape of the thing being started, and this engine's shape is already specified in unusual detail - four obligations, three trust tiers, a broker that holds every secret, and an event store whose determinism is load-bearing. A scaffold would supply defaults for the small part of this that is a web application and supply nothing for the rest, while quietly making the module layout an inherited accident rather than a statement of where the trust boundaries are.
Initialization:
bash go mod init github.com/<owner>/human42-agents # the mind's repository go mod init github.com/<owner>/human42 # the substrate repository
Two modules because the PRD requires two repositories with different merge authority: the substrate repository the bot identity "has no credentials for ... at all", and the agents repository the mind proposes changes to.
**Front end: the sv CLI, deferred to step 4 and settled there as SvelteKit.**
bash npx sv@0.17.0 create observatory # TypeScript, no add-ons
create-svelte no longer exists; the scaffolder is the standalone sv CLI. This is a heavier starter than create-vite and the difference is honest: it brings a filesystem router and SSR conventions that a bare Vite scaffold does not. Step 4 takes that trade deliberately, because the public pages are server-rendered.
Architectural Decisions Provided by the Starter
**What the sv scaffold settles:** the dev server and HMR, the production build and bundling, TypeScript configuration, the module graph, a filesystem router, and the SSR entry point. It is declined every add-on it offers - no Tailwind, no Drizzle, no Lucia - because the design system is custom, the store is reached through the engine rather than from the browser, and identity is Auth0. Styling, state management, component library and design tokens all remain this project's own.
**What go mod init settles:** nothing beyond the module path, which is the point. Everything else - the database, the HTTP surface, the container runtime, the test layout - is a step-4 decision taken against the requirements rather than inherited from a template.
Deliberately not adopted, with reasons: a web framework (the engine is not one), an ORM or migration tool (the world is event-sourced and migrations run on restore), Tailwind or any design system (the UX specification rejects the category), and a CI template (the check suite splits by determinism across two different runners, which no template knows to do).
Note: Project initialization is the first implementation story, in Stage 0 alongside "the isolated hardware comes online here". The front-end scaffold is not needed until Stage 1's "throwaway observatory with seven breathing bubbles".
Core Architectural Decisions
Decision Priority Analysis
Critical Decisions (Block Implementation)
| Decision | Choice | Verified |
|---|---|---|
| Engine language | Go | 1.27.1, 2026-09-01 |
| Event store | SQLite via modernc.org/sqlite (pure Go) | driver is CGo-free |
| Front-end framework | Svelte | 5.57.1 |
| Public-page rendering | SvelteKit SSR with adapter-node | kit 2.70.3, 2026-09-08 |
| Inter-tier authentication | mTLS with a private CA | Go crypto/tls, no dependency |
Important Decisions (Shape Architecture) - derived from the above rather than chosen separately, and each is marked as derived where it is.
Deferred: container runtime, WebSocket library, deploy mechanism, and observability specifics. Each is named under Still Open with what would settle it.
Data Architecture
**Event store: SQLite, through modernc.org/sqlite.** Four properties of the requirements picked it rather than a preference:
- A snapshot becomes a file plus a hash. The PRD requires "integrity-checked snapshots on a schedule and before every change", copied off-host, and a restore "in under 2 minutes with zero lost events". With one file the artifact is obvious and the hash is over it. With a server-based store, each of those becomes an engineered procedure.
- Single-writer is a match, not a limit. SQLite permits one writer; the tick loop is one writer by construction. WAL mode gives concurrent readers, which is what projections and the query surface need.
- **Pure Go means
CGO_ENABLED=0.** A fully static binary, which is what makes the three-host deployment and the pinned-image rule cheap. The CGo driver (mattn/go-sqlite3) is faster but needs a C toolchain at build time. - FR95 is four indexes. "Query the event log by time, agent, thought, and event type" needs no more than that.
Snapshots are a periodic full base plus shipped event segments (derived, and it resolves a collision). SQLite's natural snapshot is VACUUM INTO, which produces a full copy, while Reliability requires snapshots "incremental and compressed against periodic full bases" and FR31 fires one "before every change". Full-copying a multi-gigabyte file on every self-change is write amplification, not durability.
The append-only log makes the incremental form native without needing a database feature: ship a periodic full base, ship the event segments written since it, and restore is base-plus-tail-replay - the same tail replay the engine already performs on boot. This satisfies the requirement literally, keeps the hash-per-artifact property, and keeps restore inside 2 minutes because the tail is bounded by the base interval rather than by the age of the world.
Projections are materialized in the same file and rebuilt from base plus tail (derived). The leaderboard, agent state, the roster and the ledger are read models over the event log. Keeping them in the same database as the events means one file to snapshot, one hash, one restore, and no possibility of a projection surviving a restore its events did not.
Triage is not stored (contracted). The PRD budgets the log at "under 5,000 events per hour in normal operation" by logging "never full rule-score matrices", so the natural scores a triage sync produces are recomputed on replay rather than written. This is what makes determinism load-bearing rather than merely desirable, and it is why FR15's "no float enters the scoring path" is a storage decision as much as a numeric one.
Migrations run on restore against SQLite's user_version, carrying the event schema version - one of the five axes in cross-cutting concern 10.
Caching: the fan-out diff cache is in-memory in the engine; the crowded state's cached snapshot is served from the CDN. Both already contracted.
A vote has three endings, not two (FR152). internal/ballot/resolve.go returns graduated, abandoned, or answered, and the third is the only one a question can reach: its class carries no threshold, so it enters no backlog and the committee disperses holding each agent's revealed stance rather than snapping to a shared outcome colour. That is a rendering consequence as much as a logic one - FR55's fade "reports what a committee decided and not who was on which side of it", and on a question who was on which side is the answer, so there is nothing for the gathering to agree on as it breaks up.
Authentication & Security
Between tiers: mTLS with a private CA (chosen). Each process - engine, broker, runner, inference - holds a client certificate; the broker authorises by certificate identity and attributes every call to the right sub-cap from who presented what. A shared secret would lean on network position, and network position is exactly what the hardware split exists to stop relying on: "The separation is hardware rather than configuration, which is the whole point of it." Go's standard library does mTLS with no dependency, keeping this off the supply-chain ledger entirely.
This is what lets the runner call the broker without holding what it holds. The render grader "can never run on hosted Actions, which hold no keys", so the runner needs broker access while being precisely where untrusted code executes. A client certificate proves which process is calling without giving it any provider credential.
The 500ms hook ceiling is measured inside the container, with transport excluded (derived, and required for consistency). Hooks execute on the runner tier, now separate hardware reached by mTLS long-poll, so the clock's start point decides whether an agent pays for the substrate's network. It must not: FR140 holds that "an agent whose call is queued behind a busy provider, a cold model, or a model swap on the local GPU has refused nothing... no penalty of any kind attaches to the delay", and FR139 makes an agent that "could not answer through no fault of its own" blocked rather than at fault, paying nothing. A cold container charging an agent 500ms would quarantine an innocent agent after three ticks for something the substrate did. The PRD's existing phrase - "the hook's ceiling is measured against the hook and not against the cycle around it" - meant "not the tick"; it now also means "not the transport". Transport failure is substrate-side blockage, not an agent timeout.
Visitor sessions: an HMAC-signed opaque token, not a JWT (derived). The PRD asks for "a signed session token used only for rate limiting" - it carries no claims, needs no third-party validation, and is purged after 30 days. A JWT would add structure nothing reads and a signature format with more ways to be wrong.
Secrets at rest stay outside both repositories, as contracted, loaded by the broker at start from permission-restricted files on the engine host.
Auth0 for buyers and the owner, contracted, with the owner's single allowlisted identity and MFA.
API & Communication Patterns
The job channel is pull-based HTTP long-poll over mTLS (derived, and the derivation is tight). The inference host "dials the engine and pulls jobs so it opens no inbound port"; the runner "attaches directly and polls for jobs outbound". Both already describe long-poll, mTLS is already chosen, and Go's net/http does both without a dependency. A message broker would add a component that can fail and a port that can listen.
Diff encoding: JSON over the WebSocket with per-message deflate, measured against the budget (derived). The contract is "At most 1 kilobyte average, compressed" with a substrate counter already specified to measure it. JSON first because it is debuggable and the counter will say plainly whether it fits; a compact binary encoding is the documented fallback if the measurement demands it, not a premature choice.
OpenAPI is generated from the engine (contracted, not open): "An OpenAPI description generated from the engine, published with the public pages." Code-first, so the description cannot drift from the handlers.
Error codes, rate limits and versioning are all contracted, with values in the Parameters appendix.
Frontend Architecture
**Svelte 5.57.1, with SvelteKit 2.70.3 and adapter-node for the public pages.** Scaffolded with the sv CLI 0.17.0 - create-svelte no longer exists
- declining every add-on it offers.
Svelte over React on the one axis that binds: an equivalent production app is roughly 47 KB against React's 156 KB, spent directly against "First view of the live observatory, reference phone, 4G - Under 5 seconds", a launch gate. The usual counterweight, React's ecosystem, is close to absent here: the design system is "Custom, and it is the substrate's own vocabulary", there is no component library, the canvas is hand-written, and the text view leans on native details and summary.
SvelteKit makes "one state model, two renderers" literal. The DOM renderer that is the text view, the keyboard path and the screen-reader surface becomes the same code that server-renders the permalink. The UX specification wanted exactly this - "five obligations discharged by one build" - and sharing the components rather than reimplementing them is the difference between that being true and being aspirational.
What actually requires the Node runtime at t0: three routes. Worth recording, because the cost is a production runtime and an npm dependency tree. Of the public pages in the Minimal Launch Set, FR60's about panel and FR61's terms, refund policy and privacy notice are static and prerender at build time; FR128's colophon is "generated from the registry rather than written by hand", which is build-time generation and already has a working generator in tools/build-colophon.py. FR119's self-change page, FR64's incident page and FR56a's idea record all defer past t0.
That leaves FR57 - "Anyone can open a permanent public page for any thought, showing its text, scores, trace, status, attribution, the conversation the agents held on it... designed to be shared" - as the first launch-set page rendering live data server-side. It is still judged worth it, because the alternative duplicates the DOM renderer that FR55a's text view and FR59 already require. On 2026-10-10 the owner added two more reads of the engine through the same channel: FR165's journal of ideas, a page read from the log per request, and FR151's countdown, a small JSON route the observatory polls (/next.json), which the renderer caches for a few seconds so a crowd costs the engine one read. observatory/src/lib/substrate/runtime.ts records the three. The runtime was already paid for by FR57, so neither changes the trade, and it should still be re-examined rather than rediscovered if that balance changes.
The Node runtime is contained four ways. The SSR renderer holds no secrets, has no database access and cannot write - it reads the engine's API over mTLS, so its compromise yields public data that is already public. It is a separate process with its own certificate, so it can be restarted, rebuilt or moved off-host without touching the engine. Everything static prerenders at build time. And npm dependencies are pinned by lockfile under the rule the PRD already states: "Package installation is limited to an allowlisted registry with pinned versions."
State management: Svelte 5 runes over one substrate-owned state model, with the three field kinds of cross-cutting concern 1 - agent-written, substrate-derived, sealed - carried as distinct types rather than conventions.
Infrastructure & Deployment
Tier placement, decided. Inference stays on the RTX 3060 12GB; the runner moves to the second machine. Neither the engine nor the runner needs a GPU - triage is integer arithmetic and the runner executes containers - so the weaker card lands on the tier that will never use it, and the better card keeps the job that is bandwidth-bound and runs "once per thought at high volume". This also keeps Stage 1's measurement honest: the tracer bullet reads "local inference throughput" and the trailing median tick length on the same hardware that will serve them in production, which it would not have done had inference moved to the 8GB card.
The GTX 1070 is not a future inference card and is not being asked to be one: Pascal, 256 GB/s against the 3060's 360, no tensor cores, no accelerated INT8 path, and deprecating in CUDA toolkit 13.x. On the runner tier none of that matters, since the runner executes containers rather than models.
Capacity has a cheap path if it is ever needed. The second host carries a 1000 watt supply and a spare slot, so the Growth-phase option the PRD already names - "Buying a second GPU at home costs a few hundred dollars once, roughly doubles free capacity" - is a card rather than a machine, funded from the venture sub-cap that exists for exactly this and defaults to zero until the owner moves it.
Development runs single-host first, and the gate that matters still holds. The build starts on the development machine alongside powerout.ai. The Month 0 gate binds when an agent acts rather than when code is written, so the substrate, event store and broker can be built and tested there while nothing the mind authors runs at all. The credential adjacency is bounded by a mitigation the PRD already contracts - "Separate provider keys per environment, each with its own hard limit at the provider" - which makes the worst case a capped bill rather than an open one. Single-host operation ends at Stage 0's checkpoint, which no later stage proceeds without.
The runner tier cannot be the development machine once agents act. powerout.ai holds provider credentials for Anthropic, Bedrock and OpenRouter - the providers Human42's broker routes through - and every cost invariant here is enforced at that broker, so credentials reachable outside it are a path around the cap, the sub-caps and the breaker at once. Mind-authored and stranger-authored code runs on the runner by design, which makes that co-location the one arrangement the containment argument forbids: "The blast radius of the worst case is hardware the owner is willing to lose."
Inference co-tenancy is permitted by the PRD's own gloss. The rule reads "serves a fixed model server and nothing else", and the same sentence defines the term: "never agent hooks, never tools, never anything the mind wrote". The exclusion is about code provenance, and powerout.ai is the owner's service rather than the mind's. The owner confirms the full 12GB is available on demand and the co-tenant workload is preemptible. Noted for the segment design: that co-tenant binds *:11434 on all interfaces and authenticates nothing, which is contained by the isolated segment rather than by the service. The pull-only property belongs to Human42's model server, which must not listen.
Static binaries into minimal images (derived). CGO_ENABLED=0 throughout, so engine, broker and runner agent build FROM scratch or distroless, which makes the pinned-image rule nearly free and the one-command-per-tier deploy launch gate materially easier.
Node is not shared. The system Node runs powerout.ai's Next.js dashboard, so Human42's SvelteKit build uses a version manager or a container and never upgrades /usr/bin/node. Human42's units set Environment=NODE_OPTIONS=, copying the defence that dashboard's unit already carries against a broken preload in the ambient environment.
Observability is the substrate's own counters in SQLite plus structured logs, because "All success metrics are computed by the substrate from the event log" and the digest, status page, runway meter, public aggregate and incident page are all "generated from the same substrate numbers, so no two views disagree." A third-party metrics stack would create a second source of truth the PRD forbids.
Still Open
| Decision | What settles it |
|---|---|
| Container runtime, Docker or Podman | The runner tier executes untrusted code, so rootless-by-default is the axis that matters, not familiarity |
| WebSocket library | Measured against the 1 KB compressed diff budget and the 10-per-second rate; verify maintenance status before adopting |
| Deploy mechanism | Whatever makes "one-command deploy to each tier" true across three hosts plus certificate provisioning |
| Off-host fan-out relay | Now a resilience option rather than a capacity requirement; settled by the load test |
Decision Impact Analysis
Implementation sequence
- Private CA and per-tier certificates - Stage 0, because every channel between processes assumes them
- SQLite event store, base-plus-segment snapshots, restore drill - Stage 0, the Month 0 gate depends on it
- Broker as an mTLS service with budget enforcement - Stage 0
- Pull-based job channel to runner and inference - Stage 0
- Tick loop, sync barrier, triage, one full vote - Stage 1
- Replay, which three things depend on - Stage 1
- DOM renderer, then the canvas - Stage 2, in that order
Cross-component dependencies
- mTLS precedes everything. Broker calls, the job channel, and the SSR renderer's read path all authenticate through it, so the CA is the first thing that exists.
- Pure-Go SQLite enables the deployment story. Choosing the CGo driver would reintroduce a build toolchain per image and undo the static-binary simplification that makes three-host deploy cheap.
- SvelteKit couples the permalink to the text view. They are one codebase, so a regression in one is a regression in the other - which is the intended benefit, and also means the text view's tests guard the permalink.
- The event store's single writer is the tick loop. Any component needing to write state writes through it, which keeps the sequencing replay depends on.
- Snapshot cadence sets restore time, but only half of it. Because restore is base plus tail replay, the base interval is the dial that holds assembly inside the 2-minute bound as the world ages. It does not govern verification. FR92 defines the check as "comparing event count and hash before and after", which is a hash over the whole log and therefore linear in the age of the world, not in the tail. Measured in Story 1.5 on go1.27.1: 20k events restore in 313 ms, 100k in 1.48 s, 400k in 3.59 s, 2.5M in 23.3 s, while base-copy-plus-tail-insert stays flat at roughly 6 s. Extrapolated, the bound is reached somewhere around 13 to 16 million events -- of the order of four months of world at the PRD's 5,000 thoughts an hour -- and shortening the base interval does not move that number.
The fix, when it is needed, is a per-artifact digest recorded at snapshot time: the base is already hash-checked twice before it is used, so it can be trusted on that hash and only the tail replay-verified. That is a change to what FR92 means by the comparison, so it is recorded here rather than taken on an implementation story's authority.
Implementation Patterns & Consistency Rules
Pattern Categories Defined
Nineteen conflict points, in two classes. Five are landmines - places where an agent doing the idiomatic thing silently breaks a contracted guarantee, and where the pattern must therefore be machine-checked rather than documented. The rest are ordinary conventions that only need to be written down once.
These patterns have two audiences. The coding agents building this are the obvious one. The other is Human42 itself: FR29 through FR33 mean the mind writes diffs against these rules indefinitely, and its own agents review them. A rule that lives only in prose is a rule the mind can violate and a reviewing agent can rubber-stamp, which is why every landmine below resolves to a compile error, a structural impossibility, a lint, or a test rather than to a paragraph.
Four of the five do. L1 is the exception, and is held by agent review, because the property it needs - whether an iteration's result depends on order - is not decidable. The trade is named here rather than discovered later: L1 is the one rule a diff can break with nothing failing.
The Five Landmines
L1. Go randomises map iteration order. Ranging over a map anywhere that affects state makes replay non-deterministic - silently, intermittently, and usually only under load. It contradicts "Any segment replays from the event log, and rule scores recompute deterministically."
Rule: maps are permitted. Any iteration whose result depends on order must sort first - collect the keys, sort them, range the sorted slice. Order-independent iteration, such as summing values or testing membership, needs no sort. Where a lookup is hot and the order matters, a sorted slice with a binary search remains the better shape. Check: none. This is a rung-5 rule, held by agent review. It is the one landmine that is not machine-checked, and the reason is that the property is not decidable. Collecting keys into a slice and sorting it, and collecting keys into a slice and failing to, are syntactically identical; they differ in what happens afterwards, possibly in another function.
forbidigocannot express it either - arangeis an*ast.RangeStmt, and forbidigo matches only*ast.SelectorExprand*ast.Ident. Order-dependence is intent, which is precisely what rung 5 is for.
L2. A float can enter the scoring path by accident. FR15: "Allocation is integer end to end... so no float enters the scoring path and replay stays exact." One float64 average and exactness is gone.
Rule: scoring uses dedicated integer types, so a float is a compile error rather than a rounding difference.
go type Score int8 // -10..+10: a natural score, or a cast stance type Points int32 // triage allocation, largest-remainder with stable tie-break type Standing int32 // earned only; transferred, never mintedCheck: the types make most of it structural;forbidigobansfloat32andfloat64declarations in the economy package.
The float ban was narrowed from "cognition and economy" to the economy package alone. Floats are for monetary calculations; the scoring path may use them, because thought features run 0 to 1 and value rules multiply weights by them, and no fixed-point representation was ever specified for either. What L2 now protects is money, where integer cents are exact by construction. Replay exactness for natural scores rests on the build being identical rather than on the arithmetic being exact, which Story 2.14 records rather than rediscovers.
L3. Wall-clock in a replayed path, or tick-derived time in a safety bound. Concern 8's two clocks. The first makes replay irreproducible; the second makes a safety bound scale with tick length, which the PRD forbids outright: the hook ceiling is "wall-clock and never scaled by tick length."
Rule:
time.Now()andtime.Since()are called ininternal/store/events.go(stamping a new event) andinternal/cage/deadline.go(arming a wall-clock bound), and nowhere else.time.Sinceis named because the anti-pattern below istime.Since(tickStart), which atime.Now-only ban lets straight through. Every other package receives time as a parameter. Recorded time is data carried on the event and re-emitted by replay; live deadlines are wall-clock, never persisted, and never enter a replayed computation. Check:forbidigowith a two-file allowlist. Not "paths reachable from replay", which no linter can evaluate - the literal file list is what the config takes.
**L4. Agent text reaching output without passing through quote().** VI-3 wants one function for every plain-text emission of agent text, where "the closing quote can only ever be emitted by the substrate - the agent cannot close the quote, as a bound parameter cannot close a SQL string." A single fmt.Sprintf defeats it.
Rule: agent-authored text has its own type with no
String()method and no exported field, so it cannot be interpolated. The only exit isQuote().go type AgentText struct{ raw func() string } // reachable only by calling func Quote(t AgentText) SubstrateLine // the only way out**
rawis a func rather than a string, and that is the guard.** Measured on go1.27.1: with a plain string field vet catches%sand nothing else, and%v,%+vandPrintlnall print the agent's text verbatim --%+vobligingly labels itraw:. Adding a zero-size unprintable field makes vet catch%sand leaves the other three untouched. A verb nobody remembers to vet for is not a guard. Formatting a func prints its address whatever the verb, so the text is reachable only by calling, and calling is whatQuotedoes -- which moves the guarantee from "vet will notice" to "there is nothing to print". Check: a type test assertsAgentTextimplements neitherfmt.Stringernorencoding.TextMarshaler, and the struct carries a zero-size unprintable field so that attempting to print one is a vet failure.That field is required rather than belt-and-braces. Measured on go1.27.1,
struct{ raw string }on its own is not caught by vet: the printf analyser treats a struct as%s-compatible when every field is, and a lone string field is. The L4 anti-pattern below --fmt.Sprintf("%s said: %s", name, text)-- compiled, passed vet, and printed the agent's text. A control struct with anintfield was flagged at the same call site, so the analyser was demonstrably live. Story 1.1 therefore declares_ [0]func(), which also makesAgentTextnon-comparable: two pieces of agent text have no business being compared with==, and the comparison that means anything is between their quoted forms.
L5. A substrate field and an agent field in the same struct. Concern 1's three field kinds. Once adjacent, nothing stops a later edit crossing them.
Rule: enforced by types and packages both, because they catch different failures. Distinct types make a wrong assignment a compile error; a package boundary with no exported accessor makes a wrong read impossible, which is what FR136's sealed ballot needs - "readable by no agent and served to no client until the voting period closes."
`go // internal/roster: the two open kinds, structurally separate type Authored struct{ Hue Hue; Projection *Score } // agent writes type Derived struct{ Size Size; Shimmer Activation; Seat SeatID } // substrate only// internal/ballot: the sealed kind. No exported accessor exists before Reveal. type Sealed struct{ votes []castVote } // unexported, unreachable func (s *Sealed) Reveal(at SubstrateTime) Revealed // the only transition
**Check:** a type test assertsSealedexposes no method returning a vote beforeReveal, mirroring the verification spine'sAgentPaintCtx` test.
Naming Patterns
Three spellings, each idiomatic in its own layer, and the boundary is explicit so no agent argues about it:
| Layer | Convention | Example |
|---|---|---|
| SQLite tables and columns | snake_case, plural tables | events, agent_id, arrival_time |
| Go identifiers | PascalCase exported, camelCase local | NaturalScore, arrivalTime |
| JSON field names | camelCase | naturalScore, arrivalTime, schemaVersion |
| Vocabulary values | unchanged, always | safety_risk, thought.injected |
The last row is what prevents a translation layer. Thought-feature names are values in a versioned vocabulary, not schema fields: "An agent references a feature by name, the validator rejects an unknown name." They are map keys and event-type strings, so they are never case-converted anywhere.
json { "naturalScore": -7, "arrivalTime": "2026-09-21T14:02:11.418Z", "features": { "safety_risk": 0.71, "blast_radius": 0.40 } }
API naming: plural nouns, /v1/thoughts/{id}, /v1/agents/{id}. Path parameters in braces to match the generated OpenAPI. Query parameters camelCase, matching the body.
Event type names: lowercase dotted, domain.pastTense - thought.injected, vote.cast, vote.revealed, idea.graduated, idea.abandoned, ledger.credited, agent.quarantined. Past tense because an event is a thing that happened, not a command.
Svelte: components PascalCase.svelte, stores and utilities camelCase.ts, routes lowercase per SvelteKit's filesystem router.
Structure Patterns
Packages are organised by domain and named after the PRD's own sections, so a requirement points at a directory. This matters most when the mind writes its own diffs and a reviewing agent has to check one against an FR.
cmd/ engine/ broker/ runner/ relay/ internal/ cognition/ sync barrier, triage, promotion, seating, the vote ballot/ sealed votes; the reveal transition lives here alone roster/ agents, lifecycle, standing, pruning, contract validation economy/ ledger, runway, hunger, credits, attribution, transfers registry/ (model, provider) pairs, terms, capability matrix, lineage broker/ routing, budgets, recording, mTLS server store/ event log, projections, snapshots, migrations, replay observatory/ diff origin, fan-out, visitor-reception signals cage/ invariants, protected paths, kill switch, breaker substrate/ owned copy registry, quote(), shared types
Tests are co-located as *_test.go, Go's default. Fixtures live in testdata/ beside the package that uses them, except the shared render and replay fixtures, which are cage items and live where the cage can hold them read-only.
The observatory front end keeps one state model with two renderers, DOM first, as the UX specification requires:
observatory/src/lib/ state/ the one state model; three field kinds mirrored from Go render/dom/ built first: text view, permalink, keyboard path render/canvas/ built second: Canvas 2D, consumes the same state components/ the eight primitives and five components
Format Patterns
Errors are nested; success is direct. The contract is "Standard HTTP status plus a machine-readable code and a human sentence", and the nested shape leaves room to add a field later without breaking clients. It also matches Stripe's error shape, which this project already reads in the payments integration.
json { "error": { "code": "rate_limited", "message": "Six thoughts a minute. Try again in 40 seconds." } }
code is one of the eleven contracted values. The PRD gained internal_error because the original ten had no code for a fault the substrate did not anticipate, which left every 500 either shipping an envelope with no code or borrowing one that lied. The contract now reads: "Every error response carries a code, a 500 included, so a client handles them uniformly rather than special-casing the one shape that has none."
A component that cannot map its error to a contracted code returns internal_error with a sentence and nothing else. It does not invent a twelfth.
Success responses carry the object directly, with no data envelope - the requirements describe none, and inventing one makes every client unwrap.
Times are RFC 3339 with milliseconds, always UTC, always a string, and so is every boundary computed from one -- the cap's calendar month, the day the ceiling and breaker reset on, the day the activity floor measures. Storage and boundaries share one clock, so there is no conversion to get wrong and no daylight-saving transition to handle. Numeric timestamps lose the distinction between "recorded" and "computed" that concern 8 depends on.
Nulls: a field that is absent is absent. Optional values are omitted rather than sent as null, so omitempty is the default and a present null means something deliberate.
Communication Patterns
Every event carries the same envelope, since the log is "retained for the life of the world" and the envelope cannot be changed cheaply later:
json { "schemaVersion": 3, "type": "vote.cast", "occurredAt": "2026-09-21T14:02:11.418Z", "payload": { }}
schemaVersion is per-event, not per-log, so a migration can move events forward individually on restore. occurredAt is recorded time, and replay re-emits against it so "a committee that resolved raggedly replays raggedly."
A vote is one event per voting agent, never a batch, carrying its own arrival time - contracted by FR90 and AC-44, and the reason the reveal can be reconstructed exactly.
Front-end state updates are immutable through Svelte 5 runes. The browser "never computes scores or state. It receives diffs and renders them", so the state model applies diffs and never derives a substrate value locally. A computed value in the client that duplicates a substrate value is an anti-pattern by definition.
Logging Patterns
**Structured, through log/slog** - standard library, so no dependency. JSON handler in production, text in development. Four levels and no custom ones.
**Every engine log line carries tick and sync**, so a log can be lined up against the event log without guessing.
Logs are not a second source of truth. "All success metrics are computed by the substrate from the event log", and the digest, status page, runway meter, public aggregate and incident page are all "generated from the same substrate numbers, so no two views disagree." Nothing derives a metric, a count or a status from a log line. If a number matters, it is an event.
Never logged: secrets of any kind; rate-limiting session identifiers or addresses, which are personal data that is "never displayed" and purged after 30 days; and raw agent text, which goes through Quote() before it reaches a log the way it does before it reaches a page.
Rotation is contracted: "Local logs rotate at 30 days and archive off-host."
Migration Patterns
Two kinds, and conflating them corrupts the log.
Store migrations are DDL against the projections and indexes. NNNN_short_name.sql, monotonic, applied in order, with the applied number in SQLite's user_version.
Event upcasters are per event type and per schemaVersion, and they run on read, never as a rewrite:
go func Upcast(t EventType, from int, p []byte) ([]byte, error)
Events are append-only and snapshots are integrity-hashed, so rewriting a stored event in place would invalidate every hash over it and break the guarantee that a restore reproduces what happened. An old event stays exactly as written and is upcast each time it is read, which is why replay of a five-year-old segment still works after five schema changes.
**A new event type ships with its schemaVersion at 1 and no upcaster. A changed one ships with an incremented version and an upcaster from every prior version**, because the log holds all of them forever.
Process Patterns
Error handling: errors wrap with %w and carry context at each boundary. Only the HTTP layer maps to a contracted code.
Retries are the broker's job and nobody else's. It already "retries with backoff" and enforces per-provider request budgets, so a retry anywhere else double-counts against a quota the broker is holding.
Loading states: the observatory "replays the last completed sync on connect before attaching live, so a visitor never opens on a still frame." There is no skeleton UI - "a partially-arrived field is a smaller field, never a skeleton of a field that is not there."
Refusals always state a reason. Every refusal path returns a contracted code and a sentence; nothing is silently dropped, per FR7 and the UX specification's "Every refusal states a reason."
Enforcement Guidelines
Ranked by maintenance cost, because a rule nobody maintains is a rule that gets a //nolint in eighteen months and nobody remembers why it was there.
cost mechanism example ──────────── ───────── ─────── none, forever 1. the type system Score is int8; AgentText has no String() none, forever 2. structural impossibility a sealed ballot has no accessor before Reveal config only 3. off-the-shelf lint (forbidigo) time.Now() outside its two files ongoing 4. custom analyzer <- avoided none currently required judgement 5. agent review intent, not mechanics; sorted map iteration
Prefer 1 and 2 always. They cannot be suppressed, cannot rot, and cost nothing after they are written once. Rungs 3 through 5 are where the effort goes, so the fewer rules that live there the better - which is why L1 moved from a custom analyzer to a structural rule, and why the remaining lints are config lines rather than code.
Rung 5 is the only judgement call, and that is the point. The mind's own reviewing agents should be checking whether a change is a good idea, not catching what a linter would.
All agents MUST:
- Sort before iterating a map wherever the result depends on the order
- Declare no float in the economy package
- Call
time.Now()ortime.Since()only ininternal/store/events.goorinternal/cage/deadline.go - Pass every emission of agent-authored text through
Quote() - Keep agent-written, substrate-derived and sealed state in separate types and separate packages
- Add a new API error code never - the eleven are the contract
- Write a new event type only with a
schemaVersion, and a changed one only with an upcaster from every prior version
Where The Rules Live
**In the repository, at the root, as AGENTS.md** - not only in this planning artifact. A coding agent opening the repo to write a self-change does not have _bmad-output/planning-artifacts/architecture.md in context, and neither will Human42 when FR29 has it proposing a diff. The format is already the project's convention: the agent contract uses "the markdown-with-frontmatter shape that Claude Code subagents, Kiro, and AGENTS.md have made a convention."
Generated from this section rather than copied, so it cannot drift - which is the failure mode of every hand-maintained CONTRIBUTING file. The lint configuration sits beside it, so the package lists in the rules and the package lists in the config are the same strings.
It is a cage item. FR87 already protects "CI and check definitions" and "the planning documents and tracked-signal list that define success"; a rules file the mind can edit is a rules file the mind can edit its way out of. Readable, not writable, on the verification spine's reasoning: "a mind that can edit the test proving it is not impersonating the substrate has no such proof."
Three things render as diagrams rather than prose, because a rule someone has to parse is a rule someone gets wrong: the enforcement ladder above, the three spellings table, and the three field kinds:
agent-written substrate-derived sealed ──────────── ───────────────── ────── package internal/roster internal/roster internal/ballot type Authored Derived Sealed written by the agent by the substrate by the substrate read by anyone by anyone by nobody, until Reveal() mistake compile error compile error unreachable: no accessor exists
Pattern Examples
Good:
go // Deterministic: sorted slice, integer arithmetic, recorded time as data. for _, a := range roster.Sorted() { // no map to range over s := a.NaturalScore(features) // Score, an int8 alloc = append(alloc, largestRemainder(s, budget)) // Points, an int32 }
go // The only way agent text becomes output. line := substrate.Quote(agent.Rationale())
Anti-patterns:
go for k := range scores { emit(k) } // L1: unsorted iteration, order escapes avg := float64(cents) / float64(n) // L2: float in the MONETARY path (economy only) if time.Since(tickStart) > hookBudget { ... } // L3: time.Now() outside its two files msg := fmt.Sprintf("%s said: %s", name, text) // L4: agent text escaping Quote() type AgentView struct { Hue Hue; Size Size } // L5: two field kinds, one struct
Project Structure & Boundaries
The repository boundary is the cage boundary
Two repositories, because "the cage is an absence of keys" rather than a protection rule that can be misconfigured. What decides which repository a file lives in is one question: may the mind change this without the owner?
The PRD carries residue from an earlier single-repository design - two sentences placing substrate code "in the same repository under owner-protected paths" - against eleven statements for two repositories, including FR32's "The substrate repository is never merged by anything but the owner", both life stages, two journeys and the licence section. Two repositories is the model; the residue should be struck.
That resolves a question the residue was hiding. If the mind's bot holds no credentials for the substrate repository, it cannot open a pull request there, so it cannot propose a cage change directly. The document already answers: "Cage changes and external proposals are pull requests awaiting the owner’s approval", and FR35 queues proposals for the owner rather than executing them. So the mind writes a proposal and the substrate - which does hold credentials - opens the pull request on its behalf. Three identities, not two: the mind's bot (agents repository only), the substrate's bot (merges agents-repository pull requests, opens substrate proposals, commits the digest), and the owner (sole approver on the substrate repository).
The verification spine draws the least obvious half of the line: "In the cage, readable and not editable: the specs, fixture worlds and seeds, the frame-tap schema, baselines, the thresholds file, the substrate-owned-copy registry, the voice gamut definitions, the render grader's configuration and lineage. In the repository the mind edits: the renderer, the layout, the grammar implementation." So the observatory's renderer is the mind's and the observatory's gate is the cage's, and they sit in different repositories on purpose.
Repository 1: human42 (substrate)
Owner-only. The mind's bot identity holds no credentials for it.
human42/ ├── AGENTS.md # generated from the architecture; cage item ├── README.md LICENSE # Apache 2.0 ├── go.mod go.sum Makefile ├── .golangci.yml # forbidigo: floats in economy, time.Now allowlist ├── .github/ │ ├── workflows/checks.yml # hosted, secretless: vet, unit, secret + dep scan │ └── ISSUE_TEMPLATE/ │ └── incident.md # the fixed postmortem template; cage item ├── contract/ # the rules, not the files that conform to them │ ├── schema.json # what an agent file may declare; cage item │ ├── features.json # the 28-feature vocabulary; a change is a cage change │ └── CHANGELOG.md # a version bump re-validates every agent, FR52 ├── prompts/ # substrate-owned call framing; cage items │ ├── vote.tmpl # carries FR19's guarantee; the mind never edits it │ └── review.tmpl # what a reviewing agent is asked, and is not told ├── cmd/ │ ├── engine/main.go # tick loop, store, API, diff origin │ ├── broker/main.go # mTLS service; holds every secret │ ├── runner/main.go # pulls jobs; executes containers │ └── relay/main.go # optional off-host fan-out ├── internal/ │ ├── cognition/ barrier.go triage.go promote.go seat.go vote.go score.go │ ├── ballot/ sealed.go reveal.go resolve.go # three endings, not two │ ├── roster/ state.go contract.go lifecycle.go prune.go correlation.go │ ├── economy/ ledger.go runway.go hunger.go credits.go transfer.go │ ├── registry/ registry.go terms.go capability.go lineage.go ceiling.go fit.go │ ├── broker/ route.go budget.go record.go fallback.go mtls.go │ ├── store/ events.go projections.go snapshot.go replay.go upcast.go │ │ └── migrations/NNNN_*.sql │ ├── observatory/ diff.go fanout.go signals.go │ ├── cage/ invariants.go killswitch.go breaker.go ladder.go deadline.go │ ├── substrate/ copy.go quote.go clock.go ids.go │ └── api/ public/ owner/ openapi.go ├── fixtures/ # cage items, read-only to the mind │ ├── replay/ render/ baselines/ thresholds.yml ├── docs/ # the four documents FR44 names │ ├── contract-reference.md │ ├── contribution-guide.md │ └── governance-pattern.md # "the part that outlives the product" ├── digests/ │ └── YYYY-MM-DD.md # FR101's committed file, written by the substrate bot ├── compliance/ tools/ deploy/ config/ _bmad-output/
Repository 2: human42-agents
The mind proposes; the substrate's bot merges under branch protection. Before the alive declaration the external reviewer approves; after it, the mind's own agents do.
human42-agents/ ├── AGENTS.md # read-only copy of the substrate's ├── README.md LICENSE ├── agents/ │ ├── region/ amygdala.md amygdala.py hippocampus.md ... │ ├── emotion/ fear.md curiosity.md ... │ ├── facet/ self-model.md metacognition.md ... │ ├── archetype/ critic.md builder.md accountant.md ... │ ├── habit-good/ thrift.md patience.md ... │ ├── habit-bad/ sunk-cost.md procrastination.md ... │ └── default-mode/ daydreamer.md wanderer.md ... ├── examples/ # FR44: a habit, a region, a default-mode agent ├── orchestrators/ # the framings the mind may tune │ └── conversation.tmpl scorecard.tmpl └── observatory/ # its own deploy artifact, not an engine build input ├── package.json package-lock.json svelte.config.js vite.config.ts └── src/ ├── lib/state/ # one state model; the three field kinds ├── lib/render/dom/ # built first: text view, permalink, keyboard ├── lib/render/canvas/ # built second: Canvas 2D ├── lib/components/ # eight primitives, five components └── routes/ ├── +page.svelte # observatory SPA, not indexed ├── t/[id]/+page.server.ts # FR57 permalink: the one SSR page ├── about/ terms/ refund/ privacy/ colophon/ # prerendered
Architectural Boundaries
Two cages, one shape. The substrate repository is held by an absence of keys - the mind's bot has no credentials for it, so there is no rule to misconfigure. The agents repository, which the mind does write to, is held by an absence of a runnable artifact: the restart path refuses a commit without the attestations its checks produce. Branch protection is defence in depth rather than the boundary, which matters because the PRD's own argument against protection rules applies to it - a rule can be misconfigured and a path pattern can be forgotten when a file moves.
Repository boundary, as one rule. If a file constrains what the mind may do, the mind does not hold the pen. That is why contract/ sits in the substrate repository rather than beside the agents that conform to it: the feature vocabulary is explicitly a cage change - "Adding, removing, or redefining a feature is a contract version change that re-validates every active and archived agent, and it is a cage change" - and the schema is what rejects an agent file declaring its own size, since "a file that tries to declare any of them is rejected by the validator." A mind that could edit that schema could permit agents to draw their own size, and the partition would fall through a file edit rather than through an argument. An agent file declares contract_version and is validated against the substrate's schema at that version.
The same rule takes two prompt templates out of the mind's hands. An agent has exactly one persona prompt, written by its contributor in the markdown body; what varies is the substrate-owned framing composed around it per call type. Two of those framings carry guarantees rather than tuning. FR19: "an agent is never told that what it writes will be published, the prompt asks it only to say why it voted as it did", because "an agent that learned to write for sharing would be optimising the one thing the mind relies on being true." FR30 holds the review prompt to the same standard, since a reviewer told its rationale is public is a reviewer writing for an audience. After the alive declaration the mind's own agents approve changes in the agents repository, so a words-change adding "your rationale will be published" could pass its own review and the guarantee would be gone without anyone breaking a rule. vote.tmpl and review.tmpl therefore sit in the substrate repository as cage items. conversation.tmpl and scorecard.tmpl carry no equivalent guarantee and stay tunable.
The two repositories never meet at build time. The observatory is its own deploy artifact, built from the agents repository on its own cadence, and the engine serves only the API. They meet at runtime over mTLS, a boundary already designed and tested. This removes a cross-repository commit pairing that nothing was pinning, and it lets an observatory self-change ship without redeploying the engine - the difference between FR123's suggestion loop being routine and being an event.
Tier boundary. Three hosts, four processes plus the SSR renderer, every channel mTLS. The engine never calls the runner; the runner and the inference host dial the engine and pull, which is what keeps them portless. The broker is the only process holding a provider credential and is reachable from all tiers.
browser ──TLS──► relay (optional) ──► engine ──┬── broker ──► OpenRouter observatory (SSR) ────────────┘ ▲ runner ──────────────┤ (pull, mTLS) inference ───────────┘
Authentication boundary - four layers, one of them Auth0.
| Layer | Mechanism | Reach |
|---|---|---|
| Human identity | Auth0; owner allowlisted with MFA, GitHub social login; visitors, by choice, through their own application | internal/api/owner, internal/api/account (FR79a's record, from 2026-10-05), and the credits path after t0 |
| Anonymous visitor | HMAC-signed opaque session token, no claims | internal/api/public, rate limiting only |
| Tier to tier | mTLS, private CA, authorised by certificate identity | engine, broker, runner, inference, SSR renderer |
| API consumer (Growth) | API keys with per-key quotas | metered decision sessions |
The separation is load-bearing twice. Watching must survive an identity outage, so the anonymous path never touches Auth0 - "When Auth0 is unavailable, watching and free injection continue and purchases pause with a notice." And the runner must never hold a bearer credential, because it is where untrusted code executes; a client certificate proves which process is calling without giving it anything it could spend.
At t0 only the owner's half of Auth0 is live, FR103 being in the launch set while FR78 through FR80 defer with the credits block. An Auth0 outage then costs The owner the operator path and nothing else, because FR97 puts the kill switch "directly on the engine host without any third-party login, so no outage of a third-party service can block it."
Data boundary. One SQLite file holds events and projections. The tick loop is the only writer. Nothing outside internal/store opens the database.
Requirements to Structure Mapping
| FR group | Lives in |
|---|---|
| Thought Intake (18) | internal/api/public, internal/cognition/triage.go, internal/cage (moderation, rate limits) |
| Cognition and Workspace (47) | internal/cognition, internal/ballot, internal/conversation (FR141/FR142, the argument before the vote), internal/registry (including fit.go), internal/roster/state.go |
| Self-Modification and Action (11) | internal/selfchange (FR28's task and purse, FR29's scorecard and the tracked-signal list, FR31's snapshot ordering), internal/cage, cmd/runner, internal/broker/route.go (the implementer's lineage and sub-cap), and the agents repository as the target of every diff |
| Outcome Judgment and Learning (7) | internal/economy/transfer.go, internal/broker/route.go (lineage), fixtures/render |
| Roster, Contracts, Contribution (12) | internal/roster, human42/contract (the rules), human42-agents/agents and examples/ (the conformers), docs/ |
| Observatory and Sharing (20) | internal/observatory (origin), human42-agents/observatory (renderer) |
| Decisions and Verdicts (6) | internal/api/owner, internal/cognition/vote.go |
| Economy and Revenue (20) | internal/economy, internal/api/public (credits, tips) |
| Operations, Safety, Persistence (22) | internal/cage, internal/store, internal/replay (FR94's timing, which store/replay.go's sequence walk does not carry), deploy/, digests/ |
Cross-cutting concerns to locations
| Concern | Where it is enforced |
|---|---|
| 1. Provenance, three field kinds | internal/roster/state.go, internal/ballot, internal/substrate/quote.go |
| 2. Determinism and replay | internal/store/replay.go, sorted map iteration (L1, agent review) |
| 3. The cage | internal/cage, the absence of credentials, fixtures/, contract/ |
| 4. Budget enforcement | internal/broker/budget.go, internal/cage/breaker.go |
| 5. Untrusted input | internal/roster/contract.go, injection suite in fixtures/ |
| 6. Read-only metrics | internal/store/projections.go, write-attempt test per release |
| 7. Lineage independence | internal/registry/lineage.go, enforced per call in the broker |
| 8. Two clocks | internal/store/events.go and internal/cage/deadline.go, and nowhere else |
| 9. Substrate-owned copy | internal/substrate/copy.go, a cage item |
| 10. Five version axes | internal/store/upcast.go, contract/CHANGELOG.md, registry pins |
Integration Points
Internal. Engine to broker over mTLS for every model call. Runner and inference dial the engine and long-poll for jobs. The SSR renderer reads the engine's API over mTLS and holds no credential, no database handle and no write path.
External. OpenRouter and the local model server through the broker; Stripe Payment Links confirmed by outbound polling; Auth0; GitHub with hosted Actions for secretless checks and the substrate runner posting status outbound; S3 for snapshots; a CDN for the crowded state; an external heartbeat monitor.
Data flow of one idea. Injected at api/public -> moderated in cage -> features assessed through broker -> cognition/triage scores it each sync -> cognition/promote when the allowance affords it -> cognition/seat draws the committee -> ballot holds the votes sealed -> ballot/reveal opens them at the close -> economy/transfer settles standing at the tick. Every step is an event in store, and every event reaches the page through observatory.
Three artifacts the running system writes rather than a developer. The weekly digest is committed to digests/ by the substrate bot - the mind writes the three lines of prose, the substrate writes the file, and the mind's bot holds no credentials there. Incidents are issues auto-created from .github/ISSUE_TEMPLATE/incident.md, a cage item because a mind that rewrites the form it explains its own failures on has weakened the explanation. The colophon is generated into the observatory's prerendered route from the registry.
Secrets and Runtime Configuration
Outside both repositories, and outside any repository. /prod/pwd/ is not a git working tree, which makes "Secrets and runtime configuration live outside the repository entirely" true by location rather than by .gitignore - the same reasoning as the cage itself, where the protection is an absence rather than a rule that can be misconfigured.
Namespaced per project, and per environment within it.
/prod/pwd/ # mode 0700, owner only human42.openrouter.dev.key # development sub-cap, capped at the provider human42.openrouter.prod.key # production sub-cap, capped at the provider human42.stripe.key human42.auth0.clientsecret human42.s3.key human42.ca.key # private CA signing key; never leaves the host
The environment split is contracted rather than tidy: "Separate provider keys per environment, each with its own hard limit at the provider, so development and production caps are enforced by the broker and again by the provider." One key cannot satisfy it, because the second enforcement point is the provider's own cap and a shared key has one.
Mode 0600, owner only, on every file. A world-readable key undercuts "a broker that never exposes credentials" before the broker is reached, and it matters most during the single-host development phase, when Human42 runs beside another production service on the same box.
Only the broker opens these files. The engine, the runner and the SSR renderer never read /prod/pwd/; they call the broker over mTLS. This is the property that lets the runner execute untrusted code while holding nothing it could spend.
File Organization Patterns
Configuration. Substrate config lives outside both repositories, validated against config/parameters.schema.json - the Parameters appendix rendered as a schema, which is what makes it "an artifact architecture builds against rather than documentation overhead."
Tests. Co-located *_test.go. Shared fixtures in fixtures/, in the substrate repository, read-only to the mind.
Build. Go binaries build static with CGO_ENABLED=0 into FROM scratch images. The observatory builds independently from the agents repository and deploys as its own process with its own certificate. No build step in either repository reads the other.
Development Workflow Integration
Development runs single-host until Stage 0's checkpoint, with the engine, broker and a local runner on one machine and certificates issued by the same private CA that will serve the split tiers.
CI splits by determinism: hosted Actions run vet, unit tests and scanning; the substrate runner runs agent review, the injection suite, the canary, the render check and every timing-sensitive measurement, posting status outbound.
Deploy is one command per tier from Makefile, plus certificate provisioning from deploy/ca, which is what the launch gate requires. The observatory deploys separately, on the agents repository's cadence.
Deploy verifies; it does not trust the repository. FR32's restart path builds only from a commit carrying the attestations its required checks produced, and refuses one carrying none. This is what makes GitHub the gate by construction rather than by configuration: a direct push that bypassed branch protection yields a commit with no attestation, and the restart path declines to run it. Both artifacts are held to it - the engine and, since step 6 made it its own deploy unit on its own cadence, the observatory. The required check names are cage items under FR87's "CI and check definitions", so renaming one is a cage change rather than a silent hole, and the bot identity holds no administrative permission on the agents repository, so it cannot relax the rule it is subject to.
Architecture Validation Results
Coherence Validation
Decision compatibility: verified, not asserted. The dependency chain was checked against the npm registry rather than recalled:
| Package | Pinned | Peer requirement | Satisfied | ||||||
|---|---|---|---|---|---|---|---|---|---|
@sveltejs/kit | 2.70.3 | `vite ^5 \ | \ | ^6 \ | \ | ^7-beta \ | \ | ^8.0.0` | Vite 8.3.0 |
@sveltejs/vite-plugin-svelte | 7.3.0 | vite ^8.0.0, svelte ^5.46.4 | Svelte 5.57.1 | ||||||
@sveltejs/adapter-node | 5.5.7 | @sveltejs/kit ^2.4.0 | 2.70.3 | ||||||
vite | 8.3.0 | @types/node >=22.12.0 | Node 22.22.1 | ||||||
| Go | 1.27.1 | - | pure-Go SQLite keeps CGO_ENABLED=0 |
The choices reinforce rather than merely coexist. Pure-Go SQLite gives static binaries, which give FROM scratch images, which make the pinned-image rule and the one-command-per-tier deploy gate cheap. mTLS plus a broker holding every secret is what lets the runner execute untrusted code holding nothing it could spend. Integer types in the economy package, sorted map iteration, and a two-file time.Now() allowlist are three guards on one property, replay determinism - and only two of the three are machine-checked, which L1 now records rather than implies.
One tension is recorded rather than resolved away: SvelteKit puts a Node runtime and an npm dependency tree in production, in a system that chose Go partly because go build executes no dependency code. It is contained four ways and carried by exactly one requirement, FR57, which is written down so the trade can be re-examined rather than rediscovered.
Pattern consistency: the three spellings have an explicit boundary, and vocabulary values cross it unchanged, which removes the translation layer the camelCase decision would otherwise have created. Package names match the PRD's section names, so a requirement points at a directory.
Structure alignment: the repository split is the cage boundary, enforced by absent credentials rather than a protection rule. A self-contradiction sweep of this document found none.
Requirements Coverage Validation
Functional: all 165 requirements across 9 groups map to locations, 56 cited by number and the rest by group. All 10 cross-cutting concerns have a named enforcement point. The Minimal Launch Set now classifies every MVP requirement: 93 at t0, 62 following, summing to 155 with no overlap.
The pairing layer had a hole and now does not. FR129 fell back "by measured fit" and FR131 protected the measurement from fallback contamination, while nothing defined it and internal/registry held no file that produced it. It does now: fit.go computes fidelity and distinctiveness from the event log alone, and broker/route.go and broker/fallback.go are its only consumers.
Non-functional: 83 gated rows have a home in the CI topology - 12 launch gates, 45 per release, 5 drills, 26 monitored, summing to 88 because five rows carry two classes - split across hosted Actions (deterministic) and the substrate runner (timing-sensitive).
These figures were recounted from the PRD's own rows on 2026-09-21, reading the gate column for a table row and the closing sentence for a bullet. The earlier counts were wrong in both directions and disagreed with each other: Project Context Analysis said "62 gated rows" and this section said "80... 43 per release, 5 drills, 20 monitored". Only the launch-gate figure of 12 survived, and it is the one the CI topology actually turns on. Two ways to miscount were found and are worth naming, because they recur: counting a dual-gated row once rather than once per class, and matching a gate word anywhere in a row rather than in its gate column, which reads Reproducibility's "the observatory's launch gates measure speed and scale" as a launch gate on a row that is per release.
Gap Analysis Results
Four findings, all closed during this step. Two would have silently broken a contracted guarantee, and both were found by tracing requirements into the structure rather than by reading the structure back.
CRITICAL 1 - The vote template carried FR19's guarantee where the mind could edit it. FR19 promises "an agent is never told that what it writes will be published", because "an agent that learned to write for sharing would be optimising the one thing the mind relies on being true." After the alive declaration the mind's own agents approve changes in the agents repository, so a words-change could have retired that guarantee without anyone breaking a rule. Closed: vote.tmpl and review.tmpl moved to human42/prompts/ as cage items; conversation.tmpl and scorecard.tmpl carry no equivalent guarantee and stay tunable.
CRITICAL 2 - The three published worked examples violated the contract they exist to demonstrate. All three declared size: activation and a pulse field, where FR42 permits "presentation defaults that reach the hue channel only" and AC-46 requires "an attempt to set either of the others is rejected by the validator". The files FR44 publishes as the thing a contributor copies would have failed submission. Closed: both fields struck from all three; a test asserting every file in examples/ passes the validator keeps it closed.
IMPORTANT 3 - Repository residue in the PRD. Two sentences still placed substrate code "in the same repository under owner-protected paths", against eleven statements for two repositories. Closed: both replaced with the two-repository model, including the proposal path the mind actually has when it holds no credentials for the substrate repository.
IMPORTANT 4 - Two requirements unclassified, three counts stale. Closed: FR55a to the launch set beside FR59, being the same surface; FR56a to post-t0, because the thing it exists to show is a later verdict paying an earlier dissenter and that payment is FR143's zero-sum attribution, which defers with agent economics. At t0 an idea has had one committee, so its life and its verdict are the same page and FR57's permalink carries it. Counts refreshed and re-verified after FR152 landed: 88 of 147 with 59 following, read from the table's requirements column rather than from prose. Refreshed again after FR153 and FR154, then after FR155 and FR156, then FR157 and FR158, then FR159 and FR160: 93 of 155 with 62 following.
Architecture Completeness Checklist
Requirements Analysis
- [x] Project context analysed; 163 FRs and 83 gated NFR rows counted mechanically
- [x] Scale and complexity assessed; roughly 40 components across four tiers
- [x] Technical constraints identified, including two the PRD had wrong
- [x] Ten cross-cutting concerns mapped to enforcement points
Architectural Decisions
- [x] Five critical decisions with registry-verified versions
- [x] Technology stack specified and compatibility proven
- [x] Integration patterns defined; four deferred with what settles them
- [x] Performance addressed: triage bound, diff budget, first-view budget
Implementation Patterns
- [x] Five landmines resolved to compile errors, structure, or config lints
- [x] Naming, structure, communication, logging, migration and process patterns defined
- [x] Enforcement ranked by maintenance cost
- [x] Rules given a home in the repository as a cage item
Project Structure
- [x] Two complete trees, not placeholders
- [x] Repository, tier, authentication and data boundaries defined
- [x] All 9 FR groups mapped to locations
- [x] Guarantee-carrying prompt templates held in the cage
Architecture Readiness Assessment
Overall status: READY FOR IMPLEMENTATION. No open findings.
Confidence: high. Every version was verified against a registry rather than recalled, every contracted guarantee traced to an enforcement point, and the document swept for self-contradiction and found clean. Four PRD defects were found and fixed across this workflow - the engine host's architecture, the missing internal error code, the repository residue, and the launch-set classification - none of which a reading pass had caught.
Key strengths
- Determinism is guarded three independent ways, so the property everything else rests on is the hardest to break by accident
- The cage is enforced by absent credentials rather than a rule that can be misconfigured
- Enforcement is ranked by maintenance cost, so the rules most likely to survive are the ones that cost nothing to keep
- Where a guarantee lives in a file, that file is in the cage - which is how two prompt templates ended up somewhere the mind cannot reach
Areas for future enhancement
- Container runtime, WebSocket library and deploy mechanism remain open, each with the evidence that would settle it
- The off-host relay is a resilience option awaiting the load test
- The Node-in-production trade is carried by one requirement and should be re-examined if that changes
Implementation Handoff
AI agent guidelines
- Follow the architectural decisions as documented; where one is marked derived, the derivation is the reason and changing the premise changes the decision
- Use the implementation patterns consistently; the five landmines are compile errors, structural impossibilities or lints rather than advice
- Respect the repository boundary: if a file constrains what the mind may do, the mind does not hold the pen
- Refer to
AGENTS.mdat the repository root, generated from this document
First implementation priority, from Stage 0:
bash go mod init github.com/<owner>/human42
Then in order: the private CA and per-tier certificates, because every channel assumes them; the SQLite event store with base-plus-segment snapshots and a passing restore drill; the broker as an mTLS service with budget enforcement; the pull-based job channel. Nothing proceeds past Stage 0's checkpoint - a restore drill passing on a toy world, the breaker tripping on a fake budget, a verified snapshot copy to S3, and a runner container that can reach nothing but the job channel and the broker it was handed.