monoco-toolkit 0.3.10__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 (100) 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 +31 -4
  6. monoco/core/git.py +23 -0
  7. monoco/core/ingestion/__init__.py +20 -0
  8. monoco/core/ingestion/discovery.py +248 -0
  9. monoco/core/ingestion/watcher.py +343 -0
  10. monoco/core/ingestion/worker.py +436 -0
  11. monoco/core/loader.py +633 -0
  12. monoco/core/registry.py +34 -25
  13. monoco/core/skills.py +119 -80
  14. monoco/daemon/app.py +77 -1
  15. monoco/daemon/commands.py +10 -0
  16. monoco/daemon/mailroom_service.py +196 -0
  17. monoco/daemon/models.py +1 -0
  18. monoco/daemon/scheduler.py +236 -0
  19. monoco/daemon/services.py +185 -0
  20. monoco/daemon/triggers.py +55 -0
  21. monoco/features/agent/adapter.py +17 -7
  22. monoco/features/agent/apoptosis.py +4 -4
  23. monoco/features/agent/manager.py +41 -5
  24. monoco/{core/resources/en/skills/monoco_core → features/agent/resources/en/skills/monoco_atom_core}/SKILL.md +2 -2
  25. monoco/features/agent/resources/en/skills/{flow_engineer → monoco_workflow_agent_engineer}/SKILL.md +2 -2
  26. monoco/features/agent/resources/en/skills/{flow_manager → monoco_workflow_agent_manager}/SKILL.md +2 -2
  27. monoco/features/agent/resources/en/skills/{flow_planner → monoco_workflow_agent_planner}/SKILL.md +2 -2
  28. monoco/features/agent/resources/en/skills/{flow_reviewer → monoco_workflow_agent_reviewer}/SKILL.md +2 -2
  29. monoco/features/agent/resources/{roles/role-engineer.yaml → zh/roles/monoco_role_engineer.yaml} +3 -3
  30. monoco/features/agent/resources/{roles/role-manager.yaml → zh/roles/monoco_role_manager.yaml} +8 -8
  31. monoco/features/agent/resources/{roles/role-planner.yaml → zh/roles/monoco_role_planner.yaml} +8 -8
  32. monoco/features/agent/resources/{roles/role-reviewer.yaml → zh/roles/monoco_role_reviewer.yaml} +8 -8
  33. monoco/{core/resources/zh/skills/monoco_core → features/agent/resources/zh/skills/monoco_atom_core}/SKILL.md +2 -2
  34. monoco/features/agent/resources/zh/skills/{flow_engineer → monoco_workflow_agent_engineer}/SKILL.md +2 -2
  35. monoco/features/agent/resources/zh/skills/{flow_manager → monoco_workflow_agent_manager}/SKILL.md +2 -2
  36. monoco/features/agent/resources/zh/skills/{flow_planner → monoco_workflow_agent_planner}/SKILL.md +2 -2
  37. monoco/features/agent/resources/zh/skills/{flow_reviewer → monoco_workflow_agent_reviewer}/SKILL.md +2 -2
  38. monoco/features/agent/session.py +59 -11
  39. monoco/features/artifact/__init__.py +0 -0
  40. monoco/features/artifact/adapter.py +33 -0
  41. monoco/features/artifact/resources/zh/AGENTS.md +14 -0
  42. monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +278 -0
  43. monoco/features/glossary/adapter.py +18 -7
  44. monoco/features/glossary/resources/en/skills/{monoco_glossary → monoco_atom_glossary}/SKILL.md +2 -2
  45. monoco/features/glossary/resources/zh/skills/{monoco_glossary → monoco_atom_glossary}/SKILL.md +2 -2
  46. monoco/features/hooks/__init__.py +11 -0
  47. monoco/features/hooks/adapter.py +67 -0
  48. monoco/features/hooks/commands.py +309 -0
  49. monoco/features/hooks/core.py +441 -0
  50. monoco/features/hooks/resources/ADDING_HOOKS.md +234 -0
  51. monoco/features/i18n/adapter.py +18 -5
  52. monoco/features/i18n/core.py +482 -17
  53. monoco/features/i18n/resources/en/skills/{monoco_i18n → monoco_atom_i18n}/SKILL.md +2 -2
  54. monoco/features/i18n/resources/en/skills/{i18n_scan_workflow → monoco_workflow_i18n_scan}/SKILL.md +2 -2
  55. monoco/features/i18n/resources/zh/skills/{monoco_i18n → monoco_atom_i18n}/SKILL.md +2 -2
  56. monoco/features/i18n/resources/zh/skills/{i18n_scan_workflow → monoco_workflow_i18n_scan}/SKILL.md +2 -2
  57. monoco/features/issue/adapter.py +19 -6
  58. monoco/features/issue/commands.py +281 -7
  59. monoco/features/issue/core.py +227 -13
  60. monoco/features/issue/engine/machine.py +114 -4
  61. monoco/features/issue/linter.py +60 -5
  62. monoco/features/issue/models.py +2 -2
  63. monoco/features/issue/resources/en/AGENTS.md +109 -0
  64. monoco/features/issue/resources/en/skills/{monoco_issue → monoco_atom_issue}/SKILL.md +2 -2
  65. monoco/features/issue/resources/en/skills/{issue_create_workflow → monoco_workflow_issue_creation}/SKILL.md +2 -2
  66. monoco/features/issue/resources/en/skills/{issue_develop_workflow → monoco_workflow_issue_development}/SKILL.md +2 -2
  67. monoco/features/issue/resources/en/skills/{issue_lifecycle_workflow → monoco_workflow_issue_management}/SKILL.md +2 -2
  68. monoco/features/issue/resources/en/skills/{issue_refine_workflow → monoco_workflow_issue_refinement}/SKILL.md +2 -2
  69. monoco/features/issue/resources/hooks/post-checkout.sh +39 -0
  70. monoco/features/issue/resources/hooks/pre-commit.sh +41 -0
  71. monoco/features/issue/resources/hooks/pre-push.sh +35 -0
  72. monoco/features/issue/resources/zh/AGENTS.md +109 -0
  73. monoco/features/issue/resources/zh/skills/{monoco_issue → monoco_atom_issue_lifecycle}/SKILL.md +2 -2
  74. monoco/features/issue/resources/zh/skills/{issue_create_workflow → monoco_workflow_issue_creation}/SKILL.md +2 -2
  75. monoco/features/issue/resources/zh/skills/{issue_develop_workflow → monoco_workflow_issue_development}/SKILL.md +2 -2
  76. monoco/features/issue/resources/zh/skills/{issue_lifecycle_workflow → monoco_workflow_issue_management}/SKILL.md +2 -2
  77. monoco/features/issue/resources/zh/skills/{issue_refine_workflow → monoco_workflow_issue_refinement}/SKILL.md +2 -2
  78. monoco/features/issue/validator.py +101 -1
  79. monoco/features/memo/adapter.py +21 -8
  80. monoco/features/memo/cli.py +103 -10
  81. monoco/features/memo/core.py +178 -92
  82. monoco/features/memo/models.py +53 -0
  83. monoco/features/memo/resources/en/skills/{monoco_memo → monoco_atom_memo}/SKILL.md +2 -2
  84. monoco/features/memo/resources/en/skills/{note_processing_workflow → monoco_workflow_note_processing}/SKILL.md +2 -2
  85. monoco/features/memo/resources/zh/skills/{monoco_memo → monoco_atom_memo}/SKILL.md +2 -2
  86. monoco/features/memo/resources/zh/skills/{note_processing_workflow → monoco_workflow_note_processing}/SKILL.md +2 -2
  87. monoco/features/spike/adapter.py +18 -5
  88. monoco/features/spike/resources/en/skills/{monoco_spike → monoco_atom_spike}/SKILL.md +2 -2
  89. monoco/features/spike/resources/en/skills/{research_workflow → monoco_workflow_research}/SKILL.md +2 -2
  90. monoco/features/spike/resources/zh/skills/{monoco_spike → monoco_atom_spike}/SKILL.md +2 -2
  91. monoco/features/spike/resources/zh/skills/{research_workflow → monoco_workflow_research}/SKILL.md +2 -2
  92. monoco/main.py +38 -1
  93. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.11.dist-info}/METADATA +7 -1
  94. monoco_toolkit-0.3.11.dist-info/RECORD +181 -0
  95. monoco_toolkit-0.3.10.dist-info/RECORD +0 -156
  96. /monoco/{core → features/agent}/resources/en/AGENTS.md +0 -0
  97. /monoco/{core → features/agent}/resources/zh/AGENTS.md +0 -0
  98. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.11.dist-info}/WHEEL +0 -0
  99. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.11.dist-info}/entry_points.txt +0 -0
  100. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.11.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,441 @@
