monoco-toolkit 0.3.6__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 (113) hide show
  1. monoco/cli/workspace.py +1 -1
  2. monoco/core/config.py +58 -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/injection.py +63 -29
  11. monoco/core/integrations.py +8 -2
  12. monoco/core/output.py +5 -5
  13. monoco/core/registry.py +9 -1
  14. monoco/core/resource/__init__.py +5 -0
  15. monoco/core/resource/finder.py +98 -0
  16. monoco/core/resource/manager.py +91 -0
  17. monoco/core/resource/models.py +35 -0
  18. monoco/core/resources/en/{SKILL.md → skills/monoco_core/SKILL.md} +2 -0
  19. monoco/core/resources/zh/{SKILL.md → skills/monoco_core/SKILL.md} +2 -0
  20. monoco/core/setup.py +1 -1
  21. monoco/core/skill_framework.py +292 -0
  22. monoco/core/skills.py +538 -254
  23. monoco/core/sync.py +73 -1
  24. monoco/core/workflow_converter.py +420 -0
  25. monoco/features/{scheduler → agent}/__init__.py +5 -3
  26. monoco/features/agent/adapter.py +31 -0
  27. monoco/features/agent/apoptosis.py +44 -0
  28. monoco/features/agent/cli.py +296 -0
  29. monoco/features/agent/config.py +96 -0
  30. monoco/features/agent/defaults.py +12 -0
  31. monoco/features/{scheduler → agent}/engines.py +32 -6
  32. monoco/features/agent/flow_skills.py +281 -0
  33. monoco/features/agent/manager.py +91 -0
  34. monoco/features/{scheduler → agent}/models.py +6 -3
  35. monoco/features/agent/resources/atoms/atom-code-dev.yaml +61 -0
  36. monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +73 -0
  37. monoco/features/agent/resources/atoms/atom-knowledge.yaml +55 -0
  38. monoco/features/agent/resources/atoms/atom-review.yaml +60 -0
  39. monoco/features/agent/resources/en/skills/flow_engineer/SKILL.md +94 -0
  40. monoco/features/agent/resources/en/skills/flow_manager/SKILL.md +93 -0
  41. monoco/features/agent/resources/en/skills/flow_planner/SKILL.md +85 -0
  42. monoco/features/agent/resources/en/skills/flow_reviewer/SKILL.md +114 -0
  43. monoco/features/agent/resources/roles/role-engineer.yaml +49 -0
  44. monoco/features/agent/resources/roles/role-manager.yaml +46 -0
  45. monoco/features/agent/resources/roles/role-planner.yaml +46 -0
  46. monoco/features/agent/resources/roles/role-reviewer.yaml +47 -0
  47. monoco/features/agent/resources/workflows/workflow-dev.yaml +83 -0
  48. monoco/features/agent/resources/workflows/workflow-issue-create.yaml +72 -0
  49. monoco/features/agent/resources/workflows/workflow-review.yaml +94 -0
  50. monoco/features/agent/resources/zh/skills/flow_engineer/SKILL.md +94 -0
  51. monoco/features/agent/resources/zh/skills/flow_manager/SKILL.md +88 -0
  52. monoco/features/agent/resources/zh/skills/flow_planner/SKILL.md +259 -0
  53. monoco/features/agent/resources/zh/skills/flow_reviewer/SKILL.md +137 -0
  54. monoco/features/{scheduler → agent}/session.py +36 -1
  55. monoco/features/{scheduler → agent}/worker.py +40 -4
  56. monoco/features/glossary/adapter.py +31 -0
  57. monoco/features/glossary/config.py +5 -0
  58. monoco/features/glossary/resources/en/AGENTS.md +29 -0
  59. monoco/features/glossary/resources/en/skills/monoco_glossary/SKILL.md +35 -0
  60. monoco/features/glossary/resources/zh/AGENTS.md +29 -0
  61. monoco/features/glossary/resources/zh/skills/monoco_glossary/SKILL.md +35 -0
  62. monoco/features/i18n/resources/en/skills/i18n_scan_workflow/SKILL.md +105 -0
  63. monoco/features/i18n/resources/en/{SKILL.md → skills/monoco_i18n/SKILL.md} +2 -0
  64. monoco/features/i18n/resources/zh/skills/i18n_scan_workflow/SKILL.md +105 -0
  65. monoco/features/i18n/resources/zh/{SKILL.md → skills/monoco_i18n/SKILL.md} +2 -0
  66. monoco/features/issue/commands.py +427 -21
  67. monoco/features/issue/core.py +140 -1
  68. monoco/features/issue/criticality.py +553 -0
  69. monoco/features/issue/domain/models.py +28 -2
  70. monoco/features/issue/engine/machine.py +75 -15
  71. monoco/features/issue/git_service.py +185 -0
  72. monoco/features/issue/linter.py +291 -62
  73. monoco/features/issue/models.py +50 -2
  74. monoco/features/issue/resources/en/skills/issue_create_workflow/SKILL.md +167 -0
  75. monoco/features/issue/resources/en/skills/issue_develop_workflow/SKILL.md +224 -0
  76. monoco/features/issue/resources/en/skills/issue_lifecycle_workflow/SKILL.md +159 -0
  77. monoco/features/issue/resources/en/skills/issue_refine_workflow/SKILL.md +203 -0
  78. monoco/features/issue/resources/en/{SKILL.md → skills/monoco_issue/SKILL.md} +50 -0
  79. monoco/features/issue/resources/zh/skills/issue_create_workflow/SKILL.md +167 -0
  80. monoco/features/issue/resources/zh/skills/issue_develop_workflow/SKILL.md +224 -0
  81. monoco/features/issue/resources/zh/skills/issue_lifecycle_workflow/SKILL.md +159 -0
  82. monoco/features/issue/resources/zh/skills/issue_refine_workflow/SKILL.md +203 -0
  83. monoco/features/issue/resources/zh/{SKILL.md → skills/monoco_issue/SKILL.md} +52 -0
  84. monoco/features/issue/validator.py +185 -65
  85. monoco/features/memo/__init__.py +2 -1
  86. monoco/features/memo/adapter.py +32 -0
  87. monoco/features/memo/cli.py +36 -14
  88. monoco/features/memo/core.py +59 -0
  89. monoco/features/memo/resources/en/skills/monoco_memo/SKILL.md +77 -0
  90. monoco/features/memo/resources/en/skills/note_processing_workflow/SKILL.md +140 -0
  91. monoco/features/memo/resources/zh/AGENTS.md +8 -0
  92. monoco/features/memo/resources/zh/skills/monoco_memo/SKILL.md +77 -0
  93. monoco/features/memo/resources/zh/skills/note_processing_workflow/SKILL.md +140 -0
  94. monoco/features/spike/resources/en/{SKILL.md → skills/monoco_spike/SKILL.md} +2 -0
  95. monoco/features/spike/resources/en/skills/research_workflow/SKILL.md +121 -0
  96. monoco/features/spike/resources/zh/{SKILL.md → skills/monoco_spike/SKILL.md} +2 -0
  97. monoco/features/spike/resources/zh/skills/research_workflow/SKILL.md +121 -0
  98. monoco/main.py +2 -3
  99. monoco_toolkit-0.3.10.dist-info/METADATA +124 -0
  100. monoco_toolkit-0.3.10.dist-info/RECORD +156 -0
  101. monoco/features/scheduler/cli.py +0 -285
  102. monoco/features/scheduler/config.py +0 -68
  103. monoco/features/scheduler/defaults.py +0 -54
  104. monoco/features/scheduler/manager.py +0 -49
  105. monoco/features/scheduler/reliability.py +0 -106
  106. monoco/features/skills/core.py +0 -102
  107. monoco_toolkit-0.3.6.dist-info/METADATA +0 -127
  108. monoco_toolkit-0.3.6.dist-info/RECORD +0 -97
  109. /monoco/core/{hooks.py → githooks.py} +0 -0
  110. /monoco/features/{skills → glossary}/__init__.py +0 -0
  111. {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.10.dist-info}/WHEEL +0 -0
  112. {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.10.dist-info}/entry_points.txt +0 -0
  113. {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.10.dist-info}/licenses/LICENSE +0 -0
