monoco-toolkit 0.3.9__py3-none-any.whl → 0.3.11__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 (132) 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/config.py +38 -4
  6. monoco/core/git.py +23 -0
  7. monoco/core/hooks/builtin/git_cleanup.py +1 -1
  8. monoco/core/ingestion/__init__.py +20 -0
  9. monoco/core/ingestion/discovery.py +248 -0
  10. monoco/core/ingestion/watcher.py +343 -0
  11. monoco/core/ingestion/worker.py +436 -0
  12. monoco/core/injection.py +63 -29
  13. monoco/core/integrations.py +2 -2
  14. monoco/core/loader.py +633 -0
  15. monoco/core/output.py +5 -5
  16. monoco/core/registry.py +34 -19
  17. monoco/core/resource/__init__.py +5 -0
  18. monoco/core/resource/finder.py +98 -0
  19. monoco/core/resource/manager.py +91 -0
  20. monoco/core/resource/models.py +35 -0
  21. monoco/core/skill_framework.py +292 -0
  22. monoco/core/skills.py +524 -385
  23. monoco/core/sync.py +73 -1
  24. monoco/core/workflow_converter.py +420 -0
  25. monoco/daemon/app.py +77 -1
  26. monoco/daemon/commands.py +10 -0
  27. monoco/daemon/mailroom_service.py +196 -0
  28. monoco/daemon/models.py +1 -0
  29. monoco/daemon/scheduler.py +236 -0
  30. monoco/daemon/services.py +185 -0
  31. monoco/daemon/triggers.py +55 -0
  32. monoco/features/agent/__init__.py +2 -2
  33. monoco/features/agent/adapter.py +41 -0
  34. monoco/features/agent/apoptosis.py +44 -0
  35. monoco/features/agent/cli.py +101 -144
  36. monoco/features/agent/config.py +35 -21
  37. monoco/features/agent/defaults.py +6 -49
  38. monoco/features/agent/engines.py +32 -6
  39. monoco/features/agent/manager.py +47 -6
  40. monoco/features/agent/models.py +2 -2
  41. monoco/features/agent/resources/atoms/atom-code-dev.yaml +61 -0
  42. monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +73 -0
  43. monoco/features/agent/resources/atoms/atom-knowledge.yaml +55 -0
  44. monoco/features/agent/resources/atoms/atom-review.yaml +60 -0
  45. monoco/{core/resources/en → features/agent/resources/en/skills/monoco_atom_core}/SKILL.md +3 -1
  46. monoco/features/agent/resources/en/skills/monoco_workflow_agent_engineer/SKILL.md +94 -0
  47. monoco/features/agent/resources/en/skills/monoco_workflow_agent_manager/SKILL.md +93 -0
  48. monoco/features/agent/resources/en/skills/monoco_workflow_agent_planner/SKILL.md +85 -0
  49. monoco/features/agent/resources/en/skills/monoco_workflow_agent_reviewer/SKILL.md +114 -0
  50. monoco/features/agent/resources/workflows/workflow-dev.yaml +83 -0
  51. monoco/features/agent/resources/workflows/workflow-issue-create.yaml +72 -0
  52. monoco/features/agent/resources/workflows/workflow-review.yaml +94 -0
  53. monoco/features/agent/resources/zh/roles/monoco_role_engineer.yaml +49 -0
  54. monoco/features/agent/resources/zh/roles/monoco_role_manager.yaml +46 -0
  55. monoco/features/agent/resources/zh/roles/monoco_role_planner.yaml +46 -0
  56. monoco/features/agent/resources/zh/roles/monoco_role_reviewer.yaml +47 -0
  57. monoco/{core/resources/zh → features/agent/resources/zh/skills/monoco_atom_core}/SKILL.md +3 -1
  58. monoco/features/agent/resources/{skills/flow_engineer → zh/skills/monoco_workflow_agent_engineer}/SKILL.md +2 -2
  59. monoco/features/agent/resources/{skills/flow_manager → zh/skills/monoco_workflow_agent_manager}/SKILL.md +2 -2
  60. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_planner/SKILL.md +259 -0
  61. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_reviewer/SKILL.md +137 -0
  62. monoco/features/agent/session.py +59 -11
  63. monoco/features/agent/worker.py +38 -2
  64. monoco/features/artifact/__init__.py +0 -0
  65. monoco/features/artifact/adapter.py +33 -0
  66. monoco/features/artifact/resources/zh/AGENTS.md +14 -0
  67. monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +278 -0
  68. monoco/features/glossary/__init__.py +0 -0
  69. monoco/features/glossary/adapter.py +42 -0
  70. monoco/features/glossary/config.py +5 -0
  71. monoco/features/glossary/resources/en/AGENTS.md +29 -0
  72. monoco/features/glossary/resources/en/skills/monoco_atom_glossary/SKILL.md +35 -0
  73. monoco/features/glossary/resources/zh/AGENTS.md +29 -0
  74. monoco/features/glossary/resources/zh/skills/monoco_atom_glossary/SKILL.md +35 -0
  75. monoco/features/hooks/__init__.py +11 -0
  76. monoco/features/hooks/adapter.py +67 -0
  77. monoco/features/hooks/commands.py +309 -0
  78. monoco/features/hooks/core.py +441 -0
  79. monoco/features/hooks/resources/ADDING_HOOKS.md +234 -0
  80. monoco/features/i18n/adapter.py +18 -5
  81. monoco/features/i18n/core.py +482 -17
  82. monoco/features/i18n/resources/en/{SKILL.md → skills/monoco_atom_i18n/SKILL.md} +3 -1
  83. monoco/features/i18n/resources/en/skills/monoco_workflow_i18n_scan/SKILL.md +105 -0
  84. monoco/features/i18n/resources/zh/{SKILL.md → skills/monoco_atom_i18n/SKILL.md} +3 -1
  85. monoco/features/i18n/resources/{skills/i18n_scan_workflow → zh/skills/monoco_workflow_i18n_scan}/SKILL.md +2 -2
  86. monoco/features/issue/adapter.py +19 -6
  87. monoco/features/issue/commands.py +281 -7
  88. monoco/features/issue/core.py +272 -19
  89. monoco/features/issue/engine/machine.py +118 -5
  90. monoco/features/issue/linter.py +60 -5
  91. monoco/features/issue/models.py +3 -2
  92. monoco/features/issue/resources/en/AGENTS.md +109 -0
  93. monoco/features/issue/resources/en/{SKILL.md → skills/monoco_atom_issue/SKILL.md} +3 -1
  94. monoco/features/issue/resources/en/skills/monoco_workflow_issue_creation/SKILL.md +167 -0
  95. monoco/features/issue/resources/en/skills/monoco_workflow_issue_development/SKILL.md +224 -0
  96. monoco/features/issue/resources/en/skills/monoco_workflow_issue_management/SKILL.md +159 -0
  97. monoco/features/issue/resources/en/skills/monoco_workflow_issue_refinement/SKILL.md +203 -0
  98. monoco/features/issue/resources/hooks/post-checkout.sh +39 -0
  99. monoco/features/issue/resources/hooks/pre-commit.sh +41 -0
  100. monoco/features/issue/resources/hooks/pre-push.sh +35 -0
  101. monoco/features/issue/resources/zh/AGENTS.md +109 -0
  102. monoco/features/issue/resources/zh/{SKILL.md → skills/monoco_atom_issue_lifecycle/SKILL.md} +3 -1
  103. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_creation/SKILL.md +167 -0
  104. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_development/SKILL.md +224 -0
  105. monoco/features/issue/resources/{skills/issue_lifecycle_workflow → zh/skills/monoco_workflow_issue_management}/SKILL.md +2 -2
  106. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_refinement/SKILL.md +203 -0
  107. monoco/features/issue/validator.py +101 -1
  108. monoco/features/memo/adapter.py +21 -8
  109. monoco/features/memo/cli.py +103 -10
  110. monoco/features/memo/core.py +178 -92
  111. monoco/features/memo/models.py +53 -0
  112. monoco/features/memo/resources/en/skills/monoco_atom_memo/SKILL.md +77 -0
  113. monoco/features/memo/resources/en/skills/monoco_workflow_note_processing/SKILL.md +140 -0
  114. monoco/features/memo/resources/zh/{SKILL.md → skills/monoco_atom_memo/SKILL.md} +3 -1
  115. monoco/features/memo/resources/{skills/note_processing_workflow → zh/skills/monoco_workflow_note_processing}/SKILL.md +2 -2
  116. monoco/features/spike/adapter.py +18 -5
  117. monoco/features/spike/resources/en/{SKILL.md → skills/monoco_atom_spike/SKILL.md} +3 -1
  118. monoco/features/spike/resources/en/skills/monoco_workflow_research/SKILL.md +121 -0
  119. monoco/features/spike/resources/zh/{SKILL.md → skills/monoco_atom_spike/SKILL.md} +3 -1
  120. monoco/features/spike/resources/{skills/research_workflow → zh/skills/monoco_workflow_research}/SKILL.md +2 -2
  121. monoco/main.py +38 -1
  122. monoco_toolkit-0.3.11.dist-info/METADATA +130 -0
  123. monoco_toolkit-0.3.11.dist-info/RECORD +181 -0
  124. monoco/features/agent/reliability.py +0 -106
  125. monoco/features/agent/resources/skills/flow_reviewer/SKILL.md +0 -114
  126. monoco_toolkit-0.3.9.dist-info/METADATA +0 -127
  127. monoco_toolkit-0.3.9.dist-info/RECORD +0 -115
  128. /monoco/{core → features/agent}/resources/en/AGENTS.md +0 -0
  129. /monoco/{core → features/agent}/resources/zh/AGENTS.md +0 -0
  130. {monoco_toolkit-0.3.9.dist-info → monoco_toolkit-0.3.11.dist-info}/WHEEL +0 -0
  131. {monoco_toolkit-0.3.9.dist-info → monoco_toolkit-0.3.11.dist-info}/entry_points.txt +0 -0
  132. {monoco_toolkit-0.3.9.dist-info → monoco_toolkit-0.3.11.dist-info}/licenses/LICENSE +0 -0
