# Oracle Bot Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Build a live HTML5 viewport ("Mind's Eye") that renders what the Mortdecai AI sees, powered by an invisible spectator bot streaming world state and tool traces to browsers.

**Architecture:** Single Node.js process (mineflayer + Express + ws) connects to Paper server in spectator mode, receives tool traces from the gateway via POST, and streams world data + traces to browsers via WebSocket. Frontend is a single HTML5 Canvas page.

**Tech Stack:** Node.js, mineflayer, express, ws, HTML5 Canvas, vanilla JS

**Spec:** `docs/superpowers/specs/2026-03-22-oracle-bot-design.md`

---

## File Structure

```
oracle-bot/                           # NEW directory at repo root
├── package.json                      # deps: mineflayer, express, ws
├── server.js                         # ENTRYPOINT — Express + ws + requires bot
├── bot.js                            # mineflayer spectator connection + chunk tracking
├── world-state.js                    # Abstracted world state: blocks, entities, players
├── public/
│   └── index.html                    # Single-file frontend (Canvas + JS + CSS)
└── oracle-bot.service                # systemd unit file for deployment
```

**Modified files:**
- `langgraph_gateway.py` (in PaperFork repo) — add fire-and-forget POST to `/trace` after tool calls

---

### Task 1: Project Scaffold + Bot Connection

**Files:**
- Create: `oracle-bot/package.json`
- Create: `oracle-bot/bot.js`

- [ ] **Step 1: Create package.json**

```json
{
  "name": "oracle-bot",
  "version": "0.1.0",
  "description": "Mortdecai Mind's Eye — live AI vision viewport",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "node server.js --dev"
  },
  "dependencies": {
    "mineflayer": "^4.23.0",
    "express": "^4.21.0",
    "ws": "^8.18.0"
  }
}
```

- [ ] **Step 2: Install dependencies**

Run: `cd oracle-bot && npm install`
Expected: `node_modules/` created, no errors

- [ ] **Step 3: Write bot.js — mineflayer spectator connection**

