Gemmbot Implementation Plan
===========================

A company-wide AI assistant for Gemma Analytics, built as a single codebase
that powers both Bijan's personal bot and the shared Gemmbot deployment.


1. Architecture Overview
------------------------

Single codebase, N deployments:

    Gemma-Analytics/gemmbot (GitHub repo)
    ├── bot/          # Slack bot (Node.js, modular)
    ├── ui/           # Management UI (Next.js)
    └── docker/       # Session container image

The codebase supports two AUTH_MODEs:

    single_user:  One designated user, no DB auth needed.
                  Ideal for personal bots (power users, leadership).
    multi_user:   DB-backed roster, management UI for onboarding.
                  Ideal for team-wide shared bots.

Example deployments:

    Deployment: Bijan's personal bot
      Service:    claude-bot.service
      Auth:       AUTH_MODE=single_user, SLACK_USER_ID=U...
      Features:   All enabled (email, VPN, full shell)
      GitHub:     Personal Access Token (via management UI)
      Isolation:  Docker container per session
      Workspaces: /opt/claude-bot/workspaces/
      Domain:     *.bp.gemmaanalytics.com

    Deployment: Gemmbot (shared, multi-user)
      Service:    gemmbot.service + gemmbot-ui.service
      Auth:       AUTH_MODE=multi_user, DB-backed user roster
      Features:   Configurable (no email, no VPN at launch)
      GitHub:     GitHub App (short-lived tokens, auto-refreshed)
      Isolation:  Docker container per session
      Workspaces: /opt/gemmbot/workspaces/
      UI:         gemmbot.gemmaanalytics.com
      Dev servers: *.gemmbot.gemmaanalytics.com

    Deployment: [Future] Personal bot for employee X
      Service:    claude-bot-x.service
      Auth:       AUTH_MODE=single_user, SLACK_USER_ID=U...
      Features:   Configured per person
      GitHub:     Personal Access Token (via management UI)
      Isolation:  Docker container per session
      Workspaces: /opt/claude-bot-x/workspaces/
      Domain:     *.x.gemmaanalytics.com

All deployments use the same Docker execution path. The only differences
are configuration: auth mode, feature flags, Slack app tokens, and paths.
No code branches between deployment types.

Improvements to the codebase benefit all deployments. Per-deployment
config (CLAUDE.md template, env vars, feature flags) stays separate.


2. Repository Structure
-----------------------

    Gemma-Analytics/gemmbot/
    │
    ├── bot/
    │   ├── src/
    │   │   ├── app.js               # Entry point, Slack Bolt setup, event routing
    │   │   ├── config.js            # Env vars, feature flags, constants, model catalog
    │   │   ├── session.js           # Session lifecycle: create, resume, release, cleanup
    │   │   ├── ownership.js         # Thread ownership: claim, check, release (multi_user)
    │   │   ├── claude.js            # Claude CLI spawning (always via docker exec)
    │   │   ├── workspace.js         # Workspace creation, CLAUDE.md injection, secrets
    │   │   ├── caddy.js             # Dynamic reverse proxy (existing, as-is)
    │   │   ├── secret-dropoff.js    # Secure credential provisioning (existing, as-is)
    │   │   ├── cleanup.js           # Business-hours idle timer, warnings, teardown
    │   │   ├── audit.js             # Daily server audit (8 AM Berlin, keep/kill flow)
    │   │   ├── files.js             # File download/upload for Slack attachments
    │   │   ├── commands.js          # Command handlers (release, persist, status, model, etc.)
    │   │   ├── features.js          # Feature flag registry and checks
    │   │   ├── crypto.js            # AES-256-GCM encrypt/decrypt for secrets
    │   │   ├── db.js                # Prisma client wrapper (multi_user mode only)
    │   │   ├── docker.js            # Container lifecycle: create, exec, kill
    │   │   ├── github.js            # GitHub App token generation (multi_user mode)
    │   │   └── claude-auth.js       # OAuth flow automation for Claude Code tokens
    │   ├── bin/
    │   │   ├── request-secret       # CLI tool (existing)
    │   │   ├── vpn-exec             # VPN helper (existing)
    │   │   └── vpn-ns               # VPN namespace helper (existing)
    │   ├── templates/
    │   │   ├── SYSTEM_PROMPT_BASE.md      # Gemma-wide base template
    │   │   └── SYSTEM_PROMPT_PERSONAL.md  # Bijan-specific additions
    │   ├── deploy/
    │   │   ├── gemmbot.service            # Systemd unit for shared bot
    │   │   ├── claude-bot.service         # Systemd unit for personal bot
    │   │   ├── gemmbot-ui.service         # Systemd unit for management UI
    │   │   └── setup.sh                   # Server provisioning script
    │   ├── package.json
    │   └── .env.example
    │
    ├── ui/
    │   ├── prisma/
    │   │   ├── schema.prisma        # Shared database schema
    │   │   └── migrations/
    │   ├── src/
    │   │   ├── app/
    │   │   │   ├── layout.tsx
    │   │   │   ├── page.tsx               # Landing / redirect
    │   │   │   ├── login/page.tsx         # Google OAuth
    │   │   │   ├── settings/
    │   │   │   │   ├── page.tsx           # Profile: name, Slack ID, GitHub token
    │   │   │   │   ├── secrets/page.tsx   # Manage personal secrets
    │   │   │   │   └── prompt/page.tsx    # System prompt additions
    │   │   │   ├── sessions/page.tsx      # Own active sessions
    │   │   │   ├── admin/
    │   │   │   │   ├── page.tsx           # Admin dashboard
    │   │   │   │   ├── users/page.tsx     # User management
    │   │   │   │   └── sessions/page.tsx  # All sessions, kill switch
    │   │   │   └── api/
    │   │   │       ├── auth/[...nextauth]/route.ts
    │   │   │       ├── users/me/route.ts
    │   │   │       ├── secrets/route.ts
    │   │   │       ├── secrets/[id]/route.ts
    │   │   │       ├── sessions/route.ts
    │   │   │       ├── sessions/[id]/route.ts
    │   │   │       └── admin/
    │   │   │           ├── users/route.ts
    │   │   │           ├── users/[id]/route.ts
    │   │   │           └── sessions/route.ts
    │   │   └── lib/
    │   │       ├── auth.ts          # NextAuth config
    │   │       ├── db.ts            # Prisma client singleton
    │   │       └── crypto.ts        # AES-256-GCM (same algo as bot/src/crypto.js)
    │   ├── package.json
    │   ├── next.config.js
    │   └── .env.example
    │
    ├── docker/
    │   ├── Dockerfile               # Session container image
    │   └── entrypoint.sh            # Keeps container alive (sleep infinity)
    │
    ├── .github/
    │   ├── workflows/
    │   │   ├── deploy-bot.yml       # CI/CD: bot changes → deploy bot
    │   │   ├── deploy-ui.yml        # CI/CD: ui changes → deploy UI
    │   │   └── build-image.yml      # CI/CD: docker changes → rebuild image
    │   └── CODEOWNERS               # * @bijansoltani (all PRs require Bijan)
    │
    ├── CLAUDE.md                    # Instructions for Claude working on this repo
    └── README.md                    # Onboarding guide for employees


3. Database Schema (Prisma)
---------------------------

