Skip to content
GRPNR.

00 — SYNTHESIS: Stack architecture for the four factory apps

AMENDED 2026-07-19 (read first): after a second research + steelman pass on Robert’s “why not Postgres for everything” challenge, 05-postgres-only-comparison.md supersedes this doc on the data layer: FleetOps hook events move from SQLite into the shared Postgres (insert-only partitioned fleetmon schema, partition-drop retention); SQLite and the frser Grafana plugin exit the design. Amendment 2 (Robert’s call, same day, 05 §5b): VictoriaMetrics dropped too — the Go binary gains a ~150–400 LOC OTLP receiver writing metrics into PG; Grafana deferred (Board renders the panels). Final: ONE database (Postgres). Always-on ≈250–450MB. Languages, products, and everything else below stand.

2026-07-19. Written by Fable (orchestrator/judge) after the full oh-architecture pass: 10-packet phase-1 research workflow (11 agents, ~861k tokens), 4-candidate steelman court (12 agents, ~556k tokens), 4 writer agents. Evidence: ./evidence/. Where this doc revises a prior plan’s ADR, it says so explicitly — with the prior plan’s own escape hatch cited. Readable standalone in 5 minutes.

The decision

One polyglot-by-plane stack, chosen per plane, not per fashion:

Product Language / runtime Database Web surface Always-on?
WorkLedger TypeScript / Encore.ts (pinned by plan 002 ADR D5 — unchanged) Postgres (shared local instance, tuned; SKIP LOCKED claim path needs real row-level locking [E11]) Grafana panels (PG datasource) + views read by the Board Yes — as built binary (encore build), never the encore run dev daemon
Atlas (answer loop) TypeScript / Encore.ts (pinned by plan 003 — unchanged) Postgres (same shared instance; pg_trgm) Renderings (docs/atlas/) + capped digest rows via FleetOps Yes — same built binary/process family as WorkLedger
Parity Harness TypeScript workspace + CLI (pinned by its own architecture.md:27 — unchanged); mitmproxy stays a quarantined Python process; Bun runtime allowed for the CLI Content-addressed file store (fixtures) + verdict rows written into WorkLedger’s Postgres None (evidence bundles + one Grafana panel, per its ADR) No — batch/episodic. The footprint constraint doesn’t bite here; that’s why TS stays.
FleetOps (monitor) Go — ONE static binary (ingest + SSE board + pollers + digest in-process); existing Rust fleetops TUI and Go ghmonitor stay as-is, consumed via their JSON seams SQLite WAL (hook events, watch items — single writer goroutine) The Board is the program’s web shell: server-rendered htmx/Datastar + SSE, go:embed assets, links out to Grafana Yes — ~15–40MB
Observability VictoriaMetrics single-node (native OTLP ingest — Claude Code exports straight to it, no OTel Collector); no Loki — events live in FleetOps SQLite ONE shared Grafana (dashboard 25255 + PG + SQLite datasources), linked from the Board, never embedded Yes — vmsingle + Grafana

Sequencing follows candidate D’s instinct inside candidate C’s architecture: ship the Go binary as ingest-only + Grafana + existing TUI first (days), add the Board SSE page second. This also preserves plan 004’s own F5+F6-first sequence.

Always-on budget (the number Robert asked for):

Process RSS budget Note
Postgres (one shared, tuned shared_buffers 128–256MB) ~130–150MB serves ledger + atlas [E11]
Encore built binary (ledger + atlas) ~100–150MB (UNVERIFIED — measure at walking-skeleton time, V1 below) Encore publishes no RSS numbers [E10]
VictoriaMetrics single-node ~50–100MB tiny series count; native OTLP [E5]
Grafana (one instance, enforced) ~180–330MB the single most expensive tool we keep
FleetOps Go binary ~15–40MB GOMEMLIMIT set explicitly
Total ≈475–770MB vs ~2.3–2.4GB as-previously-documented — ~70% cut, dominated by dropping Loki (~1.5GB idle) and the OTel Collector

The fleet itself (hundreds of Claude Code processes at ~156MB measured average [E1]) costs tens of GB — the tooling is now noise against it, which is the point.

Decision matrix (weights stated, scores 1–5, justified by court + evidence)

Criteria weights: footprint is Robert’s stated hard constraint (25%); AI agents are the primary developers, so agentic maintainability is second (20%); the rest per the constitution. Sensitivity-checked below.

Criterion Wt A all-TS B Rust C Go D do-less
Always-on footprint 25% 4 5 5 5
Agentic maintainability 20% 4 2 5 4
Simplicity / ops surface 15% 4 3 4 5
Speed to ship 15% 4 2 4 5
Robustness at fleet scale 15% 4 4 5 2
Coherence & evolution 10% 4 3 4 3
Weighted 4.00 3.30 4.60 4.15

