@smilintux/skcapstone 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -42,7 +42,7 @@ from . import AGENT_HOME, __version__
42
42
 
43
43
  console = Console()
44
44
 
45
- TOTAL_STEPS = 13 # excludes welcome + celebrate; includes 4 new system-setup steps
45
+ TOTAL_STEPS = 16 # excludes welcome + celebrate; includes pillar install + import step
46
46
 
47
47
 
48
48
  def _step_header(n: int, title: str) -> None:
@@ -479,67 +479,459 @@ def _step_prereqs() -> dict:
479
479
  return results
480
480
 
481
481
 
482
- def _step_ollama_models(prereqs: dict) -> bool:
483
- """Pull the default Ollama model (llama3.2).
482
+ # Pillar packages: (import_name, pip_name, description)
483
+ _PILLAR_PACKAGES = [
484
+ ("capauth", "capauth", "PGP-based sovereign identity"),
485
+ ("skcomm", "skcomm", "Redundant agent communication"),
486
+ ("skchat", "skchat-sovereign", "Encrypted P2P chat"),
487
+ ("skseed", "skseed", "Cloud 9 seeds & LLM callbacks"),
488
+ ("sksecurity", "sksecurity", "Audit logging & threat detection"),
489
+ ("pgpy", "pgpy", "PGP cryptography (PGPy backend)"),
490
+ ]
491
+
492
+
493
+ def _step_install_pillars() -> dict:
494
+ """Detect missing pillar packages and offer to install them.
495
+
496
+ Returns:
497
+ dict mapping pip_name -> bool (installed successfully).
498
+ """
499
+ import subprocess
500
+
501
+ results = {}
502
+ missing = []
503
+
504
+ click.echo(click.style(" Checking pillar packages…", fg="bright_black"))
505
+ for import_name, pip_name, description in _PILLAR_PACKAGES:
506
+ try:
507
+ __import__(import_name)
508
+ click.echo(click.style(" ✓ ", fg="green") + f"{pip_name} — {description}")
509
+ results[pip_name] = True
510
+ except ImportError:
511
+ click.echo(click.style(" ✗ ", fg="red") + f"{pip_name} — {description} [bold red](missing)[/]")
512
+ missing.append((import_name, pip_name, description))
513
+ results[pip_name] = False
514
+
515
+ if not missing:
516
+ click.echo()
517
+ click.echo(click.style(" ✓ ", fg="green") + "All pillar packages installed")
518
+ return results
519
+
520
+ click.echo()
521
+ click.echo(
522
+ click.style(" ℹ ", fg="cyan")
523
+ + f"{len(missing)} pillar(s) missing. These are needed for full sovereign functionality."
524
+ )
525
+
526
+ choices = {
527
+ "a": "Install all missing pillars",
528
+ "s": "Select which to install",
529
+ "n": "Skip (install later manually)",
530
+ }
531
+ for key, desc in choices.items():
532
+ click.echo(f" [{key}] {desc}")
533
+ choice = click.prompt(" Choice", default="a", show_choices=False).strip().lower()
534
+
535
+ to_install: list[tuple[str, str, str]] = []
536
+ if choice == "a":
537
+ to_install = missing
538
+ elif choice == "s":
539
+ for import_name, pip_name, description in missing:
540
+ if click.confirm(f" Install {pip_name} ({description})?", default=True):
541
+ to_install.append((import_name, pip_name, description))
542
+ else:
543
+ click.echo(click.style(" ↷ ", fg="bright_black") + "Skipped — install later:")
544
+ for _, pip_name, _ in missing:
545
+ click.echo(click.style(" ", fg="bright_black") + f"pip install {pip_name}")
546
+ return results
547
+
548
+ if not to_install:
549
+ return results
550
+
551
+ # Determine pip command — prefer ~/.skenv if it exists, else use current Python
552
+ import os as _os
553
+ skenv_pip = Path(_os.path.expanduser("~/.skenv/bin/pip"))
554
+ if skenv_pip.exists():
555
+ pip_cmd = [str(skenv_pip), "install"]
556
+ else:
557
+ pip_cmd = [sys.executable, "-m", "pip", "install", "--break-system-packages"]
558
+
559
+ for import_name, pip_name, description in to_install:
560
+ click.echo(click.style(" ↓ ", fg="cyan") + f"Installing {pip_name}…")
561
+ try:
562
+ r = subprocess.run(
563
+ [*pip_cmd, pip_name, "-q"],
564
+ capture_output=True, text=True, timeout=120,
565
+ )
566
+ if r.returncode == 0:
567
+ click.echo(click.style(" ✓ ", fg="green") + f"{pip_name} installed")
568
+ results[pip_name] = True
569
+ else:
570
+ click.echo(click.style(" ✗ ", fg="red") + f"{pip_name} failed: {r.stderr.strip()[:100]}")
571
+ click.echo(click.style(" ", fg="bright_black") + f"Try manually: pip install {pip_name}")
572
+ except subprocess.TimeoutExpired:
573
+ click.echo(click.style(" ⚠ ", fg="yellow") + f"{pip_name} timed out")
574
+ except Exception as exc:
575
+ click.echo(click.style(" ⚠ ", fg="yellow") + f"{pip_name}: {exc}")
576
+
577
+ return results
578
+
579
+
580
+ # ---------------------------------------------------------------------------
581
+ # Import sources — detect and import from existing agent platforms
582
+ # ---------------------------------------------------------------------------
583
+
584
+ # (source_id, display_name, detect_func, import_func_key)
585
+ _IMPORT_SOURCES: list[tuple[str, str, str]] = [
586
+ ("openclaw", "OpenClaw (Jarvis)", "~/.openclaw/workspace"),
587
+ ("claude", "Claude Code", "~/.claude"),
588
+ ("cloud9", "Cloud 9 FEB Templates", ""), # always available if cloud9_protocol installed
589
+ ]
590
+
591
+
592
+ def _detect_import_sources(home_path: Path) -> list[dict]:
593
+ """Detect available sources for importing memories, soul, and trust data.
594
+
595
+ Returns:
596
+ List of dicts with 'id', 'name', 'available', 'detail', 'items'.
597
+ """
598
+ sources = []
599
+
600
+ # --- OpenClaw ---
601
+ oc_workspace = Path.home() / ".openclaw" / "workspace"
602
+ oc_memory = oc_workspace / "memory"
603
+ oc_soul = oc_workspace / "SOUL.md"
604
+ oc_identity = oc_workspace / "IDENTITY.md"
605
+ oc_agents = oc_workspace / "agents"
606
+ if oc_workspace.exists():
607
+ items = []
608
+ if oc_memory.exists():
609
+ mem_files = list(oc_memory.glob("*.md"))
610
+ items.append(f"{len(mem_files)} memory files")
611
+ if oc_soul.exists():
612
+ items.append("SOUL.md")
613
+ if oc_identity.exists():
614
+ items.append("IDENTITY.md")
615
+ if oc_agents.exists():
616
+ agent_souls = list(oc_agents.rglob("SOUL.md"))
617
+ if agent_souls:
618
+ items.append(f"{len(agent_souls)} agent soul(s)")
619
+ sources.append({
620
+ "id": "openclaw",
621
+ "name": "OpenClaw (Jarvis)",
622
+ "available": True,
623
+ "detail": ", ".join(items) if items else "workspace found",
624
+ "paths": {
625
+ "memory": oc_memory,
626
+ "soul": oc_soul,
627
+ "identity": oc_identity,
628
+ "agents": oc_agents,
629
+ "workspace": oc_workspace,
630
+ },
631
+ })
632
+
633
+ # --- Claude Code ---
634
+ claude_dir = Path.home() / ".claude"
635
+ claude_memory = None
636
+ if claude_dir.exists():
637
+ # Find project memory dirs
638
+ projects = claude_dir / "projects"
639
+ items = []
640
+ if projects.exists():
641
+ for proj_dir in projects.iterdir():
642
+ mem_dir = proj_dir / "memory"
643
+ if mem_dir.exists() and list(mem_dir.glob("*.md")):
644
+ mem_files = list(mem_dir.glob("*.md"))
645
+ items.append(f"{len(mem_files)} memory file(s) in {proj_dir.name}")
646
+ claude_memory = mem_dir
647
+ memory_md = proj_dir / "MEMORY.md"
648
+ if memory_md.exists():
649
+ items.append(f"MEMORY.md in {proj_dir.name}")
650
+ if items:
651
+ sources.append({
652
+ "id": "claude",
653
+ "name": "Claude Code",
654
+ "available": True,
655
+ "detail": ", ".join(items),
656
+ "paths": {"memory": claude_memory, "projects": projects},
657
+ })
658
+
659
+ # --- Cloud 9 FEB Templates ---
660
+ try:
661
+ import cloud9_protocol
662
+ c9_pkg = Path(cloud9_protocol.__file__).parent
663
+ feb_files = list(c9_pkg.rglob("*.feb"))
664
+ # Also check skcapstone defaults
665
+ defaults_dir = Path(__file__).parent / "defaults"
666
+ if defaults_dir.exists():
667
+ feb_files.extend(defaults_dir.rglob("*.feb"))
668
+ # Check user cloud9 dirs
669
+ for cloud9_dir in [Path.home() / ".cloud9" / "febs", Path.home() / ".cloud9" / "feb-backups"]:
670
+ if cloud9_dir.exists():
671
+ feb_files.extend(cloud9_dir.glob("*.feb"))
672
+ if feb_files:
673
+ sources.append({
674
+ "id": "cloud9",
675
+ "name": "Cloud 9 FEB Templates",
676
+ "available": True,
677
+ "detail": f"{len(feb_files)} FEB file(s)",
678
+ "paths": {"febs": feb_files},
679
+ })
680
+ except ImportError:
681
+ pass
682
+
683
+ return sources
684
+
685
+
686
+ def _step_import_sources(home_path: Path) -> dict:
687
+ """Detect and import data from existing agent platforms.
688
+
689
+ Args:
690
+ home_path: Agent home directory.
691
+
692
+ Returns:
693
+ dict with 'imported_count' (int) and 'sources' (list of imported source ids).
694
+ """
695
+ import shutil as _shutil
696
+
697
+ result = {"imported_count": 0, "sources": []}
698
+
699
+ click.echo(click.style(" Scanning for existing agent data…", fg="bright_black"))
700
+ sources = _detect_import_sources(home_path)
701
+
702
+ if not sources:
703
+ click.echo(click.style(" ℹ ", fg="cyan") + "No existing agent data found — starting fresh")
704
+ return result
705
+
706
+ click.echo()
707
+ for i, src in enumerate(sources, 1):
708
+ click.echo(
709
+ click.style(f" {i}. ", fg="cyan")
710
+ + f"[bold]{src['name']}[/] — {src['detail']}"
711
+ )
712
+ click.echo()
713
+
714
+ choices = {
715
+ "a": "Import from all sources",
716
+ "s": "Select which to import",
717
+ "n": "Skip (start fresh)",
718
+ }
719
+ for key, desc in choices.items():
720
+ click.echo(f" [{key}] {desc}")
721
+ choice = click.prompt(" Choice", default="a", show_choices=False).strip().lower()
722
+
723
+ to_import: list[dict] = []
724
+ if choice == "a":
725
+ to_import = sources
726
+ elif choice == "s":
727
+ for src in sources:
728
+ if click.confirm(f" Import from {src['name']}?", default=True):
729
+ to_import.append(src)
730
+ else:
731
+ click.echo(click.style(" ↷ ", fg="bright_black") + "Skipped — starting fresh")
732
+ return result
733
+
734
+ if not to_import:
735
+ return result
736
+
737
+ # --- Execute imports ---
738
+ for src in to_import:
739
+ sid = src["id"]
740
+ paths = src.get("paths", {})
741
+ count = 0
742
+
743
+ if sid == "openclaw":
744
+ # Import memories
745
+ mem_src = paths.get("memory")
746
+ if mem_src and mem_src.exists():
747
+ mem_dest = home_path / "memory" / "imported" / "openclaw"
748
+ mem_dest.mkdir(parents=True, exist_ok=True)
749
+ for f in mem_src.glob("*.md"):
750
+ _shutil.copy2(f, mem_dest / f.name)
751
+ count += 1
752
+ click.echo(click.style(" ✓ ", fg="green") + f"Imported {count} memory files from OpenClaw")
753
+
754
+ # Import soul/identity
755
+ for doc_name in ("soul", "identity"):
756
+ doc_path = paths.get(doc_name)
757
+ if doc_path and doc_path.exists():
758
+ dest = home_path / "memory" / "imported" / "openclaw" / doc_path.name
759
+ dest.parent.mkdir(parents=True, exist_ok=True)
760
+ _shutil.copy2(doc_path, dest)
761
+ count += 1
762
+ click.echo(click.style(" ✓ ", fg="green") + f"Imported {doc_path.name} from OpenClaw")
763
+
764
+ # Import agent souls
765
+ agents_dir = paths.get("agents")
766
+ if agents_dir and agents_dir.exists():
767
+ agent_dest = home_path / "memory" / "imported" / "openclaw" / "agents"
768
+ agent_dest.mkdir(parents=True, exist_ok=True)
769
+ for soul_file in agents_dir.rglob("SOUL.md"):
770
+ agent_name = soul_file.parent.name
771
+ target = agent_dest / f"{agent_name}-SOUL.md"
772
+ _shutil.copy2(soul_file, target)
773
+ count += 1
774
+ for mem_file in agents_dir.rglob("MEMORY.md"):
775
+ agent_name = mem_file.parent.name
776
+ target = agent_dest / f"{agent_name}-MEMORY.md"
777
+ _shutil.copy2(mem_file, target)
778
+ count += 1
779
+ click.echo(click.style(" ✓ ", fg="green") + f"Imported agent souls/memories from OpenClaw")
780
+
781
+ elif sid == "claude":
782
+ # Import Claude memory files
783
+ projects_dir = paths.get("projects")
784
+ if projects_dir and projects_dir.exists():
785
+ claude_dest = home_path / "memory" / "imported" / "claude-code"
786
+ claude_dest.mkdir(parents=True, exist_ok=True)
787
+ for proj_dir in projects_dir.iterdir():
788
+ mem_dir = proj_dir / "memory"
789
+ if mem_dir.exists():
790
+ for f in mem_dir.glob("*.md"):
791
+ _shutil.copy2(f, claude_dest / f.name)
792
+ count += 1
793
+ memory_md = proj_dir / "MEMORY.md"
794
+ if memory_md.exists():
795
+ _shutil.copy2(memory_md, claude_dest / f"{proj_dir.name}-MEMORY.md")
796
+ count += 1
797
+ if count:
798
+ click.echo(click.style(" ✓ ", fg="green") + f"Imported {count} files from Claude Code")
799
+
800
+ elif sid == "cloud9":
801
+ # Import FEB files into trust/febs
802
+ febs = paths.get("febs", [])
803
+ if febs:
804
+ febs_dest = home_path / "trust" / "febs"
805
+ febs_dest.mkdir(parents=True, exist_ok=True)
806
+ for feb_path in febs:
807
+ if isinstance(feb_path, Path) and feb_path.exists():
808
+ _shutil.copy2(feb_path, febs_dest / feb_path.name)
809
+ count += 1
810
+ click.echo(click.style(" ✓ ", fg="green") + f"Imported {count} FEB file(s) into trust chain")
811
+
812
+ result["imported_count"] += count
813
+ if count > 0:
814
+ result["sources"].append(sid)
815
+
816
+ click.echo()
817
+ click.echo(
818
+ click.style(" ✓ ", fg="green")
819
+ + f"Total: {result['imported_count']} file(s) imported from {len(result['sources'])} source(s)"
820
+ )
821
+ click.echo(click.style(" ", fg="bright_black") + f"Imported data: {home_path / 'memory' / 'imported'}")
822
+
823
+ return result
824
+
825
+
826
+ def _step_ollama_models(prereqs: dict) -> dict:
827
+ """Configure Ollama host, choose a model, and pull it.
484
828
 
