gobby 0.2.5__py3-none-any.whl → 0.2.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. gobby/adapters/claude_code.py +13 -4
  2. gobby/adapters/codex.py +43 -3
  3. gobby/agents/runner.py +8 -0
  4. gobby/cli/__init__.py +6 -0
  5. gobby/cli/clones.py +419 -0
  6. gobby/cli/conductor.py +266 -0
  7. gobby/cli/installers/antigravity.py +3 -9
  8. gobby/cli/installers/claude.py +9 -9
  9. gobby/cli/installers/codex.py +2 -8
  10. gobby/cli/installers/gemini.py +2 -8
  11. gobby/cli/installers/shared.py +71 -8
  12. gobby/cli/skills.py +858 -0
  13. gobby/cli/tasks/ai.py +0 -440
  14. gobby/cli/tasks/crud.py +44 -6
  15. gobby/cli/tasks/main.py +0 -4
  16. gobby/cli/tui.py +2 -2
  17. gobby/cli/utils.py +3 -3
  18. gobby/clones/__init__.py +13 -0
  19. gobby/clones/git.py +547 -0
  20. gobby/conductor/__init__.py +16 -0
  21. gobby/conductor/alerts.py +135 -0
  22. gobby/conductor/loop.py +164 -0
  23. gobby/conductor/monitors/__init__.py +11 -0
  24. gobby/conductor/monitors/agents.py +116 -0
  25. gobby/conductor/monitors/tasks.py +155 -0
  26. gobby/conductor/pricing.py +234 -0
  27. gobby/conductor/token_tracker.py +160 -0
  28. gobby/config/app.py +63 -1
  29. gobby/config/search.py +110 -0
  30. gobby/config/servers.py +1 -1
  31. gobby/config/skills.py +43 -0
  32. gobby/config/tasks.py +6 -14
  33. gobby/hooks/event_handlers.py +145 -2
  34. gobby/hooks/hook_manager.py +48 -2
  35. gobby/hooks/skill_manager.py +130 -0
  36. gobby/install/claude/hooks/hook_dispatcher.py +4 -4
  37. gobby/install/codex/hooks/hook_dispatcher.py +1 -1
  38. gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
  39. gobby/llm/claude.py +22 -34
  40. gobby/llm/claude_executor.py +46 -256
  41. gobby/llm/codex_executor.py +59 -291
  42. gobby/llm/executor.py +21 -0
  43. gobby/llm/gemini.py +134 -110
  44. gobby/llm/litellm_executor.py +143 -6
  45. gobby/llm/resolver.py +95 -33
  46. gobby/mcp_proxy/instructions.py +54 -0
  47. gobby/mcp_proxy/models.py +15 -0
  48. gobby/mcp_proxy/registries.py +68 -5
  49. gobby/mcp_proxy/server.py +33 -3
  50. gobby/mcp_proxy/services/tool_proxy.py +81 -1
  51. gobby/mcp_proxy/stdio.py +2 -1
  52. gobby/mcp_proxy/tools/__init__.py +0 -2
  53. gobby/mcp_proxy/tools/agent_messaging.py +317 -0
  54. gobby/mcp_proxy/tools/clones.py +903 -0
  55. gobby/mcp_proxy/tools/memory.py +1 -24
  56. gobby/mcp_proxy/tools/metrics.py +65 -1
  57. gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
  58. gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
  59. gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
  60. gobby/mcp_proxy/tools/session_messages.py +1 -2
  61. gobby/mcp_proxy/tools/skills/__init__.py +631 -0
  62. gobby/mcp_proxy/tools/task_orchestration.py +7 -0
  63. gobby/mcp_proxy/tools/task_readiness.py +14 -0
  64. gobby/mcp_proxy/tools/task_sync.py +1 -1
  65. gobby/mcp_proxy/tools/tasks/_context.py +0 -20
  66. gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
  67. gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
  68. gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
  69. gobby/mcp_proxy/tools/tasks/_lifecycle.py +60 -29
  70. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
  71. gobby/mcp_proxy/tools/workflows.py +1 -1
  72. gobby/mcp_proxy/tools/worktrees.py +5 -0
  73. gobby/memory/backends/__init__.py +6 -1
  74. gobby/memory/backends/mem0.py +6 -1
  75. gobby/memory/extractor.py +477 -0
  76. gobby/memory/manager.py +11 -2
  77. gobby/prompts/defaults/handoff/compact.md +63 -0
  78. gobby/prompts/defaults/handoff/session_end.md +57 -0
  79. gobby/prompts/defaults/memory/extract.md +61 -0
  80. gobby/runner.py +37 -16
  81. gobby/search/__init__.py +48 -6
  82. gobby/search/backends/__init__.py +159 -0
  83. gobby/search/backends/embedding.py +225 -0
  84. gobby/search/embeddings.py +238 -0
  85. gobby/search/models.py +148 -0
  86. gobby/search/unified.py +496 -0
  87. gobby/servers/http.py +23 -8
  88. gobby/servers/routes/admin.py +280 -0
  89. gobby/servers/routes/mcp/tools.py +241 -52
  90. gobby/servers/websocket.py +2 -2
  91. gobby/sessions/analyzer.py +2 -0
  92. gobby/sessions/transcripts/base.py +1 -0
  93. gobby/sessions/transcripts/claude.py +64 -5
  94. gobby/skills/__init__.py +91 -0
  95. gobby/skills/loader.py +685 -0
  96. gobby/skills/manager.py +384 -0
  97. gobby/skills/parser.py +258 -0
  98. gobby/skills/search.py +463 -0
  99. gobby/skills/sync.py +119 -0
  100. gobby/skills/updater.py +385 -0
  101. gobby/skills/validator.py +368 -0
  102. gobby/storage/clones.py +378 -0
  103. gobby/storage/database.py +1 -1
  104. gobby/storage/memories.py +43 -13
  105. gobby/storage/migrations.py +180 -6
  106. gobby/storage/sessions.py +73 -0
  107. gobby/storage/skills.py +749 -0
  108. gobby/storage/tasks/_crud.py +4 -4
  109. gobby/storage/tasks/_lifecycle.py +41 -6
  110. gobby/storage/tasks/_manager.py +14 -5
  111. gobby/storage/tasks/_models.py +8 -3
  112. gobby/sync/memories.py +39 -4
  113. gobby/sync/tasks.py +83 -6
  114. gobby/tasks/__init__.py +1 -2
  115. gobby/tasks/validation.py +24 -15
  116. gobby/tui/api_client.py +4 -7
  117. gobby/tui/app.py +5 -3
  118. gobby/tui/screens/orchestrator.py +1 -2
  119. gobby/tui/screens/tasks.py +2 -4
  120. gobby/tui/ws_client.py +1 -1
  121. gobby/utils/daemon_client.py +2 -2
  122. gobby/workflows/actions.py +84 -2
  123. gobby/workflows/context_actions.py +43 -0
  124. gobby/workflows/detection_helpers.py +115 -31
  125. gobby/workflows/engine.py +13 -2
  126. gobby/workflows/lifecycle_evaluator.py +29 -1
  127. gobby/workflows/loader.py +19 -6
  128. gobby/workflows/memory_actions.py +74 -0
  129. gobby/workflows/summary_actions.py +17 -0
  130. gobby/workflows/task_enforcement_actions.py +448 -6
  131. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/METADATA +82 -21
  132. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/RECORD +136 -107
  133. gobby/install/codex/prompts/forget.md +0 -7
  134. gobby/install/codex/prompts/memories.md +0 -7
  135. gobby/install/codex/prompts/recall.md +0 -7
  136. gobby/install/codex/prompts/remember.md +0 -13
  137. gobby/llm/gemini_executor.py +0 -339
  138. gobby/mcp_proxy/tools/task_expansion.py +0 -591
  139. gobby/tasks/context.py +0 -747
  140. gobby/tasks/criteria.py +0 -342
  141. gobby/tasks/expansion.py +0 -626
  142. gobby/tasks/prompts/expand.py +0 -327
  143. gobby/tasks/research.py +0 -421
  144. gobby/tasks/tdd.py +0 -352
  145. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/WHEEL +0 -0
  146. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/entry_points.txt +0 -0
  147. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/licenses/LICENSE.md +0 -0
  148. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,160 @@