monoco/core/skills.py CHANGED
@@ -2,29 +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
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
12
16
  """
13
17
 
14
18
  import shutil
15
19
  import hashlib
16
20
  from pathlib import Path
17
- from typing import Dict, List, Optional
21
+ from typing import Dict, List, Optional, Set, Union
18
22
  from pydantic import BaseModel, Field, ValidationError
19
23
  from rich.console import Console
20
24
  import yaml
21
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
+
22
37
  console = Console()
23
38
 
24
39
 
25
40
  class SkillMetadata(BaseModel):
26
41
  """
27
- Skill metadata from YAML frontmatter.
42
+ Legacy skill metadata from YAML frontmatter.
28
43
  Based on agentskills.io standard.
29
44
  """
30
45
 
@@ -37,116 +52,111 @@ class SkillMetadata(BaseModel):
37
52
  tags: Optional[List[str]] = Field(
38
53
  default=None, description="Skill tags for categorization"
39
54
  )
55
+ type: Optional[str] = Field(
56
+ default="standard", description="Skill type: standard, flow, workflow, atom, role"
57
+ )
58
+ role: Optional[str] = Field(
59
+ default=None, description="Role identifier for Flow Skills (e.g., engineer, manager)"
60
+ )
61
+ domain: Optional[str] = Field(
62
+ default=None, description="Domain identifier for Workflow Skills (e.g., issue, spike)"
63
+ )
40
64
 
41
65
 
42
66
  class Skill:
43
67
  """
