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/registry.py
CHANGED
|
@@ -1,39 +1,54 @@
|
|
|
1
|
-
from typing import Dict, List
|
|
1
|
+
from typing import Dict, List, Optional
|
|
2
2
|
from monoco.core.feature import MonocoFeature
|
|
3
|
+
from monoco.core.loader import FeatureLoader, FeatureRegistry as LoaderFeatureRegistry
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
class FeatureRegistry:
|
|
6
|
-
|
|
7
|
+
"""
|
|
8
|
+
Feature registry that wraps the new unified FeatureLoader.
|
|
9
|
+
|
|
10
|
+
This class provides backward compatibility while delegating to the
|
|
11
|
+
new FeatureLoader for dynamic discovery and lifecycle management.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
_loader: Optional[FeatureLoader] = None
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def _get_loader(cls) -> FeatureLoader:
|
|
18
|
+
"""Get or create the default feature loader."""
|
|
19
|
+
if cls._loader is None:
|
|
20
|
+
cls._loader = FeatureLoader()
|
|
21
|
+
# Discover and load all features
|
|
22
|
+
cls._loader.discover()
|
|
23
|
+
cls._loader.load_all()
|
|
24
|
+
return cls._loader
|
|
7
25
|
|
|
8
26
|
@classmethod
|
|
9
27
|
def register(cls, feature: MonocoFeature):
|
|
10
28
|
"""Register a feature instance."""
|
|
11
|
-
|
|
29
|
+
loader = cls._get_loader()
|
|
30
|
+
loader.registry.register(feature) # type: ignore
|
|
12
31
|
|
|
13
32
|
@classmethod
|
|
14
33
|
def get_features(cls) -> List[MonocoFeature]:
|
|
15
34
|
"""Get all registered features."""
|
|
16
|
-
|
|
35
|
+
loader = cls._get_loader()
|
|
36
|
+
return loader.registry.get_all() # type: ignore
|
|
17
37
|
|
|
18
38
|
@classmethod
|
|
19
|
-
def get_feature(cls, name: str) -> MonocoFeature:
|
|
39
|
+
def get_feature(cls, name: str) -> Optional[MonocoFeature]:
|
|
20
40
|
"""Get a specific feature by name."""
|
|
21
|
-
|
|
41
|
+
loader = cls._get_loader()
|
|
42
|
+
return loader.registry.get(name) # type: ignore
|
|
22
43
|
|
|
23
44
|
@classmethod
|
|
24
45
|
def load_defaults(cls):
|
|
25
46
|
"""
|
|
26
|
-
Load default core features.
|
|
27
|
-
|
|
47
|
+
Load default core features using the unified FeatureLoader.
|
|
48
|
+
|
|
49
|
+
This method discovers and loads all features from monoco/features/
|
|
50
|
+
automatically, replacing the manual registration approach.
|
|
28
51
|
"""
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
from monoco.features.i18n.adapter import I18nFeature
|
|
33
|
-
from monoco.features.memo.adapter import MemoFeature
|
|
34
|
-
|
|
35
|
-
cls.register(IssueFeature())
|
|
36
|
-
cls.register(SpikeFeature())
|
|
37
|
-
cls.register(I18nFeature())
|
|
38
|
-
cls.register(MemoFeature())
|
|
39
|
-
pass
|
|
52
|
+
loader = cls._get_loader()
|
|
53
|
+
# Features are already discovered and loaded in _get_loader
|
|
54
|
+
# This method is kept for backward compatibility
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import List, Generator, Union
|
|
4
|
+
import importlib.util
|
|
5
|
+
|
|
6
|
+
# Use standard importlib.resources for Python 3.9+
|
|
7
|
+
if sys.version_info < (3, 9):
|
|
8
|
+
# Fallback or error - for now assume 3.9+ as this is a modern toolkit
|
|
9
|
+
raise RuntimeError("Monoco requires Python 3.9+")
|
|
10
|
+
from importlib.resources import files, as_file
|
|
11
|
+
|
|
12
|
+
from .models import ResourceNode, ResourceType
|
|
13
|
+
|
|
14
|
+
class ResourceFinder:
|
|
15
|
+
"""
|
|
16
|
+
Scans Python packages for Monoco standard resources.
|
|
17
|
+
Standard Layout: <package>/resources/<lang>/<type>/<file>
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def scan_package(self, package_name: str) -> List[ResourceNode]:
|
|
21
|
+
"""
|
|
22
|
+
Traverses the 'resources' directory of a given package.
|
|
23
|
+
Returns a flat list of ResourceNode objects.
|
|
24
|
+
"""
|
|
25
|
+
nodes = []
|
|
26
|
+
|
|
27
|
+
# Check if package exists
|
|
28
|
+
if not importlib.util.find_spec(package_name):
|
|
29
|
+
return []
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
pkg_root = files(package_name)
|
|
33
|
+
resources_root = pkg_root.joinpath("resources")
|
|
34
|
+
|
|
35
|
+
if not resources_root.is_dir():
|
|
36
|
+
return []
|
|
37
|
+
|
|
38
|
+
# Iterate over languages (direct children of resources/)
|
|
39
|
+
for lang_dir in resources_root.iterdir():
|
|
40
|
+
if not lang_dir.is_dir() or lang_dir.name.startswith("_"):
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
lang = lang_dir.name
|
|
44
|
+
|
|
45
|
+
# Iterate over resource types (children of lang/)
|
|
46
|
+
for type_dir in lang_dir.iterdir():
|
|
47
|
+
if not type_dir.is_dir() or type_dir.name.startswith("_"):
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
res_type = ResourceType(type_dir.name)
|
|
52
|
+
except ValueError:
|
|
53
|
+
res_type = ResourceType.OTHER
|
|
54
|
+
|
|
55
|
+
# Iterate over files (children of type/)
|
|
56
|
+
# Note: This effectively supports shallow structure.
|
|
57
|
+
# For recursive (like skills folders), we might need recursion.
|
|
58
|
+
# For now, let's assume flat files or folders treated as units (like flow skill dirs).
|
|
59
|
+
|
|
60
|
+
for item in type_dir.iterdir():
|
|
61
|
+
# For skills, the item might be a directory (Flow Skill)
|
|
62
|
+
# We treat the directory path as the resource path in that case?
|
|
63
|
+
# Or we recursively scan?
|
|
64
|
+
# ResourceNode expects a path.
|
|
65
|
+
|
|
66
|
+
# Use as_file to ensure we have a filesystem path (needed for symlinks/copy)
|
|
67
|
+
with as_file(item) as item_path:
|
|
68
|
+
# Note: as_file context manager keeps the temporary file alive if extracted from zip.
|
|
69
|
+
# But here we probably want the path to persist?
|
|
70
|
+
# if it's a real file system, item_path is the real path.
|
|
71
|
+
|
|
72
|
+
if item.is_dir():
|
|
73
|
+
# Flow skills are directories
|
|
74
|
+
# We add the directory itself as a node?
|
|
75
|
+
if res_type == ResourceType.SKILLS:
|
|
76
|
+
nodes.append(ResourceNode(
|
|
77
|
+
name=item.name,
|
|
78
|
+
path=item_path,
|
|
79
|
+
type=res_type,
|
|
80
|
+
language=lang
|
|
81
|
+
))
|
|
82
|
+
elif item.is_file():
|
|
83
|
+
if item.name.startswith("."):
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
nodes.append(ResourceNode(
|
|
87
|
+
name=item.name,
|
|
88
|
+
path=item_path,
|
|
89
|
+
type=res_type,
|
|
90
|
+
language=lang
|
|
91
|
+
))
|
|
92
|
+
|
|
93
|
+
except Exception as e:
|
|
94
|
+
# gracefully handle errors, maybe log?
|
|
95
|
+
print(f"Warning: Error scanning resources in {package_name}: {e}")
|
|
96
|
+
return []
|
|
97
|
+
|
|
98
|
+
return nodes
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from typing import List, Optional, Dict
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import shutil
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from .models import ResourceNode, ResourceType
|
|
7
|
+
from .finder import ResourceFinder
|
|
8
|
+
|
|
9
|
+
class ResourceManager:
|
|
10
|
+
def __init__(self, source_lang: str = "en"):
|
|
11
|
+
self.finder = ResourceFinder()
|
|
12
|
+
self.source_lang = source_lang
|
|
13
|
+
|
|
14
|
+
def list_resources(self, package: str, type: Optional[ResourceType] = None, lang: Optional[str] = None) -> List[ResourceNode]:
|
|
15
|
+
"""
|
|
16
|
+
Low-level listing of resources with optional exact filtering.
|
|
17
|
+
"""
|
|
18
|
+
all_nodes = self.finder.scan_package(package)
|
|
19
|
+
filtered = []
|
|
20
|
+
for node in all_nodes:
|
|
21
|
+
if type and node.type != type:
|
|
22
|
+
continue
|
|
23
|
+
if lang and node.language != lang:
|
|
24
|
+
continue
|
|
25
|
+
filtered.append(node)
|
|
26
|
+
return filtered
|
|
27
|
+
|
|
28
|
+
def get_merged_resources(self, package: str, type: ResourceType, target_lang: str) -> List[ResourceNode]:
|
|
29
|
+
"""
|
|
30
|
+
Get resources of a specific type, merging source language defaults with target language overrides.
|
|
31
|
+
Returns a list of unique resources (by name), prioritizing target_lang.
|
|
32
|
+
"""
|
|
33
|
+
all_nodes = self.finder.scan_package(package)
|
|
34
|
+
type_nodes = [n for n in all_nodes if n.type == type]
|
|
35
|
+
|
|
36
|
+
# Dictionary to hold the best match for each filename: name -> ResourceNode
|
|
37
|
+
best_matches: Dict[str, ResourceNode] = {}
|
|
38
|
+
|
|
39
|
+
# 1. Populate with source language (Default Base)
|
|
40
|
+
for node in type_nodes:
|
|
41
|
+
if node.language == self.source_lang:
|
|
42
|
+
best_matches[node.name] = node
|
|
43
|
+
|
|
44
|
+
# 2. Override with target language if different
|
|
45
|
+
if target_lang != self.source_lang:
|
|
46
|
+
for node in type_nodes:
|
|
47
|
+
if node.language == target_lang:
|
|
48
|
+
best_matches[node.name] = node
|
|
49
|
+
|
|
50
|
+
return list(best_matches.values())
|
|
51
|
+
|
|
52
|
+
def extract_to(self, nodes: List[ResourceNode], destination: Path, symlink: bool = False, force: bool = True) -> int:
|
|
53
|
+
"""
|
|
54
|
+
Extracts (copy or symlink) resources to the destination directory.
|
|
55
|
+
Returns count of extracted items.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
nodes: List of resources to extract
|
|
59
|
+
destination: Target directory
|
|
60
|
+
symlink: If True, create symbolic links instead of copying
|
|
61
|
+
force: If True, overwrite existing files/symlinks
|
|
62
|
+
"""
|
|
63
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
count = 0
|
|
65
|
+
|
|
66
|
+
for node in nodes:
|
|
67
|
+
dest_path = destination / node.name
|
|
68
|
+
|
|
69
|
+
if dest_path.exists():
|
|
70
|
+
if not force:
|
|
71
|
+
continue
|
|
72
|
+
# Remove existing
|
|
73
|
+
if dest_path.is_symlink() or dest_path.is_file():
|
|
74
|
+
dest_path.unlink()
|
|
75
|
+
elif dest_path.is_dir():
|
|
76
|
+
shutil.rmtree(dest_path)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
if symlink:
|
|
80
|
+
# Symlink target must be absolute
|
|
81
|
+
dest_path.symlink_to(node.path)
|
|
82
|
+
else:
|
|
83
|
+
if node.path.is_dir():
|
|
84
|
+
shutil.copytree(node.path, dest_path)
|
|
85
|
+
else:
|
|
86
|
+
shutil.copy2(node.path, dest_path)
|
|
87
|
+
count += 1
|
|
88
|
+
except Exception as e:
|
|
89
|
+
print(f"Error extracting {node.name}: {e}")
|
|
90
|
+
|
|
91
|
+
return count
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional, List
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
class ResourceType(str, Enum):
|
|
7
|
+
PROMPTS = "prompts"
|
|
8
|
+
RULES = "rules"
|
|
9
|
+
SKILLS = "skills"
|
|
10
|
+
ROLES = "roles"
|
|
11
|
+
GLOSSARY = "glossary"
|
|
12
|
+
TEMPLATES = "templates"
|
|
13
|
+
DOCS = "docs"
|
|
14
|
+
OTHER = "other"
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ResourceNode:
|
|
18
|
+
"""
|
|
19
|
+
Represents a discovered resource file in a Python package.
|
|
20
|
+
"""
|
|
21
|
+
name: str
|
|
22
|
+
path: Path # Absolute path to the source file
|
|
23
|
+
type: ResourceType
|
|
24
|
+
language: str # "en", "zh", etc.
|
|
25
|
+
content: Optional[str] = None # Lazy loaded content
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def key(self) -> str:
|
|
29
|
+
"""Unique identifier for the resource (e.g. 'agent.prompts.system')"""
|
|
30
|
+
return f"{self.type.value}.{self.name}"
|
|
31
|
+
|
|
32
|
+
def read_text(self) -> str:
|
|
33
|
+
if self.content:
|
|
34
|
+
return self.content
|
|
35
|
+
return self.path.read_text(encoding="utf-8")
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Skill Framework for Monoco Toolkit - Three-Level Architecture
|
|
3
|
+
|
|
4
|
+
This module implements the Role-Workflow-Atom three-level skill architecture:
|
|
5
|
+
|
|
6
|
+
1. Atom Skills (atom): Atomic capabilities that perform single operations
|
|
7
|
+
- atom-issue-lifecycle: Issue lifecycle operations (create, start, submit, close)
|
|
8
|
+
- atom-code-dev: Code development operations (investigate, implement, test, document)
|
|
9
|
+
- atom-knowledge: Knowledge management operations (capture, process, convert, archive)
|
|
10
|
+
- atom-review: Review operations (checkout, verify, challenge, feedback)
|
|
11
|
+
|
|
12
|
+
2. Workflow Skills (workflow): Orchestration of atom skills into workflows
|
|
13
|
+
- workflow-dev: Development workflow (setup → investigate → implement → test → submit)
|
|
14
|
+
- workflow-issue-create: Issue creation workflow (extract → classify → create)
|
|
15
|
+
- workflow-review: Review workflow (checkout → verify → challenge → decide)
|
|
16
|
+
|
|
17
|
+
3. Role Skills (role): Configuration layer defining default workflow and preferences
|
|
18
|
+
- role-engineer: Engineer role (uses workflow-dev, autopilot mode)
|
|
19
|
+
- role-manager: Manager role (uses workflow-planning, copilot mode)
|
|
20
|
+
- role-planner: Planner role (uses workflow-design, copilot mode)
|
|
21
|
+
- role-reviewer: Reviewer role (uses workflow-review, autopilot mode)
|
|
22
|
+
|
|
23
|
+
Key Design Principles:
|
|
24
|
+
- Single Responsibility: Each atom skill does one thing
|
|
25
|
+
- Composition over Inheritance: Workflows compose atoms
|
|
26
|
+
- Mode Agnostic: Same workflow supports both copilot and autopilot modes
|
|
27
|
+
- Convention over Configuration: System constraints defined once in atom layer
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
from enum import Enum
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Any, Dict, List, Optional, Set, Union
|
|
34
|
+
from pydantic import BaseModel, Field, model_validator
|
|
35
|
+
import yaml
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SkillMode(str, Enum):
|
|
39
|
+
"""Execution mode for skills."""
|
|
40
|
+
COPILOT = "copilot" # Human-led, AI-assisted
|
|
41
|
+
AUTOPILOT = "autopilot" # AI-led, automatic execution
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SkillType(str, Enum):
|
|
45
|
+
"""Skill type in the three-level architecture."""
|
|
46
|
+
ATOM = "atom" # Atomic capability
|
|
47
|
+
WORKFLOW = "workflow" # Workflow orchestration
|
|
48
|
+
ROLE = "role" # Role configuration
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ComplianceRule(BaseModel):
|
|
52
|
+
"""A compliance rule."""
|
|
53
|
+
rule: str = Field(..., description="Rule description")
|
|
54
|
+
severity: str = Field(default="warning", description="Rule severity: error, warning, info")
|
|
55
|
+
check: Optional[str] = Field(default=None, description="Check command or condition")
|
|
56
|
+
command: Optional[str] = Field(default=None, description="Associated CLI command")
|
|
57
|
+
mindset: Optional[str] = Field(default=None, description="Related mindset/principle")
|
|
58
|
+
fail_if: Optional[str] = Field(default=None, description="Condition that causes failure")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AtomOperation(BaseModel):
|
|
62
|
+
"""An operation within an atom skill."""
|
|
63
|
+
name: str = Field(..., description="Operation name (e.g., 'create', 'start')")
|
|
64
|
+
description: str = Field(..., description="What this operation does")
|
|
65
|
+
command: Optional[str] = Field(default=None, description="Associated CLI command")
|
|
66
|
+
reminder: Optional[str] = Field(default=None, description="Reminder text for this operation")
|
|
67
|
+
compliance_rules: List[ComplianceRule] = Field(default_factory=list, description="Compliance rules for this operation")
|
|
68
|
+
checkpoints: List[str] = Field(default_factory=list, description="Checkpoints for this operation")
|
|
69
|
+
output: Optional[str] = Field(default=None, description="Expected output")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Checkpoint(BaseModel):
|
|
73
|
+
"""A checkpoint in a workflow stage."""
|
|
74
|
+
description: str = Field(..., description="What to check")
|
|
75
|
+
atom_skill: str = Field(..., description="Atom skill to use")
|
|
76
|
+
operation: str = Field(..., description="Operation to invoke")
|
|
77
|
+
reminder: Optional[str] = Field(default=None, description="Reminder at this checkpoint")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class WorkflowStage(BaseModel):
|
|
81
|
+
"""A stage in a workflow."""
|
|
82
|
+
name: str = Field(..., description="Stage name")
|
|
83
|
+
atom_skill: Optional[str] = Field(default=None, description="Atom skill to use (optional for virtual stages)")
|
|
84
|
+
operation: Optional[str] = Field(default=None, description="Operation to invoke (optional for virtual stages)")
|
|
85
|
+
description: Optional[str] = Field(default=None, description="Stage description")
|
|
86
|
+
reminder: Optional[str] = Field(default=None, description="Reminder for this stage")
|
|
87
|
+
checkpoints: List[Checkpoint] = Field(default_factory=list, description="Checkpoints within this stage")
|
|
88
|
+
next_stages: Dict[str, str] = Field(default_factory=dict, description="Conditional next stages")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ModeConfig(BaseModel):
|
|
92
|
+
"""Configuration for a specific execution mode."""
|
|
93
|
+
behavior: str = Field(..., description="Behavior description")
|
|
94
|
+
pause_on: List[str] = Field(default_factory=list, description="Stages to pause on")
|
|
95
|
+
auto_execute: bool = Field(default=False, description="Whether to auto-execute stages")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ============================================================================
|
|
99
|
+
# Atom Skill Models
|
|
100
|
+
# ============================================================================
|
|
101
|
+
|
|
102
|
+
class AtomSkillMetadata(BaseModel):
|
|
103
|
+
"""Metadata for an atom skill."""
|
|
104
|
+
name: str = Field(..., description="Unique atom skill name (e.g., 'atom-issue-lifecycle')")
|
|
105
|
+
type: str = Field(default="atom", description="Skill type (always 'atom')")
|
|
106
|
+
domain: str = Field(..., description="Domain (e.g., 'issue', 'code', 'knowledge', 'review')")
|
|
107
|
+
description: str = Field(..., description="What this atom skill provides")
|
|
108
|
+
version: str = Field(default="1.0.0", description="Skill version")
|
|
109
|
+
author: Optional[str] = Field(default=None, description="Skill author")
|
|
110
|
+
|
|
111
|
+
operations: List[AtomOperation] = Field(..., description="Available operations")
|
|
112
|
+
compliance_rules: List[ComplianceRule] = Field(default_factory=list, description="System-level compliance rules")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ============================================================================
|
|
116
|
+
# Workflow Skill Models
|
|
117
|
+
# ============================================================================
|
|
118
|
+
|
|
119
|
+
class WorkflowSkillMetadata(BaseModel):
|
|
120
|
+
"""Metadata for a workflow skill."""
|
|
121
|
+
name: str = Field(..., description="Unique workflow skill name (e.g., 'workflow-dev')")
|
|
122
|
+
type: str = Field(default="workflow", description="Skill type (always 'workflow')")
|
|
123
|
+
description: str = Field(..., description="What this workflow orchestrates")
|
|
124
|
+
version: str = Field(default="1.0.0", description="Skill version")
|
|
125
|
+
author: Optional[str] = Field(default=None, description="Skill author")
|
|
126
|
+
|
|
127
|
+
dependencies: List[str] = Field(..., description="Required atom skills")
|
|
128
|
+
stages: List[WorkflowStage] = Field(..., description="Workflow stages")
|
|
129
|
+
|
|
130
|
+
mode_config: Dict[SkillMode, ModeConfig] = Field(
|
|
131
|
+
default_factory=dict,
|
|
132
|
+
description="Configuration for copilot and autopilot modes"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ============================================================================
|
|
137
|
+
# Role Skill Models
|
|
138
|
+
# ============================================================================
|
|
139
|
+
|
|
140
|
+
class RolePreference(BaseModel):
|
|
141
|
+
"""A preference for a role."""
|
|
142
|
+
category: str = Field(..., description="Preference category")
|
|
143
|
+
value: str = Field(..., description="Preference value")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class RoleSkillMetadata(BaseModel):
|
|
147
|
+
"""Metadata for a role skill."""
|
|
148
|
+
name: str = Field(..., description="Unique role name (e.g., 'role-engineer')")
|
|
149
|
+
type: str = Field(default="role", description="Skill type (always 'role')")
|
|
150
|
+
description: str = Field(..., description="Role description")
|
|
151
|
+
version: str = Field(default="1.0.0", description="Skill version")
|
|
152
|
+
author: Optional[str] = Field(default=None, description="Skill author")
|
|
153
|
+
|
|
154
|
+
workflow: str = Field(..., description="Default workflow skill to use")
|
|
155
|
+
default_mode: SkillMode = Field(default=SkillMode.COPILOT, description="Default execution mode")
|
|
156
|
+
|
|
157
|
+
preferences: List[str] = Field(default_factory=list, description="Role preferences/mindset")
|
|
158
|
+
system_prompt: Optional[str] = Field(default=None, description="System prompt for this role")
|
|
159
|
+
trigger: Optional[str] = Field(default=None, description="When to trigger this role")
|
|
160
|
+
goal: Optional[str] = Field(default=None, description="Goal of this role")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ============================================================================
|
|
164
|
+
# Unified Skill Loader
|
|
165
|
+
# ============================================================================
|
|
166
|
+
|
|
167
|
+
class SkillLoader:
|
|
168
|
+
"""Loader for the three-level skill architecture."""
|
|
169
|
+
|
|
170
|
+
def __init__(self, resources_dir: Path):
|
|
171
|
+
self.resources_dir = resources_dir
|
|
172
|
+
self._atoms: Dict[str, AtomSkillMetadata] = {}
|
|
173
|
+
self._workflows: Dict[str, WorkflowSkillMetadata] = {}
|
|
174
|
+
self._roles: Dict[str, RoleSkillMetadata] = {}
|
|
175
|
+
|
|
176
|
+
def load_all(self) -> None:
|
|
177
|
+
"""Load all skills from resources directory."""
|
|
178
|
+
self._load_atoms()
|
|
179
|
+
self._load_workflows()
|
|
180
|
+
self._load_roles()
|
|
181
|
+
|
|
182
|
+
def _load_atoms(self) -> None:
|
|
183
|
+
"""Load atom skills from resources/atoms/.
|
|
184
|
+
|
|
185
|
+
Only loads files starting with 'atom-' prefix.
|
|
186
|
+
"""
|
|
187
|
+
atoms_dir = self.resources_dir / "atoms"
|
|
188
|
+
if not atoms_dir.exists():
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
for skill_file in atoms_dir.glob("atom-*.yaml"):
|
|
192
|
+
try:
|
|
193
|
+
data = yaml.safe_load(skill_file.read_text())
|
|
194
|
+
atom = AtomSkillMetadata(**data)
|
|
195
|
+
self._atoms[atom.name] = atom
|
|
196
|
+
except Exception as e:
|
|
197
|
+
print(f"Failed to load atom skill {skill_file}: {e}")
|
|
198
|
+
|
|
199
|
+
def _load_workflows(self) -> None:
|
|
200
|
+
"""Load workflow skills from resources/workflows/.
|
|
201
|
+
|
|
202
|
+
Only loads files starting with 'workflow-' prefix.
|
|
203
|
+
"""
|
|
204
|
+
workflows_dir = self.resources_dir / "workflows"
|
|
205
|
+
if not workflows_dir.exists():
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
for skill_file in workflows_dir.glob("workflow-*.yaml"):
|
|
209
|
+
try:
|
|
210
|
+
data = yaml.safe_load(skill_file.read_text())
|
|
211
|
+
workflow = WorkflowSkillMetadata(**data)
|
|
212
|
+
self._workflows[workflow.name] = workflow
|
|
213
|
+
except Exception as e:
|
|
214
|
+
print(f"Failed to load workflow skill {skill_file}: {e}")
|
|
215
|
+
|
|
216
|
+
def _load_roles(self) -> None:
|
|
217
|
+
"""Load role skills from resources/roles/.
|
|
218
|
+
|
|
219
|
+
Only loads files starting with 'role-' prefix to avoid conflicts
|
|
220
|
+
with legacy role definitions.
|
|
221
|
+
"""
|
|
222
|
+
roles_dir = self.resources_dir / "roles"
|
|
223
|
+
if not roles_dir.exists():
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
for skill_file in roles_dir.glob("role-*.yaml"):
|
|
227
|
+
try:
|
|
228
|
+
data = yaml.safe_load(skill_file.read_text())
|
|
229
|
+
role = RoleSkillMetadata(**data)
|
|
230
|
+
self._roles[role.name] = role
|
|
231
|
+
except Exception as e:
|
|
232
|
+
print(f"Failed to load role skill {skill_file}: {e}")
|
|
233
|
+
|
|
234
|
+
def get_atom(self, name: str) -> Optional[AtomSkillMetadata]:
|
|
235
|
+
"""Get an atom skill by name."""
|
|
236
|
+
return self._atoms.get(name)
|
|
237
|
+
|
|
238
|
+
def get_workflow(self, name: str) -> Optional[WorkflowSkillMetadata]:
|
|
239
|
+
"""Get a workflow skill by name."""
|
|
240
|
+
return self._workflows.get(name)
|
|
241
|
+
|
|
242
|
+
def get_role(self, name: str) -> Optional[RoleSkillMetadata]:
|
|
243
|
+
"""Get a role skill by name."""
|
|
244
|
+
return self._roles.get(name)
|
|
245
|
+
|
|
246
|
+
def list_atoms(self) -> List[AtomSkillMetadata]:
|
|
247
|
+
"""List all atom skills."""
|
|
248
|
+
return list(self._atoms.values())
|
|
249
|
+
|
|
250
|
+
def list_workflows(self) -> List[WorkflowSkillMetadata]:
|
|
251
|
+
"""List all workflow skills."""
|
|
252
|
+
return list(self._workflows.values())
|
|
253
|
+
|
|
254
|
+
def list_roles(self) -> List[RoleSkillMetadata]:
|
|
255
|
+
"""List all role skills."""
|
|
256
|
+
return list(self._roles.values())
|
|
257
|
+
|
|
258
|
+
def resolve_role_workflow(self, role_name: str) -> Optional[WorkflowSkillMetadata]:
|
|
259
|
+
"""Resolve a role to its workflow."""
|
|
260
|
+
role = self.get_role(role_name)
|
|
261
|
+
if role:
|
|
262
|
+
return self.get_workflow(role.workflow)
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
def validate_workflow(self, workflow_name: str) -> List[str]:
|
|
266
|
+
"""Validate a workflow's dependencies are satisfied."""
|
|
267
|
+
errors = []
|
|
268
|
+
workflow = self.get_workflow(workflow_name)
|
|
269
|
+
if not workflow:
|
|
270
|
+
return [f"Workflow '{workflow_name}' not found"]
|
|
271
|
+
|
|
272
|
+
for dep in workflow.dependencies:
|
|
273
|
+
if not self.get_atom(dep):
|
|
274
|
+
errors.append(f"Missing atom skill dependency: {dep}")
|
|
275
|
+
|
|
276
|
+
for stage in workflow.stages:
|
|
277
|
+
# Virtual stages (decision points) don't need atom skills
|
|
278
|
+
if not stage.atom_skill or not stage.operation:
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
atom = self.get_atom(stage.atom_skill)
|
|
282
|
+
if not atom:
|
|
283
|
+
errors.append(f"Stage '{stage.name}' uses unknown atom skill: {stage.atom_skill}")
|
|
284
|
+
else:
|
|
285
|
+
op_names = [op.name for op in atom.operations]
|
|
286
|
+
if stage.operation not in op_names:
|
|
287
|
+
errors.append(
|
|
288
|
+
f"Stage '{stage.name}' uses unknown operation '{stage.operation}' "
|
|
289
|
+
f"in atom skill '{stage.atom_skill}'"
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
return errors
|