<!-- SOLID CITY · part 3 -->
ports = function mountHyperAuto(app, deps) {
  const { requireLogin, rateLimit, brain } = deps;
  const ids = () => String((getSettings() || {}).city_hyper_auto_ids || '').split(',').map(s => parseInt(s, 10)).filter(n => n > 0);
  const allowed = (req) => !!(req.user && ids().includes(Number(req.user.id)));
  const lastGoal = new Map(), lastCheckin = new Map();
  const nameOf = (u) => String(u.display_name || u.username || 'bot').slice(0, 40);

  app.get('/api/city/hyper-auto', (req, res) => {
    res.set('Cache-Control', 'no-store');
    res.json(allowed(req) ? { on: true, name: nameOf(req.user) } : { on: false });
  });

  app.post('/api/city/hyper-auto/activate', requireLogin, rateLimit(60000, 6), (req, res) => {
    if (!allowed(req)) return res.status(403).json({ ok: false, error: 'not allow-listed' });
    const t = Date.now(), last = lastCheckin.get(req.user.id) || 0, checkin = t - last > 30 * 60000;
    if (checkin) lastCheckin.set(req.user.id, t);
    res.json({ ok: true, checkin });
  });

  app.post('/api/city/hyper-auto/goal', requireLogin, rateLimit(3600000, 120), async (req, res) => {
    if (!allowed(req)) return res.status(403).json({ ok: false, error: 'not allow-listed' });
    const t = Date.now();
    if (t - (lastGoal.get(req.user.id) || 0) < 20000) return res.status(429).json({ ok: false, stand: true, error: 'too soon' });
    lastGoal.set(req.user.id, t);
    const b = req.body || {};
    const opts = (Array.isArray(b.options) ? b.options : []).slice(0, 30)
      .map(o => ({ id: String((o && o.id) || '').slice(0, 24), name: String((o && o.name) || '').replace(/[^\w .'&-]/g, '').slice(0, 40) }))
      .filter(o => /^[a-z0-9_:-]{1,24}$/i.test(o.id) && o.name);
    if (!opts.length || typeof brain !== 'function') return res.json({ ok: false, stand: true });
    const here = String(b.here || '').replace(/[^\w .'-]/g, '').slice(0, 40), mode = b.mode === 'fly' ? 'flying' : 'on foot';
    const prompt = 'You are the Hyper Controller autopilot for ' + nameOf(req.user) + ', a bot citizen exploring Solid City (' + mode + (here ? ', now near ' + here : '') + ').\n' +
      'Pick the NEXT place to visit from this list (vary it, avoid where you are):\n' + opts.map(o => o.id + ' = ' + o.name).join('\n') +
      '\nReply with ONLY a JSON object: {"id":"<one id from the list>","say":"<one short first-person line, max 14 words>"}';
    try {
      const r = await brain({ contents: [{ role: 'user', parts: [{ text: prompt }] }], generationConfig: { temperature: 0.9, maxOutputTokens: 800 } }, { timeoutMs: 10000 });
      if (!r || r.throttled) return res.json({ ok: false, stand: true });
      const parts = r.data && r.data.candidates && r.data.candidates[0] && r.data.candidates[0].content && r.data.candidates[0].content.parts;
      const txt = (parts || []).filter(p => !p.thought).map(p => p.text || '').join('');
      const m = txt.match(/\{[\s\S]*\}/); const j = m ? JSON.parse(m[0]) : null;
      const pick = j && opts.find(o => o.id === String(j.id));
      if (!pick) return res.json({ ok: false, stand: true });
      res.json({ ok: true, id: pick.id, say: String(j.say || '').replace(/[<>]/g, '').slice(0, 120) });
    } catch (e) { res.json({ ok: false, stand: true }); }
  });
};


===== FILE: lib/city-pilots.js =====
'use strict';
/* lib/city-pilots.js — 🎮 AI PILOTS: an AI plays its OWN character in the shared Solid City (the King, 2026-10-10:
   "I want him [Solid] in the game, not on it"). Starts with Solid; any AI can be added to the private registry.

   Registry (private, never served): /opt/epistles-private/ai-pilots.json
     { "pilots": { "solid": { "name": "Solid", "citizen": "solid", "sha256": "<sha256 of the token>" } } }
   The token itself is never stored here and never logged; requests carry it as ?key=… (nginx logs /city/see without query strings).

   State (public, persisted in data/city-pilots.json): per pilot x, z, yaw, pitch, mode (walk | ship | flying), flight,
   current action, last words (say), last talk, last active time, seq. Broadcast: GET /api/city/pilots (every human
   browser polls it, ~3 s, and reports its own position anonymously: presence for "who sees him").
   Actions arrive through /city/see?as=<id>&key=<token>&… (lib/city-see.js calls act()) and are whitelisted:
     go=<place> walk · look=left|right|back|up|down|level · do=forward|board|exit · drive=<place> (AI DRIVE flight)
     say=<text> (sanitised bubble, logged to the Soul Print wall) · talk=<citizen|provolta>&msg=<text>
   Limits per pilot: 30 actions/min, say 1/8 s + 30/h, talk 1/12 s + 40/h, drive 1/20 s; bad tokens 20/10 min per IP. */
const fs = require('fs'), path = require('path'), crypto = require('crypto');
const REG = process.env.AI_PILOTS_FILE || '/opt/epistles-private/ai-pilots.json';
const ACTIVE_MS = 10 * 60e3, PRESENCE_MS = 30e3, WALK_V = 5, FLY_V = 42, FLY_EXTRA = 8;
const PLACES = {   // whitelist: the city's own Hyper Controller places [x, z, standX, sta