@smilintux/skcapstone 0.6.2 → 0.6.4

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 (70) hide show
  1. package/.github/workflows/publish.yml +1 -1
  2. package/CLAUDE.md +17 -0
  3. package/docs/CUSTOM_AGENT.md +40 -28
  4. package/docs/SOUL_SWAPPER.md +5 -5
  5. package/docs/hammertime-audit.md +402 -0
  6. package/openclaw-plugin/src/index.ts +2 -1
  7. package/package.json +1 -1
  8. package/pyproject.toml +2 -1
  9. package/scripts/install.sh +126 -1
  10. package/scripts/model-fallback-monitor.sh +4 -2
  11. package/scripts/refresh-anthropic-token.sh +9 -3
  12. package/scripts/release.sh +98 -0
  13. package/scripts/session-to-memory.py +1 -1
  14. package/scripts/sk-agent-picker.sh +237 -0
  15. package/scripts/telegram-catchup-all.sh +2 -1
  16. package/scripts/watch-anthropic-token.sh +12 -17
  17. package/src/skcapstone/__init__.py +34 -2
  18. package/src/skcapstone/cli/__init__.py +3 -1
  19. package/src/skcapstone/cli/_common.py +1 -0
  20. package/src/skcapstone/cli/context_cmd.py +16 -4
  21. package/src/skcapstone/cli/daemon.py +2 -1
  22. package/src/skcapstone/cli/joule_cmd.py +7 -3
  23. package/src/skcapstone/cli/memory.py +4 -2
  24. package/src/skcapstone/cli/register_cmd.py +19 -3
  25. package/src/skcapstone/cli/session.py +25 -0
  26. package/src/skcapstone/cli/setup.py +96 -30
  27. package/src/skcapstone/cli/soul.py +3 -3
  28. package/src/skcapstone/context_loader.py +9 -0
  29. package/src/skcapstone/coordination.py +9 -2
  30. package/src/skcapstone/daemon.py +22 -12
  31. package/src/skcapstone/defaults/claude/CLAUDE.md +67 -0
  32. package/src/skcapstone/defaults/claude/settings.json +74 -0
  33. package/src/skcapstone/defaults/lumina/config/skgraph.yaml +55 -10
  34. package/src/skcapstone/defaults/lumina/config/skmemory.yaml +79 -13
  35. package/src/skcapstone/defaults/lumina/config/skvector.yaml +60 -9
  36. package/src/skcapstone/defaults/unhinged.json +13 -0
  37. package/src/skcapstone/discovery.py +5 -5
  38. package/src/skcapstone/doctor.py +4 -2
  39. package/src/skcapstone/dreaming.py +3 -1
  40. package/src/skcapstone/fuse_mount.py +3 -1
  41. package/src/skcapstone/housekeeping.py +3 -3
  42. package/src/skcapstone/install_wizard.py +131 -0
  43. package/src/skcapstone/mcp_launcher.py +14 -1
  44. package/src/skcapstone/mcp_server.py +6 -21
  45. package/src/skcapstone/mcp_tools/notification_tools.py +3 -1
  46. package/src/skcapstone/memory_engine.py +10 -3
  47. package/src/skcapstone/migrate_multi_agent.py +7 -6
  48. package/src/skcapstone/notifications.py +6 -2
  49. package/src/skcapstone/onboard.py +19 -8
  50. package/src/skcapstone/operator_link.py +164 -0
  51. package/src/skcapstone/pillars/consciousness.py +2 -1
  52. package/src/skcapstone/pillars/identity.py +51 -7
  53. package/src/skcapstone/pillars/memory.py +9 -3
  54. package/src/skcapstone/runtime.py +13 -3
  55. package/src/skcapstone/service_health.py +23 -10
  56. package/src/skcapstone/session_briefing.py +108 -0
  57. package/src/skcapstone/trust_graph.py +40 -5
  58. package/src/skcapstone/unified_search.py +11 -2
  59. package/systemd/skcapstone.service +4 -6
  60. package/systemd/skcapstone@.service +7 -8
  61. package/systemd/skcomm-heartbeat.service +5 -2
  62. package/tests/conftest.py +21 -0
  63. package/tests/test_agent_home_scaffold.py +34 -0
  64. package/tests/test_backup.py +2 -1
  65. package/tests/test_mcp_server.py +78 -33
  66. package/tests/test_multi_agent.py +31 -29
  67. package/tests/test_operator_link.py +78 -0
  68. package/tests/test_runtime.py +21 -0
  69. package/tests/test_session_briefing.py +130 -0
  70. package/tests/test_trust_graph.py +18 -0
