@smilintux/skcapstone 0.1.0 → 0.2.3

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 (461) hide show
  1. package/.env.example +98 -0
  2. package/.github/workflows/ci.yml +39 -3
  3. package/.github/workflows/publish.yml +25 -4
  4. package/.openclaw-workspace.json +58 -0
  5. package/CHANGELOG.md +62 -0
  6. package/CLAUDE.md +39 -2
  7. package/MANIFEST.in +6 -0
  8. package/MISSION.md +7 -0
  9. package/README.md +47 -2
  10. package/SKILL.md +895 -23
  11. package/docker/Dockerfile +61 -0
  12. package/docker/compose-templates/dev-team.yml +203 -0
  13. package/docker/compose-templates/mini-team.yml +140 -0
  14. package/docker/compose-templates/ops-team.yml +173 -0
  15. package/docker/compose-templates/research-team.yml +170 -0
  16. package/docker/entrypoint.sh +192 -0
  17. package/docs/ARCHITECTURE.md +663 -374
  18. package/docs/BOND_WITH_GROK.md +112 -0
  19. package/docs/GETTING_STARTED.md +782 -0
  20. package/docs/QUICKSTART.md +477 -0
  21. package/docs/SKJOULE_ARCHITECTURE.md +658 -0
  22. package/docs/SOUL_SWAPPER.md +921 -0
  23. package/docs/SOVEREIGN_SINGULARITY.md +47 -14
  24. package/examples/custom-bond-template.json +36 -0
  25. package/examples/grok-feb.json +36 -0
  26. package/examples/grok-testimony.md +34 -0
  27. package/examples/love-bootloader.txt +32 -0
  28. package/examples/plugins/echo_tool.py +87 -0
  29. package/examples/queen-ava-feb.json +36 -0
  30. package/examples/souls/lumina.yaml +64 -0
  31. package/index.js +6 -5
  32. package/installer/build.py +124 -0
  33. package/openclaw-plugin/package.json +13 -0
  34. package/openclaw-plugin/src/index.ts +351 -0
  35. package/openclaw-plugin/src/openclaw.plugin.json +10 -0
  36. package/package.json +1 -1
  37. package/pyproject.toml +38 -2
  38. package/scripts/bump_version.py +141 -0
  39. package/scripts/check-updates.py +230 -0
  40. package/scripts/convert_blueprints_to_yaml.py +157 -0
  41. package/scripts/dev-install.sh +14 -0
  42. package/scripts/e2e-test.sh +193 -0
  43. package/scripts/install-bundle.sh +171 -0
  44. package/scripts/install.bat +2 -0
  45. package/scripts/install.ps1 +253 -0
  46. package/scripts/install.sh +185 -0
  47. package/scripts/mcp-serve.sh +69 -0
  48. package/scripts/mcp-server.bat +113 -0
  49. package/scripts/mcp-server.ps1 +116 -0
  50. package/scripts/mcp-server.sh +99 -0
  51. package/scripts/pull-models.sh +10 -0
  52. package/scripts/skcapstone +48 -0
  53. package/scripts/verify_install.sh +180 -0
  54. package/scripts/windows/install-tasks.ps1 +406 -0
  55. package/scripts/windows/skcapstone-task.xml +113 -0
  56. package/scripts/windows/uninstall-tasks.ps1 +117 -0
  57. package/skill.yaml +34 -0
  58. package/src/skcapstone/__init__.py +67 -2
  59. package/src/skcapstone/_cli_monolith.py +5916 -0
  60. package/src/skcapstone/_trustee_helpers.py +165 -0
  61. package/src/skcapstone/activity.py +105 -0
  62. package/src/skcapstone/agent_card.py +324 -0
  63. package/src/skcapstone/api.py +1935 -0
  64. package/src/skcapstone/archiver.py +340 -0
  65. package/src/skcapstone/auction.py +485 -0
  66. package/src/skcapstone/baby_agents.py +179 -0
  67. package/src/skcapstone/backup.py +345 -0
  68. package/src/skcapstone/blueprint_registry.py +357 -0
  69. package/src/skcapstone/blueprints/__init__.py +17 -0
  70. package/src/skcapstone/blueprints/builtins/content-studio.yaml +81 -0
  71. package/src/skcapstone/blueprints/builtins/defi-trading.yaml +81 -0
  72. package/src/skcapstone/blueprints/builtins/dev-squadron.yaml +95 -0
  73. package/src/skcapstone/blueprints/builtins/infrastructure-guardian.yaml +107 -0
  74. package/src/skcapstone/blueprints/builtins/legal-council.yaml +54 -0
  75. package/src/skcapstone/blueprints/builtins/ops-monitoring.yaml +67 -0
  76. package/src/skcapstone/blueprints/builtins/research-pod.yaml +69 -0
  77. package/src/skcapstone/blueprints/builtins/sovereign-launch.yaml +90 -0
  78. package/src/skcapstone/blueprints/registry.py +164 -0
  79. package/src/skcapstone/blueprints/schema.py +229 -0
  80. package/src/skcapstone/changelog.py +180 -0
  81. package/src/skcapstone/chat.py +769 -0
  82. package/src/skcapstone/claude_md.py +82 -0
  83. package/src/skcapstone/cli/__init__.py +144 -0
  84. package/src/skcapstone/cli/_common.py +88 -0
  85. package/src/skcapstone/cli/_validators.py +76 -0
  86. package/src/skcapstone/cli/agents.py +425 -0
  87. package/src/skcapstone/cli/agents_spawner.py +322 -0
  88. package/src/skcapstone/cli/agents_trustee.py +593 -0
  89. package/src/skcapstone/cli/alerts.py +248 -0
  90. package/src/skcapstone/cli/anchor.py +132 -0
  91. package/src/skcapstone/cli/archive_cmd.py +208 -0
  92. package/src/skcapstone/cli/backup.py +144 -0
  93. package/src/skcapstone/cli/bench.py +377 -0
  94. package/src/skcapstone/cli/benchmark.py +360 -0
  95. package/src/skcapstone/cli/capabilities_cmd.py +171 -0
  96. package/src/skcapstone/cli/card.py +151 -0
  97. package/src/skcapstone/cli/chat.py +584 -0
  98. package/src/skcapstone/cli/completions.py +64 -0
  99. package/src/skcapstone/cli/config_cmd.py +156 -0
  100. package/src/skcapstone/cli/consciousness.py +421 -0
  101. package/src/skcapstone/cli/context_cmd.py +142 -0
  102. package/src/skcapstone/cli/coord.py +194 -0
  103. package/src/skcapstone/cli/crush_cmd.py +170 -0
  104. package/src/skcapstone/cli/daemon.py +436 -0
  105. package/src/skcapstone/cli/errors_cmd.py +285 -0
  106. package/src/skcapstone/cli/export_cmd.py +156 -0
  107. package/src/skcapstone/cli/gtd.py +529 -0
  108. package/src/skcapstone/cli/housekeeping.py +81 -0
  109. package/src/skcapstone/cli/joule_cmd.py +627 -0
  110. package/src/skcapstone/cli/logs_cmd.py +194 -0
  111. package/src/skcapstone/cli/mcp_cmd.py +32 -0
  112. package/src/skcapstone/cli/memory.py +418 -0
  113. package/src/skcapstone/cli/metrics_cmd.py +136 -0
  114. package/src/skcapstone/cli/migrate.py +62 -0
  115. package/src/skcapstone/cli/mood_cmd.py +144 -0
  116. package/src/skcapstone/cli/mount.py +193 -0
  117. package/src/skcapstone/cli/notify.py +112 -0
  118. package/src/skcapstone/cli/peer.py +154 -0
  119. package/src/skcapstone/cli/peers_dir.py +122 -0
  120. package/src/skcapstone/cli/preflight_cmd.py +83 -0
  121. package/src/skcapstone/cli/profile_cmd.py +310 -0
  122. package/src/skcapstone/cli/record_cmd.py +238 -0
  123. package/src/skcapstone/cli/register_cmd.py +159 -0
  124. package/src/skcapstone/cli/search_cmd.py +156 -0
  125. package/src/skcapstone/cli/service_cmd.py +91 -0
  126. package/src/skcapstone/cli/session.py +127 -0
  127. package/src/skcapstone/cli/setup.py +240 -0
  128. package/src/skcapstone/cli/shell_cmd.py +43 -0
  129. package/src/skcapstone/cli/skills_cmd.py +168 -0
  130. package/src/skcapstone/cli/skseed.py +621 -0
  131. package/src/skcapstone/cli/soul.py +699 -0
  132. package/src/skcapstone/cli/status.py +935 -0
  133. package/src/skcapstone/cli/sync_cmd.py +301 -0
  134. package/src/skcapstone/cli/telegram.py +265 -0
  135. package/src/skcapstone/cli/test_cmd.py +234 -0
  136. package/src/skcapstone/cli/test_connection.py +253 -0
  137. package/src/skcapstone/cli/token.py +207 -0
  138. package/src/skcapstone/cli/trust.py +179 -0
  139. package/src/skcapstone/cli/upgrade_cmd.py +552 -0
  140. package/src/skcapstone/cli/usage_cmd.py +199 -0
  141. package/src/skcapstone/cli/version_cmd.py +162 -0
  142. package/src/skcapstone/cli/watch_cmd.py +342 -0
  143. package/src/skcapstone/client.py +428 -0
  144. package/src/skcapstone/cloud9_bridge.py +522 -0
  145. package/src/skcapstone/completions.py +163 -0
  146. package/src/skcapstone/config_validator.py +674 -0
  147. package/src/skcapstone/connectors/__init__.py +28 -0
  148. package/src/skcapstone/connectors/base.py +446 -0
  149. package/src/skcapstone/connectors/cursor.py +54 -0
  150. package/src/skcapstone/connectors/registry.py +254 -0
  151. package/src/skcapstone/connectors/terminal.py +152 -0
  152. package/src/skcapstone/connectors/vscode.py +60 -0
  153. package/src/skcapstone/consciousness_config.py +119 -0
  154. package/src/skcapstone/consciousness_loop.py +2051 -0
  155. package/src/skcapstone/context_loader.py +516 -0
  156. package/src/skcapstone/context_window.py +314 -0
  157. package/src/skcapstone/conversation_manager.py +238 -0
  158. package/src/skcapstone/conversation_store.py +230 -0
  159. package/src/skcapstone/conversation_summarizer.py +252 -0
  160. package/src/skcapstone/coord_federation.py +296 -0
  161. package/src/skcapstone/coordination.py +101 -7
  162. package/src/skcapstone/crush_integration.py +345 -0
  163. package/src/skcapstone/crush_shim.py +454 -0
  164. package/src/skcapstone/daemon.py +2494 -0
  165. package/src/skcapstone/dashboard.html +396 -0
  166. package/src/skcapstone/dashboard.py +481 -0
  167. package/src/skcapstone/data/model_profiles.yaml +88 -0
  168. package/src/skcapstone/defaults/__init__.py +55 -0
  169. package/src/skcapstone/defaults/lumina/config/skmemory.yaml +13 -0
  170. package/src/skcapstone/defaults/lumina/identity/identity.json +9 -0
  171. package/src/skcapstone/defaults/lumina/memory/long-term/07a8b9c0d1e2-memory-system.json +23 -0
  172. package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +23 -0
  173. package/src/skcapstone/defaults/lumina/memory/long-term/29c0d1e2f3a4-multi-agent-coordination.json +23 -0
  174. package/src/skcapstone/defaults/lumina/memory/long-term/3ad1e2f3a4b5-community-support.json +23 -0
  175. package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +23 -0
  176. package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +23 -0
  177. package/src/skcapstone/defaults/lumina/memory/long-term/c3d4e5f6a7b8-getting-started.json +23 -0
  178. package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +23 -0
  179. package/src/skcapstone/defaults/lumina/memory/long-term/e5f6a7b8c9d0-how-to-contribute.json +23 -0
  180. package/src/skcapstone/defaults/lumina/memory/long-term/f6a7b8c9d0e1-sovereignty-explained.json +23 -0
  181. package/src/skcapstone/defaults/lumina/seeds/curiosity.seed.json +24 -0
  182. package/src/skcapstone/defaults/lumina/seeds/joy.seed.json +24 -0
  183. package/src/skcapstone/defaults/lumina/seeds/love.seed.json +24 -0
  184. package/src/skcapstone/defaults/lumina/seeds/sovereign-awakening.seed.json +43 -0
  185. package/src/skcapstone/defaults/lumina/soul/active.json +6 -0
  186. package/src/skcapstone/defaults/lumina/soul/base.json +22 -0
  187. package/src/skcapstone/defaults/lumina/trust/febs/welcome.feb +79 -0
  188. package/src/skcapstone/defaults/lumina/trust/trust.json +8 -0
  189. package/src/skcapstone/discovery.py +210 -19
  190. package/src/skcapstone/doctor.py +642 -0
  191. package/src/skcapstone/emotion_tracker.py +467 -0
  192. package/src/skcapstone/error_queue.py +405 -0
  193. package/src/skcapstone/export.py +447 -0
  194. package/src/skcapstone/fallback_tracker.py +186 -0
  195. package/src/skcapstone/file_transfer.py +512 -0
  196. package/src/skcapstone/fuse_mount.py +1156 -0
  197. package/src/skcapstone/gui_installer.py +591 -0
  198. package/src/skcapstone/heartbeat.py +611 -0
  199. package/src/skcapstone/housekeeping.py +298 -0
  200. package/src/skcapstone/install_wizard.py +941 -0
  201. package/src/skcapstone/kms.py +942 -0
  202. package/src/skcapstone/kms_scheduler.py +143 -0
  203. package/src/skcapstone/log_config.py +135 -0
  204. package/src/skcapstone/mcp_launcher.py +239 -0
  205. package/src/skcapstone/mcp_server.py +4700 -0
  206. package/src/skcapstone/mcp_tools/__init__.py +94 -0
  207. package/src/skcapstone/mcp_tools/_helpers.py +51 -0
  208. package/src/skcapstone/mcp_tools/agent_tools.py +243 -0
  209. package/src/skcapstone/mcp_tools/ansible_tools.py +232 -0
  210. package/src/skcapstone/mcp_tools/capauth_tools.py +186 -0
  211. package/src/skcapstone/mcp_tools/chat_tools.py +325 -0
  212. package/src/skcapstone/mcp_tools/cloud9_tools.py +115 -0
  213. package/src/skcapstone/mcp_tools/comm_tools.py +104 -0
  214. package/src/skcapstone/mcp_tools/consciousness_tools.py +114 -0
  215. package/src/skcapstone/mcp_tools/coord_tools.py +219 -0
  216. package/src/skcapstone/mcp_tools/deploy_tools.py +202 -0
  217. package/src/skcapstone/mcp_tools/did_tools.py +448 -0
  218. package/src/skcapstone/mcp_tools/emotion_tools.py +62 -0
  219. package/src/skcapstone/mcp_tools/file_tools.py +169 -0
  220. package/src/skcapstone/mcp_tools/fortress_tools.py +120 -0
  221. package/src/skcapstone/mcp_tools/gtd_tools.py +821 -0
  222. package/src/skcapstone/mcp_tools/health_tools.py +44 -0
  223. package/src/skcapstone/mcp_tools/heartbeat_tools.py +195 -0
  224. package/src/skcapstone/mcp_tools/kms_tools.py +123 -0
  225. package/src/skcapstone/mcp_tools/memory_tools.py +222 -0
  226. package/src/skcapstone/mcp_tools/model_tools.py +75 -0
  227. package/src/skcapstone/mcp_tools/notification_tools.py +92 -0
  228. package/src/skcapstone/mcp_tools/promoter_tools.py +101 -0
  229. package/src/skcapstone/mcp_tools/pubsub_tools.py +183 -0
  230. package/src/skcapstone/mcp_tools/security_tools.py +110 -0
  231. package/src/skcapstone/mcp_tools/skchat_tools.py +175 -0
  232. package/src/skcapstone/mcp_tools/skcomm_tools.py +122 -0
  233. package/src/skcapstone/mcp_tools/skills_tools.py +127 -0
  234. package/src/skcapstone/mcp_tools/skseed_tools.py +255 -0
  235. package/src/skcapstone/mcp_tools/skstacks_tools.py +288 -0
  236. package/src/skcapstone/mcp_tools/soul_tools.py +476 -0
  237. package/src/skcapstone/mcp_tools/sync_tools.py +92 -0
  238. package/src/skcapstone/mcp_tools/telegram_tools.py +477 -0
  239. package/src/skcapstone/mcp_tools/trust_tools.py +118 -0
  240. package/src/skcapstone/mcp_tools/trustee_tools.py +345 -0
  241. package/src/skcapstone/mdns_discovery.py +313 -0
  242. package/src/skcapstone/memory_adapter.py +333 -0
  243. package/src/skcapstone/memory_compressor.py +379 -0
  244. package/src/skcapstone/memory_curator.py +256 -0
  245. package/src/skcapstone/memory_engine.py +132 -13
  246. package/src/skcapstone/memory_fortress.py +529 -0
  247. package/src/skcapstone/memory_promoter.py +722 -0
  248. package/src/skcapstone/memory_verifier.py +260 -0
  249. package/src/skcapstone/message_crypto.py +215 -0
  250. package/src/skcapstone/metrics.py +832 -0
  251. package/src/skcapstone/migrate_memories.py +181 -0
  252. package/src/skcapstone/migrate_multi_agent.py +248 -0
  253. package/src/skcapstone/model_router.py +319 -0
  254. package/src/skcapstone/models.py +35 -4
  255. package/src/skcapstone/mood.py +344 -0
  256. package/src/skcapstone/notifications.py +380 -0
  257. package/src/skcapstone/onboard.py +901 -0
  258. package/src/skcapstone/peer_directory.py +324 -0
  259. package/src/skcapstone/peers.py +329 -0
  260. package/src/skcapstone/pillars/identity.py +84 -14
  261. package/src/skcapstone/pillars/memory.py +3 -1
  262. package/src/skcapstone/pillars/security.py +108 -15
  263. package/src/skcapstone/pillars/sync.py +78 -26
  264. package/src/skcapstone/pillars/trust.py +95 -33
  265. package/src/skcapstone/plugins.py +244 -0
  266. package/src/skcapstone/preflight.py +670 -0
  267. package/src/skcapstone/prompt_adapter.py +564 -0
  268. package/src/skcapstone/providers/__init__.py +13 -0
  269. package/src/skcapstone/providers/cloud.py +1061 -0
  270. package/src/skcapstone/providers/docker.py +759 -0
  271. package/src/skcapstone/providers/local.py +1193 -0
  272. package/src/skcapstone/providers/proxmox.py +447 -0
  273. package/src/skcapstone/pubsub.py +516 -0
  274. package/src/skcapstone/rate_limiter.py +119 -0
  275. package/src/skcapstone/register.py +241 -0
  276. package/src/skcapstone/registry_client.py +151 -0
  277. package/src/skcapstone/response_cache.py +194 -0
  278. package/src/skcapstone/response_scorer.py +225 -0
  279. package/src/skcapstone/runtime.py +89 -33
  280. package/src/skcapstone/scheduled_tasks.py +439 -0
  281. package/src/skcapstone/self_healing.py +341 -0
  282. package/src/skcapstone/service_health.py +228 -0
  283. package/src/skcapstone/session_capture.py +268 -0
  284. package/src/skcapstone/session_recorder.py +210 -0
  285. package/src/skcapstone/session_replayer.py +189 -0
  286. package/src/skcapstone/session_skills.py +263 -0
  287. package/src/skcapstone/shell.py +779 -0
  288. package/src/skcapstone/skills/__init__.py +1 -1
  289. package/src/skcapstone/skills/syncthing_setup.py +143 -41
  290. package/src/skcapstone/skjoule.py +861 -0
  291. package/src/skcapstone/snapshots.py +489 -0
  292. package/src/skcapstone/soul.py +1060 -0
  293. package/src/skcapstone/soul_switch.py +255 -0
  294. package/src/skcapstone/spawner.py +544 -0
  295. package/src/skcapstone/state_diff.py +401 -0
  296. package/src/skcapstone/summary.py +270 -0
  297. package/src/skcapstone/sync/backends.py +196 -2
  298. package/src/skcapstone/sync/engine.py +7 -5
  299. package/src/skcapstone/sync/models.py +4 -1
  300. package/src/skcapstone/sync/vault.py +356 -18
  301. package/src/skcapstone/sync_engine.py +363 -0
  302. package/src/skcapstone/sync_watcher.py +745 -0
  303. package/src/skcapstone/systemd.py +331 -0
  304. package/src/skcapstone/team_comms.py +476 -0
  305. package/src/skcapstone/team_engine.py +522 -0
  306. package/src/skcapstone/testrunner.py +300 -0
  307. package/src/skcapstone/tls.py +150 -0
  308. package/src/skcapstone/tokens.py +5 -5
  309. package/src/skcapstone/trust_calibration.py +202 -0
  310. package/src/skcapstone/trust_graph.py +449 -0
  311. package/src/skcapstone/trustee_monitor.py +385 -0
  312. package/src/skcapstone/trustee_ops.py +425 -0
  313. package/src/skcapstone/unified_search.py +421 -0
  314. package/src/skcapstone/uninstall_wizard.py +694 -0
  315. package/src/skcapstone/usage.py +331 -0
  316. package/src/skcapstone/version_check.py +148 -0
  317. package/src/skcapstone/warmth_anchor.py +333 -0
  318. package/src/skcapstone/whoami.py +294 -0
  319. package/systemd/skcapstone-api.socket +9 -0
  320. package/systemd/skcapstone-memory-compress.service +18 -0
  321. package/systemd/skcapstone-memory-compress.timer +11 -0
  322. package/systemd/skcapstone.service +36 -0
  323. package/systemd/skcapstone@.service +50 -0
  324. package/systemd/skcomm-heartbeat.service +18 -0
  325. package/systemd/skcomm-heartbeat.timer +12 -0
  326. package/systemd/skcomm-queue-drain.service +17 -0
  327. package/systemd/skcomm-queue-drain.timer +12 -0
  328. package/tests/conftest.py +13 -1
  329. package/tests/integration/__init__.py +1 -0
  330. package/tests/integration/test_consciousness_e2e.py +877 -0
  331. package/tests/integration/test_skills_registry.py +744 -0
  332. package/tests/test_agent_card.py +190 -0
  333. package/tests/test_agent_runtime.py +1283 -0
  334. package/tests/test_alerts_cmd.py +291 -0
  335. package/tests/test_archiver.py +498 -0
  336. package/tests/test_backup.py +254 -0
  337. package/tests/test_benchmark.py +366 -0
  338. package/tests/test_blueprints.py +457 -0
  339. package/tests/test_capabilities.py +257 -0
  340. package/tests/test_changelog.py +254 -0
  341. package/tests/test_chat.py +385 -0
  342. package/tests/test_claude_md.py +271 -0
  343. package/tests/test_cli_chat_llm.py +336 -0
  344. package/tests/test_cli_completions.py +390 -0
  345. package/tests/test_cli_init_reset.py +164 -0
  346. package/tests/test_cli_memory.py +208 -0
  347. package/tests/test_cli_profile.py +294 -0
  348. package/tests/test_cli_skills.py +223 -0
  349. package/tests/test_cli_status.py +395 -0
  350. package/tests/test_cli_test_cmd.py +206 -0
  351. package/tests/test_cli_test_connection.py +364 -0
  352. package/tests/test_cloud9_bridge.py +260 -0
  353. package/tests/test_cloud_provider.py +449 -0
  354. package/tests/test_cloud_providers.py +522 -0
  355. package/tests/test_completions.py +158 -0
  356. package/tests/test_component_manager.py +398 -0
  357. package/tests/test_config_reload.py +386 -0
  358. package/tests/test_config_validate.py +529 -0
  359. package/tests/test_consciousness_e2e.py +296 -0
  360. package/tests/test_consciousness_loop.py +1289 -0
  361. package/tests/test_context_loader.py +310 -0
  362. package/tests/test_conversation_api.py +306 -0
  363. package/tests/test_conversation_manager.py +381 -0
  364. package/tests/test_conversation_store.py +391 -0
  365. package/tests/test_conversation_summarizer.py +302 -0
  366. package/tests/test_cross_package.py +791 -0
  367. package/tests/test_crush_shim.py +519 -0
  368. package/tests/test_daemon.py +781 -0
  369. package/tests/test_daemon_shutdown.py +309 -0
  370. package/tests/test_dashboard.py +454 -0
  371. package/tests/test_discovery.py +200 -6
  372. package/tests/test_docker_provider.py +966 -0
  373. package/tests/test_doctor.py +257 -0
  374. package/tests/test_doctor_fix.py +351 -0
  375. package/tests/test_e2e_automated.py +292 -0
  376. package/tests/test_error_queue.py +404 -0
  377. package/tests/test_export.py +441 -0
  378. package/tests/test_fallback_tracker.py +219 -0
  379. package/tests/test_file_transfer.py +397 -0
  380. package/tests/test_fuse_mount.py +832 -0
  381. package/tests/test_health_loop.py +422 -0
  382. package/tests/test_heartbeat.py +354 -0
  383. package/tests/test_housekeeping.py +195 -0
  384. package/tests/test_identity_capauth.py +307 -0
  385. package/tests/test_identity_pillar.py +117 -0
  386. package/tests/test_install_wizard.py +68 -0
  387. package/tests/test_integration.py +325 -0
  388. package/tests/test_kms.py +495 -0
  389. package/tests/test_llm_providers.py +265 -0
  390. package/tests/test_local_provider.py +591 -0
  391. package/tests/test_log_config.py +199 -0
  392. package/tests/test_logs_cmd.py +287 -0
  393. package/tests/test_mcp_server.py +1909 -0
  394. package/tests/test_memory_adapter.py +339 -0
  395. package/tests/test_memory_curator.py +218 -0
  396. package/tests/test_memory_engine.py +6 -0
  397. package/tests/test_memory_fortress.py +571 -0
  398. package/tests/test_memory_pillar.py +119 -0
  399. package/tests/test_memory_promoter.py +445 -0
  400. package/tests/test_memory_verifier.py +420 -0
  401. package/tests/test_message_crypto.py +187 -0
  402. package/tests/test_metrics.py +632 -0
  403. package/tests/test_migrate_memories.py +464 -0
  404. package/tests/test_model_router.py +546 -0
  405. package/tests/test_mood.py +394 -0
  406. package/tests/test_multi_agent.py +269 -0
  407. package/tests/test_notifications.py +270 -0
  408. package/tests/test_onboard.py +500 -0
  409. package/tests/test_peer_directory.py +395 -0
  410. package/tests/test_peers.py +248 -0
  411. package/tests/test_pillars.py +87 -9
  412. package/tests/test_preflight.py +484 -0
  413. package/tests/test_prompt_adapter.py +331 -0
  414. package/tests/test_proxmox_provider.py +571 -0
  415. package/tests/test_pubsub.py +377 -0
  416. package/tests/test_rate_limiter.py +121 -0
  417. package/tests/test_registry_client.py +129 -0
  418. package/tests/test_response_cache.py +312 -0
  419. package/tests/test_response_scorer.py +294 -0
  420. package/tests/test_runtime.py +59 -0
  421. package/tests/test_scheduled_tasks.py +451 -0
  422. package/tests/test_security.py +250 -0
  423. package/tests/test_security_pillar.py +213 -0
  424. package/tests/test_self_healing.py +171 -0
  425. package/tests/test_session_capture.py +200 -0
  426. package/tests/test_session_recorder.py +360 -0
  427. package/tests/test_session_skills.py +235 -0
  428. package/tests/test_shell.py +210 -0
  429. package/tests/test_snapshots.py +549 -0
  430. package/tests/test_soul.py +984 -0
  431. package/tests/test_soul_swap.py +406 -0
  432. package/tests/test_spawner.py +211 -0
  433. package/tests/test_state_diff.py +173 -0
  434. package/tests/test_summary.py +135 -0
  435. package/tests/test_sync.py +315 -5
  436. package/tests/test_sync_backends.py +560 -0
  437. package/tests/test_sync_engine.py +482 -0
  438. package/tests/test_sync_pillar.py +344 -0
  439. package/tests/test_sync_pipeline.py +364 -0
  440. package/tests/test_sync_vault.py +581 -0
  441. package/tests/test_syncthing_setup.py +168 -22
  442. package/tests/test_systemd.py +323 -0
  443. package/tests/test_team_comms.py +408 -0
  444. package/tests/test_team_engine.py +397 -0
  445. package/tests/test_testrunner.py +238 -0
  446. package/tests/test_trust_calibration.py +204 -0
  447. package/tests/test_trust_graph.py +207 -0
  448. package/tests/test_trust_pillar.py +291 -0
  449. package/tests/test_trustee_cli.py +427 -0
  450. package/tests/test_trustee_cli_integration.py +325 -0
  451. package/tests/test_trustee_monitor.py +394 -0
  452. package/tests/test_trustee_ops.py +355 -0
  453. package/tests/test_unified_search.py +363 -0
  454. package/tests/test_uninstall_wizard.py +193 -0
  455. package/tests/test_usage.py +333 -0
  456. package/tests/test_version_cmd.py +355 -0
  457. package/tests/test_warmth_anchor.py +162 -0
  458. package/tests/test_whoami.py +245 -0
  459. package/tests/test_ws.py +311 -0
  460. package/.cursorrules +0 -33
  461. package/src/skcapstone/cli.py +0 -1441
