@smilintux/skcapstone 0.10.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 (279) 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 +123 -30
  18. package/docs/DREAMING.md +70 -0
  19. package/docs/GETTING_STARTED.md +7 -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.skcomm-heartbeat.plist → com.skcapstone.skcomms-heartbeat.plist} +4 -4
  33. package/launchd/{com.skcapstone.skcomm-queue-drain.plist → com.skcapstone.skcomms-queue-drain.plist} +4 -4
  34. package/launchd/install-launchd.sh +6 -6
  35. package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/src/index.ts +3 -2
  36. package/package.json +1 -1
  37. package/pyproject.toml +16 -10
  38. package/scripts/archive-sessions.sh +7 -0
  39. package/scripts/check-updates.py +4 -4
  40. package/scripts/install-bundle.sh +8 -8
  41. package/scripts/install.ps1 +12 -11
  42. package/scripts/install.sh +159 -5
  43. package/scripts/model-fallback-monitor.sh +102 -0
  44. package/scripts/nvidia-proxy.mjs +78 -26
  45. package/scripts/refresh-anthropic-token.sh +172 -0
  46. package/scripts/release.sh +98 -0
  47. package/scripts/session-to-memory.py +219 -0
  48. package/scripts/skgateway.mjs +3 -3
  49. package/scripts/telegram-catchup-all.sh +12 -1
  50. package/scripts/verify_install.sh +2 -2
  51. package/scripts/wargov-ufo-capture/README.md +43 -0
  52. package/scripts/wargov-ufo-capture/cdp_capture_release2.py +273 -0
  53. package/scripts/wargov-ufo-capture/cdp_capture_splc_doj.py +246 -0
  54. package/scripts/wargov-ufo-capture/cdp_finish.py +271 -0
  55. package/scripts/wargov-ufo-capture/cdp_probe.py +188 -0
  56. package/scripts/wargov-ufo-capture/cdp_splc_pressrelease.py +101 -0
  57. package/scripts/wargov-ufo-capture/parse_csv.py +95 -0
  58. package/scripts/wargov-ufo-capture/pull_dvids.sh +107 -0
  59. package/scripts/watch-anthropic-token.sh +212 -0
  60. package/scripts/windows/install-tasks.ps1 +7 -7
  61. package/scripts/windows/skcapstone-task.xml +1 -1
  62. package/src/skcapstone/__init__.py +45 -3
  63. package/src/skcapstone/_cli_monolith.py +20 -15
  64. package/src/skcapstone/activity.py +5 -1
  65. package/src/skcapstone/agent_card.py +3 -2
  66. package/src/skcapstone/api.py +41 -40
  67. package/src/skcapstone/auction.py +14 -11
  68. package/src/skcapstone/backup.py +2 -1
  69. package/src/skcapstone/blueprint_registry.py +4 -3
  70. package/src/skcapstone/brain_first.py +238 -0
  71. package/src/skcapstone/changelog.py +1 -1
  72. package/src/skcapstone/chat.py +22 -17
  73. package/src/skcapstone/cli/__init__.py +9 -1
  74. package/src/skcapstone/cli/_common.py +1 -0
  75. package/src/skcapstone/cli/agents_spawner.py +5 -2
  76. package/src/skcapstone/cli/alerts.py +25 -4
  77. package/src/skcapstone/cli/bench.py +15 -15
  78. package/src/skcapstone/cli/chat.py +7 -4
  79. package/src/skcapstone/cli/consciousness.py +5 -2
  80. package/src/skcapstone/cli/context_cmd.py +18 -4
  81. package/src/skcapstone/cli/daemon.py +11 -7
  82. package/src/skcapstone/cli/gtd.py +26 -1
  83. package/src/skcapstone/cli/housekeeping.py +3 -3
  84. package/src/skcapstone/cli/identity_cmd.py +378 -0
  85. package/src/skcapstone/cli/joule_cmd.py +7 -3
  86. package/src/skcapstone/cli/memory.py +8 -6
  87. package/src/skcapstone/cli/peers_dir.py +1 -1
  88. package/src/skcapstone/cli/register_cmd.py +29 -3
  89. package/src/skcapstone/cli/scheduler_cmd.py +167 -0
  90. package/src/skcapstone/cli/session.py +25 -0
  91. package/src/skcapstone/cli/setup.py +96 -29
  92. package/src/skcapstone/cli/shell_cmd.py +53 -1
  93. package/src/skcapstone/cli/skills_cmd.py +2 -2
  94. package/src/skcapstone/cli/soul.py +8 -5
  95. package/src/skcapstone/cli/status.py +37 -11
  96. package/src/skcapstone/cli/telegram.py +21 -0
  97. package/src/skcapstone/cli/test_cmd.py +5 -5
  98. package/src/skcapstone/cli/test_connection.py +2 -2
  99. package/src/skcapstone/cli/upgrade_cmd.py +23 -14
  100. package/src/skcapstone/cli/version_cmd.py +1 -1
  101. package/src/skcapstone/cli/watch_cmd.py +9 -6
  102. package/src/skcapstone/cloud9_bridge.py +14 -14
  103. package/src/skcapstone/codex_setup.py +255 -0
  104. package/src/skcapstone/config_validator.py +7 -4
  105. package/src/skcapstone/consciousness_config.py +5 -1
  106. package/src/skcapstone/consciousness_loop.py +313 -273
  107. package/src/skcapstone/context_loader.py +121 -0
  108. package/src/skcapstone/coord_federation.py +2 -1
  109. package/src/skcapstone/coordination.py +23 -6
  110. package/src/skcapstone/crush_integration.py +2 -1
  111. package/src/skcapstone/daemon.py +132 -77
  112. package/src/skcapstone/dashboard.py +10 -10
  113. package/src/skcapstone/data/sk-agent-picker.sh +421 -0
  114. package/src/skcapstone/data/systemd/skcapstone-api.socket +9 -0
  115. package/src/skcapstone/data/systemd/skcapstone-memory-compress.service +18 -0
  116. package/src/skcapstone/data/systemd/skcapstone-memory-compress.timer +11 -0
  117. package/src/skcapstone/data/systemd/skcapstone.service +37 -0
  118. package/src/skcapstone/data/systemd/skcapstone@.service +50 -0
  119. package/src/skcapstone/data/systemd/skcomms-heartbeat.service +18 -0
  120. package/{systemd/skcomm-heartbeat.timer → src/skcapstone/data/systemd/skcomms-heartbeat.timer} +2 -2
  121. package/src/skcapstone/data/systemd/skcomms-queue-drain.service +17 -0
  122. package/{systemd/skcomm-queue-drain.timer → src/skcapstone/data/systemd/skcomms-queue-drain.timer} +2 -2
  123. package/src/skcapstone/defaults/claude/CLAUDE.md +67 -0
  124. package/src/skcapstone/defaults/claude/settings.json +74 -0
  125. package/src/skcapstone/defaults/lumina/config/claude-hooks.md +57 -0
  126. package/src/skcapstone/defaults/lumina/config/skgraph.yaml +55 -10
  127. package/src/skcapstone/defaults/lumina/config/skmemory.yaml +79 -13
  128. package/src/skcapstone/defaults/lumina/config/skvector.yaml +60 -9
  129. package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +2 -2
  130. package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +2 -2
  131. package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +9 -9
  132. package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +2 -2
  133. package/src/skcapstone/defaults/unhinged.json +13 -0
  134. package/src/skcapstone/discovery.py +43 -20
  135. package/src/skcapstone/doctor.py +941 -22
  136. package/src/skcapstone/dreaming.py +1183 -109
  137. package/src/skcapstone/emotion_tracker.py +2 -2
  138. package/src/skcapstone/export.py +4 -3
  139. package/src/skcapstone/fuse_mount.py +14 -12
  140. package/src/skcapstone/gui_installer.py +2 -2
  141. package/src/skcapstone/heartbeat.py +1 -1
  142. package/src/skcapstone/housekeeping.py +14 -14
  143. package/src/skcapstone/install_wizard.py +209 -7
  144. package/src/skcapstone/itil.py +13 -4
  145. package/src/skcapstone/kms_scheduler.py +10 -8
  146. package/src/skcapstone/launchd.py +19 -19
  147. package/src/skcapstone/mcp_launcher.py +15 -1
  148. package/src/skcapstone/mcp_server.py +83 -49
  149. package/src/skcapstone/mcp_tools/__init__.py +2 -0
  150. package/src/skcapstone/mcp_tools/_helpers.py +2 -2
  151. package/src/skcapstone/mcp_tools/ansible_tools.py +7 -4
  152. package/src/skcapstone/mcp_tools/brain_first_tools.py +90 -0
  153. package/src/skcapstone/mcp_tools/capauth_tools.py +7 -4
  154. package/src/skcapstone/mcp_tools/comm_tools.py +10 -10
  155. package/src/skcapstone/mcp_tools/coord_tools.py +8 -4
  156. package/src/skcapstone/mcp_tools/did_tools.py +11 -8
  157. package/src/skcapstone/mcp_tools/gtd_tools.py +4 -4
  158. package/src/skcapstone/mcp_tools/memory_tools.py +6 -2
  159. package/src/skcapstone/mcp_tools/notification_tools.py +22 -6
  160. package/src/skcapstone/mcp_tools/{skcomm_tools.py → skcomms_tools.py} +14 -14
  161. package/src/skcapstone/mcp_tools/soul_tools.py +8 -2
  162. package/src/skcapstone/mdns_discovery.py +2 -2
  163. package/src/skcapstone/memory_curator.py +1 -1
  164. package/src/skcapstone/memory_engine.py +10 -3
  165. package/src/skcapstone/metrics.py +30 -16
  166. package/src/skcapstone/migrate_memories.py +4 -3
  167. package/src/skcapstone/migrate_multi_agent.py +8 -7
  168. package/src/skcapstone/models.py +47 -5
  169. package/src/skcapstone/notifications.py +42 -18
  170. package/src/skcapstone/onboard.py +875 -121
  171. package/src/skcapstone/operator_link.py +170 -0
  172. package/src/skcapstone/peer_directory.py +4 -4
  173. package/src/skcapstone/peers.py +19 -19
  174. package/src/skcapstone/pillars/__init__.py +7 -5
  175. package/src/skcapstone/pillars/consciousness.py +191 -0
  176. package/src/skcapstone/pillars/identity.py +51 -7
  177. package/src/skcapstone/pillars/memory.py +9 -3
  178. package/src/skcapstone/pillars/sync.py +2 -2
  179. package/src/skcapstone/preflight.py +3 -3
  180. package/src/skcapstone/providers/docker.py +28 -28
  181. package/src/skcapstone/register.py +6 -6
  182. package/src/skcapstone/registry_client.py +5 -4
  183. package/src/skcapstone/runtime.py +14 -3
  184. package/src/skcapstone/scheduled_tasks.py +254 -19
  185. package/src/skcapstone/scheduler_jobs.py +456 -0
  186. package/src/skcapstone/scheduler_runner.py +239 -0
  187. package/src/skcapstone/scheduler_state.py +162 -0
  188. package/src/skcapstone/sdk.py +310 -0
  189. package/src/skcapstone/service_health.py +279 -39
  190. package/src/skcapstone/session_briefing.py +108 -0
  191. package/src/skcapstone/session_capture.py +1 -1
  192. package/src/skcapstone/shell.py +7 -1
  193. package/src/skcapstone/soul.py +3 -1
  194. package/src/skcapstone/soul_switch.py +3 -1
  195. package/src/skcapstone/summary.py +6 -6
  196. package/src/skcapstone/sync_engine.py +15 -15
  197. package/src/skcapstone/sync_watcher.py +2 -2
  198. package/src/skcapstone/systemd.py +55 -21
  199. package/src/skcapstone/team_comms.py +8 -8
  200. package/src/skcapstone/team_engine.py +1 -1
  201. package/src/skcapstone/testrunner.py +3 -3
  202. package/src/skcapstone/trust_graph.py +40 -5
  203. package/src/skcapstone/unified_search.py +15 -6
  204. package/src/skcapstone/uninstall_wizard.py +11 -3
  205. package/src/skcapstone/version_check.py +8 -4
  206. package/src/skcapstone/warmth_anchor.py +4 -2
  207. package/src/skcapstone/whoami.py +4 -4
  208. package/systemd/skcapstone.service +4 -6
  209. package/systemd/skcapstone@.service +7 -8
  210. package/systemd/skcomms-heartbeat.service +21 -0
  211. package/systemd/skcomms-heartbeat.timer +12 -0
  212. package/systemd/skcomms-queue-drain.service +17 -0
  213. package/systemd/skcomms-queue-drain.timer +12 -0
  214. package/tests/conftest.py +39 -0
  215. package/tests/integration/test_consciousness_e2e.py +39 -39
  216. package/tests/test_agent_card.py +1 -1
  217. package/tests/test_agent_home_scaffold.py +34 -0
  218. package/tests/test_alerts_consumer_topics.py +27 -0
  219. package/tests/test_backup.py +2 -1
  220. package/tests/test_chat.py +6 -6
  221. package/tests/test_claude_md.py +2 -2
  222. package/tests/test_cli_skills.py +10 -10
  223. package/tests/test_cli_test_cmd.py +4 -4
  224. package/tests/test_cli_test_connection.py +1 -1
  225. package/tests/test_cloud9_bridge.py +6 -6
  226. package/tests/test_consciousness_e2e.py +1 -1
  227. package/tests/test_consciousness_loop.py +10 -10
  228. package/tests/test_coordination.py +25 -0
  229. package/tests/test_cross_package.py +21 -21
  230. package/tests/test_daemon.py +4 -4
  231. package/tests/test_daemon_shutdown.py +1 -1
  232. package/tests/test_docker_provider.py +29 -29
  233. package/tests/test_doctor.py +400 -0
  234. package/tests/test_doctor_skscheduler.py +50 -0
  235. package/tests/test_dreaming_engine.py +147 -0
  236. package/tests/test_dreaming_gtd_capture.py +35 -0
  237. package/tests/test_e2e_automated.py +8 -5
  238. package/tests/test_fuse_mount.py +10 -10
  239. package/tests/test_gtd_brief.py +46 -0
  240. package/tests/test_gtd_malformed_tolerance.py +31 -0
  241. package/tests/test_housekeeping.py +15 -15
  242. package/tests/test_identity_migrate.py +251 -0
  243. package/tests/test_integration_backbone.py +598 -0
  244. package/tests/test_itil_gtd_lifecycle.py +37 -0
  245. package/tests/test_jobs_dropins.py +84 -0
  246. package/tests/test_mcp_server.py +82 -37
  247. package/tests/test_models.py +48 -4
  248. package/tests/test_multi_agent.py +31 -29
  249. package/tests/test_notifications.py +122 -32
  250. package/tests/test_onboard.py +63 -75
  251. package/tests/test_operator_link.py +78 -0
  252. package/tests/test_peers.py +14 -14
  253. package/tests/test_pillars.py +98 -0
  254. package/tests/test_preflight.py +3 -3
  255. package/tests/test_runtime.py +21 -0
  256. package/tests/test_scheduled_tasks.py +11 -6
  257. package/tests/test_scheduler_cli.py +47 -0
  258. package/tests/test_scheduler_features.py +133 -0
  259. package/tests/test_scheduler_integration.py +87 -0
  260. package/tests/test_scheduler_jobs.py +155 -0
  261. package/tests/test_scheduler_runner.py +64 -0
  262. package/tests/test_scheduler_state.py +57 -0
  263. package/tests/test_sdk.py +70 -0
  264. package/tests/test_service_health_incidents.py +34 -0
  265. package/tests/test_service_registry.py +52 -0
  266. package/tests/test_session_briefing.py +130 -0
  267. package/tests/test_snapshots.py +4 -4
  268. package/tests/test_sync_pipeline.py +26 -26
  269. package/tests/test_team_comms.py +2 -2
  270. package/tests/test_testrunner.py +2 -2
  271. package/tests/test_trust_graph.py +18 -0
  272. package/tests/test_unified_search.py +2 -2
  273. package/tests/test_version_check.py +10 -0
  274. package/tests/test_version_cmd.py +8 -8
  275. package/tests/test_whoami.py +1 -1
  276. package/systemd/skcomm-heartbeat.service +0 -18
  277. package/systemd/skcomm-queue-drain.service +0 -17
  278. /package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/package.json +0 -0
  279. /package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/src/openclaw.plugin.json +0 -0
