@smilintux/skcapstone 0.9.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 (284) 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 +278 -1
  18. package/docs/DREAMING.md +70 -0
  19. package/docs/GETTING_STARTED.md +10 -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.daemon.plist +52 -0
  33. package/launchd/com.skcapstone.memory-compress.plist +45 -0
  34. package/launchd/com.skcapstone.skcomms-heartbeat.plist +33 -0
  35. package/launchd/com.skcapstone.skcomms-queue-drain.plist +34 -0
  36. package/launchd/install-launchd.sh +156 -0
  37. package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/src/index.ts +3 -2
  38. package/package.json +1 -1
  39. package/pyproject.toml +16 -10
  40. package/scripts/archive-sessions.sh +95 -0
  41. package/scripts/check-updates.py +4 -4
  42. package/scripts/install-bundle.sh +8 -8
  43. package/scripts/install.ps1 +12 -11
  44. package/scripts/install.sh +196 -11
  45. package/scripts/model-fallback-monitor.sh +102 -0
  46. package/scripts/notion-api.py +259 -0
  47. package/scripts/nvidia-proxy.mjs +908 -0
  48. package/scripts/proxy-monitor.sh +89 -0
  49. package/scripts/refresh-anthropic-token.sh +172 -0
  50. package/scripts/release.sh +98 -0
  51. package/scripts/session-to-memory.py +219 -0
  52. package/scripts/skgateway.mjs +856 -0
  53. package/scripts/telegram-catchup-all.sh +147 -0
  54. package/scripts/verify_install.sh +2 -2
  55. package/scripts/wargov-ufo-capture/README.md +43 -0
  56. package/scripts/wargov-ufo-capture/cdp_capture_release2.py +273 -0
  57. package/scripts/wargov-ufo-capture/cdp_capture_splc_doj.py +246 -0
  58. package/scripts/wargov-ufo-capture/cdp_finish.py +271 -0
  59. package/scripts/wargov-ufo-capture/cdp_probe.py +188 -0
  60. package/scripts/wargov-ufo-capture/cdp_splc_pressrelease.py +101 -0
  61. package/scripts/wargov-ufo-capture/parse_csv.py +95 -0
  62. package/scripts/wargov-ufo-capture/pull_dvids.sh +107 -0
  63. package/scripts/watch-anthropic-token.sh +212 -0
  64. package/scripts/windows/install-tasks.ps1 +7 -7
  65. package/scripts/windows/skcapstone-task.xml +1 -1
  66. package/src/skcapstone/__init__.py +45 -3
  67. package/src/skcapstone/_cli_monolith.py +20 -15
  68. package/src/skcapstone/activity.py +5 -1
  69. package/src/skcapstone/agent_card.py +3 -2
  70. package/src/skcapstone/api.py +41 -40
  71. package/src/skcapstone/auction.py +14 -11
  72. package/src/skcapstone/backup.py +2 -1
  73. package/src/skcapstone/blueprint_registry.py +4 -3
  74. package/src/skcapstone/blueprints/builtins/itil-operations.yaml +40 -0
  75. package/src/skcapstone/brain_first.py +238 -0
  76. package/src/skcapstone/changelog.py +1 -1
  77. package/src/skcapstone/chat.py +22 -17
  78. package/src/skcapstone/cli/__init__.py +9 -1
  79. package/src/skcapstone/cli/_common.py +1 -0
  80. package/src/skcapstone/cli/agents_spawner.py +5 -2
  81. package/src/skcapstone/cli/alerts.py +25 -4
  82. package/src/skcapstone/cli/bench.py +15 -15
  83. package/src/skcapstone/cli/chat.py +7 -4
  84. package/src/skcapstone/cli/consciousness.py +5 -2
  85. package/src/skcapstone/cli/context_cmd.py +18 -4
  86. package/src/skcapstone/cli/daemon.py +121 -42
  87. package/src/skcapstone/cli/gtd.py +26 -1
  88. package/src/skcapstone/cli/housekeeping.py +3 -3
  89. package/src/skcapstone/cli/identity_cmd.py +378 -0
  90. package/src/skcapstone/cli/joule_cmd.py +7 -3
  91. package/src/skcapstone/cli/memory.py +8 -6
  92. package/src/skcapstone/cli/peers_dir.py +1 -1
  93. package/src/skcapstone/cli/register_cmd.py +29 -3
  94. package/src/skcapstone/cli/scheduler_cmd.py +167 -0
  95. package/src/skcapstone/cli/session.py +25 -0
  96. package/src/skcapstone/cli/setup.py +96 -29
  97. package/src/skcapstone/cli/shell_cmd.py +53 -1
  98. package/src/skcapstone/cli/skills_cmd.py +2 -2
  99. package/src/skcapstone/cli/soul.py +8 -5
  100. package/src/skcapstone/cli/status.py +37 -11
  101. package/src/skcapstone/cli/telegram.py +21 -0
  102. package/src/skcapstone/cli/test_cmd.py +5 -5
  103. package/src/skcapstone/cli/test_connection.py +2 -2
  104. package/src/skcapstone/cli/upgrade_cmd.py +23 -14
  105. package/src/skcapstone/cli/version_cmd.py +1 -1
  106. package/src/skcapstone/cli/watch_cmd.py +9 -6
  107. package/src/skcapstone/cloud9_bridge.py +14 -14
  108. package/src/skcapstone/codex_setup.py +255 -0
  109. package/src/skcapstone/config_validator.py +7 -4
  110. package/src/skcapstone/consciousness_config.py +5 -1
  111. package/src/skcapstone/consciousness_loop.py +313 -273
  112. package/src/skcapstone/context_loader.py +121 -0
  113. package/src/skcapstone/coord_federation.py +2 -1
  114. package/src/skcapstone/coordination.py +23 -6
  115. package/src/skcapstone/crush_integration.py +2 -1
  116. package/src/skcapstone/daemon.py +151 -88
  117. package/src/skcapstone/dashboard.py +10 -10
  118. package/src/skcapstone/data/sk-agent-picker.sh +421 -0
  119. package/src/skcapstone/data/systemd/skcapstone-api.socket +9 -0
  120. package/src/skcapstone/data/systemd/skcapstone-memory-compress.service +18 -0
  121. package/src/skcapstone/data/systemd/skcapstone-memory-compress.timer +11 -0
  122. package/src/skcapstone/data/systemd/skcapstone.service +37 -0
  123. package/src/skcapstone/data/systemd/skcapstone@.service +50 -0
  124. package/src/skcapstone/data/systemd/skcomms-heartbeat.service +18 -0
  125. package/{systemd/skcomm-heartbeat.timer → src/skcapstone/data/systemd/skcomms-heartbeat.timer} +2 -2
  126. package/src/skcapstone/data/systemd/skcomms-queue-drain.service +17 -0
  127. package/{systemd/skcomm-queue-drain.timer → src/skcapstone/data/systemd/skcomms-queue-drain.timer} +2 -2
  128. package/src/skcapstone/defaults/claude/CLAUDE.md +67 -0
  129. package/src/skcapstone/defaults/claude/settings.json +74 -0
  130. package/src/skcapstone/defaults/lumina/config/claude-hooks.md +57 -0
  131. package/src/skcapstone/defaults/lumina/config/skgraph.yaml +55 -10
  132. package/src/skcapstone/defaults/lumina/config/skmemory.yaml +79 -13
  133. package/src/skcapstone/defaults/lumina/config/skvector.yaml +60 -9
  134. package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +2 -2
  135. package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +2 -2
  136. package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +9 -9
  137. package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +2 -2
  138. package/src/skcapstone/defaults/unhinged.json +13 -0
  139. package/src/skcapstone/discovery.py +43 -20
  140. package/src/skcapstone/doctor.py +941 -22
  141. package/src/skcapstone/dreaming.py +1183 -109
  142. package/src/skcapstone/emotion_tracker.py +2 -2
  143. package/src/skcapstone/export.py +4 -3
  144. package/src/skcapstone/fuse_mount.py +35 -25
  145. package/src/skcapstone/gui_installer.py +2 -2
  146. package/src/skcapstone/heartbeat.py +34 -30
  147. package/src/skcapstone/housekeeping.py +14 -14
  148. package/src/skcapstone/install_wizard.py +209 -7
  149. package/src/skcapstone/itil.py +13 -4
  150. package/src/skcapstone/kms_scheduler.py +10 -8
  151. package/src/skcapstone/launchd.py +426 -0
  152. package/src/skcapstone/mcp_launcher.py +15 -1
  153. package/src/skcapstone/mcp_server.py +341 -49
  154. package/src/skcapstone/mcp_tools/__init__.py +2 -0
  155. package/src/skcapstone/mcp_tools/_helpers.py +2 -2
  156. package/src/skcapstone/mcp_tools/ansible_tools.py +7 -4
  157. package/src/skcapstone/mcp_tools/brain_first_tools.py +90 -0
  158. package/src/skcapstone/mcp_tools/capauth_tools.py +7 -4
  159. package/src/skcapstone/mcp_tools/comm_tools.py +10 -10
  160. package/src/skcapstone/mcp_tools/coord_tools.py +8 -4
  161. package/src/skcapstone/mcp_tools/did_tools.py +11 -8
  162. package/src/skcapstone/mcp_tools/gtd_tools.py +4 -4
  163. package/src/skcapstone/mcp_tools/memory_tools.py +6 -2
  164. package/src/skcapstone/mcp_tools/notification_tools.py +22 -6
  165. package/src/skcapstone/mcp_tools/{skcomm_tools.py → skcomms_tools.py} +14 -14
  166. package/src/skcapstone/mcp_tools/soul_tools.py +8 -2
  167. package/src/skcapstone/mdns_discovery.py +2 -2
  168. package/src/skcapstone/memory_curator.py +1 -1
  169. package/src/skcapstone/memory_engine.py +10 -3
  170. package/src/skcapstone/metrics.py +30 -16
  171. package/src/skcapstone/migrate_memories.py +4 -3
  172. package/src/skcapstone/migrate_multi_agent.py +8 -7
  173. package/src/skcapstone/models.py +47 -5
  174. package/src/skcapstone/notifications.py +42 -18
  175. package/src/skcapstone/onboard.py +1000 -126
  176. package/src/skcapstone/operator_link.py +170 -0
  177. package/src/skcapstone/peer_directory.py +4 -4
  178. package/src/skcapstone/peers.py +19 -19
  179. package/src/skcapstone/pillars/__init__.py +7 -5
  180. package/src/skcapstone/pillars/consciousness.py +191 -0
  181. package/src/skcapstone/pillars/identity.py +51 -7
  182. package/src/skcapstone/pillars/memory.py +9 -3
  183. package/src/skcapstone/pillars/sync.py +2 -2
  184. package/src/skcapstone/preflight.py +3 -3
  185. package/src/skcapstone/providers/docker.py +28 -28
  186. package/src/skcapstone/register.py +6 -6
  187. package/src/skcapstone/registry_client.py +5 -4
  188. package/src/skcapstone/runtime.py +14 -3
  189. package/src/skcapstone/scheduled_tasks.py +254 -19
  190. package/src/skcapstone/scheduler_jobs.py +456 -0
  191. package/src/skcapstone/scheduler_runner.py +239 -0
  192. package/src/skcapstone/scheduler_state.py +162 -0
  193. package/src/skcapstone/sdk.py +310 -0
  194. package/src/skcapstone/service_health.py +279 -39
  195. package/src/skcapstone/session_briefing.py +108 -0
  196. package/src/skcapstone/session_capture.py +1 -1
  197. package/src/skcapstone/shell.py +7 -1
  198. package/src/skcapstone/soul.py +3 -1
  199. package/src/skcapstone/soul_switch.py +3 -1
  200. package/src/skcapstone/summary.py +6 -6
  201. package/src/skcapstone/sync_engine.py +15 -15
  202. package/src/skcapstone/sync_watcher.py +2 -2
  203. package/src/skcapstone/systemd.py +72 -21
  204. package/src/skcapstone/team_comms.py +8 -8
  205. package/src/skcapstone/team_engine.py +1 -1
  206. package/src/skcapstone/testrunner.py +3 -3
  207. package/src/skcapstone/trust_graph.py +40 -5
  208. package/src/skcapstone/unified_search.py +15 -6
  209. package/src/skcapstone/uninstall_wizard.py +11 -3
  210. package/src/skcapstone/version_check.py +8 -4
  211. package/src/skcapstone/warmth_anchor.py +4 -2
  212. package/src/skcapstone/whoami.py +4 -4
  213. package/systemd/skcapstone.service +4 -6
  214. package/systemd/skcapstone@.service +7 -8
  215. package/systemd/skcomms-heartbeat.service +21 -0
  216. package/systemd/skcomms-heartbeat.timer +12 -0
  217. package/systemd/skcomms-queue-drain.service +17 -0
  218. package/systemd/skcomms-queue-drain.timer +12 -0
  219. package/tests/conftest.py +39 -0
  220. package/tests/integration/test_consciousness_e2e.py +39 -39
  221. package/tests/test_agent_card.py +1 -1
  222. package/tests/test_agent_home_scaffold.py +34 -0
  223. package/tests/test_alerts_consumer_topics.py +27 -0
  224. package/tests/test_backup.py +2 -1
  225. package/tests/test_chat.py +6 -6
  226. package/tests/test_claude_md.py +2 -2
  227. package/tests/test_cli_skills.py +10 -10
  228. package/tests/test_cli_test_cmd.py +4 -4
  229. package/tests/test_cli_test_connection.py +1 -1
  230. package/tests/test_cloud9_bridge.py +6 -6
  231. package/tests/test_consciousness_e2e.py +1 -1
  232. package/tests/test_consciousness_loop.py +10 -10
  233. package/tests/test_coordination.py +25 -0
  234. package/tests/test_cross_package.py +21 -21
  235. package/tests/test_daemon.py +4 -4
  236. package/tests/test_daemon_shutdown.py +1 -1
  237. package/tests/test_docker_provider.py +29 -29
  238. package/tests/test_doctor.py +400 -0
  239. package/tests/test_doctor_skscheduler.py +50 -0
  240. package/tests/test_dreaming_engine.py +147 -0
  241. package/tests/test_dreaming_gtd_capture.py +35 -0
  242. package/tests/test_e2e_automated.py +8 -5
  243. package/tests/test_fuse_mount.py +10 -10
  244. package/tests/test_gtd_brief.py +46 -0
  245. package/tests/test_gtd_malformed_tolerance.py +31 -0
  246. package/tests/test_housekeeping.py +15 -15
  247. package/tests/test_identity_migrate.py +251 -0
  248. package/tests/test_integration_backbone.py +598 -0
  249. package/tests/test_itil_gtd_lifecycle.py +37 -0
  250. package/tests/test_jobs_dropins.py +84 -0
  251. package/tests/test_mcp_server.py +82 -37
  252. package/tests/test_models.py +48 -4
  253. package/tests/test_multi_agent.py +31 -29
  254. package/tests/test_notifications.py +122 -32
  255. package/tests/test_onboard.py +63 -75
  256. package/tests/test_operator_link.py +78 -0
  257. package/tests/test_peers.py +14 -14
  258. package/tests/test_pillars.py +98 -0
  259. package/tests/test_preflight.py +3 -3
  260. package/tests/test_runtime.py +21 -0
  261. package/tests/test_scheduled_tasks.py +11 -6
  262. package/tests/test_scheduler_cli.py +47 -0
  263. package/tests/test_scheduler_features.py +133 -0
  264. package/tests/test_scheduler_integration.py +87 -0
  265. package/tests/test_scheduler_jobs.py +155 -0
  266. package/tests/test_scheduler_runner.py +64 -0
  267. package/tests/test_scheduler_state.py +57 -0
  268. package/tests/test_sdk.py +70 -0
  269. package/tests/test_service_health_incidents.py +34 -0
  270. package/tests/test_service_registry.py +52 -0
  271. package/tests/test_session_briefing.py +130 -0
  272. package/tests/test_snapshots.py +4 -4
  273. package/tests/test_sync_pipeline.py +26 -26
  274. package/tests/test_team_comms.py +2 -2
  275. package/tests/test_testrunner.py +2 -2
  276. package/tests/test_trust_graph.py +18 -0
  277. package/tests/test_unified_search.py +2 -2
  278. package/tests/test_version_check.py +10 -0
  279. package/tests/test_version_cmd.py +8 -8
  280. package/tests/test_whoami.py +1 -1
  281. package/systemd/skcomm-heartbeat.service +0 -18
  282. package/systemd/skcomm-queue-drain.service +0 -17
  283. /package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/package.json +0 -0
  284. /package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/src/openclaw.plugin.json +0 -0