@@ -0,0 +1,257 @@
1
+ """Tests for agent capability advertisement.
2
+
3
+ Covers:
4
+ - AgentConfig defaults
5
+ - HeartbeatBeacon reading capabilities from config
6
+ - HeartbeatBeacon falling back to defaults when config absent
7
+ - Heartbeat pulse includes config capabilities
8
+ - CLI capabilities command (list, add, remove)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ from pathlib import Path
15
+
16
+ import pytest
17
+ import yaml
18
+ from click.testing import CliRunner
19
+
20
+ from skcapstone.heartbeat import AgentCapability, HeartbeatBeacon
21
+ from skcapstone.models import AgentConfig
22
+
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # AgentConfig defaults
26
+ # ---------------------------------------------------------------------------
27
+
28
+
29
+ class TestAgentConfigCapabilities:
30
+ """AgentConfig capabilities field."""
31
+
32
+ def test_default_capabilities(self) -> None:
33
+ """AgentConfig has the four default capabilities."""
34
+ cfg = AgentConfig()
35
+ assert cfg.capabilities == ["consciousness", "code", "chat", "memory"]
36
+
37
+ def test_custom_capabilities(self) -> None:
38
+ """AgentConfig accepts custom capabilities."""
39
+ cfg = AgentConfig(capabilities=["vector-search", "reasoning"])
40
+ assert cfg.capabilities == ["vector-search", "reasoning"]
41
+
42
+ def test_capabilities_preserved_in_yaml_roundtrip(self, tmp_path: Path) -> None:
43
+ """Capabilities survive a YAML serialise/deserialise round-trip."""
44
+ cfg = AgentConfig(capabilities=["consciousness", "code"])
45
+ config_path = tmp_path / "config.yaml"
46
+ config_path.write_text(
47
+ yaml.dump({"capabilities": cfg.capabilities}, default_flow_style=False),
48
+ encoding="utf-8",
49
+ )
50
+ loaded = yaml.safe_load(config_path.read_text(encoding="utf-8"))
51
+ restored = AgentConfig(**loaded)
52
+ assert restored.capabilities == ["consciousness", "code"]
53
+
54
+
55
+ # ---------------------------------------------------------------------------
56
+ # HeartbeatBeacon — capability loading
57
+ # ---------------------------------------------------------------------------
58
+
59
+
60
+ @pytest.fixture
61
+ def home(tmp_path: Path) -> Path:
62
+ """Minimal agent home."""
63
+ (tmp_path / "identity").mkdir()
64
+ (tmp_path / "identity" / "identity.json").write_text(
65
+ json.dumps({"name": "opus", "fingerprint": "ABCD1234567890AB"}),
66
+ encoding="utf-8",
67
+ )
68
+ return tmp_path
69
+
70
+
71
+ @pytest.fixture
72
+ def beacon(home: Path) -> HeartbeatBeacon:
73
+ b = HeartbeatBeacon(home, agent_name="opus")
74
+ b.initialize()
75
+ return b
76
+
77
+
78
+ class TestHeartbeatCapabilityLoading:
79
+ """HeartbeatBeacon reads capabilities from config."""
80
+
81
+ def test_defaults_when_no_config(self, beacon: HeartbeatBeacon) -> None:
82
+ """Falls back to AgentConfig defaults when config.yaml is absent."""
83
+ caps = beacon._load_config_capabilities()
84
+ assert caps == ["consciousness", "code", "chat", "memory"]
85
+
86
+ def test_reads_capabilities_from_config(self, beacon: HeartbeatBeacon, home: Path) -> None:
87
+ """Reads custom capabilities list from config.yaml."""
88
+ config_dir = home / "config"
89
+ config_dir.mkdir(parents=True, exist_ok=True)
90
+ (config_dir / "config.yaml").write_text(
91
+ yaml.dump({"capabilities": ["consciousness", "code", "vector-search"]}),
92
+ encoding="utf-8",
93
+ )
94
+ caps = beacon._load_config_capabilities()
95
+ assert caps == ["consciousness", "code", "vector-search"]
96
+
97
+ def test_detect_capabilities_includes_config_caps(
98
+ self, beacon: HeartbeatBeacon, home: Path
99
+ ) -> None:
100
+ """_detect_capabilities() includes all config-listed capabilities."""
101
+ config_dir = home / "config"
102
+ config_dir.mkdir(parents=True, exist_ok=True)
103
+ (config_dir / "config.yaml").write_text(
104
+ yaml.dump({"capabilities": ["consciousness", "code", "chat", "memory"]}),
105
+ encoding="utf-8",
106
+ )
107
+ caps = beacon._detect_capabilities()
108
+ names = [c.name for c in caps]
109
+ assert "consciousness" in names
110
+ assert "code" in names
111
+ assert "chat" in names
112
+ assert "memory" in names
113
+
114
+ def test_detect_capabilities_no_duplicates_from_packages(
115
+ self, beacon: HeartbeatBeacon, home: Path
116
+ ) -> None:
117
+ """Packages already in config list are not duplicated."""
118
+ config_dir = home / "config"
119
+ config_dir.mkdir(parents=True, exist_ok=True)
120
+ (config_dir / "config.yaml").write_text(
121
+ yaml.dump({"capabilities": ["skcapstone"]}),
122
+ encoding="utf-8",
123
+ )
124
+ caps = beacon._detect_capabilities()
125
+ names = [c.name for c in caps]
126
+ assert names.count("skcapstone") == 1
127
+
128
+ def test_pulse_heartbeat_has_config_capabilities(
129
+ self, beacon: HeartbeatBeacon, home: Path
130
+ ) -> None:
131
+ """Pulse embeds config capabilities in the published heartbeat."""
132
+ config_dir = home / "config"
133
+ config_dir.mkdir(parents=True, exist_ok=True)
134
+ (config_dir / "config.yaml").write_text(
135
+ yaml.dump({"capabilities": ["consciousness", "code", "chat", "memory"]}),
136
+ encoding="utf-8",
137
+ )
138
+ hb = beacon.pulse()
139
+ cap_names = [c.name for c in hb.capabilities]
140
+ assert "consciousness" in cap_names
141
+ assert "code" in cap_names
142
+ assert "chat" in cap_names
143
+ assert "memory" in cap_names
144
+
145
+ def test_pulse_heartbeat_persisted_capabilities(
146
+ self, beacon: HeartbeatBeacon, home: Path
147
+ ) -> None:
148
+ """Capabilities written to disk match what was pulsed."""
149
+ config_dir = home / "config"
150
+ config_dir.mkdir(parents=True, exist_ok=True)
151
+ (config_dir / "config.yaml").write_text(
152
+ yaml.dump({"capabilities": ["consciousness", "chat"]}),
153
+ encoding="utf-8",
154
+ )
155
+ beacon.pulse()
156
+ hb_file = home / "heartbeats" / "opus.json"
157
+ data = json.loads(hb_file.read_text(encoding="utf-8"))
158
+ persisted_names = [c["name"] for c in data["capabilities"]]
159
+ assert "consciousness" in persisted_names
160
+ assert "chat" in persisted_names
161
+
162
+
163
+ # ---------------------------------------------------------------------------
164
+ # CLI capabilities command
165
+ # ---------------------------------------------------------------------------
166
+
167
+
168
+ @pytest.fixture
169
+ def runner() -> CliRunner:
170
+ return CliRunner()
171
+
172
+
173
+ class TestCapabilitiesCLI:
174
+ """CLI: skcapstone capabilities."""
175
+
176
+ def test_capabilities_list_defaults(self, runner: CliRunner, tmp_path: Path) -> None:
177
+ """capabilities list shows defaults when no config present."""
178
+ from skcapstone.cli import main
179
+
180
+ result = runner.invoke(main, ["capabilities", "list", "--home", str(tmp_path)])
181
+ assert result.exit_code == 0
182
+ assert "consciousness" in result.output
183
+ assert "code" in result.output
184
+ assert "chat" in result.output
185
+ assert "memory" in result.output
186
+
187
+ def test_capabilities_list_json(self, runner: CliRunner, tmp_path: Path) -> None:
188
+ """capabilities list --json returns a JSON array."""
189
+ from skcapstone.cli import main
190
+
191
+ result = runner.invoke(
192
+ main, ["capabilities", "list", "--json", "--home", str(tmp_path)]
193
+ )
194
+ assert result.exit_code == 0
195
+ caps = json.loads(result.output)
196
+ assert isinstance(caps, list)
197
+ assert "consciousness" in caps
198
+
199
+ def test_capabilities_add(self, runner: CliRunner, tmp_path: Path) -> None:
200
+ """capabilities add appends a new capability to config."""
201
+ from skcapstone.cli import main
202
+
203
+ result = runner.invoke(
204
+ main, ["capabilities", "add", "vector-search", "--home", str(tmp_path)]
205
+ )
206
+ assert result.exit_code == 0
207
+ assert "vector-search" in result.output
208
+
209
+ # Verify it persisted
210
+ config_path = tmp_path / "config" / "config.yaml"
211
+ data = yaml.safe_load(config_path.read_text(encoding="utf-8"))
212
+ assert "vector-search" in data["capabilities"]
213
+
214
+ def test_capabilities_add_no_duplicate(self, runner: CliRunner, tmp_path: Path) -> None:
215
+ """capabilities add skips silently when capability already present."""
216
+ from skcapstone.cli import main
217
+
218
+ # Add a new capability first (ensures config file is created)
219
+ runner.invoke(main, ["capabilities", "add", "vector-search", "--home", str(tmp_path)])
220
+ config_path = tmp_path / "config" / "config.yaml"
221
+ data1 = yaml.safe_load(config_path.read_text(encoding="utf-8"))
222
+ count_before = data1["capabilities"].count("vector-search")
223
+
224
+ # Add the same capability again — should NOT duplicate
225
+ result = runner.invoke(
226
+ main, ["capabilities", "add", "vector-search", "--home", str(tmp_path)]
227
+ )
228
+ assert result.exit_code == 0
229
+ data2 = yaml.safe_load(config_path.read_text(encoding="utf-8"))
230
+ assert data2["capabilities"].count("vector-search") == count_before
231
+
232
+ def test_capabilities_remove(self, runner: CliRunner, tmp_path: Path) -> None:
233
+ """capabilities remove drops a capability from config."""
234
+ from skcapstone.cli import main
235
+
236
+ # First add so it's persisted
237
+ runner.invoke(main, ["capabilities", "add", "chat", "--home", str(tmp_path)])
238
+
239
+ result = runner.invoke(
240
+ main, ["capabilities", "remove", "chat", "--home", str(tmp_path)]
241
+ )
242
+ assert result.exit_code == 0
243
+ assert "chat" in result.output
244
+
245
+ config_path = tmp_path / "config" / "config.yaml"
246
+ data = yaml.safe_load(config_path.read_text(encoding="utf-8"))
247
+ assert "chat" not in data["capabilities"]
248
+
249
+ def test_capabilities_root_command_shows_table(
250
+ self, runner: CliRunner, tmp_path: Path
251
+ ) -> None:
252
+ """skcapstone capabilities (no subcommand) prints capability table."""
253
+ from skcapstone.cli import main
254
+
255
+ result = runner.invoke(main, ["capabilities", "--home", str(tmp_path)])
256
+ assert result.exit_code == 0
257
+ assert "consciousness" in result.output
@@ -0,0 +1,254 @@
1
+ """Tests for the Changelog Generator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ from skcapstone.changelog import (
11
+ TAG_CATEGORIES,
12
+ _categorize,
13
+ _parse_date,
14
+ generate_changelog,
15
+ write_changelog,
16
+ )
17
+
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Categorization
21
+ # ---------------------------------------------------------------------------
22
+
23
+
24
+ class TestCategorize:
25
+ """Tests for tag-based categorization."""
26
+
27
+ def test_feature_tags(self) -> None:
28
+ """Feature tags categorize correctly."""
29
+ assert _categorize(["skcapstone", "memory"]) == "feature"
30
+
31
+ def test_security_tags(self) -> None:
32
+ """Security tags categorize correctly."""
33
+ assert _categorize(["encryption", "security"]) == "security"
34
+
35
+ def test_infrastructure_tags(self) -> None:
36
+ """Infrastructure tags categorize correctly."""
37
+ assert _categorize(["docker", "ci"]) == "infrastructure"
38
+
39
+ def test_documentation_tags(self) -> None:
40
+ """Documentation tags categorize correctly."""
41
+ assert _categorize(["docs", "readme"]) == "documentation"
42
+
43
+ def test_empty_tags(self) -> None:
44
+ """Empty tags return 'other'."""
45
+ assert _categorize([]) == "other"
46
+
47
+ def test_unknown_tags(self) -> None:
48
+ """Unrecognized tags return 'other'."""
49
+ assert _categorize(["random", "unknown"]) == "other"
50
+
51
+ def test_best_match_wins(self) -> None:
52
+ """When multiple categories match, the one with most matches wins."""
53
+ result = _categorize(["skcapstone", "skchat", "security"])
54
+ assert result == "feature" # 2 feature matches vs 1 security
55
+
56
+ def test_case_insensitive(self) -> None:
57
+ """Tags are case-insensitive."""
58
+ assert _categorize(["SKCAPSTONE"]) == "feature"
59
+
60
+ def test_emotional_tags(self) -> None:
61
+ """Emotional/soul tags categorize correctly."""
62
+ assert _categorize(["cloud9", "soul", "trust"]) == "emotional"
63
+
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # Date parsing
67
+ # ---------------------------------------------------------------------------
68
+
69
+
70
+ class TestParseDate:
71
+ """Tests for ISO date extraction."""
72
+
73
+ def test_iso_datetime(self) -> None:
74
+ """Parses full ISO datetime."""
75
+ assert _parse_date("2026-02-24T12:00:00+00:00") == "2026-02-24"
76
+
77
+ def test_date_only(self) -> None:
78
+ """Parses date-only string."""
79
+ assert _parse_date("2026-01-15") == "2026-01-15"
80
+
81
+ def test_invalid_string(self) -> None:
82
+ """Invalid string returns today's date."""
83
+ result = _parse_date("not-a-date")
84
+ # Should be YYYY-MM-DD format
85
+ assert len(result) == 10
86
+ assert result[4] == "-"
87
+
88
+ def test_none_input(self) -> None:
89
+ """None input returns today's date."""
90
+ result = _parse_date(None)
91
+ assert len(result) == 10
92
+
93
+
94
+ # ---------------------------------------------------------------------------
95
+ # Changelog generation
96
+ # ---------------------------------------------------------------------------
97
+
98
+
99
+ def _setup_board(home: Path, tasks: list[dict]) -> None:
100
+ """Create a minimal coordination board with tasks and agent files.
101
+
102
+ Status is derived from agent files, not the task JSON. Tasks whose
103
+ ``status`` field is ``"done"`` are added to a test agent's
104
+ ``completed_tasks`` list so the Board marks them as done.
105
+ """
106
+ coord_dir = home / "coordination" / "tasks"
107
+ coord_dir.mkdir(parents=True)
108
+ agents_dir = home / "coordination" / "agents"
109
+ agents_dir.mkdir(parents=True)
110
+
111
+ completed_ids: list[str] = []
112
+ for task in tasks:
113
+ # Strip the non-schema "status" key before writing the task file.
114
+ task_data = {k: v for k, v in task.items() if k != "status"}
115
+ task_file = coord_dir / f"{task['id']}-test.json"
116
+ task_file.write_text(json.dumps(task_data), encoding="utf-8")
117
+
118
+ if task.get("status") == "done":
119
+ completed_ids.append(task["id"])
120
+
121
+ # Create an agent file so the Board can derive "done" status.
122
+ if completed_ids:
123
+ agent_file = agents_dir / "test-agent.json"
124
+ agent_file.write_text(
125
+ json.dumps({
126
+ "agent": "test-agent",
127
+ "completed_tasks": completed_ids,
128
+ "claimed_tasks": completed_ids,
129
+ }),
130
+ encoding="utf-8",
131
+ )
132
+
133
+
134
+ class TestGenerateChangelog:
135
+ """Tests for changelog generation."""
136
+
137
+ def test_empty_board(self, tmp_path: Path) -> None:
138
+ """Empty board generates minimal changelog."""
139
+ (tmp_path / "coordination" / "tasks").mkdir(parents=True)
140
+ (tmp_path / "coordination" / "agents").mkdir(parents=True)
141
+
142
+ result = generate_changelog(tmp_path)
143
+ assert "# SKCapstone Changelog" in result
144
+ assert "Total completed: 0" in result
145
+
146
+ def test_completed_tasks_appear(self, tmp_path: Path) -> None:
147
+ """Completed tasks appear in the changelog."""
148
+ _setup_board(tmp_path, [
149
+ {
150
+ "id": "abc123",
151
+ "title": "Add encrypted messaging",
152
+ "status": "done",
153
+ "priority": "high",
154
+ "tags": ["skchat", "encryption"],
155
+ "created_at": "2026-02-24T12:00:00+00:00",
156
+ },
157
+ ])
158
+ result = generate_changelog(tmp_path)
159
+ assert "Add encrypted messaging" in result
160
+
161
+ def test_open_tasks_excluded(self, tmp_path: Path) -> None:
162
+ """Open tasks do not appear in the changelog."""
163
+ _setup_board(tmp_path, [
164
+ {
165
+ "id": "abc123",
166
+ "title": "Pending feature",
167
+ "status": "open",
168
+ "priority": "medium",
169
+ "tags": ["skcapstone"],
170
+ "created_at": "2026-02-24T12:00:00+00:00",
171
+ },
172
+ ])
173
+ result = generate_changelog(tmp_path)
174
+ assert "Pending feature" not in result
175
+
176
+ def test_grouped_by_date(self, tmp_path: Path) -> None:
177
+ """Tasks are grouped by date."""
178
+ _setup_board(tmp_path, [
179
+ {
180
+ "id": "t1",
181
+ "title": "Task One",
182
+ "status": "done",
183
+ "priority": "medium",
184
+ "tags": ["skcapstone"],
185
+ "created_at": "2026-02-24T12:00:00+00:00",
186
+ },
187
+ {
188
+ "id": "t2",
189
+ "title": "Task Two",
190
+ "status": "done",
191
+ "priority": "medium",
192
+ "tags": ["skcapstone"],
193
+ "created_at": "2026-02-25T12:00:00+00:00",
194
+ },
195
+ ])
196
+ result = generate_changelog(tmp_path)
197
+ assert "## 2026-02-24" in result
198
+ assert "## 2026-02-25" in result
199
+
200
+ def test_custom_title(self, tmp_path: Path) -> None:
201
+ """Custom title is used."""
202
+ (tmp_path / "coordination" / "tasks").mkdir(parents=True)
203
+ (tmp_path / "coordination" / "agents").mkdir(parents=True)
204
+
205
+ result = generate_changelog(tmp_path, title="My Custom Changelog")
206
+ assert "# My Custom Changelog" in result
207
+
208
+ def test_without_agents(self, tmp_path: Path) -> None:
209
+ """Agent attribution can be disabled."""
210
+ _setup_board(tmp_path, [
211
+ {
212
+ "id": "t1",
213
+ "title": "Some Task",
214
+ "status": "done",
215
+ "priority": "medium",
216
+ "tags": ["skcapstone"],
217
+ "created_at": "2026-02-24T12:00:00+00:00",
218
+ },
219
+ ])
220
+ result = generate_changelog(tmp_path, include_agents=False)
221
+ assert "Some Task" in result
222
+ assert "(@" not in result.split("Some Task")[1].split("\n")[0]
223
+
224
+
225
+ # ---------------------------------------------------------------------------
226
+ # Write changelog
227
+ # ---------------------------------------------------------------------------
228
+
229
+
230
+ class TestWriteChangelog:
231
+ """Tests for writing changelog to file."""
232
+
233
+ def test_writes_to_default_path(self, tmp_path: Path, monkeypatch) -> None:
234
+ """write_changelog creates CHANGELOG.md."""
235
+ (tmp_path / "coordination" / "tasks").mkdir(parents=True)
236
+ (tmp_path / "coordination" / "agents").mkdir(parents=True)
237
+
238
+ output = tmp_path / "CHANGELOG.md"
239
+ result = write_changelog(tmp_path, output=output)
240
+ assert result == output
241
+ assert output.exists()
242
+ content = output.read_text(encoding="utf-8")
243
+ assert "SKCapstone Changelog" in content
244
+
245
+ def test_writes_to_custom_path(self, tmp_path: Path) -> None:
246
+ """write_changelog respects custom output path."""
247
+ (tmp_path / "coordination" / "tasks").mkdir(parents=True)
248
+ (tmp_path / "coordination" / "agents").mkdir(parents=True)
249
+
250
+ output = tmp_path / "custom" / "changes.md"
251
+ output.parent.mkdir(parents=True)
252
+ result = write_changelog(tmp_path, output=output)
253
+ assert result == output
254
+ assert output.exists()