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
monoco/daemon/commands.py CHANGED
@@ -1,47 +1,481 @@
1
- import typer
2
- import uvicorn
1
+ """Monoco Daemon CLI commands for process management and service governance."""
2
+
3
3
  import os
4
+ import signal
5
+ import subprocess
6
+ import sys
7
+ import time
8
+ from datetime import datetime
9
+ from pathlib import Path
4
10
  from typing import Optional
5
- from monoco.core.output import print_output
11
+
12
+ import typer
13
+ import uvicorn
14
+ from rich.console import Console
15
+ from rich.table import Table
16
+
6
17
  from monoco.core.config import get_config
18
+ from monoco.core.daemon.pid import PIDManager, PortManager, PIDFileError
19
+ from monoco.core.output import print_output
7
20
 
8
- from pathlib import Path
21
+ console = Console()
22
+
23
+ # Create serve subcommand app
24
+ serve_app = typer.Typer(
25
+ name="serve",
26
+ help="Monoco Daemon server management",
27
+ no_args_is_help=True,
28
+ )
29
+
30
+
31
+ def _get_workspace_root(root: Optional[str] = None) -> Path:
32
+ """Get workspace root path."""
33
+ if root:
34
+ return Path(root).resolve()
35
+ env_root = os.getenv("MONOCO_SERVER_ROOT")
36
+ if env_root:
37
+ return Path(env_root)
38
+ return Path.cwd()
39
+
40
+
41
+ def _setup_signal_handlers(pid_manager: PIDManager):
42
+ """Setup signal handlers for graceful shutdown.
43
+
44
+ Note: We don't call sys.exit() here to allow uvicorn's graceful shutdown
45
+ to complete, which will execute the lifespan shutdown code and properly
46
+ stop all services (watchers, scheduler, etc.).
47
+ """
48
+
49
+ def signal_handler(signum, frame):
50
+ console.print(f"\n[yellow]Received signal {signum}, shutting down gracefully...[/yellow]")
51
+ # Only remove PID file here; let uvicorn handle the rest
52
+ # The lifespan shutdown in app.py will stop all services
53
+ pid_manager.remove_pid_file()
54
+ # Don't call sys.exit() - let uvicorn's signal handler continue
55
+ # to execute the shutdown sequence properly
56
+
57
+ signal.signal(signal.SIGTERM, signal_handler)
58
+ signal.signal(signal.SIGINT, signal_handler)
59
+
60
+
61
+ def _daemonize(
62
+ workspace_root: Path,
63
+ host: str,
64
+ port: int,
65
+ log_file: Optional[Path] = None,
66
+ ) -> int:
67
+ """Daemonize the current process using double-fork technique.
68
+
69
+ Args:
70
+ workspace_root: Workspace root path
71
+ host: Host to bind
72
+ port: Port to bind
73
+ log_file: Optional log file path (defaults to .monoco/log/daemon.log)
74
+
75
+ Returns:
76
+ Parent process returns child PID, child process returns 0
77
+ """
78
+ if log_file is None:
79
+ log_dir = workspace_root / ".monoco" / "log"
80
+ log_dir.mkdir(parents=True, exist_ok=True)
81
+ log_file = log_dir / "daemon.log"
82
+
83
+ # First fork
84
+ try:
85
+ pid = os.fork()
86
+ if pid > 0:
87
+ # Parent process: wait a moment to check if child started successfully
88
+ time.sleep(0.5)
89
+ return pid
90
+ except OSError as e:
91
+ console.print(f"[red]Fork #1 failed: {e}[/red]")
92
+ sys.exit(1)
93
+
94
+ # Child process continues
95
+ os.chdir(str(workspace_root))
96
+ os.setsid() # Create new session, detach from terminal
97
+
98
+ # Second fork
99
+ try:
100
+ pid = os.fork()
101
+ if pid > 0:
102
+ # First child exits
103
+ sys.exit(0)
104
+ except OSError as e:
105
+ console.print(f"[red]Fork #2 failed: {e}[/red]")
106
+ sys.exit(1)
107
+
108
+ # Grandchild process continues
109
+ # Redirect stdout/stderr to log file
110
+ sys.stdout.flush()
111
+ sys.stderr.flush()
112
+
113
+ with open(log_file, "a+") as log:
114
+ os.dup2(log.fileno(), sys.stdout.fileno())
115
+ os.dup2(log.fileno(), sys.stderr.fileno())
116
+
117
+ return 0
118
+
119
+
120
+ def serve_start(
121
+ host: str = typer.Option("127.0.0.1", "--host", "-h", help="Bind host"),
122
+ port: int = typer.Option(8642, "--port", "-p", help="Bind port"),
123
+ daemon: bool = typer.Option(
124
+ False, "--daemon", "-d", help="Run as background daemon"
125
+ ),
126
+ root: Optional[str] = typer.Option(
127
+ None, "--root", help="Workspace root directory"
128
+ ),
129
+ max_agents: Optional[int] = typer.Option(
130
+ None, "--max-agents", help="Override global maximum concurrent agents"
131
+ ),
132
+ auto_port: bool = typer.Option(
133
+ True, "--auto-port/--no-auto-port", help="Automatically find available port if default is in use"
134
+ ),
135
+ ):
136
+ """Start the Monoco Daemon server."""
137
+ workspace_root = _get_workspace_root(root)
138
+ pid_manager = PIDManager(workspace_root)
139
+
140
+ # Check if already running
141
+ existing = pid_manager.get_daemon_info()
142
+ if existing:
143
+ console.print(
144
+ f"[yellow]Daemon already running (PID: {existing['pid']}, "
145
+ f"http://{existing['host']}:{existing['port']})[/yellow]"
146
+ )
147
+ raise typer.Exit(code=0)
148
+
149
+ # Handle port selection
150
+ try:
151
+ if auto_port and PortManager.is_port_in_use(port, host):
152
+ new_port = PortManager.find_available_port(port + 1, host)
153
+ console.print(
154
+ f"[yellow]Port {port} is in use, using port {new_port} instead[/yellow]"
155
+ )
156
+ port = new_port
157
+ elif not auto_port and PortManager.is_port_in_use(port, host):
158
+ console.print(f"[red]Error: Port {port} is already in use[/red]")
159
+ raise typer.Exit(code=1)
160
+ except PIDFileError as e:
161
+ console.print(f"[red]Error: {e}[/red]")
162
+ raise typer.Exit(code=1)
163
+
164
+ # Set environment variables
165
+ os.environ["MONOCO_SERVER_ROOT"] = str(workspace_root)
166
+ if max_agents is not None:
167
+ os.environ["MONOCO_MAX_AGENTS"] = str(max_agents)
168
+
169
+ if daemon:
170
+ # Daemonize
171
+ log_dir = workspace_root / ".monoco" / "log"
172
+ log_dir.mkdir(parents=True, exist_ok=True)
173
+ log_file = log_dir / "daemon.log"
174
+
175
+ pid = _daemonize(workspace_root, host, port, log_file)
176
+ if pid > 0:
177
+ # Parent process: show success message and exit
178
+ console.print(f"[green]Daemon started (PID: {pid})[/green]")
179
+ console.print(f"[dim]Logs: {log_file}[/dim]")
180
+ console.print(f"[dim]URL: http://{host}:{port}[/dim]")
181
+ raise typer.Exit(code=0)
182
+
183
+ # Child process: continue to start server
184
+
185
+ # Create PID file before starting server
186
+ try:
187
+ pid_file = pid_manager.create_pid_file(host, port)
188
+ except PIDFileError as e:
189
+ console.print(f"[red]Error: {e}[/red]")
190
+ raise typer.Exit(code=1)
191
+
192
+ # Setup signal handlers for graceful shutdown in both modes
193
+ # - Foreground: Ctrl+C (SIGINT) or SIGTERM
194
+ # - Daemon: SIGTERM from `serve stop` command
195
+ _setup_signal_handlers(pid_manager)
196
+
197
+ try:
198
+ console.print(
199
+ f"[green]Starting Monoco Daemon on http://{host}:{port}[/green]"
200
+ )
201
+ if daemon:
202
+ print(
203
+ f"[{datetime.now().isoformat()}] Daemon started on {host}:{port}",
204
+ flush=True,
205
+ )
206
+
207
+ app_str = "monoco.daemon.app:app"
208
+ uvicorn.run(
209
+ app_str,
210
+ host=host,
211
+ port=port,
212
+ reload=False,
213
+ log_level="info",
214
+ )
215
+ finally:
216
+ pid_manager.remove_pid_file()
217
+
218
+
219
+ def serve_stop(
220
+ root: Optional[str] = typer.Option(None, "--root", help="Workspace root directory"),
221
+ force: bool = typer.Option(False, "--force", "-f", help="Force kill the daemon"),
222
+ ):
223
+ """Stop the running Monoco Daemon."""
224
+ workspace_root = _get_workspace_root(root)
225
+ pid_manager = PIDManager(workspace_root)
226
+
227
+ daemon_info = pid_manager.get_daemon_info()
228
+ if not daemon_info:
229
+ console.print("[yellow]Daemon is not running[/yellow]")
230
+ raise typer.Exit(code=0)
231
+
232
+ pid = daemon_info["pid"]
233
+ console.print(f"Stopping daemon (PID: {pid})...")
234
+
235
+ if force:
236
+ success = pid_manager.send_signal(signal.SIGKILL)
237
+ else:
238
+ success = pid_manager.terminate(timeout=5)
239
+
240
+ if success:
241
+ console.print("[green]Daemon stopped successfully[/green]")
242
+ else:
243
+ console.print("[red]Failed to stop daemon[/red]")
244
+ raise typer.Exit(code=1)
245
+
246
+
247
+ def serve_status(
248
+ root: Optional[str] = typer.Option(None, "--root", help="Workspace root directory"),
249
+ ):
250
+ """Show the status of the Monoco Daemon."""
251
+ workspace_root = _get_workspace_root(root)
252
+ pid_manager = PIDManager(workspace_root)
253
+
254
+ daemon_info = pid_manager.get_daemon_info()
255
+
256
+ if not daemon_info:
257
+ console.print("[yellow]Daemon is not running[/yellow]")
258
+ console.print(f"Workspace: {workspace_root}")
259
+ raise typer.Exit(code=0)
260
+
261
+ # Calculate uptime
262
+ started_at = datetime.fromisoformat(daemon_info["started_at"])
263
+ uptime = datetime.now() - started_at
264
+ uptime_str = str(uptime).split(".")[0] # Remove microseconds
265
+
266
+ table = Table(title="Monoco Daemon Status")
267
+ table.add_column("Property", style="cyan")
268
+ table.add_column("Value", style="green")
269
+
270
+ table.add_row("Status", "Running")
271
+ table.add_row("PID", str(daemon_info["pid"]))
272
+ table.add_row("Host", daemon_info["host"])
273
+ table.add_row("Port", str(daemon_info["port"]))
274
+ table.add_row("URL", f"http://{daemon_info['host']}:{daemon_info['port']}")
275
+ table.add_row("Version", daemon_info.get("version", "unknown"))
276
+ table.add_row("Started At", daemon_info["started_at"])
277
+ table.add_row("Uptime", uptime_str)
278
+ table.add_row("Workspace", str(workspace_root))
9
279
 
