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,456 @@
1
+ """
2
+ Code Guardian Plugin - Example Gobby Plugin
3
+
4
+ This plugin demonstrates the full capabilities of the Gobby plugin system:
5
+ - Hook handlers for BEFORE_TOOL and AFTER_TOOL events
6
+ - Event blocking for code quality enforcement
7
+ - Auto-fix capabilities with ruff
8
+ - Workflow actions and conditions for integration with workflows
9
+
10
+ Installation:
11
+ 1. Copy this file to ~/.gobby/plugins/code_guardian.py
12
+ 2. Enable in ~/.gobby/config.yaml:
13
+ hook_extensions:
14
+ plugins:
15
+ enabled: true
16
+ plugins:
17
+ code-guardian:
18
+ enabled: true
19
+ config:
20
+ checks: [ruff, mypy]
21
+ block_on_error: true
22
+ auto_fix: true
23
+ 3. Restart gobby daemon: gobby stop && gobby start
24
+
25
+ Configuration Options:
26
+ checks: list[str] - Enabled checkers ("ruff", "mypy")
27
+ block_on_error: bool - Block Edit/Write on lint failures (default: true)
28
+ auto_fix: bool - Auto-format with ruff before blocking (default: true)
29
+ file_patterns: list[str] - Glob patterns for files to check (default: ["*.py"])
30
+ ignore_paths: list[str] - Paths to skip (default: [".venv", "__pycache__"])
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import shutil
36
+ import subprocess # nosec B404 - subprocess needed for code linting commands
37
+ from pathlib import Path
38
+ from typing import Any
39
+
40
+ from gobby.hooks.events import HookEvent, HookEventType, HookResponse
41
+ from gobby.hooks.plugins import HookPlugin, hook_handler
42
+
43
+
44
+ class CodeGuardianPlugin(HookPlugin):
45
+ """
46
+ Enforces code quality by running linters on file modifications.
47
+
48
+ Pre-handlers (priority 10) intercept Edit/Write tools and run checks.
49
+ Post-handlers (priority 60) log results and can inject context.
50
+ """
51
+
52
+ name = "code-guardian"
53
+ version = "1.0.0"
54
+ description = "Code quality guardian - runs linters on file changes"
55
+
56
+ def __init__(self) -> None:
57
+ super().__init__()
58
+ # Configuration with defaults
59
+ self.checks: list[str] = ["ruff"]
60
+ self.block_on_error: bool = True
61
+ self.auto_fix: bool = True
62
+ self.file_patterns: list[str] = ["*.py"]
63
+ self.ignore_paths: list[str] = [".venv", "__pycache__", "node_modules"]
64
+ # Rules to exclude from auto-fix (F401=unused imports, F811=redefinition)
65
+ # These are commonly "wrong" during multi-step refactoring
66
+ self.auto_fix_exclude_rules: list[str] = ["F401", "F811"]
67
+
68
+ # State tracking
69
+ self._last_check_results: dict[str, Any] = {}
70
+ self._files_checked: int = 0
71
+ self._files_blocked: int = 0
72
+
73
+ def on_load(self, config: dict[str, Any]) -> None:
74
+ """Initialize plugin with configuration."""
75
+ self.checks = config.get("checks", self.checks)
76
+ self.block_on_error = config.get("block_on_error", self.block_on_error)
77
+ self.auto_fix = config.get("auto_fix", self.auto_fix)
78
+ self.file_patterns = config.get("file_patterns", self.file_patterns)
79
+ self.ignore_paths = config.get("ignore_paths", self.ignore_paths)
80
+ self.auto_fix_exclude_rules = config.get(
81
+ "auto_fix_exclude_rules", self.auto_fix_exclude_rules
82
+ )
83
+
84
+ self.logger.info(
85
+ f"Code Guardian loaded: checks={self.checks}, "
86
+ f"block_on_error={self.block_on_error}, auto_fix={self.auto_fix}"
87
+ )
88
+
89
+ # Register workflow actions
90
+ self.register_action("run_linter", self._action_run_linter)
91
+ self.register_action("format_code", self._action_format_code)
92
+
93
+ # Register workflow conditions
94
+ self.register_condition("passes_lint", self._condition_passes_lint)
95
+ self.register_condition("has_type_errors", self._condition_has_type_errors)
96
+
97
+ def on_unload(self) -> None:
98
+ """Cleanup on plugin unload."""
99
+ self.logger.info(
100
+ f"Code Guardian stats: checked={self._files_checked}, blocked={self._files_blocked}"
101
+ )
102
+
103
+ # =========================================================================
104
+ # Hook Handlers
105
+ # =========================================================================
106
+
107
+ @hook_handler(HookEventType.BEFORE_TOOL, priority=10)
108
+ def check_before_write(self, event: HookEvent) -> HookResponse | None:
109
+ """
110
+ Pre-handler: Intercept Edit/Write tools and run linters.
111
+
112
+ Returns HookResponse with decision="deny" to block the tool,
113
+ or None to allow it to proceed.
114
+ """
115
+ tool_name = event.data.get("tool_name", "")
116
+ tool_input = event.data.get("tool_input", {})
117
+
118
+ # Only intercept Edit and Write tools
119
+ if tool_name not in ("Edit", "Write"):
120
+ return None
121
+
122
+ # Get the file path being modified
123
+ file_path = tool_input.get("file_path", "")
124
+ if not file_path:
125
+ return None
126
+
127
+ path = Path(file_path)
128
+
129
+ # Skip non-Python files (or files not matching patterns)
130
+ if not self._should_check_file(path):
131
+ return None
132
+
133
+ # For Write tool, we check the content being written
134
+ # For Edit tool, the file will be modified - we check after
135
+ if tool_name == "Write":
136
+ content = tool_input.get("content", "")
137
+ return self._check_content(path, content)
138
+
139
+ # For Edit, we'll check in the post-handler after the edit is applied
140
+ return None
141
+
142
+ @hook_handler(HookEventType.AFTER_TOOL, priority=60)
143
+ def report_after_tool(self, event: HookEvent, core_response: HookResponse | None) -> None:
144
+ """
145
+ Post-handler: Log results and track statistics.
146
+
147
+ Post-handlers receive both the event and the core response.
148
+ They cannot block; return value is ignored.
149
+ """
150
+ tool_name = event.data.get("tool_name", "")
151
+ tool_input = event.data.get("tool_input", {})
152
+
153
+ # Only care about Edit/Write
154
+ if tool_name not in ("Edit", "Write"):
155
+ return
156
+
157
+ file_path = tool_input.get("file_path", "")
158
+ if not file_path:
159
+ return
160
+
161
+ path = Path(file_path)
162
+ if not self._should_check_file(path):
163
+ return
164
+
165
+ # For Edit tool, run checks on the modified file
166
+ if tool_name == "Edit" and path.exists():
167
+ self._files_checked += 1
168
+ errors = self._run_checks(path)
169
+
170
+ if errors:
171
+ self._last_check_results[str(path)] = {
172
+ "status": "failed",
173
+ "errors": errors,
174
+ }
175
+ self.logger.warning(f"Post-edit lint issues in {path.name}: {len(errors)} error(s)")
176
+
177
+ # Try auto-fix if enabled
178
+ if self.auto_fix and "ruff" in self.checks:
179
+ self._run_ruff_fix(path)
180
+ else:
181
+ self._last_check_results[str(path)] = {"status": "passed"}
182
+
183
+ # =========================================================================
184
+ # Check Logic
185
+ # =========================================================================
186
+
187
+ def _should_check_file(self, path: Path) -> bool:
188
+ """Determine if a file should be checked."""
189
+ # Check file patterns
190
+ matches_pattern = any(path.match(pattern) for pattern in self.file_patterns)
191
+ if not matches_pattern:
192
+ return False
193
+
194
+ # Check ignore paths
195
+ path_str = str(path)
196
+ for ignore in self.ignore_paths:
197
+ if ignore in path_str:
198
+ return False
199
+
200
+ return True
201
+
202
+ def _check_content(self, path: Path, content: str) -> HookResponse | None:
203
+ """
204
+ Check content before it's written to a file.
205
+
206
+ For Write operations, we validate the content syntax/style
207
+ before allowing the write.
208
+ """
209
+ self._files_checked += 1
210
+
211
+ # For syntax checking content before write, we'd need to write to a temp file
212
+ # For simplicity, we'll check after the file is written in post-handler
213
+ # But we can do basic checks here
214
+
215
+ # Check for obvious issues (placeholder for real checks)
216
+ issues: list[str] = []
217
+
218
+ # Example: Check for debug prints
219
+ if "print(" in content and "def " in content:
220
+ lines = content.split("\n")
221
+ for i, line in enumerate(lines, 1):
222
+ stripped = line.lstrip()
223
+ if stripped.startswith("print(") and "# noqa" not in line:
224
+ issues.append(f"Line {i}: Debug print statement found")
225
+
226
+ if issues and self.block_on_error:
227
+ self._files_blocked += 1
228
+ return HookResponse(
229
+ decision="deny",
230
+ reason=f"Code Guardian blocked write: {len(issues)} issue(s) found",
231
+ metadata={
232
+ "plugin": self.name,
233
+ "issues": issues[:5], # Limit to first 5
234
+ "file": str(path),
235
+ },
236
+ )
237
+
238
+ return None
239
+
240
+ def _run_checks(self, path: Path) -> list[str]:
241
+ """Run configured checkers on a file."""
242
+ errors: list[str] = []
243
+
244
+ if "ruff" in self.checks:
245
+ errors.extend(self._run_ruff_check(path))
246
+
247
+ if "mypy" in self.checks:
248
+ errors.extend(self._run_mypy_check(path))
249
+
250
+ return errors
251
+
252
+ def _run_ruff_check(self, path: Path) -> list[str]:
253
+ """Run ruff linter on a file."""
254
+ if not shutil.which("ruff"):
255
+ self.logger.debug("ruff not found in PATH, skipping")
256
+ return []
257
+
258
+ try:
259
+ result = subprocess.run( # nosec B603 B607 - hardcoded ruff command
260
+ ["ruff", "check", "--output-format=concise", str(path)],
261
+ capture_output=True,
262
+ text=True,
263
+ timeout=30,
264
+ )
265
+
266
+ if result.returncode != 0 and result.stdout:
267
+ return [line.strip() for line in result.stdout.strip().split("\n") if line.strip()]
268
+
269
+ except subprocess.TimeoutExpired:
270
+ self.logger.warning(f"ruff timed out on {path}")
271
+ except Exception as e:
272
+ self.logger.error(f"ruff check failed: {e}")
273
+
274
+ return []
275
+
276
+ def _run_ruff_fix(self, path: Path) -> bool:
277
+ """Run ruff --fix on a file, excluding configured rules."""
278
+ if not shutil.which("ruff"):
279
+ return False
280
+
281
+ try:
282
+ # Build command with excluded rules
283
+ cmd = ["ruff", "check", "--fix"]
284
+ for rule in self.auto_fix_exclude_rules:
285
+ cmd.extend(["--ignore", rule])
286
+ cmd.append(str(path))
287
+
288
+ result = subprocess.run( # nosec B603 - cmd built from hardcoded ruff arguments
289
+ cmd,
290
+ capture_output=True,
291
+ text=True,
292
+ timeout=30,
293
+ )
294
+ if result.returncode == 0:
295
+ self.logger.info(f"ruff auto-fixed {path.name}")
296
+ return True
297
+
298
+ except Exception as e:
299
+ self.logger.error(f"ruff fix failed: {e}")
300
+
301
+ return False
302
+
303
+ def _run_mypy_check(self, path: Path) -> list[str]:
304
+ """Run mypy type checker on a file."""
305
+ if not shutil.which("mypy"):
306
+ self.logger.debug("mypy not found in PATH, skipping")
307
+ return []
308
+
309
+ try:
310
+ result = subprocess.run( # nosec B603 B607 - hardcoded mypy command
311
+ ["mypy", "--no-error-summary", str(path)],
312
+ capture_output=True,
313
+ text=True,
314
+ timeout=60,
315
+ )
316
+
317
+ if result.returncode != 0 and result.stdout:
318
+ return [
319
+ line.strip()
320
+ for line in result.stdout.strip().split("\n")
321
+ if line.strip() and ": error:" in line
322
+ ]
323
+
324
+ except subprocess.TimeoutExpired:
325
+ self.logger.warning(f"mypy timed out on {path}")
326
+ except Exception as e:
327
+ self.logger.error(f"mypy check failed: {e}")
328
+
329
+ return []
330
+
331
+ # =========================================================================
332
+ # Workflow Actions
333
+ # =========================================================================
334
+
335
+ async def _action_run_linter(
336
+ self,
337
+ context: dict[str, Any],
338
+ files: list[str] | None = None,
339
+ **kwargs: Any,
340
+ ) -> dict[str, Any]:
341
+ """
342
+ Workflow action: Run linter on specified files.
343
+
344
+ Usage in workflow YAML:
345
+ - action: plugin:code-guardian:run_linter
346
+ files: ["src/main.py", "src/utils.py"]
347
+
348
+ Args:
349
+ context: Workflow context
350
+ files: List of file paths to check (optional, uses context if not provided)
351
+
352
+ Returns:
353
+ Dict with results: {"passed": bool, "errors": list, "files_checked": int}
354
+ """
355
+ target_files = files or context.get("files", [])
356
+ all_errors: list[str] = []
357
+ checked = 0
358
+
359
+ for file_path in target_files:
360
+ path = Path(file_path)
361
+ if path.exists() and self._should_check_file(path):
362
+ errors = self._run_checks(path)
363
+ all_errors.extend(errors)
364
+ checked += 1
365
+
366
+ return {
367
+ "passed": len(all_errors) == 0,
368
+ "errors": all_errors,
369
+ "files_checked": checked,
370
+ }
371
+
372
+ async def _action_format_code(
373
+ self,
374
+ context: dict[str, Any],
375
+ files: list[str] | None = None,
376
+ **kwargs: Any,
377
+ ) -> dict[str, Any]:
378
+ """
379
+ Workflow action: Format code files with ruff.
380
+
381
+ Usage in workflow YAML:
382
+ - action: plugin:code-guardian:format_code
383
+ files: ["src/"]
384
+
385
+ Args:
386
+ context: Workflow context
387
+ files: List of file/directory paths to format
388
+
389
+ Returns:
390
+ Dict with results: {"formatted": int, "errors": list}
391
+ """
392
+ target_files = files or context.get("files", [])
393
+ formatted = 0
394
+ errors: list[str] = []
395
+
396
+ if not shutil.which("ruff"):
397
+ return {"formatted": 0, "errors": ["ruff not found in PATH"]}
398
+
399
+ for file_path in target_files:
400
+ path = Path(file_path)
401
+ try:
402
+ result = subprocess.run( # nosec B603 B607 - hardcoded ruff command
403
+ ["ruff", "format", str(path)],
404
+ capture_output=True,
405
+ text=True,
406
+ timeout=60,
407
+ )
408
+ if result.returncode == 0:
409
+ formatted += 1
410
+ else:
411
+ errors.append(f"{path}: {result.stderr.strip()}")
412
+ except Exception as e:
413
+ errors.append(f"{path}: {e}")
414
+
415
+ return {"formatted": formatted, "errors": errors}
416
+
417
+ # =========================================================================
418
+ # Workflow Conditions
419
+ # =========================================================================
420
+
421
+ def _condition_passes_lint(self, file_path: str | None = None) -> bool:
422
+ """
423
+ Workflow condition: Check if file(s) pass linting.
424
+
425
+ Usage in workflow YAML:
426
+ when: "plugin_code_guardian_passes_lint()"
427
+
428
+ Note: Condition names are transformed to use underscores when registered.
429
+ """
430
+ if file_path:
431
+ result = self._last_check_results.get(file_path)
432
+ return result is not None and result.get("status") == "passed"
433
+
434
+ # If no specific file, check if any recent checks failed
435
+ for result in self._last_check_results.values():
436
+ if result.get("status") == "failed":
437
+ return False
438
+ return True
439
+
440
+ def _condition_has_type_errors(self, file_path: str | None = None) -> bool:
441
+ """
442
+ Workflow condition: Check if file has type errors (mypy).
443
+
444
+ Usage in workflow YAML:
445
+ when: "plugin_code_guardian_has_type_errors()"
446
+ """
447
+ if file_path:
448
+ path = Path(file_path)
449
+ if path.exists():
450
+ errors = self._run_mypy_check(path)
451
+ return len(errors) > 0
452
+ return False
453
+
454
+
455
+ # For dynamic discovery, the class must be importable
456
+ __all__ = ["CodeGuardianPlugin"]