@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,598 @@
1
+ """Dual-mode integration backbone harness — EPIC acceptance gate.
2
+
3
+ This module is the **system-level acceptance test** for the
4
+ sk* ⇄ skcapstone optional integration backbone (EPIC fca7f138, ADR
5
+ ``docs/ADR-optional-integration-backbone.md``).
6
+
7
+ For EVERY adapter that implements the integration contract it verifies TWO modes:
8
+
9
+ Mode A — STANDALONE
10
+ ``SK_STANDALONE=1`` is set (or ``_sdk`` patched to ``None``). The adapter
11
+ must not crash, its ``is_present()`` must return ``False``, ``alert()``
12
+ must return ``False`` and fall back to native logging, ``ensure_schedule()``
13
+ must return ``False`` without writing any files, and ``register_self()``
14
+ must return ``False``.
15
+
16
+ Mode B — INTEGRATED
17
+ skcapstone is importable and available. ``SKCAPSTONE_HOME`` is sandboxed
18
+ to ``tmp_path`` so no files touch ``~/.skcapstone``. The adapter must:
19
+ - ``is_present()`` → True
20
+ - ``alert(event, payload, level)`` → True, and
21
+ * the PubSub topic directory ``<home>/pubsub/topics/<svc>.<level>/``
22
+ must exist and contain exactly one ``msg-*.json`` whose payload
23
+ contains ``{"event": <event>, ...}`` and whose ``tags`` include the
24
+ level string (severity-based routing).
25
+ * topic suffix IS the severity (e.g. ``skmemory.error``) — not the
26
+ event name — so ``skcapstone alerts`` wildcards ``*.error`` etc.
27
+ match correctly.
28
+ - ``ensure_schedule()`` → True, and
29
+ * ``<home>/config/jobs.d/<job_name>.yaml`` must exist and be valid YAML.
30
+ - ``register_self()`` → True, and
31
+ * ``<home>/registry/<svc>.json`` must exist.
32
+
33
+ Each adapter is described by a namedtuple ``AdapterSpec``; the parametrized
34
+ test IDs use the service names so failures are readable.
35
+
36
+ LEAK CHECK: after integrated-mode tests the test verifies that no fragments
37
+ leaked to the *real* ``~/.skcapstone/config/jobs.d/`` or
38
+ ``~/.skcapstone/registry/`` directories.
39
+
40
+ Reference adapter: ``skmemory/skmemory/integration.py`` (commit be33179).
41
+ """
42
+
43
+ from __future__ import annotations
44
+
45
+ import importlib
46
+ import json
47
+ import logging
48
+ import os
49
+ import sys
50
+ from pathlib import Path
51
+ from types import ModuleType
52
+ from typing import NamedTuple, Optional
53
+
54
+ import pytest
55
+ import yaml
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # Adapter registry
59
+ # ---------------------------------------------------------------------------
60
+
61
+
62
+ class AdapterSpec(NamedTuple):
63
+ """Describes one sk* adapter under test."""
64
+
65
+ #: Human-readable service name — used as the pytest parametrize ID.
66
+ service: str
67
+ #: Python module path to the adapter (importable from the installed venv).
68
+ module_path: str
69
+ #: The adapter's job-name constant (the ``jobs.d/<name>.yaml`` key).
70
+ job_name: str
71
+ #: A representative non-critical alert level to exercise the routing.
72
+ alert_level: str = "warn"
73
+
74
+
75
+ #: All adapters that implement the integration contract.
76
+ #:
77
+ #: skchat (ad4f721a) is owned by another thread — deliberately excluded.
78
+ #: skgateway is Node/non-Python — it is tested via its own Node harness
79
+ #: (``skgateway/tests/integration.test.mjs``); excluded from Python parametrize.
80
+ ADAPTERS: list[AdapterSpec] = [
81
+ AdapterSpec(
82
+ service="skmemory",
83
+ module_path="skmemory.integration",
84
+ job_name="skmemory_sweep",
85
+ ),
86
+ AdapterSpec(
87
+ service="sksecurity",
88
+ module_path="sksecurity.integration",
89
+ job_name="sksecurity_intel_refresh",
90
+ ),
91
+ AdapterSpec(
92
+ service="skcomms",
93
+ module_path="skcomms.integration",
94
+ job_name="skcomms_health_sweep",
95
+ ),
96
+ AdapterSpec(
97
+ service="capauth",
98
+ module_path="capauth.integration",
99
+ job_name="capauth_key_rotation_check",
100
+ ),
101
+ AdapterSpec(
102
+ service="cloud9",
103
+ module_path="cloud9.integration",
104
+ job_name="cloud9_rehydration_check",
105
+ ),
106
+ AdapterSpec(
107
+ service="skvoice",
108
+ module_path="skvoice.integration",
109
+ job_name="skvoice_health",
110
+ ),
111
+ AdapterSpec(
112
+ service="skseed",
113
+ module_path="skseed.integration",
114
+ job_name="skseed_audit",
115
+ ),
116
+ ]
117
+
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # Fixtures
121
+ # ---------------------------------------------------------------------------
122
+
123
+
124
+ @pytest.fixture
125
+ def skcap_sandbox(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
126
+ """Sandbox skcapstone's home to ``tmp_path``; return the sandboxed root.
127
+
128
+ Sets both ``SKCAPSTONE_HOME`` (read by ``shared_home()``) and patches
129
+ ``skcapstone.AGENT_HOME`` (captured at import time) so every SDK write
130
+ goes to the temp tree, never to ``~/.skcapstone``.
131
+ """
132
+ monkeypatch.setenv("SKCAPSTONE_HOME", str(tmp_path))
133
+ import skcapstone as pkg
134
+
135
+ monkeypatch.setattr(pkg, "AGENT_HOME", str(tmp_path))
136
+ return tmp_path
137
+
138
+
139
+ @pytest.fixture
140
+ def real_jobs_d() -> Path:
141
+ """Return the *actual* ~/.skcapstone jobs.d path for leak detection.
142
+
143
+ Deliberately ignores SKCAPSTONE_HOME so that the leak check can verify
144
+ that integrated-mode writes go to the sandboxed path only, not to the
145
+ developer's real agent home.
146
+ """
147
+ return Path.home() / ".skcapstone" / "config" / "jobs.d"
148
+
149
+
150
+ @pytest.fixture
151
+ def real_registry() -> Path:
152
+ """Return the *actual* ~/.skcapstone registry path for leak detection."""
153
+ return Path.home() / ".skcapstone" / "registry"
154
+
155
+
156
+ # ---------------------------------------------------------------------------
157
+ # Helper: import adapter module freshly so monkeypatches take effect
158
+ # ---------------------------------------------------------------------------
159
+
160
+
161
+ def _load_adapter(spec: AdapterSpec) -> Optional[ModuleType]:
162
+ """Import the adapter module, returning None if not installed."""
163
+ try:
164
+ mod = importlib.import_module(spec.module_path)
165
+ # Force a fresh re-evaluation of the module-level ``_sdk`` guard by
166
+ # reloading. This is safe in a test context.
167
+ return importlib.reload(mod)
168
+ except ImportError:
169
+ return None
170
+
171
+
172
+ # ---------------------------------------------------------------------------
173
+ # Mode A — STANDALONE tests
174
+ # ---------------------------------------------------------------------------
175
+
176
+
177
+ @pytest.mark.parametrize("spec", ADAPTERS, ids=[a.service for a in ADAPTERS])
178
+ class TestStandaloneMode:
179
+ """Each adapter behaves correctly when skcapstone is absent / forced off.
180
+
181
+ Two sub-strategies are exercised:
182
+ 1. ``SK_STANDALONE=1`` env var forces native mode even when installed.
183
+ 2. ``_sdk`` attribute patched to ``None`` simulates absent package.
184
+ """
185
+
186
+ def test_is_present_false_when_env_standalone(
187
+ self, spec: AdapterSpec, monkeypatch: pytest.MonkeyPatch
188
+ ):
189
+ """is_present() returns False when SK_STANDALONE=1, regardless of package."""
190
+ monkeypatch.setenv("SK_STANDALONE", "1")
191
+ mod = _load_adapter(spec)
192
+ if mod is None:
193
+ pytest.skip(f"{spec.module_path} not installed")
194
+ # Reload after env is set so the module-level _sdk guard re-runs.
195
+ importlib.reload(mod)
196
+ assert mod.is_present() is False
197
+
198
+ def test_alert_returns_false_when_standalone(
199
+ self, spec: AdapterSpec, monkeypatch: pytest.MonkeyPatch
200
+ ):
201
+ """alert() returns False in standalone mode (native logging fallback)."""
202
+ monkeypatch.setenv("SK_STANDALONE", "1")
203
+ mod = _load_adapter(spec)
204
+ if mod is None:
205
+ pytest.skip(f"{spec.module_path} not installed")
206
+ importlib.reload(mod)
207
+ result = mod.alert("test_event", {"detail": "x"}, "warn")
208
+ assert result is False
209
+
210
+ def test_alert_does_not_raise_when_sdk_absent(
211
+ self, spec: AdapterSpec, monkeypatch: pytest.MonkeyPatch
212
+ ):
213
+ """alert() never raises even when _sdk is None."""
214
+ mod = _load_adapter(spec)
215
+ if mod is None:
216
+ pytest.skip(f"{spec.module_path} not installed")
217
+ monkeypatch.setattr(mod, "_sdk", None)
218
+ # Should not raise
219
+ result = mod.alert("boom", {"x": 1}, "error")
220
+ assert result is False
221
+
222
+ def test_ensure_schedule_returns_false_when_standalone(
223
+ self, spec: AdapterSpec, monkeypatch: pytest.MonkeyPatch
224
+ ):
225
+ """ensure_schedule() returns False and writes no files in standalone."""
226
+ monkeypatch.setenv("SK_STANDALONE", "1")
227
+ mod = _load_adapter(spec)
228
+ if mod is None:
229
+ pytest.skip(f"{spec.module_path} not installed")
230
+ importlib.reload(mod)
231
+ result = mod.ensure_schedule()
232
+ assert result is False
233
+
234
+ def test_ensure_schedule_does_not_raise_when_sdk_absent(
235
+ self, spec: AdapterSpec, monkeypatch: pytest.MonkeyPatch
236
+ ):
237
+ """ensure_schedule() never raises when _sdk is None."""
238
+ mod = _load_adapter(spec)
239
+ if mod is None:
240
+ pytest.skip(f"{spec.module_path} not installed")
241
+ monkeypatch.setattr(mod, "_sdk", None)
242
+ result = mod.ensure_schedule()
243
+ assert result is False
244
+
245
+ def test_register_self_returns_false_when_standalone(
246
+ self, spec: AdapterSpec, monkeypatch: pytest.MonkeyPatch
247
+ ):
248
+ """register_self() returns False in standalone mode."""
249
+ monkeypatch.setenv("SK_STANDALONE", "1")
250
+ mod = _load_adapter(spec)
251
+ if mod is None:
252
+ pytest.skip(f"{spec.module_path} not installed")
253
+ importlib.reload(mod)
254
+ result = mod.register_self()
255
+ assert result is False
256
+
257
+ def test_no_pubsub_files_written_in_standalone(
258
+ self,
259
+ spec: AdapterSpec,
260
+ tmp_path: Path,
261
+ monkeypatch: pytest.MonkeyPatch,
262
+ ):
263
+ """No pubsub topic files appear under tmp_path in standalone mode."""
264
+ monkeypatch.setenv("SK_STANDALONE", "1")
265
+ monkeypatch.setenv("SKCAPSTONE_HOME", str(tmp_path))
266
+ mod = _load_adapter(spec)
267
+ if mod is None:
268
+ pytest.skip(f"{spec.module_path} not installed")
269
+ importlib.reload(mod)
270
+ mod.alert("no_files_please", {"reason": "standalone"}, "error")
271
+ pubsub_root = tmp_path / "pubsub" / "topics"
272
+ topic_files = list(pubsub_root.glob("**/*.json")) if pubsub_root.exists() else []
273
+ assert topic_files == [], (
274
+ f"Standalone mode wrote pubsub files: {topic_files}"
275
+ )
276
+
277
+ def test_no_jobs_d_files_written_in_standalone(
278
+ self,
279
+ spec: AdapterSpec,
280
+ tmp_path: Path,
281
+ monkeypatch: pytest.MonkeyPatch,
282
+ ):
283
+ """No jobs.d files appear under tmp_path in standalone mode."""
284
+ monkeypatch.setenv("SK_STANDALONE", "1")
285
+ monkeypatch.setenv("SKCAPSTONE_HOME", str(tmp_path))
286
+ mod = _load_adapter(spec)
287
+ if mod is None:
288
+ pytest.skip(f"{spec.module_path} not installed")
289
+ importlib.reload(mod)
290
+ mod.ensure_schedule()
291
+ jobs_d = tmp_path / "config" / "jobs.d"
292
+ job_files = list(jobs_d.glob("*.yaml")) if jobs_d.exists() else []
293
+ assert job_files == [], (
294
+ f"Standalone mode wrote jobs.d files: {job_files}"
295
+ )
296
+
297
+
298
+ # ---------------------------------------------------------------------------
299
+ # Mode B — INTEGRATED tests
300
+ # ---------------------------------------------------------------------------
301
+
302
+
303
+ @pytest.mark.parametrize("spec", ADAPTERS, ids=[a.service for a in ADAPTERS])
304
+ class TestIntegratedMode:
305
+ """Each adapter routes correctly through skcapstone when present.
306
+
307
+ Uses the ``skcap_sandbox`` fixture to redirect all file writes to a
308
+ temporary directory, guaranteeing no leaks to the real home.
309
+ """
310
+
311
+ def test_is_present_true_when_integrated(
312
+ self, spec: AdapterSpec, skcap_sandbox: Path, monkeypatch: pytest.MonkeyPatch
313
+ ):
314
+ """is_present() returns True when skcapstone is installed and present."""
315
+ monkeypatch.delenv("SK_STANDALONE", raising=False)
316
+ mod = _load_adapter(spec)
317
+ if mod is None:
318
+ pytest.skip(f"{spec.module_path} not installed")
319
+ importlib.reload(mod)
320
+ assert mod.is_present() is True
321
+
322
+ def test_alert_returns_true_and_publishes_to_topic(
323
+ self, spec: AdapterSpec, skcap_sandbox: Path, monkeypatch: pytest.MonkeyPatch
324
+ ):
325
+ """alert() returns True and writes a PubSub message to <svc>.<level>/."""
326
+ monkeypatch.delenv("SK_STANDALONE", raising=False)
327
+ mod = _load_adapter(spec)
328
+ if mod is None:
329
+ pytest.skip(f"{spec.module_path} not installed")
330
+ importlib.reload(mod)
331
+
332
+ event_name = "test_event_integrated"
333
+ level = spec.alert_level
334
+ result = mod.alert(event_name, {"source": "harness"}, level)
335
+ assert result is True, (
336
+ f"{spec.service}.alert(..., level={level!r}) returned False in "
337
+ f"integrated mode — check skcap_sandbox isolation"
338
+ )
339
+
340
+ # Topic directory must exist: <home>/pubsub/topics/<svc>.<level>/
341
+ expected_topic = f"{spec.service}.{level}"
342
+ topic_dir = skcap_sandbox / "pubsub" / "topics" / expected_topic
343
+ assert topic_dir.is_dir(), (
344
+ f"Expected topic dir {topic_dir} — SDK alert did not create it. "
345
+ f"Check that the adapter uses topic '<svc>.<severity>' convention."
346
+ )
347
+
348
+ msgs = list(topic_dir.glob("msg-*.json"))
349
+ assert len(msgs) >= 1, f"No msg-*.json under {topic_dir}"
350
+
351
+ payload_data = json.loads(msgs[0].read_text())
352
+
353
+ # The event name must be in the payload.event field (ADR §4 convention)
354
+ assert "payload" in payload_data, f"Message missing 'payload' key: {payload_data}"
355
+ assert payload_data["payload"].get("event") == event_name, (
356
+ f"Payload event field mismatch — got {payload_data['payload'].get('event')!r}, "
357
+ f"expected {event_name!r}. The adapter must put the semantic event name in "
358
+ f"payload['event'], not in the topic suffix."
359
+ )
360
+
361
+ # Tags must contain the level string so skcapstone alerts wildcard routing works
362
+ assert level in payload_data.get("tags", []), (
363
+ f"Level {level!r} missing from message tags {payload_data.get('tags')!r}. "
364
+ f"skcapstone alerts subscribes to *.{level} — tags must carry the severity."
365
+ )
366
+
367
+ def test_alert_topic_uses_severity_suffix_not_event_name(
368
+ self, spec: AdapterSpec, skcap_sandbox: Path, monkeypatch: pytest.MonkeyPatch
369
+ ):
370
+ """The topic suffix IS the severity, NOT the event name.
371
+
372
+ This guards the bug that was caught during skmemory adapter work:
373
+ if the topic is ``<svc>.<event_name>`` then ``skcapstone alerts``'
374
+ ``*.error`` / ``*.warn`` wildcards never match it.
375
+ """
376
+ monkeypatch.delenv("SK_STANDALONE", raising=False)
377
+ mod = _load_adapter(spec)
378
+ if mod is None:
379
+ pytest.skip(f"{spec.module_path} not installed")
380
+ importlib.reload(mod)
381
+
382
+ level = "error"
383
+ event_name = "canary_event_xyzzy"
384
+ mod.alert(event_name, {}, level)
385
+
386
+ # Topic dir for <svc>.error MUST exist
387
+ correct_dir = skcap_sandbox / "pubsub" / "topics" / f"{spec.service}.error"
388
+ assert correct_dir.is_dir(), (
389
+ f"Topic dir {correct_dir} not created. "
390
+ f"Adapter may be using the event name as the topic suffix."
391
+ )
392
+
393
+ # Topic dir named after the event name must NOT exist
394
+ wrong_dir = skcap_sandbox / "pubsub" / "topics" / f"{spec.service}.{event_name}"
395
+ assert not wrong_dir.exists(), (
396
+ f"Adapter created wrong topic dir {wrong_dir} — "
397
+ f"event name must NOT be the topic suffix."
398
+ )
399
+
400
+ def test_ensure_schedule_returns_true_and_writes_jobs_d(
401
+ self, spec: AdapterSpec, skcap_sandbox: Path, monkeypatch: pytest.MonkeyPatch
402
+ ):
403
+ """ensure_schedule() returns True and writes <job_name>.yaml to jobs.d/."""
404
+ monkeypatch.delenv("SK_STANDALONE", raising=False)
405
+ mod = _load_adapter(spec)
406
+ if mod is None:
407
+ pytest.skip(f"{spec.module_path} not installed")
408
+ importlib.reload(mod)
409
+
410
+ result = mod.ensure_schedule()
411
+ assert result is True, (
412
+ f"{spec.service}.ensure_schedule() returned False in integrated mode"
413
+ )
414
+
415
+ jobs_d = skcap_sandbox / "config" / "jobs.d"
416
+ job_file = jobs_d / f"{spec.job_name}.yaml"
417
+ assert job_file.exists(), (
418
+ f"Expected jobs.d fragment {job_file} — ensure_schedule() did not write it"
419
+ )
420
+
421
+ job_data = yaml.safe_load(job_file.read_text())
422
+ assert isinstance(job_data, dict), f"jobs.d fragment is not valid YAML: {job_data}"
423
+
424
+ # The scheduler serialises fragments as:
425
+ # jobs:
426
+ # <job_name>:
427
+ # type: shell
428
+ # command: ...
429
+ # every: ...
430
+ # so the top-level key is "jobs" and the job name is the nested key.
431
+ assert "jobs" in job_data, (
432
+ f"jobs.d fragment missing top-level 'jobs' key: {job_data}"
433
+ )
434
+ job_entries = job_data["jobs"]
435
+ assert isinstance(job_entries, dict), (
436
+ f"jobs.d 'jobs' value is not a dict: {job_entries}"
437
+ )
438
+ assert spec.job_name in job_entries, (
439
+ f"Job name {spec.job_name!r} not found in jobs.d fragment keys: "
440
+ f"{list(job_entries.keys())}"
441
+ )
442
+ job_body = job_entries[spec.job_name]
443
+ assert "command" in job_body, f"Job body missing 'command' key: {job_body}"
444
+ # Must have either 'every' (interval) or 'schedule' (cron)
445
+ assert "every" in job_body or "schedule" in job_body, (
446
+ f"Job body has neither 'every' nor 'schedule': {job_body}"
447
+ )
448
+
449
+ def test_ensure_schedule_is_idempotent(
450
+ self, spec: AdapterSpec, skcap_sandbox: Path, monkeypatch: pytest.MonkeyPatch
451
+ ):
452
+ """Calling ensure_schedule() twice does not raise and writes one file."""
453
+ monkeypatch.delenv("SK_STANDALONE", raising=False)
454
+ mod = _load_adapter(spec)
455
+ if mod is None:
456
+ pytest.skip(f"{spec.module_path} not installed")
457
+ importlib.reload(mod)
458
+
459
+ mod.ensure_schedule()
460
+ mod.ensure_schedule() # second call must not raise
461
+
462
+ jobs_d = skcap_sandbox / "config" / "jobs.d"
463
+ job_files = list(jobs_d.glob(f"{spec.job_name}*.yaml"))
464
+ assert len(job_files) == 1, (
465
+ f"Expected 1 jobs.d file after idempotent calls, got {len(job_files)}: {job_files}"
466
+ )
467
+
468
+ def test_register_self_returns_true_and_writes_registry(
469
+ self, spec: AdapterSpec, skcap_sandbox: Path, monkeypatch: pytest.MonkeyPatch
470
+ ):
471
+ """register_self() returns True and writes <svc>.json to registry/."""
472
+ monkeypatch.delenv("SK_STANDALONE", raising=False)
473
+ mod = _load_adapter(spec)
474
+ if mod is None:
475
+ pytest.skip(f"{spec.module_path} not installed")
476
+ importlib.reload(mod)
477
+
478
+ result = mod.register_self()
479
+ assert result is True, (
480
+ f"{spec.service}.register_self() returned False in integrated mode"
481
+ )
482
+
483
+ registry = skcap_sandbox / "registry"
484
+ entry_file = registry / f"{spec.service}.json"
485
+ assert entry_file.exists(), (
486
+ f"Expected registry entry {entry_file} — register_self() did not write it"
487
+ )
488
+
489
+ entry = json.loads(entry_file.read_text())
490
+ assert entry.get("name") == spec.service, (
491
+ f"Registry entry name mismatch: {entry.get('name')!r} != {spec.service!r}"
492
+ )
493
+
494
+ def test_unregister_schedule_cleans_up(
495
+ self, spec: AdapterSpec, skcap_sandbox: Path, monkeypatch: pytest.MonkeyPatch
496
+ ):
497
+ """unregister_schedule() removes the jobs.d fragment written by ensure_schedule."""
498
+ monkeypatch.delenv("SK_STANDALONE", raising=False)
499
+ mod = _load_adapter(spec)
500
+ if mod is None:
501
+ pytest.skip(f"{spec.module_path} not installed")
502
+ importlib.reload(mod)
503
+
504
+ mod.ensure_schedule()
505
+ job_file = skcap_sandbox / "config" / "jobs.d" / f"{spec.job_name}.yaml"
506
+ assert job_file.exists(), "Precondition: ensure_schedule() should have written the file"
507
+
508
+ result = mod.unregister_schedule()
509
+ assert result is True, (
510
+ f"{spec.service}.unregister_schedule() returned False"
511
+ )
512
+ assert not job_file.exists(), (
513
+ f"jobs.d fragment {job_file} still present after unregister_schedule()"
514
+ )
515
+
516
+ def test_no_leak_to_real_home(
517
+ self,
518
+ spec: AdapterSpec,
519
+ skcap_sandbox: Path,
520
+ monkeypatch: pytest.MonkeyPatch,
521
+ real_jobs_d: Path,
522
+ real_registry: Path,
523
+ ):
524
+ """Integrated-mode writes go to sandbox only — nothing leaks to real home.
525
+
526
+ This test fails the suite if sandboxing is broken, preventing silent
527
+ pollution of the developer's actual ~/.skcapstone tree.
528
+ """
529
+ # Record files in real home BEFORE the test actions
530
+ real_jobs_before = set(real_jobs_d.glob(f"*{spec.service}*")) if real_jobs_d.exists() else set()
531
+ real_reg_before = set(real_registry.glob(f"{spec.service}*")) if real_registry.exists() else set()
532
+
533
+ monkeypatch.delenv("SK_STANDALONE", raising=False)
534
+ mod = _load_adapter(spec)
535
+ if mod is None:
536
+ pytest.skip(f"{spec.module_path} not installed")
537
+ importlib.reload(mod)
538
+
539
+ mod.alert("leak_check", {"harness": True}, "warn")
540
+ mod.ensure_schedule()
541
+ mod.register_self()
542
+
543
+ # Verify no NEW files appeared in the real home
544
+ real_jobs_after = set(real_jobs_d.glob(f"*{spec.service}*")) if real_jobs_d.exists() else set()
545
+ real_reg_after = set(real_registry.glob(f"{spec.service}*")) if real_registry.exists() else set()
546
+
547
+ new_job_files = real_jobs_after - real_jobs_before
548
+ new_reg_files = real_reg_after - real_reg_before
549
+
550
+ assert not new_job_files, (
551
+ f"LEAK: {spec.service} wrote to real jobs.d: {new_job_files}. "
552
+ f"Check that SKCAPSTONE_HOME env and skcapstone.AGENT_HOME are both "
553
+ f"patched to the sandbox path."
554
+ )
555
+ assert not new_reg_files, (
556
+ f"LEAK: {spec.service} wrote to real registry: {new_reg_files}."
557
+ )
558
+
559
+
560
+ # ---------------------------------------------------------------------------
561
+ # Contract summary: readable doc of what every adapter must satisfy
562
+ # ---------------------------------------------------------------------------
563
+
564
+
565
+ class TestAdapterContract:
566
+ """Documents the invariants every adapter must satisfy.
567
+
568
+ These are assertion-less documentation tests — they pass trivially but
569
+ serve as a machine-readable contract anchor in the test output.
570
+ """
571
+
572
+ def test_contract_standalone_invariants(self):
573
+ """STANDALONE contract: is_present→False; alert/ensure_schedule/register_self→False;
574
+ no files written; no crash."""
575
+ contract = {
576
+ "is_present": "returns False",
577
+ "alert": "returns False, logs locally, raises nothing",
578
+ "ensure_schedule": "returns False, writes no files, raises nothing",
579
+ "register_self": "returns False, raises nothing",
580
+ }
581
+ assert all(v for v in contract.values())
582
+
583
+ def test_contract_integrated_invariants(self):
584
+ """INTEGRATED contract: is_present→True; alert routes to PubSub
585
+ topic <svc>.<severity> with event in payload; ensure_schedule writes
586
+ jobs.d/<job>.yaml; register_self writes registry/<svc>.json; idempotent;
587
+ unregister removes the fragment; no leaks to real ~/.skcapstone."""
588
+ contract = {
589
+ "is_present": "returns True",
590
+ "alert_topic": "<svc>.<severity> — event name in payload.event not topic",
591
+ "alert_tags": "level string present in message tags",
592
+ "ensure_schedule": "writes jobs.d/<job>.yaml with name/command/every",
593
+ "idempotency": "safe to call ensure_schedule twice",
594
+ "register_self": "writes registry/<svc>.json with name field",
595
+ "unregister": "removes jobs.d fragment; returns True",
596
+ "no_leak": "all writes go to sandboxed SKCAPSTONE_HOME only",
597
+ }
598
+ assert all(v for v in contract.values())
@@ -0,0 +1,37 @@
1
+ """Test that resolving a problem completes its associated GTD project.
2
+
3
+ Regression test for the lifecycle leak where create_problem discarded the
4
+ GTD project id and update_problem never completed it on resolve.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+ import pytest
12
+
13
+
14
+ @pytest.fixture(autouse=True)
15
+ def _isolate_gtd_dir(tmp_path: Path, monkeypatch) -> None:
16
+ """Redirect _shared_root() so GTD files land in tmp_path, not ~/.skcapstone."""
17
+ import skcapstone.mcp_tools._helpers as _helpers
18
+
19
+ monkeypatch.setattr(_helpers, "SHARED_ROOT", str(tmp_path))
20
+
21
+
22
+ def test_resolving_problem_completes_its_gtd_project(tmp_path: Path):
23
+ from skcapstone.itil import ITILManager
24
+ from skcapstone.mcp_tools.gtd_tools import _load_list, _load_archive
25
+
26
+ mgr = ITILManager(str(tmp_path))
27
+
28
+ prb = mgr.create_problem(title="Flaky widget", managed_by="opus")
29
+ assert prb.gtd_item_ids, "problem should store its GTD project id"
30
+ assert any(p["id"] in prb.gtd_item_ids for p in _load_list("projects"))
31
+
32
+ mgr.update_problem(prb.id, agent="opus", new_status="analyzing")
33
+ mgr.update_problem(prb.id, agent="opus", new_status="resolved")
34
+
35
+ assert not any(p["id"] in prb.gtd_item_ids for p in _load_list("projects"))
36
+ archived = _load_archive()
37
+ assert any(a["id"] in prb.gtd_item_ids and a["status"] == "done" for a in archived)