Config policies (16 files): alert_routing, architecture_pressure, backlog, cost_weights, data_governance, incident_escalation, incident_intelligence, network_allowlist, nodes_registry, observability_sources, rbac_tools_matrix, release_gate, risk_attribution, risk_policy, slo_policy, tool_limits, tools_rollout Ops (22 files): Caddyfile, calendar compose, grafana voice dashboard, deployments/incidents logs, runbooks for alerts/audit/backlog/incidents/sofiia/voice, cron jobs, scripts (alert_triage, audit_cleanup, migrate_*, governance, schedule), task_registry, voice alerts/ha/latency/policy Docs (30+ files): HUMANIZED_STEPAN v2.7-v3 changelogs and runbooks, NODA1/NODA2 status and setup, audit index and traces, backlog, incident, supervisor, tools, voice, opencode, release, risk, aistalk, spacebot Made-with: Cursor
478 lines
17 KiB
Markdown
478 lines
17 KiB
Markdown
# Sofiia CTO Agent — Gaps & Recovery Plan (E)
|
||
|
||
> Generated: 2026-02-26 | P0 = блокуюче | P1 = критичне для vNext | P2 = покращення
|
||
|
||
---
|
||
|
||
## Критичне резюме
|
||
|
||
**Що вже готово і може йти в UI:** Chat, Voice, Projects CRUD, File upload, Sessions, Dialog Map tree, Ops actions, Node health.
|
||
|
||
**Що не готово і блокує vNext:** Tasks/Kanban, Meetings, Dialog Map canvas + Postgres schema, Doc versions, CTO Repo/Ops flow, Supervisor через BFF, Semantic search.
|
||
|
||
---
|
||
|
||
## Таблиця прогалин з пріоритетами
|
||
|
||
| # | Gap | Пріоритет | Складність | Блокує |
|
||
|---|-----|-----------|-----------|--------|
|
||
| G1 | `dialog_nodes`/`dialog_edges` Postgres tables + API | P0 | Medium | Dialog Map vNext |
|
||
| G2 | `tasks` table + CRUD API + Kanban UI | P0 | Medium | Projects Board |
|
||
| G3 | `meetings` table + CRUD API | P0 | Medium | Projects Meetings tab |
|
||
| G4 | Supervisor не проксюється через BFF | P0 | Low | CTO workflow access |
|
||
| G5 | `docs_versions` table + API | P1 | Low | Doc history/rollback |
|
||
| G6 | `entity_links` table + API | P1 | Low | Cross-entity linking |
|
||
| G7 | `repo_changesets` + `repo_patches` + PR flow | P1 | High | CTO code workflow |
|
||
| G8 | `ops_runs` job system (not one-shot) | P1 | Medium | CTO ops audit trail |
|
||
| G9 | Semantic search (Qdrant/Meilisearch) | P1 | Medium | Doc/Project search |
|
||
| G10 | NATS `attachment.created` on upload | P1 | Low | Parser pipeline hook |
|
||
| G11 | `DELETE` endpoints (projects/docs) | P1 | Low | CRUD completeness |
|
||
| G12 | Real-time WS events for map/tasks | P1 | Medium | Live UI updates |
|
||
| G13 | E2EE / confidential mode | P2 | Very High | Privacy |
|
||
| G14 | 2-step Plan → Apply for dangerous actions | P2 | High | Safe ops flow |
|
||
| G15 | `agent_id="l"` vs `"sofiia"` inconsistency | P1 | Low | Config correctness |
|
||
| G16 | `dialog_views` saved views | P2 | Low | UX |
|
||
| G17 | NODA3 integration | P2 | Medium | AI/ML workstation |
|
||
| G18 | Meilisearch deployment | P2 | Low | Full-text search |
|
||
| G19 | Privacy Gate middleware (Router) | P2 | High | Confidential mode |
|
||
| G20 | Wiki Markdown editor UI | P2 | Medium | Docs/Wiki experience |
|
||
| G21 | `doc_index_state` table + reindex jobs | P2 | Low | AI doc indexing |
|
||
| G22 | Meeting reminders (push/WS) | P2 | Medium | Meetings UX |
|
||
| G23 | `DELETE /api/nodes/{id}` | P2 | Low | Node management |
|
||
| G24 | S3/MinIO для file storage | P2 | High | Scale (replace volume) |
|
||
|
||
---
|
||
|
||
## P0 — Блокуючі прогалини (потрібні для vNext)
|
||
|
||
### G1: Dialog Map — Postgres schema + API
|
||
|
||
**Що зроблено:** SQLite tree via `parent_msg_id`. Works for conversation branching.
|
||
|
||
**Чого не вистачає:**
|
||
- Postgres tables: `dialog_nodes`, `dialog_edges`, `dialog_views`
|
||
- API: `GET /api/projects/{id}/dialog-map`, `POST /api/links`
|
||
- WS event: `dialog_map.updated`
|
||
- Auto-edge creation from NATS events
|
||
|
||
**Recovery plan:**
|
||
```sql
|
||
-- Step 1: Add to sofiia-console db.py (SQLite first, Postgres later)
|
||
CREATE TABLE IF NOT EXISTS dialog_nodes (
|
||
node_id TEXT PRIMARY KEY,
|
||
project_id TEXT NOT NULL,
|
||
node_type TEXT NOT NULL CHECK(node_type IN ('message','task','doc','meeting','agent_run','decision','goal')),
|
||
ref_id TEXT NOT NULL, -- FK to actual entity
|
||
title TEXT DEFAULT '',
|
||
created_at TEXT NOT NULL,
|
||
created_by TEXT DEFAULT 'system'
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS dialog_edges (
|
||
edge_id TEXT PRIMARY KEY,
|
||
project_id TEXT NOT NULL,
|
||
from_node_id TEXT NOT NULL REFERENCES dialog_nodes(node_id),
|
||
to_node_id TEXT NOT NULL REFERENCES dialog_nodes(node_id),
|
||
edge_type TEXT NOT NULL CHECK(edge_type IN ('references','resolves','derives_task','updates_doc','schedules','summarizes')),
|
||
created_at TEXT NOT NULL,
|
||
props TEXT DEFAULT '{}' -- JSON
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS dialog_views (
|
||
view_id TEXT PRIMARY KEY,
|
||
project_id TEXT NOT NULL,
|
||
name TEXT NOT NULL,
|
||
filters TEXT DEFAULT '{}',
|
||
layout TEXT DEFAULT '{}'
|
||
);
|
||
```
|
||
|
||
```python
|
||
# Step 2: New endpoint in docs_router.py
|
||
@router.get("/api/projects/{project_id}/dialog-map")
|
||
async def get_project_dialog_map(project_id: str):
|
||
nodes = await db.get_dialog_nodes(project_id)
|
||
edges = await db.get_dialog_edges(project_id)
|
||
return {"nodes": nodes, "edges": edges}
|
||
|
||
@router.post("/api/links")
|
||
async def create_link(body: LinkCreate):
|
||
# Creates dialog_edge between two entities
|
||
...
|
||
```
|
||
|
||
**Оцінка:** 4–6 годин роботи.
|
||
|
||
---
|
||
|
||
### G2: Tasks + Kanban
|
||
|
||
**Що зроблено:** Немає.
|
||
|
||
**Recovery plan:**
|
||
```sql
|
||
CREATE TABLE IF NOT EXISTS tasks (
|
||
task_id TEXT PRIMARY KEY,
|
||
project_id TEXT NOT NULL REFERENCES projects(project_id),
|
||
title TEXT NOT NULL,
|
||
description TEXT DEFAULT '',
|
||
status TEXT DEFAULT 'backlog' CHECK(status IN ('backlog','in_progress','review','done')),
|
||
priority TEXT DEFAULT 'medium',
|
||
assignee_id TEXT DEFAULT '',
|
||
labels TEXT DEFAULT '[]', -- JSON
|
||
due_at TEXT,
|
||
created_at TEXT NOT NULL,
|
||
updated_at TEXT NOT NULL,
|
||
msg_id TEXT -- Optional: link to originating message
|
||
);
|
||
```
|
||
|
||
- API: `GET/POST /api/projects/{id}/tasks`, `PATCH /api/tasks/{id}`, `DELETE /api/tasks/{id}`
|
||
- UI: Kanban board з drag-drop (можна почати з простим list + status buttons)
|
||
- Dialog Map auto-edge: `POST /api/links` after task creation
|
||
|
||
**Оцінка:** 1–2 дні (backend + basic UI).
|
||
|
||
---
|
||
|
||
### G3: Meetings
|
||
|
||
**Recovery plan:**
|
||
```sql
|
||
CREATE TABLE IF NOT EXISTS meetings (
|
||
meeting_id TEXT PRIMARY KEY,
|
||
project_id TEXT NOT NULL REFERENCES projects(project_id),
|
||
title TEXT NOT NULL,
|
||
starts_at TEXT NOT NULL,
|
||
duration_min INTEGER DEFAULT 60,
|
||
attendees TEXT DEFAULT '[]', -- JSON
|
||
location TEXT DEFAULT '',
|
||
agenda TEXT DEFAULT '',
|
||
created_at TEXT NOT NULL
|
||
);
|
||
```
|
||
|
||
- API: `GET/POST /api/projects/{id}/meetings`, `PATCH /api/meetings/{id}`
|
||
- UI: simple form (title, date/time, duration, attendees)
|
||
- Reminders: Phase 2 (WS push)
|
||
|
||
**Оцінка:** 1 день.
|
||
|
||
---
|
||
|
||
### G4: Supervisor → BFF proxy
|
||
|
||
**Що зроблено:** Supervisor API exists at `http://sofiia-supervisor:8080` (або port 9400).
|
||
|
||
**Recovery plan:**
|
||
```python
|
||
# Add to services/sofiia-console/app/main.py:
|
||
|
||
SUPERVISOR_URL = os.getenv("SUPERVISOR_URL", "http://sofiia-supervisor:8080")
|
||
|
||
@app.post("/api/supervisor/runs")
|
||
async def run_supervisor_graph(body: dict, _auth: str = Depends(require_auth)):
|
||
async with httpx.AsyncClient() as c:
|
||
resp = await c.post(f"{SUPERVISOR_URL}/v1/graphs/{body['graph']}/runs",
|
||
json=body, timeout=60)
|
||
return resp.json()
|
||
|
||
@app.get("/api/supervisor/runs/{run_id}")
|
||
async def get_supervisor_run(run_id: str, _auth: str = Depends(require_auth)):
|
||
async with httpx.AsyncClient() as c:
|
||
resp = await c.get(f"{SUPERVISOR_URL}/v1/runs/{run_id}", timeout=10)
|
||
return resp.json()
|
||
```
|
||
|
||
**Оцінка:** 30 хвилин.
|
||
|
||
---
|
||
|
||
## P1 — Критичні для vNext
|
||
|
||
### G5: Doc versions
|
||
|
||
```sql
|
||
CREATE TABLE IF NOT EXISTS doc_versions (
|
||
version_id TEXT PRIMARY KEY,
|
||
doc_id TEXT NOT NULL REFERENCES documents(doc_id),
|
||
content TEXT NOT NULL, -- full text
|
||
author_id TEXT DEFAULT 'system',
|
||
created_at TEXT NOT NULL
|
||
);
|
||
```
|
||
|
||
```python
|
||
# New endpoints in docs_router.py:
|
||
# GET /api/projects/{pid}/documents/{did}/versions
|
||
# POST /api/projects/{pid}/documents/{did}/restore
|
||
```
|
||
|
||
**Оцінка:** 2 години.
|
||
|
||
---
|
||
|
||
### G7: Repo Changesets (CTO Code Flow)
|
||
|
||
Це найскладніша частина. **Рекомендація:** почати з mock endpoints, потім реалізувати реальну логіку.
|
||
|
||
**Mock endpoint (30 хв):**
|
||
```python
|
||
@app.post("/api/repo/changesets")
|
||
async def create_changeset_mock(body: dict, _auth=Depends(require_auth)):
|
||
# Mock: store in SQLite, return changeset_id
|
||
cs_id = str(uuid.uuid4())
|
||
# await db.save_changeset(cs_id, body)
|
||
return {"changeset_id": cs_id, "status": "draft", "mock": True}
|
||
```
|
||
|
||
**Реальна реалізація (2–3 дні):**
|
||
```sql
|
||
CREATE TABLE repo_changesets (
|
||
cs_id TEXT PRIMARY KEY,
|
||
project_id TEXT,
|
||
repo TEXT NOT NULL, -- e.g., "github.com/IvanTytar/microdao-daarion"
|
||
base_ref TEXT NOT NULL, -- branch/commit
|
||
intent TEXT NOT NULL,
|
||
risk_level TEXT DEFAULT 'low',
|
||
status TEXT DEFAULT 'draft',
|
||
created_by TEXT,
|
||
created_at TEXT NOT NULL
|
||
);
|
||
|
||
CREATE TABLE repo_patches (
|
||
patch_id TEXT PRIMARY KEY,
|
||
cs_id TEXT NOT NULL REFERENCES repo_changesets(cs_id),
|
||
file_path TEXT NOT NULL,
|
||
patch_text TEXT NOT NULL, -- unified diff
|
||
created_at TEXT NOT NULL
|
||
);
|
||
|
||
CREATE TABLE pull_requests (
|
||
pr_id TEXT PRIMARY KEY,
|
||
cs_id TEXT NOT NULL REFERENCES repo_changesets(cs_id),
|
||
provider TEXT DEFAULT 'github', -- github/gitlab/gitea
|
||
pr_url TEXT,
|
||
pr_number INTEGER,
|
||
status TEXT DEFAULT 'draft',
|
||
created_at TEXT NOT NULL
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
### G8: Ops Runs (Job System)
|
||
|
||
Поточний `/api/ops/run` — one-shot dispatch. Потрібен job tracking.
|
||
|
||
```sql
|
||
CREATE TABLE ops_runs (
|
||
run_id TEXT PRIMARY KEY,
|
||
project_id TEXT,
|
||
node_id TEXT NOT NULL, -- noda1/noda2
|
||
action TEXT NOT NULL, -- з allowlist
|
||
params TEXT DEFAULT '{}', -- JSON
|
||
dry_run INTEGER DEFAULT 1,
|
||
status TEXT DEFAULT 'pending', -- pending/running/success/failed
|
||
result TEXT DEFAULT '',
|
||
started_at TEXT,
|
||
finished_at TEXT,
|
||
created_by TEXT
|
||
);
|
||
```
|
||
|
||
**API:**
|
||
- `POST /api/ops/runs` (створити job, dry_run=true за замовч.)
|
||
- `GET /api/ops/runs/{id}` (статус)
|
||
- `GET /api/ops/runs?project_id=&limit=20` (список)
|
||
|
||
**Оцінка:** 4 години (backend) + 2 год (UI list).
|
||
|
||
---
|
||
|
||
### G10: NATS attachment.created
|
||
|
||
Одна зміна в `docs_router.py`:
|
||
|
||
```python
|
||
# After successful file save:
|
||
try:
|
||
import nats
|
||
nc = await nats.connect(NATS_URL)
|
||
await nc.publish(f"attachment.created.{mime_category}",
|
||
json.dumps({"file_id": file_id, "doc_id": doc_id, ...}).encode())
|
||
await nc.close()
|
||
except Exception:
|
||
pass # best-effort
|
||
```
|
||
|
||
**Оцінка:** 1 година.
|
||
|
||
---
|
||
|
||
### G15: agent_id "l" vs "sofiia"
|
||
|
||
У `services/router/router-config.yml` для NODA2:
|
||
|
||
```yaml
|
||
# Check if there's "l:" entry that should be "sofiia:"
|
||
```
|
||
|
||
**Action:** знайти і замінити `"l"` → `"sofiia"` у router-config відповідної ноди.
|
||
|
||
**Оцінка:** 15 хвилин.
|
||
|
||
---
|
||
|
||
## P2 — Покращення
|
||
|
||
### G13: E2EE (confidential mode)
|
||
|
||
**Складність:** Дуже висока. Потребує:
|
||
1. Client-side key generation (WebCrypto API)
|
||
2. Server-side: store only ciphertext + key_id
|
||
3. Router Privacy Gate middleware
|
||
4. Dialog Map: тільки user-created edges (не semantic auto-edges)
|
||
5. Search: тільки metadata, не plaintext
|
||
|
||
**Рекомендація:** Не реалізовувати до завершення Projects + Dialog Map. Спочатку `mode=public` тільки.
|
||
|
||
---
|
||
|
||
### G20: Wiki Markdown Editor
|
||
|
||
Потрібна бібліотека (CodeMirror / Monaco / Tiptap). Для Phase 1 — textarea з preview.
|
||
|
||
```html
|
||
<!-- Simple Phase 1 wiki editor -->
|
||
<div id="wikiEditor">
|
||
<textarea id="wikiContent" placeholder="# Сторінка wiki..."></textarea>
|
||
<div id="wikiPreview" class="markdown-preview"></div>
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
## Quick Wins (до 2 годин кожен)
|
||
|
||
| # | Quick Win | Час | Цінність |
|
||
|---|-----------|-----|---------|
|
||
| QW1 | `DELETE /api/projects/{id}` | 15 хв | CRUD completeness |
|
||
| QW2 | `DELETE /api/projects/{id}/documents/{did}` | 15 хв | CRUD completeness |
|
||
| QW3 | BFF proxy до Supervisor (G4) | 30 хв | CTO workflow access |
|
||
| QW4 | Mock `/api/repo/changesets` | 30 хв | UI CTO panel development |
|
||
| QW5 | Mock `/api/ops/runs` | 30 хв | UI CTO panel development |
|
||
| QW6 | `docs_versions` table + API (G5) | 2 год | Doc history |
|
||
| QW7 | `USE_EMBEDDINGS=true` + Qdrant ingest | 1 год | Semantic search |
|
||
| QW8 | `agent_id "l"` → `"sofiia"` fix | 15 хв | Config consistency |
|
||
| QW9 | NATS `attachment.created` on upload | 1 год | Parser pipeline |
|
||
| QW10 | WS `dialog_map.updated` basic event | 1 год | Live map refresh |
|
||
|
||
---
|
||
|
||
## Повний план відновлення (поетапно)
|
||
|
||
### Тиждень 1: Stabilize & Quick Wins
|
||
|
||
```
|
||
Day 1–2:
|
||
- QW1, QW2, QW3, QW8 (CRUD + Supervisor proxy + agent_id fix)
|
||
- Деплой на NODA2, verify через http://localhost:8002
|
||
|
||
Day 3–4:
|
||
- G2: tasks table + basic API + simple list UI
|
||
- G3: meetings table + basic form UI
|
||
|
||
Day 5:
|
||
- G5: docs_versions + API
|
||
- G10: NATS attachment.created
|
||
- QW4, QW5: mock changeset/ops_run endpoints for UI
|
||
```
|
||
|
||
### Тиждень 2: Dialog Map + CTO Panel
|
||
|
||
```
|
||
Day 1–2:
|
||
- G1: dialog_nodes/edges tables + API
|
||
- WS event: dialog_map.updated
|
||
|
||
Day 3–4:
|
||
- UI: Dialog Map canvas (D3 tree → force graph)
|
||
- Entity links UI (drag edge between nodes)
|
||
|
||
Day 5:
|
||
- G8: ops_runs job system
|
||
- UI: CTO Ops panel (list + status)
|
||
```
|
||
|
||
### Тиждень 3: Advanced Features
|
||
|
||
```
|
||
- G7: Repo changesets (real implementation)
|
||
- G9: USE_EMBEDDINGS=true + semantic search
|
||
- G12: Full real-time WS events (tasks, docs, meetings)
|
||
- Kanban drag-drop UI
|
||
- Doc versions diff viewer
|
||
```
|
||
|
||
### Тиждень 4+: Scale & Polish
|
||
|
||
```
|
||
- G14: 2-step Plan → Apply
|
||
- G20: Wiki Markdown editor
|
||
- G22: Meeting reminders
|
||
- G24: S3/MinIO for file storage
|
||
- G13: E2EE (only when everything else is stable)
|
||
```
|
||
|
||
---
|
||
|
||
## 5 Найбільш Критичних Прогалин
|
||
|
||
1. **`dialog_nodes/edges` + project-level Dialog Map API** — без цього vNext граф неможливий
|
||
2. **Tasks/Kanban** — Projects без задач = тільки файлосховище
|
||
3. **Meetings** — Projects без зустрічей = неповний workflow
|
||
4. **Supervisor не проксюється через BFF** — CTO не може запускати LangGraph runs з UI
|
||
5. **Repo changesets / CTO code flow** — Sofiia не може "пропонувати PR" як structured artifact
|
||
|
||
---
|
||
|
||
## 5 Найбільш Готових Частин для UI
|
||
|
||
1. **Chat + Voice** — повністю готово, production-grade (Phase 2 streaming, HA, SLO, alerts)
|
||
2. **Projects + Documents + File Upload** — CRUD, search, sessions — все є
|
||
3. **Dialog Map tree** — `GET /api/sessions/{id}/map` повертає nodes/edges
|
||
4. **Ops Actions** — risk/pressure/backlog/notion/release — все є через `/api/ops/run`
|
||
5. **Node Health Dashboard** — multi-node, SSH, WebSocket realtime — все є
|
||
|
||
---
|
||
|
||
## 3 Рекомендації "Зробити Негайно"
|
||
|
||
### 1. Зберегти контекст у Dialog Map
|
||
|
||
Найпростіший спосіб не "загубити" поточний дизайн — додати `dialog_nodes/edges` tables у `db.py` прямо зараз (схема вже описана вище). Навіть якщо UI ще не готовий, дані почнуть накопичуватись від поточних повідомлень.
|
||
|
||
### 2. Proxy Supervisor через BFF
|
||
|
||
30 хвилин роботи, але це дасть Sofiia доступ до `alert_triage`, `incident_triage`, `postmortem_draft`, `release_check` прямо з UI Console — не тільки через Telegram.
|
||
|
||
### 3. Нормалізувати `agent_id`
|
||
|
||
Знайти і виправити `"l"` → `"sofiia"` у конфігурації NODA2. Це унеможливить silent routing failures де Router не знаходить агента і тихо fallbacks до дефолту.
|
||
|
||
---
|
||
|
||
## Next Actions for UI Team (1–2 days)
|
||
|
||
1. **Розгорнути і протестувати** поточний стек на NODA2 — `http://localhost:8002/` вже повністю робочий
|
||
2. **Реалізувати QW1–QW5** (прості DELETE + Supervisor proxy + mock endpoints) — 2–3 год
|
||
3. **Додати `tasks` і `meetings` tables** у `db.py` та відповідні endpoints у `docs_router.py`
|
||
4. **Додати `dialog_nodes/edges`** у `db.py` (DDL вище) і endpoint `GET /api/projects/{id}/dialog-map`
|
||
5. **Тестувати** через `tests/test_sofiia_docs.py` — всі 28 тестів мають пройти
|
||
6. **Оновити** `docker-compose.node2-sofiia.yml` з `SUPERVISOR_URL` env var
|
||
7. **Перевірити** що `ops/voice_ha_smoke.sh` проходить після деплою
|
||
8. **Прочитати** `docs/architecture_inventory/` (7 файлів) для повного контексту поточного стеку
|
||
9. **Використовувати** `ops/fabric_preflight.sh` перед кожним деплоєм (preflight-first policy)
|
||
10. **Щотижня**: запускати `ops/fabric_snapshot.py --save` і commit результат — щоб мати baseline для drift detection
|