@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
@@ -140,7 +140,8 @@ def register_daemon_commands(main: click.Group) -> None:
140
140
  effective_port = _resolve_agent_port(agent, port)
141
141
 
142
142
  if agent:
143
- # Propagate identity to child imports that read SKCAPSTONE_AGENT.
143
+ # Propagate identity to child imports that read SKAGENT.
144
+ os.environ["SKAGENT"] = agent
144
145
  os.environ["SKCAPSTONE_AGENT"] = agent
145
146
 
146
147
  if not home_path.exists():
@@ -270,60 +271,120 @@ def register_daemon_commands(main: click.Group) -> None:
270
271
  console.print(f" [yellow]API unreachable on port {effective_port}[/]\n")
271
272
 
272
273
  @daemon.command("install")
273
- def daemon_install():
274
- """Install the daemon as a systemd user service.
274
+ @click.option("--agent", "agent_name", default=None,
275
+ help="Agent name for SKCAPSTONE_AGENT (default: from env or 'sovereign').")
276
+ @click.option("--start", is_flag=True, help="Start services immediately after installing.")
277
+ def daemon_install(agent_name: str | None, start: bool):
278
+ """Install the daemon as a system service.
275
279
 
276
- Copies unit files to ~/.config/systemd/user/, enables at login,
277
- and starts immediately. No root required.
280
+ On Linux: installs systemd user service units.
281
+ On macOS: installs launchd plist files to ~/Library/LaunchAgents/.
282
+
283
+ The --agent flag sets the SKCAPSTONE_AGENT environment variable
284
+ in the service definition. If not provided, uses the
285
+ SKCAPSTONE_AGENT env var or defaults to 'sovereign'.
278
286
 
279
287
  Examples:
280
288
 
281
289
  skcapstone daemon install
290
+
291
+ skcapstone daemon install --agent myagent --start
282
292
  """
283
- from ..systemd import install_service, systemd_available
293
+ import platform
284
294
 
285
- if not systemd_available():
286
- console.print("[red]systemd user session not available.[/]")
287
- console.print("[dim]This command requires a Linux system with systemd.[/]")
288
- raise SystemExit(1)
295
+ effective_agent = agent_name or os.environ.get("SKCAPSTONE_AGENT", "sovereign")
289
296
 
290
- console.print("\n[cyan]Installing skcapstone systemd service...[/]")
291
- result = install_service()
297
+ if platform.system() == "Darwin":
298
+ from ..launchd import install_service as launchd_install
292
299
 
293
- if result["installed"]:
294
- console.print("[green] Unit files installed.[/]")
295
- if result["enabled"]:
296
- console.print("[green] Service enabled at login.[/]")
297
- if result["started"]:
298
- console.print("[green] Service started.[/]")
299
- console.print()
300
+ console.print(f"\n[cyan]Installing launchd services for agent '{effective_agent}'...[/]")
301
+ result = launchd_install(agent_name=effective_agent, start=start)
302
+
303
+ if result["installed"]:
304
+ for svc in result.get("services", []):
305
+ status = "[green]loaded[/]" if svc.get("loaded") else "[green]installed[/]"
306
+ console.print(f" [green]✓[/] {svc['label']} — {status}")
307
+ console.print()
308
+ console.print("[dim] Manage: launchctl list | grep skcapstone[/]")
309
+ if not start:
310
+ console.print("[dim] Start: launchctl start com.skcapstone.daemon[/]")
311
+ console.print("[dim] Or re-run with --start to load immediately.[/]")
312
+ else:
313
+ console.print("[red]Installation failed. Check logs.[/]")
314
+ raise SystemExit(1)
315
+ console.print()
316
+
317
+ elif platform.system() == "Linux":
318
+ from ..systemd import install_service, systemd_available, SERVICE_NAME
319
+
320
+ if not systemd_available():
321
+ console.print("[red]systemd user session not available.[/]")
322
+ console.print("[dim]This command requires a Linux system with systemd.[/]")
323
+ raise SystemExit(1)
324
+
325
+ console.print(f"\n[cyan]Installing skcapstone systemd service for agent '{effective_agent}'...[/]")
326
+ result = install_service(agent_name=effective_agent, start=start)
327
+ svc_name = result.get("service_name", SERVICE_NAME)
328
+
329
+ if result["installed"]:
330
+ console.print(f"[green] Unit files installed ({svc_name}).[/]")
331
+ if result["enabled"]:
332
+ console.print(f"[green] Service enabled at login.[/]")
333
+ if result.get("started"):
334
+ console.print(f"[green] Service started.[/]")
335
+ else:
336
+ console.print(f"[dim] Start: systemctl --user start {svc_name}[/]")
337
+ console.print()
300
338
 
301
- if not result["installed"]:
302
- console.print("[red]Installation failed. Check logs.[/]")
339
+ if not result["installed"]:
340
+ console.print("[red]Installation failed. Check logs.[/]")
341
+ raise SystemExit(1)
342
+ else:
343
+ console.print(f"[red]Auto-start not supported on {platform.system()}.[/]")
303
344
  raise SystemExit(1)
304
345
 
305
346
  @daemon.command("uninstall")
306
347
  def daemon_uninstall():
307
- """Uninstall the systemd user service.
348
+ """Uninstall the system service.
308
349
 
309
- Stops, disables, and removes the unit files.
350
+ On Linux: stops, disables, and removes systemd unit files.
351
+ On macOS: unloads and removes launchd plist files.
310
352
 
311
353
  Examples:
312
354
 
313
355
  skcapstone daemon uninstall
314
356
  """
