monoco-toolkit 0.3.5__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.
Files changed (59) hide show
  1. monoco/cli/workspace.py +1 -1
  2. monoco/core/config.py +51 -0
  3. monoco/core/hooks/__init__.py +19 -0
  4. monoco/core/hooks/base.py +104 -0
  5. monoco/core/hooks/builtin/__init__.py +11 -0
  6. monoco/core/hooks/builtin/git_cleanup.py +266 -0
  7. monoco/core/hooks/builtin/logging_hook.py +78 -0
  8. monoco/core/hooks/context.py +131 -0
  9. monoco/core/hooks/registry.py +222 -0
  10. monoco/core/integrations.py +6 -0
  11. monoco/core/registry.py +2 -0
  12. monoco/core/setup.py +1 -1
  13. monoco/core/skills.py +226 -42
  14. monoco/features/{scheduler → agent}/__init__.py +4 -2
  15. monoco/features/{scheduler → agent}/cli.py +134 -80
  16. monoco/features/{scheduler → agent}/config.py +17 -3
  17. monoco/features/agent/defaults.py +55 -0
  18. monoco/features/agent/flow_skills.py +281 -0
  19. monoco/features/{scheduler → agent}/manager.py +39 -2
  20. monoco/features/{scheduler → agent}/models.py +6 -3
  21. monoco/features/{scheduler → agent}/reliability.py +1 -1
  22. monoco/features/agent/resources/skills/flow_engineer/SKILL.md +94 -0
  23. monoco/features/agent/resources/skills/flow_manager/SKILL.md +88 -0
  24. monoco/features/agent/resources/skills/flow_reviewer/SKILL.md +114 -0
  25. monoco/features/{scheduler → agent}/session.py +39 -5
  26. monoco/features/{scheduler → agent}/worker.py +2 -2
  27. monoco/features/i18n/resources/skills/i18n_scan_workflow/SKILL.md +105 -0
  28. monoco/features/issue/commands.py +427 -21
  29. monoco/features/issue/core.py +104 -0
  30. monoco/features/issue/criticality.py +553 -0
  31. monoco/features/issue/domain/models.py +28 -2
  32. monoco/features/issue/engine/machine.py +65 -37
  33. monoco/features/issue/git_service.py +185 -0
  34. monoco/features/issue/linter.py +291 -62
  35. monoco/features/issue/models.py +91 -14
  36. monoco/features/issue/resources/en/SKILL.md +48 -0
  37. monoco/features/issue/resources/skills/issue_lifecycle_workflow/SKILL.md +159 -0
  38. monoco/features/issue/resources/zh/SKILL.md +50 -0
  39. monoco/features/issue/test_priority_integration.py +1 -0
  40. monoco/features/issue/validator.py +185 -65
  41. monoco/features/memo/__init__.py +4 -0
  42. monoco/features/memo/adapter.py +32 -0
  43. monoco/features/memo/cli.py +112 -0
  44. monoco/features/memo/core.py +146 -0
  45. monoco/features/memo/resources/skills/note_processing_workflow/SKILL.md +140 -0
  46. monoco/features/memo/resources/zh/AGENTS.md +8 -0
  47. monoco/features/memo/resources/zh/SKILL.md +75 -0
  48. monoco/features/spike/resources/skills/research_workflow/SKILL.md +121 -0
  49. monoco/main.py +6 -3
  50. {monoco_toolkit-0.3.5.dist-info → monoco_toolkit-0.3.9.dist-info}/METADATA +1 -1
  51. {monoco_toolkit-0.3.5.dist-info → monoco_toolkit-0.3.9.dist-info}/RECORD +56 -35
  52. monoco/features/scheduler/defaults.py +0 -54
  53. monoco/features/skills/__init__.py +0 -0
  54. monoco/features/skills/core.py +0 -102
  55. /monoco/core/{hooks.py → githooks.py} +0 -0
  56. /monoco/features/{scheduler → agent}/engines.py +0 -0
  57. {monoco_toolkit-0.3.5.dist-info → monoco_toolkit-0.3.9.dist-info}/WHEEL +0 -0
  58. {monoco_toolkit-0.3.5.dist-info → monoco_toolkit-0.3.9.dist-info}/entry_points.txt +0 -0
  59. {monoco_toolkit-0.3.5.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()
@@ -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.hooks import install_hooks
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__(self, root_dir: Path, skill_dir: Path):
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
- def __init__(self, root: Path, features: Optional[List] = None):
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
- Each feature should have:
215
- - monoco/features/{feature}/resources/{lang}/SKILL.md
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
- # Check for SKILL.md in language directories
242
- for lang_dir in resources_dir.iterdir():
243
- if lang_dir.is_dir() and (lang_dir / "SKILL.md").exists():
244
- # Create a Skill instance
245
- # We need to adapt the Skill class to work with feature resources
246
- skill = self._create_skill_from_feature(
247
- feature_name, resources_dir
248
- )
249
- if skill and skill.is_valid():
250
- # Use feature name as skill identifier
251
- skill_key = f"{feature_name}"
252
- if skill_key not in self.skills:
253
- self.skills[skill_key] = skill
254
- break # Only need to detect once per feature
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
- # Check if this skill has the requested language
327
- available_languages = skill.get_languages()
435
+ # Handle different skill types
436
+ skill_type = skill.get_type()
328
437
 
329
- if lang not in available_languages:
330
- console.print(
331
- f"[yellow]Skill {skill_name} does not have {lang} version, skipping[/yellow]"
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
- # Distribute the specific language version
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 _distribute_skill_language(
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
- ) -> None:
504
+ ) -> bool:
351
505
  """
352
- Distribute a specific language version of a skill.
506
+ Distribute a standard skill to target directory.
353
507
 
354
508
  Args:
355
- skill: Skill instance
356
- target_dir: Target directory (e.g., .cursor/skills/)
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)