@@ -21,10 +21,13 @@ from __future__ import annotations
21
21
  import json
22
22
  import logging
23
23
  import os
24
+ import re
24
25
  import socket
25
26
  import time
26
27
  import urllib.error
28
+ from pathlib import Path
27
29
  import urllib.request
30
+ from urllib.parse import urlparse
28
31
  from typing import Any
29
32
 
30
33
  logger = logging.getLogger("skcapstone.service_health")
@@ -32,6 +35,86 @@ logger = logging.getLogger("skcapstone.service_health")
32
35
  # Default timeout per service check (seconds).
33
36
  CHECK_TIMEOUT = 3
34
37
 
38
+ # Hostname tag used to attribute one-time state-transition notes (e.g. a
39
+ # service recovering) to the reporting node. Recurring "still down" notes are
40
+ # intentionally never written — see _create_incident_for_down_service and
41
+ # prb-7810b08e for why that churn caused Syncthing conflicts.
42
+ _HOSTNAME = socket.gethostname()
43
+
44
+
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # Per-agent YAML config fallback
48
+ # ---------------------------------------------------------------------------
49
+
50
+ def _load_agent_yaml(config_name: str, agent: str | None = None) -> dict:
51
+ """Load ~/.skcapstone/agents/<agent>/config/<config_name>.yaml.
52
+
53
+ Falls back gracefully when the file or yaml lib is unavailable. Used by
54
+ check_all_services() so the laptop's jarvis daemon can read the same
55
+ correctly-populated skvector.yaml / skgraph.yaml that skmemory uses,
56
+ instead of probing localhost defaults that don't exist here.
57
+ """
58
+ if not agent:
59
+ agent = (
60
+ os.environ.get("SKAGENT")
61
+ or os.environ.get("SKCAPSTONE_AGENT")
62
+ or os.environ.get("SKMEMORY_AGENT")
63
+ or "lumina"
64
+ )
65
+ path = os.path.expanduser(f"~/.skcapstone/agents/{agent}/config/{config_name}.yaml")
66
+ if not os.path.exists(path):
67
+ return {}
68
+ try:
69
+ import yaml # type: ignore
70
+ with open(path) as f:
71
+ data = yaml.safe_load(f) or {}
72
+ return data if isinstance(data, dict) else {}
73
+ except Exception as exc:
74
+ logger.debug("Failed to load %s: %s", path, exc)
75
+ return {}
76
+
77
+
78
+
79
+ def _load_syncthing_config() -> tuple[str | None, str | None]:
80
+ """Read ~/.config/syncthing/config.xml to get GUI URL + API key.
81
+
82
+ Returns (url, api_key) tuple — either may be None if the config can't
83
+ be parsed. Uses regex (no XML lib dep) since we only need 2 small fields.
84
+ """
85
+ candidates = [
86
+ Path.home() / ".config" / "syncthing" / "config.xml",
87
+ Path.home() / ".local" / "state" / "syncthing" / "config.xml",
88
+ ]
89
+ cfg_path = next((p for p in candidates if p.exists()), None)
90
+ if cfg_path is None:
91
+ return None, None
92
+ try:
93
+ text = cfg_path.read_text()
94
+ except Exception:
95
+ return None, None
96
+
97
+ # Find <gui ...> ... <address>HOST:PORT</address> ... </gui>
98
+ gui_match = re.search(
99
+ r"<gui[^>]*>(.*?)</gui>", text, re.S | re.I
100
+ )
101
+ addr_in_gui = None
102
+ if gui_match:
103
+ body = gui_match.group(1)
104
+ addr_match = re.search(r"<address>\s*([^<]+?)\s*</address>", body, re.I)
105
+ if addr_match:
106
+ addr_in_gui = addr_match.group(1).strip()
107
+
108
+ api_match = re.search(r"<apikey>\s*([^<]+?)\s*</apikey>", text, re.I)
109
+ api_key = api_match.group(1).strip() if api_match else None
110
+
111
+ if not addr_in_gui:
112
+ return None, api_key
113
+ # GUI tls flag
114
+ tls = bool(gui_match and ("tls=\"true\"" in gui_match.group(0) or "tls='true'" in gui_match.group(0)))
115
+ proto = "https" if tls else "http"
116
+ return f"{proto}://{addr_in_gui}", api_key
117
+
35
118
 
