Arc OS — Architecture

Overview

Arc OS replaces 11,321 lines of custom Python/JS infrastructure with Claude Code native tools. The AI agent IS the backend.

System Architecture (Phase 60)

graph TB
    User[👤 User Browser :18888]
    TG[📱 Telegram]
    Local[💻 Local IDE]

    subgraph "Edge"
      Nginx[Nginx :18888<br/>basic-auth + deny-list]
      FE[React CRM + Phaser<br/>frontend container]
    end

    subgraph "Master Bot Bun :19210 — 127.0.0.1 only"
      ApiSrv[api-server.ts<br/>196 LOC]
      Routes[master-bot/routes/<br/>auth · internal · cli · websocket]
      Router[shared/routes/router.ts<br/>373 LOC dispatcher]

      subgraph "19 Domain Modules — Phase 48"
        D1[auth · projects · workers]
        D2[skills · sage · chat]
        D3[wiki · files · onboarding]
        D4[billing · invites · analytics]
        D5[+ 7 more]
      end
    end

    subgraph "Federated Bots"
      Master[Master Bot<br/>orchestrator]
      Child[Child Bots<br/>per-project]
      Claude[claude -p CLI]
    end

    subgraph "Intelligence Layer"
      Eval[Binary Evals]
      Ctx[Context Router top-5]
      Learn[Learnings inject]
      Karp[Karpathy nightly loop]
    end

    RAG[shared/rag.ts<br/>Cohere + sqlite-vec]

    DB[(SQLite WAL<br/>50+ tables<br/>migrations 001-050)]
    Vault[(AES-256-GCM Vault<br/>config/vault.json)]
    Bridge[Local Bridge CLI<br/>WebSocket relay]

    User -->|HTTPS| Nginx
    Nginx --> FE
    Nginx -->|/api/crm/*| ApiSrv
    Nginx -->|/api/sse/*| ApiSrv
    Nginx -->|/ws/terminal| ApiSrv
    ApiSrv --> Routes
    Routes --> Router
    Router --> D1 & D2 & D3 & D4 & D5

    TG --> Master
    TG --> Child
    Master -.spawns via tmux.-> Child
    Child --> Claude
    Claude --> Eval & Ctx & Learn

    Router -.queries.-> RAG
    Child -.auto-sync.-> RAG
    RAG --> DB

    Router --> DB
    Router --> Vault
    Child --> Vault

    Local --> Bridge
    Bridge -.ws relay.-> ApiSrv
    Karp -.nightly.-> DB

    subgraph "Hetzner arc-cloud-prod 157.180.44.232 — Phase 60"
      HNet[arc-cloud-net bridge]
      C1[arc-user-xxxx<br/>claude CLI + child-bot<br/>2GB RAM / 1.5 CPU]
      C2[arc-user-yyyy<br/>...]
    end

    Router -.SSH hetzner_ed25519.-> HNet
    HNet --> C1 & C2
    TG -.wake + /continue.-> C1

Key facts:

LEGACY (v1):
User → Telegram Bot (Python 111KB) → HybridEngine → command_queue.json
       → bridge_processor.sh → Claude → command_responses.json
       → FastAPI EventBus → WebSocket → Phaser UI

V2 (NATIVE-FIRST):
User → Official Telegram Channel → Claude Code Session
       ↓                                ↓
       reply/edit_message          Agent Teams (TaskCreate/SendMessage)
                                        ↓
                                   Write state → JSON files
                                        ↓
                                   Arc OS Bridge MCP → HTTP/SSE → Phaser UI

Reduction: 11,321 lines → ~3,700 lines (67% less code)

Components

1. Claude Code Session (The Brain)

The Claude Code session replaces:

All orchestration happens natively through Agent Teams.

2. Arc OS Bridge MCP Server (State Adapter)

TypeScript MCP server (Bun) that bridges Claude Code state to the frontend.

MCP Tools (Claude Code side):

Tool Direction Description
get_office_state read Read state/office-state.json
update_agent_state write Update agent position, status, bubble
get_tasks read Read Agent Teams task files
get_knowledge read Read knowledge index

HTTP API (Frontend side):

Endpoint Method Description
/api/state GET Full office state
/api/tasks GET Task list
/api/knowledge GET Knowledge index
/api/events GET SSE stream

3. Phaser Frontend (Visual Layer)

Phaser 3.80 + Vite. Connects to MCP server via HTTP/SSE.

Key difference from v1: no WebSocket — replaced by SSE from MCP server. No command input UI — Telegram is the command interface.

4. Telegram Command Interface (Tactical UI Layer — Phase 21.0)

Telegram is the primary control panel for the entire Arc OS. Two bot tiers:

Master Bot (@citadel_ceo_bot):

Child Bots (@cv2_pt_bot, etc.):

CRM Integration (development):

CRM_BASE_URL = http://62.171.128.248:18888  (dev)
               https://crm.citadel.v2       (future prod)

All keyboard layouts defined in shared/ui_templates.ts — single source for UI changes.

Callback data protocol: action:target (max 64 bytes)

Data Flow

1. User sends "/status" via Telegram
2. Claude Code session receives via Telegram Channel
3. Rick reads state/office-state.json + TaskList()
4. Rick replies via Telegram (edit_message for live updates)
5. Rick calls update_agent_state() via MCP
6. MCP server writes to state/office-state.json
7. File watcher detects change → pushes SSE event
8. Frontend receives SSE → updates agent sprites

State Management

All state lives in JSON files under state/:

File Schema Updated By Read By
office-state.json Agent positions, status, bubbles Claude (via MCP) Frontend (via HTTP)
knowledge.json Indexed file metadata Squanchy agent Frontend, Beth
reports/*.json Analysis output Beth, Summer Frontend

Agent Teams tasks managed by Claude Code natively at ~/.claude/tasks/citadel-v2/.

Agents (6)

Agent Role Model Activation
Rick CEO/Orchestrator opus-4.6 Always active (team lead)
Morty SRE/Monitoring haiku-4.5 Health checks, cron
Summer Memory/Review sonnet-4.5 Code review, security
Jerry Maintenance haiku-4.5 Cleanup, docs
Squanchy Archivist haiku-4.5 File indexing
Beth Analyst sonnet-4.5 Research, analysis

Supply Chain

CEO → Rick (decompose) → Agent Team (execute)
                        → Cross-Review (Summer validates)
                        → Rick (synthesize)
                        → CEO (final result)

Enforced through:

Technology Stack

Layer v1 v2
Backend FastAPI + Python 3.12 Claude Code session
Database SQLite (8 tables) JSON files + Agent Teams
Messaging WebSocket + EventBus Agent Teams SendMessage
State SQLite + JSON bridge JSON files (state/)
Frontend Phaser 3.86 + WebSocket Phaser 3.80 + SSE
Telegram Custom bot (111KB) Official Channel plugin
Bridge bash + jq (19.6KB) Eliminated
MCP None Arc OS Bridge (TypeScript)
Deploy Docker Compose Docker Compose

Lifecycle Hooks

Claude Code hooks (~/.claude/settings.json) auto-sync agent state:

SubagentStop event → subagent-stop.sh → office-state.json (agent=idle)
                                              ↓
                                   StateManager (fs.watch) → SSE → Phaser UI

Stop event → session-end.sh → latest_wrapup.txt + all agents=idle

Scripts: scripts/citadel-hooks/subagent-stop.sh, scripts/citadel-hooks/session-end.sh

Key fields: subagentName (SubagentStop), stopReason (Stop), cwd (both).

Semantic Search — Self-hosted RAG (Phase 71, 2026-06-05)

Self-hosted retrieval over the existing SQLite SSOT. Replaces the Phase 36 NotebookLM Bridge (decommissioned, port 19213 free, see services/.archived-notebooklm-bridge-2026-06-05/ + docs/public/architecture/rag-architecture.md).

Chat / arc kb / Cloud PM
        │
        ▼
shared/rag.ts search()
        │
        ▼
shared/embeddings.ts  ──►  Cohere /v2/embed
  embedQuery(text)         (multilingual, 1024-dim float32)
        │
        ▼
embeddings_vec (vec0 virtual)  +  embeddings (metadata)
  KNN (k * 4 overfetch)  →  project + doc_type filter at JOIN  →  top-K passages

Fallback: executeLocalKnowledgeSearch (keyword scan over wiki + issues + roadmap)
          when top-hit distance > 1.6 or zero results (covers brand-new projects).

Data & Task Routing Policy

Three-tier system. Each tier has a strict purpose. Mixing tiers = chaos.

┌─────────────────────────────────────────────────────────────────┐
│  TIER 1: Working Memory (Hot)                                   │
│  office-state.json + state/tasks/                               │
│  CLI: /citadel-task, /citadel-status                            │
│                                                                 │
│  Daily agent tasks. Lives during session. Dies after.           │
│  Completed task → /citadel-wrapup → Tier 3.                    │
├─────────────────────────────────────────────────────────────────┤
│  TIER 2: Strategic Backlog (Warm)                               │
│  GitHub Issues (Claude-CEO + citadel-v2 repos)                  │
│                                                                 │
│  Epics only: Phase 20, Phase 21, global bugs.                   │
│  No daily tasks. No "fix typo" issues.                          │
│  One Issue = one multi-week initiative or critical defect.      │
├─────────────────────────────────────────────────────────────────┤
│  TIER 3: Long-term Memory (Cold)                                │
│  docs/library-export/ + project wiki + closed issues            │
│  → embedded via shared/rag.ts (Cohere + sqlite-vec, Phase 71)   │
│  CLI: /citadel-wrapup, arc kb search "<query>"                  │
│                                                                 │
│  Encyclopedia. Session results, architectural decisions, RAG.   │
│  Read-only archive. No active tasks. No status tracking.        │
└─────────────────────────────────────────────────────────────────┘

Routing Rules

Data Type Tier Example
"Fix deploy script" 1 — Working Memory /citadel-task "Fix deploy script"
"Phase 20: Multi-Tenant SaaS" 2 — GitHub Issues gh issue create --title "Phase 20: ..."
"Session summary: hooks implemented" 3 — Library /citadel-wrapup → wiki + auto-embed via Phase 71 hooks
"Architecture decision: chose SSE over WS" 3 — Library Archived in wrapup after session
"Bug: SSE drops on VPS" 2 — GitHub Issues Only if cross-session / blocking
"Rick is working on hooks" 1 — Working Memory office-state.json agent status

Lifecycle

CEO gives task → Tier 1 (agent works on it)
                     ↓ completed
                 /citadel-wrapup → Tier 3 (archived)
                     ↓ if strategic
                 gh issue create → Tier 2 (tracked long-term)

Anti-patterns (DO NOT)

Infrastructure (Phase 20.5)

Structured Logging (shared/logger.ts)

JSONL format, dual output (file + console), daily file splitting by category.

/var/log/citadel/
├── master/
│   ├── system-2026-04-02.log   ← lifecycle, config, health
│   ├── dialog-2026-04-02.log   ← (not used by master)
│   └── error-2026-04-02.log    ← errors (also in system)
├── citadel-v2/
│   ├── system-2026-04-02.log
│   ├── dialog-2026-04-02.log   ← user ↔ Claude messages
│   └── error-2026-04-02.log
└── <project-name>/             ← per onboarded project

Log rotation: config/logrotate-citadel.conf/etc/logrotate.d/citadel (daily, 7-day retention).

Secrets Vault (shared/vault.ts)

AES-256-GCM encrypted storage for bot tokens.

Key source: SECRET_ENCRYPTION_KEY env → config/vault-key file → auto-generated
Storage:    config/vault.json (atomic writes: tmp + mv)
Cache:      In-memory after initVault() — no disk I/O per getSecret()
Fallback:   getSecret("FOO") → vault cache → process.env.FOO
Naming:     child:<project-name>:token

Master's own token stays in .env (vault loads inside master process).

Self-Healing Watchdog (master-bot/watchdog.ts)

Background monitor for child bots. Runs inside master bot process.

Every 30s: HTTP health check → /api/child/health (5s timeout)
  Healthy → reset failures
  Unhealthy → increment failures
  3+ failures + backoff elapsed → auto-restart (kill tmux → start new with vault token)
  Backoff: 30s → 1m → 5m → 15m → 60m (cap)
  10 consecutive failures → permanently disable + notify CEO

State persisted to config/watchdog-state.json (survives master restart). CEO notifications via Telegram: first restart, restart failure, permanent disable. /watchdog command: real-time status of all children.

Project Removal Protocol (Phase 20.4 + 21.0)

/remove_project <name> → triple-confirmation → full cleanup:

1. Kill tmux session: child-<name>
2. Mass Kill ghosts: ps aux | grep child-<name> → kill -9 all PIDs
3. Port check: ss -tlnp | grep :<port> → force-free if occupied
4. Remove from bot_registry.json + in-memory reload
5. Delete /opt/repos/<name>/ (safety: path must be under /opt/repos/, min 3 segments)
6. Delete /var/log/citadel/<name>/

Protected names: citadel-v2, citadel, claude-ceo, claude-CEO — removal blocked.

Ghost process problem (lesson learned): after tmux kill, orphan bun processes may hold ports. Mass kill + port check prevents "phantom bots" that respond to health checks but ignore Telegram.

Skill Management (Phase 21.0)

Child bots load skills from two sources at startup, deduplicated via Set:

Source 1: MANIFEST.md (JSON)
  → manifest.skills[]          (matched during onboarding)
  → manifest.library_skills[]  (library .md files matched)

Source 2: skills/ directory
  → *.md files → filename without extension

Merge: Set<string>(manifest.skills + manifest.library_skills + skills/*.md)

Example (PT project): 5 from manifest + 7 from skills/ = 12 unique skills.

Skills appear as:

Security (Phase 42 — Sentinel audit 2026-04-23, 4 passes, 13 patches)

Full overview: SECURITY.md. Audit report: security/audit-2026-04-23.md.

Secrets & storage:

Auth:

Multi-tenancy (Phase 42):

Network perimeter:

Input / path safety:

Regression coverage: scripts/vps-sync.sh runs 7+ post-deploy smoke tests after every deploy (loopback bind, path traversal, chat/save validation, proxy canary, SSE ?token=, fail-closed validation, SSRF block if CEO_TOKEN set).

Master Bot Modules (Phase 25 — DEBT-1 Resolution)

The master bot is organized into focused modules:

master-bot/
├── bot.ts               ← Bootstrap: vault, registry, context, wire everything (106 lines)
├── context.ts           ← MasterContext type + domain interfaces
├── api-server.ts        ← Bun.serve() — HTTP, WebSocket, SSE, CRM routes
├── tg-api.ts            ← Telegram Bot API wrapper (sendMessage, getUpdates, etc.)
├── child-state.ts       ← Read child bot state, health, heartbeat, tmux, bridge
├── telegram-commands.ts ← All /command handlers, callback query handler, message router
├── telegram.ts          ← Slim polling loop + re-exports for backward compat
├── watchdog.ts          ← Self-healing child bot monitor
└── onboarding.ts        ← Interactive project creation wizard

bot.ts imports only from telegram.ts and api-server.ts. All other modules are internal implementation details.

Bridge CLI (Phase 25 — Local Gateway)

bridge/
├── bin/citadel-bridge.ts    ← CLI entry (commander.js): connect, pull, push, status, disconnect
├── src/
│   ├── auth.ts              ← JWT validation against CRM
│   ├── config.ts            ← ~/.citadel/bridge.json persistence
│   ├── inject.ts            ← CLAUDE.md <!-- CITADEL:START/END --> markers
│   ├── sync.ts              ← Pull skills-bundle + learnings, push local learnings
│   └── heartbeat.ts         ← Session activity reporter
├── package.json
└── tsconfig.json

CRM API Endpoints (50+ total)

Core CRM (Phase 22)

Endpoint Method Purpose
/api/master/health GET Master bot health (public)
/api/crm/projects GET List all projects + live health
/api/crm/projects/:name GET Project detail
/api/crm/projects/:name/logs GET Tail JSONL logs
/api/crm/projects/:name/files GET Safe directory listing
/api/crm/projects/:name/files/upload POST Upload files
/api/crm/projects/:name/files/mkdir POST Create folder
/api/crm/projects/:name/files/create POST Create file
/api/crm/projects/:name/files/delete DELETE Delete file/folder
/api/crm/projects/:name/files/clone POST Clone git repo
/api/crm/projects/:name/files/read GET Read file content
/api/crm/projects/:name/wiki/tree GET List wiki .md files
/api/crm/projects/:name/wiki/file GET Read wiki file content
/api/crm/projects/:name/skills GET Installed skills
/api/crm/projects/:name/metrics GET Quality timeseries
/api/crm/projects/:name/restart POST Restart child bot
/api/crm/projects/:name/specs GET List specs
/api/crm/projects/:name/specs/:id/approve POST Approve spec
/api/crm/projects/:name/specs/:id/reject POST Reject spec
/api/crm/projects/:name/active-role GET/POST Agent role
/api/crm/projects/:name/message POST Queue message to worker
/api/crm/projects/:name/workers GET/POST Worker registry CRUD
/api/crm/projects/:name/workers/:id PUT/DELETE Update/delete worker
/api/crm/projects/:name/workers/generate-prompt POST AI-generate system prompt
/api/crm/projects/:name/skills-bundle GET Skills + evals for bridge
/api/crm/projects/:name/learnings GET/POST Learnings sync
/api/sse/logs/:name GET SSE log stream (?category=)

Dashboard & Wiki (Phase 32)

Endpoint Method Purpose
/api/crm/projects/:name/wiki/save PUT Save/create wiki page
/api/crm/projects/:name/skills/save PUT Save skill file
/api/crm/projects/:name/skills/delete DELETE Delete skill file

Multi-Tenant (Phase 33)

Endpoint Method Purpose
/api/crm/account/settings GET/PUT Account-level API keys
/api/crm/projects/create POST Lightweight project creation
/api/crm/onboarding/setup POST Full onboarding wizard

MCP & CLI (Phase 34)

Endpoint Method Purpose
/api/mcp/issues/:project POST/GET Issue CRUD
/api/mcp/issues/:project/:id PUT Update issue
/api/mcp/wiki/:project PUT Wiki sync via MCP
/api/mcp/roadmap/:project GET/PUT Roadmap read/sync
/api/cli/init/:project/:mode GET Cloud context for CLI
/api/mcp/skills/:project/:skill GET Fetch skill for MCP
/api/mcp/report/:project POST Mission report
/api/mcp/learnings/:project GET Get learnings for MCP

Live Terminal (Phase 35)

Endpoint Method Purpose
/api/crm/projects/:name/terminal/log POST Receive terminal JSONL from ARC CLI

Cloud PM & Knowledge (Phase 36, Phase 71 refresh)

Endpoint Method Purpose
/api/crm/projects/:name/chat POST SSE chat proxy to Anthropic API
/api/crm/projects/:name/skills/generate POST Neural Skill Generator. Phase 71.7: re-platformed off NotebookLM-as-generator onto Claude Sonnet with RAG-augmented context (top-5 hits as house-style exemplars).
/api/crm/projects/:name/rag/search GET Phase 71.7 (new): semantic search over embeddings + embeddings_vec (Cohere + sqlite-vec). ?q=...&k=6&include_global=true&doc_types=wiki,issue,skill.
/api/crm/projects/:name/notebooks GET Phase 71.8: legacy stub — returns { notebooks: [], retired: "phase-71.8" }.

Auth & OAuth (Phase 37)

Endpoint Method Purpose
/api/auth/register POST Email/password registration
/api/auth/login POST Email/password login
/api/auth/google GET OAuth Google redirect
/api/auth/callback/google GET OAuth Google callback
/api/auth/github GET OAuth GitHub redirect
/api/auth/callback/github GET OAuth GitHub callback
/api/auth/providers GET Available OAuth providers

Pinned Notes (Phase 41.8)

Endpoint Method Purpose
/api/crm/projects/:name/pins GET List pinned notes (newest first)
/api/crm/projects/:name/pins POST Pin a worker message to Context Rail (body: worker_id, body, optional title, author)
/api/crm/projects/:name/pins/:id DELETE Remove pin

Backend: migration 009 (pinned_notes table), pinnedNoteQueries in shared/db.ts. Frontend: ContextRail.jsx fetches on mount, listens for crm-pin-created CustomEvent, optimistic DELETE.

Data Protection Layer (Phase 45)

Hybrid at-rest encryption — client-side crypto foundation + server-side vault encryption.

Client-Side (Browser)

Server-Side (Bun + SQLite)

Security Headers

PII Sanitization

Recovery Key Endpoints

Endpoint Method Purpose
/api/crm/account/recovery POST Create recovery key (stores encrypted master key)
/api/crm/account/recovery GET List active recovery keys
/api/crm/account/recovery DELETE Revoke recovery key(s)
/api/crm/account/recovery/restore GET Retrieve encrypted master key for recovery

Full security details: docs/SECURITY.md, docs/architecture/PHASE_45_E2EE.md.

Standard Cloud — Multi-server Fleet (Phase 60)

Full details: docs/architecture/PHASE_60_STANDARD_CLOUD.md.

Servers

Server IP Role
Contabo 62.171.128.248 API + Master Bot + DB + Nginx
Hetzner arc-cloud-prod 157.180.44.232 User Docker containers

SSH Bridge

All Docker operations on Hetzner are initiated from Contabo via SSH:

Contabo root → ssh -i /root/.ssh/hetzner_ed25519 [email protected] docker <cmd>

CONTAINER_SERVER_IP in vault controls routing: 127.0.0.1 = local, any other IP = SSH.

Container Lifecycle

provision → ready → [idle 30min] → paused → [TG message] → ready
                                                           ↑ wake

Lifecycle cron: */5 * * * * on Contabo → scripts/cloud-lifecycle-cron.ts.

Cloud Waitlist

Users join via POST /api/crm/cloud/waitlist. Admin invites via POST /api/crm/cloud/waitlist/invite, which auto-upgrades plan to cloud. Table: cloud_waitlist (migration 029).