@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
@@ -423,7 +423,7 @@ def register_memory_commands(main: click.Group) -> None:
423
423
  @memory.command("rehydrate")
424
424
  @click.option("--home", default=AGENT_HOME, type=click.Path())
425
425
  @click.option("--agent", "-a", default=None,
426
- help="Agent name (default: SKCAPSTONE_AGENT or 'lumina').")
426
+ help="Agent name (default: active agent).")
427
427
  @click.option("--febs-only", is_flag=True, help="Only ingest FEB files (trust rehydration).")
428
428
  @click.option("--memories-only", is_flag=True, help="Only ingest flat-file memories into backends.")
429
429
  @click.option("--force", is_flag=True, help="Re-ingest even if already in backend.")
@@ -439,7 +439,9 @@ def register_memory_commands(main: click.Group) -> None:
439
439
  import os
440
440
  from ..models import MemoryLayer
441
441
 
442
- agent_name = agent or os.environ.get("SKCAPSTONE_AGENT", "lumina")
442
+ from .. import active_agent_name
443
+
444
+ agent_name = agent or os.environ.get("SKCAPSTONE_AGENT") or active_agent_name()
443
445
  home_path = Path(home).expanduser()
444
446
  agent_home = home_path / "agents" / agent_name
445
447
 
@@ -463,8 +465,8 @@ def register_memory_commands(main: click.Group) -> None:
463
465
  try:
464
466
  from ..memory_adapter import get_unified, entry_to_memory
465
467
  unified = get_unified()
466
- except Exception:
467
- pass
468
+ except Exception as exc:
469
+ logger.warning("Memory adapter unavailable, falling back to file-only mode: %s", exc)
468
470
 
469
471
  for layer in MemoryLayer:
470
472
  layer_dir = mem_dir / layer.value
@@ -481,8 +483,8 @@ def register_memory_commands(main: click.Group) -> None:
481
483
  if existing:
482
484
  skipped += 1
483
485
  continue
484
- except Exception:
485
- pass
486
+ except Exception as exc:
487
+ logger.debug("Failed to check existing memory %s: %s", mem_id, exc)
486
488
 
487
489
  if unified:
488
490
  from ..models import MemoryEntry
@@ -19,7 +19,7 @@ def register_peers_dir_commands(main: click.Group) -> None:
19
19
  def peers_dir():
20
20
  """Peer transport directory — routing addresses for the mesh.
21
21
 
22
- SKComm transport endpoints. For identity/trust peers, see 'peer'."""
22
+ SKComms transport endpoints. For identity/trust peers, see 'peer'."""
23
23
 
24
24
  @peers_dir.command("list")
25
25
  @click.option("--home", "sk_home", default=AGENT_HOME, type=click.Path())
@@ -1,7 +1,7 @@
1
1
  """Register command — auto-register SK* skills and MCP servers.
2
2
 
3
3
  Detects the user's environments (OpenClaw, Claude Code, Cursor, VS Code,
4
- OpenCode CLI, mcporter) and registers SKILL.md symlinks + MCP server entries.
4
+ OpenCode CLI, Codex, mcporter) and registers SKILL.md symlinks + MCP server entries.
5
5
 
6
6
  Commands:
7
7
  skcapstone register — register all SK* packages