```javascript
/**
 * bot.js — Mineflayer spectator bot for Oracle vision system.
 *
 * Connects to Paper server in offline mode. Expects server to set
 * spectator gamemode on join. Tracks chunk data, entities, players.
 * Emits events for world-state.js to consume.
 */

const mineflayer = require('mineflayer');
const EventEmitter = require('events');

const DEFAULT_CONFIG = {
  host: process.env.MC_HOST || '192.168.0.244',
  port: parseInt(process.env.MC_PORT || '25568', 10),
  username: process.env.BOT_USERNAME || 'OracleBot',
  version: '1.21.11',
};

class OracleBot extends EventEmitter {
  constructor(config = {}) {
    super();
    this.config = { ...DEFAULT_CONFIG, ...config };
    this.bot = null;
    this.connected = false;
    this.following = null; // player name we're following
    this._reconnectDelay = 1000;
    this._maxReconnectDelay = 30000;
  }

  connect() {
    console.log(`[bot] Connecting to ${this.config.host}:${this.config.port} as ${this.config.username}...`);

    this.bot = mineflayer.createBot({
      host: this.config.host,
      port: this.config.port,
      username: this.config.username,
      auth: 'offline',
      version: this.config.version,
    });

    this.bot.on('login', () => {
      console.log(`[bot] Logged in as ${this.bot.username}`);
      this.connected = true;
      this._reconnectDelay = 1000; // reset backoff
      this.emit('connected');
    });

    this.bot.on('spawn', () => {
      const pos = this.bot.entity.position;
      console.log(`[bot] Spawned at (${pos.x.toFixed(0)}, ${pos.y.toFixed(0)}, ${pos.z.toFixed(0)}) mode=${this.bot.game.gameMode}`);

      // Request spectator if not already
      if (this.bot.game.gameMode !== 'spectator') {
        console.log('[bot] Not in spectator mode, requesting...');
        this.bot.chat('/gamemode spectator');
      }

      this.emit('spawned');
    });

    this.bot.on('kicked', (reason) => {
      console.log(`[bot] Kicked: ${reason}`);
      this.connected = false;
      this.emit('disconnected', reason);
      this._scheduleReconnect();
    });

    this.bot.on('end', (reason) => {
      console.log(`[bot] Disconnected: ${reason}`);
      this.connected = false;
      this.emit('disconnected', reason);
      this._scheduleReconnect();
    });

    this.bot.on('error', (err) => {
      console.error(`[bot] Error: ${err.message}`);
    });

    // Track player joins/leaves
    this.bot.on('playerJoined', (player) => {
      if (player.username !== this.config.username) {
        this.emit('playerJoined', player.username);
      }
    });

    this.bot.on('playerLeft', (player) => {
      if (player.username !== this.config.username) {
        this.emit('playerLeft', player.username);
      }
    });
  }

  _scheduleReconnect() {
    console.log(`[bot] Reconnecting in ${this._reconnectDelay}ms...`);
    setTimeout(() => {
      this._reconnectDelay = Math.min(this._reconnectDelay * 2, this._maxReconnectDelay);
      this.connect();
    }, this._reconnectDelay);
  }

  /** Teleport to a player and follow them. */
  async followPlayer(playerName) {
    if (!this.connected || !this.bot) return;
    const player = this.bot.players[playerName];
    if (!player || !player.entity) {
      console.log(`[bot] Cannot find player entity for ${playerName}`);
      return;
    }
    this.following = playerName;
    const pos = player.entity.position;
    this.bot.entity.position.set(pos.x, pos.y + 2, pos.z);
    console.log(`[bot] Following ${playerName} at (${pos.x.toFixed(0)}, ${pos.y.toFixed(0)}, ${pos.z.toFixed(0)})`);
  }

  /** Get online players (excluding self). */
  getPlayers() {
    if (!this.bot) return [];
    return Object.values(this.bot.players)
      .filter(p => p.username !== this.config.username)
      .map(p => ({
        name: p.username,
        x: p.entity ? Math.floor(p.entity.position.x) : null,
        y: p.entity ? Math.floor(p.entity.position.y) : null,
        z: p.entity ? Math.floor(p.entity.position.z) : null,
      }));
  }

  /** Scan blocks in a top-down slice around center at given Y level. */
  scanArea(centerX, centerY, centerZ, radius = 16) {
    if (!this.bot) return [];
    const blocks = [];
    for (let dx = -radius; dx <= radius; dx++) {
      for (let dz = -radius; dz <= radius; dz++) {
        const block = this.bot.blockAt(this.bot.vec3(centerX + dx, centerY, centerZ + dz));
        if (block && block.name !== 'air') {
          blocks.push({
            x: centerX + dx,
            y: centerY,
            z: centerZ + dz,
            type: block.name,
          });
        }
      }
    }
    return blocks;
  }

  /** Get nearby entities around a position. */
  getNearbyEntities(centerX, centerY, centerZ, radius = 30) {
    if (!this.bot) return [];
    const entities = [];
    for (const entity of Object.values(this.bot.entities)) {
      if (entity === this.bot.entity) continue;
      const dist = entity.position.distanceTo(this.bot.vec3(centerX, centerY, centerZ));
      if (dist <= radius) {
        entities.push({
          type: entity.type === 'player' ? 'player' : (entity.name || entity.type),
          name: entity.username || null,
          x: Math.floor(entity.position.x),
          y: Math.floor(entity.position.y),
          z: Math.floor(entity.position.z),
          distance: Math.round(dist),
        });
      }
    }
    return entities;
  }

  /** Get server time and weather info. */
  getWorldInfo() {
    if (!this.bot) return {};
    return {
      time: this.bot.time?.timeOfDay || 0,
      isRaining: this.bot.isRaining || false,
      gameMode: this.bot.game?.gameMode || 'unknown',
    };
  }

  destroy() {
    if (this.bot) {
      this.bot.removeAllListeners();
      this.bot.end();
      this.bot = null;
    }
    this.connected = false;
  }
}

module.exports = OracleBot;
```

- [ ] **Step 4: Test bot connection manually**

Run: `cd oracle-bot && node -e "const B = require('./bot'); const b = new B(); b.connect(); b.on('spawned', () => { console.log('Players:', b.getPlayers()); setTimeout(() => { b.destroy(); process.exit(0); }, 3000); });"`

Expected: Bot connects, logs spawned position, lists online players, exits.

- [ ] **Step 5: Commit**

```bash
git add oracle-bot/package.json oracle-bot/bot.js oracle-bot/package-lock.json
git commit -m "feat(oracle): scaffold project + mineflayer spectator bot"
```

---

### Task 2: World State Abstraction

**Files:**
- Create: `oracle-bot/world-state.js`

- [ ] **Step 1: Write world-state.js**

