Files
Mortdecai/docs/superpowers/plans/2026-03-22-oracle-bot.md
Seth 5b28002001 0.6.0 training session: Oracle Bot, RL combat, Mind's Eye, multilingual pipeline
Major changes from this session:

Training:
- 0.6.0 training running: 9B on steel141 3090 Ti, 27B on rented H100 NVL
- 7,256 merged training examples (up from 3,183)
- New training data: failure modes (85), midloop messaging (27),
  prompt injection defense (29), personality (32), gold from quarantine
  bank (232), new tool examples (30), claude's own experience (10)
- All training data RCON-validated at 100% pass rate
- Bake-off: gemma3:27b 66%, qwen3.5:27b 61%, translategemma:27b 56%

Oracle Bot (Mind's Eye):
- Invisible spectator bot (mineflayer) streams world state via WebSocket
- HTML5 Canvas frontend at mind.mortdec.ai
- Real-time tool trace visualization with expandable entries
- Streaming model tokens during inference
- Gateway integration: fire-and-forget POST /trace on every tool call

Reinforcement Learning:
- Gymnasium environment wrapping mineflayer bot (minecraft_env.py)
- PPO training via Stable Baselines3 (10K param policy network)
- Behavioral cloning pretraining (97.5% accuracy on expert policy)
- Infinite training loop with auto-restart and checkpoint resume
- Bot learns combat, survival, navigation from raw experience

Bot Army:
- 8-soldier marching formation with autonomous combat
- Combat bots using mineflayer-pvp, pathfinder, armor-manager
- Multilingual prayer bots via translategemma:27b (18 languages)
- Frame-based AI architecture: LLM planner + reactive micro-scripts

Infrastructure:
- Fixed mattpc.sethpc.xyz billing gateway (API key + player list parser)
- Billing gateway now tracks all LAN traffic (LAN auto-auth)
- Gateway fallback for empty god-mode responses
- Updated mortdec.ai landing page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:22:50 -04:00

866 lines
25 KiB
Markdown

# 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"
```