gobby 0.2.5__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.
Files changed (244) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +2 -1
  3. gobby/adapters/claude_code.py +13 -4
  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/agents/definitions.py +11 -1
  10. gobby/agents/isolation.py +395 -0
  11. gobby/agents/runner.py +8 -0
  12. gobby/agents/sandbox.py +261 -0
  13. gobby/agents/spawn.py +42 -287
  14. gobby/agents/spawn_executor.py +385 -0
  15. gobby/agents/spawners/__init__.py +24 -0
  16. gobby/agents/spawners/command_builder.py +189 -0
  17. gobby/agents/spawners/embedded.py +21 -2
  18. gobby/agents/spawners/headless.py +21 -2
  19. gobby/agents/spawners/prompt_manager.py +125 -0
  20. gobby/cli/__init__.py +6 -0
  21. gobby/cli/clones.py +419 -0
  22. gobby/cli/conductor.py +266 -0
  23. gobby/cli/install.py +4 -4
  24. gobby/cli/installers/antigravity.py +3 -9
  25. gobby/cli/installers/claude.py +15 -9
  26. gobby/cli/installers/codex.py +2 -8
  27. gobby/cli/installers/gemini.py +8 -8
  28. gobby/cli/installers/shared.py +175 -13
  29. gobby/cli/sessions.py +1 -1
  30. gobby/cli/skills.py +858 -0
  31. gobby/cli/tasks/ai.py +0 -440
  32. gobby/cli/tasks/crud.py +44 -6
  33. gobby/cli/tasks/main.py +0 -4
  34. gobby/cli/tui.py +2 -2
  35. gobby/cli/utils.py +12 -5
  36. gobby/clones/__init__.py +13 -0
  37. gobby/clones/git.py +547 -0
  38. gobby/conductor/__init__.py +16 -0
  39. gobby/conductor/alerts.py +135 -0
  40. gobby/conductor/loop.py +164 -0
  41. gobby/conductor/monitors/__init__.py +11 -0
  42. gobby/conductor/monitors/agents.py +116 -0
  43. gobby/conductor/monitors/tasks.py +155 -0
  44. gobby/conductor/pricing.py +234 -0
  45. gobby/conductor/token_tracker.py +160 -0
  46. gobby/config/__init__.py +12 -97
  47. gobby/config/app.py +69 -91
  48. gobby/config/extensions.py +2 -2
  49. gobby/config/features.py +7 -130
  50. gobby/config/search.py +110 -0
  51. gobby/config/servers.py +1 -1
  52. gobby/config/skills.py +43 -0
  53. gobby/config/tasks.py +9 -41
  54. gobby/hooks/__init__.py +0 -13
  55. gobby/hooks/event_handlers.py +188 -2
  56. gobby/hooks/hook_manager.py +50 -4
  57. gobby/hooks/plugins.py +1 -1
  58. gobby/hooks/skill_manager.py +130 -0
  59. gobby/hooks/webhooks.py +1 -1
  60. gobby/install/claude/hooks/hook_dispatcher.py +4 -4
  61. gobby/install/codex/hooks/hook_dispatcher.py +1 -1
  62. gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
  63. gobby/llm/claude.py +22 -34
  64. gobby/llm/claude_executor.py +46 -256
  65. gobby/llm/codex_executor.py +59 -291
  66. gobby/llm/executor.py +21 -0
  67. gobby/llm/gemini.py +134 -110
  68. gobby/llm/litellm_executor.py +143 -6
  69. gobby/llm/resolver.py +98 -35
  70. gobby/mcp_proxy/importer.py +62 -4
  71. gobby/mcp_proxy/instructions.py +56 -0
  72. gobby/mcp_proxy/models.py +15 -0
  73. gobby/mcp_proxy/registries.py +68 -8
  74. gobby/mcp_proxy/server.py +33 -3
  75. gobby/mcp_proxy/services/recommendation.py +43 -11
  76. gobby/mcp_proxy/services/tool_proxy.py +81 -1
  77. gobby/mcp_proxy/stdio.py +2 -1
  78. gobby/mcp_proxy/tools/__init__.py +0 -2
  79. gobby/mcp_proxy/tools/agent_messaging.py +317 -0
  80. gobby/mcp_proxy/tools/agents.py +31 -731
  81. gobby/mcp_proxy/tools/clones.py +518 -0
  82. gobby/mcp_proxy/tools/memory.py +3 -26
  83. gobby/mcp_proxy/tools/metrics.py +65 -1
  84. gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
  85. gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
  86. gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
  87. gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
  88. gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
  89. gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
  90. gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
  91. gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
  92. gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
  93. gobby/mcp_proxy/tools/skills/__init__.py +616 -0
  94. gobby/mcp_proxy/tools/spawn_agent.py +417 -0
  95. gobby/mcp_proxy/tools/task_orchestration.py +7 -0
  96. gobby/mcp_proxy/tools/task_readiness.py +14 -0
  97. gobby/mcp_proxy/tools/task_sync.py +1 -1
  98. gobby/mcp_proxy/tools/tasks/_context.py +0 -20
  99. gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
  100. gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
  101. gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
  102. gobby/mcp_proxy/tools/tasks/_lifecycle.py +110 -45
  103. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
  104. gobby/mcp_proxy/tools/workflows.py +1 -1
  105. gobby/mcp_proxy/tools/worktrees.py +0 -338
  106. gobby/memory/backends/__init__.py +6 -1
  107. gobby/memory/backends/mem0.py +6 -1
  108. gobby/memory/extractor.py +477 -0
  109. gobby/memory/ingestion/__init__.py +5 -0
  110. gobby/memory/ingestion/multimodal.py +221 -0
  111. gobby/memory/manager.py +73 -285
  112. gobby/memory/search/__init__.py +10 -0
  113. gobby/memory/search/coordinator.py +248 -0
  114. gobby/memory/services/__init__.py +5 -0
  115. gobby/memory/services/crossref.py +142 -0
  116. gobby/prompts/loader.py +5 -2
  117. gobby/runner.py +37 -16
  118. gobby/search/__init__.py +48 -6
  119. gobby/search/backends/__init__.py +159 -0
  120. gobby/search/backends/embedding.py +225 -0
  121. gobby/search/embeddings.py +238 -0
  122. gobby/search/models.py +148 -0
  123. gobby/search/unified.py +496 -0
  124. gobby/servers/http.py +24 -12
  125. gobby/servers/routes/admin.py +294 -0
  126. gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
  127. gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
  128. gobby/servers/routes/mcp/endpoints/execution.py +568 -0
  129. gobby/servers/routes/mcp/endpoints/registry.py +378 -0
  130. gobby/servers/routes/mcp/endpoints/server.py +304 -0
  131. gobby/servers/routes/mcp/hooks.py +1 -1
  132. gobby/servers/routes/mcp/tools.py +48 -1317
  133. gobby/servers/websocket.py +2 -2
  134. gobby/sessions/analyzer.py +2 -0
  135. gobby/sessions/lifecycle.py +1 -1
  136. gobby/sessions/processor.py +10 -0
  137. gobby/sessions/transcripts/base.py +2 -0
  138. gobby/sessions/transcripts/claude.py +79 -10
  139. gobby/skills/__init__.py +91 -0
  140. gobby/skills/loader.py +685 -0
  141. gobby/skills/manager.py +384 -0
  142. gobby/skills/parser.py +286 -0
  143. gobby/skills/search.py +463 -0
  144. gobby/skills/sync.py +119 -0
  145. gobby/skills/updater.py +385 -0
  146. gobby/skills/validator.py +368 -0
  147. gobby/storage/clones.py +378 -0
  148. gobby/storage/database.py +1 -1
  149. gobby/storage/memories.py +43 -13
  150. gobby/storage/migrations.py +162 -201
  151. gobby/storage/sessions.py +116 -7
  152. gobby/storage/skills.py +782 -0
  153. gobby/storage/tasks/_crud.py +4 -4
  154. gobby/storage/tasks/_lifecycle.py +57 -7
  155. gobby/storage/tasks/_manager.py +14 -5
  156. gobby/storage/tasks/_models.py +8 -3
  157. gobby/sync/memories.py +40 -5
  158. gobby/sync/tasks.py +83 -6
  159. gobby/tasks/__init__.py +1 -2
  160. gobby/tasks/external_validator.py +1 -1
  161. gobby/tasks/validation.py +46 -35
  162. gobby/tools/summarizer.py +91 -10
  163. gobby/tui/api_client.py +4 -7
  164. gobby/tui/app.py +5 -3
  165. gobby/tui/screens/orchestrator.py +1 -2
  166. gobby/tui/screens/tasks.py +2 -4
  167. gobby/tui/ws_client.py +1 -1
  168. gobby/utils/daemon_client.py +2 -2
  169. gobby/utils/project_context.py +2 -3
  170. gobby/utils/status.py +13 -0
  171. gobby/workflows/actions.py +221 -1135
  172. gobby/workflows/artifact_actions.py +31 -0
  173. gobby/workflows/autonomous_actions.py +11 -0
  174. gobby/workflows/context_actions.py +93 -1
  175. gobby/workflows/detection_helpers.py +115 -31
  176. gobby/workflows/enforcement/__init__.py +47 -0
  177. gobby/workflows/enforcement/blocking.py +269 -0
  178. gobby/workflows/enforcement/commit_policy.py +283 -0
  179. gobby/workflows/enforcement/handlers.py +269 -0
  180. gobby/workflows/{task_enforcement_actions.py → enforcement/task_policy.py} +29 -388
  181. gobby/workflows/engine.py +13 -2
  182. gobby/workflows/git_utils.py +106 -0
  183. gobby/workflows/lifecycle_evaluator.py +29 -1
  184. gobby/workflows/llm_actions.py +30 -0
  185. gobby/workflows/loader.py +19 -6
  186. gobby/workflows/mcp_actions.py +20 -1
  187. gobby/workflows/memory_actions.py +154 -0
  188. gobby/workflows/safe_evaluator.py +183 -0
  189. gobby/workflows/session_actions.py +44 -0
  190. gobby/workflows/state_actions.py +60 -1
  191. gobby/workflows/stop_signal_actions.py +55 -0
  192. gobby/workflows/summary_actions.py +111 -1
  193. gobby/workflows/task_sync_actions.py +347 -0
  194. gobby/workflows/todo_actions.py +34 -1
  195. gobby/workflows/webhook_actions.py +185 -0
  196. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/METADATA +87 -21
  197. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/RECORD +201 -172
  198. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
  199. gobby/adapters/codex.py +0 -1292
  200. gobby/install/claude/commands/gobby/bug.md +0 -51
  201. gobby/install/claude/commands/gobby/chore.md +0 -51
  202. gobby/install/claude/commands/gobby/epic.md +0 -52
  203. gobby/install/claude/commands/gobby/eval.md +0 -235
  204. gobby/install/claude/commands/gobby/feat.md +0 -49
  205. gobby/install/claude/commands/gobby/nit.md +0 -52
  206. gobby/install/claude/commands/gobby/ref.md +0 -52
  207. gobby/install/codex/prompts/forget.md +0 -7
  208. gobby/install/codex/prompts/memories.md +0 -7
  209. gobby/install/codex/prompts/recall.md +0 -7
  210. gobby/install/codex/prompts/remember.md +0 -13
  211. gobby/llm/gemini_executor.py +0 -339
  212. gobby/mcp_proxy/tools/session_messages.py +0 -1056
  213. gobby/mcp_proxy/tools/task_expansion.py +0 -591
  214. gobby/prompts/defaults/expansion/system.md +0 -119
  215. gobby/prompts/defaults/expansion/user.md +0 -48
  216. gobby/prompts/defaults/external_validation/agent.md +0 -72
  217. gobby/prompts/defaults/external_validation/external.md +0 -63
  218. gobby/prompts/defaults/external_validation/spawn.md +0 -83
  219. gobby/prompts/defaults/external_validation/system.md +0 -6
  220. gobby/prompts/defaults/features/import_mcp.md +0 -22
  221. gobby/prompts/defaults/features/import_mcp_github.md +0 -17
  222. gobby/prompts/defaults/features/import_mcp_search.md +0 -16
  223. gobby/prompts/defaults/features/recommend_tools.md +0 -32
  224. gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
  225. gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
  226. gobby/prompts/defaults/features/server_description.md +0 -20
  227. gobby/prompts/defaults/features/server_description_system.md +0 -6
  228. gobby/prompts/defaults/features/task_description.md +0 -31
  229. gobby/prompts/defaults/features/task_description_system.md +0 -6
  230. gobby/prompts/defaults/features/tool_summary.md +0 -17
  231. gobby/prompts/defaults/features/tool_summary_system.md +0 -6
  232. gobby/prompts/defaults/research/step.md +0 -58
  233. gobby/prompts/defaults/validation/criteria.md +0 -47
  234. gobby/prompts/defaults/validation/validate.md +0 -38
  235. gobby/storage/migrations_legacy.py +0 -1359
  236. gobby/tasks/context.py +0 -747
  237. gobby/tasks/criteria.py +0 -342
  238. gobby/tasks/expansion.py +0 -626
  239. gobby/tasks/prompts/expand.py +0 -327
  240. gobby/tasks/research.py +0 -421
  241. gobby/tasks/tdd.py +0 -352
  242. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
  243. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
  244. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
