gobby 0.2.8__py3-none-any.whl → 0.2.9__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/claude_code.py +3 -26
- gobby/app_context.py +59 -0
- gobby/cli/utils.py +5 -17
- gobby/config/features.py +0 -20
- gobby/config/tasks.py +4 -0
- gobby/hooks/event_handlers/__init__.py +155 -0
- gobby/hooks/event_handlers/_agent.py +175 -0
- gobby/hooks/event_handlers/_base.py +87 -0
- gobby/hooks/event_handlers/_misc.py +66 -0
- gobby/hooks/event_handlers/_session.py +573 -0
- gobby/hooks/event_handlers/_tool.py +196 -0
- gobby/hooks/hook_manager.py +2 -0
- gobby/llm/claude.py +377 -42
- gobby/mcp_proxy/importer.py +4 -41
- gobby/mcp_proxy/manager.py +13 -3
- gobby/mcp_proxy/registries.py +14 -0
- gobby/mcp_proxy/services/recommendation.py +2 -28
- gobby/mcp_proxy/tools/artifacts.py +3 -3
- gobby/mcp_proxy/tools/task_readiness.py +27 -4
- gobby/mcp_proxy/tools/workflows/__init__.py +266 -0
- gobby/mcp_proxy/tools/workflows/_artifacts.py +225 -0
- gobby/mcp_proxy/tools/workflows/_import.py +112 -0
- gobby/mcp_proxy/tools/workflows/_lifecycle.py +321 -0
- gobby/mcp_proxy/tools/workflows/_query.py +207 -0
- gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
- gobby/mcp_proxy/tools/workflows/_terminal.py +139 -0
- gobby/memory/components/__init__.py +0 -0
- gobby/memory/components/ingestion.py +98 -0
- gobby/memory/components/search.py +108 -0
- gobby/memory/manager.py +16 -25
- gobby/paths.py +51 -0
- gobby/prompts/loader.py +1 -35
- gobby/runner.py +23 -10
- gobby/servers/http.py +186 -149
- gobby/servers/routes/admin.py +12 -0
- gobby/servers/routes/mcp/endpoints/execution.py +15 -7
- gobby/servers/routes/mcp/endpoints/registry.py +8 -8
- gobby/sessions/analyzer.py +2 -2
- gobby/skills/parser.py +23 -0
- gobby/skills/sync.py +5 -4
- gobby/storage/artifacts.py +19 -0
- gobby/storage/migrations.py +25 -2
- gobby/storage/skills.py +47 -7
- gobby/tasks/external_validator.py +4 -17
- gobby/tasks/validation.py +13 -87
- gobby/tools/summarizer.py +18 -51
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +5 -0
- gobby/workflows/context_actions.py +21 -24
- gobby/workflows/enforcement/__init__.py +11 -1
- gobby/workflows/enforcement/blocking.py +96 -0
- gobby/workflows/enforcement/handlers.py +35 -1
- gobby/workflows/engine.py +6 -3
- gobby/workflows/lifecycle_evaluator.py +2 -1
- {gobby-0.2.8.dist-info → gobby-0.2.9.dist-info}/METADATA +1 -1
- {gobby-0.2.8.dist-info → gobby-0.2.9.dist-info}/RECORD +61 -45
- gobby/hooks/event_handlers.py +0 -1008
- gobby/mcp_proxy/tools/workflows.py +0 -1023
- {gobby-0.2.8.dist-info → gobby-0.2.9.dist-info}/WHEEL +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.9.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.9.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.9.dist-info}/top_level.txt +0 -0
|
@@ -255,17 +255,17 @@ async def refresh_mcp_tools(
|
|
|
255
255
|
try:
|
|
256
256
|
session = await server.mcp_manager.ensure_connected(server_name)
|
|
257
257
|
tools_result = await session.list_tools()
|
|
258
|
-
for
|
|
258
|
+
for mcp_tool in tools_result.tools:
|
|
259
259
|
schema = None
|
|
260
|
-
if hasattr(
|
|
261
|
-
if hasattr(
|
|
262
|
-
schema =
|
|
263
|
-
elif isinstance(
|
|
264
|
-
schema =
|
|
260
|
+
if hasattr(mcp_tool, "inputSchema"):
|
|
261
|
+
if hasattr(mcp_tool.inputSchema, "model_dump"):
|
|
262
|
+
schema = mcp_tool.inputSchema.model_dump()
|
|
263
|
+
elif isinstance(mcp_tool.inputSchema, dict):
|
|
264
|
+
schema = mcp_tool.inputSchema
|
|
265
265
|
tools.append(
|
|
266
266
|
{
|
|
267
|
-
"name": getattr(
|
|
268
|
-
"description": getattr(
|
|
267
|
+
"name": getattr(mcp_tool, "name", ""),
|
|
268
|
+
"description": getattr(mcp_tool, "description", ""),
|
|
269
269
|
"inputSchema": schema,
|
|
270
270
|
}
|
|
271
271
|
)
|
gobby/sessions/analyzer.py
CHANGED
|
@@ -32,8 +32,8 @@ class HandoffContext:
|
|
|
32
32
|
key_decisions: list[str] | None = None
|
|
33
33
|
active_worktree: dict[str, Any] | None = None
|
|
34
34
|
"""Worktree context if session is operating in a worktree."""
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
# Note: active_skills field removed - redundant with _build_skill_injection_context()
|
|
36
|
+
# which already handles skill restoration on session start
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
class TranscriptAnalyzer:
|
gobby/skills/parser.py
CHANGED
|
@@ -64,6 +64,8 @@ class ParsedSkill:
|
|
|
64
64
|
scripts: List of script file paths (relative to skill dir)
|
|
65
65
|
references: List of reference file paths (relative to skill dir)
|
|
66
66
|
assets: List of asset file paths (relative to skill dir)
|
|
67
|
+
always_apply: Whether skill should always be injected at session start
|
|
68
|
+
injection_format: How to inject skill (summary, full, content)
|
|
67
69
|
"""
|
|
68
70
|
|
|
69
71
|
name: str
|
|
@@ -80,6 +82,8 @@ class ParsedSkill:
|
|
|
80
82
|
scripts: list[str] | None = None
|
|
81
83
|
references: list[str] | None = None
|
|
82
84
|
assets: list[str] | None = None
|
|
85
|
+
always_apply: bool = False
|
|
86
|
+
injection_format: str = "summary"
|
|
83
87
|
|
|
84
88
|
def get_category(self) -> str | None:
|
|
85
89
|
"""Get category from top-level or metadata.skillport.category."""
|
|
@@ -139,6 +143,8 @@ class ParsedSkill:
|
|
|
139
143
|
"scripts": self.scripts,
|
|
140
144
|
"references": self.references,
|
|
141
145
|
"assets": self.assets,
|
|
146
|
+
"always_apply": self.always_apply,
|
|
147
|
+
"injection_format": self.injection_format,
|
|
142
148
|
}
|
|
143
149
|
|
|
144
150
|
|
|
@@ -232,6 +238,7 @@ def parse_skill_text(text: str, source_path: str | None = None) -> ParsedSkill:
|
|
|
232
238
|
# This allows both top-level and nested formats to work
|
|
233
239
|
top_level_always_apply = frontmatter.get("alwaysApply")
|
|
234
240
|
top_level_category = frontmatter.get("category")
|
|
241
|
+
top_level_injection_format = frontmatter.get("injectionFormat")
|
|
235
242
|
|
|
236
243
|
if top_level_always_apply is not None or top_level_category is not None:
|
|
237
244
|
if metadata is None:
|
|
@@ -251,6 +258,20 @@ def parse_skill_text(text: str, source_path: str | None = None) -> ParsedSkill:
|
|
|
251
258
|
if version is not None:
|
|
252
259
|
version = str(version)
|
|
253
260
|
|
|
261
|
+
# Extract always_apply: check top-level first, then metadata.skillport.alwaysApply
|
|
262
|
+
always_apply = False
|
|
263
|
+
if top_level_always_apply is not None:
|
|
264
|
+
always_apply = bool(top_level_always_apply)
|
|
265
|
+
elif metadata and isinstance(metadata, dict):
|
|
266
|
+
skillport = metadata.get("skillport", {})
|
|
267
|
+
if isinstance(skillport, dict) and skillport.get("alwaysApply"):
|
|
268
|
+
always_apply = bool(skillport["alwaysApply"])
|
|
269
|
+
|
|
270
|
+
# Extract injection_format: check top-level first, default to "summary"
|
|
271
|
+
injection_format = "summary"
|
|
272
|
+
if top_level_injection_format is not None:
|
|
273
|
+
injection_format = str(top_level_injection_format)
|
|
274
|
+
|
|
254
275
|
return ParsedSkill(
|
|
255
276
|
name=name,
|
|
256
277
|
description=description,
|
|
@@ -261,6 +282,8 @@ def parse_skill_text(text: str, source_path: str | None = None) -> ParsedSkill:
|
|
|
261
282
|
allowed_tools=allowed_tools,
|
|
262
283
|
metadata=metadata,
|
|
263
284
|
source_path=source_path,
|
|
285
|
+
always_apply=always_apply,
|
|
286
|
+
injection_format=injection_format,
|
|
264
287
|
)
|
|
265
288
|
|
|
266
289
|
|
gobby/skills/sync.py
CHANGED
|
@@ -23,10 +23,9 @@ def get_bundled_skills_path() -> Path:
|
|
|
23
23
|
Returns:
|
|
24
24
|
Path to src/gobby/install/shared/skills/
|
|
25
25
|
"""
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return Path(__file__).parent.parent / "install" / "shared" / "skills"
|
|
26
|
+
from gobby.paths import get_install_dir
|
|
27
|
+
|
|
28
|
+
return get_install_dir() / "shared" / "skills"
|
|
30
29
|
|
|
31
30
|
|
|
32
31
|
def sync_bundled_skills(db: DatabaseProtocol) -> dict[str, Any]:
|
|
@@ -101,6 +100,8 @@ def sync_bundled_skills(db: DatabaseProtocol) -> dict[str, Any]:
|
|
|
101
100
|
source_ref=None,
|
|
102
101
|
project_id=None, # Global scope
|
|
103
102
|
enabled=True,
|
|
103
|
+
always_apply=parsed.always_apply,
|
|
104
|
+
injection_format=parsed.injection_format,
|
|
104
105
|
)
|
|
105
106
|
|
|
106
107
|
logger.info(f"Synced bundled skill: {parsed.name}")
|
gobby/storage/artifacts.py
CHANGED
|
@@ -283,3 +283,22 @@ class LocalArtifactManager:
|
|
|
283
283
|
|
|
284
284
|
rows = self.db.fetchall(sql, tuple(params))
|
|
285
285
|
return [Artifact.from_row(row) for row in rows]
|
|
286
|
+
|
|
287
|
+
def count_artifacts(self, session_id: str | None = None) -> int:
|
|
288
|
+
"""Count total artifacts, optionally filtered by session.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
session_id: Optional session ID to filter by
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Total artifact count
|
|
295
|
+
"""
|
|
296
|
+
if session_id:
|
|
297
|
+
row = self.db.fetchone(
|
|
298
|
+
"SELECT COUNT(*) FROM session_artifacts WHERE session_id = ?",
|
|
299
|
+
(session_id,),
|
|
300
|
+
)
|
|
301
|
+
else:
|
|
302
|
+
row = self.db.fetchone("SELECT COUNT(*) FROM session_artifacts")
|
|
303
|
+
|
|
304
|
+
return row[0] if row else 0
|
gobby/storage/migrations.py
CHANGED
|
@@ -43,9 +43,9 @@ class MigrationUnsupportedError(Exception):
|
|
|
43
43
|
# Migration can be SQL string or a callable that takes LocalDatabase
|
|
44
44
|
MigrationAction = str | Callable[[LocalDatabase], None]
|
|
45
45
|
|
|
46
|
-
# Baseline version - the schema state at
|
|
46
|
+
# Baseline version - the schema state at v79 (flattened)
|
|
47
47
|
# This is applied for new databases directly
|
|
48
|
-
BASELINE_VERSION =
|
|
48
|
+
BASELINE_VERSION = 79
|
|
49
49
|
|
|
50
50
|
# Baseline schema - flattened from v78 production state, includes hub tracking fields
|
|
51
51
|
# This is applied for new databases directly
|
|
@@ -587,6 +587,8 @@ CREATE TABLE skills (
|
|
|
587
587
|
hub_slug TEXT,
|
|
588
588
|
hub_version TEXT,
|
|
589
589
|
enabled INTEGER DEFAULT 1,
|
|
590
|
+
always_apply INTEGER DEFAULT 0,
|
|
591
|
+
injection_format TEXT DEFAULT 'summary',
|
|
590
592
|
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
591
593
|
created_at TEXT NOT NULL,
|
|
592
594
|
updated_at TEXT NOT NULL
|
|
@@ -594,6 +596,7 @@ CREATE TABLE skills (
|
|
|
594
596
|
CREATE INDEX idx_skills_name ON skills(name);
|
|
595
597
|
CREATE INDEX idx_skills_project_id ON skills(project_id);
|
|
596
598
|
CREATE INDEX idx_skills_enabled ON skills(enabled);
|
|
599
|
+
CREATE INDEX idx_skills_always_apply ON skills(always_apply);
|
|
597
600
|
CREATE UNIQUE INDEX idx_skills_name_project ON skills(name, project_id);
|
|
598
601
|
CREATE UNIQUE INDEX idx_skills_name_global ON skills(name) WHERE project_id IS NULL;
|
|
599
602
|
|
|
@@ -709,6 +712,24 @@ def _migrate_add_hub_tracking_to_skills(db: LocalDatabase) -> None:
|
|
|
709
712
|
logger.info("Added hub tracking fields to skills table")
|
|
710
713
|
|
|
711
714
|
|
|
715
|
+
def _migrate_add_skill_injection_columns(db: LocalDatabase) -> None:
|
|
716
|
+
"""Add always_apply and injection_format columns to skills table.
|
|
717
|
+
|
|
718
|
+
These columns enable per-skill control over:
|
|
719
|
+
- always_apply: Whether skill should always be injected at session start
|
|
720
|
+
- injection_format: How to inject the skill (summary, full, content)
|
|
721
|
+
|
|
722
|
+
The values are extracted from SKILL.md frontmatter during sync and stored
|
|
723
|
+
as columns for efficient querying.
|
|
724
|
+
"""
|
|
725
|
+
with db.transaction() as conn:
|
|
726
|
+
conn.execute("ALTER TABLE skills ADD COLUMN always_apply INTEGER DEFAULT 0")
|
|
727
|
+
conn.execute("ALTER TABLE skills ADD COLUMN injection_format TEXT DEFAULT 'summary'")
|
|
728
|
+
conn.execute("CREATE INDEX idx_skills_always_apply ON skills(always_apply)")
|
|
729
|
+
|
|
730
|
+
logger.info("Added always_apply and injection_format columns to skills table")
|
|
731
|
+
|
|
732
|
+
|
|
712
733
|
MIGRATIONS: list[tuple[int, str, MigrationAction]] = [
|
|
713
734
|
# Project-scoped session refs: Change seq_num index from global to project-scoped
|
|
714
735
|
(76, "Make sessions.seq_num project-scoped", _migrate_session_seq_num_project_scoped),
|
|
@@ -716,6 +737,8 @@ MIGRATIONS: list[tuple[int, str, MigrationAction]] = [
|
|
|
716
737
|
(77, "Backfill sessions.seq_num per project", _migrate_backfill_session_seq_num_per_project),
|
|
717
738
|
# Hub tracking: Add hub_name, hub_slug, hub_version to skills table
|
|
718
739
|
(78, "Add hub tracking fields to skills", _migrate_add_hub_tracking_to_skills),
|
|
740
|
+
# Skill injection: Add always_apply and injection_format columns
|
|
741
|
+
(79, "Add skill injection columns", _migrate_add_skill_injection_columns),
|
|
719
742
|
]
|
|
720
743
|
|
|
721
744
|
|
gobby/storage/skills.py
CHANGED
|
@@ -91,6 +91,8 @@ class Skill:
|
|
|
91
91
|
|
|
92
92
|
# Gobby-specific
|
|
93
93
|
enabled: bool = True
|
|
94
|
+
always_apply: bool = False
|
|
95
|
+
injection_format: str = "summary" # "summary", "full", "content"
|
|
94
96
|
project_id: str | None = None
|
|
95
97
|
|
|
96
98
|
# Timestamps
|
|
@@ -131,6 +133,10 @@ class Skill:
|
|
|
131
133
|
hub_slug=row["hub_slug"] if "hub_slug" in row.keys() else None,
|
|
132
134
|
hub_version=row["hub_version"] if "hub_version" in row.keys() else None,
|
|
133
135
|
enabled=bool(row["enabled"]),
|
|
136
|
+
always_apply=bool(row["always_apply"]) if "always_apply" in row.keys() else False,
|
|
137
|
+
injection_format=row["injection_format"]
|
|
138
|
+
if "injection_format" in row.keys()
|
|
139
|
+
else "summary",
|
|
134
140
|
project_id=row["project_id"],
|
|
135
141
|
created_at=row["created_at"],
|
|
136
142
|
updated_at=row["updated_at"],
|
|
@@ -159,6 +165,8 @@ class Skill:
|
|
|
159
165
|
"hub_slug": self.hub_slug,
|
|
160
166
|
"hub_version": self.hub_version,
|
|
161
167
|
"enabled": self.enabled,
|
|
168
|
+
"always_apply": self.always_apply,
|
|
169
|
+
"injection_format": self.injection_format,
|
|
162
170
|
"project_id": self.project_id,
|
|
163
171
|
"created_at": self.created_at,
|
|
164
172
|
"updated_at": self.updated_at,
|
|
@@ -192,9 +200,13 @@ class Skill:
|
|
|
192
200
|
def is_always_apply(self) -> bool:
|
|
193
201
|
"""Check if this is a core skill that should always be applied.
|
|
194
202
|
|
|
195
|
-
|
|
196
|
-
|
|
203
|
+
Reads from the always_apply column first (set during sync from frontmatter).
|
|
204
|
+
Falls back to metadata for backwards compatibility with older records.
|
|
197
205
|
"""
|
|
206
|
+
# Primary: read from column (set during sync)
|
|
207
|
+
if self.always_apply:
|
|
208
|
+
return True
|
|
209
|
+
# Fallback: check metadata for backwards compatibility
|
|
198
210
|
if not self.metadata:
|
|
199
211
|
return False
|
|
200
212
|
# Check top-level first
|
|
@@ -407,6 +419,8 @@ class LocalSkillManager:
|
|
|
407
419
|
hub_slug: str | None = None,
|
|
408
420
|
hub_version: str | None = None,
|
|
409
421
|
enabled: bool = True,
|
|
422
|
+
always_apply: bool = False,
|
|
423
|
+
injection_format: str = "summary",
|
|
410
424
|
project_id: str | None = None,
|
|
411
425
|
) -> Skill:
|
|
412
426
|
"""Create a new skill.
|
|
@@ -427,6 +441,8 @@ class LocalSkillManager:
|
|
|
427
441
|
hub_slug: Optional hub slug
|
|
428
442
|
hub_version: Optional hub version
|
|
429
443
|
enabled: Whether skill is active
|
|
444
|
+
always_apply: Whether skill should always be injected at session start
|
|
445
|
+
injection_format: How to inject skill (summary, full, content)
|
|
430
446
|
project_id: Project scope (None for global)
|
|
431
447
|
|
|
432
448
|
Returns:
|
|
@@ -457,8 +473,9 @@ class LocalSkillManager:
|
|
|
457
473
|
id, name, description, content, version, license,
|
|
458
474
|
compatibility, allowed_tools, metadata, source_path,
|
|
459
475
|
source_type, source_ref, hub_name, hub_slug, hub_version,
|
|
460
|
-
enabled,
|
|
461
|
-
|
|
476
|
+
enabled, always_apply, injection_format, project_id,
|
|
477
|
+
created_at, updated_at
|
|
478
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
462
479
|
""",
|
|
463
480
|
(
|
|
464
481
|
skill_id,
|
|
@@ -477,6 +494,8 @@ class LocalSkillManager:
|
|
|
477
494
|
hub_slug,
|
|
478
495
|
hub_version,
|
|
479
496
|
enabled,
|
|
497
|
+
always_apply,
|
|
498
|
+
injection_format,
|
|
480
499
|
project_id,
|
|
481
500
|
now,
|
|
482
501
|
now,
|
|
@@ -559,6 +578,8 @@ class LocalSkillManager:
|
|
|
559
578
|
hub_slug: str | None = _UNSET,
|
|
560
579
|
hub_version: str | None = _UNSET,
|
|
561
580
|
enabled: bool | None = None,
|
|
581
|
+
always_apply: bool | None = None,
|
|
582
|
+
injection_format: str | None = None,
|
|
562
583
|
) -> Skill:
|
|
563
584
|
"""Update an existing skill.
|
|
564
585
|
|
|
@@ -579,6 +600,8 @@ class LocalSkillManager:
|
|
|
579
600
|
hub_slug: New hub slug (use _UNSET to leave unchanged, None to clear)
|
|
580
601
|
hub_version: New hub version (use _UNSET to leave unchanged, None to clear)
|
|
581
602
|
enabled: New enabled state (optional)
|
|
603
|
+
always_apply: New always_apply state (optional)
|
|
604
|
+
injection_format: New injection format (optional)
|
|
582
605
|
|
|
583
606
|
Returns:
|
|
584
607
|
The updated Skill
|
|
@@ -634,6 +657,12 @@ class LocalSkillManager:
|
|
|
634
657
|
if enabled is not None:
|
|
635
658
|
updates.append("enabled = ?")
|
|
636
659
|
params.append(enabled)
|
|
660
|
+
if always_apply is not None:
|
|
661
|
+
updates.append("always_apply = ?")
|
|
662
|
+
params.append(always_apply)
|
|
663
|
+
if injection_format is not None:
|
|
664
|
+
updates.append("injection_format = ?")
|
|
665
|
+
params.append(injection_format)
|
|
637
666
|
|
|
638
667
|
if not updates:
|
|
639
668
|
return self.get_skill(skill_id)
|
|
@@ -770,7 +799,7 @@ class LocalSkillManager:
|
|
|
770
799
|
return [Skill.from_row(row) for row in rows]
|
|
771
800
|
|
|
772
801
|
def list_core_skills(self, project_id: str | None = None) -> list[Skill]:
|
|
773
|
-
"""List skills with
|
|
802
|
+
"""List skills with always_apply=true (efficiently via column query).
|
|
774
803
|
|
|
775
804
|
Args:
|
|
776
805
|
project_id: Optional project scope
|
|
@@ -778,8 +807,19 @@ class LocalSkillManager:
|
|
|
778
807
|
Returns:
|
|
779
808
|
List of core skills (always-apply skills)
|
|
780
809
|
"""
|
|
781
|
-
|
|
782
|
-
|
|
810
|
+
query = "SELECT * FROM skills WHERE always_apply = 1 AND enabled = 1"
|
|
811
|
+
params: list[Any] = []
|
|
812
|
+
|
|
813
|
+
if project_id:
|
|
814
|
+
query += " AND (project_id = ? OR project_id IS NULL)"
|
|
815
|
+
params.append(project_id)
|
|
816
|
+
else:
|
|
817
|
+
query += " AND project_id IS NULL"
|
|
818
|
+
|
|
819
|
+
query += " ORDER BY name ASC"
|
|
820
|
+
|
|
821
|
+
rows = self.db.fetchall(query, tuple(params))
|
|
822
|
+
return [Skill.from_row(row) for row in rows]
|
|
783
823
|
|
|
784
824
|
def skill_exists(self, skill_id: str) -> bool:
|
|
785
825
|
"""Check if a skill with the given ID exists.
|
|
@@ -46,12 +46,7 @@ if TYPE_CHECKING:
|
|
|
46
46
|
logger = logging.getLogger(__name__)
|
|
47
47
|
|
|
48
48
|
# Default system prompt for external validators
|
|
49
|
-
|
|
50
|
-
"You are an objective QA validator reviewing code changes. "
|
|
51
|
-
"You have no prior context about this task - evaluate purely based on "
|
|
52
|
-
"the acceptance criteria and the changes provided. "
|
|
53
|
-
"Be thorough but fair in your assessment."
|
|
54
|
-
)
|
|
49
|
+
|
|
55
50
|
|
|
56
51
|
# Module-level loader (initialized lazily)
|
|
57
52
|
_loader: PromptLoader | None = None
|
|
@@ -62,10 +57,7 @@ def _get_loader(project_dir: Path | None = None) -> PromptLoader:
|
|
|
62
57
|
global _loader
|
|
63
58
|
if _loader is None:
|
|
64
59
|
_loader = PromptLoader(project_dir=project_dir)
|
|
65
|
-
|
|
66
|
-
_loader.register_fallback(
|
|
67
|
-
"external_validation/system", lambda: DEFAULT_EXTERNAL_SYSTEM_PROMPT
|
|
68
|
-
)
|
|
60
|
+
|
|
69
61
|
return _loader
|
|
70
62
|
|
|
71
63
|
|
|
@@ -218,13 +210,8 @@ async def _run_llm_validation(
|
|
|
218
210
|
# Build the validation prompt
|
|
219
211
|
prompt = _build_external_validation_prompt(task, changes_context)
|
|
220
212
|
|
|
221
|
-
#
|
|
222
|
-
system_prompt = (
|
|
223
|
-
"You are an objective QA validator reviewing code changes. "
|
|
224
|
-
"You have no prior context about this task - evaluate purely based on "
|
|
225
|
-
"the acceptance criteria and the changes provided. "
|
|
226
|
-
"Be thorough but fair in your assessment."
|
|
227
|
-
)
|
|
213
|
+
# Render system prompt
|
|
214
|
+
system_prompt = _get_loader().render("external_validation/system", {})
|
|
228
215
|
|
|
229
216
|
try:
|
|
230
217
|
provider = llm_service.get_provider(config.provider)
|
gobby/tasks/validation.py
CHANGED
|
@@ -27,51 +27,6 @@ from gobby.utils.json_helpers import extract_json_object
|
|
|
27
27
|
|
|
28
28
|
logger = logging.getLogger(__name__)
|
|
29
29
|
|
|
30
|
-
# Default prompts (fallbacks for strangler fig pattern)
|
|
31
|
-
DEFAULT_VALIDATE_PROMPT = """Validate if the following changes satisfy the requirements.
|
|
32
|
-
|
|
33
|
-
Task: {title}
|
|
34
|
-
{category_section}{criteria_text}
|
|
35
|
-
|
|
36
|
-
{changes_section}
|
|
37
|
-
IMPORTANT: Return ONLY a JSON object, nothing else. No explanation, no preamble.
|
|
38
|
-
Format: {{"status": "valid", "feedback": "..."}} or {{"status": "invalid", "feedback": "..."}}
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
DEFAULT_CRITERIA_PROMPT = """Generate validation criteria for this task.
|
|
42
|
-
|
|
43
|
-
Task: {title}
|
|
44
|
-
Description: {description}
|
|
45
|
-
|
|
46
|
-
CRITICAL RULES - You MUST follow these:
|
|
47
|
-
1. **Only stated requirements** - Include ONLY requirements explicitly written in the title or description
|
|
48
|
-
2. **No invented values** - Do NOT invent specific numbers, timeouts, thresholds, or limits unless they appear in the task
|
|
49
|
-
3. **No invented edge cases** - Do NOT add edge cases, error scenarios, or boundary conditions beyond what's described
|
|
50
|
-
4. **Proportional detail** - Vague tasks get vague criteria; detailed tasks get detailed criteria
|
|
51
|
-
5. **When in doubt, leave it out** - If something isn't mentioned, don't include it
|
|
52
|
-
|
|
53
|
-
For vague requirements like "fix X" or "add Y", use criteria like:
|
|
54
|
-
- "X no longer produces the reported error/warning"
|
|
55
|
-
- "Y functionality works as expected"
|
|
56
|
-
- "Existing tests continue to pass"
|
|
57
|
-
- "No regressions introduced"
|
|
58
|
-
|
|
59
|
-
DO NOT generate criteria like:
|
|
60
|
-
- "timeout defaults to 30 seconds" (unless 30 seconds is in the task description)
|
|
61
|
-
- "handles edge case Z" (unless Z is mentioned in the task)
|
|
62
|
-
- "logs with format X" (unless that format is specified)
|
|
63
|
-
|
|
64
|
-
Format as markdown checkboxes:
|
|
65
|
-
## Deliverable
|
|
66
|
-
- [ ] What the task explicitly asks for
|
|
67
|
-
|
|
68
|
-
## Functional Requirements
|
|
69
|
-
- [ ] Only requirements stated in the description
|
|
70
|
-
|
|
71
|
-
## Verification
|
|
72
|
-
- [ ] Tests pass (if applicable)
|
|
73
|
-
- [ ] No regressions
|
|
74
|
-
"""
|
|
75
30
|
|
|
76
31
|
# Default number of commits to look back when gathering context
|
|
77
32
|
DEFAULT_COMMIT_WINDOW = 10
|
|
@@ -490,10 +445,6 @@ class TaskValidator:
|
|
|
490
445
|
self.llm_service = llm_service
|
|
491
446
|
self._loader = PromptLoader(project_dir=project_dir)
|
|
492
447
|
|
|
493
|
-
# Register fallbacks for strangler fig pattern
|
|
494
|
-
self._loader.register_fallback("validation/validate", lambda: DEFAULT_VALIDATE_PROMPT)
|
|
495
|
-
self._loader.register_fallback("validation/criteria", lambda: DEFAULT_CRITERIA_PROMPT)
|
|
496
|
-
|
|
497
448
|
async def gather_validation_context(self, file_paths: list[str]) -> str:
|
|
498
449
|
"""
|
|
499
450
|
Gather context for validation from files.
|
|
@@ -588,35 +539,16 @@ class TaskValidator:
|
|
|
588
539
|
else:
|
|
589
540
|
category_section += "\n"
|
|
590
541
|
|
|
591
|
-
# Build prompt using PromptLoader
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
try:
|
|
602
|
-
prompt = self._loader.render(prompt_path, template_context)
|
|
603
|
-
except FileNotFoundError:
|
|
604
|
-
logger.debug(f"Prompt template '{prompt_path}' not found, using fallback")
|
|
605
|
-
prompt = DEFAULT_VALIDATE_PROMPT.format(**template_context)
|
|
606
|
-
if file_context:
|
|
607
|
-
prompt += f"\nFile Context:\n{file_context[:50000]}\n"
|
|
608
|
-
else:
|
|
609
|
-
# Default behavior
|
|
610
|
-
template_context = {
|
|
611
|
-
"title": title,
|
|
612
|
-
"category_section": category_section,
|
|
613
|
-
"criteria_text": criteria_text,
|
|
614
|
-
"changes_section": changes_section,
|
|
615
|
-
"file_context": file_context[:50000] if file_context else "",
|
|
616
|
-
}
|
|
617
|
-
prompt = DEFAULT_VALIDATE_PROMPT.format(**template_context)
|
|
618
|
-
if file_context:
|
|
619
|
-
prompt += f"\nFile Context:\n{file_context[:50000]}\n"
|
|
542
|
+
# Build prompt using PromptLoader
|
|
543
|
+
prompt_path = self.config.prompt_path or "validation/validate"
|
|
544
|
+
template_context = {
|
|
545
|
+
"title": title,
|
|
546
|
+
"category_section": category_section,
|
|
547
|
+
"criteria_text": criteria_text,
|
|
548
|
+
"changes_section": changes_section,
|
|
549
|
+
"file_context": file_context[:50000] if file_context else "",
|
|
550
|
+
}
|
|
551
|
+
prompt = self._loader.render(prompt_path, template_context)
|
|
620
552
|
|
|
621
553
|
try:
|
|
622
554
|
provider = self.llm_service.get_provider(self.config.provider)
|
|
@@ -670,19 +602,13 @@ class TaskValidator:
|
|
|
670
602
|
if not self.config.enabled:
|
|
671
603
|
return None
|
|
672
604
|
|
|
673
|
-
#
|
|
605
|
+
# Use PromptLoader
|
|
606
|
+
prompt_path = self.config.criteria_prompt_path or "validation/criteria"
|
|
674
607
|
template_context = {
|
|
675
608
|
"title": title,
|
|
676
609
|
"description": description or "(no description)",
|
|
677
610
|
}
|
|
678
|
-
|
|
679
|
-
# Use PromptLoader
|
|
680
|
-
prompt_path = self.config.criteria_prompt_path or "validation/criteria"
|
|
681
|
-
try:
|
|
682
|
-
prompt = self._loader.render(prompt_path, template_context)
|
|
683
|
-
except FileNotFoundError:
|
|
684
|
-
logger.debug(f"Prompt template '{prompt_path}' not found, using fallback")
|
|
685
|
-
prompt = DEFAULT_CRITERIA_PROMPT.format(**template_context)
|
|
611
|
+
prompt = self._loader.render(prompt_path, template_context)
|
|
686
612
|
|
|
687
613
|
try:
|
|
688
614
|
provider = self.llm_service.get_provider(self.config.provider)
|
gobby/tools/summarizer.py
CHANGED
|
@@ -23,25 +23,6 @@ MAX_DESCRIPTION_LENGTH = 200
|
|
|
23
23
|
_config: ToolSummarizerConfig | None = None
|
|
24
24
|
_loader: PromptLoader | None = None
|
|
25
25
|
|
|
26
|
-
DEFAULT_SUMMARY_PROMPT = """Summarize this MCP tool description in 180 characters or less.
|
|
27
|
-
Keep it to three sentences or less. Be concise and preserve the key functionality.
|
|
28
|
-
Do not add quotes, extra formatting, or code examples.
|
|
29
|
-
|
|
30
|
-
Description: {description}
|
|
31
|
-
|
|
32
|
-
Summary:"""
|
|
33
|
-
|
|
34
|
-
DEFAULT_SUMMARY_SYSTEM_PROMPT = "You are a technical summarizer. Create concise tool descriptions."
|
|
35
|
-
|
|
36
|
-
DEFAULT_SERVER_DESC_PROMPT = """Write a single concise sentence describing what the '{server_name}' MCP server does based on its tools.
|
|
37
|
-
|
|
38
|
-
Tools:
|
|
39
|
-
{tools_list}
|
|
40
|
-
|
|
41
|
-
Description (1 sentence, try to keep under 100 characters):"""
|
|
42
|
-
|
|
43
|
-
DEFAULT_SERVER_DESC_SYSTEM_PROMPT = "You write concise technical descriptions."
|
|
44
|
-
|
|
45
26
|
|
|
46
27
|
def init_summarizer_config(config: ToolSummarizerConfig, project_dir: str | None = None) -> None:
|
|
47
28
|
"""Initialize the summarizer with configuration."""
|
|
@@ -50,13 +31,6 @@ def init_summarizer_config(config: ToolSummarizerConfig, project_dir: str | None
|
|
|
50
31
|
global _config, _loader
|
|
51
32
|
_config = config
|
|
52
33
|
_loader = PromptLoader(project_dir=Path(project_dir) if project_dir else None)
|
|
53
|
-
# Register fallbacks
|
|
54
|
-
_loader.register_fallback("features/tool_summary", lambda: DEFAULT_SUMMARY_PROMPT)
|
|
55
|
-
_loader.register_fallback("features/tool_summary_system", lambda: DEFAULT_SUMMARY_SYSTEM_PROMPT)
|
|
56
|
-
_loader.register_fallback("features/server_description", lambda: DEFAULT_SERVER_DESC_PROMPT)
|
|
57
|
-
_loader.register_fallback(
|
|
58
|
-
"features/server_description_system", lambda: DEFAULT_SERVER_DESC_SYSTEM_PROMPT
|
|
59
|
-
)
|
|
60
34
|
|
|
61
35
|
|
|
62
36
|
def _get_config() -> ToolSummarizerConfig:
|
|
@@ -96,9 +70,9 @@ async def _summarize_description_with_claude(description: str) -> str:
|
|
|
96
70
|
if _loader is None:
|
|
97
71
|
raise RuntimeError("Summarizer not initialized")
|
|
98
72
|
prompt = _loader.render(prompt_path, {"description": description})
|
|
99
|
-
except (
|
|
100
|
-
logger.debug(f"Failed to load prompt from {prompt_path}: {e}
|
|
101
|
-
|
|
73
|
+
except (OSError, KeyError, ValueError, RuntimeError) as e:
|
|
74
|
+
logger.debug(f"Failed to load prompt from {prompt_path}: {e}")
|
|
75
|
+
raise
|
|
102
76
|
|
|
103
77
|
# Get system prompt
|
|
104
78
|
sys_prompt_path = config.system_prompt_path or "features/tool_summary_system"
|
|
@@ -106,9 +80,9 @@ async def _summarize_description_with_claude(description: str) -> str:
|
|
|
106
80
|
if _loader is None:
|
|
107
81
|
raise RuntimeError("Summarizer not initialized")
|
|
108
82
|
system_prompt = _loader.render(sys_prompt_path, {})
|
|
109
|
-
except (
|
|
110
|
-
logger.debug(f"Failed to load system prompt from {sys_prompt_path}: {e}
|
|
111
|
-
system_prompt =
|
|
83
|
+
except (OSError, KeyError, ValueError, RuntimeError) as e:
|
|
84
|
+
logger.debug(f"Failed to load system prompt from {sys_prompt_path}: {e}")
|
|
85
|
+
system_prompt = "You are a technical summarizer."
|
|
112
86
|
|
|
113
87
|
# Configure for single-turn completion
|
|
114
88
|
options = ClaudeAgentOptions(
|
|
@@ -198,30 +172,23 @@ async def generate_server_description(
|
|
|
198
172
|
"server_name": server_name,
|
|
199
173
|
"tools_list": tools_list,
|
|
200
174
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
prompt = _loader.render(prompt_path, context)
|
|
209
|
-
except (FileNotFoundError, OSError, KeyError, ValueError, RuntimeError) as e:
|
|
210
|
-
logger.debug(f"Failed to load prompt from {prompt_path}: {e}, using default")
|
|
211
|
-
prompt = DEFAULT_SERVER_DESC_PROMPT.format(**context)
|
|
175
|
+
if _loader is None:
|
|
176
|
+
_get_config() # force init
|
|
177
|
+
if _loader is None:
|
|
178
|
+
# Still None after _get_config, use default
|
|
179
|
+
raise RuntimeError("Summarizer not initialized")
|
|
180
|
+
else:
|
|
181
|
+
prompt = _loader.render(prompt_path, context)
|
|
212
182
|
|
|
213
183
|
# Get system prompt
|
|
214
184
|
sys_prompt_path = (
|
|
215
185
|
config.server_description_system_prompt_path or "features/server_description_system"
|
|
216
186
|
)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
except (FileNotFoundError, OSError, KeyError, ValueError, RuntimeError) as e:
|
|
223
|
-
logger.debug(f"Failed to load system prompt from {sys_prompt_path}: {e}, using default")
|
|
224
|
-
system_prompt = DEFAULT_SERVER_DESC_SYSTEM_PROMPT
|
|
187
|
+
|
|
188
|
+
if _loader is None:
|
|
189
|
+
system_prompt = "You write concise technical descriptions."
|
|
190
|
+
else:
|
|
191
|
+
system_prompt = _loader.render(sys_prompt_path, {})
|
|
225
192
|
|
|
226
193
|
# Configure for single-turn completion
|
|
227
194
|
options = ClaudeAgentOptions(
|