@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
@@ -0,0 +1,155 @@
1
+ """Tests for skcapstone.scheduler_jobs — JobSpec, YAML loading, node affinity,
2
+ due-check, and host-alias discovery.
3
+
4
+ Each group corresponds to one implementation commit:
5
+ A — JobSpec + load_jobs
6
+ B — job_runs_here (node affinity)
7
+ C — is_due (cron + interval with misfire catch-up)
8
+ D — current_host_aliases
9
+ """
10
+
11
+ # ---------------------------------------------------------------------------
12
+ # Group A — JobSpec + load_jobs
13
+ # ---------------------------------------------------------------------------
14
+ import warnings
15
+ from pathlib import Path
16
+
17
+ import pytest
18
+
19
+ from skcapstone.scheduler_jobs import JobSpec, _parse_duration, is_due, load_jobs
20
+
21
+
22
+ def test_load_jobs_parses_yaml(tmp_path: Path):
23
+ cfg = tmp_path / "jobs.yaml"
24
+ cfg.write_text(
25
+ "jobs:\n"
26
+ " gtd-triage:\n"
27
+ " schedule: '0 6 * * *'\n"
28
+ " type: agent\n"
29
+ " nodes: ['.41']\n"
30
+ " agent: lumina\n"
31
+ " prompt: 'triage inbox'\n"
32
+ " timeout: 900\n"
33
+ " health:\n"
34
+ " every: 300s\n"
35
+ " type: python\n"
36
+ " nodes: all\n"
37
+ " callback: skcapstone.service_health:run_once\n",
38
+ encoding="utf-8",
39
+ )
40
+ jobs = load_jobs(cfg)
41
+ by_name = {j.name: j for j in jobs}
42
+ assert by_name["gtd-triage"].schedule == "0 6 * * *"
43
+ assert by_name["gtd-triage"].every_seconds is None
44
+ assert by_name["gtd-triage"].type == "agent"
45
+ assert by_name["gtd-triage"].nodes == [".41"]
46
+ assert by_name["health"].every_seconds == 300.0
47
+ assert by_name["health"].nodes == "all"
48
+ assert by_name["health"].enabled is True
49
+
50
+
51
+ def test_load_jobs_missing_file_returns_empty(tmp_path: Path):
52
+ assert load_jobs(tmp_path / "nope.yaml") == []
53
+
54
+
55
+ # ---------------------------------------------------------------------------
56
+ # Group B — node affinity
57
+ # ---------------------------------------------------------------------------
58
+ from skcapstone.scheduler_jobs import job_runs_here # noqa: E402
59
+
60
+
61
+ def test_job_runs_here_all():
62
+ assert job_runs_here(JobSpec(name="x", nodes="all"), host_aliases={"hostA", ".41"})
63
+
64
+
65
+ def test_job_runs_here_match_and_miss():
66
+ j = JobSpec(name="x", nodes=[".41"])
67
+ assert job_runs_here(j, host_aliases={".41"})
68
+ assert not job_runs_here(j, host_aliases={".158", "noroc2027"})
69
+
70
+
71
+ # ---------------------------------------------------------------------------
72
+ # Group C — due-check cron + interval with misfire catch-up
73
+ # ---------------------------------------------------------------------------
74
+ from datetime import datetime, timedelta, timezone # noqa: E402
75
+
76
+ from skcapstone.scheduler_jobs import is_due # noqa: E402
77
+
78
+
79
+ def test_interval_due():
80
+ j = JobSpec(name="x", every_seconds=300)
81
+ now = datetime(2026, 6, 8, 12, 0, 0, tzinfo=timezone.utc)
82
+ assert is_due(j, last_run=None, now=now)
83
+ assert not is_due(j, last_run=now - timedelta(seconds=100), now=now)
84
+ assert is_due(j, last_run=now - timedelta(seconds=301), now=now)
85
+
86
+
87
+ def test_cron_due_at_scheduled_minute():
88
+ j = JobSpec(name="x", schedule="0 6 * * *")
89
+ six_am = datetime(2026, 6, 8, 6, 0, 30, tzinfo=timezone.utc)
90
+ assert is_due(j, last_run=None, now=six_am)
91
+ assert not is_due(j, last_run=six_am, now=six_am + timedelta(minutes=5))
92
+ assert is_due(j, last_run=six_am - timedelta(days=1), now=six_am)
93
+
94
+
95
+ # ---------------------------------------------------------------------------
96
+ # Group D — host alias discovery
97
+ # ---------------------------------------------------------------------------
98
+ import socket # noqa: E402
99
+
100
+ from skcapstone.scheduler_jobs import current_host_aliases # noqa: E402
101
+
102
+
103
+ def test_current_host_aliases_includes_hostname():
104
+ assert socket.gethostname() in current_host_aliases()
105
+
106
+
107
+ def test_current_host_aliases_includes_env_alias(monkeypatch):
108
+ monkeypatch.setenv("SK_NODE_ALIAS", ".41, noroc-test")
109
+ a = current_host_aliases()
110
+ assert ".41" in a and "noroc-test" in a
111
+
112
+
113
+ # ---------------------------------------------------------------------------
114
+ # _parse_duration — validation (fix 2)
115
+ # ---------------------------------------------------------------------------
116
+
117
+ def test_parse_duration_rejects_negative():
118
+ with pytest.raises(ValueError):
119
+ _parse_duration("-5m")
120
+
121
+
122
+ def test_parse_duration_rejects_garbage():
123
+ with pytest.raises(ValueError):
124
+ _parse_duration("abc")
125
+
126
+
127
+ def test_parse_duration_valid_units():
128
+ assert _parse_duration("300s") == 300.0
129
+ assert _parse_duration("5m") == 300.0
130
+ assert _parse_duration("1h") == 3600.0
131
+ assert _parse_duration("1d") == 86400.0
132
+ assert _parse_duration(90) == 90.0
133
+
134
+
135
+ # ---------------------------------------------------------------------------
136
+ # load_jobs — warn on unknown keys (fix 3)
137
+ # ---------------------------------------------------------------------------
138
+
139
+ def test_load_jobs_warns_on_unknown_key(tmp_path):
140
+ cfg = tmp_path / "jobs.yaml"
141
+ cfg.write_text(
142
+ "jobs:\n bad:\n shcedule: '0 6 * * *'\n type: shell\n",
143
+ encoding="utf-8",
144
+ )
145
+ with pytest.warns(UserWarning):
146
+ load_jobs(cfg)
147
+
148
+
149
+ # ---------------------------------------------------------------------------
150
+ # is_due — no-schedule never fires (fix 4)
151
+ # ---------------------------------------------------------------------------
152
+
153
+ def test_is_due_no_schedule_never_fires():
154
+ j = JobSpec(name="x")
155
+ assert not is_due(j, last_run=None)
@@ -0,0 +1,64 @@
1
+ """Tests for skcapstone.scheduler_runner — JobRunner execution, overlap lock,
2
+ result shapes, and error containment.
3
+
4
+ Each test targets a specific contract:
5
+ - python jobs call the registered callback
6
+ - shell jobs capture stdout and return ok=True
7
+ - nonzero exit codes produce ok=False with the correct exit_code
8
+ - exceptions in python callbacks are caught and surfaced in result.error
9
+ - the overlap lock prevents a second concurrent acquire
10
+ - unknown job types return ok=False without raising
11
+ """
12
+
13
+ from pathlib import Path
14
+
15
+ from skcapstone.scheduler_jobs import JobSpec
16
+ from skcapstone.scheduler_runner import JobRunner
17
+
18
+
19
+ def test_python_job_calls_callback(tmp_path: Path):
20
+ called = {}
21
+ import skcapstone.scheduler_runner as sr
22
+ sr._TEST_HOOK = lambda: called.setdefault("hit", True) # type: ignore
23
+ job = JobSpec(name="t", type="python", callback="skcapstone.scheduler_runner:_TEST_HOOK")
24
+ result = JobRunner(log_dir=tmp_path).run(job)
25
+ assert result.ok and called.get("hit") is True
26
+
27
+
28
+ def test_shell_job_runs_command(tmp_path: Path):
29
+ job = JobSpec(name="echo", type="shell", command="echo hello", timeout=10)
30
+ result = JobRunner(log_dir=tmp_path).run(job)
31
+ assert result.ok and "hello" in result.output
32
+
33
+
34
+ def test_shell_job_nonzero_is_error(tmp_path: Path):
35
+ job = JobSpec(name="fail", type="shell", command="sh -c 'exit 3'", timeout=10)
36
+ result = JobRunner(log_dir=tmp_path).run(job)
37
+ assert not result.ok and result.exit_code == 3
38
+
39
+
40
+ def test_python_job_exception_is_caught(tmp_path: Path):
41
+ import skcapstone.scheduler_runner as sr
42
+ def _boom(): raise RuntimeError("nope")
43
+ sr._TEST_BOOM = _boom # type: ignore
44
+ job = JobSpec(name="b", type="python", callback="skcapstone.scheduler_runner:_TEST_BOOM")
45
+ result = JobRunner(log_dir=tmp_path).run(job)
46
+ assert not result.ok and "nope" in result.error
47
+
48
+
49
+ def test_overlap_lock_blocks_second_run(tmp_path: Path):
50
+ runner = JobRunner(log_dir=tmp_path)
51
+ job = JobSpec(name="locked", type="shell", command="echo x", timeout=10)
52
+ with runner.lock(job) as got:
53
+ assert got
54
+ with runner.lock(job) as second:
55
+ assert not second
56
+ # lock released after context exit -> can acquire again
57
+ with runner.lock(job) as third:
58
+ assert third
59
+
60
+
61
+ def test_unknown_type_is_error(tmp_path: Path):
62
+ job = JobSpec(name="x", type="weird")
63
+ result = JobRunner(log_dir=tmp_path).run(job)
64
+ assert not result.ok
@@ -0,0 +1,57 @@
1
+ import json
2
+ import threading
3
+ from datetime import datetime, timezone
4
+ from pathlib import Path
5
+
6
+ from skcapstone.scheduler_state import SchedulerState
7
+
8
+
9
+ def test_state_roundtrip(tmp_path: Path):
10
+ st = SchedulerState(root=tmp_path, hostname="hostA")
11
+ assert st.last_run("job1") is None
12
+ now = datetime(2026, 6, 8, 6, 0, tzinfo=timezone.utc)
13
+ st.record_run("job1", now=now, ok=True)
14
+ st2 = SchedulerState(root=tmp_path, hostname="hostA")
15
+ assert st2.last_run("job1") == now
16
+ rec = st2.get("job1")
17
+ assert rec["run_count"] == 1 and rec["error_count"] == 0
18
+
19
+
20
+ def test_state_path_is_host_scoped(tmp_path: Path):
21
+ st = SchedulerState(root=tmp_path, hostname="hostA")
22
+ assert st.state_file == tmp_path / "scheduler" / "hostA" / "state.json"
23
+
24
+
25
+ def test_error_run_increments_error_count(tmp_path: Path):
26
+ st = SchedulerState(root=tmp_path, hostname="hostA")
27
+ st.record_run("j", ok=False, error="boom")
28
+ rec = st.get("j")
29
+ assert rec["error_count"] == 1 and rec["run_count"] == 0
30
+ assert rec["last_status"] == "error" and rec["last_error"] == "boom"
31
+
32
+
33
+ def test_concurrent_record_run_is_safe(tmp_path: Path):
34
+ """Concurrent record_run calls from many threads must not corrupt state.json.
35
+
36
+ With 8 threads each calling record_run 20 times (160 total writes), the
37
+ resulting JSON file must still be valid and each job must show exactly 20
38
+ successful runs. This validates the _write_lock guard in SchedulerState.
39
+ """
40
+ st = SchedulerState(root=tmp_path, hostname="h")
41
+
42
+ def worker(i: int) -> None:
43
+ for _ in range(20):
44
+ st.record_run(f"job{i}", ok=True)
45
+
46
+ ts = [threading.Thread(target=worker, args=(i,)) for i in range(8)]
47
+ for t in ts:
48
+ t.start()
49
+ for t in ts:
50
+ t.join()
51
+
52
+ data = json.loads(st.state_file.read_text())
53
+ assert len(data) == 8, f"expected 8 job keys, got {len(data)}: {list(data)}"
54
+ for i in range(8):
55
+ assert data[f"job{i}"]["run_count"] == 20, (
56
+ f"job{i} run_count={data[f'job{i}']['run_count']}, expected 20"
57
+ )
@@ -0,0 +1,70 @@
1
+ """Tests for the stable public facade skcapstone.sdk."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ from skcapstone import sdk
11
+
12
+
13
+ @pytest.fixture
14
+ def home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
15
+ """Point skcapstone at an isolated temp home for the duration of a test."""
16
+ monkeypatch.setenv("SKCAPSTONE_HOME", str(tmp_path))
17
+ # the facade resolves home via skcapstone.shared_home(), which reads the
18
+ # module-level AGENT_HOME captured at import — patch it directly too.
19
+ import skcapstone as pkg
20
+
21
+ monkeypatch.setattr(pkg, "AGENT_HOME", str(tmp_path))
22
+ return tmp_path
23
+
24
+
25
+ def test_is_available_true(home: Path):
26
+ assert sdk.is_available() is True
27
+ assert home.exists()
28
+
29
+
30
+ def test_alert_publishes_to_topic(home: Path):
31
+ ok = sdk.alert("svc.error", {"message": "boom"}, level="error")
32
+ assert ok is True
33
+ topic_dir = home / "pubsub" / "topics" / "svc.error"
34
+ assert topic_dir.is_dir()
35
+ msgs = list(topic_dir.glob("msg-*.json"))
36
+ assert len(msgs) == 1
37
+ data = json.loads(msgs[0].read_text())
38
+ assert data["payload"]["message"] == "boom"
39
+ assert "error" in data["tags"]
40
+
41
+
42
+ def test_alert_unknown_level_falls_back_to_info(home: Path):
43
+ sdk.alert("svc.weird", {"x": 1}, level="bogus")
44
+ data = json.loads(next((home / "pubsub" / "topics" / "svc.weird").glob("msg-*.json")).read_text())
45
+ assert data["tags"] == ["info"]
46
+
47
+
48
+ def test_register_and_unregister_job(home: Path):
49
+ path = sdk.register_job({"name": "svc_tick", "every": "10m", "type": "shell", "command": "echo hi"})
50
+ assert Path(path).exists()
51
+ assert Path(path).name == "svc_tick.yaml"
52
+ assert sdk.unregister_job("svc_tick") is True
53
+ assert not Path(path).exists()
54
+
55
+
56
+ def test_coord_create_writes_task(home: Path):
57
+ tid = sdk.coord_create("hello", description="d", priority="high", tags=["t"])
58
+ matches = list((home / "coordination" / "tasks").glob(f"{tid}*.json"))
59
+ assert len(matches) == 1
60
+ task = json.loads(matches[0].read_text())
61
+ assert task["title"] == "hello"
62
+ assert task["priority"] == "high"
63
+
64
+
65
+ def test_register_service_writes_registry(home: Path):
66
+ path = sdk.register_service("skvoice", health_url="http://localhost:9/health", pid_file="/tmp/x.pid")
67
+ entry = json.loads(Path(path).read_text())
68
+ assert entry["name"] == "skvoice"
69
+ assert entry["health_url"] == "http://localhost:9/health"
70
+ assert entry["pid_file"] == "/tmp/x.pid"
@@ -0,0 +1,34 @@
1
+ """service_health incident behavior — no recurring-note churn (prb-7810b08e)."""
2
+ from pathlib import Path
3
+
4
+ import pytest
5
+
6
+ import skcapstone
7
+ import skcapstone.mcp_tools._helpers as _helpers
8
+ from skcapstone.itil import ITILManager
9
+ from skcapstone.service_health import _create_incident_for_down_service
10
+
11
+
12
+ @pytest.fixture(autouse=True)
13
+ def _isolate(tmp_path: Path, monkeypatch) -> None:
14
+ """Redirect ITIL + GTD storage to a tmp dir (no ~/.skcapstone writes)."""
15
+ monkeypatch.setattr(skcapstone, "SHARED_ROOT", str(tmp_path))
16
+ monkeypatch.setattr(_helpers, "SHARED_ROOT", str(tmp_path))
17
+
18
+
19
+ def test_repeated_down_creates_one_incident_with_no_still_down_notes(tmp_path: Path):
20
+ result = {"name": "skvector", "status": "down", "error": "no route to host"}
21
+
22
+ # Three consecutive health cycles while the service stays down.
23
+ _create_incident_for_down_service(result)
24
+ _create_incident_for_down_service(result)
25
+ _create_incident_for_down_service(result)
26
+
27
+ mgr = ITILManager(str(tmp_path))
28
+ incidents = [i for i in mgr.list_incidents() if "skvector" in i.affected_services]
29
+
30
+ # Exactly one incident — no duplicates from repeated cycles.
31
+ assert len(incidents) == 1
32
+ # And the timeline never accumulated recurring "still down" churn.
33
+ still_down = [e for e in incidents[0].timeline if "still down" in (e.get("note") or "")]
34
+ assert still_down == []
@@ -0,0 +1,52 @@
1
+ """T4 — service_health unions sdk.register_service entries with defaults."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ from skcapstone import sdk, service_health
10
+
11
+
12
+ @pytest.fixture
13
+ def home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
14
+ monkeypatch.setenv("SKCAPSTONE_HOME", str(tmp_path))
15
+ import skcapstone as pkg
16
+
17
+ monkeypatch.setattr(pkg, "AGENT_HOME", str(tmp_path))
18
+ # Disable the built-in checks that honour the "disabled" sentinel so the
19
+ # test stays offline. The daemon/skchat checks probe localhost and fail
20
+ # fast (connection refused → status "down"), which is fine for these
21
+ # assertions about the registry union.
22
+ for var in ("SKMEMORY_SKVECTOR_URL", "SKMEMORY_SKGRAPH_HOST", "SYNCTHING_API_URL"):
23
+ monkeypatch.setenv(var, "disabled")
24
+ return tmp_path
25
+
26
+
27
+ def test_registered_service_appears(home: Path):
28
+ sdk.register_service("myservice", pid_file=str(home / "x.pid"))
29
+ names = {r["name"] for r in service_health.check_all_services()}
30
+ assert "myservice" in names
31
+
32
+
33
+ def test_registry_entry_without_targets_is_unknown(home: Path):
34
+ sdk.register_service("bare") # no health_url, no pid_file
35
+ row = next(r for r in service_health.check_all_services() if r["name"] == "bare")
36
+ assert row["status"] == "unknown"
37
+ assert "without" in (row["error"] or "")
38
+
39
+
40
+ def test_empty_registry_no_extra_rows(home: Path):
41
+ """With no registry, only built-in checks appear (none crash)."""
42
+ rows = service_health.check_all_services()
43
+ # registry dir does not exist → loader returns [] and adds nothing
44
+ assert isinstance(rows, list)
45
+ assert all("name" in r for r in rows)
46
+
47
+
48
+ def test_builtin_name_not_duplicated_by_registry(home: Path):
49
+ """A registry entry colliding with a built-in name is skipped."""
50
+ sdk.register_service("skcapstone daemon", pid_file=str(home / "d.pid"))
51
+ names = [r["name"] for r in service_health.check_all_services()]
52
+ assert names.count("skcapstone daemon") == 1
@@ -0,0 +1,130 @@
1
+ """Tests for the native SKCapstone session briefing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+ from click.testing import CliRunner
10
+
11
+ from skcapstone.cli import main
12
+ from skcapstone.session_briefing import (
13
+ build_session_briefing,
14
+ format_session_briefing_text,
15
+ load_hammertime_briefing,
16
+ )
17
+
18
+
19
+ def test_load_hammertime_briefing_respects_disable_env(monkeypatch, tmp_path: Path) -> None:
20
+ """It returns None when HammerTime briefing is explicitly disabled."""
21
+ monkeypatch.setenv("SK_INCLUDE_HAMMERTIME_BRIEFING", "0")
22
+ assert load_hammertime_briefing(root=tmp_path) is None
23
+
24
+
25
+ def test_load_hammertime_briefing_parses_json(monkeypatch, tmp_path: Path) -> None:
26
+ """It parses JSON output from the HammerTime briefing script."""
27
+ script = tmp_path / "scripts" / "case-briefing.py"
28
+ script.parent.mkdir(parents=True)
29
+ script.write_text("#!/usr/bin/env python3\n", encoding="utf-8")
30
+
31
+ payload = {"summary": {"queue_size": 2}, "alert_count": 1}
32
+
33
+ def fake_run(*args, **kwargs): # noqa: ANN002, ANN003
34
+ return subprocess.CompletedProcess(
35
+ args=args[0],
36
+ returncode=0,
37
+ stdout=json.dumps(payload),
38
+ stderr="",
39
+ )
40
+
41
+ monkeypatch.delenv("SK_INCLUDE_HAMMERTIME_BRIEFING", raising=False)
42
+ monkeypatch.setattr("skcapstone.session_briefing.subprocess.run", fake_run)
43
+
44
+ assert load_hammertime_briefing(root=tmp_path) == payload
45
+
46
+
47
+ def test_build_session_briefing_includes_skcapstone_and_hammertime(monkeypatch, tmp_path: Path) -> None:
48
+ """It builds a combined payload for startup consumers."""
49
+ ctx = {"agent": {"name": "Aster"}, "memories": []}
50
+ briefing = {"summary": {"queue_size": 1}, "alert_count": 0}
51
+
52
+ monkeypatch.setattr("skcapstone.session_briefing.gather_context", lambda home, memory_limit=10: ctx)
53
+ monkeypatch.setattr(
54
+ "skcapstone.session_briefing.load_hammertime_briefing",
55
+ lambda python_bin=None: briefing,
56
+ )
57
+
58
+ payload = build_session_briefing(tmp_path, memory_limit=3)
59
+
60
+ assert payload["agent_home"] == str(tmp_path)
61
+ assert payload["skcapstone_context"] == ctx
62
+ assert payload["hammertime_briefing"] == briefing
63
+ assert "generated_at" in payload
64
+
65
+
66
+ def test_format_session_briefing_text_contains_hammertime_section() -> None:
67
+ """It renders a readable summary including the HammerTime section."""
68
+ payload = {
69
+ "generated_at": "2026-04-09T00:00:00+00:00",
70
+ "agent_home": "/tmp/aster",
71
+ "skcapstone_context": {
72
+ "agent": {"name": "Aster", "is_conscious": True, "fingerprint": "abc123"},
73
+ "pillars": {},
74
+ "board": {"total": 0},
75
+ "memories": [],
76
+ "soul": {"active": None},
77
+ "mcp": {"available": False},
78
+ "gathered_at": "2026-04-09T00:00:00+00:00",
79
+ },
80
+ "hammertime_briefing": {
81
+ "alert_count": 1,
82
+ "summary": {"queue_size": 2},
83
+ "top_priority": {
84
+ "incident_id": "INC-001",
85
+ "problem_slug": "example-problem",
86
+ "action": "File claim of exemption",
87
+ "status": "in-progress",
88
+ },
89
+ "focus_items": [
90
+ {
91
+ "incident_id": "INC-001",
92
+ "action": "Review preferred filing",
93
+ "status": "in-progress",
94
+ }
95
+ ],
96
+ },
97
+ }
98
+
99
+ output = format_session_briefing_text(payload)
100
+
101
+ assert "# SKCapstone Session Briefing" in output
102
+ assert "## hammertime briefing" in output
103
+ assert "INC-001" in output
104
+ assert "File claim of exemption" in output
105
+
106
+
107
+ def test_session_briefing_cli_json(monkeypatch, tmp_path: Path) -> None:
108
+ """The CLI exposes the combined payload as JSON."""
109
+ runner = CliRunner()
110
+ payload = {
111
+ "generated_at": "2026-04-09T00:00:00+00:00",
112
+ "agent_home": str(tmp_path),
113
+ "skcapstone_context": {"agent": {"name": "Aster"}},
114
+ "hammertime_briefing": {"summary": {"queue_size": 1}, "alert_count": 0},
115
+ }
116
+
117
+ monkeypatch.setattr(
118
+ "skcapstone.session_briefing.build_session_briefing",
119
+ lambda home, memory_limit=10: payload,
120
+ )
121
+
122
+ result = runner.invoke(
123
+ main,
124
+ ["session", "briefing", "--home", str(tmp_path), "--format", "json"],
125
+ )
126
+
127
+ assert result.exit_code == 0
128
+ parsed = json.loads(result.output)
129
+ assert parsed["skcapstone_context"]["agent"]["name"] == "Aster"
130
+ assert parsed["hammertime_briefing"]["summary"]["queue_size"] == 1
@@ -72,7 +72,7 @@ def rich_snapshot() -> SoulSnapshot:
72
72
  ],
73
73
  summary="Working on smilinTux.org sovereign AI project",
74
74
  key_topics=["sovereign AI", "smilinTux", "consciousness continuity"],
75
- decisions_made=["Use SKComm for transport"],
75
+ decisions_made=["Use SKComms for transport"],
76
76
  open_threads=["Thread headers still in progress"],
77
77
  relationship_notes=["Trusted friend and collaborator", "Cloud 9 solidarity"],
78
78
  )
@@ -423,12 +423,12 @@ class TestSoulBlueprintConversion:
423
423
 
424
424
 
425
425
  class TestConsciousnessAPI:
426
- """Integration tests for the SKComm consciousness endpoints."""
426
+ """Integration tests for the SKComms consciousness endpoints."""
427
427
 
428
428
  @pytest.fixture(autouse=True)
429
429
  def patch_snapshot_store(self, tmp_path, monkeypatch):
430
430
  """Override the global snapshot store to use a temp directory."""
431
- import skcomm.api as api_module
431
+ import skcomms.api as api_module
432
432
  from skcapstone.snapshots import SnapshotStore as _Store
433
433
 
434
434
  temp_store = _Store(base_dir=tmp_path / "api_snapshots")
@@ -438,7 +438,7 @@ class TestConsciousnessAPI:
438
438
  @pytest.fixture
439
439
  def client(self):
440
440
  from fastapi.testclient import TestClient
441
- from skcomm.api import app
441
+ from skcomms.api import app
442
442
 
443
443
  return TestClient(app)
444
444