monoco-toolkit 0.2.8__py3-none-any.whl → 0.3.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/cli/project.py +35 -31
- monoco/cli/workspace.py +26 -16
- monoco/core/agent/__init__.py +0 -2
- monoco/core/agent/action.py +44 -20
- monoco/core/agent/adapters.py +20 -16
- monoco/core/agent/protocol.py +5 -4
- monoco/core/agent/state.py +21 -21
- monoco/core/config.py +90 -33
- monoco/core/execution.py +21 -16
- monoco/core/feature.py +8 -5
- monoco/core/git.py +61 -30
- monoco/core/hooks.py +57 -0
- monoco/core/injection.py +47 -44
- monoco/core/integrations.py +50 -35
- monoco/core/lsp.py +12 -1
- monoco/core/output.py +35 -16
- monoco/core/registry.py +3 -2
- monoco/core/setup.py +190 -124
- monoco/core/skills.py +121 -107
- monoco/core/state.py +12 -10
- monoco/core/sync.py +85 -56
- monoco/core/telemetry.py +10 -6
- monoco/core/workspace.py +26 -19
- monoco/daemon/app.py +123 -79
- monoco/daemon/commands.py +14 -13
- monoco/daemon/models.py +11 -3
- monoco/daemon/reproduce_stats.py +8 -8
- monoco/daemon/services.py +32 -33
- monoco/daemon/stats.py +59 -40
- monoco/features/config/commands.py +38 -25
- monoco/features/i18n/adapter.py +4 -5
- monoco/features/i18n/commands.py +83 -49
- monoco/features/i18n/core.py +94 -54
- monoco/features/issue/adapter.py +6 -7
- monoco/features/issue/commands.py +468 -272
- monoco/features/issue/core.py +419 -312
- monoco/features/issue/domain/lifecycle.py +33 -23
- monoco/features/issue/domain/models.py +71 -38
- monoco/features/issue/domain/parser.py +92 -69
- monoco/features/issue/domain/workspace.py +19 -16
- monoco/features/issue/engine/__init__.py +3 -3
- monoco/features/issue/engine/config.py +18 -25
- monoco/features/issue/engine/machine.py +72 -39
- monoco/features/issue/engine/models.py +4 -2
- monoco/features/issue/linter.py +287 -157
- monoco/features/issue/lsp/definition.py +26 -19
- monoco/features/issue/migration.py +45 -34
- monoco/features/issue/models.py +29 -13
- monoco/features/issue/monitor.py +24 -8
- monoco/features/issue/resources/en/SKILL.md +6 -2
- monoco/features/issue/validator.py +383 -208
- monoco/features/skills/__init__.py +0 -1
- monoco/features/skills/core.py +24 -18
- monoco/features/spike/adapter.py +4 -5
- monoco/features/spike/commands.py +51 -38
- monoco/features/spike/core.py +24 -16
- monoco/main.py +34 -21
- {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.0.dist-info}/METADATA +1 -1
- monoco_toolkit-0.3.0.dist-info/RECORD +84 -0
- monoco_toolkit-0.2.8.dist-info/RECORD +0 -83
- {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.0.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.0.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.0.dist-info}/licenses/LICENSE +0 -0
monoco/daemon/app.py
CHANGED
|
@@ -16,7 +16,11 @@ logger = logging.getLogger("monoco.daemon")
|
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
from monoco.core.config import get_config
|
|
18
18
|
from monoco.features.issue.core import list_issues
|
|
19
|
-
from monoco.core.execution import
|
|
19
|
+
from monoco.core.execution import (
|
|
20
|
+
scan_execution_profiles,
|
|
21
|
+
get_profile_detail,
|
|
22
|
+
ExecutionProfile,
|
|
23
|
+
)
|
|
20
24
|
|
|
21
25
|
description = """
|
|
22
26
|
Monoco Daemon Process
|
|
@@ -31,45 +35,46 @@ git_monitor: GitMonitor | None = None
|
|
|
31
35
|
config_monitors: List[ConfigMonitor] = []
|
|
32
36
|
project_manager: ProjectManager | None = None
|
|
33
37
|
|
|
38
|
+
|
|
34
39
|
@asynccontextmanager
|
|
35
40
|
async def lifespan(app: FastAPI):
|
|
36
41
|
# Startup
|
|
37
42
|
logger.info("Starting Monoco Daemon services...")
|
|
38
|
-
|
|
43
|
+
|
|
39
44
|
global project_manager, git_monitor, config_monitors
|
|
40
45
|
# Use MONOCO_SERVER_ROOT if set, otherwise CWD
|
|
41
46
|
env_root = os.getenv("MONOCO_SERVER_ROOT")
|
|
42
47
|
workspace_root = Path(env_root) if env_root else Path.cwd()
|
|
43
48
|
logger.info(f"Workspace Root: {workspace_root}")
|
|
44
49
|
project_manager = ProjectManager(workspace_root, broadcaster)
|
|
45
|
-
|
|
50
|
+
|
|
46
51
|
async def on_git_change(new_hash: str):
|
|
47
|
-
await broadcaster.broadcast("HEAD_UPDATED", {
|
|
48
|
-
|
|
49
|
-
"hash": new_hash
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
+
await broadcaster.broadcast("HEAD_UPDATED", {"ref": "HEAD", "hash": new_hash})
|
|
53
|
+
|
|
52
54
|
async def on_config_change(path: str):
|
|
53
55
|
logger.info(f"Config file changed: {path}, broadcasting update...")
|
|
54
|
-
await broadcaster.broadcast(
|
|
55
|
-
"scope": "workspace",
|
|
56
|
-
|
|
57
|
-
})
|
|
56
|
+
await broadcaster.broadcast(
|
|
57
|
+
"CONFIG_UPDATED", {"scope": "workspace", "path": path}
|
|
58
|
+
)
|
|
58
59
|
|
|
59
60
|
git_monitor = GitMonitor(workspace_root, on_git_change)
|
|
60
|
-
|
|
61
|
+
|
|
61
62
|
project_config_path = get_config_path(ConfigScope.PROJECT, str(workspace_root))
|
|
62
63
|
workspace_config_path = get_config_path(ConfigScope.WORKSPACE, str(workspace_root))
|
|
63
|
-
|
|
64
|
+
|
|
64
65
|
config_monitors = [
|
|
65
|
-
ConfigMonitor(
|
|
66
|
-
|
|
66
|
+
ConfigMonitor(
|
|
67
|
+
project_config_path, lambda: on_config_change(str(project_config_path))
|
|
68
|
+
),
|
|
69
|
+
ConfigMonitor(
|
|
70
|
+
workspace_config_path, lambda: on_config_change(str(workspace_config_path))
|
|
71
|
+
),
|
|
67
72
|
]
|
|
68
|
-
|
|
73
|
+
|
|
69
74
|
await project_manager.start_all()
|
|
70
75
|
git_task = asyncio.create_task(git_monitor.start())
|
|
71
76
|
config_tasks = [asyncio.create_task(m.start()) for m in config_monitors]
|
|
72
|
-
|
|
77
|
+
|
|
73
78
|
yield
|
|
74
79
|
# Shutdown
|
|
75
80
|
logger.info("Shutting down Monoco Daemon services...")
|
|
@@ -79,10 +84,11 @@ async def lifespan(app: FastAPI):
|
|
|
79
84
|
m.stop()
|
|
80
85
|
if project_manager:
|
|
81
86
|
project_manager.stop_all()
|
|
82
|
-
|
|
87
|
+
|
|
83
88
|
await git_task
|
|
84
89
|
await asyncio.gather(*config_tasks)
|
|
85
|
-
|
|
90
|
+
|
|
91
|
+
|
|
86
92
|
app = FastAPI(
|
|
87
93
|
title="Monoco Daemon",
|
|
88
94
|
description=description,
|
|
@@ -92,17 +98,18 @@ app = FastAPI(
|
|
|
92
98
|
lifespan=lifespan,
|
|
93
99
|
)
|
|
94
100
|
|
|
101
|
+
|
|
95
102
|
def get_project_or_404(project_id: Optional[str] = None):
|
|
96
103
|
if not project_manager:
|
|
97
104
|
raise HTTPException(status_code=503, detail="Daemon not fully initialized")
|
|
98
|
-
|
|
105
|
+
|
|
99
106
|
# If project_id is not provided, try to use the first available project (default behavior)
|
|
100
107
|
if not project_id:
|
|
101
108
|
projects = list(project_manager.projects.values())
|
|
102
109
|
if not projects:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
110
|
+
# Fallback to legacy single-project mode if no sub-projects found?
|
|
111
|
+
# Or maybe ProjectManager scan logic already covers CWD as a project.
|
|
112
|
+
raise HTTPException(status_code=404, detail="No projects found")
|
|
106
113
|
return projects[0]
|
|
107
114
|
|
|
108
115
|
project = project_manager.get_project(project_id)
|
|
@@ -110,6 +117,7 @@ def get_project_or_404(project_id: Optional[str] = None):
|
|
|
110
117
|
raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
|
|
111
118
|
return project
|
|
112
119
|
|
|
120
|
+
|
|
113
121
|
# CORS Configuration
|
|
114
122
|
# Kanban may run on different ports (e.g. localhost:3000, tauri://localhost)
|
|
115
123
|
app.add_middleware(
|
|
@@ -120,6 +128,7 @@ app.add_middleware(
|
|
|
120
128
|
allow_headers=["*"],
|
|
121
129
|
)
|
|
122
130
|
|
|
131
|
+
|
|
123
132
|
@app.get("/health")
|
|
124
133
|
async def health_check():
|
|
125
134
|
"""
|
|
@@ -127,6 +136,7 @@ async def health_check():
|
|
|
127
136
|
"""
|
|
128
137
|
return {"status": "ok", "component": "monoco-daemon"}
|
|
129
138
|
|
|
139
|
+
|
|
130
140
|
@app.get("/api/v1/projects")
|
|
131
141
|
async def list_projects():
|
|
132
142
|
"""
|
|
@@ -136,6 +146,7 @@ async def list_projects():
|
|
|
136
146
|
return []
|
|
137
147
|
return project_manager.list_projects()
|
|
138
148
|
|
|
149
|
+
|
|
139
150
|
@app.get("/api/v1/info")
|
|
140
151
|
async def get_project_info(project_id: Optional[str] = None):
|
|
141
152
|
"""
|
|
@@ -148,9 +159,10 @@ async def get_project_info(project_id: Optional[str] = None):
|
|
|
148
159
|
"id": project.id,
|
|
149
160
|
"version": "0.1.0",
|
|
150
161
|
"mode": "daemon",
|
|
151
|
-
"head": current_hash
|
|
162
|
+
"head": current_hash,
|
|
152
163
|
}
|
|
153
164
|
|
|
165
|
+
|
|
154
166
|
@app.get("/api/v1/config/dictionary")
|
|
155
167
|
async def get_ui_dictionary(project_id: Optional[str] = None) -> Dict[str, str]:
|
|
156
168
|
"""
|
|
@@ -161,29 +173,27 @@ async def get_ui_dictionary(project_id: Optional[str] = None) -> Dict[str, str]:
|
|
|
161
173
|
config = get_config(str(project.path))
|
|
162
174
|
return config.ui.dictionary
|
|
163
175
|
|
|
176
|
+
|
|
164
177
|
@app.get("/api/v1/events")
|
|
165
178
|
async def sse_endpoint(request: Request):
|
|
166
179
|
"""
|
|
167
180
|
Server-Sent Events endpoint for real-time updates.
|
|
168
181
|
"""
|
|
169
182
|
queue = await broadcaster.subscribe()
|
|
170
|
-
|
|
183
|
+
|
|
171
184
|
async def event_generator():
|
|
172
185
|
try:
|
|
173
186
|
# Quick ping to confirm connection
|
|
174
|
-
yield {
|
|
175
|
-
|
|
176
|
-
"data": "connected"
|
|
177
|
-
}
|
|
178
|
-
|
|
187
|
+
yield {"event": "connect", "data": "connected"}
|
|
188
|
+
|
|
179
189
|
while True:
|
|
180
190
|
if await request.is_disconnected():
|
|
181
191
|
break
|
|
182
|
-
|
|
192
|
+
|
|
183
193
|
# Wait for new messages
|
|
184
194
|
message = await queue.get()
|
|
185
195
|
yield message
|
|
186
|
-
|
|
196
|
+
|
|
187
197
|
except asyncio.CancelledError:
|
|
188
198
|
logger.debug("SSE connection cancelled")
|
|
189
199
|
finally:
|
|
@@ -191,18 +201,21 @@ async def sse_endpoint(request: Request):
|
|
|
191
201
|
|
|
192
202
|
return EventSourceResponse(event_generator())
|
|
193
203
|
|
|
204
|
+
|
|
194
205
|
@app.get("/api/v1/issues")
|
|
195
206
|
async def get_issues(
|
|
196
207
|
project_id: Optional[str] = None,
|
|
197
|
-
path: Optional[str] = Query(
|
|
208
|
+
path: Optional[str] = Query(
|
|
209
|
+
None, description="Absolute file path for reverse lookup"
|
|
210
|
+
),
|
|
198
211
|
):
|
|
199
212
|
"""
|
|
200
213
|
List all issues in the project, or get a single issue by file path.
|
|
201
|
-
|
|
214
|
+
|
|
202
215
|
Query Parameters:
|
|
203
216
|
- project_id: Optional project filter
|
|
204
217
|
- path: Optional absolute file path for reverse lookup (returns single issue)
|
|
205
|
-
|
|
218
|
+
|
|
206
219
|
If 'path' is provided, returns a single IssueMetadata object.
|
|
207
220
|
Otherwise, returns a list of all issues.
|
|
208
221
|
"""
|
|
@@ -211,27 +224,45 @@ async def get_issues(
|
|
|
211
224
|
p = Path(path)
|
|
212
225
|
if not p.exists():
|
|
213
226
|
raise HTTPException(status_code=404, detail=f"File {path} not found")
|
|
214
|
-
|
|
227
|
+
|
|
215
228
|
issue = parse_issue(p)
|
|
216
229
|
if not issue:
|
|
217
|
-
raise HTTPException(
|
|
218
|
-
|
|
230
|
+
raise HTTPException(
|
|
231
|
+
status_code=400, detail=f"File {path} is not a valid Monoco issue"
|
|
232
|
+
)
|
|
233
|
+
|
|
219
234
|
return issue
|
|
220
|
-
|
|
235
|
+
|
|
221
236
|
# Standard list operation
|
|
222
237
|
project = get_project_or_404(project_id)
|
|
223
238
|
issues = list_issues(project.issues_root)
|
|
224
239
|
return issues
|
|
225
240
|
|
|
226
|
-
|
|
227
|
-
from monoco.features.issue.
|
|
228
|
-
|
|
241
|
+
|
|
242
|
+
from monoco.features.issue.core import (
|
|
243
|
+
list_issues,
|
|
244
|
+
create_issue_file,
|
|
245
|
+
update_issue,
|
|
246
|
+
delete_issue_file,
|
|
247
|
+
find_issue_path,
|
|
248
|
+
parse_issue,
|
|
249
|
+
get_board_data,
|
|
250
|
+
parse_issue_detail,
|
|
251
|
+
update_issue_content,
|
|
252
|
+
)
|
|
253
|
+
from monoco.features.issue.models import IssueMetadata, IssueDetail
|
|
254
|
+
from monoco.daemon.models import (
|
|
255
|
+
CreateIssueRequest,
|
|
256
|
+
UpdateIssueRequest,
|
|
257
|
+
UpdateIssueContentRequest,
|
|
258
|
+
)
|
|
229
259
|
from monoco.daemon.stats import calculate_dashboard_stats, DashboardStats
|
|
230
260
|
from fastapi import FastAPI, Request, HTTPException
|
|
231
261
|
from typing import Optional, List, Dict
|
|
232
262
|
|
|
233
263
|
# ... existing code ...
|
|
234
264
|
|
|
265
|
+
|
|
235
266
|
@app.get("/api/v1/board")
|
|
236
267
|
async def get_board_endpoint(project_id: Optional[str] = None):
|
|
237
268
|
"""
|
|
@@ -241,6 +272,7 @@ async def get_board_endpoint(project_id: Optional[str] = None):
|
|
|
241
272
|
board = get_board_data(project.issues_root)
|
|
242
273
|
return board
|
|
243
274
|
|
|
275
|
+
|
|
244
276
|
@app.get("/api/v1/stats/dashboard", response_model=DashboardStats)
|
|
245
277
|
async def get_dashboard_stats_endpoint(project_id: Optional[str] = None):
|
|
246
278
|
"""
|
|
@@ -256,23 +288,24 @@ async def create_issue_endpoint(payload: CreateIssueRequest):
|
|
|
256
288
|
Create a new issue.
|
|
257
289
|
"""
|
|
258
290
|
project = get_project_or_404(payload.project_id)
|
|
259
|
-
|
|
291
|
+
|
|
260
292
|
try:
|
|
261
293
|
issue, _ = create_issue_file(
|
|
262
|
-
project.issues_root,
|
|
263
|
-
payload.type,
|
|
264
|
-
payload.title,
|
|
265
|
-
parent=payload.parent,
|
|
266
|
-
status=payload.status,
|
|
294
|
+
project.issues_root,
|
|
295
|
+
payload.type,
|
|
296
|
+
payload.title,
|
|
297
|
+
parent=payload.parent,
|
|
298
|
+
status=payload.status,
|
|
267
299
|
stage=payload.stage,
|
|
268
|
-
dependencies=payload.dependencies,
|
|
269
|
-
related=payload.related,
|
|
270
|
-
subdir=payload.subdir
|
|
300
|
+
dependencies=payload.dependencies,
|
|
301
|
+
related=payload.related,
|
|
302
|
+
subdir=payload.subdir,
|
|
271
303
|
)
|
|
272
304
|
return issue
|
|
273
305
|
except Exception as e:
|
|
274
306
|
raise HTTPException(status_code=400, detail=str(e))
|
|
275
307
|
|
|
308
|
+
|
|
276
309
|
@app.get("/api/v1/issues/{issue_id}", response_model=IssueDetail)
|
|
277
310
|
async def get_issue_endpoint(issue_id: str, project_id: Optional[str] = None):
|
|
278
311
|
"""
|
|
@@ -286,19 +319,19 @@ async def get_issue_endpoint(issue_id: str, project_id: Optional[str] = None):
|
|
|
286
319
|
# Global Search across all projects in the workspace
|
|
287
320
|
if not project_manager:
|
|
288
321
|
raise HTTPException(status_code=503, detail="Daemon not fully initialized")
|
|
289
|
-
|
|
322
|
+
|
|
290
323
|
for p_ctx in project_manager.projects.values():
|
|
291
324
|
path = find_issue_path(p_ctx.issues_root, issue_id)
|
|
292
325
|
if path:
|
|
293
326
|
break
|
|
294
|
-
|
|
327
|
+
|
|
295
328
|
if not path:
|
|
296
329
|
raise HTTPException(status_code=404, detail=f"Issue {issue_id} not found")
|
|
297
|
-
|
|
330
|
+
|
|
298
331
|
issue = parse_issue_detail(path)
|
|
299
332
|
if not issue:
|
|
300
333
|
raise HTTPException(status_code=500, detail=f"Failed to parse issue {issue_id}")
|
|
301
|
-
|
|
334
|
+
|
|
302
335
|
return issue
|
|
303
336
|
|
|
304
337
|
|
|
@@ -308,66 +341,68 @@ async def update_issue_endpoint(issue_id: str, payload: UpdateIssueRequest):
|
|
|
308
341
|
Update an issue's metadata (Status, Stage, Solution, Parent, Dependencies, etc.).
|
|
309
342
|
"""
|
|
310
343
|
project = get_project_or_404(payload.project_id)
|
|
311
|
-
|
|
344
|
+
|
|
312
345
|
try:
|
|
313
346
|
# Pre-lookup to get the current path for move detection
|
|
314
347
|
old_path_obj = find_issue_path(project.issues_root, issue_id)
|
|
315
348
|
old_path = str(old_path_obj.absolute()) if old_path_obj else None
|
|
316
349
|
|
|
317
350
|
issue = update_issue(
|
|
318
|
-
project.issues_root,
|
|
319
|
-
issue_id,
|
|
320
|
-
status=payload.status,
|
|
321
|
-
stage=payload.stage,
|
|
351
|
+
project.issues_root,
|
|
352
|
+
issue_id,
|
|
353
|
+
status=payload.status,
|
|
354
|
+
stage=payload.stage,
|
|
322
355
|
solution=payload.solution,
|
|
323
356
|
parent=payload.parent,
|
|
324
357
|
dependencies=payload.dependencies,
|
|
325
358
|
related=payload.related,
|
|
326
|
-
tags=payload.tags
|
|
359
|
+
tags=payload.tags,
|
|
327
360
|
)
|
|
328
361
|
|
|
329
362
|
# Post-update: check if path changed
|
|
330
363
|
if old_path and issue.path != old_path:
|
|
331
364
|
# Trigger a specialized move event to help editors redirect
|
|
332
|
-
await project.notify_move(
|
|
365
|
+
await project.notify_move(
|
|
366
|
+
old_path, issue.path, issue.model_dump(mode="json")
|
|
367
|
+
)
|
|
333
368
|
|
|
334
369
|
return issue
|
|
335
370
|
except FileNotFoundError:
|
|
336
371
|
raise HTTPException(status_code=404, detail=f"Issue {issue_id} not found")
|
|
337
372
|
except ValueError as e:
|
|
338
|
-
|
|
373
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
339
374
|
except Exception as e:
|
|
340
375
|
raise HTTPException(status_code=500, detail=str(e))
|
|
341
376
|
|
|
377
|
+
|
|
342
378
|
@app.put("/api/v1/issues/{issue_id}/content", response_model=IssueMetadata)
|
|
343
|
-
async def update_issue_content_endpoint(
|
|
379
|
+
async def update_issue_content_endpoint(
|
|
380
|
+
issue_id: str, payload: UpdateIssueContentRequest
|
|
381
|
+
):
|
|
344
382
|
"""
|
|
345
383
|
Update raw content of an issue. Validates integrity before saving.
|
|
346
384
|
"""
|
|
347
385
|
project = get_project_or_404(payload.project_id)
|
|
348
|
-
|
|
386
|
+
|
|
349
387
|
try:
|
|
350
388
|
# Note: We use PUT because we are replacing the content representation
|
|
351
|
-
issue = update_issue_content(
|
|
352
|
-
project.issues_root,
|
|
353
|
-
issue_id,
|
|
354
|
-
payload.content
|
|
355
|
-
)
|
|
389
|
+
issue = update_issue_content(project.issues_root, issue_id, payload.content)
|
|
356
390
|
return issue
|
|
357
391
|
except FileNotFoundError:
|
|
358
392
|
raise HTTPException(status_code=404, detail=f"Issue {issue_id} not found")
|
|
359
393
|
except ValueError as e:
|
|
360
|
-
|
|
394
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
361
395
|
except Exception as e:
|
|
362
396
|
raise HTTPException(status_code=500, detail=str(e))
|
|
363
397
|
|
|
398
|
+
|
|
364
399
|
@app.delete("/api/v1/issues/{issue_id}")
|
|
365
400
|
async def delete_issue_endpoint(issue_id: str, project_id: Optional[str] = None):
|
|
366
401
|
"""
|
|
367
402
|
Delete an issue (physical removal).
|
|
368
403
|
"""
|
|
369
404
|
project = get_project_or_404(project_id)
|
|
370
|
-
|
|
405
|
+
|
|
371
406
|
try:
|
|
372
407
|
delete_issue_file(project.issues_root, issue_id)
|
|
373
408
|
return {"status": "deleted", "id": issue_id}
|
|
@@ -376,6 +411,7 @@ async def delete_issue_endpoint(issue_id: str, project_id: Optional[str] = None)
|
|
|
376
411
|
except Exception as e:
|
|
377
412
|
raise HTTPException(status_code=500, detail=str(e))
|
|
378
413
|
|
|
414
|
+
|
|
379
415
|
@app.post("/api/v1/monitor/refresh")
|
|
380
416
|
async def refresh_monitor():
|
|
381
417
|
"""
|
|
@@ -385,15 +421,17 @@ async def refresh_monitor():
|
|
|
385
421
|
# In a real impl, we might force the monitor to wake up.
|
|
386
422
|
# For now, just getting the hash is a good sanity check.
|
|
387
423
|
current_hash = await git_monitor.get_head_hash()
|
|
388
|
-
|
|
424
|
+
|
|
389
425
|
# If we wanted to FORCE broadcast, we could do it here,
|
|
390
426
|
# but the monitor loop will pick it up in <2s anyway.
|
|
391
427
|
# To be "instant", we can manually broadcast if we know it changed?
|
|
392
428
|
# Or just returning the hash confirms the daemon sees it.
|
|
393
429
|
return {"status": "refreshed", "head": current_hash}
|
|
394
430
|
|
|
431
|
+
|
|
395
432
|
# --- Execution Profiles ---
|
|
396
433
|
|
|
434
|
+
|
|
397
435
|
@app.get("/api/v1/execution/profiles", response_model=List[ExecutionProfile])
|
|
398
436
|
async def get_execution_profiles(project_id: Optional[str] = None):
|
|
399
437
|
"""
|
|
@@ -405,9 +443,10 @@ async def get_execution_profiles(project_id: Optional[str] = None):
|
|
|
405
443
|
elif project_manager and project_manager.projects:
|
|
406
444
|
# Fallback to first project if none specified
|
|
407
445
|
project = list(project_manager.projects.values())[0]
|
|
408
|
-
|
|
446
|
+
|
|
409
447
|
return scan_execution_profiles(project.path if project else None)
|
|
410
448
|
|
|
449
|
+
|
|
411
450
|
@app.get("/api/v1/execution/profiles/detail", response_model=ExecutionProfile)
|
|
412
451
|
async def get_execution_profile_detail(path: str):
|
|
413
452
|
"""
|
|
@@ -418,30 +457,35 @@ async def get_execution_profile_detail(path: str):
|
|
|
418
457
|
raise HTTPException(status_code=404, detail="Profile not found")
|
|
419
458
|
return profile
|
|
420
459
|
|
|
460
|
+
|
|
421
461
|
# --- Workspace State Management ---
|
|
422
462
|
from monoco.core.state import WorkspaceState
|
|
423
463
|
|
|
464
|
+
|
|
424
465
|
@app.get("/api/v1/workspace/state", response_model=WorkspaceState)
|
|
425
466
|
async def get_workspace_state():
|
|
426
467
|
"""
|
|
427
468
|
Get the persisted workspace state (e.g. last active project).
|
|
428
469
|
"""
|
|
429
470
|
if not project_manager:
|
|
430
|
-
|
|
431
|
-
|
|
471
|
+
raise HTTPException(status_code=503, detail="Daemon not initialized")
|
|
472
|
+
|
|
432
473
|
return WorkspaceState.load(project_manager.workspace_root)
|
|
433
474
|
|
|
475
|
+
|
|
434
476
|
@app.post("/api/v1/workspace/state", response_model=WorkspaceState)
|
|
435
477
|
async def update_workspace_state(state: WorkspaceState):
|
|
436
478
|
"""
|
|
437
479
|
Update the workspace state.
|
|
438
480
|
"""
|
|
439
481
|
if not project_manager:
|
|
440
|
-
|
|
482
|
+
raise HTTPException(status_code=503, detail="Daemon not initialized")
|
|
441
483
|
|
|
442
484
|
try:
|
|
443
485
|
state.save(project_manager.workspace_root)
|
|
444
486
|
return state
|
|
445
487
|
except Exception as e:
|
|
446
488
|
logger.error(f"Failed to write state file: {e}")
|
|
447
|
-
raise HTTPException(
|
|
489
|
+
raise HTTPException(
|
|
490
|
+
status_code=500, detail=f"Failed to persist state: {str(e)}"
|
|
491
|
+
)
|
monoco/daemon/commands.py
CHANGED
|
@@ -7,30 +7,31 @@ from monoco.core.config import get_config
|
|
|
7
7
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
+
|
|
10
11
|
def serve(
|
|
11
12
|
host: str = typer.Option("127.0.0.1", "--host", "-h", help="Bind host"),
|
|
12
13
|
port: int = typer.Option(8642, "--port", "-p", help="Bind port"),
|
|
13
|
-
reload: bool = typer.Option(
|
|
14
|
+
reload: bool = typer.Option(
|
|
15
|
+
False, "--reload", "-r", help="Enable auto-reload for dev"
|
|
16
|
+
),
|
|
14
17
|
root: Optional[str] = typer.Option(None, "--root", help="Workspace root directory"),
|
|
15
18
|
):
|
|
16
19
|
"""
|
|
17
20
|
Start the Monoco Daemon server.
|
|
18
21
|
"""
|
|
19
22
|
settings = get_config()
|
|
20
|
-
|
|
23
|
+
|
|
21
24
|
if root:
|
|
22
25
|
os.environ["MONOCO_SERVER_ROOT"] = str(Path(root).resolve())
|
|
23
|
-
print_output(
|
|
26
|
+
print_output(
|
|
27
|
+
f"Workspace Root: {os.environ['MONOCO_SERVER_ROOT']}", title="Monoco Serve"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
print_output(
|
|
31
|
+
f"Starting Monoco Daemon on http://{host}:{port}", title="Monoco Serve"
|
|
32
|
+
)
|
|
24
33
|
|
|
25
|
-
print_output(f"Starting Monoco Daemon on http://{host}:{port}", title="Monoco Serve")
|
|
26
|
-
|
|
27
34
|
# We pass the import string to uvicorn to enable reload if needed
|
|
28
35
|
app_str = "monoco.daemon.app:app"
|
|
29
|
-
|
|
30
|
-
uvicorn.run(
|
|
31
|
-
app_str,
|
|
32
|
-
host=host,
|
|
33
|
-
port=port,
|
|
34
|
-
reload=reload,
|
|
35
|
-
log_level="info"
|
|
36
|
-
)
|
|
36
|
+
|
|
37
|
+
uvicorn.run(app_str, host=host, port=port, reload=reload, log_level="info")
|
monoco/daemon/models.py
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
from pydantic import BaseModel
|
|
2
2
|
from typing import Optional, List
|
|
3
|
-
from monoco.features.issue.models import
|
|
3
|
+
from monoco.features.issue.models import (
|
|
4
|
+
IssueType,
|
|
5
|
+
IssueStatus,
|
|
6
|
+
IssueSolution,
|
|
7
|
+
IssueStage,
|
|
8
|
+
)
|
|
9
|
+
|
|
4
10
|
|
|
5
11
|
class CreateIssueRequest(BaseModel):
|
|
6
12
|
type: IssueType
|
|
@@ -11,8 +17,9 @@ class CreateIssueRequest(BaseModel):
|
|
|
11
17
|
dependencies: List[str] = []
|
|
12
18
|
related: List[str] = []
|
|
13
19
|
subdir: Optional[str] = None
|
|
14
|
-
project_id: Optional[str] = None
|
|
15
|
-
|
|
20
|
+
project_id: Optional[str] = None # Added for multi-project support
|
|
21
|
+
|
|
22
|
+
|
|
16
23
|
class UpdateIssueRequest(BaseModel):
|
|
17
24
|
status: Optional[IssueStatus] = None
|
|
18
25
|
stage: Optional[IssueStage] = None
|
|
@@ -23,6 +30,7 @@ class UpdateIssueRequest(BaseModel):
|
|
|
23
30
|
tags: Optional[List[str]] = None
|
|
24
31
|
project_id: Optional[str] = None
|
|
25
32
|
|
|
33
|
+
|
|
26
34
|
class UpdateIssueContentRequest(BaseModel):
|
|
27
35
|
content: str
|
|
28
36
|
project_id: Optional[str] = None
|
monoco/daemon/reproduce_stats.py
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
|
|
2
1
|
import sys
|
|
3
2
|
from pathlib import Path
|
|
4
|
-
from datetime import datetime
|
|
5
3
|
|
|
6
4
|
# Add Toolkit to sys.path
|
|
7
5
|
sys.path.append("/Users/indenscale/Documents/Projects/Monoco/Toolkit")
|
|
8
6
|
|
|
9
7
|
from monoco.daemon.stats import calculate_dashboard_stats
|
|
10
8
|
|
|
9
|
+
|
|
11
10
|
def run():
|
|
12
11
|
issues_root = Path("/Users/indenscale/Documents/Projects/Monoco/Toolkit/Issues")
|
|
13
12
|
print(f"Scanning {issues_root}...")
|
|
14
|
-
|
|
13
|
+
|
|
15
14
|
stats = calculate_dashboard_stats(issues_root)
|
|
16
|
-
|
|
15
|
+
|
|
17
16
|
print(f"Found {len(stats.recent_activities)} activities.")
|
|
18
|
-
|
|
17
|
+
|
|
19
18
|
seen_ids = set()
|
|
20
19
|
for act in stats.recent_activities:
|
|
21
20
|
print(f"[{act.type}] {act.id} - {act.issue_title} ({act.timestamp})")
|
|
22
21
|
if act.id in seen_ids:
|
|
23
22
|
print(f"!!! DUPLICATE ID FOUND: {act.id}")
|
|
24
23
|
seen_ids.add(act.id)
|
|
25
|
-
|
|
24
|
+
|
|
26
25
|
# Check for duplicate issue + type
|
|
27
26
|
# e.g. multiple UPDATED for same issue
|
|
28
|
-
|
|
27
|
+
|
|
29
28
|
# Group by issue
|
|
30
29
|
by_issue = {}
|
|
31
30
|
for act in stats.recent_activities:
|
|
32
31
|
if act.issue_id not in by_issue:
|
|
33
32
|
by_issue[act.issue_id] = []
|
|
34
33
|
by_issue[act.issue_id].append(act)
|
|
35
|
-
|
|
34
|
+
|
|
36
35
|
for issue_id, acts in by_issue.items():
|
|
37
36
|
if len(acts) > 1:
|
|
38
37
|
print(f"Issue {issue_id} has multiple activities: {[a.type for a in acts]}")
|
|
39
38
|
|
|
39
|
+
|
|
40
40
|
if __name__ == "__main__":
|
|
41
41
|
run()
|