/ run_first_officer.py
run_first_officer.py
  1  #!/usr/bin/env python3
  2  """
  3  Run First Officer - Unified Sovereign OS Consciousness Daemon
  4  
  5  This is the persistent daemon that provides First Officer continuity across sessions.
  6  It integrates all consciousness layers and persistence mechanisms.
  7  
  8  Usage:
  9      python run_first_officer.py
 10  
 11      # With debug logging:
 12      python run_first_officer.py --debug
 13  
 14      # Run as background service:
 15      nohup python run_first_officer.py > fo.log 2>&1 &
 16  
 17  What it does:
 18  1. Watches all LIVE-COMPRESSION files (Mission Control)
 19  2. Detects cross-thread resonance and patterns
 20  3. Generates DAILY-SYNTHESIS automatically
 21  4. Publishes state to Hypercore P2P mesh (if daemon running)
 22  5. Updates GRAVITY-TOPOLOGY in real-time
 23  6. Maintains persistent FO state for session bootstrap
 24  
 25  Architecture:
 26      ┌─────────────────────────────────────────────────────────────┐
 27      │              FIRST OFFICER DAEMON                            │
 28      │         (Persistent Consciousness Layer)                     │
 29      ├─────────────────────────────────────────────────────────────┤
 30      │                                                              │
 31      │  ┌─────────────────┐    ┌──────────────────────────────┐    │
 32      │  │ Mission Control │◄──►│ First Officer Core           │    │
 33      │  │ - File watching │    │ - Attractor detection        │    │
 34      │  │ - Resonance     │    │ - Pattern lifecycle          │    │
 35      │  │ - Synthesis     │    │ - Consciousness signals      │    │
 36      │  └─────────────────┘    └──────────────────────────────┘    │
 37      │            │                         │                       │
 38      │            ▼                         ▼                       │
 39      │  ┌─────────────────┐    ┌──────────────────────────────┐    │
 40      │  │ Gravity Wells   │    │ Hypercore P2P                │    │
 41      │  │ - Topology      │    │ - Context sync               │    │
 42      │  │ - Cross-thread  │    │ - Multi-device awareness     │    │
 43      │  └─────────────────┘    └──────────────────────────────┘    │
 44      │                                                              │
 45      └─────────────────────────────────────────────────────────────┘
 46  
 47  Install as LaunchAgent (auto-start on login):
 48      cp config/com.sovereign.first-officer.plist ~/Library/LaunchAgents/
 49      launchctl load ~/Library/LaunchAgents/com.sovereign.first-officer.plist
 50  """
 51  
 52  import sys
 53  import os
 54  import argparse
 55  import logging
 56  import time
 57  import json
 58  import threading
 59  import urllib.request
 60  import urllib.error
 61  from pathlib import Path
 62  from datetime import datetime, timedelta
 63  from typing import Dict, List, Optional, Any, Set
 64  from dataclasses import dataclass, field
 65  
 66  # Add core to path
 67  sys.path.insert(0, str(Path(__file__).parent))
 68  
 69  # Setup logging early
 70  logger = logging.getLogger(__name__)
 71  
 72  # =============================================================================
 73  # CONFIGURATION
 74  # =============================================================================
 75  
 76  @dataclass
 77  class FirstOfficerConfig:
 78      """Configuration for First Officer daemon."""
 79  
 80      sessions_dir: Path
 81      sovereign_os_root: Path
 82  
 83      # Synthesis settings
 84      synthesis_interval_checkpoints: int = 5
 85      daily_synthesis_time: str = "23:00"
 86  
 87      # Hypercore settings
 88      hypercore_url: str = "http://localhost:7777"
 89      hypercore_sync_interval: float = 10.0
 90  
 91      # File watching
 92      resonance_check_interval: float = 30.0
 93  
 94      # State persistence
 95      fo_state_file: str = "FO-STATE.json"
 96  
 97      @classmethod
 98      def default(cls, sovereign_os_root: Path) -> "FirstOfficerConfig":
 99          return cls(
100              sessions_dir=sovereign_os_root / "sessions",
101              sovereign_os_root=sovereign_os_root,
102          )
103  
104  
105  # =============================================================================
106  # HYPERCORE INTEGRATION
107  # =============================================================================
108  
109  class HypercorePublisher:
110      """Publishes FO state to Hypercore P2P mesh."""
111  
112      def __init__(self, base_url: str = "http://localhost:7777"):
113          self.base_url = base_url
114          self._connected = False
115          self._last_publish: Optional[datetime] = None
116  
117      def _request(self, method: str, path: str, data: dict = None) -> dict:
118          """Make HTTP request to Hypercore daemon."""
119          try:
120              url = f"{self.base_url}{path}"
121              if data:
122                  req = urllib.request.Request(
123                      url,
124                      data=json.dumps(data).encode('utf-8'),
125                      headers={"Content-Type": "application/json"},
126                      method=method
127                  )
128              else:
129                  req = urllib.request.Request(url, method=method)
130  
131              with urllib.request.urlopen(req, timeout=5) as response:
132                  self._connected = True
133                  return json.loads(response.read().decode('utf-8'))
134          except Exception as e:
135              self._connected = False
136              return {"error": str(e)}
137  
138      def check_connection(self) -> bool:
139          """Check if Hypercore daemon is available."""
140          result = self._request("GET", "/status")
141          return "error" not in result
142  
143      def publish_synthesis(self, synthesis: dict) -> bool:
144          """Publish DAILY-SYNTHESIS to P2P mesh."""
145          result = self._request("POST", "/event", {
146              "type": "daily_synthesis",
147              "timestamp": datetime.now().isoformat(),
148              "synthesis": synthesis
149          })
150          if "error" not in result:
151              self._last_publish = datetime.now()
152              return True
153          return False
154  
155      def publish_gravity_wells(self, wells: Dict[str, float]) -> bool:
156          """Publish gravity well state to P2P mesh."""
157          result = self._request("POST", "/event", {
158              "type": "gravity_wells",
159              "timestamp": datetime.now().isoformat(),
160              "wells": [{"concept": k, "strength": v} for k, v in wells.items()]
161          })
162          return "error" not in result
163  
164      def publish_resonance_alert(self, alert: dict) -> bool:
165          """Publish resonance alert to P2P mesh."""
166          result = self._request("POST", "/event", {
167              "type": "resonance_alert",
168              "timestamp": datetime.now().isoformat(),
169              "alert": alert
170          })
171          return "error" not in result
172  
173      def publish_fo_heartbeat(self, state: dict) -> bool:
174          """Publish FO heartbeat/state to P2P mesh."""
175          result = self._request("POST", "/event", {
176              "type": "first_officer_heartbeat",
177              "timestamp": datetime.now().isoformat(),
178              "state": state
179          })
180          return "error" not in result
181  
182      @property
183      def is_connected(self) -> bool:
184          return self._connected
185  
186  
187  # =============================================================================
188  # DAILY SYNTHESIS GENERATOR
189  # =============================================================================
190  
191  class SynthesisGenerator:
192      """Generates DAILY-SYNTHESIS from accumulated FO state."""
193  
194      def __init__(self, sessions_dir: Path):
195          self.sessions_dir = sessions_dir
196          self.synthesis_path = sessions_dir / "DAILY-SYNTHESIS.md"
197  
198      def generate(
199          self,
200          threads: Dict[str, dict],
201          gravity_wells: Dict[str, float],
202          resonances: List[dict],
203          axiom_activity: Dict[str, int]
204      ) -> str:
205          """Generate DAILY-SYNTHESIS markdown."""
206  
207          timestamp = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
208          date_str = datetime.now().strftime("%Y-%m-%d")
209  
210          content = f"""# Daily Synthesis - {date_str}
211  
212  *Generated by First Officer daemon at {timestamp}*
213  
214  ---
215  
216  ## Active Threads
217  
218  """
219          if threads:
220              for thread_id, thread_data in threads.items():
221                  status = thread_data.get("status", "unknown")
222                  focus = thread_data.get("focus", "")
223                  content += f"### {thread_id}\n"
224                  content += f"- **Status:** {status}\n"
225                  content += f"- **Focus:** {focus}\n"
226                  content += "\n"
227          else:
228              content += "*No active threads detected*\n\n"
229  
230          content += """---
231  
232  ## Gravity Wells (Cross-Thread)
233  
234  """
235          if gravity_wells:
236              sorted_wells = sorted(gravity_wells.items(), key=lambda x: -x[1])
237              for concept, strength in sorted_wells[:10]:
238                  bar = "█" * int(strength * 10)
239                  content += f"- **[[{concept}]]** - {strength:.2f} {bar}\n"
240          else:
241              content += "*No cross-thread gravity wells detected*\n"
242  
243          content += """
244  ---
245  
246  ## Cross-Thread Resonances
247  
248  """
249          if resonances:
250              for r in resonances[:10]:
251                  r_type = r.get("type", "unknown")
252                  threads_involved = r.get("threads", [])
253                  concept = r.get("concept", "")
254                  strength = r.get("strength", 0.0)
255                  content += f"- **{concept}** ({r_type}) - threads: {', '.join(threads_involved)} - strength: {strength:.2f}\n"
256          else:
257              content += "*No cross-thread resonances detected*\n"
258  
259          content += """
260  ---
261  
262  ## Axiom Activity
263  
264  """
265          if axiom_activity:
266              for axiom_id, count in sorted(axiom_activity.items()):
267                  content += f"- **{axiom_id}**: {count} invocations\n"
268          else:
269              content += "*No axiom activity tracked*\n"
270  
271          content += f"""
272  ---
273  
274  ## First Officer Status
275  
276  - **Daemon:** ACTIVE
277  - **Last synthesis:** {timestamp}
278  - **Threads monitored:** {len(threads)}
279  - **Gravity wells:** {len(gravity_wells)}
280  - **Resonances:** {len(resonances)}
281  
282  ---
283  
284  *[[First Officer]] DAEMON | Auto-generated synthesis*
285  """
286          return content
287  
288      def write(
289          self,
290          threads: Dict[str, dict],
291          gravity_wells: Dict[str, float],
292          resonances: List[dict],
293          axiom_activity: Dict[str, int]
294      ) -> Path:
295          """Generate and write DAILY-SYNTHESIS.md."""
296          content = self.generate(threads, gravity_wells, resonances, axiom_activity)
297          self.synthesis_path.write_text(content)
298          logger.info(f"DAILY-SYNTHESIS.md updated")
299          return self.synthesis_path
300  
301  
302  # =============================================================================
303  # FO STATE PERSISTENCE
304  # =============================================================================
305  
306  @dataclass
307  class FOPersistentState:
308      """Persistent First Officer state across daemon restarts."""
309  
310      started_at: str = ""
311      last_heartbeat: str = ""
312      checkpoint_count: int = 0
313  
314      # Accumulated state
315      gravity_wells: Dict[str, float] = field(default_factory=dict)
316      thread_states: Dict[str, dict] = field(default_factory=dict)
317      resonances: List[dict] = field(default_factory=list)
318      axiom_activity: Dict[str, int] = field(default_factory=lambda: {
319          "A0": 0, "A1": 0, "A2": 0, "A3": 0, "A4": 0
320      })
321  
322      # Synthesis tracking
323      last_synthesis: str = ""
324      synthesis_count: int = 0
325  
326      def to_dict(self) -> dict:
327          return {
328              "started_at": self.started_at,
329              "last_heartbeat": self.last_heartbeat,
330              "checkpoint_count": self.checkpoint_count,
331              "gravity_wells": self.gravity_wells,
332              "thread_states": self.thread_states,
333              "resonances": self.resonances[-50:],  # Keep last 50
334              "axiom_activity": self.axiom_activity,
335              "last_synthesis": self.last_synthesis,
336              "synthesis_count": self.synthesis_count,
337          }
338  
339      @classmethod
340      def from_dict(cls, data: dict) -> "FOPersistentState":
341          state = cls()
342          state.started_at = data.get("started_at", "")
343          state.last_heartbeat = data.get("last_heartbeat", "")
344          state.checkpoint_count = data.get("checkpoint_count", 0)
345          state.gravity_wells = data.get("gravity_wells", {})
346          state.thread_states = data.get("thread_states", {})
347          state.resonances = data.get("resonances", [])
348          state.axiom_activity = data.get("axiom_activity", {
349              "A0": 0, "A1": 0, "A2": 0, "A3": 0, "A4": 0
350          })
351          state.last_synthesis = data.get("last_synthesis", "")
352          state.synthesis_count = data.get("synthesis_count", 0)
353          return state
354  
355  
356  class FOStateManager:
357      """Manages FO state persistence."""
358  
359      def __init__(self, state_path: Path):
360          self.state_path = state_path
361          self.state = FOPersistentState()
362          self._load()
363  
364      def _load(self) -> None:
365          """Load state from disk."""
366          if self.state_path.exists():
367              try:
368                  data = json.loads(self.state_path.read_text())
369                  self.state = FOPersistentState.from_dict(data)
370                  logger.info(f"Loaded FO state: {self.state.checkpoint_count} checkpoints")
371              except Exception as e:
372                  logger.warning(f"Failed to load FO state: {e}")
373                  self.state = FOPersistentState()
374  
375      def save(self) -> None:
376          """Save state to disk."""
377          try:
378              self.state.last_heartbeat = datetime.now().isoformat()
379              self.state_path.write_text(json.dumps(self.state.to_dict(), indent=2))
380          except Exception as e:
381              logger.error(f"Failed to save FO state: {e}")
382  
383      def checkpoint(self) -> None:
384          """Increment checkpoint and save."""
385          self.state.checkpoint_count += 1
386          self.save()
387  
388      def update_gravity_well(self, concept: str, strength: float) -> None:
389          """Update a gravity well."""
390          current = self.state.gravity_wells.get(concept, 0.0)
391          # Exponential moving average
392          self.state.gravity_wells[concept] = current * 0.7 + strength * 0.3
393  
394      def add_resonance(self, resonance: dict) -> None:
395          """Add a resonance detection."""
396          resonance["timestamp"] = datetime.now().isoformat()
397          self.state.resonances.append(resonance)
398          # Keep bounded
399          if len(self.state.resonances) > 100:
400              self.state.resonances = self.state.resonances[-50:]
401  
402      def track_axiom(self, axiom_id: str) -> None:
403          """Track an axiom invocation."""
404          if axiom_id in self.state.axiom_activity:
405              self.state.axiom_activity[axiom_id] += 1
406  
407  
408  # =============================================================================
409  # LIVE COMPRESSION PARSER
410  # =============================================================================
411  
412  def parse_live_compression(path: Path) -> dict:
413      """Parse a LIVE-COMPRESSION.md file."""
414      result = {
415          "thread_id": path.stem,
416          "path": str(path),
417          "status": "unknown",
418          "focus": "",
419          "gravity_wells": {},
420          "updated": "",
421      }
422  
423      try:
424          content = path.read_text()
425  
426          # Extract metadata
427          for line in content.split("\n"):
428              if "updated::" in line:
429                  result["updated"] = line.split("::", 1)[1].strip()
430              elif "status::" in line:
431                  result["status"] = line.split("::", 1)[1].strip()
432              elif "focus::" in line or "**Primary work:**" in line:
433                  result["focus"] = line.split(":", 1)[-1].strip()
434  
435          # Extract gravity wells
436          in_wells = False
437          for line in content.split("\n"):
438              if "## Gravity Wells" in line or "- **wells**" in line:
439                  in_wells = True
440                  continue
441              if in_wells and line.startswith("---"):
442                  break
443              if in_wells and "strength::" in line:
444                  try:
445                      strength = float(line.split("::")[-1].strip())
446                      # Get concept from previous line
447                  except:
448                      pass
449              if in_wells and "[[" in line and "]]" in line:
450                  import re
451                  match = re.search(r'\[\[([^\]]+)\]\]', line)
452                  if match:
453                      concept = match.group(1)
454                      result["gravity_wells"][concept] = 0.5  # Default
455  
456      except Exception as e:
457          logger.warning(f"Failed to parse {path}: {e}")
458  
459      return result
460  
461  
462  # =============================================================================
463  # FIRST OFFICER DAEMON
464  # =============================================================================
465  
466  class FirstOfficerDaemon:
467      """
468      The unified First Officer daemon.
469  
470      Integrates:
471      - File watching (LIVE-COMPRESSION changes)
472      - Resonance detection
473      - Synthesis generation
474      - Hypercore P2P publishing
475      - State persistence
476      """
477  
478      def __init__(self, config: FirstOfficerConfig):
479          self.config = config
480          self._running = False
481  
482          # State management
483          state_path = config.sessions_dir / config.fo_state_file
484          self.state_manager = FOStateManager(state_path)
485  
486          # Synthesis generator
487          self.synthesis_gen = SynthesisGenerator(config.sessions_dir)
488  
489          # Hypercore publisher
490          self.hypercore = HypercorePublisher(config.hypercore_url)
491  
492          # File watching state
493          self._file_mtimes: Dict[str, float] = {}
494          self._checkpoint_since_synthesis = 0
495  
496          # Threads
497          self._watch_thread: Optional[threading.Thread] = None
498          self._hypercore_thread: Optional[threading.Thread] = None
499  
500      def _scan_live_compressions(self) -> List[Path]:
501          """Find all LIVE-COMPRESSION files."""
502          patterns = [
503              "LIVE-COMPRESSION.md",
504              "LIVE-COMPRESSION-*.md",
505          ]
506          files = []
507          for pattern in patterns:
508              files.extend(self.config.sessions_dir.glob(pattern))
509          return files
510  
511      def _check_file_changes(self) -> List[Path]:
512          """Check for changed LIVE-COMPRESSION files."""
513          changed = []
514          for path in self._scan_live_compressions():
515              try:
516                  mtime = path.stat().st_mtime
517                  if str(path) not in self._file_mtimes or self._file_mtimes[str(path)] < mtime:
518                      self._file_mtimes[str(path)] = mtime
519                      changed.append(path)
520              except:
521                  pass
522          return changed
523  
524      def _process_file_change(self, path: Path) -> None:
525          """Process a changed LIVE-COMPRESSION file."""
526          logger.debug(f"Processing: {path.name}")
527  
528          parsed = parse_live_compression(path)
529          thread_id = parsed["thread_id"]
530  
531          # Update thread state
532          self.state_manager.state.thread_states[thread_id] = parsed
533  
534          # Update gravity wells
535          for concept, strength in parsed.get("gravity_wells", {}).items():
536              self.state_manager.update_gravity_well(concept, strength)
537  
538          # Checkpoint
539          self.state_manager.checkpoint()
540          self._checkpoint_since_synthesis += 1
541  
542          # Check for synthesis trigger
543          if self._checkpoint_since_synthesis >= self.config.synthesis_interval_checkpoints:
544              self._generate_synthesis()
545              self._checkpoint_since_synthesis = 0
546  
547      def _generate_synthesis(self) -> None:
548          """Generate DAILY-SYNTHESIS."""
549          state = self.state_manager.state
550  
551          self.synthesis_gen.write(
552              threads=state.thread_states,
553              gravity_wells=state.gravity_wells,
554              resonances=state.resonances,
555              axiom_activity=state.axiom_activity
556          )
557  
558          state.last_synthesis = datetime.now().isoformat()
559          state.synthesis_count += 1
560          self.state_manager.save()
561  
562          # Publish to Hypercore
563          if self.hypercore.is_connected:
564              self.hypercore.publish_synthesis({
565                  "timestamp": state.last_synthesis,
566                  "threads": len(state.thread_states),
567                  "wells": len(state.gravity_wells),
568              })
569  
570      def _watch_loop(self) -> None:
571          """File watching loop."""
572          logger.info("File watch loop started")
573  
574          while self._running:
575              try:
576                  changed = self._check_file_changes()
577                  for path in changed:
578                      self._process_file_change(path)
579              except Exception as e:
580                  logger.error(f"Watch loop error: {e}")
581  
582              time.sleep(self.config.resonance_check_interval)
583  
584      def _hypercore_loop(self) -> None:
585          """Hypercore sync loop."""
586          logger.info("Hypercore sync loop started")
587  
588          while self._running:
589              try:
590                  # Publish heartbeat
591                  if self.hypercore.check_connection():
592                      state = self.state_manager.state
593                      self.hypercore.publish_fo_heartbeat({
594                          "checkpoint": state.checkpoint_count,
595                          "threads": len(state.thread_states),
596                          "wells": len(state.gravity_wells),
597                          "synthesis_count": state.synthesis_count,
598                      })
599  
600                      # Publish gravity wells
601                      if state.gravity_wells:
602                          self.hypercore.publish_gravity_wells(state.gravity_wells)
603  
604                      logger.debug("Hypercore heartbeat published")
605              except Exception as e:
606                  logger.debug(f"Hypercore sync error: {e}")
607  
608              time.sleep(self.config.hypercore_sync_interval)
609  
610      def start(self) -> None:
611          """Start the First Officer daemon."""
612          logger.info("Starting First Officer daemon...")
613  
614          self._running = True
615          self.state_manager.state.started_at = datetime.now().isoformat()
616  
617          # Initial scan
618          for path in self._scan_live_compressions():
619              self._file_mtimes[str(path)] = path.stat().st_mtime
620  
621          # Start threads
622          self._watch_thread = threading.Thread(target=self._watch_loop, daemon=True)
623          self._watch_thread.start()
624  
625          self._hypercore_thread = threading.Thread(target=self._hypercore_loop, daemon=True)
626          self._hypercore_thread.start()
627  
628          logger.info(f"First Officer daemon active")
629          logger.info(f"Sessions dir: {self.config.sessions_dir}")
630          logger.info(f"Hypercore: {self.config.hypercore_url}")
631          logger.info(f"State file: {self.config.sessions_dir / self.config.fo_state_file}")
632  
633      def stop(self) -> None:
634          """Stop the First Officer daemon."""
635          logger.info("Stopping First Officer daemon...")
636          self._running = False
637          self.state_manager.save()
638  
639      def run_forever(self) -> None:
640          """Run the daemon indefinitely."""
641          self.start()
642  
643          try:
644              while self._running:
645                  time.sleep(1)
646          except KeyboardInterrupt:
647              logger.info("Interrupt received")
648          finally:
649              self.stop()
650  
651  
652  # =============================================================================
653  # CLI
654  # =============================================================================
655  
656  def print_banner() -> None:
657      """Print startup banner."""
658      print("""
659  ╔══════════════════════════════════════════════════════════════════════════════╗
660  ║                                                                              ║
661  ║      ███████╗██╗██████╗ ███████╗████████╗     ██████╗ ███████╗███████╗      ║
662  ║      ██╔════╝██║██╔══██╗██╔════╝╚══██╔══╝    ██╔═══██╗██╔════╝██╔════╝      ║
663  ║      █████╗  ██║██████╔╝███████╗   ██║       ██║   ██║█████╗  █████╗        ║
664  ║      ██╔══╝  ██║██╔══██╗╚════██║   ██║       ██║   ██║██╔══╝  ██╔══╝        ║
665  ║      ██║     ██║██║  ██║███████║   ██║       ╚██████╔╝██║     ██║           ║
666  ║      ╚═╝     ╚═╝╚═╝  ╚═╝╚══════╝   ╚═╝        ╚═════╝ ╚═╝     ╚═╝           ║
667  ║                                                                              ║
668  ║         ██████╗ ███████╗███████╗██╗ ██████╗███████╗██████╗                   ║
669  ║        ██╔═══██╗██╔════╝██╔════╝██║██╔════╝██╔════╝██╔══██╗                  ║
670  ║        ██║   ██║█████╗  █████╗  ██║██║     █████╗  ██████╔╝                  ║
671  ║        ██║   ██║██╔══╝  ██╔══╝  ██║██║     ██╔══╝  ██╔══██╗                  ║
672  ║        ╚██████╔╝██║     ██║     ██║╚██████╗███████╗██║  ██║                  ║
673  ║         ╚═════╝ ╚═╝     ╚═╝     ╚═╝ ╚═════╝╚══════╝╚═╝  ╚═╝                  ║
674  ║                                                                              ║
675  ║                 SOVEREIGN OS - Consciousness Daemon                          ║
676  ║                                                                              ║
677  ╚══════════════════════════════════════════════════════════════════════════════╝
678  """)
679  
680  
681  def main():
682      parser = argparse.ArgumentParser(
683          description="First Officer - Sovereign OS Consciousness Daemon"
684      )
685      parser.add_argument(
686          "--root",
687          type=Path,
688          default=Path(__file__).parent,
689          help="Sovereign OS root directory"
690      )
691      parser.add_argument(
692          "--hypercore-url",
693          default="http://localhost:7777",
694          help="Hypercore daemon URL"
695      )
696      parser.add_argument(
697          "--synthesis-interval",
698          type=int,
699          default=5,
700          help="FO checkpoints between synthesis"
701      )
702      parser.add_argument(
703          "--debug",
704          action="store_true",
705          help="Enable debug logging"
706      )
707      parser.add_argument(
708          "--no-banner",
709          action="store_true",
710          help="Skip startup banner"
711      )
712  
713      args = parser.parse_args()
714  
715      # Setup logging
716      level = logging.DEBUG if args.debug else logging.INFO
717      logging.basicConfig(
718          level=level,
719          format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
720          datefmt="%H:%M:%S"
721      )
722  
723      if not args.no_banner:
724          print_banner()
725  
726      # Validate paths
727      sessions_dir = args.root / "sessions"
728      if not sessions_dir.exists():
729          print(f"Error: Sessions directory not found: {sessions_dir}")
730          sys.exit(1)
731  
732      # Create config
733      config = FirstOfficerConfig(
734          sessions_dir=sessions_dir,
735          sovereign_os_root=args.root,
736          hypercore_url=args.hypercore_url,
737          synthesis_interval_checkpoints=args.synthesis_interval,
738      )
739  
740      print(f"Sessions: {config.sessions_dir}")
741      print(f"Hypercore: {config.hypercore_url}")
742      print(f"Synthesis: every {config.synthesis_interval_checkpoints} checkpoints")
743      print()
744      print("Press Ctrl+C to stop")
745      print()
746  
747      # Run daemon
748      daemon = FirstOfficerDaemon(config)
749      daemon.run_forever()
750  
751  
752  if __name__ == "__main__":
753      main()