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.
- monoco/__main__.py +8 -0
- monoco/core/artifacts/__init__.py +16 -0
- monoco/core/artifacts/manager.py +575 -0
- monoco/core/artifacts/models.py +161 -0
- monoco/core/config.py +38 -4
- monoco/core/git.py +23 -0
- monoco/core/hooks/builtin/git_cleanup.py +1 -1
- monoco/core/ingestion/__init__.py +20 -0
- monoco/core/ingestion/discovery.py +248 -0
- monoco/core/ingestion/watcher.py +343 -0
- monoco/core/ingestion/worker.py +436 -0
- monoco/core/injection.py +63 -29
- monoco/core/integrations.py +2 -2
- monoco/core/loader.py +633 -0
- monoco/core/output.py +5 -5
- monoco/core/registry.py +34 -19
- 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/skill_framework.py +292 -0
- monoco/core/skills.py +524 -385
- monoco/core/sync.py +73 -1
- monoco/core/workflow_converter.py +420 -0
- monoco/daemon/app.py +77 -1
- monoco/daemon/commands.py +10 -0
- monoco/daemon/mailroom_service.py +196 -0
- monoco/daemon/models.py +1 -0
- monoco/daemon/scheduler.py +236 -0
- monoco/daemon/services.py +185 -0
- monoco/daemon/triggers.py +55 -0
- monoco/features/agent/__init__.py +2 -2
- monoco/features/agent/adapter.py +41 -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 +47 -6
- 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/{core/resources/en → features/agent/resources/en/skills/monoco_atom_core}/SKILL.md +3 -1
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_engineer/SKILL.md +94 -0
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_manager/SKILL.md +93 -0
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_planner/SKILL.md +85 -0
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_reviewer/SKILL.md +114 -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/roles/monoco_role_engineer.yaml +49 -0
- monoco/features/agent/resources/zh/roles/monoco_role_manager.yaml +46 -0
- monoco/features/agent/resources/zh/roles/monoco_role_planner.yaml +46 -0
- monoco/features/agent/resources/zh/roles/monoco_role_reviewer.yaml +47 -0
- monoco/{core/resources/zh → features/agent/resources/zh/skills/monoco_atom_core}/SKILL.md +3 -1
- monoco/features/agent/resources/{skills/flow_engineer → zh/skills/monoco_workflow_agent_engineer}/SKILL.md +2 -2
- monoco/features/agent/resources/{skills/flow_manager → zh/skills/monoco_workflow_agent_manager}/SKILL.md +2 -2
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_planner/SKILL.md +259 -0
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_reviewer/SKILL.md +137 -0
- monoco/features/agent/session.py +59 -11
- monoco/features/agent/worker.py +38 -2
- monoco/features/artifact/__init__.py +0 -0
- monoco/features/artifact/adapter.py +33 -0
- monoco/features/artifact/resources/zh/AGENTS.md +14 -0
- monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +278 -0
- monoco/features/glossary/__init__.py +0 -0
- monoco/features/glossary/adapter.py +42 -0
- monoco/features/glossary/config.py +5 -0
- monoco/features/glossary/resources/en/AGENTS.md +29 -0
- monoco/features/glossary/resources/en/skills/monoco_atom_glossary/SKILL.md +35 -0
- monoco/features/glossary/resources/zh/AGENTS.md +29 -0
- monoco/features/glossary/resources/zh/skills/monoco_atom_glossary/SKILL.md +35 -0
- monoco/features/hooks/__init__.py +11 -0
- monoco/features/hooks/adapter.py +67 -0
- monoco/features/hooks/commands.py +309 -0
- monoco/features/hooks/core.py +441 -0
- monoco/features/hooks/resources/ADDING_HOOKS.md +234 -0
- monoco/features/i18n/adapter.py +18 -5
- monoco/features/i18n/core.py +482 -17
- monoco/features/i18n/resources/en/{SKILL.md → skills/monoco_atom_i18n/SKILL.md} +3 -1
- monoco/features/i18n/resources/en/skills/monoco_workflow_i18n_scan/SKILL.md +105 -0
- monoco/features/i18n/resources/zh/{SKILL.md → skills/monoco_atom_i18n/SKILL.md} +3 -1
- monoco/features/i18n/resources/{skills/i18n_scan_workflow → zh/skills/monoco_workflow_i18n_scan}/SKILL.md +2 -2
- monoco/features/issue/adapter.py +19 -6
- monoco/features/issue/commands.py +281 -7
- monoco/features/issue/core.py +272 -19
- monoco/features/issue/engine/machine.py +118 -5
- monoco/features/issue/linter.py +60 -5
- monoco/features/issue/models.py +3 -2
- monoco/features/issue/resources/en/AGENTS.md +109 -0
- monoco/features/issue/resources/en/{SKILL.md → skills/monoco_atom_issue/SKILL.md} +3 -1
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_creation/SKILL.md +167 -0
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_development/SKILL.md +224 -0
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_management/SKILL.md +159 -0
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_refinement/SKILL.md +203 -0
- monoco/features/issue/resources/hooks/post-checkout.sh +39 -0
- monoco/features/issue/resources/hooks/pre-commit.sh +41 -0
- monoco/features/issue/resources/hooks/pre-push.sh +35 -0
- monoco/features/issue/resources/zh/AGENTS.md +109 -0
- monoco/features/issue/resources/zh/{SKILL.md → skills/monoco_atom_issue_lifecycle/SKILL.md} +3 -1
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_creation/SKILL.md +167 -0
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_development/SKILL.md +224 -0
- monoco/features/issue/resources/{skills/issue_lifecycle_workflow → zh/skills/monoco_workflow_issue_management}/SKILL.md +2 -2
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_refinement/SKILL.md +203 -0
- monoco/features/issue/validator.py +101 -1
- monoco/features/memo/adapter.py +21 -8
- monoco/features/memo/cli.py +103 -10
- monoco/features/memo/core.py +178 -92
- monoco/features/memo/models.py +53 -0
- monoco/features/memo/resources/en/skills/monoco_atom_memo/SKILL.md +77 -0
- monoco/features/memo/resources/en/skills/monoco_workflow_note_processing/SKILL.md +140 -0
- monoco/features/memo/resources/zh/{SKILL.md → skills/monoco_atom_memo/SKILL.md} +3 -1
- monoco/features/memo/resources/{skills/note_processing_workflow → zh/skills/monoco_workflow_note_processing}/SKILL.md +2 -2
- monoco/features/spike/adapter.py +18 -5
- monoco/features/spike/resources/en/{SKILL.md → skills/monoco_atom_spike/SKILL.md} +3 -1
- monoco/features/spike/resources/en/skills/monoco_workflow_research/SKILL.md +121 -0
- monoco/features/spike/resources/zh/{SKILL.md → skills/monoco_atom_spike/SKILL.md} +3 -1
- monoco/features/spike/resources/{skills/research_workflow → zh/skills/monoco_workflow_research}/SKILL.md +2 -2
- monoco/main.py +38 -1
- monoco_toolkit-0.3.11.dist-info/METADATA +130 -0
- monoco_toolkit-0.3.11.dist-info/RECORD +181 -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/{core → features/agent}/resources/en/AGENTS.md +0 -0
- /monoco/{core → features/agent}/resources/zh/AGENTS.md +0 -0
- {monoco_toolkit-0.3.9.dist-info → monoco_toolkit-0.3.11.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.9.dist-info → monoco_toolkit-0.3.11.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
|
9
|
-
2. Validate skill structure and metadata
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
58
|
-
|
|
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.
|
|
72
|
-
self.
|
|
73
|
-
self.
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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)
|
|
148
|
+
if not self.resources_dir.exists():
|
|
149
|
+
return languages
|
|
145
150
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
-
|
|
185
|
-
-
|
|
186
|
-
-
|
|
187
|
-
|
|
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
|
-
#
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
281
|
-
"""
|
|
282
|
-
|
|
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
|
-
|
|
293
|
-
|
|
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
|
-
|
|
297
|
-
if not
|
|
245
|
+
skills_dir = lang_dir / "skills"
|
|
246
|
+
if not skills_dir.exists():
|
|
298
247
|
continue
|
|
299
248
|
|
|
300
|
-
|
|
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
|
-
|
|
304
|
-
|
|
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
|
-
#
|
|
312
|
-
|
|
313
|
-
if
|
|
314
|
-
|
|
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
|
-
#
|
|
318
|
-
|
|
319
|
-
|
|
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
|
|
326
|
-
"""
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
499
|
-
console.print(f"[green] ✓ Distributed {skill.name}/[/green]")
|
|
500
|
-
return True
|
|
494
|
+
return results
|
|
501
495
|
|
|
502
|
-
def
|
|
496
|
+
def _distribute_legacy_skill(
|
|
503
497
|
self, skill: Skill, target_dir: Path, lang: str, force: bool
|
|
504
498
|
) -> 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
|
|
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
|
-
|
|
514
|
+
source_file = skill.get_skill_file(lang)
|
|
515
|
+
if not source_file:
|
|
525
516
|
console.print(
|
|
526
|
-
f"[
|
|
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
|
-
|
|
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
|
|
557
|
-
self,
|
|
558
|
-
) ->
|
|
541
|
+
def _distribute_role_skill(
|
|
542
|
+
self, role: RoleSkillMetadata, target_dir: Path, lang: str, force: bool
|
|
543
|
+
) -> bool:
|
|
559
544
|
"""
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
568
|
-
|
|
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
|
-
#
|
|
571
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
778
|
+
Number of workflow files removed
|
|
628
779
|
"""
|
|
629
|
-
|
|
630
|
-
|
|
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)
|