diff --git a/apps/gateway/src/agent/provider.dto.ts b/apps/gateway/src/agent/provider.dto.ts new file mode 100644 index 0000000..cdebc62 --- /dev/null +++ b/apps/gateway/src/agent/provider.dto.ts @@ -0,0 +1,17 @@ +export interface TestConnectionDto { + /** Provider identifier to test (e.g. 'ollama', custom provider id) */ + providerId: string; + /** Optional base URL override for ad-hoc testing */ + baseUrl?: string; +} + +export interface TestConnectionResultDto { + providerId: string; + reachable: boolean; + /** Round-trip latency in milliseconds (present when reachable) */ + latencyMs?: number; + /** Human-readable error when unreachable */ + error?: string; + /** Model ids discovered at the remote endpoint (present when reachable) */ + discoveredModels?: string[]; +} diff --git a/apps/gateway/src/agent/provider.service.ts b/apps/gateway/src/agent/provider.service.ts index 8c73fe6..255655f 100644 --- a/apps/gateway/src/agent/provider.service.ts +++ b/apps/gateway/src/agent/provider.service.ts @@ -2,6 +2,7 @@ import { Injectable, Logger, type OnModuleInit } from '@nestjs/common'; import { ModelRegistry, AuthStorage } from '@mariozechner/pi-coding-agent'; import type { Model, Api } from '@mariozechner/pi-ai'; import type { ModelInfo, ProviderInfo, CustomProviderConfig } from '@mosaic/types'; +import type { TestConnectionResultDto } from './provider.dto.js'; @Injectable() export class ProviderService implements OnModuleInit { @@ -64,6 +65,63 @@ export class ProviderService implements OnModuleInit { return this.registry.getAvailable().map((m) => this.toModelInfo(m)); } + async testConnection(providerId: string, baseUrl?: string): Promise { + // Resolve baseUrl: explicit override > registered provider > ollama env + let resolvedUrl = baseUrl; + + if (!resolvedUrl) { + const allModels = this.registry.getAll(); + const providerModels = allModels.filter((m) => m.provider === providerId); + if (providerModels.length === 0) { + return { providerId, reachable: false, error: `Provider '${providerId}' not found` }; + } + // For Ollama, derive the base URL from environment + if (providerId === 'ollama') { + const ollamaUrl = process.env['OLLAMA_BASE_URL'] ?? process.env['OLLAMA_HOST']; + if (!ollamaUrl) { + return { providerId, reachable: false, error: 'OLLAMA_BASE_URL not configured' }; + } + resolvedUrl = `${ollamaUrl}/v1/models`; + } else { + // For other providers, we can only do a basic check + return { providerId, reachable: true, discoveredModels: providerModels.map((m) => m.id) }; + } + } else { + resolvedUrl = resolvedUrl.replace(/\/?$/, '') + '/models'; + } + + const start = Date.now(); + try { + const res = await fetch(resolvedUrl, { + method: 'GET', + headers: { Accept: 'application/json' }, + signal: AbortSignal.timeout(5000), + }); + + const latencyMs = Date.now() - start; + + if (!res.ok) { + return { providerId, reachable: false, latencyMs, error: `HTTP ${res.status}` }; + } + + let discoveredModels: string[] | undefined; + try { + const json = (await res.json()) as { models?: Array<{ id?: string; name?: string }> }; + if (Array.isArray(json.models)) { + discoveredModels = json.models.map((m) => m.id ?? m.name ?? '').filter(Boolean); + } + } catch { + // ignore parse errors — endpoint was reachable + } + + return { providerId, reachable: true, latencyMs, discoveredModels }; + } catch (err) { + const latencyMs = Date.now() - start; + const message = err instanceof Error ? err.message : String(err); + return { providerId, reachable: false, latencyMs, error: message }; + } + } + registerCustomProvider(config: CustomProviderConfig): void { this.registry.registerProvider(config.id, { baseUrl: config.baseUrl, diff --git a/apps/gateway/src/agent/providers.controller.ts b/apps/gateway/src/agent/providers.controller.ts index 9a78a29..8a07aa4 100644 --- a/apps/gateway/src/agent/providers.controller.ts +++ b/apps/gateway/src/agent/providers.controller.ts @@ -3,6 +3,7 @@ import type { RoutingCriteria } from '@mosaic/types'; import { AuthGuard } from '../auth/auth.guard.js'; import { ProviderService } from './provider.service.js'; import { RoutingService } from './routing.service.js'; +import type { TestConnectionDto, TestConnectionResultDto } from './provider.dto.js'; @Controller('api/providers') @UseGuards(AuthGuard) @@ -22,6 +23,11 @@ export class ProvidersController { return this.providerService.listAvailableModels(); } + @Post('test') + testConnection(@Body() body: TestConnectionDto): Promise { + return this.providerService.testConnection(body.providerId, body.baseUrl); + } + @Post('route') route(@Body() criteria: RoutingCriteria) { return this.routingService.route(criteria); diff --git a/apps/web/src/app/(dashboard)/settings/page.tsx b/apps/web/src/app/(dashboard)/settings/page.tsx index cc6c925..7469f51 100644 --- a/apps/web/src/app/(dashboard)/settings/page.tsx +++ b/apps/web/src/app/(dashboard)/settings/page.tsx @@ -1,42 +1,85 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { api } from '@/lib/api'; import { useSession } from '@/lib/auth-client'; -interface ProviderInfo { - name: string; - enabled: boolean; - modelCount: number; -} - interface ModelInfo { id: string; - name: string; provider: string; - contextWindow: number; + name: string; reasoning: boolean; - cost: { input: number; output: number }; + contextWindow: number; + maxTokens: number; + inputTypes: ('text' | 'image')[]; + cost: { input: number; output: number; cacheRead: number; cacheWrite: number }; +} + +interface ProviderInfo { + id: string; + name: string; + available: boolean; + models: ModelInfo[]; +} + +interface TestConnectionResult { + providerId: string; + reachable: boolean; + latencyMs?: number; + error?: string; + discoveredModels?: string[]; +} + +type TestState = 'idle' | 'testing' | 'success' | 'error'; + +interface ProviderTestStatus { + state: TestState; + result?: TestConnectionResult; } export default function SettingsPage(): React.ReactElement { const { data: session } = useSession(); const [providers, setProviders] = useState([]); - const [models, setModels] = useState([]); const [loading, setLoading] = useState(true); + const [testStatuses, setTestStatuses] = useState>({}); useEffect(() => { - Promise.all([ - api('/api/providers').catch(() => []), - api('/api/providers/models').catch(() => []), - ]) - .then(([p, m]) => { - setProviders(p); - setModels(m); - }) + api('/api/providers') + .catch(() => [] as ProviderInfo[]) + .then((p) => setProviders(p)) .finally(() => setLoading(false)); }, []); + const testConnection = useCallback(async (providerId: string): Promise => { + setTestStatuses((prev) => ({ + ...prev, + [providerId]: { state: 'testing' }, + })); + try { + const result = await api('/api/providers/test', { + method: 'POST', + body: { providerId }, + }); + setTestStatuses((prev) => ({ + ...prev, + [providerId]: { state: result.reachable ? 'success' : 'error', result }, + })); + } catch { + setTestStatuses((prev) => ({ + ...prev, + [providerId]: { + state: 'error', + result: { providerId, reachable: false, error: 'Request failed' }, + }, + })); + } + }, []); + + // Derive default model: first available model across all providers + const defaultModel: ModelInfo | undefined = providers + .flatMap((p) => p.models) + .find((m) => providers.find((p) => p.id === m.provider)?.available); + return (

Settings

@@ -64,82 +107,250 @@ export default function SettingsPage(): React.ReactElement {

Loading providers...

) : providers.length === 0 ? (
-

No providers configured

+

+ No providers configured. Set{' '} + + OLLAMA_BASE_URL + {' '} + or{' '} + + MOSAIC_CUSTOM_PROVIDERS + {' '} + to add providers. +

) : ( -
- {providers.map((p) => ( -
-
-

{p.name}

-

{p.modelCount} models available

-
- - {p.enabled ? 'Active' : 'Disabled'} - -
+
+ {providers.map((provider) => ( + void testConnection(provider.id)} + /> ))}
)} - - {/* Models */} -
-

Available Models

- {loading ? ( -

Loading models...

- ) : models.length === 0 ? ( -
-

No models available

-
- ) : ( -
- - - - - - - - - - - {models.map((m) => ( - - - - - - - ))} - -
ModelProviderContextCost (in/out)
- {m.name} - {m.reasoning && ( - reasoning - )} - {m.provider} - {(m.contextWindow / 1000).toFixed(0)}k - - ${m.cost.input} / ${m.cost.output} -
-
- )} -
); } +interface ProviderCardProps { + provider: ProviderInfo; + defaultModel: ModelInfo | undefined; + testStatus: ProviderTestStatus; + onTest: () => void; +} + +function ProviderCard({ + provider, + defaultModel, + testStatus, + onTest, +}: ProviderCardProps): React.ReactElement { + const [expanded, setExpanded] = useState(false); + + return ( +
+ {/* Header row */} +
+
+ +
+
+ {provider.name} + +
+

+ {provider.models.length} model{provider.models.length !== 1 ? 's' : ''} +

+
+
+ +
+ + +
+
+ + {/* Test result banner */} + {testStatus.state !== 'idle' && testStatus.state !== 'testing' && testStatus.result && ( + + )} + + {/* Model list */} + {expanded && ( +
+ + + + + + + + + + + + {provider.models.map((model) => ( + + ))} + +
ModelCapabilitiesContextCost (in/out)Default
+
+ )} +
+ ); +} + +interface ModelRowProps { + model: ModelInfo; + isDefault: boolean; +} + +function ModelRow({ model, isDefault }: ModelRowProps): React.ReactElement { + return ( + + + {model.name} + + +
+ + {model.reasoning && } + {model.inputTypes.includes('image') && } +
+ + + {formatContext(model.contextWindow)} + + + {model.cost.input === 0 && model.cost.output === 0 + ? 'free' + : `$${model.cost.input} / $${model.cost.output}`} + + + {isDefault && ( + + default + + )} + + + ); +} + +function ProviderAvatar({ id }: { id: string }): React.ReactElement { + const letter = id.charAt(0).toUpperCase(); + return ( +
+ {letter} +
+ ); +} + +function ProviderStatusBadge({ available }: { available: boolean }): React.ReactElement { + return ( + + {available ? 'Active' : 'Inactive'} + + ); +} + +interface TestConnectionButtonProps { + status: ProviderTestStatus; + onTest: () => void; +} + +function TestConnectionButton({ status, onTest }: TestConnectionButtonProps): React.ReactElement { + const isTesting = status.state === 'testing'; + return ( + + ); +} + +function TestResultBanner({ result }: { result: TestConnectionResult }): React.ReactElement { + return ( +
+ {result.reachable ? ( + <> + Connected + {result.latencyMs !== undefined && ( + ({result.latencyMs}ms) + )} + {result.discoveredModels && result.discoveredModels.length > 0 && ( + + — {result.discoveredModels.length} model + {result.discoveredModels.length !== 1 ? 's' : ''} discovered + + )} + + ) : ( + <>Connection failed{result.error ? `: ${result.error}` : ''} + )} +
+ ); +} + +function CapabilityBadge({ + label, + color = 'default', +}: { + label: string; + color?: 'default' | 'purple' | 'blue'; +}): React.ReactElement { + const colorClass = + color === 'purple' + ? 'bg-purple-500/20 text-purple-400' + : color === 'blue' + ? 'bg-blue-500/20 text-blue-400' + : 'bg-surface-elevated text-text-muted'; + return {label}; +} + function Field({ label, value }: { label: string; value: string }): React.ReactElement { return (
@@ -148,3 +359,9 @@ function Field({ label, value }: { label: string; value: string }): React.ReactE
); } + +function formatContext(tokens: number): string { + if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`; + if (tokens >= 1_000) return `${Math.round(tokens / 1_000)}k`; + return String(tokens); +} diff --git a/docs/TASKS.md b/docs/TASKS.md index 3fce45d..d236970 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -2,80 +2,80 @@ > Single-writer: orchestrator only. Workers read but never modify. -| id | status | milestone | description | pr | notes | -| ------ | ----------- | --------- | ------------------------------------------------------------------- | ---- | --------------------------- | -| P0-001 | done | Phase 0 | Scaffold monorepo | #60 | #1 | -| P0-002 | done | Phase 0 | @mosaic/types — migrate and extend shared types | #65 | #2 | -| P0-003 | done | Phase 0 | @mosaic/db — Drizzle schema and PG connection | #67 | #3 | -| P0-004 | done | Phase 0 | @mosaic/auth — BetterAuth email/password setup | #68 | #4 | -| P0-005 | done | Phase 0 | Docker Compose — PG 17, Valkey 8, SigNoz | #65 | #5 | -| P0-006 | done | Phase 0 | OTEL foundation — OpenTelemetry SDK setup | #65 | #6 | -| P0-007 | done | Phase 0 | CI pipeline — Woodpecker config | #69 | #7 | -| P0-008 | done | Phase 0 | Project docs — AGENTS.md, CLAUDE.md, README | #69 | #8 | -| P0-009 | done | Phase 0 | Verify Phase 0 — CI green, all packages build | #70 | #9 | -| P1-001 | done | Phase 1 | apps/gateway scaffold — NestJS + Fastify adapter | #61 | #10 | -| P1-002 | done | Phase 1 | Auth middleware — BetterAuth session validation | #71 | #11 | -| P1-003 | done | Phase 1 | @mosaic/brain — migrate from v0, PG backend | #71 | #12 | -| P1-004 | done | Phase 1 | @mosaic/queue — migrate from v0 | #71 | #13 | -| P1-005 | done | Phase 1 | Gateway routes — conversations CRUD + messages | #72 | #14 | -| P1-006 | done | Phase 1 | Gateway routes — tasks, projects, missions CRUD | #72 | #15 | -| P1-007 | done | Phase 1 | WebSocket server — chat streaming | #61 | #16 | -| P1-008 | done | Phase 1 | Basic agent dispatch — single provider | #61 | #17 | -| P1-009 | done | Phase 1 | Verify Phase 1 — gateway functional, API tested | #73 | #18 | -| P2-001 | done | Phase 2 | @mosaic/agent — Pi SDK integration + agent pool | #61 | #19 | -| P2-002 | done | Phase 2 | Multi-provider support — Anthropic + Ollama | #74 | #20 | -| P2-003 | done | Phase 2 | Agent routing engine — cost/capability matrix | #75 | #21 | -| P2-004 | done | Phase 2 | Tool registration — brain, queue, memory tools | #76 | #22 | -| P2-005 | done | Phase 2 | @mosaic/coord — migrate from v0, gateway integration | #77 | #23 | -| P2-006 | done | Phase 2 | Agent session management — tmux + monitoring | #78 | #24 | -| P2-007 | done | Phase 2 | Verify Phase 2 — multi-provider routing works | #79 | #25 | -| P3-001 | done | Phase 3 | apps/web scaffold — Next.js 16 + BetterAuth + Tailwind | #82 | #26 | -| P3-002 | done | Phase 3 | Auth pages — login, registration, SSO redirect | #83 | #27 | -| P3-003 | done | Phase 3 | Chat UI — conversations, messages, streaming | #84 | #28 | -| P3-004 | done | Phase 3 | Task management — list view + kanban board | #86 | #29 | -| P3-005 | done | Phase 3 | Project & mission views — dashboard + PRD viewer | #87 | #30 | -| P3-006 | done | Phase 3 | Settings — provider config, profile, integrations | #88 | #31 | -| P3-007 | done | Phase 3 | Admin panel — user management, RBAC | #89 | #32 | -| P3-008 | done | Phase 3 | Verify Phase 3 — web dashboard functional E2E | — | #33 | -| P4-001 | done | Phase 4 | @mosaic/memory — preference + insight stores | — | #34 | -| P4-002 | done | Phase 4 | Semantic search — pgvector embeddings + search API | — | #35 | -| P4-003 | done | Phase 4 | @mosaic/log — log ingest, parsing, tiered storage | — | #36 | -| P4-004 | done | Phase 4 | Summarization pipeline — Haiku-tier LLM + cron | — | #37 | -| P4-005 | done | Phase 4 | Memory integration — inject into agent sessions | — | #38 | -| P4-006 | done | Phase 4 | Skill management — catalog, install, config | — | #39 | -| P4-007 | done | Phase 4 | Verify Phase 4 — memory + log pipeline working | — | #40 | -| P5-001 | done | Phase 5 | Plugin host — gateway plugin loading + channel interface | — | #41 | -| P5-002 | done | Phase 5 | @mosaic/discord-plugin — Discord bot + channel plugin | #61 | #42 | -| P5-003 | done | Phase 5 | @mosaic/telegram-plugin — Telegraf bot + channel plugin | — | #43 | -| P5-004 | done | Phase 5 | SSO — Authentik OIDC adapter end-to-end | — | #44 | -| P5-005 | done | Phase 5 | Verify Phase 5 — Discord + Telegram + SSO working | #99 | #45 | -| P6-001 | done | Phase 6 | @mosaic/cli — unified CLI binary + subcommands | #104 | #46 | -| P6-002 | done | Phase 6 | @mosaic/prdy — migrate PRD wizard from v0 | #101 | #47 | -| P6-003 | done | Phase 6 | @mosaic/quality-rails — migrate scaffolder from v0 | #100 | #48 | -| P6-004 | done | Phase 6 | @mosaic/mosaic — install wizard for v1 | #103 | #49 | -| P6-005 | done | Phase 6 | Pi TUI integration — mosaic tui | #61 | #50 | -| P6-006 | done | Phase 6 | Verify Phase 6 — CLI functional, all subcommands | — | #51 | -| P7-009 | not-started | Phase 7 | Web chat — WebSocket integration, streaming, conversation switching | — | #120 Wave-1 | -| P7-001 | not-started | Phase 7 | MCP endpoint hardening — streamable HTTP transport | — | #52 Wave-1 | -| P7-010 | not-started | Phase 7 | Web conversation management — list, search, rename, delete, archive | — | #121 Wave-2, depends:P7-009 | -| P7-015 | not-started | Phase 7 | Agent tool expansion — file ops, git, shell exec, web fetch | — | #126 Wave-2 | -| P7-011 | not-started | Phase 7 | Web project detail views — missions, tasks, PRDs, dashboards | — | #122 Wave-3 | -| P7-016 | not-started | Phase 7 | MCP client — gateway connects to external MCP servers as tools | — | #127 Wave-3, depends:P7-001 | -| P7-012 | not-started | Phase 7 | Web provider management UI — add, configure, test LLM providers | — | #123 Wave-4 | -| P7-017 | not-started | Phase 7 | Agent skill invocation — load and execute skills from catalog | — | #128 Wave-4 | -| P7-013 | not-started | Phase 7 | Web settings persistence — profile, preferences save to DB | — | #124 Wave-5 | -| P7-018 | not-started | Phase 7 | CLI model/provider switching — --model, --provider, /model in TUI | — | #129 Wave-5 | -| P7-014 | not-started | Phase 7 | Web admin panel — user CRUD, role assignment, system health | — | #125 Wave-6 | -| P7-019 | not-started | Phase 7 | CLI session management — list, resume, destroy sessions | — | #130 Wave-6 | -| P7-020 | not-started | Phase 7 | Coord DB migration — project-scoped missions, multi-tenant RBAC | — | #131 Wave-7 | -| FIX-02 | not-started | Backlog | TUI agent:end — fix React state updater side-effect | — | #133 Wave-8 | -| FIX-03 | not-started | Backlog | Agent session — cwd sandbox, system prompt, tool restrictions | — | #134 Wave-8 | -| P7-004 | not-started | Phase 7 | E2E test suite — Playwright critical paths | — | #55 Wave-9 | -| P7-006 | not-started | Phase 7 | Documentation — user guide, admin guide, dev guide | — | #57 Wave-9 | -| P7-007 | not-started | Phase 7 | Bare-metal deployment docs + .env.example | — | #58 Wave-9 | -| P7-021 | not-started | Phase 7 | Verify Phase 7 — feature-complete platform E2E | — | #132 Wave-10 | -| P8-001 | not-started | Phase 8 | Additional SSO providers — WorkOS + Keycloak | — | #53 | -| P8-002 | not-started | Phase 8 | Additional LLM providers — Codex, Z.ai, LM Studio, llama.cpp | — | #54 | -| P8-003 | not-started | Phase 8 | Performance optimization | — | #56 | -| P8-004 | not-started | Phase 8 | Beta release gate — v0.1.0 tag | — | #59 | -| FIX-01 | done | Backlog | Call piSession.dispose() in AgentService.destroySession | #78 | #62 | +| id | status | milestone | description | pr | notes | +| ------ | ----------- | --------- | ------------------------------------------------------------------- | ---- | ------------ | +| P0-001 | done | Phase 0 | Scaffold monorepo | #60 | #1 | +| P0-002 | done | Phase 0 | @mosaic/types — migrate and extend shared types | #65 | #2 | +| P0-003 | done | Phase 0 | @mosaic/db — Drizzle schema and PG connection | #67 | #3 | +| P0-004 | done | Phase 0 | @mosaic/auth — BetterAuth email/password setup | #68 | #4 | +| P0-005 | done | Phase 0 | Docker Compose — PG 17, Valkey 8, SigNoz | #65 | #5 | +| P0-006 | done | Phase 0 | OTEL foundation — OpenTelemetry SDK setup | #65 | #6 | +| P0-007 | done | Phase 0 | CI pipeline — Woodpecker config | #69 | #7 | +| P0-008 | done | Phase 0 | Project docs — AGENTS.md, CLAUDE.md, README | #69 | #8 | +| P0-009 | done | Phase 0 | Verify Phase 0 — CI green, all packages build | #70 | #9 | +| P1-001 | done | Phase 1 | apps/gateway scaffold — NestJS + Fastify adapter | #61 | #10 | +| P1-002 | done | Phase 1 | Auth middleware — BetterAuth session validation | #71 | #11 | +| P1-003 | done | Phase 1 | @mosaic/brain — migrate from v0, PG backend | #71 | #12 | +| P1-004 | done | Phase 1 | @mosaic/queue — migrate from v0 | #71 | #13 | +| P1-005 | done | Phase 1 | Gateway routes — conversations CRUD + messages | #72 | #14 | +| P1-006 | done | Phase 1 | Gateway routes — tasks, projects, missions CRUD | #72 | #15 | +| P1-007 | done | Phase 1 | WebSocket server — chat streaming | #61 | #16 | +| P1-008 | done | Phase 1 | Basic agent dispatch — single provider | #61 | #17 | +| P1-009 | done | Phase 1 | Verify Phase 1 — gateway functional, API tested | #73 | #18 | +| P2-001 | done | Phase 2 | @mosaic/agent — Pi SDK integration + agent pool | #61 | #19 | +| P2-002 | done | Phase 2 | Multi-provider support — Anthropic + Ollama | #74 | #20 | +| P2-003 | done | Phase 2 | Agent routing engine — cost/capability matrix | #75 | #21 | +| P2-004 | done | Phase 2 | Tool registration — brain, queue, memory tools | #76 | #22 | +| P2-005 | done | Phase 2 | @mosaic/coord — migrate from v0, gateway integration | #77 | #23 | +| P2-006 | done | Phase 2 | Agent session management — tmux + monitoring | #78 | #24 | +| P2-007 | done | Phase 2 | Verify Phase 2 — multi-provider routing works | #79 | #25 | +| P3-001 | done | Phase 3 | apps/web scaffold — Next.js 16 + BetterAuth + Tailwind | #82 | #26 | +| P3-002 | done | Phase 3 | Auth pages — login, registration, SSO redirect | #83 | #27 | +| P3-003 | done | Phase 3 | Chat UI — conversations, messages, streaming | #84 | #28 | +| P3-004 | done | Phase 3 | Task management — list view + kanban board | #86 | #29 | +| P3-005 | done | Phase 3 | Project & mission views — dashboard + PRD viewer | #87 | #30 | +| P3-006 | done | Phase 3 | Settings — provider config, profile, integrations | #88 | #31 | +| P3-007 | done | Phase 3 | Admin panel — user management, RBAC | #89 | #32 | +| P3-008 | done | Phase 3 | Verify Phase 3 — web dashboard functional E2E | — | #33 | +| P4-001 | done | Phase 4 | @mosaic/memory — preference + insight stores | — | #34 | +| P4-002 | done | Phase 4 | Semantic search — pgvector embeddings + search API | — | #35 | +| P4-003 | done | Phase 4 | @mosaic/log — log ingest, parsing, tiered storage | — | #36 | +| P4-004 | done | Phase 4 | Summarization pipeline — Haiku-tier LLM + cron | — | #37 | +| P4-005 | done | Phase 4 | Memory integration — inject into agent sessions | — | #38 | +| P4-006 | done | Phase 4 | Skill management — catalog, install, config | — | #39 | +| P4-007 | done | Phase 4 | Verify Phase 4 — memory + log pipeline working | — | #40 | +| P5-001 | done | Phase 5 | Plugin host — gateway plugin loading + channel interface | — | #41 | +| P5-002 | done | Phase 5 | @mosaic/discord-plugin — Discord bot + channel plugin | #61 | #42 | +| P5-003 | done | Phase 5 | @mosaic/telegram-plugin — Telegraf bot + channel plugin | — | #43 | +| P5-004 | done | Phase 5 | SSO — Authentik OIDC adapter end-to-end | — | #44 | +| P5-005 | done | Phase 5 | Verify Phase 5 — Discord + Telegram + SSO working | #99 | #45 | +| P6-001 | done | Phase 6 | @mosaic/cli — unified CLI binary + subcommands | #104 | #46 | +| P6-002 | done | Phase 6 | @mosaic/prdy — migrate PRD wizard from v0 | #101 | #47 | +| P6-003 | done | Phase 6 | @mosaic/quality-rails — migrate scaffolder from v0 | #100 | #48 | +| P6-004 | done | Phase 6 | @mosaic/mosaic — install wizard for v1 | #103 | #49 | +| P6-005 | done | Phase 6 | Pi TUI integration — mosaic tui | #61 | #50 | +| P6-006 | done | Phase 6 | Verify Phase 6 — CLI functional, all subcommands | — | #51 | +| P7-009 | done | Phase 7 | Web chat — WebSocket integration, streaming, conversation switching | #136 | #120 W1 done | +| P7-001 | done | Phase 7 | MCP endpoint hardening — streamable HTTP transport | #137 | #52 W1 done | +| P7-010 | done | Phase 7 | Web conversation management — list, search, rename, delete, archive | #139 | #121 W2 done | +| P7-015 | done | Phase 7 | Agent tool expansion — file ops, git, shell exec, web fetch | #138 | #126 W2 done | +| P7-011 | done | Phase 7 | Web project detail views — missions, tasks, PRDs, dashboards | #140 | #122 W3 done | +| P7-016 | done | Phase 7 | MCP client — gateway connects to external MCP servers as tools | #141 | #127 W3 done | +| P7-012 | in-progress | Phase 7 | Web provider management UI — add, configure, test LLM providers | — | #123 Wave-4 | +| P7-017 | in-progress | Phase 7 | Agent skill invocation — load and execute skills from catalog | — | #128 Wave-4 | +| P7-013 | not-started | Phase 7 | Web settings persistence — profile, preferences save to DB | — | #124 Wave-5 | +| P7-018 | not-started | Phase 7 | CLI model/provider switching — --model, --provider, /model in TUI | — | #129 Wave-5 | +| P7-014 | not-started | Phase 7 | Web admin panel — user CRUD, role assignment, system health | — | #125 Wave-6 | +| P7-019 | not-started | Phase 7 | CLI session management — list, resume, destroy sessions | — | #130 Wave-6 | +| P7-020 | not-started | Phase 7 | Coord DB migration — project-scoped missions, multi-tenant RBAC | — | #131 Wave-7 | +| FIX-02 | not-started | Backlog | TUI agent:end — fix React state updater side-effect | — | #133 Wave-8 | +| FIX-03 | not-started | Backlog | Agent session — cwd sandbox, system prompt, tool restrictions | — | #134 Wave-8 | +| P7-004 | not-started | Phase 7 | E2E test suite — Playwright critical paths | — | #55 Wave-9 | +| P7-006 | not-started | Phase 7 | Documentation — user guide, admin guide, dev guide | — | #57 Wave-9 | +| P7-007 | not-started | Phase 7 | Bare-metal deployment docs + .env.example | — | #58 Wave-9 | +| P7-021 | not-started | Phase 7 | Verify Phase 7 — feature-complete platform E2E | — | #132 Wave-10 | +| P8-001 | not-started | Phase 8 | Additional SSO providers — WorkOS + Keycloak | — | #53 | +| P8-002 | not-started | Phase 8 | Additional LLM providers — Codex, Z.ai, LM Studio, llama.cpp | — | #54 | +| P8-003 | not-started | Phase 8 | Performance optimization | — | #56 | +| P8-004 | not-started | Phase 8 | Beta release gate — v0.1.0 tag | — | #59 | +| FIX-01 | done | Backlog | Call piSession.dispose() in AgentService.destroySession | #78 | #62 |