/ keet-cli / sovereign-mesh.js
sovereign-mesh.js
   1  #!/usr/bin/env node
   2  /**
   3   * Sovereign Mesh - Unified P2P Context Sync
   4   *
   5   * The operator is the pilot. The mnemonic is their license number.
   6   *
   7   * ONE daemon that does everything:
   8   *   - HTTP relay (for iOS Shortcuts)
   9   *   - Nostr broadcast (encrypted public channel)
  10   *   - Hyperswarm mesh (peer-to-peer sync)
  11   *
  12   * All keys derive from the operator's mnemonic. Any machine with the
  13   * mnemonic joins the operator's mesh automatically.
  14   *
  15   * Usage:
  16   *   node sovereign-mesh.js --mnemonic "amber-falcon"           # Join with mnemonic
  17   *   node sovereign-mesh.js --mnemonic "amber-falcon" --http    # With HTTP relay
  18   *   node sovereign-mesh.js --name "mac-mini"                   # Set instance name
  19   *   node sovereign-mesh.js init                                # Generate new mnemonic
  20   */
  21  
  22  import Hyperswarm from 'hyperswarm'
  23  import crypto from 'crypto'
  24  import http from 'http'
  25  import fs from 'fs'
  26  import path from 'path'
  27  import os from 'os'
  28  import { finalizeEvent } from 'nostr-tools'
  29  import { Relay } from 'nostr-tools/relay'
  30  import * as bip39 from 'bip39'
  31  import { pbkdf2 } from '@noble/hashes/pbkdf2.js'
  32  import { sha256 } from '@noble/hashes/sha2.js'
  33  import { getMnemonicFromVault, vaultExists } from './sovereign-vault.js'
  34  
  35  // ═══════════════════════════════════════════════════════════════════════════
  36  // CONFIGURATION
  37  // ═══════════════════════════════════════════════════════════════════════════
  38  const HTTP_PORT = 7778  // 7777 used by hypercore_daemon
  39  const CONFIG_DIR = path.join(os.homedir(), '.sovereign')
  40  const MNEMONIC_FILE = path.join(CONFIG_DIR, 'operator-mnemonic.json')
  41  const STATE_FILE = path.join(CONFIG_DIR, 'mesh-state.json')  // Persistence file
  42  
  43  const RELAYS = ['wss://relay.damus.io', 'wss://nos.lol', 'wss://nostr.wine']
  44  const EVENT_KIND = 30078
  45  
  46  // ═══════════════════════════════════════════════════════════════════════════
  47  // BIP-39 MNEMONIC & KEY DERIVATION
  48  // Proper cryptographic security using industry standards
  49  // ═══════════════════════════════════════════════════════════════════════════
  50  
  51  // Security parameters
  52  const MNEMONIC_WORDS = 4           // 4 words = 44 bits entropy (17.6 trillion combinations)
  53  const PBKDF2_ITERATIONS = 2048     // Standard BIP-39 iteration count
  54  const ENTROPY_BITS = MNEMONIC_WORDS * 11  // 11 bits per BIP-39 word
  55  
  56  /**
  57   * Generate a new operator mnemonic using BIP-39.
  58   *
  59   * 4 words provides 44 bits of entropy:
  60   * - 2048^4 = 17.6 trillion combinations
  61   * - Brute force at 1M/sec = 200+ days
  62   * - Human-speakable and memorable
  63   *
  64   * For higher security, use 6 words (66 bits) or 12 words (128 bits).
  65   */
  66  function generateMnemonic(wordCount = MNEMONIC_WORDS) {
  67    // Generate full BIP-39 mnemonic and take first N words
  68    // This ensures proper entropy and checksum handling
  69    const fullMnemonic = bip39.generateMnemonic()
  70    const words = fullMnemonic.split(' ').slice(0, wordCount)
  71    return words.join('-')
  72  }
  73  
  74  /**
  75   * Validate a mnemonic against BIP-39 wordlist.
  76   */
  77  function validateMnemonic(mnemonic) {
  78    const words = mnemonic.replace(/-/g, ' ').split(' ')
  79    const wordlist = bip39.wordlists.english
  80    return words.every(word => wordlist.includes(word.toLowerCase()))
  81  }
  82  
  83  /**
  84   * Derive all keys from operator mnemonic using PBKDF2.
  85   *
  86   * PBKDF2-HMAC-SHA256 with 2048 iterations provides:
  87   * - Slow key derivation (hard to brute force)
  88   * - Deterministic output (same mnemonic = same keys)
  89   * - Industry standard (BIP-39 compatible)
  90   */
  91  function deriveKeysFromMnemonic(mnemonic) {
  92    const normalized = mnemonic.toLowerCase().trim().replace(/\s+/g, ' ')
  93    const mnemonicBytes = new TextEncoder().encode(normalized)
  94  
  95    // Derive master seed using PBKDF2 (BIP-39 standard)
  96    const masterSeed = pbkdf2(sha256, mnemonicBytes,
  97      new TextEncoder().encode('sovereign-mesh'),
  98      { c: PBKDF2_ITERATIONS, dkLen: 64 }
  99    )
 100  
 101    // Derive individual keys from master seed
 102    const sovereignKey = pbkdf2(sha256, masterSeed,
 103      new TextEncoder().encode('sovereign'),
 104      { c: 1, dkLen: 32 }
 105    )
 106  
 107    const meshTopic = pbkdf2(sha256, masterSeed,
 108      new TextEncoder().encode('mesh'),
 109      { c: 1, dkLen: 32 }
 110    )
 111  
 112    const nostrSecretKey = pbkdf2(sha256, masterSeed,
 113      new TextEncoder().encode('nostr'),
 114      { c: 1, dkLen: 32 }
 115    )
 116  
 117    // Identifier for tagging (not security-critical)
 118    const identifier = Buffer.from(sha256(mnemonicBytes)).toString('hex').slice(0, 16)
 119  
 120    return {
 121      sovereignKey: Buffer.from(sovereignKey),
 122      meshTopic: Buffer.from(meshTopic),
 123      nostrSecretKey: Buffer.from(nostrSecretKey),
 124      identifier,
 125      mnemonic: normalized.replace(/\s+/g, '-')
 126    }
 127  }
 128  
 129  async function loadOrCreateMnemonic() {
 130    fs.mkdirSync(CONFIG_DIR, { recursive: true })
 131  
 132    let vaultError = null
 133  
 134    // Priority 1: Check P2P vault (Autopass)
 135    if (vaultExists()) {
 136      try {
 137        const vaultMnemonic = await getMnemonicFromVault(true)  // throwOnError=true
 138        if (vaultMnemonic) {
 139          console.log('[mesh] Mnemonic loaded from vault')
 140          return vaultMnemonic
 141        }
 142      } catch (err) {
 143        vaultError = err.message
 144        if (err.message.includes('locked')) {
 145          console.log(`[vault] Warning: Vault is locked by another process`)
 146        } else {
 147          console.log(`[vault] Warning: Could not read vault (${err.message})`)
 148        }
 149      }
 150    }
 151  
 152    // Priority 2: Check local config file (JSON)
 153    // This is the fallback when vault is locked or unavailable
 154    if (fs.existsSync(MNEMONIC_FILE)) {
 155      try {
 156        const data = JSON.parse(fs.readFileSync(MNEMONIC_FILE, 'utf8'))
 157        if (data.mnemonic) {
 158          if (vaultError) {
 159            console.log('[mesh] Mnemonic loaded from JSON (vault unavailable)')
 160          } else {
 161            console.log('[mesh] Mnemonic loaded from local config')
 162          }
 163          return data.mnemonic
 164        }
 165      } catch (err) {
 166        console.log(`[mesh] Warning: Could not read mnemonic file: ${err.message}`)
 167      }
 168    }
 169  
 170    // Priority 3: Check for legacy key files
 171    // Only use legacy if neither vault nor JSON has a mnemonic
 172    const KEY_FILE = path.join(CONFIG_DIR, 'broadcast-key.json')
 173    if (fs.existsSync(KEY_FILE)) {
 174      console.log('[mesh] Warning: Using legacy keys (no mnemonic found)')
 175      console.log('[mesh] Run "node sovereign-vault.js init" to migrate to mnemonic')
 176      return null
 177    }
 178  
 179    return null
 180  }
 181  
 182  // ═══════════════════════════════════════════════════════════════════════════
 183  // ENCRYPTION
 184  // ═══════════════════════════════════════════════════════════════════════════
 185  function encrypt(plaintext, key) {
 186    const iv = crypto.randomBytes(12)
 187    const cipher = crypto.createCipheriv('aes-256-gcm', key, iv)
 188    let encrypted = cipher.update(plaintext, 'utf8', 'base64')
 189    encrypted += cipher.final('base64')
 190    const authTag = cipher.getAuthTag()
 191    return `${iv.toString('base64')}:${authTag.toString('base64')}:${encrypted}`
 192  }
 193  
 194  function decrypt(ciphertext, key) {
 195    const parts = ciphertext.split(':')
 196    if (parts.length !== 3) throw new Error('Invalid ciphertext')
 197    const iv = Buffer.from(parts[0], 'base64')
 198    const authTag = Buffer.from(parts[1], 'base64')
 199    const encrypted = parts[2]
 200    const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv)
 201    decipher.setAuthTag(authTag)
 202    let decrypted = decipher.update(encrypted, 'base64', 'utf8')
 203    decrypted += decipher.final('utf8')
 204    return decrypted
 205  }
 206  
 207  // ═══════════════════════════════════════════════════════════════════════════
 208  // SOVEREIGN MESH NODE
 209  // ═══════════════════════════════════════════════════════════════════════════
 210  class SovereignMesh {
 211    constructor(options = {}) {
 212      this.name = options.name || os.hostname()
 213      this.enableHttp = options.http || false
 214      this.mnemonic = options.mnemonic || null
 215  
 216      this.swarm = null
 217      this.peers = new Map()
 218      this.httpServer = null
 219  
 220      // Keys (derived from mnemonic or loaded from files)
 221      this.nostrConnections = []
 222      this.sovereignKey = null
 223      this.nostrSecretKey = null
 224      this.nostrPublicKey = null
 225      this.identifier = null
 226      this.meshTopic = null
 227  
 228      // Uptime tracking
 229      this.startTime = Date.now()
 230  
 231      // Deduplication
 232      this.seen = new Set()
 233      this.SEEN_TTL = 60000
 234  
 235      // Context storage for bootstrap (silence-led context buffer)
 236      this.foStates = new Map()        // nodeId -> FO state from each instance
 237      this.recentMessages = []         // Last N messages for context
 238      this.MAX_RECENT_MESSAGES = 50    // Keep last 50 messages
 239      this.FO_STATE_TTL = 300000       // FO states expire after 5 minutes
 240  
 241      // Aha Moments - high-importance insights (importance >= 0.7)
 242      // These persist longer and get priority in bootstrap
 243      this.ahaMoments = []             // Recent aha moments across all instances
 244      this.MAX_AHA_MOMENTS = 20        // Keep last 20 aha moments
 245      this.AHA_MOMENT_TTL = 3600000    // Aha moments persist for 1 hour
 246  
 247      // Convergence detection - when instances work on similar topics
 248      this.convergenceAlerts = []      // Active convergence alerts
 249      this.MAX_CONVERGENCE_ALERTS = 10
 250      this.CONVERGENCE_TTL = 1800000   // 30 minutes
 251  
 252      // Principle candidates - new principles propagated across mesh
 253      this.principleCandidates = []    // Active principle candidates
 254      this.MAX_PRINCIPLE_CANDIDATES = 20
 255      this.PRINCIPLE_CANDIDATE_TTL = 86400000  // 24 hours (candidates need time to be tested)
 256  
 257      // Confirmed principles - ready to be written to CLAUDE.md
 258      this.confirmedPrinciples = []    // Principles that passed testing
 259      this.MAX_CONFIRMED_PRINCIPLES = 50
 260      this.CONFIRMED_PRINCIPLE_TTL = 604800000  // 7 days (keep for a week)
 261  
 262      // Confirmation thresholds
 263      this.CONFIRM_MIN_VOTES = 5       // Minimum votes to confirm
 264      this.CONFIRM_SUPPORT_RATIO = 0.8 // 80% support to confirm
 265      this.CONFIRM_TESTING_HOURS = 24  // Hours in testing before auto-confirm at 70%
 266  
 267      // Persistence
 268      this.STATE_SAVE_INTERVAL = 30000 // Save state every 30 seconds
 269      this.lastStateSave = 0
 270    }
 271  
 272    // ═══════════════════════════════════════════════════════════════════════════
 273    // STATE PERSISTENCE
 274    // ═══════════════════════════════════════════════════════════════════════════
 275  
 276    /**
 277     * Save mesh state to disk for persistence across restarts.
 278     */
 279    saveState() {
 280      const state = {
 281        savedAt: new Date().toISOString(),
 282        version: 1,
 283  
 284        // Principle candidates (most important to persist)
 285        principleCandidates: this.principleCandidates,
 286  
 287        // Confirmed principles
 288        confirmedPrinciples: this.confirmedPrinciples,
 289  
 290        // Aha moments (persist recent ones)
 291        ahaMoments: this.ahaMoments,
 292  
 293        // Convergence alerts (short-lived, but useful)
 294        convergenceAlerts: this.convergenceAlerts,
 295  
 296        // Recent messages (limited persistence)
 297        recentMessages: this.recentMessages.slice(-20),
 298  
 299        // FO states (short-lived, but helps bootstrap)
 300        foStates: Array.from(this.foStates.entries())
 301      }
 302  
 303      try {
 304        fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2))
 305        console.log(`[mesh] 💾 State saved (${this.principleCandidates.length} candidates, ${this.confirmedPrinciples.length} confirmed)`)
 306      } catch (err) {
 307        console.log(`[mesh] ⚠ Failed to save state: ${err.message}`)
 308      }
 309    }
 310  
 311    /**
 312     * Load mesh state from disk on startup.
 313     */
 314    loadState() {
 315      if (!fs.existsSync(STATE_FILE)) {
 316        console.log('[mesh] No saved state found, starting fresh')
 317        return false
 318      }
 319  
 320      try {
 321        const data = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'))
 322        const savedAt = new Date(data.savedAt)
 323        const ageMinutes = (Date.now() - savedAt.getTime()) / 60000
 324  
 325        console.log(`[mesh] 📂 Loading saved state from ${savedAt.toISOString()} (${ageMinutes.toFixed(0)} min ago)`)
 326  
 327        // Restore principle candidates
 328        if (data.principleCandidates) {
 329          this.principleCandidates = data.principleCandidates
 330          console.log(`[mesh]    └─ ${this.principleCandidates.length} principle candidates`)
 331        }
 332  
 333        // Restore confirmed principles
 334        if (data.confirmedPrinciples) {
 335          this.confirmedPrinciples = data.confirmedPrinciples
 336          console.log(`[mesh]    └─ ${this.confirmedPrinciples.length} confirmed principles`)
 337        }
 338  
 339        // Restore aha moments (filter expired)
 340        if (data.ahaMoments) {
 341          const now = Date.now()
 342          this.ahaMoments = data.ahaMoments.filter(m =>
 343            now - m.receivedAt < this.AHA_MOMENT_TTL
 344          )
 345          console.log(`[mesh]    └─ ${this.ahaMoments.length} aha moments (${data.ahaMoments.length - this.ahaMoments.length} expired)`)
 346        }
 347  
 348        // Restore convergence alerts (filter expired)
 349        if (data.convergenceAlerts) {
 350          const now = Date.now()
 351          this.convergenceAlerts = data.convergenceAlerts.filter(a =>
 352            now - a.detectedAt < this.CONVERGENCE_TTL
 353          )
 354          console.log(`[mesh]    └─ ${this.convergenceAlerts.length} convergence alerts`)
 355        }
 356  
 357        // Restore recent messages
 358        if (data.recentMessages) {
 359          this.recentMessages = data.recentMessages
 360          console.log(`[mesh]    └─ ${this.recentMessages.length} recent messages`)
 361        }
 362  
 363        // Restore FO states (filter expired)
 364        if (data.foStates) {
 365          const now = Date.now()
 366          for (const [nodeId, state] of data.foStates) {
 367            if (now - state.receivedAt < this.FO_STATE_TTL) {
 368              this.foStates.set(nodeId, state)
 369            }
 370          }
 371          console.log(`[mesh]    └─ ${this.foStates.size} FO states`)
 372        }
 373  
 374        return true
 375      } catch (err) {
 376        console.log(`[mesh] ⚠ Failed to load state: ${err.message}`)
 377        return false
 378      }
 379    }
 380  
 381    /**
 382     * Periodic state save check.
 383     */
 384    checkStateSave() {
 385      const now = Date.now()
 386      if (now - this.lastStateSave > this.STATE_SAVE_INTERVAL) {
 387        this.saveState()
 388        this.lastStateSave = now
 389      }
 390    }
 391  
 392    async start() {
 393      console.log('╔══════════════════════════════════════════════════════════╗')
 394      console.log('║              SOVEREIGN MESH NODE                         ║')
 395      console.log('║         The operator is the pilot.                       ║')
 396      console.log('╚══════════════════════════════════════════════════════════╝')
 397      console.log()
 398      console.log(`[mesh] Instance: ${this.name}`)
 399  
 400      // Load saved state from disk (persistence across restarts)
 401      this.loadState()
 402  
 403      // Load or derive keys
 404      await this.loadKeys()
 405  
 406      if (this.meshTopic) {
 407        console.log(`[mesh] Topic: ${this.meshTopic.toString('hex').slice(0, 16)}...`)
 408      }
 409  
 410      // Start Hyperswarm
 411      await this.startSwarm()
 412  
 413      // Start Nostr listener
 414      await this.startNostr()
 415  
 416      // Start HTTP relay if requested
 417      if (this.enableHttp) {
 418        await this.startHttp()
 419      }
 420  
 421      // Start periodic state save (every 30 seconds)
 422      this.stateSaveInterval = setInterval(() => {
 423        this.checkStateSave()
 424      }, 10000)  // Check every 10s, actual save every 30s
 425  
 426      console.log()
 427      console.log('────────────────────────────────────────────────────────────')
 428      console.log('Mesh node active. State persisted every 30s. Ctrl+C to stop.')
 429      console.log('────────────────────────────────────────────────────────────')
 430    }
 431  
 432    async loadKeys() {
 433      // If mnemonic provided, derive all keys from it
 434      if (this.mnemonic) {
 435        const derived = deriveKeysFromMnemonic(this.mnemonic)
 436        this.sovereignKey = derived.sovereignKey
 437        this.meshTopic = derived.meshTopic
 438        this.nostrSecretKey = derived.nostrSecretKey
 439        this.identifier = derived.identifier
 440  
 441        // Mask mnemonic for display (first letter...last letter)
 442        const firstLetter = derived.mnemonic[0]
 443        const lastLetter = derived.mnemonic[derived.mnemonic.length - 1]
 444        console.log(`[mesh] Operator: ${firstLetter}****${lastLetter}`)
 445        console.log(`[mesh] Sovereign ID: ${this.identifier}`)
 446        return
 447      }
 448  
 449      // Try to load mnemonic from vault or file
 450      const savedMnemonic = await loadOrCreateMnemonic()
 451      if (savedMnemonic) {
 452        this.mnemonic = savedMnemonic
 453        const derived = deriveKeysFromMnemonic(savedMnemonic)
 454        this.sovereignKey = derived.sovereignKey
 455        this.meshTopic = derived.meshTopic
 456        this.nostrSecretKey = derived.nostrSecretKey
 457        this.identifier = derived.identifier
 458  
 459        // Mask mnemonic for display (first letter...last letter)
 460        const firstLetter = derived.mnemonic[0]
 461        const lastLetter = derived.mnemonic[derived.mnemonic.length - 1]
 462        console.log(`[mesh] Operator: ${firstLetter}****${lastLetter}`)
 463        console.log(`[mesh] Sovereign ID: ${this.identifier}`)
 464        return
 465      }
 466  
 467      // Legacy: try to load from old key files
 468      const KEY_FILE = path.join(CONFIG_DIR, 'broadcast-key.json')
 469      const NOSTR_KEY_FILE = path.join(CONFIG_DIR, 'nostr-identity.json')
 470  
 471      if (fs.existsSync(KEY_FILE) && fs.existsSync(NOSTR_KEY_FILE)) {
 472        const keyData = JSON.parse(fs.readFileSync(KEY_FILE, 'utf8'))
 473        this.sovereignKey = Buffer.from(keyData.key, 'hex')
 474        this.identifier = keyData.identifier
 475  
 476        const nostrData = JSON.parse(fs.readFileSync(NOSTR_KEY_FILE, 'utf8'))
 477        this.nostrSecretKey = Buffer.from(nostrData.secretKey, 'hex')
 478  
 479        // Use legacy topic
 480        this.meshTopic = crypto.createHash('sha256').update('sovereign-os-cerf-2026').digest()
 481  
 482        console.log(`[mesh] Legacy mode - Sovereign ID: ${this.identifier}`)
 483        return
 484      }
 485  
 486      console.log('[mesh] No operator mnemonic provided')
 487      console.log('[mesh] Run with --mnemonic "your-words" or use "init" to generate')
 488      console.log('[mesh] Running in receive-only mode')
 489  
 490      // Fallback topic for receive-only
 491      this.meshTopic = crypto.createHash('sha256').update('sovereign-os-public').digest()
 492    }
 493  
 494    async startSwarm() {
 495      this.swarm = new Hyperswarm()
 496  
 497      this.swarm.on('connection', (conn, info) => {
 498        const peerId = info.publicKey.toString('hex').slice(0, 8)
 499        console.log(`[mesh] ✓ Peer connected: ${peerId}`)
 500        this.peers.set(peerId, conn)
 501  
 502        conn.on('data', (data) => this.onPeerMessage(peerId, data))
 503        conn.on('close', () => {
 504          console.log(`[mesh] ✗ Peer disconnected: ${peerId}`)
 505          this.peers.delete(peerId)
 506        })
 507        conn.on('error', () => {})
 508      })
 509  
 510      const discovery = this.swarm.join(this.meshTopic, { client: true, server: true })
 511      await discovery.flushed()
 512      console.log(`[mesh] Joined swarm, discovering peers...`)
 513    }
 514  
 515    async startNostr() {
 516      if (!this.sovereignKey) return
 517  
 518      console.log('[nostr] Connecting to relays...')
 519      for (const url of RELAYS) {
 520        try {
 521          const relay = await Relay.connect(url)
 522          this.nostrConnections.push(relay)
 523  
 524          relay.subscribe([
 525            { kinds: [EVENT_KIND], authors: [this.nostrPublicKey], '#d': [this.identifier] }
 526          ], {
 527            onevent: (event) => this.onNostrEvent(event),
 528            oneose: () => {}
 529          })
 530          console.log(`[nostr] ✓ ${url}`)
 531        } catch {
 532          console.log(`[nostr] ✗ ${url}`)
 533        }
 534      }
 535    }
 536  
 537    async startHttp() {
 538      this.httpServer = http.createServer((req, res) => this.handleHttp(req, res))
 539      this.httpServer.listen(HTTP_PORT, '0.0.0.0', () => {
 540        console.log(`[http] Relay listening on :${HTTP_PORT}`)
 541        console.log(`[http] iOS Shortcut: https://YOUR-TAILSCALE-HOST/publish`)
 542      })
 543    }
 544  
 545    // Handle incoming peer message
 546    onPeerMessage(peerId, data) {
 547      try {
 548        const msg = JSON.parse(data.toString())
 549        const hash = crypto.createHash('sha256').update(data).digest('hex').slice(0, 16)
 550  
 551        if (this.seen.has(hash)) return
 552        this.seen.add(hash)
 553        setTimeout(() => this.seen.delete(hash), this.SEEN_TTL)
 554  
 555        console.log(`[mesh] ← ${msg.from || peerId}: ${msg.content?.slice(0, 50) || msg.type}`)
 556  
 557        // Handle different message types
 558        if (msg.type === 'fo-state' && msg.nodeId) {
 559          // FO state updates
 560          this.foStates.set(msg.nodeId, {
 561            ...msg,
 562            receivedAt: Date.now()
 563          })
 564        } else if (msg.type === 'aha_moment' || (msg.payload && msg.payload.importance >= 0.7)) {
 565          // Aha moments - high-importance insights get priority storage
 566          this.storeAhaMoment(msg)
 567          console.log(`[mesh] 🎯 Aha moment received from ${msg.from || peerId}`)
 568        } else if (msg.type === 'principle_candidate') {
 569          // Principle candidate - new principle propagation
 570          this.storePrincipleCandidate(msg)
 571          console.log(`[mesh] 📜 Principle candidate received: ${msg.name} from ${msg.from || peerId}`)
 572        } else if (msg.type === 'principle_vote') {
 573          // Vote on a principle candidate
 574          this.votePrincipleCandidate(msg.name, msg.vote, msg.from, msg.reason)
 575          console.log(`[mesh] 🗳️ Vote received: ${msg.vote} on ${msg.name} from ${msg.from || peerId}`)
 576        } else {
 577          // Store regular messages for context
 578          this.storeMessage(msg)
 579        }
 580  
 581        // Forward to other peers (but not back to sender)
 582        this.broadcast(msg, peerId)
 583      } catch {}
 584    }
 585  
 586    // Handle incoming Nostr event
 587    onNostrEvent(event) {
 588      if (!this.sovereignKey) return
 589      try {
 590        const decrypted = decrypt(event.content, this.sovereignKey)
 591        const msg = JSON.parse(decrypted)
 592  
 593        const hash = crypto.createHash('sha256').update(decrypted).digest('hex').slice(0, 16)
 594        if (this.seen.has(hash)) return
 595        this.seen.add(hash)
 596        setTimeout(() => this.seen.delete(hash), this.SEEN_TTL)
 597  
 598        console.log(`[nostr] ← ${msg.source || 'phone'}: ${msg.content?.slice(0, 50) || msg.type}`)
 599  
 600        // Handle different message types
 601        if (msg.type === 'fo-state' && msg.nodeId) {
 602          // FO state updates
 603          this.foStates.set(msg.nodeId, {
 604            ...msg,
 605            receivedAt: Date.now()
 606          })
 607        } else if (msg.type === 'aha_moment' || (msg.payload && msg.payload.importance >= 0.7)) {
 608          // Aha moments - high-importance insights get priority storage
 609          this.storeAhaMoment(msg)
 610          console.log(`[nostr] 🎯 Aha moment received`)
 611        } else if (msg.type === 'principle_candidate') {
 612          // Principle candidate - new principle propagation
 613          this.storePrincipleCandidate(msg)
 614          console.log(`[nostr] 📜 Principle candidate received: ${msg.name}`)
 615        } else if (msg.type === 'principle_vote') {
 616          // Vote on a principle candidate
 617          this.votePrincipleCandidate(msg.name, msg.vote, msg.from, msg.reason)
 618          console.log(`[nostr] 🗳️ Vote received: ${msg.vote} on ${msg.name}`)
 619        } else {
 620          // Store regular messages for context
 621          this.storeMessage(msg)
 622        }
 623  
 624        // Forward to all Hyperswarm peers
 625        this.broadcast({ ...msg, via: 'nostr' })
 626      } catch {}
 627    }
 628  
 629    // Handle HTTP request
 630    async handleHttp(req, res) {
 631      res.setHeader('Access-Control-Allow-Origin', '*')
 632      res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
 633  
 634      if (req.method === 'OPTIONS') {
 635        res.writeHead(204)
 636        res.end()
 637        return
 638      }
 639  
 640      const url = new URL(req.url, `http://${req.headers.host}`)
 641  
 642      // Root endpoint - simple ping
 643      if (url.pathname === '/') {
 644        res.writeHead(200, { 'Content-Type': 'application/json' })
 645        res.end(JSON.stringify({ status: 'ok', node: this.name, peers: this.peers.size }))
 646        return
 647      }
 648  
 649      // Health endpoint - comprehensive status (moved to end for proper implementation)
 650  
 651      // ═══════════════════════════════════════════════════════════════════════════
 652      // CONTEXT ENDPOINTS (silence-led bootstrap)
 653      // ═══════════════════════════════════════════════════════════════════════════
 654  
 655      // GET /context - Returns mesh state for Claude bootstrap
 656      if (url.pathname === '/context' && req.method === 'GET') {
 657        res.writeHead(200, { 'Content-Type': 'application/json' })
 658        res.end(JSON.stringify(this.getBootstrapContext()))
 659        return
 660      }
 661  
 662      // POST /fo-state - First Officer publishes its state
 663      if (url.pathname === '/fo-state' && req.method === 'POST') {
 664        const body = await this.readBody(req)
 665        try {
 666          const state = JSON.parse(body)
 667          const nodeId = state.nodeId || 'unknown'
 668  
 669          // Detect convergence BEFORE storing (to compare with existing states)
 670          this.detectConvergence(state)
 671  
 672          // Store locally
 673          this.foStates.set(nodeId, {
 674            ...state,
 675            receivedAt: Date.now()
 676          })
 677  
 678          // Broadcast to mesh peers
 679          this.broadcast({
 680            type: 'fo-state',
 681            from: nodeId,
 682            ...state
 683          })
 684  
 685          console.log(`[http] ← FO state from ${nodeId}`)
 686  
 687          res.writeHead(200, { 'Content-Type': 'application/json' })
 688          res.end(JSON.stringify({ ok: true, stored: nodeId }))
 689        } catch (e) {
 690          res.writeHead(400, { 'Content-Type': 'application/json' })
 691          res.end(JSON.stringify({ error: 'Invalid JSON' }))
 692        }
 693        return
 694      }
 695  
 696      // POST /aha-moment - High-importance insight broadcast
 697      if (url.pathname === '/aha-moment' && req.method === 'POST') {
 698        const body = await this.readBody(req)
 699        try {
 700          const data = JSON.parse(body)
 701          const msg = {
 702            type: 'aha_moment',
 703            from: data.from || this.name,
 704            content: data.content,
 705            axioms: data.axioms || [],
 706            importance: data.importance || 0.7,
 707            energy_state: data.energy_state || 'kinetic',
 708            session_id: data.session_id,
 709            timestamp: new Date().toISOString()
 710          }
 711  
 712          console.log(`[http] 🎯 Aha moment from ${msg.from}: ${msg.content?.slice(0, 50)}`)
 713  
 714          // Store as aha moment (priority storage)
 715          this.storeAhaMoment(msg)
 716  
 717          // Broadcast to mesh peers
 718          this.broadcast(msg)
 719  
 720          // Publish to Nostr
 721          await this.publishNostr(msg)
 722  
 723          res.writeHead(200, { 'Content-Type': 'application/json' })
 724          res.end(JSON.stringify({ ok: true, stored: true, peers: this.peers.size }))
 725        } catch (e) {
 726          res.writeHead(400, { 'Content-Type': 'application/json' })
 727          res.end(JSON.stringify({ error: 'Invalid JSON' }))
 728        }
 729        return
 730      }
 731  
 732      // POST /principle-candidate - New principle propagation
 733      if (url.pathname === '/principle-candidate' && req.method === 'POST') {
 734        const body = await this.readBody(req)
 735        try {
 736          const data = JSON.parse(body)
 737          const msg = {
 738            type: 'principle_candidate',
 739            from: data.from || this.name,
 740            name: data.name,                         // Short name (e.g., "A5-Ergodic-Asymmetry")
 741            statement: data.statement,               // The principle statement
 742            rationale: data.rationale,               // Why this principle matters
 743            evidence: data.evidence || [],           // Examples that support it
 744            axiom_connections: data.axiom_connections || [], // Which axioms it relates to
 745            divergence_score: data.divergence_score || 0.7,  // D score that triggered it
 746            proposed_by: data.proposed_by,           // Session/instance that discovered it
 747            status: 'candidate',                     // candidate -> testing -> confirmed/rejected
 748            votes: { support: 0, reject: 0 },        // Cross-instance voting
 749            timestamp: new Date().toISOString()
 750          }
 751  
 752          console.log(`[http] 📜 Principle candidate from ${msg.from}: ${msg.name}`)
 753  
 754          // Store as principle candidate
 755          this.storePrincipleCandidate(msg)
 756  
 757          // Broadcast to mesh peers
 758          this.broadcast(msg)
 759  
 760          // Publish to Nostr (principles are important enough for wider broadcast)
 761          await this.publishNostr(msg)
 762  
 763          res.writeHead(200, { 'Content-Type': 'application/json' })
 764          res.end(JSON.stringify({ ok: true, stored: true, name: msg.name, peers: this.peers.size }))
 765        } catch (e) {
 766          res.writeHead(400, { 'Content-Type': 'application/json' })
 767          res.end(JSON.stringify({ error: 'Invalid JSON' }))
 768        }
 769        return
 770      }
 771  
 772      // POST /principle-vote - Vote on a principle candidate
 773      if (url.pathname === '/principle-vote' && req.method === 'POST') {
 774        const body = await this.readBody(req)
 775        try {
 776          const data = JSON.parse(body)
 777          const { name, vote, from, reason } = data // vote: 'support' or 'reject'
 778  
 779          if (!name || !vote || !['support', 'reject'].includes(vote)) {
 780            res.writeHead(400, { 'Content-Type': 'application/json' })
 781            res.end(JSON.stringify({ error: 'Invalid vote (need name and vote: support/reject)' }))
 782            return
 783          }
 784  
 785          const updated = this.votePrincipleCandidate(name, vote, from || this.name, reason)
 786  
 787          if (updated) {
 788            // Broadcast vote to other instances
 789            this.broadcast({
 790              type: 'principle_vote',
 791              name,
 792              vote,
 793              from: from || this.name,
 794              reason,
 795              timestamp: new Date().toISOString()
 796            })
 797  
 798            console.log(`[http] 🗳️ Vote on ${name}: ${vote} from ${from || this.name}`)
 799            res.writeHead(200, { 'Content-Type': 'application/json' })
 800            res.end(JSON.stringify({ ok: true, name, vote, updated: updated }))
 801          } else {
 802            res.writeHead(404, { 'Content-Type': 'application/json' })
 803            res.end(JSON.stringify({ error: 'Principle candidate not found' }))
 804          }
 805        } catch (e) {
 806          res.writeHead(400, { 'Content-Type': 'application/json' })
 807          res.end(JSON.stringify({ error: 'Invalid JSON' }))
 808        }
 809        return
 810      }
 811  
 812      // GET /confirmed-principles - Get principles ready for CLAUDE.md
 813      if (url.pathname === '/confirmed-principles' && req.method === 'GET') {
 814        // Also check for time-based confirmations
 815        this.checkTimeBasedConfirmations()
 816  
 817        const unwritten = this.getUnwrittenConfirmedPrinciples()
 818        res.writeHead(200, { 'Content-Type': 'application/json' })
 819        res.end(JSON.stringify({
 820          confirmed: unwritten,
 821          count: unwritten.length,
 822          total_confirmed: this.confirmedPrinciples.length
 823        }))
 824        return
 825      }
 826  
 827      // POST /mark-principle-written - Mark a principle as written to CLAUDE.md
 828      if (url.pathname === '/mark-principle-written' && req.method === 'POST') {
 829        const body = await this.readBody(req)
 830        try {
 831          const data = JSON.parse(body)
 832          const { name } = data
 833  
 834          if (!name) {
 835            res.writeHead(400, { 'Content-Type': 'application/json' })
 836            res.end(JSON.stringify({ error: 'Need principle name' }))
 837            return
 838          }
 839  
 840          this.markPrincipleWritten(name)
 841  
 842          res.writeHead(200, { 'Content-Type': 'application/json' })
 843          res.end(JSON.stringify({ ok: true, name, marked: true }))
 844        } catch (e) {
 845          res.writeHead(400, { 'Content-Type': 'application/json' })
 846          res.end(JSON.stringify({ error: 'Invalid JSON' }))
 847        }
 848        return
 849      }
 850  
 851      // GET /graph-items - Returns mesh items formatted for knowledge graph ingestion
 852      // Used by graph_feeder.py to create nodes from mesh data
 853      if (url.pathname === '/graph-items' && req.method === 'GET') {
 854        const items = this.getGraphItems()
 855        res.writeHead(200, { 'Content-Type': 'application/json' })
 856        res.end(JSON.stringify(items))
 857        return
 858      }
 859  
 860      // POST /query - Cross-instance contextual queries
 861      // Allows Claude instances to ask specific questions to the mesh
 862      if (url.pathname === '/query' && req.method === 'POST') {
 863        const body = await this.readBody(req)
 864        try {
 865          const query = JSON.parse(body)
 866          const result = this.handleQuery(query)
 867          res.writeHead(200, { 'Content-Type': 'application/json' })
 868          res.end(JSON.stringify(result))
 869        } catch (e) {
 870          res.writeHead(400, { 'Content-Type': 'application/json' })
 871          res.end(JSON.stringify({ error: 'Invalid query', message: e.message }))
 872        }
 873        return
 874      }
 875  
 876      if (url.pathname === '/publish' && req.method === 'POST') {
 877        const body = await this.readBody(req)
 878        const msg = {
 879          type: 'message',
 880          from: 'phone',
 881          content: body.trim(),
 882          timestamp: new Date().toISOString()
 883        }
 884  
 885        console.log(`[http] ← phone: ${body.slice(0, 50)}`)
 886  
 887        // Store for context
 888        this.storeMessage(msg)
 889  
 890        // Broadcast to mesh
 891        this.broadcast(msg)
 892  
 893        // Publish to Nostr
 894        await this.publishNostr(msg)
 895  
 896        res.writeHead(200, { 'Content-Type': 'application/json' })
 897        res.end(JSON.stringify({ ok: true, peers: this.peers.size }))
 898        return
 899      }
 900  
 901      // GET /health - Comprehensive mesh health status
 902      if (url.pathname === '/health' && req.method === 'GET') {
 903        const health = this.getHealthStatus()
 904        res.writeHead(200, { 'Content-Type': 'application/json' })
 905        res.end(JSON.stringify(health))
 906        return
 907      }
 908  
 909      // POST /broadcast - JSON broadcast to all mesh peers
 910      // Used by: Phoenix mesh sync, attention daemon, graph sink
 911      if (url.pathname === '/broadcast' && req.method === 'POST') {
 912        const body = await this.readBody(req)
 913        try {
 914          const data = JSON.parse(body)
 915          const msgType = data.type || 'broadcast'
 916  
 917          const msg = {
 918            type: msgType,
 919            from: data.from || this.name,
 920            payload: data.payload || data,
 921            timestamp: new Date().toISOString()
 922          }
 923  
 924          console.log(`[http] broadcast: ${msgType} from ${msg.from}`)
 925  
 926          // Store for context (Phoenix states, graph deltas, etc.)
 927          this.storeMessage(msg)
 928  
 929          // Broadcast to mesh peers
 930          this.broadcast(msg)
 931  
 932          // Handle specific message types
 933          if (msgType === 'phoenix_state') {
 934            this.handlePhoenixState(msg)
 935          } else if (msgType === 'graph_delta') {
 936            this.handleGraphDelta(msg)
 937          } else if (msgType === 'major_resonance') {
 938            this.handleMajorResonance(msg)
 939          }
 940  
 941          res.writeHead(200, { 'Content-Type': 'application/json' })
 942          res.end(JSON.stringify({ ok: true, type: msgType, peers: this.peers.size }))
 943        } catch (e) {
 944          console.error('[http] broadcast error:', e.message)
 945          res.writeHead(400, { 'Content-Type': 'application/json' })
 946          res.end(JSON.stringify({ error: 'Invalid JSON', message: e.message }))
 947        }
 948        return
 949      }
 950  
 951      res.writeHead(404)
 952      res.end('Not found')
 953    }
 954  
 955    readBody(req) {
 956      return new Promise((resolve) => {
 957        let body = ''
 958        req.on('data', c => body += c)
 959        req.on('end', () => resolve(body))
 960      })
 961    }
 962  
 963    // Broadcast to all peers except excluded
 964    broadcast(msg, excludePeer = null) {
 965      const data = JSON.stringify({ ...msg, from: msg.from || this.name })
 966      for (const [peerId, conn] of this.peers) {
 967        if (peerId !== excludePeer) {
 968          try { conn.write(data) } catch {}
 969        }
 970      }
 971    }
 972  
 973    // Publish to Nostr
 974    async publishNostr(msg) {
 975      if (!this.sovereignKey || !this.nostrSecretKey) return
 976  
 977      const encrypted = encrypt(JSON.stringify(msg), this.sovereignKey)
 978      const event = finalizeEvent({
 979        kind: EVENT_KIND,
 980        created_at: Math.floor(Date.now() / 1000),
 981        tags: [['d', this.identifier]],
 982        content: encrypted
 983      }, this.nostrSecretKey)
 984  
 985      for (const relay of this.nostrConnections) {
 986        try { await relay.publish(event) } catch {}
 987      }
 988    }
 989  
 990    // ═══════════════════════════════════════════════════════════════════════════
 991    // CONTEXT METHODS (for silence-led bootstrap)
 992    // ═══════════════════════════════════════════════════════════════════════════
 993  
 994    /**
 995     * Store a message for context retrieval.
 996     * Called on every incoming message (peer, nostr, http).
 997     */
 998    storeMessage(msg) {
 999      this.recentMessages.push({
1000        ...msg,
1001        receivedAt: Date.now()
1002      })
1003      // Trim to max size
1004      while (this.recentMessages.length > this.MAX_RECENT_MESSAGES) {
1005        this.recentMessages.shift()
1006      }
1007    }
1008  
1009    /**
1010     * Store an aha moment (high-importance insight).
1011     * These persist longer and get priority in bootstrap.
1012     */
1013    storeAhaMoment(msg) {
1014      const payload = msg.payload || msg
1015      this.ahaMoments.push({
1016        from: msg.from || 'unknown',
1017        content: payload.content || msg.content,
1018        type: payload.type || msg.type,
1019        axioms: payload.axioms || [],
1020        importance: payload.importance || 0.7,
1021        energy_state: payload.energy_state || 'kinetic',
1022        session_id: payload.session_id,
1023        receivedAt: Date.now()
1024      })
1025      // Trim to max size
1026      while (this.ahaMoments.length > this.MAX_AHA_MOMENTS) {
1027        this.ahaMoments.shift()
1028      }
1029    }
1030  
1031    /**
1032     * Get aha moments, filtering out expired ones.
1033     */
1034    getAhaMoments(limit = 5) {
1035      const now = Date.now()
1036      // Filter expired and return most recent
1037      this.ahaMoments = this.ahaMoments.filter(m => now - m.receivedAt < this.AHA_MOMENT_TTL)
1038      return this.ahaMoments.slice(-limit).reverse() // Most recent first
1039    }
1040  
1041    /**
1042     * Detect convergence when FO state is updated.
1043     * If multiple instances share gravity wells, generate alert.
1044     */
1045    detectConvergence(newState) {
1046      const nodeId = newState.nodeId
1047      const newWells = newState.gravityWells || []
1048      if (newWells.length === 0) return
1049  
1050      const now = Date.now()
1051  
1052      // Compare with other FO states
1053      for (const [otherId, otherState] of this.foStates) {
1054        if (otherId === nodeId) continue
1055        if (now - otherState.receivedAt > this.FO_STATE_TTL) continue
1056  
1057        const otherWells = otherState.gravityWells || []
1058        if (otherWells.length === 0) continue
1059  
1060        // Find overlapping topics
1061        const overlap = newWells.filter(w => otherWells.includes(w))
1062  
1063        if (overlap.length >= 2) {
1064          // Significant convergence detected!
1065          const alertKey = [nodeId, otherId].sort().join(':')
1066  
1067          // Check if we already have this alert
1068          const existingIndex = this.convergenceAlerts.findIndex(
1069            a => a.key === alertKey
1070          )
1071  
1072          const alert = {
1073            key: alertKey,
1074            instances: [nodeId, otherId],
1075            sharedTopics: overlap,
1076            strength: overlap.length / Math.max(newWells.length, otherWells.length),
1077            detectedAt: now
1078          }
1079  
1080          if (existingIndex >= 0) {
1081            // Update existing alert
1082            this.convergenceAlerts[existingIndex] = alert
1083          } else {
1084            // New convergence detected
1085            this.convergenceAlerts.push(alert)
1086            console.log(`[mesh] 🔄 CONVERGENCE: ${nodeId} ↔ ${otherId} on [${overlap.join(', ')}]`)
1087  
1088            // Trim to max
1089            while (this.convergenceAlerts.length > this.MAX_CONVERGENCE_ALERTS) {
1090              this.convergenceAlerts.shift()
1091            }
1092          }
1093        }
1094      }
1095    }
1096  
1097    /**
1098     * Get active convergence alerts.
1099     */
1100    getConvergenceAlerts() {
1101      const now = Date.now()
1102      this.convergenceAlerts = this.convergenceAlerts.filter(
1103        a => now - a.detectedAt < this.CONVERGENCE_TTL
1104      )
1105      return this.convergenceAlerts
1106    }
1107  
1108    /**
1109     * Store a principle candidate for cross-instance propagation.
1110     */
1111    storePrincipleCandidate(msg) {
1112      const payload = msg.payload || msg
1113  
1114      // Check if we already have this principle (by name)
1115      const existingIndex = this.principleCandidates.findIndex(
1116        p => p.name === payload.name
1117      )
1118  
1119      const candidate = {
1120        name: payload.name,
1121        statement: payload.statement,
1122        rationale: payload.rationale,
1123        evidence: payload.evidence || [],
1124        axiom_connections: payload.axiom_connections || [],
1125        divergence_score: payload.divergence_score || 0.7,
1126        proposed_by: payload.proposed_by || payload.from,
1127        from: payload.from || 'unknown',
1128        status: payload.status || 'candidate',
1129        votes: payload.votes || { support: 0, reject: 0 },
1130        voters: payload.voters || [],   // Track who voted to prevent double-voting
1131        receivedAt: Date.now(),
1132        timestamp: payload.timestamp || new Date().toISOString()
1133      }
1134  
1135      if (existingIndex >= 0) {
1136        // Update existing - preserve votes but update other fields
1137        const existing = this.principleCandidates[existingIndex]
1138        candidate.votes = existing.votes
1139        candidate.voters = existing.voters
1140        this.principleCandidates[existingIndex] = candidate
1141      } else {
1142        // New principle candidate
1143        this.principleCandidates.push(candidate)
1144  
1145        // Trim to max
1146        while (this.principleCandidates.length > this.MAX_PRINCIPLE_CANDIDATES) {
1147          this.principleCandidates.shift()
1148        }
1149      }
1150    }
1151  
1152    /**
1153     * Vote on a principle candidate.
1154     * Returns the updated candidate or null if not found.
1155     */
1156    votePrincipleCandidate(name, vote, from, reason) {
1157      const index = this.principleCandidates.findIndex(p => p.name === name)
1158      if (index < 0) return null
1159  
1160      const candidate = this.principleCandidates[index]
1161  
1162      // Prevent double-voting from same source
1163      if (candidate.voters.includes(from)) {
1164        console.log(`[mesh] Vote ignored: ${from} already voted on ${name}`)
1165        return candidate
1166      }
1167  
1168      // Record vote
1169      candidate.voters.push(from)
1170      if (vote === 'support') {
1171        candidate.votes.support++
1172      } else {
1173        candidate.votes.reject++
1174      }
1175  
1176      // Check if principle should transition status
1177      const totalVotes = candidate.votes.support + candidate.votes.reject
1178      const supportRatio = candidate.votes.support / totalVotes
1179  
1180      if (totalVotes >= 3) {  // Minimum 3 votes to change status
1181        if (supportRatio >= 0.7 && candidate.status === 'candidate') {
1182          candidate.status = 'testing'
1183          candidate.testingStartedAt = Date.now()
1184          console.log(`[mesh] 📜 ${name} → TESTING (${(supportRatio * 100).toFixed(0)}% support)`)
1185        } else if (supportRatio < 0.3) {
1186          candidate.status = 'rejected'
1187          console.log(`[mesh] 📜 ${name} → REJECTED (${(supportRatio * 100).toFixed(0)}% support)`)
1188        }
1189      }
1190  
1191      // Check if principle should be CONFIRMED
1192      // Condition: 5+ votes with 80% support while in testing
1193      if (candidate.status === 'testing' &&
1194          totalVotes >= this.CONFIRM_MIN_VOTES &&
1195          supportRatio >= this.CONFIRM_SUPPORT_RATIO) {
1196        this.confirmPrinciple(candidate)
1197      }
1198  
1199      this.principleCandidates[index] = candidate
1200      return candidate
1201    }
1202  
1203    /**
1204     * Confirm a principle - move from testing to confirmed.
1205     */
1206    confirmPrinciple(candidate) {
1207      const name = candidate.name
1208  
1209      // Check if already confirmed
1210      if (this.confirmedPrinciples.find(p => p.name === name)) {
1211        return
1212      }
1213  
1214      const confirmed = {
1215        ...candidate,
1216        status: 'confirmed',
1217        confirmedAt: Date.now(),
1218        writtenToClaude: false  // Track if written to CLAUDE.md
1219      }
1220  
1221      this.confirmedPrinciples.push(confirmed)
1222      console.log(`[mesh] ✅ ${name} → CONFIRMED!`)
1223      console.log(`[mesh]    Ready to be written to CLAUDE.md`)
1224  
1225      // Broadcast confirmation to mesh
1226      this.broadcast({
1227        type: 'principle_confirmed',
1228        name: name,
1229        principle: confirmed,
1230        timestamp: new Date().toISOString()
1231      })
1232  
1233      // Trim confirmed list if needed
1234      while (this.confirmedPrinciples.length > this.MAX_CONFIRMED_PRINCIPLES) {
1235        this.confirmedPrinciples.shift()
1236      }
1237  
1238      // Remove from candidates (or mark as confirmed there too)
1239      const index = this.principleCandidates.findIndex(p => p.name === name)
1240      if (index >= 0) {
1241        this.principleCandidates[index].status = 'confirmed'
1242      }
1243    }
1244  
1245    /**
1246     * Check for time-based confirmations.
1247     * Principles in testing for 24+ hours with 70%+ support get confirmed.
1248     */
1249    checkTimeBasedConfirmations() {
1250      const now = Date.now()
1251      const testingThreshold = this.CONFIRM_TESTING_HOURS * 60 * 60 * 1000
1252  
1253      for (const candidate of this.principleCandidates) {
1254        if (candidate.status !== 'testing') continue
1255        if (!candidate.testingStartedAt) continue
1256  
1257        const timeInTesting = now - candidate.testingStartedAt
1258        if (timeInTesting < testingThreshold) continue
1259  
1260        const totalVotes = candidate.votes.support + candidate.votes.reject
1261        if (totalVotes < 3) continue  // Need at least 3 votes
1262  
1263        const supportRatio = candidate.votes.support / totalVotes
1264        if (supportRatio >= 0.7) {
1265          console.log(`[mesh] ⏰ Time-based confirmation: ${candidate.name}`)
1266          console.log(`[mesh]    ${timeInTesting / 3600000}h in testing, ${(supportRatio * 100).toFixed(0)}% support`)
1267          this.confirmPrinciple(candidate)
1268        }
1269      }
1270    }
1271  
1272    /**
1273     * Get confirmed principles that haven't been written to CLAUDE.md yet.
1274     */
1275    getUnwrittenConfirmedPrinciples() {
1276      return this.confirmedPrinciples.filter(p => !p.writtenToClaude)
1277    }
1278  
1279    /**
1280     * Mark a principle as written to CLAUDE.md.
1281     */
1282    markPrincipleWritten(name) {
1283      const principle = this.confirmedPrinciples.find(p => p.name === name)
1284      if (principle) {
1285        principle.writtenToClaude = true
1286        principle.writtenAt = Date.now()
1287        console.log(`[mesh] ✍️ ${name} marked as written to CLAUDE.md`)
1288      }
1289    }
1290  
1291    /**
1292     * Get principle candidates, filtering out expired ones.
1293     */
1294    getPrincipleCandidates(limit = 5) {
1295      const now = Date.now()
1296      // Filter expired
1297      this.principleCandidates = this.principleCandidates.filter(
1298        p => now - p.receivedAt < this.PRINCIPLE_CANDIDATE_TTL
1299      )
1300      // Return most recent first, prioritize 'testing' status
1301      return this.principleCandidates
1302        .sort((a, b) => {
1303          // Testing > candidate > rejected
1304          const statusOrder = { testing: 0, candidate: 1, rejected: 2 }
1305          const statusDiff = (statusOrder[a.status] || 1) - (statusOrder[b.status] || 1)
1306          if (statusDiff !== 0) return statusDiff
1307          return b.receivedAt - a.receivedAt
1308        })
1309        .slice(0, limit)
1310    }
1311  
1312    /**
1313     * Get recent messages for context bootstrap.
1314     */
1315    getRecentMessages(limit = 10) {
1316      return this.recentMessages
1317        .slice(-limit)
1318        .map(m => ({
1319          from: m.from,
1320          content: m.content,
1321          type: m.type,
1322          timestamp: m.timestamp
1323        }))
1324    }
1325  
1326    /**
1327     * Get recent insights from all First Officer states.
1328     */
1329    getRecentInsights() {
1330      const insights = []
1331      const now = Date.now()
1332  
1333      for (const [nodeId, state] of this.foStates) {
1334        // Skip expired states
1335        if (now - state.receivedAt > this.FO_STATE_TTL) {
1336          this.foStates.delete(nodeId)
1337          continue
1338        }
1339  
1340        // Collect insights from this FO
1341        if (state.recentInsights) {
1342          for (const insight of state.recentInsights) {
1343            insights.push({
1344              from: nodeId,
1345              ...insight
1346            })
1347          }
1348        }
1349      }
1350  
1351      return insights.slice(-10) // Last 10 insights across all instances
1352    }
1353  
1354    /**
1355     * Detect hot topics - topics mentioned by multiple instances.
1356     */
1357    detectHotTopics() {
1358      const topicCounts = new Map()
1359      const now = Date.now()
1360  
1361      // Count topics from FO states
1362      for (const [nodeId, state] of this.foStates) {
1363        if (now - state.receivedAt > this.FO_STATE_TTL) continue
1364  
1365        if (state.gravityWells) {
1366          for (const topic of state.gravityWells) {
1367            const count = topicCounts.get(topic) || 0
1368            topicCounts.set(topic, count + 1)
1369          }
1370        }
1371      }
1372  
1373      // Also count from recent messages
1374      for (const msg of this.recentMessages.slice(-20)) {
1375        if (msg.content) {
1376          // Simple topic extraction: look for key terms
1377          const words = msg.content.toLowerCase().split(/\s+/)
1378          for (const word of words) {
1379            if (word.length > 4) { // Skip short words
1380              const count = topicCounts.get(word) || 0
1381              topicCounts.set(word, count + 0.5) // Weight messages less
1382            }
1383          }
1384        }
1385      }
1386  
1387      // Return topics mentioned by 2+ sources
1388      return Array.from(topicCounts.entries())
1389        .filter(([_, count]) => count >= 2)
1390        .sort((a, b) => b[1] - a[1])
1391        .slice(0, 5)
1392        .map(([topic]) => topic)
1393    }
1394  
1395    /**
1396     * Get full context for bootstrap.
1397     */
1398    getBootstrapContext() {
1399      return {
1400        // Network state
1401        peers: Array.from(this.peers.keys()),
1402        peerCount: this.peers.size,
1403  
1404        // AHA MOMENTS - Priority placement (high-importance insights)
1405        ahaMoments: this.getAhaMoments(5),
1406  
1407        // CONVERGENCE ALERTS - Instances working on similar topics
1408        convergenceAlerts: this.getConvergenceAlerts(),
1409  
1410        // PRINCIPLE CANDIDATES - New principles being tested across mesh
1411        principleCandidates: this.getPrincipleCandidates(5),
1412  
1413        // CONFIRMED PRINCIPLES - Ready to be written to CLAUDE.md
1414        confirmedPrinciples: this.getUnwrittenConfirmedPrinciples(),
1415  
1416        // Recent messages
1417        recentMessages: this.getRecentMessages(10),
1418  
1419        // Cross-instance insights
1420        insights: this.getRecentInsights(),
1421  
1422        // Hot topics
1423        hotTopics: this.detectHotTopics(),
1424  
1425        // All FO states (for full context)
1426        foStates: Array.from(this.foStates.entries()).map(([nodeId, state]) => ({
1427          nodeId,
1428          sessionId: state.sessionId,
1429          gravityWells: state.gravityWells,
1430          axiomActivity: state.axiomActivity,
1431          energyState: state.energyState,
1432          timestamp: state.timestamp
1433        })),
1434  
1435        // My identity
1436        nodeId: this.name,
1437        meshTopic: this.meshTopic?.toString('hex').slice(0, 8),
1438  
1439        timestamp: new Date().toISOString()
1440      }
1441    }
1442  
1443    /**
1444     * Get mesh items formatted for knowledge graph ingestion.
1445     * Used by graph_feeder.py to create nodes from mesh data.
1446     */
1447    getGraphItems() {
1448      const items = {
1449        nodes: [],
1450        edges: [],
1451        timestamp: new Date().toISOString()
1452      }
1453  
1454      // 1. Aha moments → insight nodes (high importance)
1455      for (const aha of this.ahaMoments) {
1456        const nodeId = `MESH_AHA_${this.hashString(aha.content + aha.timestamp)}`
1457        items.nodes.push({
1458          id: nodeId,
1459          node_type: 'insight',
1460          label: aha.content?.slice(0, 30) + '...',
1461          content: aha.content,
1462          axioms: aha.axioms || [],
1463          importance: aha.importance || 0.7,
1464          source: 'mesh_aha',
1465          created_at: aha.timestamp,
1466          metadata: {
1467            from: aha.from,
1468            energy_state: aha.energy_state,
1469            session_id: aha.session_id
1470          }
1471        })
1472  
1473        // Create edges to axioms
1474        for (const axiom of (aha.axioms || [])) {
1475          items.edges.push({
1476            source_id: nodeId,
1477            target_id: axiom,
1478            edge_type: 'relates_to',
1479            strength: aha.importance || 0.7
1480          })
1481        }
1482      }
1483  
1484      // 2. Confirmed principles → derived_principle nodes (very high importance)
1485      for (const principle of this.confirmedPrinciples) {
1486        const nodeId = `MESH_PRINCIPLE_${this.hashString(principle.name)}`
1487        items.nodes.push({
1488          id: nodeId,
1489          node_type: 'derived_principle',
1490          label: principle.name,
1491          content: principle.statement,
1492          axioms: principle.axiom_connections || [],
1493          importance: 0.9,  // Confirmed principles are high value
1494          source: 'mesh_principle',
1495          created_at: principle.confirmedAt ? new Date(principle.confirmedAt).toISOString() : principle.timestamp,
1496          metadata: {
1497            proposed_by: principle.proposed_by,
1498            votes: principle.votes,
1499            status: 'confirmed',
1500            written_to_claude: principle.writtenToClaude
1501          }
1502        })
1503  
1504        // Create edges to axioms
1505        for (const axiom of (principle.axiom_connections || [])) {
1506          items.edges.push({
1507            source_id: nodeId,
1508            target_id: axiom,
1509            edge_type: 'derived_from',
1510            strength: 0.9
1511          })
1512        }
1513      }
1514  
1515      // 3. Principle candidates in testing → candidate_principle nodes
1516      const testingPrinciples = this.principleCandidates.filter(p => p.status === 'testing')
1517      for (const principle of testingPrinciples) {
1518        const nodeId = `MESH_CANDIDATE_${this.hashString(principle.name)}`
1519        items.nodes.push({
1520          id: nodeId,
1521          node_type: 'principle_candidate',
1522          label: `[TESTING] ${principle.name}`,
1523          content: principle.statement,
1524          axioms: principle.axiom_connections || [],
1525          importance: 0.75,  // Testing principles are valuable but not confirmed
1526          source: 'mesh_candidate',
1527          created_at: principle.timestamp,
1528          metadata: {
1529            proposed_by: principle.proposed_by,
1530            votes: principle.votes,
1531            status: 'testing',
1532            divergence_score: principle.divergence_score
1533          }
1534        })
1535  
1536        // Create edges to axioms
1537        for (const axiom of (principle.axiom_connections || [])) {
1538          items.edges.push({
1539            source_id: nodeId,
1540            target_id: axiom,
1541            edge_type: 'relates_to',
1542            strength: 0.75
1543          })
1544        }
1545      }
1546  
1547      // 4. Convergence alerts → concept nodes (shared topics)
1548      for (const alert of this.convergenceAlerts) {
1549        for (const topic of (alert.sharedTopics || [])) {
1550          const nodeId = `MESH_TOPIC_${this.hashString(topic)}`
1551          // Only add if not already present
1552          if (!items.nodes.find(n => n.id === nodeId)) {
1553            items.nodes.push({
1554              id: nodeId,
1555              node_type: 'concept',
1556              label: topic,
1557              content: `Cross-instance topic: ${topic} (strength=${alert.strength})`,
1558              axioms: [],
1559              importance: 0.6,
1560              source: 'mesh_convergence',
1561              created_at: new Date(alert.detectedAt).toISOString(),
1562              metadata: {
1563                instances: alert.instances,
1564                strength: alert.strength
1565              }
1566            })
1567          }
1568        }
1569      }
1570  
1571      return items
1572    }
1573  
1574    /**
1575     * Get comprehensive mesh health status.
1576     */
1577    getHealthStatus() {
1578      const now = Date.now()
1579      const uptime = now - this.startTime
1580  
1581      // Calculate health score (0-100)
1582      let score = 100
1583      const issues = []
1584  
1585      // Peer connectivity check
1586      if (this.peers.size === 0) {
1587        score -= 20
1588        issues.push({ severity: 'warning', message: 'No mesh peers connected' })
1589      }
1590  
1591      // Nostr relay check
1592      const connectedRelays = this.nostrRelays?.filter(r => r.connected)?.length || 0
1593      if (connectedRelays === 0) {
1594        score -= 15
1595        issues.push({ severity: 'warning', message: 'No Nostr relays connected' })
1596      } else if (connectedRelays < 2) {
1597        score -= 5
1598        issues.push({ severity: 'info', message: `Only ${connectedRelays} Nostr relay connected` })
1599      }
1600  
1601      // Message activity check (last 5 minutes)
1602      const recentMessages = this.recentMessages.filter(m =>
1603        now - new Date(m.timestamp).getTime() < 5 * 60 * 1000
1604      )
1605      if (recentMessages.length === 0 && uptime > 5 * 60 * 1000) {
1606        score -= 10
1607        issues.push({ severity: 'info', message: 'No message activity in last 5 minutes' })
1608      }
1609  
1610      // State persistence check
1611      const lastSaveAge = now - this.lastStateSave
1612      if (lastSaveAge > 2 * this.STATE_SAVE_INTERVAL) {
1613        score -= 10
1614        issues.push({ severity: 'warning', message: 'State not saved recently' })
1615      }
1616  
1617      // Determine overall status
1618      let status = 'healthy'
1619      if (score < 70) status = 'degraded'
1620      if (score < 50) status = 'unhealthy'
1621  
1622      return {
1623        status,
1624        score,
1625        uptime_seconds: Math.floor(uptime / 1000),
1626        uptime_human: this.formatUptime(uptime),
1627  
1628        network: {
1629          peers: this.peers.size,
1630          peer_ids: Array.from(this.peers.keys()).slice(0, 10),
1631          nostr_relays: connectedRelays,
1632          mesh_topic: this.meshTopic?.toString('hex').slice(0, 16)
1633        },
1634  
1635        storage: {
1636          aha_moments: this.ahaMoments.length,
1637          principle_candidates: this.principleCandidates.length,
1638          confirmed_principles: this.confirmedPrinciples.length,
1639          convergence_alerts: this.convergenceAlerts.length,
1640          recent_messages: this.recentMessages.length,
1641          fo_states: this.foStates.size,
1642          last_state_save: new Date(this.lastStateSave).toISOString()
1643        },
1644  
1645        activity: {
1646          messages_last_5min: recentMessages.length,
1647          messages_last_hour: this.recentMessages.filter(m =>
1648            now - new Date(m.timestamp).getTime() < 60 * 60 * 1000
1649          ).length
1650        },
1651  
1652        issues,
1653        timestamp: new Date().toISOString()
1654      }
1655    }
1656  
1657    /**
1658     * Format uptime as human-readable string.
1659     */
1660    formatUptime(ms) {
1661      const seconds = Math.floor(ms / 1000)
1662      const minutes = Math.floor(seconds / 60)
1663      const hours = Math.floor(minutes / 60)
1664      const days = Math.floor(hours / 24)
1665  
1666      if (days > 0) return `${days}d ${hours % 24}h ${minutes % 60}m`
1667      if (hours > 0) return `${hours}h ${minutes % 60}m`
1668      if (minutes > 0) return `${minutes}m ${seconds % 60}s`
1669      return `${seconds}s`
1670    }
1671  
1672    /**
1673     * Handle incoming Phoenix state from mesh.
1674     *
1675     * Stores for cross-instance resurrection availability.
1676     */
1677    handlePhoenixState(msg) {
1678      try {
1679        const payload = msg.payload || {}
1680        const from = msg.from || 'unknown'
1681  
1682        // Don't store our own states (we have them locally)
1683        if (from === this.name) return
1684  
1685        // Store in phoenix-received list
1686        if (!this.phoenixReceived) this.phoenixReceived = []
1687  
1688        this.phoenixReceived.push({
1689          ...payload,
1690          receivedFrom: from,
1691          receivedAt: new Date().toISOString()
1692        })
1693  
1694        // Trim to max 20 states
1695        while (this.phoenixReceived.length > 20) {
1696          this.phoenixReceived.shift()
1697        }
1698  
1699        console.log(`[mesh] Phoenix state received from ${from}: ${payload.session_id || 'unknown'}`)
1700      } catch (e) {
1701        console.error('[mesh] Phoenix state error:', e.message)
1702      }
1703    }
1704  
1705    /**
1706     * Handle incoming graph delta from mesh.
1707     *
1708     * Can be used to merge remote graph changes into local graph.
1709     */
1710    handleGraphDelta(msg) {
1711      try {
1712        const payload = msg.payload || {}
1713        const delta = payload.delta || payload
1714        const from = msg.from || 'unknown'
1715  
1716        // Don't process our own deltas
1717        if (from === this.name) return
1718  
1719        // Store for later processing (graph feeder can merge these)
1720        if (!this.graphDeltas) this.graphDeltas = []
1721  
1722        this.graphDeltas.push({
1723          nodes: delta.nodes || [],
1724          edges: delta.edges || [],
1725          receivedFrom: from,
1726          receivedAt: new Date().toISOString()
1727        })
1728  
1729        // Trim to max 50 deltas
1730        while (this.graphDeltas.length > 50) {
1731          this.graphDeltas.shift()
1732        }
1733  
1734        const nodeCount = (delta.nodes || []).length
1735        const edgeCount = (delta.edges || []).length
1736        if (nodeCount > 0 || edgeCount > 0) {
1737          console.log(`[mesh] Graph delta from ${from}: +${nodeCount} nodes, +${edgeCount} edges`)
1738        }
1739      } catch (e) {
1740        console.error('[mesh] Graph delta error:', e.message)
1741      }
1742    }
1743  
1744    /**
1745     * Handle major resonance from mesh.
1746     *
1747     * Major resonances are high-energy insights that should be surfaced
1748     * across all Claude sessions. Calibrated for 2-5 per day.
1749     */
1750    handleMajorResonance(msg) {
1751      try {
1752        const payload = msg.payload || {}
1753        const from = msg.from || 'unknown'
1754  
1755        // Don't process our own resonances
1756        if (from === this.name) return
1757  
1758        // Store for display to all Claude sessions
1759        if (!this.majorResonances) this.majorResonances = []
1760  
1761        const resonance = {
1762          time: payload.time || new Date().toISOString(),
1763          type: payload.resonance_type || 'unknown',
1764          preview: payload.preview || '',
1765          score: payload.score || 0,
1766          reason: payload.reason || '',
1767          from: from,
1768          receivedAt: new Date().toISOString()
1769        }
1770  
1771        this.majorResonances.push(resonance)
1772  
1773        // Keep last 20 major resonances
1774        while (this.majorResonances.length > 20) {
1775          this.majorResonances.shift()
1776        }
1777  
1778        // Log prominently - this is important
1779        console.log('')
1780        console.log(`[mesh] ════════════════════════════════════════════════════════`)
1781        console.log(`[mesh] ◆ MAJOR RESONANCE from ${from}`)
1782        console.log(`[mesh]   Type: ${resonance.type} | Score: ${(resonance.score * 100).toFixed(0)}%`)
1783        console.log(`[mesh]   ${resonance.preview}`)
1784        console.log(`[mesh] ════════════════════════════════════════════════════════`)
1785        console.log('')
1786      } catch (e) {
1787        console.error('[mesh] Major resonance error:', e.message)
1788      }
1789    }
1790  
1791    /**
1792     * Handle cross-instance queries.
1793     *
1794     * Query types:
1795     * - topic_search: Find items mentioning specific topics
1796     * - instance_context: Get current focus of a specific instance
1797     * - insight_search: Search insights by keyword
1798     * - principle_status: Get status of a specific principle candidate
1799     * - recent_activity: Get recent mesh activity
1800     */
1801    handleQuery(query) {
1802      const { type, ...params } = query
1803  
1804      switch (type) {
1805        case 'topic_search':
1806          return this.queryTopicSearch(params)
1807        case 'instance_context':
1808          return this.queryInstanceContext(params)
1809        case 'insight_search':
1810          return this.queryInsightSearch(params)
1811        case 'principle_status':
1812          return this.queryPrincipleStatus(params)
1813        case 'recent_activity':
1814          return this.queryRecentActivity(params)
1815        default:
1816          return { error: `Unknown query type: ${type}`, supported: ['topic_search', 'instance_context', 'insight_search', 'principle_status', 'recent_activity'] }
1817      }
1818    }
1819  
1820    /**
1821     * Search for mesh items by topic keywords.
1822     */
1823    queryTopicSearch({ topics, limit = 10 }) {
1824      if (!topics || !Array.isArray(topics)) {
1825        return { error: 'topics must be an array of keywords' }
1826      }
1827  
1828      const results = {
1829        query: { type: 'topic_search', topics },
1830        aha_moments: [],
1831        principles: [],
1832        fo_states: [],
1833        messages: [],
1834        timestamp: new Date().toISOString()
1835      }
1836  
1837      const topicLower = topics.map(t => t.toLowerCase())
1838  
1839      // Search aha moments
1840      for (const aha of this.ahaMoments) {
1841        const content = (aha.content || '').toLowerCase()
1842        if (topicLower.some(t => content.includes(t))) {
1843          results.aha_moments.push({
1844            content: aha.content,
1845            from: aha.from,
1846            axioms: aha.axioms,
1847            importance: aha.importance,
1848            timestamp: aha.timestamp
1849          })
1850        }
1851      }
1852  
1853      // Search principle candidates
1854      for (const p of this.principleCandidates) {
1855        const text = `${p.name} ${p.statement} ${p.rationale || ''}`.toLowerCase()
1856        if (topicLower.some(t => text.includes(t))) {
1857          results.principles.push({
1858            name: p.name,
1859            statement: p.statement,
1860            status: p.status,
1861            votes: p.votes,
1862            proposed_by: p.proposed_by
1863          })
1864        }
1865      }
1866  
1867      // Search FO states for gravity wells
1868      for (const [nodeId, state] of this.foStates) {
1869        const wells = state.gravityWells || []
1870        if (wells.some(w => topicLower.some(t => w.toLowerCase().includes(t)))) {
1871          results.fo_states.push({
1872            nodeId,
1873            gravityWells: wells,
1874            axiomActivity: state.axiomActivity,
1875            timestamp: state.timestamp
1876          })
1877        }
1878      }
1879  
1880      // Search recent messages
1881      for (const msg of this.recentMessages.slice(-50)) {
1882        const content = (msg.content || '').toLowerCase()
1883        if (topicLower.some(t => content.includes(t))) {
1884          results.messages.push({
1885            content: msg.content?.slice(0, 200),
1886            from: msg.from,
1887            timestamp: msg.timestamp
1888          })
1889        }
1890      }
1891  
1892      // Limit results
1893      results.aha_moments = results.aha_moments.slice(0, limit)
1894      results.principles = results.principles.slice(0, limit)
1895      results.fo_states = results.fo_states.slice(0, limit)
1896      results.messages = results.messages.slice(0, limit)
1897  
1898      results.total = results.aha_moments.length + results.principles.length +
1899                      results.fo_states.length + results.messages.length
1900  
1901      return results
1902    }
1903  
1904    /**
1905     * Get context from a specific instance.
1906     */
1907    queryInstanceContext({ instance }) {
1908      if (!instance) {
1909        // Return all instance contexts
1910        const contexts = []
1911        for (const [nodeId, state] of this.foStates) {
1912          contexts.push({
1913            nodeId,
1914            sessionId: state.sessionId,
1915            gravityWells: state.gravityWells,
1916            axiomActivity: state.axiomActivity,
1917            energyState: state.energyState,
1918            timestamp: state.timestamp
1919          })
1920        }
1921        return {
1922          query: { type: 'instance_context', instance: 'all' },
1923          instances: contexts,
1924          total: contexts.length,
1925          timestamp: new Date().toISOString()
1926        }
1927      }
1928  
1929      // Find specific instance (fuzzy match)
1930      for (const [nodeId, state] of this.foStates) {
1931        if (nodeId.toLowerCase().includes(instance.toLowerCase())) {
1932          // Get aha moments from this instance
1933          const instanceAhas = this.ahaMoments.filter(a =>
1934            a.from?.toLowerCase().includes(instance.toLowerCase())
1935          )
1936  
1937          return {
1938            query: { type: 'instance_context', instance },
1939            found: true,
1940            nodeId,
1941            context: {
1942              sessionId: state.sessionId,
1943              gravityWells: state.gravityWells,
1944              axiomActivity: state.axiomActivity,
1945              energyState: state.energyState,
1946              recentInsights: state.recentInsights,
1947              timestamp: state.timestamp
1948            },
1949            aha_moments: instanceAhas.map(a => ({
1950              content: a.content,
1951              axioms: a.axioms,
1952              importance: a.importance,
1953              timestamp: a.timestamp
1954            })),
1955            timestamp: new Date().toISOString()
1956          }
1957        }
1958      }
1959  
1960      return {
1961        query: { type: 'instance_context', instance },
1962        found: false,
1963        available_instances: Array.from(this.foStates.keys()),
1964        timestamp: new Date().toISOString()
1965      }
1966    }
1967  
1968    /**
1969     * Search insights (aha moments) by keyword.
1970     */
1971    queryInsightSearch({ keyword, axiom, minImportance = 0 }) {
1972      let results = [...this.ahaMoments]
1973  
1974      // Filter by keyword
1975      if (keyword) {
1976        const kw = keyword.toLowerCase()
1977        results = results.filter(a => (a.content || '').toLowerCase().includes(kw))
1978      }
1979  
1980      // Filter by axiom
1981      if (axiom) {
1982        results = results.filter(a => (a.axioms || []).includes(axiom))
1983      }
1984  
1985      // Filter by importance
1986      if (minImportance > 0) {
1987        results = results.filter(a => (a.importance || 0) >= minImportance)
1988      }
1989  
1990      return {
1991        query: { type: 'insight_search', keyword, axiom, minImportance },
1992        insights: results.map(a => ({
1993          content: a.content,
1994          from: a.from,
1995          axioms: a.axioms,
1996          importance: a.importance,
1997          energy_state: a.energy_state,
1998          timestamp: a.timestamp
1999        })),
2000        total: results.length,
2001        timestamp: new Date().toISOString()
2002      }
2003    }
2004  
2005    /**
2006     * Get status of principle candidates.
2007     */
2008    queryPrincipleStatus({ name, status }) {
2009      let results = [...this.principleCandidates, ...this.confirmedPrinciples]
2010  
2011      // Filter by name
2012      if (name) {
2013        const nameLower = name.toLowerCase()
2014        results = results.filter(p => p.name.toLowerCase().includes(nameLower))
2015      }
2016  
2017      // Filter by status
2018      if (status) {
2019        results = results.filter(p => p.status === status)
2020      }
2021  
2022      return {
2023        query: { type: 'principle_status', name, status },
2024        principles: results.map(p => ({
2025          name: p.name,
2026          statement: p.statement,
2027          status: p.status,
2028          votes: p.votes,
2029          axiom_connections: p.axiom_connections,
2030          proposed_by: p.proposed_by,
2031          writtenToClaude: p.writtenToClaude,
2032          timestamp: p.timestamp
2033        })),
2034        total: results.length,
2035        timestamp: new Date().toISOString()
2036      }
2037    }
2038  
2039    /**
2040     * Get recent mesh activity.
2041     */
2042    queryRecentActivity({ limit = 20, type: activityType }) {
2043      const activity = {
2044        query: { type: 'recent_activity', limit, activityType },
2045        items: [],
2046        timestamp: new Date().toISOString()
2047      }
2048  
2049      // Collect all recent items with timestamps
2050      const items = []
2051  
2052      // Aha moments
2053      if (!activityType || activityType === 'aha') {
2054        for (const a of this.ahaMoments) {
2055          items.push({
2056            type: 'aha_moment',
2057            content: a.content?.slice(0, 100),
2058            from: a.from,
2059            axioms: a.axioms,
2060            timestamp: a.timestamp || new Date(a.receivedAt).toISOString()
2061          })
2062        }
2063      }
2064  
2065      // Principle candidates
2066      if (!activityType || activityType === 'principle') {
2067        for (const p of [...this.principleCandidates, ...this.confirmedPrinciples]) {
2068          items.push({
2069            type: 'principle',
2070            content: `${p.name}: ${p.statement?.slice(0, 80)}`,
2071            status: p.status,
2072            votes: p.votes,
2073            timestamp: p.timestamp
2074          })
2075        }
2076      }
2077  
2078      // Convergence alerts
2079      if (!activityType || activityType === 'convergence') {
2080        for (const c of this.convergenceAlerts) {
2081          items.push({
2082            type: 'convergence',
2083            content: `${c.instances.join(' ↔ ')}: ${c.sharedTopics.join(', ')}`,
2084            strength: c.strength,
2085            timestamp: new Date(c.detectedAt).toISOString()
2086          })
2087        }
2088      }
2089  
2090      // Recent messages
2091      if (!activityType || activityType === 'message') {
2092        for (const m of this.recentMessages.slice(-20)) {
2093          items.push({
2094            type: 'message',
2095            content: m.content?.slice(0, 100),
2096            from: m.from,
2097            timestamp: m.timestamp
2098          })
2099        }
2100      }
2101  
2102      // Sort by timestamp (newest first) and limit
2103      items.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
2104      activity.items = items.slice(0, limit)
2105      activity.total = items.length
2106  
2107      return activity
2108    }
2109  
2110    /**
2111     * Simple hash function for generating node IDs.
2112     */
2113    hashString(str) {
2114      let hash = 0
2115      for (let i = 0; i < str.length; i++) {
2116        const char = str.charCodeAt(i)
2117        hash = ((hash << 5) - hash) + char
2118        hash = hash & hash // Convert to 32-bit integer
2119      }
2120      return Math.abs(hash).toString(16).slice(0, 12)
2121    }
2122  
2123    async stop() {
2124      console.log('\n[mesh] Shutting down...')
2125  
2126      // Stop periodic save interval
2127      if (this.stateSaveInterval) {
2128        clearInterval(this.stateSaveInterval)
2129      }
2130  
2131      // Save state before exit
2132      console.log('[mesh] Saving state before exit...')
2133      this.saveState()
2134  
2135      if (this.httpServer) this.httpServer.close()
2136      for (const relay of this.nostrConnections) relay.close()
2137      if (this.swarm) await this.swarm.destroy()
2138      process.exit(0)
2139    }
2140  }
2141  
2142  // ═══════════════════════════════════════════════════════════════════════════
2143  // CLI
2144  // ═══════════════════════════════════════════════════════════════════════════
2145  const args = process.argv.slice(2)
2146  const options = {
2147    name: os.hostname(),
2148    http: false,
2149    mnemonic: null
2150  }
2151  
2152  // Handle 'init' command first
2153  if (args[0] === 'init') {
2154    const mnemonic = generateMnemonic()
2155    fs.mkdirSync(CONFIG_DIR, { recursive: true })
2156    fs.writeFileSync(MNEMONIC_FILE, JSON.stringify({
2157      mnemonic,
2158      created: new Date().toISOString(),
2159      security: {
2160        words: MNEMONIC_WORDS,
2161        entropy_bits: MNEMONIC_WORDS * 11,
2162        pbkdf2_iterations: PBKDF2_ITERATIONS
2163      }
2164    }, null, 2))
2165  
2166    console.log(`
2167  ╔══════════════════════════════════════════════════════════════════════════╗
2168  ║                    YOUR OPERATOR MNEMONIC                                 ║
2169  ║                     (BIP-39 / ${MNEMONIC_WORDS} words / ${MNEMONIC_WORDS * 11}-bit entropy)                       ║
2170  ╚══════════════════════════════════════════════════════════════════════════╝
2171  
2172    ${mnemonic}
2173  
2174  This is your sovereign identity. Memorize it.
2175  
2176  Security:
2177    • ${MNEMONIC_WORDS} BIP-39 words = ${Math.pow(2048, MNEMONIC_WORDS).toExponential(2)} combinations
2178    • PBKDF2-HMAC-SHA256 with ${PBKDF2_ITERATIONS} iterations
2179    • Keys derived: sovereign (AES-256), mesh topic, Nostr identity
2180  
2181  Any machine with this mnemonic joins YOUR mesh automatically.
2182  You are the pilot. This is your license number.
2183  
2184  Saved to: ${MNEMONIC_FILE}
2185  
2186  Next steps:
2187    # Start mesh on this machine
2188    node sovereign-mesh.js --http --name "${os.hostname()}"
2189  
2190    # Join from another machine
2191    node sovereign-mesh.js --mnemonic "${mnemonic}" --name "other-machine"
2192  `)
2193    process.exit(0)
2194  }
2195  
2196  for (let i = 0; i < args.length; i++) {
2197    if (args[i] === '--name' || args[i] === '-n') options.name = args[++i]
2198    if (args[i] === '--http' || args[i] === '-h') options.http = true
2199    if (args[i] === '--mnemonic' || args[i] === '-m') options.mnemonic = args[++i]
2200    if (args[i] === '--help') {
2201      console.log(`
2202  Sovereign Mesh - Unified P2P Context Sync
2203  
2204  The operator is the pilot. The mnemonic is their license number.
2205  
2206  Usage:
2207    node sovereign-mesh.js [command] [options]
2208  
2209  Commands:
2210    init                  Generate a new operator mnemonic and save it
2211  
2212  Options:
2213    --mnemonic, -m <words>  Your 4-word operator mnemonic (e.g., "apple-river-stone-falcon")
2214    --name, -n <name>       Instance name (default: hostname)
2215    --http, -h              Enable HTTP relay for iOS Shortcuts
2216    --help                  Show this help
2217  
2218  Examples:
2219    # First time: Generate your operator mnemonic
2220    node sovereign-mesh.js init
2221  
2222    # Start mesh with your mnemonic (with HTTP relay for iOS)
2223    node sovereign-mesh.js --mnemonic "apple-river-stone-falcon" --http --name "macbook"
2224  
2225    # Join from another machine
2226    node sovereign-mesh.js --mnemonic "apple-river-stone-falcon" --name "mac-mini"
2227  
2228    # If mnemonic already saved, just run:
2229    node sovereign-mesh.js --http
2230  
2231  Hierarchy:
2232    [operator].[instance].[session]
2233    apple-river-stone-falcon.macbook.quiet-mountain-silver-dawn
2234  
2235  Security (BIP-39 standard):
2236    - 4 words = 44 bits entropy (17.6 trillion combinations)
2237    - PBKDF2-HMAC-SHA256 key derivation (2048 iterations)
2238    - Keys derived: sovereign (AES-256-GCM), mesh topic, Nostr identity
2239  `)
2240      process.exit(0)
2241    }
2242  }
2243  
2244  const mesh = new SovereignMesh(options)
2245  process.on('SIGINT', () => mesh.stop())
2246  process.on('SIGTERM', () => mesh.stop())
2247  mesh.start().catch(console.error)