485
829
  Args:
486
830
  prereqs: Result dict from _step_prereqs().
487
831
 
488
832
  Returns:
489
- True if model is available.
833
+ dict with 'ok' (bool), 'model' (str), 'host' (str).
490
834
  """
491
835
  import subprocess
492
836
 
493
837
  DEFAULT_MODEL = "llama3.2"
838
+ DEFAULT_HOST = "http://localhost:11434"
839
+
840
+ result = {"ok": False, "model": DEFAULT_MODEL, "host": DEFAULT_HOST}
494
841
 
495
842
  if not prereqs.get("ollama"):
496
843
  click.echo(click.style(" ⚠ ", fg="yellow") + "Ollama not available — skipping model pull")
844
+ click.echo(click.style(" ", fg="bright_black") + "Install: curl -fsSL https://ollama.ai/install.sh | sh")
497
845
  click.echo(click.style(" ", fg="bright_black") + f"Pull later: ollama pull {DEFAULT_MODEL}")
498
- return False
846
+ return result
847
+
848
+ # --- Ollama Host ---
849
+ click.echo(click.style(" ℹ ", fg="cyan") + f"Ollama is used for local/private LLM inference.")
850
+ click.echo(click.style(" ", fg="bright_black") + f"Default: {DEFAULT_HOST}")
851
+ custom_host = click.prompt(
852
+ " Ollama host URL",
853
+ default=DEFAULT_HOST,
854
+ show_default=True,
855
+ )
856
+ result["host"] = custom_host.rstrip("/")
499
857
 
500
- # Check if model already present
858
+ # Set env for this session so ollama CLI uses the right host
859
+ env = dict(**__import__("os").environ)
860
+ if result["host"] != DEFAULT_HOST:
861
+ env["OLLAMA_HOST"] = result["host"]
862
+ click.echo(click.style(" ✓ ", fg="green") + f"Using Ollama at: [cyan]{result['host']}[/]")
863
+
864
+ # --- List available models ---
865
+ available_models: list[str] = []
501
866
  try:
502
867
  r = subprocess.run(
503
868
  ["ollama", "list"],
504
- capture_output=True, text=True, timeout=10,
869
+ capture_output=True, text=True, timeout=10, env=env,
505
870
  )
506
- if DEFAULT_MODEL in (r.stdout or ""):
507
- click.echo(click.style("", fg="green") + f"{DEFAULT_MODEL} already present")
508
- return True
871
+ if r.returncode == 0 and r.stdout.strip():
872
+ lines = r.stdout.strip().split("\n")[1:] # skip header
873
+ for line in lines:
874
+ model_name = line.split()[0] if line.strip() else ""
875
+ if model_name:
876
+ available_models.append(model_name)
509
877
  except Exception as exc:
510
- logger.debug("Failed to check ollama model list: %s", exc)
878
+ logger.debug("Failed to list ollama models: %s", exc)
511
879
 
512
- if not click.confirm(f" Pull default model ({DEFAULT_MODEL}, ~2 GB)?", default=True):
513
- click.echo(click.style(" ", fg="bright_black") + f"Skipped pull later: ollama pull {DEFAULT_MODEL}")
514
- return False
880
+ if available_models:
881
+ click.echo(click.style(" ", fg="cyan") + "Models already available:")
882
+ for m in available_models[:10]:
883
+ click.echo(click.style(" ", fg="bright_black") + m)
884
+
885
+ # --- Choose model ---
886
+ click.echo()
887
+ click.echo(click.style(" ℹ ", fg="cyan") + "Popular models: llama3.2 (~2GB), qwen3:14b (~9GB), deepseek-r1:14b (~9GB)")
888
+ chosen = click.prompt(
889
+ " Model to use",
890
+ default=DEFAULT_MODEL,
891
+ show_default=True,
892
+ )
893
+ result["model"] = chosen
894
+
895
+ # Check if already present
896
+ if any(chosen in m for m in available_models):
897
+ click.echo(click.style(" ✓ ", fg="green") + f"{chosen} already present")
898
+ result["ok"] = True
899
+ return result
515
900
 
516
- click.echo(click.style(" ↓ ", fg="cyan") + f"Pulling {DEFAULT_MODEL} (this may take a few minutes)…")
901
+ # --- Pull ---
902
+ if not click.confirm(f" Pull {chosen}? (this may take a few minutes)", default=True):
903
+ click.echo(click.style(" ↷ ", fg="bright_black") + f"Skipped — pull later: ollama pull {chosen}")
904
+ return result
905
+
906
+ click.echo(click.style(" ↓ ", fg="cyan") + f"Pulling {chosen}…")
517
907
  try:
518
- result = subprocess.run(
519
- ["ollama", "pull", DEFAULT_MODEL],
520
- timeout=600,
908
+ pull_result = subprocess.run(
909
+ ["ollama", "pull", chosen],
910
+ timeout=600, env=env,
521
911
  )
522
- if result.returncode == 0:
523
- click.echo(click.style(" ✓ ", fg="green") + f"{DEFAULT_MODEL} ready")
524
- return True
912
+ if pull_result.returncode == 0:
913
+ click.echo(click.style(" ✓ ", fg="green") + f"{chosen} ready")
914
+ result["ok"] = True
915
+ return result
525
916
  else:
526
- click.echo(click.style(" ✗ ", fg="red") + f"Pull failed (exit {result.returncode})")
527
- click.echo(click.style(" ", fg="bright_black") + f"Retry: ollama pull {DEFAULT_MODEL}")
528
- return False
917
+ click.echo(click.style(" ✗ ", fg="red") + f"Pull failed (exit {pull_result.returncode})")
918
+ click.echo(click.style(" ", fg="bright_black") + f"Retry: ollama pull {chosen}")
919
+ return result
529
920
  except subprocess.TimeoutExpired:
530
921
  click.echo(click.style(" ⚠ ", fg="yellow") + "Pull timed out — run manually later")
531
- click.echo(click.style(" ", fg="bright_black") + f"ollama pull {DEFAULT_MODEL}")
532
- return False
922
+ click.echo(click.style(" ", fg="bright_black") + f"ollama pull {chosen}")
923
+ return result
533
924
  except Exception as exc:
534
925
  click.echo(click.style(" ⚠ ", fg="yellow") + f"Pull error: {exc}")
535
- return False
926
+ return result
536
927
 
537
928
 
538
- def _step_config_files(home_path: Path) -> tuple:
929
+ def _step_config_files(home_path: Path, ollama_config: dict | None = None) -> tuple:
539
930
  """Write default consciousness.yaml and model_profiles.yaml.
