monoco-toolkit 0.3.9__py3-none-any.whl → 0.3.10__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 (86) hide show
  1. monoco/core/config.py +7 -0
  2. monoco/core/hooks/builtin/git_cleanup.py +1 -1
  3. monoco/core/injection.py +63 -29
  4. monoco/core/integrations.py +2 -2
  5. monoco/core/output.py +5 -5
  6. monoco/core/registry.py +7 -1
  7. monoco/core/resource/__init__.py +5 -0
  8. monoco/core/resource/finder.py +98 -0
  9. monoco/core/resource/manager.py +91 -0
  10. monoco/core/resource/models.py +35 -0
  11. monoco/core/resources/en/{SKILL.md → skills/monoco_core/SKILL.md} +2 -0
  12. monoco/core/resources/zh/{SKILL.md → skills/monoco_core/SKILL.md} +2 -0
  13. monoco/core/skill_framework.py +292 -0
  14. monoco/core/skills.py +471 -371
  15. monoco/core/sync.py +73 -1
  16. monoco/core/workflow_converter.py +420 -0
  17. monoco/features/agent/__init__.py +2 -2
  18. monoco/features/agent/adapter.py +31 -0
  19. monoco/features/agent/apoptosis.py +44 -0
  20. monoco/features/agent/cli.py +101 -144
  21. monoco/features/agent/config.py +35 -21
  22. monoco/features/agent/defaults.py +6 -49
  23. monoco/features/agent/engines.py +32 -6
  24. monoco/features/agent/manager.py +6 -1
  25. monoco/features/agent/models.py +2 -2
  26. monoco/features/agent/resources/atoms/atom-code-dev.yaml +61 -0
  27. monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +73 -0
  28. monoco/features/agent/resources/atoms/atom-knowledge.yaml +55 -0
  29. monoco/features/agent/resources/atoms/atom-review.yaml +60 -0
  30. monoco/features/agent/resources/en/skills/flow_engineer/SKILL.md +94 -0
  31. monoco/features/agent/resources/en/skills/flow_manager/SKILL.md +93 -0
  32. monoco/features/agent/resources/en/skills/flow_planner/SKILL.md +85 -0
  33. monoco/features/agent/resources/en/skills/flow_reviewer/SKILL.md +114 -0
  34. monoco/features/agent/resources/roles/role-engineer.yaml +49 -0
  35. monoco/features/agent/resources/roles/role-manager.yaml +46 -0
  36. monoco/features/agent/resources/roles/role-planner.yaml +46 -0
  37. monoco/features/agent/resources/roles/role-reviewer.yaml +47 -0
  38. monoco/features/agent/resources/workflows/workflow-dev.yaml +83 -0
  39. monoco/features/agent/resources/workflows/workflow-issue-create.yaml +72 -0
  40. monoco/features/agent/resources/workflows/workflow-review.yaml +94 -0
  41. monoco/features/agent/resources/zh/skills/flow_planner/SKILL.md +259 -0
  42. monoco/features/agent/resources/zh/skills/flow_reviewer/SKILL.md +137 -0
  43. monoco/features/agent/worker.py +38 -2
  44. monoco/features/glossary/__init__.py +0 -0
  45. monoco/features/glossary/adapter.py +31 -0
  46. monoco/features/glossary/config.py +5 -0
  47. monoco/features/glossary/resources/en/AGENTS.md +29 -0
  48. monoco/features/glossary/resources/en/skills/monoco_glossary/SKILL.md +35 -0
  49. monoco/features/glossary/resources/zh/AGENTS.md +29 -0
  50. monoco/features/glossary/resources/zh/skills/monoco_glossary/SKILL.md +35 -0
  51. monoco/features/i18n/resources/en/skills/i18n_scan_workflow/SKILL.md +105 -0
  52. monoco/features/i18n/resources/en/{SKILL.md → skills/monoco_i18n/SKILL.md} +2 -0
  53. monoco/features/i18n/resources/zh/{SKILL.md → skills/monoco_i18n/SKILL.md} +2 -0
  54. monoco/features/issue/core.py +45 -6
  55. monoco/features/issue/engine/machine.py +5 -2
  56. monoco/features/issue/models.py +1 -0
  57. monoco/features/issue/resources/en/skills/issue_create_workflow/SKILL.md +167 -0
  58. monoco/features/issue/resources/en/skills/issue_develop_workflow/SKILL.md +224 -0
  59. monoco/features/issue/resources/en/skills/issue_lifecycle_workflow/SKILL.md +159 -0
  60. monoco/features/issue/resources/en/skills/issue_refine_workflow/SKILL.md +203 -0
  61. monoco/features/issue/resources/en/{SKILL.md → skills/monoco_issue/SKILL.md} +2 -0
  62. monoco/features/issue/resources/zh/skills/issue_create_workflow/SKILL.md +167 -0
  63. monoco/features/issue/resources/zh/skills/issue_develop_workflow/SKILL.md +224 -0
  64. monoco/features/issue/resources/zh/skills/issue_refine_workflow/SKILL.md +203 -0
  65. monoco/features/issue/resources/zh/{SKILL.md → skills/monoco_issue/SKILL.md} +2 -0
  66. monoco/features/memo/resources/en/skills/monoco_memo/SKILL.md +77 -0
  67. monoco/features/memo/resources/en/skills/note_processing_workflow/SKILL.md +140 -0
  68. monoco/features/memo/resources/zh/{SKILL.md → skills/monoco_memo/SKILL.md} +2 -0
  69. monoco/features/spike/resources/en/{SKILL.md → skills/monoco_spike/SKILL.md} +2 -0
  70. monoco/features/spike/resources/en/skills/research_workflow/SKILL.md +121 -0
  71. monoco/features/spike/resources/zh/{SKILL.md → skills/monoco_spike/SKILL.md} +2 -0
  72. monoco_toolkit-0.3.10.dist-info/METADATA +124 -0
  73. monoco_toolkit-0.3.10.dist-info/RECORD +156 -0
  74. monoco/features/agent/reliability.py +0 -106
  75. monoco/features/agent/resources/skills/flow_reviewer/SKILL.md +0 -114
  76. monoco_toolkit-0.3.9.dist-info/METADATA +0 -127
  77. monoco_toolkit-0.3.9.dist-info/RECORD +0 -115
  78. /monoco/features/agent/resources/{skills → zh/skills}/flow_engineer/SKILL.md +0 -0
  79. /monoco/features/agent/resources/{skills → zh/skills}/flow_manager/SKILL.md +0 -0
  80. /monoco/features/i18n/resources/{skills → zh/skills}/i18n_scan_workflow/SKILL.md +0 -0
  81. /monoco/features/issue/resources/{skills → zh/skills}/issue_lifecycle_workflow/SKILL.md +0 -0
  82. /monoco/features/memo/resources/{skills → zh/skills}/note_processing_workflow/SKILL.md +0 -0
  83. /monoco/features/spike/resources/{skills → zh/skills}/research_workflow/SKILL.md +0 -0
  84. {monoco_toolkit-0.3.9.dist-info → monoco_toolkit-0.3.10.dist-info}/WHEEL +0 -0
  85. {monoco_toolkit-0.3.9.dist-info → monoco_toolkit-0.3.10.dist-info}/entry_points.txt +0 -0
  86. {monoco_toolkit-0.3.9.dist-info → monoco_toolkit-0.3.10.dist-info}/licenses/LICENSE +0 -0
