Skip to content
GRPNR.

10 - Atlas Architecture: the answer loop on the Zaruba substrate

Superseded decisions (2026-07-19, post-audit PRD ruling — read first). Four points below are OVERRIDDEN by 12-prd.md:

  1. Write-back destination is the internal-plane factory lane docs/atlas/ (12-prd FR-19), NOT core-plane _documentation/ as §3.6, the context table, the decision table and the MVP list state. _documentation/ inclusion is a future ask, not v1.
  2. Atlas sends no GChat digest of its own. Fleet monitor (plan 004) owns the send channel; Atlas only contributes capped rows (12-prd FR-25). §3.7’s daily send is dropped.
  3. The fan-out fallback described in 12-prd appendix #4 (complete every attached duplicate task) is incoherent with FR-7 (no per-duplicate Core tasks are ever minted). Respec of the fallback is an explicit deliverable of the VA-6 resume spike (14-validation-plan), before any build.
  4. The MVP list in this doc is superseded by the phased, gated cutline in 00-SYNTHESIS.md (write-back worker and digest rows build only after their gate numbers are measured).

2026-07-18. Architecture for Atlas built as a loop, not a layer, on Tomas Zaruba’s greenfield monorepo (monorepo-development). This document takes the 01-initial-recommendation.md thesis as the frame under test and grounds every component in the real substrate code (file paths cited). It obeys the hard constraints: zero PRs to Core services in v1, internal plane only, Postgres first, deterministic where determinism is safer, everything on encore run until the billing gate clears.

Passport convention (from reasons-and-decisions/08-communication-standards.md, applied throughout): every number carries MEASURED, MODELED, DARK, or DESIGN. No em-dashes. Receipts, not adjectives.

Verdict up front: the initial recommendation survives contact with the code. Atlas is one system (an answer ledger with a resume seam) plus three renderings of it. The substrate already contains every seam Atlas needs. The single most consequential design choice is that Atlas, not Core’s tasks service, owns the fan-out from one human answer to N blocked askers, because dedup collapses N asks into one Core task and Core only knows the one.


1. Principles

  1. One system, three renderings. The system is the answer ledger in Atlas Postgres. GLOSSARY.md, generated answer docs, and the GChat digest are renderings of that ledger, generated and never hand-maintained. If a rendering rots, you regenerate it. You never edit it by hand.
  2. Zero PRs to Core in v1. Atlas is a new internal-plane service. It reaches Core only through the two seams the substrate already exposes to the internal plane: mint an INFORMATION_REQUEST task (tasks._taskCreateFromWorkflow) and list tasks (tasks.tasksList). No change to any file Tomas owns. Config, service registration, and Atlas’s own code only.
  3. Deterministic core, LLM at the edges with provenance. Routing, rate-limits, expiry, exact-key dedup, and digest rendering are deterministic code. The LLM appears in exactly two places, both human-visible: dedup similarity assist (post-MVP) and artifact drafting (a draft a human approves before merge). No LLM decides who to interrupt, what is a duplicate for real, or what lands in a doc unreviewed.
  4. Atlas owns fan-out, Core owns the human surface. Because dedup means N askers share one canonical question and therefore one Core task, Core’s single-stamp signal cannot resume all N. Atlas holds the attachment list and fans one answer out to all attached askers. Core stays the one inbox and one completion mechanism, unchanged.
  5. Postgres first, no new infra without evidence. Dedup similarity is pg_trgm, a native Postgres extension, not a vector database. Search over answers is a Postgres query, not an embedding index, until a MEASURED miss-rate says otherwise. LightRAG (already running for the operator) is the post-MVP retrieval backend, adopted only when substring plus trigram provably falls short.
  6. Link, never duplicate, sibling state. Work-ledger owns unit disposition, evidence, and cost. Atlas carries a unitRef and links out. Atlas never stores what a sibling owns. The same rule binds Atlas to the existing docs pipeline and the existing decision-log convention: Atlas feeds them, it does not build a second one.
  7. Supersede, never edit. Answers are append-only with a supersedesAnswerId chain, matching the repo’s own decision-log discipline (reasons-and-decisions/07-decision-log.md is append-only). An answer is never edited in place, so provenance is always intact and a reader can always see what changed and when.

2. Context and trust boundaries

2.1 What is inside Atlas, what stays Zaruba’s, what stays git or GChat

