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.
Files changed (120) hide show
  1. monoco/core/automation/__init__.py +0 -11
  2. monoco/core/automation/handlers.py +108 -26
  3. monoco/core/config.py +28 -10
  4. monoco/core/daemon/__init__.py +5 -0
  5. monoco/core/daemon/pid.py +290 -0
  6. monoco/core/injection.py +86 -8
  7. monoco/core/integrations.py +0 -24
  8. monoco/core/router/__init__.py +1 -39
  9. monoco/core/router/action.py +3 -142
  10. monoco/core/scheduler/events.py +28 -2
  11. monoco/core/setup.py +9 -0
  12. monoco/core/sync.py +199 -4
  13. monoco/core/watcher/__init__.py +6 -0
  14. monoco/core/watcher/base.py +18 -1
  15. monoco/core/watcher/im.py +460 -0
  16. monoco/core/watcher/memo.py +40 -48
  17. monoco/daemon/app.py +3 -60
  18. monoco/daemon/commands.py +459 -25
  19. monoco/daemon/scheduler.py +1 -16
  20. monoco/daemon/services.py +15 -0
  21. monoco/features/agent/resources/en/AGENTS.md +14 -14
  22. monoco/features/agent/resources/en/skills/monoco_role_engineer/SKILL.md +101 -0
  23. monoco/features/agent/resources/en/skills/monoco_role_manager/SKILL.md +95 -0
  24. monoco/features/agent/resources/en/skills/monoco_role_planner/SKILL.md +177 -0
  25. monoco/features/agent/resources/en/skills/monoco_role_reviewer/SKILL.md +139 -0
  26. monoco/features/agent/resources/zh/skills/monoco_role_engineer/SKILL.md +101 -0
  27. monoco/features/agent/resources/zh/skills/monoco_role_manager/SKILL.md +95 -0
  28. monoco/features/agent/resources/zh/skills/monoco_role_planner/SKILL.md +177 -0
  29. monoco/features/agent/resources/zh/skills/monoco_role_reviewer/SKILL.md +139 -0
  30. monoco/features/hooks/__init__.py +61 -6
  31. monoco/features/hooks/commands.py +281 -271
  32. monoco/features/hooks/dispatchers/__init__.py +23 -0
  33. monoco/features/hooks/dispatchers/agent_dispatcher.py +486 -0
  34. monoco/features/hooks/dispatchers/git_dispatcher.py +478 -0
  35. monoco/features/hooks/manager.py +357 -0
  36. monoco/features/hooks/models.py +262 -0
  37. monoco/features/hooks/parser.py +322 -0
  38. monoco/features/hooks/universal_interceptor.py +503 -0
  39. monoco/features/im/__init__.py +67 -0
  40. monoco/features/im/core.py +782 -0
  41. monoco/features/im/models.py +311 -0
  42. monoco/features/issue/commands.py +65 -50
  43. monoco/features/issue/core.py +199 -99
  44. monoco/features/issue/domain_commands.py +0 -19
  45. monoco/features/issue/resources/en/AGENTS.md +17 -122
  46. monoco/features/issue/resources/hooks/agent/before-tool.sh +102 -0
  47. monoco/features/issue/resources/hooks/agent/session-start.sh +88 -0
  48. monoco/features/issue/resources/hooks/{post-checkout.sh → git/git-post-checkout.sh} +10 -9
  49. monoco/features/issue/resources/hooks/git/git-pre-commit.sh +31 -0
  50. monoco/features/issue/resources/hooks/{pre-push.sh → git/git-pre-push.sh} +7 -13
  51. monoco/features/issue/resources/zh/AGENTS.md +18 -123
  52. monoco/features/memo/cli.py +15 -64
  53. monoco/features/memo/core.py +6 -34
  54. monoco/features/memo/models.py +24 -15
  55. monoco/features/memo/resources/en/AGENTS.md +31 -0
  56. monoco/features/memo/resources/zh/AGENTS.md +28 -5
  57. monoco/main.py +5 -3
  58. {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/METADATA +1 -1
  59. monoco_toolkit-0.4.0.dist-info/RECORD +170 -0
  60. monoco/core/automation/config.py +0 -338
  61. monoco/core/execution.py +0 -67
  62. monoco/core/executor/__init__.py +0 -38
  63. monoco/core/executor/agent_action.py +0 -254
  64. monoco/core/executor/git_action.py +0 -303
  65. monoco/core/executor/im_action.py +0 -309
  66. monoco/core/executor/pytest_action.py +0 -218
  67. monoco/core/router/router.py +0 -392
  68. monoco/features/agent/resources/atoms/atom-code-dev.yaml +0 -61
  69. monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +0 -73
  70. monoco/features/agent/resources/atoms/atom-knowledge.yaml +0 -55
  71. monoco/features/agent/resources/atoms/atom-review.yaml +0 -60
  72. monoco/features/agent/resources/en/skills/monoco_atom_core/SKILL.md +0 -99
  73. monoco/features/agent/resources/en/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
  74. monoco/features/agent/resources/en/skills/monoco_workflow_agent_manager/SKILL.md +0 -93
  75. monoco/features/agent/resources/en/skills/monoco_workflow_agent_planner/SKILL.md +0 -85
  76. monoco/features/agent/resources/en/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -114
  77. monoco/features/agent/resources/workflows/workflow-dev.yaml +0 -83
  78. monoco/features/agent/resources/workflows/workflow-issue-create.yaml +0 -72
  79. monoco/features/agent/resources/workflows/workflow-review.yaml +0 -94
  80. monoco/features/agent/resources/zh/roles/monoco_role_engineer.yaml +0 -49
  81. monoco/features/agent/resources/zh/roles/monoco_role_manager.yaml +0 -46
  82. monoco/features/agent/resources/zh/roles/monoco_role_planner.yaml +0 -46
  83. monoco/features/agent/resources/zh/roles/monoco_role_reviewer.yaml +0 -47
  84. monoco/features/agent/resources/zh/skills/monoco_atom_core/SKILL.md +0 -99
  85. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
  86. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_manager/SKILL.md +0 -88
  87. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_planner/SKILL.md +0 -259
  88. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -137
  89. monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +0 -278
  90. monoco/features/glossary/resources/en/skills/monoco_atom_glossary/SKILL.md +0 -35
  91. monoco/features/glossary/resources/zh/skills/monoco_atom_glossary/SKILL.md +0 -35
  92. monoco/features/hooks/adapter.py +0 -67
  93. monoco/features/hooks/core.py +0 -441
  94. monoco/features/i18n/resources/en/skills/monoco_atom_i18n/SKILL.md +0 -96
  95. monoco/features/i18n/resources/en/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
  96. monoco/features/i18n/resources/zh/skills/monoco_atom_i18n/SKILL.md +0 -96
  97. monoco/features/i18n/resources/zh/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
  98. monoco/features/issue/resources/en/skills/monoco_atom_issue/SKILL.md +0 -165
  99. monoco/features/issue/resources/en/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
  100. monoco/features/issue/resources/en/skills/monoco_workflow_issue_development/SKILL.md +0 -224
  101. monoco/features/issue/resources/en/skills/monoco_workflow_issue_management/SKILL.md +0 -159
  102. monoco/features/issue/resources/en/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
  103. monoco/features/issue/resources/hooks/pre-commit.sh +0 -41
  104. monoco/features/issue/resources/zh/skills/monoco_atom_issue_lifecycle/SKILL.md +0 -190
  105. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
  106. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_development/SKILL.md +0 -224
  107. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_management/SKILL.md +0 -159
  108. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
  109. monoco/features/memo/resources/en/skills/monoco_atom_memo/SKILL.md +0 -77
  110. monoco/features/memo/resources/en/skills/monoco_workflow_note_processing/SKILL.md +0 -140
  111. monoco/features/memo/resources/zh/skills/monoco_atom_memo/SKILL.md +0 -77
  112. monoco/features/memo/resources/zh/skills/monoco_workflow_note_processing/SKILL.md +0 -140
  113. monoco/features/spike/resources/en/skills/monoco_atom_spike/SKILL.md +0 -76
  114. monoco/features/spike/resources/en/skills/monoco_workflow_research/SKILL.md +0 -121
  115. monoco/features/spike/resources/zh/skills/monoco_atom_spike/SKILL.md +0 -76
  116. monoco/features/spike/resources/zh/skills/monoco_workflow_research/SKILL.md +0 -121
  117. monoco_toolkit-0.3.12.dist-info/RECORD +0 -202
  118. {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/WHEEL +0 -0
  119. {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/entry_points.txt +0 -0
  120. {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