@@ -7,13 +7,31 @@ from unittest.mock import MagicMock, call, patch
7
7
 
8
8
  import pytest
9
9
 
10
- from skcapstone.notifications import NotificationManager, notify, get_manager
10
+ from skcapstone.notifications import (
11
+ NotificationManager,
12
+ desktop_notifications_enabled,
13
+ notify,
14
+ get_manager,
15
+ )
16
+
17
+
18
+ @pytest.fixture(autouse=True)
19
+ def _enable_desktop_notifications(monkeypatch):
20
+ """Re-enable the desktop-notification guard for this module.
21
+
22
+ The session-wide conftest fixture disables notifications so test runs
23
+ don't flood the live desktop. Every test here mocks ``subprocess.run`` /
24
+ ``osascript``, so nothing real is dispatched — they just need the guard
25
+ on to exercise the dispatch logic. Guard-specific tests override this.
26
+ """
27
+ monkeypatch.setenv("SKCAPSTONE_DESKTOP_NOTIFY", "1")
11
28
 
12
29
 
13
30
  # ---------------------------------------------------------------------------
14
31
  # Helpers
15
32
  # ---------------------------------------------------------------------------
16
33
 
34
+
17
35
  def _make_mgr(debounce: float = 0.0) -> NotificationManager:
18
36
  """Return a fresh NotificationManager with zero debounce by default."""
