diff --git a/.clinerules b/.clinerules deleted file mode 100644 index 378de20d..00000000 --- a/.clinerules +++ /dev/null @@ -1,99 +0,0 @@ -# Cline Rules - uCore Baseline - -Use this repository in local-first, auditable mode. - -## Operating Principles - -1. Prefer safe, reversible changes and keep diffs small. -2. Keep durable workflow state in `.tasker/` Markdown files. -3. Treat Cline Kanban as orchestration UI, not source of truth. -4. Keep MCP integrations localhost-only by default. -5. Preserve Git history clarity with focused, test-backed changes. - -## Workflow Defaults - -1. For new work, start from a Kanban card linked to a `.tasker` item. -2. Use isolated worktrees/branches for card execution when available. -3. Validate with targeted tests before proposing merges. -4. Report blockers with concrete next actions. -5. Before capability actions, run preflight and block on unmet prerequisites. - -## Preflight And Repair Gates - -1. Required checks before executing capability actions: - - GET /api/extensions/status - - GET /api/capabilities/{capability}/preflight -2. If preflight returns repair_required=true or HTTP 412: - - stop execution for that capability - - present repair steps - - require rerun of preflight before resume -3. Do not silently substitute hidden defaults for missing required provider/model/key values. -4. Record repair events and outcomes in docs/handovers and tasker notes. - -## MCP Usage - -1. Prefer the `uCore` MCP server for skills and knowledge access. -2. Verify MCP health before relying on tools. -3. Do not broaden network exposure of MCP services without explicit opt-in. - -## Cost-Aware Routing - -1. Simple tasks: local/free model when practical. -2. Medium tasks: Codex/Roundtable tier. -3. Complex tasks: premium reasoning tier only when justified. - -## Docs Round Completion (before every push) - -1. Create or update a FEATURE_SPEC.md in docs/ for the feature delivered. -2. Archive completed sprint plans and stale dev summaries to docs/archive/. -3. Archive completed tasker items; mark sprint status as complete when done. -4. Update devlog.mcp.yaml with all new and modified files. -5. Update fieldnotes.md with key decisions, observations, archived code, lessons. -6. Update wisdom.md lessons if any durable insights were learned. -7. Bump version patch in pyproject.toml and package.json. -8. Update .tasker.dev-flow.yaml with completed_count, sprint status, and notes. -9. Run this round checklist before the final git push of any dev session. - -## Git Commit Rules - -1. Use short commit messages (max 72 chars for first line). -2. No special characters in commit messages (no arrows, emoji, quotes, backticks). -3. Use plain ASCII only: letters, numbers, spaces, hyphens, periods, commas. -4. First line should be a concise summary. Body is optional and also plain ASCII. -5. Use simple non-blocking commands: git add -A && git commit -m ... && git push. -6. Only commit when all changes for a task are complete and verified. -7. Prefer one commit per logical feature or fix, not per file edit. - -## In-House Skills Library - -1. Prioritize uCore builtin skills over external tools when available. -2. Skills live in backend/app/skills/builtin/ as BaseSkill subclasses. -3. Register skills via SkillMeta(id, name, description, category, params). -4. Use skill_docs_roundup for end-of-session documentation automation. -5. Use skill_dev_destroy_rebuild for Dev Mode recovery workflows. -6. Use tasker_ingest for bridging session progress to .tasker/spool/wisdom. -7. Use file_edit_enhancer for batch file operations with spool logging. -8. Always consult skill registry before writing new skills to avoid duplication. - -## Feed System (Pod/Nugget/Seed/Slate/Spool) - -1. Feed is the unified incoming data layer for all user activity. -2. Activity Pod (SQLite) stores browser, email, message, alert, search activity. -3. Feed MCP server exposes ingest, query, suggest, link tools. -4. FeedConsumer bridges feed activity into the Spool (feed in motion). -5. Seed data bootstraps feed sources (browser paths, email, messages, contacts). -6. Binder suggestions are AI-generated from feed activity clusters. -7. Feed panel in Developer Surface shows incoming activity and suggestions. -8. Task-activity links connect .tasker tasks back to feed origins. - -## USX Variable & Modular Style System - -1. Zero hardcoded values in component CSS — every visual property must use var(--usx-\*). -2. Token source of truth: styles/tokens/tokens-{color,typography,spacing,touch,components}.css. -3. Theme overrides live in styles/themes/{base,dark,teletext,c64,high-contrast}.css — override only what changes. -4. Component library: styles/usx-standard.css — all BEM surface plates and primitives use variables only. -5. Hardcoded hex colors (#xxx) allowed ONLY in tokens-color.css and theme override files. -6. Use useTheme() composable (composables/useTheme.ts) for runtime theme switching — singleton, localStorage. -7. Run usx_standard skill 'validate-tokens' before every push to verify token integrity. -8. Scaffold new surfaces with usx_standard 'scaffold-surface' — generates USX-compliant Vue templates. -9. All interactive elements must meet var(--usx-touch-min) minimum touch target. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1892880..f13a1c3a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,10 +13,28 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install governance probe dependencies + run: python -m pip install aiohttp pyyaml - name: Validate planning governance run: bash scripts/validate_planning_governance.sh + - name: Reject new home-root state paths (pull request) + if: github.event_name == 'pull_request' + run: git diff --unified=0 "origin/${{ github.base_ref }}...HEAD" | python3 scripts/check_home_path_policy.py --diff-stdin + + - name: Reject new home-root state paths (push) + if: github.event_name == 'push' + run: git diff --unified=0 "${{ github.event.before }}..${{ github.sha }}" | python3 scripts/check_home_path_policy.py --diff-stdin + - name: Validate capability requirements coverage run: python3 scripts/validate_capability_requirements.py @@ -29,17 +47,6 @@ jobs: - name: Validate extension manifests run: python3 scripts/validate_extension_manifests.py - - name: Checkout SnackMachine contract source - uses: actions/checkout@v4 - with: - repository: fredporter/SnackMachine - path: external/SnackMachine - - - name: Validate SnackMachine contract compatibility - env: - UCORE_SNACKMACHINE_PATH: ${{ github.workspace }}/external/SnackMachine - run: python3 scripts/check_snackmachine_contract.py - - name: Audit duplicate API routes run: python3 scripts/audit_duplicate_routes.py @@ -52,15 +59,13 @@ jobs: - name: Validate split-repo packaging layout run: python3 scripts/validate_split_repo_packaging.py - - name: Validate MCP config lock - run: python3 scripts/validate_mcp_config.py - backend-tests: name: Backend Tests runs-on: ubuntu-latest - defaults: - run: - working-directory: backend + env: + UDOS_ROOT: ${{ github.workspace }}/.. + UDOS_HOME: ${{ github.workspace }}/.ci-udos + UCORE_UCODE_PATH: ${{ github.workspace }}/external/uCode steps: - name: Checkout uses: actions/checkout@v4 @@ -70,24 +75,64 @@ jobs: with: python-version: "3.12" + - name: Checkout uFlow + uses: actions/checkout@v4 + with: + repository: fredporter/uFlow + ref: work/2026-08-18-stabilise + path: external/uFlow + + - name: Checkout uKnowledge + uses: actions/checkout@v4 + with: + repository: fredporter/uKnowledge + ref: work/2026-08-18-stabilise + path: external/uKnowledge + + - name: Checkout uCode runtime + uses: actions/checkout@v4 + with: + repository: fredporter/uCode + ref: work/2026-08-18-stabilise + path: external/uCode + - name: Install backend dependencies run: | python -m pip install --upgrade pip - pip install -e .[dev] + pip install -e ./external/uFlow -e ./external/uKnowledge -e ./external/uCode + pip install -e ./backend[dev] + + - name: Prepare isolated runtime and Git identity + run: | + mkdir -p "$UDOS_HOME/data" + git config --global user.name "uCore CI" + git config --global user.email "ci@udos.invalid" - name: Run backend tests - run: pytest -q + run: python -m pytest -q backend/tests frontend-build: name: Frontend Build runs-on: ubuntu-latest - defaults: - run: - working-directory: frontend steps: - name: Checkout uses: actions/checkout@v4 + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Checkout uCode + uses: actions/checkout@v4 + with: + repository: fredporter/uCode + ref: work/2026-08-18-stabilise + path: external/uCode + + - name: Expose sibling uCode source contract + run: ln -s "$GITHUB_WORKSPACE/external/uCode" "$GITHUB_WORKSPACE/../uCode" + - name: Set up Node uses: actions/setup-node@v4 with: @@ -98,8 +143,8 @@ jobs: - name: Install frontend dependencies run: pnpm install --frozen-lockfile - - name: Validate prose class standard - run: pnpm run check:prose-standard - - name: Build frontend - run: pnpm run build + run: pnpm --dir frontend-vue run build + + - name: Test frontend + run: pnpm --dir frontend-vue run test -- --run diff --git a/.tasker.dev-flow.yaml b/.tasker.dev-flow.yaml deleted file mode 100644 index 42f55354..00000000 --- a/.tasker.dev-flow.yaml +++ /dev/null @@ -1,1009 +0,0 @@ -# Consolidated Dev Flow - Generated: 2026-06-28T10:00:00Z - -version: "1.4.0" -generated_by: "Cline-uCore" -source_count: 7 -task_count: 47 -completed_count: 63 -sprint: - id: "sprint.2026-07-08-triple" - name: "USX npm Packaging + Test Fixes + Self-Heal Infrastructure" - start: "2026-07-08" - end: "2026-07-08" - status: "complete" - total_tasks: 3 - completed_tasks: 3 - notes: "Sprint 1: Test fixes (502/502 passing, 5 stale archived). Sprint 2: Self-identification & repair (GET /api/health/full, POST /api/system/repair, startup health check, frontend Control Panel wiring). Sprint 3: USX tokens package v3.1.0 (c64/teletext/high-contrast synced, PUBLISH.md written, npm login pending). Version 4.0.5. 6-layer self-healing architecture documented." - waves: - - name: "Foundation" - days: "1-3" - tasks: - [ - "task.hivemind.001", - "task.hivemind.002", - "task.hivemind.003", - "task.hivemind.008", - "task.hivemind.009", - ] - - name: "Integration" - days: "3-5" - tasks: - [ - "task.hivemind.004", - "task.hivemind.005", - "task.hivemind.006", - "task.hivemind.007", - "task.hivemind.010", - ] - - name: "Polish" - days: "5-7" - tasks: ["task.hivemind.011", "task.hivemind.012"] - - previous: - id: "sprint.2026-07-02" - status: "complete" - notes: "Control Panel (8 Vue files), Lane A (4 skills), Lane B (6 skills), all panels wired to APIs. Ecosystem audit: 173 items at 99.4% health." - -metadata: - created: "2026-06-28T10:00:00Z" - user: "$USER" - workspace: "uCore" - -lanes: - maintenance: - description: "Bug fixes, refactoring, technical debt" - count: 27 - tasks: - - task.maintenance.003 - - task.maintenance.004 - - task.maintenance.005 - - task.maintenance.006 - - task.maintenance.007 - - task.maintenance.008 - - task.maintenance.009 - - task.maintenance.010 - - task.maintenance.011 - - task.maintenance.012 - - task.maintenance.013 - - task.maintenance.014 - - task.maintenance.015 - - task.maintenance.016 - - task.maintenance.017 - - task.maintenance.018 - - task.maintenance.019 - - task.maintenance.020 - - task.maintenance.021 - - task.maintenance.022 - - task.maintenance.023 - - task.maintenance.024 - - task.maintenance.025 - - task.maintenance.026 - - task.maintenance.027 - - task.hivemind.001 - - task.hivemind.002 - - task.hivemind.003 - - task.hivemind.004 - - task.hivemind.005 - - task.hivemind.007 - - task.hivemind.008 - - task.hivemind.009 - - task.hivemind.010 - - task.hivemind.011 - - task.hivemind.012 - ui: - description: "Developer Surface, frontend" - count: 5 - tasks: - - task.ui.001 - - task.ui.002 - - task.ui.003 - - task.hivemind.006 - - task.hivemind.008 - -tasks: - # === MAINTENANCE TASKS (from dev_maintenance_report_2026-06-28.md) === - - # === NEW: Spool Enhancement Tasks === - - - uid: "task.maintenance.001" - title: "Enhance spool_maintenance to archive completed tasks" - description: "Add task archiving capability to spool_maintenance skill - move completed tasks from .tasker to .tasker.archived" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p0", "tech-debt", "spool", "mcp"] - source: - file: "backend/app/skills/builtin/spool_maintenance.py" - line: 1 - type: "consolidation" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-28T10:02:00Z" - mcp_optimized: true - - - uid: "task.maintenance.002" - title: "Create devlog_mcp skill for MCP-formatted devlog generation" - description: "Generate structured devlog in MCP format from completed tasks and spool activity" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p0", "tech-debt", "spool", "mcp"] - source: - file: "backend/app/skills/builtin/skill_devlog_mcp.py" - line: 1 - type: "consolidation" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-28T10:02:00Z" - mcp_optimized: true - - # === ORIGINAL: Duplicate Function Extraction Tasks === - - - uid: "task.maintenance.003" - title: "Extract duplicate function: chat_cache.py:32 and server.py:56" - description: "Exact duplicate function found with hash 3f6e23ee64d4. Extract to shared module." - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p1", "tech-debt", "refactor"] - source: - file: "backend/app/services/chat_cache.py" - line: 32 - type: "duplicate-detector" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-28T10:05:00Z" - mcp_optimized: false - - - uid: "task.maintenance.004" - title: "Extract duplicate function: workflow_manager.py:41 and budget_manager.py:78" - description: "Exact duplicate function found with hash aaec62c54747. Extract to shared module." - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p1", "tech-debt", "refactor"] - source: - file: "backend/app/services/workflow_manager.py" - line: 41 - type: "duplicate-detector" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-28T10:05:00Z" - mcp_optimized: false - - - uid: "task.maintenance.005" - title: "Review skill_usx_spacing_normalize and skill_usx_audit_enhanced for consolidation" - description: "Skills exist in builtin/ not mcp/ - verify duplicate function and consolidate" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p1", "tech-debt", "refactor", "review"] - source: - file: "backend/app/skills/builtin/skill_usx_spacing_normalize.py" - line: 92 - type: "duplicate-detector" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-28T10:05:00Z" - mcp_optimized: false - notes: "Reviewed - skills have different purposes (normalize vs audit). Only shared helper is _get_css_files() which is trivial. No consolidation required." - - - uid: "task.maintenance.006" - title: "Review skill_lucide_icon_migration and skill_pico_component_audit for consolidation" - description: "Skills exist in builtin/ not mcp/ - verify duplicate function and consolidate" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p1", "tech-debt", "refactor", "review"] - source: - file: "backend/app/skills/builtin/skill_lucide_icon_migration.py" - line: 100 - type: "duplicate-detector" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-28T10:05:00Z" - mcp_optimized: false - notes: "Reviewed - skills have different purposes (icon audit vs component audit). skill_pico_component_audit includes badge patterns but no icon migration overlap. No consolidation required." - - - uid: "task.maintenance.027" - title: "Run duplicate detector on current codebase" - description: "Execute duplicate-detector skill to find actual duplicates in builtin/ folder" - status: "done" - priority: "medium" - lane: "maintenance" - tags: ["p2", "tech-debt", "duplicate-detector", "scan"] - source: - file: "backend/app/skills/builtin/skill_duplicate_detector.py" - line: 1 - type: "duplicate-detector" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-28T10:05:00Z" - mcp_optimized: true - notes: "Reviewed - skill_duplicate_detector.py already exists and is MCP-optimized. Tasks 005/006 were false positives from initial scan - skills have distinct purposes." - - - uid: "task.maintenance.007" - title: "Extract duplicate function: skill_duplicate_detector.py:156, skill_modularisation_planner.py:156, skill_dead_code_archiver.py:182" - description: "Exact duplicate function found with hash f5228519051e in 3 files. Extract to shared module." - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p1", "tech-debt", "refactor"] - source: - file: "backend/app/mcp/skill_duplicate_detector.py" - line: 156 - type: "duplicate-detector" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-30T20:45:00Z" - mcp_optimized: false - notes: "Stale duplicate-detector output; current files do not contain this exact duplicate. Archived as false positive." - - - uid: "task.maintenance.008" - title: "Extract duplicate function: system_snack.py:274 and surface_snack.py:110" - description: "Exact duplicate function found with hash 9b62dd6b18c6. Extract to shared module." - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p1", "tech-debt", "refactor"] - source: - file: "backend/app/menu/snacks/system_snack.py" - line: 274 - type: "duplicate-detector" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-30T20:45:00Z" - mcp_optimized: false - notes: "Line numbers no longer match current files; no duplicate found at referenced locations. Archived as stale." - - - uid: "task.maintenance.009" - title: "Clean up 242 orphaned imports across codebase" - description: "Imports that are never used in their respective files. Clean up orphaned imports in test files and main code." - status: "done" - priority: "medium" - lane: "maintenance" - tags: ["p2", "tech-debt", "cleanup"] - source: - file: "tmp/dev_maintenance_report_2026-06-28.md" - line: 1 - type: "dead-code-archiver" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-30T20:45:00Z" - mcp_optimized: false - notes: "Ruff F401 scan found 51 unused imports, many used dynamically for MCP/route registration. Auto-removal is unsafe. Archived as too risky to automate." - - - uid: "task.maintenance.010" - title: "Modernize 3 legacy code patterns" - description: "Replace typing_extensions imports with typing, convert .format() calls to f-strings, update old-style super() calls." - status: "done" - priority: "medium" - lane: "maintenance" - tags: ["p2", "tech-debt", "modernize"] - source: - file: "tmp/dev_maintenance_report_2026-06-28.md" - line: 1 - type: "dead-code-archiver" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-30T20:45:00Z" - mcp_optimized: false - notes: "typing_extensions imports already removed; .format() only in dead-code-archiver detection pattern; old-style super() calls are objc.super() for macOS menus and must remain." - - - uid: "task.maintenance.011" - title: "Modularize unified_menu.py (1328 lines, priority 85)" - description: "Split _rebuild_menu (288 lines) and refactor _refresh (11 complexity). Large file with complex functions." - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p0", "tech-debt", "modularize"] - source: - file: "backend/app/menu/unified_menu.py" - line: 1 - type: "modularisation-planner" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-29T15:57:00Z" - mcp_optimized: false - - - uid: "task.maintenance.012" - title: "Modularize mcp.py (1093 lines, priority 80)" - description: "Stale — mcp.py is now 455 lines and already modularized (mcp_handlers.py extracted). handle_mcp_discover is 278 lines of tool definitions, handle_mcp_call is 19 lines delegating to dispatcher." - status: "archived" - priority: "high" - lane: "maintenance" - tags: ["p0", "tech-debt", "modularize", "stale"] - source: - file: "backend/app/api/mcp.py" - line: 1 - type: "modularisation-planner" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-30T21:16:00Z" - mcp_optimized: false - notes: "Already modularized. handle_mcp_discover (278 lines) is a declarative tool registry — not worth splitting further. handle_mcp_call (19 lines) delegates to mcp_handlers.py dispatch_tool." - - - uid: "task.maintenance.013" - title: "Review 22 similar code blocks for consolidation" - description: "Stale — source file tmp/dev_maintenance_report_2026-06-28.md no longer exists. Similar code blocks were from stale duplicate-detector output." - status: "archived" - priority: "medium" - lane: "maintenance" - tags: ["p2", "tech-debt", "review", "stale"] - source: - file: "tmp/dev_maintenance_report_2026-06-28.md" - line: 1 - type: "duplicate-detector" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-30T21:16:00Z" - mcp_optimized: false - notes: "Source file removed. Stale reference." - - - uid: "task.maintenance.014" - title: "Add tests for /api/snacks/system endpoints and tray-facing behavior" - description: "Stale — REMNANTS_AND_DUPLICATION_AUDIT.md already archived to docs/archive/. Deferred until snackbar refactor." - status: "archived" - priority: "medium" - lane: "maintenance" - tags: ["p2", "testing", "snackbar", "stale"] - source: - file: "docs/REMNANTS_AND_DUPLICATION_AUDIT.md" - line: 42 - type: "audit" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-30T21:16:00Z" - mcp_optimized: false - notes: "REMNANTS doc archived. Deferred." - - # === UI TASKS (from frontend consistency tasker) === - - - uid: "task.ui.001" - title: "Create .usx-card-header utility in usx-layout-system.css" - description: "Foundation for card headers - part of frontend consistency effort." - status: "done" - priority: "high" - lane: "ui" - tags: ["p0", "feature", "usx"] - source: - file: "tasker/consistency_tasker.md" - line: 1 - type: "tasker" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-28T10:05:00Z" - mcp_optimized: false - notes: "Already exists in usx-layout-system.css (lines 220-250). Verified canonical implementation." - - - uid: "task.ui.002" - title: "Migrate hardcoded font-sizes in surfaces/developer.css (23 instances)" - description: "Part of frontend consistency effort - Phase 1." - status: "done" - priority: "high" - lane: "ui" - tags: ["p0", "feature", "usx"] - source: - file: "tasker/consistency_tasker.md" - line: 2 - type: "tasker" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-28T10:05:00Z" - mcp_optimized: false - notes: "Verified - developer.css already uses USX font variables (--usx-font-size-body, --usx-font-size-meta, --pico-font-size-condensed). No hardcoded px font sizes found." - - - uid: "task.ui.003" - title: "Migrate hardcoded font-sizes in hub/settings.css, surfaces/ucode.css, assistui.css" - description: "Part of frontend consistency effort - Phase 1. 12+5+7=24 instances total." - status: "done" - priority: "high" - lane: "ui" - tags: ["p0", "feature", "usx"] - source: - file: "tasker/consistency_tasker.md" - line: 3 - type: "tasker" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-28T10:05:00Z" - mcp_optimized: false - notes: "Verified - all three files already use USX font variables. settings.css uses --usx-font-size-body/meta/small, ucode.css uses --usx-font-size-body/meta/sm, assistui.css uses --usx-font-size-body. No hardcoded px font sizes found." - - # === HIVE MIND TASKS (from Hivemind-Powered Developer Workflow spec) === - - - uid: "task.hivemind.001" - title: "Install & configure Hivemind MCP server" - description: "Install npm packages, configure API keys in ~/.config/hivemind/.env, add to Cline/Claude Code" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p0", "hivemind", "setup", "mcp"] - source: - file: "docs/HIVEMIND_WORKFLOW_SPEC.md" - line: 1 - type: "spec" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-30T22:27:00Z" - mcp_optimized: true - notes: "hivemind_server.py built on port 8490 with consensus engine, LLM router, agent registry. MCP now uses canonical stdio bridge via .vscode/mcp.json and uDev/mcp-bridge/build/index.js." - - - uid: "task.hivemind.002" - title: "Configure Hivemind consensus engine" - description: "Set up multi-model consensus with GPT-5.2, Claude Opus 4.5, Gemini 3 Pro; enable MCP sampling with human-in-loop" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p0", "hivemind", "config", "consensus"] - source: - file: "docs/HIVEMIND_WORKFLOW_SPEC.md" - line: 1 - type: "spec" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-30T22:27:00Z" - mcp_optimized: true - notes: "ConsensusEngine implemented in backend/app/mcp/consensus.py with Proposal, Vote enum (approve/reject/abstain), weighted voting, and quorum-based resolution." - - - uid: "task.hivemind.003" - title: "Install & configure Roundtable AI for local swarm" - description: "pip install roundtable-ai, add to Cline/Claude Code, configure agent specialization (Gemini, Claude, Codex, Cursor)" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p0", "hivemind", "roundtable", "setup"] - source: - file: "docs/HIVEMIND_WORKFLOW_SPEC.md" - line: 1 - type: "spec" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-30T22:27:00Z" - mcp_optimized: true - notes: "roundtable-ai installed via pip. LLM router configured with Roundtable as priority 1 backend (http://localhost:4891/v1). Agent specialization via SpecializedAgentRegistry in agent_specialization.py." - - - uid: "task.hivemind.004" - title: "Configure OpenRouter cost management" - description: "Create account, add $10 credits, configure model routing priorities (free/ultra-cheap/premium tiers)" - status: "done" - priority: "medium" - lane: "maintenance" - tags: ["p1", "hivemind", "openrouter", "cost"] - source: - file: "docs/HIVEMIND_WORKFLOW_SPEC.md" - line: 1 - type: "spec" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-30T22:47:00Z" - mcp_optimized: true - notes: "OpenRouter config created at config/openrouter.yaml with 5 cost tiers (free/ultra-cheap/budget/mid-range/premium) and agent-to-tier mappings. Budget manager at backend/app/services/budget_manager.py with session/daily/monthly limits and circuit breaker." - - - uid: "task.hivemind.005" - title: "Install & configure Ollama local models" - description: "Install Ollama, pull codellama:7b, deepseek-coder:6.7b, mistral:7b; integrate with Hivemind as fallback" - status: "done" - priority: "medium" - lane: "maintenance" - tags: ["p1", "hivemind", "ollama", "local"] - source: - file: "docs/HIVEMIND_WORKFLOW_SPEC.md" - line: 1 - type: "spec" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-30T22:47:00Z" - mcp_optimized: true - notes: "Ollama v0.24.0 installed with 6 models: qwen2.5-coder (0.5b/1.5b/3b/7b), codegemma:2b, nomic-embed-text. Wired into LLM router as priority 2 backend. Provider router configured with Ollama as default local provider." - - - uid: "task.hivemind.006" - title: "Install & configure Cline Kanban developer surface" - description: "npm install -g kanban, configure agent compatibility, set up project context with git repository" - status: "done" - priority: "medium" - lane: "ui" - tags: ["p1", "hivemind", "kanban", "ui"] - source: - file: "docs/HIVEMIND_WORKFLOW_SPEC.md" - line: 1 - type: "spec" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-30T22:47:00Z" - mcp_optimized: true - notes: "Kanban v0.1.68 installed globally via npm. Config at config/kanban.yaml with 5-column board (Backlog/Ready/In Progress/Review/Done), uCore Tasker API integration, and 6 agent color mappings. Runtime URL: http://127.0.0.1:3484" - - - uid: "task.hivemind.007" - title: "Implement Hivemind shared knowledge layer" - description: "Enable shared memory layer, configure core MCP tools (publish, query, subscribe, status, lock)" - status: "done" - priority: "medium" - lane: "maintenance" - tags: ["p1", "hivemind", "knowledge", "mcp"] - source: - file: "docs/HIVEMIND_WORKFLOW_SPEC.md" - line: 1 - type: "spec" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-30T22:47:00Z" - mcp_optimized: true - notes: "KnowledgeLayer implemented at backend/app/services/knowledge_layer.py with SQLite storage, FTS5 full-text search, publish/subscribe, lock/unlock, and TTL-based expiry. API at backend/app/api/hivemind_knowledge.py with 9 endpoints. DB at ~/.ucore/knowledge/shared.db" - - - uid: "task.hivemind.008" - title: "Create template system for Dev Mode recovery" - description: "Implement ~/.ucode/templates/ structure with default/stable/experimental/custom directories" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p0", "hivemind", "template", "recovery"] - source: - file: "docs/HIVEMIND_WORKFLOW_SPEC.md" - line: 1 - type: "spec" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-30T22:27:00Z" - mcp_optimized: true - notes: "Plates system implemented with PLATES_SYSTEM_SPEC.md, plate_refresh/ engine, Cookiecutter templates, Pydantic validation, and drift detection. Template directories at plates/destroy/." - - - uid: "task.hivemind.009" - title: "Implement DESTROY/REBUILD command for Dev Mode" - description: "Build destroy/rebuild workflow with backup, component reset mapping, and template rebuild" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p0", "hivemind", "recovery", "command"] - source: - file: "docs/HIVEMIND_WORKFLOW_SPEC.md" - line: 1 - type: "spec" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-30T22:27:00Z" - mcp_optimized: true - notes: "DESTROY/REBUILD protocol implemented in plate_refresh/ with backup, component reset mapping, Cookiecutter template rebuild, and SPOOL archive integration." - - - uid: "task.hivemind.010" - title: "Build catalog service for skills and MCP servers" - description: "Implement spatial UID system, auto-discovery, SQLite storage with full-text search" - status: "done" - priority: "medium" - lane: "maintenance" - tags: ["p1", "hivemind", "catalog", "api"] - source: - file: "docs/HIVEMIND_WORKFLOW_SPEC.md" - line: 1 - type: "spec" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-30T22:47:00Z" - mcp_optimized: true - notes: "CatalogService at backend/app/services/catalog/ with SQLite storage, FTS5 search, SpatialUID system, and relationship graphs. API at backend/app/api/catalog.py with 7 endpoints (list, get, search, relationships, graph, sync, stats). Wired into routes.py." - - - uid: "task.hivemind.011" - title: "Implement template verification and promotion system" - description: "Build verification engine with test suites, promotion criteria (95% pass rate, security, dogfooding)" - status: "done" - priority: "medium" - lane: "maintenance" - tags: ["p1", "hivemind", "template", "verification"] - source: - file: "docs/HIVEMIND_WORKFLOW_SPEC.md" - line: 1 - type: "spec" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-30T22:53:00Z" - mcp_optimized: true - notes: "Verification engine at backend/plate_refresh/verification.py with verify_plate(), verify_all_plates(), promotion criteria (95% pass rate, security checks, dogfooding), and CLI integration via --verify and --promote flags." - - - uid: "task.hivemind.012" - title: "Add template monitoring and audit logging" - description: "Implement template usage tracking, audit trail, and health checks for corruption/version drift" - status: "done" - priority: "low" - lane: "maintenance" - tags: ["p2", "hivemind", "monitoring", "audit"] - source: - file: "docs/HIVEMIND_WORKFLOW_SPEC.md" - line: 1 - type: "spec" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-30T22:53:00Z" - mcp_optimized: true - notes: "Monitoring system at backend/plate_refresh/monitoring.py with usage tracking, audit trail, health checks for corruption/version drift, and CLI integration via --monitor and --audit flags." - - # === NEW: MCP Enhancement Tasks === - - - uid: "task.maintenance.015" - title: "Create MCP tasker-devlog-spool bridge skill" - description: "Bridge .tasker, devlog.mcp.yaml, and spool logs for unified MCP feed" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p0", "mcp", "bridge", "tasker"] - source: - file: "backend/app/skills/builtin/tasker_devlog_bridge.py" - line: 1 - type: "consolidation" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-28T10:00:00Z" - mcp_optimized: true - - - uid: "task.maintenance.016" - title: "Create spool_writer service for skills" - description: "Add write_spool function for skills to log to spool for audit trail" - status: "done" - priority: "medium" - lane: "maintenance" - tags: ["p1", "mcp", "spool", "service"] - source: - file: "backend/app/services/spool_writer.py" - line: 1 - type: "consolidation" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-28T10:00:00Z" - mcp_optimized: true - - - uid: "task.maintenance.017" - title: "Create file_edit_enhancer skill for MCP" - description: "Enhanced file editing with batch operations, spool logging, and tasker integration" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p0", "mcp", "editing", "skill"] - source: - file: "backend/app/skills/builtin/file_edit_enhancer.py" - line: 1 - type: "consolidation" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-28T10:00:00Z" - mcp_optimized: true - - # === SIDEBAR REFACTOR TASKS === - - - uid: "task.maintenance.018" - title: "Create Tabs module for sidebar/topbar separation" - description: "Separate tab management into reusable TabsModule component" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p0", "sidebar", "tabs", "component"] - source: - file: "frontend/src/components/Tabs/TabsModule.tsx" - line: 1 - type: "refactor" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-28T10:00:00Z" - mcp_optimized: true - - - uid: "task.maintenance.019" - title: "Create Filepicker TypeScript types" - description: "Define FileEntry, FilepickerState, and DevTag interfaces" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p0", "sidebar", "types", "typescript"] - source: - file: "frontend/src/types/filepicker.ts" - line: 1 - type: "refactor" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-28T10:00:00Z" - mcp_optimized: true - - - uid: "task.maintenance.020" - title: "Create VaultFileDiscovery Python engine" - description: "File discovery engine for all vault layers (User, Shared, Global, Code, Public)" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p0", "sidebar", "discovery", "python"] - source: - file: "backend/app/services/vault_file_discovery.py" - line: 1 - type: "refactor" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-28T10:00:00Z" - mcp_optimized: true - - - uid: "task.maintenance.021" - title: "Implement Dev Tags system for assessment" - description: "Python script to tag scripts/docs for later assessment and cleanup" - status: "done" - priority: "medium" - lane: "maintenance" - tags: ["p1", "dev-tags", "assessment", "script"] - source: - file: "scripts/dev_tags_assessment.py" - line: 1 - type: "refactor" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-28T10:00:00Z" - mcp_optimized: true - - - uid: "task.maintenance.022" - title: "Build WorkspaceFilter component for Filepicker" - description: "React component for workspace selection (User/Shared/Global/Code/Public)" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p0", "sidebar", "workspace", "component"] - source: - file: "frontend/src/components/WorkspaceFilter.tsx" - line: 1 - type: "refactor" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-28T10:00:00Z" - mcp_optimized: true - - - uid: "task.maintenance.023" - title: "Build BinderMissionFilter component for Filepicker" - description: "React component for binder/mission selection with chip selector" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p0", "sidebar", "binder", "component"] - source: - file: "frontend/src/components/BinderMissionFilter.tsx" - line: 1 - type: "refactor" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-28T10:00:00Z" - mcp_optimized: true - - - uid: "task.maintenance.024" - title: "Integrate Filepicker with VaultSidebar" - description: "Replace existing VaultSidebar with uihub Filepicker implementation" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p0", "sidebar", "integration", "refactor"] - source: - file: "frontend/src/components/FilepickerSidebar.tsx" - line: 1 - type: "refactor" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-28T10:00:00Z" - mcp_optimized: true - - - uid: "task.maintenance.025" - title: "Create FilepickerSidebar CSS styles" - description: "CSS styles for the three-filter-box filepicker system" - status: "done" - priority: "medium" - lane: "maintenance" - tags: ["p1", "sidebar", "css", "styles"] - source: - file: "frontend/src/styles/filepicker-sidebar.css" - line: 1 - type: "refactor" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-28T10:00:00Z" - mcp_optimized: true - - - uid: "task.maintenance.026" - title: "Unify FileEntry/VaultFile types" - description: "Consolidate overlapping types for backward compatibility" - status: "done" - priority: "medium" - lane: "maintenance" - tags: ["p1", "types", "unification", "refactor"] - source: - file: "frontend/src/types/filepicker.ts" - line: 1 - type: "consolidation" - created: "2026-06-28T10:00:00Z" - updated: "2026-06-28T10:00:00Z" - mcp_optimized: true - - # === NEW: MCP Modularization & Spec Tasks (Round 2026-06-30) === - - - uid: "task.maintenance.028" - title: "Modularize mcp_handlers.py into domain submodules" - description: "Split 640-line mcp_handlers.py into 8 domain modules (knowledge, clipboard, tasker, gridsmith, flow_router, toon, autostart, skill) with __init__.py registry" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p0", "tech-debt", "modularize", "mcp"] - source: - file: "backend/app/api/mcp_handlers/__init__.py" - line: 1 - type: "modularisation-planner" - created: "2026-06-30T21:00:00Z" - updated: "2026-06-30T21:30:00Z" - mcp_optimized: true - - - uid: "task.maintenance.029" - title: "Fix self_heal.py registry misalignment" - description: "Convert legacy self_heal.py classes to BaseSkill subclasses in skill_self_heal.py for proper registry discovery" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p0", "tech-debt", "registry", "skills"] - source: - file: "backend/app/skills/builtin/skill_self_heal.py" - line: 1 - type: "refactor" - created: "2026-06-30T21:00:00Z" - updated: "2026-06-30T21:30:00Z" - mcp_optimized: true - - - uid: "task.maintenance.030" - title: "Write PLATES_SYSTEM_SPEC.md" - description: "Comprehensive spec for Plates system (renamed from Templates) covering scaffolding via Cookiecutter, Pydantic validation, OpenAPI/Swagger MCP registry, DESTROY/REBUILD protocol, drift detection, and promotion workflow" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p0", "spec", "plates", "recovery"] - source: - file: "docs/PLATES_SYSTEM_SPEC.md" - line: 1 - type: "spec" - created: "2026-06-30T21:00:00Z" - updated: "2026-06-30T21:34:00Z" - mcp_optimized: true - - - uid: "task.maintenance.031" - title: "Write HIVEMIND_ORCHESTRATION_SPEC.md" - description: "Comprehensive spec for multi-agent orchestration covering MCP server topology, agent definitions, consensus engine, LLM router, and workflow templates" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p0", "spec", "hivemind", "orchestration"] - source: - file: "docs/HIVEMIND_ORCHESTRATION_SPEC.md" - line: 1 - type: "spec" - created: "2026-06-30T21:00:00Z" - updated: "2026-06-30T21:30:00Z" - mcp_optimized: true - - - uid: "task.maintenance.032" - title: "Update mcp_guardrails.py for modular mcp_handlers package" - description: "Update guardrails to validate the new mcp_handlers/ package structure instead of monolithic mcp_handlers.py" - status: "done" - priority: "medium" - lane: "maintenance" - tags: ["p1", "tech-debt", "guardrails", "mcp"] - source: - file: "backend/app/api/mcp_guardrails.py" - line: 1 - type: "refactor" - created: "2026-06-30T21:00:00Z" - updated: "2026-06-30T21:30:00Z" - mcp_optimized: true - - - uid: "task.maintenance.033" - title: "Update skill_mcp_self_heal for modular mcp_handlers package" - description: "Update self-heal skill to check domain modules in mcp_handlers/ package instead of monolithic file" - status: "done" - priority: "medium" - lane: "maintenance" - tags: ["p1", "tech-debt", "self-heal", "mcp"] - source: - file: "backend/app/skills/builtin/skill_mcp_self_heal.py" - line: 1 - type: "refactor" - created: "2026-06-30T21:00:00Z" - updated: "2026-06-30T21:30:00Z" - mcp_optimized: true - - - uid: "task.hivemind.013" - title: "Implement Hivemind MCP server (port 8490)" - description: "Build hivemind_server.py with agent orchestration, consensus engine, and LLM router. See HIVEMIND_ORCHESTRATION_SPEC.md" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p0", "hivemind", "mcp", "implementation"] - source: - file: "docs/HIVEMIND_ORCHESTRATION_SPEC.md" - line: 1 - type: "spec" - created: "2026-06-30T21:30:00Z" - updated: "2026-06-30T21:40:00Z" - mcp_optimized: true - - - uid: "task.hivemind.014" - title: "Implement plate refresh engine" - description: "Build plate_refresh/ engine with Cookiecutter rendering, Pydantic validation, drift detection, and DESTROY/REBUILD protocol. See PLATES_SYSTEM_SPEC.md" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p0", "plates", "implementation", "recovery"] - source: - file: "docs/PLATES_SYSTEM_SPEC.md" - line: 1 - type: "spec" - created: "2026-06-30T21:30:00Z" - updated: "2026-06-30T21:48:00Z" - mcp_optimized: true - - # === NEW: Vault Plates & Enhanced DESTROY Tasks === - - - uid: "task.vault.001" - title: "Add vault domain to PlateMeta and create vault plates" - description: "Add 'vault' domain to PlateMeta domain Literal, create user vault seed plate, shared workspace plate, and public publishing framework plate" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p0", "vault", "plates", "destroy"] - source: - file: "docs/VAULT_PLATES_AND_DESTROY_SPEC.md" - line: 1 - type: "spec" - created: "2026-06-30T22:37:00Z" - updated: "2026-06-30T22:40:00Z" - mcp_optimized: true - - - uid: "task.vault.002" - title: "Create Vault Discovery Skill" - description: "Build VaultDiscoverySkill (BaseSkill subclass) with dry-run mode, Nugget extraction, and vault layer scanning" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p0", "vault", "discovery", "skill"] - source: - file: "backend/app/skills/builtin/skill_vault_discovery.py" - line: 1 - type: "implementation" - created: "2026-06-30T22:37:00Z" - updated: "2026-06-30T22:40:00Z" - mcp_optimized: true - - - uid: "task.vault.003" - title: "Implement enhanced DESTROY interactive menu" - description: "Add interactive DESTROY menu with 6 options (dry-run, destroy & rebuild, destroy user data, destroy all, nuggets, cancel) to plate_refresh CLI" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p0", "destroy", "interactive", "cli"] - source: - file: "backend/plate_refresh/refresh.py" - line: 1 - type: "implementation" - created: "2026-06-30T22:37:00Z" - updated: "2026-06-30T22:40:00Z" - mcp_optimized: true - - - uid: "task.vault.004" - title: "Create vendor/ directory for distribution alignment" - description: "Create vendor/ directory structure with dist/ subdirectories and sources.yaml for GitHub pull definitions" - status: "done" - priority: "medium" - lane: "maintenance" - tags: ["p1", "vendor", "distribution", "packaging"] - source: - file: "vendor/sources.yaml" - line: 1 - type: "implementation" - created: "2026-06-30T22:37:00Z" - updated: "2026-06-30T22:40:00Z" - mcp_optimized: true - - - uid: "task.vault.005" - title: "Write VAULT_PLATES_AND_DESTROY_SPEC.md" - description: "Comprehensive spec for vault plates, vault discovery skill, enhanced DESTROY interactive options, and distribution alignment" - status: "done" - priority: "high" - lane: "maintenance" - tags: ["p0", "spec", "vault", "destroy"] - source: - file: "docs/VAULT_PLATES_AND_DESTROY_SPEC.md" - line: 1 - type: "spec" - created: "2026-06-30T22:37:00Z" - updated: "2026-06-30T22:40:00Z" - mcp_optimized: true - - - uid: "task.vault.006" - title: "Wire DistributionSystem to GitHub pulls" - description: "Implement actual GitHub pull logic in DistributionSystem using vendor/sources.yaml definitions" - status: "done" - priority: "medium" - lane: "maintenance" - tags: ["p1", "distribution", "github", "implementation"] - source: - file: "backend/app/services/distribution_system/distribution_system.py" - line: 1 - type: "implementation" - created: "2026-06-30T22:37:00Z" - updated: "2026-06-30T22:53:00Z" - mcp_optimized: true - notes: "DistributionSystem at backend/app/services/distribution_system/distribution_system.py with GitHub pull logic using vendor/sources.yaml definitions, install/update/repair/remove operations, and health checks." - - - uid: "task.vault.007" - title: "Wire PackageManager to plate system" - description: "Connect PackageManager to plate system for install/update/repair via plates" - status: "done" - priority: "medium" - lane: "maintenance" - tags: ["p1", "packaging", "plates", "implementation"] - source: - file: "backend/app/services/package_manager/package_manager.py" - line: 1 - type: "implementation" - created: "2026-06-30T22:37:00Z" - updated: "2026-06-30T22:53:00Z" - mcp_optimized: true - notes: "PackageManager at backend/app/services/distribution_system/package_manager.py wired to plate system with install/update/repair/remove operations, plate creation from packages, and health checks." - -archive: - - uid: "task.archive.001" - title: "Old Phase 6-8 tasks (UDW-001 through UDW-034)" - archived_reason: "completed" - archived_date: "2026-06-28T10:00:00Z" - notes: "All UDW tasks marked as done in UNIFIED_DEV_TASK_WORKFLOW.md" diff --git a/.tasker/README.md b/.tasker/README.md deleted file mode 100644 index 8bcafce7..00000000 --- a/.tasker/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# 📋 uCore Dev Plan — Tasker Boards - -**Canonical Source:** `.tasker.dev-flow.yaml` (25 tasks: 20 maintenance, 5 UI) - -This directory contains active tasker files. Completed tasks are archived to `.tasker.archived/`. - -## Structure - -| Directory | Description | -| ---------- | ----------------------------------- | -| `backlog/` | Archived backlog items (superseded) | -| `phases/` | Archived phase files (superseded) | - -## Template System - -**Dev Mode templates:** `.dev/templates/` (default, fallback configurations) - -## Recovery Scripts - -**Reset scripts:** `.dev/scripts/` - -- `backup-pre-reset.sh` — Backup before reset -- `reset-to-default.sh` — Full reset to template defaults -- `activate-openrouter-fallback.sh` — Emergency fallback activation - -## MCP Integration - -Use `tasker_devlog_bridge` skill for: - -- `sync` — Sync .tasker with devlog.mcp.yaml and spool -- `read` — Read current tasker/devlog state -- `archive` — Archive completed tasks older than N days -- `purge` — Purge legacy completed documentation - -## Capability Preflight Flow (Stop-The-Line) - -For any feature task that depends on tools, plugins, repos, variables, or secrets: - -1. Run capability preflight before implementation or runtime action. -2. If preflight fails (repair_required=true or HTTP 412), pause execution. -3. Complete listed repair actions (install, configure, set variable/secret, restart). -4. Re-run preflight and continue only when ready=true. -5. Include preflight output and repair evidence in task handover notes. diff --git a/.tasker/UNIFIED_DEV_TASK_WORKFLOW.md b/.tasker/UNIFIED_DEV_TASK_WORKFLOW.md deleted file mode 100644 index 058b6791..00000000 --- a/.tasker/UNIFIED_DEV_TASK_WORKFLOW.md +++ /dev/null @@ -1,109 +0,0 @@ -# uCore Unified Dev Task Workflow -- status: archived -- source: ucore-dev -- source_id: unified-dev-workflow-20260621 -- synced_at: 2026-06-21T23:59:59Z -- archived: 2026-06-28T10:02:00Z - -## ⚠️ ARCHIVED - See .tasker.dev-flow.yaml - -This file has been superseded by `.tasker.dev-flow.yaml` which contains the consolidated active task list. - -All UDW-001 through UDW-034 tasks are now archived in `.tasker.archived/`. - -## Purpose - -This file was the single source of truth for active development planning, handover actions, and backlog intake. - -## Workflow Stages - -1. Intake: collect tasks from handovers, checklists, incidents. -2. Normalize: map each item to a task id and owner lane. -3. Prioritize: classify as P0, P1, P2. -4. Execute: update status in task files and this index. -5. Verify: attach test or endpoint verification evidence. -6. Close: mark done and move summary into wisdom.md. - -## Active Work Queue (Consolidated) - -| ID | Status | Priority | Lane | Source | Summary | -|---|---|---|---|---|---| -| UDW-001 | done | P1 | System | Phase 6 | Wire S310 capture and cleanup into overnight maintenance chain | -| UDW-035 | done | P1 | MCP | Phase 5 | Deploy mcp-browser (Consolidated to Playwright) MCP server on port 8931 | -| UDW-036 | done | P1 | MCP | Phase 5 | Deploy mcp-playwright MCP server on port 8931 | -| UDW-037 | done | P1 | MCP | Phase 5 | Deploy mcp-firewatch MCP server on port 8932 -| UDW-038 | done | P1 | MCP | Phase 5 | Deploy mcp-secrets MCP server on port 8933 -| UDW-039 | done | P1 | MCP | Phase 5 | Deploy mcp-scheduler MCP server on port 8935 -| UDW-040 | done | P1 | MCP | Phase 5 | Deploy mcp-serena MCP server on port 8936 -| UDW-041 | done | P1 | MCP | Phase 5 | Create MCP server dashboard with real-time status | -| UDW-002 | done | P1 | System | Phase 6 | Add spool rotation and cleanup to maintenance scheduler | -| UDW-003 | done | P2 | System UI | Phase 6 | Expose tray and system state in maintenance model/view | -| UDW-004 | done | P1 | MCP | Phase 7 | Add clipboard MCP operations through api/mcp/call | -| UDW-005 | done | P1 | MCP | Phase 7 | Add knowledge MCP operations for workspace, docs, search | -| UDW-006 | done | P1 | Tasker | Phase 7 | Add tasker MCP operations for board/task read and write | -| UDW-007 | done | P1 | Automation | Phase 7 | Schedule tasker sync after vault sync in maintenance chain | -| UDW-008 | done | P1 | AI Runtime | Handover perf | Keep local default on qwen2.5-coder:7b-instruct-q4_K_M | -| UDW-009 | done | P1 | AI Runtime | Handover perf | Route medium and complex workloads to OpenRouter | -| UDW-010 | done | P1 | AI Runtime | Handover perf | Implement local SQLite chat cache in extension/tooling path | -| UDW-011 | done | P1 | AI Runtime | Handover perf | Ensure OpenRouter to local fallback behavior is reliable | -| UDW-012 | done | P2 | Observability | Handover perf | Track latency and cache hit ratio in recurring checks | -| UDW-013 | done | P1 | Docs | Next rounds | Define DocLang bridge export format for AppFlowy and vault | -| UDW-014 | done | P1 | Docs | Next rounds | Add transform step for AI-efficient structured context | -| UDW-015 | done | P2 | Memory | Next rounds | Extend brain sync inputs with spool, AppFlowy, test-failures | -| UDW-016 | done | P2 | Memory | Next rounds | Add episodic log runbook for durable correction history | -| UDW-017 | done | P1 | UI | Next rounds | Add global shortcut open for clipboard panel | -| UDW-018 | done | P2 | UI | Next rounds | Extend maintenance orchestration with richer cleanup tasks | -| UDW-019 | done | P1 | UI | Next rounds | Add clipboard orchestration system page | -| UDW-020 | done | P1 | UI | Next rounds | Add knowledge-local and AppFlowy tools system page | -| UDW-021 | done | P2 | UI | Next rounds | Add migration and consolidation dashboard view | -| UDW-022 | done | P2 | Workflow | Next rounds | Add direct Kanban launch and health actions in S300 | -| UDW-023 | done | P1 | Workflow | Next rounds | Add task detail and board actions in S300 and Developer | -| UDW-024 | done | P2 | Orchestration | Backlog | Complete Cline plus Roundtable orchestration hardening | -| UDW-025 | done | P2 | AI UX | Backlog | Build AI provider manager system page | -| UDW-026 | done | P2 | CLI | Backlog | Build uc CLI for server control and skill execution | -| UDW-027 | done | P1 | Knowledge | Consolidation | Add AppFlowy index coverage endpoint for per-source expected versus indexed tracking | -| UDW-028 | done | P1 | Automation | Consolidation | Add launchd installer script for scheduled AppFlowy import runs | -| UDW-029 | done | P1 | Knowledge | Consolidation | Add workspace mapping script to bind source entries to discovered AppFlowy workspace IDs | -| UDW-030 | done | P1 | UI | Consolidation | Remove legacy GridCore/Labs surface links and reduce active UI surface footprint | -| UDW-031 | done | P1 | UI | Consolidation | Add mission drop panel ingest flow for binder and mission processing | -| UDW-032 | done | P0 | Frontend/Developer | Phase 9B | Fix Developer surface runtime wiring and global task drawer query routing | -| UDW-033 | done | P0 | QA | Phase 9B | Add targeted tests for Phase 9 workflow and migration endpoints | -| UDW-034 | done | P0 | QA | Phase 9B | Validate Phase 9B frontend build, backend workflow tests, and AI stack health | - -## Execution Order - -1. UDW-001 through UDW-007 -2. UDW-008 through UDW-012 -3. UDW-019 through UDW-023 -4. UDW-013 through UDW-018 -5. UDW-024 through UDW-026 -6. UDW-032 through UDW-034 - -## Source Mapping - -- Phase files: - - .tasker/phases/in-progress-phase-6-snackbar-orch-b4c8d1e3.md - - .tasker/phases/todo-phase-7-mcp-tasker-d6e779f1.md -- Backlog files: - - .tasker/backlog/todo-cline-orchestration-c901d2e3.md - - .tasker/backlog/todo-ai-provider-manager-a12b34c5.md - - .tasker/backlog/todo-ucore-cli-b567d890.md -- Handover sources: - - HANDOVER.md - - docs/HANDOVER_UDOS_LOCAL_MODEL_PERFORMANCE.md -- Checklist source: - - docs/NEXT_ROUNDS_CHECKLIST.md -- Phase 9B sources: - - CLINE_BRIEF.md - - HANDOVER.md - - wisdom.md -- Archive snapshot: - - docs/archive/plans/2026-06-21-plan-migration-snapshot.md - -## Update Protocol - -1. Edit task detail in phase or backlog file. -2. Update corresponding UDW row status here. -3. Run validation: - - bash scripts/ai_stack_health_check.sh -4. Record significant outcomes in wisdom.md. diff --git a/.tasker/admin/review-organize-life-admin-docs-seed-organize-life-admin-docs.md b/.tasker/admin/review-organize-life-admin-docs-seed-organize-life-admin-docs.md deleted file mode 100644 index 37f49cc9..00000000 --- a/.tasker/admin/review-organize-life-admin-docs-seed-organize-life-admin-docs.md +++ /dev/null @@ -1,14 +0,0 @@ -# Organize life admin docs - -- status: review -- source: seed-user-workflow -- source_id: seed-organize-life-admin-docs -- synced_at: 2026-07-15T13:47:33Z -- priority: medium -- mission: Life Admin -- task: Organize life admin docs -- binder: records -- tags: admin, records - -## Summary -Collect invoices, reminders, and account docs. diff --git a/.tasker/archive/CSS_CENTRALIZATION_PLAN.md b/.tasker/archive/CSS_CENTRALIZATION_PLAN.md deleted file mode 100644 index 2fcb1ea8..00000000 --- a/.tasker/archive/CSS_CENTRALIZATION_PLAN.md +++ /dev/null @@ -1,73 +0,0 @@ -# CSS Centralization Plan - uCore UI System - -## Current Problem -We have 9+ CSS files defining overlapping styles: -- `nestframe.css` - Base surface layout -- `global-toolbar.css` - Toolbar specific -- `usx-pico-integration.css` - Pico nav integration (also defines toolbar) -- `assistui.css` - AssistUI specific (also defines topbar) -- `vault-sidebar.css` - Sidebar -- Individual surface CSS (developer.css, workflow.css, etc) -- Typography-global-apply.css - -**Result**: Conflicts, cascading issues, sidebar not showing, styles not applying - -## Solution: Centralized Layout System - -### New File Hierarchy (in main.tsx import order): - -1. **usx-layout-system.css** (NEW - CORE) - - All shared layout patterns - - Global toolbar/topbar standardization (24px padding) - - Main content padding - - Sidebar spacing - - No color/theme (defer to Pico) - -2. **usx-pico-integration.css** - - Pico variable integration only - - Remove toolbar/nav duplication - - Link Pico colors to components - -3. **Surface-specific CSS** - - Only component-specific styling - - No layout rules - - No toolbar/main/sidebar patterns - -### Key Principles: - -1. **Single Toolkit (usx-layout-system.css)** - - `.global-toolbar` - 24px sides, flex row - - `.usx-surface-main` - 24px padding - - `.vault-sidebar-wrapper` - standard width - - `.assistui-topbar` - inherits from .global-toolbar base - - All surfaces use same layout classes - -2. **No Duplication** - - Remove toolbar styles from: global-toolbar.css, usx-pico-integration.css, assistui.css - - Remove layout from surface-specific CSS - - Source of truth: usx-layout-system.css - -3. **Import Order (main.tsx)** - ``` - 1. nestframe.css (Pico base) - 2. usx-layout-system.css (LAYOUT ONLY) - 3. usx-pico-integration.css (COLORS/TOKENS) - 4. surface-specific CSS - 5. typography-global-apply.css - ``` - -### Files to Create/Modify - -**CREATE:** -- `usx-layout-system.css` - Centralized layout patterns - -**REFACTOR:** -- `global-toolbar.css` → Remove all styling, import from usx-layout-system -- `usx-pico-integration.css` → Keep only Pico color/token integration -- `assistui.css` → Remove toolbar/layout, use base classes - -**RESULT:** -- Single source of truth for layout -- Predictable CSS cascade -- Easy to modify spacing globally -- Surfaces inherit consistent patterns diff --git a/.tasker/archive/TOPBAR_CLEANUP_FINAL.md b/.tasker/archive/TOPBAR_CLEANUP_FINAL.md deleted file mode 100644 index 2798f0ed..00000000 --- a/.tasker/archive/TOPBAR_CLEANUP_FINAL.md +++ /dev/null @@ -1,211 +0,0 @@ -# Global Topbar Redesign & Typography System — Final Summary - -**Status**: ✅ COMPLETE -**Date**: 2026-06-24 -**Build**: Successful ✓ - ---- - -## What Was Accomplished - -### 1. ✅ Topbar Blue Box Removal & Flat Design - -**Before**: Nav tabs and header icons had blue background boxes + border remnants -**After**: Clean flat design with color-only active states - -**Changes**: -- **`global-toolbar.css`**: - - `.global-toolbar-nav`: Added vertical padding (0.5rem) for breathing room - - `.global-toolbar-nav-btn`: - - Font size reduced to **0.8em** (smaller, cleaner) - - Padding tightened (0.25rem → 0.5rem horizontal) - - Border-radius removed (0 → flat underline style) - - Active state: **color only** (blue underline on text, no background) - -- **`nestframe.css`**: - - `.usx-header-btn`: - - Border-radius removed (0 → flat/no rounding) - - Removed hover background change (flat color only) - - Active state: **color only** (no border, no background) - - Removed all background transitions (color-only transitions) - -**Result**: Professional flat design, no blue boxes, clean underlines - ---- - -### 2. ✅ Global Typography System (Complete) - -**File**: `usx-typography-scale.css` (380+ lines) - -**Hierarchy**: -- Display: 44px -- H1: 32px -- H2: 24px -- H3: 20px -- Body: 14px -- **Meta: 11.2px** (0.8em of Body) - -**Responsive Scaling**: -- **Desktop (1025px+)**: 100% — Full 10-foot ideal size -- **Tablet (768-1024px)**: 80% — Proportionally smaller -- **Mobile (≤767px)**: 65% — Compact for phones - -**Runtime Switching**: `data-typography-scale` attribute -- `"10-foot"` — Force large display scale -- `"desktop-compact"` — Force 0.8x scale -- `"mobile-compact"` — Force 0.65x scale -- Remove attribute → Auto responsive detection - ---- - -### 3. ✅ Style Conflicts Audit (Documented) - -**Found & Documented**: -- 68 hardcoded font-size declarations -- 13 hardcoded blue background remnants (rgba(13, 110, 253, 0.15)) - -**Files Affected**: -- surfaces/developer.css (15+ instances) — Partially fixed -- surfaces/ucode.css (7 instances) -- assistui.css (5 instances) -- hub/dashboard.css (4+ instances) -- hub/settings.css (2 instances) -- gridui-terminal.css (8 instances) - -**Audit Document**: `.tasker/style-conflicts-audit.md` (Complete roadmap for remaining fixes) - ---- - -### 4. ✅ Developer Surface Cleanup (Started) - -**Fixed in `surfaces/developer.css`**: -- `.developer-preview-title`: Now uses `var(--usx-font-size-body)` + `var(--usx-font-weight-heading)` -- `.developer-preview-subtitle`: Now uses `var(--usx-font-size-meta)` -- `.developer-preview-toggle`, `.developer-preview-save`: Now use `var(--usx-font-size-meta)` - ---- - -## Files Modified - -| File | Changes | -|------|---------| -| `global-toolbar.css` | ✓ Nav padding, font size 0.8em, flat design | -| `nestframe.css` | ✓ Header button flat design, no boxes | -| `usx-typography-scale.css` | ✓ Created (380 lines) | -| `surfaces/developer.css` | ✓ Partial cleanup (3 key elements) | -| `TYPOGRAPHY_USAGE.md` | ✓ Created (200+ lines) | -| `.tasker/typography-cleanup-summary.md` | ✓ Created | -| `.tasker/style-conflicts-audit.md` | ✓ Created (complete audit) | - ---- - -## Build Status - -``` -✓ vite build succeeded -✓ 1881 modules transformed -✓ CSS: 252.01 kB (gzip: 35.34 kB) -✓ JS: 1,507.53 kB (gzip: 329.34 kB) -✓ No errors or breaking changes -``` - ---- - -## Topbar Final Design - -### Visual Changes - -✅ **Nav Tabs**: -- Smaller text (0.8em) -- More breathing room (0.5rem padding top/bottom) -- Flat design (no rounded corners) -- Blue underline on active (no background box) -- Color-only hover state - -✅ **Header Icons**: -- No borders -- No backgrounds -- Flat color only -- Color change on hover/active -- Professional appearance - -✅ **Overall**: -- Clean, minimal aesthetic -- Consistent underline indicators -- Proper spacing throughout -- GitHub-style flat design - ---- - -## Typography System Features - -### Usage Methods - -**1. CSS Variables** (Recommended): -```css -.my-title { - font-size: var(--usx-font-size-h2); - font-weight: var(--usx-font-weight-heading); -} -``` - -**2. Utility Classes**: -```html -

Title

-

Content

-Supporting -``` - -**3. HTML Element Defaults**: -```html -

Automatic — uses h1 scale

-

Automatic — uses body scale

-``` - -### Runtime Switching - -```javascript -// Force 10-foot (TV mode) -document.documentElement.setAttribute('data-typography-scale', '10-foot'); - -// Force compact desktop -document.documentElement.setAttribute('data-typography-scale', 'desktop-compact'); - -// Reset to auto -document.documentElement.removeAttribute('data-typography-scale'); -``` - ---- - -## Remaining Work (Out of Scope) - -The audit document provides a complete roadmap for cleaning up the remaining 68 hardcoded font-sizes and 13 blue background remnants across: -- surfaces/ucode.css -- assistui.css -- hub/dashboard.css -- gridui-terminal.css -- and others - -Each can be fixed using the same pattern shown in developer.css. - ---- - -## Key Achievements - -🎯 **Topbar**: Complete redesign to flat, professional look -🎯 **Typography**: Global system with responsive scaling -🎯 **Documentation**: Complete usage guide + audit roadmap -🎯 **Build**: Successful, no breaking changes -🎯 **Flexibility**: Runtime switching + flexible variables -🎯 **Accessibility**: Respects user preferences - ---- - -## Next Steps - -1. Use audit document to complete remaining surface fixes -2. Test responsive scaling across devices -3. Monitor for any remaining style conflicts -4. Consider running periodic audits (quarterly) - -All documentation and roadmaps are in place for future work. diff --git a/.tasker/archive/TOPBAR_TYPOGRAPHY_REFINEMENT.md b/.tasker/archive/TOPBAR_TYPOGRAPHY_REFINEMENT.md deleted file mode 100644 index cd591c4d..00000000 --- a/.tasker/archive/TOPBAR_TYPOGRAPHY_REFINEMENT.md +++ /dev/null @@ -1,283 +0,0 @@ -# Topbar & Typography Settings Refinement — Implementation Summary - -**Status**: ✅ COMPLETE -**Date**: 2026-06-24 -**Build**: Successful ✓ - ---- - -## Objectives Completed - -### 1. ✅ Global Topbar Icon Spacing & Flat Design - -**Problem**: Icons were too close together with bright blue borders/coloring from Pico color set. - -**Solution**: Refined `global-toolbar.css` with improved spacing and flat design: - -- **Gap increased**: `0.25rem` → `var(--usx-spacing-sm)` (8px) -- **Padding standardized**: Uses USX spacing scale variables -- **Font size**: Set to `0.8em` for cleaner, professional look -- **Flat design**: Border-radius remains `0` (flat), no background fills -- **Color transitions**: Smooth 100ms ease for hover/active states -- **No bright blue boxes**: Only text color changes on hover/active, no backgrounds or glows - -**File Changed**: `frontend/src/styles/global-toolbar.css` - ---- - -### 2. ✅ Developer Surface Typography Settings Panel - -**Created**: Complete font management UI in Developer Surface Settings tab - -**Features**: -- **Font Family Selector** (3 options): - - Inter (modern sans-serif) - - Merriweather (editorial serif) - - JetBrains Mono (monospace code) -- **Font Size Controls**: - - Preset buttons: 12px, 13px, 14px, 15px, 16px, 18px, 20px - - Range slider: 10–24px with smooth increments - - Live size display -- **Live Preview**: - - Real-time preview box with editable text - - Demonstrates selected font family and size -- **Persistence**: - - Stored in localStorage with key `ucore-typography-settings` - - Applied via CSS variables to document root - - Survives page reloads and browser sessions -- **Global Application**: - - Sets `--usx-font-family-override` CSS variable - - Sets `--usx-font-size-override` CSS variable - - Applies `data-typography-family` attribute for scoped styling - -**Files Created**: -1. `frontend/src/surfaces/developer/TypographySettingsPanel.tsx` (169 lines) - - React component with state management - - localStorage integration - - CSS variable application logic -2. `frontend/src/surfaces/developer/typography-settings.css` (334 lines) - - Complete styling for typography controls - - Font family grid layout (responsive) - - Font size preset buttons - - Range slider styling (cross-browser) - - Live preview box styling - - Dark/light mode support - - Mobile responsive design - -**Files Modified**: -1. `frontend/src/surfaces/developer/DeveloperSurface.tsx` - - Imported `TypographySettingsPanel` - - Integrated into Settings tab (accessible via `Developer → Settings`) - ---- - -### 3. ✅ Architecture & Design Decisions - -**Inheritance Model**: Font settings inherit from System Settings (as requested) -- Typography settings apply globally across all USX surfaces -- No per-surface overrides needed - -**Storage Strategy**: localStorage (no server-side storage) -- User preference persists across sessions -- No backend changes required -- Clean slate for new browsers/users - -**CSS Variable Application**: -```css -/* On document root */ -document.documentElement.style.setProperty('--usx-font-family-override', fontFamily) -document.documentElement.style.setProperty('--usx-font-size-override', '14px') -document.documentElement.setAttribute('data-typography-family', 'inter') -``` - -**Flat Design Philosophy**: -- ✓ No shadows, glows, or animations (as requested) -- ✓ Minimal borders and backgrounds -- ✓ Color-only state changes on hover/active -- ✓ Consistent with GitHub dark aesthetic - ---- - -## Archive Analysis - -### Historical Context from `.tasker/TOPBAR_CLEANUP_FINAL.md` - -Previous work established: -- Nav tabs: smaller text (0.8em), breathing room (0.5rem padding), flat design -- Header icons: no borders, no backgrounds, color-only states -- Professional flat appearance without blue boxes - -**This work** extends that foundation with: -- ✓ Improved gap spacing using USX scale variables (not hardcoded) -- ✓ Typography system integration -- ✓ User-switchable fonts in Developer Settings - ---- - -## Build Status - -``` -✓ vite build succeeded -✓ 1883 modules transformed -✓ CSS: 258.48 kB (gzip: 36.15 kB) -✓ JS: 1,511.91 kB (gzip: 330.40 kB) -✓ No errors or breaking changes -``` - ---- - -## Files Changed Summary - -| File | Type | Changes | -|------|------|---------| -| `frontend/src/styles/global-toolbar.css` | Modified | Improved spacing, flat design, CSS variables | -| `frontend/src/surfaces/developer/TypographySettingsPanel.tsx` | Created | 169 lines, font controls, localStorage persistence | -| `frontend/src/surfaces/developer/typography-settings.css` | Created | 334 lines, responsive UI styling | -| `frontend/src/surfaces/developer/DeveloperSurface.tsx` | Modified | Import & integrate TypographySettingsPanel | - ---- - -## How to Use - -### For End Users - -1. **Access Settings**: - - Navigate to Developer Surface → Settings tab - - Scroll to Typography Settings section - -2. **Select Font Family**: - - Click one of three font cards: Inter, Merriweather, or JetBrains Mono - - Preview displays immediately with selected font - -3. **Adjust Font Size**: - - Click preset buttons (12px–20px) for quick changes - - Or drag slider for fine-grained control (10–24px) - - Size updates in real time - -4. **Settings Persist**: - - Changes automatically saved to browser localStorage - - Applies globally across all surfaces - - Persists across sessions and page reloads - -### For Developers - -**Accessing stored settings**: -```javascript -const settings = JSON.parse(localStorage.getItem('ucore-typography-settings')) -// { fontFamily: 'inter', fontSize: 14 } -``` - -**CSS variables available** (on document root): -```css ---usx-font-family-override: 'Inter, system-ui, -apple-system, sans-serif' ---usx-font-size-override: '14px' -``` - -**Data attribute for scoped styling**: -```html - - -``` - ---- - -## Design Philosophy Adherence - -### ✅ Flat Styles (No Animation, Glow, Shadow) -- Topbar buttons: color-only transitions -- Settings UI: minimal borders, flat backgrounds -- No box-shadows or glows anywhere -- Smooth 100ms ease transitions for UX polish - -### ✅ Icon Spacing Improvements -- Buttons now use `var(--usx-spacing-sm)` = 8px gap (from 4px) -- Breathing room makes navbar less cramped -- Consistent with GitHub dark aesthetic - -### ✅ Typography System Integration -- Font choices: Inter (modern), Merriweather (editorial), JetBrains Mono (code) -- Sizes: 10–24px range covers all UI needs -- Live preview gives users confidence in choices - -### ✅ Developer Surface Settings Tab -- Single location for all typography controls -- Inherits from System Settings architecture -- Persistent across sessions - ---- - -## Testing Notes - -**Build Verification**: -- ✓ No TS errors -- ✓ All modules transform correctly -- ✓ CSS properly scoped -- ✓ localStorage integration functional -- ✓ CSS variables apply to document root - -**Manual Testing Recommended**: -1. Navigate to Developer → Settings -2. Verify Typography Settings panel renders -3. Test each font family button -4. Drag font size slider (should update live) -5. Edit preview text (should display with current font) -6. Refresh page (settings should persist) -7. Check global topbar spacing (icons properly spaced) - ---- - -## Next Steps (Out of Scope) - -Future work could include: -- Per-surface typography overrides (if needed) -- Theme color picker integration -- Export/import settings -- Server-side settings sync -- Material3 icon library alternative (currently Lucide-only) - ---- - -## Key Achievements - -🎯 **Topbar**: Improved icon spacing with flat design -🎯 **Typography**: Full font family & size controls in Developer Settings -🎯 **Persistence**: Browser-native localStorage (no backend needed) -🎯 **UX**: Live preview, responsive design, dark/light mode support -🎯 **Build**: Zero breaking changes, successful production build -🎯 **Documentation**: Clear implementation for future maintenance - ---- - -## Architecture Diagram - -``` -User Interaction - ↓ -TypographySettingsPanel.tsx - ↓ -localStorage.setItem('ucore-typography-settings') - ↓ -applyTypographySettings() - ↓ -document.documentElement.style.setProperty() → CSS Variables -document.documentElement.setAttribute() → data- attribute - ↓ -Global Application Across All Surfaces -``` - ---- - -## Standards Compliance - -✓ Follows `.clinerules` (local-first, auditable, no network) -✓ Uses USX spacing scale variables (not hardcoded) -✓ Maintains Pico CSS integration -✓ Respects user preferences (no forced changes) -✓ Flat design (matches GitHub dark aesthetics) -✓ Accessible (proper contrast, keyboard support) - ---- - -**Implementation Date**: 2026-06-24 -**Completed By**: Cline -**Status**: Ready for Production ✓ diff --git a/.tasker/archive/plan-server-workflow-split.md b/.tasker/archive/plan-server-workflow-split.md deleted file mode 100644 index d25d9d22..00000000 --- a/.tasker/archive/plan-server-workflow-split.md +++ /dev/null @@ -1,13 +0,0 @@ -# Plan: Server/System/Workflow Surface Split - -## Issues Found -1. UServerSurface is 1892-line monolithic file with 14 tabs mixing backend ops and admin config -2. WorkflowSurface has mock/static data, needs real API wiring -3. No workflow-specific tasker API endpoint -4. No deduplicated SystemSurface - -## Plan -1. **Backend**: Add `/api/workflow/tasks` endpoint filtering `.tasker/workflow/` and `.tasker/gridsmith/` -2. **SystemSurface**: Extract `pages`, `tools`, `secrets`, `settings` tabs from UServerSurface into new top-level `/system` surface -3. **UServerSurface**: Cleaned to 10 tabs (dashboard, ingest, story, services, logs, workflows, budget, agents, snacks) -4. **WorkflowSurface**: Wire up with real API calls, add detail panel, add right-panel editor \ No newline at end of file diff --git a/.tasker/archive/style-conflicts-audit.md b/.tasker/archive/style-conflicts-audit.md deleted file mode 100644 index c85c4600..00000000 --- a/.tasker/archive/style-conflicts-audit.md +++ /dev/null @@ -1,204 +0,0 @@ -# Style Conflicts Audit — Hardcoded Font-Sizes & Blue Remnants - -**Scope**: Found 68 hardcoded font-size declarations conflicting with new typography system -**Date**: 2026-06-24 - ---- - -## Critical Findings - -### 1. Hardcoded Font-Sizes (68 instances) - -These override the global typography system and should be using CSS variables: - -#### **assistui.css** -```css -.assistui-prompt-card-icon { font-size: 18px; } → Should use --usx-font-size-display -.assistui-prompt-card-label { /* inherits 14px */ } → Should use --usx-font-size-body -.assistui-model-btn { font-size: 13px; } → Should use --usx-font-size-body or --usx-font-size-meta -``` - -#### **surfaces/developer.css** (Most conflicts here) -```css -.developer-preview-title { font-size: 13px; } → Use --usx-font-size-h3 -.developer-preview-subtitle { font-size: 11px; } → Use --usx-font-size-meta -.developer-skill-title { font-size: 12px; } → Use --usx-font-size-body -.developer-chat-prompt-icon { font-size: 18px; } → Use --usx-font-size-h2 or display -.kanban-detail-meta-text { font-size: 11px; } → Use --usx-font-size-meta -.kanban-detail-tag { font-size: 10px; } → Use --usx-font-size-meta (0.8em) -.diff-editor-simple-label { font-size: 11px; } → Use --usx-font-size-meta -``` - -#### **surfaces/ucode.css** -```css -.ucode-tool-card-title { font-size: 13px; } → Use --usx-font-size-h3 -.ucode-tool-card-subtitle { font-size: 11px; } → Use --usx-font-size-meta -.ucode-tool-btn { font-size: 12px; } → Use --usx-font-size-body -``` - -#### **gridui-terminal.css** (Grid-specific) -```css -.gridui-teletext-msg { font-size: 16px; } → Keep (grid-specific override) -.gridui-teletext-poll-time { font-size: 10px; } → Use --usx-font-size-meta -.gridui-teletext-nav-label { font-size: 14px; } → Use --usx-font-size-body -``` - ---- - -### 2. Blue Background Remnants (13 instances) - -These should use semantic Pico variables instead of hardcoded blue: - -**Pattern**: `background: rgba(13, 110, 253, 0.15);` ← Should use Pico variables - -#### **hub/settings.css** -```css -.hub-settings-fontsize-btn--active { - background: rgba(13, 110, 253, 0.15); → Use var(--pico-primary-container) - border-color: var(--pico-primary, #58a6ff); -} -``` - -#### **hub/dashboard.css** -```css -.hub-card--pending { - background: rgba(13, 110, 253, 0.15); → Use var(--pico-primary-container) -} -``` - -#### **surfaces/developer.css** -```css -.developer-chat-msg--user .developer-chat-msg-content { - background: rgba(13, 110, 253, 0.15); → Should be consistent -} -``` - -#### **assistui.css** -```css -.assistui-agent-pill--active { - background: rgba(13, 110, 253, 0.15); → Use var(--pico-primary-container) -} -``` - -#### **nestframe.css** -```css -.usx-badge--blue { - background: rgba(13, 110, 253, 0.15); → Already defined, but check consistency -} -``` - ---- - -## Recommended Actions - -### Phase 1: Replace Hardcoded Font-Sizes (High Priority) - -Map hardcoded sizes to typography variables: - -| Hardcoded | → | Variable | Notes | -|-----------|---|----------|-------| -| 18px | → | `var(--usx-font-size-display)` | Display/hero text | -| 16px | → | `var(--usx-font-size-h1)` | Large labels | -| 14px | → | `var(--usx-font-size-body)` | Default text | -| 13px | → | `var(--usx-font-size-body)` | Slightly smaller body | -| 12px | → | `var(--usx-font-size-body)` | Also body size | -| 11px | → | `var(--usx-font-size-meta)` | Supporting text | -| 10px | → | `var(--usx-font-size-meta)` | Also meta (0.8em) | - -### Phase 2: Clean Up Blue Backgrounds - -Replace all `rgba(13, 110, 253, 0.15)` with: -```css -background: var(--pico-primary-container, rgba(13, 110, 253, 0.15)); -``` - -This ensures: -1. Uses the system's primary container color -2. Falls back to blue if not defined -3. Makes it themeable - -### Phase 3: Remove Conflicting Declarations - -Files to audit and potentially remove redundant styles: -- `usx/usx-typography-prose.css` - Has font-size overrides that may conflict - ---- - -## Impact Analysis - -### Current Conflicts -- **68 instances** of hardcoded font-sizes override the typography system -- **13 instances** of hardcoded blue backgrounds not using Pico variables -- Typography scaling only works where no hardcoded size exists -- Active/hover states use inconsistent blue tonality - -### After Cleanup -✅ All text respects typography hierarchy -✅ All active/disabled states use consistent Pico variables -✅ System responds properly to `data-typography-scale` changes -✅ Easier to maintain — change one variable, affects everything -✅ Better theme support — respects user preferences - ---- - -## Files to Modify - -| File | Issue | Action | -|------|-------|--------| -| `surfaces/developer.css` | 15+ hardcoded sizes | Replace with variables | -| `gridui-terminal.css` | 8 hardcoded sizes | Replace with variables (except grid-specific) | -| `surfaces/ucode.css` | 7 hardcoded sizes | Replace with variables | -| `assistui.css` | 5 hardcoded sizes | Replace with variables | -| `hub/settings.css` | 2 blue backgrounds | Use var(--pico-primary-container) | -| `hub/dashboard.css` | 2 blue backgrounds + sizes | Replace both | -| `nestframe.css` | 1 blue background | Verify consistency | -| `usx/usx-typography-prose.css` | Font-size catch-alls | Review for conflicts | - ---- - -## Next Steps - -1. **Run audit on each file** — Identify which hardcoded sizes are intentional (grid/special) -2. **Create replacement plan** — Map each hardcoded size to appropriate variable -3. **Update files** — Replace hardcoded with CSS variables -4. **Test responsive scaling** — Verify `data-typography-scale` works correctly -5. **Verify theme consistency** — Ensure all active states use Pico variables -6. **Rebuild & verify** — Check visual consistency across viewports - ---- - -## Example Fixes - -### Before (Conflicting) -```css -.developer-preview-title { - font-size: 13px; ← Hardcoded, overrides system - font-weight: 600; -} -``` - -### After (Clean) -```css -.developer-preview-title { - font-size: var(--usx-font-size-h3); ← Uses system - font-weight: var(--usx-font-weight-heading); -} -``` - ---- - -## Severity Levels - -🔴 **Critical** (Breaks responsive scaling): -- All hardcoded font-sizes in display/interactive components -- Should be replaced immediately - -🟡 **Medium** (Hardcoded but acceptable): -- Grid-specific sizes (terminal, teletext) -- Monospace-specific adjustments -- Can be reviewed case-by-case - -🟢 **Low** (Good practice): -- Blue background hardcodes -- Should use Pico variables for consistency - diff --git a/.tasker/archive/typography-cleanup-summary.md b/.tasker/archive/typography-cleanup-summary.md deleted file mode 100644 index cfbb5556..00000000 --- a/.tasker/archive/typography-cleanup-summary.md +++ /dev/null @@ -1,255 +0,0 @@ -# Typography System Cleanup & Global Scaling — Completion Summary - -**Date Completed**: 2026-06-24 -**Scope**: Global topbar cleanup + comprehensive typography refactor -**Status**: ✅ COMPLETE - ---- - -## What Was Done - -### 1. ✅ Topbar Blue Border/Box Cleanup - -**Issue**: Global topbar had blue background boxes around active nav buttons and header icons instead of clean underline indicators. - -**Solution**: -- **nestframe.css** (.usx-header-btn.active): Removed `background: rgba(13, 110, 253, 0.15)` → Added `border-bottom: 2px solid var(--pico-primary)` -- **global-toolbar.css** (.global-toolbar-nav-btn.active): Removed solid background fill → Added `border-bottom-color: var(--pico-primary)` -- Both now use consistent underline pattern for active states - -**Files Modified**: -- `frontend/src/styles/nestframe.css` -- `frontend/src/styles/global-toolbar.css` - ---- - -### 2. ✅ Global Typography Variable System Created - -**New File**: `frontend/src/styles/usx/usx-typography-scale.css` - -**Features**: -- **Hierarchy**: Display/H1/H2/H3/Body/Meta (5 levels + meta support) -- **Meta = 0.8em of Body size** (11.2px when body is 14px) -- **All properties variablized**: font-size, font-weight, line-height, letter-spacing -- **Font families**: Configurable via `--usx-font-family-*` variables -- **1400+ lines of comprehensive typography system** - -**Key Variables**: -```css ---usx-font-size-display: 44px ---usx-font-size-h1: 32px ---usx-font-size-h2: 24px ---usx-font-size-h3: 20px ---usx-font-size-body: 14px ---usx-font-size-meta: calc(var(--usx-font-size-body) * 0.8) -``` - ---- - -### 3. ✅ Responsive Scaling Profiles Implemented - -**Three Automatic Breakpoints**: - -| Profile | Viewport | Scale | Behavior | -|---------|----------|-------|----------| -| **10-foot** | TV/Large display | 100% | Base ideal size (44px display) | -| **Desktop** | 1025px+ | 80% | 35.2px display, proportional down | -| **Tablet** | 768-1024px | 80% | Same as desktop for consistency | -| **Mobile** | ≤767px | 65% | 28.6px display, compact | - -**Runtime Override Support**: -```html - - - - -``` - ---- - -### 4. ✅ Integration into Global Layer - -**Expected Import Chain**: -```css -nestframe.css (main foundation) - ├─ @import 'pico.min.css' (Layer 1: Components) - ├─ @import 'prose-ui-standard.css' (Layer 2: Markdown) - ├─ @import 'usx/usx-typography-scale.css' (Layer 2.5: NEW - Typography) - ├─ USX Surface Layout (Layer 4 - existing) - └─ Pico Integration (Layer 5-6 - existing) -``` - -**All HTML elements automatically use the variables**: -- `

` → `var(--usx-font-size-h1)`, `var(--usx-font-weight-heading)`, etc. -- `

` → `var(--usx-font-size-body)`, `var(--usx-line-height-body)` -- `` → `var(--usx-font-size-meta)`, `var(--usx-line-height-meta)` -- `button`, `input`, `.badge`, `.card-title` → all use appropriate hierarchy - ---- - -### 5. ✅ Documentation Created - -**New File**: `frontend/src/styles/TYPOGRAPHY_USAGE.md` - -**Sections**: -- Quick start guide -- Typography hierarchy table -- 3 usage methods (variables, utility classes, element defaults) -- Complete CSS variables reference -- Runtime scaling guide with JavaScript examples -- Component typography reference (nav, buttons, badges, cards, dialogs, prose) -- Customization guide (font families, scale factors) -- Migration guide from old system -- Accessibility notes -- Troubleshooting section -- Browser support matrix - ---- - -## Files Modified - -### Direct Changes -1. **nestframe.css**: Added `usx-typography-scale.css` import + fixed `.usx-header-btn.active` styling -2. **global-toolbar.css**: Fixed `.global-toolbar-nav-btn` and `.global-toolbar-nav-btn.active` styling - -### Files Created -1. **usx-typography-scale.css**: 380+ lines of global typography system -2. **TYPOGRAPHY_USAGE.md**: Comprehensive usage documentation (200+ lines) - ---- - -## Key Improvements - -### Visual Consistency -✅ Blue boxes removed from topbar → clean underline indicators -✅ Font sizes now consistent across all surfaces -✅ Typography hierarchy clearly defined (Display → H1 → H2 → H3 → Body → Meta) -✅ All text uses measured hierarchy instead of random hardcoded sizes - -### Developer Experience -✅ CSS Variables for everything — no hardcoded font sizes -✅ Utility classes available for quick styling -✅ HTML elements automatically inherit correct scale -✅ Easy customization — change one variable, scales everywhere - -### Responsiveness -✅ Automatic scaling based on viewport (10-foot → 0.8x → 0.65x) -✅ Runtime override capability for special displays (TVs, kiosks, etc.) -✅ Media query breakpoints at industry standards - -### Switchability -✅ Runtime profiles via `data-typography-scale` attribute -✅ JavaScript API for switching at runtime -✅ Graceful fallback to automatic responsive behavior - ---- - -## Usage Examples - -### Using CSS Variables (Recommended) -```css -.section-title { - font-size: var(--usx-font-size-h2); - font-weight: var(--usx-font-weight-heading); - line-height: var(--usx-line-height-h2); -} -``` - -### Using Utility Classes -```html -

Section Title

-

Content here

-Supporting text -``` - -### Runtime Scaling -```javascript -// Force large display mode -document.documentElement.setAttribute('data-typography-scale', '10-foot'); - -// Switch to mobile compact -document.documentElement.setAttribute('data-typography-scale', 'mobile-compact'); - -// Reset to auto -document.documentElement.removeAttribute('data-typography-scale'); -``` - ---- - -## Testing Checklist - -- [x] Topbar buttons render without blue boxes -- [x] Topbar buttons show underline on active state -- [x] Typography hierarchy displays correctly on desktop (0.8x scale) -- [x] Mobile view shows correct scaling (0.65x) -- [x] Utility classes (`.usx-h1`, `.usx-body`, `.usx-meta`) apply correctly -- [x] CSS variables resolve in browser DevTools -- [x] Media queries trigger at correct breakpoints -- [x] Data-attribute override works (runtime switching) -- [x] All HTML elements inherit typography defaults -- [x] Components (buttons, badges, cards) follow hierarchy - ---- - -## Next Steps (Optional Future Work) - -1. **Surface Migration**: Apply utility classes to existing surfaces for consistency -2. **Pico Integration Review**: Verify nestframe/pico-integration work with new system -3. **Component Audit**: Scan all surfaces for inconsistent font sizes -4. **Design System Docs**: Add typography to design system documentation -5. **Testing**: Create visual regression tests for typography across viewports - ---- - -## Backward Compatibility - -✅ **No Breaking Changes**: -- Old hardcoded sizes still work (not removed) -- Nestframe structural layout unchanged -- All Pico.css styles still applied -- Existing surfaces function without modification -- Variables are **additive**, not replacements - -✅ **Gradual Adoption**: -- Use new variables in new code -- Migrate existing code when refactoring -- No need to update everything at once - ---- - -## Architecture Notes - -### Layer Structure -``` -1. Pico.css (base components) -2. Prose UI (markdown) -2.5. USX Typography Scale (NEW - global font sizing) -3. NestFrame Grid (layout) -4. USX Surface Layout (app shell) -5. Pico Integration (overrides) -6. Service-specific styles (surfaces, components) -``` - -### Variable Cascade -``` -:root (10-foot base) - ├─ @media (min-width: 1025px) → 0.8x - ├─ @media (min-width: 768px, max-width: 1024px) → 0.8x - ├─ @media (max-width: 767px) → 0.65x - └─ data-typography-scale attribute (override all) -``` - ---- - -## Summary - -The typography system is now: -- ✅ **Global**: Centralized font-size hierarchy -- ✅ **Responsive**: Auto-scales based on viewport + runtime override -- ✅ **Variablized**: All sizes configurable via CSS variables -- ✅ **Documented**: Comprehensive usage guide included -- ✅ **Clean**: Topbar blue boxes replaced with clean underlines -- ✅ **Consistent**: All text follows Display/H1/H2/H3/Body/Meta hierarchy -- ✅ **Switchable**: Runtime profiles for different display types (TV, desktop, mobile) - -**Total Implementation**: ~600 lines of CSS + 200 lines of documentation diff --git a/.tasker/archive/usx-fixes-complete.md b/.tasker/archive/usx-fixes-complete.md deleted file mode 100644 index 9c628555..00000000 --- a/.tasker/archive/usx-fixes-complete.md +++ /dev/null @@ -1,235 +0,0 @@ -# USX Fixes Complete Summary - -**Date**: 2026-06-27 -**Status**: ✅ ALL PHASES COMPLETE -**Total Effort**: 3 phases, 4 days -**CSS Reduction**: 520+ lines (29% less code) - ---- - -## 🎯 Executive Summary - -Successfully completed all 3 phases of USX standardization: - -1. ✅ **Phase 1**: Font-size migration (15 instances) -2. ✅ **Phase 2**: Component utilities (420 lines saved) -3. ✅ **Phase 3**: Icon consolidation (100+ lines saved) - -**Result**: Cleaner, more maintainable CSS with full USX compliance. - ---- - -## 📊 Phase-by-Phase Results - -### **Phase 1: Font-Size Migration** ✅ - -| File | Before | After | Status | -|------|--------|-------|--------| -| `surfaces/developer.css` | 4 hardcoded | 0 | ✅ Complete | -| `hub/settings.css` | 11 hardcoded | 0 | ✅ Complete | -| **Total** | **15** | **0** | ✅ **100%** | - -**Migrations**: -- `0.9em` → `var(--usx-font-size-meta)` -- `0.75em` → `var(--usx-font-size-small)` -- `1.05rem` → `var(--usx-font-size-h4)` -- `0.8rem` → `var(--usx-font-size-meta)` -- `0.85rem` → `var(--usx-font-size-meta)` -- `1.1rem` → `var(--usx-font-size-h4)` - ---- - -### **Phase 2: Component Utilities** ✅ - -Created 5 canonical utilities in `usx-layout-system.css`: - -| Utility | Replaces | Files | Lines Saved | -|---------|----------|-------|-------------| -| `.usx-card-header` | 15+ duplicates | 15 | 150 | -| `.usx-panel-header` | 8+ duplicates | 8 | 80 | -| `.usx-status-badge` | 5+ duplicates | 5 | 50 | -| `.usx-section-header` | 8+ duplicates | 8 | 80 | -| `.usx-filter-btn` | 6+ duplicates | 6 | 60 | -| **Total** | **42+** | **42** | **420** | - -**Files Affected**: -- `hub/settings.css` -- `hub/dashboard.css` -- `hub/apps.css` -- `surfaces/developer.css` -- `surfaces/workflow.css` -- `userver.css` -- `system/story-forms.css` -- And 35+ more files - ---- - -### **Phase 3: Icon Consolidation** ✅ - -Created `usx-icons.css` — Canonical icon system: - -**Features**: -- Unified sizing for SVG, Material Icons, Bootstrap Icons -- Proportional scaling with `--usx-icon-size-*` variables -- Responsive viewport scaling (0.8x tablet, 0.65x mobile) -- Single source of truth - -**Replaces**: -- Legacy `usx-icons.css` (40+ rules) -- `usx-icon-refinement.css` (15+ rules) -- Icon rules in `usx-typography-responsive.css` (5+ rules) -- Surface-specific icon rules (10+ rules) - -**Lines Saved**: 100+ lines CSS - ---- - -## 🎨 Style System Architecture - -### **The 3 Style Systems** - -| System | Variables | Use Case | Example | -|--------|-----------|----------|---------| -| **Prose UI** | `--p-*` | Markdown content, documentation | `.prose` containers | -| **Pico.css** | `--pico-*` | UI components (buttons, forms, cards) | All surfaces | -| **GridCore/System** | `--usx-*` | Developer panels, admin controls | Developer surface | - -### **Import Order (Final)** - -```css -1. nestframe.css ← Pico base + CSS variables -2. usx-spacing-scale.css ← Spacing tokens -3. usx-pico-reset.css ← Pico CSS resets -4. usx-layout-system.css ← Layout (no colors) -5. usx-pico-integration.css ← Colors/tokens -6. usx-icons.css ← Canonical icon system -7. usx-typography-standard.css ← Font stack -8. hub/index.css ← Hub-specific -9. surface-specific CSS ← Developer, Workflow, etc. -``` - ---- - -## 📈 Impact Metrics - -| Metric | Before | After | Improvement | -|--------|--------|-------|-------------| -| **Hardcoded font-sizes** | 15 | 0 | -100% | -| **Duplicate patterns** | 42+ | 0 | -100% | -| **Icon rule duplication** | 70+ | 0 | -100% | -| **CSS lines** | ~1800 | ~1280 | -29% | -| **Maintainability** | Low | High | ✅ | -| **Responsive scaling** | Partial | Full | ✅ | -| **USX compliance** | 85% | 100% | +15% | - ---- - -## 🔄 Migration Guide - -### **For Font-Sizes** - -```diff -- font-size: 0.9em; -+ font-size: var(--usx-font-size-meta); -``` - -### **For Card Headers** - -```diff --
-+
-``` - -### **For Icons** - -```tsx -// SVG Icons (Lucide) - - -// Material Icons - - play_arrow - - -// Bootstrap Icons - -``` - ---- - -## 📝 Files Created - -1. `.tasker/usx-phase1-completion.md` — Font-size migration report -2. `.tasker/usx-phase2-completion.md` — Component utilities report -3. `.tasker/usx-phase3-completion.md` — Icon consolidation report -4. `frontend/src/styles/usx/usx-icons.css` — Canonical icon system -5. `.tasker/usx-fixes-complete.md` — This summary - ---- - -## 🗂️ Files Modified - -1. `frontend/src/styles/surfaces/developer.css` — 4 font-sizes migrated -2. `frontend/src/styles/hub/settings.css` — 11 font-sizes migrated -3. `frontend/src/styles/usx/usx-layout-system.css` — 5 utilities added - ---- - -## 🗂️ Files to Deprecate - -After surface migration completes: -- `usx/legacy/usx-icons.css` -- `usx/legacy/usx-icon-refinement.css` -- Icon rules in `usx-typography-responsive.css` - ---- - -## ✅ Verification Checklist - -- [x] All hardcoded font-sizes migrated -- [x] All duplicate patterns consolidated -- [x] Canonical icon system created -- [x] All utilities use USX standard variables -- [x] Responsive scaling implemented -- [x] Style system compliance achieved -- [x] Documentation complete - ---- - -## 🎉 Final Status - -**All 3 Phases Complete**: -- ✅ Phase 1: Font-size migration -- ✅ Phase 2: Component utilities -- ✅ Phase 3: Icon consolidation - -**Total CSS Reduction**: ~520 lines (29% less code) -**Maintainability**: Significantly improved -**Responsive Scaling**: Fully implemented -**Style System Compliance**: 100% USX standard - ---- - -## 🚀 Next Steps - -### **Surface Migration** (Optional) - -Migrate existing surfaces to use canonical utilities: - -1. Replace `.{surface}-card-header` → `.usx-card-header` -2. Replace `.{surface}-panel-header` → `.usx-panel-header` -3. Replace `.{surface}-status-badge` → `.usx-status-badge` -4. Replace `.{surface}-section-header` → `.usx-section-header` -5. Replace `.{surface}-filter-btn` → `.usx-filter-btn` - -**Estimated Effort**: 2-3 days -**Additional Savings**: 200+ lines CSS - ---- - -**Status**: 🟢 **ALL PHASES COMPLETE** -**Impact**: Cleaner, more maintainable CSS architecture -**Compliance**: 100% USX standard diff --git a/.tasker/archive/usx-phase1-completion.md b/.tasker/archive/usx-phase1-completion.md deleted file mode 100644 index 4f87c28d..00000000 --- a/.tasker/archive/usx-phase1-completion.md +++ /dev/null @@ -1,113 +0,0 @@ -# USX Phase 1 Completion Report - -**Date**: 2026-06-27 -**Status**: ✅ COMPLETE -**Scope**: Font-size migration to USX standard - ---- - -## ✅ Completed Work - -### **Priority 1: Hardcoded Font-Sizes Migrated** - -| File | Before | After | Status | -|------|--------|-------|--------| -| `surfaces/developer.css` | 4 hardcoded values | 0 | ✅ Complete | -| `hub/settings.css` | 11 hardcoded values | 0 | ✅ Complete | -| **Total** | **15 instances** | **0** | ✅ **100%** | - ---- - -## 📋 Migrations Applied - -### **developer.css** (GridCore/System Style) - -| Line | Before | After | Context | -|------|--------|-------|---------| -| 276 | `font-size: 0.9em` | `var(--usx-font-size-meta)` | Tab labels | -| 417 | `font-size: 0.75em` | `var(--usx-font-size-small)` | Chat timestamps | -| 544 | `font-size: 0.9em` | `var(--usx-font-size-meta)` | Inline code | -| 1319 | `font-size: 1.05rem` | `var(--usx-font-size-h4)` | Kanban titles | - -### **settings.css** (GridCore/System Style) - -| Line | Before | After | Context | -|------|--------|-------|---------| -| 48 | `font-size: 0.8rem` | `var(--usx-font-size-meta)` | Card subtitles | -| 89 | `font-size: 0.9rem` | `var(--usx-font-size-body)` | Display labels | -| 94 | `font-size: 0.75rem` | `var(--usx-font-size-small)` | Display descriptions | -| 113 | `font-size: 0.85rem` | `var(--usx-font-size-meta)` | Font size buttons | -| 146 | `font-size: 0.85rem` | `var(--usx-font-size-meta)` | Palette buttons | -| 169 | `font-size: 0.85rem` | `var(--usx-font-size-meta)` | Palette names | -| 185 | `font-size: 0.9rem` | `var(--usx-font-size-body)` | Connection rows | -| 195 | `font-size: 0.85rem` | `var(--usx-font-size-meta)` | Mono text | -| 214 | `font-size: 0.9rem` | `var(--usx-font-size-body)` | System links | -| 226 | `font-size: 0.75rem` | `var(--usx-font-size-small)` | System codes | -| 263 | `font-size: 1.1rem` | `var(--usx-font-size-h4)` | Dialog titles | - ---- - -## 🎯 Style System Compliance - -### **GridCore/System Style** (`--usx-*` variables) - -All migrated files now use the correct USX standard: - -```css -/* Typography hierarchy */ ---usx-font-size-display: 44px ---usx-font-size-h1: 32px ---usx-font-size-h2: 24px ---usx-font-size-h3: 20px ---usx-font-size-h4: 18px ---usx-font-size-body: 14px ---usx-font-size-meta: 11.2px (0.8em) ---usx-font-size-small: 10.5px (0.75em) -``` - ---- - -## 📊 Impact Summary - -| Metric | Before | After | Improvement | -|--------|--------|-------|--------------| -| Hardcoded font-sizes | 15 | 0 | -100% | -| USX compliance | 85% | 100% | +15% | -| Maintainability | Medium | High | ✅ | -| Responsive scaling | Partial | Full | ✅ | - ---- - -## 🔄 Next Steps - -### **Phase 2: Component Consolidation** (2 days) - -- [ ] Create `.usx-card-header` utility -- [ ] Create `.usx-panel-header` utility -- [ ] Create `.usx-status-badge` utility -- [ ] Create `.usx-section-header` utility -- [ ] Create `.usx-filter-btn` utility - -**Estimated Savings**: 300+ lines CSS - -### **Phase 3: Icon Consolidation** (1 day) - -- [ ] Centralize icon rules to `usx-icons.css` -- [ ] Remove redundant icon declarations - -**Estimated Savings**: 100+ lines CSS - ---- - -## ✅ Verification - -All font-size migrations verified: -- ✅ No hardcoded px/em/rem values remaining -- ✅ All values use USX standard variables -- ✅ Responsive scaling preserved -- ✅ Style system compliance achieved - ---- - -**Status**: 🟢 Phase 1 Complete | 🟡 Phase 2-3 Pending -**Total CSS Reduction**: 15 instances migrated (100% complete) diff --git a/.tasker/archive/usx-phase2-completion.md b/.tasker/archive/usx-phase2-completion.md deleted file mode 100644 index 4b6f85da..00000000 --- a/.tasker/archive/usx-phase2-completion.md +++ /dev/null @@ -1,211 +0,0 @@ -# USX Phase 2 Completion Report - -**Date**: 2026-06-27 -**Status**: ✅ COMPLETE -**Scope**: Component consolidation and utility creation - ---- - -## ✅ Completed Work - -### **Phase 1: Font-Size Migration** ✅ - -| File | Migrated | Status | -|------|----------|--------| -| `surfaces/developer.css` | 4 instances | ✅ Complete | -| `hub/settings.css` | 11 instances | ✅ Complete | -| **Total** | **15 instances** | ✅ **100%** | - ---- - -### **Phase 2: Component Utilities Created** ✅ - -| Utility | Replaces | Files Affected | Lines Saved | -|---------|----------|----------------|-------------| -| `.usx-card-header` | 15+ duplicates | 15 files | 150 lines | -| `.usx-panel-header` | 8+ duplicates | 8 files | 80 lines | -| `.usx-status-badge` | 5+ duplicates | 5 files | 50 lines | -| `.usx-section-header` | 8+ duplicates | 8 files | 80 lines | -| `.usx-filter-btn` | 6+ duplicates | 6 files | 60 lines | -| **Total** | **42+ duplicates** | **42 files** | **420 lines** | - ---- - -## 📋 Canonical Utilities Available - -### **1. `.usx-card-header`** -```css -.usx-card-header { /* Standard card header */ } -.usx-card-header--compact { /* Compact variant */ } -.usx-card-header-subtitle { /* Subtitle styling */ } -.usx-card-header-actions { /* Action buttons */ } -``` - -**Replaces:** -- `.hub-settings-card-header` -- `.hub-dash-card-header` -- `.developer-repo-card-header` -- `.developer-skill-card-header` -- `.developer-review-card-header` -- `.kanban-card-header` -- `.story-card-header` -- `.sys-page-browser-card-header` -- `.hub-app-card-header` -- `.hub-install-card-header` -- `.userver-card-header` - ---- - -### **2. `.usx-panel-header`** -```css -.usx-panel-header { /* Standard panel header */ } -``` - -**Replaces:** -- `.developer-panel-header` -- `.workflow-panel-header` - ---- - -### **3. `.usx-status-badge`** -```css -.usx-status-badge { /* Standard badge */ } -.usx-status-badge--active { /* Active state */ } -.usx-status-badge--warning { /* Warning state */ } -.usx-status-badge--error { /* Error state */ } -.usx-status-badge-dot { /* Status indicator dot */ } -``` - -**Replaces:** -- `.developer-status-badge` -- `.hub-status-badge` - ---- - -### **4. `.usx-section-header`** -```css -.usx-section-header { /* Standard section header */ } -``` - -**Replaces:** -- `.hub-dashboard-section-header` -- `.workflow-section-header` -- `.mc-section-header` -- `.sys-page-section-header` - ---- - -### **5. `.usx-filter-btn`** -```css -.usx-filter-btn { /* Standard filter button */ } -.usx-filter-btn--active { /* Active state */ } -``` - -**Replaces:** -- `.sys-page-filter-btn` -- `.system-filter-btn` - ---- - -## 🎯 Style System Compliance - -### **GridCore/System Style** (`--usx-*` variables) - -All utilities use the correct USX standard: - -```css -/* Typography */ ---usx-font-size-display: 44px ---usx-font-size-h1: 32px ---usx-font-size-h2: 24px ---usx-font-size-h3: 20px ---usx-font-size-h4: 18px ---usx-font-size-body: 14px ---usx-font-size-meta: 11.2px (0.8em) ---usx-font-size-small: 10.5px (0.75em) - -/* Spacing */ ---usx-spacing-xs: 4px ---usx-spacing-sm: 8px ---usx-spacing-md: 12px ---usx-spacing-lg: 16px ---usx-spacing-xl: 24px ---usx-spacing-2xl: 32px -``` - ---- - -## 📊 Impact Summary - -| Metric | Before | After | Improvement | -|--------|--------|-------|-------------| -| Hardcoded font-sizes | 15 | 0 | -100% | -| Duplicate patterns | 42+ | 0 | -100% | -| CSS lines | ~1800 | ~1380 | -23% | -| Maintainability | Low | High | ✅ | -| Responsive scaling | Partial | Full | ✅ | - ---- - -## 🔄 Next Steps - -### **Phase 3: Icon Consolidation** (1 day) - -- [ ] Centralize icon rules to `usx-icons.css` -- [ ] Remove redundant icon declarations -- [ ] Verify icon scaling across viewports - -**Estimated Savings**: 100+ lines CSS - ---- - -## ✅ Verification - -All utilities verified: -- ✅ Canonical utilities created -- ✅ All use USX standard variables -- ✅ Responsive scaling preserved -- ✅ Style system compliance achieved -- ✅ Ready for surface migration - ---- - -## 📝 Migration Guide - -### **How to Migrate Existing Surfaces** - -1. **Replace card headers:** - ```diff - -
- +
- ``` - -2. **Replace panel headers:** - ```diff - -
- +
- ``` - -3. **Replace status badges:** - ```diff - - - + - ``` - -4. **Replace section headers:** - ```diff - -
- +
- ``` - -5. **Replace filter buttons:** - ```diff - - - ``` - -2. **Material Icons**: - ```tsx - - play_arrow - - ``` - -3. **Bootstrap Icons**: - ```tsx - - ``` - -### **Icon Sizing** - -Icons automatically inherit size from parent context: -- In buttons: `1em` (matches button text) -- In headings: `1em` (matches heading text) -- Large icons: `1.8em` (scaled per viewport) - ---- - -## ✅ Verification - -All icon rules verified: -- ✅ Canonical icon system created -- ✅ All icon types supported (SVG, Material, Bootstrap) -- ✅ Responsive scaling implemented -- ✅ Single source of truth achieved -- ✅ Legacy icon files can be deprecated - ---- - -## 📝 Files to Deprecate - -After migration, these legacy files can be removed: -- `usx/legacy/usx-icons.css` (replaced by `usx-icons.css`) -- `usx/legacy/usx-icon-refinement.css` (consolidated) -- Icon rules in `usx-typography-responsive.css` (moved) - ---- - -## 🎉 Final Summary - -**All 3 Phases Complete**: -- ✅ Phase 1: Font-size migration (15 instances) -- ✅ Phase 2: Component utilities (420 lines saved) -- ✅ Phase 3: Icon consolidation (100+ lines saved) - -**Total CSS Reduction**: ~520 lines (29% less code) -**Maintainability**: Significantly improved -**Responsive Scaling**: Fully implemented -**Style System Compliance**: 100% USX standard - ---- - -**Status**: 🟢 All Phases Complete -**Next**: Surface migration to use canonical utilities diff --git a/.tasker/archive/workflow-usx-alignment-report.md b/.tasker/archive/workflow-usx-alignment-report.md deleted file mode 100644 index ecd96956..00000000 --- a/.tasker/archive/workflow-usx-alignment-report.md +++ /dev/null @@ -1,112 +0,0 @@ -# Workflow Surface — USX Alignment Report - -**Date**: 2026-06-24 -**Author**: Cline (automated audit) -**Scope**: `frontend/src/styles/surfaces/workflow.css` vs USX Layout System Spec v1.0 - ---- - -## Summary - -| Metric | Value | -|--------|-------| -| Total CSS rules | ~50+ classes | -| Fully USX-aligned classes | ~100% ✅ | -| Misalignments found **pre-fix** | 2 (1 critical, 1 minor) | -| Misalignments found **post-fix** | 0 ✅ | -| Hardcoded hex colors | ✅ None — all use `var(--pico-*)` | -| Hardcoded pixel spacing | ✅ None — all use `var(--usx-spacing-*)` | -| Hardcoded font sizes | ✅ None — all use `var(--pico-font-size*)` | - ---- - -## Findings — RESOLVED - -### Finding 1 (CRITICAL — FIXED): `.workflow-surface-main` removed, canonical `.usx-surface-main` used - -**Before**: Component used `
` — creating a cascading conflict where `.workflow-surface-main` overrode canonical 24px padding to 16px. - -**Fix applied**: -- **CSS**: Removed the `.workflow-surface-main` rule entirely from `workflow.css` -- **TSX**: Changed `
` → `
` in `WorkflowSurface.tsx` - -### Finding 2 (MINOR — FIXED): `.workflow-surface` replaced by canonical `.usx-surface-layout` - -**Before**: Root `
` had inline layout props (`display: flex; flex-direction: column; height: 100vh;`) duplicated from canon. - -**Fix applied**: -- **TSX**: Changed `
` → `
` in `WorkflowSurface.tsx` -- **CSS**: Removed the duplicated flexbox layout properties from `.workflow-surface`, keeping only surface-specific tokens (background, color, font-family) - ---- - -## Server Surface (userver.css) — Issues FIXED - -### Finding 1: `.userver-surface` layout duplication — FIXED - -**Before**: `.userver-surface` redefined `display: flex; flex-direction: column; height: 100vh;` — duplicating the canonical `.usx-surface-layout`. - -**Fix applied**: -- **CSS**: Removed the duplicate flexbox layout properties from `.userver-surface`, keeping only theme tokens (background, color) -- **TSX**: Changed `
` → `
` - -### Finding 2-7: Hardcoded px padding values — FIXED - -Seven instances of hardcoded `padding: Npx` replaced with `var(--usx-*)` tokens: - -| Line | Before | After | -|------|--------|-------| -| 56 | `padding: 12px var(--pico-spacing)` | `padding: var(--usx-spacing-md) var(--pico-spacing)` | -| 70 | `padding: 12px var(--pico-spacing)` | `padding: var(--usx-spacing-md) var(--pico-spacing)` | -| 122 | `padding: 6px 0` | `padding: var(--usx-compact-gap) 0` | -| 218 | `padding: 4px 0` | `padding: var(--usx-spacing-xs) 0` | -| 270 | `padding: 12px var(--pico-spacing)` | `padding: var(--usx-spacing-md) var(--pico-spacing)` | -| 297 | `padding: 8px 0` | `padding: var(--usx-spacing-sm) 0` | -| 362 | `padding: 12px var(--pico-spacing)` | `padding: var(--usx-spacing-md) var(--pico-spacing)` | - -### Audit verification - -Pre-fix and post-fix confirmed: userver.css appears **nowhere** in the audit findings — all categories clean. - ---- - -## Global Audit Impact - -| Metric | Pre-fix | Post-workflow-fix | Post-server-fix | -|--------|---------|-------------------|-----------------| -| Total issues | 87 | 86 | 86 | -| Clean files | 14 | 15 | 15 | -| workflow in surface_layout | Present | **Removed** ✅ | **Removed** ✅ | -| userver in surface_layout | N/A (not present) | Not present | **Still clean** ✅ | - -> Note: The total issue count stayed at 86 because the 7 hardcoded px values in userver.css were a known Phase 2 migration item. The audit tool **only flagged surface_layout patterns** by design, not individual hardcoded spacing values. The values themselves have been proactively fixed. - ---- - -## Scorecard - -| Criterion | Workflow (Post-fix) | Server (Post-fix) | -|-----------|---------------------|-------------------| -| Uses `var(--usx-spacing-*)` for padding/margin | ✅ | ✅ | -| Uses `var(--pico-*)` for colors | ✅ | ✅ | -| Uses `var(--pico-font-size*)` for typography | ✅ | ✅ | -| Uses canonical `.usx-surface-layout` | ✅ | ✅ | -| Uses canonical `.usx-surface-main` | ✅ | ✅ | -| No hardcoded pixel values | ✅ | ✅ | -| No duplicate USX class definitions | ✅ | ✅ | -| No unscoped element selectors | ✅ | ✅ | -| **Overall Alignment** | **100%** | **100%** | - ---- - -## Files Changed - -1. **`frontend/src/styles/surfaces/workflow.css`** — Removed `.workflow-surface-main` block, trimmed `.workflow-surface` to theme-only tokens -2. **`frontend/src/surfaces/workflow/WorkflowSurface.tsx`** — Switched root `
` to `.usx-surface-layout workflow-surface`, removed `workflow-surface-main` from `
` -3. **`frontend/src/styles/userver.css`** — Removed layout duplication from `.userver-surface`, replaced 7 hardcoded px padding values with `var(--usx-*)` tokens -4. **`frontend/src/surfaces/userver/UServerSurface.tsx`** — Switched root `
` to `.usx-surface-layout userver-surface` - ---- - -*Report generated via uCore MCP `skill_usx-standard` audit + manual cross-reference. -Verification: `curl http://localhost:8484/api/mcp/call -X POST -d '{"name":"skill_usx-standard","arguments":{"query":"{\"mode\":\"audit\"}"}}'`* \ No newline at end of file diff --git a/.tasker/backlog/gridcore-ucode-canonical-migration.md b/.tasker/backlog/gridcore-ucode-canonical-migration.md deleted file mode 100644 index 596f497b..00000000 --- a/.tasker/backlog/gridcore-ucode-canonical-migration.md +++ /dev/null @@ -1,58 +0,0 @@ -# GridCore canonical migration — uCore surface → uCode packages - -Status: not-started -Priority: P1 -Area: uCode / GridCore / viewport-renderer -Created: 2026-08-14 - -## Goal - -uCode's `@udos/gridcore` + `@udos/viewport-renderer` are canonical. Remove -uCore's remaining local grid implementation (`frontend-vue/src/grid-core/`) and -rewire `UCodeSurface.vue` onto the uCode packages via the existing Vite aliases -(`@udos/gridcore`, `@udos/viewport-renderer`). - -## Current state (2026-08-14) - -- uCore's dead duplicates already removed: `frontend-vue/src/vendor/gridui-canvas/` - and the unused viewer components (`GridCoreUI.vue`, `MultiColumnViewer.vue`, - `ProseViewer.vue`, `SlideViewer.vue`). -- Live uCore grid code (still used by `UCodeSurface.vue`): - - `frontend-vue/src/grid-core/{buffer,types,algebra,palette,g0-renderer,gridui-canvas,index}.ts` -- Canonical uCode packages: - - `~/Code/uCode/packages/gridcore/src/` — geometry, buffer, layers, teletext, - terminal, viewport, spatial, editor, fonts, bridge - - `~/Code/uCode/packages/viewport-renderer/src/` — CanvasViewport, DOMViewport, - ViewportWidget, TeletextWidget, TerminalWidget, fonts, palette/usx - -## API mismatch (the reason this is a migration, not an import swap) - -| uCore `grid-core` | uCode `@udos/gridcore` | -|---|---| -| `GridCell` = `{char, fg, bg, ...}` (uCore shape) | `BufferCell` = `{char, fg, bg, bold, flash, doubleHeight, doubleWidth}` | -| `createBuffer`, `writeString`, `fill`, `scroll`, `clear`, `cloneBuffer`, `bufferToString`, `stringToBuffer` | `createBuffer`, `createBufferCell`, `cloneBuffer`, `getBufferDimensions`, `sameDimensions` (no writeString/fill/scroll/clear) | -| `PALETTE_DARK`, `PALETTE_LIGHT`, `getColour`, `colourCSS` | `@udos/viewport-renderer` `palette/usx` | -| `GRID_PRESETS`, `getGridPreset`, `resolveColumns`, `calcViewport`, column algebra | `viewport/calculator` | -| `G0Renderer`, `` Web Component | `viewport-renderer` canvas/dom/widgets | - -## Migration steps - -1. **uCode side (canonical home):** add the missing string/grid primitives to - `@udos/gridcore` — `writeString`, `fill`, `scroll`, `clear`, `bufferToString`, - `stringToBuffer`, and a viewport/preset helper (`resolveColumns`, `calcViewport`) - — operating on the canonical `BufferCell` shape. Rebuild the package (tsup). -2. **Renderer:** confirm `@udos/viewport-renderer` provides a drop-in for the - `` Web Component (or port the element into viewport-renderer), - then replace `grid-core/gridui-canvas.ts` usage in `UCodeSurface.vue`. -3. **uCore side:** rewire `UCodeSurface.vue` imports from - `../../grid-core/*` → `@udos/gridcore` + `@udos/viewport-renderer`. -4. Delete `frontend-vue/src/grid-core/` and `frontend-vue/src/styles/gridcore.css` - only after the surface renders identically (browser-verify grid + terminal tabs). -5. Remove the now-unused `@uCode3` Vite alias (points at a repo that may not exist). - -## Acceptance criteria - -- `UCodeSurface.vue` imports only `@udos/*` — no local `grid-core/` imports. -- uCore's `frontend-vue/src/grid-core/` directory is deleted. -- GridCore canvas, column/prose/slide modes, and the terminal tab render as before. -- Frontend type-check + production build pass; vitest grid tests green. diff --git a/.tasker/backlog/runtime-backed-terminal-adapter.md b/.tasker/backlog/runtime-backed-terminal-adapter.md deleted file mode 100644 index 6b47c548..00000000 --- a/.tasker/backlog/runtime-backed-terminal-adapter.md +++ /dev/null @@ -1,61 +0,0 @@ -# Replace Terminal Demo Shell With Runtime-Backed Adapter - -Status: complete -Priority: P1 -Area: uCode / GridCore / runtime bridge -Created: 2026-07-18 -Closed: 2026-08-14 - -## Current Status - -- `TerminalSurface` is a demo/local terminal shell rendered through the GridCore canvas path. -- The uCode Terminal tab is a local GridCore buffer demo inside the uCode hub. -- GridCore is the rendering/data primitive layer: `GridBuffer`, `GridCell`, canvas sizing, palette, and font rendering. -- A real uCode/BBC BASIC/terminal runtime bridge is not wired yet. - -## Goal - -Replace the terminal demo shell with a runtime-backed adapter while keeping GridCore runtime-agnostic. The adapter should own runtime I/O, transport, lifecycle, and conversion from runtime output into `GridBuffer` updates. - -## Contract To Define First - -- Command/input stream from the frontend into the selected runtime. -- Runtime output stream back to the frontend as ordered text, control events, or `GridBuffer` patches/snapshots. -- Transport shape: REST for request/response commands, WebSocket for interactive terminal/PTY streaming. -- Buffer mapping rules from runtime output into `GridBuffer` cells. -- Runtime target selection: shell/PTY, BBC BASIC, uCode VM, or GridSmith world runner. -- Runtime lifecycle events: start, stop, resize, reset, exit, and error. - -## Acceptance Criteria - -- `TerminalSurface` and the uCode Terminal tab no longer pretend demo buffers are runtime-backed. -- A typed adapter contract exists for runtime input, runtime output, lifecycle state, and buffer updates. -- The first runtime target is explicitly chosen and documented before implementation. -- GridCore remains a renderer/data primitive package and does not learn backend/runtime details. -- The selected transport has one focused integration check proving input reaches the runtime and output maps to the rendered buffer. - -## Progress - -- First runtime target selected: local shell/PTY. -- Backend scaffold added at `/api/terminal/runtime/ws` using `aiohttp.web.WebSocketResponse`. -- Frontend uCode Terminal tab now connects to the runtime WebSocket, forwards key input, and maps text output into the GridCore canvas buffer. -- Direct PTY smoke test passes: shell starts, input reaches the process, and output returns through the adapter queue. -- Browser-level verification passes: typed terminal input reaches the PTY and sentinel output appears in the GridCore canvas buffer. -- PTY sessions now set `TERM=xterm-256color`; `clear` emits terminal control sequences instead of a TERM error. -- Initial ANSI/control handling supports OSC skip, CSI home, clear-screen, and clear-line sequences. - -## Remaining - -- ANSI/control-sequence handling beyond the initial home/clear-line/clear-screen path. -- Decide whether standalone `TerminalSurface` should share this adapter or be retired in favor of the uCode Terminal tab. - -## Close-out (2026-08-14) - -- **Decision:** the standalone `TerminalSurface` is retired — it no longer exists - as a frontend component; the uCode Terminal tab (`UCodeSurface.vue`) is the - canonical runtime-backed terminal and is browser-verified end-to-end. -- Acceptance criteria met: runtime-backed (no demo buffer), typed adapter - contract, first target (shell/PTY) chosen and documented, GridCore stays a - renderer primitive, and a focused PTY integration check passes. -- Deferred to follow-up: richer ANSI/CSI coverage (current: OSC skip, CSI home, - clear-screen, clear-line). diff --git a/.tasker/handover-pyrchard-2026-07-04.md b/.tasker/handover-pyrchard-2026-07-04.md deleted file mode 100644 index 0d926d2f..00000000 --- a/.tasker/handover-pyrchard-2026-07-04.md +++ /dev/null @@ -1,168 +0,0 @@ -# pyrchard-uCode Development Handover Notes - -**Date:** 2026-07-04T21:39:00+08:00 -**Session:** PyCharm (Girdui) setup complete → handoff to next agent -**Branch:** main @ 9f4c802 - ---- - -## 1. Current State - -### Git HEAD -``` -9f4c802 fix: match Grid colour popover swatch style to Pixel — 1px border, inset shadow active states -4f39f05 fix: unify colour popovers — true 3x3 square grid, 36px cells, matching Pixel & Grid -c946237 refactor(Grid): remove sidebar palette, make colour popover a true 3x3 grid with transparent cell -389f5f6 fix(Grid): remove remaining F marker from sidebar palette -7699c7c refactor(Grid): remove close-up editor pane, make Layer the primary editing surface with Pixel-style toolbar -``` - -### Version -`pyproject.toml`: **4.0.3** - -### Sprint Status -- **Sprint:** sprint.2026-07-02 "Control Panel + Agentic Execution Sprint" — **complete** (10/10 tasks done) -- **Completed count:** 58 tasks total in `.tasker.dev-flow.yaml` -- **Task count:** 44 defined tasks - ---- - -## 2. What Was Just Done (Recent Session) - -### PyCharm / Girdui Setup -- PyCharm IDE environment configured for the uCore workspace -- All project directories and virtual environments mapped - -### Recent Grid UI Refinements (last 5 commits) -- Colour popover now matches Pixel's swatch style: 1px border, inset shadow active states -- Unified colour popovers: true 3x3 square grid, 36px cells, consistent between Pixel & Grid surfaces -- Removed sidebar palette from Grid; colour popover is now a standalone 3x3 grid -- Removed close-up editor pane; Layer is now the primary editing surface -- Pixel-style toolbar integrated into Layer editing surface - ---- - -## 3. Architecture Overview (for the next agent) - -### Project Structure -- **Backend:** Python 3.12 (`backend/app/`) — ASGI server on port 8484, managed by launchd (`com.udos.ucore-server`) -- **Frontend (Vue):** `frontend-vue/` — Vite dev server on localhost:5175 -- **Frontend (React/legacy):** `frontend/` — older surfaces -- **MCP Servers:** Multiple — Hivemind (8490), Firewatch, Serena, Scheduler, Secrets, Knowledge -- **Config:** `config/` — agents.yaml, llm_router.yaml, openrouter.yaml, kanban.yaml - -### Key Subsystems -| Subsystem | Location | Status | -|-----------|----------|--------| -| Control Panel | `frontend-vue/src/` (8 Vue files) + `backend/app/api/control.py` | Live | -| Hivemind Consensus | `backend/app/mcp/consensus.py` + `hivemind_server.py:8490` | Live | -| LLM Router | `backend/app/services/provider_router.py` | Live | -| Skills Registry | `backend/app/skills/builtin/` (BaseSkill subclasses) | Live | -| Feed System | Pod (SQLite) + MCP server + FeedConsumer | Live | -| Plates (Templates) | `backend/plate_refresh/` + `plates/` dirs | Live | -| USX Style System | `frontend-vue/src/styles/tokens/` + themes | Live | -| Catalog Service | `backend/app/services/catalog/` | Live | -| Distribution/Packages | `backend/app/services/distribution_system/` | Live | -| GridCore (Vue) | `frontend-vue/src/grid-core/` | Renderer only | -| GridSmith API | `backend/app/api/gridsmith_api.py` | REST endpoints | - -### Key Ports -| Port | Service | -|------|---------| -| 8484 | uCore backend (ASGI) | -| 5175 | Vue frontend dev server | -| 8490 | Hivemind MCP server | -| 3484 | Cline Kanban | -| 4891 | Roundtable AI | -| 11434 | Ollama | - ---- - -## 4. Known Issues & Watch Items - -### Active Test Failures (10 signals, last 24h) -- `test_chat_cache.py::test_provider_router_chat_uses_cache` -- `test_core_snackbar.py::test_shutdown_handler` -- `test_episodic_log.py` — 4 tests failing (skill entry, invalid type, description requirement, comma tags) -- `test_episodic_log.py::test_brain_sync_includes_episodic_summary` -- `test_flow_router.py` — 3 tests failing (basic, analytics, clear) - -### Backend Stability -- **Snackbar 500 crash loop** (fieldnotes 2026-07-01): Backend exits immediately after maintenance scheduler starts. Suspect event loop exit in `app/core/snackbar.py`. Workaround: frontend gracefully degrades with try/catch. -- **Skill load warnings:** `skill_dev_destroy_rebuild.py` and `skill_hardcoded_path_detector` fail to load from registry -- **Maintenance jobs:** `vault_sync` and `tasker_sync` report `success=False` - -### Durability Lessons (from wisdom.md) -1. **Skill loop guards required:** Any multi-step pipeline skill MUST have `max_iterations` guard (default 3) + cooldown tracker (5 min TTL) — `skill_surface_rebuild` has it; add to new pipeline skills -2. **launchd KeepAlive trap:** Use `launchctl bootout` to stop, not `kill`. Deprecated configs with `KeepAlive ` respawn killed processes immediately -3. **Syntax error detection:** Always run `flake8` or `py_compile` on changed files — a bare `except` nesting bug in `snackbar_menu.py` only surfaced in stderr.log - ---- - -## 5. Next Steps / Suggested Work - -### Immediate Priority -1. **Fix the 10 failing tests** — episodic_log and flow_router modules need attention -2. **Investigate snackbar crash loop** — backend on 8484 may be down; check `launchctl` status -3. **Fix skill load warnings** — `skill_dev_destroy_rebuild` and `skill_hardcoded_path_detector` import errors - -### Continuing Work -- **Grid UI polish:** Colour popover and Layer editing surface are actively being refined -- **GridSmith Node agent:** Still needs implementation as per gap analysis in fieldnotes (backend CLI + MCP tools for world building) -- **DocLang bridge export:** Spec exists at `docs/DOCLANG_BRIDGE_EXPORT_SPEC.md` -- **Settings architecture:** Spec at `docs/SETTINGS_ARCHITECTURE_2026.md` - -### Documentation Round (before next push) -Per `.clinerules` Docs Round Completion checklist: -- [ ] Update FEATURE_SPEC.md for any feature delivered -- [ ] Archive completed sprint plans to `docs/archive/` -- [ ] Archive completed tasker items -- [ ] Update `devlog.mcp.yaml` with new/modified files -- [ ] Update `fieldnotes.md` with key decisions -- [ ] Update `wisdom.md` lessons if any -- [ ] Bump version patch in `pyproject.toml` and `package.json` -- [ ] Update `.tasker.dev-flow.yaml` - ---- - -## 6. Useful Commands - -```bash -# Check backend health -curl http://localhost:8484/api/health - -# Check launchd status -launchctl list | grep ucore - -# Run tests -cd backend && python -m pytest -x --tb=short - -# Start frontend dev server -cd frontend-vue && pnpm dev - -# Check spool activity -cat ~/.ucore/spool/*.log | tail -50 - -# Git workflow -git log --oneline -10 -git status -``` - ---- - -## 7. Key Files Reference - -| File | Purpose | -|------|---------| -| `.tasker.dev-flow.yaml` | Canonical task list (58 done, 44 total) | -| `devlog.mcp.yaml` | Recent change log | -| `fieldnotes.md` | Developer observations and debugging notes | -| `wisdom.md` | Durable lessons + spool activity + test failures | -| `pyproject.toml` | Python project metadata (v4.0.3) | -| `package.json` | Node project metadata | -| `.clinerules` | Agent operating principles and workflow rules | -| `CONTEXT.md` | Project context overview | - ---- - -**Handoff complete.** Next agent should start by checking backend health, running the test suite to confirm current state, and picking up from the known issues list above. \ No newline at end of file diff --git a/.tasker/inbox/task.test.001-ollama-list.md b/.tasker/inbox/task.test.001-ollama-list.md deleted file mode 100644 index db94b40e..00000000 --- a/.tasker/inbox/task.test.001-ollama-list.md +++ /dev/null @@ -1,16 +0,0 @@ -# task.test.001 - -**Title:** Run ollama list and return the output - -**Status:** pending - -**Complexity:** simple - -**Description:** -Execute `ollama list` command locally and return the list of available Ollama models. -This is a simple, safe action that tests the local execution path without consuming DeepSeek credits. - -**Expected Output:** -- List of installed Ollama models with their sizes and tags. - -**Execution Path:** local (Ollama) \ No newline at end of file diff --git a/.tasker/phases/active-phase-10-ecosystem-hardening-2026-07-31.md b/.tasker/phases/active-phase-10-ecosystem-hardening-2026-07-31.md deleted file mode 100644 index 52ceacf2..00000000 --- a/.tasker/phases/active-phase-10-ecosystem-hardening-2026-07-31.md +++ /dev/null @@ -1,111 +0,0 @@ -# Phase 10 - Ecosystem Hardening and Autonomous Rounds - -- status: active -- owner: developer-lane -- started: 2026-07-31 -- governance: stop-the-line required per wave - -## Goal - -Ship a stable core host shell with clean extension/plugin boundaries, zero legacy fallback drift, and repeatable autonomous dev rounds with verifiable evidence. - -## Restructure Checkpoint (2026-08-03) - -- [x] Confirm core split repos exist: `uCore`, `uDev`, `uFlow`, `uKnowledge`, `uCode` -- [x] Confirm plugin repos exist with scaffold baseline: `udos-budget`, `udos-identity`, `udos-google`, `udos-dreamscape`, `udos-publishing` -- [x] Add missing `udos-publishing` plugin manifest (`ucore-extension.json`) to complete baseline plugin shape - -## Gate 0 - Stability Baseline - -- [x] Confirm uCore boot + `/api/health` + `/api/mcp/diagnostics` -- [x] Confirm `python3 scripts/audit_duplicate_routes.py` returns zero duplicates -- [x] Confirm `python3 scripts/validate_extension_manifests.py` passes -- [x] Confirm `python3 scripts/validate_legacy_settings_cleanup.py` passes -- [x] Confirm Developer Surface build + dev startup on fixed `5176` - -## Gate 1 - Governance and Cleanup - -- [x] Remove obsolete compatibility notes that imply in-core fallback ownership -- [x] Remove dead/legacy settings references from active docs and scripts -- [x] Validate no forbidden legacy modules reappear in uCore host tree -- [x] Record cleanup evidence bundle in `docs/handovers/CLINE_REPO_SPLIT_HANDOFF.md` - -## Gate 1B - Surface Ownership Cleanup - -- [x] Remove detached legacy `snackmachine` standalone surface (merged into Server/Workflow/System redirects) -- [x] Remove detached legacy standalone `teletext` and `terminal` surfaces (canonical home is uCode tabs) -- [x] Re-scan `/api/surfaces/discover` and verify no detached surfaces remain -- [x] Publish surface placement policy in docs (host UI vs plugin backend ownership) - -## Wave E2 - Documentation Publishing Recovery - -- [x] Restore documentation backend API contract used by Documentation surface (`/api/docs`, `/api/docs/sites`, `/api/docs/global-knowledge`, `/api/docs/export`, `/api/docs/serve/{site}`) -- [x] Validate doc-site discovery from `~/Public/doc-sites` and global knowledge from `~/Public/global-knowledge` -- [x] Define publishing ownership split: uCore UI shell vs plugin/provider logic -- [x] Add route parity and runtime probes for documentation publishing endpoints - -## Wave F - Identity, Story Forms, Publishing - -- [x] Finalize capability map: `identity_gateway`, `wordpress_gateway` -- [x] Expand `udos-identity` API contracts for story progression + variables -- [x] Add privacy/share rules with expiry checks -- [x] Add WordPress mapping adapters for user meta/taxonomy -- [x] Add route parity + preflight validation for all new identity routes - -## Wave F2 - Publishing and Place Cloud Mirror - -- [x] Confirm `udos-publishing` as the owning repo for cloud mirror publishing -- [x] Define `udo.guide` and `udo.place` contracts for guide/place mirror behavior (scaffold baseline) -- [x] Add frontmatter, tagging, location, beacon, and portal mapping checks (scaffold baseline) -- [x] Add verification and shareable publishing reference gates (scaffold baseline) -- [x] Add route parity + preflight validation for publishing/place routes (scaffold baseline) - -Wave F2 implementation note: - -- [ ] Complete production-depth publishing integrations and route behavior hardening in the next execution plan. - -## Wave G - Google Plugin Foundation - -- [x] Create `udos-google` repository skeleton and manifest -- [x] Implement OAuth contract and token ownership model -- [x] Implement Gemini chat + gems thin slice -- [x] Implement Vault docs <-> Drive thin sync slice -- [x] Add `google_ai_bridge` capability preflight and health gates - -## Wave H - Dreamscape Foundation - -- [x] Create `udos-dreamscape` repository skeleton and manifest -- [x] Implement interest intake -> mission/task scaffolding routes -- [x] Implement daily briefing baseline output flow -- [x] Define Chronos integration contracts without core fallback logic -- [x] Add `dreamscape_orchestration` capability preflight and readiness checks - -## Wave I - Developer Surface and MCP Bridge Integration - -- [x] Add readiness cards for WordPress, Google, Dreamscape capabilities -- [x] Hide/disable controls until capability preflight is ready -- [x] Add bridge tool coverage in `uDev/mcp-bridge` for new plugin APIs -- [x] Validate deterministic startup and no stale API client imports - -## Wave J - Final Dev-Round and Closure - -- [x] Execute the final autonomous dev-round after Waves G, H, and I are complete -- [x] Collect the stop-the-line evidence bundle across all completed waves -- [x] Publish the final repairs log, release notes, and rollback notes -- [x] Commit and push remaining planning/documentation updates before closing the phase - -## Autonomous Dev Rounds - -- [x] Adopt runbook: `docs/specs/AUTONOMOUS_DEV_ROUNDS_RUNBOOK.md` -- [x] Execute low-cost route first (ollama-first) -- [x] Execute broad-strokes pass with diagnostics + hivemind health checks -- [x] Capture cost/perf metrics and compare with manual throughput -- [x] Publish per-round artifacts and repairs log -- [x] Run the final dev-round closure pass for completed waves before phase handoff - -## Evidence Gate (Mandatory Each Wave) - -- [x] Runtime proof -- [x] Command proof -- [x] Test proof -- [x] Evidence proof diff --git a/.tasker/phases/active-phase-11-docs-mirror-publishing-2026-08-13.md b/.tasker/phases/active-phase-11-docs-mirror-publishing-2026-08-13.md deleted file mode 100644 index 4f487cd1..00000000 --- a/.tasker/phases/active-phase-11-docs-mirror-publishing-2026-08-13.md +++ /dev/null @@ -1,106 +0,0 @@ -# Phase 11 — Docs Mirroring & Publishing (Lane-Separated) - -- status: complete -- owner: developer-lane -- started: 2026-08-13 -- closed: 2026-08-14 -- governance: stop-the-line required per wave - -## Goal - -Ship one readable Documentation surface that: - -1. Mirrors uDos **component docs** (Dev Lane) — in-repo `docs/` is the source of truth. -2. Keeps the **user's published vaults** (User Lane) strictly separate — never merged into the component mirror or the docs-site. -3. Supports two-way sync in Dev Mode and a publish path to `docs.udo.guide`. - -## Lane Separation (non-negotiable) - -### Dev Lane — uDos Component Docs - -| Concern | Value | -|---------|-------| -| Source of truth | In-repo `docs/` in core repos (`uCore`, `uFlow`, `uKnowledge`, `uCode`, `uVector`) and extension repos (`udos-*`) | -| Readable mirror | `~/.ucore/docs-mirror/` (internal, provenance-tagged) | -| Read surface | Documentation surface — read-only in User lane | -| Edit surface | Developer surface — Dev Mode only; writes back to the source repo | -| Publish target | `~/Public/doc-sites//` build → `docs.udo.guide` | - -### User Lane — User Published Vaults - -| Concern | Value | -|---------|-------| -| Source of truth | `~/Vault` (master), `~/Shared/*`, `~/Public/*` user content | -| Separation rule | User vault content is **never** indexed into the component mirror or built into the docs-site | -| Publish path | Existing DocLang export (`POST /api/docs/export`) → the user's own published spaces | - -## Architecture - -```mermaid -flowchart TB - subgraph DEV["Dev Lane — uDos Component Docs"] - SRC["In-repo docs/
(uCore, uFlow, uKnowledge, uCode, udos-*)"] - MIR["~/.ucore/docs-mirror/
(provenance-tagged copy)"] - SURF["Documentation Surface
(read-only in User lane)"] - DEVEDIT["Developer Surface
(Dev Mode: edit + write-back)"] - BUILD["docs-site build
(Jekyll/Hugo)"] - GUIDE["docs.udo.guide"] - SRC -->|"pull sync (docs_mirror service)"| MIR - MIR --> SURF - DEVEDIT -->|"write-back: PUT /api/developer/repos/{repo}/file-preview"| SRC - MIR --> BUILD --> GUIDE - end - subgraph USER["User Lane — Published Vaults"] - VAULT["~/Vault (master)
~/Shared/* ~/Public/*"] - PUB["Vault publishing
(POST /api/docs/export DocLang)"] - VAULT --> PUB - end -``` - -## Waves - -### Wave 1 — docs_mirror sync engine (pull only) - -- [x] **D1.1** Add `backend/app/services/docs_mirror.py` with `sync_from_repos()` - - Scan core repos (`uCore`, `uFlow`, `uKnowledge`, `uCode`, `uVector`) plus discovered `udos-*` repos - - Copy markdown into `~/.ucore/docs-mirror//...` - - Write a `_mirror.json` index: `{source_repo, source_path, mirrored_path, synced_at, git_sha}` -- [x] **D1.2** Add `POST /api/docs/mirror/sync` and `GET /api/docs/mirror/status` -- [x] **D1.3** Register the sync in the maintenance scheduler (periodic pull, 04:10 daily) -- [x] **D1.4** Verify: no user-vault path (`~/Vault`, `~/Shared`, `~/Public`) is ever scanned by the mirror (guarded by `_ensure_allowed` + test) - -### Wave 2 — provenance + two-way sync (Dev Mode gate) - -- [x] **D2.1** Tag every mirrored file with provenance (`source_repo`, `source_path`, `git_sha`) — kept in `_mirror.json` index so mirrored files stay verbatim (clean diffs) -- [x] **D2.2** Add `POST /api/docs/mirror/push` — writes a mirrored doc back to its source repo; reject unless Dev Mode is active -- [x] **D2.3** Enforce lane gate server-side: User lane (Dev Mode off) gets 403 on any push mutation -- [x] **D2.4** Add `GET /api/docs/mirror/diff/{repo}/{path}` to show repo-vs-mirror drift - -### Wave 3 — Surface lane separation - -- [x] **D3.1** Documentation surface serves component docs from the mirror (`GET /api/docs/content?source=mirror&path=/`); the old `repo-docs` tab is folded into the Guide tab as "Component Docs" -- [x] **D3.2** Component docs are read-only in the Documentation surface: clicking opens a side-panel viewer (no inline edit); the separate "Repo Docs" tab is removed -- [x] **D3.3** Developer surface: Dev Mode enables edit/write-back on mirrored docs via the Documentation side panel (Edit → `POST /api/docs/mirror/push`); User lane stays read-only (403 gate) -- [x] **D3.4** User vaults stay on their own tabs/surfaces: Learning tab scans user `~/Vault` + `~/Public/learning` + `docs/archive` but never writes into the component mirror; mirror `_ensure_allowed` blocks vault paths - -### Wave 4 — docs-site publish pipeline - -- [x] **D4.1** Build step: mirror → `~/Public/doc-sites/udos-docs/` (self-contained static HTML site, USX-styled) -- [x] **D4.2** `POST /api/docs/publish` (build + optional deploy) and `GET /api/docs/publish/status` -- [x] **D4.3** Deployment hook `deploy_site()` (git commit + push when the site root is a git repo; best-effort) -- [x] **D4.4** Route parity + preflight: contract check 17/17, `test_docs_publish.py` unit tests, live build of 168 pages verified - -### Wave 5 — Verification & evidence - -- [x] **D5.1** Runtime proof: sync (168 files) + status + push (403 in User lane) + publish (168 pages) all exercised -- [x] **D5.2** Test proof: 490 pytest passed + route contract 17/17 + `test_docs_publish.py` -- [x] **D5.3** Command proof: `audit_duplicate_routes.py` (194 routes, 0 dupes) + frontend type-check + production build -- [x] **D5.4** Evidence bundle written to `docs/handovers/PHASE11_DOCS_MIRROR_PUBLISHING_EVIDENCE_2026-08-14.md` - -## Exit criteria - -1. ✅ Component docs mirror exists with provenance and periodic pull sync. -2. ✅ Dev Mode can edit + write back to source repos; User lane cannot mutate. -3. ✅ Documentation surface clearly separates Dev-lane component docs from User-lane vaults. -4. ✅ docs-site builds from the mirror and publishes to `~/Public/doc-sites/udos-docs/`. -5. ✅ All waves pass stop-the-line evidence gates. diff --git a/.tasker/planning/in-progress-capability-preflight-repair-gate-2026-07-30.md b/.tasker/planning/in-progress-capability-preflight-repair-gate-2026-07-30.md deleted file mode 100644 index 429ccb62..00000000 --- a/.tasker/planning/in-progress-capability-preflight-repair-gate-2026-07-30.md +++ /dev/null @@ -1,33 +0,0 @@ -# Capability Preflight Repair Gate Rollout - -- status: in-progress -- source: ucore-dev -- source_id: capability-preflight-repair-gate-20260730 -- synced_at: 2026-07-30T00:00:00Z - -## Goal - -Enforce strict preflight + repair gating for capability actions across S-pages. - -## Tasks - -- [x] Add backend extension status endpoint (`/api/extensions/status`) -- [x] Add backend capability preflight endpoint (`/api/capabilities/{capability}/preflight`) -- [x] Add capability requirements config map (`config/capability_requirements.json`) -- [x] Add frontend preflight API helper (`frontend-vue/src/api/preflight.ts`) -- [x] Wire preflight blocking in workflow store for workflow/knowledge capabilities -- [x] Add reusable repair panel component for S-pages -- [x] Wire repair panel into Workflow surface -- [x] Add startup readiness snapshot for top capabilities -- [x] Add CI gate to fail when capability requirements are missing - -## Verification Evidence - -- backend compile: `python3 -m compileall -q backend/app` -- frontend typecheck: `npx tsc --noEmit` -- route audit: `python3 scripts/audit_duplicate_routes.py` - -## Notes - -No silent fallback for required capability paths. -Missing prerequisites must trigger repair steps and explicit rerun of preflight. diff --git a/.tasker/planning/in-progress-plan-the-week-seed-plan-the-week.md b/.tasker/planning/in-progress-plan-the-week-seed-plan-the-week.md deleted file mode 100644 index 778e3537..00000000 --- a/.tasker/planning/in-progress-plan-the-week-seed-plan-the-week.md +++ /dev/null @@ -1,14 +0,0 @@ -# Plan the week - -- status: in-progress -- source: seed-user-workflow -- source_id: seed-plan-the-week -- synced_at: 2026-07-15T13:47:33Z -- priority: high -- mission: Weekly Planning -- task: Plan the week -- binder: planner -- tags: planning, weekly - -## Summary -Review goals and select top 3 priorities. diff --git a/.tasker/sandbox/review-add-a-sandbox-note-seed-add-a-sandbox-note.md b/.tasker/sandbox/review-add-a-sandbox-note-seed-add-a-sandbox-note.md deleted file mode 100644 index 416d3bc3..00000000 --- a/.tasker/sandbox/review-add-a-sandbox-note-seed-add-a-sandbox-note.md +++ /dev/null @@ -1,14 +0,0 @@ -# Add a Sandbox note - -- status: review -- source: seed-user-workflow -- source_id: seed-add-a-sandbox-note -- synced_at: 2026-07-21T14:21:41Z -- priority: medium -- mission: Getting Started -- task: Add a Sandbox note -- binder: Sandbox -- tags: seed, notes - -## Summary -Create or edit a Markdown note from the filepicker. diff --git a/.tasker/sandbox/todo-my-first-binder-seed-my-first-binder.md b/.tasker/sandbox/todo-my-first-binder-seed-my-first-binder.md deleted file mode 100644 index c9c47650..00000000 --- a/.tasker/sandbox/todo-my-first-binder-seed-my-first-binder.md +++ /dev/null @@ -1,14 +0,0 @@ -# My First Binder - -- status: todo -- source: seed-user-workflow -- source_id: seed-my-first-binder -- synced_at: 2026-07-21T14:21:41Z -- priority: medium -- mission: Getting Started -- task: My First Binder -- binder: Sandbox -- tags: seed, binder - -## Summary -Add one note, one task, and one reference to Sandbox. diff --git a/.tasker/sandbox/todo-welcome-to-ucode-seed-welcome-to-ucode.md b/.tasker/sandbox/todo-welcome-to-ucode-seed-welcome-to-ucode.md deleted file mode 100644 index 6f35f26f..00000000 --- a/.tasker/sandbox/todo-welcome-to-ucode-seed-welcome-to-ucode.md +++ /dev/null @@ -1,14 +0,0 @@ -# Welcome to uCode - -- status: todo -- source: seed-user-workflow -- source_id: seed-welcome-to-ucode -- synced_at: 2026-07-21T14:21:41Z -- priority: high -- mission: Getting Started -- task: Welcome to uCode -- binder: Sandbox -- tags: seed, user, ucode - -## Summary -Open the Sandbox binder and review the starter docs. diff --git a/.tasker/sprints/sprint-plan-2026-07-08.md b/.tasker/sprints/sprint-plan-2026-07-08.md deleted file mode 100644 index 12be2170..00000000 --- a/.tasker/sprints/sprint-plan-2026-07-08.md +++ /dev/null @@ -1,145 +0,0 @@ -# Sprint A — Fold uDev Developer Surface into uCore - -**Status:** Complete -**Started:** 2026-08-10 -**Completed:** 2026-08-13 -**Scope:** Migrate uDev developer-surface capabilities into uCore surfaces. - ---- - -## Outcome - -The Developer Surface is the in-core Code/Repository/Editor repo browser at -`/developer`, backed by `/api/developer/repos/*`. Operational panels were -folded into: - -- **Intelligence** (`/intelligence`) — Chat, Models, Agents, Budget, History. -- **Snackbar** (`/snackbar`) — Dashboard, Services, Agents, Feeds, Skills, - Snacks, Extensions, Logs, MCP. - -## Completed - -- [x] Developer repo browser (Code/Repository/Editor) wired to `/api/developer/repos/*` -- [x] Agents/Models/Budget tabs live in Intelligence -- [x] Services/Feeds/Skills/Snacks/Extensions tabs live in Snackbar -- [x] Logs + MCP tabs restored in Snackbar -- [x] Fixed `/api/server/models` (was returning empty) -- [x] Deduplicated `/api/skills` (now returns the full registry) -- [x] Fixed `/api/server/agents` (agents only, no skills/tools mixed in) -- [x] Removed dead code: `DeveloperGatewaySurface.vue`, `SnackbarReposPanel.vue` -- [x] Deprecated uDev developer-surface (repo no longer present in `~/Code`) - -## Notes - -- The earlier "fold repos into Snackbar / consolidate editor into Workflow" - waves were superseded: the repo browser stays in `/developer`. -- No remaining blocking work. Optional follow-up: sweep stale `uDev` mentions - in archived specs. - ---- - -## Health Baseline (2026-07-08) - -| Metric | Before | After Sprint 1 | -|--------|--------|----------------| -| Test collection errors | 5 | 0 | -| Test failures | 19 | 0 | -| Tests passing | 483 | 502 | -| MCP integrity checks | 1/6 (syntax failing) | 6/6 (all pass) | -| Stale tests | 5 archived | 0 in test tree | - ---- - -## Sprint 1: Test Fixes ✅ COMPLETE - -- [x] Fix `mcp_guardrails.py`: `validate_mcp_syntax()` now walks `mcp_handlers/` package instead of referencing deleted monolithic `mcp_handlers.py` -- [x] Archive 5 stale tests: `test_budget_manager.py` (BudgetPolicy class deleted), `test_skill_container.py`, `test_skill_export_import.py`, `test_skill_surface_repair.py`, `test_skill_surface_restart.py` (imported deleted modules) -- [x] Fix `test_api_clipboard.py`: both `asyncSetUp` and `asyncTearDown` had wrong import path `app.menu.clipboard_buffer` → `app.clipboard.clipboard_buffer` -- [x] Fix `test_api_chat.py`: `test_list_models` count relaxed from `>= 4` to `>= 1`; `test_list_models_structure` relaxed provider fields assertion -- [x] Result: **502 passed, 0 failed, 6 warnings** - ---- - -## Sprint 2: MCP Server Self-Identification & Repair (NEXT) - -### Goal -uCore must be able to identify its own system state, determine if it's working, and REPAIR itself — no workarounds. - -### Current Repair Infrastructure (audit) - -| Component | File | Status | -|-----------|------|--------| -| MCP Guardrails | `backend/app/api/mcp_guardrails.py` | ✅ 6/6 checks pass | -| MCP Self-Heal Skill | `backend/app/skills/builtin/skill_mcp_self_heal.py` | ✅ Loads, passes health check | -| Plate Refresh Engine | `backend/plate_refresh/refresh.py` | ✅ DESTROY/REBUILD interactive menu | -| Plate Verification | `backend/plate_refresh/verification.py` | ✅ verify_plate(), promotion criteria | -| Plate Monitoring | `backend/plate_refresh/monitoring.py` | ✅ Usage tracking, drift detection | -| Dev Destroy Rebuild Skill | `backend/app/skills/builtin/skill_dev_destroy_rebuild.py` | ✅ Registered in skills registry | -| Health Watchdog | `backend/health/health_watchdog.py` | ✅ | -| AI Stack Rebuild Script | `scripts/ai_stack_rebuild_and_verify.sh` | ✅ Shell script | -| AI Stack Health Check | `scripts/ai_stack_health_check.sh` | ✅ Shell script | -| Bootstrap Script | `scripts/bootstrap.sh` | ✅ | - -### Gaps to Address - -1. **No unified system health endpoint** — `validate_mcp_integrity()` only checks MCP layer. Need a `GET /api/health/full` that aggregates: backend HTTP, MCP integrity, skill registry, plate health, database connectivity, disk space, frontend dev server -2. **No automated repair endpoint** — `skill_mcp_self_heal` can only be invoked via MCP. Need a `POST /api/system/repair` endpoint that triggers multi-component self-heal -3. **Snackbar crash loop unresolved** — per fieldnotes, backend exits after maintenance scheduler starts on launchd. Root cause never fixed. Affects self-identification (can't report health if process is dead) -4. **No startup sequence validation** — no check that all required services start in order and report ready -5. **No auto-recovery on crash** — launchd restarts the process but doesn't diagnose why it died - -### Plan - -- [x] **Sprint 2.1:** Fix snackbar crash loop root cause — backend currently running healthy, crash loop not active. Root cause identified as `web.run_app()` blocking + maintenance scheduler `cleanup_ctx` completing without error catching. Fix deferred to next session. -- [x] **Sprint 2.2:** Build `GET /api/health/full` — unified system health endpoint (7 checks: http_server, mcp_integrity, skill_registry, plate_health, database, disk_space, maintenance_scheduler) — **DONE** (`bfc68b8`) -- [x] **Sprint 2.3:** Build `POST /api/system/repair` — triggers MCP self-heal + plate verify + skill registry reload — **DONE** (`bfc68b8`) -- [x] **Sprint 2.4:** Add startup sequence validation — `__main__.py` now runs `get_full_health()` on boot, auto-repairs if degraded — **DONE** -- [x] **Sprint 2.5:** Wire health/repair into Developer Surface → Control Panel — QuickActions now has "Health Check" + "System Repair" buttons — **DONE** -- [x] **Sprint 2.6:** Run full test suite — **502 passed, 0 failed, frontend builds clean** - ---- - -## Sprint 3: @udos/usx-tokens npm Publishing - -### Goal -Publish `@udos/usx-tokens` to npm (public) so it can be installed as a versioned dependency instead of a `file:` protocol link. - -### Current State -- Package v3.0.0 exists at `~/Code/HomeNest/packages/usx-tokens/` -- uCore links via `"file:../../HomeNest/packages/usx-tokens"` ✅ -- Package has publishConfig `"access": "public"` ✅ -- uCore-specific themes (c64, teletext, high-contrast) NOT in package — only uCore has them -- HomeNest 10-foot console additions (console-grid, controller-focus, media-player) in package - -### Plan - -- [ ] **Sprint 3.1:** Sync c64, teletext themes into the shared package (optional — could stay uCore-only) -- [ ] **Sprint 3.2:** Test `npm publish --access public` from HomeNest packages/usx-tokens/ -- [ ] **Sprint 3.3:** Switch uCore from `file:` to versioned `"@udos/usx-tokens": "^3.0.0"` with npm registry as fallback -- [ ] **Sprint 3.4:** Update docs/USX_ALIGNMENT_2026-07.md with final state -- [ ] **Sprint 3.5:** Add CI/CD note about npm publish on token changes - ---- - -## Overall Health/Maintenance/Install/Repair Strategy - -### Philosophy -uCore must be self-aware and self-healing. No manual diagnosis. No workarounds. - -### Layers - -1. **Runtime Health** — `GET /api/health` (exists) + `GET /api/health/full` ✅ (7 concurrent checks in `system_health.py`) -2. **Structural Integrity** — `validate_mcp_integrity()` ✅ (all 6 checks pass) -3. **Self-Repair** — `POST /api/system/repair` ✅ (triggers MCP self-heal + registry reload + plate verify) -4. **Deep Recovery** — DESTROY/REBUILD protocol via `skill_dev_destroy_rebuild` (exists) -5. **Bootstrap** — `scripts/bootstrap.sh` + `scripts/ai_stack_rebuild_and_verify.sh` (exists) -6. **Watchdog** — `backend/health/health_watchdog.py` (exists) - -### Startup Flow (target) -``` -1. launchd starts backend on port 8484 -2. Backend initializes: DB migration → skill registry → maintenance scheduler -3. Backend runs startup health check (MCP + skills + plates + DB + disk) -4. If all checks pass → report "ready", start accepting requests -5. If any check fails → report error, attempt self-repair before accepting requests -6. Frontend polls /api/health/full and shows system status in Control Panel \ No newline at end of file diff --git a/.tasker/sprints/sprint-plan-2026-08-10-adapters.md b/.tasker/sprints/sprint-plan-2026-08-10-adapters.md deleted file mode 100644 index 82ae94ad..00000000 --- a/.tasker/sprints/sprint-plan-2026-08-10-adapters.md +++ /dev/null @@ -1,42 +0,0 @@ -# Sprint B — Move Adapters to uFlow and uKnowledge - -**Status:** Complete -**Started:** 2026-08-10 -**Completed:** 2026-08-13 -**Scope:** Move workflow_adapter and knowledge_adapter out of uCore into their target repos - ---- - -## Outcome - -uCore no longer contains thin `workflow_adapter.py` / `knowledge_adapter.py` -wrappers. The extension registry wires `uflow` and `uknowledge` directly: - -- `route_registrar: uflow.routes.register_routes` (entrypoint `uflow.setup`) -- `route_registrar: uknowledge.routes.register_routes` (entrypoint `uknowledge.setup`) - -Each external package owns its own path discovery (`setup(app)` in its -`__init__.py` adds the local repo to `sys.path` when running split-repo). - -## Completed - -- [x] `workflow_adapter.py` removed from uCore -- [x] `knowledge_adapter.py` removed from uCore -- [x] `app/extensions/adapters/__init__.py` updated — only runtime adapters remain -- [x] Registry declares `uflow`/`uknowledge` with direct import paths -- [x] uFlow `__init__.py` provides `setup(app)` + path discovery -- [x] uKnowledge `__init__.py` provides `setup(app)` + path discovery -- [x] uFlow + uKnowledge `ucore-extension.json` point at `uflow.setup` / `uknowledge.setup` -- [x] Fixed `scripts/check_knowledge_route_contract.py` to add the uKnowledge repo path in split-repo dev mode - -## Verification (all pass) - -- [x] `python3 scripts/validate_extension_manifests.py` -- [x] `python3 scripts/smoke_split_repo_imports.py` — route_count=272, identity_pairs=11 -- [x] `python3 scripts/check_knowledge_route_contract.py` — 20/20 routes -- [x] Backend boots with external routes registered (knowledge + workflow respond 200) - -## Notes - -- `/api/knowledge/status` is a stub in uKnowledge (returns 501 "Not implemented - in uKnowledge yet") — expected, route registration itself is proven working. \ No newline at end of file diff --git a/.tasker/sprints/sprint-plan-browserui-research-portal.md b/.tasker/sprints/sprint-plan-browserui-research-portal.md deleted file mode 100644 index 8e298e5b..00000000 --- a/.tasker/sprints/sprint-plan-browserui-research-portal.md +++ /dev/null @@ -1,113 +0,0 @@ -# Sprint D — BrowserUI Research Portal Upgrade - -**Status:** Complete -**Started:** 2026-08-11 -**Scope:** Transform BrowserUI into an automated research web portal with binder integration, ChatUI assistant bridge, and prose-style preview/editing tabs. - ---- - -## Current State - -### BrowserUI (single file, 754 lines) -At `frontend-vue/src/surfaces/browserui/BrowserUISurface.vue`: -- Card stacks with hardcoded defaults (Research, Bookmarks, Learning) -- Search/filter, slide-in editor panel (Expand, Summarise, Research buttons) -- "Open in Markdown Editor" dispatches to workflow store -- Uses `webScraper.ts` → `/api/editor/scrape-web` - -### Existing Backend -- `editor_api.py`: scrape, summarise, save-to-binder endpoints -- `chat.py`: `scrape_web` and `save_to_vault` chat tools (from Sprint C) -- `mission_task_binder_adapter.py`: Binder metadata adapter -- `chat_context.py`: Vault context gathering (from Sprint C) - -### What's Missing -- No research queue API (async scrape→summarise→save pipeline) -- No quality scoring (auto-compute from token count + AI confidence) -- No binder metadata files (`binder.json`, `CITATIONS.md`) -- No ChatUI assistant bridge from BrowserUI -- Summarise/Enhance buttons are stubs (no backend AI call) -- No "Research" tab with request → queue → result flow -- Cards not sortable by score/category -- No prose preview vs raw edit distinction - ---- - -## Architecture - -``` -BrowserUI Shell -├── ResearchDashboard.vue Queue, status, progress, "Research Gaps" -├── CardStack.vue Sortable cards, score badges, tag filters -├── PreviewTab.vue Prose-style rendered markdown -├── EditTab.vue Markdown editor with binder context -└── ApiBridge.ts Unified API client - -Backend (new) -├── research_api.py POST /api/research/start, /status, /cancel -├── binder_api.py GET/POST/PATCH /api/binder/* -├── research_queue.py Async job: scrape→summarise→enhance→save -└── quality_scorer.py ext Auto-score: tokens × confidence → 0-5 -``` - ---- - -## Tasks - -### Wave 1: Backend Research API (2 hours) - -- [x] **browser.001** Create `research_api.py` — `POST /api/research/start`, `GET /api/research/status`, `POST /api/research/cancel`. Body: { url, binderId, tags, mode: "summarise"|"enhance"|"full" }. -- [x] **browser.002** Create `research_queue.py` — lightweight async job queue (SQLite-backed). Jobs: scrape → summarise (ChatUI) → enhance (optional) → save to binder. Track state/progress. -- [x] **browser.003** Create `binder_api.py` — `GET /api/binder/list` (all binders with metadata), `POST /api/binder/add`, `PATCH /api/binder/update`, `PATCH /api/binder/score`. -### Wave 2: Frontend Panel Decomposition (3 hours) - -- [x] **browser.005** Split BrowserUI into shell + sub-panels. `BrowserUISurface.vue` becomes a tab router. Create `panels/` directory with `ResearchDashboard.vue`, `CardStack.vue`, `PreviewTab.vue`, `EditTab.vue`. -- [x] **browser.006** `CardStack.vue` — sortable by date/score/category. Drag-and-drop between stacks. Category tags as filter chips. Quality score badge (color-coded 0-5 green→yellow→red). "Research" quick-action per card. -- [x] **browser.007** `PreviewTab.vue` — prose-style rendered markdown. Frontmatter metadata table. Source citation block with favicon. Expand/collapse sections. "Send to ChatUI" button. -- [x] **browser.008** `EditTab.vue` — markdown editor. Binder context indicator. Auto-insert frontmatter. Save-to-binder with confirmation. Undo/redo via textarea history. -- [x] **browser.009** `ResearchDashboard.vue` — queue list with progress bars. Scraper logs panel. "Approve & Commit" per-topic. "Request Research" button pushes to ChatUI. "Research Gaps" section from vault analysis. - -### Wave 3: ChatUI Assistant Bridge (2 hours) - -- [x] **browser.010** `POST /api/research/summarise` — delegates to ChatUI Plan mode. Body: { text, model, budget }. Returns structured summary + citations + suggested tags. -- [x] **browser.011** "Send to ChatUI" button in PreviewTab → dispatch to `chatStore.sendMessage()` with research content pre-filled in Plan mode. Opens AssistUI surface or inline chat panel. -- [x] **browser.012** Batch research: multi-select cards → "Research All" → queue processes each → results grouped into single binder with cross-reference index. - -### Wave 4: Binder File System (2 hours) - -- [x] **browser.013** `binder.json` schema: `{ name, description, created, updated, score, tags, sources: [{url, title, date}] }`. Created per binder directory. -- [x] **browser.014** `CITATIONS.md` auto-generation: append source URL + access date + title on scrape complete. Format: `- [Title](URL) — accessed YYYY-MM-DD` -- [x] **browser.015** Save flow: research result → `~/Vault//` or `global-knowledge//`. Update `SUMMARY.md` with new entry. - -### Wave 5: Polish & Verification (2 hours) - -- [x] **browser.016** Vault knowledge gap detection: scan `SUMMARY.md` for broken/missing links → suggest topics in "Research Gaps" dashboard section. -- [x] **browser.017** Topic enhancement: existing binder doc → "Enhance" → ChatUI expands with deeper detail → diff preview → approve merge. -- [x] **browser.018** E2E smoke test: scrape URL → summarise via ChatUI → save to binder → view in PreviewTab → edit → approve → verify CITATIONS.md updated. - ---- - -### Exit Criteria -1. BrowserUI has Dashboard, Preview, Edit tabs with card stack + queue view -2. Cards sortable by score/category/tags with quality score badge -3. Research queue API: scrape → ChatUI summarise → save to binder, end to end -4. "Send to ChatUI" pushes content to AssistUI Plan mode -5. Binder files created with binder.json + CITATIONS.md + SUMMARY.md updated -6. All new endpoints respond and tests pass - -### Files to Create -- `backend/app/api/research_api.py` — async research endpoints -- `backend/app/services/research_queue.py` — SQLite-backed job queue -- `backend/app/api/binder_api.py` — binder CRUD endpoints -- `frontend-vue/src/surfaces/browserui/panels/ResearchDashboard.vue` -- `frontend-vue/src/surfaces/browserui/panels/CardStack.vue` -- `frontend-vue/src/surfaces/browserui/panels/PreviewTab.vue` -- `frontend-vue/src/surfaces/browserui/panels/EditTab.vue` -- `frontend-vue/src/surfaces/browserui/ApiBridge.ts` — unified API client - -### Files to Modify -- `frontend-vue/src/surfaces/browserui/BrowserUISurface.vue` → shell with tab routing -- `backend/app/api/routes.py` → register research + binder routes -- `backend/app/services/quality_scorer.py` → add auto-score function - -- [x] **browser.004** Extend `quality_scorer.py` — auto-score: token count × AI confidence → 0-5 scale with color code. Store in binder metadata. diff --git a/.tasker/sprints/sprint-plan-openrouter-ask-plan-act.md b/.tasker/sprints/sprint-plan-openrouter-ask-plan-act.md deleted file mode 100644 index fff4440d..00000000 --- a/.tasker/sprints/sprint-plan-openrouter-ask-plan-act.md +++ /dev/null @@ -1,89 +0,0 @@ -# Sprint C — OpenRouter/Free Ask → Plan → Act Pipeline in uCore Chat - -**Status:** Complete -**Started:** 2026-08-11 -**Scope:** Implement OpenRouter/Free integration with Ask/Plan/Act modes in uCore Chat, with vault+repo context enrichment, budget-aware routing, and Act-mode tool whitelist. - ---- - -## Current State - -### What Exists -- **Chat UI**: AssistUI with Chat + Workflow tabs, model picker, SSE streaming -- **Provider Router**: `provider_router.py` already speaks OpenRouter + Ollama -- **Chat API**: `chat.py` with tool-calling loop, system prompts about vault/knowledge APIs -- **OpenRouter Config**: `config/openrouter.yaml` with tier-based models (free, ultra-cheap, budget, mid-range, premium) -- **Budget Manager**: `budget_manager.py` with session/daily/monthly tracking, circuit breaker -- **Vaults**: `~/Vault/`, `~/Shared/`, `~/Public/` via `vault_api.py` -- **Repos**: `/api/developer/repos/*` — 12 routes for repo CRUD -- **Content Tools**: `editor_api.py` — web scraping, summarization, save-to-binder - -### What's Missing -- No Ask/Plan vs Act mode distinction in chat -- No automated context enrichment from vaults + repos -- No budget-aware routing in chat pipeline -- No Act-mode tool whitelist -- No plan-parsing or interactive plan cards - ---- - -## Tasks - -### Wave 1: Backend — OpenRouter/Free Chat Pipeline (3-4 hours) - -- [x] **openrouter.001** Add free-tier models to ProviderRouter — Register config/openrouter.yaml free-tier models with priority below Ollama -- [x] **openrouter.002** Create Ask/Plan system prompt — New `_UCORE_ASK_PLAN_SYSTEM` and `_UCORE_ACT_SYSTEM` in chat.py with vault topology, repo paths, skills -- [x] **openrouter.003** Add mode routing to POST /api/chat — Accept `mode: "ask" | "plan" | "act"` with enriched prompts -- [x] **openrouter.004** Wire BudgetManager into chat pipeline — budget check before routing, Ollama fallback on exhaustion -- [x] **openrouter.005** Add GET /api/chat/modes endpoint — Returns available modes, descriptions, budget status - -### Wave 2: Backend — Context Enrichment Engine (3-4 hours) - -- [x] **openrouter.006** Build Vault Context Gatherer — Scan vaults for relevant .md/.yaml files matching user query -- [x] **openrouter.007** Build Repo Context Gatherer — git grep across repos for relevant snippets -- [x] **openrouter.008** Build Skill/Snack Catalog Gatherer — Load ecosystem registry and skills metadata -- [x] **openrouter.009** Integrate context enrichment into chat pipeline — gather_context() before routing -- [x] **openrouter.010** Add plan-parsing to response handler — Parse structured plan steps from LLM output - -### Wave 3: Frontend — Ask/Plan/Act Mode Toggle (3-4 hours) - -- [x] **openrouter.011** Extend AssistUI mode toggle from 2 to 4 modes — Chat, Plan, Act, Workflow -- [x] **openrouter.012** Add budget indicator to chat topbar — Remaining daily budget, current tier badge -- [x] **openrouter.013** Add Plan card renderer — Interactive plan cards with checkboxes, "Execute in Act" button -- [x] **openrouter.014** Add free-tier models to model picker — Show free models with "free" badge - -### Wave 4: Act Mode — Vault Write & Content Operations (3-4 hours) - -- [x] **openrouter.015** Define Act-mode tool whitelist — scrape_web, save_to_vault implemented as chat tools -- [x] **openrouter.016** Add scrape-web as a chat tool — Wired _html_title/desc/body_text extractors in chat.py -- [x] **openrouter.017** Add save-to-vault as a chat tool — Saves to ~/Vault/ with frontmatter and CITATIONS.md -- [x] **openrouter.018** Add Act-mode confirmation gate — assistui-act-confirm component with approve/cancel - -### Wave 5: Verification & Documentation (2-3 hours) - -- [x] **openrouter.019** Write test suite — 12 chat tests + 4 research queue tests passing -- [x] **openrouter.020** Create FEATURE_SPEC — docs/FEATURE_SPEC_OPENROUTER_ASK_PLAN_ACT.md -- [x] **openrouter.021** Update devlog, fieldnotes, wisdom — Docs round complete, version bump to v0.2.0-dev -- [x] **openrouter.022** End-to-end smoke test infrastructure ready — Plan mode → Act → vault-result pipeline wired - ---- - -### Exit Criteria -1. User can toggle between Chat, Plan, Act, and Workflow in AssistUI -2. Plan mode sends prompts to OpenRouter free tier with vault+repo+skill context -3. Plan responses contain structured plan_steps[] rendered as interactive cards -4. Act mode has a whitelist of safe tools -5. Act mode requires user confirmation before executing any tool -6. Budget is tracked per request; exhausted budget falls back to Ollama -7. Free-tier OpenRouter models appear in the model picker with cost badges -8. Full end-to-end Plan → Approve → Act → Vault-result flow passes manual test - -### Files to Create -- `backend/app/services/chat_context.py` — Vault + repo + skill context gathering -- `docs/FEATURE_SPEC_OPENROUTER_ASK_PLAN_ACT.md` — Feature specification - -### Files to Modify -- `backend/app/api/chat.py` — Mode routing, context enrichment, Act tool whitelist, plan parsing -- `backend/app/services/provider_router.py` — Free-tier model registration, budget-aware routing -- `frontend-vue/src/stores/chat.ts` — Ask/Act modes, budget state, plan step types -- `frontend-vue/src/surfaces/assistui/AssistUISurface.vue` — 4-mode toggle, plan cards, budget indicator diff --git a/.tasker/test-task.md b/.tasker/test-task.md deleted file mode 100644 index 66ba88e3..00000000 --- a/.tasker/test-task.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -uid: task.test.001 -title: Test Ollama Connection -description: Run ollama list and verify models are available -priority: low -lane: dev-flow -tags: [test, ollama] ---- -# Task: Test Ollama Connection - -## Steps -1. Run `ollama list` -2. Verify at least 3 models are available -3. Report the output - -## Expected Result -- At least 3 models listed -- No errors diff --git a/.tasker/vaul-consolidation-sync-repair.md b/.tasker/vaul-consolidation-sync-repair.md deleted file mode 100644 index ae1290a9..00000000 --- a/.tasker/vaul-consolidation-sync-repair.md +++ /dev/null @@ -1,52 +0,0 @@ -# Vault Consolidation & Sync Repair - Sprint Plan - -**Date:** 2026-07-26 -**Status:** Complete -**Branch:** vault-consolidation-sync-repair - -## Objectives - -### O1: Consolidate Vault Content — ✅ Complete -- No stale vault content found at `~/.ucore/vaults/` (doesn't exist) -- `~/Vault/`, `~/Shared/`, `~/Public/` already have content and correct structure -- No stale config paths in `~/.ucore/config/`, `backend/config/`, or `frontend-vue/src/` -- **Result:** Nothing to migrate. Architecture already correct. - -### O2: Update Documentation — ✅ Complete -Files updated: -- `docs/VAULT_BINDER_WORKFLOW_INTEGRATION.md` — Converted from 5-layer to 3-type vault system (User/Shared/Public). Added Section 9: "Lane Separation — Boundary Rules" with agent boundary rules, mental model diagram. Removed Code layer (now part of Developer Lane). Corrected diagram and all path references. -- `docs/DEVELOPER_SURFACE.md` — Added "Lane Separation — Read This First" section at top with boundary table, agent rules, vault architecture reference. Added "Vault vs Code — The Boundary" section at bottom. -- `docs/FEATURE_SPEC_ASSISTUI_DEVELOPER_CHAT_LANE_SEPARATION.md` — Added "Lane Separation — Boundary Rules" section with User/Developer Lane breakdown, boundary rules, lane diagram. Corrected vault path reference from `~/Public/global-knowledge/, ~/Code/` to `~/Public/ (reference)`. - -### O3: Diagnose AppFlowy Sync — ✅ Complete -Issues found and fixed: -- `backend/app/af_manager/config.py` — Fallback config had stale paths: `~/Vault/Public` and `~/Vault/Shared` instead of `~/Public/` and `~/Shared/`. Fixed to canonical 3-layer vault architecture. -- `config/vault-sync.example.yaml` — Updated from 5-layer topology comment and removed stale `code-docs` container. Merged global + public containers into single Public vault entry. -- Sync tested with dry-run: **566 files detected**, engine working correctly. -- AppFlowy data directory exists and is populated (local workspace `604455883014934528`, cloud workspace `604455307002777600`). -- Note: `config/vault-sync.yaml` doesn't exist yet (only the `.example.yaml` template). Sync will work when user copies the example and customizes. - -### O4: Audit Vault Filepicker Sidebar — ✅ Complete -Files updated: -- `frontend-vue/src/skills/molecules/WorkspaceFilter.vue` — Fixed filter to show all 3 vault types (was hard-coded to only `user`). Added `shared` and `public` to static fallback list. Updated topology filter from `layer.id === 'user'` to `['user', 'shared', 'public'].includes(layer.id)`. -- `backend/app/api/vault_api.py` — Rewrote topology from 5-layer (User/Shared/Global/Public/Code) to 3-layer (User Vault, Shared Vaults, Public Vaults). Added `PUBLIC_SUB_LAYERS` for granular public vault access. Added `permissions` field. Removed Code layer (now Developer Lane). - -## Summary of Changes - -| File | Change | -|------|--------| -| `docs/VAULT_BINDER_WORKFLOW_INTEGRATION.md` | Major rewrite: 3 vault types, lane separation section, corrected diagram | -| `docs/DEVELOPER_SURFACE.md` | Added lane separation warning at top, boundary section at bottom | -| `docs/FEATURE_SPEC_ASSISTUI_DEVELOPER_CHAT_LANE_SEPARATION.md` | Added boundary rules section, corrected vault path | -| `backend/app/api/vault_api.py` | 3-layer topology, removed Code, merged Global+Public, added sub-layers | -| `backend/app/af_manager/config.py` | Fixed stale fallback paths to canonical 3-layer architecture | -| `config/vault-sync.example.yaml` | Updated topology comment, merged containers, removed Code | -| `frontend-vue/src/skills/molecules/WorkspaceFilter.vue` | Show all 3 vault types in dropdown | -| `.tasker/vaul-consolidation-sync-repair.md` | This sprint plan (new) | - -## Verification -- All documentation: 3 vault types (User, Shared, Public), Code excluded from vaults -- Lane separation rules: documented in all 3 core docs with agent boundary rules -- Backend topology: returns 3 layers with `permissions` field -- Sync engine: dry-run passes with 566 files detected -- Sidebar filter: now shows User, Shared, and Public vaults \ No newline at end of file diff --git a/.tasker/writing/todo-draft-article-outline-seed-draft-article-outline.md b/.tasker/writing/todo-draft-article-outline-seed-draft-article-outline.md deleted file mode 100644 index 5ae3e194..00000000 --- a/.tasker/writing/todo-draft-article-outline-seed-draft-article-outline.md +++ /dev/null @@ -1,14 +0,0 @@ -# Draft article outline - -- status: todo -- source: seed-user-workflow -- source_id: seed-draft-article-outline -- synced_at: 2026-07-15T13:47:33Z -- priority: medium -- mission: Writing Project -- task: Draft article outline -- binder: writing -- tags: writing, content - -## Summary -Create a clear outline before drafting sections. diff --git a/CONTEXT.md b/CONTEXT.md index dbb099c9..e817144e 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -64,9 +64,9 @@ HomeRuntime should be treated as part of the HomeNest ecosystem. If the runtime ## Data -All mutable data lives in `~/.ucore/`: +All mutable ecosystem data lives beneath `~/Code/.udos/` (or `$UDOS_HOME`): -- `~/.ucore/indices/library.db` — FTS5 search index -- `~/.ucore/knowledge/shared.db` — Multi-agent memory -- `~/.ucore/logs/` — Spool files -- `~/.ucore/secrets.enc` — Encrypted secrets +- `~/Code/.udos/indices/library.db` — FTS5 search index +- `~/Code/.udos/knowledge/shared.db` — Multi-agent memory +- `~/Code/.udos/logs/` — Spool files +- `~/Code/.udos/secrets/` — Encrypted secrets diff --git a/README.md b/README.md index 5cae3358..44d130d6 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,6 @@ Self-hosted runner prerequisites: What it runs: -- python3 scripts/check_snackmachine_contract.py - bash scripts/smoke_snackmachine_integration.sh ## Architecture @@ -88,7 +87,8 @@ knowledge, and domain plugins — live in dedicated repos and plug in via a lightweight extension contract. ``` -VS Code (Cline) → MCP Bridge → uCore (port 8484) +Codex (external development) → Git/GitHub → uCore (port 8484) +uCore guided agents → Ollama/OpenRouter/OpenAI APIs │ ├── Core shell (uCore host-only) │ ├── Skills (15 built-in) — backup, sync, route, ask vault @@ -148,12 +148,12 @@ VS Code (Cline) → MCP Bridge → uCore (port 8484) | Surface | Route | Description | | ------------- | -------------------- | --------------------------------------------------------------------- | | Dashboard | `/` | Main landing, Dev Mode filtering | -| Assistant | `/assistui` | AI chat & agent-assisted workflows | -| Server | `/server` | Server management | +| Intelligence | `/intelligence` | Chat, planning, models, agents, budget, and history | +| Snackbar | `/snackbar` | Services, feeds, skills, snacks, extensions, logs, and MCP | | Developer | `/developer` | Developer tools | | System | `/system` | System settings | | Workflow | `/workflow` | Workflow builder | -| SnackMachine | `/server?tab=snacks` | Core snack workspace (packaged snacks via SnackMachine extension) | +| SnackMachine | `/snackbar?tab=snacks` | Core snack workspace (packaged snacks via SnackMachine extension) | | BrowserUI | `/browserui` | Browser automation | | Documentation | `/documentation` | Docs viewer | | uCode | `/ucode` | uCode runtime bridge: GridCore, GridSmith, teletext, terminal widgets | @@ -168,7 +168,7 @@ Canonical docs live in **[uDocs](https://github.com/uDosGo/uDocs)**: | [API Reference](https://github.com/uDosGo/uDocs/blob/main/api/rest-api.md) | All endpoints with examples | | [Runbooks](https://github.com/uDosGo/uDocs/blob/main/runbooks/development.md) | Setup, deploy, backup, troubleshooting | | [Surfaces](https://github.com/uDosGo/uDocs/tree/main/surfaces) | All 12 surfaces | -| [Cline Guide](https://github.com/uDosGo/uDocs/blob/main/guides/cline-roundtable-setup.md) | Cline + Roundtable orchestration | +| [Agent Architecture](docs/AGENT_EXECUTION_ARCHITECTURE.md) | Guided model routing and orchestration | Local docs in `docs/` cover vault plates, USX layout, and system specs. diff --git a/backend/app/api/config_api.py b/backend/app/api/config_api.py index 86a5a10a..a261c769 100644 --- a/backend/app/api/config_api.py +++ b/backend/app/api/config_api.py @@ -57,9 +57,13 @@ async def handle_get_config(request: web.Request) -> web.Response: ], "installation": [ _entry("UDOS_ROOT", str(settings.udos_root)), + _entry("UDOS_HOME", str(settings.udos_home)), _entry("Data Dir", str(settings.data_dir)), _entry("Config Dir", str(settings.config_dir)), _entry("Logs Dir", str(settings.logs_dir)), + _entry("User Vault", str(settings.vault_root)), + _entry("Shared Vaults", str(settings.shared_vault_root)), + _entry("Public Vaults", str(settings.public_vault_root)), _entry("Surface Registry", settings.surface_registry_path), _entry("Install Name", settings.install_name), ], diff --git a/backend/app/api/dev_layer_api.py b/backend/app/api/dev_layer_api.py index 0a4ec9f1..c2d2e16e 100644 --- a/backend/app/api/dev_layer_api.py +++ b/backend/app/api/dev_layer_api.py @@ -5,6 +5,7 @@ POST /api/dev-layer/toggle — Cycle through OFF → MINIMAL → ON → OFF GET /api/dev-layer/hud — Aggregate Dev HUD data (tasks + vars + capabilities + actions) """ + from __future__ import annotations import json @@ -13,6 +14,7 @@ from aiohttp import web +from app.core.settings import settings from app.services.dev_layer import DevMode, get_dev_layer log = logging.getLogger("ucore.api.dev_layer") @@ -35,9 +37,12 @@ async def handle_set_dev_state(request: web.Request) -> web.Response: mode_str = str(body.get("mode", "")).strip().lower() if mode_str not in ("on", "off", "minimal"): - return web.json_response({ - "error": "Invalid mode. Use: on, off, or minimal", - }, status=400) + return web.json_response( + { + "error": "Invalid mode. Use: on, off, or minimal", + }, + status=400, + ) layer = get_dev_layer() layer.mode = DevMode(mode_str) @@ -56,6 +61,7 @@ async def handle_toggle_dev_state(request: web.Request) -> web.Response: # ── Dev HUD Aggregate ───────────────────────────────────────────── + async def handle_get_dev_hud(request: web.Request) -> web.Response: """GET /api/dev-layer/hud — aggregate Dev HUD data. @@ -75,10 +81,12 @@ async def handle_get_dev_hud(request: web.Request) -> web.Response: # ── HUD sub-collectors ───────────────────────────────────────────── + def _get_tasker_hud() -> dict: """Collect tasker summary from the .tasker markdown boards.""" try: from app.services.workflow_status import default_tasker_dir + base = default_tasker_dir() except Exception: return {"error": "tasker not available"} @@ -133,7 +141,7 @@ def _parse_markdown_task_inline(path: Path) -> dict: def _get_variables_hud() -> dict: """Collect user variables for the Dev HUD.""" try: - var_file = Path.home() / ".ucore" / "data" / "variables.json" + var_file = settings.data_dir / "variables.json" if var_file.exists(): return {"user": json.loads(var_file.read_text(encoding="utf-8"))} except Exception: @@ -146,8 +154,11 @@ def _get_capabilities_hud() -> dict: caps: dict = {} try: from app.extensions.registry import registry + ext = registry.get_extensions() - running = {e.get("id", "") for e in ext if isinstance(e, dict) and e.get("status") == "online"} + running = { + e.get("id", "") for e in ext if isinstance(e, dict) and e.get("status") == "online" + } except Exception: running = set() diff --git a/backend/app/api/developer_api.py b/backend/app/api/developer_api.py index 034592dd..a8f5d33f 100644 --- a/backend/app/api/developer_api.py +++ b/backend/app/api/developer_api.py @@ -1,4 +1,5 @@ """Developer API — local repo discovery and workspace file listing.""" + from __future__ import annotations import subprocess @@ -13,12 +14,31 @@ # ─── File discovery constants (stable, not policy-driven) ──────── ALLOWED_EXTENSIONS = { - ".md", ".json", ".yaml", ".yml", ".txt", ".csv", - ".py", ".ts", ".tsx", ".js", ".jsx", ".css", ".sh", ".toml", + ".md", + ".json", + ".yaml", + ".yml", + ".txt", + ".csv", + ".py", + ".ts", + ".tsx", + ".js", + ".jsx", + ".css", + ".sh", + ".toml", } IGNORED_DIRS = { - ".git", "node_modules", "dist", "build", ".venv", "venv", - "__pycache__", ".pytest_cache", ".mypy_cache", + ".git", + "node_modules", + "dist", + "build", + ".venv", + "venv", + "__pycache__", + ".pytest_cache", + ".mypy_cache", } MAX_PREVIEW_BYTES = 200_000 @@ -71,10 +91,7 @@ def _looks_like_doc_library(repo_path: Path) -> bool: return True doc_marker_dirs = policy["doc_marker_dirs"] - if any( - (repo_path / marker).exists() - for marker in doc_marker_dirs if marker != "docs" - ): + if any((repo_path / marker).exists() for marker in doc_marker_dirs if marker != "docs"): return True tracked_files = _git_output(repo_path, "ls-files").splitlines() @@ -99,10 +116,7 @@ def _looks_like_doc_library(repo_path: Path) -> bool: if code_files >= 3: return False - return ( - doc_files >= threshold["min_doc_files"] - and code_files <= threshold["max_code_files"] - ) + return doc_files >= threshold["min_doc_files"] and code_files <= threshold["max_code_files"] def _has_code_markers(repo_path: Path) -> bool: @@ -225,17 +239,19 @@ def _list_repos(scope: str = "code", exclude_system: bool = False) -> list[dict] status_lines = _git_output(child, "status", "--porcelain").splitlines() remote = _git_output(child, "remote", "get-url", "origin") or "No remote" changes = len([line for line in status_lines if line.strip()]) - repos.append({ - "id": child.name, - "name": child.name, - "path": str(child), - "branch": branch, - "status": "clean" if changes == 0 else "modified", - "changes": changes, - "remote": remote, - "fileCount": _repo_file_count(child), - "kind": kind, - }) + repos.append( + { + "id": child.name, + "name": child.name, + "path": str(child), + "branch": branch, + "status": "clean" if changes == 0 else "modified", + "changes": changes, + "remote": remote, + "fileCount": _repo_file_count(child), + "kind": kind, + } + ) return repos @@ -261,22 +277,25 @@ def _list_repo_files( continue stat = path.stat() - files.append({ - "id": len(files) + 1, - "name": str(rel_path), - "type": path.suffix.lstrip(".").lower() or "file", - "size": stat.st_size, - "updatedAt": path.stat().st_mtime, - "tags": [path.suffix.lstrip(".").lower()] if path.suffix else [], - "binder": repo_name, - }) + files.append( + { + "id": len(files) + 1, + "name": str(rel_path), + "type": path.suffix.lstrip(".").lower() or "file", + "size": stat.st_size, + "updatedAt": path.stat().st_mtime, + "tags": [path.suffix.lstrip(".").lower()] if path.suffix else [], + "binder": repo_name, + } + ) if len(files) >= limit: break files.sort(key=lambda item: item["updatedAt"], reverse=True) for file in files: - file["updatedAt"] = __import__("datetime").datetime.fromtimestamp( - file["updatedAt"]).isoformat() + file["updatedAt"] = ( + __import__("datetime").datetime.fromtimestamp(file["updatedAt"]).isoformat() + ) return files @@ -320,25 +339,31 @@ def _list_repo_status(repo_name: str) -> dict[str, list[dict[str, Any]]]: path = path.split(" -> ", 1)[1] if x not in {" ", "?"}: - staged.append({ - "file": path, - "code": x, - "status": _status_label(x), - }) + staged.append( + { + "file": path, + "code": x, + "status": _status_label(x), + } + ) if y not in {" "}: - unstaged.append({ - "file": path, - "code": y, - "status": _status_label(y), - }) + unstaged.append( + { + "file": path, + "code": y, + "status": _status_label(y), + } + ) if x == "?": - unstaged.append({ - "file": path, - "code": "??", - "status": "added", - }) + unstaged.append( + { + "file": path, + "code": "??", + "status": "added", + } + ) return {"staged": staged, "unstaged": unstaged} @@ -371,12 +396,14 @@ def _list_repo_review(repo_name: str) -> list[dict[str, Any]]: if " -> " in path: path = path.split(" -> ", 1)[1] status = _status_label(code) - reviews.append({ - "file": path, - "status": status, - "lines": line_counts.get(path, 0), - "summary": _review_summary(code, path), - }) + reviews.append( + { + "file": path, + "status": status, + "lines": line_counts.get(path, 0), + "summary": _review_summary(code, path), + } + ) return reviews @@ -502,7 +529,7 @@ def _clean_inline(value: Any, limit: int = 220) -> str: compact = " ".join(value.split()) if len(compact) <= limit: return compact - return f"{compact[:limit - 3]}..." + return f"{compact[: limit - 3]}..." def _summarize_binder_context( @@ -531,7 +558,9 @@ def _summarize_binder_context( tasks_raw = focus.get("tasks") tasks: list[str] = [] if isinstance(tasks_raw, list): - tasks = [_clean_inline(item, 120) for item in tasks_raw if isinstance(item, str) and item.strip()][:3] + tasks = [ + _clean_inline(item, 120) for item in tasks_raw if isinstance(item, str) and item.strip() + ][:3] source_repo = "" source_path = "" @@ -593,11 +622,13 @@ async def handle_start_developer(request: web.Request) -> web.Response: log.info("[DEVMODE] Developer surface is self-hosted within uCore (uDev retired)") _announce_dev("online") - return web.json_response({ - "success": True, - "message": "Developer surface is integrated into uCore — served by Vite on port 5175", - "dev_mode": {"active": True, "self_hosted": True} - }) + return web.json_response( + { + "success": True, + "message": "Developer surface is integrated into uCore — served by Vite on port 5175", + "dev_mode": {"active": True, "self_hosted": True}, + } + ) async def handle_stop_developer(request: web.Request) -> web.Response: @@ -611,11 +642,13 @@ async def handle_stop_developer(request: web.Request) -> web.Response: log.info("[DEVMODE] Developer surface stop requested (self-hosted, no external process)") _announce_dev("offline") - return web.json_response({ - "success": True, - "message": "Developer surface status set to offline", - "dev_mode": {"active": False} - }) + return web.json_response( + { + "success": True, + "message": "Developer surface status set to offline", + "dev_mode": {"active": False}, + } + ) async def handle_developer_status(request: web.Request) -> web.Response: @@ -635,25 +668,30 @@ async def handle_developer_status(request: web.Request) -> web.Response: active = resp.status < 500 if active: log.debug("[DEVMODE] Vite dev server is active on :5175") - return web.json_response({ - "active": active, - "description": "Developer Surface — self-hosted in uCore (Vite :5175)", - "icon_visible": active - }) + return web.json_response( + { + "active": active, + "description": "Developer Surface — self-hosted in uCore (Vite :5175)", + "icon_visible": active, + } + ) except Exception: log.debug("[DEVMODE] Vite dev server not reachable on :5175") - return web.json_response({ - "active": False, - "description": "Developer Surface — Vite dev server not running", - "icon_visible": False - }) + return web.json_response( + { + "active": False, + "description": "Developer Surface — Vite dev server not running", + "icon_visible": False, + } + ) async def handle_list_repos(request: web.Request) -> web.Response: """GET /api/developer/repos — list code repositories under ~/Code.""" scope = request.query.get("scope", "code") exclude_system = _to_bool( - request.query.get("exclude_system"), default=False, + request.query.get("exclude_system"), + default=False, ) try: repos = _list_repos(scope=scope, exclude_system=exclude_system) @@ -683,13 +721,15 @@ async def handle_list_repo_files(request: web.Request) -> web.Response: ) except FileNotFoundError: return web.json_response({"error": f"Repository not found: {repo_name}"}, status=404) - return web.json_response({ - "repo": repo_name, - "files": files, - "limit": limit, - "include_hidden": include_hidden, - "include_all_extensions": include_all_extensions, - }) + return web.json_response( + { + "repo": repo_name, + "files": files, + "limit": limit, + "include_hidden": include_hidden, + "include_all_extensions": include_all_extensions, + } + ) async def handle_get_repo_file_preview(request: web.Request) -> web.Response: @@ -719,23 +759,27 @@ async def handle_workspace_switch(request: web.Request) -> web.Response: body = await request.json() except Exception: return web.json_response( - {"error": "Invalid JSON body"}, status=400, + {"error": "Invalid JSON body"}, + status=400, ) workspace = body.get("workspace", "") lane = body.get("lane", "ecosystem") if not workspace: return web.json_response( - {"error": "workspace is required"}, status=400, + {"error": "workspace is required"}, + status=400, ) # Store as app-level config for this session request.app["_dev_workspace"] = workspace request.app["_dev_lane"] = lane - return web.json_response({ - "success": True, - "workspace": workspace, - "lane": lane, - }) + return web.json_response( + { + "success": True, + "workspace": workspace, + "lane": lane, + } + ) async def handle_update_repo_file(request: web.Request) -> web.Response: @@ -843,6 +887,7 @@ async def handle_commit_repo_files(request: web.Request) -> web.Response: # ─── Dev Chat ─────────────────────────────────────────────────────── + async def handle_developer_chat(request: web.Request) -> web.Response: """POST /api/developer/chat — dev-lane chat completion. @@ -875,11 +920,16 @@ async def handle_developer_chat(request: web.Request) -> web.Response: binder_fp = _clean_inline(binder_context.get("fingerprint"), 80) log.info( "Dev chat: lane=%s workspace=%s model=%s binder_fp=%s message=%s...", - lane, workspace, model, binder_fp or "none", message[:80], + lane, + workspace, + model, + binder_fp or "none", + message[:80], ) try: from ..services.provider_router import ProviderRouter + router = ProviderRouter() dev_system = ( "You are the uCore Developer Assistant. You work in the Developer Surface " @@ -899,7 +949,7 @@ async def handle_developer_chat(request: web.Request) -> web.Response: "• /api/mcp/tools — list MCP server tools\n" "• /api/mcp/diagnostics — MCP health diagnostics\n\n" "**Health & System:**\n" - "• /api/control/status — full ecosystem health (Cline, Ollama, Hivemind, etc.)\n" + "• /api/control/status — full ecosystem health (Ollama, Hivemind, providers, etc.)\n" "• /api/ollama/status — Ollama model status\n" "• /api/system — system info\n" "• /api/health — health check\n\n" @@ -919,19 +969,24 @@ async def handle_developer_chat(request: web.Request) -> web.Response: chat_messages.extend(history) chat_messages.append({"role": "user", "content": message}) response = await router.chat(messages=chat_messages, model=model) - return web.json_response({ - "response": response.get("content", ""), - "lane": lane, - "workspace": workspace, - "model": response.get("model", model), - "usage": response.get("usage", {}), - }) + return web.json_response( + { + "response": response.get("content", ""), + "lane": lane, + "workspace": workspace, + "model": response.get("model", model), + "usage": response.get("usage", {}), + } + ) except Exception as e: log.error("Dev chat error: %s", e) - return web.json_response({ - "error": str(e), - "message": "Dev chat request failed", - }, status=500) + return web.json_response( + { + "error": str(e), + "message": "Dev chat request failed", + }, + status=500, + ) async def handle_developer_chat_stream(request: web.Request) -> web.StreamResponse: @@ -985,6 +1040,7 @@ async def handle_developer_chat_stream(request: web.Request) -> web.StreamRespon try: from ..services.provider_router import ProviderRouter + router = ProviderRouter() dev_system = ( "You are a developer assistant working in uCore. " @@ -1016,9 +1072,8 @@ async def handle_developer_chat_stream(request: web.Request) -> web.StreamRespon ) if stream_context_lines: - dev_system = ( - f"{dev_system}\n\nBinder context (stream metadata):\n" - + "\n".join(stream_context_lines) + dev_system = f"{dev_system}\n\nBinder context (stream metadata):\n" + "\n".join( + stream_context_lines ) chat_messages = [ {"role": "system", "content": dev_system}, @@ -1030,6 +1085,7 @@ async def handle_developer_chat_stream(request: web.Request) -> web.StreamRespon # Simulate streaming by sending tokens one at a time import asyncio import json + words = content.split(" ") for i, word in enumerate(words): token = word + (" " if i < len(words) - 1 else "") @@ -1039,7 +1095,7 @@ async def handle_developer_chat_stream(request: web.Request) -> web.StreamRespon await response.write(b"data: [DONE]\n\n") except Exception as e: log.error("Dev chat stream error: %s", e) - await response.write(f"data: {{\"error\": \"{str(e)}\"}}\n\n".encode()) + await response.write(f'data: {{"error": "{str(e)}"}}\n\n'.encode()) finally: await response.write_eof() diff --git a/backend/app/api/mcp.py b/backend/app/api/mcp.py index 20e57a48..a3f0c497 100644 --- a/backend/app/api/mcp.py +++ b/backend/app/api/mcp.py @@ -1,6 +1,6 @@ """MCP Integration — Expose uCore skills/tools as MCP tools. -The Model Context Protocol (MCP) lets editors like VS Code/Continue +The Model Context Protocol (MCP) lets compatible external clients discover and call uCore skills directly. MCP Server spec: https://modelcontextprotocol.io @@ -9,6 +9,7 @@ to check structural health. The ``skill_mcp_self_heal`` skill can auto-repair common issues. """ + from __future__ import annotations import json @@ -39,6 +40,7 @@ # ─── MCP Discovery ──────────────────────────────────────── + async def handle_mcp_discover(request: web.Request) -> web.Response: """GET /api/mcp/tools — Return all available MCP tools. @@ -49,274 +51,364 @@ async def handle_mcp_discover(request: web.Request) -> web.Response: # ── Skill tools ───────────────────────────────────────── for skill_meta in _list_skills(): - tools.append({ - "name": f"skill_{skill_meta['id']}", - "description": skill_meta.get("description", ""), + tools.append( + { + "name": f"skill_{skill_meta['id']}", + "description": skill_meta.get("description", ""), + "input_schema": { + "type": "object", + "properties": { + p["name"]: { + "type": p.get("type", "string"), + "description": p.get("description", ""), + } + for p in skill_meta.get("params", []) + }, + }, + } + ) + + # ── Knowledge tools ────────────────────────────────────── + tools.append( + { + "name": "knowledge_search", + "description": "Semantic search across knowledge workspaces", "input_schema": { "type": "object", "properties": { - p["name"]: { - "type": p.get("type", "string"), - "description": p.get("description", ""), - } - for p in skill_meta.get("params", []) + "query": {"type": "string", "description": "Search query"}, + "workspace_id": {"type": "string", "description": "Optional workspace filter"}, + "limit": { + "type": "number", + "description": "Max results (default 10)", + "default": 10, + }, }, + "required": ["query"], }, - }) - - # ── Knowledge tools ────────────────────────────────────── - tools.append({ - "name": "knowledge_search", - "description": "Semantic search across knowledge workspaces", - "input_schema": { - "type": "object", - "properties": { - "query": {"type": "string", "description": "Search query"}, - "workspace_id": {"type": "string", "description": "Optional workspace filter"}, - "limit": {"type": "number", "description": "Max results (default 10)", "default": 10}, - }, - "required": ["query"], - }, - }) - tools.append({ - "name": "knowledge_list_workspaces", - "description": "List all knowledge workspaces", - "input_schema": {"type": "object", "properties": {}}, - }) - tools.append({ - "name": "knowledge_list_documents", - "description": "List documents in a workspace", - "input_schema": { - "type": "object", - "properties": { - "workspace_id": {"type": "string", "description": "Workspace ID (omit for all)"}, + } + ) + tools.append( + { + "name": "knowledge_list_workspaces", + "description": "List all knowledge workspaces", + "input_schema": {"type": "object", "properties": {}}, + } + ) + tools.append( + { + "name": "knowledge_list_documents", + "description": "List documents in a workspace", + "input_schema": { + "type": "object", + "properties": { + "workspace_id": { + "type": "string", + "description": "Workspace ID (omit for all)", + }, + }, }, - }, - }) + } + ) # ── Clipboard tools ────────────────────────────────────── - tools.append({ - "name": "clipboard_capture", - "description": "Capture current clipboard content", - "input_schema": { - "type": "object", - "properties": { - "source": {"type": "string", "description": "Capture source", "default": "user_copy"}, - "metadata": {"type": "object", "description": "Optional metadata"}, + tools.append( + { + "name": "clipboard_capture", + "description": "Capture current clipboard content", + "input_schema": { + "type": "object", + "properties": { + "source": { + "type": "string", + "description": "Capture source", + "default": "user_copy", + }, + "metadata": {"type": "object", "description": "Optional metadata"}, + }, }, - }, - }) - tools.append({ - "name": "clipboard_get", - "description": "Get clipboard item(s)", - "input_schema": { - "type": "object", - "properties": { - "item_id": {"type": "string", "description": "Specific item ID (omit for recent)"}, - "limit": {"type": "number", "description": "Max items (default 50)", "default": 50}, - "include_pinned": {"type": "boolean", "description": "Include pinned items", "default": True}, + } + ) + tools.append( + { + "name": "clipboard_get", + "description": "Get clipboard item(s)", + "input_schema": { + "type": "object", + "properties": { + "item_id": { + "type": "string", + "description": "Specific item ID (omit for recent)", + }, + "limit": { + "type": "number", + "description": "Max items (default 50)", + "default": 50, + }, + "include_pinned": { + "type": "boolean", + "description": "Include pinned items", + "default": True, + }, + }, }, - }, - }) - tools.append({ - "name": "clipboard_delete", - "description": "Delete a clipboard item", - "input_schema": { - "type": "object", - "properties": { - "item_id": {"type": "string", "description": "Item to delete"}, + } + ) + tools.append( + { + "name": "clipboard_delete", + "description": "Delete a clipboard item", + "input_schema": { + "type": "object", + "properties": { + "item_id": {"type": "string", "description": "Item to delete"}, + }, + "required": ["item_id"], }, - "required": ["item_id"], - }, - }) + } + ) # ── Tasker tools ───────────────────────────────────────── - tools.append({ - "name": "tasker_list_boards", - "description": "List all tasker boards", - "input_schema": { - "type": "object", - "properties": { - "tasker_dir": {"type": "string", "description": "Custom tasker directory"}, + tools.append( + { + "name": "tasker_list_boards", + "description": "List all tasker boards", + "input_schema": { + "type": "object", + "properties": { + "tasker_dir": {"type": "string", "description": "Custom tasker directory"}, + }, }, - }, - }) - tools.append({ - "name": "tasker_read_task", - "description": "Read a task from a board", - "input_schema": { - "type": "object", - "properties": { - "board": {"type": "string", "description": "Board name"}, - "task": {"type": "string", "description": "Task ID"}, - "tasker_dir": {"type": "string", "description": "Custom tasker directory"}, + } + ) + tools.append( + { + "name": "tasker_read_task", + "description": "Read a task from a board", + "input_schema": { + "type": "object", + "properties": { + "board": {"type": "string", "description": "Board name"}, + "task": {"type": "string", "description": "Task ID"}, + "tasker_dir": {"type": "string", "description": "Custom tasker directory"}, + }, + "required": ["board", "task"], }, - "required": ["board", "task"], - }, - }) - tools.append({ - "name": "tasker_write_task", - "description": "Create or update a task", - "input_schema": { - "type": "object", - "properties": { - "title": {"type": "string", "description": "Task title"}, - "board": {"type": "string", "description": "Board name (default: inbox)", "default": "inbox"}, - "status": {"type": "string", "description": "Task status (default: todo)", "default": "todo"}, - "body": {"type": "string", "description": "Task body/markdown"}, - "source": {"type": "string", "description": "Creation source"}, - "source_id": {"type": "string", "description": "Source ID"}, - "metadata": {"type": "object", "description": "Custom metadata"}, - "task": {"type": "string", "description": "Task ID (for updates)"}, - "tasker_dir": {"type": "string", "description": "Custom tasker directory"}, + } + ) + tools.append( + { + "name": "tasker_write_task", + "description": "Create or update a task", + "input_schema": { + "type": "object", + "properties": { + "title": {"type": "string", "description": "Task title"}, + "board": { + "type": "string", + "description": "Board name (default: inbox)", + "default": "inbox", + }, + "status": { + "type": "string", + "description": "Task status (default: todo)", + "default": "todo", + }, + "body": {"type": "string", "description": "Task body/markdown"}, + "source": {"type": "string", "description": "Creation source"}, + "source_id": {"type": "string", "description": "Source ID"}, + "metadata": {"type": "object", "description": "Custom metadata"}, + "task": {"type": "string", "description": "Task ID (for updates)"}, + "tasker_dir": {"type": "string", "description": "Custom tasker directory"}, + }, + "required": ["title"], }, - "required": ["title"], - }, - }) - tools.append({ - "name": "tasker_sync_export", - "description": "Sync and export tasker data", - "input_schema": { - "type": "object", - "properties": { - "tasker_dir": {"type": "string", "description": "Custom tasker directory"}, + } + ) + tools.append( + { + "name": "tasker_sync_export", + "description": "Sync and export tasker data", + "input_schema": { + "type": "object", + "properties": { + "tasker_dir": {"type": "string", "description": "Custom tasker directory"}, + }, }, - }, - }) + } + ) # ── Gridsmith tools ────────────────────────────────────── - tools.append({ - "name": "gridsmith_tools_list", - "description": "List available gridsmith tools", - "input_schema": {"type": "object", "properties": {}}, - }) - tools.append({ - "name": "gridsmith_create_grid", - "description": "Create a new grid", - "input_schema": { - "type": "object", - "properties": { - "cols": {"type": "number", "description": "Columns (default 80)", "default": 80}, - "rows": {"type": "number", "description": "Rows (default 24)", "default": 24}, + tools.append( + { + "name": "gridsmith_tools_list", + "description": "List available gridsmith tools", + "input_schema": {"type": "object", "properties": {}}, + } + ) + tools.append( + { + "name": "gridsmith_create_grid", + "description": "Create a new grid", + "input_schema": { + "type": "object", + "properties": { + "cols": { + "type": "number", + "description": "Columns (default 80)", + "default": 80, + }, + "rows": {"type": "number", "description": "Rows (default 24)", "default": 24}, + }, }, - }, - }) - tools.append({ - "name": "gridsmith_latlon_to_ucode", - "description": "Convert lat/lon to uCode coordinate", - "input_schema": { - "type": "object", - "properties": { - "lat": {"type": "number", "description": "Latitude"}, - "lon": {"type": "number", "description": "Longitude"}, - "level": {"type": "number", "description": "Precision level (default 340)", "default": 340}, + } + ) + tools.append( + { + "name": "gridsmith_latlon_to_ucode", + "description": "Convert lat/lon to uCode coordinate", + "input_schema": { + "type": "object", + "properties": { + "lat": {"type": "number", "description": "Latitude"}, + "lon": {"type": "number", "description": "Longitude"}, + "level": { + "type": "number", + "description": "Precision level (default 340)", + "default": 340, + }, + }, + "required": ["lat", "lon"], }, - "required": ["lat", "lon"], - }, - }) - tools.append({ - "name": "gridsmith_ucode_to_latlon", - "description": "Convert uCode coordinate to lat/lon", - "input_schema": { - "type": "object", - "properties": { - "coord": {"type": "string", "description": "uCode coordinate"}, + } + ) + tools.append( + { + "name": "gridsmith_ucode_to_latlon", + "description": "Convert uCode coordinate to lat/lon", + "input_schema": { + "type": "object", + "properties": { + "coord": {"type": "string", "description": "uCode coordinate"}, + }, + "required": ["coord"], }, - "required": ["coord"], - }, - }) - tools.append({ - "name": "gridsmith_import_basic_program", - "description": "Import a basic program into a world", - "input_schema": { - "type": "object", - "properties": { - "program": {"type": "string", "description": "Program code"}, - "world_name": {"type": "string", "description": "Target world name"}, + } + ) + tools.append( + { + "name": "gridsmith_import_basic_program", + "description": "Import a basic program into a world", + "input_schema": { + "type": "object", + "properties": { + "program": {"type": "string", "description": "Program code"}, + "world_name": {"type": "string", "description": "Target world name"}, + }, + "required": ["program", "world_name"], }, - "required": ["program", "world_name"], - }, - }) + } + ) # ── Toon tools ─────────────────────────────────────────── - tools.append({ - "name": "toon_encode", - "description": "Encode data to toon format", - "input_schema": { - "type": "object", - "properties": { - "data": {"type": "string", "description": "Data to encode"}, + tools.append( + { + "name": "toon_encode", + "description": "Encode data to toon format", + "input_schema": { + "type": "object", + "properties": { + "data": {"type": "string", "description": "Data to encode"}, + }, + "required": ["data"], }, - "required": ["data"], - }, - }) - tools.append({ - "name": "toon_stats", - "description": "Get toon encoding statistics", - "input_schema": {"type": "object", "properties": {}}, - }) - tools.append({ - "name": "toon_clear", - "description": "Clear toon state", - "input_schema": {"type": "object", "properties": {}}, - }) + } + ) + tools.append( + { + "name": "toon_stats", + "description": "Get toon encoding statistics", + "input_schema": {"type": "object", "properties": {}}, + } + ) + tools.append( + { + "name": "toon_clear", + "description": "Clear toon state", + "input_schema": {"type": "object", "properties": {}}, + } + ) # ── Flow Router tools ──────────────────────────────────── - tools.append({ - "name": "flow_router_route", - "description": "Route a flow task", - "input_schema": { - "type": "object", - "properties": { - "task": {"type": "string", "description": "Task to route"}, + tools.append( + { + "name": "flow_router_route", + "description": "Route a flow task", + "input_schema": { + "type": "object", + "properties": { + "task": {"type": "string", "description": "Task to route"}, + }, + "required": ["task"], }, - "required": ["task"], - }, - }) - tools.append({ - "name": "flow_router_analytics", - "description": "Get flow router analytics", - "input_schema": {"type": "object", "properties": {}}, - }) - tools.append({ - "name": "flow_router_history", - "description": "Get flow routing history", - "input_schema": { - "type": "object", - "properties": { - "limit": {"type": "number", "description": "Max entries (default 100)", "default": 100}, + } + ) + tools.append( + { + "name": "flow_router_analytics", + "description": "Get flow router analytics", + "input_schema": {"type": "object", "properties": {}}, + } + ) + tools.append( + { + "name": "flow_router_history", + "description": "Get flow routing history", + "input_schema": { + "type": "object", + "properties": { + "limit": { + "type": "number", + "description": "Max entries (default 100)", + "default": 100, + }, + }, }, - }, - }) + } + ) # ── 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, + 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", - "result": { - "tools": tools, - "protocolVersion": "2025-03-26", - "serverInfo": { - "name": "uCore MCP", - "version": "4.0.0", + } + ) + + return web.json_response( + { + "jsonrpc": "2.0", + "result": { + "tools": tools, + "protocolVersion": "2025-03-26", + "serverInfo": { + "name": "uCore MCP", + "version": "4.0.0", + }, }, - }, - "id": None, - }) + "id": None, + } + ) async def handle_mcp_call(request: web.Request) -> web.Response: @@ -324,18 +416,21 @@ async def handle_mcp_call(request: web.Request) -> web.Response: Accepts multiple payload shapes: - Standard MCP: { "name": "tool_name", "arguments": {...} } - - VS Code/Continue: { "tool": "tool_name", "params": {...} } or { "tool": "tool_name", "input": {...} } + - Compatible clients: { "tool": "tool_name", "params": {...} } or { "tool": "tool_name", "input": {...} } Delegates all tool execution to the modular dispatcher in mcp_handlers.py. """ try: body = await request.json() except Exception: - return web.json_response({ - "jsonrpc": "2.0", - "error": {"code": -32700, "message": "Parse error"}, - "id": None, - }, status=400) + return web.json_response( + { + "jsonrpc": "2.0", + "error": {"code": -32700, "message": "Parse error"}, + "id": None, + }, + status=400, + ) return await dispatch_tool(body) @@ -368,34 +463,41 @@ async def handle_mcp_diagnostics(request: web.Request) -> web.Response: tool_snapshot = [] for name, handler in sorted(TOOL_HANDLERS.items()): - tool_snapshot.append({ - "name": name, - "handler": getattr(handler, "__name__", str(handler)), - "module": getattr(handler, "__module__", "unknown"), - }) + tool_snapshot.append( + { + "name": name, + "handler": getattr(handler, "__name__", str(handler)), + "module": getattr(handler, "__module__", "unknown"), + } + ) # Add dynamic skill tools skill_tools = [] for skill_meta in _list_skills(): - skill_tools.append({ - "name": f"skill_{skill_meta['id']}", - "skill_id": skill_meta["id"], - "category": skill_meta.get("category", ""), - }) + skill_tools.append( + { + "name": f"skill_{skill_meta['id']}", + "skill_id": skill_meta["id"], + "category": skill_meta.get("category", ""), + } + ) # ── 3. Recent tool calls from spool ───────────────────── recent_calls: list[dict[str, Any]] = [] try: from app.services.spool_reader import read_spool + spool_entries = read_spool(max_entries=20, search="mcp") for entry in spool_entries: - recent_calls.append({ - "timestamp": entry.timestamp, - "level": entry.level, - "module": entry.module, - "message": entry.message[:200], - "source": entry.source, - }) + recent_calls.append( + { + "timestamp": entry.timestamp, + "level": entry.level, + "module": entry.module, + "message": entry.message[:200], + "source": entry.source, + } + ) except Exception: # Spool reader may not be available pass @@ -417,7 +519,7 @@ async def handle_mcp_diagnostics(request: web.Request) -> web.Response: if check_name == "syntax": remediation.append( "Fix syntax errors in mcp.py/mcp_handlers.py manually — " - "run: python -c \"from app.api.mcp_guardrails import validate_mcp_integrity; validate_mcp_integrity()\"" + 'run: python -c "from app.api.mcp_guardrails import validate_mcp_integrity; validate_mcp_integrity()"' ) elif check_name == "exports": remediation.append( @@ -437,9 +539,7 @@ async def handle_mcp_diagnostics(request: web.Request) -> web.Response: "Run skill_mcp_self_heal with dry_run=false to auto-fix stale port references" ) elif check_name == "dispatch_tool": - remediation.append( - "Restore dispatch_tool() in mcp_handlers.py — check git history" - ) + remediation.append("Restore dispatch_tool() in mcp_handlers.py — check git history") if not remediation: remediation.append("No issues detected — MCP layer is healthy") @@ -449,24 +549,27 @@ async def handle_mcp_diagnostics(request: web.Request) -> web.Response: if backend_health["hivemind"]["ok"] is False: remediation.append("Hivemind health check failed — start backend/mcp/start_hivemind.sh") - return web.json_response({ - "status": "ok" if integrity_report.get("ok") else "degraded", - "timestamp": _utc_now_iso(), - "integrity": integrity_report, - "backend_health": backend_health, - "tool_registry": { - "registered_tools": len(TOOL_HANDLERS), - "tools": tool_snapshot, - "skill_tools": skill_tools, - "skill_tool_count": len(skill_tools), - }, - "recent_calls": recent_calls, - "remediation": remediation, - }) + return web.json_response( + { + "status": "ok" if integrity_report.get("ok") else "degraded", + "timestamp": _utc_now_iso(), + "integrity": integrity_report, + "backend_health": backend_health, + "tool_registry": { + "registered_tools": len(TOOL_HANDLERS), + "tools": tool_snapshot, + "skill_tools": skill_tools, + "skill_tool_count": len(skill_tools), + }, + "recent_calls": recent_calls, + "remediation": remediation, + } + ) def _utc_now_iso() -> str: from datetime import UTC, datetime + return datetime.now(UTC).isoformat() diff --git a/backend/app/api/mcp_handlers/__init__.py b/backend/app/api/mcp_handlers/__init__.py index 12a493be..4c3d789f 100644 --- a/backend/app/api/mcp_handlers/__init__.py +++ b/backend/app/api/mcp_handlers/__init__.py @@ -7,6 +7,7 @@ Usage: from app.api.mcp_handlers import dispatch_tool, TOOL_HANDLERS """ + from __future__ import annotations import logging @@ -90,7 +91,7 @@ async def dispatch_tool(body: Dict[str, Any]) -> web.Response: Accepts multiple payload shapes: - Standard MCP: { "name": "tool_name", "arguments": {...} } - - VS Code/Continue: { "tool": "tool_name", "params": {...} } + - Compatible clients: { "tool": "tool_name", "params": {...} } - Alternate: { "tool_name": "tool_name", "input": {...} } Delegates to individual handler functions by domain module. @@ -99,7 +100,9 @@ async def dispatch_tool(body: Dict[str, Any]) -> web.Response: 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 []) + 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) @@ -108,8 +111,11 @@ async def dispatch_tool(body: Dict[str, Any]) -> web.Response: 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) + return web.json_response( + { + "jsonrpc": "2.0", + "error": {"code": -32601, "message": f"Tool '{tool_name}' not found"}, + "id": request_id, + }, + status=404, + ) diff --git a/backend/app/api/metadata.py b/backend/app/api/metadata.py index 37bd236f..5547ba1d 100644 --- a/backend/app/api/metadata.py +++ b/backend/app/api/metadata.py @@ -1,4 +1,5 @@ """uCore API — metadata endpoints (system info, etc.)""" + from __future__ import annotations import os @@ -11,33 +12,37 @@ from ..core.settings import settings -POPCORN_PID_FILE = Path.home() / ".ucore" / "ucore-popcorn.pid" +POPCORN_PID_FILE = settings.udos_home / "ucore-popcorn.pid" async def health_handler(request: web.Request) -> web.Response: """Return service health status (for liveness probe).""" - return web.json_response({ - "status": "ok", - "service": "uCore", - "version": settings.version, - "popcorn": _get_popcorn_status(), - }) + return web.json_response( + { + "status": "ok", + "service": "uCore", + "version": settings.version, + "popcorn": _get_popcorn_status(), + } + ) async def system_info_handler(request: web.Request) -> web.Response: """Return system/platform metadata with real-time resource data.""" resources = _get_resource_snapshot() - return web.json_response({ - "platform": plat.system(), - "machine": plat.machine(), - "python": plat.python_version(), - "hostname": plat.node(), - "app": settings.app_name, - "version": settings.version, - "clipboard_shortcut": settings.clipboard_shortcut, - "resources": resources, - "services": _get_service_status(), - }) + return web.json_response( + { + "platform": plat.system(), + "machine": plat.machine(), + "python": plat.python_version(), + "hostname": plat.node(), + "app": settings.app_name, + "version": settings.version, + "clipboard_shortcut": settings.clipboard_shortcut, + "resources": resources, + "services": _get_service_status(), + } + ) def _get_resource_snapshot() -> dict: @@ -93,6 +98,7 @@ def _get_load_avg() -> tuple[float, float, float]: def _count_cores() -> int: import os + return os.cpu_count() or 1 @@ -157,6 +163,7 @@ def _get_uptime() -> float: timeout=5, ) import re + m = re.search(r"sec\s*=\s*(\d+)", result.stdout) if m: return time_mod.time() - int(m.group(1)) @@ -187,9 +194,7 @@ def _get_service_status() -> dict: timeout=3, ) containers = [c for c in result.stdout.splitlines() if c.strip()] - services["docker"] = ( - "running" if result.returncode == 0 else "unavailable" - ) + services["docker"] = "running" if result.returncode == 0 else "unavailable" if containers: services["containers"] = containers[:5] except Exception: diff --git a/backend/app/api/routes.py b/backend/app/api/routes.py index f4ad492c..765f38af 100644 --- a/backend/app/api/routes.py +++ b/backend/app/api/routes.py @@ -1,4 +1,5 @@ """uCore API — route registration (non-core API endpoints)""" + from __future__ import annotations import logging @@ -186,7 +187,9 @@ def register_routes(app: web.Application) -> None: app.router.add_post("/api/budget/reload", handle_budget_reload) app.router.add_get("/api/developer/repos", handle_list_repos) app.router.add_get("/api/developer/repos/{repo_name}/files", handle_list_repo_files) - app.router.add_get("/api/developer/repos/{repo_name}/file-preview", handle_get_repo_file_preview) + app.router.add_get( + "/api/developer/repos/{repo_name}/file-preview", handle_get_repo_file_preview + ) app.router.add_put("/api/developer/repos/{repo_name}/file-preview", handle_update_repo_file) app.router.add_get("/api/developer/repos/{repo_name}/diff", handle_get_repo_file_diff) app.router.add_get("/api/developer/repos/{repo_name}/review", handle_list_repo_review) @@ -272,6 +275,7 @@ def register_routes(app: web.Application) -> None: app.router.add_post("/api/skills/run", handle_run_named_skill) # Health and skill state endpoints from .skills import handle_health, handle_skill_source, handle_skill_state + app.router.add_get("/api/skills/state", handle_skill_state) app.router.add_get("/api/skills/health", handle_health) app.router.add_get("/api/skills/{skill_id}/source", handle_skill_source) @@ -279,6 +283,7 @@ def register_routes(app: web.Application) -> None: # ── Unified Executables (Skills + Snack plugins) ────────────── try: from .executables_api import register_executables_routes + register_executables_routes(app) log.debug("Executables API routes registered") except ImportError as e: @@ -292,6 +297,7 @@ def register_routes(app: web.Application) -> None: # ── Unified Services (Services + Tools + MCP) ───────────────── try: from .services_api import register_services_routes + register_services_routes(app) log.debug("Services API routes registered") except ImportError as e: @@ -304,6 +310,7 @@ def register_routes(app: web.Application) -> None: handle_render, handle_stream, ) + app.router.add_post("/api/render", handle_render) app.router.add_get("/api/render/stream", handle_stream) app.router.add_post("/api/render/event", handle_publish_event) @@ -311,6 +318,7 @@ def register_routes(app: web.Application) -> None: # ── Editor surface (Markdown scrape/summarize/binder) ─────────── from .editor_api import handle_save_to_binder, handle_scrape_web, handle_summarize + app.router.add_post("/api/editor/scrape-web", handle_scrape_web) app.router.add_post("/api/editor/summarize", handle_summarize) app.router.add_post("/api/editor/save-to-binder", handle_save_to_binder) @@ -325,6 +333,7 @@ def register_routes(app: web.Application) -> None: handle_research_stream, handle_vault_scan, ) + app.router.add_post("/api/research/start", handle_research_start) app.router.add_get("/api/research/status", handle_research_status) app.router.add_get("/api/research/list", handle_research_list) @@ -341,13 +350,12 @@ def register_routes(app: web.Application) -> None: handle_binder_score, handle_binder_update, ) + app.router.add_get("/api/binder/list", handle_binder_list) app.router.add_post("/api/binder/add", handle_binder_add) app.router.add_patch("/api/binder/update", handle_binder_update) app.router.add_patch("/api/binder/score", handle_binder_score) - - # ── Autonomy Engine ──────────────────────────────────────────── try: import json @@ -355,7 +363,9 @@ def register_routes(app: web.Application) -> None: from aiohttp import web - STATE_FILE = Path.home() / ".ucore" / "logs" / "autonomy_state.json" + from app.core.settings import settings + + STATE_FILE = settings.logs_dir / "autonomy_state.json" async def handle_autonomy_state(_request: web.Request) -> web.Response: """GET /api/autonomy/state — return last autonomy check state.""" @@ -376,6 +386,7 @@ async def handle_autonomy_state(_request: web.Request) -> web.Response: # ── Spool / Activity Feed ─────────────────────────────────────── try: from .spool import register_spool_routes + register_spool_routes(app) log.debug("Spool activity feed routes registered") except ImportError as e: @@ -384,6 +395,7 @@ async def handle_autonomy_state(_request: web.Request) -> web.Response: # ── Identity (UDN-IDENTITY-API-001) ───────────────────────────── try: from .identity_api import register_identity_routes + register_identity_routes(app) log.debug("Identity routes registered") except ImportError as e: @@ -414,6 +426,7 @@ async def handle_autonomy_state(_request: web.Request) -> web.Response: # ── Dashboard Surface ────────────────────────────────────────── try: from ..surfaces.dashboard import DashboardStore, register_dashboard_routes + if not hasattr(app, "_dashboard_store"): app[DASHBOARD_STORE_KEY] = DashboardStore() register_dashboard_routes(app, app[DASHBOARD_STORE_KEY]) @@ -424,6 +437,7 @@ async def handle_autonomy_state(_request: web.Request) -> web.Response: # ── Documentation Surface ──────────────────────────────────── try: from ..surfaces.documentation_api import register_documentation_routes + register_documentation_routes(app) log.debug("Documentation surface registered") except ImportError as e: @@ -432,6 +446,7 @@ async def handle_autonomy_state(_request: web.Request) -> web.Response: # ── Server Surface ──────────────────────────────────────────── try: from ..surfaces.server import ServerStore, register_server_routes + if not hasattr(app, "_server_store"): app["_server_store"] = ServerStore() register_server_routes(app, app["_server_store"]) @@ -442,6 +457,7 @@ async def handle_autonomy_state(_request: web.Request) -> web.Response: # ── System Surface API ──────────────────────────────────────── try: from ..surfaces.system_api import register_system_api_routes + register_system_api_routes(app) log.debug("System surface API registered") except ImportError as e: @@ -450,6 +466,7 @@ async def handle_autonomy_state(_request: web.Request) -> web.Response: # ── Library Index (unified vault search) ──────────────────────── try: from .library import register_library_routes + register_library_routes(app) log.debug("Library index routes registered") except ImportError as e: @@ -458,6 +475,7 @@ async def handle_autonomy_state(_request: web.Request) -> web.Response: # ── Vault Topology (vault layer config for frontend) ──────────── try: from .vault_api import register_vault_routes + register_vault_routes(app) log.debug("Vault topology routes registered") except ImportError as e: @@ -466,6 +484,7 @@ async def handle_autonomy_state(_request: web.Request) -> web.Response: # ── Catalog Service (skills, MCP servers, LLMs) ────────────────── try: from .catalog import setup_routes as setup_catalog_routes + setup_catalog_routes(app) log.debug("Catalog API routes registered") except ImportError as e: @@ -474,6 +493,7 @@ async def handle_autonomy_state(_request: web.Request) -> web.Response: # ── Hivemind Knowledge Layer ───────────────────────────────────── try: from .hivemind_knowledge import setup_routes as setup_hivemind_knowledge_routes + setup_hivemind_knowledge_routes(app) log.debug("Hivemind knowledge layer routes registered") except ImportError as e: @@ -482,6 +502,7 @@ async def handle_autonomy_state(_request: web.Request) -> web.Response: # ── Dev Layer API (Dev Mode toggle) ───────────────────────────── try: from .dev_layer_api import register_dev_layer_routes + register_dev_layer_routes(app) log.debug("Dev Layer API routes registered") except ImportError as e: @@ -490,6 +511,7 @@ async def handle_autonomy_state(_request: web.Request) -> web.Response: # ── Tasker API (backend data for Kanban) ───────────────────────── try: from .tasker_api import handle_workflow_tasks, register_tasker_routes + register_tasker_routes(app) # Workflow-specific filtered task endpoint app.router.add_get("/api/workflow/tasks", handle_workflow_tasks) @@ -500,6 +522,7 @@ async def handle_autonomy_state(_request: web.Request) -> web.Response: # ── Feed API (unified incoming data layer) ──────────────────────── try: from .feed_api import register_feed_routes + register_feed_routes(app) log.debug("Feed API routes registered") except ImportError as e: @@ -508,6 +531,7 @@ async def handle_autonomy_state(_request: web.Request) -> web.Response: # ── Control Panel API (unified ecosystem status) ────────────────── try: from .control_api import register_control_routes + register_control_routes(app) log.debug("Control Panel API routes registered") except ImportError as e: @@ -516,16 +540,19 @@ async def handle_autonomy_state(_request: web.Request) -> web.Response: # ── Surface Registry API ─────────────────────────────────────────── try: from .surface_registry_api import register_surface_routes + register_surface_routes(app) log.debug("Surface Registry API routes registered") except ImportError as e: log.debug( - "Surface Registry routes not available: %s", e, + "Surface Registry routes not available: %s", + e, ) # ── Template (Slate) API ────────────────────────────────────────── try: from .template_api import register_template_routes + register_template_routes(app) log.debug("Template (Slate) API routes registered") except ImportError as e: @@ -534,6 +561,7 @@ async def handle_autonomy_state(_request: web.Request) -> web.Response: # ── History API (action log, snapshots, rollback) ────────────────── try: from .history_api import register_history_routes + register_history_routes(app) log.debug("History API routes registered") except ImportError as e: diff --git a/backend/app/api/skills.py b/backend/app/api/skills.py index add287ba..5fa503b7 100644 --- a/backend/app/api/skills.py +++ b/backend/app/api/skills.py @@ -18,7 +18,7 @@ # Default skill paths — try Python registry first, then filesystem from app.services.health import get_health_summary -from app.skills.registry import get_skill +from app.skills.registry import get_skill, run_skill_by_id from app.skills.state import read_state SKILL_PATHS = [ @@ -91,7 +91,11 @@ async def _run_skill_by_id( "requires_confirmation": True, }, status=403) - result = await skill.run(**kwargs) + result = await run_skill_by_id( + skill_id, + execution_authorized=confirmed, + **kwargs, + ) # Broadcast completion to SSE subscribers try: from app.api.render_api import publish_event diff --git a/backend/app/api/variables_api.py b/backend/app/api/variables_api.py index dd64e93b..86ef0712 100644 --- a/backend/app/api/variables_api.py +++ b/backend/app/api/variables_api.py @@ -19,13 +19,10 @@ from aiohttp import web +from app.core.settings import settings + # ─── Variable Store Path ───────────────────────────────────────────── -_VARIABLE_STORE_DIR = Path( - os.environ.get( - "UCORE_DATA_DIR", - os.path.expanduser("~/.ucore/data"), - ), -) +_VARIABLE_STORE_DIR = settings.data_dir _VARIABLE_STORE_FILE = _VARIABLE_STORE_DIR / "variables.json" _INSTALL_META_FILE = _VARIABLE_STORE_DIR / "install_meta.json" @@ -42,11 +39,10 @@ def _ensure_store() -> None: datetime.now(UTC).astimezone().tzinfo, ) or "UTC", "uid": str(uuid.uuid4()), - "cline_provider": "ollama", - "cline_model": os.environ.get( + "model_routing": "automatic", + "local_model": os.environ.get( "UCORE_OLLAMA_MODEL", "qwen2.5-coder:3b", ), - "cline_thinking": "low", } _VARIABLE_STORE_FILE.write_text(json.dumps(default_vars, indent=2)) @@ -132,8 +128,7 @@ async def handle_update_user_variables(request: web.Request) -> web.Response: allowed_keys = { "username", "role", "location", "timezone", "uid", "email", - "cline_provider", "cline_model", "cline_thinking", - "cline_auto_approve", + "model_routing", "local_model", } for key, value in body.items(): if key in allowed_keys: diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 442a21d3..8755354e 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,6 +1,4 @@ -"""Unified Configuration for uCore (Python) -Merges Cline settings, Secret Store values, and Environment Variables. -""" +"""Unified configuration for uCore.""" from __future__ import annotations import os @@ -32,7 +30,7 @@ class LoggingConfig: @dataclass -class ClineConfig: +class AgentPolicyConfig: operating_principles: list[str] | None = field(default=None) mcp_usage: list[str] | None = field(default=None) @@ -40,8 +38,8 @@ def __post_init__(self): if self.operating_principles is None: self.operating_principles = [ "Prefer safe, reversible changes and keep diffs small", - "Keep durable workflow state in .tasker/ Markdown files", - "Treat Cline Kanban as orchestration UI, not source of truth", + "Keep durable workflow state in uFlow Markdown files", + "Route providers by task policy instead of user selection", "Keep MCP integrations localhost-only by default", "Preserve Git history clarity with focused, test-backed changes", ] @@ -59,7 +57,7 @@ class AppConfig: mcp: MCPConfig | None = field(default=None) database: DatabaseConfig | None = field(default=None) logging: LoggingConfig | None = field(default=None) - cline: ClineConfig | None = field(default=None) + agent_policy: AgentPolicyConfig | None = field(default=None) def __post_init__(self): if self.github is None: @@ -70,8 +68,8 @@ def __post_init__(self): self.database = DatabaseConfig() if self.logging is None: self.logging = LoggingConfig() - if self.cline is None: - self.cline = ClineConfig() + if self.agent_policy is None: + self.agent_policy = AgentPolicyConfig() # Singleton instance diff --git a/backend/app/core/settings.py b/backend/app/core/settings.py index 0f261051..0dd1d875 100644 --- a/backend/app/core/settings.py +++ b/backend/app/core/settings.py @@ -7,6 +7,32 @@ from pathlib import Path +def _udos_code_root() -> Path: + return Path( + os.environ.get( + "UDOS_ROOT", + os.environ.get( + "ROOT", + os.environ.get("UDOS_CODE", os.path.expanduser("~/Code")), + ), + ), + ).expanduser() + + +def _udos_home(code_root: Path) -> Path: + """Resolve the detachable runtime home.""" + explicit = os.environ.get("UDOS_HOME") + if explicit: + return Path(explicit).expanduser() + + return code_root / ".udos" + + +_UDOS_ROOT = _udos_code_root() +_UDOS_HOME = _udos_home(_UDOS_ROOT) +_SECRETS_HOME = _UDOS_HOME / "secrets" + + @dataclass class Settings: """Central uCore configuration.""" @@ -28,26 +54,36 @@ class Settings: ).lower() in ("1", "true", "yes") # ── Paths ──────────────────────────────────────────────── + udos_home: Path = _UDOS_HOME data_dir: Path = Path( - os.environ.get("UCORE_DATA_DIR", os.path.expanduser("~/.ucore/data")), + os.environ.get("UCORE_DATA_DIR", str(_UDOS_HOME / "data")), ) config_dir: Path = Path( os.environ.get( "UCORE_CONFIG_DIR", - os.path.expanduser("~/.ucore/config"), + str(_UDOS_HOME / "config"), ), ) logs_dir: Path = Path( - os.environ.get("UCORE_LOGS_DIR", os.path.expanduser("~/.ucore/logs")), + os.environ.get("UCORE_LOGS_DIR", str(_UDOS_HOME / "logs")), ) memory_dir: Path = Path( os.environ.get( "UCORE_MEMORY_DIR", - os.path.expanduser("~/.ucore/memory"), + str(_UDOS_HOME / "memory"), ), ) secrets_dir: Path = Path( - os.environ.get("UCORE_SECRETS_DIR", os.path.expanduser("~/.ucore")), + os.environ.get("UCORE_SECRETS_DIR", str(_SECRETS_HOME)), + ) + vault_root: Path = Path( + os.environ.get("UDOS_VAULT_ROOT", os.path.expanduser("~/Vault")), + ) + shared_vault_root: Path = Path( + os.environ.get("UDOS_SHARED_ROOT", os.path.expanduser("~/Shared")), + ) + public_vault_root: Path = Path( + os.environ.get("UDOS_PUBLIC_ROOT", os.path.expanduser("~/Public")), ) # ── Snackbar ───────────────────────────────────────────── @@ -57,7 +93,7 @@ class Settings: # ── Surfaces ───────────────────────────────────────────── surface_registry_path: str = os.environ.get( "UCORE_SURFACE_REGISTRY", - os.path.expanduser("~/.ucore/surfaces.json"), + str(_UDOS_HOME / "surfaces.json"), ) # ── Security ───────────────────────────────────────────── @@ -79,15 +115,7 @@ class Settings: install_name: str = os.environ.get("UDOS_INSTALL_NAME", socket.gethostname()) # ── Spine (Code base path — all repos under ~/Code/) ───── - udos_root: Path = Path( - os.environ.get( - "UDOS_ROOT", - os.environ.get( - "ROOT", - os.environ.get("UDOS_CODE", os.path.expanduser("~/Code")), - ), - ), - ).expanduser() + udos_root: Path = _UDOS_ROOT # ── Ollama / AI ────────────────────────────────────────── ollama_base_url: str = os.environ.get( diff --git a/backend/app/extensions/registry.py b/backend/app/extensions/registry.py index 3800f76a..fd7545cc 100644 --- a/backend/app/extensions/registry.py +++ b/backend/app/extensions/registry.py @@ -23,6 +23,8 @@ from pathlib import Path from typing import Any +from app.core.settings import settings + from .manifest import ExtensionKind, ExtensionManifest log = logging.getLogger("ucore.extensions.registry") @@ -34,7 +36,7 @@ DISCOVERY_PATHS: list[Path] = [ Path(__file__).parent.parent / "extensions" / "manifests", - Path.home() / ".ucore" / "extensions", + settings.udos_home / "extensions", ] """Default search locations for extension manifests.""" @@ -68,7 +70,11 @@ def _add_external_path(ext_id: str) -> None: Uses env var if set, otherwise defaults to ~/Code/{ext_id}. Only adds the path if it exists on disk. """ - default = str(Path.home() / "Code" / ext_id.title() if ext_id == "uflow" else Path.home() / "Code" / ext_id) + default = str( + Path.home() / "Code" / ext_id.title() + if ext_id == "uflow" + else Path.home() / "Code" / ext_id + ) # Normalise: uflow -> ~/Code/uFlow, uknowledge -> ~/Code/uKnowledge repo_name_map = {"uflow": "uFlow", "uknowledge": "uKnowledge"} repo_dir = repo_name_map.get(ext_id, ext_id) @@ -135,7 +141,7 @@ def _register_builtins(self) -> None: "id": "ucore-tools", "name": "Dev Tools", "kind": ExtensionKind.TOOL, - "description": "Docker, Git, GitHub CLI, Ollama, Node, Python, VS Code tool integrations", + "description": "Docker, Git, GitHub CLI, Ollama, Node, and Python tool integrations", "optional": True, "api_prefix": "/api/tools", }, @@ -192,12 +198,14 @@ def discover(self, extra_paths: list[Path] | None = None) -> int: discovered += 1 log.info( "Discovered extension: %s (%s)", - manifest.id, manifest_file, + manifest.id, + manifest_file, ) except Exception as exc: log.warning( "Failed to load manifest %s: %s", - manifest_file, exc, + manifest_file, + exc, ) return discovered @@ -257,12 +265,14 @@ def load_all(self, app: Any | None = None) -> dict[str, bool]: if not manifest.optional: log.error( "Required extension %s failed to load: %s", - ext_id, exc, + ext_id, + exc, ) else: log.warning( "Optional extension %s failed to load: %s", - ext_id, exc, + ext_id, + exc, ) return results @@ -283,7 +293,8 @@ def register_routes(self, app: Any) -> None: self._loaded[ext_id] = True self._errors.pop(ext_id, None) log.debug( - "Routes registered for extension: %s", ext_id, + "Routes registered for extension: %s", + ext_id, ) except ImportError: # Path-discovery fallback for external split-repo packages @@ -304,9 +315,9 @@ def register_routes(self, app: Any) -> None: self._errors[ext_id] = "Route registrar import failed" if manifest.optional: log.debug( - "Extension %s route registrar not available " - "(optional, skipping): %s", - ext_id, manifest.route_registrar, + "Extension %s route registrar not available (optional, skipping): %s", + ext_id, + manifest.route_registrar, ) else: log.exception( diff --git a/backend/app/knowledge/sqlite_utils.py b/backend/app/knowledge/sqlite_utils.py index 00061e42..c011aace 100644 --- a/backend/app/knowledge/sqlite_utils.py +++ b/backend/app/knowledge/sqlite_utils.py @@ -3,6 +3,7 @@ Discovers uDos-managed SQLite databases on disk and provides read/write-guarded query helpers. """ + from __future__ import annotations import json @@ -15,12 +16,16 @@ from pathlib import Path from typing import Any +from app.core.settings import settings + SPOOL_PATH = Path( - os.getenv("UCORE_SNACKS_REPLIES", "~/.local/share/snackmachine/replies.jsonl"), + os.getenv("UCORE_SNACKS_REPLIES", str(settings.udos_home / "replies.jsonl")), +).expanduser() +BACKUP_DIR = Path( + os.getenv("UCORE_DB_BACKUPS", str(settings.udos_home / "backups" / "db")), ).expanduser() -BACKUP_DIR = Path(os.getenv("UCORE_DB_BACKUPS", "~/.ucore/backups/db")).expanduser() -UCORE_DIR = Path.home() / ".ucore" +UCORE_DIR = settings.udos_home def _utc_now() -> str: diff --git a/backend/app/knowledge/vault.py b/backend/app/knowledge/vault.py index 48b77be4..af8493fa 100644 --- a/backend/app/knowledge/vault.py +++ b/backend/app/knowledge/vault.py @@ -1,243 +1,24 @@ -"""Knowledge bridge — filesystem vault search (no external apps). +"""Compatibility imports for the uKnowledge-owned filesystem library. -Reads the uDos vault topology: - - ~/Vault — master user vault (one only) - ~/Shared — shared vaults - ~/Public — public vaults (incl. ~/Public/global-knowledge) - -Search is backed by the unified library index (FTS5) at -``~/.ucore/indices/library.db``, with a direct filesystem fallback -when the index has not been built yet. +Knowledge storage and search live in the separate uKnowledge package. Keeping +these names here avoids breaking existing uCore chat, MCP, and skill callers +while preventing a second implementation from drifting. """ -from __future__ import annotations - -import logging -import re -from pathlib import Path -from typing import Any - -from app.services import library_index -log = logging.getLogger("ucore.knowledge.vault") - -VAULT_LAYERS: tuple[dict[str, Any], ...] = ( - {"id": "user", "name": "Vault", "path": Path.home() / "Vault"}, - {"id": "shared", "name": "Shared", "path": Path.home() / "Shared"}, - {"id": "public", "name": "Public", "path": Path.home() / "Public"}, +from uknowledge.library import ( + get_document, + get_document_content, + list_documents, + list_workspaces, + search, ) -_FTS_SPECIAL = re.compile(r'["()*:^\-~]') - - -def _safe_fts(query: str) -> str: - """Sanitize a query for SQLite FTS5 MATCH syntax.""" - cleaned = _FTS_SPECIAL.sub(" ", query) - tokens = [t for t in cleaned.split() if t] - return " ".join(tokens) or "*" - - -def _layer_path(workspace_id: str | None) -> Path | None: - if not workspace_id: - return None - for layer in VAULT_LAYERS: - if layer["id"] == workspace_id: - return layer["path"] - return library_index.workspace_root(workspace_id) - - -def list_workspaces() -> list[dict[str, Any]]: - """List the vault layers plus any user-registered workspaces.""" - workspaces: list[dict[str, Any]] = [] - for layer in VAULT_LAYERS: - exists = layer["path"].exists() and layer["path"].is_dir() - workspaces.append({ - "id": layer["id"], - "name": layer["name"], - "icon": None, - "member_count": 0, - "source": layer["id"], - "path": str(layer["path"]), - "exists": exists, - }) - for ws in library_index.list_workspaces(): - workspaces.append({ - "id": ws.get("source") or ws.get("name"), - "name": ws.get("name", "Workspace"), - "icon": None, - "member_count": 0, - "source": ws.get("source") or ws.get("name"), - "path": ws.get("path"), - "exists": bool(ws.get("exists")), - }) - return workspaces - - -def list_documents( - workspace_id: str | None = None, - limit: int = 100, -) -> list[dict[str, Any]]: - """List markdown documents in a vault layer (or across all layers).""" - rows = _index_search("", workspace_id, max(1, limit)) - if not rows: - rows = _fallback_list(workspace_id, max(1, limit)) - docs: list[dict[str, Any]] = [] - for r in rows: - docs.append({ - "id": r.get("id"), - "title": r.get("filename") or Path(r.get("path", "")).name, - "type": "markdown", - "updated_at": r.get("modified_at"), - "workspace_id": r.get("source"), - "source": r.get("source"), - "rel_path": r.get("path"), - "path": r.get("path"), - }) - return docs - - -def semantic_search( - query: str, - workspace_id: str | None = None, - limit: int = 10, -) -> list[dict[str, Any]]: - """Search the vault library index (FTS5), with a filesystem fallback.""" - if not query or not query.strip(): - return [] - rows = _index_search(query, workspace_id, max(1, limit)) - if not rows: - rows = _fallback_search(query, workspace_id, max(1, limit)) - results: list[dict[str, Any]] = [] - for r in rows: - results.append({ - "id": r.get("id"), - "title": r.get("filename") or Path(r.get("path", "")).name, - "path": r.get("path"), - "content": r.get("preview") or "", - "source": r.get("source"), - "score": r.get("score"), - }) - return results[:limit] - - -def _index_search( - query: str, - source: str | None, - limit: int, -) -> list[dict[str, Any]]: - try: - fts = _safe_fts(query) if query.strip() else "" - return library_index.search(fts, source=source, limit=limit) - except Exception as exc: - log.warning("Library index search failed: %s", exc) - return [] - - -def _fallback_list( - workspace_id: str | None, - limit: int, -) -> list[dict[str, Any]]: - roots = ( - [_layer_path(workspace_id)] - if workspace_id - else [layer["path"] for layer in VAULT_LAYERS] - ) - out: list[dict[str, Any]] = [] - for root in roots: - if not root or not root.exists(): - continue - for md in root.rglob("*.md"): - if len(out) >= limit: - break - rel = str(md.relative_to(root)) - out.append({ - "id": f"{root.name}:{rel}", - "filename": md.name, - "path": str(md), - "source": root.name, - "modified_at": None, - "preview": "", - }) - return out - - -def _fallback_search( - query: str, - workspace_id: str | None, - limit: int, -) -> list[dict[str, Any]]: - q = query.strip().lower() - roots = ( - [_layer_path(workspace_id)] - if workspace_id - else [layer["path"] for layer in VAULT_LAYERS] - ) - out: list[dict[str, Any]] = [] - for root in roots: - if not root or not root.exists(): - continue - for md in root.rglob("*.md"): - if len(out) >= limit: - break - try: - text = md.read_text(encoding="utf-8", errors="ignore") - except Exception: - continue - if q not in text.lower() and q not in md.name.lower(): - continue - rel = str(md.relative_to(root)) - out.append({ - "id": f"{root.name}:{rel}", - "filename": md.name, - "path": str(md), - "source": root.name, - "modified_at": None, - "preview": text[:500], - }) - return out - - -def get_document( - object_id: str, - workspace_id: str | None = None, -) -> dict[str, Any] | None: - path = _resolve_path(object_id, workspace_id) - if not path or not path.is_file(): - return None - stat = path.stat() - return { - "id": object_id, - "title": path.name, - "type": "markdown", - "description": None, - "updated_at": stat.st_mtime, - "path": str(path), - "data_size": stat.st_size, - } - - -def get_document_content( - object_id: str, - workspace_id: str | None = None, -) -> str | None: - path = _resolve_path(object_id, workspace_id) - if not path or not path.is_file(): - return None - try: - return path.read_text(encoding="utf-8", errors="replace")[:10000] - except Exception: - return None - +semantic_search = search -def _resolve_path(object_id: str, workspace_id: str | None) -> Path | None: - if not object_id: - return None - direct = Path(object_id).expanduser() - if direct.is_file(): - return direct - root = _layer_path(workspace_id) - if root: - candidate = root / object_id - if candidate.is_file(): - return candidate - return None +__all__ = [ + "get_document", + "get_document_content", + "list_documents", + "list_workspaces", + "semantic_search", +] diff --git a/backend/app/mcp/feed/feed_server.py b/backend/app/mcp/feed/feed_server.py index 340d9d1a..abedcdda 100644 --- a/backend/app/mcp/feed/feed_server.py +++ b/backend/app/mcp/feed/feed_server.py @@ -6,7 +6,7 @@ - feed_suggest_binders: AI-driven binder suggestions from activity clusters - feed_link_task: link a .tasker task to a feed activity -The Activity Pod is stored at ~/.ucore/pods/activity.db and initialized +The Activity Pod is stored at ``$UDOS_HOME/pods/activity.db`` and initialized from backend/schemas/activity.schema.sql on first access. """ from __future__ import annotations @@ -17,9 +17,11 @@ from pathlib import Path from typing import Any +from app.core.settings import settings + log = logging.getLogger("ucore.mcp.feed_server") -DEFAULT_POD_PATH = Path.home() / ".ucore" / "pods" / "activity.db" +DEFAULT_POD_PATH = settings.udos_home / "pods" / "activity.db" HERE = Path(__file__).resolve().parent SCHEMA_PATH = HERE.parent.parent.parent / "schemas" / "activity.schema.sql" diff --git a/backend/app/mcp/mcp_bridge/.gitignore b/backend/app/mcp/mcp_bridge/.gitignore new file mode 100644 index 00000000..567609b1 --- /dev/null +++ b/backend/app/mcp/mcp_bridge/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/backend/app/mcp/mcp_bridge/index.ts b/backend/app/mcp/mcp_bridge/index.ts index 77df2f04..13d473d1 100644 --- a/backend/app/mcp/mcp_bridge/index.ts +++ b/backend/app/mcp/mcp_bridge/index.ts @@ -6,6 +6,12 @@ import { ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; +declare const process: { + env: Record; + on(event: string, handler: () => void | Promise): void; + exit(code?: number): never; +}; + const UCORE_BASE = process.env.UCORE_URL || "http://localhost:8484"; async function apiGet(path: string): Promise { diff --git a/backend/app/mcp/mcp_bridge/tsconfig.json b/backend/app/mcp/mcp_bridge/tsconfig.json index 59ee6342..cf3de683 100644 --- a/backend/app/mcp/mcp_bridge/tsconfig.json +++ b/backend/app/mcp/mcp_bridge/tsconfig.json @@ -4,13 +4,13 @@ "module": "Node16", "moduleResolution": "Node16", "outDir": "./build", - "rootDir": "./src", + "rootDir": ".", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "declaration": true }, - "include": ["src/**/*"], + "include": ["index.ts"], "exclude": ["node_modules", "build"] -} \ No newline at end of file +} diff --git a/backend/app/mcp/tasker_ingest.py b/backend/app/mcp/tasker_ingest.py deleted file mode 100644 index 8d67053c..00000000 --- a/backend/app/mcp/tasker_ingest.py +++ /dev/null @@ -1,480 +0,0 @@ -"""tasker_ingest — MCP bridge: Cline task_progress to dev flow state. - -Ingests ephemeral task_progress checklists from Cline or other AI agent -sessions and syncs them into .tasker.dev-flow.yaml, spool, devlog.mcp.yaml, -and private wisdom. Also triggers optional git commit. - -Usage: - POST /api/skills/tasker_ingest/run - Body: { - "action": "ingest", - "workspace": "uCore", - "session_id": "cline-2026-07-01-001", - "checklist": "# Progress\\n- [x] Step 1 done\\n- [ ] Step 2 pending", - "outcome": "completed", - "summary": "Implemented tasker_ingest bridge", - "lessons": ["Use MCP tools for persistence", "Always update wisdom"], - "auto_commit": false - } -""" -from __future__ import annotations - -import logging -import subprocess -from datetime import UTC, datetime -from pathlib import Path -from typing import Any - -import yaml - -from app.core.settings import settings -from app.services.spool_writer import write_spool -from app.services.wisdom_paths import writable_wisdom_path -from app.skills.base import BaseSkill, SkillMeta, SkillParam - -log = logging.getLogger("ucore.mcp.tasker_ingest") - -PROJECT_ROOT = settings.udos_root / "uCore" -DEFAULT_TASKER_FILE = PROJECT_ROOT / ".tasker.dev-flow.yaml" -DEFAULT_DEVLOG_FILE = PROJECT_ROOT / "devlog.mcp.yaml" -DEFAULT_WISDOM_FILE = writable_wisdom_path() - - -class TaskerIngest(BaseSkill): - """Ingest Cline task_progress into persistent dev-flow tracking.""" - - meta = SkillMeta( - id="tasker_ingest", - name="Tasker Ingest", - description=( - "Bridge: ingest Cline/session task_progress checklists into " - ".tasker.dev-flow.yaml, spool, devlog.mcp.yaml, and private wisdom" - ), - category="workflow", - timeout=120, - params=[ - SkillParam( - name="action", - type="string", - required=True, - description="Action: ingest, status, archive, commit", - ), - SkillParam( - name="workspace", - type="string", - required=False, - default="uCore", - description="Workspace/project name", - ), - SkillParam( - name="session_id", - type="string", - required=False, - default="", - description="Unique session identifier (e.g. cline-YYYY-MM-DD-NNN)", - ), - SkillParam( - name="checklist", - type="string", - required=False, - default="", - description="Markdown task_progress checklist from session", - ), - SkillParam( - name="outcome", - type="string", - required=False, - default="completed", - description="Session outcome: completed, partial, failed", - ), - SkillParam( - name="summary", - type="string", - required=False, - default="", - description="Human-readable summary of what was done", - ), - SkillParam( - name="lessons", - type="list", - required=False, - default=[], - description=( - "Durable lessons learned to append to private wisdom" - ), - ), - SkillParam( - name="auto_commit", - type="boolean", - required=False, - default=False, - description="Auto git commit after ingest", - ), - SkillParam( - name="tasker_file", - type="string", - required=False, - default=str(DEFAULT_TASKER_FILE), - description="Path to .tasker.dev-flow.yaml", - ), - SkillParam( - name="dry_run", - type="boolean", - required=False, - default=False, - description="Preview without writing changes", - ), - ], - requires_confirmation=False, - ) - - async def run(self, **kwargs) -> dict: - action = str(kwargs.get("action", "ingest")).strip().lower() - workspace = str(kwargs.get("workspace", "uCore")) - session_id = str(kwargs.get("session_id", "")) - checklist = str(kwargs.get("checklist", "")) - outcome = str(kwargs.get("outcome", "completed")) - summary = str(kwargs.get("summary", "")) - lessons = kwargs.get("lessons", []) - auto_commit = bool(kwargs.get("auto_commit", False)) - tasker_file = Path(kwargs.get("tasker_file", DEFAULT_TASKER_FILE)).expanduser() - dry_run = bool(kwargs.get("dry_run", False)) - - if action == "ingest": - return await self._ingest( - workspace=workspace, - session_id=session_id, - checklist=checklist, - outcome=outcome, - summary=summary, - lessons=lessons, - auto_commit=auto_commit, - tasker_file=tasker_file, - dry_run=dry_run, - ) - elif action == "status": - return self._status(tasker_file) - elif action == "commit": - return self._git_commit(tasker_file, summary, dry_run) - elif action == "archive": - return self._archive_completed(tasker_file, dry_run) - else: - return {"success": False, "error": f"Unknown action: {action}"} - - async def _ingest( - self, - workspace: str, - session_id: str, - checklist: str, - outcome: str, - summary: str, - lessons: list[str], - auto_commit: bool, - tasker_file: Path, - dry_run: bool, - ) -> dict: - """Ingest a task_progress checklist into persistent storage.""" - now = datetime.now(UTC) - timestamp = now.isoformat() - results: dict[str, Any] = { - "spool_written": False, - "devlog_appended": False, - "tasker_updated": False, - "wisdom_appended": False, - "git_committed": False, - } - - # 1. Parse checklist into structured items - parsed_items = self._parse_checklist(checklist) - completed = [i for i in parsed_items if i.get("done")] - pending = [i for i in parsed_items if not i.get("done")] - total = len(parsed_items) - - # 2. Write to spool - if not dry_run: - write_spool( - level="INFO", - module="tasker_ingest", - message=( - f"Ingested session={session_id or 'unknown'} " - f"workspace={workspace} outcome={outcome} " - f"items={total} completed={len(completed)} pending={len(pending)}" - ), - tags=["tasker-ingest", workspace, outcome], - ) - results["spool_written"] = True - - # 3. Append to devlog.mcp.yaml - devlog_path = PROJECT_ROOT / "devlog.mcp.yaml" - if not dry_run: - devlog_entry = self._render_devlog_entry( - timestamp=timestamp, - session_id=session_id, - workspace=workspace, - outcome=outcome, - summary=summary, - completed=completed, - pending=pending, - ) - existing = "" - if devlog_path.exists(): - existing = devlog_path.read_text(encoding="utf-8") - devlog_path.write_text(existing + "\n" + devlog_entry, encoding="utf-8") - results["devlog_appended"] = True - - # 4. Update .tasker.dev-flow.yaml - if not dry_run and tasker_file.exists(): - self._update_tasker_yaml( - tasker_file=tasker_file, - session_id=session_id or f"session-{now.strftime('%Y%m%d%H%M%S')}", - summary=summary or f"Session {session_id}", - outcome=outcome, - items=parsed_items, - workspace=workspace, - timestamp=timestamp, - ) - results["tasker_updated"] = True - - # 5. Append lessons to private wisdom - if not dry_run and lessons: - wisdom_path = DEFAULT_WISDOM_FILE - existing = "" - if wisdom_path.exists(): - existing = wisdom_path.read_text(encoding="utf-8") - wisdom_appends = [] - for lesson in lessons: - if lesson and lesson not in existing: - wisdom_appends.append(f"- {lesson}") - if wisdom_appends: - lesson_block = ( - f"\n## Session: {session_id or timestamp}\n" - + "\n".join(wisdom_appends) - + "\n" - ) - wisdom_path.write_text(existing + lesson_block, encoding="utf-8") - results["wisdom_appended"] = len(wisdom_appends) - - # 6. Auto git commit - if not dry_run and auto_commit: - commit_result = self._git_commit(tasker_file, summary, dry_run=False) - results["git_committed"] = commit_result.get("success", False) - results["git_result"] = commit_result - - return { - "success": True, - "action": "ingest", - "workspace": workspace, - "outcome": outcome, - "items_total": total, - "items_completed": len(completed), - "items_pending": len(pending), - "results": results, - "dry_run": dry_run, - } - - def _parse_checklist(self, checklist: str) -> list[dict[str, Any]]: - """Parse markdown checklist into structured items.""" - items = [] - for line in checklist.split("\n"): - line = line.strip() - # Match: - [x] Description or - [ ] Description - if line.startswith("- [x]") or line.startswith("- [X]") or line.startswith("- [*]"): - items.append({ - "text": line[5:].strip(), - "done": True, - }) - elif line.startswith("- [ ]"): - items.append({ - "text": line[5:].strip(), - "done": False, - }) - elif line.startswith("* [x]") or line.startswith("* [X]"): - items.append({ - "text": line[5:].strip(), - "done": True, - }) - elif line.startswith("* [ ]"): - items.append({ - "text": line[5:].strip(), - "done": False, - }) - return items - - def _render_devlog_entry( - self, - timestamp: str, - session_id: str, - workspace: str, - outcome: str, - summary: str, - completed: list[dict], - pending: list[dict], - ) -> str: - """Render a devlog entry in MCP format.""" - lines = [ - f"\n## Session: {session_id or 'unknown'}", - f"- timestamp: {timestamp}", - f"- workspace: {workspace}", - f"- outcome: {outcome}", - f"- completed: {len(completed)}", - f"- pending: {len(pending)}", - ] - if summary: - lines.append(f"- summary: {summary}") - if completed: - lines.append("- completed_items:") - for item in completed: - lines.append(f" - {item['text']}") - if pending: - lines.append("- pending_items:") - for item in pending: - lines.append(f" - {item['text']}") - return "\n".join(lines) - - def _update_tasker_yaml( - self, - tasker_file: Path, - session_id: str, - summary: str, - outcome: str, - items: list[dict], - workspace: str, - timestamp: str, - ) -> None: - """Append session tasks to .tasker.dev-flow.yaml.""" - try: - content = tasker_file.read_text(encoding="utf-8") - except Exception: - content = "" - - # Build new task entries - new_tasks = [] - for i, item in enumerate(items): - task_uid = f"task.ingest.{session_id}.{i:03d}" if session_id else f"task.auto.{i:03d}" - status = "done" if item["done"] else "todo" - new_tasks.append({ - "uid": task_uid, - "title": item["text"], - "description": f"From session {session_id}: {item['text']}", - "status": status, - "priority": "medium", - "lane": "maintenance", - "tags": ["tasker-ingest", workspace, outcome], - "source": { - "file": "tasker_ingest.py", - "type": "session-ingest", - }, - "created": timestamp, - "updated": timestamp, - }) - - # Parse existing YAML, merge tasks - try: - data = yaml.safe_load(content) or {} - except yaml.YAMLError: - data = {} - - existing_tasks = data.get("tasks", []) - # Avoid duplicating by uid - existing_uids = {t.get("uid") for t in existing_tasks if t.get("uid")} - merged_tasks = existing_tasks + [t for t in new_tasks if t["uid"] not in existing_uids] - - data["tasks"] = merged_tasks - data["task_count"] = len(merged_tasks) - data["completed_count"] = sum(1 for t in merged_tasks if t.get("status") == "done") - data["updated"] = timestamp - - # Write back - tasker_file.write_text(yaml.dump(data, default_flow_style=False, sort_keys=False), encoding="utf-8") - - def _status(self, tasker_file: Path) -> dict: - """Return current status of the tasker file.""" - if not tasker_file.exists(): - return {"success": False, "error": "tasker file not found"} - - try: - content = tasker_file.read_text(encoding="utf-8") - data = yaml.safe_load(content) or {} - tasks = data.get("tasks", []) - total = len(tasks) - done = sum(1 for t in tasks if t.get("status") == "done") - archived = sum(1 for t in tasks if t.get("status") == "archived") - todo = total - done - archived - - return { - "success": True, - "action": "status", - "total_tasks": total, - "completed": done, - "archived": archived, - "pending": todo, - "updated": data.get("updated", "unknown"), - } - except Exception as e: - return {"success": False, "error": str(e)} - - def _git_commit(self, tasker_file: Path, message: str, dry_run: bool) -> dict: - """Auto git commit the changes.""" - repo_dir = PROJECT_ROOT - if not (repo_dir / ".git").exists(): - return {"success": False, "error": "not a git repository"} - - commit_msg = message or "chore: auto-sync tasker ingest" - try: - if not dry_run: - subprocess.run( - ["git", "-C", str(repo_dir), "add", "-A"], - capture_output=True, text=True, check=False, - ) - result = subprocess.run( - ["git", "-C", str(repo_dir), "commit", "-m", commit_msg], - capture_output=True, text=True, check=False, - ) - return { - "success": result.returncode == 0, - "stdout": result.stdout, - "stderr": result.stderr, - "message": commit_msg, - } - return { - "success": True, - "dry_run": True, - "message": commit_msg, - } - except Exception as e: - return {"success": False, "error": str(e)} - - def _archive_completed(self, tasker_file: Path, dry_run: bool) -> dict: - """Move done tasks to archive section.""" - if not tasker_file.exists(): - return {"success": False, "error": "tasker file not found"} - - try: - content = tasker_file.read_text(encoding="utf-8") - data = yaml.safe_load(content) or {} - tasks = data.get("tasks", []) - archive = data.get("archive", []) - - done_tasks = [t for t in tasks if t.get("status") == "done" and t.get("uid", "").startswith("task.ingest.")] - kept_tasks = [t for t in tasks if t not in done_tasks] - - for t in done_tasks: - t["archived_reason"] = "auto-archive" - t["archived_date"] = datetime.now(UTC).isoformat() - archive.append(t) - - if not dry_run: - data["tasks"] = kept_tasks - data["archive"] = archive - data["task_count"] = len(kept_tasks) - tasker_file.write_text(yaml.dump(data, default_flow_style=False, sort_keys=False), encoding="utf-8") - - return { - "success": True, - "action": "archive", - "archived": len(done_tasks), - "remaining": len(kept_tasks), - "dry_run": dry_run, - } - except Exception as e: - return {"success": False, "error": str(e)} diff --git a/backend/app/menu/launchd_manager.py b/backend/app/menu/launchd_manager.py index 3610bc14..a5d8e687 100644 --- a/backend/app/menu/launchd_manager.py +++ b/backend/app/menu/launchd_manager.py @@ -16,8 +16,10 @@ UCORE_LABEL = "com.udos.ucore-menu" UCORE_PLIST = os.path.expanduser(f"~/Library/LaunchAgents/{UCORE_LABEL}.plist") -UCORE_LOCKFILE = os.path.expanduser("~/.ucore/ucore-menu.pid") UCORE_BACKEND_DIR = os.environ.get("UCORE_BACKEND_DIR", str(Path.home() / "Code" / "uCore" / "backend")) +UDOS_HOME = Path(os.environ.get("UDOS_HOME", Path.home() / "Code" / ".udos")).expanduser() +UCORE_LOCKFILE = str(UDOS_HOME / "ucore-menu.pid") +UCORE_LOG_DIR = UDOS_HOME / "logs" # The canonical module to run - MUST be kept in sync across all callers CANONICAL_MODULE = "app.menu.unified_menu_simple" @@ -48,21 +50,26 @@ def get_plist_content() -> str: RunAtLoad KeepAlive - + + SuccessfulExit + + LSUIElement LimitLoadToSessionType Aqua StandardOutPath - {os.path.expanduser('~/.ucore/logs/ucore-menu-stdout.log')} + {UCORE_LOG_DIR / 'ucore-menu-stdout.log'} StandardErrorPath - {os.path.expanduser('~/.ucore/logs/ucore-menu-stderr.log')} + {UCORE_LOG_DIR / 'ucore-menu-stderr.log'} TimeOut 60 EnvironmentVariables UCORE_DEBUG 1 + UDOS_HOME + {UDOS_HOME} PATH /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin @@ -80,7 +87,7 @@ def install() -> bool: log.info("Installing uCore Menu launchd plist...") # Ensure directories exist - os.makedirs(os.path.expanduser("~/.ucore/logs"), exist_ok=True) + UCORE_LOG_DIR.mkdir(parents=True, exist_ok=True) Path(UCORE_PLIST).parent.mkdir(parents=True, exist_ok=True) # Write plist @@ -218,15 +225,17 @@ def get_frontend_plist_content() -> str: StandardOutPath - {os.path.expanduser('~/.ucore/logs/ucore-frontend-stdout.log')} + {UCORE_LOG_DIR / 'ucore-frontend-stdout.log'} StandardErrorPath - {os.path.expanduser('~/.ucore/logs/ucore-frontend-stderr.log')} + {UCORE_LOG_DIR / 'ucore-frontend-stderr.log'} TimeOut 60 EnvironmentVariables UCORE_DEBUG 1 + UDOS_HOME + {UDOS_HOME} PATH /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin @@ -241,7 +250,7 @@ def install_frontend() -> bool: uid = os.getuid() log.info("Installing uCore Frontend launchd plist...") - os.makedirs(os.path.expanduser("~/.ucore/logs"), exist_ok=True) + UCORE_LOG_DIR.mkdir(parents=True, exist_ok=True) Path(UCORE_FRONTEND_PLIST).parent.mkdir(parents=True, exist_ok=True) Path(UCORE_FRONTEND_PLIST).write_text(get_frontend_plist_content(), encoding="utf-8") diff --git a/backend/app/menu/lockfile.py b/backend/app/menu/lockfile.py index 26558a10..4e344e8a 100644 --- a/backend/app/menu/lockfile.py +++ b/backend/app/menu/lockfile.py @@ -14,7 +14,8 @@ log = logging.getLogger("ucore.menu.lockfile") -LOCKFILE_PATH = Path.home() / ".ucore" / "menu.lock" +UDOS_HOME = Path(os.environ.get("UDOS_HOME", Path.home() / "Code" / ".udos")).expanduser() +LOCKFILE_PATH = UDOS_HOME / "menu.lock" def _get_boot_time() -> float: @@ -161,4 +162,3 @@ def release_lock() -> None: pass # Corrupt lock file — leave it, next start will clean it except Exception as exc: log.error("Failed to release lock: %s", exc) - diff --git a/backend/app/menu/snack_shack.py b/backend/app/menu/snack_shack.py index b07a17db..cbcbdb62 100644 --- a/backend/app/menu/snack_shack.py +++ b/backend/app/menu/snack_shack.py @@ -8,26 +8,37 @@ - Dogfooding protection via version tracking - Lane tagging (System Operations, Dev Mode, User Mission Operations) """ + from __future__ import annotations import json import logging +import os import shutil import subprocess import time from pathlib import Path from typing import Any, Optional +from app.core.settings import settings from app.services.spool_reader import read_spool from snackmachine.registry import SnackPlugin, SnackSpec, register_snack log = logging.getLogger("snack-shack") UCORE_URL = "http://127.0.0.1:8484" -import os -SNACKS_REPO_PATH = Path(os.environ.get("UCORE_BACKEND_DIR", str(Path.home() / "Code" / "uCore" / "backend"))) / "app" / "snacks" -SNACKS_USER_PATH = Path.home() / ".ucore/snacks" +SNACKS_REPO_PATH = ( + Path( + os.environ.get( + "UCORE_BACKEND_DIR", + str(settings.udos_root / "uCore" / "backend"), + ), + ) + / "app" + / "snacks" +) +SNACKS_USER_PATH = settings.udos_home / "snacks" SNACKS_TEMPLATE_PATH = SNACKS_REPO_PATH / "templates" @@ -125,6 +136,7 @@ def _post_notification(self, title: str, message: str) -> bool: if self._menu_delegate: try: from app.menu.unified_menu_simple import post_notification + post_notification(title, message) return True except Exception as e: diff --git a/backend/app/menu/snacks/ollama_snack.py b/backend/app/menu/snacks/ollama_snack.py index 9610a437..15b3dea1 100644 --- a/backend/app/menu/snacks/ollama_snack.py +++ b/backend/app/menu/snacks/ollama_snack.py @@ -1,12 +1,13 @@ """Ollama Snack — Ollama LLM server management for uCore menu.""" + from __future__ import annotations import logging import subprocess import time -from pathlib import Path from typing import Any, Optional +from app.core.settings import settings from snackmachine.registry import SnackPlugin, SnackSpec, register_snack log = logging.getLogger("ollama-snack") @@ -19,7 +20,7 @@ def __init__(self, menu_delegate=None): self._menu_delegate = menu_delegate self._status = "checking" self._models = [] - self._disable_file = Path("~/.ucore/ollama_disabled").expanduser() + self._disable_file = settings.udos_home / "ollama_disabled" @property def spec(self) -> SnackSpec: diff --git a/backend/app/menu/snacks/system_snack.py b/backend/app/menu/snacks/system_snack.py index de78c3c1..6fbd8e12 100644 --- a/backend/app/menu/snacks/system_snack.py +++ b/backend/app/menu/snacks/system_snack.py @@ -1,4 +1,5 @@ """System Snack — Backend/frontend/service management for uCore menu.""" + from __future__ import annotations import json @@ -10,6 +11,7 @@ from pathlib import Path from typing import Any, Optional +from app.core.settings import settings from app.skills.shared_utils import update_menu_delegate from snackmachine.registry import SnackPlugin, SnackSpec, register_snack @@ -19,7 +21,10 @@ UI_HUB_URL = "http://localhost:5175" UCORE_LABEL = "com.udos.ucore-menu" UCORE_PLIST = os.path.expanduser("~/Library/LaunchAgents/com.udos.ucore-menu.plist") -UCORE_BACKEND_DIR = os.environ.get("UCORE_BACKEND_DIR", str(Path.home() / "Code" / "uCore" / "backend")) +UCORE_BACKEND_DIR = os.environ.get( + "UCORE_BACKEND_DIR", + str(settings.udos_root / "uCore" / "backend"), +) class SystemSnack(SnackPlugin): @@ -48,7 +53,7 @@ def spec(self) -> SnackSpec: "restart-backend", "restart-frontend", "toggle-start-at-login", - "open-s190-diagnostics" + "open-s190-diagnostics", ], metadata={ "backend": self._backend_connected, @@ -112,6 +117,7 @@ def _heal_ui_hub(self) -> bool: while time.time() < deadline: if self._is_uihub_alive(): from AppKit import NSURL, NSWorkspace + NSWorkspace.sharedWorkspace().openURL_(NSURL.URLWithString_(UI_HUB_URL)) return True time.sleep(0.3) @@ -156,7 +162,9 @@ def _restart_frontend(self) -> bool: # Start frontend subprocess.Popen( ["pnpm", "run", "dev"], - cwd=os.environ.get("UCORE_FRONTEND_DIR", str(Path.home() / "Code" / "uCore" / "frontend")), + cwd=os.environ.get( + "UCORE_FRONTEND_DIR", str(Path.home() / "Code" / "uCore" / "frontend") + ), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True, @@ -169,6 +177,7 @@ def _restart_frontend(self) -> bool: def _enable_start_at_login(self) -> bool: """Install and load the uCore launchd plist (delegates to launchd_manager).""" from app.menu.launchd_manager import install as launchd_install + self._start_at_login = launchd_install() return self._start_at_login @@ -182,12 +191,13 @@ def _toggle_start_at_login(self) -> bool: def _disable_start_at_login(self) -> bool: """Unload and remove the launchd plist (delegates to launchd_manager).""" from app.menu.launchd_manager import uninstall as launchd_uninstall + self._start_at_login = not launchd_uninstall() return not self._start_at_login def _open_s190_diagnostics(self, reason: str = "manual") -> bool: """Open local S190 diagnostics fallback page.""" - fallback_path = Path.home() / ".ucore" / "s190-uihub-fallback.html" + fallback_path = settings.udos_home / "s190-uihub-fallback.html" fallback_path.parent.mkdir(parents=True, exist_ok=True) safe_reason = reason.replace("<", "<").replace(">", ">") @@ -230,6 +240,7 @@ def _open_s190_diagnostics(self, reason: str = "manual") -> bool: fallback_path.write_text(html) from AppKit import NSURL, NSWorkspace + NSWorkspace.sharedWorkspace().openURL_(NSURL.URLWithString_(f"file://{fallback_path}")) return True diff --git a/backend/app/menu/unified_menu_simple.py b/backend/app/menu/unified_menu_simple.py index 9dd35863..0bd19780 100644 --- a/backend/app/menu/unified_menu_simple.py +++ b/backend/app/menu/unified_menu_simple.py @@ -68,7 +68,8 @@ UCORE_LABEL = "com.udos.ucore-menu" UCORE_PLIST = os.path.expanduser(f"~/Library/LaunchAgents/{UCORE_LABEL}.plist") -UCORE_LOCKFILE = os.path.expanduser("~/.ucore/ucore-menu.pid") +UDOS_HOME = Path(os.environ.get("UDOS_HOME", Path.home() / "Code" / ".udos")).expanduser() +UCORE_LOCKFILE = str(UDOS_HOME / "ucore-menu.pid") UCORE_BACKEND_DIR = os.environ.get("UCORE_BACKEND_DIR", str(Path.home() / "Code" / "uCore" / "backend")) SNACKMACHINE_REPO_DIR = Path( os.environ.get( @@ -93,13 +94,13 @@ "roundtable": "http://localhost:5175/assistui", } -log_dir = os.path.expanduser("~/.ucore/logs") -os.makedirs(log_dir, exist_ok=True) +log_dir = UDOS_HOME / "logs" +log_dir.mkdir(parents=True, exist_ok=True) logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] ucore-menu: %(message)s", handlers=[ - logging.FileHandler(os.path.join(log_dir, "ucore-menu.log")), + logging.FileHandler(log_dir / "ucore-menu.log"), logging.StreamHandler(), ], ) @@ -196,38 +197,6 @@ def _installed_extensions() -> list[dict[str, str]]: return extensions -def _launch_dev_server_direct() -> bool: - """Directly launch the uDev developer-surface server on port 5176. - - Used as a fallback when the backend is not running, so the Developer - card can still be brought up in UI Hub. - """ - import subprocess - from pathlib import Path - - udev_dir = Path( - os.environ.get( - "UDEV_DIR", - str(Path.home() / "Code" / "uDev"), - ) - ) - if not (udev_dir / "package.json").exists(): - log.warning("uDev repository not found at %s", udev_dir) - return False - try: - subprocess.Popen( - ["npm", "run", "dev:surface"], - cwd=str(udev_dir), - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - start_new_session=True, - ) - return True - except Exception as exc: - log.error("Failed to launch uDev developer server directly: %s", exc) - return False - - # ─── App Delegate ───────────────────────────────────────────────────── class UnifiedMenuDelegate(NSObject): @@ -764,9 +733,8 @@ def restartMenu_(self, _sender): log.info("Restart menu: %s", result.get("message", result)) def startDevMode_(self, _sender): - """Start the uDev developer server (Dev Mode) so the Developer - card appears in UI Hub.""" - log.info("Starting Dev Mode developer server") + """Enable uCore's built-in Developer surface.""" + log.info("Enabling built-in Developer surface") try: if is_ucore_alive(): result = api_post_sync( @@ -777,11 +745,7 @@ def startDevMode_(self, _sender): "Backend dev start returned failure: %s", result ) else: - log.warning( - "Backend down — launching uDev developer server directly" - ) - _launch_dev_server_direct() - # Give Vite a short boot window before re-checking status. + log.warning("Backend unavailable; Developer surface cannot be enabled") time.sleep(1.5) except Exception as exc: log.error("Failed to start dev server: %s", exc) @@ -791,19 +755,11 @@ def startDevMode_(self, _sender): ) def stopDevMode_(self, _sender): - """Stop the uDev developer server (Dev Mode).""" - log.info("Stopping Dev Mode developer server") + """Disable uCore's built-in Developer mode.""" + log.info("Disabling built-in Developer mode") try: if is_ucore_alive(): api_post_sync("/api/developer/stop", timeout=5.0) - else: - import subprocess - - subprocess.run( - ["pkill", "-f", "developer-surface.*vite|vite.*5176"], - capture_output=True, - timeout=5, - ) except Exception as exc: log.error("Failed to stop dev server: %s", exc) self._dev_connected = is_dev_alive() @@ -812,7 +768,7 @@ def stopDevMode_(self, _sender): ) def toggleDevMode_(self, _sender): - """Toggle the uDev developer server (Dev Mode). + """Toggle uCore's built-in Developer mode. Single menu item shows the live status; clicking starts the server when stopped and stops it when running. @@ -823,7 +779,7 @@ def toggleDevMode_(self, _sender): self.startDevMode_(None) def quitApp_(self, _sender): - """Quit the menu app.""" + """Quit cleanly; launchd restarts crashes, not successful exits.""" log.info("Quitting uCore Menu") if self._refresh_timer: self._refresh_timer.cancel() diff --git a/backend/app/secret/store.py b/backend/app/secret/store.py index d4bdbe68..48e91ebf 100644 --- a/backend/app/secret/store.py +++ b/backend/app/secret/store.py @@ -1,8 +1,9 @@ """AES-256-GCM encrypted secret store for uCore. -Stores API keys and credentials encrypted at rest in ~/.ucore/secrets.enc. +Stores API keys and credentials encrypted beneath ``$UDOS_HOME/secrets``. Uses a 256-bit key derived from a stored salt + host identifier. """ + from __future__ import annotations import json @@ -17,6 +18,7 @@ AESGCM: Any try: from cryptography.hazmat.primitives.ciphers.aead import AESGCM as _AESGCM + AESGCM = _AESGCM except ImportError: AESGCM = None @@ -36,6 +38,7 @@ def _derive_key(master_key: bytes) -> bytes: return master_key from cryptography.hazmat.primitives import hashes # type: ignore from cryptography.hazmat.primitives.hkdf import HKDF # type: ignore + hkdf = HKDF( algorithm=hashes.SHA256(), length=32, @@ -103,7 +106,7 @@ def list_audit(self, limit: int = 100) -> list[dict]: return [] events: list[dict] = [] - for line in reversed(lines[-max(limit * 2, 200):]): + for line in reversed(lines[-max(limit * 2, 200) :]): if len(events) >= limit: break try: @@ -174,6 +177,7 @@ def save(self): def _save_plaintext(self): path = DATA_DIR / "secrets.json" + path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(self._secrets, indent=2)) path.chmod(0o600) self._dirty = False @@ -237,10 +241,18 @@ def get_all_env_vars(self) -> dict[str, str]: def sync_from_env(self): """Import known env vars into the store.""" known_keys = [ - "OPENROUTER_API_KEY", "GITHUB_TOKEN", "ANTHROPIC_API_KEY", - "MISTRAL_API_KEY", "DEEPSEEK_API_KEY", "OPENAI_API_KEY", - "GEMINI_API_KEY", "GROQ_API_KEY", "HUGGINGFACE_TOKEN", - "REPLICATE_API_KEY", "COHERE_API_KEY", "AI21_API_KEY", + "OPENROUTER_API_KEY", + "GITHUB_TOKEN", + "ANTHROPIC_API_KEY", + "MISTRAL_API_KEY", + "DEEPSEEK_API_KEY", + "OPENAI_API_KEY", + "GEMINI_API_KEY", + "GROQ_API_KEY", + "HUGGINGFACE_TOKEN", + "REPLICATE_API_KEY", + "COHERE_API_KEY", + "AI21_API_KEY", ] changed = False for key in known_keys: diff --git a/backend/app/services/agent_specialization.py b/backend/app/services/agent_specialization.py index c8fbbe00..7c0ce12d 100644 --- a/backend/app/services/agent_specialization.py +++ b/backend/app/services/agent_specialization.py @@ -10,6 +10,7 @@ - Cost tracking and timeout configuration - Capability tags for skill matching """ + from __future__ import annotations import logging @@ -19,6 +20,8 @@ import yaml # type: ignore[import-untyped] +from app.core.settings import settings + log = logging.getLogger(__name__) @@ -71,7 +74,7 @@ def __init__(self, config_path: str | None = None): self.workflow_templates: dict[str, WorkflowTemplate] = {} if config_path is None: - config_path = os.path.expanduser("~/.ucore/config/agents.yaml") + config_path = str(settings.config_dir / "agents.yaml") if not os.path.exists(config_path): # Fallback to repo config config_path = os.path.join( @@ -114,8 +117,7 @@ def load_config(self, config_path: str) -> None: for workflow_id, workflow_data in workflows.items(): try: stage_templates = [ - WorkflowStageTemplate(**stage) - for stage in workflow_data.get("stages", []) + WorkflowStageTemplate(**stage) for stage in workflow_data.get("stages", []) ] self.workflow_templates[workflow_id] = WorkflowTemplate( id=workflow_id, @@ -186,7 +188,8 @@ def _load_defaults(self) -> None: } def get_cost_tier_for_agent( - self, agent_id: str, + self, + agent_id: str, ) -> str: """Map an agent to its appropriate cost tier. @@ -208,20 +211,19 @@ def get_cost_tier_for_agent( return "premium" def get_agents_by_cost_tier( - self, tier: str, + self, + tier: str, ) -> list[AgentSpecialization]: """Return all agents mapped to a given cost tier.""" return [ - agent for agent in self.agents.values() + agent + for agent in self.agents.values() if self.get_cost_tier_for_agent(agent.id) == tier ] def get_max_cost_tier(self) -> str: """Return the highest (most expensive) tier currently configured.""" - tiers = [ - self.get_cost_tier_for_agent(a.id) - for a in self.agents.values() - ] + tiers = [self.get_cost_tier_for_agent(a.id) for a in self.agents.values()] if not tiers: return "free" return max(tiers, key=lambda t: self.COST_TIER_MAP.get(t, 99)) @@ -274,10 +276,7 @@ def get_agents_for_capability( capability: str, ) -> list[AgentSpecialization]: """Get all agents that have a specific capability.""" - return [ - agent for agent in self.agents.values() - if capability in agent.capabilities - ] + return [agent for agent in self.agents.values() if capability in agent.capabilities] def get_surface_taxonomy(self) -> dict[str, dict[str, Any]]: """Return the configured canonical surface ownership model.""" @@ -293,9 +292,7 @@ def get_workflow_template( for workflow in self.workflow_templates.values(): if task_type in workflow.task_types: return workflow - if summary and any( - token.lower() in summary for token in workflow.match_any - ): + if summary and any(token.lower() in summary for token in workflow.match_any): return workflow return None diff --git a/backend/app/services/automation/README.md b/backend/app/services/automation/README.md deleted file mode 100644 index 398bd5cd..00000000 --- a/backend/app/services/automation/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# Automation Engine - -uDev's knowledge ingestion and enrichment pipeline. Runs on the **OpenRouter free tier** (no paid API). - -## Quick Start - -```bash -# 1. Install dependencies -cd ~/Code/uDev/automation -pip install -r requirements.txt - -# 2. Configure (edit config.yaml) -# - Set openrouter_api_key if using OpenRouter -# - Verify browserui_endpoint points to your BrowserUI server -# - Verify knowledge_root points to ~/Code/uDev/global-knowledge - -# 3. Dry-run (preview only) -python engine.py --dry-run - -# 4. Run full pipeline -python engine.py - -# 5. Process a single topic -python engine.py --topic binder -``` - -## Pipeline Stages - -| Step | Action | Description | -|------|--------|-------------| -| 1 | **Scan** | Walk `global-knowledge/` for `.md` files | -| 2 | **Parse** | Extract front‑matter metadata | -| 3 | **Scrape** | Fetch web references via BrowserUI | -| 4 | **Citations** | Insert footnote references into markdown | -| 5 | **Index** | Update `SUMMARY.md` table of contents | -| 6 | **Notebook** | Convert markdown to `.ipynb` | -| 7 | **Dedup** | SHA256 cache to skip unchanged files | -| 8 | **Git** | Commit & push to `automation/updates` branch | - -## Configuration - -Edit `config.yaml`: - -```yaml -knowledge_root: "~/Code/uDev/global-knowledge" -browserui_endpoint: "http://localhost:8000" -openrouter_api_key: "" # optional, for enriched scraping - -git: - branch: "automation/updates" - base_branch: "main" - -scraping: - max_references_per_article: 5 - timeout_seconds: 30 - max_retries: 3 -``` - -## File Structure - -``` -automation/ -├── engine.py # Main orchestration script -├── config.yaml # Settings -├── requirements.txt # Python dependencies -└── README.md # This file -``` - -## Risks & Mitigation - -| Risk | Mitigation | -|------|------------| -| Duplicate content | SHA256-based deduplication cache | -| BrowserUI rate limits | Exponential backoff, respects robots.txt | -| Citation errors | URL validation, requests.head pre-check | -| Notebook conversion errors | Fallback to simple cell conversion | -| Unintended commits | Runs on separate `automation/updates` branch | diff --git a/backend/app/services/automation/config.yaml b/backend/app/services/automation/config.yaml deleted file mode 100644 index e71c1b0f..00000000 --- a/backend/app/services/automation/config.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# uDev Automation Engine Configuration -# ====================================== - -# Root of the global-knowledge library -knowledge_root: "~/Code/uDev/global-knowledge" - -# Repository root for git operations -repo_root: "~/Code/uDev" - -# BrowserUI endpoint for web scraping references -browserui_endpoint: "http://localhost:8000" - -# OpenRouter API key (free tier — no paid models) -openrouter_api_key: "" - -# Git settings -git: - branch: "automation/updates" # PR branch for automated changes - base_branch: "main" - author_name: "uDev Automation" - author_email: "automation@udev.local" - -# Scanning -scanning: - # File patterns to include - include_patterns: - - "*.md" - # Directories to exclude - exclude_dirs: - - ".git" - - "node_modules" - - "__pycache__" - -# Scraping -scraping: - timeout_seconds: 30 - max_retries: 3 - backoff_factor: 2.0 # exponential backoff multiplier - respect_robots_txt: true - max_references_per_article: 5 - -# Notebook generation -notebook: - kernel: "python3" - output_suffix: "-demo" - -# Deduplication -dedup: - hash_algorithm: "sha256" - cache_file: ".automation-cache.json" diff --git a/backend/app/services/automation/requirements.txt b/backend/app/services/automation/requirements.txt deleted file mode 100644 index e8e40d42..00000000 --- a/backend/app/services/automation/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Automation Engine Dependencies -pyyaml>=6.0 -requests>=2.31 -beautifulsoup4>=4.12 -nbconvert>=7.14 -nbformat>=5.10 -gitpython>=3.1 -markdown>=3.5 -python-frontmatter>=1.0 diff --git a/backend/app/services/autonomy/autonomy_engine.py b/backend/app/services/autonomy/autonomy_engine.py index 49f2b108..a21c4097 100644 --- a/backend/app/services/autonomy/autonomy_engine.py +++ b/backend/app/services/autonomy/autonomy_engine.py @@ -3,7 +3,7 @@ Runs every 24 hours (or on demand) to: - Execute ecosystem-audit - Check health thresholds -- Log results to ~/.ucore/logs/ +- Log results to ``$UDOS_HOME/logs`` - Alert if health drops below 95% Usage as cron job: @@ -12,6 +12,7 @@ Usage as one-shot: python -m health.autonomy_engine --once """ + from __future__ import annotations import json @@ -23,7 +24,9 @@ from pathlib import Path from typing import Any -LOG_DIR = Path.home() / ".ucore" / "logs" +from app.core.settings import settings + +LOG_DIR = settings.logs_dir LOG_DIR.mkdir(parents=True, exist_ok=True) AUDIT_LOG = LOG_DIR / "autonomy.log" @@ -41,7 +44,9 @@ log = logging.getLogger("autonomy") -def _call_api(path: str, method: str = "GET", body: dict | None = None, timeout: int = 120) -> dict | None: +def _call_api( + path: str, method: str = "GET", body: dict | None = None, timeout: int = 120 +) -> dict | None: """Call the uCore backend API.""" import urllib.request @@ -161,7 +166,9 @@ def run_full_check() -> dict[str, Any]: } save_state(state) - log.info(f"State saved. Health: {health_pct}% | Ollama: {'online' if ollama.get('online') else 'offline'}") + log.info( + f"State saved. Health: {health_pct}% | Ollama: {'online' if ollama.get('online') else 'offline'}" + ) return state @@ -177,7 +184,9 @@ def main() -> None: parser = argparse.ArgumentParser(description="uCore Autonomy Engine") parser.add_argument("--once", action="store_true", help="Run once and exit") - parser.add_argument("--interval", type=int, default=86400, help="Seconds between checks (default: 24h)") + parser.add_argument( + "--interval", type=int, default=86400, help="Seconds between checks (default: 24h)" + ) args = parser.parse_args() if args.once: diff --git a/backend/app/services/autonomy/setup_launchd.sh b/backend/app/services/autonomy/setup_launchd.sh deleted file mode 100644 index 8a4d53c0..00000000 --- a/backend/app/services/autonomy/setup_launchd.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash -# Setup overnight autonomy engine as a launchd job -# Runs every 6 hours to ensure 4x daily health checks -set -e - -PLIST="$HOME/Library/LaunchAgents/com.udos.ucore-autonomy.plist" -REPO="$HOME/Code/uDev" - -cat > "$PLIST" < - - - - Label - com.udos.ucore-autonomy - ProgramArguments - - /usr/bin/python3 - ${REPO}/autonomy/autonomy_engine.py - --once - - WorkingDirectory - ${REPO} - StartInterval - 21600 - StandardOutPath - ${HOME}/.ucore/logs/autonomy_launchd.log - StandardErrorPath - ${HOME}/.ucore/logs/autonomy_launchd_err.log - RunAtLoad - - - -PLIST - -launchctl unload "$PLIST" 2>/dev/null || true -launchctl load "$PLIST" - -echo "Autonomy engine installed — runs every 6 hours" -echo "State file: ~/.ucore/logs/autonomy_state.json" -echo "API endpoint: GET /api/autonomy/state" \ No newline at end of file diff --git a/backend/app/services/budget_manager.py b/backend/app/services/budget_manager.py index 66638ec4..8959a400 100644 --- a/backend/app/services/budget_manager.py +++ b/backend/app/services/budget_manager.py @@ -7,7 +7,7 @@ - Per-agent budget caps (reviewer gets more than dev) - Circuit breaker: when budget < threshold, only free tier -Config: ~/.ucore/config/budget.yaml +Config: ``$UDOS_HOME/config/budget.yaml`` Usage: bm = BudgetManager.get() @@ -21,13 +21,14 @@ import logging import sqlite3 from datetime import UTC, datetime, timedelta -from pathlib import Path from typing import Any +from app.core.settings import settings + log = logging.getLogger("ucore.budget_manager") -DB_PATH = Path.home() / ".ucore" / "indices" / "budget.db" -CONFIG_PATH = Path.home() / ".ucore" / "config" / "budget.yaml" +DB_PATH = settings.udos_home / "indices" / "budget.db" +CONFIG_PATH = settings.config_dir / "budget.yaml" SCHEMA_SQL = """ CREATE TABLE IF NOT EXISTS spend_log ( diff --git a/backend/app/services/catalog/catalog_service.py b/backend/app/services/catalog/catalog_service.py index 2b2ecbe7..e7574c9f 100644 --- a/backend/app/services/catalog/catalog_service.py +++ b/backend/app/services/catalog/catalog_service.py @@ -9,6 +9,8 @@ from pathlib import Path from typing import Any, Dict, List, Optional, Set +from app.core.settings import settings + from .models import ( CatalogEntry, EntryType, @@ -36,7 +38,7 @@ def __init__(self): return # Catalog directory - self.catalog_dir = Path.home() / ".ucore" / "catalog" + self.catalog_dir = settings.udos_home / "catalog" self.catalog_dir.mkdir(parents=True, exist_ok=True) # Database path @@ -124,34 +126,43 @@ def add_entry(self, entry: CatalogEntry) -> bool: cursor = conn.cursor() # Insert or replace entry - cursor.execute(""" + cursor.execute( + """ INSERT OR REPLACE INTO entries (uid, type, name, description, metadata, relationships, tags, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ( - entry.uid, - entry.type.value, - entry.name, - entry.description, - json.dumps(entry.metadata), - json.dumps([r.dict() for r in entry.relationships]), - json.dumps(entry.tags), - entry.created_at.isoformat(), - entry.updated_at.isoformat(), - )) + """, + ( + entry.uid, + entry.type.value, + entry.name, + entry.description, + json.dumps(entry.metadata), + json.dumps([r.dict() for r in entry.relationships]), + json.dumps(entry.tags), + entry.created_at.isoformat(), + entry.updated_at.isoformat(), + ), + ) # Update FTS index - cursor.execute(""" + cursor.execute( + """ INSERT OR REPLACE INTO entry_fts (uid, name, description) VALUES (?, ?, ?) - """, (entry.uid, entry.name, entry.description)) + """, + (entry.uid, entry.name, entry.description), + ) # Insert relationships for rel in entry.relationships: - cursor.execute(""" + cursor.execute( + """ INSERT INTO relationships (uid, type, target, weight) VALUES (?, ?, ?, ?) - """, (entry.uid, rel.type.value, rel.target, rel.weight)) + """, + (entry.uid, rel.type.value, rel.target, rel.weight), + ) conn.commit() conn.close() @@ -174,11 +185,14 @@ def get_entry(self, uid: SpatialUID) -> Optional[CatalogEntry]: conn = sqlite3.connect(self.db_path) cursor = conn.cursor() - cursor.execute(""" + cursor.execute( + """ SELECT uid, type, name, description, metadata, relationships, tags, created_at, updated_at FROM entries WHERE uid = ? - """, (uid,)) + """, + (uid,), + ) row = cursor.fetchone() conn.close() @@ -213,20 +227,26 @@ def list_entries( cursor = conn.cursor() if entry_type: - cursor.execute(""" + cursor.execute( + """ SELECT uid, type, name, description, metadata, relationships, tags, created_at, updated_at FROM entries WHERE type = ? ORDER BY updated_at DESC LIMIT ? OFFSET ? - """, (entry_type.value, limit, offset)) + """, + (entry_type.value, limit, offset), + ) else: - cursor.execute(""" + cursor.execute( + """ SELECT uid, type, name, description, metadata, relationships, tags, created_at, updated_at FROM entries ORDER BY updated_at DESC LIMIT ? OFFSET ? - """, (limit, offset)) + """, + (limit, offset), + ) rows = cursor.fetchall() conn.close() @@ -258,23 +278,29 @@ def search( cursor = conn.cursor() if entry_type: - cursor.execute(""" + cursor.execute( + """ SELECT e.uid, e.type, e.name, e.description, e.metadata, e.relationships, e.tags, e.created_at, e.updated_at FROM entries e JOIN entry_fts fts ON e.uid = fts.uid WHERE fts MATCH ? AND e.type = ? ORDER BY fts.rank LIMIT ? - """, (query, entry_type.value, limit)) + """, + (query, entry_type.value, limit), + ) else: - cursor.execute(""" + cursor.execute( + """ SELECT e.uid, e.type, e.name, e.description, e.metadata, e.relationships, e.tags, e.created_at, e.updated_at FROM entries e JOIN entry_fts fts ON e.uid = fts.uid WHERE fts MATCH ? ORDER BY fts.rank LIMIT ? - """, (query, limit)) + """, + (query, limit), + ) rows = cursor.fetchall() conn.close() @@ -304,11 +330,14 @@ def get_relationships( cursor = conn.cursor() # Get direct relationships - cursor.execute(""" + cursor.execute( + """ SELECT type, target, weight FROM relationships WHERE uid = ? - """, (uid,)) + """, + (uid,), + ) rows = cursor.fetchall() conn.close() @@ -356,21 +385,25 @@ def traverse(current_uid: SpatialUID, current_depth: int): # Add node entry = self.get_entry(current_uid) if entry: - graph["nodes"].append({ - "uid": entry.uid, - "type": entry.type.value, - "name": entry.name, - }) + graph["nodes"].append( + { + "uid": entry.uid, + "type": entry.type.value, + "name": entry.name, + } + ) # Get relationships relationships = self.get_relationships(current_uid, 0) for rel in relationships: target_uid = SpatialUID(rel["target"]) - graph["edges"].append({ - "from": current_uid, - "to": target_uid, - "type": rel["type"], - }) + graph["edges"].append( + { + "from": current_uid, + "to": target_uid, + "type": rel["type"], + } + ) traverse(target_uid, current_depth + 1) traverse(uid, 0) @@ -447,14 +480,7 @@ def _row_to_entry(self, row: tuple) -> CatalogEntry: name=name, description=description, metadata=json.loads(metadata_json), - relationships=[ - Relationship( - type=str(r["type"]), - target=SpatialUID(r["target"]), - weight=r.get("weight", 0.5), - ) - for r in json.loads(relationships_json) - ], + relationships=json.loads(relationships_json), tags=json.loads(tags_json), created_at=datetime.fromisoformat(created_at), updated_at=datetime.fromisoformat(updated_at), diff --git a/backend/app/services/control_service.py b/backend/app/services/control_service.py index 6f70b8b7..35a23657 100644 --- a/backend/app/services/control_service.py +++ b/backend/app/services/control_service.py @@ -1,9 +1,9 @@ """Control Service — aggregates all uCore ecosystem status into one payload. Used by the Control Panel (Developer Surface) to display: -- Status badges (Cline, OpenRouter, Hivemind, Roundtable, Ollama, Feed, Slate, Budget) +- Status badges (OpenRouter, Hivemind, Roundtable, Ollama, Feed, Slate, Budget) - Live feed stream -- Agent status (Hivemind consensus, Roundtable swarm, Cline session, Ollama models) +- Agent status (Hivemind consensus, Roundtable swarm, Ollama models) - Cost dashboard (daily/weekly/monthly) - Active mission (from .tasker) - Tasker overview, MCP servers, Slates, Alerts @@ -110,25 +110,6 @@ async def _start_hivemind_server() -> tuple[bool, str]: # Status badge checks # --------------------------------------------------------------------------- -async def check_cline() -> dict: - """Check Cline CLI availability.""" - result = {"online": False, "detail": "Cline CLI not reachable"} - # Try Cline CLI health endpoint - data = await _http_get("http://localhost:8485/health", timeout=1.0) - if data: - result["online"] = True - result["detail"] = "Cline CLI connected" - result["data"] = data - return result - # Fallback: check if cline CLI is in PATH - cli_path = await _run_shell(["which", "cline"], timeout=1.0) - if cli_path: - result["online"] = True - result["detail"] = f"Cline CLI available at {cli_path}" - return result - return result - - async def check_openrouter() -> dict: """Check OpenRouter API connectivity.""" # Try local config @@ -270,11 +251,6 @@ async def recover_offline_services() -> dict: # Roundtable is hosted in the same server process. actions.append("Roundtable piggybacks on Hivemind process") - # OpenRouter and Cline are not daemonized by uCore here; report guidance. - cline_before = statuses_before.get("cline", {}) - if not cline_before.get("online"): - actions.append("Cline not detected locally; verify Cline installation/CLI path") - openrouter_before = statuses_before.get("openrouter", {}) if not openrouter_before.get("online"): actions.append("OpenRouter remains unavailable; verify OPENROUTER_API_KEY in Secret Store") @@ -345,14 +321,6 @@ async def get_roundtable_status() -> dict: return {"status": "unknown", "detail": "Roundtable status unavailable"} -async def get_cline_status() -> dict: - """Get current Cline session info.""" - data = await _http_get("http://localhost:8485/status", timeout=1.0) - if data: - return {"active": True, "data": data} - return {"active": False, "detail": "No active Cline session"} - - async def get_ollama_status() -> dict: """Get detailed Ollama status.""" data = await _http_get("http://localhost:11434/api/tags", timeout=1.5) @@ -407,8 +375,8 @@ async def get_active_mission() -> dict: lines = content.splitlines() name = latest.stem.replace("sprint-", "").replace("-", " ").title() - tasks_total = sum(1 for l in lines if l.strip().startswith("- [")) - tasks_done = sum(1 for l in lines if l.strip().startswith("- [x]")) + tasks_total = sum(1 for line in lines if line.strip().startswith("- [")) + tasks_done = sum(1 for line in lines if line.strip().startswith("- [x]")) # Count binders binders = list(td.glob("binder-*.md")) @@ -609,7 +577,6 @@ async def get_control_status() -> dict: async def _gather_statuses() -> dict: results = await asyncio.gather( - check_cline(), check_openrouter(), check_hivemind(), check_roundtable(), @@ -618,7 +585,7 @@ async def _gather_statuses() -> dict: check_slate(), get_cost_status(), ) - keys = ["cline", "openrouter", "hivemind", "roundtable", "ollama", "feed", "slate", "budget"] + keys = ["openrouter", "hivemind", "roundtable", "ollama", "feed", "slate", "budget"] return dict(zip(keys, results)) @@ -631,10 +598,9 @@ async def _gather_feed() -> dict: async def _gather_agents() -> dict: - hive, rt, cline, ollama = await asyncio.gather( + hive, rt, ollama = await asyncio.gather( get_hivemind_status(), get_roundtable_status(), - get_cline_status(), get_ollama_status(), ) - return {"hivemind": hive, "roundtable": rt, "cline": cline, "ollama": ollama} + return {"hivemind": hive, "roundtable": rt, "ollama": ollama} diff --git a/backend/app/services/dev_layer.py b/backend/app/services/dev_layer.py index 0d781d2c..d458f6a2 100644 --- a/backend/app/services/dev_layer.py +++ b/backend/app/services/dev_layer.py @@ -1,19 +1,20 @@ """dev_layer — Dev Mode toggle service. -Simple 3-state toggle persisted to ~/.ucore/config.yaml. +Simple 3-state toggle persisted under ``$UDOS_HOME``. Exposed via REST API at /api/dev-layer/state and /api/dev-layer/toggle. """ from __future__ import annotations import logging from enum import Enum -from pathlib import Path import yaml +from app.core.settings import settings + log = logging.getLogger("ucore.services.dev_layer") -CONFIG_FILE = Path.home() / ".ucore" / "config.yaml" +CONFIG_FILE = settings.udos_home / "config.yaml" class DevMode(Enum): diff --git a/backend/app/services/distribution_system/package_manager.py b/backend/app/services/distribution_system/package_manager.py index 02cafc52..c0270c65 100644 --- a/backend/app/services/distribution_system/package_manager.py +++ b/backend/app/services/distribution_system/package_manager.py @@ -6,6 +6,7 @@ - Repairing a package re-renders the plate - Removing a package triggers the destroy workflow """ + from __future__ import annotations import json @@ -108,13 +109,12 @@ def _create_plate_from_package( "destroy": { "salvage_keys": ["package_name", "version"], "rebuild_command": ( - f"python -m app.services.distribution_system " - f"--install {package_name}" + f"python -m app.services.distribution_system --install {package_name}" ), "backup_before_destroy": True, "spool_archive": { "enabled": True, - "spool_dir": "~/.ucore/logs", + "spool_dir": "${UDOS_HOME}/logs", "compress_metadata": True, "include_source": False, "include_lessons": True, @@ -127,6 +127,7 @@ def _create_plate_from_package( # Write plate with open(target_path, "w") as f: import yaml + yaml.dump(plate_data, f, default_flow_style=False) log.info("Created plate %s from package %s", plate_id, package_name) @@ -288,26 +289,26 @@ def list_packages(self) -> list[dict[str, Any]]: List of package info dicts with plate status """ modules = self.dist.list_modules() - plates = discover_plates() - results = [] for module in modules: plate_id = self._find_plate_for_package(module.name) plate_status = "found" if plate_id else "not_found" - results.append({ - "name": module.name, - "version": module.version, - "status": module.status.value, - "health_status": module.health_status, - "plate_id": plate_id, - "plate_status": plate_status, - "size_bytes": module.size_bytes, - "installed_path": module.installed_path, - "last_updated": ( - module.last_updated.isoformat() if module.last_updated else None - ), - }) + results.append( + { + "name": module.name, + "version": module.version, + "status": module.status.value, + "health_status": module.health_status, + "plate_id": plate_id, + "plate_status": plate_status, + "size_bytes": module.size_bytes, + "installed_path": module.installed_path, + "last_updated": ( + module.last_updated.isoformat() if module.last_updated else None + ), + } + ) return results @@ -346,9 +347,7 @@ def get_package_info(self, package_name: str) -> dict[str, Any] | None: "health_status": module.health_status, "size_bytes": module.size_bytes, "installed_path": module.installed_path, - "last_updated": ( - module.last_updated.isoformat() if module.last_updated else None - ), + "last_updated": (module.last_updated.isoformat() if module.last_updated else None), "plate": plate_info, "dependencies": module.dependencies, "metadata": module.metadata, @@ -369,39 +368,25 @@ def health_check(self) -> dict[str, Any]: for pkg in packages: if pkg["health_status"] == "unhealthy": issues.append(f"Package {pkg['name']} is unhealthy") - recommendations.append( - f"Run: repair {pkg['name']}" - ) + recommendations.append(f"Run: repair {pkg['name']}") if pkg["plate_status"] == "not_found": issues.append(f"Package {pkg['name']} has no plate definition") - recommendations.append( - f"Run: install {pkg['name']} to create plate" - ) + recommendations.append(f"Run: install {pkg['name']} to create plate") # Check for orphaned plates (plates without packages) for pid in plates: - has_package = any( - pkg.get("plate_id") == pid for pkg in packages - ) + has_package = any(pkg.get("plate_id") == pid for pkg in packages) if not has_package: issues.append(f"Orphaned plate: {pid} (no corresponding package)") return { "total_packages": len(packages), "total_plates": len(plates), - "healthy_packages": sum( - 1 for p in packages if p["health_status"] == "healthy" - ), - "unhealthy_packages": sum( - 1 for p in packages if p["health_status"] == "unhealthy" - ), - "packages_with_plates": sum( - 1 for p in packages if p["plate_status"] == "found" - ), - "packages_without_plates": sum( - 1 for p in packages if p["plate_status"] == "not_found" - ), + "healthy_packages": sum(1 for p in packages if p["health_status"] == "healthy"), + "unhealthy_packages": sum(1 for p in packages if p["health_status"] == "unhealthy"), + "packages_with_plates": sum(1 for p in packages if p["plate_status"] == "found"), + "packages_without_plates": sum(1 for p in packages if p["plate_status"] == "not_found"), "issues": issues, "recommendations": recommendations, } diff --git a/backend/app/services/docs_mirror.py b/backend/app/services/docs_mirror.py index cfd6aa82..be2628d2 100644 --- a/backend/app/services/docs_mirror.py +++ b/backend/app/services/docs_mirror.py @@ -15,17 +15,15 @@ from typing import Any from app.core.logging import log +from app.core.settings import settings -MIRROR_ROOT = Path.home() / ".ucore" / "docs-mirror" +MIRROR_ROOT = settings.udos_home / "docs-mirror" MIRROR_INDEX = MIRROR_ROOT / "_mirror.json" # Dev-lane component doc roots — in-repo docs/ only. CORE_DOC_ROOTS: dict[str, Path] = { - "uCore": Path.home() / "Code" / "uCore" / "docs", - "uFlow": Path.home() / "Code" / "uFlow" / "docs", - "uKnowledge": Path.home() / "Code" / "uKnowledge" / "docs", - "uCode": Path.home() / "Code" / "uCode" / "docs", - "uVector": Path.home() / "Code" / "uVector" / "docs", + name: settings.udos_root / name / "docs" + for name in ("uCore", "uFlow", "uKnowledge", "uCode", "uVector") } # User-lane paths that must NEVER be mirrored. @@ -61,7 +59,7 @@ def _git_sha(repo_root: Path) -> str: def discover_extension_doc_roots(code_root: Path | None = None) -> dict[str, Path]: """Discover `udos-*` extension repos under ~/Code with a docs/ directory.""" - code = code_root or (Path.home() / "Code") + code = code_root or settings.udos_root roots: dict[str, Path] = {} if not code.is_dir(): return roots diff --git a/backend/app/services/docs_publish.py b/backend/app/services/docs_publish.py index 367dacba..389c46a9 100644 --- a/backend/app/services/docs_publish.py +++ b/backend/app/services/docs_publish.py @@ -1,11 +1,12 @@ """Docs publish — build a static docs site from the component mirror. Dev Lane only. Generates a self-contained static HTML site from -``~/.ucore/docs-mirror/`` into ``~/Public/doc-sites/udos-docs/`` and +``$UDOS_HOME/docs-mirror`` into ``~/Public/doc-sites/udos-docs/`` and optionally deploys it (git commit + push) to ``docs.udo.guide``. This module never touches user-lane vault paths. """ + from __future__ import annotations import html @@ -19,12 +20,15 @@ from typing import Any from app.core.logging import log +from app.core.settings import settings SITE_ROOT = Path.home() / "Public" / "doc-sites" / "udos-docs" STATUS_FILE = "publish.json" SITE_TITLE = "uDos Documentation" -SITE_DESCRIPTION = "Component documentation for the uDos ecosystem, mirrored from in-repo docs/ directories." +SITE_DESCRIPTION = ( + "Component documentation for the uDos ecosystem, mirrored from in-repo docs/ directories." +) # ─── Markdown rendering (stdlib only) ───────────────────────────── @@ -113,20 +117,21 @@ def flush_list() -> None: # ─── Static site generation ─────────────────────────────────────── + def _page(title: str, body: str, crumb: str = "") -> str: header = ( '" ) return ( - "" - f"" + '' + f'' f"{html.escape(title)} — {html.escape(SITE_TITLE)}" '' "" - f"{header}
{body}
" + f'{header}
{body}
' '
Generated by uCore docs-publish.
' "" ) @@ -212,7 +217,7 @@ def _slug(path: str) -> str: def build_site(mirror_root: Path | None = None, site_root: Path | None = None) -> dict[str, Any]: """Build the static site from the mirror. Returns status summary.""" - mirror = mirror_root or (Path.home() / ".ucore" / "docs-mirror") + mirror = mirror_root or (settings.udos_home / "docs-mirror") root = site_root or SITE_ROOT index_file = mirror / "_mirror.json" @@ -266,7 +271,7 @@ def build_site(mirror_root: Path | None = None, site_root: Path | None = None) - repo_page = _page( f"{repo} — {len(repo_rows)} docs", - f"

{repo}

{len(repo_rows)} documents

" + "\n".join(repo_rows), + f'

{repo}

{len(repo_rows)} documents

' + "\n".join(repo_rows), repo, ) (docs_dir / repo / "index.html").write_text(repo_page, encoding="utf-8") @@ -293,9 +298,11 @@ def build_site(mirror_root: Path | None = None, site_root: Path | None = None) - (root / ".gitignore").write_text(".deploy-remote\n", encoding="utf-8") # Sitemap. - sitemap_rows = ["
  • Home
  • "] + sitemap_rows = ['
  • Home
  • '] for repo in sorted(by_repo): - sitemap_rows.append(f"
  • {html.escape(repo)}
  • ") + sitemap_rows.append( + f'
  • {html.escape(repo)}
  • ' + ) (root / "sitemap.html").write_text( _page("Sitemap", "

    Sitemap

      " + "".join(sitemap_rows) + "
    "), encoding="utf-8", @@ -370,9 +377,7 @@ def _run(args: list[str]) -> dict[str, Any]: add = _run(["add", "-A"]) commit = _run(["commit", "-m", f"publish: {datetime.now(UTC).isoformat()}"]) - commit_ok = commit["ok"] or "nothing to commit" in ( - commit["stdout"] + commit["stderr"] - ) + commit_ok = commit["ok"] or "nothing to commit" in (commit["stdout"] + commit["stderr"]) remote = _remote_url(root) if remote: diff --git a/backend/app/services/health.py b/backend/app/services/health.py index b6ae5d4e..477bd710 100644 --- a/backend/app/services/health.py +++ b/backend/app/services/health.py @@ -3,6 +3,7 @@ import glob from pathlib import Path +from app.core.settings import settings from app.skills.state import read_state @@ -10,12 +11,13 @@ def _clean_and_truncate_line(line: str, max_len: int = 150) -> str: """Truncate the line to a safe length and redact secret/token patterns.""" # Simple redaction for common authorization/token values if any import re + # Match strings resembling bearer tokens, api keys, password/secret strings # Redact Authorization header or passwords or any other standard credential strings redacted = re.sub( r'(?i)(token|bearer|auth|authorization|api_key|password|secret|key)\s*[:= ]\s*["\']?[a-zA-Z0-9_\.\-]{8,150}["\']?', r'\1: "[REDACTED]"', - line + line, ) if len(redacted) > max_len: return redacted[:max_len] + "..." @@ -26,14 +28,14 @@ def _tail_lines(path: Path, max_lines: int = 200) -> list[str]: try: with open(path, encoding="utf-8", errors="replace") as f: lines = f.readlines() - return [l.rstrip("\n") for l in lines[-max_lines:]] + return [line.rstrip("\n") for line in lines[-max_lines:]] except Exception: return [] def recent_errors_from_logs(log_dir: Path | None = None, max_entries: int = 50) -> list[dict]: if log_dir is None: - log_dir = Path.home() / ".ucore" / "logs" + log_dir = settings.logs_dir out: list[dict] = [] if not log_dir.exists(): return out diff --git a/backend/app/services/health_monitor.py b/backend/app/services/health_monitor.py index 3dc11c3c..e616f2d0 100644 --- a/backend/app/services/health_monitor.py +++ b/backend/app/services/health_monitor.py @@ -21,6 +21,8 @@ from pathlib import Path from typing import Any, Dict, List, Optional +from app.core.settings import settings + log = logging.getLogger("health_monitor") # ─── Data Models ────────────────────────────────────────────────── @@ -29,6 +31,7 @@ @dataclass class HealthEvent: """Single health check event""" + timestamp: str component: str # "backend", "frontend", "ollama", "database", etc. status: str # "ok", "degraded", "error", "recovering" @@ -41,6 +44,7 @@ class HealthEvent: @dataclass class ComponentHealth: """Component health status""" + name: str status: str # "ok", "degraded", "error" last_check: str @@ -60,7 +64,7 @@ def __init__(self): self.components: Dict[str, ComponentHealth] = {} self.events: List[HealthEvent] = [] self.max_events = 500 # Keep last 500 events - self.log_dir = Path("~/.ucore/logs").expanduser() + self.log_dir = settings.logs_dir self.log_dir.mkdir(parents=True, exist_ok=True) self.running = False self.check_interval = 5.0 # seconds @@ -76,7 +80,7 @@ async def stop(self): """Stop the health monitor.""" log.info("Health monitor stopping...") self.running = False - if hasattr(self, '_task') and self._task: + if hasattr(self, "_task") and self._task: self._task.cancel() try: await self._task @@ -114,12 +118,10 @@ async def _check_all_components(self): def _check_backend(self) -> tuple[str, str]: """Check if backend is responsive (runs in thread pool).""" try: - from app.core.settings import settings port = settings.port import urllib.request - response = urllib.request.urlopen( - f"http://localhost:{port}/api/health", timeout=2 - ) + + response = urllib.request.urlopen(f"http://localhost:{port}/api/health", timeout=2) if response.status == 200: return "ok", f"Backend responding normally on port {port}" else: @@ -132,12 +134,10 @@ def _check_backend(self) -> tuple[str, str]: def _check_database(self) -> tuple[str, str]: """Check database connectivity (runs in thread pool).""" try: - indices_dir = Path("~/.ucore/indices").expanduser() - db_files = ( - list(indices_dir.glob("*.db")) if indices_dir.exists() else [] - ) + indices_dir = settings.udos_home / "indices" + db_files = list(indices_dir.glob("*.db")) if indices_dir.exists() else [] if not db_files: - return "error", "No database files in ~/.ucore/indices" + return "error", f"No database files in {indices_dir}" if os.access(db_files[0], os.R_OK): return "ok", f"Database accessible ({len(db_files)} db file(s))" @@ -169,11 +169,13 @@ def _check_imports(self) -> tuple[str, str]: def _check_popcorn(self) -> tuple[str, str]: """Check Popcorn status (runs in thread pool, macOS only).""" import platform + if platform.system() != "Darwin": return "ok", "Popcorn not applicable (not macOS)" try: from app.services.popcorn_manager import get_popcorn_status + status = get_popcorn_status() menu = status.get("menu", {}) @@ -230,7 +232,7 @@ async def _record_check(self, component: str, status: str, message: str): self.events.append(event) if len(self.events) > self.max_events: - self.events = self.events[-self.max_events:] + self.events = self.events[-self.max_events :] # Log it if severity == "error": @@ -268,6 +270,7 @@ async def _recover_backend(self) -> str: log.info("Attempting backend recovery via /api/control/recover...") try: import urllib.request + req = urllib.request.Request( "http://127.0.0.1:8484/api/control/recover", method="POST", @@ -282,10 +285,16 @@ async def _recover_backend(self) -> str: # Backend is completely down — try restart via launchd log.info("Backend unreachable; attempting launchd kickstart...") import subprocess + try: subprocess.run( - ["launchctl", "kickstart", "gui/$(id -u)/com.udos.ucore-server"], - capture_output=True, timeout=5, + [ + "launchctl", + "kickstart", + f"gui/{os.getuid()}/com.udos.ucore-server", + ], + capture_output=True, + timeout=5, ) return "backend_launchd_kickstart_attempted" except Exception as e: @@ -297,11 +306,12 @@ async def _recover_database(self) -> str: """Attempt to recover database connectivity.""" log.info("Attempting database recovery...") try: - db_path = Path("~/.ucore/ucore.db").expanduser() + db_path = settings.data_dir / "ucore.db" if not db_path.exists(): # Try to recreate from migration try: from app.core.database import migrate_db + result = migrate_db() return f"database_recreated_v{result.get('version', '?')}" except Exception as e: @@ -322,6 +332,7 @@ async def _recover_popcorn(self) -> str: log.info("Attempting Popcorn recovery...") try: from app.services.popcorn_manager import perform_action + result = perform_action("restart-menu") if result.get("success"): return "popcorn_restarted_via_menu" @@ -333,9 +344,7 @@ def get_status(self) -> Dict[str, Any]: """Get current health status""" return { "timestamp": datetime.now(timezone.utc).isoformat(), - "components": { - name: asdict(comp) for name, comp in self.components.items() - }, + "components": {name: asdict(comp) for name, comp in self.components.items()}, "events_count": len(self.events), "last_events": [asdict(e) for e in self.events[-10:]], # Last 10 } diff --git a/backend/app/services/history_service.py b/backend/app/services/history_service.py index a46c860b..4cad5715 100644 --- a/backend/app/services/history_service.py +++ b/backend/app/services/history_service.py @@ -6,7 +6,7 @@ - Manual snapshots (user-triggered via History tab) - Undo/rollback operations -Database: ~/.ucore/history/actions.db +Database: ``$UDOS_HOME/history/actions.db`` """ from __future__ import annotations @@ -18,10 +18,12 @@ from pathlib import Path from typing import Any +from app.core.settings import settings + log = logging.getLogger("ucore.history") -DB_PATH = Path.home() / ".ucore" / "history" / "actions.db" -UCORE_ROOT = Path.home() / "Code" / "uCore" +DB_PATH = settings.udos_home / "history" / "actions.db" +UCORE_ROOT = settings.udos_root / "uCore" SCHEMA = """ CREATE TABLE IF NOT EXISTS actions ( diff --git a/backend/app/services/knowledge_layer.py b/backend/app/services/knowledge_layer.py index 5b43f53c..b0927b73 100644 --- a/backend/app/services/knowledge_layer.py +++ b/backend/app/services/knowledge_layer.py @@ -22,9 +22,11 @@ from pathlib import Path from typing import Any +from app.core.settings import settings + log = logging.getLogger("ucore.knowledge_layer") -DB_PATH = Path.home() / ".ucore" / "knowledge" / "shared.db" +DB_PATH = settings.udos_home / "knowledge" / "shared.db" class KnowledgeLayer: diff --git a/backend/app/services/library_index.py b/backend/app/services/library_index.py index 232fec06..81084caa 100644 --- a/backend/app/services/library_index.py +++ b/backend/app/services/library_index.py @@ -1,5 +1,5 @@ """Unified Library Index — consolidates all vault sources into a -single searchable index at ~/.ucore/indices/. +single searchable index under ``$UDOS_HOME/indices``. Vault topology (3 types, see backend/app/api/vault_api.py): user -> ~/Vault/ (personal vault) @@ -8,7 +8,7 @@ Note: ~/Code/ is NOT a vault — it is the Developer Lane codebase. -The index is stored as SQLite at ~/.ucore/indices/library.db with +The index is stored as SQLite at ``$UDOS_HOME/indices/library.db`` with FTS5 for full-text search across all sources. """ from __future__ import annotations @@ -21,9 +21,11 @@ from pathlib import Path from typing import Any +from app.core.settings import settings + log = logging.getLogger("ucore.library_index") -INDEX_DIR = Path.home() / ".ucore" / "indices" +INDEX_DIR = settings.udos_home / "indices" INDEX_DB = INDEX_DIR / "library.db" VAULT_PATHS = { @@ -43,7 +45,7 @@ } # Additional workspaces (user-registered vaults/folders) --------------- -WORKSPACES_FILE = Path.home() / ".ucore" / "workspaces.json" +WORKSPACES_FILE = settings.udos_home / "workspaces.json" # Workspaces may only be selected from the shared/public vault roots. WORKSPACE_ROOTS = [Path.home() / "Shared", Path.home() / "Public"] diff --git a/backend/app/services/provider_router.py b/backend/app/services/provider_router.py index 7b2e71b0..9463c581 100644 --- a/backend/app/services/provider_router.py +++ b/backend/app/services/provider_router.py @@ -16,6 +16,8 @@ from pathlib import Path from typing import Any +from app.core.settings import settings + log = logging.getLogger("snackbar.provider_router") _env_loaded = False @@ -43,8 +45,8 @@ def _load_env_file(path: Path) -> None: # Load env files on import _env_paths = [ - Path.home() / ".ucore" / ".env", - Path.home() / ".ucore" / "config" / "hivemind.env", + settings.udos_home / ".env", + settings.config_dir / "hivemind.env", ] for _ep in _env_paths: _load_env_file(_ep) diff --git a/backend/app/services/quality_scorer.py b/backend/app/services/quality_scorer.py index d1dd9922..7140600b 100644 --- a/backend/app/services/quality_scorer.py +++ b/backend/app/services/quality_scorer.py @@ -6,7 +6,7 @@ - Output quality (heuristic: length, structure, code blocks) - Task completion (did the response address the prompt?) -Stores results in SQLite at ~/.ucore/indices/quality.db for historical analysis. +Stores results in SQLite at ``$UDOS_HOME/indices/quality.db``. Usage: scorer = QualityScorer.get() @@ -30,9 +30,11 @@ from pathlib import Path from typing import Any +from app.core.settings import settings + log = logging.getLogger("ucore.quality_scorer") -DB_PATH = Path.home() / ".ucore" / "indices" / "quality.db" +DB_PATH = settings.udos_home / "indices" / "quality.db" SCHEMA_SQL = """ CREATE TABLE IF NOT EXISTS quality_log ( diff --git a/backend/app/services/research_queue.py b/backend/app/services/research_queue.py index 169678df..ce7e4a2d 100644 --- a/backend/app/services/research_queue.py +++ b/backend/app/services/research_queue.py @@ -1,7 +1,7 @@ """Research Job Queue — SQLite-backed async pipeline for BrowserUI research. Orchestrates: scrape URL → summarise via ChatUI → save to binder. -Jobs tracked in ~/.ucore/indices/research_queue.db. +Jobs tracked in ``$UDOS_HOME/indices/research_queue.db``. """ from __future__ import annotations @@ -13,9 +13,11 @@ from pathlib import Path from typing import Any +from app.core.settings import settings + log = logging.getLogger("ucore.research_queue") -DB_PATH = Path.home() / ".ucore" / "indices" / "research_queue.db" +DB_PATH = settings.udos_home / "indices" / "research_queue.db" SCHEMA = """ CREATE TABLE IF NOT EXISTS jobs ( @@ -100,11 +102,14 @@ async def update_state(self, job_id, state, progress=0, result=None, error=None) updates = ["state = ?", "progress = ?", "started = COALESCE(started, ?)"] params: list = [state, progress, now] if result is not None: - updates.append("result = ?"); params.append(result) + updates.append("result = ?") + params.append(result) if error is not None: - updates.append("error = ?"); params.append(error) + updates.append("error = ?") + params.append(error) if state in ("completed", "failed"): - updates.append("completed = ?"); params.append(now) + updates.append("completed = ?") + params.append(now) params.append(job_id) self._conn.execute( f"UPDATE jobs SET {', '.join(updates)} WHERE id = ?", params @@ -115,9 +120,14 @@ async def list_jobs(self, state=None, binder=None, limit=50): """List jobs, optionally filtered.""" query = "SELECT * FROM jobs WHERE 1=1" params: list = [] - if state: query += " AND state = ?"; params.append(state) - if binder: query += " AND binder = ?"; params.append(binder) - query += " ORDER BY created DESC LIMIT ?"; params.append(limit) + if state: + query += " AND state = ?" + params.append(state) + if binder: + query += " AND binder = ?" + params.append(binder) + query += " ORDER BY created DESC LIMIT ?" + params.append(limit) return [ResearchJob(r).to_dict() for r in self._conn.execute(query, params).fetchall()] diff --git a/backend/app/services/spool_reader.py b/backend/app/services/spool_reader.py index 1288c669..bf12cbbf 100644 --- a/backend/app/services/spool_reader.py +++ b/backend/app/services/spool_reader.py @@ -1,6 +1,6 @@ """spool_reader — Unified activity feed reader for uCore logs. -Reads from ~/.ucore/logs/*.log and parses structured log entries into +Reads from ``$UDOS_HOME/logs/*.log`` and parses structured log entries into a queryable activity feed. Supports real-time watching, filtering, and search for the clipboard popover Logs tab and brain_sync synthesis. @@ -14,9 +14,10 @@ from pathlib import Path from typing import Any +from app.core.settings import settings from app.services.identity import get_full_identity as _get_identity -LOG_DIR = Path.home() / ".ucore" / "logs" +LOG_DIR = settings.logs_dir LOG_PATTERNS = ("*.log",) # Identity cache — refreshed once per session diff --git a/backend/app/services/spool_writer.py b/backend/app/services/spool_writer.py index 88b8087c..f8f5dfe1 100644 --- a/backend/app/services/spool_writer.py +++ b/backend/app/services/spool_writer.py @@ -8,9 +8,10 @@ from __future__ import annotations from datetime import UTC, datetime -from pathlib import Path -LOG_DIR = Path.home() / ".ucore" / "logs" +from app.core.settings import settings + +LOG_DIR = settings.logs_dir def write_spool( diff --git a/backend/app/services/tasker_bridge.py b/backend/app/services/tasker_bridge.py index 0e0bd306..b7638c6f 100644 --- a/backend/app/services/tasker_bridge.py +++ b/backend/app/services/tasker_bridge.py @@ -1,278 +1,21 @@ -from __future__ import annotations - -import re -from datetime import UTC, datetime -from pathlib import Path -from typing import Any - -import yaml - -STATUS_ALIASES: dict[str, str] = { - "todo": "todo", - "to-do": "todo", - "open": "todo", - "pending": "todo", - "in-progress": "in-progress", - "inprogress": "in-progress", - "wip": "in-progress", - "review": "review", - "blocked": "blocked", - "done": "completed", - "complete": "completed", - "completed": "completed", -} - -PRIORITY_ALIASES: dict[str, str] = { - "high": "high", - "h": "high", - "urgent": "high", - "medium": "medium", - "med": "medium", - "normal": "medium", - "low": "low", - "l": "low", -} - - -def normalize_status(value: str) -> str: - key = str(value or "").strip().lower() - if not key: - return "todo" - return STATUS_ALIASES.get(key, key) - - -def normalize_priority(value: str) -> str: - raw = str(value or "").strip() - key = raw.lower() - if not key: - return "medium" - return PRIORITY_ALIASES.get(key, raw) - - -def normalize_tags(value: Any) -> list[str]: - if isinstance(value, list): - raw = [str(v).strip() for v in value if str(v).strip()] - elif isinstance(value, str): - raw = [part.strip() for part in value.split(",") if part.strip()] - else: - raw = [] - - deduped: list[str] = [] - seen: set[str] = set() - for item in raw: - lowered = item.lower() - if lowered in seen: - continue - seen.add(lowered) - deduped.append(item) - return deduped - - -def pick_alias(mapping: dict[str, Any], *keys: str) -> str: - for key in keys: - val = mapping.get(key) - if val is None: - continue - text = str(val).strip() - if text: - return text - return "" - - -def slugify(text: str) -> str: - cleaned = re.sub(r"[^a-zA-Z0-9]+", "-", text.strip().lower()) - cleaned = cleaned.strip("-") - return cleaned or "task" - - -def render_task_markdown( - *, - title: str, - source: str, - source_id: str, - status: str, - body: str, - metadata: dict[str, Any], - created_at: str | None = None, -) -> str: - """Render a task as Obsidian-compatible markdown. - - Format: YAML frontmatter (Properties) + `# Title` + `## Summary` body. - Obsidian reads the frontmatter natively (properties panel) and scans the - body for `- [ ]` task checkboxes, so tasker notes open/query in Obsidian. - """ - timestamp = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ") - mission = pick_alias(metadata, "mission", "project", "objective") - task_name = pick_alias(metadata, "task", "todo", "work_item") - binder = pick_alias(metadata, "binder", "notebook", "collection") - priority = normalize_priority( - pick_alias(metadata, "priority", "prio", "urgency"), - ) - tags = normalize_tags( - metadata.get("tags") or metadata.get("labels") or metadata.get("tag"), - ) - - frontmatter: dict[str, Any] = { - "id": metadata.get("id") or source_id or slugify(title), - "board": metadata.get("board") or "", - "status": normalize_status(status), - "priority": priority, - "source": source, - "source_id": source_id, - "created": created_at or metadata.get("created") or timestamp, - "updated": timestamp, - } - if mission: - frontmatter["mission"] = mission - if task_name: - frontmatter["task"] = task_name - if binder: - frontmatter["binder"] = binder - if tags: - frontmatter["tags"] = ", ".join(tags) - if metadata.get("due"): - frontmatter["due"] = metadata.get("due") - - extra_meta = { - key: value - for key, value in metadata.items() - if key - not in { - "title", - "name", - "status", - "source", - "source_id", - "synced_at", - "priority", - "mission", - "task", - "binder", - "tags", - "labels", - "tag", - "assignee", - "due", - "dueDate", - "id", - "uuid", - "description", - "notes", - "board", - "created", - "created_at", - "updated", - } - } - if extra_meta: - for key, value in extra_meta.items(): - if value not in (None, ""): - frontmatter[key] = value - - # Drop empty optional fields for a clean properties panel - frontmatter = { - key: value for key, value in frontmatter.items() if value not in (None, "") - } - - try: - fm_text = yaml.safe_dump( - frontmatter, - sort_keys=False, - allow_unicode=True, - ).strip() - except Exception: - fm_text = "\n".join( - f"{key}: {value}" for key, value in frontmatter.items() - ) - - lines = ["---", fm_text, "---", "", f"# {title}", ""] - if body: - lines.extend(["## Summary", body, ""]) - return "\n".join(lines) - - -def export_rows_to_tasker( - rows: list[dict[str, Any]], - *, - tasker_dir: str, - board: str = "inbox", - title_field: str = "title", - body_fields: list[str] | None = None, - status_field: str = "status", - id_field: str = "id", - source: str = "local-db", - dry_run: bool = False, -) -> dict[str, Any]: - body_fields = body_fields or ["description", "notes", "content"] - out_dir = Path(tasker_dir).expanduser() / board - exported: list[dict[str, Any]] = [] - - if not dry_run: - out_dir.mkdir(parents=True, exist_ok=True) - - for index, row in enumerate(rows, start=1): - title = str( - row.get(title_field) or row.get("name") or f"Task {index}", - ).strip() - source_id = str(row.get(id_field) or row.get("uuid") or index) - status = normalize_status(str(row.get(status_field) or "todo")) - - metadata = dict(row) - metadata["priority"] = normalize_priority( - pick_alias(row, "priority", "prio", "urgency"), - ) - metadata["mission"] = pick_alias( - row, - "mission", - "project", - "objective", - ) - metadata["task"] = pick_alias(row, "task", "todo", "work_item") - metadata["binder"] = pick_alias( - row, - "binder", - "notebook", - "collection", - ) - metadata["tags"] = normalize_tags( - row.get("tags") or row.get("labels") or row.get("tag"), - ) - - body_parts: list[str] = [] - for field in body_fields: - value = row.get(field) - if value is None: - continue - text = str(value).strip() - if text: - body_parts.append(f"{field}: {text}") - body = "\n\n".join(body_parts) - - filename = f"{status}-{slugify(title)}-{slugify(source_id)}.md" - path = out_dir / filename - content = render_task_markdown( - title=title, - source=source, - source_id=source_id, - status=status, - body=body, - metadata=metadata, - ) - if not dry_run: - path.write_text(content, encoding="utf-8") - exported.append( - { - "title": title, - "status": status, - "source_id": source_id, - "file": str(path), - }, - ) - - return { - "tasker_dir": str(Path(tasker_dir).expanduser()), - "board": board, - "count": len(exported), - "exports": exported, - "dry_run": dry_run, - } +"""Compatibility exports for uFlow-owned Markdown task primitives.""" + +from uflow.task_store import ( + export_rows_to_tasker, + normalize_priority, + normalize_status, + normalize_tags, + pick_alias, + render_task_markdown, + slugify, +) + +__all__ = [ + "export_rows_to_tasker", + "normalize_priority", + "normalize_status", + "normalize_tags", + "pick_alias", + "render_task_markdown", + "slugify", +] diff --git a/backend/app/services/tasker_ops.py b/backend/app/services/tasker_ops.py index d43e7158..92f5f91f 100644 --- a/backend/app/services/tasker_ops.py +++ b/backend/app/services/tasker_ops.py @@ -3,8 +3,12 @@ from pathlib import Path from typing import Any -from app.services.tasker_bridge import render_task_markdown, slugify -from app.services.workflow_status import default_tasker_dir, scan_tasker_boards +from uflow.task_store import ( + default_tasker_dir, + render_task_markdown, + scan_tasker_boards, + slugify, +) def resolve_tasker_dir(tasker_dir: str | None = None) -> Path: diff --git a/backend/app/services/template_manager.py b/backend/app/services/template_manager.py index 56c11389..a666d776 100644 --- a/backend/app/services/template_manager.py +++ b/backend/app/services/template_manager.py @@ -1,8 +1,7 @@ """Slate — Dev Mode recovery template system (renamed from Plate). ->>>>>>> renamed: Plate → Slate across the uCore ecosystem. ->>>>>>> Slate now hosts versioned, verifiable Dev Mode templates ->>>>>>> organized in four tiers: default, stable, experimental, custom. +Slate hosts versioned, verifiable Dev Mode templates organized in four tiers: +default, stable, experimental, and custom. Manages application state templates stored in ~/.ucode/templates/ across four tiers: default, stable, experimental, custom. @@ -23,9 +22,11 @@ from pathlib import Path from typing import Any +from app.core.settings import settings + log = logging.getLogger("ucore.services.template_manager") -TEMPLATE_DIR = Path.home() / ".ucore" / "templates" +TEMPLATE_DIR = settings.udos_home / "templates" TIERS = ["default", "stable", "experimental", "custom"] diff --git a/backend/app/services/workflow_status.py b/backend/app/services/workflow_status.py index 4335340a..f3d16a49 100644 --- a/backend/app/services/workflow_status.py +++ b/backend/app/services/workflow_status.py @@ -1,44 +1,10 @@ +"""uCore presentation of status from the uFlow-owned task store.""" + from __future__ import annotations -import os -from pathlib import Path from typing import Any -from app.core.settings import settings - - -def default_tasker_dir() -> Path: - return Path( - os.getenv( - "UCORE_TASKER_DIR", - str(settings.udos_root / "uCore/.tasker"), - ), - ).expanduser() - - -def scan_tasker_boards(tasker_dir: Path | None = None) -> dict[str, Any]: - base = tasker_dir or default_tasker_dir() - boards: list[dict[str, Any]] = [] - - if base.exists(): - for board_dir in sorted(p for p in base.iterdir() if p.is_dir()): - files = sorted(board_dir.glob("*.md")) - boards.append( - { - "name": board_dir.name, - "path": str(board_dir), - "count": len(files), - "items": [f.name for f in files[:10]], - }, - ) - - return { - "tasker_dir": str(base), - "exists": base.exists(), - "boards": boards, - "count": len(boards), - "total_items": sum(board["count"] for board in boards), - } +from uflow.task_store import default_tasker_dir, scan_tasker_boards def build_workflow_status( @@ -56,33 +22,23 @@ def build_workflow_status( return { "engine": { - "name": "Cline Kanban", - "role": "Primary Developer workflow engine", - "command": "npx kanban", - "bind": "127.0.0.1:3484", - "access": "localhost-only", - "isolation": "ephemeral git worktrees per card", - "review_loop": "diff + inline feedback per task", + "name": "uFlow Markdown Workflow Engine", + "role": "Canonical user, developer, system, and autonomous workflow state", + "storage": str(default_tasker_dir()), + "access": "uCore API and vault-compatible Markdown", + "isolation": "workflow type, workspace, mission, task, and step", + "review_loop": "task evidence, artifact links, approval, and outcome", "automation": [ - "linked cards", - "auto-commit", - "auto-pr", + "uFlow task transitions", + "budget-gated execution", + "reviewable developer changes", ], }, "guardrails": [ - "Keep the Kanban server bound to localhost only.", - ( - "Do not expose via public host, tunnel, or 0.0.0.0 in " - "the default dev workflow." - ), - ( - "Use ephemeral worktrees to isolate agent changes from " - "the main workspace." - ), - ( - "Prefer SSH tunnel or Tailscale only if remote access is " - "ever required." - ), + "uFlow is the sole durable task and workflow authority.", + "Do not create repository-local or agent-owned task stores.", + "Require explicit authorization for destructive or external actions.", + "Record budget, evidence, artifacts, and outcome on durable tasks.", ], "task_markdown": tasker, "maintenance": { @@ -92,11 +48,11 @@ def build_workflow_status( "endpoint": "/api/system/maintenance", }, "next_actions": [ - "Expose .tasker board actions in the Workflow Builder UI.", - "Add sync controls for tasker_sync and vault_sync.", - ( - "Integrate richer agent orchestration only after the local " - "workflow substrate is stable." - ), + "Expose uFlow board actions in the Workflow surface.", + "Add sync controls for task and vault sync.", + "Route models through HiveMind and the budget manager.", ], } + + +__all__ = ["build_workflow_status", "default_tasker_dir", "scan_tasker_boards"] diff --git a/backend/app/skills/builtin/attach_context.py b/backend/app/skills/builtin/attach_context.py index fec6ede0..bb506436 100644 --- a/backend/app/skills/builtin/attach_context.py +++ b/backend/app/skills/builtin/attach_context.py @@ -1,12 +1,13 @@ """attach_context — Inject CONTEXT.md into AI sessions. Reads the project CONTEXT.md and returns it as a system prompt -or context block for injection into Continue/Cline sessions. +or context block for injection into external agent sessions. Usage: POST /api/skills/attach_context/run Body: { "project": "uCore" } — optional, defaults to uCore """ + from __future__ import annotations from app.core.settings import settings @@ -43,20 +44,14 @@ class AttachContext(BaseSkill): type="string", required=False, default="system_prompt", - description=( - "Output format: system_prompt, raw, " - "or markdown_block" - ), + description=("Output format: system_prompt, raw, or markdown_block"), ), SkillParam( name="include_wisdom", type="boolean", required=False, default=True, - description=( - "Include private project wisdom alongside CONTEXT.md " - "when available" - ), + description=("Include private project wisdom alongside CONTEXT.md when available"), ), ], ) @@ -65,22 +60,13 @@ async def run(self, **kwargs) -> dict: project = kwargs.get("project", "ucore").lower().strip() fmt = kwargs.get("format", "system_prompt").strip() include_wisdom = bool(kwargs.get("include_wisdom", True)) - context_path = ( - PROJECT_CONTEXT_FILES.get(project) - or PROJECT_CONTEXT_FILES["default"] - ) - wisdom_path = ( - PROJECT_WISDOM_FILES.get(project) - or PROJECT_WISDOM_FILES["default"] - ) + context_path = PROJECT_CONTEXT_FILES.get(project) or PROJECT_CONTEXT_FILES["default"] + wisdom_path = PROJECT_WISDOM_FILES.get(project) or PROJECT_WISDOM_FILES["default"] if not context_path.exists(): return { "success": False, - "error": ( - f"CONTEXT.md not found for '{project}' " - f"at {context_path}" - ), + "error": (f"CONTEXT.md not found for '{project}' at {context_path}"), } context_text = context_path.read_text(encoding="utf-8") @@ -90,10 +76,7 @@ async def run(self, **kwargs) -> dict: combined_text = context_text if wisdom_text: - combined_text = ( - f"{context_text}\n\n---\n\n" - f"# Project Wisdom\n\n{wisdom_text}" - ) + combined_text = f"{context_text}\n\n---\n\n# Project Wisdom\n\n{wisdom_text}" if fmt == "raw": return { @@ -129,7 +112,6 @@ async def run(self, **kwargs) -> dict: "length": len(system_prompt), "has_wisdom": wisdom_text is not None, "instructions": ( - "Prepend this system prompt to the current AI " - "session for full project context." + "Prepend this system prompt to the current AI session for full project context." ), } diff --git a/backend/app/skills/builtin/backup.py b/backend/app/skills/builtin/backup.py index afce6f2c..8bc26656 100644 --- a/backend/app/skills/builtin/backup.py +++ b/backend/app/skills/builtin/backup.py @@ -7,10 +7,11 @@ POST /api/skills/backup/run Body: { "type": "full" | "database" | "config" | "secrets" | "wisdom", - "destination": "~/.ucore/backups", + "destination": "$UDOS_HOME/backups", "retention_days": 14 } """ + from __future__ import annotations import logging @@ -50,7 +51,7 @@ class BackupData(BaseSkill): name="destination", type="string", required=False, - default="~/.ucore/backups", + default=str(settings.udos_home / "backups"), ), SkillParam( name="retention_days", @@ -65,7 +66,9 @@ class BackupData(BaseSkill): async def run(self, **kwargs) -> dict: backup_type = kwargs.get("type", "full").strip().lower() - dest = Path(kwargs.get("destination", "~/.ucore/backups")).expanduser() + dest = Path( + kwargs.get("destination", str(settings.udos_home / "backups")), + ).expanduser() dest.mkdir(parents=True, exist_ok=True) retention_days = kwargs.get("retention_days", RETENTION_DAYS) ts = time.strftime("%Y%m%d-%H%M%S") @@ -117,6 +120,7 @@ async def run(self, **kwargs) -> dict: def _backup_database(self, dest: Path, ts: str) -> dict | None: """Backup the SQLite database.""" from app.core.database import get_db_path + db_path = get_db_path() if db_path and Path(db_path).exists(): backup_file = dest / f"ucore-backup-{ts}.db" diff --git a/backend/app/skills/builtin/brain_sync.py b/backend/app/skills/builtin/brain_sync.py index fd5a0fb2..226df485 100644 --- a/backend/app/skills/builtin/brain_sync.py +++ b/backend/app/skills/builtin/brain_sync.py @@ -4,22 +4,15 @@ spool logs, and vault activity, then refreshing private wisdom with durable lessons, recent change summaries, and spool activity analysis. -Also provides tasker/devlog bridge actions (sync, read, write, archive, purge) -merged from the former tasker_devlog_bridge skill. - Usage: POST /api/skills/brain_sync/run Body: { - "action": "sync" | "read" | "write" | "archive" | "purge" | "summarize", + "action": "summarize" | "purge", "hours": 24, "limit": 12, "include_spool": true, "include_vault_activity": true, "include_test_failures": true, - "tasker_dir": ".tasker", - "devlog_file": "devlog.mcp.yaml", - "content": "", - "max_age_days": 7, "dry_run": false } """ @@ -41,9 +34,7 @@ from app.skills.base import BaseSkill, SkillMeta, SkillParam WISDOM_PATH = writable_wisdom_path() -DEFAULT_SCAN_DIRS = ("backend", "docs", "frontend", "scripts", ".tasker") -DEFAULT_TASKER_DIR = PROJECT_ROOT / ".tasker" -DEFAULT_DEVLOG_FILE = PROJECT_ROOT / "devlog.mcp.yaml" +DEFAULT_SCAN_DIRS = ("backend", "docs", "frontend", "scripts") TEST_REPORT_PATTERNS = ( "**/junit*.xml", "**/pytest*.xml", @@ -61,8 +52,7 @@ class BrainSync(BaseSkill): name="Brain Sync", description=( "Synthesize recent project changes, spool activity, and " - "vault changes into private wisdom. Also provides " - "tasker/devlog bridge actions (sync, read, write, archive, purge)." + "vault changes into private wisdom." ), category="assist", timeout=30, @@ -72,7 +62,7 @@ class BrainSync(BaseSkill): type="string", required=False, default="summarize", - description="Action: summarize, sync, read, write, archive, purge", + description="Action: summarize or purge", ), SkillParam( name="hours", @@ -122,27 +112,6 @@ class BrainSync(BaseSkill): "Include recent episodic log entries in private wisdom" ), ), - SkillParam( - name="tasker_dir", - type="string", - required=False, - default=str(DEFAULT_TASKER_DIR), - description="Path to .tasker directory (for sync/read/write/archive)", - ), - SkillParam( - name="devlog_file", - type="string", - required=False, - default=str(DEFAULT_DEVLOG_FILE), - description="Path to devlog.mcp.yaml (for sync/read/write)", - ), - SkillParam( - name="content", - type="string", - required=False, - default="", - description="Content to write to devlog (for write action)", - ), SkillParam( name="max_age_days", type="integer", @@ -164,15 +133,7 @@ class BrainSync(BaseSkill): async def run(self, **kwargs) -> dict: action = str(kwargs.get("action", "summarize")).strip().lower() - if action == "sync": - return await self._sync_tasker_devlog(**kwargs) - elif action == "read": - return self._read_tasker_devlog(**kwargs) - elif action == "write": - return self._write_devlog(**kwargs) - elif action == "archive": - return self._archive_old_tasks(**kwargs) - elif action == "purge": + if action == "purge": return self._purge_legacy_docs(**kwargs) # Default: summarize (original brain_sync behavior) @@ -241,125 +202,6 @@ async def _summarize(self, **kwargs) -> dict: "episodic_included": episodic_summary is not None, } - # ─── Tasker/Devlog Bridge Actions (merged from tasker_devlog_bridge) ── - - async def _sync_tasker_devlog(self, **kwargs) -> dict: - """Sync .tasker with devlog.mcp.yaml and spool activity.""" - tasker_dir = Path(kwargs.get("tasker_dir", DEFAULT_TASKER_DIR)).expanduser() - devlog_file = Path(kwargs.get("devlog_file", DEFAULT_DEVLOG_FILE)).expanduser() - hours = int(kwargs.get("hours", 24)) - dry_run = bool(kwargs.get("dry_run", False)) - - completed_tasks = self._collect_completed_tasks(tasker_dir) - cutoff = (datetime.now(UTC) - timedelta(hours=hours)).isoformat() - spool_entries = read_spool(since=cutoff) - - devlog_content = self._render_devlog( - completed_tasks=completed_tasks, - spool_entries=spool_entries, - hours=hours, - ) - - if not dry_run: - devlog_file.parent.mkdir(parents=True, exist_ok=True) - devlog_file.write_text(devlog_content, encoding="utf-8") - - return { - "success": True, - "action": "sync", - "devlog_path": str(devlog_file), - "completed_tasks": len(completed_tasks), - "spool_entries": len(spool_entries), - "hours": hours, - "dry_run": dry_run, - } - - def _read_tasker_devlog(self, **kwargs) -> dict: - """Read current state of tasker and devlog.""" - tasker_dir = Path(kwargs.get("tasker_dir", DEFAULT_TASKER_DIR)).expanduser() - devlog_file = Path(kwargs.get("devlog_file", DEFAULT_DEVLOG_FILE)).expanduser() - - tasks = [] - if tasker_dir.exists(): - for task_file in tasker_dir.rglob("*.md"): - if task_file.name == "README.md": - continue - try: - content = task_file.read_text(encoding="utf-8") - tasks.append({ - "path": str(task_file.relative_to(tasker_dir.parent)), - "content": content[:500], - }) - except Exception: - continue - - devlog_content = "" - if devlog_file.exists(): - devlog_content = devlog_file.read_text(encoding="utf-8") - - return { - "success": True, - "action": "read", - "tasks": tasks, - "devlog_preview": devlog_content[:1000] if devlog_content else None, - } - - def _write_devlog(self, **kwargs) -> dict: - """Write content to devlog.mcp.yaml.""" - devlog_file = Path(kwargs.get("devlog_file", DEFAULT_DEVLOG_FILE)).expanduser() - content = kwargs.get("content", "") - dry_run = bool(kwargs.get("dry_run", False)) - - if not dry_run: - devlog_file.parent.mkdir(parents=True, exist_ok=True) - devlog_file.write_text(content, encoding="utf-8") - - return { - "success": True, - "action": "write", - "devlog_path": str(devlog_file), - "dry_run": dry_run, - } - - def _archive_old_tasks(self, **kwargs) -> dict: - """Archive completed tasks older than max_age_days.""" - tasker_dir = Path(kwargs.get("tasker_dir", DEFAULT_TASKER_DIR)).expanduser() - max_age_days = int(kwargs.get("max_age_days", 7)) - dry_run = bool(kwargs.get("dry_run", False)) - - archived_dir = tasker_dir.parent / ".tasker.archived" - archived_count = 0 - - if not tasker_dir.exists(): - return {"success": True, "action": "archive", "archived": 0, "dry_run": dry_run} - - cutoff = datetime.now(UTC) - timedelta(days=max_age_days) - - for task_file in tasker_dir.rglob("*.md"): - if task_file.name == "README.md": - continue - try: - content = task_file.read_text(encoding="utf-8") - if "status: done" in content or "status: completed" in content: - mtime = datetime.fromtimestamp(task_file.stat().st_mtime, tz=UTC) - if mtime < cutoff: - relative = task_file.relative_to(tasker_dir) - dest = archived_dir / relative - if not dry_run: - dest.parent.mkdir(parents=True, exist_ok=True) - dest.write_text(content, encoding="utf-8") - task_file.unlink(missing_ok=True) - archived_count += 1 - except Exception: - continue - - return { - "success": True, - "action": "archive", - "archived": archived_count, - "dry_run": dry_run, - } - def _purge_legacy_docs(self, **kwargs) -> dict: """Purge legacy completed documentation reports.""" docs_dir = Path(kwargs.get("docs_dir", PROJECT_ROOT / "docs")) @@ -391,77 +233,6 @@ def _purge_legacy_docs(self, **kwargs) -> dict: "dry_run": dry_run, } - def _collect_completed_tasks(self, tasker_dir: Path) -> list[dict[str, Any]]: - """Collect completed tasks from .tasker directory.""" - tasks = [] - if not tasker_dir.exists(): - return tasks - - for task_file in tasker_dir.rglob("*.md"): - if task_file.name == "README.md": - continue - try: - content = task_file.read_text(encoding="utf-8") - if "status: done" in content or "status: completed" in content: - task_info = self._parse_task_file(task_file, content) - tasks.append(task_info) - except Exception: - continue - - return tasks - - def _parse_task_file(self, path: Path, content: str) -> dict[str, Any]: - """Parse task file to extract metadata.""" - lines = content.split("\n") - title = lines[0].replace("#", "").strip() if lines else path.stem - - status = "done" - for line in lines[:20]: - if line.startswith("- status:"): - status = line.split(":", 1)[1].strip() - break - - return { - "file": str(path.relative_to(path.parent.parent)), - "archived": True, - "title": title, - "status": status, - } - - def _render_devlog( - self, - completed_tasks: list[dict[str, Any]], - spool_entries: list[dict[str, Any]], - hours: int, - ) -> str: - """Render MCP-formatted devlog.""" - lines = [ - f"# Devlog MCP — Generated: {datetime.now(UTC).isoformat()}", - "", - 'version: "1.0.0"', - 'generated_by: "brain_sync"', - f"hours: {hours}", - f"completed_tasks: {len(completed_tasks)}", - f"spool_entries: {len(spool_entries)}", - "", - "## Completed Tasks", - ] - - for task in completed_tasks: - lines.append(f"- file: {task['file']}") - lines.append(f" archived: {task['archived']}") - lines.append("") - - lines.append("## Spool Activity") - for entry in spool_entries[-50:]: - lines.append(f"- timestamp: {entry.get('timestamp', 'unknown')}") - lines.append(f" level: {entry.get('level', 'INFO')}") - lines.append(f" module: {entry.get('module', 'unknown')}") - lines.append(f" message: {entry.get('message', '')}") - lines.append("") - - return "\n".join(lines) - def _collect_recent_files( self, cutoff: datetime, diff --git a/backend/app/skills/builtin/route_task.py b/backend/app/skills/builtin/route_task.py index 66d5b9d2..e17e0d56 100644 --- a/backend/app/skills/builtin/route_task.py +++ b/backend/app/skills/builtin/route_task.py @@ -94,7 +94,7 @@ class RouteTask(BaseSkill): "Target agent: 'auto' (routing matrix), " "'architect', 'dev', 'reviewer', 'debugger', " "'docgen', 'gridsmith-dev', 'hivemind', " - "'roundtable', 'cline', 'ollama', 'openrouter'" + "'roundtable', 'hivemind', 'ollama', 'openrouter'" ), ), ], @@ -261,7 +261,7 @@ async def run(self, **kwargs) -> dict: } # ── Explicit agent dispatch ────────────────────────────────── - # When target_agent is a named executor (cline, hivemind, + # When target_agent is a named executor (hivemind, # roundtable, ollama, openrouter), bypass the complexity matrix # and route directly to the requested agent. explicit_routing = self._route_by_target(target_agent, task, execute) @@ -337,19 +337,6 @@ def _route_by_target( # ── Agent routing table ──────────────────────────────────── agent_map: dict[str, dict] = { - "cline": { - "agent": "cline", - "provider": "cline", - "model": "deepseek-via-cline", - "cost": "DeepSeek credits (via Cline Pass)", - "reason": ( - "Explicit Cline dispatch —" - " premium autonomous executor" - ), - "tokens_per_second": "~60", - "skill_id": "cline-invoke", - "mode": "yolo", - }, "hivemind": { "agent": "hivemind", "provider": "hivemind", diff --git a/backend/app/skills/builtin/skill_autostart.py b/backend/app/skills/builtin/skill_autostart.py index 15b8d2e6..004b2369 100644 --- a/backend/app/skills/builtin/skill_autostart.py +++ b/backend/app/skills/builtin/skill_autostart.py @@ -6,6 +6,7 @@ - Auto-start services if not running - Integration with health API and MCP """ + from __future__ import annotations import json @@ -17,12 +18,16 @@ 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(Path.home() / "Code" / "uCore" / "backend")) +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" @@ -37,9 +42,7 @@ def _api_get(path: str, timeout: float = 3.0) -> dict | None: return None -def _api_post_json( - path: str, payload: dict | None = None, timeout: float = 6.0 -) -> dict | 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") @@ -63,7 +66,7 @@ def is_backend_alive() -> bool: def is_menu_running() -> bool: """Check if uCore menu is running.""" - lockfile = Path.home() / ".ucore" / "ucore-menu.pid" + lockfile = settings.udos_home / "ucore-menu.pid" if not lockfile.exists(): return False try: @@ -102,9 +105,7 @@ 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" - ) + python_bin = str(venv_python) if venv_python.exists() else "/usr/bin/python3" subprocess.Popen( [python_bin, "-m", "app", "--port", "8484"], @@ -124,9 +125,7 @@ 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" - ) + python_bin = str(venv_python) if venv_python.exists() else "/usr/bin/python3" subprocess.Popen( [python_bin, "-m", "app.menu.unified_menu_simple"], @@ -194,10 +193,10 @@ def run_health_check() -> dict[str, Any]: # Determine overall status all_healthy = ( - services["backend"]["running"] and - services["menu"]["running"] and - services["server_plist"]["installed"] and - services["menu_plist"]["installed"] + services["backend"]["running"] + and services["menu"]["running"] + and services["server_plist"]["installed"] + and services["menu_plist"]["installed"] ) return { @@ -215,18 +214,13 @@ def _get_recommendations(services: dict) -> list[str]: if not services["server_plist"]["installed"]: recs.append( - "Install server plist: bash scripts/install_ucore_menu_launchd.sh " - "--install-server" + "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" - ) + recs.append("Install menu plist: bash scripts/install_ucore_menu_launchd.sh") - backend_ok = ( - services["backend"]["running"] or services["backend"]["started"] - ) + backend_ok = services["backend"]["running"] or services["backend"]["started"] if not backend_ok: recs.append("Backend failed to start - check logs") @@ -248,6 +242,7 @@ def _get_recommendations(services: dict) -> list[str]: # ─── Skill Class ───────────────────────────────────────────────────── + class AutoStartSkill(BaseSkill): """Skill for auto-start health checking and service management.""" @@ -269,6 +264,6 @@ async def run(self, **kwargs) -> dict[str, Any]: 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_cline_invoke.py b/backend/app/skills/builtin/skill_cline_invoke.py deleted file mode 100644 index d72b23ed..00000000 --- a/backend/app/skills/builtin/skill_cline_invoke.py +++ /dev/null @@ -1,440 +0,0 @@ -"""Cline Invoke Skill — invoke Cline CLI from uCore skills. - -Invokes Cline CLI (VS Code extension's CLI) from within uCore's skill -ecosystem, allowing Dev Mode to use Cline as an agentic executor for -complex multi-step tasks. - -Modes: - - yolo: autonomous execution with auto-approval on - - interactive: auto-approval off - -Integrates with: Cline CLI, OpenRouter API, gh CLI. -""" -from __future__ import annotations - -import asyncio -import json -import logging -import os -import subprocess -from pathlib import Path - -from app.core.settings import settings -from app.skills.base import BaseSkill, SkillMeta, SkillParam - -log = logging.getLogger("ucore.skills.cline_invoke") - - -def _find_cline_binary() -> str | None: - """Locate the Cline CLI binary.""" - candidates = [ - "cline", - str(Path.home() / ".local" / "bin" / "cline"), - str(Path.home() / ".npm-global" / "bin" / "cline"), - "/usr/local/bin/cline", - ] - for c in candidates: - if Path(c).exists() or _which(c): - return c - return None - - -def _which(cmd: str) -> bool: - """Check if a command is in PATH.""" - try: - result = subprocess.run( - ["which", cmd], - capture_output=True, text=True, timeout=2, check=False, - ) - return result.returncode == 0 - except Exception: - return False - - -def _load_user_vars() -> dict: - """Load uCore user/dev variables from the internal variable store.""" - path = settings.data_dir / "variables.json" - if not path.exists(): - return {} - try: - data = json.loads(path.read_text(encoding="utf-8")) - return data if isinstance(data, dict) else {} - except Exception: - return {} - - -def _resolve_cline_runtime_config(kwargs: dict, mode: str) -> dict[str, str]: - """Resolve provider/model/thinking/approval from internal config first. - - Fail-fast policy: - - provider/model must be explicitly configured in kwargs, variables store, - or UCORE_CLINE_* env vars. - - do not silently hardcode provider/model defaults. - """ - user_vars = _load_user_vars() - - provider = ( - str(kwargs.get("provider", "")).strip() - or str(user_vars.get("cline_provider", "")).strip() - or os.environ.get("UCORE_CLINE_PROVIDER", "").strip() - ) - - model = ( - str(kwargs.get("model", "")).strip() - or str(user_vars.get("cline_model", "")).strip() - or os.environ.get("UCORE_CLINE_MODEL", "").strip() - ) - - thinking = ( - str(kwargs.get("thinking", "")).strip() - or str(user_vars.get("cline_thinking", "")).strip() - or os.environ.get("UCORE_CLINE_THINKING", "").strip() - or "low" - ) - - auto_approve = str( - kwargs.get( - "auto_approve", - user_vars.get( - "cline_auto_approve", - "true" if mode == "yolo" else "false", - ), - ), - ).strip().lower() - if auto_approve not in {"true", "false"}: - auto_approve = "true" if mode == "yolo" else "false" - - return { - "provider": provider, - "model": model, - "thinking": thinking, - "auto_approve": auto_approve, - } - - -def _build_repair_instructions(missing: list[str]) -> list[str]: - """Return actionable repair steps for missing runtime config.""" - steps = [ - "Set values via internal API: PUT /api/variables/user", - "Example payload: {\"cline_provider\": \"ollama\", \"cline_model\": \"qwen2.5-coder:3b\", \"cline_thinking\": \"low\"}", - "Or set env vars: UCORE_CLINE_PROVIDER and UCORE_CLINE_MODEL", - ] - if "api_key" in missing: - steps.append( - "For key-based providers, set API key in Secret Store (/api/secrets) or env (e.g. OPENROUTER_API_KEY)", - ) - return steps - - -class ClineInvokeSkill(BaseSkill): - """Invoke Cline CLI for autonomous task execution.""" - - meta = SkillMeta( - id="cline-invoke", - name="Cline Invoke", - description=( - "Invoke Cline CLI from uCore skills using positional" - " prompt mode. Supports yolo (auto-approve true) and" - " interactive (auto-approve false)." - ), - category="developer", - timeout=300, - params=[ - SkillParam( - name="task", - type="string", - required=True, - description="Task description for Cline to execute", - ), - SkillParam( - name="mode", - type="string", - required=False, - default="interactive", - description="Execution mode: 'yolo' or 'interactive'", - ), - SkillParam( - name="cwd", - type="string", - required=False, - default="", - description="Working directory (default: uCore root)", - ), - SkillParam( - name="timeout", - type="integer", - required=False, - default=120, - description="Max execution time in seconds", - ), - SkillParam( - name="context", - type="string", - required=False, - default="", - description="Additional context for Cline", - ), - SkillParam( - name="provider", - type="string", - required=False, - default="", - description="Override provider (else uCore variables/settings)", - ), - SkillParam( - name="model", - type="string", - required=False, - default="", - description="Override model id (else uCore variables/settings)", - ), - SkillParam( - name="thinking", - type="string", - required=False, - default="", - description="Thinking level override: none|low|medium|high|xhigh", - ), - SkillParam( - name="auto_approve", - type="string", - required=False, - default="", - description="Override auto-approve: true|false", - ), - ], - requires_confirmation=True, - ) - - async def run(self, **kwargs) -> dict: - task = kwargs.get("task", "").strip() - mode = kwargs.get("mode", "interactive").lower() - cwd = kwargs.get("cwd", str(Path.cwd())) - timeout = int(kwargs.get("timeout", 120)) - context = kwargs.get("context", "") - - if not task: - return {"success": False, "error": "task is required"} - - if mode not in ("yolo", "interactive"): - mode = "interactive" - - # Locate Cline CLI - cline_bin = _find_cline_binary() - if not cline_bin: - return { - "success": False, - "error": "Cline CLI not found in PATH", - "fallback": ( - "Install Cline CLI: npm install -g @cline/cli" - " or use roundtable-dispatch instead" - ), - } - - runtime_cfg = _resolve_cline_runtime_config(kwargs, mode) - - missing_cfg: list[str] = [] - if not runtime_cfg["provider"]: - missing_cfg.append("provider") - if not runtime_cfg["model"]: - missing_cfg.append("model") - if missing_cfg: - return { - "success": False, - "error": ( - "Cline runtime config missing required values: " - f"{', '.join(missing_cfg)}" - ), - "repair_required": True, - "missing": missing_cfg, - "repair_steps": _build_repair_instructions(missing_cfg), - } - - # Resolve API key: DEEPSEEK_API_KEY first, then SecretStore, then - # OPENROUTER_API_KEY, then env file fallback - api_key: str = "" - # Priority 1: DEEPSEEK_API_KEY from environment - api_key = os.environ.get("DEEPSEEK_API_KEY", "") - # Priority 2: DEEPSEEK_API_KEY from SecretStore - if not api_key: - try: - from app.secret.store import get_store - store = get_store() - dsk_val = store.get("DEEPSEEK_API_KEY") - if dsk_val: - api_key = dsk_val - except Exception: - pass - # Priority 3: OPENROUTER_API_KEY from environment (fallback) - if not api_key: - api_key = os.environ.get("OPENROUTER_API_KEY", "") - # Priority 4: DEEPSEEK_API_KEY or OPENROUTER_API_KEY from env file - - if not api_key: - try: - config_path = ( - Path.home() / ".config" / "hivemind" / ".env" - ) - if config_path.exists(): - content = config_path.read_text() - for line in content.splitlines(): - if line.startswith("DEEPSEEK_API_KEY="): - api_key = line.split("=", 1)[1].strip().strip('"') - if api_key: - break - if line.startswith("OPENROUTER_API_KEY="): - api_key = line.split("=", 1)[1].strip().strip('"') - if api_key: - break - except Exception: - pass - - # Build command against current Cline CLI interface. - prompt = task - if context: - prompt = f"{task}\n\nContext:\n{context}" - - cmd = [ - cline_bin, - "--json", - "--cwd", cwd, - "-P", runtime_cfg["provider"], - "-m", runtime_cfg["model"], - "--thinking", runtime_cfg["thinking"], - "-t", str(timeout), - "--auto-approve", runtime_cfg["auto_approve"], - ] - - # API key is only required for key-based providers. - needs_key_provider = runtime_cfg["provider"].lower() in { - "openrouter", "openai", "anthropic", "gemini", "groq", "deepseek", "mistral", - } - if needs_key_provider and not api_key: - missing_cfg = ["api_key"] - return { - "success": False, - "error": ( - f"Provider '{runtime_cfg['provider']}' requires an API key, but none was found" - ), - "repair_required": True, - "missing": missing_cfg, - "repair_steps": _build_repair_instructions(missing_cfg), - "provider": runtime_cfg["provider"], - "model": runtime_cfg["model"], - } - if needs_key_provider and api_key: - cmd.extend(["-k", api_key]) - - cmd.append(prompt) - - # Execute - try: - result = await self._run_cline(cmd, cwd, timeout) - if result.get("needs_auth"): - return { - "success": False, - "action": "cline-invoke", - "mode": mode, - "binary": cline_bin, - "error": "Cline requires authentication before headless task execution", - "fallback": "Run: cline auth, then retry cline-invoke", - "output": result.get("output", ""), - "exit_code": result.get("exit_code", -1), - "duration_ms": result.get("duration_ms", 0), - } - - return { - "success": result.get("success", False), - "action": "cline-invoke", - "mode": mode, - "binary": cline_bin, - "output": result.get("output", ""), - "provider": runtime_cfg["provider"], - "model": runtime_cfg["model"], - "exit_code": result.get("exit_code", -1), - "duration_ms": result.get("duration_ms", 0), - } - except asyncio.TimeoutError: - return { - "success": False, - "error": f"Cline task timed out after {timeout}s", - "mode": mode, - "binary": cline_bin, - } - except Exception as exc: - return { - "success": False, - "error": str(exc), - "mode": mode, - "binary": cline_bin, - } - - async def _run_cline( - self, cmd: list[str], cwd: str, timeout: int, - ) -> dict: - """Execute Cline CLI and capture output.""" - import time - t0 = time.perf_counter() - - process = await asyncio.create_subprocess_exec( - *cmd, - cwd=cwd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - env={**os.environ}, - ) - - try: - stdout, stderr = await asyncio.wait_for( - process.communicate(), - timeout=timeout, - ) - except asyncio.TimeoutError: - process.kill() - await process.wait() - raise - - duration = round((time.perf_counter() - t0) * 1000, 1) - output = ( - stdout.decode("utf-8", errors="replace") - if stdout else "" - ) - if stderr: - err_text = stderr.decode("utf-8", errors="replace") - if err_text.strip(): - output += f"\n[stderr]\n{err_text}" - - needs_auth = "requires re-authentication" in output.lower() - - # Cline --json may emit one JSON object per line. - parsed_obj = None - for line in output.splitlines(): - line = line.strip() - if not line or not line.startswith("{"): - continue - try: - obj = json.loads(line) - except (json.JSONDecodeError, ValueError): - continue - if isinstance(obj, dict): - parsed_obj = obj - - if isinstance(parsed_obj, dict): - msg = parsed_obj.get("message") or parsed_obj.get("response") or output - if str(parsed_obj.get("type", "")).lower() == "error" and "requires re-authentication" in str(msg).lower(): - needs_auth = True - return { - "success": process.returncode == 0 and not needs_auth, - "output": msg, - "exit_code": process.returncode, - "duration_ms": duration, - "needs_auth": needs_auth, - } - - return { - "success": process.returncode == 0 and not needs_auth, - "output": output[:5000], - "exit_code": process.returncode, - "duration_ms": duration, - "needs_auth": needs_auth, - } diff --git a/backend/app/skills/builtin/skill_dev_destroy_rebuild.py b/backend/app/skills/builtin/skill_dev_destroy_rebuild.py index 88bafa81..36fcf3d3 100644 --- a/backend/app/skills/builtin/skill_dev_destroy_rebuild.py +++ b/backend/app/skills/builtin/skill_dev_destroy_rebuild.py @@ -10,6 +10,7 @@ Safety: never touches data, only Dev Mode config/state. """ + from __future__ import annotations import json @@ -20,23 +21,24 @@ 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 = Path.home() / ".ucore" / "recovery" -SPOOL_DIR = Path.home() / ".tasker" / "spool" +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": Path.home() / ".ucore" / "config.yaml", + "path": settings.udos_home / "config.yaml", "description": "Dev Mode layer state (mode, surface visibility)", }, "templates": { - "path": Path.home() / ".ucore" / "templates", + "path": settings.udos_home / "templates", "description": "All template snapshots (default, stable, exp, custom)", }, "dev_mode_store": { @@ -45,7 +47,7 @@ }, "spool_wisdom": { "path": WISDOM_DIR, - "description": "Wisdom records in .tasker/spool/wisdom", + "description": "Wisdom records in $UDOS_HOME/spool/wisdom", }, } @@ -186,9 +188,7 @@ def destroy( saved_template = self._template_mgr.create_template( name=template_name, tier="custom", - description=( - f"Auto-saved before DESTROY of {', '.join(targets)}" - ), + description=(f"Auto-saved before DESTROY of {', '.join(targets)}"), tags=["auto-save", "pre-destroy", *targets], state=self._capture_state(targets), ) @@ -215,8 +215,7 @@ def destroy( "destroyed": destroyed, "template_id": saved_template.id if saved_template else None, "message": ( - f"Destroyed {len(destroyed)} component(s). " - f"Use 'rebuild {backup.id}' to restore." + f"Destroyed {len(destroyed)} component(s). Use 'rebuild {backup.id}' to restore." ), } @@ -265,9 +264,7 @@ def rebuild( 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]: + 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: @@ -317,9 +314,7 @@ def _rebuild_from_template( "state": state, } - def _rebuild_defaults( - self, components: list[str] | None - ) -> dict[str, Any]: + 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)) @@ -327,8 +322,7 @@ def _rebuild_defaults( "restored_from": "defaults", "reset": targets, "message": ( - f"Reset {len(targets)} component(s) to defaults. " - "Restart surfaces to apply." + f"Reset {len(targets)} component(s) to defaults. Restart surfaces to apply." ), } @@ -393,10 +387,7 @@ def _capture_state(self, components: list[str]) -> dict: "path": str(path), "exists": True, "type": "directory", - "entries": [ - str(p.relative_to(path)) - for p in path.rglob("*") - ], + "entries": [str(p.relative_to(path)) for p in path.rglob("*")], } except Exception: state["components"][comp] = {"path": str(path), "error": "read failed"} @@ -434,9 +425,7 @@ def _find_recovery(self, rp_id: str) -> RecoveryPoint | 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" - ) + 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.""" @@ -448,12 +437,14 @@ def _load_recovery_points(self) -> None: 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, - )) + 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 diff --git a/backend/app/skills/builtin/skill_dev_mode_executor.py b/backend/app/skills/builtin/skill_dev_mode_executor.py index f6a40160..6b586684 100644 --- a/backend/app/skills/builtin/skill_dev_mode_executor.py +++ b/backend/app/skills/builtin/skill_dev_mode_executor.py @@ -4,16 +4,16 @@ 1. Analyze task complexity (route_task) 2. Select specialized agent (agents.yaml routing matrix) 3. (Optional) Multi-model deliberation (hivemind-consensus) - 4. Execute via Cline or Roundtable (cline-invoke / roundtable-dispatch) + 4. Execute via the governed router, Roundtable, or Hivemind 5. Review results (reviewer agent) - 6. Log to spool + update .tasker + 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, - cline-invoke, tasker API, spool. + route_task, task API, spool. """ from __future__ import annotations @@ -23,6 +23,8 @@ 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") @@ -37,10 +39,10 @@ } EXECUTOR_CHOICE = { - "implementation": "cline-invoke", - "coding": "cline-invoke", - "debugging": "cline-invoke", - "testing": "cline-invoke", + "implementation": "route_task", + "coding": "route_task", + "debugging": "route_task", + "testing": "route_task", "architecture": "roundtable-dispatch", "design": "hivemind-consensus", "planning": "hivemind-consensus", @@ -67,7 +69,7 @@ class DevModeExecutorSkill(BaseSkill): name="task_uid", type="string", required=True, - description="Task UID from .tasker to execute", + description="Task UID from uFlow to execute", ), SkillParam( name="use_consensus", @@ -83,7 +85,7 @@ class DevModeExecutorSkill(BaseSkill): default="auto", description=( "Execution mode: 'auto' (select best)," - " 'cline', 'roundtable', 'hivemind'" + " 'route_task', 'roundtable', 'hivemind'" ), ), SkillParam( @@ -129,7 +131,7 @@ async def run(self, **kwargs) -> dict: "status": "skipped", "reason": str(exc), } - # Stage 1: Fetch task from .tasker + # Stage 1: Fetch task from uFlow task_data = self._fetch_task(task_uid) if not task_data: return { @@ -197,13 +199,13 @@ async def run(self, **kwargs) -> dict: # ─── Stage 1: Fetch Task ─────────────────────────────────── def _fetch_task(self, task_uid: str) -> dict | None: - """Fetch task from .tasker directory.""" + """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.glob("*.md"): + 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 @@ -234,15 +236,9 @@ def _fetch_task(self, task_uid: str) -> dict | None: @staticmethod def _find_tasker_dir() -> Path | None: - """Locate .tasker directory.""" - candidates = [ - Path.cwd() / ".tasker", - Path.home() / "Code" / "uCore" / ".tasker", - ] - for c in candidates: - if c.is_dir(): - return c - return None + """Locate uFlow's canonical task directory.""" + path = default_tasker_dir() + return path if path.is_dir() else None # ─── Stage 2: Analyze & Route ────────────────────────────── @@ -268,7 +264,7 @@ async def _analyze_route( # Map to specialized agent agent = AGENT_ROUTING.get(task_type, "dev") - executor = EXECUTOR_CHOICE.get(task_type, "cline-invoke") + executor = EXECUTOR_CHOICE.get(task_type, "route_task") # Estimate complexity complex_signals = [ @@ -330,9 +326,6 @@ async def _execute( if executor == "roundtable-dispatch": return await self._call_roundtable(task) - if executor == "cline-invoke": - return await self._call_cline(task) - # Fallback: route_task return await self._call_route_task(task) @@ -355,46 +348,6 @@ async def _call_roundtable(self, task: str) -> dict: except Exception as exc: return {"executor": "roundtable", "error": str(exc)} - async def _call_cline(self, task: str) -> dict: - """Invoke Cline CLI.""" - import asyncio - import os - import subprocess - - cline_bin = None - for c in ["cline", "npx @cline/cli"]: - try: - subprocess.run(["which", c.split()[0]], capture_output=True, text=True, timeout=2, check=True) - cline_bin = c - break - except Exception: - continue - - if not cline_bin: - return {"executor": "cline", "error": "Cline CLI not found"} - - try: - cmd = cline_bin.split() + ["--task", task[:500]] - proc = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - env={**os.environ}, - ) - stdout, stderr = await asyncio.wait_for( - proc.communicate(), timeout=120, - ) - output = stdout.decode("utf-8", errors="replace") if stdout else "" - return { - "executor": "cline", - "exit_code": proc.returncode, - "output": output[:2000], - } - except asyncio.TimeoutError: - return {"executor": "cline", "error": "Timeout after 120s"} - except Exception as exc: - return {"executor": "cline", "error": str(exc)} - async def _call_route_task(self, task: str) -> dict: """Fallback: route through route_task skill.""" return { @@ -432,7 +385,7 @@ def _log_to_spool(self, task_uid: str, pipeline: dict) -> None: pass def _update_tasker(self, task_uid: str, status: str) -> None: - """Update task status in .tasker.""" + """Update task status through uFlow's API.""" try: payload = json.dumps({ "task_id": task_uid, diff --git a/backend/app/skills/builtin/skill_devlog_mcp.py b/backend/app/skills/builtin/skill_devlog_mcp.py deleted file mode 100644 index 74a1bc51..00000000 --- a/backend/app/skills/builtin/skill_devlog_mcp.py +++ /dev/null @@ -1,180 +0,0 @@ -"""Devlog MCP — Generate MCP-formatted devlog from completed tasks and spool activity. - -Creates a structured devlog in MCP format for AI consumption, linking completed -tasks to spool history and archive system. - -Usage: - POST /api/skills/devlog_mcp/run - Body: { - "tasker_dir": ".tasker", - "output_file": "devlog.mcp.yaml", - "hours": 24 - } -""" -from __future__ import annotations - -import json -from datetime import UTC, datetime, timedelta -from pathlib import Path -from typing import Any - -from app.core.settings import settings -from app.services.spool_reader import read_spool -from app.skills.base import BaseSkill, SkillMeta, SkillParam - -PROJECT_ROOT = settings.udos_root / "uCore" -DEFAULT_TASKER_DIR = PROJECT_ROOT / ".tasker" - - -class DevlogMCP(BaseSkill): - meta = SkillMeta( - id="devlog_mcp", - name="Devlog MCP", - description="Generate MCP-formatted devlog from completed tasks and spool activity", - category="workflow", - timeout=60, - params=[ - SkillParam( - name="tasker_dir", - type="string", - required=False, - default=str(DEFAULT_TASKER_DIR), - description="Path to .tasker directory", - ), - SkillParam( - name="output_file", - type="string", - required=False, - default="devlog.mcp.yaml", - description="Output filename for MCP devlog", - ), - SkillParam( - name="hours", - type="integer", - required=False, - default=24, - description="Hours of spool activity to include", - ), - SkillParam( - name="include_archived", - type="boolean", - required=False, - default=True, - description="Include archived tasks", - ), - ], - requires_confirmation=False, - ) - - async def run(self, **kwargs) -> dict: - tasker_dir = Path(kwargs.get("tasker_dir", DEFAULT_TASKER_DIR)).expanduser() - output_file = kwargs.get("output_file", "devlog.mcp.yaml") - hours = int(kwargs.get("hours", 24)) - include_archived = bool(kwargs.get("include_archived", True)) - - # Collect completed tasks - completed_tasks = self._collect_completed_tasks(tasker_dir, include_archived) - - # Collect spool activity - cutoff = (datetime.now(UTC) - timedelta(hours=hours)).isoformat() - spool_entries = read_spool(since=cutoff) - - # Generate MCP devlog - devlog_content = self._render_devlog( - completed_tasks=completed_tasks, - spool_entries=spool_entries, - hours=hours, - ) - - # Write output - output_path = tasker_dir.parent / output_file - output_path.write_text(devlog_content, encoding="utf-8") - - return { - "success": True, - "output_path": str(output_path), - "completed_tasks": len(completed_tasks), - "spool_entries": len(spool_entries), - "hours": hours, - } - - def _collect_completed_tasks( - self, - tasker_dir: Path, - include_archived: bool, - ) -> list[dict[str, Any]]: - """Collect completed tasks from .tasker and optionally .tasker.archived.""" - tasks = [] - - for source_dir in [tasker_dir]: - if not source_dir.exists(): - continue - for task_file in source_dir.rglob("*.md"): - if task_file.name == "README.md": - continue - try: - content = task_file.read_text(encoding="utf-8") - if "status: done" in content or "status: completed" in content: - tasks.append({ - "file": str(task_file.relative_to(tasker_dir.parent)), - "content": content, - }) - except Exception: - continue - - if include_archived: - archived_dir = tasker_dir.parent / ".tasker.archived" - if archived_dir.exists(): - for task_file in archived_dir.rglob("*.md"): - try: - content = task_file.read_text(encoding="utf-8") - tasks.append({ - "file": str(task_file.relative_to(tasker_dir.parent)), - "content": content, - "archived": True, - }) - except Exception: - continue - - return tasks - - def _render_devlog( - self, - completed_tasks: list[dict[str, Any]], - spool_entries: list[dict[str, Any]], - hours: int, - ) -> str: - """Render MCP-formatted devlog.""" - timestamp = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ") - - lines = [ - f"# Devlog MCP — Generated: {timestamp}", - "", - "version: \"1.0.0\"", - "generated_by: \"Copilot-Poolside-Laguana-M1\"", - f"hours: {hours}", - f"completed_tasks: {len(completed_tasks)}", - f"spool_entries: {len(spool_entries)}", - "", - "## Completed Tasks", - "", - ] - - for task in completed_tasks: - lines.append(f"- file: {task['file']}") - lines.append(f" archived: {task.get('archived', False)}") - lines.append("") - - lines.extend([ - "## Spool Activity", - "", - ]) - - for entry in spool_entries[:50]: # Limit to 50 entries - lines.append(f"- timestamp: {entry.timestamp}") - lines.append(f" level: {entry.level}") - lines.append(f" module: {entry.module}") - lines.append(f" message: {entry.message[:100]}") - lines.append("") - - return "\n".join(lines) diff --git a/backend/app/skills/builtin/skill_docs_roundup.py b/backend/app/skills/builtin/skill_docs_roundup.py deleted file mode 100644 index c861b7b0..00000000 --- a/backend/app/skills/builtin/skill_docs_roundup.py +++ /dev/null @@ -1,311 +0,0 @@ -"""docs_roundup — End-of-dev-round docs automation. - -Performs a complete docs round before commit: -1. Archive completed tasks from .tasker to .tasker.archived -2. Update devlog.mcp.yaml with session completion data -3. Organise docs: remove legacy files, consolidate related docs -4. Update specs that need updating -5. Archive old/unused specs -6. Plan new docs needed for next round - -Usage: - POST /api/skills/docs_roundup/run - Body: { - "action": "roundup", - "session_summary": "Implemented tasker_ingest bridge + dev_layer + docs_roundup", - "next_round_plans": ["Phase 2: Web Chat bridge", "Phase 3: GitHub automation"], - "dry_run": false - } -""" -from __future__ import annotations - -import logging -import re -import shutil -from datetime import UTC, datetime, timedelta -from pathlib import Path -from typing import Any - -from app.core.settings import settings -from app.services.spool_writer import write_spool -from app.skills.base import BaseSkill, SkillMeta, SkillParam - -log = logging.getLogger("ucore.skills.docs_roundup") - -PROJECT_ROOT = settings.udos_root / "uCore" -DEFAULT_TASKER_DIR = PROJECT_ROOT / ".tasker" -DEFAULT_ARCHIVE_DIR = PROJECT_ROOT / ".tasker.archived" -DEFAULT_DEVLOG_FILE = PROJECT_ROOT / "devlog.mcp.yaml" -DEFAULT_DOCS_DIR = PROJECT_ROOT / "docs" -DEFAULT_TASKER_FILE = PROJECT_ROOT / ".tasker.dev-flow.yaml" - - -class DocsRoundup(BaseSkill): - """End-of-round docs automation: archive, update, organise, plan.""" - - meta = SkillMeta( - id="docs_roundup", - name="Docs Roundup", - description=( - "End-of-dev-round docs automation: archive completed tasks, " - "update devlog, organise docs, update/archive specs, plan new docs" - ), - category="workflow", - timeout=180, - params=[ - SkillParam( - name="action", - type="string", - required=True, - description="Action: roundup, archive, organise, plan, full", - ), - SkillParam( - name="session_summary", - type="string", - required=False, - default="", - description="Summary of what was accomplished this round", - ), - SkillParam( - name="next_round_plans", - type="list", - required=False, - default=[], - description="List of planned items for next round", - ), - SkillParam( - name="max_age_days", - type="integer", - required=False, - default=14, - description="Days before archiving old completed tasks", - ), - SkillParam( - name="dry_run", - type="boolean", - required=False, - default=False, - description="Preview without making changes", - ), - ], - requires_confirmation=True, # Mutating operation - ) - - async def run(self, **kwargs) -> dict: - action = str(kwargs.get("action", "full")).strip().lower() - session_summary = str(kwargs.get("session_summary", "")) - next_round_plans = kwargs.get("next_round_plans", []) - max_age_days = int(kwargs.get("max_age_days", 14)) - dry_run = bool(kwargs.get("dry_run", False)) - - if action == "roundup" or action == "full": - return await self._full_roundup( - session_summary, next_round_plans, max_age_days, dry_run, - ) - elif action == "archive": - return self._archive_old_tasks(max_age_days, dry_run) - elif action == "organise": - return self._organise_docs(dry_run) - elif action == "plan": - return self._plan_docs(session_summary, next_round_plans) - else: - return {"success": False, "error": f"Unknown action: {action}"} - - async def _full_roundup( - self, - session_summary: str, - next_round_plans: list[str], - max_age_days: int, - dry_run: bool, - ) -> dict: - """Run full docs roundup: archive → organise → update devlog → plan.""" - results: dict[str, Any] = { - "archive": None, - "organise": None, - "devlog_updated": False, - "plan": None, - } - - # Step 1: Archive old tasks - archive_result = self._archive_old_tasks(max_age_days, dry_run) - results["archive"] = archive_result - - # Step 2: Organise docs (remove legacy, consolidate) - organise_result = self._organise_docs(dry_run) - results["organise"] = organise_result - - # Step 3: Update devlog with session completion - if not dry_run: - self._update_devlog(session_summary) - results["devlog_updated"] = True - - # Step 4: Generate plan for next round - plan_result = self._plan_docs(session_summary, next_round_plans) - results["plan"] = plan_result - - # Step 5: Write to spool - if not dry_run: - write_spool( - level="INFO", - module="docs_roundup", - message=( - f"Docs roundup complete: " - f"archived={archive_result.get('archived', 0)} tasks, " - f"cleaned={organise_result.get('cleaned', 0)} files, " - f"planned={len(next_round_plans)} items" - ), - tags=["docs-roundup"], - ) - - return { - "success": True, - "action": "full", - "results": results, - "dry_run": dry_run, - } - - def _archive_old_tasks(self, max_age_days: int, dry_run: bool) -> dict: - """Archive completed tasks older than max_age_days from .tasker.""" - tasker_dir = DEFAULT_TASKER_DIR - archive_dir = DEFAULT_ARCHIVE_DIR - archived_count = 0 - archive_list: list[str] = [] - - if not tasker_dir.exists(): - return {"archived": 0, "cleaned": []} - - cutoff = datetime.now(UTC) - timedelta(days=max_age_days) - - for task_file in tasker_dir.rglob("*.md"): - if task_file.name == "README.md": - continue - try: - content = task_file.read_text(encoding="utf-8") - if "status: done" in content or "status: completed" in content: - mtime = datetime.fromtimestamp(task_file.stat().st_mtime, tz=UTC) - if mtime < cutoff: - relative = task_file.relative_to(tasker_dir) - dest = archive_dir / relative - archive_list.append(str(relative)) - if not dry_run: - dest.parent.mkdir(parents=True, exist_ok=True) - if dest.exists(): - dest.write_text(content, encoding="utf-8") - else: - shutil.copy2(str(task_file), str(dest)) - task_file.unlink(missing_ok=True) - archived_count += 1 - except Exception as e: - log.warning("Failed to archive %s: %s", task_file, e) - continue - - return { - "archived": archived_count, - "files": archive_list, - "dry_run": dry_run, - } - - def _organise_docs(self, dry_run: bool) -> dict: - """Organise docs directory: remove legacy, consolidate related.""" - docs_dir = DEFAULT_DOCS_DIR - cleaned = 0 - kept = 0 - legacy_patterns = [ - r"COMPLETE.*\.md$", - r"DONE.*\.md$", - r"REPORT_.*\.md$", - r"CHECKLIST.*\.md$", - r"OLD_.*\.md$", - r"DEPRECATED_.*\.md$", - ] - - if not docs_dir.exists(): - return {"cleaned": 0, "kept": 0, "files": []} - - removed_files: list[str] = [] - for doc_file in docs_dir.rglob("*.md"): - # Skip archive directory - if "archive" in doc_file.parts: - kept += 1 - continue - for pattern in legacy_patterns: - if re.search(pattern, doc_file.name, re.IGNORECASE): - if not dry_run: - # Move to archive instead of delete - archive_dest = docs_dir / "archive" / doc_file.relative_to(docs_dir) - archive_dest.parent.mkdir(parents=True, exist_ok=True) - try: - shutil.move(str(doc_file), str(archive_dest)) - removed_files.append(str(doc_file.relative_to(docs_dir.parent))) - cleaned += 1 - except Exception as e: - log.warning("Failed to move %s: %s", doc_file, e) - else: - removed_files.append(str(doc_file.relative_to(docs_dir.parent))) - cleaned += 1 - break - else: - kept += 1 - - return { - "cleaned": cleaned, - "kept": kept, - "files": removed_files, - "dry_run": dry_run, - } - - def _update_devlog(self, session_summary: str) -> None: - """Update devlog.mcp.yaml with roundup completion entry.""" - devlog_path = DEFAULT_DEVLOG_FILE - now = datetime.now(UTC) - - entry = ( - f"\n## Docs Roundup: {now.strftime('%Y-%m-%d %H:%M:%S')} UTC" - f"\n- type: docs_roundup" - f"\n- timestamp: {now.isoformat()}" - ) - if session_summary: - entry += f"\n- summary: {session_summary}" - - existing = "" - if devlog_path.exists(): - existing = devlog_path.read_text(encoding="utf-8") - devlog_path.write_text(existing + entry + "\n", encoding="utf-8") - - def _plan_docs( - self, - session_summary: str, - next_round_plans: list[str], - ) -> dict: - """Generate a docs plan for the next round.""" - # Scan for existing specs that may need updating - docs_dir = DEFAULT_DOCS_DIR - existing_specs: list[str] = [] - if docs_dir.exists(): - for f in sorted(docs_dir.glob("*_SPEC*.md")): - existing_specs.append(f.name) - for f in sorted(docs_dir.glob("*_PLAN*.md")): - existing_specs.append(f.name) - - # Identify stale specs (no changes in 30+ days) - stale_specs: list[str] = [] - cutoff = datetime.now() - timedelta(days=30) - for spec_name in existing_specs: - spec_path = docs_dir / spec_name - if spec_path.exists(): - mtime = datetime.fromtimestamp(spec_path.stat().st_mtime) - if mtime < cutoff: - stale_specs.append(spec_name) - - return { - "session_summary": session_summary or "No summary provided", - "next_round_plans": next_round_plans or ["TBD"], - "existing_specs": existing_specs, - "stale_specs": stale_specs, - "suggestions": [ - f"Review stale specs: {', '.join(stale_specs)}" - if stale_specs else "No stale specs found", - f"Docs archive has {len(existing_specs)} existing specs", - "Consider adding to docs/archive/README.md index", - ], - } diff --git a/backend/app/skills/builtin/skill_ecosystem_audit.py b/backend/app/skills/builtin/skill_ecosystem_audit.py index 4905dfee..0afeb27a 100644 --- a/backend/app/skills/builtin/skill_ecosystem_audit.py +++ b/backend/app/skills/builtin/skill_ecosystem_audit.py @@ -5,12 +5,13 @@ - Paths (file system paths used by skills, configs, vault, seeds) - Variables (scope, key, type, default from variable APIs) - Secrets (key, store, scope from config/env files) - - MCP Servers (name, command, args, env from .vscode/mcp.json) + - MCP servers and bridge components owned by uCore - Routes (method, path, handler from routes.py) - Runtimes (name, file, endpoints, variables, commands from backend modules) Generates seeds/ecosystem-registry.json for frontend consumption. """ + from __future__ import annotations import json @@ -18,6 +19,7 @@ import re from pathlib import Path +from app.core.settings import settings from app.skills.base import BaseSkill, SkillMeta, SkillParam log = logging.getLogger("ucore.skills.ecosystem_audit") @@ -41,9 +43,10 @@ def _repo_for(path: Path) -> str: if sp.startswith(root): return name return "unknown" + + API_DIR = BACKEND_DIR / "api" ROUTES_FILE = API_DIR / "routes.py" -MCP_CONFIG_FILE = ROOT_DIR / ".vscode" / "mcp.json" ENV_FILE = ROOT_DIR.parent.parent / ".config" / "hivemind" / ".env" VAULT_CONFIG = ROOT_DIR.parent / "uCode" / "config" / "vault.yaml" SECRETS_API = API_DIR / "secret_store_api.py" @@ -157,15 +160,14 @@ def _parse_skill_file(self, filepath: Path) -> dict | None: # Extract description desc_match = re.search( - r'description\s*=\s*\(\s*\n?(.*?)\n?\s*\)', - content, re.DOTALL, + r"description\s*=\s*\(\s*\n?(.*?)\n?\s*\)", + content, + re.DOTALL, ) if desc_match: desc = desc_match.group(1).strip().strip('"').strip("'") parts = desc.split() - desc = " ".join([ - d.strip().strip('"').strip("'") for d in parts - ]) + desc = " ".join([d.strip().strip('"').strip("'") for d in parts]) info["description"] = desc[:200] # Extract category @@ -185,25 +187,27 @@ def _parse_skill_file(self, filepath: Path) -> dict | None: # Extract SkillParams param_matches = re.findall( r'SkillParam\(\s*name\s*=\s*"([^"]+)"' - r'.*?description\s*=\s*\(\s*(.*?)\s*\)', - content, re.DOTALL, + r".*?description\s*=\s*\(\s*(.*?)\s*\)", + content, + re.DOTALL, ) for pname, pdesc in param_matches: - pdesc_clean = " ".join( - d.strip().strip('"').strip("'") for d in pdesc.split() - ) + pdesc_clean = " ".join(d.strip().strip('"').strip("'") for d in pdesc.split()) ptype = "string" type_match = re.search( rf'SkillParam\(\s*name\s*=\s*"{pname}".*?type\s*=\s*"([^"]+)"', - content, re.DOTALL, + content, + re.DOTALL, ) if type_match: ptype = type_match.group(1) - info["params"].append({ - "name": pname, - "type": ptype, - "description": pdesc_clean[:120], - }) + info["params"].append( + { + "name": pname, + "type": ptype, + "description": pdesc_clean[:120], + } + ) return info @@ -222,15 +226,18 @@ def _audit_routes(self) -> dict: r'app\.router\.add_(get|post|put|delete)\("([^"]+)"\s*,\s*(\w+)', content, ): - routes.append({ - "method": match.group(1).upper(), - "path": match.group(2), - "handler": match.group(3), - }) + routes.append( + { + "method": match.group(1).upper(), + "path": match.group(2), + "handler": match.group(3), + } + ) # Also look for try/except block imports import_blocks = re.findall( - r"from \.(\w+)\s+import\s+(\w+)", content, + r"from \.(\w+)\s+import\s+(\w+)", + content, ) modules_used = list(set(m[0] for m in import_blocks)) @@ -251,16 +258,19 @@ def _audit_secrets(self) -> dict: if ENV_FILE.exists(): content = ENV_FILE.read_text() for match in re.finditer( - r'(#\s*(.*?)\n)?\s*(\w+)\s*=\s*"[^"]*"', content, + r'(#\s*(.*?)\n)?\s*(\w+)\s*=\s*"[^"]*"', + content, ): key = match.group(3) comment = match.group(2) or "" - secrets.append({ - "key": key, - "scope": "environment", - "store": "~/.config/hivemind/.env", - "description": comment.strip()[:100], - }) + secrets.append( + { + "key": key, + "scope": "environment", + "store": "~/.config/hivemind/.env", + "description": comment.strip()[:100], + } + ) # Check vault.yaml for secret references if VAULT_CONFIG.exists(): @@ -268,12 +278,14 @@ def _audit_secrets(self) -> dict: key_matches = re.findall(r'-\s*"(\w+)"', content) for key in key_matches: if any(k.upper() in ("KEY", "TOKEN", "SECRET") for k in [key]): - secrets.append({ - "key": key, - "scope": "vault", - "store": "~/.local/share/udos/Vault/", - "description": "Referenced in vault.yaml", - }) + secrets.append( + { + "key": key, + "scope": "vault", + "store": "~/.local/share/udos/Vault/", + "description": "Referenced in vault.yaml", + } + ) return {"success": True, "secrets": secrets, "total": len(secrets)} @@ -285,83 +297,87 @@ def _audit_variables(self) -> dict: # From vault.yaml if VAULT_CONFIG.exists(): - variables.append({ - "scope": "user", - "file": "~/.local/share/udos/Vault/variables/user.yaml", - "description": "User-level persistent variables", - "examples": ["theme", "editor_font_size", "last_world"], - }) - variables.append({ - "scope": "global", - "file": "~/.local/share/udos/Vault/variables/global.yaml", - "description": "System-wide variables", - "examples": ["runtime_version", "engine"], - }) - variables.append({ - "scope": "snack", - "file": "snack manifest (per-container)", - "description": "Per-snack container state", - "examples": ["level", "player_hp"], - }) - variables.append({ - "scope": "system", - "file": "memory only (not persisted)", - "description": "Runtime-only state", - "examples": ["pid", "uptime_seconds"], - }) + variables.append( + { + "scope": "user", + "file": "~/.local/share/udos/Vault/variables/user.yaml", + "description": "User-level persistent variables", + "examples": ["theme", "editor_font_size", "last_world"], + } + ) + variables.append( + { + "scope": "global", + "file": "~/.local/share/udos/Vault/variables/global.yaml", + "description": "System-wide variables", + "examples": ["runtime_version", "engine"], + } + ) + variables.append( + { + "scope": "snack", + "file": "snack manifest (per-container)", + "description": "Per-snack container state", + "examples": ["level", "player_hp"], + } + ) + variables.append( + { + "scope": "system", + "file": "memory only (not persisted)", + "description": "Runtime-only state", + "examples": ["pid", "uptime_seconds"], + } + ) # From variables_api.py if VARIABLES_API.exists(): content = VARIABLES_API.read_text() - get_vars = re.findall(r'handle_get_(\w+)_variables', content) + get_vars = re.findall(r"handle_get_(\w+)_variables", content) for gv in get_vars: - variables.append({ - "scope": gv, - "source": "variables_api.py", - "description": f"Exposed via /api/variables/{gv}", - "examples": [], - }) + variables.append( + { + "scope": gv, + "source": "variables_api.py", + "description": f"Exposed via /api/variables/{gv}", + "examples": [], + } + ) return {"success": True, "variables": variables, "total": len(variables)} # ─── Audit MCP ──────────────────────────────────────────────────── def _audit_mcp(self) -> dict: - """Discover MCP server configurations from canonical workspace config.""" + """Discover MCP servers and bridge components owned by uCore.""" servers = [] - # From .vscode/mcp.json - if MCP_CONFIG_FILE.exists(): - try: - data = json.loads(MCP_CONFIG_FILE.read_text()) - server_map = data.get("servers", {}) if isinstance(data, dict) else {} - for name, config in server_map.items(): - if not isinstance(config, dict): - continue - servers.append({ - "name": name, - "type": config.get("type", ""), - "command": config.get("command", ""), - "args": config.get("args", []), - "cwd": config.get("cwd", ""), - "env": config.get("env", {}), - "disabled": config.get("disabled", False), - "source": ".vscode/mcp.json", - }) - except Exception: - pass + bridge_dir = BACKEND_DIR / "mcp" / "mcp_bridge" + if (bridge_dir / "index.ts").exists(): + servers.append( + { + "name": "ucore-bridge", + "type": "stdio", + "file": str((bridge_dir / "index.ts").relative_to(ROOT_DIR)), + "command": "node backend/app/mcp/mcp_bridge/build/index.js", + "cwd": str(ROOT_DIR), + "source": "uCore self-hosted bridge", + } + ) # Also check registered MCP tools in the backend mcp_tools_dir = BACKEND_DIR / "mcp" if mcp_tools_dir.exists(): for mcp_mod in mcp_tools_dir.glob("*_server.py"): - servers.append({ - "name": mcp_mod.stem.replace("_server", ""), - "file": str(mcp_mod.relative_to(ROOT_DIR)), - "source": "backend auto-discovery", - "command": f"python3 -m app.mcp.{mcp_mod.stem}", - "cwd": str(BACKEND_DIR), - }) + servers.append( + { + "name": mcp_mod.stem.replace("_server", ""), + "file": str(mcp_mod.relative_to(ROOT_DIR)), + "source": "backend auto-discovery", + "command": f"python3 -m app.mcp.{mcp_mod.stem}", + "cwd": str(BACKEND_DIR), + } + ) return {"success": True, "servers": servers, "total": len(servers)} @@ -382,55 +398,67 @@ def _audit_paths(self) -> dict: for d in config_dirs: path = Path(d) if path.exists(): - paths.append({ - "path": str(path.relative_to(ROOT_DIR)), - "type": "config", - "description": "Configuration directory", - }) + paths.append( + { + "path": str(path.relative_to(ROOT_DIR)), + "type": "config", + "description": "Configuration directory", + } + ) # Runtime paths runtime_paths = [ - "~/.config/hivemind/.env", - "~/.cline/mcp_settings.json", - "~/.continue/config.yaml", - "~/.local/share/udos/Vault/", - "~/.local/share/udos/programs/", - "~/.local/share/udos/snacks/", + str(settings.udos_home), + str(settings.config_dir), + str(settings.logs_dir), + str(settings.vault_root), + str(settings.shared_vault_root), + str(settings.public_vault_root), ] for p in runtime_paths: - paths.append({ - "path": p, - "type": "runtime", - "description": "User runtime path", - }) + paths.append( + { + "path": p, + "type": "runtime", + "description": "User runtime path", + } + ) # Skills directory - paths.append({ - "path": "backend/app/skills/builtin/", - "type": "skills", - "description": "Builtin skills directory", - }) + paths.append( + { + "path": "backend/app/skills/builtin/", + "type": "skills", + "description": "Builtin skills directory", + } + ) # API directory - paths.append({ - "path": "backend/app/api/", - "type": "api", - "description": "REST API handlers", - }) + paths.append( + { + "path": "backend/app/api/", + "type": "api", + "description": "REST API handlers", + } + ) # MCP directory - paths.append({ - "path": "backend/app/mcp/", - "type": "mcp", - "description": "MCP servers directory", - }) + paths.append( + { + "path": "backend/app/mcp/", + "type": "mcp", + "description": "MCP servers directory", + } + ) # Services - paths.append({ - "path": "backend/app/services/", - "type": "services", - "description": "Backend service modules", - }) + paths.append( + { + "path": "backend/app/services/", + "type": "services", + "description": "Backend service modules", + } + ) return {"success": True, "paths": paths, "total": len(paths)} @@ -448,7 +476,6 @@ def _audit_runtimes(self) -> dict: "llm_router": BACKEND_DIR / "mcp" / "llm_router.py", "model_pricing": BACKEND_DIR / "services" / "model_pricing.py", "template_manager": BACKEND_DIR / "services" / "template_manager.py", - "tasker_ingest": BACKEND_DIR / "mcp" / "tasker_ingest.py", } for name, path in candidate_modules.items(): @@ -458,7 +485,8 @@ def _audit_runtimes(self) -> dict: endpoints = [] for match in re.findall( - r'"([a-z_]+)"\s*:\s*self\._[a-z_]+', content, + r'"([a-z_]+)"\s*:\s*self\._[a-z_]+', + content, ): endpoints.append(match) for match in re.findall(r'name="([a-z_]+)"', content): @@ -466,7 +494,8 @@ def _audit_runtimes(self) -> dict: variables = {} for match in re.findall( - r"self\.([a-z_]+)\s*=\s*([^#\n]+)", content, + r"self\.([a-z_]+)\s*=\s*([^#\n]+)", + content, ): key, val = match if len(key) > 2 and not key.startswith("_"): @@ -503,8 +532,6 @@ def _assess(self, output_path: str) -> dict: eco = full.get("ecosystem", {}) assessed: dict[str, list[dict]] = {} - issues: list[dict] = [] - # Score skills scored_skills = [] for s in eco.get("skills", {}).get("items", []): @@ -530,11 +557,13 @@ def _assess(self, output_path: str) -> dict: except Exception: status = "broken" s_issues.append("Cannot read file") - scored_skills.append({ - **s, - "health": status, - "issues": s_issues, - }) + scored_skills.append( + { + **s, + "health": status, + "issues": s_issues, + } + ) assessed["skills"] = scored_skills # Score MCP servers @@ -543,27 +572,24 @@ def _assess(self, output_path: str) -> dict: status = "working" m_issues: list[str] = [] cmd = m.get("command", "") - if cmd and not any( - Path(c.split()[0]).exists() - for c in [cmd] - ): + if cmd and not any(Path(c.split()[0]).exists() for c in [cmd]): # If it's a python module, check the file if "python" in cmd or "app.mcp" in cmd: mod_part = cmd.replace("python3 -m ", "").replace("python -m ", "") - mod_path = ( - BACKEND_DIR / "mcp" / (mod_part.split(".")[-1] + ".py") - ) + mod_path = BACKEND_DIR / "mcp" / (mod_part.split(".")[-1] + ".py") if not mod_path.exists(): status = "broken" m_issues.append(f"Module not found: {mod_path}") if m.get("disabled"): status = "untested" m_issues.append("Server is disabled") - scored_mcp.append({ - **m, - "health": status, - "issues": m_issues, - }) + scored_mcp.append( + { + **m, + "health": status, + "issues": m_issues, + } + ) assessed["mcp_servers"] = scored_mcp # Score runtimes @@ -575,22 +601,22 @@ def _assess(self, output_path: str) -> dict: if not rt_path.exists(): status = "broken" r_issues.append(f"File not found: {rt.get('file')}") - scored_runtimes.append({ - "name": name, - **rt, - "health": status, - "issues": r_issues, - }) + scored_runtimes.append( + { + "name": name, + **rt, + "health": status, + "issues": r_issues, + } + ) assessed["runtimes"] = scored_runtimes # Routes and paths are always 'working' if discovered assessed["routes"] = [ - {**r, "health": "working", "issues": []} - for r in eco.get("routes", {}).get("items", []) + {**r, "health": "working", "issues": []} for r in eco.get("routes", {}).get("items", []) ] assessed["paths"] = [ - {**p, "health": "working", "issues": []} - for p in eco.get("paths", {}).get("items", []) + {**p, "health": "working", "issues": []} for p in eco.get("paths", {}).get("items", []) ] assessed["secrets"] = [ {**s, "health": "working", "issues": []} @@ -603,9 +629,13 @@ def _assess(self, output_path: str) -> dict: # Aggregate health all_items = ( - scored_skills + scored_mcp + scored_runtimes - + assessed["routes"] + assessed["paths"] - + assessed["secrets"] + assessed["variables"] + scored_skills + + scored_mcp + + scored_runtimes + + assessed["routes"] + + assessed["paths"] + + assessed["secrets"] + + assessed["variables"] ) health_counts = {"working": 0, "untested": 0, "broken": 0, "orphaned": 0} for item in all_items: @@ -613,9 +643,7 @@ def _assess(self, output_path: str) -> dict: health_counts[h] = health_counts.get(h, 0) + 1 total = sum(health_counts.values()) - health_pct = ( - round((health_counts["working"] / total) * 100, 1) if total > 0 else 0 - ) + health_pct = round((health_counts["working"] / total) * 100, 1) if total > 0 else 0 result = { "success": True, @@ -626,9 +654,7 @@ def _assess(self, output_path: str) -> dict: **health_counts, "health_pct": health_pct, }, - "recommendations": self._health_recommendations( - health_counts, scored_skills - ), + "recommendations": self._health_recommendations(health_counts, scored_skills), } # Persist @@ -643,23 +669,14 @@ def _assess(self, output_path: str) -> dict: def _health_recommendations(counts: dict, skills: list) -> list[str]: recs = [] if counts.get("broken", 0) > 0: - recs.append( - f"Fix {counts['broken']} broken items — " - "check error logs for details" - ) + recs.append(f"Fix {counts['broken']} broken items — check error logs for details") if counts.get("untested", 0) > 0: - recs.append( - f"Smoke-test {counts['untested']} untested items " - "to validate they work" - ) + recs.append(f"Smoke-test {counts['untested']} untested items to validate they work") if counts.get("orphaned", 0) > 0: recs.append( - f"Review {counts['orphaned']} orphaned items — " - "consider archiving or re-wiring" + f"Review {counts['orphaned']} orphaned items — consider archiving or re-wiring" ) - untested_skills = [ - s["name"] for s in skills if s.get("health") == "untested" - ] + untested_skills = [s["name"] for s in skills if s.get("health") == "untested"] if untested_skills: recs.append( f"Untested skills: {', '.join(untested_skills[:5])}" diff --git a/backend/app/skills/builtin/skill_nuggets_and_spool.py b/backend/app/skills/builtin/skill_nuggets_and_spool.py index 5c7ff586..61a90646 100644 --- a/backend/app/skills/builtin/skill_nuggets_and_spool.py +++ b/backend/app/skills/builtin/skill_nuggets_and_spool.py @@ -8,7 +8,7 @@ Enables tiny archival records that dont consume space like .git can. 2. BACKUP — Timelined/diff-based backups that also get SPOOLed. - Backups are stored as small, timestamped chunks in ~/.ucore/backups/ + Backups are stored as small, timestamped chunks in $UDOS_HOME/backups/ and automatically SPOOLed so they dont accumulate indefinitely. Old backups are pruned based on max_age_days. @@ -25,7 +25,7 @@ data and can be redeployed/reborn. Unlike a SPOOL (which is a system by-product of Destroy/Backup), a Nugget is intentionally created and left by a user, or may be a by-product of a Destroy and System Plate - Reset. Nuggets are stored in ~/.ucore/nuggets/ and can be: + Reset. Nuggets are stored in $UDOS_HOME/nuggets/ and can be: - Intentionally created by a user to preserve something valuable - By-product of a DESTROY and System Plate Reset - Redeployed/reborn via unfurl back into a living component @@ -64,9 +64,9 @@ # ─── Paths ───────────────────────────────────────────────── -SPOOL_DIR = Path("~/.ucore/logs").expanduser() -BACKUP_DIR = Path("~/.ucore/backups").expanduser() -NUGGET_DIR = Path("~/.ucore/nuggets").expanduser() +SPOOL_DIR = settings.logs_dir +BACKUP_DIR = settings.udos_home / "backups" +NUGGET_DIR = settings.udos_home / "nuggets" PLATES_ROOT = Path(__file__).resolve().parents[4] / "plates" # uCore/plates/ # ─── Log rotation constants (merged from spool_maintenance) ────────── @@ -80,6 +80,7 @@ # ─── Helpers ─────────────────────────────────────────────── + def _ensure_dirs() -> None: """Ensure SPOOL and BACKUP directories exist.""" SPOOL_DIR.mkdir(parents=True, exist_ok=True) @@ -169,18 +170,20 @@ def _list_spool_files( continue if event and record.get("event") != event: continue - results.append({ - "file": str(f), - "timestamp": record.get("timestamp", ""), - "event": record.get("event", ""), - "component_id": record.get("component", {}).get("id", ""), - "component_type": record.get("component", {}).get("type", ""), - "version": record.get("component", {}).get("version", ""), - "salvaged_keys": list(record.get("salvaged", {}).keys()), - "lesson_count": len(record.get("lessons", [])), - "has_errors": len(record.get("errors", [])) > 0, - "has_backup": bool(record.get("backup")), - }) + results.append( + { + "file": str(f), + "timestamp": record.get("timestamp", ""), + "event": record.get("event", ""), + "component_id": record.get("component", {}).get("id", ""), + "component_type": record.get("component", {}).get("type", ""), + "version": record.get("component", {}).get("version", ""), + "salvaged_keys": list(record.get("salvaged", {}).keys()), + "lesson_count": len(record.get("lessons", [])), + "has_errors": len(record.get("errors", [])) > 0, + "has_backup": bool(record.get("backup")), + } + ) if len(results) >= max_results: break except Exception: @@ -214,6 +217,7 @@ def _compute_diff(source: str, target: str) -> dict[str, Any]: # ─── Skill: spool_archive ────────────────────────────────── + class SpoolArchiveSkill(BaseSkill): """ARCHIVE — Compress legacy content into SPOOL record. @@ -316,7 +320,9 @@ async def run(self, **kwargs) -> dict: "source_size_bytes": src.stat().st_size if src.exists() else 0, "source_modified": datetime.fromtimestamp( src.stat().st_mtime, tz=timezone.utc - ).isoformat() if src.exists() else "", + ).isoformat() + if src.exists() + else "", **extra_metadata, } @@ -357,10 +363,11 @@ async def run(self, **kwargs) -> dict: # ─── Skill: spool_backup ─────────────────────────────────── + class SpoolBackupSkill(BaseSkill): """BACKUP — Timelined/diff-based backup with SPOOL integration. - Creates timestamped backups in ~/.ucore/backups/ and writes a + Creates timestamped backups in $UDOS_HOME/backups/ and writes a SPOOL record referencing the backup. Supports diff-based backups (only store changes from previous backup) and automatic pruning of old backups based on max_age_days. @@ -527,6 +534,7 @@ async def run(self, **kwargs) -> dict: # ─── Skill: spool_destroy ────────────────────────────────── + class SpoolDestroySkill(BaseSkill): """DESTROY — Component destruction with full SPOOL preservation. @@ -637,11 +645,13 @@ async def run(self, **kwargs) -> dict: except Exception: pass - steps.append({ - "step": "salvage", - "status": "ok", - "salvaged_keys": list(salvaged.keys()), - }) + steps.append( + { + "step": "salvage", + "status": "ok", + "salvaged_keys": list(salvaged.keys()), + } + ) # Step 2: BACKUP if source_path and not dry_run: @@ -662,15 +672,15 @@ async def run(self, **kwargs) -> dict: root_dir=src.parent, base_dir=src.name, ) - backup_path = str( - comp_backup_dir / f"{component_id}_{ts}.pre_destroy.tar.gz" - ) + backup_path = str(comp_backup_dir / f"{component_id}_{ts}.pre_destroy.tar.gz") - steps.append({ - "step": "backup", - "status": "ok" if backup_path else "skipped", - "backup_path": backup_path, - }) + steps.append( + { + "step": "backup", + "status": "ok" if backup_path else "skipped", + "backup_path": backup_path, + } + ) # Step 3: ARCHIVE (write SPOOL record) record = _build_spool_record( @@ -690,11 +700,13 @@ async def run(self, **kwargs) -> dict: ) spool_path = _write_spool_file(record) - steps.append({ - "step": "archive", - "status": "ok", - "spool_path": spool_path, - }) + steps.append( + { + "step": "archive", + "status": "ok", + "spool_path": spool_path, + } + ) # Step 4: DESTROY (skip in dry_run) if not dry_run and source_path: @@ -704,23 +716,29 @@ async def run(self, **kwargs) -> dict: src.unlink() elif src.is_dir(): shutil.rmtree(src) - steps.append({ - "step": "destroy", - "status": "ok", - "removed": str(src), - }) + steps.append( + { + "step": "destroy", + "status": "ok", + "removed": str(src), + } + ) else: - steps.append({ - "step": "destroy", - "status": "skipped", - "reason": "Source not found", - }) + steps.append( + { + "step": "destroy", + "status": "skipped", + "reason": "Source not found", + } + ) else: - steps.append({ - "step": "destroy", - "status": "dry_run", - "would_remove": source_path, - }) + steps.append( + { + "step": "destroy", + "status": "dry_run", + "would_remove": source_path, + } + ) # Step 5: REBUILD if rebuild_command and not dry_run: @@ -729,16 +747,22 @@ async def run(self, **kwargs) -> dict: for k, v in salvaged.items(): cmd = cmd.replace(f"${{{k}}}", str(v)) result = subprocess.run( - cmd, shell=True, capture_output=True, text=True, timeout=120, + cmd, + shell=True, + capture_output=True, + text=True, + timeout=120, ) rebuild_output = result.stdout if result.returncode != 0: errors.append(f"Rebuild failed: {result.stderr}") - steps.append({ - "step": "rebuild", - "status": "ok" if result.returncode == 0 else "failed", - "output": rebuild_output[:200], - }) + steps.append( + { + "step": "rebuild", + "status": "ok" if result.returncode == 0 else "failed", + "output": rebuild_output[:200], + } + ) except subprocess.TimeoutExpired: errors.append("Rebuild timed out") steps.append({"step": "rebuild", "status": "timeout"}) @@ -746,18 +770,22 @@ async def run(self, **kwargs) -> dict: errors.append(f"Rebuild error: {exc}") steps.append({"step": "rebuild", "status": "error"}) else: - steps.append({ - "step": "rebuild", - "status": "dry_run" if dry_run else "skipped", - "command": rebuild_command, - }) + steps.append( + { + "step": "rebuild", + "status": "dry_run" if dry_run else "skipped", + "command": rebuild_command, + } + ) # Step 6: VERIFY - steps.append({ - "step": "verify", - "status": "ok" if not errors else "failed", - "errors": errors, - }) + steps.append( + { + "step": "verify", + "status": "ok" if not errors else "failed", + "errors": errors, + } + ) success = len(errors) == 0 return { @@ -778,6 +806,7 @@ async def run(self, **kwargs) -> dict: # ─── Skill: spool_unfurl ─────────────────────────────────── + class SpoolUnfurlSkill(BaseSkill): """UNFURL — Reverse SPOOL: reconstruct component from archived essence. @@ -870,6 +899,7 @@ async def run(self, **kwargs) -> dict: for plate_file in PLATES_ROOT.rglob("*.yaml"): try: import yaml as yl + with open(plate_file) as f: data = yl.safe_load(f) if data and data.get("plate", {}).get("id") == component_id: @@ -948,6 +978,7 @@ async def run(self, **kwargs) -> dict: # ─── Skill: spool_list ───────────────────────────────────── + class SpoolListSkill(BaseSkill): """List and search SPOOL archives. @@ -1003,10 +1034,7 @@ async def run(self, **kwargs) -> dict: # Apply text search filter if search: search_lower = search.lower() - archives = [ - a for a in archives - if search_lower in a["component_id"].lower() - ] + archives = [a for a in archives if search_lower in a["component_id"].lower()] return { "success": True, @@ -1022,6 +1050,7 @@ async def run(self, **kwargs) -> dict: # ─── Skill: spool_prune ──────────────────────────────────── + class SpoolPruneSkill(BaseSkill): """Prune old backups and SPOOL records by age, rotate logs, and archive tasks. @@ -1213,6 +1242,7 @@ def _rotate_file(self, log_file: Path, backup_count: int) -> None: # ─── Nugget Helpers ──────────────────────────────────────── + def _ensure_nugget_dir() -> None: """Ensure the Nugget directory exists.""" NUGGET_DIR.mkdir(parents=True, exist_ok=True) @@ -1262,9 +1292,7 @@ def _generate_cookiecutter_template( template_content = source_content for key, value in context.items(): if isinstance(value, (str, int, float)): - template_content = template_content.replace( - str(value), f"{{{{cookiecutter.{key}}}}}" - ) + template_content = template_content.replace(str(value), f"{{{{cookiecutter.{key}}}}}") # Write the main template file main_file = template_dir / "main.py" @@ -1288,7 +1316,7 @@ def _write_nugget_file( """Write a Nugget record to gzip'd JSON file. Nuggets use the same gzip'd JSON format as SPOOLs but are stored - in ~/.ucore/nuggets/ and have a 'nugget' event type. They are + in $UDOS_HOME/nuggets/ and have a 'nugget' event type. They are intentionally created artifacts meant to be discovered and reborn. If cookiecutter_template is provided, the Nugget is stored as a @@ -1308,6 +1336,7 @@ def _write_nugget_file( # Store as gzip'd tar: record.json + cookiecutter template dir import io as _io import tarfile + nugget_path = NUGGET_DIR / filename.replace(".json.gz", ".tar.gz") cc_dir = Path(cookiecutter_template) with tarfile.open(nugget_path, "w:gz") as tar: @@ -1364,10 +1393,12 @@ def _read_nugget_file( # Extract Cookiecutter template to temp dir try: tar.getmember("cookiecutter_template") - tmp_dir = Path(tempfile.mkdtemp( - prefix=f"nugget_extract_" - f"{record.get('component', {}).get('id', 'unknown')}_" - )) + tmp_dir = Path( + tempfile.mkdtemp( + prefix=f"nugget_extract_" + f"{record.get('component', {}).get('id', 'unknown')}_" + ) + ) tar.extractall(path=tmp_dir) cc_path = str(tmp_dir / "cookiecutter_template") return record, cc_path @@ -1400,19 +1431,21 @@ def _list_nugget_files( record = json.load(fh) if component_type and record.get("component", {}).get("type") != component_type: continue - results.append({ - "file": str(f), - "timestamp": record.get("timestamp", ""), - "component_id": record.get("component", {}).get("id", ""), - "component_type": record.get("component", {}).get("type", ""), - "version": record.get("component", {}).get("version", ""), - "description": record.get("component", {}).get("description", ""), - "salvaged_keys": list(record.get("salvaged", {}).keys()), - "lesson_count": len(record.get("lessons", [])), - "has_schema": bool(record.get("schema")), - "created_by": record.get("component", {}).get("created_by", ""), - "legacy": record.get("component", {}).get("legacy", False), - }) + results.append( + { + "file": str(f), + "timestamp": record.get("timestamp", ""), + "component_id": record.get("component", {}).get("id", ""), + "component_type": record.get("component", {}).get("type", ""), + "version": record.get("component", {}).get("version", ""), + "description": record.get("component", {}).get("description", ""), + "salvaged_keys": list(record.get("salvaged", {}).keys()), + "lesson_count": len(record.get("lessons", [])), + "has_schema": bool(record.get("schema")), + "created_by": record.get("component", {}).get("created_by", ""), + "legacy": record.get("component", {}).get("legacy", False), + } + ) if len(results) >= max_results: break except Exception: @@ -1422,6 +1455,7 @@ def _list_nugget_files( # ─── Skill: nugget_create ────────────────────────────────── + class NuggetCreateSkill(BaseSkill): """NUGGET CREATE — Intentionally create a Nugget from a component. @@ -1431,7 +1465,7 @@ class NuggetCreateSkill(BaseSkill): left by a user, or may be a by-product of a Destroy and System Plate Reset. - Nuggets are stored in ~/.ucore/nuggets/ and can be: + Nuggets are stored in $UDOS_HOME/nuggets/ and can be: - Intentionally created by a user to preserve something valuable - By-product of a DESTROY and System Plate Reset - Redeployed/reborn via unfurl back into a living component @@ -1598,6 +1632,7 @@ async def run(self, **kwargs) -> dict: # ─── Skill: nugget_list ──────────────────────────────────── + class NuggetListSkill(BaseSkill): """NUGGET LIST — List and search Nuggets. @@ -1653,7 +1688,8 @@ async def run(self, **kwargs) -> dict: if search: search_lower = search.lower() nuggets = [ - n for n in nuggets + n + for n in nuggets if search_lower in n["component_id"].lower() or search_lower in n.get("description", "").lower() ] @@ -1676,10 +1712,11 @@ async def run(self, **kwargs) -> dict: # ─── Skill: nugget_redeploy ──────────────────────────────── + class NuggetRedeploySkill(BaseSkill): """NUGGET REDEPLOY — Redeploy/reborn a Nugget back into a living component. - Reads a Nugget file from ~/.ucore/nuggets/ and materializes the + Reads a Nugget file from $UDOS_HOME/nuggets/ and materializes the component from its plate definition with salvaged state. This is the rebirth path for Nuggets — taking a compressed legacy relic and bringing it back to life. @@ -1777,6 +1814,7 @@ async def run(self, **kwargs) -> dict: for plate_file in PLATES_ROOT.rglob("*.yaml"): try: import yaml as yl + with open(plate_file) as f: data = yl.safe_load(f) if data and data.get("plate", {}).get("id") == component_id: @@ -1838,9 +1876,7 @@ async def run(self, **kwargs) -> dict: output_lines.append("# This component was reborn from a Nugget.") output_lines.append("# To fully restore, run:") if plate_found: - output_lines.append( - f"# python -m plate_refresh.refresh --destroy {component_id}" - ) + output_lines.append(f"# python -m plate_refresh.refresh --destroy {component_id}") output_lines.append("# Or use the plate directly from plates/") # Write reborn component @@ -1859,10 +1895,11 @@ async def run(self, **kwargs) -> dict: # ─── Skill: nugget_discover ──────────────────────────────── + class NuggetDiscoverSkill(BaseSkill): """NUGGET DISCOVER — Discover Nuggets left by other users or systems. - Scans ~/.ucore/nuggets/ for Nuggets that were left behind as legacy + Scans $UDOS_HOME/nuggets/ for Nuggets that were left behind as legacy artifacts. This is the discovery mechanism for finding useful relics that may contain valuable data, schemas, or lessons. @@ -1929,7 +1966,8 @@ async def run(self, **kwargs) -> dict: if search: search_lower = search.lower() nuggets = [ - n for n in nuggets + n + for n in nuggets if search_lower in n["component_id"].lower() or search_lower in n.get("description", "").lower() ] @@ -1942,9 +1980,9 @@ async def run(self, **kwargs) -> dict: if max_age_days > 0: cutoff = datetime.now(timezone.utc) - timedelta(days=max_age_days) nuggets = [ - n for n in nuggets - if n.get("timestamp", "") and - datetime.fromisoformat(n["timestamp"]) >= cutoff + n + for n in nuggets + if n.get("timestamp", "") and datetime.fromisoformat(n["timestamp"]) >= cutoff ] # Limit results @@ -1966,4 +2004,3 @@ async def run(self, **kwargs) -> dict: "useful data, schemas, or lessons from past components." ), } - diff --git a/backend/app/skills/builtin/skill_surface_registry.py b/backend/app/skills/builtin/skill_surface_registry.py index 11a4efcf..1e646c95 100644 --- a/backend/app/skills/builtin/skill_surface_registry.py +++ b/backend/app/skills/builtin/skill_surface_registry.py @@ -557,12 +557,12 @@ def _parse_developer_tabs(self) -> list[str]: def _detect_backend_wiring(self, surfaces: list[str]) -> list[str]: """Detect surfaces with known backend runtime connections.""" known_runtimes = { - "developer": ["dev_layer", "tasker_ingest"], + "developer": ["dev_layer", "control_service"], "server": ["hivemind_server", "llm_router"], "workflow": ["task_processor"], "snackmachine": ["snackmachine"], "assistui": ["assistui_runtime"], - "documentation": ["docs_roundup"], + "documentation": ["documentation_api"], "terminal": ["terminal_runtime"], "ucode": ["ucode_runtime"], "browserui": ["browser_runtime"], @@ -592,7 +592,6 @@ def _discover_backend_runtimes(self) -> dict: "template_manager": ( BACKEND_DIR / "services" / "template_manager.py" ), - "tasker_ingest": BACKEND_DIR / "mcp" / "tasker_ingest.py", } for name, path in candidate_modules.items(): diff --git a/backend/app/skills/builtin/skill_vault_discovery.py b/backend/app/skills/builtin/skill_vault_discovery.py index 3ca0fd34..c41dd9b1 100644 --- a/backend/app/skills/builtin/skill_vault_discovery.py +++ b/backend/app/skills/builtin/skill_vault_discovery.py @@ -19,6 +19,7 @@ nugget_output_dir="~/Nuggets/" ) """ + from __future__ import annotations import json @@ -27,6 +28,7 @@ from pathlib import Path from typing import Any +from app.core.settings import settings from app.skills.base import BaseSkill, SkillMeta, SkillParam log = logging.getLogger("ucore.skills.vault_discovery") @@ -39,18 +41,33 @@ } UCORE_PATHS = { - "config": Path("~/.ucore/").expanduser(), - "logs": Path("~/.ucore/logs/").expanduser(), + "config": settings.config_dir, + "logs": settings.logs_dir, "plates": Path("plates/").resolve(), } SUPPORTED_EXTENSIONS = { - ".md", ".yaml", ".yml", ".json", ".txt", ".csv", - ".py", ".ts", ".tsx", ".css", ".html", + ".md", + ".yaml", + ".yml", + ".json", + ".txt", + ".csv", + ".py", + ".ts", + ".tsx", + ".css", + ".html", } EXCLUDE_DIRS = { - ".git", "node_modules", "__pycache__", ".next", - ".obsidian", ".vscode", ".venv", ".mypy_cache", + ".git", + "node_modules", + "__pycache__", + ".next", + ".obsidian", + ".vscode", + ".venv", + ".mypy_cache", } @@ -67,16 +84,14 @@ class VaultDiscoverySkill(BaseSkill): SkillParam( name="dry_run", type="boolean", - description="If true, only identify without " - "destructive actions", + description="If true, only identify without destructive actions", required=False, default=True, ), SkillParam( name="extract_nuggets", type="boolean", - description="If true, extract reusable components " - "as Nuggets", + description="If true, extract reusable components as Nuggets", required=False, default=False, ), @@ -113,9 +128,7 @@ async def run(self, **kwargs: Any) -> dict[str, Any]: """ dry_run = kwargs.get("dry_run", True) extract_nuggets = kwargs.get("extract_nuggets", False) - nugget_output_dir = Path( - kwargs.get("nugget_output_dir", "~/Nuggets/") - ).expanduser() + nugget_output_dir = Path(kwargs.get("nugget_output_dir", "~/Nuggets/")).expanduser() vault_layers_str = kwargs.get("vault_layers", "all") # Determine which types to scan @@ -130,7 +143,9 @@ async def run(self, **kwargs: Any) -> dict[str, Any]: log.info( "Vault discovery: dry_run=%s, layers=%s, nuggets=%s", - dry_run, layers_to_scan, extract_nuggets, + dry_run, + layers_to_scan, + extract_nuggets, ) # Scan each vault type @@ -152,7 +167,8 @@ async def run(self, **kwargs: Any) -> dict[str, Any]: nuggets: list[dict[str, Any]] = [] if extract_nuggets and not dry_run: nuggets = self._extract_nuggets( - vaults, nugget_output_dir, + vaults, + nugget_output_dir, ) report = { @@ -168,13 +184,17 @@ async def run(self, **kwargs: Any) -> dict[str, Any]: log.info( "Vault discovery complete: %d files, %d bytes, %d nuggets", - total_files, total_size, len(nuggets), + total_files, + total_size, + len(nuggets), ) return report def _scan_vault_layer( - self, vault_path: Path, layer: str, + self, + vault_path: Path, + layer: str, ) -> dict[str, Any]: """Scan a single vault type and return stats.""" if not vault_path.exists(): @@ -223,10 +243,7 @@ def _scan_vault_layer( "files": files_count, "size_bytes": size_bytes, "extensions": extensions, - "structure": [ - {"dir": d, "files": c} - for d, c in sorted(structure.items()) - ], + "structure": [{"dir": d, "files": c} for d, c in sorted(structure.items())], } def _scan_ucore_data(self) -> dict[str, Any]: @@ -245,13 +262,16 @@ def _scan_ucore_data(self) -> dict[str, Any]: files = [] for f in sorted(path.rglob("*")): if f.is_file() and ".git" not in f.parts: - files.append({ - "name": f.name, - "size": f.stat().st_size, - "modified": datetime.fromtimestamp( - f.stat().st_mtime, tz=UTC, - ).isoformat(), - }) + files.append( + { + "name": f.name, + "size": f.stat().st_size, + "modified": datetime.fromtimestamp( + f.stat().st_mtime, + tz=UTC, + ).isoformat(), + } + ) results[name] = { "path": str(path), @@ -261,7 +281,7 @@ def _scan_ucore_data(self) -> dict[str, Any]: } # Also scan for spool archives specifically - spool_dir = Path("~/.ucore/logs").expanduser() + spool_dir = settings.logs_dir spool_archives = list(spool_dir.glob("plate_*.spool.json.gz")) results["spool_archives"] = { "path": str(spool_dir), diff --git a/backend/app/skills/builtin/vault_sync.py b/backend/app/skills/builtin/vault_sync.py index ebf69a91..8a169a29 100644 --- a/backend/app/skills/builtin/vault_sync.py +++ b/backend/app/skills/builtin/vault_sync.py @@ -1,13 +1,14 @@ """vault_sync — Rebuild the unified vault library index. Scans ~/Vault (master user vault), ~/Shared, and ~/Public into the -FTS5 index at ~/.ucore/indices/library.db, then reports per-source +FTS5 index at ``$UDOS_HOME/indices/library.db``, then reports per-source file counts. Usage: POST /api/skills/vault_sync/run Body: { "dry_run": false, "summary_only": true } """ + from __future__ import annotations from app.services import library_index diff --git a/backend/app/skills/registry.py b/backend/app/skills/registry.py index d992682b..69130436 100644 --- a/backend/app/skills/registry.py +++ b/backend/app/skills/registry.py @@ -6,6 +6,7 @@ import sys from pathlib import Path +from app.core.settings import settings from app.skills.base import BaseSkill log = logging.getLogger("ucore.skills.registry") @@ -13,16 +14,19 @@ _loaded = False SKILL_PATHS = [ Path(__file__).parent / "builtin", - Path.home() / ".ucore/skills", + settings.udos_home / "skills", ] + def _discover(): skills = {} for sd in SKILL_PATHS: - if not sd.exists(): continue + if not sd.exists(): + continue sys.path.insert(0, str(sd.parent)) for f in sd.iterdir(): - if f.suffix != ".py" or f.name.startswith("_"): continue + if f.suffix != ".py" or f.name.startswith("_"): + continue try: spec = importlib.util.spec_from_file_location(f"skills_{f.stem}", f) if spec and spec.loader: @@ -30,23 +34,40 @@ def _discover(): sys.modules[spec.name] = mod spec.loader.exec_module(mod) for _, obj in inspect.getmembers(mod): - if inspect.isclass(obj) and issubclass(obj, BaseSkill) and obj is not BaseSkill: - inst = obj(); skills[inst.meta.id] = inst + if ( + inspect.isclass(obj) + and issubclass(obj, BaseSkill) + and obj is not BaseSkill + ): + inst = obj() + skills[inst.meta.id] = inst except Exception as e: log.warning(f"Skill load fail {f.name}: {e}") sys.path.pop(0) return skills + def _ensure(): global _registry, _loaded - if not _loaded: _registry = _discover(); _loaded = True + if not _loaded: + _registry = _discover() + _loaded = True + def list_skills() -> list[dict]: _ensure() - return [{"id": s.meta.id, "name": s.meta.name, "description": s.meta.description, - "category": s.meta.category, "timeout": s.meta.timeout, - "requires_confirmation": getattr(s.meta, "requires_confirmation", False), - "category_priority": _get_category_priority(s.meta.category)} for s in _registry.values()] + return [ + { + "id": s.meta.id, + "name": s.meta.name, + "description": s.meta.description, + "category": s.meta.category, + "timeout": s.meta.timeout, + "requires_confirmation": getattr(s.meta, "requires_confirmation", False), + "category_priority": _get_category_priority(s.meta.category), + } + for s in _registry.values() + ] def _get_category_priority(category: str) -> int: @@ -62,12 +83,32 @@ def _get_category_priority(category: str) -> int: } return priorities.get(category, 7) + def get_skill(skill_id: str) -> BaseSkill | None: - _ensure(); return _registry.get(skill_id) + _ensure() + return _registry.get(skill_id) + -async def run_skill_by_id(skill_id: str, **kwargs) -> dict: +async def run_skill_by_id( + skill_id: str, + *, + execution_authorized: bool = False, + **kwargs, +) -> dict: skill = get_skill(skill_id) - if not skill: return {"success": False, "error": f"Skill '{skill_id}' not found"} + if not skill: + return {"success": False, "error": f"Skill '{skill_id}' not found"} + requires_confirmation = getattr( + skill.meta, "requires_confirmation", False + ) or skill.meta.category in ("mutating", "destructive", "write") + if requires_confirmation and not execution_authorized: + return { + "success": False, + "error": "Skill requires explicit execution authorization", + "skill_id": skill_id, + "requires_confirmation": True, + } errors = skill.validate(**kwargs) - if errors: return {"success": False, "errors": errors} + if errors: + return {"success": False, "errors": errors} return await skill.run(**kwargs) diff --git a/backend/app/skills/state.py b/backend/app/skills/state.py index ff548b05..8f40812d 100644 --- a/backend/app/skills/state.py +++ b/backend/app/skills/state.py @@ -2,9 +2,10 @@ import json import time -from pathlib import Path -_STATE_DIR = Path.home() / ".ucore" +from app.core.settings import settings + +_STATE_DIR = settings.udos_home _STATE_FILE = _STATE_DIR / "skill-state.json" diff --git a/backend/app/snacks/templates/snack_template.yaml b/backend/app/snacks/templates/snack_template.yaml index 7218bb2b..1d191229 100644 --- a/backend/app/snacks/templates/snack_template.yaml +++ b/backend/app/snacks/templates/snack_template.yaml @@ -31,4 +31,4 @@ publish: source: github # github, local, mcp repo: uDosGo/uCore # For GitHub publishing path: backend/app/snacks/custom/ # Install path in repo - restore_to: ~/.ucore/snacks/ # User restore location \ No newline at end of file + restore_to: ${UDOS_HOME}/snacks/ # User restore location diff --git a/backend/app/surfaces/documentation_api.py b/backend/app/surfaces/documentation_api.py index eee63e57..498a6999 100644 --- a/backend/app/surfaces/documentation_api.py +++ b/backend/app/surfaces/documentation_api.py @@ -1,4 +1,5 @@ """Documentation Surface API routes for doc site discovery, browsing, and export.""" + from __future__ import annotations import asyncio @@ -9,6 +10,7 @@ from aiohttp import web +from app.core.settings import settings from app.services.doclang_bridge import export_vault_to_doclang_context log = logging.getLogger("ucore.documentation") @@ -21,10 +23,7 @@ # Repos to scan for documentation indexing REPO_DOC_ROOTS: dict[str, Path] = { - "uCore": Path.home() / "Code" / "uCore" / "docs", - "uFlow": Path.home() / "Code" / "uFlow" / "docs", - "uCode": Path.home() / "Code" / "uCode" / "docs", - "uKnowledge": Path.home() / "Code" / "uKnowledge" / "docs", + name: settings.udos_root / name / "docs" for name in ("uCore", "uFlow", "uCode", "uKnowledge") } # Courses frontmatter field extraction @@ -49,13 +48,15 @@ def _list_doc_sites() -> list[dict[str, Any]]: for child in sorted(DOC_SITES_ROOT.iterdir(), key=lambda p: p.name.lower()): if not child.is_dir() or child.name.startswith("."): continue - sites.append({ - "id": child.name, - "name": child.name.replace("-", " ").replace("_", " ").title(), - "path": str(child), - "description": "Published documentation site", - "built": _site_built(child), - }) + sites.append( + { + "id": child.name, + "name": child.name.replace("-", " ").replace("_", " ").title(), + "path": str(child), + "description": "Published documentation site", + "built": _site_built(child), + } + ) return sites @@ -68,11 +69,13 @@ def _list_knowledge_sections() -> list[dict[str, Any]]: if child.name.startswith(".") or child.name.startswith("_"): continue if child.is_dir(): - sections.append({ - "id": child.name, - "name": child.name.replace("-", " ").replace("_", " ").title(), - "path": str(child), - }) + sections.append( + { + "id": child.name, + "name": child.name.replace("-", " ").replace("_", " ").title(), + "path": str(child), + } + ) return sections @@ -83,6 +86,7 @@ def _extract_frontmatter(markdown: str) -> dict[str, Any]: return {} try: import yaml + parsed = yaml.safe_load(match.group(1)) if isinstance(parsed, dict): return parsed @@ -118,11 +122,7 @@ def _list_courses() -> list[dict[str, Any]]: fm = _extract_frontmatter(text) course: dict[str, Any] = { - "name": ( - md_file.stem.replace("-", " ") - .replace("_", " ") - .title() - ), + "name": (md_file.stem.replace("-", " ").replace("_", " ").title()), "path": str(md_file.relative_to(root)), "source": source, "level": fm.get("level", "basic"), @@ -162,14 +162,16 @@ def _list_notebooks() -> list[dict[str, Any]]: notebooks: list[dict[str, Any]] = [] for nb_file in sorted(knowledge_root.rglob("*.ipynb"), key=lambda p: p.name.lower()): stat = nb_file.stat() - notebooks.append({ - "name": nb_file.name, - "stem": nb_file.stem, - "path": str(nb_file.relative_to(knowledge_root)), - "full_path": str(nb_file), - "size": stat.st_size, - "mtime": stat.st_mtime, - }) + notebooks.append( + { + "name": nb_file.name, + "stem": nb_file.stem, + "path": str(nb_file.relative_to(knowledge_root)), + "full_path": str(nb_file), + "size": stat.st_size, + "mtime": stat.st_mtime, + } + ) return notebooks @@ -189,19 +191,23 @@ def _list_repo_docs() -> list[dict[str, Any]]: # Skip archived docs continue rel = str(md_file.relative_to(docs_root)) - docs.append({ - "name": md_file.stem.replace("-", " ").replace("_", " ").title(), - "path": rel, - "size": md_file.stat().st_size, - }) + docs.append( + { + "name": md_file.stem.replace("-", " ").replace("_", " ").title(), + "path": rel, + "size": md_file.stat().st_size, + } + ) if docs: - repos.append({ - "repo": repo_name, - "root": str(docs_root), - "docs": docs, - "count": len(docs), - }) + repos.append( + { + "repo": repo_name, + "root": str(docs_root), + "docs": docs, + "count": len(docs), + } + ) return repos @@ -293,23 +299,27 @@ async def handle_docs_root(_request: web.Request) -> web.Response: async def handle_docs_sites(_request: web.Request) -> web.Response: """GET /api/docs/sites - list discovered documentation sites.""" sites = _list_doc_sites() - return web.json_response({ - "root": str(DOC_SITES_ROOT), - "exists": DOC_SITES_ROOT.exists(), - "sites": sites, - "count": len(sites), - }) + return web.json_response( + { + "root": str(DOC_SITES_ROOT), + "exists": DOC_SITES_ROOT.exists(), + "sites": sites, + "count": len(sites), + } + ) async def handle_docs_global_knowledge(_request: web.Request) -> web.Response: """GET /api/docs/global-knowledge - list knowledge sections.""" sections = _list_knowledge_sections() - return web.json_response({ - "root": str(GLOBAL_KNOWLEDGE_ROOT), - "exists": GLOBAL_KNOWLEDGE_ROOT.exists(), - "sections": sections, - "count": len(sections), - }) + return web.json_response( + { + "root": str(GLOBAL_KNOWLEDGE_ROOT), + "exists": GLOBAL_KNOWLEDGE_ROOT.exists(), + "sections": sections, + "count": len(sections), + } + ) async def handle_docs_serve_site(request: web.Request) -> web.Response: @@ -348,39 +358,47 @@ async def handle_docs_export(request: web.Request) -> web.Response: if result.get("error"): return web.json_response(result, status=400) - return web.json_response({ - "message": "Vault export completed", - **result, - }) + return web.json_response( + { + "message": "Vault export completed", + **result, + } + ) async def handle_docs_courses(_request: web.Request) -> web.Response: """GET /api/docs/courses - list learning courses from ~/Public/learning/.""" courses = _list_courses() - return web.json_response({ - "root": str(LEARNING_ROOT), - "exists": LEARNING_ROOT.exists(), - "courses": courses, - "count": len(courses), - }) + return web.json_response( + { + "root": str(LEARNING_ROOT), + "exists": LEARNING_ROOT.exists(), + "courses": courses, + "count": len(courses), + } + ) async def handle_docs_notebooks(_request: web.Request) -> web.Response: """GET /api/docs/notebooks - list Jupyter notebooks from knowledge directories.""" notebooks = _list_notebooks() - return web.json_response({ - "notebooks": notebooks, - "count": len(notebooks), - }) + return web.json_response( + { + "notebooks": notebooks, + "count": len(notebooks), + } + ) async def handle_docs_repo_docs(_request: web.Request) -> web.Response: """GET /api/docs/repo-docs - index documentation from ~/Code/* repos.""" repo_docs = _list_repo_docs() - return web.json_response({ - "repos": repo_docs, - "count": len(repo_docs), - }) + return web.json_response( + { + "repos": repo_docs, + "count": len(repo_docs), + } + ) async def handle_docs_mirror_sync(_request: web.Request) -> web.Response: @@ -471,8 +489,8 @@ async def handle_docs_publish_status(_request: web.Request) -> web.Response: "learning": LEARNING_ROOT, "vault": Path.home() / "Vault", "knowledge": GLOBAL_KNOWLEDGE_ROOT, - "archive": Path.home() / "Code" / "uCore" / "docs" / "archive", - "mirror": Path.home() / ".ucore" / "docs-mirror", + "archive": settings.udos_root / "uCore" / "docs" / "archive", + "mirror": settings.udos_home / "docs-mirror", } diff --git a/backend/app/surfaces/system_api.py b/backend/app/surfaces/system_api.py index 11da81c2..52d813f1 100644 --- a/backend/app/surfaces/system_api.py +++ b/backend/app/surfaces/system_api.py @@ -12,6 +12,7 @@ import aiohttp from aiohttp import ClientTimeout, web +from app.core.settings import settings from app.utils.config_loader import ( load_service_registry, load_system_pages_registry, @@ -21,12 +22,7 @@ # ─── Settings Store Path ────────────────────────────────────────── -_SETTINGS_STORE_DIR = Path( - os.environ.get( - "UCORE_DATA_DIR", - os.path.expanduser("~/.ucore/data"), - ), -) +_SETTINGS_STORE_DIR = settings.data_dir _SETTINGS_STORE_FILE = _SETTINGS_STORE_DIR / "system_settings.json" @@ -78,6 +74,7 @@ def _save_settings(data: dict) -> None: {"id": "S600", "title": "Help and Recovery", "icon": "help"}, ] + def _get_pages() -> list[dict]: """Load S-pages from config; fall back to built-in defaults.""" return load_system_pages_registry() @@ -96,13 +93,15 @@ async def handle_pages(request: web.Request) -> web.Response: s_pages = _get_pages() page_type = request.query.get("type", "all").lower() pages = s_pages if page_type in ("all", "s") else [] - return web.json_response({ - "pages": pages, - "count": len(pages), - "s_count": len(s_pages), - # Kept for backward compatibility with older frontend payload readers. - "p_count": 0, - }) + return web.json_response( + { + "pages": pages, + "count": len(pages), + "s_count": len(s_pages), + # Kept for backward compatibility with older frontend payload readers. + "p_count": 0, + } + ) # ── Settings (disk-persisted) ─────────────────────────────── async def handle_get_settings(_request: web.Request) -> web.Response: @@ -165,11 +164,7 @@ async def _probe(svc: dict) -> dict: timeout=ClientTimeout(total=timeout), ) as session: async with session.get(url) as resp: - status = ( - "up" - if resp.status in accept_status - else "degraded" - ) + status = "up" if resp.status in accept_status else "degraded" return _build_result(svc, status, resp.status) except Exception: return _build_result(svc, "down", None) @@ -181,14 +176,16 @@ async def _probe(svc: dict) -> dict: degraded = sum(1 for s in results if s["status"] == "degraded") down = sum(1 for s in results if s["status"] == "down") - return web.json_response({ - "services": results, - "count": len(results), - "up": up, - "degraded": degraded, - "down": down, - "health_pct": round((up / max(len(results), 1)) * 100), - }) + return web.json_response( + { + "services": results, + "count": len(results), + "up": up, + "degraded": degraded, + "down": down, + "health_pct": round((up / max(len(results), 1)) * 100), + } + ) app.router.add_get("/api/system/pages", handle_pages) app.router.add_get("/api/system/services", handle_system_services) diff --git a/backend/app/tools/vscode_tool.py b/backend/app/tools/vscode_tool.py deleted file mode 100644 index c952cb3d..00000000 --- a/backend/app/tools/vscode_tool.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Tools: VS Code probe.""" - -from __future__ import annotations - -import asyncio -import logging - -from app.tools.base import BaseTool, ToolInfo - -log = logging.getLogger(__name__) - - -class VSCodeTool(BaseTool): - id = "vscode" - name = "VS Code" - description = "Visual Studio Code editor" - - async def check(self) -> ToolInfo: - """Check whether VS Code is installed and return ToolInfo.""" - info = ToolInfo(id=self.id, name=self.name) - try: - proc = await asyncio.create_subprocess_exec( - "code", - "--version", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - out, _ = await asyncio.wait_for(proc.communicate(), timeout=5) - if proc.returncode == 0: - info.installed = True - lines = out.decode().strip().split("\n") - info.version = lines[0] if lines else "" - except Exception as exc: - log.debug("VSCode check failed: %s", exc) - return info diff --git a/backend/app/utils/config_loader.py b/backend/app/utils/config_loader.py index 20098f1a..250afade 100644 --- a/backend/app/utils/config_loader.py +++ b/backend/app/utils/config_loader.py @@ -1,6 +1,6 @@ """Shared config loader for uCore YAML configuration files. -Reads YAML files from ~/.ucore/config/ with safe fallback defaults. +Reads YAML files from ``$UDOS_HOME/config`` with safe fallback defaults. Used by server.py, system_api.py, and developer_api.py to replace hardcoded policy/service/page arrays. """ @@ -14,6 +14,8 @@ import yaml +from app.core.settings import settings + log = logging.getLogger("ucore.config") @@ -22,7 +24,7 @@ def _config_dir() -> Path: env_dir = os.environ.get("UCORE_CONFIG_DIR") if env_dir: return Path(env_dir).expanduser() - return Path.home() / ".ucore" / "config" + return settings.config_dir def _read_yaml(filename: str) -> dict[str, Any] | None: diff --git a/backend/health/autonomy_engine.py b/backend/health/autonomy_engine.py index 49f2b108..e68cb775 100644 --- a/backend/health/autonomy_engine.py +++ b/backend/health/autonomy_engine.py @@ -3,7 +3,7 @@ Runs every 24 hours (or on demand) to: - Execute ecosystem-audit - Check health thresholds -- Log results to ~/.ucore/logs/ +- Log results to ``$UDOS_HOME/logs`` - Alert if health drops below 95% Usage as cron job: @@ -12,6 +12,7 @@ Usage as one-shot: python -m health.autonomy_engine --once """ + from __future__ import annotations import json @@ -23,7 +24,8 @@ from pathlib import Path from typing import Any -LOG_DIR = Path.home() / ".ucore" / "logs" +UDOS_HOME = Path(os.environ.get("UDOS_HOME", Path.home() / "Code" / ".udos")) +LOG_DIR = UDOS_HOME / "logs" LOG_DIR.mkdir(parents=True, exist_ok=True) AUDIT_LOG = LOG_DIR / "autonomy.log" @@ -41,7 +43,9 @@ log = logging.getLogger("autonomy") -def _call_api(path: str, method: str = "GET", body: dict | None = None, timeout: int = 120) -> dict | None: +def _call_api( + path: str, method: str = "GET", body: dict | None = None, timeout: int = 120 +) -> dict | None: """Call the uCore backend API.""" import urllib.request @@ -161,7 +165,9 @@ def run_full_check() -> dict[str, Any]: } save_state(state) - log.info(f"State saved. Health: {health_pct}% | Ollama: {'online' if ollama.get('online') else 'offline'}") + log.info( + f"State saved. Health: {health_pct}% | Ollama: {'online' if ollama.get('online') else 'offline'}" + ) return state @@ -177,7 +183,9 @@ def main() -> None: parser = argparse.ArgumentParser(description="uCore Autonomy Engine") parser.add_argument("--once", action="store_true", help="Run once and exit") - parser.add_argument("--interval", type=int, default=86400, help="Seconds between checks (default: 24h)") + parser.add_argument( + "--interval", type=int, default=86400, help="Seconds between checks (default: 24h)" + ) args = parser.parse_args() if args.once: diff --git a/backend/health/health_watchdog.py b/backend/health/health_watchdog.py index ae37bdf5..7c4ccaca 100644 --- a/backend/health/health_watchdog.py +++ b/backend/health/health_watchdog.py @@ -15,6 +15,7 @@ UCORE_MENU_LABEL = "com.udos.ucore-menu" UCORE_SERVER_LABEL = "com.udos.ucore-server" UCORE_BACKEND_DIR = os.environ.get("UCORE_BACKEND_DIR", str(Path.home() / "Code" / "uCore" / "backend")) +UDOS_HOME = Path(os.environ.get("UDOS_HOME", Path.home() / "Code" / ".udos")) def check_health(): """Check if snackbar backend is healthy.""" @@ -28,7 +29,7 @@ def check_health(): def check_menu_running(): """Check if uCore menu is running.""" - lockfile = Path.home() / ".ucore" / "ucore-menu.pid" + lockfile = UDOS_HOME / "ucore-menu.pid" if not lockfile.exists(): return False try: diff --git a/backend/mcp/README.md b/backend/mcp/README.md index 8d4267ba..a07103e0 100644 --- a/backend/mcp/README.md +++ b/backend/mcp/README.md @@ -1,13 +1,14 @@ # uCore MCP -Canonical implementation uses one MCP JSON-RPC stdio bridge: +Canonical implementation uses one self-hosted MCP JSON-RPC stdio bridge: - Server id: `ucore-bridge` -- Config source: `.vscode/mcp.json` -- Command: `node ../uDev/mcp-bridge/build/index.js` +- Source: `backend/app/mcp/mcp_bridge/` +- Command: `node backend/app/mcp/mcp_bridge/build/index.js` - Backend target: `UCORE_URL=http://127.0.0.1:8484` -The old multi-manifest MCP layout is deprecated. +Client-specific MCP configuration is external. uCore does not depend on an +editor-owned configuration directory. The old multi-manifest layout is retired. ## Diagnostics @@ -17,7 +18,5 @@ cd backend && python3 -m mcp.mcp_diagnostics This validates: -- `.vscode/mcp.json` exists -- `ucore-bridge` is declared -- no HTTP-type MCP servers are configured -- bridge binary exists at `../uDev/mcp-bridge/build/index.js` +- bridge source and package metadata exist +- the local bridge build exists diff --git a/backend/mcp/mcp_diagnostics.py b/backend/mcp/mcp_diagnostics.py index 1c96d05b..80d38eaa 100644 --- a/backend/mcp/mcp_diagnostics.py +++ b/backend/mcp/mcp_diagnostics.py @@ -1,63 +1,25 @@ -"""MCP diagnostics for the canonical uCore stdio bridge setup. +"""Diagnostics for uCore's self-hosted MCP bridge. -Source of truth: - - Workspace config: .vscode/mcp.json - - Bridge binary: discovered from multiple candidate paths (uDev retired, - bridge may live in uCore itself or a sibling repo) +Client-specific configuration belongs to the external client. uCore owns the +bridge source, its package metadata, and the backend tool registry. """ + from __future__ import annotations import json from pathlib import Path -from typing import Any def _repo_root() -> Path: return Path(__file__).resolve().parents[2] -def _mcp_config_path() -> Path: - return _repo_root() / ".vscode" / "mcp.json" - - -def _load_mcp_config() -> dict[str, Any]: - path = _mcp_config_path() - try: - data = json.loads(path.read_text()) - if not isinstance(data, dict): - return {"error": "mcp_config_not_object", "path": str(path)} - return data - except Exception as exc: - return {"error": f"mcp_config_unreadable: {exc}", "path": str(path)} - - -def _get_servers() -> dict[str, Any]: - data = _load_mcp_config() - if "error" in data: - return {} - servers = data.get("servers", {}) - return servers if isinstance(servers, dict) else {} - - -def list_servers() -> list[dict[str, Any]]: - rows: list[dict[str, Any]] = [] - for name, cfg in _get_servers().items(): - if not isinstance(cfg, dict): - continue - rows.append( - { - "name": name, - "type": cfg.get("type", ""), - "command": cfg.get("command", ""), - "args": cfg.get("args", []), - "cwd": cfg.get("cwd", ""), - } - ) - return rows +def _bridge_root() -> Path: + return _repo_root() / "backend" / "app" / "mcp" / "mcp_bridge" def list_tools() -> list[str]: - """Static tool names exposed by the canonical uCore bridge.""" + """Static tool names exposed by the self-hosted bridge.""" return [ "ucore_ecosystem_audit", "ucore_list_skills", @@ -75,45 +37,19 @@ def list_tools() -> list[str]: ] -def health() -> dict[str, Any]: - repo_root = _repo_root() - mcp_path = _mcp_config_path() - servers = _get_servers() - - has_ucore_bridge = "ucore-bridge" in servers - stale_http = [ - name - for name, cfg in servers.items() - if isinstance(cfg, dict) and cfg.get("type") == "http" - ] - - # Discover bridge binary — uDev has been retired, check multiple candidates. - candidates = [ - repo_root / "bmcp" / "mcp-bridge" / "build" / "index.js", - repo_root.parent / "uDev" / "mcp-bridge" / "build" / "index.js", - ] - bridge_bin = None - for cand in candidates: - if cand.exists(): - bridge_bin = cand - break - if bridge_bin is None: - bridge_bin = candidates[0] # report the first candidate for diagnostics - +def health() -> dict[str, object]: + bridge_root = _bridge_root() checks = { - "mcp_config_exists": mcp_path.exists(), - "ucore_bridge_declared": has_ucore_bridge, - "no_http_servers": len(stale_http) == 0, - "bridge_binary_exists": bridge_bin.exists(), + "bridge_source_exists": (bridge_root / "index.ts").exists(), + "bridge_package_exists": (bridge_root / "package.json").exists(), + "bridge_build_exists": (bridge_root / "build" / "index.js").exists(), } - ok = all(checks.values()) - return { - "health": "ok" if ok else "degraded", + "health": "ok" if all(checks.values()) else "degraded", "checks": checks, - "stale_http_servers": stale_http, - "servers": list_servers(), + "bridge_root": str(bridge_root), "tool_count": len(list_tools()), + "client_configuration": "external", } diff --git a/backend/plate_refresh/models.py b/backend/plate_refresh/models.py index 1acc9db7..7042cd43 100644 --- a/backend/plate_refresh/models.py +++ b/backend/plate_refresh/models.py @@ -3,6 +3,7 @@ Defines PlateMeta, DestroyRebuildConfig, and related types for canonical, versioned, recoverable blueprints. """ + from __future__ import annotations import hashlib @@ -23,7 +24,7 @@ class SpoolArchiveConfig(BaseModel): """ enabled: bool = True - spool_dir: str = "~/.ucore/logs" + spool_dir: str = "${UDOS_HOME}/logs" compress_metadata: bool = True include_source: bool = False include_lessons: bool = True diff --git a/backend/plate_refresh/monitoring.py b/backend/plate_refresh/monitoring.py index 73aab4f3..c5f1dc3e 100644 --- a/backend/plate_refresh/monitoring.py +++ b/backend/plate_refresh/monitoring.py @@ -22,6 +22,7 @@ import json import logging +import os import sqlite3 import time from datetime import UTC, datetime, timedelta @@ -34,7 +35,8 @@ log = logging.getLogger("ucore.plate_refresh.monitoring") ROOT = Path(__file__).resolve().parents[2] -MONITOR_DB = Path.home() / ".ucore" / "plate_monitor.db" +UDOS_HOME = Path(os.environ.get("UDOS_HOME", Path.home() / "Code" / ".udos")) +MONITOR_DB = UDOS_HOME / "plate_monitor.db" # ─── Database Setup ─────────────────────────────────────── diff --git a/backend/plate_refresh/refresh.py b/backend/plate_refresh/refresh.py index d8bc087a..8c404719 100644 --- a/backend/plate_refresh/refresh.py +++ b/backend/plate_refresh/refresh.py @@ -7,11 +7,13 @@ python -m plate_refresh.refresh --destroy skill.recover_port_conflict python -m plate_refresh.refresh --spool-list """ + from __future__ import annotations import gzip import json import logging +import os import shutil import subprocess import time @@ -22,6 +24,7 @@ import yaml +from app.core.settings import settings from plate_refresh.models import ( DriftReport, PlateMeta, @@ -34,12 +37,21 @@ ROOT = Path(__file__).resolve().parents[2] # /Users/fredbook/Code/uCore PLATES_ROOT = ROOT / "plates" BACKUP_ROOT = ROOT / "plates" / ".backups" -EXCLUDE_DIRS = {".git", "node_modules", "__pycache__", ".next", - ".obsidian", ".vscode", ".venv", ".mypy_cache"} +EXCLUDE_DIRS = { + ".git", + "node_modules", + "__pycache__", + ".next", + ".obsidian", + ".vscode", + ".venv", + ".mypy_cache", +} # ─── Plate Discovery ────────────────────────────────────── + def discover_plates() -> dict[str, Path]: """Discover all plate YAML files in plates/ directory.""" plates: dict[str, Path] = {} @@ -72,6 +84,7 @@ def load_plate(plate_id: str) -> tuple[PlateMeta, dict[str, Any], Path] | None: # ─── Rendering ──────────────────────────────────────────── + def render_plates(context: dict[str, str] | None = None) -> list[str]: """Render all plates with context substitution. @@ -110,7 +123,10 @@ def render_plates(context: dict[str, str] | None = None) -> list[str]: new_checksum = meta.compute_checksum(rendered_content) log.info( "Rendered plate %s (v%s) -> %s [checksum: %s]", - plate_id, meta.version, output_path, new_checksum[:12], + plate_id, + meta.version, + output_path, + new_checksum[:12], ) rendered.append(str(output_path)) @@ -119,6 +135,7 @@ def render_plates(context: dict[str, str] | None = None) -> list[str]: # ─── Validation ─────────────────────────────────────────── + def validate_plate(plate_id: str) -> dict[str, Any]: """Validate a plate against its Pydantic schema. @@ -166,6 +183,7 @@ def validate_all_plates() -> list[dict[str, Any]]: # ─── Drift Detection ────────────────────────────────────── + def detect_drift(plate_id: str) -> DriftReport: """Detect drift between a plate and its rendered output. @@ -237,9 +255,11 @@ def detect_all_drift() -> list[DriftReport]: # ─── SPOOL Archive ──────────────────────────────────────── + def _resolve_spool_dir(spool_cfg: SpoolArchiveConfig) -> Path: - """Resolve spool directory, expanding ~ to home.""" - spool_path = Path(spool_cfg.spool_dir).expanduser() + """Resolve spool directory against the detachable uDos runtime home.""" + raw_path = spool_cfg.spool_dir.replace("${UDOS_HOME}", str(settings.udos_home)) + spool_path = Path(os.path.expandvars(raw_path)).expanduser() spool_path.mkdir(parents=True, exist_ok=True) return spool_path @@ -255,7 +275,7 @@ def write_spool_archive( ) -> str | None: """Compress a destroyed component's essence into an MCP-formatted SPOOL record. - The SPOOL record is a gzip-compressed JSON blob written to ~/.ucore/logs/ + The SPOOL record is a gzip-compressed JSON blob written to $UDOS_HOME/logs/ that preserves: - Plate metadata (id, version, domain, description) - Salvaged state (keys preserved from corruption) @@ -311,9 +331,7 @@ def write_spool_archive( } # Compress to gzip - spool_filename = ( - f"plate_{plate_id}_{now.strftime('%Y%m%d_%H%M%S')}.spool.json.gz" - ) + spool_filename = f"plate_{plate_id}_{now.strftime('%Y%m%d_%H%M%S')}.spool.json.gz" spool_path = spool_dir / spool_filename with gzip.open(spool_path, "wt", encoding="utf-8") as f: @@ -329,7 +347,7 @@ def write_spool_archive( def list_spool_archives(domain: str | None = None) -> list[dict[str, Any]]: - """List all SPOOL archive records in ~/.ucore/logs/. + """List all SPOOL archive records in ``$UDOS_HOME/logs``. Args: domain: Optional domain filter (skill, snack, mcp, etc.) @@ -337,7 +355,7 @@ def list_spool_archives(domain: str | None = None) -> list[dict[str, Any]]: Returns: List of spool record summaries """ - spool_dir = Path("~/.ucore/logs").expanduser() + spool_dir = settings.logs_dir if not spool_dir.exists(): return [] @@ -348,16 +366,18 @@ def list_spool_archives(domain: str | None = None) -> list[dict[str, Any]]: record = json.load(fh) if domain and record.get("plate", {}).get("domain") != domain: continue - archives.append({ - "file": str(f), - "timestamp": record.get("timestamp", ""), - "plate_id": record.get("plate", {}).get("id", ""), - "version": record.get("plate", {}).get("version", ""), - "domain": record.get("plate", {}).get("domain", ""), - "salvaged_keys": list(record.get("salvaged", {}).keys()), - "lesson_count": len(record.get("lessons", [])), - "has_errors": len(record.get("errors", [])) > 0, - }) + archives.append( + { + "file": str(f), + "timestamp": record.get("timestamp", ""), + "plate_id": record.get("plate", {}).get("id", ""), + "version": record.get("plate", {}).get("version", ""), + "domain": record.get("plate", {}).get("domain", ""), + "salvaged_keys": list(record.get("salvaged", {}).keys()), + "lesson_count": len(record.get("lessons", [])), + "has_errors": len(record.get("errors", [])) > 0, + } + ) except Exception as exc: log.warning("Failed to read spool archive %s: %s", f, exc) @@ -387,6 +407,7 @@ def read_spool_archive(spool_path: str) -> dict[str, Any] | None: # ─── DESTROY/REBUILD Protocol ───────────────────────────── + def destroy_and_rebuild( plate_id: str, salvage_state: dict[str, Any] | None = None, @@ -434,9 +455,7 @@ def destroy_and_rebuild( if meta.destroy.backup_before_destroy: backup_dir = BACKUP_ROOT / meta.domain backup_dir.mkdir(parents=True, exist_ok=True) - backup_name = ( - f"{plate_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.yaml" - ) + backup_name = f"{plate_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.yaml" backup_path = str(backup_dir / backup_name) shutil.copy2(str(path), backup_path) log.info("Backed up %s to %s", plate_id, backup_path) @@ -459,7 +478,11 @@ def destroy_and_rebuild( for k, v in salvaged.items(): cmd = cmd.replace(f"${{{k}}}", str(v)) result_proc = subprocess.run( - cmd, shell=True, capture_output=True, text=True, timeout=120, + cmd, + shell=True, + capture_output=True, + text=True, + timeout=120, ) rebuild_output = result_proc.stdout if result_proc.returncode != 0: @@ -476,7 +499,7 @@ def destroy_and_rebuild( errors.append(f"Validation after rebuild: {err}") # Write SPOOL archive (after rebuild so we can include rebuild_output) - spool_path = write_spool_archive( + write_spool_archive( plate_id=plate_id, meta=meta, raw=raw, @@ -489,7 +512,8 @@ def destroy_and_rebuild( success = len(errors) == 0 log.info( "DESTROY/REBUILD for %s: %s", - plate_id, "SUCCESS" if success else "FAILED", + plate_id, + "SUCCESS" if success else "FAILED", ) return RebuildResult( @@ -513,8 +537,8 @@ def destroy_and_rebuild( } UCORE_DIRS = { - "config": Path("~/.ucore/").expanduser(), - "logs": Path("~/.ucore/logs/").expanduser(), + "config": settings.config_dir, + "logs": settings.logs_dir, "plates": ROOT / "plates", } @@ -533,12 +557,13 @@ def _discover_vaults() -> dict[str, Any]: count = 0 for dirpath, dirnames, filenames in os.walk(path): - dirnames[:] = [d for d in dirnames - if d not in EXCLUDE_DIRS] + dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS] count += len(filenames) vaults[layer] = { - "path": str(path), "exists": True, "files": count, + "path": str(path), + "exists": True, + "files": count, } total_files += count @@ -551,10 +576,11 @@ def _discover_vaults() -> dict[str, Any]: ucore_data[name] = {"path": str(path), "files": count} # Spool archives - spool_dir = Path("~/.ucore/logs").expanduser() + spool_dir = settings.logs_dir spool_count = len(list(spool_dir.glob("plate_*.spool.json.gz"))) ucore_data["spool_archives"] = { - "path": str(spool_dir), "count": spool_count, + "path": str(spool_dir), + "count": spool_count, } return { @@ -585,6 +611,7 @@ def _destroy_user_vault() -> RebuildResult: backup_path = str(backup_dir / backup_name) import tarfile + try: with tarfile.open(backup_path, "w:gz") as tar: tar.add(vault_path, arcname="Vault") @@ -603,18 +630,19 @@ def _destroy_user_vault() -> RebuildResult: }, "errors": errors, } - spool_dir = Path("~/.ucore/logs").expanduser() + spool_dir = settings.logs_dir spool_dir.mkdir(parents=True, exist_ok=True) spool_path = spool_dir / ( - f"vault_user_{datetime.now().strftime('%Y%m%d_%H%M%S')}" - ".spool.json.gz" + f"vault_user_{datetime.now().strftime('%Y%m%d_%H%M%S')}.spool.json.gz" ) import gzip + with gzip.open(spool_path, "wt", encoding="utf-8") as f: json.dump(spool_record, f, indent=2, default=str) # Remove vault contents (keep directory) import shutil + for item in vault_path.iterdir(): try: if item.is_dir(): @@ -631,8 +659,7 @@ def _destroy_user_vault() -> RebuildResult: errors=errors, backup_path=backup_path, rebuild_output=( - f"User vault destroyed. Backup at {backup_path}. " - f"SPOOL archive at {spool_path}." + f"User vault destroyed. Backup at {backup_path}. SPOOL archive at {spool_path}." ), ) @@ -646,23 +673,23 @@ def _destroy_installation() -> RebuildResult: if not vault_result.success: errors.extend(vault_result.errors) - # 2. Backup and remove ~/.ucore/ - ucore_dir = Path("~/.ucore/").expanduser() + # 2. Backup and remove the detachable uDos runtime home. + ucore_dir = settings.udos_home if ucore_dir.exists(): backup_dir = BACKUP_ROOT / "installation" backup_dir.mkdir(parents=True, exist_ok=True) - backup_name = ( - f"ucore_install_{datetime.now().strftime('%Y%m%d_%H%M%S')}.tar.gz" - ) + backup_name = f"ucore_install_{datetime.now().strftime('%Y%m%d_%H%M%S')}.tar.gz" backup_path = str(backup_dir / backup_name) import tarfile + try: with tarfile.open(backup_path, "w:gz") as tar: - tar.add(ucore_dir, arcname=".ucore") + tar.add(ucore_dir, arcname=".udos") import shutil + shutil.rmtree(ucore_dir) except Exception as exc: - errors.append(f"Failed to backup/remove ~/.ucore/: {exc}") + errors.append(f"Failed to backup/remove {ucore_dir}: {exc}") # 3. Write final SPOOL archive spool_record = { @@ -670,13 +697,13 @@ def _destroy_installation() -> RebuildResult: "timestamp": datetime.now(timezone.utc).isoformat(), "errors": errors, } - spool_dir = Path("~/.ucore/logs").expanduser() + spool_dir = settings.logs_dir spool_dir.mkdir(parents=True, exist_ok=True) spool_path = spool_dir / ( - f"installation_destroy_{datetime.now().strftime('%Y%m%d_%H%M%S')}" - ".spool.json.gz" + f"installation_destroy_{datetime.now().strftime('%Y%m%d_%H%M%S')}.spool.json.gz" ) import gzip + with gzip.open(spool_path, "wt", encoding="utf-8") as f: json.dump(spool_record, f, indent=2, default=str) @@ -685,14 +712,13 @@ def _destroy_installation() -> RebuildResult: plate_id="installation", success=success, errors=errors, - rebuild_output=( - f"Installation destroyed. SPOOL archive at {spool_path}." - ), + rebuild_output=(f"Installation destroyed. SPOOL archive at {spool_path}."), ) # ─── Interactive DESTROY Menu ──────────────────────────── + def interactive_destroy(): """Present interactive DESTROY/REBUILD options to the user.""" print() @@ -707,13 +733,13 @@ def interactive_destroy(): print() for layer, stats in discovery["vaults"].items(): status = "EXISTS" if stats["exists"] else "NOT FOUND" - print(f" {layer:8s} -> {stats['path']:<35s} [{status}] " - f"{stats['files']} files") + print(f" {layer:8s} -> {stats['path']:<35s} [{status}] {stats['files']} files") print() print(" uCore data:") for name, stats in discovery["ucore_data"].items(): - print(f" {name:15s}: {stats['path']} " - f"({stats.get('files', stats.get('count', 0))} files)") + print( + f" {name:15s}: {stats['path']} ({stats.get('files', stats.get('count', 0))} files)" + ) print() print(f" Total: {discovery['total_files']} files across all vaults") print() @@ -722,13 +748,10 @@ def interactive_destroy(): print("Select an option:") print() print(" [1] Dry-run only (identify everything, no changes)") - print(" [2] Destroy & Rebuild (reset corrupted components, " - "keep user data)") - print(" [3] Destroy User Data Only (remove ~/Vault/, " - "keep installation)") + print(" [2] Destroy & Rebuild (reset corrupted components, keep user data)") + print(" [3] Destroy User Data Only (remove ~/Vault/, keep installation)") print(" [4] Destroy Installation & Data (complete uninstall)") - print(" [5] Break off Nuggets (extract reusable components, " - "then rebuild)") + print(" [5] Break off Nuggets (extract reusable components, then rebuild)") print(" [6] Cancel") print() @@ -744,10 +767,14 @@ def interactive_destroy(): elif choice == "2": print("\n[2] Destroy & Rebuild selected.") - confirm = input( - "This will reset corrupted components. " - "User data will be preserved. Continue? [y/N]: " - ).strip().lower() + confirm = ( + input( + "This will reset corrupted components. " + "User data will be preserved. Continue? [y/N]: " + ) + .strip() + .lower() + ) if confirm != "y": print("Cancelled.") return @@ -762,10 +789,14 @@ def interactive_destroy(): elif choice == "3": print("\n[3] Destroy User Data Only selected.") - confirm = input( - "This will remove all contents of ~/Vault/ " - "(backup will be created). Continue? [y/N]: " - ).strip().lower() + confirm = ( + input( + "This will remove all contents of ~/Vault/ " + "(backup will be created). Continue? [y/N]: " + ) + .strip() + .lower() + ) if confirm != "y": print("Cancelled.") return @@ -777,16 +808,18 @@ def interactive_destroy(): elif choice == "4": print("\n[4] Destroy Installation & Data selected.") - confirm = input( - "WARNING: This will remove ALL user data and the " - "uCore installation. Continue? [y/N]: " - ).strip().lower() + confirm = ( + input( + "WARNING: This will remove ALL user data and the " + "uCore installation. Continue? [y/N]: " + ) + .strip() + .lower() + ) if confirm != "y": print("Cancelled.") return - confirm2 = input( - "Type 'DESTROY' to confirm complete uninstall: " - ).strip() + confirm2 = input("Type 'DESTROY' to confirm complete uninstall: ").strip() if confirm2 != "DESTROY": print("Cancelled.") return @@ -798,10 +831,11 @@ def interactive_destroy(): elif choice == "5": print("\n[5] Break off Nuggets selected.") - confirm = input( - "Extract reusable components from vaults as Nuggets? " - "Continue? [y/N]: " - ).strip().lower() + confirm = ( + input("Extract reusable components from vaults as Nuggets? Continue? [y/N]: ") + .strip() + .lower() + ) if confirm != "y": print("Cancelled.") return @@ -810,13 +844,17 @@ def interactive_destroy(): from app.skills.builtin.skill_vault_discovery import ( VaultDiscoverySkill, ) + skill = VaultDiscoverySkill() import asyncio - result = asyncio.run(skill.run( - dry_run=False, - extract_nuggets=True, - nugget_output_dir="~/Nuggets/", - )) + + result = asyncio.run( + skill.run( + dry_run=False, + extract_nuggets=True, + nugget_output_dir="~/Nuggets/", + ) + ) print(f"\nExtracted {len(result['nuggets'])} Nuggets:") for n in result["nuggets"]: print(f" {n['id']}: {n['source']} ({n['files']} files)") @@ -832,48 +870,67 @@ def interactive_destroy(): # ─── CLI ────────────────────────────────────────────────── + def main(): import argparse parser = argparse.ArgumentParser( - description="Plate Refresh Engine -- Render, validate, " - "detect drift, rebuild", + description="Plate Refresh Engine -- Render, validate, detect drift, rebuild", ) parser.add_argument("--render", action="store_true", help="Render all plates") parser.add_argument( - "--validate", type=str, nargs="?", const="all", default=None, + "--validate", + type=str, + nargs="?", + const="all", + default=None, help="Validate a specific plate or 'all'", ) parser.add_argument( - "--drift-detect", type=str, nargs="?", const="all", default=None, + "--drift-detect", + type=str, + nargs="?", + const="all", + default=None, help="Detect drift for a plate or 'all'", ) parser.add_argument( - "--destroy", type=str, default=None, + "--destroy", + type=str, + default=None, help="DESTROY/REBUILD a specific plate", ) parser.add_argument( - "--destroy-interactive", action="store_true", + "--destroy-interactive", + action="store_true", help="Interactive DESTROY menu with vault discovery", ) parser.add_argument( - "--list", action="store_true", help="List all discovered plates", + "--list", + action="store_true", + help="List all discovered plates", ) parser.add_argument( - "--spool-list", action="store_true", + "--spool-list", + action="store_true", help="List all SPOOL archive records", ) parser.add_argument( - "--spool-read", type=str, default=None, + "--spool-read", + type=str, + default=None, help="Read and decompress a SPOOL archive file", ) parser.add_argument( - "--spool-domain", type=str, default=None, + "--spool-domain", + type=str, + default=None, help="Filter spool list by domain", ) # Add verification and monitoring arguments from plate_refresh.monitoring import add_monitoring_args from plate_refresh.verification import add_verification_args + add_verification_args(parser) add_monitoring_args(parser) @@ -963,6 +1020,7 @@ def main(): # Handle verification and monitoring commands from plate_refresh.monitoring import handle_monitoring_commands from plate_refresh.verification import handle_verification_commands + handle_verification_commands(args) handle_monitoring_commands(args) diff --git a/backend/plate_refresh/verification.py b/backend/plate_refresh/verification.py index eb8c1b71..dbed964f 100644 --- a/backend/plate_refresh/verification.py +++ b/backend/plate_refresh/verification.py @@ -22,6 +22,7 @@ version="1.1.0", ) """ + from __future__ import annotations import json @@ -250,8 +251,7 @@ def _check_dogfooding(plate_id: str, meta: PlateMeta) -> dict[str, Any]: continue # Built-in render variables if var not in meta.destroy.salvage_keys: warnings.append( - f"Rebuild command uses variable '${{{var}}}' " - f"but it's not in salvage_keys" + f"Rebuild command uses variable '${{{var}}}' but it's not in salvage_keys" ) # Check the command references exist @@ -262,9 +262,7 @@ def _check_dogfooding(plate_id: str, meta: PlateMeta) -> dict[str, Any]: template_path = match.group(1) resolved = Path(template_path).expanduser() if not resolved.exists(): - warnings.append( - f"Cookiecutter template not found: {template_path}" - ) + warnings.append(f"Cookiecutter template not found: {template_path}") return { "name": "dogfooding", @@ -366,13 +364,15 @@ def verify_plate( pass_rate=0.0, total_checks=1, passed_checks=0, - checks=[{ - "name": "plate_exists", - "description": "Check that the plate exists", - "passed": False, - "errors": [f"Plate '{plate_id}' not found"], - "warnings": [], - }], + checks=[ + { + "name": "plate_exists", + "description": "Check that the plate exists", + "passed": False, + "errors": [f"Plate '{plate_id}' not found"], + "warnings": [], + } + ], errors=[f"Plate '{plate_id}' not found"], warnings=[], duration_seconds=time.time() - start, @@ -474,7 +474,6 @@ def promote_plate( Returns: PromotionResult with promotion status """ - errors: list[str] = [] source_path = Path(source).expanduser() target_path = Path(target).expanduser() @@ -553,7 +552,9 @@ def promote_plate( log.info( "Promoted %s -> %s (v%s)", - source, target_path, version, + source, + target_path, + version, ) return PromotionResult( @@ -605,6 +606,7 @@ def _build_plate_from_source( try: content = source_path.read_text() import hashlib + checksum = hashlib.sha256(content.encode("utf-8")).hexdigest() except Exception: pass @@ -621,19 +623,22 @@ def _build_plate_from_source( "lessons": base.get("lessons", []), "created": base.get("created", datetime.now(UTC).isoformat()), "updated": datetime.now(UTC).isoformat(), - "destroy": base.get("destroy", { - "salvage_keys": [], - "rebuild_command": "", - "backup_before_destroy": True, - "spool_archive": { - "enabled": True, - "spool_dir": "~/.ucore/logs", - "compress_metadata": True, - "include_source": False, - "include_lessons": True, - "max_spool_age_days": 365, + "destroy": base.get( + "destroy", + { + "salvage_keys": [], + "rebuild_command": "", + "backup_before_destroy": True, + "spool_archive": { + "enabled": True, + "spool_dir": "${UDOS_HOME}/logs", + "compress_metadata": True, + "include_source": False, + "include_lessons": True, + "max_spool_age_days": 365, + }, }, - }), + ), } return plate_data @@ -645,27 +650,38 @@ def _build_plate_from_source( def add_verification_args(parser: Any) -> None: """Add verification and promotion arguments to an argparse parser.""" parser.add_argument( - "--verify", type=str, nargs="?", const="all", default=None, + "--verify", + type=str, + nargs="?", + const="all", + default=None, help="Verify a specific plate or 'all'", ) parser.add_argument( - "--verify-no-dogfooding", action="store_true", + "--verify-no-dogfooding", + action="store_true", help="Skip dogfooding checks during verification", ) parser.add_argument( - "--verify-no-security", action="store_true", + "--verify-no-security", + action="store_true", help="Skip security checks during verification", ) parser.add_argument( - "--promote", type=str, default=None, + "--promote", + type=str, + default=None, help="Promote a source file to a plate. Format: source=target", ) parser.add_argument( - "--promote-version", type=str, default="1.0.0", + "--promote-version", + type=str, + default="1.0.0", help="Version for promoted plate (default: 1.0.0)", ) parser.add_argument( - "--promote-force", action="store_true", + "--promote-force", + action="store_true", help="Skip verification and force promotion", ) @@ -682,17 +698,18 @@ def handle_verification_commands(args: Any) -> None: run_security=run_security, ) else: - results = [verify_plate( - args.verify, - run_dogfooding=run_dogfooding, - run_security=run_security, - )] + results = [ + verify_plate( + args.verify, + run_dogfooding=run_dogfooding, + run_security=run_security, + ) + ] for r in results: status = "PASSED" if r.passed else "FAILED" print(f"\n {r.plate_id} (v{r.version}) [{r.domain}]: {status}") - print(f" Pass rate: {r.pass_rate:.0%} " - f"({r.passed_checks}/{r.total_checks})") + print(f" Pass rate: {r.pass_rate:.0%} ({r.passed_checks}/{r.total_checks})") print(f" Duration: {r.duration_seconds:.2f}s") for c in r.checks: c_status = "PASS" if c["passed"] else "FAIL" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 09233b97..5b00a028 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -17,10 +17,6 @@ dependencies = [ "aiohttp>=3.9,<4.0", "pydantic>=2.0", "pyyaml>=6.0", - "snackmachine>=1.0", - "udos-budget>=1.0", - "udos-agents>=1.0", - "udos-identity>=1.0", "litellm>=1.0", "langgraph>=1.0", "psutil>=5.9", @@ -35,8 +31,8 @@ dev = [ ] [tool.setuptools.packages.find] -where = ["app"] -include = ["app*", "app.models*", "app.core*", "app.api*", "app.services*", "app.surfaces*"] +where = ["."] +include = ["app*", "snackmachine*"] [tool.pytest.ini_options] testpaths = ["tests"] @@ -51,4 +47,3 @@ ignore = [ "E501", # line too long - handled by formatter "F401", # unused imports - handled by isort ] - diff --git a/backend/schemas/activity.schema.sql b/backend/schemas/activity.schema.sql index 0e13b9fc..1e2db48e 100644 --- a/backend/schemas/activity.schema.sql +++ b/backend/schemas/activity.schema.sql @@ -1,5 +1,5 @@ -- Activity Pod Schema - uCore Feed System (Pod/Nugget/Seed/Slate/Spool) --- Stored at: ~/.ucore/pods/activity.db +-- Stored at: $UDOS_HOME/pods/activity.db CREATE TABLE IF NOT EXISTS user_activity ( id INTEGER PRIMARY KEY AUTOINCREMENT, diff --git a/backend/seeds/feed-pod-seed.json b/backend/seeds/feed-pod-seed.json index 17ffd44b..26e8fcb8 100644 --- a/backend/seeds/feed-pod-seed.json +++ b/backend/seeds/feed-pod-seed.json @@ -1,7 +1,7 @@ { "name": "feed-pod", "version": "1.0.0", - "schema": "~/.ucore/schemas/activity.schema.sql", + "schema": "${UDOS_HOME}/schemas/activity.schema.sql", "sources": { "browser": { "enabled": true, @@ -57,4 +57,4 @@ "suggestion_min_confidence": 0.5, "activity_retention_days": 90 } -} \ No newline at end of file +} diff --git a/backend/snackmachine/cli.py b/backend/snackmachine/cli.py index 99452279..31145d99 100644 --- a/backend/snackmachine/cli.py +++ b/backend/snackmachine/cli.py @@ -7,12 +7,14 @@ import os from pathlib import Path +from app.core.settings import settings + def main() -> None: parser = argparse.ArgumentParser(description="SnackMachine CLI") sub = parser.add_subparsers(dest="command") - init_parser = sub.add_parser("init", help="Seed ~/.ucore/ with default configs") + init_parser = sub.add_parser("init", help="Seed $UDOS_HOME with default configs") init_parser.add_argument("--force", action="store_true", help="Overwrite existing") sub.add_parser("serve", help="Start snackmachine daemon (requires uCore)") @@ -34,8 +36,8 @@ def main() -> None: def cmd_init(force: bool = False) -> None: - """Seed ~/.ucore/ with default snackmachine config.""" - data_dir = Path(os.environ.get("SNACKMACHINE_DATA_DIR", Path.home() / ".ucore")) + """Seed the canonical uDos runtime home with SnackMachine config.""" + data_dir = Path(os.environ.get("SNACKMACHINE_DATA_DIR", settings.udos_home)) dirs = [ data_dir / "config" / "mcp-manifests", data_dir / "indices", @@ -62,32 +64,35 @@ def cmd_init(force: bool = False) -> None: print(f"\n✅ snackmachine initialized at {data_dir}") print(" Run 'snackmachine index' to build the FTS5 search index.") - print(" Drop snacks into ~/.ucore/snacks/ to auto-discover them.") - - -MCP_KNOWLEDGE_MANIFEST = json.dumps({ - "name": "mcp-knowledge-conduit", - "version": "1.0.0", - "description": "Knowledge conduit: vault search, AI-ranked retrieval, doclang", - "tools": [ - "knowledge_search", - "knowledge_ask", - "knowledge_list_sources", - "knowledge_extract_links", - "knowledge_summarize", - "knowledge_publish", - "knowledge_query_memory", - ], - "transport": "http", - "health_check": "/api/mcp/status", - "protocolVersion": "0.1.0", - "serverInfo": {"name": "SnackMachine Knowledge Conduit", "version": "1.0.0"}, -}, indent=2) + print(f" Drop snacks into {data_dir / 'snacks'} to auto-discover them.") + + +MCP_KNOWLEDGE_MANIFEST = json.dumps( + { + "name": "mcp-knowledge-conduit", + "version": "1.0.0", + "description": "Knowledge conduit: vault search, AI-ranked retrieval, doclang", + "tools": [ + "knowledge_search", + "knowledge_ask", + "knowledge_list_sources", + "knowledge_extract_links", + "knowledge_summarize", + "knowledge_publish", + "knowledge_query_memory", + ], + "transport": "http", + "health_check": "/api/mcp/status", + "protocolVersion": "0.1.0", + "serverInfo": {"name": "SnackMachine Knowledge Conduit", "version": "1.0.0"}, + }, + indent=2, +) SNACKMACHINE_CONFIG = """\ # SnackMachine config -# Drop .py snacks into ~/.ucore/snacks/ — they auto-discover +# Drop .py snacks into $UDOS_HOME/snacks — they auto-discover scheduler: interval: 60 # seconds between job checks @@ -98,12 +103,12 @@ def cmd_init(force: bool = False) -> None: time: "*/2 * * * *" # every 2 hours spool: - dir: ~/.ucore/logs + dir: ${UDOS_HOME}/logs max_items: 1000 max_days: 30 indices: - dir: ~/.ucore/indices + dir: ${UDOS_HOME}/indices vault_layers: - user: ~/Vault - shared: ~/Shared diff --git a/backend/snackmachine/scheduler.py b/backend/snackmachine/scheduler.py index 63dd06b0..016a30a2 100644 --- a/backend/snackmachine/scheduler.py +++ b/backend/snackmachine/scheduler.py @@ -12,7 +12,7 @@ from snackmachine.registry import get_registry as _get_registry log = logging.getLogger("snackmachine.scheduler") -DATA_DIR = Path(os.environ.get("SNACKMACHINE_DATA_DIR", Path.home() / ".ucore")) +DATA_DIR = Path(os.environ.get("SNACKMACHINE_DATA_DIR", settings.udos_home)) def run_skill_by_id(skill_id: str, **kw): @@ -158,9 +158,7 @@ async def run_due(self, now: datetime | None = None) -> list[dict]: if not self._is_due(job, now): continue result = await run_skill_by_id(job.skill_id, **job.params) - self._state.setdefault("last_run", {})[ - job.skill_id - ] = now.date().isoformat() + self._state.setdefault("last_run", {})[job.skill_id] = now.date().isoformat() self._state.setdefault("last_result", {})[job.skill_id] = { "success": result.get("success", False), "timestamp": now.isoformat(), diff --git a/backend/snackmachine/spool_reader.py b/backend/snackmachine/spool_reader.py index dc131b0a..fe31ea81 100644 --- a/backend/snackmachine/spool_reader.py +++ b/backend/snackmachine/spool_reader.py @@ -1,11 +1,12 @@ """spool_reader — Unified activity feed reader for uCore logs. -Reads from ~/.ucore/logs/*.log and parses structured log entries into +Reads from ``$UDOS_HOME/logs/*.log`` and parses structured log entries into a queryable activity feed. Supports real-time watching, filtering, and search for the clipboard popover Logs tab and brain_sync synthesis. Spec: docs/SPOOL_SPEC.md """ + from __future__ import annotations import os @@ -16,6 +17,8 @@ from pathlib import Path from typing import Any +from app.core.settings import settings + def _get_identity() -> dict[str, str]: return { @@ -23,7 +26,8 @@ def _get_identity() -> dict[str, str]: "session_id": socket.gethostname(), } -LOG_DIR = Path.home() / ".ucore" / "logs" + +LOG_DIR = settings.logs_dir LOG_PATTERNS = ("*.log",) # Identity cache — refreshed once per session @@ -72,7 +76,9 @@ def to_dict(self) -> dict[str, Any]: return asdict(self) -def discover_log_files(log_dir: str | Path | None = None, patterns: tuple[str, ...] = LOG_PATTERNS) -> list[Path]: +def discover_log_files( + log_dir: str | Path | None = None, patterns: tuple[str, ...] = LOG_PATTERNS +) -> list[Path]: log_dir = Path(log_dir or LOG_DIR) if not log_dir.exists(): return [] @@ -105,7 +111,7 @@ def parse_line(line: str, source: str = "unknown") -> SpoolEntry | None: module = "unknown" message = line if ts_match and level_match: - after_level = line[level_match.end():].strip() + after_level = line[level_match.end() :].strip() mod_match = MODULE_RE.match(after_level) if mod_match: module = mod_match.group(1) @@ -136,17 +142,27 @@ def parse_line(line: str, source: str = "unknown") -> SpoolEntry | None: # Attach UDOS identity identity = _get_udos_identity() return SpoolEntry( - timestamp=ts, level=level, source=source, module=module, - message=message, raw=line, tags=tags, + timestamp=ts, + level=level, + source=source, + module=module, + message=message, + raw=line, + tags=tags, user_id=identity.get("user_id", ""), session_id=identity.get("session_id", ""), ) -def read_spool(log_dir: str | Path | None = None, max_entries: int = 500, - levels: list[str] | None = None, modules: list[str] | None = None, - search: str | None = None, since: str | None = None, - errors_only: bool = False) -> list[SpoolEntry]: +def read_spool( + log_dir: str | Path | None = None, + max_entries: int = 500, + levels: list[str] | None = None, + modules: list[str] | None = None, + search: str | None = None, + since: str | None = None, + errors_only: bool = False, +) -> list[SpoolEntry]: log_dir = Path(log_dir or LOG_DIR) files = discover_log_files(log_dir) entries: list[SpoolEntry] = [] @@ -169,7 +185,11 @@ def read_spool(log_dir: str | Path | None = None, max_entries: int = 500, entries = [e for e in entries if e.module in modules] if search: search_lower = search.lower() - entries = [e for e in entries if search_lower in e.message.lower() or search_lower in e.module.lower()] + entries = [ + e + for e in entries + if search_lower in e.message.lower() or search_lower in e.module.lower() + ] if since: entries = [e for e in entries if e.timestamp >= since] return entries[:max_entries] @@ -177,6 +197,7 @@ def read_spool(log_dir: str | Path | None = None, max_entries: int = 500, def summarize_spool(log_dir: str | Path | None = None, hours: int = 24, max_lines: int = 30) -> str: from datetime import timedelta + cutoff = (datetime.now(UTC) - timedelta(hours=hours)).isoformat() entries = read_spool(log_dir, max_entries=500, since=cutoff) if not entries: @@ -192,8 +213,12 @@ def summarize_spool(log_dir: str | Path | None = None, hours: int = 24, max_line by_module.setdefault(e.module, []).append(e) lines: list[str] = [ f"## Spool Activity (last {hours}h)", - "", f"Total entries: {len(entries)}", f"Errors: {len(errors)}", - f"Warnings: {len(warnings)}", "", "### By Module", + "", + f"Total entries: {len(entries)}", + f"Errors: {len(errors)}", + f"Warnings: {len(warnings)}", + "", + "### By Module", ] for module, mod_entries in sorted(by_module.items(), key=lambda x: len(x[1]), reverse=True): errors_in_mod = sum(1 for e in mod_entries if e.is_error) diff --git a/backend/snackmachine/spool_writer.py b/backend/snackmachine/spool_writer.py index 88b8087c..f7065c2f 100644 --- a/backend/snackmachine/spool_writer.py +++ b/backend/snackmachine/spool_writer.py @@ -5,12 +5,14 @@ Spec: docs/SPOOL_SPEC.md """ + from __future__ import annotations from datetime import UTC, datetime -from pathlib import Path -LOG_DIR = Path.home() / ".ucore" / "logs" +from app.core.settings import settings + +LOG_DIR = settings.logs_dir def write_spool( diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 9eb2266d..46450175 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -1,10 +1,43 @@ """uCore test configuration.""" + from __future__ import annotations import sys from pathlib import Path +import pytest + # Ensure backend/ is on sys.path so `from app import ...` works BACKEND_DIR = Path(__file__).resolve().parent.parent if str(BACKEND_DIR) not in sys.path: sys.path.insert(0, str(BACKEND_DIR)) + + +@pytest.fixture(scope="session", autouse=True) +def register_test_hello_world_skill(): + """Provide the documented hello-world API fixture without shipping it.""" + from app.skills import registry + from app.skills.base import BaseSkill, SkillMeta, SkillParam + + class HelloWorldTestSkill(BaseSkill): + meta = SkillMeta( + id="hello-world", + name="Hello World test fixture", + category="general", + params=[SkillParam(name="name", required=False, default="World")], + ) + + async def run(self, **kwargs) -> dict: + return { + "success": True, + "message": f"Hello, {kwargs.get('name', 'World')}!", + } + + previous_registry = registry._registry + previous_loaded = registry._loaded + registry._registry = registry._discover() + registry._registry["hello-world"] = HelloWorldTestSkill() + registry._loaded = True + yield + registry._registry = previous_registry + registry._loaded = previous_loaded diff --git a/backend/tests/e2e_playwright.py b/backend/tests/e2e_playwright.py index 5161c130..712497c7 100644 --- a/backend/tests/e2e_playwright.py +++ b/backend/tests/e2e_playwright.py @@ -12,6 +12,7 @@ pytest tests/e2e_playwright.py::test_backend_health -v """ +import os import time from pathlib import Path @@ -248,7 +249,8 @@ async def test_backend_logs_requests(page: Page): assert response.status == 200 # Log file should exist - log_file = Path.home() / ".ucore" / "logs" / "ucore-menu.log" + udos_home = Path(os.environ.get("UDOS_HOME", Path.home() / "Code" / ".udos")) + log_file = udos_home / "logs" / "ucore-menu.log" assert log_file.parent.exists(), "Log directory should exist" diff --git a/backend/tests/test_api_tools.py b/backend/tests/test_api_tools.py index 71c4bf50..8b06c0b3 100644 --- a/backend/tests/test_api_tools.py +++ b/backend/tests/test_api_tools.py @@ -20,7 +20,7 @@ async def test_list_tools(self): data = await resp.json() assert "tools" in data assert "count" in data - assert data["count"] >= 7 # 7 tools: git, docker, node, python, ollama, github_cli, vscode + assert data["count"] >= 6 async def test_tool_status_existing(self): """Should return status for known tools without error.""" diff --git a/backend/tests/test_core_config.py b/backend/tests/test_core_config.py index 8bdd32d5..7c6e93c1 100644 --- a/backend/tests/test_core_config.py +++ b/backend/tests/test_core_config.py @@ -15,8 +15,10 @@ def test_config_loads_defaults(): assert cfg.logging.level == "INFO" -def test_config_has_cline_principles(): +def test_config_has_agent_policy_principles(): cfg = get_config() - principles = cfg.cline.operating_principles if cfg.cline else [] + principles = ( + cfg.agent_policy.operating_principles if cfg.agent_policy else [] + ) assert principles assert "Prefer safe, reversible changes" in principles[0] diff --git a/backend/tests/test_home_path_policy.py b/backend/tests/test_home_path_policy.py new file mode 100644 index 00000000..e8e0bbd8 --- /dev/null +++ b/backend/tests/test_home_path_policy.py @@ -0,0 +1,22 @@ +# path-policy: allow-literals +from scripts.check_home_path_policy import violations + + +def test_rejects_new_legacy_home_state_path(): + diff = "+++ b/backend/example.py\n+STATE = Path.home() / '.ucore' / 'state.json'\n" + assert violations(diff) + + +def test_accepts_udos_home_path(): + diff = "+++ b/backend/example.py\n+STATE = settings.udos_home / 'state.json'\n" + assert violations(diff) == [] + + +def test_accepts_canonical_default_home(): + diff = '+++ b/backend/example.py\n+HOME = Path.home() / "Code" / ".udos"\n' + assert violations(diff) == [] + + +def test_explicit_line_exception_is_auditable(): + diff = "+++ b/backend/example.py\n+LEGACY = '~/.ucore' # path-policy: allow\n" + assert violations(diff) == [] diff --git a/backend/tests/test_launchd_manager.py b/backend/tests/test_launchd_manager.py new file mode 100644 index 00000000..c833af33 --- /dev/null +++ b/backend/tests/test_launchd_manager.py @@ -0,0 +1,20 @@ +import plistlib +from pathlib import Path + +from app.menu.launchd_manager import UDOS_HOME, get_frontend_plist_content, get_plist_content + + +def test_menu_restarts_crashes_but_respects_clean_quit(): + plist = plistlib.loads(get_plist_content().encode()) + + assert plist["RunAtLoad"] is True + assert plist["KeepAlive"] == {"SuccessfulExit": False} + assert plist["EnvironmentVariables"]["UDOS_HOME"] == str(UDOS_HOME) + assert Path(plist["StandardOutPath"]).is_relative_to(UDOS_HOME) + + +def test_frontend_uses_canonical_runtime_home(): + plist = plistlib.loads(get_frontend_plist_content().encode()) + + assert plist["EnvironmentVariables"]["UDOS_HOME"] == str(UDOS_HOME) + assert Path(plist["StandardOutPath"]).is_relative_to(UDOS_HOME) diff --git a/backend/tests/test_skill_registry_authorization.py b/backend/tests/test_skill_registry_authorization.py new file mode 100644 index 00000000..a667fe05 --- /dev/null +++ b/backend/tests/test_skill_registry_authorization.py @@ -0,0 +1,33 @@ +from app.skills.base import BaseSkill, SkillMeta +from app.skills import registry + + +class _DestructiveSkill(BaseSkill): + meta = SkillMeta( + id="test-destructive", + name="Test destructive", + category="destructive", + ) + + async def run(self, **kwargs) -> dict: + return {"success": True} + + +async def test_core_registry_blocks_unauthorized_destructive_execution(monkeypatch): + monkeypatch.setattr(registry, "get_skill", lambda _skill_id: _DestructiveSkill()) + + result = await registry.run_skill_by_id("test-destructive") + + assert result["success"] is False + assert result["requires_confirmation"] is True + + +async def test_core_registry_runs_destructive_skill_after_authorization(monkeypatch): + monkeypatch.setattr(registry, "get_skill", lambda _skill_id: _DestructiveSkill()) + + result = await registry.run_skill_by_id( + "test-destructive", + execution_authorized=True, + ) + + assert result == {"success": True} diff --git a/backend/tests/test_tools_registry.py b/backend/tests/test_tools_registry.py index f1477212..1e07497a 100644 --- a/backend/tests/test_tools_registry.py +++ b/backend/tests/test_tools_registry.py @@ -10,12 +10,12 @@ async def test_list_tools(): """list_tools returns all discovered tools.""" tools = await list_tools() - assert len(tools) >= 6 # github_cli, git, python, node, docker, ollama, vscode + assert len(tools) >= 6 # github_cli, git, python, node, docker, ollama tool_ids = {t.id for t in tools} assert "github_cli" in tool_ids assert "git" in tool_ids assert "python" in tool_ids - assert "vscode" in tool_ids + assert "ollama" in tool_ids @pytest.mark.asyncio diff --git a/backend/tests/test_workflow_status.py b/backend/tests/test_workflow_status.py index 638caf50..61756ffb 100644 --- a/backend/tests/test_workflow_status.py +++ b/backend/tests/test_workflow_status.py @@ -48,8 +48,8 @@ def test_build_workflow_status_includes_guardrails( }, ) - assert result["engine"]["name"] == "Cline Kanban" - assert result["engine"]["bind"] == "127.0.0.1:3484" - assert any("localhost" in rule for rule in result["guardrails"]) + assert result["engine"]["name"] == "uFlow Markdown Workflow Engine" + assert result["engine"]["storage"] == str(tasker_dir) + assert any("sole durable" in rule for rule in result["guardrails"]) assert result["maintenance"]["jobs"][0]["skill_id"] == "brain_sync" assert result["maintenance"]["tray"]["status"] == "running" diff --git a/config/api-registry.example.yaml b/config/api-registry.example.yaml index 236541e1..9a5cab9b 100644 --- a/config/api-registry.example.yaml +++ b/config/api-registry.example.yaml @@ -1,5 +1,5 @@ # uCore API Registry Configuration -# Copy to ~/.ucore/config/api-registry.yaml and adjust +# Copy to $UDOS_HOME/config/api-registry.yaml and adjust # Variable definitions: ${tier:purpose} → provider+model # These are fallbacks when the registry cannot find a match @@ -25,4 +25,4 @@ tier_overrides: reviewer: "deepseek/deepseek-chat" premium: reviewer: "claude-opus-4.7" - architect: "glm-5.1" \ No newline at end of file + architect: "glm-5.1" diff --git a/config/budget.example.yaml b/config/budget.example.yaml index c35f4422..6fc16fd9 100644 --- a/config/budget.example.yaml +++ b/config/budget.example.yaml @@ -1,5 +1,5 @@ # uCore Budget Configuration -# Copy to ~/.ucore/config/budget.yaml and adjust +# Copy to $UDOS_HOME/config/budget.yaml and adjust # Session budget (resets on restart) session_budget_usd: 5.0 @@ -35,4 +35,4 @@ per_agent: max_per_task_usd: 0.05 # When budget is exhausted, only use free tier models -free_tier_only_when_exhausted: true \ No newline at end of file +free_tier_only_when_exhausted: true diff --git a/config/capability_requirements.json b/config/capability_requirements.json index f2176e1b..8234778a 100644 --- a/config/capability_requirements.json +++ b/config/capability_requirements.json @@ -3,7 +3,7 @@ "extensions": ["uflow"], "tools": ["git"], "repos": ["uFlow"], - "variables": ["cline_provider", "cline_model"], + "variables": [], "secrets": [] }, "knowledge.search": { @@ -20,18 +20,18 @@ "variables": [], "secrets": [] }, - "developer.autonomous": { + "developer.guided": { "extensions": [], "tools": ["git", "node", "python"], - "repos": ["uDev"], - "variables": ["cline_provider", "cline_model"], + "repos": ["uCore", "uFlow", "uKnowledge", "uCode"], + "variables": [], "secrets": [] }, "llm.openrouter": { "extensions": [], "tools": [], "repos": [], - "variables": ["cline_provider", "cline_model"], + "variables": ["openrouter_model"], "secrets": ["OPENROUTER_API_KEY"] }, "identity_gateway": { diff --git a/config/developer-repo-policy.example.yaml b/config/developer-repo-policy.example.yaml index 122afd9b..dec70a6d 100644 --- a/config/developer-repo-policy.example.yaml +++ b/config/developer-repo-policy.example.yaml @@ -1,5 +1,5 @@ # uCore Developer Repo Policy Configuration -# Copy to ~/.ucore/config/developer-repo-policy.yaml and adjust. +# Copy to $UDOS_HOME/config/developer-repo-policy.yaml and adjust. # # Purpose: # - Backend classification policy for Developer Surface repository discovery. @@ -31,7 +31,7 @@ system_repos: core_repos: # Additional core repos beyond system_repos - - udev + - uCore extension_repos: # Extension plugins (udos-* repos automatically included) diff --git a/config/openrouter.yaml b/config/openrouter.yaml index 1de2e6a2..2d566e4e 100644 --- a/config/openrouter.yaml +++ b/config/openrouter.yaml @@ -1,5 +1,5 @@ # OpenRouter Configuration for uCore Hivemind -# Copy to ~/.ucore/config/openrouter.yaml and set API key +# Copy to $UDOS_HOME/config/openrouter.yaml; keep the API key in Secret Store. version: "1.0.0" diff --git a/config/service-registry.example.yaml b/config/service-registry.example.yaml index 891955b3..eed35f4b 100644 --- a/config/service-registry.example.yaml +++ b/config/service-registry.example.yaml @@ -1,5 +1,5 @@ # uCore Service Registry Configuration -# Copy to ~/.ucore/config/service-registry.yaml and adjust. +# Copy to $UDOS_HOME/config/service-registry.yaml and adjust. # # Purpose: # - Single source of truth for service definitions used by: diff --git a/config/system-pages-registry.example.yaml b/config/system-pages-registry.example.yaml index 6dae1ef7..744c0ba1 100644 --- a/config/system-pages-registry.example.yaml +++ b/config/system-pages-registry.example.yaml @@ -1,5 +1,5 @@ # uCore System Pages Registry Configuration -# Copy to ~/.ucore/config/system-pages-registry.yaml and adjust. +# Copy to $UDOS_HOME/config/system-pages-registry.yaml and adjust. # # Purpose: # - External registry for System Surface S-page menus. diff --git a/config/vault-sync.example.yaml b/config/vault-sync.example.yaml index 8d49b173..35d114a3 100644 --- a/config/vault-sync.example.yaml +++ b/config/vault-sync.example.yaml @@ -1,7 +1,7 @@ # Bidirectional vault sync configuration for uCore. # Copy to config/vault-sync.yaml and adjust paths as needed. -state_file: ~/.ucore/vault-sync-state.json +state_file: ${UDOS_HOME}/vault-sync-state.json # Vault topology — 3 vault types only (see docs/VAULT_BINDER_WORKFLOW_INTEGRATION.md): # user → ~/Vault/ (personal vault — single source of truth) @@ -12,24 +12,24 @@ state_file: ~/.ucore/vault-sync-state.json containers: - id: user-vault - left: /Users/fredbook/Vault - right: /Users/fredbook/Code/uDocs/ingest/user-vault + left: ${UDOS_VAULT_ROOT} + right: ${UDOS_ROOT}/uDocs/ingest/user-vault mode: bidirectional include: - "**/*.md" - "**/*.markdown" - id: shared-vault - left: /Users/fredbook/Shared - right: /Users/fredbook/Code/uDocs/ingest/shared + left: ${UDOS_SHARED_ROOT} + right: ${UDOS_ROOT}/uDocs/ingest/shared mode: bidirectional include: - "**/*.md" - "**/*.markdown" - id: global-knowledge - left: /Users/fredbook/Public - right: /Users/fredbook/Code/uDocs/ingest/public + left: ${UDOS_PUBLIC_ROOT} + right: ${UDOS_ROOT}/uDocs/ingest/public mode: bidirectional include: - "**/*.md" diff --git a/devlog.mcp.yaml b/devlog.mcp.yaml deleted file mode 100644 index 89cc8f16..00000000 --- a/devlog.mcp.yaml +++ /dev/null @@ -1,286 +0,0 @@ -# Devlog MCP — Updated: 2026-08-11 - -version: "1.1.0" -generated_by: "Cline-uCore" -hours: 30 -completed_tasks: 18 -spool_entries: 500 - -## Completed Tasks - -- task: openrouter.001-014 — OpenRouter/Free Ask/Plan/Act Pipeline - status: complete - files: - - backend/app/api/chat.py - - backend/app/api/routes.py - - backend/app/services/provider_router.py - - backend/app/services/chat_context.py - - backend/tests/test_api_chat.py - - frontend-vue/src/stores/chat.ts - - frontend-vue/src/surfaces/assistui/AssistUISurface.vue - - docs/FEATURE_SPEC_OPENROUTER_ASK_PLAN_ACT.md - - .tasker/sprints/sprint-plan-openrouter-ask-plan-act.md - - fieldnotes.md - - devlog.mcp.yaml - - pyproject.toml - - package.json - -- task: openrouter.015-018 — Act Mode Tools (scrape_web, save_to_vault, confirmation gate) - status: complete - files: - - backend/app/api/chat.py - -- task: openrouter.019-021 — Tests, docs, version bump - status: complete - -## Spool Activity - -- timestamp: 2026-08-04T12:51:01+00:00 - level: INFO - module: stderr - message: [2026-08-04 12:51:01] WARNING ucore.secret — cryptography not installed, using plaintext fallback - -- timestamp: 2026-08-04T12:51:00.380000+00:00 - level: WARNING - module: ucore-menu - message: ucore-menu: Menu lock file exists — PID 46182 is still running - -- timestamp: 2026-08-04T12:50:51+00:00 - level: INFO - module: stderr - message: [2026-08-04 12:50:51] INFO ucore.skills.daily_backup — Cleaned up 4 old backups - -- timestamp: 2026-08-04T12:50:48+00:00 - level: INFO - module: stderr - message: [2026-08-04 12:50:48] INFO ucore — Backup completed: 3 files backed up to /Users/fredbook/.ucore - -- timestamp: 2026-08-04T12:50:31+00:00 - level: INFO - module: stderr - message: [2026-08-04 12:50:31] ERROR ucore.skills.self_heal — Diagnostics failed: No module named 'psutil' - -- timestamp: 2026-08-04T12:50:31+00:00 - level: INFO - module: stderr - message: [2026-08-04 12:50:31] ERROR ucore.skills.self_heal — Port recovery failed: No module named 'psuti - -- timestamp: 2026-08-04T12:50:25+00:00 - level: INFO - module: stderr - message: [2026-08-04 12:50:25] WARNING ucore.secret — cryptography not installed, using plaintext fallback - -- timestamp: 2026-08-04T12:50:24.717000+00:00 - level: INFO - module: ucore-menu - message: ucore-menu: Global clipboard shortcut registered: Ctrl+Cmd+V - -- timestamp: 2026-08-04T12:50:24.638000+00:00 - level: INFO - module: ucore-menu - message: ucore-menu: Registered snack: clipboard-buffer (clipboard) - -- timestamp: 2026-08-04T12:50:24.599000+00:00 - level: INFO - module: ucore-menu - message: ucore-menu: Lockfile is 25 hours old — stale (safety net) - -- timestamp: 2026-08-04T12:50:24.599000+00:00 - level: INFO - module: ucore-menu - message: ucore-menu: Removing stale lock file - -- timestamp: 2026-08-04T12:50:24.599000+00:00 - level: INFO - module: ucore-menu - message: ucore-menu: Lock acquired (PID 46182) - -- timestamp: 2026-08-04T09:14:46.894000+00:00 - level: INFO - module: autonomy - message: 2026-08-04 09:14:46,894 [autonomy] INFO State saved. Health: 99.4% | Ollama: online - -- timestamp: 2026-08-04T09:14:46.894000+00:00 - level: INFO - module: autonomy_launchd - message: 2026-08-04 09:14:46,894 [autonomy] INFO State saved. Health: 99.4% | Ollama: online - -- timestamp: 2026-08-04T09:14:46.893000+00:00 - level: INFO - module: autonomy - message: 2026-08-04 09:14:46,893 [autonomy] INFO Health OK: 99.4% (threshold: 95.0%) - -- timestamp: 2026-08-04T09:14:46.893000+00:00 - level: INFO - module: autonomy_launchd - message: 2026-08-04 09:14:46,893 [autonomy] INFO Health OK: 99.4% (threshold: 95.0%) - -- timestamp: 2026-08-04T09:14:46.726000+00:00 - level: INFO - module: autonomy - message: 2026-08-04 09:14:46,726 [autonomy] INFO Starting ecosystem audit... - -- timestamp: 2026-08-04T09:14:46.726000+00:00 - level: INFO - module: autonomy_launchd - message: 2026-08-04 09:14:46,726 [autonomy] INFO Starting ecosystem audit... - -- timestamp: 2026-08-04T09:14:46.725000+00:00 - level: INFO - module: autonomy - message: 2026-08-04 09:14:46,725 [autonomy] INFO === Autonomy Engine: Full Health Check === - -- timestamp: 2026-08-04T09:14:46.725000+00:00 - level: INFO - module: autonomy_launchd - message: 2026-08-04 09:14:46,725 [autonomy] INFO === Autonomy Engine: Full Health Check === - -- timestamp: 2026-08-04T04:51:17.265250+00:00 - level: INFO - module: ucore-server - message: (Press CTRL+C to quit) - -- timestamp: 2026-08-04T04:51:17.265247+00:00 - level: INFO - module: ucore-server - message: ======== Running on http://0.0.0.0:8484 ======== - -- timestamp: 2026-08-04T04:51:17.265097+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265093+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265090+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265087+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265083+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265080+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265077+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265074+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265070+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265063+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265060+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265056+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265053+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265050+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265047+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265043+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265040+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265037+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265034+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265030+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265027+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265024+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265019+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265015+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265012+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265009+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265005+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn - -- timestamp: 2026-08-04T04:51:17.265002+00:00 - level: INFO - module: ucore-popcorn-stderr - message: /Applications/Xcode.app/Contents/Developer/usr/bin/python3: No module named app.ui.popcorn diff --git a/docs/AGENT_EXECUTION_ARCHITECTURE.md b/docs/AGENT_EXECUTION_ARCHITECTURE.md new file mode 100644 index 00000000..adf1e0e5 --- /dev/null +++ b/docs/AGENT_EXECUTION_ARCHITECTURE.md @@ -0,0 +1,94 @@ +# Agent Execution Architecture + +**Status:** Canonical direction + +**Updated:** 2026-08-18 + +## Product principle + +Users choose an intention, not an agent, provider, or model. uDOS converts that +intention into a governed task envelope and selects the least expensive capable +execution path. Provider selection remains observable in diagnostics and +advanced settings, but is not normal user interaction. + +## Authorities + +| Component | Authority | +| --- | --- | +| uFlow | Durable missions, tasks, workflow state, approval state and resumability | +| HiveMind | Decomposition, capability routing, retries, escalation and evidence collection | +| Roundtable | Optional multi-model deliberation and review strategy | +| Provider router | Capability-to-provider/model resolution and health-aware fallback | +| Budget service | Per-task, daily and monthly admission control and cost ledger | +| uCode BASIC | User coding and structured document computation | +| Codex | External ecosystem and add-on development environment | +| GitHub/Copilot | Source collaboration, Actions, issues, pull requests and optional review | + +HiveMind is not a model and Roundtable is not a general executor. Neither owns +tasks. They operate on uFlow task envelopes and return structured evidence. + +## Task envelope + +Every model or agent request must carry: + +- intention and success criteria; +- lane: user, workflow, developer or system; +- referenced vault, repository and file context rather than unrestricted roots; +- privacy classification and cloud permission; +- required capabilities; +- allowed tools and write targets; +- risk class and approval requirements; +- maximum cost, attempts and wall-clock time; +- provenance identifiers for prompts, models, tools and outputs. + +## Routing ladder + +1. Deterministic implementation: parsing, indexing, validation, transforms, + policy, budgets and workflow state. +2. Ollama: private/local drafting, classification, summaries, Markdown and BASIC + assistance. +3. OpenRouter free or low-cost models: larger or specialist user work and + optional diverse review. +4. OpenAI API efficient tier: reliable escalation where free/local quality is + insufficient. +5. Roundtable: ambiguity, disagreement, high-value review or failed single-agent + attempts only. +6. Frontier API model or external Codex: difficult, high-value developer work. + +Failure moves upward only when the next tier is permitted by privacy and budget +policy. Budget exhaustion moves downward or pauses; it never silently selects a +more expensive provider. + +## Surface contract + +- Intelligence owns user-visible planning, agent history, provider diagnostics + and budget decisions. +- Workflow owns uFlow missions, tasks, approvals and resumability. +- Snackbar owns process health, logs, provider availability and runtime alerts. +- Developer remains Code / Repository / Editor. It supplies repository, file, + selection and diff context to actions such as Plan, Review and Implement; it + does not regain dedicated agent, model or Kanban tabs. + +## Developer executors + +Codex is the primary environment for real ecosystem development. GitHub/Copilot +may add repository-native issue, pull-request, Actions and review assistance. + +Cline is not part of the installed runtime or current Developer surface. A future +contained adapter may be evaluated, but it must live outside the authority path, +remain disabled by default, and cannot use yolo/auto-approval. Act mode would +require an isolated worktree, path allow-list, budget gate, command policy, diff +review, tests, rollback and explicit merge/push approval before it could ship. + +Cline Kanban is not installed. Durable task presentation belongs to uFlow and +uCore; duplicating it would reintroduce task and status drift. + +## Non-negotiable controls + +- Model calls pass through the provider router and budget service. +- Mutating execution requires authorization at the core registry, not only UI. +- Vault content cannot reach cloud providers unless its privacy policy permits. +- No executor receives unrestricted credential or home-directory access. +- No agent may merge, push, publish, delete or spend above its envelope without + the required approval. +- Every result records provider/model, cost, attempts, evidence and mutations. diff --git a/docs/CLINE_GITHUB_WORKFLOWS.md b/docs/CLINE_GITHUB_WORKFLOWS.md deleted file mode 100644 index 4cc14c3d..00000000 --- a/docs/CLINE_GITHUB_WORKFLOWS.md +++ /dev/null @@ -1,136 +0,0 @@ -> **Canonical version:** `/Users/fredbook/Code/uDocs/runbooks/cline-github-workflows.md` -> This repo copy is kept for local reference; edits should be made in uDocs. - -# Cline GitHub Workflows (uCore) - -Date: 2026-06-21 -Status: Canonical Cline playbook - -## Purpose - -Replace legacy Continue rule files with Cline-native, prompt-driven MCP workflows. - -This runbook maps three high-value automation flows: - -- Auto PR creation -- CI/CD monitoring -- Issue triage - -## Prerequisites - -- Cline configured with uCore MCP server in `~/.cline/mcp_settings.json` -- uCore backend running and reachable -- GitHub token available for MCP GitHub tools - -Quick checks: - -```bash -cline auth -cline mcp --help -curl -fsS http://127.0.0.1:8484/api/mcp/tools | head -``` - -## Workflow 1: Auto PR Creation - -Trigger phrases: - -- "ready to PR" -- "create a PR for this branch" -- "submit these changes" - -Prompt pattern for Cline: - -```text -Create a PR for the current branch. -1) inspect git status and branch -2) summarize changed files and commit messages -3) use MCP GitHub tool to open PR against main -4) return PR URL and next reviewer actions -``` - -Expected behavior: - -1. Detect non-main branch. -2. Verify branch is pushed (or push). -3. Generate title/body from commit diff. -4. Invoke GitHub MCP PR creation tool. -5. Return PR URL. - -## Workflow 2: CI/CD Monitoring - -Trigger phrases: - -- "check ci status" -- "actions status" -- "show failing workflows" - -Prompt pattern for Cline: - -```text -Check GitHub Actions status across uDosGo repositories. -1) call MCP CI status tool -2) summarize passing/failing runs -3) provide links for failures -4) suggest retry candidates for recent transient failures -``` - -Expected behavior: - -1. Query workflow runs. -2. Highlight failures by repo and branch. -3. Suggest retry path where appropriate. - -## Workflow 3: Issue Triage - -Trigger phrases: - -- "triage issues" -- "clean up issue inbox" -- "review stale issues" - -Prompt pattern for Cline: - -```text -Triage open issues across target repositories. -1) call MCP issue triage/heal tool -2) auto-label by keyword categories -3) flag stale issues older than 30 days -4) produce a concise triage report -``` - -Expected behavior: - -1. Apply label heuristics. -2. Mark stale items. -3. Return summary and priority list. - -## Scheduled Automation (launchd/cron) - -Use `launchd`, `cron`, or `cline schedule` wrappers that execute Cline prompts or uCore API calls. - -### Option A: Cron (simple) - -```bash -# every hour: CI check via Cline -0 * * * * cd /Users/fredbook/Code/uCore && cline --json --cwd /Users/fredbook/Code/uCore "Check CI status across uDosGo repos and summarize failures only" >> /tmp/ucore-ci.log 2>&1 - -# weekdays 9:15: issue triage via Cline -15 9 * * 1-5 cd /Users/fredbook/Code/uCore && cline --json --cwd /Users/fredbook/Code/uCore "Triage open issues for uCore using MCP and summarize actions taken" >> /tmp/ucore-triage.log 2>&1 -``` - -### Option B: launchd (macOS) - -Create a LaunchAgent that runs a shell wrapper calling one of: - -- `cline "...prompt..." --json --cwd ...` -- `curl http://127.0.0.1:8484/api/github/trigger/...` - -Use launchd when you need reliable startup behavior and per-user service management. - -## Legacy Archive - -Legacy Continue rule files were archived to: - -- `.continue/rules_legacy_continue_20260621/` - -They are retained only for historical reference and are not part of the active Cline workflow. diff --git a/docs/CONSOLIDATION_PLAN.md b/docs/CONSOLIDATION_PLAN.md index 1da89998..426e5c81 100644 --- a/docs/CONSOLIDATION_PLAN.md +++ b/docs/CONSOLIDATION_PLAN.md @@ -1,6 +1,6 @@ # Docs Consolidation Plan -> **Superseded for active task tracking by `.tasker.dev-flow.yaml`.** +> **Historical tracker only. Active workflow state is owned by uFlow.** > **Canonical docs destination: `/Users/fredbook/Code/uDocs`.** > This file is kept as a historical migration tracker only. @@ -115,4 +115,4 @@ Archive or deprecate in-place: - Mission Control global toolbar removed legacy ProseUI-era tabs and now keeps only canonical navigation (`Dashboard`, `Missions`). - Archived legacy frontend remnants removed from codebase: - `frontend/src/surfaces/gridcore/GridCoreSurface.tsx` - - `frontend/src/pages/S800Labs.tsx` \ No newline at end of file + - `frontend/src/pages/S800Labs.tsx` diff --git a/docs/CORE_STABILIZATION_MERGE_LEDGER_2026-08-18.md b/docs/CORE_STABILIZATION_MERGE_LEDGER_2026-08-18.md new file mode 100644 index 00000000..4a96bedf --- /dev/null +++ b/docs/CORE_STABILIZATION_MERGE_LEDGER_2026-08-18.md @@ -0,0 +1,92 @@ +# Core Stabilization Merge Ledger — 2026-08-18 + +**Status:** Ready for review; merging requires explicit maintainer approval. + +## Outcome + +This wave establishes one ownership and storage path across the four core +repositories: + +- uFlow owns durable missions, tasks, workflow definitions, runs and approvals. +- uKnowledge owns filesystem-first workspace registration, safe Markdown reads, + offline search and the read-only Public-vault boundary. +- uCode owns the BASIC/GridCore runtime and uses the shared ecosystem environment + and `$UDOS_HOME`; it owns no editor, provider, secret or task configuration. +- uCore hosts the user surfaces, delegates to those owners, and keeps Developer + focused on repository/code work without restoring agent/model/Kanban tabs. + +The canonical mutable runtime root is `$UDOS_HOME` (normally +`~/Code/.udos`). User documents remain in `~/Vault`; shared/add-on vaults in +`~/Shared`; public read-only editions in `~/Public`. The shared Python environment +is `~/Code/.venv`. + +## Review branches and checkpoints + +| Repository | Branch | Key review checkpoint | +| --- | --- | --- | +| uFlow | `work/2026-08-18-stabilise` | `028df3d` — task substrate moved into uFlow | +| uKnowledge | `work/2026-08-18-stabilise` | `7e52162` — filesystem-first knowledge library | +| uCode | `work/2026-08-18-stabilise` | `65b1706` — shared state/runtime boundary | +| uCore | `work/2026-08-18-stabilise` | `6dff063` — clean-checkout CI aligned with the stabilized architecture | + +## Verification evidence + +| Gate | Result | +| --- | --- | +| uCore backend | 498 passed; 6 existing aiohttp warnings | +| uCore Vue unit tests | 12 passed | +| uCore Vue production build | passed; existing chunk-size warnings only | +| uCore changed-file Ruff gate | passed | +| uCore home-path policy | 4 passed | +| uCore planning governance | passed | +| uCore documentation and knowledge route contracts | passed | +| uCore self-hosted MCP bridge | TypeScript build passed; diagnostics healthy | +| uFlow | 4 passed | +| uKnowledge | 10 passed, including Public read-only and traversal controls | +| uCode JavaScript/TypeScript | 188 tests passed across GridCore, viewport and GridSmith | +| uCode package build | all three workspaces built, including declarations | +| uCode BASIC runtime | 169 passed, 48 skipped, 2 warnings | + +The skipped BASIC tests are marked optional/integration tests in the existing +suite; they are not newly skipped by this wave. + +## Merge order + +1. uFlow — establishes workflow/task ownership. +2. uKnowledge — establishes knowledge and vault contracts. +3. uCode — establishes runtime/state boundary and buildable UI dependencies. +4. uCore — consumes all three contracts and supplies the reconciled surfaces. + +After each merge, rerun that repository's gate. After uCore merges, rerun the +complete table above from clean `main` checkouts before tagging or releasing. + +## Review focus + +- Confirm no duplicate task or knowledge store remains active in uCore/uCode. +- Confirm Public knowledge mutation is rejected by capability, not merely hidden + in the UI. +- Confirm Workflow and BrowserUI show live owner-backed state. +- Confirm the Developer surface remains Repo / Code / Editor oriented. +- Confirm Snackbar/System/Intelligence contain operational concerns without new + top-level navigation. +- Confirm no active install/runtime path recreates `~/.ucore`, `~/.udos`, + `.tasker`, `.vscode`, or `.clinerules`. +- Confirm generated files, credentials, vault contents and local runtime state are + absent from every diff. + +The final drift audit also removed the tracked Cline rules, legacy Tasker/devlog +state, and their duplicate writer skills. `brain_sync` now owns private memory +only; durable tasks remain exclusively under uFlow. + +## Local-worktree note + +`uCore/backend/tests/test_skill_registry_authorization.py` has a local import-order +change that predates/is unrelated to this wave. It was intentionally excluded from +all stabilization commits and must not be swept into a merge. + +## Rollback + +Each repository is independently reversible to `origin/main`. Do not partially +revert uCore's delegation commits while retaining the owner-repository changes; +either revert the consuming uCore wave first or roll back in reverse merge order. +Runtime/user vault data is not part of these Git changes. diff --git a/docs/ECOSYSTEM_STORAGE_ARCHITECTURE.md b/docs/ECOSYSTEM_STORAGE_ARCHITECTURE.md new file mode 100644 index 00000000..f4dd4eed --- /dev/null +++ b/docs/ECOSYSTEM_STORAGE_ARCHITECTURE.md @@ -0,0 +1,51 @@ + +# Ecosystem Storage Architecture + +**Status:** Canonical +**Updated:** 2026-08-18 + +## Ownership boundary + +| Location | Owner | Lifecycle | +| --- | --- | --- | +| `~/Code/` | Git repositories | Clone, develop, archive independently | +| `~/Code/.udos` | uDOS runtime | Detachable and destructible as one unit | +| `~/Vault` | User | Primary private document vault | +| `~/Shared` | User/workspaces | Shared and add-on vaults | +| `~/Public` | User/publishing | Public and publishable documents | +| Standard credential paths | User and owning applications | Survive uDOS removal | + +`UDOS_HOME` defaults to `~/Code/.udos`. It owns application configuration, +logs, indexes, caches, model data, container data, generated state, runtime +metadata, compatibility archives and shared toolchains. + +Credentials and operating-system integration are deliberately excluded: +`~/.ssh`, `~/.gitconfig`, `~/.config/gh`, `~/.npmrc`, `~/.docker`, `~/.kube`, +`~/.codex`, macOS Keychain and application-owned `~/Library` data. + +## Compatibility links + +The current workstation temporarily retains zero-storage links at historical +paths while active code is migrated. They are compatibility interfaces, not +approved write targets. New code must resolve `UDOS_HOME` instead. + +## Drift prevention + +Three controls apply: + +1. The workspace `AGENTS.md` gives every compatible coding agent the same + storage, ownership and lifecycle contract. +2. `scripts/check_home_path_policy.py` rejects newly added hard-coded uDOS + state paths in staged changes or supplied CI diffs. +3. CI applies the checker to additions in pull requests and pushes. Historical + references are migration debt, not precedent for new work. + +Literal legacy paths in migration documentation require the explicit +`path-policy: allow-literals` marker. Individual exceptional lines require a +`path-policy: allow` comment so exceptions remain searchable. + +## Destruction contract + +Destroying or snapping off uDOS may include `~/Code/.udos` and selected code +repositories only. It must never remove document vaults, credentials, Codex +configuration or general application data without a separately approved plan. diff --git a/docs/FEATURE_SPEC_OPENROUTER_ASK_PLAN_ACT.md b/docs/FEATURE_SPEC_OPENROUTER_ASK_PLAN_ACT.md index e6707e24..79e43a3e 100644 --- a/docs/FEATURE_SPEC_OPENROUTER_ASK_PLAN_ACT.md +++ b/docs/FEATURE_SPEC_OPENROUTER_ASK_PLAN_ACT.md @@ -113,10 +113,9 @@ Shows free-tier models with cost badges (free/ultra-cheap/budget/mid-range/premi --- -## Remaining Work (Wave 4-5) +## Deferred follow-up (owned by uFlow) -- [ ] Wire `editor_api.py` scrape endpoint as a chat tool -- [ ] Wire save-to-vault as a chat tool with vault layer boundaries -- [ ] Full test suite for new functionality -- [ ] End-to-end smoke test (Plan → Approve → Act → Vault result) -- [ ] Update devlog, fieldnotes, wisdom +The remaining product work is to connect governed browser capture and +vault-bound writes, expand the functionality tests, add the Plan → Approve → Act +end-to-end probe, and record its evidence. These are uFlow tasks rather than an +active checklist embedded in this specification. diff --git a/docs/MCP_SETUP.md b/docs/MCP_SETUP.md index 256bcc5c..93aa173a 100644 --- a/docs/MCP_SETUP.md +++ b/docs/MCP_SETUP.md @@ -2,13 +2,14 @@ # uCore MCP Setup -uCore now uses one MCP JSON-RPC stdio server in workspace config. +uCore exposes one self-hosted MCP JSON-RPC stdio server. External developer +clients may connect to it, but their configuration is not part of uCore. ## Canonical MCP Server -- Config file: .vscode/mcp.json - Server id: ucore-bridge -- Command: node ../uDev/mcp-bridge/build/index.js +- Source: backend/app/mcp/mcp_bridge +- Command: node backend/app/mcp/mcp_bridge/build/index.js - Env: UCORE_URL=http://127.0.0.1:8484 ## Start Sequence @@ -16,7 +17,7 @@ uCore now uses one MCP JSON-RPC stdio server in workspace config. ```bash cd /Users/fredbook/Code/uCore pnpm run dev:backend -cd /Users/fredbook/Code/uDev/mcp-bridge && npm run build +cd /Users/fredbook/Code/uCore/backend/app/mcp/mcp_bridge && npm run build ``` ## Diagnostics @@ -28,7 +29,5 @@ python3 -m mcp.mcp_diagnostics Expected checks: -- .vscode/mcp.json exists -- ucore-bridge is declared -- no HTTP MCP servers exist in active config -- bridge binary exists +- bridge source and package metadata exist +- the local bridge build exists diff --git a/docs/README.md b/docs/README.md index f01e0976..651d3fe4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,7 +7,6 @@ | Doc | Purpose | | ---------------------------------------------------------------------------------------- | --------------------------------- | | [MCP_SETUP.md](MCP_SETUP.md) | Install and configure MCP servers | -| [CLINE_GITHUB_WORKFLOWS.md](CLINE_GITHUB_WORKFLOWS.md) | GitHub automation for Cline | | [USER_SETUP_VAULT_MCP_WORKSPACES.md](USER_SETUP_VAULT_MCP_WORKSPACES.md) | Vault and MCP workspace setup | ## Active System Specs @@ -40,6 +39,12 @@ | [MENUBAR_UNIFICATION_PLAN.md](MENUBAR_UNIFICATION_PLAN.md) | Menubar unification | | [FRONTEND_CONSOLIDATION_PLAYBOOK.md](FRONTEND_CONSOLIDATION_PLAYBOOK.md) | Frontend consolidation | | [VUE_REFACTOR_SURFACE_TAGGING.md](VUE_REFACTOR_SURFACE_TAGGING.md) | Vue refactor surface tags | +| [SURFACE_OWNERSHIP.md](SURFACE_OWNERSHIP.md) | Canonical route and tab ownership | +| [UDOS_HOME_MIGRATION.md](UDOS_HOME_MIGRATION.md) | Runtime/vault boundary and safe migration | +| [ECOSYSTEM_STORAGE_ARCHITECTURE.md](ECOSYSTEM_STORAGE_ARCHITECTURE.md) | Canonical storage, credentials and drift controls | +| [WORKSTATION_MIGRATION_2026-08-18.md](WORKSTATION_MIGRATION_2026-08-18.md) | Migration record, verification and rollback boundary | +| [AGENT_EXECUTION_ARCHITECTURE.md](AGENT_EXECUTION_ARCHITECTURE.md) | Intention routing, providers, orchestration and controls | +| [SKILLS_AUDIT_2026-08-18.md](SKILLS_AUDIT_2026-08-18.md) | Skill disposition and remediation sequence | ## Active Developer / Dev Mode Specs @@ -57,8 +62,7 @@ ## Active Task / Planning Trackers -- `.tasker.dev-flow.yaml` — canonical active task list -- `.tasker/README.md` — tasker directory guide +- `uFlow` — canonical workflow and task owner (`$UDOS_HOME/flow/tasks`) - `docs/CONSOLIDATION_PLAN.md` — docs consolidation tracker (kept for reference) ## Archive diff --git a/docs/RELIABILITY_SINGLE_PATH_POLICY.md b/docs/RELIABILITY_SINGLE_PATH_POLICY.md index 933d5644..30fede71 100644 --- a/docs/RELIABILITY_SINGLE_PATH_POLICY.md +++ b/docs/RELIABILITY_SINGLE_PATH_POLICY.md @@ -72,8 +72,8 @@ skipped. Recommended practical split: - local Ollama models: draft/refactor/scaffold bursts -- Copilot in VS Code: integration fixes, verification, final wave closeout -- Cline: bounded branch-local execution only, never source-of-truth +- Codex: primary external ecosystem and add-on development environment +- GitHub/Copilot: repository-native review, Actions, issues and pull requests ## Documentation Non-Regression Gate @@ -87,17 +87,17 @@ Required checks in CI: ## Config Source-of-Truth -For Cline invocation and model/provider settings: +For model/provider settings: -1. uCore user variables store (`~/.ucore/data/variables.json`) -2. uCore settings/env (`UCORE_CLINE_*`, `UCORE_OLLAMA_*`) +1. uCore user variables store (`$UDOS_HOME/data/variables.json`) +2. uCore settings/env (`UCORE_OLLAMA_*`, `OPENROUTER_*`, `OPENAI_*`) 3. Secret store for key-based providers only No hardcoded provider/model values in execution code. ## Fail-Fast Config Policy -For execution skills (for example Cline invocation): +For execution skills: - Required runtime values (provider/model and required API keys) must be validated. - If missing, return repair-required response with concrete steps. diff --git a/docs/SKILLS_AUDIT_2026-08-18.md b/docs/SKILLS_AUDIT_2026-08-18.md new file mode 100644 index 00000000..fdadf0bc --- /dev/null +++ b/docs/SKILLS_AUDIT_2026-08-18.md @@ -0,0 +1,94 @@ +# Skills Audit — 2026-08-18 + +**Status:** Active remediation + +**Observed registry:** 53 executable Skills, including one user example + +**Dedicated Skill test files before remediation:** 8 + +## Findings + +The current term “Skill” covers unrelated concepts: + +- backend executable capabilities; +- destructive recovery/admin operations; +- orchestration and provider adapters; +- workflow and legacy Tasker adapters; +- a frontend Vue component library at `frontend-vue/src/skills`; +- menu Snacks and uCode Snack runtimes. + +The backend registry dynamically imports every Python file and previously +enforced confirmation only in the HTTP API. Internal scheduler and executable +registry calls could bypass it. Core authorization is now enforced in +`run_skill_by_id` with regression tests. + +Provider and executor selection is duplicated across `route_task`, Dev Mode, +HiveMind, Roundtable and Cline. Cline integration also contained obsolete CLI +flags, direct key discovery and auto-approval behavior; it is now contained and +disabled by default. + +## Disposition + +### Keep and harden + +- Vault/user capability: `ask_vault`, `attach_context`, `vault_discovery`, + `vault_sync`. +- Safe maintenance: `backup`, `clipboard_maintenance`, `docs_mirror_sync`, + `git_maintenance`, `lint_fix`. +- Workflow controls: `workflow_audit`, `workflow_guard`, `workflow_pause`. +- Context/provenance: `episodic_log` after privacy and retention review. + +Each retained capability needs an owner, risk class, input/output schema, +allowed roots, deterministic dry run where relevant and dedicated tests. + +### Repair behind canonical contracts + +- `route_task`: become intention/capability classification only; remove direct + provider names from user inputs and duplicated execution logic. +- `hivemind-consensus`: become HiveMind orchestration client with budget, + privacy, attempts and evidence fields. +- `roundtable-dispatch`: become a selective deliberation strategy invoked by + HiveMind, not a default provider. +- `cline-invoke`: retain disabled, plan-only adapter until worktree harness. +- `gh-workflow-bridge`: narrow to GitHub issues, Actions, PR/review and Codex + handoff with explicit external-write approval. +- `brain_sync`: separate deterministic indexing from model synthesis. +- `skill-audit` and `ecosystem-audit`: replace source-text heuristics with + manifest/schema validation and executable tests. + +### Merge or split + +- Merge `enhancement-planner`, `modularisation-planner`, `duplicate-detector`, + `dead-code-archiver` and `hardcoded-path-detector` behind one code-analysis + capability with separate read-only checks and reviewed mutations. +- Split the nearly 2,000-line `skill_nuggets_and_spool.py` into an archive + service, recovery service and small capability adapters. +- Consolidate `diagnose_system`, `recover_port_conflict`, `cleanup_resources`, + `restart_backend` and MCP self-heal behind one governed recovery service. +- Move `tasker_sync` and `devlog_mcp` behind the uFlow compatibility adapter; + they must not maintain a second task engine. +- Rename the frontend Vue `skills` directory to a UI component/system term so + it cannot be mistaken for executable agent capabilities. + +### Remove from general Skill execution + +- `reset_database`, `spool_destroy` and `dev-destroy-rebuild`: privileged + recovery workflows, never ordinary agent-selected Skills. +- `autostart`: lifecycle/service management, owned by Snackbar/System. +- `surface-registry` and `usx-standard`: build/registry tooling rather than + user-selectable agent Skills. +- `hello-world`: example fixture only; do not dynamically load in production. +- Cline Kanban and all Cline/VS Code surface assumptions. + +## Remediation order + +1. Core authorization and Cline containment — completed. +2. Add lifecycle, risk, owner, lane, capabilities and allowed-root metadata to + the Skill contract. +3. Default-deny unclassified and example Skills in production discovery. +4. Separate privileged recovery operations from general execution. +5. Implement the intention task envelope and one provider/budget route. +6. Rewire HiveMind, Roundtable, GitHub and Cline as bounded adapters. +7. Split/merge the oversized and duplicated capabilities. +8. Add catalogue validation, dedicated tests and CI coverage for every enabled + capability. diff --git a/docs/SURFACE_OWNERSHIP.md b/docs/SURFACE_OWNERSHIP.md new file mode 100644 index 00000000..8729275c --- /dev/null +++ b/docs/SURFACE_OWNERSHIP.md @@ -0,0 +1,57 @@ +# uCore Surface Ownership + +**Status:** Canonical +**Updated:** 2026-08-18 + +This is the current UI Hub surface contract. Current routes and components take +precedence over historical feature plans. Superseded plans remain evidence, but +must not be used to recreate retired tabs. + +## Canonical surfaces + +| Surface | Route | Owns | +| --- | --- | --- | +| Dashboard | `/` | Navigation and ecosystem overview | +| Developer | `/developer` | Repositories, file preview, editing, diff and Git actions | +| Intelligence | `/intelligence` | Chat, planning, models, agents, budget decisions and history | +| Snackbar | `/snackbar` | Service health, feeds, skills, snacks, extensions, logs and MCP | +| System | `/system` | System pages, variables, secrets, global and user settings | +| Workflow | `/workflow` | User missions, tasks, automation, document editing and publishing | +| uCode | `/ucode` | Terminal, teletext, pixel, grid, layer and glyph runtime tools | +| Documentation | `/documentation` | Documentation and published knowledge | + +## Developer boundary + +Developer is deliberately a small repository tool with three tabs: Code, +Repository and Editor. It does not own models, agents, budgets, services, feeds, +skills, extensions, logs, MCP, missions or user tasks. + +The historical `udev` identifier may remain temporarily in saved extension +state and compatibility APIs. It refers to the built-in Developer surface; it +does not identify a separately installable repository or server. + +## Workflow boundary + +uFlow is the canonical workflow and task engine. uCore owns the Vue Workflow +surface and delegates persistence and execution through the uFlow extension +contract. uCore Tasker files are migration/development adapters, not a second +user workflow engine. + +## Compatibility routes + +- `/assistui/*` redirects to `/intelligence`. +- `/server/*` redirects to `/snackbar/*`. +- `/teletext/*` and `/terminal/*` redirect to their uCode tabs. +- `/snackmachine?tab=mcp` redirects to `/snackbar?tab=mcp`. + +New code and documentation must use canonical routes. + +## Duplicate presentation + +Some operational data can be summarized on more than one surface, but it has +one owner: + +- Intelligence owns agent use, model selection and budget decisions. +- Snackbar may show agent process health and budget alerts. +- `udos-budget` owns budget policy and persistence once its extension contract + replaces the current in-core implementation. diff --git a/docs/UDOS_HOME_MIGRATION.md b/docs/UDOS_HOME_MIGRATION.md new file mode 100644 index 00000000..62daf1cc --- /dev/null +++ b/docs/UDOS_HOME_MIGRATION.md @@ -0,0 +1,56 @@ +# uDOS Runtime Home Migration + + +**Status:** Approved architecture; migration not yet executed +**Updated:** 2026-08-18 + +## Boundary + +`~/Code/.udos` is the canonical `UDOS_HOME`. It contains mutable application +state that should travel with, or be destroyed with, the uDOS installation: +configuration, logs, caches, indexes, service state, secrets, model metadata, +shared Python environments and compatibility data. + +User documents remain portable and outside the runtime home: + +| Root | Ownership | +| --- | --- | +| `~/Vault` | Primary private user vault | +| `~/Shared` | Add-on/shared vaults | +| `~/Public` | Public and publishable vaults | + +These roots must never be folded into `UDOS_HOME` by an automated migration. + +## Legacy inputs + +The current workstation has used four runtime roots over time: + +- `~/.ucore` +- `~/.udos` +- `~/.config/udos` +- `~/.local/share/udos` + +They may contain overlapping names with different meanings. Migration therefore +requires a manifest, collision classification and backup; it is not a recursive +merge. + +## Safe sequence + +1. Stop uDOS services and record their launch configuration. +2. Run `python scripts/audit_udos_home.py --json` and save the inventory. +3. Classify every collision as canonical, mergeable, obsolete or quarantined. +4. Back up all four legacy roots with a checksum manifest. +5. Stage runtime data into `~/Code/.udos` without deleting the sources. +6. Point services at `UDOS_HOME=~/Code/.udos` and run health/integration tests. +7. Retain legacy roots through a rollback window; archive or remove only after + explicit approval. + +The audit script is deliberately read-only. A separate migration command should +be implemented only after the collision manifest has been reviewed. + +## Compatibility phase + +uCore currently selects an explicit `UDOS_HOME` first. Without one, it continues +to use an existing `~/.ucore` installation until `~/Code/.udos` exists. Fresh +installs default to `~/Code/.udos`. This prevents a code deployment from silently +switching the live state directory before migration is complete. diff --git a/docs/UI_SURFACE_WIRING_AUDIT_2026-08-18.md b/docs/UI_SURFACE_WIRING_AUDIT_2026-08-18.md new file mode 100644 index 00000000..40562ea8 --- /dev/null +++ b/docs/UI_SURFACE_WIRING_AUDIT_2026-08-18.md @@ -0,0 +1,80 @@ +# Vue Surface Wiring Audit — 2026-08-18 + +## Navigation rule + +uCore navigation follows user intentions, not implementation inventory. A capability +gets a top-level surface only when it represents a durable user activity. Tools, +providers, agents, and runtime details remain contextual actions or operational +views inside the owning surface. + +## Canonical surface set + +| Surface | User intention | Current tabs | Decision | +| --- | --- | --- | --- | +| Dashboard | Start and resume work | Dashboard, Workflow, Intelligence, Snackbar, System | Keep as the small primary switchboard. Optional extensions remain cards, not permanent tabs. | +| Workflow | Plan, track, edit, automate, and publish work | Workflow, Tasks, Automation, Editor, Publish | Keep. Editor is a contextual full-workspace state reached from a task or document, even though it is represented as a routable tab. uFlow is the authority. | +| Intelligence | Ask, plan, inspect cost, and review history | Chat, Settings, Models, Budget, History | Keep provisionally. Agent selection was removed: HiveMind/provider routing chooses execution. Models remain visible while automatic routing and policy controls mature. | +| uCode | Use the compact user runtime | Terminal, Teletext, Pixel, Grid, Layer, Glyphs | Keep as work modes within one runtime surface. They are not ecosystem navigation destinations. | +| Snackbar | Observe and administer runtime capabilities | Dashboard, Services, Agents, Feeds, Skills, Snacks, Extensions, Logs, MCP | Keep for the current operations pass, but treat this as an operator surface. Next simplification should group inventory views under Dashboard without creating more tabs. | +| System | Configure identity, variables, secrets, and recovery | Pages, Variables, Secrets, Global, User | Keep. Runtime diagnostics stay in Snackbar. | +| Documentation | Read guides, knowledge, and learning | Guide & Docs, Knowledge, Learning | Keep. Publishing is owned by Workflow; the historical Documentation deep link redirects there. | +| Developer | Inspect repositories and edit real code | Code, Repository, Editor | Keep. Repository and Editor are contextual states selected from Code, not a replacement for uCode user tasks. | + +## Contextual and compatibility surfaces + +- BrowserUI is the user-owned knowledge acquisition/research workbench and should be + invoked by Intelligence or Workflow actions. It writes to user/add-on vaults and + may package contributions; Global Knowledge remains read-only outside authorized + Dev/maintainer workflows. Its current sample data and standalone tab model are not + ready for primary navigation. +- Groovebox is a project/add-on built on top of the core. Its route may remain + stable, but it appears only as a project card when available. +- SonicScrewdriver is a standalone GridCore-based device toolkit: a device library, + reflashing/build tooling, and a path to create USB-hosted uDos runtimes for older + Linux-first machines. It is not a uCore extension or permanent core tab. +- `/assistui`, `/server`, `/snackmachine`, `/gridui`, `/userver`, `/teletext`, and + `/terminal` are compatibility routes and must resolve into a canonical surface. +- The retired uDev name may remain only as saved extension compatibility data. + +## Wiring contract + +Every visible tab must have all of the following before it is described as wired: + +1. A canonical surface owner and a valid `?tab=` deep link. +2. A rendered Vue panel with loading, empty, success, and error behaviour appropriate + to its contract. +3. A registered backend API or an explicitly local-only state contract. +4. No dependency on retired uDev, VS Code, Cline autonomy, or direct provider choice. +5. A build/test gate that fails when its imported contract drifts. + +This pass repaired query-tab synchronization for Intelligence, Documentation, and +uCode. It also made the historical Intelligence Agents link resolve to Snackbar +Agents rather than presenting agent choice to the user. + +## Reconciliation queue + +1. Consolidate Snackbar's inventory-only tabs into dashboard sections or contextual + detail views; retain direct compatibility links during the transition. +2. Wire vault document content and research capture persistence through uKnowledge; + sample stacks have been removed and empty state is now truthful. +3. After the core repositories are stable on `main`, revisit or rebuild + SonicScrewdriver as the first standalone GridCore proving project. + +## Release sequence + +1. Stabilize, reconcile, and merge uCore, uCode, uKnowledge, and uFlow. +2. Prove that repository work, task state, budgets, and internal tools can be managed + clearly through uCore's Developer and Workflow surfaces. +3. Revisit SonicScrewdriver using the settled uCode/GridCore contracts. +4. Complete Docs Libraries, the Global Knowledge bank, and the Learning Pathway as + the new-user onboarding layer. +5. Expand into downstream projects and add-ons such as Groovebox. + +SonicScrewdriver is also the first **pull product** for the ecosystem: a concrete, +easy-to-explain reason to discover uDos before a new user understands that they want +the wider platform. Its device revival and portable-runtime journey should lead into +uCode, GridCore, vault workflows, documentation, and learning without making Sonic +itself part of uCore. + +The Global Knowledge bank and Sonic device library have separate ownership and +packaging contracts; see `UKNOWLEDGE_OFFLINE_LIBRARY_ARCHITECTURE.md`. diff --git a/docs/UKNOWLEDGE_OFFLINE_LIBRARY_ARCHITECTURE.md b/docs/UKNOWLEDGE_OFFLINE_LIBRARY_ARCHITECTURE.md new file mode 100644 index 00000000..7e74a292 --- /dev/null +++ b/docs/UKNOWLEDGE_OFFLINE_LIBRARY_ARCHITECTURE.md @@ -0,0 +1,191 @@ +# uKnowledge Offline Library Architecture + +## Product intent + +uKnowledge is the offline knowledge layer of uDos. It should remain useful when +internet services, cloud accounts, and remote models are unavailable. Its public +library aims for broad general-reference coverage comparable to a compact offline +encyclopaedia, while going further in practical, local, and actionable knowledge. + +The installed Global Knowledge vault is `~/Public/global-knowledge`. It is read-only +in normal user mode. uKnowledge owns the contracts that validate, package, index, +search, update, and serve signed editions. uCore hosts the Vue experience; it does +not own the corpus or knowledge engine. + +## Boundaries + +| Owner | Responsibility | +| --- | --- | +| Global Knowledge vault | Read-only installed editions for normal users; canonical corpus writes and releases are restricted to the Dev/maintainer workflow. | +| uKnowledge | Corpus schema, validation, provenance, offline indexes, packages/deltas, search/retrieval, citations, integrity, and knowledge APIs. | +| uCore | Browse/search/read UI, download/update controls, storage reporting, and links into Learning and Workflow. | +| BrowserUI | User research workbench: capture, snapshot, cite, extract, compare, enhance, and save into a user-owned knowledge vault; optionally create a Global Knowledge submission. | +| Learning Pathway | Sequenced lessons, exercises, assessment, and progress built from cited knowledge items. | +| uCode | The small supported coding/runtime language and user-facing computing concepts documented in the knowledge bank. | +| SonicScrewdriver | Device identity, specifications, firmware/reflash knowledge, compatibility, transformation recipes, and device-derived portals. | +| User/add-on vaults | Personal notes and specialist packs. They may be indexed alongside the public library but are not part of its distributable edition. | + +General computing coverage should teach the concepts required to understand and use +uDos and uCode. It should not attempt to mirror documentation for every programming +language, operating system, or machine. Device-specific technical depth belongs in +SonicScrewdriver's device library and may be linked by stable identifiers. + +## Access and contribution boundary + +- Normal user mode may browse, search, cite, link, and learn from an installed Global + Knowledge edition. It cannot modify that edition. +- BrowserUI writes research into the user's own vault, or another explicitly selected + writable add-on/shared vault. It never writes directly into Global Knowledge. +- A user can create a portable contribution package containing proposed Markdown, + sources, provenance, licence assertions, diffs, and optional supporting media. +- Submission does not grant publication. It enters a review queue analogous to a wiki + contribution or pull request. +- Only Dev Mode maintainers may accept submissions into the canonical candidate + corpus, edit release material, sign an edition, or publish update packages. +- Installed editions are replaced or updated atomically from validated packages; + local annotations and user extensions remain in user-owned vaults. + +This is a capability rule, not merely a hidden button. uKnowledge's write APIs must +reject canonical-corpus mutation without an authorized Dev/maintainer context. + +This boundary reconciles existing specifications rather than introducing a new +model: `VAULT_BINDER_WORKFLOW_INTEGRATION.md` already classifies Public vaults as +read-only, `VAULT_PLATES_AND_DESTROY_SPEC.md` defines Global Knowledge as a read-only +seed plate, and the corpus's `survival/INDEX.md` directs contributions through a +separate knowledge-bank system. + +## Maintainer corpus lanes + +Every item must be in exactly one lane: + +1. **Release** — verified metadata, defensible redistribution licence, reviewed + safety/accuracy, and included in a signed edition. +2. **Candidate** — useful material being normalized, sourced, reviewed, or rewritten. +3. **Reference quarantine** — private research that cannot be redistributed or whose + provenance is uncertain. It is never included in packages or public indexes. +4. **Compost** — duplicates, superseded conversions, corrupt imports, and rejected + material retained only while provenance or recovery work remains useful. + +The current `contributor/` tree contains numerous apparent book conversions. Until +each item has a provenance and licence record, it must be treated as reference +quarantine rather than distributable content. + +## Required item metadata + +Each release candidate needs a stable identifier and machine-readable metadata: + +- title, summary, topics, audience, reading level, language, and region; +- author/publisher, source URL or source record, acquisition date, and content hash; +- licence identifier, redistribution status, attribution, and modification policy; +- created, reviewed, and freshness dates plus reviewer identity/method; +- safety class, evidence/citation list, geographic limits, and explicit uncertainty; +- relationships to prerequisites, related items, lessons, uCode concepts, and Sonic + device identifiers. + +High-stakes medical, food safety, electrical, structural, weapons, and emergency +content requires stronger review policy and conspicuous limitations. Generated or +converted prose is not considered verified merely because it is well structured. + +## Offline edition contract + +A distributable edition is immutable and content-addressed. It contains: + +- a signed edition manifest with schema and minimum-runtime versions; +- normalized source documents and approved media; +- a compact lexical index that works without a model; +- optional local embeddings built from the exact release content; +- topic graph, redirects, aliases, citations, and learning relationships; +- per-file hashes, total size, locale/region coverage, and licence inventory; +- optional delta packages from prior editions and a complete rollback path. + +Search must degrade gracefully: lexical search and browsing always work; semantic +search is an optional local enhancement. No core read/search action may require +AppFlowy, a cloud model, or an internet connection. + +## BrowserUI acquisition pipeline + +BrowserUI is not a general-purpose browser surface. It is the contextual research +workbench used by Intelligence and Workflow when online material should become +durable offline knowledge in a user-owned vault: + +1. Capture the URL, retrieval time, publisher/author, licence signals, and a hash or + permitted snapshot before transformation. +2. Extract useful text, tables, media references, and citations into portable + Markdown plus structured source metadata. +3. Compare against the local corpus, identify duplicates/conflicts, and attach the + result to an existing topic or create a candidate topic. +4. Use local Ollama models first for classification, cleanup, tags, summaries, and + link suggestions. Use free/low-cost OpenRouter models only when policy and budget + allow; reserve frontier review for genuinely difficult or high-risk material. +5. Keep source text, model-produced changes, and reviewer decisions distinguishable. + A model may propose edits but may not invent provenance, erase uncertainty, or + promote a candidate into a release edition. +6. Save the resulting inspectable Markdown into the selected user-owned knowledge + vault. If the user chooses to contribute it, build a submission package and create + a uFlow submission/review task without mutating the installed Global Knowledge. + +In Dev Mode, an authorized maintainer can review a submission, request changes, +accept it into the canonical candidate lane, and later include it in a signed release. + +Because Markdown is the durable substrate, low-cost agents can perform most routine +corpus development. Quality comes from schemas, citations, diffs, validators, +budgets, and review gates rather than requiring a frontier model for every document. + +## Device portal contract + +SonicScrewdriver owns a separate device corpus keyed by stable device identifiers. +uKnowledge may provide general principles and link to device records, but it must not +absorb volatile per-model specifications or firmware recipes. + +A future device portal can accept a photo or observed attributes, produce candidate +identities with confidence and evidence, resolve a Sonic device record, and present: + +- what the device is and what useful components/capabilities it contains; +- safe inspection, recovery, reuse, or reflashing options; +- compatible uDos runtime images and required tools; +- cited general knowledge and a guided Learning/Workflow path. + +Recognition may be model-assisted, but identity must remain confirmable offline from +observable features and the local device library. + +## Stabilized implementation (2026-08-18) + +- `~/Public/global-knowledge` is approximately 409 MB with 1,157 files, including + about 501 Markdown files. Its strongest coverage is practical survival knowledge. +- The vault is on `knowledge-maintenance-20260614-192525` with an untracked `doclang/` + export. Generated personal-vault DocLang does not belong in a public edition. +- Root documentation contradicts the current tree: it describes a migrated subset + and archived topic trees that are physically present. +- Index/version figures are stale and disagree with the filesystem. +- Provenance and redistribution metadata are insufficient for public packaging. +- uKnowledge now owns filesystem-first workspace registration, safe Markdown read, + lexical search, and the Public-vault write boundary. uCore delegates to those + contracts instead of owning a second knowledge implementation. +- Mutable workspace and index state resolves beneath `$UDOS_HOME` (normally + `~/Code/.udos`); public and user vaults remain portable Markdown trees. +- BrowserUI reads live knowledge/bookmark state rather than sample stacks and saves + only through the selected writable workspace contract. + +## Stabilization sequence + +1. Preserve the current corpus and create an auditable inventory without publishing. +2. Enforce read-only installed editions, writable user knowledge vaults, and an + explicit contribution-package boundary. +3. Separate release, candidate, reference-quarantine, compost, and generated output. +4. Define schemas for items, sources, licences, editions, submissions, citations, and + device links. +5. Maintain the completed filesystem-first reader/search boundary without AppFlowy + or uCore ownership regressions. +6. Keep all mutable indexes, caches, registries, and jobs in `UDOS_HOME`; keep the + public vault portable and human-readable. +7. Build a deterministic edition validator and a minimal lexical offline package. +8. Extend the wired uCore browser/search/read experience with clear offline, + provenance, freshness, and safety states. +9. Extend BrowserUI's live knowledge/bookmark foundation with the full + provenance-preserving contribution pipeline. +10. Curate a balanced minimum edition: orientation, language, maths, science, + geography, history/civics, health, practical life, nature, making/repair, + emergency readiness, and uDos/uCode basics. +11. Connect reviewed items to the Learning Pathway. +12. After the core stabilizes, define stable cross-links to SonicScrewdriver's device + library and build the first device-to-knowledge portal journey. diff --git a/docs/WORKSTATION_MIGRATION_2026-08-18.md b/docs/WORKSTATION_MIGRATION_2026-08-18.md new file mode 100644 index 00000000..f5dc073a --- /dev/null +++ b/docs/WORKSTATION_MIGRATION_2026-08-18.md @@ -0,0 +1,70 @@ + +# Workstation Migration — 2026-08-18 + +## uFlow task-state migration + +The 32 Markdown files formerly stored at `uCore/.tasker` were verified and moved to +the uFlow-owned runtime location `~/Code/.udos/flow/tasks`. The original directory is +quarantined at `~/Code/ARCHIVED/cleanup-2026-08-18/uCore-runtime-state/tasker` and can +be recovered until the stabilization branches are merged and accepted. + +The ecosystem Python environment now lives at `~/Code/.venv`; uCore's historical +`.venv` paths are compatibility symlinks. Setup installs uFlow and uKnowledge as +editable packages into that shared environment. + +**Status:** Completed with compatibility links + +**Host:** fredbook +**Canonical runtime:** `/Users/fredbook/Code/.udos` + +## Result + +- Current uCore state moved from `~/.ucore` to `~/Code/.udos`. +- Historical uDOS, Snackbar, uCode, HomeNest and related state roots preserved + beneath `~/Code/.udos/legacy-home-roots`. +- Colima data moved beneath `~/Code/.udos/runtimes/colima` and verified healthy. +- Ollama models moved beneath `~/Code/.udos/runtimes/ollama`; eight models were + visible after restart. +- Developer SDKs, environments and caches moved beneath + `~/Code/.udos/toolchains/home-roots`. +- Obsolete VS Code and external-agent data moved to + `~/Code/ARCHIVED/cleanup-2026-08-18` for a rollback window. +- Docker and Kubernetes configuration were restored physically to `~/.docker` + and `~/.kube` because they contain external-integration credentials/context. +- User vaults were not modified. + +## Intentionally retained home state + +The remaining directories are owned by macOS, Codex, credentials, or installed +applications rather than uDOS: `.codex`, `.config`, `.ssh`, `.docker`, `.kube`, +`.adobe`, `.cups`, `.dropbox`, `.slack`, `.swiftbar-plugin-state`, `.swiftpm`, +`.homebrew`, `.zsh_sessions`, and `.Trash`. + +GitHub CLI authentication remains in `~/.config/gh`; SSH and Git identity remain +in their standard locations. These paths are excluded from uDOS destruction. + +## Temporary compatibility links + +Compatibility links remain for historical consumers, including `.ucore`, +`.udos`, `.colima`, `.ollama`, `.local`, `.nvm`, `.npm`, `.cache`, `.android`, +language tool caches and old virtual environments. Their data resides physically +under `~/Code/.udos`. New code must not use these links as canonical paths. + +They can be removed individually after repository scans report no active +consumer and launch/login tests pass without them. + +## Verification + +- uCore backend `/api/health`: healthy, version 4.0.5. +- uCore frontend: HTTP 200 on port 5175. +- Menu, server and frontend launch jobs: running. +- Menu launch policy: start at login, restart unsuccessful exits, respect clean + user Quit until the next login/restart. +- Colima: running with Docker runtime under Virtualization.Framework. +- Ollama: service running with eight migrated models visible. +- Node 20.20.2 and npm 10.8.2 resolve through compatibility paths. +- GitHub CLI authentication remains available for the `fredporter` account. +- Docker context `colima` is healthy on server version 29.2.1. +- No Kubernetes context is currently selected. +- The SSH agent currently has no loaded identities; key files remain untouched + in the standard `~/.ssh` directory. diff --git a/docs/specs/UDEV_BINDER_COMPILER_INTEGRATION_CHECKLIST_2026-08.md b/docs/specs/UDEV_BINDER_COMPILER_INTEGRATION_CHECKLIST_2026-08.md deleted file mode 100644 index 01b469df..00000000 --- a/docs/specs/UDEV_BINDER_COMPILER_INTEGRATION_CHECKLIST_2026-08.md +++ /dev/null @@ -1,83 +0,0 @@ -# uDev Binder Compiler Integration Checklist (uCore Alignment) - -Status: Draft for Wave 1 execution -Date: 2026-08-03 -Scope: uDev + uCore parallel alignment - -## Goal - -Ensure uDev can compile deterministic Binder context and consume uCore contracts without duplicating ownership. - -## Contract Baseline - -1. uCore owns runtime APIs, capability readiness, and extension registry. -2. uDev owns Binder authoring/visibility and context compiler UX. -3. Every AI action in uDev must carry inspectable compiled context. - -## Required uCore Endpoints (must remain stable) - -- GET /api/capabilities/readiness -- GET /api/capabilities/{capability}/preflight -- GET /api/control/status -- GET /api/system/workflow -- Chat endpoints used by uDev: - - POST /api/chat/stream - - POST /api/chat - -## Binder Compiler Input Contract (uDev -> uCore coordination) - -Required binder inputs: - -- binder/current.md -- binder/architecture.md -- binder/roadmap.md -- binder/decisions.md -- repository branch/head metadata -- selected lane and active rules -- active issue/task reference (if available) - -Compiler output: - -- binder/context.json validated by binder/context.schema.json -- stable fingerprint hash for identical input sets - -## Integration Tasks - -## A. Capability and Policy Integration - -- [ ] Confirm control panel capabilities remain aligned with capability_requirements.json keys. -- [ ] Ensure readiness/preflight failures always return actionable repair payloads. -- [ ] Verify HTTP 412 semantics are preserved and surfaced to uDev. - -## B. Context Attachment Path - -- [ ] Define server-accepted envelope fields for context payload attachment to chat actions. -- [ ] Validate no hidden server-side prompt state overrides Binder intent. -- [ ] Add explicit logging marker when Binder context is attached successfully. - -## C. Determinism and Auditability - -- [ ] Add validation check that context schema version is supported. -- [ ] Ensure deterministic normalization rules are documented and testable. -- [ ] Add evidence output in logs/telemetry for context fingerprint per request. - -## D. Reliability Gates - -- [ ] CI: validate capability requirements parity (already present). -- [ ] CI: validate docs/runtime parity for capability counts and key contracts. -- [ ] Add lightweight contract probe script for Binder-context chat envelope acceptance. - -## E. Dogfood Runbook (Daily) - -- [ ] Start in uDev binder/current.md. -- [ ] Regenerate/verify binder/context.json. -- [ ] Run one AI task with context attachment. -- [ ] Verify preflight/readiness before capability actions. -- [ ] Record decisions and blockers in Binder markdown. - -## Exit Criteria (Wave 1) - -1. uDev control routes never target invalid tabs. -2. uDev sends validated binder/context.json with default AI actions. -3. uCore returns deterministic preflight/readiness responses for all Wave 1 capabilities. -4. One full MVP loop completes daily without manual context reconstruction. diff --git a/docs/specs/WISDOM_SYSTEM.md b/docs/specs/WISDOM_SYSTEM.md index 7a26f7bd..b396f712 100644 --- a/docs/specs/WISDOM_SYSTEM.md +++ b/docs/specs/WISDOM_SYSTEM.md @@ -13,14 +13,14 @@ The Wisdom system gives agents durable project context without committing person | Context | `CONTEXT.md` | tracked | Public project architecture and working conventions | | Wisdom template | `docs/templates/wisdom.md` | tracked | Sanitized seed for local installs | | Fieldnotes template | `docs/templates/fieldnotes.md` | tracked | Sanitized seed for local installs | -| Private wisdom | `~/.ucore/memory/uCore/wisdom.md` | untracked | Generated durable lessons and local synthesis | -| Private fieldnotes | `~/.ucore/memory/uCore/fieldnotes.md` | untracked | Optional developer notebook | +| Private wisdom | `$UDOS_HOME/memory/uCore/wisdom.md` | untracked | Generated durable lessons and local synthesis | +| Private fieldnotes | `$UDOS_HOME/memory/uCore/fieldnotes.md` | untracked | Optional developer notebook | | Legacy local files | `wisdom.md`, `fieldnotes.md` | ignored | Local-only compatibility files if they already exist | ## Runtime Flow 1. `brain_sync` reads existing private wisdom, recent project changes, spool activity, test failure signals, and episodic entries. -2. `brain_sync` writes refreshed wisdom to `~/.ucore/memory/uCore/wisdom.md`. +2. `brain_sync` writes refreshed wisdom to `$UDOS_HOME/memory/uCore/wisdom.md`. 3. `attach_context` injects `CONTEXT.md` plus private wisdom when available. 4. `backup` includes private wisdom in local backups. 5. `tasker_ingest` appends durable lessons to private wisdom, not to the repository root. diff --git a/docs/templates/fieldnotes.md b/docs/templates/fieldnotes.md index 9cff508e..c751c5c6 100644 --- a/docs/templates/fieldnotes.md +++ b/docs/templates/fieldnotes.md @@ -9,7 +9,7 @@ Use fieldnotes for optional local observations that are useful during developmen ## Local Path -`~/.ucore/memory/uCore/fieldnotes.md` +`~/Code/.udos/memory/uCore/fieldnotes.md` (or `$UDOS_HOME/memory/uCore/fieldnotes.md`) ## Promotion Rule diff --git a/docs/templates/wisdom.md b/docs/templates/wisdom.md index e18cc7b1..928bd89b 100644 --- a/docs/templates/wisdom.md +++ b/docs/templates/wisdom.md @@ -9,7 +9,7 @@ Status: Seed template for local project wisdom ## Memory Architecture - Public repo: this template and documentation only. -- Private local state: `~/.ucore/memory/uCore/wisdom.md`. +- Private local state: `~/Code/.udos/memory/uCore/wisdom.md` (or `$UDOS_HOME/memory/uCore/wisdom.md`). - Runtime integration: `brain_sync` refreshes private wisdom; `attach_context` injects it with `CONTEXT.md` when available. ## Synthesis Inputs diff --git a/frontend-vue/src/grid-core/teletext/index.ts b/frontend-vue/src/grid-core/teletext/index.ts new file mode 100644 index 00000000..21237ae9 --- /dev/null +++ b/frontend-vue/src/grid-core/teletext/index.ts @@ -0,0 +1,74 @@ +// ── uCore Frontend: Teletext Module ────────────────────────────── +// E1: extracted from UCodeSurface.vue +// Re-exports stateless types/helpers/builders from @udos/gridcore's reader-model, +// plus adds Vue-specific/reactive config and functions (loaders, renderers that +// use local grid-core buffer functions). +// ──────────────────────────────────────────────────────────────────── + +// ── Re-exports from @udos/gridcore (stateless) ─────────────────── +export { + ceefaxClock, + DOC_PAGE_OFFSET, + DOC_SCREEN_LINES, + docContentPage, + docListPage, + DOCS_PER_LIST_PAGE, + docScreens, + // Helpers (pure) + docTitle, + helpPage, + libraryForPage, + // Page builders (pure, take BuilderContext) + mainIndexPage, + MAX_DOCS_PER_LIBRARY, + newsPage, + subIndexPage, + // Constants + TELETEXT_FASTEXT, + teletextContent, + wrapText, + writeBoxedDoubleHeightTitle, + // Layout renderers (pure, target ReaderBuffer) + writeDoubleHeight, + writeMosaicRule, + writeSeparatedBar, + type BuilderContext, + type PublicLibraryDef, + type ReaderBuffer, + type ReaderBufferCell, + // Types + type ReaderTeletextPage, + type VaultDoc, + type VaultLibrary, +} from "@udos/gridcore/teletext"; + +// ── Local config: uCore vault public library definitions ────────── +// This maps uCore's vault sources (public / global-knowledge) to Ceefax page ranges. +import type { PublicLibraryDef } from "@udos/gridcore/teletext"; + +export const PUBLIC_LIBRARY_DEFS: PublicLibraryDef[] = [ + { + id: "documentation", + label: "Documentation", + source: "public", + tag: "doc-sites", + page: 200, + colour: 2, + }, + { + id: "knowledge", + label: "Global Knowledge", + source: "global-knowledge", + tag: null, + page: 300, + colour: 3, + }, + { + id: "learning", + label: "Learning", + source: "public", + tag: "learning", + page: 400, + colour: 6, + }, +]; diff --git a/frontend-vue/src/router/index.ts b/frontend-vue/src/router/index.ts index 7e014c30..330287f9 100644 --- a/frontend-vue/src/router/index.ts +++ b/frontend-vue/src/router/index.ts @@ -21,7 +21,11 @@ const routes: RouteRecordRaw[] = [ }, { path: "/assistui/:pathMatch(.*)*", - redirect: "/intelligence", + redirect: (to) => { + const tab = String(to.query.tab || "chat"); + if (tab === "agents") return "/snackbar?tab=agents"; + return { path: "/intelligence", query: to.query }; + }, }, { path: "/intelligence/:pathMatch(.*)*", @@ -79,7 +83,7 @@ const routes: RouteRecordRaw[] = [ const tab = String(to.query.tab || "snacks"); if (tab === "workflows") return "/workflow?tab=publish"; if (tab === "vault") return "/workflow?tab=binder"; - if (tab === "mcp") return "/developer?tab=mcp-servers"; + if (tab === "mcp") return "/snackbar?tab=mcp"; if (tab === "variables") return "/system?tab=variables"; if (tab === "scheduler") return "/snackbar?tab=dashboard"; return "/snackbar?tab=snacks"; @@ -140,6 +144,13 @@ export const router = createRouter({ routes, }); +router.beforeEach((to) => { + // Agent selection is operational state, not a user-facing Intelligence mode. + if (to.path.startsWith("/intelligence") && to.query.tab === "agents") { + return { path: "/snackbar", query: { tab: "agents" } }; + } +}); + const DYNAMIC_IMPORT_RELOAD_KEY = "ucore.router.dynamic-import-reload"; const RUNTIME_WARNING_KEY = "ucore.runtime.warning"; diff --git a/frontend-vue/src/skills/organisms/OverlayLayer.vue b/frontend-vue/src/skills/organisms/OverlayLayer.vue index 8954e910..9844ba8c 100644 --- a/frontend-vue/src/skills/organisms/OverlayLayer.vue +++ b/frontend-vue/src/skills/organisms/OverlayLayer.vue @@ -6,7 +6,6 @@