monoco-toolkit 0.3.10__py3-none-any.whl → 0.3.12__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 (130) hide show
  1. monoco/__main__.py +8 -0
  2. monoco/core/artifacts/__init__.py +16 -0
  3. monoco/core/artifacts/manager.py +575 -0
  4. monoco/core/artifacts/models.py +161 -0
  5. monoco/core/automation/__init__.py +51 -0
  6. monoco/core/automation/config.py +338 -0
  7. monoco/core/automation/field_watcher.py +296 -0
  8. monoco/core/automation/handlers.py +723 -0
  9. monoco/core/config.py +31 -4
  10. monoco/core/executor/__init__.py +38 -0
  11. monoco/core/executor/agent_action.py +254 -0
  12. monoco/core/executor/git_action.py +303 -0
  13. monoco/core/executor/im_action.py +309 -0
  14. monoco/core/executor/pytest_action.py +218 -0
  15. monoco/core/git.py +38 -0
  16. monoco/core/hooks/context.py +74 -13
  17. monoco/core/ingestion/__init__.py +20 -0
  18. monoco/core/ingestion/discovery.py +248 -0
  19. monoco/core/ingestion/watcher.py +343 -0
  20. monoco/core/ingestion/worker.py +436 -0
  21. monoco/core/loader.py +633 -0
  22. monoco/core/registry.py +34 -25
  23. monoco/core/router/__init__.py +55 -0
  24. monoco/core/router/action.py +341 -0
  25. monoco/core/router/router.py +392 -0
  26. monoco/core/scheduler/__init__.py +63 -0
  27. monoco/core/scheduler/base.py +152 -0
  28. monoco/core/scheduler/engines.py +175 -0
  29. monoco/core/scheduler/events.py +171 -0
  30. monoco/core/scheduler/local.py +377 -0
  31. monoco/core/skills.py +119 -80
  32. monoco/core/watcher/__init__.py +57 -0
  33. monoco/core/watcher/base.py +365 -0
  34. monoco/core/watcher/dropzone.py +152 -0
  35. monoco/core/watcher/issue.py +303 -0
  36. monoco/core/watcher/memo.py +200 -0
  37. monoco/core/watcher/task.py +238 -0
  38. monoco/daemon/app.py +77 -1
  39. monoco/daemon/commands.py +10 -0
  40. monoco/daemon/events.py +34 -0
  41. monoco/daemon/mailroom_service.py +196 -0
  42. monoco/daemon/models.py +1 -0
  43. monoco/daemon/scheduler.py +207 -0
  44. monoco/daemon/services.py +27 -58
  45. monoco/daemon/triggers.py +55 -0
  46. monoco/features/agent/__init__.py +25 -7
  47. monoco/features/agent/adapter.py +17 -7
  48. monoco/features/agent/cli.py +91 -57
  49. monoco/features/agent/engines.py +31 -170
  50. monoco/{core/resources/en/skills/monoco_core → features/agent/resources/en/skills/monoco_atom_core}/SKILL.md +2 -2
  51. monoco/features/agent/resources/en/skills/{flow_engineer → monoco_workflow_agent_engineer}/SKILL.md +2 -2
  52. monoco/features/agent/resources/en/skills/{flow_manager → monoco_workflow_agent_manager}/SKILL.md +2 -2
  53. monoco/features/agent/resources/en/skills/{flow_planner → monoco_workflow_agent_planner}/SKILL.md +2 -2
  54. monoco/features/agent/resources/en/skills/{flow_reviewer → monoco_workflow_agent_reviewer}/SKILL.md +2 -2
  55. monoco/features/agent/resources/{roles/role-engineer.yaml → zh/roles/monoco_role_engineer.yaml} +3 -3
  56. monoco/features/agent/resources/{roles/role-manager.yaml → zh/roles/monoco_role_manager.yaml} +8 -8
  57. monoco/features/agent/resources/{roles/role-planner.yaml → zh/roles/monoco_role_planner.yaml} +8 -8
  58. monoco/features/agent/resources/{roles/role-reviewer.yaml → zh/roles/monoco_role_reviewer.yaml} +8 -8
  59. monoco/{core/resources/zh/skills/monoco_core → features/agent/resources/zh/skills/monoco_atom_core}/SKILL.md +2 -2
  60. monoco/features/agent/resources/zh/skills/{flow_engineer → monoco_workflow_agent_engineer}/SKILL.md +2 -2
  61. monoco/features/agent/resources/zh/skills/{flow_manager → monoco_workflow_agent_manager}/SKILL.md +2 -2
  62. monoco/features/agent/resources/zh/skills/{flow_planner → monoco_workflow_agent_planner}/SKILL.md +2 -2
  63. monoco/features/agent/resources/zh/skills/{flow_reviewer → monoco_workflow_agent_reviewer}/SKILL.md +2 -2
  64. monoco/features/agent/worker.py +1 -1
  65. monoco/features/artifact/__init__.py +0 -0
  66. monoco/features/artifact/adapter.py +33 -0
  67. monoco/features/artifact/resources/zh/AGENTS.md +14 -0
  68. monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +278 -0
  69. monoco/features/glossary/adapter.py +18 -7
  70. monoco/features/glossary/resources/en/skills/{monoco_glossary → monoco_atom_glossary}/SKILL.md +2 -2
  71. monoco/features/glossary/resources/zh/skills/{monoco_glossary → monoco_atom_glossary}/SKILL.md +2 -2
  72. monoco/features/hooks/__init__.py +11 -0
  73. monoco/features/hooks/adapter.py +67 -0
  74. monoco/features/hooks/commands.py +309 -0
  75. monoco/features/hooks/core.py +441 -0
  76. monoco/features/hooks/resources/ADDING_HOOKS.md +234 -0
  77. monoco/features/i18n/adapter.py +18 -5
  78. monoco/features/i18n/core.py +482 -17
  79. monoco/features/i18n/resources/en/skills/{monoco_i18n → monoco_atom_i18n}/SKILL.md +2 -2
  80. monoco/features/i18n/resources/en/skills/{i18n_scan_workflow → monoco_workflow_i18n_scan}/SKILL.md +2 -2
  81. monoco/features/i18n/resources/zh/skills/{monoco_i18n → monoco_atom_i18n}/SKILL.md +2 -2
  82. monoco/features/i18n/resources/zh/skills/{i18n_scan_workflow → monoco_workflow_i18n_scan}/SKILL.md +2 -2
  83. monoco/features/issue/adapter.py +19 -6
  84. monoco/features/issue/commands.py +352 -20
  85. monoco/features/issue/core.py +475 -16
  86. monoco/features/issue/engine/machine.py +114 -4
  87. monoco/features/issue/linter.py +60 -5
  88. monoco/features/issue/models.py +2 -2
  89. monoco/features/issue/resources/en/AGENTS.md +109 -0
  90. monoco/features/issue/resources/en/skills/{monoco_issue → monoco_atom_issue}/SKILL.md +2 -2
  91. monoco/features/issue/resources/en/skills/{issue_create_workflow → monoco_workflow_issue_creation}/SKILL.md +2 -2
  92. monoco/features/issue/resources/en/skills/{issue_develop_workflow → monoco_workflow_issue_development}/SKILL.md +2 -2
  93. monoco/features/issue/resources/en/skills/{issue_lifecycle_workflow → monoco_workflow_issue_management}/SKILL.md +2 -2
  94. monoco/features/issue/resources/en/skills/{issue_refine_workflow → monoco_workflow_issue_refinement}/SKILL.md +2 -2
  95. monoco/features/issue/resources/hooks/post-checkout.sh +39 -0
  96. monoco/features/issue/resources/hooks/pre-commit.sh +41 -0
  97. monoco/features/issue/resources/hooks/pre-push.sh +35 -0
  98. monoco/features/issue/resources/zh/AGENTS.md +109 -0
  99. monoco/features/issue/resources/zh/skills/{monoco_issue → monoco_atom_issue_lifecycle}/SKILL.md +2 -2
  100. monoco/features/issue/resources/zh/skills/{issue_create_workflow → monoco_workflow_issue_creation}/SKILL.md +2 -2
  101. monoco/features/issue/resources/zh/skills/{issue_develop_workflow → monoco_workflow_issue_development}/SKILL.md +2 -2
  102. monoco/features/issue/resources/zh/skills/{issue_lifecycle_workflow → monoco_workflow_issue_management}/SKILL.md +2 -2
  103. monoco/features/issue/resources/zh/skills/{issue_refine_workflow → monoco_workflow_issue_refinement}/SKILL.md +2 -2
  104. monoco/features/issue/validator.py +101 -1
  105. monoco/features/memo/adapter.py +21 -8
  106. monoco/features/memo/cli.py +103 -10
  107. monoco/features/memo/core.py +178 -92
  108. monoco/features/memo/models.py +53 -0
  109. monoco/features/memo/resources/en/skills/{monoco_memo → monoco_atom_memo}/SKILL.md +2 -2
  110. monoco/features/memo/resources/en/skills/{note_processing_workflow → monoco_workflow_note_processing}/SKILL.md +2 -2
  111. monoco/features/memo/resources/zh/skills/{monoco_memo → monoco_atom_memo}/SKILL.md +2 -2
  112. monoco/features/memo/resources/zh/skills/{note_processing_workflow → monoco_workflow_note_processing}/SKILL.md +2 -2
  113. monoco/features/spike/adapter.py +18 -5
  114. monoco/features/spike/commands.py +5 -3
  115. monoco/features/spike/resources/en/skills/{monoco_spike → monoco_atom_spike}/SKILL.md +2 -2
  116. monoco/features/spike/resources/en/skills/{research_workflow → monoco_workflow_research}/SKILL.md +2 -2
  117. monoco/features/spike/resources/zh/skills/{monoco_spike → monoco_atom_spike}/SKILL.md +2 -2
  118. monoco/features/spike/resources/zh/skills/{research_workflow → monoco_workflow_research}/SKILL.md +2 -2
  119. monoco/main.py +38 -1
  120. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/METADATA +7 -1
  121. monoco_toolkit-0.3.12.dist-info/RECORD +202 -0
  122. monoco/features/agent/apoptosis.py +0 -44
  123. monoco/features/agent/manager.py +0 -91
  124. monoco/features/agent/session.py +0 -121
  125. monoco_toolkit-0.3.10.dist-info/RECORD +0 -156
  126. /monoco/{core → features/agent}/resources/en/AGENTS.md +0 -0
  127. /monoco/{core → features/agent}/resources/zh/AGENTS.md +0 -0
  128. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/WHEEL +0 -0
  129. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/entry_points.txt +0 -0
  130. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,377 @@
