dd-harness-mcp 0.1.2 → 0.2.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.
@@ -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 {};
@@ -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
- * Curadoria pelo agente: editar e arquivar.
5
+ * A memoria inteira, no markdown que `gravar` e `editar` consomem.
8
6
  *
9
- * `gravar` sabia criar e mais nada. Uma memoria errada ficava errada, porque corrigir
10
- * exigia abrir a interface e o `CLAUDE.md` trata curadoria como obrigacao ("se
11
- * encontrar uma memoria obsoleta ou errada, corrija").
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 marcaComoEnviado(raiz, caminho, conteudo) {
27
- const chave = relative(raiz, caminho).split("\\").join("/");
28
- const manifesto = await leManifesto(raiz);
29
- // Arquivo de fora do repositorio (um rascunho em /tmp, por exemplo) nao esta no
30
- // manifesto e nao deve entrar: o manifesto descreve o que o `sync` materializou.
31
- if (!manifesto.arquivos[chave])
32
- return;
33
- manifesto.arquivos[chave] = hashDe(conteudo);
34
- await gravaManifesto(raiz, manifesto);
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) {
@@ -3,14 +3,12 @@ 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_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 { sync } from "./sync.js";
14
12
  /**
15
13
  * `dd-harness` — o cliente que materializa os artefatos no repositorio.
16
14
  *
@@ -19,80 +17,51 @@ import { sync } from "./sync.js";
19
17
  * em repositorio alheio tem que envelhecer bem, e cada dependencia e uma chance de nao
20
18
  * envelhecer.
21
19
  */
22
- const AJUDA = `dd-harness — materializa política, briefing e Brain no repositório
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 sync escreve os artefatos em disco
39
- dd-harness check [--commit <sha>] mede as âncoras e reporta a deriva
40
- dd-harness status só lê: o tamanho do Brain e o que espera julgamento
41
- dd-harness politica imprime a política do serviço (para o hook de sessão)
42
- saída 0 = veio; 3 = projeto sem política;
43
- 1 = não consegui buscar
44
- dd-harness --help
45
-
46
- O gerado vive em dd-harness/. Na raiz fica só o seu CLAUDE.md, que importa a
47
- política com a linha ${LINHA_DE_IMPORT}
20
+ const AJUDA = `dd-harness — a política e o Brain do projeto, no serviço
21
+
22
+ dd-harness login --token <token> [--api <url>]
23
+ guarda a credencial desta máquina
24
+ dd-harness projeto <slug> --nome "<nome>" [--tenant <t>] [--api <url>]
25
+ cria o projeto no serviço (antes do init)
26
+ dd-harness init --tenant <t> --projeto <p> [--api <url>]
27
+ prepara o repositório (config + CLAUDE.md)
28
+ dd-harness pasta <slug> --definicao "o que entra e o que não entra"
29
+ cria a pasta que o gravar exige
30
+ dd-harness gravar <arquivo.md> registra uma memória a partir de um markdown
31
+ dd-harness editar <arquivo.md> corrige o que já está gravado
32
+ dd-harness arquivar <pasta>/<slug> --motivo <obsoleta|incorreta|fora_dos_filtros>
33
+ [--substituida-por <pasta>/<slug>]
34
+ tira de circulação sem apagar
35
+ dd-harness buscar "<pergunta>" acha memória por relevância, não por arquivo
36
+ dd-harness ler <pasta>/<slug> imprime a memória inteira, no formato de gravar
37
+ dd-harness check [--commit <sha>] mede as âncoras e reporta a deriva
38
+ dd-harness status só lê: o tamanho do Brain e o que espera julgamento
39
+ dd-harness politica [--hook] imprime a política do serviço
40
+ saída 0 = veio; 3 = projeto sem política;
41
+ 1 = não consegui buscar
42
+ --hook: fala o protocolo do SessionStart do
43
+ Claude Code, para pôr a política no contexto
44
+ dd-harness --help
45
+
46
+ Nada do dd-harness fica em disco: a política chega pelo hook de sessão, e a
47
+ memória pela busca, na hora.
48
48
  `;
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
49
  /**
80
50
  * Os ganchos sao IMPRESSOS, nunca instalados. `.git/hooks` nao e versionado e nao e
81
51
  * nosso: escrever la dentro sem a pessoa pedir e o mesmo tipo de invasao que
82
52
  * sobrescrever o CLAUDE.md dela. Quem cola, decide.
83
53
  */
84
- const GANCHOS = `
85
- Opcional — dois ganchos que valem a pena:
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
- .claude/settings.json (na abertura da sessão, o que espera julgamento)
92
- "hooks": { "SessionStart": [{ "hooks": [{ "type": "command",
93
- "command": "dd-harness status" }] }] }
94
-
95
- Os dois terminam em sucesso mesmo com deriva: avisam, não bloqueiam.`;
54
+ const GANCHOS = `
55
+ Opcional — o gancho que devolve a memória ao code review:
56
+
57
+ .git/hooks/post-commit (avisa quais memórias falam do que você mudou)
58
+ #!/bin/sh
59
+ dd-harness check --commit "$(git rev-parse HEAD)" || true
60
+
61
+ Termina em sucesso mesmo com deriva: avisa, não bloqueia.
62
+
63
+ O hook da política (\`dd-harness politica --hook\`) é outra coisa, e não é
64
+ opcional — \`dd-harness init\` imprime a linha para o \`.claude/settings.json\`.`;
96
65
  function argumento(argv, nome) {
97
66
  const i = argv.indexOf(`--${nome}`);
98
67
  return i >= 0 ? argv[i + 1] : undefined;
@@ -109,12 +78,19 @@ async function comandoInit(argv) {
109
78
  api: argumento(argv, "api"),
110
79
  });
111
80
  console.log(r.config === "criada" ? "criado .dd-harness.json" : "mantido .dd-harness.json");
112
- console.log({
113
- criado: "criado CLAUDE.md com a linha de import",
114
- "linha-acrescentada": "ajustado CLAUDE.md linha de import acrescentada ao seu",
115
- "ja-tinha-a-linha": "mantido CLAUDE.md — já importava a política",
116
- }[r.claudeMd]);
117
- console.log("\nAgora: dd-harness login --token <token> && dd-harness sync");
81
+ console.log("\nAgora: dd-harness login --token <token>");
82
+ // O hook vem primeiro e nao e opcional: sem ele a sessao abre sem politica, que e a
83
+ // falha que este projeto existe para combater. O MCP e conveniencia; este, nao.
84
+ if (r.hook === "ja-declarado") {
85
+ console.log("\nmantido .claude/settings.json — o hook da política já está declarado");
86
+ }
87
+ else {
88
+ console.log("\nOBRIGATÓRIO: o hook que carrega a política no início de cada sessão." +
89
+ "\nSem ele a sessão abre sem protocolo, e nada avisa. Acrescente ao" +
90
+ "\n`.claude/settings.json` (não escrevo nele: o arquivo é seu e pode já" +
91
+ "\nter hooks e permissões):\n");
92
+ console.log(SUGESTAO_HOOK);
93
+ }
118
94
  if (r.mcp === "ja-declarado") {
119
95
  console.log("\nmantido .mcp.json — o servidor dd-harness já está declarado");
120
96
  return;
@@ -147,7 +123,7 @@ async function comandoEditar(argv) {
147
123
  }
148
124
  const r = await edita(process.cwd(), caminho);
149
125
  console.log(`editado ${r.endereco}`);
150
- console.log(` ${r.ancoras} âncora(s). Rode \`dd-harness sync\` para materializar.`);
126
+ console.log(` ${r.ancoras} âncora(s).`);
151
127
  }
152
128
  const MOTIVOS = ["obsoleta", "incorreta", "fora_dos_filtros"];
153
129
  async function comandoArquivar(argv) {
@@ -166,7 +142,20 @@ async function comandoArquivar(argv) {
166
142
  substituidaPor: argumento(argv, "substituida-por"),
167
143
  });
168
144
  console.log(`arquivado ${r.endereco} (${r.motivo})`);
169
- console.log(" foi para o histórico, não foi apagada. Rode `dd-harness sync`.");
145
+ console.log(" foi para o histórico, não foi apagada.");
146
+ }
147
+ /**
148
+ * A memoria inteira no stdout, no mesmo markdown que `gravar` e `editar` consomem.
149
+ *
150
+ * Sem materializacao este e o unico caminho para o corpo: a busca devolve so endereco,
151
+ * titulo e resumo. Tambem e o ponto de partida de toda edicao — corrigir exige ver.
152
+ */
153
+ async function comandoLer(argv) {
154
+ const endereco = argv[0];
155
+ if (!endereco || endereco.startsWith("-")) {
156
+ throw new Error("uso: dd-harness ler <pasta>/<slug>");
157
+ }
158
+ console.log(await le(process.cwd(), endereco));
170
159
  }
171
160
  async function comandoBuscar(argv) {
172
161
  const consulta = termosDaConsulta(argv);
@@ -191,36 +180,6 @@ async function comandoBuscar(argv) {
191
180
  console.log(` ${a.resumo}`);
192
181
  }
193
182
  }
194
- async function comandoSync() {
195
- const resultado = await sync(process.cwd());
196
- if (resultado.tipo === "editado-a-mao") {
197
- console.error([
198
- "parei sem escrever nada: estes arquivos foram editados à mão.",
199
- ...resultado.arquivos.map((a) => ` ${a}`),
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;
208
- return;
209
- }
210
- if (resultado.tipo === "sem-mudanca") {
211
- console.log("nada mudou no serviço — disco já está em dia.");
212
- }
213
- else {
214
- for (const a of resultado.escritos)
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);
223
- }
224
183
  async function comandoCheck(argv) {
225
184
  const r = await check(process.cwd(), argumento(argv, "commit"));
226
185
  if (r.medidas === 0) {
@@ -273,8 +232,16 @@ async function comandoCheck(argv) {
273
232
  * um "falhou" generico colapsaria: projeto novo (que precisa de briefing) de politica
274
233
  * inalcancavel (que precisa PARAR a sessao). Mudar estes numeros quebra o hook.
275
234
  */
276
- async function comandoPolitica() {
235
+ async function comandoPolitica(argv) {
277
236
  const r = await buscaPolitica(process.cwd());
237
+ // `--hook`: fala o protocolo do SessionStart do Claude Code, que injeta
238
+ // `additionalContext` no contexto da sessao. Sem a flag, saida legivel para quem roda
239
+ // no terminal. A diferenca importa: o hook precisa que o AVISO chegue ao modelo, e
240
+ // stderr so chega ao transcript — aviso que o modelo nao le e o mesmo que silencio.
241
+ if (argv.includes("--hook")) {
242
+ console.log(JSON.stringify({ hookSpecificOutput: contextoDaSessao(r) }));
243
+ return;
244
+ }
278
245
  if (r.estado === "ok") {
279
246
  console.log(r.conteudo);
280
247
  return;
@@ -287,6 +254,42 @@ async function comandoPolitica() {
287
254
  console.error(`não consegui buscar a política: ${r.motivo}`);
288
255
  process.exitCode = 1;
289
256
  }
257
+ /**
258
+ * O que o hook injeta no contexto, por estado.
259
+ *
260
+ * Sai sempre com codigo 0: o que precisa chegar ao modelo e o TEXTO, e um codigo de erro
261
+ * so faria o Claude Code registrar falha no transcript — que ninguem le — enquanto a
262
+ * sessao seguiria sem saber que esta sem protocolo.
263
+ */
264
+ function contextoDaSessao(r) {
265
+ const base = { hookEventName: "SessionStart" };
266
+ if (r.estado === "ok") {
267
+ return {
268
+ ...base,
269
+ additionalContext: "# Política deste projeto (carregada do dd-harness)\n\n" +
270
+ "As regras abaixo valem para esta sessão inteira.\n\n" +
271
+ r.conteudo,
272
+ };
273
+ }
274
+ if (r.estado === "sem-politica") {
275
+ return {
276
+ ...base,
277
+ additionalContext: "AVISO DO DD-HARNESS: este projeto existe no serviço mas **nunca foi briefado** " +
278
+ "— não há política.\n\nIsto não é uma falha: é um projeto novo. Antes de " +
279
+ "implementar qualquer coisa, diga isso ao usuário e proponha rodar `/briefar`.",
280
+ };
281
+ }
282
+ return {
283
+ ...base,
284
+ additionalContext: "PARE: NÃO FOI POSSÍVEL CARREGAR A POLÍTICA DESTE PROJETO.\n\n" +
285
+ `Motivo: ${r.motivo}\n\n` +
286
+ "A política pode existir no serviço e não ter chegado até aqui, então esta sessão " +
287
+ "está **sem protocolo** — as proibições e a regra do OK não foram carregadas.\n\n" +
288
+ "Antes de qualquer outra coisa: avise o usuário com estas palavras e **não " +
289
+ "modifique nenhum arquivo** até ele decidir como prosseguir. Seguir como se nada " +
290
+ "tivesse acontecido é exatamente a falha que este projeto combate.",
291
+ };
292
+ }
290
293
  async function comandoStatus() {
291
294
  const r = await status(process.cwd());
292
295
  // O acervo vem primeiro, e sempre: e a resposta para "o que ha no Brain deste projeto?",
@@ -361,9 +364,8 @@ async function comandoPasta(argv) {
361
364
  }
362
365
  /**
363
366
  * 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 nao fica no repositorio: quem
365
- * materializa e o `sync`, a partir do servico, para nao existir copia escrita a mao ao
366
- * lado da copia gerada.
367
+ * comando o transforma em requisicao. O arquivo e so o veiculo: depois de gravado, a
368
+ * memoria vive no servico, e quem quiser le-la usa a busca. Nada fica em disco.
367
369
  */
368
370
  async function comandoGravar(argv) {
369
371
  const caminho = argv[0];
@@ -372,8 +374,7 @@ async function comandoGravar(argv) {
372
374
  }
373
375
  const r = await grava(process.cwd(), caminho);
374
376
  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.");
377
+ console.log(` ${r.ancoras} âncora(s), valendo para ${r.projetos} projeto(s).`);
377
378
  }
378
379
  async function principal() {
379
380
  const [comando, ...resto] = process.argv.slice(2);
@@ -392,16 +393,16 @@ async function principal() {
392
393
  return comandoEditar(resto);
393
394
  case "arquivar":
394
395
  return comandoArquivar(resto);
396
+ case "ler":
397
+ return comandoLer(resto);
395
398
  case "buscar":
396
399
  return comandoBuscar(resto);
397
- case "sync":
398
- return comandoSync();
399
400
  case "check":
400
401
  return comandoCheck(resto);
401
402
  case "status":
402
403
  return comandoStatus();
403
404
  case "politica":
404
- return comandoPolitica();
405
+ return comandoPolitica(resto);
405
406
  case "--help":
406
407
  case "-h":
407
408
  case undefined:
@@ -1,36 +1,45 @@
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
- * Prepara um repositorio para o dd-harness.
5
+ * O `.mcp.json` e sugerido, nunca escrito.
7
6
  *
8
- * Funciona nos dois casos, e o segundo e o que importa para a ambicao de adotar projeto
9
- * que ja existe: se nao ha `CLAUDE.md`, cria um com a linha de import; se **ja ha**,
10
- * acrescenta a linha ao que voce escreveu, sem tocar no resto. Adotar um projeto vira
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 CABECALHO = `# CLAUDE.md
14
-
15
- Este arquivo é seu: escreva aqui o que for específico deste repositório.
16
-
17
- A linha abaixo importa a política gerenciada pelo dd-harness. **Não a remova** — sem
18
- ela a sessão abre sem protocolo, e nada avisa.
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 `.mcp.json` e sugerido, nunca escrito.
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
- * Pelo mesmo motivo do `CLAUDE.md` da raiz: o arquivo e do repositorio, pode ja declarar
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 SUGESTAO_MCP = `{
28
- "mcpServers": {
29
- "dd-harness": {
30
- "command": "npx",
31
- "args": ["-y", "dd-harness-mcp"]
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
  }`;
35
44
  async function declaraMcp(raiz) {
36
45
  try {
@@ -43,6 +52,26 @@ async function declaraMcp(raiz) {
43
52
  return "a-declarar";
44
53
  }
45
54
  }
55
+ /**
56
+ * O hook ja esta declarado?
57
+ *
58
+ * Procura pelo COMANDO, nao pela forma: `settings.json` aceita varios formatos de
59
+ * matcher, e quem ja tem o hook pode te-lo escrito de outro jeito. O que importa e se
60
+ * `dd-harness politica` roda no inicio da sessao.
61
+ */
62
+ async function declaraHook(raiz) {
63
+ for (const arquivo of [".claude/settings.json", ".claude/settings.local.json"]) {
64
+ try {
65
+ const cru = await readFile(join(raiz, arquivo), "utf8");
66
+ if (cru.includes("dd-harness politica"))
67
+ return "ja-declarado";
68
+ }
69
+ catch {
70
+ // Sem arquivo: segue para o proximo.
71
+ }
72
+ }
73
+ return "a-declarar";
74
+ }
46
75
  export async function init(raiz, dados) {
47
76
  const caminhoConfig = join(raiz, CAMINHO_CONFIG);
48
77
  let config = "ja-existia";
@@ -58,27 +87,9 @@ export async function init(raiz, dados) {
58
87
  await writeFile(caminhoConfig, `${JSON.stringify(conteudo, null, 2)}\n`, "utf8");
59
88
  config = "criada";
60
89
  }
61
- const caminhoClaude = join(raiz, "CLAUDE.md");
62
- let claudeMd;
63
- let atual = null;
64
- try {
65
- atual = await readFile(caminhoClaude, "utf8");
66
- }
67
- catch {
68
- atual = null;
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";
82
- }
83
- return { config, claudeMd, mcp: await declaraMcp(raiz) };
90
+ return {
91
+ config,
92
+ mcp: await declaraMcp(raiz),
93
+ hook: await declaraHook(raiz),
94
+ };
84
95
  }
@@ -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`, `sync`, `login` e `check` ficam de fora de proposito: sao bootstrap e fluxo de
26
- * quem esta no terminal, e materializar arquivo nao e trabalho de ferramenta de sessao.
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, abra o arquivo que o \`dd-harness sync\` materializou em \`dd-harness/brain/<endereço>.md\`.
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 no formato que o sync materializa: 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."),
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 `dd-harness sync` para materializar, e `pnpm worker --uma-vez` para entrar na busca semântica.");
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), então edite o arquivo que o \`sync\` materializou.
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 da memória, normalmente `dd-harness/brain/<pasta>/<slug>.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-harness-mcp",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Servidor MCP do dd-harness: o agente consulta e grava memoria como ferramenta, sem passar por arquivo.",
6
6
  "license": "UNLICENSED",