1
+ """
2
+ Core logic for Git Hooks management.
3
+
4
+ Implements the distributed hooks + aggregator pattern:
5
+ - Each Feature stores its hooks in resources/hooks/{hook-type}.sh
6
+ - This module discovers, sorts by priority, and generates final hooks
7
+ """
8
+
9
+ import os
10
+ import stat
11
+ from dataclasses import dataclass, field
12
+ from enum import Enum
13
+ from pathlib import Path
14
+ from typing import Dict, List, Optional, Tuple
15
+
16
+ from rich.console import Console
17
+
18
+ console = Console()
19
+
20
+
21
+ class HookType(str, Enum):
22
+ """Supported git hook types."""
23
+
24
+ PRE_COMMIT = "pre-commit"
25
+ PRE_PUSH = "pre-push"
26
+ POST_CHECKOUT = "post-checkout"
27
+ PRE_REBASE = "pre-rebase"
28
+ COMMIT_MSG = "commit-msg"
29
+
30
+
31
+ @dataclass
32
+ class HookDeclaration:
33
+ """
34
+ Metadata for a hook script contributed by a Feature.
35
+
36
+ Attributes:
37
+ hook_type: Type of git hook (pre-commit, pre-push, etc.)
38
+ script_path: Path to the hook script file
39
+ feature_name: Name of the contributing Feature
40
+ priority: Execution priority (lower = earlier, default 100)
41
+ """
42
+
43
+ hook_type: HookType
44
+ script_path: Path
45
+ feature_name: str
46
+ priority: int = 100
47
+
48
+
49
+ @dataclass
50
+ class HookConfig:
51
+ """Configuration for hooks feature."""
52
+
53
+ enabled: bool = True
54
+ enabled_features: Dict[str, bool] = field(default_factory=dict)
55
+ # Global hook enable/disable by type
56
+ enabled_hooks: Dict[str, bool] = field(default_factory=lambda: {
57
+ "pre-commit": True,
58
+ "pre-push": False, # Disabled by default
59
+ "post-checkout": False,
60
+ })
61
+
62
+
63
+ class GitHooksManager:
64
+ """
65
+ Manages discovery, aggregation, and installation of git hooks.
66
+
67
+ This class implements the aggregator pattern in the distributed hooks architecture:
68
+ 1. Discovers hooks from all Features in resources/hooks/
69
+ 2. Sorts them by priority
70
+ 3. Generates combined hook scripts
71
+ 4. Installs them to .git/hooks/
72
+ """
73
+
74
+ # Header marker to identify Monoco-managed hooks
75
+ MONOCO_MARKER = "# Monoco Managed Hook - Auto-generated. Do not edit manually."
76
+
77
+ def __init__(self, project_root: Path, config: Optional[HookConfig] = None):
78
+ """
79
+ Initialize the hooks manager.
80
+
81
+ Args:
82
+ project_root: Root directory of the project (contains .git/)
83
+ config: Optional hooks configuration
84
+ """
85
+ self.project_root = project_root
86
+ self.config = config or HookConfig()
87
+ self.git_dir = project_root / ".git"
88
+ self.hooks_dir = self.git_dir / "hooks"
89
+
90
+ def is_git_repo(self) -> bool:
91
+ """Check if project root is a git repository."""
92
+ return self.git_dir.exists() and self.git_dir.is_dir()
93
+
94
+ def collect_hooks(
95
+ self, features_dir: Optional[Path] = None
96
+ ) -> Dict[HookType, List[HookDeclaration]]:
97
+ """
98
+ Discover all hook scripts from Features.
99
+
100
+ Scans features/{feature}/resources/hooks/ for hook scripts.
101
+
102
+ Args:
103
+ features_dir: Root directory containing features (defaults to monoco/features/)
104
+
105
+ Returns:
106
+ Dictionary mapping hook types to sorted list of hook declarations
107
+ """
108
+ if features_dir is None:
109
+ # Default to monoco/features/ relative to this file
110
+ features_dir = Path(__file__).parent.parent
111
+
112
+ hooks_by_type: Dict[HookType, List[HookDeclaration]] = {
113
+ hook_type: [] for hook_type in HookType
114
+ }
115
+
116
+ if not features_dir.exists():
117
+ return hooks_by_type
118
+
119
+ for feature_dir in features_dir.iterdir():
120
+ if not feature_dir.is_dir() or feature_dir.name.startswith("_"):
121
+ continue
122
+
123
+ feature_name = feature_dir.name
124
+
125
+ # Check if this feature is enabled in config
126
+ if self.config.enabled_features.get(feature_name, True) is False:
127
+ continue
128
+
129
+ hooks_dir = feature_dir / "resources" / "hooks"
130
+ if not hooks_dir.exists():
131
+ continue
132
+
133
+ # Discover hook scripts
134
+ for hook_script in hooks_dir.iterdir():
135
+ if not hook_script.is_file():
136
+ continue
137
+
138
+ # Parse hook type from filename (e.g., pre-commit.sh -> pre-commit)
139
+ hook_type = self._parse_hook_type(hook_script.name)
140
+ if hook_type is None:
141
+ continue
142
+
143
+ # Check if this hook type is globally enabled
144
+ if not self.config.enabled_hooks.get(hook_type.value, True):
145
+ continue
146
+
147
+ # Get feature priority from adapter if available
148
+ priority = self._get_feature_priority(features_dir, feature_name)
149
+
150
+ declaration = HookDeclaration(
151
+ hook_type=hook_type,
152
+ script_path=hook_script,
153
+ feature_name=feature_name,
154
+ priority=priority,
155
+ )
156
+ hooks_by_type[hook_type].append(declaration)
157
+
158
+ # Sort each list by priority
159
+ for hook_type in hooks_by_type:
160
+ hooks_by_type[hook_type].sort(key=lambda h: h.priority)
161
+
162
+ return hooks_by_type
163
+
164
+ def _parse_hook_type(self, filename: str) -> Optional[HookType]:
165
+ """Parse hook type from filename (e.g., 'pre-commit.sh' -> HookType.PRE_COMMIT)."""
166
+ # Remove extension
167
+ name = filename
168
+ for ext in [".sh", ".py", ".bash"]:
169
+ if name.endswith(ext):
170
+ name = name[: -len(ext)]
171
+ break
172
+
173
+ try:
174
+ return HookType(name)
175
+ except ValueError:
176
+ return None
177
+
178
+ def _get_feature_priority(self, features_dir: Path, feature_name: str) -> int:
179
+ """Get feature priority from its adapter metadata."""
180
+ adapter_path = features_dir / feature_name / "adapter.py"
181
+ if not adapter_path.exists():
182
+ return 100
183
+
184
+ try:
185
+ # Import the adapter module
186
+ import importlib.util
187
+
188
+ spec = importlib.util.spec_from_file_location(
189
+ f"monoco.features.{feature_name}.adapter", adapter_path
190
+ )
191
+ if spec is None or spec.loader is None:
192
+ return 100
193
+
194
+ module = importlib.util.module_from_spec(spec)
195
+ spec.loader.exec_module(module)
196
+
197
+ # Look for Feature class with metadata
198
+ for attr_name in dir(module):
199
+ attr = getattr(module, attr_name)
200
+ if hasattr(attr, "metadata"):
201
+ metadata = attr.metadata
202
+ if hasattr(metadata, "priority"):
203
+ return metadata.priority
204
+ except Exception:
205
+ pass
206
+
207
+ return 100
208
+
209
+ def generate_hook_script(self, declarations: List[HookDeclaration]) -> str:
210
+ """
211
+ Generate a combined hook script from multiple declarations.
212
+
213
+ Args:
214
+ declarations: List of hook declarations to combine
215
+
216
+ Returns:
217
+ Generated shell script content
218
+ """
219
+ lines = [
220
+ "#!/bin/sh",
221
+ self.MONOCO_MARKER,
222
+ "# Generated by Monoco Toolkit",
223
+ "",
224
+ "# Store the original exit code",
225
+ "OVERALL_EXIT=0",
226
+ "",
227
+ ]
228
+
229
+ # Add virtual environment detection
230
+ lines.extend([
231
+ "# Detect virtual environment",
232
+ 'if [ -n "$VIRTUAL_ENV" ]; then',
233
+ ' PYTHON_CMD="$VIRTUAL_ENV/bin/python"',
234
+ 'elif [ -f "./.venv/bin/python" ]; then',
235
+ ' PYTHON_CMD="./.venv/bin/python"',
236
+ 'elif [ -f "./venv/bin/python" ]; then',
237
+ ' PYTHON_CMD="./venv/bin/python"',
238
+ 'else',
239
+ ' PYTHON_CMD="python3"',
240
+ 'fi',
241
+ '',
242
+ "# Detect monoco command",
243
+ 'MONOCO_CMD="$PYTHON_CMD -m monoco"',
244
+ "",
245
+ ])
246
+
247
+ for decl in declarations:
248
+ lines.extend([
249
+ f"# --- Hook from {decl.feature_name} (priority: {decl.priority}) ---",
250
+ f'echo "[Monoco] Running {decl.hook_type.value} hook: {decl.feature_name}"',
251
+ ])
252
+
253
+ # Read and include the hook script content
254
+ try:
255
+ content = decl.script_path.read_text(encoding="utf-8")
256
+ # Remove shebang if present since we're in a combined script
257
+ lines_content = content.splitlines()
258
+ if lines_content and lines_content[0].startswith("#!/"):
259
+ lines_content = lines_content[1:]
260
+
261
+ for line in lines_content:
262
+ lines.append(line)
263
+ except Exception as e:
264
+ lines.append(f'echo "[Monoco] Error reading hook: {e}" >&2')
265
+
266
+ lines.extend([
267
+ "# Capture exit code",
268
+ "HOOK_EXIT=$?",
269
+ 'if [ $HOOK_EXIT -ne 0 ]; then',
270
+ ' echo "[Monoco] Hook failed with exit code $HOOK_EXIT"',
271
+ " OVERALL_EXIT=$HOOK_EXIT",
272
+ "fi",
273
+ "",
274
+ ])
275
+
276
+ lines.extend([
277
+ "# Exit with the first non-zero exit code",
278
+ "exit $OVERALL_EXIT",
279
+ "",
280
+ ])
281
+
282
+ return "\n".join(lines)
283
+
284
+ def install(self, features_dir: Optional[Path] = None) -> Dict[HookType, bool]:
285
+ """
286
+ Install all discovered hooks to .git/hooks/.
287
+
288
+ Args:
289
+ features_dir: Root directory containing features
290
+
291
+ Returns:
292
+ Dictionary mapping hook types to success status
293
+ """
294
+ results = {}
295
+
296
+ if not self.is_git_repo():
297
+ console.print("[yellow]Warning: Not a git repository. Skipping hook installation.[/yellow]")
298
+ return results
299
+
300
+ self.hooks_dir.mkdir(exist_ok=True)
301
+
302
+ # Collect all hooks
303
+ hooks_by_type = self.collect_hooks(features_dir)
304
+
305
+ for hook_type, declarations in hooks_by_type.items():
306
+ if not declarations:
307
+ continue
308
+
309
+ # Check if hook type is enabled
310
+ if not self.config.enabled_hooks.get(hook_type.value, True):
311
+ continue
312
+
313
+ hook_path = self.hooks_dir / hook_type.value
314
+
315
+ # Check for existing non-monoco hook
316
+ if hook_path.exists():
317
+ try:
318
+ content = hook_path.read_text(encoding="utf-8")
319
+ if self.MONOCO_MARKER not in content:
320
+ console.print(
321
+ f"[yellow]Warning: {hook_type.value} already exists and is not managed by Monoco. Skipping.[/yellow]"
322
+ )
323
+ results[hook_type] = False
324
+ continue
325
+ except Exception as e:
326
+ console.print(
327
+ f"[yellow]Warning: Cannot read existing {hook_type.value}: {e}. Skipping.[/yellow]"
328
+ )
329
+ results[hook_type] = False
330
+ continue
331
+
332
+ # Generate and write hook script
333
+ script_content = self.generate_hook_script(declarations)
334
+
335
+ try:
336
+ hook_path.write_text(script_content, encoding="utf-8")
337
+ # Make executable
338
+ hook_path.chmod(hook_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
339
+ console.print(f"[green]✓ Installed {hook_type.value} hook ({len(declarations)} scripts)[/green]")
340
+ results[hook_type] = True
341
+ except Exception as e:
342
+ console.print(f"[red]✗ Failed to install {hook_type.value}: {e}[/red]")
343
+ results[hook_type] = False
344
+
345
+ return results
346
+
347
+ def uninstall(self) -> Dict[HookType, bool]:
348
+ """
349
+ Uninstall all Monoco-managed hooks from .git/hooks/.
350
+
351
+ Returns:
352
+ Dictionary mapping hook types to success status
353
+ """
354
+ results = {}
355
+
356
+ if not self.is_git_repo():
357
+ console.print("[yellow]Warning: Not a git repository.[/yellow]")
358
+ return results
359
+
360
+ for hook_type in HookType:
361
+ hook_path = self.hooks_dir / hook_type.value
362
+
363
+ if not hook_path.exists():
364
+ continue
365
+
366
+ try:
367
+ content = hook_path.read_text(encoding="utf-8")
368
+ if self.MONOCO_MARKER in content:
369
+ hook_path.unlink()
370
+ console.print(f"[green]✓ Removed {hook_type.value} hook[/green]")
371
+ results[hook_type] = True
372
+ else:
373
+ console.print(
374
+ f"[dim]Skipping {hook_type.value}: not managed by Monoco[/dim]"
375
+ )
376
+ results[hook_type] = False
377
+ except Exception as e:
378
+ console.print(f"[red]✗ Failed to remove {hook_type.value}: {e}[/red]")
379
+ results[hook_type] = False
380
+
381
+ return results
382
+
383
+ def get_status(self, features_dir: Optional[Path] = None) -> Dict[str, any]:
384
+ """
385
+ Get current hooks installation status.
386
+
387
+ Args:
388
+ features_dir: Root directory containing features
389
+
390
+ Returns:
391
+ Status dictionary with installed hooks and discovered scripts
392
+ """
393
+ status = {
394
+ "is_git_repo": self.is_git_repo(),
395
+ "hooks_dir": str(self.hooks_dir) if self.is_git_repo() else None,
396
+ "installed": {},
397
+ "discovered": {},
398
+ "config": {
399
+ "enabled": self.config.enabled,
400
+ "enabled_features": self.config.enabled_features,
401
+ "enabled_hooks": self.config.enabled_hooks,
402
+ },
403
+ }
404
+
405
+ if not self.is_git_repo():
406
+ return status
407
+
408
+ # Check installed hooks
409
+ for hook_type in HookType:
410
+ hook_path = self.hooks_dir / hook_type.value
411
+ if hook_path.exists():
412
+ try:
413
+ content = hook_path.read_text(encoding="utf-8")
414
+ is_monoco = self.MONOCO_MARKER in content
415
+ status["installed"][hook_type.value] = {
416
+ "exists": True,
417
+ "managed_by_monoco": is_monoco,
418
+ }
419
+ except Exception:
420
+ status["installed"][hook_type.value] = {
421
+ "exists": True,
422
+ "managed_by_monoco": False,
423
+ "error": "Cannot read file",
424
+ }
425
+ else:
426
+ status["installed"][hook_type.value] = {"exists": False}
427
+
428
+ # Discover available hooks
429
+ hooks_by_type = self.collect_hooks(features_dir)
430
+ for hook_type, declarations in hooks_by_type.items():
431
+ if declarations:
432
+ status["discovered"][hook_type.value] = [
433
+ {
434
+ "feature": d.feature_name,
435
+ "path": str(d.script_path),
436
+ "priority": d.priority,
437
+ }
438
+ for d in declarations
439
+ ]
440
+
441
+ return status
@@ -0,0 +1,234 @@
1
+ # 为 Feature 添加自定义 Git Hooks
2
+
3
+ 本文档介绍如何为 Monoco Feature 添加自定义 Git Hooks。
4
+
5
+ ## 概述
6
+
7
+ Monoco 使用**分布式 Hooks + 聚合器**架构:
8
+
9
+ 1. **分布式**:每个 Feature 在 `resources/hooks/` 目录存放自己的 hook 脚本
10
+ 2. **聚合器**:`features/hooks/` 负责收集、排序、生成最终 hook
11
+
12
+ ## 快速开始
13
+
14
+ ### 1. 创建 Hooks 目录
15
+
16
+ 在你的 Feature 目录下创建 hooks 目录:
17
+
18
+ ```
19
+ monoco/features/{your_feature}/
20
+ └── resources/
21
+ └── hooks/
22
+ ├── pre-commit.sh
23
+ ├── pre-push.sh
24
+ └── post-checkout.sh
25
+ ```
26
+
27
+ ### 2. 编写 Hook 脚本
28
+
29
+ 创建 shell 脚本文件,例如 `pre-commit.sh`:
30
+
31
+ ```bash
32
+ #!/bin/sh
33
+ # Your Feature Pre-Commit Hook
34
+ # Description of what this hook does
35
+
36
+ echo "[Monoco] Running your-feature pre-commit check..."
37
+
38
+ # Your logic here
39
+ # Use $MONOCO_CMD to call monoco commands
40
+ $MONOCO_CMD your-command
41
+
42
+ if [ $? -ne 0 ]; then
43
+ echo "[Monoco] Your check failed!"
44
+ exit 1
45
+ fi
46
+
47
+ echo "[Monoco] Your check passed."
48
+ exit 0
49
+ ```
50
+
51
+ ### 3. 可用的环境变量
52
+
53
+ 在 hook 脚本中,以下环境变量可用:
54
+
55
+ | 变量 | 说明 | 示例 |
56
+ |------|------|------|
57
+ | `$MONOCO_CMD` | Monoco 命令的完整调用路径 | `python -m monoco` 或 `/path/to/python -m monoco` |
58
+ | `$PYTHON_CMD` | Python 解释器路径 | `./.venv/bin/python` |
59
+
60
+ ### 4. 支持的 Hook 类型
61
+
62
+ | Hook 类型 | 触发时机 | 典型用途 |
63
+ |-----------|----------|----------|
64
+ | `pre-commit` | 执行 `git commit` 前 | 代码检查、Issue 验证 |
65
+ | `pre-push` | 执行 `git push` 前 | 关键 Issue 检查、测试运行 |
66
+ | `post-checkout` | 执行 `git checkout` 后 | 同步 Issue 状态、更新隔离环境 |
67
+ | `pre-rebase` | 执行 `git rebase` 前 | 防止在特定状态下 rebase |
68
+ | `commit-msg` | 提交信息编辑后 | 验证提交信息格式 |
69
+
70
+ ## 最佳实践
71
+
72
+ ### 保持脚本简洁
73
+
74
+ Hook 脚本应该快速执行,避免长时间阻塞开发者工作流:
75
+
76
+ ```bash
77
+ # ✅ 好的做法:快速检查
78
+ echo "[Monoco] Running quick validation..."
79
+ $MONOCO_CMD quick-check
80
+
81
+ # ❌ 避免:长时间运行的任务
82
+ # $MONOCO_CMD run-all-tests # 这可能需要几分钟
83
+ ```
84
+
85
+ ### 提供清晰的错误信息
86
+
87
+ 当检查失败时,提供可操作的错误信息:
88
+
89
+ ```bash
90
+ if [ $RESULT -ne 0 ]; then
91
+ echo ""
92
+ echo "[Monoco] ❌ Check failed!"
93
+ echo "[Monoco] Reason: Specific explanation of what went wrong"
94
+ echo "[Monoco] Fix: Suggest how to fix the issue"
95
+ exit 1
96
+ fi
97
+ ```
98
+
99
+ ### 处理没有变更的情况
100
+
101
+ 对于只检查特定文件类型的 hooks,优雅处理没有相关变更的情况:
102
+
103
+ ```bash
104
+ # 获取暂存的特定类型文件
105
+ STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.py$' || true)
106
+
107
+ if [ -z "$STAGED_FILES" ]; then
108
+ echo "[Monoco] No Python files staged. Skipping check."
109
+ exit 0
110
+ fi
111
+ ```
112
+
113
+ ### 设置优先级
114
+
115
+ 如果需要控制 hook 执行顺序,可以在 Feature 的 `adapter.py` 中设置优先级:
116
+
117
+ ```python
118
+ from monoco.core.loader import FeatureModule, FeatureMetadata
119
+
120
+ class YourFeature(FeatureModule):
121
+ @property
122
+ def metadata(self) -> FeatureMetadata:
123
+ return FeatureMetadata(
124
+ name="your_feature",
125
+ priority=50, # 数字越小优先级越高,默认 100
126
+ ...
127
+ )
128
+ ```
129
+
130
+ 执行顺序:按优先级升序(数字小的先执行)。
131
+
132
+ ## 示例
133
+
134
+ ### 示例 1:简单的代码风格检查
135
+
136
+ ```bash
137
+ #!/bin/sh
138
+ # Lint Feature Pre-Commit Hook
139
+
140
+ STAGED_PY=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$' || true)
141
+
142
+ if [ -z "$STAGED_PY" ]; then
143
+ exit 0
144
+ fi
145
+
146
+ echo "[Monoco] Running linter on staged Python files..."
147
+ $MONOCO_CMD lint --files $STAGED_PY
148
+ exit $?
149
+ ```
150
+
151
+ ### 示例 2:检查测试覆盖率
152
+
153
+ ```bash
154
+ #!/bin/sh
155
+ # Coverage Feature Pre-Push Hook
156
+
157
+ echo "[Monoco] Checking test coverage..."
158
+ $MONOCO_CMD coverage check --min 80
159
+
160
+ if [ $? -ne 0 ]; then
161
+ echo "[Monoco] ❌ Coverage check failed!"
162
+ echo "[Monoco] Run 'monoco coverage report' to see details."
163
+ exit 1
164
+ fi
165
+
166
+ echo "[Monoco] ✓ Coverage check passed."
167
+ exit 0
168
+ ```
169
+
170
+ ### 示例 3:分支切换后同步状态
171
+
172
+ ```bash
173
+ #!/bin/sh
174
+ # Sync Feature Post-Checkout Hook
175
+
176
+ BRANCH_SWITCH="$3"
177
+
178
+ if [ "$BRANCH_SWITCH" != "1" ]; then
179
+ exit 0
180
+ fi
181
+
182
+ echo "[Monoco] Syncing after branch switch..."
183
+ $MONOCO_CMD sync
184
+ exit 0
185
+ ```
186
+
187
+ ## 调试 Hooks
188
+
189
+ ### 查看生成的 Hook
190
+
191
+ ```bash
192
+ # 查看当前安装的 pre-commit hook
193
+ cat .git/hooks/pre-commit
194
+ ```
195
+
196
+ ### 手动运行 Hook
197
+
198
+ ```bash
199
+ # 手动运行 pre-commit hook
200
+ sh .git/hooks/pre-commit
201
+ ```
202
+
203
+ ### 临时禁用 Hooks
204
+
205
+ ```bash
206
+ # 跳过 pre-commit hook
207
+ git commit --no-verify -m "Your message"
208
+
209
+ # 跳过 pre-push hook
210
+ git push --no-verify
211
+ ```
212
+
213
+ ## 故障排除
214
+
215
+ ### Hook 没有执行
216
+
217
+ 1. 检查 hook 是否已安装:`monoco hooks status`
218
+ 2. 检查 hook 文件权限:`ls -la .git/hooks/`
219
+ 3. 检查 Feature 是否启用:`monoco config get hooks.features.your_feature`
220
+
221
+ ### Hook 执行失败
222
+
223
+ 1. 检查 `$MONOCO_CMD` 是否可用
224
+ 2. 查看详细的错误输出
225
+ 3. 手动运行命令测试:`python -m monoco your-command`
226
+
227
+ ### 多个 Feature 的 Hook 冲突
228
+
229
+ 使用优先级控制执行顺序,或检查其他 Feature 的 hook 逻辑。
230
+
231
+ ## 参考
232
+
233
+ - [Git Hooks 官方文档](https://git-scm.com/docs/githooks)
234
+ - Monoco Issue Feature 的 hooks 实现:`monoco/features/issue/resources/hooks/`
@@ -1,18 +1,31 @@
1
1
  from pathlib import Path
2
2
  from typing import Dict
3
- from monoco.core.feature import MonocoFeature, IntegrationData
3
+ from monoco.core.loader import FeatureModule, FeatureMetadata
4
+ from monoco.core.feature import IntegrationData
4
5
  from monoco.features.i18n import core
5
6
 
6
7
 
7
- class I18nFeature(MonocoFeature):
8
+ class I18nFeature(FeatureModule):
9
+ """Internationalization feature module with unified lifecycle support."""
10
+
8
11
  @property
9
- def name(self) -> str:
10
- return "i18n"
12
+ def metadata(self) -> FeatureMetadata:
13
+ return FeatureMetadata(
14
+ name="i18n",
15
+ version="1.0.0",
16
+ description="Documentation internationalization support",
17
+ dependencies=["core"],
18
+ priority=50,
19
+ lazy=True, # Can be lazy loaded - not critical for startup
20
+ )
11
21
 
12
- def initialize(self, root: Path, config: Dict) -> None:
22
+ def _on_mount(self, context: "FeatureContext") -> None: # type: ignore
23
+ """Initialize i18n feature with workspace context."""
24
+ root = context.root
13
25
  core.init(root)
14
26
 
15
27
  def integrate(self, root: Path, config: Dict) -> IntegrationData:
28
+ """Provide integration data for agent environment."""
16
29
  # Determine language from config, default to 'en'
17
30
  lang = config.get("i18n", {}).get("source_lang", "en")
18
31
  base_dir = Path(__file__).parent / "resources"