1
+ """
2
+ LocalProcessScheduler - Local process-based agent scheduler.
3
+
4
+ Implements the AgentScheduler ABC using local subprocess execution.
5
+ Integrates with SessionManager and Worker for process lifecycle management.
6
+ """
7
+
8
+ import asyncio
9
+ import logging
10
+ import subprocess
11
+ import sys
12
+ import time
13
+ import uuid
14
+ from pathlib import Path
15
+ from typing import Dict, Optional, Any
16
+
17
+ from .base import AgentScheduler, AgentTask, AgentStatus
18
+ from .engines import EngineFactory
19
+ from .events import AgentEventType, event_bus
20
+
21
+ logger = logging.getLogger("monoco.core.scheduler.local")
22
+
23
+
24
+ class LocalProcessScheduler(AgentScheduler):
25
+ """
26
+ Local process-based scheduler for agent execution.
27
+
28
+ This scheduler manages agent tasks as local subprocesses, providing:
29
+ - Process lifecycle management (spawn, monitor, terminate)
30
+ - Concurrency quota control via semaphore
31
+ - Timeout handling
32
+ - Session tracking and status reporting
33
+
34
+ Attributes:
35
+ max_concurrent: Maximum number of concurrent agent processes
36
+ project_root: Root path of the Monoco project
37
+
38
+ Example:
39
+ >>> scheduler = LocalProcessScheduler(max_concurrent=5)
40
+ >>> session_id = await scheduler.schedule(task)
41
+ >>> status = scheduler.get_status(session_id)
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ max_concurrent: int = 5,
47
+ project_root: Optional[Path] = None,
48
+ ):
49
+ self.max_concurrent = max_concurrent
50
+ self.project_root = project_root or Path.cwd()
51
+
52
+ # Session tracking: session_id -> process info
53
+ self._sessions: Dict[str, Dict[str, Any]] = {}
54
+
55
+ # Concurrency control
56
+ self._semaphore = asyncio.Semaphore(max_concurrent)
57
+
58
+ # Background monitoring task
59
+ self._monitor_task: Optional[asyncio.Task] = None
60
+ self._running = False
61
+
62
+ async def start(self):
63
+ """Start the scheduler and monitoring loop."""
64
+ if self._running:
65
+ return
66
+ self._running = True
67
+ self._monitor_task = asyncio.create_task(self._monitor_loop())
68
+ logger.info(f"LocalProcessScheduler started (max_concurrent={self.max_concurrent})")
69
+
70
+ async def stop(self):
71
+ """Stop the scheduler and terminate all sessions."""
72
+ if not self._running:
73
+ return
74
+ self._running = False
75
+
76
+ # Cancel monitor loop
77
+ if self._monitor_task:
78
+ self._monitor_task.cancel()
79
+ try:
80
+ await self._monitor_task
81
+ except asyncio.CancelledError:
82
+ pass
83
+
84
+ # Terminate all active sessions
85
+ for session_id in list(self._sessions.keys()):
86
+ await self.terminate(session_id)
87
+
88
+ logger.info("LocalProcessScheduler stopped")
89
+
90
+ async def schedule(self, task: AgentTask) -> str:
91
+ """
92
+ Schedule a task for execution as a local subprocess.
93
+
94
+ Args:
95
+ task: The task to schedule
96
+
97
+ Returns:
98
+ session_id: Unique identifier for the scheduled session
99
+
100
+ Raises:
101
+ RuntimeError: If scheduling fails or engine is not supported
102
+ """
103
+ session_id = str(uuid.uuid4())
104
+
105
+ # Acquire semaphore slot
106
+ acquired = await self._semaphore.acquire()
107
+ if not acquired:
108
+ # This shouldn't happen with asyncio.Semaphore, but just in case
109
+ raise RuntimeError("Failed to acquire concurrency slot")
110
+
111
+ try:
112
+ # Get engine adapter
113
+ adapter = EngineFactory.create(task.engine)
114
+ command = adapter.build_command(task.prompt)
115
+
116
+ logger.info(f"[{session_id}] Starting {task.role_name} with {task.engine} engine")
117
+
118
+ # Start subprocess
119
+ process = subprocess.Popen(
120
+ command,
121
+ stdout=sys.stdout,
122
+ stderr=sys.stderr,
123
+ text=True,
124
+ cwd=self.project_root,
125
+ )
126
+
127
+ # Track session
128
+ self._sessions[session_id] = {
129
+ "task": task,
130
+ "process": process,
131
+ "status": AgentStatus.RUNNING,
132
+ "started_at": time.time(),
133
+ "role_name": task.role_name,
134
+ "issue_id": task.issue_id,
135
+ }
136
+
137
+ # Publish session started event
138
+ await event_bus.publish(
139
+ AgentEventType.SESSION_STARTED,
140
+ {
141
+ "session_id": session_id,
142
+ "issue_id": task.issue_id,
143
+ "role_name": task.role_name,
144
+ "engine": task.engine,
145
+ },
146
+ source="LocalProcessScheduler"
147
+ )
148
+
149
+ return session_id
150
+
151
+ except Exception as e:
152
+ # Release semaphore on failure
153
+ self._semaphore.release()
154
+ logger.error(f"[{session_id}] Failed to start task: {e}")
155
+ raise RuntimeError(f"Failed to schedule task: {e}")
156
+
157
+ async def terminate(self, session_id: str) -> bool:
158
+ """
159
+ Terminate a running or pending session.
160
+
161
+ Args:
162
+ session_id: The session ID to terminate
163
+
164
+ Returns:
165
+ True if termination was successful, False otherwise
166
+ """
167
+ session = self._sessions.get(session_id)
168
+ if not session:
169
+ logger.warning(f"[{session_id}] Session not found for termination")
170
+ return False
171
+
172
+ process = session.get("process")
173
+ if not process:
174
+ return False
175
+
176
+ try:
177
+ # Try graceful termination
178
+ process.terminate()
179
+
180
+ # Wait a bit for graceful shutdown
181
+ try:
182
+ process.wait(timeout=2)
183
+ except subprocess.TimeoutExpired:
184
+ # Force kill if still running
185
+ process.kill()
186
+ process.wait()
187
+
188
+ session["status"] = AgentStatus.TERMINATED
189
+
190
+ # Publish session terminated event
191
+ await event_bus.publish(
192
+ AgentEventType.SESSION_TERMINATED,
193
+ {
194
+ "session_id": session_id,
195
+ "issue_id": session.get("issue_id"),
196
+ "role_name": session.get("role_name"),
197
+ },
198
+ source="LocalProcessScheduler"
199
+ )
200
+
201
+ # Release semaphore
202
+ self._semaphore.release()
203
+
204
+ logger.info(f"[{session_id}] Session terminated")
205
+ return True
206
+
207
+ except Exception as e:
208
+ logger.error(f"[{session_id}] Error terminating session: {e}")
209
+ return False
210
+
211
+ def get_status(self, session_id: str) -> Optional[AgentStatus]:
212
+ """
213
+ Get the current status of a session.
214
+
215
+ Args:
216
+ session_id: The session ID to query
217
+
218
+ Returns:
219
+ The current AgentStatus, or None if session not found
220
+ """
221
+ session = self._sessions.get(session_id)
222
+ if not session:
223
+ return None
224
+ return session.get("status")
225
+
226
+ def list_active(self) -> Dict[str, AgentStatus]:
227
+ """
228
+ List all active (pending or running) sessions.
229
+
230
+ Returns:
231
+ Dictionary mapping session_id to AgentStatus
232
+ """
233
+ return {
234
+ session_id: session["status"]
235
+ for session_id, session in self._sessions.items()
236
+ if session["status"] in (AgentStatus.PENDING, AgentStatus.RUNNING)
237
+ }
238
+
239
+ def get_stats(self) -> Dict[str, Any]:
240
+ """
241
+ Get scheduler statistics.
242
+
243
+ Returns:
244
+ Dictionary containing scheduler metrics
245
+ """
246
+ active_count = len(self.list_active())
247
+ total_count = len(self._sessions)
248
+
249
+ return {
250
+ "running": self._running,
251
+ "max_concurrent": self.max_concurrent,
252
+ "active_sessions": active_count,
253
+ "total_sessions": total_count,
254
+ "available_slots": self.max_concurrent - active_count,
255
+ }
256
+
257
+ async def _monitor_loop(self):
258
+ """Background loop to monitor session statuses."""
259
+ logger.info("Starting session monitor loop")
260
+
261
+ while self._running:
262
+ try:
263
+ await self._check_sessions()
264
+ await asyncio.sleep(2) # Check every 2 seconds
265
+ except asyncio.CancelledError:
266
+ break
267
+ except Exception as e:
268
+ logger.error(f"Error in monitor loop: {e}")
269
+ await asyncio.sleep(2)
270
+
271
+ async def _check_sessions(self):
272
+ """Check all sessions and update statuses."""
273
+ for session_id, session in list(self._sessions.items()):
274
+ process = session.get("process")
275
+ if not process:
276
+ continue
277
+
278
+ current_status = session["status"]
279
+
280
+ # Skip if already in terminal state
281
+ if current_status in (
282
+ AgentStatus.COMPLETED,
283
+ AgentStatus.FAILED,
284
+ AgentStatus.TERMINATED,
285
+ AgentStatus.TIMEOUT,
286
+ ):
287
+ continue
288
+
289
+ # Check timeout
290
+ task = session.get("task")
291
+ started_at = session.get("started_at", 0)
292
+ if task and task.timeout and (time.time() - started_at) > task.timeout:
293
+ logger.warning(f"[{session_id}] Task timeout exceeded ({task.timeout}s)")
294
+ await self._handle_timeout(session_id, session)
295
+ continue
296
+
297
+ # Check process status
298
+ returncode = process.poll()
299
+ if returncode is None:
300
+ # Still running
301
+ continue
302
+
303
+ # Process finished
304
+ if returncode == 0:
305
+ await self._handle_completion(session_id, session)
306
+ else:
307
+ await self._handle_failure(session_id, session, returncode)
308
+
309
+ async def _handle_completion(self, session_id: str, session: Dict[str, Any]):
310
+ """Handle successful session completion."""
311
+ session["status"] = AgentStatus.COMPLETED
312
+
313
+ # Publish completion event
314
+ await event_bus.publish(
315
+ AgentEventType.SESSION_COMPLETED,
316
+ {
317
+ "session_id": session_id,
318
+ "issue_id": session.get("issue_id"),
319
+ "role_name": session.get("role_name"),
320
+ },
321
+ source="LocalProcessScheduler"
322
+ )
323
+
324
+ # Release semaphore
325
+ self._semaphore.release()
326
+
327
+ logger.info(f"[{session_id}] Session completed successfully")
328
+
329
+ async def _handle_failure(self, session_id: str, session: Dict[str, Any], returncode: int):
330
+ """Handle session failure."""
331
+ session["status"] = AgentStatus.FAILED
332
+
333
+ # Publish failure event
334
+ await event_bus.publish(
335
+ AgentEventType.SESSION_FAILED,
336
+ {
337
+ "session_id": session_id,
338
+ "issue_id": session.get("issue_id"),
339
+ "role_name": session.get("role_name"),
340
+ "reason": f"Process exited with code {returncode}",
341
+ },
342
+ source="LocalProcessScheduler"
343
+ )
344
+
345
+ # Release semaphore
346
+ self._semaphore.release()
347
+
348
+ logger.error(f"[{session_id}] Session failed with exit code {returncode}")
349
+
350
+ async def _handle_timeout(self, session_id: str, session: Dict[str, Any]):
351
+ """Handle session timeout."""
352
+ process = session.get("process")
353
+
354
+ # Kill the process
355
+ if process:
356
+ try:
357
+ process.kill()
358
+ process.wait()
359
+ except Exception as e:
360
+ logger.error(f"[{session_id}] Error killing timed out process: {e}")
361
+
362
+ session["status"] = AgentStatus.TIMEOUT
363
+
364
+ # Publish failure event (timeout is a type of failure)
365
+ await event_bus.publish(
366
+ AgentEventType.SESSION_FAILED,
367
+ {
368
+ "session_id": session_id,
369
+ "issue_id": session.get("issue_id"),
370
+ "role_name": session.get("role_name"),
371
+ "reason": "Timeout exceeded",
372
+ },
373
+ source="LocalProcessScheduler"
374
+ )
375
+
376
+ # Release semaphore
377
+ self._semaphore.release()
monoco/core/skills.py CHANGED
@@ -2,17 +2,21 @@
2
2
  Skill Manager for Monoco Toolkit.
3
3
 
4
4
  This module provides centralized management and distribution of Agent Skills
5
- following the agentskills.io standard and the three-level architecture:
6
- - Atom Skills: Atomic capabilities
7
- - Workflow Skills: Orchestration of atoms
8
- - Role Skills: Configuration layer
5
+ following the standardized architecture:
6
+ - Atom Skills: monoco_atom_{name} - Atomic capabilities
7
+ - Workflow Skills: monoco_workflow_{name} - Orchestration of atoms
8
+ - Role Skills: monoco_role_{name} - Configuration layer
9
9
 
10
10
  Key Responsibilities:
11
- 1. Discover skills from resources/{lang}/skills/ directory (legacy)
12
- 2. Discover skills from resources/atoms/, workflows/, roles/ (new three-level)
13
- 3. Validate skill structure and metadata
14
- 4. Distribute skills to target agent framework directories
15
- 5. Support i18n for skill content
11
+ 1. Discover skills from features (monoco/features/{feature}/resources/)
12
+ 2. Validate skill structure and metadata
13
+ 3. Distribute skills to target agent framework directories
14
+ 4. Support i18n for skill content
15
+
16
+ Architecture Principle:
17
+ - Core is framework-only, no skills
18
+ - All skills are defined in Features (value delivery atoms)
19
+ - All skills follow naming convention: monoco_{type}_{name}
16
20
  """
