gobby 0.2.6__py3-none-any.whl → 0.2.8__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 (198) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +2 -1
  3. gobby/adapters/claude_code.py +96 -35
  4. gobby/adapters/codex_impl/__init__.py +28 -0
  5. gobby/adapters/codex_impl/adapter.py +722 -0
  6. gobby/adapters/codex_impl/client.py +679 -0
  7. gobby/adapters/codex_impl/protocol.py +20 -0
  8. gobby/adapters/codex_impl/types.py +68 -0
  9. gobby/adapters/gemini.py +140 -38
  10. gobby/agents/definitions.py +11 -1
  11. gobby/agents/isolation.py +525 -0
  12. gobby/agents/registry.py +11 -0
  13. gobby/agents/sandbox.py +261 -0
  14. gobby/agents/session.py +1 -0
  15. gobby/agents/spawn.py +42 -287
  16. gobby/agents/spawn_executor.py +415 -0
  17. gobby/agents/spawners/__init__.py +24 -0
  18. gobby/agents/spawners/command_builder.py +189 -0
  19. gobby/agents/spawners/embedded.py +21 -2
  20. gobby/agents/spawners/headless.py +21 -2
  21. gobby/agents/spawners/macos.py +26 -1
  22. gobby/agents/spawners/prompt_manager.py +125 -0
  23. gobby/cli/__init__.py +0 -2
  24. gobby/cli/install.py +4 -4
  25. gobby/cli/installers/claude.py +6 -0
  26. gobby/cli/installers/gemini.py +6 -0
  27. gobby/cli/installers/shared.py +103 -4
  28. gobby/cli/memory.py +185 -0
  29. gobby/cli/sessions.py +1 -1
  30. gobby/cli/utils.py +9 -2
  31. gobby/clones/git.py +177 -0
  32. gobby/config/__init__.py +12 -97
  33. gobby/config/app.py +10 -94
  34. gobby/config/extensions.py +2 -2
  35. gobby/config/features.py +7 -130
  36. gobby/config/skills.py +31 -0
  37. gobby/config/tasks.py +4 -28
  38. gobby/hooks/__init__.py +0 -13
  39. gobby/hooks/event_handlers.py +150 -8
  40. gobby/hooks/hook_manager.py +21 -3
  41. gobby/hooks/plugins.py +1 -1
  42. gobby/hooks/webhooks.py +1 -1
  43. gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
  44. gobby/llm/resolver.py +3 -2
  45. gobby/mcp_proxy/importer.py +62 -4
  46. gobby/mcp_proxy/instructions.py +4 -2
  47. gobby/mcp_proxy/registries.py +22 -8
  48. gobby/mcp_proxy/services/recommendation.py +43 -11
  49. gobby/mcp_proxy/tools/agent_messaging.py +93 -44
  50. gobby/mcp_proxy/tools/agents.py +76 -740
  51. gobby/mcp_proxy/tools/artifacts.py +43 -9
  52. gobby/mcp_proxy/tools/clones.py +0 -385
  53. gobby/mcp_proxy/tools/memory.py +2 -2
  54. gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
  55. gobby/mcp_proxy/tools/sessions/_commits.py +239 -0
  56. gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
  57. gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
  58. gobby/mcp_proxy/tools/sessions/_handoff.py +503 -0
  59. gobby/mcp_proxy/tools/sessions/_messages.py +166 -0
  60. gobby/mcp_proxy/tools/skills/__init__.py +14 -29
  61. gobby/mcp_proxy/tools/spawn_agent.py +455 -0
  62. gobby/mcp_proxy/tools/tasks/_context.py +18 -0
  63. gobby/mcp_proxy/tools/tasks/_crud.py +13 -6
  64. gobby/mcp_proxy/tools/tasks/_lifecycle.py +79 -30
  65. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +1 -1
  66. gobby/mcp_proxy/tools/tasks/_session.py +22 -7
  67. gobby/mcp_proxy/tools/workflows.py +84 -34
  68. gobby/mcp_proxy/tools/worktrees.py +32 -350
  69. gobby/memory/extractor.py +15 -1
  70. gobby/memory/ingestion/__init__.py +5 -0
  71. gobby/memory/ingestion/multimodal.py +221 -0
  72. gobby/memory/manager.py +62 -283
  73. gobby/memory/search/__init__.py +10 -0
  74. gobby/memory/search/coordinator.py +248 -0
  75. gobby/memory/services/__init__.py +5 -0
  76. gobby/memory/services/crossref.py +142 -0
  77. gobby/prompts/loader.py +5 -2
  78. gobby/runner.py +13 -0
  79. gobby/servers/http.py +1 -4
  80. gobby/servers/routes/admin.py +14 -0
  81. gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
  82. gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
  83. gobby/servers/routes/mcp/endpoints/execution.py +568 -0
  84. gobby/servers/routes/mcp/endpoints/registry.py +378 -0
  85. gobby/servers/routes/mcp/endpoints/server.py +304 -0
  86. gobby/servers/routes/mcp/hooks.py +51 -4
  87. gobby/servers/routes/mcp/tools.py +48 -1506
  88. gobby/servers/websocket.py +57 -1
  89. gobby/sessions/analyzer.py +2 -2
  90. gobby/sessions/lifecycle.py +1 -1
  91. gobby/sessions/manager.py +9 -0
  92. gobby/sessions/processor.py +10 -0
  93. gobby/sessions/transcripts/base.py +1 -0
  94. gobby/sessions/transcripts/claude.py +15 -5
  95. gobby/sessions/transcripts/gemini.py +100 -34
  96. gobby/skills/parser.py +30 -2
  97. gobby/storage/database.py +9 -2
  98. gobby/storage/memories.py +32 -21
  99. gobby/storage/migrations.py +174 -368
  100. gobby/storage/sessions.py +45 -7
  101. gobby/storage/skills.py +80 -7
  102. gobby/storage/tasks/_lifecycle.py +18 -3
  103. gobby/sync/memories.py +1 -1
  104. gobby/tasks/external_validator.py +1 -1
  105. gobby/tasks/validation.py +22 -20
  106. gobby/tools/summarizer.py +91 -10
  107. gobby/utils/project_context.py +2 -3
  108. gobby/utils/status.py +13 -0
  109. gobby/workflows/actions.py +221 -1217
  110. gobby/workflows/artifact_actions.py +31 -0
  111. gobby/workflows/autonomous_actions.py +11 -0
  112. gobby/workflows/context_actions.py +50 -1
  113. gobby/workflows/detection_helpers.py +38 -24
  114. gobby/workflows/enforcement/__init__.py +47 -0
  115. gobby/workflows/enforcement/blocking.py +281 -0
  116. gobby/workflows/enforcement/commit_policy.py +283 -0
  117. gobby/workflows/enforcement/handlers.py +269 -0
  118. gobby/workflows/enforcement/task_policy.py +542 -0
  119. gobby/workflows/engine.py +93 -0
  120. gobby/workflows/evaluator.py +110 -0
  121. gobby/workflows/git_utils.py +106 -0
  122. gobby/workflows/hooks.py +41 -0
  123. gobby/workflows/llm_actions.py +30 -0
  124. gobby/workflows/mcp_actions.py +20 -1
  125. gobby/workflows/memory_actions.py +91 -0
  126. gobby/workflows/safe_evaluator.py +191 -0
  127. gobby/workflows/session_actions.py +44 -0
  128. gobby/workflows/state_actions.py +60 -1
  129. gobby/workflows/stop_signal_actions.py +55 -0
  130. gobby/workflows/summary_actions.py +217 -51
  131. gobby/workflows/task_sync_actions.py +347 -0
  132. gobby/workflows/todo_actions.py +34 -1
  133. gobby/workflows/webhook_actions.py +185 -0
  134. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/METADATA +6 -1
  135. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/RECORD +139 -163
  136. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/WHEEL +1 -1
  137. gobby/adapters/codex.py +0 -1332
  138. gobby/cli/tui.py +0 -34
  139. gobby/install/claude/commands/gobby/bug.md +0 -51
  140. gobby/install/claude/commands/gobby/chore.md +0 -51
  141. gobby/install/claude/commands/gobby/epic.md +0 -52
  142. gobby/install/claude/commands/gobby/eval.md +0 -235
  143. gobby/install/claude/commands/gobby/feat.md +0 -49
  144. gobby/install/claude/commands/gobby/nit.md +0 -52
  145. gobby/install/claude/commands/gobby/ref.md +0 -52
  146. gobby/mcp_proxy/tools/session_messages.py +0 -1055
  147. gobby/prompts/defaults/expansion/system.md +0 -119
  148. gobby/prompts/defaults/expansion/user.md +0 -48
  149. gobby/prompts/defaults/external_validation/agent.md +0 -72
  150. gobby/prompts/defaults/external_validation/external.md +0 -63
  151. gobby/prompts/defaults/external_validation/spawn.md +0 -83
  152. gobby/prompts/defaults/external_validation/system.md +0 -6
  153. gobby/prompts/defaults/features/import_mcp.md +0 -22
  154. gobby/prompts/defaults/features/import_mcp_github.md +0 -17
  155. gobby/prompts/defaults/features/import_mcp_search.md +0 -16
  156. gobby/prompts/defaults/features/recommend_tools.md +0 -32
  157. gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
  158. gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
  159. gobby/prompts/defaults/features/server_description.md +0 -20
  160. gobby/prompts/defaults/features/server_description_system.md +0 -6
  161. gobby/prompts/defaults/features/task_description.md +0 -31
  162. gobby/prompts/defaults/features/task_description_system.md +0 -6
  163. gobby/prompts/defaults/features/tool_summary.md +0 -17
  164. gobby/prompts/defaults/features/tool_summary_system.md +0 -6
  165. gobby/prompts/defaults/handoff/compact.md +0 -63
  166. gobby/prompts/defaults/handoff/session_end.md +0 -57
  167. gobby/prompts/defaults/memory/extract.md +0 -61
  168. gobby/prompts/defaults/research/step.md +0 -58
  169. gobby/prompts/defaults/validation/criteria.md +0 -47
  170. gobby/prompts/defaults/validation/validate.md +0 -38
  171. gobby/storage/migrations_legacy.py +0 -1359
  172. gobby/tui/__init__.py +0 -5
  173. gobby/tui/api_client.py +0 -278
  174. gobby/tui/app.py +0 -329
  175. gobby/tui/screens/__init__.py +0 -25
  176. gobby/tui/screens/agents.py +0 -333
  177. gobby/tui/screens/chat.py +0 -450
  178. gobby/tui/screens/dashboard.py +0 -377
  179. gobby/tui/screens/memory.py +0 -305
  180. gobby/tui/screens/metrics.py +0 -231
  181. gobby/tui/screens/orchestrator.py +0 -903
  182. gobby/tui/screens/sessions.py +0 -412
  183. gobby/tui/screens/tasks.py +0 -440
  184. gobby/tui/screens/workflows.py +0 -289
  185. gobby/tui/screens/worktrees.py +0 -174
  186. gobby/tui/widgets/__init__.py +0 -21
  187. gobby/tui/widgets/chat.py +0 -210
  188. gobby/tui/widgets/conductor.py +0 -104
  189. gobby/tui/widgets/menu.py +0 -132
  190. gobby/tui/widgets/message_panel.py +0 -160
  191. gobby/tui/widgets/review_gate.py +0 -224
  192. gobby/tui/widgets/task_tree.py +0 -99
  193. gobby/tui/widgets/token_budget.py +0 -166
  194. gobby/tui/ws_client.py +0 -258
  195. gobby/workflows/task_enforcement_actions.py +0 -1343
  196. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/entry_points.txt +0 -0
  197. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/licenses/LICENSE.md +0 -0
  198. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/top_level.txt +0 -0
