Implementation Plan: Identity-Aware CLI Event Sync

Branch: 032-identity-aware-cli-event-sync | Date: 2026-02-07 | Spec: spec.md Target Branch: 2.x Input: Feature specification from /kitty-specs/032-identity-aware-cli-event-sync/spec.md

Summary

Add project identity (project_uuid, project_slug, node_id) to all CLI-emitted events and enable automatic background sync on CLI startup. This enables the SaaS to correctly attribute events to specific projects and removes the friction of manual sync startup.

Technical Approach:

  • Lazy Singleton pattern for runtime bootstrap via get_emitter()
  • Graceful Backfill for config schema (auto-generate missing identity fields)
  • Atomic writes for config.yaml (temp file + rename)
  • Read-only fallback to in-memory identity when repo is not writable

Technical Context

Language/Version: Python 3.11+ (per constitution) Primary Dependencies: typer, rich, ruamel.yaml, websockets, ulid (existing) Storage: .kittify/config.yaml (YAML frontmatter), offline queue (JSONL) Testing: pytest with 90%+ coverage, mypy --strict Target Platform: Linux, macOS, Windows 10+ (cross-platform CLI) Project Type: Single project (CLI tool) Performance Goals: CLI operations < 2 seconds, sync runtime startup < 100ms Constraints: Must work when unauthenticated (graceful degradation to queue-only) Scale/Scope: Supports 100+ work packages, thousands of events per project

Constitution Check

GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.

PrincipleStatusNotes
Python 3.11+✅ PASSAll new modules use Python 3.11+ features
mypy --strict✅ PASSType annotations required for all new code
90%+ test coverage✅ PASSUnit tests for identity, integration tests for sync
CLI < 2 seconds✅ PASSLazy singleton avoids startup overhead
Target 2.x branch✅ PASSAll changes target 2.x, no 1.x compatibility needed
spec-kitty-events integration✅ PASSEvents lib updated separately (out of scope)

No violations requiring justification.

Project Structure

Documentation (this feature)

kitty-specs/032-identity-aware-cli-event-sync/
├── plan.md              # This file
├── research.md          # Phase 0 output (minimal - clear requirements)
├── data-model.md        # Phase 1 output (entity definitions)
├── quickstart.md        # Phase 1 output (getting started guide)
├── contracts/           # Phase 1 output (event schema updates)
└── tasks.md             # Phase 2 output (NOT created by /spec-kitty.plan)

Source Code (on 2.x branch)

src/specify_cli/
├── sync/
│   ├── __init__.py           # Exports get_emitter, SyncRuntime
│   ├── project_identity.py   # NEW: ProjectIdentity class, generation, persistence
│   ├── runtime.py            # NEW: SyncRuntime bootstrap, lazy singleton
│   ├── emitter.py            # MODIFY: Inject identity, lazy-start runtime
│   ├── auth.py               # MODIFY: Add get_team_slug(), store on login
│   ├── client.py             # (existing WebSocketClient)
│   ├── queue.py              # (existing OfflineQueue)
│   ├── clock.py              # (existing LamportClock)
│   ├── config.py             # (existing SyncConfig)
│   └── background.py         # (existing BackgroundSyncService)
├── cli/
│   └── commands/
│       ├── implement.py      # MODIFY: Fix duplicate emissions (on 2.x)
│       └── accept.py         # MODIFY: Fix duplicate emissions (on 2.x)
└── ...

tests/
├── sync/
│   ├── test_project_identity.py   # NEW: Unit tests for identity generation
│   ├── test_runtime.py            # NEW: Tests for lazy singleton
│   ├── test_event_emission.py     # MODIFY: Add identity verification
│   └── test_auth.py               # MODIFY: Test get_team_slug()
└── integration/
    └── test_sync_e2e.py           # NEW: End-to-end sync tests

Structure Decision: Single project structure, all sync code in src/specify_cli/sync/.

Architectural Decisions

AD-1: Lazy Singleton for Runtime Bootstrap

