gobby 0.2.5__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 (383) hide show
  1. gobby/__init__.py +3 -0
  2. gobby/adapters/__init__.py +30 -0
  3. gobby/adapters/base.py +93 -0
  4. gobby/adapters/claude_code.py +276 -0
  5. gobby/adapters/codex.py +1292 -0
  6. gobby/adapters/gemini.py +343 -0
  7. gobby/agents/__init__.py +37 -0
  8. gobby/agents/codex_session.py +120 -0
  9. gobby/agents/constants.py +112 -0
  10. gobby/agents/context.py +362 -0
  11. gobby/agents/definitions.py +133 -0
  12. gobby/agents/gemini_session.py +111 -0
  13. gobby/agents/registry.py +618 -0
  14. gobby/agents/runner.py +968 -0
  15. gobby/agents/session.py +259 -0
  16. gobby/agents/spawn.py +916 -0
  17. gobby/agents/spawners/__init__.py +77 -0
  18. gobby/agents/spawners/base.py +142 -0
  19. gobby/agents/spawners/cross_platform.py +266 -0
  20. gobby/agents/spawners/embedded.py +225 -0
  21. gobby/agents/spawners/headless.py +226 -0
  22. gobby/agents/spawners/linux.py +125 -0
  23. gobby/agents/spawners/macos.py +277 -0
  24. gobby/agents/spawners/windows.py +308 -0
  25. gobby/agents/tty_config.py +319 -0
  26. gobby/autonomous/__init__.py +32 -0
  27. gobby/autonomous/progress_tracker.py +447 -0
  28. gobby/autonomous/stop_registry.py +269 -0
  29. gobby/autonomous/stuck_detector.py +383 -0
  30. gobby/cli/__init__.py +67 -0
  31. gobby/cli/__main__.py +8 -0
  32. gobby/cli/agents.py +529 -0
  33. gobby/cli/artifacts.py +266 -0
  34. gobby/cli/daemon.py +329 -0
  35. gobby/cli/extensions.py +526 -0
  36. gobby/cli/github.py +263 -0
  37. gobby/cli/init.py +53 -0
  38. gobby/cli/install.py +614 -0
  39. gobby/cli/installers/__init__.py +37 -0
  40. gobby/cli/installers/antigravity.py +65 -0
  41. gobby/cli/installers/claude.py +363 -0
  42. gobby/cli/installers/codex.py +192 -0
  43. gobby/cli/installers/gemini.py +294 -0
  44. gobby/cli/installers/git_hooks.py +377 -0
  45. gobby/cli/installers/shared.py +737 -0
  46. gobby/cli/linear.py +250 -0
  47. gobby/cli/mcp.py +30 -0
  48. gobby/cli/mcp_proxy.py +698 -0
  49. gobby/cli/memory.py +304 -0
  50. gobby/cli/merge.py +384 -0
  51. gobby/cli/projects.py +79 -0
  52. gobby/cli/sessions.py +622 -0
  53. gobby/cli/tasks/__init__.py +30 -0
  54. gobby/cli/tasks/_utils.py +658 -0
  55. gobby/cli/tasks/ai.py +1025 -0
  56. gobby/cli/tasks/commits.py +169 -0
  57. gobby/cli/tasks/crud.py +685 -0
  58. gobby/cli/tasks/deps.py +135 -0
  59. gobby/cli/tasks/labels.py +63 -0
  60. gobby/cli/tasks/main.py +273 -0
  61. gobby/cli/tasks/search.py +178 -0
  62. gobby/cli/tui.py +34 -0
  63. gobby/cli/utils.py +513 -0
  64. gobby/cli/workflows.py +927 -0
  65. gobby/cli/worktrees.py +481 -0
  66. gobby/config/__init__.py +129 -0
  67. gobby/config/app.py +551 -0
  68. gobby/config/extensions.py +167 -0
  69. gobby/config/features.py +472 -0
  70. gobby/config/llm_providers.py +98 -0
  71. gobby/config/logging.py +66 -0
  72. gobby/config/mcp.py +346 -0
  73. gobby/config/persistence.py +247 -0
  74. gobby/config/servers.py +141 -0
  75. gobby/config/sessions.py +250 -0
  76. gobby/config/tasks.py +784 -0
  77. gobby/hooks/__init__.py +104 -0
  78. gobby/hooks/artifact_capture.py +213 -0
  79. gobby/hooks/broadcaster.py +243 -0
  80. gobby/hooks/event_handlers.py +723 -0
  81. gobby/hooks/events.py +218 -0
  82. gobby/hooks/git.py +169 -0
  83. gobby/hooks/health_monitor.py +171 -0
  84. gobby/hooks/hook_manager.py +856 -0
  85. gobby/hooks/hook_types.py +575 -0
  86. gobby/hooks/plugins.py +813 -0
  87. gobby/hooks/session_coordinator.py +396 -0
  88. gobby/hooks/verification_runner.py +268 -0
  89. gobby/hooks/webhooks.py +339 -0
  90. gobby/install/claude/commands/gobby/bug.md +51 -0
  91. gobby/install/claude/commands/gobby/chore.md +51 -0
  92. gobby/install/claude/commands/gobby/epic.md +52 -0
  93. gobby/install/claude/commands/gobby/eval.md +235 -0
  94. gobby/install/claude/commands/gobby/feat.md +49 -0
  95. gobby/install/claude/commands/gobby/nit.md +52 -0
  96. gobby/install/claude/commands/gobby/ref.md +52 -0
  97. gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
  98. gobby/install/claude/hooks/hook_dispatcher.py +364 -0
  99. gobby/install/claude/hooks/validate_settings.py +102 -0
  100. gobby/install/claude/hooks-template.json +118 -0
  101. gobby/install/codex/hooks/hook_dispatcher.py +153 -0
  102. gobby/install/codex/prompts/forget.md +7 -0
  103. gobby/install/codex/prompts/memories.md +7 -0
  104. gobby/install/codex/prompts/recall.md +7 -0
  105. gobby/install/codex/prompts/remember.md +13 -0
  106. gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
  107. gobby/install/gemini/hooks-template.json +138 -0
  108. gobby/install/shared/plugins/code_guardian.py +456 -0
  109. gobby/install/shared/plugins/example_notify.py +331 -0
  110. gobby/integrations/__init__.py +10 -0
  111. gobby/integrations/github.py +145 -0
  112. gobby/integrations/linear.py +145 -0
  113. gobby/llm/__init__.py +40 -0
  114. gobby/llm/base.py +120 -0
  115. gobby/llm/claude.py +578 -0
  116. gobby/llm/claude_executor.py +503 -0
  117. gobby/llm/codex.py +322 -0
  118. gobby/llm/codex_executor.py +513 -0
  119. gobby/llm/executor.py +316 -0
  120. gobby/llm/factory.py +34 -0
  121. gobby/llm/gemini.py +258 -0
  122. gobby/llm/gemini_executor.py +339 -0
  123. gobby/llm/litellm.py +287 -0
  124. gobby/llm/litellm_executor.py +303 -0
  125. gobby/llm/resolver.py +499 -0
  126. gobby/llm/service.py +236 -0
  127. gobby/mcp_proxy/__init__.py +29 -0
  128. gobby/mcp_proxy/actions.py +175 -0
  129. gobby/mcp_proxy/daemon_control.py +198 -0
  130. gobby/mcp_proxy/importer.py +436 -0
  131. gobby/mcp_proxy/lazy.py +325 -0
  132. gobby/mcp_proxy/manager.py +798 -0
  133. gobby/mcp_proxy/metrics.py +609 -0
  134. gobby/mcp_proxy/models.py +139 -0
  135. gobby/mcp_proxy/registries.py +215 -0
  136. gobby/mcp_proxy/schema_hash.py +381 -0
  137. gobby/mcp_proxy/semantic_search.py +706 -0
  138. gobby/mcp_proxy/server.py +549 -0
  139. gobby/mcp_proxy/services/__init__.py +0 -0
  140. gobby/mcp_proxy/services/fallback.py +306 -0
  141. gobby/mcp_proxy/services/recommendation.py +224 -0
  142. gobby/mcp_proxy/services/server_mgmt.py +214 -0
  143. gobby/mcp_proxy/services/system.py +72 -0
  144. gobby/mcp_proxy/services/tool_filter.py +231 -0
  145. gobby/mcp_proxy/services/tool_proxy.py +309 -0
  146. gobby/mcp_proxy/stdio.py +565 -0
  147. gobby/mcp_proxy/tools/__init__.py +27 -0
  148. gobby/mcp_proxy/tools/agents.py +1103 -0
  149. gobby/mcp_proxy/tools/artifacts.py +207 -0
  150. gobby/mcp_proxy/tools/hub.py +335 -0
  151. gobby/mcp_proxy/tools/internal.py +337 -0
  152. gobby/mcp_proxy/tools/memory.py +543 -0
  153. gobby/mcp_proxy/tools/merge.py +422 -0
  154. gobby/mcp_proxy/tools/metrics.py +283 -0
  155. gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
  156. gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
  157. gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
  158. gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
  159. gobby/mcp_proxy/tools/orchestration/review.py +736 -0
  160. gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
  161. gobby/mcp_proxy/tools/session_messages.py +1056 -0
  162. gobby/mcp_proxy/tools/task_dependencies.py +219 -0
  163. gobby/mcp_proxy/tools/task_expansion.py +591 -0
  164. gobby/mcp_proxy/tools/task_github.py +393 -0
  165. gobby/mcp_proxy/tools/task_linear.py +379 -0
  166. gobby/mcp_proxy/tools/task_orchestration.py +77 -0
  167. gobby/mcp_proxy/tools/task_readiness.py +522 -0
  168. gobby/mcp_proxy/tools/task_sync.py +351 -0
  169. gobby/mcp_proxy/tools/task_validation.py +843 -0
  170. gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
  171. gobby/mcp_proxy/tools/tasks/_context.py +112 -0
  172. gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
  173. gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
  174. gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
  175. gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
  176. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
  177. gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
  178. gobby/mcp_proxy/tools/tasks/_search.py +215 -0
  179. gobby/mcp_proxy/tools/tasks/_session.py +125 -0
  180. gobby/mcp_proxy/tools/workflows.py +973 -0
  181. gobby/mcp_proxy/tools/worktrees.py +1264 -0
  182. gobby/mcp_proxy/transports/__init__.py +0 -0
  183. gobby/mcp_proxy/transports/base.py +95 -0
  184. gobby/mcp_proxy/transports/factory.py +44 -0
  185. gobby/mcp_proxy/transports/http.py +139 -0
  186. gobby/mcp_proxy/transports/stdio.py +213 -0
  187. gobby/mcp_proxy/transports/websocket.py +136 -0
  188. gobby/memory/backends/__init__.py +116 -0
  189. gobby/memory/backends/mem0.py +408 -0
  190. gobby/memory/backends/memu.py +485 -0
  191. gobby/memory/backends/null.py +111 -0
  192. gobby/memory/backends/openmemory.py +537 -0
  193. gobby/memory/backends/sqlite.py +304 -0
  194. gobby/memory/context.py +87 -0
  195. gobby/memory/manager.py +1001 -0
  196. gobby/memory/protocol.py +451 -0
  197. gobby/memory/search/__init__.py +66 -0
  198. gobby/memory/search/text.py +127 -0
  199. gobby/memory/viz.py +258 -0
  200. gobby/prompts/__init__.py +13 -0
  201. gobby/prompts/defaults/expansion/system.md +119 -0
  202. gobby/prompts/defaults/expansion/user.md +48 -0
  203. gobby/prompts/defaults/external_validation/agent.md +72 -0
  204. gobby/prompts/defaults/external_validation/external.md +63 -0
  205. gobby/prompts/defaults/external_validation/spawn.md +83 -0
  206. gobby/prompts/defaults/external_validation/system.md +6 -0
  207. gobby/prompts/defaults/features/import_mcp.md +22 -0
  208. gobby/prompts/defaults/features/import_mcp_github.md +17 -0
  209. gobby/prompts/defaults/features/import_mcp_search.md +16 -0
  210. gobby/prompts/defaults/features/recommend_tools.md +32 -0
  211. gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
  212. gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
  213. gobby/prompts/defaults/features/server_description.md +20 -0
  214. gobby/prompts/defaults/features/server_description_system.md +6 -0
  215. gobby/prompts/defaults/features/task_description.md +31 -0
  216. gobby/prompts/defaults/features/task_description_system.md +6 -0
  217. gobby/prompts/defaults/features/tool_summary.md +17 -0
  218. gobby/prompts/defaults/features/tool_summary_system.md +6 -0
  219. gobby/prompts/defaults/research/step.md +58 -0
  220. gobby/prompts/defaults/validation/criteria.md +47 -0
  221. gobby/prompts/defaults/validation/validate.md +38 -0
  222. gobby/prompts/loader.py +346 -0
  223. gobby/prompts/models.py +113 -0
  224. gobby/py.typed +0 -0
  225. gobby/runner.py +488 -0
  226. gobby/search/__init__.py +23 -0
  227. gobby/search/protocol.py +104 -0
  228. gobby/search/tfidf.py +232 -0
  229. gobby/servers/__init__.py +7 -0
  230. gobby/servers/http.py +636 -0
  231. gobby/servers/models.py +31 -0
  232. gobby/servers/routes/__init__.py +23 -0
  233. gobby/servers/routes/admin.py +416 -0
  234. gobby/servers/routes/dependencies.py +118 -0
  235. gobby/servers/routes/mcp/__init__.py +24 -0
  236. gobby/servers/routes/mcp/hooks.py +135 -0
  237. gobby/servers/routes/mcp/plugins.py +121 -0
  238. gobby/servers/routes/mcp/tools.py +1337 -0
  239. gobby/servers/routes/mcp/webhooks.py +159 -0
  240. gobby/servers/routes/sessions.py +582 -0
  241. gobby/servers/websocket.py +766 -0
  242. gobby/sessions/__init__.py +13 -0
  243. gobby/sessions/analyzer.py +322 -0
  244. gobby/sessions/lifecycle.py +240 -0
  245. gobby/sessions/manager.py +563 -0
  246. gobby/sessions/processor.py +225 -0
  247. gobby/sessions/summary.py +532 -0
  248. gobby/sessions/transcripts/__init__.py +41 -0
  249. gobby/sessions/transcripts/base.py +125 -0
  250. gobby/sessions/transcripts/claude.py +386 -0
  251. gobby/sessions/transcripts/codex.py +143 -0
  252. gobby/sessions/transcripts/gemini.py +195 -0
  253. gobby/storage/__init__.py +21 -0
  254. gobby/storage/agents.py +409 -0
  255. gobby/storage/artifact_classifier.py +341 -0
  256. gobby/storage/artifacts.py +285 -0
  257. gobby/storage/compaction.py +67 -0
  258. gobby/storage/database.py +357 -0
  259. gobby/storage/inter_session_messages.py +194 -0
  260. gobby/storage/mcp.py +680 -0
  261. gobby/storage/memories.py +562 -0
  262. gobby/storage/merge_resolutions.py +550 -0
  263. gobby/storage/migrations.py +860 -0
  264. gobby/storage/migrations_legacy.py +1359 -0
  265. gobby/storage/projects.py +166 -0
  266. gobby/storage/session_messages.py +251 -0
  267. gobby/storage/session_tasks.py +97 -0
  268. gobby/storage/sessions.py +817 -0
  269. gobby/storage/task_dependencies.py +223 -0
  270. gobby/storage/tasks/__init__.py +42 -0
  271. gobby/storage/tasks/_aggregates.py +180 -0
  272. gobby/storage/tasks/_crud.py +449 -0
  273. gobby/storage/tasks/_id.py +104 -0
  274. gobby/storage/tasks/_lifecycle.py +311 -0
  275. gobby/storage/tasks/_manager.py +889 -0
  276. gobby/storage/tasks/_models.py +300 -0
  277. gobby/storage/tasks/_ordering.py +119 -0
  278. gobby/storage/tasks/_path_cache.py +110 -0
  279. gobby/storage/tasks/_queries.py +343 -0
  280. gobby/storage/tasks/_search.py +143 -0
  281. gobby/storage/workflow_audit.py +393 -0
  282. gobby/storage/worktrees.py +547 -0
  283. gobby/sync/__init__.py +29 -0
  284. gobby/sync/github.py +333 -0
  285. gobby/sync/linear.py +304 -0
  286. gobby/sync/memories.py +284 -0
  287. gobby/sync/tasks.py +641 -0
  288. gobby/tasks/__init__.py +8 -0
  289. gobby/tasks/build_verification.py +193 -0
  290. gobby/tasks/commits.py +633 -0
  291. gobby/tasks/context.py +747 -0
  292. gobby/tasks/criteria.py +342 -0
  293. gobby/tasks/enhanced_validator.py +226 -0
  294. gobby/tasks/escalation.py +263 -0
  295. gobby/tasks/expansion.py +626 -0
  296. gobby/tasks/external_validator.py +764 -0
  297. gobby/tasks/issue_extraction.py +171 -0
  298. gobby/tasks/prompts/expand.py +327 -0
  299. gobby/tasks/research.py +421 -0
  300. gobby/tasks/tdd.py +352 -0
  301. gobby/tasks/tree_builder.py +263 -0
  302. gobby/tasks/validation.py +712 -0
  303. gobby/tasks/validation_history.py +357 -0
  304. gobby/tasks/validation_models.py +89 -0
  305. gobby/tools/__init__.py +0 -0
  306. gobby/tools/summarizer.py +170 -0
  307. gobby/tui/__init__.py +5 -0
  308. gobby/tui/api_client.py +281 -0
  309. gobby/tui/app.py +327 -0
  310. gobby/tui/screens/__init__.py +25 -0
  311. gobby/tui/screens/agents.py +333 -0
  312. gobby/tui/screens/chat.py +450 -0
  313. gobby/tui/screens/dashboard.py +377 -0
  314. gobby/tui/screens/memory.py +305 -0
  315. gobby/tui/screens/metrics.py +231 -0
  316. gobby/tui/screens/orchestrator.py +904 -0
  317. gobby/tui/screens/sessions.py +412 -0
  318. gobby/tui/screens/tasks.py +442 -0
  319. gobby/tui/screens/workflows.py +289 -0
  320. gobby/tui/screens/worktrees.py +174 -0
  321. gobby/tui/widgets/__init__.py +21 -0
  322. gobby/tui/widgets/chat.py +210 -0
  323. gobby/tui/widgets/conductor.py +104 -0
  324. gobby/tui/widgets/menu.py +132 -0
  325. gobby/tui/widgets/message_panel.py +160 -0
  326. gobby/tui/widgets/review_gate.py +224 -0
  327. gobby/tui/widgets/task_tree.py +99 -0
  328. gobby/tui/widgets/token_budget.py +166 -0
  329. gobby/tui/ws_client.py +258 -0
  330. gobby/utils/__init__.py +3 -0
  331. gobby/utils/daemon_client.py +235 -0
  332. gobby/utils/git.py +222 -0
  333. gobby/utils/id.py +38 -0
  334. gobby/utils/json_helpers.py +161 -0
  335. gobby/utils/logging.py +376 -0
  336. gobby/utils/machine_id.py +135 -0
  337. gobby/utils/metrics.py +589 -0
  338. gobby/utils/project_context.py +182 -0
  339. gobby/utils/project_init.py +263 -0
  340. gobby/utils/status.py +256 -0
  341. gobby/utils/validation.py +80 -0
  342. gobby/utils/version.py +23 -0
  343. gobby/workflows/__init__.py +4 -0
  344. gobby/workflows/actions.py +1310 -0
  345. gobby/workflows/approval_flow.py +138 -0
  346. gobby/workflows/artifact_actions.py +103 -0
  347. gobby/workflows/audit_helpers.py +110 -0
  348. gobby/workflows/autonomous_actions.py +286 -0
  349. gobby/workflows/context_actions.py +394 -0
  350. gobby/workflows/definitions.py +130 -0
  351. gobby/workflows/detection_helpers.py +208 -0
  352. gobby/workflows/engine.py +485 -0
  353. gobby/workflows/evaluator.py +669 -0
  354. gobby/workflows/git_utils.py +96 -0
  355. gobby/workflows/hooks.py +169 -0
  356. gobby/workflows/lifecycle_evaluator.py +613 -0
  357. gobby/workflows/llm_actions.py +70 -0
  358. gobby/workflows/loader.py +333 -0
  359. gobby/workflows/mcp_actions.py +60 -0
  360. gobby/workflows/memory_actions.py +272 -0
  361. gobby/workflows/premature_stop.py +164 -0
  362. gobby/workflows/session_actions.py +139 -0
  363. gobby/workflows/state_actions.py +123 -0
  364. gobby/workflows/state_manager.py +104 -0
  365. gobby/workflows/stop_signal_actions.py +163 -0
  366. gobby/workflows/summary_actions.py +344 -0
  367. gobby/workflows/task_actions.py +249 -0
  368. gobby/workflows/task_enforcement_actions.py +901 -0
  369. gobby/workflows/templates.py +52 -0
  370. gobby/workflows/todo_actions.py +84 -0
  371. gobby/workflows/webhook.py +223 -0
  372. gobby/workflows/webhook_executor.py +399 -0
  373. gobby/worktrees/__init__.py +5 -0
  374. gobby/worktrees/git.py +690 -0
  375. gobby/worktrees/merge/__init__.py +20 -0
  376. gobby/worktrees/merge/conflict_parser.py +177 -0
  377. gobby/worktrees/merge/resolver.py +485 -0
  378. gobby-0.2.5.dist-info/METADATA +351 -0
  379. gobby-0.2.5.dist-info/RECORD +383 -0
  380. gobby-0.2.5.dist-info/WHEEL +5 -0
  381. gobby-0.2.5.dist-info/entry_points.txt +2 -0
  382. gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
  383. gobby-0.2.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,532 @@