44
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
45
72
  """
46
73
 
47
- def __init__(self, root_dir: Path, skill_dir: Path):
48
- """
49
- Initialize a Skill instance.
50
-
51
- Args:
52
- root_dir: Project root directory
53
- skill_dir: Path to the skill directory (e.g., Toolkit/skills/issues-management)
54
- """
74
+ def __init__(
75
+ self,
76
+ root_dir: Path,
77
+ skill_name: str,
78
+ resources_dir: Path,
79
+ ):
55
80
  self.root_dir = root_dir
56
- self.skill_dir = skill_dir
57
- self.name = skill_dir.name
58
- self.skill_file = skill_dir / "SKILL.md"
81
+ self.skill_name = skill_name
82
+ self.resources_dir = resources_dir
83
+ self.name = skill_name
59
84
  self.metadata: Optional[SkillMetadata] = None
60
85
  self._load_metadata()
61
86
 
62
87
  def _load_metadata(self) -> None:
63
88
  """Load and validate skill metadata from SKILL.md frontmatter."""
64
- # Try to load from language subdirectories first (Feature resources pattern)
65
- # Then fallback to root SKILL.md (legacy pattern)
66
- skill_file_to_use = None
67
-
68
- # Check language subdirectories
69
- if self.skill_dir.exists():
70
- for item in sorted(self.skill_dir.iterdir()):
71
- if item.is_dir() and len(item.name) == 2: # 2-letter lang code
72
- candidate = item / "SKILL.md"
73
- if candidate.exists():
74
- skill_file_to_use = candidate
75
- break
76
-
77
- # Fallback to root SKILL.md
78
- if not skill_file_to_use and self.skill_file.exists():
79
- skill_file_to_use = self.skill_file
80
-
81
- if not skill_file_to_use:
89
+ skill_file = self._get_first_available_skill_file()
90
+
91
+ if not skill_file:
82
92
  return
83
93
 
84
94
  try:
85
- content = skill_file_to_use.read_text(encoding="utf-8")
86
- # Extract YAML frontmatter
95
+ content = skill_file.read_text(encoding="utf-8")
87
96
  if content.startswith("---"):
88
97
  parts = content.split("---", 2)
89
98
  if len(parts) >= 3:
90
99
  frontmatter = parts[1].strip()
91
100
  metadata_dict = yaml.safe_load(frontmatter)
92
-
93
- # Validate against schema
94
101
  self.metadata = SkillMetadata(**metadata_dict)
95
102
  except ValidationError as e:
96
- 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]")
97
104
  except Exception as e:
98
105
  console.print(
99
- 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]"
100
107
  )
101
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
+
102
128
  def is_valid(self) -> bool:
103
129
  """Check if the skill has valid metadata."""
104
130
  return self.metadata is not None
105
131
 
106
- def get_languages(self) -> List[str]:
107
- """
108
- Detect available language versions of this skill.
132
+ def get_type(self) -> str:
133
+ """Get skill type, defaults to 'standard'."""
134
+ return self.metadata.type if self.metadata and self.metadata.type else "standard"
109
135
 
