monoco-toolkit 0.3.10__py3-none-any.whl → 0.3.12__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 (130) hide show
  1. monoco/__main__.py +8 -0
  2. monoco/core/artifacts/__init__.py +16 -0
  3. monoco/core/artifacts/manager.py +575 -0
  4. monoco/core/artifacts/models.py +161 -0
  5. monoco/core/automation/__init__.py +51 -0
  6. monoco/core/automation/config.py +338 -0
  7. monoco/core/automation/field_watcher.py +296 -0
  8. monoco/core/automation/handlers.py +723 -0
  9. monoco/core/config.py +31 -4
  10. monoco/core/executor/__init__.py +38 -0
  11. monoco/core/executor/agent_action.py +254 -0
  12. monoco/core/executor/git_action.py +303 -0
  13. monoco/core/executor/im_action.py +309 -0
  14. monoco/core/executor/pytest_action.py +218 -0
  15. monoco/core/git.py +38 -0
  16. monoco/core/hooks/context.py +74 -13
  17. monoco/core/ingestion/__init__.py +20 -0
  18. monoco/core/ingestion/discovery.py +248 -0
  19. monoco/core/ingestion/watcher.py +343 -0
  20. monoco/core/ingestion/worker.py +436 -0
  21. monoco/core/loader.py +633 -0
  22. monoco/core/registry.py +34 -25
  23. monoco/core/router/__init__.py +55 -0
  24. monoco/core/router/action.py +341 -0
  25. monoco/core/router/router.py +392 -0
  26. monoco/core/scheduler/__init__.py +63 -0
  27. monoco/core/scheduler/base.py +152 -0
  28. monoco/core/scheduler/engines.py +175 -0
  29. monoco/core/scheduler/events.py +171 -0
  30. monoco/core/scheduler/local.py +377 -0
  31. monoco/core/skills.py +119 -80
  32. monoco/core/watcher/__init__.py +57 -0
  33. monoco/core/watcher/base.py +365 -0
  34. monoco/core/watcher/dropzone.py +152 -0
  35. monoco/core/watcher/issue.py +303 -0
  36. monoco/core/watcher/memo.py +200 -0
  37. monoco/core/watcher/task.py +238 -0
  38. monoco/daemon/app.py +77 -1
  39. monoco/daemon/commands.py +10 -0
  40. monoco/daemon/events.py +34 -0
  41. monoco/daemon/mailroom_service.py +196 -0
  42. monoco/daemon/models.py +1 -0
  43. monoco/daemon/scheduler.py +207 -0
  44. monoco/daemon/services.py +27 -58
  45. monoco/daemon/triggers.py +55 -0
  46. monoco/features/agent/__init__.py +25 -7
  47. monoco/features/agent/adapter.py +17 -7
  48. monoco/features/agent/cli.py +91 -57
  49. monoco/features/agent/engines.py +31 -170
  50. monoco/{core/resources/en/skills/monoco_core → features/agent/resources/en/skills/monoco_atom_core}/SKILL.md +2 -2
  51. monoco/features/agent/resources/en/skills/{flow_engineer → monoco_workflow_agent_engineer}/SKILL.md +2 -2
  52. monoco/features/agent/resources/en/skills/{flow_manager → monoco_workflow_agent_manager}/SKILL.md +2 -2
  53. monoco/features/agent/resources/en/skills/{flow_planner → monoco_workflow_agent_planner}/SKILL.md +2 -2
  54. monoco/features/agent/resources/en/skills/{flow_reviewer → monoco_workflow_agent_reviewer}/SKILL.md +2 -2
  55. monoco/features/agent/resources/{roles/role-engineer.yaml → zh/roles/monoco_role_engineer.yaml} +3 -3
  56. monoco/features/agent/resources/{roles/role-manager.yaml → zh/roles/monoco_role_manager.yaml} +8 -8
  57. monoco/features/agent/resources/{roles/role-planner.yaml → zh/roles/monoco_role_planner.yaml} +8 -8
  58. monoco/features/agent/resources/{roles/role-reviewer.yaml → zh/roles/monoco_role_reviewer.yaml} +8 -8
  59. monoco/{core/resources/zh/skills/monoco_core → features/agent/resources/zh/skills/monoco_atom_core}/SKILL.md +2 -2
  60. monoco/features/agent/resources/zh/skills/{flow_engineer → monoco_workflow_agent_engineer}/SKILL.md +2 -2
  61. monoco/features/agent/resources/zh/skills/{flow_manager → monoco_workflow_agent_manager}/SKILL.md +2 -2
  62. monoco/features/agent/resources/zh/skills/{flow_planner → monoco_workflow_agent_planner}/SKILL.md +2 -2
  63. monoco/features/agent/resources/zh/skills/{flow_reviewer → monoco_workflow_agent_reviewer}/SKILL.md +2 -2
  64. monoco/features/agent/worker.py +1 -1
  65. monoco/features/artifact/__init__.py +0 -0
  66. monoco/features/artifact/adapter.py +33 -0
  67. monoco/features/artifact/resources/zh/AGENTS.md +14 -0
  68. monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +278 -0
  69. monoco/features/glossary/adapter.py +18 -7
  70. monoco/features/glossary/resources/en/skills/{monoco_glossary → monoco_atom_glossary}/SKILL.md +2 -2
  71. monoco/features/glossary/resources/zh/skills/{monoco_glossary → monoco_atom_glossary}/SKILL.md +2 -2
  72. monoco/features/hooks/__init__.py +11 -0
  73. monoco/features/hooks/adapter.py +67 -0
  74. monoco/features/hooks/commands.py +309 -0
  75. monoco/features/hooks/core.py +441 -0
  76. monoco/features/hooks/resources/ADDING_HOOKS.md +234 -0
  77. monoco/features/i18n/adapter.py +18 -5
  78. monoco/features/i18n/core.py +482 -17
  79. monoco/features/i18n/resources/en/skills/{monoco_i18n → monoco_atom_i18n}/SKILL.md +2 -2
  80. monoco/features/i18n/resources/en/skills/{i18n_scan_workflow → monoco_workflow_i18n_scan}/SKILL.md +2 -2
  81. monoco/features/i18n/resources/zh/skills/{monoco_i18n → monoco_atom_i18n}/SKILL.md +2 -2
  82. monoco/features/i18n/resources/zh/skills/{i18n_scan_workflow → monoco_workflow_i18n_scan}/SKILL.md +2 -2
  83. monoco/features/issue/adapter.py +19 -6
  84. monoco/features/issue/commands.py +352 -20
  85. monoco/features/issue/core.py +475 -16
  86. monoco/features/issue/engine/machine.py +114 -4
  87. monoco/features/issue/linter.py +60 -5
  88. monoco/features/issue/models.py +2 -2
  89. monoco/features/issue/resources/en/AGENTS.md +109 -0
  90. monoco/features/issue/resources/en/skills/{monoco_issue → monoco_atom_issue}/SKILL.md +2 -2
  91. monoco/features/issue/resources/en/skills/{issue_create_workflow → monoco_workflow_issue_creation}/SKILL.md +2 -2
  92. monoco/features/issue/resources/en/skills/{issue_develop_workflow → monoco_workflow_issue_development}/SKILL.md +2 -2
  93. monoco/features/issue/resources/en/skills/{issue_lifecycle_workflow → monoco_workflow_issue_management}/SKILL.md +2 -2
  94. monoco/features/issue/resources/en/skills/{issue_refine_workflow → monoco_workflow_issue_refinement}/SKILL.md +2 -2
  95. monoco/features/issue/resources/hooks/post-checkout.sh +39 -0
  96. monoco/features/issue/resources/hooks/pre-commit.sh +41 -0
  97. monoco/features/issue/resources/hooks/pre-push.sh +35 -0
  98. monoco/features/issue/resources/zh/AGENTS.md +109 -0
  99. monoco/features/issue/resources/zh/skills/{monoco_issue → monoco_atom_issue_lifecycle}/SKILL.md +2 -2
  100. monoco/features/issue/resources/zh/skills/{issue_create_workflow → monoco_workflow_issue_creation}/SKILL.md +2 -2
  101. monoco/features/issue/resources/zh/skills/{issue_develop_workflow → monoco_workflow_issue_development}/SKILL.md +2 -2
  102. monoco/features/issue/resources/zh/skills/{issue_lifecycle_workflow → monoco_workflow_issue_management}/SKILL.md +2 -2
  103. monoco/features/issue/resources/zh/skills/{issue_refine_workflow → monoco_workflow_issue_refinement}/SKILL.md +2 -2
  104. monoco/features/issue/validator.py +101 -1
  105. monoco/features/memo/adapter.py +21 -8
  106. monoco/features/memo/cli.py +103 -10
  107. monoco/features/memo/core.py +178 -92
  108. monoco/features/memo/models.py +53 -0
  109. monoco/features/memo/resources/en/skills/{monoco_memo → monoco_atom_memo}/SKILL.md +2 -2
  110. monoco/features/memo/resources/en/skills/{note_processing_workflow → monoco_workflow_note_processing}/SKILL.md +2 -2
  111. monoco/features/memo/resources/zh/skills/{monoco_memo → monoco_atom_memo}/SKILL.md +2 -2
  112. monoco/features/memo/resources/zh/skills/{note_processing_workflow → monoco_workflow_note_processing}/SKILL.md +2 -2
  113. monoco/features/spike/adapter.py +18 -5
  114. monoco/features/spike/commands.py +5 -3
  115. monoco/features/spike/resources/en/skills/{monoco_spike → monoco_atom_spike}/SKILL.md +2 -2
  116. monoco/features/spike/resources/en/skills/{research_workflow → monoco_workflow_research}/SKILL.md +2 -2
  117. monoco/features/spike/resources/zh/skills/{monoco_spike → monoco_atom_spike}/SKILL.md +2 -2
  118. monoco/features/spike/resources/zh/skills/{research_workflow → monoco_workflow_research}/SKILL.md +2 -2
  119. monoco/main.py +38 -1
  120. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/METADATA +7 -1
  121. monoco_toolkit-0.3.12.dist-info/RECORD +202 -0
  122. monoco/features/agent/apoptosis.py +0 -44
  123. monoco/features/agent/manager.py +0 -91
  124. monoco/features/agent/session.py +0 -121
  125. monoco_toolkit-0.3.10.dist-info/RECORD +0 -156
  126. /monoco/{core → features/agent}/resources/en/AGENTS.md +0 -0
  127. /monoco/{core → features/agent}/resources/zh/AGENTS.md +0 -0
  128. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/WHEEL +0 -0
  129. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/entry_points.txt +0 -0
  130. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,57 @@