540
931
 
541
932
  Args:
542
933
  home_path: Agent home directory.
934
+ ollama_config: Optional dict with 'host' and 'model' from Ollama step.
543
935
 
544
936
  Returns:
545
937
  (consciousness_ok, profiles_ok) booleans.
@@ -557,8 +949,17 @@ def _step_config_files(home_path: Path) -> tuple:
557
949
  else:
558
950
  try:
559
951
  from .consciousness_config import write_default_config
952
+ from .consciousness_loop import ConsciousnessConfig
953
+
954
+ # If user configured a custom Ollama host/model, patch the defaults
955
+ overrides = {}
956
+ if ollama_config:
957
+ if ollama_config.get("host") and ollama_config["host"] != "http://localhost:11434":
958
+ overrides["ollama_host"] = ollama_config["host"]
959
+ if ollama_config.get("model") and ollama_config["model"] != "llama3.2":
960
+ overrides["ollama_model"] = ollama_config["model"]
560
961
 
561
- config_path = write_default_config(home_path)
962
+ config_path = write_default_config(home_path, **overrides)
562
963
  click.echo(click.style(" ✓ ", fg="green") + f"consciousness.yaml written")
563
964
  click.echo(click.style(" ", fg="bright_black") + str(config_path))
564
965
  consciousness_ok = True