36
119
  # ---------------------------------------------------------------------------
37
120
  # Individual service checks
@@ -76,8 +159,8 @@ def _http_check(
76
159
  try:
77
160
  body = json.loads(resp.read().decode("utf-8"))
78
161
  result["version"] = body.get(version_key)
79
- except Exception:
80
- pass
162
+ except Exception as exc:
163
+ logger.warning("Failed to parse version from service health response: %s", exc)
81
164
  except urllib.error.HTTPError as exc:
82
165
  latency = (time.monotonic() - t0) * 1000
83
166
  result["latency_ms"] = round(latency, 1)
@@ -130,6 +213,34 @@ def _tcp_check(name: str, host: str, port: int) -> dict[str, Any]:
130
213
  return result
131
214
 
132
215
 
216
+ def _pid_check(name: str, pid_path: Path) -> dict[str, Any]:
217
+ """Check a local daemon that advertises health through a PID file."""
218
+ result: dict[str, Any] = {
219
+ "name": name,
220
+ "url": f"pid://{pid_path}",
221
+ "status": "unknown",
222
+ "latency_ms": 0,
223
+ "version": None,
224
+ "error": None,
225
+ }
226
+ t0 = time.monotonic()
227
+ try:
228
+ pid = int(pid_path.read_text(encoding="utf-8").strip())
229
+ os.kill(pid, 0)
230
+ result["status"] = "up"
231
+ except FileNotFoundError:
232
+ result["status"] = "down"
233
+ result["error"] = "PID file missing"
234
+ except ProcessLookupError:
235
+ result["status"] = "down"
236
+ result["error"] = "process not found"
237
+ except Exception as exc:
238
+ result["status"] = "down"
239
+ result["error"] = str(exc)[:200]
240
+ result["latency_ms"] = round((time.monotonic() - t0) * 1000, 1)
241
+ return result
242
+
243
+
133
244
  # ---------------------------------------------------------------------------
134
245
  # Aggregate check
135
246
  # ---------------------------------------------------------------------------
@@ -138,14 +249,22 @@ def _tcp_check(name: str, host: str, port: int) -> dict[str, Any]:
138
249
  def check_all_services() -> list[dict[str, Any]]:
139
250
  """Ping every known service and return a list of status dicts.
140
251
 
141
- Environment variables override default URLs:
142
- SKMEMORY_SKVECTOR_URL — Qdrant REST base (default http://localhost:6333)
143
- SKMEMORY_SKGRAPH_HOST — FalkorDB host (default localhost)
144
- SKMEMORY_SKGRAPH_PORT — FalkorDB port (default 6379)
145
- SYNCTHING_API_URL Syncthing REST (default http://localhost:8384)
146
- SYNCTHING_API_KEY Syncthing API key (optional)
147
- SKCAPSTONE_DAEMON_URL — Daemon HTTP base (default http://localhost:9383)
148
- SKCHAT_DAEMON_URL — SKChat daemon (default http://localhost:9385)
252
+ Environment variables override default URLs (set any to "disabled" to skip):
253
+ SKMEMORY_SKVECTOR_URL — Qdrant REST base (default: read from
254
+ ~/.skcapstone/agents/<agent>/config/skvector.yaml,
255
+ else http://localhost:6333)
256
+ SKMEMORY_SKVECTOR_API_KEY Qdrant API key (default: from skvector.yaml)
257
+ SKMEMORY_SKGRAPH_HOST FalkorDB host (default: read from
258
+ ~/.skcapstone/agents/<agent>/config/skgraph.yaml,
259
+ else localhost)
260
+ SKMEMORY_SKGRAPH_PORT — FalkorDB port (default: from skgraph.yaml,
261
+ else 6379)
262
+ SYNCTHING_API_URL — Syncthing REST (default: discovered from
263
+ ~/.config/syncthing/config.xml gui address,
264
+ else http://localhost:8384)
265
+ SYNCTHING_API_KEY — Syncthing API key (default: from config.xml)
266
+ SKCAPSTONE_DAEMON_URL — Daemon HTTP base (default http://localhost:9383)
267
+ SKCHAT_DAEMON_URL — SKChat daemon (default http://localhost:9385)
149
268
 
150
269
  Returns:
151
270
  List of dicts, each containing: name, url, status ("up"|"down"|"unknown"),
@@ -154,30 +273,85 @@ def check_all_services() -> list[dict[str, Any]]:
154
273
  results: list[dict[str, Any]] = []
155
274
 
156
275
  # -- SKVector (Qdrant) --------------------------------------------------
157
- qdrant_base = os.environ.get("SKMEMORY_SKVECTOR_URL", "http://localhost:6333")
158
- qdrant_url = qdrant_base.rstrip("/") + "/healthz"
159
- results.append(_http_check("skvector (Qdrant)", qdrant_url))
276
+ qdrant_base = os.environ.get("SKMEMORY_SKVECTOR_URL", "")
277
+ qdrant_api_key = os.environ.get("SKMEMORY_SKVECTOR_API_KEY", "")
278
+ # Fall back to per-agent skvector.yaml when env vars are absent
279
+ if not qdrant_base or not qdrant_api_key:
280
+ cfg = _load_agent_yaml("skvector")
281
+ if cfg.get("enabled", True):
282
+ if not qdrant_base:
283
+ if cfg.get("url"):
284
+ qdrant_base = str(cfg["url"])
285
+ else:
286
+ # Reconstruct URL from host/port/https
287
+ host = cfg.get("host", "localhost")
288
+ port = cfg.get("port", 6333)
289
+ proto = "https" if cfg.get("https") or int(port) == 443 else "http"
290
+ if int(port) in (80, 443):
291
+ qdrant_base = f"{proto}://{host}"
292
+ else:
293
+ qdrant_base = f"{proto}://{host}:{port}"
294
+ if not qdrant_api_key and cfg.get("api_key") and cfg["api_key"] != "CHANGE_ME":
295
+ qdrant_api_key = cfg["api_key"]
296
+ if not qdrant_base:
297
+ qdrant_base = "http://localhost:6333"
298
+ if qdrant_base.lower() != "disabled":
299
+ qdrant_url = qdrant_base.rstrip("/") + "/healthz"
300
+ qdrant_headers: dict[str, str] = {}
301
+ if qdrant_api_key:
302
+ qdrant_headers["api-key"] = qdrant_api_key
303
+ results.append(_http_check("skvector (Qdrant)", qdrant_url, headers=qdrant_headers))
160
304
 
161
305
  # -- SKGraph (FalkorDB) — TCP check on Redis protocol port ---------------
162
- graph_host = os.environ.get("SKMEMORY_SKGRAPH_HOST", "localhost")
163
- graph_port = int(os.environ.get("SKMEMORY_SKGRAPH_PORT", "6379"))
164
- results.append(_tcp_check("skgraph (FalkorDB)", graph_host, graph_port))
306
+ graph_host = os.environ.get("SKMEMORY_SKGRAPH_HOST", "")
307
+ graph_port_str = os.environ.get("SKMEMORY_SKGRAPH_PORT", "")
308
+ # Fall back to per-agent skgraph.yaml when env vars are absent
309
+ if not graph_host or not graph_port_str:
310
+ cfg = _load_agent_yaml("skgraph")
311
+ if cfg.get("enabled", True):
312
+ if cfg.get("url") and (not graph_host or not graph_port_str):
313
+ parsed = urlparse(str(cfg["url"]))
314
+ if not graph_host and parsed.hostname:
315
+ graph_host = parsed.hostname
316
+ if not graph_port_str and parsed.port:
317
+ graph_port_str = str(parsed.port)
318
+ if not graph_host and cfg.get("host"):
319
+ graph_host = str(cfg["host"])
320
+ if not graph_port_str and cfg.get("port"):
321
+ graph_port_str = str(cfg["port"])
322
+ if not graph_host:
323
+ graph_host = "localhost"
324
+ if not graph_port_str:
325
+ graph_port_str = "6379"
326
+ if graph_host.lower() != "disabled":
327
+ graph_port = int(graph_port_str)
328
+ results.append(_tcp_check("skgraph (FalkorDB)", graph_host, graph_port))
165
329
 
166
330
  # -- Syncthing -----------------------------------------------------------
167
- syncthing_base = os.environ.get("SYNCTHING_API_URL", "http://localhost:8384")
168
- syncthing_url = syncthing_base.rstrip("/") + "/rest/system/status"
169
- syncthing_headers: dict[str, str] = {}
331
+ syncthing_base = os.environ.get("SYNCTHING_API_URL", "")
170
332
  api_key = os.environ.get("SYNCTHING_API_KEY", "")
171
- if api_key:
172
- syncthing_headers["X-API-Key"] = api_key
173
- results.append(
174
- _http_check(
175
- "syncthing",
176
- syncthing_url,
177
- headers=syncthing_headers,
178
- version_key="version",
333
+ # Fall back to ~/.config/syncthing/config.xml discovery
334
+ if not syncthing_base or not api_key:
335
+ discovered_url, discovered_key = _load_syncthing_config()
336
+ if not syncthing_base and discovered_url:
337
+ syncthing_base = discovered_url
338
+ if not api_key and discovered_key:
339
+ api_key = discovered_key
340
+ if not syncthing_base:
341
+ syncthing_base = "http://localhost:8384"
342
+ if syncthing_base.lower() != "disabled":
343
+ syncthing_url = syncthing_base.rstrip("/") + "/rest/system/status"
344
+ syncthing_headers: dict[str, str] = {}
345
+ if api_key:
346
+ syncthing_headers["X-API-Key"] = api_key
347
+ results.append(
348
+ _http_check(
349
+ "syncthing",
350
+ syncthing_url,
351
+ headers=syncthing_headers,
352
+ version_key="version",
353
+ )
179
354
  )
180
- )
181
355
 
182
356
  # -- skcapstone daemon ---------------------------------------------------
183
357
  daemon_base = os.environ.get("SKCAPSTONE_DAEMON_URL", "http://localhost:9383")
@@ -185,13 +359,70 @@ def check_all_services() -> list[dict[str, Any]]:
185
359
  results.append(_http_check("skcapstone daemon", daemon_url))
186
360
 
187
361
  # -- skchat daemon -------------------------------------------------------
188
- chat_base = os.environ.get("SKCHAT_DAEMON_URL", "http://localhost:9385")
189
- chat_url = chat_base.rstrip("/") + "/health"
190
- results.append(_http_check("skchat daemon", chat_url))
362
+ chat_base = os.environ.get("SKCHAT_DAEMON_URL", "")
363
+ if not chat_base:
364
+ results.append(_pid_check("skchat daemon", Path.home() / ".skchat" / "daemon.pid"))
365
+ elif chat_base.lower() != "disabled":
366
+ chat_url = chat_base.rstrip("/") + "/health"
367
+ results.append(_http_check("skchat daemon", chat_url))
368
+
369
+ # -- self-registered services (~/.skcapstone/registry/*.json) ------------
370
+ # Services that called sdk.register_service() become discoverable here
371
+ # without being hardcoded above. Names already covered by a built-in
372
+ # check are skipped (built-in wins) so there are no duplicates.
373
+ known = {r["name"] for r in results}
374
+ for entry in _load_registry_entries():
375
+ name = entry.get("name")
376
+ if not name or name in known:
377
+ continue
378
+ health_url = entry.get("health_url")
379
+ pid_file = entry.get("pid_file")
380
+ if health_url and str(health_url).lower() != "disabled":
381
+ results.append(_http_check(name, str(health_url).rstrip("/")))
382
+ elif pid_file:
383
+ results.append(_pid_check(name, Path(pid_file).expanduser()))
384
+ else:
385
+ results.append({
386
+ "name": name, "url": None, "status": "unknown",
387
+ "latency_ms": None, "version": None,
388
+ "error": "registered without health_url or pid_file",
389
+ })
390
+ known.add(name)
191
391
 
192
392
  return results
193
393
 
194
394
 
395
+ def _load_registry_entries() -> list[dict[str, Any]]:
396
+ """Load service self-registration entries from the discovery registry.
397
+
398
+ Reads every ``<shared_home>/registry/*.json`` file written by
399
+ :func:`skcapstone.sdk.register_service`. Missing directory or malformed
400
+ files are skipped silently — discovery is best-effort.
401
+
402
+ Returns:
403
+ A list of registry entry dicts (each with at least a ``name`` key).
404
+ """
405
+ import json
406
+
407
+ try:
408
+ from . import shared_home
409
+
410
+ registry_dir = shared_home() / "registry"
411
+ except Exception:
412
+ return []
413
+
414
+ if not registry_dir.is_dir():
415
+ return []
416
+
417
+ entries: list[dict[str, Any]] = []
418
+ for path in sorted(registry_dir.glob("*.json")):
419
+ try:
420
+ entries.append(json.loads(path.read_text(encoding="utf-8")))
421
+ except (json.JSONDecodeError, OSError):
422
+ continue
423
+ return entries
424
+
425
+
195
426
  # ---------------------------------------------------------------------------
196
427
  # Scheduled-task factory
197
428
  # ---------------------------------------------------------------------------
@@ -209,18 +440,23 @@ def _create_incident_for_down_service(service_result: dict[str, Any]) -> None:
209
440
  from .itil import ITILManager
210
441
 
211
442
  svc_name = service_result["name"]
443
+ error_info = service_result.get("error") or "unreachable"
212
444
  mgr = ITILManager(os.path.expanduser(SHARED_ROOT))
213
445
 
214
- # Dedup: skip if there's already an open incident for this service
446
+ # Dedup: already tracked by an open incident do nothing.
447
+ # We deliberately do NOT append recurring "still down" notes. That
448
+ # read-modify-write churn on a Syncthing-synced incident file, from
449
+ # multiple nodes every health cycle, is exactly what produced the
450
+ # sync-conflicts and 80+-entry timelines tracked in prb-7810b08e.
451
+ # Outage duration is derivable from the incident's created_at; the
452
+ # recovery (down->up) edge is handled by _auto_resolve_recovered_service.
215
453
  existing = mgr.find_open_incident_for_service(svc_name)
216
454
  if existing:
217
455
  logger.debug(
218
- "Skipping incident creation for %s open incident %s exists",
456
+ "Service %s already tracked by incident %s; no note appended",
219
457
  svc_name, existing.id,
220
458
  )
221
459
  return
222
-
223
- error_info = service_result.get("error") or "unreachable"
224
460
  mgr.create_incident(
225
461
  title=f"{svc_name} down",
226
462
  severity="sev3",
@@ -258,10 +494,14 @@ def _auto_resolve_recovered_service(service_result: dict[str, Any]) -> None:
258
494
  logger.info("Auto-resolved sev4 incident %s for recovered service %s",
259
495
  existing.id, svc_name)
260
496
  else:
261
- mgr.update_incident(
262
- existing.id, "service_health",
263
- note=f"Service {svc_name} appears to be back up",
264
- )
497
+ # Skip if this host already noted recovery recently
498
+ last_notes = [e.get("note", "") for e in (existing.timeline or [])[-3:]]
499
+ host_tag = f"[{_HOSTNAME}]"
500
+ if not any(host_tag in n and "back up" in n for n in last_notes):
501
+ mgr.update_incident(
502
+ existing.id, "service_health",
503
+ note=f"[{_HOSTNAME}] Service {svc_name} appears to be back up",
504
+ )
265
505
  except Exception as exc:
266
506
  logger.debug("Failed to auto-resolve incident for %s: %s",
267
507
  service_result.get("name"), exc)
@@ -0,0 +1,108 @@
1
+ """Session briefing helpers for SKCapstone startup flows."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import subprocess
8
+ import sys
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from .context_loader import format_text, gather_context
14
+
15
+
16
+ DEFAULT_HAMMERTIME_ROOT = Path("/mnt/cloud/onedrive/projects/DAVE AI/hammerTime")
17
+
18
+
19
+ def _resolve_hammertime_root() -> Path:
20
+ """Resolve the HammerTime workspace root."""
21
+ return Path(os.environ.get("HAMMERTIME_ROOT", DEFAULT_HAMMERTIME_ROOT)).expanduser()
22
+
23
+
24
+ def load_hammertime_briefing(
25
+ *,
26
+ python_bin: str | None = None,
27
+ root: Path | None = None,
28
+ ) -> dict[str, Any] | None:
29
+ """Load the HammerTime case briefing if the repo is available."""
30
+ if os.environ.get("SK_INCLUDE_HAMMERTIME_BRIEFING", "1") == "0":
31
+ return None
32
+
33
+ hammer_root = root or _resolve_hammertime_root()
34
+ script = hammer_root / "scripts" / "case-briefing.py"
35
+ if not script.exists():
36
+ return None
37
+
38
+ try:
39
+ completed = subprocess.run(
40
+ [python_bin or sys.executable, str(script)],
41
+ check=True,
42
+ capture_output=True,
43
+ text=True,
44
+ )
45
+ except (OSError, subprocess.CalledProcessError):
46
+ return None
47
+
48
+ try:
49
+ payload = json.loads(completed.stdout)
50
+ except json.JSONDecodeError:
51
+ return None
52
+ return payload if isinstance(payload, dict) else None
53
+
54
+
55
+ def build_session_briefing(
56
+ home: Path,
57
+ *,
58
+ memory_limit: int = 10,
59
+ python_bin: str | None = None,
60
+ ) -> dict[str, Any]:
61
+ """Build a native session briefing payload."""
62
+ return {
63
+ "generated_at": datetime.now(timezone.utc).isoformat(),
64
+ "agent_home": str(home),
65
+ "skcapstone_context": gather_context(home, memory_limit=memory_limit),
66
+ "hammertime_briefing": load_hammertime_briefing(python_bin=python_bin),
67
+ }
68
+
69
+
70
+ def format_session_briefing_text(payload: dict[str, Any]) -> str:
71
+ """Render a human-readable session briefing."""
72
+ lines = [
73
+ "# SKCapstone Session Briefing",
74
+ "",
75
+ f"generated_at={payload.get('generated_at')}",
76
+ f"agent_home={payload.get('agent_home')}",
77
+ "",
78
+ "## skcapstone context",
79
+ format_text(payload["skcapstone_context"]).rstrip(),
80
+ ]
81
+
82
+ briefing = payload.get("hammertime_briefing")
83
+ if briefing:
84
+ top = briefing.get("top_priority") or {}
85
+ lines.extend(
86
+ [
87
+ "",
88
+ "## hammertime briefing",
89
+ f"- alert_count: {briefing.get('alert_count', 0)}",
90
+ f"- queue_size: {briefing.get('summary', {}).get('queue_size', 0)}",
91
+ ]
92
+ )
93
+ if top:
94
+ lines.extend(
95
+ [
96
+ f"- do_this_now_incident: {top.get('incident_id')} ({top.get('problem_slug')})",
97
+ f"- do_this_now_action: {top.get('action')}",
98
+ f"- do_this_now_status: {top.get('status')}",
99
+ ]
100
+ )
101
+ for item in (briefing.get("focus_items") or [])[:3]:
102
+ lines.append(
103
+ f"- focus: {item.get('incident_id')} -> {item.get('action')} [{item.get('status')}]"
104
+ )
105
+ else:
106
+ lines.extend(["", "## hammertime briefing", "- unavailable"])
107
+
108
+ return "\n".join(lines).rstrip() + "\n"
@@ -70,7 +70,7 @@ _TAG_PATTERNS: list[tuple[re.Pattern, str]] = [
70
70
  (re.compile(r"\bcapauth\b", re.I), "capauth"),
71
71
  (re.compile(r"\bskcapstone\b", re.I), "skcapstone"),
72
72
  (re.compile(r"\bskmemory\b", re.I), "skmemory"),
73
- (re.compile(r"\bskcomm\b", re.I), "skcomm"),
73
+ (re.compile(r"\bskcomms\b", re.I), "skcomms"),
74
74
  (re.compile(r"\bskchat\b", re.I), "skchat"),
75
75
  (re.compile(r"\bsyncthing\b", re.I), "syncthing"),
76
76
  (re.compile(r"\bMCP\b", re.I), "mcp"),
@@ -43,6 +43,9 @@ from rich.table import Table
43
43
 
44
44
  from . import AGENT_HOME, __version__
45
45
 
46
+ import logging
47
+ logger = logging.getLogger(__name__)
48
+
46
49
  console = Console()
47
50
 
48
51
  COMMANDS = [
@@ -98,7 +101,8 @@ def _agent_name() -> str:
98
101
  from .runtime import get_runtime
99
102
  runtime = get_runtime(_home())
100
103
  return runtime.manifest.name or "unknown"
101
- except Exception:
104
+ except Exception as e:
105
+ logger.warning("shell.py: %s", e)
102
106
  return "unknown"
103
107
 
104
108
 
@@ -354,6 +358,7 @@ def _handle_chat(args: list[str]) -> None:
354
358
  except ImportError:
355
359
  console.print(" [yellow]Chat module not available[/]")
356
360
  except Exception as e:
361
+ logger.warning("shell.py: %s", e)
357
362
  console.print(f" [red]{e}[/]")
358
363
 
359
364
  elif sub == "inbox":
@@ -669,6 +674,7 @@ def _dispatch_line(line: str) -> None:
669
674
  except _ExitShell:
670
675
  raise
671
676
  except Exception as exc:
677
+ logger.warning("shell.py: %s", exc)
672
678
  console.print(f" [red]Error:[/] {exc}")
673
679
  else:
674
680
  console.print(f" Unknown: [yellow]{cmd}[/]. Type [bold]help[/] for options.")
@@ -435,6 +435,7 @@ def load_yaml_blueprint(path: Path) -> SoulBlueprint:
435
435
  try:
436
436
  return SoulBlueprint.model_validate(data)
437
437
  except Exception as exc:
438
+ logger.warning("soul.py: %s", exc)
438
439
  raise ValueError(f"Invalid blueprint data in {path}: {exc}") from exc
439
440
 
440
441
 
@@ -855,7 +856,8 @@ class SoulManager:
855
856
  "source": "github",
856
857
  "description": "",
857
858
  }
858
- except Exception:
859
+ except Exception as e:
860
+ logger.warning("soul.py: %s", e)
859
861
  pass # offline — show only installed souls
860
862
 
861
863
  # Sort by category, then name
@@ -102,7 +102,8 @@ def _load_state(home: Path) -> SoulSwitchState:
102
102
  try:
103
103
  data = json.loads(path.read_text(encoding="utf-8"))
104
104
  return SoulSwitchState.model_validate(data)
105
- except Exception:
105
+ except Exception as e:
106
+ logger.warning("soul_switch.py: %s", e)
106
107
  return SoulSwitchState()
107
108
 
108
109
 
@@ -161,6 +162,7 @@ def load_switch_soul(home: Path, name: str) -> SoulSwitchBlueprint:
161
162
  try:
162
163
  return SoulSwitchBlueprint.model_validate(data)
163
164
  except Exception as exc:
165
+ logger.warning("soul_switch.py: %s", exc)
164
166
  raise ValueError(f"Invalid soul blueprint in {blueprint_path}: {exc}") from exc
165
167
 
166
168
 
@@ -197,17 +197,17 @@ def _health_summary(home: Path) -> dict:
197
197
 
198
198
 
199
199
  def _inbox_summary(home: Path) -> dict:
200
- """Count unread messages in the SKComm and SKChat inboxes."""
200
+ """Count unread messages in the SKComms and SKChat inboxes."""
201
201
  total = 0
202
202
  sources: list[str] = []
203
203
 
204
- # SKComm file transport inbox
205
- skcomm_inbox = home / "comms" / "inbox"
206
- if skcomm_inbox.exists():
207
- count = sum(1 for f in skcomm_inbox.iterdir() if f.is_file())
204
+ # SKComms file transport inbox
205
+ skcomms_inbox = home / "comms" / "inbox"
206
+ if skcomms_inbox.exists():
207
+ count = sum(1 for f in skcomms_inbox.iterdir() if f.is_file())
208
208
  if count:
209
209
  total += count
210
- sources.append(f"skcomm:{count}")
210
+ sources.append(f"skcomms:{count}")
211
211
 
212
212
  # SKChat local inbox (skchat daemon stores messages here)
213
213
  skchat_inbox = home / "skchat" / "inbox"