monoco/core/loader.py ADDED
@@ -0,0 +1,633 @@
1
+ """
2
+ Unified Module Loader and Lifecycle Management for Monoco Features.
3
+
4
+ This module provides dynamic discovery, loading, and lifecycle management
5
+ for Monoco feature modules. It implements the FeatureModule protocol with
6
+ standard lifecycle hooks (mount/unmount) and supports dependency injection
7
+ and lazy loading for improved startup performance.
8
+ """
9
+
10
+ import importlib
11
+ import importlib.util
12
+ import inspect
13
+ import pkgutil
14
+ from abc import ABC, abstractmethod
15
+ from dataclasses import dataclass, field
16
+ from pathlib import Path
17
+ from typing import Any, Callable, Dict, List, Optional, Set, Type, TypeVar, Union
18
+
19
+ from monoco.core.feature import IntegrationData, MonocoFeature
20
+
21
+
22
+ T = TypeVar("T")
23
+
24
+
25
+ class LifecycleState:
26
+ """Lifecycle states for a feature module."""
27
+
28
+ UNLOADED = "unloaded"
29
+ LOADING = "loading"
30
+ LOADED = "loaded"
31
+ MOUNTING = "mounting"
32
+ MOUNTED = "mounted"
33
+ UNMOUNTING = "unmounting"
34
+ ERROR = "error"
35
+
36
+
37
+ @dataclass
38
+ class FeatureMetadata:
39
+ """Metadata for a feature module."""
40
+
41
+ name: str
42
+ version: str = "1.0.0"
43
+ description: str = ""
44
+ author: str = ""
45
+ dependencies: List[str] = field(default_factory=list)
46
+ optional_dependencies: List[str] = field(default_factory=list)
47
+ tags: List[str] = field(default_factory=list)
48
+ lazy: bool = False # Whether this feature supports lazy loading
49
+ priority: int = 100 # Lower values = higher priority for loading order
50
+
51
+
52
+ class FeatureModule(MonocoFeature, ABC):
53
+ """
54
+ Abstract base class for all Monoco feature modules.
55
+
56
+ Features must implement this protocol to participate in the unified
57
+ module loading system with full lifecycle management.
58
+
59
+ Lifecycle:
60
+ 1. Discovery: Feature is discovered in monoco/features/
61
+ 2. Load: Module is imported and class instantiated
62
+ 3. Mount: Feature is mounted (initialized) with workspace context
63
+ 4. Runtime: Feature is active and responding to commands
64
+ 5. Unmount: Feature is gracefully shut down
65
+
66
+ Example:
67
+ class MyFeature(FeatureModule):
68
+ @property
69
+ def metadata(self) -> FeatureMetadata:
70
+ return FeatureMetadata(
71
+ name="myfeature",
72
+ version="1.0.0",
73
+ dependencies=["core"]
74
+ )
75
+
76
+ def mount(self, context: FeatureContext) -> None:
77
+ # Initialize feature with workspace context
78
+ pass
79
+
80
+ def unmount(self) -> None:
81
+ # Cleanup resources
82
+ pass
83
+ """
84
+
85
+ _state: str = LifecycleState.UNLOADED
86
+ _context: Optional["FeatureContext"] = None
87
+ _instance_id: Optional[str] = None
88
+
89
+ @property
90
+ @abstractmethod
91
+ def metadata(self) -> FeatureMetadata:
92
+ """Return feature metadata including dependencies."""
93
+ pass
94
+
95
+ @property
96
+ def name(self) -> str:
97
+ """Unique identifier for the feature (from metadata)."""
98
+ return self.metadata.name
99
+
100
+ @property
101
+ def state(self) -> str:
102
+ """Current lifecycle state of the feature."""
103
+ return self._state
104
+
105
+ @property
106
+ def context(self) -> Optional["FeatureContext"]:
107
+ """The feature context if mounted."""
108
+ return self._context
109
+
110
+ def mount(self, context: "FeatureContext") -> None:
111
+ """
112
+ Lifecycle hook: Mount the feature with workspace context.
113
+
114
+ Called when the feature is being activated. The feature should
115
+ initialize any resources needed for operation.
116
+
117
+ Args:
118
+ context: The feature context containing workspace and config.
119
+
120
+ Raises:
121
+ FeatureMountError: If mounting fails.
122
+ """
123
+ self._context = context
124
+ self._state = LifecycleState.MOUNTING
125
+ try:
126
+ self._on_mount(context)
127
+ self._state = LifecycleState.MOUNTED
128
+ except Exception as e:
129
+ self._state = LifecycleState.ERROR
130
+ raise FeatureMountError(f"Failed to mount feature '{self.name}': {e}") from e
131
+
132
+ def unmount(self) -> None:
133
+ """
134
+ Lifecycle hook: Unmount the feature and cleanup resources.
135
+
136
+ Called when the feature is being deactivated or the application
137
+ is shutting down. The feature should release all resources.
138
+
139
+ Raises:
140
+ FeatureUnmountError: If unmounting fails.
141
+ """
142
+ self._state = LifecycleState.UNMOUNTING
143
+ try:
144
+ self._on_unmount()
145
+ self._state = LifecycleState.UNLOADED
146
+ except Exception as e:
147
+ self._state = LifecycleState.ERROR
148
+ raise FeatureUnmountError(f"Failed to unmount feature '{self.name}': {e}") from e
149
+ finally:
150
+ self._context = None
151
+
152
+ def _on_mount(self, context: "FeatureContext") -> None:
153
+ """
154
+ Override this method to implement mount logic.
155
+ Default implementation delegates to legacy initialize().
156
+ """
157
+ if hasattr(self, "initialize"):
158
+ # Legacy support: call initialize if defined
159
+ root = context.root if context else Path.cwd()
160
+ config = context.config if context else {}
161
+ self.initialize(root, config) # type: ignore
162
+
163
+ def _on_unmount(self) -> None:
164
+ """
165
+ Override this method to implement unmount logic.
166
+ """
167
+ pass
168
+
169
+ def initialize(self, root: Path, config: Dict) -> None:
170
+ """
171
+ Legacy lifecycle hook: Physical Structure Initialization.
172
+
173
+ New features should override _on_mount() instead.
174
+ """
175
+ pass
176
+
177
+ def integrate(self, root: Path, config: Dict) -> IntegrationData:
178
+ """
179
+ Lifecycle hook: Agent Environment Integration.
180
+
181
+ Called during sync to provide prompts and skills.
182
+ """
183
+ return IntegrationData()
184
+
185
+
186
+ @dataclass
187
+ class FeatureContext:
188
+ """
189
+ Context provided to features during mount/unmount operations.
190
+ """
191
+
192
+ root: Path
193
+ config: Dict[str, Any]
194
+ registry: "FeatureRegistry"
195
+ services: "ServiceContainer" = field(default_factory=lambda: ServiceContainer())
196
+
197
+ def get_service(self, interface: Type[T]) -> Optional[T]:
198
+ """Get a service from the service container."""
199
+ return self.services.get(interface)
200
+
201
+
202
+ class ServiceContainer:
203
+ """
204
+ Simple IoC container for dependency injection.
205
+
206
+ Supports registration of services by interface and implementation.
207
+ """
208
+
209
+ def __init__(self):
210
+ self._services: Dict[Type, Any] = {}
211
+ self._factories: Dict[Type, Callable[[], Any]] = {}
212
+
213
+ def register(self, interface: Type[T], implementation: T) -> None:
214
+ """Register a service instance for an interface."""
215
+ self._services[interface] = implementation
216
+
217
+ def register_factory(self, interface: Type[T], factory: Callable[[], T]) -> None:
218
+ """Register a factory function for lazy instantiation."""
219
+ self._factories[interface] = factory
220
+
221
+ def get(self, interface: Type[T]) -> Optional[T]:
222
+ """Get a service implementation by interface."""
223
+ if interface in self._services:
224
+ return self._services[interface]
225
+ if interface in self._factories:
226
+ instance = self._factories[interface]()
227
+ self._services[interface] = instance
228
+ return instance
229
+ return None
230
+
231
+ def has(self, interface: Type) -> bool:
232
+ """Check if a service is registered."""
233
+ return interface in self._services or interface in self._factories
234
+
235
+
236
+ class FeatureError(Exception):
237
+ """Base exception for feature-related errors."""
238
+ pass
239
+
240
+
241
+ class FeatureMountError(FeatureError):
242
+ """Raised when feature mounting fails."""
243
+ pass
244
+
245
+
246
+ class FeatureUnmountError(FeatureError):
247
+ """Raised when feature unmounting fails."""
248
+ pass
249
+
250
+
251
+ class FeatureLoadError(FeatureError):
252
+ """Raised when feature loading fails."""
253
+ pass
254
+
255
+
256
+ class FeatureDependencyError(FeatureError):
257
+ """Raised when feature dependencies cannot be resolved."""
258
+ pass
259
+
260
+
261
+ class FeatureRegistry:
262
+ """
263
+ Registry for managing feature modules.
264
+
265
+ Maintains a collection of registered features and provides
266
+ lifecycle management operations.
267
+ """
268
+
269
+ def __init__(self):
270
+ self._features: Dict[str, FeatureModule] = {}
271
+ self._states: Dict[str, str] = {}
272
+ self._context: Optional[FeatureContext] = None
273
+
274
+ def register(self, feature: FeatureModule) -> None:
275
+ """Register a feature instance."""
276
+ self._features[feature.name] = feature
277
+ self._states[feature.name] = feature.state
278
+
279
+ def unregister(self, name: str) -> None:
280
+ """Unregister a feature by name."""
281
+ if name in self._features:
282
+ del self._features[name]
283
+ del self._states[name]
284
+
285
+ def get(self, name: str) -> Optional[FeatureModule]:
286
+ """Get a feature by name."""
287
+ return self._features.get(name)
288
+
289
+ def get_all(self) -> List[FeatureModule]:
290
+ """Get all registered features."""
291
+ return list(self._features.values())
292
+
293
+ def get_mounted(self) -> List[FeatureModule]:
294
+ """Get all mounted features."""
295
+ return [f for f in self._features.values() if f.state == LifecycleState.MOUNTED]
296
+
297
+ def is_registered(self, name: str) -> bool:
298
+ """Check if a feature is registered."""
299
+ return name in self._features
300
+
301
+ def is_mounted(self, name: str) -> bool:
302
+ """Check if a feature is mounted."""
303
+ return self._states.get(name) == LifecycleState.MOUNTED
304
+
305
+ def mount_all(self, context: FeatureContext) -> Dict[str, Exception]:
306
+ """
307
+ Mount all registered features.
308
+
309
+ Returns a dictionary of errors keyed by feature name.
310
+ """
311
+ self._context = context
312
+ errors = {}
313
+
314
+ # Sort features by priority (lower = higher priority)
315
+ sorted_features = sorted(
316
+ self._features.values(),
317
+ key=lambda f: f.metadata.priority if hasattr(f, "metadata") else 100
318
+ )
319
+
320
+ for feature in sorted_features:
321
+ try:
322
+ feature.mount(context)
323
+ self._states[feature.name] = LifecycleState.MOUNTED
324
+ except Exception as e:
325
+ errors[feature.name] = e
326
+ self._states[feature.name] = LifecycleState.ERROR
327
+
328
+ return errors
329
+
330
+ def unmount_all(self) -> Dict[str, Exception]:
331
+ """
332
+ Unmount all mounted features.
333
+
334
+ Returns a dictionary of errors keyed by feature name.
335
+ """
336
+ errors = {}
337
+
338
+ # Unmount in reverse priority order
339
+ sorted_features = sorted(
340
+ self._features.values(),
341
+ key=lambda f: f.metadata.priority if hasattr(f, "metadata") else 100,
342
+ reverse=True
343
+ )
344
+
345
+ for feature in sorted_features:
346
+ if feature.state == LifecycleState.MOUNTED:
347
+ try:
348
+ feature.unmount()
349
+ self._states[feature.name] = LifecycleState.UNLOADED
350
+ except Exception as e:
351
+ errors[feature.name] = e
352
+
353
+ self._context = None
354
+ return errors
355
+
356
+
357
+ class FeatureLoader:
358
+ """
359
+ Dynamic feature loader with discovery and lifecycle management.
360
+
361
+ Discovers features in the monoco.features package and provides
362
+ lazy loading capabilities for improved startup performance.
363
+ """
364
+
365
+ FEATURES_PACKAGE = "monoco.features"
366
+ ADAPTER_MODULE = "adapter"
367
+ FEATURE_CLASS_NAME = "Feature"
368
+
369
+ def __init__(
370
+ self,
371
+ registry: Optional[FeatureRegistry] = None,
372
+ service_container: Optional[ServiceContainer] = None,
373
+ ):
374
+ self.registry = registry or FeatureRegistry()
375
+ self.services = service_container or ServiceContainer()
376
+ self._discovered: Dict[str, Type[FeatureModule]] = {}
377
+ self._loaded: Dict[str, FeatureModule] = {}
378
+ self._lazy_queue: Set[str] = set()
379
+ self._load_hooks: List[Callable[[FeatureModule], None]] = []
380
+ self._mount_hooks: List[Callable[[FeatureModule], None]] = []
381
+
382
+ def add_load_hook(self, hook: Callable[[FeatureModule], None]) -> None:
383
+ """Add a hook to be called after a feature is loaded."""
384
+ self._load_hooks.append(hook)
385
+
386
+ def add_mount_hook(self, hook: Callable[[FeatureModule], None]) -> None:
387
+ """Add a hook to be called after a feature is mounted."""
388
+ self._mount_hooks.append(hook)
389
+
390
+ def discover(self, package: Optional[str] = None) -> List[str]:
391
+ """
392
+ Discover available features in the features package.
393
+
394
+ Scans subpackages of monoco.features for adapter modules
395
+ containing FeatureModule implementations.
396
+
397
+ Args:
398
+ package: Package to scan (defaults to monoco.features).
399
+
400
+ Returns:
401
+ List of discovered feature names.
402
+ """
403
+ package = package or self.FEATURES_PACKAGE
404
+ discovered = []
405
+
406
+ try:
407
+ import monoco.features as features_pkg
408
+
409
+ package_path = Path(features_pkg.__file__).parent
410
+
411
+ for item in package_path.iterdir():
412
+ if not item.is_dir() or item.name.startswith("_"):
413
+ continue
414
+
415
+ feature_name = item.name
416
+ adapter_path = item / f"{self.ADAPTER_MODULE}.py"
417
+
418
+ if adapter_path.exists():
419
+ self._discovered[feature_name] = None # Mark as discovered, not loaded
420
+ discovered.append(feature_name)
421
+
422
+ except ImportError:
423
+ pass
424
+
425
+ return discovered
426
+
427
+ def load(self, name: str, lazy: bool = False) -> Optional[FeatureModule]:
428
+ """
429
+ Load a feature by name.
430
+
431
+ Args:
432
+ name: Feature name (subpackage name in monoco.features).
433
+ lazy: If True, defer actual instantiation until mount.
434
+
435
+ Returns:
436
+ Loaded FeatureModule instance or None if loading failed.
437
+
438
+ Raises:
439
+ FeatureLoadError: If the feature cannot be loaded.
440
+ """
441
+ if name in self._loaded:
442
+ return self._loaded[name]
443
+
444
+ if lazy:
445
+ self._lazy_queue.add(name)
446
+ return None
447
+
448
+ try:
449
+ module_path = f"{self.FEATURES_PACKAGE}.{name}.{self.ADAPTER_MODULE}"
450
+ module = importlib.import_module(module_path)
451
+
452
+ # Find FeatureModule subclass in the module
453
+ feature_class = None
454
+ for attr_name in dir(module):
455
+ attr = getattr(module, attr_name)
456
+ if (
457
+ inspect.isclass(attr)
458
+ and issubclass(attr, FeatureModule)
459
+ and attr is not FeatureModule
460
+ ):
461
+ feature_class = attr
462
+ break
463
+
464
+ # Fallback: Check for legacy MonocoFeature implementations
465
+ if feature_class is None:
466
+ for attr_name in dir(module):
467
+ attr = getattr(module, attr_name)
468
+ if (
469
+ inspect.isclass(attr)
470
+ and issubclass(attr, MonocoFeature)
471
+ and attr is not MonocoFeature
472
+ and attr is not FeatureModule
473
+ ):
474
+ # Wrap legacy feature in adapter
475
+ feature_class = self._wrap_legacy_feature(attr)
476
+ break
477
+
478
+ if feature_class is None:
479
+ raise FeatureLoadError(f"No FeatureModule found in {module_path}")
480
+
481
+ # Instantiate the feature
482
+ instance = feature_class()
483
+ instance._state = LifecycleState.LOADED
484
+ self._loaded[name] = instance
485
+ self.registry.register(instance)
486
+
487
+ # Call load hooks
488
+ for hook in self._load_hooks:
489
+ hook(instance)
490
+
491
+ return instance
492
+
493
+ except ImportError as e:
494
+ raise FeatureLoadError(f"Failed to import feature '{name}': {e}") from e
495
+ except Exception as e:
496
+ raise FeatureLoadError(f"Failed to load feature '{name}': {e}") from e
497
+
498
+ def load_all(self, lazy: bool = False) -> Dict[str, Optional[FeatureModule]]:
499
+ """
500
+ Load all discovered features.
501
+
502
+ Args:
503
+ lazy: If True, defer non-critical feature loading.
504
+
505
+ Returns:
506
+ Dictionary mapping feature names to instances (or None for lazy).
507
+ """
508
+ if not self._discovered:
509
+ self.discover()
510
+
511
+ results = {}
512
+ for name in self._discovered:
513
+ # Check if feature supports lazy loading
514
+ should_lazy = lazy and self._is_lazy_feature(name)
515
+ try:
516
+ results[name] = self.load(name, lazy=should_lazy)
517
+ except FeatureLoadError as e:
518
+ results[name] = None
519
+ # Log error but continue loading other features
520
+ print(f"Warning: {e}")
521
+
522
+ return results
523
+
524
+ def mount(self, name: str, context: FeatureContext) -> None:
525
+ """
526
+ Mount a specific feature.
527
+
528
+ Args:
529
+ name: Feature name.
530
+ context: Feature context for mounting.
531
+
532
+ Raises:
533
+ FeatureError: If the feature is not loaded or cannot be mounted.
534
+ """
535
+ if name in self._lazy_queue:
536
+ # Load now if it was deferred
537
+ self._lazy_queue.discard(name)
538
+ self.load(name, lazy=False)
539
+
540
+ feature = self.registry.get(name)
541
+ if feature is None:
542
+ raise FeatureError(f"Feature '{name}' is not loaded")
543
+
544
+ feature.mount(context)
545
+
546
+ for hook in self._mount_hooks:
547
+ hook(feature)
548
+
549
+ def mount_all(self, context: FeatureContext) -> Dict[str, Exception]:
550
+ """
551
+ Mount all loaded features.
552
+
553
+ Args:
554
+ context: Feature context for mounting.
555
+
556
+ Returns:
557
+ Dictionary of errors keyed by feature name.
558
+ """
559
+ # Load any remaining lazy features first
560
+ for name in list(self._lazy_queue):
561
+ self.load(name, lazy=False)
562
+ self._lazy_queue.clear()
563
+
564
+ return self.registry.mount_all(context)
565
+
566
+ def unmount(self, name: str) -> None:
567
+ """Unmount a specific feature."""
568
+ feature = self.registry.get(name)
569
+ if feature:
570
+ feature.unmount()
571
+
572
+ def unmount_all(self) -> Dict[str, Exception]:
573
+ """Unmount all mounted features."""
574
+ return self.registry.unmount_all()
575
+
576
+ def _is_lazy_feature(self, name: str) -> bool:
577
+ """Check if a feature supports lazy loading."""
578
+ # For now, use a simple heuristic based on known non-critical features
579
+ # In the future, this could be determined from metadata
580
+ lazy_features = {"glossary", "i18n"}
581
+ return name in lazy_features
582
+
583
+ def _wrap_legacy_feature(self, legacy_class: Type[MonocoFeature]) -> Type[FeatureModule]:
584
+ """
585
+ Wrap a legacy MonocoFeature class to support the new FeatureModule protocol.
586
+
587
+ This allows gradual migration of existing features.
588
+ """
589
+
590
+ class LegacyFeatureAdapter(FeatureModule):
591
+ def __init__(self):
592
+ self._legacy = legacy_class()
593
+ self._state = LifecycleState.UNLOADED
594
+
595
+ @property
596
+ def metadata(self) -> FeatureMetadata:
597
+ return FeatureMetadata(
598
+ name=self._legacy.name,
599
+ version="1.0.0",
600
+ description=f"Legacy feature: {self._legacy.name}",
601
+ )
602
+
603
+ @property
604
+ def name(self) -> str:
605
+ return self._legacy.name
606
+
607
+ def _on_mount(self, context: FeatureContext) -> None:
608
+ self._legacy.initialize(context.root, context.config)
609
+
610
+ def integrate(self, root: Path, config: Dict) -> IntegrationData:
611
+ return self._legacy.integrate(root, config)
612
+
613
+ # Preserve the original class name
614
+ LegacyFeatureAdapter.__name__ = f"{legacy_class.__name__}Adapter"
615
+ return LegacyFeatureAdapter
616
+
617
+
618
+ # Global loader instance for convenience
619
+ _default_loader: Optional[FeatureLoader] = None
620
+
621
+
622
+ def get_loader() -> FeatureLoader:
623
+ """Get the default feature loader instance."""
624
+ global _default_loader
625
+ if _default_loader is None:
626
+ _default_loader = FeatureLoader()
627
+ return _default_loader
628
+
629
+
630
+ def set_loader(loader: FeatureLoader) -> None:
631
+ """Set the default feature loader instance."""
632
+ global _default_loader
633
+ _default_loader = loader
monoco/core/output.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  import json
3
3
  import typer