1
+ """Session-based token tracking for budget management.
2
+
3
+ This module provides SessionTokenTracker which aggregates usage from sessions
4
+ over time and enables budget tracking for agent spawning decisions.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from dataclasses import dataclass, field
11
+ from datetime import UTC, datetime, timedelta
12
+ from typing import Any
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class SessionTokenTracker:
19
+ """Track token usage from sessions for budget management.
20
+
21
+ This class aggregates usage data from sessions over time and provides
22
+ budget checking for agent spawning decisions.
23
+
24
+ Example:
25
+ tracker = SessionTokenTracker(
26
+ session_storage=session_manager,
27
+ daily_budget_usd=10.0,
28
+ )
29
+
30
+ # Get usage summary for last 7 days
31
+ summary = tracker.get_usage_summary(days=7)
32
+
33
+ # Check if we can spawn an agent
34
+ can_spawn, reason = tracker.can_spawn_agent()
35
+ """
36
+
37
+ session_storage: Any # LocalSessionManager
38
+ daily_budget_usd: float = 50.0 # Default daily budget in USD
39
+
40
+ # Cached data
41
+ _usage_cache: dict[str, Any] = field(default_factory=dict)
42
+ _cache_time: datetime | None = field(default=None)
43
+ _cache_ttl_seconds: int = 60 # Cache for 60 seconds
44
+
45
+ def get_usage_summary(self, days: int = 1) -> dict[str, Any]:
46
+ """Get usage summary for the specified number of days.
47
+
48
+ Args:
49
+ days: Number of days to look back (default: 1 = today)
50
+
51
+ Returns:
52
+ Dict with total cost, tokens, session count, and model breakdown
53
+ """
54
+ since = datetime.now(UTC) - timedelta(days=days)
55
+ sessions = self.session_storage.get_sessions_since(since)
56
+
57
+ total_cost = 0.0
58
+ total_input_tokens = 0
59
+ total_output_tokens = 0
60
+ total_cache_creation_tokens = 0
61
+ total_cache_read_tokens = 0
62
+ usage_by_model: dict[str, dict[str, Any]] = {}
63
+
64
+ for session in sessions:
65
+ # Safely handle None values by defaulting to 0
66
+ total_cost += session.usage_total_cost_usd or 0
67
+ total_input_tokens += session.usage_input_tokens or 0
68
+ total_output_tokens += session.usage_output_tokens or 0
69
+ total_cache_creation_tokens += session.usage_cache_creation_tokens or 0
70
+ total_cache_read_tokens += session.usage_cache_read_tokens or 0
71
+
72
+ # Aggregate by model
73
+ model = session.model or "unknown"
74
+ if model not in usage_by_model:
75
+ usage_by_model[model] = {
76
+ "cost": 0.0,
77
+ "input_tokens": 0,
78
+ "output_tokens": 0,
79
+ "sessions": 0,
80
+ }
81
+
82
+ usage_by_model[model]["cost"] += session.usage_total_cost_usd or 0
83
+ usage_by_model[model]["input_tokens"] += session.usage_input_tokens or 0
84
+ usage_by_model[model]["output_tokens"] += session.usage_output_tokens or 0
85
+ usage_by_model[model]["sessions"] += 1
86
+
87
+ return {
88
+ "total_cost_usd": total_cost,
89
+ "total_input_tokens": total_input_tokens,
90
+ "total_output_tokens": total_output_tokens,
91
+ "total_cache_creation_tokens": total_cache_creation_tokens,
92
+ "total_cache_read_tokens": total_cache_read_tokens,
93
+ "session_count": len(sessions),
94
+ "usage_by_model": usage_by_model,
95
+ "period_days": days,
96
+ }
97
+
98
+ def get_budget_status(self) -> dict[str, Any]:
99
+ """Get current budget status for today.
100
+
101
+ Returns:
102
+ Dict with budget info: daily_budget_usd, used_today_usd,
103
+ remaining_usd, percentage_used, over_budget
104
+ """
105
+ summary = self.get_usage_summary(days=1)
106
+ used_today = summary["total_cost_usd"]
107
+
108
+ # Handle unlimited budget (daily_budget_usd <= 0)
109
+ if self.daily_budget_usd <= 0:
110
+ return {
111
+ "daily_budget_usd": self.daily_budget_usd,
112
+ "used_today_usd": used_today,
113
+ "remaining_usd": float("inf"),
114
+ "percentage_used": 0.0,
115
+ "over_budget": False,
116
+ }
117
+
118
+ remaining = self.daily_budget_usd - used_today
119
+
120
+ return {
121
+ "daily_budget_usd": self.daily_budget_usd,
122
+ "used_today_usd": used_today,
123
+ "remaining_usd": remaining,
124
+ "percentage_used": (used_today / self.daily_budget_usd * 100),
125
+ "over_budget": used_today > self.daily_budget_usd,
126
+ }
127
+
128
+ def can_spawn_agent(self, estimated_cost: float | None = None) -> tuple[bool, str | None]:
129
+ """Check if we can spawn an agent based on budget.
130
+
131
+ Args:
132
+ estimated_cost: Optional estimated cost for the agent run
133
+
134
+ Returns:
135
+ Tuple of (can_spawn, reason if not)
136
+ """
137
+ # Unlimited budget (0 or negative means no limit)
138
+ if self.daily_budget_usd <= 0.0:
139
+ return True, None
140
+
141
+ status = self.get_budget_status()
142
+
143
+ # Already over budget
144
+ if status["over_budget"]:
145
+ return (
146
+ False,
147
+ f"Daily budget exceeded: ${status['used_today_usd']:.2f} used "
148
+ f"of ${self.daily_budget_usd:.2f}",
149
+ )
150
+
151
+ # Check if estimated cost would exceed budget
152
+ if estimated_cost is not None:
153
+ if status["used_today_usd"] + estimated_cost > self.daily_budget_usd:
154
+ return (
155
+ False,
156
+ f"Estimated cost ${estimated_cost:.2f} would exceed budget. "
157
+ f"Remaining: ${status['remaining_usd']:.2f}",
158
+ )
159
+
160
+ return True, None
gobby/config/app.py CHANGED
@@ -39,6 +39,7 @@ from gobby.config.persistence import (
39
39
  MemoryConfig,
40
40
  MemorySyncConfig,
41
41
  )
42
+ from gobby.config.search import SearchConfig
42
43
  from gobby.config.servers import MCPClientProxyConfig, WebSocketSettings
43
44
  from gobby.config.sessions import (
44
45
  ArtifactHandoffConfig,
@@ -48,6 +49,7 @@ from gobby.config.sessions import (
48
49
  SessionSummaryConfig,
49
50
  TitleSynthesisConfig,
50
51
  )
52
+ from gobby.config.skills import SkillsConfig
51
53
  from gobby.config.tasks import (
52
54
  CompactHandoffConfig,
53
55
  GobbyTasksConfig,
@@ -81,9 +83,13 @@ __all__ = [
81
83
  # From gobby.config.persistence
82
84
  "MemoryConfig",
83
85
  "MemorySyncConfig",
86
+ # From gobby.config.search
87
+ "SearchConfig",
84
88
  # From gobby.config.servers
85
89
  "MCPClientProxyConfig",
86
90
  "WebSocketSettings",
91
+ # From gobby.config.skills
92
+ "SkillsConfig",
87
93
  # From gobby.config.sessions
88
94
  "ArtifactHandoffConfig",
89
95
  "ContextInjectionConfig",
@@ -99,6 +105,7 @@ __all__ = [
99
105
  "TaskValidationConfig",
100
106
  "WorkflowConfig",
101
107
  # Local definitions
108
+ "ConductorConfig",
102
109
  "DaemonConfig",
103
110
  "expand_env_vars",
104
111
  "load_yaml",
@@ -114,6 +121,37 @@ __all__ = [
114
121
  ENV_VAR_PATTERN = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}")
115
122
 
116
123
 
124
+ class ConductorConfig(BaseModel):
125
+ """
126
+ Configuration for the Conductor orchestration system.
127
+
128
+ Controls token budget management and agent spawning throttling.
129
+ """
130
+
131
+ daily_budget_usd: float = Field(
132
+ default=50.0,
133
+ ge=0.0,
134
+ description="Daily budget limit in USD. Set to 0 for unlimited.",
135
+ )
136
+ warning_threshold: float = Field(
137
+ default=0.8,
138
+ ge=0.0,
139
+ le=1.0,
140
+ description="Budget percentage at which to issue warnings (0.0-1.0).",
141
+ )
142
+ throttle_threshold: float = Field(
143
+ default=0.9,
144
+ ge=0.0,
145
+ le=1.0,
146
+ description="Budget percentage at which to throttle agent spawning (0.0-1.0).",
147
+ )
148
+ tracking_window_days: int = Field(
149
+ default=7,
150
+ gt=0,
151
+ description="Number of days to track usage for reporting.",
152
+ )
153
+
154
+
117
155
  def expand_env_vars(content: str) -> str:
118
156
  """