Concern Owner Why
Raw questions, canonical questions, answers, attachments, artifact links Atlas (internal plane, own Postgres) The compounding asset. New records, no sibling owns them.
Dedup, routing, rate-limit, expiry, fan-out Atlas Deterministic logic, Atlas’s reason to exist.
The one human inbox and one completion mechanism Zaruba tasks service (Core) services/tasks/ already is “one store, one inbox, one completion mechanism” (tasks.interfaces.ts:1-7). Atlas does not rebuild it.
Group queue membership Zaruba user service (Core) user_groups / user_group_memberships live in services/user/. The agent-questions slug is just a string in tasks; real membership is a config act in user.
Blocked-workflow signal resume Zaruba workflow-management (Core) + workflow-proxy (internal) The _workflowSignalSend seam (expose:false) already exists and is already reached from the internal plane. Atlas reuses it, does not build it.
The rendered corpus (GLOSSARY.md, generated answer docs) git via _documentation/ and the existing docs pipeline _documentation/ syncs into the docs service (scripts/docs-sync.mjs) and is searchable by the existing docs MCP tool. Atlas writes files here, the existing pipeline indexes them.
The daily digest GChat via a webhook A rendering, transient, never the source of truth.
The binding org decision log Zaruba (reasons-and-decisions/, confidential) Atlas never writes here. Atlas’s refactor Q&A decisions render into _documentation/, a separate surface, to avoid a competing knowledge system.

2.2 Trust boundaries

Three boundaries matter.

  1. Agent to Atlas. Agents are semi-trusted. They author question text that may quote legacy source, which may carry secrets or PII. Atlas classifies and redacts at ingest. Question text is never trusted as instructions by any downstream consumer.
  2. Atlas to Core (cross-plane). Structural, not a convention (root CLAUDE.md, “Core / internal boundary”). Internal reads Core through generated clients, writes only through Core’s own endpoints. Atlas adds a tasks namespace to its own core-client adapter contract (Atlas’s file, zero Core change), exactly as workflow-proxy did for workflow_management (services/workflow-proxy/adapters/core-client.adapter.ts:36-58).
  3. Human answer to agents (the poison surface). The one answer a human gives is fanned out to N agents and rendered into docs that future agents read as trusted context. Answers come only from trusted humans (completion ACL is assignee, group member, or ADMIN: taskComplete.controller.ts). Machine-drafted artifacts are untrusted until a human approves the diff.
graph TB
  subgraph fleet["Agent fleet (Claude Code, 64 concurrent ceiling MEASURED)"]
    A1["asking agent"]
    A2["reading agent"]
  end

  subgraph atlas["Atlas (internal plane, NEW, own Postgres)"]
    TS["triage service"]
    DD["dedup module (pg_trgm)"]
    WB["write-back worker"]
    DG["digest cron"]
    DB[("Atlas Postgres")]
  end

  subgraph core["Zaruba Core plane (UNCHANGED)"]
    TASK["tasks service"]
    USER["user service (groups)"]
    WFM["workflow-management"]
    DOCS["docs service + MCP search"]
  end

  subgraph edges["Rendered surfaces"]
    GIT["git: _documentation/ GLOSSARY.md"]
    GCHAT["Google Chat digest"]
  end

  HUMAN["Tomas / Robert (answerers)"]

  A1 -->|ask_question, search_answers| TS
  A2 -->|search_answers| TS
  TS --> DD
  TS --> DB
  TS -->|mint INFORMATION_REQUEST| TASK
  TASK -->|assigneeGroupSlug agent-questions| USER
  HUMAN -->|answer once in existing admin UI| TASK
  DG -->|reconcile completed tasks| TASK
  TASK -.->|optional signal for stamped askers| WFM
  WFM -.->|resume| A1
  WB --> GIT
  GIT --> DOCS
  DG --> GCHAT
  WB --> DB
  DG --> DB

3. Components

Each component states responsibility, inputs, outputs, failure behavior, and a build-vs-buy call.

