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
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hook Context - Data passed to hooks during session lifecycle events.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional, Dict, Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class IssueInfo:
|
|
13
|
+
"""Information about the issue associated with a session."""
|
|
14
|
+
id: str
|
|
15
|
+
status: Optional[str] = None
|
|
16
|
+
stage: Optional[str] = None
|
|
17
|
+
title: Optional[str] = None
|
|
18
|
+
branch_name: Optional[str] = None
|
|
19
|
+
is_merged: bool = False
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_metadata(cls, metadata: Any) -> "IssueInfo":
|
|
23
|
+
"""Create IssueInfo from IssueMetadata."""
|
|
24
|
+
return cls(
|
|
25
|
+
id=getattr(metadata, "id", ""),
|
|
26
|
+
status=getattr(metadata, "status", None),
|
|
27
|
+
stage=getattr(metadata, "stage", None),
|
|
28
|
+
title=getattr(metadata, "title", None),
|
|
29
|
+
branch_name=getattr(metadata, "isolation", {}).get("ref") if hasattr(metadata, "isolation") and metadata.isolation else None,
|
|
30
|
+
is_merged=False, # Will be determined by GitCleanupHook
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class GitInfo:
|
|
36
|
+
"""Git repository information."""
|
|
37
|
+
project_root: Path
|
|
38
|
+
current_branch: Optional[str] = None
|
|
39
|
+
has_uncommitted_changes: bool = False
|
|
40
|
+
default_branch: str = "main"
|
|
41
|
+
|
|
42
|
+
def __post_init__(self):
|
|
43
|
+
if self.current_branch is None:
|
|
44
|
+
# Lazy load current branch
|
|
45
|
+
try:
|
|
46
|
+
from monoco.core import git
|
|
47
|
+
self.current_branch = git.get_current_branch(self.project_root)
|
|
48
|
+
except Exception:
|
|
49
|
+
self.current_branch = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class HookContext:
|
|
54
|
+
"""
|
|
55
|
+
Context object passed to lifecycle hooks.
|
|
56
|
+
|
|
57
|
+
Contains all relevant information about the session, issue, and environment
|
|
58
|
+
that hooks might need to perform their operations.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
# Session Information
|
|
62
|
+
session_id: str
|
|
63
|
+
role_name: str
|
|
64
|
+
session_status: str
|
|
65
|
+
created_at: datetime
|
|
66
|
+
|
|
67
|
+
# Issue Information
|
|
68
|
+
issue: Optional[IssueInfo] = None
|
|
69
|
+
|
|
70
|
+
# Git Information
|
|
71
|
+
git: Optional[GitInfo] = None
|
|
72
|
+
|
|
73
|
+
# Additional Context
|
|
74
|
+
extra: Dict[str, Any] = field(default_factory=dict)
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def from_runtime_session(
|
|
78
|
+
cls,
|
|
79
|
+
runtime_session: Any,
|
|
80
|
+
project_root: Optional[Path] = None,
|
|
81
|
+
) -> "HookContext":
|
|
82
|
+
"""
|
|
83
|
+
Create a HookContext from a RuntimeSession.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
runtime_session: The RuntimeSession object
|
|
87
|
+
project_root: Optional project root path
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
A populated HookContext
|
|
91
|
+
"""
|
|
92
|
+
model = runtime_session.model
|
|
93
|
+
|
|
94
|
+
# Build IssueInfo if we have an issue_id
|
|
95
|
+
issue_info = None
|
|
96
|
+
if model.issue_id:
|
|
97
|
+
issue_info = IssueInfo(
|
|
98
|
+
id=model.issue_id,
|
|
99
|
+
branch_name=model.branch_name,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Try to load full issue metadata
|
|
103
|
+
try:
|
|
104
|
+
from monoco.features.issue.core import find_issue_path, parse_issue
|
|
105
|
+
from monoco.core.config import find_monoco_root
|
|
106
|
+
|
|
107
|
+
if project_root is None:
|
|
108
|
+
project_root = find_monoco_root()
|
|
109
|
+
|
|
110
|
+
issues_root = project_root / "Issues"
|
|
111
|
+
issue_path = find_issue_path(issues_root, model.issue_id)
|
|
112
|
+
if issue_path:
|
|
113
|
+
metadata = parse_issue(issue_path)
|
|
114
|
+
if metadata:
|
|
115
|
+
issue_info = IssueInfo.from_metadata(metadata)
|
|
116
|
+
except Exception:
|
|
117
|
+
pass # Use basic issue info
|
|
118
|
+
|
|
119
|
+
# Build GitInfo
|
|
120
|
+
git_info = None
|
|
121
|
+
if project_root:
|
|
122
|
+
git_info = GitInfo(project_root=project_root)
|
|
123
|
+
|
|
124
|
+
return cls(
|
|
125
|
+
session_id=model.id,
|
|
126
|
+
role_name=model.role_name,
|
|
127
|
+
session_status=model.status,
|
|
128
|
+
created_at=model.created_at,
|
|
129
|
+
issue=issue_info,
|
|
130
|
+
git=git_info,
|
|
131
|
+
)
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hook Registry - Manages registration and execution of session lifecycle hooks.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import List, Type, Optional, Dict, Any
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .base import SessionLifecycleHook, HookResult, HookStatus
|
|
10
|
+
from .context import HookContext
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("monoco.core.hooks")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HookRegistry:
|
|
16
|
+
"""
|
|
17
|
+
Registry for managing session lifecycle hooks.
|
|
18
|
+
|
|
19
|
+
Responsible for:
|
|
20
|
+
- Registering hooks
|
|
21
|
+
- Executing hooks in order
|
|
22
|
+
- Handling hook errors gracefully
|
|
23
|
+
- Loading hooks from configuration
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self._hooks: List[SessionLifecycleHook] = []
|
|
28
|
+
self._hook_classes: Dict[str, Type[SessionLifecycleHook]] = {}
|
|
29
|
+
|
|
30
|
+
def register(self, hook: SessionLifecycleHook) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Register a hook instance.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
hook: The hook instance to register
|
|
36
|
+
"""
|
|
37
|
+
if not isinstance(hook, SessionLifecycleHook):
|
|
38
|
+
raise TypeError(f"Hook must be a SessionLifecycleHook, got {type(hook)}")
|
|
39
|
+
|
|
40
|
+
self._hooks.append(hook)
|
|
41
|
+
logger.debug(f"Registered hook: {hook.name}")
|
|
42
|
+
|
|
43
|
+
def register_class(
|
|
44
|
+
self,
|
|
45
|
+
hook_class: Type[SessionLifecycleHook],
|
|
46
|
+
name: Optional[str] = None,
|
|
47
|
+
config: Optional[Dict[str, Any]] = None
|
|
48
|
+
) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Register a hook class (will be instantiated).
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
hook_class: The hook class to register
|
|
54
|
+
name: Optional name for the hook instance
|
|
55
|
+
config: Optional configuration for the hook
|
|
56
|
+
"""
|
|
57
|
+
if not issubclass(hook_class, SessionLifecycleHook):
|
|
58
|
+
raise TypeError(f"Hook class must inherit from SessionLifecycleHook")
|
|
59
|
+
|
|
60
|
+
instance = hook_class(name=name, config=config)
|
|
61
|
+
self.register(instance)
|
|
62
|
+
|
|
63
|
+
def unregister(self, name: str) -> bool:
|
|
64
|
+
"""
|
|
65
|
+
Unregister a hook by name.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
name: The name of the hook to unregister
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
True if a hook was removed, False otherwise
|
|
72
|
+
"""
|
|
73
|
+
for i, hook in enumerate(self._hooks):
|
|
74
|
+
if hook.name == name:
|
|
75
|
+
self._hooks.pop(i)
|
|
76
|
+
logger.debug(f"Unregistered hook: {name}")
|
|
77
|
+
return True
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
def get_hooks(self, enabled_only: bool = True) -> List[SessionLifecycleHook]:
|
|
81
|
+
"""
|
|
82
|
+
Get all registered hooks.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
enabled_only: If True, only return enabled hooks
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
List of hook instances
|
|
89
|
+
"""
|
|
90
|
+
if enabled_only:
|
|
91
|
+
return [h for h in self._hooks if h.is_enabled()]
|
|
92
|
+
return self._hooks.copy()
|
|
93
|
+
|
|
94
|
+
def clear(self) -> None:
|
|
95
|
+
"""Clear all registered hooks."""
|
|
96
|
+
self._hooks.clear()
|
|
97
|
+
logger.debug("Cleared all hooks")
|
|
98
|
+
|
|
99
|
+
def execute_on_session_start(self, context: HookContext) -> List[HookResult]:
|
|
100
|
+
"""
|
|
101
|
+
Execute all registered hooks' on_session_start methods.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
context: The hook context
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
List of results from each hook
|
|
108
|
+
"""
|
|
109
|
+
return self._execute_hooks("on_session_start", context)
|
|
110
|
+
|
|
111
|
+
def execute_on_session_end(self, context: HookContext) -> List[HookResult]:
|
|
112
|
+
"""
|
|
113
|
+
Execute all registered hooks' on_session_end methods.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
context: The hook context
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
List of results from each hook
|
|
120
|
+
"""
|
|
121
|
+
return self._execute_hooks("on_session_end", context)
|
|
122
|
+
|
|
123
|
+
def _execute_hooks(
|
|
124
|
+
self,
|
|
125
|
+
method_name: str,
|
|
126
|
+
context: HookContext
|
|
127
|
+
) -> List[HookResult]:
|
|
128
|
+
"""
|
|
129
|
+
Execute a hook method on all registered hooks.
|
|
130
|
+
|
|
131
|
+
Errors in individual hooks don't stop execution of other hooks.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
method_name: The name of the method to call
|
|
135
|
+
context: The hook context
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
List of results from each hook
|
|
139
|
+
"""
|
|
140
|
+
results = []
|
|
141
|
+
hooks = self.get_hooks(enabled_only=True)
|
|
142
|
+
|
|
143
|
+
for hook in hooks:
|
|
144
|
+
try:
|
|
145
|
+
method = getattr(hook, method_name)
|
|
146
|
+
result = method(context)
|
|
147
|
+
results.append(result)
|
|
148
|
+
|
|
149
|
+
if result.status == HookStatus.FAILURE:
|
|
150
|
+
logger.warning(
|
|
151
|
+
f"Hook '{hook.name}' {method_name} failed: {result.message}"
|
|
152
|
+
)
|
|
153
|
+
elif result.status == HookStatus.WARNING:
|
|
154
|
+
logger.warning(
|
|
155
|
+
f"Hook '{hook.name}' {method_name} warning: {result.message}"
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
logger.debug(
|
|
159
|
+
f"Hook '{hook.name}' {method_name} succeeded: {result.message}"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
except Exception as e:
|
|
163
|
+
logger.error(f"Hook '{hook.name}' {method_name} raised exception: {e}")
|
|
164
|
+
results.append(HookResult.failure(str(e)))
|
|
165
|
+
|
|
166
|
+
return results
|
|
167
|
+
|
|
168
|
+
def load_from_config(self, config: Dict[str, Any], project_root: Path) -> None:
|
|
169
|
+
"""
|
|
170
|
+
Load and register hooks from configuration.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
config: The hooks configuration dictionary
|
|
174
|
+
project_root: The project root path
|
|
175
|
+
"""
|
|
176
|
+
if not config:
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
# Import built-in hooks
|
|
180
|
+
from .builtin.git_cleanup import GitCleanupHook
|
|
181
|
+
from .builtin.logging_hook import LoggingHook
|
|
182
|
+
|
|
183
|
+
# Map of hook names to classes
|
|
184
|
+
builtin_hooks = {
|
|
185
|
+
"git_cleanup": GitCleanupHook,
|
|
186
|
+
"logging": LoggingHook,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for hook_name, hook_config in config.items():
|
|
190
|
+
if isinstance(hook_config, bool):
|
|
191
|
+
# Simple enable/disable: "git_cleanup: true"
|
|
192
|
+
if hook_config and hook_name in builtin_hooks:
|
|
193
|
+
self.register_class(builtin_hooks[hook_name], name=hook_name)
|
|
194
|
+
elif isinstance(hook_config, dict):
|
|
195
|
+
# Full configuration: "git_cleanup: { enabled: true, ... }"
|
|
196
|
+
enabled = hook_config.get("enabled", True)
|
|
197
|
+
if enabled and hook_name in builtin_hooks:
|
|
198
|
+
self.register_class(
|
|
199
|
+
builtin_hooks[hook_name],
|
|
200
|
+
name=hook_name,
|
|
201
|
+
config=hook_config
|
|
202
|
+
)
|
|
203
|
+
else:
|
|
204
|
+
logger.warning(f"Unknown hook config format for '{hook_name}'")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# Global registry instance
|
|
208
|
+
_global_registry: Optional[HookRegistry] = None
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def get_registry() -> HookRegistry:
|
|
212
|
+
"""Get the global hook registry."""
|
|
213
|
+
global _global_registry
|
|
214
|
+
if _global_registry is None:
|
|
215
|
+
_global_registry = HookRegistry()
|
|
216
|
+
return _global_registry
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def reset_registry() -> None:
|
|
220
|
+
"""Reset the global registry (mainly for testing)."""
|
|
221
|
+
global _global_registry
|
|
222
|
+
_global_registry = HookRegistry()
|
monoco/core/injection.py
CHANGED
|
@@ -10,6 +10,8 @@ class PromptInjector:
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
MANAGED_HEADER = "## Monoco Toolkit"
|
|
13
|
+
MANAGED_START = "<!-- MONOCO_GENERATED_START -->"
|
|
14
|
+
MANAGED_END = "<!-- MONOCO_GENERATED_END -->"
|
|
13
15
|
|
|
14
16
|
def __init__(self, target_file: Path):
|
|
15
17
|
self.target_file = target_file
|
|
@@ -52,19 +54,40 @@ class PromptInjector:
|
|
|
52
54
|
# Sanitize content: remove leading header if it matches the title
|
|
53
55
|
clean_content = content.strip()
|
|
54
56
|
# Regex to match optional leading hash header matching the title (case insensitive)
|
|
55
|
-
# e.g. "### Issue Management" or "# Issue Management"
|
|
56
57
|
pattern = r"^(#+\s*)" + re.escape(title) + r"\s*\n"
|
|
57
58
|
match = re.match(pattern, clean_content, re.IGNORECASE)
|
|
58
59
|
|
|
59
60
|
if match:
|
|
60
61
|
clean_content = clean_content[match.end() :].strip()
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
|
|
63
|
+
# Demote headers in content to be below ### (so start at ####)
|
|
64
|
+
# We assume the content headers start at # or ##.
|
|
65
|
+
# We map # -> ####, ## -> #####, etc. (+3 offset)
|
|
66
|
+
demoted_content = []
|
|
67
|
+
for line in clean_content.splitlines():
|
|
68
|
+
if line.lstrip().startswith("#"):
|
|
69
|
+
demoted_content.append("###" + line)
|
|
70
|
+
else:
|
|
71
|
+
demoted_content.append(line)
|
|
72
|
+
|
|
73
|
+
managed_block.append("\n".join(demoted_content))
|
|
63
74
|
managed_block.append("") # Blank line after section
|
|
64
75
|
|
|
65
76
|
managed_block_str = "\n".join(managed_block).strip() + "\n"
|
|
77
|
+
managed_block_str = f"{self.MANAGED_START}\n{managed_block_str}\n{self.MANAGED_END}\n"
|
|
66
78
|
|
|
67
79
|
# 2. Find and replace/append in the original content
|
|
80
|
+
# Check for delimiters first
|
|
81
|
+
if self.MANAGED_START in original and self.MANAGED_END in original:
|
|
82
|
+
try:
|
|
83
|
+
pre = original.split(self.MANAGED_START)[0]
|
|
84
|
+
post = original.split(self.MANAGED_END)[1]
|
|
85
|
+
# Reconstruct
|
|
86
|
+
return pre + managed_block_str.strip() + post
|
|
87
|
+
except IndexError:
|
|
88
|
+
# Fallback to header detection if delimiters malformed
|
|
89
|
+
pass
|
|
90
|
+
|
|
68
91
|
lines = original.splitlines()
|
|
69
92
|
start_idx = -1
|
|
70
93
|
end_idx = -1
|
|
@@ -74,31 +97,29 @@ class PromptInjector:
|
|
|
74
97
|
if line.strip() == self.MANAGED_HEADER:
|
|
75
98
|
start_idx = i
|
|
76
99
|
break
|
|
100
|
+
|
|
101
|
+
if start_idx == -1:
|
|
102
|
+
# Check if we have delimiters even if header is missing/changed?
|
|
103
|
+
# Handled above.
|
|
104
|
+
pass
|
|
77
105
|
|
|
78
106
|
if start_idx == -1:
|
|
79
107
|
# Block not found, append to end
|
|
80
108
|
if original and not original.endswith("\n"):
|
|
81
|
-
return original + "\n\n" + managed_block_str
|
|
109
|
+
return original + "\n\n" + managed_block_str.strip()
|
|
82
110
|
elif original:
|
|
83
|
-
return original + "\n" + managed_block_str
|
|
111
|
+
return original + "\n" + managed_block_str.strip()
|
|
84
112
|
else:
|
|
85
|
-
return managed_block_str
|
|
86
|
-
|
|
87
|
-
# Find end: Look for next header of level 1 (assuming Managed Header is H1)
|
|
88
|
-
# Or EOF
|
|
89
|
-
# Note: If MANAGED_HEADER is "# ...", we look for next "# ..."
|
|
90
|
-
# But allow "## ..." as children.
|
|
113
|
+
return managed_block_str.strip() + "\n"
|
|
91
114
|
|
|
115
|
+
# Find end: Look for next header of level 1 or 2 (siblings or parents)
|
|
92
116
|
header_level_match = re.match(r"^(#+)\s", self.MANAGED_HEADER)
|
|
93
|
-
header_level_prefix = header_level_match.group(1) if header_level_match else "
|
|
117
|
+
header_level_prefix = header_level_match.group(1) if header_level_match else "##"
|
|
94
118
|
|
|
95
119
|
for i in range(start_idx + 1, len(lines)):
|
|
96
120
|
line = lines[i]
|
|
97
121
|
# Check if this line is a header of the same level or higher (fewer #s)
|
|
98
|
-
# e.g. if Managed is "###", then "#" and "##" are higher/parents, "###" is sibling.
|
|
99
|
-
# We treat siblings as end of block too.
|
|
100
122
|
if line.startswith("#"):
|
|
101
|
-
# Match regex to get level
|
|
102
123
|
match = re.match(r"^(#+)\s", line)
|
|
103
124
|
if match:
|
|
104
125
|
level = match.group(1)
|
|
@@ -146,26 +167,39 @@ class PromptInjector:
|
|
|
146
167
|
|
|
147
168
|
# Find start
|
|
148
169
|
for i, line in enumerate(lines):
|
|
149
|
-
if
|
|
170
|
+
if self.MANAGED_START in line:
|
|
150
171
|
start_idx = i
|
|
172
|
+
# Look for end from here
|
|
173
|
+
for j in range(i, len(lines)):
|
|
174
|
+
if self.MANAGED_END in lines[j]:
|
|
175
|
+
end_idx = j + 1 # Include the end line
|
|
176
|
+
break
|
|
151
177
|
break
|
|
178
|
+
|
|
179
|
+
if start_idx == -1:
|
|
180
|
+
# Fallback to header logic
|
|
181
|
+
for i, line in enumerate(lines):
|
|
182
|
+
if line.strip() == self.MANAGED_HEADER:
|
|
183
|
+
start_idx = i
|
|
184
|
+
break
|
|
152
185
|
|
|
153
186
|
if start_idx == -1:
|
|
154
187
|
return False
|
|
155
188
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
189
|
+
if end_idx == -1:
|
|
190
|
+
# Find end: exact logic as in _merge_content
|
|
191
|
+
header_level_match = re.match(r"^(#+)\s", self.MANAGED_HEADER)
|
|
192
|
+
header_level_prefix = header_level_match.group(1) if header_level_match else "##"
|
|
193
|
+
|
|
194
|
+
for i in range(start_idx + 1, len(lines)):
|
|
195
|
+
line = lines[i]
|
|
196
|
+
if line.startswith("#"):
|
|
197
|
+
match = re.match(r"^(#+)\s", line)
|
|
198
|
+
if match:
|
|
199
|
+
level = match.group(1)
|
|
200
|
+
if len(level) <= len(header_level_prefix):
|
|
201
|
+
end_idx = i
|
|
202
|
+
break
|
|
169
203
|
|
|
170
204
|
if end_idx == -1:
|
|
171
205
|
end_idx = len(lines)
|
monoco/core/integrations.py
CHANGED
|
@@ -129,11 +129,17 @@ DEFAULT_INTEGRATIONS: Dict[str, AgentIntegration] = {
|
|
|
129
129
|
"kimi": AgentIntegration(
|
|
130
130
|
key="kimi",
|
|
131
131
|
name="Kimi CLI",
|
|
132
|
-
system_prompt_file="
|
|
133
|
-
skill_root_dir=".
|
|
132
|
+
system_prompt_file="AGENTS.md",
|
|
133
|
+
skill_root_dir=".agent/skills/",
|
|
134
134
|
bin_name="kimi",
|
|
135
135
|
version_cmd="--version",
|
|
136
136
|
),
|
|
137
|
+
"agent": AgentIntegration(
|
|
138
|
+
key="agent",
|
|
139
|
+
name="Generic Agent",
|
|
140
|
+
system_prompt_file="AGENTS.md",
|
|
141
|
+
skill_root_dir=".agent/skills/",
|
|
142
|
+
),
|
|
137
143
|
}
|
|
138
144
|
|
|
139
145
|
|
monoco/core/output.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import json
|
|
3
3
|
import typer
|
|
4
|
-
from typing import Any, List, Union, Annotated
|
|
4
|
+
from typing import Any, List, Union, Annotated, Optional
|
|
5
5
|
from pydantic import BaseModel
|
|
6
6
|
from rich.console import Console
|
|
7
7
|
from rich.table import Table
|
|
@@ -41,7 +41,7 @@ class OutputManager:
|
|
|
41
41
|
|
|
42
42
|
@staticmethod
|
|
43
43
|
def print(
|
|
44
|
-
data: Union[BaseModel, List[BaseModel], dict, list, str], title: str = ""
|
|
44
|
+
data: Union[BaseModel, List[BaseModel], dict, list, str], title: str = "", style: Optional[str] = None
|
|
45
45
|
):
|
|
46
46
|
"""
|
|
47
47
|
Dual frontend dispatcher.
|
|
@@ -49,7 +49,7 @@ class OutputManager:
|
|
|
49
49
|
if OutputManager.is_agent_mode():
|
|
50
50
|
OutputManager._render_agent(data)
|
|
51
51
|
else:
|
|
52
|
-
OutputManager._render_human(data, title)
|
|
52
|
+
OutputManager._render_human(data, title, style=style)
|
|
53
53
|
|
|
54
54
|
@staticmethod
|
|
55
55
|
def error(message: str):
|
|
@@ -94,7 +94,7 @@ class OutputManager:
|
|
|
94
94
|
print(str(data))
|
|
95
95
|
|
|
96
96
|
@staticmethod
|
|
97
|
-
def _render_human(data: Any, title: str):
|
|
97
|
+
def _render_human(data: Any, title: str, style: Optional[str] = None):
|
|
98
98
|
"""
|
|
99
99
|
Human channel: Visual priority.
|
|
100
100
|
"""
|
|
@@ -104,7 +104,7 @@ class OutputManager:
|
|
|
104
104
|
console.rule(f"[bold blue]{title}[/bold blue]")
|
|
105
105
|
|
|
106
106
|
if isinstance(data, str):
|
|
107
|
-
console.print(data)
|
|
107
|
+
console.print(data, style=style)
|
|
108
108
|
return
|
|
109
109
|
|
|
110
110
|
# Special handling for Lists of Pydantic Models -> Table
|
monoco/core/registry.py
CHANGED
|
@@ -30,8 +30,16 @@ class FeatureRegistry:
|
|
|
30
30
|
from monoco.features.issue.adapter import IssueFeature
|
|
31
31
|
from monoco.features.spike.adapter import SpikeFeature
|
|
32
32
|
from monoco.features.i18n.adapter import I18nFeature
|
|
33
|
+
from monoco.features.memo.adapter import MemoFeature
|
|
33
34
|
|
|
34
35
|
cls.register(IssueFeature())
|
|
35
36
|
cls.register(SpikeFeature())
|
|
36
37
|
cls.register(I18nFeature())
|
|
37
|
-
|
|
38
|
+
cls.register(MemoFeature())
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
from monoco.features.glossary.adapter import GlossaryFeature
|
|
42
|
+
cls.register(GlossaryFeature())
|
|
43
|
+
|
|
44
|
+
from monoco.features.agent.adapter import AgentFeature
|
|
45
|
+
cls.register(AgentFeature())
|
|
@@ -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
|