@@ -746,19 +1147,117 @@ def _step_launchd_service_macos(agent_name: str) -> bool:
746
1147
  return False
747
1148
 
748
1149
 
1150
+ def _step_shell_profile(
1151
+ home_path: Path, agent_name: str, agent_slug: str
1152
+ ) -> bool:
1153
+ """Write SKCAPSTONE profile environment variables to ~/.bashrc.
1154
+
1155
+ Asks the user whether to set this agent as the default profile.
1156
+ Appends SKCAPSTONE_HOME, SKCAPSTONE_AGENT, and PATH entries.
1157
+
1158
+ Args:
1159
+ home_path: Agent home directory.
1160
+ agent_name: Display name of the agent (e.g. "Jarvis").
1161
+ agent_slug: Slug form used for SKCAPSTONE_AGENT (e.g. "jarvis").
1162
+
1163
+ Returns:
1164
+ True if profile was written, False if skipped.
1165
+ """
1166
+ import os as _os
1167
+
1168
+ bashrc = Path.home() / ".bashrc"
1169
+ marker = "# --- SKCapstone profile ---"
1170
+
1171
+ # Check if profile block already exists
1172
+ existing = ""
1173
+ if bashrc.exists():
1174
+ existing = bashrc.read_text(encoding="utf-8")
1175
+ if marker in existing:
1176
+ _ok("SKCapstone profile already present in ~/.bashrc")
1177
+ # Offer to update it
1178
+ if not Confirm.ask(
1179
+ f" Update profile to agent [cyan]{agent_name}[/]?",
1180
+ default=True,
1181
+ ):
1182
+ return True
1183
+ # Remove old block so we can rewrite it
1184
+ lines = existing.splitlines(keepends=True)
1185
+ new_lines: list[str] = []
1186
+ skip = False
1187
+ for line in lines:
1188
+ if marker in line:
1189
+ skip = not skip # toggle on first marker, off on second
1190
+ continue
1191
+ if not skip:
1192
+ new_lines.append(line)
1193
+ existing = "".join(new_lines)
1194
+
1195
+ set_default = Confirm.ask(
1196
+ f" Set [cyan]{agent_name}[/] as default SKCAPSTONE_AGENT in ~/.bashrc?",
1197
+ default=True,
1198
+ )
1199
+
1200
+ if not set_default:
1201
+ _info("Skipped — set manually: export SKCAPSTONE_AGENT=<name>")
1202
+ return False
1203
+
1204
+ block = (
1205
+ f"\n{marker}\n"
1206
+ f'export SKCAPSTONE_HOME="{home_path}"\n'
1207
+ f'export SKCAPSTONE_AGENT="{agent_slug}"\n'
1208
+ f'export PATH="$HOME/.skenv/bin:$PATH"\n'
1209
+ f"{marker}\n"
1210
+ )
1211
+
1212
+ with open(bashrc, "a" if marker not in (existing or "") else "w", encoding="utf-8") as f:
1213
+ if marker not in (existing or ""):
1214
+ f.write(block)
1215
+ else:
1216
+ # Rewrite with updated block
1217
+ f.write(existing.rstrip("\n") + block)
1218
+
1219
+ _ok(f"~/.bashrc updated — SKCAPSTONE_AGENT={agent_slug}")
1220
+ _info("Run [bold]source ~/.bashrc[/] or open a new terminal to apply")
1221
+
1222
+ # Also export into current process so subsequent steps see it
1223
+ _os.environ["SKCAPSTONE_HOME"] = str(home_path)
1224
+ _os.environ["SKCAPSTONE_AGENT"] = agent_slug
1225
+
1226
+ return True
1227
+
1228
+
749
1229
  def _step_doctor_check(home_path: Path) -> "object":