3.1 Triage service (Encore internal plane, own Postgres, Drizzle)

  • Responsibility. Accept a question, classify and redact it, run dedup, attach to a canonical question or create a new one, and on a new canonical mint exactly one Core task. Serve search_answers. This is the front door.
  • Template. Copy the github internal service, which already is a full internal-plane service with its own Postgres, Drizzle schema, and repositories (apps/backend-encore-internal-ts/services/github/{db,schemas,repositories}). Atlas is that shape.
  • Layering (binding, .claude/rules/encore-ts.md). Controller validates input and authorizes, service holds the loop logic, repository extends BaseRepository and does data access only. Never collapse a layer.
  • Inputs. ask_question(rawText, askerRef, unitRef?, workflowAttachment?, classificationHint?); search_answers(query, scopeHint?).
  • Outputs. ask_question returns { questionId, canonicalId, state, existingAnswer? }. If dedup lands on an already-ANSWERED canonical, the answer returns immediately and no task is minted, the invisible success path. search_answers returns ranked answers with provenance.
  • Failure behavior. Core unreachable at mint time: the question is persisted OPEN with coreTaskId=null and a retry cron mints it later. The asker still gets a questionId to poll. Never lose a question because Core blinked. Dedup lookup failure degrades to “create new canonical” (a duplicate is cheaper than a dropped question).
  • Build vs buy. BUILD, thin. No OSS candidate is this loop (R7: “No candidate is a drop-in question loop plus knowledge write-back product”). The service is a few hundred lines on a proven in-repo template.

3.2 Dedup module

  • Responsibility. Decide whether a new question is the same as an open or answered one. Two deterministic lines, then an optional assist.
    1. Normalized-key exact match. Lowercase, strip punctuation, collapse whitespace, drop stopwords, extract entity tokens (service name, file path, symbol). Equal key means auto-attach. This catches the highest-volume duplicate class named in R2: “is this code alive / who owns this” over EOL estates (VIS frozen ~2013, orders ~2017, MODELED), which arrive in near-identical phrasing.
    2. Trigram similarity. pg_trgm similarity() over normalized text, GIN-indexed, native Postgres. Above a high threshold (DESIGN start 0.75, tuned on real data) suggest a merge, confirmed by the searching agent or a human. Never silently merge on similarity alone: R2 is explicit that dedup precision at fleet scale is “genuinely unsolved in public literature,” so v1 auto-merges only on exact key and human-or-agent-confirms the fuzzy tier.
  • Inputs. Normalized question text and key.
  • Outputs. { decision: attach|new, canonicalId?, confidence, matchedOn }.
  • Failure behavior. On any doubt, create new. A missed merge costs one extra routed question. A wrong merge silently feeds N agents a wrong answer, which is the worst outcome, so the module is biased hard toward false-new over false-merge.
  • Build vs buy. BUILD deterministic core on pg_trgm (Postgres first, zero new dependency). Embedding assist is POSTPONE: adopt LightRAG (already running, R7 ~40% of the storage half) as the similarity backend only when a MEASURED exact-plus-trigram miss-rate justifies the machinery. The normalize-and-match function is a pure function with one runnable assert test.
  • Responsibility. Give agents a stable way to ask and to search before asking.
  • Decision. Do NOT add tools to Core’s MCP registry, that is a PR to a Core service (services/mcp/tools/_tool_registry.ts) and breaks zero-PR. Instead Atlas exposes two typed internal-plane HTTP endpoints (POST /atlas/questions, GET /atlas/answers/search) and ships a Claude Code skill wrapping them. Skills are already an in-repo convention (.claude/skills/). get_glossary is not a new tool: agents read GLOSSARY.md from git or via the existing docs search, so building a third tool for it is skipped.
  • Inputs / outputs. As in 3.1.
  • Failure behavior. Endpoint down: the agent’s skill surfaces a typed error and the agent parks and retries. No question is invented on the agent side.
  • Build vs buy. BUILD, trivial. Two endpoints plus a skill. POSTPONE promoting to a first-class MCP server (reusing Core’s OAuth 2.1 JSON-RPC skeleton pattern in the internal plane) until agents standardize on MCP over HTTP for this.