Score justifications (compressed; full arguments in 04-steelman.md):

  • B’s agentic 2: Rust is the only benchmarked language with real failures (38/40), repo-level real-issue resolution 21–28% vs ~70% Python-tier, fixes touch 9.8 files/139.9 LOC avg [E7]; under the Haiku-first routing rule this also means silent model-tier escalation cost. Its unique advantage (in-process reuse of the 6.4k-LOC fleetops crate) rests on an unverified premise — nobody has confirmed fleetops is a lib crate with an async-safe API (court residual).
  • C’s robustness 5: SQLite’s multi-writer cliff is answered structurally — pollers fan into a channel, ONE writer goroutine owns the handle; that preserves the single-writer-process envelope (8.4k writes/s real-world [E6]) at any poller count. Driver: modernc.org/sqlite (pure Go — no CGO thread growth; its ~2× write penalty still yields ~400k inserts/s in benchmarks, orders above need).
  • D’s robustness 2 — the disqualifier: daemon-less ingest turns every hook event into an independent OS process contending on one SQLite file, and Claude Code hooks execute synchronously — ingest contention becomes agent tool-call latency at exactly peak fleet load. The court judged this the strongest single attack of the whole proceeding. D’s saving over C is only ~50–100MB.
  • A vs C: the court settled that the maintainability evidence establishes TS-over-Rust, not TS-over-Go. Go matches A on every coherence argument that survives (both are one new process; both toolchains already installed), beats it on footprint floor and runtime robustness (goroutines vs one event loop; no Bun SSE idle-timeout gotcha), and carries the best agentic grade among low-footprint languages (fast compile loop = no cargo jobserver-style core grab against the running fleet [E7][E9]).

Sensitivity: C’s win survives every plausible weight shift. Raising footprint to 35% doesn’t reorder (B/C/D all score 5). D overtakes C only if robustness drops below ~5% weight — i.e., only if the ingest-latency coupling is proven harmless, which is exactly what it isn’t. Discounting the Rust repo-level study (single source) lifts B to ~3.5 — still last. A never leads under any tested shift.

Winner architecture

flowchart LR
  subgraph FLEET["Claude Code fleet (hundreds of agents)"]
    CC["agent sessions"]
  end

  CC -- "OTLP metrics, direct (no collector)" --> VM[("VictoriaMetrics\nsingle-node")]
  CC -- "hook events (stripped) HTTP" --> GOB

  subgraph GOB["FleetOps — ONE Go binary (~15-40MB)"]
    ING["ingest handler"] --> CH["channel"] --> WR["single writer goroutine"]
    PL["pollers (goroutines):\nfleet snapshot · ghmonitor · groupon2 REST"] --> CH
    BRD["Board: htmx/Datastar + SSE\n(go:embed, the web shell)"]
    DIG["digest cron (in-process)"]
  end
  WR --> SQ[("SQLite WAL")]
  SQ --> BRD
  FO["fleetops TUI (Rust, as-is)"] -. "fleet snapshot JSON" .-> PL
  GH["ghmonitor (Go, as-is)"] -. "JSON" .-> PL

  subgraph ENC["Encore built binary (TS)"]
    WL["WorkLedger svc"]
    AT["Atlas svc"]
  end
  WL --> PG[("Postgres\n(one shared, tuned)")]
  AT --> PG

  HAR["Parity Harness (TS CLI, batch)\n+ mitmproxy (Python, quarantined)"] -- "verdict rows / block()" --> WL
  HAR --> FS[("content-addressed\nfixture files")]

  VM --> GRAF["Grafana (ONE instance)\n25255 + PG + SQLite datasources"]
  PG -. "read-only views" .-> GRAF
  PG -. "read-only views" .-> BRD
  BRD -. "links, never embeds" .-> GRAF

Top-3 reasons

  1. The only genuinely open slot gets the best-fit language. WorkLedger/Atlas (Encore.ts) and Harness (TS CLI) were already pinned by their own ADRs; the research confirmed those pins are sound and cheap (harness is batch — footprint irrelevant; ledger/atlas ride the substrate). FleetOps was open, and Go wins it on the joint evidence: near-Rust footprint, best agentic iteration loop, structural burst-robustness, and precedent (PocketBase/Beszel/ghmonitor are this exact shape).
  2. The observability cut is the real footprint win — not the language choice. Dropping the OTel Collector (Claude Code exports OTLP per-signal natively) and Loki (~1.5GB idle for content-free events that already land in SQLite), and swapping Prometheus for vmsingle, takes the always-on base from ~2.3–2.4GB to under ~800MB. No candidate disagreement here; the court was unanimous.
  3. Hundreds-of-agents robustness is designed, not assumed: single-writer-goroutine ingest, GOMEMLIMIT, loopback-only binding, poll intervals as calibration knobs — and the two genuinely unknown numbers (hook burst rate, Encore binary RSS) are named validation spikes, not hopes.