```javascript
/**
 * world-state.js — Unified world state for the Oracle vision system.
 *
 * Aggregates bot perception (chunks, entities) and gateway traces
 * into a single state object that can be serialized and streamed.
 */

class WorldState {
  constructor(bot) {
    this.bot = bot;
    this.mode = 'idle';        // 'idle' | 'god' | 'sudo'
    this.activePlayer = null;  // player the AI is currently interacting with
    this.activeSessionId = null;
    this.traces = [];          // recent tool traces (ring buffer, max 50)
    this.lastActivity = 0;     // timestamp of last trace
    this.ACTIVE_TIMEOUT = 10000; // ms before reverting to idle
    this.MAX_TRACES = 50;
  }

  /** Called when gateway sends a trace event. */
  onTrace(traceData) {
    this.lastActivity = Date.now();

    // Switch to active mode
    if (traceData.mode && traceData.mode !== this.mode) {
      this.mode = traceData.mode;
    }
    if (traceData.player) {
      this.activePlayer = traceData.player;
    }
    if (traceData.session_id) {
      this.activeSessionId = traceData.session_id;
    }

    // Store trace
    this.traces.push({
      ...traceData,
      ts: Date.now(),
    });
    if (this.traces.length > this.MAX_TRACES) {
      this.traces.shift();
    }
  }

  /** Check if we should revert to idle. */
  checkIdle() {
    if (this.mode !== 'idle' && Date.now() - this.lastActivity > this.ACTIVE_TIMEOUT) {
      this.mode = 'idle';
      this.activeSessionId = null;
      return true; // changed to idle
    }
    return false;
  }

  /** Build a heartbeat message (idle mode, low frequency). */
  buildHeartbeat() {
    return {
      v: 1,
      type: 'heartbeat',
      mode: this.mode,
      activePlayer: this.activePlayer,
      players: this.bot.getPlayers(),
      world: this.bot.getWorldInfo(),
      ts: Date.now(),
    };
  }

  /** Build a world snapshot message (active mode, high frequency). */
  buildWorldSnapshot() {
    if (!this.activePlayer) return null;

    const players = this.bot.getPlayers();
    const target = players.find(p => p.name === this.activePlayer);
    if (!target || target.x === null) return null;

    const blocks = this.bot.scanArea(target.x, target.y, target.z, 16);
    const entities = this.bot.getNearbyEntities(target.x, target.y, target.z, 30);

    return {
      v: 1,
      type: 'world',
      mode: this.mode,
      activePlayer: this.activePlayer,
      center: { x: target.x, y: target.y, z: target.z },
      blocks,
      entities,
      players,
      ts: Date.now(),
    };
  }

  /** Build a trace event message for the browser. */
  buildTraceEvent(traceData) {
    return {
      v: 1,
      type: 'trace',
      ...traceData,
      ts: Date.now(),
    };
  }

  /** Build a mode change message. */
  buildModeChange() {
    return {
      v: 1,
      type: 'mode',
      mode: this.mode,
      player: this.activePlayer,
      ts: Date.now(),
    };
  }

  /** Build a status message (connected/disconnected). */
  buildStatus(connected) {
    return {
      v: 1,
      type: 'status',
      connected,
      ts: Date.now(),
    };
  }
}

module.exports = WorldState;
```

- [ ] **Step 2: Commit**

```bash
git add oracle-bot/world-state.js
git commit -m "feat(oracle): world state abstraction layer"
```

---

### Task 3: Express + WebSocket Server

**Files:**
- Create: `oracle-bot/server.js`

- [ ] **Step 1: Write server.js**