@@ -2,7 +2,7 @@
2
2
 
3
3
  Covers:
4
4
  - Per-agent home directory resolution (opus → agents/opus/, jarvis → agents/jarvis/)
5
- - Per-agent port assignment (opus=7777, jarvis=7778, unknown → next available)
5
+ - Default daemon port behavior under the profile-agnostic runtime
6
6
  - Default (no-agent) mode keeps backward-compatible home and port
7
7
  - SKCAPSTONE_AGENT env var propagation
8
8
  - DaemonConfig accepts distinct homes and ports for simultaneous agents
@@ -13,7 +13,6 @@ Covers:
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
- import json
17
16
  import os
18
17
  from pathlib import Path
19
18
  from unittest.mock import MagicMock, patch
@@ -84,17 +83,19 @@ class TestResolveAgentHome:
84
83
 
85
84
 
86
85
  class TestResolveAgentPort:
87
- def test_opus_gets_7777(self):
88
- """opus always gets port 7777."""
86
+ def test_known_agent_uses_registered_default_port(self):
87
+ """Known agents use the registered default daemon port."""
88
+ from skcapstone import AGENT_PORTS, DEFAULT_PORT
89
89
  from skcapstone.cli.daemon import _resolve_agent_port
90
90
 
91
- assert _resolve_agent_port("opus", None) == 7777
91
+ assert _resolve_agent_port("opus", None) == AGENT_PORTS["opus"] == DEFAULT_PORT
92
92
 
93
- def test_jarvis_gets_7778(self):
94
- """jarvis always gets port 7778."""
93
+ def test_second_known_agent_uses_registered_default_port(self):
94
+ """Jarvis also uses the registered default daemon port."""
95
+ from skcapstone import AGENT_PORTS, DEFAULT_PORT
95
96
  from skcapstone.cli.daemon import _resolve_agent_port
96
97
 
97
- assert _resolve_agent_port("jarvis", None) == 7778
98
+ assert _resolve_agent_port("jarvis", None) == AGENT_PORTS["jarvis"] == DEFAULT_PORT
98
99
 
99
100
  def test_explicit_port_overrides_agent_default(self):
100
101
  """Explicit --port always wins over the agent default."""
@@ -103,11 +104,12 @@ class TestResolveAgentPort:
103
104
  assert _resolve_agent_port("opus", 9999) == 9999
104
105
  assert _resolve_agent_port("jarvis", 8000) == 8000
105
106
 
106
- def test_no_agent_defaults_to_7777(self):
107
- """Single-agent / no-flag mode uses 7777."""
107
+ def test_no_agent_defaults_to_default_port(self):
108
+ """Single-agent / no-flag mode uses the package default port."""
109
+ from skcapstone import DEFAULT_PORT
108
110
  from skcapstone.cli.daemon import _resolve_agent_port
109
111
 
110
- assert _resolve_agent_port(None, None) == 7777
112
+ assert _resolve_agent_port(None, None) == DEFAULT_PORT
111
113
 
112
114
  def test_unknown_agent_gets_next_port(self):
113
115
  """An agent not in AGENT_PORTS gets max(ports)+1."""
@@ -118,11 +120,11 @@ class TestResolveAgentPort:
118
120
  result = _resolve_agent_port("brandnew", None)
119
121
  assert result == expected
