monoco-toolkit 0.3.12__py3-none-any.whl → 0.4.0__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.
- monoco/core/automation/__init__.py +0 -11
- monoco/core/automation/handlers.py +108 -26
- monoco/core/config.py +28 -10
- monoco/core/daemon/__init__.py +5 -0
- monoco/core/daemon/pid.py +290 -0
- monoco/core/injection.py +86 -8
- monoco/core/integrations.py +0 -24
- monoco/core/router/__init__.py +1 -39
- monoco/core/router/action.py +3 -142
- monoco/core/scheduler/events.py +28 -2
- monoco/core/setup.py +9 -0
- monoco/core/sync.py +199 -4
- monoco/core/watcher/__init__.py +6 -0
- monoco/core/watcher/base.py +18 -1
- monoco/core/watcher/im.py +460 -0
- monoco/core/watcher/memo.py +40 -48
- monoco/daemon/app.py +3 -60
- monoco/daemon/commands.py +459 -25
- monoco/daemon/scheduler.py +1 -16
- monoco/daemon/services.py +15 -0
- monoco/features/agent/resources/en/AGENTS.md +14 -14
- monoco/features/agent/resources/en/skills/monoco_role_engineer/SKILL.md +101 -0
- monoco/features/agent/resources/en/skills/monoco_role_manager/SKILL.md +95 -0
- monoco/features/agent/resources/en/skills/monoco_role_planner/SKILL.md +177 -0
- monoco/features/agent/resources/en/skills/monoco_role_reviewer/SKILL.md +139 -0
- monoco/features/agent/resources/zh/skills/monoco_role_engineer/SKILL.md +101 -0
- monoco/features/agent/resources/zh/skills/monoco_role_manager/SKILL.md +95 -0
- monoco/features/agent/resources/zh/skills/monoco_role_planner/SKILL.md +177 -0
- monoco/features/agent/resources/zh/skills/monoco_role_reviewer/SKILL.md +139 -0
- monoco/features/hooks/__init__.py +61 -6
- monoco/features/hooks/commands.py +281 -271
- monoco/features/hooks/dispatchers/__init__.py +23 -0
- monoco/features/hooks/dispatchers/agent_dispatcher.py +486 -0
- monoco/features/hooks/dispatchers/git_dispatcher.py +478 -0
- monoco/features/hooks/manager.py +357 -0
- monoco/features/hooks/models.py +262 -0
- monoco/features/hooks/parser.py +322 -0
- monoco/features/hooks/universal_interceptor.py +503 -0
- monoco/features/im/__init__.py +67 -0
- monoco/features/im/core.py +782 -0
- monoco/features/im/models.py +311 -0
- monoco/features/issue/commands.py +65 -50
- monoco/features/issue/core.py +199 -99
- monoco/features/issue/domain_commands.py +0 -19
- monoco/features/issue/resources/en/AGENTS.md +17 -122
- monoco/features/issue/resources/hooks/agent/before-tool.sh +102 -0
- monoco/features/issue/resources/hooks/agent/session-start.sh +88 -0
- monoco/features/issue/resources/hooks/{post-checkout.sh → git/git-post-checkout.sh} +10 -9
- monoco/features/issue/resources/hooks/git/git-pre-commit.sh +31 -0
- monoco/features/issue/resources/hooks/{pre-push.sh → git/git-pre-push.sh} +7 -13
- monoco/features/issue/resources/zh/AGENTS.md +18 -123
- monoco/features/memo/cli.py +15 -64
- monoco/features/memo/core.py +6 -34
- monoco/features/memo/models.py +24 -15
- monoco/features/memo/resources/en/AGENTS.md +31 -0
- monoco/features/memo/resources/zh/AGENTS.md +28 -5
- monoco/main.py +5 -3
- {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/METADATA +1 -1
- monoco_toolkit-0.4.0.dist-info/RECORD +170 -0
- monoco/core/automation/config.py +0 -338
- monoco/core/execution.py +0 -67
- monoco/core/executor/__init__.py +0 -38
- monoco/core/executor/agent_action.py +0 -254
- monoco/core/executor/git_action.py +0 -303
- monoco/core/executor/im_action.py +0 -309
- monoco/core/executor/pytest_action.py +0 -218
- monoco/core/router/router.py +0 -392
- monoco/features/agent/resources/atoms/atom-code-dev.yaml +0 -61
- monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +0 -73
- monoco/features/agent/resources/atoms/atom-knowledge.yaml +0 -55
- monoco/features/agent/resources/atoms/atom-review.yaml +0 -60
- monoco/features/agent/resources/en/skills/monoco_atom_core/SKILL.md +0 -99
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_manager/SKILL.md +0 -93
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_planner/SKILL.md +0 -85
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -114
- monoco/features/agent/resources/workflows/workflow-dev.yaml +0 -83
- monoco/features/agent/resources/workflows/workflow-issue-create.yaml +0 -72
- monoco/features/agent/resources/workflows/workflow-review.yaml +0 -94
- monoco/features/agent/resources/zh/roles/monoco_role_engineer.yaml +0 -49
- monoco/features/agent/resources/zh/roles/monoco_role_manager.yaml +0 -46
- monoco/features/agent/resources/zh/roles/monoco_role_planner.yaml +0 -46
- monoco/features/agent/resources/zh/roles/monoco_role_reviewer.yaml +0 -47
- monoco/features/agent/resources/zh/skills/monoco_atom_core/SKILL.md +0 -99
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_manager/SKILL.md +0 -88
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_planner/SKILL.md +0 -259
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -137
- monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +0 -278
- monoco/features/glossary/resources/en/skills/monoco_atom_glossary/SKILL.md +0 -35
- monoco/features/glossary/resources/zh/skills/monoco_atom_glossary/SKILL.md +0 -35
- monoco/features/hooks/adapter.py +0 -67
- monoco/features/hooks/core.py +0 -441
- monoco/features/i18n/resources/en/skills/monoco_atom_i18n/SKILL.md +0 -96
- monoco/features/i18n/resources/en/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
- monoco/features/i18n/resources/zh/skills/monoco_atom_i18n/SKILL.md +0 -96
- monoco/features/i18n/resources/zh/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
- monoco/features/issue/resources/en/skills/monoco_atom_issue/SKILL.md +0 -165
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_development/SKILL.md +0 -224
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_management/SKILL.md +0 -159
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
- monoco/features/issue/resources/hooks/pre-commit.sh +0 -41
- monoco/features/issue/resources/zh/skills/monoco_atom_issue_lifecycle/SKILL.md +0 -190
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_development/SKILL.md +0 -224
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_management/SKILL.md +0 -159
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
- monoco/features/memo/resources/en/skills/monoco_atom_memo/SKILL.md +0 -77
- monoco/features/memo/resources/en/skills/monoco_workflow_note_processing/SKILL.md +0 -140
- monoco/features/memo/resources/zh/skills/monoco_atom_memo/SKILL.md +0 -77
- monoco/features/memo/resources/zh/skills/monoco_workflow_note_processing/SKILL.md +0 -140
- monoco/features/spike/resources/en/skills/monoco_atom_spike/SKILL.md +0 -76
- monoco/features/spike/resources/en/skills/monoco_workflow_research/SKILL.md +0 -121
- monoco/features/spike/resources/zh/skills/monoco_atom_spike/SKILL.md +0 -76
- monoco/features/spike/resources/zh/skills/monoco_workflow_research/SKILL.md +0 -121
- monoco_toolkit-0.3.12.dist-info/RECORD +0 -202
- {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Universal Hooks: Manager
|
|
3
|
+
|
|
4
|
+
Core manager class for discovering, validating, and organizing
|
|
5
|
+
Universal Hooks across Git, IDE, and Agent contexts.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Callable, Optional
|
|
12
|
+
|
|
13
|
+
from .models import (
|
|
14
|
+
GitEvent,
|
|
15
|
+
AgentEvent,
|
|
16
|
+
IDEEvent,
|
|
17
|
+
HookGroup,
|
|
18
|
+
HookMetadata,
|
|
19
|
+
HookType,
|
|
20
|
+
ParsedHook,
|
|
21
|
+
)
|
|
22
|
+
from .parser import HookParser, ParseError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ValidationResult:
|
|
27
|
+
"""Result of validating a hook."""
|
|
28
|
+
|
|
29
|
+
is_valid: bool
|
|
30
|
+
errors: list[str] = field(default_factory=list)
|
|
31
|
+
warnings: list[str] = field(default_factory=list)
|
|
32
|
+
|
|
33
|
+
def add_error(self, message: str) -> None:
|
|
34
|
+
"""Add an error message."""
|
|
35
|
+
self.errors.append(message)
|
|
36
|
+
self.is_valid = False
|
|
37
|
+
|
|
38
|
+
def add_warning(self, message: str) -> None:
|
|
39
|
+
"""Add a warning message."""
|
|
40
|
+
self.warnings.append(message)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class HookDispatcher(ABC):
|
|
44
|
+
"""
|
|
45
|
+
Abstract base class for hook type dispatchers.
|
|
46
|
+
|
|
47
|
+
Dispatchers are responsible for executing hooks of a specific type.
|
|
48
|
+
They are registered with UniversalHookManager to handle hooks
|
|
49
|
+
for their respective types.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, hook_type: HookType, provider: Optional[str] = None):
|
|
53
|
+
"""
|
|
54
|
+
Initialize the dispatcher.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
hook_type: The type of hooks this dispatcher handles
|
|
58
|
+
provider: Optional provider name (for agent/ide types)
|
|
59
|
+
"""
|
|
60
|
+
self.hook_type = hook_type
|
|
61
|
+
self.provider = provider
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def key(self) -> str:
|
|
65
|
+
"""Get the unique key for this dispatcher."""
|
|
66
|
+
if self.hook_type == HookType.GIT:
|
|
67
|
+
return self.hook_type.value
|
|
68
|
+
return f"{self.hook_type.value}:{self.provider}"
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def can_execute(self, hook: ParsedHook) -> bool:
|
|
72
|
+
"""
|
|
73
|
+
Check if this dispatcher can execute the given hook.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
hook: The parsed hook to check
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
True if this dispatcher can execute the hook
|
|
80
|
+
"""
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
@abstractmethod
|
|
84
|
+
def execute(self, hook: ParsedHook, context: Optional[dict] = None) -> bool:
|
|
85
|
+
"""
|
|
86
|
+
Execute a hook.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
hook: The parsed hook to execute
|
|
90
|
+
context: Optional execution context
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
True if execution succeeded
|
|
94
|
+
"""
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class UniversalHookManager:
|
|
99
|
+
"""
|
|
100
|
+
Core manager for Universal Hooks system.
|
|
101
|
+
|
|
102
|
+
This class provides:
|
|
103
|
+
- Scanning directories for hook scripts with Front Matter metadata
|
|
104
|
+
- Validating hook metadata integrity
|
|
105
|
+
- Organizing hooks by type and provider
|
|
106
|
+
- Registering dispatchers for different hook types
|
|
107
|
+
|
|
108
|
+
Example usage:
|
|
109
|
+
manager = UniversalHookManager()
|
|
110
|
+
|
|
111
|
+
# Scan for hooks
|
|
112
|
+
groups = manager.scan("./hooks")
|
|
113
|
+
|
|
114
|
+
# Validate a hook
|
|
115
|
+
result = manager.validate(hook)
|
|
116
|
+
|
|
117
|
+
# Register a dispatcher
|
|
118
|
+
manager.register_dispatcher(HookType.GIT, GitHookDispatcher())
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def __init__(self):
|
|
122
|
+
"""Initialize the Universal Hook Manager."""
|
|
123
|
+
self.parser = HookParser()
|
|
124
|
+
self._dispatchers: dict[str, HookDispatcher] = {}
|
|
125
|
+
self._validation_hooks: list[Callable[[HookMetadata], Optional[str]]] = []
|
|
126
|
+
|
|
127
|
+
def scan(
|
|
128
|
+
self,
|
|
129
|
+
directory: Path | str,
|
|
130
|
+
pattern: str = "*",
|
|
131
|
+
) -> dict[str, HookGroup]:
|
|
132
|
+
"""
|
|
133
|
+
Recursively scan a directory for hook scripts.
|
|
134
|
+
|
|
135
|
+
Discovers all hook scripts with valid Front Matter metadata and
|
|
136
|
+
organizes them into groups by type and provider.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
directory: Directory to scan
|
|
140
|
+
pattern: Glob pattern for matching files (default: "*")
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Dictionary mapping group keys to HookGroup objects
|
|
144
|
+
"""
|
|
145
|
+
dir_path = Path(directory)
|
|
146
|
+
groups: dict[str, HookGroup] = {}
|
|
147
|
+
|
|
148
|
+
# Parse all hooks in the directory
|
|
149
|
+
parsed_hooks = self.parser.parse_directory(dir_path, pattern)
|
|
150
|
+
|
|
151
|
+
# Organize hooks into groups
|
|
152
|
+
for hook in parsed_hooks:
|
|
153
|
+
key = hook.metadata.get_key()
|
|
154
|
+
|
|
155
|
+
if key not in groups:
|
|
156
|
+
groups[key] = HookGroup(
|
|
157
|
+
key=key,
|
|
158
|
+
hook_type=hook.metadata.type,
|
|
159
|
+
provider=hook.metadata.provider,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
groups[key].add_hook(hook)
|
|
163
|
+
|
|
164
|
+
return groups
|
|
165
|
+
|
|
166
|
+
def validate(self, hook: ParsedHook | HookMetadata) -> ValidationResult:
|
|
167
|
+
"""
|
|
168
|
+
Validate hook metadata integrity.
|
|
169
|
+
|
|
170
|
+
Performs both Pydantic model validation and additional
|
|
171
|
+
semantic checks (e.g., provider required for agent/ide types).
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
hook: The hook or metadata to validate
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
ValidationResult with errors and warnings
|
|
178
|
+
"""
|
|
179
|
+
result = ValidationResult(is_valid=True)
|
|
180
|
+
|
|
181
|
+
# Get metadata from hook if needed
|
|
182
|
+
if isinstance(hook, ParsedHook):
|
|
183
|
+
metadata = hook.metadata
|
|
184
|
+
script_path = hook.script_path
|
|
185
|
+
else:
|
|
186
|
+
metadata = hook
|
|
187
|
+
script_path = None
|
|
188
|
+
|
|
189
|
+
# Basic Pydantic validation already happened during parsing
|
|
190
|
+
# but we can add additional semantic checks here
|
|
191
|
+
|
|
192
|
+
# Check for valid event types
|
|
193
|
+
self._validate_event(metadata, result)
|
|
194
|
+
|
|
195
|
+
# Check for script executability (if we have a path)
|
|
196
|
+
if script_path:
|
|
197
|
+
self._validate_script_executable(script_path, result)
|
|
198
|
+
|
|
199
|
+
# Check matcher patterns are valid
|
|
200
|
+
if metadata.matcher:
|
|
201
|
+
self._validate_matchers(metadata.matcher, result)
|
|
202
|
+
|
|
203
|
+
# Run custom validation hooks
|
|
204
|
+
for validator in self._validation_hooks:
|
|
205
|
+
error = validator(metadata)
|
|
206
|
+
if error:
|
|
207
|
+
result.add_error(error)
|
|
208
|
+
|
|
209
|
+
return result
|
|
210
|
+
|
|
211
|
+
def _validate_event(self, metadata: HookMetadata, result: ValidationResult) -> None:
|
|
212
|
+
"""Validate that the event is appropriate for the hook type."""
|
|
213
|
+
event_validators = {
|
|
214
|
+
HookType.GIT: lambda e: e in {e.value for e in GitEvent},
|
|
215
|
+
HookType.AGENT: lambda e: e in {e.value for e in AgentEvent},
|
|
216
|
+
HookType.IDE: lambda e: e in {e.value for e in IDEEvent},
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
validator = event_validators.get(metadata.type)
|
|
220
|
+
if validator and not validator(metadata.event):
|
|
221
|
+
valid_events = {
|
|
222
|
+
HookType.GIT: GitEvent,
|
|
223
|
+
HookType.AGENT: AgentEvent,
|
|
224
|
+
HookType.IDE: IDEEvent,
|
|
225
|
+
}.get(metadata.type)
|
|
226
|
+
|
|
227
|
+
if valid_events:
|
|
228
|
+
valid_list = ", ".join(e.value for e in valid_events)
|
|
229
|
+
result.add_error(
|
|
230
|
+
f"Invalid event '{metadata.event}' for type '{metadata.type.value}'. "
|
|
231
|
+
f"Valid events: {valid_list}"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def _validate_script_executable(
|
|
235
|
+
self, path: Path, result: ValidationResult
|
|
236
|
+
) -> None:
|
|
237
|
+
"""Validate that the script file is executable."""
|
|
238
|
+
if not path.exists():
|
|
239
|
+
result.add_error(f"Script file does not exist: {path}")
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
# Check if file is executable (on Unix systems)
|
|
243
|
+
import stat
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
mode = path.stat().st_mode
|
|
247
|
+
if not (mode & stat.S_IXUSR):
|
|
248
|
+
result.add_warning(
|
|
249
|
+
f"Script may not be executable (missing execute permission): {path}"
|
|
250
|
+
)
|
|
251
|
+
except Exception:
|
|
252
|
+
# If we can't stat the file, just skip this check
|
|
253
|
+
pass
|
|
254
|
+
|
|
255
|
+
def _validate_matchers(self, matchers: list[str], result: ValidationResult) -> None:
|
|
256
|
+
"""Validate glob patterns in matchers."""
|
|
257
|
+
import fnmatch
|
|
258
|
+
|
|
259
|
+
for pattern in matchers:
|
|
260
|
+
# Basic pattern validation - fnmatch doesn't raise errors
|
|
261
|
+
# but we can check for obviously invalid patterns
|
|
262
|
+
if not pattern:
|
|
263
|
+
result.add_warning("Empty matcher pattern found")
|
|
264
|
+
elif pattern.startswith("!") and len(pattern) == 1:
|
|
265
|
+
result.add_warning("Negation pattern '!' without actual pattern")
|
|
266
|
+
|
|
267
|
+
def register_dispatcher(
|
|
268
|
+
self,
|
|
269
|
+
hook_type: HookType,
|
|
270
|
+
dispatcher: HookDispatcher,
|
|
271
|
+
) -> None:
|
|
272
|
+
"""
|
|
273
|
+
Register a dispatcher for a hook type.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
hook_type: The type of hooks this dispatcher handles
|
|
277
|
+
dispatcher: The dispatcher instance
|
|
278
|
+
"""
|
|
279
|
+
key = dispatcher.key
|
|
280
|
+
self._dispatchers[key] = dispatcher
|
|
281
|
+
|
|
282
|
+
def unregister_dispatcher(
|
|
283
|
+
self,
|
|
284
|
+
hook_type: HookType,
|
|
285
|
+
provider: Optional[str] = None,
|
|
286
|
+
) -> bool:
|
|
287
|
+
"""
|
|
288
|
+
Unregister a dispatcher.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
hook_type: The type of hooks
|
|
292
|
+
provider: Optional provider name
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
True if a dispatcher was removed
|
|
296
|
+
"""
|
|
297
|
+
if hook_type == HookType.GIT:
|
|
298
|
+
key = hook_type.value
|
|
299
|
+
else:
|
|
300
|
+
key = f"{hook_type.value}:{provider}"
|
|
301
|
+
|
|
302
|
+
if key in self._dispatchers:
|
|
303
|
+
del self._dispatchers[key]
|
|
304
|
+
return True
|
|
305
|
+
return False
|
|
306
|
+
|
|
307
|
+
def get_dispatcher(
|
|
308
|
+
self,
|
|
309
|
+
hook_type: HookType,
|
|
310
|
+
provider: Optional[str] = None,
|
|
311
|
+
) -> Optional[HookDispatcher]:
|
|
312
|
+
"""
|
|
313
|
+
Get a registered dispatcher.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
hook_type: The type of hooks
|
|
317
|
+
provider: Optional provider name
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
The dispatcher if registered, None otherwise
|
|
321
|
+
"""
|
|
322
|
+
if hook_type == HookType.GIT:
|
|
323
|
+
key = hook_type.value
|
|
324
|
+
else:
|
|
325
|
+
key = f"{hook_type.value}:{provider}"
|
|
326
|
+
|
|
327
|
+
return self._dispatchers.get(key)
|
|
328
|
+
|
|
329
|
+
def list_dispatchers(self) -> dict[str, HookDispatcher]:
|
|
330
|
+
"""
|
|
331
|
+
List all registered dispatchers.
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
Dictionary mapping keys to dispatcher instances
|
|
335
|
+
"""
|
|
336
|
+
return self._dispatchers.copy()
|
|
337
|
+
|
|
338
|
+
def add_validation_hook(
|
|
339
|
+
self,
|
|
340
|
+
validator: Callable[[HookMetadata], Optional[str]],
|
|
341
|
+
) -> None:
|
|
342
|
+
"""
|
|
343
|
+
Add a custom validation function.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
validator: Function that takes HookMetadata and returns error message
|
|
347
|
+
or None if valid
|
|
348
|
+
"""
|
|
349
|
+
self._validation_hooks.append(validator)
|
|
350
|
+
|
|
351
|
+
def get_parsing_errors(self) -> list[ParseError]:
|
|
352
|
+
"""Get all parsing errors from the last scan operation."""
|
|
353
|
+
return self.parser.get_errors()
|
|
354
|
+
|
|
355
|
+
def clear_parsing_errors(self) -> None:
|
|
356
|
+
"""Clear parsing errors."""
|
|
357
|
+
self.parser.clear_errors()
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Universal Hooks: Core Models and Types
|
|
3
|
+
|
|
4
|
+
Defines the foundational data models for the Universal Hooks system that supports
|
|
5
|
+
Git, IDE, and Agent hook types with unified metadata management.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Optional, Any
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field, field_validator, model_validator, ConfigDict
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HookType(str, Enum):
|
|
16
|
+
"""
|
|
17
|
+
Universal Hook types supported by the system.
|
|
18
|
+
|
|
19
|
+
- git: Git lifecycle hooks (pre-commit, pre-push, etc.)
|
|
20
|
+
- ide: IDE integration hooks (on-save, on-open, etc.)
|
|
21
|
+
- agent: Agent lifecycle hooks (session-start, before-tool, etc.)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
GIT = "git"
|
|
25
|
+
IDE = "ide"
|
|
26
|
+
AGENT = "agent"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class GitEvent(str, Enum):
|
|
30
|
+
"""
|
|
31
|
+
Git hook events.
|
|
32
|
+
|
|
33
|
+
See: https://git-scm.com/docs/githooks
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
PRE_COMMIT = "pre-commit"
|
|
37
|
+
PREPARE_COMMIT_MSG = "prepare-commit-msg"
|
|
38
|
+
COMMIT_MSG = "commit-msg"
|
|
39
|
+
POST_MERGE = "post-merge"
|
|
40
|
+
PRE_PUSH = "pre-push"
|
|
41
|
+
POST_CHECKOUT = "post-checkout"
|
|
42
|
+
PRE_REBASE = "pre-rebase"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AgentEvent(str, Enum):
|
|
46
|
+
"""
|
|
47
|
+
Agent lifecycle events.
|
|
48
|
+
|
|
49
|
+
Events that occur during agent session execution.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
SESSION_START = "session-start"
|
|
53
|
+
BEFORE_TOOL = "before-tool"
|
|
54
|
+
AFTER_TOOL = "after-tool"
|
|
55
|
+
BEFORE_AGENT = "before-agent"
|
|
56
|
+
AFTER_AGENT = "after-agent"
|
|
57
|
+
SESSION_END = "session-end"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class IDEEvent(str, Enum):
|
|
61
|
+
"""
|
|
62
|
+
IDE integration events.
|
|
63
|
+
|
|
64
|
+
Events triggered by IDE user interactions.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
ON_SAVE = "on-save"
|
|
68
|
+
ON_OPEN = "on-open"
|
|
69
|
+
ON_CLOSE = "on-close"
|
|
70
|
+
ON_BUILD = "on-build"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class HookMetadata(BaseModel):
|
|
74
|
+
"""
|
|
75
|
+
Metadata for a Universal Hook script.
|
|
76
|
+
|
|
77
|
+
This model defines the Front Matter schema that can be embedded in hook scripts
|
|
78
|
+
to declare their type, event binding, matching rules, and execution priority.
|
|
79
|
+
|
|
80
|
+
Example Front Matter in a shell script:
|
|
81
|
+
# ---
|
|
82
|
+
# type: git
|
|
83
|
+
# event: pre-commit
|
|
84
|
+
# matcher:
|
|
85
|
+
# - "*.py"
|
|
86
|
+
# - "*.js"
|
|
87
|
+
# priority: 10
|
|
88
|
+
# description: "Lint Python and JS files before commit"
|
|
89
|
+
# ---
|
|
90
|
+
|
|
91
|
+
Example Front Matter for IDE/Agent hooks (requires provider):
|
|
92
|
+
# ---
|
|
93
|
+
# type: agent
|
|
94
|
+
# provider: claude-code
|
|
95
|
+
# event: before-tool
|
|
96
|
+
# priority: 5
|
|
97
|
+
# description: "Log tool usage"
|
|
98
|
+
# ---
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
type: HookType = Field(
|
|
102
|
+
..., # Required
|
|
103
|
+
description="The hook type: git, ide, or agent"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
event: str = Field(
|
|
107
|
+
..., # Required
|
|
108
|
+
description="The event to hook into (e.g., pre-commit, on-save, before-tool)"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
matcher: Optional[list[str]] = Field(
|
|
112
|
+
default=None,
|
|
113
|
+
description="Optional file patterns to match (e.g., ['*.py', '*.js'])"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
priority: int = Field(
|
|
117
|
+
default=100,
|
|
118
|
+
description="Execution priority (lower = earlier, default 100)",
|
|
119
|
+
ge=0,
|
|
120
|
+
le=1000
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
description: str = Field(
|
|
124
|
+
default="",
|
|
125
|
+
description="Human-readable description of the hook's purpose"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
provider: Optional[str] = Field(
|
|
129
|
+
default=None,
|
|
130
|
+
description="Required when type is 'agent' or 'ide'. Specifies the provider (e.g., 'claude-code', 'vscode')"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Additional flexible metadata for extensibility
|
|
134
|
+
extra: dict[str, Any] = Field(
|
|
135
|
+
default_factory=dict,
|
|
136
|
+
description="Additional metadata for extensibility"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
@field_validator("matcher", mode="before")
|
|
140
|
+
@classmethod
|
|
141
|
+
def validate_matcher(cls, v):
|
|
142
|
+
"""Ensure matcher is a list of strings."""
|
|
143
|
+
if v is None:
|
|
144
|
+
return None
|
|
145
|
+
if isinstance(v, str):
|
|
146
|
+
return [v]
|
|
147
|
+
if isinstance(v, list):
|
|
148
|
+
return [str(item) for item in v]
|
|
149
|
+
raise ValueError("matcher must be a string or list of strings")
|
|
150
|
+
|
|
151
|
+
@model_validator(mode="after")
|
|
152
|
+
def validate_provider_required(self):
|
|
153
|
+
"""
|
|
154
|
+
Validate that provider is provided for agent and ide hook types.
|
|
155
|
+
"""
|
|
156
|
+
if self.type in (HookType.AGENT, HookType.IDE) and not self.provider:
|
|
157
|
+
raise ValueError(
|
|
158
|
+
f"'provider' is required when hook type is '{self.type.value}'"
|
|
159
|
+
)
|
|
160
|
+
return self
|
|
161
|
+
|
|
162
|
+
@model_validator(mode="after")
|
|
163
|
+
def validate_event_for_type(self):
|
|
164
|
+
"""
|
|
165
|
+
Validate that the event is valid for the given hook type.
|
|
166
|
+
"""
|
|
167
|
+
valid_events = {
|
|
168
|
+
HookType.GIT: {e.value for e in GitEvent},
|
|
169
|
+
HookType.AGENT: {e.value for e in AgentEvent},
|
|
170
|
+
HookType.IDE: {e.value for e in IDEEvent},
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
valid = valid_events.get(self.type, set())
|
|
174
|
+
if valid and self.event not in valid:
|
|
175
|
+
raise ValueError(
|
|
176
|
+
f"Invalid event '{self.event}' for type '{self.type.value}'. "
|
|
177
|
+
f"Valid events: {', '.join(sorted(valid))}"
|
|
178
|
+
)
|
|
179
|
+
return self
|
|
180
|
+
|
|
181
|
+
def get_key(self) -> str:
|
|
182
|
+
"""
|
|
183
|
+
Generate a unique key for grouping hooks.
|
|
184
|
+
|
|
185
|
+
Returns a key in the format: "{type}:{provider}" where provider
|
|
186
|
+
is omitted for git hooks.
|
|
187
|
+
"""
|
|
188
|
+
if self.type == HookType.GIT:
|
|
189
|
+
return self.type.value
|
|
190
|
+
return f"{self.type.value}:{self.provider}"
|
|
191
|
+
|
|
192
|
+
model_config = ConfigDict(frozen=True, extra="allow") # Allow extra fields
|
|
193
|
+
|
|
194
|
+
@model_validator(mode="before")
|
|
195
|
+
@classmethod
|
|
196
|
+
def collect_extra_fields(cls, data):
|
|
197
|
+
"""Collect unknown fields into the extra dictionary."""
|
|
198
|
+
if not isinstance(data, dict):
|
|
199
|
+
return data
|
|
200
|
+
|
|
201
|
+
# Known field names
|
|
202
|
+
known_fields = {"type", "event", "matcher", "priority", "description", "provider", "extra"}
|
|
203
|
+
|
|
204
|
+
# Separate known and unknown fields
|
|
205
|
+
extra = {}
|
|
206
|
+
cleaned = {}
|
|
207
|
+
for key, value in data.items():
|
|
208
|
+
if key in known_fields:
|
|
209
|
+
cleaned[key] = value
|
|
210
|
+
else:
|
|
211
|
+
extra[key] = value
|
|
212
|
+
|
|
213
|
+
# Add extra fields to the extra field
|
|
214
|
+
if extra:
|
|
215
|
+
existing_extra = cleaned.get("extra", {})
|
|
216
|
+
if isinstance(existing_extra, dict):
|
|
217
|
+
existing_extra.update(extra)
|
|
218
|
+
else:
|
|
219
|
+
existing_extra = extra
|
|
220
|
+
cleaned["extra"] = existing_extra
|
|
221
|
+
|
|
222
|
+
return cleaned
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class ParsedHook(BaseModel):
|
|
226
|
+
"""
|
|
227
|
+
A hook script that has been parsed with its metadata.
|
|
228
|
+
|
|
229
|
+
Contains both the metadata and the original script content/path
|
|
230
|
+
for execution.
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
metadata: HookMetadata
|
|
234
|
+
script_path: Path
|
|
235
|
+
content: str
|
|
236
|
+
front_matter_start_line: int = 0
|
|
237
|
+
front_matter_end_line: int = 0
|
|
238
|
+
|
|
239
|
+
model_config = {"arbitrary_types_allowed": True, "frozen": True}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class HookGroup(BaseModel):
|
|
243
|
+
"""
|
|
244
|
+
A group of hooks organized by type and provider.
|
|
245
|
+
|
|
246
|
+
Used by UniversalHookManager to organize scanned hooks.
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
key: str # e.g., "git" or "agent:claude-code"
|
|
250
|
+
hook_type: HookType
|
|
251
|
+
provider: Optional[str] = None
|
|
252
|
+
hooks: list[ParsedHook] = Field(default_factory=list)
|
|
253
|
+
|
|
254
|
+
def add_hook(self, hook: ParsedHook) -> None:
|
|
255
|
+
"""Add a hook to this group and re-sort by priority."""
|
|
256
|
+
self.hooks.append(hook)
|
|
257
|
+
# Sort by priority (lower = earlier)
|
|
258
|
+
self.hooks.sort(key=lambda h: h.metadata.priority)
|
|
259
|
+
|
|
260
|
+
def get_prioritized_hooks(self) -> list[ParsedHook]:
|
|
261
|
+
"""Get hooks sorted by priority."""
|
|
262
|
+
return self.hooks
|