3.4 Write-back worker

  • Responsibility. When an answer is persisted, draft a permanent artifact (a GLOSSARY.md entry, a generated answer page under _documentation/, or a skill edit) and open a review-gated PR. Close the loop by making the next agent find the artifact before asking.
  • Mechanism. A queue-or-cron worker in the internal plane scans newly ANSWERED canonical questions that lack an ArtifactLink, drafts the artifact (LLM assist, provenance-stamped “DRAFT, machine-written from answer ”), and opens a PR. Reuse the github internal service, which already performs GitHub API writes, for PR creation. Optionally reuse the worker-core internal worker framework (defineInternalWorker, services/workflow-proxy/worker-core/lib/define-internal-worker.ts) if the drafting becomes a durable multi-step workflow, but a plain cron is enough for MVP.
  • Inputs. ANSWERED canonical questions with their answers and provenance.
  • Outputs. A PR (draft artifact diff) into the factory lane, plus an ArtifactLink row (status proposed). On merge, status flips to merged and the docs pipeline indexes it.
  • Failure behavior. Drafting or PR creation fails: the answer is already persisted and already fanned out to askers, so the loop’s blocking work is done. Write-back is retried; a stuck draft is visible in the digest as “answered, not yet written back.” Write-back never blocks resume.
  • Build vs buy. BUILD, thin. AVOID Backstage TechDocs (R7: 2-3 FTE, ~$1.52M 3-yr TCO MODELED, engine on unmaintained MkDocs) and memory vendors with going-concern risk (Letta sunsetting server-side memory, Zep pulled its self-host CE, R7). The write-back target is the EXISTING _documentation/ pipeline, so Atlas builds no second knowledge system.

3.5 Digest cron

  • Responsibility. Two jobs. (1) Reconcile: poll Core for completed agent-questions tasks, map each back to its canonical question, persist the answer, and trigger fan-out. (2) Render: emit a daily GChat digest of decisions made, questions open, and answers written back.
  • Reconcile mechanism. GET /tasks?assigneeGroupSlug=agent-questions&type=information_request&status=completed&entityType=atlas_question (all filters confirmed present, tasksList.controller.ts:16-30, expose:true auth:true). Atlas mints tasks with entityType="atlas_question", entityId=canonicalId, so a completed task maps deterministically to its canonical and its formPayload carries the answer. A cursor on completedAt makes the poll incremental. This is the MVP backbone for detecting a human answer, and it needs no new Core event topic (R1: the task-events fan-out topic does not exist, “add when a consumer exists”, and building it would be a Core PR).
  • Inputs. Completed tasks since cursor; the answer ledger.
  • Outputs. New Answer rows plus fan-out triggers; a GChat message.
  • Failure behavior. GChat webhook down: the digest is skipped and retried next tick, no state is lost (the ledger is the truth). Reconcile is idempotent on coreTaskId: re-processing a completed task is a no-op.
  • Build vs buy. BUILD. No GChat client exists in the repo (R1 confirmed zero chat or slack code), so this is a roughly 20-line webhook POST plus a deterministic render function. No LLM in the digest. Note the estate precedent: the legacy Google-Chat triage gateway (Sherlock) was deliberately not ported (services/notifications/README.md:4-6), so Atlas’s GChat surface is intentionally minimal (a one-way digest, not a two-way chat tool).

3.6 Renderings (GLOSSARY.md, generated answer docs, decision records)

  • Responsibility. Be the read surface for agents and the two humans. Not a system, an output.
  • Mechanism. Write-back produces markdown under _documentation/. The existing pnpm docs:sync bundles it into the docs service, where the existing docs MCP search indexes it (R1: docsContent.service.ts). Atlas gets a searchable, agent-readable corpus for free, no new search system in MVP.
  • Decision-record boundary. Atlas renders refactor Q&A decisions into _documentation/, NOT into reasons-and-decisions/07-decision-log.md, which is Tomas’s binding, confidential, append-only org log. This resolves the R2 overlap risk: Atlas does not build a competing decision store and does not touch the confidential one.
  • Build vs buy. BUY the pipeline (it exists). BUILD only the render functions.

4. Data model

Naming discipline (R2): never “ticket”, “task”, or “issue” for Atlas’s own records, those collide with Core’s tasks service. Atlas entities are Question, CanonicalQuestion, Answer, WorkflowAttachment, ArtifactLink. The Core-side artifact is a Task of type INFORMATION_REQUEST, Core’s word, created by Atlas but referenced by id only.

Every table spreads ...defaultTableColumns (libs/drizzle/defaults.ts: uuid pk, created_at, updated_at timestamptz) per the encore-ts rule, unless it is append-only by intent.

