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.
- monoco/core/automation/__init__.py +40 -0
- monoco/core/automation/field_watcher.py +296 -0
- monoco/core/automation/handlers.py +805 -0
- monoco/core/config.py +29 -11
- monoco/core/daemon/__init__.py +5 -0
- monoco/core/daemon/pid.py +290 -0
- monoco/core/git.py +15 -0
- monoco/core/hooks/context.py +74 -13
- monoco/core/injection.py +86 -8
- monoco/core/integrations.py +0 -24
- monoco/core/router/__init__.py +17 -0
- monoco/core/router/action.py +202 -0
- monoco/core/scheduler/__init__.py +63 -0
- monoco/core/scheduler/base.py +152 -0
- monoco/core/scheduler/engines.py +175 -0
- monoco/core/scheduler/events.py +197 -0
- monoco/core/scheduler/local.py +377 -0
- monoco/core/setup.py +9 -0
- monoco/core/sync.py +199 -4
- monoco/core/watcher/__init__.py +63 -0
- monoco/core/watcher/base.py +382 -0
- monoco/core/watcher/dropzone.py +152 -0
- monoco/core/watcher/im.py +460 -0
- monoco/core/watcher/issue.py +303 -0
- monoco/core/watcher/memo.py +192 -0
- monoco/core/watcher/task.py +238 -0
- monoco/daemon/app.py +3 -60
- monoco/daemon/commands.py +459 -25
- monoco/daemon/events.py +34 -0
- monoco/daemon/scheduler.py +157 -201
- monoco/daemon/services.py +42 -243
- monoco/features/agent/__init__.py +25 -7
- monoco/features/agent/cli.py +91 -57
- monoco/features/agent/engines.py +31 -170
- monoco/features/agent/resources/en/AGENTS.md +14 -14
- monoco/features/agent/resources/en/skills/monoco_role_engineer/SKILL.md +101 -0
- monoco/features/agent/resources/en/skills/monoco_role_manager/SKILL.md +95 -0
- monoco/features/agent/resources/en/skills/monoco_role_planner/SKILL.md +177 -0
- monoco/features/agent/resources/en/skills/monoco_role_reviewer/SKILL.md +139 -0
- monoco/features/agent/resources/zh/skills/monoco_role_engineer/SKILL.md +101 -0
- monoco/features/agent/resources/zh/skills/monoco_role_manager/SKILL.md +95 -0
- monoco/features/agent/resources/zh/skills/monoco_role_planner/SKILL.md +177 -0
- monoco/features/agent/resources/zh/skills/monoco_role_reviewer/SKILL.md +139 -0
- monoco/features/agent/worker.py +1 -1
- monoco/features/hooks/__init__.py +61 -6
- monoco/features/hooks/commands.py +281 -271
- monoco/features/hooks/dispatchers/__init__.py +23 -0
- monoco/features/hooks/dispatchers/agent_dispatcher.py +486 -0
- monoco/features/hooks/dispatchers/git_dispatcher.py +478 -0
- monoco/features/hooks/manager.py +357 -0
- monoco/features/hooks/models.py +262 -0
- monoco/features/hooks/parser.py +322 -0
- monoco/features/hooks/universal_interceptor.py +503 -0
- monoco/features/im/__init__.py +67 -0
- monoco/features/im/core.py +782 -0
- monoco/features/im/models.py +311 -0
- monoco/features/issue/commands.py +133 -60
- monoco/features/issue/core.py +385 -40
- monoco/features/issue/domain_commands.py +0 -19
- monoco/features/issue/resources/en/AGENTS.md +17 -122
- monoco/features/issue/resources/hooks/agent/before-tool.sh +102 -0
- monoco/features/issue/resources/hooks/agent/session-start.sh +88 -0
- monoco/features/issue/resources/hooks/{post-checkout.sh → git/git-post-checkout.sh} +10 -9
- monoco/features/issue/resources/hooks/git/git-pre-commit.sh +31 -0
- monoco/features/issue/resources/hooks/{pre-push.sh → git/git-pre-push.sh} +7 -13
- monoco/features/issue/resources/zh/AGENTS.md +18 -123
- monoco/features/memo/cli.py +15 -64
- monoco/features/memo/core.py +6 -34
- monoco/features/memo/models.py +24 -15
- monoco/features/memo/resources/en/AGENTS.md +31 -0
- monoco/features/memo/resources/zh/AGENTS.md +28 -5
- monoco/features/spike/commands.py +5 -3
- monoco/main.py +5 -3
- {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/METADATA +1 -1
- monoco_toolkit-0.4.0.dist-info/RECORD +170 -0
- monoco/core/execution.py +0 -67
- monoco/features/agent/apoptosis.py +0 -44
- monoco/features/agent/manager.py +0 -127
- monoco/features/agent/resources/atoms/atom-code-dev.yaml +0 -61
- monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +0 -73
- monoco/features/agent/resources/atoms/atom-knowledge.yaml +0 -55
- monoco/features/agent/resources/atoms/atom-review.yaml +0 -60
- monoco/features/agent/resources/en/skills/monoco_atom_core/SKILL.md +0 -99
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_manager/SKILL.md +0 -93
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_planner/SKILL.md +0 -85
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -114
- monoco/features/agent/resources/workflows/workflow-dev.yaml +0 -83
- monoco/features/agent/resources/workflows/workflow-issue-create.yaml +0 -72
- monoco/features/agent/resources/workflows/workflow-review.yaml +0 -94
- monoco/features/agent/resources/zh/roles/monoco_role_engineer.yaml +0 -49
- monoco/features/agent/resources/zh/roles/monoco_role_manager.yaml +0 -46
- monoco/features/agent/resources/zh/roles/monoco_role_planner.yaml +0 -46
- monoco/features/agent/resources/zh/roles/monoco_role_reviewer.yaml +0 -47
- monoco/features/agent/resources/zh/skills/monoco_atom_core/SKILL.md +0 -99
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_manager/SKILL.md +0 -88
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_planner/SKILL.md +0 -259
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -137
- monoco/features/agent/session.py +0 -169
- monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +0 -278
- monoco/features/glossary/resources/en/skills/monoco_atom_glossary/SKILL.md +0 -35
- monoco/features/glossary/resources/zh/skills/monoco_atom_glossary/SKILL.md +0 -35
- monoco/features/hooks/adapter.py +0 -67
- monoco/features/hooks/core.py +0 -441
- monoco/features/i18n/resources/en/skills/monoco_atom_i18n/SKILL.md +0 -96
- monoco/features/i18n/resources/en/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
- monoco/features/i18n/resources/zh/skills/monoco_atom_i18n/SKILL.md +0 -96
- monoco/features/i18n/resources/zh/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
- monoco/features/issue/resources/en/skills/monoco_atom_issue/SKILL.md +0 -165
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_development/SKILL.md +0 -224
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_management/SKILL.md +0 -159
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
- monoco/features/issue/resources/hooks/pre-commit.sh +0 -41
- monoco/features/issue/resources/zh/skills/monoco_atom_issue_lifecycle/SKILL.md +0 -190
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_development/SKILL.md +0 -224
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_management/SKILL.md +0 -159
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
- monoco/features/memo/resources/en/skills/monoco_atom_memo/SKILL.md +0 -77
- monoco/features/memo/resources/en/skills/monoco_workflow_note_processing/SKILL.md +0 -140
- monoco/features/memo/resources/zh/skills/monoco_atom_memo/SKILL.md +0 -77
- monoco/features/memo/resources/zh/skills/monoco_workflow_note_processing/SKILL.md +0 -140
- monoco/features/spike/resources/en/skills/monoco_atom_spike/SKILL.md +0 -76
- monoco/features/spike/resources/en/skills/monoco_workflow_research/SKILL.md +0 -121
- monoco/features/spike/resources/zh/skills/monoco_atom_spike/SKILL.md +0 -76
- monoco/features/spike/resources/zh/skills/monoco_workflow_research/SKILL.md +0 -121
- monoco_toolkit-0.3.11.dist-info/RECORD +0 -181
- {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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"
|
|
456
|
+
f"Starting Monoco Daemon on http://{host}:{port}", title="Monoco Serve"
|
|
31
457
|
)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
480
|
+
# Keep 'serve' as alias for 'serve start' for backward compatibility
|
|
481
|
+
serve = serve_legacy
|
monoco/daemon/events.py
ADDED
|
@@ -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
|
+
]
|