vibesop

VibeSOP - Modern Python Edition.

A battle-tested, multi-platform workflow SOP for AI-assisted development.

This project is a complete rewrite of the Ruby version, leveraging:

  • Python 3.12+ type system
  • Pydantic v2 for runtime validation
  • Modern async/await patterns
  • Type-safe LLM clients
 1"""VibeSOP - Modern Python Edition.
 2
 3A battle-tested, multi-platform workflow SOP for AI-assisted development.
 4
 5This project is a complete rewrite of the Ruby version, leveraging:
 6- Python 3.12+ type system
 7- Pydantic v2 for runtime validation
 8- Modern async/await patterns
 9- Type-safe LLM clients
10"""
11
12from vibesop._version import __version__
13
14__author__ = "nehcuh"
15__license__ = "MIT"
16
17# Core public API
18from vibesop.core.models import (
19    RoutingLayer,
20    RoutingRequest,
21    RoutingResult,
22    SkillRegistry,
23    SkillRoute,
24)
25
26__all__ = [
27    "RoutingLayer",
28    "RoutingRequest",
29    "RoutingResult",
30    "SkillRegistry",
31    "SkillRoute",
32    "__version__",
33]
class RoutingLayer(enum.StrEnum):
16class RoutingLayer(StrEnum):
17    """Routing layers in priority order (Layer 0 → Layer 9)."""
18
19    EXPLICIT = "explicit"           # Layer 0
20    SCENARIO = "scenario"           # Layer 1
21    AI_TRIAGE = "ai_triage"         # Layer 2
22    KEYWORD = "keyword"             # Layer 3
23    TFIDF = "tfidf"                 # Layer 4
24    EMBEDDING = "embedding"         # Layer 5
25    LEVENSHTEIN = "levenshtein"     # Layer 6
26    CUSTOM = "custom"               # Layer 7
27    NO_MATCH = "no_match"           # Layer 8
28    FALLBACK_LLM = "fallback_llm"   # Layer 9
29
30    @property
31    def layer_number(self) -> int:
32        """Return numeric layer index for backward compatibility."""
33        mapping = {
34            RoutingLayer.EXPLICIT: 0,
35            RoutingLayer.SCENARIO: 1,
36            RoutingLayer.AI_TRIAGE: 2,
37            RoutingLayer.KEYWORD: 3,
38            RoutingLayer.TFIDF: 4,
39            RoutingLayer.EMBEDDING: 5,
40            RoutingLayer.LEVENSHTEIN: 6,
41            RoutingLayer.CUSTOM: 7,
42            RoutingLayer.NO_MATCH: 8,
43            RoutingLayer.FALLBACK_LLM: 9,
44        }
45        return mapping[self]

Routing layers in priority order (Layer 0 → Layer 9).

EXPLICIT = <RoutingLayer.EXPLICIT: 'explicit'>
SCENARIO = <RoutingLayer.SCENARIO: 'scenario'>
AI_TRIAGE = <RoutingLayer.AI_TRIAGE: 'ai_triage'>
KEYWORD = <RoutingLayer.KEYWORD: 'keyword'>
TFIDF = <RoutingLayer.TFIDF: 'tfidf'>
EMBEDDING = <RoutingLayer.EMBEDDING: 'embedding'>
LEVENSHTEIN = <RoutingLayer.LEVENSHTEIN: 'levenshtein'>
CUSTOM = <RoutingLayer.CUSTOM: 'custom'>
NO_MATCH = <RoutingLayer.NO_MATCH: 'no_match'>
FALLBACK_LLM = <RoutingLayer.FALLBACK_LLM: 'fallback_llm'>
layer_number: int
30    @property
31    def layer_number(self) -> int:
32        """Return numeric layer index for backward compatibility."""
33        mapping = {
34            RoutingLayer.EXPLICIT: 0,
35            RoutingLayer.SCENARIO: 1,
36            RoutingLayer.AI_TRIAGE: 2,
37            RoutingLayer.KEYWORD: 3,
38            RoutingLayer.TFIDF: 4,
39            RoutingLayer.EMBEDDING: 5,
40            RoutingLayer.LEVENSHTEIN: 6,
41            RoutingLayer.CUSTOM: 7,
42            RoutingLayer.NO_MATCH: 8,
43            RoutingLayer.FALLBACK_LLM: 9,
44        }
45        return mapping[self]