119
157
  Expand environment variables in configuration content.
@@ -188,13 +226,17 @@ class DaemonConfig(BaseModel):
188
226
 
189
227
  # Daemon settings
190
228
  daemon_port: int = Field(
191
- default=8765,
229
+ default=60887,
192
230
  description="Port for daemon to listen on",
193
231
  )
194
232
  daemon_health_check_interval: float = Field(
195
233
  default=10.0,
196
234
  description="Daemon health check interval in seconds",
197
235
  )
236
+ test_mode: bool = Field(
237
+ default=False,
238
+ description="Run daemon in test mode (enables test endpoints)",
239
+ )
198
240
 
199
241
  # Local storage
200
242
  database_path: str = Field(
@@ -279,6 +321,10 @@ class DaemonConfig(BaseModel):
279
321
  default_factory=MemorySyncConfig,
280
322
  description="Memory synchronization configuration",
281
323
  )
324
+ skills: SkillsConfig = Field(
325
+ default_factory=SkillsConfig,
326
+ description="Skills injection configuration",
327
+ )
282
328
  message_tracking: MessageTrackingConfig = Field(
283
329
  default_factory=MessageTrackingConfig,
284
330
  description="Session message tracking configuration",
@@ -295,6 +341,14 @@ class DaemonConfig(BaseModel):
295
341
  default_factory=ProjectVerificationConfig,
296
342
  description="Default verification commands for projects without auto-detected config",
297
343
  )