19
37
  return NotificationManager(debounce_seconds=debounce)
@@ -23,14 +41,17 @@ def _make_mgr(debounce: float = 0.0) -> NotificationManager:
23
41
  # notify-send (Linux)
24
42
  # ---------------------------------------------------------------------------
25
43
 
44
+
26
45
  class TestNotifyLinux:
27
46
  """Tests for Linux notify-send path."""
28
47
 
29
48
  def test_notify_send_called_with_correct_args(self):
30
49
  """notify-send is invoked with urgency, title, body."""
31
50
  mgr = _make_mgr()
32
- with patch("skcapstone.notifications.platform.system", return_value="Linux"), \
33
- patch("skcapstone.notifications.subprocess.run") as mock_run:
51
+ with (
52
+ patch("skcapstone.notifications.platform.system", return_value="Linux"),
53
+ patch("skcapstone.notifications.subprocess.run") as mock_run,
54
+ ):
34
55
  mock_run.return_value = MagicMock(returncode=0)
35
56
  result = mgr.notify("Hello", "World", urgency="normal")
36
57
 
@@ -45,8 +66,10 @@ class TestNotifyLinux:
45
66
  def test_notify_send_urgency_low(self):
46
67
  """Low urgency maps to notify-send --urgency low."""
47
68
  mgr = _make_mgr()