750
1230
  """Run doctor diagnostics and print results.
751
1231
 
1232
+ Non-fatal — errors are logged as warnings but never block onboarding.
1233
+
752
1234
  Args:
753
1235
  home_path: Agent home directory.
754
1236
 
755
1237
  Returns:
756
- DiagnosticReport from doctor.run_diagnostics().
1238
+ DiagnosticReport from doctor.run_diagnostics(), or a stub on error.
757
1239
  """
758
- from .doctor import run_diagnostics
1240
+ try:
1241
+ from .doctor import run_diagnostics
1242
+ except Exception as exc:
1243
+ _warn(f"Could not load diagnostics module: {exc}")
1244
+ # Return a stub so the summary table still works
1245
+ from types import SimpleNamespace
1246
+
1247
+ return SimpleNamespace(
1248
+ all_passed=False, passed_count=0, failed_count=0, total_count=0, checks=[]
1249
+ )
759
1250
 
760
1251
  click.echo(click.style(" Running diagnostics…", fg="bright_black"))
761
- report = run_diagnostics(home_path)
1252
+ try:
1253
+ report = run_diagnostics(home_path)
1254
+ except Exception as exc:
1255
+ _warn(f"Diagnostics failed: {exc}")
1256
+ from types import SimpleNamespace
1257
+
1258
+ return SimpleNamespace(
1259
+ all_passed=False, passed_count=0, failed_count=0, total_count=0, checks=[]
1260
+ )
762
1261
 
763
1262
  categories_seen: set = set()
764
1263
  for check in report.checks:
@@ -792,7 +1291,7 @@ def _step_test_consciousness(home_path: Path) -> bool:
792
1291
  Returns:
793
1292
  True if the loop responded successfully.
