gobby 0.2.8__py3-none-any.whl → 0.2.11__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 +6 -0
- gobby/adapters/base.py +11 -2
- gobby/adapters/claude_code.py +5 -28
- gobby/adapters/codex_impl/adapter.py +38 -43
- gobby/adapters/copilot.py +324 -0
- gobby/adapters/cursor.py +373 -0
- gobby/adapters/gemini.py +2 -26
- gobby/adapters/windsurf.py +359 -0
- gobby/agents/definitions.py +162 -2
- gobby/agents/isolation.py +33 -1
- gobby/agents/pty_reader.py +192 -0
- gobby/agents/registry.py +10 -1
- gobby/agents/runner.py +24 -8
- gobby/agents/sandbox.py +8 -3
- gobby/agents/session.py +4 -0
- gobby/agents/spawn.py +9 -2
- gobby/agents/spawn_executor.py +49 -61
- gobby/agents/spawners/command_builder.py +4 -4
- gobby/app_context.py +64 -0
- gobby/cli/__init__.py +4 -0
- gobby/cli/install.py +259 -4
- gobby/cli/installers/__init__.py +12 -0
- gobby/cli/installers/copilot.py +242 -0
- gobby/cli/installers/cursor.py +244 -0
- gobby/cli/installers/shared.py +3 -0
- gobby/cli/installers/windsurf.py +242 -0
- gobby/cli/pipelines.py +639 -0
- gobby/cli/sessions.py +3 -1
- gobby/cli/skills.py +209 -0
- gobby/cli/tasks/crud.py +6 -5
- gobby/cli/tasks/search.py +1 -1
- gobby/cli/ui.py +116 -0
- gobby/cli/utils.py +5 -17
- gobby/cli/workflows.py +38 -17
- gobby/config/app.py +5 -0
- gobby/config/features.py +0 -20
- gobby/config/skills.py +23 -2
- gobby/config/tasks.py +4 -0
- gobby/hooks/broadcaster.py +9 -0
- gobby/hooks/event_handlers/__init__.py +155 -0
- gobby/hooks/event_handlers/_agent.py +175 -0
- gobby/hooks/event_handlers/_base.py +92 -0
- gobby/hooks/event_handlers/_misc.py +66 -0
- gobby/hooks/event_handlers/_session.py +487 -0
- gobby/hooks/event_handlers/_tool.py +196 -0
- gobby/hooks/events.py +48 -0
- gobby/hooks/hook_manager.py +27 -3
- gobby/install/copilot/hooks/hook_dispatcher.py +203 -0
- gobby/install/cursor/hooks/hook_dispatcher.py +203 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +8 -0
- gobby/install/windsurf/hooks/hook_dispatcher.py +205 -0
- gobby/llm/__init__.py +14 -1
- gobby/llm/claude.py +594 -43
- gobby/llm/service.py +149 -0
- gobby/mcp_proxy/importer.py +4 -41
- gobby/mcp_proxy/instructions.py +9 -27
- gobby/mcp_proxy/manager.py +13 -3
- gobby/mcp_proxy/models.py +1 -0
- gobby/mcp_proxy/registries.py +66 -5
- gobby/mcp_proxy/server.py +6 -2
- gobby/mcp_proxy/services/recommendation.py +2 -28
- gobby/mcp_proxy/services/tool_filter.py +7 -0
- gobby/mcp_proxy/services/tool_proxy.py +19 -1
- gobby/mcp_proxy/stdio.py +37 -21
- gobby/mcp_proxy/tools/agents.py +7 -0
- gobby/mcp_proxy/tools/artifacts.py +3 -3
- gobby/mcp_proxy/tools/hub.py +30 -1
- gobby/mcp_proxy/tools/orchestration/cleanup.py +5 -5
- gobby/mcp_proxy/tools/orchestration/monitor.py +1 -1
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +8 -3
- gobby/mcp_proxy/tools/orchestration/review.py +17 -4
- gobby/mcp_proxy/tools/orchestration/wait.py +7 -7
- gobby/mcp_proxy/tools/pipelines/__init__.py +254 -0
- gobby/mcp_proxy/tools/pipelines/_discovery.py +67 -0
- gobby/mcp_proxy/tools/pipelines/_execution.py +281 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +4 -4
- gobby/mcp_proxy/tools/sessions/_handoff.py +1 -1
- gobby/mcp_proxy/tools/skills/__init__.py +184 -30
- gobby/mcp_proxy/tools/spawn_agent.py +229 -14
- gobby/mcp_proxy/tools/task_readiness.py +27 -4
- gobby/mcp_proxy/tools/tasks/_context.py +8 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +27 -1
- gobby/mcp_proxy/tools/tasks/_helpers.py +1 -1
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +125 -8
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +2 -1
- gobby/mcp_proxy/tools/tasks/_search.py +1 -1
- gobby/mcp_proxy/tools/workflows/__init__.py +273 -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 +332 -0
- gobby/mcp_proxy/tools/workflows/_query.py +226 -0
- gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
- gobby/mcp_proxy/tools/workflows/_terminal.py +175 -0
- gobby/mcp_proxy/tools/worktrees.py +54 -15
- gobby/memory/components/__init__.py +0 -0
- gobby/memory/components/ingestion.py +98 -0
- gobby/memory/components/search.py +108 -0
- gobby/memory/context.py +5 -5
- gobby/memory/manager.py +16 -25
- gobby/paths.py +51 -0
- gobby/prompts/loader.py +1 -35
- gobby/runner.py +131 -16
- gobby/servers/http.py +193 -150
- gobby/servers/routes/__init__.py +2 -0
- gobby/servers/routes/admin.py +56 -0
- gobby/servers/routes/mcp/endpoints/execution.py +33 -32
- gobby/servers/routes/mcp/endpoints/registry.py +8 -8
- gobby/servers/routes/mcp/hooks.py +10 -1
- gobby/servers/routes/pipelines.py +227 -0
- gobby/servers/websocket.py +314 -1
- gobby/sessions/analyzer.py +89 -3
- gobby/sessions/manager.py +5 -5
- gobby/sessions/transcripts/__init__.py +3 -0
- gobby/sessions/transcripts/claude.py +5 -0
- gobby/sessions/transcripts/codex.py +5 -0
- gobby/sessions/transcripts/gemini.py +5 -0
- gobby/skills/hubs/__init__.py +25 -0
- gobby/skills/hubs/base.py +234 -0
- gobby/skills/hubs/claude_plugins.py +328 -0
- gobby/skills/hubs/clawdhub.py +289 -0
- gobby/skills/hubs/github_collection.py +465 -0
- gobby/skills/hubs/manager.py +263 -0
- gobby/skills/hubs/skillhub.py +342 -0
- gobby/skills/parser.py +23 -0
- gobby/skills/sync.py +5 -4
- gobby/storage/artifacts.py +19 -0
- gobby/storage/memories.py +4 -4
- gobby/storage/migrations.py +118 -3
- gobby/storage/pipelines.py +367 -0
- gobby/storage/sessions.py +23 -4
- gobby/storage/skills.py +48 -8
- gobby/storage/tasks/_aggregates.py +2 -2
- gobby/storage/tasks/_lifecycle.py +4 -4
- gobby/storage/tasks/_models.py +7 -1
- gobby/storage/tasks/_queries.py +3 -3
- gobby/sync/memories.py +4 -3
- gobby/tasks/commits.py +48 -17
- 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 +80 -0
- gobby/workflows/context_actions.py +265 -27
- gobby/workflows/definitions.py +119 -1
- gobby/workflows/detection_helpers.py +23 -11
- gobby/workflows/enforcement/__init__.py +11 -1
- gobby/workflows/enforcement/blocking.py +96 -0
- gobby/workflows/enforcement/handlers.py +35 -1
- gobby/workflows/enforcement/task_policy.py +18 -0
- gobby/workflows/engine.py +26 -4
- gobby/workflows/evaluator.py +8 -5
- gobby/workflows/lifecycle_evaluator.py +59 -27
- gobby/workflows/loader.py +567 -30
- gobby/workflows/lobster_compat.py +147 -0
- gobby/workflows/pipeline_executor.py +801 -0
- gobby/workflows/pipeline_state.py +172 -0
- gobby/workflows/pipeline_webhooks.py +206 -0
- gobby/workflows/premature_stop.py +5 -0
- gobby/worktrees/git.py +135 -20
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/METADATA +56 -22
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/RECORD +166 -122
- gobby/hooks/event_handlers.py +0 -1008
- gobby/mcp_proxy/tools/workflows.py +0 -1023
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/WHEEL +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/top_level.txt +0 -0
gobby/storage/skills.py
CHANGED
|
@@ -23,7 +23,7 @@ logger = logging.getLogger(__name__)
|
|
|
23
23
|
_UNSET: Any = object()
|
|
24
24
|
|
|
25
25
|
# Valid source types for skills
|
|
26
|
-
SkillSourceType = Literal["local", "github", "url", "zip", "filesystem"]
|
|
26
|
+
SkillSourceType = Literal["local", "github", "url", "zip", "filesystem", "hub"]
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
@dataclass
|
|
@@ -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.
|
|
@@ -97,7 +97,7 @@ def count_ready_tasks(
|
|
|
97
97
|
-- Blocker is unresolved if not closed AND not in review without requiring user review
|
|
98
98
|
AND NOT (
|
|
99
99
|
blocker.status = 'closed'
|
|
100
|
-
OR (blocker.status = '
|
|
100
|
+
OR (blocker.status = 'needs_review' AND blocker.requires_user_review = 0)
|
|
101
101
|
)
|
|
102
102
|
-- Exclude ancestor blocked by any descendant (completion block, not work block)
|
|
103
103
|
-- Check if t.id appears anywhere in blocker's ancestor chain
|
|
@@ -153,7 +153,7 @@ def count_blocked_tasks(
|
|
|
153
153
|
-- Blocker is unresolved if not closed AND not in review without requiring user review
|
|
154
154
|
AND NOT (
|
|
155
155
|
blocker.status = 'closed'
|
|
156
|
-
OR (blocker.status = '
|
|
156
|
+
OR (blocker.status = 'needs_review' AND blocker.requires_user_review = 0)
|
|
157
157
|
)
|
|
158
158
|
-- Exclude ancestor blocked by any descendant (completion block, not work block)
|
|
159
159
|
-- Check if t.id appears anywhere in blocker's ancestor chain
|
|
@@ -57,9 +57,9 @@ def close_task(
|
|
|
57
57
|
f"Cannot close task {task_id}: has {len(open_children)} open child task(s): {child_list}"
|
|
58
58
|
)
|
|
59
59
|
|
|
60
|
-
# Check if task is being closed from
|
|
60
|
+
# Check if task is being closed from needs_review state (user acceptance)
|
|
61
61
|
current_task = get_task(db, task_id)
|
|
62
|
-
accepted_by_user = current_task.status == "
|
|
62
|
+
accepted_by_user = current_task.status == "needs_review" if current_task else False
|
|
63
63
|
|
|
64
64
|
now = datetime.now(UTC).isoformat()
|
|
65
65
|
with db.transaction() as conn:
|
|
@@ -117,8 +117,8 @@ def reopen_task(
|
|
|
117
117
|
ValueError: If task not found or not closed/review
|
|
118
118
|
"""
|
|
119
119
|
task = get_task(db, task_id)
|
|
120
|
-
if task.status not in ("closed", "
|
|
121
|
-
raise ValueError(f"Task {task_id} is not closed or in
|
|
120
|
+
if task.status not in ("closed", "needs_review"):
|
|
121
|
+
raise ValueError(f"Task {task_id} is not closed or in needs_review (status: {task.status})")
|
|
122
122
|
|
|
123
123
|
now = datetime.now(UTC).isoformat()
|
|
124
124
|
|
gobby/storage/tasks/_models.py
CHANGED
|
@@ -82,7 +82,13 @@ class Task:
|
|
|
82
82
|
project_id: str
|
|
83
83
|
title: str
|
|
84
84
|
status: Literal[
|
|
85
|
-
"open",
|
|
85
|
+
"open",
|
|
86
|
+
"in_progress",
|
|
87
|
+
"needs_review",
|
|
88
|
+
"closed",
|
|
89
|
+
"failed",
|
|
90
|
+
"escalated",
|
|
91
|
+
"needs_decomposition",
|
|
86
92
|
]
|
|
87
93
|
priority: int
|
|
88
94
|
task_type: str # bug, feature, task, epic, chore
|
gobby/storage/tasks/_queries.py
CHANGED
|
@@ -156,7 +156,7 @@ def list_ready_tasks(
|
|
|
156
156
|
-- Blocker is unresolved if not closed AND not in review without requiring user review
|
|
157
157
|
AND NOT (
|
|
158
158
|
blocker.status = 'closed'
|
|
159
|
-
OR (blocker.status = '
|
|
159
|
+
OR (blocker.status = 'needs_review' AND blocker.requires_user_review = 0)
|
|
160
160
|
)
|
|
161
161
|
-- Exclude ancestor blocked by any descendant (completion block, not work block)
|
|
162
162
|
AND NOT EXISTS (
|
|
@@ -186,7 +186,7 @@ def list_ready_tasks(
|
|
|
186
186
|
-- Blocker is unresolved if not closed AND not in review without requiring user review
|
|
187
187
|
AND NOT (
|
|
188
188
|
blocker.status = 'closed'
|
|
189
|
-
OR (blocker.status = '
|
|
189
|
+
OR (blocker.status = 'needs_review' AND blocker.requires_user_review = 0)
|
|
190
190
|
)
|
|
191
191
|
-- Exclude ancestor blocked by any descendant (completion block, not work block)
|
|
192
192
|
AND NOT EXISTS (
|
|
@@ -266,7 +266,7 @@ def list_blocked_tasks(
|
|
|
266
266
|
-- Blocker is unresolved if not closed AND not in review without requiring user review
|
|
267
267
|
AND NOT (
|
|
268
268
|
blocker.status = 'closed'
|
|
269
|
-
OR (blocker.status = '
|
|
269
|
+
OR (blocker.status = 'needs_review' AND blocker.requires_user_review = 0)
|
|
270
270
|
)
|
|
271
271
|
-- Exclude ancestor blocked by any descendant (completion block, not work block)
|
|
272
272
|
AND NOT EXISTS (
|
gobby/sync/memories.py
CHANGED
|
@@ -131,8 +131,8 @@ class MemoryBackupManager:
|
|
|
131
131
|
from gobby.utils.project_context import get_project_context
|
|
132
132
|
|
|
133
133
|
project_ctx = get_project_context()
|
|
134
|
-
if project_ctx and project_ctx.get("
|
|
135
|
-
project_path = Path(project_ctx["
|
|
134
|
+
if project_ctx and project_ctx.get("project_path"):
|
|
135
|
+
project_path = Path(project_ctx["project_path"]).expanduser().resolve()
|
|
136
136
|
return project_path / self.export_path
|
|
137
137
|
except Exception:
|
|
138
138
|
pass # nosec B110 - fall back to cwd if project context unavailable
|
|
@@ -289,7 +289,8 @@ class MemoryBackupManager:
|
|
|
289
289
|
return 0
|
|
290
290
|
|
|
291
291
|
try:
|
|
292
|
-
memories
|
|
292
|
+
# Use high limit to export all memories for backup (default is 50)
|
|
293
|
+
memories = self.memory_manager.list_memories(limit=10000)
|
|
293
294
|
|
|
294
295
|
# Deduplicate by content before export
|
|
295
296
|
unique_memories = self._deduplicate_memories(memories)
|
gobby/tasks/commits.py
CHANGED
|
@@ -487,30 +487,49 @@ def extract_mentioned_symbols(task: dict[str, Any]) -> list[str]:
|
|
|
487
487
|
|
|
488
488
|
|
|
489
489
|
# Task ID patterns to search for in commit messages
|
|
490
|
-
#
|
|
490
|
+
# Uses {project}-#N format to avoid GitHub auto-linking and match CLI display format
|
|
491
|
+
# Patterns capture both project name and task number for validation
|
|
491
492
|
TASK_ID_PATTERNS = [
|
|
492
|
-
# [
|
|
493
|
-
r"\[
|
|
494
|
-
#
|
|
495
|
-
r"(?:^|\s)
|
|
496
|
-
# Implements/Fixes/Closes/Refs
|
|
497
|
-
r"(?:implements|fixes|closes|refs)\s
|
|
498
|
-
# Standalone #N after whitespace (with word boundary to avoid false positives)
|
|
499
|
-
r"(?:^|\s)#(\d+)\b(?![\d.])",
|
|
493
|
+
# [project-#N] - bracket format (primary)
|
|
494
|
+
r"\[(\w+)-#(\d+)\]",
|
|
495
|
+
# project-#N - standalone format (word boundary before, after digits)
|
|
496
|
+
r"(?:^|\s)(\w+)-#(\d+)\b",
|
|
497
|
+
# Implements/Fixes/Closes/Refs project-#N
|
|
498
|
+
r"(?:implements|fixes|closes|refs)\s+(\w+)-#(\d+)",
|
|
500
499
|
]
|
|
501
500
|
|
|
502
501
|
|
|
503
|
-
def
|
|
502
|
+
def get_current_project_name() -> str | None:
|
|
503
|
+
"""Get current project name from context.
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
Project name or None if not in a project.
|
|
507
|
+
"""
|
|
508
|
+
from gobby.utils.project_context import get_project_context
|
|
509
|
+
|
|
510
|
+
ctx = get_project_context()
|
|
511
|
+
if ctx and ctx.get("name"):
|
|
512
|
+
name: str = ctx["name"]
|
|
513
|
+
return name
|
|
514
|
+
return None
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def extract_task_ids_from_message(
|
|
518
|
+
message: str,
|
|
519
|
+
project_name: str | None = None,
|
|
520
|
+
) -> list[str]:
|
|
504
521
|
"""Extract task IDs from a commit message.
|
|
505
522
|
|
|
506
523
|
Supports patterns:
|
|
507
|
-
- [
|
|
508
|
-
-
|
|
509
|
-
- Implements/Fixes/Closes/Refs
|
|
510
|
-
- Multiple references: #1, #2, #3
|
|
524
|
+
- [project-#N] - bracket format (primary)
|
|
525
|
+
- project-#N - standalone format
|
|
526
|
+
- Implements/Fixes/Closes/Refs project-#N
|
|
511
527
|
|
|
512
528
|
Args:
|
|
513
529
|
message: Commit message to parse.
|
|
530
|
+
project_name: Optional project name to filter matches. If provided,
|
|
531
|
+
only returns task IDs from commits referencing this project.
|
|
532
|
+
If None, returns all task IDs found regardless of project.
|
|
514
533
|
|
|
515
534
|
Returns:
|
|
516
535
|
List of unique task references found (e.g., ["#1", "#42"]).
|
|
@@ -520,8 +539,13 @@ def extract_task_ids_from_message(message: str) -> list[str]:
|
|
|
520
539
|
for pattern in TASK_ID_PATTERNS:
|
|
521
540
|
matches = re.findall(pattern, message, re.IGNORECASE | re.MULTILINE)
|
|
522
541
|
for match in matches:
|
|
542
|
+
# match is a tuple: (project, task_number)
|
|
543
|
+
found_project, task_num = match
|
|
544
|
+
# Filter by project name if specified
|
|
545
|
+
if project_name and found_project.lower() != project_name.lower():
|
|
546
|
+
continue
|
|
523
547
|
# Format as #N
|
|
524
|
-
task_id = f"#{
|
|
548
|
+
task_id = f"#{task_num}"
|
|
525
549
|
task_ids.add(task_id)
|
|
526
550
|
|
|
527
551
|
return list(task_ids)
|
|
@@ -547,6 +571,7 @@ def auto_link_commits(
|
|
|
547
571
|
task_id: str | None = None,
|
|
548
572
|
since: str | None = None,
|
|
549
573
|
cwd: str | Path | None = None,
|
|
574
|
+
project_name: str | None = None,
|
|
550
575
|
) -> AutoLinkResult:
|
|
551
576
|
"""Auto-detect and link commits that mention task IDs.
|
|
552
577
|
|
|
@@ -558,12 +583,18 @@ def auto_link_commits(
|
|
|
558
583
|
task_id: Optional specific task ID to filter for.
|
|
559
584
|
since: Optional git --since parameter (e.g., "1 week ago", "2024-01-01").
|
|
560
585
|
cwd: Working directory for git commands.
|
|
586
|
+
project_name: Optional project name to filter commits. If not provided,
|
|
587
|
+
auto-detects from current project context.
|
|
561
588
|
|
|
562
589
|
Returns:
|
|
563
590
|
AutoLinkResult with details of linked and skipped commits.
|
|
564
591
|
"""
|
|
565
592
|
working_dir = Path(cwd) if cwd else Path.cwd()
|
|
566
593
|
|
|
594
|
+
# Get project name for filtering (auto-detect if not provided)
|
|
595
|
+
if project_name is None:
|
|
596
|
+
project_name = get_current_project_name()
|
|
597
|
+
|
|
567
598
|
# Build git log command
|
|
568
599
|
# Format: "sha|message" for easy parsing
|
|
569
600
|
git_cmd = ["git", "log", "--pretty=format:%h|%s"]
|
|
@@ -590,8 +621,8 @@ def auto_link_commits(
|
|
|
590
621
|
|
|
591
622
|
commit_sha, message = parts
|
|
592
623
|
|
|
593
|
-
# Extract task IDs from message
|
|
594
|
-
found_task_ids = extract_task_ids_from_message(message)
|
|
624
|
+
# Extract task IDs from message (filtered by project name)
|
|
625
|
+
found_task_ids = extract_task_ids_from_message(message, project_name)
|
|
595
626
|
|
|
596
627
|
if not found_task_ids:
|
|
597
628
|
continue
|
|
@@ -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(
|