monoco/core/skills.py CHANGED
@@ -2,30 +2,44 @@
2
2
  Skill Manager for Monoco Toolkit.
3
3
 
4
4
  This module provides centralized management and distribution of Agent Skills
5
- following the agentskills.io standard.
5
+ following the agentskills.io standard and the three-level architecture:
6
+ - Atom Skills: Atomic capabilities
7
+ - Workflow Skills: Orchestration of atoms
8
+ - Role Skills: Configuration layer
6
9
 
7
10
  Key Responsibilities:
8
- 1. Discover skills from the source directory (Toolkit/skills/)
9
- 2. Validate skill structure and metadata (YAML frontmatter)
10
- 3. Distribute skills to target agent framework directories
11
- 4. Support i18n for skill content
12
- 5. Support multi-skill architecture (1 Feature : N Skills)
11
+ 1. Discover skills from resources/{lang}/skills/ directory (legacy)
12
+ 2. Discover skills from resources/atoms/, workflows/, roles/ (new three-level)
13
+ 3. Validate skill structure and metadata
14
+ 4. Distribute skills to target agent framework directories
15
+ 5. Support i18n for skill content
13
16
  """
14
17
 
15
18
  import shutil
16
19
  import hashlib
17
20
  from pathlib import Path
18
- from typing import Dict, List, Optional, Set
21
+ from typing import Dict, List, Optional, Set, Union
19
22
  from pydantic import BaseModel, Field, ValidationError
20
23
  from rich.console import Console
21
24
  import yaml
22
25
 
26
+ # Import new skill framework
27
+ from monoco.core.skill_framework import (
28
+ SkillLoader,
29
+ AtomSkillMetadata,
30
+ WorkflowSkillMetadata,
31
+ RoleSkillMetadata,
32
+ SkillType,
33
+ SkillMode,
34
+ )
35
+ from monoco.core.workflow_converter import WorkflowDistributor
36
+
23
37
  console = Console()
24
38
 
25
39
 
26
40
  class SkillMetadata(BaseModel):
27
41
  """
28
- Skill metadata from YAML frontmatter.
42
+ Legacy skill metadata from YAML frontmatter.
29
43
  Based on agentskills.io standard.
30
44
  """
31
45
 
@@ -39,81 +53,78 @@ class SkillMetadata(BaseModel):
39
53
  default=None, description="Skill tags for categorization"
40
54
  )
41
55
  type: Optional[str] = Field(
42
- default="standard", description="Skill type: standard, flow, etc."
56
+ default="standard", description="Skill type: standard, flow, workflow, atom, role"
43
57
  )
44
58
  role: Optional[str] = Field(
45
59
  default=None, description="Role identifier for Flow Skills (e.g., engineer, manager)"
46
60
  )
61
+ domain: Optional[str] = Field(
62
+ default=None, description="Domain identifier for Workflow Skills (e.g., issue, spike)"
63
+ )
47
64
 
48
65
 
49
66
  class Skill:
50
67
  """
51
68
  Represents a single skill with its metadata and file paths.
