@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.
Files changed (50) hide show
  1. package/.cursorrules +33 -0
  2. package/.github/workflows/ci.yml +23 -0
  3. package/.github/workflows/publish.yml +52 -0
  4. package/AGENTS.md +74 -0
  5. package/CLAUDE.md +56 -0
  6. package/LICENSE +674 -0
  7. package/README.md +242 -0
  8. package/SKILL.md +36 -0
  9. package/bin/cli.js +18 -0
  10. package/docs/ARCHITECTURE.md +510 -0
  11. package/docs/SECURITY_DESIGN.md +315 -0
  12. package/docs/SOVEREIGN_SINGULARITY.md +371 -0
  13. package/docs/TOKEN_SYSTEM.md +201 -0
  14. package/index.d.ts +9 -0
  15. package/index.js +32 -0
  16. package/package.json +32 -0
  17. package/pyproject.toml +84 -0
  18. package/src/skcapstone/__init__.py +13 -0
  19. package/src/skcapstone/cli.py +1441 -0
  20. package/src/skcapstone/connectors/__init__.py +6 -0
  21. package/src/skcapstone/coordination.py +590 -0
  22. package/src/skcapstone/discovery.py +275 -0
  23. package/src/skcapstone/memory_engine.py +457 -0
  24. package/src/skcapstone/models.py +223 -0
  25. package/src/skcapstone/pillars/__init__.py +8 -0
  26. package/src/skcapstone/pillars/identity.py +91 -0
  27. package/src/skcapstone/pillars/memory.py +61 -0
  28. package/src/skcapstone/pillars/security.py +83 -0
  29. package/src/skcapstone/pillars/sync.py +486 -0
  30. package/src/skcapstone/pillars/trust.py +335 -0
  31. package/src/skcapstone/runtime.py +190 -0
  32. package/src/skcapstone/skills/__init__.py +1 -0
  33. package/src/skcapstone/skills/syncthing_setup.py +297 -0
  34. package/src/skcapstone/sync/__init__.py +14 -0
  35. package/src/skcapstone/sync/backends.py +330 -0
  36. package/src/skcapstone/sync/engine.py +301 -0
  37. package/src/skcapstone/sync/models.py +97 -0
  38. package/src/skcapstone/sync/vault.py +284 -0
  39. package/src/skcapstone/tokens.py +439 -0
  40. package/tests/__init__.py +0 -0
  41. package/tests/conftest.py +42 -0
  42. package/tests/test_coordination.py +299 -0
  43. package/tests/test_discovery.py +57 -0
  44. package/tests/test_memory_engine.py +391 -0
  45. package/tests/test_models.py +63 -0
  46. package/tests/test_pillars.py +87 -0
  47. package/tests/test_runtime.py +60 -0
  48. package/tests/test_sync.py +507 -0
  49. package/tests/test_syncthing_setup.py +76 -0
  50. 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