344
+ conductor: ConductorConfig = Field(
345
+ default_factory=ConductorConfig,
346
+ description="Conductor orchestration system configuration",
347
+ )
348
+ search: SearchConfig = Field(
349
+ default_factory=SearchConfig,
350
+ description="Unified search configuration with embedding fallback",
351
+ )
298
352
 
299
353
  def get_recommend_tools_config(self) -> RecommendToolsConfig:
300
354
  """Get recommend_tools configuration."""
@@ -324,6 +378,10 @@ class DaemonConfig(BaseModel):
324
378
  """Get memory sync configuration."""
325
379
  return self.memory_sync
326
380
 
381
+ def get_skills_config(self) -> SkillsConfig:
382
+ """Get skills configuration."""
383
+ return self.skills
384
+
327
385
  def get_gobby_tasks_config(self) -> GobbyTasksConfig:
328
386
  """Get gobby-tasks configuration."""
329
387
  return self.gobby_tasks
@@ -336,6 +394,10 @@ class DaemonConfig(BaseModel):
336
394
  """Get default verification commands configuration."""
337
395
  return self.verification_defaults
338
396
 
397
+ def get_search_config(self) -> SearchConfig:
398
+ """Get search configuration."""
399
+ return self.search
400
+
339
401
  @field_validator("daemon_port")
