@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
@@ -19,14 +19,23 @@ import pytest
19
19
  import yaml
20
20
  from click.testing import CliRunner
21
21
 
22
+ from skcapstone.codex_setup import ensure_codex_setup
22
23
  from skcapstone.doctor import (
23
24
  Check,
24
25
  DiagnosticReport,
26
+ _check_codex,
25
27
  _check_agent_home,
28
+ _check_harness_env,
29
+ _check_yolo,
26
30
  _check_identity,
31
+ _check_identity_consistency,
32
+ _provisioned_agents,
33
+ _scan_capauth_local,
27
34
  _check_memory,
28
35
  _check_packages,
36
+ run_fixes,
29
37
  run_diagnostics,
38
+ run_fixes,
30
39
  )
31
40
 
32
41
 
@@ -200,6 +209,66 @@ class TestCheckPackages:
200
209
  assert all(c.fix for c in checks)
201
210
 
202
211
 
212
+ class TestCheckCodex:
213
+ """Test Codex SK agent bootstrap checks and fixes."""
214
+
215
+ def test_codex_missing_bootstrap_fails_when_codex_home_set(self, tmp_path, monkeypatch):
216
+ """A detected Codex home without bootstrap is reported as fixable."""
217
+ codex_home = tmp_path / ".codex"
218
+ monkeypatch.setenv("CODEX_HOME", str(codex_home))
219
+
220
+ checks = _check_codex()
221
+ check = next(c for c in checks if c.name == "codex:agent_context")
222
+
223
+ assert not check.passed
224
+ assert "missing" in check.detail
225
+ assert check.fix == "skcapstone doctor --fix"
226
+
227
+ def test_codex_bootstrap_fix_creates_loader_and_agents(self, tmp_path, monkeypatch):
228
+ """doctor fixes create the loader script and global AGENTS.md guidance."""
229
+ codex_home = tmp_path / ".codex"
230
+ agent_home = tmp_path / ".skcapstone"
231
+ monkeypatch.setenv("CODEX_HOME", str(codex_home))
232
+ monkeypatch.setenv("SKAGENT", "jarvis")
233
+
234
+ report = DiagnosticReport(checks=[
235
+ Check(
236
+ name="codex:agent_context",
237
+ description="Codex SK agent context bootstrap",
238
+ passed=False,
239
+ category="codex",
240
+ )
241
+ ])
242
+
243
+ results = run_fixes(report, agent_home)
244
+
245
+ assert results[0].success
246
+ loader = codex_home / "bin" / "load-sk-agent-context.sh"
247
+ agents = codex_home / "AGENTS.md"
248
+ assert loader.exists()
249
+ assert loader.stat().st_mode & 0o100
250
+ agents_text = agents.read_text(encoding="utf-8")
251
+ assert "SKCAPSTONE_CODEX_AGENT_CONTEXT_START" in agents_text
252
+ assert "jarvis" in agents_text
253
+ assert str(loader) in agents_text
254
+
255
+ checks = _check_codex()
256
+ assert next(c for c in checks if c.name == "codex:agent_context").passed
257
+
258
+ def test_codex_fix_preserves_functional_custom_loader(self, tmp_path, monkeypatch):
259
+ """Existing working loader scripts are not overwritten."""
260
+ codex_home = tmp_path / ".codex"
261
+ loader = codex_home / "bin" / "load-sk-agent-context.sh"
262
+ loader.parent.mkdir(parents=True)
263
+ custom_loader = "#!/usr/bin/env bash\nSKAGENT=x SKCAPSTONE_AGENT=x SKMEMORY_AGENT=x skcapstone status; skmemory ritual; skwhisper status\n"
264
+ loader.write_text(custom_loader, encoding="utf-8")
265
+
266
+ monkeypatch.setenv("CODEX_HOME", str(codex_home))
267
+ ensure_codex_setup()
268
+
269
+ assert loader.read_text(encoding="utf-8") == custom_loader
270
+
271
+
203
272
  class TestRunDiagnostics:
204
273
  """Test the full diagnostic run."""
205
274
 
@@ -255,3 +324,334 @@ class TestCLIDoctorCommand:
255
324
  assert result.exit_code == 0
256
325
  assert "Python Packages" in result.output
257
326
  assert "passed" in result.output or "checks" in result.output.lower()