File: ui/prisma/schema.prisma

    datasource db {
      provider = "sqlite"
      url      = env("DATABASE_URL")  // file:/opt/gemmbot/data/gemmbot.db
    }

    // SQLite with WAL mode for concurrent reads from bot + UI.
    // If write contention becomes an issue at scale, migrate to Postgres
    // (one-line change in Prisma datasource).

    model User {
      id                    String     @id @default(cuid())
      email                 String     @unique    // must be @gemmaanalytics.com
      name                  String
      slackUserId           String?    @unique
      role                  Role       @default(USER)
      isActive              Boolean    @default(true)
      aboutUser             String?               // "About you" — user context
      modelInstructions     String?               // "Model instructions" — behavioral directives
      claudeTokenExpiresAt  DateTime?             // when OAuth token expires
      githubToken           String?               // encrypted PAT (single_user mode)
      createdAt             DateTime   @default(now())
      updatedAt             DateTime   @updatedAt
      lastActiveAt          DateTime?
      secrets               Secret[]
      sessions              BotSession[]
      // NextAuth relations
      accounts              Account[]
      authSessions          AuthSession[]
    }

    enum Role {
      USER
      ADMIN
    }

    model Secret {
      id              String   @id @default(cuid())
      userId          String
      user            User     @relation(fields: [userId], references: [id], onDelete: Cascade)
      name            String                       // human label: "Anthropic API Key"
      key             String                       // env var name: "ANTHROPIC_API_KEY"
      encryptedValue  String                       // base64(iv + ciphertext + authTag)
      createdAt       DateTime @default(now())
      updatedAt       DateTime @updatedAt
      @@unique([userId, key])
    }

    model BotSession {
      id             String    @id @default(cuid())
      userId         String
      user           User      @relation(fields: [userId], references: [id])
      threadTs       String    @unique              // Slack thread timestamp
      channel        String
      sessionId      String?                        // Claude CLI session ID
      workdir        String?
      containerName  String?                        // Docker container name
      model          String    @default("opus")     // current model for this session
      provider       String    @default("direct")   // "direct" or "bedrock"
      summary        String?                        // first user message, truncated
      persistUntil   DateTime?                      // null = not persisted
      totalCost      Float     @default(0)          // accumulated Bedrock cost (USD)
      totalInput     Int       @default(0)          // accumulated input tokens
      totalOutput    Int       @default(0)          // accumulated output tokens
      messageCount   Int       @default(0)          // number of user messages
      createdAt      DateTime  @default(now())
      lastActivity   DateTime  @default(now())
      externalRepos  ExternalRepoAccess[]
      @@index([userId])
    }

    model Deployment {
      id              String    @id @default(cuid())
      name            String    @unique              // "gemmbot", "claude-bot-bijan", etc.
      authMode        AuthMode                       // single_user or multi_user

      // Single-user config
      singleUserId    String?                        // for single_user mode
      singleUser      User?     @relation(fields: [singleUserId], references: [id])

      // Infrastructure
      workspacesDir   String                         // /opt/{name}/workspaces/
      reposDir        String    @default("/opt/gemmbot/repos")
      baseDomain      String                         // *.name.gemmaanalytics.com

      // Feature flags (stored as JSON)
      features        String    @default("{}")       // { email: true, vpn: false, ... }

      // Docker resource limits
      dockerMemory    String    @default("4g")
      dockerCpus      String    @default("2")

      // Cleanup config
      idleTimeoutBusinessHours  Int     @default(24)
      warning1HoursRemaining    Int     @default(12)
      warning2HoursRemaining    Int     @default(4)
      maxPersistDays            Int     @default(30)
      defaultPersistDays        Int     @default(7)

      // Secret dropoff config
      secretDropoffPort Int?                         // null = disabled

      // GitHub App config (multi_user mode only)
      githubAppId           String?                  // encrypted
      githubAppInstallId    String?                  // encrypted
      gitUserName           String   @default("Gemmbot")
      gitUserEmail          String   @default("gemmbot[bot]@users.noreply.github.com")

      // Status
      isActive        Boolean   @default(true)
      createdAt       DateTime  @default(now())
      updatedAt       DateTime  @updatedAt
    }

    enum AuthMode {
      SINGLE_USER
      MULTI_USER
    }

    model ExternalRepoAccess {
      id              String     @id @default(cuid())
      sessionId       String
      session         BotSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
      installationId  String               // GitHub App installation ID
      repoFullName    String               // e.g. "client-org/their-repo"
      createdAt       DateTime   @default(now())
    }

    // NextAuth required models
    model Account { ... }       // standard NextAuth Prisma adapter
    model AuthSession { ... }   // standard NextAuth Prisma adapter
    model VerificationToken { ... }

Changes from previous plan:
  - Removed slackBotToken/slackAppToken from Deployment (now in .env)
  - Added model, provider, summary, cost/token/message tracking to BotSession
  - Added githubToken (encrypted PAT) to User for single_user mode
  - Added claudeTokenExpiresAt to User


4. Feature Flag System
----------------------

File: bot/src/features.js

Features are stored as JSON in the Deployment.features column in the DB,
configured via the admin UI:

    {
      "email": true|false,          // mutt/msmtp email sending
      "vpn": true|false,            // WireGuard vpn-exec
      "servePages": true|false,     // Dev servers + Caddy routing
      "secretDropoff": true|false,  // request-secret CLI tool
      "selfImprove": true|false     // Can Claude PR improvements to its own repo
    }

On bot startup, features are loaded from the DB for the current deployment.
The bot identifies its deployment by DEPLOYMENT_NAME env var (the one
piece of config that must stay in the thin .env file).

Feature flags are enforced at TWO levels:

  a) Code level (bot/src/features.js):
     - isEnabled('email') checks before exposing capabilities
     - Controls what gets mounted into Docker containers
     - Controls what env vars are passed to Claude subprocess
     - Controls what binaries are available in the container

  b) CLAUDE.md level (bot/src/workspace.js):
     - The SYSTEM_PROMPT_BASE.md has conditional sections
     - buildClaudeMd() only includes sections for enabled features
     - If email is disabled, Claude never sees email instructions

Bijan's deployment:  All features enabled.
Gemmbot deployment:  email=false, vpn=false, rest=true (at launch).
New deployments:     Configured via the admin UI on creation.


5. Docker Container Architecture
---------------------------------

5a. Dockerfile (docker/Dockerfile):

    FROM ubuntu:22.04
    RUN apt-get update && apt-get install -y \
        nodejs npm git curl jq python3 \
        mutt msmtp \
        && npm install -g @anthropic-ai/claude-code \
        && apt-get clean
    RUN useradd -m -s /bin/bash claude-session
    COPY entrypoint.sh /entrypoint.sh
    RUN chmod +x /entrypoint.sh
    ENTRYPOINT ["/entrypoint.sh"]

    docker/entrypoint.sh:
      #!/bin/bash
      # Configure git credentials if GITHUB_TOKEN is set
      if [ -n "$GITHUB_TOKEN" ]; then
        git config --global credential.helper store
        echo "https://x-access-token:${GITHUB_TOKEN}@github.com" \
          > /home/claude-session/.git-credentials
        git config --global user.name "${GIT_USER_NAME:-Gemmbot}"
        git config --global user.email "${GIT_USER_EMAIL:-gemmbot[bot]@users.noreply.github.com}"
      fi
      exec sleep infinity

    The container stays alive. Claude is invoked via "docker exec".
    Git credentials are set up once on container start, not per message.