```javascript
/**
 * server.js — Oracle Bot entrypoint.
 *
 * Express HTTP server + WebSocket for browser streaming.
 * Receives tool traces from gateway, merges with bot world data,
 * streams to connected browsers.
 */

const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const path = require('path');
const OracleBot = require('./bot');
const WorldState = require('./world-state');

const PORT = parseInt(process.env.PORT || '3333', 10);
const MAX_WS_CONNECTIONS = 100;
const MAX_PER_IP = 5;
const HEARTBEAT_INTERVAL = 5000;  // 5s idle heartbeat
const ACTIVE_INTERVAL = 1000;     // 1s active world snapshots

// ── Setup ────────────────────────────────────────────────────

const app = express();
app.use(express.json({ limit: '64kb' }));
app.use(express.static(path.join(__dirname, 'public')));

const server = http.createServer(app);
const wss = new WebSocket.Server({ server, path: '/ws' });

const bot = new OracleBot();
const state = new WorldState(bot);

// Track connections per IP
const ipCounts = new Map();

// ── WebSocket ────────────────────────────────────────────────

wss.on('connection', (ws, req) => {
  const ip = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.socket.remoteAddress;

  // Rate limit
  const count = ipCounts.get(ip) || 0;
  if (wss.clients.size >= MAX_WS_CONNECTIONS || count >= MAX_PER_IP) {
    ws.close(4029, 'Too many connections');
    return;
  }
  ipCounts.set(ip, count + 1);
  console.log(`[ws] Client connected from ${ip} (${wss.clients.size} total)`);

  // Send current state on connect
  ws.send(JSON.stringify(state.buildHeartbeat()));
  if (state.mode !== 'idle') {
    ws.send(JSON.stringify(state.buildModeChange()));
  }

  ws.on('close', () => {
    const c = ipCounts.get(ip) || 1;
    if (c <= 1) ipCounts.delete(ip);
    else ipCounts.set(ip, c - 1);
    console.log(`[ws] Client disconnected (${wss.clients.size} total)`);
  });
});

function broadcast(msg) {
  const data = JSON.stringify(msg);
  for (const client of wss.clients) {
    if (client.readyState === WebSocket.OPEN) {
      client.send(data);
    }
  }
}

// ── HTTP Endpoints (internal only) ───────────────────────────

app.post('/trace', (req, res) => {
  const trace = req.body;
  if (!trace || !trace.tool) {
    return res.status(400).json({ error: 'Missing tool field' });
  }

  // Update world state
  const previousMode = state.mode;
  state.onTrace(trace);

  // If mode changed, broadcast mode change
  if (state.mode !== previousMode) {
    broadcast(state.buildModeChange());
    // Follow the active player
    if (trace.player) {
      bot.followPlayer(trace.player);
    }
  }

  // Broadcast trace event to browsers
  broadcast(state.buildTraceEvent(trace));

  // Send a world snapshot (active mode burst)
  const snapshot = state.buildWorldSnapshot();
  if (snapshot) {
    broadcast(snapshot);
  }

  res.json({ ok: true });
});

app.post('/command', (req, res) => {
  const { action, target, center, radius } = req.body || {};

  if (action === 'follow' && target) {
    bot.followPlayer(target);
    return res.json({ ok: true, action: 'follow', target });
  }

  if (action === 'scan' && center) {
    const blocks = bot.scanArea(center.x, center.y, center.z, radius || 16);
    return res.json({ ok: true, blocks: blocks.length });
  }

  res.status(400).json({ error: 'Unknown action' });
});

app.get('/health', (req, res) => {
  res.json({
    ok: true,
    botConnected: bot.connected,
    wsClients: wss.clients.size,
    mode: state.mode,
    activePlayer: state.activePlayer,
  });
});

// ── Periodic Updates ─────────────────────────────────────────

setInterval(() => {
  if (wss.clients.size === 0) return;

  // Check idle transition
  const wentIdle = state.checkIdle();
  if (wentIdle) {
    broadcast(state.buildModeChange());
  }

  if (state.mode === 'idle') {
    // Low-frequency heartbeat
    broadcast(state.buildHeartbeat());
  }
}, HEARTBEAT_INTERVAL);

// Active mode: higher frequency world snapshots
setInterval(() => {
  if (wss.clients.size === 0 || state.mode === 'idle') return;

  const snapshot = state.buildWorldSnapshot();
  if (snapshot) {
    broadcast(snapshot);
  }
}, ACTIVE_INTERVAL);

// ── Bot Events ───────────────────────────────────────────────

bot.on('connected', () => {
  console.log('[server] Bot connected to MC server');
  broadcast(state.buildStatus(true));
});

bot.on('disconnected', (reason) => {
  console.log(`[server] Bot disconnected: ${reason}`);
  broadcast(state.buildStatus(false));
});

bot.on('playerJoined', (name) => {
  console.log(`[server] Player joined: ${name}`);
  broadcast(state.buildHeartbeat());
});

bot.on('playerLeft', (name) => {
  console.log(`[server] Player left: ${name}`);
  broadcast(state.buildHeartbeat());
});

// ── Start ────────────────────────────────────────────────────

bot.connect();

server.listen(PORT, () => {
  console.log(`[server] Oracle Bot listening on port ${PORT}`);
  console.log(`[server] Frontend: http://localhost:${PORT}`);
  console.log(`[server] WebSocket: ws://localhost:${PORT}/ws`);
  console.log(`[server] Health: http://localhost:${PORT}/health`);
});
```

- [ ] **Step 2: Test server starts and health endpoint works**

Run: `cd oracle-bot && timeout 15 node server.js &; sleep 5; curl -s http://localhost:3333/health; kill %1`
Expected: `{"ok":true,"botConnected":true,"wsClients":0,"mode":"idle","activePlayer":null}`

