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.
Files changed (113) hide show
  1. monoco/cli/workspace.py +1 -1
  2. monoco/core/config.py +58 -0
  3. monoco/core/hooks/__init__.py +19 -0
  4. monoco/core/hooks/base.py +104 -0
  5. monoco/core/hooks/builtin/__init__.py +11 -0
  6. monoco/core/hooks/builtin/git_cleanup.py +266 -0
  7. monoco/core/hooks/builtin/logging_hook.py +78 -0
  8. monoco/core/hooks/context.py +131 -0
  9. monoco/core/hooks/registry.py +222 -0
  10. monoco/core/injection.py +63 -29
  11. monoco/core/integrations.py +8 -2
  12. monoco/core/output.py +5 -5
  13. monoco/core/registry.py +9 -1
  14. monoco/core/resource/__init__.py +5 -0
  15. monoco/core/resource/finder.py +98 -0
  16. monoco/core/resource/manager.py +91 -0
  17. monoco/core/resource/models.py +35 -0
  18. monoco/core/resources/en/{SKILL.md → skills/monoco_core/SKILL.md} +2 -0
  19. monoco/core/resources/zh/{SKILL.md → skills/monoco_core/SKILL.md} +2 -0
  20. monoco/core/setup.py +1 -1
  21. monoco/core/skill_framework.py +292 -0
  22. monoco/core/skills.py +538 -254
  23. monoco/core/sync.py +73 -1
  24. monoco/core/workflow_converter.py +420 -0
  25. monoco/features/{scheduler → agent}/__init__.py +5 -3
  26. monoco/features/agent/adapter.py +31 -0
  27. monoco/features/agent/apoptosis.py +44 -0
  28. monoco/features/agent/cli.py +296 -0
  29. monoco/features/agent/config.py +96 -0
  30. monoco/features/agent/defaults.py +12 -0
  31. monoco/features/{scheduler → agent}/engines.py +32 -6
  32. monoco/features/agent/flow_skills.py +281 -0
  33. monoco/features/agent/manager.py +91 -0
  34. monoco/features/{scheduler → agent}/models.py +6 -3
  35. monoco/features/agent/resources/atoms/atom-code-dev.yaml +61 -0
  36. monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +73 -0
  37. monoco/features/agent/resources/atoms/atom-knowledge.yaml +55 -0
  38. monoco/features/agent/resources/atoms/atom-review.yaml +60 -0
  39. monoco/features/agent/resources/en/skills/flow_engineer/SKILL.md +94 -0
  40. monoco/features/agent/resources/en/skills/flow_manager/SKILL.md +93 -0
  41. monoco/features/agent/resources/en/skills/flow_planner/SKILL.md +85 -0
  42. monoco/features/agent/resources/en/skills/flow_reviewer/SKILL.md +114 -0
  43. monoco/features/agent/resources/roles/role-engineer.yaml +49 -0
  44. monoco/features/agent/resources/roles/role-manager.yaml +46 -0
  45. monoco/features/agent/resources/roles/role-planner.yaml +46 -0
  46. monoco/features/agent/resources/roles/role-reviewer.yaml +47 -0
  47. monoco/features/agent/resources/workflows/workflow-dev.yaml +83 -0
  48. monoco/features/agent/resources/workflows/workflow-issue-create.yaml +72 -0
  49. monoco/features/agent/resources/workflows/workflow-review.yaml +94 -0
  50. monoco/features/agent/resources/zh/skills/flow_engineer/SKILL.md +94 -0
  51. monoco/features/agent/resources/zh/skills/flow_manager/SKILL.md +88 -0
  52. monoco/features/agent/resources/zh/skills/flow_planner/SKILL.md +259 -0
  53. monoco/features/agent/resources/zh/skills/flow_reviewer/SKILL.md +137 -0
  54. monoco/features/{scheduler → agent}/session.py +36 -1
  55. monoco/features/{scheduler → agent}/worker.py +40 -4
  56. monoco/features/glossary/adapter.py +31 -0
  57. monoco/features/glossary/config.py +5 -0
  58. monoco/features/glossary/resources/en/AGENTS.md +29 -0
  59. monoco/features/glossary/resources/en/skills/monoco_glossary/SKILL.md +35 -0
  60. monoco/features/glossary/resources/zh/AGENTS.md +29 -0
  61. monoco/features/glossary/resources/zh/skills/monoco_glossary/SKILL.md +35 -0
  62. monoco/features/i18n/resources/en/skills/i18n_scan_workflow/SKILL.md +105 -0
  63. monoco/features/i18n/resources/en/{SKILL.md → skills/monoco_i18n/SKILL.md} +2 -0
  64. monoco/features/i18n/resources/zh/skills/i18n_scan_workflow/SKILL.md +105 -0
  65. monoco/features/i18n/resources/zh/{SKILL.md → skills/monoco_i18n/SKILL.md} +2 -0
  66. monoco/features/issue/commands.py +427 -21
  67. monoco/features/issue/core.py +140 -1
  68. monoco/features/issue/criticality.py +553 -0
  69. monoco/features/issue/domain/models.py +28 -2
  70. monoco/features/issue/engine/machine.py +75 -15
  71. monoco/features/issue/git_service.py +185 -0
  72. monoco/features/issue/linter.py +291 -62
  73. monoco/features/issue/models.py +50 -2
  74. monoco/features/issue/resources/en/skills/issue_create_workflow/SKILL.md +167 -0
  75. monoco/features/issue/resources/en/skills/issue_develop_workflow/SKILL.md +224 -0
  76. monoco/features/issue/resources/en/skills/issue_lifecycle_workflow/SKILL.md +159 -0
  77. monoco/features/issue/resources/en/skills/issue_refine_workflow/SKILL.md +203 -0
  78. monoco/features/issue/resources/en/{SKILL.md → skills/monoco_issue/SKILL.md} +50 -0
  79. monoco/features/issue/resources/zh/skills/issue_create_workflow/SKILL.md +167 -0
  80. monoco/features/issue/resources/zh/skills/issue_develop_workflow/SKILL.md +224 -0
  81. monoco/features/issue/resources/zh/skills/issue_lifecycle_workflow/SKILL.md +159 -0
  82. monoco/features/issue/resources/zh/skills/issue_refine_workflow/SKILL.md +203 -0
  83. monoco/features/issue/resources/zh/{SKILL.md → skills/monoco_issue/SKILL.md} +52 -0
  84. monoco/features/issue/validator.py +185 -65
  85. monoco/features/memo/__init__.py +2 -1
  86. monoco/features/memo/adapter.py +32 -0
  87. monoco/features/memo/cli.py +36 -14
  88. monoco/features/memo/core.py +59 -0
  89. monoco/features/memo/resources/en/skills/monoco_memo/SKILL.md +77 -0
  90. monoco/features/memo/resources/en/skills/note_processing_workflow/SKILL.md +140 -0
  91. monoco/features/memo/resources/zh/AGENTS.md +8 -0
  92. monoco/features/memo/resources/zh/skills/monoco_memo/SKILL.md +77 -0
  93. monoco/features/memo/resources/zh/skills/note_processing_workflow/SKILL.md +140 -0
  94. monoco/features/spike/resources/en/{SKILL.md → skills/monoco_spike/SKILL.md} +2 -0
  95. monoco/features/spike/resources/en/skills/research_workflow/SKILL.md +121 -0
  96. monoco/features/spike/resources/zh/{SKILL.md → skills/monoco_spike/SKILL.md} +2 -0
  97. monoco/features/spike/resources/zh/skills/research_workflow/SKILL.md +121 -0
  98. monoco/main.py +2 -3
  99. monoco_toolkit-0.3.10.dist-info/METADATA +124 -0
  100. monoco_toolkit-0.3.10.dist-info/RECORD +156 -0
  101. monoco/features/scheduler/cli.py +0 -285
  102. monoco/features/scheduler/config.py +0 -68
  103. monoco/features/scheduler/defaults.py +0 -54
  104. monoco/features/scheduler/manager.py +0 -49
  105. monoco/features/scheduler/reliability.py +0 -106
  106. monoco/features/skills/core.py +0 -102
  107. monoco_toolkit-0.3.6.dist-info/METADATA +0 -127
  108. monoco_toolkit-0.3.6.dist-info/RECORD +0 -97
  109. /monoco/core/{hooks.py → githooks.py} +0 -0
  110. /monoco/features/{skills → glossary}/__init__.py +0 -0
  111. {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.10.dist-info}/WHEEL +0 -0
  112. {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.10.dist-info}/entry_points.txt +0 -0
  113. {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
- managed_block.append(clean_content)
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 line.strip() == self.MANAGED_HEADER:
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
- # Find end: exact logic as in _merge_content
157
- header_level_match = re.match(r"^(#+)\s", self.MANAGED_HEADER)
158
- header_level_prefix = header_level_match.group(1) if header_level_match else "#"
159
-
160
- for i in range(start_idx + 1, len(lines)):
161
- line = lines[i]
162
- if line.startswith("#"):
163
- match = re.match(r"^(#+)\s", line)
164
- if match:
165
- level = match.group(1)
166
- if len(level) <= len(header_level_prefix):
167
- end_idx = i
168
- break
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)
@@ -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="KIMI.md",
133
- skill_root_dir=".kimi/skills/",
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
- pass
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,5 @@
1
+ from .models import ResourceNode, ResourceType
2
+ from .finder import ResourceFinder
3
+ from .manager import ResourceManager
4
+
5
+ __all__ = ["ResourceNode", "ResourceType", "ResourceFinder", "ResourceManager"]
@@ -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