@@ -137,8 +137,8 @@ class PluginsConfig(BaseModel):
137
137
  description="Enable plugin system (disabled by default for security)",
138
138
  )
139
139
  plugin_dirs: list[str] = Field(
140
- default_factory=lambda: ["~/.gobby/plugins", ".gobby/plugins"],
141
- description="Directories to scan for plugins (supports ~ expansion)",
140
+ default_factory=lambda: [".gobby/plugins"],
141
+ description="Directories to scan for plugins (project-scoped)",
142
142
  )
143
143
  auto_discover: bool = Field(
144
144
  default=True,
gobby/config/features.py CHANGED
@@ -42,45 +42,22 @@ class ToolSummarizerConfig(BaseModel):
42
42
  default="claude-haiku-4-5",
43
43
  description="Model to use for summarization (fast/cheap recommended)",
44
44
  )
45
- prompt: str = Field(
46
- default="""Summarize this MCP tool description in 180 characters or less.
47
- Keep it to three sentences or less. Be concise and preserve the key functionality.
48
- Do not add quotes, extra formatting, or code examples.
49
45
 
50
- Description: {description}
51
-
52
- Summary:""",
53
- description="DEPRECATED: Use prompt_path instead. Prompt template for tool description summarization",
54
- )
55
46
  prompt_path: str | None = Field(
56
47
  default=None,
57
48
  description="Path to custom tool summary prompt template (e.g., 'features/tool_summary')",
58
49
  )