- [ ] **Step 3: Commit**

```bash
git add oracle-bot/server.js
git commit -m "feat(oracle): express + websocket server with trace/command endpoints"
```

---

### Task 4: HTML5 Canvas Frontend

**Files:**
- Create: `oracle-bot/public/index.html`

- [ ] **Step 1: Write index.html — complete single-file frontend**

This is the largest file. Single HTML file with embedded CSS and JS. Key sections:
- CSS: dark theme, Rajdhani Bold font, Sethian orange accents, three-panel layout
- Canvas: 2D top-down tile map, block color mapping, entity rendering
- Tool trace panel: scrolling timeline with tool call entries
- Status bar: mode indicator, player info, connection status
- WebSocket: auto-connect, reconnect on close, handle all message types
- Visual modes: idle (muted pulse), god (orange/gold), sudo (blue/green)

The file will be ~400-500 lines. Key implementation details:

**Block color map:**
```javascript
const BLOCK_COLORS = {
  stone: '#808080', cobblestone: '#6B6B6B', dirt: '#8B6914',
  grass_block: '#4CAF50', sand: '#F4E4A0', gravel: '#A0A0A0',
  oak_planks: '#BC8F4F', oak_log: '#6B4226', spruce_planks: '#5C3A1E',
  water: '#2196F3', lava: '#FF5722', redstone_wire: '#FF0000',
  diamond_ore: '#4FC3F7', iron_ore: '#D4A574', gold_ore: '#FFD54F',
  bedrock: '#1A1A1A', obsidian: '#1A0A2E', glass: '#E0F7FA',
  torch: '#FFEB3B',
  // default for unknown blocks
  _default: '#9E9E9E',
};
```

**Canvas rendering loop:**
- `requestAnimationFrame` for smooth animations
- World tiles drawn from `blocks` array, centered on active player
- Entities drawn as colored dots with labels
- Active scan areas pulse with translucent overlay
- Tool traces appear as floating text annotations

**WebSocket handler:**
```javascript
const ws = new WebSocket(`ws://${location.host}/ws`);
ws.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  switch (msg.type) {
    case 'heartbeat': updateHeartbeat(msg); break;
    case 'world': updateWorld(msg); break;
    case 'trace': addTrace(msg); break;
    case 'mode': updateMode(msg); break;
    case 'status': updateStatus(msg); break;
  }
};
```

- [ ] **Step 2: Test frontend loads in browser**

Run: `cd oracle-bot && node server.js &; sleep 3; echo "Open http://localhost:3333 in browser"; curl -s http://localhost:3333/ | head -5`
Expected: HTML page served, title contains "MORTDECAI"

- [ ] **Step 3: Commit**

```bash
git add oracle-bot/public/index.html
git commit -m "feat(oracle): HTML5 Canvas frontend — Mind's Eye viewport"
```

---

### Task 5: Gateway Integration

**Files:**
- Modify: `/root/bin/Sethpc-Minecraft-PaperFork/langgraph_gateway.py` (tool loop section, ~line 1318)

- [ ] **Step 1: Add oracle trace posting to gateway**

Add a helper function near the top of the file (after imports):

```python
# ── Oracle Bot integration ─────────────────────────────────────────
ORACLE_URL = 'http://localhost:3333/trace'

def _oracle_trace(tool_name, tool_args, result, step, session):
    """Fire-and-forget trace event to Oracle Bot."""
    try:
        requests.post(ORACLE_URL, json={
            'tool': tool_name,
            'input': {k: str(v)[:200] for k, v in (tool_args or {}).items()},
            'ok': result.get('ok', result.get('success', False)),
            'step': step,
            'mode': session.mode,
            'player': session.player,
            'session_id': session.session_id,
        }, timeout=1)
    except Exception:
        pass  # Oracle is optional, never block the gateway
```