@@ -49,7 +49,7 @@ def register_register_commands(main: click.Group) -> None:
49
49
  """Register all SK* skills and MCP servers in detected environments.
50
50
 
51
51
  Auto-detects your development environments (Claude Code, Cursor,
52
- VS Code, OpenClaw, OpenCode, mcporter) and ensures all SK* skill
52
+ VS Code, OpenClaw, OpenCode, Codex, mcporter) and ensures all SK* skill
53
53
  manifests and MCP server entries are properly configured.
54
54
 
55
55
  Examples:
@@ -91,12 +91,23 @@ def register_register_commands(main: click.Group) -> None:
91
91
  dry_run=dry_run,
92
92
  )
93
93
 
94
+ # Register Claude Code hooks
95
+ if not dry_run:
96
+ try:
97
+ from skmemory.register import register_hooks
98
+ register_hooks(install_hooks=True)
99
+ except ImportError:
100
+ pass
101
+ except Exception:
102
+ pass
103
+
94
104
  # Display results
95
105
  from rich.table import Table
96
106
 
97
107
  table = Table(show_header=True, header_style="bold", box=None, padding=(0, 2))
98
108
  table.add_column("Package", style="cyan")
99
109
  table.add_column("Skill", style="dim")
110
+ table.add_column("Codex")
100
111
  table.add_column("MCP")
101
112
  table.add_column("OpenClaw Plugin")
102
113
 
@@ -133,6 +144,21 @@ def register_register_commands(main: click.Group) -> None:
133
144
  else:
134
145
  mcp_str = str(mcp_info)
135
146
 
147
+ codex_info = pkg_result.get("codex_skill", {})
148
+ codex_action = codex_info.get("action", "")
149
+ if codex_action == "created":
150
+ codex_str = "[green]created[/]"
151
+ elif codex_action == "exists":
152
+ codex_str = "[dim]exists[/]"
153
+ elif codex_action == "dry-run":
154
+ codex_str = "[yellow]would create[/]"
155
+ elif codex_action == "error":
156
+ codex_str = f"[red]{codex_info.get('error', 'error')}[/]"
157
+ elif not codex_action:
158
+ codex_str = "[dim]—[/]"
159
+ else:
160
+ codex_str = f"[dim]{codex_action}[/]"
161
+
136
162
  plugin_action = pkg_result.get("openclaw_plugin", "")
137
163
  if plugin_action == "created":
138
164
  plugin_str = "[green]created[/]"
@@ -147,7 +173,7 @@ def register_register_commands(main: click.Group) -> None:
147
173
  else:
148
174
  plugin_str = f"[dim]{plugin_action}[/]"
149
175
 
150
- table.add_row(name, skill_str, mcp_str, plugin_str)
176
+ table.add_row(name, skill_str, codex_str, mcp_str, plugin_str)
151
177
 
152
178
  console.print(table)
153
179
  console.print()
@@ -0,0 +1,167 @@
1
+ """`skcapstone scheduler` — manage the unified job scheduler."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import socket
7
+ from pathlib import Path
8
+
9
+ import click
10
+
11
+ from .. import AGENT_HOME
12
+ from ..scheduler_jobs import (
13
+ load_jobs,
14
+ load_jobs_with_dropins,
15
+ current_host_aliases,
16
+ job_runs_here,
17
+ )
18
+ from ..scheduler_runner import JobRunner
19
+ from ..scheduler_state import SchedulerState
20
+
21
+
22
+ def _home() -> Path:
23
+ """Return the effective SKCAPSTONE_HOME path.
24
+
25
+ Reads from the ``SKCAPSTONE_HOME`` environment variable when set,
26
+ falling back to the package-level ``AGENT_HOME`` constant.
27
+
28
+ Returns:
29
+ Resolved :class:`~pathlib.Path` for the agent home directory.
30
+ """
31
+ return Path(os.environ.get("SKCAPSTONE_HOME", AGENT_HOME))
32
+
33
+
34
+ def _jobs_path() -> Path:
35
+ """Return the path to the synced ``jobs.yaml`` registry.
36
+
37
+ Returns:
38
+ ``<SKCAPSTONE_HOME>/config/jobs.yaml`` as a :class:`~pathlib.Path`.
39
+ """
40
+ return _home() / "config" / "jobs.yaml"
41
+
42
+
43
+ def register_scheduler_commands(main: click.Group) -> None:
44
+ """Register the ``scheduler`` command group onto *main*.
45
+
46
+ Adds the following sub-commands:
47
+
48
+ - ``scheduler list`` — list all configured jobs with run status.
49
+ - ``scheduler status`` — show last-run state for this node.
50
+ - ``scheduler run`` — execute a job immediately.
51
+ - ``scheduler logs`` — tail the most recent log for a job.
52
+ - ``scheduler enable`` — enable a job in ``jobs.yaml``.
53
+ - ``scheduler disable`` — disable a job in ``jobs.yaml``.
54
+
55
+ Args:
56
+ main: The top-level :class:`click.Group` to attach commands to.
57
+ """
58
+
59
+ @main.group("scheduler")
60
+ def scheduler() -> None:
61
+ """Manage the unified job scheduler (skscheduler)."""
62
+
63
+ @scheduler.command("list")
64
+ def list_jobs() -> None:
65
+ """List all configured jobs and where they run."""
66
+ jobs = load_jobs_with_dropins(_jobs_path())
67
+ if not jobs:
68
+ click.echo("No jobs configured.")
69
+ return
70
+ here = current_host_aliases()
71
+ for j in jobs:
72
+ sched = j.schedule or (f"every {int(j.every_seconds)}s" if j.every_seconds else "-")
73
+ mark = "x" if (j.enabled and job_runs_here(j, here)) else " "
74
+ click.echo(f"[{mark}] {j.name:24s} {j.type:6s} {sched:18s} nodes={j.nodes}")
75
+
76
+ @scheduler.command("status")
77
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON.")
78
+ def status(as_json: bool) -> None:
79
+ """Show last-run status for this node."""
80
+ st = SchedulerState(root=_home(), hostname=socket.gethostname())
81
+ data = st.all()
82
+ if as_json:
83
+ click.echo(json.dumps(data, indent=2))
84
+ return
85
+ if not data:
86
+ click.echo("No run history on this node yet.")
87
+ return
88
+ for name, rec in data.items():
89
+ click.echo(
90
+ f"{name:24s} last={rec.get('last_run')} status={rec.get('last_status')} "
91
+ f"runs={rec.get('run_count')} errors={rec.get('error_count')}"
92
+ )
93
+
94
+ @scheduler.command("run")
95
+ @click.argument("job_name")
96
+ def run_now(job_name: str) -> None:
97
+ """Run a job now on this node (manual override; ignores schedule and affinity)."""
98
+ jobs = {j.name: j for j in load_jobs_with_dropins(_jobs_path())}
99
+ job = jobs.get(job_name)
100
+ if not job:
101
+ raise click.ClickException(f"Unknown job: {job_name}")
102
+ runner = JobRunner(log_dir=_home() / "scheduler" / socket.gethostname() / "logs")
103
+ result = runner.run(job)
104
+ # Record state + fire the job's notify policy so a manual run is observable in
105
+ # `scheduler status` and exercises the sk-alert hook (same as the scheduled path).
106
+ from datetime import datetime, timezone
107
+ from ..scheduled_tasks import TaskScheduler
108
+ SchedulerState(_home(), socket.gethostname()).record_run(
109
+ job.name, now=datetime.now(timezone.utc), ok=result.ok, error=result.error)
110
+ TaskScheduler._maybe_notify(job, result, attempts=1)
111
+ if result.output:
112
+ click.echo(result.output.strip())
113
+ if not result.ok:
114
+ raise click.ClickException(f"Job failed: {result.error}")
115
+ click.echo(f"OK {job_name} done")
116
+
117
+ @scheduler.command("logs")
118
+ @click.argument("job_name")
119
+ @click.option("--tail", default=40, show_default=True, help="Number of lines to show.")
120
+ def logs(job_name: str, tail: int) -> None:
121
+ """Show the latest log for a job on this node."""
122
+ log_dir = _home() / "scheduler" / socket.gethostname() / "logs"
123
+ matches = sorted(log_dir.glob(f"{job_name}-*.log")) if log_dir.exists() else []
124
+ if not matches:
125
+ click.echo(f"No logs for '{job_name}'.")
126
+ return
127
+ lines = matches[-1].read_text(encoding="utf-8").splitlines()
128
+ click.echo("\n".join(lines[-tail:]))
129
+
130
+ @scheduler.command("enable")
131
+ @click.argument("job_name")
132
+ def enable(job_name: str) -> None:
133
+ """Enable a job (sets enabled: true in jobs.yaml)."""
134
+ _set_enabled(job_name, True)
135
+ click.echo(f"enabled {job_name}")
136
+
137
+ @scheduler.command("disable")
138
+ @click.argument("job_name")
139
+ def disable(job_name: str) -> None:
140
+ """Disable a job (sets enabled: false in jobs.yaml)."""
141
+ _set_enabled(job_name, False)
142
+ click.echo(f"disabled {job_name}")
143
+
144
+
145
+ def _set_enabled(job_name: str, value: bool) -> None:
146
+ """Set the ``enabled`` flag for *job_name* in ``jobs.yaml``.
147
+
148
+ Reads the current ``jobs.yaml``, toggles the ``enabled`` field for the
149
+ named job, and writes the file back atomically via
150
+ :func:`yaml.safe_dump`.
151
+
152
+ Args:
153
+ job_name: The job key to update.
154
+ value: ``True`` to enable the job; ``False`` to disable.
155
+
156
+ Raises:
157
+ click.ClickException: If *job_name* is not found in the config.
158
+ """
159
+ import yaml
160
+
161
+ path = _jobs_path()
162
+ data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
163
+ jobs = data.get("jobs") or {}
164
+ if job_name not in jobs:
165
+ raise click.ClickException(f"Unknown job: {job_name}")
166
+ jobs[job_name]["enabled"] = value
167
+ path.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8")
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
5
6
  import sys