17
21
 
18
22
  import shutil
@@ -165,22 +169,17 @@ class Skill:
165
169
 
166
170
  class SkillManager:
167
171
  """
168
- Central manager for Monoco skills supporting both legacy and three-level architecture.
172
+ Central manager for Monoco skills.
169
173
 
170
- Three-Level Architecture:
171
- - Atom Skills: resources/atoms/*.yaml
172
- - Workflow Skills: resources/workflows/*.yaml
173
- - Role Skills: resources/roles/*.yaml
174
+ Architecture:
175
+ - Atom Skills: resources/{lang}/skills/monoco_atom_*/SKILL.md or resources/atoms/*.yaml
176
+ - Workflow Skills: resources/{lang}/skills/monoco_workflow_*/SKILL.md or resources/workflows/*.yaml
177
+ - Role Skills: resources/{lang}/roles/monoco_role_*.yaml
174
178
 
175
- Legacy Architecture:
176
- - Standard Skills: resources/{lang}/skills/{name}/SKILL.md
177
- - Flow Skills: resources/{lang}/skills/flow_*/SKILL.md
179
+ All skills follow the naming convention: monoco_{type}_{name}
178
180
  """
179
181
 
180
- # Default prefix for flow skills
181
- FLOW_SKILL_PREFIX = "monoco_flow_"
182
-
183
- # Prefix for three-level architecture skills
182
+ # Prefix for standardized skill naming
184
183
  ATOM_PREFIX = "monoco_atom_"
