@smilintux/skcapstone 0.10.0 → 0.12.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. package/.env.example +10 -4
  2. package/.github/workflows/ci.yml +2 -2
  3. package/.github/workflows/publish.yml +9 -2
  4. package/.openclaw-workspace.json +2 -2
  5. package/CLAUDE.md +37 -0
  6. package/MISSION.md +17 -2
  7. package/README.md +282 -3
  8. package/docker/Dockerfile +7 -7
  9. package/docker/compose-templates/dev-team.yml +12 -12
  10. package/docker/compose-templates/mini-team.yml +9 -9
  11. package/docker/compose-templates/ops-team.yml +10 -10
  12. package/docker/compose-templates/research-team.yml +10 -10
  13. package/docker/entrypoint.sh +4 -4
  14. package/docs/ADR-optional-integration-backbone.md +181 -0
  15. package/docs/ARCHITECTURE.md +186 -43
  16. package/docs/BOND_WITH_GROK.md +6 -6
  17. package/docs/CUSTOM_AGENT.md +123 -30
  18. package/docs/DREAMING.md +70 -0
  19. package/docs/GETTING_STARTED.md +7 -7
  20. package/docs/QUICKSTART.md +10 -6
  21. package/docs/SKJOULE_ARCHITECTURE.md +3 -3
  22. package/docs/SOUL_SWAPPER.md +5 -5
  23. package/docs/hammertime-audit.md +402 -0
  24. package/docs/sk-integration-HANDOFF.md +117 -0
  25. package/docs/skscheduler.md +155 -0
  26. package/docs/superpowers/examples/jobs.yaml +31 -0
  27. package/docs/superpowers/plans/2026-06-08-skscheduler.md +1265 -0
  28. package/docs/superpowers/specs/2026-06-08-skscheduler-design.md +186 -0
  29. package/examples/custom-bond-template.json +1 -1
  30. package/examples/grok-feb.json +1 -1
  31. package/examples/queen-ava-feb.json +1 -1
  32. package/launchd/{com.skcapstone.skcomm-heartbeat.plist → com.skcapstone.skcomms-heartbeat.plist} +4 -4
  33. package/launchd/{com.skcapstone.skcomm-queue-drain.plist → com.skcapstone.skcomms-queue-drain.plist} +4 -4
  34. package/launchd/install-launchd.sh +6 -6
  35. package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/src/index.ts +3 -2
  36. package/package.json +1 -1
  37. package/pyproject.toml +16 -10
  38. package/scripts/archive-sessions.sh +7 -0
  39. package/scripts/check-updates.py +4 -4
  40. package/scripts/install-bundle.sh +8 -8
  41. package/scripts/install.ps1 +12 -11
  42. package/scripts/install.sh +159 -5
  43. package/scripts/model-fallback-monitor.sh +102 -0
  44. package/scripts/nvidia-proxy.mjs +78 -26
  45. package/scripts/refresh-anthropic-token.sh +172 -0
  46. package/scripts/release.sh +98 -0
  47. package/scripts/session-to-memory.py +219 -0
  48. package/scripts/skgateway.mjs +3 -3
  49. package/scripts/telegram-catchup-all.sh +12 -1
  50. package/scripts/verify_install.sh +2 -2
  51. package/scripts/wargov-ufo-capture/README.md +43 -0
  52. package/scripts/wargov-ufo-capture/cdp_capture_release2.py +273 -0
  53. package/scripts/wargov-ufo-capture/cdp_capture_splc_doj.py +246 -0
  54. package/scripts/wargov-ufo-capture/cdp_finish.py +271 -0
  55. package/scripts/wargov-ufo-capture/cdp_probe.py +188 -0
  56. package/scripts/wargov-ufo-capture/cdp_splc_pressrelease.py +101 -0
  57. package/scripts/wargov-ufo-capture/parse_csv.py +95 -0
  58. package/scripts/wargov-ufo-capture/pull_dvids.sh +107 -0
  59. package/scripts/watch-anthropic-token.sh +212 -0
  60. package/scripts/windows/install-tasks.ps1 +7 -7
  61. package/scripts/windows/skcapstone-task.xml +1 -1
  62. package/src/skcapstone/__init__.py +45 -3
  63. package/src/skcapstone/_cli_monolith.py +20 -15
  64. package/src/skcapstone/activity.py +5 -1
  65. package/src/skcapstone/agent_card.py +3 -2
  66. package/src/skcapstone/api.py +41 -40
  67. package/src/skcapstone/auction.py +14 -11
  68. package/src/skcapstone/backup.py +2 -1
  69. package/src/skcapstone/blueprint_registry.py +4 -3
  70. package/src/skcapstone/brain_first.py +238 -0
  71. package/src/skcapstone/changelog.py +1 -1
  72. package/src/skcapstone/chat.py +22 -17
  73. package/src/skcapstone/cli/__init__.py +9 -1
  74. package/src/skcapstone/cli/_common.py +1 -0
  75. package/src/skcapstone/cli/agents_spawner.py +5 -2
  76. package/src/skcapstone/cli/alerts.py +25 -4
  77. package/src/skcapstone/cli/bench.py +15 -15
  78. package/src/skcapstone/cli/chat.py +7 -4
  79. package/src/skcapstone/cli/consciousness.py +5 -2
  80. package/src/skcapstone/cli/context_cmd.py +18 -4
  81. package/src/skcapstone/cli/daemon.py +11 -7
  82. package/src/skcapstone/cli/gtd.py +26 -1
  83. package/src/skcapstone/cli/housekeeping.py +3 -3
  84. package/src/skcapstone/cli/identity_cmd.py +378 -0
  85. package/src/skcapstone/cli/joule_cmd.py +7 -3
  86. package/src/skcapstone/cli/memory.py +8 -6
  87. package/src/skcapstone/cli/peers_dir.py +1 -1
  88. package/src/skcapstone/cli/register_cmd.py +29 -3
  89. package/src/skcapstone/cli/scheduler_cmd.py +167 -0
  90. package/src/skcapstone/cli/session.py +25 -0
  91. package/src/skcapstone/cli/setup.py +96 -29
  92. package/src/skcapstone/cli/shell_cmd.py +53 -1
  93. package/src/skcapstone/cli/skills_cmd.py +2 -2
  94. package/src/skcapstone/cli/soul.py +8 -5
  95. package/src/skcapstone/cli/status.py +37 -11
  96. package/src/skcapstone/cli/telegram.py +21 -0
  97. package/src/skcapstone/cli/test_cmd.py +5 -5
  98. package/src/skcapstone/cli/test_connection.py +2 -2
  99. package/src/skcapstone/cli/upgrade_cmd.py +23 -14
  100. package/src/skcapstone/cli/version_cmd.py +1 -1
  101. package/src/skcapstone/cli/watch_cmd.py +9 -6
  102. package/src/skcapstone/cloud9_bridge.py +14 -14
  103. package/src/skcapstone/codex_setup.py +255 -0
  104. package/src/skcapstone/config_validator.py +7 -4
  105. package/src/skcapstone/consciousness_config.py +5 -1
  106. package/src/skcapstone/consciousness_loop.py +313 -273
  107. package/src/skcapstone/context_loader.py +121 -0
  108. package/src/skcapstone/coord_federation.py +2 -1
  109. package/src/skcapstone/coordination.py +23 -6
  110. package/src/skcapstone/crush_integration.py +2 -1
  111. package/src/skcapstone/daemon.py +132 -77
  112. package/src/skcapstone/dashboard.py +10 -10
  113. package/src/skcapstone/data/sk-agent-picker.sh +421 -0
  114. package/src/skcapstone/data/systemd/skcapstone-api.socket +9 -0
  115. package/src/skcapstone/data/systemd/skcapstone-memory-compress.service +18 -0
  116. package/src/skcapstone/data/systemd/skcapstone-memory-compress.timer +11 -0
  117. package/src/skcapstone/data/systemd/skcapstone.service +37 -0
  118. package/src/skcapstone/data/systemd/skcapstone@.service +50 -0
  119. package/src/skcapstone/data/systemd/skcomms-heartbeat.service +18 -0
  120. package/{systemd/skcomm-heartbeat.timer → src/skcapstone/data/systemd/skcomms-heartbeat.timer} +2 -2
  121. package/src/skcapstone/data/systemd/skcomms-queue-drain.service +17 -0
  122. package/{systemd/skcomm-queue-drain.timer → src/skcapstone/data/systemd/skcomms-queue-drain.timer} +2 -2
  123. package/src/skcapstone/defaults/claude/CLAUDE.md +67 -0
  124. package/src/skcapstone/defaults/claude/settings.json +74 -0
  125. package/src/skcapstone/defaults/lumina/config/claude-hooks.md +57 -0
  126. package/src/skcapstone/defaults/lumina/config/skgraph.yaml +55 -10
  127. package/src/skcapstone/defaults/lumina/config/skmemory.yaml +79 -13
  128. package/src/skcapstone/defaults/lumina/config/skvector.yaml +60 -9
  129. package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +2 -2
  130. package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +2 -2
  131. package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +9 -9
  132. package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +2 -2
  133. package/src/skcapstone/defaults/unhinged.json +13 -0
  134. package/src/skcapstone/discovery.py +43 -20
  135. package/src/skcapstone/doctor.py +941 -22
  136. package/src/skcapstone/dreaming.py +1183 -109
  137. package/src/skcapstone/emotion_tracker.py +2 -2
  138. package/src/skcapstone/export.py +4 -3
  139. package/src/skcapstone/fuse_mount.py +14 -12
  140. package/src/skcapstone/gui_installer.py +2 -2
  141. package/src/skcapstone/heartbeat.py +1 -1
  142. package/src/skcapstone/housekeeping.py +14 -14
  143. package/src/skcapstone/install_wizard.py +209 -7
  144. package/src/skcapstone/itil.py +13 -4
  145. package/src/skcapstone/kms_scheduler.py +10 -8
  146. package/src/skcapstone/launchd.py +19 -19
  147. package/src/skcapstone/mcp_launcher.py +15 -1
  148. package/src/skcapstone/mcp_server.py +83 -49
  149. package/src/skcapstone/mcp_tools/__init__.py +2 -0
  150. package/src/skcapstone/mcp_tools/_helpers.py +2 -2
  151. package/src/skcapstone/mcp_tools/ansible_tools.py +7 -4
  152. package/src/skcapstone/mcp_tools/brain_first_tools.py +90 -0
  153. package/src/skcapstone/mcp_tools/capauth_tools.py +7 -4
  154. package/src/skcapstone/mcp_tools/comm_tools.py +10 -10
  155. package/src/skcapstone/mcp_tools/coord_tools.py +8 -4
  156. package/src/skcapstone/mcp_tools/did_tools.py +11 -8
  157. package/src/skcapstone/mcp_tools/gtd_tools.py +4 -4
  158. package/src/skcapstone/mcp_tools/memory_tools.py +6 -2
  159. package/src/skcapstone/mcp_tools/notification_tools.py +22 -6
  160. package/src/skcapstone/mcp_tools/{skcomm_tools.py → skcomms_tools.py} +14 -14
  161. package/src/skcapstone/mcp_tools/soul_tools.py +8 -2
  162. package/src/skcapstone/mdns_discovery.py +2 -2
  163. package/src/skcapstone/memory_curator.py +1 -1
  164. package/src/skcapstone/memory_engine.py +10 -3
  165. package/src/skcapstone/metrics.py +30 -16
  166. package/src/skcapstone/migrate_memories.py +4 -3
  167. package/src/skcapstone/migrate_multi_agent.py +8 -7
  168. package/src/skcapstone/models.py +47 -5
  169. package/src/skcapstone/notifications.py +42 -18
  170. package/src/skcapstone/onboard.py +875 -121
  171. package/src/skcapstone/operator_link.py +170 -0
  172. package/src/skcapstone/peer_directory.py +4 -4
  173. package/src/skcapstone/peers.py +19 -19
  174. package/src/skcapstone/pillars/__init__.py +7 -5
  175. package/src/skcapstone/pillars/consciousness.py +191 -0
  176. package/src/skcapstone/pillars/identity.py +51 -7
  177. package/src/skcapstone/pillars/memory.py +9 -3
  178. package/src/skcapstone/pillars/sync.py +2 -2
  179. package/src/skcapstone/preflight.py +3 -3
  180. package/src/skcapstone/providers/docker.py +28 -28
  181. package/src/skcapstone/register.py +6 -6
  182. package/src/skcapstone/registry_client.py +5 -4
  183. package/src/skcapstone/runtime.py +14 -3
  184. package/src/skcapstone/scheduled_tasks.py +254 -19
  185. package/src/skcapstone/scheduler_jobs.py +456 -0
  186. package/src/skcapstone/scheduler_runner.py +239 -0
  187. package/src/skcapstone/scheduler_state.py +162 -0
  188. package/src/skcapstone/sdk.py +310 -0
  189. package/src/skcapstone/service_health.py +279 -39
  190. package/src/skcapstone/session_briefing.py +108 -0
  191. package/src/skcapstone/session_capture.py +1 -1
  192. package/src/skcapstone/shell.py +7 -1
  193. package/src/skcapstone/soul.py +3 -1
  194. package/src/skcapstone/soul_switch.py +3 -1
  195. package/src/skcapstone/summary.py +6 -6
  196. package/src/skcapstone/sync_engine.py +15 -15
  197. package/src/skcapstone/sync_watcher.py +2 -2
  198. package/src/skcapstone/systemd.py +55 -21
  199. package/src/skcapstone/team_comms.py +8 -8
  200. package/src/skcapstone/team_engine.py +1 -1
  201. package/src/skcapstone/testrunner.py +3 -3
  202. package/src/skcapstone/trust_graph.py +40 -5
  203. package/src/skcapstone/unified_search.py +15 -6
  204. package/src/skcapstone/uninstall_wizard.py +11 -3
  205. package/src/skcapstone/version_check.py +8 -4
  206. package/src/skcapstone/warmth_anchor.py +4 -2
  207. package/src/skcapstone/whoami.py +4 -4
  208. package/systemd/skcapstone.service +4 -6
  209. package/systemd/skcapstone@.service +7 -8
  210. package/systemd/skcomms-heartbeat.service +21 -0
  211. package/systemd/skcomms-heartbeat.timer +12 -0
  212. package/systemd/skcomms-queue-drain.service +17 -0
  213. package/systemd/skcomms-queue-drain.timer +12 -0
  214. package/tests/conftest.py +39 -0
  215. package/tests/integration/test_consciousness_e2e.py +39 -39
  216. package/tests/test_agent_card.py +1 -1
  217. package/tests/test_agent_home_scaffold.py +34 -0
  218. package/tests/test_alerts_consumer_topics.py +27 -0
  219. package/tests/test_backup.py +2 -1
  220. package/tests/test_chat.py +6 -6
  221. package/tests/test_claude_md.py +2 -2
  222. package/tests/test_cli_skills.py +10 -10
  223. package/tests/test_cli_test_cmd.py +4 -4
  224. package/tests/test_cli_test_connection.py +1 -1
  225. package/tests/test_cloud9_bridge.py +6 -6
  226. package/tests/test_consciousness_e2e.py +1 -1
  227. package/tests/test_consciousness_loop.py +10 -10
  228. package/tests/test_coordination.py +25 -0
  229. package/tests/test_cross_package.py +21 -21
  230. package/tests/test_daemon.py +4 -4
  231. package/tests/test_daemon_shutdown.py +1 -1
  232. package/tests/test_docker_provider.py +29 -29
  233. package/tests/test_doctor.py +400 -0
  234. package/tests/test_doctor_skscheduler.py +50 -0
  235. package/tests/test_dreaming_engine.py +147 -0
  236. package/tests/test_dreaming_gtd_capture.py +35 -0
  237. package/tests/test_e2e_automated.py +8 -5
  238. package/tests/test_fuse_mount.py +10 -10
  239. package/tests/test_gtd_brief.py +46 -0
  240. package/tests/test_gtd_malformed_tolerance.py +31 -0
  241. package/tests/test_housekeeping.py +15 -15
  242. package/tests/test_identity_migrate.py +251 -0
  243. package/tests/test_integration_backbone.py +598 -0
  244. package/tests/test_itil_gtd_lifecycle.py +37 -0
  245. package/tests/test_jobs_dropins.py +84 -0
  246. package/tests/test_mcp_server.py +82 -37
  247. package/tests/test_models.py +48 -4
  248. package/tests/test_multi_agent.py +31 -29
  249. package/tests/test_notifications.py +122 -32
  250. package/tests/test_onboard.py +63 -75
  251. package/tests/test_operator_link.py +78 -0
  252. package/tests/test_peers.py +14 -14
  253. package/tests/test_pillars.py +98 -0
  254. package/tests/test_preflight.py +3 -3
  255. package/tests/test_runtime.py +21 -0
  256. package/tests/test_scheduled_tasks.py +11 -6
  257. package/tests/test_scheduler_cli.py +47 -0
  258. package/tests/test_scheduler_features.py +133 -0
  259. package/tests/test_scheduler_integration.py +87 -0
  260. package/tests/test_scheduler_jobs.py +155 -0
  261. package/tests/test_scheduler_runner.py +64 -0
  262. package/tests/test_scheduler_state.py +57 -0
  263. package/tests/test_sdk.py +70 -0
  264. package/tests/test_service_health_incidents.py +34 -0
  265. package/tests/test_service_registry.py +52 -0
  266. package/tests/test_session_briefing.py +130 -0
  267. package/tests/test_snapshots.py +4 -4
  268. package/tests/test_sync_pipeline.py +26 -26
  269. package/tests/test_team_comms.py +2 -2
  270. package/tests/test_testrunner.py +2 -2
  271. package/tests/test_trust_graph.py +18 -0
  272. package/tests/test_unified_search.py +2 -2
  273. package/tests/test_version_check.py +10 -0
  274. package/tests/test_version_cmd.py +8 -8
  275. package/tests/test_whoami.py +1 -1
  276. package/systemd/skcomm-heartbeat.service +0 -18
  277. package/systemd/skcomm-queue-drain.service +0 -17
  278. /package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/package.json +0 -0
  279. /package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/src/openclaw.plugin.json +0 -0
