monoco-toolkit 0.3.9__py3-none-any.whl → 0.3.11__py3-none-any.whl

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