185
184
  WORKFLOW_PREFIX = "monoco_workflow_"
186
185
  ROLE_PREFIX = "monoco_role_"
@@ -189,37 +188,24 @@ class SkillManager:
189
188
  self,
190
189
  root: Path,
191
190
  features: Optional[List] = None,
192
- flow_skill_prefix: str = FLOW_SKILL_PREFIX,
193
191
  ):
194
192
  self.root = root
195
193
  self.features = features or []
196
- self.flow_skill_prefix = flow_skill_prefix
197
194
 
198
- # Legacy skills
195
+ # Skills discovered from resources/{lang}/skills/monoco_*/SKILL.md
199
196
  self.skills: Dict[str, Skill] = {}
200
197
 
201
- # New three-level architecture skills
198
+ # Three-level architecture skills
202
199
  self._skill_loaders: Dict[str, SkillLoader] = {}
203
200
  self._atoms: Dict[str, AtomSkillMetadata] = {}
204
201
  self._workflows: Dict[str, WorkflowSkillMetadata] = {}
205
202
  self._roles: Dict[str, RoleSkillMetadata] = {}
206
203
 
207
- # Load legacy skills
204
+ # Discover skills from features only (core is framework-only, no skills)
208
205
  if self.features:
209
206
  self._discover_skills_from_features()