315
- from ..systemd import uninstall_service
357
+ import platform
316
358
 
317
- console.print("\n[cyan]Uninstalling skcapstone systemd service...[/]")
318
- result = uninstall_service()
359
+ if platform.system() == "Darwin":
360
+ from ..launchd import uninstall_service as launchd_uninstall
319
361
 
320
- if result["stopped"]:
321
- console.print("[green] Service stopped.[/]")
322
- if result["disabled"]:
323
- console.print("[green] Service disabled.[/]")
324
- if result["removed"]:
325
- console.print("[green] Unit files removed.[/]")
326
- console.print()
362
+ console.print("\n[cyan]Uninstalling skcapstone launchd services...[/]")
363
+ result = launchd_uninstall()
364
+
365
+ if result["stopped"]:
366
+ console.print("[green] Services unloaded.[/]")
367
+ if result["removed"]:
368
+ for label in result.get("services", []):
369
+ console.print(f" [green]✓[/] Removed {label}")
370
+ console.print()
371
+
372
+ elif platform.system() == "Linux":
373
+ from ..systemd import uninstall_service
374
+
375
+ console.print("\n[cyan]Uninstalling skcapstone systemd service...[/]")
376
+ result = uninstall_service()
377
+
378
+ if result["stopped"]:
379
+ console.print("[green] Service stopped.[/]")
380
+ if result["disabled"]:
381
+ console.print("[green] Service disabled.[/]")
382
+ if result["removed"]:
383
+ console.print("[green] Unit files removed.[/]")
384
+ console.print()
385
+
386
+ else:
387
+ console.print(f"[red]Not supported on {platform.system()}.[/]")
327
388
 
328
389
  @daemon.command("components")
329
390
  @click.option("--agent", default=None, help="Named agent to query.")
@@ -413,7 +474,10 @@ def register_daemon_commands(main: click.Group) -> None:
413
474
  @click.option("--lines", "-n", default=50, help="Number of lines (default: 50).")
414
475
  @click.option("--follow", "-f", is_flag=True, help="Show the command to follow logs live.")
415
476
  def daemon_logs(lines: int, follow: bool):
416
- """Show daemon logs from journald.
477
+ """Show daemon logs.
478
+
479
+ On Linux: reads from journald.
480
+ On macOS: reads from ~/.skcapstone/logs/ files.
417
481
 
418
482
  Examples:
419
483
 
@@ -423,14 +487,29 @@ def register_daemon_commands(main: click.Group) -> None:
423
487
 
424
488
  skcapstone daemon logs -f