4
- from typing import Any, List, Union, Annotated
4
+ from typing import Any, List, Union, Annotated, Optional
5
5
  from pydantic import BaseModel
6
6
  from rich.console import Console
7
7
  from rich.table import Table
@@ -41,7 +41,7 @@ class OutputManager:
41
41
 
42
42
  @staticmethod
43
43
  def print(
44
- data: Union[BaseModel, List[BaseModel], dict, list, str], title: str = ""
44
+ data: Union[BaseModel, List[BaseModel], dict, list, str], title: str = "", style: Optional[str] = None
45
45
  ):
46
46
  """
47
47
  Dual frontend dispatcher.
@@ -49,7 +49,7 @@ class OutputManager:
49
49
  if OutputManager.is_agent_mode():
50
50
  OutputManager._render_agent(data)
51
51
  else:
52
- OutputManager._render_human(data, title)
52
+ OutputManager._render_human(data, title, style=style)
53
53
 
54
54
  @staticmethod
55
55
  def error(message: str):
@@ -94,7 +94,7 @@ class OutputManager:
94
94
  print(str(data))
95
95
 
96
96
  @staticmethod
97
- def _render_human(data: Any, title: str):
97
+ def _render_human(data: Any, title: str, style: Optional[str] = None):
98
98
  """
99
99
  Human channel: Visual priority.
100
100
  """
@@ -104,7 +104,7 @@ class OutputManager:
104
104
  console.rule(f"[bold blue]{title}[/bold blue]")
105
105
 
106
106
  if isinstance(data, str):
107
- console.print(data)
107
+ console.print(data, style=style)
108
108
  return
109
109
 
110
110
  # Special handling for Lists of Pydantic Models -> Table