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.
- gobby/__init__.py +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- 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"]
|