425
489
  """
426
- from ..systemd import service_logs
490
+ import platform
427
491
 
428
- if follow:
429
- cmd = service_logs(follow=True)
430
- console.print(f"\n Run: [bold cyan]{cmd}[/]\n")
492
+ if platform.system() == "Darwin":
493
+ if follow:
494
+ log_path = Path.home() / ".skcapstone" / "logs" / "daemon.stdout.log"
495
+ console.print(f"\n Run: [bold cyan]tail -f {log_path}[/]\n")
496
+ else:
497
+ from ..launchd import service_logs
498
+ output = service_logs(lines=lines)
499
+ if output.strip():
500
+ click.echo(output)
501
+ else:
502
+ console.print("[dim]No logs found in ~/.skcapstone/logs/[/]")
503
+ console.print("[dim]Is the service installed? Run: skcapstone daemon install[/]")
431
504
  else:
432
- output = service_logs(lines=lines)
433
- if output.strip():
434
- click.echo(output)
505
+ from ..systemd import service_logs
506
+
507
+ if follow:
508
+ cmd = service_logs(follow=True)
509
+ console.print(f"\n Run: [bold cyan]{cmd}[/]\n")
435
510
  else:
436
- console.print("[dim]No logs found. Is the service installed?[/]")
511
+ output = service_logs(lines=lines)
512
+ if output.strip():
513
+ click.echo(output)
514
+ else:
515
+ console.print("[dim]No logs found. Is the service installed?[/]")
@@ -108,10 +108,35 @@ def register_gtd_commands(main: click.Group) -> None:
108
108
  console.print()
109
109
 
110
110
  @gtd.command("status")
111
- def gtd_status():
111
+ @click.option("--brief", is_flag=True,
112
+ help="One-line summary (for hooks / session start).")
113
+ def gtd_status(brief: bool):
112
114
  """Summary of all GTD lists."""
113
115
  from ..mcp_tools.gtd_tools import _load_list, _GTD_LISTS
114
116
 
117
+ if brief:
118
+ from datetime import datetime, timezone
119
+
120
+ counts = {name: len(_load_list(name)) for name in _GTD_LISTS}
121
+ now = datetime.now(timezone.utc)
122
+ stale = 0
123
+ for p in _load_list("projects"):
124
+ ts = p.get("moved_at") or p.get("created_at")
125
+ try:
126
+ if ts and (now - datetime.fromisoformat(ts)).days >= 7:
127
+ stale += 1
128
+ except (ValueError, TypeError):
129
+ pass
130
+ stale_str = f" ({stale} stale)" if stale else ""
131
+ click.echo(
132
+ f"GTD: {counts.get('inbox', 0)} inbox · "
133
+ f"{counts.get('next-actions', 0)} next · "
134
+ f"{counts.get('projects', 0)} projects{stale_str} · "
135
+ f"{counts.get('waiting-for', 0)} waiting · "
136
+ f"{counts.get('someday-maybe', 0)} someday"
137
+ )
138
+ return
139
+
115
140
  console.print()
116
141
  total = 0
117
142
  rows = []
@@ -14,9 +14,9 @@ def register_housekeeping_commands(main: click.Group) -> None:
14
14
 
15
15
  @main.command("housekeeping")
16
16
  @click.option("--home", default=AGENT_HOME, type=click.Path(), help="Agent home directory.")
17
- @click.option("--skcomm-home", default="~/.skcomm", type=click.Path(), help="SKComm home directory.")
17
+ @click.option("--skcomms-home", default="~/.skcomms", type=click.Path(), help="SKComms home directory.")
18
18
  @click.option("--dry-run", is_flag=True, help="Report what would be deleted without deleting.")
19
- def housekeeping(home: str, skcomm_home: str, dry_run: bool):
19
+ def housekeeping(home: str, skcomms_home: str, dry_run: bool):
20
20
  """Prune stale ACKs, delivered envelopes, and old seeds.
21
21
 
22
22
  Reclaims disk space from files that accumulate in the agent
@@ -33,7 +33,7 @@ def register_housekeeping_commands(main: click.Group) -> None:
33
33
 
34
34
  results = run_housekeeping(
35
35
  skcapstone_home=Path(home).expanduser(),
36
- skcomm_home=Path(skcomm_home).expanduser(),
36
+ skcomms_home=Path(skcomms_home).expanduser(),
37
37
  dry_run=dry_run,
38
38
  )
39
39
 