69
+
70
+ Directory structure: resources/{lang}/skills/{name}/SKILL.md
71
+ Example: resources/en/skills/monoco_core/SKILL.md
52
72
  """
53
73
 
54
74
  def __init__(
55
75
  self,
56
76
  root_dir: Path,
57
- skill_dir: Path,
58
- name: Optional[str] = None,
59
- skill_file: Optional[Path] = None,
77
+ skill_name: str,
78
+ resources_dir: Path,
60
79
  ):
61
- """
62
- Initialize a Skill instance.
63
-
64
- Args:
65
- root_dir: Project root directory
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)
69
- """
70
80
  self.root_dir = root_dir
71
- self.skill_dir = skill_dir
72
- self.name = name or skill_dir.name
73
- self.skill_file = skill_file or (skill_dir / "SKILL.md")
81
+ self.skill_name = skill_name
82
+ self.resources_dir = resources_dir
83
+ self.name = skill_name
74
84
  self.metadata: Optional[SkillMetadata] = None
75
85
  self._load_metadata()
76
86
 
77
87
  def _load_metadata(self) -> None:
78
88
  """Load and validate skill metadata from SKILL.md frontmatter."""
79
- # Try to load from language subdirectories first (Feature resources pattern)
80
- # Then fallback to root SKILL.md (legacy pattern)
81
- skill_file_to_use = None
82
-
83
- # Check language subdirectories
84
- if self.skill_dir.exists():
85
- for item in sorted(self.skill_dir.iterdir()):
86
- if item.is_dir() and len(item.name) == 2: # 2-letter lang code
87
- candidate = item / "SKILL.md"
88
- if candidate.exists():
89
- skill_file_to_use = candidate
90
- break
91
-
92
- # Fallback to root SKILL.md
93
- if not skill_file_to_use and self.skill_file.exists():
94
- skill_file_to_use = self.skill_file
95
-
96
- if not skill_file_to_use:
89
+ skill_file = self._get_first_available_skill_file()
90
+
91
+ if not skill_file:
97
92
  return
98
93
 
99
94
  try:
100
- content = skill_file_to_use.read_text(encoding="utf-8")
101
- # Extract YAML frontmatter
95
+ content = skill_file.read_text(encoding="utf-8")
102
96
  if content.startswith("---"):
103
97
  parts = content.split("---", 2)
104
98
  if len(parts) >= 3:
105
99
  frontmatter = parts[1].strip()
106
100
  metadata_dict = yaml.safe_load(frontmatter)
107
-
108
- # Validate against schema
109
101
  self.metadata = SkillMetadata(**metadata_dict)
110
102
  except ValidationError as e:
111
- console.print(f"[red]Invalid metadata in {skill_file_to_use}: {e}[/red]")
103
+ console.print(f"[red]Invalid metadata in {skill_file}: {e}[/red]")
112
104
  except Exception as e:
113
105
  console.print(
114
- f"[yellow]Warning: Failed to parse metadata from {skill_file_to_use}: {e}[/yellow]"
106
+ f"[yellow]Warning: Failed to parse metadata from {skill_file}: {e}[/yellow]"
115
107
  )
116
108
 
109
+ def _get_first_available_skill_file(self) -> Optional[Path]:
110
+ """Get the first available SKILL.md file from language subdirectories."""
111
+ if not self.resources_dir.exists():
112
+ return None
113
+
114
+ for lang_dir in sorted(self.resources_dir.iterdir()):
115
+ if lang_dir.is_dir() and len(lang_dir.name) == 2:
116
+ skill_file = lang_dir / "skills" / self.skill_name / "SKILL.md"
117
+ if skill_file.exists():
118
+ return skill_file
119
+ return None
120
+
121
+ def get_skill_file(self, lang: str) -> Optional[Path]:
122
+ """Get the SKILL.md file path for a specific language."""
123
+ skill_file = self.resources_dir / lang / "skills" / self.skill_name / "SKILL.md"
124
+ if skill_file.exists():
125
+ return skill_file
126
+ return None
127
+
117
128
  def is_valid(self) -> bool:
118
129
  """Check if the skill has valid metadata."""
119
130
  return self.metadata is not None
@@ -127,49 +138,25 @@ class Skill:
127
138
  return self.metadata.role if self.metadata else None
128
139
 
129
140
  def get_languages(self) -> List[str]:
130
- """
131
- Detect available language versions of this skill.
132
-
133
- Returns:
134
- List of language codes (e.g., ['en', 'zh'])
135
- """
141
+ """Detect available language versions of this skill."""
136
142
  languages = []
137
143
 
138
- # Check for language subdirectories (Feature resources pattern)
139
- # resources/en/SKILL.md, resources/zh/SKILL.md
140
- for item in self.skill_dir.iterdir():
141
- if item.is_dir() and len(item.name) == 2: # Assume 2-letter lang codes
142
- lang_skill_file = item / "SKILL.md"
143
- if lang_skill_file.exists():
144
- languages.append(item.name)
144
+ if not self.resources_dir.exists():
145
+ return languages
145
146
 
146
- # Fallback: check for root SKILL.md (legacy Toolkit/skills pattern)
147
- # We don't assume a default language, just return what we found
148
- if not languages and self.skill_file.exists():
149
- # For legacy pattern, we can't determine the language from structure
150
- # Return empty to indicate this skill uses legacy pattern
151
- pass
147
+ for lang_dir in self.resources_dir.iterdir():
148
+ if lang_dir.is_dir() and len(lang_dir.name) == 2:
149
+ lang_skill_file = lang_dir / "skills" / self.skill_name / "SKILL.md"
150
+ if lang_skill_file.exists():
151
+ languages.append(lang_dir.name)
152
152
 
153
- return languages
153
+ return sorted(languages)
154
154
 
155
155
  def get_checksum(self, lang: str) -> str:
156
- """
157
- Calculate checksum for the skill content.
158
-
159
- Args:
160
- lang: Language code
156
+ """Calculate checksum for the skill content."""
157
+ target_file = self.get_skill_file(lang)
161
158
 
162
- Returns:
163
- SHA256 checksum of the skill file
164
- """
165
- # Try language subdirectory first (Feature resources pattern)
166
- target_file = self.skill_dir / lang / "SKILL.md"
167
-
168
- # Fallback to root SKILL.md (legacy pattern)
169
- if not target_file.exists():
170
- target_file = self.skill_file
171
-
172
- if not target_file.exists():
159
+ if not target_file:
173
160
  return ""
174
161
 
175
162
  content = target_file.read_bytes()
@@ -178,17 +165,25 @@ class Skill:
178
165
 
179
166
  class SkillManager:
180
167
  """