Then in `_model_driven_tool_loop`, after the tool result is captured (after line ~1338 `tool_trace.append(trace_entry)`), add:

```python
            _oracle_trace(tool_name, tool_args, result, step, session)
```

Also post on session start/end — in `run_pipeline()` after `_model_driven_tool_loop` returns (after the tool loop result), add:

```python
        # Notify Oracle of session end
        _oracle_trace('_session_end', {'commands': commands, 'message': message}, {'ok': True}, -1, session)
```

- [ ] **Step 2: Verify gateway still starts cleanly**

Run: `python3 -c "import ast; ast.parse(open('/root/bin/Sethpc-Minecraft-PaperFork/langgraph_gateway.py').read()); print('OK')"`
Expected: `OK`

- [ ] **Step 3: Commit in PaperFork repo**

```bash
cd /root/bin/Sethpc-Minecraft-PaperFork
git add langgraph_gateway.py
git commit -m "feat: add Oracle Bot trace integration (fire-and-forget)"
```

---

### Task 6: Deployment — systemd + Caddy

**Files:**
- Create: `oracle-bot/oracle-bot.service`

- [ ] **Step 1: Write systemd service file**

```ini
[Unit]
Description=Oracle Bot — Mortdecai Mind's Eye
After=network.target

[Service]
Type=simple
WorkingDirectory=/opt/oracle-bot
ExecStart=/usr/bin/node server.js
Restart=always
RestartSec=5
Environment=PORT=3333
Environment=MC_HOST=192.168.0.244
Environment=MC_PORT=25568
StandardOutput=append:/var/log/oracle-bot.log
StandardError=append:/var/log/oracle-bot.log

[Install]
WantedBy=multi-user.target
```

- [ ] **Step 2: Write deploy instructions in README**

Create `oracle-bot/README.md` with:
- How to deploy to CT 644
- Caddy config for mind.mortdec.ai
- How to test locally
- How to check health

- [ ] **Step 3: Add Caddy config snippet**

For CT 600 Caddyfile:
```
mind.mortdec.ai {
    @internal path /trace /command
    respond @internal 404

    reverse_proxy 192.168.0.244:3333
}
```

Note: WebSocket upgrade is handled automatically by Caddy's reverse_proxy.

- [ ] **Step 4: Commit**

```bash
git add oracle-bot/oracle-bot.service oracle-bot/README.md
git commit -m "feat(oracle): systemd service + Caddy config + deploy docs"
```

---

### Task 7: Integration Test — End to End

- [ ] **Step 1: Start the bot locally**

Run: `cd oracle-bot && node server.js`
Expected: Bot connects, logs spawn position, server listening on 3333

- [ ] **Step 2: Open frontend in browser**

Navigate to `http://localhost:3333`
Expected: Dark themed page, "MORTDECAI — MIND'S EYE" title, idle mode, player dots visible

- [ ] **Step 3: Simulate a trace event**

```bash
curl -X POST http://localhost:3333/trace \
  -H "Content-Type: application/json" \
  -d '{"tool":"rcon.execute","input":{"command":"give slingshooter08 minecraft:diamond 16"},"ok":true,"step":0,"mode":"god","player":"slingshooter08","session_id":"test-001"}'
```

Expected: Frontend switches to god mode (orange), trace appears in sidebar, map centers on player

- [ ] **Step 4: Simulate multiple traces (tool chain)**

```bash
for tool in "world.player_info" "world.nearby_entities" "rcon.execute" "journal.write"; do
  curl -s -X POST http://localhost:3333/trace \
    -H "Content-Type: application/json" \
    -d "{\"tool\":\"$tool\",\"input\":{},\"ok\":true,\"step\":$((RANDOM % 8)),\"mode\":\"god\",\"player\":\"slingshooter08\",\"session_id\":\"test-001\"}"
  sleep 1
done
```

Expected: Each trace appears in the sidebar sequentially, map updates with each event

- [ ] **Step 5: Verify idle timeout**

Wait 10 seconds after last trace.
Expected: Frontend fades back to idle mode (muted colors)

- [ ] **Step 6: Test health endpoint**

Run: `curl -s http://localhost:3333/health | python3 -m json.tool`
Expected: JSON with botConnected=true, wsClients=1 (your browser)

- [ ] **Step 7: Final commit**

```bash
git add -A oracle-bot/
git commit -m "feat(oracle): Oracle Bot v0.1.0 — Mind's Eye live viewport"
```