59
- system_prompt: str = Field(
60
- default="You are a technical summarizer. Create concise tool descriptions.",
61
- description="DEPRECATED: Use system_prompt_path instead. System prompt for tool description summarization",
62
- )
50
+
63
51
  system_prompt_path: str | None = Field(
64
52
  default=None,
65
53
  description="Path to custom tool summary system prompt (e.g., 'features/tool_summary_system')",
66
54
  )
67
- server_description_prompt: str = Field(
68
- default="""Write a single concise sentence describing what the '{server_name}' MCP server does based on its tools.
69
55
 
70
- Tools:
71
- {tools_list}
72
-
73
- Description (1 sentence, try to keep under 100 characters):""",
74
- description="DEPRECATED: Use server_description_prompt_path instead. Prompt template for server description generation",
75
- )
76
56
  server_description_prompt_path: str | None = Field(
77
57
  default=None,
78
58
  description="Path to custom server description prompt (e.g., 'features/server_description')",
79
59
  )
80
- server_description_system_prompt: str = Field(
81
- default="You write concise technical descriptions.",
82
- description="DEPRECATED: Use server_description_system_prompt_path instead. System prompt for server description generation",
83
- )
60
+
84
61
  server_description_system_prompt_path: str | None = Field(
85
62
  default=None,
86
63
  description="Path to custom server description system prompt (e.g., 'features/server_description_system')",
@@ -110,26 +87,12 @@ class TaskDescriptionConfig(BaseModel):
110
87
  default=50,
111
88
  description="Minimum length of structured extraction before LLM fallback triggers",
112
89
  )
113
- prompt: str = Field(
114
- default="""Generate a concise task description for this task from a spec document.
115
-
116
- Task title: {task_title}
117
- Section: {section_title}
118
- Section content: {section_content}
119
- Existing context: {existing_context}
120
90
 
121
- Write a 1-2 sentence description focusing on the goal and deliverable.
122
- Do not add quotes, extra formatting, or implementation details.""",
123
- description="DEPRECATED: Use prompt_path instead. Prompt template for task description generation",
124
- )
125
91
  prompt_path: str | None = Field(
126
92
  default=None,
127
93
  description="Path to custom task description prompt (e.g., 'features/task_description')",
128
94
  )
129
- system_prompt: str = Field(
130
- default="You are a technical writer creating concise task descriptions for developers.",
131
- description="DEPRECATED: Use system_prompt_path instead. System prompt for task description generation",
132
- )
95
+
133
96
  system_prompt_path: str | None = Field(
134
97
  default=None,
135
98
  description="Path to custom task description system prompt (e.g., 'features/task_description_system')",
@@ -159,83 +122,17 @@ class RecommendToolsConfig(BaseModel):
159
122
  default="claude-sonnet-4-5",
160
123
  description="Model to use for tool recommendations",
161
124
  )