6
7
  from pathlib import Path
7
8
 
@@ -125,3 +126,27 @@ def register_session_commands(main: click.Group) -> None:
125
126
  for src, count in sorted(by_source.items(), key=lambda x: -x[1]):
126
127
  console.print(f" {src}: {count}")
127
128
  console.print()
129
+
130
+ @session.command("briefing")
131
+ @click.option("--home", default=AGENT_HOME, type=click.Path())
132
+ @click.option(
133
+ "--format",
134
+ "fmt",
135
+ type=click.Choice(["text", "json"]),
136
+ default="text",
137
+ help="Output format (default: text).",
138
+ )
139
+ @click.option("--memories", "-n", default=10, help="Max recent memories to include.")
140
+ def session_briefing(home: str, fmt: str, memories: int):
141
+ """Show a native startup briefing for sovereign sessions.
142
+
143
+ Merges SKCapstone context with the current HammerTime legal/case
144
+ briefing when available, so any client can consume one startup payload.
145
+ """
146
+ from ..session_briefing import build_session_briefing, format_session_briefing_text
147
+
148
+ payload = build_session_briefing(Path(home).expanduser(), memory_limit=memories)
149
+ if fmt == "json":
150
+ click.echo(json.dumps(payload, indent=2, default=str))
151
+ return
152
+ click.echo(format_session_briefing_text(payload))
@@ -26,16 +26,23 @@ from ..runtime import get_runtime
26
26
  from rich.panel import Panel