Entity Key fields Notes
Question (raw ask) id, canonicalId (FK), askerRef, unitRef? (work-ledger link, never duplicated), rawText, normalizedKey, classification (none, internal, sensitive), createdAt One row per ask, even duplicates. rawText for sensitive rows is redacted after a retention window.
CanonicalQuestion id, title, normalizedKey, state (see 4.1), coreTaskId?, attachedCount, scope?, answeredAt? The unit of routing. One Core task per canonical.
Answer id, canonicalId (FK), text, scope (for example orders-only or program-wide), confidence, expiry?, provenance (answeredByUserId, coreTaskId, answeredAt), supersedesAnswerId?, classification Append-only, supersede-never-edit. Scope and expiry are load-bearing: a stale answer served as fresh is worse than no answer.
WorkflowAttachment id, questionId (FK), workflowId, runId, signal The Temporal stamp for an asker that is itself a blocked workflow. Enables instant signal-resume. Absent for pure pollers.
ArtifactLink id, canonicalId or answerId (FK), kind (glossary, doc, skill, decision-record), path, prUrl?, commitSha?, status (draft, proposed, merged) Ties an answer to the artifact it produced, so write-back is auditable and idempotent.

4.1 Lifecycle (CanonicalQuestion state machine)

stateDiagram-v2
  [*] --> OPEN: new canonical created
  OPEN --> ROUTED: INFORMATION_REQUEST task minted
  OPEN --> OPEN: duplicate ask attaches
  ROUTED --> ROUTED: duplicate ask attaches
  ROUTED --> ANSWERED: task completed, answer persisted
  ANSWERED --> ANSWERED: duplicate ask returns answer, no route
  ANSWERED --> EXPIRED: answer expiry passed
  EXPIRED --> ROUTED: re-asked, re-routed
  ANSWERED --> SUPERSEDED: new answer supersedes
  SUPERSEDED --> [*]

Duplicate asks attach at OPEN, ROUTED, and ANSWERED. Attaching to an ANSWERED canonical returns the answer immediately with no new interrupt, which is the whole point.

4.2 Retention, auditability, PII

  • Retention. Answers are kept indefinitely, they are the compounding asset. Raw Questions flagged sensitive have rawText redacted after a window (DESIGN 30 days), keeping normalizedKey and metadata so dedup and audit still work but the sensitive body stops dwelling.
  • Auditability. Every Answer carries provenance (who, when, which Core task). Supersede-never-edit means the full history is always reconstructable. Every ArtifactLink ties a rendered fact back to the answer and PR that produced it, so any doc line is traceable to a human decision.
  • PII and secrets. Question text may quote legacy Ruby or Java, which may carry credentials or customer data. Deterministic controls at ingest: a regex denylist (gitleaks-style key patterns) scans rawText, a hit sets classification=sensitive. The digest renderer never emits full question bodies, only titles, and skips sensitive items entirely (rendering “one redacted question routed to ”). Sensitive text never reaches GChat and never reaches a write-back artifact without explicit human clearance. No LLM in the redaction path, it is a regex plus a flag.

5. Integration

Seam Mechanism Substrate evidence PR to Core?
Mint a question task tasks._taskCreateFromWorkflow, called through Atlas’s core-client adapter with a service-account token expose:false auth:false, doc comment names “the internal plane through its proxy” as an intended caller (_taskCreateFromWorkflow.controller.ts:6-18). Proven cross-plane path: workflow-proxy already calls expose:false workflow-management endpoints this way. No
Detect a human answer Reconcile cron on tasks.tasksList filtered by group, type, status, entityType expose:true auth:true, all filters present (tasksList.controller.ts:16-30) No
Route to the queue Mint with assigneeGroupSlug="agent-questions" Group slug is a plain string in tasks (tasks.interfaces.ts:55); real membership is a config act in the user service (GET /user/groups) No, config only
Instant resume for stamped askers Fan-out signals each WorkflowAttachment via workflow-proxy to workflow-management._workflowSignalSend The signal seam exists and is expose:false (_workflowSignalSend.controller.ts); tasks already uses it (tasks.service.ts:176-182) No
Resume for pollers Agent polls search_answers or get_answer; canonical flips to ANSWERED Atlas-owned No
Write-back PR Reuse the github internal service GitHub API writes services/github already writes to GitHub No
Digest One-way GChat webhook POST No chat client exists, build a 20-line POST (R1) No

The one manual, non-code ask to Tomas: create the agent-questions group and add the answerers to it (a user service config act), exactly the naive-first, zero-PR path plan 001 already decided.