@@ -17,7 +17,9 @@ from typing import TYPE_CHECKING, Any
17
17
  from gobby.hooks.events import HookEvent, HookEventType, HookResponse
18
18
 
19
19
  if TYPE_CHECKING:
20
+ from gobby.config.skills import SkillsConfig
20
21
  from gobby.hooks.session_coordinator import SessionCoordinator
22
+ from gobby.hooks.skill_manager import HookSkillManager
21
23
  from gobby.sessions.manager import SessionManager
22
24
  from gobby.sessions.summary import SummaryFileGenerator
23
25
  from gobby.storage.session_messages import LocalSessionMessageManager
@@ -27,6 +29,16 @@ if TYPE_CHECKING:
27
29
  from gobby.workflows.hooks import WorkflowHookHandler
28
30
 
29
31
 
32
+ EDIT_TOOLS = {
33
+ "write_file",
34
+ "replace",
35
+ "edit_file",
36
+ "notebook_edit",
37
+ "edit",
38
+ "write",
39
+ }
40
+
41
+
30
42
  class EventHandlers:
31
43
  """
32
44
  Manages event handler registration and dispatch.
@@ -48,6 +60,8 @@ class EventHandlers:
48
60
  task_manager: LocalTaskManager | None = None,
49
61
  session_coordinator: SessionCoordinator | None = None,
50
62
  message_manager: LocalSessionMessageManager | None = None,
63
+ skill_manager: HookSkillManager | None = None,
64
+ skills_config: SkillsConfig | None = None,
51
65
  get_machine_id: Callable[[], str] | None = None,
52
66
  resolve_project_id: Callable[[str | None, str | None], str] | None = None,
53
67
  logger: logging.Logger | None = None,
@@ -65,6 +79,8 @@ class EventHandlers:
65
79
  task_manager: LocalTaskManager for task operations
66
80
  session_coordinator: SessionCoordinator for session tracking
67
81
  message_manager: LocalSessionMessageManager for messages
82
+ skill_manager: HookSkillManager for skill discovery
83
+ skills_config: SkillsConfig for skill injection settings
68
84
  get_machine_id: Function to get machine ID
69
85
  resolve_project_id: Function to resolve project ID from cwd
70
86
  logger: Optional logger instance
@@ -78,6 +94,8 @@ class EventHandlers:
78
94
  self._task_manager = task_manager
79
95
  self._session_coordinator = session_coordinator
80
96
  self._message_manager = message_manager
97
+ self._skill_manager = skill_manager
98
+ self._skills_config = skills_config
81
99
  self._get_machine_id = get_machine_id or (lambda: "unknown-machine")
82
100
  self._resolve_project_id = resolve_project_id or (lambda p, c: p or "")
83
101
  self.logger = logger or logging.getLogger(__name__)
@@ -213,9 +231,27 @@ class EventHandlers:
213
231
  self.logger.warning(f"Workflow error: {e}")
214
232
 
215
233
  # Build system message (terminal display only)
216
- system_message = "\nSession enhanced by gobby."
234
+ # Display #N format if seq_num available, fallback to UUID
235
+ session_ref = (
236
+ f"#{existing_session.seq_num}" if existing_session.seq_num else session_id
237
+ )
238
+ system_message = f"\nGobby Session Ref: {session_ref}"
239
+ system_message += f"\nGobby Session ID: {session_id}"
240
+ system_message += f"\nExternal ID: {external_id}"
217
241
  if parent_session_id:
218
242
  context_parts.append(f"Parent session: {parent_session_id}")
243
+
244
+ # Add active lifecycle workflows
245
+ if wf_response.metadata and "discovered_workflows" in wf_response.metadata:
246
+ wf_list = wf_response.metadata["discovered_workflows"]
247
+ if wf_list:
248
+ system_message += "\nActive workflows:"
249
+ for w in wf_list:
250
+ source = "project" if w["is_project"] else "global"
251
+ system_message += (
252
+ f"\n - {w['name']} ({source}, priority={w['priority']})"
253
+ )
254
+
219
255
  if wf_response.system_message:
220
256
  system_message += f"\n\n{wf_response.system_message}"
221
257
 
@@ -312,7 +348,25 @@ class EventHandlers:
312
348
  context_parts.append(f"Parent session: {parent_session_id}")
313
349
 
314
350
  # Build system message (terminal display only)
315
- system_message = "\nSession enhanced by gobby."
351
+ # Fetch session to get seq_num for #N display
352
+ session_ref = session_id # fallback
353
+ if session_id and self._session_storage:
354
+ session_obj = self._session_storage.get(session_id)
355
+ if session_obj and session_obj.seq_num:
356
+ session_ref = f"#{session_obj.seq_num}"
357
+ system_message = f"\nGobby Session Ref: {session_ref}"
358
+ system_message += f"\nGobby Session ID: {session_id}"
359
+ system_message += f"\nExternal ID: {external_id}"
360
+
361
+ # Add active lifecycle workflows
362
+ if wf_response.metadata and "discovered_workflows" in wf_response.metadata:
363
+ wf_list = wf_response.metadata["discovered_workflows"]
364
+ if wf_list:
365
+ system_message += "\nActive workflows:"
366
+ for w in wf_list:
367
+ source = "project" if w["is_project"] else "global"
368
+ system_message += f"\n - {w['name']} ({source}, priority={w['priority']})"
369
+
316
370
  if wf_response.system_message:
317
371
  system_message += f"\n\n{wf_response.system_message}"
318
372
 
@@ -322,6 +376,11 @@ class EventHandlers:
322
376
  context_parts.append("\n## Active Task Context\n")
323
377
  context_parts.append(f"You are working on task: {task_title} ({event.task_id})")
324
378
 
379
+ # Inject core skills if enabled (restoring from parent session if available)
380
+ skill_context = self._build_skill_injection_context(parent_session_id)
381
+ if skill_context:
382
+ context_parts.append(skill_context)
383
+
325
384
  # Build metadata with terminal context (filter out nulls)
326
385
  metadata: dict[str, Any] = {
327
386
  "session_id": session_id,
@@ -434,6 +493,112 @@ class EventHandlers:
434
493
 
435
494
  return HookResponse(decision="allow")
436
495
 
496
+ def _build_skill_injection_context(self, parent_session_id: str | None = None) -> str | None:
497
+ """Build skill injection context for session-start.
498
+
499
+ Combines alwaysApply skills with skills restored from parent session.
500
+
501
+ Args:
502
+ parent_session_id: Optional parent session ID to restore skills from
503
+
504
+ Returns context string with available skills if injection is enabled,
505
+ or None if disabled.
506
+ """
507
+ # Skip if no skill manager or config
508
+ if not self._skill_manager or not self._skills_config:
509
+ return None
510
+
511
+ # Check if injection is enabled
512
+ if not self._skills_config.inject_core_skills:
513
+ return None
514
+
515
+ # Check injection format
516
+ if self._skills_config.injection_format == "none":
517
+ return None
518
+
519
+ # Get alwaysApply skills
520
+ try:
521
+ core_skills = self._skill_manager.discover_core_skills()
522
+ always_apply_skills = [s for s in core_skills if s.is_always_apply()]
523
+
524
+ # Get restored skills from parent session
525
+ restored_skills = self._restore_skills_from_parent(parent_session_id)
526
+
527
+ # Combine: alwaysApply skills + any additional restored skills
528
+ skill_names = [s.name for s in always_apply_skills]
529
+ for skill_name in restored_skills:
530
+ if skill_name not in skill_names:
531
+ skill_names.append(skill_name)
532
+
533
+ if not skill_names:
534
+ return None
535
+
536
+ # Build context based on format
537
+ if self._skills_config.injection_format == "summary":
538
+ return (
539
+ "\n## Available Skills\n"
540
+ f"The following skills are always available: {', '.join(skill_names)}\n"
541
+ "Use the /skill-name syntax to invoke them."
542
+ )
543
+ elif self._skills_config.injection_format == "full":
544
+ parts = ["\n## Available Skills\n"]
545
+ # Build a map of always_apply skills for quick lookup
546
+ always_apply_map = {s.name: s for s in always_apply_skills}
547
+ # Iterate over combined skill_names list (always_apply + restored)
548
+ for skill_name in skill_names:
549
+ parts.append(f"### {skill_name}")
550
+ # Get description from always_apply skill if available
551
+ if skill_name in always_apply_map:
552
+ skill = always_apply_map[skill_name]
553
+ if skill.description:
554
+ parts.append(skill.description)
555
+ parts.append("")
556
+ return "\n".join(parts)
557
+ else:
558
+ return None
559
+
560
+ except Exception as e:
561
+ self.logger.warning(f"Failed to build skill injection context: {e}")
562
+ return None
563
+
564
+ def _restore_skills_from_parent(self, parent_session_id: str | None) -> list[str]:
565
+ """Restore active skills from parent session's handoff context.
566
+
567
+ Args:
568
+ parent_session_id: Parent session ID to restore from
569
+
570
+ Returns:
571
+ List of skill names from the parent session
572
+ """
573
+ if not parent_session_id or not self._session_storage:
574
+ return []
575
+
576
+ try:
577
+ parent = self._session_storage.get(parent_session_id)
578
+ if not parent:
579
+ return []
580
+
581
+ compact_md = getattr(parent, "compact_markdown", None)
582
+ if not compact_md:
583
+ return []
584
+
585
+ # Parse active skills from markdown
586
+ # Format: "### Active Skills\nSkills available: skill1, skill2, skill3"
587
+ import re
588
+
589
+ match = re.search(r"### Active Skills\s*\nSkills available:\s*([^\n]+)", compact_md)
590
+ if match:
591
+ skills_str = match.group(1).strip()
592
+ skills = [s.strip() for s in skills_str.split(",") if s.strip()]
593
+ self.logger.debug(f"Restored {len(skills)} skills from parent session")
594
+ return skills
595
+
596
+ return []
597
+
598
+ except Exception as e:
599
+ self.logger.warning(f"Failed to restore skills from parent: {e}")
600
+ return []
601
+
437
602
  # ==================== AGENT HANDLERS ====================
438
603
 
439
604
  def handle_before_agent(self, event: HookEvent) -> HookResponse:
@@ -548,6 +713,27 @@ class EventHandlers:
548
713
  status = "FAIL" if is_failure else "OK"
549
714
  if session_id:
550
715
  self.logger.debug(f"AFTER_TOOL [{status}]: {tool_name}, session {session_id}")
716
+
717
+ # Track edits for session high-water mark
718
+ # Only if tool succeeded, matches edit tools, and session has claimed a task
719
+ if (
720
+ not is_failure
721
+ and tool_name
722
+ and tool_name.lower() in EDIT_TOOLS
723
+ and self._session_storage
724
+ and self._task_manager
725
+ ):
726
+ try:
727
+ # Check if session has any claimed tasks in progress
728
+ claimed_tasks = self._task_manager.list_tasks(
729
+ assignee=session_id, status="in_progress", limit=1
730
+ )
731
+ if claimed_tasks:
732
+ self._session_storage.mark_had_edits(session_id)
733
+ self.logger.debug(f"Marked session {session_id} as had_edits")
734
+ except Exception as e:
735
+ self.logger.warning(f"Failed to track edit history: {e}")
736
+
551
737
  else:
552
738
  self.logger.debug(f"AFTER_TOOL [{status}]: {tool_name}")
553
739
 
@@ -17,7 +17,7 @@ Example:
17
17
 
18
18
  manager = HookManager(
19
19
  daemon_host="localhost",
20
- daemon_port=8765
20
+ daemon_port=60887
21
21
  )
22
22
 
23
23
  result = manager.execute(
@@ -42,6 +42,7 @@ from gobby.hooks.events import HookEvent, HookEventType, HookResponse
42
42
  from gobby.hooks.health_monitor import HealthMonitor
43
43
  from gobby.hooks.plugins import PluginLoader, run_plugin_handlers
44
44
  from gobby.hooks.session_coordinator import SessionCoordinator
45
+ from gobby.hooks.skill_manager import HookSkillManager
45
46
  from gobby.hooks.webhooks import WebhookDispatcher
46
47
  from gobby.memory.manager import MemoryManager
47
48
  from gobby.sessions.manager import SessionManager
@@ -79,6 +80,21 @@ class HookManager:
79
80
  - TranscriptProcessor: JSONL parsing and analysis
80
81
  - WorkflowEngine: Handles session handoff and LLM-powered summaries
81
82
 
83
+ Session ID Mapping:
84
+ There are two types of session IDs used throughout the system:
85
+
86
+ | Name | Description | Example |
87
+ |--------------------------|------------------------------------------------|----------------------------------------|
88
+ | external_id / session_id | CLI's internal session UUID (Claude Code, etc) | 683bc13e-091e-4911-9e59-e7546e385cd6 |
89
+ | _platform_session_id | Gobby's internal session.id (database PK) | 0ebb2c00-0f58-4c39-9370-eba1833dec33 |
90
+
91
+ The _platform_session_id is derived from session_manager.get_session_id(external_id, source)
92
+ which looks up Gobby's session by the CLI's external_id.
93
+
94
+ When injecting into agent context:
95
+ - "session_id" in response.metadata = Gobby's _platform_session_id (for MCP tool calls)
96
+ - "external_id" in response.metadata = CLI's session UUID (for transcript lookups)
97
+
82
98
  Attributes:
83
99
  daemon_host: Host for daemon communication
84
100
  daemon_port: Port for daemon communication
@@ -89,7 +105,7 @@ class HookManager:
89
105
  def __init__(
90
106
  self,
91
107
  daemon_host: str = "localhost",
92
- daemon_port: int = 8765,
108
+ daemon_port: int = 60887,
93
109
  llm_service: "LLMService | None" = None,
94
110
  config: Any | None = None,
95
111
  log_file: str | None = None,
@@ -212,7 +228,7 @@ class HookManager:
212
228
  )
213
229
 
214
230
  if not memory_config:
215
- from gobby.config.app import MemoryConfig
231
+ from gobby.config.persistence import MemoryConfig
216
232
 
217
233
  memory_config = MemoryConfig()
218
234
 
@@ -300,7 +316,7 @@ class HookManager:
300
316
  if self._config and hasattr(self._config, "hook_extensions"):
301
317
  webhooks_config = self._config.hook_extensions.webhooks
302
318
  if not webhooks_config:
303
- from gobby.config.app import WebhooksConfig
319
+ from gobby.config.extensions import WebhooksConfig
304
320
 
305
321
  webhooks_config = WebhooksConfig()
306
322
  self._webhook_dispatcher = WebhookDispatcher(webhooks_config)
@@ -350,6 +366,9 @@ class HookManager:
350
366
  logger=self.logger,
351
367
  )
352
368
 
369
+ # Skill manager for core skill injection
370
+ self._skill_manager = HookSkillManager()
371
+
353
372
  # Event handlers (delegated to EventHandlers module)
354
373
  self._event_handlers = EventHandlers(
355
374
  session_manager=self._session_manager,
@@ -361,6 +380,8 @@ class HookManager:
361
380
  task_manager=self._task_manager,
362
381
  session_coordinator=self._session_coordinator,
363
382
  message_manager=self._message_manager,
383
+ skill_manager=self._skill_manager,
384
+ skills_config=self._config.skills if self._config else None,
364
385
  get_machine_id=self.get_machine_id,
365
386
  resolve_project_id=self._resolve_project_id,
366
387
  logger=self.logger,
@@ -620,6 +641,31 @@ class HookManager:
620
641
  try:
621
642
  response = handler(event)
622
643
 
644
+ # Copy session metadata from event to response for adapter injection
645
+ # The adapter reads response.metadata to inject session info into agent context
646
+ if event.metadata.get("_platform_session_id"):
647
+ response.metadata["session_id"] = event.metadata["_platform_session_id"]
648
+ if event.session_id: # external_id (e.g., Claude Code's session UUID)
649
+ response.metadata["external_id"] = event.session_id
650
+ if event.machine_id:
651
+ response.metadata["machine_id"] = event.machine_id
652
+ if event.project_id:
653
+ response.metadata["project_id"] = event.project_id
654
+ # Copy terminal context if present
655
+ for key in [
656
+ "terminal_term_program",
657
+ "terminal_tty",
658
+ "terminal_parent_pid",
659
+ "terminal_iterm_session_id",
660
+ "terminal_term_session_id",
661
+ "terminal_kitty_window_id",
662
+ "terminal_tmux_pane",
663
+ "terminal_vscode_terminal_id",
664
+ "terminal_alacritty_socket",
665
+ ]:
666
+ if event.metadata.get(key):
667
+ response.metadata[key] = event.metadata[key]
668
+
623
669
  # Merge workflow context if present
624
670
  if workflow_context:
625
671
  if response.context:
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
 
@@ -0,0 +1,130 @@
1
+ """HookSkillManager - Skill management for the hook system.
2
+
3
+ This module provides skill discovery and management for the hook system,
4
+ allowing hooks to access and use skills (Agent Skills specification).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from pathlib import Path
11
+
12
+ from gobby.skills.loader import SkillLoader
13
+ from gobby.skills.parser import ParsedSkill
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class HookSkillManager:
19
+ """Manage skills for the hook system.
20
+
21
+ Provides discovery and access to core skills bundled with Gobby,
22
+ as well as project-specific skills.
23
+
24
+ Example usage:
25
+ ```python
26
+ from gobby.hooks.skill_manager import HookSkillManager
27
+
28
+ manager = HookSkillManager()
29
+ skills = manager.discover_core_skills()
30
+
31
+ # Get a specific skill
32
+ tasks_skill = manager.get_skill_by_name("gobby-tasks")
33
+ ```
34
+ """
35
+
36
+ def __init__(self) -> None:
37
+ """Initialize the skill manager."""
38
+ # Path to built-in skills: src/gobby/hooks/ -> src/gobby/install/shared/skills/
39
+ self._base_dir = Path(__file__).parent.parent
40
+ self._core_skills_path = self._base_dir / "install" / "shared" / "skills"
41
+
42
+ # Loader for parsing skills (use "filesystem" for bundled core skills)
43
+ self._loader = SkillLoader(default_source_type="filesystem")
44
+
45
+ # Cache of discovered skills
46
+ self._core_skills: list[ParsedSkill] | None = None
47
+
48
+ def discover_core_skills(self) -> list[ParsedSkill]:
49
+ """Discover built-in skills from install/shared/skills/.
50
+
51
+ Returns:
52
+ List of ParsedSkill objects for all valid core skills.
53
+ Invalid skills are logged as warnings and skipped.
54
+ """
55
+ if self._core_skills is not None:
56
+ return self._core_skills
57
+
58
+ if not self._core_skills_path.exists():
59
+ logger.warning(f"Core skills path not found: {self._core_skills_path}")
60
+ self._core_skills = []
61
+ return self._core_skills
62
+
63
+ # Load all skills from the core directory
64
+ self._core_skills = self._loader.load_directory(
65
+ self._core_skills_path,
66
+ validate=True,
67
+ )
68
+
69
+ logger.debug(f"Discovered {len(self._core_skills)} core skills")
70
+ return self._core_skills
71
+
72
+ def get_skill_by_name(self, name: str) -> ParsedSkill | None:
73
+ """Get a skill by name.
74
+
75
+ Args:
76
+ name: The skill name to look up.
77
+
78
+ Returns:
79
+ ParsedSkill if found, None otherwise.
80
+ """
81
+ # Ensure skills are discovered
82
+ skills = self.discover_core_skills()
83
+
84
+ for skill in skills:
85
+ if skill.name == name:
86
+ return skill
87
+
88
+ return None
89
+
90
+ def refresh(self) -> None:
91
+ """Clear the cache and rediscover skills."""
92
+ self._core_skills = None
93
+
94
+ def recommend_skills(self, category: str | None = None) -> list[str]:
95
+ """Recommend relevant skills based on task category.
96
+
97
+ Maps task categories to relevant core skills that would be helpful
98
+ for that type of work.
99
+
100
+ Args:
101
+ category: Task category (e.g., 'code', 'docs', 'test', 'config')
102
+
103
+ Returns:
104
+ List of skill names that are relevant for the category
105
+ """
106
+ # Category to skill mappings
107
+ category_skills: dict[str, list[str]] = {
108
+ "code": ["gobby-tasks", "gobby-expand", "gobby-worktrees"],
109
+ "test": ["gobby-tasks", "gobby-expand"],
110
+ "docs": ["gobby-tasks", "gobby-plan"],
111
+ "config": ["gobby-tasks", "gobby-mcp"],
112
+ "refactor": ["gobby-tasks", "gobby-expand", "gobby-worktrees"],
113
+ "planning": ["gobby-tasks", "gobby-plan", "gobby-expand"],
114
+ "research": ["gobby-tasks", "gobby-memory"],
115
+ }
116
+
117
+ # Get skills for the category (or empty list if no match)
118
+ recommended = category_skills.get(category or "", [])
119
+
120
+ # Always include alwaysApply skills
121
+ skills = self.discover_core_skills()
122
+ always_apply = [s.name for s in skills if s.is_always_apply()]
123
+
124
+ # Combine and dedupe while preserving order
125
+ result = list(always_apply)
126
+ for skill_name in recommended:
127
+ if skill_name not in result:
128
+ result.append(skill_name)
129
+
130
+ return result
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__)
@@ -23,7 +23,7 @@ from pathlib import Path
23
23
  # No longer need to import HookManager - we call it via HTTP daemon instead