210
- self._discover_core_skills()
211
-
212
- # Load new three-level skills
213
- self._discover_three_level_skills()
214
-
215
- def _discover_core_skills(self) -> None:
216
- """Discover skills from monoco/core/resources/{lang}/skills/."""
217
- core_resources_dir = self.root / "monoco" / "core" / "resources"
218
-
219
- if not core_resources_dir.exists():
220
- return
207
+ self._discover_three_level_skills()
221
208
 
222
- self._discover_skills_in_resources(core_resources_dir, "monoco_core")
223
209
 
224
210
  def _discover_skills_from_features(self) -> None:
225
211
  """Discover skills from Feature resources."""
@@ -250,7 +236,7 @@ class SkillManager:
250
236
  if not resources_dir.exists():
251
237
  return
252
238
 
253
- skill_names: Set[str] = set()
239
+ skill_folders: Set[Path] = set()
254
240
 
255
241
  for lang_dir in resources_dir.iterdir():
256
242
  if not lang_dir.is_dir() or len(lang_dir.name) != 2:
@@ -262,9 +248,11 @@ class SkillManager:
262
248
 
263
249
  for skill_subdir in skills_dir.iterdir():
264
250
  if skill_subdir.is_dir() and (skill_subdir / "SKILL.md").exists():
265
- skill_names.add(skill_subdir.name)
251
+ # print(f"DEBUG: Found skill folder {skill_subdir.name} in feature {feature_name}")
252
+ skill_folders.add(skill_subdir)
266
253
 