5.1 Fan-out: why Atlas owns it

Dedup collapses N asks into one CanonicalQuestion and therefore one Core task. Core’s completion signals at most one stamped workflow (tasks.service.ts:176-182). It cannot know about the other N-1 askers, because dedup happened inside Atlas. So Atlas holds the attachment list and, on reconcile of the completed task, fans the one answer out: signal every WorkflowAttachment through workflow-proxy for instant resume, and flip the canonical to ANSWERED so every poller picks it up on its next get_answer. This is the architectural crux. Getting it wrong means either re-routing duplicates to the human (defeating dedup) or leaving askers blocked forever.

sequenceDiagram
  participant A as Asking agent
  participant AT as Atlas triage
  participant DD as Dedup
  participant T as Core tasks
  participant H as Human (Tomas)
  participant RC as Atlas reconcile cron
  participant WB as Write-back

  A->>AT: ask_question(rawText, attachment?)
  AT->>DD: normalize + match
  alt duplicate of ANSWERED
    DD-->>AT: attach, existing answer
    AT-->>A: answer now (no interrupt)
  else new canonical
    DD-->>AT: new
    AT->>T: mint INFORMATION_REQUEST (group=agent-questions, entityId=canonicalId)
    AT-->>A: questionId (park and poll)
    H->>T: answer once in existing admin UI
    RC->>T: list completed agent-questions since cursor
    T-->>RC: completed task + formPayload
    RC->>AT: persist Answer, flip canonical ANSWERED
    AT-->>A: resume (signal if stamped, else poll picks it up)
    Note over AT,A: fan-out to ALL attached askers, not just one
    AT->>WB: enqueue write-back
    WB->>WB: draft artifact, open review-gated PR
  end

6. Security threat model

Threat Vector Mitigation
Prompt injection via answers into agents An answer or a rendered artifact carries injected instructions that steer the N agents who read it Answers come only from trusted humans (completion ACL: assignee, group member, ADMIN, taskComplete.controller.ts). The real injection surface is legacy quotes in question text and LLM drafts, not the human answer. Agents treat GLOSSARY.md and answers as data, not instructions. Provenance stamps let a reader see the author.
Poisoned write-back An LLM draft is wrong or hostile, poisons the corpus, misleads future agents at machine speed Write-back is a review-gated PR, never an auto-merge. Every draft is marked “DRAFT, machine-written from answer ”. Scope, expiry, and confidence on every answer bound its blast radius. This is the initial recommendation’s “biggest usability risk” made load-bearing, not optional.
Secrets or PII in question text An agent quotes a legacy config with a live credential into a question, which lands in Postgres and possibly GChat Regex denylist at ingest flags sensitive, redaction window on rawText, digest never emits bodies and skips sensitive items entirely. Atlas Postgres is internal-plane, access-controlled.
Wrong dedup merge A false-merge silently feeds N agents one wrong answer Auto-merge only on exact normalized key. Fuzzy tier suggests, human or agent confirms. Bias toward false-new.
Cross-plane trust Atlas reaching expose:false auth:false Core endpoints Same trust model workflow-proxy already relies on: platform network isolation plus service-account bearer where auth is required. This is Tomas’s platform boundary, not Atlas’s to redefine. Atlas uses the existing path unchanged. (ASSUMPTION: Encore Cloud cross-app internal reachability is configured as it is for workflow-proxy today. Locally it is plain HTTP between :4000 and :4001.)
Runaway question volume The fleet floods the queue and burns the one scarce human Deterministic rate-limit per asker and per normalized key. Dedup is the primary defense. Falsifiable failure metric already set (R2): questions per unit flat after 4 weeks means the loop is not compounding.

7. Deployment, testing, SLOs

7.1 Deployment

Everything runs on encore run today: core on :4000, internal on :4001 (root CLAUDE.md port registry). Atlas is a new internal-plane service with its own Encore SQLDatabase (Encore provisions a local Postgres via Docker), a digest cron (Encore CronJob), and one secret (the GChat webhook URL, held inside the service per the secrets rule). Nothing needs the unprovisioned infra (R1: infra is 0% behind a billing gate, Grafana scripted-but-not-run). Cloud later changes nothing in the design: Encore provisions the DB, cron, and secret from the same code. SLO dashboards wait for Grafana to be billed.

