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