Data Model: Identity-Aware CLI Event Sync
Feature: 032-identity-aware-cli-event-sync Date: 2026-02-07
Entities
ProjectIdentity
Represents the unique identity of a spec-kitty project. Persisted in .kittify/config.yaml.
@dataclass
class ProjectIdentity:
"""Unique identity for a spec-kitty project."""
project_uuid: UUID | None = None
"""UUID4 identifying this project. Generated once, stable forever."""
project_slug: str | None = None
"""Human-readable slug derived from repo directory or git remote."""
node_id: str | None = None
"""Machine-stable identifier (12-char hex) from LamportClock generator."""
@property
def is_complete(self) -> bool:
"""True if all identity fields are populated."""
return all([self.project_uuid, self.project_slug, self.node_id])
def with_defaults(self) -> "ProjectIdentity":
"""Return new identity with missing fields filled in."""
return ProjectIdentity(
project_uuid=self.project_uuid or uuid4(),
project_slug=self.project_slug or derive_slug_from_repo(),
node_id=self.node_id or generate_node_id(),
)
Persistence (in .kittify/config.yaml):
project:
uuid: "550e8400-e29b-41d4-a716-446655440000"
slug: "my-project"
node_id: "a1b2c3d4e5f6"
Generation Rules:
project_uuid: UUID4, generated once per projectproject_slug: Kebab-case from directory name, or from git remoteoriginURLnode_id: Stable machine ID fromsync.clock.generate_node_id()(12-char hex, stable across restarts)- Use the same generator as LamportClock so identity node_id matches event node_id
EventEnvelope (Updated)
The event envelope includes identity metadata for attribution.
# Existing fields (from spec-kitty-events)
event_id: str # ULID
event_type: str # e.g., "WPStatusChanged"
aggregate_id: str # e.g., "WP01"
aggregate_type: str # e.g., "WorkPackage"
payload: dict # Event-specific data
timestamp: str # ISO 8601
node_id: str # From LamportClock
lamport_clock: int # Causal ordering
causation_id: str | None # Parent event
# NEW fields (this feature)
project_uuid: str # UUID4 string (required for WebSocket send)
project_slug: str | None # Human-readable slug (optional)
team_slug: str # From auth or "local" if unauthenticated
Validation Rules:
project_uuidMUST be present for WebSocket send (queue-only if missing)project_uuidMUST be valid UUID4 formatteam_slugdefaults to "local" if not authenticated
SyncRuntime
Manages background sync services. Singleton pattern.
@dataclass
class SyncRuntime:
"""Background sync runtime managing WebSocket and queue."""
background_service: BackgroundSyncService | None = None
ws_client: WebSocketClient | None = None
emitter: EventEmitter | None = None
started: bool = False
def start(self) -> None:
"""Start background services (idempotent)."""
if self.started:
return
if not auto_start_enabled():
return
# Reuse singleton background service (registers its own atexit)
self.background_service = get_sync_service()
if is_authenticated():
self.ws_client = WebSocketClient(...)
self.ws_client.connect()
if self.emitter is not None:
self.emitter.ws_client = self.ws_client
else:
logger.info("Not authenticated; events queued locally")
self.started = True
def attach_emitter(self, emitter: EventEmitter) -> None:
"""Attach emitter so WS client can be injected when available."""
self.emitter = emitter
if self.ws_client is not None:
self.emitter.ws_client = self.ws_client
def stop(self) -> None:
"""Stop background services."""
if self.background_service:
self.background_service.stop()
if self.ws_client:
self.ws_client.disconnect()
self.started = False
Lifecycle: 1. Created lazily on first get_emitter() call 2. Starts BackgroundSyncService via get_sync_service() if sync.auto_start is enabled 3. Starts WebSocketClient only if authenticated and attaches to emitter 4. Stopped on process exit (atexit handler)
Config.yaml Schema (Updated)
# Existing fields
vcs:
type: git
agents:
available: [claude, opencode, codex]
selection:
strategy: random
# NEW fields (this feature)
project:
uuid: "550e8400-e29b-41d4-a716-446655440000"
slug: "my-project"
node_id: "node-abc123"
sync:
auto_start: true # Optional, default true
Note: This is the project-level .kittify/config.yaml (not ~/.spec-kitty/config.toml).
Relationships
ProjectIdentity (1) ─── persisted in ──→ Config.yaml (1)
│
│ injected into
▼
EventEnvelope (*) ─── routed by ──→ EventEmitter (1)
│ │
│ │ bootstrapped by
│ ▼
│ SyncRuntime (1)
│ │
│ ├── starts → BackgroundSyncService (1)
│ └── attaches → WebSocketClient (1) if authenticated
│
└── always ──→ OfflineQueue (1)
State Transitions
Event Routing State Machine
┌─────────────────┐
│ Event Created │
└────────┬────────┘
│
┌────────▼────────┐
│ Has Identity? │
└────────┬────────┘
│
┌──────────────┼──────────────┐
│ NO │ │ YES
▼ │ ▼
┌─────────────────┐ │ ┌─────────────────┐
│ Log Warning │ │ │ Is Connected? │
│ Queue Only │ │ └────────┬────────┘
└─────────────────┘ │ │
│ ┌─────────┼─────────┐
│ │ NO │ │ YES
│ ▼ │ ▼
│ Queue │ Send via WS
│ Event │ + Queue backup
└─────────────┴─────────────────