181
- Central manager for Monoco skills.
182
-
183
- Responsibilities:
184
- - Collect skills from Feature resources (standard + multi-skill architecture)
185
- - Validate skill structure
186
- - Distribute skills to agent framework directories
187
- - Support Flow Skills with custom prefixes
168
+ Central manager for Monoco skills supporting both legacy and three-level architecture.
169
+
170
+ Three-Level Architecture:
171
+ - Atom Skills: resources/atoms/*.yaml
172
+ - Workflow Skills: resources/workflows/*.yaml
173
+ - Role Skills: resources/roles/*.yaml
174
+
175
+ Legacy Architecture:
176
+ - Standard Skills: resources/{lang}/skills/{name}/SKILL.md
177
+ - Flow Skills: resources/{lang}/skills/flow_*/SKILL.md
188
178
  """
189
179
 
190
180
  # Default prefix for flow skills
191
181
  FLOW_SKILL_PREFIX = "monoco_flow_"
182
+
183
+ # Prefix for three-level architecture skills
184
+ ATOM_PREFIX = "monoco_atom_"
185
+ WORKFLOW_PREFIX = "monoco_workflow_"
186
+ ROLE_PREFIX = "monoco_role_"
192
187
 
193
188
  def __init__(
194
189
  self,
@@ -196,66 +191,44 @@ class SkillManager:
196
191
  features: Optional[List] = None,
197
192
  flow_skill_prefix: str = FLOW_SKILL_PREFIX,
198
193
  ):
199
- """
200
- Initialize SkillManager.
201
-
202
- Args:
203
- root: Project root directory
204
- features: List of MonocoFeature instances (if None, will load from registry)
205
- flow_skill_prefix: Prefix for flow skill directory names
206
- """
207
194
  self.root = root
208
195
  self.features = features or []
209
196
  self.flow_skill_prefix = flow_skill_prefix
197
+
198
+ # Legacy skills
210
199
  self.skills: Dict[str, Skill] = {}
211
-
200
+
201
+ # New three-level architecture skills
202
+ self._skill_loaders: Dict[str, SkillLoader] = {}
203
+ self._atoms: Dict[str, AtomSkillMetadata] = {}
204
+ self._workflows: Dict[str, WorkflowSkillMetadata] = {}
205
+ self._roles: Dict[str, RoleSkillMetadata] = {}
206
+
207
+ # Load legacy skills
212
208
  if self.features:
213
209
  self._discover_skills_from_features()
210
+ self._discover_core_skills()
211
+
212
+ # Load new three-level skills
213
+ self._discover_three_level_skills()
214
214
 
215
- # Also discover core skill (monoco/core/resources/)
216
- self._discover_core_skill()
217
-
218
- def _discover_core_skill(self) -> None:
219
- """
220
- Discover skill from monoco/core/resources/.
221
-
222
- Core is special - it's not a Feature but still has a skill.
223
- """
215
+ def _discover_core_skills(self) -> None:
216
+ """Discover skills from monoco/core/resources/{lang}/skills/."""
224
217
  core_resources_dir = self.root / "monoco" / "core" / "resources"
225
218
 
226
219
  if not core_resources_dir.exists():
227
220
  return
228
221
 
229
- # Check for SKILL.md in language directories
230
- for lang_dir in core_resources_dir.iterdir():
231
- if lang_dir.is_dir() and (lang_dir / "SKILL.md").exists():
232
- skill = Skill(self.root, core_resources_dir)
233
-
234
- # Use the skill's metadata name if available
235
- if skill.metadata and skill.metadata.name:
236
- skill.name = skill.metadata.name.replace("-", "_")
237
- else:
238
- skill.name = "monoco_core"
239
-
240
- if skill.is_valid():
241
- self.skills[skill.name] = skill
242
- break # Only need to detect once
222
+ self._discover_skills_in_resources(core_resources_dir, "monoco_core")
243
223
 
244
224
  def _discover_skills_from_features(self) -> None:
245
- """
246
- Discover skills from Feature resources.
247
-
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
251
- """
225
+ """Discover skills from Feature resources."""
252
226
  from monoco.core.feature import MonocoFeature
253
227
 
254
228
  for feature in self.features:
255
229
  if not isinstance(feature, MonocoFeature):
256
230
  continue
257
231
 
258
- # Determine feature module path
259
232
  module_parts = feature.__class__.__module__.split(".")
260
233
  if (
261
234
  len(module_parts) >= 3
@@ -264,277 +237,247 @@ class SkillManager:
264
237
  ):
265
238
  feature_name = module_parts[2]
266
239
 
267
- # Construct path to feature resources
268
240
  feature_dir = self.root / "monoco" / "features" / feature_name
269
241
  resources_dir = feature_dir / "resources"
270
242
 
271
243
  if not resources_dir.exists():
272
244
  continue
273
245
 
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).
246
+ self._discover_skills_in_resources(resources_dir, feature_name)
283
247
 
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():
248
+ def _discover_skills_in_resources(self, resources_dir: Path, feature_name: str) -> None:
249
+ """Discover skills from resources/{lang}/skills/ directories."""
250
+ if not resources_dir.exists():
290
251
  return
291
252
 
292
- for skill_subdir in skills_dir.iterdir():
293
- if not skill_subdir.is_dir():
253
+ skill_names: Set[str] = set()
254
+
255
+ for lang_dir in resources_dir.iterdir():
256
+ if not lang_dir.is_dir() or len(lang_dir.name) != 2:
294
257
  continue
295
258
 
296
- skill_file = skill_subdir / "SKILL.md"
297
- if not skill_file.exists():
259
+ skills_dir = lang_dir / "skills"
260
+ if not skills_dir.exists():
298
261
  continue
299
262
 
300
- # Create skill instance
263
+ for skill_subdir in skills_dir.iterdir():
264
+ if skill_subdir.is_dir() and (skill_subdir / "SKILL.md").exists():
265
+ skill_names.add(skill_subdir.name)
266
+
267
+ for skill_name in skill_names:
301
268
  skill = Skill(
302
269
  root_dir=self.root,
303
- skill_dir=skill_subdir,
304
- name=skill_subdir.name,
305
- skill_file=skill_file,
270
+ skill_name=skill_name,
271
+ resources_dir=resources_dir,
306
272
  )
307
273
 
308
274
  if not skill.is_valid():
275
+ console.print(
276
+ f"[yellow]Warning: Skill {skill_name} has invalid metadata, skipping[/yellow]"
277
+ )
309
278
  continue
310
279
 
311
- # Determine skill key based on type
312
280
  skill_type = skill.get_type()
313
281
  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}"
282
+ name = skill_name
283
+ if name.startswith("flow_"):
284
+ name = name[5:]
285
+ skill_key = f"{self.flow_skill_prefix}{name}"
316
286
  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}"
287
+ skill_key = f"{feature_name}_{skill_name}"
320
288
 
321
- # Override name for distribution
322
289
  skill.name = skill_key
323
290
  self.skills[skill_key] = skill
324
291
 
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
344
-
345
- def _create_skill_from_feature(
346
- self, feature_name: str, resources_dir: Path
347
- ) -> Optional[Skill]:
348
- """
349
- Create a Skill instance from a feature's resources directory.
350
-
351
- Args:
352
- feature_name: Name of the feature (e.g., 'issue', 'spike')
353
- resources_dir: Path to the feature's resources directory
354
-
355
- Returns:
356
- Skill instance or None if creation fails
357
- """
358
- # Use the resources directory as the skill directory
359
- skill = Skill(self.root, resources_dir)
360
-
361
- # Use the skill's metadata name if available (e.g., 'monoco-issue')
362
- # Convert to snake_case for directory name (e.g., 'monoco_issue')
363
- if skill.metadata and skill.metadata.name:
364
- # Convert kebab-case to snake_case for directory name
365
- skill.name = skill.metadata.name.replace("-", "_")
366
- else:
367
- # Fallback to feature name
368
- skill.name = f"monoco_{feature_name}"
369
-
370
- return skill
292
+ def _discover_three_level_skills(self) -> None:
293
+ """Discover skills from the new three-level architecture."""
294
+ # Discover from agent feature resources
295
+ agent_resources_dir = self.root / "monoco" / "features" / "agent" / "resources"
296
+
297
+ if not agent_resources_dir.exists():
298
+ return
299
+
300
+ loader = SkillLoader(agent_resources_dir)
301
+ loader.load_all()
302
+
303
+ self._skill_loaders["agent"] = loader
304
+
305
+ # Merge loaded skills
306
+ for name, atom in loader._atoms.items():
307
+ self._atoms[f"{self.ATOM_PREFIX}{name}"] = atom
308
+
309
+ for name, workflow in loader._workflows.items():
310
+ self._workflows[f"{self.WORKFLOW_PREFIX}{name}"] = workflow
311
+
312
+ for name, role in loader._roles.items():
313
+ self._roles[f"{self.ROLE_PREFIX}{name}"] = role
314
+
315
+ # ========================================================================
316
+ # Legacy Skill API (backward compatible)
317
+ # ========================================================================
371
318
 
372
319
  def list_skills(self) -> List[Skill]:
373
- """
374
- Get all available skills.
375
-
376
- Returns:
377
- List of Skill instances
378
- """
320
+ """Get all available legacy skills."""
379
321
  return list(self.skills.values())
380
322
 
381
323
  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
- """
324
+ """Get skills filtered by type."""
391
325
  return [s for s in self.skills.values() if s.get_type() == skill_type]