110
- Returns:
111
- List of language codes (e.g., ['en', 'zh'])
112
- """
136
+ def get_role(self) -> Optional[str]:
137
+ """Get skill role (for Flow Skills)."""
138
+ return self.metadata.role if self.metadata else None
139
+
140
+ def get_languages(self) -> List[str]:
141
+ """Detect available language versions of this skill."""
113
142
  languages = []
114
143
 
115
- # Check for language subdirectories (Feature resources pattern)
116
- # resources/en/SKILL.md, resources/zh/SKILL.md
117
- for item in self.skill_dir.iterdir():
118
- if item.is_dir() and len(item.name) == 2: # Assume 2-letter lang codes
119
- lang_skill_file = item / "SKILL.md"
120
- if lang_skill_file.exists():
121
- languages.append(item.name)
144
+ if not self.resources_dir.exists():
145
+ return languages
122
146
 
123
- # Fallback: check for root SKILL.md (legacy Toolkit/skills pattern)
124
- # We don't assume a default language, just return what we found
125
- if not languages and self.skill_file.exists():
126
- # For legacy pattern, we can't determine the language from structure
127
- # Return empty to indicate this skill uses legacy pattern
128
- 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)
129
152
 
130
- return languages
153
+ return sorted(languages)
131
154
 
132
155
  def get_checksum(self, lang: str) -> str:
133
- """
134
- Calculate checksum for the skill content.
156
+ """Calculate checksum for the skill content."""
157
+ target_file = self.get_skill_file(lang)
135
158
 
136
- Args:
137
- lang: Language code
138
-
139
- Returns:
140
- SHA256 checksum of the skill file
141
- """
142
- # Try language subdirectory first (Feature resources pattern)
143
- target_file = self.skill_dir / lang / "SKILL.md"
144
-
145
- # Fallback to root SKILL.md (legacy pattern)
146
- if not target_file.exists():
147
- target_file = self.skill_file
148
-
149
- if not target_file.exists():
159
+ if not target_file:
150
160
  return ""
151
161
 
152
162
  content = target_file.read_bytes()
@@ -155,73 +165,70 @@ class Skill:
155
165
 
156
166
  class SkillManager:
157
167
  """