24
24
 
25
25
  # Default daemon configuration
26
- DEFAULT_DAEMON_PORT = 8765
26
+ DEFAULT_DAEMON_PORT = 60887
27
27
  DEFAULT_CONFIG_PATH = "~/.gobby/config.yaml"
28
28
 
29
29
 
@@ -31,10 +31,10 @@ def get_daemon_url() -> str:
31
31
  """Get the daemon HTTP URL from config file.
32
32
 
33
33
  Reads daemon_port from ~/.gobby/config.yaml if it exists,
34
- otherwise uses the default port 8765.
34
+ otherwise uses the default port 60887.
35
35
 
36
36
  Returns:
37
- Full daemon URL like http://localhost:8765
37
+ Full daemon URL like http://localhost:60887
38
38
  """
39
39
  config_path = Path(DEFAULT_CONFIG_PATH).expanduser()
40
40
 
@@ -308,7 +308,7 @@ def main() -> int:
308
308
  "input_data": input_data,
309
309
  "source": "claude", # Required: identifies CLI source
310
310
  },
311
- timeout=30.0, # Generous timeout for hook processing
311
+ timeout=90.0, # LLM-powered hooks (pre-compact summary) need more time
312
312
  )
313
313
 
314
314
  if response.status_code == 200:
@@ -19,7 +19,7 @@ from typing import Any
19
19
 
20
20
  import httpx
21
21
 
22
- DEFAULT_DAEMON_PORT = 8765
22
+ DEFAULT_DAEMON_PORT = 60887
23
23
  DEFAULT_CONFIG_PATH = "~/.gobby/config.yaml"
24
24
  DEBUG_ENV_VAR = "GOBBY_CODEX_NOTIFY_DEBUG"
25
25