1
+ """
2
+ Watcher Module - Layer 1 of the Event Automation Framework.
3
+
4
+ This module provides file system watching capabilities with event emission.
5
+ It is part of the three-layer architecture:
6
+ - Layer 1: File Watcher (this module)
7
+ - Layer 2: Action Router
8
+ - Layer 3: Action Executor
9
+
10
+ Example Usage:
11
+ >>> from monoco.core.watcher import IssueWatcher, WatchConfig
12
+ >>> from pathlib import Path
13
+ >>>
14
+ >>> config = WatchConfig(
15
+ ... path=Path("./Issues"),
16
+ ... patterns=["*.md"],
17
+ ... recursive=True,
18
+ ... )
19
+ >>> watcher = IssueWatcher(config)
20
+ >>> await watcher.start()
21
+ >>> # Events are automatically emitted to EventBus
22
+ >>> await watcher.stop()
23
+ """
24
+
25
+ from .base import (
26
+ ChangeType,
27
+ FieldChange,
28
+ FileEvent,
29
+ FilesystemWatcher,
30
+ PollingWatcher,
31
+ WatchdogWatcher,
32
+ WatchConfig,
33
+ )
34
+ from .issue import IssueWatcher, IssueFileEvent
35
+ from .memo import MemoWatcher, MemoFileEvent
36
+ from .task import TaskWatcher, TaskFileEvent
37
+ from .dropzone import DropzoneWatcher, DropzoneFileEvent
38
+
39
+ __all__ = [
40
+ # Base classes
41
+ "ChangeType",
42
+ "FieldChange",
43
+ "FileEvent",
44
+ "FilesystemWatcher",
45
+ "PollingWatcher",
46
+ "WatchdogWatcher",
47
+ "WatchConfig",
48
+ # Concrete watchers
49
+ "IssueWatcher",
50
+ "IssueFileEvent",
51
+ "MemoWatcher",
52
+ "MemoFileEvent",
53
+ "TaskWatcher",
54
+ "TaskFileEvent",
55
+ "DropzoneWatcher",
56
+ "DropzoneFileEvent",
57
+ ]
@@ -0,0 +1,365 @@
1
+ """
2
+ Base abstractions for FilesystemWatcher - Layer 1 of the event automation framework.
3
+
4
+ This module defines the core abstractions for file system event watching:
5
+ - FilesystemWatcher: Abstract base class for all file watchers
6
+ - FileEvent: Dataclass representing a file system event
7
+ - WatchConfig: Configuration for file watching
8
+ - ChangeType: Enum for types of file changes
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ import inspect
15
+ import logging
16
+ from abc import ABC, abstractmethod
17
+ from dataclasses import dataclass, field
18
+ from datetime import datetime
19
+ from enum import Enum, auto
20
+ from pathlib import Path
21
+ from typing import Any, Callable, Dict, List, Optional, Set, Union
22
+
23
+ from monoco.core.scheduler import AgentEventType, EventBus, event_bus
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class ChangeType(Enum):
29
+ """Types of file system changes."""
30
+ CREATED = "created"
31
+ MODIFIED = "modified"
32
+ DELETED = "deleted"
33
+ MOVED = "moved"
34
+ RENAMED = "renamed"
35
+
36
+
37
+ @dataclass
38
+ class FileEvent:
39
+ """
40
+ Represents a file system event.
41
+
42
+ Attributes:
43
+ path: Path to the file or directory
44
+ change_type: Type of change (created, modified, deleted, etc.)
45
+ watcher_name: Name of the watcher that emitted this event
46
+ old_path: Original path for move/rename events
47
+ old_content: Previous content hash or snapshot (for content tracking)
48
+ new_content: Current content hash or snapshot
49
+ metadata: Additional event metadata
50
+ timestamp: Event timestamp
51
+ """
52
+ path: Path
53
+ change_type: ChangeType
54
+ watcher_name: str
55
+ old_path: Optional[Path] = None
56
+ old_content: Optional[str] = None
57
+ new_content: Optional[str] = None
58
+ metadata: Dict[str, Any] = field(default_factory=dict)
59
+ timestamp: datetime = field(default_factory=datetime.now)
60
+
61
+ def to_agent_event_type(self) -> Optional[AgentEventType]:
62
+ """Convert FileEvent to AgentEventType if applicable."""
63
+ # This will be overridden by specific watchers
64
+ return None
65
+
66
+ def to_payload(self) -> Dict[str, Any]:
67
+ """Convert to payload dict for EventBus."""
68
+ return {
69
+ "path": str(self.path),
70
+ "change_type": self.change_type.value,
71
+ "watcher_name": self.watcher_name,
72
+ "old_path": str(self.old_path) if self.old_path else None,
73
+ "old_content": self.old_content,
74
+ "new_content": self.new_content,
75
+ "metadata": self.metadata,
76
+ "timestamp": self.timestamp.isoformat(),
77
+ }
78
+
79
+
80
+ @dataclass
81
+ class WatchConfig:
82
+ """
83
+ Configuration for file watching.
84
+
85
+ Attributes:
86
+ path: Path to watch (file or directory)
87
+ patterns: Glob patterns to match (e.g., "*.md", "*.yaml")
88
+ exclude_patterns: Patterns to exclude
89
+ recursive: Whether to watch recursively
90
+ field_extractors: Optional field extractors for content parsing
91
+ poll_interval: Polling interval in seconds (for polling-based watchers)
92
+ """
93
+ path: Path
94
+ patterns: List[str] = field(default_factory=lambda: ["*"])
95
+ exclude_patterns: List[str] = field(default_factory=list)
96
+ recursive: bool = True
97
+ field_extractors: Dict[str, Callable[[str], Any]] = field(default_factory=dict)
98
+ poll_interval: float = 5.0
99
+
100
+ def should_watch(self, file_path: Path) -> bool:
101
+ """Check if a file should be watched based on patterns."""
102
+ # Check exclude patterns first
103
+ for pattern in self.exclude_patterns:
104
+ if file_path.match(pattern):
105
+ return False
106
+
107
+ # Check include patterns
108
+ for pattern in self.patterns:
109
+ if file_path.match(pattern):
110
+ return True
111
+
112
+ return False
113
+
114
+
115
+ @dataclass
116
+ class FieldChange:
117
+ """Represents a change in a specific field."""
118
+ field_name: str
119
+ old_value: Any
120
+ new_value: Any
121
+ change_type: ChangeType = ChangeType.MODIFIED
122
+
123
+
124
+ class FilesystemWatcher(ABC):
125
+ """
126
+ Abstract base class for file system watchers (Layer 1).
127
+
128
+ Responsibilities:
129
+ - Monitor file system changes
130
+ - Emit FileEvent objects
131
+ - Integrate with EventBus for event publishing
132
+
133
+ Lifecycle:
134
+ 1. Create watcher with config
135
+ 2. Call start() to begin watching
136
+ 3. File events are emitted via emit() or callbacks
137
+ 4. Call stop() to cleanup
138
+
139
+ Example:
140
+ >>> config = WatchConfig(path=Path("./Issues"), patterns=["*.md"])
141
+ >>> watcher = IssueWatcher(config)
142
+ >>> await watcher.start()
143
+ >>> # Events are automatically emitted to EventBus
144
+ >>> await watcher.stop()
145
+ """
146
+
147
+ def __init__(
148
+ self,
149
+ config: WatchConfig,
150
+ event_bus: Optional[EventBus] = None,
151
+ name: Optional[str] = None,
152
+ ):
153
+ self.config = config
154
+ self.event_bus = event_bus or event_bus
155
+ self.name = name or self.__class__.__name__
156
+ self._running = False
157
+ self._callbacks: List[Callable[[FileEvent], None]] = []
158
+ self._state_cache: Dict[str, Any] = {} # For tracking state changes
159
+
160
+ @abstractmethod
161
+ async def start(self) -> None:
162
+ """Start watching the file system."""
163
+ pass
164
+
165
+ @abstractmethod
166
+ async def stop(self) -> None:
167
+ """Stop watching and cleanup resources."""
168
+ pass
169
+
170
+ def is_running(self) -> bool:
171
+ """Check if the watcher is currently running."""
172
+ return self._running
173
+
174
+ def register_callback(self, callback: Callable[[FileEvent], None]) -> None:
175
+ """Register a callback for file events."""
176
+ self._callbacks.append(callback)
177
+
178
+ def unregister_callback(self, callback: Callable[[FileEvent], None]) -> None:
179
+ """Unregister a callback."""
180
+ if callback in self._callbacks:
181
+ self._callbacks.remove(callback)
182
+
183
+ async def emit(self, event: FileEvent) -> None:
184
+ """
185
+ Emit a file event to all registered callbacks and EventBus.
186
+
187
+ Args:
188
+ event: The FileEvent to emit
189
+ """
190
+ # Call local callbacks
191
+ for callback in self._callbacks:
192
+ try:
193
+ if inspect.iscoroutinefunction(callback):
194
+ await callback(event)
195
+ else:
196
+ callback(event)
197
+ except Exception as e:
198
+ logger.error(f"Error in callback for {event}: {e}")
199
+
200
+ # Publish to EventBus if available
201
+ if self.event_bus:
202
+ try:
203
+ agent_event_type = event.to_agent_event_type()
204
+ if agent_event_type:
205
+ await self.event_bus.publish(
206
+ agent_event_type,
207
+ event.to_payload(),
208
+ source=f"watcher.{self.name}",
209
+ )
210
+ except Exception as e:
211
+ logger.error(f"Error publishing to EventBus: {e}")
212
+
213
+ def _get_file_hash(self, file_path: Path) -> Optional[str]:
214
+ """Get a hash of file content for change detection."""
215
+ try:
216
+ import hashlib
217
+ content = file_path.read_text(encoding="utf-8")
218
+ return hashlib.md5(content.encode()).hexdigest()
219
+ except Exception:
220
+ return None
221
+
222
+ def _read_file_content(self, file_path: Path) -> Optional[str]:
223
+ """Read file content safely."""
224
+ try:
225
+ return file_path.read_text(encoding="utf-8")
226
+ except Exception as e:
227
+ logger.debug(f"Could not read {file_path}: {e}")
228
+ return None
229
+
230
+ def get_stats(self) -> Dict[str, Any]:
231
+ """Get watcher statistics."""
232
+ return {
233
+ "name": self.name,
234
+ "running": self._running,
235
+ "config": {
236
+ "path": str(self.config.path),
237
+ "patterns": self.config.patterns,
238
+ "recursive": self.config.recursive,
239
+ },
240
+ "callbacks": len(self._callbacks),
241
+ }
242
+
243
+
244
+ class PollingWatcher(FilesystemWatcher):
245
+ """
246
+ Base class for polling-based file watchers.
247
+
248
+ Useful for watching specific files or when native file system
249
+ events are not available/reliable.
250
+ """
251
+
252
+ def __init__(
253
+ self,
254
+ config: WatchConfig,
255
+ event_bus: Optional[EventBus] = None,
256
+ name: Optional[str] = None,
257
+ ):
258
+ super().__init__(config, event_bus, name)
259
+ self._poll_task: Optional[asyncio.Task] = None
260
+ self._file_states: Dict[Path, Dict[str, Any]] = {}
261
+
262
+ async def start(self) -> None:
263
+ """Start polling loop."""
264
+ if self._running:
265
+ return
266
+
267
+ self._running = True
268
+ self._poll_task = asyncio.create_task(self._poll_loop())
269
+ logger.info(f"Started polling watcher: {self.name}")
270
+
271
+ async def stop(self) -> None:
272
+ """Stop polling loop."""
273
+ if not self._running:
274
+ return
275
+
276
+ self._running = False
277
+
278
+ if self._poll_task:
279
+ self._poll_task.cancel()
280
+ try:
281
+ await self._poll_task
282
+ except asyncio.CancelledError:
283
+ pass
284
+ self._poll_task = None
285
+
286
+ logger.info(f"Stopped polling watcher: {self.name}")
287
+
288
+ async def _poll_loop(self) -> None:
289
+ """Main polling loop."""
290
+ while self._running:
291
+ try:
292
+ await self._check_changes()
293
+ await asyncio.sleep(self.config.poll_interval)
294
+ except asyncio.CancelledError:
295
+ break
296
+ except Exception as e:
297
+ logger.error(f"Error in poll loop: {e}")
298
+ await asyncio.sleep(self.config.poll_interval)
299
+
300
+ @abstractmethod
301
+ async def _check_changes(self) -> None:
302
+ """Check for changes - implement in subclass."""
303
+ pass
304
+
305
+ def _scan_files(self) -> Dict[Path, Dict[str, Any]]:
306
+ """Scan watched path and return file states."""
307
+ states = {}
308
+
309
+ if self.config.path.is_file():
310
+ files = [self.config.path]
311
+ else:
312
+ if self.config.recursive:
313
+ files = list(self.config.path.rglob("*"))
314
+ else:
315
+ files = list(self.config.path.glob("*"))
316
+
317
+ for file_path in files:
318
+ if not file_path.is_file():
319
+ continue
320
+
321
+ if not self.config.should_watch(file_path):
322
+ continue
323
+
324
+ try:
325
+ stat = file_path.stat()
326
+ content = self._read_file_content(file_path)
327
+ states[file_path] = {
328
+ "mtime": stat.st_mtime,
329
+ "size": stat.st_size,
330
+ "content": content,
331
+ "hash": self._get_file_hash(file_path) if content else None,
332
+ }
333
+ except Exception as e:
334
+ logger.debug(f"Could not stat {file_path}: {e}")
335
+
336
+ return states
337
+
338
+
339
+ class WatchdogWatcher(FilesystemWatcher):
340
+ """
341
+ Base class for watchdog-based file watchers.
342
+
343
+ Uses the watchdog library for efficient native file system events.
344
+ """
345
+
346
+ def __init__(
347
+ self,
348
+ config: WatchConfig,
349
+ event_bus: Optional[EventBus] = None,
350
+ name: Optional[str] = None,
351
+ ):
352
+ super().__init__(config, event_bus, name)
353
+ self._observer: Optional[Any] = None
354
+
355
+ def _should_process(self, file_path: Path) -> bool:
356
+ """Check if a file should be processed."""
357
+ # Skip hidden files
358
+ if file_path.name.startswith("."):
359
+ return False
360
+
361
+ # Skip temporary files
362
+ if file_path.suffix in (".tmp", ".temp", ".part", ".swp", "~"):
363
+ return False
364
+
365
+ return self.config.should_watch(file_path)
@@ -0,0 +1,152 @@
1
+ """
2
+ DropzoneWatcher - Adapter for the existing dropzone watcher.
3
+
4
+ Part of Layer 1 (File Watcher) in the event automation framework.
5
+ Wraps the existing ingestion watcher to fit the FilesystemWatcher interface.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import logging
12
+ from pathlib import Path
13
+ from typing import Any, Callable, Dict, Optional
14
+
15
+ from monoco.core.scheduler import AgentEventType, EventBus, event_bus
16
+ from monoco.core.ingestion.watcher import (
17
+ DropzoneWatcher as LegacyDropzoneWatcher,
18
+ IngestionEvent,
19
+ IngestionEventType,
20
+ )
21
+ from monoco.core.artifacts.manager import ArtifactManager
22
+
23
+ from .base import (
24
+ ChangeType,
25
+ FileEvent,
26
+ FilesystemWatcher,
27
+ WatchConfig,
28
+ )
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ class DropzoneFileEvent(FileEvent):
34
+ """FileEvent specific to Dropzone."""
35
+
36
+ def __init__(
37
+ self,
38
+ path: Path,
39
+ change_type: ChangeType,
40
+ ingestion_event_type: IngestionEventType,
41
+ artifact_id: Optional[str] = None,
42
+ **kwargs,
43
+ ):
44
+ super().__init__(
45
+ path=path,
46
+ change_type=change_type,
47
+ watcher_name="DropzoneWatcher",
48
+ **kwargs,
49
+ )
50
+ self.ingestion_event_type = ingestion_event_type
51
+ self.artifact_id = artifact_id
52
+
53
+ def to_agent_event_type(self) -> Optional[AgentEventType]:
54
+ """Dropzone events don't map directly to agent events."""
55
+ return None
56
+
57
+ def to_payload(self) -> Dict[str, Any]:
58
+ """Convert to payload with Dropzone-specific fields."""
59
+ payload = super().to_payload()
60
+ payload["ingestion_event_type"] = self.ingestion_event_type.value
61
+ payload["artifact_id"] = self.artifact_id
62
+ return payload
63
+
64
+
65
+ class DropzoneWatcher(FilesystemWatcher):
66
+ """
67
+ Adapter for the existing DropzoneWatcher.
68
+
69
+ Wraps the legacy ingestion watcher to provide a unified interface
70
+ while maintaining backward compatibility.
71
+
72
+ Example:
73
+ >>> from monoco.core.artifacts.manager import ArtifactManager
74
+ >>> artifact_manager = ArtifactManager()
75
+ >>> config = WatchConfig(path=Path("./.monoco/dropzone"))
76
+ >>> watcher = DropzoneWatcher(config, artifact_manager)
77
+ >>> await watcher.start()
78
+ """
79
+
80
+ def __init__(
81
+ self,
82
+ config: WatchConfig,
83
+ artifact_manager: ArtifactManager,
84
+ event_bus: Optional[EventBus] = None,
85
+ name: str = "DropzoneWatcher",
86
+ ):
87
+ super().__init__(config, event_bus, name)
88
+ self.artifact_manager = artifact_manager
89
+
90
+ # Create legacy watcher
91
+ self._legacy_watcher = LegacyDropzoneWatcher(
92
+ dropzone_path=config.path,
93
+ artifact_manager=artifact_manager,
94
+ process_existing=False,
95
+ )
96
+
97
+ # Set up event forwarding
98
+ self._legacy_watcher.set_event_callback(self._on_ingestion_event)
99
+
100
+ async def start(self) -> None:
101
+ """Start watching the dropzone."""
102
+ if self._running:
103
+ return
104
+
105
+ self._legacy_watcher.start()
106
+ self._running = True
107
+ logger.info(f"Started DropzoneWatcher: {self.config.path}")
108
+
109
+ async def stop(self) -> None:
110
+ """Stop watching the dropzone."""
111
+ if not self._running:
112
+ return
113
+
114
+ self._legacy_watcher.stop()
115
+ self._running = False
116
+ logger.info(f"Stopped DropzoneWatcher: {self.config.path}")
117
+
118
+ def _on_ingestion_event(self, event: IngestionEvent) -> None:
119
+ """Handle ingestion events from legacy watcher."""
120
+ # Map ingestion event type to change type
121
+ change_type_map = {
122
+ IngestionEventType.FILE_DETECTED: ChangeType.CREATED,
123
+ IngestionEventType.CONVERSION_STARTED: ChangeType.MODIFIED,
124
+ IngestionEventType.CONVERSION_COMPLETED: ChangeType.MODIFIED,
125
+ IngestionEventType.CONVERSION_FAILED: ChangeType.MODIFIED,
126
+ IngestionEventType.ARTIFACT_REGISTERED: ChangeType.MODIFIED,
127
+ }
128
+
129
+ change_type = change_type_map.get(event.event_type, ChangeType.MODIFIED)
130
+
131
+ # Create unified event
132
+ file_event = DropzoneFileEvent(
133
+ path=event.file_path,
134
+ change_type=change_type,
135
+ ingestion_event_type=event.event_type,
136
+ artifact_id=event.artifact_id,
137
+ metadata={
138
+ "task_id": event.task_id,
139
+ "error_message": event.error_message,
140
+ **event.metadata,
141
+ },
142
+ )
143
+
144
+ # Emit synchronously (called from sync context)
145
+ asyncio.create_task(self.emit(file_event))
146
+
147
+ def get_stats(self) -> Dict[str, Any]:
148
+ """Get watcher statistics."""
149
+ stats = super().get_stats()
150
+ legacy_stats = self._legacy_watcher.get_stats()
151
+ stats.update(legacy_stats)
152
+ return stats