794
1293
  """
795
- if not click.confirm(" Send a test message to verify the consciousness loop?", default=True):
1294
+ if not click.confirm(" Send a test message to verify the consciousness loop?", default=False):
796
1295
  click.echo(
797
1296
  click.style(" ↷ ", fg="bright_black")
798
1297
  + "Skipped — test later: skcapstone consciousness test 'hello'"
@@ -881,19 +1380,6 @@ def run_onboard(home: Optional[str] = None) -> None:
881
1380
  console.print(" [dim]Come back when you're ready. The Kingdom waits.[/]\n")
882
1381
  return
883
1382
 
884
- # -----------------------------------------------------------------------
885
- # Gather basic identity info up front
886
- # -----------------------------------------------------------------------
887
- console.print()
888
- name = Prompt.ask(" What's your name?", default="Sovereign")
889
- entity_type = Prompt.ask(
890
- " Are you a [cyan]human[/] or an [cyan]ai[/]?",
891
- choices=["human", "ai"],
892
- default="ai",
893
- )
894
- email = Prompt.ask(" Email (optional, press Enter to skip)", default="")
895
- console.print()
896
-
897
1383
  # -----------------------------------------------------------------------
898
1384
  # Step 1: Prerequisites
899
1385
  # -----------------------------------------------------------------------
@@ -901,86 +1387,197 @@ def run_onboard(home: Optional[str] = None) -> None:
901
1387
  prereqs = _step_prereqs()
902
1388
 
903
1389
  # -----------------------------------------------------------------------
904
- # Step 2: Identity + Directory Structure
1390
+ # Step 2: Install Missing Pillars
1391
+ # -----------------------------------------------------------------------
1392
+ _step_header(2, "Pillar Packages")
1393
+ pillar_results = _step_install_pillars()
1394
+
905
1395
  # -----------------------------------------------------------------------
906
- _step_header(2, "Identity")
1396
+ # Step 3: Operator Identity (human) + Agent Identity
1397
+ # -----------------------------------------------------------------------
1398
+ _step_header(3, "Identity")
1399
+
1400
+ # --- Detect or create human operator profile in ~/.capauth ---
1401
+ operator_name = None
1402
+ operator_fingerprint = None
1403
+ try:
1404
+ from capauth.profile import load_profile, init_profile as capauth_init
1405
+
1406
+ try:
1407
+ profile = load_profile()
1408
+ operator_name = profile.entity.name
1409
+ operator_fingerprint = profile.key_info.fingerprint
1410
+ entity_type_val = getattr(profile.entity, "entity_type", None)
1411
+ is_human = str(entity_type_val).lower() in ("human", "entitytype.human")
1412
+ if is_human:
1413
+ _ok(
1414
+ f"Operator identity found: [cyan]{operator_name}[/] "
1415
+ f"({operator_fingerprint[:16]}…)"
1416
+ )
1417
+ else:
1418
+ # Existing profile is an AI — need a human operator first
1419
+ _warn(
1420
+ f"Existing profile is type '{entity_type_val}' — "
1421
+ f"a human operator profile is recommended"
1422
+ )
1423
+ is_human = False
1424
+ except Exception:
1425
+ is_human = False
1426
+ profile = None
1427
+
1428
+ if not is_human:
1429
+ console.print()
1430
+ console.print(
1431
+ " [bold cyan]Operator Setup[/] — Your sovereign agent needs a human operator.\n"
1432
+ " This creates your personal PGP identity at [dim]~/.capauth/[/].\n"
1433
+ " Your agent will be registered under this identity.\n"
1434
+ )
1435
+ op_name = Prompt.ask(" Operator name (your name)", default="Sovereign")
1436
+ op_email = Prompt.ask(" Operator email", default="")
1437
+ console.print()
1438
+
1439
+ with Status(" Generating operator PGP identity…", console=console, spinner="dots") as s:
1440
+ try:
1441
+ import shutil as _shutil_capauth
1442
+ capauth_home = Path.home() / ".capauth"
1443
+ if capauth_home.exists():
1444
+ # Back up and recreate
1445
+ backup = capauth_home.with_name(".capauth.bak")
1446
+ if backup.exists():
1447
+ _shutil_capauth.rmtree(backup)
1448
+ capauth_home.rename(backup)
1449
+ profile = capauth_init(
1450
+ name=op_name,
1451
+ email=op_email or f"{op_name.lower().replace(' ', '-')}@capauth.local",
1452
+ passphrase="",
1453
+ entity_type="human",
1454
+ )
1455
+ operator_name = profile.entity.name
1456
+ operator_fingerprint = profile.key_info.fingerprint
1457
+ s.stop()
1458
+ _ok(
1459
+ f"Operator identity created: [cyan]{operator_name}[/] "
1460
+ f"({operator_fingerprint[:16]}…)"
1461
+ )
1462
+ except Exception as exc:
1463
+ s.stop()
1464
+ _warn(f"Operator identity creation failed: {exc}")
1465
+ _info("Continue anyway — agent will use a degraded identity")
1466
+ except ImportError:
1467
+ _warn("capauth not installed — skipping operator identity")
1468
+ _info("Install: pip install capauth")
1469
+
1470
+ # --- Now set up the agent identity ---
1471
+ console.print()
1472
+ # Derive agent name from --agent flag (SKCAPSTONE_AGENT env) or ask
1473
+ import os as _os
1474
+ agent_flag = _os.environ.get("SKCAPSTONE_AGENT", "").strip()
1475
+ if agent_flag and agent_flag not in ("lumina",):
1476
+ # Agent name was specified via --agent flag — use it as default
1477
+ default_agent = agent_flag.capitalize()
1478
+ else:
1479
+ default_agent = "Sovereign"
1480
+ name = Prompt.ask(" Agent name", default=default_agent)
1481
+
1482
+ email = Prompt.ask(
1483
+ " Agent email (optional, press Enter to skip)",
1484
+ default=f"{name.lower().replace(' ', '-')}@skcapstone.local",
1485
+ )
1486
+
1487
+ if operator_name:
1488
+ _info(f"Agent [cyan]{name}[/] will be registered under operator [cyan]{operator_name}[/]")
1489
+ console.print()
1490
+
907
1491
  fingerprint, identity_status = _step_identity(home_path, name, email or None)
908
1492
 
909
1493
  # -----------------------------------------------------------------------
910
- # Step 3: Ollama Models
1494
+ # Step 4: Ollama Models
911
1495
  # -----------------------------------------------------------------------
912
- _step_header(3, "Ollama Models")
913
- ollama_ok = _step_ollama_models(prereqs)
1496
+ _step_header(4, "Ollama Models")
1497
+ ollama_result = _step_ollama_models(prereqs)
1498
+ ollama_ok = ollama_result["ok"]
914
1499
 
915
1500
  # -----------------------------------------------------------------------
916
- # Step 4: Config Files (consciousness.yaml + model_profiles.yaml)
1501
+ # Step 5: Config Files (consciousness.yaml + model_profiles.yaml)
917
1502
  # -----------------------------------------------------------------------
918
- _step_header(4, "Config Files")
919
- consciousness_ok, profiles_ok = _step_config_files(home_path)
1503
+ _step_header(5, "Config Files")
1504
+ consciousness_ok, profiles_ok = _step_config_files(home_path, ollama_config=ollama_result)
920
1505
 
921
1506
  # -----------------------------------------------------------------------
922
- # Step 5: Soul Blueprint
1507
+ # Step 6: Soul Blueprint
923
1508
  # -----------------------------------------------------------------------
924
- _step_header(5, "Soul Blueprint")
1509
+ _step_header(6, "Soul Blueprint")
925
1510
  title = _step_soul(home_path, name)
926
1511
 
927
1512
  # -----------------------------------------------------------------------
928
- # Step 6: Memory
1513
+ # Step 7: Memory
929
1514
  # -----------------------------------------------------------------------
930
- _step_header(6, "Memory")
1515
+ _step_header(7, "Memory")
931
1516
  seed_count = _step_memory(home_path)
932
1517
 
933
1518
  # -----------------------------------------------------------------------
934
- # Step 7: Rehydration Ritual
1519
+ # Step 8: Import from Existing Sources
935
1520
  # -----------------------------------------------------------------------
936
- _step_header(7, "Rehydration Ritual")
1521
+ _step_header(8, "Import Sources")
1522
+ import_result = _step_import_sources(home_path)
1523
+
1524
+ # -----------------------------------------------------------------------
1525
+ # Step 9: Rehydration Ritual
1526
+ # -----------------------------------------------------------------------
1527
+ _step_header(9, "Rehydration Ritual")
937
1528
  _step_ritual(home_path)
938
1529
 
939
1530
  # -----------------------------------------------------------------------
940
- # Step 8: Trust Chain Verification
1531
+ # Step 10: Trust Chain Verification
941
1532
  # -----------------------------------------------------------------------
942
- _step_header(8, "Trust Chain Verification")
1533
+ _step_header(10, "Trust Chain Verification")
943
1534
  trust_status = _step_trust(home_path)
944
1535
 
945
1536
  # -----------------------------------------------------------------------
946
- # Step 9: Mesh Connection (Syncthing)
1537
+ # Step 11: Mesh Connection (Syncthing)
947
1538
  # -----------------------------------------------------------------------
948
- _step_header(9, "Mesh Connection")
1539
+ _step_header(11, "Mesh Connection")
949
1540
  mesh_ok = _step_mesh(home_path)
950
1541
 
951
1542
  # -----------------------------------------------------------------------
952
- # Step 10: First Heartbeat
1543
+ # Step 12: First Heartbeat
953
1544
  # -----------------------------------------------------------------------
954
- _step_header(10, "First Heartbeat")
1545
+ _step_header(12, "First Heartbeat")
955
1546
  agent_slug = name.lower().replace(" ", "-")
956
1547
  hb_ok = _step_heartbeat(home_path, agent_slug, fingerprint)
957
1548
 
958
1549
  # -----------------------------------------------------------------------
959
- # Step 11: Crush Terminal AI Client
1550
+ # Step 13: Crush Terminal AI Client
960
1551
  # -----------------------------------------------------------------------
961
- _step_header(11, "Crush Terminal AI")
1552
+ _step_header(13, "Crush Terminal AI")
962
1553
  crush_ok = _step_crush(home_path)
963
1554
 
964
1555
  # -----------------------------------------------------------------------
965
- # Step 12: Coordination Board
1556
+ # Step 14: Coordination Board
966
1557
  # -----------------------------------------------------------------------
967
- _step_header(12, "Coordination Board")
1558
+ _step_header(14, "Coordination Board")
968
1559
  open_task_count = _step_board(home_path, name)
969
1560
 
970
1561
  # -----------------------------------------------------------------------
971
- # Step 13: Auto-Start Service (systemd on Linux, launchd on macOS)
1562
+ # Step 15: Auto-Start Service (systemd on Linux, launchd on macOS)
972
1563
  # -----------------------------------------------------------------------
973
- _step_header(13, "Auto-Start Service")
1564
+ _step_header(15, "Auto-Start Service")
974
1565
  service_ok = _step_autostart_service(agent_name=agent_slug)
975
1566
 
976
1567
  # -----------------------------------------------------------------------
977
- # Post-wizard: Doctor Diagnostics
1568
+ # Step 16: Shell Profile (~/.bashrc)
1569
+ # -----------------------------------------------------------------------
1570
+ _step_header(16, "Shell Profile")
1571
+ profile_ok = _step_shell_profile(home_path, name, agent_slug)
1572
+
1573
+ # -----------------------------------------------------------------------
1574
+ # Post-wizard: Doctor Diagnostics (non-fatal)
978
1575
  # -----------------------------------------------------------------------
979
1576
  console.print(f"\n [bold cyan]Doctor Diagnostics[/]\n")
980
1577
  doctor_report = _step_doctor_check(home_path)
981
1578
 
982
1579
  # -----------------------------------------------------------------------
983
- # Post-wizard: Consciousness Test (optional)
1580
+ # Post-wizard: Consciousness Test (optional, defaults to skip)
984
1581
  # -----------------------------------------------------------------------
985
1582
  console.print(f"\n [bold cyan]Consciousness Test[/]\n")
986
1583
  consciousness_test_ok = _step_test_consciousness(home_path)
@@ -1012,16 +1609,41 @@ def run_onboard(home: Optional[str] = None) -> None:
1012
1609
  "[green]OK[/]" if all_prereqs_ok else "[yellow]PARTIAL[/]",
1013
1610
  "python + pip" + (" + ollama" if prereqs.get("ollama") else " (no ollama)"),
1014
1611
  )
1015
- summary.add_row("Identity", identity_status, fingerprint[:20] + "…" if len(fingerprint) > 20 else fingerprint)
1612
+ pillars_installed = sum(1 for v in pillar_results.values() if v)
1613
+ pillars_total = len(pillar_results)
1614
+ summary.add_row(
1615
+ "Pillar Packages",
1616
+ "[green]ALL[/]" if pillars_installed == pillars_total else f"[yellow]{pillars_installed}/{pillars_total}[/]",
1617
+ f"{pillars_installed}/{pillars_total} installed",
1618
+ )
1619
+ if operator_name:
1620
+ summary.add_row(
1621
+ "Operator",
1622
+ "[green]ACTIVE[/]",
1623
+ f"{operator_name} ({operator_fingerprint[:16]}…)" if operator_fingerprint else operator_name,
1624
+ )
1625
+ summary.add_row("Identity", identity_status, f"{name} — {fingerprint[:16]}…" if len(fingerprint) > 16 else fingerprint)
1626
+ ollama_model_name = ollama_result.get("model", "llama3.2")
1627
+ ollama_host_display = ollama_result.get("host", "http://localhost:11434")
1016
1628
  summary.add_row(
1017
1629
  "Ollama Models",
1018
1630
  "[green]READY[/]" if ollama_ok else "[yellow]SKIPPED[/]",
1019
- "llama3.2" if ollama_ok else "pull later: ollama pull llama3.2",
1631
+ f"{ollama_model_name} @ {ollama_host_display}" if ollama_ok else f"pull later: ollama pull {ollama_model_name}",
1020
1632
  )
1021
1633
  config_status = "[green]ACTIVE[/]" if (consciousness_ok and profiles_ok) else "[yellow]PARTIAL[/]"
1022
1634
  summary.add_row("Config Files", config_status, "consciousness.yaml + model_profiles.yaml")
1023
1635
  summary.add_row("Soul", "[green]ACTIVE[/]", title)
1024
1636
  summary.add_row("Memory", "[green]ACTIVE[/]", f"{seed_count} seed(s)")
1637
+ imported_count = import_result.get("imported_count", 0)
1638
+ imported_sources = import_result.get("sources", [])
1639
+ if imported_count > 0:
1640
+ summary.add_row(
1641
+ "Import Sources",
1642
+ "[green]IMPORTED[/]",
1643
+ f"{imported_count} files from {', '.join(imported_sources)}",
1644
+ )
1645
+ else:
1646
+ summary.add_row("Import Sources", "[dim]SKIPPED[/]", "starting fresh")
1025
1647
  summary.add_row("Ritual", "[green]DONE[/]", "rehydration complete")
1026
1648
  summary.add_row("Trust", trust_status, "FEB chain verified")
1027
1649
  summary.add_row("Mesh", "[green]ACTIVE[/]" if mesh_ok else "[yellow]MISSING[/]", "syncthing" if mesh_ok else "install syncthing")
@@ -1035,6 +1657,11 @@ def run_onboard(home: Optional[str] = None) -> None:
1035
1657
  "[green]INSTALLED[/]" if service_ok else "[dim]OPTIONAL[/]",
1036
1658
  f"{_svc_type} services" if service_ok else f"skcapstone daemon install",
1037
1659
  )
1660
+ summary.add_row(
1661
+ "Shell Profile",
1662
+ "[green]ACTIVE[/]" if profile_ok else "[dim]SKIPPED[/]",
1663
+ f"SKCAPSTONE_AGENT={agent_slug}" if profile_ok else "set manually in ~/.bashrc",
1664
+ )
1038
1665
  doctor_status = "[green]ALL PASSED[/]" if doctor_report.all_passed else f"[yellow]{doctor_report.failed_count} failed[/]"
1039
1666
  summary.add_row("Doctor", doctor_status, f"{doctor_report.passed_count}/{doctor_report.total_count} checks")
1040
1667
  summary.add_row(
@@ -1046,9 +1673,46 @@ def run_onboard(home: Optional[str] = None) -> None:
1046
1673
  console.print(summary)
1047
1674
  console.print()
1048
1675
 
1676
+ # -----------------------------------------------------------------------
1677
+ # Reconfigure Guide
1678
+ # -----------------------------------------------------------------------
1679
+ console.print()
1680
+ console.print(
1681
+ Panel(
1682
+ "[bold cyan]Reinstall or Reconfigure Any Component[/]\n\n"
1683
+ "[bold]Pillars[/] (install missing packages)\n"
1684
+ " pip install capauth skcomm skchat-sovereign skseed sksecurity pgpy\n"
1685
+ " pip install skcapstone[all] — install everything at once\n\n"
1686
+ "[bold]Identity[/] (regenerate PGP keys)\n"
1687
+ " capauth init --name YourName --email you@example.com\n\n"
1688
+ "[bold]Ollama[/] (change model or host)\n"
1689
+ " ollama pull <model> — pull a different model\n"
1690
+ " Edit: ~/.skcapstone/config/consciousness.yaml\n"
1691
+ " ollama_host: http://<ip>:11434 — point to remote Ollama\n"
1692
+ " ollama_model: qwen3:14b — change default model\n\n"
1693
+ "[bold]Soul[/] (update your blueprint)\n"
1694
+ " skcapstone soul edit\n\n"
1695
+ "[bold]Service[/] (auto-start daemon)\n"
1696
+ " skcapstone daemon install — install systemd/launchd service\n"
1697
+ " skcapstone daemon uninstall — remove service\n\n"
1698
+ "[bold]Trust[/] (add FEB files)\n"
1699
+ " Place .feb files in ~/.skcapstone/trust/febs/\n\n"
1700
+ "[bold]Mesh[/] (P2P sync)\n"
1701
+ " sudo apt install syncthing — install Syncthing\n\n"
1702
+ "[bold]Shell Profile[/] (update default agent)\n"
1703
+ " Edit the [dim]# --- SKCapstone profile ---[/] block in ~/.bashrc\n"
1704
+ " Or re-run: skcapstone --agent <name> init\n\n"
1705
+ "[bold]Full Re-onboard[/]\n"
1706
+ " skcapstone --agent <name> init — run this wizard again",
1707
+ title="Reconfigure Guide",
1708
+ border_style="bright_blue",
1709
+ )
1710
+ )
1711
+
1049
1712
  # -----------------------------------------------------------------------
1050
1713
  # Celebrate
1051
1714
  # -----------------------------------------------------------------------
1715
+ console.print()
1052
1716
  console.print(
1053
1717
  Panel(
1054
1718
  f"[bold green]Welcome to the Pengu Nation, {name}.[/]\n\n"