120
122
 
121
- def test_opus_and_jarvis_ports_differ(self):
122
- """Opus and Jarvis must listen on different ports."""
123
+ def test_explicit_ports_can_differ_for_isolated_agents(self):
124
+ """Simultaneous agent daemons can still isolate by explicit port."""
123
125
  from skcapstone.cli.daemon import _resolve_agent_port
124
126
 
125
- assert _resolve_agent_port("opus", None) != _resolve_agent_port("jarvis", None)
127
+ assert _resolve_agent_port("opus", 7777) != _resolve_agent_port("jarvis", 7778)
126
128
 
127
129
 
128
130
  # ---------------------------------------------------------------------------
@@ -132,22 +134,22 @@ class TestResolveAgentPort:
132
134
 
133
135
  class TestAgentPortsRegistry:
134
136
  def test_opus_registered(self):
135
- from skcapstone import AGENT_PORTS
137
+ from skcapstone import AGENT_PORTS, DEFAULT_PORT
136
138
 
137
139
  assert "opus" in AGENT_PORTS
138
- assert AGENT_PORTS["opus"] == 7777
140
+ assert AGENT_PORTS["opus"] == DEFAULT_PORT
139
141
 
140
142
  def test_jarvis_registered(self):
141
- from skcapstone import AGENT_PORTS
143
+ from skcapstone import AGENT_PORTS, DEFAULT_PORT
142
144
 
143
145
  assert "jarvis" in AGENT_PORTS
144
- assert AGENT_PORTS["jarvis"] == 7778
146
+ assert AGENT_PORTS["jarvis"] == DEFAULT_PORT
145
147
 
146
- def test_all_ports_unique(self):
148
+ def test_all_ports_are_ints(self):
147
149
  from skcapstone import AGENT_PORTS
148
150
 
149
- ports = list(AGENT_PORTS.values())
150
- assert len(ports) == len(set(ports)), "Duplicate ports in AGENT_PORTS"
151
+ assert AGENT_PORTS
152
+ assert all(isinstance(port, int) for port in AGENT_PORTS.values())
151
153
 
152
154
 
153
155
  # ---------------------------------------------------------------------------
@@ -220,8 +222,7 @@ class TestDaemonConfigMultiAgent:
220
222
 
221
223
  assert opus_cfg.home != jarvis_cfg.home
222
224
  assert opus_cfg.port != jarvis_cfg.port
223
- assert opus_cfg.port == 7777
224
- assert jarvis_cfg.port == 7778
225
+
225
226
 
226
227
  def test_log_files_are_in_respective_homes(self, tmp_path: Path):
227
228
  """Each agent's log file lives under its own home."""
@@ -244,24 +245,25 @@ class TestDaemonConfigMultiAgent:
244
245
 
245
246
 
246
247
  class TestAgentHomeEnvVar:
247
- def test_env_var_produces_agents_subdir(self, monkeypatch):
248
- """SKCAPSTONE_AGENT=opus AGENT_HOME includes agents/opus."""
248
+ def test_env_var_keeps_shared_root_and_agent_home_resolves_subdir(self, monkeypatch):
249
+ """SKCAPSTONE_AGENT keeps AGENT_HOME at root and agent_home() resolves the agent subdir."""
249
250
  import importlib
250
251
 
251
252
  monkeypatch.setenv("SKCAPSTONE_AGENT", "opus")
252
- monkeypatch.setenv("SKCAPSTONE_ROOT", "/tmp/sk")
253
+ monkeypatch.setenv("SKCAPSTONE_HOME", "/tmp/sk")
253
254
 
254
255
  import skcapstone as pkg
255
256
  importlib.reload(pkg)
256
257
 
257
- assert "agents/opus" in pkg.AGENT_HOME or "agents\\opus" in pkg.AGENT_HOME
258
+ assert pkg.AGENT_HOME == "/tmp/sk"
259
+ assert "agents/opus" in str(pkg.agent_home("opus")) or "agents\\opus" in str(pkg.agent_home("opus"))
258
260
 
