@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
@@ -6,7 +6,7 @@ Covers:
6
6
  - list_peers (empty, with peers)
7
7
  - get_peer / remove_peer
8
8
  - PeerRecord model
9
- - SKComm peer file creation
9
+ - SKComms peer file creation
10
10
  - CLI commands (add, list, remove, show)
11
11
  """
12
12
 
@@ -30,9 +30,9 @@ from skcapstone.peers import (
30
30
 
31
31
  @pytest.fixture
32
32
  def homes(tmp_path):
33
- """Create skcapstone and skcomm home directories."""
33
+ """Create skcapstone and skcomms home directories."""
34
34
  sk = tmp_path / ".skcapstone"
35
- sc = tmp_path / ".skcomm"
35
+ sc = tmp_path / ".skcomms"
36
36
  sk.mkdir()
37
37
  sc.mkdir()
38
38
  return sk, sc
@@ -87,7 +87,7 @@ class TestAddPeerFromCard:
87
87
  def test_add_from_card(self, card_file, homes):
88
88
  """Card import creates peer records in both registries."""
89
89
  sk, sc = homes
90
- peer = add_peer_from_card(card_file, skcapstone_home=sk, skcomm_home=sc)
90
+ peer = add_peer_from_card(card_file, skcapstone_home=sk, skcomms_home=sc)
91
91
 
92
92
  assert peer.name == "Lumina"
93
93
  assert peer.fingerprint == "AABB1122CCDD3344EEFF5566"
@@ -104,7 +104,7 @@ class TestAddPeerFromCard:
104
104
  """Nonexistent card raises FileNotFoundError."""
105
105
  sk, sc = homes
106
106
  with pytest.raises(FileNotFoundError):
107
- add_peer_from_card(Path("/nope.json"), skcapstone_home=sk, skcomm_home=sc)
107
+ add_peer_from_card(Path("/nope.json"), skcapstone_home=sk, skcomms_home=sc)
108
108
 
109
109
  def test_invalid_json_raises(self, tmp_path, homes):
110
110
  """Invalid JSON raises ValueError."""
@@ -112,7 +112,7 @@ class TestAddPeerFromCard:
112
112
  bad = tmp_path / "bad.json"
113
113
  bad.write_text("{{{not json")
114
114
  with pytest.raises(ValueError):
115
- add_peer_from_card(bad, skcapstone_home=sk, skcomm_home=sc)
115
+ add_peer_from_card(bad, skcapstone_home=sk, skcomms_home=sc)
116
116
 
117
117
  def test_missing_name_raises(self, tmp_path, homes):
118
118
  """Card without name raises ValueError."""
@@ -120,7 +120,7 @@ class TestAddPeerFromCard:
120
120
  no_name = tmp_path / "noname.json"
121
121
  no_name.write_text(json.dumps({"fingerprint": "123"}))
122
122
  with pytest.raises(ValueError, match="name"):
123
- add_peer_from_card(no_name, skcapstone_home=sk, skcomm_home=sc)
123
+ add_peer_from_card(no_name, skcapstone_home=sk, skcomms_home=sc)
124
124
 
125
125
 
126
126
  class TestAddPeerManual:
@@ -131,7 +131,7 @@ class TestAddPeerManual:
131
131
  sk, sc = homes
132
132
  peer = add_peer_manual(
133
133
  name="Opus", email="opus@smilintux.org",
134
- skcapstone_home=sk, skcomm_home=sc,
134
+ skcapstone_home=sk, skcomms_home=sc,
135
135
  )
136
136
  assert peer.name == "Opus"
137
137
  assert peer.email == "opus@smilintux.org"
@@ -145,7 +145,7 @@ class TestAddPeerManual:
145
145
 
146
146
  peer = add_peer_manual(
147
147
  name="Opus", public_key_path=key_file,
148
- skcapstone_home=sk, skcomm_home=sc,
148
+ skcapstone_home=sk, skcomms_home=sc,
149
149
  )
150
150
  assert peer.public_key != ""
151
151
  assert peer.trust_level == "verified"
@@ -163,7 +163,7 @@ class TestListPeers:
163
163
  def test_list_with_peers(self, card_file, homes):
164
164
  """Added peers appear in listing."""
165
165
  sk, sc = homes
166
- add_peer_from_card(card_file, skcapstone_home=sk, skcomm_home=sc)
166
+ add_peer_from_card(card_file, skcapstone_home=sk, skcomms_home=sc)
167
167
 
168
168
  peers = list_peers(skcapstone_home=sk)
169
169
  assert len(peers) == 1
@@ -176,7 +176,7 @@ class TestGetPeer:
176
176
  def test_get_existing(self, card_file, homes):
177
177
  """Known peer is returned."""
178
178
  sk, sc = homes
179
- add_peer_from_card(card_file, skcapstone_home=sk, skcomm_home=sc)
179
+ add_peer_from_card(card_file, skcapstone_home=sk, skcomms_home=sc)
180
180
 
181
181
  peer = get_peer("Lumina", skcapstone_home=sk)
182
182
  assert peer is not None
@@ -194,16 +194,16 @@ class TestRemovePeer:
194
194
  def test_remove_existing(self, card_file, homes):
195
195
  """Removing an existing peer cleans up all files."""
196
196
  sk, sc = homes
197
- add_peer_from_card(card_file, skcapstone_home=sk, skcomm_home=sc)
197
+ add_peer_from_card(card_file, skcapstone_home=sk, skcomms_home=sc)
198
198
 
199
- assert remove_peer("Lumina", skcapstone_home=sk, skcomm_home=sc)
199
+ assert remove_peer("Lumina", skcapstone_home=sk, skcomms_home=sc)
200
200
  assert not (sk / "peers" / "lumina.json").exists()
201
201
  assert not (sc / "peers" / "lumina.yml").exists()
202
202
 
203
203
  def test_remove_unknown(self, homes):
204
204
  """Removing unknown peer returns False."""
205
205
  sk, sc = homes
206
- assert not remove_peer("Nobody", skcapstone_home=sk, skcomm_home=sc)
206
+ assert not remove_peer("Nobody", skcapstone_home=sk, skcomms_home=sc)
207
207
 
208
208
 
209
209
  class TestCLI:
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import json
6
6
  from pathlib import Path
7
7
 
8
+ from skcapstone.pillars.consciousness import initialize_consciousness
8
9
  from skcapstone.pillars.identity import generate_identity
9
10
  from skcapstone.pillars.security import (
10
11
  AuditEntry,
@@ -163,3 +164,100 @@ class TestSecurityPillar:
163
164
  """read_audit_log returns empty list when no log exists."""
164
165
  entries = read_audit_log(tmp_path / "nonexistent")
165
166
  assert entries == []
167
+
168
+
169
+ class TestConsciousnessPillar:
170
+ """Tests for consciousness pillar initialization (SKWhisper + SKTrip)."""
171
+
172
+ def test_returns_missing_when_no_skwhisper(self, tmp_agent_home: Path):
173
+ """initialize_consciousness returns MISSING when no SKWhisper data exists."""
174
+ state = initialize_consciousness(tmp_agent_home)
175
+ assert state.status == PillarStatus.MISSING
176
+
177
+ def test_degraded_with_digested_sessions_no_daemon(self, tmp_agent_home: Path, monkeypatch):
178
+ """DEGRADED when sessions have been digested but daemon is not running."""
179
+ import os
180
+ agent_name = os.environ.get("SKCAPSTONE_AGENT", "lumina")
181
+ whisper_dir = tmp_agent_home / "agents" / agent_name / "skwhisper"
182
+ whisper_dir.mkdir(parents=True, exist_ok=True)
183
+
184
+ # Write state.json with one digested session
185
+ state_json = whisper_dir / "state.json"
186
+ state_json.write_text(json.dumps({
187
+ "last_digest": "2026-03-25T12:00:00+00:00",
188
+ "sessions": {
189
+ "abc123": {"digested_at": "2026-03-25T12:00:00+00:00"}
190
+ }
191
+ }))
192
+
193
+ # Daemon not running (systemctl will fail in test env)
194
+ state = initialize_consciousness(tmp_agent_home)
195
+ assert state.sessions_digested == 1
196
+ assert state.sessions_pending == 0
197
+ assert state.status in (PillarStatus.DEGRADED, PillarStatus.ACTIVE)
198
+
199
+ def test_whisper_md_age_tracked(self, tmp_agent_home: Path):
200
+ """whisper.md existence and age are captured correctly."""
201
+ import os
202
+ agent_name = os.environ.get("SKCAPSTONE_AGENT", "lumina")
203
+ whisper_dir = tmp_agent_home / "agents" / agent_name / "skwhisper"
204
+ whisper_dir.mkdir(parents=True, exist_ok=True)
205
+
206
+ whisper_md = whisper_dir / "whisper.md"
207
+ whisper_md.write_text("# Whisper context\n")
208
+
209
+ state = initialize_consciousness(tmp_agent_home)
210
+ assert state.whisper_md == whisper_md
211
+ assert state.whisper_md_age_hours >= 0.0
212
+
213
+ def test_patterns_json_topic_count(self, tmp_agent_home: Path):
214
+ """topics_tracked reflects the number of topics in patterns.json."""
215
+ import os
216
+ agent_name = os.environ.get("SKCAPSTONE_AGENT", "lumina")
217
+ whisper_dir = tmp_agent_home / "agents" / agent_name / "skwhisper"
218
+ whisper_dir.mkdir(parents=True, exist_ok=True)
219
+
220
+ patterns = whisper_dir / "patterns.json"
221
+ patterns.write_text(json.dumps({
222
+ "topics": {"sovereignty": {}, "memory": {}, "consciousness": {}}
223
+ }))
224
+
225
+ state = initialize_consciousness(tmp_agent_home)
226
+ assert state.topics_tracked == 3
227
+
228
+ def test_active_with_daemon_fresh_whisper_and_patterns(
229
+ self, tmp_agent_home: Path, monkeypatch
230
+ ):
231
+ """Fresh SKWhisper context plus tracked patterns is active when daemon is active."""
232
+ agent_name = "jarvis"
233
+ monkeypatch.setenv("SKCAPSTONE_AGENT", agent_name)
234
+ whisper_dir = tmp_agent_home / "agents" / agent_name / "skwhisper"
235
+ whisper_dir.mkdir(parents=True, exist_ok=True)
236
+ (whisper_dir / "whisper.md").write_text("# Whisper context\n")
237
+ (whisper_dir / "patterns.json").write_text(json.dumps({
238
+ "topics": {"sovereignty": {}, "memory": {}}
239
+ }))
240
+
241
+ class Result:
242
+ stdout = "active\n"
243
+
244
+ monkeypatch.setattr(
245
+ "subprocess.run",
246
+ lambda *args, **kwargs: Result(),
247
+ )
248
+
249
+ state = initialize_consciousness(tmp_agent_home)
250
+
251
+ assert state.status == PillarStatus.ACTIVE
252
+
253
+ def test_trip_sessions_counted(self, tmp_agent_home: Path):
254
+ """trip_sessions counts .json files in the sktrip directory."""
255
+ import os
256
+ agent_name = os.environ.get("SKCAPSTONE_AGENT", "lumina")
257
+ trip_dir = tmp_agent_home / "agents" / agent_name / "sktrip"
258
+ trip_dir.mkdir(parents=True, exist_ok=True)
259
+ (trip_dir / "trip-001.json").write_text("{}")
260
+ (trip_dir / "trip-002.json").write_text("{}")
261
+
262
+ state = initialize_consciousness(tmp_agent_home)
263
+ assert state.trip_sessions == 2
@@ -301,15 +301,15 @@ class TestPreflightCheckerPackages:
301
301
  real_import = builtins.__import__
302
302
 
303
303
  def _mock_import(name, *args, **kwargs):
304
- if name == "skcomm":
305
- raise ImportError("no module named skcomm")
304
+ if name == "skcomms":
305
+ raise ImportError("no module named skcomms")
306
306
  return real_import(name, *args, **kwargs)
307
307
 
308
308
  checker = PreflightChecker(home=tmp_path)
309
309
  with patch("builtins.__import__", side_effect=_mock_import):
310
310
  result = checker.check_packages()
311
311
  assert result.status == "fail"
312
- assert "skcomm" in result.message
312
+ assert "skcomms" in result.message
313
313
 
314
314
  def test_all_present_ok(self, tmp_path: Path) -> None:
315
315
  checker = PreflightChecker(home=tmp_path)
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
5
6
  from pathlib import Path
6
7
 
7
8
  from skcapstone.runtime import AgentRuntime
@@ -28,6 +29,26 @@ class TestAgentRuntime:
28
29
  assert manifest.name == "test-agent"
29
30
  assert manifest.last_awakened is not None
30
31
 
32
+ def test_awaken_prefers_identity_name_over_shared_config(self, tmp_path: Path):
33
+ """Agent-local identity should beat a shared-root fallback config name."""
34
+ shared_root = tmp_path / ".skcapstone"
35
+ agent_home = shared_root / "agents" / "aster"
36
+ (agent_home / "identity").mkdir(parents=True)
37
+ (agent_home / "config").mkdir(parents=True)
38
+
39
+ (shared_root / "config").mkdir(parents=True)
40
+ (shared_root / "config" / "config.yaml").write_text("agent_name: Jarvis\n")
41
+ (agent_home / "manifest.json").write_text(json.dumps({"name": "aster"}))
42
+ (agent_home / "identity" / "identity.json").write_text(json.dumps({
43
+ "name": "Aster",
44
+ "fingerprint": "A" * 40,
45
+ "capauth_managed": True,
46
+ }))
47
+
48
+ runtime = AgentRuntime(home=agent_home)
49
+ manifest = runtime.awaken()
50
+ assert manifest.name == "Aster"
51
+
31
52
  def test_register_connector(self, initialized_agent_home: Path):
32
53
  """Registering a connector should persist it."""
33
54
  runtime = AgentRuntime(home=initialized_agent_home)
@@ -225,12 +225,14 @@ class TestBuildScheduler:
225
225
  stop = threading.Event()
226
226
  scheduler = build_scheduler(tmp_path, stop)
227
227
  names = {s["name"] for s in scheduler.status()}
228
- assert names == {
228
+ # The core standard tasks must be registered (more — sync, service
229
+ # health, itil, dreaming — have since been added to build_scheduler).
230
+ assert {
229
231
  "heartbeat_pulse",
230
232
  "backend_reprobe",
231
233
  "memory_promotion_sweep",
232
234
  "profile_freshness_check",
233
- }
235
+ }.issubset(names)
234
236
 
235
237
  def test_heartbeat_interval_is_30s(self, tmp_path):
236
238
  stop = threading.Event()
@@ -292,12 +294,15 @@ class TestMemoryPromotionTask:
292
294
  with patch("skcapstone.memory_promoter.PromotionEngine", return_value=mock_engine):
293
295
  callback() # should not raise
294
296
 
295
- def test_import_error_propagates_as_exception(self, tmp_path):
296
- """If PromotionEngine raises on import the task should propagate (caught by runner)."""
297
+ def test_import_error_is_swallowed_not_propagated(self, tmp_path):
298
+ """A failing promotion sweep must NOT crash the scheduler: the sweep runs
299
+ in a background thread and catches/logs its own errors."""
300
+ import time as _time
301
+
297
302
  callback = make_memory_promotion_task(tmp_path)
298
303
  with patch("skcapstone.memory_promoter.PromotionEngine", side_effect=RuntimeError("unavailable")):
299
- with pytest.raises(RuntimeError, match="unavailable"):
300
- callback()
304
+ callback() # spawns a daemon thread; must return without raising
305
+ _time.sleep(0.1) # let the background sweep run and swallow the error
301
306
 
302
307
 
303
308
  class TestBackendReprobeTask:
@@ -0,0 +1,47 @@
1
+ """Tests for `skcapstone scheduler` CLI command group."""
2
+
3
+ import click
4
+ from click.testing import CliRunner
5
+ from skcapstone.cli.scheduler_cmd import register_scheduler_commands
6
+
7
+
8
+ def _app(tmp_path, monkeypatch):
9
+ monkeypatch.setenv("SKCAPSTONE_HOME", str(tmp_path))
10
+ (tmp_path / "config").mkdir(parents=True, exist_ok=True)
11
+ (tmp_path / "config" / "jobs.yaml").write_text(
12
+ "jobs:\n demo:\n every: 60s\n type: shell\n command: 'echo hi'\n nodes: all\n",
13
+ encoding="utf-8")
14
+
15
+ @click.group()
16
+ def main():
17
+ pass
18
+
19
+ register_scheduler_commands(main)
20
+ return main
21
+
22
+
23
+ def test_scheduler_list(tmp_path, monkeypatch):
24
+ main = _app(tmp_path, monkeypatch)
25
+ res = CliRunner().invoke(main, ["scheduler", "list"])
26
+ assert res.exit_code == 0 and "demo" in res.output
27
+
28
+
29
+ def test_scheduler_run_now(tmp_path, monkeypatch):
30
+ main = _app(tmp_path, monkeypatch)
31
+ res = CliRunner().invoke(main, ["scheduler", "run", "demo"])
32
+ assert res.exit_code == 0 and "hi" in res.output
33
+
34
+
35
+ def test_scheduler_run_unknown_job_errors(tmp_path, monkeypatch):
36
+ main = _app(tmp_path, monkeypatch)
37
+ res = CliRunner().invoke(main, ["scheduler", "run", "nope"])
38
+ assert res.exit_code != 0
39
+
40
+
41
+ def test_scheduler_disable_then_list(tmp_path, monkeypatch):
42
+ main = _app(tmp_path, monkeypatch)
43
+ assert CliRunner().invoke(main, ["scheduler", "disable", "demo"]).exit_code == 0
44
+ # after disable, jobs.yaml should mark it disabled
45
+ import yaml
46
+ data = yaml.safe_load((tmp_path / "config" / "jobs.yaml").read_text())
47
+ assert data["jobs"]["demo"]["enabled"] is False
@@ -0,0 +1,133 @@
1
+ """Tests for skscheduler reliability features (2026-06-09):
2
+ retries + backoff, jitter parsing, and the notify (sk-alert) hook."""
3
+
4
+ import threading
5
+ from contextlib import contextmanager
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+
9
+ from skcapstone import scheduled_tasks as st
10
+ from skcapstone.scheduled_tasks import TaskScheduler
11
+ from skcapstone.scheduler_jobs import JobSpec, load_jobs
12
+
13
+
14
+ # --- JobSpec / loader -------------------------------------------------------
15
+
16
+ def test_load_jobs_parses_reliability_fields(tmp_path: Path):
17
+ cfg = tmp_path / "jobs.yaml"
18
+ cfg.write_text(
19
+ "jobs:\n"
20
+ " flaky:\n"
21
+ " every: 60s\n"
22
+ " type: shell\n"
23
+ " command: 'true'\n"
24
+ " retries: 3\n"
25
+ " retry_backoff: 2.5\n"
26
+ " jitter: 30\n"
27
+ " notify: on_failure\n"
28
+ " notify_level: crit\n"
29
+ )
30
+ (job,) = load_jobs(cfg)
31
+ assert job.retries == 3
32
+ assert job.retry_backoff == 2.5
33
+ assert job.jitter == 30.0
34
+ assert job.notify == "on_failure"
35
+ assert job.notify_level == "crit"
36
+
37
+
38
+ def test_jobspec_reliability_defaults():
39
+ j = JobSpec(name="x")
40
+ assert j.retries == 0 and j.retry_backoff == 0.0 and j.jitter == 0.0
41
+ assert j.notify == "off" and j.notify_level == "warn"
42
+
43
+
44
+ # --- notify hook ------------------------------------------------------------
45
+
46
+ class _Res:
47
+ def __init__(self, ok, output="", error=""):
48
+ self.ok, self.output, self.error = ok, output, error
49
+
50
+
51
+ def _patch_alert(monkeypatch):
52
+ calls = []
53
+ monkeypatch.setattr(st.shutil, "which", lambda _n: "/usr/bin/sk-alert")
54
+ monkeypatch.setattr(st.subprocess, "run", lambda args, **k: calls.append(args) or None)
55
+ return calls
56
+
57
+
58
+ def test_notify_off_sends_nothing(monkeypatch):
59
+ calls = _patch_alert(monkeypatch)
60
+ TaskScheduler._maybe_notify(JobSpec(name="j", notify="off"), _Res(False, error="boom"), 1)
61
+ assert calls == []
62
+
63
+
64
+ def test_notify_on_failure_fires_with_tail(monkeypatch):
65
+ calls = _patch_alert(monkeypatch)
66
+ TaskScheduler._maybe_notify(
67
+ JobSpec(name="j", notify="on_failure", notify_level="crit"),
68
+ _Res(False, output="line1\nline2", error="boom"), attempts=2)
69
+ assert len(calls) == 1
70
+ args = calls[0]
71
+ assert "-l" in args and "crit" in args
72
+ assert "❌ FAILED" in args[-1] and "after 2 attempts" in args[-1]
73
+ assert "line2" in args[-1]
74
+
75
+
76
+ def test_notify_on_success_skips_failure(monkeypatch):
77
+ calls = _patch_alert(monkeypatch)
78
+ TaskScheduler._maybe_notify(JobSpec(name="j", notify="on_success"), _Res(False, error="x"), 1)
79
+ assert calls == []
80
+
81
+
82
+ def test_notify_always_fires_on_success_info_level(monkeypatch):
83
+ calls = _patch_alert(monkeypatch)
84
+ TaskScheduler._maybe_notify(JobSpec(name="j", notify="always"), _Res(True, output="done"), 1)
85
+ assert len(calls) == 1 and "info" in calls[0] and "✅ ok" in calls[0][-1]
86
+
87
+
88
+ # --- retries ----------------------------------------------------------------
89
+
90
+ def test_run_config_job_retries_until_success(monkeypatch):
91
+ seq = [_Res(False, error="e1"), _Res(False, error="e2"), _Res(True)]
92
+ n = {"runs": 0}
93
+ recorded = {}
94
+
95
+ class FakeRunner:
96
+ @contextmanager
97
+ def lock(self, job):
98
+ yield True
99
+ def run(self, job):
100
+ r = seq[n["runs"]]; n["runs"] += 1; return r
101
+
102
+ class FakeState:
103
+ def record_run(self, name, now, ok, error):
104
+ recorded.update(ok=ok, error=error, name=name)
105
+
106
+ mgr = TaskScheduler(home=Path("/tmp"), stop_event=threading.Event())
107
+ mgr._job_runner = FakeRunner()
108
+ mgr._state = FakeState()
109
+ job = JobSpec(name="flaky", retries=2, retry_backoff=0.0, jitter=0.0)
110
+ mgr._run_config_job(job, datetime.now(timezone.utc))
111
+ assert n["runs"] == 3 # failed twice, succeeded on 3rd
112
+ assert recorded["ok"] is True
113
+
114
+
115
+ def test_run_config_job_stops_after_exhausting_retries(monkeypatch):
116
+ n = {"runs": 0}; recorded = {}
117
+
118
+ class FakeRunner:
119
+ @contextmanager
120
+ def lock(self, job):
121
+ yield True
122
+ def run(self, job):
123
+ n["runs"] += 1; return _Res(False, error="always fails")
124
+
125
+ class FakeState:
126
+ def record_run(self, name, now, ok, error):
127
+ recorded.update(ok=ok)
128
+
129
+ mgr = TaskScheduler(home=Path("/tmp"), stop_event=threading.Event())
130
+ mgr._job_runner = FakeRunner(); mgr._state = FakeState()
131
+ mgr._run_config_job(JobSpec(name="bad", retries=1), datetime.now(timezone.utc))
132
+ assert n["runs"] == 2 # 1 + 1 retry
133
+ assert recorded["ok"] is False
@@ -0,0 +1,87 @@
1
+ """Integration tests for config-driven jobs in TaskScheduler.
2
+
3
+ Tests that load_config_jobs and tick_config_jobs work correctly, and that
4
+ build_scheduler picks up a jobs.yaml file from the config directory.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import threading
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+
12
+ import pytest
13
+
14
+ from skcapstone.scheduler_jobs import JobSpec
15
+ from skcapstone.scheduled_tasks import TaskScheduler
16
+
17
+
18
+ def test_due_config_job_for_this_host_fires(tmp_path: Path):
19
+ """Due jobs are dispatched to a worker thread; assert fired after the thread completes."""
20
+ sched = TaskScheduler(home=tmp_path, stop_event=threading.Event())
21
+ fired = []
22
+ done = threading.Event()
23
+ job = JobSpec(name="j", type="shell", command="true", every_seconds=1, nodes=["hostA"])
24
+ sched.load_config_jobs(jobs=[job], hostname="hostA", host_aliases={"hostA"}, state_root=tmp_path)
25
+ from skcapstone.scheduler_runner import JobResult
26
+
27
+ def _run(j):
28
+ fired.append(j.name)
29
+ done.set()
30
+ return JobResult(ok=True)
31
+
32
+ sched._job_runner.run = _run # type: ignore
33
+ sched.tick_config_jobs(now=datetime(2026, 6, 8, 12, 0, tzinfo=timezone.utc))
34
+ assert done.wait(2), "job should have run within 2s"
35
+ assert fired == ["j"]
36
+
37
+
38
+ def test_job_not_for_this_host_skipped(tmp_path: Path):
39
+ sched = TaskScheduler(home=tmp_path, stop_event=threading.Event())
40
+ fired = []
41
+ job = JobSpec(name="j", type="shell", command="true", every_seconds=1, nodes=[".41"])
42
+ sched.load_config_jobs(jobs=[job], hostname="hostB", host_aliases={"hostB"}, state_root=tmp_path)
43
+ sched._job_runner.run = lambda j: fired.append(j.name) # type: ignore
44
+ sched.tick_config_jobs()
45
+ assert fired == []
46
+
47
+
48
+ def test_build_scheduler_loads_jobs_yaml(tmp_path, monkeypatch):
49
+ cfg_dir = tmp_path / "config"; cfg_dir.mkdir()
50
+ (cfg_dir / "jobs.yaml").write_text(
51
+ "jobs:\n noop:\n every: 60s\n type: shell\n command: 'true'\n nodes: all\n",
52
+ encoding="utf-8")
53
+ from skcapstone.scheduled_tasks import build_scheduler
54
+ sched = build_scheduler(home=tmp_path, stop_event=threading.Event())
55
+ assert any(j.name == "noop" for j in sched._config_jobs)
56
+
57
+
58
+ def test_tick_does_not_block_on_slow_job(tmp_path):
59
+ """Verify that tick_config_jobs returns immediately even when a job is slow.
60
+
61
+ The job worker runs in a daemon thread; a slow job must not hold up the
62
+ scheduler tick thread (which also drives heartbeats and built-in tasks).
63
+ """
64
+ import time
65
+ from datetime import datetime, timezone
66
+
67
+ sched = TaskScheduler(home=tmp_path, stop_event=threading.Event())
68
+ job = JobSpec(name="slow", type="shell", command="true", every_seconds=1, nodes=["h"])
69
+ sched.load_config_jobs(jobs=[job], hostname="h", host_aliases={"h"}, state_root=tmp_path)
70
+ started = threading.Event()
71
+ release = threading.Event()
72
+
73
+ def slow_run(j):
74
+ started.set()
75
+ release.wait(5)
76
+ from skcapstone.scheduler_runner import JobResult
77
+ return JobResult(ok=True)
78
+
79
+ sched._job_runner.run = slow_run # type: ignore
80
+
81
+ t0 = time.monotonic()
82
+ sched.tick_config_jobs(now=datetime(2026, 6, 8, 12, 0, tzinfo=timezone.utc))
83
+ elapsed = time.monotonic() - t0
84
+
85
+ assert elapsed < 1.0, f"tick must not block on the job, but took {elapsed:.3f}s"
86
+ assert started.wait(2), "job worker should have started in background"
87
+ release.set()