@@ -0,0 +1,378 @@
1
+ """Identity commands: migrate.
2
+
3
+ The ``skcapstone identity migrate`` command backfills every provisioned
4
+ agent's ``identity/identity.json`` with the explicit sovereign-identity
5
+ fields the unified layer expects (skcomms T2 / epic ``2b264064``):
6
+
7
+ * ``realm`` + ``operator`` — mirrored from ``cluster.json``.
8
+ * ``fqid`` — the three-tier ``<agent>@<operator>.<realm>`` label, sourced
9
+ from :func:`capauth.resolve_agent_identity` (the canonical resolver).
10
+ * ``pgp_fingerprint`` — the agent's 40-char PGP fingerprint, also from the
11
+ resolver / the agent's CapAuth profile.
12
+
13
+ This command does **not** reimplement identity logic — it delegates to
14
+ ``capauth.resolve_agent_identity`` for the per-agent identity and only mirrors
15
+ ``realm``/``operator`` from cluster.json directly (those are cluster facts, not
16
+ agent facts). It is a *walker*: it finds every provisioned agent (one with a
17
+ CapAuth home, never a ``*-template``) and merges the missing fields into its
18
+ identity.json without clobbering unrelated keys.
19
+
20
+ Safety: these are LIVE identity files, so the command defaults to a dry-run
21
+ (it prints a plan and writes nothing). Pass ``--apply`` (alias ``--write``) to
22
+ actually modify files. The operation is idempotent — a second run on an
23
+ already-complete home reports every agent as unchanged.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import json
29
+ from dataclasses import dataclass, field
30
+ from pathlib import Path
31
+ from typing import Optional
32
+
33
+ import click
34
+
35
+ from ._common import SHARED_ROOT, console
36
+
37
+ # Fields the walker backfills, in stable display order.
38
+ _MANAGED_FIELDS = ("realm", "operator", "fqid", "pgp_fingerprint")
39
+
40
+ # cluster.json search path (mirrors capauth.agent_identity._CLUSTER_LOOKUP so
41
+ # realm/operator come from the same source the resolver uses for the fqid).
42
+ _CLUSTER_LOOKUP = [
43
+ Path("/etc/skcapstone/cluster.json"),
44
+ ]
45
+
46
+
47
+ @dataclass
48
+ class AgentPlan:
49
+ """Planned identity.json changes for a single agent.
50
+
51
+ Attributes:
52
+ agent: Short agent name.
53
+ path: Path to the agent's identity/identity.json.
54
+ additions: Field → value mapping that would be written. Empty when
55
+ the agent is already complete (nothing to add).
56
+ applied: Whether the additions were actually written to disk.
57
+ error: Non-empty when the agent could not be processed (e.g. an
58
+ unreadable identity.json); such agents are skipped, not crashed.
59
+ """
60
+
61
+ agent: str
62
+ path: Path
63
+ additions: dict[str, str] = field(default_factory=dict)
64
+ applied: bool = False
65
+ error: str = ""
66
+
67
+ @property
68
+ def changed(self) -> bool:
69
+ """True when this agent has at least one field to add."""
70
+ return bool(self.additions)
71
+
72
+
73
+ @dataclass
74
+ class MigrationPlan:
75
+ """Aggregate plan across every walked agent.
76
+
77
+ Attributes:
78
+ home: Shared root that was walked (``~/.skcapstone``).
79
+ dry_run: True when nothing was written to disk.
80
+ cluster_found: Whether a cluster.json was located (realm/operator are
81
+ unavailable when False).
82
+ agents: Per-agent plans (one per provisioned, non-template agent).
83
+ """
84
+
85
+ home: Path
86
+ dry_run: bool
87
+ cluster_found: bool
88
+ agents: list[AgentPlan] = field(default_factory=list)
89
+
90
+ @property
91
+ def changed_count(self) -> int:
92
+ """Number of agents with at least one field to add."""
93
+ return sum(1 for a in self.agents if a.changed)
94
+
95
+ @property
96
+ def unchanged_count(self) -> int:
97
+ """Number of already-complete agents (no additions, no error)."""
98
+ return sum(1 for a in self.agents if not a.changed and not a.error)
99
+
100
+ def to_dict(self) -> dict:
101
+ """Serialise to a JSON-friendly dict."""
102
+ return {
103
+ "home": str(self.home),
104
+ "dry_run": self.dry_run,
105
+ "cluster_found": self.cluster_found,
106
+ "changed": self.changed_count,
107
+ "unchanged": self.unchanged_count,
108
+ "agents": [
109
+ {
110
+ "agent": a.agent,
111
+ "path": str(a.path),
112
+ "additions": a.additions,
113
+ "applied": a.applied,
114
+ "error": a.error,
115
+ }
116
+ for a in self.agents
117
+ ],
118
+ }
119
+
120
+
121
+ def _load_cluster(home: Path) -> Optional[dict]:
122
+ """Load cluster.json from ``/etc/skcapstone`` then the agent home.
123
+
124
+ Mirrors :data:`capauth.agent_identity._CLUSTER_LOOKUP` but resolves the
125
+ home-local copy relative to *home* so a test (or alternate root) reads the
126
+ fixture cluster.json rather than the real ``~/.skcapstone`` one.
127
+
128
+ Args:
129
+ home: Shared root directory being walked.
130
+
131
+ Returns:
132
+ The parsed cluster dict, or ``None`` when no cluster.json exists or it
133
+ cannot be parsed.
134
+ """
135
+ for path in [*_CLUSTER_LOOKUP, home / "cluster.json"]:
136
+ if path.exists():
137
+ try:
138
+ return json.loads(path.read_text(encoding="utf-8"))
139
+ except (json.JSONDecodeError, OSError):
140
+ continue
141
+ return None
142
+
143
+
144
+ def _provisioned_agents(home: Path) -> list[str]:
145
+ """List agents with a CapAuth home (and thus a real identity).
146
+
147
+ Reuses the "provisioned agent" notion from
148
+ :func:`skcapstone.doctor._provisioned_agents`: an agent counts only when
149
+ ``agents/<name>/capauth/`` exists, and ``*-template`` scaffolds are
150
+ excluded.
151
+
152
+ Args:
153
+ home: Shared root directory (``~/.skcapstone``).
154
+
155
+ Returns:
156
+ Sorted provisioned agent names.
157
+ """
158
+ from ..doctor import _provisioned_agents as _doctor_provisioned
159
+
160
+ return _doctor_provisioned(home)
161
+
162
+
163
+ def _plan_agent(home: Path, agent: str, cluster: Optional[dict]) -> AgentPlan:
164
+ """Compute the identity.json additions for one agent.
165
+
166
+ Reads the agent's current ``identity/identity.json`` and determines which
167
+ of ``realm``/``operator``/``fqid``/``pgp_fingerprint`` are missing, using
168
+ cluster.json (realm/operator) and ``capauth.resolve_agent_identity`` (fqid
169
+ + fingerprint) as the source of truth. Existing values are never
170
+ overwritten.
171
+
172
+ Args:
173
+ home: Shared root directory.
174
+ agent: Short agent name.
175
+ cluster: Parsed cluster.json dict, or ``None``.
176
+
177
+ Returns:
178
+ An :class:`AgentPlan` describing the additions (empty when complete or
179
+ when no source value is available), or carrying an ``error`` when the
180
+ identity.json is unreadable.
181
+ """
182
+ path = home / "agents" / agent / "identity" / "identity.json"
183
+ plan = AgentPlan(agent=agent, path=path)
184
+
185
+ existing: dict = {}
186
+ if path.exists():
187
+ try:
188
+ loaded = json.loads(path.read_text(encoding="utf-8"))
189
+ if isinstance(loaded, dict):
190
+ existing = loaded
191
+ except (json.JSONDecodeError, OSError) as exc:
192
+ plan.error = f"unreadable identity.json: {exc}"
193
+ return plan
194
+
195
+ # realm / operator come straight from cluster.json (cluster facts).
196
+ desired: dict[str, Optional[str]] = {}
197
+ if cluster is not None:
198
+ desired["realm"] = cluster.get("realm")
199
+ desired["operator"] = cluster.get("operator")
200
+
201
+ # fqid + pgp_fingerprint come from the canonical resolver — never
202
+ # reimplemented here (epic 2b264064; capauth is the source of truth).
203
+ try:
204
+ from capauth import resolve_agent_identity
205
+
206
+ ident = resolve_agent_identity(agent)
207
+ desired["fqid"] = getattr(ident, "fqid", None)
208
+ desired["pgp_fingerprint"] = getattr(ident, "fingerprint", None)
209
+ except Exception: # noqa: BLE001 — resolver failure must not crash the walk
210
+ pass
211
+
212
+ for key in _MANAGED_FIELDS:
213
+ value = desired.get(key)
214
+ # Only add when we have a real value AND it is not already present.
215
+ if value and not existing.get(key):
216
+ plan.additions[key] = str(value)
217
+
218
+ return plan
219
+
220
+
221
+ def migrate_identities(home: Path, *, apply: bool = False) -> MigrationPlan:
222
+ """Walk provisioned agents and backfill their identity.json.
223
+
224
+ For every provisioned agent (one with a CapAuth home, never a
225
+ ``*-template``), ensure its ``identity/identity.json`` carries ``realm``,
226
+ ``operator``, ``fqid`` and ``pgp_fingerprint``. Missing fields are merged
227
+ in without clobbering unrelated keys; files are only written when something
228
+ actually changed.
229
+
230
+ Args:
231
+ home: Shared root directory (``~/.skcapstone``).
232
+ apply: When ``True``, write the changes to disk. When ``False`` (the
233
+ default — these are live files), nothing is written and the
234
+ returned plan is a preview only.
235
+
236
+ Returns:
237
+ A :class:`MigrationPlan` describing per-agent additions and whether
238
+ each was applied.
239
+
240
+ Examples:
241
+ >>> plan = migrate_identities(Path("~/.skcapstone").expanduser())
242
+ >>> plan.dry_run
243
+ True
244
+ """
245
+ cluster = _load_cluster(home)
246
+ plan = MigrationPlan(
247
+ home=home,
248
+ dry_run=not apply,
249
+ cluster_found=cluster is not None,
250
+ )
251
+
252
+ for agent in _provisioned_agents(home):
253
+ agent_plan = _plan_agent(home, agent, cluster)
254
+ if apply and agent_plan.changed and not agent_plan.error:
255
+ try:
256
+ _apply_additions(agent_plan)
257
+ agent_plan.applied = True
258
+ except OSError as exc:
259
+ agent_plan.error = f"write failed: {exc}"
260
+ plan.agents.append(agent_plan)
261
+
262
+ return plan
263
+
264
+
265
+ def _apply_additions(plan: AgentPlan) -> None:
266
+ """Merge a plan's additions into its identity.json on disk.
267
+
268
+ Reads the current file (or starts from ``{}`` if absent), updates only the
269
+ planned keys, and writes the result back with stable indentation. Unrelated
270
+ keys are preserved.
271
+
272
+ Args:
273
+ plan: The agent plan whose ``additions`` should be persisted.
274
+
275
+ Raises:
276
+ OSError: If the file cannot be read or written.
277
+ """
278
+ data: dict = {}
279
+ if plan.path.exists():
280
+ try:
281
+ loaded = json.loads(plan.path.read_text(encoding="utf-8"))
282
+ if isinstance(loaded, dict):
283
+ data = loaded
284
+ except json.JSONDecodeError:
285
+ data = {}
286
+ data.update(plan.additions)
287
+ plan.path.parent.mkdir(parents=True, exist_ok=True)
288
+ plan.path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
289
+
290
+
291
+ def register_identity_commands(main: click.Group) -> None:
292
+ """Register the ``identity`` command group on the main CLI."""
293
+
294
+ @main.group()
295
+ def identity():
296
+ """Identity management — migrate per-agent identity.json files."""
297
+
298
+ @identity.command("migrate")
299
+ @click.option(
300
+ "--home", default=SHARED_ROOT, type=click.Path(),
301
+ help="Shared root directory (~/.skcapstone).",
302
+ )
303
+ @click.option(
304
+ "--apply", "--write", "apply_", is_flag=True,
305
+ help="Actually write changes. Default is a dry-run (writes nothing).",
306
+ )
307
+ @click.option(
308
+ "--dry-run", is_flag=True,
309
+ help="Explicitly preview only (the default). Overrides --apply if both given.",
310
+ )
311
+ @click.option("--json-out", is_flag=True, help="Output as machine-readable JSON.")
312
+ def migrate(home: str, apply_: bool, dry_run: bool, json_out: bool) -> None:
313
+ """Backfill realm/operator/fqid/pgp_fingerprint into agent identity.json.
314
+
315
+ Walks every provisioned agent (one with a CapAuth home, excluding
316
+ ``*-template`` dirs) under ``~/.skcapstone/agents/`` and ensures each
317
+ agent's ``identity/identity.json`` carries the explicit sovereign
318
+ fields. Delegates to ``capauth.resolve_agent_identity`` for the fqid
319
+ and fingerprint; realm/operator are mirrored from cluster.json.
320
+
321
+ SAFETY: defaults to a dry-run (prints a plan, writes nothing). Pass
322
+ ``--apply`` (or ``--write``) to actually modify the live identity
323
+ files. Idempotent — re-running on a complete home changes nothing.
324
+ """
325
+ home_path = Path(home).expanduser()
326
+ do_apply = apply_ and not dry_run
327
+ plan = migrate_identities(home_path, apply=do_apply)
328
+
329
+ if json_out:
330
+ click.echo(json.dumps(plan.to_dict(), indent=2))
331
+ return
332
+
333
+ _render_plan(plan)
334
+
335
+ return None
336
+
337
+
338
+ def _render_plan(plan: MigrationPlan) -> None:
339
+ """Render a migration plan as human-readable Rich output."""
340
+ mode = "[yellow]DRY-RUN[/] (no files written — pass --apply to write)" \
341
+ if plan.dry_run else "[green]APPLY[/] (files written)"
342
+ console.print()
343
+ console.print(f" [bold]identity migrate[/] {mode}")
344
+ console.print(f" [dim]{plan.home}[/]")
345
+ if not plan.cluster_found:
346
+ console.print(
347
+ " [yellow]~ cluster.json not found — realm/operator unavailable, "
348
+ "fqid may be incomplete[/]"
349
+ )
350
+ console.print()
351
+
352
+ if not plan.agents:
353
+ console.print(" [dim]No provisioned agents found (none with a CapAuth home).[/]")
354
+ console.print()
355
+ return
356
+
357
+ for a in plan.agents:
358
+ if a.error:
359
+ console.print(f" [red]✗ {a.agent}[/] {a.error}")
360
+ elif not a.changed:
361
+ console.print(f" [green]✓ {a.agent}[/] [dim]unchanged (already complete)[/]")
362
+ else:
363
+ verb = "added" if a.applied else "would add"
364
+ fields = ", ".join(f"{k}={v}" for k, v in a.additions.items())
365
+ color = "green" if a.applied else "cyan"
366
+ console.print(f" [{color}]→ {a.agent}[/] {verb}: {fields}")
367
+ console.print(f" [dim]{a.path}[/]")
368
+
369
+ console.print()
370
+ summary = (
371
+ f" {plan.changed_count} to change, {plan.unchanged_count} unchanged"
372
+ if plan.dry_run
373
+ else f" {plan.changed_count} changed, {plan.unchanged_count} unchanged"
374
+ )
375
+ console.print(f"[bold]{summary}[/]")
376
+ if plan.dry_run and plan.changed_count:
377
+ console.print(" [dim]Re-run with --apply to write these changes.[/]")
378
+ console.print()
@@ -439,7 +439,7 @@ def register_joule_commands(main: click.Group) -> None:
439
439
  @joule_group.command("dashboard")
440
440
  @click.option(
441
441
  "--agent", "-a", "agent_name", default=None,
442
- help="Agent name (default: lumina).",
442
+ help="Agent name (default: current agent).",
443
443
  )
444
444
  def dashboard_cmd(agent_name: str | None):
445
445
  """Show a financial dashboard for an agent."""
@@ -451,7 +451,9 @@ def register_joule_commands(main: click.Group) -> None:
451
451
 
452
452
  from ..skjoule import JouleEngine, TransactionKind
453
453
 
454
- agent_name = agent_name or "lumina"
454
+ from .. import active_agent_name
455
+
456
+ agent_name = agent_name or active_agent_name()
455
457
  engine = JouleEngine(home=Path(SHARED_ROOT).expanduser())
456
458
  wallet = engine.get_wallet(agent_name)
457
459
  balance = wallet.balance
@@ -624,4 +626,6 @@ def _resolve_agent(agent_name: str | None) -> str:
624
626
  if agent_name:
625
627
  return agent_name
626
628
  from .. import SKCAPSTONE_AGENT
627
- return SKCAPSTONE_AGENT or "lumina"
629
+ from .. import active_agent_name
630
+
631
+ return SKCAPSTONE_AGENT or active_agent_name() or ""