158
- Central manager for Monoco skills.
159
-
160
- Responsibilities:
161
- - Collect skills from Feature resources
162
- - Validate skill structure
163
- - Distribute skills to agent framework directories
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
164
178
  """
165
179
 
166
- def __init__(self, root: Path, features: Optional[List] = None):
167
- """
168
- Initialize SkillManager.
169
-
170
- Args:
171
- root: Project root directory
172
- features: List of MonocoFeature instances (if None, will load from registry)
173
- """
180
+ # Default prefix for flow skills
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_"
187
+
188
+ def __init__(
189
+ self,
190
+ root: Path,
191
+ features: Optional[List] = None,
192
+ flow_skill_prefix: str = FLOW_SKILL_PREFIX,
193
+ ):
174
194
  self.root = root
175
195
  self.features = features or []
196
+ self.flow_skill_prefix = flow_skill_prefix
197
+
198
+ # Legacy skills
176
199
  self.skills: Dict[str, Skill] = {}
177
-
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
178
208
  if self.features:
179
209
  self._discover_skills_from_features()
210
+ self._discover_core_skills()
211
+
212
+ # Load new three-level skills
213
+ self._discover_three_level_skills()
180
214
 
181
- # Also discover core skill (monoco/core/resources/)
182
- self._discover_core_skill()
183
-
184
- def _discover_core_skill(self) -> None:
185
- """
186
- Discover skill from monoco/core/resources/.
187
-
188
- Core is special - it's not a Feature but still has a skill.
189
- """
215
+ def _discover_core_skills(self) -> None:
216
+ """Discover skills from monoco/core/resources/{lang}/skills/."""
190
217
  core_resources_dir = self.root / "monoco" / "core" / "resources"
191
218
 
192
219
  if not core_resources_dir.exists():
193
220
  return
194
221
 
195
- # Check for SKILL.md in language directories
196
- for lang_dir in core_resources_dir.iterdir():
197
- if lang_dir.is_dir() and (lang_dir / "SKILL.md").exists():
198
- skill = Skill(self.root, core_resources_dir)
199
-
200
- # Use the skill's metadata name if available
201
- if skill.metadata and skill.metadata.name:
202
- skill.name = skill.metadata.name.replace("-", "_")
203
- else:
204
- skill.name = "monoco_core"
205
-
206
- if skill.is_valid():
207
- self.skills[skill.name] = skill
208
- break # Only need to detect once
222
+ self._discover_skills_in_resources(core_resources_dir, "monoco_core")
209
223
 
210
224
  def _discover_skills_from_features(self) -> None:
211
- """
212
- Discover skills from Feature resources.
213
-
214
- Each feature should have:
215
- - monoco/features/{feature}/resources/{lang}/SKILL.md
216
- """
225
+ """Discover skills from Feature resources."""
217
226
  from monoco.core.feature import MonocoFeature
218
227
 
219
228
  for feature in self.features:
220
229
  if not isinstance(feature, MonocoFeature):
221
230
  continue
222
231
 
223
- # Determine feature module path
224
- # feature.__class__.__module__ is like 'monoco.features.issue.adapter'
225
232
  module_parts = feature.__class__.__module__.split(".")
226
233
  if (
227
234
  len(module_parts) >= 3
@@ -230,154 +237,247 @@ class SkillManager:
230
237
  ):
231
238
  feature_name = module_parts[2]
232
239
 
233
- # Construct path to feature resources
234
- # monoco/features/{feature}/resources/
235
240
  feature_dir = self.root / "monoco" / "features" / feature_name
236
241
  resources_dir = feature_dir / "resources"
237
242
 
238
243
  if not resources_dir.exists():
239
244
  continue
240
245
 
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
255
-
256
- def _create_skill_from_feature(
257
- self, feature_name: str, resources_dir: Path
258
- ) -> Optional[Skill]:
259
- """
260
- Create a Skill instance from a feature's resources directory.
246
+ self._discover_skills_in_resources(resources_dir, feature_name)
261
247
 
262
- Args:
263
- feature_name: Name of the feature (e.g., 'issue', 'spike')
264
- resources_dir: Path to the feature's resources directory
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():
251
+ return
265
252
 
266
- Returns:
267
- Skill instance or None if creation fails
268
- """
269
- # Use the resources directory as the skill directory
270
- # The Skill class expects a directory with SKILL.md or {lang}/SKILL.md
271
- skill = Skill(self.root, resources_dir)
253
+ skill_names: Set[str] = set()
272
254
 
273
- # Use the skill's metadata name if available (e.g., 'monoco-issue')
274
- # Convert to snake_case for directory name (e.g., 'monoco_issue')
275
- if skill.metadata and skill.metadata.name:
276
- # Convert kebab-case to snake_case for directory name
277
- skill.name = skill.metadata.name.replace("-", "_")
278
- else:
279
- # Fallback to feature name
280
- skill.name = f"monoco_{feature_name}"
255
+ for lang_dir in resources_dir.iterdir():
256
+ if not lang_dir.is_dir() or len(lang_dir.name) != 2:
257
+ continue
281
258
 
282
- return skill
259
+ skills_dir = lang_dir / "skills"
260
+ if not skills_dir.exists():
261
+ continue
283
262
 
284
- def list_skills(self) -> List[Skill]:
285
- """
286
- Get all available skills.
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)
287
266
 
288
- Returns:
289
- List of Skill instances
290
- """
267
+ for skill_name in skill_names:
268
+ skill = Skill(
269
+ root_dir=self.root,
270
+ skill_name=skill_name,
271
+ resources_dir=resources_dir,
272
+ )
273
+
274
+ if not skill.is_valid():
275
+ console.print(
276
+ f"[yellow]Warning: Skill {skill_name} has invalid metadata, skipping[/yellow]"
277
+ )
278
+ continue
279
+
280
+ skill_type = skill.get_type()
281
+ if skill_type == "flow":
282
+ name = skill_name
283
+ if name.startswith("flow_"):
284
+ name = name[5:]
285
+ skill_key = f"{self.flow_skill_prefix}{name}"
286
+ else:
287
+ skill_key = f"{feature_name}_{skill_name}"
288
+
289
+ skill.name = skill_key
290
+ self.skills[skill_key] = skill
291
+
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
+ # ========================================================================
318
+
319
+ def list_skills(self) -> List[Skill]:
320
+ """Get all available legacy skills."""
291
321
  return list(self.skills.values())
292
322
 
323
+ def list_skills_by_type(self, skill_type: str) -> List[Skill]:
324
+ """Get skills filtered by type."""
325
+ return [s for s in self.skills.values() if s.get_type() == skill_type]
326
+
293
327
  def get_skill(self, name: str) -> Optional[Skill]:
294
- """
295
- Get a specific skill by name.
328
+ """Get a specific legacy skill by name."""
329
+ return self.skills.get(name)
296
330
 