48
- with patch("skcapstone.notifications.platform.system", return_value="Linux"), \
49
- patch("skcapstone.notifications.subprocess.run") as mock_run:
69
+ with (
70
+ patch("skcapstone.notifications.platform.system", return_value="Linux"),
71
+ patch("skcapstone.notifications.subprocess.run") as mock_run,
72
+ ):
50
73
  mock_run.return_value = MagicMock(returncode=0)
51
74
  mgr.notify("T", "B", urgency="low")
52
75
 
@@ -57,8 +80,10 @@ class TestNotifyLinux:
57
80
  def test_notify_send_urgency_critical(self):
58
81
  """Critical urgency maps to notify-send --urgency critical."""
59
82
  mgr = _make_mgr()
60
- with patch("skcapstone.notifications.platform.system", return_value="Linux"), \
61
- patch("skcapstone.notifications.subprocess.run") as mock_run:
83
+ with (
84
+ patch("skcapstone.notifications.platform.system", return_value="Linux"),
85
+ patch("skcapstone.notifications.subprocess.run") as mock_run,
86
+ ):
62
87
  mock_run.return_value = MagicMock(returncode=0)
63
88
  mgr.notify("T", "B", urgency="critical")
64
89
 
@@ -68,8 +93,10 @@ class TestNotifyLinux:
68
93
  def test_notify_send_not_found_returns_false(self):
69
94
  """Returns False gracefully when notify-send binary is missing."""
70
95
  mgr = _make_mgr()
71
- with patch("skcapstone.notifications.platform.system", return_value="Linux"), \
72
- patch("skcapstone.notifications.subprocess.run", side_effect=FileNotFoundError):
96
+ with (
97
+ patch("skcapstone.notifications.platform.system", return_value="Linux"),
98
+ patch("skcapstone.notifications.subprocess.run", side_effect=FileNotFoundError),
99
+ ):
73
100
  result = mgr.notify("T", "B")
74
101
 
75
102
  assert result is False
@@ -77,12 +104,15 @@ class TestNotifyLinux:
77
104
  def test_notify_send_nonzero_exit_returns_false(self):
78
105
  """Returns False when notify-send exits non-zero."""
79
106
  import subprocess
107
+
80
108
  mgr = _make_mgr()
81
- with patch("skcapstone.notifications.platform.system", return_value="Linux"), \
82
- patch(
83
- "skcapstone.notifications.subprocess.run",
84
- side_effect=subprocess.CalledProcessError(1, "notify-send", stderr=b"err"),
85
- ):
109
+ with (
110
+ patch("skcapstone.notifications.platform.system", return_value="Linux"),
111
+ patch(
112
+ "skcapstone.notifications.subprocess.run",
113
+ side_effect=subprocess.CalledProcessError(1, "notify-send", stderr=b"err"),
114
+ ),
115
+ ):
86
116
  result = mgr.notify("T", "B")
87
117
 
88
118
  assert result is False
@@ -90,12 +120,15 @@ class TestNotifyLinux:
90
120
  def test_notify_send_timeout_returns_false(self):
91
121
  """Returns False when notify-send times out."""
92
122
  import subprocess
123
+
93
124
  mgr = _make_mgr()
94
- with patch("skcapstone.notifications.platform.system", return_value="Linux"), \
95
- patch(
96
- "skcapstone.notifications.subprocess.run",
97
- side_effect=subprocess.TimeoutExpired("notify-send", 5),
98
- ):
125
+ with (
126
+ patch("skcapstone.notifications.platform.system", return_value="Linux"),
127
+ patch(
128
+ "skcapstone.notifications.subprocess.run",
129
+ side_effect=subprocess.TimeoutExpired("notify-send", 5),
130
+ ),
131
+ ):
99
132
  result = mgr.notify("T", "B")
100
133
 
101
134
  assert result is False
@@ -105,6 +138,7 @@ class TestNotifyLinux:
105
138
  # osascript (macOS)
106
139
  # ---------------------------------------------------------------------------
107
140
 
141
+
108
142
  class TestNotifyMacOS:
109
143
  """Tests for macOS osascript path."""
110
144
 
@@ -159,6 +193,7 @@ class TestNotifyMacOS:
159
193
  # Unsupported platform
160
194
  # ---------------------------------------------------------------------------
161
195
 
196
+
162
197
  class TestNotifyUnsupportedPlatform:
163
198
  """Windows and unknown platforms return False without error."""
164
199
 
@@ -185,14 +220,17 @@ class TestNotifyUnsupportedPlatform:
185
220
  # Debounce logic
186
221
  # ---------------------------------------------------------------------------
