@smilintux/skcapstone 0.1.0
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/.cursorrules +33 -0
- package/.github/workflows/ci.yml +23 -0
- package/.github/workflows/publish.yml +52 -0
- package/AGENTS.md +74 -0
- package/CLAUDE.md +56 -0
- package/LICENSE +674 -0
- package/README.md +242 -0
- package/SKILL.md +36 -0
- package/bin/cli.js +18 -0
- package/docs/ARCHITECTURE.md +510 -0
- package/docs/SECURITY_DESIGN.md +315 -0
- package/docs/SOVEREIGN_SINGULARITY.md +371 -0
- package/docs/TOKEN_SYSTEM.md +201 -0
- package/index.d.ts +9 -0
- package/index.js +32 -0
- package/package.json +32 -0
- package/pyproject.toml +84 -0
- package/src/skcapstone/__init__.py +13 -0
- package/src/skcapstone/cli.py +1441 -0
- package/src/skcapstone/connectors/__init__.py +6 -0
- package/src/skcapstone/coordination.py +590 -0
- package/src/skcapstone/discovery.py +275 -0
- package/src/skcapstone/memory_engine.py +457 -0
- package/src/skcapstone/models.py +223 -0
- package/src/skcapstone/pillars/__init__.py +8 -0
- package/src/skcapstone/pillars/identity.py +91 -0
- package/src/skcapstone/pillars/memory.py +61 -0
- package/src/skcapstone/pillars/security.py +83 -0
- package/src/skcapstone/pillars/sync.py +486 -0
- package/src/skcapstone/pillars/trust.py +335 -0
- package/src/skcapstone/runtime.py +190 -0
- package/src/skcapstone/skills/__init__.py +1 -0
- package/src/skcapstone/skills/syncthing_setup.py +297 -0
- package/src/skcapstone/sync/__init__.py +14 -0
- package/src/skcapstone/sync/backends.py +330 -0
- package/src/skcapstone/sync/engine.py +301 -0
- package/src/skcapstone/sync/models.py +97 -0
- package/src/skcapstone/sync/vault.py +284 -0
- package/src/skcapstone/tokens.py +439 -0
- package/tests/__init__.py +0 -0
- package/tests/conftest.py +42 -0
- package/tests/test_coordination.py +299 -0
- package/tests/test_discovery.py +57 -0
- package/tests/test_memory_engine.py +391 -0
- package/tests/test_models.py +63 -0
- package/tests/test_pillars.py +87 -0
- package/tests/test_runtime.py +60 -0
- package/tests/test_sync.py +507 -0
- package/tests/test_syncthing_setup.py +76 -0
- package/tests/test_tokens.py +265 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""Tests for the SKCapstone coordination module."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from skcapstone.coordination import (
|
|
9
|
+
AgentFile,
|
|
10
|
+
AgentState,
|
|
11
|
+
Board,
|
|
12
|
+
Task,
|
|
13
|
+
TaskPriority,
|
|
14
|
+
TaskStatus,
|
|
15
|
+
TaskView,
|
|
16
|
+
get_briefing_json,
|
|
17
|
+
get_briefing_text,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def board(tmp_path: Path) -> Board:
|
|
23
|
+
"""Create a board with a temporary home directory."""
|
|
24
|
+
b = Board(tmp_path)
|
|
25
|
+
b.ensure_dirs()
|
|
26
|
+
return b
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TestTask:
|
|
30
|
+
"""Tests for the Task model."""
|
|
31
|
+
|
|
32
|
+
def test_defaults(self):
|
|
33
|
+
t = Task(title="Test task")
|
|
34
|
+
assert t.title == "Test task"
|
|
35
|
+
assert len(t.id) == 8
|
|
36
|
+
assert t.priority == TaskPriority.MEDIUM
|
|
37
|
+
assert t.tags == []
|
|
38
|
+
assert t.created_at is not None
|
|
39
|
+
|
|
40
|
+
def test_full_construction(self):
|
|
41
|
+
t = Task(
|
|
42
|
+
id="abc12345",
|
|
43
|
+
title="Build CLI",
|
|
44
|
+
description="Implement capauth CLI",
|
|
45
|
+
priority=TaskPriority.HIGH,
|
|
46
|
+
tags=["capauth", "cli"],
|
|
47
|
+
created_by="jarvis",
|
|
48
|
+
acceptance_criteria=["capauth init works"],
|
|
49
|
+
dependencies=["dep001"],
|
|
50
|
+
)
|
|
51
|
+
assert t.id == "abc12345"
|
|
52
|
+
assert t.priority == TaskPriority.HIGH
|
|
53
|
+
assert "capauth" in t.tags
|
|
54
|
+
assert len(t.dependencies) == 1
|
|
55
|
+
|
|
56
|
+
def test_serialization_roundtrip(self):
|
|
57
|
+
t = Task(title="Roundtrip", tags=["test"])
|
|
58
|
+
data = t.model_dump()
|
|
59
|
+
t2 = Task.model_validate(data)
|
|
60
|
+
assert t2.title == t.title
|
|
61
|
+
assert t2.id == t.id
|
|
62
|
+
assert t2.tags == t.tags
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TestAgentFile:
|
|
66
|
+
"""Tests for the AgentFile model."""
|
|
67
|
+
|
|
68
|
+
def test_defaults(self):
|
|
69
|
+
af = AgentFile(agent="jarvis")
|
|
70
|
+
assert af.agent == "jarvis"
|
|
71
|
+
assert af.state == AgentState.ACTIVE
|
|
72
|
+
assert af.current_task is None
|
|
73
|
+
assert af.claimed_tasks == []
|
|
74
|
+
assert af.completed_tasks == []
|
|
75
|
+
|
|
76
|
+
def test_with_claims(self):
|
|
77
|
+
af = AgentFile(
|
|
78
|
+
agent="opus",
|
|
79
|
+
current_task="task001",
|
|
80
|
+
claimed_tasks=["task001", "task002"],
|
|
81
|
+
capabilities=["python", "docs"],
|
|
82
|
+
)
|
|
83
|
+
assert af.current_task == "task001"
|
|
84
|
+
assert len(af.claimed_tasks) == 2
|
|
85
|
+
assert "python" in af.capabilities
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TestBoard:
|
|
89
|
+
"""Tests for the Board coordination logic."""
|
|
90
|
+
|
|
91
|
+
def test_ensure_dirs(self, board: Board):
|
|
92
|
+
assert board.tasks_dir.exists()
|
|
93
|
+
assert board.agents_dir.exists()
|
|
94
|
+
|
|
95
|
+
def test_create_and_load_task(self, board: Board):
|
|
96
|
+
task = Task(title="Test task", tags=["test"])
|
|
97
|
+
path = board.create_task(task)
|
|
98
|
+
assert path.exists()
|
|
99
|
+
loaded = board.load_tasks()
|
|
100
|
+
assert len(loaded) == 1
|
|
101
|
+
assert loaded[0].title == "Test task"
|
|
102
|
+
|
|
103
|
+
def test_save_and_load_agent(self, board: Board):
|
|
104
|
+
agent = AgentFile(agent="jarvis", capabilities=["python"])
|
|
105
|
+
board.save_agent(agent)
|
|
106
|
+
loaded = board.load_agent("jarvis")
|
|
107
|
+
assert loaded is not None
|
|
108
|
+
assert loaded.agent == "jarvis"
|
|
109
|
+
assert "python" in loaded.capabilities
|
|
110
|
+
|
|
111
|
+
def test_load_nonexistent_agent(self, board: Board):
|
|
112
|
+
assert board.load_agent("nobody") is None
|
|
113
|
+
|
|
114
|
+
def test_load_all_agents(self, board: Board):
|
|
115
|
+
board.save_agent(AgentFile(agent="jarvis"))
|
|
116
|
+
board.save_agent(AgentFile(agent="opus"))
|
|
117
|
+
agents = board.load_agents()
|
|
118
|
+
assert len(agents) == 2
|
|
119
|
+
names = {a.agent for a in agents}
|
|
120
|
+
assert names == {"jarvis", "opus"}
|
|
121
|
+
|
|
122
|
+
def test_task_views_open(self, board: Board):
|
|
123
|
+
board.create_task(Task(id="t1", title="Open task"))
|
|
124
|
+
views = board.get_task_views()
|
|
125
|
+
assert len(views) == 1
|
|
126
|
+
assert views[0].status == TaskStatus.OPEN
|
|
127
|
+
assert views[0].claimed_by is None
|
|
128
|
+
|
|
129
|
+
def test_task_views_claimed(self, board: Board):
|
|
130
|
+
board.create_task(Task(id="t1", title="Claimable"))
|
|
131
|
+
board.claim_task("jarvis", "t1")
|
|
132
|
+
views = board.get_task_views()
|
|
133
|
+
assert views[0].status == TaskStatus.IN_PROGRESS
|
|
134
|
+
assert views[0].claimed_by == "jarvis"
|
|
135
|
+
|
|
136
|
+
def test_task_views_completed(self, board: Board):
|
|
137
|
+
board.create_task(Task(id="t1", title="Completable"))
|
|
138
|
+
board.claim_task("jarvis", "t1")
|
|
139
|
+
board.complete_task("jarvis", "t1")
|
|
140
|
+
views = board.get_task_views()
|
|
141
|
+
assert views[0].status == TaskStatus.DONE
|
|
142
|
+
|
|
143
|
+
def test_claim_task(self, board: Board):
|
|
144
|
+
board.create_task(Task(id="t1", title="Claim me"))
|
|
145
|
+
agent = board.claim_task("opus", "t1")
|
|
146
|
+
assert "t1" in agent.claimed_tasks
|
|
147
|
+
assert agent.current_task == "t1"
|
|
148
|
+
|
|
149
|
+
def test_claim_nonexistent_task(self, board: Board):
|
|
150
|
+
with pytest.raises(ValueError, match="not found"):
|
|
151
|
+
board.claim_task("jarvis", "nonexistent")
|
|
152
|
+
|
|
153
|
+
def test_claim_already_claimed_by_other(self, board: Board):
|
|
154
|
+
board.create_task(Task(id="t1", title="Contested"))
|
|
155
|
+
board.claim_task("jarvis", "t1")
|
|
156
|
+
with pytest.raises(ValueError, match="already"):
|
|
157
|
+
board.claim_task("opus", "t1")
|
|
158
|
+
|
|
159
|
+
def test_claim_idempotent_same_agent(self, board: Board):
|
|
160
|
+
board.create_task(Task(id="t1", title="Idempotent"))
|
|
161
|
+
board.claim_task("jarvis", "t1")
|
|
162
|
+
agent = board.claim_task("jarvis", "t1")
|
|
163
|
+
assert agent.claimed_tasks.count("t1") == 1
|
|
164
|
+
|
|
165
|
+
def test_complete_task(self, board: Board):
|
|
166
|
+
board.create_task(Task(id="t1", title="Complete me"))
|
|
167
|
+
board.claim_task("jarvis", "t1")
|
|
168
|
+
agent = board.complete_task("jarvis", "t1")
|
|
169
|
+
assert "t1" not in agent.claimed_tasks
|
|
170
|
+
assert "t1" in agent.completed_tasks
|
|
171
|
+
assert agent.current_task is None
|
|
172
|
+
|
|
173
|
+
def test_complete_advances_current(self, board: Board):
|
|
174
|
+
"""Completing current task moves to next claimed task."""
|
|
175
|
+
board.create_task(Task(id="t1", title="First"))
|
|
176
|
+
board.create_task(Task(id="t2", title="Second"))
|
|
177
|
+
board.claim_task("jarvis", "t1")
|
|
178
|
+
board.claim_task("jarvis", "t2")
|
|
179
|
+
agent = board.complete_task("jarvis", "t1")
|
|
180
|
+
assert agent.current_task == "t2"
|
|
181
|
+
|
|
182
|
+
def test_multiple_agents_independent(self, board: Board):
|
|
183
|
+
"""Two agents can work on different tasks simultaneously."""
|
|
184
|
+
board.create_task(Task(id="t1", title="Jarvis work"))
|
|
185
|
+
board.create_task(Task(id="t2", title="Opus work"))
|
|
186
|
+
board.claim_task("jarvis", "t1")
|
|
187
|
+
board.claim_task("opus", "t2")
|
|
188
|
+
views = board.get_task_views()
|
|
189
|
+
status_map = {v.task.id: (v.status, v.claimed_by) for v in views}
|
|
190
|
+
assert status_map["t1"] == (TaskStatus.IN_PROGRESS, "jarvis")
|
|
191
|
+
assert status_map["t2"] == (TaskStatus.IN_PROGRESS, "opus")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class TestBoardMd:
|
|
195
|
+
"""Tests for BOARD.md generation."""
|
|
196
|
+
|
|
197
|
+
def test_empty_board(self, board: Board):
|
|
198
|
+
md = board.generate_board_md()
|
|
199
|
+
assert "Coordination Board" in md
|
|
200
|
+
|
|
201
|
+
def test_board_with_tasks(self, board: Board):
|
|
202
|
+
board.create_task(Task(id="t1", title="Open task", tags=["test"]))
|
|
203
|
+
board.create_task(Task(id="t2", title="Done task"))
|
|
204
|
+
board.claim_task("jarvis", "t2")
|
|
205
|
+
board.complete_task("jarvis", "t2")
|
|
206
|
+
md = board.generate_board_md()
|
|
207
|
+
assert "Open task" in md
|
|
208
|
+
assert "Done task" in md
|
|
209
|
+
assert "jarvis" in md
|
|
210
|
+
|
|
211
|
+
def test_write_board_md(self, board: Board):
|
|
212
|
+
board.create_task(Task(id="t1", title="File test"))
|
|
213
|
+
path = board.write_board_md()
|
|
214
|
+
assert path.exists()
|
|
215
|
+
assert "File test" in path.read_text()
|
|
216
|
+
|
|
217
|
+
def test_board_shows_agents(self, board: Board):
|
|
218
|
+
board.save_agent(
|
|
219
|
+
AgentFile(agent="opus", notes="Building tokens")
|
|
220
|
+
)
|
|
221
|
+
md = board.generate_board_md()
|
|
222
|
+
assert "opus" in md
|
|
223
|
+
assert "Building tokens" in md
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class TestCorruptFiles:
|
|
227
|
+
"""Edge cases: malformed JSON, missing fields."""
|
|
228
|
+
|
|
229
|
+
def test_corrupt_task_file_skipped(self, board: Board):
|
|
230
|
+
(board.tasks_dir / "bad.json").write_text("not json{{{")
|
|
231
|
+
board.create_task(Task(id="good", title="Valid task"))
|
|
232
|
+
tasks = board.load_tasks()
|
|
233
|
+
assert len(tasks) == 1
|
|
234
|
+
assert tasks[0].id == "good"
|
|
235
|
+
|
|
236
|
+
def test_corrupt_agent_file_skipped(self, board: Board):
|
|
237
|
+
(board.agents_dir / "bad.json").write_text("{broken")
|
|
238
|
+
board.save_agent(AgentFile(agent="good"))
|
|
239
|
+
agents = board.load_agents()
|
|
240
|
+
assert len(agents) == 1
|
|
241
|
+
assert agents[0].agent == "good"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class TestBriefing:
|
|
245
|
+
"""Tests for the tool-agnostic briefing functions."""
|
|
246
|
+
|
|
247
|
+
def test_briefing_text_contains_protocol(self, tmp_path: Path):
|
|
248
|
+
text = get_briefing_text(tmp_path)
|
|
249
|
+
assert "SKCapstone Agent Coordination Protocol" in text
|
|
250
|
+
assert "skcapstone coord briefing" in text
|
|
251
|
+
assert "Conflict-Free Design" in text
|
|
252
|
+
|
|
253
|
+
def test_briefing_text_includes_live_tasks(self, board: Board):
|
|
254
|
+
board.create_task(Task(id="t1", title="Test task"))
|
|
255
|
+
text = get_briefing_text(board.home)
|
|
256
|
+
assert "Current Board Snapshot" in text
|
|
257
|
+
assert "t1" in text
|
|
258
|
+
assert "Test task" in text
|
|
259
|
+
|
|
260
|
+
def test_briefing_text_includes_agents(self, board: Board):
|
|
261
|
+
board.create_task(Task(id="t2", title="Another"))
|
|
262
|
+
board.save_agent(AgentFile(agent="tester", current_task="t2"))
|
|
263
|
+
text = get_briefing_text(board.home)
|
|
264
|
+
assert "tester" in text
|
|
265
|
+
|
|
266
|
+
def test_briefing_text_empty_board(self, tmp_path: Path):
|
|
267
|
+
text = get_briefing_text(tmp_path)
|
|
268
|
+
assert "Current Board Snapshot" not in text
|
|
269
|
+
|
|
270
|
+
def test_briefing_json_valid(self, tmp_path: Path):
|
|
271
|
+
raw = get_briefing_json(tmp_path)
|
|
272
|
+
data = json.loads(raw)
|
|
273
|
+
assert data["protocol_version"] == "1.0"
|
|
274
|
+
assert "commands" in data
|
|
275
|
+
assert "rules" in data
|
|
276
|
+
assert "agent_names" in data
|
|
277
|
+
|
|
278
|
+
def test_briefing_json_includes_tasks(self, board: Board):
|
|
279
|
+
board.create_task(Task(id="j1", title="JSON task", priority=TaskPriority.HIGH))
|
|
280
|
+
raw = get_briefing_json(board.home)
|
|
281
|
+
data = json.loads(raw)
|
|
282
|
+
assert len(data["tasks"]) == 1
|
|
283
|
+
assert data["tasks"][0]["id"] == "j1"
|
|
284
|
+
assert data["tasks"][0]["priority"] == "high"
|
|
285
|
+
|
|
286
|
+
def test_briefing_json_includes_agents(self, board: Board):
|
|
287
|
+
board.save_agent(AgentFile(agent="bot", state=AgentState.ACTIVE))
|
|
288
|
+
raw = get_briefing_json(board.home)
|
|
289
|
+
data = json.loads(raw)
|
|
290
|
+
assert len(data["agents"]) == 1
|
|
291
|
+
assert data["agents"][0]["name"] == "bot"
|
|
292
|
+
assert data["agents"][0]["state"] == "active"
|
|
293
|
+
|
|
294
|
+
def test_briefing_text_mentions_tool_agnostic(self, tmp_path: Path):
|
|
295
|
+
text = get_briefing_text(tmp_path)
|
|
296
|
+
assert "Cursor" in text
|
|
297
|
+
assert "Claude Code" in text
|
|
298
|
+
assert "Aider" in text
|
|
299
|
+
assert "Windsurf" in text
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Tests for component discovery engine."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from skcapstone.discovery import discover_identity, discover_memory, discover_all
|
|
9
|
+
from skcapstone.models import PillarStatus
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestIdentityDiscovery:
|
|
13
|
+
"""Tests for identity (CapAuth) discovery."""
|
|
14
|
+
|
|
15
|
+
def test_missing_when_no_identity_dir(self, tmp_agent_home: Path):
|
|
16
|
+
"""Should report MISSING when no identity exists."""
|
|
17
|
+
state = discover_identity(tmp_agent_home)
|
|
18
|
+
# Reason: capauth may or may not be installed in test env
|
|
19
|
+
assert state.status in (PillarStatus.MISSING, PillarStatus.DEGRADED)
|
|
20
|
+
|
|
21
|
+
def test_active_with_identity_manifest(self, tmp_agent_home: Path):
|
|
22
|
+
"""Should report ACTIVE when identity.json exists with data."""
|
|
23
|
+
identity_dir = tmp_agent_home / "identity"
|
|
24
|
+
identity_dir.mkdir()
|
|
25
|
+
identity_data = {
|
|
26
|
+
"name": "test-agent",
|
|
27
|
+
"email": "test@skcapstone.local",
|
|
28
|
+
"fingerprint": "ABCD1234" * 5,
|
|
29
|
+
"created_at": "2026-02-22T00:00:00+00:00",
|
|
30
|
+
}
|
|
31
|
+
(identity_dir / "identity.json").write_text(json.dumps(identity_data))
|
|
32
|
+
|
|
33
|
+
state = discover_identity(tmp_agent_home)
|
|
34
|
+
assert state.status == PillarStatus.ACTIVE
|
|
35
|
+
assert state.name == "test-agent"
|
|
36
|
+
assert state.fingerprint is not None
|
|
37
|
+
|
|
38
|
+
def test_error_with_corrupt_manifest(self, tmp_agent_home: Path):
|
|
39
|
+
"""Should report ERROR when identity.json is corrupt."""
|
|
40
|
+
identity_dir = tmp_agent_home / "identity"
|
|
41
|
+
identity_dir.mkdir()
|
|
42
|
+
(identity_dir / "identity.json").write_text("{not valid json")
|
|
43
|
+
|
|
44
|
+
state = discover_identity(tmp_agent_home)
|
|
45
|
+
assert state.status == PillarStatus.ERROR
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TestDiscoverAll:
|
|
49
|
+
"""Tests for full discovery sweep."""
|
|
50
|
+
|
|
51
|
+
def test_discover_all_returns_four_pillars(self, tmp_agent_home: Path):
|
|
52
|
+
"""discover_all should return all four pillar states."""
|
|
53
|
+
result = discover_all(tmp_agent_home)
|
|
54
|
+
assert "identity" in result
|
|
55
|
+
assert "memory" in result
|
|
56
|
+
assert "trust" in result
|
|
57
|
+
assert "security" in result
|