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>
9.5 KiB
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:
{
"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:
- В per-repo worker добавляется параллельный
getRawFile(user, repo, '.wiki/index.md', branch)рядом с уже существующим.tasks/STATUS.md. - Если оба
null→ skip как и раньше. Если есть хоть один — пишемProjectStatus.
Cache extension
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.tsfreshIndexMd()/insertIndexEntry()). - Возвращает
Array<{slug: string; type: WikiPageType; title: string}>. - Структура — секции по типам, под каждой list
- [title](type/slug.md).
Read flow ask_projects
readCache(cacheFile)→ найти проекты изprojects[]параметра поname.- Для каждого:
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).
- Если проекта нет в кэше →
{project, error: "unknown project — run sync first"}. - Если live fetch упал → matched entry с
snippet: "(fetch failed)", slug+title сохраняем.
Read flow get_from
readCache→ найти project →default_branch.backend.getRawFile(user, project, '.wiki/' + slug + '.md', branch)— slug включает type-префикс (тот же формат что отдаётask_projectsвmatches[].slug).null→ errorpage 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.tstests/lib/wiki-index-parser.test.tstests/tools/knowledge-ask-projects.test.tstests/tools/knowledge-get-from.test.ts
Modify:
src/lib/cache.ts—wiki_index?: stringвProjectStatussrc/lib/sync-runner.ts— параллельныйgetRawFileдля.wiki/index.mdsrc/tools/knowledge.ts— 2 новых тула, описания включают цепочку сtasks.createдля случая "знания нет"src/server.ts— проверить чтоcacheFile+backendпробрасываются вmakeKnowledgeTools(уже частично есть)tests/lib/sync-runner.test.ts— расширить existing
Implementation phases
- Parser —
wiki-index-parser.ts+ тест. - Cache + sync — поле
wiki_index, расширенныйrunSync, тесты. ask_projects— тул + тест.get_from— тул + тест.- Wiring —
server.tsdeps +descriptionтулов с hint проtasks.create. - 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 — это компромисс размер/свежесть).