297
- Args:
298
- name: Skill name
331
+ def get_flow_skills(self) -> List[Skill]:
332
+ """Get all Flow Skills."""
333
+ return self.list_skills_by_type("flow")
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
+ )
299
407
 
300
- Returns:
301
- Skill instance or None if not found
302
- """
303
- return self.skills.get(name)
408
+ return errors
409
+
410
+ # ========================================================================
411
+ # Distribution
412
+ # ========================================================================
304
413
 
305
414
  def distribute(
306
415
  self, target_dir: Path, lang: str, force: bool = False
307
416
  ) -> Dict[str, bool]:
308
417
  """
309
- Distribute skills to a target directory.
310
-
311
- Args:
312
- target_dir: Target directory for skill distribution (e.g., .cursor/skills/)
313
- lang: Language code to distribute (e.g., 'en', 'zh')
314
- force: Force overwrite even if checksum matches
315
-
316
- Returns:
317
- 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)
318
423
  """
319
424
  results = {}
320
425
 
321
- # Ensure target directory exists
322
426
  target_dir.mkdir(parents=True, exist_ok=True)
323
427
 
428
+ # Distribute legacy skills
324
429
  for skill_name, skill in self.skills.items():
325
430
  try:
326
- # Check if this skill has the requested language
327
- available_languages = skill.get_languages()
328
-
329
- if lang not in available_languages:
330
- console.print(
331
- f"[yellow]Skill {skill_name} does not have {lang} version, skipping[/yellow]"
332
- )
333
- results[skill_name] = False
334
- continue
335
-
336
- # Distribute the specific language version
337
- self._distribute_skill_language(skill, target_dir, lang, force)
338
- results[skill_name] = True
339
-
431
+ success = self._distribute_legacy_skill(skill, target_dir, lang, force)
432
+ results[skill_name] = success
340
433
  except Exception as e:
341
434
  console.print(
342
435
  f"[red]Failed to distribute skill {skill_name}: {e}[/red]"
343
436
  )
344
437
  results[skill_name] = False
345
438
 
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
449
+
346
450
  return results
347
451
 
348
- def _distribute_skill_language(
452
+ def _distribute_legacy_skill(
349
453
  self, skill: Skill, target_dir: Path, lang: str, force: bool
350
- ) -> None:
351
- """
352
- Distribute a specific language version of a skill.
353
-
354
- Args:
355
- skill: Skill instance
356
- target_dir: Target directory (e.g., .cursor/skills/)
357
- lang: Language code
358
- force: Force overwrite
359
- """
360
- # Determine source file (try language subdirectory first)
361
- source_file = skill.skill_dir / lang / "SKILL.md"
362
-
363
- # Fallback to root SKILL.md (legacy pattern)
364
- if not source_file.exists():
365
- source_file = skill.skill_file
454
+ ) -> bool:
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
366
469
 
367
- if not source_file.exists():
470
+ source_file = skill.get_skill_file(lang)
471
+ if not source_file:
368
472
  console.print(
369
- f"[yellow]Source file not found for {skill.name}/{lang}[/yellow]"
473
+ f"[red]Source file not found for {skill.name}/{lang}[/red]"
370
474
  )
371
- return
475
+ return False
372
476
 
373
- # Target path: {target_dir}/{skill_name}/SKILL.md (no language subdirectory)
374
477
  target_skill_dir = target_dir / skill.name
375
-
376
- # Create target directory
377
478
  target_skill_dir.mkdir(parents=True, exist_ok=True)
378
479
  target_file = target_skill_dir / "SKILL.md"
379
480
 
380
- # Check if update is needed
381
481
  if target_file.exists() and not force:
382
482
  source_checksum = skill.get_checksum(lang)
383
483
  target_content = target_file.read_bytes()
@@ -385,63 +485,183 @@ class SkillManager:
385
485
 
386
486
  if source_checksum == target_checksum:
387
487
  console.print(f"[dim] = {skill.name}/SKILL.md is up to date[/dim]")
388
- return
488
+ return True
389
489
 
390
- # Copy the file
391
490
  shutil.copy2(source_file, target_file)
392
491
  console.print(f"[green] ✓ Distributed {skill.name}/SKILL.md ({lang})[/green]")
393
492
 
394
- # Copy additional resources if they exist
395
- 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)
396
494
 
397
- def _copy_skill_resources(
398
- self, source_dir: Path, target_dir: Path, lang: str
399
- ) -> None:
400
- """
401
- Copy additional skill resources (scripts, examples, etc.).
495
+ return True
402
496
 
