feat(lib): bundle MCP servers from .common/lib into factory/lib/
Copies source (no node_modules, dist, .tasks, .wiki, __pycache__) for: - projects-meta-mcp v2.25.0 (TypeScript/Node) - wiki-graph v0.3.1 (TypeScript/Node) - interns-mcp v0.3.3 (Python/FastMCP) .gitignore: exclude lib build artefacts (node_modules, dist, .venv, __pycache__, *.pyc) bootstrap.ps1: add MCP build step — npm install+build for TS servers, venv+pip for Python Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
# projects-meta-mcp — Design
|
||||
|
||||
**Date:** 2026-04-29
|
||||
**Status:** Approved (брейнсторм завершён, спека к ревью)
|
||||
**Owner:** vitya
|
||||
|
||||
## Цель
|
||||
|
||||
Дать Claude (и любому MCP-клиенту) единый, дешёвый и offline-устойчивый доступ к двум видам мета-информации, разбросанным по множеству проектов и нескольких машин:
|
||||
|
||||
1. **Статусы задач** — свод `.tasks/STATUS.md` всех проектов в Gitea.
|
||||
2. **Общие знания** — поиск и чтение страниц общей вики (`projects-wiki`), отфильтрованных по домену текущего проекта.
|
||||
|
||||
## Не цели (явный YAGNI)
|
||||
|
||||
- ❌ Удалённый MCP-процесс на Synology (HTTP/SSE-транспорт). Откладывается до момента, когда «truly always-on» станет реальной болью.
|
||||
- ❌ Карантин `_pending/` для projects-wiki. Решение по промоушену принимается в моменте.
|
||||
- ❌ Sparse-checkout всех проектов на каждую машину. API + кэш проще.
|
||||
- ❌ Серверный агрегатор / git-hook на стороне Gitea. Клиентский sync проще.
|
||||
- ❌ Embedding/семантический поиск projects-wiki. Достаточно поиска по тексту/тегам.
|
||||
- ❌ Управление зависимостями между проектами, GANTT, deadlines. Только статусы.
|
||||
|
||||
## Архитектура
|
||||
|
||||
```
|
||||
┌──────────────────┐
|
||||
│ Gitea (Synology)│
|
||||
│ - все проекты │
|
||||
│ - projects-wiki │
|
||||
│ - projects-meta-│
|
||||
│ mcp (этот код)│
|
||||
└────────┬─────────┘
|
||||
│ pull / API
|
||||
▼
|
||||
┌────────────────── каждая машина ─────────────────┐
|
||||
│ │
|
||||
│ ~/projects/.wiki/ ← git clone │
|
||||
│ ~/.cache/projects-mcp/ ← JSON-кэш STATUS │
|
||||
│ │
|
||||
│ sync-script (cron / руками): │
|
||||
│ - git pull в projects-wiki │
|
||||
│ - Gitea API → собрать STATUS.md → JSON-кэш │
|
||||
│ - offline → no-op, кэш остаётся │
|
||||
│ │
|
||||
│ mcp-server (локальный, stdio): │
|
||||
│ Tools: │
|
||||
│ tasks.aggregate() │
|
||||
│ tasks.search(q) │
|
||||
│ knowledge.search(q, domain?) │
|
||||
│ knowledge.get(slug) │
|
||||
│ knowledge.suggest_promote() │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Принцип:** MCP в hot path читает только локальные файлы. Сетевые вызовы — только в `sync-script`.
|
||||
|
||||
## Компоненты
|
||||
|
||||
### 1. `projects-wiki` — отдельный git-репозиторий на Gitea
|
||||
|
||||
**Имя репо:** `projects-wiki`. Клонируется на каждую машину под `~/projects/.wiki/`.
|
||||
|
||||
**Структура:**
|
||||
|
||||
```
|
||||
~/projects/.wiki/
|
||||
├── CLAUDE.md ← правила промоушена + список доменов
|
||||
├── README.md
|
||||
├── cross/ ← кросс-доменные знания (git, vscode, claude-code, общие гочи)
|
||||
├── node/ ← Node.js / TypeScript / npm / yarn
|
||||
├── embedded/ ← микроконтроллеры, PlatformIO, STM32, Arduino
|
||||
├── web/ ← фронтенд, браузер, HTTP, Nginx
|
||||
└── ... ← новые домены добавляются по мере появления проектов
|
||||
```
|
||||
|
||||
**Frontmatter каждой страницы:**
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: Windows yarn requires exec()
|
||||
domain: node # обязательное: node | embedded | web | cross | ...
|
||||
shared_at: 2026-04-29 # дата промоушена
|
||||
applies_to: [windows] # опц. — платформенные ограничения
|
||||
source_project: npm-mcp # опц. — откуда промоутили
|
||||
---
|
||||
```
|
||||
|
||||
Тело — стандартный concept: **Why** + **Когда / Как чинить**.
|
||||
|
||||
**`projects-wiki/CLAUDE.md`** содержит:
|
||||
- Полный список валидных доменов и что в каждый кладётся.
|
||||
- Эвристику промоушена (см. ниже).
|
||||
- Чёрный список (имена проектов, бизнес-доменов — никогда не упоминаются в shared).
|
||||
|
||||
### 2. `sync-script`
|
||||
|
||||
Bash или Node-скрипт. На каждой машине, запускается cron'ом / руками / git-хуком.
|
||||
|
||||
**Шаги:**
|
||||
|
||||
1. **Pull projects-wiki:**
|
||||
```
|
||||
git -C ~/projects/.wiki pull --quiet
|
||||
```
|
||||
Offline → silently fail (exit 0), кэш не трогается.
|
||||
|
||||
2. **Скачать список репозиториев пользователя через Gitea API:**
|
||||
```
|
||||
GET https://git.kzntsv.site/api/v1/users/OpeItcLoc03/repos?limit=50
|
||||
```
|
||||
С токеном (в `~/.config/projects-mcp/auth.toml`).
|
||||
|
||||
3. **Параллельно (limit 10) скачать `STATUS.md` каждого:**
|
||||
```
|
||||
GET https://git.kzntsv.site/api/v1/repos/OpeItcLoc03/{repo}/raw/.tasks/STATUS.md
|
||||
```
|
||||
404 → у проекта нет тасок, пропустить. Сетевая ошибка → пропустить, оставить старое значение в кэше.
|
||||
|
||||
4. **Собрать в `~/.cache/projects-mcp/tasks.json`** атомарно (write to `tasks.json.tmp`, rename):
|
||||
```json
|
||||
{
|
||||
"synced_at": "2026-04-29T12:00:00Z",
|
||||
"synced_from": "https://git.kzntsv.site",
|
||||
"machine": "vitya-desktop",
|
||||
"projects": [
|
||||
{
|
||||
"name": "npm-mcp",
|
||||
"default_branch": "main",
|
||||
"fetched_at": "2026-04-29T12:00:01Z",
|
||||
"active_tasks": [
|
||||
{"slug": "wiki-self-ingest", "status": "active", "next": "..."}
|
||||
],
|
||||
"all_tasks_count": 5,
|
||||
"raw": "<полный STATUS.md>"
|
||||
}
|
||||
],
|
||||
"errors": [{"project": "books", "reason": "network"}]
|
||||
}
|
||||
```
|
||||
|
||||
5. **Exit 0 даже при частичных ошибках.** Логи в `~/.cache/projects-mcp/sync.log`.
|
||||
|
||||
### 3. `mcp-server`
|
||||
|
||||
Stateless Node-процесс, stdio-транспорт MCP. Читает только локальные файлы. Не делает сетевых вызовов.
|
||||
|
||||
#### Tool surface
|
||||
|
||||
| Tool | Аргументы | Возвращает |
|
||||
|------|-----------|------------|
|
||||
| `tasks.aggregate()` | `(filter?: { status?, project? })` | Список активных задач по всем проектам. Только мета (slug, project, status, next-action), не полный текст. |
|
||||
| `tasks.search(query)` | `query: string` | Поиск по `STATUS.md`-кэшу (substring + per-task title). |
|
||||
| `tasks.get(project)` | `project: string` | Полный `STATUS.md` одного проекта. |
|
||||
| `knowledge.search(query, opts?)` | `query: string, opts?: { domain?: string\|"all", limit?: number }` | До 10 заголовков + 1-строчное описание. По умолчанию фильтр зависит от detected domain текущего проекта (см. ниже). |
|
||||
| `knowledge.get(slug)` | `slug: string` (например `node/windows-yarn-exec`) | Полная страница (frontmatter + тело). |
|
||||
| `knowledge.suggest_promote()` | `()` | Возвращает кандидатов на промоушен из текущего проекта. См. логику ниже. |
|
||||
| `meta.status()` | `()` | Возвращает диагностику: когда был последний sync, кэш-stale-возраст, сколько проектов, сколько shared-страниц. |
|
||||
|
||||
#### Two-step паттерн
|
||||
|
||||
`search` всегда возвращает только индекс (заголовок + 1 строка). `get` возвращает полный текст. Минимизирует токены — модель сама решает что углублять.
|
||||
|
||||
#### Detect domain текущего проекта
|
||||
|
||||
При запуске MCP получает `cwd` от клиента. Алгоритм:
|
||||
|
||||
1. Если в `cwd` есть `package.json` → `node`.
|
||||
2. Если есть `platformio.ini` / `*.ino` / `CMakeLists.txt` с упоминанием `arm-none-eabi` или embedded SDK → `embedded`.
|
||||
3. Если есть `next.config.*` / `vite.config.*` / `index.html` без `package.json` (статика) → `web`.
|
||||
4. Иначе → `unknown`.
|
||||
|
||||
Логика фильтрации `knowledge.search`:
|
||||
- Detected domain — конкретный (`node` / `embedded` / `web`) → показываем `domain == detected || domain == "cross"`.
|
||||
- Detected domain `unknown` → фильтра нет (видны все домены).
|
||||
- Опциональный аргумент `opts.domain` пользователя/Claude переопределяет: `"all"` снимает фильтр явно, конкретное имя меняет таргет.
|
||||
|
||||
`cross` в frontmatter — это **тег страницы** (универсальное знание). Detected `unknown` — это **режим поиска** (фильтра нет). Не путать.
|
||||
|
||||
Логика выносится в отдельный модуль `domain-detector` (тестируется юнит-тестами).
|
||||
|
||||
#### `knowledge.suggest_promote()`
|
||||
|
||||
Логика:
|
||||
|
||||
1. Прочесть все `concepts/*.md` в `<cwd>/.wiki/concepts/`.
|
||||
2. Для каждого применить эвристику:
|
||||
- **Включить как кандидата**, если страница: упоминает платформу/тулинг (Windows, yarn, Node, MCP, git, Docker), не упоминает доменные имена из чёрного списка `projects-wiki/CLAUDE.md`.
|
||||
- **Усилить сигнал**, если в `projects-wiki` есть страница с близким заголовком в другом домене (= знание подтверждено повторно).
|
||||
3. Вернуть список кандидатов: `{ slug, current_path, suggested_domain, confidence, reason }`.
|
||||
4. Решение принимает пользователь (Claude в чате предлагает строкой `_Кандидат на shared: ... Промоутить?_`, ждёт `y`/`n`).
|
||||
|
||||
Промоушен ≠ автомат. Tool только **возвращает кандидатов**, ничего не пишет.
|
||||
|
||||
### 4. Bootstrap
|
||||
|
||||
На новой машине (пример Windows + Git Bash; на других ОС адаптировать пути):
|
||||
|
||||
```bash
|
||||
# один раз:
|
||||
GITEA=https://git.kzntsv.site
|
||||
OWNER=OpeItcLoc03
|
||||
git clone $GITEA/$OWNER/projects-meta-mcp ~/.local/projects-meta-mcp
|
||||
cd ~/.local/projects-meta-mcp && npm install && npm run build
|
||||
git clone $GITEA/$OWNER/projects-wiki ~/projects/.wiki
|
||||
|
||||
mkdir -p ~/.config/projects-mcp
|
||||
cp ~/.local/projects-meta-mcp/auth.toml.example ~/.config/projects-mcp/auth.toml
|
||||
# отредактировать auth.toml — заполнить gitea_token
|
||||
|
||||
node ~/.local/projects-meta-mcp/dist/sync.js # первый sync
|
||||
# зарегистрировать MCP в claude code config (см. ниже)
|
||||
```
|
||||
|
||||
**Формат `auth.toml`:**
|
||||
|
||||
```toml
|
||||
gitea_url = "https://git.kzntsv.site"
|
||||
gitea_user = "OpeItcLoc03"
|
||||
gitea_token = "..." # personal access token, scope: read:repository
|
||||
```
|
||||
|
||||
**Регистрация в Claude Code** (`~/.claude.json` или per-project) — пути в Windows-формате с прямыми слэшами:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"projects-meta": {
|
||||
"command": "node",
|
||||
"args": ["C:/Users/vitya/.local/projects-meta-mcp/dist/server.js"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Поведение в offline
|
||||
|
||||
| Ситуация | Что работает | Что не работает |
|
||||
|----------|--------------|------------------|
|
||||
| Сеть есть, синк свежий | Всё | — |
|
||||
| Сеть есть, sync не запускался N часов | Всё (кэш отдаёт старое) | Свежесть других машин |
|
||||
| Сеть пропала | `tasks.*`, `knowledge.search/get` — всё работает | `knowledge.suggest_promote` работает; `git push` после промоушена откладывается |
|
||||
| Кэш отсутствует (новая машина без bootstrap) | Только `meta.status()` (отдаёт diagnostic с пустым кэшем) | Всё остальное |
|
||||
|
||||
`meta.status()` всегда возвращает возраст кэша, чтобы Claude мог предупредить пользователя «данные старше 24 часов».
|
||||
|
||||
## Авторизация
|
||||
|
||||
- Gitea API token: scope `read:repository` (и `write:repository` для пуша projects-wiki).
|
||||
- Хранится в `~/.config/projects-mcp/auth.toml` (gitignore'd).
|
||||
- MCP-сервер сам не читает токен — только `sync-script`. MCP оперирует уже скачанными локальными файлами.
|
||||
|
||||
## Тестирование
|
||||
|
||||
- **Unit:** `domain-detector`, парсер `STATUS.md`, эвристика промоушена.
|
||||
- **Integration:** sync-script против локального Gitea (Docker fixture). Проверка: rate limit, 404 на отсутствующий `STATUS.md`, частичный отказ.
|
||||
- **MCP smoke test:** запустить сервер, дёрнуть каждый tool через MCP-клиент-fixture.
|
||||
|
||||
## Открытые вопросы (не блокеры)
|
||||
|
||||
1. **Cron vs git-hook vs руками** — определится при первом использовании. Стартовать с «руками + npm script», добавить cron когда станет лень.
|
||||
2. **Уведомление о stale-кэше** — порог в часах? Стартовать с 24, корректировать.
|
||||
3. **Multi-user** — пока однопользовательский (`OpeItcLoc03`). Если кто-то ещё начнёт пользоваться — переделать `users/{owner}/repos` в параметр из `auth.toml`.
|
||||
4. **`projects-wiki` GC** — раз в полгода аудит, какие страницы не использовались. Будет отдельный tool `meta.audit_shared(months)` в v2.
|
||||
|
||||
## Дальнейшие шаги
|
||||
|
||||
1. Создать репозиторий `projects-meta-mcp` на Gitea, запушить этот скелет.
|
||||
2. Создать репозиторий `projects-wiki` на Gitea с минимальной структурой (`CLAUDE.md`, `cross/`, `node/`).
|
||||
3. Перейти к фазе writing-plans: расписать пошаговый план реализации (sync-script сначала, MCP-сервер вторым, эвристика промоушена третьим).
|
||||
@@ -0,0 +1,170 @@
|
||||
# Federated knowledge query — `knowledge.ask_projects` / `knowledge.get_from`
|
||||
|
||||
**Status:** design approved 2026-04-30
|
||||
**Repo:** `projects-meta-mcp`
|
||||
**Adds:** 2 read-only MCP tools + 1 sync extension + 1 cache field
|
||||
|
||||
## Problem
|
||||
|
||||
Сейчас `knowledge.search` / `knowledge.get` читают только из shared `projects-wiki` (общая мета-вики). Если знания там нет — у агента тупик. При этом локальные `.wiki/` отдельных проектов могут это знание содержать (например, `node-utils/.wiki/concepts/windows-yarn-exec.md` — частный кейс который никто ещё не промоутил в shared).
|
||||
|
||||
Хотим: если в shared не нашлось → опросить **конкретные** другие проекты (юзер указывает имена) на предмет того же знания. Если и там нет — оформить через уже существующий `tasks.create({target_project, body})`.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- **Broadcast по всем проектам.** Спрашиваем только перечисленные явно — экономим токены и сетку, юзер знает кого спрашивать.
|
||||
- **Авто-fallback внутри `knowledge.search`.** `search` остаётся узким (только shared); агент сам решает звать ли `ask_projects`.
|
||||
- **Новый "request-knowledge-prep" механизм.** `tasks.create` уже умеет ставить задачу другому проекту — Оккам.
|
||||
- **Confirm-gate / dry-run.** Оба новых тула read-only.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Tools (новые, read-only)
|
||||
|
||||
#### `knowledge.ask_projects(query, projects[], limit?, types?)`
|
||||
|
||||
Ищет в `.wiki/` указанных репо.
|
||||
|
||||
**Input:**
|
||||
| field | type | required | default |
|
||||
|---|---|---|---|
|
||||
| `query` | string | yes | — |
|
||||
| `projects` | string[] | yes | — |
|
||||
| `limit` | int (1–20) | no | `5` (per project) |
|
||||
| `types` | string[] | no | `["entities","concepts","packages","sources"]` (`raw` исключён по дефолту — тяжёлые dumps; чтобы включить, явно перечисли все 5) |
|
||||
|
||||
**Output:**
|
||||
```json
|
||||
{
|
||||
"asked_projects": ["foo", "bar"],
|
||||
"results": [
|
||||
{
|
||||
"project": "foo",
|
||||
"matches": [
|
||||
{"slug": "concepts/x", "type": "concepts", "title": "...", "snippet": "..."}
|
||||
]
|
||||
},
|
||||
{"project": "bar", "error": "no wiki — repo has no .wiki/index.md"}
|
||||
],
|
||||
"next_action_hint": "knowledge.get_from(project, slug) для полного текста; tasks.create(target_project, body) если знание не оформлено"
|
||||
}
|
||||
```
|
||||
|
||||
#### `knowledge.get_from(project, slug)`
|
||||
|
||||
Полный текст одной страницы из конкретного проекта (live через Gitea API).
|
||||
|
||||
**Input:**
|
||||
| field | type | required |
|
||||
|---|---|---|
|
||||
| `project` | string | yes |
|
||||
| `slug` | string | yes — относительный путь под `.wiki/` без `.md`, включает type-префикс (например `concepts/foo` или `entities/bar`) |
|
||||
|
||||
**Output:** raw markdown body, либо `{error}` если не найдено.
|
||||
|
||||
### Sync extension
|
||||
|
||||
Расширяем `runSync` в [src/lib/sync-runner.ts](../../src/lib/sync-runner.ts):
|
||||
- В per-repo worker добавляется параллельный `getRawFile(user, repo, '.wiki/index.md', branch)` рядом с уже существующим `.tasks/STATUS.md`.
|
||||
- Если оба `null` → skip как и раньше. Если есть хоть один — пишем `ProjectStatus`.
|
||||
|
||||
### Cache extension
|
||||
|
||||
В [src/lib/cache.ts](../../src/lib/cache.ts):
|
||||
|
||||
```ts
|
||||
interface ProjectStatus {
|
||||
// ...existing fields
|
||||
wiki_index?: string; // NEW — raw .wiki/index.md, undefined если нет
|
||||
}
|
||||
```
|
||||
|
||||
Один общий `cache.json`, не плодим параллельный кэш.
|
||||
|
||||
### Index parser
|
||||
|
||||
Новый `src/lib/wiki-index-parser.ts`:
|
||||
- Парсит canonical `index.md` (формат из [src/lib/wiki-writer.ts](../../src/lib/wiki-writer.ts) `freshIndexMd()` / `insertIndexEntry()`).
|
||||
- Возвращает `Array<{slug: string; type: WikiPageType; title: string}>`.
|
||||
- Структура — секции по типам, под каждой list `- [title](type/slug.md)`.
|
||||
|
||||
### Read flow `ask_projects`
|
||||
|
||||
1. `readCache(cacheFile)` → найти проекты из `projects[]` параметра по `name`.
|
||||
2. Для каждого:
|
||||
- `wiki_index === undefined` → `{project, error: "no wiki — ..."}`
|
||||
- Иначе: парсим index, фильтр по `types`, substring case-insensitive по `slug` + `title` против `query`.
|
||||
- Берём первые `limit` совпадений.
|
||||
- Для каждого — `backend.getRawFile(user, project, '.wiki/' + type + '/' + slug + '.md', branch)` (concurrency-pool).
|
||||
- Из body вырезаем snippet (~80 char вокруг матча, как в `knowledge.search`).
|
||||
3. Если проекта нет в кэше → `{project, error: "unknown project — run sync first"}`.
|
||||
4. Если live fetch упал → matched entry с `snippet: "(fetch failed)"`, slug+title сохраняем.
|
||||
|
||||
### Read flow `get_from`
|
||||
|
||||
1. `readCache` → найти project → `default_branch`.
|
||||
2. `backend.getRawFile(user, project, '.wiki/' + slug + '.md', branch)` — slug включает type-префикс (тот же формат что отдаёт `ask_projects` в `matches[].slug`).
|
||||
3. `null` → error `page not found: <project>/.wiki/<slug>.md`.
|
||||
|
||||
## Matching semantics
|
||||
|
||||
Реплицируем `knowledge.search`:
|
||||
- Substring case-insensitive
|
||||
- Сначала по `slug` + `title` (на index)
|
||||
- Потом по `body` (для snippet) при fetched-странице
|
||||
- `raw` тип исключён по дефолту (тяжёлые dumps); включается явным перечислением — `types: ["concepts","raw"]` или все 5 типов
|
||||
|
||||
## Error branches
|
||||
|
||||
| Случай | Поведение |
|
||||
|---|---|
|
||||
| Index.md отсутствует в репо | `{project, error: "no wiki"}` — тихо, остальные проекты обрабатываются |
|
||||
| Project не в `cache.projects[]` | `{project, error: "unknown project — run sync first"}` |
|
||||
| Live fetch страницы упал | matched entry с `snippet: "(fetch failed)"` |
|
||||
| Парсинг index.md упал | `{project, error: "wiki index malformed: <reason>"}` |
|
||||
| `backend` не сконфигурирован (auth.toml нет) | tool возвращает error — как сейчас у write-tools |
|
||||
|
||||
## Configuration
|
||||
|
||||
Новых конфиг-полей нет. Целевые проекты передаются параметром `projects[]`. Sync уже знает все репы юзера через `listUserRepos`.
|
||||
|
||||
## Testing
|
||||
|
||||
Все новые тесты — vitest, stub Backend. Никаких реальных Gitea-вызовов.
|
||||
|
||||
| Файл | Что покрывает |
|
||||
|---|---|
|
||||
| `tests/lib/wiki-index-parser.test.ts` | Валидный index.md, пустые секции, поломанный markdown, nested slugs (`concepts/foo/bar`) |
|
||||
| `tests/lib/sync-runner.test.ts` (расширение) | `wiki_index` подтягивается параллельно с STATUS.md; репо только с `.wiki/` без `.tasks/` попадает в кэш; ни того ни другого → skip |
|
||||
| `tests/tools/knowledge-ask-projects.test.ts` | Match по slug+title, default types-фильтр (raw исключён), limit per project, error-branches (`no wiki`, `unknown project`, fetch fail, malformed index), partial success (один проект упал, другие отдали) |
|
||||
| `tests/tools/knowledge-get-from.test.ts` | Happy path, project not found, page not found |
|
||||
|
||||
## Files
|
||||
|
||||
**New:**
|
||||
- `src/lib/wiki-index-parser.ts`
|
||||
- `tests/lib/wiki-index-parser.test.ts`
|
||||
- `tests/tools/knowledge-ask-projects.test.ts`
|
||||
- `tests/tools/knowledge-get-from.test.ts`
|
||||
|
||||
**Modify:**
|
||||
- `src/lib/cache.ts` — `wiki_index?: string` в `ProjectStatus`
|
||||
- `src/lib/sync-runner.ts` — параллельный `getRawFile` для `.wiki/index.md`
|
||||
- `src/tools/knowledge.ts` — 2 новых тула, описания включают цепочку с `tasks.create` для случая "знания нет"
|
||||
- `src/server.ts` — проверить что `cacheFile` + `backend` пробрасываются в `makeKnowledgeTools` (уже частично есть)
|
||||
- `tests/lib/sync-runner.test.ts` — расширить existing
|
||||
|
||||
## Implementation phases
|
||||
|
||||
1. **Parser** — `wiki-index-parser.ts` + тест.
|
||||
2. **Cache + sync** — поле `wiki_index`, расширенный `runSync`, тесты.
|
||||
3. **`ask_projects`** — тул + тест.
|
||||
4. **`get_from`** — тул + тест.
|
||||
5. **Wiring** — `server.ts` deps + `description` тулов с hint про `tasks.create`.
|
||||
6. **Smoke** — `npm run build`, реальный sync, ручной вызов из новой Claude-сессии.
|
||||
|
||||
## Out of scope (для будущих итераций)
|
||||
|
||||
- Авто-suggest проектов в response от `knowledge.search` (heuristic / тематический mapping).
|
||||
- Federated write — пробрасывать ingest сразу в чужой репо (можно уже сейчас через `knowledge.ingest({target_project: "foo", ...})` — есть в MVP-4).
|
||||
- Кэширование тел страниц (сейчас кэшируем только index — это компромисс размер/свежесть).
|
||||
Reference in New Issue
Block a user