Return numeric layer index for backward compatibility.

class RoutingRequest(pydantic.main.BaseModel):
126class RoutingRequest(BaseModel):
127    """Request for skill routing.
128
129    Attributes:
130        query: User's natural language query
131        context: Additional context (file type, error count, etc.)
132    """
133
134    query: str = Field(..., min_length=1, description="User query")
135    context: dict[str, str | int] = Field(
136        default_factory=dict,
137        description="Routing context",
138    )

Request for skill routing.

Attributes:
  • query: User's natural language query
  • context: Additional context (file type, error count, etc.)
query: str = PydanticUndefined

User query

context: dict[str, str | int] = PydanticUndefined

Routing context

class RoutingResult(pydantic.main.BaseModel):
210class RoutingResult(BaseModel):
211    """Result of skill routing operation.
212
213    Attributes:
214        primary: Best matching skill (None if no match)
215        alternatives: List of alternative matches
216        routing_path: Which layers were consulted
217        layer_details: Per-layer diagnostic details for transparency
218        query: The original query
219        duration_ms: How long routing took
220    """
221
222    model_config = {"arbitrary_types_allowed": True}
223
224    primary: SkillRoute | None = Field(
225        default=None,
226        description="Primary skill match",
227    )
228    alternatives: list[SkillRoute] = Field(
229        default_factory=list,
230        description="Alternative skill matches",
231    )
232    routing_path: list[RoutingLayer] = Field(
233        default_factory=list,
234        description="Layers consulted during routing",
235    )
236    layer_details: list[LayerDetail] = Field(
237        default_factory=list,
238        description="Per-layer diagnostic details for transparency",
239    )
240    query: str = Field(default="", description="Original query")
241    duration_ms: float = Field(default=0.0, description="Routing duration in ms")
242
243    @property
244    def has_match(self) -> bool:
245        """Whether a match was found (excluding fallback)."""
246        return self.primary is not None and self.primary.layer != RoutingLayer.FALLBACK_LLM
247
248    def to_dict(self) -> dict[str, Any]:
249        """Convert to dictionary for serialization."""
250        return {
251            "primary": self.primary.to_dict() if self.primary else None,
252            "alternatives": [a.to_dict() for a in self.alternatives],
253            "routing_path": [layer.value for layer in self.routing_path],
254            "layer_details": [d.to_dict() for d in self.layer_details],
255            "query": self.query,
256            "duration_ms": self.duration_ms,
257            "has_match": self.has_match,
258        }

Result of skill routing operation.

Attributes:
  • primary: Best matching skill (None if no match)
  • alternatives: List of alternative matches
  • routing_path: Which layers were consulted
  • layer_details: Per-layer diagnostic details for transparency
  • query: The original query
  • duration_ms: How long routing took
primary: SkillRoute | None = None

Primary skill match

alternatives: list[SkillRoute] = PydanticUndefined

Alternative skill matches

routing_path: list[RoutingLayer] = PydanticUndefined

Layers consulted during routing

layer_details: list[vibesop.core.models.LayerDetail] = PydanticUndefined

Per-layer diagnostic details for transparency

query: str = ''

Original query

duration_ms: float = 0.0

Routing duration in ms

has_match: bool
243    @property
244    def has_match(self) -> bool:
245        """Whether a match was found (excluding fallback)."""
246        return self.primary is not None and self.primary.layer != RoutingLayer.FALLBACK_LLM

Whether a match was found (excluding fallback).