392
326
 
393
327
  def get_skill(self, name: str) -> Optional[Skill]:
394
- """
395
- Get a specific skill by name.
396
-
397
- Args:
398
- name: Skill name
399
-
400
- Returns:
401
- Skill instance or None if not found
402
- """
328
+ """Get a specific legacy skill by name."""
403
329
  return self.skills.get(name)
404
330
 
405
331
  def get_flow_skills(self) -> List[Skill]:
406
- """
407
- Get all Flow Skills.
408
-
409
- Returns:
410
- List of Flow Skill instances
411
- """
332
+ """Get all Flow Skills."""
412
333
  return self.list_skills_by_type("flow")
413
334
 
335
+ # ========================================================================
336
+ # Three-Level Architecture API
337
+ # ========================================================================
338
+
339
+ def get_atom(self, name: Optional[str]) -> Optional[AtomSkillMetadata]:
340
+ """Get an atom skill by name."""
341
+ if not name:
342
+ return None
343
+ # Handle both prefixed and unprefixed names
344
+ if not name.startswith(self.ATOM_PREFIX):
345
+ name = f"{self.ATOM_PREFIX}{name}"
346
+ return self._atoms.get(name)
347
+
348
+ def get_workflow(self, name: str) -> Optional[WorkflowSkillMetadata]:
349
+ """Get a workflow skill by name."""
350
+ if not name.startswith(self.WORKFLOW_PREFIX):
351
+ name = f"{self.WORKFLOW_PREFIX}{name}"
352
+ return self._workflows.get(name)
353
+
354
+ def get_role(self, name: str) -> Optional[RoleSkillMetadata]:
355
+ """Get a role skill by name."""
356
+ if not name.startswith(self.ROLE_PREFIX):
357
+ name = f"{self.ROLE_PREFIX}{name}"
358
+ return self._roles.get(name)
359
+
360
+ def list_atoms(self) -> List[AtomSkillMetadata]:
361
+ """List all atom skills."""
362
+ return list(self._atoms.values())
363
+
364
+ def list_workflows(self) -> List[WorkflowSkillMetadata]:
365
+ """List all workflow skills."""
366
+ return list(self._workflows.values())
367
+
368
+ def list_roles(self) -> List[RoleSkillMetadata]:
369
+ """List all role skills."""
370
+ return list(self._roles.values())
371
+
372
+ def resolve_role_workflow(self, role_name: str) -> Optional[WorkflowSkillMetadata]:
373
+ """Resolve a role to its workflow."""
374
+ role = self.get_role(role_name)
375
+ if role:
376
+ return self.get_workflow(role.workflow)
377
+ return None
378
+
379
+ def validate_workflow(self, workflow_name: str) -> List[str]:
380
+ """Validate a workflow's dependencies are satisfied."""
381
+ errors = []
382
+ workflow = self.get_workflow(workflow_name)
383
+ if not workflow:
384
+ return [f"Workflow '{workflow_name}' not found"]
385
+
386
+ for dep in workflow.dependencies:
387
+ if not self.get_atom(dep):
388
+ errors.append(f"Missing atom skill dependency: {dep}")
389
+
390
+ for stage in workflow.stages:
391
+ # Skip virtual stages (decision points without atom skills)
392
+ if not stage.atom_skill:
393
+ continue
394
+
395
+ atom = self.get_atom(stage.atom_skill)
396
+ if not atom:
397
+ errors.append(f"Stage '{stage.name}' uses unknown atom skill: {stage.atom_skill}")
398
+ continue
399
+
400
+ if stage.operation:
401
+ op_names = [op.name for op in atom.operations]
402
+ if stage.operation not in op_names:
403
+ errors.append(
404
+ f"Stage '{stage.name}' uses unknown operation '{stage.operation}' "
405
+ f"in atom skill '{stage.atom_skill}'"
406
+ )
407
+
408
+ return errors
409
+
410
+ # ========================================================================
411
+ # Distribution
412
+ # ========================================================================
413
+
414
414
  def distribute(
415
415
  self, target_dir: Path, lang: str, force: bool = False
416
416
  ) -> Dict[str, bool]:
417
417
  """
418
- Distribute skills to a target directory.
419
-
420
- Args:
421
- target_dir: Target directory for skill distribution (e.g., .cursor/skills/)
422
- lang: Language code to distribute (e.g., 'en', 'zh')
423
- force: Force overwrite even if checksum matches
424
-
425
- Returns:
426
- Dictionary mapping skill names to success status
418
+ Distribute all skills to a target directory.
419
+
420
+ This includes:
421
+ - Legacy skills (SKILL.md files)
422
+ - Three-level skills (generated SKILL.md from YAML)
427
423
  """
428
424
  results = {}
429
425
 
430
- # Ensure target directory exists
431
426
  target_dir.mkdir(parents=True, exist_ok=True)
432
427
 
428
+ # Distribute legacy skills
433
429
  for skill_name, skill in self.skills.items():
434
430
  try:
435
- # Handle different skill types
436
- skill_type = skill.get_type()
437
-
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
454
- )
455
-
431
+ success = self._distribute_legacy_skill(skill, target_dir, lang, force)
456
432
  results[skill_name] = success
457
-
458
433
  except Exception as e:
459
434
  console.print(
460
435
  f"[red]Failed to distribute skill {skill_name}: {e}[/red]"
461
436
  )
462
437
  results[skill_name] = False
463
438
 
464
- return results
465
-
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)
439
+ # Distribute three-level skills (generate SKILL.md from YAML)
440
+ for role_name, role in self._roles.items():
441
+ try:
442
+ success = self._distribute_role_skill(role, target_dir, lang, force)
443
+ results[role_name] = success
444
+ except Exception as e:
445
+ console.print(
446
+ f"[red]Failed to distribute role skill {role_name}: {e}[/red]"
447
+ )
448
+ results[role_name] = False
497
449
 
498
- shutil.copytree(skill.skill_dir, target_skill_dir)
499
- console.print(f"[green] ✓ Distributed {skill.name}/[/green]")
500
- return True
450
+ return results
501
451
 