340
402
  @classmethod
341
403
  def validate_port(cls, v: int) -> int:
gobby/config/search.py ADDED
@@ -0,0 +1,110 @@
1
+ """Search configuration for Gobby daemon.
2
+
3
+ Provides configuration for the unified search layer with embedding
4
+ support and TF-IDF fallback.
5
+
6
+ Example usage in config.yaml:
7
+ search:
8
+ mode: auto # tfidf, embedding, auto, hybrid
9
+ embedding_model: text-embedding-3-small
10
+ tfidf_weight: 0.4
11
+ embedding_weight: 0.6
12
+ notify_on_fallback: true
13
+
14
+ For Ollama (local embeddings):
15
+ search:
16
+ mode: auto
17
+ embedding_model: openai/nomic-embed-text
18
+ embedding_api_base: http://localhost:11434/v1
19
+ """
20
+
21
+ from pydantic import BaseModel, Field
22
+
23
+ from gobby.search.models import SearchMode
24
+
25
+
26
+ class SearchConfig(BaseModel):
27
+ """Configuration for unified search with fallback.
28
+
29
+ This config controls how UnifiedSearcher behaves, including:
30
+ - Which search mode to use (tfidf, embedding, auto, hybrid)
31
+ - Which embedding model to use (LiteLLM format)
32
+ - Weights for hybrid mode
33
+ - Whether to notify on fallback
34
+
35
+ Supported modes:
36
+ - tfidf: TF-IDF only (always works, no API needed)
37
+ - embedding: Embedding-based search only (fails if unavailable)
38
+ - auto: Try embedding, fallback to TF-IDF if unavailable
39
+ - hybrid: Combine both with weighted scores
40
+
41
+ LiteLLM model format examples:
42
+ - OpenAI: text-embedding-3-small (needs OPENAI_API_KEY)
43
+ - Ollama: openai/nomic-embed-text (with embedding_api_base)
44
+ - Azure: azure/azure-embedding-model
45
+ - Vertex AI: vertex_ai/text-embedding-004
46
+ - Gemini: gemini/text-embedding-004 (needs GEMINI_API_KEY)
47
+ - Mistral: mistral/mistral-embed (needs MISTRAL_API_KEY)
48
+ """
49
+
50
+ mode: str = Field(
51
+ default="auto",
52
+ description="Search mode: tfidf, embedding, auto, hybrid",
53
+ )
54
+ embedding_model: str = Field(
55
+ default="text-embedding-3-small",
56
+ description="LiteLLM model string (e.g., text-embedding-3-small, openai/nomic-embed-text)",
57
+ )
58
+ embedding_api_base: str | None = Field(
59
+ default=None,
60
+ description="API base URL for Ollama/custom endpoints (e.g., http://localhost:11434/v1)",
61
+ )
62
+ embedding_api_key: str | None = Field(
63
+ default=None,
64
+ description="API key for embedding provider (uses env var if not set)",
65
+ )
66
+ tfidf_weight: float = Field(
67
+ default=0.4,
68
+ ge=0.0,
69
+ le=1.0,
70
+ description="Weight for TF-IDF scores in hybrid mode",
71
+ )
72
+ embedding_weight: float = Field(
73
+ default=0.6,
74
+ ge=0.0,
75
+ le=1.0,
76
+ description="Weight for embedding scores in hybrid mode",
77
+ )
78
+ notify_on_fallback: bool = Field(
79
+ default=True,
80
+ description="Log warning when falling back to TF-IDF",
81
+ )
82
+
83
+ def get_normalized_weights(self) -> tuple[float, float]:
84
+ """Get normalized weights that sum to 1.0.
85
+
86
+ Returns:
87
+ Tuple of (tfidf_weight, embedding_weight) normalized to sum to 1.0
88
+ """
89
+ total = self.tfidf_weight + self.embedding_weight
90
+ if total == 0:
91
+ # Default to equal weights if both are 0
92
+ return (0.5, 0.5)
93
+ return (self.tfidf_weight / total, self.embedding_weight / total)
94
+
95
+ def get_mode_enum(self) -> SearchMode:
96
+ """Get the SearchMode enum instance for the configured mode.
97
+
98
+ Returns:
99
+ SearchMode enum corresponding to the mode string value
100
+
101
+ Raises:
102
+ ValueError: If the configured mode is not a valid SearchMode
103
+ """
104
+ try:
105
+ return SearchMode(self.mode)
106
+ except ValueError as e:
107
+ valid_modes = [m.value for m in SearchMode]
108
+ raise ValueError(
109
+ f"Invalid search mode '{self.mode}'. Valid modes are: {', '.join(valid_modes)}"
110
+ ) from e
gobby/config/servers.py CHANGED
@@ -23,7 +23,7 @@ class WebSocketSettings(BaseModel):
23
23
  description="Enable WebSocket server for real-time communication",
