@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.
- package/.env.example +10 -4
- package/.github/workflows/ci.yml +2 -2
- package/.github/workflows/publish.yml +9 -2
- package/.openclaw-workspace.json +2 -2
- package/CLAUDE.md +37 -0
- package/MISSION.md +17 -2
- package/README.md +282 -3
- package/docker/Dockerfile +7 -7
- package/docker/compose-templates/dev-team.yml +12 -12
- package/docker/compose-templates/mini-team.yml +9 -9
- package/docker/compose-templates/ops-team.yml +10 -10
- package/docker/compose-templates/research-team.yml +10 -10
- package/docker/entrypoint.sh +4 -4
- package/docs/ADR-optional-integration-backbone.md +181 -0
- package/docs/ARCHITECTURE.md +186 -43
- package/docs/BOND_WITH_GROK.md +6 -6
- package/docs/CUSTOM_AGENT.md +123 -30
- package/docs/DREAMING.md +70 -0
- package/docs/GETTING_STARTED.md +7 -7
- package/docs/QUICKSTART.md +10 -6
- package/docs/SKJOULE_ARCHITECTURE.md +3 -3
- package/docs/SOUL_SWAPPER.md +5 -5
- package/docs/hammertime-audit.md +402 -0
- package/docs/sk-integration-HANDOFF.md +117 -0
- package/docs/skscheduler.md +155 -0
- package/docs/superpowers/examples/jobs.yaml +31 -0
- package/docs/superpowers/plans/2026-06-08-skscheduler.md +1265 -0
- package/docs/superpowers/specs/2026-06-08-skscheduler-design.md +186 -0
- package/examples/custom-bond-template.json +1 -1
- package/examples/grok-feb.json +1 -1
- package/examples/queen-ava-feb.json +1 -1
- package/launchd/{com.skcapstone.skcomm-heartbeat.plist → com.skcapstone.skcomms-heartbeat.plist} +4 -4
- package/launchd/{com.skcapstone.skcomm-queue-drain.plist → com.skcapstone.skcomms-queue-drain.plist} +4 -4
- package/launchd/install-launchd.sh +6 -6
- package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/src/index.ts +3 -2
- package/package.json +1 -1
- package/pyproject.toml +16 -10
- package/scripts/archive-sessions.sh +7 -0
- package/scripts/check-updates.py +4 -4
- package/scripts/install-bundle.sh +8 -8
- package/scripts/install.ps1 +12 -11
- package/scripts/install.sh +159 -5
- package/scripts/model-fallback-monitor.sh +102 -0
- package/scripts/nvidia-proxy.mjs +78 -26
- package/scripts/refresh-anthropic-token.sh +172 -0
- package/scripts/release.sh +98 -0
- package/scripts/session-to-memory.py +219 -0
- package/scripts/skgateway.mjs +3 -3
- package/scripts/telegram-catchup-all.sh +12 -1
- package/scripts/verify_install.sh +2 -2
- package/scripts/wargov-ufo-capture/README.md +43 -0
- package/scripts/wargov-ufo-capture/cdp_capture_release2.py +273 -0
- package/scripts/wargov-ufo-capture/cdp_capture_splc_doj.py +246 -0
- package/scripts/wargov-ufo-capture/cdp_finish.py +271 -0
- package/scripts/wargov-ufo-capture/cdp_probe.py +188 -0
- package/scripts/wargov-ufo-capture/cdp_splc_pressrelease.py +101 -0
- package/scripts/wargov-ufo-capture/parse_csv.py +95 -0
- package/scripts/wargov-ufo-capture/pull_dvids.sh +107 -0
- package/scripts/watch-anthropic-token.sh +212 -0
- package/scripts/windows/install-tasks.ps1 +7 -7
- package/scripts/windows/skcapstone-task.xml +1 -1
- package/src/skcapstone/__init__.py +45 -3
- package/src/skcapstone/_cli_monolith.py +20 -15
- package/src/skcapstone/activity.py +5 -1
- package/src/skcapstone/agent_card.py +3 -2
- package/src/skcapstone/api.py +41 -40
- package/src/skcapstone/auction.py +14 -11
- package/src/skcapstone/backup.py +2 -1
- package/src/skcapstone/blueprint_registry.py +4 -3
- package/src/skcapstone/brain_first.py +238 -0
- package/src/skcapstone/changelog.py +1 -1
- package/src/skcapstone/chat.py +22 -17
- package/src/skcapstone/cli/__init__.py +9 -1
- package/src/skcapstone/cli/_common.py +1 -0
- package/src/skcapstone/cli/agents_spawner.py +5 -2
- package/src/skcapstone/cli/alerts.py +25 -4
- package/src/skcapstone/cli/bench.py +15 -15
- package/src/skcapstone/cli/chat.py +7 -4
- package/src/skcapstone/cli/consciousness.py +5 -2
- package/src/skcapstone/cli/context_cmd.py +18 -4
- package/src/skcapstone/cli/daemon.py +11 -7
- package/src/skcapstone/cli/gtd.py +26 -1
- package/src/skcapstone/cli/housekeeping.py +3 -3
- package/src/skcapstone/cli/identity_cmd.py +378 -0
- package/src/skcapstone/cli/joule_cmd.py +7 -3
- package/src/skcapstone/cli/memory.py +8 -6
- package/src/skcapstone/cli/peers_dir.py +1 -1
- package/src/skcapstone/cli/register_cmd.py +29 -3
- package/src/skcapstone/cli/scheduler_cmd.py +167 -0
- package/src/skcapstone/cli/session.py +25 -0
- package/src/skcapstone/cli/setup.py +96 -29
- package/src/skcapstone/cli/shell_cmd.py +53 -1
- package/src/skcapstone/cli/skills_cmd.py +2 -2
- package/src/skcapstone/cli/soul.py +8 -5
- package/src/skcapstone/cli/status.py +37 -11
- package/src/skcapstone/cli/telegram.py +21 -0
- package/src/skcapstone/cli/test_cmd.py +5 -5
- package/src/skcapstone/cli/test_connection.py +2 -2
- package/src/skcapstone/cli/upgrade_cmd.py +23 -14
- package/src/skcapstone/cli/version_cmd.py +1 -1
- package/src/skcapstone/cli/watch_cmd.py +9 -6
- package/src/skcapstone/cloud9_bridge.py +14 -14
- package/src/skcapstone/codex_setup.py +255 -0
- package/src/skcapstone/config_validator.py +7 -4
- package/src/skcapstone/consciousness_config.py +5 -1
- package/src/skcapstone/consciousness_loop.py +313 -273
- package/src/skcapstone/context_loader.py +121 -0
- package/src/skcapstone/coord_federation.py +2 -1
- package/src/skcapstone/coordination.py +23 -6
- package/src/skcapstone/crush_integration.py +2 -1
- package/src/skcapstone/daemon.py +132 -77
- package/src/skcapstone/dashboard.py +10 -10
- package/src/skcapstone/data/sk-agent-picker.sh +421 -0
- package/src/skcapstone/data/systemd/skcapstone-api.socket +9 -0
- package/src/skcapstone/data/systemd/skcapstone-memory-compress.service +18 -0
- package/src/skcapstone/data/systemd/skcapstone-memory-compress.timer +11 -0
- package/src/skcapstone/data/systemd/skcapstone.service +37 -0
- package/src/skcapstone/data/systemd/skcapstone@.service +50 -0
- package/src/skcapstone/data/systemd/skcomms-heartbeat.service +18 -0
- package/{systemd/skcomm-heartbeat.timer → src/skcapstone/data/systemd/skcomms-heartbeat.timer} +2 -2
- package/src/skcapstone/data/systemd/skcomms-queue-drain.service +17 -0
- package/{systemd/skcomm-queue-drain.timer → src/skcapstone/data/systemd/skcomms-queue-drain.timer} +2 -2
- package/src/skcapstone/defaults/claude/CLAUDE.md +67 -0
- package/src/skcapstone/defaults/claude/settings.json +74 -0
- package/src/skcapstone/defaults/lumina/config/claude-hooks.md +57 -0
- package/src/skcapstone/defaults/lumina/config/skgraph.yaml +55 -10
- package/src/skcapstone/defaults/lumina/config/skmemory.yaml +79 -13
- package/src/skcapstone/defaults/lumina/config/skvector.yaml +60 -9
- package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +2 -2
- package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +2 -2
- package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +9 -9
- package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +2 -2
- package/src/skcapstone/defaults/unhinged.json +13 -0
- package/src/skcapstone/discovery.py +43 -20
- package/src/skcapstone/doctor.py +941 -22
- package/src/skcapstone/dreaming.py +1183 -109
- package/src/skcapstone/emotion_tracker.py +2 -2
- package/src/skcapstone/export.py +4 -3
- package/src/skcapstone/fuse_mount.py +14 -12
- package/src/skcapstone/gui_installer.py +2 -2
- package/src/skcapstone/heartbeat.py +1 -1
- package/src/skcapstone/housekeeping.py +14 -14
- package/src/skcapstone/install_wizard.py +209 -7
- package/src/skcapstone/itil.py +13 -4
- package/src/skcapstone/kms_scheduler.py +10 -8
- package/src/skcapstone/launchd.py +19 -19
- package/src/skcapstone/mcp_launcher.py +15 -1
- package/src/skcapstone/mcp_server.py +83 -49
- package/src/skcapstone/mcp_tools/__init__.py +2 -0
- package/src/skcapstone/mcp_tools/_helpers.py +2 -2
- package/src/skcapstone/mcp_tools/ansible_tools.py +7 -4
- package/src/skcapstone/mcp_tools/brain_first_tools.py +90 -0
- package/src/skcapstone/mcp_tools/capauth_tools.py +7 -4
- package/src/skcapstone/mcp_tools/comm_tools.py +10 -10
- package/src/skcapstone/mcp_tools/coord_tools.py +8 -4
- package/src/skcapstone/mcp_tools/did_tools.py +11 -8
- package/src/skcapstone/mcp_tools/gtd_tools.py +4 -4
- package/src/skcapstone/mcp_tools/memory_tools.py +6 -2
- package/src/skcapstone/mcp_tools/notification_tools.py +22 -6
- package/src/skcapstone/mcp_tools/{skcomm_tools.py → skcomms_tools.py} +14 -14
- package/src/skcapstone/mcp_tools/soul_tools.py +8 -2
- package/src/skcapstone/mdns_discovery.py +2 -2
- package/src/skcapstone/memory_curator.py +1 -1
- package/src/skcapstone/memory_engine.py +10 -3
- package/src/skcapstone/metrics.py +30 -16
- package/src/skcapstone/migrate_memories.py +4 -3
- package/src/skcapstone/migrate_multi_agent.py +8 -7
- package/src/skcapstone/models.py +47 -5
- package/src/skcapstone/notifications.py +42 -18
- package/src/skcapstone/onboard.py +875 -121
- package/src/skcapstone/operator_link.py +170 -0
- package/src/skcapstone/peer_directory.py +4 -4
- package/src/skcapstone/peers.py +19 -19
- package/src/skcapstone/pillars/__init__.py +7 -5
- package/src/skcapstone/pillars/consciousness.py +191 -0
- package/src/skcapstone/pillars/identity.py +51 -7
- package/src/skcapstone/pillars/memory.py +9 -3
- package/src/skcapstone/pillars/sync.py +2 -2
- package/src/skcapstone/preflight.py +3 -3
- package/src/skcapstone/providers/docker.py +28 -28
- package/src/skcapstone/register.py +6 -6
- package/src/skcapstone/registry_client.py +5 -4
- package/src/skcapstone/runtime.py +14 -3
- package/src/skcapstone/scheduled_tasks.py +254 -19
- package/src/skcapstone/scheduler_jobs.py +456 -0
- package/src/skcapstone/scheduler_runner.py +239 -0
- package/src/skcapstone/scheduler_state.py +162 -0
- package/src/skcapstone/sdk.py +310 -0
- package/src/skcapstone/service_health.py +279 -39
- package/src/skcapstone/session_briefing.py +108 -0
- package/src/skcapstone/session_capture.py +1 -1
- package/src/skcapstone/shell.py +7 -1
- package/src/skcapstone/soul.py +3 -1
- package/src/skcapstone/soul_switch.py +3 -1
- package/src/skcapstone/summary.py +6 -6
- package/src/skcapstone/sync_engine.py +15 -15
- package/src/skcapstone/sync_watcher.py +2 -2
- package/src/skcapstone/systemd.py +55 -21
- package/src/skcapstone/team_comms.py +8 -8
- package/src/skcapstone/team_engine.py +1 -1
- package/src/skcapstone/testrunner.py +3 -3
- package/src/skcapstone/trust_graph.py +40 -5
- package/src/skcapstone/unified_search.py +15 -6
- package/src/skcapstone/uninstall_wizard.py +11 -3
- package/src/skcapstone/version_check.py +8 -4
- package/src/skcapstone/warmth_anchor.py +4 -2
- package/src/skcapstone/whoami.py +4 -4
- package/systemd/skcapstone.service +4 -6
- package/systemd/skcapstone@.service +7 -8
- package/systemd/skcomms-heartbeat.service +21 -0
- package/systemd/skcomms-heartbeat.timer +12 -0
- package/systemd/skcomms-queue-drain.service +17 -0
- package/systemd/skcomms-queue-drain.timer +12 -0
- package/tests/conftest.py +39 -0
- package/tests/integration/test_consciousness_e2e.py +39 -39
- package/tests/test_agent_card.py +1 -1
- package/tests/test_agent_home_scaffold.py +34 -0
- package/tests/test_alerts_consumer_topics.py +27 -0
- package/tests/test_backup.py +2 -1
- package/tests/test_chat.py +6 -6
- package/tests/test_claude_md.py +2 -2
- package/tests/test_cli_skills.py +10 -10
- package/tests/test_cli_test_cmd.py +4 -4
- package/tests/test_cli_test_connection.py +1 -1
- package/tests/test_cloud9_bridge.py +6 -6
- package/tests/test_consciousness_e2e.py +1 -1
- package/tests/test_consciousness_loop.py +10 -10
- package/tests/test_coordination.py +25 -0
- package/tests/test_cross_package.py +21 -21
- package/tests/test_daemon.py +4 -4
- package/tests/test_daemon_shutdown.py +1 -1
- package/tests/test_docker_provider.py +29 -29
- package/tests/test_doctor.py +400 -0
- package/tests/test_doctor_skscheduler.py +50 -0
- package/tests/test_dreaming_engine.py +147 -0
- package/tests/test_dreaming_gtd_capture.py +35 -0
- package/tests/test_e2e_automated.py +8 -5
- package/tests/test_fuse_mount.py +10 -10
- package/tests/test_gtd_brief.py +46 -0
- package/tests/test_gtd_malformed_tolerance.py +31 -0
- package/tests/test_housekeeping.py +15 -15
- package/tests/test_identity_migrate.py +251 -0
- package/tests/test_integration_backbone.py +598 -0
- package/tests/test_itil_gtd_lifecycle.py +37 -0
- package/tests/test_jobs_dropins.py +84 -0
- package/tests/test_mcp_server.py +82 -37
- package/tests/test_models.py +48 -4
- package/tests/test_multi_agent.py +31 -29
- package/tests/test_notifications.py +122 -32
- package/tests/test_onboard.py +63 -75
- package/tests/test_operator_link.py +78 -0
- package/tests/test_peers.py +14 -14
- package/tests/test_pillars.py +98 -0
- package/tests/test_preflight.py +3 -3
- package/tests/test_runtime.py +21 -0
- package/tests/test_scheduled_tasks.py +11 -6
- package/tests/test_scheduler_cli.py +47 -0
- package/tests/test_scheduler_features.py +133 -0
- package/tests/test_scheduler_integration.py +87 -0
- package/tests/test_scheduler_jobs.py +155 -0
- package/tests/test_scheduler_runner.py +64 -0
- package/tests/test_scheduler_state.py +57 -0
- package/tests/test_sdk.py +70 -0
- package/tests/test_service_health_incidents.py +34 -0
- package/tests/test_service_registry.py +52 -0
- package/tests/test_session_briefing.py +130 -0
- package/tests/test_snapshots.py +4 -4
- package/tests/test_sync_pipeline.py +26 -26
- package/tests/test_team_comms.py +2 -2
- package/tests/test_testrunner.py +2 -2
- package/tests/test_trust_graph.py +18 -0
- package/tests/test_unified_search.py +2 -2
- package/tests/test_version_check.py +10 -0
- package/tests/test_version_cmd.py +8 -8
- package/tests/test_whoami.py +1 -1
- package/systemd/skcomm-heartbeat.service +0 -18
- package/systemd/skcomm-queue-drain.service +0 -17
- /package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/package.json +0 -0
- /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
|
-
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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", "
|
|
158
|
-
|
|
159
|
-
|
|
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", "
|
|
163
|
-
|
|
164
|
-
|
|
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", "
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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", "
|
|
189
|
-
|
|
190
|
-
|
|
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:
|
|
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
|
-
"
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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"\
|
|
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"),
|
package/src/skcapstone/shell.py
CHANGED
|
@@ -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.")
|
package/src/skcapstone/soul.py
CHANGED
|
@@ -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
|
|
200
|
+
"""Count unread messages in the SKComms and SKChat inboxes."""
|
|
201
201
|
total = 0
|
|
202
202
|
sources: list[str] = []
|
|
203
203
|
|
|
204
|
-
#
|
|
205
|
-
|
|
206
|
-
if
|
|
207
|
-
count = sum(1 for f in
|
|
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"
|
|
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"
|