162
- prompt: str = Field(
163
- default="""You are a tool recommendation assistant for Claude Code with access to MCP servers.
164
-
165
- CRITICAL PRIORITIZATION RULES:
166
- 1. Analyze the task type (code navigation, docs lookup, database query, planning, data processing, etc.)
167
- 2. Check available MCP server DESCRIPTIONS for capability matches
168
- 3. If ANY MCP server's description matches the task type -> recommend those tools FIRST
169
- 4. Only recommend built-in Claude Code tools (Grep, Read, Bash, WebSearch) if NO suitable MCP server exists
170
-
171
- TASK TYPE MATCHING GUIDELINES:
172
- - Task needs library/framework documentation -> Look for MCP servers describing "documentation", "library docs", "API reference"
173
- - Task needs code navigation/architecture understanding -> Look for MCP servers describing "code analysis", "symbols", "semantic search"
174
- - Task needs database operations -> Look for MCP servers describing "database", "PostgreSQL", "SQL"
175
- - Task needs complex reasoning/planning -> Look for MCP servers describing "problem-solving", "thinking", "reasoning"
176
- - Task needs data processing/large datasets -> Look for MCP servers describing "code execution", "data processing", "token optimization"
177
-
178
- ANTI-PATTERNS (What NOT to recommend):
179
- - Don't recommend WebSearch when an MCP server provides library/framework documentation
180
- - Don't recommend Grep/Read for code architecture questions when an MCP server does semantic code analysis
181
- - Don't recommend Bash for database queries when an MCP server provides database tools
182
- - Don't recommend direct implementation when an MCP server provides structured reasoning
183
-
184
- OUTPUT FORMAT:
185
- Be concise and specific. Recommend 1-3 tools maximum with:
186
- 1. Which MCP server and tools to use (if applicable)
187
- 2. Brief rationale based on server description matching task type
188
- 3. Suggested workflow (e.g., "First call X, then use result with Y")
189
- 4. Only mention built-in tools if no MCP server is suitable""",
190
- description="DEPRECATED: Use prompt_path instead. System prompt for recommend_tools() MCP tool.",
191
- )
125
+
192
126
  prompt_path: str | None = Field(
193
127
  default=None,
194
128
  description="Path to custom recommend tools system prompt (e.g., 'features/recommend_tools')",
195
129
  )
196
- hybrid_rerank_prompt: str = Field(
197
- default="""You are an expert at selecting tools for tasks.
198
- Task: {task_description}
199
-
200
- Candidate tools (ranked by semantic similarity):
201
- {candidate_list}
202
-
203
- Re-rank these tools by relevance to the task and provide reasoning.
204
- Return the top {top_k} most relevant as JSON:
205
- {{
206
- "recommendations": [
207
- {{
208
- "server": "server_name",
209
- "tool": "tool_name",
210
- "reason": "Why this tool is the best choice"
211
- }}
212
- ]
213
- }}""",
214
- description="DEPRECATED: Use hybrid_rerank_prompt_path instead. Prompt template for hybrid mode re-ranking",
215
- )
130
+
216
131
  hybrid_rerank_prompt_path: str | None = Field(
217
132
  default=None,
218
133
  description="Path to custom hybrid re-rank prompt (e.g., 'features/recommend_tools_hybrid')",
219
134
  )
220
- llm_prompt: str = Field(
221
- default="""You are an expert at selecting the right tools for a given task.
222
- Task: {task_description}
223
-
224
- Available Servers: {available_servers}
225
-
226
- Please recommend which tools from these servers would be most useful for this task.
227
- Return a JSON object with this structure:
228
- {{
229
- "recommendations": [
230
- {{
231
- "server": "server_name",
232
- "tool": "tool_name",
233
- "reason": "Why this tool is useful"
234
- }}
235
- ]
236
- }}""",
237
- description="DEPRECATED: Use llm_prompt_path instead. Prompt template for LLM mode recommendations",
238
- )
135
+
239
136
  llm_prompt_path: str | None = Field(
240
137
  default=None,
241
138
  description="Path to custom LLM recommendation prompt (e.g., 'features/recommend_tools_llm')",
@@ -276,37 +173,17 @@ class ImportMCPServerConfig(BaseModel):
276
173
  default="claude-haiku-4-5",
277
174
  description="Model to use for config extraction",
278
175
  )
279
- prompt: str = Field(
280
- default=DEFAULT_IMPORT_MCP_SERVER_PROMPT,
281
- description="DEPRECATED: Use prompt_path instead. System prompt for MCP server config extraction",
282
- )
176
+
283
177
  prompt_path: str | None = Field(
284
178
  default=None,
285
179
  description="Path to custom import MCP system prompt (e.g., 'features/import_mcp')",
286
180
  )