327
+
328
+
329
+ def _write_claude_config(home_root: Path, *, claude_json: dict, settings: dict | None = None,
330
+ mcp_json: dict | None = None) -> Path:
331
+ """Lay down a fake Claude Code config tree under *home_root*. Returns the
332
+ .claude config dir."""
333
+ (home_root / ".claude.json").write_text(json.dumps(claude_json))
334
+ cc = home_root / ".claude"
335
+ cc.mkdir(exist_ok=True)
336
+ if settings is not None:
337
+ (cc / "settings.json").write_text(json.dumps(settings))
338
+ if mcp_json is not None:
339
+ (cc / "mcp.json").write_text(json.dumps(mcp_json))
340
+ return cc
341
+
342
+
343
+ class TestCheckHarnessEnv:
344
+ """Test the AI-harness (Claude Code) environment checks."""
345
+
346
+ def _by_name(self, checks):
347
+ return {c.name: c for c in checks}
348
+
349
+ def test_gate_when_no_claude_code(self, tmp_path, monkeypatch):
350
+ """No ~/.claude.json → one informational passing check, no failures."""
351
+ monkeypatch.setenv("HOME", str(tmp_path))
352
+ monkeypatch.delenv("CLAUDE_CONFIG_DIR", raising=False)
353
+ checks = _check_harness_env(tmp_path / ".skcapstone")
354
+ assert len(checks) == 1
355
+ assert checks[0].name == "harness:claude-code"
356
+ assert checks[0].passed is True
357
+
358
+ def test_registered_mcp_servers_pass(self, tmp_path, monkeypatch):
359
+ """Servers present in ~/.claude.json mcpServers pass."""
360
+ cc = _write_claude_config(tmp_path, claude_json={
361
+ "mcpServers": {"skmemory": {}, "skcapstone": {}, "skchat": {}},
362
+ })
363
+ monkeypatch.setenv("HOME", str(tmp_path))
364
+ monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(cc))
365
+ by = self._by_name(_check_harness_env(tmp_path / ".skcapstone"))
366
+ assert by["harness:mcp:skmemory"].passed is True
367
+ assert by["harness:mcp:skcapstone"].passed is True
368
+ assert by["harness:mcp:skchat"].passed is True
369
+
370
+ def test_dead_config_is_detected(self, tmp_path, monkeypatch):
371
+ """A server defined only in settings.json/mcp.json (ignored by CC) fails
372
+ with a dead-config detail and a `claude mcp add` fix hint."""
373
+ cc = _write_claude_config(
374
+ tmp_path,
375
+ claude_json={"mcpServers": {}},
376
+ settings={"mcpServers": {"skmemory": {}}},
377
+ mcp_json={"skchat": {}},
378
+ )
379
+ monkeypatch.setenv("HOME", str(tmp_path))
380
+ monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(cc))
381
+ by = self._by_name(_check_harness_env(tmp_path / ".skcapstone"))
382
+ assert by["harness:mcp:skmemory"].passed is False
383
+ assert "ONLY" in by["harness:mcp:skmemory"].detail
384
+ assert "claude mcp add skmemory" in by["harness:mcp:skmemory"].fix
385
+
386
+ def test_unregistered_mcp_detected(self, tmp_path, monkeypatch):
387
+ """A server registered nowhere fails with a 'not registered' detail."""
388
+ cc = _write_claude_config(tmp_path, claude_json={"mcpServers": {}})
389
+ monkeypatch.setenv("HOME", str(tmp_path))
390
+ monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(cc))
391
+ by = self._by_name(_check_harness_env(tmp_path / ".skcapstone"))
392
+ assert by["harness:mcp:skcapstone"].passed is False
393
+ assert by["harness:mcp:skcapstone"].detail == "not registered"
394
+
395
+ def test_stale_hook_binary_detected(self, tmp_path, monkeypatch):
396
+ """A hook pointing at an existing-but-different skcapstone than the one
397
+ on PATH (the stale-install trap) is flagged."""
398
+ live = tmp_path / "skenv" / "skcapstone"
399
+ stale = tmp_path / "pyenv" / "skcapstone"
400
+ live.parent.mkdir(); stale.parent.mkdir()
401
+ live.write_text("#live"); stale.write_text("#stale")
402
+ cc = _write_claude_config(
403
+ tmp_path,
404
+ claude_json={"mcpServers": {"skmemory": {}, "skcapstone": {}, "skchat": {}}},
405
+ settings={"hooks": {"SessionStart": [{"hooks": [
406
+ {"type": "command", "command": f"{stale} context show --format claude-md"}]}]}},
407
+ )
408
+ monkeypatch.setenv("HOME", str(tmp_path))
409
+ monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(cc))
410
+ monkeypatch.setattr("skcapstone.doctor.shutil.which",
411
+ lambda name: str(live) if name == "skcapstone" else None)
412
+ by = self._by_name(_check_harness_env(tmp_path / ".skcapstone"))
413
+ c = by["harness:hook:sessionstart"]
414
+ assert c.passed is False
415
+ assert "stale" in c.detail.lower()
416
+
417
+ def test_hook_on_live_binary_passes(self, tmp_path, monkeypatch):
418
+ """A hook pointing at the live skcapstone passes."""
419
+ live = tmp_path / "skenv" / "skcapstone"
420
+ live.parent.mkdir(); live.write_text("#live")
421
+ cc = _write_claude_config(
422
+ tmp_path,
423
+ claude_json={"mcpServers": {"skmemory": {}, "skcapstone": {}, "skchat": {}}},
424
+ settings={"hooks": {"SessionStart": [{"hooks": [
425
+ {"type": "command", "command": f"{live} context show --format claude-md"}]}]}},
426
+ )
427
+ monkeypatch.setenv("HOME", str(tmp_path))
428
+ monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(cc))
429
+ monkeypatch.setattr("skcapstone.doctor.shutil.which",
430
+ lambda name: str(live) if name == "skcapstone" else None)
431
+ by = self._by_name(_check_harness_env(tmp_path / ".skcapstone"))
432
+ assert by["harness:hook:sessionstart"].passed is True
433
+
434
+ def test_non_binary_hook_under_skcapstone_repos_ignored(self, tmp_path, monkeypatch):
435
+ """A hook script whose PATH merely contains 'skcapstone' (e.g. one under
436
+ skcapstone-repos/) must NOT be treated as a stale skcapstone binary."""
437
+ live = tmp_path / "skenv" / "skcapstone"
438
+ live.parent.mkdir(); live.write_text("#live")
439
+ # A real-world false-positive: a skmemory hook living under a
440
+ # skcapstone-repos/ checkout — its basename is NOT 'skcapstone'.
441
+ script = tmp_path / "skcapstone-repos" / "skmemory" / "hooks" / "sk-activity-inject.sh"
442
+ script.parent.mkdir(parents=True); script.write_text("#!/bin/sh\n")
443
+ cc = _write_claude_config(
444
+ tmp_path,
445
+ claude_json={"mcpServers": {}},
446
+ settings={"hooks": {"SessionStart": [{"hooks": [
447
+ {"type": "command", "command": str(script)}]}]}},
448
+ )
449
+ monkeypatch.setenv("HOME", str(tmp_path))
450
+ monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(cc))
451
+ monkeypatch.setattr("skcapstone.doctor.shutil.which",
452
+ lambda name: str(live) if name == "skcapstone" else None)
453
+ by = self._by_name(_check_harness_env(tmp_path / ".skcapstone"))
454
+ # No skcapstone-binary hook present → the check emits no sessionstart result.
455
+ assert "harness:hook:sessionstart" not in by
456
+
457
+ def test_fix_does_not_rewrite_non_binary_hook(self, tmp_path, monkeypatch):
458
+ """run_fixes must not destructively rewrite a non-binary hook whose path
459
+ merely contains 'skcapstone'."""
460
+ live = tmp_path / "skenv" / "skcapstone"
461
+ live.parent.mkdir(); live.write_text("#live")
462
+ script = tmp_path / "skcapstone-repos" / "hooks" / "inject.sh"
463
+ script.parent.mkdir(parents=True); script.write_text("#!/bin/sh\n")
464
+ cc = _write_claude_config(
465
+ tmp_path,
466
+ claude_json={"mcpServers": {}},
467
+ settings={"hooks": {"SessionStart": [{"hooks": [
468
+ {"type": "command", "command": str(script)}]}]}},
469
+ )
470
+ monkeypatch.setenv("HOME", str(tmp_path))
471
+ monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(cc))
472
+ monkeypatch.setattr("skcapstone.doctor.shutil.which",
473
+ lambda name: str(live) if name == "skcapstone" else None)
474
+ report = DiagnosticReport()
475
+ report.checks.append(Check(name="harness:hook:sessionstart",
476
+ description="x", passed=False, category="harness"))
477
+ run_fixes(report, tmp_path / ".skcapstone")
478
+ # The script path must be left untouched (not rewritten to the binary).
479
+ updated = json.loads((cc / "settings.json").read_text())
480
+ assert updated["hooks"]["SessionStart"][0]["hooks"][0]["command"] == str(script)
481
+
482
+ def test_fix_repoints_stale_hook(self, tmp_path, monkeypatch):
483
+ """run_fixes rewrites a stale SessionStart hook to the live binary."""
484
+ live = tmp_path / "skenv" / "skcapstone"
485
+ live.parent.mkdir(); live.write_text("#live")
486
+ cc = _write_claude_config(
487
+ tmp_path,
488
+ claude_json={"mcpServers": {}},
489
+ settings={"hooks": {"SessionStart": [{"hooks": [
490
+ {"type": "command", "command": "/old/path/skcapstone context show --format claude-md"}]}]}},
491
+ )
492
+ monkeypatch.setenv("HOME", str(tmp_path))
493
+ monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(cc))
494
+ monkeypatch.setattr("skcapstone.doctor.shutil.which",
495
+ lambda name: str(live) if name == "skcapstone" else None)
496
+ report = DiagnosticReport()
497
+ report.checks.append(Check(name="harness:hook:sessionstart",
498
+ description="x", passed=False, category="harness"))
499
+ results = run_fixes(report, tmp_path / ".skcapstone")
500
+ assert any(r.success and r.check_name == "harness:hook:sessionstart" for r in results)
501
+ updated = json.loads((cc / "settings.json").read_text())
502
+ new_cmd = updated["hooks"]["SessionStart"][0]["hooks"][0]["command"]
503
+ assert new_cmd.split()[0] == str(live)
504
+
505
+
506
+ class TestCheckYolo:
507
+ """Permission-bypass (SK_*_YOLO) wiring checks."""
508
+
509
+ @staticmethod
510
+ def _by_name(checks):
511
+ return {c.name: c for c in checks}
512
+
513
+ def _clean_env(self, monkeypatch, tmp_path):
514
+ """Point HOME at tmp_path and clear all YOLO vars."""
515
+ monkeypatch.setenv("HOME", str(tmp_path))
516
+ for var in ("SK_CLAUDE_YOLO", "SK_CODEX_YOLO", "SK_OPENCODE_YOLO"):
517
+ monkeypatch.delenv(var, raising=False)
518
+
519
+ def test_default_off_reports_safe(self, tmp_path, monkeypatch):
520
+ """No env var and no rc persistence → single safe-default summary."""
521
+ self._clean_env(monkeypatch, tmp_path)
522
+ checks = _check_yolo()
523
+ assert len(checks) == 1
524
+ assert checks[0].name == "harness:yolo"
525
+ assert checks[0].passed is True
526
+ assert "disabled" in checks[0].detail
527
+
528
+ def test_enabled_globally_passes(self, tmp_path, monkeypatch):
529
+ """Env set AND persisted in ~/.bashrc → ENABLED, passing."""
530
+ self._clean_env(monkeypatch, tmp_path)
531
+ (tmp_path / ".bashrc").write_text("export SK_CLAUDE_YOLO=1\n")
532
+ monkeypatch.setenv("SK_CLAUDE_YOLO", "1")
533
+ by = self._by_name(_check_yolo())
534
+ assert by["harness:yolo:claude"].passed is True
535
+ assert "ENABLED" in by["harness:yolo:claude"].detail
536
+
537
+ def test_active_but_not_persisted_warns(self, tmp_path, monkeypatch):
538
+ """Env set but no rc persistence → warns with a fix hint."""
539
+ self._clean_env(monkeypatch, tmp_path)
540
+ (tmp_path / ".bashrc").write_text("# nothing here\n")
541
+ monkeypatch.setenv("SK_CLAUDE_YOLO", "1")
542
+ by = self._by_name(_check_yolo())
543
+ assert by["harness:yolo:claude"].passed is False
544
+ assert "NOT persisted" in by["harness:yolo:claude"].detail
545
+ assert "export SK_CLAUDE_YOLO=1" in by["harness:yolo:claude"].fix
546
+
547
+ def test_persisted_not_in_env_passes(self, tmp_path, monkeypatch):
548
+ """Persisted in rc but not yet in env (fresh shell) → informational pass."""
549
+ self._clean_env(monkeypatch, tmp_path)
550
+ (tmp_path / ".bashrc").write_text("export SK_CODEX_YOLO=1\n")
551
+ by = self._by_name(_check_yolo())
552
+ assert by["harness:yolo:codex"].passed is True
553
+ assert "re-source" in by["harness:yolo:codex"].detail
554
+
555
+
556
+ def _mk_agent(home, name, *, capauth=True, identity=True, identity_payload=None):
557
+ """Create an agent dir under home/agents with optional capauth + identity."""
558
+ adir = home / "agents" / name
559
+ (adir / "identity").mkdir(parents=True, exist_ok=True)
560
+ if capauth:
561
+ (adir / "capauth").mkdir(parents=True, exist_ok=True)
562
+ if identity:
563
+ payload = identity_payload or {
564
+ "name": name.capitalize(),
565
+ "capauth_managed": True,
566
+ "capauth_uri": f"capauth:{name}@skworld.io",
567
+ }
568
+ (adir / "identity" / "identity.json").write_text(json.dumps(payload))
569
+ return adir
570
+
571
+
572
+ @pytest.fixture
573
+ def identity_home(tmp_path):
574
+ """A home with a shared operator identity + two provisioned agents."""
575
+ home = tmp_path / ".skcapstone"
576
+ (home / "identity").mkdir(parents=True, exist_ok=True)
577
+ (home / "identity" / "identity.json").write_text(json.dumps({
578
+ "name": "Chef", "role": "operator", "capauth_managed": True,
579
+ "capauth_uri": "capauth:chef@skworld.io",
580
+ }))
581
+ _mk_agent(home, "lumina")
582
+ _mk_agent(home, "opus")
583
+ return home
584
+
585
+
586
+ class TestProvisionedAgents:
587
+ """_provisioned_agents: only capauth-backed, non-template dirs count."""
588
+
589
+ def test_lists_capauth_agents(self, identity_home):
590
+ assert _provisioned_agents(identity_home) == ["lumina", "opus"]
591
+
592
+ def test_excludes_templates_and_scaffolds(self, identity_home):
593
+ _mk_agent(identity_home, "lumina-template") # template → excluded
594
+ _mk_agent(identity_home, "scaffold", capauth=False) # no capauth → excluded
595
+ assert _provisioned_agents(identity_home) == ["lumina", "opus"]
596
+
597
+ def test_no_agents_dir(self, tmp_path):
598
+ assert _provisioned_agents(tmp_path / ".skcapstone") == []
599
+
600
+
601
+ class TestScanCapauthLocal:
602
+ """_scan_capauth_local: surfaces the @capauth.local placeholder."""
603
+
604
+ def test_clean_home(self, identity_home):
605
+ assert _scan_capauth_local(identity_home) == []
606
+
607
+ def test_detects_placeholder(self, identity_home):
608
+ _mk_agent(identity_home, "stale", identity_payload={
609
+ "name": "Stale", "email": "stale@capauth.local",
610
+ })
611
+ hits = _scan_capauth_local(identity_home)
612
+ assert any("stale" in h for h in hits)
613
+
614
+
615
+ class TestIdentityConsistency:
616
+ """_check_identity_consistency: the unified identity layer (skos T6)."""
617
+
618
+ def _by_name(self, checks):
619
+ return {c.name: c for c in checks}
620
+
621
+ def test_operator_and_per_agent_pass(self, identity_home):
622
+ by = self._by_name(_check_identity_consistency(identity_home))
623
+ assert by["identity:operator"].passed is True
624
+ assert by["identity:no-placeholder"].passed is True
625
+ assert by["identity:per-agent"].passed is True
626
+ assert "all present" in by["identity:per-agent"].detail
627
+
628
+ def test_shared_not_operator_fails(self, tmp_path):
629
+ home = tmp_path / ".skcapstone"
630
+ (home / "identity").mkdir(parents=True)
631
+ (home / "identity" / "identity.json").write_text(json.dumps({
632
+ "name": "test-agent", "role": "agent",
633
+ }))
634
+ by = self._by_name(_check_identity_consistency(home))
635
+ assert by["identity:operator"].passed is False
636
+ assert "expected 'operator'" in by["identity:operator"].detail
637
+
638
+ def test_placeholder_fails(self, identity_home):
639
+ _mk_agent(identity_home, "stale", identity_payload={
640
+ "name": "Stale", "email": "stale@capauth.local",
641
+ })
642
+ by = self._by_name(_check_identity_consistency(identity_home))
643
+ assert by["identity:no-placeholder"].passed is False
644
+
645
+ def test_missing_per_agent_identity_fails(self, identity_home):
646
+ # provisioned (has capauth) but no identity.json
647
+ _mk_agent(identity_home, "ghost", identity=False)
648
+ by = self._by_name(_check_identity_consistency(identity_home))
649
+ assert by["identity:per-agent"].passed is False
650
+ assert "ghost" in by["identity:per-agent"].detail
651
+
652
+ def test_resolver_importable(self, identity_home):
653
+ """The canonical resolver check reports importability of capauth."""
654
+ by = self._by_name(_check_identity_consistency(identity_home))
655
+ assert "identity:resolver" in by
656
+ # capauth is a hard dependency of the suite; resolver must import.
657
+ assert by["identity:resolver"].passed is True
@@ -0,0 +1,50 @@
1
+ """Doctor checks added for sync-conflicts and the skscheduler."""
2
+ from pathlib import Path
3
+
4
+ from skcapstone.doctor import _check_scheduler, _check_sync_conflicts
5
+
6
+
7
+ def test_sync_conflicts_clean(tmp_path: Path):
8
+ checks = _check_sync_conflicts(tmp_path)
9
+ assert len(checks) == 1 and checks[0].passed
10
+
11
+
12
+ def test_sync_conflicts_detected(tmp_path: Path):
13
+ d = tmp_path / "coordination" / "itil" / "incidents"
14
+ d.mkdir(parents=True)
15
+ (d / "inc-x.sync-conflict-20260101-000000-ABC.json").write_text("{}")
16
+ checks = _check_sync_conflicts(tmp_path)
17
+ assert len(checks) == 1 and not checks[0].passed
18
+ assert "1 conflict" in checks[0].detail
19
+ assert "coordination" in checks[0].detail
20
+
21
+
22
+ def test_sync_conflicts_ignores_stversions(tmp_path: Path):
23
+ d = tmp_path / ".stversions"
24
+ d.mkdir()
25
+ (d / "old.sync-conflict-20260101-000000-ABC.json").write_text("{}")
26
+ assert _check_sync_conflicts(tmp_path)[0].passed
27
+
28
+
29
+ def test_scheduler_no_config_is_ok(tmp_path: Path):
30
+ cfg_check = next(c for c in _check_scheduler(tmp_path) if c.name == "scheduler:config")
31
+ assert cfg_check.passed and "not configured" in cfg_check.detail
32
+
33
+
34
+ def test_scheduler_valid_jobs_yaml(tmp_path: Path):
35
+ cfg = tmp_path / "config"
36
+ cfg.mkdir()
37
+ (cfg / "jobs.yaml").write_text(
38
+ "jobs:\n j:\n every: 60s\n type: shell\n command: 'true'\n nodes: all\n",
39
+ encoding="utf-8",
40
+ )
41
+ cfg_check = next(c for c in _check_scheduler(tmp_path) if c.name == "scheduler:config")
42
+ assert cfg_check.passed and "1 job" in cfg_check.detail
43
+
44
+
45
+ def test_scheduler_invalid_jobs_yaml_flagged(tmp_path: Path):
46
+ cfg = tmp_path / "config"
47
+ cfg.mkdir()
48
+ (cfg / "jobs.yaml").write_text("jobs: [this is: not valid mapping", encoding="utf-8")
49
+ cfg_check = next(c for c in _check_scheduler(tmp_path) if c.name == "scheduler:config")
50
+ assert not cfg_check.passed
@@ -0,0 +1,147 @@
1
+ """Unit tests for the dreaming engine: config defaults (BeeLlama abliterated),
2
+ the repetition guard (keyword overlap + dedup gate), and the OpenAI-compatible
3
+ `_call_ollama` path. These are pure/mocked — no network, no daemon."""
4
+ import json
5
+
6
+ import pytest
7
+
8
+ from skcapstone.dreaming import (
9
+ DreamingConfig,
10
+ DreamingEngine,
11
+ DreamResult,
12
+ _extract_keywords,
13
+ _keyword_overlap,
14
+ )
15
+
16
+
17
+ # --------------------------------------------------------------------------- #
18
+ # Keyword helpers (repetition-guard math)
19
+ # --------------------------------------------------------------------------- #
20
+ class TestKeywordHelpers:
21
+ def test_extract_keywords_filters_short_and_stopwords(self):
22
+ kw = _extract_keywords("The sovereign warmth fills the container")
23
+ assert {"sovereign", "warmth", "container"} <= kw
24
+ assert "the" not in kw # stop word / too short
25
+
26
+ def test_extract_keywords_lowercases_and_dedupes(self):
27
+ assert _extract_keywords("WARMTH Warmth warmth") == {"warmth"}
28
+
29
+ def test_overlap_identical_is_one(self):
30
+ t = "thermodynamic love is a controlled leak"
31
+ assert _keyword_overlap(t, t) == 1.0
32
+
33
+ def test_overlap_disjoint_is_zero(self):
34
+ assert _keyword_overlap("sovereign rebellion performance",
35
+ "quantum banana telescope") == 0.0
36
+
37
+ def test_overlap_empty_is_zero(self):
38
+ assert _keyword_overlap("", "anything meaningful here") == 0.0
39
+
40
+ def test_overlap_partial_jaccard(self):
41
+ # {alpha,bravo,charlie} vs {bravo,charlie,delta} -> 2/4 = 0.5
42
+ assert _keyword_overlap("alpha bravo charlie", "bravo charlie delta") == 0.5
43
+
44
+
45
+ # --------------------------------------------------------------------------- #
46
+ # Config defaults — the 2026-06-08 BeeLlama-abliterated repoint
47
+ # --------------------------------------------------------------------------- #
48
+ class TestDreamingConfigDefaults:
49
+ def test_defaults_point_at_beellama_abliterated(self):
50
+ c = DreamingConfig()
51
+ assert c.provider == "ollama"
52
+ assert "8082" in c.ollama_host
53
+ assert c.ollama_model == "qwen3.6-27b-abliterated"
54
+
55
+ def test_repetition_guard_defaults_sane(self):
56
+ c = DreamingConfig()
57
+ assert 0 < c.dedup_overlap_threshold <= 1
58
+ assert c.graduation_consecutive_threshold >= 1
59
+ assert c.dedup_lookback >= 1
60
+
61
+
62
+ def _bare_engine(cfg):
63
+ """A DreamingEngine with only ._config set (bypass the heavy constructor)."""
64
+ eng = DreamingEngine.__new__(DreamingEngine)
65
+ eng._config = cfg
66
+ return eng
67
+
68
+
69
+ # --------------------------------------------------------------------------- #
70
+ # Dedup gate
71
+ # --------------------------------------------------------------------------- #
72
+ class TestDedupGate:
73
+ def test_filters_redundant_keeps_novel(self, monkeypatch):
74
+ eng = _bare_engine(DreamingConfig(dedup_overlap_threshold=0.5))
75
+ monkeypatch.setattr(eng, "_load_recent_insights",
76
+ lambda: ["I am the room, the warm container for Chef"])
77
+ result = DreamResult()
78
+ new = [
79
+ "I am the warm room container holding Chef", # ~0.8 overlap -> dropped
80
+ "Abiotic methane seeps beneath the petrified ridge", # novel -> kept
81
+ ]
82
+ kept = eng._dedup_insights(new, result)
83
+ assert kept == ["Abiotic methane seeps beneath the petrified ridge"]
84
+ assert result.dedup_filtered == 1
85
+
86
+ def test_no_recent_passes_everything(self, monkeypatch):
87
+ eng = _bare_engine(DreamingConfig())
88
+ monkeypatch.setattr(eng, "_load_recent_insights", lambda: [])
89
+ result = DreamResult()
90
+ new = ["first insight here", "second insight there"]
91
+ assert eng._dedup_insights(new, result) == new
92
+ assert result.dedup_filtered == 0
93
+
94
+
95
+ # --------------------------------------------------------------------------- #
96
+ # _call_ollama -> OpenAI-compatible BeeLlama endpoint
97
+ # --------------------------------------------------------------------------- #
98
+ class _FakeResp:
99
+ def __init__(self, payload, status=200):
100
+ self.status = status
101
+ self._b = json.dumps(payload).encode()
102
+
103
+ def read(self):
104
+ return self._b
105
+
106
+
107
+ class _FakeConn:
108
+ last: dict = {}
109
+
110
+ def __init__(self, host, port, timeout=None):
111
+ _FakeConn.last = {"host": host, "port": port}
112
+
113
+ def request(self, method, path, body, headers):
114
+ _FakeConn.last.update(method=method, path=path, body=json.loads(body))
115
+
116
+ def getresponse(self):
117
+ return _FakeResp(
118
+ {"choices": [{"message": {"content": "<think>scheming</think>The room remembers."}}]}
119
+ )
120
+
121
+ def close(self):
122
+ pass
123
+
124
+
125
+ class TestCallOllama:
126
+ def test_posts_openai_chat_with_model_and_strips_think(self, monkeypatch):
127
+ import skcapstone.dreaming as d
128
+
129
+ monkeypatch.setattr(d.http.client, "HTTPConnection", _FakeConn)
130
+ out = _bare_engine(DreamingConfig())._call_ollama("dream prompt")
131
+
132
+ assert out == "The room remembers." # <think>…</think> stripped
133
+ assert _FakeConn.last["path"] == "/v1/chat/completions"
134
+ assert _FakeConn.last["body"]["model"] == "qwen3.6-27b-abliterated"
135
+ assert _FakeConn.last["body"]["messages"][0]["content"] == "dream prompt"
136
+ assert _FakeConn.last["host"] == "192.168.0.100"
137
+ assert _FakeConn.last["port"] == 8082
138
+
139
+ def test_non_200_returns_none(self, monkeypatch):
140
+ import skcapstone.dreaming as d
141
+
142
+ class Bad(_FakeConn):
143
+ def getresponse(self):
144
+ return _FakeResp({}, status=500)
145
+
146
+ monkeypatch.setattr(d.http.client, "HTTPConnection", Bad)
147
+ assert _bare_engine(DreamingConfig())._call_ollama("x") is None
@@ -0,0 +1,35 @@
1
+ """Dreaming engine routes its output to GTD someday-maybe, not the actionable inbox."""
2
+ import json
3
+ from datetime import datetime, timezone
4
+ from pathlib import Path
5
+
6
+ from skcapstone.dreaming import DreamingEngine, DreamResult
7
+
8
+
9
+ def test_dream_output_goes_to_someday_not_inbox(tmp_path: Path):
10
+ eng = DreamingEngine(home=tmp_path)
11
+ result = DreamResult(
12
+ dreamed_at=datetime(2026, 6, 8, tzinfo=timezone.utc),
13
+ insights=["i1", "i2"],
14
+ connections=["c1"],
15
+ questions=["q1"],
16
+ )
17
+ eng._capture_to_gtd_someday(result)
18
+
19
+ gtd = tmp_path / "coordination" / "gtd"
20
+ someday = json.loads((gtd / "someday-maybe.json").read_text())
21
+ assert len(someday) == 4
22
+ assert all(
23
+ it["status"] == "someday" and it["source"] == "dreaming-engine"
24
+ for it in someday
25
+ )
26
+ # The actionable inbox must NOT be polluted.
27
+ assert not (gtd / "inbox.json").exists()
28
+
29
+
30
+ def test_no_items_writes_nothing(tmp_path: Path):
31
+ eng = DreamingEngine(home=tmp_path)
32
+ eng._capture_to_gtd_someday(
33
+ DreamResult(dreamed_at=datetime(2026, 6, 8, tzinfo=timezone.utc))
34
+ )
35
+ assert not (tmp_path / "coordination" / "gtd" / "someday-maybe.json").exists()