gobby 0.2.5__py3-none-any.whl → 0.2.6__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.
Files changed (148) hide show
  1. gobby/adapters/claude_code.py +13 -4
  2. gobby/adapters/codex.py +43 -3
  3. gobby/agents/runner.py +8 -0
  4. gobby/cli/__init__.py +6 -0
  5. gobby/cli/clones.py +419 -0
  6. gobby/cli/conductor.py +266 -0
  7. gobby/cli/installers/antigravity.py +3 -9
  8. gobby/cli/installers/claude.py +9 -9
  9. gobby/cli/installers/codex.py +2 -8
  10. gobby/cli/installers/gemini.py +2 -8
  11. gobby/cli/installers/shared.py +71 -8
  12. gobby/cli/skills.py +858 -0
  13. gobby/cli/tasks/ai.py +0 -440
  14. gobby/cli/tasks/crud.py +44 -6
  15. gobby/cli/tasks/main.py +0 -4
  16. gobby/cli/tui.py +2 -2
  17. gobby/cli/utils.py +3 -3
  18. gobby/clones/__init__.py +13 -0
  19. gobby/clones/git.py +547 -0
  20. gobby/conductor/__init__.py +16 -0
  21. gobby/conductor/alerts.py +135 -0
  22. gobby/conductor/loop.py +164 -0
  23. gobby/conductor/monitors/__init__.py +11 -0
  24. gobby/conductor/monitors/agents.py +116 -0
  25. gobby/conductor/monitors/tasks.py +155 -0
  26. gobby/conductor/pricing.py +234 -0
  27. gobby/conductor/token_tracker.py +160 -0
  28. gobby/config/app.py +63 -1
  29. gobby/config/search.py +110 -0
  30. gobby/config/servers.py +1 -1
  31. gobby/config/skills.py +43 -0
  32. gobby/config/tasks.py +6 -14
  33. gobby/hooks/event_handlers.py +145 -2
  34. gobby/hooks/hook_manager.py +48 -2
  35. gobby/hooks/skill_manager.py +130 -0
  36. gobby/install/claude/hooks/hook_dispatcher.py +4 -4
  37. gobby/install/codex/hooks/hook_dispatcher.py +1 -1
  38. gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
  39. gobby/llm/claude.py +22 -34
  40. gobby/llm/claude_executor.py +46 -256
  41. gobby/llm/codex_executor.py +59 -291
  42. gobby/llm/executor.py +21 -0
  43. gobby/llm/gemini.py +134 -110
  44. gobby/llm/litellm_executor.py +143 -6
  45. gobby/llm/resolver.py +95 -33
  46. gobby/mcp_proxy/instructions.py +54 -0
  47. gobby/mcp_proxy/models.py +15 -0
  48. gobby/mcp_proxy/registries.py +68 -5
  49. gobby/mcp_proxy/server.py +33 -3
  50. gobby/mcp_proxy/services/tool_proxy.py +81 -1
  51. gobby/mcp_proxy/stdio.py +2 -1
  52. gobby/mcp_proxy/tools/__init__.py +0 -2
  53. gobby/mcp_proxy/tools/agent_messaging.py +317 -0
  54. gobby/mcp_proxy/tools/clones.py +903 -0
  55. gobby/mcp_proxy/tools/memory.py +1 -24
  56. gobby/mcp_proxy/tools/metrics.py +65 -1
  57. gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
  58. gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
  59. gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
  60. gobby/mcp_proxy/tools/session_messages.py +1 -2
  61. gobby/mcp_proxy/tools/skills/__init__.py +631 -0
  62. gobby/mcp_proxy/tools/task_orchestration.py +7 -0
  63. gobby/mcp_proxy/tools/task_readiness.py +14 -0
  64. gobby/mcp_proxy/tools/task_sync.py +1 -1
  65. gobby/mcp_proxy/tools/tasks/_context.py +0 -20
  66. gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
  67. gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
  68. gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
  69. gobby/mcp_proxy/tools/tasks/_lifecycle.py +60 -29
  70. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
  71. gobby/mcp_proxy/tools/workflows.py +1 -1
  72. gobby/mcp_proxy/tools/worktrees.py +5 -0
  73. gobby/memory/backends/__init__.py +6 -1
  74. gobby/memory/backends/mem0.py +6 -1
  75. gobby/memory/extractor.py +477 -0
  76. gobby/memory/manager.py +11 -2
  77. gobby/prompts/defaults/handoff/compact.md +63 -0
  78. gobby/prompts/defaults/handoff/session_end.md +57 -0
  79. gobby/prompts/defaults/memory/extract.md +61 -0
  80. gobby/runner.py +37 -16
  81. gobby/search/__init__.py +48 -6
  82. gobby/search/backends/__init__.py +159 -0
  83. gobby/search/backends/embedding.py +225 -0
  84. gobby/search/embeddings.py +238 -0
  85. gobby/search/models.py +148 -0
  86. gobby/search/unified.py +496 -0
  87. gobby/servers/http.py +23 -8
  88. gobby/servers/routes/admin.py +280 -0
  89. gobby/servers/routes/mcp/tools.py +241 -52
  90. gobby/servers/websocket.py +2 -2
  91. gobby/sessions/analyzer.py +2 -0
  92. gobby/sessions/transcripts/base.py +1 -0
  93. gobby/sessions/transcripts/claude.py +64 -5
  94. gobby/skills/__init__.py +91 -0
  95. gobby/skills/loader.py +685 -0
  96. gobby/skills/manager.py +384 -0
  97. gobby/skills/parser.py +258 -0
  98. gobby/skills/search.py +463 -0
  99. gobby/skills/sync.py +119 -0
  100. gobby/skills/updater.py +385 -0
  101. gobby/skills/validator.py +368 -0
  102. gobby/storage/clones.py +378 -0
  103. gobby/storage/database.py +1 -1
  104. gobby/storage/memories.py +43 -13
  105. gobby/storage/migrations.py +180 -6
  106. gobby/storage/sessions.py +73 -0
  107. gobby/storage/skills.py +749 -0
  108. gobby/storage/tasks/_crud.py +4 -4
  109. gobby/storage/tasks/_lifecycle.py +41 -6
  110. gobby/storage/tasks/_manager.py +14 -5
  111. gobby/storage/tasks/_models.py +8 -3
  112. gobby/sync/memories.py +39 -4
  113. gobby/sync/tasks.py +83 -6
  114. gobby/tasks/__init__.py +1 -2
  115. gobby/tasks/validation.py +24 -15
  116. gobby/tui/api_client.py +4 -7
  117. gobby/tui/app.py +5 -3
  118. gobby/tui/screens/orchestrator.py +1 -2
  119. gobby/tui/screens/tasks.py +2 -4
  120. gobby/tui/ws_client.py +1 -1
  121. gobby/utils/daemon_client.py +2 -2
  122. gobby/workflows/actions.py +84 -2
  123. gobby/workflows/context_actions.py +43 -0
  124. gobby/workflows/detection_helpers.py +115 -31
  125. gobby/workflows/engine.py +13 -2
  126. gobby/workflows/lifecycle_evaluator.py +29 -1
  127. gobby/workflows/loader.py +19 -6
  128. gobby/workflows/memory_actions.py +74 -0
  129. gobby/workflows/summary_actions.py +17 -0
  130. gobby/workflows/task_enforcement_actions.py +448 -6
  131. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/METADATA +82 -21
  132. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/RECORD +136 -107
  133. gobby/install/codex/prompts/forget.md +0 -7
  134. gobby/install/codex/prompts/memories.md +0 -7
  135. gobby/install/codex/prompts/recall.md +0 -7
  136. gobby/install/codex/prompts/remember.md +0 -13
  137. gobby/llm/gemini_executor.py +0 -339
  138. gobby/mcp_proxy/tools/task_expansion.py +0 -591
  139. gobby/tasks/context.py +0 -747
  140. gobby/tasks/criteria.py +0 -342
  141. gobby/tasks/expansion.py +0 -626
  142. gobby/tasks/prompts/expand.py +0 -327
  143. gobby/tasks/research.py +0 -421
  144. gobby/tasks/tdd.py +0 -352
  145. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/WHEEL +0 -0
  146. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/entry_points.txt +0 -0
  147. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/licenses/LICENSE.md +0 -0
  148. {gobby-0.2.5.dist-info → gobby-0.2.6.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)