5b. Container lifecycle (bot/src/docker.js):

    Session start (first message in a thread):
      → docker create \
          --name gemmbot-{threadId} \
          --network host \
          --memory 4g \
          --cpus 2 \
          -v /opt/gemmbot/workspaces/{threadId}:/workspace \
          -v /opt/gemmbot/repos:/workspace/repos:ro \
          -e GITHUB_TOKEN=... \
          -e CLAUDE_CODE_OAUTH_TOKEN=... (user's own token) \
          -e SECRET_DROPOFF_API=http://127.0.0.1:7899 \
          -e GIT_USER_NAME=... \
          -e GIT_USER_EMAIL=... \
          gemmbot-session
      → docker start gemmbot-{threadId}

    Each message:
      → docker exec -i gemmbot-{threadId} \
          env ANTHROPIC_MODEL={model_id} \
              ANTHROPIC_SMALL_FAST_MODEL={haiku_id} \
              CLAUDE_CODE_USE_BEDROCK={0|1} \
              CLAUDE_SESSION_PROVIDER={direct|bedrock} \
              AWS_REGION=... AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... \
          claude --print --output-format stream-json \
          --dangerously-skip-permissions --verbose \
          --resume {sessionId}

      Note: Model/provider env vars are set per-message via docker exec
      (not at container creation) because users can switch models/providers
      mid-session. Static env vars (GITHUB_TOKEN, CLAUDE_CODE_OAUTH_TOKEN,
      git config, SECRET_DROPOFF_API) are set at container creation.

    Session release / cleanup:
      → docker kill gemmbot-{threadId}
      → docker rm gemmbot-{threadId}
      → rm -rf /opt/gemmbot/workspaces/{threadId}
      → Remove Caddy routes

    Feature-based mounts:
      If FEATURE_EMAIL=true:  mount msmtp config
      If FEATURE_VPN=true:    mount vpn-exec, WireGuard config
      If feature disabled:    binary not in container, credentials not passed

5c. Dev server traffic flow:

    User's browser
      → HTTPS to abc123.gemmbot.gemmaanalytics.com
      → Caddy (on host, port 443, terminates TLS)
      → Looks up dynamic route: abc123 → localhost:34567
      → localhost:34567 reaches the container (--network host)
      → Dev server process inside container responds
      → Response flows back through Caddy to browser

    Because of --network host, container ports ARE host ports.
    No Docker port mapping needed. Caddy config unchanged from current.

5d. Process registration:

    Claude (inside container) starts dev server, writes .processes.json
      → File is at /workspace/.processes.json (container path)
      → Volume mount makes it /opt/gemmbot/workspaces/{threadId}/.processes.json (host)
      → Bot reads it from the host side after Claude finishes
      → Bot calls Caddy admin API to create reverse proxy route
      → Bot posts URL in Slack thread

    Liveness checks (bot/src/docker.js):
      For processes inside containers, check via:
        docker exec gemmbot-{threadId} kill -0 {pid} 2>/dev/null
      For Docker-in-Docker containers:
        docker inspect from the host (container names are globally visible
        with --network host)
      Bot reads .processes.json from the host-side volume mount, filters
      to alive-only, and updates the file.

    Route restoration (on bot startup + on session resume):
      On startup: iterate all sessions, read .processes.json, re-register
      Caddy routes for any live processes with ports.
      On resume: before processing a message, check for live processes
      and ensure their Caddy routes exist (they may have been lost if
      the bot restarted between messages).

5e. Single-user mode:

    AUTH_MODE=single_user uses Docker containers identically to multi_user.
    Same container lifecycle, same volume mounts, same execution path.
    Differences are config only: all feature flags enabled, personal
    CLAUDE.md template, higher resource limits if desired.
    One codebase, one execution path, two configurations.


6. Multi-User Auth & Thread Ownership
--------------------------------------

6a. User authentication (bot/src/app.js):

    Every Slack event → extract event.user (Slack user ID)
      → Query DB: User where slackUserId = event.user AND isActive = true
      → If not found: ignore silently (unregistered user)
      → If found: pass user object to message handler

    In single_user mode: skip DB lookup, use hardcoded user from SLACK_USER_ID.

6b. Thread ownership (bot/src/ownership.js):

    Multi_user mode only. In single_user mode, ownership is implicit
    (all threads belong to the single user).

    On message in a thread:
      1. Look up BotSession by threadTs
      2. If no session exists → this user claims the thread (create BotSession)
      3. If session exists AND session.userId === current user → proceed
      4. If session exists AND session.userId !== current user →
         Reply: "This thread is owned by {name}. Start a new thread
                 or ask them to release their session."
         Return (do not process message).

    "release" command (multi_user mode):
      → Only the thread owner can release
      → Full cleanup: kill container, remove Caddy routes, delete
        workspace, delete BotSession from DB
      → Posts confirmation: "Session released. This thread is now available."
      → Another user can now start a new session in the same thread.

    In single_user mode, "release" also performs full cleanup (kill
    container, routes, workspace, session) — same action, just no
    ownership transfer semantics.


7. Session Cleanup System
-------------------------

File: bot/src/cleanup.js

Applies to ALL deployment modes (single_user and multi_user).

7a. Business hours definition:

    Business hours: Monday-Friday, 09:00-17:00 Berlin time (Europe/Berlin)
    1 business day = 8 business hours
    Timer only ticks during business hours.

7b. Idle session cleanup:

    Budget: 24 business hours (3 business days) of inactivity.

    Every 15 minutes, the bot checks all sessions:
      → Calculate business hours elapsed since lastActivity
      → At 12 business hours remaining (= 12h idle): send first warning
           "Your session in #{channel} has been idle. It will be cleaned up
            in ~12 business hours. Reply 'keep' to reset or 'release' to
            clean up now."
      → At 4 business hours remaining (= 20h idle): send final warning
           "Your session will be cleaned up in ~4 business hours.
            Reply 'keep' to reset or 'release' to clean up now."
      → At 0 remaining (= 24h idle): full cleanup
           Kill container, remove routes, delete workspace, delete session,
           notify user.

    "keep" reply → resets lastActivity to now (full 24 business hours again)
    "release" reply → immediate full cleanup

7c. Persisted sessions:

    "persist 3d" → sets persistUntil = now + 3 calendar days
    "persist 2w" → sets persistUntil = now + 2 weeks
    "persist"    → defaults to 7 calendar days (from Deployment.defaultPersistDays)
    Maximum: 30 calendar days (from Deployment.maxPersistDays).
    Can re-persist to extend (resets from now, capped at max).
    No permanent persistence option.

    Persisted sessions skip the idle timer.
    Warning at 56 business hours (7 business days) before persistUntil.
    At persistUntil: standard full cleanup.

7d. Cleanup actions (shared by release, idle timeout, and persist expiry):

    Full cleanup always performs ALL of these steps:
    1. docker kill {containerName}
    2. docker rm {containerName}
    3. Remove all Caddy routes for this session's subdomain
    4. Delete workspace directory
    5. Delete BotSession from database (cascades ExternalRepoAccess)
    6. Post notification in the original Slack thread

    For multi_user sessions with ExternalRepoAccess records:
    7. DM user: "Should I keep or remove access to {repos}?"
       → "remove" → call GitHub API to remove app installation from repos
       → "keep" → leave installation, delete tracking records
       → No response within 24h → auto-remove


8. Daily Server Audit
---------------------

File: bot/src/audit.js

Separate from the session cleanup system. The server audit monitors
running dev servers (processes with ports) and shuts down unacknowledged
ones to prevent resource leaks.

8a. Schedule:

    Runs at 8:00 AM Berlin time on business days (Mon-Fri).
    Checked via a 5-minute interval timer.

8b. Audit flow:

    Step 1 — Process previous audit results:
      If there was a previous audit with unacknowledged servers:
        → Kill each unacknowledged server (process or Docker container)
        → Remove its Caddy routes
        → Notify in the original thread
        → DM user with summary of shutdowns

    Step 2 — Collect currently active servers:
      Iterate all sessions, read .processes.json, find processes with ports.
      Build a list: server name, URL, thread, container/pid.

    Step 3 — DM the user (single_user) or each affected user (multi_user):
      "Daily server audit — N active server(s):
       1. frontend dev server → https://dev-abc123.gemmbot.gemmaanalytics.com
       2. API server → https://dev-abc123.gemmbot.gemmaanalytics.com
       Reply with numbers to keep (e.g. 'keep 1, 3'). Servers not mentioned
       will be shut down tomorrow morning."

    Step 4 — Save audit state to disk (DATA_DIR/server-audit.json):
      { servers: [...], kept: [], sentAt, messageTs, channel }

8c. "keep" reply handling:

    When user replies "keep 1, 3" or "keep all":
      → Parse numbers, record in audit state
      → Confirm: "Got it! Keeping N server(s): ..."
      → Unkept servers will be shut down next morning.


9. Slack Commands
-----------------

File: bot/src/commands.js

All commands are available in both single_user and multi_user modes
unless noted otherwise.

    release
      Full cleanup: kill container, remove Caddy routes, delete workspace,
      delete session. In multi_user mode, also releases thread ownership.
      Not available mid-processing (must wait for Claude to finish).

    stop
      Kills the running Claude process for this thread. Does NOT clean up
      the session — the user can send another message to continue.

    persist [duration]
      Sets a persist timer on the workspace. Duration format: "3d", "2w".
      Default: 7 days. Maximum: 30 days. Persisted sessions skip the
      idle cleanup timer but expire when persistUntil is reached.

    status
      Shows running processes, dev server URLs, current model, and
      current provider.

    model [name]
      Without argument: shows current model and available options.
      With argument: switches the model for this thread.
      Available: opus, sonnet, haiku.
      Model is stored per-session and takes effect on the next message.

    provider [name]
      Without argument: shows current provider.
      With argument: switches between "cc" (Claude Code subscription)
      and "bedrock" (AWS Bedrock).
      Bedrock requires AWS credentials to be configured.
      Provider is stored per-session and takes effect on the next message.

    cost
      Shows session usage: message count, input/output tokens.
      For Bedrock sessions: shows estimated cost in USD.
      For subscription sessions: shows usage only (flat pricing).

    workspaces [delete N]
      Without argument: lists all workspaces for the current user with:
        - Summary (first user message, linked to thread)
        - Disk size
        - Model
        - Last activity age
        - Expiry (days left or persist date)
      In multi_user mode: users see only their own workspaces.
      Admins see all workspaces.
      In single_user mode: shows all workspaces.
      "workspaces delete N" deletes workspace N (full cleanup).
      Cannot delete the current thread's workspace.

    keep [numbers]
      Reply to daily server audit DMs. "keep 1, 3" or "keep all".
      Only meaningful in the context of an active server audit.


10. Model & Provider Switching
------------------------------

File: bot/src/config.js (model catalog), bot/src/commands.js (handlers)

Available in BOTH single_user and multi_user modes.

10a. Model catalog:

    MODEL_CATALOG = {
      opus:   { label: "Opus",   bedrock: "eu.anthropic.claude-opus-4-6-v1",              direct: "claude-opus-4-6" },
      sonnet: { label: "Sonnet", bedrock: "eu.anthropic.claude-sonnet-4-5-20250929-v1:0", direct: "claude-sonnet-4-5-20250929" },
      haiku:  { label: "Haiku",  bedrock: "eu.anthropic.claude-haiku-4-5-20251001-v1:0",  direct: "claude-haiku-4-5-20251001" },
    }

    Default model: opus
    Default provider: direct (Claude Code subscription)

10b. Per-session storage:

    Model and provider are stored in BotSession (DB) per thread.
    When the user types "model sonnet", the session record is updated.
    On the next message, the bot passes the correct model ID and provider
    env vars to docker exec.

10c. Provider switching:

    "provider cc" → CLAUDE_CODE_USE_BEDROCK=0, uses direct model IDs
    "provider bedrock" → CLAUDE_CODE_USE_BEDROCK=1, uses Bedrock model IDs,
      requires AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY

    AWS credentials come from the deployment's .env file. They are shared
    across all users in a deployment (Bedrock is an infrastructure-level
    resource, not per-user).

    The CLAUDE_SESSION_PROVIDER env var is also set so that vpn-exec can
    enforce the Bedrock requirement for client data access.

10d. Cost tracking:

    After each Claude invocation, the bot accumulates:
      - totalCost (from result.total_cost_usd, Bedrock only)
      - totalInput (input_tokens + cache_read + cache_creation)
      - totalOutput (output_tokens)
      - messageCount

    For Bedrock sessions, cost is shown after each message:
      "≈$0.0234 this message | ≈$0.1456 session total (approx.)"

    The "cost" command shows a full summary at any time.


11. Real-Time Progress Updates
------------------------------

File: bot/src/app.js (processMessage function)

When Claude is working, the bot posts a "thinking" message and updates
it in real-time as Claude uses different tools:

    "_Claude is thinking..._"
    → "_Claude is working (12s) — reading files..._"
    → "_Claude is working (25s) — running commands..._"
    → "_Claude is working (1m 3s) — editing files..._"

Tool name → human-readable description mapping:

    Read         → "reading files"
    Glob         → "searching for files"
    Grep         → "searching code"
    Edit         → "editing files"
    Write        → "writing files"
    Bash         → "running commands"
    WebSearch    → "searching the web"
    WebFetch     → "fetching a webpage"
    Task         → "delegating to a subagent"

The thinking message is deleted when Claude finishes and replaced with
the actual response.


12. Thread Context for Channel Mentions
---------------------------------------

File: bot/src/app.js (fetchThreadContext function)

When Claude is first @mentioned in an existing channel thread (not a DM),
the bot fetches the thread's conversation history and prepends it to the
prompt so Claude has context:

    [System: You were mentioned in an existing Slack thread. Here is the
    conversation so far for context:
    [User U123]: Can someone help with the API?
    [User U456]: I think the endpoint changed.
    --- End of thread context ---]

    {actual user message}

This only happens on the FIRST message in a thread (when no session
exists yet). Subsequent messages are part of the ongoing Claude session.


13. Management UI
-----------------

8a. Auth flow:

    1. User visits gemmbot.gemmaanalytics.com
    2. "Sign in with Google" → NextAuth → Google OAuth
       (restricted to @gemmaanalytics.com via hd parameter)
    3. On first sign-in: User record created automatically (role=USER)
    4. Redirect to /settings

13b. Pages:

    /settings (main page after login):
      - Display name (from Google, editable)
      - Slack User ID (text input, required to use the bot)
        Instructions: "Find your Slack ID in your Slack profile → ⋮ → Copy member ID"
      - Claude Code status: "Connected ✓ (expires Dec 2026)" or
        "Not connected — required to use the bot"
        "Connect Claude Code" button → triggers OAuth flow (section 15)
      - GitHub Personal Access Token (for single_user deployments):
        Status indicator + "Set token" button (password input, encrypted)
        Note: only shown if the user is assigned to a single_user deployment
      - Link to /settings/secrets and /settings/prompt

    /settings/secrets:
      - List of configured secrets (name + key only, never values)
        Each with a "Delete" button and "Last updated" timestamp
      - "Add secret" form: name (label), key (env var name), value (password input)
      - Optional: any secrets users want in their workspace
        (e.g. Snowflake creds, third-party API keys)
      - Note: Claude Code auth and GitHub tokens are handled separately

    /settings/prompt:
      - Textarea for system prompt additions
      - Preview of what will be appended to CLAUDE.md
      - Examples: "I work on the data pipeline team. I primarily use Python and dbt.
        My Snowflake role is ANALYST."

    /sessions (own sessions):
      - List of active sessions: thread link, created, last activity, container status
      - "Release session" button per session

    /admin (ADMIN role only):
      - User count, active session count, container count
      - List of all users: name, email, Slack ID, role, active status, last active
      - Toggle active/inactive per user
      - Promote/demote admin role
      - List of all active sessions with "Kill" button

    Note: /admin/deployments is deferred to post-v1. Deployments are
    managed manually via .env files and systemd for v1.

13c. API routes:

    Auth:
      POST /api/auth/[...nextauth]      NextAuth handlers

    User self-service:
      GET    /api/users/me               Own profile
      PATCH  /api/users/me               Update Slack ID, name, prompt addition
      GET    /api/secrets                 List own secrets (names only)
      POST   /api/secrets                 Add secret (encrypt + store)
      DELETE /api/secrets/[id]            Delete own secret
      GET    /api/sessions               List own active sessions
      DELETE /api/sessions/[id]           Release own session (calls bot admin API)

    Admin (role=ADMIN only):
      GET    /api/admin/users             List all users
      PATCH  /api/admin/users/[id]        Activate/deactivate, change role
      GET    /api/admin/sessions          List all sessions
      DELETE /api/admin/sessions/[id]     Kill any session

13d. Bot admin API (bot/src/app.js, localhost:7900):

    A minimal HTTP server on localhost only, for the UI to trigger bot actions:

      DELETE /sessions/{threadTs}         Kill a session (container + cleanup)
      GET    /sessions                    List active sessions with container status
      GET    /health                      Bot health check


14. CLAUDE.md Templating
------------------------

File: bot/src/workspace.js, function buildClaudeMd(user, deployment, features)

Base template (bot/templates/SYSTEM_PROMPT_BASE.md):
  - Slack output formatting (mrkdwn, emojis, no markdown headers, no nested code blocks)
  - File attachment handling (receiving via [Attached file:...], sending via [upload:...])
  - Working style (ask clarifying questions, break down tasks)
  - Response transparency (always summarize work, user can't see tool use)
  - Environment description (workspace, repos, tools)
  - Process management (.processes.json with port reuse rules, SSL cert patience)
  - Git/GitHub workflow (claude/ branch prefix, never push to main, never merge)
  - Secret handling (never output in Slack, use request-secret)
  - Cross-session learnings (commit to git repos, never save to local disk)
  - Placeholder: {{USER_NAME}}, {{USER_EMAIL}}, {{BASE_DOMAIN}}

Conditional sections (included only if feature enabled):
  - Email sending (FEATURE_EMAIL): mutt/msmtp instructions, @gemmaanalytics.com restriction
  - VPN access (FEATURE_VPN): vpn-exec usage, WireGuard namespace docs
  - Client data governance (FEATURE_VPN): Bedrock + VPN required for client data
  - Secret dropoff / request-secret (FEATURE_SECRET_DROPOFF)
  - Self-improvement (FEATURE_SELF_IMPROVE): PR instructions for system prompt, repos

Per-user sections (appended dynamically from Settings):
  - user.aboutUser — "About you" context, wrapped in <user_context> tags
  - user.modelInstructions — "Model instructions" directives, wrapped in <user_instructions> tags

Personal template (bot/templates/SYSTEM_PROMPT_PERSONAL.md):
  - Bijan-specific notes (CEO context, personal preferences)
  - Appended only in single_user mode for Bijan's deployment


15. Claude Code Authentication (Per-User OAuth)
-------------------------------------------------

Gemmbot uses Claude Code Teams subscriptions. Each user authenticates
with their own Claude account via OAuth. Tokens are stored encrypted
in the database and passed to containers via the CLAUDE_CODE_OAUTH_TOKEN
env var.

15a. Key technical facts (from reverse-engineering the CLI):

  IMPORTANT: These facts need to be verified via a spike BEFORE
  implementing this section. The CLI may have changed since the
  original research. See "Spike: Claude Code OAuth" in section 20.

  - CLAUDE_CODE_OAUTH_TOKEN env var: if set, Claude Code uses this
    access token directly — skips the login flow entirely.
  - `claude setup-token` command: may run the OAuth flow and produce
    a token (needs verification).
  - OAuth details:
      Client ID: 9d1c250a-e61b-44d9-88ed-5944d1962f5e
      Auth URL (Teams): https://claude.ai/oauth/authorize
      Token URL: https://platform.claude.com/v1/oauth/token
      Scopes: user:inference user:profile user:sessions:claude_code
      Flow: PKCE (code_verifier / code_challenge)

15b. V1 approach — manual token entry (simplified):

  If the spike shows that automated OAuth is complex or unreliable,
  v1 uses a simpler flow:

    1. User authenticates Claude Code on their own machine:
       `claude login` or `claude setup-token`
    2. User copies the token (from ~/.claude/.credentials.json or stdout)
    3. User pastes it into the management UI (settings page, password field)
    4. UI encrypts and stores it in the DB
    5. Bot decrypts and injects as CLAUDE_CODE_OAUTH_TOKEN at session start

  This is less polished but functional and requires zero reverse-engineering.
  Can be improved to automated flow post-v1.

15c. Automated OAuth flow (post-spike, if viable):

  Primary flow (reverse-proxy the callback):

    1. User clicks "Connect Claude Code" in the management UI
    2. Server spawns `claude setup-token` in a PTY
    3. Server detects the random callback port the CLI opened
       (via /proc/{pid}/net/tcp or parsing the auth URL from stdout)
    4. Server creates a temporary Caddy reverse proxy route:
         gemmbot.gemmaanalytics.com/auth/{unique-id}/callback
           → localhost:{port}/callback
    5. Server rewrites the authorization URL:
         replace redirect_uri=http://localhost:{port}/callback
         with    redirect_uri=https://gemmbot.gemmaanalytics.com/auth/{unique-id}/callback
    6. User is redirected to claude.ai → authorizes → callback flows
       through Caddy → to the CLI's local server
    7. CLI exchanges the authorization code for a 1-year token
    8. Server captures token from CLI stdout → encrypts → stores in DB
    9. Temporary Caddy route removed
    10. UI shows: "Claude Code connected ✓"

  Fallback flow (if Anthropic strictly validates redirect_uri):

    1. User clicks "Connect Claude Code" in the management UI
    2. Server spawns `claude setup-token` in a PTY
    3. Server extracts the "manual" auth URL from CLI output
    4. UI opens the auth URL in a popup/new tab
    5. User authorizes → sees auth code displayed on screen
    6. User pastes the code back into the management UI
    7. Server feeds code to CLI stdin / exchanges directly at token endpoint
    8. Server captures token → encrypts → stores in DB
    9. UI shows: "Claude Code connected ✓"

15d. Token lifecycle:

  - Tokens are expected to be long-lived (1 year based on setup-token)
  - Users re-authenticate at most once a year
  - Server tracks token expiry per user (User.claudeTokenExpiresAt)
  - 30 days before expiry: UI shows a warning banner + Slack DM
  - 7 days before expiry: daily Slack reminders
  - On expiry: user cannot start new sessions until re-authenticated

15e. Token storage:

  Stored as an encrypted Secret in the DB:
    - Key: CLAUDE_CODE_OAUTH_TOKEN
    - Value: encrypted with AES-256-GCM
    - Never displayed in the UI after storage
    - Decrypted at session start, passed as env var to container

15f. Management UI additions:

    /settings page gains a "Claude Code" section:
      - Status: "Connected ✓" (with expiry date) or "Not connected"
      - "Connect Claude Code" button → triggers OAuth flow (or manual entry for v1)
      - "Disconnect" button → deletes stored token
      - Expiry warning banner when < 30 days remaining

    New API routes:
      POST /api/auth/claude/start       Start the OAuth flow (spawns setup-token)
      GET  /api/auth/claude/status      Check flow progress (polling)
      POST /api/auth/claude/code        Submit manual auth code (fallback)
      POST /api/auth/claude/token       Submit token directly (v1 manual flow)
      DELETE /api/auth/claude           Disconnect / delete token

15g. Bot module: bot/src/claude-auth.js

    Handles the automated flow (post-spike):
      - Spawning `claude setup-token` in a PTY
      - Detecting the callback port from the spawned process
      - Creating/removing temporary Caddy routes for the callback
      - Rewriting the auth URL
      - Capturing the token from stdout
      - Communicating with the management UI via the admin API

    For v1 (manual flow), this module is minimal — just receives and
    encrypts tokens submitted via the management UI.


16. Secrets Lifecycle
---------------------

16a. Entry:
  User visits /settings/secrets in the Management UI.
  Enters key name (e.g. a custom API key) and value.
  Value sent over HTTPS to POST /api/secrets.
  Note: Claude Code OAuth tokens are handled separately (section 15),
  not through the general secrets UI.

16b. Encryption:
  API route encrypts value with AES-256-GCM using ENCRYPTION_KEY from env.
  Format: base64(12-byte-IV || ciphertext || 16-byte-authTag)
  Stored in Secret.encryptedValue.

16c. At session start:
  Bot reads user's secrets from DB → decrypts → passes as env vars to
  docker create (via -e flags). Secrets exist in the container's
  environment only. NOT written to disk in the workspace.
  Claude Code OAuth token also injected as CLAUDE_CODE_OAUTH_TOKEN.

16d. At session end:
  docker kill destroys the container. Env vars gone. Nothing persists.

16e. UI display:
  GET /api/secrets returns [ { id, name, key, updatedAt } ] only.
  Never returns values. One-way entry only.

16f. Rotation:
  User deletes old secret, adds new one. Next session gets new value.
  Running sessions keep old value until they end.


17. GitHub Setup
----------------

Two different GitHub access mechanisms depending on deployment mode:

17a. Multi-user mode: GitHub App

  Create a GitHub App called "Gemmbot" (not a user account — an App).
  This provides scoped, short-lived tokens and enables ephemeral access
  to external repos without manual PAT creation.

  App permissions:
    - Repository: contents (read & write), pull_requests (read & write),
      metadata (read)
    - No admin, no org-level write permissions

  Installation:
    - Install on the Gemma-Analytics org → automatic access to all
      internal repos
    - For external repos: users install the app on specific repos
      on-demand (see 17e below)

  Token lifecycle:
    - GitHub App installation tokens are short-lived (1 hour)
    - The bot refreshes tokens automatically using the App's private key
    - No long-lived PATs to rotate or manage

  Required credentials (stored in /etc/gemmbot/.env):
    - GITHUB_APP_PRIVATE_KEY_PATH — path to PEM file

  Token generation (bot/src/github.js):
    1. Bot creates a JWT signed with the App private key
    2. Calls GitHub API: POST /app/installations/{id}/access_tokens
       (App ID and Installation ID read from Deployment record in DB)
    3. Receives short-lived token scoped to installed repos
    4. Passes token to container via GITHUB_TOKEN env var
    5. Token refreshed automatically before expiry

17b. Single-user mode: Personal Access Token

  Users provide their own GitHub PAT via the management UI settings page.
  The token is encrypted and stored in User.githubToken.

  At session start:
    Bot reads user's githubToken from DB → decrypts → passes as
    GITHUB_TOKEN env var to the container.

  This is simpler and appropriate for single-user deployments where
  the user already has broad GitHub access.

17c. Branch protection (all Gemma-Analytics repos):
  - Require PR before merge on main
  - Require at least 1 approval
  - Require review from CODEOWNERS

17d. CODEOWNERS (in gemmbot repo AND optionally other repos):
  .github/CODEOWNERS:
    * @bijansoltani

  This ensures every PR — including ones the bot creates for
  self-improvement — requires Bijan's review before merge.

17e. Git config inside containers:
  Set via env vars at container creation:
    GIT_AUTHOR_NAME / GIT_COMMITTER_NAME → from Deployment.gitUserName
    GIT_AUTHOR_EMAIL / GIT_COMMITTER_EMAIL → from Deployment.gitUserEmail
  Token passed via GITHUB_TOKEN env var.

17f. Ephemeral access to external repos (multi_user mode only):

  When a user asks Gemmbot to work on a repo outside Gemma-Analytics:

    1. Bot checks if the GitHub App is installed on that repo
    2. If not installed, bot replies:
       "I don't have access to that repo. Install the Gemmbot GitHub App
        to grant me access: https://github.com/apps/gemmbot/installations/new
        Select the specific repo(s) you want me to access."
    3. User clicks link, selects repo, authorizes (standard GitHub flow)
    4. Bot now has access via the App installation — generates scoped token
    5. Bot clones and works on the repo normally

  On session cleanup, bot checks for external repo installations and
  prompts (see section 7d, step 7).

  NOTE: Ephemeral repo access is deferred to post-v1 (see section 20,
  Phase 8). For v1, users who need access to external repos can install
  the GitHub App manually and leave it installed.


18. Deployment
--------------

18a. Server directory structure:

    /opt/gemmbot/
    ├── repo/              # git clone of Gemma-Analytics/gemmbot
    ├── workspaces/        # Per-session workspace directories (Docker volumes)
    ├── repos/             # Shared repo cache (mounted read-only into containers)
    └── data/
        └── gemmbot.db     # SQLite database

    /etc/gemmbot/
    ├── .env               # Infrastructure config (see below)
    └── github-app.pem     # GitHub App private key (multi_user only)

18b. .env file (/etc/gemmbot/.env):

    # Which deployment am I?
    DEPLOYMENT_NAME=gemmbot

    # Slack (infrastructure-level, needed before DB is available)
    SLACK_BOT_TOKEN=xoxb-...
    SLACK_APP_TOKEN=xapp-...

    # Database (needed to read everything else)
    DATABASE_URL=file:/opt/gemmbot/data/gemmbot.db

    # Encryption key (needed to decrypt DB-stored secrets)
    ENCRYPTION_KEY=<64-hex-char-random-key>

    # Caddy admin API (infrastructure-level)
    CADDY_ADMIN_API=http://localhost:2019

    # GitHub App (PEM file lives on disk, IDs in DB)
    # Only needed for multi_user mode
    GITHUB_APP_PRIVATE_KEY_PATH=/etc/gemmbot/github-app.pem

    # Google OAuth (for management UI)
    GOOGLE_CLIENT_ID=...
    GOOGLE_CLIENT_SECRET=...
    NEXTAUTH_URL=https://gemmbot.gemmaanalytics.com
    NEXTAUTH_SECRET=<random-secret>

    # Docker image name
    DOCKER_IMAGE=gemmbot-session

    # Bot admin API
    BOT_ADMIN_PORT=7900

    # Secret dropoff
    SECRET_DROPOFF_PORT=7899

    # AWS Bedrock (optional, shared across all users)
    AWS_REGION=eu-central-1
    AWS_ACCESS_KEY_ID=...
    AWS_SECRET_ACCESS_KEY=...

    # Single-user mode only (skip DB auth lookup)
    # SLACK_USER_ID=U...

18c. What lives in the DB (Deployment model) vs. .env:

    DB (managed via admin UI):            .env (server config):
    ─────────────────────────             ──────────────────────
    Feature flags                         DEPLOYMENT_NAME
    Base domain                           SLACK_BOT_TOKEN / SLACK_APP_TOKEN
    Workspace/repos paths                 DATABASE_URL
    Docker memory/CPU limits              ENCRYPTION_KEY
    Cleanup timers                        CADDY_ADMIN_API
    Secret dropoff port                   GITHUB_APP_PRIVATE_KEY_PATH
    GitHub App ID + Install ID            Google OAuth credentials
    Git user name/email                   DOCKER_IMAGE
    Auth mode                             BOT_ADMIN_PORT / SECRET_DROPOFF_PORT
                                          AWS credentials
                                          SLACK_USER_ID (single_user only)

    On first startup, if no Deployment record exists for DEPLOYMENT_NAME,
    the bot creates one with sensible defaults. No manual seed needed.

18d. Caddy config:

    # Management UI (multi_user deployments)
    gemmbot.gemmaanalytics.com {
        reverse_proxy localhost:3100
    }

    # Dev server subdomains (dynamic routes added via admin API)
    # Requires wildcard DNS: *.gemmbot.gemmaanalytics.com → server IP

18e. Systemd services:

    gemmbot.service:
      ExecStart=/usr/bin/node /opt/gemmbot/repo/bot/src/app.js
      EnvironmentFile=/etc/gemmbot/.env
      User=claude
      Restart=on-failure

    gemmbot-ui.service:
      ExecStart=/usr/bin/node /opt/gemmbot/repo/ui/node_modules/.bin/next start -p 3100
      EnvironmentFile=/etc/gemmbot/.env
      User=claude
      Restart=on-failure

18f. CI/CD (GitHub Actions):

    deploy-bot.yml (triggered by changes to bot/**):
      → SSH to server
      → cd /opt/gemmbot/repo && git pull
      → cd bot && npm install --production
      → npx prisma generate
      → systemctl restart gemmbot

    deploy-ui.yml (triggered by changes to ui/**):
      → SSH to server
      → cd /opt/gemmbot/repo && git pull
      → cd ui && npm install && npx prisma migrate deploy && npm run build
      → systemctl restart gemmbot-ui

    build-image.yml (triggered by changes to docker/**):
      → SSH to server
      → cd /opt/gemmbot/repo/docker && docker build -t gemmbot-session .
      → Manual trigger only (no auto-rebuild)


19. Migration from Current Setup
---------------------------------

This is the ordered checklist for going live.

Phase 0 — Prerequisites (manual, one-time):

  [ ] Create Slack app "Gemmbot" in Gemma workspace
      - Enable Socket Mode, get App-Level Token (xapp-...)
      - Bot scopes: chat:write, files:read, files:write, im:read,
        im:write, im:history, app_mentions:read, channels:history, users:read
      - Event subscriptions: message.im, app_mention
      - Install to workspace, get Bot Token (xoxb-...)

  [ ] Create "Gemmbot" GitHub App
      - Register at https://github.com/organizations/Gemma-Analytics/settings/apps/new
      - Permissions: contents (r/w), pull_requests (r/w), metadata (read)
      - Generate private key (PEM file)
      - Install on Gemma-Analytics org (all repos)
      - Note the App ID and Installation ID
      - Set up branch protection + CODEOWNERS on gemmbot repo

  [ ] Set up Google Cloud OAuth
      - Create project or use existing Gemma project
      - OAuth consent screen: internal, gemmaanalytics.com
      - Create OAuth 2.0 Client ID (web app)
      - Redirect URI: https://gemmbot.gemmaanalytics.com/api/auth/callback/google

  [ ] DNS records
      - gemmbot.gemmaanalytics.com → server IP (A record)
      - *.gemmbot.gemmaanalytics.com → server IP (wildcard A record)

  [ ] Generate encryption key: openssl rand -hex 32

Phase 1 — Server setup:

  [ ] Create directory structure
      mkdir -p /opt/gemmbot/{workspaces,repos,data}
      mkdir -p /etc/gemmbot

  [ ] Clone repo
      cd /opt/gemmbot
      git clone https://github.com/Gemma-Analytics/gemmbot.git repo

  [ ] Write /etc/gemmbot/.env with all values

  [ ] Install dependencies
      cd /opt/gemmbot/repo/bot && npm install --production
      cd /opt/gemmbot/repo/ui && npm install

  [ ] Run database migrations
      cd /opt/gemmbot/repo/ui && npx prisma migrate deploy

  [ ] Build UI
      cd /opt/gemmbot/repo/ui && npm run build

  [ ] Build Docker image
      cd /opt/gemmbot/repo/docker && docker build -t gemmbot-session .

  [ ] Install systemd services
      cp deploy/gemmbot.service /etc/systemd/system/
      cp deploy/gemmbot-ui.service /etc/systemd/system/
      systemctl daemon-reload
      systemctl enable gemmbot gemmbot-ui

  [ ] Update Caddy config
      Add gemmbot.gemmaanalytics.com block
      systemctl restart caddy

  [ ] Start services
      systemctl start gemmbot gemmbot-ui

Phase 2 — Initial config:

  [ ] Bijan signs into gemmbot.gemmaanalytics.com via Google
  [ ] Set Bijan as ADMIN:
      sqlite3 /opt/gemmbot/data/gemmbot.db \
        "UPDATE User SET role='ADMIN' WHERE email='bijan.soltani@gemmaanalytics.com';"
  [ ] Bijan enters Slack ID in UI
  [ ] Bijan clicks "Connect Claude Code" → completes OAuth / manual token flow
  [ ] Bijan tests: DM the Gemmbot in Slack

Phase 3 — Migrate personal bot:

  [ ] Point Bijan's claude-bot.service at the new repo
      (same code, different env: AUTH_MODE=single_user, all features enabled)
  [ ] Test personal bot still works as before
  [ ] Archive old bijans-playground/slack-claude-bot directory

Phase 4 — Rollout:

  [ ] Announce to company with link to gemmbot.gemmaanalytics.com
  [ ] Employees sign in, link Slack ID, connect Claude Code (OAuth)
  [ ] Employees DM or @mention Gemmbot in Slack


20. Implementation Phases & Sequence
-------------------------------------

Spike (do first, before any implementation):

  Spike: Claude Code OAuth
    - Run `claude setup-token` (or `claude --help`) inside a Docker
      container to verify the command exists and works as expected
    - Test CLAUDE_CODE_OAUTH_TOKEN env var injection
    - Determine if the reverse-proxy callback approach works or if
      redirect_uri validation is strict
    - Outcome: choose between automated OAuth flow, manual code flow,
      or simple token paste for v1
    - This spike informs how much of section 15 to implement

Phase 1 — Foundation:
  1. Set up monorepo structure (bot/ + ui/ + docker/)
  2. Prisma schema + initial migration
  3. config.js with feature flags, AUTH_MODE, model catalog
  4. crypto.js (AES-256-GCM, shared between bot and UI)

Phase 2 — Management UI:
  5. NextAuth + Google OAuth setup
  6. Settings page (Slack ID, profile, GitHub token)
  7. Secrets management (add/delete, encrypted storage)
  8. System prompt editor
  9. Admin dashboard (user list, role management, session list)
  10. Claude Code connection (manual token entry for v1, or automated if spike succeeds)

Phase 3 — Bot core (modularize existing bot.js):
  11. app.js: Slack Bolt setup, event routing, bot admin API
  12. session.js: session lifecycle (create, resume, release)
  13. commands.js: all command handlers (release, stop, persist, status,
      model, provider, cost, workspaces, keep)
  14. workspace.js: workspace creation, CLAUDE.md templating, repos symlink
  15. files.js: Slack file download + upload tag extraction
  16. features.js: feature flag system
  17. Replace sessions.json with Prisma-backed sessions
  18. Verify personal bot (single_user mode) works with new structure

Phase 4 — Docker isolation:
  19. Dockerfile + entrypoint (git credential setup, sleep infinity)
  20. docker.js: container lifecycle (create, exec, kill, liveness checks)
  21. claude.js: all Claude execution via docker exec (both modes)
  22. Feature-based volume mounts and env var filtering
  23. Per-message model/provider env var injection via docker exec
  24. Process registration: read .processes.json from host volume mount
  25. Caddy route restoration on startup + on resume

Phase 5 — Multi-user:
  26. Multi-user auth (DB lookup by Slack user ID)
  27. Thread ownership (ownership.js) — claim, check, release
  28. Per-user CLAUDE.md generation
  29. Per-user secrets + Claude Code token injection via env vars
  30. github.js: GitHub App token generation + refresh (multi_user mode)
  31. Business-hours cleanup system (cleanup.js)
  32. Daily server audit (audit.js)

Phase 6 — Claude Code OAuth (scope depends on spike):
  33. If spike shows automated flow works:
      - claude-auth.js: spawn setup-token in PTY, detect callback port
      - Temporary Caddy route for OAuth callback proxy
      - Auth URL rewriting and token capture
      - Fallback manual code flow
  34. If spike shows it doesn't work:
      - Simple token paste in management UI (already built in Phase 2)
      - Skip claude-auth.js entirely for v1
  35. Token expiry tracking + warning notifications (Slack DM + UI banner)

Phase 7 — Integration & deployment:
  36. Systemd service files (gemmbot, claude-bot, gemmbot-ui)
  37. CI/CD workflows (deploy-bot, deploy-ui, build-image)
  38. setup.sh provisioning script
  39. CODEOWNERS + branch protection

Phase 8 — Ephemeral repo access (post-v1):
  40. Ephemeral repo access flow (detect missing access, prompt install)
  41. ExternalRepoAccess tracking in DB
  42. Cleanup prompt (keep or remove external repo access)

Phase 9 — Polish & rollout:
  43. Write SYSTEM_PROMPT_BASE.md (from current SYSTEM_PROMPT.md)
  44. Write SYSTEM_PROMPT_PERSONAL.md (Bijan's additions)
  45. Migrate personal bot to new codebase
  46. End-to-end testing with 2nd user
  47. README / onboarding guide
  48. Company announcement


21. Single-User vs Multi-User: Complete Comparison
---------------------------------------------------

This section explicitly documents every behavioral difference between
the two modes to prevent ambiguity during implementation.

    Feature/Behavior              single_user              multi_user
    ────────────────              ───────────              ──────────
    User auth                     SLACK_USER_ID from .env  DB lookup by Slack ID
    Unrecognized user             Silently ignored         Silently ignored
    Thread ownership              Implicit (all threads    Explicit claim/release
                                  belong to single user)   per thread
    "release" command             Full cleanup             Full cleanup + release
                                  (no ownership change)    thread ownership
    Management UI                 Optional (single user    Required (user onboarding,
                                  can use just .env)       secrets, Claude Code auth)
    GitHub access                 Personal Access Token    GitHub App (short-lived)
                                  (from User.githubToken   (from Deployment github*
                                  in DB or management UI)  fields in DB)
    Claude Code auth              Token in DB or .env      Token in DB (per user)
    Secrets                       From DB (user's secrets) From DB (user's secrets)
    Feature flags                 From Deployment record   From Deployment record
    Model/provider switching      Per-thread, same as      Per-thread, same as
                                  multi_user               single_user
    Cost tracking                 Per-session, same        Per-session, same
    Cleanup (idle sessions)       Business-hours timer     Business-hours timer
                                  (same as multi_user)     (same as single_user)
    Daily server audit            DM to single user        DM to each affected user
    Persist                       Time-limited, same       Time-limited, same
    Workspaces command            Shows all workspaces     Shows user's own
                                                           (admins see all)
    Docker container              Same (per-session)       Same (per-session)
    CLAUDE.md template            Base + personal          Base + per-user addition
    Welcome message               Same                     Same
    Progress updates              Same                     Same
    Thread context (mentions)     Same                     Same
    File attachments              Same                     Same
    Secret dropoff                Same (per session)       Same (per session)
    Caddy routing                 Same                     Same
    Admin UI                      Not needed               Required
    AWS Bedrock                   From .env (shared)       From .env (shared)

Implementation note: the code should branch on AUTH_MODE as little as
possible. Most logic is shared. The key branch points are:
  1. User authentication (app.js): DB lookup vs. hardcoded user
  2. Thread ownership (ownership.js): enforced vs. skipped
  3. GitHub token source (docker.js): User.githubToken vs. GitHub App
  4. Workspaces visibility (commands.js): all vs. user-filtered
  5. Daily audit DM target (audit.js): single user vs. per-user
  6. CLAUDE.md personal section (workspace.js): appended vs. not


22. Reference Codebase
-----------------------

The existing single-user bot that this project is based on lives at:

    Repo:  Gemma-Analytics/bijans-playground
    Path:  slack-claude-bot/

Key files to study before implementing:

    src/bot.js           (1,707 lines) Main bot — Slack event handling, session
                         management, Claude CLI spawning, file handling, cleanup,
                         audit, model/provider switching, cost tracking, workspaces
                         command, progress updates. Primary source to modularize.

    src/caddy.js         (202 lines) Dynamic reverse proxy via Caddy admin API.
                         Reuse as-is. Includes PUT-by-ID dedup, stale route
                         cleanup, SSL cert pre-warming.

    src/secret-dropoff.js (347 lines) Two-server secure credential provisioning.
                         Reuse as-is.

    bin/request-secret   (104 lines) CLI tool for Claude to request secrets from
                         users. Reuse as-is.

    bin/vpn-exec         (32 lines) VPN command wrapper with Bedrock enforcement.
                         Reuse as-is.

    bin/vpn-ns           (146 lines) WireGuard namespace management.
                         Reuse as-is.

    SYSTEM_PROMPT.md     (237 lines) The current CLAUDE.md template. Adapt into
                         SYSTEM_PROMPT_BASE.md + SYSTEM_PROMPT_PERSONAL.md with
                         per-user placeholders and conditional feature sections.

    deploy/setup.sh      Server provisioning script. Reference for new setup.sh.

    deploy/claude-bot.service  Systemd unit. Reference for new service files.

    deploy/auth-headless.md    Documents the manual OAuth SSH tunnel flow that
                               section 15 (Claude Code OAuth) automates.

    .env.example         Environment variable reference.

Important patterns in bot.js to preserve:

    - Lines 474-612: Claude CLI spawning with env var injection, stream-JSON
      parsing, tool progress callbacks, session ID capture, cost accumulation
    - Lines 614-636: Slack Bolt setup, bot user ID detection
    - Lines 638-718: File download/upload, prompt building with attachments
    - Lines 679-707: Thread context fetching for channel @mentions
    - Lines 720-758: Stop command (kill process group)
    - Lines 831-937: Model and provider switching with catalog lookup
    - Lines 939-989: Cost tracking and display
    - Lines 992-1193: Workspaces command (list, delete, summaries, permalinks)
    - Lines 1196-1216: Welcome message with session info
    - Lines 1219-1515: Main processMessage flow (thinking indicator, workspace
      setup, repos symlink, file handling, process context injection, Caddy
      route registration on resume, progress updates, Claude invocation,
      result posting with chunking, upload tag extraction, cost display)
    - Lines 1518-1546: Event handlers (app_mention + message, audit reply)
    - Lines 1637-1662: Caddy route restoration on startup
    - Lines 1667-1691: Secret dropoff server startup + Slack notification wiring
    - Lines 1694-1698: Cleanup scheduling + server audit scheduling
    - activeThreads Map: prevents concurrent invocations per thread
    - threadToSubdomain: SHA-256 hash for dev server URLs


23. Risks & Mitigations
------------------------

SQLite concurrency:
  Both bot and UI write to the same SQLite file. Mitigate with WAL mode
  (PRAGMA journal_mode=WAL). At 20 users, write contention is minimal.
  If it becomes an issue, migrate to Postgres (one-line Prisma change).

Thread ownership race condition:
  Two users message an unowned thread simultaneously. Mitigate with a
  mutex/lock in ownership.js that serializes the claim check + DB write
  per threadTs.

Docker image staleness:
  Claude Code CLI updates won't automatically reach the container image.
  Rebuild manually via CI/CD when needed.

Port collisions:
  Two containers could bind the same random port (--network host).
  Low probability with random 10000-60000 range. The CLAUDE.md already
  instructs checking port availability before binding.

Container resource exhaustion:
  Memory limit (4g) and CPU limit (2 cores) per container prevent one
  session from starving others. Monitor with docker stats.

Secret key rotation:
  If ENCRYPTION_KEY is rotated, all existing secrets become unreadable.
  Document this. A migration script would decrypt-with-old, encrypt-with-new.

Claude Code OAuth — unknown CLI behavior:
  The `claude setup-token` command and CLAUDE_CODE_OAUTH_TOKEN env var
  behavior was reverse-engineered and may have changed. The spike in
  section 20 addresses this. Worst case, users paste tokens manually —
  functional but less polished.

Claude Code OAuth token expiry:
  If tokens expire and users don't re-authenticate, they can't start
  sessions. Mitigate with proactive warnings (30 days, 7 days, daily)
  via Slack DM and UI banner.

Bare-metal → Docker migration:
  The current bot runs Claude as a direct child process. Moving to
  Docker containers changes: env var injection, PATH setup, file access,
  process tracking (PIDs are container-local), VPN binary availability.
  Each of these is addressed in the plan but needs careful testing,
  especially for single_user mode where everything works today.