graph LR
  subgraph local["Local now (encore run)"]
    direction TB
    CORE["core :4000 (unchanged)"]
    INT["internal :4001"]
    ATLASsvc["atlas service (in internal)"]
    APG[("Atlas Postgres, Encore-provisioned local")]
    INT --- ATLASsvc
    ATLASsvc --- APG
    ATLASsvc -->|distributed client| CORE
  end

  subgraph cloud["Cloud later (billing gate clears, SAME code)"]
    direction TB
    CORE2["encore-core app"]
    INT2["encore-internal app"]
    ATLAS2["atlas service"]
    APG2[("Cloud SQL, Encore-managed")]
    GRAF["Grafana SLOs (deferred)"]
    INT2 --- ATLAS2
    ATLAS2 --- APG2
    ATLAS2 -->|distributed client + service account| CORE2
    ATLAS2 -.-> GRAF
  end

  local -.->|no design change, encore provisions from code| cloud

7.2 Testing seams

Component Seam Test
Dedup normalize and match Pure function One assert-based unit test on normalize plus exact and fuzzy match (ponytail check).
Reconcile cron Mock the tasks list client Assert a completed task maps to its canonical and persists one Answer, idempotent on coreTaskId.
Fan-out Mock workflow-proxy and the poller flip Assert one answer signals every stamped attachment and flips canonical.
Write-back drafter Mock LLM and the github client Assert a draft PR is opened once and an ArtifactLink row is written.
Digest render Pure render over ledger rows to string Snapshot test, assert sensitive items are excluded.
Core-client adapter Contract cast Integration test against local core.

Follow the repo testing rule: mirror the source tree, arrange-act-assert, name by behavior, cover failure paths (.claude/rules/testing.md).

7.3 SLOs (modest, all DESIGN)

  • ask_question p95 under 500 ms (a write plus a pg_trgm lookup). DESIGN.
  • search_answers p95 under 300 ms (indexed Postgres query). DESIGN.
  • Resume latency: instant for a stamped asker (signal), at most one reconcile interval (DESIGN 60 s) for a poller. DESIGN.
  • Digest: daily. DESIGN.

These are deliberately loose. The scarce resource is human decision-hours, not milliseconds.


8. Architecture decisions

Decision Alternatives Recommendation Trade-offs Reversibility
Detect answers by reconcile cron on tasksList Build a Core task-events topic; Atlas Temporal workflow-of-record per question Reconcile cron Adds up to 60 s latency; avoids a Core PR and avoids a Temporal workflow per question High: swap to signal-driven later with no data-model change
Atlas owns fan-out to N askers Rely on Core’s single-stamp signal Atlas owns fan-out Atlas must hold the attachment list; correct by construction because dedup lives in Atlas Structural, not meant to reverse
Dedup on pg_trgm exact-plus-fuzzy Embedding or vector DB from day one; LightRAG immediately pg_trgm, confirm fuzzy Lower recall than embeddings; zero new infra, deterministic, Postgres-first High: add LightRAG as the fuzzy backend behind the same interface
Agent tools as internal-plane HTTP plus a skill Add tools to Core MCP registry; stand up a full Atlas MCP server now HTTP plus skill No OAuth JSON-RPC niceties yet; zero Core PR, trivial to build High: promote to MCP later
Write-back into existing _documentation/ pipeline A new Atlas wiki or knowledge store Existing pipeline Bound to the repo’s docs conventions; no second knowledge system, free search High: the renderings are regenerable
Answers append-only, supersede-never-edit Mutable answers Append-only Slightly more storage; provenance always intact, matches repo discipline Structural
GChat digest one-way only Two-way chat triage (a Sherlock rebuild) One-way digest No in-chat answering; matches the deliberate non-port of Sherlock, avoids scope creep Medium
New Atlas service copies the github internal template Extend an existing service New service One more service to register; clean boundary, no Core change Medium

9. MVP versus post-MVP

MVP architecture (all on encore run, zero Core PR):

  • Triage service, own Postgres, Drizzle, layered.
  • Dedup: normalized-key exact auto-attach plus pg_trgm fuzzy-suggest.
  • Two HTTP endpoints plus a Claude Code skill (ask, search).
  • Mint via _taskCreateFromWorkflow, route to agent-questions.
  • Reconcile cron detects completed tasks, persists answers, fans out.
  • Write-back worker opens review-gated PRs into _documentation/.
  • Digest cron to GChat.
  • Data model: Question, CanonicalQuestion, Answer, WorkflowAttachment, ArtifactLink.