Top-3 risks (winner’s prosecution = risk register; full list in 04)

  1. No Go data exists at the repo-level maintenance depth where Rust collapsed (21–28%). Go’s A- grade is directional consensus, not a measured repo-level number. Tripwire: track agent PR failure rate on FleetOps in the ledger from week 1; if Haiku/Sonnet agents can’t hold the Go binary, the A-grade was wrong and A (all-TS) is the fallback (board is one small process behind a stable HTTP/SSE contract — swap cost is days).
  2. Encore built-binary RSS is unpublished [E10] — the budget table’s ~100–150MB line is an assumption in an exercise that is entirely about RSS. Spike V1 measures it at ledger walking-skeleton time; if it lands fat (>400MB), revisit ledger/atlas packaging (A-lite standalone repo fallback per plan 002 D5 — same code, different deploy target).
  3. Real hook-event burst shape is unmeasured at hundreds-of-agents scale — nobody anywhere has published it [E9]. The ingest envelope (8.4k writes/s) has ~40× headroom over any modeled rate, but the model is a model. Spike V2 instruments the next real wave and reads events/sec before the Board ships.

Supersessions this decision makes (each uses the prior plan’s own escape hatch)

Prior decision Status Why
Plan 004 ADR-9: reinstate Loki SUPERSEDED — no Loki ~1.5GB idle RSS [E5] for content-free events already in SQLite; ADR-9’s own reversibility note (“one more compose service, additive”) cuts both ways. Grafana SQLite datasource + Board search cover the read path. VictoriaLogs (0.6–2GiB at 500GB/7d scale) is the named upgrade if log search ever demands a real store.
Plan 004 §2.5 / component table: OTel Collector + Prometheus SUPERSEDED — vmsingle, direct OTLP Collector is provably optional (per-signal endpoint override documented); VM is listed as a supported backend by dashboard 25255 itself [E5]. Config change, not architecture change.
Plan 004 ADR-6 (SQLite ingest), ADR-2 (web board), ADR-5 (Grafana linked) CONFIRMED Unchanged; board language now decided (Go) — ADR-2 said “UI framework choice, data model unaffected”.
Plan 004 §7 compose layout (ingest as compose service, pollers as host cron) REVISED Pollers+digest move into the one Go binary (in-process timers) — fewer moving parts than cron + daemon split.
Plans 002/003 stack pins (Encore.ts+Drizzle+PG), harness TS ADR CONFIRMED Re-examined under the new constraint; survive it (batch/substrate reasoning above). One addition: always-on = built binary, never encore run.

Validation spikes (before/at build start — all cheap)

  • V1 (at ledger walking-skeleton): measure Encore built-binary RSS idle + under claim load. Gate: <400MB or packaging revisited.
  • V2 (next real wave): instrument hook events/sec + burst shape via a 20-line stub ingest. Feeds the ingest golden test’s load numbers.
  • V3 (day 1 of Go build): modernc.org/sqlite single-writer-goroutine throughput golden test — burst 1k events, assert no SQLITE_BUSY, p99 write <5ms.
  • V4: fleet snapshot subprocess cost at current session count; set default poll interval (start 5s, calibration knob).

First implementation steps (test-first, inside plan 004’s 2–4 day envelope)

  1. RED: Go repo fleetmon/ — failing test: POST /ingest with a stripped hook JSON → row in SQLite via the writer channel; concurrent-burst test (V3) alongside.
  2. GREEN: one main.go + ingest.go + store.go (modernc, WAL, busy_timeout, GOMEMLIMIT); go:embed empty shell page.
  3. Claude Code hook config → POST to :PORT/ingest; OTLP env → vmsingle; import dashboard 25255 into the ONE Grafana; wire PG datasource to ledger views (when ledger exists).
  4. Board SSE page (htmx/Datastar): queue derivation golden test first (given fixture events → expected attention queue), then render.
  5. Pollers as goroutines behind the channel (fleet snapshot, groupon2 REST) — contract-tolerant parsing tests (fields may drift, per plan 004 risk 5).

Status: draft. Flip to active on Robert’s acceptance. Full reasoning: 01 (constraints + footprint evidence), 02 (case studies + agentic-fit), 03 (candidates + diagrams), 04 (steelman court), evidence/ (raw packets).