From 8869394feb89833e0a96b9c7544b4e8aa306779f Mon Sep 17 00:00:00 2001 From: Fred Porter Date: Wed, 19 Aug 2026 19:26:50 +0800 Subject: [PATCH] Retire legacy Dev Mode orchestration --- backend/app/api/archived/mcp_handlers.py.old | 640 ------------------ backend/app/api/chat.py | 10 +- backend/app/api/developer_api.py | 3 +- backend/app/api/mcp.py | 18 - backend/app/api/mcp_guardrails.py | 3 - backend/app/api/mcp_handlers/__init__.py | 4 - backend/app/api/mcp_handlers/autostart.py | 24 - backend/app/skills/builtin/skill_autostart.py | 269 -------- .../builtin/skill_dev_destroy_rebuild.py | 470 ------------- .../skills/builtin/skill_dev_mode_executor.py | 402 ----------- backend/app/skills/catalogue.json | 3 - backend/tests/test_mcp_guardrails.py | 1 - ...PEC_CONTROL_PANEL_AND_AGENTIC_EXECUTION.md | 75 -- docs/README.md | 2 - docs/SKILLS_AUDIT_2026-08-18.md | 5 +- docs/SNACKS_SKILLS_STATUS_AUDIT.md | 128 ---- scripts/ucore_startup.sh | 6 +- scripts/ucore_watchdog.sh | 2 +- 18 files changed, 12 insertions(+), 2053 deletions(-) delete mode 100644 backend/app/api/archived/mcp_handlers.py.old delete mode 100644 backend/app/api/mcp_handlers/autostart.py delete mode 100644 backend/app/skills/builtin/skill_autostart.py delete mode 100644 backend/app/skills/builtin/skill_dev_destroy_rebuild.py delete mode 100644 backend/app/skills/builtin/skill_dev_mode_executor.py delete mode 100644 docs/FEATURE_SPEC_CONTROL_PANEL_AND_AGENTIC_EXECUTION.md delete mode 100644 docs/SNACKS_SKILLS_STATUS_AUDIT.md diff --git a/backend/app/api/archived/mcp_handlers.py.old b/backend/app/api/archived/mcp_handlers.py.old deleted file mode 100644 index 064ece4a..00000000 --- a/backend/app/api/archived/mcp_handlers.py.old +++ /dev/null @@ -1,640 +0,0 @@ -#!/usr/bin/env python3 -"""MCP Tool Handlers — Extracted from mcp.py for modularization. - -This module contains individual tool handlers extracted from the monolithic -handle_mcp_call function to improve maintainability. - -NOTE: All shared utilities are imported lazily inside handlers so that -tests can patch them via ``mcp_api.`` (monkeypatch pattern). -""" -from __future__ import annotations - -import json -import logging -from typing import Any, Dict - -from aiohttp import web - -from app.skills.registry import get_skill - -log = logging.getLogger("ucore.api.mcp_handlers") - - -async def handle_skill_tool(tool_name: str, arguments: dict, request_id: Any) -> web.Response: - """Handle skill_xxx tool calls.""" - from app.api.mcp import get_skill - - skill_id = tool_name[6:] # Remove "skill_" prefix - skill = get_skill(skill_id) - if not skill: - return web.json_response({ - "jsonrpc": "2.0", - "error": { - "code": -32601, - "message": f"Tool '{tool_name}' not found", - }, - "id": request_id, - }, status=404) - - result = await skill.run(**arguments) - return web.json_response({ - "jsonrpc": "2.0", - "result": { - "content": [ - {"type": "text", "text": json.dumps(result, indent=2)}, - ], - }, - "id": request_id, - }) - - -async def handle_knowledge_search(arguments: dict, request_id: Any) -> web.Response: - """Handle knowledge_search tool.""" - from app.knowledge.appflowy import semantic_search - - query = str(arguments.get("query", "")).strip() - if not query: - return web.json_response({ - "jsonrpc": "2.0", - "error": { - "code": -32602, - "message": "query is required", - }, - "id": request_id, - }, status=400) - - try: - limit = int(arguments.get("limit", 10)) - except (TypeError, ValueError): - return web.json_response({ - "jsonrpc": "2.0", - "error": { - "code": -32602, - "message": "limit must be an integer", - }, - "id": request_id, - }, status=400) - - workspace_id = arguments.get("workspace_id") - results = semantic_search(query, workspace_id, max(1, limit)) - return web.json_response({ - "jsonrpc": "2.0", - "result": { - "content": [ - {"type": "text", "text": json.dumps(results, indent=2)}, - ], - }, - "id": request_id, - }) - - -async def handle_knowledge_list_workspaces(arguments: dict, request_id: Any) -> web.Response: - """Handle knowledge_list_workspaces tool.""" - from app.knowledge.appflowy import list_workspaces - - ws = list_workspaces() - return web.json_response({ - "jsonrpc": "2.0", - "result": { - "content": [ - {"type": "text", "text": json.dumps(ws, indent=2)}, - ], - }, - "id": request_id, - }) - - -async def handle_knowledge_list_documents(arguments: dict, request_id: Any) -> web.Response: - """Handle knowledge_list_documents tool.""" - from app.knowledge.appflowy import list_documents - - workspace_id = arguments.get("workspace_id") - docs = list_documents(str(workspace_id) if workspace_id else None) - return web.json_response({ - "jsonrpc": "2.0", - "result": { - "content": [ - {"type": "text", "text": json.dumps(docs, indent=2)}, - ], - }, - "id": request_id, - }) - - -async def handle_clipboard_capture(arguments: dict, request_id: Any) -> web.Response: - """Handle clipboard_capture tool.""" - from app.api.mcp import capture_current_clipboard - - source = arguments.get("source", "user_copy") - metadata = arguments.get("metadata") or {} - try: - item = capture_current_clipboard(source=source, metadata=metadata) - except Exception as exc: - return web.json_response({ - "jsonrpc": "2.0", - "error": {"code": -32000, "message": str(exc)}, - "id": request_id, - }, status=400) - return web.json_response({ - "jsonrpc": "2.0", - "result": { - "content": [ - {"type": "text", "text": json.dumps(item, indent=2)}, - ], - }, - "id": request_id, - }) - - -async def handle_clipboard_get(arguments: dict, request_id: Any) -> web.Response: - """Handle clipboard_get tool.""" - from app.api.mcp import get_item_by_id, get_recent_items - - item_id = arguments.get("item_id") - if item_id: - found_item = get_item_by_id(str(item_id)) - if not found_item: - return web.json_response({ - "jsonrpc": "2.0", - "error": { - "code": -32602, - "message": f"Item {item_id} not found", - }, - "id": request_id, - }, status=404) - return web.json_response({ - "jsonrpc": "2.0", - "result": { - "content": [ - {"type": "text", "text": json.dumps(found_item, indent=2)}, - ], - }, - "id": request_id, - }) - - limit = int(arguments.get("limit", 20)) - include_pinned = bool(arguments.get("include_pinned", True)) - items = get_recent_items(limit=limit, include_pinned=include_pinned) - return web.json_response({ - "jsonrpc": "2.0", - "result": { - "content": [ - {"type": "text", "text": json.dumps({"items": items, "count": len(items)}, indent=2)}, - ], - }, - "id": request_id, - }) - - -async def handle_clipboard_delete(arguments: dict, request_id: Any) -> web.Response: - """Handle clipboard_delete tool.""" - from app.api.mcp import delete_item - - item_id = arguments.get("item_id") - if not item_id: - return web.json_response({ - "jsonrpc": "2.0", - "error": { - "code": -32602, - "message": "item_id is required", - }, - "id": request_id, - }, status=400) - - success = delete_item(str(item_id)) - return web.json_response({ - "jsonrpc": "2.0", - "result": { - "content": [ - {"type": "text", "text": json.dumps({"status": "deleted" if success else "not_found", "id": item_id})}, - ], - }, - "id": request_id, - }) - - -async def handle_tasker_list_boards(arguments: dict, request_id: Any) -> web.Response: - """Handle tasker_list_boards tool.""" - from app.api.mcp import list_tasker_boards - - boards = list_tasker_boards(arguments.get("tasker_dir")) - return web.json_response({ - "jsonrpc": "2.0", - "result": { - "content": [ - {"type": "text", "text": json.dumps(boards, indent=2)}, - ], - }, - "id": request_id, - }) - - -async def handle_tasker_read_task(arguments: dict, request_id: Any) -> web.Response: - """Handle tasker_read_task tool.""" - from app.api.mcp import read_task_markdown - - board = arguments.get("board") - task = arguments.get("task") - if not board or not task: - return web.json_response({ - "jsonrpc": "2.0", - "error": { - "code": -32602, - "message": "board and task are required", - }, - "id": request_id, - }, status=400) - - result = read_task_markdown( - board=board, task=task, tasker_dir=arguments.get("tasker_dir") - ) - return web.json_response({ - "jsonrpc": "2.0", - "result": { - "content": [ - {"type": "text", "text": json.dumps(result, indent=2)}, - ], - }, - "id": request_id, - }) - - -async def handle_tasker_write_task(arguments: dict, request_id: Any) -> web.Response: - """Handle tasker_write_task tool.""" - from app.api.mcp import write_task_markdown - - title = str(arguments.get("title", "")) - if not title: - return web.json_response({ - "jsonrpc": "2.0", - "error": { - "code": -32602, - "message": "title is required", - }, - "id": request_id, - }, status=400) - - metadata = arguments.get("metadata") - if metadata is not None and not isinstance(metadata, dict): - return web.json_response({ - "jsonrpc": "2.0", - "error": { - "code": -32602, - "message": "metadata must be an object", - }, - "id": request_id, - }, status=400) - - try: - result = write_task_markdown( - title=title, - board=str(arguments.get("board", "inbox")), - status=str(arguments.get("status", "todo")), - body=str(arguments.get("body", "")), - source=str(arguments.get("source", "manual")), - source_id=str(arguments["source_id"]) if "source_id" in arguments else None, - metadata=metadata, - task=str(arguments["task"]) if "task" in arguments else None, - tasker_dir=arguments.get("tasker_dir"), - ) - except ValueError as exc: - return web.json_response({ - "jsonrpc": "2.0", - "error": {"code": -32602, "message": str(exc)}, - "id": request_id, - }, status=400) - - return web.json_response({ - "jsonrpc": "2.0", - "result": { - "content": [ - {"type": "text", "text": json.dumps(result, indent=2)}, - ], - }, - "id": request_id, - }) - - -async def handle_flow_router_analytics(arguments: dict, request_id: Any) -> web.Response: - """Handle flow_router_analytics tool.""" - from app.api.flow_router.api import get_flow_router - - router = get_flow_router() - analytics = router.get_analytics() - return web.json_response({ - "jsonrpc": "2.0", - "result": { - "content": [ - {"type": "text", "text": json.dumps(analytics, indent=2)}, - ], - }, - "id": request_id, - }) - - -async def handle_flow_router_history(arguments: dict, request_id: Any) -> web.Response: - """Handle flow_router_history tool.""" - from app.api.flow_router.api import get_flow_router - - try: - limit = int(arguments.get("limit", 100)) - except (TypeError, ValueError): - limit = 100 - - router = get_flow_router() - history = router.get_routing_history(limit=limit) - return web.json_response({ - "jsonrpc": "2.0", - "result": { - "content": [ - {"type": "text", "text": json.dumps(history, indent=2)}, - ], - }, - "id": request_id, - }) - - -async def handle_gridsmith_grid(arguments: dict, request_id: Any) -> web.Response: - """Handle gridsmith_grid tool.""" - rows = int(arguments.get("rows", 10)) - cols = int(arguments.get("cols", 10)) - bridge = get_gridsmith_bridge() - result = bridge.create_grid(rows=rows, cols=cols) - return web.json_response({ - "jsonrpc": "2.0", - "result": { - "content": [ - {"type": "text", "text": json.dumps(result, indent=2)}, - ], - }, - "id": request_id, - }) - - -async def handle_autostart_health_check(arguments: dict, request_id: Any) -> web.Response: - """Handle autostart_health_check tool.""" - check_only = arguments.get("check_only", False) - from app.skills.builtin.skill_autostart import run_health_check - result = run_health_check() - return web.json_response({ - "jsonrpc": "2.0", - "result": { - "content": [ - {"type": "text", "text": json.dumps(result, indent=2)}, - ], - }, - "id": request_id, - }) - - -async def handle_toon_encode(arguments: dict, request_id: Any) -> web.Response: - """Handle toon_encode tool.""" - from app.api.toon import handle_toon_encode - from app.skills.shared_utils import create_mock_request - mock_request = create_mock_request(arguments) - response = await handle_toon_encode(mock_request) - response_data = await response.json() - return web.json_response({ - "jsonrpc": "2.0", - "result": { - "content": [ - {"type": "text", "text": json.dumps(response_data, indent=2)}, - ], - }, - "id": request_id, - }) - - -async def handle_toon_stats(arguments: dict, request_id: Any) -> web.Response: - """Handle toon_stats tool.""" - from app.api.toon import handle_toon_stats - from app.skills.shared_utils import create_mock_request - mock_request = create_mock_request({}) - response = await handle_toon_stats(mock_request) - response_data = await response.json() - return web.json_response({ - "jsonrpc": "2.0", - "result": { - "content": [ - {"type": "text", "text": json.dumps(response_data, indent=2)}, - ], - }, - "id": request_id, - }) - - -async def handle_toon_clear(arguments: dict, request_id: Any) -> web.Response: - """Handle toon_clear tool.""" - from app.api.toon import handle_toon_clear - from app.skills.shared_utils import create_mock_request - mock_request = create_mock_request({}) - response = await handle_toon_clear(mock_request) - response_data = await response.json() - return web.json_response({ - "jsonrpc": "2.0", - "result": { - "content": [ - {"type": "text", "text": json.dumps(response_data, indent=2)}, - ], - }, - "id": request_id, - }) - - -async def handle_flow_router_route(arguments: dict, request_id: Any) -> web.Response: - """Handle flow_router_route tool.""" - from app.api.flow_router import handle_flow_router_route - from app.skills.shared_utils import create_mock_request - mock_request = create_mock_request(arguments) - response = await handle_flow_router_route(mock_request) - response_data = await response.json() - return web.json_response({ - "jsonrpc": "2.0", - "result": { - "content": [ - {"type": "text", "text": json.dumps(response_data, indent=2)}, - ], - }, - "id": request_id, - }) - - -async def handle_gridsmith_tools_list(arguments: dict, request_id: Any) -> web.Response: - """Handle gridsmith_tools_list tool.""" - from app.api.mcp import get_gridsmith_bridge - - bridge = get_gridsmith_bridge() - payload = bridge.run("tools", "list") - return web.json_response({ - "jsonrpc": "2.0", - "result": { - "content": [ - {"type": "text", "text": json.dumps(payload, indent=2)}, - ], - }, - "id": request_id, - }) - - -async def handle_gridsmith_create_grid(arguments: dict, request_id: Any) -> web.Response: - """Handle gridsmith_create_grid tool.""" - from app.api.mcp import get_gridsmith_bridge - - bridge = get_gridsmith_bridge() - payload = bridge.run( - "grid", "create", - "--cols", str(int(arguments.get("cols", 80))), - "--rows", str(int(arguments.get("rows", 24))), - ) - return web.json_response({ - "jsonrpc": "2.0", - "result": { - "content": [ - {"type": "text", "text": json.dumps(payload, indent=2)}, - ], - }, - "id": request_id, - }) - - -async def handle_gridsmith_latlon_to_ucode(arguments: dict, request_id: Any) -> web.Response: - """Handle gridsmith_latlon_to_ucode tool.""" - from app.api.mcp import get_gridsmith_bridge - - bridge = get_gridsmith_bridge() - payload = bridge.run( - "location", "latlon-to-ucode", - "--lat", str(arguments.get("lat")), - "--lon", str(arguments.get("lon")), - "--level", str(int(arguments.get("level", 340))), - ) - return web.json_response({ - "jsonrpc": "2.0", - "result": { - "content": [ - {"type": "text", "text": json.dumps(payload, indent=2)}, - ], - }, - "id": request_id, - }) - - -async def handle_gridsmith_ucode_to_latlon(arguments: dict, request_id: Any) -> web.Response: - """Handle gridsmith_ucode_to_latlon tool.""" - from app.api.mcp import get_gridsmith_bridge - - bridge = get_gridsmith_bridge() - payload = bridge.run( - "location", "ucode-to-latlon", - "--coord", str(arguments.get("coord", "")), - ) - return web.json_response({ - "jsonrpc": "2.0", - "result": { - "content": [ - {"type": "text", "text": json.dumps(payload, indent=2)}, - ], - }, - "id": request_id, - }) - - -async def handle_gridsmith_import_basic_program(arguments: dict, request_id: Any) -> web.Response: - """Handle gridsmith_import_basic_program tool.""" - from app.api.mcp import get_gridsmith_bridge - - bridge = get_gridsmith_bridge() - payload = bridge.run( - "world", "import-basic", - "--program", str(arguments.get("program", "")), - "--world-name", str(arguments.get("world_name", "")), - ) - return web.json_response({ - "jsonrpc": "2.0", - "result": { - "content": [ - {"type": "text", "text": json.dumps(payload, indent=2)}, - ], - }, - "id": request_id, - }) - - -async def handle_tasker_sync_export(arguments: dict, request_id: Any) -> web.Response: - """Handle tasker_sync_export tool.""" - from app.api.mcp import get_skill - - skill = get_skill("tasker_sync") - if not skill: - return web.json_response({ - "jsonrpc": "2.0", - "error": { - "code": -32601, - "message": "Tool 'tasker_sync_export' not available", - }, - "id": request_id, - }, status=404) - - result = await skill.run(**arguments) - return web.json_response({ - "jsonrpc": "2.0", - "result": { - "content": [ - {"type": "text", "text": json.dumps(result, indent=2)}, - ], - }, - "id": request_id, - }) - - -# Tool handler registry -TOOL_HANDLERS: dict[str, Any] = { - "knowledge_search": handle_knowledge_search, - "knowledge_list_workspaces": handle_knowledge_list_workspaces, - "knowledge_list_documents": handle_knowledge_list_documents, - "clipboard_capture": handle_clipboard_capture, - "clipboard_get": handle_clipboard_get, - "clipboard_delete": handle_clipboard_delete, - "tasker_list_boards": handle_tasker_list_boards, - "tasker_read_task": handle_tasker_read_task, - "tasker_write_task": handle_tasker_write_task, - "tasker_sync_export": handle_tasker_sync_export, - "flow_router_route": handle_flow_router_route, - "flow_router_analytics": handle_flow_router_analytics, - "flow_router_history": handle_flow_router_history, - "gridsmith_tools_list": handle_gridsmith_tools_list, - "gridsmith_create_grid": handle_gridsmith_create_grid, - "gridsmith_latlon_to_ucode": handle_gridsmith_latlon_to_ucode, - "gridsmith_ucode_to_latlon": handle_gridsmith_ucode_to_latlon, - "gridsmith_import_basic_program": handle_gridsmith_import_basic_program, - "toon_encode": handle_toon_encode, - "toon_stats": handle_toon_stats, - "toon_clear": handle_toon_clear, - "autostart_health_check": handle_autostart_health_check, -} - - -async def dispatch_tool(body: Dict[str, Any]) -> web.Response: - """Dispatch an MCP request to the appropriate handler. - - This mirrors the original ``handle_mcp_call`` logic but delegates to the - individual handler functions defined above. - """ - tool_name = body.get("name") or body.get("tool") or body.get("tool_name") or "" - arguments = body.get("arguments") or body.get("params") or body.get("input") or {} - request_id = body.get("id") - - log.info("[MCP call] tool=%r args_keys=%s", tool_name, list(arguments.keys()) if arguments else []) - - if tool_name.startswith("skill_"): - return await handle_skill_tool(tool_name, arguments, request_id) - - handler = TOOL_HANDLERS.get(tool_name) - if handler: - return await handler(arguments, request_id) - - return web.json_response({ - "jsonrpc": "2.0", - "error": {"code": -32601, "message": f"Tool '{tool_name}' not found"}, - "id": request_id, - }, status=404) \ No newline at end of file diff --git a/backend/app/api/chat.py b/backend/app/api/chat.py index a7fb9039..224815c0 100644 --- a/backend/app/api/chat.py +++ b/backend/app/api/chat.py @@ -313,12 +313,10 @@ async def _execute_tool(tool_name: str, arguments: dict) -> str: except (ImportError, AttributeError): return json.dumps({"skills": [ {"id": "ecosystem-audit", "name": "Ecosystem Audit", "category": "system"}, - {"id": "dev-mode-executor", "name": "Dev Mode Executor", "category": "developer"}, - {"id": "file-edit-enhancer", "name": "File Edit Enhancer", "category": "tools"}, - {"id": "tasker-ingest", "name": "Tasker Ingest", "category": "workflow"}, - {"id": "vault-discovery", "name": "Vault Discovery", "category": "vault"}, - {"id": "skill-docs-roundup", "name": "Docs Roundup", "category": "documentation"}, - ], "total": 6, "source": "samples"}) + {"id": "route_task", "name": "Route Task", "category": "assist"}, + {"id": "vault_discovery", "name": "Vault Discovery", "category": "knowledge"}, + {"id": "workflow_audit", "name": "Workflow Audit", "category": "workflow"}, + ], "total": 3, "source": "samples"}) if tool_name == "system_health": return json.dumps({ diff --git a/backend/app/api/developer_api.py b/backend/app/api/developer_api.py index f01eeee5..aedafab8 100644 --- a/backend/app/api/developer_api.py +++ b/backend/app/api/developer_api.py @@ -840,8 +840,7 @@ async def handle_workspace_switch(request: web.Request) -> web.Response: Called by the frontend when the user switches between System and Project lanes and/or changes the selected project repository. The workspace path is stored in-memory so - that subsequent skill executions (dev-mode-executor, file-edit, etc.) - can operate on the correct codebase. + that subsequent governed developer operations can use the correct codebase. """ try: body = await request.json() diff --git a/backend/app/api/mcp.py b/backend/app/api/mcp.py index a3f0c497..b1d3554f 100644 --- a/backend/app/api/mcp.py +++ b/backend/app/api/mcp.py @@ -377,24 +377,6 @@ async def handle_mcp_discover(request: web.Request) -> web.Response: } ) - # ── Autostart Health Check ─────────────────────────────── - tools.append( - { - "name": "autostart_health_check", - "description": "Check and auto-start snackbar services (backend, menu, MCP)", - "input_schema": { - "type": "object", - "properties": { - "check_only": { - "type": "boolean", - "description": "Only check status, don't auto-start", - "default": False, - }, - }, - }, - } - ) - return web.json_response( { "jsonrpc": "2.0", diff --git a/backend/app/api/mcp_guardrails.py b/backend/app/api/mcp_guardrails.py index a0d4aaaf..1d53b090 100644 --- a/backend/app/api/mcp_guardrails.py +++ b/backend/app/api/mcp_guardrails.py @@ -54,7 +54,6 @@ "handle_toon_encode": ["arguments", "request_id"], "handle_toon_stats": ["arguments", "request_id"], "handle_toon_clear": ["arguments", "request_id"], - "handle_autostart_health_check": ["arguments", "request_id"], } # Domain modules that make up the mcp_handlers package @@ -65,7 +64,6 @@ "gridsmith", "flow_router", "toon", - "autostart", "skill", ] @@ -91,7 +89,6 @@ "toon_encode", "toon_stats", "toon_clear", - "autostart_health_check", } diff --git a/backend/app/api/mcp_handlers/__init__.py b/backend/app/api/mcp_handlers/__init__.py index 4c3d789f..9c9bdc4a 100644 --- a/backend/app/api/mcp_handlers/__init__.py +++ b/backend/app/api/mcp_handlers/__init__.py @@ -15,9 +15,6 @@ from aiohttp import web -from app.api.mcp_handlers.autostart import ( - handle_autostart_health_check, -) from app.api.mcp_handlers.clipboard import ( handle_clipboard_capture, handle_clipboard_delete, @@ -82,7 +79,6 @@ "toon_encode": handle_toon_encode, "toon_stats": handle_toon_stats, "toon_clear": handle_toon_clear, - "autostart_health_check": handle_autostart_health_check, } diff --git a/backend/app/api/mcp_handlers/autostart.py b/backend/app/api/mcp_handlers/autostart.py deleted file mode 100644 index 402c8c42..00000000 --- a/backend/app/api/mcp_handlers/autostart.py +++ /dev/null @@ -1,24 +0,0 @@ -"""MCP handlers: Autostart domain (health check).""" -from __future__ import annotations - -import json -import logging -from typing import Any - -from aiohttp import web - -log = logging.getLogger("ucore.api.mcp_handlers.autostart") - - -async def handle_autostart_health_check(arguments: dict, request_id: Any) -> web.Response: - """Handle autostart_health_check tool.""" - from app.skills.builtin.skill_autostart import run_health_check - - result = run_health_check() - return web.json_response({ - "jsonrpc": "2.0", - "result": { - "content": [{"type": "text", "text": json.dumps(result, indent=2)}], - }, - "id": request_id, - }) diff --git a/backend/app/skills/builtin/skill_autostart.py b/backend/app/skills/builtin/skill_autostart.py deleted file mode 100644 index 004b2369..00000000 --- a/backend/app/skills/builtin/skill_autostart.py +++ /dev/null @@ -1,269 +0,0 @@ -"""Auto-Start Skill — Health check and auto-start for snackbar services. - -Provides: -- Check if snackbar backend is running -- Check if menu bar app is running -- Auto-start services if not running -- Integration with health API and MCP -""" - -from __future__ import annotations - -import json -import logging -import os -import subprocess -import time -import urllib.request -from pathlib import Path -from typing import Any - -from app.core.settings import settings -from app.skills.base import BaseSkill, SkillMeta - -log = logging.getLogger("skill_autostart") - -UCORE_URL = "http://127.0.0.1:8484" -UCORE_BACKEND_DIR = os.environ.get( - "UCORE_BACKEND_DIR", - str(settings.udos_root / "uCore" / "backend"), -) -UCORE_MENU_LABEL = "com.udos.ucore-menu" -UCORE_SERVER_LABEL = "com.udos.ucore-server" - - -def _api_get(path: str, timeout: float = 3.0) -> dict | None: - """Call snackbar API and return parsed JSON, or None.""" - try: - req = urllib.request.Request(f"{UCORE_URL}{path}") - with urllib.request.urlopen(req, timeout=timeout) as resp: - return json.loads(resp.read().decode("utf-8")) - except Exception: - return None - - -def _api_post_json(path: str, payload: dict | None = None, timeout: float = 6.0) -> dict | None: - """POST JSON to snackbar API and return parsed JSON, or None.""" - try: - body = json.dumps(payload or {}).encode("utf-8") - req = urllib.request.Request( - f"{UCORE_URL}{path}", - data=body, - headers={"Content-Type": "application/json"}, - method="POST", - ) - with urllib.request.urlopen(req, timeout=timeout) as resp: - return json.loads(resp.read().decode("utf-8")) - except Exception: - return None - - -def is_backend_alive() -> bool: - """Check if snackbar backend is responding.""" - result = _api_get("/api/health") - return result is not None and result.get("status") == "ok" - - -def is_menu_running() -> bool: - """Check if uCore menu is running.""" - lockfile = settings.udos_home / "ucore-menu.pid" - if not lockfile.exists(): - return False - try: - pid = int(lockfile.read_text().strip()) - os.kill(pid, 0) - return True - except (ProcessLookupError, ValueError): - return False - - -def is_menu_installed() -> bool: - """Check if menu launchd plist is installed.""" - plist = f"{UCORE_MENU_LABEL}.plist" - plist_path = Path.home() / "Library/LaunchAgents" / plist - return os.path.exists(plist_path) - - -def is_server_installed() -> bool: - """Check if server launchd plist is installed.""" - plist = f"{UCORE_SERVER_LABEL}.plist" - plist_path = Path.home() / "Library/LaunchAgents" / plist - return os.path.exists(plist_path) - - -def vault_layers_available() -> dict[str, bool]: - """Check which vault layers exist on disk.""" - home = Path.home() - return { - "user": (home / "Vault").is_dir(), - "shared": (home / "Shared").is_dir(), - "public": (home / "Public").is_dir(), - } - - -def start_backend() -> bool: - """Start the snackbar backend.""" - try: - venv_python = Path(UCORE_BACKEND_DIR) / ".venv" / "bin" / "python" - python_bin = str(venv_python) if venv_python.exists() else "/usr/bin/python3" - - subprocess.Popen( - [python_bin, "-m", "app", "--port", "8484"], - cwd=UCORE_BACKEND_DIR, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - start_new_session=True, - ) - log.info("Backend start initiated") - return True - except Exception as e: - log.error(f"Failed to start backend: {e}") - return False - - -def start_menu() -> bool: - """Start the uCore menu bar app.""" - try: - venv_python = Path(UCORE_BACKEND_DIR) / ".venv" / "bin" / "python" - python_bin = str(venv_python) if venv_python.exists() else "/usr/bin/python3" - - subprocess.Popen( - [python_bin, "-m", "app.menu.unified_menu_simple"], - cwd=UCORE_BACKEND_DIR, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - start_new_session=True, - ) - log.info("Menu start initiated") - return True - except Exception as e: - log.error(f"Failed to start menu: {e}") - return False - - -def check_and_start_services() -> dict[str, Any]: - """Check all services and start if needed.""" - results = { - "backend": {"running": False, "started": False}, - "menu": {"running": False, "started": False}, - "server_plist": {"installed": False}, - "menu_plist": {"installed": False}, - } - - # Check backend - results["backend"]["running"] = is_backend_alive() - if not results["backend"]["running"]: - results["backend"]["started"] = start_backend() - time.sleep(2) - results["backend"]["running"] = is_backend_alive() - - # Check menu - results["menu"]["running"] = is_menu_running() - if not results["menu"]["running"]: - results["menu"]["started"] = start_menu() - time.sleep(1) - results["menu"]["running"] = is_menu_running() - - # Check plists - results["server_plist"]["installed"] = is_server_installed() - results["menu_plist"]["installed"] = is_menu_installed() - - # Check vault layers - results["vaults"] = vault_layers_available() - - return results - - -def run_health_check() -> dict[str, Any]: - """Run comprehensive health check for auto-start services.""" - services = check_and_start_services() - - # Get detailed health from API if available - health = _api_get("/api/health") - if health: - services["health_api"] = health - - # Get MCP status - mcp_status = _api_get("/api/mcp/tools") - if mcp_status: - tools_count = len(mcp_status.get("tools", [])) - services["mcp"] = {"available": True, "tools": tools_count} - else: - services["mcp"] = {"available": False} - - # Determine overall status - all_healthy = ( - services["backend"]["running"] - and services["menu"]["running"] - and services["server_plist"]["installed"] - and services["menu_plist"]["installed"] - ) - - return { - "success": True, - "action": "autostart_health_check", - "status": "ok" if all_healthy else "degraded", - "services": services, - "recommendations": _get_recommendations(services), - } - - -def _get_recommendations(services: dict) -> list[str]: - """Generate recommendations based on service status.""" - recs = [] - - if not services["server_plist"]["installed"]: - recs.append( - "Install server plist: bash scripts/install_ucore_menu_launchd.sh --install-server" - ) - - if not services["menu_plist"]["installed"]: - recs.append("Install menu plist: bash scripts/install_ucore_menu_launchd.sh") - - backend_ok = services["backend"]["running"] or services["backend"]["started"] - if not backend_ok: - recs.append("Backend failed to start - check logs") - - menu_ok = services["menu"]["running"] or services["menu"]["started"] - if not menu_ok: - recs.append("Menu failed to start - check logs") - - # Vault layer recommendations - vaults = services.get("vaults", {}) - missing = [name for name, exists in vaults.items() if not exists] - if missing: - recs.append( - "Create missing vault directories: " - + ", ".join(f"~/{name.title()}" for name in missing) - ) - - return recs - - -# ─── Skill Class ───────────────────────────────────────────────────── - - -class AutoStartSkill(BaseSkill): - """Skill for auto-start health checking and service management.""" - - meta = SkillMeta( - id="autostart", - name="Auto-Start Health Check", - description="Check and auto-start snackbar services", - category="system", - timeout=30, - requires_confirmation=False, - ) - - def validate(self, **kwargs) -> list[str]: - return [] - - async def run(self, **kwargs) -> dict[str, Any]: - return run_health_check() - - -if __name__ == "__main__": - import asyncio - - result = asyncio.run(AutoStartSkill().run()) - print(json.dumps(result, indent=2, default=str)) diff --git a/backend/app/skills/builtin/skill_dev_destroy_rebuild.py b/backend/app/skills/builtin/skill_dev_destroy_rebuild.py deleted file mode 100644 index 36fcf3d3..00000000 --- a/backend/app/skills/builtin/skill_dev_destroy_rebuild.py +++ /dev/null @@ -1,470 +0,0 @@ -"""DESTROY/REBUILD — Dev Mode recovery command. - -Safely destroys and rebuilds Dev Mode components using template -snapshots managed by TemplateManager (hivemind.008). - -Workflow: -1. Backup current state → SPOOL as recovery point -2. DESTROY: remove component state files, reset surfaces -3. REBUILD: restore from template (default or specified) - -Safety: never touches data, only Dev Mode config/state. -""" - -from __future__ import annotations - -import json -import logging -import shutil -from dataclasses import dataclass -from datetime import datetime, timezone -from pathlib import Path -from typing import Any - -from app.core.settings import settings -from app.services.template_manager import get_template_manager -from app.skills.base import BaseSkill, SkillMeta, SkillParam - -log = logging.getLogger("ucore.skills.destroy_rebuild") - -RECOVERY_DIR = settings.udos_home / "recovery" -SPOOL_DIR = settings.udos_home / "spool" -WISDOM_DIR = SPOOL_DIR / "wisdom" - -# Components that can be destroyed/rebuild -RESETTABLE_COMPONENTS = { - "dev_layer": { - "path": settings.udos_home / "config.yaml", - "description": "Dev Mode layer state (mode, surface visibility)", - }, - "templates": { - "path": settings.udos_home / "templates", - "description": "All template snapshots (default, stable, exp, custom)", - }, - "dev_mode_store": { - "path": None, # In-memory / Vue store; reset via API - "description": "Frontend devMode.ts store state", - }, - "spool_wisdom": { - "path": WISDOM_DIR, - "description": "Wisdom records in $UDOS_HOME/spool/wisdom", - }, -} - - -@dataclass -class RecoveryPoint: - """A backup of component state before destruction.""" - - id: str - timestamp: str - components: list[str] - path: Path - - def to_dict(self) -> dict: - return { - "id": self.id, - "timestamp": self.timestamp, - "components": self.components, - "path": str(self.path), - } - - -class DestroyRebuildSkill(BaseSkill): - """DESTROY/REBUILD — Component-level Dev Mode recovery.""" - - meta = SkillMeta( - id="dev-destroy-rebuild", - name="Dev Destroy Rebuild", - description=( - "Safely destroy and rebuild Dev Mode components using " - "template snapshots with full SPOOL preservation." - ), - category="system", - timeout=120, - requires_confirmation=True, - params=[ - SkillParam( - name="action", - type="string", - description="Action: 'destroy', 'rebuild', 'list'", - required=True, - ), - SkillParam( - name="components", - type="string", - description="Comma-separated component list (default: all)", - required=False, - ), - SkillParam( - name="backup_id", - type="string", - description="Recovery point ID for rebuild", - required=False, - ), - SkillParam( - name="template_id", - type="string", - description="Template ID for rebuild", - required=False, - ), - SkillParam( - name="dry_run", - type="boolean", - description="Preview without executing", - required=False, - default=False, - ), - ], - ) - - def __init__(self): - self._template_mgr = get_template_manager() - self._recovery: list[RecoveryPoint] = [] - self._load_recovery_points() - - async def run(self, **kwargs) -> dict[str, Any]: - """Execute destroy/rebuild based on action parameter.""" - action = kwargs.get("action", "").strip().lower() - if action == "list": - return {"recovery_points": self.list_recovery_points()} - if action == "destroy": - components = kwargs.get("components") - if components: - components = [c.strip() for c in components.split(",")] - return self.destroy( - components=components, - template_id=kwargs.get("template_id"), - dry_run=kwargs.get("dry_run", False), - ) - if action == "rebuild": - components = kwargs.get("components") - if components: - components = [c.strip() for c in components.split(",")] - return self.rebuild( - backup_id=kwargs.get("backup_id"), - template_id=kwargs.get("template_id"), - components=components, - ) - return {"error": f"Unknown action: {action}. Use 'destroy', 'rebuild', or 'list'."} - - # ── DESTROY ────────────────────────────────────────────── - - def destroy( - self, - components: list[str] | None = None, - template_id: str | None = None, - dry_run: bool = False, - ) -> dict[str, Any]: - """Destroy specified components after backup. - - Args: - components: List of component IDs to destroy. - None = all resettable components. - template_id: Save a template before destruction. - dry_run: If True, only report what would be done. - - Returns: - Dict with backup_id, destroyed_components, template_id - """ - targets = components or list(RESETTABLE_COMPONENTS.keys()) - unknown = [c for c in targets if c not in RESETTABLE_COMPONENTS] - if unknown: - return { - "error": f"Unknown components: {unknown}", - "known": list(RESETTABLE_COMPONENTS.keys()), - } - - # 1. Backup - backup = self._backup(targets) - if backup: - log.info("Backup created: %s", backup.id) - - # 2. Template snapshot - saved_template = None - if template_id or dry_run: - template_name = template_id or f"pre-destroy-{_now_slug()}" - try: - saved_template = self._template_mgr.create_template( - name=template_name, - tier="custom", - description=(f"Auto-saved before DESTROY of {', '.join(targets)}"), - tags=["auto-save", "pre-destroy", *targets], - state=self._capture_state(targets), - ) - log.info("Template saved: %s", saved_template.id) - except Exception as exc: - log.warning("Template save failed: %s", exc) - - if dry_run: - return { - "dry_run": True, - "would_destroy": targets, - "would_backup": backup is not None, - "would_save_template": saved_template is not None, - } - - # 3. Execute destruction - destroyed = [] - for component in targets: - if self._destroy_component(component): - destroyed.append(component) - - return { - "backup_id": backup.id if backup else None, - "destroyed": destroyed, - "template_id": saved_template.id if saved_template else None, - "message": ( - f"Destroyed {len(destroyed)} component(s). Use 'rebuild {backup.id}' to restore." - ), - } - - def _destroy_component(self, component: str) -> bool: - """Remove component state files.""" - info = RESETTABLE_COMPONENTS[component] - path = info["path"] - if path is None: - # In-memory component; just report success - log.info("Component '%s' is in-memory — reset via API", component) - return True - if not path.exists(): - log.info("Component '%s' has no state to destroy", component) - return True - try: - if path.is_dir(): - shutil.rmtree(path) - else: - path.unlink() - log.info("Destroyed %s: %s", component, path) - return True - except OSError as exc: - log.error("Failed to destroy %s: %s", component, exc) - return False - - # ── REBUILD ────────────────────────────────────────────── - - def rebuild( - self, - backup_id: str | None = None, - template_id: str | None = None, - components: list[str] | None = None, - ) -> dict[str, Any]: - """Rebuild components from backup or template. - - Args: - backup_id: Restore from a specific recovery point. - template_id: Rebuild from a template snapshot. - components: Subset of components to restore (default: all). - - Priority: backup > template > default - """ - if backup_id: - return self._rebuild_from_backup(backup_id, components) - if template_id: - return self._rebuild_from_template(template_id, components) - return self._rebuild_defaults(components) - - def _rebuild_from_backup(self, backup_id: str, components: list[str] | None) -> dict[str, Any]: - """Restore from a specific recovery point.""" - rp = self._find_recovery(backup_id) - if not rp: - return {"error": f"Recovery point '{backup_id}' not found"} - if not rp.path.exists(): - return {"error": f"Recovery point path missing: {rp.path}"} - - restored = [] - targets = components or rp.components - for comp in targets: - if comp not in RESETTABLE_COMPONENTS: - continue - comp_path = RESETTABLE_COMPONENTS[comp]["path"] - backup_comp = rp.path / comp - if comp_path and backup_comp.exists(): - comp_path.parent.mkdir(parents=True, exist_ok=True) - if backup_comp.is_dir(): - shutil.copytree(backup_comp, comp_path, dirs_exist_ok=True) - else: - shutil.copy2(backup_comp, comp_path) - restored.append(comp) - log.info("Restored %s from backup %s", comp, backup_id) - - return { - "restored_from": "backup", - "backup_id": backup_id, - "restored": restored, - } - - def _rebuild_from_template( - self, template_id: str, components: list[str] | None - ) -> dict[str, Any]: - """Rebuild from a template snapshot.""" - tm = get_template_manager() - tp = tm.get_path(template_id) - if not tp: - return {"error": f"Template '{template_id}' not found"} - - state = tm._load_state(tp) # noqa: protected-access - if not state: - return {"error": f"Template '{template_id}' has no state"} - - log.info("Rebuilding from template %s", template_id) - return { - "restored_from": "template", - "template_id": template_id, - "state": state, - } - - def _rebuild_defaults(self, components: list[str] | None) -> dict[str, Any]: - """Reset components to defaults.""" - targets = components or list(RESETTABLE_COMPONENTS.keys()) - log.info("Resetting %d components to defaults", len(targets)) - return { - "restored_from": "defaults", - "reset": targets, - "message": ( - f"Reset {len(targets)} component(s) to defaults. Restart surfaces to apply." - ), - } - - # ── Backup ─────────────────────────────────────────────── - - def _backup(self, components: list[str]) -> RecoveryPoint | None: - """Backup component state before destruction.""" - now = datetime.now(timezone.utc) - rp_id = f"recovery-{now.strftime('%Y%m%d-%H%M%S')}" - rp_path = RECOVERY_DIR / rp_id - rp_path.mkdir(parents=True, exist_ok=True) - - backed_up = [] - for comp in components: - info = RESETTABLE_COMPONENTS[comp] - path = info["path"] - if path is None or not path.exists(): - continue - dest = rp_path / comp - try: - if path.is_dir(): - shutil.copytree(path, dest) - else: - shutil.copy2(path, dest) - backed_up.append(comp) - except OSError as exc: - log.warning("Backup failed for %s: %s", comp, exc) - - if not backed_up: - shutil.rmtree(rp_path, ignore_errors=True) - return None - - rp = RecoveryPoint( - id=rp_id, - timestamp=now.isoformat(), - components=backed_up, - path=rp_path, - ) - self._recovery.append(rp) - self._save_recovery_point(rp) - return rp - - def _capture_state(self, components: list[str]) -> dict: - """Capture current Dev Mode state for template snapshot.""" - state: dict[str, Any] = { - "captured_at": datetime.now(timezone.utc).isoformat(), - "components": {}, - } - for comp in components: - info = RESETTABLE_COMPONENTS[comp] - path = info["path"] - if path and path.exists(): - try: - if path.is_file(): - state["components"][comp] = { - "path": str(path), - "exists": True, - "type": "file", - } - elif path.is_dir(): - state["components"][comp] = { - "path": str(path), - "exists": True, - "type": "directory", - "entries": [str(p.relative_to(path)) for p in path.rglob("*")], - } - except Exception: - state["components"][comp] = {"path": str(path), "error": "read failed"} - else: - state["components"][comp] = {"path": str(path) if path else "in-memory"} - return state - - # ── Recovery Point Management ──────────────────────────── - - def list_recovery_points(self) -> list[dict]: - return [rp.to_dict() for rp in self._recovery] - - def _find_recovery(self, rp_id: str) -> RecoveryPoint | None: - for rp in self._recovery: - if rp.id == rp_id: - return rp - # Check disk - rp_path = RECOVERY_DIR / rp_id - if rp_path.is_dir(): - manifest_file = rp_path / "manifest.json" - if manifest_file.exists(): - try: - data = json.loads(manifest_file.read_text("utf-8")) - rp = RecoveryPoint( - id=data.get("id", rp_id), - timestamp=data.get("timestamp", ""), - components=data.get("components", []), - path=rp_path, - ) - self._recovery.append(rp) - return rp - except (json.JSONDecodeError, KeyError): - pass - return None - - def _save_recovery_point(self, rp: RecoveryPoint) -> None: - manifest_file = rp.path / "manifest.json" - manifest_file.write_text(json.dumps(rp.to_dict(), indent=2), "utf-8") - - def _load_recovery_points(self) -> None: - """Scan recovery directory for existing points.""" - if not RECOVERY_DIR.is_dir(): - return - for entry in sorted(RECOVERY_DIR.iterdir()): - if entry.is_dir(): - manifest_file = entry / "manifest.json" - if manifest_file.exists(): - try: - data = json.loads(manifest_file.read_text("utf-8")) - self._recovery.append( - RecoveryPoint( - id=data.get("id", entry.name), - timestamp=data.get("timestamp", ""), - components=data.get("components", []), - path=entry, - ) - ) - except (json.JSONDecodeError, KeyError): - pass - - -# ─── Singleton ────────────────────────────────────────────── - -_skill: DestroyRebuildSkill | None = None - - -def get_destroy_rebuild_skill() -> DestroyRebuildSkill: - global _skill - if _skill is None: - _skill = DestroyRebuildSkill() - return _skill - - -def reset_skill() -> None: - global _skill - _skill = None - - -def _now_slug() -> str: - return datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S") diff --git a/backend/app/skills/builtin/skill_dev_mode_executor.py b/backend/app/skills/builtin/skill_dev_mode_executor.py deleted file mode 100644 index 6b586684..00000000 --- a/backend/app/skills/builtin/skill_dev_mode_executor.py +++ /dev/null @@ -1,402 +0,0 @@ -"""Dev Mode Executor — unified agentic orchestration pipeline. - -The flagship Dev Mode skill that chains the entire agentic workflow: - 1. Analyze task complexity (route_task) - 2. Select specialized agent (agents.yaml routing matrix) - 3. (Optional) Multi-model deliberation (hivemind-consensus) - 4. Execute via the governed router, Roundtable, or Hivemind - 5. Review results (reviewer agent) - 6. Log to spool + update uFlow - -Usage: - POST /api/skills/dev-mode-executor/run - Body: {"task_uid": "task.auth.001", "use_consensus": true} - -Integrates with: route_task, hivemind-consensus, roundtable-dispatch, - route_task, task API, spool. -""" -from __future__ import annotations - -import json -import logging -import time -import urllib.request -from pathlib import Path - -from uflow.task_store import default_tasker_dir - -from app.skills.base import BaseSkill, SkillMeta, SkillParam - -log = logging.getLogger("ucore.skills.dev_mode_executor") - -AGENT_ROUTING = { - "design": "architect", - "implement": "dev", - "review": "reviewer", - "debug": "debugger", - "document": "docgen", - "worldbuild": "gridsmith-dev", -} - -EXECUTOR_CHOICE = { - "implementation": "route_task", - "coding": "route_task", - "debugging": "route_task", - "testing": "route_task", - "architecture": "roundtable-dispatch", - "design": "hivemind-consensus", - "planning": "hivemind-consensus", - "documentation": "roundtable-dispatch", - "analysis": "hivemind-consensus", -} - - -class DevModeExecutorSkill(BaseSkill): - """Unified Dev Mode orchestration pipeline.""" - - meta = SkillMeta( - id="dev-mode-executor", - name="Dev Mode Executor", - description=( - "Unified agentic orchestration pipeline." - " Chains analyze → route → consensus → execute" - " → review → log." - ), - category="developer", - timeout=600, - params=[ - SkillParam( - name="task_uid", - type="string", - required=True, - description="Task UID from uFlow to execute", - ), - SkillParam( - name="use_consensus", - type="boolean", - required=False, - default=False, - description="Use Hivemind consensus before execution", - ), - SkillParam( - name="mode", - type="string", - required=False, - default="auto", - description=( - "Execution mode: 'auto' (select best)," - " 'route_task', 'roundtable', 'hivemind'" - ), - ), - SkillParam( - name="dry_run", - type="boolean", - required=False, - default=False, - description="Plan only, do not execute", - ), - ], - requires_confirmation=True, - ) - - async def run(self, **kwargs) -> dict: - task_uid = kwargs.get("task_uid", "").strip() - use_consensus = kwargs.get("use_consensus", False) - mode = kwargs.get("mode", "auto") - dry_run = kwargs.get("dry_run", False) - - if not task_uid: - return {"success": False, "error": "task_uid is required"} - - pipeline = { - "task_uid": task_uid, - "started_at": time.time(), - "stages": {}, - } - - # Stage 0: Create safety snapshot before execution - # Uses HistoryService to auto-commit current state via git, - # enabling rollback if the task causes problems. - if not dry_run: - try: - from app.services.history_service import get_history - history = get_history() - snapshot = history.create_snapshot( - f"Auto-snapshot before task {task_uid}", - ) - pipeline["stages"]["snapshot"] = snapshot - except Exception as exc: - log.warning("Pre-execution snapshot failed: %s", exc) - pipeline["stages"]["snapshot"] = { - "status": "skipped", "reason": str(exc), - } - - # Stage 1: Fetch task from uFlow - task_data = self._fetch_task(task_uid) - if not task_data: - return { - "success": False, - "error": f"Task not found: {task_uid}", - } - pipeline["task"] = task_data - pipeline["stages"]["fetch"] = { - "status": "success", - "task_title": task_data.get("title", ""), - } - - task_description = task_data.get("title", "") - task_body = task_data.get("description", "") - - # Stage 2: Analyze and route - routing = await self._analyze_route(task_description, task_body) - pipeline["stages"]["analyze"] = routing - - # Stage 3: Optional consensus - if use_consensus: - consensus = await self._run_consensus( - task_description, task_body, - ) - pipeline["stages"]["consensus"] = consensus - else: - pipeline["stages"]["consensus"] = {"status": "skipped"} - - # Stage 4: Execute - if dry_run: - pipeline["stages"]["execute"] = { - "status": "skipped", - "reason": "dry_run=True", - } - return { - "success": True, - "action": "dev-mode-executor", - "pipeline": pipeline, - "summary": "Dry run completed — no execution.", - } - - executor = self._select_executor(routing, mode) - execution = await self._execute( - executor, task_description, task_body, - ) - pipeline["stages"]["execute"] = { - "executor": executor, - "result": execution, - } - - # Stage 5: Log - self._log_to_spool(task_uid, pipeline) - self._update_tasker(task_uid, "in-progress" if not dry_run else "backlog") - - return { - "success": True, - "action": "dev-mode-executor", - "pipeline": pipeline, - "summary": ( - f"Task '{task_uid}' routed to {executor}" - + (" (consensus used)" if use_consensus else "") - ), - } - - # ─── Stage 1: Fetch Task ─────────────────────────────────── - - def _fetch_task(self, task_uid: str) -> dict | None: - """Fetch a task from uFlow's Markdown store.""" - td = self._find_tasker_dir() - if not td: - return None - - # Search for task file - for tf in td.rglob("*.md"): - if task_uid in tf.name or task_uid in tf.stem: - content = tf.read_text(encoding="utf-8", errors="replace") - title = tf.stem - desc = content[:500] - return { - "id": task_uid, - "title": title, - "description": desc, - "file": str(tf), - } - - # Try tasker API - try: - req = urllib.request.Request( - "http://localhost:8484/api/tasker/tasks", - method="GET", - ) - with urllib.request.urlopen(req, timeout=3) as resp: - data = json.loads(resp.read().decode()) - tasks = data if isinstance(data, list) else data.get("tasks", []) - for t in tasks: - if t.get("id") == task_uid or task_uid in str(t.get("title", "")): - return t - except Exception: - pass - - return None - - @staticmethod - def _find_tasker_dir() -> Path | None: - """Locate uFlow's canonical task directory.""" - path = default_tasker_dir() - return path if path.is_dir() else None - - # ─── Stage 2: Analyze & Route ────────────────────────────── - - async def _analyze_route( - self, title: str, body: str, - ) -> dict: - """Analyze task and determine routing.""" - combined = f"{title} {body}".lower() - - # Determine task type - task_type = "implement" # default - type_keywords = { - "design": ["architect", "design", "plan", "refactor", "restructure"], - "implement": ["implement", "build", "create", "add", "feature", "code"], - "review": ["review", "audit", "check", "validate", "verify"], - "debug": ["bug", "fix", "debug", "error", "crash", "trace"], - "document": ["document", "doc", "readme", "comment", "guide"], - } - for typ, keywords in type_keywords.items(): - if any(kw in combined for kw in keywords): - task_type = typ - break - - # Map to specialized agent - agent = AGENT_ROUTING.get(task_type, "dev") - executor = EXECUTOR_CHOICE.get(task_type, "route_task") - - # Estimate complexity - complex_signals = [ - "refactor", "architecture", "security", "distributed", - "migration", "concurrency", - ] - complexity = "complex" if any(s in combined for s in complex_signals) else "medium" - if len(combined) < 100 and task_type not in ("design", "review"): - complexity = "simple" - - return { - "task_type": task_type, - "agent": agent, - "executor": executor, - "complexity": complexity, - } - - # ─── Stage 3: Consensus ──────────────────────────────────── - - async def _run_consensus( - self, title: str, body: str, - ) -> dict: - """Run Hivemind consensus on the task.""" - try: - payload = json.dumps({ - "task": f"{title}\n\n{body}", - "mode": "weighted", - "rounds": 2, - }).encode("utf-8") - req = urllib.request.Request( - "http://localhost:8490/api/consensus", - data=payload, - headers={"Content-Type": "application/json"}, - method="POST", - ) - with urllib.request.urlopen(req, timeout=60) as resp: - return {"status": "success", "data": json.loads(resp.read().decode())} - except Exception as exc: - return {"status": "unavailable", "error": str(exc)} - - # ─── Stage 4: Execute ────────────────────────────────────── - - def _select_executor(self, routing: dict, mode: str) -> str: - """Select executor based on routing and user preference.""" - if mode != "auto": - return mode - return routing.get("executor", "route_task") - - async def _execute( - self, executor: str, title: str, body: str, - ) -> dict: - """Execute task through selected executor.""" - task = f"{title}\n\n{body}" if body else title - - if executor == "hivemind-consensus": - # Already handled above or use direct call - return await self._run_consensus(title, body) - - if executor == "roundtable-dispatch": - return await self._call_roundtable(task) - - # Fallback: route_task - return await self._call_route_task(task) - - async def _call_roundtable(self, task: str) -> dict: - """Call Roundtable dispatch.""" - try: - payload = json.dumps({ - "task": task, - "agents": "auto", - "mode": "parallel", - }).encode("utf-8") - req = urllib.request.Request( - "http://localhost:8490/api/hivemind/roundtable/dispatch", - data=payload, - headers={"Content-Type": "application/json"}, - method="POST", - ) - with urllib.request.urlopen(req, timeout=120) as resp: - return {"executor": "roundtable", "result": json.loads(resp.read().decode())} - except Exception as exc: - return {"executor": "roundtable", "error": str(exc)} - - async def _call_route_task(self, task: str) -> dict: - """Fallback: route through route_task skill.""" - return { - "executor": "route_task", - "recommendation": ( - "Task routed through cost-aware router." - " Use 'execute: true' for automatic execution." - ), - } - - # ─── Stage 5: Log ────────────────────────────────────────── - - def _log_to_spool(self, task_uid: str, pipeline: dict) -> None: - """Log execution to spool.""" - try: - payload = json.dumps({ - "type": "dev_mode_execution", - "task_uid": task_uid, - "stages": { - k: v.get("status", "unknown") - for k, v in pipeline.get("stages", {}).items() - }, - "duration_ms": round( - (time.time() - pipeline["started_at"]) * 1000 - ), - }).encode("utf-8") - req = urllib.request.Request( - "http://localhost:8484/api/spool/ingest", - data=payload, - headers={"Content-Type": "application/json"}, - method="POST", - ) - urllib.request.urlopen(req, timeout=2) - except Exception: - pass - - def _update_tasker(self, task_uid: str, status: str) -> None: - """Update task status through uFlow's API.""" - try: - payload = json.dumps({ - "task_id": task_uid, - "status": status, - }).encode("utf-8") - req = urllib.request.Request( - f"http://localhost:8484/api/workflows/task/{task_uid}", - data=payload, - headers={"Content-Type": "application/json"}, - method="PUT", - ) - urllib.request.urlopen(req, timeout=2) - except Exception: - pass diff --git a/backend/app/skills/catalogue.json b/backend/app/skills/catalogue.json index 13bf2084..5cfd0046 100644 --- a/backend/app/skills/catalogue.json +++ b/backend/app/skills/catalogue.json @@ -12,9 +12,6 @@ {"module": "lint_fix.py", "owner": "uCore", "lifecycle": "active", "risk": "write", "lane": "developer", "allowed_roots": ["Code"]}, {"module": "route_task.py", "owner": "uCore", "lifecycle": "review", "risk": "external", "lane": "assist", "allowed_roots": ["UDOS_HOME"]}, {"module": "skill_audit.py", "owner": "uCore", "lifecycle": "replace", "risk": "read", "lane": "system", "allowed_roots": ["Code"]}, - {"module": "skill_autostart.py", "owner": "uCore", "lifecycle": "remove", "risk": "write", "lane": "system", "allowed_roots": ["UDOS_HOME"]}, - {"module": "skill_dev_destroy_rebuild.py", "owner": "uCore", "lifecycle": "privileged", "risk": "destructive", "lane": "recovery", "allowed_roots": ["UDOS_HOME", "Code"]}, - {"module": "skill_dev_mode_executor.py", "owner": "uCore", "lifecycle": "replace", "risk": "external", "lane": "developer", "allowed_roots": ["Code", "UDOS_HOME"]}, {"module": "skill_ecosystem_audit.py", "owner": "uCore", "lifecycle": "replace", "risk": "read", "lane": "system", "allowed_roots": ["Code", "UDOS_HOME"]}, {"module": "skill_hivemind_consensus.py", "owner": "uCore", "lifecycle": "review", "risk": "external", "lane": "assist", "allowed_roots": ["UDOS_HOME"]}, {"module": "skill_mcp_self_heal.py", "owner": "uCore", "lifecycle": "merge", "risk": "write", "lane": "recovery", "allowed_roots": ["Code", "UDOS_HOME"]}, diff --git a/backend/tests/test_mcp_guardrails.py b/backend/tests/test_mcp_guardrails.py index ef181403..b08f6b94 100644 --- a/backend/tests/test_mcp_guardrails.py +++ b/backend/tests/test_mcp_guardrails.py @@ -55,7 +55,6 @@ def test_required_tools_are_complete(self): assert "gridsmith_create_grid" in REQUIRED_TOOL_NAMES assert "toon_encode" in REQUIRED_TOOL_NAMES assert "flow_router_route" in REQUIRED_TOOL_NAMES - assert "autostart_health_check" in REQUIRED_TOOL_NAMES class TestMCPSelfHealSkill: diff --git a/docs/FEATURE_SPEC_CONTROL_PANEL_AND_AGENTIC_EXECUTION.md b/docs/FEATURE_SPEC_CONTROL_PANEL_AND_AGENTIC_EXECUTION.md deleted file mode 100644 index 36d1a0b2..00000000 --- a/docs/FEATURE_SPEC_CONTROL_PANEL_AND_AGENTIC_EXECUTION.md +++ /dev/null @@ -1,75 +0,0 @@ -# Feature Spec: Control Panel & Agentic Execution - -**Version:** 1.0.0 -**Date:** 2026-07-02 -**Status:** Delivered - -## Overview - -Unified Control Panel tab for the Developer Surface replacing 11 scattered panels with a single dashboard, plus 10 new skills implementing a complete agentic execution pipeline for Dev Mode. - -## What Was Built - -### Backend Aggregation (2 files) -- `backend/app/services/control_service.py` — Aggregates all ecosystem health checks: Cline, OpenRouter, Hivemind, Roundtable, Ollama, Feed, Slate, Budget. Concurrent health probes with fallback paths. -- `backend/app/api/control_api.py` — `GET /api/control/status` endpoint serving unified payload. -- Route registered in `backend/app/api/routes.py`. - -### Control Panel UI (8 Vue files) -- `ControlPanel.vue` — Unified dashboard with status badges, live feed, agent cards, cost dashboard, active mission, bottom bar, quick actions. 60-second polling. -- 7 sub-components: `StatusBadges.vue`, `LiveFeedStream.vue`, `AgentStatusCard.vue`, `CostDashboard.vue`, `ActiveMission.vue`, `BottomBar.vue`, `QuickActions.vue`. - -### Post-Delivery Enhancements (2026-07) -- Lane selector renamed: `Ecosystem Dev` → `System`, `Project Dev` → `Project`. -- Project lane now supports repo selection from `~/Code` (excluding system repos) via second dropdown selector. -- Developer repo discovery now enforces a Code vs Vault boundary: Developer Surface lists code repos only and excludes vault/doc libraries (including mirrored doc-vault repos such as `global-knowledge`), which belong to Workflow/Missions. -- Added `POST /api/control/recover` for one-click offline-service recovery. -- Restart backend action now uses `POST /api/surfaces/popcorn/restart-backend`. - -### Lane A: Catalogue & Assess (4 skills) -- `skill_audit.py` — Rewritten as BaseSkill. Smoke-tests all 28+ builtin skills by importing, instantiating, and executing with dry_run. Classifies as working/untested/broken. -- `skill_ecosystem_audit.py` — Enhanced with `assess` action. Catalogues 173 items across 7 categories with health scoring. -- `skill_ucore_index.py` — Enhanced with `health-report` action. Cross-references audit reports with live service checks. -- `skill_enhancement_planner.py` — New. Bridges audit gaps to `.tasker` items, renders `ENHANCEMENT_PLAN.md`. - -### Lane B: Agentic Execution (6 skills) -- `skill_hivemind_consensus.py` — Multi-model deliberation via Hivemind (port 8490). 4 consensus modes. -- `skill_roundtable_dispatch.py` — Parallel specialized agent execution via Roundtable integration. -- `skill_cline_invoke.py` — Cline CLI bridge supporting yolo and interactive modes. -- `skill_dev_mode_executor.py` — Flagship 5-stage unified pipeline: fetch → analyze/route → consensus → execute → log. -- `skill_gh_workflow_bridge.py` — GitHub Actions/CLI bridge: trigger CI, create PRs, run workflows. -- `route_task.py` — Enhanced with `target_agent` parameter for specialized agent routing. - -### Existing Panels Wired to APIs (10 Vue files) -- ModelsPanel, AgentsPanel, MCPServersPanel, SkillsPanel, ReposPanel, ReviewPanel, KanbanPanel, WorkflowsPanel, SettingsPanel — all now call real backend endpoints instead of hardcoded arrays. -- ServerSurface + SystemSurface — all 13 tabs across both surfaces now call real APIs. - -### Ecosystem Audit Results -- 173 items catalogued across skills (41), MCP servers (1), runtimes (8), routes (97), paths (14), secrets (4), variables (8) -- Health: 172 working, 1 untested (dev-destroy-rebuild), 0 broken — 99.4% - -## Architecture - -``` -GET /api/control/status - ├── Status badges (Cline, OpenRouter, Hivemind, Roundtable, Ollama, Feed, Slate, Budget) - ├── Live feed stream (20 recent activities) - ├── Agent status (Hivemind consensus, Roundtable swarm, Cline session, Ollama models) - ├── Cost dashboard (daily/weekly/monthly budget + top models) - ├── Active mission (from .tasker) - ├── Bottom bar (Tasker, MCP servers, Slates) - ├── Alerts (budget warnings, feed backlog, offline services) - └── 60-second polling for live updates -``` - -## Agentic Execution Pipeline - -``` -User Task (.tasker) - → dev-mode-executor (B4) - → route_task (C1) — analyze & select agent - → hivemind-consensus (B1) — multi-model deliberation - → roundtable-dispatch (B2) — parallel agent execution - → cline-invoke (B3) — autonomous file/terminal ops - → gh-workflow-bridge (B5) — CI/CD integration - → Log to spool + update .tasker \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 651d3fe4..69d7f6f1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -50,9 +50,7 @@ | Doc | Purpose | | ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | -| [DEVMODE_CODE_ANALYSIS_SKILLS.md](DEVMODE_CODE_ANALYSIS_SKILLS.md) | DevMode code analysis skills | | [DEVELOPER_SURFACE.md](DEVELOPER_SURFACE.md) | Current Developer Surface UX, lanes, controls, and APIs | -| [FEATURE_SPEC_CONTROL_PANEL_AND_AGENTIC_EXECUTION.md](FEATURE_SPEC_CONTROL_PANEL_AND_AGENTIC_EXECUTION.md) | Control panel and agentic pipeline feature spec | | [specs/ECOSYSTEM_EXTENDED_DEV_FLOW_2026-07-31.md](specs/ECOSYSTEM_EXTENDED_DEV_FLOW_2026-07-31.md) | Extended ecosystem flow, repo ownership, and sprint plan | | [specs/AUTONOMOUS_DEV_ROUNDS_RUNBOOK.md](specs/AUTONOMOUS_DEV_ROUNDS_RUNBOOK.md) | Stable autonomous dev round lifecycle and hard gates | | [QQCODE_TUI_INTEGRATION.md](QQCODE_TUI_INTEGRATION.md) | QQCode TUI integration | diff --git a/docs/SKILLS_AUDIT_2026-08-18.md b/docs/SKILLS_AUDIT_2026-08-18.md index 7f0129cd..6f7df294 100644 --- a/docs/SKILLS_AUDIT_2026-08-18.md +++ b/docs/SKILLS_AUDIT_2026-08-18.md @@ -79,8 +79,9 @@ allowed roots, deterministic dry run where relevant and dedicated tests. ### Remove from general Skill execution -- `reset_database`, `spool_destroy` and `dev-destroy-rebuild`: privileged - recovery workflows, never ordinary agent-selected Skills. +- `reset_database` and `spool_destroy`: privileged recovery operations, never + ordinary agent-selected capabilities. The duplicate `dev-destroy-rebuild` + skill has been removed. - `autostart`: lifecycle/service management, owned by Snackbar/System. - `surface-registry` and `usx-standard`: build/registry tooling rather than user-selectable agent Skills. diff --git a/docs/SNACKS_SKILLS_STATUS_AUDIT.md b/docs/SNACKS_SKILLS_STATUS_AUDIT.md deleted file mode 100644 index 8a5cc612..00000000 --- a/docs/SNACKS_SKILLS_STATUS_AUDIT.md +++ /dev/null @@ -1,128 +0,0 @@ -# Snacks & Skills Status Audit - -**Generated:** 2026-06-30T20:53:00Z -**Scope:** All snack plugins and builtin skills in `backend/app/` - ---- - -## Snacks - -### Menu Snacks (`backend/app/menu/snacks/`) - -| Snack | File | Status | Notes | -|-------|------|--------|-------| -| **SystemSnack** | `system_snack.py` | ✅ Working | Health checks, service mgmt. Hardcoded paths to user's machine. | -| **SurfaceSnack** | `surface_snack.py` | ✅ Working | Surface navigation, auto-starts backend. Hardcoded paths. | -| **ClipboardSnack** | `clipboard_snack.py` | ✅ Working | Delegates to ObjC menu delegate. Requires macOS. | -| **OllamaSnack** | `ollama_snack.py` | ⚠️ Untested | Ollama model management. Needs Ollama running. | - -### API Snacks (`backend/app/snacks/`) - -Removed (2026-08-13): `dev_mode_snack.py`, `mission_ops_snack.py`, -`spool_monitor_snack.py` — dead modules with no importers; their behaviour is -covered by the Developer surface, the snack queue API, and SnackShack. - -### Snack Template - -| File | Status | Notes | -|------|--------|-------| -| `templates/snack_template.py` | ✅ Working | BaseSnack class with Pydantic validation. | -| `templates/snack_template.yaml` | ✅ Working | YAML metadata template. | - ---- - -## Skills - -### Builtin Skills (`backend/app/skills/builtin/`) - -| Skill | File | Status | Notes | -|-------|------|--------|-------| -| **ask_vault** | `ask_vault.py` | ⚠️ Untested | Vault Q&A skill. | -| **attach_context** | `attach_context.py` | ⚠️ Untested | Context attachment. | -| **backup** | `backup.py` | ⚠️ Untested | Backup operations. | -| **brain_sync** | `brain_sync.py` | ⚠️ Untested | Brain sync. | -| **clipboard_maintenance** | `clipboard_maintenance.py` | ⚠️ Untested | Clipboard cleanup. | -| **container_start** | `container_start.py` | ⚠️ Untested | Docker container start. | -| **container_stop** | `container_stop.py` | ⚠️ Untested | Docker container stop. | -| **daily_backup** | `daily_backup.py` | ⚠️ Untested | Daily backup routine. | -| **episodic_log** | `episodic_log.py` | ⚠️ Untested | Episodic memory logging. | -| **export** | `export.py` | ⚠️ Untested | Data export. | -| **file_edit_enhancer** | `file_edit_enhancer.py` | ✅ Working | MCP file editing with spool logging. | -| **git_maintenance** | `git_maintenance.py` | ⚠️ Untested | Git operations. | -| **import_skill** | `import_skill.py` | ⚠️ Untested | Skill import. | -| **lint_fix** | `lint_fix.py` | ⚠️ Untested | Lint auto-fix. | -| **route_task** | `route_task.py` | ⚠️ Untested | Task routing. | -| **skill_audit** | `skill_audit.py` | ⚠️ Untested | Skill audit. | -| **skill_autostart** | `skill_autostart.py` | ⚠️ Untested | Auto-start skills. | -| **skill_color_reset** | `skill_color_reset.py` | ⚠️ Untested | Terminal color reset. | -| **skill_dead_code_archiver** | `skill_dead_code_archiver.py` | ✅ Working | Dead code detection. | -| **skill_devlog_mcp** | `skill_devlog_mcp.py` | ✅ Working | MCP devlog generation. | -| **skill_duplicate_detector** | `skill_duplicate_detector.py` | ✅ Working | Duplicate code detection. | -| **skill_hardcoded_path_detector** | `skill_hardcoded_path_detector.py` | ✅ Working | Hardcoded path detection. | -| **skill_lint_fix** | `skill_lint_fix.py` | ⚠️ Untested | Lint fix (duplicate of lint_fix?). | -| **skill_lucide_icon_migration** | `skill_lucide_icon_migration.py` | ⚠️ Untested | Icon migration audit. | -| **skill_mcp_self_heal** | `skill_mcp_self_heal.py` | ⚠️ Untested | MCP self-healing. | -| **skill_modularisation_planner** | `skill_modularisation_planner.py` | ✅ Working | Modularization planning. | -| **skill_pico_component_audit** | `skill_pico_component_audit.py` | ⚠️ Untested | Pico CSS component audit. | -| **skill_surface_enhancement_report** | `skill_surface_enhancement_report.py` | ⚠️ Untested | Surface enhancement reporting. | -| **skill_surface_rebuild** | `skill_surface_rebuild.py` | ⚠️ Untested | Surface rebuild. | -| **skill_ucore_index** | `skill_ucore_index.py` | ⚠️ Untested | uCore index. | -| **skill_usx_audit_enhanced** | `skill_usx_audit_enhanced.py` | ⚠️ Untested | USX audit. | -| **skill_usx_spacing_normalize** | `skill_usx_spacing_normalize.py` | ⚠️ Untested | USX spacing normalize. | -| **spool_maintenance** | `spool_maintenance.py` | ✅ Working | Spool maintenance. | -| **surface_repair** | `surface_repair.py` | ⚠️ Untested | Surface repair. | -| **surface_restart** | `surface_restart.py` | ⚠️ Untested | Surface restart. | -| **tasker_devlog_bridge** | `tasker_devlog_bridge.py` | ✅ Working | Tasker↔devlog bridge. | -| **tasker_sync** | `tasker_sync.py` | ⚠️ Untested | Tasker sync. | -| **usx_standard** | `usx_standard.py` | ⚠️ Untested | USX standard. | -| **vault_sync** | `vault_sync.py` | ⚠️ Untested | Vault sync. | -| **workflow_audit** | `workflow_audit.py` | ⚠️ Untested | Workflow audit. | -| **workflow_guard** | `workflow_guard.py` | ⚠️ Untested | Workflow guard. | -| **workflow_pause** | `workflow_pause.py` | ⚠️ Untested | Workflow pause. | - -### Scheduled Skills (`backend/app/skills/builtin/scheduled/`) - -| Skill | Status | Notes | -|-------|--------|-------| -| *(empty directory)* | ⚠️ Empty | No scheduled skills defined yet. | - -### Skill Templates - -| File | Status | Notes | -|------|--------|-------| -| `templates/skill_template.py` | ✅ Working | BaseSkill class. | -| `templates/skill_template.yaml` | ✅ Working | YAML metadata template. | - ---- - -## Issues Found & Fixed - -### Fixed -1. **`tasker_bridge.py`** — Restored backward-compat exports and stable markdown rendering. - -### Pre-existing Issues (not fixed) -1. **`system_snack.py`** — Hardcoded paths (`/Users/fredbook/Code/uCore/backend`). Not portable. -2. **`surface_snack.py`** — Hardcoded paths. Auto-starts backend with hardcoded cwd. -3. **Many skills untested** — many builtin skills are marked ⚠️ Untested. They parse cleanly but have never been executed in this audit. - ---- - -## Alignment with Reference Docs - -| Doc | Alignment Status | Notes | -|-----|-----------------|-------| -| `docs/SNACKS_SYSTEM_SPEC.md` | ✅ Aligned | Snack registry, BaseSnack, and menu snacks match spec. | -| `docs/SPOOL_SPEC.md` | ✅ Aligned | SnackShack uses `spool_reader`. | -| `docs/DEVMODE_CODE_ANALYSIS_SKILLS.md` | ✅ Aligned | duplicate-detector, dead-code-archiver, modularisation-planner all exist. | -| `docs/MCP_SETUP.md` | ✅ Aligned | MCP skills (file_edit_enhancer, tasker_devlog_bridge) match. | -| `docs/OPTIMIZED_WORKFLOW.md` | ⚠️ Partial | TOON/Flow-LLM skills exist but not as builtin skills. | - ---- - -## Recommendations - -1. **Run the 28 untested skills** through a smoke test to confirm they import and execute without errors. -2. **Consolidate `lint_fix.py` and `skill_lint_fix.py`** — they appear to be duplicates. -3. **Remove hardcoded paths** from system_snack and surface_snack — use env vars or config. -4. **Add scheduled skills** or remove the empty `scheduled/` directory. -5. **Run ruff on all skills** to catch any other syntax errors. \ No newline at end of file diff --git a/scripts/ucore_startup.sh b/scripts/ucore_startup.sh index a5b231b0..2cd2b466 100644 --- a/scripts/ucore_startup.sh +++ b/scripts/ucore_startup.sh @@ -110,10 +110,10 @@ fi # Ensure only one menu process remains (single popcorn icon) enforce_single_menu_instance -# Run health check skill +# Probe the canonical backend health endpoint log "🏥 Running health check..." if check_backend; then - curl -s --max-time 5 "http://localhost:8484/api/skills/autostart_health_check/run" > /dev/null 2>&1 || true + curl -s --max-time 5 "http://localhost:8484/api/health" > /dev/null 2>&1 || true fi -log "✅ uCore startup complete" \ No newline at end of file +log "✅ uCore startup complete" diff --git a/scripts/ucore_watchdog.sh b/scripts/ucore_watchdog.sh index 3e85dd41..47f29ea5 100755 --- a/scripts/ucore_watchdog.sh +++ b/scripts/ucore_watchdog.sh @@ -75,7 +75,7 @@ enforce_single_menu_instance() { run_autonomy_health_action() { curl -s --max-time 6 \ - "http://127.0.0.1:8484/api/skills/autostart_health_check/run" \ + "http://127.0.0.1:8484/api/health" \ > /dev/null 2>&1 || true }