Decision: Start BackgroundSyncService lazily on first get_emitter() call.

Rationale:

  • Zero overhead for non-event commands (most planning commands)
  • Centralized startup logic in one place
  • Idempotent (safe to call get_emitter() multiple times)

Implementation:

# sync/runtime.py
_runtime: SyncRuntime | None = None

def get_runtime() -> SyncRuntime:
    global _runtime
    if _runtime is None:
        _runtime = SyncRuntime()
        _runtime.start()  # Idempotent
    return _runtime

AD-2: Graceful Backfill for Config Schema

Decision: Auto-generate missing identity fields on first access.

Rationale:

  • Seamless UX for existing projects
  • No migration required
  • Handles read-only repos gracefully

Implementation:

# sync/project_identity.py
def ensure_identity(config_path: Path) -> ProjectIdentity:
    """Load or generate project identity. Atomic write if generating."""
    identity = load_identity(config_path)
    if identity.is_complete:
        return identity

    # Generate missing fields
    identity = identity.with_defaults()

    # Atomic persist (if writable)
    if is_writable(config_path):
        atomic_write(config_path, identity)
    else:
        logger.warning("Config not writable; using in-memory identity")

    return identity

AD-3: Identity Injection in Event Envelope

Decision: Inject project_uuid and project_slug in EventEmitter._emit().

Rationale:

  • Single point of injection (all emit_* methods go through _emit)
  • Validation before WebSocket send (queue-only if missing)
  • Consistent across all event types

Implementation:

# In EventEmitter._emit()
identity = get_project_identity()
event["project_uuid"] = str(identity.project_uuid)
event["project_slug"] = identity.project_slug  # Optional

# Validation: if project_uuid missing, queue only
if not event.get("project_uuid"):
    logger.warning("Event missing project_uuid; queued locally only")
    self.queue.queue_event(event)
    return event  # Don't send via WebSocket

Parallel Work Analysis

Dependency Graph

WP01: ProjectIdentity module (foundation)
  ↓
WP02: Emitter identity injection (depends on WP01)
WP03: AuthClient get_team_slug() (parallel with WP02)
  ↓
WP04: SyncRuntime lazy singleton (depends on WP02)
  ↓
WP05: Fix duplicate emissions (depends on WP04, needs 2.x branch)
  ↓
WP06: Integration tests (depends on all above)

Work Distribution

  • Sequential (foundation): WP01 must complete before WP02/WP04
  • Parallel streams: WP02 and WP03 can run simultaneously
  • Sequential (integration): WP05 and WP06 depend on prior work

Coordination Points

  • After WP01: Identity API frozen, others can depend on it
  • After WP04: Full runtime available for integration testing
  • Final: WP06 validates entire feature

Files Modified (Summary)

FileActionDescription
sync/project_identity.pyNEWProjectIdentity class, generation, atomic persistence
sync/runtime.pyNEWSyncRuntime, lazy singleton, startup logic
sync/emitter.pyMODIFYInject identity, lazy-start runtime, validation
sync/auth.pyMODIFYAdd get_team_slug(), store team_slug on login
sync/events.pyMODIFYUpdate get_emitter() for lazy singleton
cli/commands/implement.pyMODIFYFix duplicate WPStatusChanged emission
cli/commands/accept.pyMODIFYFix duplicate WPStatusChanged emission
tests/sync/test_project_identity.pyNEWUnit tests for identity
tests/sync/test_runtime.pyNEWTests for lazy singleton
tests/sync/test_event_emission.pyMODIFYAdd identity verification
tests/integration/test_sync_e2e.pyNEWEnd-to-end sync tests

Risk Mitigation

RiskMitigation
Config.yaml corruptionAtomic writes (temp + rename), validate before persist
Read-only reposFallback to in-memory identity, clear warning
Race conditionsFirst-write-wins for UUID, Lamport clock for ordering
Network failuresExisting BackgroundSyncService handles retries
Duplicate emissionsConsolidate to single emission point per command