@smilintux/skcapstone 0.6.2 → 0.6.3
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.
- package/.github/workflows/publish.yml +1 -1
- package/CLAUDE.md +17 -0
- package/docs/CUSTOM_AGENT.md +40 -28
- package/docs/SOUL_SWAPPER.md +5 -5
- package/docs/hammertime-audit.md +402 -0
- package/openclaw-plugin/src/index.ts +2 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/install.sh +126 -1
- package/scripts/model-fallback-monitor.sh +4 -2
- package/scripts/refresh-anthropic-token.sh +9 -3
- package/scripts/release.sh +98 -0
- package/scripts/session-to-memory.py +1 -1
- package/scripts/sk-agent-picker.sh +237 -0
- package/scripts/telegram-catchup-all.sh +2 -1
- package/scripts/watch-anthropic-token.sh +12 -17
- package/src/skcapstone/__init__.py +34 -2
- package/src/skcapstone/cli/__init__.py +3 -1
- package/src/skcapstone/cli/_common.py +1 -0
- package/src/skcapstone/cli/context_cmd.py +16 -4
- package/src/skcapstone/cli/daemon.py +2 -1
- package/src/skcapstone/cli/joule_cmd.py +7 -3
- package/src/skcapstone/cli/memory.py +4 -2
- package/src/skcapstone/cli/register_cmd.py +19 -3
- package/src/skcapstone/cli/session.py +25 -0
- package/src/skcapstone/cli/setup.py +96 -30
- package/src/skcapstone/cli/soul.py +3 -3
- package/src/skcapstone/context_loader.py +9 -0
- package/src/skcapstone/coordination.py +9 -2
- package/src/skcapstone/daemon.py +22 -12
- package/src/skcapstone/defaults/claude/CLAUDE.md +67 -0
- package/src/skcapstone/defaults/claude/settings.json +74 -0
- package/src/skcapstone/defaults/lumina/config/skgraph.yaml +55 -10
- package/src/skcapstone/defaults/lumina/config/skmemory.yaml +79 -13
- package/src/skcapstone/defaults/lumina/config/skvector.yaml +60 -9
- package/src/skcapstone/defaults/unhinged.json +13 -0
- package/src/skcapstone/discovery.py +5 -5
- package/src/skcapstone/doctor.py +4 -2
- package/src/skcapstone/dreaming.py +3 -1
- package/src/skcapstone/fuse_mount.py +3 -1
- package/src/skcapstone/housekeeping.py +3 -3
- package/src/skcapstone/install_wizard.py +131 -0
- package/src/skcapstone/mcp_launcher.py +14 -1
- package/src/skcapstone/mcp_server.py +6 -21
- package/src/skcapstone/mcp_tools/notification_tools.py +3 -1
- package/src/skcapstone/memory_engine.py +10 -3
- package/src/skcapstone/migrate_multi_agent.py +7 -6
- package/src/skcapstone/notifications.py +6 -2
- package/src/skcapstone/onboard.py +19 -8
- package/src/skcapstone/operator_link.py +164 -0
- package/src/skcapstone/pillars/consciousness.py +2 -1
- package/src/skcapstone/pillars/identity.py +51 -7
- package/src/skcapstone/pillars/memory.py +9 -3
- package/src/skcapstone/runtime.py +13 -3
- package/src/skcapstone/service_health.py +23 -10
- package/src/skcapstone/session_briefing.py +108 -0
- package/src/skcapstone/trust_graph.py +40 -5
- package/src/skcapstone/unified_search.py +11 -2
- package/systemd/skcapstone.service +4 -6
- package/systemd/skcapstone@.service +7 -8
- package/systemd/skcomm-heartbeat.service +5 -2
- package/tests/conftest.py +21 -0
- package/tests/test_agent_home_scaffold.py +34 -0
- package/tests/test_backup.py +2 -1
- package/tests/test_mcp_server.py +78 -33
- package/tests/test_multi_agent.py +31 -29
- package/tests/test_operator_link.py +78 -0
- package/tests/test_runtime.py +21 -0
- package/tests/test_session_briefing.py +130 -0
- package/tests/test_trust_graph.py +18 -0
|
@@ -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"
|
package/tests/test_runtime.py
CHANGED
|
@@ -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."""
|