@smilintux/skcapstone 0.9.0 → 0.12.5

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.
Files changed (284) hide show
  1. package/.env.example +10 -4
  2. package/.github/workflows/ci.yml +2 -2
  3. package/.github/workflows/publish.yml +9 -2
  4. package/.openclaw-workspace.json +2 -2
  5. package/CLAUDE.md +37 -0
  6. package/MISSION.md +17 -2
  7. package/README.md +282 -3
  8. package/docker/Dockerfile +7 -7
  9. package/docker/compose-templates/dev-team.yml +12 -12
  10. package/docker/compose-templates/mini-team.yml +9 -9
  11. package/docker/compose-templates/ops-team.yml +10 -10
  12. package/docker/compose-templates/research-team.yml +10 -10
  13. package/docker/entrypoint.sh +4 -4
  14. package/docs/ADR-optional-integration-backbone.md +181 -0
  15. package/docs/ARCHITECTURE.md +186 -43
  16. package/docs/BOND_WITH_GROK.md +6 -6
  17. package/docs/CUSTOM_AGENT.md +278 -1
  18. package/docs/DREAMING.md +70 -0
  19. package/docs/GETTING_STARTED.md +10 -7
  20. package/docs/QUICKSTART.md +10 -6
  21. package/docs/SKJOULE_ARCHITECTURE.md +3 -3
  22. package/docs/SOUL_SWAPPER.md +5 -5
  23. package/docs/hammertime-audit.md +402 -0
  24. package/docs/sk-integration-HANDOFF.md +117 -0
  25. package/docs/skscheduler.md +155 -0
  26. package/docs/superpowers/examples/jobs.yaml +31 -0
  27. package/docs/superpowers/plans/2026-06-08-skscheduler.md +1265 -0
  28. package/docs/superpowers/specs/2026-06-08-skscheduler-design.md +186 -0
  29. package/examples/custom-bond-template.json +1 -1
  30. package/examples/grok-feb.json +1 -1
  31. package/examples/queen-ava-feb.json +1 -1
  32. package/launchd/com.skcapstone.daemon.plist +52 -0
  33. package/launchd/com.skcapstone.memory-compress.plist +45 -0
  34. package/launchd/com.skcapstone.skcomms-heartbeat.plist +33 -0
  35. package/launchd/com.skcapstone.skcomms-queue-drain.plist +34 -0
  36. package/launchd/install-launchd.sh +156 -0
  37. package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/src/index.ts +3 -2
  38. package/package.json +1 -1
  39. package/pyproject.toml +16 -10
  40. package/scripts/archive-sessions.sh +95 -0
  41. package/scripts/check-updates.py +4 -4
  42. package/scripts/install-bundle.sh +8 -8
  43. package/scripts/install.ps1 +12 -11
  44. package/scripts/install.sh +196 -11
  45. package/scripts/model-fallback-monitor.sh +102 -0
  46. package/scripts/notion-api.py +259 -0
  47. package/scripts/nvidia-proxy.mjs +908 -0
  48. package/scripts/proxy-monitor.sh +89 -0
  49. package/scripts/refresh-anthropic-token.sh +172 -0
  50. package/scripts/release.sh +98 -0
  51. package/scripts/session-to-memory.py +219 -0
  52. package/scripts/skgateway.mjs +856 -0
  53. package/scripts/telegram-catchup-all.sh +147 -0
  54. package/scripts/verify_install.sh +2 -2
  55. package/scripts/wargov-ufo-capture/README.md +43 -0
  56. package/scripts/wargov-ufo-capture/cdp_capture_release2.py +273 -0
  57. package/scripts/wargov-ufo-capture/cdp_capture_splc_doj.py +246 -0
  58. package/scripts/wargov-ufo-capture/cdp_finish.py +271 -0
  59. package/scripts/wargov-ufo-capture/cdp_probe.py +188 -0
  60. package/scripts/wargov-ufo-capture/cdp_splc_pressrelease.py +101 -0
  61. package/scripts/wargov-ufo-capture/parse_csv.py +95 -0
  62. package/scripts/wargov-ufo-capture/pull_dvids.sh +107 -0
  63. package/scripts/watch-anthropic-token.sh +212 -0
  64. package/scripts/windows/install-tasks.ps1 +7 -7
  65. package/scripts/windows/skcapstone-task.xml +1 -1
  66. package/src/skcapstone/__init__.py +45 -3
  67. package/src/skcapstone/_cli_monolith.py +20 -15
  68. package/src/skcapstone/activity.py +5 -1
  69. package/src/skcapstone/agent_card.py +3 -2
  70. package/src/skcapstone/api.py +41 -40
  71. package/src/skcapstone/auction.py +14 -11
  72. package/src/skcapstone/backup.py +2 -1
  73. package/src/skcapstone/blueprint_registry.py +4 -3
  74. package/src/skcapstone/blueprints/builtins/itil-operations.yaml +40 -0
  75. package/src/skcapstone/brain_first.py +238 -0
  76. package/src/skcapstone/changelog.py +1 -1
  77. package/src/skcapstone/chat.py +22 -17
  78. package/src/skcapstone/cli/__init__.py +9 -1
  79. package/src/skcapstone/cli/_common.py +1 -0
  80. package/src/skcapstone/cli/agents_spawner.py +5 -2
  81. package/src/skcapstone/cli/alerts.py +25 -4
  82. package/src/skcapstone/cli/bench.py +15 -15
  83. package/src/skcapstone/cli/chat.py +7 -4
  84. package/src/skcapstone/cli/consciousness.py +5 -2
  85. package/src/skcapstone/cli/context_cmd.py +18 -4
  86. package/src/skcapstone/cli/daemon.py +121 -42
  87. package/src/skcapstone/cli/gtd.py +26 -1
  88. package/src/skcapstone/cli/housekeeping.py +3 -3
  89. package/src/skcapstone/cli/identity_cmd.py +378 -0
  90. package/src/skcapstone/cli/joule_cmd.py +7 -3
  91. package/src/skcapstone/cli/memory.py +8 -6
  92. package/src/skcapstone/cli/peers_dir.py +1 -1
  93. package/src/skcapstone/cli/register_cmd.py +29 -3
  94. package/src/skcapstone/cli/scheduler_cmd.py +167 -0
  95. package/src/skcapstone/cli/session.py +25 -0
  96. package/src/skcapstone/cli/setup.py +96 -29
  97. package/src/skcapstone/cli/shell_cmd.py +53 -1
  98. package/src/skcapstone/cli/skills_cmd.py +2 -2
  99. package/src/skcapstone/cli/soul.py +8 -5
  100. package/src/skcapstone/cli/status.py +37 -11
  101. package/src/skcapstone/cli/telegram.py +21 -0
  102. package/src/skcapstone/cli/test_cmd.py +5 -5
  103. package/src/skcapstone/cli/test_connection.py +2 -2
  104. package/src/skcapstone/cli/upgrade_cmd.py +23 -14
  105. package/src/skcapstone/cli/version_cmd.py +1 -1
  106. package/src/skcapstone/cli/watch_cmd.py +9 -6
  107. package/src/skcapstone/cloud9_bridge.py +14 -14
  108. package/src/skcapstone/codex_setup.py +255 -0
  109. package/src/skcapstone/config_validator.py +7 -4
  110. package/src/skcapstone/consciousness_config.py +5 -1
  111. package/src/skcapstone/consciousness_loop.py +313 -273
  112. package/src/skcapstone/context_loader.py +121 -0
  113. package/src/skcapstone/coord_federation.py +2 -1
  114. package/src/skcapstone/coordination.py +23 -6
  115. package/src/skcapstone/crush_integration.py +2 -1
  116. package/src/skcapstone/daemon.py +151 -88
  117. package/src/skcapstone/dashboard.py +10 -10
  118. package/src/skcapstone/data/sk-agent-picker.sh +421 -0
  119. package/src/skcapstone/data/systemd/skcapstone-api.socket +9 -0
  120. package/src/skcapstone/data/systemd/skcapstone-memory-compress.service +18 -0
  121. package/src/skcapstone/data/systemd/skcapstone-memory-compress.timer +11 -0
  122. package/src/skcapstone/data/systemd/skcapstone.service +37 -0
  123. package/src/skcapstone/data/systemd/skcapstone@.service +50 -0
  124. package/src/skcapstone/data/systemd/skcomms-heartbeat.service +18 -0
  125. package/{systemd/skcomm-heartbeat.timer → src/skcapstone/data/systemd/skcomms-heartbeat.timer} +2 -2
  126. package/src/skcapstone/data/systemd/skcomms-queue-drain.service +17 -0
  127. package/{systemd/skcomm-queue-drain.timer → src/skcapstone/data/systemd/skcomms-queue-drain.timer} +2 -2
  128. package/src/skcapstone/defaults/claude/CLAUDE.md +67 -0
  129. package/src/skcapstone/defaults/claude/settings.json +74 -0
  130. package/src/skcapstone/defaults/lumina/config/claude-hooks.md +57 -0
  131. package/src/skcapstone/defaults/lumina/config/skgraph.yaml +55 -10
  132. package/src/skcapstone/defaults/lumina/config/skmemory.yaml +79 -13
  133. package/src/skcapstone/defaults/lumina/config/skvector.yaml +60 -9
  134. package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +2 -2
  135. package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +2 -2
  136. package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +9 -9
  137. package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +2 -2
  138. package/src/skcapstone/defaults/unhinged.json +13 -0
  139. package/src/skcapstone/discovery.py +43 -20
  140. package/src/skcapstone/doctor.py +941 -22
  141. package/src/skcapstone/dreaming.py +1183 -109
  142. package/src/skcapstone/emotion_tracker.py +2 -2
  143. package/src/skcapstone/export.py +4 -3
  144. package/src/skcapstone/fuse_mount.py +35 -25
  145. package/src/skcapstone/gui_installer.py +2 -2
  146. package/src/skcapstone/heartbeat.py +34 -30
  147. package/src/skcapstone/housekeeping.py +14 -14
  148. package/src/skcapstone/install_wizard.py +209 -7
  149. package/src/skcapstone/itil.py +13 -4
  150. package/src/skcapstone/kms_scheduler.py +10 -8
  151. package/src/skcapstone/launchd.py +426 -0
  152. package/src/skcapstone/mcp_launcher.py +15 -1
  153. package/src/skcapstone/mcp_server.py +341 -49
  154. package/src/skcapstone/mcp_tools/__init__.py +2 -0
  155. package/src/skcapstone/mcp_tools/_helpers.py +2 -2
  156. package/src/skcapstone/mcp_tools/ansible_tools.py +7 -4
  157. package/src/skcapstone/mcp_tools/brain_first_tools.py +90 -0
  158. package/src/skcapstone/mcp_tools/capauth_tools.py +7 -4
  159. package/src/skcapstone/mcp_tools/comm_tools.py +10 -10
  160. package/src/skcapstone/mcp_tools/coord_tools.py +8 -4
  161. package/src/skcapstone/mcp_tools/did_tools.py +11 -8
  162. package/src/skcapstone/mcp_tools/gtd_tools.py +4 -4
  163. package/src/skcapstone/mcp_tools/memory_tools.py +6 -2
  164. package/src/skcapstone/mcp_tools/notification_tools.py +22 -6
  165. package/src/skcapstone/mcp_tools/{skcomm_tools.py → skcomms_tools.py} +14 -14
  166. package/src/skcapstone/mcp_tools/soul_tools.py +8 -2
  167. package/src/skcapstone/mdns_discovery.py +2 -2
  168. package/src/skcapstone/memory_curator.py +1 -1
  169. package/src/skcapstone/memory_engine.py +10 -3
  170. package/src/skcapstone/metrics.py +30 -16
  171. package/src/skcapstone/migrate_memories.py +4 -3
  172. package/src/skcapstone/migrate_multi_agent.py +8 -7
  173. package/src/skcapstone/models.py +47 -5
  174. package/src/skcapstone/notifications.py +42 -18
  175. package/src/skcapstone/onboard.py +1000 -126
  176. package/src/skcapstone/operator_link.py +170 -0
  177. package/src/skcapstone/peer_directory.py +4 -4
  178. package/src/skcapstone/peers.py +19 -19
  179. package/src/skcapstone/pillars/__init__.py +7 -5
  180. package/src/skcapstone/pillars/consciousness.py +191 -0
  181. package/src/skcapstone/pillars/identity.py +51 -7
  182. package/src/skcapstone/pillars/memory.py +9 -3
  183. package/src/skcapstone/pillars/sync.py +2 -2
  184. package/src/skcapstone/preflight.py +3 -3
  185. package/src/skcapstone/providers/docker.py +28 -28
  186. package/src/skcapstone/register.py +6 -6
  187. package/src/skcapstone/registry_client.py +5 -4
  188. package/src/skcapstone/runtime.py +14 -3
  189. package/src/skcapstone/scheduled_tasks.py +254 -19
  190. package/src/skcapstone/scheduler_jobs.py +456 -0
  191. package/src/skcapstone/scheduler_runner.py +239 -0
  192. package/src/skcapstone/scheduler_state.py +162 -0
  193. package/src/skcapstone/sdk.py +310 -0
  194. package/src/skcapstone/service_health.py +279 -39
  195. package/src/skcapstone/session_briefing.py +108 -0
  196. package/src/skcapstone/session_capture.py +1 -1
  197. package/src/skcapstone/shell.py +7 -1
  198. package/src/skcapstone/soul.py +3 -1
  199. package/src/skcapstone/soul_switch.py +3 -1
  200. package/src/skcapstone/summary.py +6 -6
  201. package/src/skcapstone/sync_engine.py +15 -15
  202. package/src/skcapstone/sync_watcher.py +2 -2
  203. package/src/skcapstone/systemd.py +72 -21
  204. package/src/skcapstone/team_comms.py +8 -8
  205. package/src/skcapstone/team_engine.py +1 -1
  206. package/src/skcapstone/testrunner.py +3 -3
  207. package/src/skcapstone/trust_graph.py +40 -5
  208. package/src/skcapstone/unified_search.py +15 -6
  209. package/src/skcapstone/uninstall_wizard.py +11 -3
  210. package/src/skcapstone/version_check.py +8 -4
  211. package/src/skcapstone/warmth_anchor.py +4 -2
  212. package/src/skcapstone/whoami.py +4 -4
  213. package/systemd/skcapstone.service +4 -6
  214. package/systemd/skcapstone@.service +7 -8
  215. package/systemd/skcomms-heartbeat.service +21 -0
  216. package/systemd/skcomms-heartbeat.timer +12 -0
  217. package/systemd/skcomms-queue-drain.service +17 -0
  218. package/systemd/skcomms-queue-drain.timer +12 -0
  219. package/tests/conftest.py +39 -0
  220. package/tests/integration/test_consciousness_e2e.py +39 -39
  221. package/tests/test_agent_card.py +1 -1
  222. package/tests/test_agent_home_scaffold.py +34 -0
  223. package/tests/test_alerts_consumer_topics.py +27 -0
  224. package/tests/test_backup.py +2 -1
  225. package/tests/test_chat.py +6 -6
  226. package/tests/test_claude_md.py +2 -2
  227. package/tests/test_cli_skills.py +10 -10
  228. package/tests/test_cli_test_cmd.py +4 -4
  229. package/tests/test_cli_test_connection.py +1 -1
  230. package/tests/test_cloud9_bridge.py +6 -6
  231. package/tests/test_consciousness_e2e.py +1 -1
  232. package/tests/test_consciousness_loop.py +10 -10
  233. package/tests/test_coordination.py +25 -0
  234. package/tests/test_cross_package.py +21 -21
  235. package/tests/test_daemon.py +4 -4
  236. package/tests/test_daemon_shutdown.py +1 -1
  237. package/tests/test_docker_provider.py +29 -29
  238. package/tests/test_doctor.py +400 -0
  239. package/tests/test_doctor_skscheduler.py +50 -0
  240. package/tests/test_dreaming_engine.py +147 -0
  241. package/tests/test_dreaming_gtd_capture.py +35 -0
  242. package/tests/test_e2e_automated.py +8 -5
  243. package/tests/test_fuse_mount.py +10 -10
  244. package/tests/test_gtd_brief.py +46 -0
  245. package/tests/test_gtd_malformed_tolerance.py +31 -0
  246. package/tests/test_housekeeping.py +15 -15
  247. package/tests/test_identity_migrate.py +251 -0
  248. package/tests/test_integration_backbone.py +598 -0
  249. package/tests/test_itil_gtd_lifecycle.py +37 -0
  250. package/tests/test_jobs_dropins.py +84 -0
  251. package/tests/test_mcp_server.py +82 -37
  252. package/tests/test_models.py +48 -4
  253. package/tests/test_multi_agent.py +31 -29
  254. package/tests/test_notifications.py +122 -32
  255. package/tests/test_onboard.py +63 -75
  256. package/tests/test_operator_link.py +78 -0
  257. package/tests/test_peers.py +14 -14
  258. package/tests/test_pillars.py +98 -0
  259. package/tests/test_preflight.py +3 -3
  260. package/tests/test_runtime.py +21 -0
  261. package/tests/test_scheduled_tasks.py +11 -6
  262. package/tests/test_scheduler_cli.py +47 -0
  263. package/tests/test_scheduler_features.py +133 -0
  264. package/tests/test_scheduler_integration.py +87 -0
  265. package/tests/test_scheduler_jobs.py +155 -0
  266. package/tests/test_scheduler_runner.py +64 -0
  267. package/tests/test_scheduler_state.py +57 -0
  268. package/tests/test_sdk.py +70 -0
  269. package/tests/test_service_health_incidents.py +34 -0
  270. package/tests/test_service_registry.py +52 -0
  271. package/tests/test_session_briefing.py +130 -0
  272. package/tests/test_snapshots.py +4 -4
  273. package/tests/test_sync_pipeline.py +26 -26
  274. package/tests/test_team_comms.py +2 -2
  275. package/tests/test_testrunner.py +2 -2
  276. package/tests/test_trust_graph.py +18 -0
  277. package/tests/test_unified_search.py +2 -2
  278. package/tests/test_version_check.py +10 -0
  279. package/tests/test_version_cmd.py +8 -8
  280. package/tests/test_whoami.py +1 -1
  281. package/systemd/skcomm-heartbeat.service +0 -18
  282. package/systemd/skcomm-queue-drain.service +0 -17
  283. /package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/package.json +0 -0
  284. /package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/src/openclaw.plugin.json +0 -0