187
222
 
223
+
188
224
  class TestDebounce:
189
225
  """Debounce prevents more than one notification per interval."""
190
226
 
191
227
  def test_second_call_within_window_is_debounced(self):
192
228
  """A second notify() within the debounce window returns False."""
193
229
  mgr = NotificationManager(debounce_seconds=5.0)
194
- with patch("skcapstone.notifications.platform.system", return_value="Linux"), \
195
- patch("skcapstone.notifications.subprocess.run") as mock_run:
230
+ with (
231
+ patch("skcapstone.notifications.platform.system", return_value="Linux"),
232
+ patch("skcapstone.notifications.subprocess.run") as mock_run,
233
+ ):
196
234
  mock_run.return_value = MagicMock(returncode=0)
197
235
  first = mgr.notify("T", "B")
198
236
  second = mgr.notify("T", "B")
@@ -204,8 +242,10 @@ class TestDebounce:
204
242
  def test_call_after_window_is_allowed(self):
205
243
  """A notify() after the debounce window passes through."""
206
244
  mgr = NotificationManager(debounce_seconds=0.05)
207
- with patch("skcapstone.notifications.platform.system", return_value="Linux"), \
208
- patch("skcapstone.notifications.subprocess.run") as mock_run:
245
+ with (
246
+ patch("skcapstone.notifications.platform.system", return_value="Linux"),
247
+ patch("skcapstone.notifications.subprocess.run") as mock_run,
248
+ ):
209
249
  mock_run.return_value = MagicMock(returncode=0)
210
250
  mgr.notify("T", "B")
211
251
  time.sleep(0.1)
@@ -217,13 +257,17 @@ class TestDebounce:
217
257
  def test_debounce_does_not_update_timestamp_on_failed_dispatch(self):
218
258
  """Failed dispatch (notify-send missing) does not reset the debounce clock."""
219
259
  mgr = NotificationManager(debounce_seconds=5.0)
220
- with patch("skcapstone.notifications.platform.system", return_value="Linux"), \
221
- patch("skcapstone.notifications.subprocess.run", side_effect=FileNotFoundError):
260
+ with (
261
+ patch("skcapstone.notifications.platform.system", return_value="Linux"),
262
+ patch("skcapstone.notifications.subprocess.run", side_effect=FileNotFoundError),
263
+ ):
222
264
  mgr.notify("T", "B") # fails → _last_sent stays 0
223
265
 
224
266
  # Now try again immediately — should not be debounced
225
- with patch("skcapstone.notifications.platform.system", return_value="Linux"), \
226
- patch("skcapstone.notifications.subprocess.run") as mock_run2:
267
+ with (
268
+ patch("skcapstone.notifications.platform.system", return_value="Linux"),
269
+ patch("skcapstone.notifications.subprocess.run") as mock_run2,
270
+ ):
227
271
  mock_run2.return_value = MagicMock(returncode=0)
228
272
  result = mgr.notify("T", "B")
229
273
 
@@ -232,8 +276,10 @@ class TestDebounce:
232
276
  def test_zero_debounce_allows_rapid_calls(self):
233
277
  """debounce_seconds=0 means every call is dispatched."""
234
278
  mgr = NotificationManager(debounce_seconds=0)
235
- with patch("skcapstone.notifications.platform.system", return_value="Linux"), \
236
- patch("skcapstone.notifications.subprocess.run") as mock_run:
279
+ with (
280
+ patch("skcapstone.notifications.platform.system", return_value="Linux"),
281
+ patch("skcapstone.notifications.subprocess.run") as mock_run,
282
+ ):
237
283
  mock_run.return_value = MagicMock(returncode=0)
238
284
  for _ in range(3):
239
285
  mgr.notify("T", "B")
@@ -245,17 +291,21 @@ class TestDebounce:
245
291
  # Module-level helpers
246
292
  # ---------------------------------------------------------------------------
247
293
 
294
+
248
295
  class TestModuleLevelHelpers:
249
296
  """notify() convenience function and get_manager() singleton."""
250
297
 
251
298
  def test_notify_convenience_function(self):
252
299
  """Module-level notify() delegates to the singleton manager."""
253
- with patch("skcapstone.notifications.platform.system", return_value="Linux"), \
254
- patch("skcapstone.notifications.subprocess.run") as mock_run, \
255
- patch("skcapstone.notifications._manager", None):
300
+ with (
301
+ patch("skcapstone.notifications.platform.system", return_value="Linux"),
302
+ patch("skcapstone.notifications.subprocess.run") as mock_run,
303
+ patch("skcapstone.notifications._manager", None),
304
+ ):
256
305
  mock_run.return_value = MagicMock(returncode=0)
257
306
  # Reset singleton so debounce is fresh
258
307
  import skcapstone.notifications as _notif_mod
308
+
259
309
  _notif_mod._manager = None
260
310
  result = notify("Hello", "World")
261
311
 
@@ -264,7 +314,47 @@ class TestModuleLevelHelpers:
264
314
  def test_get_manager_returns_singleton(self):
265
315
  """get_manager() returns the same instance on repeated calls."""
266
316
  import skcapstone.notifications as _notif_mod
317
+
267
318
  _notif_mod._manager = None # reset
268
319
  m1 = get_manager()
269
320
  m2 = get_manager()
270
321
  assert m1 is m2