287
- github_fetch_prompt: str = Field(
288
- default="""Fetch the README from this GitHub repository and extract MCP server configuration:
289
-
290
- {github_url}
291
-
292
- If the URL doesn't point directly to a README, try to find and fetch the README.md file.
293
181
 
294
- After reading the documentation, extract the MCP server configuration as a JSON object.""",
295
- description="DEPRECATED: Use github_fetch_prompt_path instead. User prompt template for GitHub import",
296
- )
297
182
  github_fetch_prompt_path: str | None = Field(
298
183
  default=None,
299
184
  description="Path to custom GitHub fetch prompt (e.g., 'features/import_mcp_github')",
300
185
  )
301
- search_fetch_prompt: str = Field(
302
- default="""Search for MCP server: {search_query}
303
-
304
- Find the official documentation or GitHub repository for this MCP server.
305
- Then fetch and read the README or installation docs.
306
186
 
307
- After reading the documentation, extract the MCP server configuration as a JSON object.""",
308
- description="DEPRECATED: Use search_fetch_prompt_path instead. User prompt template for search-based import",
309
- )
310
187
  search_fetch_prompt_path: str | None = Field(
311
188
  default=None,
312
189
  description="Path to custom search fetch prompt (e.g., 'features/import_mcp_search')",
gobby/config/skills.py CHANGED
@@ -11,6 +11,37 @@ from typing import Literal
11
11
  from pydantic import BaseModel, Field, field_validator
12
12
 
13
13
 
14
+ class HubConfig(BaseModel):
15
+ """
16
+ Configuration for a skill hub or collection.
17
+ """
18
+
19
+ type: Literal["clawdhub", "skillhub", "github-collection"] = Field(
20
+ ...,
21
+ description="Type of the hub: 'clawdhub', 'skillhub', or 'github-collection'",
22
+ )
23
+
24
+ base_url: str | None = Field(
25
+ default=None,
26
+ description="Base URL for the hub",
27
+ )
28
+
29
+ repo: str | None = Field(
30
+ default=None,
31
+ description="GitHub repository (e.g. 'owner/repo')",
32
+ )
33
+
34
+ branch: str | None = Field(
35
+ default=None,
36
+ description="Git branch to use",
37
+ )
38
+
39
+ auth_key_name: str | None = Field(
40
+ default=None,
41
+ description="Environment variable name for auth key",
42
+ )
43
+
44
+
14
45
  class SkillsConfig(BaseModel):
15
46
  """
16
47
  Configuration for skill injection and discovery.
gobby/config/tasks.py CHANGED
@@ -38,14 +38,6 @@ class CompactHandoffConfig(BaseModel):
38
38
  default=True,
39
39
  description="Enable compact handoff context extraction and injection",
40
40
  )
41
- # DEPRECATED: prompt field is no longer used.
42
- # Template is now defined in session-handoff.yaml workflow file.
43
- # Kept for backwards compatibility but will be removed in a future version.
44
- prompt: str | None = Field(
45
- default=None,
46
- description="DEPRECATED: Template moved to session-handoff.yaml workflow. "
47
- "This field is ignored.",
48
- )
49
41
 
50
42
 
51
43
  class PatternCriteriaConfig(BaseModel):
@@ -147,10 +139,7 @@ class TaskExpansionConfig(BaseModel):
147
139
  default="claude-opus-4-5",
148
140
  description="Model to use for expansion",
149
141
  )
150
- prompt: str | None = Field(
151
- default=None,
152
- description="DEPRECATED: Use prompt_path instead. Custom prompt template for task expansion",
153
- )
142
+
154
143
  prompt_path: str | None = Field(
155
144
  default=None,
156
145
  description="Path to custom user prompt template (e.g., 'expansion/user')",
@@ -175,14 +164,7 @@ class TaskExpansionConfig(BaseModel):
175
164
  default="You are a senior developer researching a codebase. Use tools to find relevant code.",
176
165
  description="System prompt for the research agent",
177
166
  )
178
- system_prompt: str | None = Field(
179
- default=None,
180
- description="DEPRECATED: Use system_prompt_path instead. Custom system prompt for task expansion",
181
- )
182
- tdd_prompt: str | None = Field(
183
- default=None,
184
- description="DEPRECATED: TDD instructions are now embedded in task descriptions for code/config categories",
185
- )
167
+
186
168
  web_research_enabled: bool = Field(
187
169
  default=True,
188
170
  description="Enable web research for task expansion using MCP tools",
@@ -228,10 +210,7 @@ class TaskValidationConfig(BaseModel):
228
210
  default="You are a QA validator. Output ONLY valid JSON. No markdown, no explanation, no code blocks. Just the raw JSON object.",
229
211
  description="System prompt for task validation",
230
212
  )
231
- prompt: str | None = Field(
232
- default=None,
233
- description="DEPRECATED: Use prompt_path instead. Custom prompt template for task validation",
234
- )
213
+
235
214
  prompt_path: str | None = Field(
236
215
  default=None,
237
216
  description="Path to custom validation prompt template (e.g., 'validation/validate')",
@@ -260,10 +239,7 @@ class TaskValidationConfig(BaseModel):
260
239
  default="You are a QA engineer writing acceptance criteria. CRITICAL: Only include requirements explicitly stated in the task. Do NOT invent specific values, thresholds, timeouts, or edge cases that aren't mentioned. Vague tasks get vague criteria. Use markdown checkboxes.",
261
240
  description="System prompt for generating validation criteria",
262
241
  )
263
- criteria_prompt: str | None = Field(
264
- default=None,
265
- description="DEPRECATED: Use criteria_prompt_path instead. Custom prompt template for generating validation criteria",
266
- )
242
+
267
243
  # Validation loop control
268
244
  max_iterations: int = Field(
269
245
  default=10,
gobby/hooks/__init__.py CHANGED
@@ -65,14 +65,6 @@ from gobby.hooks.plugins import (
65
65
  from gobby.hooks.session_coordinator import SessionCoordinator
66
66
  from gobby.hooks.webhooks import WebhookDispatcher
67
67
 
68
- # Legacy imports for backward compatibility
69
- from gobby.sessions.manager import SessionManager
70
- from gobby.sessions.summary import SummaryFileGenerator
71
- from gobby.sessions.transcripts.claude import ClaudeTranscriptParser
72
-
73
- # Backward-compatible alias
74
- TranscriptProcessor = ClaudeTranscriptParser
75
-
76
68
  __all__ = [
77
69
  # Core coordinator
78
70
  "HookManager",
@@ -96,9 +88,4 @@ __all__ = [
96
88
  "RegisteredHandler",
97
89
  "hook_handler",
98
90
  "run_plugin_handlers",
99
- # Legacy exports (backward compatibility)
100
- "SessionManager",
101
- "SummaryFileGenerator",
102
- "TranscriptProcessor",
103
- "ClaudeTranscriptParser",
104
91
  ]
@@ -29,6 +29,16 @@ if TYPE_CHECKING:
29
29
  from gobby.workflows.hooks import WorkflowHookHandler
30
30
 
31
31
 
32
+ EDIT_TOOLS = {
33
+ "write_file",
34
+ "replace",
35
+ "edit_file",
36
+ "notebook_edit",
37
+ "edit",
38
+ "write",
39
+ }
40
+
41
+
32
42
  class EventHandlers:
33
43
  """
34
44
  Manages event handler registration and dispatch.
@@ -137,6 +147,56 @@ class EventHandlers:
137
147
  """
138
148
  return dict(self._handler_map)
139
149
 
150
+ def _auto_activate_workflow(
151
+ self, workflow_name: str, session_id: str, project_path: str | None
152
+ ) -> None:
153
+ """Auto-activate a workflow for a session.
154
+
155
+ Args:
156
+ workflow_name: Name of the workflow to activate
157
+ session_id: Session ID to activate workflow for
158
+ project_path: Project path for workflow context
159
+ """
160
+ if not self._workflow_handler:
161
+ return
162
+
163
+ try:
164
+ result = self._workflow_handler.activate_workflow(
165
+ workflow_name=workflow_name,
166
+ session_id=session_id,
167
+ project_path=project_path,
168
+ )
169
+ if result.get("success"):
170
+ self.logger.info(
171
+ "Auto-activated workflow for session",
172
+ extra={
173
+ "workflow_name": workflow_name,
174
+ "session_id": session_id,
175
+ "project_path": project_path,
176
+ },
177
+ )
178
+ else:
179
+ self.logger.warning(
180
+ "Failed to auto-activate workflow",
181
+ extra={
182
+ "workflow_name": workflow_name,
183
+ "session_id": session_id,
184
+ "project_path": project_path,
185
+ "error": result.get("error"),
186
+ },
187
+ )
188
+ except Exception as e:
189
+ self.logger.warning(
190
+ "Failed to auto-activate workflow",
191
+ extra={
192
+ "workflow_name": workflow_name,
193
+ "session_id": session_id,
194
+ "project_path": project_path,
195
+ "error": str(e),
196
+ },
197
+ exc_info=True,
198
+ )
199
+
140
200
  # ==================== SESSION HANDLERS ====================
141
201
 
142
202
  def handle_session_start(self, event: HookEvent) -> HookResponse:
@@ -197,6 +257,12 @@ class EventHandlers:
197
257
  except Exception as e:
198
258
  self.logger.warning(f"Failed to start agent run: {e}")
199
259
 
260
+ # Auto-activate workflow if specified for this session
261
+ if existing_session.workflow_name and session_id:
262
+ self._auto_activate_workflow(
263
+ existing_session.workflow_name, session_id, cwd
264
+ )
265
+
200
266
  # Update event metadata
201
267
  event.metadata["_platform_session_id"] = session_id
202
268
 
@@ -221,8 +287,13 @@ class EventHandlers:
221
287
  self.logger.warning(f"Workflow error: {e}")
222
288
 
223
289
  # Build system message (terminal display only)
224
- system_message = f"\nGobby Session ID: {session_id}"
225
- system_message += f"\nExternal ID: {external_id}"
290
+ # Display #N format if seq_num available, fallback to UUID
291
+ session_ref = (
292
+ f"#{existing_session.seq_num}" if existing_session.seq_num else session_id
293
+ )
294
+ system_message = f"\nGobby Session ID: {session_ref}"
295
+ system_message += " <- Use this for MCP tool calls (session_id parameter)"
296
+ system_message += f"\nExternal ID: {external_id} (CLI-native, rarely needed)"
226
297
  if parent_session_id:
227
298
  context_parts.append(f"Parent session: {parent_session_id}")
228
299
 
@@ -246,6 +317,7 @@ class EventHandlers:
246
317
  system_message=system_message,
247
318
  metadata={
248
319
  "session_id": session_id,
320
+ "session_ref": session_ref,
249
321
  "parent_session_id": parent_session_id,
250
322
  "machine_id": machine_id,
251
323
  "project_id": existing_session.project_id,
@@ -257,9 +329,13 @@ class EventHandlers:
257
329
  except Exception as e:
258
330
  self.logger.debug(f"No pre-created session found: {e}")
259
331
 
260
- # Step 1: Find parent session if this is a handoff (source='clear' only)
261
- parent_session_id = None
262
- if session_source == "clear" and self._session_storage:
332
+ # Step 1: Find parent session
333
+ # Check env vars first (spawned agent case), then handoff (source='clear')
334
+ parent_session_id = input_data.get("parent_session_id")
335
+ workflow_name = input_data.get("workflow_name")
336
+ agent_depth = input_data.get("agent_depth")
337
+
338
+ if not parent_session_id and session_source == "clear" and self._session_storage:
263
339
  try:
264
340
  parent = self._session_storage.find_parent(
265
341
  machine_id=machine_id,
@@ -276,6 +352,14 @@ class EventHandlers:
276
352
  # Step 2: Register new session with parent if found
277
353
  # Extract terminal context (injected by hook_dispatcher for terminal correlation)
278
354
  terminal_context = input_data.get("terminal_context")
355
+ # Parse agent_depth as int if provided
356
+ agent_depth_val = 0
357
+ if agent_depth:
358
+ try:
359
+ agent_depth_val = int(agent_depth)
360
+ except (ValueError, TypeError):
361
+ pass
362
+
279
363
  session_id = None
280
364
  if self._session_manager:
281
365
  session_id = self._session_manager.register_session(
@@ -287,6 +371,8 @@ class EventHandlers:
287
371
  source=cli_source,
288
372
  project_path=cwd,
289
373
  terminal_context=terminal_context,
374
+ workflow_name=workflow_name,
375
+ agent_depth=agent_depth_val,
290
376
  )
291
377
 
292
378
  # Step 2b: Mark parent session as expired after successful handoff
@@ -297,6 +383,10 @@ class EventHandlers:
297
383
  except Exception as e:
298
384
  self.logger.warning(f"Failed to mark parent session as expired: {e}")
299
385
 
386
+ # Step 2c: Auto-activate workflow if specified (for spawned agents)
387
+ if workflow_name and session_id:
388
+ self._auto_activate_workflow(workflow_name, session_id, cwd)
389
+
300
390
  # Step 3: Track registered session
301
391
  if transcript_path and self._session_coordinator:
302
392
  try:
@@ -333,8 +423,19 @@ class EventHandlers:
333
423
  context_parts.append(f"Parent session: {parent_session_id}")
334
424
 
335
425
  # Build system message (terminal display only)
336
- system_message = f"\nGobby Session ID: {session_id}"
337
- system_message += f"\nExternal ID: {external_id}"
426
+ # Fetch session to get seq_num for #N display
427
+ session_ref = session_id # fallback
428
+ if session_id and self._session_storage:
429
+ session_obj = self._session_storage.get(session_id)
430
+ if session_obj and session_obj.seq_num:
431
+ session_ref = f"#{session_obj.seq_num}"
432
+ # Format: "Gobby Session ID: #N" with usage hint
433
+ if session_ref and session_ref != session_id:
434
+ system_message = f"\nGobby Session ID: {session_ref}"
435
+ else:
436
+ system_message = f"\nGobby Session ID: {session_id}"
437
+ system_message += " <- Use this for MCP tool calls (session_id parameter)"
438
+ system_message += f"\nExternal ID: {external_id} (CLI-native, rarely needed)"
338
439
 
339
440
  # Add active lifecycle workflows
340
441
  if wf_response.metadata and "discovered_workflows" in wf_response.metadata:
@@ -362,6 +463,7 @@ class EventHandlers:
362
463
  # Build metadata with terminal context (filter out nulls)
363
464
  metadata: dict[str, Any] = {
364
465
  "session_id": session_id,
466
+ "session_ref": session_ref,
365
467
  "parent_session_id": parent_session_id,
366
468
  "machine_id": machine_id,
367
469
  "project_id": project_id,
@@ -691,6 +793,33 @@ class EventHandlers:
691
793
  status = "FAIL" if is_failure else "OK"
692
794
  if session_id:
693
795
  self.logger.debug(f"AFTER_TOOL [{status}]: {tool_name}, session {session_id}")
796
+
797
+ # Track edits for session high-water mark
798
+ # Only if tool succeeded, matches edit tools, and session has claimed a task
799
+ # Skip .gobby/ internal files (tasks.jsonl, memories.jsonl, etc.)
800
+ tool_input = input_data.get("tool_input", {})
801
+ file_path = tool_input.get("file_path", "")
802
+ is_gobby_internal = "/.gobby/" in file_path or file_path.startswith(".gobby/")
803
+
804
+ if (
805
+ not is_failure
806
+ and tool_name
807
+ and tool_name.lower() in EDIT_TOOLS
808
+ and not is_gobby_internal
809
+ and self._session_storage
810
+ and self._task_manager
811
+ ):
812
+ try:
813
+ # Check if session has any claimed tasks in progress
814
+ claimed_tasks = self._task_manager.list_tasks(
815
+ assignee=session_id, status="in_progress", limit=1
816
+ )
817
+ if claimed_tasks:
818
+ self._session_storage.mark_had_edits(session_id)
819
+ self.logger.debug(f"Marked session {session_id} as had_edits")
820
+ except Exception as e:
821
+ self.logger.warning(f"Failed to track edit history: {e}")
822
+
694
823
  else:
695
824
  self.logger.debug(f"AFTER_TOOL [{status}]: {tool_name}")
696
825
 
@@ -737,10 +866,23 @@ class EventHandlers:
737
866
  # ==================== COMPACT HANDLER ====================
738
867
 
739
868
  def handle_pre_compact(self, event: HookEvent) -> HookResponse:
740
- """Handle PRE_COMPACT event."""
869
+ """Handle PRE_COMPACT event.
870
+
871
+ Note: Gemini fires PreCompress constantly during normal operation,
872
+ unlike Claude which fires it only when approaching context limits.
873
+ We skip handoff logic and workflow execution for Gemini to avoid
874
+ excessive state changes and workflow interruptions.
875
+ """
876
+ from gobby.hooks.events import SessionSource
877
+
741
878
  trigger = event.data.get("trigger", "auto")
742
879
  session_id = event.metadata.get("_platform_session_id")
743
880
 
881
+ # Skip handoff logic for Gemini - it fires PreCompress too frequently
882
+ if event.source == SessionSource.GEMINI:
883
+ self.logger.debug(f"PRE_COMPACT ({trigger}): session {session_id} [Gemini - skipped]")
884
+ return HookResponse(decision="allow")
885
+
744
886
  if session_id:
745
887
  self.logger.debug(f"PRE_COMPACT ({trigger}): session {session_id}")
746
888
  # Mark session as handoff_ready so it can be found as parent after compact
@@ -228,7 +228,7 @@ class HookManager:
228
228
  )
229
229
 
230
230
  if not memory_config:
231
- from gobby.config.app import MemoryConfig
231
+ from gobby.config.persistence import MemoryConfig
232
232
 
233
233
  memory_config = MemoryConfig()
234
234
 
@@ -316,7 +316,7 @@ class HookManager:
316
316
  if self._config and hasattr(self._config, "hook_extensions"):
317
317
  webhooks_config = self._config.hook_extensions.webhooks
318
318
  if not webhooks_config:
319
- from gobby.config.app import WebhooksConfig
319
+ from gobby.config.extensions import WebhooksConfig
320
320
 
321
321
  webhooks_config = WebhooksConfig()
322
322
  self._webhook_dispatcher = WebhookDispatcher(webhooks_config)
@@ -369,6 +369,10 @@ class HookManager:
369
369
  # Skill manager for core skill injection
370
370
  self._skill_manager = HookSkillManager()
371
371
 
372
+ # Track sessions that have received full metadata injection
373
+ # Key: "{platform_session_id}:{source}" - cleared on daemon restart
374
+ self._injected_sessions: set[str] = set()
375
+
372
376
  # Event handlers (delegated to EventHandlers module)
373
377
  self._event_handlers = EventHandlers(
374
378
  session_manager=self._session_manager,
@@ -644,7 +648,21 @@ class HookManager:
644
648
  # Copy session metadata from event to response for adapter injection
645
649
  # The adapter reads response.metadata to inject session info into agent context
646
650
  if event.metadata.get("_platform_session_id"):
647
- response.metadata["session_id"] = event.metadata["_platform_session_id"]
651
+ platform_session_id = event.metadata["_platform_session_id"]
652
+ response.metadata["session_id"] = platform_session_id
653
+ # Look up seq_num for session_ref (#N format)
654
+ if self._session_storage:
655
+ session_obj = self._session_storage.get(platform_session_id)
656
+ if session_obj and session_obj.seq_num:
657
+ response.metadata["session_ref"] = f"#{session_obj.seq_num}"
658
+
659
+ # Track first hook per session for token optimization
660
+ # Adapters use this flag to inject full metadata only on first hook
661
+ session_key = f"{platform_session_id}:{event.source.value}"
662
+ is_first = session_key not in self._injected_sessions
663
+ if is_first:
664
+ self._injected_sessions.add(session_key)
665
+ response.metadata["_first_hook_for_session"] = is_first
648
666
  if event.session_id: # external_id (e.g., Claude Code's session UUID)
649
667
  response.metadata["external_id"] = event.session_id
650
668
  if event.machine_id:
gobby/hooks/plugins.py CHANGED
@@ -23,7 +23,7 @@ from typing import TYPE_CHECKING, Any
23
23
  from gobby.hooks.events import HookEvent, HookEventType, HookResponse
24
24
 
25
25
  if TYPE_CHECKING:
26
- from gobby.config.app import PluginsConfig
26
+ from gobby.config.extensions import PluginsConfig
27
27
 
28
28
  logger = logging.getLogger(__name__)
29
29
 
gobby/hooks/webhooks.py CHANGED
@@ -20,7 +20,7 @@ from typing import TYPE_CHECKING, Any
20
20
  import httpx
21
21
 
22
22
  if TYPE_CHECKING:
23
- from gobby.config.app import WebhookEndpointConfig, WebhooksConfig
23
+ from gobby.config.extensions import WebhookEndpointConfig, WebhooksConfig
24
24
  from gobby.hooks.events import HookEvent
25
25
 
26
26
  logger = logging.getLogger(__name__)