@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
@@ -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}[/]",
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ import logging
6
7
  import os
7
8
  import shutil
8
9
  import sys
@@ -19,6 +20,8 @@ from ..runtime import get_runtime
19
20
  from rich.panel import Panel
20
21
  from rich.table import Table
21
22
 
23
+ logger = logging.getLogger(__name__)
24
+
22
25
 
23
26
  def _probe_llm_backends() -> dict[str, bool]:
24
27
  """Probe LLM backend availability.
@@ -43,8 +46,8 @@ def _probe_llm_backends() -> dict[str, bool]:
43
46
  urllib.request.Request(f"{host}/api/tags"), timeout=2
44
47
  ):
45
48
  backends["ollama"] = True
46
- except Exception:
47
- pass
49
+ except Exception as exc:
50
+ logger.debug("Ollama probe failed (not available): %s", exc)
48
51
  return backends
49
52
 
50
53
 
@@ -129,8 +132,8 @@ def _read_local_heartbeat(home: Path) -> Optional[dict]:
129
132
  hb_path = home / "heartbeats" / f"{agent_name}.json"
130
133
  if hb_path.exists():
131
134
  return json.loads(hb_path.read_text())
132
- except Exception:
133
- pass
135
+ except Exception as exc:
136
+ logger.warning("Failed to load heartbeat data: %s", exc)
134
137
  return None
135
138
 
136
139
 
@@ -148,8 +151,11 @@ def _print_consciousness_metrics(console, home: Optional[Path] = None) -> None:
148
151
  import urllib.request
149
152
  import urllib.error
150
153
 
154
+ from .. import AGENT_PORTS, DEFAULT_PORT, SKCAPSTONE_AGENT
155
+
156
+ port = AGENT_PORTS.get(SKCAPSTONE_AGENT, DEFAULT_PORT)
151
157
  try:
152
- with urllib.request.urlopen("http://localhost:7777/consciousness", timeout=2) as resp:
158
+ with urllib.request.urlopen(f"http://localhost:{port}/consciousness", timeout=2) as resp:
153
159
  data = json.loads(resp.read().decode("utf-8"))
154
160
  enabled = data.get("enabled", False)
155
161
  messages = data.get("messages_processed", 0)
@@ -250,6 +256,14 @@ def register_status_commands(main: click.Group) -> None:
250
256
  trust_detail += " [green]ENTANGLED[/]"
251
257
  table.add_row("Trust", "Cloud 9", status_icon(trust.status), trust_detail)
252
258
 
259
+ con = m.consciousness
260
+ con_detail = f"{con.sessions_digested} sessions digested, {con.topics_tracked} topics"
261
+ if con.whisper_active:
262
+ con_detail += " [green]daemon active[/]"
263
+ if con.whisper_md_age_hours < 24:
264
+ con_detail += f" [dim](whisper {con.whisper_md_age_hours:.1f}h old)[/]"
265
+ table.add_row("Consciousness", "SKWhisper", status_icon(con.status), con_detail)
266
+
253
267
  sec = m.security
254
268
  table.add_row(
255
269
  "Security", "SKSecurity", status_icon(sec.status),
@@ -332,8 +346,8 @@ def register_status_commands(main: click.Group) -> None:
332
346
  f"\n [bold yellow]WARNING:[/] [yellow]Low disk space: "
333
347
  f"{free_gb:.1f} GB free[/]"
334
348
  )
335
- except Exception:
336
- pass
349
+ except Exception as exc:
350
+ logger.debug("Failed to check disk space: %s", exc)
337
351
 
338
352
  console.print()
339
353
  console.print(f" [dim]Home: {m.home}[/]")
@@ -575,11 +589,23 @@ def register_status_commands(main: click.Group) -> None:
575
589
  "agent": "Agent Home",
576
590
  "identity": "Identity (CapAuth)",
577
591
  "memory": "Memory (SKMemory)",
578
- "transport": "Transport (SKComm)",
592
+ "transport": "Transport (SKComms)",
579
593
  "sync": "Sync (Singularity)",
594
+ "codex": "Codex Integration",
595
+ "harness": "AI Harness (Claude Code)",
580
596
  }
581
597
 
582
- for cat_key in ["packages", "system", "agent", "identity", "memory", "transport", "sync"]:
598
+ for cat_key in [
599
+ "packages",
600
+ "system",
601
+ "agent",
602
+ "identity",
603
+ "memory",
604
+ "transport",
605
+ "sync",
606
+ "codex",
607
+ "harness",
608
+ ]:
583
609
  checks = categories.get(cat_key, [])
584
610
  if not checks:
585
611
  continue
@@ -718,8 +744,8 @@ def register_status_commands(main: click.Group) -> None:
718
744
  import webbrowser
719
745
  try:
720
746
  webbrowser.open(url)
721
- except Exception:
722
- pass
747
+ except Exception as exc:
748
+ logger.warning("Failed to open browser for dashboard: %s", exc)
723
749
 
724
750
  server = start_dashboard(home_path, port=port)
725
751
  try:
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  import json
7
+ import os
7
8
  import sys
8
9
  from pathlib import Path
9
10
 
@@ -166,6 +167,26 @@ def register_telegram_commands(main: click.Group) -> None:
166
167
  lines.append(f" {key}: [cyan]{val}[/]")
167
168
 
168
169
  console.print(Panel("\n".join(lines), title="Telegram Catch-Up", border_style="green"))
170
+
171
+ # Post-import hook: run auto-tagger on the last 6 minutes of files
172
+ # (just enough to cover what the catchup brought in). Fires
173
+ # inline so failures are visible; the hourly cron is the safety net.
174
+ _auto_tag_script = os.path.expanduser(
175
+ "~/.skcapstone/agents/lumina/scripts/auto-tag-hallucinations.py"
176
+ )
177
+ if os.path.isfile(_auto_tag_script):
178
+ import subprocess as _subprocess
179
+ _tag_result = _subprocess.run(
180
+ [sys.executable, _auto_tag_script, "--hours", "0.1", "--quiet"],
181
+ capture_output=True,
182
+ text=True,
183
+ )
184
+ if _tag_result.returncode != 0:
185
+ console.print(
186
+ f"[yellow]auto-tag-hallucinations warning:[/] {_tag_result.stderr.strip() or 'non-zero exit'}"
187
+ )
188
+ elif _tag_result.stdout.strip():
189
+ console.print(f"[dim]auto-tag:[/] {_tag_result.stdout.strip()}")
169
190
  except Exception as e:
170
191
  console.print(f"[red]Error:[/] {e}")
171
192
  raise SystemExit(1)
@@ -2,7 +2,7 @@
2
2
 
3
3
  Usage:
4
4
  skcapstone test # run all packages
5
- skcapstone test --package skcomm # run one package
5
+ skcapstone test --package skcomms # run one package
6
6
  skcapstone test --fast # stop on first failure
7
7
  skcapstone test --json-out # machine-readable JSON output
8
8
  skcapstone test --verbose # pass -v to pytest
@@ -183,15 +183,15 @@ def register_test_commands(main: click.Group) -> None:
183
183
  ) -> None:
184
184
  """Run pytest across all ecosystem packages and show a summary table.