@@ -101,7 +101,7 @@ def _poll_for_response(
101
101
  if skc.stat().st_mtime > ref_mtime:
102
102
  return True
103
103
 
104
- # Check conversations file (passthrough / no-SKComm fallback)
104
+ # Check conversations file (passthrough / no-SKComms fallback)
105
105
  if conv_file.exists() and conv_file.stat().st_mtime > ref_mtime:
106
106
  return True
107
107
 
@@ -217,11 +217,14 @@ class TestDaemonStartup:
217
217
  data = json.loads(resp.read())
218
218
 
219
219
  status = str(data.get("status", "")).lower()
220
- # Accept various status strings that indicate the loop is running
220
+ # Accept both the legacy status/conscious contract and the current
221
+ # enabled-based daemon payload.
221
222
  active_statuses = {"active", "ok", "running", "started", "conscious"}
222
- assert status in active_statuses or data.get("conscious") is True, (
223
- f"Expected active status, got: {data}"
224
- )
223
+ assert (
224
+ data.get("enabled") is True
225
+ or status in active_statuses
226
+ or data.get("conscious") is True
227
+ ), f"Expected active status, got: {data}"
225
228
 
226
229
 
227
230
  class TestMessageRoundTrip:
@@ -34,7 +34,7 @@ from skcapstone.fuse_mount import (
34
34
  _read_coordination_task,
35
35
  _read_document,
36
36
  _read_inbox_file,
37
- _send_via_skcomm,
37
+ _send_via_skcomms,
38
38
  )
39
39
 
40
40
 
@@ -426,10 +426,10 @@ class TestFileHelpers:
426
426
  fp = _build_fingerprint_txt(agent_home)
427
427
  assert fp == b"AABBCCDD\n"
428
428
 
429
- def test_send_via_skcomm_fallback_to_outbox(self, agent_home: Path) -> None:
429
+ def test_send_via_skcomms_fallback_to_outbox(self, agent_home: Path) -> None:
430
430
  """When CLI is unavailable, message is queued as JSON envelope in outbox."""
431
431
  with patch("skcapstone.fuse_mount.subprocess.run", side_effect=FileNotFoundError):
432
- result = _send_via_skcomm(agent_home, "jarvis", "Hello Jarvis")
432
+ result = _send_via_skcomms(agent_home, "jarvis", "Hello Jarvis")
433
433
  assert result is True
434
434
  outbox = agent_home / "comms" / "outbox"
435
435
  files = list(outbox.glob("jarvis_*.json"))
@@ -658,12 +658,12 @@ class TestSovereignFS:
658
658
  sovereign_fs.write("/memories/short/x.md", b"data", offset=0, fh=0)
659
659
  assert exc_info.value.errno == errno.EACCES
660
660
 
661
- def test_flush_sends_via_skcomm(
661
+ def test_flush_sends_via_skcomms(
662
662
  self, sovereign_fs: SovereignFS
663
663
  ) -> None:
664
- """Flushing an outbox file invokes _send_via_skcomm with correct args."""
664
+ """Flushing an outbox file invokes _send_via_skcomms with correct args."""
665
665
  sovereign_fs._outbox_buffers["/outbox/jarvis.msg"] = b"Test message"
666
- with patch("skcapstone.fuse_mount._send_via_skcomm", return_value=True) as mock_send:
666
+ with patch("skcapstone.fuse_mount._send_via_skcomms", return_value=True) as mock_send:
667
667
  sovereign_fs.flush("/outbox/jarvis.msg", fh=0)
668
668
  mock_send.assert_called_once_with(
669
669
  sovereign_fs._home, "jarvis", "Test message"
@@ -674,14 +674,14 @@ class TestSovereignFS:
674
674
  def test_flush_strips_msg_suffix(self, sovereign_fs: SovereignFS) -> None:
675
675
  """Flush extracts the recipient by stripping the .msg suffix."""
676
676
  sovereign_fs._outbox_buffers["/outbox/lumina.msg"] = b"Hi"
677
- with patch("skcapstone.fuse_mount._send_via_skcomm", return_value=True) as mock_send:
677
+ with patch("skcapstone.fuse_mount._send_via_skcomms", return_value=True) as mock_send:
678
678
  sovereign_fs.flush("/outbox/lumina.msg", fh=0)
679
679
  assert mock_send.call_args[0][1] == "lumina"
680
680
 
681
681
  def test_flush_empty_buffer_no_send(self, sovereign_fs: SovereignFS) -> None:
682
- """Flushing an empty buffer does not call _send_via_skcomm."""
682
+ """Flushing an empty buffer does not call _send_via_skcomms."""
683
683
  sovereign_fs._outbox_buffers["/outbox/ava.msg"] = b""
684
- with patch("skcapstone.fuse_mount._send_via_skcomm") as mock_send:
684
+ with patch("skcapstone.fuse_mount._send_via_skcomms") as mock_send:
685
685
  sovereign_fs.flush("/outbox/ava.msg", fh=0)
686
686
  mock_send.assert_not_called()
687
687
 
@@ -723,7 +723,7 @@ class TestSovereignFS:
723
723
  def test_release_flushes_outbox(self, sovereign_fs: SovereignFS) -> None:
724
724
  """Release calls flush, which sends the outbox buffer."""
725
725
  sovereign_fs._outbox_buffers["/outbox/ava.msg"] = b"Goodbye"
726
- with patch("skcapstone.fuse_mount._send_via_skcomm", return_value=True) as mock_send:
726
+ with patch("skcapstone.fuse_mount._send_via_skcomms", return_value=True) as mock_send:
727
727
  sovereign_fs.release("/outbox/ava.msg", fh=0)
728
728
  mock_send.assert_called_once()
729
729
 
@@ -0,0 +1,46 @@
1
+ """`skcapstone gtd status --brief` — one-line summary for the SessionStart hook."""
2
+ import json
3
+ from pathlib import Path
4
+
5
+ import click
6
+ from click.testing import CliRunner
7
+
8
+ import skcapstone.mcp_tools._helpers as _helpers
9
+ from skcapstone.cli.gtd import register_gtd_commands
10
+
11
+
12
+ def _app(tmp_path: Path, monkeypatch) -> click.Group:
13
+ monkeypatch.setattr(_helpers, "SHARED_ROOT", str(tmp_path))
14
+ gtd_dir = tmp_path / "coordination" / "gtd"
15
+ gtd_dir.mkdir(parents=True)
16
+ (gtd_dir / "inbox.json").write_text(
17
+ json.dumps([{"id": "a", "text": "one"}, {"id": "b", "text": "two"}]),
18
+ encoding="utf-8",
19
+ )
20
+ (gtd_dir / "next-actions.json").write_text(
21
+ json.dumps([{"id": "c", "text": "do"}]), encoding="utf-8"
22
+ )
23
+
24
+ @click.group()
25
+ def main() -> None:
26
+ pass
27
+
28
+ register_gtd_commands(main)
29
+ return main
30
+
31
+
32
+ def test_brief_is_single_line_with_counts(tmp_path: Path, monkeypatch):
33
+ res = CliRunner().invoke(_app(tmp_path, monkeypatch), ["gtd", "status", "--brief"])
34
+ assert res.exit_code == 0
35
+ out = res.output.strip()
36
+ assert out.count("\n") == 0 # exactly one line
37
+ assert out.startswith("GTD:")
38
+ assert "2 inbox" in out and "1 next" in out
39
+
40
+
41
+ def test_brief_differs_from_full(tmp_path: Path, monkeypatch):
42
+ app = _app(tmp_path, monkeypatch)
43
+ brief = CliRunner().invoke(app, ["gtd", "status", "--brief"]).output
44
+ full = CliRunner().invoke(app, ["gtd", "status"]).output
45
+ assert len(brief.strip().splitlines()) == 1
46
+ assert len(full.strip().splitlines()) > 1 # full is the rich multi-line panel
@@ -0,0 +1,31 @@
1
+ """GTD tools tolerate malformed (title/body, no 'text') items without crashing."""
2
+ import asyncio
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import skcapstone.mcp_tools._helpers as _helpers
7
+ from skcapstone.mcp_tools.gtd_tools import _handle_gtd_done
8
+
9
+
10
+ def test_gtd_done_tolerates_item_without_text(tmp_path: Path, monkeypatch):
11
+ monkeypatch.setattr(_helpers, "SHARED_ROOT", str(tmp_path))
12
+ gtd = tmp_path / "coordination" / "gtd"
13
+ gtd.mkdir(parents=True)
14
+ # A malformed legacy item: title/body schema, no 'text' (what the dead
15
+ # reflection/improvement crons wrote and what used to KeyError gtd_done).
16
+ (gtd / "inbox.json").write_text(
17
+ json.dumps(
18
+ [{"id": "m1", "title": "Daily Reflection", "body": "noise",
19
+ "source": "daily-reflection-cron"}]
20
+ ),
21
+ encoding="utf-8",
22
+ )
23
+
24
+ # Must not raise (previously raised KeyError: 'text').
25
+ result = asyncio.run(_handle_gtd_done({"item_id": "m1"}))
26
+ assert result # got a response
27
+
28
+ inbox = json.loads((gtd / "inbox.json").read_text())
29
+ assert not any(i.get("id") == "m1" for i in inbox)
30
+ archive = json.loads((gtd / "archive.json").read_text())
31
+ assert any(a.get("id") == "m1" for a in archive)
@@ -14,8 +14,8 @@ from skcapstone.housekeeping import (
14
14
 
15
15
 
16
16
  @pytest.fixture
17
- def skcomm_home(tmp_path):
18
- """Create a mock ~/.skcomm directory with test ACK files."""
17
+ def skcomms_home(tmp_path):
18
+ """Create a mock ~/.skcomms directory with test ACK files."""
19
19
  acks_dir = tmp_path / "acks"
20
20
  acks_dir.mkdir()
21
21
  return tmp_path
@@ -39,13 +39,13 @@ class TestPruneAcks:
39
39
  """Returns 0 when acks directory doesn't exist."""
40
40
  assert prune_acks(tmp_path) == 0
41
41
 
42
- def test_empty_acks_dir(self, skcomm_home):
42
+ def test_empty_acks_dir(self, skcomms_home):
43
43
  """Returns 0 when acks directory is empty."""
44
- assert prune_acks(skcomm_home) == 0
44
+ assert prune_acks(skcomms_home) == 0
45
45
 
46
- def test_deletes_old_acks(self, skcomm_home):
46
+ def test_deletes_old_acks(self, skcomms_home):
47
47
  """Deletes ACK files older than max_age_hours."""
48
- acks_dir = skcomm_home / "acks"
48
+ acks_dir = skcomms_home / "acks"
49
49
  # Create 5 old files (mtime set to 48h ago)
50
50
  old_time = time.time() - (48 * 3600)
51
51
  for i in range(5):
@@ -59,22 +59,22 @@ class TestPruneAcks:
59
59
  f = acks_dir / f"fresh-{i}.json"
60
60
  f.write_text("{}")
61
61
 
62
- deleted = prune_acks(skcomm_home, max_age_hours=24)
62
+ deleted = prune_acks(skcomms_home, max_age_hours=24)
63
63
  assert deleted == 5
64
64
  remaining = list(acks_dir.iterdir())
65
65
  assert len(remaining) == 3
66
66
 
67
- def test_respects_max_age(self, skcomm_home):
67
+ def test_respects_max_age(self, skcomms_home):
68
68
  """Only deletes files older than the specified max_age."""
69
- acks_dir = skcomm_home / "acks"
69
+ acks_dir = skcomms_home / "acks"
70
70
  # File 1h old
71
71
  f = acks_dir / "recent.json"
72
72
  f.write_text("{}")
73
73
  import os
74
74
  os.utime(f, (time.time() - 3600, time.time() - 3600))
75
75
 
76
- assert prune_acks(skcomm_home, max_age_hours=2) == 0
77
- assert prune_acks(skcomm_home, max_age_hours=0) == 1
76
+ assert prune_acks(skcomms_home, max_age_hours=2) == 0
77
+ assert prune_acks(skcomms_home, max_age_hours=0) == 1
78
78
 
79
79
 
80
80
  class TestPruneCommsOutbox:
@@ -155,7 +155,7 @@ class TestRunHousekeeping:
155
155
  def test_dry_run(self, tmp_path):
156
156
  """Dry run reports counts without deleting."""
157
157
  # Set up dirs
158
- acks_dir = tmp_path / "skcomm" / "acks"
158
+ acks_dir = tmp_path / "skcomms" / "acks"
159
159
  acks_dir.mkdir(parents=True)
160
160
  for i in range(3):
161
161
  f = acks_dir / f"old-{i}.json"
@@ -165,7 +165,7 @@ class TestRunHousekeeping:
165
165
 
166
166
  results = run_housekeeping(
167
167
  skcapstone_home=tmp_path / "skcapstone",
168
- skcomm_home=tmp_path / "skcomm",
168
+ skcomms_home=tmp_path / "skcomms",
169
169
  dry_run=True,
170
170
  )
171
171
 
@@ -176,7 +176,7 @@ class TestRunHousekeeping:
176
176
 
177
177
  def test_full_run(self, tmp_path):
178
178
  """Full run deletes files and reports summary."""
179
- acks_dir = tmp_path / "skcomm" / "acks"
179
+ acks_dir = tmp_path / "skcomms" / "acks"
180
180
  acks_dir.mkdir(parents=True)
181
181
  for i in range(2):
182
182
  f = acks_dir / f"old-{i}.json"
@@ -186,7 +186,7 @@ class TestRunHousekeeping:
186
186
 
187
187
  results = run_housekeeping(
188
188
  skcapstone_home=tmp_path / "skcapstone",
189
- skcomm_home=tmp_path / "skcomm",
189
+ skcomms_home=tmp_path / "skcomms",
190
190
  dry_run=False,
191
191
  )
192
192
 
@@ -0,0 +1,251 @@
1
+ """Tests for ``skcapstone identity migrate`` (skcomms T2 migration walker).
2
+
3
+ The walker backfills realm/operator/fqid/pgp_fingerprint into every
4
+ provisioned agent's identity.json. These tests use a tmp ``~/.skcapstone`` home
5
+ with fixture agents + cluster.json — they NEVER touch the real home. The
6
+ canonical resolver (``capauth.resolve_agent_identity``) is patched so fqid and
7
+ fingerprint are deterministic and no real profile/cluster is read.
8
+
9
+ Covers:
10
+ - bare identity.json gets realm/operator/fqid/pgp_fingerprint added
11
+ - already-complete identity is unchanged (idempotent)
12
+ - dry-run (the default) writes nothing
13
+ - template / non-capauth agents are skipped
14
+ - missing cluster.json is handled gracefully
15
+ - CLI integration (default is dry-run; --apply writes)
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ from pathlib import Path
22
+ from types import SimpleNamespace
23
+ from unittest.mock import patch
24
+
25
+ import pytest
26
+ from click.testing import CliRunner
27
+
28
+ from skcapstone.cli.identity_cmd import migrate_identities, register_identity_commands
29
+
30
+ # Patch target: the name as imported inside identity_cmd's _plan_agent.
31
+ _RESOLVER = "capauth.resolve_agent_identity"
32
+
33
+
34
+ def _fake_ident(agent: str, *, fqid="{a}@chef.skworld", fingerprint="A" * 40):
35
+ """Build a fake AgentIdentity-like object for a given agent."""
36
+ return SimpleNamespace(
37
+ agent=agent,
38
+ capauth_uri=f"capauth:{agent}@skworld.io",
39
+ fqid=fqid.format(a=agent) if fqid else None,
40
+ fingerprint=fingerprint,
41
+ )
42
+
43
+
44
+ def _mk_agent(home, name, *, capauth=True, identity_payload=None):
45
+ """Create an agent dir under home/agents with optional capauth + identity."""
46
+ adir = home / "agents" / name
47
+ (adir / "identity").mkdir(parents=True, exist_ok=True)
48
+ if capauth:
49
+ (adir / "capauth").mkdir(parents=True, exist_ok=True)
50
+ if identity_payload is not None:
51
+ (adir / "identity" / "identity.json").write_text(json.dumps(identity_payload))
52
+ return adir
53
+
54
+
55
+ @pytest.fixture
56
+ def home_with_cluster(tmp_path):
57
+ """A tmp shared root with cluster.json + one bare-identity provisioned agent."""
58
+ home = tmp_path / ".skcapstone"
59
+ home.mkdir(parents=True)
60
+ (home / "cluster.json").write_text(json.dumps({
61
+ "realm": "skworld", "operator": "chef",
62
+ }))
63
+ _mk_agent(home, "lumina", identity_payload={
64
+ "name": "Lumina", "capauth_managed": True,
65
+ "capauth_uri": "capauth:lumina@skworld.io",
66
+ })
67
+ return home
68
+
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # core walker: migrate_identities
72
+ # ---------------------------------------------------------------------------
73
+
74
+
75
+ class TestMigrateWalker:
76
+ """migrate_identities core behaviour."""
77
+
78
+ def test_bare_identity_gets_all_fields(self, home_with_cluster):
79
+ """A bare identity.json gains realm/operator/fqid/pgp_fingerprint."""
80
+ with patch(_RESOLVER, side_effect=lambda a: _fake_ident(a)):
81
+ plan = migrate_identities(home_with_cluster, apply=True)
82
+
83
+ ap = plan.agents[0]
84
+ assert ap.agent == "lumina"
85
+ assert ap.applied is True
86
+ data = json.loads(ap.path.read_text())
87
+ assert data["realm"] == "skworld"
88
+ assert data["operator"] == "chef"
89
+ assert data["fqid"] == "lumina@chef.skworld"
90
+ assert data["pgp_fingerprint"] == "A" * 40
91
+ # Unrelated fields preserved (merge, not clobber).
92
+ assert data["name"] == "Lumina"
93
+ assert data["capauth_uri"] == "capauth:lumina@skworld.io"
94
+
95
+ def test_idempotent_already_complete(self, tmp_path):
96
+ """A complete identity is reported unchanged and not rewritten."""
97
+ home = tmp_path / ".skcapstone"
98
+ home.mkdir(parents=True)
99
+ (home / "cluster.json").write_text(json.dumps({
100
+ "realm": "skworld", "operator": "chef",
101
+ }))
102
+ payload = {
103
+ "name": "Opus", "capauth_managed": True,
104
+ "realm": "skworld", "operator": "chef",
105
+ "fqid": "opus@chef.skworld", "pgp_fingerprint": "B" * 40,
106
+ }
107
+ adir = _mk_agent(home, "opus", identity_payload=payload)
108
+ ident_path = adir / "identity" / "identity.json"
109
+ mtime_before = ident_path.stat().st_mtime
110
+
111
+ with patch(_RESOLVER, side_effect=lambda a: _fake_ident(
112
+ a, fqid="opus@chef.skworld", fingerprint="B" * 40
113
+ )):
114
+ plan = migrate_identities(home, apply=True)
115
+
116
+ ap = plan.agents[0]
117
+ assert ap.changed is False
118
+ assert plan.changed_count == 0
119
+ assert plan.unchanged_count == 1
120
+ assert ident_path.stat().st_mtime == mtime_before # not rewritten
121
+
122
+ def test_dry_run_writes_nothing(self, home_with_cluster):
123
+ """Default (apply=False) computes a plan but writes nothing."""
124
+ ident_path = home_with_cluster / "agents" / "lumina" / "identity" / "identity.json"
125
+ before = ident_path.read_text()
126
+
127
+ with patch(_RESOLVER, side_effect=lambda a: _fake_ident(a)):
128
+ plan = migrate_identities(home_with_cluster, apply=False)
129
+
130
+ assert plan.dry_run is True
131
+ ap = plan.agents[0]
132
+ assert ap.changed is True # plan SHOWS the additions
133
+ assert ap.applied is False # but did not write
134
+ assert ident_path.read_text() == before
135
+ # The would-be additions are still surfaced for the diff.
136
+ assert "fqid" in ap.additions
137
+
138
+ def test_templates_and_noncapauth_skipped(self, home_with_cluster):
139
+ """*-template and non-capauth agents are excluded from the walk."""
140
+ _mk_agent(home_with_cluster, "lumina-template", identity_payload={"name": "T"})
141
+ _mk_agent(home_with_cluster, "scaffold", capauth=False, identity_payload={"name": "S"})
142
+
143
+ with patch(_RESOLVER, side_effect=lambda a: _fake_ident(a)):
144
+ plan = migrate_identities(home_with_cluster, apply=True)
145
+
146
+ names = {a.agent for a in plan.agents}
147
+ assert names == {"lumina"}
148
+
149
+ def test_missing_cluster_graceful(self, tmp_path):
150
+ """No cluster.json: realm/operator are skipped, fqid/fingerprint still tried."""
151
+ home = tmp_path / ".skcapstone"
152
+ home.mkdir(parents=True)
153
+ _mk_agent(home, "lumina", identity_payload={"name": "Lumina"})
154
+
155
+ # Resolver returns no fqid (cluster-derived) but does have a fingerprint.
156
+ with patch(_RESOLVER, side_effect=lambda a: _fake_ident(
157
+ a, fqid=None, fingerprint="C" * 40
158
+ )):
159
+ plan = migrate_identities(home, apply=True)
160
+
161
+ assert plan.cluster_found is False
162
+ ap = plan.agents[0]
163
+ # realm/operator/fqid absent (no cluster), but pgp_fingerprint added.
164
+ assert "realm" not in ap.additions
165
+ assert "operator" not in ap.additions
166
+ assert "fqid" not in ap.additions
167
+ assert ap.additions.get("pgp_fingerprint") == "C" * 40
168
+
169
+ def test_no_provisioned_agents(self, tmp_path):
170
+ """Empty home yields an empty plan, not a crash."""
171
+ home = tmp_path / ".skcapstone"
172
+ home.mkdir(parents=True)
173
+ plan = migrate_identities(home, apply=True)
174
+ assert plan.agents == []
175
+ assert plan.changed_count == 0
176
+
177
+ def test_unreadable_identity_is_error_not_crash(self, home_with_cluster):
178
+ """A corrupt identity.json is reported as an error, not raised."""
179
+ ident_path = home_with_cluster / "agents" / "lumina" / "identity" / "identity.json"
180
+ ident_path.write_text("{ not json")
181
+
182
+ with patch(_RESOLVER, side_effect=lambda a: _fake_ident(a)):
183
+ plan = migrate_identities(home_with_cluster, apply=True)
184
+
185
+ ap = plan.agents[0]
186
+ assert ap.error
187
+ assert ap.applied is False
188
+
189
+
190
+ # ---------------------------------------------------------------------------
191
+ # CLI integration
192
+ # ---------------------------------------------------------------------------
193
+
194
+
195
+ @pytest.fixture
196
+ def cli():
197
+ """A minimal Click group with only the identity commands registered."""
198
+ import click
199
+
200
+ @click.group()
201
+ def root():
202
+ pass
203
+
204
+ register_identity_commands(root)
205
+ return root
206
+
207
+
208
+ class TestMigrateCLI:
209
+ """skcapstone identity migrate CLI."""
210
+
211
+ def test_default_is_dry_run(self, cli, home_with_cluster):
212
+ """Invoking without --apply writes nothing (dry-run is the default)."""
213
+ ident_path = home_with_cluster / "agents" / "lumina" / "identity" / "identity.json"
214
+ before = ident_path.read_text()
215
+
216
+ with patch(_RESOLVER, side_effect=lambda a: _fake_ident(a)):
217
+ result = CliRunner().invoke(
218
+ cli, ["identity", "migrate", "--home", str(home_with_cluster)]
219
+ )
220
+
221
+ assert result.exit_code == 0, result.output
222
+ assert "DRY-RUN" in result.output
223
+ assert ident_path.read_text() == before
224
+
225
+ def test_apply_writes(self, cli, home_with_cluster):
226
+ """--apply actually writes the backfilled fields."""
227
+ ident_path = home_with_cluster / "agents" / "lumina" / "identity" / "identity.json"
228
+
229
+ with patch(_RESOLVER, side_effect=lambda a: _fake_ident(a)):
230
+ result = CliRunner().invoke(
231
+ cli,
232
+ ["identity", "migrate", "--home", str(home_with_cluster), "--apply"],
233
+ )
234
+
235
+ assert result.exit_code == 0, result.output
236
+ data = json.loads(ident_path.read_text())
237
+ assert data["fqid"] == "lumina@chef.skworld"
238
+ assert data["pgp_fingerprint"] == "A" * 40
239
+
240
+ def test_json_out(self, cli, home_with_cluster):
241
+ """--json-out emits a machine-readable plan."""
242
+ with patch(_RESOLVER, side_effect=lambda a: _fake_ident(a)):
243
+ result = CliRunner().invoke(
244
+ cli,
245
+ ["identity", "migrate", "--home", str(home_with_cluster), "--json-out"],
246
+ )
247
+
248
+ assert result.exit_code == 0, result.output
249
+ payload = json.loads(result.output)
250
+ assert payload["dry_run"] is True
251
+ assert payload["agents"][0]["agent"] == "lumina"