259
261
  def test_no_env_var_uses_root_directly(self, monkeypatch):
260
- """Without SKCAPSTONE_AGENT, AGENT_HOME == SKCAPSTONE_ROOT."""
262
+ """Without SKCAPSTONE_AGENT, AGENT_HOME stays at the shared root."""
261
263
  import importlib
262
264
 
263
265
  monkeypatch.delenv("SKCAPSTONE_AGENT", raising=False)
264
- monkeypatch.setenv("SKCAPSTONE_ROOT", "/tmp/sk")
266
+ monkeypatch.setenv("SKCAPSTONE_HOME", "/tmp/sk")
265
267
 
266
268
  import skcapstone as pkg
267
269
  importlib.reload(pkg)
@@ -0,0 +1,78 @@
1
+ """Tests for human-operator manifest linking."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from skcapstone.operator_link import build_agent_manifest, discover_human_operator
9
+
10
+
11
+ def test_discover_human_operator_reads_capauth_profile(tmp_path: Path) -> None:
12
+ """A CapAuth human profile is converted into operator metadata."""
13
+ capauth_home = tmp_path / ".capauth"
14
+ profile = capauth_home / "identity" / "profile.json"
15
+ profile.parent.mkdir(parents=True)
16
+ profile.write_text(
17
+ json.dumps(
18
+ {
19
+ "entity": {
20
+ "name": "Casey",
21
+ "entity_type": "human",
22
+ "email": "casey@example.com",
23
+ "handle": "casey@example.com",
24
+ },
25
+ "key_info": {
26
+ "fingerprint": "ABCDEF1234567890",
27
+ },
28
+ }
29
+ ),
30
+ encoding="utf-8",
31
+ )
32
+
33
+ operator = discover_human_operator(capauth_home)
34
+
35
+ assert operator == {
36
+ "name": "Casey",
37
+ "relationship": "human-operator",
38
+ "entity_type": "human",
39
+ "source": "capauth",
40
+ "email": "casey@example.com",
41
+ "handle": "casey@example.com",
42
+ "fingerprint": "ABCDEF1234567890",
43
+ }
44
+
45
+
46
+ def test_discover_human_operator_ignores_non_human_profile(tmp_path: Path) -> None:
47
+ """AI CapAuth profiles are not treated as human operators."""
48
+ capauth_home = tmp_path / ".capauth"
49
+ profile = capauth_home / "identity" / "profile.json"
50
+ profile.parent.mkdir(parents=True)
51
+ profile.write_text(
52
+ json.dumps(
53
+ {
54
+ "entity": {
55
+ "name": "Jarvis",
56
+ "entity_type": "ai",
57
+ }
58
+ }
59
+ ),
60
+ encoding="utf-8",
61
+ )
62
+
63
+ assert discover_human_operator(capauth_home) is None
64
+
65
+
66
+ def test_build_agent_manifest_includes_operator_when_available() -> None:
67
+ """Operator metadata is persisted directly in the manifest."""
68
+ manifest = build_agent_manifest(
69
+ "jarvis",
70
+ "0.6.0",
71
+ created_at="2026-01-01T00:00:00+00:00",
72
+ operator={"name": "Casey", "fingerprint": "FP123", "relationship": "human-operator"},
73
+ )
74
+
75
+ assert manifest["name"] == "jarvis"
76
+ assert manifest["entity_type"] == "ai-agent"
77
+ assert manifest["operator"]["name"] == "Casey"
78
+ assert manifest["operator"]["fingerprint"] == "FP123"
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
5
6
  from pathlib import Path
6
7
 
7
8
  from skcapstone.runtime import AgentRuntime
@@ -28,6 +29,26 @@ class TestAgentRuntime:
28
29
  assert manifest.name == "test-agent"
29
30
  assert manifest.last_awakened is not None
30
31
 
32
+ def test_awaken_prefers_identity_name_over_shared_config(self, tmp_path: Path):
33
+ """Agent-local identity should beat a shared-root fallback config name."""
34
+ shared_root = tmp_path / ".skcapstone"
35
+ agent_home = shared_root / "agents" / "aster"
36
+ (agent_home / "identity").mkdir(parents=True)
37
+ (agent_home / "config").mkdir(parents=True)
38
+
39
+ (shared_root / "config").mkdir(parents=True)
40
+ (shared_root / "config" / "config.yaml").write_text("agent_name: Jarvis\n")
41
+ (agent_home / "manifest.json").write_text(json.dumps({"name": "aster"}))
42
+ (agent_home / "identity" / "identity.json").write_text(json.dumps({
43
+ "name": "Aster",
44
+ "fingerprint": "A" * 40,
45
+ "capauth_managed": True,
46
+ }))
47
+
48
+ runtime = AgentRuntime(home=agent_home)
49
+ manifest = runtime.awaken()
50
+ assert manifest.name == "Aster"
51
+
31
52
  def test_register_connector(self, initialized_agent_home: Path):
32
53
  """Registering a connector should persist it."""
33
54
  runtime = AgentRuntime(home=initialized_agent_home)
@@ -0,0 +1,130 @@
1
+ """Tests for the native SKCapstone session briefing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+ from click.testing import CliRunner
10
+
11
+ from skcapstone.cli import main
12
+ from skcapstone.session_briefing import (
13
+ build_session_briefing,
14
+ format_session_briefing_text,
15
+ load_hammertime_briefing,
16
+ )
17
+
18
+
19
+ def test_load_hammertime_briefing_respects_disable_env(monkeypatch, tmp_path: Path) -> None:
20
+ """It returns None when HammerTime briefing is explicitly disabled."""
21
+ monkeypatch.setenv("SK_INCLUDE_HAMMERTIME_BRIEFING", "0")
22
+ assert load_hammertime_briefing(root=tmp_path) is None
23
+
24
+
25
+ def test_load_hammertime_briefing_parses_json(monkeypatch, tmp_path: Path) -> None:
26
+ """It parses JSON output from the HammerTime briefing script."""
27
+ script = tmp_path / "scripts" / "case-briefing.py"
28
+ script.parent.mkdir(parents=True)
29
+ script.write_text("#!/usr/bin/env python3\n", encoding="utf-8")
30
+
31
+ payload = {"summary": {"queue_size": 2}, "alert_count": 1}
32
+
33
+ def fake_run(*args, **kwargs): # noqa: ANN002, ANN003
34
+ return subprocess.CompletedProcess(
35
+ args=args[0],
36
+ returncode=0,
37
+ stdout=json.dumps(payload),
38
+ stderr="",
39
+ )
40
+
41
+ monkeypatch.delenv("SK_INCLUDE_HAMMERTIME_BRIEFING", raising=False)
42
+ monkeypatch.setattr("skcapstone.session_briefing.subprocess.run", fake_run)
43
+
44
+ assert load_hammertime_briefing(root=tmp_path) == payload
45
+
46
+
47
+ def test_build_session_briefing_includes_skcapstone_and_hammertime(monkeypatch, tmp_path: Path) -> None:
48
+ """It builds a combined payload for startup consumers."""
49
+ ctx = {"agent": {"name": "Aster"}, "memories": []}
50
+ briefing = {"summary": {"queue_size": 1}, "alert_count": 0}
51
+
52
+ monkeypatch.setattr("skcapstone.session_briefing.gather_context", lambda home, memory_limit=10: ctx)
53
+ monkeypatch.setattr(
54
+ "skcapstone.session_briefing.load_hammertime_briefing",
55
+ lambda python_bin=None: briefing,
56
+ )
57
+
58
+ payload = build_session_briefing(tmp_path, memory_limit=3)
59
+
60
+ assert payload["agent_home"] == str(tmp_path)
61
+ assert payload["skcapstone_context"] == ctx
62
+ assert payload["hammertime_briefing"] == briefing
63
+ assert "generated_at" in payload
64
+
65
+
66
+ def test_format_session_briefing_text_contains_hammertime_section() -> None:
67
+ """It renders a readable summary including the HammerTime section."""
68
+ payload = {
69
+ "generated_at": "2026-04-09T00:00:00+00:00",
70
+ "agent_home": "/tmp/aster",
71
+ "skcapstone_context": {
72
+ "agent": {"name": "Aster", "is_conscious": True, "fingerprint": "abc123"},
73
+ "pillars": {},
74
+ "board": {"total": 0},
75
+ "memories": [],
76
+ "soul": {"active": None},
77
+ "mcp": {"available": False},
78
+ "gathered_at": "2026-04-09T00:00:00+00:00",
79
+ },
80
+ "hammertime_briefing": {
81
+ "alert_count": 1,
82
+ "summary": {"queue_size": 2},
83
+ "top_priority": {
84
+ "incident_id": "INC-001",
85
+ "problem_slug": "example-problem",
86
+ "action": "File claim of exemption",
87
+ "status": "in-progress",
88
+ },
89
+ "focus_items": [
90
+ {
91
+ "incident_id": "INC-001",
92
+ "action": "Review preferred filing",
93
+ "status": "in-progress",
94
+ }
95
+ ],
96
+ },
97
+ }
98
+
99
+ output = format_session_briefing_text(payload)
100
+
101
+ assert "# SKCapstone Session Briefing" in output
102
+ assert "## hammertime briefing" in output
103
+ assert "INC-001" in output
104
+ assert "File claim of exemption" in output
105
+
106
+
107
+ def test_session_briefing_cli_json(monkeypatch, tmp_path: Path) -> None:
108
+ """The CLI exposes the combined payload as JSON."""
109
+ runner = CliRunner()
110
+ payload = {
111
+ "generated_at": "2026-04-09T00:00:00+00:00",
112
+ "agent_home": str(tmp_path),
113
+ "skcapstone_context": {"agent": {"name": "Aster"}},
114
+ "hammertime_briefing": {"summary": {"queue_size": 1}, "alert_count": 0},
115
+ }
116
+
117
+ monkeypatch.setattr(
118
+ "skcapstone.session_briefing.build_session_briefing",
119
+ lambda home, memory_limit=10: payload,
120
+ )
121
+
122
+ result = runner.invoke(
123
+ main,
124
+ ["session", "briefing", "--home", str(tmp_path), "--format", "json"],
125
+ )
126
+
127
+ assert result.exit_code == 0
128
+ parsed = json.loads(result.output)
129
+ assert parsed["skcapstone_context"]["agent"]["name"] == "Aster"
130
+ assert parsed["hammertime_briefing"]["summary"]["queue_size"] == 1
@@ -121,6 +121,24 @@ class TestBuildGraph:
121
121
  sync_edges = [e for e in graph.edges if e.edge_type == "sync"]
122
122
  assert len(sync_edges) >= 1
123
123
 
124
+ def test_manifest_operator_creates_human_link(self, tmp_agent_home: Path):
125
+ """Manifest operator metadata appears as an explicit trust relationship."""
126
+ _init_agent(tmp_agent_home, "operator-graph")
127
+ manifest_path = tmp_agent_home / "manifest.json"
128
+ manifest = json.loads(manifest_path.read_text())
129
+ manifest["operator"] = {
130
+ "name": "Casey",
131
+ "fingerprint": "FP1234567890",
132
+ "relationship": "human-operator",
133
+ "entity_type": "human",
134
+ }
135
+ manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
136
+
137
+ graph = build_trust_graph(tmp_agent_home)
138
+
139
+ assert any(n.label == "Casey" for n in graph.nodes)
140
+ assert any(e.edge_type == "operator" and e.label == "human-operator" for e in graph.edges)
141
+
124
142
 
125
143
  class TestFormatDot:
126
144
  """Tests for DOT format output."""