24
24
  )
25
25
  port: int = Field(
26
- default=8766,
26
+ default=60888,
27
27
  description="Port for WebSocket server to listen on",
28
28
  )
29
29
  ping_interval: int = Field(
gobby/config/skills.py ADDED
@@ -0,0 +1,43 @@
1
+ """
2
+ Skills configuration for Gobby daemon.
3
+
4
+ Provides configuration for skill injection and discovery.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Literal
10
+
11
+ from pydantic import BaseModel, Field, field_validator
12
+
13
+
14
+ class SkillsConfig(BaseModel):
15
+ """
16
+ Configuration for skill injection and discovery.
17
+
18
+ Controls whether and how skills are injected into session context.
19
+ """
20
+
21
+ inject_core_skills: bool = Field(
22
+ default=True,
23
+ description="Whether to inject core skills into session context",
24
+ )
25
+
26
+ core_skills_path: str | None = Field(
27
+ default=None,
28
+ description="Override path for core skills (default: install/shared/skills/)",
29
+ )
30
+
31
+ injection_format: Literal["summary", "full", "none"] = Field(
32
+ default="summary",
33
+ description="Format for skill injection: 'summary' (names only), 'full' (with content), 'none' (disabled)",
34
+ )
35
+
36
+ @field_validator("injection_format")
37
+ @classmethod
38
+ def validate_injection_format(cls, v: str) -> str:
39
+ """Validate injection_format is one of the allowed values."""
40
+ allowed = {"summary", "full", "none"}
41
+ if v not in allowed:
42
+ raise ValueError(f"injection_format must be one of {allowed}, got '{v}'")
43
+ return v
gobby/config/tasks.py CHANGED
@@ -181,16 +181,12 @@ class TaskExpansionConfig(BaseModel):
181
181
  )
182
182
  tdd_prompt: str | None = Field(
183
183
  default=None,
184
- description="DEPRECATED: TDD mode is now integrated into the system prompt template via Jinja2 conditionals",
184
+ description="DEPRECATED: TDD instructions are now embedded in task descriptions for code/config categories",
185
185
  )
186
186
  web_research_enabled: bool = Field(
187
187
  default=True,
188
188
  description="Enable web research for task expansion using MCP tools",
189
189
  )
190
- tdd_mode: bool = Field(
191
- default=True,
192
- description="Enable TDD mode: create test->implement task pairs with appropriate blocking for coding tasks",
193
- )
194
190
  max_subtasks: int = Field(
195
191
  default=15,
196
192
  description="Maximum number of subtasks to create per expansion",
@@ -724,10 +720,6 @@ class WorkflowVariablesConfig(BaseModel):
724
720
  default=False,
725
721
  description="Require an active task (in_progress) before allowing file edits",
726
722
  )
727
- tdd_mode: bool = Field(
728
- default=True,
729
- description="Enable TDD mode for task expansion (test-implementation pairs)",
730
- )
731
723
  session_task: str | list[str] | None = Field(
732
724
  default=None,
733
725
  description="Task(s) to complete before stopping. "
@@ -759,13 +751,13 @@ def merge_workflow_variables(
759
751
  ValidationError: If validate=True and merged values fail validation.
760
752
 
761
753
  Example:
762
- >>> yaml_defaults = {"tdd_mode": True, "require_task_before_edit": False}
763
- >>> db_overrides = {"tdd_mode": False}
754
+ >>> yaml_defaults = {"require_task_before_edit": False, "session_task": None}
755
+ >>> db_overrides = {"require_task_before_edit": True}
764
756
  >>> effective = merge_workflow_variables(yaml_defaults, db_overrides)
765
- >>> effective["tdd_mode"]
766
- False
767
757
  >>> effective["require_task_before_edit"]
768
- False
758
+ True
759
+ >>> effective["session_task"]
760
+ None
769
761
  """
770
762
  # Start with defaults
771
763
  effective = dict(yaml_defaults)