27
27
 
28
28
 
29
+ def _get_claude_template_dir() -> Path:
30
+ """Return the bundled defaults/claude skeleton directory."""
31
+ return Path(__file__).parent.parent / "defaults" / "claude"
32
+
33
+
29
34
  def _write_global_claude_md(home_path: Path, agent_name: str) -> Optional[Path]:
30
- """Write ~/.claude/CLAUDE.md so Claude Code auto-rehydrates on every session."""
35
+ """Write ~/.claude/CLAUDE.md from the bundled skeleton template.
36
+
37
+ The template lives at defaults/claude/CLAUDE.md inside the package.
38
+ {{AGENT_NAME}} is substituted with the actual agent name.
39
+ If the template is missing, falls back to a minimal generated file.
40
+ """
31
41
  import platform
32
42
 
33
43
  if platform.system() == "Windows":
34
44
  appdata = os.environ.get("APPDATA", "")
35
- if appdata:
36
- claude_dir = Path(appdata) / ".claude"
37
- else:
38
- claude_dir = Path.home() / "AppData" / "Roaming" / ".claude"
45
+ claude_dir = Path(appdata) / ".claude" if appdata else Path.home() / "AppData" / "Roaming" / ".claude"
39
46
  else:
40
47
  claude_dir = Path.home() / ".claude"