502
- def _distribute_standard_skill(
452
+ def _distribute_legacy_skill(
503
453
  self, skill: Skill, target_dir: Path, lang: str, force: bool
504
454
  ) -> bool:
505
- """
506
- Distribute a standard skill to target directory.
507
-
508
- Args:
509
- skill: Standard Skill instance
510
- target_dir: Target directory
511
- lang: Language code
512
- force: Force overwrite
513
-
514
- Returns:
515
- True if distribution successful
516
- """
517
- # Determine source file (try language subdirectory first)
518
- source_file = skill.skill_dir / lang / "SKILL.md"
519
-
520
- # Fallback to root SKILL.md (legacy pattern)
521
- if not source_file.exists():
522
- source_file = skill.skill_file
455
+ """Distribute a legacy skill to target directory."""
456
+ available_languages = skill.get_languages()
457
+
458
+ if lang not in available_languages:
459
+ if 'en' in available_languages:
460
+ console.print(
461
+ f"[yellow]Skill {skill.name} does not have {lang} version, falling back to 'en'[/yellow]"
462
+ )
463
+ lang = 'en'
464
+ else:
465
+ console.print(
466
+ f"[red]Skill {skill.name} does not have {lang} or 'en' version, skipping[/red]"
467
+ )
468
+ return False
523
469
 
524
- if not source_file.exists():
470
+ source_file = skill.get_skill_file(lang)
471
+ if not source_file:
525
472
  console.print(
526
- f"[yellow]Source file not found for {skill.name}/{lang}[/yellow]"
473
+ f"[red]Source file not found for {skill.name}/{lang}[/red]"
527
474
  )
528
475
  return False
529
476
 
530
- # Target path: {target_dir}/{skill_name}/SKILL.md (no language subdirectory)
531
477
  target_skill_dir = target_dir / skill.name
532
-
533
- # Create target directory
534
478
  target_skill_dir.mkdir(parents=True, exist_ok=True)
535
479
  target_file = target_skill_dir / "SKILL.md"
536
480
 
537
- # Check if update is needed
538
481
  if target_file.exists() and not force:
539
482
  source_checksum = skill.get_checksum(lang)
540
483
  target_content = target_file.read_bytes()
@@ -544,63 +487,181 @@ class SkillManager:
544
487
  console.print(f"[dim] = {skill.name}/SKILL.md is up to date[/dim]")
545
488
  return True
546
489
 
547
- # Copy the file
548
490
  shutil.copy2(source_file, target_file)
549
491
  console.print(f"[green] ✓ Distributed {skill.name}/SKILL.md ({lang})[/green]")
550
492
 
551
- # Copy additional resources if they exist
552
- self._copy_skill_resources(skill.skill_dir, target_skill_dir, lang)
493
+ self._copy_skill_resources(skill.resources_dir, skill.skill_name, target_skill_dir, lang)
553
494
 
554
495
  return True
555
496
 
556
- def _copy_skill_resources(
557
- self, source_dir: Path, target_dir: Path, lang: str
558
- ) -> None:
497
+ def _distribute_role_skill(
498
+ self, role: RoleSkillMetadata, target_dir: Path, lang: str, force: bool
499
+ ) -> bool:
559
500
  """
560
- Copy additional skill resources (scripts, examples, etc.).
561
-
562
- Args:
563
- source_dir: Source skill directory
564
- target_dir: Target skill directory
565
- lang: Language code
501
+ Generate and distribute a role skill as SKILL.md.
502
+
503
+ This generates a comprehensive SKILL.md that includes:
504
+ - Role configuration
505
+ - Workflow stages
506
+ - Atom operations
566
507
  """
567
- # Define resource directories to copy
568
- resource_dirs = ["scripts", "examples", "resources"]
508
+ target_skill_dir = target_dir / role.name
509
+ target_file = target_skill_dir / "SKILL.md"
569
510
 
570
- # Try language subdirectory first (Feature resources pattern)
571
- source_base = source_dir / lang
511
+ # Check if update is needed
512
+ if target_file.exists() and not force:
513
+ # Simple check: compare role version
514
+ try:
515
+ existing_content = target_file.read_text()
516
+ if f"version: {role.version}" in existing_content:
517
+ console.print(f"[dim] = {role.name}/SKILL.md is up to date[/dim]")
518
+ return True
519
+ except:
520
+ pass
521
+
522
+ # Generate SKILL.md content
523
+ content = self._generate_role_skill_content(role, lang)
524
+
525
+ target_skill_dir.mkdir(parents=True, exist_ok=True)
526
+ target_file.write_text(content, encoding="utf-8")
527
+
528
+ console.print(f"[green] ✓ Generated {role.name}/SKILL.md ({lang})[/green]")
529
+ return True
530
+
531
+ def _generate_role_skill_content(self, role: RoleSkillMetadata, lang: str) -> str:
532
+ """Generate SKILL.md content from role metadata."""
533
+ workflow = self.get_workflow(role.workflow)
534
+
535
+ lines = [
536
+ "---",
537
+ f"name: {role.name}",
538
+ f"description: {role.description}",
539
+ f"type: role",
540
+ f"version: {role.version}",
541
+ ]
542
+ if role.author:
543
+ lines.append(f"author: {role.author}")
544
+ lines.append("---")
545
+ lines.append("")
546
+
547
+ # Title
548
+ role_title = role.name.replace("role-", "").replace("-", " ").title()
549
+ lines.append(f"# {role_title} Role")
550
+ lines.append("")
551
+ lines.append(role.description)
552
+ lines.append("")
553
+
554
+ # System prompt
555
+ if role.system_prompt:
556
+ lines.append(role.system_prompt)
557
+ lines.append("")
558
+
559
+ # Workflow section
560
+ if workflow:
561
+ lines.append(f"## Workflow: {workflow.name}")
562
+ lines.append("")
563
+ lines.append(workflow.description)
564
+ lines.append("")
565
+
566
+ # Mode configuration
567
+ lines.append("### Execution Mode")
568
+ lines.append("")
569
+ lines.append(f"**Default Mode**: {role.default_mode.value}")
570
+ lines.append("")
571
+
572
+ if workflow.mode_config:
573
+ for mode, config in workflow.mode_config.items():
574
+ lines.append(f"#### {mode.value.title()} Mode")
575
+ lines.append("")
576
+ lines.append(config.behavior)
577
+ lines.append("")
578
+ if config.pause_on:
579
+ lines.append("**Pause Points**:")
580
+ for pause in config.pause_on:
581
+ lines.append(f"- {pause}")
582
+ lines.append("")
583
+
584
+ # Stages
585
+ lines.append("### Workflow Stages")
586
+ lines.append("")
587
+
588
+ for i, stage in enumerate(workflow.stages, 1):
589
+ lines.append(f"#### {i}. {stage.name.title()}")
590
+ lines.append("")
591
+ if stage.description:
592
+ lines.append(stage.description)
593
+ lines.append("")
594
+
595
+ # Atom operation details (skip for virtual stages)
596
+ if stage.atom_skill and stage.operation:
597
+ atom = self.get_atom(stage.atom_skill)
598
+ if atom:
599
+ operation = next(
600
+ (op for op in atom.operations if op.name == stage.operation),
601
+ None
602
+ )
603
+ if operation:
604
+ lines.append(f"**Operation**: `{atom.name}.{operation.name}`")
605
+ lines.append("")
606
+ lines.append(f"{operation.description}")
607
+ lines.append("")
608
+
609
+ if operation.reminder:
610
+ lines.append(f"> 💡 **Reminder**: {operation.reminder}")
611
+ lines.append("")
612
+
613
+ if operation.checkpoints:
614
+ lines.append("**Checkpoints**:")
615
+ for checkpoint in operation.checkpoints:
616
+ lines.append(f"- [ ] {checkpoint}")
617
+ lines.append("")
618
+
619
+ if stage.reminder:
620
+ lines.append(f"> ⚠️ **Stage Reminder**: {stage.reminder}")
621
+ lines.append("")
622
+
623
+ # Preferences
624
+ if role.preferences:
625
+ lines.append("## Mindset & Preferences")
626
+ lines.append("")
627
+ for pref in role.preferences:
628
+ lines.append(f"- {pref}")
629
+ lines.append("")
630
+
631
+ return "\n".join(lines)
632
+
633
+ def _copy_skill_resources(
634
+ self, resources_dir: Path, skill_name: str, target_dir: Path, lang: str
635
+ ) -> None:
636
+ """Copy additional skill resources."""
637
+ resource_dirs = ["scripts", "examples", "resources"]
638
+ source_base = resources_dir / lang / "skills" / skill_name
572
639
 
573
- # Fallback to root directory (legacy pattern)
574
640
  if not source_base.exists():
575
- source_base = source_dir
641
+ return
576
642
 
577
643
  for resource_name in resource_dirs:
578
644
  source_resource = source_base / resource_name
579
645
  if source_resource.exists() and source_resource.is_dir():
580
646
  target_resource = target_dir / resource_name
581
647
 
582
- # Remove existing and copy fresh
583
648
  if target_resource.exists():
584
649
  shutil.rmtree(target_resource)
585
650
 
586
651
  shutil.copytree(source_resource, target_resource)
587
652
  console.print(
588
- f"[dim] Copied {resource_name}/ for {source_dir.name}/{lang}[/dim]"
653
+ f"[dim] Copied {resource_name}/ for {skill_name}/{lang}[/dim]"
589
654
  )
590
655
 
591
656
  def cleanup(self, target_dir: Path) -> None:
592
- """
593
- Remove distributed skills from a target directory.
594
-
595
- Args:
596
- target_dir: Target directory to clean
597
- """
657
+ """Remove distributed skills from a target directory."""
598
658
  if not target_dir.exists():
599
659
  console.print(f"[dim]Target directory does not exist: {target_dir}[/dim]")
600
660
  return
601
661
 
602
662
  removed_count = 0
603
663
 
664
+ # Remove legacy skills
604
665
  for skill_name in self.skills.keys():
605
666
  skill_target = target_dir / skill_name
606
667
  if skill_target.exists():
@@ -608,7 +669,14 @@ class SkillManager:
608
669
  console.print(f"[green] ✓ Removed {skill_name}[/green]")
609
670
  removed_count += 1
610
671
 
611
- # Remove empty parent directory if no skills remain
672
+ # Remove three-level skills
673
+ for role_name in self._roles.keys():
674
+ skill_target = target_dir / role_name
675
+ if skill_target.exists():
676
+ shutil.rmtree(skill_target)
677
+ console.print(f"[green] ✓ Removed {role_name}[/green]")
678
+ removed_count += 1
679
+
612
680
  if target_dir.exists() and not any(target_dir.iterdir()):
613
681
  target_dir.rmdir()
614
682
  console.print(f"[dim] Removed empty directory: {target_dir}[/dim]")
@@ -617,26 +685,58 @@ class SkillManager:
617
685
  console.print(f"[dim]No skills to remove from {target_dir}[/dim]")
618
686
 
619
687
  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
- """
688
+ """Get list of available flow skill commands."""
629
689
  commands = []
690
+
691
+ # Legacy flow skills
630
692
  for skill in self.get_flow_skills():
631
693
  role = skill.get_role()
632
694
  if role:
633
695
  commands.append(f"/flow:{role}")
634
696
  else:
635
- # Extract role from skill name
636
- # e.g., monoco_flow_engineer -> engineer
637
697
  name = skill.name
638
698
  if name.startswith(self.flow_skill_prefix):
639
- role = name[len(self.flow_skill_prefix) + 5:] # Remove prefix + "flow_"
699
+ role = name[len(self.flow_skill_prefix):]
640
700
  if role:
641
701
  commands.append(f"/flow:{role}")
642
- return sorted(commands)
702
+
703
+ # New role skills
704
+ for role_name in self._roles.keys():
705
+ short_name = role_name.replace(self.ROLE_PREFIX, "")
706
+ commands.append(f"/flow:{short_name}")
707
+
708
+ return sorted(set(commands))
709
+
710
+ # ========================================================================
711
+ # Workflow Distribution (for Antigravity IDE compatibility)
712
+ # ========================================================================
713
+
714
+ def distribute_workflows(self, force: bool = False, lang: str = "zh") -> Dict[str, bool]:
715
+ """
716
+ Convert and distribute Flow Skills as Antigravity Workflows.
717
+
718
+ Flow Skills are converted to Antigravity Workflow format and saved
719
+ to .agent/workflows/ directory for IDE compatibility.
720
+
721
+ Args:
722
+ force: Overwrite existing files even if unchanged
723
+ lang: Language code for Flow Skills (default: "zh")
724
+
725
+ Returns:
726
+ Dictionary mapping workflow filenames to success status
727
+ """
728
+ distributor = WorkflowDistributor(self.root)
729
+ return distributor.distribute(force=force, lang=lang)
730
+
731
+ def cleanup_workflows(self, lang: str = "zh") -> int:
732
+ """
733
+ Remove distributed Antigravity Workflows.
734
+
735
+ Args:
736
+ lang: Language code for Flow Skills (default: "zh")
737
+
738
+ Returns:
739
+ Number of workflow files removed
740
+ """
741
+ distributor = WorkflowDistributor(self.root)
742
+ return distributor.cleanup(lang=lang)