gobby 0.2.5__py3-none-any.whl → 0.2.7__py3-none-any.whl
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.
- gobby/__init__.py +1 -1
- gobby/adapters/__init__.py +2 -1
- gobby/adapters/claude_code.py +13 -4
- gobby/adapters/codex_impl/__init__.py +28 -0
- gobby/adapters/codex_impl/adapter.py +722 -0
- gobby/adapters/codex_impl/client.py +679 -0
- gobby/adapters/codex_impl/protocol.py +20 -0
- gobby/adapters/codex_impl/types.py +68 -0
- gobby/agents/definitions.py +11 -1
- gobby/agents/isolation.py +395 -0
- gobby/agents/runner.py +8 -0
- gobby/agents/sandbox.py +261 -0
- gobby/agents/spawn.py +42 -287
- gobby/agents/spawn_executor.py +385 -0
- gobby/agents/spawners/__init__.py +24 -0
- gobby/agents/spawners/command_builder.py +189 -0
- gobby/agents/spawners/embedded.py +21 -2
- gobby/agents/spawners/headless.py +21 -2
- gobby/agents/spawners/prompt_manager.py +125 -0
- gobby/cli/__init__.py +6 -0
- gobby/cli/clones.py +419 -0
- gobby/cli/conductor.py +266 -0
- gobby/cli/install.py +4 -4
- gobby/cli/installers/antigravity.py +3 -9
- gobby/cli/installers/claude.py +15 -9
- gobby/cli/installers/codex.py +2 -8
- gobby/cli/installers/gemini.py +8 -8
- gobby/cli/installers/shared.py +175 -13
- gobby/cli/sessions.py +1 -1
- gobby/cli/skills.py +858 -0
- gobby/cli/tasks/ai.py +0 -440
- gobby/cli/tasks/crud.py +44 -6
- gobby/cli/tasks/main.py +0 -4
- gobby/cli/tui.py +2 -2
- gobby/cli/utils.py +12 -5
- gobby/clones/__init__.py +13 -0
- gobby/clones/git.py +547 -0
- gobby/conductor/__init__.py +16 -0
- gobby/conductor/alerts.py +135 -0
- gobby/conductor/loop.py +164 -0
- gobby/conductor/monitors/__init__.py +11 -0
- gobby/conductor/monitors/agents.py +116 -0
- gobby/conductor/monitors/tasks.py +155 -0
- gobby/conductor/pricing.py +234 -0
- gobby/conductor/token_tracker.py +160 -0
- gobby/config/__init__.py +12 -97
- gobby/config/app.py +69 -91
- gobby/config/extensions.py +2 -2
- gobby/config/features.py +7 -130
- gobby/config/search.py +110 -0
- gobby/config/servers.py +1 -1
- gobby/config/skills.py +43 -0
- gobby/config/tasks.py +9 -41
- gobby/hooks/__init__.py +0 -13
- gobby/hooks/event_handlers.py +188 -2
- gobby/hooks/hook_manager.py +50 -4
- gobby/hooks/plugins.py +1 -1
- gobby/hooks/skill_manager.py +130 -0
- gobby/hooks/webhooks.py +1 -1
- gobby/install/claude/hooks/hook_dispatcher.py +4 -4
- gobby/install/codex/hooks/hook_dispatcher.py +1 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
- gobby/llm/claude.py +22 -34
- gobby/llm/claude_executor.py +46 -256
- gobby/llm/codex_executor.py +59 -291
- gobby/llm/executor.py +21 -0
- gobby/llm/gemini.py +134 -110
- gobby/llm/litellm_executor.py +143 -6
- gobby/llm/resolver.py +98 -35
- gobby/mcp_proxy/importer.py +62 -4
- gobby/mcp_proxy/instructions.py +56 -0
- gobby/mcp_proxy/models.py +15 -0
- gobby/mcp_proxy/registries.py +68 -8
- gobby/mcp_proxy/server.py +33 -3
- gobby/mcp_proxy/services/recommendation.py +43 -11
- gobby/mcp_proxy/services/tool_proxy.py +81 -1
- gobby/mcp_proxy/stdio.py +2 -1
- gobby/mcp_proxy/tools/__init__.py +0 -2
- gobby/mcp_proxy/tools/agent_messaging.py +317 -0
- gobby/mcp_proxy/tools/agents.py +31 -731
- gobby/mcp_proxy/tools/clones.py +518 -0
- gobby/mcp_proxy/tools/memory.py +3 -26
- gobby/mcp_proxy/tools/metrics.py +65 -1
- gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
- gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
- gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
- gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
- gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
- gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
- gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
- gobby/mcp_proxy/tools/skills/__init__.py +616 -0
- gobby/mcp_proxy/tools/spawn_agent.py +417 -0
- gobby/mcp_proxy/tools/task_orchestration.py +7 -0
- gobby/mcp_proxy/tools/task_readiness.py +14 -0
- gobby/mcp_proxy/tools/task_sync.py +1 -1
- gobby/mcp_proxy/tools/tasks/_context.py +0 -20
- gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
- gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +110 -45
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
- gobby/mcp_proxy/tools/workflows.py +1 -1
- gobby/mcp_proxy/tools/worktrees.py +0 -338
- gobby/memory/backends/__init__.py +6 -1
- gobby/memory/backends/mem0.py +6 -1
- gobby/memory/extractor.py +477 -0
- gobby/memory/ingestion/__init__.py +5 -0
- gobby/memory/ingestion/multimodal.py +221 -0
- gobby/memory/manager.py +73 -285
- gobby/memory/search/__init__.py +10 -0
- gobby/memory/search/coordinator.py +248 -0
- gobby/memory/services/__init__.py +5 -0
- gobby/memory/services/crossref.py +142 -0
- gobby/prompts/loader.py +5 -2
- gobby/runner.py +37 -16
- gobby/search/__init__.py +48 -6
- gobby/search/backends/__init__.py +159 -0
- gobby/search/backends/embedding.py +225 -0
- gobby/search/embeddings.py +238 -0
- gobby/search/models.py +148 -0
- gobby/search/unified.py +496 -0
- gobby/servers/http.py +24 -12
- gobby/servers/routes/admin.py +294 -0
- gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
- gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
- gobby/servers/routes/mcp/endpoints/execution.py +568 -0
- gobby/servers/routes/mcp/endpoints/registry.py +378 -0
- gobby/servers/routes/mcp/endpoints/server.py +304 -0
- gobby/servers/routes/mcp/hooks.py +1 -1
- gobby/servers/routes/mcp/tools.py +48 -1317
- gobby/servers/websocket.py +2 -2
- gobby/sessions/analyzer.py +2 -0
- gobby/sessions/lifecycle.py +1 -1
- gobby/sessions/processor.py +10 -0
- gobby/sessions/transcripts/base.py +2 -0
- gobby/sessions/transcripts/claude.py +79 -10
- gobby/skills/__init__.py +91 -0
- gobby/skills/loader.py +685 -0
- gobby/skills/manager.py +384 -0
- gobby/skills/parser.py +286 -0
- gobby/skills/search.py +463 -0
- gobby/skills/sync.py +119 -0
- gobby/skills/updater.py +385 -0
- gobby/skills/validator.py +368 -0
- gobby/storage/clones.py +378 -0
- gobby/storage/database.py +1 -1
- gobby/storage/memories.py +43 -13
- gobby/storage/migrations.py +162 -201
- gobby/storage/sessions.py +116 -7
- gobby/storage/skills.py +782 -0
- gobby/storage/tasks/_crud.py +4 -4
- gobby/storage/tasks/_lifecycle.py +57 -7
- gobby/storage/tasks/_manager.py +14 -5
- gobby/storage/tasks/_models.py +8 -3
- gobby/sync/memories.py +40 -5
- gobby/sync/tasks.py +83 -6
- gobby/tasks/__init__.py +1 -2
- gobby/tasks/external_validator.py +1 -1
- gobby/tasks/validation.py +46 -35
- gobby/tools/summarizer.py +91 -10
- gobby/tui/api_client.py +4 -7
- gobby/tui/app.py +5 -3
- gobby/tui/screens/orchestrator.py +1 -2
- gobby/tui/screens/tasks.py +2 -4
- gobby/tui/ws_client.py +1 -1
- gobby/utils/daemon_client.py +2 -2
- gobby/utils/project_context.py +2 -3
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +221 -1135
- gobby/workflows/artifact_actions.py +31 -0
- gobby/workflows/autonomous_actions.py +11 -0
- gobby/workflows/context_actions.py +93 -1
- gobby/workflows/detection_helpers.py +115 -31
- gobby/workflows/enforcement/__init__.py +47 -0
- gobby/workflows/enforcement/blocking.py +269 -0
- gobby/workflows/enforcement/commit_policy.py +283 -0
- gobby/workflows/enforcement/handlers.py +269 -0
- gobby/workflows/{task_enforcement_actions.py → enforcement/task_policy.py} +29 -388
- gobby/workflows/engine.py +13 -2
- gobby/workflows/git_utils.py +106 -0
- gobby/workflows/lifecycle_evaluator.py +29 -1
- gobby/workflows/llm_actions.py +30 -0
- gobby/workflows/loader.py +19 -6
- gobby/workflows/mcp_actions.py +20 -1
- gobby/workflows/memory_actions.py +154 -0
- gobby/workflows/safe_evaluator.py +183 -0
- gobby/workflows/session_actions.py +44 -0
- gobby/workflows/state_actions.py +60 -1
- gobby/workflows/stop_signal_actions.py +55 -0
- gobby/workflows/summary_actions.py +111 -1
- gobby/workflows/task_sync_actions.py +347 -0
- gobby/workflows/todo_actions.py +34 -1
- gobby/workflows/webhook_actions.py +185 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/METADATA +87 -21
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/RECORD +201 -172
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
- gobby/adapters/codex.py +0 -1292
- gobby/install/claude/commands/gobby/bug.md +0 -51
- gobby/install/claude/commands/gobby/chore.md +0 -51
- gobby/install/claude/commands/gobby/epic.md +0 -52
- gobby/install/claude/commands/gobby/eval.md +0 -235
- gobby/install/claude/commands/gobby/feat.md +0 -49
- gobby/install/claude/commands/gobby/nit.md +0 -52
- gobby/install/claude/commands/gobby/ref.md +0 -52
- gobby/install/codex/prompts/forget.md +0 -7
- gobby/install/codex/prompts/memories.md +0 -7
- gobby/install/codex/prompts/recall.md +0 -7
- gobby/install/codex/prompts/remember.md +0 -13
- gobby/llm/gemini_executor.py +0 -339
- gobby/mcp_proxy/tools/session_messages.py +0 -1056
- gobby/mcp_proxy/tools/task_expansion.py +0 -591
- gobby/prompts/defaults/expansion/system.md +0 -119
- gobby/prompts/defaults/expansion/user.md +0 -48
- gobby/prompts/defaults/external_validation/agent.md +0 -72
- gobby/prompts/defaults/external_validation/external.md +0 -63
- gobby/prompts/defaults/external_validation/spawn.md +0 -83
- gobby/prompts/defaults/external_validation/system.md +0 -6
- gobby/prompts/defaults/features/import_mcp.md +0 -22
- gobby/prompts/defaults/features/import_mcp_github.md +0 -17
- gobby/prompts/defaults/features/import_mcp_search.md +0 -16
- gobby/prompts/defaults/features/recommend_tools.md +0 -32
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
- gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
- gobby/prompts/defaults/features/server_description.md +0 -20
- gobby/prompts/defaults/features/server_description_system.md +0 -6
- gobby/prompts/defaults/features/task_description.md +0 -31
- gobby/prompts/defaults/features/task_description_system.md +0 -6
- gobby/prompts/defaults/features/tool_summary.md +0 -17
- gobby/prompts/defaults/features/tool_summary_system.md +0 -6
- gobby/prompts/defaults/research/step.md +0 -58
- gobby/prompts/defaults/validation/criteria.md +0 -47
- gobby/prompts/defaults/validation/validate.md +0 -38
- gobby/storage/migrations_legacy.py +0 -1359
- gobby/tasks/context.py +0 -747
- gobby/tasks/criteria.py +0 -342
- gobby/tasks/expansion.py +0 -626
- gobby/tasks/prompts/expand.py +0 -327
- gobby/tasks/research.py +0 -421
- gobby/tasks/tdd.py +0 -352
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""Skill validation per Agent Skills specification.
|
|
2
|
+
|
|
3
|
+
This module provides validation functions for skill fields following
|
|
4
|
+
the Agent Skills spec (agentskills.io) constraints:
|
|
5
|
+
|
|
6
|
+
- name: max 64 chars, lowercase + hyphens only, no leading/trailing/consecutive hyphens
|
|
7
|
+
- description: max 1024 chars, non-empty
|
|
8
|
+
- compatibility: max 500 chars (optional)
|
|
9
|
+
- tags: list of strings
|
|
10
|
+
- version: semver pattern
|
|
11
|
+
- category: lowercase alphanumeric + hyphens
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
# Constants for validation limits
|
|
19
|
+
MAX_NAME_LENGTH = 64
|
|
20
|
+
MAX_DESCRIPTION_LENGTH = 1024
|
|
21
|
+
MAX_COMPATIBILITY_LENGTH = 500
|
|
22
|
+
MAX_TAG_LENGTH = 64
|
|
23
|
+
|
|
24
|
+
# Regex patterns
|
|
25
|
+
NAME_PATTERN = re.compile(r"^[a-z][a-z0-9]*(-[a-z0-9]+)*$")
|
|
26
|
+
# SemVer 2.0.0 compliant pattern: no leading zeros in numeric identifiers
|
|
27
|
+
# Prerelease and build metadata allow alphanumeric, hyphens, and dots
|
|
28
|
+
_SEMVER_NUM = r"(?:0|[1-9]\d*)" # 0 or non-zero-prefixed number
|
|
29
|
+
SEMVER_PATTERN = re.compile(
|
|
30
|
+
rf"^{_SEMVER_NUM}\.{_SEMVER_NUM}\.{_SEMVER_NUM}(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$"
|
|
31
|
+
)
|
|
32
|
+
CATEGORY_PATTERN = re.compile(r"^[a-z][a-z0-9]*(-[a-z0-9]+)*$")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ValidationResult:
|
|
37
|
+
"""Result of a validation check.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
valid: Whether the validation passed
|
|
41
|
+
errors: List of error messages if validation failed
|
|
42
|
+
warnings: List of warning messages (non-fatal issues)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
valid: bool = True
|
|
46
|
+
errors: list[str] = field(default_factory=list)
|
|
47
|
+
warnings: list[str] = field(default_factory=list)
|
|
48
|
+
|
|
49
|
+
def add_error(self, message: str) -> None:
|
|
50
|
+
"""Add an error and mark result as invalid."""
|
|
51
|
+
self.errors.append(message)
|
|
52
|
+
self.valid = False
|
|
53
|
+
|
|
54
|
+
def add_warning(self, message: str) -> None:
|
|
55
|
+
"""Add a warning (does not affect validity)."""
|
|
56
|
+
self.warnings.append(message)
|
|
57
|
+
|
|
58
|
+
def merge(self, other: "ValidationResult") -> None:
|
|
59
|
+
"""Merge another result into this one."""
|
|
60
|
+
if not other.valid:
|
|
61
|
+
self.valid = False
|
|
62
|
+
self.errors.extend(other.errors)
|
|
63
|
+
self.warnings.extend(other.warnings)
|
|
64
|
+
|
|
65
|
+
def to_dict(self) -> dict[str, Any]:
|
|
66
|
+
"""Convert to dictionary representation."""
|
|
67
|
+
return {
|
|
68
|
+
"valid": self.valid,
|
|
69
|
+
"errors": self.errors,
|
|
70
|
+
"warnings": self.warnings,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def validate_skill_name(name: str | None) -> ValidationResult:
|
|
75
|
+
"""Validate a skill name per Agent Skills spec.
|
|
76
|
+
|
|
77
|
+
Requirements:
|
|
78
|
+
- Required (non-empty)
|
|
79
|
+
- Max 64 characters
|
|
80
|
+
- Lowercase letters, numbers, and hyphens only
|
|
81
|
+
- Must start with a letter
|
|
82
|
+
- No leading, trailing, or consecutive hyphens
|
|
83
|
+
- No uppercase letters
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
name: The skill name to validate
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
ValidationResult with any errors
|
|
90
|
+
"""
|
|
91
|
+
result = ValidationResult()
|
|
92
|
+
|
|
93
|
+
if name is None or name == "":
|
|
94
|
+
result.add_error("Skill name is required")
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
# Check for uppercase letters
|
|
98
|
+
if name != name.lower():
|
|
99
|
+
result.add_error("Skill name must be lowercase")
|
|
100
|
+
|
|
101
|
+
# Check length
|
|
102
|
+
if len(name) > MAX_NAME_LENGTH:
|
|
103
|
+
result.add_error(f"Skill name exceeds maximum length of {MAX_NAME_LENGTH} characters")
|
|
104
|
+
|
|
105
|
+
# Check for leading hyphen
|
|
106
|
+
if name.startswith("-"):
|
|
107
|
+
result.add_error("Skill name cannot start with a hyphen")
|
|
108
|
+
|
|
109
|
+
# Check for trailing hyphen
|
|
110
|
+
if name.endswith("-"):
|
|
111
|
+
result.add_error("Skill name cannot end with a hyphen")
|
|
112
|
+
|
|
113
|
+
# Check for consecutive hyphens
|
|
114
|
+
if "--" in name:
|
|
115
|
+
result.add_error("Skill name cannot contain consecutive hyphens")
|
|
116
|
+
|
|
117
|
+
# Check overall pattern (lowercase alphanumeric with single hyphens)
|
|
118
|
+
if not NAME_PATTERN.match(name):
|
|
119
|
+
# Only add this if we haven't already identified the specific issue
|
|
120
|
+
if result.valid:
|
|
121
|
+
result.add_error(
|
|
122
|
+
"Skill name must contain only lowercase letters, numbers, and single hyphens"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return result
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def validate_skill_description(description: str | None) -> ValidationResult:
|
|
129
|
+
"""Validate a skill description per Agent Skills spec.
|
|
130
|
+
|
|
131
|
+
Requirements:
|
|
132
|
+
- Required (non-empty)
|
|
133
|
+
- Max 1024 characters
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
description: The skill description to validate
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
ValidationResult with any errors
|
|
140
|
+
"""
|
|
141
|
+
result = ValidationResult()
|
|
142
|
+
|
|
143
|
+
if description is None or description.strip() == "":
|
|
144
|
+
result.add_error("Skill description is required")
|
|
145
|
+
return result
|
|
146
|
+
|
|
147
|
+
if len(description) > MAX_DESCRIPTION_LENGTH:
|
|
148
|
+
result.add_error(
|
|
149
|
+
f"Skill description exceeds maximum length of {MAX_DESCRIPTION_LENGTH} characters"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def validate_skill_compatibility(compatibility: str | None) -> ValidationResult:
|
|
156
|
+
"""Validate a skill compatibility string per Agent Skills spec.
|
|
157
|
+
|
|
158
|
+
Requirements:
|
|
159
|
+
- Optional (can be None or empty)
|
|
160
|
+
- Max 500 characters if provided
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
compatibility: The compatibility string to validate
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
ValidationResult with any errors
|
|
167
|
+
"""
|
|
168
|
+
result = ValidationResult()
|
|
169
|
+
|
|
170
|
+
if compatibility is None or compatibility == "":
|
|
171
|
+
# Compatibility is optional
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
if len(compatibility) > MAX_COMPATIBILITY_LENGTH:
|
|
175
|
+
result.add_error(
|
|
176
|
+
f"Skill compatibility exceeds maximum length of {MAX_COMPATIBILITY_LENGTH} characters"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return result
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def validate_skill_tags(tags: list[str] | None) -> ValidationResult:
|
|
183
|
+
"""Validate skill tags.
|
|
184
|
+
|
|
185
|
+
Requirements:
|
|
186
|
+
- Optional (can be None or empty list)
|
|
187
|
+
- Must be a list of strings
|
|
188
|
+
- Each tag max 64 characters
|
|
189
|
+
- No empty tags
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
tags: The tags list to validate
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
ValidationResult with any errors
|
|
196
|
+
"""
|
|
197
|
+
result = ValidationResult()
|
|
198
|
+
|
|
199
|
+
if tags is None:
|
|
200
|
+
return result
|
|
201
|
+
|
|
202
|
+
if not isinstance(tags, list):
|
|
203
|
+
result.add_error("Tags must be a list")
|
|
204
|
+
return result
|
|
205
|
+
|
|
206
|
+
for i, tag in enumerate(tags):
|
|
207
|
+
if not isinstance(tag, str):
|
|
208
|
+
result.add_error(f"Tag at index {i} must be a string")
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
if tag.strip() == "":
|
|
212
|
+
result.add_error(f"Tag at index {i} cannot be empty")
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
if len(tag) > MAX_TAG_LENGTH:
|
|
216
|
+
result.add_error(
|
|
217
|
+
f"Tag '{tag[:20]}...' exceeds maximum length of {MAX_TAG_LENGTH} characters"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return result
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def validate_skill_version(version: str | None) -> ValidationResult:
|
|
224
|
+
"""Validate a skill version string.
|
|
225
|
+
|
|
226
|
+
Requirements:
|
|
227
|
+
- Optional (can be None)
|
|
228
|
+
- Must follow semver 2.0.0 pattern: MAJOR.MINOR.PATCH[-prerelease][+build]
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
version: The version string to validate
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
ValidationResult with any errors
|
|
235
|
+
"""
|
|
236
|
+
result = ValidationResult()
|
|
237
|
+
|
|
238
|
+
if version is None or version == "":
|
|
239
|
+
return result
|
|
240
|
+
|
|
241
|
+
if not SEMVER_PATTERN.match(version):
|
|
242
|
+
result.add_error(
|
|
243
|
+
f"Version '{version}' does not follow semver pattern (e.g., '1.0.0', '2.1.3', '1.0.0-beta')"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
return result
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def validate_skill_category(category: str | None) -> ValidationResult:
|
|
250
|
+
"""Validate a skill category.
|
|
251
|
+
|
|
252
|
+
Requirements:
|
|
253
|
+
- Optional (can be None)
|
|
254
|
+
- Must be lowercase alphanumeric + hyphens
|
|
255
|
+
- Must start with a letter
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
category: The category to validate
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
ValidationResult with any errors
|
|
262
|
+
"""
|
|
263
|
+
result = ValidationResult()
|
|
264
|
+
|
|
265
|
+
if category is None or category == "":
|
|
266
|
+
return result
|
|
267
|
+
|
|
268
|
+
if not CATEGORY_PATTERN.match(category):
|
|
269
|
+
result.add_error(
|
|
270
|
+
"Category must be lowercase letters, numbers, and hyphens, starting with a letter"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return result
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class SkillValidator:
|
|
277
|
+
"""Validates a complete skill against the Agent Skills specification.
|
|
278
|
+
|
|
279
|
+
This class combines all field validators to provide comprehensive
|
|
280
|
+
skill validation. It can validate either a ParsedSkill object or
|
|
281
|
+
raw field values.
|
|
282
|
+
|
|
283
|
+
Example usage:
|
|
284
|
+
```python
|
|
285
|
+
from gobby.skills.parser import parse_skill_file
|
|
286
|
+
from gobby.skills.validator import SkillValidator
|
|
287
|
+
|
|
288
|
+
skill = parse_skill_file("SKILL.md")
|
|
289
|
+
validator = SkillValidator()
|
|
290
|
+
result = validator.validate(skill)
|
|
291
|
+
|
|
292
|
+
if not result.valid:
|
|
293
|
+
for error in result.errors:
|
|
294
|
+
print(f"Error: {error}")
|
|
295
|
+
```
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
def validate(
|
|
299
|
+
self,
|
|
300
|
+
skill: Any = None,
|
|
301
|
+
*,
|
|
302
|
+
name: str | None = None,
|
|
303
|
+
description: str | None = None,
|
|
304
|
+
compatibility: str | None = None,
|
|
305
|
+
tags: list[str] | None = None,
|
|
306
|
+
version: str | None = None,
|
|
307
|
+
category: str | None = None,
|
|
308
|
+
) -> ValidationResult:
|
|
309
|
+
"""Validate a skill against the Agent Skills specification.
|
|
310
|
+
|
|
311
|
+
Can accept either a ParsedSkill object or individual field values.
|
|
312
|
+
If a skill object is provided, its fields take precedence.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
skill: A ParsedSkill object to validate (optional)
|
|
316
|
+
name: Skill name (required if no skill object)
|
|
317
|
+
description: Skill description (required if no skill object)
|
|
318
|
+
compatibility: Compatibility notes (optional)
|
|
319
|
+
tags: List of tags (optional)
|
|
320
|
+
version: Version string (optional)
|
|
321
|
+
category: Category string (optional)
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
ValidationResult with all errors and warnings
|
|
325
|
+
"""
|
|
326
|
+
result = ValidationResult()
|
|
327
|
+
|
|
328
|
+
# Extract fields from skill object if provided
|
|
329
|
+
if skill is not None:
|
|
330
|
+
name = getattr(skill, "name", name)
|
|
331
|
+
description = getattr(skill, "description", description)
|
|
332
|
+
compatibility = getattr(skill, "compatibility", compatibility)
|
|
333
|
+
version = getattr(skill, "version", version)
|
|
334
|
+
|
|
335
|
+
# Extract tags and category from metadata if available
|
|
336
|
+
metadata = getattr(skill, "metadata", None)
|
|
337
|
+
if metadata and isinstance(metadata, dict):
|
|
338
|
+
skillport = metadata.get("skillport", {})
|
|
339
|
+
if tags is None:
|
|
340
|
+
tags = skillport.get("tags")
|
|
341
|
+
if category is None:
|
|
342
|
+
category = skillport.get("category")
|
|
343
|
+
|
|
344
|
+
# Validate required fields
|
|
345
|
+
result.merge(validate_skill_name(name))
|
|
346
|
+
result.merge(validate_skill_description(description))
|
|
347
|
+
|
|
348
|
+
# Validate optional fields
|
|
349
|
+
result.merge(validate_skill_compatibility(compatibility))
|
|
350
|
+
result.merge(validate_skill_tags(tags))
|
|
351
|
+
result.merge(validate_skill_version(version))
|
|
352
|
+
result.merge(validate_skill_category(category))
|
|
353
|
+
|
|
354
|
+
return result
|
|
355
|
+
|
|
356
|
+
def validate_parsed_skill(self, skill: Any) -> ValidationResult:
|
|
357
|
+
"""Validate a ParsedSkill object.
|
|
358
|
+
|
|
359
|
+
This is a convenience method that wraps validate() for
|
|
360
|
+
ParsedSkill objects specifically.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
skill: A ParsedSkill object
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
ValidationResult with all errors and warnings
|
|
367
|
+
"""
|
|
368
|
+
return self.validate(skill=skill)
|