267
- for skill_name in skill_names:
254
+ for skill_dir in skill_folders:
255
+ skill_name = skill_dir.name
268
256
  skill = Skill(
269
257
  root_dir=self.root,
270
258
  skill_name=skill_name,
@@ -277,40 +265,96 @@ class SkillManager:
277
265
  )
278
266
  continue
279
267
 
280
- skill_type = skill.get_type()
281
- if skill_type == "flow":
282
- name = skill_name
283
- if name.startswith("flow_"):
284
- name = name[5:]
285
- skill_key = f"{self.flow_skill_prefix}{name}"
268
+ # Naming Logic: All skills must follow monoco_{type}_{name} convention
269
+ # The skill_key is the folder name (which should match the metadata name)
270
+ if skill_name.startswith("monoco_"):
271
+ skill_key = skill_name
286
272
  else:
287
- skill_key = f"{feature_name}_{skill_name}"
273
+ # Non-compliant skills are skipped (should not happen after standardization)
274
+ console.print(
275
+ f"[yellow]Warning: Skill {skill_name} does not follow monoco_{{type}}_{{name}} naming, skipping[/yellow]"
276
+ )
277
+ continue
288
278
 
289
279
  skill.name = skill_key
290
280
  self.skills[skill_key] = skill
291
281
 