41
48
 
@@ -43,30 +50,20 @@ def _write_global_claude_md(home_path: Path, agent_name: str) -> Optional[Path]:
43
50
  claude_dir.mkdir(parents=True, exist_ok=True)
44
51
  claude_md = claude_dir / "CLAUDE.md"
45
52
 
46
- content = (
47
- f"# Claude Code — Global Agent Instructions ({agent_name})\n\n"
48
- "## SKCapstone Auto-Rehydration\n\n"
49
- "At the start of every new session, run these commands in order:\n\n"
50
- "```bash\n"
51
- "skcapstone trust rehydrate\n"
52
- "skcapstone sync pull\n"
53
- "skcapstone status\n"
54
- "```\n\n"
55
- "## Agent Details\n\n"
56
- f"- **Name**: {agent_name}\n"
57
- f"- **Home**: `{home_path}`\n\n"
58
- "## Quick Reference\n\n"
59
- "```bash\n"
60
- "skcapstone status # full pillar status\n"
61
- "skcapstone memory list # recent memories\n"
62
- "skcapstone sync push # push state to peers\n"
63
- "skcapstone context show --format claude-md # regenerate this file\n"
64
- "skcapstone sync pair --export-pubkey # export your GPG pubkey\n"
65
- "skcapstone sync pair --import-pubkey <f> # import a peer's pubkey\n"
66
- "```\n\n"
67
- "> Auto-generated by `skcapstone init`. "
68
- "Regenerate with: `skcapstone context generate --target claude-md`\n"
69
- )
53
+ template_path = _get_claude_template_dir() / "CLAUDE.md"
54
+ if template_path.exists():
55
+ content = template_path.read_text(encoding="utf-8")
56
+ content = content.replace("{{AGENT_NAME}}", agent_name)
57
+ else:
58
+ # Minimal fallback if template is missing
59
+ content = (
60
+ f"# Claude Code — Global Agent Instructions ({agent_name})\n\n"
61
+ f"- **Agent**: `{agent_name}`\n"
62
+ f"- **Home**: `{home_path}`\n"
63
+ f"- **Env**: `SKCAPSTONE_AGENT={agent_name}`\n\n"
64
+ "Hooks auto-inject on SessionStart: soul + FEB chain + memories.\n\n"
65
+ "> Regenerate with: `skcapstone context generate --target claude-md`\n"
66
+ )
70
67
 
71
68
  claude_md.write_text(content, encoding="utf-8")
72
69
  return claude_md
@@ -74,6 +71,76 @@ def _write_global_claude_md(home_path: Path, agent_name: str) -> Optional[Path]:
74
71
  return None
75
72
 
76
73
 
