gobby 0.2.6__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/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/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/install.py +4 -4
- gobby/cli/installers/claude.py +6 -0
- gobby/cli/installers/gemini.py +6 -0
- gobby/cli/installers/shared.py +103 -4
- gobby/cli/sessions.py +1 -1
- gobby/cli/utils.py +9 -2
- gobby/config/__init__.py +12 -97
- gobby/config/app.py +10 -94
- gobby/config/extensions.py +2 -2
- gobby/config/features.py +7 -130
- gobby/config/tasks.py +4 -28
- gobby/hooks/__init__.py +0 -13
- gobby/hooks/event_handlers.py +45 -2
- gobby/hooks/hook_manager.py +2 -2
- gobby/hooks/plugins.py +1 -1
- gobby/hooks/webhooks.py +1 -1
- gobby/llm/resolver.py +3 -2
- gobby/mcp_proxy/importer.py +62 -4
- gobby/mcp_proxy/instructions.py +2 -0
- gobby/mcp_proxy/registries.py +1 -4
- gobby/mcp_proxy/services/recommendation.py +43 -11
- gobby/mcp_proxy/tools/agents.py +31 -731
- gobby/mcp_proxy/tools/clones.py +0 -385
- gobby/mcp_proxy/tools/memory.py +2 -2
- 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 +14 -29
- gobby/mcp_proxy/tools/spawn_agent.py +417 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +52 -18
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +1 -1
- gobby/mcp_proxy/tools/worktrees.py +0 -343
- gobby/memory/ingestion/__init__.py +5 -0
- gobby/memory/ingestion/multimodal.py +221 -0
- gobby/memory/manager.py +62 -283
- 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/servers/http.py +1 -4
- gobby/servers/routes/admin.py +14 -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 -1506
- gobby/sessions/lifecycle.py +1 -1
- gobby/sessions/processor.py +10 -0
- gobby/sessions/transcripts/base.py +1 -0
- gobby/sessions/transcripts/claude.py +15 -5
- gobby/skills/parser.py +30 -2
- gobby/storage/migrations.py +159 -372
- gobby/storage/sessions.py +43 -7
- gobby/storage/skills.py +37 -4
- gobby/storage/tasks/_lifecycle.py +18 -3
- gobby/sync/memories.py +1 -1
- gobby/tasks/external_validator.py +1 -1
- gobby/tasks/validation.py +22 -20
- gobby/tools/summarizer.py +91 -10
- gobby/utils/project_context.py +2 -3
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +221 -1217
- gobby/workflows/artifact_actions.py +31 -0
- gobby/workflows/autonomous_actions.py +11 -0
- gobby/workflows/context_actions.py +50 -1
- 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/enforcement/task_policy.py +542 -0
- gobby/workflows/git_utils.py +106 -0
- gobby/workflows/llm_actions.py +30 -0
- gobby/workflows/mcp_actions.py +20 -1
- gobby/workflows/memory_actions.py +80 -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 +94 -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.6.dist-info → gobby-0.2.7.dist-info}/METADATA +6 -1
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/RECORD +111 -111
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
- gobby/adapters/codex.py +0 -1332
- 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/mcp_proxy/tools/session_messages.py +0 -1055
- 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/handoff/compact.md +0 -63
- gobby/prompts/defaults/handoff/session_end.md +0 -57
- gobby/prompts/defaults/memory/extract.md +0 -61
- 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/workflows/task_enforcement_actions.py +0 -1343
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
gobby/storage/sessions.py
CHANGED
|
@@ -52,6 +52,8 @@ class Session:
|
|
|
52
52
|
terminal_context: dict[str, Any] | None = None
|
|
53
53
|
# Global sequence number
|
|
54
54
|
seq_num: int | None = None
|
|
55
|
+
# Edit history tracking
|
|
56
|
+
had_edits: bool = False
|
|
55
57
|
|
|
56
58
|
@classmethod
|
|
57
59
|
def from_row(cls, row: Any) -> Session:
|
|
@@ -86,6 +88,7 @@ class Session:
|
|
|
86
88
|
model=row["model"] if "model" in row.keys() else None,
|
|
87
89
|
terminal_context=cls._parse_terminal_context(row["terminal_context"]),
|
|
88
90
|
seq_num=row["seq_num"] if "seq_num" in row.keys() else None,
|
|
91
|
+
had_edits=bool(row["had_edits"]) if "had_edits" in row.keys() else False,
|
|
89
92
|
)
|
|
90
93
|
|
|
91
94
|
@classmethod
|
|
@@ -135,6 +138,7 @@ class Session:
|
|
|
135
138
|
"usage_cache_read_tokens": self.usage_cache_read_tokens,
|
|
136
139
|
"usage_total_cost_usd": self.usage_total_cost_usd,
|
|
137
140
|
"terminal_context": self.terminal_context,
|
|
141
|
+
"had_edits": self.had_edits,
|
|
138
142
|
"created_at": self.created_at,
|
|
139
143
|
"updated_at": self.updated_at,
|
|
140
144
|
"seq_num": self.seq_num,
|
|
@@ -224,8 +228,11 @@ class LocalSessionManager:
|
|
|
224
228
|
max_retries = 3
|
|
225
229
|
for attempt in range(max_retries):
|
|
226
230
|
try:
|
|
227
|
-
# Get next seq_num (
|
|
228
|
-
max_seq_row = self.db.fetchone(
|
|
231
|
+
# Get next seq_num (per-project)
|
|
232
|
+
max_seq_row = self.db.fetchone(
|
|
233
|
+
"SELECT MAX(seq_num) as max_seq FROM sessions WHERE project_id = ?",
|
|
234
|
+
(project_id,),
|
|
235
|
+
)
|
|
229
236
|
next_seq_num = ((max_seq_row["max_seq"] if max_seq_row else None) or 0) + 1
|
|
230
237
|
|
|
231
238
|
self.db.execute(
|
|
@@ -234,9 +241,9 @@ class LocalSessionManager:
|
|
|
234
241
|
id, external_id, machine_id, source, project_id, title,
|
|
235
242
|
jsonl_path, git_branch, parent_session_id,
|
|
236
243
|
agent_depth, spawned_by_agent_id, terminal_context,
|
|
237
|
-
status, created_at, updated_at, seq_num
|
|
244
|
+
status, created_at, updated_at, seq_num, had_edits
|
|
238
245
|
)
|
|
239
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?,
|
|
246
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, 0)
|
|
240
247
|
""",
|
|
241
248
|
(
|
|
242
249
|
session_id,
|
|
@@ -279,18 +286,20 @@ class LocalSessionManager:
|
|
|
279
286
|
row = self.db.fetchone("SELECT * FROM sessions WHERE id = ?", (session_id,))
|
|
280
287
|
return Session.from_row(row) if row else None
|
|
281
288
|
|
|
282
|
-
def resolve_session_reference(self, ref: str) -> str:
|
|
289
|
+
def resolve_session_reference(self, ref: str, project_id: str | None = None) -> str:
|
|
283
290
|
"""
|
|
284
291
|
Resolve a session reference to a UUID.
|
|
285
292
|
|
|
286
293
|
Supports:
|
|
287
|
-
- #N:
|
|
294
|
+
- #N: Project-scoped Sequence Number (e.g., #1) - requires project_id
|
|
288
295
|
- N: Integer string treated as #N (e.g., "1")
|
|
289
296
|
- UUID: Full UUID
|
|
290
297
|
- Prefix: UUID prefix (must be unambiguous)
|
|
291
298
|
|
|
292
299
|
Args:
|
|
293
300
|
ref: Session reference string
|
|
301
|
+
project_id: Project ID for project-scoped #N lookup.
|
|
302
|
+
If not provided, falls back to global lookup for backwards compat.
|
|
294
303
|
|
|
295
304
|
Returns:
|
|
296
305
|
Resolved Session UUID
|
|
@@ -308,7 +317,15 @@ class LocalSessionManager:
|
|
|
308
317
|
|
|
309
318
|
if seq_num_ref.isdigit():
|
|
310
319
|
seq_num = int(seq_num_ref)
|
|
311
|
-
|
|
320
|
+
if project_id:
|
|
321
|
+
# Project-scoped lookup
|
|
322
|
+
row = self.db.fetchone(
|
|
323
|
+
"SELECT id FROM sessions WHERE project_id = ? AND seq_num = ?",
|
|
324
|
+
(project_id, seq_num),
|
|
325
|
+
)
|
|
326
|
+
else:
|
|
327
|
+
# Fallback to global lookup for backwards compat
|
|
328
|
+
row = self.db.fetchone("SELECT id FROM sessions WHERE seq_num = ?", (seq_num,))
|
|
312
329
|
if not row:
|
|
313
330
|
raise ValueError(f"Session #{seq_num} not found")
|
|
314
331
|
return str(row["id"])
|
|
@@ -426,6 +443,15 @@ class LocalSessionManager:
|
|
|
426
443
|
)
|
|
427
444
|
return self.get(session_id)
|
|
428
445
|
|
|
446
|
+
def mark_had_edits(self, session_id: str) -> Session | None:
|
|
447
|
+
"""Mark session as having edits."""
|
|
448
|
+
now = datetime.now(UTC).isoformat()
|
|
449
|
+
self.db.execute(
|
|
450
|
+
"UPDATE sessions SET had_edits = 1, updated_at = ? WHERE id = ?",
|
|
451
|
+
(now, session_id),
|
|
452
|
+
)
|
|
453
|
+
return self.get(session_id)
|
|
454
|
+
|
|
429
455
|
def update_title(self, session_id: str, title: str) -> Session | None:
|
|
430
456
|
"""Update session title."""
|
|
431
457
|
now = datetime.now(UTC).isoformat()
|
|
@@ -435,6 +461,16 @@ class LocalSessionManager:
|
|
|
435
461
|
)
|
|
436
462
|
return self.get(session_id)
|
|
437
463
|
|
|
464
|
+
def update_model(self, session_id: str, model: str) -> Session | None:
|
|
465
|
+
"""Update session model (LLM model used)."""
|
|
466
|
+
now = datetime.now(UTC).isoformat()
|
|
467
|
+
with self.db.transaction():
|
|
468
|
+
self.db.execute(
|
|
469
|
+
"UPDATE sessions SET model = ?, updated_at = ? WHERE id = ?",
|
|
470
|
+
(model, now, session_id),
|
|
471
|
+
)
|
|
472
|
+
return self.get(session_id)
|
|
473
|
+
|
|
438
474
|
def update_summary(
|
|
439
475
|
self,
|
|
440
476
|
session_id: str,
|
gobby/storage/skills.py
CHANGED
|
@@ -149,9 +149,18 @@ class Skill:
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
def get_category(self) -> str | None:
|
|
152
|
-
"""Get the skill category from metadata.skillport.category.
|
|
152
|
+
"""Get the skill category from top-level or metadata.skillport.category.
|
|
153
|
+
|
|
154
|
+
Supports both top-level category and nested metadata.skillport.category.
|
|
155
|
+
Top-level takes precedence.
|
|
156
|
+
"""
|
|
153
157
|
if not self.metadata:
|
|
154
158
|
return None
|
|
159
|
+
# Check top-level first
|
|
160
|
+
result = self.metadata.get("category")
|
|
161
|
+
if result is not None:
|
|
162
|
+
return str(result)
|
|
163
|
+
# Fall back to nested skillport.category
|
|
155
164
|
skillport = self.metadata.get("skillport", {})
|
|
156
165
|
result = skillport.get("category")
|
|
157
166
|
return str(result) if result is not None else None
|
|
@@ -165,9 +174,18 @@ class Skill:
|
|
|
165
174
|
return list(tags) if isinstance(tags, list) else []
|
|
166
175
|
|
|
167
176
|
def is_always_apply(self) -> bool:
|
|
168
|
-
"""Check if this is a core skill that should always be applied.
|
|
177
|
+
"""Check if this is a core skill that should always be applied.
|
|
178
|
+
|
|
179
|
+
Supports both top-level alwaysApply and nested metadata.skillport.alwaysApply.
|
|
180
|
+
Top-level takes precedence.
|
|
181
|
+
"""
|
|
169
182
|
if not self.metadata:
|
|
170
183
|
return False
|
|
184
|
+
# Check top-level first
|
|
185
|
+
top_level = self.metadata.get("alwaysApply")
|
|
186
|
+
if top_level is not None:
|
|
187
|
+
return bool(top_level)
|
|
188
|
+
# Fall back to nested skillport.alwaysApply
|
|
171
189
|
skillport = self.metadata.get("skillport", {})
|
|
172
190
|
return bool(skillport.get("alwaysApply", False))
|
|
173
191
|
|
|
@@ -465,21 +483,32 @@ class LocalSkillManager:
|
|
|
465
483
|
self,
|
|
466
484
|
name: str,
|
|
467
485
|
project_id: str | None = None,
|
|
486
|
+
include_global: bool = True,
|
|
468
487
|
) -> Skill | None:
|
|
469
488
|
"""Get a skill by name within a project scope.
|
|
470
489
|
|
|
471
490
|
Args:
|
|
472
491
|
name: The skill name
|
|
473
492
|
project_id: Project scope (None for global)
|
|
493
|
+
include_global: Include global skills when project_id is set.
|
|
494
|
+
When True and project_id is set, first looks for
|
|
495
|
+
project-scoped skill, then falls back to global.
|
|
474
496
|
|
|
475
497
|
Returns:
|
|
476
498
|
The Skill if found, None otherwise
|
|
477
499
|
"""
|
|
478
500
|
if project_id:
|
|
501
|
+
# First try project-scoped skill
|
|
479
502
|
row = self.db.fetchone(
|
|
480
503
|
"SELECT * FROM skills WHERE name = ? AND project_id = ?",
|
|
481
504
|
(name, project_id),
|
|
482
505
|
)
|
|
506
|
+
# If not found and include_global, try global
|
|
507
|
+
if row is None and include_global:
|
|
508
|
+
row = self.db.fetchone(
|
|
509
|
+
"SELECT * FROM skills WHERE name = ? AND project_id IS NULL",
|
|
510
|
+
(name,),
|
|
511
|
+
)
|
|
483
512
|
else:
|
|
484
513
|
row = self.db.fetchone(
|
|
485
514
|
"SELECT * FROM skills WHERE name = ? AND project_id IS NULL",
|
|
@@ -649,9 +678,13 @@ class LocalSkillManager:
|
|
|
649
678
|
params.append(enabled)
|
|
650
679
|
|
|
651
680
|
# Filter by category using JSON extraction in SQL to avoid under-filled results
|
|
681
|
+
# Check both top-level $.category and nested $.skillport.category
|
|
652
682
|
if category:
|
|
653
|
-
query += " AND
|
|
654
|
-
|
|
683
|
+
query += """ AND (
|
|
684
|
+
json_extract(metadata, '$.category') = ?
|
|
685
|
+
OR json_extract(metadata, '$.skillport.category') = ?
|
|
686
|
+
)"""
|
|
687
|
+
params.extend([category, category])
|
|
655
688
|
|
|
656
689
|
query += " ORDER BY name ASC LIMIT ? OFFSET ?"
|
|
657
690
|
params.extend([limit, offset])
|
|
@@ -275,7 +275,11 @@ def unlink_commit(
|
|
|
275
275
|
|
|
276
276
|
|
|
277
277
|
def delete_task(
|
|
278
|
-
db: DatabaseProtocol,
|
|
278
|
+
db: DatabaseProtocol,
|
|
279
|
+
task_id: str,
|
|
280
|
+
cascade: bool = False,
|
|
281
|
+
unlink: bool = False,
|
|
282
|
+
_visited: set[str] | None = None,
|
|
279
283
|
) -> bool:
|
|
280
284
|
"""Delete a task.
|
|
281
285
|
|
|
@@ -285,6 +289,8 @@ def delete_task(
|
|
|
285
289
|
cascade: If True, delete children AND dependent tasks recursively
|
|
286
290
|
unlink: If True, remove dependency links but preserve dependent tasks
|
|
287
291
|
(ignored if cascade=True)
|
|
292
|
+
_visited: Internal parameter to track visited tasks and prevent infinite recursion
|
|
293
|
+
when a parent task depends on its children (circular dependency)
|
|
288
294
|
|
|
289
295
|
Returns:
|
|
290
296
|
True if task was deleted, False if task not found.
|
|
@@ -292,6 +298,15 @@ def delete_task(
|
|
|
292
298
|
Raises:
|
|
293
299
|
ValueError: If task has children or dependents and neither cascade nor unlink is True.
|
|
294
300
|
"""
|
|
301
|
+
# Initialize visited set on first call to prevent infinite recursion
|
|
302
|
+
if _visited is None:
|
|
303
|
+
_visited = set()
|
|
304
|
+
|
|
305
|
+
# Skip if already being deleted (prevents cycles when parent depends on children)
|
|
306
|
+
if task_id in _visited:
|
|
307
|
+
return True
|
|
308
|
+
_visited.add(task_id)
|
|
309
|
+
|
|
295
310
|
# Check if task exists first
|
|
296
311
|
existing = db.fetchone("SELECT 1 FROM tasks WHERE id = ?", (task_id,))
|
|
297
312
|
if not existing:
|
|
@@ -326,7 +341,7 @@ def delete_task(
|
|
|
326
341
|
# Recursive delete children
|
|
327
342
|
children = db.fetchall("SELECT id FROM tasks WHERE parent_task_id = ?", (task_id,))
|
|
328
343
|
for child in children:
|
|
329
|
-
delete_task(db, child["id"], cascade=True)
|
|
344
|
+
delete_task(db, child["id"], cascade=True, _visited=_visited)
|
|
330
345
|
|
|
331
346
|
# Delete tasks that depend on this task (only 'blocks' dependencies)
|
|
332
347
|
dependents = db.fetchall(
|
|
@@ -336,7 +351,7 @@ def delete_task(
|
|
|
336
351
|
(task_id,),
|
|
337
352
|
)
|
|
338
353
|
for dep in dependents:
|
|
339
|
-
delete_task(db, dep["id"], cascade=True)
|
|
354
|
+
delete_task(db, dep["id"], cascade=True, _visited=_visited)
|
|
340
355
|
|
|
341
356
|
# Note: if unlink=True, dependency links are removed by ON DELETE CASCADE
|
|
342
357
|
# when the task is deleted - no explicit action needed
|
gobby/sync/memories.py
CHANGED
|
@@ -29,7 +29,7 @@ __all__ = [
|
|
|
29
29
|
# TODO: Rename MemorySyncConfig to MemoryBackupConfig in gobby.config.persistence
|
|
30
30
|
# for consistency with MemoryBackupManager naming. Keeping current name for now
|
|
31
31
|
# to minimize breaking changes across the codebase.
|
|
32
|
-
from gobby.config.
|
|
32
|
+
from gobby.config.persistence import MemorySyncConfig
|
|
33
33
|
from gobby.memory.manager import MemoryManager
|
|
34
34
|
from gobby.storage.database import DatabaseProtocol
|
|
35
35
|
|
|
@@ -14,7 +14,7 @@ from dataclasses import dataclass, field
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import TYPE_CHECKING, Any
|
|
16
16
|
|
|
17
|
-
from gobby.config.
|
|
17
|
+
from gobby.config.tasks import TaskValidationConfig
|
|
18
18
|
from gobby.llm import LLMService
|
|
19
19
|
from gobby.prompts import PromptLoader
|
|
20
20
|
from gobby.tasks.commits import (
|
gobby/tasks/validation.py
CHANGED
|
@@ -20,7 +20,7 @@ from dataclasses import dataclass
|
|
|
20
20
|
from pathlib import Path
|
|
21
21
|
from typing import Literal
|
|
22
22
|
|
|
23
|
-
from gobby.config.
|
|
23
|
+
from gobby.config.tasks import TaskValidationConfig
|
|
24
24
|
from gobby.llm import LLMService
|
|
25
25
|
from gobby.prompts import PromptLoader
|
|
26
26
|
from gobby.utils.json_helpers import extract_json_object
|
|
@@ -589,14 +589,8 @@ class TaskValidator:
|
|
|
589
589
|
category_section += "\n"
|
|
590
590
|
|
|
591
591
|
# Build prompt using PromptLoader or legacy config
|
|
592
|
-
if self.config.
|
|
593
|
-
|
|
594
|
-
prompt = self.config.prompt
|
|
595
|
-
if file_context:
|
|
596
|
-
prompt += f"\nFile Context:\n{file_context[:50000]}\n"
|
|
597
|
-
else:
|
|
598
|
-
# Use PromptLoader
|
|
599
|
-
prompt_path = self.config.prompt_path or "validation/validate"
|
|
592
|
+
if self.config.prompt_path:
|
|
593
|
+
prompt_path = self.config.prompt_path
|
|
600
594
|
template_context = {
|
|
601
595
|
"title": title,
|
|
602
596
|
"category_section": category_section,
|
|
@@ -611,6 +605,18 @@ class TaskValidator:
|
|
|
611
605
|
prompt = DEFAULT_VALIDATE_PROMPT.format(**template_context)
|
|
612
606
|
if file_context:
|
|
613
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"
|
|
614
620
|
|
|
615
621
|
try:
|
|
616
622
|
provider = self.llm_service.get_provider(self.config.provider)
|
|
@@ -670,17 +676,13 @@ class TaskValidator:
|
|
|
670
676
|
"description": description or "(no description)",
|
|
671
677
|
}
|
|
672
678
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
prompt_path
|
|
679
|
-
|
|
680
|
-
prompt = self._loader.render(prompt_path, template_context)
|
|
681
|
-
except FileNotFoundError:
|
|
682
|
-
logger.debug(f"Prompt template '{prompt_path}' not found, using fallback")
|
|
683
|
-
prompt = DEFAULT_CRITERIA_PROMPT.format(**template_context)
|
|
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)
|
|
684
686
|
|
|
685
687
|
try:
|
|
686
688
|
provider = self.llm_service.get_provider(self.config.provider)
|
gobby/tools/summarizer.py
CHANGED
|
@@ -11,7 +11,8 @@ import logging
|
|
|
11
11
|
from typing import TYPE_CHECKING, Any
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
|
-
from gobby.config.
|
|
14
|
+
from gobby.config.features import ToolSummarizerConfig
|
|
15
|
+
from gobby.prompts import PromptLoader
|
|
15
16
|
|
|
16
17
|
logger = logging.getLogger(__name__)
|
|
17
18
|
|
|
@@ -20,12 +21,42 @@ MAX_DESCRIPTION_LENGTH = 200
|
|
|
20
21
|
|
|
21
22
|
# Module-level config reference (set by init_summarizer_config)
|
|
22
23
|
_config: ToolSummarizerConfig | None = None
|
|
24
|
+
_loader: PromptLoader | None = None
|
|
23
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.
|
|
24
29
|
|
|
25
|
-
|
|
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
|
+
|
|
46
|
+
def init_summarizer_config(config: ToolSummarizerConfig, project_dir: str | None = None) -> None:
|
|
26
47
|
"""Initialize the summarizer with configuration."""
|
|
27
|
-
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
|
|
50
|
+
global _config, _loader
|
|
28
51
|
_config = config
|
|
52
|
+
_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
|
+
)
|
|
29
60
|
|
|
30
61
|
|
|
31
62
|
def _get_config() -> ToolSummarizerConfig:
|
|
@@ -33,7 +64,12 @@ def _get_config() -> ToolSummarizerConfig:
|
|
|
33
64
|
if _config is not None:
|
|
34
65
|
return _config
|
|
35
66
|
# Import here to avoid circular imports
|
|
36
|
-
from gobby.config.
|
|
67
|
+
from gobby.config.features import ToolSummarizerConfig
|
|
68
|
+
|
|
69
|
+
# Ensure loader defaults exist even if init wasn't called
|
|
70
|
+
global _loader
|
|
71
|
+
if _loader is None:
|
|
72
|
+
_loader = PromptLoader()
|
|
37
73
|
|
|
38
74
|
return ToolSummarizerConfig()
|
|
39
75
|
|
|
@@ -53,11 +89,30 @@ async def _summarize_description_with_claude(description: str) -> str:
|
|
|
53
89
|
try:
|
|
54
90
|
from claude_agent_sdk import AssistantMessage, ClaudeAgentOptions, TextBlock, query
|
|
55
91
|
|
|
56
|
-
|
|
92
|
+
# Get summary prompt
|
|
93
|
+
prompt_path = config.prompt_path or "features/tool_summary"
|
|
94
|
+
try:
|
|
95
|
+
# We assume _loader is initialized by _get_config() logic or init
|
|
96
|
+
if _loader is None:
|
|
97
|
+
raise RuntimeError("Summarizer not initialized")
|
|
98
|
+
prompt = _loader.render(prompt_path, {"description": description})
|
|
99
|
+
except (FileNotFoundError, OSError, KeyError, ValueError, RuntimeError) as e:
|
|
100
|
+
logger.debug(f"Failed to load prompt from {prompt_path}: {e}, using default")
|
|
101
|
+
prompt = DEFAULT_SUMMARY_PROMPT.format(description=description)
|
|
102
|
+
|
|
103
|
+
# Get system prompt
|
|
104
|
+
sys_prompt_path = config.system_prompt_path or "features/tool_summary_system"
|
|
105
|
+
try:
|
|
106
|
+
if _loader is None:
|
|
107
|
+
raise RuntimeError("Summarizer not initialized")
|
|
108
|
+
system_prompt = _loader.render(sys_prompt_path, {})
|
|
109
|
+
except (FileNotFoundError, OSError, KeyError, ValueError, RuntimeError) as e:
|
|
110
|
+
logger.debug(f"Failed to load system prompt from {sys_prompt_path}: {e}, using default")
|
|
111
|
+
system_prompt = DEFAULT_SUMMARY_SYSTEM_PROMPT
|
|
57
112
|
|
|
58
113
|
# Configure for single-turn completion
|
|
59
114
|
options = ClaudeAgentOptions(
|
|
60
|
-
system_prompt=
|
|
115
|
+
system_prompt=system_prompt,
|
|
61
116
|
max_turns=1,
|
|
62
117
|
model=config.model,
|
|
63
118
|
allowed_tools=[],
|
|
@@ -137,14 +192,40 @@ async def generate_server_description(
|
|
|
137
192
|
# Build tools list for prompt
|
|
138
193
|
tools_list = "\n".join([f"- {t['name']}: {t['description']}" for t in tool_summaries])
|
|
139
194
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
195
|
+
# Build prompt
|
|
196
|
+
prompt_path = config.server_description_prompt_path or "features/server_description"
|
|
197
|
+
context = {
|
|
198
|
+
"server_name": server_name,
|
|
199
|
+
"tools_list": tools_list,
|
|
200
|
+
}
|
|
201
|
+
try:
|
|
202
|
+
if _loader is None:
|
|
203
|
+
_get_config() # force init
|
|
204
|
+
if _loader is None:
|
|
205
|
+
# Still None after _get_config, use default
|
|
206
|
+
prompt = DEFAULT_SERVER_DESC_PROMPT.format(**context)
|
|
207
|
+
else:
|
|
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)
|
|
212
|
+
|
|
213
|
+
# Get system prompt
|
|
214
|
+
sys_prompt_path = (
|
|
215
|
+
config.server_description_system_prompt_path or "features/server_description_system"
|
|
143
216
|
)
|
|
217
|
+
try:
|
|
218
|
+
if _loader is None:
|
|
219
|
+
system_prompt = DEFAULT_SERVER_DESC_SYSTEM_PROMPT
|
|
220
|
+
else:
|
|
221
|
+
system_prompt = _loader.render(sys_prompt_path, {})
|
|
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
|
|
144
225
|
|
|
145
226
|
# Configure for single-turn completion
|
|
146
227
|
options = ClaudeAgentOptions(
|
|
147
|
-
system_prompt=
|
|
228
|
+
system_prompt=system_prompt,
|
|
148
229
|
max_turns=1,
|
|
149
230
|
model=config.model,
|
|
150
231
|
allowed_tools=[],
|
gobby/utils/project_context.py
CHANGED
|
@@ -10,8 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
from typing import TYPE_CHECKING, Any, cast
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
|
-
from gobby.config.
|
|
14
|
-
from gobby.config.features import HooksConfig
|
|
13
|
+
from gobby.config.features import HooksConfig, ProjectVerificationConfig
|
|
15
14
|
|
|
16
15
|
logger = logging.getLogger(__name__)
|
|
17
16
|
|
|
@@ -138,7 +137,7 @@ def get_verification_config(cwd: Path | None = None) -> ProjectVerificationConfi
|
|
|
138
137
|
Returns:
|
|
139
138
|
ProjectVerificationConfig if verification section exists, None otherwise.
|
|
140
139
|
"""
|
|
141
|
-
from gobby.config.
|
|
140
|
+
from gobby.config.features import ProjectVerificationConfig
|
|
142
141
|
|
|
143
142
|
context = get_project_context(cwd)
|
|
144
143
|
if not context:
|
gobby/utils/status.py
CHANGED
|
@@ -79,6 +79,11 @@ def fetch_rich_status(http_port: int, timeout: float = 2.0) -> dict[str, Any]:
|
|
|
79
79
|
status_kwargs["memories_count"] = memory.get("count", 0)
|
|
80
80
|
status_kwargs["memories_avg_importance"] = memory.get("avg_importance", 0.0)
|
|
81
81
|
|
|
82
|
+
# Skills
|
|
83
|
+
skills_data = data.get("skills", {})
|
|
84
|
+
if skills_data:
|
|
85
|
+
status_kwargs["skills_total"] = skills_data.get("total", 0)
|
|
86
|
+
|
|
82
87
|
except (httpx.ConnectError, httpx.TimeoutException):
|
|
83
88
|
# Daemon not responding - return empty
|
|
84
89
|
pass
|
|
@@ -117,6 +122,8 @@ def format_status_message(
|
|
|
117
122
|
# Memory
|
|
118
123
|
memories_count: int | None = None,
|
|
119
124
|
memories_avg_importance: float | None = None,
|
|
125
|
+
# Skills
|
|
126
|
+
skills_total: int | None = None,
|
|
120
127
|
**kwargs: Any,
|
|
121
128
|
) -> str:
|
|
122
129
|
"""
|
|
@@ -202,6 +209,12 @@ def format_status_message(
|
|
|
202
209
|
lines.append(f" Unhealthy: {unhealthy_str}")
|
|
203
210
|
lines.append("")
|
|
204
211
|
|
|
212
|
+
# Skills section (only show if we have data)
|
|
213
|
+
if skills_total is not None:
|
|
214
|
+
lines.append("Skills:")
|
|
215
|
+
lines.append(f" Loaded: {skills_total}")
|
|
216
|
+
lines.append("")
|
|
217
|
+
|
|
205
218
|
# Sessions section (only show if we have data)
|
|
206
219
|
if sessions_active is not None or sessions_paused is not None:
|
|
207
220
|
lines.append("Sessions:")
|