diff --git a/.github/workflows/roadmap.yml b/.github/workflows/roadmap.yml new file mode 100644 index 000000000..10ced766d --- /dev/null +++ b/.github/workflows/roadmap.yml @@ -0,0 +1,41 @@ +name: roadmap + +# Keep ROADMAP.md in sync with roadmap.yaml and the per-spec tasks.md badges. +# ROADMAP.md is a generated file (scripts/gen-roadmap.py); this canary fails the +# build if it is stale so the roadmap never silently drifts. Regenerate locally +# with `python3 scripts/gen-roadmap.py` and commit the result. + +on: + pull_request: + paths: + - 'roadmap.yaml' + - 'ROADMAP.md' + - 'scripts/gen-roadmap.py' + - 'specs/**/tasks.md' + - '.github/workflows/roadmap.yml' + push: + branches: [main, next] + paths: + - 'roadmap.yaml' + - 'ROADMAP.md' + - 'scripts/gen-roadmap.py' + - 'specs/**/tasks.md' + - '.github/workflows/roadmap.yml' + +permissions: + contents: read + +jobs: + roadmap-check: + name: roadmap-up-to-date + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install PyYAML + run: pip install pyyaml + - name: Verify ROADMAP.md is up to date + run: python3 scripts/gen-roadmap.py --check diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b7b48e458..386a06b64 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,6 +25,13 @@ repos: stages: [pre-push] pass_filenames: false + - id: roadmap-verify + name: Verify ROADMAP.md is up to date + entry: python3 scripts/gen-roadmap.py --check + language: system + pass_filenames: false + files: '^(roadmap\.yaml|ROADMAP\.md|scripts/gen-roadmap\.py|specs/.*/tasks\.md)$' + - id: go-build name: go build entry: go build -o /dev/null ./cmd/mcpproxy diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 000000000..fb2d3ce7c --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,234 @@ + + + + +# MCPProxy Roadmap + +> **Generated — do not edit by hand.** This file is rendered from [`roadmap.yaml`](./roadmap.yaml) by [`scripts/gen-roadmap.py`](./scripts/gen-roadmap.py). Edit `roadmap.yaml` and re-run the generator. + +The roadmap models cross-spec **epics → tasks** with a dependency DAG, execution `status`, `assignee`, `priority`, and links — the things a per-spec `tasks.md` checkbox list cannot express. Per-spec checkbox progress is recomputed live from each `specs//tasks.md`. + +## How to regenerate + +```bash +python3 scripts/gen-roadmap.py # writes ROADMAP.md +scripts/gen-roadmap # convenience wrapper (same thing) +python3 scripts/gen-roadmap.py --check # CI canary: fail if stale +``` + +## roadmap.yaml schema (short form) + +- **epics[]** — each has `id` (stable slug, DAG node), `title`, `status` (todo·in_progress·in_review·blocked·done), `assignee`, `priority` (P0–P3), `depends_on: [ids]` (DAG edges, prerequisite→dependent), optional `parked: true`, and links `spec:` / `pr:` / `mcp:` (external MCP-xxxx). +- **epics[].tasks[]** — child tasks with the same fields; their `depends_on` may reference sibling tasks or other epics. +- See the header comment in `roadmap.yaml` for the full field reference. + +## Epic / task DAG + +Node colour = status (green done · blue in-progress · amber in-review · red blocked · grey todo · dashed grey parked). Edges point prerequisite → dependent. + +```mermaid +graph TD + subgraph sg_profiles_v2["Profiles v2 (per-profile tool views)"] + profiles_v2["Profiles v2 (per-profile tool views)
MCP-33"] + profiles_v2_indexes["Per-profile Bleve indexes (T1)
MCP-3240"] + profiles_v2_set_profile["set_profile tool + session resolver + REST (T2)
MCP-3241"] + profiles_v2_profile_pin["Per-agent-token profile_pin (T3)
MCP-3242"] + profiles_v2_tray_switcher["Tray profile switcher Go + Swift (T5)
MCP-3244"] + end + subgraph sg_sandbox_isolation["Non-Docker sandbox isolation (Landlock)"] + sandbox_isolation["Non-Docker sandbox isolation (Landlock)
MCP-34"] + sandbox_spike["Landlock sandbox spike (MCP-34.1)
MCP-3232"] + sandbox_mode_config["isolation.mode enum + resolver (MCP-34.2)
MCP-3233"] + sandbox_launcher["Native sandbox launcher Landlock+rlimits (MCP-34.3)
MCP-3234"] + sandbox_scanner_parity["Scanner-flow parity under sandbox (MCP-34.4)
MCP-3235"] + sandbox_snap_docker_it["snap-docker integration tests + CI (MCP-34.5)
MCP-3236"] + end + subgraph sg_ts_code_exec_ga["TypeScript code-execution GA + cookbook"] + ts_code_exec_ga["TypeScript code-execution GA + cookbook
MCP-38"] + ts_code_exec_cookbook["Cookbook (10 TS recipes) + GA docs
MCP-38"] + end + subgraph sg_scanner_v2["Spec 076 deterministic offline tool-scanner"] + scanner_v2["Spec 076 deterministic offline tool-scanner
MCP-3574"] + scanner_v2_foundation["detect-engine foundation (T1)
MCP-3575"] + scanner_v2_hard_checks["3 hard checks + scanner wiring (US1 MVP)
MCP-3576"] + scanner_v2_soft_checks["3 soft checks + patterns confidence (US2)
MCP-3577"] + scanner_v2_consensus["Consensus risk-score + report transparency (US4)
MCP-3578"] + scanner_v2_eval_gate["Eval corpus + CI recall/FP gate (US3)
MCP-3579"] + scanner_v2_docs["Tool-scanner detect-engine docs (T22)
MCP-3683"] + end + subgraph sg_windows_tray["Windows native tray app"] + windows_tray["Windows native tray app
MCP-43"] + windows_tray_window["WebView2 native window + profile submenu
MCP-43"] + end + subgraph sg_ux_audit["Web UI + macOS app UX audit"] + ux_audit["Web UI + macOS app UX audit"] + ux_audit_webui_sweep["Web UI heuristic + Playwright UX sweep"] + ux_audit_macos_sweep["macOS tray app UX sweep (settings parity, flows)"] + end + subgraph sg_action_log_transparency["Action log / transparency — info at a glance"] + action_log_transparency["Action log / transparency — info at a glance"] + action_log_glance_view["At-a-glance action log view (top signals, health)"] + action_log_retention_tie_in["Tie activity retention/size into the glance view"] + end + subgraph sg_analytics_dashboard["Analytics dashboard as default page"] + analytics_dashboard["Analytics dashboard as default page"] + analytics_token_drain_graphs["Per-server / per-tool token-drain graphs"] + analytics_default_landing["Make dashboard the default landing page"] + end + subgraph sg_registries_search_add["Registries — easier search + add-server"] + registries_search_add["Registries — easier search + add-server"] + registries_search_ux["Improved registry search UX"] + registries_official_protocol["Official registry protocol integration"] + end + subgraph sg_scanner_simplification["Scanner simplification (deterministic default, opt-in deep scan)"] + scanner_simplification["Scanner simplification (deterministic default, opt-in deep scan)"] + scanner_simpl_baseline["US1: deterministic offline baseline default + curated hard phrase_injection check (delete duplicate legacy rules)"] + scanner_simpl_unified_report["US2: single merged report + cross-scanner consensus confidence"] + scanner_simpl_deep_optin["US3: opt-in deep scan (off by default), never blocks/degrades baseline; config migration"] + scanner_simpl_notifications["US4: collapse scan-notification storm into one debounced settled event (MCP-2207)"] + end + marketplace["Server marketplace
MCP-37"] + siem["Audit SIEM integration
MCP-39"] + paid_tier["Paid-tier MVP (billing / seats / license)
MCP-40"] + sdk_v1_migration["SDK v1 migration"] + sso["SSO (server edition)"] + + profiles_v2_indexes --> profiles_v2_set_profile + profiles_v2_set_profile --> profiles_v2_profile_pin + profiles_v2_set_profile --> profiles_v2_tray_switcher + sandbox_spike --> sandbox_mode_config + sandbox_mode_config --> sandbox_launcher + sandbox_launcher --> sandbox_scanner_parity + scanner_v2 --> sandbox_scanner_parity + sandbox_scanner_parity --> sandbox_snap_docker_it + scanner_v2_foundation --> scanner_v2_hard_checks + scanner_v2_foundation --> scanner_v2_soft_checks + scanner_v2_hard_checks --> scanner_v2_consensus + scanner_v2_soft_checks --> scanner_v2_consensus + scanner_v2_hard_checks --> scanner_v2_eval_gate + scanner_v2_eval_gate --> scanner_v2_docs + ux_audit --> action_log_transparency + action_log_glance_view --> action_log_retention_tie_in + ux_audit --> analytics_dashboard + analytics_token_drain_graphs --> analytics_default_landing + ux_audit --> registries_search_add + scanner_v2 --> scanner_simplification + scanner_simpl_baseline --> scanner_simpl_unified_report + scanner_simpl_baseline --> scanner_simpl_deep_optin + scanner_simpl_unified_report --> scanner_simpl_deep_optin + scanner_simpl_unified_report --> scanner_simpl_notifications + + classDef done fill:#1f7a1f,stroke:#0d3d0d,color:#ffffff; + classDef in_progress fill:#1f6feb,stroke:#0b3d91,color:#ffffff; + classDef in_review fill:#9a6700,stroke:#5c3d00,color:#ffffff; + classDef blocked fill:#a40e26,stroke:#5c0712,color:#ffffff; + classDef todo fill:#6e7781,stroke:#3d4248,color:#ffffff; + classDef parked fill:#30363d,stroke:#161b22,color:#9da7b3,stroke-dasharray:4 3; + class profiles_v2,profiles_v2_indexes,profiles_v2_set_profile,profiles_v2_profile_pin,profiles_v2_tray_switcher,sandbox_isolation,sandbox_spike,sandbox_mode_config,sandbox_launcher,sandbox_scanner_parity,sandbox_snap_docker_it,ts_code_exec_ga,ts_code_exec_cookbook,scanner_v2,scanner_v2_foundation,scanner_v2_hard_checks,scanner_v2_soft_checks,scanner_v2_consensus,scanner_v2_eval_gate,scanner_v2_docs done; + class scanner_simplification in_progress; + class windows_tray,windows_tray_window in_review; + class ux_audit,ux_audit_webui_sweep,ux_audit_macos_sweep,action_log_transparency,action_log_glance_view,action_log_retention_tie_in,analytics_dashboard,analytics_token_drain_graphs,analytics_default_landing,registries_search_add,registries_search_ux,registries_official_protocol,scanner_simpl_baseline,scanner_simpl_unified_report,scanner_simpl_deep_optin,scanner_simpl_notifications todo; + class marketplace,siem,paid_tier,sdk_v1_migration,sso parked; +``` + +## Epics + +| Epic | Status | Assignee | Priority | Progress | Spec | PR | +| --- | --- | --- | --- | --- | --- | --- | +| Scanner simplification (deterministic default, opt-in deep scan) | In progress | unassigned | P1 | — | [077-scanner-simplification](./specs/077-scanner-simplification/) | | +| Windows native tray app `MCP-43` | In review | BackendEngineer | P2 | 25/60 (42%) | [002-windows-installer](./specs/002-windows-installer/) | | +| Web UI + macOS app UX audit | Todo | unassigned | P0 | — | [064-glass-cockpit](./specs/064-glass-cockpit/) | | +| Action log / transparency — info at a glance | Todo | unassigned | P0 | 63/66 (95%) | [024-expand-activity-log](./specs/024-expand-activity-log/) | | +| Analytics dashboard as default page | Todo | unassigned | P1 | 16/26 (62%) | [069-observability-usage-graphs](./specs/069-observability-usage-graphs/) | | +| Registries — easier search + add-server | Todo | unassigned | P1 | 3/24 (12%) | [070-registry-easy-upstream-add](./specs/070-registry-easy-upstream-add/) | | +| Server marketplace `MCP-37` | Todo (parked) | | P3 | 3/24 (12%) | [070-registry-easy-upstream-add](./specs/070-registry-easy-upstream-add/) | | +| Audit SIEM integration `MCP-39` | Todo (parked) | | P3 | — | | | +| Paid-tier MVP (billing / seats / license) `MCP-40` | Todo (parked) | | P3 | — | | | +| SDK v1 migration | Todo (parked) | | P3 | — | | | +| SSO (server edition) | Todo (parked) | | P3 | — | | | +| Profiles v2 (per-profile tool views) `MCP-33` | Done | BackendEngineer | P1 | — | | | +| Non-Docker sandbox isolation (Landlock) `MCP-34` | Done | BackendEngineer | P1 | — | [054-mcp-security-gateway](./specs/054-mcp-security-gateway/) | | +| Spec 076 deterministic offline tool-scanner `MCP-3574` | Done | BackendEngineer | P1 | 22/24 (92%) | [076-deterministic-tool-scanner](./specs/076-deterministic-tool-scanner/) | | +| TypeScript code-execution GA + cookbook `MCP-38` | Done | BackendEngineer | P2 | 19/19 (100%) | [033-typescript-code-execution](./specs/033-typescript-code-execution/) | | + +## Per-spec progress (recomputed from `specs//tasks.md`) + +Legend: `shipped` ≥95% checked · `in-flight` 1–94% · `drafted` 0% · `—` no `tasks.md`. This aggregate is regenerated here rather than overwriting the hand-maintained [`specs/README.md`](./specs/README.md), which keeps its curated prose, runbooks and design-doc links. + +| # | Status | Progress | +| --- | --- | --- | +| [001-code-execution](./specs/001-code-execution/) | `drafted` | 0/127 (0%) | +| [001-fix-skipped-auth-tests](./specs/001-fix-skipped-auth-tests/) | — | — | +| [001-oas-endpoint-documentation](./specs/001-oas-endpoint-documentation/) | `in-flight` | 49/69 (71%) | +| [001-oauth-scope-discovery](./specs/001-oauth-scope-discovery/) | — | — | +| [001-update-version-display](./specs/001-update-version-display/) | `in-flight` | 11/58 (19%) | +| [002-windows-installer](./specs/002-windows-installer/) | `in-flight` | 25/60 (42%) | +| [003-tool-annotations-webui](./specs/003-tool-annotations-webui/) | `in-flight` | 10/64 (16%) | +| [004-management-health-refactor](./specs/004-management-health-refactor/) | `in-flight` | 45/101 (45%) | +| [005-rest-management-integration](./specs/005-rest-management-integration/) | `shipped` | 45/45 (100%) | +| [006-oauth-extra-params](./specs/006-oauth-extra-params/) | `in-flight` | 31/65 (48%) | +| [007-oauth-e2e-testing](./specs/007-oauth-e2e-testing/) | `in-flight` | 88/103 (85%) | +| [008-oauth-token-refresh](./specs/008-oauth-token-refresh/) | `in-flight` | 57/64 (89%) | +| [009-proactive-oauth-refresh](./specs/009-proactive-oauth-refresh/) | `drafted` | 0/87 (0%) | +| [010-release-notes-generator](./specs/010-release-notes-generator/) | `in-flight` | 24/36 (67%) | +| [011-resource-auto-detect](./specs/011-resource-auto-detect/) | `shipped` | 39/39 (100%) | +| [012-docusaurus-docs-site](./specs/012-docusaurus-docs-site/) | `in-flight` | 74/89 (83%) | +| [012-unified-health-status](./specs/012-unified-health-status/) | `shipped` | 44/44 (100%) | +| [013-structured-server-state](./specs/013-structured-server-state/) | `shipped` | 46/46 (100%) | +| [013-tool-change-notifications](./specs/013-tool-change-notifications/) | `in-flight` | 26/45 (58%) | +| [014-cli-output-formatting](./specs/014-cli-output-formatting/) | `shipped` | 65/66 (98%) | +| [015-server-management-cli](./specs/015-server-management-cli/) | `shipped` | 50/50 (100%) | +| [016-activity-log-backend](./specs/016-activity-log-backend/) | `drafted` | 0/50 (0%) | +| [017-activity-cli-commands](./specs/017-activity-cli-commands/) | `drafted` | 0/60 (0%) | +| [018-intent-declaration](./specs/018-intent-declaration/) | `shipped` | 69/69 (100%) | +| [019-activity-webui](./specs/019-activity-webui/) | `shipped` | 73/73 (100%) | +| [020-oauth-login-feedback](./specs/020-oauth-login-feedback/) | — | — | +| [021-request-id-logging](./specs/021-request-id-logging/) | `in-flight` | 20/42 (48%) | +| [022-oauth-redirect-uri-persistence](./specs/022-oauth-redirect-uri-persistence/) | `shipped` | 24/25 (96%) | +| [023-oauth-state-persistence](./specs/023-oauth-state-persistence/) | `shipped` | 38/39 (97%) | +| [023-smart-config-patch](./specs/023-smart-config-patch/) | `shipped` | 52/53 (98%) | +| [024-expand-activity-log](./specs/024-expand-activity-log/) | `shipped` | 63/66 (95%) | +| [026-pii-detection](./specs/026-pii-detection/) | `shipped` | 130/130 (100%) | +| [027-status-command](./specs/027-status-command/) | `shipped` | 25/25 (100%) | +| [028-agent-tokens](./specs/028-agent-tokens/) | `drafted` | 0/43 (0%) | +| [029-mcpproxy-teams](./specs/029-mcpproxy-teams/) | `shipped` | 29/29 (100%) | +| [033-typescript-code-execution](./specs/033-typescript-code-execution/) | `shipped` | 19/19 (100%) | +| [034-expand-secret-refs](./specs/034-expand-secret-refs/) | `shipped` | 17/17 (100%) | +| [035-enhanced-annotations](./specs/035-enhanced-annotations/) | — | — | +| [037-macos-swift-tray](./specs/037-macos-swift-tray/) | — | — | +| [038-mcp-accessibility-server](./specs/038-mcp-accessibility-server/) | — | — | +| [039-connect-and-dashboard](./specs/039-connect-and-dashboard/) | — | — | +| [039-scanner-qa-audit](./specs/039-scanner-qa-audit/) | — | — | +| [039-security-scanner-plugins](./specs/039-security-scanner-plugins/) | — | — | +| [040-server-ux](./specs/040-server-ux/) | `drafted` | 0/35 (0%) | +| [041-quarantine-invariants](./specs/041-quarantine-invariants/) | — | — | +| [042-telemetry-tier2](./specs/042-telemetry-tier2/) | `drafted` | 0/91 (0%) | +| [043-linux-package-repos](./specs/043-linux-package-repos/) | `shipped` | 39/41 (95%) | +| [044-diagnostics-taxonomy](./specs/044-diagnostics-taxonomy/) | `drafted` | 0/106 (0%) | +| [044-retention-telemetry-v3](./specs/044-retention-telemetry-v3/) | `drafted` | 0/70 (0%) | +| [045-paperclip-cockpit](./specs/045-paperclip-cockpit/) | `in-flight` | 40/47 (85%) | +| [046-local-first-onboarding](./specs/046-local-first-onboarding/) | — | — | +| [046-local-launcher-for-http-sse](./specs/046-local-launcher-for-http-sse/) | — | — | +| [047-cpu-hotpath-fix](./specs/047-cpu-hotpath-fix/) | `in-flight` | 5/46 (11%) | +| [048-tray-refetch-elimination](./specs/048-tray-refetch-elimination/) | `in-flight` | 5/31 (16%) | +| [049-agent-discoverable-disabled-tools](./specs/049-agent-discoverable-disabled-tools/) | `shipped` | 18/18 (100%) | +| [050-global-tools-page](./specs/050-global-tools-page/) | `drafted` | 0/26 (0%) | +| [051-readme-hero-demo](./specs/051-readme-hero-demo/) | — | — | +| [053-oss-repo-improvements](./specs/053-oss-repo-improvements/) | — | — | +| [054-mcp-security-gateway](./specs/054-mcp-security-gateway/) | — | — | +| [055-docs-diataxis](./specs/055-docs-diataxis/) | — | — | +| [055-frontend-major-upgrades](./specs/055-frontend-major-upgrades/) | `drafted` | 0/24 (0%) | +| [056-output-schema-validation](./specs/056-output-schema-validation/) | `shipped` | 23/24 (96%) | +| [057-in-proxy-profiles](./specs/057-in-proxy-profiles/) | `drafted` | 0/25 (0%) | +| [058-mcp-2026-upgrade](./specs/058-mcp-2026-upgrade/) | — | — | +| [059-output-sanitisation](./specs/059-output-sanitisation/) | `shipped` | 25/25 (100%) | +| [060-settings-page](./specs/060-settings-page/) | `shipped` | 16/16 (100%) | +| [064-glass-cockpit](./specs/064-glass-cockpit/) | — | — | +| [065-evaluation-foundation](./specs/065-evaluation-foundation/) | — | — | +| [069-observability-usage-graphs](./specs/069-observability-usage-graphs/) | `in-flight` | 16/26 (62%) | +| [070-registry-easy-upstream-add](./specs/070-registry-easy-upstream-add/) | `in-flight` | 3/24 (12%) | +| [071-official-registry-protocol](./specs/071-official-registry-protocol/) | `shipped` | 12/12 (100%) | +| [073-activity-size-retention](./specs/073-activity-size-retention/) | `drafted` | 0/14 (0%) | +| [074-discovery-intervals](./specs/074-discovery-intervals/) | `drafted` | 0/19 (0%) | +| [075-macos-tcc-connect](./specs/075-macos-tcc-connect/) | `in-flight` | 11/30 (37%) | +| [076-deterministic-tool-scanner](./specs/076-deterministic-tool-scanner/) | `in-flight` | 22/24 (92%) | diff --git a/docs/personal-edition-polish.md b/docs/personal-edition-polish.md new file mode 100644 index 000000000..d30593765 --- /dev/null +++ b/docs/personal-edition-polish.md @@ -0,0 +1,155 @@ +# Personal-Edition Polish Initiative + +**Started**: 2026-06-30 · **Owner**: Algis · **Status**: active (vertical 1 in progress) + +> Durable brief for the personal-edition polish push. This is the master doc that +> survives a context clean — it captures **intent, scope, decisions, and the +> existing foundation** for each of the 5 verticals so any future session can +> resume a vertical cold. Live status/DAG is in [`../roadmap.yaml`](../roadmap.yaml) +> (rendered to [`../ROADMAP.md`](../ROADMAP.md)). Each vertical gets its own +> grounded `/speckit` brainstorm + spec when we reach it. + +## North star + +Make the **personal edition** so simple and reliable that developers tell their +teammates about it. Three themes cut across everything: **simpler, more reliable, +more transparent.** Paid-tier and server-edition work are **on hold** (minimal +priority) until this push lands. + +## Sequencing + +`ux-audit` (5) is a cross-cutting discovery pass that feeds 2/3/4. We chose to +start with **Scanner + Quarantine (1)** — highest user pain, and Spec 076's +detect engine gave us a foundation. Rough order: **1 → (5 discovery) → 3 / 2 / 4**. +Priorities in `roadmap.yaml`: ux-audit P0, action-log P0, scanner/analytics/ +registries P1. + +--- + +## Vertical 1 — Scanner + Quarantine simplification ✅ IN PROGRESS + +**Goal**: One simple, reliable, deterministic scanner that works offline with +zero Docker; demote the third-party-scanner mess to opt-in; single unified report. + +**Status**: Spec written — [`specs/077-scanner-simplification/spec.md`](../specs/077-scanner-simplification/spec.md), +branch `077-scanner-simplification`. Next: `/speckit.plan`. + +**Decisions (locked)**: See the spec + memory `project-personal-edition-polish-initiative`. +Summary: baseline = Spec 076 detect engine (always-on, offline); delete duplicate +legacy `tpaRules` + legacy embedded-secret path; new **hard-tier `phrase_injection`** +check preserves blocking posture for curated high-confidence phrases (rest stay +soft/review-only); Docker scanners + source extraction → opt-in `security.deep_scan` +(off by default, never blocks/degrades baseline); single merged report via existing +`ScanFinding`/`ScanSummary`/`CalculateRiskScore` with cross-scanner consensus; +baseline-only verdict; collapse MCP-2207 notification storm; remove orphaned +`auto_scan_quarantined`. **Out of scope**: removing Docker plugins, touching the +quarantine state machine, registry redesign. + +**Foundation**: `internal/security/detect/` (Spec 076), `internal/security/scanner/` +(`inprocess.go`, `engine.go`, `docker.go`), `internal/runtime/tool_quarantine.go` +(unchanged). Docs: `docs/features/tool-scanner.md`, `docs/features/security-scanner-plugins.md`. + +--- + +## Vertical 2 — Action Log / Transparency (backlog, P0) + +**Goal**: Make the activity/action log genuinely usable — the most important +signals (security, connection health, recent tool calls, errors) **at a glance**, +not buried. Transparency is a core selling point. + +**Scope (intent)**: An at-a-glance action-log view surfacing top signals + health; +tie in retention/size so the view stays fast and bounded. **Out (for now)**: SIEM +export (parked epic `siem`), deep forensic tooling. + +**Existing foundation** (substantial): +- `specs/019-activity-webui` — **shipped 73/73**. The activity Web UI already exists. +- `specs/024-expand-activity-log` — **~95% (63/66)**. Activity-log backend/expansion. +- `specs/073-activity-size-retention` — drafted (0/14). Retention/size work not started. +- Code: `internal/httpapi/activity.go` (JSONL), activity CLI (`mcpproxy activity …`), + SSE `/events`. Every response carries `X-Request-Id` for correlation. + +**Open questions to brainstorm when we start**: What are the "top signals" worth +promoting (security findings, disconnects, denied calls, sensitive-data hits)? +Is this a new default panel or a redesign of the existing activity view? How does +it relate to the analytics dashboard (vertical 3) — same landing surface? + +--- + +## Vertical 3 — Analytics Dashboard as default page (backlog, P1) + +**Goal**: Make a **graph-first dashboard the default landing page**; show **which +server / which tool drains tokens** (and calls/latency/errors), so users see value +and cost at a glance. + +**Scope (intent)**: Per-server and per-tool token-drain graphs; promote the +dashboard to the default route. **Out**: full BI/exports, cross-instance +aggregation. + +**Existing foundation** (partial — good starting point): +- `specs/069-observability-usage-graphs` — **in-flight ~62% (16/26)**. Usage-graph + work already underway; the token/usage metrics likely exist. +- `specs/039-connect-and-dashboard` — Approved, no tasks yet. The dashboard/connect + surface concept. +- Metrics context: `mcpproxy_tool_calls_total{server,tool,status}` (cardinality-safe; + user_id/profile are span attrs, not labels — see memory `mcp3207…`). OTLP spans + carry richer per-call attributes. + +**Open questions**: Where do per-tool **token** counts come from today (are tokens +measured per call, or estimated)? Is 069 close enough to extend, or does the +default-landing change belong to 039? What's the default time window / granularity? + +--- + +## Vertical 4 — Registries: easier search + add-server (backlog, P1) + +**Goal**: Lower the friction of **finding a server in a registry and adding it** — +better search, one-click add. + +**Scope (intent)**: Improved registry search UX; frictionless add-server flow, +leaning on the official registry protocol. **Out**: marketplace metadata/telemetry +(parked epic `marketplace`), custom private-registry hardening. + +**Existing foundation**: +- `specs/071-official-registry-protocol` — **Implemented 12/12**. Official registry + protocol integration is done — build on it. +- `specs/070-registry-easy-upstream-add` — **early (3/24)**. The easy-add work is + mostly unstarted — this is where most of the vertical lives. +- Code: `Repositories.vue`, `add_from_registry.go`, `search_servers`/`list_registries` + MCP tools. ~60% of a marketplace already ships (browse/search/one-click add). + +**Open questions**: What's the current search's weakness (ranking? filters? +discoverability?)? Is "add server" friction in the UI flow, the quarantine gate, or +config plumbing? Web UI only, or tray deep-links too? + +--- + +## Vertical 5 — UX audit (Web UI + macOS app) (backlog, P0, cross-cutting) + +**Goal**: A grounded, end-to-end UX pass across the **Web UI** and the **macOS tray +app** — the umbrella/discovery step that feeds concrete findings into verticals 2–4. + +**Scope (intent)**: Heuristic + Playwright UX sweep of the Web UI; a macOS tray UX +sweep (settings parity, core flows). Produce a prioritized findings list, not a +redesign. **Out**: net-new features (those become their own verticals). + +**Existing foundation / tooling**: +- `specs/064-glass-cockpit` — **Planned (spec + plan complete)**, no tasks. This is + the likely home/umbrella for the UX vision. +- `specs/037-macos-swift-tray` — Draft. macOS tray. +- Tooling ready: Playwright Web-UI verification (`docs/development/web-ui-verification.md`), + `mcpproxy-ui-test` MCP (macOS tray a11y), `claude-in-chrome` + `computer-use` MCPs, + the `mcpproxy-qa` skill. + +**Open questions**: Run the audit *first* (before 2–4) or interleave? What's the +severity bar / output format (the repo already publishes HTML QA reports under +`docs/qa/`)? Which flows are highest-traffic and worth auditing first? + +--- + +## How to resume after a context clean + +1. Read this doc + [`../roadmap.yaml`](../roadmap.yaml) (or `ROADMAP.md`). +2. Memory `project-personal-edition-polish-initiative` auto-loads the summary. +3. For the active vertical, read its `specs//` spec. +4. Before deep-designing a new vertical, dispatch a code explorer to ground it + against the "Existing foundation" pointers above (as we did for the scanner). diff --git a/roadmap.yaml b/roadmap.yaml new file mode 100644 index 000000000..a5550ba96 --- /dev/null +++ b/roadmap.yaml @@ -0,0 +1,358 @@ +# roadmap.yaml — git-native roadmap for mcpproxy-go +# +# This file is the SOURCE OF TRUTH for the cross-spec roadmap: epics, their +# child tasks, the dependency DAG between them, and execution state that a +# tasks.md checkbox cannot express (status beyond done/not-done, assignee, +# priority, blocked-by edges, external tracker ids, PR links). +# +# `tasks.md` answers "how much of spec NNN is checked off?". This file answers +# "what are we building next, what blocks what, and who owns it?". +# +# Regenerate the human-readable view after editing: +# python3 scripts/gen-roadmap.py # writes ROADMAP.md +# # or: scripts/gen-roadmap # same thing (wrapper) +# +# ── Schema ────────────────────────────────────────────────────────────────── +# version: schema version (int). +# epics: list of epic objects. Each epic: +# id: REQUIRED. Stable slug, unique across epics AND tasks. Used as +# the DAG node id and as a depends_on target. +# title: REQUIRED. Human label. +# status: REQUIRED. one of: todo | in_progress | in_review | blocked | done +# assignee: optional. Owner (agent/role/person). +# priority: optional. P0 (highest) .. P3. +# spec: optional. Path to a specs/ folder (drives progress badge). +# pr: optional. PR ref, e.g. "#761" or a list of refs. +# mcp: optional. External tracker id mirroring MCP-xxxx vocabulary. +# depends_on: optional. List of epic/task ids that must land first (DAG edge). +# parked: optional bool. true = intentionally on hold (still status: todo). +# note: optional. One-line context. +# tasks: optional. List of child task objects. Each task has the same +# fields as an epic except `tasks`. A task's depends_on may point +# at sibling tasks or at other epics. +# +# Conventions: +# - depends_on edges flow PREREQUISITE -> DEPENDENT (drawn A --> B = "A unblocks B"). +# - Keep ids slug-cased and stable; renaming an id breaks inbound depends_on. +# - `done` epics keep their PR refs as provenance. +# ───────────────────────────────────────────────────────────────────────────── + +version: 1 + +epics: + # ── DONE ──────────────────────────────────────────────────────────────── + - id: profiles-v2 + title: Profiles v2 (per-profile tool views) + status: done + assignee: BackendEngineer + priority: P1 + mcp: MCP-33 + # No epic-level spec link: Profiles v2 shipped via the PRs below, not via a + # speckit tasks.md. (specs/057-in-proxy-profiles is a SEPARATE, still-draft + # "permanent URLs" spec at 0/25 — linking it here showed a false "drafted" + # badge on a done epic. Provenance lives in the per-task PR refs.) + depends_on: [] + note: "Stateful profiles: per-profile Bleve indexes, set_profile, token profile_pin, tray + Web UI switchers. Shipped via #756/#761/#766/#767." + tasks: + - id: profiles-v2-indexes + title: Per-profile Bleve indexes (T1) + status: done + assignee: BackendEngineer + mcp: MCP-3240 + pr: "#756" + depends_on: [] + - id: profiles-v2-set-profile + title: set_profile tool + session resolver + REST (T2) + status: done + assignee: BackendEngineer + mcp: MCP-3241 + pr: "#761" + depends_on: [profiles-v2-indexes] + - id: profiles-v2-profile-pin + title: Per-agent-token profile_pin (T3) + status: done + assignee: BackendEngineer + mcp: MCP-3242 + pr: "#766" + spec: specs/028-agent-tokens + depends_on: [profiles-v2-set-profile] + - id: profiles-v2-tray-switcher + title: Tray profile switcher Go + Swift (T5) + status: done + assignee: BackendEngineer + mcp: MCP-3244 + pr: "#767" + depends_on: [profiles-v2-set-profile] + + - id: sandbox-isolation + title: Non-Docker sandbox isolation (Landlock) + status: done + assignee: BackendEngineer + priority: P1 + mcp: MCP-34 + spec: specs/054-mcp-security-gateway + depends_on: [] + note: "Landlock LSM + setrlimit native sandbox for stdio upstreams; no userns (Ubuntu 24.04 safe). Code in internal/sandbox/." + tasks: + - id: sandbox-spike + title: Landlock sandbox spike (MCP-34.1) + status: done + mcp: MCP-3232 + pr: "#754" + depends_on: [] + - id: sandbox-mode-config + title: isolation.mode enum + resolver (MCP-34.2) + status: done + mcp: MCP-3233 + pr: "#759" + depends_on: [sandbox-spike] + - id: sandbox-launcher + title: Native sandbox launcher Landlock+rlimits (MCP-34.3) + status: done + mcp: MCP-3234 + pr: "#768" + depends_on: [sandbox-mode-config] + - id: sandbox-scanner-parity + title: Scanner-flow parity under sandbox (MCP-34.4) + status: done + mcp: MCP-3235 + pr: "#781" + depends_on: [sandbox-launcher, scanner-v2] + - id: sandbox-snap-docker-it + title: snap-docker integration tests + CI (MCP-34.5) + status: done + mcp: MCP-3236 + pr: "#782" + depends_on: [sandbox-scanner-parity] + + - id: ts-code-exec-ga + title: TypeScript code-execution GA + cookbook + status: done + assignee: BackendEngineer + priority: P2 + mcp: MCP-38 + spec: specs/033-typescript-code-execution + depends_on: [] + note: "TS runtime graduated from preview (shipped v0.45.0); MCP-38 = docs/spec + cookbook only." + tasks: + - id: ts-code-exec-cookbook + title: Cookbook (10 TS recipes) + GA docs + status: done + mcp: MCP-38 + pr: "#753" + depends_on: [] + + - id: scanner-v2 + title: Spec 076 deterministic offline tool-scanner + status: done + assignee: BackendEngineer + priority: P1 + mcp: MCP-3574 + spec: specs/076-deterministic-tool-scanner + depends_on: [] + note: "Deterministic offline signal pipeline replaces ~10%-recall scanner; scan-eval --gate (recall>=0.90 / FP<=5%) in CI." + tasks: + - id: scanner-v2-foundation + title: detect-engine foundation (T1) + status: done + mcp: MCP-3575 + pr: "#769" + depends_on: [] + - id: scanner-v2-hard-checks + title: 3 hard checks + scanner wiring (US1 MVP) + status: done + mcp: MCP-3576 + pr: "#770" + depends_on: [scanner-v2-foundation] + - id: scanner-v2-soft-checks + title: 3 soft checks + patterns confidence (US2) + status: done + mcp: MCP-3577 + pr: "#775" + depends_on: [scanner-v2-foundation] + - id: scanner-v2-consensus + title: Consensus risk-score + report transparency (US4) + status: done + mcp: MCP-3578 + pr: "#776" + depends_on: [scanner-v2-hard-checks, scanner-v2-soft-checks] + - id: scanner-v2-eval-gate + title: Eval corpus + CI recall/FP gate (US3) + status: done + mcp: MCP-3579 + pr: "#777" + depends_on: [scanner-v2-hard-checks] + - id: scanner-v2-docs + title: Tool-scanner detect-engine docs (T22) + status: done + mcp: MCP-3683 + pr: "#780" + depends_on: [scanner-v2-eval-gate] + + # ── IN REVIEW ─────────────────────────────────────────────────────────── + - id: windows-tray + title: Windows native tray app + status: in_review + assignee: BackendEngineer + priority: P2 + mcp: MCP-43 + spec: specs/002-windows-installer + depends_on: [] + note: "Option C: WebView2 window reusing shipped Web UI. Most exit criteria already ship; gaps = native window, toasts, profile submenu, Win11 smoke." + tasks: + - id: windows-tray-window + title: WebView2 native window + profile submenu + status: in_review + mcp: MCP-43 + depends_on: [] + + # ── BACKLOG: personal-edition polish (NEW priorities) ───────────────────── + - id: ux-audit + title: Web UI + macOS app UX audit + status: todo + assignee: unassigned + priority: P0 + spec: specs/064-glass-cockpit + depends_on: [] + note: "End-to-end UX pass across Web UI and the macOS tray app; the umbrella for the polish push." + tasks: + - id: ux-audit-webui-sweep + title: Web UI heuristic + Playwright UX sweep + status: todo + depends_on: [] + - id: ux-audit-macos-sweep + title: macOS tray app UX sweep (settings parity, flows) + status: todo + spec: specs/037-macos-swift-tray + depends_on: [] + + - id: action-log-transparency + title: Action log / transparency — info at a glance + status: todo + assignee: unassigned + priority: P0 + spec: specs/024-expand-activity-log + depends_on: [ux-audit] + note: "Surface the most important activity/security/connection signals at a glance; reduce digging. Builds on activity-log backend + retention." + tasks: + - id: action-log-glance-view + title: At-a-glance action log view (top signals, health) + status: todo + spec: specs/019-activity-webui + depends_on: [] + - id: action-log-retention-tie-in + title: Tie activity retention/size into the glance view + status: todo + spec: specs/073-activity-size-retention + depends_on: [action-log-glance-view] + + - id: analytics-dashboard + title: Analytics dashboard as default page + status: todo + assignee: unassigned + priority: P1 + spec: specs/069-observability-usage-graphs + depends_on: [ux-audit] + note: "Per-server / per-tool token-drain graphs; make the dashboard the default landing page." + tasks: + - id: analytics-token-drain-graphs + title: Per-server / per-tool token-drain graphs + status: todo + spec: specs/069-observability-usage-graphs + depends_on: [] + - id: analytics-default-landing + title: Make dashboard the default landing page + status: todo + spec: specs/039-connect-and-dashboard + depends_on: [analytics-token-drain-graphs] + + - id: registries-search-add + title: Registries — easier search + add-server + status: todo + assignee: unassigned + priority: P1 + spec: specs/070-registry-easy-upstream-add + depends_on: [ux-audit] + note: "Lower the friction of finding a server in a registry and adding it; lean on the official registry protocol work." + tasks: + - id: registries-search-ux + title: Improved registry search UX + status: todo + spec: specs/070-registry-easy-upstream-add + depends_on: [] + - id: registries-official-protocol + title: Official registry protocol integration + status: todo + spec: specs/071-official-registry-protocol + depends_on: [] + + - id: scanner-simplification + title: Scanner simplification (deterministic default, opt-in deep scan) + status: in_progress + assignee: unassigned + priority: P1 + spec: specs/077-scanner-simplification + depends_on: [scanner-v2] + note: "Make the Spec 076 detect engine the always-on offline default; demote Docker scanners + source extraction to opt-in deep scan that never blocks/degrades the baseline; single unified report. Spec drafted (branch 077-scanner-simplification); plan next. First of the 5 personal-edition polish verticals." + tasks: + - id: scanner-simpl-baseline + title: "US1: deterministic offline baseline default + curated hard phrase_injection check (delete duplicate legacy rules)" + status: todo + depends_on: [] + - id: scanner-simpl-unified-report + title: "US2: single merged report + cross-scanner consensus confidence" + status: todo + depends_on: [scanner-simpl-baseline] + - id: scanner-simpl-deep-optin + title: "US3: opt-in deep scan (off by default), never blocks/degrades baseline; config migration" + status: todo + depends_on: [scanner-simpl-baseline, scanner-simpl-unified-report] + - id: scanner-simpl-notifications + title: "US4: collapse scan-notification storm into one debounced settled event (MCP-2207)" + status: todo + depends_on: [scanner-simpl-unified-report] + + # ── PARKED epics (intentionally on hold) ────────────────────────────────── + - id: marketplace + title: Server marketplace + status: todo + parked: true + priority: P3 + mcp: MCP-37 + spec: specs/070-registry-easy-upstream-add + depends_on: [] + note: "PARKED. ~60% already ships (browse/search/one-click add). Remaining = tray entries, metadata, telemetry." + + - id: siem + title: Audit SIEM integration + status: todo + parked: true + priority: P3 + mcp: MCP-39 + depends_on: [] + note: "PARKED. Splunk HEC / Elastic _bulk / syslog shippers reusing JSONL export pipeline." + + - id: paid-tier + title: Paid-tier MVP (billing / seats / license) + status: todo + parked: true + priority: P3 + mcp: MCP-40 + depends_on: [] + note: "PARKED. Server-edition revenue motion: Ed25519 license tokens, seats, Stripe checkout. Behind //go:build server." + + - id: sdk-v1-migration + title: SDK v1 migration + status: todo + parked: true + priority: P3 + depends_on: [] + note: "PARKED. Migrate to the v1 MCP Go SDK surface." + + - id: sso + title: SSO (server edition) + status: todo + parked: true + priority: P3 + depends_on: [] + note: "PARKED. Single sign-on for the multi-user server edition." diff --git a/scripts/gen-roadmap b/scripts/gen-roadmap new file mode 100755 index 000000000..615c0bbd1 --- /dev/null +++ b/scripts/gen-roadmap @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Convenience wrapper around scripts/gen-roadmap.py. +# Generates ROADMAP.md at the repo root from roadmap.yaml. +set -euo pipefail +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec python3 "${DIR}/gen-roadmap.py" "$@" diff --git a/scripts/gen-roadmap.py b/scripts/gen-roadmap.py new file mode 100755 index 000000000..9591307a7 --- /dev/null +++ b/scripts/gen-roadmap.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python3 +"""Generate ROADMAP.md from roadmap.yaml (+ live specs//tasks.md badges). + +This is the renderer for the git-native roadmap prototype. It reads the +hand-maintained DAG in roadmap.yaml, recomputes per-spec progress by counting +checkboxes in each specs//tasks.md, and writes a single ROADMAP.md +containing: + + 1. A generated-file banner + schema/regenerate instructions. + 2. A Mermaid `graph TD` of the epic/task DAG, styled by status. + 3. A status table (epic, status, assignee, progress, spec/PR links). + 4. An aggregate per-spec progress table (recomputed from tasks.md). + +Design choice: we write the aggregate spec table into ROADMAP.md rather than +overwriting the hand-maintained specs/README.md, so the existing curated index +(with its prose, runbooks and design-doc links) is never clobbered. ROADMAP.md +is fully generated and safe to overwrite on every run. + +Usage: + python3 scripts/gen-roadmap.py [--check] + + --check Exit non-zero if ROADMAP.md is out of date (does not write). + Useful as a CI canary. + +Pure stdlib + PyYAML (already used by scripts/check-settings-parity.py). +Idempotent: running twice with no source change produces identical output. +""" +from __future__ import annotations + +import argparse +import os +import re +import sys + +try: + import yaml +except ImportError: # pragma: no cover + sys.stderr.write("error: PyYAML required (pip install pyyaml)\n") + sys.exit(2) + +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +ROADMAP_YAML = os.path.join(REPO_ROOT, "roadmap.yaml") +ROADMAP_MD = os.path.join(REPO_ROOT, "ROADMAP.md") +SPECS_DIR = os.path.join(REPO_ROOT, "specs") + +# A checkbox line: "- [ ] ...", "- [x] ...", "- [X] ..." (matches specs/README.md). +CHECKBOX_RE = re.compile(r"^- \[([ xX])\]") + +STATUS_ORDER = ["in_progress", "in_review", "blocked", "todo", "done"] +STATUS_LABEL = { + "todo": "Todo", + "in_progress": "In progress", + "in_review": "In review", + "blocked": "Blocked", + "done": "Done", +} +# Mermaid classDef colours keyed by status. +STATUS_CLASSDEF = { + "done": "fill:#1f7a1f,stroke:#0d3d0d,color:#ffffff", + "in_progress": "fill:#1f6feb,stroke:#0b3d91,color:#ffffff", + "in_review": "fill:#9a6700,stroke:#5c3d00,color:#ffffff", + "blocked": "fill:#a40e26,stroke:#5c0712,color:#ffffff", + "todo": "fill:#6e7781,stroke:#3d4248,color:#ffffff", + "parked": "fill:#30363d,stroke:#161b22,color:#9da7b3,stroke-dasharray:4 3", +} + + +# ── spec checkbox accounting ──────────────────────────────────────────────── +def count_checkboxes(tasks_md: str) -> tuple[int, int]: + """Return (checked, total) from a tasks.md path. (0, 0) if absent.""" + if not os.path.isfile(tasks_md): + return (0, 0) + checked = total = 0 + with open(tasks_md, encoding="utf-8") as fh: + for line in fh: + m = CHECKBOX_RE.match(line) + if not m: + continue + total += 1 + if m.group(1) in ("x", "X"): + checked += 1 + return (checked, total) + + +def spec_badge(checked: int, total: int) -> tuple[str, str]: + """Map counts to (status_word, progress_str) using the specs/README legend.""" + if total == 0: + return ("—", "—") + pct = round(100 * checked / total) + if pct >= 95: + word = "shipped" + elif pct >= 1: + word = "in-flight" + else: + word = "drafted" + return (word, f"{checked}/{total} ({pct}%)") + + +def spec_progress(spec_path: str | None) -> tuple[str, str]: + """Resolve a roadmap spec: link to a (status, progress) badge pair.""" + if not spec_path: + return ("", "") + tasks_md = os.path.join(REPO_ROOT, spec_path, "tasks.md") + return spec_badge(*count_checkboxes(tasks_md)) + + +# ── node id sanitising for Mermaid ────────────────────────────────────────── +def node_id(raw: str) -> str: + """Mermaid node ids must be alnum/underscore.""" + return re.sub(r"[^0-9A-Za-z]", "_", raw) + + +def status_of(item: dict) -> str: + """Effective status; parked todos render as a distinct 'parked' class.""" + st = item.get("status", "todo") + if st == "todo" and item.get("parked"): + return "parked" + return st + + +# ── rendering ─────────────────────────────────────────────────────────────── +def fmt_pr(pr) -> str: + if not pr: + return "" + if isinstance(pr, list): + return " ".join(str(p) for p in pr) + return str(pr) + + +def mermaid_label(item: dict) -> str: + """Node shape+label: `["title
MCP-xxx"]`. Quotes let parens/slashes/em + dashes survive; brackets give the node its (default rectangle) shape.""" + title = item["title"].replace('"', "'") + mcp = item.get("mcp") + inner = f"{title}
{mcp}" if mcp else title + return f'["{inner}"]' + + +def render_mermaid(epics: list[dict]) -> str: + lines = ["```mermaid", "graph TD"] + classed: dict[str, list[str]] = {k: [] for k in STATUS_CLASSDEF} + + # Declare nodes (epics as subgraphs containing their tasks). + for epic in epics: + eid = node_id(epic["id"]) + classed[status_of(epic)].append(eid) + tasks = epic.get("tasks") or [] + if tasks: + lines.append(f' subgraph sg_{eid}["{epic["title"].replace(chr(34), chr(39))}"]') + lines.append(f" {eid}{mermaid_label(epic)}") + for t in tasks: + tid = node_id(t["id"]) + lines.append(f" {tid}{mermaid_label(t)}") + classed[status_of(t)].append(tid) + lines.append(" end") + else: + lines.append(f" {eid}{mermaid_label(epic)}") + + # Edges (prerequisite --> dependent). + lines.append("") + for epic in epics: + eid = node_id(epic["id"]) + for dep in epic.get("depends_on") or []: + lines.append(f" {node_id(dep)} --> {eid}") + for t in epic.get("tasks") or []: + tid = node_id(t["id"]) + for dep in t.get("depends_on") or []: + lines.append(f" {node_id(dep)} --> {tid}") + + # Class definitions + assignments. + lines.append("") + for status, style in STATUS_CLASSDEF.items(): + lines.append(f" classDef {status} {style};") + for status, ids in classed.items(): + if ids: + lines.append(f" class {','.join(ids)} {status};") + + lines.append("```") + return "\n".join(lines) + + +def render_status_table(epics: list[dict]) -> str: + rows = ["| Epic | Status | Assignee | Priority | Progress | Spec | PR |", + "| --- | --- | --- | --- | --- | --- | --- |"] + order = {s: i for i, s in enumerate(STATUS_ORDER)} + + def sort_key(e): + return (order.get(e.get("status", "todo"), 99), + 1 if e.get("parked") else 0, + e.get("priority", "P9")) + + for epic in sorted(epics, key=sort_key): + st = STATUS_LABEL.get(epic.get("status", "todo"), epic.get("status", "")) + if epic.get("parked"): + st += " (parked)" + _, progress = spec_progress(epic.get("spec")) + spec = epic.get("spec") + spec_cell = f"[{os.path.basename(spec)}](./{spec}/)" if spec else "" + pr = fmt_pr(epic.get("pr")) + mcp = epic.get("mcp") + epic_cell = epic["title"] + (f" `{mcp}`" if mcp else "") + rows.append( + f"| {epic_cell} | {st} | {epic.get('assignee', '')} | " + f"{epic.get('priority', '')} | {progress or '—'} | {spec_cell} | {pr} |" + ) + return "\n".join(rows) + + +def render_spec_table() -> str: + """Recompute the aggregate per-spec progress table from tasks.md files.""" + rows = ["| # | Status | Progress |", "| --- | --- | --- |"] + for name in sorted(os.listdir(SPECS_DIR)): + spec_dir = os.path.join(SPECS_DIR, name) + if not os.path.isdir(spec_dir): + continue + if not re.match(r"^\d", name): # only numbered spec dirs + continue + word, progress = spec_badge(*count_checkboxes(os.path.join(spec_dir, "tasks.md"))) + badge = f"`{word}`" if word != "—" else "—" + rows.append(f"| [{name}](./specs/{name}/) | {badge} | {progress} |") + return "\n".join(rows) + + +def render(data: dict) -> str: + epics = data.get("epics", []) + out = [] + out.append("") + out.append("") + out.append("") + out.append("") + out.append("# MCPProxy Roadmap") + out.append("") + out.append("> **Generated — do not edit by hand.** This file is rendered from " + "[`roadmap.yaml`](./roadmap.yaml) by [`scripts/gen-roadmap.py`](./scripts/gen-roadmap.py). " + "Edit `roadmap.yaml` and re-run the generator.") + out.append("") + out.append("The roadmap models cross-spec **epics → tasks** with a dependency DAG, " + "execution `status`, `assignee`, `priority`, and links — the things a " + "per-spec `tasks.md` checkbox list cannot express. Per-spec checkbox " + "progress is recomputed live from each `specs//tasks.md`.") + out.append("") + out.append("## How to regenerate") + out.append("") + out.append("```bash") + out.append("python3 scripts/gen-roadmap.py # writes ROADMAP.md") + out.append("scripts/gen-roadmap # convenience wrapper (same thing)") + out.append("python3 scripts/gen-roadmap.py --check # CI canary: fail if stale") + out.append("```") + out.append("") + out.append("## roadmap.yaml schema (short form)") + out.append("") + out.append("- **epics[]** — each has `id` (stable slug, DAG node), `title`, " + "`status` (todo·in_progress·in_review·blocked·done), `assignee`, " + "`priority` (P0–P3), `depends_on: [ids]` (DAG edges, prerequisite→dependent), " + "optional `parked: true`, and links `spec:` / `pr:` / `mcp:` (external MCP-xxxx).") + out.append("- **epics[].tasks[]** — child tasks with the same fields; their " + "`depends_on` may reference sibling tasks or other epics.") + out.append("- See the header comment in `roadmap.yaml` for the full field reference.") + out.append("") + out.append("## Epic / task DAG") + out.append("") + out.append("Node colour = status (green done · blue in-progress · amber in-review · " + "red blocked · grey todo · dashed grey parked). Edges point " + "prerequisite → dependent.") + out.append("") + out.append(render_mermaid(epics)) + out.append("") + out.append("## Epics") + out.append("") + out.append(render_status_table(epics)) + out.append("") + out.append("## Per-spec progress (recomputed from `specs//tasks.md`)") + out.append("") + out.append("Legend: `shipped` ≥95% checked · `in-flight` 1–94% · `drafted` 0% · " + "`—` no `tasks.md`. This aggregate is regenerated here rather than " + "overwriting the hand-maintained [`specs/README.md`](./specs/README.md), " + "which keeps its curated prose, runbooks and design-doc links.") + out.append("") + out.append(render_spec_table()) + out.append("") + return "\n".join(out) + + +def main() -> int: + ap = argparse.ArgumentParser(description="Generate ROADMAP.md from roadmap.yaml") + ap.add_argument("--check", action="store_true", + help="exit non-zero if ROADMAP.md is stale (do not write)") + args = ap.parse_args() + + with open(ROADMAP_YAML, encoding="utf-8") as fh: + data = yaml.safe_load(fh) + + rendered = render(data) + + if args.check: + current = "" + if os.path.isfile(ROADMAP_MD): + with open(ROADMAP_MD, encoding="utf-8") as fh: + current = fh.read() + if current != rendered: + sys.stderr.write("ROADMAP.md is out of date; run scripts/gen-roadmap.py\n") + return 1 + print("ROADMAP.md is up to date.") + return 0 + + with open(ROADMAP_MD, "w", encoding="utf-8") as fh: + fh.write(rendered) + print(f"wrote {os.path.relpath(ROADMAP_MD, REPO_ROOT)} " + f"({len(data.get('epics', []))} epics)") + return 0 + + +if __name__ == "__main__": + sys.exit(main())