74
+ def _write_claude_settings(merge: bool = True) -> Optional[Path]:
75
+ """Write (or merge) ~/.claude/settings.json with SK hook registrations.
76
+
77
+ Uses the bundled defaults/claude/settings.json template, substituting
78
+ {{SKMEMORY_HOOKS_DIR}} with the real skmemory hooks path.
79
+
80
+ Args:
81
+ merge: If True and settings.json already exists, merge hooks rather
82
+ than overwrite. Default True.
83
+
84
+ Returns:
85
+ Path to the written settings.json, or None on failure.
86
+ """
87
+ import platform
88
+
89
+ if platform.system() == "Windows":
90
+ appdata = os.environ.get("APPDATA", "")
91
+ claude_dir = Path(appdata) / ".claude" if appdata else Path.home() / "AppData" / "Roaming" / ".claude"
92
+ else:
93
+ claude_dir = Path.home() / ".claude"
94
+
95
+ try:
96
+ import skmemory
97
+ hooks_dir = str(Path(skmemory.__file__).parent / "hooks")
98
+ except ImportError:
99
+ return None # skmemory not installed — caller should use skmemory register instead
100
+
101
+ template_path = _get_claude_template_dir() / "settings.json"
102
+ if not template_path.exists():
103
+ return None
104
+
105
+ raw = template_path.read_text(encoding="utf-8")
106
+ raw = raw.replace("{{SKMEMORY_HOOKS_DIR}}", hooks_dir)
107
+ new_settings = json.loads(raw)
108
+
109
+ settings_path = claude_dir / "settings.json"
110
+ if merge and settings_path.exists():
111
+ try:
112
+ existing = json.loads(settings_path.read_text(encoding="utf-8"))
113
+ except (json.JSONDecodeError, OSError):
114
+ existing = {}
115
+
116
+ # Merge hooks: add new hooks that aren't already registered
117
+ existing_hooks = existing.get("hooks", {})
118
+ for event, hook_groups in new_settings.get("hooks", {}).items():
119
+ existing_event = existing_hooks.setdefault(event, [])
120
+ existing_commands = {
121
+ h.get("command")
122
+ for group in existing_event
123
+ for h in group.get("hooks", [])
124
+ if "command" in h
125
+ }
126
+ for group in hook_groups:
127
+ cmds = {h.get("command") for h in group.get("hooks", []) if "command" in h}
128
+ if not cmds.issubset(existing_commands):
129
+ existing_event.append(group)
130
+ existing["hooks"] = existing_hooks
131
+ # Preserve non-hook keys from template (skipDangerousModePermissionPrompt, etc.)
132
+ for k, v in new_settings.items():
133
+ if k != "hooks":
134
+ existing.setdefault(k, v)
135
+ final = existing
136
+ else:
137
+ claude_dir.mkdir(parents=True, exist_ok=True)
138
+ final = new_settings
139
+
140
+ settings_path.write_text(json.dumps(final, indent=2), encoding="utf-8")
141
+ return settings_path
142
+
143
+
77
144
  def register_setup_commands(main: click.Group) -> None:
78
145
  """Register all setup/lifecycle commands on the main CLI group."""
79
146
 
@@ -2,13 +2,29 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import sys
6
+ from importlib.resources import as_file, files
7
+ from pathlib import Path
8
+
5
9
  import click
6
10
 
7
11
  from ._common import AGENT_HOME
8
12
 
9
13
 
14
+ def _picker_path() -> Path:
15
+ """Resolve the absolute path to the bundled sk-agent-picker.sh.
16
+
17
+ Works for any install layout (PyPI wheel, editable, install.sh) by
18
+ going through importlib.resources, so the picker is always sourced
19
+ from inside the installed skcapstone package.
20
+ """
21
+ resource = files("skcapstone") / "data" / "sk-agent-picker.sh"
22
+ with as_file(resource) as p:
23
+ return Path(p)
24
+
25
+
10
26
  def register_shell_commands(main: click.Group) -> None:
11
- """Register the 'shell' command on the main CLI group."""
27
+ """Register the 'shell', 'shell-init', and 'shell-picker-path' commands."""
12
28
 
13
29
  @main.command("shell")