292
282
  def _discover_three_level_skills(self) -> None:
293
- """Discover skills from the new three-level architecture."""
294
- # Discover from agent feature resources
295
- agent_resources_dir = self.root / "monoco" / "features" / "agent" / "resources"
296
-
297
- if not agent_resources_dir.exists():
298
- return
299
-
300
- loader = SkillLoader(agent_resources_dir)
301
- loader.load_all()
302
-
303
- self._skill_loaders["agent"] = loader
283
+ """Discover skills from the new three-level architecture in resources/{atoms,workflows,roles}/."""
284
+ from monoco.core.feature import MonocoFeature
304
285
 
305
- # Merge loaded skills
306
- for name, atom in loader._atoms.items():
307
- self._atoms[f"{self.ATOM_PREFIX}{name}"] = atom
308
-
309
- for name, workflow in loader._workflows.items():
310
- self._workflows[f"{self.WORKFLOW_PREFIX}{name}"] = workflow
311
-
312
- for name, role in loader._roles.items():
313
- self._roles[f"{self.ROLE_PREFIX}{name}"] = role
286
+ for feature in self.features:
287
+ if not isinstance(feature, MonocoFeature):
288
+ continue
289
+
290
+ module_parts = feature.__class__.__module__.split(".")
291
+ if (
292
+ len(module_parts) >= 3
293
+ and module_parts[0] == "monoco"
294
+ and module_parts[1] == "features"
295
+ ):
296
+ feature_name = module_parts[2]
297
+ resources_dir = self.root / "monoco" / "features" / feature_name / "resources"
298
+
299
+ if not resources_dir.exists():
300
+ continue
301
+
302
+ # Discover atoms from resources/atoms/*.yaml
303
+ atoms_dir = resources_dir / "atoms"
304
+ if atoms_dir.exists():
305
+ for atom_file in atoms_dir.glob("*.yaml"):
306
+ try:
307
+ data = yaml.safe_load(atom_file.read_text())
308
+ atom = AtomSkillMetadata(**data)
309
+
310
+ # Ensure name follows monoco_atom_ prefix
311
+ atom_key = atom.name
312
+ if not atom_key.startswith(self.ATOM_PREFIX):
313
+ atom_key = f"{self.ATOM_PREFIX}{atom_key}"
314
+
315
+ self._atoms[atom_key] = atom
316
+ except Exception as e:
317
+ console.print(f"[red]Failed to load atom skill {atom_file}: {e}[/red]")
318
+
319
+ # Discover workflows from resources/workflows/*.yaml
320
+ workflows_dir = resources_dir / "workflows"
321
+ if workflows_dir.exists():
322
+ for workflow_file in workflows_dir.glob("*.yaml"):
323
+ try:
324
+ data = yaml.safe_load(workflow_file.read_text())
325
+ workflow = WorkflowSkillMetadata(**data)
326
+
327
+ # Ensure name follows monoco_workflow_ prefix
328
+ workflow_key = workflow.name
329
+ if not workflow_key.startswith(self.WORKFLOW_PREFIX):
330
+ workflow_key = f"{self.WORKFLOW_PREFIX}{workflow_key}"
331
+
332
+ self._workflows[workflow_key] = workflow
333
+ except Exception as e:
334
+ console.print(f"[red]Failed to load workflow skill {workflow_file}: {e}[/red]")
335
+
336
+ # Discover roles from resources/{lang}/roles/*.yaml
337
+ for lang_dir in resources_dir.iterdir():
338
+ if not lang_dir.is_dir() or len(lang_dir.name) != 2:
339
+ continue
340
+
341
+ roles_dir = lang_dir / "roles"
342
+ if not roles_dir.exists():
343
+ continue
344
+
345
+ for role_file in roles_dir.glob("*.yaml"):
346
+ try:
347
+ data = yaml.safe_load(role_file.read_text())
348
+ role = RoleSkillMetadata(**data)
349
+
350
+ # Ensure name follows monoco_role_ prefix
351
+ role_key = role.name
352
+ if not role_key.startswith(self.ROLE_PREFIX):
353
+ role_key = f"{self.ROLE_PREFIX}{role_key}"
354
+
355
+ self._roles[role_key] = role
356
+ except Exception as e:
357
+ console.print(f"[red]Failed to load role skill {role_file}: {e}[/red]")
314
358
 
315
359
  # ========================================================================
316
360
  # Legacy Skill API (backward compatible)
@@ -688,19 +732,14 @@ class SkillManager:
688
732
  """Get list of available flow skill commands."""
689
733
  commands = []
690
734
 
691
- # Legacy flow skills
692
- for skill in self.get_flow_skills():
693
- role = skill.get_role()
694
- if role:
695
- commands.append(f"/flow:{role}")
696
- else:
697
- name = skill.name
698
- if name.startswith(self.flow_skill_prefix):
699
- role = name[len(self.flow_skill_prefix):]
700
- if role:
701
- commands.append(f"/flow:{role}")
735
+ # Workflow/Flow skills with role attribute from legacy skills
736
+ for skill in self.skills.values():
737
+ if skill.get_type() in ["flow", "workflow"]:
738
+ role = skill.get_role()
739
+ if role:
740
+ commands.append(f"/flow:{role}")
702
741
 
703
- # New role skills
742
+ # Role skills from three-level architecture
704
743
  for role_name in self._roles.keys():
705
744
  short_name = role_name.replace(self.ROLE_PREFIX, "")
706
745
  commands.append(f"/flow:{short_name}")