403
- Args:
404
- source_dir: Source skill directory
405
- target_dir: Target skill directory
406
- lang: Language code
497
+ def _distribute_role_skill(
498
+ self, role: RoleSkillMetadata, target_dir: Path, lang: str, force: bool
499
+ ) -> bool:
407
500
  """
408
- # Define resource directories to copy
409
- resource_dirs = ["scripts", "examples", "resources"]
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
507
+ """
508
+ target_skill_dir = target_dir / role.name
509
+ target_file = target_skill_dir / "SKILL.md"
410
510
 
411
- # Try language subdirectory first (Feature resources pattern)
412
- 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
413
639
 
414
- # Fallback to root directory (legacy pattern)
415
640
  if not source_base.exists():
416
- source_base = source_dir
641
+ return
417
642
 
418
643
  for resource_name in resource_dirs:
419
644
  source_resource = source_base / resource_name
420
645
  if source_resource.exists() and source_resource.is_dir():
421
646
  target_resource = target_dir / resource_name
422
647
 
423
- # Remove existing and copy fresh
424
648
  if target_resource.exists():
425
649
  shutil.rmtree(target_resource)
426
650
 
427
651
  shutil.copytree(source_resource, target_resource)
428
652
  console.print(
429
- f"[dim] Copied {resource_name}/ for {source_dir.name}/{lang}[/dim]"
653
+ f"[dim] Copied {resource_name}/ for {skill_name}/{lang}[/dim]"
430
654
  )
431
655
 
432
656
  def cleanup(self, target_dir: Path) -> None:
433
- """
434
- Remove distributed skills from a target directory.
435
-
436
- Args:
437
- target_dir: Target directory to clean
438
- """
657
+ """Remove distributed skills from a target directory."""
439
658
  if not target_dir.exists():
440
659
  console.print(f"[dim]Target directory does not exist: {target_dir}[/dim]")
441
660
  return
442
661
 
443
662
  removed_count = 0
444
663
 
664
+ # Remove legacy skills
445
665
  for skill_name in self.skills.keys():
446
666
  skill_target = target_dir / skill_name
447
667
  if skill_target.exists():
@@ -449,10 +669,74 @@ class SkillManager:
449
669
  console.print(f"[green] ✓ Removed {skill_name}[/green]")
450
670
  removed_count += 1
451
671
 
452
- # 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
+
453
680
  if target_dir.exists() and not any(target_dir.iterdir()):
454
681
  target_dir.rmdir()
455
682
  console.print(f"[dim] Removed empty directory: {target_dir}[/dim]")
456
683
 
457
684
  if removed_count == 0:
458
685
  console.print(f"[dim]No skills to remove from {target_dir}[/dim]")
686
+
687
+ def get_flow_skill_commands(self) -> List[str]:
688
+ """Get list of available flow skill commands."""
689
+ commands = []
690
+
691
+ # Legacy flow skills
692
+ for skill in self.get_flow_skills():
693
+ role = skill.get_role()
694
+ if role:
695
+ commands.append(f"/flow:{role}")
696
+ else:
697
+ name = skill.name
698
+ if name.startswith(self.flow_skill_prefix):
699
+ role = name[len(self.flow_skill_prefix):]
700
+ if role:
701
+ commands.append(f"/flow:{role}")
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)