dd-harness-mcp 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/src/brain.js +9 -0
- package/dist/cli/src/buscar.js +6 -1
- package/dist/cli/src/check.js +21 -5
- package/dist/cli/src/curar.js +39 -29
- package/dist/cli/src/diff.js +74 -16
- package/dist/cli/src/index.js +217 -121
- package/dist/cli/src/init.js +97 -43
- package/dist/cli/src/politica.js +3 -2
- package/dist/cli/src/reancorar.js +48 -0
- package/dist/cli/src/worker.js +65 -0
- package/dist/mcp/src/index.js +70 -48
- package/package.json +1 -1
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A forma do Brain no contrato `/api/v1`.
|
|
3
|
+
*
|
|
4
|
+
* So os tipos: o que o servico devolve, e o que o CLI le. Ate a fase 0 este modulo
|
|
5
|
+
* tambem transformava o payload em arquivos de disco — o `sync` materializava politica,
|
|
6
|
+
* briefing e o Brain inteiro no repositorio consumidor. Isso acabou: a politica chega
|
|
7
|
+
* pelo hook de sessao, e a memoria pela busca, na hora. Nada do dd-harness vive em disco.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
package/dist/cli/src/buscar.js
CHANGED
|
@@ -11,5 +11,10 @@ export async function busca(raiz, consulta, limite) {
|
|
|
11
11
|
if (!resposta.ok)
|
|
12
12
|
await recusa(resposta);
|
|
13
13
|
const lido = (await resposta.json());
|
|
14
|
-
return {
|
|
14
|
+
return {
|
|
15
|
+
semantica: lido.semantica,
|
|
16
|
+
provedor: lido.provedor,
|
|
17
|
+
achados: lido.resultados,
|
|
18
|
+
esperandoIndexacao: lido.esperando_indexacao ?? 0,
|
|
19
|
+
};
|
|
15
20
|
}
|
package/dist/cli/src/check.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { pede } from "./api.js";
|
|
2
2
|
import { leConfigDoRepo, leToken } from "./config.js";
|
|
3
|
-
import { caminhosDoCommit, memoriasTocadas } from "./diff.js";
|
|
3
|
+
import { caminhosDoCommit, memoriasTocadas, sugereReancoragem, } from "./diff.js";
|
|
4
4
|
import { mede } from "./medir.js";
|
|
5
5
|
/** Separado do IO para poder ser testado sem rede: dado um Brain, o que se mede. */
|
|
6
6
|
export async function medeAsAncoras(raiz, brain) {
|
|
@@ -80,11 +80,26 @@ export async function check(raiz, commit) {
|
|
|
80
80
|
const cabecalhos = { Authorization: `Bearer ${token}` };
|
|
81
81
|
const medicoes = await medeAsAncoras(raiz, brain);
|
|
82
82
|
// O cruzamento com o diff so faz sentido quando ha um commit para olhar.
|
|
83
|
-
const
|
|
84
|
-
?
|
|
85
|
-
: [];
|
|
83
|
+
const mudancas = commit
|
|
84
|
+
? await caminhosDoCommit(raiz, commit)
|
|
85
|
+
: { caminhos: [], renames: [] };
|
|
86
|
+
const tocadas = commit ? memoriasTocadas(brain, mudancas.caminhos) : [];
|
|
87
|
+
// Para onde o alvo ausente foi. Sem `--commit` a lista de renames e vazia, entao isto
|
|
88
|
+
// devolve vazio sozinho — nao ha caso especial a escrever.
|
|
89
|
+
const reancoragens = sugereReancoragem(medicoes
|
|
90
|
+
.filter((m) => m.sha === null)
|
|
91
|
+
.map((m) => ({ pasta: m.pasta, memoria: m.memoria, valor: m.valor })), mudancas.renames);
|
|
86
92
|
if (medicoes.length === 0) {
|
|
87
|
-
return {
|
|
93
|
+
return {
|
|
94
|
+
medidas: 0,
|
|
95
|
+
ausentes: [],
|
|
96
|
+
novas: 0,
|
|
97
|
+
base: 0,
|
|
98
|
+
jaAbertas: 0,
|
|
99
|
+
fechadas: 0,
|
|
100
|
+
tocadas,
|
|
101
|
+
reancoragens,
|
|
102
|
+
};
|
|
88
103
|
}
|
|
89
104
|
const envio = await pede(`${config.api}/api/v1/deriva`, {
|
|
90
105
|
method: "POST",
|
|
@@ -111,5 +126,6 @@ export async function check(raiz, commit) {
|
|
|
111
126
|
jaAbertas: julgamento.ja_abertas,
|
|
112
127
|
fechadas: julgamento.fechadas ?? 0,
|
|
113
128
|
tocadas,
|
|
129
|
+
reancoragens,
|
|
114
130
|
};
|
|
115
131
|
}
|
package/dist/cli/src/curar.js
CHANGED
|
@@ -1,37 +1,48 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { relative } from "node:path";
|
|
3
2
|
import { cabecalhos, credencial, pede, recusa } from "./api.js";
|
|
4
|
-
import { gravaManifesto, hashDe, leManifesto } from "./config.js";
|
|
5
3
|
import { interpreta } from "./gravar.js";
|
|
6
4
|
/**
|
|
7
|
-
*
|
|
5
|
+
* A memoria inteira, no markdown que `gravar` e `editar` consomem.
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* Editar reaproveita o mesmo markdown de `gravar`, de proposito: o agente edita o arquivo
|
|
14
|
-
* que o `sync` materializou e manda de volta. Um formato so para as duas operacoes, e o
|
|
15
|
-
* que ele ja sabe ler.
|
|
16
|
-
*/
|
|
17
|
-
/**
|
|
18
|
-
* Depois que o servico aceita, o arquivo em disco deixa de ser "edicao nao enviada" — e o
|
|
19
|
-
* manifesto tem que saber, senao o `sync` seguinte para com "editado a mao" e a unica saida
|
|
20
|
-
* que resta e descartar o arquivo. O ciclo materializa-corrige-envia travava justamente
|
|
21
|
-
* aqui, com o servico ja atualizado.
|
|
22
|
-
*
|
|
23
|
-
* Guarda o hash do que foi enviado, nao do que o servico devolveria: o `sync` seguinte
|
|
24
|
-
* reescreve o arquivo na forma canonica quando o ETag mudar.
|
|
7
|
+
* Sem materializacao, esta e a unica forma de ler o corpo: a busca devolve so endereco,
|
|
8
|
+
* titulo e resumo, e o disco nao tem mais nada. Tambem e o ponto de partida de qualquer
|
|
9
|
+
* edicao — corrigir exige ver o que esta la.
|
|
25
10
|
*/
|
|
26
|
-
async function
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
await
|
|
11
|
+
export async function le(raiz, endereco) {
|
|
12
|
+
const { config, token } = await credencial(raiz);
|
|
13
|
+
const url = new URL(`${config.api}/api/v1/memorias/${endereco}`);
|
|
14
|
+
url.searchParams.set("tenant", config.tenant);
|
|
15
|
+
url.searchParams.set("projeto", config.projeto);
|
|
16
|
+
const resposta = await pede(url, { headers: cabecalhos(token) });
|
|
17
|
+
if (!resposta.ok)
|
|
18
|
+
await recusa(resposta);
|
|
19
|
+
return comoMarkdown((await resposta.json()));
|
|
20
|
+
}
|
|
21
|
+
/** O formato canonico: o mesmo que `interpreta` le, para o ciclo fechar sem conversao. */
|
|
22
|
+
export function comoMarkdown(m) {
|
|
23
|
+
const ancoras = m.ancoras.length
|
|
24
|
+
? `\n## Âncoras\n\n${m.ancoras.map((a) => `- \`${a.valor}\``).join("\n")}\n`
|
|
25
|
+
: "";
|
|
26
|
+
return [
|
|
27
|
+
"---",
|
|
28
|
+
`name: ${m.slug}`,
|
|
29
|
+
`titulo: ${m.titulo}`,
|
|
30
|
+
`description: ${m.resumo}`,
|
|
31
|
+
`pasta: ${m.pasta}`,
|
|
32
|
+
...(m.revisar_ate ? [`revisar-ate: ${m.revisar_ate.slice(0, 10)}`] : []),
|
|
33
|
+
"---",
|
|
34
|
+
"",
|
|
35
|
+
m.corpo.trim(),
|
|
36
|
+
"",
|
|
37
|
+
"## Os três filtros",
|
|
38
|
+
"",
|
|
39
|
+
`**Dano:** ${m.dano}`,
|
|
40
|
+
"",
|
|
41
|
+
`**Invisibilidade:** ${m.invisibilidade}`,
|
|
42
|
+
"",
|
|
43
|
+
`**Externalidade:** ${m.externalidade}`,
|
|
44
|
+
ancoras,
|
|
45
|
+
].join("\n");
|
|
35
46
|
}
|
|
36
47
|
export async function edita(raiz, caminho) {
|
|
37
48
|
const { config, token } = await credencial(raiz);
|
|
@@ -55,7 +66,6 @@ export async function edita(raiz, caminho) {
|
|
|
55
66
|
});
|
|
56
67
|
if (!resposta.ok)
|
|
57
68
|
await recusa(resposta);
|
|
58
|
-
await marcaComoEnviado(raiz, caminho, cru);
|
|
59
69
|
return { endereco, ancoras: memoria.ancoras.length };
|
|
60
70
|
}
|
|
61
71
|
export async function arquiva(raiz, endereco, opcoes) {
|
package/dist/cli/src/diff.js
CHANGED
|
@@ -2,31 +2,89 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
const roda = promisify(execFile);
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* O que um commit mudou: caminhos tocados e renames detectados.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* `-M` liga a deteccao de rename por similaridade, e e o que torna a reancoragem
|
|
8
|
+
* possivel: sem ela, mover um arquivo aparece como um apagado mais um criado, e a ancora
|
|
9
|
+
* que apontava para o antigo so tem "alvo ausente" a dizer. Com ela, o git responde para
|
|
10
|
+
* ONDE o conteudo foi, com um percentual de confianca.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* nem foi medida, e util mesmo que a memoria continue valendo.
|
|
12
|
+
* Vazio quando o git nao responde — nunca quebra: um aviso que nao pode ser dado nao vira
|
|
13
|
+
* erro que atrapalha o commit.
|
|
14
14
|
*/
|
|
15
|
-
/** Caminhos alterados num commit. Vazio quando o git nao responde — nunca quebra. */
|
|
16
15
|
export async function caminhosDoCommit(raiz, commit) {
|
|
17
16
|
try {
|
|
18
|
-
const { stdout } = await roda("git", ["diff-tree", "--no-commit-id", "
|
|
19
|
-
return stdout
|
|
20
|
-
.split(/\r?\n/)
|
|
21
|
-
.map((l) => l.trim())
|
|
22
|
-
.filter(Boolean);
|
|
17
|
+
const { stdout } = await roda("git", ["diff-tree", "--no-commit-id", "-r", "-M", "--name-status", commit], { cwd: raiz });
|
|
18
|
+
return interpretaNameStatus(stdout);
|
|
23
19
|
}
|
|
24
20
|
catch {
|
|
25
|
-
|
|
26
|
-
// acontece. Um aviso que nao pode ser dado nao vira erro que atrapalha o commit.
|
|
27
|
-
return [];
|
|
21
|
+
return { caminhos: [], renames: [] };
|
|
28
22
|
}
|
|
29
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Le a saida de `--name-status`: uma letra de status, TAB, e um ou dois caminhos.
|
|
26
|
+
*
|
|
27
|
+
* Rename vem como `R<similaridade>\t<de>\t<para>` (ex: `R094`), e conta como mudanca nos
|
|
28
|
+
* DOIS caminhos: quem tinha ancora no antigo precisa saber, e quem tem ancora no novo
|
|
29
|
+
* tambem — o conteudo chegou la.
|
|
30
|
+
*/
|
|
31
|
+
export function interpretaNameStatus(stdout) {
|
|
32
|
+
const caminhos = [];
|
|
33
|
+
const renames = [];
|
|
34
|
+
for (const linha of stdout.split(/\r?\n/)) {
|
|
35
|
+
if (!linha.trim())
|
|
36
|
+
continue;
|
|
37
|
+
const [status, primeiro, segundo] = linha.split("\t");
|
|
38
|
+
if (!status || !primeiro)
|
|
39
|
+
continue;
|
|
40
|
+
if (status.startsWith("R") && segundo) {
|
|
41
|
+
caminhos.push(primeiro, segundo);
|
|
42
|
+
renames.push({
|
|
43
|
+
de: primeiro,
|
|
44
|
+
para: segundo,
|
|
45
|
+
// `R094` -> 94. Sem numero (formatos antigos do git), 0 diz "nao sei o quanto".
|
|
46
|
+
similaridade: Number(status.slice(1)) || 0,
|
|
47
|
+
});
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
caminhos.push(primeiro);
|
|
51
|
+
}
|
|
52
|
+
return { caminhos, renames };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Cruza ancoras ausentes com os renames do commit.
|
|
56
|
+
*
|
|
57
|
+
* O caso que isto resolve: uma refatoracao move um arquivo (ou uma pasta inteira) e toda
|
|
58
|
+
* memoria ancorada ali passa a acusar "alvo ausente" de uma vez. A lista sozinha nao
|
|
59
|
+
* ajuda — o git sabe para onde o conteudo foi, e e essa a resposta que faltava.
|
|
60
|
+
*
|
|
61
|
+
* Ancora de trecho (`arquivo#alvo`) mantem o alvo na sugestao: o arquivo mudou de lugar,
|
|
62
|
+
* o trecho dentro dele provavelmente nao.
|
|
63
|
+
*/
|
|
64
|
+
export function sugereReancoragem(ausentes, renames) {
|
|
65
|
+
const sugestoes = [];
|
|
66
|
+
for (const ausente of ausentes) {
|
|
67
|
+
const corte = ausente.valor.indexOf("#");
|
|
68
|
+
const arquivo = corte === -1 ? ausente.valor : ausente.valor.slice(0, corte);
|
|
69
|
+
const trecho = corte === -1 ? "" : ausente.valor.slice(corte);
|
|
70
|
+
// Casa o arquivo exato ou um diretorio que o continha: mover `src/db/` inteiro
|
|
71
|
+
// renomeia cada arquivo dentro, e a ancora de diretorio precisa achar isso.
|
|
72
|
+
const rename = renames.find((r) => r.de === arquivo || arquivo.startsWith(`${r.de}/`));
|
|
73
|
+
if (!rename)
|
|
74
|
+
continue;
|
|
75
|
+
const destino = rename.de === arquivo
|
|
76
|
+
? rename.para
|
|
77
|
+
: `${rename.para}${arquivo.slice(rename.de.length)}`;
|
|
78
|
+
sugestoes.push({
|
|
79
|
+
pasta: ausente.pasta,
|
|
80
|
+
memoria: ausente.memoria,
|
|
81
|
+
ancora: ausente.valor,
|
|
82
|
+
sugestao: `${destino}${trecho}`,
|
|
83
|
+
similaridade: rename.similaridade,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return sugestoes;
|
|
87
|
+
}
|
|
30
88
|
/**
|
|
31
89
|
* Cruza caminhos com ancoras. A ancora casa quando e o proprio caminho ou quando e um
|
|
32
90
|
* diretorio que o contem — `supabase/migrations` tem que casar com a migration nova.
|
package/dist/cli/src/index.js
CHANGED
|
@@ -3,14 +3,14 @@ import { termosDaConsulta } from "./argv.js";
|
|
|
3
3
|
import { check, status } from "./check.js";
|
|
4
4
|
import { leConfigDoRepo, guardaToken } from "./config.js";
|
|
5
5
|
import { grava } from "./gravar.js";
|
|
6
|
-
import { init, SUGESTAO_MCP } from "./init.js";
|
|
7
|
-
import { LINHA_DE_IMPORT } from "./materializa.js";
|
|
6
|
+
import { init, SUGESTAO_AGENTS, SUGESTAO_HOOK, SUGESTAO_MCP } from "./init.js";
|
|
8
7
|
import { buscaPolitica } from "./politica.js";
|
|
9
8
|
import { busca } from "./buscar.js";
|
|
10
|
-
import { arquiva, edita } from "./curar.js";
|
|
9
|
+
import { arquiva, edita, le } from "./curar.js";
|
|
11
10
|
import { criaPasta } from "./pasta.js";
|
|
12
11
|
import { criaProjeto } from "./projeto.js";
|
|
13
|
-
import {
|
|
12
|
+
import { reancora } from "./reancorar.js";
|
|
13
|
+
import { subiuOWorker } from "./worker.js";
|
|
14
14
|
/**
|
|
15
15
|
* `dd-harness` — o cliente que materializa os artefatos no repositorio.
|
|
16
16
|
*
|
|
@@ -19,80 +19,53 @@ import { sync } from "./sync.js";
|
|
|
19
19
|
* em repositorio alheio tem que envelhecer bem, e cada dependencia e uma chance de nao
|
|
20
20
|
* envelhecer.
|
|
21
21
|
*/
|
|
22
|
-
const AJUDA = `dd-harness —
|
|
23
|
-
|
|
24
|
-
dd-harness login --token <token> [--api <url>]
|
|
25
|
-
guarda a credencial desta máquina
|
|
26
|
-
dd-harness projeto <slug> --nome "<nome>" [--tenant <t>] [--api <url>]
|
|
27
|
-
cria o projeto no serviço (antes do init)
|
|
28
|
-
dd-harness init --tenant <t> --projeto <p> [--api <url>]
|
|
29
|
-
prepara o repositório (config + CLAUDE.md)
|
|
30
|
-
dd-harness pasta <slug> --definicao "o que entra e o que não entra"
|
|
31
|
-
cria a pasta que o gravar exige
|
|
32
|
-
dd-harness gravar <arquivo.md> registra uma memória a partir de um markdown
|
|
33
|
-
dd-harness editar <arquivo.md> corrige o que já está gravado
|
|
34
|
-
dd-harness arquivar <pasta>/<slug> --motivo <obsoleta|incorreta|fora_dos_filtros>
|
|
35
|
-
[--substituida-por <pasta>/<slug>]
|
|
36
|
-
tira de circulação sem apagar
|
|
37
|
-
dd-harness buscar "<pergunta>" acha memória por relevância, não por arquivo
|
|
38
|
-
dd-harness
|
|
39
|
-
dd-harness
|
|
40
|
-
|
|
41
|
-
dd-harness
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
22
|
+
const AJUDA = `dd-harness — a política e o Brain do projeto, no serviço
|
|
23
|
+
|
|
24
|
+
dd-harness login --token <token> [--api <url>]
|
|
25
|
+
guarda a credencial desta máquina
|
|
26
|
+
dd-harness projeto <slug> --nome "<nome>" [--tenant <t>] [--api <url>]
|
|
27
|
+
cria o projeto no serviço (antes do init)
|
|
28
|
+
dd-harness init --tenant <t> --projeto <p> [--api <url>]
|
|
29
|
+
prepara o repositório (config + CLAUDE.md)
|
|
30
|
+
dd-harness pasta <slug> --definicao "o que entra e o que não entra"
|
|
31
|
+
cria a pasta que o gravar exige
|
|
32
|
+
dd-harness gravar <arquivo.md> registra uma memória a partir de um markdown
|
|
33
|
+
dd-harness editar <arquivo.md> corrige o que já está gravado
|
|
34
|
+
dd-harness arquivar <pasta>/<slug> --motivo <obsoleta|incorreta|fora_dos_filtros>
|
|
35
|
+
[--substituida-por <pasta>/<slug>]
|
|
36
|
+
tira de circulação sem apagar
|
|
37
|
+
dd-harness buscar "<pergunta>" acha memória por relevância, não por arquivo
|
|
38
|
+
dd-harness ler <pasta>/<slug> imprime a memória inteira, no formato de gravar
|
|
39
|
+
dd-harness reancorar <pasta>/<slug> --de "<alvo>" --para "<alvo>"
|
|
40
|
+
troca o alvo de uma âncora que mudou de lugar
|
|
41
|
+
dd-harness check [--commit <sha>] mede as âncoras e reporta a deriva
|
|
42
|
+
dd-harness status só lê: o tamanho do Brain e o que espera julgamento
|
|
43
|
+
dd-harness politica [--hook] imprime a política do serviço
|
|
44
|
+
saída 0 = veio; 3 = projeto sem política;
|
|
45
|
+
1 = não consegui buscar
|
|
46
|
+
--hook: fala o protocolo do SessionStart do
|
|
47
|
+
Claude Code, para pôr a política no contexto
|
|
48
|
+
dd-harness --help
|
|
49
|
+
|
|
50
|
+
Nada do dd-harness fica em disco: a política chega pelo hook de sessão, e a
|
|
51
|
+
memória pela busca, na hora.
|
|
48
52
|
`;
|
|
49
|
-
/** O aviso existe porque o import falha em silêncio — foi medido, não suposto. */
|
|
50
|
-
function avisaSobreOPonteiro(ponteiro) {
|
|
51
|
-
if (ponteiro === "ok" || ponteiro === "sem-politica")
|
|
52
|
-
return;
|
|
53
|
-
// O import pendurado e o inverso dos outros dois: a linha esta la, o alvo e que nao
|
|
54
|
-
// existe. Dizer "acrescente a linha" aqui mandaria a pessoa para o lugar errado.
|
|
55
|
-
if (ponteiro === "aponta-para-o-vazio") {
|
|
56
|
-
console.error([
|
|
57
|
-
"",
|
|
58
|
-
`AVISO: o CLAUDE.md importa ${LINHA_DE_IMPORT}, mas não há política no serviço.`,
|
|
59
|
-
"O arquivo apontado não existe, e import quebrado falha em silêncio: a sessão abre",
|
|
60
|
-
"sem protocolo e nada avisa.",
|
|
61
|
-
"",
|
|
62
|
-
"Escreva a política do projeto no serviço, ou tire a linha do CLAUDE.md.",
|
|
63
|
-
].join("\n"));
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
const motivo = ponteiro === "sem-claude-md"
|
|
67
|
-
? "não há CLAUDE.md na raiz"
|
|
68
|
-
: "o CLAUDE.md da raiz não importa a política";
|
|
69
|
-
console.error([
|
|
70
|
-
"",
|
|
71
|
-
`AVISO: ${motivo}.`,
|
|
72
|
-
"A política existe no serviço e está em disco, mas não chega à sessão: o import",
|
|
73
|
-
"ausente falha em silêncio, e a sessão abre sem protocolo sem avisar ninguém.",
|
|
74
|
-
"",
|
|
75
|
-
`Acrescente esta linha ao CLAUDE.md da raiz: ${LINHA_DE_IMPORT}`,
|
|
76
|
-
"Ou rode: dd-harness init --tenant <t> --projeto <p>",
|
|
77
|
-
].join("\n"));
|
|
78
|
-
}
|
|
79
53
|
/**
|
|
80
54
|
* Os ganchos sao IMPRESSOS, nunca instalados. `.git/hooks` nao e versionado e nao e
|
|
81
55
|
* nosso: escrever la dentro sem a pessoa pedir e o mesmo tipo de invasao que
|
|
82
56
|
* sobrescrever o CLAUDE.md dela. Quem cola, decide.
|
|
83
57
|
*/
|
|
84
|
-
const GANCHOS = `
|
|
85
|
-
Opcional —
|
|
86
|
-
|
|
87
|
-
.git/hooks/post-commit (avisa quais memórias falam do que você mudou)
|
|
88
|
-
#!/bin/sh
|
|
89
|
-
dd-harness check --commit "$(git rev-parse HEAD)" || true
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
Os dois terminam em sucesso mesmo com deriva: avisam, não bloqueiam.`;
|
|
58
|
+
const GANCHOS = `
|
|
59
|
+
Opcional — o gancho que devolve a memória ao code review:
|
|
60
|
+
|
|
61
|
+
.git/hooks/post-commit (avisa quais memórias falam do que você mudou)
|
|
62
|
+
#!/bin/sh
|
|
63
|
+
dd-harness check --commit "$(git rev-parse HEAD)" || true
|
|
64
|
+
|
|
65
|
+
Termina em sucesso mesmo com deriva: avisa, não bloqueia.
|
|
66
|
+
|
|
67
|
+
O hook da política (\`dd-harness politica --hook\`) é outra coisa, e não é
|
|
68
|
+
opcional — \`dd-harness init\` imprime a linha para o \`.claude/settings.json\`.`;
|
|
96
69
|
function argumento(argv, nome) {
|
|
97
70
|
const i = argv.indexOf(`--${nome}`);
|
|
98
71
|
return i >= 0 ? argv[i + 1] : undefined;
|
|
@@ -109,20 +82,39 @@ async function comandoInit(argv) {
|
|
|
109
82
|
api: argumento(argv, "api"),
|
|
110
83
|
});
|
|
111
84
|
console.log(r.config === "criada" ? "criado .dd-harness.json" : "mantido .dd-harness.json");
|
|
112
|
-
console.log(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
85
|
+
console.log("\nAgora: dd-harness login --token <token>");
|
|
86
|
+
// O hook vem primeiro e nao e opcional: sem ele a sessao abre sem politica, que e a
|
|
87
|
+
// falha que este projeto existe para combater. O MCP e conveniencia; este, nao.
|
|
88
|
+
if (r.hook === "ja-declarado") {
|
|
89
|
+
console.log("\nmantido .claude/settings.json — o hook da política já está declarado");
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.log("\nOBRIGATÓRIO: o hook que carrega a política no início de cada sessão." +
|
|
93
|
+
"\nSem ele a sessão abre sem protocolo, e nada avisa. Acrescente ao" +
|
|
94
|
+
"\n`.claude/settings.json` (não escrevo nele: o arquivo é seu e pode já" +
|
|
95
|
+
"\nter hooks e permissões):\n");
|
|
96
|
+
console.log(SUGESTAO_HOOK);
|
|
97
|
+
}
|
|
118
98
|
if (r.mcp === "ja-declarado") {
|
|
119
99
|
console.log("\nmantido .mcp.json — o servidor dd-harness já está declarado");
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
console.log("\nOpcional: as memórias como ferramenta, para o agente buscar e gravar sem" +
|
|
103
|
+
"\nescrever arquivo. Acrescente ao `.mcp.json` da raiz (não escrevo nele: o" +
|
|
104
|
+
"\narquivo é seu e pode declarar outros servidores):\n");
|
|
105
|
+
console.log(SUGESTAO_MCP);
|
|
106
|
+
}
|
|
107
|
+
// Fora do Claude Code o hook nao roda, e ai o MCP deixa de ser conveniencia: e o unico
|
|
108
|
+
// caminho da politica. Mas ferramenta disponivel nao e ferramenta chamada — sem esta
|
|
109
|
+
// instrucao, o agente pode nunca perguntar.
|
|
110
|
+
if (r.agents === "ja-aponta") {
|
|
111
|
+
console.log("\nmantido AGENTS.md — já manda buscar a política pelo MCP");
|
|
120
112
|
return;
|
|
121
113
|
}
|
|
122
|
-
console.log("\
|
|
123
|
-
"\
|
|
124
|
-
"\
|
|
125
|
-
console.log(
|
|
114
|
+
console.log("\nUsa Codex, Cursor, Gemini CLI ou Windsurf? Eles NÃO rodam o hook do" +
|
|
115
|
+
"\nClaude Code, e sem isto a sessão abre sem política. Crie um `AGENTS.md`" +
|
|
116
|
+
"\nna raiz (ou acrescente ao seu) com:\n");
|
|
117
|
+
console.log(SUGESTAO_AGENTS);
|
|
126
118
|
}
|
|
127
119
|
/**
|
|
128
120
|
* Nao exige `.dd-harness.json`, e isso importa: sem projeto no servico o `init` nao tem o
|
|
@@ -147,7 +139,7 @@ async function comandoEditar(argv) {
|
|
|
147
139
|
}
|
|
148
140
|
const r = await edita(process.cwd(), caminho);
|
|
149
141
|
console.log(`editado ${r.endereco}`);
|
|
150
|
-
console.log(` ${r.ancoras} âncora(s)
|
|
142
|
+
console.log(` ${r.ancoras} âncora(s).`);
|
|
151
143
|
}
|
|
152
144
|
const MOTIVOS = ["obsoleta", "incorreta", "fora_dos_filtros"];
|
|
153
145
|
async function comandoArquivar(argv) {
|
|
@@ -166,7 +158,20 @@ async function comandoArquivar(argv) {
|
|
|
166
158
|
substituidaPor: argumento(argv, "substituida-por"),
|
|
167
159
|
});
|
|
168
160
|
console.log(`arquivado ${r.endereco} (${r.motivo})`);
|
|
169
|
-
console.log(" foi para o histórico, não foi apagada.
|
|
161
|
+
console.log(" foi para o histórico, não foi apagada.");
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* A memoria inteira no stdout, no mesmo markdown que `gravar` e `editar` consomem.
|
|
165
|
+
*
|
|
166
|
+
* Sem materializacao este e o unico caminho para o corpo: a busca devolve so endereco,
|
|
167
|
+
* titulo e resumo. Tambem e o ponto de partida de toda edicao — corrigir exige ver.
|
|
168
|
+
*/
|
|
169
|
+
async function comandoLer(argv) {
|
|
170
|
+
const endereco = argv[0];
|
|
171
|
+
if (!endereco || endereco.startsWith("-")) {
|
|
172
|
+
throw new Error("uso: dd-harness ler <pasta>/<slug>");
|
|
173
|
+
}
|
|
174
|
+
console.log(await le(process.cwd(), endereco));
|
|
170
175
|
}
|
|
171
176
|
async function comandoBuscar(argv) {
|
|
172
177
|
const consulta = termosDaConsulta(argv);
|
|
@@ -181,6 +186,7 @@ async function comandoBuscar(argv) {
|
|
|
181
186
|
if (!r.semantica) {
|
|
182
187
|
console.log(" (só busca textual: sem provedor de embedding no serviço)");
|
|
183
188
|
}
|
|
189
|
+
avisaSobreAFila(r.esperandoIndexacao);
|
|
184
190
|
return;
|
|
185
191
|
}
|
|
186
192
|
console.log(r.semantica
|
|
@@ -190,36 +196,32 @@ async function comandoBuscar(argv) {
|
|
|
190
196
|
console.log(` ${a.endereco} — ${a.titulo}`);
|
|
191
197
|
console.log(` ${a.resumo}`);
|
|
192
198
|
}
|
|
199
|
+
avisaSobreAFila(r.esperandoIndexacao);
|
|
193
200
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
"O disco é projeção do serviço, uma direção só. Duas saídas:",
|
|
202
|
-
"",
|
|
203
|
-
" 1. Leve a edição para o serviço — `dd-harness editar <arquivo>` para cada um",
|
|
204
|
-
" acima. É o caminho normal de corrigir memória, e destrava o sync.",
|
|
205
|
-
" 2. Descarte a edição local (git checkout / apague o arquivo) e sincronize.",
|
|
206
|
-
].join("\n"));
|
|
207
|
-
process.exitCode = 1;
|
|
201
|
+
/**
|
|
202
|
+
* Memoria sem vetor nao aparece na busca semantica, e a resposta parece completa do
|
|
203
|
+
* mesmo jeito. `semantica: true` diz so que a CONSULTA foi vetorizada — a base pode
|
|
204
|
+
* estar inteira na fila, e ai a busca responde lexical com cara de semantica.
|
|
205
|
+
*/
|
|
206
|
+
function avisaSobreAFila(esperando) {
|
|
207
|
+
if (esperando <= 0)
|
|
208
208
|
return;
|
|
209
|
+
console.log("");
|
|
210
|
+
console.log(`ATENÇÃO: ${esperando} memória(s) ainda sem vetor — podem existir respostas`);
|
|
211
|
+
console.log(" melhores que não apareceram aqui. Rode `start-worker.bat`.");
|
|
212
|
+
}
|
|
213
|
+
async function comandoReancorar(argv) {
|
|
214
|
+
const endereco = argv[0];
|
|
215
|
+
const de = argumento(argv, "de");
|
|
216
|
+
const para = argumento(argv, "para");
|
|
217
|
+
if (!endereco || endereco.startsWith("-") || !endereco.includes("/") || !de || !para) {
|
|
218
|
+
throw new Error('uso: dd-harness reancorar <pasta>/<slug> --de "<alvo>" --para "<alvo>"');
|
|
209
219
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
console.log(`escrito ${a}`);
|
|
216
|
-
for (const a of resultado.removidos)
|
|
217
|
-
console.log(`removido ${a}`);
|
|
218
|
-
if (!resultado.escritos.length && !resultado.removidos.length) {
|
|
219
|
-
console.log("conteúdo novo do serviço, sem diferença em disco.");
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
avisaSobreOPonteiro(resultado.ponteiro);
|
|
220
|
+
const r = await reancora(process.cwd(), endereco, de, para);
|
|
221
|
+
console.log(`reancorado ${r.endereco}`);
|
|
222
|
+
console.log(` de ${r.de}`);
|
|
223
|
+
console.log(` para ${r.para}`);
|
|
224
|
+
console.log(" Rode `dd-harness check --commit <sha>` para medir a base nova.");
|
|
223
225
|
}
|
|
224
226
|
async function comandoCheck(argv) {
|
|
225
227
|
const r = await check(process.cwd(), argumento(argv, "commit"));
|
|
@@ -236,8 +238,24 @@ async function comandoCheck(argv) {
|
|
|
236
238
|
if (r.ausentes.length) {
|
|
237
239
|
console.log("");
|
|
238
240
|
console.log("ALVO AUSENTE — alguém apagou ou moveu o que uma memória guarda:");
|
|
239
|
-
for (const valor of r.ausentes)
|
|
241
|
+
for (const valor of r.ausentes) {
|
|
240
242
|
console.log(` ${valor}`);
|
|
243
|
+
// Quando o git achou para onde o conteúdo foi, a saída deixa de ser só um
|
|
244
|
+
// diagnóstico e passa a ter uma ação — que é o que faltava numa refatoração
|
|
245
|
+
// grande, onde a lista de ausentes vira uma parede sem resposta.
|
|
246
|
+
const sugerida = r.reancoragens.find((s) => s.ancora === valor);
|
|
247
|
+
if (sugerida) {
|
|
248
|
+
console.log(` → provavelmente virou ${sugerida.sugestao}` +
|
|
249
|
+
(sugerida.similaridade ? ` (git: ${sugerida.similaridade}% similar)` : ""));
|
|
250
|
+
console.log(` dd-harness reancorar ${sugerida.pasta}/${sugerida.memoria}` +
|
|
251
|
+
` --de "${valor}" --para "${sugerida.sugestao}"`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (r.reancoragens.length) {
|
|
255
|
+
console.log("");
|
|
256
|
+
console.log(" A sugestão vem da detecção de rename do git, por similaridade de");
|
|
257
|
+
console.log(" conteúdo — confira antes de aplicar. Nada é reancorado sozinho.");
|
|
258
|
+
}
|
|
241
259
|
}
|
|
242
260
|
if (r.novas > 0) {
|
|
243
261
|
console.log("");
|
|
@@ -273,8 +291,20 @@ async function comandoCheck(argv) {
|
|
|
273
291
|
* um "falhou" generico colapsaria: projeto novo (que precisa de briefing) de politica
|
|
274
292
|
* inalcancavel (que precisa PARAR a sessao). Mudar estes numeros quebra o hook.
|
|
275
293
|
*/
|
|
276
|
-
async function comandoPolitica() {
|
|
294
|
+
async function comandoPolitica(argv) {
|
|
277
295
|
const r = await buscaPolitica(process.cwd());
|
|
296
|
+
// `--hook`: fala o protocolo do SessionStart do Claude Code, que injeta
|
|
297
|
+
// `additionalContext` no contexto da sessao. Sem a flag, saida legivel para quem roda
|
|
298
|
+
// no terminal. A diferenca importa: o hook precisa que o AVISO chegue ao modelo, e
|
|
299
|
+
// stderr so chega ao transcript — aviso que o modelo nao le e o mesmo que silencio.
|
|
300
|
+
if (argv.includes("--hook")) {
|
|
301
|
+
// A abertura da sessao e o momento certo de esvaziar a fila: o worker nao esta
|
|
302
|
+
// hospedado, e memoria sem vetor some da busca sem nada denunciar. Sobe so quando ha
|
|
303
|
+
// fila de verdade — o hook roda ate nas sessoes que so leem codigo.
|
|
304
|
+
const worker = await subiuOWorker(process.cwd(), r.esperandoIndexacao ?? 0);
|
|
305
|
+
console.log(JSON.stringify({ hookSpecificOutput: contextoDaSessao(r, worker) }));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
278
308
|
if (r.estado === "ok") {
|
|
279
309
|
console.log(r.conteudo);
|
|
280
310
|
return;
|
|
@@ -287,6 +317,72 @@ async function comandoPolitica() {
|
|
|
287
317
|
console.error(`não consegui buscar a política: ${r.motivo}`);
|
|
288
318
|
process.exitCode = 1;
|
|
289
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* O que o hook injeta no contexto, por estado.
|
|
322
|
+
*
|
|
323
|
+
* Sai sempre com codigo 0: o que precisa chegar ao modelo e o TEXTO, e um codigo de erro
|
|
324
|
+
* so faria o Claude Code registrar falha no transcript — que ninguem le — enquanto a
|
|
325
|
+
* sessao seguiria sem saber que esta sem protocolo.
|
|
326
|
+
*/
|
|
327
|
+
function contextoDaSessao(r, worker) {
|
|
328
|
+
const base = { hookEventName: "SessionStart" };
|
|
329
|
+
const fila = avisoDaFila(r.esperandoIndexacao ?? 0, worker);
|
|
330
|
+
if (r.estado === "ok") {
|
|
331
|
+
return {
|
|
332
|
+
...base,
|
|
333
|
+
additionalContext: "# Política deste projeto (carregada do dd-harness)\n\n" +
|
|
334
|
+
"As regras abaixo valem para esta sessão inteira.\n\n" +
|
|
335
|
+
r.conteudo +
|
|
336
|
+
fila,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
if (r.estado === "sem-politica") {
|
|
340
|
+
return {
|
|
341
|
+
...base,
|
|
342
|
+
additionalContext: "AVISO DO DD-HARNESS: este projeto existe no serviço mas **nunca foi briefado** " +
|
|
343
|
+
"— não há política.\n\nIsto não é uma falha: é um projeto novo. Antes de " +
|
|
344
|
+
"implementar qualquer coisa, diga isso ao usuário e proponha rodar `/briefar`." +
|
|
345
|
+
fila,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
...base,
|
|
350
|
+
additionalContext: "PARE: NÃO FOI POSSÍVEL CARREGAR A POLÍTICA DESTE PROJETO.\n\n" +
|
|
351
|
+
`Motivo: ${r.motivo}\n\n` +
|
|
352
|
+
"A política pode existir no serviço e não ter chegado até aqui, então esta sessão " +
|
|
353
|
+
"está **sem protocolo** — as proibições e a regra do OK não foram carregadas.\n\n" +
|
|
354
|
+
"Antes de qualquer outra coisa: avise o usuário com estas palavras e **não " +
|
|
355
|
+
"modifique nenhum arquivo** até ele decidir como prosseguir. Seguir como se nada " +
|
|
356
|
+
"tivesse acontecido é exatamente a falha que este projeto combate.",
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* O estado da fila de indexacao, para ir junto da politica no contexto.
|
|
361
|
+
*
|
|
362
|
+
* Fila vazia nao gera linha nenhuma: aviso sem motivo em toda sessao e o que ensina a
|
|
363
|
+
* ignorar aviso. O texto muda conforme o worker subiu ou nao, porque a acao que se espera
|
|
364
|
+
* do agente e diferente em cada caso.
|
|
365
|
+
*/
|
|
366
|
+
function avisoDaFila(esperando, worker) {
|
|
367
|
+
if (esperando <= 0)
|
|
368
|
+
return "";
|
|
369
|
+
if (worker.subiu) {
|
|
370
|
+
return (`\n\n---\n\nNOTA DO DD-HARNESS: ${esperando} memória(s) estavam sem vetor, e o ` +
|
|
371
|
+
"worker de indexação foi iniciado automaticamente agora. Até ele terminar, " +
|
|
372
|
+
"`buscar_memoria` pode não encontrar o que foi gravado recentemente — se uma " +
|
|
373
|
+
"busca vier vazia nos próximos minutos, tente de novo antes de concluir que a " +
|
|
374
|
+
"memória não existe.");
|
|
375
|
+
}
|
|
376
|
+
// Sem worker local (o caso do repositorio consumidor) ou falha ao subir: so avisar.
|
|
377
|
+
const comoResolver = worker.motivo === "sem-worker"
|
|
378
|
+
? "O worker não roda a partir deste repositório — ele vive no monorepo do " +
|
|
379
|
+
"dd-harness. Avise o usuário que a indexação está pendente lá."
|
|
380
|
+
: `Não consegui iniciar o worker${worker.detalhe ? ` (${worker.detalhe})` : ""}. ` +
|
|
381
|
+
"Peça ao usuário para rodar `start-worker.bat`.";
|
|
382
|
+
return (`\n\n---\n\nAVISO DO DD-HARNESS: ${esperando} memória(s) estão sem vetor e **não ` +
|
|
383
|
+
"aparecem na busca semântica**. A busca vai responder mesmo assim, o que a faz " +
|
|
384
|
+
`parecer completa quando não está.\n\n${comoResolver}`);
|
|
385
|
+
}
|
|
290
386
|
async function comandoStatus() {
|
|
291
387
|
const r = await status(process.cwd());
|
|
292
388
|
// O acervo vem primeiro, e sempre: e a resposta para "o que ha no Brain deste projeto?",
|
|
@@ -361,9 +457,8 @@ async function comandoPasta(argv) {
|
|
|
361
457
|
}
|
|
362
458
|
/**
|
|
363
459
|
* O agente escreve o arquivo — que e o que ele ja fazia no modelo file-based — e este
|
|
364
|
-
* comando o transforma em requisicao. O arquivo
|
|
365
|
-
*
|
|
366
|
-
* lado da copia gerada.
|
|
460
|
+
* comando o transforma em requisicao. O arquivo e so o veiculo: depois de gravado, a
|
|
461
|
+
* memoria vive no servico, e quem quiser le-la usa a busca. Nada fica em disco.
|
|
367
462
|
*/
|
|
368
463
|
async function comandoGravar(argv) {
|
|
369
464
|
const caminho = argv[0];
|
|
@@ -372,8 +467,7 @@ async function comandoGravar(argv) {
|
|
|
372
467
|
}
|
|
373
468
|
const r = await grava(process.cwd(), caminho);
|
|
374
469
|
console.log(`gravado ${r.endereco}`);
|
|
375
|
-
console.log(` ${r.ancoras} âncora(s), valendo para ${r.projetos} projeto(s).`
|
|
376
|
-
" Rode `dd-harness sync` para materializar.");
|
|
470
|
+
console.log(` ${r.ancoras} âncora(s), valendo para ${r.projetos} projeto(s).`);
|
|
377
471
|
}
|
|
378
472
|
async function principal() {
|
|
379
473
|
const [comando, ...resto] = process.argv.slice(2);
|
|
@@ -392,16 +486,18 @@ async function principal() {
|
|
|
392
486
|
return comandoEditar(resto);
|
|
393
487
|
case "arquivar":
|
|
394
488
|
return comandoArquivar(resto);
|
|
489
|
+
case "ler":
|
|
490
|
+
return comandoLer(resto);
|
|
395
491
|
case "buscar":
|
|
396
492
|
return comandoBuscar(resto);
|
|
397
|
-
case "
|
|
398
|
-
return
|
|
493
|
+
case "reancorar":
|
|
494
|
+
return comandoReancorar(resto);
|
|
399
495
|
case "check":
|
|
400
496
|
return comandoCheck(resto);
|
|
401
497
|
case "status":
|
|
402
498
|
return comandoStatus();
|
|
403
499
|
case "politica":
|
|
404
|
-
return comandoPolitica();
|
|
500
|
+
return comandoPolitica(resto);
|
|
405
501
|
case "--help":
|
|
406
502
|
case "-h":
|
|
407
503
|
case undefined:
|
package/dist/cli/src/init.js
CHANGED
|
@@ -1,37 +1,72 @@
|
|
|
1
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { CAMINHO_CONFIG } from "./config.js";
|
|
4
|
-
import { LINHA_DE_IMPORT } from "./materializa.js";
|
|
5
4
|
/**
|
|
6
|
-
*
|
|
5
|
+
* O `.mcp.json` e sugerido, nunca escrito.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* uma linha, e nao um ritual de mover arquivo.
|
|
7
|
+
* O arquivo e do repositorio, pode ja declarar outros servidores, e mesclar JSON alheio e
|
|
8
|
+
* onde falha silenciosa nasce — entrada errada nao da erro, a ferramenta so nao aparece.
|
|
9
|
+
* Quem cola sabe o que colou.
|
|
12
10
|
*/
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
export const SUGESTAO_MCP = `{
|
|
12
|
+
"mcpServers": {
|
|
13
|
+
"dd-harness": {
|
|
14
|
+
"command": "npx",
|
|
15
|
+
"args": ["-y", "dd-harness-mcp"]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}`;
|
|
20
19
|
/**
|
|
21
|
-
* O
|
|
20
|
+
* O hook que carrega a politica no inicio de cada sessao.
|
|
21
|
+
*
|
|
22
|
+
* E a garantia de que nenhuma sessao abre sem protocolo — o papel que antes era do
|
|
23
|
+
* arquivo materializado mais a linha de import. Vive num hook, e nao numa instrucao no
|
|
24
|
+
* `CLAUDE.md`, porque instrucao o modelo pode pular: o import quebrado falhava em
|
|
25
|
+
* silencio, e isso foi medido.
|
|
22
26
|
*
|
|
23
|
-
*
|
|
24
|
-
* outros servidores, e mesclar JSON alheio e onde falha silenciosa nasce — entrada errada
|
|
25
|
-
* nao da erro, a ferramenta so nao aparece. Quem cola sabe o que colou.
|
|
27
|
+
* Sugerido e nao escrito, pelo mesmo motivo do `.mcp.json`.
|
|
26
28
|
*/
|
|
27
|
-
export const
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
export const SUGESTAO_HOOK = `{
|
|
30
|
+
"hooks": {
|
|
31
|
+
"SessionStart": [
|
|
32
|
+
{
|
|
33
|
+
"hooks": [
|
|
34
|
+
{
|
|
35
|
+
"type": "command",
|
|
36
|
+
"command": "dd-harness politica --hook",
|
|
37
|
+
"statusMessage": "Carregando a política do dd-harness..."
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
34
43
|
}`;
|
|
44
|
+
/**
|
|
45
|
+
* O que escrever num `AGENTS.md`, para agente que NAO e o Claude Code.
|
|
46
|
+
*
|
|
47
|
+
* O hook de `SessionStart` e um mecanismo do Claude Code: Codex, Cursor, Gemini CLI e
|
|
48
|
+
* Windsurf nao o executam. Nessas ferramentas nada carrega a politica sozinho — e o
|
|
49
|
+
* resultado e a mesma falha de sempre, a sessao abrindo sem protocolo sem avisar.
|
|
50
|
+
*
|
|
51
|
+
* O que resta la e o servidor MCP, que essas ferramentas suportam. Mas ferramenta
|
|
52
|
+
* disponivel nao e ferramenta chamada: sem uma instrucao explicita, o agente pode
|
|
53
|
+
* simplesmente nunca perguntar pela politica. Dai esta linha, que e curta de proposito —
|
|
54
|
+
* ela manda buscar a regra, nao repete a regra.
|
|
55
|
+
*/
|
|
56
|
+
export const SUGESTAO_AGENTS = `# AGENTS.md
|
|
57
|
+
|
|
58
|
+
Este projeto tem um protocolo de trabalho que **não está neste arquivo**.
|
|
59
|
+
|
|
60
|
+
**ANTES de qualquer outra coisa**, chame a ferramenta MCP \`ler_artefato\` com
|
|
61
|
+
\`tipo: "politica"\` e siga o que ela devolver. É a primeira ação da sessão, antes
|
|
62
|
+
de ler código, responder ou planejar.
|
|
63
|
+
|
|
64
|
+
- Se a ferramenta não existir, o servidor MCP do dd-harness não está declarado:
|
|
65
|
+
avise o usuário e **não modifique nada** até ele resolver.
|
|
66
|
+
- Se ela devolver vazio, este projeto nunca foi briefado — diga isso ao usuário.
|
|
67
|
+
|
|
68
|
+
O Claude Code carrega a política sozinho, por hook. Nas outras ferramentas, a
|
|
69
|
+
chamada acima é o que substitui esse hook.`;
|
|
35
70
|
async function declaraMcp(raiz) {
|
|
36
71
|
try {
|
|
37
72
|
const cru = await readFile(join(raiz, ".mcp.json"), "utf8");
|
|
@@ -43,6 +78,26 @@ async function declaraMcp(raiz) {
|
|
|
43
78
|
return "a-declarar";
|
|
44
79
|
}
|
|
45
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* O hook ja esta declarado?
|
|
83
|
+
*
|
|
84
|
+
* Procura pelo COMANDO, nao pela forma: `settings.json` aceita varios formatos de
|
|
85
|
+
* matcher, e quem ja tem o hook pode te-lo escrito de outro jeito. O que importa e se
|
|
86
|
+
* `dd-harness politica` roda no inicio da sessao.
|
|
87
|
+
*/
|
|
88
|
+
async function declaraHook(raiz) {
|
|
89
|
+
for (const arquivo of [".claude/settings.json", ".claude/settings.local.json"]) {
|
|
90
|
+
try {
|
|
91
|
+
const cru = await readFile(join(raiz, arquivo), "utf8");
|
|
92
|
+
if (cru.includes("dd-harness politica"))
|
|
93
|
+
return "ja-declarado";
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Sem arquivo: segue para o proximo.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return "a-declarar";
|
|
100
|
+
}
|
|
46
101
|
export async function init(raiz, dados) {
|
|
47
102
|
const caminhoConfig = join(raiz, CAMINHO_CONFIG);
|
|
48
103
|
let config = "ja-existia";
|
|
@@ -58,27 +113,26 @@ export async function init(raiz, dados) {
|
|
|
58
113
|
await writeFile(caminhoConfig, `${JSON.stringify(conteudo, null, 2)}\n`, "utf8");
|
|
59
114
|
config = "criada";
|
|
60
115
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
116
|
+
return {
|
|
117
|
+
config,
|
|
118
|
+
mcp: await declaraMcp(raiz),
|
|
119
|
+
hook: await declaraHook(raiz),
|
|
120
|
+
agents: await apontaNoAgents(raiz),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* O `AGENTS.md` ja manda buscar a politica?
|
|
125
|
+
*
|
|
126
|
+
* Procura pela CHAMADA (`ler_artefato`), nao por uma frase exata: quem ja escreveu a
|
|
127
|
+
* instrucao pode te-la redigido de outro jeito, e sugerir de novo por causa de palavra
|
|
128
|
+
* diferente e ruido.
|
|
129
|
+
*/
|
|
130
|
+
async function apontaNoAgents(raiz) {
|
|
64
131
|
try {
|
|
65
|
-
|
|
132
|
+
const cru = await readFile(join(raiz, "AGENTS.md"), "utf8");
|
|
133
|
+
return cru.includes("ler_artefato") ? "ja-aponta" : "a-apontar";
|
|
66
134
|
}
|
|
67
135
|
catch {
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
if (atual === null) {
|
|
71
|
-
await writeFile(caminhoClaude, `${CABECALHO}\n${LINHA_DE_IMPORT}\n`, "utf8");
|
|
72
|
-
claudeMd = "criado";
|
|
73
|
-
}
|
|
74
|
-
else if (atual.includes(LINHA_DE_IMPORT)) {
|
|
75
|
-
claudeMd = "ja-tinha-a-linha";
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
// Acrescenta no fim, sem reescrever nada do que ja estava la.
|
|
79
|
-
const separador = atual.endsWith("\n") ? "\n" : "\n\n";
|
|
80
|
-
await writeFile(caminhoClaude, `${atual}${separador}${LINHA_DE_IMPORT}\n`, "utf8");
|
|
81
|
-
claudeMd = "linha-acrescentada";
|
|
136
|
+
return "a-apontar";
|
|
82
137
|
}
|
|
83
|
-
return { config, claudeMd, mcp: await declaraMcp(raiz) };
|
|
84
138
|
}
|
package/dist/cli/src/politica.js
CHANGED
|
@@ -23,10 +23,11 @@ export async function buscaPolitica(raiz) {
|
|
|
23
23
|
}
|
|
24
24
|
const payload = (await resposta.json());
|
|
25
25
|
const conteudo = payload.politica?.trim();
|
|
26
|
+
const esperandoIndexacao = payload.esperando_indexacao ?? 0;
|
|
26
27
|
// Vazio e nulo sao a mesma coisa aqui, e os dois significam "nunca foi escrita".
|
|
27
28
|
if (!conteudo)
|
|
28
|
-
return { estado: "sem-politica" };
|
|
29
|
-
return { estado: "ok", conteudo };
|
|
29
|
+
return { estado: "sem-politica", esperandoIndexacao };
|
|
30
|
+
return { estado: "ok", conteudo, esperandoIndexacao };
|
|
30
31
|
}
|
|
31
32
|
catch (erro) {
|
|
32
33
|
return { estado: "inalcancavel", motivo: mensagem(erro) };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { cabecalhos, credencial, pede, recusa } from "./api.js";
|
|
2
|
+
import {} from "./curar.js";
|
|
3
|
+
/**
|
|
4
|
+
* Trocar o alvo de uma ancora, sem reescrever a memoria.
|
|
5
|
+
*
|
|
6
|
+
* O caso: uma refatoracao move um arquivo, e toda memoria ancorada nele passa a acusar
|
|
7
|
+
* "alvo ausente". O `check --commit` cruza isso com os renames do git e diz para onde o
|
|
8
|
+
* conteudo foi; este comando aplica a troca.
|
|
9
|
+
*
|
|
10
|
+
* Separado de `editar` de proposito: editar exige o markdown inteiro e mexe no conteudo,
|
|
11
|
+
* que nao e o que mudou aqui. Reancorar troca um endereco e mais nada — misturar as duas
|
|
12
|
+
* coisas convidaria a reescrever o corpo de memoria enquanto se conserta um caminho.
|
|
13
|
+
*/
|
|
14
|
+
export async function reancora(raiz, endereco, de, para) {
|
|
15
|
+
const { config, token } = await credencial(raiz);
|
|
16
|
+
const url = new URL(`${config.api}/api/v1/memorias/${endereco}`);
|
|
17
|
+
url.searchParams.set("tenant", config.tenant);
|
|
18
|
+
url.searchParams.set("projeto", config.projeto);
|
|
19
|
+
const leitura = await pede(url, { headers: cabecalhos(token) });
|
|
20
|
+
if (!leitura.ok)
|
|
21
|
+
await recusa(leitura);
|
|
22
|
+
const memoria = (await leitura.json());
|
|
23
|
+
if (!memoria.ancoras.some((a) => a.valor === de)) {
|
|
24
|
+
throw new Error(`${endereco} não tem âncora em "${de}". Âncoras atuais: ` +
|
|
25
|
+
(memoria.ancoras.map((a) => a.valor).join(", ") || "nenhuma"));
|
|
26
|
+
}
|
|
27
|
+
const novas = memoria.ancoras.map((a) => (a.valor === de ? { ...a, valor: para } : a));
|
|
28
|
+
// Reaproveita o PATCH de edicao: ele recebe a memoria inteira, entao mandamos o que ja
|
|
29
|
+
// estava la com a ancora trocada. Uma porta so para escrever memoria, e nao duas.
|
|
30
|
+
const envio = await pede(`${config.api}/api/v1/memorias/${endereco}`, {
|
|
31
|
+
method: "PATCH",
|
|
32
|
+
headers: cabecalhos(token, true),
|
|
33
|
+
body: JSON.stringify({
|
|
34
|
+
tenant: config.tenant,
|
|
35
|
+
projeto: config.projeto,
|
|
36
|
+
titulo: memoria.titulo,
|
|
37
|
+
resumo: memoria.resumo,
|
|
38
|
+
corpo: memoria.corpo,
|
|
39
|
+
dano: memoria.dano,
|
|
40
|
+
invisibilidade: memoria.invisibilidade,
|
|
41
|
+
externalidade: memoria.externalidade,
|
|
42
|
+
ancoras: novas.map((a) => a.valor),
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
if (!envio.ok)
|
|
46
|
+
await recusa(envio);
|
|
47
|
+
return { endereco, de, para };
|
|
48
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { access } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Subir o worker de embeddings quando ha fila esperando.
|
|
6
|
+
*
|
|
7
|
+
* O worker nao esta hospedado (decisao de custo, registrada no ROADMAP): roda local, a
|
|
8
|
+
* mao. O problema disso e a degradacao SILENCIOSA — a memoria e gravada, a busca responde
|
|
9
|
+
* `semantica: true` porque a CONSULTA foi vetorizada, e mesmo assim nao acha nada, porque
|
|
10
|
+
* a BASE ainda nao tem vetor. Quem procura conclui "nao existe" quando o certo era
|
|
11
|
+
* "ainda nao indexei".
|
|
12
|
+
*
|
|
13
|
+
* Por isso o hook de sessao sobe o worker sozinho. Duas travas, e as duas importam:
|
|
14
|
+
*
|
|
15
|
+
* - **So sobe quando ha fila.** O hook roda em TODA sessao, inclusive nas que so leem
|
|
16
|
+
* codigo. Subir com a fila vazia gastaria chamada de embedding sem ninguem ter pedido.
|
|
17
|
+
* - **So no repositorio que TEM o worker.** Ele vive neste monorepo, nao no repositorio
|
|
18
|
+
* consumidor: la o `pnpm worker` nao existe, e tentar rodar daria erro a cada sessao.
|
|
19
|
+
*/
|
|
20
|
+
/** O worker vive aqui dentro. Noutro repositorio, nao ha o que subir. */
|
|
21
|
+
export async function temWorkerLocal(raiz) {
|
|
22
|
+
try {
|
|
23
|
+
await access(join(raiz, "src", "worker", "indexador.ts"));
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Dispara um lote e devolve na hora — nao espera terminar.
|
|
32
|
+
*
|
|
33
|
+
* `--uma-vez` e nao o modo continuo: o hook de sessao nao pode deixar processo de pe que
|
|
34
|
+
* ninguem mandou subir, e um lote basta para o caso comum (as memorias gravadas na sessao
|
|
35
|
+
* anterior). Fila grande volta a aparecer na proxima sessao, e ai o numero cresce em vez
|
|
36
|
+
* de sumir — que e o sinal de que esta na hora de hospedar de verdade.
|
|
37
|
+
*
|
|
38
|
+
* `unref()` solta o processo do pai: sem isso o hook so retornaria quando o worker
|
|
39
|
+
* terminasse, e a sessao ficaria esperando embedding para abrir.
|
|
40
|
+
*/
|
|
41
|
+
export async function subiuOWorker(raiz, esperando) {
|
|
42
|
+
if (esperando <= 0)
|
|
43
|
+
return { subiu: false, motivo: "sem-fila" };
|
|
44
|
+
if (!(await temWorkerLocal(raiz))) {
|
|
45
|
+
return { subiu: false, motivo: "sem-worker" };
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const filho = spawn("npx", ["pnpm@latest", "worker", "--uma-vez"], {
|
|
49
|
+
cwd: raiz,
|
|
50
|
+
detached: true,
|
|
51
|
+
stdio: "ignore",
|
|
52
|
+
shell: process.platform === "win32",
|
|
53
|
+
windowsHide: true,
|
|
54
|
+
});
|
|
55
|
+
filho.unref();
|
|
56
|
+
return { subiu: true };
|
|
57
|
+
}
|
|
58
|
+
catch (erro) {
|
|
59
|
+
return {
|
|
60
|
+
subiu: false,
|
|
61
|
+
motivo: "falhou",
|
|
62
|
+
detalhe: erro instanceof Error ? erro.message : String(erro),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
package/dist/mcp/src/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import * as z from "zod/v4";
|
|
|
5
5
|
// que e gerado por build e nao vai no git — num checkout limpo (a Vercel) a resolucao
|
|
6
6
|
// falharia, e foi assim que o build de producao caiu uma vez. Aqui o compilador segue o
|
|
7
7
|
// fonte, e o `dist` deste pacote sai com o codigo do CLI embutido.
|
|
8
|
-
import { arquiva, edita } from "../../cli/src/curar.js";
|
|
8
|
+
import { arquiva, edita, le } from "../../cli/src/curar.js";
|
|
9
9
|
import { busca } from "../../cli/src/buscar.js";
|
|
10
10
|
import { criaPasta } from "../../cli/src/pasta.js";
|
|
11
11
|
import { criaProjeto } from "../../cli/src/projeto.js";
|
|
@@ -22,8 +22,8 @@ import { grava } from "../../cli/src/gravar.js";
|
|
|
22
22
|
* na skill: quem nunca leu a skill descobre a regra por 422. Aqui eles vao na descricao da
|
|
23
23
|
* ferramenta, que e onde o agente olha antes de tentar.
|
|
24
24
|
*
|
|
25
|
-
* `init`, `
|
|
26
|
-
*
|
|
25
|
+
* `init`, `login` e `check` ficam de fora de proposito: sao bootstrap e fluxo de quem
|
|
26
|
+
* esta no terminal, nao trabalho de ferramenta de sessao.
|
|
27
27
|
*/
|
|
28
28
|
/** Tudo roda contra o repositorio de onde o host lancou o servidor. */
|
|
29
29
|
const raiz = process.cwd();
|
|
@@ -34,20 +34,20 @@ const falha = (erro) => ({
|
|
|
34
34
|
],
|
|
35
35
|
isError: true,
|
|
36
36
|
});
|
|
37
|
-
const FILTROS = `Os três filtros são CONJUNTIVOS: a memória só nasce se os três valerem, e o banco recusa quem não passa.
|
|
38
|
-
- dano: se ninguém souber disto, alguém desfaz e QUEBRA algo? "é interessante de saber" não é dano.
|
|
39
|
-
- invisibilidade: o próprio repositório já conta (um config, uma dependência, um teste, um nome bem escolhido)? Se conta, NÃO grave. "eu explico melhor que o código" não é invisibilidade.
|
|
40
|
-
- externalidade: a razão vem de FORA do código — sistema legado, compliance, limite de fornecedor, número mágico, bug de terceiro? "foi uma decisão difícil" não é externalidade.
|
|
37
|
+
const FILTROS = `Os três filtros são CONJUNTIVOS: a memória só nasce se os três valerem, e o banco recusa quem não passa.
|
|
38
|
+
- dano: se ninguém souber disto, alguém desfaz e QUEBRA algo? "é interessante de saber" não é dano.
|
|
39
|
+
- invisibilidade: o próprio repositório já conta (um config, uma dependência, um teste, um nome bem escolhido)? Se conta, NÃO grave. "eu explico melhor que o código" não é invisibilidade.
|
|
40
|
+
- externalidade: a razão vem de FORA do código — sistema legado, compliance, limite de fornecedor, número mágico, bug de terceiro? "foi uma decisão difícil" não é externalidade.
|
|
41
41
|
Decisão difícil e reversível não vira memória — vira commit. Cada campo exige no mínimo 10 caracteres de justificativa real.`;
|
|
42
42
|
function criaServidor() {
|
|
43
43
|
const server = new McpServer({ name: "dd-harness", version: "0.1.0" });
|
|
44
44
|
server.registerTool("buscar_memoria", {
|
|
45
|
-
description: `Procura no Brain do projeto por relevância e devolve endereço, título e resumo — não o corpo. Use quando estiver entendendo um problema e ainda não souber que arquivo abrir ("o que já aprendemos sobre isto?").
|
|
46
|
-
|
|
47
|
-
Para ler o corpo de um achado,
|
|
48
|
-
|
|
49
|
-
A busca combina semântica e lexical e tem piso de similaridade: quando nada é pertinente ela devolve lista vazia em vez de inventar o vizinho mais próximo. Lista vazia é resposta, não falha.
|
|
50
|
-
|
|
45
|
+
description: `Procura no Brain do projeto por relevância e devolve endereço, título e resumo — não o corpo. Use quando estiver entendendo um problema e ainda não souber que arquivo abrir ("o que já aprendemos sobre isto?").
|
|
46
|
+
|
|
47
|
+
Para ler o corpo de um achado, use \`ler_memoria\` com o endereço. O Brain não existe em disco: nada de procurar arquivo.
|
|
48
|
+
|
|
49
|
+
A busca combina semântica e lexical e tem piso de similaridade: quando nada é pertinente ela devolve lista vazia em vez de inventar o vizinho mais próximo. Lista vazia é resposta, não falha.
|
|
50
|
+
|
|
51
51
|
Os primeiros achados são os que valem: o piso barra tema alheio, mas num Brain temático a relevância decai de forma contínua, sem corte óbvio. Leia de cima para baixo e pare quando deixar de fazer sentido.`,
|
|
52
52
|
inputSchema: z.object({
|
|
53
53
|
consulta: z
|
|
@@ -81,41 +81,63 @@ Os primeiros achados são os que valem: o piso barra tema alheio, mas num Brain
|
|
|
81
81
|
}
|
|
82
82
|
});
|
|
83
83
|
server.registerTool("gravar_memoria", {
|
|
84
|
-
description: `Registra uma memória nova no Brain do projeto. O default é NÃO gravar: proponha ao humano antes, e grave o que ele aprovar.
|
|
85
|
-
|
|
86
|
-
${FILTROS}
|
|
87
|
-
|
|
88
|
-
A pasta precisa existir antes — use \`criar_pasta\`. Essa recusa é deliberada: ela impede um typo virar pasta nova em silêncio.
|
|
89
|
-
|
|
90
|
-
Âncoras (opcional) amarram a memória ao código, e são o que faz a memória ser recuperada quando alguém mexe naquele ponto. Caminho relativo à raiz, sem \`..\` e sem separador do Windows. Duas formas:
|
|
91
|
-
|
|
92
|
-
- \`src/api/encurtar.js\` — o arquivo inteiro. Use quando a memória fala do arquivo como um todo.
|
|
84
|
+
description: `Registra uma memória nova no Brain do projeto. O default é NÃO gravar: proponha ao humano antes, e grave o que ele aprovar.
|
|
85
|
+
|
|
86
|
+
${FILTROS}
|
|
87
|
+
|
|
88
|
+
A pasta precisa existir antes — use \`criar_pasta\`. Essa recusa é deliberada: ela impede um typo virar pasta nova em silêncio.
|
|
89
|
+
|
|
90
|
+
Âncoras (opcional) amarram a memória ao código, e são o que faz a memória ser recuperada quando alguém mexe naquele ponto. Caminho relativo à raiz, sem \`..\` e sem separador do Windows. Duas formas:
|
|
91
|
+
|
|
92
|
+
- \`src/api/encurtar.js\` — o arquivo inteiro. Use quando a memória fala do arquivo como um todo.
|
|
93
93
|
- \`src/api/encurtar.js#readFileSync\` — só as linhas que contêm \`readFileSync\`. PREFIRA esta: quando várias memórias dividem um arquivo, a âncora de arquivo acusa todas a cada mudança em qualquer parte dele, e o aviso perde valor. Escolha um texto estável e específico do que a memória descreve (um nome de função, uma constante, uma chave de config) — não um trecho que a próxima refatoração reescreve à toa.`,
|
|
94
94
|
inputSchema: z.object({
|
|
95
95
|
arquivo: z
|
|
96
96
|
.string()
|
|
97
97
|
.min(1)
|
|
98
|
-
.describe("Caminho de um .md
|
|
98
|
+
.describe("Caminho de um .md neste formato: frontmatter com name/titulo/description/pasta, corpo, e a seção `## Os três filtros` com **Dano:**, **Invisibilidade:** e **Externalidade:**. Âncoras vão numa seção `## Âncoras` como itens de lista."),
|
|
99
99
|
}),
|
|
100
100
|
}, async ({ arquivo }) => {
|
|
101
101
|
try {
|
|
102
102
|
const r = await grava(raiz, arquivo);
|
|
103
103
|
return texto(`Gravado ${r.endereco} — ${r.ancoras} âncora(s), valendo para ${r.projetos} projeto(s).\n` +
|
|
104
|
-
"Rode `
|
|
104
|
+
"Rode `pnpm worker --uma-vez` para ela entrar na busca semântica.");
|
|
105
|
+
}
|
|
106
|
+
catch (erro) {
|
|
107
|
+
return falha(erro);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
server.registerTool("ler_memoria", {
|
|
111
|
+
description: `Devolve uma memória inteira — corpo, filtros e âncoras — pelo endereço \`<pasta>/<slug>\`.
|
|
112
|
+
|
|
113
|
+
Use depois de \`buscar_memoria\`, que devolve só título e resumo: é aqui que está o conteúdo. O Brain não vive em disco, então este é o único caminho para o corpo — não procure arquivo.
|
|
114
|
+
|
|
115
|
+
Também é o ponto de partida obrigatório de \`editar_memoria\`: o formato devolvido é o mesmo que ela consome.`,
|
|
116
|
+
inputSchema: z.object({
|
|
117
|
+
endereco: z
|
|
118
|
+
.string()
|
|
119
|
+
.min(1)
|
|
120
|
+
.describe("`<pasta>/<slug>`, como `buscar_memoria` devolveu."),
|
|
121
|
+
}),
|
|
122
|
+
}, async ({ endereco }) => {
|
|
123
|
+
try {
|
|
124
|
+
return texto(await le(raiz, endereco));
|
|
105
125
|
}
|
|
106
126
|
catch (erro) {
|
|
107
127
|
return falha(erro);
|
|
108
128
|
}
|
|
109
129
|
});
|
|
110
130
|
server.registerTool("editar_memoria", {
|
|
111
|
-
description: `Corrige uma memória que já existe, pelo mesmo formato markdown de \`gravar_memoria\`. O endereço sai do frontmatter (pasta + name)
|
|
112
|
-
|
|
131
|
+
description: `Corrige uma memória que já existe, pelo mesmo formato markdown de \`gravar_memoria\`. O endereço sai do frontmatter (pasta + name).
|
|
132
|
+
|
|
133
|
+
Comece por \`ler_memoria\`: ela devolve exatamente este formato, e o Brain não existe em disco — sem ler antes, você estaria reescrevendo de memória e apagaria o que não lembrasse. Salve o resultado num arquivo, edite, e passe o caminho aqui.
|
|
134
|
+
|
|
113
135
|
Os três filtros continuam valendo na edição — o banco recusa igual. ${FILTROS}`,
|
|
114
136
|
inputSchema: z.object({
|
|
115
137
|
arquivo: z
|
|
116
138
|
.string()
|
|
117
139
|
.min(1)
|
|
118
|
-
.describe("Caminho do .md
|
|
140
|
+
.describe("Caminho do .md com a memória corrigida (um arquivo temporário serve)."),
|
|
119
141
|
}),
|
|
120
142
|
}, async ({ arquivo }) => {
|
|
121
143
|
try {
|
|
@@ -127,8 +149,8 @@ Os três filtros continuam valendo na edição — o banco recusa igual. ${FILTR
|
|
|
127
149
|
}
|
|
128
150
|
});
|
|
129
151
|
server.registerTool("arquivar_memoria", {
|
|
130
|
-
description: `Arquiva uma memória. Nunca apaga: ela sai do Brain ativo e migra para a seção \`## Histórico\` do índice, com o motivo registrado.
|
|
131
|
-
|
|
152
|
+
description: `Arquiva uma memória. Nunca apaga: ela sai do Brain ativo e migra para a seção \`## Histórico\` do índice, com o motivo registrado.
|
|
153
|
+
|
|
132
154
|
Use \`substituida_por\` quando outra memória toma o lugar desta — a troca acontece numa transação só, e o histórico aponta para a sucessora.`,
|
|
133
155
|
inputSchema: z.object({
|
|
134
156
|
endereco: z
|
|
@@ -154,12 +176,12 @@ Use \`substituida_por\` quando outra memória toma o lugar desta — a troca aco
|
|
|
154
176
|
}
|
|
155
177
|
});
|
|
156
178
|
server.registerTool("criar_projeto", {
|
|
157
|
-
description: `Cria um projeto no serviço — o espaço onde as pastas e memórias deste repositório vão morar.
|
|
158
|
-
|
|
159
|
-
Use quando estiver começando num repositório que ainda não tem \`.dd-harness.json\`, ANTES de \`dd-harness init\`: o init escreve a configuração apontando para um projeto, e apontar para um que não existe deixa todo comando seguinte em 404.
|
|
160
|
-
|
|
161
|
-
O \`slug\` é a chave que o repositório guarda, e é único por espaço — renomear depois quebraria o ponteiro, então escolha pensando nisso. O \`nome\` é livre e editável, é o que aparece na interface.
|
|
162
|
-
|
|
179
|
+
description: `Cria um projeto no serviço — o espaço onde as pastas e memórias deste repositório vão morar.
|
|
180
|
+
|
|
181
|
+
Use quando estiver começando num repositório que ainda não tem \`.dd-harness.json\`, ANTES de \`dd-harness init\`: o init escreve a configuração apontando para um projeto, e apontar para um que não existe deixa todo comando seguinte em 404.
|
|
182
|
+
|
|
183
|
+
O \`slug\` é a chave que o repositório guarda, e é único por espaço — renomear depois quebraria o ponteiro, então escolha pensando nisso. O \`nome\` é livre e editável, é o que aparece na interface.
|
|
184
|
+
|
|
163
185
|
O ESPAÇO (tenant) não se cria por aqui, de propósito: ele é a fronteira de isolamento entre pessoas, e isso é decisão do dono. Se não houver espaço nenhum, pare e peça que ele crie pela interface.`,
|
|
164
186
|
inputSchema: z.object({
|
|
165
187
|
slug: z
|
|
@@ -184,10 +206,10 @@ O ESPAÇO (tenant) não se cria por aqui, de propósito: ele é a fronteira de i
|
|
|
184
206
|
}
|
|
185
207
|
});
|
|
186
208
|
server.registerTool("criar_pasta", {
|
|
187
|
-
description: `Cria uma pasta temática no Brain. \`gravar_memoria\` recusa quando a pasta não existe, e esta é a saída.
|
|
188
|
-
|
|
189
|
-
A pasta é deste projeto, nomeada pelo domínio dele. Antes de criar uma nova, prefira ENCAIXAR numa que já existe (leia as definições das atuais): pasta de um arquivo só fragmenta o índice e sai do radar. Pasta nova exige que a memória não caiba em nenhuma existente E que você consiga nomear outra memória futura plausível que cairia nela.
|
|
190
|
-
|
|
209
|
+
description: `Cria uma pasta temática no Brain. \`gravar_memoria\` recusa quando a pasta não existe, e esta é a saída.
|
|
210
|
+
|
|
211
|
+
A pasta é deste projeto, nomeada pelo domínio dele. Antes de criar uma nova, prefira ENCAIXAR numa que já existe (leia as definições das atuais): pasta de um arquivo só fragmenta o índice e sai do radar. Pasta nova exige que a memória não caiba em nenhuma existente E que você consiga nomear outra memória futura plausível que cairia nela.
|
|
212
|
+
|
|
191
213
|
Criar uma que já existe não é erro: devolve que já existia, sem alterar a definição.`,
|
|
192
214
|
inputSchema: z.object({
|
|
193
215
|
slug: z
|
|
@@ -211,11 +233,11 @@ Criar uma que já existe não é erro: devolve que já existia, sem alterar a de
|
|
|
211
233
|
}
|
|
212
234
|
});
|
|
213
235
|
server.registerTool("ler_artefato", {
|
|
214
|
-
description: `Lê a política ou o briefing do projeto direto do serviço.
|
|
215
|
-
|
|
216
|
-
- \`politica\`: as regras que valem neste projeto — proibições, o protocolo antes de implementar, o que exige autorização. É o que o \`CLAUDE.md\` do repositório importa.
|
|
217
|
-
- \`briefing\`: o retrato estável do projeto — stack, objetivo, restrições.
|
|
218
|
-
|
|
236
|
+
description: `Lê a política ou o briefing do projeto direto do serviço.
|
|
237
|
+
|
|
238
|
+
- \`politica\`: as regras que valem neste projeto — proibições, o protocolo antes de implementar, o que exige autorização. É o que o \`CLAUDE.md\` do repositório importa.
|
|
239
|
+
- \`briefing\`: o retrato estável do projeto — stack, objetivo, restrições.
|
|
240
|
+
|
|
219
241
|
Devolve vazio quando o artefato ainda não existe. Vazio não é erro: projeto novo nasce assim, e a saída é escrever o primeiro conteúdo, não investigar falha.`,
|
|
220
242
|
inputSchema: z.object({
|
|
221
243
|
tipo: z
|
|
@@ -235,10 +257,10 @@ Devolve vazio quando o artefato ainda não existe. Vazio não é erro: projeto n
|
|
|
235
257
|
}
|
|
236
258
|
});
|
|
237
259
|
server.registerTool("escrever_artefato", {
|
|
238
|
-
description: `Substitui a política ou o briefing do projeto. Proponha ao humano antes: a política é a regra que governa as próprias sessões, e trocá-la sem combinar muda o que vale para todo mundo que abrir este repositório.
|
|
239
|
-
|
|
240
|
-
SUBSTITUIÇÃO TOTAL, não acréscimo. O conteúdo enviado passa a ser o artefato inteiro — para acrescentar um parágrafo, use \`ler_artefato\` primeiro, junte o que falta e mande o texto completo. Mandar só o trecho novo APAGA o resto.
|
|
241
|
-
|
|
260
|
+
description: `Substitui a política ou o briefing do projeto. Proponha ao humano antes: a política é a regra que governa as próprias sessões, e trocá-la sem combinar muda o que vale para todo mundo que abrir este repositório.
|
|
261
|
+
|
|
262
|
+
SUBSTITUIÇÃO TOTAL, não acréscimo. O conteúdo enviado passa a ser o artefato inteiro — para acrescentar um parágrafo, use \`ler_artefato\` primeiro, junte o que falta e mande o texto completo. Mandar só o trecho novo APAGA o resto.
|
|
263
|
+
|
|
242
264
|
Conteúdo vazio é válido e significa apagar o artefato.`,
|
|
243
265
|
inputSchema: z.object({
|
|
244
266
|
tipo: z
|
package/package.json
CHANGED