monoco-toolkit 0.3.12__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 (120) hide show
  1. monoco/core/automation/__init__.py +0 -11
  2. monoco/core/automation/handlers.py +108 -26
  3. monoco/core/config.py +28 -10
  4. monoco/core/daemon/__init__.py +5 -0
  5. monoco/core/daemon/pid.py +290 -0
  6. monoco/core/injection.py +86 -8
  7. monoco/core/integrations.py +0 -24
  8. monoco/core/router/__init__.py +1 -39
  9. monoco/core/router/action.py +3 -142
  10. monoco/core/scheduler/events.py +28 -2
  11. monoco/core/setup.py +9 -0
  12. monoco/core/sync.py +199 -4
  13. monoco/core/watcher/__init__.py +6 -0
  14. monoco/core/watcher/base.py +18 -1
  15. monoco/core/watcher/im.py +460 -0
  16. monoco/core/watcher/memo.py +40 -48
  17. monoco/daemon/app.py +3 -60
  18. monoco/daemon/commands.py +459 -25
  19. monoco/daemon/scheduler.py +1 -16
  20. monoco/daemon/services.py +15 -0
  21. monoco/features/agent/resources/en/AGENTS.md +14 -14
  22. monoco/features/agent/resources/en/skills/monoco_role_engineer/SKILL.md +101 -0
  23. monoco/features/agent/resources/en/skills/monoco_role_manager/SKILL.md +95 -0
  24. monoco/features/agent/resources/en/skills/monoco_role_planner/SKILL.md +177 -0
  25. monoco/features/agent/resources/en/skills/monoco_role_reviewer/SKILL.md +139 -0
  26. monoco/features/agent/resources/zh/skills/monoco_role_engineer/SKILL.md +101 -0
  27. monoco/features/agent/resources/zh/skills/monoco_role_manager/SKILL.md +95 -0
  28. monoco/features/agent/resources/zh/skills/monoco_role_planner/SKILL.md +177 -0
  29. monoco/features/agent/resources/zh/skills/monoco_role_reviewer/SKILL.md +139 -0
  30. monoco/features/hooks/__init__.py +61 -6
  31. monoco/features/hooks/commands.py +281 -271
  32. monoco/features/hooks/dispatchers/__init__.py +23 -0
  33. monoco/features/hooks/dispatchers/agent_dispatcher.py +486 -0
  34. monoco/features/hooks/dispatchers/git_dispatcher.py +478 -0
  35. monoco/features/hooks/manager.py +357 -0
  36. monoco/features/hooks/models.py +262 -0
  37. monoco/features/hooks/parser.py +322 -0
  38. monoco/features/hooks/universal_interceptor.py +503 -0
  39. monoco/features/im/__init__.py +67 -0
  40. monoco/features/im/core.py +782 -0
  41. monoco/features/im/models.py +311 -0
  42. monoco/features/issue/commands.py +65 -50
  43. monoco/features/issue/core.py +199 -99
  44. monoco/features/issue/domain_commands.py +0 -19
  45. monoco/features/issue/resources/en/AGENTS.md +17 -122
  46. monoco/features/issue/resources/hooks/agent/before-tool.sh +102 -0
  47. monoco/features/issue/resources/hooks/agent/session-start.sh +88 -0
  48. monoco/features/issue/resources/hooks/{post-checkout.sh → git/git-post-checkout.sh} +10 -9
  49. monoco/features/issue/resources/hooks/git/git-pre-commit.sh +31 -0
  50. monoco/features/issue/resources/hooks/{pre-push.sh → git/git-pre-push.sh} +7 -13
  51. monoco/features/issue/resources/zh/AGENTS.md +18 -123
  52. monoco/features/memo/cli.py +15 -64
  53. monoco/features/memo/core.py +6 -34
  54. monoco/features/memo/models.py +24 -15
  55. monoco/features/memo/resources/en/AGENTS.md +31 -0
  56. monoco/features/memo/resources/zh/AGENTS.md +28 -5
  57. monoco/main.py +5 -3
  58. {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/METADATA +1 -1
  59. monoco_toolkit-0.4.0.dist-info/RECORD +170 -0
  60. monoco/core/automation/config.py +0 -338
  61. monoco/core/execution.py +0 -67
  62. monoco/core/executor/__init__.py +0 -38
  63. monoco/core/executor/agent_action.py +0 -254
  64. monoco/core/executor/git_action.py +0 -303
  65. monoco/core/executor/im_action.py +0 -309
  66. monoco/core/executor/pytest_action.py +0 -218
  67. monoco/core/router/router.py +0 -392
  68. monoco/features/agent/resources/atoms/atom-code-dev.yaml +0 -61
  69. monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +0 -73
  70. monoco/features/agent/resources/atoms/atom-knowledge.yaml +0 -55
  71. monoco/features/agent/resources/atoms/atom-review.yaml +0 -60
  72. monoco/features/agent/resources/en/skills/monoco_atom_core/SKILL.md +0 -99
  73. monoco/features/agent/resources/en/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
  74. monoco/features/agent/resources/en/skills/monoco_workflow_agent_manager/SKILL.md +0 -93
  75. monoco/features/agent/resources/en/skills/monoco_workflow_agent_planner/SKILL.md +0 -85
  76. monoco/features/agent/resources/en/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -114
  77. monoco/features/agent/resources/workflows/workflow-dev.yaml +0 -83
  78. monoco/features/agent/resources/workflows/workflow-issue-create.yaml +0 -72
  79. monoco/features/agent/resources/workflows/workflow-review.yaml +0 -94
  80. monoco/features/agent/resources/zh/roles/monoco_role_engineer.yaml +0 -49
  81. monoco/features/agent/resources/zh/roles/monoco_role_manager.yaml +0 -46
  82. monoco/features/agent/resources/zh/roles/monoco_role_planner.yaml +0 -46
  83. monoco/features/agent/resources/zh/roles/monoco_role_reviewer.yaml +0 -47
  84. monoco/features/agent/resources/zh/skills/monoco_atom_core/SKILL.md +0 -99
  85. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
  86. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_manager/SKILL.md +0 -88
  87. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_planner/SKILL.md +0 -259
  88. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -137
  89. monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +0 -278
  90. monoco/features/glossary/resources/en/skills/monoco_atom_glossary/SKILL.md +0 -35
  91. monoco/features/glossary/resources/zh/skills/monoco_atom_glossary/SKILL.md +0 -35
  92. monoco/features/hooks/adapter.py +0 -67
  93. monoco/features/hooks/core.py +0 -441
  94. monoco/features/i18n/resources/en/skills/monoco_atom_i18n/SKILL.md +0 -96
  95. monoco/features/i18n/resources/en/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
  96. monoco/features/i18n/resources/zh/skills/monoco_atom_i18n/SKILL.md +0 -96
  97. monoco/features/i18n/resources/zh/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
  98. monoco/features/issue/resources/en/skills/monoco_atom_issue/SKILL.md +0 -165
  99. monoco/features/issue/resources/en/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
  100. monoco/features/issue/resources/en/skills/monoco_workflow_issue_development/SKILL.md +0 -224
  101. monoco/features/issue/resources/en/skills/monoco_workflow_issue_management/SKILL.md +0 -159
  102. monoco/features/issue/resources/en/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
  103. monoco/features/issue/resources/hooks/pre-commit.sh +0 -41
  104. monoco/features/issue/resources/zh/skills/monoco_atom_issue_lifecycle/SKILL.md +0 -190
  105. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
  106. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_development/SKILL.md +0 -224
  107. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_management/SKILL.md +0 -159
  108. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
  109. monoco/features/memo/resources/en/skills/monoco_atom_memo/SKILL.md +0 -77
  110. monoco/features/memo/resources/en/skills/monoco_workflow_note_processing/SKILL.md +0 -140
  111. monoco/features/memo/resources/zh/skills/monoco_atom_memo/SKILL.md +0 -77
  112. monoco/features/memo/resources/zh/skills/monoco_workflow_note_processing/SKILL.md +0 -140
  113. monoco/features/spike/resources/en/skills/monoco_atom_spike/SKILL.md +0 -76
  114. monoco/features/spike/resources/en/skills/monoco_workflow_research/SKILL.md +0 -121
  115. monoco/features/spike/resources/zh/skills/monoco_atom_spike/SKILL.md +0 -76
  116. monoco/features/spike/resources/zh/skills/monoco_workflow_research/SKILL.md +0 -121
  117. monoco_toolkit-0.3.12.dist-info/RECORD +0 -202
  118. {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/WHEEL +0 -0
  119. {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/entry_points.txt +0 -0
  120. {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/licenses/LICENSE +0 -0
monoco/daemon/app.py CHANGED
@@ -5,10 +5,10 @@ from sse_starlette.sse import EventSourceResponse
5
5
  import asyncio
6
6
  import logging
7
7
  import os
8
- from typing import Optional, List, Dict
8
+ from typing import Optional, Dict
9
9
  from monoco.daemon.services import Broadcaster, ProjectManager
10
10
  from monoco.core.git import GitMonitor
11
- from monoco.core.config import get_config, ConfigMonitor, ConfigScope, get_config_path
11
+ from monoco.core.config import get_config
12
12
  from monoco.daemon.scheduler import SchedulerService
13
13
  from monoco.daemon.mailroom_service import MailroomService
14
14
 
@@ -18,11 +18,6 @@ logger = logging.getLogger("monoco.daemon")
18
18
  from pathlib import Path
19
19
  from monoco.core.config import get_config
20
20
  from monoco.features.issue.core import list_issues
21
- from monoco.core.execution import (
22
- scan_execution_profiles,
23
- get_profile_detail,
24
- ExecutionProfile,
25
- )
26
21
 
27
22
  description = """
28
23
  Monoco Daemon Process
@@ -34,7 +29,6 @@ Monoco Daemon Process
34
29
  # Service Instances
35
30
  broadcaster = Broadcaster()
36
31
  git_monitor: GitMonitor | None = None
37
- config_monitors: List[ConfigMonitor] = []
38
32
  project_manager: ProjectManager | None = None
39
33
  scheduler_service: SchedulerService | None = None
40
34
  mailroom_service: MailroomService | None = None
@@ -45,7 +39,7 @@ async def lifespan(app: FastAPI):
45
39
  # Startup
46
40
  logger.info("Starting Monoco Daemon services...")
47
41
 
48
- global project_manager, git_monitor, config_monitors, scheduler_service, mailroom_service
42
+ global project_manager, git_monitor, scheduler_service, mailroom_service
49
43
  # Use MONOCO_SERVER_ROOT if set, otherwise CWD
50
44
  env_root = os.getenv("MONOCO_SERVER_ROOT")
51
45
  workspace_root = Path(env_root) if env_root else Path.cwd()
@@ -55,26 +49,8 @@ async def lifespan(app: FastAPI):
55
49
  async def on_git_change(new_hash: str):
56
50
  await broadcaster.broadcast("HEAD_UPDATED", {"ref": "HEAD", "hash": new_hash})
57
51
 
58
- async def on_config_change(path: str):
59
- logger.info(f"Config file changed: {path}, broadcasting update...")
60
- await broadcaster.broadcast(
61
- "CONFIG_UPDATED", {"scope": "workspace", "path": path}
62
- )
63
-
64
52
  git_monitor = GitMonitor(workspace_root, on_git_change)
65
53
 
66
- project_config_path = get_config_path(ConfigScope.PROJECT, str(workspace_root))
67
- workspace_config_path = get_config_path(ConfigScope.WORKSPACE, str(workspace_root))
68
-
69
- config_monitors = [
70
- ConfigMonitor(
71
- project_config_path, lambda: on_config_change(str(project_config_path))
72
- ),
73
- ConfigMonitor(
74
- workspace_config_path, lambda: on_config_change(str(workspace_config_path))
75
- ),
76
- ]
77
-
78
54
  await project_manager.start_all()
79
55
  # Start Scheduler
80
56
  scheduler_service = SchedulerService(project_manager)
@@ -88,15 +64,12 @@ async def lifespan(app: FastAPI):
88
64
  await mailroom_service.start()
89
65
 
90
66
  git_task = asyncio.create_task(git_monitor.start())
91
- config_tasks = [asyncio.create_task(m.start()) for m in config_monitors]
92
67
 
93
68
  yield
94
69
  # Shutdown
95
70
  logger.info("Shutting down Monoco Daemon services...")
96
71
  if git_monitor:
97
72
  git_monitor.stop()
98
- for m in config_monitors:
99
- m.stop()
100
73
  if project_manager:
101
74
  project_manager.stop_all()
102
75
  if scheduler_service:
@@ -105,7 +78,6 @@ async def lifespan(app: FastAPI):
105
78
  await mailroom_service.stop()
106
79
 
107
80
  await git_task
108
- await asyncio.gather(*config_tasks)
109
81
 
110
82
 
111
83
  app = FastAPI(
@@ -463,35 +435,6 @@ async def refresh_monitor():
463
435
  return {"status": "refreshed", "head": current_hash}
464
436
 
465
437
 
466
- # --- Execution Profiles ---
467
-
468
-
469
- @app.get("/api/v1/execution/profiles", response_model=List[ExecutionProfile])
470
- async def get_execution_profiles(project_id: Optional[str] = None):
471
- """
472
- List all execution profiles available for the project/workspace.
473
- """
474
- project = None
475
- if project_id:
476
- project = get_project_or_404(project_id)
477
- elif project_manager and project_manager.projects:
478
- # Fallback to first project if none specified
479
- project = list(project_manager.projects.values())[0]
480
-
481
- return scan_execution_profiles(project.path if project else None)
482
-
483
-
484
- @app.get("/api/v1/execution/profiles/detail", response_model=ExecutionProfile)
485
- async def get_execution_profile_detail(path: str):
486
- """
487
- Get full content of an execution profile.
488
- """
489
- profile = get_profile_detail(path)
490
- if not profile:
491
- raise HTTPException(status_code=404, detail="Profile not found")
492
- return profile
493
-
494
-
495
438
  # --- Workspace State Management ---
496
439
  from monoco.core.state import WorkspaceState
497
440
 
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