mcp-memorybank
Gerencia e navega uma base de conhecimento memory_bank: gravações controladas, roteamento sem leitura e desvio em relação ao código.
Documentação
mcp-memorybank
Um servidor MCP para uma base de conhecimento memory_bank. Ele a cria do zero, controla cada escrita
de acordo com as regras que a própria base declara, roteia para o documento certo sem ler a base,
e reporta onde o código avançou sem os documentos.
Ele não escreve nada por iniciativa própria. Cada documento chega por uma chamada deliberada que carrega um propósito, uma dependência upstream e uma entrada de índice; o que o hook de fim de sessão coleta cai em uma quarentena que apenas um humano esvazia.
Especificação — specification.md, plano — implementation-plan.md, do que uma base semeada é feita e onde ela se estica — architecture.md.
Tudo no plano está feito: E0 índice, E1 leituras, E2 validação, E3 grafo, E4 busca e delta, E5 escritas.
No que ele é comprovadamente bom, e no que não é
Contado em seis bases em uso real, analisando as sessões em vez de por impressão:
writing into the bank, through the server 985 calls
reading the bank, through the server 187 calls
reading the bank, around the server 1755 calls (cat, grep, Read)
bank_route specifically 33 calls
A metade de escrita funciona. bank_create, bank_edit e bank_update_section são usados em toda
base, com ou sem instrução para isso, porque escrever um documento governado à mão é
mais difícil do que chamar a ferramenta: seria preciso reproduzir o frontmatter, resolver o
derived_from e registrar-se em um índice de seção, e falharia na validação se errasse qualquer
parte disso. Noventa por cento de todas as chamadas ao servidor são escritas.
A metade de leitura é em grande parte ignorada. Nove em cada dez vezes um agente abre a base com cat
e grep em vez de perguntar ao bank_route quais documentos importam. Isso não é uma afirmação de que o roteamento
responde mal — ninguém mediu isso. É uma afirmação de que ele não é consultado, porque alcançar
o shell é o que um modelo faz em todo repositório e nada aqui supera o hábito.
Duas coisas se seguem, e nenhuma é confortável. O roteamento é o recurso que este README lidera e o que a aritmética de tokens abaixo sustenta, e é o menos usado. Editar documentos existentes foi escrito na especificação como algo que o servidor não faria — veja §8 — e acabou sendo a metade que carrega o projeto.
A única alavanca com evidência por trás é o parágrafo sob Dizendo ao agente para usá-lo: as quatro bases que o carregam roteiam oito vezes mais do que as duas que não o fazem, 32% das leituras contra 4%. Isso é uma correlação em seis projetos, e está sendo testada adequadamente — veja A-06 em backlog.md.
Como funciona
Não há banco de dados, nem daemon, nem configuração. Um diretório de arquivos markdown é todo o estado, e o servidor é um leitor dele que por acaso fala MCP. Tudo o que ele sabe ele re-deriva dos arquivos; delete seu processo e nada se perde, porque nada foi mantido em outro lugar.
O índice, e por que não há watcher
Na inicialização, o servidor percorre a base uma vez, e em cada chamada ele a percorre novamente: readdir, stat,
e um parse apenas para os arquivos cujo mtime mudou. Uma construção a frio de uma base de 368 documentos leva cerca de
85 ms; a varredura antes de uma chamada inalterada custa 11–13 ms.
É por isso que não há watcher de arquivos. Um watcher economizaria esses milissegundos e compraria uma classe de erro em troca — um índice que divergiu silenciosamente do disco, em uma ferramenta cujo trabalho inteiro é ser confiável sobre o que o disco diz. A verificação barata vence em ambos os aspectos.
O que um documento se torna
Cada arquivo é analisado uma vez em um registro: os campos do frontmatter, os cabeçalhos de seção de segundo nível
com seus intervalos de linha, tamanho em bytes, mtime — e uma coisa que o arquivo não contém, uma camada
calculada a partir do caminho (dna, knowledge, decision, delivery, flow, inbox, other).
O frontmatter passa por gray-matter em exatamente um lugar. Quando o YAML é inválido — um dois-pontos sem aspas
em purpose é o caso comum — o documento não cai fora do índice: seus metadados são
recuperados linha por linha e o erro de parse é mantido para bank_validate reportar. Um documento que
desaparece do roteamento por causa de um erro de digitação é pior do que um que classifica mal.
O contrato vem da base
doc_kind, doc_function, status, se derived_from é obrigatório, qual documento é a
raiz declarada — tudo isso é lido do próprio dna/frontmatter.md e dna/governance.md da base
no momento da atualização. Nada é codificado, e os conjuntos são abertos.
Isso não é polidez: as bases discordam sobre seu próprio vocabulário, e uma enumeração fixa rejeita uma
minoria considerável de documentos reais apenas pelo doc_kind. Uma base sem dna/ ainda funciona: o servidor roda em modo degradado — leitura,
roteamento, busca e as regras estruturais — e diz isso em vez de impor um contrato que ninguém
declarou.
Roteamento: classificando o cabeçalho, nunca a prosa
bank_route responde "o que devo ler sobre isso", e lê apenas o que um humano escreveu à mão
sobre cada documento — nunca o corpo. Quatro campos, com pesos fixos:
| Campo | Peso | Pontuado como |
|---|---|---|
canonical_for | 5 | quanto de uma chave de fato a pergunta cobre, não se uma palavra dela correspondeu |
purpose | 3 | sobreposição de palavras |
title | 2 | sobreposição de palavras |
| cabeçalhos de seção | 1 | o cabeçalho de melhor correspondência, que também é retornado para que a resposta possa ser lida com escopo de seção |
A pontuação bruta é então multiplicada, e os multiplicadores são onde a classificação realmente obtém seu julgamento:
- Camada.
knowledge×1.5,decision×1.2,dna/flow/other×1.0,delivery×0.6,inbox×0.2. A entrega é amortecida porque em uma base que está em uso há um tempo, ela é a maioria dos documentos; sem isso, uma pergunta sobre uma regra retorna os recursos fechados que a mencionam. - Status.
active×1,draft×0.7,archived×0.2. - Trabalho fechado.
delivery_status: doneoucancelledreduz a pontuação pela metade novamente. Um recurso concluído é história, não uma resposta. - Intenção. Uma pergunta contendo por que, justificativa, em vez de, почему, вместо re-pondera
toda a execução em direção a decisões:
decision×1.6,knowledge×1.2. "Por que X" e "o que é X" são perguntas diferentes e não devem retornar o mesmo documento primeiro.
Modelos nunca aparecem nos resultados: eles são estruturalmente idênticos a documentos reais e inundariam toda lista.
Busca: um índice diferente para uma pergunta diferente
bank_search não é um fallback para roteamento, ele responde a outra metade. O roteamento classifica o
cabeçalho escrito à mão ("qual documento é sobre isso"); a busca lê a prosa ("onde essa string
realmente aparece"). Identificadores, mensagens de erro e literais vivem apenas nos corpos.
Ele constrói um índice invertido sobre os corpos dos documentos, incrementalmente na mesma verificação de mtime. Uma consulta
corresponde em três níveis por confiança — a palavra como digitada pesa 10, seu equivalente no outro
idioma 6, um fragmento de um token composto 1 — e documentos que correspondem a todos os conceitos são classificados
antes de documentos que correspondem a alguns. O nível de fragmento é o que mantém FT-042 de elevar o
registro features/README.md, com suas setenta linhas de FT-*, acima do próprio recurso.
Escrita: uma operação, ou nenhuma
bank_create escreve o documento, preenche o frontmatter que o contrato pede e o registra no
índice de seção na mesma chamada — para que a etapa de registro não possa ser esquecida, que é a
maneira mais comum de uma base apodrecer. O registro copia a forma da última entrada naquele índice,
linha de tabela ou marcador ou item numerado, para que um arquivo escrito à mão não seja reformatado.
Antes de tudo isso, ele recusa, com o motivo nomeado: o caminho está ocupado, derived_from não
resolve, canonical_for já é propriedade de outro documento, o caminho sai da raiz da base, ou
não termina em .md. Uma recusa não escreve nada — sem arquivo parcial, sem linha de índice órfã.
Duas regras seguem dessas portas, e são a razão pela qual as escritas valem a pena: um conflito SSoT e uma aresta quebrada não podem entrar na base através deste servidor. Elas só podem chegar editando um arquivo pelas costas dele.
O grafo
bank_graph percorre derived_from em largura com um teto de nós, para que um documento hub não arraste
toda a base e um ciclo não faça loop. Cada aresta cai em um de três resultados: interno (um nó),
external (sai da raiz da base — em um monorepo, as bases se aninham), ou broken. up é o que um
documento é construído; down é o raio de explosão de mudá-lo.
Uma aresta externa é seguida exatamente um salto: o frontmatter do alvo é lido e retornado sob
neighbours, e nada mais sobre ele é. Não é indexado, não é validado, não é pesquisável, e suas próprias
arestas não são percorridas. A fronteira permanece onde estava — mas em um monorepo essas arestas carregam decisões
reais, e um raio de explosão que termina silenciosamente na parede do repositório é errado em vez de parcial.
Passe neighbours: false para pular as leituras.
Deriva
bank_drift responde à pergunta que a validação não pode: não "algo está faltando", mas "o que
escrevemos ficou desatualizado". Um documento que descreve código o lista em anchors:, e o servidor compara o
último commit que tocou o documento contra o último commit que tocou o código. Onde o código
está à frente por mais do que o limite, ele diz isso — e onde uma âncora aponta para um caminho que não existe
mais, ele sempre diz isso, porque isso é o código se movendo para fora de baixo do documento.
Ele nunca adivinha qual documento é dono de qual arquivo. O pareamento é anotado à mão ou não existe, o que significa que uma base que não foi anotada recebe uma resposta vazia honesta e uma nota dizendo por quê. E ele reporta sem editar: se uma lacuna de seis meses importa não é um julgamento que um timestamp pode fazer.
O que uma base semeada contém
bank_init escreve 24 arquivos: o conjunto de governança em dna/, um índice raiz, oito registros de seção,
os quatro documentos de fluxo, um ponteiro onde os modelos estariam e dois rascunhos para preencher. Mais três seções — epics, prd, prompts — são construídas
na primeira vez que um documento precisa de uma, incluindo índice e entrada de índice raiz, em vez de ficarem vazias
desde o início.
Os modelos de documento não são copiados. Eles acompanham o servidor e bank_create os lê de
lá, para que uma base se comporte exatamente como se os tivesse; --materialize-flows os copia para um
projeto que pretende mudar um, e a partir de então as cópias da própria base vencem. Os fluxos de prosa são
copiados, porque as pessoas os leem e o roteamento responde com eles.
dna/ também é copiado, e esse não é negociável: é a lei pela qual a base é julgada, e o
design depende dele viajar com o corpus. A camada de cada diretório de nível superior e o peso de classificação
são declarados lá também — uma base que renomeia uma seção diz isso nessa tabela e mantém seu
peso, e uma base que não declara nada recebe o mapa embutido.
O que ele não fará
Ele não cria um documento por edição — cada novo documento passa por bank_create, então nada
entra na base sem uma verificação de caminho, um derived_from que resolve e uma entrada de índice. Ele não
promove nada para fora de _inbox/ por conta própria. Ele não inventa um esquema, e não impõe uma
regra que a base não declarou. Ele não exclui um documento fora de _inbox/. E ele não
indexa, valida ou busca nada fora da raiz da base — o único salto que bank_graph dá para uma
base vizinha lê um cabeçalho e para ali.
O que custa em tokens
Wiring a server in is not free, and the cost is paid in two different currencies. All figures below were measured on the shipped build, counting characters of the actual JSON-RPC payloads and converting at 3.5 characters per token — dense JSON runs closer to that than the 4 that suits prose.
The fixed half is paid on every request, whether a tool is called or not, because the definitions live in the context:
| characters | ~tokens | |
|---|---|---|
| server instructions | 343 | 98 |
| 15 tool definitions | 20,473 | 5,849 |
| 5 prompts | 1,357 | 388 |
| 4 resources | 913 | 261 |
| total, per request | 23,086 | ~6,600 |
bank_create é a definição mais cara, com ~715 tokens — sete parâmetros e uma descrição
que precisa explicar o portão. bank_changed é a mais barata, com ~183.
Essa tabela é o que um projeto com um banco paga. Um projeto que não tem nenhum recebe apenas
bank_init e paga ~450; um que está desligado — por --off ou um arquivo .memorybank-off — paga ~25 por uma superfície vazia. Veja
Instalando uma vez, para cada projeto — a razão pela qual o
servidor se dá ao trabalho de fazer essa distinção é que uma instalação global cobraria a taxa
cheia em todos os projetos da máquina, incluindo todos os projetos que nunca terão um banco.
A metade variável é onde ele recupera a metade fixa. Em um banco com 27 documentos:
| characters | ~tokens | |
|---|---|---|
| lendo o banco inteiro | 91,319 | 26,091 |
uma resposta bank_route, 3 resultados | 839 | 240 |
| os três documentos que ela nomeou | 8,386 | 2,396 |
Roteando respostas em 240 tokens, o que ler o corpus custa 26,091 — e um décimo do que custa ler
até mesmo os três documentos corretos, porque retorna caminhos, títulos, purpose e uma linha de
raciocínio em vez de qualquer prosa. O que ler depois é então uma decisão tomada com base em evidências.
bank_read é limitado separadamente: 40.000 bytes por lote, aproximadamente 11.000 tokens. Peça vinte
documentos e você recebe o que cabe mais uma lista do que o orçamento não alcançou, dividido por
seção, para que uma única chamada não inunde o contexto.
Onde está o ponto de equilíbrio. Os ~6.600 são pagos na primeira vez que o servidor evita uma leitura desnecessária, e isso acontece imediatamente em qualquer banco grande o suficiente para importar. Medido em quatro bancos da mesma linhagem:
27 documents ~26k tokens to read whole
78 documents ~87k
96 documents ~158k
112 documents ~305k
No topo dessa faixa, o corpus tem duas janelas de contexto completas e lê-lo não é uma opção a qualquer preço, enquanto uma resposta roteada permanece na casa das centenas baixas de tokens.
A conclusão honesta é que em um banco pequeno o servidor perde. Enquanto o corpus ainda cabe em uma
janela de contexto, cat é mais barato do que uma cobrança fixa de 6.600 tokens, e o teste de campo mediu
exatamente isso: em um banco pequeno o suficiente para ser lido inteiro, o roteamento compete com "já está no contexto" e
perde. O servidor ganha seu custo em corpora que cresceram além de serem lidos.
E tokens não são a principal coisa sendo comprada. Metade das ferramentas não economiza nada — bank_create,
bank_validate e bank_drift apenas gastam. Elas existem para que o banco mantenha sua forma, o que nenhuma quantidade
de orçamento de contexto fará sozinho. A economia no roteamento é um efeito colateral, não o ponto.
Executando
Conectá-lo a um projeto — <project>/.mcp.json:
{
"mcpServers": {
"memorybank": {
"command": "npx",
"args": ["-y", "@maxweb4u/mcp-memorybank", "--root", "./memory_bank"]
}
}
}
Fixe a versão assim que depender dela — @maxweb4u/mcp-memorybank@0.2.2 — para que o servidor não
mude de forma sob um projeto que você não está olhando.
O pacote tem escopo porque a verificação de similaridade do npm não aceitará mcp-memorybank sem escopo: ele
normaliza a pontuação, o que o torna indistinguível do não relacionado mcp-memory-bank
já no registro. O comando que o pacote instala ainda é mcp-memorybank.
Se o projeto ainda não tem banco, crie um primeiro. O comando escreve o esqueleto, o conjunto de governança e um índice registrado por seção, e um banco criado dessa forma valida limpo por construção:
npx @maxweb4u/mcp-memorybank --root ./memory_bank --init "Project Name"
Requer Node 22. O hook descrito em hooks/README.md também quer jq e git.
Instalando uma vez, para cada projeto
Um .mcp.json por projeto é explícito e viaja com o repositório, que é por isso que tem a forma
acima. A alternativa é uma entrada para cada projeto na máquina:
claude mcp add --scope user memorybank -- npx -y @maxweb4u/mcp-memorybank --root ./memory_bank
--root ./memory_bank resolve em relação ao diretório do projeto, então uma entrada atende a todos os projetos
que têm um banco.
O problema é que um servidor com escopo de usuário não tem um interruptor de desligamento por projeto no lado do cliente —
disabledMcpjsonServers governa entradas .mcp.json e nada mais. Então o servidor decide por
si mesmo quanta superfície um projeto recebe:
| O projeto | O que ele vê | Custo por requisição |
|---|---|---|
| tem um banco | tudo | ~6.600 tokens |
| não tem banco | bank_init, e nada mais | ~450 tokens |
é nomeado por --off, ou tem um arquivo .memorybank-off ao lado | nada | ~25 tokens |
A linha do meio é a que torna uma instalação global razoável: um projeto que nunca teve um
banco paga cerca de 7% da superfície completa, e o que é oferecido — bank_init — é a única chamada
que teria feito sentido ali de qualquer forma. Nada precisa ser configurado para isso; é o que o servidor
faz quando a raiz não contém documentos.
Há duas maneiras de dizer "não aqui", e elas diferem em quem é dono da decisão.
--off <path> pertence à máquina. Repetível, e um caminho cobre tudo abaixo dele:
claude mcp add --scope user memorybank -- npx -y @maxweb4u/mcp-memorybank \
--root ./memory_bank --off /path/to/a/repo/you/do/not/govern
Nada é escrito no projeto que ele nomeia — o caminho vive na própria configuração do cliente — então não há arquivo para aparecer no git status desse repositório e nada para um colega encontrar. Isso é o que o torna a forma certa para um repositório comercial em que a equipe de outra pessoa trabalha, e para um projeto com vários bancos em profundidades diferentes: um caminho cobre todos.
.memorybank-off pertence ao projeto. Um arquivo vazio no diretório do projeto, ao lado do
banco em vez de dentro dele — manter o servidor fora é a decisão do projeto, não do banco:
touch .memorybank-off
Ele viaja com o repositório, que é o ponto quando o próprio projeto quer dizer não, e a razão para não recorrer a ele quando você preferiria que ninguém soubesse.
A superfície é decidida uma vez, quando a sessão começa. A exceção é bank_init: crie um banco em um
projeto que não tinha nenhum e o resto das ferramentas aparece imediatamente, sem reiniciar a sessão.
Instalando o comando, conectando por projeto
A terceira forma, e a preferida quando várias pessoas ou máquinas compartilham os repositórios. Instale uma vez:
npm i -g @maxweb4u/mcp-memorybank
Então cada projeto que quer o servidor carrega um .mcp.json de seis linhas nomeando o comando em vez
de um caminho:
{
"mcpServers": {
"memorybank": {
"command": "mcp-memorybank",
"args": ["--root", "./memory_bank"]
}
}
}
Este é o mesmo arquivo do topo desta seção com npx -y @maxweb4u/mcp-memorybank substituído
pelo comando instalado. O que isso compra:
por projeto, via npx | escopo de usuário, em todos os lugares | comando instalado, por projeto | |
|---|---|---|---|
| um novo projeto com um banco | um arquivo | funciona sozinho | um arquivo |
| um projeto sem banco | ~450 tokens | ~450 tokens | nada |
| um repositório para ficar fora | .memorybank-off | --off <path> | não adicione arquivo |
| custo de inicialização | ~0,6 s de npx | ~0,6 s de npx | ~0,1 s |
| um caminho absoluto no git | não | não | não |
| funciona na máquina de um colega | sim | não | sim, após npm i -g |
| versão | fixada no arquivo | fixada no arquivo | o que estiver instalado |
A terceira coluna é a única onde um projeto que nunca terá um banco não custa nada, e
onde ficar fora de um repositório não precisa de configuração em lugar nenhum — você simplesmente não adiciona o arquivo.
Isso paga com um passo manual por projeto, e com a versão sendo o que npm i -g colocou
lá por último, em vez de um número escrito.
O hook quer a mesma instalação, por uma razão diferente — veja hooks/README.md.
A partir de um clone
Para trabalhar no próprio servidor, ou para apontar vários projetos para um build:
npm install && npm run build
node dist/cli.js --root /path/to/project/memory_bank
Um .mcp.json escrito dessa forma precisa de um caminho absoluto para dist/cli.js, o que amarra o projeto a
um checkout em uma máquina. Tudo bem durante o desenvolvimento, errado para qualquer coisa que você pretende manter.
npx não funciona dentro deste repositório. Do próprio checkout do servidor,
npx @maxweb4u/mcp-memorybank falha com sh: mcp-memorybank: command not found: o npx vê que o
nome solicitado corresponde ao package.json local, decide que o pacote já está presente, e procura
pelo binário no node_modules/.bin local, onde nada o vincula. Nada está errado com o
pacote publicado — execute o mesmo comando de qualquer outro diretório e funciona. Então, quando você quiser
verificar o que os usuários realmente recebem, faça isso de um diretório temporário, não daqui.
Publicar uma nova versão tem uma ordem que importa — o que é verificado no tarball em vez da árvore de trabalho, e quais pins se movem antes da publicação e quais apenas depois. Está escrito em releasing.md.
Dizendo ao agente para usá-lo
Conectar o servidor torna as ferramentas disponíveis; não faz um agente alcançá-las. Deixado por
conta própria, um modelo frequentemente abrirá README.md e percorrerá os índices de seção, porque é o que faz
em todos os outros lugares — que é exatamente a caminhada que este servidor existe para remover.
Um parágrafo no CLAUDE.md do projeto resolve isso:
# Memory bank
This project is served by the `memorybank` MCP server. **Its tools all begin with `bank_`; load them
at the start of a session and look at what is there** — the set grows, and anything not named here
still exists. Working the bank through them instead of through `sed`, `cat` and `grep` is the point:
they keep the frontmatter, the section indexes and the ownership rules intact, which a shell cannot.
- **Finding.** `bank_route` answers "which document covers this"; `bank_search` finds a literal
across the bank; `bank_read` takes a path and a section, so a long document costs one section.
Reach for those before grepping `memory_bank/`.
- **Changing.** `bank_edit` replaces one exact fragment — a backlog row, a table cell, a heading.
`bank_update_section` writes a whole named section. `bank_set_status` moves a document between
`draft`, `active` and `archived`, and releases its `canonical_for` when it retires.
- **Creating.** `bank_create` writes the frontmatter the contract asks for and registers the document
in its section index in the same operation. A document written by hand is reachable from no index.
- **Checking.** Run `bank_validate` before committing anything under `memory_bank/`, and say what it
found. Never promote a note out of `_inbox/` on your own — that is a review step for the owner.
Esta redação não é uma questão de gosto; foi medida. Sem tal parágrafo, o mecanismo simplesmente
fica lá — nove horas de uma sessão de trabalho com bank_route carregado e nunca chamado. Uma versão
anterior do parágrafo nomeava cinco ferramentas explicitamente e obteve exatamente cinco ferramentas usadas: as que
nomeava, e nada mais. Nomear o prefixo e dizer para ir olhar, depois agrupar por tarefa em vez de
recitar nomes, trouxe todas as ferramentas que a reescrita mencionou recentemente:
baseline five named rewritten
writes into the bank, shell 20 0 0
bank_edit 0 19 14
bank_route / bank_search 0/0 0/0 2/2
shell searches of the bank 18 11 8
bank_validate findings 3→5 6 0
O controle torna a razão legível. Um projeto sem nenhum CLAUDE.md ainda encontrou a família
de ferramentas por conta própria — bank_edit trinta vezes, bank_create dezoito — e ainda fez sete escritas
de shell em seu próprio banco, cada uma moldada exatamente como as ferramentas que já estava usando. O
servidor é o que faz um banco se manter; instruções não podem verificar nada. O parágrafo é o que torna a
escolha consistente. Nenhum substitui o outro — veja field-test.md.
Depurando a partir do terminal
O CLI é um subconjunto principalmente de leitura para olhar um banco sem um agente no loop, não um espelho da
superfície de ferramentas: bank_drift, bank_edit, bank_update_section, bank_set_status e
bank_discard são apenas MCP. Instalado do npm, o comando é mcp-memorybank; de um clone, é
node dist/cli.js.
mcp-memorybank --root <bank> --stats
mcp-memorybank --root <bank> --route "filter thresholds" --limit 5
mcp-memorybank --root <bank> --read domain/rules.md --section Thresholds
mcp-memorybank --root <bank> --validate --summary
mcp-memorybank --root <bank> --validate --rule broken-derived-from
mcp-memorybank --root <bank> --graph domain/rules.md --direction down --depth 1
mcp-memorybank --root <bank> --search "FT-042" --limit 5
mcp-memorybank --root <bank> --changed HEAD~5
mcp-memorybank --root <bank> --create adr/ADR-...-name.md --kind adr --title "..." --purpose "..." --derived ../engineering/architecture.md --dry-run
mcp-memorybank --root <new-path> --init "Project Name" --dry-run
mcp-memorybank --root <bank> --materialize-flows
mcp-memorybank --root <bank> --list-inbox
mcp-memorybank --root <bank> --promote _inbox/note.md --to engineering/thing.md --derived ../dna/principles.md --dry-run
O que existe
| Ferramenta | O que ela faz |
|---|---|
bank_route | "o que devo ler sobre isso" — ranqueia por canonical_for, purpose, title, títulos de seção |
bank_read | um documento inteiro, uma seção de segundo nível, ou vários documentos em uma única chamada, sob um orçamento de bytes |
bank_validate | verifica o banco contra as regras que o próprio banco declara em dna/ |
bank_graph | percorre derived_from: down — de quem isso depende (raio de impacto), up — sobre o que isso é construído |
bank_drift | compara cada documento com o código que ele declara em anchors: e relata onde o código está à frente |
bank_search | busca em texto completo nos corpos dos documentos — identificadores, nomes, literais |
bank_changed | o que mudou desde uma referência git ou uma data ISO; uma referência mais antiga que o repositório cai no primeiro commit, e um banco sem repositório algum responde com tudo |
bank_init | cria um banco do nada: esqueleto, dna/, os fluxos, índices de seção |
bank_materialize_flows | copia os modelos de documento fornecidos para o banco, que então passa a ser dono deles |
bank_create | cria um documento a partir do modelo do próprio banco e o registra no índice, com portões e _inbox |
bank_edit | substitui exatamente um fragmento de um corpo — uma linha de tabela, um passo, um título; recusa uma correspondência ambígua |
bank_update_section | escreve o corpo de uma seção nomeada de um documento existente, deixando todo o resto intacto |
bank_set_status | move um documento para outro status de ciclo de vida, executando os portões que protegem a ativação |
bank_promote | move uma nota para fora de _inbox/ para uma camada canônica, registrando-a e excluindo a origem |
bank_discard | descarta uma nota em quarentena, de forma registrada — apenas _inbox/, e um motivo é obrigatório |
| Recurso | Conteúdo |
|---|---|
memorybank://index | índice anotado de todos os documentos (modelos excluídos) |
memorybank://schema/frontmatter | o próprio dna/frontmatter.md do banco, na íntegra |
memorybank://health | uma execução nova de bank_validate, agrupada por regra |
memorybank://inbox | o que está em quarentena, do mais antigo ao mais recente |
Prompts: session-start (o que é o projeto, o que está em andamento, o que está em aberto — a partir do índice e
do delta, não lendo tudo), route-then-read (rotear primeiro, ler depois),
check-before-commit (executar validação e explicar cada achado), record-adr (montar uma decisão
em um ADR), review-inbox (trabalhar a quarentena).
Dois deles existem por causa do que uma sessão de trabalho realmente fez, e não do que o design
esperava. Numa tarde, um agente chamou bank_read zero vezes e cat em um loop de shell 85
vezes: dez documentos eram dez chamadas de ferramenta de um jeito e uma chamada do outro, então o escopo de seção — o
recurso que mantém documentos grandes fora da janela de contexto — perdeu na aritmética antes de ser
considerado. Daí um bank_read que aceita uma lista. E a primeira pergunta de toda sessão era
"o que é isso, onde paramos, o que está em aberto", o que o roteamento não responde; sem um lugar para
enviá-la, o agente lia o banco inteiro. Daí session-start.
Criando um banco
mcp-memorybank --root <new-path> --init "Project Name"
Escreve 24 arquivos: dna/ (8 documentos de governança, incluindo a tabela de camadas), os quatro fluxos
documentos, um índice para cada uma das oito seções semeadas mais o índice raiz, um ponteiro onde os
modelos estariam, e dois rascunhos — product/context.md e engineering/testing-policy.md —
que tanto os modelos quanto o portão de fechamento de recurso apontam.
Três seções adicionais — epics/, prd/, prompts/ — não são criadas antecipadamente. bank_create
constrói uma, seu índice e sua entrada no índice raiz na primeira vez que um documento precisar dela, e avisa.
O conjunto inicial vive em starter/ e pertence ao banco no momento em que é copiado: reescreva-o
como quiser, o servidor não continua impondo-o. O que não é copiado é
flows/templates/ — veja O que um banco semeado contém.
Um banco recém-criado valida com zero achados — que é exatamente o ponto de bank_init
em vez de copiar o banco de outra pessoa: uma cópia traria os defeitos daquele banco junto.
Modelos e a quarentena
A mecânica de uma escrita está em Como funciona; o que segue é de onde vem o conteúdo e para onde vai uma nota não revisada.
bank_create escreve o corpo que recebe e, na falta disso, o corpo do modelo correspondente. Passe
body em vez de criar o documento e escrever a prosa depois — um documento que
chega apenas com títulos tende a ter seu conteúdo preenchido via shell, fora de todos os portões que o servidor tem.
Três ferramentas alcançam um documento que já existe, e a divisão entre elas é medida em vez de organizada.
bank_update_section escreve o corpo de uma seção de segundo nível nomeada, deixando o frontmatter, o
título e todas as outras seções intactos. Ela existe porque bank_init semeia product/context.md e
engineering/testing-policy.md como rascunhos especificamente para serem preenchidos, enquanto bank_create
recusa um caminho ocupado — corretamente — o que deixou os dois documentos que o servidor pede como os dois que
não conseguia escrever. Uma seção que não existe é recusada com a lista das reais em vez de
ser anexada, então um título digitado errado não pode adicionar uma seção silenciosamente.
Tanto bank_update_section quanto bank_create aceitam seu conteúdo de um arquivo em vez da
chamada — contentFile e bodyFile. Isso existe por um motivo medido: uma sessão que construiu um banco
gerou uma tabela de 610 linhas a partir de dezenove outros documentos com um script e a escreveu no banco
diretamente, contornando todos os portões. Não porque a ferramenta não conseguisse fazer a escrita, mas porque a ferramenta
queria a tabela inteira como um argumento de string, e a tabela nunca tinha estado no contexto do agente
de forma alguma. Conteúdo que foi calculado em vez de composto não deveria precisar ser recitado para ser governado, e
um corpo que não viaja como uma string JSON não pode falhar ao serializar como uma.
bank_edit substitui exatamente um fragmento. Escrita em nível de seção acabou sendo o grão errado para
a maioria das edições: ao longo de uma sessão de trabalho, cinco de onze escritas via shell no banco mudaram algumas linhas
dentro de uma seção de dezenas de linhas — uma linha de tabela, dois passos de um plano, um parágrafo de um
argumento — e uma sexta renomeou um título. Substituir a seção inteira significa reenviar tudo
inalterado ao redor da edição, então um agente recorre a uma substituição de string, toda vez. O contrato
é o que ele já conhece: uma correspondência exata, recusada a menos que ocorra exatamente uma vez, com a contagem
relatada em vez de adivinhar a primeira ocorrência. section estreita a busca quando as mesmas
palavras aparecem duas vezes. O frontmatter está fora de alcance por construção, já que a busca roda no
corpo — uma edição pode renomear um título, mas não pode reescrever silenciosamente canonical_for.
bank_set_status move um documento de um status de ciclo de vida para outro, e existe porque o
frontmatter está fora do alcance das outras duas. Um rascunho que bank_init semeou e
bank_update_section preencheu precisa se tornar ativo em algum momento, e sem uma ferramenta para isso, um agente
recorre a um editor de fluxo no frontmatter — medido, em uma sessão que construiu um banco do
nada. Essa é a pior edição para deixar para um shell: status: active é o portão que exige
derived_from, então a única transição que a governança existe para proteger foi a única transição que a ignorou.
Aqui a verificação roda na entrada, e uma nota em _inbox/ é recusada de imediato, já que a saída
da quarentena é bank_promote, que coloca a nota e define seu status em um único movimento.
Arquivar é a única transição que abre mão de algo. ownerByKey não olha para o status, então um
documento arquivado enquanto ainda declara canonical_for continua bloqueando o sucessor que deveria
possuir a chave — bank_create recusa o sucessor com "já possuído por", nomeando um documento que ninguém
lê mais. Arquivar um proprietário é portanto recusado a menos que releaseCanonical diga isso, e as
chaves são removidas como parte da mesma escrita. Medido, novamente: uma sessão arquivou um documento e
removeu o bloqueio com um regex em python, porque essa era a única maneira de fazer isso.
bank_create pega o modelo do próprio flows/templates/ do banco quando ele tem um, e do
conjunto que o servidor fornece quando não tem. Modelos são
invólucros: o documento a instanciar fica dentro deles como dois blocos sob
## Instantiated Frontmatter e ## Instantiated Body. O servidor desembrulha exatamente esses,
substitui o título, preenche o espaço reservado de data e carrega must_not_define, que o
modelo fornece como governança. Um doc_kind desconhecido é um aviso, não uma recusa, e dryRun mostra
o resultado inteiro — frontmatter, modelo escolhido, linha do índice — sem escrever nada.
inbox: true coloca o documento em _inbox/ com status: draft, sem modelo e sem
registro. Documentos em quarentena estão isentos da regra de unregistered-doc, e bank_route
e bank_search não os retornam de forma alguma — eles são alcançáveis por bank_read, o
recurso memorybank://inbox e bank_promote, e em nenhum outro lugar. Amortecê-los pelo peso da camada foi
tentado primeiro e não é a mesma coisa: em um banco jovem, uma nota chegou ao top três para a pergunta
que respondia, que é exatamente o que uma quarentena deveria prevenir.
bank_promote esvazia a quarentena: o corpo da nota é mantido como escrito, o frontmatter é reconstruído
contra o contrato, o modelo de destino contribui apenas com seus campos de governança, o documento
é registrado no índice e a origem é excluída. Mesmos portões que bank_create, mais dois próprios:
você só pode promover para fora de _inbox/, e apenas para fora. Status após a promoção é active, então
o requisito de governança sobre derived_from não está mais relaxado aqui.
bank_discard é o terceiro resultado, e existe porque a segunda revisão de uma quarentena real
foi para fora do servidor para alcançá-lo. Uma nota pode ser promovida, dobrada no documento que já
possui o fato com bank_update_section, ou descartada — e com apenas os dois primeiros implementados, o
agente dobrou uma nota e depois recorreu a rm em um shell para limpar o que acabara de consumir. Uma
ferramenta que nomeia três resultados e suporta dois envia o terceiro para além de todos os portões que tem. _inbox/
apenas, e um motivo é obrigatório: uma nota vai para o registro ou não vai.
A quarentena é preenchida por um hook Stop — veja hooks/README.md.
Consultas em duas línguas
O banco é escrito em inglês — isso é uma regra de governança e nada aqui a relaxa. A
pergunta é outra questão: digitada à mão, muitas vezes não é inglês, e o ranqueamento lexical sobre
campos em inglês não consegue respondê-la de forma alguma. "где пороги фильтрации" tokeniza em três palavras que
não aparecem em nenhum purpose ou canonical_for, então o resultado é vazio em vez de meramente errado.
bank_route e bank_search portanto tratam uma palavra de consulta como um conceito em vez de uma string. Cada
palavra carrega seus equivalentes na outra língua, extraídos de um dicionário de domínio construído a partir dos
termos mais frequentes em title / purpose / canonical_for em um corpo de bancos reais. Três
coisas se seguem:
- Um conceito pontua uma vez. Três equivalentes em inglês correspondendo a um único
purposeé uma ocorrência, não três, então uma consulta expandida não pode superar uma literal apenas por amplitude. - A palavra realmente digitada vence em empate. Uma tradução é descontada, o que só importa em um banco que mistura idiomas — lá a correspondência literal fica em primeiro lugar.
- O russo flexiona, então uma entrada de dicionário é um prefixo de radical, não uma palavra.
порогcobre пороги / порогов / порогам; um prefixo verbal é removido apenas quando o que resta cai em um radical conhecido, o que é o que permite queзадеплоитьalcancedeployment.
Medido em um banco real, uma pergunta de controle em russo retorna o mesmo primeiro documento que sua contraparte em inglês. A busca é simétrica: cirílico é um caractere de palavra no índice, então um documento citando uma fonte russa é pesquisável, e uma consulta em russo alcança prosa em inglês através do mesmo dicionário.
Uma palavra desconhecida é deixada exatamente como digitada — identificadores como FT-042 e filter_thresholds
nunca chegam perto do dicionário.
O banco pode adicionar suas próprias palavras. O dicionário embutido carrega o assunto do qual foi extraído
— agentes, implantação, frontend, backend. Aponte o servidor para um projeto sobre parsing de livros e
o lado russo fica silencioso em корпус, книга, прогон: não uma resposta ruim, mas uma vazia, enquanto
a mesma pergunta em inglês acerta três de três. O lado inglês nunca tem esse problema
porque é lido do banco; o lado russo não pode ser, já que o banco é em inglês por regra
de governança. Então anote isso, em um dna/vocabulary.md opcional:
| Russian stem | English |
|---|---|
| корпус | corpus, collection |
| книг | book, books, ebook |
| сегмент | segmentation, segmenter, sentence |
A coluna esquerda é um radical — o prefixo mais curto compartilhado por toda forma flexionada — e linhas que
não são duas colunas com um lado esquerdo cirílico são ignoradas, então o arquivo pode carregar prosa e cabeçalhos
ao redor da tabela. Ele é relido a cada atualização, então editá-lo tem efeito sem reiniciar. O
arquivo não é semeado por bank_init: um banco que não precisa de um não deve carregar um vazio.
Regras de validação
| Regra | Severidade | Captura |
|---|---|---|
broken-derived-from | erro | uma aresta cujo alvo não resolve a partir do documento que a declara |
invalid-frontmatter | erro | YAML que o parser rejeita — geralmente dois pontos sem aspas em purpose |
cycle-in-derived-from | erro | A deriva de B deriva de A |
ssot-conflict | erro | dois documentos reivindicando a mesma chave canonical_for |
missing-derived-from | erro | um documento active não raiz sem upstream |
must-not-define-violated | erro | um documento definindo uma chave que declarou que não definiria |
dangling-index-entry | erro | um índice linkando para um arquivo que não existe |
unknown-enum-value | aviso | um doc_kind / doc_function / status fora do que dna/ declara |
unregistered-doc | aviso | um documento ao qual nenhum índice linka — inalcançável por navegação |
unresolved-rule-reference | aviso | prosa citando uma regra por um caminho que não resolve |
unknown-layer-value | aviso | a tabela de camadas em dna/ nomeia uma camada que não existe; a linha é ignorada |
no-contract | aviso | o banco não tem dna/, então regras de contrato não podem rodar |
A ordem não é alfabética: é decrescente por quantas vezes cada regra disparou enquanto o validador estava sendo construído, então as regras produtivas são lidas primeiro.
Regras estruturais funcionam em qualquer banco. Regras de contrato (missing-derived-from, ciclos,
unknown-enum-value) se aplicam apenas onde o banco as declarou em dna/governance.md — o
servidor aplica as regras do banco, não as suas próprias.
Decisões onde a implementação diverge da especificação
- O esquema é lido do banco.
doc_kind/doc_function/statussão conjuntos abertos, lidos dedna/frontmatter.mdedna/governance.md. Uma enumeração rígida rejeita uma minoria considerável de documentos em um banco real. - Um documento tem uma camada (
dna/knowledge/decision/delivery/flow), derivada de seu caminho e ponderada na classificação. Sem ela, o roteamento falha em um banco onde 80% dos documentos são um diário de entrega. derived_fromresolve relativo ao documento e suporta ambas as formas — uma string e{ path, fit }.- Uma referência que sai da raiz é marcada como
externalem vez de contada como quebrada: em um monorepo, bancos se aninham. - Um documento com YAML inválido não sai do índice. 22 documentos nos bancos
reais têm dois pontos sem aspas em
purpose; seus metadados são recuperados linha por linha e o erro é mantido parabank_validaterelatar. - Modelos são excluídos dos resultados de
bank_routee dememorybank://index. bank_routeebank_searchrespondem perguntas diferentes. O primeiro classifica pelo cabeçalho escrito à mão ("sobre o que é este documento"), o segundo busca na prosa ("onde esta string aparece"). Na busca, um token inteiro pesa dez vezes seus próprios fragmentos: caso contrário, a consultaFT-042elevaria o registrofeatures/README.md, com suas setenta linhasFT-*, acima da própria funcionalidade.- Uma consulta é bilíngue, o banco não é. A classificação expande uma palavra da consulta em seus equivalentes no outro idioma e pontua o conceito uma vez. Documentos permanecem em inglês; apenas a pergunta pode não ser.
- Frontmatter é analisado em exatamente um lugar.
gray-mattersem opções memoiza por texto de entrada, e após lançar retorna um objeto em cache cujo frontmatter inteiro está dentro decontent— silenciosamente. Opções são sempre passadas, e a análise passa por um únicosplitFrontmatter. - O grafo separa três resultados de aresta: interno (um nó), externo (
external— um banco aninhado), quebrado (broken). Busca em largura com um teto de nós, então um documento hub não arrasta o banco inteiro e um ciclo não faz loop.
Testes
npm test
195 testes em três camadas, que existem por razões diferentes.
Gerados — test/acceptance-generated.test.ts semeia um banco com o bank_init atual, preenche-o
através de bank_create apenas, e então roda os critérios de prontidão do plano contra isso: roteamento em
ambos os idiomas, o grafo sobre arestas que o próprio servidor escreveu, busca, o delta do git, e cada
portão de recusa. Não precisa de nada além de um diretório temporário e git, então esta é a camada que tem que permanecer
verde. Ela também carrega a afirmação mais forte do design — um banco construído inteiramente através do
servidor valida com zero achados, e ainda valida após três escritas recusadas.
Fixture — test/fixture-bank/ é um banco com violações deliberadas: uma aresta quebrada, um segundo
dono de um fato, um órfão, YAML quebrado, uma referência fora da raiz. Ele permanece escrito à mão de
propósito, porque bank_create não pode produzir nenhum deles.
Corpus — test/acceptance.test.ts roda os mesmos critérios contra quaisquer bancos que você apontar
para ele, que é como as decisões de design foram medidas em primeiro lugar. Esses bancos são seus e
vivem fora do repositório, então a suíte é opt-in:
MEMORYBANK_TEST_ROOTS=/path/to/projects npm run test:corpus
Com a variável não definida, ela pula. Com ela definida mas os bancos ausentes, ela falha em vez de pular — um erro de digitação em um caminho não deve ser lido como um passe.