1
+ """
2
+ Summary File Generator for session summaries (failover).
3
+
4
+ Handles:
5
+ - Session summary generation from JSONL transcripts using LLM synthesis
6
+ - Storage in markdown files (independent of database/workflow)
7
+ """
8
+
9
+ import json
10
+ import logging
11
+ import subprocess # nosec B404 - subprocess needed for git commands
12
+ import time
13
+ from datetime import UTC, datetime
14
+ from pathlib import Path
15
+ from typing import TYPE_CHECKING, Any
16
+
17
+ import anyio
18
+
19
+ from gobby.llm.base import LLMProvider
20
+ from gobby.llm.claude import ClaudeLLMProvider
21
+ from gobby.sessions.transcripts.claude import ClaudeTranscriptParser
22
+
23
+ if TYPE_CHECKING:
24
+ from gobby.config.app import DaemonConfig
25
+ from gobby.llm.service import LLMService
26
+
27
+ # Backward-compatible alias
28
+ TranscriptProcessor = ClaudeTranscriptParser
29
+
30
+
31
+ class SummaryFileGenerator:
32
+ """
33
+ Generates session summaries to files using LLM synthesis (failover).
34
+
35
+ Handles:
36
+ - Independent summary generation from JSONL transcripts
37
+ - File storage in ~/.gobby/session_summaries (strictly file-based)
38
+ - Configuration check (session_summary.enabled)
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ transcript_processor: ClaudeTranscriptParser,
44
+ summary_file_path: str = "~/.gobby/session_summaries",
45
+ logger_instance: logging.Logger | None = None,
46
+ llm_service: "LLMService | None" = None,
47
+ config: "DaemonConfig | None" = None,
48
+ ) -> None:
49
+ """
50
+ Initialize SummaryFileGenerator.
51
+
52
+ Args:
53
+ transcript_processor: Processor for JSONL transcript parsing
54
+ summary_file_path: Directory path for session summary files
55
+ logger_instance: Optional logger instance
56
+ llm_service: Optional LLMService for multi-provider support
57
+ config: Optional DaemonConfig instance for feature configuration
58
+ """
59
+ self._transcript_processor = transcript_processor
60
+ self._summary_file_path = summary_file_path
61
+ self.logger = logger_instance or logging.getLogger(__name__)
62
+ self._llm_service = llm_service
63
+ self._config = config
64
+
65
+ # Initialize LLM provider from llm_service or create default
66
+ self.llm_provider: LLMProvider | None = None
67
+
68
+ if llm_service:
69
+ try:
70
+ self.llm_provider = llm_service.get_default_provider()
71
+ provider_name = (
72
+ getattr(self.llm_provider, "provider_name", "unknown")
73
+ if self.llm_provider
74
+ else "unknown"
75
+ )
76
+ self.logger.debug(f"Using '{provider_name}' provider for SummaryFileGenerator")
77
+ except ValueError as e:
78
+ self.logger.warning(f"LLMService has no providers: {e}")
79
+
80
+ if not self.llm_provider:
81
+ # Fallback to ClaudeLLMProvider
82
+ try:
83
+ from gobby.config.app import load_config
84
+
85
+ config = config or load_config()
86
+ self._config = config
87
+ self.llm_provider = ClaudeLLMProvider(config)
88
+ self.logger.debug("Initialized default ClaudeLLMProvider for SummaryFileGenerator")
89
+ except Exception as e:
90
+ self.logger.error(f"Failed to initialize default LLM provider: {e}")
91
+
92
+ def _get_provider_for_feature(
93
+ self, feature_name: str
94
+ ) -> tuple["LLMProvider | None", str | None]:
95
+ """
96
+ Get LLM provider and prompt for a specific feature.
97
+
98
+ Args:
99
+ feature_name: Feature name (e.g., "session_summary")
100
+
101
+ Returns:
102
+ Tuple of (provider, prompt) where prompt is from feature config.
103
+ Returns (None, None) if feature is disabled.
104
+ """
105
+ config = self._config
106
+ if not config:
107
+ return self.llm_provider, None
108
+
109
+ # Try to get feature-specific config
110
+ try:
111
+ if feature_name == "session_summary":
112
+ feature_config = getattr(config, "session_summary", None)
113
+ else:
114
+ return self.llm_provider, None
115
+
116
+ if not feature_config:
117
+ return self.llm_provider, None
118
+
119
+ # Check if feature is enabled
120
+ if not getattr(feature_config, "enabled", True):
121
+ self.logger.debug(f"Feature '{feature_name}' is disabled in config")
122
+ return None, None
123
+
124
+ # Get provider from LLMService if available
125
+ provider_name = getattr(feature_config, "provider", None)
126
+ prompt = getattr(feature_config, "prompt", None)
127
+
128
+ llm_service = self._llm_service
129
+ if llm_service and provider_name:
130
+ try:
131
+ provider = llm_service.get_provider(provider_name)
132
+ self.logger.debug(f"Using provider '{provider_name}' for {feature_name}")
133
+ return provider, prompt
134
+ except ValueError as e:
135
+ self.logger.warning(
136
+ f"Provider '{provider_name}' not available for {feature_name}: {e}"
137
+ )
138
+
139
+ return self.llm_provider, prompt
140
+
141
+ except Exception as e:
142
+ self.logger.warning(f"Failed to get feature config for {feature_name}: {e}")
143
+ return self.llm_provider, None
144
+
145
+ def generate_session_summary(
146
+ self, session_id: str, input_data: dict[str, Any]
147
+ ) -> dict[str, Any]:
148
+ """
149
+ Generate comprehensive LLM-powered session summary file from JSONL transcript.
150
+
151
+ Args:
152
+ session_id: Internal database UUID (sessions.id), not cli_key
153
+ input_data: Session end input data containing cli_key and transcript_path
154
+
155
+ Returns:
156
+ Dict with status and file path
157
+ """
158
+ external_id = None
159
+ try:
160
+ # Check if feature is enabled via config
161
+ config = self._config
162
+ if config and hasattr(config, "session_summary") and config.session_summary:
163
+ if not getattr(config.session_summary, "enabled", True):
164
+ self.logger.info("Session summary file generation disabled in config")
165
+ return {"status": "disabled"}
166
+ # Update path from config if available
167
+ new_path = getattr(config.session_summary, "summary_file_path", None)
168
+ if new_path:
169
+ self._summary_file_path = new_path
170
+
171
+ # Extract external_id from input_data
172
+ external_id = input_data.get("session_id")
173
+ if not external_id:
174
+ self.logger.error(f"No external_id in input_data for session_id={session_id}")
175
+ return {"status": "no_external_id", "session_id": session_id}
176
+
177
+ # Source is hardcoded since all hook calls are from Claude Code
178
+ session_source = "Claude Code"
179
+
180
+ # Get transcript path
181
+ transcript_path = input_data.get("transcript_path")
182
+ if not transcript_path:
183
+ self.logger.warning(f"No transcript path found for session {external_id}")
184
+ return {"status": "no_transcript", "external_id": external_id}
185
+
186
+ # Read JSONL transcript
187
+ transcript_file = Path(transcript_path)
188
+ if not transcript_file.exists():
189
+ self.logger.warning(f"Transcript file not found: {transcript_path}")
190
+ return {"status": "transcript_not_found", "path": transcript_path}
191
+
192
+ # Parse JSONL and extract last 50 turns
193
+ turns = []
194
+ with open(transcript_file) as f:
195
+ for line in f:
196
+ if line.strip():
197
+ turns.append(json.loads(line))
198
+
199
+ # Get turns since last /clear (up to 50 turns)
200
+ last_turns = self._transcript_processor.extract_turns_since_clear(turns, max_turns=50)
201
+
202
+ # Get last two user<>agent message pairs
203
+ last_messages = self._transcript_processor.extract_last_messages(
204
+ last_turns, num_pairs=2
205
+ )
206
+
207
+ # Extract last TodoWrite tool call
208
+ todowrite_list = self._extract_last_todowrite(last_turns)
209
+
210
+ # Get git status and file changes
211
+ git_status = self._get_git_status()
212
+ file_changes = self._get_file_changes()
213
+
214
+ # Generate summary using LLM
215
+ summary_markdown = self._generate_summary_with_llm(
216
+ last_turns=last_turns,
217
+ last_messages=last_messages,
218
+ git_status=git_status,
219
+ file_changes=file_changes,
220
+ external_id=external_id,
221
+ session_id=session_id,
222
+ session_source=session_source,
223
+ todowrite_list=todowrite_list,
224
+ session_tasks_str=None, # Task integration removed for failover simplicity
225
+ )
226
+
227
+ # Write summary to file (FAILOVER ONLY)
228
+ file_result = self.write_summary_to_file(session_id, summary_markdown)
229
+
230
+ return {
231
+ "status": "success",
232
+ "external_id": external_id,
233
+ "file_written": file_result,
234
+ "summary_length": len(summary_markdown),
235
+ }
236
+
237
+ except Exception as e:
238
+ self.logger.error(f"Failed to create session summary file: {e}", exc_info=True)
239
+ return {"status": "error", "error": str(e), "external_id": external_id}
240
+
241
+ def write_summary_to_file(self, session_id: str, summary: str) -> str | None:
242
+ """
243
+ Write session summary to markdown file.
244
+
245
+ Args:
246
+ session_id: Internal database UUID (sessions.id) or external_id
247
+ summary: Markdown summary content
248
+
249
+ Returns:
250
+ Path to written file, or None on failure
251
+ """
252
+ try:
253
+ # Create summary directory from config
254
+ summary_dir = Path(self._summary_file_path).expanduser()
255
+ summary_dir.mkdir(parents=True, exist_ok=True)
256
+
257
+ # Write markdown file with Unix timestamp for chronological sorting
258
+ timestamp = int(time.time())
259
+ summary_file = summary_dir / f"session_{timestamp}_{session_id}.md"
260
+ summary_file.write_text(summary, encoding="utf-8")
261
+
262
+ self.logger.info(f"💾 FAILBACK: Session summary written to: {summary_file}")
263
+ return str(summary_file)
264
+
265
+ except Exception as e:
266
+ self.logger.exception(f"Failed to write summary file: {e}")
267
+ return None
268
+
269
+ def _generate_summary_with_llm(
270
+ self,
271
+ last_turns: list[dict[str, Any]],
272
+ last_messages: list[dict[str, Any]],
273
+ git_status: str,
274
+ file_changes: str,
275
+ external_id: str,
276
+ session_id: str | None,
277
+ session_source: str | None,
278
+ todowrite_list: str | None = None,
279
+ session_tasks_str: str | None = None,
280
+ ) -> str:
281
+ """
282
+ Generate session summary using LLM provider.
283
+
284
+ Args:
285
+ last_turns: List of recent transcript turns
286
+ last_messages: List of last user<>agent message pairs
287
+ git_status: Git status output
288
+ file_changes: Formatted file changes
289
+ external_id: Claude Code session key
290
+ session_id: Internal database UUID
291
+ session_source: Session source (e.g., "Claude Code")
292
+ todowrite_list: Optional TodoWrite list markdown
293
+ session_tasks_str: Optional formatted session tasks list
294
+
295
+ Returns:
296
+ Formatted markdown summary
297
+ """
298
+ # Get feature-specific provider and prompt
299
+ provider, prompt = self._get_provider_for_feature("session_summary")
300
+
301
+ if not provider:
302
+ return "Session summary unavailable (LLM provider not initialized)"
303
+
304
+ # Prepare context
305
+ transcript_summary = self._format_turns_for_llm(last_turns)
306
+
307
+ context = {
308
+ "transcript_summary": transcript_summary,
309
+ "last_messages": last_messages,
310
+ "git_status": git_status,
311
+ "file_changes": file_changes,
312
+ "todo_list": f"## Agent's TODO List\n{todowrite_list}" if todowrite_list else "",
313
+ "session_tasks": session_tasks_str,
314
+ "external_id": external_id,
315
+ "session_id": session_id,
316
+ "session_source": session_source,
317
+ }
318
+
319
+ # Validate prompt is available
320
+ if not prompt:
321
+ return (
322
+ "Session summary unavailable: No prompt template configured. "
323
+ "Set 'session_summary.prompt' in ~/.gobby/config.yaml"
324
+ )
325
+
326
+ try:
327
+
328
+ async def _run_gen() -> str:
329
+ # Ensure provider is narrowed for the closure
330
+ active_provider = provider
331
+ if not active_provider:
332
+ return ""
333
+ result: str = await active_provider.generate_summary(
334
+ context, prompt_template=prompt
335
+ )
336
+ return result
337
+
338
+ llm_summary: str = anyio.run(_run_gen)
339
+
340
+ if not llm_summary:
341
+ raise RuntimeError("LLM summary generation failed - no summary produced")
342
+
343
+ # Build header
344
+ timestamp = datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S UTC")
345
+
346
+ if session_id and session_source:
347
+ header = f"# Session Summary (Failover)\nSession ID: {session_id}\n{session_source} ID: {external_id}\nGenerated: {timestamp}\n\n"
348
+ elif session_id:
349
+ header = f"# Session Summary (Failover)\nSession ID: {session_id}\nClaude Code ID: {external_id}\nGenerated: {timestamp}\n\n"
350
+ else:
351
+ header = f"# Session Summary (Failover)\nClaude Code ID: {external_id}\nGenerated: {timestamp}\n\n"
352
+
353
+ final_summary = header + llm_summary
354
+
355
+ # Insert TodoWrite list if it exists
356
+ if todowrite_list:
357
+ todo_section_marker = "## Claude's Todo List"
358
+ if todo_section_marker in final_summary:
359
+ parts = final_summary.split(todo_section_marker)
360
+ if len(parts) == 2:
361
+ next_section_idx = parts[1].find("\n##")
362
+ if next_section_idx != -1:
363
+ after_next = parts[1][next_section_idx:]
364
+ final_summary = (
365
+ f"{parts[0]}{todo_section_marker}\n{todowrite_list}\n{after_next}"
366
+ )
367
+ else:
368
+ final_summary = f"{parts[0]}{todo_section_marker}\n{todowrite_list}"
369
+ else:
370
+ # Fallback: insert before Next Steps
371
+ if "## Next Steps" in final_summary:
372
+ parts = final_summary.split("## Next Steps", 1)
373
+ final_summary = f"{parts[0]}\n## Claude's Todo List\n{todowrite_list}\n\n## Next Steps{parts[1]}"
374
+ else:
375
+ final_summary = (
376
+ f"{final_summary}\n\n## Claude's Todo List\n{todowrite_list}"
377
+ )
378
+
379
+ return final_summary
380
+
381
+ except Exception as e:
382
+ self.logger.error(f"LLM summary generation failed: {e}", exc_info=True)
383
+ timestamp = datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S UTC")
384
+
385
+ if session_id and session_source:
386
+ error_header = f"# Session Summary (Error)\nSession ID: {session_id}\n{session_source} ID: {external_id}\nGenerated: {timestamp}\n\n"
387
+ elif session_id:
388
+ error_header = f"# Session Summary (Error)\nSession ID: {session_id}\nClaude Code ID: {external_id}\nGenerated: {timestamp}\n\n"
389
+ else:
390
+ error_header = f"# Session Summary (Error)\nClaude Code ID: {external_id}\nGenerated: {timestamp}\n\n"
391
+
392
+ error_summary = error_header + f"Error generating summary: {str(e)}"
393
+
394
+ if todowrite_list:
395
+ error_summary = f"{error_summary}\n\n## Claude's Todo List\n{todowrite_list}"
396
+
397
+ return error_summary
398
+
399
+ def _format_turns_for_llm(self, turns: list[dict[str, Any]]) -> str:
400
+ """
401
+ Format transcript turns for LLM analysis.
402
+
403
+ Args:
404
+ turns: List of transcript turn dicts
405
+
406
+ Returns:
407
+ Formatted string with turn summaries
408
+ """
409
+ formatted: list[str] = []
410
+ for i, turn in enumerate(turns):
411
+ message = turn.get("message", {})
412
+ role = message.get("role", "unknown")
413
+ content = message.get("content", "")
414
+
415
+ # Assistant messages have content as array of blocks
416
+ if isinstance(content, list):
417
+ text_parts: list[str] = []
418
+ for block in content:
419
+ if isinstance(block, dict):
420
+ if block.get("type") == "text":
421
+ text_parts.append(str(block.get("text", "")))
422
+ elif block.get("type") == "thinking":
423
+ text_parts.append(f"[Thinking: {block.get('thinking', '')}]")
424
+ elif block.get("type") == "tool_use":
425
+ text_parts.append(f"[Tool: {block.get('name', 'unknown')}]")
426
+ content = " ".join(text_parts)
427
+
428
+ formatted.append(f"[Turn {i + 1} - {role}]: {content}")
429
+
430
+ return "\n\n".join(formatted)
431
+
432
+ def _extract_last_todowrite(self, turns: list[dict[str, Any]]) -> str | None:
433
+ """
434
+ Extract the last TodoWrite tool call's todos list from transcript.
435
+
436
+ Args:
437
+ turns: List of transcript turns
438
+
439
+ Returns:
440
+ Formatted markdown string with todo list, or None if not found
441
+ """
442
+ # Scan turns in reverse to find most recent TodoWrite
443
+ for turn in reversed(turns):
444
+ message = turn.get("message", {})
445
+ content = message.get("content", [])
446
+
447
+ if isinstance(content, list):
448
+ for block in content:
449
+ if isinstance(block, dict) and block.get("type") == "tool_use":
450
+ if block.get("name") == "TodoWrite":
451
+ tool_input = block.get("input", {})
452
+ todos = tool_input.get("todos", [])
453
+
454
+ if not todos:
455
+ return None
456
+
457
+ # Format as markdown checklist
458
+ lines: list[str] = []
459
+ for todo in todos:
460
+ content_text = todo.get("content", "")
461
+ status = todo.get("status", "pending")
462
+
463
+ # Map status to checkbox style
464
+ if status == "completed":
465
+ checkbox = "[x]"
466
+ elif status == "in_progress":
467
+ checkbox = "[>]"
468
+ else:
469
+ checkbox = "[ ]"
470
+
471
+ lines.append(f"- {checkbox} {content_text} ({status})")
472
+
473
+ return "\n".join(lines)
474
+
475
+ return None
476
+
477
+ def _get_git_status(self) -> str:
478
+ """
479
+ Get git status for current directory.
480
+
481
+ Returns:
482
+ Git status output or error message
483
+ """
484
+ try:
485
+ result = subprocess.run( # nosec B603 B607 - hardcoded git command
486
+ ["git", "status", "--short"],
487
+ capture_output=True,
488
+ text=True,
489
+ timeout=5,
490
+ )
491
+ return result.stdout.strip()
492
+ except Exception:
493
+ return "Not a git repository or git not available"
494
+
495
+ def _get_file_changes(self) -> str:
496
+ """
497
+ Get detailed file changes from git.
498
+
499
+ Returns:
500
+ Formatted file changes or error message
501
+ """
502
+ try:
503
+ # Get changed files with status
504
+ diff_result = subprocess.run( # nosec B603 B607 - hardcoded git command
505
+ ["git", "diff", "HEAD", "--name-status"],
506
+ capture_output=True,
507
+ text=True,
508
+ timeout=5,
509
+ )
510
+
511
+ # Get untracked files
512
+ untracked_result = subprocess.run( # nosec B603 B607 - hardcoded git command
513
+ ["git", "ls-files", "--others", "--exclude-standard"],
514
+ capture_output=True,
515
+ text=True,
516
+ timeout=5,
517
+ )
518
+
519
+ # Combine results
520
+ changes = []
521
+ if diff_result.stdout.strip():
522
+ changes.append("Modified/Deleted:")
523
+ changes.append(diff_result.stdout.strip())
524
+
525
+ if untracked_result.stdout.strip():
526
+ changes.append("\nUntracked:")
527
+ changes.append(untracked_result.stdout.strip())
528
+
529
+ return "\n".join(changes) if changes else "No changes"
530
+
531
+ except Exception:
532
+ return "Unable to determine file changes"
@@ -0,0 +1,41 @@
1
+ """
2
+ Transcript parsers.
3
+
4
+ Exports transcript parsers for different CLI tools.
5
+ """
6
+
7
+ from gobby.sessions.transcripts.base import ParsedMessage, TranscriptParser
8
+ from gobby.sessions.transcripts.claude import ClaudeTranscriptParser
9
+ from gobby.sessions.transcripts.codex import CodexTranscriptParser
10
+ from gobby.sessions.transcripts.gemini import GeminiTranscriptParser
11
+
12
+ __all__ = [
13
+ "TranscriptParser",
14
+ "ParsedMessage",
15
+ "ClaudeTranscriptParser",
16
+ "GeminiTranscriptParser",
17
+ "CodexTranscriptParser",
18
+ "get_parser",
19
+ "PARSER_REGISTRY",
20
+ ]
21
+
22
+ PARSER_REGISTRY: dict[str, type[TranscriptParser]] = {
23
+ "claude": ClaudeTranscriptParser,
24
+ "gemini": GeminiTranscriptParser,
25
+ "antigravity": GeminiTranscriptParser,
26
+ "codex": CodexTranscriptParser,
27
+ }
28
+
29
+
30
+ def get_parser(source: str) -> TranscriptParser:
31
+ """
32
+ Get a transcript parser instance for the given source.
33
+
34
+ Args:
35
+ source: CLI source name (e.g., 'claude', 'gemini')
36
+
37
+ Returns:
38
+ TranscriptParser instance
39
+ """
40
+ parser_cls = PARSER_REGISTRY.get(source, ClaudeTranscriptParser)
41
+ return parser_cls()
@@ -0,0 +1,125 @@
1
+ """
2
+ Base transcript parser protocol.
3
+
4
+ Defines the interface for CLI-specific transcript parsers.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from dataclasses import dataclass
11
+ from datetime import datetime
12
+ from typing import Any, Protocol, runtime_checkable
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class TokenUsage:
19
+ """Token usage metrics for a message or session."""
20
+
21
+ input_tokens: int = 0
22
+ output_tokens: int = 0
23
+ cache_creation_tokens: int = 0
24
+ cache_read_tokens: int = 0
25
+ total_cost_usd: float | None = None
26
+
27
+
28
+ @dataclass
29
+ class ParsedMessage:
30
+ """Normalized message from any CLI transcript."""
31
+
32
+ index: int
33
+ role: str
34
+ content: str
35
+ content_type: str # text, thinking, tool_use, tool_result
36
+ tool_name: str | None
37
+ tool_input: dict[str, Any] | None
38
+ tool_result: dict[str, Any] | None
39
+ timestamp: datetime
40
+ raw_json: dict[str, Any]
41
+ usage: TokenUsage | None = None
42
+
43
+
44
+ @runtime_checkable
45
+ class TranscriptParser(Protocol):
46
+ """
47
+ Protocol for transcript parsers.
48
+
49
+ Each CLI tool (Claude Code, Codex, Gemini, Antigravity) has its own
50
+ transcript format. Implementations of this protocol handle parsing
51
+ and extracting conversation data from each format.
52
+ """
53
+
54
+ def parse_line(self, line: str, index: int) -> ParsedMessage | None:
55
+ """
56
+ Parse a single line from the transcript JSONL.
57
+
58
+ Args:
59
+ line: Raw JSON line string
60
+ index: Line index (0-based)
61
+
62
+ Returns:
63
+ ParsedMessage object or None if line should be skipped
64
+ """
65
+ ...
66
+
67
+ def parse_lines(self, lines: list[str], start_index: int = 0) -> list[ParsedMessage]:
68
+ """
69
+ Parse multiple lines from the transcript.
70
+
71
+ Args:
72
+ lines: List of raw JSON line strings
73
+ start_index: Starting line index for first line in list
74
+
75
+ Returns:
76
+ List of ParsedMessage objects
77
+ """
78
+ ...
79
+
80
+ def extract_last_messages(
81
+ self, turns: list[dict[str, Any]], num_pairs: int = 2
82
+ ) -> list[dict[str, Any]]:
83
+ """
84
+ Extract last N user<>agent message pairs from transcript.
85
+
86
+ Args:
87
+ turns: List of transcript turns
88
+ num_pairs: Number of user/agent message pairs to extract
89
+
90
+ Returns:
91
+ List of message dicts with "role" and "content" fields
92
+ """
93
+ ...
94
+
95
+ def extract_turns_since_clear(
96
+ self, turns: list[dict[str, Any]], max_turns: int = 50
97
+ ) -> list[dict[str, Any]]:
98
+ """
99
+ Extract turns since the most recent session boundary, up to max_turns.
100
+
101
+ What constitutes a "session boundary" varies by CLI:
102
+ - Claude Code: /clear command
103
+ - Codex: New session in history
104
+ - Gemini: Session delimiter
105
+
106
+ Args:
107
+ turns: List of all transcript turns
108
+ max_turns: Maximum number of turns to extract
109
+
110
+ Returns:
111
+ List of turns representing the current conversation segment
112
+ """
113
+ ...
114
+
115
+ def is_session_boundary(self, turn: dict[str, Any]) -> bool:
116
+ """
117
+ Check if a turn represents a session boundary.
118
+
119
+ Args:
120
+ turn: Transcript turn dict
121
+
122
+ Returns:
123
+ True if turn marks a session boundary
124
+ """
125
+ ...