322
+
323
+
324
+ # ---------------------------------------------------------------------------
325
+ # Desktop-notification guard (SKCAPSTONE_DESKTOP_NOTIFY)
326
+ # ---------------------------------------------------------------------------
327
+
328
+
329
+ class TestDesktopNotificationGuard:
330
+ """SKCAPSTONE_DESKTOP_NOTIFY suppresses every dispatch path."""
331
+
332
+ def test_enabled_by_default(self, monkeypatch):
333
+ """Unset env var means notifications are enabled."""
334
+ monkeypatch.delenv("SKCAPSTONE_DESKTOP_NOTIFY", raising=False)
335
+ assert desktop_notifications_enabled() is True
336
+
337
+ @pytest.mark.parametrize("value", ["0", "false", "no", "off", "silent", "null", "none"])
338
+ def test_disabled_values(self, monkeypatch, value):
339
+ """Recognised falsy values disable notifications (case-insensitive)."""
340
+ monkeypatch.setenv("SKCAPSTONE_DESKTOP_NOTIFY", value.upper())
341
+ assert desktop_notifications_enabled() is False
342
+
343
+ @pytest.mark.parametrize("value", ["1", "true", "yes", "on"])
344
+ def test_enabled_values(self, monkeypatch, value):
345
+ """Other values keep notifications enabled."""
346
+ monkeypatch.setenv("SKCAPSTONE_DESKTOP_NOTIFY", value)
347
+ assert desktop_notifications_enabled() is True
348
+
349
+ def test_notify_short_circuits_when_disabled(self, monkeypatch):
350
+ """notify() returns False and never shells out when disabled."""
351
+ monkeypatch.setenv("SKCAPSTONE_DESKTOP_NOTIFY", "0")
352
+ mgr = _make_mgr()
353
+ with (
354
+ patch("skcapstone.notifications.platform.system", return_value="Linux"),
355
+ patch("skcapstone.notifications.subprocess.run") as mock_run,
356
+ ):
357
+ result = mgr.notify("Hello", "World")
358
+
359
+ assert result is False
360
+ mock_run.assert_not_called()
@@ -7,7 +7,7 @@ Covers:
7
7
  - _step_systemd_service(): Linux-only, click.confirm gating
8
8
  - _step_doctor_check(): doctor diagnostics output
9
9
  - _step_test_consciousness(): consciousness loop test with click.confirm gating
