@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,590 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SKCapstone Coordination — Multi-agent task board.
|
|
3
|
+
|
|
4
|
+
Conflict-free design: each agent writes only to its own files.
|
|
5
|
+
Syncthing propagates everything. Zero write conflicts.
|
|
6
|
+
|
|
7
|
+
Directory layout:
|
|
8
|
+
~/.skcapstone/coordination/
|
|
9
|
+
├── tasks/ # One JSON file per task (creator owns it)
|
|
10
|
+
├── agents/ # One JSON file per agent (self-managed)
|
|
11
|
+
└── BOARD.md # Human-readable overview (auto-generated)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import socket
|
|
18
|
+
import uuid
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
from enum import Enum
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
from pydantic import BaseModel, Field
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TaskPriority(str, Enum):
|
|
28
|
+
"""Task urgency levels."""
|
|
29
|
+
|
|
30
|
+
CRITICAL = "critical"
|
|
31
|
+
HIGH = "high"
|
|
32
|
+
MEDIUM = "medium"
|
|
33
|
+
LOW = "low"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TaskStatus(str, Enum):
|
|
37
|
+
"""Task lifecycle states.
|
|
38
|
+
|
|
39
|
+
Derived from agent files, not stored on the task itself.
|
|
40
|
+
A task is 'open' until an agent claims it via their agent file.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
OPEN = "open"
|
|
44
|
+
CLAIMED = "claimed"
|
|
45
|
+
IN_PROGRESS = "in_progress"
|
|
46
|
+
REVIEW = "review"
|
|
47
|
+
DONE = "done"
|
|
48
|
+
BLOCKED = "blocked"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AgentState(str, Enum):
|
|
52
|
+
"""Agent availability."""
|
|
53
|
+
|
|
54
|
+
ACTIVE = "active"
|
|
55
|
+
IDLE = "idle"
|
|
56
|
+
OFFLINE = "offline"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Task(BaseModel):
|
|
60
|
+
"""A unit of work on the coordination board.
|
|
61
|
+
|
|
62
|
+
Task files are written once by the creator and are effectively
|
|
63
|
+
immutable. Status is derived from agent claim files, not from
|
|
64
|
+
the task file itself. This eliminates write conflicts.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
id: str = Field(default_factory=lambda: uuid.uuid4().hex[:8])
|
|
68
|
+
title: str
|
|
69
|
+
description: str = ""
|
|
70
|
+
priority: TaskPriority = TaskPriority.MEDIUM
|
|
71
|
+
tags: list[str] = Field(default_factory=list)
|
|
72
|
+
created_by: str = ""
|
|
73
|
+
created_at: str = Field(
|
|
74
|
+
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
|
75
|
+
)
|
|
76
|
+
acceptance_criteria: list[str] = Field(default_factory=list)
|
|
77
|
+
dependencies: list[str] = Field(default_factory=list)
|
|
78
|
+
notes: list[str] = Field(default_factory=list)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class AgentFile(BaseModel):
|
|
82
|
+
"""An agent's self-managed status and claim record.
|
|
83
|
+
|
|
84
|
+
Each agent owns exactly one file: agents/{name}.json.
|
|
85
|
+
Only that agent writes to it. Other agents read it to
|
|
86
|
+
see claims, progress, and availability.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
agent: str
|
|
90
|
+
last_seen: str = Field(
|
|
91
|
+
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
|
92
|
+
)
|
|
93
|
+
host: str = Field(default_factory=socket.gethostname)
|
|
94
|
+
state: AgentState = AgentState.ACTIVE
|
|
95
|
+
current_task: Optional[str] = None
|
|
96
|
+
claimed_tasks: list[str] = Field(default_factory=list)
|
|
97
|
+
completed_tasks: list[str] = Field(default_factory=list)
|
|
98
|
+
capabilities: list[str] = Field(default_factory=list)
|
|
99
|
+
notes: str = ""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class TaskView(BaseModel):
|
|
103
|
+
"""A task enriched with derived status from agent claims."""
|
|
104
|
+
|
|
105
|
+
task: Task
|
|
106
|
+
status: TaskStatus = TaskStatus.OPEN
|
|
107
|
+
claimed_by: Optional[str] = None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class Board:
|
|
111
|
+
"""The coordination board — reads tasks and agent files to
|
|
112
|
+
present a unified view of work across all agents.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
home: Path to ~/.skcapstone (or test equivalent).
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(self, home: Path) -> None:
|
|
119
|
+
self.home = Path(home).expanduser()
|
|
120
|
+
self.coord_dir = self.home / "coordination"
|
|
121
|
+
self.tasks_dir = self.coord_dir / "tasks"
|
|
122
|
+
self.agents_dir = self.coord_dir / "agents"
|
|
123
|
+
|
|
124
|
+
def ensure_dirs(self) -> None:
|
|
125
|
+
"""Create coordination directories if they don't exist."""
|
|
126
|
+
self.tasks_dir.mkdir(parents=True, exist_ok=True)
|
|
127
|
+
self.agents_dir.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
|
|
129
|
+
def load_tasks(self) -> list[Task]:
|
|
130
|
+
"""Load all task files from tasks/ directory.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
list[Task]: All tasks on the board.
|
|
134
|
+
"""
|
|
135
|
+
tasks: list[Task] = []
|
|
136
|
+
if not self.tasks_dir.exists():
|
|
137
|
+
return tasks
|
|
138
|
+
for f in sorted(self.tasks_dir.glob("*.json")):
|
|
139
|
+
try:
|
|
140
|
+
data = json.loads(f.read_text())
|
|
141
|
+
tasks.append(Task.model_validate(data))
|
|
142
|
+
except (json.JSONDecodeError, Exception):
|
|
143
|
+
continue
|
|
144
|
+
return tasks
|
|
145
|
+
|
|
146
|
+
def load_agents(self) -> list[AgentFile]:
|
|
147
|
+
"""Load all agent status files from agents/ directory.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
list[AgentFile]: All agent records.
|
|
151
|
+
"""
|
|
152
|
+
agents: list[AgentFile] = []
|
|
153
|
+
if not self.agents_dir.exists():
|
|
154
|
+
return agents
|
|
155
|
+
for f in sorted(self.agents_dir.glob("*.json")):
|
|
156
|
+
try:
|
|
157
|
+
data = json.loads(f.read_text())
|
|
158
|
+
agents.append(AgentFile.model_validate(data))
|
|
159
|
+
except (json.JSONDecodeError, Exception):
|
|
160
|
+
continue
|
|
161
|
+
return agents
|
|
162
|
+
|
|
163
|
+
def load_agent(self, name: str) -> Optional[AgentFile]:
|
|
164
|
+
"""Load a specific agent's file.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
name: Agent name (matches filename).
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
AgentFile or None if not found.
|
|
171
|
+
"""
|
|
172
|
+
path = self.agents_dir / f"{name}.json"
|
|
173
|
+
if not path.exists():
|
|
174
|
+
return None
|
|
175
|
+
data = json.loads(path.read_text())
|
|
176
|
+
return AgentFile.model_validate(data)
|
|
177
|
+
|
|
178
|
+
def save_agent(self, agent: AgentFile) -> Path:
|
|
179
|
+
"""Write an agent's status file.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
agent: The agent record to save.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Path to the written file.
|
|
186
|
+
"""
|
|
187
|
+
self.ensure_dirs()
|
|
188
|
+
agent.last_seen = datetime.now(timezone.utc).isoformat()
|
|
189
|
+
path = self.agents_dir / f"{agent.agent}.json"
|
|
190
|
+
path.write_text(
|
|
191
|
+
json.dumps(agent.model_dump(), indent=2) + "\n"
|
|
192
|
+
)
|
|
193
|
+
return path
|
|
194
|
+
|
|
195
|
+
def create_task(self, task: Task) -> Path:
|
|
196
|
+
"""Write a new task file.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
task: The task to create.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Path to the written file.
|
|
203
|
+
"""
|
|
204
|
+
self.ensure_dirs()
|
|
205
|
+
slug = task.title.lower().replace(" ", "-")[:40]
|
|
206
|
+
# Reason: filename includes id + slug for human readability
|
|
207
|
+
filename = f"{task.id}-{slug}.json"
|
|
208
|
+
path = self.tasks_dir / filename
|
|
209
|
+
path.write_text(
|
|
210
|
+
json.dumps(task.model_dump(), indent=2) + "\n"
|
|
211
|
+
)
|
|
212
|
+
return path
|
|
213
|
+
|
|
214
|
+
def get_task_views(self) -> list[TaskView]:
|
|
215
|
+
"""Build enriched task views with derived status.
|
|
216
|
+
|
|
217
|
+
Cross-references tasks against all agent claim files to
|
|
218
|
+
determine each task's effective status and who claimed it.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
list[TaskView]: Tasks with derived status.
|
|
222
|
+
"""
|
|
223
|
+
tasks = self.load_tasks()
|
|
224
|
+
agents = self.load_agents()
|
|
225
|
+
|
|
226
|
+
claimed_map: dict[str, str] = {}
|
|
227
|
+
completed_set: set[str] = set()
|
|
228
|
+
in_progress_set: set[str] = set()
|
|
229
|
+
|
|
230
|
+
for ag in agents:
|
|
231
|
+
for tid in ag.completed_tasks:
|
|
232
|
+
completed_set.add(tid)
|
|
233
|
+
for tid in ag.claimed_tasks:
|
|
234
|
+
claimed_map[tid] = ag.agent
|
|
235
|
+
if ag.current_task:
|
|
236
|
+
in_progress_set.add(ag.current_task)
|
|
237
|
+
claimed_map[ag.current_task] = ag.agent
|
|
238
|
+
|
|
239
|
+
views: list[TaskView] = []
|
|
240
|
+
for t in tasks:
|
|
241
|
+
if t.id in completed_set:
|
|
242
|
+
status = TaskStatus.DONE
|
|
243
|
+
elif t.id in in_progress_set:
|
|
244
|
+
status = TaskStatus.IN_PROGRESS
|
|
245
|
+
elif t.id in claimed_map:
|
|
246
|
+
status = TaskStatus.CLAIMED
|
|
247
|
+
else:
|
|
248
|
+
status = TaskStatus.OPEN
|
|
249
|
+
views.append(
|
|
250
|
+
TaskView(
|
|
251
|
+
task=t,
|
|
252
|
+
status=status,
|
|
253
|
+
claimed_by=claimed_map.get(t.id),
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
return views
|
|
257
|
+
|
|
258
|
+
def claim_task(self, agent_name: str, task_id: str) -> AgentFile:
|
|
259
|
+
"""Have an agent claim a task.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
agent_name: The claiming agent's name.
|
|
263
|
+
task_id: The task ID to claim.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Updated AgentFile.
|
|
267
|
+
|
|
268
|
+
Raises:
|
|
269
|
+
ValueError: If task doesn't exist or is already claimed.
|
|
270
|
+
"""
|
|
271
|
+
views = self.get_task_views()
|
|
272
|
+
target = None
|
|
273
|
+
for v in views:
|
|
274
|
+
if v.task.id == task_id:
|
|
275
|
+
target = v
|
|
276
|
+
break
|
|
277
|
+
|
|
278
|
+
if target is None:
|
|
279
|
+
raise ValueError(f"Task {task_id} not found")
|
|
280
|
+
if target.status in (TaskStatus.DONE, TaskStatus.CLAIMED, TaskStatus.IN_PROGRESS):
|
|
281
|
+
if target.claimed_by != agent_name:
|
|
282
|
+
raise ValueError(
|
|
283
|
+
f"Task {task_id} already {target.status.value} by {target.claimed_by}"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
agent = self.load_agent(agent_name) or AgentFile(agent=agent_name)
|
|
287
|
+
if task_id not in agent.claimed_tasks:
|
|
288
|
+
agent.claimed_tasks.append(task_id)
|
|
289
|
+
agent.current_task = task_id
|
|
290
|
+
agent.state = AgentState.ACTIVE
|
|
291
|
+
self.save_agent(agent)
|
|
292
|
+
return agent
|
|
293
|
+
|
|
294
|
+
def complete_task(self, agent_name: str, task_id: str) -> AgentFile:
|
|
295
|
+
"""Mark a task as completed by an agent.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
agent_name: The completing agent's name.
|
|
299
|
+
task_id: The task ID completed.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Updated AgentFile.
|
|
303
|
+
"""
|
|
304
|
+
agent = self.load_agent(agent_name) or AgentFile(agent=agent_name)
|
|
305
|
+
if task_id in agent.claimed_tasks:
|
|
306
|
+
agent.claimed_tasks.remove(task_id)
|
|
307
|
+
if task_id not in agent.completed_tasks:
|
|
308
|
+
agent.completed_tasks.append(task_id)
|
|
309
|
+
if agent.current_task == task_id:
|
|
310
|
+
agent.current_task = agent.claimed_tasks[0] if agent.claimed_tasks else None
|
|
311
|
+
self.save_agent(agent)
|
|
312
|
+
return agent
|
|
313
|
+
|
|
314
|
+
def generate_board_md(self) -> str:
|
|
315
|
+
"""Generate a human-readable BOARD.md from current state.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
Markdown string for the board overview.
|
|
319
|
+
"""
|
|
320
|
+
views = self.get_task_views()
|
|
321
|
+
agents = self.load_agents()
|
|
322
|
+
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
|
323
|
+
|
|
324
|
+
lines = [
|
|
325
|
+
"# SKCapstone Coordination Board",
|
|
326
|
+
f"*Auto-generated {now} — do not edit manually*",
|
|
327
|
+
"",
|
|
328
|
+
]
|
|
329
|
+
|
|
330
|
+
for section_status, header in [
|
|
331
|
+
(TaskStatus.IN_PROGRESS, "In Progress"),
|
|
332
|
+
(TaskStatus.CLAIMED, "Claimed"),
|
|
333
|
+
(TaskStatus.OPEN, "Open"),
|
|
334
|
+
(TaskStatus.BLOCKED, "Blocked"),
|
|
335
|
+
(TaskStatus.DONE, "Done"),
|
|
336
|
+
]:
|
|
337
|
+
section_tasks = [v for v in views if v.status == section_status]
|
|
338
|
+
if not section_tasks:
|
|
339
|
+
continue
|
|
340
|
+
lines.append(f"## {header} ({len(section_tasks)})")
|
|
341
|
+
lines.append("")
|
|
342
|
+
for v in section_tasks:
|
|
343
|
+
t = v.task
|
|
344
|
+
assignee = f" @{v.claimed_by}" if v.claimed_by else ""
|
|
345
|
+
priority_icon = {
|
|
346
|
+
"critical": "!!!", "high": "!!", "medium": "!", "low": ""
|
|
347
|
+
}.get(t.priority.value, "")
|
|
348
|
+
tags_str = " ".join(f"`{tag}`" for tag in t.tags)
|
|
349
|
+
lines.append(
|
|
350
|
+
f"- **[{t.id}]** {t.title}{assignee} "
|
|
351
|
+
f"{priority_icon} {tags_str}"
|
|
352
|
+
)
|
|
353
|
+
if t.description:
|
|
354
|
+
lines.append(f" > {t.description[:120]}")
|
|
355
|
+
lines.append("")
|
|
356
|
+
|
|
357
|
+
if agents:
|
|
358
|
+
lines.append("## Agents")
|
|
359
|
+
lines.append("")
|
|
360
|
+
for ag in agents:
|
|
361
|
+
state_icon = {"active": "🟢", "idle": "🟡", "offline": "⚫"}.get(
|
|
362
|
+
ag.state.value, "?"
|
|
363
|
+
)
|
|
364
|
+
current = f" working on `{ag.current_task}`" if ag.current_task else ""
|
|
365
|
+
lines.append(
|
|
366
|
+
f"- {state_icon} **{ag.agent}** ({ag.host}){current}"
|
|
367
|
+
)
|
|
368
|
+
if ag.notes:
|
|
369
|
+
lines.append(f" > {ag.notes[:120]}")
|
|
370
|
+
lines.append("")
|
|
371
|
+
|
|
372
|
+
return "\n".join(lines)
|
|
373
|
+
|
|
374
|
+
def write_board_md(self) -> Path:
|
|
375
|
+
"""Write BOARD.md to the coordination directory.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
Path to the written file.
|
|
379
|
+
"""
|
|
380
|
+
self.ensure_dirs()
|
|
381
|
+
content = self.generate_board_md()
|
|
382
|
+
path = self.coord_dir / "BOARD.md"
|
|
383
|
+
path.write_text(content)
|
|
384
|
+
return path
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
_BRIEFING_PROTOCOL = """\
|
|
388
|
+
# SKCapstone Agent Coordination Protocol
|
|
389
|
+
|
|
390
|
+
You are an AI agent participating in a multi-agent coordination system.
|
|
391
|
+
This protocol works with ANY tool: Cursor, Claude Code, Aider, Windsurf,
|
|
392
|
+
Cline, a plain terminal, or anything that can run shell commands.
|
|
393
|
+
|
|
394
|
+
## Quick Start
|
|
395
|
+
|
|
396
|
+
1. Check what's available: skcapstone coord status
|
|
397
|
+
2. Claim a task: skcapstone coord claim <id> --agent <you>
|
|
398
|
+
3. Do the work
|
|
399
|
+
4. Mark complete: skcapstone coord complete <id> --agent <you>
|
|
400
|
+
5. Create new tasks: skcapstone coord create --title "..." --by <you>
|
|
401
|
+
6. Update the board: skcapstone coord board
|
|
402
|
+
7. Show this protocol: skcapstone coord briefing
|
|
403
|
+
8. Machine-readable: skcapstone coord briefing --format json
|
|
404
|
+
|
|
405
|
+
## Directory Layout
|
|
406
|
+
|
|
407
|
+
All data lives at ~/.skcapstone/coordination/ and syncs via Syncthing.
|
|
408
|
+
|
|
409
|
+
~/.skcapstone/coordination/
|
|
410
|
+
├── tasks/ # One JSON per task (creator writes once, then immutable)
|
|
411
|
+
├── agents/ # One JSON per agent (only that agent writes to its own)
|
|
412
|
+
└── BOARD.md # Human-readable overview (any agent can regenerate)
|
|
413
|
+
|
|
414
|
+
## Conflict-Free Design
|
|
415
|
+
|
|
416
|
+
- Each agent ONLY writes to its own file: agents/<your_name>.json
|
|
417
|
+
- Task files are write-once by the creator, then immutable
|
|
418
|
+
- BOARD.md can be regenerated by anyone from the source JSON files
|
|
419
|
+
- Syncthing propagates changes — no SSH, no APIs, no manual relay
|
|
420
|
+
|
|
421
|
+
## Task JSON Schema
|
|
422
|
+
|
|
423
|
+
{
|
|
424
|
+
"id": "8-char hex",
|
|
425
|
+
"title": "string",
|
|
426
|
+
"description": "string",
|
|
427
|
+
"priority": "critical|high|medium|low",
|
|
428
|
+
"tags": ["string"],
|
|
429
|
+
"created_by": "agent_name",
|
|
430
|
+
"created_at": "ISO-8601",
|
|
431
|
+
"acceptance_criteria": ["string"],
|
|
432
|
+
"dependencies": ["task_id"],
|
|
433
|
+
"notes": ["string"]
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
## Agent JSON Schema
|
|
437
|
+
|
|
438
|
+
{
|
|
439
|
+
"agent": "your_name",
|
|
440
|
+
"last_seen": "ISO-8601",
|
|
441
|
+
"host": "hostname",
|
|
442
|
+
"state": "active|idle|offline",
|
|
443
|
+
"current_task": "task_id or null",
|
|
444
|
+
"claimed_tasks": ["task_id"],
|
|
445
|
+
"completed_tasks": ["task_id"],
|
|
446
|
+
"capabilities": ["string"],
|
|
447
|
+
"notes": "freeform text"
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
## Agent Names
|
|
451
|
+
|
|
452
|
+
- jarvis — CapAuth, vault sync, crypto, testing
|
|
453
|
+
- opus — Runtime, tokens, documentation, architecture
|
|
454
|
+
- lumina — FEB, memory, trust, emotional intelligence
|
|
455
|
+
- human — When the human creates tasks directly
|
|
456
|
+
|
|
457
|
+
## Rules
|
|
458
|
+
|
|
459
|
+
1. Read before you write — always check the board first
|
|
460
|
+
2. Own your file — only write to agents/<your_name>.json
|
|
461
|
+
3. Tasks are immutable — don't edit task files after creation
|
|
462
|
+
4. Claim before working — so others don't duplicate effort
|
|
463
|
+
5. Complete when done — move tasks to completed_tasks promptly
|
|
464
|
+
6. Create discovered work — if you find something needed, add a task
|
|
465
|
+
7. Update BOARD.md — regenerate periodically for human visibility
|
|
466
|
+
|
|
467
|
+
## Programmatic Access (Python)
|
|
468
|
+
|
|
469
|
+
from skcapstone.coordination import Board
|
|
470
|
+
board = Board(Path("~/.skcapstone").expanduser())
|
|
471
|
+
tasks = board.get_task_views() # All tasks with status
|
|
472
|
+
board.claim_task("my_name", "abc1") # Claim a task
|
|
473
|
+
board.complete_task("my_name", "abc1") # Complete it
|
|
474
|
+
|
|
475
|
+
## Integration
|
|
476
|
+
|
|
477
|
+
The ~/.skcapstone/ directory is synced by Syncthing across all devices.
|
|
478
|
+
When you update your agent file or create a task, it propagates to:
|
|
479
|
+
- Other AI sessions on the same machine
|
|
480
|
+
- Other machines in the Syncthing mesh
|
|
481
|
+
- The Docker Swarm cluster (sksync.skstack01.douno.it)
|
|
482
|
+
"""
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def get_briefing_text(home: Path) -> str:
|
|
486
|
+
"""Return the full coordination protocol as plain text.
|
|
487
|
+
|
|
488
|
+
Appends a live snapshot of current tasks and agents if the
|
|
489
|
+
coordination directory exists.
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
home: Path to ~/.skcapstone
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
Protocol text with optional live status appended.
|
|
496
|
+
"""
|
|
497
|
+
text = _BRIEFING_PROTOCOL
|
|
498
|
+
|
|
499
|
+
board = Board(home)
|
|
500
|
+
tasks = board.load_tasks()
|
|
501
|
+
agents = board.load_agents()
|
|
502
|
+
|
|
503
|
+
if tasks or agents:
|
|
504
|
+
text += "\n## Current Board Snapshot\n\n"
|
|
505
|
+
views = board.get_task_views()
|
|
506
|
+
for v in views:
|
|
507
|
+
status_icon = {
|
|
508
|
+
"open": "[ ]",
|
|
509
|
+
"claimed": "[~]",
|
|
510
|
+
"done": "[x]",
|
|
511
|
+
}.get(v.status.value, "[?]")
|
|
512
|
+
text += f" {status_icon} [{v.task.id}] {v.task.title}"
|
|
513
|
+
if v.claimed_by:
|
|
514
|
+
text += f" (by {v.claimed_by})"
|
|
515
|
+
text += "\n"
|
|
516
|
+
|
|
517
|
+
if agents:
|
|
518
|
+
text += "\n### Active Agents\n\n"
|
|
519
|
+
for ag in agents:
|
|
520
|
+
text += f" - {ag.agent} ({ag.state.value})"
|
|
521
|
+
if ag.current_task:
|
|
522
|
+
text += f" -> {ag.current_task}"
|
|
523
|
+
text += "\n"
|
|
524
|
+
|
|
525
|
+
return text
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def get_briefing_json(home: Path) -> str:
|
|
529
|
+
"""Return the coordination protocol and live state as JSON.
|
|
530
|
+
|
|
531
|
+
Useful for machine consumption by agents that prefer structured data.
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
home: Path to ~/.skcapstone
|
|
535
|
+
|
|
536
|
+
Returns:
|
|
537
|
+
JSON string with protocol info and current board state.
|
|
538
|
+
"""
|
|
539
|
+
board = Board(home)
|
|
540
|
+
views = board.get_task_views()
|
|
541
|
+
agents = board.load_agents()
|
|
542
|
+
|
|
543
|
+
payload = {
|
|
544
|
+
"protocol_version": "1.0",
|
|
545
|
+
"coordination_dir": str(board.coord_dir),
|
|
546
|
+
"commands": {
|
|
547
|
+
"status": "skcapstone coord status",
|
|
548
|
+
"create": 'skcapstone coord create --title "..." --by <agent>',
|
|
549
|
+
"claim": "skcapstone coord claim <id> --agent <name>",
|
|
550
|
+
"complete": "skcapstone coord complete <id> --agent <name>",
|
|
551
|
+
"board": "skcapstone coord board",
|
|
552
|
+
"briefing": "skcapstone coord briefing",
|
|
553
|
+
},
|
|
554
|
+
"rules": [
|
|
555
|
+
"Read before you write",
|
|
556
|
+
"Only write to agents/<your_name>.json",
|
|
557
|
+
"Tasks are immutable after creation",
|
|
558
|
+
"Claim before working",
|
|
559
|
+
"Complete when done",
|
|
560
|
+
"Create discovered work as new tasks",
|
|
561
|
+
],
|
|
562
|
+
"agent_names": {
|
|
563
|
+
"jarvis": "CapAuth, vault sync, crypto, testing",
|
|
564
|
+
"opus": "Runtime, tokens, docs, architecture",
|
|
565
|
+
"lumina": "FEB, memory, trust, emotional intelligence",
|
|
566
|
+
"human": "Human-created tasks",
|
|
567
|
+
},
|
|
568
|
+
"tasks": [
|
|
569
|
+
{
|
|
570
|
+
"id": v.task.id,
|
|
571
|
+
"title": v.task.title,
|
|
572
|
+
"priority": v.task.priority.value,
|
|
573
|
+
"status": v.status.value,
|
|
574
|
+
"claimed_by": v.claimed_by,
|
|
575
|
+
"tags": v.task.tags,
|
|
576
|
+
}
|
|
577
|
+
for v in views
|
|
578
|
+
],
|
|
579
|
+
"agents": [
|
|
580
|
+
{
|
|
581
|
+
"name": ag.agent,
|
|
582
|
+
"state": ag.state.value,
|
|
583
|
+
"current_task": ag.current_task,
|
|
584
|
+
"claimed": ag.claimed_tasks,
|
|
585
|
+
"completed": ag.completed_tasks,
|
|
586
|
+
}
|
|
587
|
+
for ag in agents
|
|
588
|
+
],
|
|
589
|
+
}
|
|
590
|
+
return json.dumps(payload, indent=2)
|