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.
- monoco/core/config.py +7 -0
- monoco/core/hooks/builtin/git_cleanup.py +1 -1
- monoco/core/injection.py +63 -29
- monoco/core/integrations.py +2 -2
- monoco/core/output.py +5 -5
- monoco/core/registry.py +7 -1
- monoco/core/resource/__init__.py +5 -0
- monoco/core/resource/finder.py +98 -0
- monoco/core/resource/manager.py +91 -0
- monoco/core/resource/models.py +35 -0
- monoco/core/resources/en/{SKILL.md → skills/monoco_core/SKILL.md} +2 -0
- monoco/core/resources/zh/{SKILL.md → skills/monoco_core/SKILL.md} +2 -0
- monoco/core/skill_framework.py +292 -0
- monoco/core/skills.py +471 -371
- monoco/core/sync.py +73 -1
- monoco/core/workflow_converter.py +420 -0
- monoco/features/agent/__init__.py +2 -2
- monoco/features/agent/adapter.py +31 -0
- monoco/features/agent/apoptosis.py +44 -0
- monoco/features/agent/cli.py +101 -144
- monoco/features/agent/config.py +35 -21
- monoco/features/agent/defaults.py +6 -49
- monoco/features/agent/engines.py +32 -6
- monoco/features/agent/manager.py +6 -1
- monoco/features/agent/models.py +2 -2
- monoco/features/agent/resources/atoms/atom-code-dev.yaml +61 -0
- monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +73 -0
- monoco/features/agent/resources/atoms/atom-knowledge.yaml +55 -0
- monoco/features/agent/resources/atoms/atom-review.yaml +60 -0
- monoco/features/agent/resources/en/skills/flow_engineer/SKILL.md +94 -0
- monoco/features/agent/resources/en/skills/flow_manager/SKILL.md +93 -0
- monoco/features/agent/resources/en/skills/flow_planner/SKILL.md +85 -0
- monoco/features/agent/resources/en/skills/flow_reviewer/SKILL.md +114 -0
- monoco/features/agent/resources/roles/role-engineer.yaml +49 -0
- monoco/features/agent/resources/roles/role-manager.yaml +46 -0
- monoco/features/agent/resources/roles/role-planner.yaml +46 -0
- monoco/features/agent/resources/roles/role-reviewer.yaml +47 -0
- monoco/features/agent/resources/workflows/workflow-dev.yaml +83 -0
- monoco/features/agent/resources/workflows/workflow-issue-create.yaml +72 -0
- monoco/features/agent/resources/workflows/workflow-review.yaml +94 -0
- monoco/features/agent/resources/zh/skills/flow_planner/SKILL.md +259 -0
- monoco/features/agent/resources/zh/skills/flow_reviewer/SKILL.md +137 -0
- monoco/features/agent/worker.py +38 -2
- monoco/features/glossary/__init__.py +0 -0
- monoco/features/glossary/adapter.py +31 -0
- monoco/features/glossary/config.py +5 -0
- monoco/features/glossary/resources/en/AGENTS.md +29 -0
- monoco/features/glossary/resources/en/skills/monoco_glossary/SKILL.md +35 -0
- monoco/features/glossary/resources/zh/AGENTS.md +29 -0
- monoco/features/glossary/resources/zh/skills/monoco_glossary/SKILL.md +35 -0
- monoco/features/i18n/resources/en/skills/i18n_scan_workflow/SKILL.md +105 -0
- monoco/features/i18n/resources/en/{SKILL.md → skills/monoco_i18n/SKILL.md} +2 -0
- monoco/features/i18n/resources/zh/{SKILL.md → skills/monoco_i18n/SKILL.md} +2 -0
- monoco/features/issue/core.py +45 -6
- monoco/features/issue/engine/machine.py +5 -2
- monoco/features/issue/models.py +1 -0
- monoco/features/issue/resources/en/skills/issue_create_workflow/SKILL.md +167 -0
- monoco/features/issue/resources/en/skills/issue_develop_workflow/SKILL.md +224 -0
- monoco/features/issue/resources/en/skills/issue_lifecycle_workflow/SKILL.md +159 -0
- monoco/features/issue/resources/en/skills/issue_refine_workflow/SKILL.md +203 -0
- monoco/features/issue/resources/en/{SKILL.md → skills/monoco_issue/SKILL.md} +2 -0
- monoco/features/issue/resources/zh/skills/issue_create_workflow/SKILL.md +167 -0
- monoco/features/issue/resources/zh/skills/issue_develop_workflow/SKILL.md +224 -0
- monoco/features/issue/resources/zh/skills/issue_refine_workflow/SKILL.md +203 -0
- monoco/features/issue/resources/zh/{SKILL.md → skills/monoco_issue/SKILL.md} +2 -0
- monoco/features/memo/resources/en/skills/monoco_memo/SKILL.md +77 -0
- monoco/features/memo/resources/en/skills/note_processing_workflow/SKILL.md +140 -0
- monoco/features/memo/resources/zh/{SKILL.md → skills/monoco_memo/SKILL.md} +2 -0
- monoco/features/spike/resources/en/{SKILL.md → skills/monoco_spike/SKILL.md} +2 -0
- monoco/features/spike/resources/en/skills/research_workflow/SKILL.md +121 -0
- monoco/features/spike/resources/zh/{SKILL.md → skills/monoco_spike/SKILL.md} +2 -0
- monoco_toolkit-0.3.10.dist-info/METADATA +124 -0
- monoco_toolkit-0.3.10.dist-info/RECORD +156 -0
- monoco/features/agent/reliability.py +0 -106
- monoco/features/agent/resources/skills/flow_reviewer/SKILL.md +0 -114
- monoco_toolkit-0.3.9.dist-info/METADATA +0 -127
- monoco_toolkit-0.3.9.dist-info/RECORD +0 -115
- /monoco/features/agent/resources/{skills → zh/skills}/flow_engineer/SKILL.md +0 -0
- /monoco/features/agent/resources/{skills → zh/skills}/flow_manager/SKILL.md +0 -0
- /monoco/features/i18n/resources/{skills → zh/skills}/i18n_scan_workflow/SKILL.md +0 -0
- /monoco/features/issue/resources/{skills → zh/skills}/issue_lifecycle_workflow/SKILL.md +0 -0
- /monoco/features/memo/resources/{skills → zh/skills}/note_processing_workflow/SKILL.md +0 -0
- /monoco/features/spike/resources/{skills → zh/skills}/research_workflow/SKILL.md +0 -0
- {monoco_toolkit-0.3.9.dist-info → monoco_toolkit-0.3.10.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.9.dist-info → monoco_toolkit-0.3.10.dist-info}/entry_points.txt +0 -0
- {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
|
|
9
|
-
2.
|
|
10
|
-
3.
|
|
11
|
-
4.
|
|
12
|
-
5. Support
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
58
|
-
|
|
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.
|
|
72
|
-
self.
|
|
73
|
-
self.
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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 =
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
-
|
|
185
|
-
-
|
|
186
|
-
-
|
|
187
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
293
|
-
|
|
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
|
-
|
|
297
|
-
if not
|
|
259
|
+
skills_dir = lang_dir / "skills"
|
|
260
|
+
if not skills_dir.exists():
|
|
298
261
|
continue
|
|
299
262
|
|
|
300
|
-
|
|
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
|
-
|
|
304
|
-
|
|
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
|
-
|
|
315
|
-
|
|
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
|
-
|
|
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
|
|
326
|
-
"""
|
|
327
|
-
Discover
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
499
|
-
console.print(f"[green] ✓ Distributed {skill.name}/[/green]")
|
|
500
|
-
return True
|
|
450
|
+
return results
|
|
501
451
|
|
|
502
|
-
def
|
|
452
|
+
def _distribute_legacy_skill(
|
|
503
453
|
self, skill: Skill, target_dir: Path, lang: str, force: bool
|
|
504
454
|
) -> bool:
|
|
505
|
-
"""
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
-
|
|
470
|
+
source_file = skill.get_skill_file(lang)
|
|
471
|
+
if not source_file:
|
|
525
472
|
console.print(
|
|
526
|
-
f"[
|
|
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
|
-
|
|
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
|
|
557
|
-
self,
|
|
558
|
-
) ->
|
|
497
|
+
def _distribute_role_skill(
|
|
498
|
+
self, role: RoleSkillMetadata, target_dir: Path, lang: str, force: bool
|
|
499
|
+
) -> bool:
|
|
559
500
|
"""
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
568
|
-
|
|
508
|
+
target_skill_dir = target_dir / role.name
|
|
509
|
+
target_file = target_skill_dir / "SKILL.md"
|
|
569
510
|
|
|
570
|
-
#
|
|
571
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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)
|
|
699
|
+
role = name[len(self.flow_skill_prefix):]
|
|
640
700
|
if role:
|
|
641
701
|
commands.append(f"/flow:{role}")
|
|
642
|
-
|
|
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)
|