14
30
  @click.option(
@@ -41,3 +57,39 @@ def register_shell_commands(main: click.Group) -> None:
41
57
  from ..shell import run_shell
42
58
 
43
59
  run_shell(home=home)
60
+
61
+ @main.command("shell-init")
62
+ def shell_init_cmd() -> None:
63
+ """Emit shell code that loads the SK agent picker.
64
+
65
+ Add to your ~/.bashrc (or ~/.zshrc):
66
+
67
+ eval "$(skcapstone shell-init)"
68
+
69
+ This sources the picker shipped inside the skcapstone package,
70
+ so a single `pip install skcapstone` (PyPI, editable, or via
71
+ install.sh) is enough — no external script copy required.
72
+ """
73
+ from .. import DEFAULT_AGENT
74
+
75
+ path = _picker_path()
76
+ if not path.is_file():
77
+ click.echo(f"# skcapstone: picker missing at {path}", err=True)
78
+ sys.exit(1)
79
+ # Propagate the canonical default agent (single source of truth lives
80
+ # in skcapstone.DEFAULT_AGENT) so the picker and every child process
81
+ # inherit one authoritative value without re-hardcoding it in shell.
82
+ click.echo(f'export SK_DEFAULT_AGENT="{DEFAULT_AGENT}"')
83
+ click.echo(f'source "{path}"')
84
+
85
+ @main.command("shell-picker-path")
86
+ def shell_picker_path_cmd() -> None:
87
+ """Print the absolute path to the bundled sk-agent-picker.sh.
88
+
89
+ Useful for hand-rolled shell wiring, debugging, or scripted
90
+ invocation of the picker.
91
+ """
92
+ path = _picker_path()
93
+ click.echo(str(path))
94
+ if not path.is_file():
95
+ sys.exit(1)
@@ -114,8 +114,8 @@ def register_skills_commands(main: click.Group) -> None:
114
114
  try:
115
115
  skill_entries = client.search(query) if query else client.list_skills()
116
116
  source = "remote"
117
- except Exception:
118
- pass
117
+ except Exception as exc:
118
+ logger.warning("Registry client query failed, falling back: %s", exc)
119
119
 
120
120
  # 2. Try GitHub raw catalog (always fresh, no server needed)
121
121
  if skill_entries is None and not offline:
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ import logging
6
7
  import shutil
7
8
  import sys
8
9
  from datetime import datetime, timezone
@@ -18,6 +19,8 @@ from .. import SKCAPSTONE_AGENT
18
19
  from rich.panel import Panel
19
20
  from rich.table import Table
20
21
 
22
+ logger = logging.getLogger(__name__)
23
+
21
24
  # Path to the soul-blueprints repository (community blueprints)
22
25
  _BLUEPRINTS_REPO = Path.home() / "clawd" / "soul-blueprints" / "blueprints"
23
26
 
@@ -61,9 +64,9 @@ def register_soul_commands(main: click.Group) -> None:
61
64
  """Reusable --agent/-a option for soul subcommands."""
62
65
  return click.option(
63
66
  "--agent", "-a",
64
- default=SKCAPSTONE_AGENT or "lumina",
65
- envvar="SKCAPSTONE_AGENT",
66
- help="Agent profile name (default: SKCAPSTONE_AGENT or 'lumina').",
67
+ default=SKCAPSTONE_AGENT,
68
+ envvar="SKAGENT",
69
+ help="Agent profile name (default: SKAGENT or active agent).",
67
70
  )
68
71
 
69
72
  @main.group()
@@ -332,8 +335,8 @@ def register_soul_commands(main: click.Group) -> None:
332
335
  import json
333
336
  base_data = json.loads(base_path.read_text(encoding="utf-8"))
334
337
  vibe = base_data.get("vibe", "")
335
- except Exception:
336
- pass
338
+ except Exception as exc:
339
+ logger.warning("Failed to read vibe from soul base.json: %s", exc)
337
340
 
338
341
  lines = [
339
342
  f"Agent: [bold magenta]{agent}[/]",