Post-MVP (only on MEASURED evidence):

  • LightRAG or embedding assist for dedup and search, when exact-plus-trigram miss-rate is measured too high.
  • Instant Temporal signal-resume as the default (already supported by the WorkflowAttachment field) once askers are reliably workflow-wrapped.
  • A first-class Atlas MCP server on the Core skeleton pattern.
  • Grafana SLO dashboards once infra is billed.
  • Auto-resolve on high-confidence exact duplicates without any human touch, logged, only after dedup precision is proven on real data.

Buy: the docs pipeline (_documentation/ plus docs-sync plus docs MCP search), the tasks inbox, the workflow signal seam, the github PR-write path. All exist.

Build: the triage service, the dedup module, the reconcile and digest crons, the write-back drafter, the two agent endpoints plus skill, the GChat webhook POST. All thin.

Postpone: embedding retrieval, MCP server, auto-resolve, SLO dashboards.

Avoid: Backstage TechDocs (TCO ~$1.52M 3-yr MODELED, unmaintained engine, R7), memory vendors with going-concern or benchmark risk (Letta sunsetting server-side memory, Zep pulled self-host CE, mem0 disputed benchmark, R7), a new Core task-events topic (Core PR), a second decision-log or knowledge store (competes with the confidential reasons-and-decisions/ and the docs pipeline), any human UI (audience is two people who already have the admin UI and a GChat thread), a two-way GChat triage tool (Sherlock was deliberately not ported).


10. Top technical risks

  1. Fan-out correctness (highest). One answer must resume every attached asker and no more. A bug re-routes duplicates to the human (defeating the product) or strands askers. Mitigation: the attachment list is the single source, reconcile is idempotent on coreTaskId, fan-out is covered by a dedicated test.
  2. Dedup precision at fleet scale (unsolved in literature, R2). Too loose poisons N agents, too tight fails to protect the human. Mitigation: exact-only auto-merge, human-confirmed fuzzy, bias to false-new, tune the 0.75 threshold on real data, watch the “questions per unit flat after 4 weeks” failure metric.
  3. Cross-plane reachability assumption. The whole zero-PR mint path rests on the internal plane reaching expose:false Core task endpoints, as workflow-proxy does. If Encore Cloud config differs from local, minting breaks. Mitigation: it is the exact path already in production use for workflow reporting; validate on the first cloud environment, fall back to a Robert-owned proxy (plan 001 A-lite) if the lane is not granted.
  4. Write-back doc-rot. Machine-drafted artifacts at machine speed can poison the corpus. Mitigation: review-gated PRs, provenance, scope, expiry, never auto-merge.
  5. Secret leakage into GChat. A missed secret in question text reaches the digest. Mitigation: regex denylist at ingest, digest emits no bodies and skips sensitive items, but regex is imperfect, so sensitive classification defaults conservative.

11. Honest operational-complexity estimate

Atlas adds one internal-plane service, one Postgres database, two crons, one worker, one secret, and one Claude Code skill. It changes zero files Tomas owns. It stands on a template that already exists in the repo (github internal service) and seams that already exist and are already used (_taskCreateFromWorkflow, tasksList, _workflowSignalSend, the docs pipeline, the github PR-write path).

The genuinely new logic is small: a normalize-and-match function, a fan-out routine, a reconcile loop, a draft-and-PR worker, and a render function. The hard parts are not code volume, they are two judgment calls that no framework solves: the dedup threshold and the fan-out correctness. Both are testable and both have falsifiable failure metrics already set.

Operational load in steady state is one database to back up, two crons to watch, and one queue of write-back PRs for a human to approve. The write-back review queue is the one place Atlas could recreate the bottleneck it exists to kill (initial recommendation open question 6): if answer volume is low (the initial recommendation’s own test: fewer than ~5 write-backs per week means glossary and digest are a human habit, not a product), that review is trivial. If it is high, the review itself must be batched or sampled, and that is the first thing to measure in the one-week live test before building any more of Atlas than the triage core.

Bottom line: the architecture is thin because the substrate is rich. Atlas is mostly wiring existing seams into a loop, plus one deterministic dedup brain and one fan-out routine. That is the right size for a two-human program, and it is reversible at almost every decision.