monoco-toolkit 0.3.6__py3-none-any.whl → 0.3.9__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/cli/workspace.py +1 -1
- monoco/core/config.py +51 -0
- monoco/core/hooks/__init__.py +19 -0
- monoco/core/hooks/base.py +104 -0
- monoco/core/hooks/builtin/__init__.py +11 -0
- monoco/core/hooks/builtin/git_cleanup.py +266 -0
- monoco/core/hooks/builtin/logging_hook.py +78 -0
- monoco/core/hooks/context.py +131 -0
- monoco/core/hooks/registry.py +222 -0
- monoco/core/integrations.py +6 -0
- monoco/core/registry.py +2 -0
- monoco/core/setup.py +1 -1
- monoco/core/skills.py +226 -42
- monoco/features/{scheduler → agent}/__init__.py +4 -2
- monoco/features/{scheduler → agent}/cli.py +134 -80
- monoco/features/{scheduler → agent}/config.py +17 -3
- monoco/features/agent/defaults.py +55 -0
- monoco/features/agent/flow_skills.py +281 -0
- monoco/features/{scheduler → agent}/manager.py +39 -2
- monoco/features/{scheduler → agent}/models.py +6 -3
- monoco/features/{scheduler → agent}/reliability.py +1 -1
- monoco/features/agent/resources/skills/flow_engineer/SKILL.md +94 -0
- monoco/features/agent/resources/skills/flow_manager/SKILL.md +88 -0
- monoco/features/agent/resources/skills/flow_reviewer/SKILL.md +114 -0
- monoco/features/{scheduler → agent}/session.py +36 -1
- monoco/features/{scheduler → agent}/worker.py +2 -2
- monoco/features/i18n/resources/skills/i18n_scan_workflow/SKILL.md +105 -0
- monoco/features/issue/commands.py +427 -21
- monoco/features/issue/core.py +100 -0
- monoco/features/issue/criticality.py +553 -0
- monoco/features/issue/domain/models.py +28 -2
- monoco/features/issue/engine/machine.py +70 -13
- monoco/features/issue/git_service.py +185 -0
- monoco/features/issue/linter.py +291 -62
- monoco/features/issue/models.py +49 -2
- monoco/features/issue/resources/en/SKILL.md +48 -0
- monoco/features/issue/resources/skills/issue_lifecycle_workflow/SKILL.md +159 -0
- monoco/features/issue/resources/zh/SKILL.md +50 -0
- monoco/features/issue/validator.py +185 -65
- monoco/features/memo/__init__.py +2 -1
- monoco/features/memo/adapter.py +32 -0
- monoco/features/memo/cli.py +36 -14
- monoco/features/memo/core.py +59 -0
- monoco/features/memo/resources/skills/note_processing_workflow/SKILL.md +140 -0
- monoco/features/memo/resources/zh/AGENTS.md +8 -0
- monoco/features/memo/resources/zh/SKILL.md +75 -0
- monoco/features/spike/resources/skills/research_workflow/SKILL.md +121 -0
- monoco/main.py +2 -3
- {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.9.dist-info}/METADATA +1 -1
- {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.9.dist-info}/RECORD +55 -37
- monoco/features/scheduler/defaults.py +0 -54
- monoco/features/skills/__init__.py +0 -0
- monoco/features/skills/core.py +0 -102
- /monoco/core/{hooks.py → githooks.py} +0 -0
- /monoco/features/{scheduler → agent}/engines.py +0 -0
- {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.9.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.9.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hook Registry - Manages registration and execution of session lifecycle hooks.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import List, Type, Optional, Dict, Any
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .base import SessionLifecycleHook, HookResult, HookStatus
|
|
10
|
+
from .context import HookContext
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("monoco.core.hooks")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HookRegistry:
|
|
16
|
+
"""
|
|
17
|
+
Registry for managing session lifecycle hooks.
|
|
18
|
+
|
|
19
|
+
Responsible for:
|
|
20
|
+
- Registering hooks
|
|
21
|
+
- Executing hooks in order
|
|
22
|
+
- Handling hook errors gracefully
|
|
23
|
+
- Loading hooks from configuration
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self._hooks: List[SessionLifecycleHook] = []
|
|
28
|
+
self._hook_classes: Dict[str, Type[SessionLifecycleHook]] = {}
|
|
29
|
+
|
|
30
|
+
def register(self, hook: SessionLifecycleHook) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Register a hook instance.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
hook: The hook instance to register
|
|
36
|
+
"""
|
|
37
|
+
if not isinstance(hook, SessionLifecycleHook):
|
|
38
|
+
raise TypeError(f"Hook must be a SessionLifecycleHook, got {type(hook)}")
|
|
39
|
+
|
|
40
|
+
self._hooks.append(hook)
|
|
41
|
+
logger.debug(f"Registered hook: {hook.name}")
|
|
42
|
+
|
|
43
|
+
def register_class(
|
|
44
|
+
self,
|
|
45
|
+
hook_class: Type[SessionLifecycleHook],
|
|
46
|
+
name: Optional[str] = None,
|
|
47
|
+
config: Optional[Dict[str, Any]] = None
|
|
48
|
+
) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Register a hook class (will be instantiated).
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
hook_class: The hook class to register
|
|
54
|
+
name: Optional name for the hook instance
|
|
55
|
+
config: Optional configuration for the hook
|
|
56
|
+
"""
|
|
57
|
+
if not issubclass(hook_class, SessionLifecycleHook):
|
|
58
|
+
raise TypeError(f"Hook class must inherit from SessionLifecycleHook")
|
|
59
|
+
|
|
60
|
+
instance = hook_class(name=name, config=config)
|
|
61
|
+
self.register(instance)
|
|
62
|
+
|
|
63
|
+
def unregister(self, name: str) -> bool:
|
|
64
|
+
"""
|
|
65
|
+
Unregister a hook by name.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
name: The name of the hook to unregister
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
True if a hook was removed, False otherwise
|
|
72
|
+
"""
|
|
73
|
+
for i, hook in enumerate(self._hooks):
|
|
74
|
+
if hook.name == name:
|
|
75
|
+
self._hooks.pop(i)
|
|
76
|
+
logger.debug(f"Unregistered hook: {name}")
|
|
77
|
+
return True
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
def get_hooks(self, enabled_only: bool = True) -> List[SessionLifecycleHook]:
|
|
81
|
+
"""
|
|
82
|
+
Get all registered hooks.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
enabled_only: If True, only return enabled hooks
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
List of hook instances
|
|
89
|
+
"""
|
|
90
|
+
if enabled_only:
|
|
91
|
+
return [h for h in self._hooks if h.is_enabled()]
|
|
92
|
+
return self._hooks.copy()
|
|
93
|
+
|
|
94
|
+
def clear(self) -> None:
|
|
95
|
+
"""Clear all registered hooks."""
|
|
96
|
+
self._hooks.clear()
|
|
97
|
+
logger.debug("Cleared all hooks")
|
|
98
|
+
|
|
99
|
+
def execute_on_session_start(self, context: HookContext) -> List[HookResult]:
|
|
100
|
+
"""
|
|
101
|
+
Execute all registered hooks' on_session_start methods.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
context: The hook context
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
List of results from each hook
|
|
108
|
+
"""
|
|
109
|
+
return self._execute_hooks("on_session_start", context)
|
|
110
|
+
|
|
111
|
+
def execute_on_session_end(self, context: HookContext) -> List[HookResult]:
|
|
112
|
+
"""
|
|
113
|
+
Execute all registered hooks' on_session_end methods.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
context: The hook context
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
List of results from each hook
|
|
120
|
+
"""
|
|
121
|
+
return self._execute_hooks("on_session_end", context)
|
|
122
|
+
|
|
123
|
+
def _execute_hooks(
|
|
124
|
+
self,
|
|
125
|
+
method_name: str,
|
|
126
|
+
context: HookContext
|
|
127
|
+
) -> List[HookResult]:
|
|
128
|
+
"""
|
|
129
|
+
Execute a hook method on all registered hooks.
|
|
130
|
+
|
|
131
|
+
Errors in individual hooks don't stop execution of other hooks.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
method_name: The name of the method to call
|
|
135
|
+
context: The hook context
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
List of results from each hook
|
|
139
|
+
"""
|
|
140
|
+
results = []
|
|
141
|
+
hooks = self.get_hooks(enabled_only=True)
|
|
142
|
+
|
|
143
|
+
for hook in hooks:
|
|
144
|
+
try:
|
|
145
|
+
method = getattr(hook, method_name)
|
|
146
|
+
result = method(context)
|
|
147
|
+
results.append(result)
|
|
148
|
+
|
|
149
|
+
if result.status == HookStatus.FAILURE:
|
|
150
|
+
logger.warning(
|
|
151
|
+
f"Hook '{hook.name}' {method_name} failed: {result.message}"
|
|
152
|
+
)
|
|
153
|
+
elif result.status == HookStatus.WARNING:
|
|
154
|
+
logger.warning(
|
|
155
|
+
f"Hook '{hook.name}' {method_name} warning: {result.message}"
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
logger.debug(
|
|
159
|
+
f"Hook '{hook.name}' {method_name} succeeded: {result.message}"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
except Exception as e:
|
|
163
|
+
logger.error(f"Hook '{hook.name}' {method_name} raised exception: {e}")
|
|
164
|
+
results.append(HookResult.failure(str(e)))
|
|
165
|
+
|
|
166
|
+
return results
|
|
167
|
+
|
|
168
|
+
def load_from_config(self, config: Dict[str, Any], project_root: Path) -> None:
|
|
169
|
+
"""
|
|
170
|
+
Load and register hooks from configuration.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
config: The hooks configuration dictionary
|
|
174
|
+
project_root: The project root path
|
|
175
|
+
"""
|
|
176
|
+
if not config:
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
# Import built-in hooks
|
|
180
|
+
from .builtin.git_cleanup import GitCleanupHook
|
|
181
|
+
from .builtin.logging_hook import LoggingHook
|
|
182
|
+
|
|
183
|
+
# Map of hook names to classes
|
|
184
|
+
builtin_hooks = {
|
|
185
|
+
"git_cleanup": GitCleanupHook,
|
|
186
|
+
"logging": LoggingHook,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for hook_name, hook_config in config.items():
|
|
190
|
+
if isinstance(hook_config, bool):
|
|
191
|
+
# Simple enable/disable: "git_cleanup: true"
|
|
192
|
+
if hook_config and hook_name in builtin_hooks:
|
|
193
|
+
self.register_class(builtin_hooks[hook_name], name=hook_name)
|
|
194
|
+
elif isinstance(hook_config, dict):
|
|
195
|
+
# Full configuration: "git_cleanup: { enabled: true, ... }"
|
|
196
|
+
enabled = hook_config.get("enabled", True)
|
|
197
|
+
if enabled and hook_name in builtin_hooks:
|
|
198
|
+
self.register_class(
|
|
199
|
+
builtin_hooks[hook_name],
|
|
200
|
+
name=hook_name,
|
|
201
|
+
config=hook_config
|
|
202
|
+
)
|
|
203
|
+
else:
|
|
204
|
+
logger.warning(f"Unknown hook config format for '{hook_name}'")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# Global registry instance
|
|
208
|
+
_global_registry: Optional[HookRegistry] = None
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def get_registry() -> HookRegistry:
|
|
212
|
+
"""Get the global hook registry."""
|
|
213
|
+
global _global_registry
|
|
214
|
+
if _global_registry is None:
|
|
215
|
+
_global_registry = HookRegistry()
|
|
216
|
+
return _global_registry
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def reset_registry() -> None:
|
|
220
|
+
"""Reset the global registry (mainly for testing)."""
|
|
221
|
+
global _global_registry
|
|
222
|
+
_global_registry = HookRegistry()
|
monoco/core/integrations.py
CHANGED
|
@@ -134,6 +134,12 @@ DEFAULT_INTEGRATIONS: Dict[str, AgentIntegration] = {
|
|
|
134
134
|
bin_name="kimi",
|
|
135
135
|
version_cmd="--version",
|
|
136
136
|
),
|
|
137
|
+
"agent": AgentIntegration(
|
|
138
|
+
key="agent",
|
|
139
|
+
name="Generic Agent",
|
|
140
|
+
system_prompt_file="AGENTS.md",
|
|
141
|
+
skill_root_dir=".agent/skills/",
|
|
142
|
+
),
|
|
137
143
|
}
|
|
138
144
|
|
|
139
145
|
|
monoco/core/registry.py
CHANGED
|
@@ -30,8 +30,10 @@ class FeatureRegistry:
|
|
|
30
30
|
from monoco.features.issue.adapter import IssueFeature
|
|
31
31
|
from monoco.features.spike.adapter import SpikeFeature
|
|
32
32
|
from monoco.features.i18n.adapter import I18nFeature
|
|
33
|
+
from monoco.features.memo.adapter import MemoFeature
|
|
33
34
|
|
|
34
35
|
cls.register(IssueFeature())
|
|
35
36
|
cls.register(SpikeFeature())
|
|
36
37
|
cls.register(I18nFeature())
|
|
38
|
+
cls.register(MemoFeature())
|
|
37
39
|
pass
|
monoco/core/setup.py
CHANGED
|
@@ -321,7 +321,7 @@ def init_cli(
|
|
|
321
321
|
|
|
322
322
|
# Initialize Hooks
|
|
323
323
|
try:
|
|
324
|
-
from monoco.core.
|
|
324
|
+
from monoco.core.githooks import install_hooks
|
|
325
325
|
|
|
326
326
|
# Re-load config to get the just-written hooks (or default ones)
|
|
327
327
|
# Actually we have the dict right here in workspace_config['hooks']
|
monoco/core/skills.py
CHANGED
|
@@ -9,12 +9,13 @@ Key Responsibilities:
|
|
|
9
9
|
2. Validate skill structure and metadata (YAML frontmatter)
|
|
10
10
|
3. Distribute skills to target agent framework directories
|
|
11
11
|
4. Support i18n for skill content
|
|
12
|
+
5. Support multi-skill architecture (1 Feature : N Skills)
|
|
12
13
|
"""
|
|
13
14
|
|
|
14
15
|
import shutil
|
|
15
16
|
import hashlib
|
|
16
17
|
from pathlib import Path
|
|
17
|
-
from typing import Dict, List, Optional
|
|
18
|
+
from typing import Dict, List, Optional, Set
|
|
18
19
|
from pydantic import BaseModel, Field, ValidationError
|
|
19
20
|
from rich.console import Console
|
|
20
21
|
import yaml
|
|
@@ -37,6 +38,12 @@ class SkillMetadata(BaseModel):
|
|
|
37
38
|
tags: Optional[List[str]] = Field(
|
|
38
39
|
default=None, description="Skill tags for categorization"
|
|
39
40
|
)
|
|
41
|
+
type: Optional[str] = Field(
|
|
42
|
+
default="standard", description="Skill type: standard, flow, etc."
|
|
43
|
+
)
|
|
44
|
+
role: Optional[str] = Field(
|
|
45
|
+
default=None, description="Role identifier for Flow Skills (e.g., engineer, manager)"
|
|
46
|
+
)
|
|
40
47
|
|
|
41
48
|
|
|
42
49
|
class Skill:
|
|
@@ -44,18 +51,26 @@ class Skill:
|
|
|
44
51
|
Represents a single skill with its metadata and file paths.
|
|
45
52
|
"""
|
|
46
53
|
|
|
47
|
-
def __init__(
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
root_dir: Path,
|
|
57
|
+
skill_dir: Path,
|
|
58
|
+
name: Optional[str] = None,
|
|
59
|
+
skill_file: Optional[Path] = None,
|
|
60
|
+
):
|
|
48
61
|
"""
|
|
49
62
|
Initialize a Skill instance.
|
|
50
63
|
|
|
51
64
|
Args:
|
|
52
65
|
root_dir: Project root directory
|
|
53
66
|
skill_dir: Path to the skill directory (e.g., Toolkit/skills/issues-management)
|
|
67
|
+
name: Optional custom skill name (overrides directory name)
|
|
68
|
+
skill_file: Optional specific SKILL.md path (for multi-skill architecture)
|
|
54
69
|
"""
|
|
55
70
|
self.root_dir = root_dir
|
|
56
71
|
self.skill_dir = skill_dir
|
|
57
|
-
self.name = skill_dir.name
|
|
58
|
-
self.skill_file = skill_dir / "SKILL.md"
|
|
72
|
+
self.name = name or skill_dir.name
|
|
73
|
+
self.skill_file = skill_file or (skill_dir / "SKILL.md")
|
|
59
74
|
self.metadata: Optional[SkillMetadata] = None
|
|
60
75
|
self._load_metadata()
|
|
61
76
|
|
|
@@ -103,6 +118,14 @@ class Skill:
|
|
|
103
118
|
"""Check if the skill has valid metadata."""
|
|
104
119
|
return self.metadata is not None
|
|
105
120
|
|
|
121
|
+
def get_type(self) -> str:
|
|
122
|
+
"""Get skill type, defaults to 'standard'."""
|
|
123
|
+
return self.metadata.type if self.metadata and self.metadata.type else "standard"
|
|
124
|
+
|
|
125
|
+
def get_role(self) -> Optional[str]:
|
|
126
|
+
"""Get skill role (for Flow Skills)."""
|
|
127
|
+
return self.metadata.role if self.metadata else None
|
|
128
|
+
|
|
106
129
|
def get_languages(self) -> List[str]:
|
|
107
130
|
"""
|
|
108
131
|
Detect available language versions of this skill.
|
|
@@ -158,21 +181,32 @@ class SkillManager:
|
|
|
158
181
|
Central manager for Monoco skills.
|
|
159
182
|
|
|
160
183
|
Responsibilities:
|
|
161
|
-
- Collect skills from Feature resources
|
|
184
|
+
- Collect skills from Feature resources (standard + multi-skill architecture)
|
|
162
185
|
- Validate skill structure
|
|
163
186
|
- Distribute skills to agent framework directories
|
|
187
|
+
- Support Flow Skills with custom prefixes
|
|
164
188
|
"""
|
|
165
189
|
|
|
166
|
-
|
|
190
|
+
# Default prefix for flow skills
|
|
191
|
+
FLOW_SKILL_PREFIX = "monoco_flow_"
|
|
192
|
+
|
|
193
|
+
def __init__(
|
|
194
|
+
self,
|
|
195
|
+
root: Path,
|
|
196
|
+
features: Optional[List] = None,
|
|
197
|
+
flow_skill_prefix: str = FLOW_SKILL_PREFIX,
|
|
198
|
+
):
|
|
167
199
|
"""
|
|
168
200
|
Initialize SkillManager.
|
|
169
201
|
|
|
170
202
|
Args:
|
|
171
203
|
root: Project root directory
|
|
172
204
|
features: List of MonocoFeature instances (if None, will load from registry)
|
|
205
|
+
flow_skill_prefix: Prefix for flow skill directory names
|
|
173
206
|
"""
|
|
174
207
|
self.root = root
|
|
175
208
|
self.features = features or []
|
|
209
|
+
self.flow_skill_prefix = flow_skill_prefix
|
|
176
210
|
self.skills: Dict[str, Skill] = {}
|
|
177
211
|
|
|
178
212
|
if self.features:
|
|
@@ -211,8 +245,9 @@ class SkillManager:
|
|
|
211
245
|
"""
|
|
212
246
|
Discover skills from Feature resources.
|
|
213
247
|
|
|
214
|
-
|
|
215
|
-
|
|
248
|
+
Supports two patterns:
|
|
249
|
+
1. Legacy: monoco/features/{feature}/resources/{lang}/SKILL.md
|
|
250
|
+
2. Multi-skill: monoco/features/{feature}/resources/skills/{skill-name}/SKILL.md
|
|
216
251
|
"""
|
|
217
252
|
from monoco.core.feature import MonocoFeature
|
|
218
253
|
|
|
@@ -221,7 +256,6 @@ class SkillManager:
|
|
|
221
256
|
continue
|
|
222
257
|
|
|
223
258
|
# Determine feature module path
|
|
224
|
-
# feature.__class__.__module__ is like 'monoco.features.issue.adapter'
|
|
225
259
|
module_parts = feature.__class__.__module__.split(".")
|
|
226
260
|
if (
|
|
227
261
|
len(module_parts) >= 3
|
|
@@ -231,27 +265,82 @@ class SkillManager:
|
|
|
231
265
|
feature_name = module_parts[2]
|
|
232
266
|
|
|
233
267
|
# Construct path to feature resources
|
|
234
|
-
# monoco/features/{feature}/resources/
|
|
235
268
|
feature_dir = self.root / "monoco" / "features" / feature_name
|
|
236
269
|
resources_dir = feature_dir / "resources"
|
|
237
270
|
|
|
238
271
|
if not resources_dir.exists():
|
|
239
272
|
continue
|
|
240
273
|
|
|
241
|
-
#
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
274
|
+
# First, discover multi-skill architecture (resources/skills/*)
|
|
275
|
+
self._discover_multi_skills(resources_dir, feature_name)
|
|
276
|
+
|
|
277
|
+
# Second, discover legacy pattern (resources/{lang}/SKILL.md)
|
|
278
|
+
self._discover_legacy_skill(resources_dir, feature_name)
|
|
279
|
+
|
|
280
|
+
def _discover_multi_skills(self, resources_dir: Path, feature_name: str) -> None:
|
|
281
|
+
"""
|
|
282
|
+
Discover skills from resources/skills/ directory (multi-skill architecture).
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
resources_dir: Path to the feature's resources directory
|
|
286
|
+
feature_name: Name of the feature
|
|
287
|
+
"""
|
|
288
|
+
skills_dir = resources_dir / "skills"
|
|
289
|
+
if not skills_dir.exists():
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
for skill_subdir in skills_dir.iterdir():
|
|
293
|
+
if not skill_subdir.is_dir():
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
skill_file = skill_subdir / "SKILL.md"
|
|
297
|
+
if not skill_file.exists():
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
# Create skill instance
|
|
301
|
+
skill = Skill(
|
|
302
|
+
root_dir=self.root,
|
|
303
|
+
skill_dir=skill_subdir,
|
|
304
|
+
name=skill_subdir.name,
|
|
305
|
+
skill_file=skill_file,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
if not skill.is_valid():
|
|
309
|
+
continue
|
|
310
|
+
|
|
311
|
+
# Determine skill key based on type
|
|
312
|
+
skill_type = skill.get_type()
|
|
313
|
+
if skill_type == "flow":
|
|
314
|
+
# Flow skills get prefixed (e.g., monoco_flow_engineer)
|
|
315
|
+
skill_key = f"{self.flow_skill_prefix}{skill_subdir.name}"
|
|
316
|
+
else:
|
|
317
|
+
# Standard skills use feature-scoped name to avoid conflicts
|
|
318
|
+
# e.g., scheduler_config, scheduler_utils
|
|
319
|
+
skill_key = f"{feature_name}_{skill_subdir.name}"
|
|
320
|
+
|
|
321
|
+
# Override name for distribution
|
|
322
|
+
skill.name = skill_key
|
|
323
|
+
self.skills[skill_key] = skill
|
|
324
|
+
|
|
325
|
+
def _discover_legacy_skill(self, resources_dir: Path, feature_name: str) -> None:
|
|
326
|
+
"""
|
|
327
|
+
Discover legacy single skill from resources/{lang}/SKILL.md.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
resources_dir: Path to the feature's resources directory
|
|
331
|
+
feature_name: Name of the feature
|
|
332
|
+
"""
|
|
333
|
+
# Check for SKILL.md in language directories
|
|
334
|
+
for lang_dir in resources_dir.iterdir():
|
|
335
|
+
if lang_dir.is_dir() and (lang_dir / "SKILL.md").exists():
|
|
336
|
+
# Create a Skill instance
|
|
337
|
+
skill = self._create_skill_from_feature(feature_name, resources_dir)
|
|
338
|
+
if skill and skill.is_valid():
|
|
339
|
+
# Use feature name as skill identifier
|
|
340
|
+
skill_key = f"{feature_name}"
|
|
341
|
+
if skill_key not in self.skills:
|
|
342
|
+
self.skills[skill_key] = skill
|
|
343
|
+
break # Only need to detect once per feature
|
|
255
344
|
|
|
256
345
|
def _create_skill_from_feature(
|
|
257
346
|
self, feature_name: str, resources_dir: Path
|
|
@@ -267,7 +356,6 @@ class SkillManager:
|
|
|
267
356
|
Skill instance or None if creation fails
|
|
268
357
|
"""
|
|
269
358
|
# Use the resources directory as the skill directory
|
|
270
|
-
# The Skill class expects a directory with SKILL.md or {lang}/SKILL.md
|
|
271
359
|
skill = Skill(self.root, resources_dir)
|
|
272
360
|
|
|
273
361
|
# Use the skill's metadata name if available (e.g., 'monoco-issue')
|
|
@@ -290,6 +378,18 @@ class SkillManager:
|
|
|
290
378
|
"""
|
|
291
379
|
return list(self.skills.values())
|
|
292
380
|
|
|
381
|
+
def list_skills_by_type(self, skill_type: str) -> List[Skill]:
|
|
382
|
+
"""
|
|
383
|
+
Get skills filtered by type.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
skill_type: Skill type to filter by (e.g., 'flow', 'standard')
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
List of Skill instances matching the type
|
|
390
|
+
"""
|
|
391
|
+
return [s for s in self.skills.values() if s.get_type() == skill_type]
|
|
392
|
+
|
|
293
393
|
def get_skill(self, name: str) -> Optional[Skill]:
|
|
294
394
|
"""
|
|
295
395
|
Get a specific skill by name.
|
|
@@ -302,6 +402,15 @@ class SkillManager:
|
|
|
302
402
|
"""
|
|
303
403
|
return self.skills.get(name)
|
|
304
404
|
|
|
405
|
+
def get_flow_skills(self) -> List[Skill]:
|
|
406
|
+
"""
|
|
407
|
+
Get all Flow Skills.
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
List of Flow Skill instances
|
|
411
|
+
"""
|
|
412
|
+
return self.list_skills_by_type("flow")
|
|
413
|
+
|
|
305
414
|
def distribute(
|
|
306
415
|
self, target_dir: Path, lang: str, force: bool = False
|
|
307
416
|
) -> Dict[str, bool]:
|
|
@@ -323,19 +432,28 @@ class SkillManager:
|
|
|
323
432
|
|
|
324
433
|
for skill_name, skill in self.skills.items():
|
|
325
434
|
try:
|
|
326
|
-
#
|
|
327
|
-
|
|
435
|
+
# Handle different skill types
|
|
436
|
+
skill_type = skill.get_type()
|
|
328
437
|
|
|
329
|
-
if
|
|
330
|
-
|
|
331
|
-
|
|
438
|
+
if skill_type == "flow":
|
|
439
|
+
# Flow skills: copy entire directory (no language filtering)
|
|
440
|
+
success = self._distribute_flow_skill(skill, target_dir, force)
|
|
441
|
+
else:
|
|
442
|
+
# Standard skills: distribute specific language version
|
|
443
|
+
available_languages = skill.get_languages()
|
|
444
|
+
|
|
445
|
+
if lang not in available_languages:
|
|
446
|
+
console.print(
|
|
447
|
+
f"[yellow]Skill {skill_name} does not have {lang} version, skipping[/yellow]"
|
|
448
|
+
)
|
|
449
|
+
results[skill_name] = False
|
|
450
|
+
continue
|
|
451
|
+
|
|
452
|
+
success = self._distribute_standard_skill(
|
|
453
|
+
skill, target_dir, lang, force
|
|
332
454
|
)
|
|
333
|
-
results[skill_name] = False
|
|
334
|
-
continue
|
|
335
455
|
|
|
336
|
-
|
|
337
|
-
self._distribute_skill_language(skill, target_dir, lang, force)
|
|
338
|
-
results[skill_name] = True
|
|
456
|
+
results[skill_name] = success
|
|
339
457
|
|
|
340
458
|
except Exception as e:
|
|
341
459
|
console.print(
|
|
@@ -345,17 +463,56 @@ class SkillManager:
|
|
|
345
463
|
|
|
346
464
|
return results
|
|
347
465
|
|
|
348
|
-
def
|
|
466
|
+
def _distribute_flow_skill(
|
|
467
|
+
self, skill: Skill, target_dir: Path, force: bool
|
|
468
|
+
) -> bool:
|
|
469
|
+
"""
|
|
470
|
+
Distribute a Flow Skill to target directory.
|
|
471
|
+
|
|
472
|
+
Flow skills are copied as entire directories (including subdirectories).
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
skill: Flow Skill instance
|
|
476
|
+
target_dir: Target directory
|
|
477
|
+
force: Force overwrite
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
True if distribution successful
|
|
481
|
+
"""
|
|
482
|
+
target_skill_dir = target_dir / skill.name
|
|
483
|
+
|
|
484
|
+
# Check if update is needed (compare SKILL.md mtime)
|
|
485
|
+
if target_skill_dir.exists() and not force:
|
|
486
|
+
source_mtime = skill.skill_file.stat().st_mtime
|
|
487
|
+
target_skill_file = target_skill_dir / "SKILL.md"
|
|
488
|
+
if target_skill_file.exists():
|
|
489
|
+
target_mtime = target_skill_file.stat().st_mtime
|
|
490
|
+
if source_mtime <= target_mtime:
|
|
491
|
+
console.print(f"[dim] = {skill.name}/ is up to date[/dim]")
|
|
492
|
+
return True
|
|
493
|
+
|
|
494
|
+
# Remove existing and copy fresh
|
|
495
|
+
if target_skill_dir.exists():
|
|
496
|
+
shutil.rmtree(target_skill_dir)
|
|
497
|
+
|
|
498
|
+
shutil.copytree(skill.skill_dir, target_skill_dir)
|
|
499
|
+
console.print(f"[green] ✓ Distributed {skill.name}/[/green]")
|
|
500
|
+
return True
|
|
501
|
+
|
|
502
|
+
def _distribute_standard_skill(
|
|
349
503
|
self, skill: Skill, target_dir: Path, lang: str, force: bool
|
|
350
|
-
) ->
|
|
504
|
+
) -> bool:
|
|
351
505
|
"""
|
|
352
|
-
Distribute a
|
|
506
|
+
Distribute a standard skill to target directory.
|
|
353
507
|
|
|
354
508
|
Args:
|
|
355
|
-
skill: Skill instance
|
|
356
|
-
target_dir: Target directory
|
|
509
|
+
skill: Standard Skill instance
|
|
510
|
+
target_dir: Target directory
|
|
357
511
|
lang: Language code
|
|
358
512
|
force: Force overwrite
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
True if distribution successful
|
|
359
516
|
"""
|
|
360
517
|
# Determine source file (try language subdirectory first)
|
|
361
518
|
source_file = skill.skill_dir / lang / "SKILL.md"
|
|
@@ -368,7 +525,7 @@ class SkillManager:
|
|
|
368
525
|
console.print(
|
|
369
526
|
f"[yellow]Source file not found for {skill.name}/{lang}[/yellow]"
|
|
370
527
|
)
|
|
371
|
-
return
|
|
528
|
+
return False
|
|
372
529
|
|
|
373
530
|
# Target path: {target_dir}/{skill_name}/SKILL.md (no language subdirectory)
|
|
374
531
|
target_skill_dir = target_dir / skill.name
|
|
@@ -385,7 +542,7 @@ class SkillManager:
|
|
|
385
542
|
|
|
386
543
|
if source_checksum == target_checksum:
|
|
387
544
|
console.print(f"[dim] = {skill.name}/SKILL.md is up to date[/dim]")
|
|
388
|
-
return
|
|
545
|
+
return True
|
|
389
546
|
|
|
390
547
|
# Copy the file
|
|
391
548
|
shutil.copy2(source_file, target_file)
|
|
@@ -394,6 +551,8 @@ class SkillManager:
|
|
|
394
551
|
# Copy additional resources if they exist
|
|
395
552
|
self._copy_skill_resources(skill.skill_dir, target_skill_dir, lang)
|
|
396
553
|
|
|
554
|
+
return True
|
|
555
|
+
|
|
397
556
|
def _copy_skill_resources(
|
|
398
557
|
self, source_dir: Path, target_dir: Path, lang: str
|
|
399
558
|
) -> None:
|
|
@@ -456,3 +615,28 @@ class SkillManager:
|
|
|
456
615
|
|
|
457
616
|
if removed_count == 0:
|
|
458
617
|
console.print(f"[dim]No skills to remove from {target_dir}[/dim]")
|
|
618
|
+
|
|
619
|
+
def get_flow_skill_commands(self) -> List[str]:
|
|
620
|
+
"""
|
|
621
|
+
Get list of available flow skill commands.
|
|
622
|
+
|
|
623
|
+
In Kimi CLI, flow skills are invoked via /flow:<role> command.
|
|
624
|
+
This function extracts the role names from flow skills.
|
|
625
|
+
|
|
626
|
+
Returns:
|
|
627
|
+
List of available /flow:<role> commands
|
|
628
|
+
"""
|
|
629
|
+
commands = []
|
|
630
|
+
for skill in self.get_flow_skills():
|
|
631
|
+
role = skill.get_role()
|
|
632
|
+
if role:
|
|
633
|
+
commands.append(f"/flow:{role}")
|
|
634
|
+
else:
|
|
635
|
+
# Extract role from skill name
|
|
636
|
+
# e.g., monoco_flow_engineer -> engineer
|
|
637
|
+
name = skill.name
|
|
638
|
+
if name.startswith(self.flow_skill_prefix):
|
|
639
|
+
role = name[len(self.flow_skill_prefix) + 5:] # Remove prefix + "flow_"
|
|
640
|
+
if role:
|
|
641
|
+
commands.append(f"/flow:{role}")
|
|
642
|
+
return sorted(commands)
|