280
+ console.print(table)
10
281
 
11
- def serve(
282
+
283
+ def serve_restart(
284
+ host: str = typer.Option("127.0.0.1", "--host", "-h", help="Bind host"),
285
+ port: int = typer.Option(8642, "--port", "-p", help="Bind port"),
286
+ daemon: bool = typer.Option(
287
+ False, "--daemon", "-d", help="Run as background daemon"
288
+ ),
289
+ root: Optional[str] = typer.Option(
290
+ None, "--root", help="Workspace root directory"
291
+ ),
292
+ max_agents: Optional[int] = typer.Option(
293
+ None, "--max-agents", help="Override global maximum concurrent agents"
294
+ ),
295
+ auto_port: bool = typer.Option(
296
+ True, "--auto-port/--no-auto-port", help="Automatically find available port if default is in use"
297
+ ),
298
+ ):
299
+ """Restart the Monoco Daemon."""
300
+ workspace_root = _get_workspace_root(root)
301
+ pid_manager = PIDManager(workspace_root)
302
+
303
+ # Stop if running
304
+ if pid_manager.get_daemon_info():
305
+ console.print("Stopping existing daemon...")
306
+ serve_stop(root=root)
307
+ time.sleep(1) # Wait for process to fully terminate
308
+
309
+ # Start new daemon
310
+ serve_start(
311
+ host=host,
312
+ port=port,
313
+ daemon=daemon,
314
+ root=root,
315
+ max_agents=max_agents,
316
+ auto_port=auto_port,
317
+ )
318
+
319
+
320
+ def serve_cleanup(
321
+ root: Optional[str] = typer.Option(None, "--root", help="Workspace root directory"),
322
+ port: int = typer.Option(8642, "--port", "-p", help="Default port to check for orphans"),
323
+ dry_run: bool = typer.Option(
324
+ False, "--dry-run", help="Show what would be cleaned without actually doing it"
325
+ ),
326
+ ):
327
+ """Clean up orphaned daemon processes.
328
+
329
+ Scans for and terminates orphaned uvicorn processes that may have been
330
+ left behind when terminals were closed without proper shutdown.
331
+ """
332
+ workspace_root = _get_workspace_root(root)
333
+ pid_manager = PIDManager(workspace_root)
334
+
335
+ cleaned = []
336
+ errors = []
337
+
338
+ # Check PID file first
339
+ pid_data = pid_manager.read_pid_file()
340
+ if pid_data:
341
+ pid = pid_data["pid"]
342
+ if not PIDManager.is_process_alive(pid):
343
+ if not dry_run:
344
+ pid_manager.remove_pid_file()
345
+ cleaned.append(f"Stale PID file (PID: {pid})")
346
+
347
+ # Find uvicorn processes
348
+ try:
349
+ result = subprocess.run(
350
+ ["ps", "aux"],
351
+ capture_output=True,
352
+ text=True,
353
+ check=True,
354
+ )
355
+
356
+ for line in result.stdout.splitlines():
357
+ if "uvicorn" in line.lower() and "monoco.daemon.app" in line:
358
+ parts = line.split()
359
+ if len(parts) >= 2:
360
+ try:
361
+ uvicorn_pid = int(parts[1])
362
+ # Check if this is the current valid daemon
363
+ if pid_data and uvicorn_pid == pid_data["pid"]:
364
+ continue
365
+
366
+ proc_info = " ".join(parts[10:]) if len(parts) > 10 else line
367
+
368
+ if not dry_run:
369
+ try:
370
+ os.kill(uvicorn_pid, signal.SIGTERM)
371
+ # Wait briefly for graceful termination
372
+ time.sleep(0.5)
373
+ if PIDManager.is_process_alive(uvicorn_pid):
374
+ os.kill(uvicorn_pid, signal.SIGKILL)
375
+ cleaned.append(f"Orphan uvicorn (PID: {uvicorn_pid}): {proc_info[:50]}...")
376
+ except (OSError, ProcessLookupError) as e:
377
+ errors.append(f"Failed to kill PID {uvicorn_pid}: {e}")
378
+ else:
379
+ cleaned.append(f"[DRY RUN] Orphan uvicorn (PID: {uvicorn_pid}): {proc_info[:50]}...")
380
+ except (ValueError, IndexError):
381
+ continue
382
+
383
+ except subprocess.SubprocessError as e:
384
+ console.print(f"[red]Error scanning processes: {e}[/red]")
385
+ raise typer.Exit(code=1)
386
+
387
+ # Check for port conflicts
388
+ for check_port in range(port, port + 10):
389
+ if PortManager.is_port_in_use(check_port, "127.0.0.1"):
390
+ # Try to find which process is using this port
391
+ try:
392
+ result = subprocess.run(
393
+ ["lsof", "-ti", f":{check_port}"],
394
+ capture_output=True,
395
+ text=True,
396
+ )
397
+ if result.returncode == 0 and result.stdout.strip():
398
+ pids = result.stdout.strip().split("\n")
399
+ for pid_str in pids:
400
+ try:
401
+ pid = int(pid_str.strip())
402
+ # Skip if it's our known daemon
403
+ if pid_data and pid == pid_data["pid"]:
404
+ continue
405
+
406
+ if not dry_run:
407
+ try:
408
+ os.kill(pid, signal.SIGTERM)
409
+ cleaned.append(f"Process on port {check_port} (PID: {pid})")
410
+ except (OSError, ProcessLookupError):
411
+ pass
412
+ else:
413
+ cleaned.append(f"[DRY RUN] Process on port {check_port} (PID: {pid})")
414
+ except ValueError:
415
+ continue
416
+ except (subprocess.SubprocessError, FileNotFoundError):
417
+ # lsof might not be available
418
+ pass
419
+
420
+ # Report results
421
+ if cleaned:
422
+ console.print(f"[green]Cleaned {len(cleaned)} item(s):[/green]")
423
+ for item in cleaned:
424
+ console.print(f" - {item}")
425
+ else:
426
+ console.print("[green]No orphaned processes found[/green]")
427
+
428
+ if errors:
429
+ console.print(f"[yellow]Errors ({len(errors)}):[/yellow]")
430
+ for error in errors:
431
+ console.print(f" - {error}")
432
+
433
+
434
+ def serve_legacy(
12
435
  host: str = typer.Option("127.0.0.1", "--host", "-h", help="Bind host"),
13
436
  port: int = typer.Option(8642, "--port", "-p", help="Bind port"),
14
437
  reload: bool = typer.Option(
15
438
  False, "--reload", "-r", help="Enable auto-reload for dev"
16
439
  ),
17
- root: Optional[str] = typer.Option(None, "--root", help="Workspace root directory"),
440
+ root: Optional[str] = typer.Option(
441
+ None, "--root", help="Workspace root directory"
442
+ ),
18
443
  max_agents: Optional[int] = typer.Option(
19
444
  None, "--max-agents", help="Override global maximum concurrent agents (default: 3)"
20
445
  ),
21
446
  ):
22
- """
23
- Start the Monoco Daemon server.
24
- """
25
- settings = get_config()
447
+ """Start the Monoco Daemon server (legacy command, same as 'serve start')."""
448
+ # For backward compatibility, --reload implies foreground mode
449
+ if reload:
450
+ workspace_root = _get_workspace_root(root)
451
+ os.environ["MONOCO_SERVER_ROOT"] = str(workspace_root)
452
+ if max_agents is not None:
453
+ os.environ["MONOCO_MAX_AGENTS"] = str(max_agents)
26
454
 
27
- if root:
28
- os.environ["MONOCO_SERVER_ROOT"] = str(Path(root).resolve())
29
455
  print_output(
30
- f"Workspace Root: {os.environ['MONOCO_SERVER_ROOT']}", title="Monoco Serve"
456
+ f"Starting Monoco Daemon on http://{host}:{port}", title="Monoco Serve"
31
457
  )
32
-
33
- # Set max agents override if provided
34
- if max_agents is not None:
35
- os.environ["MONOCO_MAX_AGENTS"] = str(max_agents)
36
- print_output(
37
- f"Max Agents: {max_agents}", title="Monoco Serve"
458
+
459
+ app_str = "monoco.daemon.app:app"
460
+ uvicorn.run(app_str, host=host, port=port, reload=reload, log_level="info")
461
+ else:
462
+ # Without --reload, use the new start command
463
+ serve_start(
464
+ host=host,
465
+ port=port,
466
+ daemon=False,
467
+ root=root,
468
+ max_agents=max_agents,
469
+ auto_port=True,
38
470
  )
39
471
 
40
- print_output(
41
- f"Starting Monoco Daemon on http://{host}:{port}", title="Monoco Serve"
42
- )
43
472
 
44
- # We pass the import string to uvicorn to enable reload if needed
45
- app_str = "monoco.daemon.app:app"
473
+ # Register subcommands
474
+ serve_app.command(name="start")(serve_start)
475
+ serve_app.command(name="stop")(serve_stop)
476
+ serve_app.command(name="status")(serve_status)
477
+ serve_app.command(name="restart")(serve_restart)
478
+ serve_app.command(name="cleanup")(serve_cleanup)
46
479
 
47
- uvicorn.run(app_str, host=host, port=port, reload=reload, log_level="info")
480
+ # Keep 'serve' as alias for 'serve start' for backward compatibility
481
+ serve = serve_legacy
@@ -0,0 +1,34 @@
1
+ """
2
+ EventBus - Central event system for Agent scheduling.
3
+
4
+ DEPRECATED: This module has been moved to monoco.core.scheduler.
5
+ This file is kept for backward compatibility and re-exports from the new location.
6
+
7
+ Migration:
8
+ Old: from monoco.daemon.events import AgentEventType, event_bus
9
+ New: from monoco.core.scheduler import AgentEventType, event_bus
10
+ """
11
+
12
+ import warnings
13
+ from monoco.core.scheduler import (
14
+ AgentEventType,
15
+ AgentEvent,
16
+ EventBus,
17
+ event_bus,
18
+ EventHandler,
19
+ )
20
+
21
+ warnings.warn(
22
+ "monoco.daemon.events is deprecated. "
23
+ "Use monoco.core.scheduler instead.",
24
+ DeprecationWarning,
25
+ stacklevel=2
26
+ )
27
+
28
+ __all__ = [
29
+ "AgentEventType",
30
+ "AgentEvent",
31
+ "EventBus",
32
+ "event_bus",
33
+ "EventHandler",
34
+ ]