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.
- monoco/__main__.py +8 -0
- monoco/core/artifacts/__init__.py +16 -0
- monoco/core/artifacts/manager.py +575 -0
- monoco/core/artifacts/models.py +161 -0
- monoco/core/config.py +31 -4
- monoco/core/git.py +23 -0
- monoco/core/ingestion/__init__.py +20 -0
- monoco/core/ingestion/discovery.py +248 -0
- monoco/core/ingestion/watcher.py +343 -0
- monoco/core/ingestion/worker.py +436 -0
- monoco/core/loader.py +633 -0
- monoco/core/registry.py +34 -25
- monoco/core/skills.py +119 -80
- monoco/daemon/app.py +77 -1
- monoco/daemon/commands.py +10 -0
- monoco/daemon/mailroom_service.py +196 -0
- monoco/daemon/models.py +1 -0
- monoco/daemon/scheduler.py +236 -0
- monoco/daemon/services.py +185 -0
- monoco/daemon/triggers.py +55 -0
- monoco/features/agent/adapter.py +17 -7
- monoco/features/agent/apoptosis.py +4 -4
- monoco/features/agent/manager.py +41 -5
- monoco/{core/resources/en/skills/monoco_core → features/agent/resources/en/skills/monoco_atom_core}/SKILL.md +2 -2
- monoco/features/agent/resources/en/skills/{flow_engineer → monoco_workflow_agent_engineer}/SKILL.md +2 -2
- monoco/features/agent/resources/en/skills/{flow_manager → monoco_workflow_agent_manager}/SKILL.md +2 -2
- monoco/features/agent/resources/en/skills/{flow_planner → monoco_workflow_agent_planner}/SKILL.md +2 -2
- monoco/features/agent/resources/en/skills/{flow_reviewer → monoco_workflow_agent_reviewer}/SKILL.md +2 -2
- monoco/features/agent/resources/{roles/role-engineer.yaml → zh/roles/monoco_role_engineer.yaml} +3 -3
- monoco/features/agent/resources/{roles/role-manager.yaml → zh/roles/monoco_role_manager.yaml} +8 -8
- monoco/features/agent/resources/{roles/role-planner.yaml → zh/roles/monoco_role_planner.yaml} +8 -8
- monoco/features/agent/resources/{roles/role-reviewer.yaml → zh/roles/monoco_role_reviewer.yaml} +8 -8
- monoco/{core/resources/zh/skills/monoco_core → features/agent/resources/zh/skills/monoco_atom_core}/SKILL.md +2 -2
- monoco/features/agent/resources/zh/skills/{flow_engineer → monoco_workflow_agent_engineer}/SKILL.md +2 -2
- monoco/features/agent/resources/zh/skills/{flow_manager → monoco_workflow_agent_manager}/SKILL.md +2 -2
- monoco/features/agent/resources/zh/skills/{flow_planner → monoco_workflow_agent_planner}/SKILL.md +2 -2
- monoco/features/agent/resources/zh/skills/{flow_reviewer → monoco_workflow_agent_reviewer}/SKILL.md +2 -2
- monoco/features/agent/session.py +59 -11
- monoco/features/artifact/__init__.py +0 -0
- monoco/features/artifact/adapter.py +33 -0
- monoco/features/artifact/resources/zh/AGENTS.md +14 -0
- monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +278 -0
- monoco/features/glossary/adapter.py +18 -7
- monoco/features/glossary/resources/en/skills/{monoco_glossary → monoco_atom_glossary}/SKILL.md +2 -2
- monoco/features/glossary/resources/zh/skills/{monoco_glossary → monoco_atom_glossary}/SKILL.md +2 -2
- monoco/features/hooks/__init__.py +11 -0
- monoco/features/hooks/adapter.py +67 -0
- monoco/features/hooks/commands.py +309 -0
- monoco/features/hooks/core.py +441 -0
- monoco/features/hooks/resources/ADDING_HOOKS.md +234 -0
- monoco/features/i18n/adapter.py +18 -5
- monoco/features/i18n/core.py +482 -17
- monoco/features/i18n/resources/en/skills/{monoco_i18n → monoco_atom_i18n}/SKILL.md +2 -2
- monoco/features/i18n/resources/en/skills/{i18n_scan_workflow → monoco_workflow_i18n_scan}/SKILL.md +2 -2
- monoco/features/i18n/resources/zh/skills/{monoco_i18n → monoco_atom_i18n}/SKILL.md +2 -2
- monoco/features/i18n/resources/zh/skills/{i18n_scan_workflow → monoco_workflow_i18n_scan}/SKILL.md +2 -2
- monoco/features/issue/adapter.py +19 -6
- monoco/features/issue/commands.py +281 -7
- monoco/features/issue/core.py +227 -13
- monoco/features/issue/engine/machine.py +114 -4
- monoco/features/issue/linter.py +60 -5
- monoco/features/issue/models.py +2 -2
- monoco/features/issue/resources/en/AGENTS.md +109 -0
- monoco/features/issue/resources/en/skills/{monoco_issue → monoco_atom_issue}/SKILL.md +2 -2
- monoco/features/issue/resources/en/skills/{issue_create_workflow → monoco_workflow_issue_creation}/SKILL.md +2 -2
- monoco/features/issue/resources/en/skills/{issue_develop_workflow → monoco_workflow_issue_development}/SKILL.md +2 -2
- monoco/features/issue/resources/en/skills/{issue_lifecycle_workflow → monoco_workflow_issue_management}/SKILL.md +2 -2
- monoco/features/issue/resources/en/skills/{issue_refine_workflow → monoco_workflow_issue_refinement}/SKILL.md +2 -2
- monoco/features/issue/resources/hooks/post-checkout.sh +39 -0
- monoco/features/issue/resources/hooks/pre-commit.sh +41 -0
- monoco/features/issue/resources/hooks/pre-push.sh +35 -0
- monoco/features/issue/resources/zh/AGENTS.md +109 -0
- monoco/features/issue/resources/zh/skills/{monoco_issue → monoco_atom_issue_lifecycle}/SKILL.md +2 -2
- monoco/features/issue/resources/zh/skills/{issue_create_workflow → monoco_workflow_issue_creation}/SKILL.md +2 -2
- monoco/features/issue/resources/zh/skills/{issue_develop_workflow → monoco_workflow_issue_development}/SKILL.md +2 -2
- monoco/features/issue/resources/zh/skills/{issue_lifecycle_workflow → monoco_workflow_issue_management}/SKILL.md +2 -2
- monoco/features/issue/resources/zh/skills/{issue_refine_workflow → monoco_workflow_issue_refinement}/SKILL.md +2 -2
- monoco/features/issue/validator.py +101 -1
- monoco/features/memo/adapter.py +21 -8
- monoco/features/memo/cli.py +103 -10
- monoco/features/memo/core.py +178 -92
- monoco/features/memo/models.py +53 -0
- monoco/features/memo/resources/en/skills/{monoco_memo → monoco_atom_memo}/SKILL.md +2 -2
- monoco/features/memo/resources/en/skills/{note_processing_workflow → monoco_workflow_note_processing}/SKILL.md +2 -2
- monoco/features/memo/resources/zh/skills/{monoco_memo → monoco_atom_memo}/SKILL.md +2 -2
- monoco/features/memo/resources/zh/skills/{note_processing_workflow → monoco_workflow_note_processing}/SKILL.md +2 -2
- monoco/features/spike/adapter.py +18 -5
- monoco/features/spike/resources/en/skills/{monoco_spike → monoco_atom_spike}/SKILL.md +2 -2
- monoco/features/spike/resources/en/skills/{research_workflow → monoco_workflow_research}/SKILL.md +2 -2
- monoco/features/spike/resources/zh/skills/{monoco_spike → monoco_atom_spike}/SKILL.md +2 -2
- monoco/features/spike/resources/zh/skills/{research_workflow → monoco_workflow_research}/SKILL.md +2 -2
- monoco/main.py +38 -1
- {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.11.dist-info}/METADATA +7 -1
- monoco_toolkit-0.3.11.dist-info/RECORD +181 -0
- monoco_toolkit-0.3.10.dist-info/RECORD +0 -156
- /monoco/{core → features/agent}/resources/en/AGENTS.md +0 -0
- /monoco/{core → features/agent}/resources/zh/AGENTS.md +0 -0
- {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.11.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.11.dist-info}/entry_points.txt +0 -0
- {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/`
|
monoco/features/i18n/adapter.py
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
from typing import Dict
|
|
3
|
-
from monoco.core.
|
|
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(
|
|
8
|
+
class I18nFeature(FeatureModule):
|
|
9
|
+
"""Internationalization feature module with unified lifecycle support."""
|
|
10
|
+
|
|
8
11
|
@property
|
|
9
|
-
def
|
|
10
|
-
return
|
|
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
|
|
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"
|