def to_dict(self) -> dict[str, typing.Any]:
248    def to_dict(self) -> dict[str, Any]:
249        """Convert to dictionary for serialization."""
250        return {
251            "primary": self.primary.to_dict() if self.primary else None,
252            "alternatives": [a.to_dict() for a in self.alternatives],
253            "routing_path": [layer.value for layer in self.routing_path],
254            "layer_details": [d.to_dict() for d in self.layer_details],
255            "query": self.query,
256            "duration_ms": self.duration_ms,
257            "has_match": self.has_match,
258        }

Convert to dictionary for serialization.

class SkillRegistry(pydantic.main.BaseModel):
580class SkillRegistry(BaseModel):
581    """Registry of all available skills.
582
583    Attributes:
584        skills: Map of skill_id to SkillDefinition
585        version: Registry version
586    """
587
588    skills: dict[str, SkillDefinition] = Field(
589        default_factory=dict,
590        description="Available skills",
591    )
592    version: str = Field(default="1.0.0", description="Registry version")

Registry of all available skills.

Attributes:
  • skills: Map of skill_id to SkillDefinition
  • version: Registry version
skills: dict[str, vibesop.core.models.SkillDefinition] = PydanticUndefined

Available skills

version: str = '1.0.0'

Registry version

class SkillRoute(pydantic.main.BaseModel):
 78class SkillRoute(BaseModel):
 79    """Result of skill routing operation.
 80
 81    Attributes:
 82        skill_id: Unique skill identifier (e.g., 'gstack/review')
 83        confidence: Routing confidence (0.0 to 1.0)
 84        layer: Which routing layer made this decision
 85        source: Skill pack source (e.g., 'builtin', 'gstack', 'external')
 86        metadata: Additional routing metadata
 87    """
 88
 89    model_config = {"arbitrary_types_allowed": True}
 90
 91    skill_id: str = Field(..., min_length=1, description="Skill identifier")
 92    confidence: float = Field(
 93        default=0.0,
 94        ge=0.0,
 95        le=1.0,
 96        description="Routing confidence score",
 97    )
 98    layer: RoutingLayer = Field(
 99        ...,
100        description="Routing layer that produced this match",
101    )
102    source: str = Field(default="builtin", description="Skill pack source")
103    description: str = Field(
104        default="",
105        description="Skill description for display in CLI",
106    )
107    metadata: dict[str, Any] = Field(
108        default_factory=dict,
109        description="Additional routing metadata",
110    )
111
112    # Note: min_length=1 on the Field already enforces non-empty skill_id
113    # No additional field_validator needed
114
115    def to_dict(self) -> dict[str, Any]:
116        """Convert to dictionary for serialization."""
117        return {
118            "skill_id": self.skill_id,
119            "confidence": self.confidence,
120            "layer": self.layer.value,
121            "source": self.source,
122            "metadata": self.metadata,
123        }

Result of skill routing operation.

Attributes:
  • skill_id: Unique skill identifier (e.g., 'gstack/review')
  • confidence: Routing confidence (0.0 to 1.0)
  • layer: Which routing layer made this decision
  • source: Skill pack source (e.g., 'builtin', 'gstack', 'external')
  • metadata: Additional routing metadata
skill_id: str = PydanticUndefined

Skill identifier

confidence: float = 0.0

Routing confidence score

layer: RoutingLayer = PydanticUndefined

Routing layer that produced this match

source: str = 'builtin'

Skill pack source

description: str = ''

Skill description for display in CLI

metadata: dict[str, typing.Any] = PydanticUndefined

Additional routing metadata

def to_dict(self) -> dict[str, typing.Any]:
115    def to_dict(self) -> dict[str, Any]:
116        """Convert to dictionary for serialization."""
117        return {
118            "skill_id": self.skill_id,
119            "confidence": self.confidence,
120            "layer": self.layer.value,
121            "source": self.source,
122            "metadata": self.metadata,
123        }

Convert to dictionary for serialization.

__version__ = '5.4.0'