10
- - TOTAL_STEPS constant updated to 13
10
+ - TOTAL_STEPS constant updated to 16
11
11
  """
12
12
 
13
13
  from __future__ import annotations
@@ -42,13 +42,13 @@ def tmp_home(tmp_path: Path) -> Path:
42
42
 
43
43
 
44
44
  class TestTotalSteps:
45
- """Ensure TOTAL_STEPS was updated to 13."""
45
+ """Ensure TOTAL_STEPS reflects the current 16-step wizard."""
46
46
 
47
- def test_total_steps_is_13(self) -> None:
48
- """Wizard now has 13 numbered steps."""
47
+ def test_total_steps_is_16(self) -> None:
48
+ """Wizard now has 16 numbered steps."""
49
49
  from skcapstone.onboard import TOTAL_STEPS
50
50
 
51
- assert TOTAL_STEPS == 13
51
+ assert TOTAL_STEPS == 16
52
52
 
53
53
 
54
54
  # ---------------------------------------------------------------------------
@@ -152,71 +152,77 @@ class TestStepOllamaModels:
152
152
  """Tests for _step_ollama_models()."""
153
153
 
154
154
  def test_skips_when_ollama_not_available(self) -> None:
155
- """Returns False immediately when prereqs['ollama'] is False."""
155
+ """Returns default structured result when prereqs['ollama'] is False."""
156
156
  from skcapstone.onboard import _step_ollama_models
157
157
 
158
158
  result = _step_ollama_models({"ollama": False})
159
159
 
160
- assert result is False
160
+ assert result == {
161
+ "ok": False,
162
+ "model": "llama3.2",
163
+ "host": "http://localhost:11434",
164
+ }
161
165
 
162
166
  def test_skips_when_user_declines(self) -> None:
163
- """Returns False when user does not confirm the pull."""
167
+ """Returns a non-ok result when user does not confirm the pull."""
164
168
  from skcapstone.onboard import _step_ollama_models
165
169
 
166
- def fake_which(t: str) -> str | None:
167
- return "/usr/local/bin/ollama" if t == "ollama" else None
168
-
169
- with patch("shutil.which", side_effect=fake_which), \
170
- patch("subprocess.run", return_value=MagicMock(returncode=0, stdout="")), \
170
+ with patch("subprocess.run", return_value=MagicMock(returncode=0, stdout="")), \
171
+ patch("click.prompt", side_effect=["http://localhost:11434", "llama3.2"]), \
171
172
  patch("click.confirm", return_value=False):
172
173
  result = _step_ollama_models({"ollama": True})
173
174
 
174
- assert result is False
175
+ assert result == {
176
+ "ok": False,
177
+ "model": "llama3.2",
178
+ "host": "http://localhost:11434",
179
+ }
175
180
 
176
181
  def test_returns_true_when_model_already_present(self) -> None:
177
- """Returns True without pulling if model already in 'ollama list'."""
182
+ """Returns ok=True without pulling if model already in 'ollama list'."""
178
183
  from skcapstone.onboard import _step_ollama_models
179
184
 
180
- with patch("subprocess.run", return_value=MagicMock(returncode=0, stdout="llama3.2 2.0 GB")):
185
+ with patch("subprocess.run", return_value=MagicMock(returncode=0, stdout="NAME ID SIZE\nllama3.2 abc123 2.0 GB")), \
186
+ patch("click.prompt", side_effect=["http://localhost:11434", "llama3.2"]):
181
187
  result = _step_ollama_models({"ollama": True})
182
188
 
183
- assert result is True
189
+ assert result["ok"] is True
190
+ assert result["model"] == "llama3.2"
184
191
 
185
192
  def test_returns_true_on_successful_pull(self) -> None:
186
- """Returns True after a successful ollama pull."""
193
+ """Returns ok=True after a successful ollama pull."""
187
194
  from skcapstone.onboard import _step_ollama_models
188
195
 
189
- call_count = {"n": 0}
190
-
191
196
  def fake_run(cmd, **kwargs):
192
- call_count["n"] += 1
193
197
  if "list" in cmd:
194
- return MagicMock(returncode=0, stdout="") # model not present
195
- return MagicMock(returncode=0) # pull succeeds
198
+ return MagicMock(returncode=0, stdout="")
199
+ return MagicMock(returncode=0)
196
200
 
197
201
  with patch("subprocess.run", side_effect=fake_run), \
202
+ patch("click.prompt", side_effect=["http://localhost:11434", "llama3.2"]), \
198
203
  patch("click.confirm", return_value=True):
199
204
  result = _step_ollama_models({"ollama": True})
200
205
 
201
- assert result is True
206
+ assert result["ok"] is True
202
207
 
203
208
  def test_returns_false_on_pull_failure(self) -> None:
204
- """Returns False when ollama pull exits non-zero."""
209
+ """Returns ok=False when ollama pull exits non-zero."""
205
210
  from skcapstone.onboard import _step_ollama_models
206
211
 
207
212
  def fake_run(cmd, **kwargs):
208
213
  if "list" in cmd:
209
214
  return MagicMock(returncode=0, stdout="")
210
- return MagicMock(returncode=1) # pull fails
215
+ return MagicMock(returncode=1)
211
216
 
212
217
  with patch("subprocess.run", side_effect=fake_run), \
218
+ patch("click.prompt", side_effect=["http://localhost:11434", "llama3.2"]), \
213
219
  patch("click.confirm", return_value=True):
214
220
  result = _step_ollama_models({"ollama": True})
215
221
 
216
- assert result is False
222
+ assert result["ok"] is False
217
223
 
218
224
  def test_returns_false_on_timeout(self) -> None:
219
- """Returns False when ollama pull times out."""
225
+ """Returns ok=False when ollama pull times out."""
220
226
  from skcapstone.onboard import _step_ollama_models
221
227
 
222
228
  def fake_run(cmd, **kwargs):
@@ -225,10 +231,11 @@ class TestStepOllamaModels:
225
231
  raise subprocess.TimeoutExpired(cmd, 600)
226
232
 
227
233
  with patch("subprocess.run", side_effect=fake_run), \
234
+ patch("click.prompt", side_effect=["http://localhost:11434", "llama3.2"]), \
228
235
  patch("click.confirm", return_value=True):
229
236
  result = _step_ollama_models({"ollama": True})
230
237
 
231
- assert result is False
238
+ assert result["ok"] is False
232
239
 
233
240
 
234
241
  # ---------------------------------------------------------------------------
@@ -306,59 +313,55 @@ class TestStepConfigFiles:
306
313
 
307
314
 
308
315
  class TestStepSystemdService:
309
- """Tests for _step_systemd_service()."""
316
+ """Tests for Linux systemd and platform dispatch."""
310
317
 
311
318
  def test_returns_false_on_non_linux(self) -> None:
312
- """Returns False immediately on non-Linux platforms."""
313
- from skcapstone.onboard import _step_systemd_service
319
+ """Returns False immediately on unsupported non-Linux/macOS platforms."""
320
+ from skcapstone.onboard import _step_autostart_service
314
321
 
315
- with patch("platform.system", return_value="Darwin"):
316
- result = _step_systemd_service()
322
+ with patch("platform.system", return_value="Windows"):
323
+ result = _step_autostart_service()
317
324
 
318
325
  assert result is False
319
326
 
320
327
  def test_returns_false_when_user_declines(self) -> None:
321
328
  """Returns False when user does not confirm the install."""
322
- from skcapstone.onboard import _step_systemd_service
329
+ from skcapstone.onboard import _step_systemd_service_linux
323
330
 
324
- with patch("platform.system", return_value="Linux"), \
325
- patch("click.confirm", return_value=False):
326
- result = _step_systemd_service()
331
+ with patch("click.confirm", return_value=False):
332
+ result = _step_systemd_service_linux()
327
333
 
328
334
  assert result is False
329
335
 
330
336
  def test_returns_false_when_systemd_unavailable(self) -> None:
331
337
  """Returns False when systemd user session is not running."""
332
- from skcapstone.onboard import _step_systemd_service
338
+ from skcapstone.onboard import _step_systemd_service_linux
333
339
 
334
- with patch("platform.system", return_value="Linux"), \
335
- patch("click.confirm", return_value=True), \
340
+ with patch("click.confirm", return_value=True), \
336
341
  patch("skcapstone.systemd.systemd_available", return_value=False):
337
- result = _step_systemd_service()
342
+ result = _step_systemd_service_linux()
338
343
 
339
344
  assert result is False
340
345
 
341
346
  def test_returns_true_on_successful_install(self) -> None:
342
347
  """Returns True when systemd install succeeds."""
343
- from skcapstone.onboard import _step_systemd_service
348
+ from skcapstone.onboard import _step_systemd_service_linux
344
349
 
345
- with patch("platform.system", return_value="Linux"), \
346
- patch("click.confirm", return_value=True), \
350
+ with patch("click.confirm", return_value=True), \
347
351
  patch("skcapstone.systemd.systemd_available", return_value=True), \
348
352
  patch("skcapstone.systemd.install_service", return_value={"installed": True, "enabled": True}):
349
- result = _step_systemd_service()
353
+ result = _step_systemd_service_linux()
350
354
 
351
355
  assert result is True
352
356
 
353
357
  def test_returns_false_on_install_failure(self) -> None:
354
358
  """Returns False when install_service reports not installed."""
355
- from skcapstone.onboard import _step_systemd_service
359
+ from skcapstone.onboard import _step_systemd_service_linux
356
360
 
357
- with patch("platform.system", return_value="Linux"), \
358
- patch("click.confirm", return_value=True), \
361
+ with patch("click.confirm", return_value=True), \
359
362
  patch("skcapstone.systemd.systemd_available", return_value=True), \
360
363
  patch("skcapstone.systemd.install_service", return_value={"installed": False}):
361
- result = _step_systemd_service()
364
+ result = _step_systemd_service_linux()
362
365
 
363
366
  assert result is False
364
367
 
@@ -419,54 +422,39 @@ class TestStepTestConsciousness:
419
422
  assert result is False
420
423
 
421
424
  def test_returns_true_when_loop_responds(self, tmp_home: Path) -> None:
422
- """Returns True when LLMBridge.generate returns a non-empty response."""
425
+ """Returns True when the configured Ollama callback yields a response."""
423
426
  from skcapstone.onboard import _step_test_consciousness
424
427
 
425
- mock_bridge = MagicMock()
426
- mock_bridge.generate.return_value = "Hello, I am running fine."
427
- mock_builder = MagicMock()
428
- mock_builder.build.return_value = "system prompt"
429
- mock_config = MagicMock()
430
- mock_config.max_context_tokens = 8000
428
+ mock_config = MagicMock(ollama_model="llama3.2", ollama_host="http://localhost:11434")
429
+ mock_callback = MagicMock(return_value="Hello, I am running fine.")
431
430
 
432
431
  with patch("click.confirm", return_value=True), \
433
432
  patch("skcapstone.consciousness_config.load_consciousness_config", return_value=mock_config), \
434
- patch("skcapstone.consciousness_loop.LLMBridge", return_value=mock_bridge), \
435
- patch("skcapstone.consciousness_loop.SystemPromptBuilder", return_value=mock_builder), \
436
- patch("skcapstone.consciousness_loop._classify_message",
437
- return_value=MagicMock(tags=[], estimated_tokens=10)):
433
+ patch("skseed.llm.ollama_callback", return_value=mock_callback):
438
434
  result = _step_test_consciousness(tmp_home)
439
435
 
440
436
  assert result is True
441
437
 
442
438
  def test_returns_false_when_loop_returns_empty(self, tmp_home: Path) -> None:
443
- """Returns False when LLMBridge.generate returns an empty string."""
439
+ """Returns False when the configured Ollama callback yields an empty string."""
444
440
  from skcapstone.onboard import _step_test_consciousness
445
441
 
446
- mock_bridge = MagicMock()
447
- mock_bridge.generate.return_value = ""
448
- mock_builder = MagicMock()
449
- mock_builder.build.return_value = "system prompt"
450
- mock_config = MagicMock()
451
- mock_config.max_context_tokens = 8000
442
+ mock_config = MagicMock(ollama_model="llama3.2", ollama_host="http://localhost:11434")
443
+ mock_callback = MagicMock(return_value="")
452
444
 
453
445
  with patch("click.confirm", return_value=True), \
454
446
  patch("skcapstone.consciousness_config.load_consciousness_config", return_value=mock_config), \
455
- patch("skcapstone.consciousness_loop.LLMBridge", return_value=mock_bridge), \
456
- patch("skcapstone.consciousness_loop.SystemPromptBuilder", return_value=mock_builder), \
457
- patch("skcapstone.consciousness_loop._classify_message",
458
- return_value=MagicMock(tags=[], estimated_tokens=10)):
447
+ patch("skseed.llm.ollama_callback", return_value=mock_callback):
459
448
  result = _step_test_consciousness(tmp_home)
460
449
 
461
450
  assert result is False
462
451
 
463
452
  def test_returns_false_on_exception(self, tmp_home: Path) -> None:
464
- """Returns False when consciousness config raises an exception."""
453
+ """Returns False when the Ollama callback raises an exception."""
465
454
  from skcapstone.onboard import _step_test_consciousness
466
455
 
467
456
  with patch("click.confirm", return_value=True), \
468
- patch("skcapstone.consciousness_config.load_consciousness_config",
469
- side_effect=RuntimeError("no config")):
457
+ patch("skseed.llm.ollama_callback", side_effect=RuntimeError("backend down")):
470
458
  result = _step_test_consciousness(tmp_home)
471
459
 
472
460
  assert result is False
@@ -0,0 +1,78 @@
1
+ """Tests for human-operator manifest linking."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from skcapstone.operator_link import build_agent_manifest, discover_human_operator
9
+
10
+
11
+ def test_discover_human_operator_reads_capauth_profile(tmp_path: Path) -> None:
12
+ """A CapAuth human profile is converted into operator metadata."""
13
+ capauth_home = tmp_path / ".capauth"
14
+ profile = capauth_home / "identity" / "profile.json"
15
+ profile.parent.mkdir(parents=True)
16
+ profile.write_text(
17
+ json.dumps(
18
+ {
19
+ "entity": {
20
+ "name": "Casey",
21
+ "entity_type": "human",
22
+ "email": "casey@example.com",
23
+ "handle": "casey@example.com",
24
+ },
25
+ "key_info": {
26
+ "fingerprint": "ABCDEF1234567890",
27
+ },
28
+ }
29
+ ),
30
+ encoding="utf-8",
31
+ )
32
+
33
+ operator = discover_human_operator(capauth_home)
34
+
35
+ assert operator == {
36
+ "name": "Casey",
37
+ "relationship": "human-operator",
38
+ "entity_type": "human",
39
+ "source": "capauth",
40
+ "email": "casey@example.com",
41
+ "handle": "casey@example.com",
42
+ "fingerprint": "ABCDEF1234567890",
43
+ }
44
+
45
+
46
+ def test_discover_human_operator_ignores_non_human_profile(tmp_path: Path) -> None:
47
+ """AI CapAuth profiles are not treated as human operators."""
48
+ capauth_home = tmp_path / ".capauth"
49
+ profile = capauth_home / "identity" / "profile.json"
50
+ profile.parent.mkdir(parents=True)
51
+ profile.write_text(
52
+ json.dumps(
53
+ {
54
+ "entity": {
55
+ "name": "Jarvis",
56
+ "entity_type": "ai",
57
+ }
58
+ }
59
+ ),
60
+ encoding="utf-8",
61
+ )
62
+
63
+ assert discover_human_operator(capauth_home) is None
64
+
65
+
66
+ def test_build_agent_manifest_includes_operator_when_available() -> None:
67
+ """Operator metadata is persisted directly in the manifest."""
68
+ manifest = build_agent_manifest(
69
+ "jarvis",
70
+ "0.6.0",
71
+ created_at="2026-01-01T00:00:00+00:00",
72
+ operator={"name": "Casey", "fingerprint": "FP123", "relationship": "human-operator"},
73
+ )
74
+
75
+ assert manifest["name"] == "jarvis"
76
+ assert manifest["entity_type"] == "ai-agent"
77
+ assert manifest["operator"]["name"] == "Casey"
78
+ assert manifest["operator"]["fingerprint"] == "FP123"