185
185
 
186
- Discovers packages in the monorepo (skcapstone, capauth, skcomm,
187
- skchat, skmemory, cloud9-python) and runs their test suites in
186
+ Discovers packages in the monorepo (skcapstone, capauth, skcomms,
187
+ skchat, skmemory, cloud9) and runs their test suites in
188
188
  sequence, then renders a consolidated Rich table.
189
189
 
190
190
  \b
191
191
  Examples:
192
192
  skcapstone test
193
- skcapstone test --package skcomm
194
- skcapstone test -p skcomm -p skchat
193
+ skcapstone test --package skcomms
194
+ skcapstone test -p skcomms -p skchat
195
195
  skcapstone test --fast --verbose
196
196
  skcapstone test --json-out | jq .
197
197
  """
@@ -1,4 +1,4 @@
1
- """test-connection command — ping a peer via SKComm and measure latency.
1
+ """test-connection command — ping a peer via SKComms and measure latency.
2
2
 
3
3
  Usage:
4
4
  skcapstone test-connection <peer>
@@ -176,7 +176,7 @@ def register_test_connection_commands(main: click.Group) -> None:
176
176
  )
177
177
  @click.option("--home", default=AGENT_HOME, type=click.Path())
178
178
  def test_connection(peer: str, timeout: float, count: int, home: str) -> None:
179
- """Test connectivity to a peer by sending a ping via SKComm.
179
+ """Test connectivity to a peer by sending a ping via SKComms.
180
180
 
181
181
  Sends a ping message, waits for the peer to reply with a pong,
182
182
  and reports the round-trip latency. Exits with code 1 when the