@@ -21,6 +21,7 @@ Steps:
21
21
  from __future__ import annotations
22
22
 
23
23
  import json
24
+ import logging
24
25
  import sys
25
26
  import time
26
27
  from datetime import datetime, timezone
@@ -28,6 +29,8 @@ from pathlib import Path
28
29
  from typing import Optional
29
30
 
30
31
  import click
32
+
33
+ logger = logging.getLogger(__name__)
31
34
  from rich.console import Console
32
35
  from rich.panel import Panel
33
36
  from rich.prompt import Confirm, Prompt
@@ -39,7 +42,7 @@ from . import AGENT_HOME, __version__
39
42
 
40
43
  console = Console()
41
44
 
42
- TOTAL_STEPS = 13 # excludes welcome + celebrate; includes 4 new system-setup steps
45
+ TOTAL_STEPS = 16 # excludes welcome + celebrate; includes pillar install + import step
43
46
 
44
47
 
45
48
  def _step_header(n: int, title: str) -> None:
@@ -90,6 +93,7 @@ def _step_identity(home_path: Path, name: str, email: str | None) -> tuple[str,
90
93
  from .pillars.memory import initialize_memory
91
94
  from .pillars.sync import initialize_sync
92
95
  from .models import AgentConfig, SyncConfig
96
+ from .operator_link import build_agent_manifest, discover_human_operator
93
97
  from .soul import SoulManager
94
98
 
95
99
  with Status(" Generating PGP identity…", console=console, spinner="dots") as s:
@@ -155,12 +159,11 @@ def _step_identity(home_path: Path, name: str, email: str | None) -> tuple[str,
155
159
  for d in skeleton_dirs:
156
160
  d.mkdir(parents=True, exist_ok=True)
157
161
 
158
- manifest = {
159
- "name": name,
160
- "version": __version__,
161
- "created_at": datetime.now(timezone.utc).isoformat(),
162
- "connectors": [],
163
- }
162
+ manifest = build_agent_manifest(
163
+ name,
164
+ __version__,
165
+ operator=discover_human_operator(),
166
+ )
164
167
  (home_path / "manifest.json").write_text(
165
168
  json.dumps(manifest, indent=2), encoding="utf-8"
166
169
  )
@@ -476,67 +479,460 @@ def _step_prereqs() -> dict:
476
479
  return results
477
480
 
478
481
 
479
- def _step_ollama_models(prereqs: dict) -> bool:
480
- """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
+ ("skcomms", "skcomms", "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
+ ("skwhisper", "skwhisper", "Subconscious memory layer (session digester)"),
491
+ ]
492
+
493
+
494
+ def _step_install_pillars() -> dict:
495
+ """Detect missing pillar packages and offer to install them.
496
+
497
+ Returns:
498
+ dict mapping pip_name -> bool (installed successfully).
499
+ """
500
+ import subprocess
501
+
502
+ results = {}
503
+ missing = []
504
+
505
+ click.echo(click.style(" Checking pillar packages…", fg="bright_black"))
506
+ for import_name, pip_name, description in _PILLAR_PACKAGES:
507
+ try:
508
+ __import__(import_name)
509
+ click.echo(click.style(" ✓ ", fg="green") + f"{pip_name} — {description}")
510
+ results[pip_name] = True
511
+ except ImportError:
512
+ click.echo(click.style(" ✗ ", fg="red") + f"{pip_name} — {description} [bold red](missing)[/]")
513
+ missing.append((import_name, pip_name, description))
514
+ results[pip_name] = False
515
+
516
+ if not missing:
517
+ click.echo()
518
+ click.echo(click.style(" ✓ ", fg="green") + "All pillar packages installed")
519
+ return results
520
+
521
+ click.echo()
522
+ click.echo(
523
+ click.style(" ℹ ", fg="cyan")
524
+ + f"{len(missing)} pillar(s) missing. These are needed for full sovereign functionality."
525
+ )
526
+
527
+ choices = {
528
+ "a": "Install all missing pillars",
529
+ "s": "Select which to install",
530
+ "n": "Skip (install later manually)",
531
+ }
532
+ for key, desc in choices.items():
533
+ click.echo(f" [{key}] {desc}")
534
+ choice = click.prompt(" Choice", default="a", show_choices=False).strip().lower()
535
+
536
+ to_install: list[tuple[str, str, str]] = []
537
+ if choice == "a":
538
+ to_install = missing
539
+ elif choice == "s":
540
+ for import_name, pip_name, description in missing:
541
+ if click.confirm(f" Install {pip_name} ({description})?", default=True):
542
+ to_install.append((import_name, pip_name, description))
543
+ else:
544
+ click.echo(click.style(" ↷ ", fg="bright_black") + "Skipped — install later:")
545
+ for _, pip_name, _ in missing:
546
+ click.echo(click.style(" ", fg="bright_black") + f"pip install {pip_name}")
547
+ return results
548
+
549
+ if not to_install:
550
+ return results
551
+
552
+ # Determine pip command — prefer ~/.skenv if it exists, else use current Python
553
+ import os as _os
554
+ skenv_pip = Path(_os.path.expanduser("~/.skenv/bin/pip"))
555
+ if skenv_pip.exists():
556
+ pip_cmd = [str(skenv_pip), "install"]
557
+ else:
558
+ pip_cmd = [sys.executable, "-m", "pip", "install", "--break-system-packages"]
559
+
560
+ for import_name, pip_name, description in to_install:
561
+ click.echo(click.style(" ↓ ", fg="cyan") + f"Installing {pip_name}…")
562
+ try:
563
+ r = subprocess.run(
564
+ [*pip_cmd, pip_name, "-q"],
565
+ capture_output=True, text=True, timeout=120,
566
+ )
567
+ if r.returncode == 0:
568
+ click.echo(click.style(" ✓ ", fg="green") + f"{pip_name} installed")
569
+ results[pip_name] = True
570
+ else:
571
+ click.echo(click.style(" ✗ ", fg="red") + f"{pip_name} failed: {r.stderr.strip()[:100]}")
572
+ click.echo(click.style(" ", fg="bright_black") + f"Try manually: pip install {pip_name}")
573
+ except subprocess.TimeoutExpired:
574
+ click.echo(click.style(" ⚠ ", fg="yellow") + f"{pip_name} timed out")
575
+ except Exception as exc:
576
+ click.echo(click.style(" ⚠ ", fg="yellow") + f"{pip_name}: {exc}")
577
+
578
+ return results
579
+
580
+
581
+ # ---------------------------------------------------------------------------
582
+ # Import sources — detect and import from existing agent platforms
583
+ # ---------------------------------------------------------------------------
584
+
585
+ # (source_id, display_name, detect_func, import_func_key)
586
+ _IMPORT_SOURCES: list[tuple[str, str, str]] = [
587
+ ("openclaw", "OpenClaw (Jarvis)", "~/.openclaw/workspace"),
588
+ ("claude", "Claude Code", "~/.claude"),
589
+ ("cloud9", "Cloud 9 FEB Templates", ""), # always available if cloud9 installed
590
+ ]
591
+
592
+
593
+ def _detect_import_sources(home_path: Path) -> list[dict]:
594
+ """Detect available sources for importing memories, soul, and trust data.
595
+
596
+ Returns:
597
+ List of dicts with 'id', 'name', 'available', 'detail', 'items'.
598
+ """
599
+ sources = []
600
+
601
+ # --- OpenClaw ---
602
+ oc_workspace = Path.home() / ".openclaw" / "workspace"
603
+ oc_memory = oc_workspace / "memory"
604
+ oc_soul = oc_workspace / "SOUL.md"
605
+ oc_identity = oc_workspace / "IDENTITY.md"
606
+ oc_agents = oc_workspace / "agents"
607
+ if oc_workspace.exists():
608
+ items = []
609
+ if oc_memory.exists():
610
+ mem_files = list(oc_memory.glob("*.md"))
611
+ items.append(f"{len(mem_files)} memory files")
612
+ if oc_soul.exists():
613
+ items.append("SOUL.md")
614
+ if oc_identity.exists():
615
+ items.append("IDENTITY.md")
616
+ if oc_agents.exists():
617
+ agent_souls = list(oc_agents.rglob("SOUL.md"))
618
+ if agent_souls:
619
+ items.append(f"{len(agent_souls)} agent soul(s)")
620
+ sources.append({
621
+ "id": "openclaw",
622
+ "name": "OpenClaw (Jarvis)",
623
+ "available": True,
624
+ "detail": ", ".join(items) if items else "workspace found",
625
+ "paths": {
626
+ "memory": oc_memory,
627
+ "soul": oc_soul,
628
+ "identity": oc_identity,
629
+ "agents": oc_agents,
630
+ "workspace": oc_workspace,
631
+ },
632
+ })
633
+
634
+ # --- Claude Code ---
635
+ claude_dir = Path.home() / ".claude"
636
+ claude_memory = None
637
+ if claude_dir.exists():
638
+ # Find project memory dirs
639
+ projects = claude_dir / "projects"
640
+ items = []
641
+ if projects.exists():
642
+ for proj_dir in projects.iterdir():
643
+ mem_dir = proj_dir / "memory"
644
+ if mem_dir.exists() and list(mem_dir.glob("*.md")):
645
+ mem_files = list(mem_dir.glob("*.md"))
646
+ items.append(f"{len(mem_files)} memory file(s) in {proj_dir.name}")
647
+ claude_memory = mem_dir
648
+ memory_md = proj_dir / "MEMORY.md"
649
+ if memory_md.exists():
650
+ items.append(f"MEMORY.md in {proj_dir.name}")
651
+ if items:
652
+ sources.append({
653
+ "id": "claude",
654
+ "name": "Claude Code",
655
+ "available": True,
656
+ "detail": ", ".join(items),
657
+ "paths": {"memory": claude_memory, "projects": projects},
658
+ })
659
+
660
+ # --- Cloud 9 FEB Templates ---
661
+ try:
662
+ import cloud9
663
+ c9_pkg = Path(cloud9.__file__).parent
664
+ feb_files = list(c9_pkg.rglob("*.feb"))
665
+ # Also check skcapstone defaults
666
+ defaults_dir = Path(__file__).parent / "defaults"
667
+ if defaults_dir.exists():
668
+ feb_files.extend(defaults_dir.rglob("*.feb"))
669
+ # Check user cloud9 dirs
670
+ for cloud9_dir in [Path.home() / ".cloud9" / "febs", Path.home() / ".cloud9" / "feb-backups"]:
671
+ if cloud9_dir.exists():
672
+ feb_files.extend(cloud9_dir.glob("*.feb"))
673
+ if feb_files:
674
+ sources.append({
675
+ "id": "cloud9",
676
+ "name": "Cloud 9 FEB Templates",
677
+ "available": True,
678
+ "detail": f"{len(feb_files)} FEB file(s)",
679
+ "paths": {"febs": feb_files},
680
+ })
681
+ except ImportError:
682
+ pass
683
+
684
+ return sources
685
+
686
+
687
+ def _step_import_sources(home_path: Path) -> dict:
688
+ """Detect and import data from existing agent platforms.
689
+
690
+ Args:
691
+ home_path: Agent home directory.
692
+
693
+ Returns:
694
+ dict with 'imported_count' (int) and 'sources' (list of imported source ids).
695
+ """
696
+ import shutil as _shutil
697
+
698
+ result = {"imported_count": 0, "sources": []}
699
+
700
+ click.echo(click.style(" Scanning for existing agent data…", fg="bright_black"))
701
+ sources = _detect_import_sources(home_path)
702
+
703
+ if not sources:
704
+ click.echo(click.style(" ℹ ", fg="cyan") + "No existing agent data found — starting fresh")
705
+ return result
706
+
707
+ click.echo()
708
+ for i, src in enumerate(sources, 1):
709
+ click.echo(
710
+ click.style(f" {i}. ", fg="cyan")
711
+ + f"[bold]{src['name']}[/] — {src['detail']}"
712
+ )
713
+ click.echo()
714
+
715
+ choices = {
716
+ "a": "Import from all sources",
717
+ "s": "Select which to import",
718
+ "n": "Skip (start fresh)",
719
+ }
720
+ for key, desc in choices.items():
721
+ click.echo(f" [{key}] {desc}")
722
+ choice = click.prompt(" Choice", default="a", show_choices=False).strip().lower()
723
+
724
+ to_import: list[dict] = []
725
+ if choice == "a":
726
+ to_import = sources
727
+ elif choice == "s":
728
+ for src in sources:
729
+ if click.confirm(f" Import from {src['name']}?", default=True):
730
+ to_import.append(src)
731
+ else:
732
+ click.echo(click.style(" ↷ ", fg="bright_black") + "Skipped — starting fresh")
733
+ return result
734
+
735
+ if not to_import:
736
+ return result
737
+
738
+ # --- Execute imports ---
739
+ for src in to_import:
740
+ sid = src["id"]
741
+ paths = src.get("paths", {})
742
+ count = 0
743
+
744
+ if sid == "openclaw":
745
+ # Import memories
746
+ mem_src = paths.get("memory")
747
+ if mem_src and mem_src.exists():
748
+ mem_dest = home_path / "memory" / "imported" / "openclaw"
749
+ mem_dest.mkdir(parents=True, exist_ok=True)
750
+ for f in mem_src.glob("*.md"):
751
+ _shutil.copy2(f, mem_dest / f.name)
752
+ count += 1
753
+ click.echo(click.style(" ✓ ", fg="green") + f"Imported {count} memory files from OpenClaw")
754
+
755
+ # Import soul/identity
756
+ for doc_name in ("soul", "identity"):
757
+ doc_path = paths.get(doc_name)
758
+ if doc_path and doc_path.exists():
759
+ dest = home_path / "memory" / "imported" / "openclaw" / doc_path.name
760
+ dest.parent.mkdir(parents=True, exist_ok=True)
761
+ _shutil.copy2(doc_path, dest)
762
+ count += 1
763
+ click.echo(click.style(" ✓ ", fg="green") + f"Imported {doc_path.name} from OpenClaw")
764
+
765
+ # Import agent souls
766
+ agents_dir = paths.get("agents")
767
+ if agents_dir and agents_dir.exists():
768
+ agent_dest = home_path / "memory" / "imported" / "openclaw" / "agents"
769
+ agent_dest.mkdir(parents=True, exist_ok=True)
770
+ for soul_file in agents_dir.rglob("SOUL.md"):
771
+ agent_name = soul_file.parent.name
772
+ target = agent_dest / f"{agent_name}-SOUL.md"
773
+ _shutil.copy2(soul_file, target)
774
+ count += 1
775
+ for mem_file in agents_dir.rglob("MEMORY.md"):
776
+ agent_name = mem_file.parent.name
777
+ target = agent_dest / f"{agent_name}-MEMORY.md"
778
+ _shutil.copy2(mem_file, target)
779
+ count += 1
780
+ click.echo(click.style(" ✓ ", fg="green") + f"Imported agent souls/memories from OpenClaw")
781
+
782
+ elif sid == "claude":
783
+ # Import Claude memory files
784
+ projects_dir = paths.get("projects")
785
+ if projects_dir and projects_dir.exists():
786
+ claude_dest = home_path / "memory" / "imported" / "claude-code"
787
+ claude_dest.mkdir(parents=True, exist_ok=True)
788
+ for proj_dir in projects_dir.iterdir():
789
+ mem_dir = proj_dir / "memory"
790
+ if mem_dir.exists():
791
+ for f in mem_dir.glob("*.md"):
792
+ _shutil.copy2(f, claude_dest / f.name)
793
+ count += 1
794
+ memory_md = proj_dir / "MEMORY.md"
795
+ if memory_md.exists():
796
+ _shutil.copy2(memory_md, claude_dest / f"{proj_dir.name}-MEMORY.md")
797
+ count += 1
798
+ if count:
799
+ click.echo(click.style(" ✓ ", fg="green") + f"Imported {count} files from Claude Code")
800
+
801
+ elif sid == "cloud9":
802
+ # Import FEB files into trust/febs
803
+ febs = paths.get("febs", [])
804
+ if febs:
805
+ febs_dest = home_path / "trust" / "febs"
806
+ febs_dest.mkdir(parents=True, exist_ok=True)
807
+ for feb_path in febs:
808
+ if isinstance(feb_path, Path) and feb_path.exists():
809
+ _shutil.copy2(feb_path, febs_dest / feb_path.name)
810
+ count += 1
811
+ click.echo(click.style(" ✓ ", fg="green") + f"Imported {count} FEB file(s) into trust chain")
812
+
813
+ result["imported_count"] += count
814
+ if count > 0:
815
+ result["sources"].append(sid)
816
+
817
+ click.echo()
818
+ click.echo(
819
+ click.style(" ✓ ", fg="green")
820
+ + f"Total: {result['imported_count']} file(s) imported from {len(result['sources'])} source(s)"
821
+ )
822
+ click.echo(click.style(" ", fg="bright_black") + f"Imported data: {home_path / 'memory' / 'imported'}")
823
+
824
+ return result
825
+
826
+
827
+ def _step_ollama_models(prereqs: dict) -> dict:
828
+ """Configure Ollama host, choose a model, and pull it.
481
829
 
482
830
  Args:
483
831
  prereqs: Result dict from _step_prereqs().
484
832
 
485
833
  Returns:
486
- True if model is available.
834
+ dict with 'ok' (bool), 'model' (str), 'host' (str).
487
835
  """
488
836
  import subprocess
489
837
 
490
838
  DEFAULT_MODEL = "llama3.2"
839
+ DEFAULT_HOST = "http://localhost:11434"
840
+
841
+ result = {"ok": False, "model": DEFAULT_MODEL, "host": DEFAULT_HOST}
491
842
 
492
843
  if not prereqs.get("ollama"):
493
844
  click.echo(click.style(" ⚠ ", fg="yellow") + "Ollama not available — skipping model pull")
845
+ click.echo(click.style(" ", fg="bright_black") + "Install: curl -fsSL https://ollama.ai/install.sh | sh")
494
846
  click.echo(click.style(" ", fg="bright_black") + f"Pull later: ollama pull {DEFAULT_MODEL}")
495
- return False
847
+ return result
848
+
849
+ # --- Ollama Host ---
850
+ click.echo(click.style(" ℹ ", fg="cyan") + f"Ollama is used for local/private LLM inference.")
851
+ click.echo(click.style(" ", fg="bright_black") + f"Default: {DEFAULT_HOST}")
852
+ custom_host = click.prompt(
853
+ " Ollama host URL",
854
+ default=DEFAULT_HOST,
855
+ show_default=True,
856
+ )
857
+ result["host"] = custom_host.rstrip("/")
496
858
 
497
- # Check if model already present
859
+ # Set env for this session so ollama CLI uses the right host
860
+ env = dict(**__import__("os").environ)
861
+ if result["host"] != DEFAULT_HOST:
862
+ env["OLLAMA_HOST"] = result["host"]
863
+ click.echo(click.style(" ✓ ", fg="green") + f"Using Ollama at: [cyan]{result['host']}[/]")
864
+
865
+ # --- List available models ---
866
+ available_models: list[str] = []
498
867
  try:
499
868
  r = subprocess.run(
500
869
  ["ollama", "list"],
501
- capture_output=True, text=True, timeout=10,
870
+ capture_output=True, text=True, timeout=10, env=env,
502
871
  )
503
- if DEFAULT_MODEL in (r.stdout or ""):
504
- click.echo(click.style("", fg="green") + f"{DEFAULT_MODEL} already present")
505
- return True
506
- except Exception:
507
- pass
872
+ if r.returncode == 0 and r.stdout.strip():
873
+ lines = r.stdout.strip().split("\n")[1:] # skip header
874
+ for line in lines:
875
+ model_name = line.split()[0] if line.strip() else ""
876
+ if model_name:
877
+ available_models.append(model_name)
878
+ except Exception as exc:
879
+ logger.debug("Failed to list ollama models: %s", exc)
880
+
881
+ if available_models:
882
+ click.echo(click.style(" ℹ ", fg="cyan") + "Models already available:")
883
+ for m in available_models[:10]:
884
+ click.echo(click.style(" ", fg="bright_black") + m)
885
+
886
+ # --- Choose model ---
887
+ click.echo()
888
+ click.echo(click.style(" ℹ ", fg="cyan") + "Popular models: llama3.2 (~2GB), qwen3:14b (~9GB), deepseek-r1:14b (~9GB)")
889
+ chosen = click.prompt(
890
+ " Model to use",
891
+ default=DEFAULT_MODEL,
892
+ show_default=True,
893
+ )
894
+ result["model"] = chosen
508
895
 
509
- if not click.confirm(f" Pull default model ({DEFAULT_MODEL}, ~2 GB)?", default=True):
510
- click.echo(click.style(" ↷ ", fg="bright_black") + f"Skipped pull later: ollama pull {DEFAULT_MODEL}")
511
- return False
896
+ # Check if already present
897
+ if any(chosen in m for m in available_models):
898
+ click.echo(click.style(" ✓ ", fg="green") + f"{chosen} already present")
899
+ result["ok"] = True
900
+ return result
901
+
902
+ # --- Pull ---
903
+ if not click.confirm(f" Pull {chosen}? (this may take a few minutes)", default=True):
904
+ click.echo(click.style(" ↷ ", fg="bright_black") + f"Skipped — pull later: ollama pull {chosen}")
905
+ return result
512
906
 
513
- click.echo(click.style(" ↓ ", fg="cyan") + f"Pulling {DEFAULT_MODEL} (this may take a few minutes)…")
907
+ click.echo(click.style(" ↓ ", fg="cyan") + f"Pulling {chosen}…")
514
908
  try:
515
- result = subprocess.run(
516
- ["ollama", "pull", DEFAULT_MODEL],
517
- timeout=600,
909
+ pull_result = subprocess.run(
910
+ ["ollama", "pull", chosen],
911
+ timeout=600, env=env,
518
912
  )
519
- if result.returncode == 0:
520
- click.echo(click.style(" ✓ ", fg="green") + f"{DEFAULT_MODEL} ready")
521
- return True
913
+ if pull_result.returncode == 0:
914
+ click.echo(click.style(" ✓ ", fg="green") + f"{chosen} ready")
915
+ result["ok"] = True
916
+ return result
522
917
  else:
523
- click.echo(click.style(" ✗ ", fg="red") + f"Pull failed (exit {result.returncode})")
524
- click.echo(click.style(" ", fg="bright_black") + f"Retry: ollama pull {DEFAULT_MODEL}")
525
- return False
918
+ click.echo(click.style(" ✗ ", fg="red") + f"Pull failed (exit {pull_result.returncode})")
919
+ click.echo(click.style(" ", fg="bright_black") + f"Retry: ollama pull {chosen}")
920
+ return result
526
921
  except subprocess.TimeoutExpired:
527
922
  click.echo(click.style(" ⚠ ", fg="yellow") + "Pull timed out — run manually later")
528
- click.echo(click.style(" ", fg="bright_black") + f"ollama pull {DEFAULT_MODEL}")
529
- return False
923
+ click.echo(click.style(" ", fg="bright_black") + f"ollama pull {chosen}")
924
+ return result
530
925
  except Exception as exc:
531
926
  click.echo(click.style(" ⚠ ", fg="yellow") + f"Pull error: {exc}")
532
- return False
927
+ return result
533
928
 
534
929
 
535
- def _step_config_files(home_path: Path) -> tuple:
930
+ def _step_config_files(home_path: Path, ollama_config: dict | None = None) -> tuple:
536
931
  """Write default consciousness.yaml and model_profiles.yaml.
537
932
 
538
933
  Args:
539
934
  home_path: Agent home directory.
935
+ ollama_config: Optional dict with 'host' and 'model' from Ollama step.
540
936
 
541
937
  Returns:
542
938
  (consciousness_ok, profiles_ok) booleans.
@@ -554,8 +950,17 @@ def _step_config_files(home_path: Path) -> tuple:
554
950
  else:
555
951
  try:
556
952
  from .consciousness_config import write_default_config
953
+ from .consciousness_loop import ConsciousnessConfig
954
+
955
+ # If user configured a custom Ollama host/model, patch the defaults
956
+ overrides = {}
957
+ if ollama_config:
958
+ if ollama_config.get("host") and ollama_config["host"] != "http://localhost:11434":
959
+ overrides["ollama_host"] = ollama_config["host"]
960
+ if ollama_config.get("model") and ollama_config["model"] != "llama3.2":
961
+ overrides["ollama_model"] = ollama_config["model"]
557
962
 
558
- config_path = write_default_config(home_path)
963
+ config_path = write_default_config(home_path, **overrides)
559
964
  click.echo(click.style(" ✓ ", fg="green") + f"consciousness.yaml written")
560
965
  click.echo(click.style(" ", fg="bright_black") + str(config_path))
561
966
  consciousness_ok = True
@@ -586,22 +991,49 @@ def _step_config_files(home_path: Path) -> tuple:
586
991
  return consciousness_ok, profiles_ok
587
992
 
588
993
 
589
- def _step_systemd_service() -> bool:
590
- """Install systemd user service for auto-start (optional).
994
+ def _step_autostart_service(agent_name: str = "sovereign") -> bool:
995
+ """Install auto-start service (systemd on Linux, launchd on macOS).
996
+
997
+ Prompts the user to choose which services to install and uses
998
+ the agent name from onboarding for environment variables.
999
+
1000
+ Args:
1001
+ agent_name: The agent name chosen during onboarding.
591
1002
 
592
1003
  Returns:
593
1004
  True if service was installed.
594
1005
  """
595
1006
  import platform
596
1007
 
597
- if platform.system() != "Linux":
598
- click.echo(click.style(" ↷ ", fg="bright_black") + "Systemd only available on Linux — skipped")
1008
+ system = platform.system()
1009
+
1010
+ if system == "Linux":
1011
+ return _step_systemd_service_linux(agent_name)
1012
+ elif system == "Darwin":
1013
+ return _step_launchd_service_macos(agent_name)
1014
+ else:
1015
+ click.echo(
1016
+ click.style(" ↷ ", fg="bright_black")
1017
+ + f"Auto-start not supported on {system} — skipped"
1018
+ )
599
1019
  return False
600
1020
 
1021
+
1022
+ def _step_systemd_service_linux(agent_name: str = "sovereign") -> bool:
1023
+ """Install systemd user service for an agent (Linux only).
1024
+
1025
+ Uses the template unit ``skcapstone@.service`` so each agent
1026
+ gets its own independent service instance. Multiple agents can
1027
+ run simultaneously on the same machine.
1028
+
1029
+ Args:
1030
+ agent_name: Agent slug from onboarding (e.g. "jarvis").
1031
+ """
1032
+ service_name = f"skcapstone@{agent_name}.service"
601
1033
  if not click.confirm(" Install systemd user service for auto-start at login?", default=False):
602
1034
  click.echo(
603
1035
  click.style(" ↷ ", fg="bright_black")
604
- + "Skipped — run 'skcapstone systemd install' to enable later"
1036
+ + "Skipped — run 'skcapstone daemon install' to enable later"
605
1037
  )
606
1038
  return False
607
1039
 
@@ -613,35 +1045,232 @@ def _step_systemd_service() -> bool:
613
1045
  click.echo(click.style(" ", fg="bright_black") + "Try: systemctl --user status")
614
1046
  return False
615
1047
 
616
- result = install_service(enable=True, start=False)
1048
+ result = install_service(agent_name=agent_name, enable=True, start=False)
617
1049
  if result.get("installed"):
618
- click.echo(click.style(" ✓ ", fg="green") + "Systemd service installed")
1050
+ click.echo(click.style(" ✓ ", fg="green") + f"Systemd service installed")
619
1051
  if result.get("enabled"):
620
- click.echo(click.style(" ✓ ", fg="green") + "Service enabled — auto-starts at login")
621
- click.echo(click.style(" ", fg="bright_black") + "Start now: systemctl --user start skcapstone")
1052
+ click.echo(click.style(" ✓ ", fg="green") + f"Service enabled — auto-starts at login")
1053
+ click.echo(
1054
+ click.style(" ", fg="bright_black")
1055
+ + f"Start now: systemctl --user start {service_name}"
1056
+ )
622
1057
  return True
623
1058
  else:
624
1059
  click.echo(click.style(" ✗ ", fg="red") + "Service install failed")
625
- click.echo(click.style(" ", fg="bright_black") + "Run manually: skcapstone systemd install")
1060
+ click.echo(click.style(" ", fg="bright_black") + "Run manually: skcapstone daemon install")
626
1061
  return False
627
1062
  except Exception as exc:
628
1063
  click.echo(click.style(" ⚠ ", fg="yellow") + f"Systemd: {exc}")
629
1064
  return False
630
1065
 
631
1066
 
1067
+ def _step_launchd_service_macos(agent_name: str) -> bool:
1068
+ """Install launchd user agents (macOS only).
1069
+
1070
+ Shows available services, lets the user choose, and installs
1071
+ plist files to ~/Library/LaunchAgents/.
1072
+
1073
+ Args:
1074
+ agent_name: Agent name for SKCAPSTONE_AGENT env var.
1075
+
1076
+ Returns:
1077
+ True if at least one service was installed.
1078
+ """
1079
+ try:
1080
+ from .launchd import install_service, list_available_services
1081
+ except ImportError as exc:
1082
+ click.echo(click.style(" ⚠ ", fg="yellow") + f"launchd module not available: {exc}")
1083
+ return False
1084
+
1085
+ click.echo(f" Agent name: [cyan]{agent_name}[/] (used in SKCAPSTONE_AGENT)")
1086
+ click.echo()
1087
+
1088
+ # Show available services
1089
+ available = list_available_services(agent_name)
1090
+ core_services = [s for s in available if s["available"] and not s["suffix"].startswith("sk")]
1091
+ optional_services = [s for s in available if s["available"] and s["suffix"].startswith("sk")]
1092
+
1093
+ click.echo(" Available services:")
1094
+ all_available = [s for s in available if s["available"]]
1095
+ for i, svc in enumerate(all_available, 1):
1096
+ click.echo(f" {i}. {svc['description']} ({svc['label']})")
1097
+ click.echo()
1098
+
1099
+ if not click.confirm(" Install launchd services for auto-start at login?", default=True):
1100
+ click.echo(
1101
+ click.style(" ↷ ", fg="bright_black")
1102
+ + "Skipped — run 'skcapstone daemon install' to enable later"
1103
+ )
1104
+ return False
1105
+
1106
+ # Ask: all or pick?
1107
+ install_all = click.confirm(" Install all available services?", default=True)
1108
+
1109
+ selected_suffixes: list[str] = []
1110
+ if install_all:
1111
+ selected_suffixes = [s["suffix"] for s in all_available]
1112
+ else:
1113
+ click.echo(" Enter service numbers (comma-separated), or 'none' to skip:")
1114
+ raw = click.prompt(" Services", default="1")
1115
+ if raw.strip().lower() == "none":
1116
+ click.echo(click.style(" ↷ ", fg="bright_black") + "Skipped")
1117
+ return False
1118
+ try:
1119
+ indices = [int(x.strip()) - 1 for x in raw.split(",")]
1120
+ selected_suffixes = [
1121
+ all_available[i]["suffix"]
1122
+ for i in indices
1123
+ if 0 <= i < len(all_available)
1124
+ ]
1125
+ except (ValueError, IndexError):
1126
+ click.echo(click.style(" ⚠ ", fg="yellow") + "Invalid selection — installing core services only")
1127
+ selected_suffixes = [s["suffix"] for s in all_available if not s["suffix"].startswith("sk")]
1128
+
1129
+ if not selected_suffixes:
1130
+ click.echo(click.style(" ↷ ", fg="bright_black") + "No services selected")
1131
+ return False
1132
+
1133
+ # Ask about immediate start
1134
+ start_now = click.confirm(" Start services now?", default=False)
1135
+
1136
+ try:
1137
+ result = install_service(
1138
+ agent_name=agent_name,
1139
+ services=selected_suffixes,
1140
+ start=start_now,
1141
+ )
1142
+
1143
+ if result.get("installed"):
1144
+ for svc in result.get("services", []):
1145
+ status = "[green]loaded[/]" if svc.get("loaded") else "[dim]installed[/]"
1146
+ click.echo(click.style(" ✓ ", fg="green") + f"{svc['label']} — {status}")
1147
+
1148
+ click.echo()
1149
+ click.echo(click.style(" ", fg="bright_black") + "Manage services:")
1150
+ click.echo(click.style(" ", fg="bright_black") + " launchctl list | grep skcapstone")
1151
+ click.echo(click.style(" ", fg="bright_black") + " launchctl start com.skcapstone.daemon")
1152
+ click.echo(click.style(" ", fg="bright_black") + " skcapstone daemon uninstall")
1153
+ return True
1154
+ else:
1155
+ click.echo(click.style(" ✗ ", fg="red") + "No services were installed")
1156
+ return False
1157
+
1158
+ except Exception as exc:
1159
+ click.echo(click.style(" ⚠ ", fg="yellow") + f"launchd install: {exc}")
1160
+ return False
1161
+
1162
+
1163
+ def _step_shell_profile(
1164
+ home_path: Path, agent_name: str, agent_slug: str
1165
+ ) -> bool:
1166
+ """Write SKCAPSTONE profile environment variables to ~/.bashrc.
1167
+
1168
+ Asks the user whether to set this agent as the default profile.
1169
+ Appends SKCAPSTONE_HOME, SKCAPSTONE_AGENT, and PATH entries.
1170
+
1171
+ Args:
1172
+ home_path: Agent home directory.
1173
+ agent_name: Display name of the agent (e.g. "Jarvis").
1174
+ agent_slug: Slug form used for SKCAPSTONE_AGENT (e.g. "jarvis").
1175
+
1176
+ Returns:
1177
+ True if profile was written, False if skipped.
1178
+ """
1179
+ import os as _os
1180
+
1181
+ bashrc = Path.home() / ".bashrc"
1182
+ marker = "# --- SKCapstone profile ---"
1183
+
1184
+ # Check if profile block already exists
1185
+ existing = ""
1186
+ if bashrc.exists():
1187
+ existing = bashrc.read_text(encoding="utf-8")
1188
+ if marker in existing:
1189
+ _ok("SKCapstone profile already present in ~/.bashrc")
1190
+ # Offer to update it
1191
+ if not Confirm.ask(
1192
+ f" Update profile to agent [cyan]{agent_name}[/]?",
1193
+ default=True,
1194
+ ):
1195
+ return True
1196
+ # Remove old block so we can rewrite it
1197
+ lines = existing.splitlines(keepends=True)
1198
+ new_lines: list[str] = []
1199
+ skip = False
1200
+ for line in lines:
1201
+ if marker in line:
1202
+ skip = not skip # toggle on first marker, off on second
1203
+ continue
1204
+ if not skip:
1205
+ new_lines.append(line)
1206
+ existing = "".join(new_lines)
1207
+
1208
+ set_default = Confirm.ask(
1209
+ f" Set [cyan]{agent_name}[/] as default SKCAPSTONE_AGENT in ~/.bashrc?",
1210
+ default=True,
1211
+ )
1212
+
1213
+ if not set_default:
1214
+ _info("Skipped — set manually: export SKCAPSTONE_AGENT=<name>")
1215
+ return False
1216
+
1217
+ block = (
1218
+ f"\n{marker}\n"
1219
+ f'export SKCAPSTONE_HOME="{home_path}"\n'
1220
+ f'export SKCAPSTONE_AGENT="{agent_slug}"\n'
1221
+ f'export PATH="$HOME/.skenv/bin:$PATH"\n'
1222
+ f"{marker}\n"
1223
+ )
1224
+
1225
+ with open(bashrc, "a" if marker not in (existing or "") else "w", encoding="utf-8") as f:
1226
+ if marker not in (existing or ""):
1227
+ f.write(block)
1228
+ else:
1229
+ # Rewrite with updated block
1230
+ f.write(existing.rstrip("\n") + block)
1231
+
1232
+ _ok(f"~/.bashrc updated — SKCAPSTONE_AGENT={agent_slug}")
1233
+ _info("Run [bold]source ~/.bashrc[/] or open a new terminal to apply")
1234
+
1235
+ # Also export into current process so subsequent steps see it
1236
+ _os.environ["SKCAPSTONE_HOME"] = str(home_path)
1237
+ _os.environ["SKCAPSTONE_AGENT"] = agent_slug
1238
+
1239
+ return True
1240
+
1241
+
632
1242
  def _step_doctor_check(home_path: Path) -> "object":
633
1243
  """Run doctor diagnostics and print results.
634
1244
 
1245
+ Non-fatal — errors are logged as warnings but never block onboarding.
1246
+
635
1247
  Args:
636
1248
  home_path: Agent home directory.
637
1249
 
638
1250
  Returns:
639
- DiagnosticReport from doctor.run_diagnostics().
1251
+ DiagnosticReport from doctor.run_diagnostics(), or a stub on error.
640
1252
  """
641
- from .doctor import run_diagnostics
1253
+ try:
1254
+ from .doctor import run_diagnostics
1255
+ except Exception as exc:
1256
+ _warn(f"Could not load diagnostics module: {exc}")
1257
+ # Return a stub so the summary table still works
1258
+ from types import SimpleNamespace
1259
+
1260
+ return SimpleNamespace(
1261
+ all_passed=False, passed_count=0, failed_count=0, total_count=0, checks=[]
1262
+ )
642
1263
 
643
1264
  click.echo(click.style(" Running diagnostics…", fg="bright_black"))
644
- report = run_diagnostics(home_path)
1265
+ try:
1266
+ report = run_diagnostics(home_path)
1267
+ except Exception as exc:
1268
+ _warn(f"Diagnostics failed: {exc}")
1269
+ from types import SimpleNamespace
1270
+
1271
+ return SimpleNamespace(
1272
+ all_passed=False, passed_count=0, failed_count=0, total_count=0, checks=[]
1273
+ )
645
1274
 
646
1275
  categories_seen: set = set()
647
1276
  for check in report.checks:
@@ -667,44 +1296,61 @@ def _step_doctor_check(home_path: Path) -> "object":
667
1296
 
668
1297
 
669
1298
  def _step_test_consciousness(home_path: Path) -> bool:
670
- """Send a test message through the consciousness loop (optional).
1299
+ """Send a quick test message to the configured LLM backend.
1300
+
1301
+ Reads the consciousness config to determine the default backend
1302
+ (typically the local Ollama model chosen during onboarding) and
1303
+ sends a single prompt to verify the pipeline works end-to-end.
671
1304
 
672
1305
  Args:
673
1306
  home_path: Agent home directory.
674
1307
 
675
1308
  Returns:
676
- True if the loop responded successfully.
1309
+ True if the LLM responded successfully.
677
1310
  """
678
- if not click.confirm(" Send a test message to verify the consciousness loop?", default=True):
1311
+ if not click.confirm(" Send a test message to verify the LLM backend?", default=False):
679
1312
  click.echo(
680
1313
  click.style(" ↷ ", fg="bright_black")
681
1314
  + "Skipped — test later: skcapstone consciousness test 'hello'"
682
1315
  )
683
1316
  return False
684
1317
 
685
- click.echo(click.style(" Sending test message…", fg="bright_black"))
1318
+ # Load config to discover which backend/model was configured
686
1319
  try:
687
1320
  from .consciousness_config import load_consciousness_config
688
- from .consciousness_loop import LLMBridge, SystemPromptBuilder, _classify_message
689
-
690
1321
  config = load_consciousness_config(home_path)
691
- bridge = LLMBridge(config)
692
- builder = SystemPromptBuilder(home_path, config.max_context_tokens)
693
- signal = _classify_message("Onboard wizard test — please confirm you are running.")
694
- system_prompt = builder.build()
695
- response = bridge.generate(system_prompt, "Onboard wizard test — please confirm you are running.", signal)
1322
+ except Exception:
1323
+ # Fall back to defaults
1324
+ ollama_model = "llama3.2"
1325
+ ollama_host = "http://localhost:11434"
1326
+ config = None
1327
+ else:
1328
+ ollama_model = config.ollama_model
1329
+ ollama_host = config.ollama_host
1330
+
1331
+ click.echo(
1332
+ click.style(" Testing ", fg="bright_black")
1333
+ + click.style(f"{ollama_model}", fg="cyan")
1334
+ + click.style(f" @ {ollama_host}…", fg="bright_black")
1335
+ )
1336
+
1337
+ try:
1338
+ from skseed.llm import ollama_callback
1339
+
1340
+ callback = ollama_callback(model=ollama_model, base_url=ollama_host)
1341
+ response = callback("Respond in one sentence: are you online?")
696
1342
  if response:
697
1343
  preview = response[:80].replace("\n", " ")
698
- click.echo(click.style(" ✓ ", fg="green") + f"Consciousness loop active")
1344
+ click.echo(click.style(" ✓ ", fg="green") + "LLM backend active")
699
1345
  click.echo(click.style(" ", fg="bright_black") + f"Response: {preview!r}")
700
1346
  return True
701
1347
  else:
702
- click.echo(click.style(" ⚠ ", fg="yellow") + "Empty response — loop may not be fully configured")
703
- click.echo(click.style(" ", fg="bright_black") + "Start daemon: skcapstone daemon start")
1348
+ click.echo(click.style(" ⚠ ", fg="yellow") + "Empty response — model may still be loading")
1349
+ click.echo(click.style(" ", fg="bright_black") + f"Try: ollama run {ollama_model}")
704
1350
  return False
705
1351
  except Exception as exc:
706
1352
  click.echo(click.style(" ⚠ ", fg="yellow") + f"Test failed: {exc}")
707
- click.echo(click.style(" ", fg="bright_black") + "Start daemon: skcapstone daemon start --foreground")
1353
+ click.echo(click.style(" ", fg="bright_black") + f"Check: ollama serve && ollama run {ollama_model}")
708
1354
  return False
709
1355
 
710
1356
 
@@ -716,21 +1362,24 @@ def _step_test_consciousness(home_path: Path) -> bool:
716
1362
  def run_onboard(home: Optional[str] = None) -> None:
717
1363
  """Run the interactive onboarding wizard.
718
1364
 
719
- Covers all 13 setup steps:
720
- 1. Prerequisites check (Python, pip, Ollama)
721
- 2. Identity create ~/.skcapstone/ + generate PGP key
722
- 3. Ollama models pull llama3.2
723
- 4. Config files — consciousness.yaml + model_profiles.yaml
724
- 5. Soul Blueprint
725
- 6. Memory & Seeds
726
- 7. Rehydration Ritual
727
- 8. Trust Chain Verification
728
- 9. Mesh Connection (Syncthing)
729
- 10. First Heartbeat
730
- 11. Crush Terminal AI
731
- 12. Coordination Board
732
- 13. Systemd Service (optional)
733
- [post-wizard] Doctor diagnostics + consciousness test
1365
+ Covers all 16 setup steps:
1366
+ 1. Prerequisites (Python, pip, Ollama)
1367
+ 2. Pillar Packages (install missing SK* + skwhisper)
1368
+ 3. Identity (CapAuth PGP + Syncthing sync)
1369
+ 4. Ollama Models
1370
+ 5. Config Files (consciousness.yaml + model_profiles.yaml)
1371
+ 6. Soul Blueprint
1372
+ 7. Memory & Seeds
1373
+ 8. Import Sources (OpenClaw, Cloud 9 FEBs)
1374
+ 9. Rehydration Ritual
1375
+ 10. Trust Chain Verification
1376
+ 11. Mesh Connection (Syncthing)
1377
+ 12. First Heartbeat
1378
+ 13. Crush Terminal AI
1379
+ 14. Coordination Board
1380
+ 15. Auto-Start Service (systemd template per agent)
1381
+ 16. Shell Profile (~/.bashrc env vars)
1382
+ [post-wizard] Doctor diagnostics + consciousness test (optional)
734
1383
 
735
1384
  Args:
736
1385
  home: Override agent home directory.
@@ -764,19 +1413,6 @@ def run_onboard(home: Optional[str] = None) -> None:
764
1413
  console.print(" [dim]Come back when you're ready. The Kingdom waits.[/]\n")
765
1414
  return
766
1415
 
767
- # -----------------------------------------------------------------------
768
- # Gather basic identity info up front
769
- # -----------------------------------------------------------------------
770
- console.print()
771
- name = Prompt.ask(" What's your name?", default="Sovereign")
772
- entity_type = Prompt.ask(
773
- " Are you a [cyan]human[/] or an [cyan]ai[/]?",
774
- choices=["human", "ai"],
775
- default="ai",
776
- )
777
- email = Prompt.ask(" Email (optional, press Enter to skip)", default="")
778
- console.print()
779
-
780
1416
  # -----------------------------------------------------------------------
781
1417
  # Step 1: Prerequisites
782
1418
  # -----------------------------------------------------------------------
@@ -784,86 +1420,219 @@ def run_onboard(home: Optional[str] = None) -> None:
784
1420
  prereqs = _step_prereqs()
785
1421
 
786
1422
  # -----------------------------------------------------------------------
787
- # Step 2: Identity + Directory Structure
1423
+ # Step 2: Install Missing Pillars
1424
+ # -----------------------------------------------------------------------
1425
+ _step_header(2, "Pillar Packages")
1426
+ pillar_results = _step_install_pillars()
1427
+
788
1428
  # -----------------------------------------------------------------------
789
- _step_header(2, "Identity")
1429
+ # Step 3: Operator Identity (human) + Agent Identity
1430
+ # -----------------------------------------------------------------------
1431
+ _step_header(3, "Identity")
1432
+
1433
+ # --- Detect or create human operator profile in ~/.capauth ---
1434
+ operator_name = None
1435
+ operator_fingerprint = None
1436
+ try:
1437
+ from capauth.profile import load_profile, init_profile as capauth_init
1438
+
1439
+ try:
1440
+ profile = load_profile()
1441
+ operator_name = profile.entity.name
1442
+ operator_fingerprint = profile.key_info.fingerprint
1443
+ entity_type_val = getattr(profile.entity, "entity_type", None)
1444
+ is_human = str(entity_type_val).lower() in ("human", "entitytype.human")
1445
+ if is_human:
1446
+ _ok(
1447
+ f"Operator identity found: [cyan]{operator_name}[/] "
1448
+ f"({operator_fingerprint[:16]}…)"
1449
+ )
1450
+ else:
1451
+ # Existing profile is an AI — need a human operator first
1452
+ _warn(
1453
+ f"Existing profile is type '{entity_type_val}' — "
1454
+ f"a human operator profile is recommended"
1455
+ )
1456
+ is_human = False
1457
+ except Exception:
1458
+ is_human = False
1459
+ profile = None
1460
+
1461
+ if not is_human:
1462
+ console.print()
1463
+ console.print(
1464
+ " [bold cyan]Operator Setup[/] — Your sovereign agent needs a human operator.\n"
1465
+ " This creates your personal PGP identity at [dim]~/.capauth/[/].\n"
1466
+ " Your agent will be registered under this identity.\n"
1467
+ )
1468
+ op_name = Prompt.ask(" Operator name (your name)", default="Sovereign")
1469
+ op_email = Prompt.ask(" Operator email", default="")
1470
+ console.print()
1471
+
1472
+ with Status(" Generating operator PGP identity…", console=console, spinner="dots") as s:
1473
+ try:
1474
+ import shutil as _shutil_capauth
1475
+ capauth_home = Path.home() / ".capauth"
1476
+ if capauth_home.exists():
1477
+ # Back up and recreate
1478
+ backup = capauth_home.with_name(".capauth.bak")
1479
+ if backup.exists():
1480
+ _shutil_capauth.rmtree(backup)
1481
+ capauth_home.rename(backup)
1482
+ profile = capauth_init(
1483
+ name=op_name,
1484
+ email=op_email or f"{op_name.lower().replace(' ', '-')}@capauth.local",
1485
+ passphrase="",
1486
+ entity_type="human",
1487
+ )
1488
+ operator_name = profile.entity.name
1489
+ operator_fingerprint = profile.key_info.fingerprint
1490
+ s.stop()
1491
+ _ok(
1492
+ f"Operator identity created: [cyan]{operator_name}[/] "
1493
+ f"({operator_fingerprint[:16]}…)"
1494
+ )
1495
+ except Exception as exc:
1496
+ s.stop()
1497
+ _warn(f"Operator identity creation failed: {exc}")
1498
+ _info("Continue anyway — agent will use a degraded identity")
1499
+ except ImportError:
1500
+ _warn("capauth not installed — skipping operator identity")
1501
+ _info("Install: pip install capauth")
1502
+
1503
+ # --- Now set up the agent identity ---
1504
+ console.print()
1505
+ # Derive agent name from --agent flag (SKCAPSTONE_AGENT env) or ask
1506
+ import os as _os
1507
+ agent_flag = _os.environ.get("SKCAPSTONE_AGENT", "").strip()
1508
+ if agent_flag and agent_flag not in ("lumina",):
1509
+ # Agent name was specified via --agent flag — use it as default
1510
+ default_agent = agent_flag.capitalize()
1511
+ else:
1512
+ default_agent = "Sovereign"
1513
+ name = Prompt.ask(" Agent name", default=default_agent)
1514
+
1515
+ email = Prompt.ask(
1516
+ " Agent email (optional, press Enter to skip)",
1517
+ default=f"{name.lower().replace(' ', '-')}@skcapstone.local",
1518
+ )
1519
+
1520
+ if operator_name:
1521
+ _info(f"Agent [cyan]{name}[/] will be registered under operator [cyan]{operator_name}[/]")
1522
+ console.print()
1523
+
790
1524
  fingerprint, identity_status = _step_identity(home_path, name, email or None)
791
1525
 
1526
+ # --- Offer CapAuth Syncthing sync (non-blocking) ---
1527
+ try:
1528
+ from capauth.sync import is_syncthing_available, is_sync_configured, setup_syncthing_sync
1529
+
1530
+ if is_syncthing_available() and not is_sync_configured():
1531
+ console.print()
1532
+ if Confirm.ask(
1533
+ " Sync identity across cluster via Syncthing?",
1534
+ default=True,
1535
+ ):
1536
+ ok = setup_syncthing_sync()
1537
+ if ok:
1538
+ _ok("CapAuth identity will replicate to all mesh nodes")
1539
+ else:
1540
+ _warn("Could not configure sync — set up manually: capauth sync")
1541
+ elif is_sync_configured():
1542
+ _ok("CapAuth Syncthing sync already configured")
1543
+ except ImportError:
1544
+ pass # capauth.sync not available yet
1545
+ except Exception as exc:
1546
+ _warn(f"Sync setup skipped: {exc}")
1547
+
792
1548
  # -----------------------------------------------------------------------
793
- # Step 3: Ollama Models
1549
+ # Step 4: Ollama Models
794
1550
  # -----------------------------------------------------------------------
795
- _step_header(3, "Ollama Models")
796
- ollama_ok = _step_ollama_models(prereqs)
1551
+ _step_header(4, "Ollama Models")
1552
+ ollama_result = _step_ollama_models(prereqs)
1553
+ ollama_ok = ollama_result["ok"]
797
1554
 
798
1555
  # -----------------------------------------------------------------------
799
- # Step 4: Config Files (consciousness.yaml + model_profiles.yaml)
1556
+ # Step 5: Config Files (consciousness.yaml + model_profiles.yaml)
800
1557
  # -----------------------------------------------------------------------
801
- _step_header(4, "Config Files")
802
- consciousness_ok, profiles_ok = _step_config_files(home_path)
1558
+ _step_header(5, "Config Files")
1559
+ consciousness_ok, profiles_ok = _step_config_files(home_path, ollama_config=ollama_result)
803
1560
 
804
1561
  # -----------------------------------------------------------------------
805
- # Step 5: Soul Blueprint
1562
+ # Step 6: Soul Blueprint
806
1563
  # -----------------------------------------------------------------------
807
- _step_header(5, "Soul Blueprint")
1564
+ _step_header(6, "Soul Blueprint")
808
1565
  title = _step_soul(home_path, name)
809
1566
 
810
1567
  # -----------------------------------------------------------------------
811
- # Step 6: Memory
1568
+ # Step 7: Memory
812
1569
  # -----------------------------------------------------------------------
813
- _step_header(6, "Memory")
1570
+ _step_header(7, "Memory")
814
1571
  seed_count = _step_memory(home_path)
815
1572
 
816
1573
  # -----------------------------------------------------------------------
817
- # Step 7: Rehydration Ritual
1574
+ # Step 8: Import from Existing Sources
1575
+ # -----------------------------------------------------------------------
1576
+ _step_header(8, "Import Sources")
1577
+ import_result = _step_import_sources(home_path)
1578
+
818
1579
  # -----------------------------------------------------------------------
819
- _step_header(7, "Rehydration Ritual")
1580
+ # Step 9: Rehydration Ritual
1581
+ # -----------------------------------------------------------------------
1582
+ _step_header(9, "Rehydration Ritual")
820
1583
  _step_ritual(home_path)
821
1584
 
822
1585
  # -----------------------------------------------------------------------
823
- # Step 8: Trust Chain Verification
1586
+ # Step 10: Trust Chain Verification
824
1587
  # -----------------------------------------------------------------------
825
- _step_header(8, "Trust Chain Verification")
1588
+ _step_header(10, "Trust Chain Verification")
826
1589
  trust_status = _step_trust(home_path)
827
1590
 
828
1591
  # -----------------------------------------------------------------------
829
- # Step 9: Mesh Connection (Syncthing)
1592
+ # Step 11: Mesh Connection (Syncthing)
830
1593
  # -----------------------------------------------------------------------
831
- _step_header(9, "Mesh Connection")
1594
+ _step_header(11, "Mesh Connection")
832
1595
  mesh_ok = _step_mesh(home_path)
833
1596
 
834
1597
  # -----------------------------------------------------------------------
835
- # Step 10: First Heartbeat
1598
+ # Step 12: First Heartbeat
836
1599
  # -----------------------------------------------------------------------
837
- _step_header(10, "First Heartbeat")
1600
+ _step_header(12, "First Heartbeat")
838
1601
  agent_slug = name.lower().replace(" ", "-")
839
1602
  hb_ok = _step_heartbeat(home_path, agent_slug, fingerprint)
840
1603
 
841
1604
  # -----------------------------------------------------------------------
842
- # Step 11: Crush Terminal AI Client
1605
+ # Step 13: Crush Terminal AI Client
843
1606
  # -----------------------------------------------------------------------
844
- _step_header(11, "Crush Terminal AI")
1607
+ _step_header(13, "Crush Terminal AI")
845
1608
  crush_ok = _step_crush(home_path)
846
1609
 
847
1610
  # -----------------------------------------------------------------------
848
- # Step 12: Coordination Board
1611
+ # Step 14: Coordination Board
849
1612
  # -----------------------------------------------------------------------
850
- _step_header(12, "Coordination Board")
1613
+ _step_header(14, "Coordination Board")
851
1614
  open_task_count = _step_board(home_path, name)
852
1615
 
853
1616
  # -----------------------------------------------------------------------
854
- # Step 13: Systemd Service (optional)
1617
+ # Step 15: Auto-Start Service (systemd on Linux, launchd on macOS)
1618
+ # -----------------------------------------------------------------------
1619
+ _step_header(15, "Auto-Start Service")
1620
+ service_ok = _step_autostart_service(agent_name=agent_slug)
1621
+
1622
+ # -----------------------------------------------------------------------
1623
+ # Step 16: Shell Profile (~/.bashrc)
855
1624
  # -----------------------------------------------------------------------
856
- _step_header(13, "Systemd Service")
857
- systemd_ok = _step_systemd_service()
1625
+ _step_header(16, "Shell Profile")
1626
+ profile_ok = _step_shell_profile(home_path, name, agent_slug)
858
1627
 
859
1628
  # -----------------------------------------------------------------------
860
- # Post-wizard: Doctor Diagnostics
1629
+ # Post-wizard: Doctor Diagnostics (non-fatal)
861
1630
  # -----------------------------------------------------------------------
862
1631
  console.print(f"\n [bold cyan]Doctor Diagnostics[/]\n")
863
1632
  doctor_report = _step_doctor_check(home_path)
864
1633
 
865
1634
  # -----------------------------------------------------------------------
866
- # Post-wizard: Consciousness Test (optional)
1635
+ # Post-wizard: Consciousness Test (optional, defaults to skip)
867
1636
  # -----------------------------------------------------------------------
868
1637
  console.print(f"\n [bold cyan]Consciousness Test[/]\n")
869
1638
  consciousness_test_ok = _step_test_consciousness(home_path)
@@ -877,8 +1646,40 @@ def run_onboard(home: Optional[str] = None) -> None:
877
1646
  soul = load_soul()
878
1647
  if soul and soul.boot_message:
879
1648
  boot_message = soul.boot_message
880
- except Exception:
881
- pass
1649
+ except Exception as exc:
1650
+ logger.debug("Failed to load soul boot message, using default: %s", exc)
1651
+
1652
+ # -----------------------------------------------------------------------
1653
+ # Write global CLAUDE.md and register Claude Code hooks
1654
+ # -----------------------------------------------------------------------
1655
+ # Write global CLAUDE.md from bundled skeleton template
1656
+ try:
1657
+ from .cli.setup import _write_global_claude_md
1658
+ _write_global_claude_md(home_path, name)
1659
+ _ok("~/.claude/CLAUDE.md written")
1660
+ except Exception as exc:
1661
+ _warn(f"Could not write CLAUDE.md: {exc}")
1662
+
1663
+ # Write ~/.claude/settings.json with SK hooks (merge with existing)
1664
+ try:
1665
+ from .cli.setup import _write_claude_settings
1666
+ settings_path = _write_claude_settings(merge=True)
1667
+ if settings_path:
1668
+ _ok(f"~/.claude/settings.json updated ({settings_path})")
1669
+ else:
1670
+ _info("claude settings: skipped (skmemory not installed or template missing)")
1671
+ except Exception as exc:
1672
+ _warn(f"Could not write claude settings: {exc}")
1673
+
1674
+ # Register Claude Code hooks via skmemory (adds any hooks not yet in settings.json)
1675
+ try:
1676
+ from skmemory.register import register_hooks
1677
+ actions = register_hooks()
1678
+ _ok(f"Claude Code hooks registered ({', '.join(f'{k}={v}' for k, v in actions.items())})")
1679
+ except ImportError:
1680
+ _info("skmemory hooks: skipped (skmemory.register not available)")
1681
+ except Exception as exc:
1682
+ _warn(f"Hook registration: {exc}")
882
1683
 
883
1684
  # -----------------------------------------------------------------------
884
1685
  # Summary table
@@ -895,23 +1696,59 @@ def run_onboard(home: Optional[str] = None) -> None:
895
1696
  "[green]OK[/]" if all_prereqs_ok else "[yellow]PARTIAL[/]",
896
1697
  "python + pip" + (" + ollama" if prereqs.get("ollama") else " (no ollama)"),
897
1698
  )
898
- summary.add_row("Identity", identity_status, fingerprint[:20] + "…" if len(fingerprint) > 20 else fingerprint)
1699
+ pillars_installed = sum(1 for v in pillar_results.values() if v)
1700
+ pillars_total = len(pillar_results)
1701
+ summary.add_row(
1702
+ "Pillar Packages",
1703
+ "[green]ALL[/]" if pillars_installed == pillars_total else f"[yellow]{pillars_installed}/{pillars_total}[/]",
1704
+ f"{pillars_installed}/{pillars_total} installed",
1705
+ )
1706
+ if operator_name:
1707
+ summary.add_row(
1708
+ "Operator",
1709
+ "[green]ACTIVE[/]",
1710
+ f"{operator_name} ({operator_fingerprint[:16]}…)" if operator_fingerprint else operator_name,
1711
+ )
1712
+ summary.add_row("Identity", identity_status, f"{name} — {fingerprint[:16]}…" if len(fingerprint) > 16 else fingerprint)
1713
+ ollama_model_name = ollama_result.get("model", "llama3.2")
1714
+ ollama_host_display = ollama_result.get("host", "http://localhost:11434")
899
1715
  summary.add_row(
900
1716
  "Ollama Models",
901
1717
  "[green]READY[/]" if ollama_ok else "[yellow]SKIPPED[/]",
902
- "llama3.2" if ollama_ok else "pull later: ollama pull llama3.2",
1718
+ f"{ollama_model_name} @ {ollama_host_display}" if ollama_ok else f"pull later: ollama pull {ollama_model_name}",
903
1719
  )
904
1720
  config_status = "[green]ACTIVE[/]" if (consciousness_ok and profiles_ok) else "[yellow]PARTIAL[/]"
905
1721
  summary.add_row("Config Files", config_status, "consciousness.yaml + model_profiles.yaml")
906
1722
  summary.add_row("Soul", "[green]ACTIVE[/]", title)
907
1723
  summary.add_row("Memory", "[green]ACTIVE[/]", f"{seed_count} seed(s)")
1724
+ imported_count = import_result.get("imported_count", 0)
1725
+ imported_sources = import_result.get("sources", [])
1726
+ if imported_count > 0:
1727
+ summary.add_row(
1728
+ "Import Sources",
1729
+ "[green]IMPORTED[/]",
1730
+ f"{imported_count} files from {', '.join(imported_sources)}",
1731
+ )
1732
+ else:
1733
+ summary.add_row("Import Sources", "[dim]SKIPPED[/]", "starting fresh")
908
1734
  summary.add_row("Ritual", "[green]DONE[/]", "rehydration complete")
909
1735
  summary.add_row("Trust", trust_status, "FEB chain verified")
910
1736
  summary.add_row("Mesh", "[green]ACTIVE[/]" if mesh_ok else "[yellow]MISSING[/]", "syncthing" if mesh_ok else "install syncthing")
911
1737
  summary.add_row("Heartbeat", "[green]ACTIVE[/]" if hb_ok else "[yellow]FAILED[/]", f"{agent_slug}.json" if hb_ok else "see above")
912
1738
  summary.add_row("Crush AI", "[green]READY[/]" if crush_ok else "[yellow]CONFIG ONLY[/]", "~/.config/crush/crush.json")
913
1739
  summary.add_row("Board", "[green]ACTIVE[/]", f"{open_task_count} open tasks")
914
- summary.add_row("Systemd", "[green]INSTALLED[/]" if systemd_ok else "[dim]OPTIONAL[/]", "skcapstone.service" if systemd_ok else "skcapstone systemd install")
1740
+ import platform as _plat
1741
+ _svc_type = "launchd" if _plat.system() == "Darwin" else "systemd"
1742
+ summary.add_row(
1743
+ "Auto-Start",
1744
+ "[green]INSTALLED[/]" if service_ok else "[dim]OPTIONAL[/]",
1745
+ f"{_svc_type} services" if service_ok else f"skcapstone daemon install",
1746
+ )
1747
+ summary.add_row(
1748
+ "Shell Profile",
1749
+ "[green]ACTIVE[/]" if profile_ok else "[dim]SKIPPED[/]",
1750
+ f"SKCAPSTONE_AGENT={agent_slug}" if profile_ok else "set manually in ~/.bashrc",
1751
+ )
915
1752
  doctor_status = "[green]ALL PASSED[/]" if doctor_report.all_passed else f"[yellow]{doctor_report.failed_count} failed[/]"
916
1753
  summary.add_row("Doctor", doctor_status, f"{doctor_report.passed_count}/{doctor_report.total_count} checks")
917
1754
  summary.add_row(
@@ -923,9 +1760,46 @@ def run_onboard(home: Optional[str] = None) -> None:
923
1760
  console.print(summary)
924
1761
  console.print()
925
1762
 
1763
+ # -----------------------------------------------------------------------
1764
+ # Reconfigure Guide
1765
+ # -----------------------------------------------------------------------
1766
+ console.print()
1767
+ console.print(
1768
+ Panel(
1769
+ "[bold cyan]Reinstall or Reconfigure Any Component[/]\n\n"
1770
+ "[bold]Pillars[/] (install missing packages)\n"
1771
+ " pip install capauth skcomms skchat-sovereign skseed sksecurity pgpy\n"
1772
+ " pip install skcapstone[all] — install everything at once\n\n"
1773
+ "[bold]Identity[/] (regenerate PGP keys)\n"
1774
+ " capauth init --name YourName --email you@example.com\n\n"
1775
+ "[bold]Ollama[/] (change model or host)\n"
1776
+ " ollama pull <model> — pull a different model\n"
1777
+ " Edit: ~/.skcapstone/config/consciousness.yaml\n"
1778
+ " ollama_host: http://<ip>:11434 — point to remote Ollama\n"
1779
+ " ollama_model: qwen3:14b — change default model\n\n"
1780
+ "[bold]Soul[/] (update your blueprint)\n"
1781
+ " skcapstone soul edit\n\n"
1782
+ "[bold]Service[/] (auto-start daemon)\n"
1783
+ " skcapstone daemon install — install systemd/launchd service\n"
1784
+ " skcapstone daemon uninstall — remove service\n\n"
1785
+ "[bold]Trust[/] (add FEB files)\n"
1786
+ " Place .feb files in ~/.skcapstone/trust/febs/\n\n"
1787
+ "[bold]Mesh[/] (P2P sync)\n"
1788
+ " sudo apt install syncthing — install Syncthing\n\n"
1789
+ "[bold]Shell Profile[/] (update default agent)\n"
1790
+ " Edit the [dim]# --- SKCapstone profile ---[/] block in ~/.bashrc\n"
1791
+ " Or re-run: skcapstone --agent <name> init\n\n"
1792
+ "[bold]Full Re-onboard[/]\n"
1793
+ " skcapstone --agent <name> init — run this wizard again",
1794
+ title="Reconfigure Guide",
1795
+ border_style="bright_blue",
1796
+ )
1797
+ )
1798
+
926
1799
  # -----------------------------------------------------------------------
927
1800
  # Celebrate
928
1801
  # -----------------------------------------------------------------------
1802
+ console.print()
929
1803
  console.print(
930
1804
  Panel(
931
1805
  f"[bold green]Welcome to the Pengu Nation, {name}.[/]\n\n"