monoco-toolkit 0.3.11__py3-none-any.whl → 0.4.0__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 (132) hide show
  1. monoco/core/automation/__init__.py +40 -0
  2. monoco/core/automation/field_watcher.py +296 -0
  3. monoco/core/automation/handlers.py +805 -0
  4. monoco/core/config.py +29 -11
  5. monoco/core/daemon/__init__.py +5 -0
  6. monoco/core/daemon/pid.py +290 -0
  7. monoco/core/git.py +15 -0
  8. monoco/core/hooks/context.py +74 -13
  9. monoco/core/injection.py +86 -8
  10. monoco/core/integrations.py +0 -24
  11. monoco/core/router/__init__.py +17 -0
  12. monoco/core/router/action.py +202 -0
  13. monoco/core/scheduler/__init__.py +63 -0
  14. monoco/core/scheduler/base.py +152 -0
  15. monoco/core/scheduler/engines.py +175 -0
  16. monoco/core/scheduler/events.py +197 -0
  17. monoco/core/scheduler/local.py +377 -0
  18. monoco/core/setup.py +9 -0
  19. monoco/core/sync.py +199 -4
  20. monoco/core/watcher/__init__.py +63 -0
  21. monoco/core/watcher/base.py +382 -0
  22. monoco/core/watcher/dropzone.py +152 -0
  23. monoco/core/watcher/im.py +460 -0
  24. monoco/core/watcher/issue.py +303 -0
  25. monoco/core/watcher/memo.py +192 -0
  26. monoco/core/watcher/task.py +238 -0
  27. monoco/daemon/app.py +3 -60
  28. monoco/daemon/commands.py +459 -25
  29. monoco/daemon/events.py +34 -0
  30. monoco/daemon/scheduler.py +157 -201
  31. monoco/daemon/services.py +42 -243
  32. monoco/features/agent/__init__.py +25 -7
  33. monoco/features/agent/cli.py +91 -57
  34. monoco/features/agent/engines.py +31 -170
  35. monoco/features/agent/resources/en/AGENTS.md +14 -14
  36. monoco/features/agent/resources/en/skills/monoco_role_engineer/SKILL.md +101 -0
  37. monoco/features/agent/resources/en/skills/monoco_role_manager/SKILL.md +95 -0
  38. monoco/features/agent/resources/en/skills/monoco_role_planner/SKILL.md +177 -0
  39. monoco/features/agent/resources/en/skills/monoco_role_reviewer/SKILL.md +139 -0
  40. monoco/features/agent/resources/zh/skills/monoco_role_engineer/SKILL.md +101 -0
  41. monoco/features/agent/resources/zh/skills/monoco_role_manager/SKILL.md +95 -0
  42. monoco/features/agent/resources/zh/skills/monoco_role_planner/SKILL.md +177 -0
  43. monoco/features/agent/resources/zh/skills/monoco_role_reviewer/SKILL.md +139 -0
  44. monoco/features/agent/worker.py +1 -1
  45. monoco/features/hooks/__init__.py +61 -6
  46. monoco/features/hooks/commands.py +281 -271
  47. monoco/features/hooks/dispatchers/__init__.py +23 -0
  48. monoco/features/hooks/dispatchers/agent_dispatcher.py +486 -0
  49. monoco/features/hooks/dispatchers/git_dispatcher.py +478 -0
  50. monoco/features/hooks/manager.py +357 -0
  51. monoco/features/hooks/models.py +262 -0
  52. monoco/features/hooks/parser.py +322 -0
  53. monoco/features/hooks/universal_interceptor.py +503 -0
  54. monoco/features/im/__init__.py +67 -0
  55. monoco/features/im/core.py +782 -0
  56. monoco/features/im/models.py +311 -0
  57. monoco/features/issue/commands.py +133 -60
  58. monoco/features/issue/core.py +385 -40
  59. monoco/features/issue/domain_commands.py +0 -19
  60. monoco/features/issue/resources/en/AGENTS.md +17 -122
  61. monoco/features/issue/resources/hooks/agent/before-tool.sh +102 -0
  62. monoco/features/issue/resources/hooks/agent/session-start.sh +88 -0
  63. monoco/features/issue/resources/hooks/{post-checkout.sh → git/git-post-checkout.sh} +10 -9
  64. monoco/features/issue/resources/hooks/git/git-pre-commit.sh +31 -0
  65. monoco/features/issue/resources/hooks/{pre-push.sh → git/git-pre-push.sh} +7 -13
  66. monoco/features/issue/resources/zh/AGENTS.md +18 -123
  67. monoco/features/memo/cli.py +15 -64
  68. monoco/features/memo/core.py +6 -34
  69. monoco/features/memo/models.py +24 -15
  70. monoco/features/memo/resources/en/AGENTS.md +31 -0
  71. monoco/features/memo/resources/zh/AGENTS.md +28 -5
  72. monoco/features/spike/commands.py +5 -3
  73. monoco/main.py +5 -3
  74. {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/METADATA +1 -1
  75. monoco_toolkit-0.4.0.dist-info/RECORD +170 -0
  76. monoco/core/execution.py +0 -67
  77. monoco/features/agent/apoptosis.py +0 -44
  78. monoco/features/agent/manager.py +0 -127
  79. monoco/features/agent/resources/atoms/atom-code-dev.yaml +0 -61
  80. monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +0 -73
  81. monoco/features/agent/resources/atoms/atom-knowledge.yaml +0 -55
  82. monoco/features/agent/resources/atoms/atom-review.yaml +0 -60
  83. monoco/features/agent/resources/en/skills/monoco_atom_core/SKILL.md +0 -99
  84. monoco/features/agent/resources/en/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
  85. monoco/features/agent/resources/en/skills/monoco_workflow_agent_manager/SKILL.md +0 -93
  86. monoco/features/agent/resources/en/skills/monoco_workflow_agent_planner/SKILL.md +0 -85
  87. monoco/features/agent/resources/en/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -114
  88. monoco/features/agent/resources/workflows/workflow-dev.yaml +0 -83
  89. monoco/features/agent/resources/workflows/workflow-issue-create.yaml +0 -72
  90. monoco/features/agent/resources/workflows/workflow-review.yaml +0 -94
  91. monoco/features/agent/resources/zh/roles/monoco_role_engineer.yaml +0 -49
  92. monoco/features/agent/resources/zh/roles/monoco_role_manager.yaml +0 -46
  93. monoco/features/agent/resources/zh/roles/monoco_role_planner.yaml +0 -46
  94. monoco/features/agent/resources/zh/roles/monoco_role_reviewer.yaml +0 -47
  95. monoco/features/agent/resources/zh/skills/monoco_atom_core/SKILL.md +0 -99
  96. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
  97. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_manager/SKILL.md +0 -88
  98. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_planner/SKILL.md +0 -259
  99. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -137
  100. monoco/features/agent/session.py +0 -169
  101. monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +0 -278
  102. monoco/features/glossary/resources/en/skills/monoco_atom_glossary/SKILL.md +0 -35
  103. monoco/features/glossary/resources/zh/skills/monoco_atom_glossary/SKILL.md +0 -35
  104. monoco/features/hooks/adapter.py +0 -67
  105. monoco/features/hooks/core.py +0 -441
  106. monoco/features/i18n/resources/en/skills/monoco_atom_i18n/SKILL.md +0 -96
  107. monoco/features/i18n/resources/en/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
  108. monoco/features/i18n/resources/zh/skills/monoco_atom_i18n/SKILL.md +0 -96
  109. monoco/features/i18n/resources/zh/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
  110. monoco/features/issue/resources/en/skills/monoco_atom_issue/SKILL.md +0 -165
  111. monoco/features/issue/resources/en/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
  112. monoco/features/issue/resources/en/skills/monoco_workflow_issue_development/SKILL.md +0 -224
  113. monoco/features/issue/resources/en/skills/monoco_workflow_issue_management/SKILL.md +0 -159
  114. monoco/features/issue/resources/en/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
  115. monoco/features/issue/resources/hooks/pre-commit.sh +0 -41
  116. monoco/features/issue/resources/zh/skills/monoco_atom_issue_lifecycle/SKILL.md +0 -190
  117. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
  118. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_development/SKILL.md +0 -224
  119. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_management/SKILL.md +0 -159
  120. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
  121. monoco/features/memo/resources/en/skills/monoco_atom_memo/SKILL.md +0 -77
  122. monoco/features/memo/resources/en/skills/monoco_workflow_note_processing/SKILL.md +0 -140
  123. monoco/features/memo/resources/zh/skills/monoco_atom_memo/SKILL.md +0 -77
  124. monoco/features/memo/resources/zh/skills/monoco_workflow_note_processing/SKILL.md +0 -140
  125. monoco/features/spike/resources/en/skills/monoco_atom_spike/SKILL.md +0 -76
  126. monoco/features/spike/resources/en/skills/monoco_workflow_research/SKILL.md +0 -121
  127. monoco/features/spike/resources/zh/skills/monoco_atom_spike/SKILL.md +0 -76
  128. monoco/features/spike/resources/zh/skills/monoco_workflow_research/SKILL.md +0 -121
  129. monoco_toolkit-0.3.11.dist-info/RECORD +0 -181
  130. {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/WHEEL +0 -0
  131. {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/entry_points.txt +0 -0
  132. {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,197 @@
1
+ """
2
+ EventBus - Central event system for Agent scheduling (FEAT-0155).
3
+
4
+ Provides async event publishing/subscription mechanism for decoupled
5
+ Agent lifecycle management.
6
+ """
7
+
8
+ import asyncio
9
+ import inspect
10
+ import logging
11
+ from enum import Enum, auto
12
+ from typing import Dict, List, Callable, Any, Optional, Union
13
+ from dataclasses import dataclass, field
14
+ from datetime import datetime
15
+
16
+
17
+ def _is_async_handler(handler: Callable) -> bool:
18
+ """
19
+ Check if a handler is async (coroutine function or has async __call__).
20
+
21
+ Handles both:
22
+ - Regular async functions: async def func(): ...
23
+ - Callable objects with async __call__: class Handler: async def __call__(self, ...): ...
24
+ """
25
+ # Direct check for coroutine function
26
+ if inspect.iscoroutinefunction(handler):
27
+ return True
28
+ # Check for callable object with async __call__ method
29
+ if hasattr(handler, "__call__") and inspect.iscoroutinefunction(handler.__call__):
30
+ return True
31
+ return False
32
+
33
+ logger = logging.getLogger("monoco.core.scheduler.events")
34
+
35
+
36
+ class AgentEventType(Enum):
37
+ """Event types for Agent lifecycle and triggers."""
38
+ # Memo events
39
+ MEMO_CREATED = "memo.created"
40
+ MEMO_THRESHOLD = "memo.threshold"
41
+
42
+ # Issue events
43
+ ISSUE_CREATED = "issue.created"
44
+ ISSUE_UPDATED = "issue.updated"
45
+ ISSUE_STAGE_CHANGED = "issue.stage_changed"
46
+ ISSUE_STATUS_CHANGED = "issue.status_changed"
47
+
48
+ # Session events
49
+ SESSION_STARTED = "session.started"
50
+ SESSION_COMPLETED = "session.completed"
51
+ SESSION_FAILED = "session.failed"
52
+ SESSION_CRASHED = "session.crashed"
53
+ SESSION_TERMINATED = "session.terminated"
54
+
55
+ # PR events (for Reviewer trigger)
56
+ PR_CREATED = "pr.created"
57
+ PR_UPDATED = "pr.updated"
58
+
59
+ # IM events (FEAT-0167)
60
+ IM_MESSAGE_RECEIVED = "im.message.received"
61
+ IM_MESSAGE_REPLIED = "im.message.replied"
62
+ IM_AGENT_TRIGGER = "im.agent.trigger"
63
+ IM_SESSION_STARTED = "im.session.started"
64
+ IM_SESSION_CLOSED = "im.session.closed"
65
+ IM_CHANNEL_CREATED = "im.channel.created"
66
+ IM_CHANNEL_UPDATED = "im.channel.updated"
67
+
68
+
69
+ @dataclass
70
+ class AgentEvent:
71
+ """Event data structure."""
72
+ type: AgentEventType
73
+ payload: Dict[str, Any]
74
+ timestamp: datetime = None
75
+ source: str = None
76
+
77
+ def __post_init__(self):
78
+ if self.timestamp is None:
79
+ self.timestamp = datetime.now()
80
+
81
+
82
+ EventHandler = Callable[[AgentEvent], Any]
83
+
84
+
85
+ class EventBus:
86
+ """
87
+ Central async event bus for Agent scheduling.
88
+
89
+ Supports:
90
+ - Subscribe/unsubscribe handlers for specific event types
91
+ - Publish events to all subscribed handlers
92
+ - Async handler execution
93
+ """
94
+
95
+ def __init__(self):
96
+ self._handlers: Dict[AgentEventType, List[EventHandler]] = {
97
+ event_type: [] for event_type in AgentEventType
98
+ }
99
+ self._lock = asyncio.Lock()
100
+ self._event_queue: asyncio.Queue = asyncio.Queue()
101
+ self._dispatch_task: Optional[asyncio.Task] = None
102
+ self._running = False
103
+
104
+ async def start(self):
105
+ """Start the event dispatch loop."""
106
+ if self._running:
107
+ return
108
+ self._running = True
109
+ self._dispatch_task = asyncio.create_task(self._dispatch_loop())
110
+ logger.info("EventBus started")
111
+
112
+ async def stop(self):
113
+ """Stop the event dispatch loop."""
114
+ if not self._running:
115
+ return
116
+ self._running = False
117
+ if self._dispatch_task:
118
+ self._dispatch_task.cancel()
119
+ try:
120
+ await self._dispatch_task
121
+ except asyncio.CancelledError:
122
+ pass
123
+ logger.info("EventBus stopped")
124
+
125
+ async def _dispatch_loop(self):
126
+ """Background loop to dispatch events."""
127
+ while self._running:
128
+ try:
129
+ event = await asyncio.wait_for(self._event_queue.get(), timeout=1.0)
130
+ await self._dispatch_event(event)
131
+ except asyncio.TimeoutError:
132
+ continue
133
+ except asyncio.CancelledError:
134
+ break
135
+ except Exception as e:
136
+ logger.error(f"Error dispatching event: {e}")
137
+
138
+ async def _dispatch_event(self, event: AgentEvent):
139
+ """Dispatch event to all subscribed handlers."""
140
+ handlers = self._handlers.get(event.type, [])
141
+ if not handlers:
142
+ logger.debug(f"No handlers for event {event.type.value}")
143
+ return
144
+
145
+ logger.debug(f"Dispatching {event.type.value} to {len(handlers)} handlers")
146
+
147
+ # Execute handlers concurrently
148
+ tasks = []
149
+ for handler in handlers:
150
+ try:
151
+ if _is_async_handler(handler):
152
+ tasks.append(asyncio.create_task(handler(event)))
153
+ else:
154
+ handler(event)
155
+ except Exception as e:
156
+ logger.error(f"Handler error for {event.type.value}: {e}")
157
+
158
+ if tasks:
159
+ await asyncio.gather(*tasks, return_exceptions=True)
160
+
161
+ def subscribe(self, event_type: AgentEventType, handler: EventHandler):
162
+ """Subscribe a handler to an event type."""
163
+ if handler not in self._handlers[event_type]:
164
+ self._handlers[event_type].append(handler)
165
+ logger.debug(f"Handler subscribed to {event_type.value}")
166
+
167
+ def unsubscribe(self, event_type: AgentEventType, handler: EventHandler):
168
+ """Unsubscribe a handler from an event type."""
169
+ if handler in self._handlers[event_type]:
170
+ self._handlers[event_type].remove(handler)
171
+ logger.debug(f"Handler unsubscribed from {event_type.value}")
172
+
173
+ async def publish(self, event_type: AgentEventType, payload: Dict[str, Any], source: str = None):
174
+ """Publish an event to the bus."""
175
+ event = AgentEvent(type=event_type, payload=payload, source=source)
176
+ await self._event_queue.put(event)
177
+ logger.debug(f"Published event {event_type.value}")
178
+
179
+ def get_subscriber_count(self, event_type: AgentEventType) -> int:
180
+ """Get number of subscribers for an event type."""
181
+ return len(self._handlers.get(event_type, []))
182
+
183
+ def get_stats(self) -> Dict[str, Any]:
184
+ """Get event bus statistics."""
185
+ return {
186
+ "running": self._running,
187
+ "queue_size": self._event_queue.qsize(),
188
+ "subscribers": {
189
+ event_type.value: len(handlers)
190
+ for event_type, handlers in self._handlers.items()
191
+ if handlers
192
+ }
193
+ }
194
+
195
+
196
+ # Global event bus instance
197
+ event_bus = EventBus()
@@ -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/setup.py CHANGED
@@ -211,6 +211,8 @@ def init_cli(
211
211
  project_config_path = project_config_dir / "project.yaml"
212
212
 
213
213
  project_initialized = False
214
+ workspace_config = {}
215
+ project_key = "MON"
214
216
 
215
217
  # Check if we should init project
216
218
  if workspace_config_path.exists() or project_config_path.exists():
@@ -323,6 +325,13 @@ def init_cli(
323
325
  try:
324
326
  from monoco.core.githooks import install_hooks
325
327
 
328
+ # Check if git initialized, if not, do it
329
+ if not (cwd / ".git").exists():
330
+ console.print("[dim]Git repository not found. Initializing...[/dim]")
331
+ # Set global default branch to main
332
+ subprocess.run(["git", "config", "--global", "init.defaultBranch", "main"], check=False)
333
+ subprocess.run(["git", "init"], cwd=cwd, check=False)
334
+
326
335
  # Re-load config to get the just-written hooks (or default ones)
327
336
  # Actually we have the dict right here in workspace_config['hooks']
328
337
  hooks_config = workspace_config.get("hooks", {})